diff options
| author | Ken D'Ambrosio <ken.dambrosio@constantcontact.com> | 2026-06-03 21:50:28 -0400 |
|---|---|---|
| committer | Ken D'Ambrosio <ken.dambrosio@constantcontact.com> | 2026-06-03 21:50:28 -0400 |
| commit | 7a48d2a3cb7d3969be5e3e00b64a51c1da0550ee (patch) | |
| tree | 430027d4f01d173644bcfbd3afa01e44b0e27f5e /esheep.py | |
| parent | ef25374771e78d503d14446ed9d637306c0eb224 (diff) | |
Add flower-chomping, greetings, black sheep, and bumping
- Flowers: every 60-120s a sheep stops to eat a daisy (procedurally
drawn with petals disappearing as it chomps, using anim 26)
- Greetings: two sheep approaching each other freeze and face one
another for ~2s before going their separate ways
- Bumping: overlapping sheep boing off each other and reverse direction
- Black sheep: a near-black-tinted visitor wanders in from a random
edge every 3-6 minutes, does the full blacksheep sequence (anims
28-34) and fades out; UFOs won't abduct it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'esheep.py')
| -rw-r--r-- | esheep.py | 202 |
1 files changed, 194 insertions, 8 deletions
@@ -624,6 +624,63 @@ class UFO: cr.restore() +# ─────────────────────── Flower ─────────────────────── + +class Flower: + """Procedural flower that appears at a sheep's feet during eat animation (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 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: + 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() + + # ─────────────────────── Cairo sprite drawing ─────────────────────── def draw_frame(cr: cairo.Context, surface: cairo.ImageSurface, @@ -669,6 +726,7 @@ class SheepOverlay: f"animations: {len(self.anim_defs)}", flush=True) self.cairo_surface = self._pil_to_cairo(self.sprite) + self.dark_surface = self._make_dark_surface(self.sprite) self.wayfire = WayfireWindows() self.sheep_list: List[Sheep] = [] self.ufos: List[UFO] = [] @@ -678,9 +736,17 @@ class SheepOverlay: # Drag state self._drag_sheep: Optional[Sheep] = None - self._drag_ox = 0.0 # mouse_x - sheep.x at press time + self._drag_ox = 0.0 self._drag_oy = 0.0 + # Extra features + self._flowers: List[Flower] = [] + self._black_sheep_set: set = set() + self._black_sheep_countdown = random.uniform(180, 300) # seconds + self._eat_countdown = random.uniform(30, 90) # seconds + self._interact_cooldowns: Dict[Tuple[int,int], float] = {} # ms + self._frozen_sheep: Dict[int, float] = {} # id→remaining ms + self._build_window() @staticmethod @@ -702,6 +768,15 @@ class SheepOverlay: surf.mark_dirty() return surf + @staticmethod + def _make_dark_surface(img: 'Image.Image') -> cairo.ImageSurface: + """Near-black tinted sprite for the black sheep visitor.""" + import numpy as np + arr = np.array(img, dtype=np.float32) + arr[:, :, :3] *= 0.18 # desaturate to near-black, preserve alpha + from PIL import Image as _Image + return SheepOverlay._pil_to_cairo(_Image.fromarray(arr.astype(np.uint8))) + def _build_window(self): self.win = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) self.win.set_app_paintable(True) @@ -843,6 +918,68 @@ class SheepOverlay: sheep.y = max(0, min(sheep.y, self.screen_h - self.tile_h)) return True + # ── extra features ── + + def _spawn_black_sheep(self): + """Spawn a dark-tinted sheep from a random edge doing the blacksheep sequence.""" + from_right = random.random() < 0.5 + anim_id = 28 if from_right else 31 # 28=approach-left, 31=approach-right + sp = SpawnDef( + id=99, probability=100, + x_expr='screenW' if from_right else '0-imageW', + y_expr='screenH-imageH', + next_anim=anim_id, + ) + sheep = Sheep(self.anim_defs, self.tile_w, self.tile_h, + self.screen_w, self.screen_h, sp) + self.sheep_list.append(sheep) + self._black_sheep_set.add(sheep) + print(f"Black sheep enters from the {'right' if from_right else 'left'}!", flush=True) + + def _check_interactions(self, dt_ms: float): + """Bump and greet walking sheep that get too close.""" + busy = {self._drag_sheep} | {u.sheep for u in self.ufos} + active = [s for s in self.sheep_list if s not in busy] + + for i, a in enumerate(active): + for b in active[i + 1:]: + key = (min(id(a), id(b)), max(id(a), id(b))) + cd = self._interact_cooldowns.get(key, 0.0) + if cd > 0: + self._interact_cooldowns[key] = cd - dt_ms + continue + + same_level = abs(a.y - b.y) < self.tile_h * 0.7 + hdist = abs(a.x - b.x) + overlapping = hdist < self.tile_w * 0.6 and same_level + + # One or both sheep is black → skip social interactions + either_black = a in self._black_sheep_set or b in self._black_sheep_set + + if overlapping and not either_black: + # Bump: boing and reverse + a.facing_right = not a.facing_right + b.facing_right = not b.facing_right + a._init_anim(8) + b._init_anim(8) + self._interact_cooldowns[key] = 7000.0 + print("Sheep bumped into each other!", flush=True) + + elif (hdist < self.tile_w * 2.2 and same_level and not either_black + and id(a) not in self._frozen_sheep + and id(b) not in self._frozen_sheep): + # Approaching each other? (each facing toward the other) + a_toward = (a.x < b.x) == a.facing_right + b_toward = (b.x < a.x) == b.facing_right + if a_toward and b_toward: + # Greet: face each other and freeze briefly + a.facing_right = a.x < b.x + b.facing_right = b.x < a.x + self._frozen_sheep[id(a)] = 2200.0 + self._frozen_sheep[id(b)] = 2200.0 + self._interact_cooldowns[key] = 20000.0 + print("Sheep greeting!", flush=True) + def _spawn_sheep(self): total = sum(s.probability for s in self.spawns) r = random.randint(0, total - 1) @@ -870,10 +1007,16 @@ class SheepOverlay: windows = self.wayfire.get_windows() taskbar = self.wayfire.get_taskbar() - # Update all sheep (skip everything for the one being dragged) + # Update all sheep (skip dragged; respect greeting freeze) for sheep in self.sheep_list: if sheep is self._drag_sheep: continue + frozen_ms = self._frozen_sheep.get(id(sheep), 0.0) + if frozen_ms > 0: + self._frozen_sheep[id(sheep)] = frozen_ms - dt + continue + elif id(sheep) in self._frozen_sheep: + del self._frozen_sheep[id(sheep)] if not any(u.sheep is sheep for u in self.ufos): sheep.update_floors(windows, taskbar, self.screen_w, self.screen_h) sheep.tick(dt) @@ -881,13 +1024,50 @@ class SheepOverlay: # Keep the input region hugging the sheep positions self._update_input_region() + # ── flowers ── + self._eat_countdown -= dt / 1000.0 + if self._eat_countdown <= 0: + self._eat_countdown = random.uniform(60, 120) + candidates = [s for s in self.sheep_list + if s is not self._drag_sheep + and s not in self._black_sheep_set + and s.floor_cond == COND_NONE + and not any(u.sheep is s for u in self.ufos) + and s.anim_id not in (5, 6, 13, 26)] + if candidates: + chosen = random.choice(candidates) + chosen._init_anim(26) + self._flowers.append(Flower(chosen)) + 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] + + # ── black sheep ── + self._black_sheep_countdown -= dt / 1000.0 + if self._black_sheep_countdown <= 0: + self._spawn_black_sheep() + self._black_sheep_countdown = random.uniform(180, 360) + # Remove black sheep that have fully faded out (finished anim 34, opacity→0) + gone = [s for s in self._black_sheep_set if s.anim_id == 34 and s.opacity < 0.05] + for s in gone: + self._black_sheep_set.discard(s) + if s in self.sheep_list: + self.sheep_list.remove(s) + print("Black sheep has vanished.", flush=True) + + # ── bumps & greetings ── + self._check_interactions(dt) + # UFO timer self._ufo_countdown -= dt / 1000.0 if self._ufo_countdown <= 0 and self.sheep_list: - target = random.choice(self.sheep_list) - if not any(u.sheep is target for u in self.ufos): - self.ufos.append(UFO(target, self.screen_w, self.screen_h)) - print("UFO abduction started!", flush=True) + candidates = [s for s in self.sheep_list if s not in self._black_sheep_set] + if candidates: + target = random.choice(candidates) + if not any(u.sheep is target for u in self.ufos): + self.ufos.append(UFO(target, self.screen_w, self.screen_h)) + print("UFO abduction started!", flush=True) self._ufo_countdown = self.UFO_INTERVAL_S + random.randint(-20, 30) # Tick UFOs @@ -908,10 +1088,16 @@ class SheepOverlay: for ufo in self.ufos: ufo.draw(cr) - # Draw sheep + # Draw flowers (behind sheep) + for flower in self._flowers: + flower.draw(cr) + + # Draw sheep (black sheep use darkened sprite) for sheep in self.sheep_list: frame, sx, sy, flip, alpha = sheep.draw_info() - draw_frame(cr, self.cairo_surface, + surface = (self.dark_surface if sheep in self._black_sheep_set + else self.cairo_surface) + draw_frame(cr, surface, self.tile_w, self.tile_h, self.tiles_x, frame, sx, sy, flip, alpha) |
