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
176 lines
6.0 KiB
Rust
176 lines
6.0 KiB
Rust
//! 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);
|
||
}
|
||
}
|