diff --git a/.scratch/rustkeep-1.0/issues/07-game-state-baum.md b/.scratch/rustkeep-1.0/issues/07-game-state-baum.md index 1138ff1..c50f31a 100644 --- a/.scratch/rustkeep-1.0/issues/07-game-state-baum.md +++ b/.scratch/rustkeep-1.0/issues/07-game-state-baum.md @@ -217,3 +217,24 @@ bleiben Indizes stabil. `Stats` zerfällt in `Attributes` (Katalog aus 01) und Zustandsfelder — heute stehen `max_health` und `current_health` nebeneinander in derselben Struktur. `Player` wird zu `Creature`. Neben `Item` als Definition entsteht `ItemInstance` als Laufzeitform. + +### Befund aus dem Spielen — der Stapel hatte keine Verbraucher + +Ticket 07 beschreibt die Stapelregel, M4 hat sie gebaut: `ItemInstance.count`, `give` verschmilzt, +`take` zieht ab. Gebaut wurde damit die **Erzeugungsseite** — Beute, Kauf, Verkauf. Die drei älteren +Verbraucher blieben, wie sie waren, und die nahmen alle denselben Weg: `inventory.remove(index)`. + +Im Spiel sichtbar an zwei Heiltränken, von denen ein Schluck beide verbrauchte. Derselbe Fehler steckte +in `equip` (legt den ganzen Stapel an) und dieselbe Lücke in `unequip` (schob am Verschmelzen vorbei +zurück ins Inventar). + +Entschieden: **ein** Griff für alle Verbraucher — `take_at(index)` nimmt ein Stück aus dem Eintrag und +räumt ihn ab, wenn er leer ist. Über den Index und nicht über die `def`, weil zwei Tränke mit +verschiedenen Affixen sich die `def` teilen. `unequip` läuft jetzt durch `give`. + +Die Lehre ist nicht „Stapel sind schwierig", sondern: Ein Feld, das die Bedeutung eines Eintrags +ändert, macht **jeden** bestehenden Leser zum Kandidaten. `count` einzuführen war ein Vierzeiler; die +Verbraucher nachzuziehen war die eigentliche Arbeit, und sie wurde beim ersten Mal vergessen. + +Nebenbei aufgefallen: Der Heiltrank hatte nie `actions` — er war seit M1 verbrauchbar, ohne zu wirken. +Kein Test hat es gemerkt, weil kein Test ihn getrunken hat. diff --git a/mods/core/items.toml b/mods/core/items.toml index 65998ba..ad6f074 100644 --- a/mods/core/items.toml +++ b/mods/core/items.toml @@ -28,6 +28,7 @@ stackable = true consumed_on_use = true symbol = "!" color = "red" +actions = [ { action = "change_attribute", attribute = "health", amount = 25, target = "self" } ] [[items]] id = "gold" diff --git a/src/game/mod.rs b/src/game/mod.rs index 3c94a53..7a486e4 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -741,7 +741,7 @@ impl Game { Command::PickUp => self.pick_up(reg), Command::UseItem(i) => self.use_item(*i, reg), Command::Equip(i) => self.equip(*i, reg), - Command::Unequip(slot) => self.unequip(&slot.clone()), + Command::Unequip(slot) => self.unequip(&slot.clone(), reg), Command::Talk => self.talk(reg), Command::Choose(i) => self.choose(*i, reg), Command::Buy(i) => self.buy(*i, reg), @@ -828,25 +828,33 @@ impl Game { fn sell(&mut self, index: usize, reg: &Registries) { // Ein Stapel wird stueckweise verkauft — sonst gaebe es keinen Weg, // fuenf von fuenfzig Traenken loszuwerden. - let inst = ItemInstance { count: 1, ..self.player.inventory[index].clone() }; + let inst = self.take_at(index); let preis = self.sell_price(&inst, reg); let name = self.item_label(&inst, reg); - - // Ueber den Index, nicht ueber `take`: Zwei Traenke mit verschiedenen - // Affixen teilen sich die `def`, und `take` naehme den erstbesten. - let slot = &mut self.player.inventory[index]; - slot.count -= 1; - if slot.count == 0 { - self.player.inventory.remove(index); - } self.give(ItemInstance::stack("core:gold", preis), reg); // Der Haendler hat kein Gold-Konto: Die Spanne, nicht ein Vorrat, // begrenzt den Handel. self.log(format!("{name} verkauft fuer {preis} Gold")); } + /// 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. + fn take_at(&mut self, index: usize) -> ItemInstance { + let slot = &mut self.player.inventory[index]; + slot.count -= 1; + let leer = slot.count == 0; + let einzeln = ItemInstance { count: 1, ..slot.clone() }; + if leer { + self.player.inventory.remove(index); + } + einzeln + } + fn use_item(&mut self, index: usize, reg: &Registries) { - let inst = self.player.inventory[index].clone(); + // Ein Stapel wird stueckweise benutzt — `count` wegzuwerfen hiesse, + // mit einem Trank alle zu trinken. + let inst = ItemInstance { count: 1, ..self.player.inventory[index].clone() }; let Some(def) = reg.items.get(&inst.def) else { return }; let name = self.item_label(&inst, reg); @@ -860,29 +868,30 @@ impl Game { } if def.consumed_on_use { - self.player.inventory.remove(index); + self.take_at(index); } self.log(format!("{name} benutzt")); self.events.push_back(Event::new(EventKind::ItemUsed, CreatureRef::Player)); } fn equip(&mut self, index: usize, reg: &Registries) { - let inst = self.player.inventory[index].clone(); - let Some(slot) = reg.items.get(&inst.def).and_then(|d| d.slot.clone()) else { return }; + let def = self.player.inventory[index].def.clone(); + let Some(slot) = reg.items.get(&def).and_then(|d| d.slot.clone()) else { return }; + let inst = self.take_at(index); // Was im Slot lag, wandert zurueck ins Inventar — nicht ins Nichts. if let Some(alt) = self.player.equipment.remove(&slot) { - self.player.inventory.push(alt); + self.give(alt, reg); } - self.player.inventory.remove(index); let name = self.item_label(&inst, reg); self.player.equipment.insert(slot, inst); self.log(format!("{name} angelegt")); } - fn unequip(&mut self, slot: &str) { + fn unequip(&mut self, slot: &str, reg: &Registries) { if let Some(inst) = self.player.equipment.remove(slot) { - self.player.inventory.push(inst); + // Ueber `give`, damit ein abgelegter Stapel wieder verschmilzt. + self.give(inst, reg); self.log(String::from("Abgelegt")); } } @@ -1655,4 +1664,28 @@ mod tests { assert_eq!(game.player.health, vorher, "ein gestoerter Gegner richtet nichts an"); assert!(game.enemies[0].intent.is_some(), "und er sagt fuer den naechsten Zug neu an"); } + + /// Gefunden im Spiel: Zwei Heiltraenke lagen als **ein** Stapel im + /// Inventar, und `use_item` entfernte den Eintrag — ein Schluck, beide weg. + /// Der Stapel war in M4 dazugekommen, seine Verbraucher nicht mitgezogen. + #[test] + fn ein_stapel_wird_stueckweise_verbraucht() { + let (mut game, reg) = zwergenkrieger(); + game.give(ItemInstance::stack("core:health_potion", 2), ®); + let platz = game.player.inventory.len() - 1; + game.player.health = 10.0; + + assert!(matches!(game.execute(Command::UseItem(platz), ®), Outcome::TurnPassed)); + assert_eq!(game.count_of("core:health_potion"), 1, "nur einer darf weg sein"); + assert!(game.player.health > 10.0, "und er muss auch wirken"); + + // Der zweite raeumt den Eintrag ab, statt einen Stapel der Groesse 0 + // stehen zu lassen. + assert!(matches!(game.execute(Command::UseItem(platz), ®), Outcome::TurnPassed)); + assert_eq!(game.count_of("core:health_potion"), 0); + assert!( + !game.player.inventory.iter().any(|i| i.def == "core:health_potion"), + "der leere Eintrag muss verschwinden" + ); + } }