nodes/town_npc.pyΒΆ

Part of Dungeon Explorer.

 1"""Town NPC base: dialog trigger and trade interface."""
 2
 3from collision_layers import LAYER_NPC, MASK_NONE
 4
 5from simvx.core import CharacterBody2D, CircleShape2D, Property, Signal, Vec2
 6
 7
 8class TownNPC(CharacterBody2D):
 9    """NPC in the town hub. Triggers dialog when player interacts nearby."""
10
11    npc_id = Property("", hint="NPC identifier for dialog lookup")
12    display_name = Property("NPC", hint="Display name shown above NPC")
13    interaction_range = Property(40.0, range=(20, 200), hint="Interaction range in pixels")
14
15    interact_requested = Signal()  # (npc_id)
16
17    def __init__(self, npc_id: str = "", **kwargs):
18        # An NPC never sweeps, but the SWEEPING player would bump into every NPC in
19        # the town hub if they shared a layer. Interaction is a range check.
20        kwargs.setdefault("collision_layer", LAYER_NPC)
21        kwargs.setdefault("collision_mask", MASK_NONE)
22        super().__init__(shape=CircleShape2D(12.0), **kwargs)
23        self.npc_id = npc_id
24        self._colour = (0.3, 0.7, 0.4, 1.0)
25
26    def can_interact(self, player_pos: Vec2) -> bool:
27        dx = float(player_pos.x) - self.position.x
28        dy = float(player_pos.y) - self.position.y
29        dist = (dx * dx + dy * dy) ** 0.5
30        return dist <= self.interaction_range
31
32    def interact(self) -> None:
33        self.interact_requested(self.npc_id)
34
35    def on_draw(self, renderer):
36        px, py = self.position.x, self.position.y
37        c = self._colour
38        nid = self.npc_id
39
40        # Body (circle head + rect torso)
41        renderer.draw_rect((px - 7, py - 4), (14, 16), colour=c, filled=True)  # Torso
42        renderer.draw_circle((px, py - 8), 6, colour=c, filled=True)  # Head
43
44        # Role-specific details
45        if nid == "shopkeeper":
46            # Apron
47            renderer.draw_rect((px - 6, py), (12, 10), colour=(0.85, 0.85, 0.75, 1.0), filled=True)
48        elif nid == "innkeeper":
49            # Tankard in hand
50            renderer.draw_rect((px + 7, py - 2), (5, 6), colour=(0.6, 0.45, 0.2, 1.0), filled=True)
51            renderer.draw_rect((px + 6, py - 3), (7, 2), colour=(0.65, 0.5, 0.25, 1.0), filled=True)
52        elif nid == "blacksmith":
53            # Hammer
54            renderer.draw_line((px + 7, py + 2), (px + 14, py - 8), colour=(0.5, 0.4, 0.3, 1.0))
55            renderer.draw_rect((px + 11, py - 10), (6, 4), colour=(0.5, 0.5, 0.55, 1.0), filled=True)
56        elif nid == "well_keeper":
57            # Robes (longer body, lighter)
58            renderer.draw_rect((px - 8, py + 2), (16, 10), colour=(0.3, 0.4, 0.65, 1.0), filled=True)
59        elif nid == "quest_board":
60            # Wooden board with papers
61            renderer.draw_rect((px - 10, py - 10), (20, 24), colour=(0.5, 0.35, 0.2, 1.0), filled=True)
62            renderer.draw_rect((px - 7, py - 7), (6, 8), colour=(0.9, 0.85, 0.7, 0.8), filled=True)
63            renderer.draw_rect((px + 1, py - 7), (6, 8), colour=(0.85, 0.8, 0.65, 0.8), filled=True)
64            renderer.draw_rect((px - 4, py + 3), (8, 6), colour=(0.9, 0.9, 0.75, 0.7), filled=True)
65        elif nid == "enchanter":
66            # Wizard robe with star
67            renderer.draw_rect((px - 8, py + 2), (16, 12), colour=(0.4, 0.2, 0.6, 1.0), filled=True)
68            renderer.fill_triangle(px, py - 16, px - 6, py - 4, px + 6, py - 4, colour=(0.5, 0.25, 0.7, 1.0))
69
70        # Feet
71        renderer.draw_rect((px - 5, py + 12), (4, 3), colour=(0.3, 0.2, 0.15, 1.0), filled=True)
72        renderer.draw_rect((px + 1, py + 12), (4, 3), colour=(0.3, 0.2, 0.15, 1.0), filled=True)
73
74        # Name above
75        renderer.draw_text(self.display_name, (px - 15, py - 22), scale=0.8, colour=(1.0, 1.0, 1.0))