feat(handel): NPC-Registry, Dialog, Laden und Stapelregel

Vierzehnte Registry: NPCs sind keine Wesen — kein Kampfzustand, keine
Initiative, keine Attribute. Ihre Antwortliste ist flach; was erst nach
einer Antwort erscheint, ist eine Option mit einer Bedingung.

- open_shop als elfte Aktion (game/ setzt eine Absicht, die UI schiebt)
- Talk/Choose/Buy/Sell, Talk wirkt auf das eigene Feld wie PickUp
- Preisspanne statt Goldkonto beim Haendler, Charisma in der Preisfunktion
- Warenkorb einmal bei der Etagenerzeugung gezogen
- Stapelregel aus Ticket 07 endlich gebaut: 50-120 Gold war 120 Eintraege
- Dialog- und Shop-Screen, NPC-Symbol im Grid
This commit is contained in:
2026-08-12 20:31:35 +02:00
parent 197f62ce33
commit 7ed15afaf9
22 changed files with 818 additions and 26 deletions

View File

@@ -212,8 +212,14 @@ pub struct Balance {
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.
/// Anteil des Preises, den ein Haendler beim Ankauf zahlt. **Die Spanne**:
/// Ohne sie ist Handel ein Exploit oder ein No-op — kaufen und sofort
/// zurueckverkaufen waere entweder gratis oder eine Geldmaschine.
pub verkauf: f32,
/// Wie stark `charisma` auf `price_modifier` wirkt (Ticket 02).
pub charisma_preis: f32,
// TODO(M5): `exp_base`, `exp_exponent` (Ticket 15) und `luck_factor`
// (Ticket 16) — sie kommen mit ihren Lesern, nicht davor.
}
impl Default for Balance {
@@ -225,10 +231,54 @@ impl Default for Balance {
resistance_cap: 0.8,
hit_floor: 0.05,
hit_ceiling: 0.95,
verkauf: 0.4,
charisma_preis: 1.5,
}
}
}
/// Eine Wahlmoeglichkeit im Gespraech. **Flach, kein Knotengraph** — was erst
/// ein Graph koennte, ist Erzaehlung, und das Story-Modul steht unter
/// *Out of scope*.
#[derive(Debug, Clone, Deserialize)]
pub struct DialogOption {
pub text: String,
#[serde(default)]
pub condition: Vec<Condition>,
#[serde(default)]
pub actions: Vec<Action>,
}
/// Die **vierzehnte** Registry. Bewusst kein `creatures`-Eintrag mit
/// `hostile = false`: Ein Haendler truege sonst `health`, `initiative`,
/// `cooldowns`, `ai` und `exp_reward` als dauerhaft toten Ballast.
#[derive(Debug, Clone, Deserialize)]
pub struct Npc {
pub id: String,
pub name: String,
#[serde(default = "npc_symbol")]
pub symbol: char,
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub greeting: String,
/// **Der Warenbestand ist eine Beutetabelle** — dieselben Gewichte,
/// dieselbe Tabelle-in-Tabelle, dieselben Affixe. Kein neuer
/// „Waren"-Begriff.
#[serde(default)]
pub shop: Option<String>,
#[serde(default)]
pub options: Vec<DialogOption>,
// TODO(M5): `triggers` — erst wenn `triggers_for` NPCs einsammelt. Als
// Feld ohne Leser waere es eine Zusage, die niemand einloest.
#[serde(default)]
pub overrides: Option<String>,
}
fn npc_symbol() -> char {
'n'
}
/// Eine benannte Modifikatorliste, die an einem **einzelnen Exemplar** haengt —
/// „Flammendes Eisenschwert" (Ticket 07).
///
@@ -323,6 +373,11 @@ pub struct Prefab {
/// Ersetzt die Etagentabelle fuer die `l`-Plaetze dieses Raums (Ticket 16).
#[serde(default)]
pub loot: Option<String>,
/// Wer auf den `n`-Plaetzen steht. Anders als bei `e` nennt das **Prefab**
/// den NPC, nicht die Etage: NPCs skalieren nicht mit der Tiefe — ein
/// Schmied auf Etage 5 ist derselbe wie auf Etage 1 (Ticket 10).
#[serde(default)]
pub npcs: Vec<String>,
#[serde(default)]
pub overrides: Option<String>,
}
@@ -397,6 +452,7 @@ pub struct Registries {
pub modifiers: HashMap<String, ModifierDef>,
pub affixes: HashMap<String, Affix>,
pub loot_tables: HashMap<String, LootTable>,
pub npcs: HashMap<String, Npc>,
/// Keine Registry — ein Block mit festem Schema, feldweise verschmolzen.
pub balance: Balance,
// TODO(M5+): `quests` (Ticket 08) und `scripts` (Ticket 13).