playtest mode

This commit is contained in:
2026-06-20 19:05:46 -05:00
parent e7c3ca1139
commit d32914d99d
10 changed files with 168 additions and 9 deletions
+46 -2
View File
@@ -195,6 +195,9 @@ fn run(terminal: &mut ratatui::DefaultTerminal, mode: &mut Mode, ui: &mut Ui) ->
// Apply any Play↔Edit switch a menu action requested this frame.
apply_pending_mode(mode, ui);
// Enter/leave a playtest if the editor or a playtest Esc requested it.
apply_playtest_transition(mode, ui);
// A menu action may have set should_quit; exit once any close animation finishes.
if ui.should_quit && ui.active_animation.is_none() {
return Ok(());
@@ -229,6 +232,40 @@ fn apply_pending_mode(mode: &mut Mode, ui: &mut Ui) {
}
}
/// Applies a pending **playtest** enter or exit, swapping the top-level [`Mode`].
///
/// Entering: the editor parked a built [`GameState`] in
/// [`EditorState::pending_playtest`]; swap to [`Mode::Play`] and stash the editor in
/// [`Ui::suspended_editor`] so `Esc` can restore it. Taking the option first drops the
/// `&mut EditorState` borrow before [`std::mem::replace`], whose return value hands us
/// the old `Mode::Edit` (so the editor is preserved with no placeholder variant).
///
/// Exiting (`Esc` during a playtest set [`Ui::exit_playtest`]): drop the playtest game
/// and restore the suspended editor verbatim. The deep-copied playtest world is simply
/// dropped, so the editor's own boards were never touched.
fn apply_playtest_transition(mode: &mut Mode, ui: &mut Ui) {
// Enter: take the parked game (this ends the editor borrow before the swap).
let new_game = if let Mode::Edit(ed) = mode {
ed.pending_playtest.take()
} else {
None
};
if let Some(game) = new_game {
let old = std::mem::replace(mode, Mode::Play(game));
if let Mode::Edit(editor) = old {
ui.suspended_editor = Some(editor);
}
}
// Exit: restore the parked editor, dropping the playtest game.
if ui.exit_playtest {
ui.exit_playtest = false;
if let Some(editor) = ui.suspended_editor.take() {
*mode = Mode::Edit(editor);
}
}
}
/// Render a single frame for the active [`Mode`], then the overlay layer on top.
///
/// The body of the frame is either the play view ([`draw_play`]) or the editor
@@ -286,6 +323,13 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
// Borrow the board for the duration of the frame (no script runs during draw).
let board = &game.board();
// A playtest (launched from the editor) marks its title so it's distinct from play.
let title = if ui.suspended_editor.is_some() {
format!(" {} (playtest) ", board.name)
} else {
format!(" {} ", board.name)
};
if ui.log.open {
// Split the screen: board on top, log panel of `height` at the bottom.
let [board_area, log_area] =
@@ -294,7 +338,7 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
.areas(frame.area());
let block = Block::bordered()
.title(format!(" {} ", board.name))
.title(title)
.merge_borders(MergeStrategy::Exact);
let inner = block.inner(board_area);
frame.render_widget(block, board_area);
@@ -302,7 +346,7 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
frame.render_stateful_widget(LogWidget::new(&game.log), log_area, &mut ui.log);
} else {
// Panel closed: board fills the screen, latest message in the border.
let mut block = Block::bordered().title(format!(" {} ", board.name));
let mut block = Block::bordered().title(title);
block = block.title_bottom(log_preview_line(&game.log).left_aligned());
let inner = block.inner(frame.area());
frame.render_widget(block, frame.area());