From a8fea8b52620f701d5e143b802eb6ff852188dc8 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Thu, 13 Aug 2026 08:24:06 +0200 Subject: [PATCH] fix(balance): Startausruestung wird angelegt, Krit wirkt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemessen statt geschaetzt — es waren nicht die Zahlen: - on_create konnte nur give_item, die Startwaffe lag also im Beutel. Jeder Held schlug mit blossen Haenden zu: Schaden 2 statt 7,7, zehn Zuege pro Ratte statt zweieinhalb. Neue Aktion equip_item. - Der Dieb bekam gar keine Waffe. Jetzt ein Dolch (+3 Schaden, +5 Krit). - critical_chance percent = 15 auf einer Basis von 0 ist ein No-op — die Signaturzahl des Diebs tat nichts. Basis jetzt 5, Dieb traegt flat 10. Die Prozent-auf-Null-Falle steht im Glossar unter Modifikator: Sie folgt aus der Stapelregel aus Ticket 01 und schnappt sonst wieder zu. --- .scratch/rustkeep-1.0/map.md | 16 ++++++++++ CONTEXT.md | 10 ++++-- mods/core/character_options.toml | 13 +++++--- mods/core/items.toml | 10 ++++++ src/game/action.rs | 11 ++++++- src/game/attributes.rs | 5 +++ src/game/mod.rs | 52 +++++++++++++++++++++++++++++++- 7 files changed, 108 insertions(+), 9 deletions(-) diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index 31b548c..77a0875 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -219,6 +219,22 @@ nur flache Modifikatoren geben, also genau die Hälfte, die laut demselben Ticke Speicherstand ohne Rassen-Mod hat nach der Verwerf-Regel ebenfalls keine Optionen und würde neu gebaut statt fortgesetzt). +**Erste Balancing-Runde** (nach M8, 73 Tests) — ausgelöst durch „der erste Gegner ist sehr schwer, +Elf Dieb". Gemessen statt geschätzt, und es waren nicht die Zahlen: + +| Ursache | Wirkung | +|---|---| +| `on_create` konnte nur `give_item` | Die Startwaffe lag im **Beutel**. Jeder Held schlug mit bloßen Händen zu: Schaden 2 statt 7,7 — zehn Züge pro Ratte statt zweieinhalb | +| Der Dieb bekam gar keine Waffe | Nur Kappe und Gold, und die Kappe war ebenfalls nicht angelegt | +| `critical_chance percent = 15` auf Basis 0 | **No-op.** Die Signaturzahl der Klasse tat nichts, und keine Zeile Code war daran falsch | + +Behoben mit `equip_item` als **zehnter Aktion** (kein Engine-Begriff „Startausrüstung" — eine Aktion +wie jede andere, überall verwendbar), einem Dolch für den Dieb, und einer Basis von 5 auf +`critical_chance`. Die Prozent-auf-Null-Falle steht jetzt im Glossar unter *Modifikator*: Sie folgt +direkt aus der Stapelregel aus Ticket 01 und wird wieder zuschnappen, wenn sie nirgends steht. + +Startkampf danach: Krieger 2,5 Züge je Ratte, Dieb 3,9 bei 20 % Krit und 108 Leben. + **Drei Befunde aus M1, die nur der Compiler liefern konnte** — alle in die Tickets zurückgetragen: - **11**: Der Grenztest in seiner wörtlichen Fassung schlägt beim eigenen Doc-Kommentar an. Muss Kommentarzeilen ausnehmen. diff --git a/CONTEXT.md b/CONTEXT.md index 6f95c26..965c090 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -51,6 +51,11 @@ Ein **benannter** Modifikator (`core:poisoned`) ist ein Bündel: mehrere Umrechn von der [Aktion](#aktion) `apply_modifier` aufgelegt. Er **wohnt beim Träger** — stirbt der, verschwindet er mit ihm. +Die Stapelregel ist `(Basis + Σ flach) × (1 + Σ prozentual)`, und daraus folgt eine Falle, die schon +einmal zugeschnappt hat: **`percent` auf einer Basis von 0 bleibt 0.** „+15 % kritische Chance" an einer +Klasse tat nichts, weil `critical_chance` beim Helden bei 0 anfing. Wer ein Attribut prozentual +verändern will, braucht dort eine Basis ungleich null — sonst gehört der Wert als `flat` hin. + ## Aktion Eine einmalige Zustandsänderung. Sie feuert, wirkt, ist vorbei. Quellen sind Angriffe, @@ -61,8 +66,9 @@ Beispiele: „12 Schaden zufügen", „20 Leben heilen", „50 Gold geben", „G Eine Aktion kann einen befristeten [Modifikator](#modifikator) erzeugen — so hängen die beiden Begriffe zusammen, ohne dasselbe zu sein. -**Neun** sind gebaut: `damage`, `change_attribute`, `apply_modifier`, `remove_modifier`, `flee`, -`open_shop`, `give_exp`, `start_quest`, `give_item`. Offen bleiben `remove_item` und `run_script`. +**Zehn** sind gebaut: `damage`, `change_attribute`, `apply_modifier`, `remove_modifier`, `flee`, +`open_shop`, `give_exp`, `start_quest`, `give_item`, `equip_item`. Offen bleiben `remove_item` und +`run_script`. Der Katalog trägt **keine Ordnungszahlen** mehr. Vier Tickets haben ihn erweitert und dabei jeweils vom eigenen Stand aus gezählt — „`start_quest` als siebte", „`flee` als achte", „`open_shop` als elfte" —, diff --git a/mods/core/character_options.toml b/mods/core/character_options.toml index 40c0328..91b04b1 100644 --- a/mods/core/character_options.toml +++ b/mods/core/character_options.toml @@ -42,8 +42,8 @@ abilities = ["core:power_strike"] # `on_create` ist KEINE neue Mechanik, sondern gewoehnliche Aktionen, einmal # beim Bau ausgefuehrt (Ticket 02). Die Engine kennt den Begriff # "Startausruestung" deshalb gar nicht. -on_create = [ { action = "give_item", item = "core:iron_sword" }, - { action = "give_item", item = "core:health_potion", count = 2 } ] +on_create = [ { action = "equip_item", item = "core:iron_sword" }, + { action = "give_item", item = "core:health_potion", count = 2 } ] [[options]] id = "thief" @@ -52,7 +52,10 @@ name = "Dieb" description = "Nimmt, was nicht niet- und nagelfest ist." modifiers = [ { attribute = "dexterity", flat = 2 }, { attribute = "luck", flat = 2 }, - { attribute = "critical_chance", percent = 15 } ] + # `flat`, nicht `percent`: Die Basis war 0, und Prozent auf 0 + # bleibt 0 — die Signaturzahl des Diebs tat nichts (Befund M9). + { attribute = "critical_chance", flat = 10 } ] abilities = ["core:backstab"] -on_create = [ { action = "give_item", item = "core:leather_cap" }, - { action = "give_item", item = "core:gold", count = 40 } ] +on_create = [ { action = "equip_item", item = "core:dagger" }, + { action = "equip_item", item = "core:leather_cap" }, + { action = "give_item", item = "core:gold", count = 40 } ] diff --git a/mods/core/items.toml b/mods/core/items.toml index ad6f074..bf690dd 100644 --- a/mods/core/items.toml +++ b/mods/core/items.toml @@ -11,6 +11,16 @@ symbol = "/" color = "white" modifiers = [ { attribute = "damage", flat = 5 } ] +[[items]] +id = "dagger" +name = "Dolch" +base_price = 28 +slot = "core:hand" +symbol = "/" +color = "white" +modifiers = [ { attribute = "damage", flat = 3 }, + { attribute = "critical_chance", flat = 5 } ] + [[items]] id = "leather_cap" name = "Lederkappe" diff --git a/src/game/action.rs b/src/game/action.rs index feac42d..96cccca 100644 --- a/src/game/action.rs +++ b/src/game/action.rs @@ -137,6 +137,14 @@ pub enum Action { #[serde(default = "one")] count: u32, }, + /// Gibt **und legt an**. Aus M9: `on_create` konnte bis dahin nur `give_item`, + /// also startete jeder Held mit der Waffe im Beutel und schlug mit blossen + /// Haenden zu. „Startausruestung" als eigener Engine-Begriff waere die + /// falsche Antwort gewesen — das hier ist eine Aktion wie jede andere und + /// steht ueberall, wo eine stehen darf. + EquipItem { + item: String, + }, } fn one() -> u32 { @@ -163,7 +171,8 @@ impl Action { | Action::OpenShop | Action::GiveExp { .. } | Action::StartQuest { .. } - | Action::GiveItem { .. } => Target::Zelf, + | Action::GiveItem { .. } + | Action::EquipItem { .. } => Target::Zelf, } } } diff --git a/src/game/attributes.rs b/src/game/attributes.rs index d5368bc..e93399b 100644 --- a/src/game/attributes.rs +++ b/src/game/attributes.rs @@ -116,6 +116,11 @@ impl Default for Attributes { a.set(Attribute::MaxStamina, 100.0); a.set(Attribute::Damage, 2.0); a.set(Attribute::HitChance, 0.8); + // **Befund aus M9:** Die Basis stand auf 0, und ein Modifikator mit + // `percent` rechnet auf 0 immer 0 — die Signaturzahl des Diebs + // („+15 % kritische Chance") tat schlicht nichts. Gegner tragen seit M3 + // eine Basis von 5; der Held hatte keine. + a.set(Attribute::CriticalChance, 5.0); a.set(Attribute::CriticalFactor, 1.5); a.set(Attribute::CarryCapacity, 50.0); a diff --git a/src/game/mod.rs b/src/game/mod.rs index 1646602..d99fd53 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -430,6 +430,15 @@ impl Game { Action::StartQuest { quest } => self.start_quest(quest, reg), + Action::EquipItem { item } => { + self.give(ItemInstance::new(item.clone()), reg); + // Ueber den normalen Weg, damit Verdraengtes zurueck ins + // Inventar wandert statt zu verschwinden. + if let Some(i) = self.player.inventory.iter().rposition(|x| x.def == *item) { + self.equip(i, reg); + } + } + Action::GiveItem { item, count } => { let inst = ItemInstance::stack(item.clone(), *count); let name = self.item_label(&inst, reg); @@ -2205,7 +2214,18 @@ mod tests { ); // `on_create` hat die Ausruestung gegeben — als gewoehnliche Aktionen. - assert_eq!(game.count_of("core:iron_sword"), 1); + // **Befund aus M9:** `equip_item`, nicht `give_item`. Vorher lag das + // Schwert im Beutel und der Held schlug mit blossen Haenden zu. + assert_eq!( + game.player.equipment.get("core:hand").map(|i| i.def.as_str()), + Some("core:iron_sword"), + "die Waffe muss getragen werden, nicht getragen *werden koennen*" + ); + assert!( + game.attribute(p, Attribute::Damage, None, ®) > 7.0, + "mit Waffe, nicht ohne: {}", + game.attribute(p, Attribute::Damage, None, ®) + ); assert_eq!(game.count_of("core:health_potion"), 2, "der Stapel kommt als Stapel"); // Die Faehigkeiten von Rasse *und* Klasse stehen im Kampfmenue. @@ -2214,4 +2234,34 @@ mod tests { assert!(koennen.iter().any(|a| a == "core:power_strike"), "Klasse: {koennen:?}"); assert!(koennen.iter().any(|a| a == "core:attack"), "und die impliziten bleiben"); } + + + /// **Befund aus M9:** Ein Modifikator mit `percent` rechnet auf einer Basis + /// von 0 immer 0. `critical_chance` stand auf 0, also tat „+15 % kritische + /// Chance" am Dieb schlicht nichts — die Signaturzahl der Klasse war ein + /// No-op, und keine Zeile Code war daran falsch. + #[test] + fn prozent_auf_null_bleibt_null() { + let (reg, _) = load_mods().expect("core"); + let mut game = Game::new(Creature::new("M"), ®, Rng::new(7)).expect("Etage 1"); + let p = CreatureRef::Player; + + assert!( + game.attribute(p, Attribute::CriticalChance, None, ®) > 0.0, + "ohne Basis wirkt kein prozentualer Modifikator darauf" + ); + + game.execute( + Command::CreateCharacter(vec![ + String::from("core:elf"), + String::from("core:thief"), + ]), + ®, + ); + assert!( + game.attribute(p, Attribute::CriticalChance, None, ®) >= 15.0, + "der Dieb muss seine Signaturzahl auch spueren: {}", + game.attribute(p, Attribute::CriticalChance, None, ®) + ); + } }