From aae1d1a56a935b2aeba7eb953b71106b190da974 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 11 Jul 2026 01:35:02 +0200 Subject: animations, introduction, no terminal --- src/game.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 13 deletions(-) (limited to 'src/game.rs') diff --git a/src/game.rs b/src/game.rs index 80ee0c6..ca30207 100644 --- a/src/game.rs +++ b/src/game.rs @@ -608,6 +608,7 @@ impl Player { const MOVE_DURATION: f64 = 0.18; // seconds to walk one tile const TURN_DURATION: f64 = 0.12; // seconds for a 90° turn const ENEMY_ANIM_DURATION: f64 = 0.10; // seconds for enemy movement +const ATTACK_DURATION: f64 = 0.12; // seconds for attack lunge pub const MAX_HP: u32 = 5; pub const MAX_SANITY: u32 = 100; @@ -659,6 +660,19 @@ pub struct Game { // ---- animation: enemies ---- enemy_anim_t: f64, + + // ---- animation: attack ---- + pub attacking: bool, + pub attack_t: f64, + attack_base_x: f64, + attack_base_y: f64, + + // ---- screen flash ---- + pub flash_color: u32, + pub flash_alpha: f64, + + // ---- controls overlay ---- + pub show_controls: bool, } fn smoothstep(t: f64) -> f64 { @@ -703,6 +717,13 @@ impl Game { turn_delta: 0.0, turn_t: 0.0, enemy_anim_t: 0.0, + attacking: false, + attack_t: 0.0, + attack_base_x: 0.0, + attack_base_y: 0.0, + flash_color: 0, + flash_alpha: 0.0, + show_controls: true, } } @@ -717,6 +738,11 @@ impl Game { /// Returns true when the current animation phase finishes and we should /// advance the state machine. pub fn update_animation(&mut self, dt: f64) -> bool { + // Flash always fades, regardless of state. + if self.flash_alpha > 0.0 { + self.flash_alpha = (self.flash_alpha - dt * 4.0).max(0.0); + } + // Turn animation runs independently — turning never consumes a turn. if self.turning { self.turn_t += dt / TURN_DURATION; @@ -751,6 +777,28 @@ impl Game { } } + // Animate attack lunge. + if self.attacking { + self.attack_t += dt / ATTACK_DURATION; + if self.attack_t >= 1.0 { + self.player.x = self.attack_base_x; + self.player.y = self.attack_base_y; + self.attacking = false; + } else { + // Lunge forward then back. + let lunge = if self.attack_t < 0.5 { + smoothstep(self.attack_t * 2.0) * 0.25 + } else { + (1.0 - smoothstep((self.attack_t - 0.5) * 2.0)) * 0.25 + }; + let fx = self.player.angle.cos(); + let fy = self.player.angle.sin(); + self.player.x = self.attack_base_x + fx * lunge; + self.player.y = self.attack_base_y + fy * lunge; + done = false; + } + } + if done { self.state = GameState::ResolvingTurn; self.resolve_turn(); @@ -921,12 +969,10 @@ impl Game { let new_hp = hp.saturating_sub(total_dmg); if new_hp == 0 { - // Enemy dies. self.maze.take_entity(fx, fy); self.kills += 1; self.set_message(&format!("You slay the enemy! ({} dmg)", total_dmg)); } else { - // Enemy survives and retaliates. self.maze.entity_at_mut(fx, fy).map(|e| { if let Entity::Enemy { hp: ref mut h, .. } = e { *h = new_hp; @@ -941,7 +987,9 @@ impl Game { self.state = GameState::GameOver; } } - self.advance_turn(); + self.flash_color = 0xFF_CC3333; + self.flash_alpha = 0.4; + self.begin_attack_animation(); true } @@ -950,11 +998,9 @@ impl Game { let new_hp = hp.saturating_sub(1); if !persistent || new_hp == 0 { - // Vanishes. self.maze.take_entity(fx, fy); self.set_message("The hallucination dissipates."); } else { - // Persistent — doesn't vanish yet. self.maze.entity_at_mut(fx, fy).map(|e| { if let Entity::Hallucination { hp: ref mut h, .. } = e { *h = new_hp; @@ -962,18 +1008,30 @@ impl Game { }); self.set_message("You strike the apparition - it remains."); } - self.advance_turn(); + self.flash_color = 0xFF_8833CC; + self.flash_alpha = 0.35; + self.begin_attack_animation(); true } _ => { self.set_message("You swing at the air."); - self.advance_turn(); + self.flash_color = 0xFF_FFFFFF; + self.flash_alpha = 0.05; + self.begin_attack_animation(); true } } } + fn begin_attack_animation(&mut self) { + self.attack_base_x = self.player.x; + self.attack_base_y = self.player.y; + self.attacking = true; + self.attack_t = 0.0; + self.state = GameState::AnimatingPlayer; + } + // ---- inventory -------------------------------------------------------- /// Use the currently selected inventory item (Z key). @@ -1107,7 +1165,10 @@ impl Game { /// Apply all per-turn effects (weakness, heartbeat, HP drain, message decay). fn resolve_turn(&mut self) { - self.weakness = (self.weakness + 1).min(MAX_WEAKNESS); + // Weakness grows every 3 turns, not every turn. + if self.heartbeat_count % 3 == 0 { + self.weakness = (self.weakness + 1).min(MAX_WEAKNESS); + } self.heartbeat_count += 1; // Message decay. @@ -1118,14 +1179,11 @@ impl Game { } } - // HP drain at max weakness. + // HP drain at max weakness — stops at 1, never kills. if self.weakness >= MAX_WEAKNESS { - if self.hp > 0 { + if self.hp > 1 { self.hp -= 1; } - if self.hp == 0 { - self.state = GameState::GameOver; - } } } -- cgit v1.2.3