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

@@ -16,7 +16,7 @@ use toml::Spanned;
use super::registries::{
Ability, Affix, Category, CharacterOption, CreatureDef, FloorDef, Item, LootTable, ModifierDef,
Prefab, Registries, Slot,
Npc, Prefab, Registries, Slot,
};
/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus
@@ -261,6 +261,8 @@ struct ModFile {
affixes: Vec<Spanned<toml::Value>>,
#[serde(default)]
loot_tables: Vec<Spanned<toml::Value>>,
#[serde(default)]
npcs: Vec<Spanned<toml::Value>>,
/// **Keine** Tabelle mit `[[...]]`, sondern ein einzelner Block, der
/// feldweise verschmilzt — die ausdrueckliche Ausnahme zu Ticket 03.
#[serde(default)]
@@ -300,6 +302,7 @@ impl_entry!(CreatureDef);
impl_entry!(ModifierDef);
impl_entry!(Affix);
impl_entry!(LootTable);
impl_entry!(Npc);
struct FileCtx<'a> {
module: &'a str,
@@ -510,6 +513,7 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
parse_entries(parsed.modifiers, &ctx, &mut reg.modifiers, &mut warnings);
parse_entries(parsed.affixes, &ctx, &mut reg.affixes, &mut warnings);
parse_entries(parsed.loot_tables, &ctx, &mut reg.loot_tables, &mut warnings);
parse_entries(parsed.npcs, &ctx, &mut reg.npcs, &mut warnings);
// `[balance]` verschmilzt **feldweise** (Ticket 14): Ein Mod, der
// einen Knopf dreht, schreibt eine Zeile — und ein neuer Knopf in

View File

@@ -6,5 +6,5 @@ pub mod registries;
pub use loader::{load_mods, write_log};
pub use registries::{
Balance, CreatureDef, FloorDef, LootEntry, Prefab, Registries, parse_range,
Balance, CreatureDef, FloorDef, DialogOption, LootEntry, Npc, Prefab, Registries, parse_range,
};

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).