feat: m3 kampf, events und trigger
Der Dungeon hat jetzt Gegner, und man kann sterben.
- game/action.rs: sieben Schadenstypen, fuenf der zehn Aktionen, Target
- game/condition.rs: Bedingungsgrammatik (Liste = UND, not je Eintrag)
- game/event.rs: zwoelf Events, Trigger als vierte Wirkungsform
- game/combat.rs: Treffer- und Schadensformel mit drei Klemmbereichen
- Game::apply, drain_events (FIFO), Initiative, Rundenablauf aus Ticket 14
- data/: abilities-, creatures- und modifiers-Registry, [balance] feldweise
- ui/: Combat-Screen mit Faehigkeitsmenue und Sperrgrund
Drei Befunde:
- Die Trefferformel war toter Code — jeder Angriff traf. Nur der Compiler
hat es gemeldet ('hit_chance is never used'). Test haelt sie am Leben.
- Die Warteschlange war ein Stack (pop statt pop_front), obwohl Ticket 08
ausdruecklich 'haengt es hinten an' sagt. Jetzt VecDeque.
- Benannte Modifikatoren (core:poisoned) hatten keine Registry — Ticket 01
laesst apply_modifier per ID referenzieren, aber keine der zwoelf
definierte sie. Das ist die dreizehnte.
43 Tests.
This commit is contained in:
@@ -14,6 +14,9 @@ use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::game::action::Action;
|
||||
use crate::game::condition::Condition;
|
||||
use crate::game::event::Trigger;
|
||||
use crate::game::Modifier;
|
||||
|
||||
/// Ein Ding im Spiel, **einheitlich strukturiert**. Es gibt keinen Item-Typ:
|
||||
@@ -92,6 +95,132 @@ pub struct CharacterOption {
|
||||
// Startausruestung brauchen Kampf bzw. Inventar.
|
||||
}
|
||||
|
||||
/// Ein benannter, befristeter Modifikator — „Vergiftet", „Gesegnet".
|
||||
///
|
||||
/// **Befund aus M3:** Ticket 01 laesst `apply_modifier` einen Modifikator per ID
|
||||
/// referenzieren (`core:poisoned`), aber keine der zwoelf Registries definiert
|
||||
/// benannte Modifikatoren. Das hier ist die **dreizehnte**.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ModifierDef {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default = "default_symbol")]
|
||||
pub symbol: char,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
/// Zuege bis zum Verfall. Eine Uhr: der Zug (Ticket 01) — Debuffs ticken
|
||||
/// auch beim Erkunden, damit „ich warte den Debuff draussen ab" eine
|
||||
/// Entscheidung mit Kosten bleibt.
|
||||
#[serde(default = "three")]
|
||||
pub turns: u32,
|
||||
/// Die Umrechnungen, die er traegt. Bewusst `attributes` und nicht
|
||||
/// `modifiers`, damit ein Modifikator nicht Modifikatoren enthaelt.
|
||||
#[serde(default)]
|
||||
pub attributes: Vec<Modifier>,
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<Trigger>,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
}
|
||||
|
||||
fn three() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
/// Ein benanntes, wiederholt ausloesbares **Angebot** an den Spieler
|
||||
/// (Ticket 02) mit seiner Kampfsemantik aus Ticket 14.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Ability {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Ausdauer — kein zweiter Ressourcenpool (Ticket 14).
|
||||
#[serde(default)]
|
||||
pub cost: f32,
|
||||
/// Abklingzeit in Zuegen.
|
||||
#[serde(default)]
|
||||
pub cooldown: u32,
|
||||
#[serde(default)]
|
||||
pub condition: Vec<Condition>,
|
||||
#[serde(default)]
|
||||
pub actions: Vec<Action>,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
}
|
||||
|
||||
/// Ein Eintrag der `ai`-Liste: erster passender **und verfuegbarer** gewinnt
|
||||
/// (Ticket 14). Ein Eintrag ohne `condition` greift immer.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AiEntry {
|
||||
#[serde(default)]
|
||||
pub condition: Vec<Condition>,
|
||||
pub ability: String,
|
||||
}
|
||||
|
||||
/// Gegnerdefinition. Die Luecke lag seit Ticket 02 offen — `Creature` war der
|
||||
/// Laufzeittyp, woher seine Werte kommen, hat nie jemand gesagt (Ticket 14).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CreatureDef {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default = "default_symbol")]
|
||||
pub symbol: char,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// Grundwerte. Was fehlt, bleibt beim Standard.
|
||||
#[serde(default)]
|
||||
pub base: HashMap<String, f32>,
|
||||
#[serde(default)]
|
||||
pub abilities: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub ai: Vec<AiEntry>,
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<Trigger>,
|
||||
/// Kurzform fuer einen Todes-Trigger, den sonst jede Gegnerdefinition
|
||||
/// mitschleppen muesste (Ticket 15).
|
||||
#[serde(default)]
|
||||
pub exp_reward: u32,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
}
|
||||
|
||||
/// Die Stellknoepfe aus Ticket 14 und 16. **Keine Registry**: ein einzelner
|
||||
/// Block mit festem Schema, der **feldweise** verschmilzt statt als ganzer
|
||||
/// Eintrag ersetzt zu werden — sonst muesste ein Mod, der nur `ausweich` dreht,
|
||||
/// alle Werte abschreiben, und ein neuer Knopf braeche ihn stillschweigend.
|
||||
///
|
||||
/// Die Standardwerte liegen hier in der Engine, nicht in `core`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Balance {
|
||||
pub ausweich: f32,
|
||||
pub flucht: f32,
|
||||
pub stamina_regen: f32,
|
||||
/// Deckel, damit ein Wesen nicht immun wird — Immunitaet ist keine Zahl,
|
||||
/// sondern ein Spielabbruch (Ticket 14).
|
||||
pub resistance_cap: f32,
|
||||
pub hit_floor: f32,
|
||||
pub hit_ceiling: f32,
|
||||
// TODO(M4/M5): `exp_base`, `exp_exponent` (Ticket 15) und `luck_factor`
|
||||
// (Ticket 16) — sie kommen mit ihren Lesern, nicht davor.
|
||||
}
|
||||
|
||||
impl Default for Balance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ausweich: 0.02,
|
||||
flucht: 0.05,
|
||||
stamina_regen: 4.0,
|
||||
resistance_cap: 0.8,
|
||||
hit_floor: 0.05,
|
||||
hit_ceiling: 0.95,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein von Hand gezeichneter Raum als ASCII-Block (Ticket 10).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Prefab {
|
||||
@@ -142,12 +271,22 @@ pub struct FloorDef {
|
||||
pub required: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub prefabs: Vec<WeightedTag>,
|
||||
/// Welche Gegner die `e`-Plaetze fuellen. Gewichtet, per **ID** — anders
|
||||
/// als bei Prefabs, weil eine Etage ihre Gegner benennen koennen soll.
|
||||
#[serde(default)]
|
||||
pub creatures: Vec<WeightedId>,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
// TODO(M3): `creatures` — welche Gegner die `e`-Plaetze fuellen.
|
||||
// TODO(M4): `loot` — die Beutetabelle fuer die `l`-Plaetze (Ticket 16).
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WeightedId {
|
||||
pub id: String,
|
||||
#[serde(default = "one")]
|
||||
pub weight: u32,
|
||||
}
|
||||
|
||||
/// Alle geladenen Definitionen. Liegt **neben** `Game`, nicht darin — als Feld
|
||||
/// kollidierte `&mut game.player` mit `&game.registries` bei jedem
|
||||
/// Methodenaufruf (Ticket 07).
|
||||
@@ -159,8 +298,12 @@ pub struct Registries {
|
||||
pub options: HashMap<String, CharacterOption>,
|
||||
pub prefabs: HashMap<String, Prefab>,
|
||||
pub floors: HashMap<String, FloorDef>,
|
||||
// TODO(M3+): `affixes`, `abilities`, `quests`, `creatures`, `loot_tables`,
|
||||
// `scripts` — zwoelf sind es am Ende.
|
||||
pub abilities: HashMap<String, Ability>,
|
||||
pub creatures: HashMap<String, CreatureDef>,
|
||||
pub modifiers: HashMap<String, ModifierDef>,
|
||||
/// Keine Registry — ein Block mit festem Schema, feldweise verschmolzen.
|
||||
pub balance: Balance,
|
||||
// TODO(M4+): `affixes`, `quests`, `loot_tables`, `scripts`.
|
||||
}
|
||||
|
||||
impl Registries {
|
||||
|
||||
Reference in New Issue
Block a user