feat(save): Speicherstand als Game-Baum
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.
This commit is contained in:
@@ -10,7 +10,7 @@ use ratatui::widgets::{Block, Paragraph, Row, Table};
|
||||
use super::grid::GridWidget;
|
||||
use super::screen::{Response, Screen};
|
||||
use super::theme;
|
||||
use crate::data::Registries;
|
||||
use crate::data::{Registries, save};
|
||||
use crate::game::{Attribute, CreatureRef, DamageType, Game, Outcome, RunOutcome};
|
||||
|
||||
pub struct App {
|
||||
@@ -28,6 +28,17 @@ impl App {
|
||||
/// Der Spielzustand fuehrt, der Stack folgt: `Game.enemies` und `Game.over`
|
||||
/// sind die Wahrheit. Ohne das muesste jede Stelle, die einen Kampf startet
|
||||
/// oder beendet, daran denken, den Screen mitzuschieben.
|
||||
/// Beim Beenden und beim Betreten einer Etage. Ein gescheiterter Autosave
|
||||
/// bricht das Spiel nicht ab — er landet in der Statuszeile.
|
||||
fn autosave(&mut self) {
|
||||
if self.game.over.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = save::save(&self.game, &self.registries) {
|
||||
self.game.last = format!("Speichern fehlgeschlagen: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_screens(&mut self) {
|
||||
let in_combat = !self.game.enemies.is_empty();
|
||||
let shows_combat = self.screens.iter().any(|s| matches!(s, Screen::Combat { .. }));
|
||||
@@ -63,11 +74,34 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(game: Game, registries: Registries, warnings: usize, log_name: Option<String>) -> std::io::Result<()> {
|
||||
pub fn run(
|
||||
game: Game,
|
||||
registries: Registries,
|
||||
warnings: usize,
|
||||
verworfen: usize,
|
||||
log_name: Option<String>,
|
||||
) -> std::io::Result<()> {
|
||||
let mut screens = vec![Screen::MainMenu];
|
||||
|
||||
// Die Rueckfrage steht **vor** dem Spiel, nicht als Zeile darin (Ticket
|
||||
// 09): Wer hier „nein" sagt, will den fehlenden Mod nachinstallieren — und
|
||||
// diese Wahl hat er nur jetzt, danach ist der verkrueppelte Held gespeichert.
|
||||
if verworfen > 0 {
|
||||
let wo = log_name.as_deref().unwrap_or("kein Log schreibbar");
|
||||
screens.push(Screen::Confirm {
|
||||
text: format!(
|
||||
"{verworfen} Einträge aus dem Speicherstand fehlen (siehe logs/{wo}).
|
||||
Fortfahren?"
|
||||
),
|
||||
on_yes: Box::new(Response::Pop),
|
||||
on_no: Box::new(Response::Quit),
|
||||
});
|
||||
}
|
||||
|
||||
let mut app = App {
|
||||
game,
|
||||
registries,
|
||||
screens: vec![Screen::MainMenu],
|
||||
screens,
|
||||
warnings,
|
||||
log_name,
|
||||
rejected: None,
|
||||
@@ -99,11 +133,13 @@ pub fn run(game: Game, registries: Registries, warnings: usize, log_name: Option
|
||||
}
|
||||
app.screens.pop();
|
||||
if app.screens.is_empty() {
|
||||
app.autosave();
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
Response::Push(s) => app.screens.push(s),
|
||||
Response::Command(cmd) => {
|
||||
let etage_vorher = app.game.floor.number;
|
||||
// Hier steht die Zusage aus Ticket 07 auf dem Pruefstand:
|
||||
// `&mut app.game` neben `&app.registries` in einem Aufruf.
|
||||
app.rejected = match app.game.execute(cmd, &app.registries) {
|
||||
@@ -117,8 +153,22 @@ pub fn run(game: Game, registries: Registries, warnings: usize, log_name: Option
|
||||
// die UI — die reagiert nur darauf. Dasselbe gilt fuer den
|
||||
// Kampf: `Game.enemies` ist die Wahrheit, der Screen folgt.
|
||||
app.sync_screens();
|
||||
|
||||
// Autosave beim **Betreten einer Etage** (Ticket 09). Bei
|
||||
// Tod stattdessen loeschen — Permadeath, und zwar erst hier
|
||||
// beim `GameOver`-Screen: Stuerzt das Spiel in der Sekunde
|
||||
// davor ab, ist der letzte Autosave noch da, und der stammt
|
||||
// vom Betreten der Etage, verschenkt also nichts.
|
||||
if app.game.over.is_some() {
|
||||
save::delete();
|
||||
} else if app.game.floor.number != etage_vorher {
|
||||
app.autosave();
|
||||
}
|
||||
}
|
||||
Response::Quit => {
|
||||
app.autosave();
|
||||
break Ok(());
|
||||
}
|
||||
Response::Quit => break Ok(()),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -141,6 +191,7 @@ fn draw(frame: &mut Frame, app: &App) {
|
||||
}
|
||||
Screen::Dialog { pick } => draw_dialog(frame, area, app, *pick),
|
||||
Screen::Shop { pick, selling } => draw_shop(frame, area, app, *pick, *selling),
|
||||
Screen::Confirm { text, .. } => draw_confirm(frame, area, text),
|
||||
Screen::Debug { pick } => draw_debug(frame, area, app, *pick),
|
||||
Screen::GameOver => draw_game_over(frame, area, app),
|
||||
}
|
||||
@@ -347,6 +398,17 @@ fn draw_shop(frame: &mut Frame, area: Rect, app: &App, pick: usize, selling: boo
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_confirm(frame: &mut Frame, area: Rect, text: &str) {
|
||||
let mut zeilen: Vec<Line> = text.lines().map(Line::from).collect();
|
||||
zeilen.push(Line::from(""));
|
||||
zeilen.push(Line::from("j / Enter fortfahren").style(Style::new().fg(Color::Yellow)));
|
||||
zeilen.push(Line::from("n / Esc beenden").style(Style::new().fg(Color::DarkGray)));
|
||||
frame.render_widget(
|
||||
Paragraph::new(zeilen).block(Block::bordered().title(" Rückfrage ")),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
/// Die Werkbank. Eine Liste, vier Tasten — was sich mit zwei vorhandenen
|
||||
/// Kommandos nachstellen laesst, steht hier nicht.
|
||||
fn draw_debug(frame: &mut Frame, area: Rect, app: &App, pick: usize) {
|
||||
|
||||
Reference in New Issue
Block a user