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 =
165 if state.player.transport == TransportMode::Biking && state.player.bike_speedup_active {
166 2
167 } else {
168 1
169 };
170
171 state.walk_counter = state.walk_counter.saturating_sub(decrement);
172
173 if state.walk_counter == 0 {
174 let (dx, dy) = direction_delta(state.player.facing);
175
176 if state.player.movement_state == MovementState::Jumping {
177 let new_x = (state.player.x as i32 + dx as i32 * 2) as u16;
178 let new_y = (state.player.y as i32 + dy as i32 * 2) as u16;
179 state.player.x = new_x;
180 state.player.y = new_y;
181 } else {
182 let new_x = (state.player.x as i32 + dx as i32) as u16;
183 let new_y = (state.player.y as i32 + dy as i32) as u16;
184 state.player.x = new_x;
185 state.player.y = new_y;
186 }
187
188 state.player.movement_state = MovementState::Idle;
189
190 if state.encounter_cooldown > 0 {
191 state.encounter_cooldown -= 1;
192 }
193
194 return true;
202 }
203
204 false
205}
206
207pub fn tick_repel_step<M: MapTrait>(state: &mut GenericOverworldState<M>) {
211 if state.repel_steps > 0 {
212 state.repel_steps -= 1;
213 }
214}
215
216pub fn direction_delta(dir: Direction) -> (i8, i8) {
218 match dir {
219 Direction::Down => (0, 1),
220 Direction::Up => (0, -1),
221 Direction::Left => (-1, 0),
222 Direction::Right => (1, 0),
223 }
224}
225
226pub fn opposite_direction(dir: Direction) -> Direction {
228 match dir {
229 Direction::Down => Direction::Up,
230 Direction::Up => Direction::Down,
231 Direction::Left => Direction::Right,
232 Direction::Right => Direction::Left,
233 }
234}
235
236pub fn frames_per_step(transport: TransportMode) -> u8 {
238 match transport {
239 TransportMode::Walking => WALK_COUNTER_INIT,
240 TransportMode::Biking => WALK_COUNTER_INIT / 2,
241 TransportMode::Surfing => WALK_COUNTER_INIT,
242 }
243}
244
245pub fn direction_to_facing_index(dir: Direction) -> u8 {
247 match dir {
248 Direction::Down => 0,
249 Direction::Up => 1,
250 Direction::Left => 2,
251 Direction::Right => 3,
252 }
253}
254
255pub fn get_tile_at_position<M: MapTrait, T: TilesetTrait, Mus>(
257 map: &MapData<M, T, Mus>,
258 x: u16,
259 y: u16,
260 provider: &impl CollisionProvider<T>,
261) -> u8 {
262 provider.get_tile_at_position(map.tileset, &map.blocks, map.width, x, y)
263}
264
265pub fn get_target_tile_for_direction<M: MapTrait, T: TilesetTrait, Mus>(
266 map: &MapData<M, T, Mus>,
267 x: u16,
268 y: u16,
269 dir: Direction,
270 provider: &impl CollisionProvider<T>,
271) -> u8 {
272 let (dx, dy) = direction_delta(dir);
273 let target_x = ((x as i32) + dx as i32).max(0) as u16;
274 let target_y = ((y as i32) + dy as i32).max(0) as u16;
275 provider.get_tile_at_position(map.tileset, &map.blocks, map.width, target_x, target_y)
276}
277
278pub fn extra_warp_check<M: MapTrait, T: TilesetTrait, Mus>(
280 map: &MapData<M, T, Mus>,
281 player_x: u16,
282 player_y: u16,
283 facing: Direction,
284 provider: &impl CollisionProvider<T>,
285) -> bool {
286 let tile_in_front = get_target_tile_for_direction(map, player_x, player_y, facing, provider);
288 if let Some(result) = provider.check_extra_warp_special(map.tileset, tile_in_front) {
289 return result;
290 }
291
292 if provider.uses_warp_tile_in_front_check(map.tileset) {
293 let facing_idx = direction_to_facing_index(facing);
294 provider.is_warp_carpet_tile_in_front(map.tileset, facing_idx, tile_in_front)
295 } else {
296 is_facing_map_edge(player_x, player_y, facing, map.width, map.height)
297 }
298}
299
300pub fn check_warps_no_collision<M: MapTrait, T: TilesetTrait, Mus>(
302 state: &mut GenericOverworldState<M>,
303 map: &MapData<M, T, Mus>,
304 standing_tile: u8,
305 direction_held: bool,
306 provider: &impl CollisionProvider<T>,
307) -> Option<usize> {
308 let warp_idx = check_warp_at_position(state.player.x, state.player.y, map)?;
309
310 state.standing_on_warp = true;
311
312 if provider.is_door_tile(map.tileset, standing_tile) {
313 return Some(warp_idx);
314 }
315
316 if provider.is_warp_tile(map.tileset, standing_tile) {
317 state.standing_on_warp = false;
318 return Some(warp_idx);
319 }
320
321 if extra_warp_check(
322 map,
323 state.player.x,
324 state.player.y,
325 state.player.facing,
326 provider,
327 ) {
328 if direction_held {
329 return Some(warp_idx);
330 }
331 }
332
333 None
334}
335
336pub fn check_collision_warp<M: MapTrait, T: TilesetTrait, Mus>(
338 state: &mut GenericOverworldState<M>,
339 map: &MapData<M, T, Mus>,
340 move_result: MoveResult,
341 provider: &impl CollisionProvider<T>,
342) -> MoveResult {
343 match move_result {
344 MoveResult::Blocked(_) | MoveResult::ReachedMapEdge => {
345 if state.standing_on_warp {
346 if extra_warp_check(
347 map,
348 state.player.x,
349 state.player.y,
350 state.player.facing,
351 provider,
352 ) {
353 if let Some(warp_idx) =
354 check_warp_at_position(state.player.x, state.player.y, map)
355 {
356 return MoveResult::Warped {
357 warp_index: warp_idx,
358 };
359 }
360 }
361 }
362 move_result
363 }
364 _ => move_result,
365 }
366}
367
368pub fn process_frame<M: MapTrait, T: TilesetTrait, Mus>(
373 state: &mut GenericOverworldState<M>,
374 input: &InputState,
375 map: &MapData<M, T, Mus>,
376 standing_tile: u8,
377 target_tile: u8,
378 npc_positions: &[SpritePosition],
379 provider: &impl CollisionProvider<T>,
380) -> MoveResult {
381 if state.player.movement_state != MovementState::Idle {
383 let step_done = advance_step(state);
384 if step_done {
385 let new_standing_tile =
386 get_tile_at_position(map, state.player.x, state.player.y, provider);
387 let direction_held = input.direction_pressed().is_some();
388
389 if let Some(warp_idx) =
390 check_warps_no_collision(state, map, new_standing_tile, direction_held, provider)
391 {
392 return MoveResult::Warped {
393 warp_index: warp_idx,
394 };
395 }
396
397 if let Some(direction) = input.direction_pressed() {
398 let held_input = input.to_pad_bits();
399
400 let new_target_tile = get_target_tile_for_direction(
401 map,
402 state.player.x,
403 state.player.y,
404 direction,
405 provider,
406 );
407
408 let move_result = try_move(
409 state,
410 direction,
411 map.tileset,
412 map.width,
413 map.height,
414 new_standing_tile,
415 new_target_tile,
416 npc_positions,
417 held_input,
418 provider,
419 );
420
421 return check_collision_warp(state, map, move_result, provider);
422 }
423 }
424 return MoveResult::StillMoving;
425 }
426
427 let direction = match input.direction_pressed() {
429 Some(dir) => dir,
430 None => return MoveResult::NoInput,
431 };
432
433 let held_input = input.to_pad_bits();
434
435 let move_result = try_move(
436 state,
437 direction,
438 map.tileset,
439 map.width,
440 map.height,
441 standing_tile,
442 target_tile,
443 npc_positions,
444 held_input,
445 provider,
446 );
447
448 check_collision_warp(state, map, move_result, provider)
449}