Skip to main content

dotzuki_renderer/
palette.rs

1use std::marker::PhantomData;
2
3use dotzuki_engine::palette::{PaletteProvider, PaletteTrait, SgbColor, SgbPaletteEntry};
4use dotzuki_engine::render::Rgba;
5
6// ============================================================================
7// ColorIndex trait
8// ============================================================================
9
10/// Trait for color index types that can be used with [`Palette`].
11///
12/// Implementations define the maximum number of colors and provide
13/// conversion from raw byte values to typed indices.
14pub trait ColorIndex: Copy + Clone + core::fmt::Debug + PartialEq + Eq {
15    /// Maximum number of colors in a palette using this index type.
16    const MAX: usize;
17
18    /// Create a color index from a raw byte, applying the appropriate mask.
19    fn from_u8(val: u8) -> Self;
20
21    /// Convert this index to a `usize` for array lookup.
22    fn to_index(self) -> usize;
23}
24
25// ============================================================================
26// GbColor — 2-bit Game Boy color index (4 colors)
27// ============================================================================
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum GbColor {
32    White = 0,
33    LightGray = 1,
34    DarkGray = 2,
35    Black = 3,
36}
37
38impl ColorIndex for GbColor {
39    const MAX: usize = 4;
40
41    fn from_u8(val: u8) -> Self {
42        match val & 0x03 {
43            0 => GbColor::White,
44            1 => GbColor::LightGray,
45            2 => GbColor::DarkGray,
46            _ => GbColor::Black,
47        }
48    }
49
50    fn to_index(self) -> usize {
51        self as usize
52    }
53}
54
55impl GbColor {
56    pub const ALL: [GbColor; 4] = [
57        GbColor::White,
58        GbColor::LightGray,
59        GbColor::DarkGray,
60        GbColor::Black,
61    ];
62
63    pub fn from_u8(val: u8) -> Self {
64        <Self as ColorIndex>::from_u8(val)
65    }
66}
67
68// ============================================================================
69// GbaColor — 4-bit GBA color index (16 colors)
70// ============================================================================
71
72/// A 4-bit color index for GBA-style palettes (up to 16 colors).
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct GbaColor(pub u8);
75
76impl ColorIndex for GbaColor {
77    const MAX: usize = 16;
78
79    fn from_u8(val: u8) -> Self {
80        GbaColor(val & 0x0F)
81    }
82
83    fn to_index(self) -> usize {
84        self.0 as usize
85    }
86}
87
88// ============================================================================
89// Palette — generic over color index type
90// ============================================================================
91
92/// A color palette mapping color indices to RGBA values.
93///
94/// The type parameter `C` controls the maximum number of colors.
95/// - `Palette` (defaults to `Palette<GbColor>`) — 4-color DMG palette.
96/// - `Palette<GbaColor>` — 16-color GBA palette.
97///
98/// The internal storage is always `[Rgba; 16]` with a `count` field,
99/// so const construction works for both 4- and 16-color palettes.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct Palette<C: ColorIndex = GbColor> {
102    /// Color data. Only the first `count` entries are meaningful.
103    pub colors: [Rgba; 16],
104    /// Number of valid colors (1–16).
105    pub count: u8,
106    #[doc(hidden)]
107    pub _phantom: PhantomData<C>,
108}
109
110impl<C: ColorIndex> Palette<C> {
111    /// Create a palette from a slice of RGBA colors.
112    ///
113    /// Colors beyond index 15 are silently ignored.
114    pub fn new(colors: &[Rgba]) -> Self {
115        let mut arr = [Rgba::BLACK; 16];
116        let count = colors.len().min(16);
117        arr[..count].copy_from_slice(&colors[..count]);
118        Self {
119            colors: arr,
120            count: count as u8,
121            _phantom: PhantomData,
122        }
123    }
124
125    /// Look up the RGBA color for a given color index.
126    pub fn color(&self, index: C) -> Rgba {
127        self.colors[index.to_index()]
128    }
129}
130
131impl Palette<GbColor> {
132    /// Create a GB palette from a BGP/OBP register value and a base palette.
133    ///
134    /// Each 2-bit field of the register selects a color from the base palette,
135    /// effectively remapping the 4 palette entries.
136    pub fn from_bgp_register(bgp: u8, base_palette: &Self) -> Self {
137        let mut arr = [Rgba::BLACK; 16];
138        for i in 0..4 {
139            let shade = (bgp >> (i * 2)) & 0x03;
140            arr[i] = base_palette.colors[shade as usize];
141        }
142        Self {
143            colors: arr,
144            count: 4,
145            _phantom: PhantomData,
146        }
147    }
148}
149
150impl Palette<GbaColor> {
151    /// Create a GBA palette from a full 16-color array.
152    pub fn from_gba_palette(colors: [Rgba; 16]) -> Self {
153        Self {
154            colors,
155            count: 16,
156            _phantom: PhantomData,
157        }
158    }
159}
160
161// ============================================================================
162// Built-in palettes (all Palette = Palette<GbColor>)
163// ============================================================================
164
165pub const DMG_PALETTE: Palette = Palette {
166    colors: [
167        Rgba::rgb(0x9B, 0xBC, 0x0F), // White (lightest green)
168        Rgba::rgb(0x8B, 0xAC, 0x0F), // Light gray
169        Rgba::rgb(0x30, 0x62, 0x30), // Dark gray
170        Rgba::rgb(0x0F, 0x38, 0x0F), // Black (darkest green)
171        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
172        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
173        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
174    ],
175    count: 4,
176    _phantom: PhantomData,
177};
178
179pub const GRAYSCALE_PALETTE: Palette = Palette {
180    colors: [
181        Rgba::rgb(0xFF, 0xFF, 0xFF),
182        Rgba::rgb(0xAA, 0xAA, 0xAA),
183        Rgba::rgb(0x55, 0x55, 0x55),
184        Rgba::rgb(0x00, 0x00, 0x00),
185        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
186        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
187        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
188    ],
189    count: 4,
190    _phantom: PhantomData,
191};
192
193pub const GRAYSCALE_SPRITE_PALETTE: Palette = Palette {
194    colors: [
195        Rgba::TRANSPARENT,
196        Rgba::rgb(0xAA, 0xAA, 0xAA),
197        Rgba::rgb(0x55, 0x55, 0x55),
198        Rgba::rgb(0x00, 0x00, 0x00),
199        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
200        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
201        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
202    ],
203    count: 4,
204    _phantom: PhantomData,
205};
206
207pub const POCKET_SPRITE_PALETTE: Palette = Palette {
208    colors: [
209        Rgba::TRANSPARENT,
210        Rgba::rgb(0x8B, 0x95, 0x6D),
211        Rgba::rgb(0x4D, 0x53, 0x3C),
212        Rgba::rgb(0x1F, 0x1F, 0x1F),
213        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
214        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
215        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
216    ],
217    count: 4,
218    _phantom: PhantomData,
219};
220
221pub const POCKET_PALETTE: Palette = Palette {
222    colors: [
223        Rgba::rgb(0xC4, 0xCF, 0xA1), // lightest
224        Rgba::rgb(0x8B, 0x95, 0x6D), // light
225        Rgba::rgb(0x4D, 0x53, 0x3C), // dark
226        Rgba::rgb(0x1F, 0x1F, 0x1F), // darkest
227        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
228        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
229        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
230    ],
231    count: 4,
232    _phantom: PhantomData,
233};
234
235pub const HP_BAR_GREEN_PALETTE: Palette = Palette {
236    colors: [
237        Rgba::rgb(0xFF, 0xFF, 0xFF),
238        Rgba::rgb(0xAA, 0xAA, 0xAA),
239        Rgba::rgb(0x00, 0xC8, 0x00),
240        Rgba::rgb(0x00, 0x00, 0x00),
241        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
242        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
243        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
244    ],
245    count: 4,
246    _phantom: PhantomData,
247};
248
249pub const HP_BAR_YELLOW_PALETTE: Palette = Palette {
250    colors: [
251        Rgba::rgb(0xFF, 0xFF, 0xFF),
252        Rgba::rgb(0xAA, 0xAA, 0xAA),
253        Rgba::rgb(0xE8, 0xA8, 0x00),
254        Rgba::rgb(0x00, 0x00, 0x00),
255        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
256        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
257        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
258    ],
259    count: 4,
260    _phantom: PhantomData,
261};
262
263pub const HP_BAR_RED_PALETTE: Palette = Palette {
264    colors: [
265        Rgba::rgb(0xFF, 0xFF, 0xFF),
266        Rgba::rgb(0xAA, 0xAA, 0xAA),
267        Rgba::rgb(0xD8, 0x20, 0x00),
268        Rgba::rgb(0x00, 0x00, 0x00),
269        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
270        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
271        Rgba::BLACK, Rgba::BLACK, Rgba::BLACK, Rgba::BLACK,
272    ],
273    count: 4,
274    _phantom: PhantomData,
275};
276
277pub const DEFAULT_BGP: u8 = 0b11100100; // shade 3,2,1,0 (normal)
278pub const DEFAULT_OBP0: u8 = 0b11010000; // shade 3,1,0,0 (sprite palette 0, color 0 = transparent)
279pub const DEFAULT_OBP1: u8 = 0b11100100; // shade 3,2,1,0
280
281// ============================================================================
282// PaletteState
283// ============================================================================
284
285#[derive(Debug, Clone)]
286pub struct PaletteState {
287    pub base: Palette,
288    pub bgp: u8,
289    pub obp0: u8,
290    pub obp1: u8,
291}
292
293impl PaletteState {
294    pub fn new(base: Palette) -> Self {
295        Self {
296            base,
297            bgp: DEFAULT_BGP,
298            obp0: DEFAULT_OBP0,
299            obp1: DEFAULT_OBP1,
300        }
301    }
302
303    pub fn bg_palette(&self) -> Palette {
304        Palette::from_bgp_register(self.bgp, &self.base)
305    }
306
307    pub fn obj_palette0(&self) -> Palette {
308        let mut pal = Palette::from_bgp_register(self.obp0, &self.base);
309        pal.colors[0] = Rgba::TRANSPARENT;
310        pal
311    }
312
313    pub fn obj_palette1(&self) -> Palette {
314        let mut pal = Palette::from_bgp_register(self.obp1, &self.base);
315        pal.colors[0] = Rgba::TRANSPARENT;
316        pal
317    }
318
319    pub fn white_out(&mut self) {
320        self.bgp = 0x00;
321        self.obp0 = 0x00;
322        self.obp1 = 0x00;
323    }
324
325    pub fn reset_normal(&mut self) {
326        self.bgp = DEFAULT_BGP;
327        self.obp0 = DEFAULT_OBP0;
328    }
329}
330
331impl Default for PaletteState {
332    fn default() -> Self {
333        Self::new(DMG_PALETTE)
334    }
335}
336
337// ============================================================================
338// SGB palette slot table — default palette IDs for classic GB-style games
339// ============================================================================
340
341/// SGB palette IDs — indexes into the game's SuperPalettes.
342///
343/// A convenience default palette-slot table in the style of classic Game Boy
344/// JRPGs (route/town/logo/monster/bar slots). Games are free to define their
345/// own palette ID type and use [`ColorPaletteState`] generically instead.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
347#[repr(u8)]
348pub enum SgbPaletteId {
349    Route = 0x00,
350    HomeTown = 0x01,
351    Viridian = 0x02,
352    Pewter = 0x03,
353    Cerulean = 0x04,
354    Lavender = 0x05,
355    Vermilion = 0x06,
356    Celadon = 0x07,
357    Fuchsia = 0x08,
358    Cinnabar = 0x09,
359    Indigo = 0x0A,
360    Saffron = 0x0B,
361    TownMap = 0x0C,
362    Logo1 = 0x0D,
363    Logo2 = 0x0E,
364    Pal0F = 0x0F,
365    PaleMon = 0x10,
366    BlueMon = 0x11,
367    RedMon = 0x12,
368    CyanMon = 0x13,
369    PurpleMon = 0x14,
370    BrownMon = 0x15,
371    GreenMon = 0x16,
372    PinkMon = 0x17,
373    YellowMon = 0x18,
374    GrayMon = 0x19,
375    Slots1 = 0x1A,
376    Slots2 = 0x1B,
377    Slots3 = 0x1C,
378    Slots4 = 0x1D,
379    Black = 0x1E,
380    GreenBar = 0x1F,
381    YellowBar = 0x20,
382    RedBar = 0x21,
383    Badge = 0x22,
384    Cave = 0x23,
385    CompanyLogo = 0x24,
386}
387
388impl PaletteTrait for SgbPaletteId {}
389
390/// Total number of SGB palettes (NUM_SGB_PALS = 0x25 = 37).
391pub const NUM_SGB_PALS: usize = 0x25;
392
393impl SgbPaletteId {
394    pub fn from_u8(val: u8) -> Option<Self> {
395        if val < NUM_SGB_PALS as u8 {
396            Some(unsafe { core::mem::transmute(val) })
397        } else {
398            None
399        }
400    }
401
402    pub fn as_u8(self) -> u8 {
403        self as u8
404    }
405}
406
407/// Palette command IDs dispatched during palette transitions.
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409#[repr(u8)]
410pub enum SetPalCommand {
411    BattleBlack = 0x00,
412    Battle = 0x01,
413    TownMap = 0x02,
414    StatusScreen = 0x03,
415    Dex = 0x04,
416    Slots = 0x05,
417    TitleScreen = 0x06,
418    MonsterIntro = 0x07,
419    Generic = 0x08,
420    Overworld = 0x09,
421    PartyMenu = 0x0A,
422    WholeScreen = 0x0B,
423    CompanyLogoIntro = 0x0C,
424    TrainerCard = 0x0D,
425}
426
427/// Special command: update HP bar colors in party menu BLK packet.
428pub const SET_PAL_PARTY_MENU_HP_BARS: u8 = 0xFC;
429/// Special command: use the stored default palette command instead.
430pub const SET_PAL_DEFAULT: u8 = 0xFF;
431
432impl SetPalCommand {
433    pub fn from_u8(val: u8) -> Option<Self> {
434        match val {
435            0x00 => Some(Self::BattleBlack),
436            0x01 => Some(Self::Battle),
437            0x02 => Some(Self::TownMap),
438            0x03 => Some(Self::StatusScreen),
439            0x04 => Some(Self::Dex),
440            0x05 => Some(Self::Slots),
441            0x06 => Some(Self::TitleScreen),
442            0x07 => Some(Self::MonsterIntro),
443            0x08 => Some(Self::Generic),
444            0x09 => Some(Self::Overworld),
445            0x0A => Some(Self::PartyMenu),
446            0x0B => Some(Self::WholeScreen),
447            0x0C => Some(Self::CompanyLogoIntro),
448            0x0D => Some(Self::TrainerCard),
449            _ => None,
450        }
451    }
452
453    pub fn as_u8(self) -> u8 {
454        self as u8
455    }
456}
457
458// ============================================================================
459// SGB → Renderer Bridge Functions
460// ============================================================================
461
462/// Convert 5-bit SGB color to 8-bit RGBA.
463/// Uses the standard conversion: `(val << 3) | (val >> 2)`.
464pub fn sgb_color_to_rgba(color: &SgbColor) -> Rgba {
465    let r8 = (color.r << 3) | (color.r >> 2);
466    let g8 = (color.g << 3) | (color.g >> 2);
467    let b8 = (color.b << 3) | (color.b >> 2);
468    Rgba::rgb(r8, g8, b8)
469}
470
471/// Convert an SgbPaletteEntry (4 SgbColors) into a Palette (4 Rgba colors).
472pub fn sgb_entry_to_palette(entry: &SgbPaletteEntry) -> Palette {
473    Palette::new(&[
474        sgb_color_to_rgba(&entry[0]),
475        sgb_color_to_rgba(&entry[1]),
476        sgb_color_to_rgba(&entry[2]),
477        sgb_color_to_rgba(&entry[3]),
478    ])
479}
480
481// ============================================================================
482// Rendering Palette Mode
483// ============================================================================
484
485/// The rendering palette mode.
486/// - `Dmg`: Original Game Boy 4-shade grayscale (green-tinted).
487/// - `Sgb`: Super Game Boy colorized mode (uses SuperPalettes).
488/// - `Grayscale`: Clean grayscale (no green tint).
489/// - `Pocket`: Game Boy Pocket palette.
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub enum PaletteMode {
492    Dmg,
493    Sgb,
494    Grayscale,
495    Pocket,
496}
497
498/// Extended palette state that supports both DMG (4-shade) and SGB (colorized) rendering.
499///
500/// Generic over the game's palette ID type `P` (defaults to the classic
501/// [`SgbPaletteId`] slot table). When in DMG/Grayscale/Pocket mode, uses
502/// `PaletteState` (bgp/obp0/obp1 registers). When in SGB mode, overlays SGB
503/// color palettes on top of the DMG register state.
504pub struct ColorPaletteState<'a, P: PaletteTrait = SgbPaletteId> {
505    /// The base DMG palette register state (always maintained).
506    pub dmg: PaletteState,
507    /// Current palette mode.
508    pub mode: PaletteMode,
509    /// Whether this is the Red (true) or Blue (false) palette set.
510    pub is_red: bool,
511    /// Palette provider for SGB palette ID lookups.
512    pub palette_provider: &'a dyn PaletteProvider<P>,
513    /// Current SGB palette for background (active when mode == Sgb).
514    pub sgb_bg_palette: P,
515    /// Current SGB palette for OBJ0 (sprites).
516    pub sgb_obj0_palette: P,
517    /// Current SGB palette for OBJ1 (sprites).
518    pub sgb_obj1_palette: P,
519    /// The default palette command.
520    pub default_command: SetPalCommand,
521}
522
523impl<'a, P: PaletteTrait> ColorPaletteState<'a, P> {
524    pub fn new(
525        mode: PaletteMode,
526        is_red: bool,
527        palette_provider: &'a dyn PaletteProvider<P>,
528        bg: P,
529        obj0: P,
530        obj1: P,
531    ) -> Self {
532        Self {
533            dmg: PaletteState::default(),
534            mode,
535            is_red,
536            palette_provider,
537            sgb_bg_palette: bg,
538            sgb_obj0_palette: obj0,
539            sgb_obj1_palette: obj1,
540            default_command: SetPalCommand::Generic,
541        }
542    }
543
544    /// Get the effective background palette for rendering.
545    pub fn bg_palette(&self) -> Palette {
546        match self.mode {
547            PaletteMode::Sgb => {
548                sgb_entry_to_palette(&self.palette_provider.sgb_palette_data(
549                    self.sgb_bg_palette,
550                    self.is_red,
551                ))
552            }
553            PaletteMode::Dmg => self.dmg.bg_palette(),
554            PaletteMode::Grayscale => {
555                let mut state = self.dmg.clone();
556                state.base = GRAYSCALE_PALETTE;
557                state.bg_palette()
558            }
559            PaletteMode::Pocket => {
560                let mut state = self.dmg.clone();
561                state.base = POCKET_PALETTE;
562                state.bg_palette()
563            }
564        }
565    }
566
567    /// Get the effective OBJ palette 0 for rendering.
568    pub fn obj_palette0(&self) -> Palette {
569        match self.mode {
570            PaletteMode::Sgb => {
571                sgb_entry_to_palette(&self.palette_provider.sgb_palette_data(
572                    self.sgb_obj0_palette,
573                    self.is_red,
574                ))
575            }
576            PaletteMode::Dmg => self.dmg.obj_palette0(),
577            PaletteMode::Grayscale => {
578                let mut state = self.dmg.clone();
579                state.base = GRAYSCALE_PALETTE;
580                state.obj_palette0()
581            }
582            PaletteMode::Pocket => {
583                let mut state = self.dmg.clone();
584                state.base = POCKET_PALETTE;
585                state.obj_palette0()
586            }
587        }
588    }
589
590    /// Get the effective OBJ palette 1 for rendering.
591    pub fn obj_palette1(&self) -> Palette {
592        match self.mode {
593            PaletteMode::Sgb => {
594                sgb_entry_to_palette(&self.palette_provider.sgb_palette_data(
595                    self.sgb_obj1_palette,
596                    self.is_red,
597                ))
598            }
599            PaletteMode::Dmg => self.dmg.obj_palette1(),
600            PaletteMode::Grayscale => {
601                let mut state = self.dmg.clone();
602                state.base = GRAYSCALE_PALETTE;
603                state.obj_palette1()
604            }
605            PaletteMode::Pocket => {
606                let mut state = self.dmg.clone();
607                state.base = POCKET_PALETTE;
608                state.obj_palette1()
609            }
610        }
611    }
612
613    /// Set the overworld palette based on current map context.
614    pub fn set_overworld_palette(&mut self, tileset: u8, map_id: u8, last_map: u8) {
615        let pal = self
616            .palette_provider
617            .overworld_palette_for(tileset, map_id, last_map);
618        self.sgb_bg_palette = pal;
619        self.default_command = SetPalCommand::Overworld;
620    }
621
622    /// Set battle palettes from the two battlers' palettes and the player HP bar color.
623    ///
624    /// The caller supplies the per-battler palette IDs (including any
625    /// transformed/fainted fallbacks — the renderer does not know a game's
626    /// palette semantics).
627    pub fn set_battle_palette(&mut self, player_pal: P, enemy_pal: P, player_hp_bar_color: u8) {
628        // In battle, BG palette areas are:
629        // - Player HP bar → GreenBar/YellowBar/RedBar based on HP
630        // - Enemy HP bar → GreenBar/YellowBar/RedBar based on HP
631        // - Player mon → species palette
632        // - Enemy mon → species palette
633        // For simplicity, we store the main bg as player HP bar, obj0 as player mon, obj1 as enemy mon.
634        self.sgb_bg_palette = self.palette_provider.hp_bar_to_palette_id(player_hp_bar_color);
635        self.sgb_obj0_palette = player_pal;
636        self.sgb_obj1_palette = enemy_pal;
637        self.default_command = SetPalCommand::Battle;
638    }
639}
640
641impl<'a> ColorPaletteState<'a, SgbPaletteId> {
642    /// Convenience constructor using the classic slot table, defaulting all
643    /// three palette slots to [`SgbPaletteId::Route`].
644    pub fn new_classic(
645        mode: PaletteMode,
646        is_red: bool,
647        palette_provider: &'a dyn PaletteProvider<SgbPaletteId>,
648    ) -> Self {
649        Self::new(
650            mode,
651            is_red,
652            palette_provider,
653            SgbPaletteId::Route,
654            SgbPaletteId::Route,
655            SgbPaletteId::Route,
656        )
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn gb_color_from_u8() {
666        assert_eq!(GbColor::from_u8(0), GbColor::White);
667        assert_eq!(GbColor::from_u8(1), GbColor::LightGray);
668        assert_eq!(GbColor::from_u8(2), GbColor::DarkGray);
669        assert_eq!(GbColor::from_u8(3), GbColor::Black);
670        // Masking
671        assert_eq!(GbColor::from_u8(4), GbColor::White);
672        assert_eq!(GbColor::from_u8(7), GbColor::Black);
673    }
674
675    #[test]
676    fn gb_color_to_index() {
677        assert_eq!(GbColor::White.to_index(), 0);
678        assert_eq!(GbColor::LightGray.to_index(), 1);
679        assert_eq!(GbColor::DarkGray.to_index(), 2);
680        assert_eq!(GbColor::Black.to_index(), 3);
681    }
682
683    #[test]
684    fn gba_color_from_u8() {
685        assert_eq!(GbaColor::from_u8(0).0, 0);
686        assert_eq!(GbaColor::from_u8(15).0, 15);
687        // Masking to 0x0F
688        assert_eq!(GbaColor::from_u8(16).0, 0);
689        assert_eq!(GbaColor::from_u8(31).0, 15);
690        assert_eq!(GbaColor::from_u8(0xFF).0, 15);
691    }
692
693    #[test]
694    fn gba_color_to_index() {
695        assert_eq!(GbaColor(0).to_index(), 0);
696        assert_eq!(GbaColor(7).to_index(), 7);
697        assert_eq!(GbaColor(15).to_index(), 15);
698    }
699
700    #[test]
701    fn palette_color_lookup() {
702        let pal = DMG_PALETTE;
703        assert_eq!(pal.color(GbColor::White), Rgba::rgb(0x9B, 0xBC, 0x0F));
704        assert_eq!(pal.color(GbColor::Black), Rgba::rgb(0x0F, 0x38, 0x0F));
705    }
706
707    #[test]
708    fn palette_new_from_slice() {
709        let colors = [
710            Rgba::rgb(0xFF, 0x00, 0x00),
711            Rgba::rgb(0x00, 0xFF, 0x00),
712            Rgba::rgb(0x00, 0x00, 0xFF),
713            Rgba::TRANSPARENT,
714        ];
715        let pal: Palette = Palette::new(&colors);
716        assert_eq!(pal.count, 4);
717        assert_eq!(pal.color(GbColor::White), Rgba::rgb(0xFF, 0x00, 0x00));
718        assert_eq!(pal.color(GbColor::LightGray), Rgba::rgb(0x00, 0xFF, 0x00));
719        assert_eq!(pal.color(GbColor::DarkGray), Rgba::rgb(0x00, 0x00, 0xFF));
720        assert_eq!(pal.color(GbColor::Black), Rgba::TRANSPARENT);
721    }
722
723    #[test]
724    fn palette_from_bgp_register() {
725        let base = GRAYSCALE_PALETTE;
726        // Default BGP: shade 3,2,1,0 → identity mapping
727        let pal = Palette::from_bgp_register(DEFAULT_BGP, &base);
728        assert_eq!(pal.color(GbColor::White), Rgba::rgb(0xFF, 0xFF, 0xFF));
729        assert_eq!(pal.color(GbColor::LightGray), Rgba::rgb(0xAA, 0xAA, 0xAA));
730        assert_eq!(pal.color(GbColor::DarkGray), Rgba::rgb(0x55, 0x55, 0x55));
731        assert_eq!(pal.color(GbColor::Black), Rgba::rgb(0x00, 0x00, 0x00));
732
733        // All white (bgp=0x00)
734        let pal = Palette::from_bgp_register(0x00, &base);
735        assert_eq!(pal.color(GbColor::White), Rgba::rgb(0xFF, 0xFF, 0xFF));
736        assert_eq!(pal.color(GbColor::Black), Rgba::rgb(0xFF, 0xFF, 0xFF));
737    }
738
739    #[test]
740    fn gba_palette_from_array() {
741        let mut colors = [Rgba::BLACK; 16];
742        colors[0] = Rgba::rgb(0xFF, 0x00, 0x00);
743        colors[15] = Rgba::rgb(0x00, 0x00, 0xFF);
744        let pal = Palette::<GbaColor>::from_gba_palette(colors);
745        assert_eq!(pal.count, 16);
746        assert_eq!(pal.color(GbaColor(0)), Rgba::rgb(0xFF, 0x00, 0x00));
747        assert_eq!(pal.color(GbaColor(15)), Rgba::rgb(0x00, 0x00, 0xFF));
748    }
749
750    #[test]
751    fn palette_state_defaults() {
752        let state = PaletteState::default();
753        assert_eq!(state.bgp, DEFAULT_BGP);
754        assert_eq!(state.obp0, DEFAULT_OBP0);
755        assert_eq!(state.obp1, DEFAULT_OBP1);
756    }
757
758    #[test]
759    fn palette_state_white_out() {
760        let mut state = PaletteState::default();
761        state.white_out();
762        assert_eq!(state.bgp, 0x00);
763        assert_eq!(state.obp0, 0x00);
764    }
765
766    #[test]
767    fn dmg_palette_const_is_valid() {
768        assert_eq!(DMG_PALETTE.count, 4);
769        assert_eq!(DMG_PALETTE.color(GbColor::White), Rgba::rgb(0x9B, 0xBC, 0x0F));
770    }
771
772    #[test]
773    fn grayscale_palette_const_is_valid() {
774        assert_eq!(GRAYSCALE_PALETTE.count, 4);
775        assert_eq!(GRAYSCALE_PALETTE.color(GbColor::White), Rgba::rgb(0xFF, 0xFF, 0xFF));
776    }
777
778    #[test]
779    fn palette_copy_and_eq() {
780        let a = DMG_PALETTE;
781        let b = a; // Copy
782        assert_eq!(a, b);
783    }
784
785    #[test]
786    fn color_palette_state_is_generic_over_custom_palette() {
787        // A game can drive the SGB state with its own palette ID type —
788        // no dependency on the renderer's default SgbPaletteId table.
789        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
790        enum TestPalette {
791            Field,
792            HpBar,
793        }
794        impl PaletteTrait for TestPalette {}
795
796        struct TestProvider;
797        impl PaletteProvider<TestPalette> for TestProvider {
798            fn bg_palette(&self, _: TestPalette) -> [u8; 4] {
799                [0; 4]
800            }
801            fn obj_palette0(&self, _: TestPalette) -> [u8; 4] {
802                [0; 4]
803            }
804            fn obj_palette1(&self, _: TestPalette) -> [u8; 4] {
805                [0; 4]
806            }
807            fn overworld_palette_for(&self, _: u8, _: u8, _: u8) -> TestPalette {
808                TestPalette::Field
809            }
810            fn monster_palette(&self, _: u8) -> TestPalette {
811                TestPalette::Field
812            }
813            fn hp_bar_to_palette_id(&self, _: u8) -> TestPalette {
814                TestPalette::HpBar
815            }
816        }
817
818        let mut state = ColorPaletteState::new(
819            PaletteMode::Sgb,
820            true,
821            &TestProvider,
822            TestPalette::Field,
823            TestPalette::Field,
824            TestPalette::Field,
825        );
826        state.set_overworld_palette(1, 2, 0);
827        assert_eq!(state.sgb_bg_palette, TestPalette::Field);
828        assert_eq!(state.default_command, SetPalCommand::Overworld);
829
830        state.set_battle_palette(TestPalette::Field, TestPalette::Field, 1);
831        assert_eq!(state.sgb_bg_palette, TestPalette::HpBar);
832        assert_eq!(state.default_command, SetPalCommand::Battle);
833
834        // The default `sgb_palette_data` is black → SGB bg renders as black.
835        let pal = state.bg_palette();
836        assert_eq!(pal.color(GbColor::White), Rgba::rgb(0, 0, 0));
837    }
838}