diff options
Diffstat (limited to 'esheep.py')
| -rw-r--r-- | esheep.py | 127 |
1 files changed, 76 insertions, 51 deletions
@@ -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__': |
