feat(debug): Werkbank auf F12
Gegenstandsliste mit Cursor, dazu heilen, absteigen und Raum leeren. Laeuft ueber ein Kommando Debug(Cheat) statt am Spielkern vorbei — die Zusage aus Ticket 11, dass Response::Command der einzige Weg nach game/ ist, gilt auch fuer Cheats. ClearRoom toetet die Gegner, statt die Liste zu leeren: So laeuft der normale Weg samt Todes-Triggern und "Der Raum ist geraeumt".
This commit is contained in:
@@ -459,6 +459,15 @@ pub struct Registries {
|
||||
}
|
||||
|
||||
impl Registries {
|
||||
/// Alle Gegenstaende in stabiler Reihenfolge — fuer die Werkbank. Ohne das
|
||||
/// haengt die Liste an der HashMap-Iterationsreihenfolge und der Cursor
|
||||
/// zeigt bei jedem Start woanders hin.
|
||||
pub fn items_ordered(&self) -> Vec<&Item> {
|
||||
let mut v: Vec<&Item> = self.items.values().collect();
|
||||
v.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
v
|
||||
}
|
||||
|
||||
/// Slots in Anzeigereihenfolge. Der Charakterbogen schleift darueber,
|
||||
/// statt ein Layout zu mauern (Ticket 02).
|
||||
pub fn slots_ordered(&self) -> Vec<&Slot> {
|
||||
|
||||
@@ -44,6 +44,26 @@ pub enum Command {
|
||||
ability: String,
|
||||
target: super::creature::CreatureRef,
|
||||
},
|
||||
/// Werkbank. **Ein** Kommando mit einer Variantenliste statt fuenf
|
||||
/// Kommandos, die sich durch `rejects` und `perform` ziehen — ein Cheat
|
||||
/// wird nie abgewiesen, das ist sein ganzer Zweck.
|
||||
Debug(Cheat),
|
||||
}
|
||||
|
||||
/// Was die Werkbank kann. Bewusst klein: Was sich mit zwei vorhandenen
|
||||
/// Kommandos nachstellen laesst, gehoert nicht hierher.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Cheat {
|
||||
/// Gibt den Gegenstand — zehn Stueck, wenn er stapelbar ist. Sonst muesste
|
||||
/// man fuer hundert Gold hundertmal druecken.
|
||||
Give(String),
|
||||
/// Leben und Ausdauer voll.
|
||||
Heal,
|
||||
/// Eine Etage tiefer, ohne Treppe.
|
||||
Descend,
|
||||
/// Setzt alle Gegner auf null Leben. **Nicht** `enemies.clear()`: So laeuft
|
||||
/// der normale Weg samt Todes-Triggern und „Der Raum ist geräumt".
|
||||
ClearRoom,
|
||||
}
|
||||
|
||||
/// Ergebnis eines Kommandos — der Befund aus dem Durchstich in Ticket 12:
|
||||
|
||||
@@ -14,7 +14,7 @@ pub mod rng;
|
||||
|
||||
pub use action::{Action, ActiveModifier, DamageType, StateField, Target};
|
||||
pub use attributes::{Attribute, Modifier};
|
||||
pub use command::{Command, Direction, Outcome};
|
||||
pub use command::{Cheat, Command, Direction, Outcome};
|
||||
pub use condition::{Check, Condition, Who};
|
||||
pub use creature::{Creature, CreatureRef, ItemInstance};
|
||||
pub use event::{Event, EventKind, Trigger};
|
||||
@@ -599,6 +599,8 @@ impl Game {
|
||||
// Sein eigenes Gold zurueckzuverkaufen ergibt keinen Sinn.
|
||||
Command::Sell(i) => (self.player.inventory[*i].def == "core:gold")
|
||||
.then_some("Gold verkauft man nicht"),
|
||||
// Ein Cheat wird nie abgewiesen — das ist sein Zweck.
|
||||
Command::Debug(_) => None,
|
||||
Command::Wait => None,
|
||||
}
|
||||
}
|
||||
@@ -746,6 +748,7 @@ impl Game {
|
||||
Command::Choose(i) => self.choose(*i, reg),
|
||||
Command::Buy(i) => self.buy(*i, reg),
|
||||
Command::Sell(i) => self.sell(*i, reg),
|
||||
Command::Debug(cheat) => self.cheat(cheat, reg),
|
||||
Command::Drop(i) => {
|
||||
let inst = self.player.inventory.remove(*i);
|
||||
let name = self.item_label(&inst, reg);
|
||||
@@ -837,6 +840,37 @@ impl Game {
|
||||
self.log(format!("{name} verkauft fuer {preis} Gold"));
|
||||
}
|
||||
|
||||
/// Die Werkbank. Steht im Spielkern und nicht in der UI, weil sie durch
|
||||
/// dieselbe Tuer muss wie alles andere: `Command` ist der einzige Weg
|
||||
/// hinein (Ticket 11).
|
||||
fn cheat(&mut self, cheat: &Cheat, reg: &Registries) {
|
||||
match cheat {
|
||||
Cheat::Give(id) => {
|
||||
let stapelbar = reg.items.get(id).is_some_and(|d| d.stackable);
|
||||
let inst = ItemInstance::stack(id.clone(), if stapelbar { 10 } else { 1 });
|
||||
let name = self.item_label(&inst, reg);
|
||||
self.give(inst, reg);
|
||||
self.log(format!("[debug] {name}"));
|
||||
}
|
||||
Cheat::Heal => {
|
||||
let p = CreatureRef::Player;
|
||||
self.player.health = self.attribute(p, Attribute::MaxHealth, None, reg);
|
||||
self.player.stamina = self.attribute(p, Attribute::MaxStamina, None, reg);
|
||||
self.log(String::from("[debug] geheilt"));
|
||||
}
|
||||
Cheat::Descend => {
|
||||
self.log(String::from("[debug] abgestiegen"));
|
||||
self.descend(reg);
|
||||
}
|
||||
Cheat::ClearRoom => {
|
||||
for e in &mut self.enemies {
|
||||
e.health = 0.0;
|
||||
}
|
||||
self.log(String::from("[debug] Raum geleert"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Nimmt **ein Stueck** aus dem Eintrag an `index` und gibt es zurueck.
|
||||
/// Ueber den Index, nicht ueber die `def`: Zwei Traenke mit verschiedenen
|
||||
/// Affixen teilen sich die `def`, und `take` naehme den erstbesten.
|
||||
@@ -1688,4 +1722,31 @@ mod tests {
|
||||
"der leere Eintrag muss verschwinden"
|
||||
);
|
||||
}
|
||||
|
||||
/// Die Werkbank laeuft durch dieselbe Tuer wie alles andere — sie ist ein
|
||||
/// `Command`, kein Seiteneingang in `Game`. Der Test haelt genau das fest.
|
||||
#[test]
|
||||
fn werkbank_gibt_stapelbares_im_stapel() {
|
||||
let (mut game, reg) = zwergenkrieger();
|
||||
|
||||
let cheat = Command::Debug;
|
||||
game.execute(cheat(Cheat::Give(String::from("core:gold"))), ®);
|
||||
game.execute(cheat(Cheat::Give(String::from("core:iron_sword"))), ®);
|
||||
assert_eq!(game.gold(), 10, "stapelbares kommt zu zehnt");
|
||||
assert_eq!(game.count_of("core:iron_sword"), 1, "der Rest einzeln");
|
||||
|
||||
game.player.health = 1.0;
|
||||
game.player.stamina = 0.0;
|
||||
game.execute(cheat(Cheat::Heal), ®);
|
||||
let voll = game.attribute(CreatureRef::Player, Attribute::MaxHealth, None, ®);
|
||||
assert_eq!(game.player.health, voll);
|
||||
|
||||
// Gegner werden getoetet, nicht entfernt — so laeuft der normale Weg
|
||||
// samt „Der Raum ist geräumt".
|
||||
let ratte = reg.creatures.get("core:rat").expect("Riesenratte");
|
||||
game.enemies = vec![Creature::from_def(ratte)];
|
||||
game.execute(cheat(Cheat::ClearRoom), ®);
|
||||
assert!(game.enemies.is_empty());
|
||||
assert!(game.journal.iter().any(|z| z.contains("geräumt")), "{:?}", game.journal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ fn draw(frame: &mut Frame, app: &App) {
|
||||
Screen::CharacterSheet => draw_sheet(frame, area, app),
|
||||
Screen::Dialog { pick } => draw_dialog(frame, area, app, *pick),
|
||||
Screen::Shop { pick, selling } => draw_shop(frame, area, app, *pick, *selling),
|
||||
Screen::Debug { pick } => draw_debug(frame, area, app, *pick),
|
||||
Screen::GameOver => draw_game_over(frame, area, app),
|
||||
}
|
||||
}
|
||||
@@ -344,6 +345,34 @@ fn draw_shop(frame: &mut Frame, area: Rect, app: &App, pick: usize, selling: boo
|
||||
);
|
||||
}
|
||||
|
||||
/// Die Werkbank. Eine Liste, vier Tasten — was sich mit zwei vorhandenen
|
||||
/// Kommandos nachstellen laesst, steht hier nicht.
|
||||
fn draw_debug(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
// ponytail: kein mitlaufender Ausschnitt wie im Grid — mehr Gegenstaende
|
||||
// als Zeilen fallen stumm hinten raus. `core` hat sieben; der Ausschnitt
|
||||
// kommt, wenn der erste Mod das sprengt.
|
||||
let mut zeilen = Vec::new();
|
||||
for (i, def) in app.registries.items_ordered().iter().enumerate() {
|
||||
let zeiger = if i == pick { "›" } else { " " };
|
||||
let menge = if def.stackable { "×10" } else { " " };
|
||||
zeilen.push(
|
||||
Line::from(format!("{zeiger} {:<26} {menge} {}", def.name, def.id))
|
||||
.style(Style::new().fg(if i == pick { Color::Yellow } else { Color::Reset })),
|
||||
);
|
||||
}
|
||||
zeilen.push(Line::from(""));
|
||||
zeilen.push(
|
||||
Line::from("↑↓ wählen · Enter geben · h heilen · > abstiegen · x Raum leeren · Esc")
|
||||
.style(Style::new().fg(Color::DarkGray)),
|
||||
);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(zeilen)
|
||||
.block(Block::bordered().title(" Werkbank ").title_bottom(" F12 ")),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
/// Menue-Kampf (Charter: Dungeon Master / Etrian Odyssey). Links die Gegner mit
|
||||
/// Ziffern, rechts die Faehigkeiten mit Kosten und **Sperrgrund** — der kommt
|
||||
/// aus dem Spielkern, nicht aus der UI (Ticket 14).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use ratatui::crossterm::event::KeyCode;
|
||||
|
||||
use crate::data::Registries;
|
||||
use crate::game::{Command, CreatureRef, Direction, Game};
|
||||
use crate::game::{Cheat, Command, CreatureRef, Direction, Game};
|
||||
|
||||
/// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay
|
||||
/// ausweisen — ein gemeinsamer `Vec<Screen>` statt `current` plus
|
||||
@@ -30,6 +30,12 @@ pub enum Screen {
|
||||
/// Meldungszeile im Hauptmenue: Ein Abstiegs-Roguelike lebt davon, dass der
|
||||
/// Lauf am Ende *erzaehlt* wird (Ticket 14).
|
||||
GameOver,
|
||||
/// Werkbank. Immer mit gebaut statt hinter `cfg(debug_assertions)`: Das
|
||||
/// waere ein `#[cfg]` an der Variante *und* an jedem `match` darueber, fuer
|
||||
/// ein Spiel, das niemand ausliefert. Ein `#[cfg]` reicht spaeter nach.
|
||||
Debug {
|
||||
pick: usize,
|
||||
},
|
||||
/// **Flache Optionsliste**, kein Knotengraph (M4): Was erst nach einer
|
||||
/// Antwort erscheint, ist eine Option mit einer Bedingung.
|
||||
Dialog {
|
||||
@@ -96,6 +102,7 @@ impl Screen {
|
||||
KeyCode::Char('>') => Response::Command(Command::Descend),
|
||||
KeyCode::Char('g') => Response::Command(Command::PickUp),
|
||||
KeyCode::Char('t') => Response::Command(Command::Talk),
|
||||
KeyCode::F(12) => Response::Push(Screen::Debug { pick: 0 }),
|
||||
// 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 }),
|
||||
@@ -155,6 +162,7 @@ impl Screen {
|
||||
}
|
||||
KeyCode::Char('.') => Response::Command(Command::Wait),
|
||||
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
|
||||
KeyCode::F(12) => Response::Push(Screen::Debug { pick: 0 }),
|
||||
_ => Response::None,
|
||||
}
|
||||
}
|
||||
@@ -259,6 +267,32 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
|
||||
Screen::Debug { pick } => {
|
||||
let items = reg.items_ordered();
|
||||
if !items.is_empty() {
|
||||
*pick = (*pick).min(items.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(items.len().saturating_sub(1));
|
||||
Response::None
|
||||
}
|
||||
KeyCode::Enter if !items.is_empty() => {
|
||||
Response::Command(Command::Debug(Cheat::Give(items[*pick].id.clone())))
|
||||
}
|
||||
KeyCode::Char('h') => Response::Command(Command::Debug(Cheat::Heal)),
|
||||
KeyCode::Char('>') => Response::Command(Command::Debug(Cheat::Descend)),
|
||||
KeyCode::Char('x') => Response::Command(Command::Debug(Cheat::ClearRoom)),
|
||||
KeyCode::Esc | KeyCode::F(12) | KeyCode::Char('q') => Response::Pop,
|
||||
_ => Response::None,
|
||||
}
|
||||
}
|
||||
|
||||
Screen::GameOver => Response::Quit,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user