Skip to main content

proof_engine/game/
menu.rs

1//! Complete menu system for proof-engine game state navigation.
2//!
3//! Provides MenuStack, screen implementations, renderer, tooltip system,
4//! dialog system, and all menu navigation logic.
5
6use std::collections::HashMap;
7
8// ─── Key Code ───────────────────────────────────────────────────────────────────
9
10#[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// ─── Input Event ────────────────────────────────────────────────────────────────
52
53#[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// ─── Menu Render Context ─────────────────────────────────────────────────────────
66
67#[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// ─── Menu Cell ──────────────────────────────────────────────────────────────────
97
98#[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
128// ─── Menu Buffer ────────────────────────────────────────────────────────────────
129
130pub 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        // Corners
183        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        // Top/bottom edges
188        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        // Left/right edges
193        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
200// ─── Menu Action ────────────────────────────────────────────────────────────────
201
202pub 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
217// ─── Menu Screen Trait ──────────────────────────────────────────────────────────
218
219pub 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
229// ─── Menu Stack ─────────────────────────────────────────────────────────────────
230
231pub 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// ─── Button ─────────────────────────────────────────────────────────────────────
312
313#[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// ─── Character Class ────────────────────────────────────────────────────────────
359
360#[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// ─── Settings Types ──────────────────────────────────────────────────────────────
437
438#[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// ─── Tooltip ────────────────────────────────────────────────────────────────────
615
616#[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
667// ─── Dialog ─────────────────────────────────────────────────────────────────────
668
669pub 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        // Background
731        buf.fill_rect(x, y, w, h, ' ', (20, 20, 40));
732        buf.draw_box(x, y, w, h, (100, 100, 200));
733        // Title
734        buf.put_str_bold(x + 2, y + 1, &self.title, (200, 200, 255));
735        // Message
736        let msg = &self.message;
737        buf.put_str(x + 2, y + 3, msg, (220, 220, 220));
738        // Buttons
739        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
746// ─── Background Animator ────────────────────────────────────────────────────────
747
748pub 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
839// ─── Main Menu Screen ────────────────────────────────────────────────────────────
840
841pub 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); // Remove "New Game" duplicate if no save
860        }
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        // Subtitle
888        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        // Footer hint
908        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
952// ─── Pause Menu Screen ───────────────────────────────────────────────────────────
953
954pub 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        // Dim overlay hint
979        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// ─── Settings Screen ─────────────────────────────────────────────────────────────
1047
1048#[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        // Separator
1119        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
1332// ─── Character Select Screen ─────────────────────────────────────────────────────
1333
1334pub 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        // Icon
1353        buf.put(x + 7, y + 2, MenuCell::new(class.icon()).with_fg(180, 180, 255));
1354        // Name
1355        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        // Stats preview
1359        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        // Description of selected
1380        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// ─── Level Select Screen ─────────────────────────────────────────────────────────
1412
1413#[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        // Info bar for selected level
1476        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// ─── Save Slot ──────────────────────────────────────────────────────────────────
1523
1524#[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
1549// ─── Load Game Screen ────────────────────────────────────────────────────────────
1550
1551pub 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
1651// ─── New Game Screen ─────────────────────────────────────────────────────────────
1652
1653pub 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, // Normal
1673        }
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        // Name field
1691        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        // Class selector
1702        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        // Difficulty selector
1709        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        // Confirm button
1714        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
1792// ─── Game Over Screen ────────────────────────────────────────────────────────────
1793
1794pub 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
1860// ─── Victory Screen ──────────────────────────────────────────────────────────────
1861
1862pub 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        // Score breakdown
1895        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        // Loot
1904        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
1939// ─── Credits Screen ──────────────────────────────────────────────────────────────
1940
1941pub 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
2026// ─── Loading Screen ──────────────────────────────────────────────────────────────
2027
2028pub 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        // Spinner
2090        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        // Tip
2100        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
2110// ─── Menu Renderer ───────────────────────────────────────────────────────────────
2111
2112pub 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// ─── Tests ──────────────────────────────────────────────────────────────────────
2159
2160#[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        // Box corners should be set
2195        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        // Navigate to audio tab
2210        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)); // focus yes
2221        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        // Glyphs should have moved
2260        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        // Trigger delete dialog
2278        screen.handle_input(&InputEvent::KeyDown(KeyCode::Delete));
2279        assert!(screen.delete_dialog.is_some());
2280        // Confirm deletion
2281        screen.handle_input(&InputEvent::KeyDown(KeyCode::Left)); // focus yes
2282        screen.handle_input(&InputEvent::KeyDown(KeyCode::Enter));
2283        assert!(!screen.slots[0].occupied);
2284    }
2285}