Files
rustkeep/src/game/attributes.rs
MDietermann 7ed15afaf9 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
2026-08-12 20:31:35 +02:00

176 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Attributkatalog und Modifikatoren — Ticket 01.
//!
//! Der Katalog ist **fest** und Engine-Vokabular ohne Namensraum. Ein Modder
//! kann ihn nicht erweitern; das ist Absicht (siehe Glossar, „Attribut").
use serde::Deserialize;
/// Fester Katalog aus Ticket 01. `level` steht hier, weil Bedingungen und
/// Formeln es lesen — die Quelle fuer Aufstiegspunkte ist aber `base.level`
/// und nicht dieser gelesene Wert (Ticket 15).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(usize)]
pub enum Attribute {
Strength,
Dexterity,
Constitution,
Charisma,
Luck,
Level,
MaxHealth,
MaxStamina,
Damage,
HitChance,
CriticalChance,
CriticalFactor,
PriceModifier,
CarryCapacity,
/// **Typ-indiziert**: Ein Modifikator darauf traegt zusaetzlich einen
/// `damage_type`. Die Basis in `Attributes` bleibt ungenutzt — Resistenz
/// kommt ausschliesslich aus Modifikatoren (Ticket 01).
Resistance,
}
impl Attribute {
pub const COUNT: usize = 15;
pub const ALL: [Attribute; Self::COUNT] = [
Attribute::Strength,
Attribute::Dexterity,
Attribute::Constitution,
Attribute::Charisma,
Attribute::Luck,
Attribute::Level,
Attribute::MaxHealth,
Attribute::MaxStamina,
Attribute::Damage,
Attribute::HitChance,
Attribute::CriticalChance,
Attribute::CriticalFactor,
Attribute::PriceModifier,
Attribute::CarryCapacity,
Attribute::Resistance,
];
pub fn name(self) -> &'static str {
match self {
Attribute::Strength => "Stärke",
Attribute::Dexterity => "Geschicklichkeit",
Attribute::Constitution => "Konstitution",
Attribute::Charisma => "Charisma",
Attribute::Luck => "Glück",
Attribute::Level => "Level",
Attribute::MaxHealth => "Leben (max)",
Attribute::MaxStamina => "Ausdauer (max)",
Attribute::Damage => "Schaden",
Attribute::HitChance => "Trefferchance",
Attribute::CriticalChance => "Kritische Chance",
Attribute::CriticalFactor => "Kritischer Faktor",
Attribute::PriceModifier => "Preis-Modifikator",
Attribute::CarryCapacity => "Tragfähigkeit",
Attribute::Resistance => "Resistenz",
}
}
}
/// Die Grundwerte eines Wesens. Bewusst ein Array statt vierzehn benannter
/// Felder: `get` und `set` sind damit je eine Zeile statt je eines
/// vierzehnarmigen `match`.
#[derive(Debug, Clone)]
pub struct Attributes([f32; Attribute::COUNT]);
impl Attributes {
pub fn get(&self, a: Attribute) -> f32 {
self.0[a as usize]
}
pub fn set(&mut self, a: Attribute, v: f32) {
self.0[a as usize] = v;
}
}
impl Default for Attributes {
/// Startwerte eines frischen Helden. Zahlen sind bewusst grob — Balancing
/// setzt ein spielbares Spiel voraus (siehe Map, „Not yet specified").
fn default() -> Self {
let mut a = Attributes([0.0; Attribute::COUNT]);
a.set(Attribute::Strength, 1.0);
a.set(Attribute::Dexterity, 1.0);
a.set(Attribute::Constitution, 1.0);
a.set(Attribute::Charisma, 1.0);
a.set(Attribute::Luck, 1.0);
a.set(Attribute::Level, 1.0);
a.set(Attribute::MaxHealth, 100.0);
a.set(Attribute::MaxStamina, 100.0);
a.set(Attribute::Damage, 2.0);
a.set(Attribute::HitChance, 0.8);
a.set(Attribute::CriticalFactor, 1.5);
a.set(Attribute::CarryCapacity, 50.0);
a
}
}
/// Rechnet ein Attribut um, solange er existiert — Ticket 01. Er wird nie
/// ausgefuehrt, sondern beim Auslesen aufsummiert.
#[derive(Debug, Clone, Deserialize)]
pub struct Modifier {
pub attribute: Attribute,
/// Nur bei `attribute = "resistance"`: Resistenz ist typ-indiziert
/// (Ticket 01), alle uebrigen Attribute sind Skalare.
#[serde(default)]
pub damage_type: Option<super::action::DamageType>,
#[serde(default)]
pub flat: f32,
#[serde(default)]
pub percent: f32,
// TODO(M5): `target` fuer Auren (`all_enemies`) und `condition` fuer
// bedingte Modifikatoren („+5 gegen Untote", Ticket 08). Beide
// brauchen `Game::attribute`s `against`-Parameter, der bereits
// in der Signatur steht.
}
/// Stapelregel aus Ticket 01: `(Basis + Σ flach) × (1 + Σ prozentual)`,
/// intern `f32`, gerundet an genau einer Stelle — hier gar nicht, weil das
/// Runden dorthin gehoert, wo ein Wert angezeigt oder verrechnet wird.
pub fn stack<'a>(base: f32, modifiers: impl Iterator<Item = &'a Modifier>, attr: Attribute) -> f32 {
let (flat, percent) = modifiers
.filter(|m| m.attribute == attr)
.fold((0.0, 0.0), |(f, p), m| (f + m.flat, p + m.percent));
(base + flat) * (1.0 + percent / 100.0)
}
#[cfg(test)]
mod tests {
use super::*;
/// `ALL` wird von Hand neben dem Enum gepflegt. Wer eine Variante ergaenzt
/// und die Liste vergisst, faellt sonst nirgends auf — der Charakterbogen
/// zeigt das Attribut dann einfach nie.
#[test]
fn all_ist_vollstaendig_und_in_indexreihenfolge() {
assert_eq!(Attribute::ALL.len(), Attribute::COUNT);
for (i, a) in Attribute::ALL.iter().enumerate() {
assert_eq!(*a as usize, i, "{a:?} steht an der falschen Stelle in ALL");
}
}
#[test]
fn stapelregel_erst_flach_dann_prozentual() {
let m = |attribute, flat, percent| Modifier {
attribute,
damage_type: None,
flat,
percent,
};
let mods = [
m(Attribute::Strength, 2.0, 0.0),
m(Attribute::Strength, 3.0, 10.0),
// Fremdes Attribut darf nicht mitzaehlen:
m(Attribute::Damage, 100.0, 0.0),
];
// (10 + 5) * 1.10 == 16.5, nicht 10*1.1 + 5 == 16.0
assert_eq!(stack(10.0, mods.iter(), Attribute::Strength), 16.5);
}
}