summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--esheep.py127
1 files changed, 76 insertions, 51 deletions
diff --git a/esheep.py b/esheep.py
index 29e03d2..5bf2328 100644
--- a/esheep.py
+++ b/esheep.py
@@ -627,58 +627,44 @@ class UFO:
# ─────────────────────── Flower ───────────────────────
class Flower:
- """Procedural flower that appears at a sheep's feet during eat animation (26)."""
+ """Flower sprite (anim 27) shown at the sheep's side during eat animation (26).
+ Loops anim 27 for the full duration the sheep stays in anim 26."""
- COLORS = [(1.0, 0.9, 0.15), (1.0, 0.4, 0.65), (0.75, 0.5, 1.0), (0.4, 0.9, 0.4)]
- LIFESPAN_MS = 2400.0
-
- def __init__(self, sheep: 'Sheep'):
- self.sheep = sheep
- self._color = random.choice(self.COLORS)
- self._age = 0.0
- self.done = False
+ def __init__(self, sheep: 'Sheep', flower_adef):
+ self.sheep = sheep
+ self._adef = flower_adef # AnimDef for anim 27, may be None
+ self._frame_idx = 0
+ self._tick_accum = 0.0
+ self.done = False
def tick(self, dt_ms: float):
if self.sheep.anim_id != 26:
self.done = True
return
- self._age += dt_ms
- if self._age >= self.LIFESPAN_MS:
- self.done = True
-
- def draw(self, cr: cairo.Context):
- if self.done:
+ if not self._adef or not self._adef.frames:
return
- sheep = self.sheep
- progress = self._age / self.LIFESPAN_MS # 0→1
- n_petals = max(0, round(5 * (1.0 - progress))) # 5 petals → 0 as eaten
- alpha = 1.0 - max(0.0, (progress - 0.85) / 0.15)
-
- # Position: in front of the sheep at ground level
- fx = sheep.x + (sheep.tile_w * 0.15 if not sheep.facing_right
- else sheep.tile_w * 0.75)
- fy = sheep.y + sheep.tile_h - 2
-
- cr.save()
- # Stem
- cr.set_source_rgba(0.2, 0.65, 0.15, alpha)
- cr.set_line_width(1.5)
- cr.move_to(fx, fy)
- cr.line_to(fx, fy - 10)
- cr.stroke()
- # Petals
- cr.set_source_rgba(*self._color, alpha)
- for i in range(n_petals):
- ang = i * (2 * math.pi / 5) - math.pi / 2
- cr.arc(fx + math.cos(ang) * 4.5, fy - 10 + math.sin(ang) * 4.5,
- 3.0, 0, 2 * math.pi)
- cr.fill()
- # Centre
- if n_petals > 0:
- cr.set_source_rgba(1.0, 0.55, 0.1, alpha)
- cr.arc(fx, fy - 10, 2.5, 0, 2 * math.pi)
- cr.fill()
- cr.restore()
+ interval = self._adef.sint
+ self._tick_accum += dt_ms
+ while self._tick_accum >= interval:
+ self._tick_accum -= interval
+ self._frame_idx += 1
+ if self._frame_idx >= len(self._adef.frames):
+ self._frame_idx = 0 # loop while sheep keeps eating
+
+ def draw(self, cr: cairo.Context,
+ surface: cairo.ImageSurface,
+ tile_w: int, tile_h: int, tiles_x: int):
+ if self.done or not self._adef or not self._adef.frames:
+ return
+ sheep = self.sheep
+ fi = self._adef.frames[min(self._frame_idx, len(self._adef.frames) - 1)]
+ # Place the flower sprite in front of the sheep at the same ground level
+ if not sheep.facing_right:
+ fx = sheep.x - tile_w * 0.5
+ else:
+ fx = sheep.x + tile_w * 0.75
+ fy = sheep.y
+ draw_frame(cr, surface, tile_w, tile_h, tiles_x, fi, fx, fy)
# ─────────────────────── Cairo sprite drawing ───────────────────────
@@ -727,7 +713,11 @@ class SheepOverlay:
self.cairo_surface = self._pil_to_cairo(self.sprite)
- # Patch fall-hard (10) to occasionally boing on landing — 20% chance
+ # Patch fall-soft (9) and fall-hard (10) to occasionally boing on landing
+ soft = self.anim_defs.get(9)
+ if soft:
+ soft.seq_nexts = [NextRef(probability=15, only='', anim_id=8),
+ NextRef(probability=85, only='', anim_id=1)]
hard = self.anim_defs.get(10)
if hard:
hard.seq_nexts = [NextRef(probability=20, only='', anim_id=8),
@@ -747,6 +737,7 @@ class SheepOverlay:
# Extra features
self._flowers: List[Flower] = []
self._eat_countdown = random.uniform(30, 90) # seconds
+ self._hop_countdown = random.uniform(45, 90) # seconds
self._interact_cooldowns: Dict[Tuple[int,int], float] = {} # ms
self._frozen_sheep: Dict[int, float] = {} # id→remaining ms
@@ -1011,12 +1002,27 @@ class SheepOverlay:
if candidates:
chosen = random.choice(candidates)
chosen._init_anim(26)
- self._flowers.append(Flower(chosen))
+ self._flowers.append(Flower(chosen, self.anim_defs.get(27)))
print("Sheep is eating flowers!", flush=True)
for f in self._flowers:
f.tick(dt)
self._flowers = [f for f in self._flowers if not f.done]
+ # ── small hop (simulates walking off a low ledge → soft landing → occasional boing) ──
+ self._hop_countdown -= dt / 1000.0
+ if self._hop_countdown <= 0:
+ self._hop_countdown = random.uniform(45, 90)
+ hop_candidates = [s for s in self.sheep_list
+ if s is not self._drag_sheep
+ and s.floor_cond == COND_NONE
+ and s.anim_id in (1, 7, 35, 36, 49, 50)
+ and not any(u.sheep is s for u in self.ufos)]
+ if hop_candidates:
+ chosen = random.choice(hop_candidates)
+ chosen.y -= 25
+ chosen._init_anim(5)
+ print("Sheep small hop!", flush=True)
+
# ── bumps & greetings ──
self._check_interactions(dt)
@@ -1049,7 +1055,7 @@ class SheepOverlay:
# Draw flowers (behind sheep)
for flower in self._flowers:
- flower.draw(cr)
+ flower.draw(cr, self.cairo_surface, self.tile_w, self.tile_h, self.tiles_x)
# Draw sheep
for sheep in self.sheep_list:
@@ -1069,13 +1075,29 @@ class SheepOverlay:
# ─────────────────────── entry point ───────────────────────
+def _daemonize():
+ """Re-exec as a detached subprocess (avoids fork-in-multithreaded-process issues)."""
+ import subprocess
+ argv = [a for a in sys.argv if a != '--daemon']
+ subprocess.Popen(
+ [sys.executable] + argv,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ start_new_session=True,
+ )
+ sys.exit(0)
+
+
def main():
import argparse, signal, subprocess
p = argparse.ArgumentParser(description='eSheep for Wayland')
- p.add_argument('--xml', default=os.path.join(os.path.dirname(__file__), 'animations.xml'),
+ p.add_argument('--xml', default=os.path.join(os.path.dirname(__file__), 'animations.xml'),
help='Path to animations.xml')
- p.add_argument('--count', type=int, default=1, help='Number of sheep')
- p.add_argument('--exit', action='store_true',
+ p.add_argument('--count', type=int, default=1, help='Number of sheep')
+ p.add_argument('--daemon', action='store_true',
+ help='Detach from terminal and run in the background')
+ p.add_argument('--exit', action='store_true',
help='Kill any running eSheep instances and quit')
args = p.parse_args()
@@ -1100,6 +1122,9 @@ def main():
print(f"animations.xml not found: {args.xml}", file=sys.stderr)
sys.exit(1)
+ if args.daemon:
+ _daemonize()
+
SheepOverlay(args.xml, args.count).run()
if __name__ == '__main__':