Skip to main content

dotzuki_engine/overworld/
types.rs

1//! Core overworld type definitions for a JRPG engine.
2//!
3//! These types define the foundational data structures for the overworld
4//! map system, player movement, NPCs, and map data. All types are generic
5//! over their game-specific identifiers using the [`MapTrait`] and
6//! [`TilesetTrait`] trait bounds.
7
8use crate::map::MapTrait;
9use crate::tileset::TilesetTrait;
10
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::hash::Hash;
14
15// ── Direction ──────────────────────────────────────────────────────
16
17/// Cardinal direction for movement and connections.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum Direction {
20    Down,
21    Up,
22    Left,
23    Right,
24}
25
26/// Transport mode for player movement.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum TransportMode {
29    Walking,
30    Biking,
31    Surfing,
32}
33
34/// Player movement state.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36pub enum MovementState {
37    Idle,
38    Walking,
39    Jumping,
40}
41
42// ── Map Connection ─────────────────────────────────────────────────
43
44/// A single map connection (e.g., north exit leads to Route 1).
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[non_exhaustive]
47pub struct MapConnection<M: MapTrait> {
48    pub direction: Direction,
49    pub target_map: M,
50    /// Offset in blocks for alignment when crossing the boundary.
51    pub offset: i8,
52}
53
54impl<M: MapTrait> MapConnection<M> {
55    /// Create a new map connection.
56    pub fn new(direction: Direction, target_map: M, offset: i8) -> Self {
57        Self {
58            direction,
59            target_map,
60            offset,
61        }
62    }
63}
64
65/// All connections for a map (up to one per cardinal direction).
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[non_exhaustive]
68pub struct MapConnections<M: MapTrait> {
69    pub north: Option<MapConnection<M>>,
70    pub south: Option<MapConnection<M>>,
71    pub west: Option<MapConnection<M>>,
72    pub east: Option<MapConnection<M>>,
73}
74
75impl<M: MapTrait> Default for MapConnections<M> {
76    fn default() -> Self {
77        Self {
78            north: None,
79            south: None,
80            west: None,
81            east: None,
82        }
83    }
84}
85
86impl<M: MapTrait> MapConnections<M> {
87    /// Number of active connections.
88    pub fn count(&self) -> usize {
89        self.north.is_some() as usize
90            + self.south.is_some() as usize
91            + self.west.is_some() as usize
92            + self.east.is_some() as usize
93    }
94
95    /// Get connection for a direction, if any.
96    pub fn get(&self, dir: Direction) -> Option<&MapConnection<M>> {
97        match dir {
98            Direction::Up => self.north.as_ref(),
99            Direction::Down => self.south.as_ref(),
100            Direction::Left => self.west.as_ref(),
101            Direction::Right => self.east.as_ref(),
102        }
103    }
104}
105
106// ── Warp Point ─────────────────────────────────────────────────────
107
108/// A warp point within a map (door, staircase, etc.).
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[non_exhaustive]
111pub struct WarpPoint<M: MapTrait> {
112    /// Position in the map (block coordinates).
113    pub x: u8,
114    pub y: u8,
115    /// Target map to warp to.
116    pub target_map: M,
117    /// Index of the target warp in the destination map.
118    pub target_warp_id: u8,
119    /// Whether this warp sends the player back to the last-visited map.
120    pub is_last_map: bool,
121}
122
123impl<M: MapTrait> WarpPoint<M> {
124    /// Create a new warp point.
125    pub fn new(x: u8, y: u8, target_map: M, target_warp_id: u8) -> Self {
126        Self {
127            x,
128            y,
129            target_map,
130            target_warp_id,
131            is_last_map: false,
132        }
133    }
134}
135
136// ── Sign ───────────────────────────────────────────────────────────
137
138/// A sign in the map that displays text when interacted with.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[non_exhaustive]
141pub struct Sign {
142    pub x: u8,
143    pub y: u8,
144    /// Index into the map's text table.
145    pub text_id: u8,
146}
147
148impl Sign {
149    /// Create a new sign.
150    pub fn new(x: u8, y: u8, text_id: u8) -> Self {
151        Self { x, y, text_id }
152    }
153}
154
155// ── NPC Definition ─────────────────────────────────────────────────
156
157/// NPC movement pattern.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159pub enum NpcMovementType {
160    /// NPC stays in place and faces a fixed direction.
161    Stationary,
162    /// NPC walks randomly within their range.
163    Wander,
164    /// NPC walks a fixed path.
165    FixedPath,
166    /// NPC turns to face the player when spoken to.
167    FacePlayer,
168}
169
170/// Axis restriction for [`NpcMovementType::Wander`] — the classic GB
171/// "movement byte 2" of a random-walk NPC ($00 any / $01 up-down /
172/// $02 left-right). Gen-1 random walkers have NO radial leash: they walk
173/// the allowed axis until blocked, forever.
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
175pub enum NpcWanderAxis {
176    /// Any of the four directions (movement byte 2 = $00).
177    #[default]
178    Any,
179    /// Only vertically — up/down (movement byte 2 = $01, UP_DOWN).
180    Vertical,
181    /// Only horizontally — left/right (movement byte 2 = $02, LEFT_RIGHT).
182    Horizontal,
183}
184
185/// Definition of an NPC placed on the map (static data from map objects).
186///
187/// This is a generic NPC definition with no game-specific fields.
188/// Game-specific data (e.g., trainer flags, item drops) should be
189/// stored in a separate struct alongside this one.
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[non_exhaustive]
192pub struct NpcDefinition {
193    /// Sprite ID (index into sprite table).
194    pub sprite_id: u8,
195    /// Starting position.
196    pub x: u8,
197    pub y: u8,
198    /// Movement type.
199    pub movement: NpcMovementType,
200    /// Axis restriction for Wander NPCs (classic movement byte 2). Ignored
201    /// for other movement types.
202    pub wander_axis: NpcWanderAxis,
203    /// Facing direction.
204    pub facing: Direction,
205    /// Range of movement (0 = stationary).
206    pub range: u8,
207    /// Text ID triggered on interaction.
208    pub text_id: u8,
209}
210
211impl NpcDefinition {
212    /// Create a new NPC definition.
213    #[allow(clippy::too_many_arguments)]
214    pub fn new(
215        sprite_id: u8,
216        x: u8,
217        y: u8,
218        movement: NpcMovementType,
219        facing: Direction,
220        range: u8,
221        text_id: u8,
222    ) -> Self {
223        Self {
224            sprite_id,
225            x,
226            y,
227            movement,
228            wander_axis: NpcWanderAxis::Any,
229            facing,
230            range,
231            text_id,
232        }
233    }
234
235    /// Create a new NPC definition with an explicit Wander axis (the classic
236    /// GB "movement byte 2"). Only meaningful for [`NpcMovementType::Wander`].
237    #[allow(clippy::too_many_arguments)]
238    pub fn new_with_axis(
239        sprite_id: u8,
240        x: u8,
241        y: u8,
242        movement: NpcMovementType,
243        wander_axis: NpcWanderAxis,
244        facing: Direction,
245        range: u8,
246        text_id: u8,
247    ) -> Self {
248        Self {
249            sprite_id,
250            x,
251            y,
252            movement,
253            wander_axis,
254            facing,
255            range,
256            text_id,
257        }
258    }
259}
260
261// ── Map Data ───────────────────────────────────────────────────────
262
263/// Complete runtime data for a loaded map.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[non_exhaustive]
266pub struct MapData<M: MapTrait, T: TilesetTrait, Mus> {
267    pub id: M,
268    pub width: u8,
269    pub height: u8,
270    pub tileset: T,
271    pub music: Mus,
272    /// Block data — the actual tile layout. Each byte is a block index
273    /// into the tileset's block definitions. Size = width * height.
274    pub blocks: Vec<u8>,
275    pub warps: Vec<WarpPoint<M>>,
276    pub npcs: Vec<NpcDefinition>,
277    pub signs: Vec<Sign>,
278    pub connections: MapConnections<M>,
279}
280
281impl<M: MapTrait, T: TilesetTrait, Mus> MapData<M, T, Mus> {
282    /// Create a new MapData with all fields.
283    #[allow(clippy::too_many_arguments)]
284    pub fn new(
285        id: M,
286        width: u8,
287        height: u8,
288        tileset: T,
289        music: Mus,
290        blocks: Vec<u8>,
291        warps: Vec<WarpPoint<M>>,
292        npcs: Vec<NpcDefinition>,
293        signs: Vec<Sign>,
294        connections: MapConnections<M>,
295    ) -> Self {
296        Self {
297            id,
298            width,
299            height,
300            tileset,
301            music,
302            blocks,
303            warps,
304            npcs,
305            signs,
306            connections,
307        }
308    }
309
310    /// Replace the block at BLOCK coords (`block_x`, `block_y`) with `block_id`.
311    ///
312    /// Returns `false` (no-op) if the coordinates fall outside the map. The
313    /// collision and rendering systems read `blocks` live every frame, so the
314    /// change takes effect immediately with no cache to invalidate. The change
315    /// is transient: a map reload rebuilds `blocks`, so callers that need a
316    /// persistent change must re-apply it on map entry.
317    pub fn set_block(&mut self, block_x: u8, block_y: u8, block_id: u8) -> bool {
318        let (w, h) = (self.width as usize, self.height as usize);
319        let (bx, by) = (block_x as usize, block_y as usize);
320        if bx >= w || by >= h {
321            return false;
322        }
323        let idx = by * w + bx;
324        if idx < self.blocks.len() {
325            self.blocks[idx] = block_id;
326            true
327        } else {
328            false
329        }
330    }
331}
332
333// ── Player State ───────────────────────────────────────────────────
334
335/// Runtime player state in the overworld.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[non_exhaustive]
338pub struct PlayerState {
339    pub x: u16,
340    pub y: u16,
341    pub facing: Direction,
342    pub movement_state: MovementState,
343    pub transport: TransportMode,
344    /// Whether biking advances at double speed (the classic bike speedup).
345    /// Defaults to on; a game can switch it off for specific rules (e.g.
346    /// a steep slope cancels the speedup while the player presses
347    /// UP/LEFT/RIGHT).
348    #[serde(default = "default_bike_speedup")]
349    pub bike_speedup_active: bool,
350}
351
352fn default_bike_speedup() -> bool {
353    true
354}
355
356impl Default for PlayerState {
357    fn default() -> Self {
358        Self {
359            x: 0,
360            y: 0,
361            facing: Direction::Down,
362            movement_state: MovementState::Idle,
363            transport: TransportMode::Walking,
364            bike_speedup_active: true,
365        }
366    }
367}
368
369// ── Overworld State ────────────────────────────────────────────────
370
371/// Top-level overworld state, holding the current map and player.
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[non_exhaustive]
374pub struct OverworldState<M: MapTrait> {
375    pub current_map: M,
376    pub player: PlayerState,
377    /// Walk animation counter (0-15).
378    pub walk_counter: u8,
379    /// Steps until next wild encounter check resets.
380    pub encounter_cooldown: u8,
381    /// Remaining repel steps (0 = inactive).
382    pub repel_steps: u16,
383    /// Whether the player is currently standing on a warp coordinate.
384    pub standing_on_warp: bool,
385    /// Whether the player just warped onto a door tile and needs to auto-step off.
386    pub standing_on_door: bool,
387    /// Whether the player is currently performing the auto-step out of a door.
388    pub exiting_door: bool,
389}
390
391impl<M: MapTrait> OverworldState<M> {
392    /// Create a new overworld state starting at the given map.
393    pub fn new(start_map: M) -> Self {
394        Self {
395            current_map: start_map,
396            player: PlayerState::default(),
397            walk_counter: 0,
398            encounter_cooldown: 0,
399            repel_steps: 0,
400            standing_on_warp: false,
401            standing_on_door: false,
402            exiting_door: false,
403        }
404    }
405}
406
407// ── Overworld Input ────────────────────────────────────────────────
408
409/// Overworld input state for a single frame.
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
411#[non_exhaustive]
412pub struct OverworldInput {
413    pub up: bool,
414    pub down: bool,
415    pub left: bool,
416    pub right: bool,
417    pub a: bool,
418    pub b: bool,
419    pub start: bool,
420    pub select: bool,
421}
422
423impl OverworldInput {
424    /// Create a new overworld input state with the given button states.
425    pub fn new(
426        up: bool,
427        down: bool,
428        left: bool,
429        right: bool,
430        a: bool,
431        b: bool,
432        start: bool,
433        select: bool,
434    ) -> Self {
435        Self {
436            up,
437            down,
438            left,
439            right,
440            a,
441            b,
442            start,
443            select,
444        }
445    }
446
447    /// Create an input state with no keys pressed.
448    pub fn none() -> Self {
449        Self {
450            up: false,
451            down: false,
452            left: false,
453            right: false,
454            a: false,
455            b: false,
456            start: false,
457            select: false,
458        }
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
467    struct TestMap;
468    impl MapTrait for TestMap {}
469
470    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
471    struct TestTileset;
472    impl TilesetTrait for TestTileset {
473        fn id(&self) -> u8 {
474            0
475        }
476        fn name(&self) -> &'static str {
477            "test"
478        }
479    }
480
481    /// Build a 3×2 (w×h) map whose blocks are [0,1,2, 3,4,5].
482    fn make_map() -> MapData<TestMap, TestTileset, u8> {
483        MapData::new(
484            TestMap,
485            3,
486            2,
487            TestTileset,
488            0u8,
489            vec![0, 1, 2, 3, 4, 5],
490            Vec::new(),
491            Vec::new(),
492            Vec::new(),
493            MapConnections::default(),
494        )
495    }
496
497    #[test]
498    fn set_block_in_bounds_mutates_and_returns_true() {
499        let mut map = make_map();
500        // (block_x=2, block_y=1) -> idx = 1*3 + 2 = 5
501        assert!(map.set_block(2, 1, 99));
502        assert_eq!(map.blocks[5], 99);
503        // Other blocks untouched.
504        assert_eq!(map.blocks, vec![0, 1, 2, 3, 4, 99]);
505
506        // Top-left corner -> idx 0.
507        assert!(map.set_block(0, 0, 42));
508        assert_eq!(map.blocks[0], 42);
509    }
510
511    #[test]
512    fn set_block_out_of_bounds_returns_false_and_no_change() {
513        let mut map = make_map();
514        let before = map.blocks.clone();
515
516        // x out of range (width is 3, so valid x is 0..=2).
517        assert!(!map.set_block(3, 0, 99));
518        // y out of range (height is 2, so valid y is 0..=1).
519        assert!(!map.set_block(0, 2, 99));
520        // Both out of range.
521        assert!(!map.set_block(10, 10, 99));
522
523        assert_eq!(map.blocks, before, "out-of-bounds writes must not mutate");
524    }
525}