Skip to main content

dotzuki_renderer/
menu.rs

1use crate::charmap::encode_char;
2use crate::text_renderer::{write_tiles_at, ScreenTileBuffer};
3use crate::textbox::{TextBoxFrame, TILE_SPACE};
4
5pub const TILE_CURSOR_FILLED: u8 = 0xED;
6pub const TILE_CURSOR_UNFILLED: u8 = 0xEC;
7pub const TILE_SCROLL_INDICATOR: u8 = 0xEE;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct MenuCursor {
11    pub x: u32,
12    pub y: u32,
13    pub tile_behind: u8,
14    pub visible: bool,
15}
16
17impl MenuCursor {
18    pub fn new(x: u32, y: u32) -> Self {
19        Self {
20            x,
21            y,
22            tile_behind: TILE_SPACE,
23            visible: false,
24        }
25    }
26
27    pub fn place(&mut self, buf: &mut ScreenTileBuffer) {
28        let current = buf.get(self.x, self.y);
29        if current != TILE_CURSOR_FILLED {
30            self.tile_behind = current;
31        }
32        buf.set(self.x, self.y, TILE_CURSOR_FILLED);
33        self.visible = true;
34    }
35
36    pub fn erase(&self, buf: &mut ScreenTileBuffer) {
37        buf.set(self.x, self.y, TILE_SPACE);
38    }
39
40    pub fn place_unfilled(&self, buf: &mut ScreenTileBuffer) {
41        buf.set(self.x, self.y, TILE_CURSOR_UNFILLED);
42    }
43
44    pub fn restore(&self, buf: &mut ScreenTileBuffer) {
45        buf.set(self.x, self.y, self.tile_behind);
46    }
47
48    pub fn move_to(&mut self, buf: &mut ScreenTileBuffer, new_x: u32, new_y: u32) {
49        if self.visible {
50            self.restore(buf);
51        }
52        self.x = new_x;
53        self.y = new_y;
54        self.place(buf);
55    }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct MenuLayout {
60    pub frame: TextBoxFrame,
61    pub top_item_x: u32,
62    pub top_item_y: u32,
63    pub item_count: u32,
64    pub double_spaced: bool,
65    pub current_item: u32,
66    pub wrapping: bool,
67}
68
69impl MenuLayout {
70    pub fn new(frame: TextBoxFrame, top_item_x: u32, top_item_y: u32, item_count: u32) -> Self {
71        Self {
72            frame,
73            top_item_x,
74            top_item_y,
75            item_count,
76            double_spaced: true,
77            current_item: 0,
78            wrapping: false,
79        }
80    }
81
82    pub fn draw_frame(&self, buf: &mut ScreenTileBuffer) {
83        self.frame.draw_frame(buf);
84    }
85
86    pub fn cursor_x(&self) -> u32 {
87        self.top_item_x.saturating_sub(1)
88    }
89
90    pub fn cursor_y_for_item(&self, item_index: u32) -> u32 {
91        let spacing = if self.double_spaced { 2 } else { 1 };
92        self.top_item_y + item_index * spacing
93    }
94
95    pub fn item_position(&self, item_index: u32) -> (u32, u32) {
96        (self.top_item_x, self.cursor_y_for_item(item_index))
97    }
98
99    pub fn place_cursor(&self, buf: &mut ScreenTileBuffer) -> MenuCursor {
100        let cx = self.cursor_x();
101        let cy = self.cursor_y_for_item(self.current_item);
102        let mut cursor = MenuCursor::new(cx, cy);
103        cursor.place(buf);
104        cursor
105    }
106
107    pub fn move_cursor_up(&mut self) -> bool {
108        if self.current_item > 0 {
109            self.current_item -= 1;
110            true
111        } else if self.wrapping && self.item_count > 0 {
112            self.current_item = self.item_count - 1;
113            true
114        } else {
115            false
116        }
117    }
118
119    pub fn move_cursor_down(&mut self) -> bool {
120        if self.current_item + 1 < self.item_count {
121            self.current_item += 1;
122            true
123        } else if self.wrapping && self.item_count > 0 {
124            self.current_item = 0;
125            true
126        } else {
127            false
128        }
129    }
130
131    pub fn place_item_text(&self, buf: &mut ScreenTileBuffer, item_index: u32, tiles: &[u8]) {
132        let (x, y) = self.item_position(item_index);
133        write_tiles_at(buf, x, y, tiles);
134    }
135}
136
137pub struct StartMenuRenderer;
138
139impl StartMenuRenderer {
140    pub fn draw(buf: &mut ScreenTileBuffer, has_dex: bool) -> MenuLayout {
141        let height = if has_dex { 16 } else { 14 };
142        let frame = TextBoxFrame::new(10, 0, 10, height);
143        frame.draw_frame(buf);
144
145        let item_x = 12;
146        let item_y = 2;
147        let mut y = item_y;
148
149        if has_dex {
150            write_tiles_at(buf, item_x, y, &Self::encode_str("DEX"));
151            y += 2;
152        }
153        write_tiles_at(buf, item_x, y, &Self::encode_str("MONSTER"));
154        y += 2;
155        write_tiles_at(buf, item_x, y, &Self::encode_str("ITEM"));
156        y += 2;
157        write_tiles_at(buf, item_x, y, &Self::encode_str("SAVE"));
158        y += 2;
159        write_tiles_at(buf, item_x, y, &Self::encode_str("OPTION"));
160        y += 2;
161        write_tiles_at(buf, item_x, y, &Self::encode_str("EXIT"));
162
163        let item_count = if has_dex { 7 } else { 6 };
164        MenuLayout::new(frame, item_x, item_y, item_count)
165    }
166
167    pub fn draw_with_player_name(
168        buf: &mut ScreenTileBuffer,
169        has_dex: bool,
170        player_name: &[u8],
171        is_link: bool,
172    ) -> MenuLayout {
173        let height = if has_dex { 16 } else { 14 };
174        let frame = TextBoxFrame::new(10, 0, 10, height);
175        frame.draw_frame(buf);
176
177        let item_x = 12;
178        let item_y = 2;
179        let mut y = item_y;
180
181        if has_dex {
182            write_tiles_at(buf, item_x, y, &Self::encode_str("DEX"));
183            y += 2;
184        }
185        write_tiles_at(buf, item_x, y, &Self::encode_str("MONSTER"));
186        y += 2;
187        write_tiles_at(buf, item_x, y, &Self::encode_str("ITEM"));
188        y += 2;
189        write_tiles_at(buf, item_x, y, player_name);
190        y += 2;
191        let save_or_reset = if is_link { "RESET" } else { "SAVE" };
192        write_tiles_at(buf, item_x, y, &Self::encode_str(save_or_reset));
193        y += 2;
194        write_tiles_at(buf, item_x, y, &Self::encode_str("OPTION"));
195        y += 2;
196        write_tiles_at(buf, item_x, y, &Self::encode_str("EXIT"));
197
198        let item_count = if has_dex { 7 } else { 6 };
199        MenuLayout::new(frame, item_x, item_y, item_count)
200    }
201
202    fn encode_str(s: &str) -> Vec<u8> {
203        s.chars().filter_map(encode_char).collect()
204    }
205}
206
207pub fn encode_menu_str(s: &str) -> Vec<u8> {
208    s.chars().filter_map(encode_char).collect()
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum TextBoxTemplateId {
213    MessageBox,
214    MenuTemplate03,
215    MenuTemplate07,
216    ListMenuBox,
217    MenuTemplate10,
218    MonSpritePopup,
219    BattleMenu,
220    SafariBattleMenu,
221    UseTossMenu,
222    SwitchStatsCancelMenu,
223    BuySellQuitMenu,
224    MoneyBox,
225}
226
227impl TextBoxTemplateId {
228    pub fn coords(self) -> (u32, u32, u32, u32) {
229        match self {
230            Self::MessageBox => (0, 12, 19, 17),
231            Self::MenuTemplate03 => (0, 0, 19, 14),
232            Self::MenuTemplate07 => (0, 0, 11, 6),
233            Self::ListMenuBox => (4, 2, 19, 12),
234            Self::MenuTemplate10 => (7, 0, 19, 17),
235            Self::MonSpritePopup => (6, 4, 14, 13),
236            Self::BattleMenu => (8, 12, 19, 17),
237            Self::SafariBattleMenu => (0, 12, 19, 17),
238            Self::UseTossMenu => (13, 10, 19, 14),
239            Self::SwitchStatsCancelMenu => (11, 11, 19, 17),
240            Self::BuySellQuitMenu => (0, 0, 10, 6),
241            Self::MoneyBox => (11, 0, 19, 2),
242        }
243    }
244
245    pub fn to_frame(self) -> TextBoxFrame {
246        let (x1, y1, x2, y2) = self.coords();
247        let width = x2 - x1 + 1;
248        let height = y2 - y1 + 1;
249        TextBoxFrame::new(x1, y1, width, height)
250    }
251
252    pub fn draw(self, buf: &mut ScreenTileBuffer) {
253        self.to_frame().draw_frame(buf);
254    }
255
256    pub fn draw_with_text(self, buf: &mut ScreenTileBuffer) {
257        self.draw(buf);
258        match self {
259            Self::BattleMenu => {
260                let tiles_line1 = encode_menu_str("FIGHT  ");
261                let tiles_line2 = encode_menu_str("ITEM  RUN");
262                write_tiles_at(buf, 10, 14, &tiles_line1);
263                write_tiles_at(buf, 10, 16, &tiles_line2);
264            }
265            Self::SafariBattleMenu => {
266                let tiles_line1 = encode_menu_str("BALL");
267                let tiles_line2 = encode_menu_str("THROW ROCK  RUN");
268                write_tiles_at(buf, 2, 14, &tiles_line1);
269                buf.set(4, 14, 0xF1); // ×
270                write_tiles_at(buf, 14, 14, &encode_menu_str("BAIT"));
271                write_tiles_at(buf, 2, 16, &tiles_line2);
272            }
273            Self::UseTossMenu => {
274                write_tiles_at(buf, 15, 11, &encode_menu_str("USE"));
275                write_tiles_at(buf, 15, 13, &encode_menu_str("TOSS"));
276            }
277            Self::SwitchStatsCancelMenu => {
278                write_tiles_at(buf, 13, 12, &encode_menu_str("SWITCH"));
279                write_tiles_at(buf, 13, 14, &encode_menu_str("STATS"));
280                write_tiles_at(buf, 13, 16, &encode_menu_str("CANCEL"));
281            }
282            Self::BuySellQuitMenu => {
283                write_tiles_at(buf, 2, 1, &encode_menu_str("BUY"));
284                write_tiles_at(buf, 2, 3, &encode_menu_str("SELL"));
285                write_tiles_at(buf, 2, 5, &encode_menu_str("QUIT"));
286            }
287            Self::MoneyBox => {
288                write_tiles_at(buf, 13, 0, &encode_menu_str("MONEY"));
289            }
290            _ => {}
291        }
292    }
293}
294
295#[derive(Debug, Clone)]
296pub struct TwoOptionMenu {
297    pub frame: TextBoxFrame,
298    pub option1_tiles: Vec<u8>,
299    pub option2_tiles: Vec<u8>,
300    pub text_x: u32,
301    pub text_y: u32,
302    pub current_item: u32,
303    saved_tiles: Vec<u8>,
304    saved_x: u32,
305    saved_y: u32,
306    saved_w: u32,
307    saved_h: u32,
308}
309
310impl TwoOptionMenu {
311    pub fn yes_no(x: u32, y: u32) -> Self {
312        let frame = TextBoxFrame::new(x, y, 6, 5);
313        Self {
314            frame,
315            option1_tiles: encode_menu_str("YES"),
316            option2_tiles: encode_menu_str("NO"),
317            text_x: x + 2,
318            text_y: y + 1,
319            current_item: 0,
320            saved_tiles: Vec::new(),
321            saved_x: x,
322            saved_y: y,
323            saved_w: 6,
324            saved_h: 5,
325        }
326    }
327
328    pub fn custom(frame: TextBoxFrame, opt1: &str, opt2: &str) -> Self {
329        Self {
330            text_x: frame.x + 2,
331            text_y: frame.y + 1,
332            option1_tiles: encode_menu_str(opt1),
333            option2_tiles: encode_menu_str(opt2),
334            current_item: 0,
335            saved_tiles: Vec::new(),
336            saved_x: frame.x,
337            saved_y: frame.y,
338            saved_w: frame.width,
339            saved_h: frame.height,
340            frame,
341        }
342    }
343
344    pub fn save_area(&mut self, buf: &ScreenTileBuffer) {
345        self.saved_tiles.clear();
346        for row in 0..self.saved_h {
347            for col in 0..self.saved_w {
348                self.saved_tiles
349                    .push(buf.get(self.saved_x + col, self.saved_y + row));
350            }
351        }
352    }
353
354    pub fn restore_area(&self, buf: &mut ScreenTileBuffer) {
355        let mut i = 0;
356        for row in 0..self.saved_h {
357            for col in 0..self.saved_w {
358                if i < self.saved_tiles.len() {
359                    buf.set(self.saved_x + col, self.saved_y + row, self.saved_tiles[i]);
360                    i += 1;
361                }
362            }
363        }
364    }
365
366    pub fn draw(&self, buf: &mut ScreenTileBuffer) {
367        self.frame.draw_frame(buf);
368        write_tiles_at(buf, self.text_x, self.text_y, &self.option1_tiles);
369        write_tiles_at(buf, self.text_x, self.text_y + 2, &self.option2_tiles);
370    }
371
372    pub fn cursor_position(&self) -> (u32, u32) {
373        let cy = self.text_y + self.current_item * 2;
374        (self.text_x - 1, cy)
375    }
376}
377
378#[derive(Debug, Clone)]
379pub struct ListMenuRenderer {
380    pub frame: TextBoxFrame,
381    pub visible_count: u32,
382    pub scroll_offset: u32,
383    pub total_count: u32,
384    pub current_item: u32,
385    pub item_x: u32,
386    pub item_start_y: u32,
387    pub row_spacing: u32,
388}
389
390impl ListMenuRenderer {
391    pub fn new(total_count: u32) -> Self {
392        let frame = TextBoxFrame::new(4, 2, 16, 11);
393        Self {
394            frame,
395            visible_count: 4,
396            scroll_offset: 0,
397            total_count,
398            current_item: 0,
399            item_x: 6,
400            item_start_y: 4,
401            row_spacing: 2,
402        }
403    }
404
405    pub fn draw_frame(&self, buf: &mut ScreenTileBuffer) {
406        self.frame.draw_frame(buf);
407    }
408
409    pub fn clear_items(&self, buf: &mut ScreenTileBuffer) {
410        for row in 0..self.visible_count {
411            let y = self.item_start_y + row * self.row_spacing;
412            for col in self.item_x..(self.frame.x + self.frame.width - 1) {
413                buf.set(col, y, TILE_SPACE);
414                if y + 1 < buf.height_tiles {
415                    buf.set(col, y + 1, TILE_SPACE);
416                }
417            }
418        }
419    }
420
421    pub fn draw_item(&self, buf: &mut ScreenTileBuffer, visible_index: u32, name_tiles: &[u8]) {
422        let y = self.item_start_y + visible_index * self.row_spacing;
423        write_tiles_at(buf, self.item_x, y, name_tiles);
424    }
425
426    pub fn draw_cancel(&self, buf: &mut ScreenTileBuffer, visible_index: u32) {
427        let y = self.item_start_y + visible_index * self.row_spacing;
428        write_tiles_at(buf, self.item_x, y, &encode_menu_str("CANCEL"));
429    }
430
431    pub fn draw_scroll_indicator(&self, buf: &mut ScreenTileBuffer) {
432        let has_more = self.scroll_offset + self.visible_count < self.total_count;
433        if has_more {
434            let y = self.item_start_y + (self.visible_count - 1) * self.row_spacing + 1;
435            let x = self.frame.x + self.frame.width - 2;
436            if x < buf.width_tiles && y < buf.height_tiles {
437                buf.set(x, y, TILE_SCROLL_INDICATOR);
438            }
439        }
440    }
441
442    pub fn draw_item_quantity(&self, buf: &mut ScreenTileBuffer, visible_index: u32, quantity: u8) {
443        let y = self.item_start_y + visible_index * self.row_spacing + 1;
444        let x = self.item_x + 8;
445        if x < buf.width_tiles && y < buf.height_tiles {
446            buf.set(x, y, 0xF1); // ×
447            let tens = quantity / 10;
448            let ones = quantity % 10;
449            if tens > 0 {
450                buf.set(x + 1, y, 0xF6 + tens);
451            }
452            buf.set(x + 2, y, 0xF6 + ones);
453        }
454    }
455
456    pub fn cursor_position(&self) -> (u32, u32) {
457        let relative = self.current_item.saturating_sub(self.scroll_offset);
458        let y = self.item_start_y + relative * self.row_spacing;
459        (self.item_x - 1, y)
460    }
461
462    pub fn scroll_down(&mut self) -> bool {
463        if self.scroll_offset + self.visible_count <= self.total_count {
464            self.scroll_offset += 1;
465            true
466        } else {
467            false
468        }
469    }
470
471    pub fn scroll_up(&mut self) -> bool {
472        if self.scroll_offset > 0 {
473            self.scroll_offset -= 1;
474            true
475        } else {
476            false
477        }
478    }
479}
480
481pub struct PartyMenuRenderer;
482
483impl PartyMenuRenderer {
484    pub const NAME_X: u32 = 3;
485    pub const FIRST_ROW_Y: u32 = 0;
486    pub const ROW_HEIGHT: u32 = 2;
487    pub const LEVEL_OFFSET_X: u32 = 10;
488    pub const STATUS_OFFSET_X: u32 = 14;
489    pub const HP_BAR_OFFSET_X: u32 = 4;
490    pub const HP_BAR_OFFSET_Y: u32 = 1;
491
492    pub fn draw_mon_entry(buf: &mut ScreenTileBuffer, index: u32, name_tiles: &[u8]) {
493        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT;
494        write_tiles_at(buf, Self::NAME_X, y, name_tiles);
495    }
496
497    pub fn draw_level(buf: &mut ScreenTileBuffer, index: u32, level: u8) {
498        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT;
499        let x = Self::NAME_X + Self::LEVEL_OFFSET_X;
500        buf.set(x, y, 0x6E); // "Lv" tile
501        if level >= 100 {
502            let h = level / 100;
503            let t = (level % 100) / 10;
504            let o = level % 10;
505            buf.set(x + 1, y, 0xF6 + h);
506            buf.set(x + 2, y, 0xF6 + t);
507            buf.set(x + 3, y, 0xF6 + o);
508        } else if level >= 10 {
509            let t = level / 10;
510            let o = level % 10;
511            buf.set(x + 1, y, 0xF6 + t);
512            buf.set(x + 2, y, 0xF6 + o);
513        } else {
514            buf.set(x + 1, y, 0xF6 + level);
515        }
516    }
517
518    pub fn draw_status(buf: &mut ScreenTileBuffer, index: u32, status_tiles: &[u8]) {
519        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT;
520        let x = Self::NAME_X + Self::STATUS_OFFSET_X;
521        write_tiles_at(buf, x, y, status_tiles);
522    }
523
524    pub fn hp_bar_position(index: u32) -> (u32, u32) {
525        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT + Self::HP_BAR_OFFSET_Y;
526        let x = Self::NAME_X + Self::HP_BAR_OFFSET_X;
527        (x, y)
528    }
529
530    pub fn draw_hp_bar(
531        buf: &mut ScreenTileBuffer,
532        index: u32,
533        current_hp: u16,
534        max_hp: u16,
535        bar_width: u32,
536    ) {
537        let (x, y) = Self::hp_bar_position(index);
538        let filled = if max_hp == 0 {
539            0
540        } else {
541            ((current_hp as u32) * bar_width / (max_hp as u32)).min(bar_width)
542        };
543        buf.set(x, y, 0x71); // HP label tile
544        for i in 0..bar_width {
545            let tile = if i < filled { 0x72 } else { 0x73 };
546            buf.set(x + 1 + i, y, tile);
547        }
548        buf.set(x + 1 + bar_width, y, 0x6D); // end cap
549    }
550
551    pub fn draw_hp_text(
552        buf: &mut ScreenTileBuffer,
553        index: u32,
554        current_hp: u16,
555        max_hp: u16,
556        bar_width: u32,
557    ) {
558        let (x, y) = Self::hp_bar_position(index);
559        let text_x = x + 2 + bar_width;
560        let cur_str = format!("{:>3}", current_hp);
561        let max_str = format!("{:>3}", max_hp);
562        let combined = format!("{}/ {}", cur_str, max_str);
563        let tiles: Vec<u8> = combined
564            .chars()
565            .filter_map(encode_char)
566            .collect();
567        write_tiles_at(buf, text_x, y, &tiles);
568    }
569
570    pub fn draw_unfilled_arrow(buf: &mut ScreenTileBuffer, index: u32) {
571        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT;
572        buf.set(0, y, TILE_CURSOR_UNFILLED);
573    }
574
575    pub fn cursor_position(index: u32) -> (u32, u32) {
576        let y = Self::FIRST_ROW_Y + index * Self::ROW_HEIGHT;
577        (0, y)
578    }
579}