1use crate::map::MapTrait;
7use crate::tileset::TilesetTrait;
8
9use super::collision::{
10 check_movement_collision, check_warp_at_position, is_facing_map_edge, CollisionProvider,
11 CollisionResult, SpritePosition,
12};
13use super::types::{
14 Direction, MapData, MovementState, OverworldState as GenericOverworldState, TransportMode,
15};
16
17pub const WALK_COUNTER_INIT: u8 = 8;
19
20#[derive(Debug, Clone, Copy, Default)]
22pub struct InputState {
23 pub up: bool,
24 pub down: bool,
25 pub left: bool,
26 pub right: bool,
27 pub a_button: bool,
28 pub b_button: bool,
29 pub start: bool,
30 pub select: bool,
31}
32
33impl InputState {
34 pub fn direction_pressed(&self) -> Option<Direction> {
37 if self.down {
38 Some(Direction::Down)
39 } else if self.up {
40 Some(Direction::Up)
41 } else if self.left {
42 Some(Direction::Left)
43 } else if self.right {
44 Some(Direction::Right)
45 } else {
46 None
47 }
48 }
49
50 pub fn to_pad_bits(&self) -> u8 {
52 let mut bits = 0u8;
53 if self.down {
54 bits |= super::collision::PAD_DOWN;
55 }
56 if self.up {
57 bits |= super::collision::PAD_UP;
58 }
59 if self.left {
60 bits |= super::collision::PAD_LEFT;
61 }
62 if self.right {
63 bits |= super::collision::PAD_RIGHT;
64 }
65 bits
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum MoveResult {
72 Walking,
74 LedgeJump,
76 TurnedOnly,
78 Blocked(CollisionResult),
80 ReachedMapEdge,
82 Warped { warp_index: usize },
84 StillMoving,
86 NoInput,
88}
89
90pub fn try_move<M: MapTrait, T: TilesetTrait>(
94 state: &mut GenericOverworldState<M>,
95 direction: Direction,
96 tileset: T,
97 map_width_blocks: u8,
98 map_height_blocks: u8,
99 standing_tile: u8,
100 target_tile: u8,
101 npc_positions: &[SpritePosition],
102 held_input: u8,
103 provider: &impl CollisionProvider<T>,
104) -> MoveResult {
105 if state.player.movement_state != MovementState::Idle {
106 return MoveResult::StillMoving;
107 }
108
109 let was_facing = state.player.facing;
110 state.player.facing = direction;
111
112 let result = check_movement_collision(
113 state.player.x,
114 state.player.y,
115 direction,
116 tileset,
117 map_width_blocks,
118 map_height_blocks,
119 standing_tile,
120 target_tile,
121 state.player.transport,
122 npc_positions,
123 held_input,
124 provider,
125 );
126
127 match result {
128 CollisionResult::Passable | CollisionResult::StopSurfing => {
129 if result == CollisionResult::StopSurfing {
132 state.player.transport = TransportMode::Walking;
133 }
134 state.standing_on_warp = false;
135 state.player.movement_state = MovementState::Walking;
136 state.walk_counter = WALK_COUNTER_INIT;
137 MoveResult::Walking
138 }
139 CollisionResult::LedgeJump => {
140 state.standing_on_warp = false;
141 state.player.movement_state = MovementState::Jumping;
142 state.walk_counter = WALK_COUNTER_INIT * 2;
143 MoveResult::LedgeJump
144 }
145 CollisionResult::MapEdge => MoveResult::ReachedMapEdge,
146 _ => {
147 if was_facing != direction {
148 MoveResult::TurnedOnly
149 } else {
150 MoveResult::Blocked(result)
151 }
152 }
153 }
154}
155
156pub fn advance_step<M: MapTrait>(state: &mut GenericOverworldState<M>) -> bool {
160 if state.walk_counter == 0 {
161 return true;
162 }
163
164 let decrement = if state.player.transport == TransportMode::Biking
165 && state.player.bike_speedup_active
166 {
167 2
168 } else {
169 1
170 };
171
172 state.walk_counter = state.walk_counter.saturating_sub(decrement);
173
174 if state.walk_counter == 0 {
175 let (dx, dy) = direction_delta(state.player.facing);
176
177 if state.player.movement_state == MovementState::Jumping {
178 let new_x = (state.player.x as i32 + dx as i32 * 2) as u16;
179 let new_y = (state.player.y as i32 + dy as i32 * 2) as u16;
180 state.player.x = new_x;
181 state.player.y = new_y;
182 } else {
183 let new_x = (state.player.x as i32 + dx as i32) as u16;
184 let new_y = (state.player.y as i32 + dy as i32) as u16;
185 state.player.x = new_x;
186 state.player.y = new_y;
187 }
188
189 state.player.movement_state = MovementState::Idle;
190
191 if state.encounter_cooldown > 0 {
192 state.encounter_cooldown -= 1;
193 }
194
195 if state.repel_steps > 0 {
196 state.repel_steps -= 1;
197 }
198
199 return true;
200 }
201
202 false
203}
204
205pub fn direction_delta(dir: Direction) -> (i8, i8) {
207 match dir {
208 Direction::Down => (0, 1),
209 Direction::Up => (0, -1),
210 Direction::Left => (-1, 0),
211 Direction::Right => (1, 0),
212 }
213}
214
215pub fn opposite_direction(dir: Direction) -> Direction {
217 match dir {
218 Direction::Down => Direction::Up,
219 Direction::Up => Direction::Down,
220 Direction::Left => Direction::Right,
221 Direction::Right => Direction::Left,
222 }
223}
224
225pub fn frames_per_step(transport: TransportMode) -> u8 {
227 match transport {
228 TransportMode::Walking => WALK_COUNTER_INIT,
229 TransportMode::Biking => WALK_COUNTER_INIT / 2,
230 TransportMode::Surfing => WALK_COUNTER_INIT,
231 }
232}
233
234pub fn direction_to_facing_index(dir: Direction) -> u8 {
236 match dir {
237 Direction::Down => 0,
238 Direction::Up => 1,
239 Direction::Left => 2,
240 Direction::Right => 3,
241 }
242}
243
244pub fn get_tile_at_position<M: MapTrait, T: TilesetTrait, Mus>(
246 map: &MapData<M, T, Mus>,
247 x: u16,
248 y: u16,
249 provider: &impl CollisionProvider<T>,
250) -> u8 {
251 provider.get_tile_at_position(map.tileset, &map.blocks, map.width, x, y)
252}
253
254pub fn get_target_tile_for_direction<M: MapTrait, T: TilesetTrait, Mus>(
255 map: &MapData<M, T, Mus>,
256 x: u16,
257 y: u16,
258 dir: Direction,
259 provider: &impl CollisionProvider<T>,
260) -> u8 {
261 let (dx, dy) = direction_delta(dir);
262 let target_x = ((x as i32) + dx as i32).max(0) as u16;
263 let target_y = ((y as i32) + dy as i32).max(0) as u16;
264 provider.get_tile_at_position(map.tileset, &map.blocks, map.width, target_x, target_y)
265}
266
267pub fn extra_warp_check<M: MapTrait, T: TilesetTrait, Mus>(
269 map: &MapData<M, T, Mus>,
270 player_x: u16,
271 player_y: u16,
272 facing: Direction,
273 provider: &impl CollisionProvider<T>,
274) -> bool {
275 let tile_in_front = get_target_tile_for_direction(map, player_x, player_y, facing, provider);
277 if let Some(result) = provider.check_extra_warp_special(map.tileset, tile_in_front) {
278 return result;
279 }
280
281 if provider.uses_warp_tile_in_front_check(map.tileset) {
282 let facing_idx = direction_to_facing_index(facing);
283 provider.is_warp_carpet_tile_in_front(map.tileset, facing_idx, tile_in_front)
284 } else {
285 is_facing_map_edge(player_x, player_y, facing, map.width, map.height)
286 }
287}
288
289pub fn check_warps_no_collision<M: MapTrait, T: TilesetTrait, Mus>(
291 state: &mut GenericOverworldState<M>,
292 map: &MapData<M, T, Mus>,
293 standing_tile: u8,
294 direction_held: bool,
295 provider: &impl CollisionProvider<T>,
296) -> Option<usize> {
297 let warp_idx = check_warp_at_position(state.player.x, state.player.y, map)?;
298
299 state.standing_on_warp = true;
300
301 if provider.is_door_tile(map.tileset, standing_tile) {
302 return Some(warp_idx);
303 }
304
305 if provider.is_warp_tile(map.tileset, standing_tile) {
306 state.standing_on_warp = false;
307 return Some(warp_idx);
308 }
309
310 if extra_warp_check(map, state.player.x, state.player.y, state.player.facing, provider) {
311 if direction_held {
312 return Some(warp_idx);
313 }
314 }
315
316 None
317}
318
319pub fn check_collision_warp<M: MapTrait, T: TilesetTrait, Mus>(
321 state: &mut GenericOverworldState<M>,
322 map: &MapData<M, T, Mus>,
323 move_result: MoveResult,
324 provider: &impl CollisionProvider<T>,
325) -> MoveResult {
326 match move_result {
327 MoveResult::Blocked(_) | MoveResult::ReachedMapEdge => {
328 if state.standing_on_warp {
329 if extra_warp_check(
330 map,
331 state.player.x,
332 state.player.y,
333 state.player.facing,
334 provider,
335 ) {
336 if let Some(warp_idx) =
337 check_warp_at_position(state.player.x, state.player.y, map)
338 {
339 return MoveResult::Warped {
340 warp_index: warp_idx,
341 };
342 }
343 }
344 }
345 move_result
346 }
347 _ => move_result,
348 }
349}
350
351pub fn process_frame<M: MapTrait, T: TilesetTrait, Mus>(
356 state: &mut GenericOverworldState<M>,
357 input: &InputState,
358 map: &MapData<M, T, Mus>,
359 standing_tile: u8,
360 target_tile: u8,
361 npc_positions: &[SpritePosition],
362 provider: &impl CollisionProvider<T>,
363) -> MoveResult {
364 if state.player.movement_state != MovementState::Idle {
366 let step_done = advance_step(state);
367 if step_done {
368 let new_standing_tile = get_tile_at_position(map, state.player.x, state.player.y, provider);
369 let direction_held = input.direction_pressed().is_some();
370
371 if let Some(warp_idx) = check_warps_no_collision(
372 state,
373 map,
374 new_standing_tile,
375 direction_held,
376 provider,
377 ) {
378 return MoveResult::Warped {
379 warp_index: warp_idx,
380 };
381 }
382
383 if let Some(direction) = input.direction_pressed() {
384 let held_input = input.to_pad_bits();
385
386 let new_target_tile = get_target_tile_for_direction(
387 map,
388 state.player.x,
389 state.player.y,
390 direction,
391 provider,
392 );
393
394 let move_result = try_move(
395 state,
396 direction,
397 map.tileset,
398 map.width,
399 map.height,
400 new_standing_tile,
401 new_target_tile,
402 npc_positions,
403 held_input,
404 provider,
405 );
406
407 return check_collision_warp(state, map, move_result, provider);
408 }
409 }
410 return MoveResult::StillMoving;
411 }
412
413 let direction = match input.direction_pressed() {
415 Some(dir) => dir,
416 None => return MoveResult::NoInput,
417 };
418
419 let held_input = input.to_pad_bits();
420
421 let move_result = try_move(
422 state,
423 direction,
424 map.tileset,
425 map.width,
426 map.height,
427 standing_tile,
428 target_tile,
429 npc_positions,
430 held_input,
431 provider,
432 );
433
434 check_collision_warp(state, map, move_result, provider)
435}