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); 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); 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); 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); 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); }
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}