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)