Skip to main content

dotzuki_engine/overworld/sprites/
mod.rs

1pub mod collision;
2pub mod oam;
3pub mod update;
4
5#[cfg(test)]
6mod tests;
7
8// ---------------------------------------------------------------------------
9// Generic constants (Game Boy hardware limits / sprite protocol)
10// ---------------------------------------------------------------------------
11
12pub const NUM_SPRITESTATEDATA_STRUCTS: usize = 16;
13pub const OAM_COUNT: usize = 40;
14pub const OAM_Y_OFS: u8 = 16;
15pub const OAM_X_OFS: u8 = 8;
16pub const OAM_BG_PRIORITY: u8 = 0x80;
17pub const OAM_XFLIP: u8 = 1 << 5;
18pub const FACING_END: u8 = 1 << 0;
19pub const UNDER_GRASS: u8 = 1 << 1;
20pub const GRASS_PRIORITY: u8 = 0x80;
21pub const IMAGE_INDEX_OFFSCREEN: u8 = 0xFF;
22pub const MOVEMENT_WALK: u8 = 0xFE;
23pub const MOVEMENT_STAY: u8 = 0xFF;
24
25// Facing direction constants
26pub const FACING_DOWN: u8 = 0x00;
27pub const FACING_UP: u8 = 0x04;
28pub const FACING_LEFT: u8 = 0x08;
29pub const FACING_RIGHT: u8 = 0x0C;
30
31// ---------------------------------------------------------------------------
32// MovementStatus enum
33// ---------------------------------------------------------------------------
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[repr(u8)]
37pub enum MovementStatus {
38    Uninitialized = 0,
39    Ready = 1,
40    Delayed = 2,
41    Moving = 3,
42}
43
44impl MovementStatus {
45    pub fn from_byte(b: u8) -> Self {
46        match b & 0x7F {
47            0 => Self::Uninitialized,
48            1 => Self::Ready,
49            2 => Self::Delayed,
50            3 => Self::Moving,
51            _ => Self::Uninitialized,
52        }
53    }
54
55    pub fn face_player_bit(b: u8) -> bool {
56        b & (1 << 7) != 0
57    }
58}
59
60// ---------------------------------------------------------------------------
61// Sprite-facing types (moved from pokered-data::sprite_facing)
62// ---------------------------------------------------------------------------
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct OamTemplate {
66    pub y_offset: u8,
67    pub x_offset: u8,
68    pub attributes: u8,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct SpriteTilePattern {
73    pub tiles: [u8; 4],
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct SpriteFacingEntry {
78    pub tile_pattern: &'static SpriteTilePattern,
79    pub oam_template: &'static [OamTemplate; 4],
80}
81
82// Tile patterns for each direction
83pub const STANDING_DOWN: SpriteTilePattern = SpriteTilePattern {
84    tiles: [0x00, 0x01, 0x02, 0x03],
85};
86pub const WALKING_DOWN: SpriteTilePattern = SpriteTilePattern {
87    tiles: [0x80, 0x81, 0x82, 0x83],
88};
89pub const STANDING_UP: SpriteTilePattern = SpriteTilePattern {
90    tiles: [0x04, 0x05, 0x06, 0x07],
91};
92pub const WALKING_UP: SpriteTilePattern = SpriteTilePattern {
93    tiles: [0x84, 0x85, 0x86, 0x87],
94};
95pub const STANDING_LEFT: SpriteTilePattern = SpriteTilePattern {
96    tiles: [0x08, 0x09, 0x0A, 0x0B],
97};
98pub const WALKING_LEFT: SpriteTilePattern = SpriteTilePattern {
99    tiles: [0x88, 0x89, 0x8A, 0x8B],
100};
101
102// OAM layout templates for normal and flipped sprites
103pub const NORMAL_OAM: [OamTemplate; 4] = [
104    OamTemplate {
105        y_offset: 0,
106        x_offset: 0,
107        attributes: 0x00,
108    },
109    OamTemplate {
110        y_offset: 0,
111        x_offset: 8,
112        attributes: 0x00,
113    },
114    OamTemplate {
115        y_offset: 8,
116        x_offset: 0,
117        attributes: UNDER_GRASS,
118    },
119    OamTemplate {
120        y_offset: 8,
121        x_offset: 8,
122        attributes: UNDER_GRASS | FACING_END,
123    },
124];
125
126pub const FLIPPED_OAM: [OamTemplate; 4] = [
127    OamTemplate {
128        y_offset: 0,
129        x_offset: 8,
130        attributes: OAM_XFLIP,
131    },
132    OamTemplate {
133        y_offset: 0,
134        x_offset: 0,
135        attributes: OAM_XFLIP,
136    },
137    OamTemplate {
138        y_offset: 8,
139        x_offset: 8,
140        attributes: OAM_XFLIP | UNDER_GRASS,
141    },
142    OamTemplate {
143        y_offset: 8,
144        x_offset: 0,
145        attributes: OAM_XFLIP | UNDER_GRASS | FACING_END,
146    },
147];
148
149// Sprite facing table (32 entries: 16 full-direction sprites + 16 immobile sprites)
150pub const SPRITE_FACING_TABLE: [SpriteFacingEntry; 32] = [
151    // Sprites $1-$9: full directional sprites (16 entries)
152    // Facing down
153    SpriteFacingEntry {
154        tile_pattern: &STANDING_DOWN,
155        oam_template: &NORMAL_OAM,
156    },
157    SpriteFacingEntry {
158        tile_pattern: &WALKING_DOWN,
159        oam_template: &NORMAL_OAM,
160    },
161    SpriteFacingEntry {
162        tile_pattern: &STANDING_DOWN,
163        oam_template: &NORMAL_OAM,
164    },
165    SpriteFacingEntry {
166        tile_pattern: &WALKING_DOWN,
167        oam_template: &FLIPPED_OAM,
168    },
169    // Facing up
170    SpriteFacingEntry {
171        tile_pattern: &STANDING_UP,
172        oam_template: &NORMAL_OAM,
173    },
174    SpriteFacingEntry {
175        tile_pattern: &WALKING_UP,
176        oam_template: &NORMAL_OAM,
177    },
178    SpriteFacingEntry {
179        tile_pattern: &STANDING_UP,
180        oam_template: &NORMAL_OAM,
181    },
182    SpriteFacingEntry {
183        tile_pattern: &WALKING_UP,
184        oam_template: &FLIPPED_OAM,
185    },
186    // Facing left
187    SpriteFacingEntry {
188        tile_pattern: &STANDING_LEFT,
189        oam_template: &NORMAL_OAM,
190    },
191    SpriteFacingEntry {
192        tile_pattern: &WALKING_LEFT,
193        oam_template: &NORMAL_OAM,
194    },
195    SpriteFacingEntry {
196        tile_pattern: &STANDING_LEFT,
197        oam_template: &NORMAL_OAM,
198    },
199    SpriteFacingEntry {
200        tile_pattern: &WALKING_LEFT,
201        oam_template: &NORMAL_OAM,
202    },
203    // Facing right (reuses left tiles with flipped OAM)
204    SpriteFacingEntry {
205        tile_pattern: &STANDING_LEFT,
206        oam_template: &FLIPPED_OAM,
207    },
208    SpriteFacingEntry {
209        tile_pattern: &WALKING_LEFT,
210        oam_template: &FLIPPED_OAM,
211    },
212    SpriteFacingEntry {
213        tile_pattern: &STANDING_LEFT,
214        oam_template: &FLIPPED_OAM,
215    },
216    SpriteFacingEntry {
217        tile_pattern: &WALKING_LEFT,
218        oam_template: &FLIPPED_OAM,
219    },
220    // Sprites $a-$b: immobile sprites (16 entries, all same)
221    SpriteFacingEntry {
222        tile_pattern: &STANDING_DOWN,
223        oam_template: &NORMAL_OAM,
224    },
225    SpriteFacingEntry {
226        tile_pattern: &STANDING_DOWN,
227        oam_template: &NORMAL_OAM,
228    },
229    SpriteFacingEntry {
230        tile_pattern: &STANDING_DOWN,
231        oam_template: &NORMAL_OAM,
232    },
233    SpriteFacingEntry {
234        tile_pattern: &STANDING_DOWN,
235        oam_template: &NORMAL_OAM,
236    },
237    SpriteFacingEntry {
238        tile_pattern: &STANDING_DOWN,
239        oam_template: &NORMAL_OAM,
240    },
241    SpriteFacingEntry {
242        tile_pattern: &STANDING_DOWN,
243        oam_template: &NORMAL_OAM,
244    },
245    SpriteFacingEntry {
246        tile_pattern: &STANDING_DOWN,
247        oam_template: &NORMAL_OAM,
248    },
249    SpriteFacingEntry {
250        tile_pattern: &STANDING_DOWN,
251        oam_template: &NORMAL_OAM,
252    },
253    SpriteFacingEntry {
254        tile_pattern: &STANDING_DOWN,
255        oam_template: &NORMAL_OAM,
256    },
257    SpriteFacingEntry {
258        tile_pattern: &STANDING_DOWN,
259        oam_template: &NORMAL_OAM,
260    },
261    SpriteFacingEntry {
262        tile_pattern: &STANDING_DOWN,
263        oam_template: &NORMAL_OAM,
264    },
265    SpriteFacingEntry {
266        tile_pattern: &STANDING_DOWN,
267        oam_template: &NORMAL_OAM,
268    },
269    SpriteFacingEntry {
270        tile_pattern: &STANDING_DOWN,
271        oam_template: &NORMAL_OAM,
272    },
273    SpriteFacingEntry {
274        tile_pattern: &STANDING_DOWN,
275        oam_template: &NORMAL_OAM,
276    },
277    SpriteFacingEntry {
278        tile_pattern: &STANDING_DOWN,
279        oam_template: &NORMAL_OAM,
280    },
281    SpriteFacingEntry {
282        tile_pattern: &STANDING_DOWN,
283        oam_template: &NORMAL_OAM,
284    },
285];
286
287pub const TILES_PER_SPRITE: usize = 12;
288pub const FOUR_TILE_SPRITE_A: u8 = 0x0A;
289pub const FOUR_TILE_SPRITE_B: u8 = 0x0B;
290pub const FOUR_TILE_SPRITE_B_OFFSET: u8 = 0x0A * 12 + 4;
291
292pub fn facing_table_index(image_index: u8) -> usize {
293    let is_unchanging = image_index >= 0xA0;
294    if is_unchanging {
295        let base = (image_index & 0x0F) as usize;
296        base + 16
297    } else {
298        (image_index & 0x0F) as usize
299    }
300}
301
302pub fn sprite_tile_base_offset(image_index: u8) -> u8 {
303    let sprite_num = (image_index >> 4) & 0x0F;
304    if sprite_num == FOUR_TILE_SPRITE_B {
305        FOUR_TILE_SPRITE_B_OFFSET
306    } else {
307        sprite_num.wrapping_mul(TILES_PER_SPRITE as u8)
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Core sprite data types
313// ---------------------------------------------------------------------------
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
316pub struct OamEntry {
317    pub y: u8,
318    pub x: u8,
319    pub tile_id: u8,
320    pub attributes: u8,
321}
322
323#[derive(Debug, Clone)]
324pub struct ShadowOam {
325    pub entries: [OamEntry; OAM_COUNT],
326}
327
328impl Default for ShadowOam {
329    fn default() -> Self {
330        Self {
331            entries: [OamEntry::default(); OAM_COUNT],
332        }
333    }
334}
335
336impl ShadowOam {
337    pub fn clear(&mut self) {
338        for entry in &mut self.entries {
339            *entry = OamEntry::default();
340        }
341    }
342
343    pub fn hide_all(&mut self, screen_height: u8) {
344        for entry in &mut self.entries {
345            entry.y = OAM_Y_OFS + screen_height;
346        }
347    }
348}
349
350#[derive(Debug, Clone, Copy, Default)]
351pub struct SpriteStateData1 {
352    pub picture_id: u8,
353    pub movement_status: u8,
354    pub image_index: u8,
355    pub y_step_vector: i8,
356    pub y_pixels: u8,
357    pub x_step_vector: i8,
358    pub x_pixels: u8,
359    pub intra_anim_frame_counter: u8,
360    pub anim_frame_counter: u8,
361    pub facing_direction: u8,
362    pub y_adjusted: u8,
363    pub x_adjusted: u8,
364    pub collision_data: u8,
365    pub field_0d: u8,
366    pub collision_sprite_lo: u8,
367    pub collision_sprite_hi: u8,
368}
369
370impl SpriteStateData1 {
371    pub fn is_active(&self) -> bool {
372        self.picture_id != 0
373    }
374
375    pub fn is_visible(&self) -> bool {
376        self.image_index != IMAGE_INDEX_OFFSCREEN
377    }
378
379    pub fn movement_status(&self) -> MovementStatus {
380        MovementStatus::from_byte(self.movement_status)
381    }
382
383    pub fn faces_player(&self) -> bool {
384        MovementStatus::face_player_bit(self.movement_status)
385    }
386
387    pub fn clear_collision(&mut self) {
388        self.collision_data = 0;
389        self.collision_sprite_lo = 0;
390        self.collision_sprite_hi = 0;
391    }
392}
393
394#[derive(Debug, Clone, Copy, Default)]
395pub struct SpriteStateData2 {
396    pub walk_anim_counter: u8,
397    pub field_01: u8,
398    pub y_displacement: u8,
399    pub x_displacement: u8,
400    pub map_y: u8,
401    pub map_x: u8,
402    pub movement_byte1: u8,
403    pub grass_priority: u8,
404    pub movement_delay: u8,
405    pub orig_facing_direction: u8,
406    pub field_0a: u8,
407    pub field_0b: u8,
408    pub field_0c: u8,
409    pub picture_id: u8,
410    pub image_base_offset: u8,
411    pub field_0f: u8,
412}
413
414impl SpriteStateData2 {
415    pub fn init_displacement(&mut self) {
416        self.y_displacement = 8;
417        self.x_displacement = 8;
418    }
419
420    pub fn has_grass_priority(&self) -> bool {
421        self.grass_priority & GRASS_PRIORITY != 0
422    }
423}
424
425pub struct SpriteTable {
426    pub data1: [SpriteStateData1; NUM_SPRITESTATEDATA_STRUCTS],
427    pub data2: [SpriteStateData2; NUM_SPRITESTATEDATA_STRUCTS],
428    pub shadow_oam: ShadowOam,
429    pub oam_count: usize,
430    pub ledge_or_fishing: bool,
431}
432
433impl Default for SpriteTable {
434    fn default() -> Self {
435        Self {
436            data1: [SpriteStateData1::default(); NUM_SPRITESTATEDATA_STRUCTS],
437            data2: [SpriteStateData2::default(); NUM_SPRITESTATEDATA_STRUCTS],
438            shadow_oam: ShadowOam::default(),
439            oam_count: 0,
440            ledge_or_fishing: false,
441        }
442    }
443}
444
445impl SpriteTable {
446    pub fn clear_all(&mut self) {
447        self.data1 = [SpriteStateData1::default(); NUM_SPRITESTATEDATA_STRUCTS];
448        self.data2 = [SpriteStateData2::default(); NUM_SPRITESTATEDATA_STRUCTS];
449        self.shadow_oam.clear();
450        self.oam_count = 0;
451    }
452
453    pub fn player_data1(&self) -> &SpriteStateData1 {
454        &self.data1[0]
455    }
456
457    pub fn player_data1_mut(&mut self) -> &mut SpriteStateData1 {
458        &mut self.data1[0]
459    }
460
461    pub fn player_data2(&self) -> &SpriteStateData2 {
462        &self.data2[0]
463    }
464
465    pub fn player_data2_mut(&mut self) -> &mut SpriteStateData2 {
466        &mut self.data2[0]
467    }
468
469    pub fn active_sprite_count(&self) -> usize {
470        self.data1.iter().filter(|s| s.is_active()).count()
471    }
472}