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.
523 lines
18 KiB
Rust
523 lines
18 KiB
Rust
//! Die Definitionen, die aus TOML geladen werden — Ticket 02.
|
||
//!
|
||
//! Einige Felder liest in M1 noch niemand: `base_price` wartet auf den Handel,
|
||
//! `stackable` und `consumed_on_use` aufs Inventar, `symbol` auf Gegenstaende,
|
||
//! die auf dem Boden liegen. Sie stehen trotzdem hier, weil sie **das
|
||
//! Datenmodell** sind und `mods/core` sie bereits schreibt — sie erst spaeter
|
||
//! einzufuehren hiesse, die TOML-Dateien zweimal anzufassen.
|
||
//!
|
||
//! ponytail: modulweites `allow` — es versteckt ab M2 auch echte Leichen.
|
||
//! Raus, sobald Handel und Inventar die Felder tatsaechlich lesen.
|
||
#![allow(dead_code)]
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use serde::Deserialize;
|
||
|
||
use crate::game::action::Action;
|
||
use crate::game::condition::Condition;
|
||
use crate::game::event::Trigger;
|
||
use crate::game::Modifier;
|
||
|
||
/// Ein Ding im Spiel, **einheitlich strukturiert**. Es gibt keinen Item-Typ:
|
||
/// `enum ItemType` vermischte Slot, Verhalten und Kategorie (Ticket 02).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Item {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default)]
|
||
pub base_price: u32,
|
||
/// Leer = nicht ausruestbar.
|
||
pub slot: Option<String>,
|
||
#[serde(default)]
|
||
pub stackable: bool,
|
||
#[serde(default)]
|
||
pub consumed_on_use: bool,
|
||
#[serde(default = "default_symbol")]
|
||
pub symbol: char,
|
||
#[serde(default)]
|
||
pub color: Option<String>,
|
||
#[serde(default)]
|
||
pub modifiers: Vec<Modifier>,
|
||
/// Was beim Benutzen passiert (Trank, Schriftrolle).
|
||
#[serde(default)]
|
||
pub actions: Vec<Action>,
|
||
/// Faehigkeiten, die der Gegenstand **gewaehrt**, solange er getragen wird.
|
||
/// Nicht im Inventar tragbar — sonst koennte ein Magier seine Zauber
|
||
/// verkaufen (Ticket 02).
|
||
#[serde(default)]
|
||
pub abilities: Vec<String>,
|
||
#[serde(default)]
|
||
pub triggers: Vec<Trigger>,
|
||
/// Ersetzt den genannten Eintrag und uebernimmt dessen ID (Ticket 03).
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
fn default_symbol() -> char {
|
||
'?'
|
||
}
|
||
|
||
/// Eine benannte Stelle am Charakter. Moddbarer Inhalt, kein fester
|
||
/// Aufzaehlungswert (Ticket 02).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Slot {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default)]
|
||
pub order: i32,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
/// Kategorie von Charakteroptionen — selbst Inhalt, mit `pick = N`.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Category {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default)]
|
||
pub order: i32,
|
||
#[serde(default = "one")]
|
||
pub pick: u32,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
fn one() -> u32 {
|
||
1
|
||
}
|
||
|
||
/// Eine Wahl beim Charakterbau. Rasse und Klasse sind beides davon.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct CharacterOption {
|
||
pub id: String,
|
||
pub category: String,
|
||
pub name: String,
|
||
#[serde(default)]
|
||
pub description: String,
|
||
#[serde(default)]
|
||
pub modifiers: Vec<Modifier>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
// TODO(M3): `abilities` und `on_create` (Ticket 02) — Faehigkeiten und
|
||
// Startausruestung brauchen Kampf bzw. Inventar.
|
||
}
|
||
|
||
/// Ein benannter, befristeter Modifikator — „Vergiftet", „Gesegnet".
|
||
///
|
||
/// **Befund aus M3:** Ticket 01 laesst `apply_modifier` einen Modifikator per ID
|
||
/// referenzieren (`core:poisoned`), aber keine der zwoelf Registries definiert
|
||
/// benannte Modifikatoren. Das hier ist die **dreizehnte**.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct ModifierDef {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default = "default_symbol")]
|
||
pub symbol: char,
|
||
#[serde(default)]
|
||
pub color: Option<String>,
|
||
/// Zuege bis zum Verfall. Eine Uhr: der Zug (Ticket 01) — Debuffs ticken
|
||
/// auch beim Erkunden, damit „ich warte den Debuff draussen ab" eine
|
||
/// Entscheidung mit Kosten bleibt.
|
||
#[serde(default = "three")]
|
||
pub turns: u32,
|
||
/// Die Umrechnungen, die er traegt. Bewusst `attributes` und nicht
|
||
/// `modifiers`, damit ein Modifikator nicht Modifikatoren enthaelt.
|
||
#[serde(default)]
|
||
pub attributes: Vec<Modifier>,
|
||
#[serde(default)]
|
||
pub triggers: Vec<Trigger>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
fn three() -> u32 {
|
||
3
|
||
}
|
||
|
||
/// Ein benanntes, wiederholt ausloesbares **Angebot** an den Spieler
|
||
/// (Ticket 02) mit seiner Kampfsemantik aus Ticket 14.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Ability {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default)]
|
||
pub description: String,
|
||
/// Ausdauer — kein zweiter Ressourcenpool (Ticket 14).
|
||
#[serde(default)]
|
||
pub cost: f32,
|
||
/// Abklingzeit in Zuegen.
|
||
#[serde(default)]
|
||
pub cooldown: u32,
|
||
#[serde(default)]
|
||
pub condition: Vec<Condition>,
|
||
#[serde(default)]
|
||
pub actions: Vec<Action>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
/// Ein Eintrag der `ai`-Liste: erster passender **und verfuegbarer** gewinnt
|
||
/// (Ticket 14). Ein Eintrag ohne `condition` greift immer.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct AiEntry {
|
||
#[serde(default)]
|
||
pub condition: Vec<Condition>,
|
||
pub ability: String,
|
||
}
|
||
|
||
/// Gegnerdefinition. Die Luecke lag seit Ticket 02 offen — `Creature` war der
|
||
/// Laufzeittyp, woher seine Werte kommen, hat nie jemand gesagt (Ticket 14).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct CreatureDef {
|
||
pub id: String,
|
||
pub name: String,
|
||
#[serde(default = "default_symbol")]
|
||
pub symbol: char,
|
||
#[serde(default)]
|
||
pub color: Option<String>,
|
||
#[serde(default)]
|
||
pub tags: Vec<String>,
|
||
/// Grundwerte. Was fehlt, bleibt beim Standard.
|
||
#[serde(default)]
|
||
pub base: HashMap<String, f32>,
|
||
#[serde(default)]
|
||
pub abilities: Vec<String>,
|
||
#[serde(default)]
|
||
pub ai: Vec<AiEntry>,
|
||
#[serde(default)]
|
||
pub triggers: Vec<Trigger>,
|
||
/// Kurzform fuer einen Todes-Trigger, den sonst jede Gegnerdefinition
|
||
/// mitschleppen muesste (Ticket 15).
|
||
#[serde(default)]
|
||
pub exp_reward: u32,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
/// Die Stellknoepfe aus Ticket 14 und 16. **Keine Registry**: ein einzelner
|
||
/// Block mit festem Schema, der **feldweise** verschmilzt statt als ganzer
|
||
/// Eintrag ersetzt zu werden — sonst muesste ein Mod, der nur `ausweich` dreht,
|
||
/// alle Werte abschreiben, und ein neuer Knopf braeche ihn stillschweigend.
|
||
///
|
||
/// Die Standardwerte liegen hier in der Engine, nicht in `core`.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
#[serde(default)]
|
||
pub struct Balance {
|
||
pub ausweich: f32,
|
||
pub flucht: f32,
|
||
pub stamina_regen: f32,
|
||
/// Deckel, damit ein Wesen nicht immun wird — Immunitaet ist keine Zahl,
|
||
/// sondern ein Spielabbruch (Ticket 14).
|
||
pub resistance_cap: f32,
|
||
pub hit_floor: f32,
|
||
pub hit_ceiling: f32,
|
||
/// 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,
|
||
/// Die Erfahrungskurve: `exp_base × level^exp_exponent`. **Zwei Zahlen
|
||
/// statt fuenfzig Tabellenzeilen** (Ticket 15) — eine Tabelle liesse einen
|
||
/// Modder Level 13 zum Sprung machen, und das ist der Ausbau, nicht der
|
||
/// Anfang.
|
||
pub exp_base: f32,
|
||
pub exp_exponent: f32,
|
||
/// `p_extra = clamp(luck × luck_factor, 0, 0.9)` — eine Chance auf einen
|
||
/// zusaetzlichen Zug auf **jeder** Tabelle (Ticket 16).
|
||
pub luck_factor: f32,
|
||
/// Was ein Punkt in `strength` bzw. `constitution` tatsaechlich bewirkt.
|
||
///
|
||
/// **Befund aus M5:** Ticket 15 verteilt Punkte auf fuenf Grundwerte, aber
|
||
/// `strength` und `constitution` las bis dahin *niemand* — zwei der fuenf
|
||
/// Wahlmoeglichkeiten waren Fallen. Die Umrechnung sitzt hier und nicht in
|
||
/// `Game::attribute`, weil sie beim **Verteilen** einmal passiert und nicht
|
||
/// bei jedem Auslesen: Ein Punkt in Konstitution hebt den Grundwert
|
||
/// `max_health`, wie es Ticket 15 fuer die Zuwachs-Regel voraussetzt.
|
||
pub damage_per_strength: f32,
|
||
pub health_per_constitution: f32,
|
||
}
|
||
|
||
impl Default for Balance {
|
||
fn default() -> Self {
|
||
Self {
|
||
ausweich: 0.02,
|
||
flucht: 0.05,
|
||
stamina_regen: 4.0,
|
||
resistance_cap: 0.8,
|
||
hit_floor: 0.05,
|
||
hit_ceiling: 0.95,
|
||
verkauf: 0.4,
|
||
charisma_preis: 1.5,
|
||
exp_base: 20.0,
|
||
exp_exponent: 1.5,
|
||
luck_factor: 0.02,
|
||
damage_per_strength: 1.0,
|
||
health_per_constitution: 8.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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).
|
||
///
|
||
/// Abstufungen sind **eigene Eintraege** mit kleinerem Gewicht (`core:flaming`
|
||
/// neben `core:greater_flaming`), keine Zahl am Exemplar: Eine Staerke je
|
||
/// Exemplar haette Save-Format, Preisformel und Stapelregel auf einmal
|
||
/// gekostet (Ticket 16).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Affix {
|
||
pub id: String,
|
||
/// Wird dem Gegenstandsnamen vorangestellt: „Flammendes Eisenschwert".
|
||
pub name: String,
|
||
#[serde(default = "one_f32")]
|
||
pub price_factor: f32,
|
||
#[serde(default)]
|
||
pub modifiers: Vec<Modifier>,
|
||
#[serde(default)]
|
||
pub triggers: Vec<Trigger>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
fn one_f32() -> f32 {
|
||
1.0
|
||
}
|
||
|
||
/// Ein Eintrag einer Beutetabelle. Genau eine der vier Formen (Ticket 16).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct LootEntry {
|
||
#[serde(default = "one")]
|
||
pub weight: u32,
|
||
/// Wird **immer** ausgegeben und nimmt an der Gewichtung nicht teil — mit
|
||
/// Gewichten allein laesst sich „sicher" nicht ausdruecken, und Bosse
|
||
/// stehen in der Destination.
|
||
#[serde(default)]
|
||
pub guaranteed: bool,
|
||
|
||
#[serde(default)]
|
||
pub item: Option<String>,
|
||
/// Tabelle-in-Tabelle: liefert Seltenheitsstufen, ohne dass „Seltenheit"
|
||
/// ein Begriff werden muss. Zugleich der **Mod-Einhaengepunkt** — ein Mod
|
||
/// bindet `core:floor_3_table` als Untertabelle ein.
|
||
#[serde(default)]
|
||
pub table: Option<String>,
|
||
/// Dieselbe Maschinerie zieht Gegenstaende *und* Affixe.
|
||
#[serde(default)]
|
||
pub affix: Option<String>,
|
||
/// Niete. Ein gewoehnlicher Eintrag, kein eigenes `drop_chance`-Feld — die
|
||
/// Engine kennt eine Mechanik statt zweier.
|
||
#[serde(default)]
|
||
pub nothing: bool,
|
||
|
||
/// Stapelmenge, als Bereich `"1-2"`.
|
||
#[serde(default)]
|
||
pub count: Option<String>,
|
||
/// Affix-Tabelle fuer diesen Gegenstand.
|
||
#[serde(default)]
|
||
pub affixes: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct LootTable {
|
||
pub id: String,
|
||
/// Wie viele Zuege, als Bereich `"1-3"`.
|
||
#[serde(default)]
|
||
pub rolls: Option<String>,
|
||
#[serde(default)]
|
||
pub entries: Vec<LootEntry>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
/// Bereichsangabe `"1-3"` oder `"2"`. Ein Zweizeiler statt einer Grammatik.
|
||
pub fn parse_range(spec: Option<&String>) -> (u32, u32) {
|
||
let Some(s) = spec else { return (1, 1) };
|
||
match s.split_once('-') {
|
||
Some((a, b)) => (a.trim().parse().unwrap_or(1), b.trim().parse().unwrap_or(1)),
|
||
None => {
|
||
let n = s.trim().parse().unwrap_or(1);
|
||
(n, n)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Ein von Hand gezeichneter Raum als ASCII-Block (Ticket 10).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct Prefab {
|
||
pub id: String,
|
||
#[serde(default)]
|
||
pub tags: Vec<String>,
|
||
pub layout: String,
|
||
/// 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>,
|
||
}
|
||
|
||
impl Prefab {
|
||
/// Zeilen ohne fuehrende/abschliessende Leerzeile. TOML-Mehrzeiler beginnen
|
||
/// mit einem Zeilenumbruch, sobald `"""` allein auf der Zeile steht.
|
||
pub fn rows(&self) -> Vec<&str> {
|
||
self.layout.lines().filter(|l| !l.trim().is_empty()).collect()
|
||
}
|
||
|
||
pub fn size(&self) -> (u16, u16) {
|
||
let rows = self.rows();
|
||
(rows.first().map_or(0, |r| r.chars().count() as u16), rows.len() as u16)
|
||
}
|
||
}
|
||
|
||
/// Gewichteter Verweis auf eine Prefab-**Tag**-Gruppe. Tags statt IDs, sonst
|
||
/// muesste jede Etagentabelle jedes neue Modder-Prefab einzeln aufzaehlen.
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct WeightedTag {
|
||
pub tag: String,
|
||
#[serde(default = "one")]
|
||
pub weight: u32,
|
||
}
|
||
|
||
/// Eine Ebene des Dungeons und zugleich ihr Bauplan (Ticket 10).
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct FloorDef {
|
||
pub id: String,
|
||
pub number: u8,
|
||
/// Felder, nicht Zellen.
|
||
pub size: (u16, u16),
|
||
/// Zielzahl inklusive Pflichtraeume.
|
||
pub rooms: usize,
|
||
/// Tags, die garantiert vorkommen — zuerst platziert, das *ist* die Garantie.
|
||
#[serde(default)]
|
||
pub required: Vec<String>,
|
||
#[serde(default)]
|
||
pub prefabs: Vec<WeightedTag>,
|
||
/// Welche Gegner die `e`-Plaetze fuellen. Gewichtet, per **ID** — anders
|
||
/// als bei Prefabs, weil eine Etage ihre Gegner benennen koennen soll.
|
||
#[serde(default)]
|
||
pub creatures: Vec<WeightedId>,
|
||
/// Beutetabelle fuer die `l`-Plaetze (Ticket 16).
|
||
#[serde(default)]
|
||
pub loot: Option<String>,
|
||
#[serde(default)]
|
||
pub overrides: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct WeightedId {
|
||
pub id: String,
|
||
#[serde(default = "one")]
|
||
pub weight: u32,
|
||
}
|
||
|
||
/// Alle geladenen Definitionen. Liegt **neben** `Game`, nicht darin — als Feld
|
||
/// kollidierte `&mut game.player` mit `&game.registries` bei jedem
|
||
/// Methodenaufruf (Ticket 07).
|
||
#[derive(Debug, Default)]
|
||
pub struct Registries {
|
||
pub items: HashMap<String, Item>,
|
||
pub slots: HashMap<String, Slot>,
|
||
pub categories: HashMap<String, Category>,
|
||
pub options: HashMap<String, CharacterOption>,
|
||
pub prefabs: HashMap<String, Prefab>,
|
||
pub floors: HashMap<String, FloorDef>,
|
||
pub abilities: HashMap<String, Ability>,
|
||
pub creatures: HashMap<String, CreatureDef>,
|
||
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,
|
||
/// Wer geladen wurde, in Ladereihenfolge. **Einziger Leser ist der
|
||
/// Save-Kopf** (Ticket 03/09): Er stempelt die Versionen, damit ein
|
||
/// spaeterer Ladelauf „`meinmod` 1.0.0 → 1.2.0 seit dem Speichern" melden
|
||
/// kann. Das ist die ganze Bilanz des `version`-Felds.
|
||
pub mods: Vec<crate::data::save::ModStamp>,
|
||
// TODO(M5+): `quests` (Ticket 08) und `scripts` (Ticket 13).
|
||
}
|
||
|
||
impl Registries {
|
||
/// Alle Gegenstaende in stabiler Reihenfolge — fuer die Werkbank. Ohne das
|
||
/// haengt die Liste an der HashMap-Iterationsreihenfolge und der Cursor
|
||
/// zeigt bei jedem Start woanders hin.
|
||
pub fn items_ordered(&self) -> Vec<&Item> {
|
||
let mut v: Vec<&Item> = self.items.values().collect();
|
||
v.sort_by(|a, b| a.id.cmp(&b.id));
|
||
v
|
||
}
|
||
|
||
/// Slots in Anzeigereihenfolge. Der Charakterbogen schleift darueber,
|
||
/// statt ein Layout zu mauern (Ticket 02).
|
||
pub fn slots_ordered(&self) -> Vec<&Slot> {
|
||
let mut v: Vec<&Slot> = self.slots.values().collect();
|
||
v.sort_by_key(|s| (s.order, s.id.clone()));
|
||
v
|
||
}
|
||
|
||
/// Prefabs mit einem bestimmten Tag, in stabiler Reihenfolge — sonst
|
||
/// haengt die Etage von der HashMap-Iterationsreihenfolge ab und derselbe
|
||
/// Seed erzeugt zwei verschiedene Dungeons.
|
||
pub fn prefabs_tagged(&self, tag: &str) -> Vec<&Prefab> {
|
||
let mut v: Vec<&Prefab> =
|
||
self.prefabs.values().filter(|p| p.tags.iter().any(|t| t == tag)).collect();
|
||
v.sort_by(|a, b| a.id.cmp(&b.id));
|
||
v
|
||
}
|
||
|
||
pub fn floor_number(&self, number: u8) -> Option<&FloorDef> {
|
||
// Stabil, falls zwei Mods dieselbe Nummer vergeben: die kleinere ID.
|
||
// Ohne das entscheidet die HashMap-Reihenfolge, und derselbe Seed
|
||
// erzeugte zwei verschiedene Dungeons.
|
||
self.floors.values().filter(|f| f.number == number).min_by_key(|f| &f.id)
|
||
}
|
||
}
|