diff --git a/examples/survivor_game/src/enemy.rs b/examples/survivor_game/src/enemy.rs new file mode 100644 index 0000000..8b0cd33 --- /dev/null +++ b/examples/survivor_game/src/enemy.rs @@ -0,0 +1,26 @@ +use voltex_math::Vec3; + +pub struct Enemy { + pub position: Vec3, + pub speed: f32, + pub alive: bool, +} + +impl Enemy { + pub fn new(position: Vec3) -> Self { + Self { + position, + speed: 3.0, + alive: true, + } + } + + pub fn update(&mut self, player_pos: Vec3, dt: f32) { + let dir = player_pos - self.position; + let dir_xz = Vec3::new(dir.x, 0.0, dir.z); + if dir_xz.length_squared() > 0.01 { + let move_dir = dir_xz.normalize(); + self.position = self.position + move_dir * self.speed * dt; + } + } +} diff --git a/examples/survivor_game/src/game.rs b/examples/survivor_game/src/game.rs index 5b37c22..0c2f98f 100644 --- a/examples/survivor_game/src/game.rs +++ b/examples/survivor_game/src/game.rs @@ -2,12 +2,15 @@ use voltex_math::Vec3; use voltex_platform::InputState; use crate::player::Player; use crate::projectile::Projectile; +use crate::enemy::Enemy; +use crate::wave::WaveSystem; pub struct GameState { pub player: Player, pub projectiles: Vec, + pub enemies: Vec, + pub wave: WaveSystem, pub score: u32, - pub wave: u32, pub game_over: bool, } @@ -16,8 +19,9 @@ impl GameState { Self { player: Player::new(), projectiles: Vec::new(), + enemies: Vec::new(), + wave: WaveSystem::new(), score: 0, - wave: 1, game_over: false, } } @@ -42,5 +46,23 @@ impl GameState { proj.update(dt); } self.projectiles.retain(|p| p.is_alive()); + + // Wave system spawns + let alive_count = self.enemies.iter().filter(|e| e.alive).count(); + let spawns = self.wave.update(dt, alive_count); + for pos in spawns { + self.enemies.push(Enemy::new(pos)); + } + + // Update enemies (chase player) + let player_pos = self.player.position; + for enemy in &mut self.enemies { + if enemy.alive { + enemy.update(player_pos, dt); + } + } + + // Remove dead enemies + self.enemies.retain(|e| e.alive); } } diff --git a/examples/survivor_game/src/main.rs b/examples/survivor_game/src/main.rs index a69fc35..bf46e61 100644 --- a/examples/survivor_game/src/main.rs +++ b/examples/survivor_game/src/main.rs @@ -1,8 +1,10 @@ mod arena; mod camera; +mod enemy; mod game; mod player; mod projectile; +mod wave; use winit::{ application::ApplicationHandler, @@ -415,11 +417,13 @@ impl ApplicationHandler for SurvivorApp { // Camera follows player state.camera.target = state.game.player.position; - // Build per-frame model/material lists: arena entities + player + projectiles + // Build per-frame model/material lists: arena entities + player + projectiles + enemies let player_pos = state.game.player.position; let player_model = Mat4::translation(player_pos.x, player_pos.y + 0.5, player_pos.z); let num_projectiles = state.game.projectiles.len().min(MAX_ENTITIES - ARENA_ENTITIES - 1); - let num_entities = ARENA_ENTITIES + 1 + num_projectiles; + let base_enemy_idx = ARENA_ENTITIES + 1 + num_projectiles; + let num_enemies = state.game.enemies.len().min(MAX_ENTITIES.saturating_sub(base_enemy_idx)); + let num_entities = base_enemy_idx + num_enemies; // ----- Compute light VP for shadows ----- let light_dir = Vec3::new(-1.0, -2.0, -1.0).normalize(); @@ -440,11 +444,16 @@ impl ApplicationHandler for SurvivorApp { state.models[i] } else if i == ARENA_ENTITIES { player_model - } else { + } else if i < base_enemy_idx { let pi = i - ARENA_ENTITIES - 1; let p = &state.game.projectiles[pi]; Mat4::translation(p.position.x, p.position.y, p.position.z) * Mat4::scale(0.3, 0.3, 0.3) + } else { + let ei = i - base_enemy_idx; + let e = &state.game.enemies[ei]; + Mat4::translation(e.position.x, e.position.y, e.position.z) + * Mat4::scale(0.4, 0.4, 0.4) }; let sp_uniform = ShadowPassUniform { light_vp_model: (light_vp * model).cols, @@ -475,13 +484,20 @@ impl ApplicationHandler for SurvivorApp { } else if i == ARENA_ENTITIES { // Player: blue sphere (player_model, [0.2, 0.4, 1.0, 1.0], 0.3, 0.5) - } else { + } else if i < base_enemy_idx { // Projectile: yellow sphere let pi = i - ARENA_ENTITIES - 1; let p = &state.game.projectiles[pi]; let m = Mat4::translation(p.position.x, p.position.y, p.position.z) * Mat4::scale(0.3, 0.3, 0.3); (m, [1.0, 0.9, 0.1, 1.0], 0.1, 0.4) + } else { + // Enemy: red sphere + let ei = i - base_enemy_idx; + let e = &state.game.enemies[ei]; + let m = Mat4::translation(e.position.x, e.position.y, e.position.z) + * Mat4::scale(0.4, 0.4, 0.4); + (m, [1.0, 0.15, 0.1, 1.0], 0.2, 0.6) }; let cam_uniform = CameraUniform { @@ -619,6 +635,10 @@ impl ApplicationHandler for SurvivorApp { for pi in 0..num_projectiles { draw_shadow!(shadow_pass, state.player_mesh, ARENA_ENTITIES + 1 + pi); } + // Enemies + for ei in 0..num_enemies { + draw_shadow!(shadow_pass, state.player_mesh, base_enemy_idx + ei); + } } // ===== Pass 2: Color (PBR) ===== @@ -670,6 +690,10 @@ impl ApplicationHandler for SurvivorApp { for pi in 0..num_projectiles { draw_color!(render_pass, state.player_mesh, ARENA_ENTITIES + 1 + pi); } + // Enemies + for ei in 0..num_enemies { + draw_color!(render_pass, state.player_mesh, base_enemy_idx + ei); + } } state.gpu.queue.submit(std::iter::once(encoder.finish())); diff --git a/examples/survivor_game/src/wave.rs b/examples/survivor_game/src/wave.rs new file mode 100644 index 0000000..e1890d6 --- /dev/null +++ b/examples/survivor_game/src/wave.rs @@ -0,0 +1,66 @@ +use voltex_math::Vec3; + +pub struct WaveSystem { + pub wave_number: u32, + pub enemies_to_spawn: u32, + pub spawn_timer: f32, + pub wave_delay: f32, + pub between_spawn_delay: f32, + pub spawn_accumulator: f32, +} + +impl WaveSystem { + pub fn new() -> Self { + Self { + wave_number: 1, + enemies_to_spawn: 3, + spawn_timer: 2.0, // 2s before first wave + wave_delay: 5.0, + between_spawn_delay: 0.3, // stagger spawns + spawn_accumulator: 0.0, + } + } + + /// Returns spawn positions for this frame (if any) + pub fn update(&mut self, dt: f32, current_enemy_count: usize) -> Vec { + let mut spawns = Vec::new(); + + self.spawn_timer -= dt; + + if self.spawn_timer <= 0.0 && self.enemies_to_spawn > 0 { + self.spawn_accumulator += dt; + while self.spawn_accumulator >= self.between_spawn_delay && self.enemies_to_spawn > 0 { + self.spawn_accumulator -= self.between_spawn_delay; + self.enemies_to_spawn -= 1; + spawns.push(random_edge_position( + self.wave_number * 100 + self.enemies_to_spawn + spawns.len() as u32, + )); + } + } + + // All enemies spawned and killed -> next wave + if self.enemies_to_spawn == 0 && current_enemy_count == 0 && self.spawn_timer <= 0.0 { + self.wave_number += 1; + self.enemies_to_spawn = 2 + self.wave_number; + self.spawn_timer = self.wave_delay; + self.spawn_accumulator = 0.0; + } + + spawns + } +} + +fn random_edge_position(seed: u32) -> Vec3 { + // Deterministic pseudo-random position on arena edge + let hash = seed.wrapping_mul(2654435761); + let side = hash % 4; + let t = ((hash >> 8) % 100) as f32 / 100.0; // 0..1 + let pos = t * 18.0 - 9.0; // -9..9 + + match side { + 0 => Vec3::new(pos, 0.5, -9.5), // north edge + 1 => Vec3::new(pos, 0.5, 9.5), // south edge + 2 => Vec3::new(-9.5, 0.5, pos), // west edge + _ => Vec3::new(9.5, 0.5, pos), // east edge + } +}