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:
2026-08-12 20:58:35 +02:00
parent 036f60d463
commit 26d9533a93
6 changed files with 175 additions and 2 deletions

View File

@@ -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"))), &reg);
game.execute(cheat(Cheat::Give(String::from("core:iron_sword"))), &reg);
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), &reg);
let voll = game.attribute(CreatureRef::Player, Attribute::MaxHealth, None, &reg);
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), &reg);
assert!(game.enemies.is_empty());
assert!(game.journal.iter().any(|z| z.contains("geräumt")), "{:?}", game.journal);
}
}