docs: ticket 10 (raum-prefab-format) aufloesen, nachtraege an 08 und 14, prefab und etage ins glossar

This commit is contained in:
2026-08-12 12:11:42 +02:00
parent d51ebb26a1
commit 6442e104ab
6 changed files with 214 additions and 6 deletions

View File

@@ -102,6 +102,20 @@ Stufe (c) kann niemand ein eigenes Event *auslösen* — ein namespaced Event w
Kommt Scripting (Ticket 13), wird `emit_event` eine gewöhnliche Aktion und die Liste bekommt eine
`Custom(String)`-Variante. Ein Zusatz, kein Umbau.
### Nachtrag aus Ticket 10: zwölf Events
`room_entered { prefab_id }` kommt dazu. `floor_entered` deckt nur die Etage ab — „die Tür fällt hinter
dir zu", ein Hinterhalt oder eine Quest „finde die Bibliothek" brauchen den einzelnen Raum. Prefabs
tragen Trigger wie jeder andere Registry-Inhalt, neue Maschinerie kostet es also nicht.
Das Event trägt zugleich den Kampfbeginn: Ein `room_entered` in einen Raum mit lebenden Gegnern
verschiebt sie nach `Game.enemies`.
**Beobachtung zur Liste selbst:** Sie wurde hier als „fest und geschlossen" festgelegt, und seither hat
*jedes* Ticket, das echte Spielsituationen beschreibt, einen Eintrag gebraucht — 14 zwei, 10 einen. Das
spricht nicht gegen die Entscheidung (namespaced Mod-Events wären ohne Scripting weiterhin sinnlos),
aber wer 15 oder 16 bearbeitet, sollte mit einem weiteren rechnen.
### Nachtrag aus Ticket 14: elf Events statt neun
`combat_started` und `combat_ended { outcome: Victory | Fled }` kommen dazu. Ohne sie ist „gewinne drei

View File

@@ -1,7 +1,7 @@
# 10 — Raum-Prefab-Format
Type: grilling
Status: open
Status: resolved
Blocked by: 03
## Question
@@ -35,3 +35,170 @@ Prefabs sind eine **weitere Registry über dieselbe Ladepipeline**, kein eigener
beantwortet die letzte Frage oben halb: Hinzufügen ist der Normalfall, Ersetzen geht über `overrides`.
Offen bleibt, ob **Generator-Parameter** (Etagen-Tabellen, Gewichtungen, Pflichträume) ebenfalls
Registry-Einträge sind — dann mit derselben Maschine moddbar — oder ein Sonderfall.
## Answer
Begriffe im [Glossar](../../../CONTEXT.md).
### Etagenaufbau: freie Platzierung mit Korridoren
Prefabs werden nacheinander an zufälliger Position gesetzt, Überlappung (plus ein Feld Rand)
verworfen, nach N Fehlversuchen aufgegeben — der klassische Rogue-Ansatz.
Verbunden wird **Raum `i` mit Raum `i−1`** in Platzierungsreihenfolge, danach ein paar zufällige
Zusatzkorridore für Schleifen. Korridore als L-Form, erst horizontal, dann vertikal.
Die Kette statt eines minimalen Spannbaums, weil sie **Erreichbarkeit strukturell garantiert**: Hängt
jeder Raum am vorigen, ist jeder Raum von jedem erreichbar — ohne Flutfüllung und ohne
Reparaturschleife. Das beantwortet die Garantiefrage aus dem Ticketkörper nebenbei mit. Prim wären
zwanzig Zeilen für dasselbe Ergebnis mit hübscheren Korridorlängen.
**Rotation und Spiegelung sind erlaubt** — acht Varianten je Prefab, und das Drehen eines ASCII-Blocks
ist eine Transposition. Achtmal so viel Inhalt aus derselben Handarbeit.
Kein Opt-out-Feld: 1.0 hat kein Prefab, dessen Ausrichtung etwas bedeutet. Taucht eines auf, ist
`rotate = false` eine Zeile — vorher wäre es ein Schalter, den niemand stellt.
### Ein Prefab
```toml
[[prefabs]]
id = "core:kreuzung"
tags = ["core:normal"]
layout = """
####+####
#.......#
#..e...e#
+.......+
#..l....#
#.......#
####+####
"""
```
| Zeichen | Bedeutung | | Zeichen | Bedeutung |
|---|---|---|---|---|
| `#` | Wand | | `l` | Beute-Platz |
| `.` | Boden | | `>` | Treppe |
| `+` | Tür | | `<` | Eingang |
| `e` | Gegner-Platz | | `n` | NPC-Platz |
Kein eigener Händler-Marker: **Ein Händler ist ein NPC.** Ein zweites Zeichen für denselben Platz wäre
dieselbe Sorte Vermischung, die Ticket 02 beim `ItemType` abgeräumt hat.
Marker stehen **im Block**, nicht als Koordinatenliste daneben. Der ganze Sinn von ASCII ist, dass ein
Modder den Raum *sieht*; Koordinaten daneben zu pflegen wäre die schlechtere Hälfte von beidem.
**Türen sind explizit.** Ohne sie stanzt der Generator Löcher in eine von Hand gezeichnete Wand — wer
Prefabs baut, will bestimmen, wo man hineinkommt, sonst hätte man auch würfeln können. Korridore
dürfen nur an `+` andocken.
**Ein `e` sagt „hier steht ein Gegner", nicht *welcher*.** Was einzieht, entscheidet die Etage. Sonst
wäre jedes Prefab an eine Schwierigkeitsstufe gebunden und derselbe hübsche Raum ließe sich nicht auf
Etage 1 *und* auf Etage 5 verwenden — genau die Wiederverwendung, für die man Prefabs baut.
Bei `n` ist es **umgekehrt**: Den NPC nennt das Prefab selbst (`npcs = ["core:blacksmith"]`), nicht
die Etage. NPCs skalieren nicht mit der Tiefe — ein Schmied auf Etage 5 ist derselbe Schmied wie auf
Etage 1, und ein `core:merchant`-Raum, dessen Händler ausgewürfelt würde, wäre kein Händlerraum. Das
Feld selbst gehört dem NPC-Ticket; hier steht nur, auf welcher Seite die Entscheidung liegt.
### Die Etage
```toml
[[floors]]
id = "core:floor_3"
number = 3
size = [80, 40] # Felder
rooms = 9 # Zielzahl inklusive Pflichtraeume
required = ["core:entrance", "core:stairs", "core:merchant"] # Tags
prefabs = [ { tag = "core:normal", weight = 10 }, { tag = "core:treasure", weight = 2 } ]
creatures = [ { id = "core:skeleton", weight = 5 }, { id = "core:ghoul", weight = 2 } ]
loot = "core:floor_3_table"
```
Ablauf: **Pflichträume zuerst** auf die leere Fläche, danach gewichtet auffüllen bis `rooms` erreicht
oder die Versuche erschöpft sind.
Ausgewählt wird durchgehend über **Tags**, auch bei den Pflichträumen. IDs würden bedeuten, dass ein
Mod keinen eigenen Eingangsraum beisteuern kann, ohne die ganze Etagen-Tabelle zu überschreiben.
Dass Pflichträume zuerst kommen, *ist* die Garantie — auf einer leeren Fläche passen sie praktisch
immer. Optionale Räume, die nicht mehr passen, entfallen einfach; das ist der Normalfall, kein Fehler.
Passt die Flächensumme der Pflicht-Tags **grundsätzlich** nicht in `size`, ist das ein Datenfehler und
beim Laden erkennbar: Warnzeile über die Maschinerie aus 04. Zur Laufzeit dann kein
Wiederholungszähler und kein Rückfallpfad — die Prüfung sitzt dort, wo der Fehler gemacht wurde.
`core:merchant` steht unter `required` und nicht unter `prefabs`: Ticket 07 hat Backtracking
ausgeschlossen, der Händler muss also garantiert auftauchen statt gewichtet.
Damit beantwortet sich auch die Frage aus der 03-Vorgabe: **Generator-Parameter sind Daten.**
`prefabs` und `floors` sind zwei gewöhnliche Registries — ein Mod ergänzt Etage 6 oder ersetzt Etage 3
über `overrides`, ohne dass hier etwas erfunden wird. Die Gesamtzahl steigt damit auf **zehn**.
### Kampf beginnt beim Betreten eines Raums
Die Bestellung aus 14, wer `Game.enemies` füllt: **Gegner gehören zum Raum, nicht zur Etage.** Ein
`room_entered` in einen Raum mit lebenden Gegnern verschiebt sie nach `Game.enemies` und startet den
Kampf; nach dem Sieg gilt der Raum als geräumt und ein erneutes Betreten startet nichts mehr.
Der Grund ist, was dadurch **nicht** gebaut werden muss: Frei umherlaufende Gegner bräuchten Pfadsuche,
Sichtlinien, Verfolgung und ein Bewegungsbudget pro Zug — ein eigenes System, das niemand bestellt hat
und das die Charter mit „Kampf im Menü" auch nicht nahelegt. Die `e`-Plätze aus dem Prefab sind
schlicht die Gegnerliste des Raums.
### `Floor` — der Innenbau aus Ticket 07
```rust
struct Floor {
number: u8,
tiles: Vec<Tile>, width: u16, height: u16,
rooms: Vec<Room>,
player_room: Option<usize>,
}
struct Room {
prefab: PrefabId, // fuer room_entered und Trigger
rect: Rect,
enemies: Vec<Creature>,
loot: Vec<(Pos, ItemInstance)>,
cleared: bool,
}
enum Tile { Wall, Floor, Door, Stairs, Entrance }
```
Korridore gehören zu **keinem** Raum — sie sind schlicht `Tile::Floor` außerhalb aller `rect`. Dadurch
feuert `room_entered` sauber beim Übergang Korridor → Raum, ohne Sonderfall, und `player_room: None`
ist ein echter Zustand statt eines Fehlers.
Gegner werden bei Kampfbeginn **verschoben, nicht kopiert** — sonst gäbe es zwei Wahrheiten über
denselben Gegner.
Ladeprüfung über die 04-Maschinerie: **`layout`-Zeilen müssen gleich lang sein**, sonst ist der Block
kein Rechteck und die Platzierung rechnet mit Müll. Ein Zweizeiler, der eine ganze Klasse von
Modder-Frust abfängt.
### Siegbedingung — bisher nirgends definiert
Kein Ticket hatte je festgelegt, was beim Abstieg von der letzten Etage passiert.
**`Descend` auf der letzten definierten Etage gewinnt den Lauf.** Es gibt keine Zahl „5" im Code: Die
Engine sucht `[[floors]]` mit `number = aktuell + 1` und findet sie keine, ist Schluss. Ein Mod, der
Etage 6 ergänzt, verlängert das Spiel ohne eine Zeile Engine-Code — dieselbe Bewegung wie bei
`core:attack` in Ticket 11: Was wie Engine-Logik aussieht, ist Inhalt.
### Zwei Nachträge an geschlossene Tickets
| Ticket | Nachtrag |
|---|---|
| **08** | `room_entered { prefab_id }` als **zwölftes** Event. `floor_entered` deckt nur die Etage ab; „die Tür fällt hinter dir zu", ein Hinterhalt oder eine Quest „finde die Bibliothek" brauchen den einzelnen Raum. Neue Maschinerie kostet es nicht — Prefabs sind gewöhnlicher Registry-Inhalt und dürfen Trigger tragen wie alles andere |
| **14** | `GameOver { outcome: Died \| Won }` statt nur `GameOver`. Der Bildschirm zeigt in beiden Fällen dasselbe — Etage, Zug, Gold, Verlauf —, nur die Überschrift unterscheidet sich. Zwei Screens für zwei Texte wären Verdopplung |
### Auflagen an andere Tickets
- **09 Save-Schema**: Layout oder Seed? Ticket 14 hat einen seedbaren Generator geliefert, Regenerieren
*wäre* also möglich — aber der Raumzustand (`cleared`, gefallene Gegner, genommene Beute) lässt sich
nicht regenerieren und muss ohnehin mit. 09 entscheidet, ob sich der Seed dann noch lohnt.
- **16 Loot-Tabellen**: `loot` an der Etage verweist auf eine Tabelle, die `l`-Plätze ziehen daraus.
- **Boss-Design**: Ein Boss ist derzeit ein Gegner in einem Prefab mit `core:boss`-Tag. Phasen fehlen
weiterhin.

View File

@@ -120,7 +120,9 @@ Trank.
`MainMenu` (trägt die Ladewarnungs-Zeile aus 04) · `CharacterCreation` (Registry-Schleife aus 02) ·
`Explore` · `Combat` · `Inventory` · `CharacterSheet` · `QuestLog` · `Shop` · `Dialog` · `Confirm`
**Nachtrag aus Ticket 14: `GameOver` als elfter Screen**, Vollbild. Die lazy Variante — zurück ins
**Nachtrag aus Ticket 14: `GameOver { outcome: Died | Won }` als elfter Screen**, Vollbild. Das
`outcome` kam aus Ticket 10 dazu — der Abstieg von der letzten definierten Etage gewinnt den Lauf, und
Sieg wie Tod zeigen dieselben Zahlen unter einer anderen Überschrift. Die lazy Variante — zurück ins
`MainMenu` mit einer Meldungszeile — wurde bewusst verworfen: Ein Abstiegs-Roguelike lebt davon, dass
der Lauf am Ende *erzählt* wird (Etage, Zug, Gold, Todesursache). Das ist der Moment, für den man fünf
Etagen gespielt hat, und kein anderer Screen kann ihn übernehmen.

View File

@@ -213,7 +213,7 @@ Zeile lang.
| **01** | `damage`-Aktion: `menge = amount + scale * damage-Attribut(Verursacher)`, beide Felder optional (Standard 0). Deckt Basisangriff (`scale = 1.0`), Feuerball (`amount = 20`) und Wuchtschlag (`scale = 1.5`) mit *einer* Formel. Dazu `flee` als **achte Aktion** |
| **07** | `initiative: i32` und `cooldowns: HashMap<AbilityId, u32>` am `Creature`; RNG-Seed in `Game`; **`enemies` ist der laufende Kampf**, nicht die Etage |
| **08** | `combat_started` und `combat_ended { outcome: Victory \| Fled }` — die Eventliste wächst auf **elf**. Der Tod des Helden braucht keine dritte Variante, `creature_died` mit `source = Player` deckt ihn ab |
| **11** | **`GameOver`** als elfter Screen, Vollbild. Bewusst nicht die lazy Variante mit einer Meldungszeile im Hauptmenü: Ein Abstiegs-Roguelike lebt davon, dass der Lauf am Ende erzählt wird — Etage, Zug, Gold, Todesursache |
| **11** | **`GameOver`** als elfter Screen, Vollbild. Bewusst nicht die lazy Variante mit einer Meldungszeile im Hauptmenü: Ein Abstiegs-Roguelike lebt davon, dass der Lauf am Ende erzählt wird — Etage, Zug, Gold, Todesursache. *Ticket 10 hat daraus `GameOver { outcome: Died \| Won }` gemacht, weil der Abstieg von der letzten Etage den Lauf gewinnt* |
### Auflagen an andere Tickets