feat: m2 etagengenerierung — prefabs, floors, korridore, abstieg und sieg
Der handgebaute Testraum ist weg; Etagen entstehen aus Daten. - game/rng.rs: xorshift64*, Seed im Game (Ticket 14) - game/generate.rs: freie Platzierung, Pflichtraeume zuerst, Kette i->i-1 fuer strukturelle Erreichbarkeit, acht Orientierungen - data/: [[prefabs]] und [[floors]] als neunte und zehnte Registry, Ladepruefungen fuer rechteckige Layouts und Pflichtraum-Flaeche - Descend plus Siegbedingung: keine [[floors]] mit der naechsten Nummer heisst gewonnen — die Zahl 5 steht nirgends im Code - GameOver-Screen, mitlaufender Bildausschnitt im Grid Befund: Korridore stanzten 2340 Loecher in gezeichnete Waende, weil sie AUF der Tuer starteten. Erst Schritt nach aussen, dann L-Form, sonst Wegsuche ueber den freien Raum — die kann eine Raumwand nicht beruehren. Der Test zaehlt jetzt null. 21 Tests.
This commit is contained in:
@@ -14,7 +14,7 @@ use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use toml::Spanned;
|
||||
|
||||
use super::registries::{Category, CharacterOption, Item, Registries, Slot};
|
||||
use super::registries::{Category, CharacterOption, FloorDef, Item, Prefab, Registries, Slot};
|
||||
|
||||
/// Genau drei harte Fehler (Ticket 04). Alles Uebrige ist Warnung plus
|
||||
/// Verwerfen des **einzelnen Eintrags**.
|
||||
@@ -244,6 +244,10 @@ struct ModFile {
|
||||
categories: Vec<Spanned<toml::Value>>,
|
||||
#[serde(default)]
|
||||
options: Vec<Spanned<toml::Value>>,
|
||||
#[serde(default)]
|
||||
prefabs: Vec<Spanned<toml::Value>>,
|
||||
#[serde(default)]
|
||||
floors: Vec<Spanned<toml::Value>>,
|
||||
}
|
||||
|
||||
/// Damit die vier Registry-Typen durch dieselbe Einlese-Schleife passen.
|
||||
@@ -272,6 +276,8 @@ impl_entry!(Item);
|
||||
impl_entry!(Slot);
|
||||
impl_entry!(Category);
|
||||
impl_entry!(CharacterOption);
|
||||
impl_entry!(Prefab);
|
||||
impl_entry!(FloorDef);
|
||||
|
||||
struct FileCtx<'a> {
|
||||
module: &'a str,
|
||||
@@ -366,6 +372,66 @@ fn resolve(reg: &mut Registries, warnings: &mut Vec<String>) {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
// Ticket 10: `layout`-Zeilen muessen gleich lang sein, sonst ist der Block
|
||||
// kein Rechteck und die Platzierung rechnet mit Muell. Ein Zweizeiler, der
|
||||
// eine ganze Klasse von Modder-Frust abfaengt.
|
||||
reg.prefabs.retain(|id, prefab| {
|
||||
let rows = prefab.rows();
|
||||
if rows.is_empty() {
|
||||
warnings.push(format!("{id}: layout ist leer — verworfen"));
|
||||
return false;
|
||||
}
|
||||
let width = rows[0].chars().count();
|
||||
match rows.iter().position(|r| r.chars().count() != width) {
|
||||
Some(i) => {
|
||||
warnings.push(format!(
|
||||
"{id}: layout-Zeile {} ist {} Zeichen breit, erwartet {width} — verworfen",
|
||||
i + 1,
|
||||
rows[i].chars().count()
|
||||
));
|
||||
false
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
});
|
||||
|
||||
// Ticket 10: Passen die Pflicht-Tags grundsaetzlich nicht in `size`, ist das
|
||||
// ein **Datenfehler und beim Laden erkennbar**. Zur Laufzeit gibt es dafuer
|
||||
// dann keinen Wiederholungszaehler und keinen Rueckfallpfad — die Pruefung
|
||||
// sitzt dort, wo der Fehler gemacht wurde.
|
||||
let prefabs = ®.prefabs;
|
||||
reg.floors.retain(|id, floor| {
|
||||
let mut needed = 0u32;
|
||||
|
||||
for tag in &floor.required {
|
||||
let smallest = prefabs
|
||||
.values()
|
||||
.filter(|p| p.tags.iter().any(|t| t == tag))
|
||||
.map(|p| {
|
||||
let (w, h) = p.size();
|
||||
(w as u32 + 1) * (h as u32 + 1)
|
||||
})
|
||||
.min();
|
||||
|
||||
match smallest {
|
||||
None => {
|
||||
warnings.push(format!("{id}: kein Prefab mit dem Pflicht-Tag '{tag}'"));
|
||||
return false;
|
||||
}
|
||||
Some(area) => needed += area,
|
||||
}
|
||||
}
|
||||
|
||||
let available = floor.size.0 as u32 * floor.size.1 as u32;
|
||||
if needed > available {
|
||||
warnings.push(format!(
|
||||
"{id}: Pflichträume brauchen mindestens {needed} Felder, size bietet {available}"
|
||||
));
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -414,6 +480,8 @@ pub fn load_mods() -> Result<(Registries, Vec<String>), LoadError> {
|
||||
parse_entries(parsed.slots, &ctx, &mut reg.slots, &mut warnings);
|
||||
parse_entries(parsed.categories, &ctx, &mut reg.categories, &mut warnings);
|
||||
parse_entries(parsed.options, &ctx, &mut reg.options, &mut warnings);
|
||||
parse_entries(parsed.prefabs, &ctx, &mut reg.prefabs, &mut warnings);
|
||||
parse_entries(parsed.floors, &ctx, &mut reg.floors, &mut warnings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ pub mod loader;
|
||||
pub mod registries;
|
||||
|
||||
pub use loader::{load_mods, write_log};
|
||||
pub use registries::Registries;
|
||||
pub use registries::{FloorDef, Prefab, Registries};
|
||||
|
||||
@@ -92,6 +92,62 @@ pub struct CharacterOption {
|
||||
// Startausruestung brauchen Kampf bzw. Inventar.
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
#[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>,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<String>,
|
||||
// TODO(M3): `creatures` — welche Gegner die `e`-Plaetze fuellen.
|
||||
// TODO(M4): `loot` — die Beutetabelle fuer die `l`-Plaetze (Ticket 16).
|
||||
}
|
||||
|
||||
/// Alle geladenen Definitionen. Liegt **neben** `Game`, nicht darin — als Feld
|
||||
/// kollidierte `&mut game.player` mit `&game.registries` bei jedem
|
||||
/// Methodenaufruf (Ticket 07).
|
||||
@@ -101,8 +157,10 @@ pub struct Registries {
|
||||
pub slots: HashMap<String, Slot>,
|
||||
pub categories: HashMap<String, Category>,
|
||||
pub options: HashMap<String, CharacterOption>,
|
||||
// TODO(M2+): `affixes`, `abilities`, `quests`, `creatures`, `prefabs`,
|
||||
// `floors`, `loot_tables`, `scripts` — zwoelf sind es am Ende.
|
||||
pub prefabs: HashMap<String, Prefab>,
|
||||
pub floors: HashMap<String, FloorDef>,
|
||||
// TODO(M3+): `affixes`, `abilities`, `quests`, `creatures`, `loot_tables`,
|
||||
// `scripts` — zwoelf sind es am Ende.
|
||||
}
|
||||
|
||||
impl Registries {
|
||||
@@ -113,4 +171,21 @@ impl Registries {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user