fix(inventar): ein Stapel wird stueckweise verbraucht
use_item und equip entfernten den ganzen Eintrag — ein Schluck aus zwei Heiltraenken verbrauchte beide. `count` kam in M4 dazu, die drei aelteren Verbraucher wurden nicht mitgezogen. Ein Griff fuer alle: take_at(index) nimmt ein Stueck und raeumt den Eintrag ab, wenn er leer ist. unequip laeuft jetzt durch give, damit ein abgelegter Stapel wieder verschmilzt. Dabei aufgefallen: Der Heiltrank hatte nie `actions` — seit M1 verbrauchbar, ohne zu wirken.
This commit is contained in:
@@ -217,3 +217,24 @@ bleiben Indizes stabil.
|
|||||||
`Stats` zerfällt in `Attributes` (Katalog aus 01) und Zustandsfelder — heute stehen `max_health` und
|
`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
|
`current_health` nebeneinander in derselben Struktur. `Player` wird zu `Creature`. Neben `Item` als
|
||||||
Definition entsteht `ItemInstance` als Laufzeitform.
|
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.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ stackable = true
|
|||||||
consumed_on_use = true
|
consumed_on_use = true
|
||||||
symbol = "!"
|
symbol = "!"
|
||||||
color = "red"
|
color = "red"
|
||||||
|
actions = [ { action = "change_attribute", attribute = "health", amount = 25, target = "self" } ]
|
||||||
|
|
||||||
[[items]]
|
[[items]]
|
||||||
id = "gold"
|
id = "gold"
|
||||||
|
|||||||
@@ -741,7 +741,7 @@ impl Game {
|
|||||||
Command::PickUp => self.pick_up(reg),
|
Command::PickUp => self.pick_up(reg),
|
||||||
Command::UseItem(i) => self.use_item(*i, reg),
|
Command::UseItem(i) => self.use_item(*i, reg),
|
||||||
Command::Equip(i) => self.equip(*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::Talk => self.talk(reg),
|
||||||
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),
|
||||||
@@ -828,25 +828,33 @@ impl Game {
|
|||||||
fn sell(&mut self, index: usize, reg: &Registries) {
|
fn sell(&mut self, index: usize, reg: &Registries) {
|
||||||
// Ein Stapel wird stueckweise verkauft — sonst gaebe es keinen Weg,
|
// Ein Stapel wird stueckweise verkauft — sonst gaebe es keinen Weg,
|
||||||
// fuenf von fuenfzig Traenken loszuwerden.
|
// 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 preis = self.sell_price(&inst, reg);
|
||||||
let name = self.item_label(&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);
|
self.give(ItemInstance::stack("core:gold", preis), reg);
|
||||||
// Der Haendler hat kein Gold-Konto: Die Spanne, nicht ein Vorrat,
|
// Der Haendler hat kein Gold-Konto: Die Spanne, nicht ein Vorrat,
|
||||||
// begrenzt den Handel.
|
// begrenzt den Handel.
|
||||||
self.log(format!("{name} verkauft fuer {preis} Gold"));
|
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) {
|
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 Some(def) = reg.items.get(&inst.def) else { return };
|
||||||
|
|
||||||
let name = self.item_label(&inst, reg);
|
let name = self.item_label(&inst, reg);
|
||||||
@@ -860,29 +868,30 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if def.consumed_on_use {
|
if def.consumed_on_use {
|
||||||
self.player.inventory.remove(index);
|
self.take_at(index);
|
||||||
}
|
}
|
||||||
self.log(format!("{name} benutzt"));
|
self.log(format!("{name} benutzt"));
|
||||||
self.events.push_back(Event::new(EventKind::ItemUsed, CreatureRef::Player));
|
self.events.push_back(Event::new(EventKind::ItemUsed, CreatureRef::Player));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn equip(&mut self, index: usize, reg: &Registries) {
|
fn equip(&mut self, index: usize, reg: &Registries) {
|
||||||
let inst = self.player.inventory[index].clone();
|
let def = self.player.inventory[index].def.clone();
|
||||||
let Some(slot) = reg.items.get(&inst.def).and_then(|d| d.slot.clone()) else { return };
|
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.
|
// Was im Slot lag, wandert zurueck ins Inventar — nicht ins Nichts.
|
||||||
if let Some(alt) = self.player.equipment.remove(&slot) {
|
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);
|
let name = self.item_label(&inst, reg);
|
||||||
self.player.equipment.insert(slot, inst);
|
self.player.equipment.insert(slot, inst);
|
||||||
self.log(format!("{name} angelegt"));
|
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) {
|
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"));
|
self.log(String::from("Abgelegt"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1655,4 +1664,28 @@ mod tests {
|
|||||||
assert_eq!(game.player.health, vorher, "ein gestoerter Gegner richtet nichts an");
|
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");
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user