1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum KeyCode {
12 A, B, C, D, E, F, G, H, I, J, K, L, M,
13 N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
14 Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9,
15 F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
16 Up, Down, Left, Right,
17 Enter, Escape, Space, Tab, Backspace, Delete,
18 LeftShift, RightShift, LeftCtrl, RightCtrl, LeftAlt, RightAlt,
19 Home, End, PageUp, PageDown,
20 Comma, Period, Slash, Backslash, Semicolon, Apostrophe,
21 LeftBracket, RightBracket, Grave, Minus, Equals,
22 MouseLeft, MouseRight, MouseMiddle,
23 GamepadA, GamepadB, GamepadX, GamepadY,
24 GamepadStart, GamepadSelect,
25 GamepadDPadUp, GamepadDPadDown, GamepadDPadLeft, GamepadDPadRight,
26}
27
28impl KeyCode {
29 pub fn display_name(&self) -> &str {
30 match self {
31 KeyCode::Up => "Up",
32 KeyCode::Down => "Down",
33 KeyCode::Left => "Left",
34 KeyCode::Right => "Right",
35 KeyCode::Enter => "Enter",
36 KeyCode::Escape => "Escape",
37 KeyCode::Space => "Space",
38 KeyCode::Tab => "Tab",
39 KeyCode::Backspace => "Backspace",
40 KeyCode::A => "A",
41 KeyCode::B => "B",
42 KeyCode::C => "C",
43 KeyCode::D => "D",
44 KeyCode::W => "W",
45 KeyCode::S => "S",
46 _ => "?",
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
54pub enum InputEvent {
55 KeyDown(KeyCode),
56 KeyUp(KeyCode),
57 CharInput(char),
58 MouseMove { x: f32, y: f32 },
59 MouseButton { button: KeyCode, pressed: bool },
60 MouseScroll { delta: f32 },
61 GamepadButton { button: KeyCode, pressed: bool },
62 GamepadAxis { axis: u8, value: f32 },
63}
64
65#[derive(Debug, Clone)]
68pub struct MenuRenderCtx {
69 pub width: u32,
70 pub height: u32,
71 pub time: f32,
72 pub dt: f32,
73 pub frame: u64,
74}
75
76impl MenuRenderCtx {
77 pub fn new(width: u32, height: u32) -> Self {
78 Self {
79 width,
80 height,
81 time: 0.0,
82 dt: 0.016,
83 frame: 0,
84 }
85 }
86
87 pub fn center_x(&self) -> u32 {
88 self.width / 2
89 }
90
91 pub fn center_y(&self) -> u32 {
92 self.height / 2
93 }
94}
95
96#[derive(Debug, Clone)]
99pub struct MenuCell {
100 pub ch: char,
101 pub fg: (u8, u8, u8),
102 pub bg: (u8, u8, u8),
103 pub bold: bool,
104 pub blink: bool,
105}
106
107impl MenuCell {
108 pub fn new(ch: char) -> Self {
109 Self { ch, fg: (255, 255, 255), bg: (0, 0, 0), bold: false, blink: false }
110 }
111
112 pub fn with_fg(mut self, r: u8, g: u8, b: u8) -> Self {
113 self.fg = (r, g, b);
114 self
115 }
116
117 pub fn with_bg(mut self, r: u8, g: u8, b: u8) -> Self {
118 self.bg = (r, g, b);
119 self
120 }
121
122 pub fn bold(mut self) -> Self {
123 self.bold = true;
124 self
125 }
126}
127
128pub struct MenuBuffer {
131 pub cells: Vec<Vec<MenuCell>>,
132 pub width: u32,
133 pub height: u32,
134}
135
136impl MenuBuffer {
137 pub fn new(width: u32, height: u32) -> Self {
138 let cells = (0..height).map(|_| {
139 (0..width).map(|_| MenuCell::new(' ')).collect()
140 }).collect();
141 Self { cells, width, height }
142 }
143
144 pub fn clear(&mut self) {
145 for row in &mut self.cells {
146 for cell in row {
147 *cell = MenuCell::new(' ');
148 }
149 }
150 }
151
152 pub fn put(&mut self, x: u32, y: u32, cell: MenuCell) {
153 if x < self.width && y < self.height {
154 self.cells[y as usize][x as usize] = cell;
155 }
156 }
157
158 pub fn put_str(&mut self, x: u32, y: u32, s: &str, fg: (u8, u8, u8)) {
159 for (i, ch) in s.chars().enumerate() {
160 self.put(x + i as u32, y, MenuCell::new(ch).with_fg(fg.0, fg.1, fg.2));
161 }
162 }
163
164 pub fn put_str_bold(&mut self, x: u32, y: u32, s: &str, fg: (u8, u8, u8)) {
165 for (i, ch) in s.chars().enumerate() {
166 self.put(x + i as u32, y, MenuCell::new(ch).with_fg(fg.0, fg.1, fg.2).bold());
167 }
168 }
169
170 pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, ch: char, bg: (u8, u8, u8)) {
171 for dy in 0..h {
172 for dx in 0..w {
173 let mut cell = MenuCell::new(ch);
174 cell.bg = bg;
175 self.put(x + dx, y + dy, cell);
176 }
177 }
178 }
179
180 pub fn draw_box(&mut self, x: u32, y: u32, w: u32, h: u32, fg: (u8, u8, u8)) {
181 if w < 2 || h < 2 { return; }
182 self.put(x, y, MenuCell::new('┌').with_fg(fg.0, fg.1, fg.2));
184 self.put(x + w - 1, y, MenuCell::new('┐').with_fg(fg.0, fg.1, fg.2));
185 self.put(x, y + h - 1, MenuCell::new('└').with_fg(fg.0, fg.1, fg.2));
186 self.put(x + w - 1, y + h - 1, MenuCell::new('┘').with_fg(fg.0, fg.1, fg.2));
187 for dx in 1..w - 1 {
189 self.put(x + dx, y, MenuCell::new('─').with_fg(fg.0, fg.1, fg.2));
190 self.put(x + dx, y + h - 1, MenuCell::new('─').with_fg(fg.0, fg.1, fg.2));
191 }
192 for dy in 1..h - 1 {
194 self.put(x, y + dy, MenuCell::new('│').with_fg(fg.0, fg.1, fg.2));
195 self.put(x + w - 1, y + dy, MenuCell::new('│').with_fg(fg.0, fg.1, fg.2));
196 }
197 }
198}
199
200pub enum MenuAction {
203 None,
204 Push(Box<dyn MenuScreen>),
205 Pop,
206 PopToRoot,
207 Quit,
208 StartGame { difficulty: super::DifficultyPreset, class: CharacterClass, name: String },
209 LoadGame { slot: usize },
210 OpenSettings,
211 ApplySettings,
212 ReturnToMainMenu,
213 ShowCredits,
214 Retry,
215}
216
217pub trait MenuScreen: Send + Sync {
220 fn name(&self) -> &str;
221 fn render(&self, ctx: &MenuRenderCtx, buf: &mut MenuBuffer);
222 fn handle_input(&mut self, input: &InputEvent) -> MenuAction;
223 fn on_push(&mut self) {}
224 fn on_pop(&mut self) {}
225 fn tooltip(&self) -> Option<&str> { None }
226 fn update(&mut self, _dt: f32) {}
227}
228
229pub struct MenuStack {
232 screens: Vec<Box<dyn MenuScreen>>,
233}
234
235impl MenuStack {
236 pub fn new() -> Self {
237 Self { screens: Vec::new() }
238 }
239
240 pub fn push(&mut self, mut screen: Box<dyn MenuScreen>) {
241 screen.on_push();
242 self.screens.push(screen);
243 }
244
245 pub fn pop(&mut self) -> Option<Box<dyn MenuScreen>> {
246 if let Some(mut screen) = self.screens.pop() {
247 screen.on_pop();
248 Some(screen)
249 } else {
250 None
251 }
252 }
253
254 pub fn pop_to_root(&mut self) {
255 while self.screens.len() > 1 {
256 if let Some(mut s) = self.screens.pop() {
257 s.on_pop();
258 }
259 }
260 }
261
262 pub fn current(&self) -> Option<&dyn MenuScreen> {
263 self.screens.last().map(|s| s.as_ref())
264 }
265
266 pub fn current_mut(&mut self) -> Option<&mut Box<dyn MenuScreen>> {
267 self.screens.last_mut()
268 }
269
270 pub fn is_empty(&self) -> bool {
271 self.screens.is_empty()
272 }
273
274 pub fn depth(&self) -> usize {
275 self.screens.len()
276 }
277
278 pub fn update(&mut self, dt: f32) {
279 if let Some(s) = self.screens.last_mut() {
280 s.update(dt);
281 }
282 }
283
284 pub fn handle_input(&mut self, input: &InputEvent) -> Option<MenuAction> {
285 let action = self.screens.last_mut()?.handle_input(input);
286 Some(action)
287 }
288
289 pub fn render(&self, ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
290 if let Some(s) = self.screens.last() {
291 s.render(ctx, buf);
292 }
293 }
294
295 pub fn process_action(&mut self, action: MenuAction) -> bool {
296 match action {
297 MenuAction::Pop => { self.pop(); true }
298 MenuAction::PopToRoot => { self.pop_to_root(); true }
299 MenuAction::Push(screen) => { self.push(screen); true }
300 _ => false,
301 }
302 }
303}
304
305impl Default for MenuStack {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311#[derive(Debug, Clone)]
314pub struct Button {
315 pub label: String,
316 pub x: u32,
317 pub y: u32,
318 pub width: u32,
319 pub enabled: bool,
320 pub focused: bool,
321}
322
323impl Button {
324 pub fn new(label: impl Into<String>, x: u32, y: u32, width: u32) -> Self {
325 Self {
326 label: label.into(),
327 x,
328 y,
329 width,
330 enabled: true,
331 focused: false,
332 }
333 }
334
335 pub fn render(&self, buf: &mut MenuBuffer) {
336 let fg = if !self.enabled {
337 (100, 100, 100)
338 } else if self.focused {
339 (255, 220, 0)
340 } else {
341 (200, 200, 200)
342 };
343 let label = if self.label.len() < self.width as usize {
344 let pad = (self.width as usize - self.label.len()) / 2;
345 format!("{:>pad$}{}{:>pad$}", "", self.label, "", pad = pad)
346 } else {
347 self.label.clone()
348 };
349 buf.draw_box(self.x, self.y, self.width, 3, fg);
350 buf.put_str(self.x + 1, self.y + 1, &label, fg);
351 if self.focused {
352 buf.put(self.x - 1, self.y + 1, MenuCell::new('>').with_fg(255, 220, 0));
353 buf.put(self.x + self.width, self.y + 1, MenuCell::new('<').with_fg(255, 220, 0));
354 }
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub enum CharacterClass {
362 Warrior,
363 Mage,
364 Rogue,
365 Cleric,
366 Ranger,
367 Paladin,
368}
369
370impl CharacterClass {
371 pub fn name(&self) -> &str {
372 match self {
373 CharacterClass::Warrior => "Warrior",
374 CharacterClass::Mage => "Mage",
375 CharacterClass::Rogue => "Rogue",
376 CharacterClass::Cleric => "Cleric",
377 CharacterClass::Ranger => "Ranger",
378 CharacterClass::Paladin => "Paladin",
379 }
380 }
381
382 pub fn description(&self) -> &str {
383 match self {
384 CharacterClass::Warrior => "Melee fighter with high defense and strength.",
385 CharacterClass::Mage => "Spellcaster with powerful area attacks.",
386 CharacterClass::Rogue => "Agile assassin specializing in critical hits.",
387 CharacterClass::Cleric => "Support healer with light magic.",
388 CharacterClass::Ranger => "Ranged combatant skilled in traps and archery.",
389 CharacterClass::Paladin => "Holy warrior balancing offense and healing.",
390 }
391 }
392
393 pub fn icon(&self) -> char {
394 match self {
395 CharacterClass::Warrior => '⚔',
396 CharacterClass::Mage => '✦',
397 CharacterClass::Rogue => '◆',
398 CharacterClass::Cleric => '✚',
399 CharacterClass::Ranger => '◉',
400 CharacterClass::Paladin => '☀',
401 }
402 }
403
404 pub fn stats(&self) -> ClassStats {
405 match self {
406 CharacterClass::Warrior => ClassStats { hp: 120, mp: 30, atk: 15, def: 12, spd: 8 },
407 CharacterClass::Mage => ClassStats { hp: 60, mp: 120, atk: 18, def: 5, spd: 10 },
408 CharacterClass::Rogue => ClassStats { hp: 80, mp: 50, atk: 20, def: 6, spd: 18 },
409 CharacterClass::Cleric => ClassStats { hp: 90, mp: 100, atk: 8, def: 10, spd: 9 },
410 CharacterClass::Ranger => ClassStats { hp: 85, mp: 60, atk: 16, def: 8, spd: 14 },
411 CharacterClass::Paladin => ClassStats { hp: 110, mp: 70, atk: 12, def: 14, spd: 7 },
412 }
413 }
414
415 pub fn all() -> &'static [CharacterClass] {
416 &[
417 CharacterClass::Warrior,
418 CharacterClass::Mage,
419 CharacterClass::Rogue,
420 CharacterClass::Cleric,
421 CharacterClass::Ranger,
422 CharacterClass::Paladin,
423 ]
424 }
425}
426
427#[derive(Debug, Clone)]
428pub struct ClassStats {
429 pub hp: u32,
430 pub mp: u32,
431 pub atk: u32,
432 pub def: u32,
433 pub spd: u32,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub enum QualityPreset {
440 Low,
441 Medium,
442 High,
443 Ultra,
444}
445
446impl QualityPreset {
447 pub fn name(&self) -> &str {
448 match self {
449 QualityPreset::Low => "Low",
450 QualityPreset::Medium => "Medium",
451 QualityPreset::High => "High",
452 QualityPreset::Ultra => "Ultra",
453 }
454 }
455}
456
457#[derive(Debug, Clone)]
458pub struct GraphicsSettings {
459 pub resolution: (u32, u32),
460 pub fullscreen: bool,
461 pub vsync: bool,
462 pub target_fps: u32,
463 pub quality_preset: QualityPreset,
464}
465
466impl Default for GraphicsSettings {
467 fn default() -> Self {
468 Self {
469 resolution: (1920, 1080),
470 fullscreen: false,
471 vsync: true,
472 target_fps: 60,
473 quality_preset: QualityPreset::High,
474 }
475 }
476}
477
478#[derive(Debug, Clone)]
479pub struct AudioSettings {
480 pub master: f32,
481 pub music: f32,
482 pub sfx: f32,
483 pub voice: f32,
484 pub subtitles: bool,
485}
486
487impl Default for AudioSettings {
488 fn default() -> Self {
489 Self {
490 master: 1.0,
491 music: 0.8,
492 sfx: 1.0,
493 voice: 1.0,
494 subtitles: false,
495 }
496 }
497}
498
499#[derive(Debug, Clone)]
500pub struct ControlSettings {
501 pub key_bindings: HashMap<String, KeyCode>,
502 pub mouse_sensitivity: f32,
503 pub controller_deadzone: f32,
504}
505
506impl Default for ControlSettings {
507 fn default() -> Self {
508 let mut bindings = HashMap::new();
509 bindings.insert("move_up".to_string(), KeyCode::W);
510 bindings.insert("move_down".to_string(), KeyCode::S);
511 bindings.insert("move_left".to_string(), KeyCode::A);
512 bindings.insert("move_right".to_string(), KeyCode::D);
513 bindings.insert("attack".to_string(), KeyCode::MouseLeft);
514 bindings.insert("use_skill".to_string(), KeyCode::Space);
515 bindings.insert("interact".to_string(), KeyCode::F);
516 bindings.insert("inventory".to_string(), KeyCode::I);
517 bindings.insert("map".to_string(), KeyCode::M);
518 bindings.insert("pause".to_string(), KeyCode::Escape);
519 Self {
520 key_bindings: bindings,
521 mouse_sensitivity: 1.0,
522 controller_deadzone: 0.15,
523 }
524 }
525}
526
527impl ControlSettings {
528 pub fn bind(&mut self, action: impl Into<String>, key: KeyCode) {
529 self.key_bindings.insert(action.into(), key);
530 }
531
532 pub fn binding_for(&self, action: &str) -> Option<KeyCode> {
533 self.key_bindings.get(action).copied()
534 }
535
536 pub fn unbind(&mut self, action: &str) {
537 self.key_bindings.remove(action);
538 }
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
542pub enum ColorblindMode {
543 None,
544 Deuteranopia,
545 Protanopia,
546 Tritanopia,
547 Monochrome,
548}
549
550impl ColorblindMode {
551 pub fn name(&self) -> &str {
552 match self {
553 ColorblindMode::None => "None",
554 ColorblindMode::Deuteranopia => "Deuteranopia (Red-Green)",
555 ColorblindMode::Protanopia => "Protanopia (Red-Green Alt)",
556 ColorblindMode::Tritanopia => "Tritanopia (Blue-Yellow)",
557 ColorblindMode::Monochrome => "Monochrome",
558 }
559 }
560}
561
562#[derive(Debug, Clone)]
563pub struct AccessibilitySettings {
564 pub colorblind_mode: ColorblindMode,
565 pub high_contrast: bool,
566 pub reduce_motion: bool,
567 pub large_text: bool,
568 pub screen_reader: bool,
569}
570
571impl Default for AccessibilitySettings {
572 fn default() -> Self {
573 Self {
574 colorblind_mode: ColorblindMode::None,
575 high_contrast: false,
576 reduce_motion: false,
577 large_text: false,
578 screen_reader: false,
579 }
580 }
581}
582
583#[derive(Debug, Clone, PartialEq, Eq)]
584pub enum Language {
585 English,
586 French,
587 German,
588 Japanese,
589 Chinese,
590 Korean,
591 Spanish,
592 Portuguese,
593 Russian,
594 Arabic,
595}
596
597impl Language {
598 pub fn name(&self) -> &str {
599 match self {
600 Language::English => "English",
601 Language::French => "Français",
602 Language::German => "Deutsch",
603 Language::Japanese => "日本語",
604 Language::Chinese => "中文",
605 Language::Korean => "한국어",
606 Language::Spanish => "Español",
607 Language::Portuguese => "Português",
608 Language::Russian => "Русский",
609 Language::Arabic => "العربية",
610 }
611 }
612}
613
614#[derive(Debug, Clone)]
617pub struct Tooltip {
618 pub text: String,
619 pub x: u32,
620 pub y: u32,
621 pub visible: bool,
622 pub timer: f32,
623 pub delay: f32,
624}
625
626impl Tooltip {
627 pub fn new(text: impl Into<String>) -> Self {
628 Self {
629 text: text.into(),
630 x: 0,
631 y: 0,
632 visible: false,
633 timer: 0.0,
634 delay: 0.5,
635 }
636 }
637
638 pub fn show(&mut self, x: u32, y: u32) {
639 self.x = x;
640 self.y = y;
641 self.timer = 0.0;
642 }
643
644 pub fn hide(&mut self) {
645 self.visible = false;
646 self.timer = 0.0;
647 }
648
649 pub fn update(&mut self, dt: f32) {
650 if !self.visible {
651 self.timer += dt;
652 if self.timer >= self.delay {
653 self.visible = true;
654 }
655 }
656 }
657
658 pub fn render(&self, buf: &mut MenuBuffer) {
659 if !self.visible { return; }
660 let w = self.text.len() as u32 + 4;
661 let h = 3u32;
662 buf.draw_box(self.x, self.y, w, h, (200, 200, 0));
663 buf.put_str(self.x + 2, self.y + 1, &self.text, (255, 255, 180));
664 }
665}
666
667pub struct Dialog {
670 pub title: String,
671 pub message: String,
672 pub yes_label: String,
673 pub no_label: String,
674 pub focused_yes: bool,
675 pub result: Option<bool>,
676}
677
678impl Dialog {
679 pub fn new(message: impl Into<String>) -> Self {
680 Self {
681 title: "Confirm".to_string(),
682 message: message.into(),
683 yes_label: "Yes".to_string(),
684 no_label: "No".to_string(),
685 focused_yes: false,
686 result: None,
687 }
688 }
689
690 pub fn with_title(mut self, title: impl Into<String>) -> Self {
691 self.title = title.into();
692 self
693 }
694
695 pub fn with_labels(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
696 self.yes_label = yes.into();
697 self.no_label = no.into();
698 self
699 }
700
701 pub fn handle_input(&mut self, input: &InputEvent) {
702 match input {
703 InputEvent::KeyDown(KeyCode::Left) | InputEvent::KeyDown(KeyCode::Right) |
704 InputEvent::KeyDown(KeyCode::Tab) => {
705 self.focused_yes = !self.focused_yes;
706 }
707 InputEvent::KeyDown(KeyCode::Enter) => {
708 self.result = Some(self.focused_yes);
709 }
710 InputEvent::KeyDown(KeyCode::Escape) => {
711 self.result = Some(false);
712 }
713 _ => {}
714 }
715 }
716
717 pub fn is_resolved(&self) -> bool {
718 self.result.is_some()
719 }
720
721 pub fn answer(&self) -> Option<bool> {
722 self.result
723 }
724
725 pub fn render(&self, ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
726 let w = 40u32;
727 let h = 8u32;
728 let x = ctx.center_x().saturating_sub(w / 2);
729 let y = ctx.center_y().saturating_sub(h / 2);
730 buf.fill_rect(x, y, w, h, ' ', (20, 20, 40));
732 buf.draw_box(x, y, w, h, (100, 100, 200));
733 buf.put_str_bold(x + 2, y + 1, &self.title, (200, 200, 255));
735 let msg = &self.message;
737 buf.put_str(x + 2, y + 3, msg, (220, 220, 220));
738 let yes_fg = if self.focused_yes { (255, 220, 0) } else { (180, 180, 180) };
740 let no_fg = if !self.focused_yes { (255, 220, 0) } else { (180, 180, 180) };
741 buf.put_str(x + 8, y + 6, &self.yes_label, yes_fg);
742 buf.put_str(x + 24, y + 6, &self.no_label, no_fg);
743 }
744}
745
746pub struct BackgroundAnimator {
749 pub time: f32,
750 glyphs: Vec<AnimGlyph>,
751}
752
753struct AnimGlyph {
754 x: f32,
755 y: f32,
756 ch: char,
757 speed_x: f32,
758 speed_y: f32,
759 color: (u8, u8, u8),
760 phase: f32,
761}
762
763impl BackgroundAnimator {
764 pub fn new(seed: u64) -> Self {
765 let chars = ['·', '∘', '○', '◦', '◌', '◎', '✦', '✧', '⋆', '∗'];
766 let mut glyphs = Vec::with_capacity(30);
767 for i in 0..30 {
768 let rng = Self::lcg(seed.wrapping_add(i as u64 * 1234567));
769 let x = (rng % 200) as f32;
770 let rng2 = Self::lcg(rng);
771 let y = (rng2 % 50) as f32;
772 let rng3 = Self::lcg(rng2);
773 let ch = chars[(rng3 % chars.len() as u64) as usize];
774 let rng4 = Self::lcg(rng3);
775 let speed_x = ((rng4 % 100) as f32 / 100.0 - 0.5) * 2.0;
776 let rng5 = Self::lcg(rng4);
777 let speed_y = ((rng5 % 100) as f32 / 100.0 - 0.5) * 0.5;
778 let rng6 = Self::lcg(rng5);
779 let hue = (rng6 % 360) as f32;
780 let color = Self::hsv_to_rgb(hue, 0.6, 0.7);
781 glyphs.push(AnimGlyph {
782 x,
783 y,
784 ch,
785 speed_x,
786 speed_y,
787 color,
788 phase: (i as f32) * 0.3,
789 });
790 }
791 Self { time: 0.0, glyphs }
792 }
793
794 fn lcg(n: u64) -> u64 {
795 n.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407)
796 }
797
798 fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
799 let h = h % 360.0;
800 let c = v * s;
801 let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
802 let m = v - c;
803 let (r1, g1, b1) = if h < 60.0 { (c, x, 0.0) }
804 else if h < 120.0 { (x, c, 0.0) }
805 else if h < 180.0 { (0.0, c, x) }
806 else if h < 240.0 { (0.0, x, c) }
807 else if h < 300.0 { (x, 0.0, c) }
808 else { (c, 0.0, x) };
809 (
810 ((r1 + m) * 255.0) as u8,
811 ((g1 + m) * 255.0) as u8,
812 ((b1 + m) * 255.0) as u8,
813 )
814 }
815
816 pub fn update(&mut self, dt: f32) {
817 self.time += dt;
818 for g in &mut self.glyphs {
819 g.x += g.speed_x * dt * 5.0;
820 g.y += g.speed_y * dt * 2.0;
821 if g.x < 0.0 { g.x += 200.0; }
822 if g.x >= 200.0 { g.x -= 200.0; }
823 if g.y < 0.0 { g.y += 50.0; }
824 if g.y >= 50.0 { g.y -= 50.0; }
825 }
826 }
827
828 pub fn render(&self, buf: &mut MenuBuffer) {
829 for g in &self.glyphs {
830 let alpha = ((self.time * 2.0 + g.phase).sin() * 0.5 + 0.5).clamp(0.1, 1.0);
831 let r = (g.color.0 as f32 * alpha) as u8;
832 let gr = (g.color.1 as f32 * alpha) as u8;
833 let b = (g.color.2 as f32 * alpha) as u8;
834 buf.put(g.x as u32, g.y as u32, MenuCell::new(g.ch).with_fg(r, gr, b));
835 }
836 }
837}
838
839pub struct MainMenuScreen {
842 buttons: Vec<Button>,
843 selected: usize,
844 bg: BackgroundAnimator,
845 has_save: bool,
846 anim_time: f32,
847}
848
849impl MainMenuScreen {
850 pub fn new(has_save: bool) -> Self {
851 let mut buttons = vec![
852 Button::new(if has_save { "Continue" } else { "New Game" }, 35, 18, 20),
853 Button::new("New Game", 35, 22, 20),
854 Button::new("Settings", 35, 26, 20),
855 Button::new("Credits", 35, 30, 20),
856 Button::new("Quit", 35, 34, 20),
857 ];
858 if !has_save {
859 buttons.remove(1); }
861 buttons[0].focused = true;
862 Self {
863 buttons,
864 selected: 0,
865 bg: BackgroundAnimator::new(42),
866 has_save,
867 anim_time: 0.0,
868 }
869 }
870
871 fn render_title(&self, buf: &mut MenuBuffer) {
872 let title_lines = [
873 " ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
874 " ██╔══██╗██╔══██╗██╔═══██╗██╔═══██╗██╔════╝",
875 " ██████╔╝██████╔╝██║ ██║██║ ██║█████╗ ",
876 " ██╔═══╝ ██╔══██╗██║ ██║██║ ██║██╔══╝ ",
877 " ██║ ██║ ██║╚██████╔╝╚██████╔╝██║ ",
878 " ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ",
879 ];
880 let wave_amp = (self.anim_time * 3.0).sin() * 0.5 + 1.0;
881 let r = (180.0 * wave_amp).min(255.0) as u8;
882 let g = (100.0 * wave_amp).min(255.0) as u8;
883 let b = (200.0 * wave_amp).min(255.0) as u8;
884 for (i, line) in title_lines.iter().enumerate() {
885 buf.put_str_bold(10, 4 + i as u32, line, (r, g, b));
886 }
887 let sub = "MATHEMATICAL RENDERING ENGINE";
889 buf.put_str(28, 11, sub, (150, 150, 200));
890 }
891}
892
893impl MenuScreen for MainMenuScreen {
894 fn name(&self) -> &str { "MainMenu" }
895
896 fn update(&mut self, dt: f32) {
897 self.anim_time += dt;
898 self.bg.update(dt);
899 }
900
901 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
902 self.bg.render(buf);
903 self.render_title(buf);
904 for btn in &self.buttons {
905 btn.render(buf);
906 }
907 buf.put_str(28, 48, "Use UP/DOWN to navigate, ENTER to select", (120, 120, 120));
909 }
910
911 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
912 match input {
913 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
914 self.buttons[self.selected].focused = false;
915 self.selected = self.selected.saturating_sub(1);
916 self.buttons[self.selected].focused = true;
917 }
918 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
919 self.buttons[self.selected].focused = false;
920 self.selected = (self.selected + 1).min(self.buttons.len() - 1);
921 self.buttons[self.selected].focused = true;
922 }
923 InputEvent::KeyDown(KeyCode::Enter) => {
924 let label = self.buttons[self.selected].label.clone();
925 match label.as_str() {
926 "Continue" => return MenuAction::LoadGame { slot: 0 },
927 "New Game" => return MenuAction::Push(Box::new(NewGameScreen::new())),
928 "Settings" => return MenuAction::OpenSettings,
929 "Credits" => return MenuAction::ShowCredits,
930 "Quit" => return MenuAction::Quit,
931 _ => {}
932 }
933 }
934 InputEvent::KeyDown(KeyCode::Escape) => {
935 return MenuAction::Quit;
936 }
937 _ => {}
938 }
939 MenuAction::None
940 }
941
942 fn tooltip(&self) -> Option<&str> {
943 match self.selected {
944 0 if self.has_save => Some("Continue from your last save point"),
945 0 => Some("Start a brand new adventure"),
946 1 if self.has_save => Some("Begin a fresh game (your save will be kept)"),
947 _ => None,
948 }
949 }
950}
951
952pub struct PauseMenuScreen {
955 buttons: Vec<Button>,
956 selected: usize,
957 pending_dialog: Option<Dialog>,
958}
959
960impl PauseMenuScreen {
961 pub fn new() -> Self {
962 let mut buttons = vec![
963 Button::new("Resume", 35, 15, 20),
964 Button::new("Restart", 35, 19, 20),
965 Button::new("Settings", 35, 23, 20),
966 Button::new("Main Menu", 35, 27, 20),
967 Button::new("Quit to Desktop", 35, 31, 20),
968 ];
969 buttons[0].focused = true;
970 Self { buttons, selected: 0, pending_dialog: None }
971 }
972}
973
974impl MenuScreen for PauseMenuScreen {
975 fn name(&self) -> &str { "Pause" }
976
977 fn render(&self, ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
978 buf.put_str(30, 8, "─── PAUSED ───", (200, 200, 255));
980 buf.put_str(26, 10, "Game is paused. Your progress is safe.", (150, 150, 200));
981 for btn in &self.buttons {
982 btn.render(buf);
983 }
984 if let Some(ref dlg) = self.pending_dialog {
985 dlg.render(ctx, buf);
986 }
987 }
988
989 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
990 if let Some(ref mut dlg) = self.pending_dialog {
991 dlg.handle_input(input);
992 if dlg.is_resolved() {
993 let answer = dlg.answer();
994 self.pending_dialog = None;
995 if answer == Some(true) {
996 return MenuAction::ReturnToMainMenu;
997 }
998 }
999 return MenuAction::None;
1000 }
1001
1002 match input {
1003 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1004 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1005 self.buttons[self.selected].focused = false;
1006 self.selected = self.selected.saturating_sub(1);
1007 self.buttons[self.selected].focused = true;
1008 }
1009 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1010 self.buttons[self.selected].focused = false;
1011 self.selected = (self.selected + 1).min(self.buttons.len() - 1);
1012 self.buttons[self.selected].focused = true;
1013 }
1014 InputEvent::KeyDown(KeyCode::Enter) => {
1015 let label = self.buttons[self.selected].label.clone();
1016 match label.as_str() {
1017 "Resume" => return MenuAction::Pop,
1018 "Restart" => {
1019 self.pending_dialog = Some(
1020 Dialog::new("Restart the current run? Progress will be lost.")
1021 .with_labels("Restart", "Cancel")
1022 );
1023 }
1024 "Settings" => return MenuAction::OpenSettings,
1025 "Main Menu" => {
1026 self.pending_dialog = Some(
1027 Dialog::new("Return to main menu? Unsaved progress will be lost.")
1028 .with_labels("Yes", "No")
1029 );
1030 }
1031 "Quit to Desktop" => {
1032 self.pending_dialog = Some(
1033 Dialog::new("Quit to desktop?")
1034 .with_labels("Quit", "Cancel")
1035 );
1036 }
1037 _ => {}
1038 }
1039 }
1040 _ => {}
1041 }
1042 MenuAction::None
1043 }
1044}
1045
1046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1049enum SettingsTab {
1050 Graphics,
1051 Audio,
1052 Controls,
1053 Accessibility,
1054 Language,
1055}
1056
1057impl SettingsTab {
1058 fn all() -> &'static [SettingsTab] {
1059 &[
1060 SettingsTab::Graphics,
1061 SettingsTab::Audio,
1062 SettingsTab::Controls,
1063 SettingsTab::Accessibility,
1064 SettingsTab::Language,
1065 ]
1066 }
1067
1068 fn name(&self) -> &str {
1069 match self {
1070 SettingsTab::Graphics => "Graphics",
1071 SettingsTab::Audio => "Audio",
1072 SettingsTab::Controls => "Controls",
1073 SettingsTab::Accessibility => "Accessibility",
1074 SettingsTab::Language => "Language",
1075 }
1076 }
1077}
1078
1079pub struct SettingsScreen {
1080 pub graphics: GraphicsSettings,
1081 pub audio: AudioSettings,
1082 pub controls: ControlSettings,
1083 pub accessibility: AccessibilitySettings,
1084 pub language: Language,
1085 current_tab: SettingsTab,
1086 tab_index: usize,
1087 row_index: usize,
1088 binding_capture: Option<String>,
1089}
1090
1091impl SettingsScreen {
1092 pub fn new() -> Self {
1093 Self {
1094 graphics: GraphicsSettings::default(),
1095 audio: AudioSettings::default(),
1096 controls: ControlSettings::default(),
1097 accessibility: AccessibilitySettings::default(),
1098 language: Language::English,
1099 current_tab: SettingsTab::Graphics,
1100 tab_index: 0,
1101 row_index: 0,
1102 binding_capture: None,
1103 }
1104 }
1105
1106 fn render_tab_bar(&self, buf: &mut MenuBuffer) {
1107 let tabs = SettingsTab::all();
1108 let mut x = 5u32;
1109 for (i, tab) in tabs.iter().enumerate() {
1110 let fg = if i == self.tab_index { (255, 220, 0) } else { (180, 180, 180) };
1111 if i == self.tab_index {
1112 buf.put_str_bold(x, 5, &format!("[{}]", tab.name()), fg);
1113 } else {
1114 buf.put_str(x, 5, tab.name(), fg);
1115 }
1116 x += tab.name().len() as u32 + 3;
1117 }
1118 let sep: String = "─".repeat(80);
1120 buf.put_str(2, 6, &sep, (80, 80, 120));
1121 }
1122
1123 fn render_graphics_tab(&self, buf: &mut MenuBuffer) {
1124 let items = [
1125 format!("Resolution: {}x{}", self.graphics.resolution.0, self.graphics.resolution.1),
1126 format!("Fullscreen: {}", if self.graphics.fullscreen { "On" } else { "Off" }),
1127 format!("VSync: {}", if self.graphics.vsync { "On" } else { "Off" }),
1128 format!("Target FPS: {}", self.graphics.target_fps),
1129 format!("Quality: {}", self.graphics.quality_preset.name()),
1130 ];
1131 for (i, item) in items.iter().enumerate() {
1132 let fg = if i == self.row_index { (255, 220, 0) } else { (200, 200, 200) };
1133 buf.put_str(10, 10 + i as u32 * 2, item, fg);
1134 }
1135 }
1136
1137 fn render_audio_tab(&self, buf: &mut MenuBuffer) {
1138 let items = [
1139 format!("Master Volume: {:.0}%", self.audio.master * 100.0),
1140 format!("Music Volume: {:.0}%", self.audio.music * 100.0),
1141 format!("SFX Volume: {:.0}%", self.audio.sfx * 100.0),
1142 format!("Voice Volume: {:.0}%", self.audio.voice * 100.0),
1143 format!("Subtitles: {}", if self.audio.subtitles { "On" } else { "Off" }),
1144 ];
1145 for (i, item) in items.iter().enumerate() {
1146 let fg = if i == self.row_index { (255, 220, 0) } else { (200, 200, 200) };
1147 buf.put_str(10, 10 + i as u32 * 2, item, fg);
1148 }
1149 }
1150
1151 fn render_controls_tab(&self, buf: &mut MenuBuffer) {
1152 let actions = ["move_up", "move_down", "move_left", "move_right",
1153 "attack", "use_skill", "interact", "inventory", "map", "pause"];
1154 for (i, action) in actions.iter().enumerate() {
1155 let key = self.controls.key_bindings.get(*action)
1156 .map(|k| k.display_name())
1157 .unwrap_or("Unbound");
1158 let line = format!("{:<20} {}", action, key);
1159 let fg = if i == self.row_index { (255, 220, 0) } else { (200, 200, 200) };
1160 buf.put_str(10, 10 + i as u32 * 2, &line, fg);
1161 }
1162 if let Some(ref action) = self.binding_capture {
1163 buf.put_str(10, 32, &format!("Press key for: {}", action), (255, 100, 100));
1164 }
1165 }
1166
1167 fn render_accessibility_tab(&self, buf: &mut MenuBuffer) {
1168 let items = [
1169 format!("Colorblind Mode: {}", self.accessibility.colorblind_mode.name()),
1170 format!("High Contrast: {}", if self.accessibility.high_contrast { "On" } else { "Off" }),
1171 format!("Reduce Motion: {}", if self.accessibility.reduce_motion { "On" } else { "Off" }),
1172 format!("Large Text: {}", if self.accessibility.large_text { "On" } else { "Off" }),
1173 format!("Screen Reader: {}", if self.accessibility.screen_reader { "On" } else { "Off" }),
1174 ];
1175 for (i, item) in items.iter().enumerate() {
1176 let fg = if i == self.row_index { (255, 220, 0) } else { (200, 200, 200) };
1177 buf.put_str(10, 10 + i as u32 * 2, item, fg);
1178 }
1179 }
1180
1181 fn render_language_tab(&self, buf: &mut MenuBuffer) {
1182 let langs = [
1183 Language::English, Language::French, Language::German,
1184 Language::Japanese, Language::Chinese, Language::Korean,
1185 Language::Spanish, Language::Portuguese, Language::Russian, Language::Arabic,
1186 ];
1187 for (i, lang) in langs.iter().enumerate() {
1188 let selected = *lang == self.language;
1189 let fg = if i == self.row_index { (255, 220, 0) } else { (200, 200, 200) };
1190 let marker = if selected { "● " } else { "○ " };
1191 buf.put_str(10, 10 + i as u32 * 2, &format!("{}{}", marker, lang.name()), fg);
1192 }
1193 }
1194
1195 fn rows_in_tab(&self) -> usize {
1196 match self.current_tab {
1197 SettingsTab::Graphics => 5,
1198 SettingsTab::Audio => 5,
1199 SettingsTab::Controls => 10,
1200 SettingsTab::Accessibility => 5,
1201 SettingsTab::Language => 10,
1202 }
1203 }
1204}
1205
1206impl MenuScreen for SettingsScreen {
1207 fn name(&self) -> &str { "Settings" }
1208
1209 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1210 buf.put_str_bold(30, 2, "SETTINGS", (200, 200, 255));
1211 self.render_tab_bar(buf);
1212 match self.current_tab {
1213 SettingsTab::Graphics => self.render_graphics_tab(buf),
1214 SettingsTab::Audio => self.render_audio_tab(buf),
1215 SettingsTab::Controls => self.render_controls_tab(buf),
1216 SettingsTab::Accessibility => self.render_accessibility_tab(buf),
1217 SettingsTab::Language => self.render_language_tab(buf),
1218 }
1219 buf.put_str(5, 46, "TAB: Switch tabs UP/DOWN: Navigate LEFT/RIGHT: Change ESC: Back", (120, 120, 120));
1220 }
1221
1222 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1223 if let Some(ref action) = self.binding_capture.clone() {
1224 if let InputEvent::KeyDown(key) = input {
1225 self.controls.bind(action.clone(), *key);
1226 self.binding_capture = None;
1227 }
1228 return MenuAction::None;
1229 }
1230
1231 match input {
1232 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1233 InputEvent::KeyDown(KeyCode::Tab) => {
1234 let tabs = SettingsTab::all();
1235 self.tab_index = (self.tab_index + 1) % tabs.len();
1236 self.current_tab = tabs[self.tab_index];
1237 self.row_index = 0;
1238 }
1239 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1240 self.row_index = self.row_index.saturating_sub(1);
1241 }
1242 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1243 let max = self.rows_in_tab().saturating_sub(1);
1244 self.row_index = (self.row_index + 1).min(max);
1245 }
1246 InputEvent::KeyDown(KeyCode::Left) => {
1247 self.adjust_setting(-1);
1248 }
1249 InputEvent::KeyDown(KeyCode::Right) => {
1250 self.adjust_setting(1);
1251 }
1252 InputEvent::KeyDown(KeyCode::Enter) => {
1253 if self.current_tab == SettingsTab::Controls {
1254 let actions = ["move_up", "move_down", "move_left", "move_right",
1255 "attack", "use_skill", "interact", "inventory", "map", "pause"];
1256 if let Some(&action) = actions.get(self.row_index) {
1257 self.binding_capture = Some(action.to_string());
1258 }
1259 } else {
1260 self.adjust_setting(1);
1261 }
1262 }
1263 _ => {}
1264 }
1265 MenuAction::None
1266 }
1267}
1268
1269impl SettingsScreen {
1270 fn adjust_setting(&mut self, delta: i32) {
1271 match self.current_tab {
1272 SettingsTab::Graphics => match self.row_index {
1273 0 => {
1274 let resolutions = [(1280u32, 720u32), (1920, 1080), (2560, 1440), (3840, 2160)];
1275 let cur = resolutions.iter().position(|&r| r == self.graphics.resolution).unwrap_or(1);
1276 let next = ((cur as i32 + delta).rem_euclid(resolutions.len() as i32)) as usize;
1277 self.graphics.resolution = resolutions[next];
1278 }
1279 1 => self.graphics.fullscreen = !self.graphics.fullscreen,
1280 2 => self.graphics.vsync = !self.graphics.vsync,
1281 3 => {
1282 let fps_options = [30u32, 60, 120, 144, 240];
1283 let cur = fps_options.iter().position(|&f| f == self.graphics.target_fps).unwrap_or(1);
1284 let next = ((cur as i32 + delta).rem_euclid(fps_options.len() as i32)) as usize;
1285 self.graphics.target_fps = fps_options[next];
1286 }
1287 4 => {
1288 let presets = [QualityPreset::Low, QualityPreset::Medium, QualityPreset::High, QualityPreset::Ultra];
1289 let cur = presets.iter().position(|&p| p == self.graphics.quality_preset).unwrap_or(2);
1290 let next = ((cur as i32 + delta).rem_euclid(presets.len() as i32)) as usize;
1291 self.graphics.quality_preset = presets[next];
1292 }
1293 _ => {}
1294 },
1295 SettingsTab::Audio => match self.row_index {
1296 0 => self.audio.master = (self.audio.master + delta as f32 * 0.05).clamp(0.0, 1.0),
1297 1 => self.audio.music = (self.audio.music + delta as f32 * 0.05).clamp(0.0, 1.0),
1298 2 => self.audio.sfx = (self.audio.sfx + delta as f32 * 0.05).clamp(0.0, 1.0),
1299 3 => self.audio.voice = (self.audio.voice + delta as f32 * 0.05).clamp(0.0, 1.0),
1300 4 => self.audio.subtitles = !self.audio.subtitles,
1301 _ => {}
1302 },
1303 SettingsTab::Accessibility => match self.row_index {
1304 0 => {
1305 let modes = [ColorblindMode::None, ColorblindMode::Deuteranopia,
1306 ColorblindMode::Protanopia, ColorblindMode::Tritanopia, ColorblindMode::Monochrome];
1307 let cur = modes.iter().position(|&m| m == self.accessibility.colorblind_mode).unwrap_or(0);
1308 let next = ((cur as i32 + delta).rem_euclid(modes.len() as i32)) as usize;
1309 self.accessibility.colorblind_mode = modes[next];
1310 }
1311 1 => self.accessibility.high_contrast = !self.accessibility.high_contrast,
1312 2 => self.accessibility.reduce_motion = !self.accessibility.reduce_motion,
1313 3 => self.accessibility.large_text = !self.accessibility.large_text,
1314 4 => self.accessibility.screen_reader = !self.accessibility.screen_reader,
1315 _ => {}
1316 },
1317 SettingsTab::Language => {
1318 let langs = [
1319 Language::English, Language::French, Language::German,
1320 Language::Japanese, Language::Chinese, Language::Korean,
1321 Language::Spanish, Language::Portuguese, Language::Russian, Language::Arabic,
1322 ];
1323 if let Some(lang) = langs.get(self.row_index) {
1324 self.language = lang.clone();
1325 }
1326 }
1327 _ => {}
1328 }
1329 }
1330}
1331
1332pub struct CharacterSelectScreen {
1335 selected: usize,
1336 classes: Vec<CharacterClass>,
1337 anim_time: f32,
1338}
1339
1340impl CharacterSelectScreen {
1341 pub fn new() -> Self {
1342 Self {
1343 selected: 0,
1344 classes: CharacterClass::all().to_vec(),
1345 anim_time: 0.0,
1346 }
1347 }
1348
1349 fn render_class_card(&self, buf: &mut MenuBuffer, class: &CharacterClass, x: u32, y: u32, focused: bool) {
1350 let border_fg = if focused { (255, 220, 0) } else { (100, 100, 180) };
1351 buf.draw_box(x, y, 16, 10, border_fg);
1352 buf.put(x + 7, y + 2, MenuCell::new(class.icon()).with_fg(180, 180, 255));
1354 let name = class.name();
1356 let name_x = x + (16u32.saturating_sub(name.len() as u32)) / 2;
1357 buf.put_str_bold(name_x, y + 4, name, border_fg);
1358 let stats = class.stats();
1360 buf.put_str(x + 2, y + 6, &format!("HP:{:3} MP:{:3}", stats.hp, stats.mp), (160, 200, 160));
1361 buf.put_str(x + 2, y + 8, &format!("ATK:{:2} DEF:{:2} SPD:{:2}", stats.atk, stats.def, stats.spd), (160, 160, 200));
1362 }
1363}
1364
1365impl MenuScreen for CharacterSelectScreen {
1366 fn name(&self) -> &str { "CharacterSelect" }
1367
1368 fn update(&mut self, dt: f32) {
1369 self.anim_time += dt;
1370 }
1371
1372 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1373 buf.put_str_bold(25, 2, "SELECT YOUR CLASS", (200, 200, 255));
1374 for (i, class) in self.classes.iter().enumerate() {
1375 let x = 5 + (i as u32 % 3) * 20;
1376 let y = 8 + (i as u32 / 3) * 12;
1377 self.render_class_card(buf, class, x, y, i == self.selected);
1378 }
1379 let class = &self.classes[self.selected];
1381 buf.put_str(5, 38, class.description(), (200, 200, 200));
1382 buf.put_str(5, 46, "LEFT/RIGHT: Select ENTER: Confirm ESC: Back", (120, 120, 120));
1383 }
1384
1385 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1386 match input {
1387 InputEvent::KeyDown(KeyCode::Left) | InputEvent::KeyDown(KeyCode::A) => {
1388 self.selected = self.selected.saturating_sub(1);
1389 }
1390 InputEvent::KeyDown(KeyCode::Right) | InputEvent::KeyDown(KeyCode::D) => {
1391 self.selected = (self.selected + 1).min(self.classes.len() - 1);
1392 }
1393 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1394 self.selected = self.selected.saturating_sub(3);
1395 }
1396 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1397 self.selected = (self.selected + 3).min(self.classes.len() - 1);
1398 }
1399 InputEvent::KeyDown(KeyCode::Enter) => {
1400 return MenuAction::Push(Box::new(NewGameScreen::with_class(self.classes[self.selected])));
1401 }
1402 InputEvent::KeyDown(KeyCode::Escape) => {
1403 return MenuAction::Pop;
1404 }
1405 _ => {}
1406 }
1407 MenuAction::None
1408 }
1409}
1410
1411#[derive(Debug, Clone)]
1414pub struct LevelInfo {
1415 pub id: String,
1416 pub name: String,
1417 pub unlocked: bool,
1418 pub stars: u8,
1419 pub best_time: Option<f32>,
1420 pub best_score: Option<u64>,
1421}
1422
1423pub struct LevelSelectScreen {
1424 levels: Vec<LevelInfo>,
1425 selected: usize,
1426 scroll: u32,
1427 cols: u32,
1428}
1429
1430impl LevelSelectScreen {
1431 pub fn new(levels: Vec<LevelInfo>) -> Self {
1432 Self { levels, selected: 0, scroll: 0, cols: 4 }
1433 }
1434
1435 pub fn with_demo_levels() -> Self {
1436 let levels = (1..=20).map(|i| LevelInfo {
1437 id: format!("level_{:02}", i),
1438 name: format!("Level {}", i),
1439 unlocked: i <= 5,
1440 stars: if i <= 3 { 3 - (i as u8 % 3) } else { 0 },
1441 best_time: if i <= 3 { Some(60.0 * i as f32) } else { None },
1442 best_score: if i <= 3 { Some(1000 * i as u64) } else { None },
1443 }).collect();
1444 Self::new(levels)
1445 }
1446
1447 fn render_level_cell(&self, buf: &mut MenuBuffer, level: &LevelInfo, x: u32, y: u32, focused: bool) {
1448 let border_fg = if focused { (255, 220, 0) }
1449 else if level.unlocked { (100, 180, 100) }
1450 else { (80, 80, 80) };
1451 buf.draw_box(x, y, 14, 8, border_fg);
1452 if level.unlocked {
1453 buf.put_str(x + 2, y + 2, &level.name, border_fg);
1454 let stars: String = (0..3).map(|i| if i < level.stars { '★' } else { '☆' }).collect();
1455 buf.put_str(x + 2, y + 4, &stars, (255, 200, 0));
1456 } else {
1457 buf.put(x + 6, y + 3, MenuCell::new('🔒').with_fg(80, 80, 80));
1458 buf.put_str(x + 4, y + 5, "Locked", (80, 80, 80));
1459 }
1460 }
1461}
1462
1463impl MenuScreen for LevelSelectScreen {
1464 fn name(&self) -> &str { "LevelSelect" }
1465
1466 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1467 buf.put_str_bold(25, 2, "SELECT LEVEL", (200, 200, 255));
1468 for (i, level) in self.levels.iter().enumerate() {
1469 let col = i as u32 % self.cols;
1470 let row = i as u32 / self.cols;
1471 let x = 5 + col * 16;
1472 let y = 6 + row * 10;
1473 self.render_level_cell(buf, level, x, y, i == self.selected);
1474 }
1475 if let Some(level) = self.levels.get(self.selected) {
1477 if level.unlocked {
1478 if let Some(score) = level.best_score {
1479 buf.put_str(5, 46, &format!("Best Score: {} Best Time: {:.0}s",
1480 score,
1481 level.best_time.unwrap_or(0.0)), (180, 220, 180));
1482 }
1483 } else {
1484 buf.put_str(5, 46, "Complete previous levels to unlock this one.", (180, 180, 120));
1485 }
1486 }
1487 buf.put_str(5, 48, "Arrow keys: Navigate ENTER: Play ESC: Back", (120, 120, 120));
1488 }
1489
1490 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1491 match input {
1492 InputEvent::KeyDown(KeyCode::Left) | InputEvent::KeyDown(KeyCode::A) => {
1493 self.selected = self.selected.saturating_sub(1);
1494 }
1495 InputEvent::KeyDown(KeyCode::Right) | InputEvent::KeyDown(KeyCode::D) => {
1496 self.selected = (self.selected + 1).min(self.levels.len() - 1);
1497 }
1498 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1499 self.selected = self.selected.saturating_sub(self.cols as usize);
1500 }
1501 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1502 self.selected = (self.selected + self.cols as usize).min(self.levels.len() - 1);
1503 }
1504 InputEvent::KeyDown(KeyCode::Enter) => {
1505 if let Some(level) = self.levels.get(self.selected) {
1506 if level.unlocked {
1507 return MenuAction::StartGame {
1508 difficulty: super::DifficultyPreset::Normal,
1509 class: CharacterClass::Warrior,
1510 name: level.name.clone(),
1511 };
1512 }
1513 }
1514 }
1515 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1516 _ => {}
1517 }
1518 MenuAction::None
1519 }
1520}
1521
1522#[derive(Debug, Clone)]
1525pub struct SaveSlot {
1526 pub slot: usize,
1527 pub occupied: bool,
1528 pub character_name: String,
1529 pub character_class: CharacterClass,
1530 pub level: u32,
1531 pub playtime: f64,
1532 pub saved_at: u64,
1533}
1534
1535impl SaveSlot {
1536 pub fn empty(slot: usize) -> Self {
1537 Self {
1538 slot,
1539 occupied: false,
1540 character_name: String::new(),
1541 character_class: CharacterClass::Warrior,
1542 level: 0,
1543 playtime: 0.0,
1544 saved_at: 0,
1545 }
1546 }
1547}
1548
1549pub struct LoadGameScreen {
1552 slots: Vec<SaveSlot>,
1553 selected: usize,
1554 pending_delete: Option<usize>,
1555 delete_dialog: Option<Dialog>,
1556}
1557
1558impl LoadGameScreen {
1559 pub fn new(slots: Vec<SaveSlot>) -> Self {
1560 Self { slots, selected: 0, pending_delete: None, delete_dialog: None }
1561 }
1562
1563 pub fn with_demo_slots() -> Self {
1564 let mut slots = vec![SaveSlot::empty(0), SaveSlot::empty(1), SaveSlot::empty(2)];
1565 slots[0].occupied = true;
1566 slots[0].character_name = "Aldric".to_string();
1567 slots[0].character_class = CharacterClass::Warrior;
1568 slots[0].level = 12;
1569 slots[0].playtime = 7200.0;
1570 slots[0].saved_at = 1711000000;
1571 Self::new(slots)
1572 }
1573}
1574
1575impl MenuScreen for LoadGameScreen {
1576 fn name(&self) -> &str { "LoadGame" }
1577
1578 fn render(&self, ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1579 buf.put_str_bold(25, 2, "LOAD GAME", (200, 200, 255));
1580 for (i, slot) in self.slots.iter().enumerate() {
1581 let y = 8 + i as u32 * 12;
1582 let border_fg = if i == self.selected { (255, 220, 0) } else { (100, 100, 180) };
1583 buf.draw_box(10, y, 60, 10, border_fg);
1584 if slot.occupied {
1585 buf.put_str_bold(14, y + 1, &format!("Slot {} — {}", slot.slot + 1, slot.character_name), border_fg);
1586 buf.put_str(14, y + 3, &format!("{} — Level {}", slot.character_class.name(), slot.level), (180, 180, 220));
1587 let hours = slot.playtime as u64 / 3600;
1588 let mins = (slot.playtime as u64 % 3600) / 60;
1589 buf.put_str(14, y + 5, &format!("Playtime: {}h {}m", hours, mins), (160, 160, 160));
1590 buf.put_str(14, y + 7, "ENTER: Load DEL: Delete", (120, 120, 120));
1591 } else {
1592 buf.put_str(14, y + 4, &format!("Slot {} — Empty", slot.slot + 1), (100, 100, 100));
1593 }
1594 }
1595 if let Some(ref dlg) = self.delete_dialog {
1596 dlg.render(ctx, buf);
1597 }
1598 buf.put_str(5, 46, "UP/DOWN: Navigate ENTER: Load DELETE: Remove save ESC: Back", (120, 120, 120));
1599 }
1600
1601 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1602 if let Some(ref mut dlg) = self.delete_dialog {
1603 dlg.handle_input(input);
1604 if dlg.is_resolved() {
1605 if dlg.answer() == Some(true) {
1606 if let Some(idx) = self.pending_delete {
1607 if let Some(slot) = self.slots.get_mut(idx) {
1608 *slot = SaveSlot::empty(idx);
1609 }
1610 }
1611 }
1612 self.delete_dialog = None;
1613 self.pending_delete = None;
1614 }
1615 return MenuAction::None;
1616 }
1617
1618 match input {
1619 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1620 self.selected = self.selected.saturating_sub(1);
1621 }
1622 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1623 self.selected = (self.selected + 1).min(self.slots.len() - 1);
1624 }
1625 InputEvent::KeyDown(KeyCode::Enter) => {
1626 if let Some(slot) = self.slots.get(self.selected) {
1627 if slot.occupied {
1628 return MenuAction::LoadGame { slot: self.selected };
1629 }
1630 }
1631 }
1632 InputEvent::KeyDown(KeyCode::Delete) => {
1633 if let Some(slot) = self.slots.get(self.selected) {
1634 if slot.occupied {
1635 self.pending_delete = Some(self.selected);
1636 self.delete_dialog = Some(
1637 Dialog::new(format!("Delete save in slot {}?", self.selected + 1))
1638 .with_title("Delete Save")
1639 .with_labels("Delete", "Cancel")
1640 );
1641 }
1642 }
1643 }
1644 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1645 _ => {}
1646 }
1647 MenuAction::None
1648 }
1649}
1650
1651pub struct NewGameScreen {
1654 name_input: String,
1655 name_cursor: usize,
1656 selected_class: CharacterClass,
1657 selected_difficulty: super::DifficultyPreset,
1658 focused_field: usize,
1659 class_index: usize,
1660 difficulty_index: usize,
1661}
1662
1663impl NewGameScreen {
1664 pub fn new() -> Self {
1665 Self {
1666 name_input: String::new(),
1667 name_cursor: 0,
1668 selected_class: CharacterClass::Warrior,
1669 selected_difficulty: super::DifficultyPreset::Normal,
1670 focused_field: 0,
1671 class_index: 0,
1672 difficulty_index: 2, }
1674 }
1675
1676 pub fn with_class(class: CharacterClass) -> Self {
1677 let mut s = Self::new();
1678 s.selected_class = class;
1679 s.class_index = CharacterClass::all().iter().position(|&c| c == class).unwrap_or(0);
1680 s
1681 }
1682}
1683
1684impl MenuScreen for NewGameScreen {
1685 fn name(&self) -> &str { "NewGame" }
1686
1687 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1688 buf.put_str_bold(25, 2, "NEW GAME", (200, 200, 255));
1689
1690 let name_fg = if self.focused_field == 0 { (255, 220, 0) } else { (180, 180, 180) };
1692 buf.put_str(10, 8, "Character Name:", name_fg);
1693 buf.draw_box(10, 10, 30, 3, name_fg);
1694 let display = if self.name_input.is_empty() {
1695 "Enter name...".to_string()
1696 } else {
1697 self.name_input.clone()
1698 };
1699 buf.put_str(12, 11, &display, (220, 220, 220));
1700
1701 let class_fg = if self.focused_field == 1 { (255, 220, 0) } else { (180, 180, 180) };
1703 buf.put_str(10, 15, "Class:", class_fg);
1704 let class = &self.selected_class;
1705 buf.put_str(10, 17, &format!("< {} >", class.name()), class_fg);
1706 buf.put_str(10, 19, class.description(), (160, 160, 200));
1707
1708 let diff_fg = if self.focused_field == 2 { (255, 220, 0) } else { (180, 180, 180) };
1710 buf.put_str(10, 23, "Difficulty:", diff_fg);
1711 buf.put_str(10, 25, &format!("< {} >", self.selected_difficulty.name()), diff_fg);
1712
1713 let confirm_fg = if self.focused_field == 3 { (255, 220, 0) } else { (180, 180, 180) };
1715 buf.draw_box(10, 29, 20, 3, confirm_fg);
1716 buf.put_str(12, 30, "Start Adventure!", confirm_fg);
1717
1718 buf.put_str(5, 46, "UP/DOWN: Navigate LEFT/RIGHT: Change ENTER: Confirm ESC: Back", (120, 120, 120));
1719 }
1720
1721 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1722 match input {
1723 InputEvent::KeyDown(KeyCode::Up) | InputEvent::KeyDown(KeyCode::W) => {
1724 if self.focused_field > 0 { self.focused_field -= 1; }
1725 }
1726 InputEvent::KeyDown(KeyCode::Down) | InputEvent::KeyDown(KeyCode::S) => {
1727 if self.focused_field < 3 { self.focused_field += 1; }
1728 }
1729 InputEvent::CharInput(c) if self.focused_field == 0 => {
1730 if self.name_input.len() < 20 && c.is_alphanumeric() || *c == ' ' || *c == '-' {
1731 self.name_input.push(*c);
1732 self.name_cursor = self.name_input.len();
1733 }
1734 }
1735 InputEvent::KeyDown(KeyCode::Backspace) if self.focused_field == 0 => {
1736 if !self.name_input.is_empty() {
1737 self.name_input.pop();
1738 self.name_cursor = self.name_input.len();
1739 }
1740 }
1741 InputEvent::KeyDown(KeyCode::Left) => {
1742 match self.focused_field {
1743 1 => {
1744 let classes = CharacterClass::all();
1745 self.class_index = self.class_index.saturating_sub(1);
1746 self.selected_class = classes[self.class_index];
1747 }
1748 2 => {
1749 let presets = super::DifficultyPreset::all();
1750 self.difficulty_index = self.difficulty_index.saturating_sub(1);
1751 self.selected_difficulty = presets[self.difficulty_index];
1752 }
1753 _ => {}
1754 }
1755 }
1756 InputEvent::KeyDown(KeyCode::Right) => {
1757 match self.focused_field {
1758 1 => {
1759 let classes = CharacterClass::all();
1760 self.class_index = (self.class_index + 1).min(classes.len() - 1);
1761 self.selected_class = classes[self.class_index];
1762 }
1763 2 => {
1764 let presets = super::DifficultyPreset::all();
1765 self.difficulty_index = (self.difficulty_index + 1).min(presets.len() - 1);
1766 self.selected_difficulty = presets[self.difficulty_index];
1767 }
1768 _ => {}
1769 }
1770 }
1771 InputEvent::KeyDown(KeyCode::Enter) => {
1772 if self.focused_field == 3 {
1773 let name = if self.name_input.is_empty() {
1774 "Hero".to_string()
1775 } else {
1776 self.name_input.clone()
1777 };
1778 return MenuAction::StartGame {
1779 difficulty: self.selected_difficulty,
1780 class: self.selected_class,
1781 name,
1782 };
1783 }
1784 }
1785 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1786 _ => {}
1787 }
1788 MenuAction::None
1789 }
1790}
1791
1792pub struct GameOverScreen {
1795 data: super::GameOverData,
1796 buttons: Vec<Button>,
1797 selected: usize,
1798 anim_time: f32,
1799}
1800
1801impl GameOverScreen {
1802 pub fn new(data: super::GameOverData) -> Self {
1803 let mut buttons = vec![
1804 Button::new("Retry", 25, 30, 16),
1805 Button::new("Main Menu", 45, 30, 16),
1806 ];
1807 buttons[0].focused = true;
1808 Self { data, buttons, selected: 0, anim_time: 0.0 }
1809 }
1810}
1811
1812impl MenuScreen for GameOverScreen {
1813 fn name(&self) -> &str { "GameOver" }
1814
1815 fn update(&mut self, dt: f32) {
1816 self.anim_time += dt;
1817 }
1818
1819 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1820 let pulse = (self.anim_time * 2.0).sin() * 30.0 + 200.0;
1821 let r = pulse as u8;
1822 buf.put_str_bold(22, 4, "G A M E O V E R", (r, 30, 30));
1823 buf.put_str(15, 8, &format!("Cause of death: {}", self.data.cause), (220, 120, 120));
1824 buf.put_str(15, 10, &format!("Score: {}", self.data.score), (200, 200, 100));
1825 let mins = (self.data.survival_time / 60.0) as u64;
1826 let secs = (self.data.survival_time % 60.0) as u64;
1827 buf.put_str(15, 12, &format!("Survival time: {}m {}s", mins, secs), (180, 200, 180));
1828 buf.put_str(15, 14, &format!("Enemies killed: {}", self.data.kills), (180, 180, 220));
1829 buf.put_str(15, 16, &format!("Level reached: {}", self.data.level_reached), (180, 220, 180));
1830 for btn in &self.buttons {
1831 btn.render(buf);
1832 }
1833 buf.put_str(20, 46, "LEFT/RIGHT: Select ENTER: Confirm", (120, 120, 120));
1834 }
1835
1836 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1837 match input {
1838 InputEvent::KeyDown(KeyCode::Left) | InputEvent::KeyDown(KeyCode::A) => {
1839 self.buttons[self.selected].focused = false;
1840 self.selected = self.selected.saturating_sub(1);
1841 self.buttons[self.selected].focused = true;
1842 }
1843 InputEvent::KeyDown(KeyCode::Right) | InputEvent::KeyDown(KeyCode::D) => {
1844 self.buttons[self.selected].focused = false;
1845 self.selected = (self.selected + 1).min(self.buttons.len() - 1);
1846 self.buttons[self.selected].focused = true;
1847 }
1848 InputEvent::KeyDown(KeyCode::Enter) => {
1849 return match self.selected {
1850 0 => MenuAction::Retry,
1851 _ => MenuAction::ReturnToMainMenu,
1852 };
1853 }
1854 _ => {}
1855 }
1856 MenuAction::None
1857 }
1858}
1859
1860pub struct VictoryScreen {
1863 score: super::Score,
1864 loot: Vec<String>,
1865 buttons: Vec<Button>,
1866 selected: usize,
1867 anim_time: f32,
1868 scroll: f32,
1869}
1870
1871impl VictoryScreen {
1872 pub fn new(score: super::Score, loot: Vec<String>) -> Self {
1873 let mut buttons = vec![
1874 Button::new("Continue", 25, 38, 16),
1875 Button::new("Main Menu", 45, 38, 16),
1876 ];
1877 buttons[0].focused = true;
1878 Self { score, loot, buttons, selected: 0, anim_time: 0.0, scroll: 0.0 }
1879 }
1880}
1881
1882impl MenuScreen for VictoryScreen {
1883 fn name(&self) -> &str { "Victory" }
1884
1885 fn update(&mut self, dt: f32) {
1886 self.anim_time += dt;
1887 self.scroll += dt * 20.0;
1888 }
1889
1890 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1891 let pulse = (self.anim_time * 3.0).sin() * 30.0 + 200.0;
1892 buf.put_str_bold(20, 3, "V I C T O R Y !", (pulse as u8, pulse as u8, 50));
1893
1894 buf.put_str(15, 7, "─── Score Breakdown ───────────────────────", (150, 150, 200));
1896 buf.put_str(15, 9, &format!("Base Score: {:>10}", self.score.base), (180, 180, 200));
1897 buf.put_str(15, 11, &format!("Combo Bonus: {:>10}", self.score.combo_bonus), (200, 180, 150));
1898 buf.put_str(15, 13, &format!("Time Bonus: {:>10}", self.score.time_bonus), (180, 200, 150));
1899 buf.put_str(15, 15, &format!("Style Bonus: {:>10}", self.score.style_bonus), (200, 150, 200));
1900 buf.put_str(15, 17, &format!("Total: {:>10}", self.score.total), (255, 220, 0));
1901 buf.put_str(15, 19, &format!("Grade: {:>10}", self.score.grade()), (255, 200, 100));
1902
1903 buf.put_str(15, 22, "─── Items Received ────────────────────────", (150, 150, 200));
1905 for (i, item) in self.loot.iter().take(8).enumerate() {
1906 buf.put_str(17, 24 + i as u32, &format!("• {}", item), (180, 220, 180));
1907 }
1908
1909 for btn in &self.buttons {
1910 btn.render(buf);
1911 }
1912 }
1913
1914 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
1915 match input {
1916 InputEvent::KeyDown(KeyCode::Left) | InputEvent::KeyDown(KeyCode::A) => {
1917 self.buttons[self.selected].focused = false;
1918 self.selected = self.selected.saturating_sub(1);
1919 self.buttons[self.selected].focused = true;
1920 }
1921 InputEvent::KeyDown(KeyCode::Right) | InputEvent::KeyDown(KeyCode::D) => {
1922 self.buttons[self.selected].focused = false;
1923 self.selected = (self.selected + 1).min(self.buttons.len() - 1);
1924 self.buttons[self.selected].focused = true;
1925 }
1926 InputEvent::KeyDown(KeyCode::Enter) => {
1927 return match self.selected {
1928 0 => MenuAction::Pop,
1929 _ => MenuAction::ReturnToMainMenu,
1930 };
1931 }
1932 InputEvent::KeyDown(KeyCode::Escape) => return MenuAction::Pop,
1933 _ => {}
1934 }
1935 MenuAction::None
1936 }
1937}
1938
1939pub struct CreditsScreen {
1942 scroll: f32,
1943 speed: f32,
1944 lines: Vec<(String, (u8, u8, u8), bool)>,
1945}
1946
1947impl CreditsScreen {
1948 pub fn new() -> Self {
1949 let lines = vec![
1950 ("PROOF ENGINE".to_string(), (200, 200, 255), true),
1951 ("".to_string(), (0,0,0), false),
1952 ("A Mathematical Rendering Engine".to_string(), (180, 180, 200), false),
1953 ("".to_string(), (0,0,0), false),
1954 ("─────────────────────────────────".to_string(), (100, 100, 150), false),
1955 ("".to_string(), (0,0,0), false),
1956 ("PROGRAMMING".to_string(), (255, 220, 0), true),
1957 ("Lead Engineer".to_string(), (200, 200, 200), false),
1958 ("Math Systems".to_string(), (200, 200, 200), false),
1959 ("Rendering Pipeline".to_string(), (200, 200, 200), false),
1960 ("Physics Engine".to_string(), (200, 200, 200), false),
1961 ("".to_string(), (0,0,0), false),
1962 ("DESIGN".to_string(), (255, 220, 0), true),
1963 ("Game Design".to_string(), (200, 200, 200), false),
1964 ("Level Design".to_string(), (200, 200, 200), false),
1965 ("UI/UX Design".to_string(), (200, 200, 200), false),
1966 ("".to_string(), (0,0,0), false),
1967 ("ART & AUDIO".to_string(), (255, 220, 0), true),
1968 ("Glyph Design".to_string(), (200, 200, 200), false),
1969 ("Music Composition".to_string(), (200, 200, 200), false),
1970 ("Sound Effects".to_string(), (200, 200, 200), false),
1971 ("".to_string(), (0,0,0), false),
1972 ("SPECIAL THANKS".to_string(), (255, 220, 0), true),
1973 ("The Rust Community".to_string(), (200, 200, 200), false),
1974 ("glam — Linear Algebra Library".to_string(), (200, 200, 200), false),
1975 ("All our playtesters".to_string(), (200, 200, 200), false),
1976 ("".to_string(), (0,0,0), false),
1977 ("─────────────────────────────────".to_string(), (100, 100, 150), false),
1978 ("".to_string(), (0,0,0), false),
1979 ("Built with pure Rust + mathematics".to_string(), (180, 180, 220), false),
1980 ("Every visual is an equation.".to_string(), (180, 180, 220), false),
1981 ("".to_string(), (0,0,0), false),
1982 ("© 2026 Proof Engine Project".to_string(), (150, 150, 150), false),
1983 ];
1984 Self { scroll: 50.0, speed: 8.0, lines }
1985 }
1986}
1987
1988impl MenuScreen for CreditsScreen {
1989 fn name(&self) -> &str { "Credits" }
1990
1991 fn update(&mut self, dt: f32) {
1992 self.scroll -= self.speed * dt;
1993 }
1994
1995 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
1996 for (i, (text, color, bold)) in self.lines.iter().enumerate() {
1997 let y = self.scroll as i32 + i as i32 * 2;
1998 if y < 0 || y > 48 { continue; }
1999 let x = (40u32).saturating_sub(text.len() as u32 / 2);
2000 if *bold {
2001 buf.put_str_bold(x, y as u32, text, *color);
2002 } else {
2003 buf.put_str(x, y as u32, text, *color);
2004 }
2005 }
2006 buf.put_str(30, 48, "ESC: Back", (120, 120, 120));
2007 }
2008
2009 fn handle_input(&mut self, input: &InputEvent) -> MenuAction {
2010 match input {
2011 InputEvent::KeyDown(KeyCode::Escape) | InputEvent::KeyDown(KeyCode::Enter) => {
2012 return MenuAction::Pop;
2013 }
2014 InputEvent::KeyDown(KeyCode::Up) => {
2015 self.scroll += 5.0;
2016 }
2017 InputEvent::KeyDown(KeyCode::Down) => {
2018 self.scroll -= 5.0;
2019 }
2020 _ => {}
2021 }
2022 MenuAction::None
2023 }
2024}
2025
2026pub struct LoadingScreen {
2029 progress: super::LoadProgress,
2030 tips: Vec<String>,
2031 tip_index: usize,
2032 tip_timer: f32,
2033 bg: BackgroundAnimator,
2034 anim_time: f32,
2035}
2036
2037impl LoadingScreen {
2038 pub fn new(progress: super::LoadProgress) -> Self {
2039 let tips = vec![
2040 "Tip: Use the combo system to multiply your score!".to_string(),
2041 "Tip: Enemies have elemental weaknesses. Exploit them!".to_string(),
2042 "Tip: Rest at bonfires to restore health.".to_string(),
2043 "Tip: Every mathematical function creates unique visuals.".to_string(),
2044 "Tip: Unlock skills in the progression tree to customize your build.".to_string(),
2045 "Tip: Secret areas often contain rare loot.".to_string(),
2046 "Tip: High scores earn extra achievement points.".to_string(),
2047 "Tip: The Lorenz attractor is a real chaotic system.".to_string(),
2048 ];
2049 Self {
2050 progress,
2051 tips,
2052 tip_index: 0,
2053 tip_timer: 0.0,
2054 bg: BackgroundAnimator::new(99),
2055 anim_time: 0.0,
2056 }
2057 }
2058
2059 fn render_progress_bar(&self, buf: &mut MenuBuffer, x: u32, y: u32, width: u32) {
2060 let fraction = self.progress.fraction();
2061 let filled = (fraction * width as f32) as u32;
2062 buf.put(x - 1, y, MenuCell::new('[').with_fg(180, 180, 220));
2063 buf.put(x + width, y, MenuCell::new(']').with_fg(180, 180, 220));
2064 for i in 0..width {
2065 let ch = if i < filled { '█' } else { '░' };
2066 let fg = if i < filled { (100, 200, 100) } else { (60, 60, 80) };
2067 buf.put(x + i, y, MenuCell::new(ch).with_fg(fg.0, fg.1, fg.2));
2068 }
2069 let pct = format!("{:.0}%", fraction * 100.0);
2070 buf.put_str(x + width / 2 - 2, y + 2, &pct, (200, 200, 200));
2071 }
2072}
2073
2074impl MenuScreen for LoadingScreen {
2075 fn name(&self) -> &str { "Loading" }
2076
2077 fn update(&mut self, dt: f32) {
2078 self.anim_time += dt;
2079 self.bg.update(dt);
2080 self.tip_timer += dt;
2081 if self.tip_timer > 5.0 {
2082 self.tip_timer = 0.0;
2083 self.tip_index = (self.tip_index + 1) % self.tips.len();
2084 }
2085 }
2086
2087 fn render(&self, _ctx: &MenuRenderCtx, buf: &mut MenuBuffer) {
2088 self.bg.render(buf);
2089 let spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2091 let spin_ch = spinners[(self.anim_time * 10.0) as usize % spinners.len()];
2092 buf.put(38, 18, MenuCell::new(spin_ch).with_fg(100, 200, 255));
2093
2094 buf.put_str_bold(30, 20, "LOADING...", (180, 180, 255));
2095 buf.put_str(15, 22, &format!("Stage: {}", self.progress.stage), (160, 160, 200));
2096
2097 self.render_progress_bar(buf, 10, 25, 60);
2098
2099 if let Some(tip) = self.tips.get(self.tip_index) {
2101 buf.put_str(8, 34, tip, (160, 200, 160));
2102 }
2103 }
2104
2105 fn handle_input(&mut self, _input: &InputEvent) -> MenuAction {
2106 MenuAction::None
2107 }
2108}
2109
2110pub struct MenuRenderer {
2113 pub buf: MenuBuffer,
2114 pub ctx: MenuRenderCtx,
2115}
2116
2117impl MenuRenderer {
2118 pub fn new(width: u32, height: u32) -> Self {
2119 Self {
2120 buf: MenuBuffer::new(width, height),
2121 ctx: MenuRenderCtx::new(width, height),
2122 }
2123 }
2124
2125 pub fn render_screen(&mut self, screen: &dyn MenuScreen) {
2126 self.buf.clear();
2127 screen.render(&self.ctx, &mut self.buf);
2128 }
2129
2130 pub fn render_stack(&mut self, stack: &MenuStack) {
2131 self.buf.clear();
2132 stack.render(&self.ctx, &mut self.buf);
2133 }
2134
2135 pub fn tick(&mut self, dt: f32) {
2136 self.ctx.time += dt;
2137 self.ctx.dt = dt;
2138 self.ctx.frame += 1;
2139 }
2140
2141 pub fn to_ansi_string(&self) -> String {
2142 let mut out = String::new();
2143 for row in &self.buf.cells {
2144 for cell in row {
2145 if cell.ch == ' ' && cell.bg == (0, 0, 0) {
2146 out.push(' ');
2147 } else {
2148 let (fr, fg_b, fb) = cell.fg;
2149 out.push_str(&format!("\x1b[38;2;{};{};{}m{}\x1b[0m", fr, fg_b, fb, cell.ch));
2150 }
2151 }
2152 out.push('\n');
2153 }
2154 out
2155 }
2156}
2157
2158#[cfg(test)]
2161mod tests {
2162 use super::*;
2163
2164 #[test]
2165 fn test_menu_stack_push_pop() {
2166 let mut stack = MenuStack::new();
2167 assert!(stack.is_empty());
2168 stack.push(Box::new(MainMenuScreen::new(false)));
2169 assert_eq!(stack.depth(), 1);
2170 stack.push(Box::new(CreditsScreen::new()));
2171 assert_eq!(stack.depth(), 2);
2172 stack.pop();
2173 assert_eq!(stack.depth(), 1);
2174 assert_eq!(stack.current().map(|s| s.name()), Some("MainMenu"));
2175 }
2176
2177 #[test]
2178 fn test_menu_stack_pop_to_root() {
2179 let mut stack = MenuStack::new();
2180 stack.push(Box::new(MainMenuScreen::new(false)));
2181 stack.push(Box::new(SettingsScreen::new()));
2182 stack.push(Box::new(CreditsScreen::new()));
2183 assert_eq!(stack.depth(), 3);
2184 stack.pop_to_root();
2185 assert_eq!(stack.depth(), 1);
2186 }
2187
2188 #[test]
2189 fn test_button_render() {
2190 let mut buf = MenuBuffer::new(80, 50);
2191 let mut btn = Button::new("Play", 10, 10, 12);
2192 btn.focused = true;
2193 btn.render(&mut buf);
2194 assert_eq!(buf.cells[10][10].ch, '┌');
2196 }
2197
2198 #[test]
2199 fn test_main_menu_keyboard_nav() {
2200 let mut screen = MainMenuScreen::new(false);
2201 let input_down = InputEvent::KeyDown(KeyCode::Down);
2202 screen.handle_input(&input_down);
2203 assert_eq!(screen.selected, 1);
2204 }
2205
2206 #[test]
2207 fn test_settings_audio_adjust() {
2208 let mut settings = SettingsScreen::new();
2209 settings.handle_input(&InputEvent::KeyDown(KeyCode::Tab));
2211 assert_eq!(settings.current_tab, SettingsTab::Audio);
2212 let initial_vol = settings.audio.master;
2213 settings.handle_input(&InputEvent::KeyDown(KeyCode::Right));
2214 assert!(settings.audio.master > initial_vol - 0.01);
2215 }
2216
2217 #[test]
2218 fn test_dialog_resolve() {
2219 let mut dlg = Dialog::new("Really quit?");
2220 dlg.handle_input(&InputEvent::KeyDown(KeyCode::Left)); dlg.handle_input(&InputEvent::KeyDown(KeyCode::Enter));
2222 assert!(dlg.is_resolved());
2223 assert_eq!(dlg.answer(), Some(true));
2224 }
2225
2226 #[test]
2227 fn test_dialog_cancel() {
2228 let mut dlg = Dialog::new("Delete?");
2229 dlg.handle_input(&InputEvent::KeyDown(KeyCode::Escape));
2230 assert!(dlg.is_resolved());
2231 assert_eq!(dlg.answer(), Some(false));
2232 }
2233
2234 #[test]
2235 fn test_character_class_stats() {
2236 let warrior = CharacterClass::Warrior;
2237 let mage = CharacterClass::Mage;
2238 assert!(warrior.stats().hp > mage.stats().hp);
2239 assert!(mage.stats().mp > warrior.stats().mp);
2240 }
2241
2242 #[test]
2243 fn test_new_game_screen_name_input() {
2244 let mut screen = NewGameScreen::new();
2245 screen.focused_field = 0;
2246 screen.handle_input(&InputEvent::CharInput('A'));
2247 screen.handle_input(&InputEvent::CharInput('l'));
2248 screen.handle_input(&InputEvent::CharInput('i'));
2249 assert_eq!(screen.name_input, "Ali");
2250 screen.handle_input(&InputEvent::KeyDown(KeyCode::Backspace));
2251 assert_eq!(screen.name_input, "Al");
2252 }
2253
2254 #[test]
2255 fn test_background_animator() {
2256 let mut anim = BackgroundAnimator::new(12345);
2257 let initial_x = anim.glyphs[0].x;
2258 anim.update(1.0);
2259 let new_x = anim.glyphs[0].x;
2261 assert!((new_x - initial_x).abs() > 0.001 || anim.glyphs[0].speed_x.abs() < 0.001);
2262 }
2263
2264 #[test]
2265 fn test_menu_buffer_put_str() {
2266 let mut buf = MenuBuffer::new(80, 50);
2267 buf.put_str(5, 5, "Hello", (255, 0, 0));
2268 assert_eq!(buf.cells[5][5].ch, 'H');
2269 assert_eq!(buf.cells[5][6].ch, 'e');
2270 assert_eq!(buf.cells[5][9].ch, 'o');
2271 }
2272
2273 #[test]
2274 fn test_load_game_screen_delete() {
2275 let mut screen = LoadGameScreen::with_demo_slots();
2276 assert!(screen.slots[0].occupied);
2277 screen.handle_input(&InputEvent::KeyDown(KeyCode::Delete));
2279 assert!(screen.delete_dialog.is_some());
2280 screen.handle_input(&InputEvent::KeyDown(KeyCode::Left)); screen.handle_input(&InputEvent::KeyDown(KeyCode::Enter));
2283 assert!(!screen.slots[0].occupied);
2284 }
2285}