//! Screen-Stack und Antwort — Ticket 11, im Durchstich (12) bestaetigt. use ratatui::crossterm::event::KeyCode; use crate::data::Registries; use crate::game::{Command, CreatureRef, Direction, Game}; /// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay /// ausweisen — ein gemeinsamer `Vec` statt `current` plus /// `Option`, weil Popup ueber Popup real ist. pub enum Screen { MainMenu, Explore, /// Menue-Kampf (Charter): Faehigkeitsliste, Zielwahl mit den Zifferntasten. Combat { /// Cursor in der Faehigkeitsliste. pick: usize, }, /// Zwei Bereiche: Inventar mit Cursor, Ausrüstung als Registry-Schleife. Inventory { pick: usize, }, CharacterSheet, /// Vollbild, kein Overlay. Bewusst nicht die lazy Variante mit einer /// Meldungszeile im Hauptmenue: Ein Abstiegs-Roguelike lebt davon, dass der /// Lauf am Ende *erzaehlt* wird (Ticket 14). GameOver, /// **Flache Optionsliste**, kein Knotengraph (M4): Was erst nach einer /// Antwort erscheint, ist eine Option mit einer Bedingung. Dialog { pick: usize, }, /// Zwei Spalten: Angebot links, eigenes Inventar rechts. `Tab` wechselt. Shop { pick: usize, /// `false` = Angebot, `true` = Inventar. Ein `bool` statt eines Enums /// mit zwei Werten, die nie mehr werden. selling: bool, }, // TODO(M5+): `CharacterCreation`, `QuestLog`, `Confirm` — elf sind es am // Ende (11/14). } /// Der Screen **gibt zurueck, statt selbst zu schieben**: sonst haelt er ein /// `&mut` auf genau die `Vec`, in der er liegt. Nebeneffekt — `Response::Command` /// ist der einzige Weg nach `game/`. pub enum Response { None, Pop, Push(Screen), Command(Command), Quit, } impl Screen { pub fn is_overlay(&self) -> bool { // TODO(M2): `Confirm` ist das erste echte Overlay. Bis dahin haelt die // Regel „ein Command schliesst alle Overlays" nur die Luft. false } /// **Befund aus M3:** Ticket 11 hatte `handle_key(&mut self, key)` ohne /// Kontext festgelegt. Der Kampf-Screen muss aber wissen, wie viele /// Faehigkeiten es gibt, um den Cursor zu klemmen, und welche der Cursor /// meint. Lesender Zugriff genuegt — die Regel „ein Screen fasst `Game` /// nicht an" bleibt, weil nur `Response::Command` etwas aendert. pub fn handle_key(&mut self, key: KeyCode, game: &Game, reg: &Registries) -> Response { match self { Screen::MainMenu => match key { KeyCode::Enter => Response::Push(Screen::Explore), KeyCode::Char('q') | KeyCode::Esc => Response::Quit, _ => Response::None, }, Screen::Explore => match key { // Pfeiltasten *und* hjkl — drei zusaetzliche Arme sind die // billige Hoeflichkeit gegenueber Vim-Fingern (Ticket 11). KeyCode::Up | KeyCode::Char('k') => { Response::Command(Command::Move(Direction::North)) } KeyCode::Down | KeyCode::Char('j') => { Response::Command(Command::Move(Direction::South)) } KeyCode::Left | KeyCode::Char('h') => { Response::Command(Command::Move(Direction::West)) } KeyCode::Right | KeyCode::Char('l') => { Response::Command(Command::Move(Direction::East)) } KeyCode::Char('.') => Response::Command(Command::Wait), KeyCode::Char('>') => Response::Command(Command::Descend), KeyCode::Char('g') => Response::Command(Command::PickUp), KeyCode::Char('t') => Response::Command(Command::Talk), // Screen-Navigation ist reine UI und fasst `Game` nie an. KeyCode::Char('c') => Response::Push(Screen::CharacterSheet), KeyCode::Char('i') => Response::Push(Screen::Inventory { pick: 0 }), KeyCode::Esc => Response::Pop, _ => Response::None, }, Screen::Combat { pick } => { let abilities = game.abilities_of(CreatureRef::Player, reg); if abilities.is_empty() { return Response::None; } *pick = (*pick).min(abilities.len() - 1); match key { KeyCode::Up | KeyCode::Char('k') => { *pick = pick.saturating_sub(1); Response::None } KeyCode::Down | KeyCode::Char('j') => { *pick = (*pick + 1).min(abilities.len() - 1); Response::None } // Ziel ueber die Zifferntasten; ohne Gegner (Flucht, // Selbstheilung) zaehlt der Held selbst als Ziel. KeyCode::Enter | KeyCode::Char('1'..='9') => { let index = match key { KeyCode::Char(c @ '1'..='9') => (c as u8 - b'1') as usize, _ => 0, }; let target = if game.enemies.is_empty() { CreatureRef::Player } else { CreatureRef::Enemy(index.min(game.enemies.len() - 1)) }; Response::Command(Command::UseAbility { ability: abilities[*pick].clone(), target, }) } KeyCode::Char('.') => Response::Command(Command::Wait), KeyCode::Char('c') => Response::Push(Screen::CharacterSheet), _ => Response::None, } } Screen::Inventory { pick } => { let n = game.player.inventory.len(); if n > 0 { *pick = (*pick).min(n - 1); } match key { KeyCode::Up | KeyCode::Char('k') => { *pick = pick.saturating_sub(1); Response::None } KeyCode::Down | KeyCode::Char('j') => { *pick = (*pick + 1).min(n.saturating_sub(1)); Response::None } KeyCode::Enter | KeyCode::Char('u') if n > 0 => { Response::Command(Command::UseItem(*pick)) } KeyCode::Char('e') if n > 0 => Response::Command(Command::Equip(*pick)), KeyCode::Char('d') if n > 0 => Response::Command(Command::Drop(*pick)), // Ablegen ueber die Slot-Reihenfolge aus der Registry — // der Charakterbogen mauert kein Layout (Ticket 02). KeyCode::Char(c @ '1'..='9') => { let index = (c as u8 - b'1') as usize; match reg.slots_ordered().get(index) { Some(slot) => { Response::Command(Command::Unequip(slot.id.clone())) } None => Response::None, } } KeyCode::Esc | KeyCode::Char('i') | KeyCode::Char('q') => Response::Pop, _ => Response::None, } } Screen::CharacterSheet => match key { KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('q') => Response::Pop, _ => Response::None, }, Screen::Dialog { pick } => { let options = game.dialog_options(reg); match key { KeyCode::Up | KeyCode::Char('k') => { *pick = pick.saturating_sub(1); Response::None } KeyCode::Down | KeyCode::Char('j') => { *pick = (*pick + 1).min(options.len().saturating_sub(1)); Response::None } KeyCode::Enter | KeyCode::Char('1'..='9') => { let i = match key { KeyCode::Char(c @ '1'..='9') => (c as u8 - b'1') as usize, _ => *pick, }; match options.get(i) { Some(_) => Response::Command(Command::Choose(i)), None => Response::None, } } KeyCode::Esc | KeyCode::Char('q') => Response::Pop, _ => Response::None, } } Screen::Shop { pick, selling } => { let n = if *selling { game.player.inventory.len() } else { game.shop_stock().map_or(0, |s| s.len()) }; if n > 0 { *pick = (*pick).min(n - 1); } match key { KeyCode::Tab | KeyCode::Left | KeyCode::Right => { *selling = !*selling; *pick = 0; Response::None } KeyCode::Up | KeyCode::Char('k') => { *pick = pick.saturating_sub(1); Response::None } KeyCode::Down | KeyCode::Char('j') => { *pick = (*pick + 1).min(n.saturating_sub(1)); Response::None } KeyCode::Enter if n > 0 && *selling => { Response::Command(Command::Sell(*pick)) } KeyCode::Enter if n > 0 => Response::Command(Command::Buy(*pick)), KeyCode::Esc | KeyCode::Char('q') => Response::Pop, _ => Response::None, } } Screen::GameOver => Response::Quit, } } }