1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use crate::palette::{GbColor, Palette};
5use crate::asset_provider::ResourceProvider;
6use crate::tile::{TileSet, TILE_PIXELS};
7use crate::FbSurface;
8
9pub use crate::icon::IconKind;
10
11#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
17pub enum IconFrame {
18 Frame1,
19 Frame2,
20}
21
22impl IconFrame {
23 pub fn from_counter(counter: u64, period: u64) -> Self {
27 if period == 0 || (counter / period) % 2 == 0 {
28 IconFrame::Frame1
29 } else {
30 IconFrame::Frame2
31 }
32 }
33}
34
35static CACHE: Mutex<Option<HashMap<(IconKind, IconFrame), &'static TileSet>>> = Mutex::new(None);
38
39struct IconAsset {
40 category: &'static str,
41 filename: &'static str,
42 start_tile: usize,
43 tile_count: usize,
44}
45
46fn asset_for(kind: IconKind, frame: IconFrame) -> IconAsset {
47 match (kind, frame) {
48 (IconKind::Mon, _) => IconAsset { category: "sprites", filename: "monster.png", start_tile: 12, tile_count: 4 },
49 (IconKind::Fairy, _) => IconAsset { category: "sprites", filename: "fairy.png", start_tile: 12, tile_count: 4 },
50 (IconKind::Bird, _) => IconAsset { category: "sprites", filename: "bird.png", start_tile: 12, tile_count: 4 },
51 (IconKind::Water, _) => IconAsset { category: "sprites", filename: "fish.png", start_tile: 0, tile_count: 4 },
52 (IconKind::Ball, _) => IconAsset { category: "sprites", filename: "ball.png", start_tile: 0, tile_count: 4 },
53 (IconKind::Helix, _) => IconAsset { category: "sprites", filename: "ball.png", start_tile: 0, tile_count: 4 },
54 (IconKind::Bug, IconFrame::Frame1) => IconAsset { category: "icons", filename: "bug.png", start_tile: 2, tile_count: 2 },
55 (IconKind::Bug, IconFrame::Frame2) => IconAsset { category: "icons", filename: "bug.png", start_tile: 4, tile_count: 2 },
56 (IconKind::Grass, IconFrame::Frame1) => IconAsset { category: "icons", filename: "plant.png", start_tile: 2, tile_count: 2 },
57 (IconKind::Grass, IconFrame::Frame2) => IconAsset { category: "icons", filename: "plant.png", start_tile: 4, tile_count: 2 },
58 (IconKind::Snake, IconFrame::Frame1) => IconAsset { category: "icons", filename: "snake.png", start_tile: 2, tile_count: 2 },
59 (IconKind::Snake, IconFrame::Frame2) => IconAsset { category: "icons", filename: "snake.png", start_tile: 4, tile_count: 2 },
60 (IconKind::Quadruped, IconFrame::Frame1) => IconAsset { category: "icons", filename: "quadruped.png", start_tile: 2, tile_count: 2 },
61 (IconKind::Quadruped, IconFrame::Frame2) => IconAsset { category: "icons", filename: "quadruped.png", start_tile: 4, tile_count: 2 },
62 }
63}
64
65fn extract_16wide(source: &TileSet, start: usize) -> TileSet {
66 let indices = [start, start + 2, start + 1, start + 3];
67 let mut ts = TileSet::blank(4);
68 for (i, &idx) in indices.iter().enumerate() {
69 ts.set(i, source.get(idx).clone());
70 }
71 ts
72}
73
74fn extract_8wide(source: &TileSet, start: usize) -> TileSet {
81 let top = source.get(start).clone();
82 let bot = source.get(start + 1).clone();
83 let top_flip = top.flip_x();
84 let bot_flip = bot.flip_x();
85 let mut ts = TileSet::blank(4);
86 ts.set(0, top);
87 ts.set(1, bot);
88 ts.set(2, top_flip);
89 ts.set(3, bot_flip);
90 ts
91}
92
93pub fn load_mon_icon_tiles(
94 provider: &mut dyn ResourceProvider,
95 kind: IconKind,
96 frame: IconFrame,
97) -> Result<&TileSet, String> {
98 let key = (kind, frame);
99 {
100 let guard = CACHE.lock().map_err(|e| format!("cache lock poisoned: {}", e))?;
101 if let Some(ref map) = *guard {
102 if let Some(tiles) = map.get(&key) {
103 return Ok(tiles);
104 }
105 }
106 }
107
108 let asset = asset_for(kind, frame);
109 let loaded = provider
110 .load_asset(asset.category, asset.filename)
111 .map_err(|e| format!("failed to load {}: {}", asset.filename, e))?;
112
113 let source = loaded;
114 if source.len() < asset.start_tile + asset.tile_count {
115 if frame == IconFrame::Frame2 && asset.tile_count == 2 {
118 return load_mon_icon_tiles(provider, kind, IconFrame::Frame1);
119 }
120 return Err(format!(
121 "{} has only {} tiles, need at least {}",
122 asset.filename,
123 source.len(),
124 asset.start_tile + asset.tile_count
125 ));
126 }
127
128 let icon_tiles = if asset.tile_count == 2 {
129 extract_8wide(source, asset.start_tile)
130 } else {
131 extract_16wide(source, asset.start_tile)
132 };
133
134 let leaked: &'static TileSet = Box::leak(Box::new(icon_tiles));
135 let mut guard = CACHE.lock().map_err(|e| format!("cache lock poisoned: {}", e))?;
136 let map = guard.get_or_insert_with(HashMap::new);
137 map.insert(key, leaked);
138 Ok(leaked)
139}
140
141pub fn draw_mon_icon(
142 fb: &mut impl FbSurface,
143 tiles: &TileSet,
144 x: u32,
145 y: u32,
146 palette: &Palette,
147) {
148 let fb_h = fb.height();
149 let fb_w = fb.width();
150 let positions = [(0u32, 0u32), (0, 1), (1, 0), (1, 1)];
151 for (i, (col, row)) in positions.iter().enumerate() {
152 let tile = tiles.get(i);
153 let base_x = x + col * TILE_PIXELS as u32;
154 let base_y = y + row * TILE_PIXELS as u32;
155 for r in 0..TILE_PIXELS {
156 let screen_y = base_y + r as u32;
157 if screen_y >= fb_h {
158 continue;
159 }
160 for c in 0..TILE_PIXELS {
161 let screen_x = base_x + c as u32;
162 if screen_x >= fb_w {
163 continue;
164 }
165 let color_idx = tile.get(r, c);
166 if color_idx == 0 {
167 continue;
168 }
169 fb.set_pixel(screen_x, screen_y, palette.color(GbColor::from_u8(color_idx)));
170 }
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use dotzuki_engine::render::Rgba;
179
180 #[test]
181 fn frame_from_counter_alternates() {
182 assert_eq!(IconFrame::from_counter(0, 16), IconFrame::Frame1);
183 assert_eq!(IconFrame::from_counter(15, 16), IconFrame::Frame1);
184 assert_eq!(IconFrame::from_counter(16, 16), IconFrame::Frame2);
185 assert_eq!(IconFrame::from_counter(31, 16), IconFrame::Frame2);
186 assert_eq!(IconFrame::from_counter(32, 16), IconFrame::Frame1);
187 }
188
189 #[test]
190 fn draw_mon_icon_respects_transparent_color0() {
191 let mut fb = crate::FrameBuffer::new(dotzuki_engine::render_config::RenderConfig::new(160, 144), Rgba::BLACK);
192 let ts = TileSet::blank(4);
193 draw_mon_icon(&mut fb, &ts, 0, 0, &crate::palette::GRAYSCALE_SPRITE_PALETTE);
194 for dy in 0..16u32 {
196 for dx in 0..16u32 {
197 assert_eq!(fb.get_pixel(dx, dy), Some(Rgba::BLACK),
198 "blank icon area should remain background color at ({},{})", dx, dy);
199 }
200 }
201 }
202}