feat(game): add collision detection, damage, and scoring

Projectiles destroy enemies on contact (radius check 0.55). Enemies
damage the player on contact with 1s invincibility window. Score +100
per kill. Game over when HP reaches 0. update() now returns FrameEvents
so the caller can trigger audio/visual feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:46:34 +09:00
parent a7cd14f413
commit d17732fcd5

View File

@@ -5,6 +5,12 @@ use crate::projectile::Projectile;
use crate::enemy::Enemy; use crate::enemy::Enemy;
use crate::wave::WaveSystem; use crate::wave::WaveSystem;
pub struct FrameEvents {
pub shot_fired: bool,
pub enemies_killed: u32,
pub player_hit: bool,
}
pub struct GameState { pub struct GameState {
pub player: Player, pub player: Player,
pub projectiles: Vec<Projectile>, pub projectiles: Vec<Projectile>,
@@ -26,18 +32,28 @@ impl GameState {
} }
} }
pub fn try_shoot(&mut self, aim_dir: Vec3) { pub fn try_shoot(&mut self, aim_dir: Vec3) -> bool {
if self.player.fire_cooldown <= 0.0 { if self.player.fire_cooldown <= 0.0 {
let origin = self.player.position + Vec3::new(0.0, 0.5, 0.0); let origin = self.player.position + Vec3::new(0.0, 0.5, 0.0);
self.projectiles.push(Projectile::new(origin, aim_dir, 20.0)); self.projectiles.push(Projectile::new(origin, aim_dir, 20.0));
self.player.fire_cooldown = 0.2; self.player.fire_cooldown = 0.2;
true
} else {
false
} }
} }
pub fn update(&mut self, input: &InputState, dt: f32) { pub fn update(&mut self, input: &InputState, dt: f32) -> FrameEvents {
let mut events = FrameEvents {
shot_fired: false,
enemies_killed: 0,
player_hit: false,
};
if self.game_over { if self.game_over {
return; return events;
} }
self.player.update(input, dt); self.player.update(input, dt);
self.player.fire_cooldown = (self.player.fire_cooldown - dt).max(0.0); self.player.fire_cooldown = (self.player.fire_cooldown - dt).max(0.0);
@@ -64,5 +80,56 @@ impl GameState {
// Remove dead enemies // Remove dead enemies
self.enemies.retain(|e| e.alive); self.enemies.retain(|e| e.alive);
// Collision: projectile vs enemy
let mut killed_enemies = Vec::new();
let mut killed_projectiles = Vec::new();
for (pi, proj) in self.projectiles.iter().enumerate() {
for (ei, enemy) in self.enemies.iter().enumerate() {
let dist = (proj.position - enemy.position).length();
if dist < 0.55 {
// projectile radius 0.15 + enemy radius 0.4
killed_enemies.push(ei);
killed_projectiles.push(pi);
self.score += 100;
events.enemies_killed += 1;
}
}
}
// Remove killed (reverse order to preserve indices)
killed_enemies.sort();
killed_enemies.dedup();
for &i in killed_enemies.iter().rev() {
self.enemies.remove(i);
}
killed_projectiles.sort();
killed_projectiles.dedup();
for &i in killed_projectiles.iter().rev() {
self.projectiles.remove(i);
}
// Collision: enemy vs player
if self.player.invincible_timer <= 0.0 {
for enemy in self.enemies.iter() {
let dist = (enemy.position - self.player.position).length();
if dist < 0.9 {
// player radius 0.5 + enemy radius 0.4
self.player.hp -= 1;
self.player.invincible_timer = 1.0;
events.player_hit = true;
if self.player.hp <= 0 {
self.game_over = true;
}
break;
}
}
}
// Update invincible timer
self.player.invincible_timer = (self.player.invincible_timer - dt).max(0.0);
events
} }
} }