nodes/settler.pyΒΆ
Part of Tiny Yurts.
1"""Settler: an agent that walks a precomputed cell path between yurt and farm.
2
3Settlers are owned by a Yurt; they leave home, travel to the farm, "feed"
4(decrement demand), then return home. Movement is plain lerp along cell
5midpoints converted via the iso projection -- no engine NavigationAgent2D
6because the agents follow the player-drawn graph, not a free-space grid.
7"""
8
9from __future__ import annotations
10
11import math
12
13from simvx.core import Node2D, Property, Vec2
14
15from . import iso
16
17
18class Settler(Node2D):
19 speed_cells_per_sec = Property(2.4, range=(0.5, 10.0))
20
21 def __init__(self, kind: str, home_cell: tuple[int, int], colour, **kwargs):
22 super().__init__(**kwargs)
23 self.kind = kind
24 self.home_cell = home_cell
25 self.colour = colour
26 self._route: list[tuple[int, int]] = []
27 self._segment_index: int = 0
28 self._segment_t: float = 0.0
29 self._returning: bool = False
30 self.farm = None # Set by Yurt when dispatched
31 self._delivered_callback = None
32 # Place at home in screen space
33 sx, sy = iso.world_to_screen(*home_cell)
34 self.position = Vec2(sx, sy)
35 self._idle = True
36
37 # ---------- Public API ----------
38
39 def dispatch(self, route: list[tuple[int, int]], farm, on_delivered) -> None:
40 """Send this settler along ``route`` to ``farm``. Calls back when home."""
41 if not route:
42 return
43 self._route = route
44 self._segment_index = 0
45 self._segment_t = 0.0
46 self._returning = False
47 self.farm = farm
48 self._delivered_callback = on_delivered
49 self._idle = False
50
51 @property
52 def is_idle(self) -> bool:
53 return self._idle
54
55 # ---------- Tick ----------
56
57 def on_update(self, dt: float) -> None:
58 if self._idle:
59 # Idle settlers wait at their yurt. Re-project rather than trusting
60 # the position from spawn time: a resize moves the whole board.
61 self._move_to(iso.world_to_screen(*self.home_cell))
62 return
63 if not self._route:
64 return
65 # Advance along current segment
66 if self._segment_index >= len(self._route) - 1:
67 self._reach_end()
68 return
69 a = self._route[self._segment_index]
70 b = self._route[self._segment_index + 1]
71 # Step in cell space, scaled by diagonal length so diagonals take ~1.41x time
72 di, dj = b[0] - a[0], b[1] - a[1]
73 seg_len = math.hypot(di, dj) or 1.0
74 self._segment_t += (self.speed_cells_per_sec * dt) / seg_len
75 if self._segment_t >= 1.0:
76 self._segment_t = 0.0
77 self._segment_index += 1
78 if self._segment_index >= len(self._route) - 1:
79 self._reach_end()
80 return
81 a = self._route[self._segment_index]
82 b = self._route[self._segment_index + 1]
83 ax, ay = iso.world_to_screen(*a)
84 bx, by = iso.world_to_screen(*b)
85 t = self._segment_t
86 self._move_to((ax + (bx - ax) * t, ay + (by - ay) * t))
87
88 def _move_to(self, screen: tuple[float, float]) -> None:
89 """Set ``position``, skipping the write (and the redraw) when unchanged."""
90 if (float(self.position.x), float(self.position.y)) != screen:
91 self.position = Vec2(*screen)
92
93 def _reach_end(self) -> None:
94 if not self._returning:
95 # Arrived at farm: feed, then turn around
96 if self.farm is not None:
97 # Each delivery takes 1.5 capacity off demand
98 self.farm.demand = max(0.0, self.farm.demand - 1.5)
99 self._route = list(reversed(self._route))
100 self._segment_index = 0
101 self._segment_t = 0.0
102 self._returning = True
103 else:
104 # Arrived back home: idle, notify yurt
105 self._idle = True
106 if self._delivered_callback is not None:
107 self._delivered_callback(self)
108 self._delivered_callback = None
109
110 # ---------- Draw ----------
111
112 def on_draw(self, renderer) -> None:
113 cx, cy = self.position.x, self.position.y
114 renderer.draw_circle((cx, cy + 1), 4.5, colour=(0.0, 0.0, 0.0, 0.4), filled=True)
115 renderer.draw_circle((cx, cy - 2), 4.5, colour=self.colour, filled=True)
116 # Tiny "head"
117 renderer.draw_circle((cx, cy - 8), 2.5, colour=(0.95, 0.85, 0.75, 1.0), filled=True)