summaryrefslogtreecommitdiffstats
path: root/esheep.py
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken.dambrosio@constantcontact.com>2026-06-03 21:50:28 -0400
committerKen D'Ambrosio <ken.dambrosio@constantcontact.com>2026-06-03 21:50:28 -0400
commit7a48d2a3cb7d3969be5e3e00b64a51c1da0550ee (patch)
tree430027d4f01d173644bcfbd3afa01e44b0e27f5e /esheep.py
parentef25374771e78d503d14446ed9d637306c0eb224 (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.py202
1 files changed, 194 insertions, 8 deletions
diff --git a/esheep.py b/esheep.py
index 590627c..190c4fd 100644
--- a/esheep.py
+++ b/esheep.py
@@ -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)