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:
@@ -252,3 +252,23 @@ Alternative gewesen — weniger Tastendrücke, aber eine Bedienung, die es sonst
|
|||||||
Dazu zeigt die Gegnerliste unter jedem Lebensbalken die **Ansage** aus Ticket 14 (`↳ Wuchtschlag →
|
Dazu zeigt die Gegnerliste unter jedem Lebensbalken die **Ansage** aus Ticket 14 (`↳ Wuchtschlag →
|
||||||
dich`, sonst `↳ zögert`). Sie steht bewusst als eigene Zeile und nicht als Kürzel hinter dem Namen:
|
dich`, sonst `↳ zögert`). Sie steht bewusst als eigene Zeile und nicht als Kürzel hinter dem Namen:
|
||||||
Sie ist die Information, auf der die Entscheidung dieses Zuges beruht.
|
Sie ist die Information, auf der die Entscheidung dieses Zuges beruht.
|
||||||
|
|
||||||
|
### Nachtrag — die Werkbank (Debug-Screen, nach M4)
|
||||||
|
|
||||||
|
Ein zwölfter Screen, `F12` aus Erkundung und Kampf: Gegenstandsliste mit Cursor, dazu vier Tasten für
|
||||||
|
heilen, absteigen, Raum leeren.
|
||||||
|
|
||||||
|
**Sie geht durch dieselbe Tür wie alles andere.** Es wäre kürzer gewesen, den Screen direkt auf
|
||||||
|
`Game` schreiben zu lassen — er hat den `&mut` ohnehin nicht, aber man hätte ihn ihm geben können. Das
|
||||||
|
hätte die Zusage aus Ticket 11 gebrochen, dass `Response::Command` der einzige Weg nach `game/` ist,
|
||||||
|
und zwar ausgerechnet an der Stelle, die am seltensten gelesen wird. Stattdessen ein Kommando
|
||||||
|
`Debug(Cheat)` mit vier Varianten — **eins**, nicht vier, sonst zögen sich Cheats durch `rejects` und
|
||||||
|
`perform` wie echte Kommandos. In `rejects` steht genau eine Zeile: Ein Cheat wird nie abgewiesen.
|
||||||
|
|
||||||
|
`ClearRoom` setzt die Lebenspunkte auf null, statt `enemies` zu leeren. So läuft der normale Weg samt
|
||||||
|
Todes-Triggern, Erfahrung (später) und „Der Raum ist geräumt" — ein Cheat, der am Spielablauf
|
||||||
|
vorbeigeht, testet den Spielablauf nicht.
|
||||||
|
|
||||||
|
**Immer mitgebaut**, nicht hinter `cfg(debug_assertions)`. Das wäre ein `#[cfg]` an der Variante *und*
|
||||||
|
an jedem `match` darüber — für ein Spiel, das niemand ausliefert. Ein `#[cfg]` reicht später nach,
|
||||||
|
wenn es je einen Release-Build gibt.
|
||||||
|
|||||||
@@ -459,6 +459,15 @@ pub struct Registries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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,
|
/// Slots in Anzeigereihenfolge. Der Charakterbogen schleift darueber,
|
||||||
/// statt ein Layout zu mauern (Ticket 02).
|
/// statt ein Layout zu mauern (Ticket 02).
|
||||||
pub fn slots_ordered(&self) -> Vec<&Slot> {
|
pub fn slots_ordered(&self) -> Vec<&Slot> {
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ pub enum Command {
|
|||||||
ability: String,
|
ability: String,
|
||||||
target: super::creature::CreatureRef,
|
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:
|
/// 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 action::{Action, ActiveModifier, DamageType, StateField, Target};
|
||||||
pub use attributes::{Attribute, Modifier};
|
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 condition::{Check, Condition, Who};
|
||||||
pub use creature::{Creature, CreatureRef, ItemInstance};
|
pub use creature::{Creature, CreatureRef, ItemInstance};
|
||||||
pub use event::{Event, EventKind, Trigger};
|
pub use event::{Event, EventKind, Trigger};
|
||||||
@@ -599,6 +599,8 @@ impl Game {
|
|||||||
// Sein eigenes Gold zurueckzuverkaufen ergibt keinen Sinn.
|
// Sein eigenes Gold zurueckzuverkaufen ergibt keinen Sinn.
|
||||||
Command::Sell(i) => (self.player.inventory[*i].def == "core:gold")
|
Command::Sell(i) => (self.player.inventory[*i].def == "core:gold")
|
||||||
.then_some("Gold verkauft man nicht"),
|
.then_some("Gold verkauft man nicht"),
|
||||||
|
// Ein Cheat wird nie abgewiesen — das ist sein Zweck.
|
||||||
|
Command::Debug(_) => None,
|
||||||
Command::Wait => None,
|
Command::Wait => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,6 +748,7 @@ impl Game {
|
|||||||
Command::Choose(i) => self.choose(*i, reg),
|
Command::Choose(i) => self.choose(*i, reg),
|
||||||
Command::Buy(i) => self.buy(*i, reg),
|
Command::Buy(i) => self.buy(*i, reg),
|
||||||
Command::Sell(i) => self.sell(*i, reg),
|
Command::Sell(i) => self.sell(*i, reg),
|
||||||
|
Command::Debug(cheat) => self.cheat(cheat, reg),
|
||||||
Command::Drop(i) => {
|
Command::Drop(i) => {
|
||||||
let inst = self.player.inventory.remove(*i);
|
let inst = self.player.inventory.remove(*i);
|
||||||
let name = self.item_label(&inst, reg);
|
let name = self.item_label(&inst, reg);
|
||||||
@@ -837,6 +840,37 @@ impl Game {
|
|||||||
self.log(format!("{name} verkauft fuer {preis} Gold"));
|
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.
|
/// Nimmt **ein Stueck** aus dem Eintrag an `index` und gibt es zurueck.
|
||||||
/// Ueber den Index, nicht ueber die `def`: Zwei Traenke mit verschiedenen
|
/// Ueber den Index, nicht ueber die `def`: Zwei Traenke mit verschiedenen
|
||||||
/// Affixen teilen sich die `def`, und `take` naehme den erstbesten.
|
/// Affixen teilen sich die `def`, und `take` naehme den erstbesten.
|
||||||
@@ -1688,4 +1722,31 @@ mod tests {
|
|||||||
"der leere Eintrag muss verschwinden"
|
"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::CharacterSheet => draw_sheet(frame, area, app),
|
||||||
Screen::Dialog { pick } => draw_dialog(frame, area, app, *pick),
|
Screen::Dialog { pick } => draw_dialog(frame, area, app, *pick),
|
||||||
Screen::Shop { pick, selling } => draw_shop(frame, area, app, *pick, *selling),
|
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),
|
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
|
/// Menue-Kampf (Charter: Dungeon Master / Etrian Odyssey). Links die Gegner mit
|
||||||
/// Ziffern, rechts die Faehigkeiten mit Kosten und **Sperrgrund** — der kommt
|
/// Ziffern, rechts die Faehigkeiten mit Kosten und **Sperrgrund** — der kommt
|
||||||
/// aus dem Spielkern, nicht aus der UI (Ticket 14).
|
/// aus dem Spielkern, nicht aus der UI (Ticket 14).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use ratatui::crossterm::event::KeyCode;
|
use ratatui::crossterm::event::KeyCode;
|
||||||
|
|
||||||
use crate::data::Registries;
|
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
|
/// Popups sind gewoehnliche Screens, die sich beim Rendern als Overlay
|
||||||
/// ausweisen — ein gemeinsamer `Vec<Screen>` statt `current` plus
|
/// 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
|
/// Meldungszeile im Hauptmenue: Ein Abstiegs-Roguelike lebt davon, dass der
|
||||||
/// Lauf am Ende *erzaehlt* wird (Ticket 14).
|
/// Lauf am Ende *erzaehlt* wird (Ticket 14).
|
||||||
GameOver,
|
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
|
/// **Flache Optionsliste**, kein Knotengraph (M4): Was erst nach einer
|
||||||
/// Antwort erscheint, ist eine Option mit einer Bedingung.
|
/// Antwort erscheint, ist eine Option mit einer Bedingung.
|
||||||
Dialog {
|
Dialog {
|
||||||
@@ -96,6 +102,7 @@ impl Screen {
|
|||||||
KeyCode::Char('>') => Response::Command(Command::Descend),
|
KeyCode::Char('>') => Response::Command(Command::Descend),
|
||||||
KeyCode::Char('g') => Response::Command(Command::PickUp),
|
KeyCode::Char('g') => Response::Command(Command::PickUp),
|
||||||
KeyCode::Char('t') => Response::Command(Command::Talk),
|
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.
|
// Screen-Navigation ist reine UI und fasst `Game` nie an.
|
||||||
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
|
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
|
||||||
KeyCode::Char('i') => Response::Push(Screen::Inventory { pick: 0 }),
|
KeyCode::Char('i') => Response::Push(Screen::Inventory { pick: 0 }),
|
||||||
@@ -155,6 +162,7 @@ impl Screen {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('.') => Response::Command(Command::Wait),
|
KeyCode::Char('.') => Response::Command(Command::Wait),
|
||||||
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
|
KeyCode::Char('c') => Response::Push(Screen::CharacterSheet),
|
||||||
|
KeyCode::F(12) => Response::Push(Screen::Debug { pick: 0 }),
|
||||||
_ => Response::None,
|
_ => 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,
|
Screen::GameOver => Response::Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user