dotzuki_engine/overworld/npc_interaction.rs
1//! Generic NPC interaction system — talk, line-of-sight, sign interaction.
2//!
3//! Implements interaction logic: the talk/use interaction handler and
4//! trainer sight checks (fighting-map trainers).
5
6use crate::map::MapTrait;
7use crate::tileset::TilesetTrait;
8
9use super::collision::CollisionProvider;
10use super::npc_movement::{npc_in_front_of_player, NpcRuntimeState};
11use super::player_movement::direction_delta;
12use super::types::{Direction, MapData};
13
14// ── Interaction Result ─────────────────────────────────────────────
15
16/// Result of an NPC interaction attempt (pressing A near an NPC).
17///
18/// Game-specific interaction types (e.g., trainer battle, item pickup)
19/// are handled by the consuming crate using additional NPC metadata
20/// stored alongside the runtime state.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum InteractionResult {
23 /// No NPC in front of the player.
24 NoTarget,
25 /// Regular NPC dialog — show text_id from the map's text table.
26 Talk { npc_index: u8, text_id: u8 },
27 /// NPC already defeated/collected — shows post-defeat dialog.
28 AlreadyDefeated { npc_index: u8, text_id: u8 },
29}
30
31// ── Try Interact ───────────────────────────────────────────────────
32
33/// Attempt to interact with the NPC the player is facing.
34///
35/// In the original game, pressing A checks the tile in front of
36/// the player for an NPC sprite, then dispatches based on the NPC's
37/// type (regular text, trainer, or item ball).
38///
39/// If the tile in front is a counter tile, extends the range by one
40/// more tile to allow talking to NPCs behind counters.
41///
42/// Game-specific NPC types (trainers, item balls) should be checked
43/// by the caller before or after calling this function.
44pub fn try_interact<M: MapTrait, T: TilesetTrait, Mus>(
45 npcs: &[NpcRuntimeState],
46 player_x: u16,
47 player_y: u16,
48 facing: Direction,
49 map: Option<&MapData<M, T, Mus>>,
50 provider: &impl CollisionProvider<T>,
51) -> InteractionResult {
52 let npc = match npc_in_front_of_player(npcs, player_x, player_y, facing, map, provider) {
53 Some(n) => n,
54 None => return InteractionResult::NoTarget,
55 };
56
57 if npc.defeated {
58 return InteractionResult::AlreadyDefeated {
59 npc_index: npc.npc_index,
60 text_id: npc.text_id,
61 };
62 }
63
64 InteractionResult::Talk {
65 npc_index: npc.npc_index,
66 text_id: npc.text_id,
67 }
68}
69
70// ── Line of Sight ──────────────────────────────────────────────────
71
72/// Result of a line-of-sight check between an NPC and the player.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct LineOfSightResult {
75 pub npc_index: u8,
76 pub distance: u8,
77}
78
79/// Check if any NPC can see the player along their facing direction.
80///
81/// Each NPC's range determines how far they can see in their facing
82/// direction. An NPC with range of 0 is never checked.
83///
84/// This is the generic LOS algorithm — the caller should filter which
85/// NPCs to include (e.g., only trainer-type NPCs).
86pub fn check_line_of_sight(
87 npcs: &[NpcRuntimeState],
88 player_x: u16,
89 player_y: u16,
90) -> Option<LineOfSightResult> {
91 for npc in npcs {
92 if !npc.visible || npc.defeated || npc.range == 0 {
93 continue;
94 }
95
96 let (dx, dy) = direction_delta(npc.facing);
97 let mut check_x = npc.x as i32;
98 let mut check_y = npc.y as i32;
99
100 for dist in 1..=npc.range {
101 check_x += dx as i32;
102 check_y += dy as i32;
103
104 if check_x < 0 || check_y < 0 {
105 break;
106 }
107
108 if check_x as u16 == player_x && check_y as u16 == player_y {
109 return Some(LineOfSightResult {
110 npc_index: npc.npc_index,
111 distance: dist,
112 });
113 }
114 }
115 }
116 None
117}
118
119// ── Mark Defeated ──────────────────────────────────────────────────
120
121/// Mark an NPC as defeated after a battle or other interaction.
122pub fn mark_defeated(npcs: &mut [NpcRuntimeState], npc_index: u8) {
123 if let Some(npc) = npcs.iter_mut().find(|n| n.npc_index == npc_index) {
124 npc.defeated = true;
125 }
126}
127
128// ── Sign Interaction ───────────────────────────────────────────────
129
130/// Check if a sign is at the tile the player is facing.
131///
132/// Signs are interacted with by pressing A while facing them.
133pub fn check_sign_interaction(
134 signs: &[(u8, u8, u8)],
135 player_x: u16,
136 player_y: u16,
137 facing: Direction,
138) -> Option<u8> {
139 let (dx, dy) = direction_delta(facing);
140 let target_x = (player_x as i32 + dx as i32) as u8;
141 let target_y = (player_y as i32 + dy as i32) as u8;
142
143 signs
144 .iter()
145 .find(|&&(sx, sy, _)| sx == target_x && sy == target_y)
146 .map(|&(_, _, text_id)| text_id)
147}