Skip to main content

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}