dotzuki_engine/overworld/
npc_movement.rs1use alloc::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, NpcWanderAxis};
16
17#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
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 wander_axis: NpcWanderAxis,
36 pub range: u8,
37 pub walk_counter: u8,
38 pub delay_counter: u8,
39 pub text_id: u8,
40 pub defeated: bool,
41 pub visible: bool,
42 pub scripted_path: VecDeque<(u16, u16)>,
43}
44
45pub const NPC_WALK_FRAMES: u8 = 16;
50pub const NPC_MAX_DELAY: u8 = 127;
54
55pub fn direction_toward(from_x: u16, from_y: u16, to_x: u16, to_y: u16) -> Option<Direction> {
62 let dx = to_x as i32 - from_x as i32;
63 let dy = to_y as i32 - from_y as i32;
64 if dx == 0 && dy == 0 {
65 return None;
66 }
67 if dx.abs() > dy.abs() {
68 Some(if dx > 0 {
69 Direction::Right
70 } else {
71 Direction::Left
72 })
73 } else {
74 Some(if dy > 0 {
75 Direction::Down
76 } else {
77 Direction::Up
78 })
79 }
80}
81
82pub fn start_scripted_move(npc: &mut NpcRuntimeState, path: &[(u8, u8)]) {
86 npc.scripted_path.clear();
87 for &(x, y) in path {
88 npc.scripted_path.push_back((x as u16, y as u16));
89 }
90}
91
92pub fn is_scripted_move_done(npc: &NpcRuntimeState) -> bool {
94 npc.scripted_path.is_empty() && npc.walk_counter == 0
95}
96
97pub fn get_npc_positions(npcs: &[NpcRuntimeState]) -> Vec<SpritePosition> {
101 npcs.iter()
102 .filter(|n| n.visible)
103 .map(|n| SpritePosition { x: n.x, y: n.y })
104 .collect()
105}
106
107pub fn npc_at_position(npcs: &[NpcRuntimeState], x: u16, y: u16) -> Option<&NpcRuntimeState> {
111 npcs.iter().find(|n| n.visible && n.x == x && n.y == y)
112}
113
114pub fn npc_at_position_mut(
116 npcs: &mut [NpcRuntimeState],
117 x: u16,
118 y: u16,
119) -> Option<&mut NpcRuntimeState> {
120 npcs.iter_mut().find(|n| n.visible && n.x == x && n.y == y)
121}
122
123pub fn update_npc_movement<T: TilesetTrait>(
143 npcs: &mut [NpcRuntimeState],
144 player_x: u16,
145 player_y: u16,
146 player_dest: Option<(u16, u16)>,
147 map_width_blocks: u8,
148 map_height_blocks: u8,
149 rng_value: u8,
150 blocks: &[u8],
151 tileset: T,
152 provider: &impl CollisionProvider<T>,
153) {
154 let max_x = (map_width_blocks as u16) * 2;
155 let max_y = (map_height_blocks as u16) * 2;
156
157 let occupied: Vec<(u16, u16)> = npcs
160 .iter()
161 .filter(|n| n.visible)
162 .flat_map(|n| {
163 let cur = (n.x, n.y);
164 if n.walk_counter > 0 {
165 let (dx, dy) = direction_delta(n.facing);
166 let dest = (
167 (n.x as i32 + dx as i32).max(0) as u16,
168 (n.y as i32 + dy as i32).max(0) as u16,
169 );
170 vec![cur, dest]
171 } else {
172 vec![cur]
173 }
174 })
175 .collect();
176
177 for i in 0..npcs.len() {
178 let npc = &mut npcs[i];
179 if !npc.visible {
180 continue;
181 }
182
183 if npc.walk_counter > 0 {
185 npc.walk_counter -= 1;
186 if npc.walk_counter == 0 {
187 let (dx, dy) = direction_delta(npc.facing);
188 npc.x = (npc.x as i32 + dx as i32).max(0) as u16;
189 npc.y = (npc.y as i32 + dy as i32).max(0) as u16;
190
191 if !npc.scripted_path.is_empty() {
193 let &(tx, ty) = npc.scripted_path.front().unwrap();
194 if npc.x == tx && npc.y == ty {
195 npc.scripted_path.pop_front();
196 }
197 if let Some(&(ntx, nty)) = npc.scripted_path.front() {
198 if let Some(dir) = direction_toward(npc.x, npc.y, ntx, nty) {
199 npc.facing = dir;
200 npc.walk_counter = NPC_WALK_FRAMES;
201 }
202 }
203 }
204 }
205 continue;
206 }
207
208 if !npc.scripted_path.is_empty() {
210 let &(tx, ty) = npc.scripted_path.front().unwrap();
211 if npc.x == tx && npc.y == ty {
212 npc.scripted_path.pop_front();
213 if npc.scripted_path.is_empty() {
214 continue;
215 }
216 let &(tx, ty) = npc.scripted_path.front().unwrap();
217 if let Some(dir) = direction_toward(npc.x, npc.y, tx, ty) {
218 npc.facing = dir;
219 npc.walk_counter = NPC_WALK_FRAMES;
220 }
221 } else if let Some(dir) = direction_toward(npc.x, npc.y, tx, ty) {
222 npc.facing = dir;
223 npc.walk_counter = NPC_WALK_FRAMES;
224 }
225 continue;
226 }
227
228 match npc.movement_type {
230 NpcMovementType::Stationary => {}
231 NpcMovementType::Wander => {
232 if npc.delay_counter > 0 {
233 npc.delay_counter -= 1;
234 continue;
235 }
236
237 let dir_bits = (rng_value.wrapping_add(i as u8)) & 0x03;
243 let dir = match dir_bits {
244 0 => Direction::Down,
245 1 => Direction::Up,
246 2 => Direction::Left,
247 3 => Direction::Right,
248 _ => unreachable!(),
249 };
250 let axis_ok = match npc.wander_axis {
251 NpcWanderAxis::Any => true,
252 NpcWanderAxis::Vertical => {
253 dir == Direction::Up || dir == Direction::Down
254 }
255 NpcWanderAxis::Horizontal => {
256 dir == Direction::Left || dir == Direction::Right
257 }
258 };
259 if !axis_ok {
260 npc.delay_counter = rng_value & NPC_MAX_DELAY;
261 continue;
262 }
263
264 let (dx, dy) = direction_delta(dir);
265 let tx = (npc.x as i32 + dx as i32) as u16;
266 let ty = (npc.y as i32 + dy as i32) as u16;
267
268 if tx >= max_x || ty >= max_y {
271 npc.facing = dir;
272 npc.delay_counter = rng_value & NPC_MAX_DELAY;
273 continue;
274 }
275
276 let blocked = occupied
278 .iter()
279 .any(|&(ox, oy)| !(ox == npc.x && oy == npc.y) && ox == tx && oy == ty);
280 let player_blocked = (tx == player_x && ty == player_y)
281 || player_dest.map_or(false, |(px, py)| tx == px && ty == py);
282
283 if blocked || player_blocked {
284 npc.facing = dir;
285 npc.delay_counter = rng_value & NPC_MAX_DELAY;
286 continue;
287 }
288
289 let target_tile =
291 provider.get_tile_at_position(tileset, blocks, map_width_blocks, tx, ty);
292 if !provider.is_tile_passable(tileset, target_tile) {
293 npc.facing = dir;
294 npc.delay_counter = rng_value & NPC_MAX_DELAY;
295 continue;
296 }
297
298 npc.facing = dir;
299 npc.walk_counter = NPC_WALK_FRAMES;
300 npc.delay_counter = rng_value & NPC_MAX_DELAY;
301 }
302 NpcMovementType::FacePlayer => {
303 let dx = player_x as i32 - npc.x as i32;
304 let dy = player_y as i32 - npc.y as i32;
305
306 if dx.abs() >= dy.abs() {
307 npc.facing = if dx > 0 {
308 Direction::Right
309 } else {
310 Direction::Left
311 };
312 } else {
313 npc.facing = if dy > 0 {
314 Direction::Down
315 } else {
316 Direction::Up
317 };
318 }
319 }
320 NpcMovementType::FixedPath => {}
321 }
322 }
323}
324
325pub fn npc_in_front_of_player<'a, M: MapTrait, T: TilesetTrait, Mus>(
333 npcs: &'a [NpcRuntimeState],
334 player_x: u16,
335 player_y: u16,
336 facing: Direction,
337 map: Option<&MapData<M, T, Mus>>,
338 provider: &impl CollisionProvider<T>,
339) -> Option<&'a NpcRuntimeState> {
340 let (dx, dy) = direction_delta(facing);
341 let target_x = (player_x as i32 + dx as i32) as u16;
342 let target_y = (player_y as i32 + dy as i32) as u16;
343
344 if let Some(npc) = npc_at_position(npcs, target_x, target_y) {
345 return Some(npc);
346 }
347
348 if let Some(map_data) = map {
351 let tile = provider.get_tile_at_position(
352 map_data.tileset,
353 &map_data.blocks,
354 map_data.width,
355 target_x,
356 target_y,
357 );
358 if provider.is_counter_tile(map_data.tileset, tile) {
359 let extended_x = (target_x as i32 + dx as i32) as u16;
360 let extended_y = (target_y as i32 + dy as i32) as u16;
361 return npc_at_position(npcs, extended_x, extended_y);
362 }
363 }
364
365 None
366}