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))