initial editor mode
This commit is contained in:
+91
-31
@@ -9,8 +9,10 @@
|
||||
|
||||
mod animation;
|
||||
mod cp437;
|
||||
mod editor;
|
||||
mod log;
|
||||
mod menu;
|
||||
mod mode;
|
||||
mod overlay;
|
||||
mod render;
|
||||
mod scroll_overlay;
|
||||
@@ -38,9 +40,14 @@ use std::process::ExitCode;
|
||||
use std::time::{Duration, Instant};
|
||||
use term::TerminalCaps;
|
||||
use crate::animation::{AnimationLayer, AnimWidget};
|
||||
use crate::input::{current_input_mode, handle_board_input, handle_menu_input, handle_scroll_input, InputMode};
|
||||
use crate::editor::{draw_editor, EditorState};
|
||||
use crate::input::{
|
||||
current_input_mode, handle_board_input, handle_editor_input, handle_menu_input,
|
||||
handle_scroll_input, InputMode,
|
||||
};
|
||||
use crate::log::{log_preview_line, LogWidget};
|
||||
use crate::menu::MenuWidget;
|
||||
use crate::mode::{Mode, PendingMode};
|
||||
use crate::scroll_overlay::ScrollOverlayWidget;
|
||||
use crate::speech::SpeechBubblesWidget;
|
||||
use crate::ui::Ui;
|
||||
@@ -85,8 +92,12 @@ fn main() -> ExitCode {
|
||||
// scripted object's `init()` hook.
|
||||
game.run_init();
|
||||
|
||||
let mut ui = Ui::default();
|
||||
let result = run(&mut terminal, &mut game, &mut ui);
|
||||
// Wrap the live game in the top-level Mode; editing later swaps this whole value.
|
||||
let mut mode = Mode::Play(game);
|
||||
|
||||
// Remember the world path so the editor (and play-reload) can reload from it.
|
||||
let mut ui = Ui { world_path: path.clone(), ..Ui::default() };
|
||||
let result = run(&mut terminal, &mut mode, &mut ui);
|
||||
|
||||
if caps.keyboard_enhancement {
|
||||
let _ = term::pop_kitty_flags();
|
||||
@@ -112,14 +123,14 @@ const FRAME: Duration = Duration::from_nanos(1_000_000_000 / 30);
|
||||
/// arrives, so input stays responsive while the game keeps ticking on its own.
|
||||
fn run(
|
||||
terminal: &mut ratatui::DefaultTerminal,
|
||||
game: &mut GameState,
|
||||
mode: &mut Mode,
|
||||
ui: &mut Ui,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// Redraw every iteration; ratatui diffs against the previous frame so
|
||||
// only changed cells are actually written to the terminal.
|
||||
terminal.draw(|frame| draw(frame, game, ui))?;
|
||||
terminal.draw(|frame| draw(frame, mode, ui))?;
|
||||
|
||||
// Wait for input only up to the next frame deadline so the loop wakes to
|
||||
// tick even with no keypresses.
|
||||
@@ -131,27 +142,37 @@ fn run(
|
||||
if matches!(&event, Event::Key(k) if k.kind == KeyEventKind::Release) {
|
||||
continue;
|
||||
}
|
||||
let terminal_h = terminal.size()?.height;
|
||||
match current_input_mode(game, ui) {
|
||||
let size = terminal.size()?;
|
||||
match current_input_mode(mode, ui) {
|
||||
InputMode::Board | InputMode::Frozen => {
|
||||
handle_board_input(event, ui.log.open, terminal_h, game, ui);
|
||||
if let Mode::Play(game) = mode {
|
||||
handle_board_input(event, ui.log.open, size.height, game, ui);
|
||||
}
|
||||
}
|
||||
InputMode::Menu => handle_menu_input(event, ui),
|
||||
InputMode::Scroll => {
|
||||
if let Mode::Play(game) = mode { handle_scroll_input(event, game, ui); }
|
||||
}
|
||||
InputMode::Editor => {
|
||||
if let Mode::Edit(ed) = mode { handle_editor_input(event, size.width, ed, ui); }
|
||||
}
|
||||
InputMode::Menu => handle_menu_input(event, game, ui),
|
||||
InputMode::Scroll => handle_scroll_input(event, game, ui),
|
||||
// Animation running and claiming no input mode — discard all events.
|
||||
InputMode::Ignore => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the game by the real time elapsed since the last tick. Only
|
||||
// resetting `last_tick` when we actually tick means time spent waiting on
|
||||
// input (or skipped Release events) is preserved in the next `dt`.
|
||||
// Advance the active mode by the real time elapsed since the last tick.
|
||||
// Only resetting `last_tick` when we actually tick means time spent waiting
|
||||
// on input (or skipped Release events) is preserved in the next `dt`.
|
||||
if last_tick.elapsed() >= FRAME {
|
||||
let dt = last_tick.elapsed();
|
||||
last_tick = Instant::now();
|
||||
ui.tick(dt, game);
|
||||
ui.tick(dt, mode);
|
||||
}
|
||||
|
||||
// Apply any Play↔Edit switch a menu action requested this frame.
|
||||
apply_pending_mode(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(());
|
||||
@@ -159,11 +180,64 @@ fn run(
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single frame: the board in a bordered block, and — when open — a log
|
||||
/// Applies a pending [`Mode`] switch, reloading the world from `ui.world_path`.
|
||||
///
|
||||
/// Entering the editor drops the running game; returning to play rebuilds a fresh
|
||||
/// [`GameState`] (so it reflects any edits) and re-runs object `init()` hooks. A
|
||||
/// load failure is logged to the play game (if any) and leaves the mode unchanged.
|
||||
fn apply_pending_mode(mode: &mut Mode, ui: &mut Ui) {
|
||||
let Some(pending) = ui.pending_mode.take() else { return };
|
||||
match world::load(&ui.world_path) {
|
||||
Ok(world) => match pending {
|
||||
PendingMode::EnterEditor => *mode = Mode::Edit(EditorState::new(world)),
|
||||
PendingMode::PlayWorld => {
|
||||
let mut game = GameState::from_world(world);
|
||||
game.run_init();
|
||||
*mode = Mode::Play(game);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Best-effort: surface the failure in the play log if we're still playing.
|
||||
if let Mode::Play(game) = mode {
|
||||
game.log(LogLine::error(format!("reload failed: {e}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// view ([`draw_editor`]); overlays (animation, menu, and — play-only — the scroll
|
||||
/// window) are drawn above whichever was rendered.
|
||||
fn draw(frame: &mut Frame, mode: &mut Mode, ui: &mut Ui) {
|
||||
match mode {
|
||||
Mode::Play(game) => draw_play(frame, game, ui),
|
||||
Mode::Edit(ed) => draw_editor(frame, ed, ui),
|
||||
}
|
||||
|
||||
// Overlay layer: animation > menu > scroll (mutually exclusive at runtime).
|
||||
let is_overlay_anim = ui.active_animation.as_ref()
|
||||
.map(|a| matches!(a.layer(), AnimationLayer::Overlay))
|
||||
.unwrap_or(false);
|
||||
|
||||
let area = frame.area();
|
||||
if is_overlay_anim {
|
||||
frame.render_widget(AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()), area);
|
||||
} else if let Some(menu) = ui.menu.as_mut() {
|
||||
frame.render_stateful_widget(MenuWidget, area, menu);
|
||||
} else if let Mode::Play(game) = mode {
|
||||
// The scroll window only exists while playing.
|
||||
if let Some(scroll) = &game.active_scroll {
|
||||
frame.render_stateful_widget(ScrollOverlayWidget::new(&scroll.lines), area, &mut ui.overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the play view: the board in a bordered block, and — when open — a log
|
||||
/// panel across the bottom. When the panel is closed the latest log message is
|
||||
/// previewed in the board's bottom-left border after a `[l]og:` label.
|
||||
/// Overlays (menu, scroll, animations) are drawn on top of everything.
|
||||
fn draw(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
|
||||
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();
|
||||
|
||||
@@ -189,20 +263,6 @@ fn draw(frame: &mut Frame, game: &GameState, ui: &mut Ui) {
|
||||
frame.render_widget(block, frame.area());
|
||||
draw_board_area(frame, inner, game, ui);
|
||||
}
|
||||
|
||||
// Overlay layer: animation > menu > scroll (mutually exclusive at runtime).
|
||||
let is_overlay_anim = ui.active_animation.as_ref()
|
||||
.map(|a| matches!(a.layer(), AnimationLayer::Overlay))
|
||||
.unwrap_or(false);
|
||||
|
||||
let area = frame.area();
|
||||
if is_overlay_anim {
|
||||
frame.render_widget(AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()), area);
|
||||
} else if let Some(menu) = ui.menu.as_mut() {
|
||||
frame.render_stateful_widget(MenuWidget, area, menu);
|
||||
} else if let Some(scroll) = &game.active_scroll {
|
||||
frame.render_stateful_widget(ScrollOverlayWidget::new(&scroll.lines), area, &mut ui.overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the inner board area: either the active board-layer animation or the live board.
|
||||
|
||||
Reference in New Issue
Block a user