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:
2026-08-12 15:39:17 +02:00
parent 9c5efab331
commit b39491916f
28 changed files with 3414 additions and 279 deletions

91
src/game/floor.rs Normal file
View File

@@ -0,0 +1,91 @@
//! Etage — Ticket 10 entscheidet den Innenbau, M1 baut einen Testraum von Hand.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tile {
Wall,
Floor,
Door,
Stairs,
Entrance,
}
#[derive(Debug)]
pub struct Floor {
pub number: u8,
pub width: u16,
pub height: u16,
pub tiles: Vec<Tile>,
// TODO(M2): `rooms: Vec<Room>` und `player_room: Option<usize>` aus Ticket
// 10 — mit ihnen kommen `room_entered`, Kampfbeginn und die
// Raum-Beutetabellen. M1 hat genau einen Raum und braucht die
// Buchfuehrung noch nicht.
}
impl Floor {
pub fn tile(&self, x: u16, y: u16) -> Tile {
if x >= self.width || y >= self.height {
return Tile::Wall;
}
self.tiles[y as usize * self.width as usize + x as usize]
}
/// Handgebauter Testraum. Ticket 10 legt fest, dass Prefabs als ASCII-Bloecke
/// geschrieben werden — deshalb steht der Raum hier schon in derselben Form,
/// auch wenn der Generator erst in M2 kommt.
pub fn test_room() -> Self {
const LAYOUT: &[&str] = &[
"########################",
"#......................#",
"#..####....######......#",
"#..#..........#........#",
"#..#..........#...##...#",
"#.....####....#...##...#",
"#........#........>....#",
"#..<.....#.............#",
"########################",
];
let width = LAYOUT[0].len() as u16;
let height = LAYOUT.len() as u16;
let tiles = LAYOUT
.iter()
.flat_map(|row| row.chars())
.map(|c| match c {
'#' => Tile::Wall,
'>' => Tile::Stairs,
'<' => Tile::Entrance,
'+' => Tile::Door,
_ => Tile::Floor,
})
.collect();
Self { number: 1, width, height, tiles }
}
/// Startfeld: das `<` im Layout, sonst das erste begehbare Feld.
pub fn entrance(&self) -> (u16, u16) {
for y in 0..self.height {
for x in 0..self.width {
if self.tile(x, y) == Tile::Entrance {
return (x, y);
}
}
}
(1, 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn testraum_ist_rechteckig_und_hat_eingang() {
let f = Floor::test_room();
assert_eq!(f.tiles.len(), f.width as usize * f.height as usize);
assert_eq!(f.tile(f.entrance().0, f.entrance().1), Tile::Entrance);
// Rand ist geschlossen, sonst laeuft der Held aus der Karte.
assert_eq!(f.tile(0, 0), Tile::Wall);
assert_eq!(f.tile(f.width - 1, f.height - 1), Tile::Wall);
}
}