14 — Kampfformeln und Rundenablauf #15

Closed
opened 2026-08-11 14:07:19 +00:00 by RDietermann · 2 comments
Owner

Teil der Map #1 · Type: wayfinder:grilling · Blocked by: — · graduiert aus #2

Wie laeuft ein Kampf ab, und mit welchen Formeln? Reihenfolge, Trefferformel, Schadensformel, Krit, Aktionen pro Zug, Flucht, Zufall und Seed.

Volle Fragestellung (kanonisch): .scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md

Teil der Map #1 · **Type: `wayfinder:grilling`** · **Blocked by: —** · graduiert aus #2 Wie laeuft ein Kampf ab, und mit welchen Formeln? Reihenfolge, Trefferformel, Schadensformel, Krit, Aktionen pro Zug, Flucht, Zufall und Seed. Volle Fragestellung (kanonisch): `.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md`
RDietermann added the wayfinder:grilling label 2026-08-11 14:07:19 +00:00
Author
Owner

Nachtrag aus #3 (Typ-Registry): Dieses Ticket entscheidet zusaetzlich die Kampfsemantik von Faehigkeiten — was sie kosten (Ausdauer, eigener Pool, Ladungen), ob es Abklingzeiten gibt und wie sie gezaehlt werden, wann eine Faehigkeit gesperrt ist und woran der Spieler das sieht. #3 hat Faehigkeiten als Registry festgelegt und ihre Kampfsemantik hierher delegiert. Kanonischer Ticketkoerper ist entsprechend erweitert.

**Nachtrag aus #3 (Typ-Registry):** Dieses Ticket entscheidet zusaetzlich die Kampfsemantik von **Faehigkeiten** — was sie kosten (Ausdauer, eigener Pool, Ladungen), ob es Abklingzeiten gibt und wie sie gezaehlt werden, wann eine Faehigkeit gesperrt ist und woran der Spieler das sieht. #3 hat Faehigkeiten als Registry festgelegt und ihre Kampfsemantik hierher delegiert. Kanonischer Ticketkoerper ist entsprechend erweitert.
Author
Owner

Aufgeloest in d51ebb2. Kanonische Fassung: .scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md

Zahlen gehoeren nicht hierher — die Map haelt Balancing getrennt. Hier stehen Formeln und Ablauf.

Rundenablauf

Ein Zug = Spielerhandlung plus alle Gegnerreaktionen. Nicht jede Handlung ein eigener Zug: bei drei Gegnern tickte ein Gift-Debuff sonst viermal pro Runde, und ein Tastendruck erzeugte vier Zuege — das braeche die Zusage aus #12, die #13 gerade gemessen hat.

Reihenfolge ueber Initiative, einmal beim Betreten des Kampfes aus dexterity plus Wurf gewuerfelt und als i32 am Wesen gehalten. Ausdruecklich nicht als Reihenfolgeliste aus CreatureRef: die waere eine zweite Indexliste mit demselben Ungueltigkeitsproblem beim Tod eines Gegners. Gleichstand geht an den Spieler.

Game::execute(cmd, reg):
  1. turn_started (+ combat_started) in die Warteschlange
  2. Modifikatoren ticken, Ausdauer regeneriert, Abklingzeiten sinken
  3. Teilnehmer handeln in Initiativreihenfolge -> Abbruch, sobald der Held tot ist
  4. nach JEDER Handlung die Warteschlange leerarbeiten
  5. retain auf tote Gegner, danach ggf. combat_ended
  6. turn += 1

Schritt 4 nach jeder Handlung: sonst schlaegt ein Gegner zu, den ein Dornenpanzer eine Handlung zuvor getoetet hat. Schritt 5 erst nach dem Leeren: sonst zeigt ein creature_died-Trigger auf einen bereits entfernten Gegner. Innerhalb eines Zuges bleiben Indizes stabil — das ist die Antwort auf die Frage, die #8 hierher gereicht hatte.

Genau eine Handlung pro Zug, kein Ausdauer-Budget: max_stamina ist die Ressource fuer Faehigkeiten.

Formeln

p_treffer = clamp(hit_chance(A, gegen=B) - dexterity(B) * AUSWEICH, 0.05, 0.95)

roh    = attribute(damage, A, gegen=B)
krit   = rand() < clamp(critical_chance(A), 0.0, 1.0)
roh    = roh * critical_factor(A)   falls krit
r      = clamp(resistance(B, typ), -1.0, 0.8)
final  = max(1, round(roh * (1.0 - r)))

p_flucht = clamp(0.5 + (dexterity(Held) - dexterity(schnellster Gegner)) * FLUCHT, 0.1, 0.9)

Die Frage aus dem Ticketkoerper loest sich auf: ob critical_factor den Rohschaden oder den geminderten multipliziert, ist mathematisch belanglos — multiplikativ ist kommutativ. Festzulegen war nur, dass beide multiplikativ bleiben.

Drei Klemmbereiche mit Grund: Resistenz bei 0.8, weil 1.0 Immunitaet und damit ein Spielabbruch waere (nach unten -1.0 fuer Verwundbarkeit); max(1, …), damit kein Kampf rechnerisch ewig laeuft; 0.05/0.95, damit weder Verteidiger noch Angreifer unfehlbar werden. Keine Schadensstreuung — der Krit liefert die Varianz.

Zufall

Eigener xorshift64* (acht Zeilen statt einer Dependency), Seed als Feld in Game und damit im Speicherstand. Das ist eine Spielentscheidung, keine Implementierungsfrage: Nachladen wuerfelt nicht neu, Save-Scumming laeuft ins Leere.

Faehigkeiten (Bestellung aus #3)

cost in Ausdauer — kein zweiter Ressourcenpool. cooldown in Zuegen als cooldowns: HashMap<AbilityId, u32> am Creature. Gesperrt heisst: Ausdauer < cost, oder Cooldown > 0, oder condition unerfuellt — Ausloesen ergibt dann Outcome::Rejected aus #13 und kostet keinen Zug. Die Regel steht im Spielkern, nicht in der UI, sonst kennt ein Script sie nicht.

Ausdauer regeneriert pro Zug um einen festen Betrag — das gibt Wait aus #12 seinen Zweck.

Achte Registry: Wesen

Beim Aufschreiben der KI fiel eine Luecke auf, die seit #3 offen lag: Wesen stehen in keiner Registry. #3 hatte fuenf, #8 ergaenzte Affixe, #9 Quests — woher Gegnerdefinitionen kommen, hat nie jemand gesagt, obwohl #11 sie in Raeume setzen und #17 Beute an sie haengen muss.

[[creatures]]
id = "core:skeleton"
tags = ["core:undead"]
base = { max_health = 30, damage = 6, hit_chance = 0.7, dexterity = 3 }
abilities = ["core:attack", "core:bone_throw"]
ai = [
  { condition = [ { check = "attribute", who = "self", attribute = "health", below = 0.3 } ],
    ability = "core:flee" },
  { ability = "core:attack" },        # ohne condition = greift immer
]

Erster passender und verfuegbarer Eintrag gewinnt. Passt nichts, setzt der Gegner aus — einer, der die Runde blockiert, waere schlimmer. Fehlt der bedingungslose letzte Eintrag, gibt es eine Ladewarnung ueber die Maschinerie aus #5.

[balance] — ausdrueckliche Ausnahme zu #4

Fuenf Stellknoepfe (AUSWEICH, FLUCHT, Ausdauer-Regeneration, Resistenz-Deckel, Treffer-Klemmbereich) liegen als TOML-Tabelle und verschmelzen feldweise. Das widerspricht der Ganz-oder-gar-nicht-Regel aus #4, und zwar bewusst: ein Mod, der nur AUSWEICH dreht, muesste sonst alle fuenf abschreiben, und ein sechster Knopf braeche ihn stillschweigend.

Die Ausnahme bleibt eng, weil ihre Begruendung eng ist: [balance] hat ein festes, der Engine bekanntes Schema. Genau das fehlte bei Inhalt mit offenen Feldern — das war #4s Argument gegen feldweises Patchen, und es greift hier nicht. Standardwerte liegen in der Engine, nicht in core.

Vier Nachtraege an geschlossene Tickets

  • #2 (01): damage-Aktion wird menge = amount + scale * damage-Attribut, beide optional — eine Formel fuer Basisangriff (scale = 1.0), Feuerball (amount = 20) und Wuchtschlag (scale = 1.5). Dazu flee als achte Aktion
  • #8 (07): initiative: i32 und cooldowns am Creature, RNG-Seed in Game, und enemies ist der laufende Kampf, nicht die Etage
  • #9 (08): combat_started und combat_ended { outcome: Victory | Fled } — elf Events. Ohne die Unterscheidung zaehlte Weglaufen als Sieg
  • #12 (11): GameOver als elfter Screen. Bewusst nicht die lazy Variante: ein Abstiegs-Roguelike lebt davon, dass der Lauf am Ende erzaehlt wird — Etage, Zug, Gold, Todesursache

Auflagen

  • #11 (10 Prefabs): fuellt Game.enemies, Kampfbeginn ist ein Prefab-Ereignis
  • #16 (15 Progression): was bei Sieg passiert (EXP), plus alle Zahlen inklusive der fuenf [balance]-Knoepfe
  • #17 (16 Loot): Beute haengt an der neuen creatures-Registry
**Aufgeloest** in d51ebb2. Kanonische Fassung: `.scratch/rustkeep-1.0/issues/14-kampfformeln-und-rundenablauf.md` Zahlen gehoeren nicht hierher — die Map haelt Balancing getrennt. Hier stehen Formeln und Ablauf. ### Rundenablauf **Ein Zug = Spielerhandlung plus alle Gegnerreaktionen.** Nicht jede Handlung ein eigener Zug: bei drei Gegnern tickte ein Gift-Debuff sonst viermal pro Runde, und ein Tastendruck erzeugte vier Zuege — das braeche die Zusage aus #12, die #13 gerade gemessen hat. Reihenfolge ueber **Initiative**, einmal beim Betreten des Kampfes aus `dexterity` plus Wurf gewuerfelt und als `i32` **am Wesen** gehalten. Ausdruecklich *nicht* als Reihenfolgeliste aus `CreatureRef`: die waere eine zweite Indexliste mit demselben Ungueltigkeitsproblem beim Tod eines Gegners. Gleichstand geht an den Spieler. ``` Game::execute(cmd, reg): 1. turn_started (+ combat_started) in die Warteschlange 2. Modifikatoren ticken, Ausdauer regeneriert, Abklingzeiten sinken 3. Teilnehmer handeln in Initiativreihenfolge -> Abbruch, sobald der Held tot ist 4. nach JEDER Handlung die Warteschlange leerarbeiten 5. retain auf tote Gegner, danach ggf. combat_ended 6. turn += 1 ``` Schritt 4 nach jeder Handlung: sonst schlaegt ein Gegner zu, den ein Dornenpanzer eine Handlung zuvor getoetet hat. Schritt 5 erst nach dem Leeren: sonst zeigt ein `creature_died`-Trigger auf einen bereits entfernten Gegner. **Innerhalb eines Zuges bleiben Indizes stabil** — das ist die Antwort auf die Frage, die #8 hierher gereicht hatte. Genau eine Handlung pro Zug, kein Ausdauer-Budget: `max_stamina` ist die Ressource fuer Faehigkeiten. ### Formeln ``` p_treffer = clamp(hit_chance(A, gegen=B) - dexterity(B) * AUSWEICH, 0.05, 0.95) roh = attribute(damage, A, gegen=B) krit = rand() < clamp(critical_chance(A), 0.0, 1.0) roh = roh * critical_factor(A) falls krit r = clamp(resistance(B, typ), -1.0, 0.8) final = max(1, round(roh * (1.0 - r))) p_flucht = clamp(0.5 + (dexterity(Held) - dexterity(schnellster Gegner)) * FLUCHT, 0.1, 0.9) ``` **Die Frage aus dem Ticketkoerper loest sich auf**: ob `critical_factor` den Rohschaden oder den geminderten multipliziert, ist mathematisch belanglos — multiplikativ ist kommutativ. Festzulegen war nur, dass beide multiplikativ *bleiben*. Drei Klemmbereiche mit Grund: Resistenz bei `0.8`, weil `1.0` Immunitaet und damit ein Spielabbruch waere (nach unten `-1.0` fuer Verwundbarkeit); `max(1, …)`, damit kein Kampf rechnerisch ewig laeuft; `0.05/0.95`, damit weder Verteidiger noch Angreifer unfehlbar werden. Keine Schadensstreuung — der Krit liefert die Varianz. ### Zufall Eigener xorshift64* (acht Zeilen statt einer Dependency), Seed als Feld in `Game` und damit **im Speicherstand**. Das ist eine Spielentscheidung, keine Implementierungsfrage: **Nachladen wuerfelt nicht neu**, Save-Scumming laeuft ins Leere. ### Faehigkeiten (Bestellung aus #3) `cost` in Ausdauer — **kein zweiter Ressourcenpool**. `cooldown` in Zuegen als `cooldowns: HashMap<AbilityId, u32>` am Creature. Gesperrt heisst: Ausdauer < cost, oder Cooldown > 0, oder `condition` unerfuellt — Ausloesen ergibt dann `Outcome::Rejected` aus #13 und **kostet keinen Zug**. Die Regel steht im Spielkern, nicht in der UI, sonst kennt ein Script sie nicht. Ausdauer regeneriert pro Zug um einen festen Betrag — das gibt `Wait` aus #12 seinen Zweck. ### Achte Registry: Wesen Beim Aufschreiben der KI fiel eine Luecke auf, die **seit #3 offen lag**: Wesen stehen in keiner Registry. #3 hatte fuenf, #8 ergaenzte Affixe, #9 Quests — woher Gegnerdefinitionen kommen, hat nie jemand gesagt, obwohl #11 sie in Raeume setzen und #17 Beute an sie haengen muss. ```toml [[creatures]] id = "core:skeleton" tags = ["core:undead"] base = { max_health = 30, damage = 6, hit_chance = 0.7, dexterity = 3 } abilities = ["core:attack", "core:bone_throw"] ai = [ { condition = [ { check = "attribute", who = "self", attribute = "health", below = 0.3 } ], ability = "core:flee" }, { ability = "core:attack" }, # ohne condition = greift immer ] ``` Erster passender **und verfuegbarer** Eintrag gewinnt. Passt nichts, setzt der Gegner aus — einer, der die Runde blockiert, waere schlimmer. Fehlt der bedingungslose letzte Eintrag, gibt es eine Ladewarnung ueber die Maschinerie aus #5. ### `[balance]` — ausdrueckliche Ausnahme zu #4 Fuenf Stellknoepfe (`AUSWEICH`, `FLUCHT`, Ausdauer-Regeneration, Resistenz-Deckel, Treffer-Klemmbereich) liegen als TOML-Tabelle und verschmelzen **feldweise**. Das widerspricht der Ganz-oder-gar-nicht-Regel aus #4, und zwar bewusst: ein Mod, der nur `AUSWEICH` dreht, muesste sonst alle fuenf abschreiben, und ein sechster Knopf braeche ihn stillschweigend. Die Ausnahme bleibt eng, weil ihre Begruendung eng ist: `[balance]` hat ein **festes, der Engine bekanntes Schema**. Genau das fehlte bei Inhalt mit offenen Feldern — das war #4s Argument gegen feldweises Patchen, und es greift hier nicht. Standardwerte liegen in der Engine, nicht in `core`. ### Vier Nachtraege an geschlossene Tickets - **#2 (01)**: `damage`-Aktion wird `menge = amount + scale * damage-Attribut`, beide optional — eine Formel fuer Basisangriff (`scale = 1.0`), Feuerball (`amount = 20`) und Wuchtschlag (`scale = 1.5`). Dazu `flee` als **achte Aktion** - **#8 (07)**: `initiative: i32` und `cooldowns` am Creature, RNG-Seed in `Game`, und **`enemies` ist der laufende Kampf**, nicht die Etage - **#9 (08)**: `combat_started` und `combat_ended { outcome: Victory | Fled }` — **elf Events**. Ohne die Unterscheidung zaehlte Weglaufen als Sieg - **#12 (11)**: **`GameOver`** als elfter Screen. Bewusst nicht die lazy Variante: ein Abstiegs-Roguelike lebt davon, dass der Lauf am Ende erzaehlt wird — Etage, Zug, Gold, Todesursache ### Auflagen - **#11** (10 Prefabs): fuellt `Game.enemies`, Kampfbeginn ist ein Prefab-Ereignis - **#16** (15 Progression): was bei Sieg passiert (EXP), plus alle Zahlen inklusive der fuenf `[balance]`-Knoepfe - **#17** (16 Loot): Beute haengt an der neuen `creatures`-Registry
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/rustkeep#15