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/// Definition of an NPC placed on the map (static data from map objects).
171///
172/// This is a generic NPC definition with no game-specific fields.
173/// Game-specific data (e.g., trainer flags, item drops) should be
174/// stored in a separate struct alongside this one.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[non_exhaustive]
177pub struct NpcDefinition {
178    /// Sprite ID (index into sprite table).
179    pub sprite_id: u8,
180    /// Starting position.
181    pub x: u8,
182    pub y: u8,
183    /// Movement type.
184    pub movement: NpcMovementType,
185    /// Facing direction.
186    pub facing: Direction,
187    /// Range of movement (0 = stationary).
188    pub range: u8,
189    /// Text ID triggered on interaction.
190    pub text_id: u8,
191}
192
193impl NpcDefinition {
194    /// Create a new NPC definition.
195    #[allow(clippy::too_many_arguments)]
196    pub fn new(
197        sprite_id: u8,
198        x: u8,
199        y: u8,
200        movement: NpcMovementType,
201        facing: Direction,
202        range: u8,
203        text_id: u8,
204    ) -> Self {
205        Self {
206            sprite_id,
207            x,
208            y,
209            movement,
210            facing,
211            range,
212            text_id,
213        }
214    }
215}
216
217// ── Map Data ───────────────────────────────────────────────────────
218
219/// Complete runtime data for a loaded map.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[non_exhaustive]
222pub struct MapData<M: MapTrait, T: TilesetTrait, Mus> {
223    pub id: M,
224    pub width: u8,
225    pub height: u8,
226    pub tileset: T,
227    pub music: Mus,
228    /// Block data — the actual tile layout. Each byte is a block index
229    /// into the tileset's block definitions. Size = width * height.
230    pub blocks: Vec<u8>,
231    pub warps: Vec<WarpPoint<M>>,
232    pub npcs: Vec<NpcDefinition>,
233    pub signs: Vec<Sign>,
234    pub connections: MapConnections<M>,
235}
236
237impl<M: MapTrait, T: TilesetTrait, Mus> MapData<M, T, Mus> {
238    /// Create a new MapData with all fields.
239    #[allow(clippy::too_many_arguments)]
240    pub fn new(
241        id: M,
242        width: u8,
243        height: u8,
244        tileset: T,
245        music: Mus,
246        blocks: Vec<u8>,
247        warps: Vec<WarpPoint<M>>,
248        npcs: Vec<NpcDefinition>,
249        signs: Vec<Sign>,
250        connections: MapConnections<M>,
251    ) -> Self {
252        Self {
253            id,
254            width,
255            height,
256            tileset,
257            music,
258            blocks,
259            warps,
260            npcs,
261            signs,
262            connections,
263        }
264    }
265
266    /// Replace the block at BLOCK coords (`block_x`, `block_y`) with `block_id`.
267    ///
268    /// Returns `false` (no-op) if the coordinates fall outside the map. The
269    /// collision and rendering systems read `blocks` live every frame, so the
270    /// change takes effect immediately with no cache to invalidate. The change
271    /// is transient: a map reload rebuilds `blocks`, so callers that need a
272    /// persistent change must re-apply it on map entry.
273    pub fn set_block(&mut self, block_x: u8, block_y: u8, block_id: u8) -> bool {
274        let (w, h) = (self.width as usize, self.height as usize);
275        let (bx, by) = (block_x as usize, block_y as usize);
276        if bx >= w || by >= h {
277            return false;
278        }
279        let idx = by * w + bx;
280        if idx < self.blocks.len() {
281            self.blocks[idx] = block_id;
282            true
283        } else {
284            false
285        }
286    }
287}
288
289// ── Player State ───────────────────────────────────────────────────
290
291/// Runtime player state in the overworld.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293#[non_exhaustive]
294pub struct PlayerState {
295    pub x: u16,
296    pub y: u16,
297    pub facing: Direction,
298    pub movement_state: MovementState,
299    pub transport: TransportMode,
300    /// Whether biking advances at double speed (the classic bike speedup).
301    /// Defaults to on; a game can switch it off for specific rules (e.g.
302    /// a steep slope cancels the speedup while the player presses
303    /// UP/LEFT/RIGHT).
304    #[serde(default = "default_bike_speedup")]
305    pub bike_speedup_active: bool,
306}
307
308fn default_bike_speedup() -> bool {
309    true
310}
311
312impl Default for PlayerState {
313    fn default() -> Self {
314        Self {
315            x: 0,
316            y: 0,
317            facing: Direction::Down,
318            movement_state: MovementState::Idle,
319            transport: TransportMode::Walking,
320            bike_speedup_active: true,
321        }
322    }
323}
324
325// ── Overworld State ────────────────────────────────────────────────
326
327/// Top-level overworld state, holding the current map and player.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[non_exhaustive]
330pub struct OverworldState<M: MapTrait> {
331    pub current_map: M,
332    pub player: PlayerState,
333    /// Walk animation counter (0-15).
334    pub walk_counter: u8,
335    /// Steps until next wild encounter check resets.
336    pub encounter_cooldown: u8,
337    /// Remaining repel steps (0 = inactive).
338    pub repel_steps: u16,
339    /// Whether the player is currently standing on a warp coordinate.
340    pub standing_on_warp: bool,
341    /// Whether the player just warped onto a door tile and needs to auto-step off.
342    pub standing_on_door: bool,
343    /// Whether the player is currently performing the auto-step out of a door.
344    pub exiting_door: bool,
345}
346
347impl<M: MapTrait> OverworldState<M> {
348    /// Create a new overworld state starting at the given map.
349    pub fn new(start_map: M) -> Self {
350        Self {
351            current_map: start_map,
352            player: PlayerState::default(),
353            walk_counter: 0,
354            encounter_cooldown: 0,
355            repel_steps: 0,
356            standing_on_warp: false,
357            standing_on_door: false,
358            exiting_door: false,
359        }
360    }
361}
362
363// ── Overworld Input ────────────────────────────────────────────────
364
365/// Overworld input state for a single frame.
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
367#[non_exhaustive]
368pub struct OverworldInput {
369    pub up: bool,
370    pub down: bool,
371    pub left: bool,
372    pub right: bool,
373    pub a: bool,
374    pub b: bool,
375    pub start: bool,
376    pub select: bool,
377}
378
379impl OverworldInput {
380    /// Create a new overworld input state with the given button states.
381    pub fn new(
382        up: bool,
383        down: bool,
384        left: bool,
385        right: bool,
386        a: bool,
387        b: bool,
388        start: bool,
389        select: bool,
390    ) -> Self {
391        Self {
392            up,
393            down,
394            left,
395            right,
396            a,
397            b,
398            start,
399            select,
400        }
401    }
402
403    /// Create an input state with no keys pressed.
404    pub fn none() -> Self {
405        Self {
406            up: false,
407            down: false,
408            left: false,
409            right: false,
410            a: false,
411            b: false,
412            start: false,
413            select: false,
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
423    struct TestMap;
424    impl MapTrait for TestMap {}
425
426    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
427    struct TestTileset;
428    impl TilesetTrait for TestTileset {
429        fn id(&self) -> u8 {
430            0
431        }
432        fn name(&self) -> &'static str {
433            "test"
434        }
435    }
436
437    /// Build a 3×2 (w×h) map whose blocks are [0,1,2, 3,4,5].
438    fn make_map() -> MapData<TestMap, TestTileset, u8> {
439        MapData::new(
440            TestMap,
441            3,
442            2,
443            TestTileset,
444            0u8,
445            vec![0, 1, 2, 3, 4, 5],
446            Vec::new(),
447            Vec::new(),
448            Vec::new(),
449            MapConnections::default(),
450        )
451    }
452
453    #[test]
454    fn set_block_in_bounds_mutates_and_returns_true() {
455        let mut map = make_map();
456        // (block_x=2, block_y=1) -> idx = 1*3 + 2 = 5
457        assert!(map.set_block(2, 1, 99));
458        assert_eq!(map.blocks[5], 99);
459        // Other blocks untouched.
460        assert_eq!(map.blocks, vec![0, 1, 2, 3, 4, 99]);
461
462        // Top-left corner -> idx 0.
463        assert!(map.set_block(0, 0, 42));
464        assert_eq!(map.blocks[0], 42);
465    }
466
467    #[test]
468    fn set_block_out_of_bounds_returns_false_and_no_change() {
469        let mut map = make_map();
470        let before = map.blocks.clone();
471
472        // x out of range (width is 3, so valid x is 0..=2).
473        assert!(!map.set_block(3, 0, 99));
474        // y out of range (height is 2, so valid y is 0..=1).
475        assert!(!map.set_block(0, 2, 99));
476        // Both out of range.
477        assert!(!map.set_block(10, 10, 99));
478
479        assert_eq!(map.blocks, before, "out-of-bounds writes must not mutate");
480    }
481}