Files
rustkeep/src/game/attributes.rs
MDietermann d09fef7069 feat(save): Speicherstand als Game-Baum
Autosave beim Betreten einer Etage und beim Beenden, ein Slot, bei Tod
geloescht. Der Save ist der Baum selbst — keine eigenen Save-Structs, die
man zweimal pflegt.

- Kacheln als ASCII-Zeilen wie ein Prefab, nicht 3200 Enum-Eintraege
- atomar geschrieben: tmp plus rename, der einzige Stand ueberlebt einen
  Abbruch
- fremdes Schema wird abgelehnt, die Datei bleibt liegen
- fehlende Mods verwerfen Eintraege, nie den Lauf
- Confirm-Screen vor dem Spiel, wenn der Save Eintraege verloren hat

Fuenf Befunde: die Ereigniswarteschlange gehoert nicht in den Save, die
Feldreihenfolge ist Teil des TOML-Formats, Serialize fuer Floor muss von
Hand, Confirm braucht on_no, und die Rueckfrage galt zuerst auch fuer
Mod-Ladewarnungen.
2026-08-12 22:01:12 +02:00

187 lines
6.4 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, Serialize};
/// 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, Serialize, 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,
];
/// Worauf ein Aufstiegspunkt gesetzt werden darf (Ticket 15). Abgeleitete
/// Groessen wie `damage` fehlen bewusst: Punkte direkt dorthin zu setzen
/// umginge das Modifikatorsystem aus Ticket 01.
pub const ASSIGNABLE: [Attribute; 5] = [
Attribute::Strength,
Attribute::Dexterity,
Attribute::Constitution,
Attribute::Charisma,
Attribute::Luck,
];
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, Serialize, Deserialize)]
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);
}
}