feat: m1 vertikaler durchstich — mod-lader, registries, attributberechnung, screen-stack
Ersetzt src/ vollstaendig: print_box, include_str! und enum ItemType fallen, wie Ticket 01-03 es vorgezeichnet haben. - data/: Mod-Lader mit zwei Suchpfaden, selbstheilender load_order.toml, drei Phasen und zweistufiger Deserialisierung (03/04) - game/: Attributkatalog, Creature, Game::attribute ueber mehrere Quellen, Command mit Outcome (01/07/11/12) - ui/: Screen-Stack mit Response, Grid-Widget, Press-Filter (06/11/12) - mods/core/: echte TOML-Dateien auf der Platte statt include_str! 10 Tests, darunter der Borrow-Beweis fuer Registries-neben-Game, den Ticket 12 nicht fuehren konnte.
This commit is contained in:
161
src/game/attributes.rs
Normal file
161
src/game/attributes.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! 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).
|
||||
///
|
||||
// TODO(M3): `resistance` fehlt — es ist typ-indiziert (sieben Schadenstypen aus
|
||||
// Ticket 01) und braucht damit eine andere Form als die uebrigen.
|
||||
// Kommt mit dem Kampf.
|
||||
#[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,
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub const COUNT: usize = 14;
|
||||
|
||||
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,
|
||||
];
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
#[serde(default)]
|
||||
pub flat: f32,
|
||||
#[serde(default)]
|
||||
pub percent: f32,
|
||||
// TODO(M3): `duration`, `target` und `condition` aus Ticket 01/08. In M1
|
||||
// sind alle Modifikatoren dauerhaft und wirken auf den Traeger.
|
||||
}
|
||||
|
||||
/// 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 mods = [
|
||||
Modifier { attribute: Attribute::Strength, flat: 2.0, percent: 0.0 },
|
||||
Modifier { attribute: Attribute::Strength, flat: 3.0, percent: 10.0 },
|
||||
// Fremdes Attribut darf nicht mitzaehlen:
|
||||
Modifier { attribute: Attribute::Damage, flat: 100.0, percent: 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user