Timer
This commit is contained in:
@@ -3,6 +3,8 @@ use color::Rgba8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tinyrand::{Rand, Seeded, StdRand};
|
||||
|
||||
/// The visual representation of a single board cell.
|
||||
///
|
||||
@@ -397,14 +399,26 @@ pub struct GameState {
|
||||
pub board: Board,
|
||||
/// The in-game message log, oldest first (newest pushed at the end).
|
||||
pub log: Vec<LogLine>,
|
||||
/// Elapsed real time not yet consumed by [`GameState::tick`], used to fire
|
||||
/// once-per-second events regardless of the actual frame rate.
|
||||
tick_accum: Duration,
|
||||
/// The engine's random-number generator (currently used for tick colors).
|
||||
rng: StdRand,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
/// Creates a `GameState` from a pre-loaded [`Board`].
|
||||
pub fn new(board: Board) -> Self {
|
||||
// Seed the RNG from the wall clock so colors differ run-to-run.
|
||||
let seed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
Self {
|
||||
board,
|
||||
log: Vec::new(),
|
||||
tick_accum: Duration::ZERO,
|
||||
rng: StdRand::seed(seed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +427,29 @@ impl GameState {
|
||||
self.log.push(line);
|
||||
}
|
||||
|
||||
/// Advances real-time game state by `dt` (the elapsed time since the last
|
||||
/// tick). Called once per frame by the front-end's game loop.
|
||||
///
|
||||
/// For now this is a demo: it logs the word "tick" once per elapsed second,
|
||||
/// each in a random bright color. A `while` loop (rather than `if`) keeps the
|
||||
/// count correct even if a frame ran long enough to span multiple seconds.
|
||||
pub fn tick(&mut self, dt: Duration) {
|
||||
self.tick_accum += dt;
|
||||
while self.tick_accum >= Duration::from_secs(1) {
|
||||
self.tick_accum -= Duration::from_secs(1);
|
||||
let color = self.random_color();
|
||||
self.log(LogLine::new().push("tick", Some(color), None));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a random fully-saturated, full-value color by picking a random
|
||||
/// hue — bright enough to stay readable against dark backgrounds.
|
||||
fn random_color(&mut self) -> Rgba8 {
|
||||
let hue = self.rng.next_lim_u32(360) as f32;
|
||||
let (r, g, b) = hue_to_rgb(hue);
|
||||
Rgba8 { r, g, b, a: 255 }
|
||||
}
|
||||
|
||||
/// Attempts to move the player by `(dx, dy)` cells.
|
||||
///
|
||||
/// The move is ignored if the target cell is out of bounds or its behavior
|
||||
@@ -430,3 +467,64 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a hue in degrees to an RGB triple at full saturation and value.
|
||||
///
|
||||
/// Standard 6-segment HSV→RGB conversion (S = V = 1). Kept here rather than
|
||||
/// shared with the front-end because kiln-core does not depend on any UI crate;
|
||||
/// `kiln-tui` has its own copy for the terminal-status badge.
|
||||
fn hue_to_rgb(hue: f32) -> (u8, u8, u8) {
|
||||
let h = (hue % 360.0) / 60.0;
|
||||
// `x` ramps the secondary channel up/down within each 60° segment.
|
||||
let x = 1.0 - (h % 2.0 - 1.0).abs();
|
||||
let (r, g, b) = match h as u32 {
|
||||
0 => (1.0, x, 0.0),
|
||||
1 => (x, 1.0, 0.0),
|
||||
2 => (0.0, 1.0, x),
|
||||
3 => (0.0, x, 1.0),
|
||||
4 => (x, 0.0, 1.0),
|
||||
_ => (1.0, 0.0, x),
|
||||
};
|
||||
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A minimal 1×1 empty board for exercising `GameState` logic.
|
||||
fn empty_board() -> Board {
|
||||
Board {
|
||||
name: "test".into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty)],
|
||||
player: Player { x: 0, y: 0 },
|
||||
objects: Vec::new(),
|
||||
portals: Vec::new(),
|
||||
font: None,
|
||||
zoom: 1,
|
||||
scripts: HashMap::new(),
|
||||
board_script_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_logs_one_message_per_elapsed_second() {
|
||||
let mut game = GameState::new(empty_board());
|
||||
assert_eq!(game.log.len(), 0);
|
||||
|
||||
// Under a second of accumulated time logs nothing yet.
|
||||
game.tick(Duration::from_millis(500));
|
||||
assert_eq!(game.log.len(), 0);
|
||||
|
||||
// Crossing the one-second mark logs exactly one "tick".
|
||||
game.tick(Duration::from_millis(600));
|
||||
assert_eq!(game.log.len(), 1);
|
||||
assert_eq!(game.log[0].spans[0].text, "tick");
|
||||
|
||||
// A single long frame logs one message per whole second it spans.
|
||||
game.tick(Duration::from_secs(3));
|
||||
assert_eq!(game.log.len(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user