dotzuki_engine/overworld/
npc_movement.rs1use std::collections::VecDeque;
9
10use crate::map::MapTrait;
11use crate::tileset::TilesetTrait;
12
13use super::collision::{CollisionProvider, SpritePosition};
14use super::player_movement::direction_delta;
15use super::types::{Direction, MapData, NpcMovementType};
16
17#[derive(Debug, Clone)]
24pub struct NpcRuntimeState {
25 pub npc_index: u8,
26 pub sprite_id: u8,
27 pub x: u16,
28 pub y: u16,
29 pub home_x: u16,
30 pub home_y: u16,
31 pub facing: Direction,
32 pub scripted_frame: Option<u8>,
33 pub movement_type: NpcMovementType,
34 pub range: u8,
35 pub walk_counter: u8,
36 pub delay_counter: u8,
37 pub text_id: u8,
38 pub defeated: bool,
39 pub visible: bool,
40 pub scripted_path: VecDeque<(u16, u16)>,
41}
42
43pub const NPC_WALK_FRAMES: u8 = 8;
47pub const NPC_MAX_DELAY: u8 = 63;
49
50pub fn direction_toward(from_x: u16, from_y: u16, to_x: u16, to_y: u16) -> Option<Direction> {
57 let dx = to_x as i32 - from_x as i32;
58 let dy = to_y as i32 - from_y as i32;
59 if dx == 0 && dy == 0 {
60 return None;
61 }
62 if dx.abs() > dy.abs() {
63 Some(if dx > 0 {
64 Direction::Right
65 } else {
66 Direction::Left
67 })
68 } else {
69 Some(if dy > 0 {
70 Direction::Down
71 } else {
72 Direction::Up
73 })
74 }
75}
76
77pub fn start_scripted_move(npc: &mut NpcRuntimeState, path: &[(u8, u8)]) {
81 npc.scripted_path.clear();
82 for &(x, y) in path {
83 npc.scripted_path.push_back((x as u16, y as u16));
84 }
85}
86
87pub fn is_scripted_move_done(npc: &NpcRuntimeState) -> bool {
89 npc.scripted_path.is_empty() && npc.walk_counter == 0
90}
91
92pub fn get_npc_positions(npcs: &[NpcRuntimeState]) -> Vec<SpritePosition> {
96 npcs.iter()
97 .filter(|n| n.visible)
98 .map(|n| SpritePosition { x: n.x, y: n.y })
99 .collect()
100}
101
102pub fn npc_at_position(npcs: &[NpcRuntimeState], x: u16, y: u16) -> Option<&NpcRuntimeState> {
106 npcs.iter().find(|n| n.visible && n.x == x && n.y == y)
107}
108
109pub fn npc_at_position_mut(
111 npcs: &mut [NpcRuntimeState],
112 x: u16,
113 y: u16,
114) -> Option<&mut NpcRuntimeState> {
115 npcs.iter_mut().find(|n| n.visible && n.x == x && n.y == y)
116}
117
118pub fn update_npc_movement<T: TilesetTrait>(
138 npcs: &mut [NpcRuntimeState],
139 player_x: u16,
140 player_y: u16,
141 player_dest: Option<(u16, u16)>,
142 map_width_blocks: u8,
143 map_height_blocks: u8,
144 rng_value: u8,
145 blocks: &[u8],
146 tileset: T,
147 provider: &impl CollisionProvider<T>,
148) {
149 let max_x = (map_width_blocks as u16) * 2;
150 let max_y = (map_height_blocks as u16) * 2;
151
152 let occupied: Vec<(u16, u16)> = npcs
155 .iter()
156 .filter(|n| n.visible)
157 .flat_map(|n| {
158 let cur = (n.x, n.y);
159 if n.walk_counter > 0 {
160 let (dx, dy) = direction_delta(n.facing);
161 let dest = (
162 (n.x as i32 + dx as i32).max(0) as u16,
163 (n.y as i32 + dy as i32).max(0) as u16,
164 );
165 vec![cur, dest]
166 } else {
167 vec![cur]
168 }
169 })
170 .collect();
171
172 for i in 0..npcs.len() {
173 let npc = &mut npcs[i];
174 if !npc.visible {
175 continue;
176 }
177
178 if npc.walk_counter > 0 {
180 npc.walk_counter -= 1;
181 if npc.walk_counter == 0 {
182 let (dx, dy) = direction_delta(npc.facing);
183 npc.x = (npc.x as i32 + dx as i32).max(0) as u16;
184 npc.y = (npc.y as i32 + dy as i32).max(0) as u16;
185
186 if !npc.scripted_path.is_empty() {
188 let &(tx, ty) = npc.scripted_path.front().unwrap();
189 if npc.x == tx && npc.y == ty {
190 npc.scripted_path.pop_front();
191 }
192 if let Some(&(ntx, nty)) = npc.scripted_path.front() {
193 if let Some(dir) = direction_toward(npc.x, npc.y, ntx, nty) {
194 npc.facing = dir;
195 npc.walk_counter = NPC_WALK_FRAMES;
196 }
197 }
198 }
199 }
200 continue;
201 }
202
203 if !npc.scripted_path.is_empty() {
205 let &(tx, ty) = npc.scripted_path.front().unwrap();
206 if npc.x == tx && npc.y == ty {
207 npc.scripted_path.pop_front();
208 if npc.scripted_path.is_empty() {
209 continue;
210 }
211 let &(tx, ty) = npc.scripted_path.front().unwrap();
212 if let Some(dir) = direction_toward(npc.x, npc.y, tx, ty) {
213 npc.facing = dir;
214 npc.walk_counter = NPC_WALK_FRAMES;
215 }
216 } else if let Some(dir) = direction_toward(npc.x, npc.y, tx, ty) {
217 npc.facing = dir;
218 npc.walk_counter = NPC_WALK_FRAMES;
219 }
220 continue;
221 }
222
223 match npc.movement_type {
225 NpcMovementType::Stationary => {}
226 NpcMovementType::Wander => {
227 if npc.delay_counter > 0 {
228 npc.delay_counter -= 1;
229 continue;
230 }
231
232 let dir_bits = (rng_value.wrapping_add(i as u8)) & 0x03;
234 let dir = match dir_bits {
235 0 => Direction::Down,
236 1 => Direction::Up,
237 2 => Direction::Left,
238 3 => Direction::Right,
239 _ => unreachable!(),
240 };
241
242 let (dx, dy) = direction_delta(dir);
243 let tx = (npc.x as i32 + dx as i32) as u16;
244 let ty = (npc.y as i32 + dy as i32) as u16;
245
246 if tx >= max_x || ty >= max_y {
248 npc.facing = dir;
249 npc.delay_counter = rng_value & NPC_MAX_DELAY;
250 continue;
251 }
252
253 if npc.range > 0 {
255 let dist_x = (tx as i32 - npc.home_x as i32).unsigned_abs();
256 let dist_y = (ty as i32 - npc.home_y as i32).unsigned_abs();
257 if dist_x > npc.range as u32 || dist_y > npc.range as u32 {
258 npc.facing = dir;
259 npc.delay_counter = rng_value & NPC_MAX_DELAY;
260 continue;
261 }
262 }
263
264 let blocked = occupied
266 .iter()
267 .any(|&(ox, oy)| !(ox == npc.x && oy == npc.y) && ox == tx && oy == ty);
268 let player_blocked = (tx == player_x && ty == player_y)
269 || player_dest.map_or(false, |(px, py)| tx == px && ty == py);
270
271 if blocked || player_blocked {
272 npc.facing = dir;
273 npc.delay_counter = rng_value & NPC_MAX_DELAY;
274 continue;
275 }
276
277 let target_tile = provider.get_tile_at_position(tileset, blocks, map_width_blocks, tx, ty);
279 if !provider.is_tile_passable(tileset, target_tile) {
280 npc.facing = dir;
281 npc.delay_counter = rng_value & NPC_MAX_DELAY;
282 continue;
283 }
284
285 npc.facing = dir;
286 npc.walk_counter = NPC_WALK_FRAMES;
287 npc.delay_counter = rng_value & NPC_MAX_DELAY;
288 }
289 NpcMovementType::FacePlayer => {
290 let dx = player_x as i32 - npc.x as i32;
291 let dy = player_y as i32 - npc.y as i32;
292
293 if dx.abs() >= dy.abs() {
294 npc.facing = if dx > 0 {
295 Direction::Right
296 } else {
297 Direction::Left
298 };
299 } else {
300 npc.facing = if dy > 0 {
301 Direction::Down
302 } else {
303 Direction::Up
304 };
305 }
306 }
307 NpcMovementType::FixedPath => {}
308 }
309 }
310}
311
312pub fn npc_in_front_of_player<'a, M: MapTrait, T: TilesetTrait, Mus>(
320 npcs: &'a [NpcRuntimeState],
321 player_x: u16,
322 player_y: u16,
323 facing: Direction,
324 map: Option<&MapData<M, T, Mus>>,
325 provider: &impl CollisionProvider<T>,
326) -> Option<&'a NpcRuntimeState> {
327 let (dx, dy) = direction_delta(facing);
328 let target_x = (player_x as i32 + dx as i32) as u16;
329 let target_y = (player_y as i32 + dy as i32) as u16;
330
331 if let Some(npc) = npc_at_position(npcs, target_x, target_y) {
332 return Some(npc);
333 }
334
335 if let Some(map_data) = map {
338 let tile = provider.get_tile_at_position(map_data.tileset, &map_data.blocks, map_data.width, target_x, target_y);
339 if provider.is_counter_tile(map_data.tileset, tile) {
340 let extended_x = (target_x as i32 + dx as i32) as u16;
341 let extended_y = (target_y as i32 + dy as i32) as u16;
342 return npc_at_position(npcs, extended_x, extended_y);
343 }
344 }
345
346 None
347}