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.emit(self.npc_id)
34
35 def on_draw(self, renderer):
36 c = self._colour
37 nid = self.npc_id
38
39 # Body (circle head + rect torso)
40 renderer.rect((-7, -4), (14, 16), fill=c) # Torso
41 renderer.circle((0, -8), 6, fill=c) # Head
42
43 # Role-specific details
44 if nid == "shopkeeper":
45 # Apron
46 renderer.rect((-6, 0), (12, 10), fill=(0.85, 0.85, 0.75, 1.0))
47 elif nid == "innkeeper":
48 # Tankard in hand
49 renderer.rect((7, -2), (5, 6), fill=(0.6, 0.45, 0.2, 1.0))
50 renderer.rect((6, -3), (7, 2), fill=(0.65, 0.5, 0.25, 1.0))
51 elif nid == "blacksmith":
52 # Hammer
53 renderer.line((7, 2), (14, -8), stroke=(0.5, 0.4, 0.3, 1.0))
54 renderer.rect((11, -10), (6, 4), fill=(0.5, 0.5, 0.55, 1.0))
55 elif nid == "well_keeper":
56 # Robes (longer body, lighter)
57 renderer.rect((-8, 2), (16, 10), fill=(0.3, 0.4, 0.65, 1.0))
58 elif nid == "quest_board":
59 # Wooden board with papers
60 renderer.rect((-10, -10), (20, 24), fill=(0.5, 0.35, 0.2, 1.0))
61 renderer.rect((-7, -7), (6, 8), fill=(0.9, 0.85, 0.7, 0.8))
62 renderer.rect((1, -7), (6, 8), fill=(0.85, 0.8, 0.65, 0.8))
63 renderer.rect((-4, 3), (8, 6), fill=(0.9, 0.9, 0.75, 0.7))
64 elif nid == "enchanter":
65 # Wizard robe with star
66 renderer.rect((-8, 2), (16, 12), fill=(0.4, 0.2, 0.6, 1.0))
67 renderer.polygon([(0, -16), (-6, -4), (6, -4)], fill=(0.5, 0.25, 0.7, 1.0))
68
69 # Feet
70 renderer.rect((-5, 12), (4, 3), fill=(0.3, 0.2, 0.15, 1.0))
71 renderer.rect((1, 12), (4, 3), fill=(0.3, 0.2, 0.15, 1.0))
72
73 # Name above
74 renderer.text(self.display_name, (-15, -22), fill=(1.0, 1.0, 1.0), scale=0.8)