Dead code removed (21 methods, 4 fields, 3 constants): - Cell: MaterialId::ALL, MaterialId::from_u8 (unsafe transmute) - Grid: get_mut, clear, fill_rect, swap, dump_region, next buffer field - Entity: move_center, EntityManager::iter_mut - Player: move_dir field, entity_mut - VerletSolver: step, SubBody::add_vel, SubBody::apply_force - CellularAutomaton: tick_count - InputHandler: release_all, poll, Action::None - WindowInput: clear - GameSession: perform_action_and_step, is_recording, grid_mut - ReplayPlayer: from_recording - Material: empty() - VulkanRenderer: tick_count field - MaterialBrush: name() Warnings fixed: - Remove unused MaterialRegistry imports from renderers - Remove unused reg variables in terminal/vulkan/graphics - Remove unused water_surface in game.rs - Remove unused p/y_death in tests - Remove unused qf_slice in graphics.rs Duplication eliminated: - main.rs: run_ascii_mode + run_graphics_mode → generic run_gpu_mode<R: GpuRenderer> ~140 lines of duplicated event loop code removed - GpuRenderer trait unifies VulkanRenderer and GraphicsRenderer API Unsafe code fixed: - rand_u8: static mut + unsafe → AtomicU8 + fetch_add (thread-safe) Module cleanup: - world/mod.rs: removed all unused re-exports - physics/mod.rs: removed all unused re-exports - entity/mod.rs: removed unused Entity/EntityId re-exports Result: ~6500 → ~5964 lines, 0 non-deprecation warnings, 109 tests pass
707 lines
23 KiB
Rust
707 lines
23 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use crate::entity::{EntityManager, EntityKind};
|
|
use crate::input::{Action, InputHandler};
|
|
use crate::physics::verlet::VerletSolver;
|
|
use crate::physics::collision::resolve_grid_collision;
|
|
use crate::render::Renderer;
|
|
use crate::world::cell::MaterialId;
|
|
use crate::world::grid::Grid;
|
|
use crate::world::cellular::CellularAutomaton;
|
|
use crate::entity::player::Player;
|
|
|
|
pub struct Game {
|
|
pub grid: Grid,
|
|
pub ca: CellularAutomaton,
|
|
pub verlet: VerletSolver,
|
|
pub entities: EntityManager,
|
|
pub player: Player,
|
|
pub input: InputHandler,
|
|
pub cam_x: i32,
|
|
pub cam_y: i32,
|
|
pub running: bool,
|
|
pub tick: u64,
|
|
pub fixed_dt: Duration,
|
|
pub accumulator: Duration,
|
|
pub last_time: Instant,
|
|
}
|
|
|
|
impl Game {
|
|
pub fn new() -> Self {
|
|
let mut entities = EntityManager::new();
|
|
let player = Player::new(&mut entities);
|
|
Self {
|
|
grid: Grid::new(),
|
|
ca: CellularAutomaton::new(),
|
|
verlet: VerletSolver::new(),
|
|
entities,
|
|
player,
|
|
input: InputHandler::new(),
|
|
cam_x: 100,
|
|
cam_y: 100,
|
|
running: true,
|
|
tick: 0,
|
|
fixed_dt: Duration::from_millis(16),
|
|
accumulator: Duration::ZERO,
|
|
last_time: Instant::now(),
|
|
}
|
|
}
|
|
|
|
pub fn init_world(&mut self) {
|
|
let w = self.grid.width;
|
|
let h = self.grid.height;
|
|
|
|
for x in 0..w {
|
|
self.grid.set_material(x as i32, (h - 1) as i32, MaterialId::Stone);
|
|
self.grid.set_material(x as i32, (h - 2) as i32, MaterialId::Dirt);
|
|
}
|
|
|
|
for x in 0..w {
|
|
let surface = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let surface = surface.max(10).min(h as i32 - 3);
|
|
for y in surface..(h as i32 - 2) {
|
|
if y == surface {
|
|
self.grid.set_material(x as i32, y, MaterialId::Grass);
|
|
} else {
|
|
self.grid.set_material(x as i32, y, MaterialId::Dirt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Water pool (left side)
|
|
let water_x = 40;
|
|
for x in water_x - 12..=water_x + 12 {
|
|
let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let s = s.max(10).min(h as i32 - 3);
|
|
for y in s - 8..s {
|
|
if self.grid.get(x as i32, y).is_empty() {
|
|
self.grid.set_material(x as i32, y, MaterialId::Water);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lava pool (right side)
|
|
let lava_x = 200;
|
|
for x in lava_x - 10..=lava_x + 10 {
|
|
let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let s = s.max(10).min(h as i32 - 3);
|
|
for y in s - 5..s {
|
|
if self.grid.get(x as i32, y).is_empty() {
|
|
self.grid.set_material(x as i32, y, MaterialId::Lava);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wood structure near center-left
|
|
let wood_x = 90;
|
|
let wood_surface = (h as i32 - 3) - ((wood_x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let wood_surface = wood_surface.max(10).min(h as i32 - 3);
|
|
for y in wood_surface - 8..wood_surface {
|
|
self.grid.set_material(wood_x, y, MaterialId::Wood);
|
|
self.grid.set_material(wood_x + 4, y, MaterialId::Wood);
|
|
}
|
|
for x in wood_x..=wood_x + 4 {
|
|
self.grid.set_material(x, wood_surface - 8, MaterialId::Wood);
|
|
}
|
|
|
|
// Sand dune (right of center)
|
|
let sand_x = 160;
|
|
let sand_surface = (h as i32 - 3) - ((sand_x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let sand_surface = sand_surface.max(10).min(h as i32 - 3);
|
|
for dx in -8..=8 {
|
|
let pile_h = (8.0 - (dx as f32).abs()) as i32;
|
|
for dy in 0..pile_h {
|
|
let y = sand_surface - 1 - dy;
|
|
if self.grid.get(sand_x + dx, y).is_empty() {
|
|
self.grid.set_material(sand_x + dx, y, MaterialId::Sand);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Acid pool (far left)
|
|
let acid_x = 15;
|
|
for x in acid_x - 5..=acid_x + 5 {
|
|
let s = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let s = s.max(10).min(h as i32 - 3);
|
|
for y in s - 4..s {
|
|
if self.grid.get(x as i32, y).is_empty() {
|
|
self.grid.set_material(x as i32, y, MaterialId::Acid);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stone wall obstacle (between player and water)
|
|
let wall_x = 110;
|
|
let wall_surface = (h as i32 - 3) - ((wall_x as f32 * 0.1).sin() * 5.0) as i32;
|
|
let wall_surface = wall_surface.max(10).min(h as i32 - 3);
|
|
for y in wall_surface - 6..wall_surface {
|
|
self.grid.set_material(wall_x, y, MaterialId::Stone);
|
|
self.grid.set_material(wall_x + 1, y, MaterialId::Stone);
|
|
}
|
|
|
|
self.grid.fill_border(MaterialId::Stone);
|
|
|
|
let cx = (w / 2) as f32;
|
|
let surface_x = cx as i32;
|
|
let mut surface_y = h as i32 - 3;
|
|
for y in 0..h as i32 {
|
|
if self.grid.get(surface_x, y).is_solid() && self.grid.get(surface_x, y).material != MaterialId::Stone {
|
|
surface_y = y;
|
|
break;
|
|
}
|
|
}
|
|
let cy = (surface_y as f32) - 5.0;
|
|
self.player.spawn_at(&mut self.entities, cx, cy);
|
|
|
|
let (px, py) = self.player.center(&self.entities);
|
|
self.center_camera_on(px, py);
|
|
}
|
|
|
|
pub fn center_camera_on(&mut self, px: f32, py: f32) {
|
|
self.cam_x = px as i32 - 60;
|
|
self.cam_y = py as i32 - 20;
|
|
}
|
|
|
|
pub fn run<R: Renderer>(&mut self, renderer: &mut R) {
|
|
if let Err(e) = renderer.init() {
|
|
eprintln!("Renderer init failed: {}", e);
|
|
return;
|
|
}
|
|
self.init_world();
|
|
self.input.start();
|
|
|
|
self.last_time = Instant::now();
|
|
|
|
while self.running {
|
|
let now = Instant::now();
|
|
let frame_time = now.duration_since(self.last_time);
|
|
self.last_time = now;
|
|
self.accumulator += frame_time;
|
|
|
|
while self.accumulator >= self.fixed_dt {
|
|
self.fixed_update();
|
|
self.accumulator -= self.fixed_dt;
|
|
}
|
|
|
|
let vw = renderer.viewport_w();
|
|
let vh = renderer.viewport_h();
|
|
let (px, py) = self.player.center(&self.entities);
|
|
self.cam_x = px as i32 - (vw as i32 / 2);
|
|
self.cam_y = py as i32 - (vh as i32 / 2);
|
|
|
|
if let Err(e) = renderer.render(&self.grid, &self.entities, self.cam_x, self.cam_y) {
|
|
eprintln!("Render error: {}", e);
|
|
break;
|
|
}
|
|
|
|
self.handle_input(vw, vh);
|
|
}
|
|
|
|
self.input.stop();
|
|
|
|
if let Err(e) = renderer.shutdown() {
|
|
eprintln!("Renderer shutdown failed: {}", e);
|
|
}
|
|
}
|
|
|
|
pub fn handle_input(&mut self, vw: usize, vh: usize) {
|
|
let one_shots = self.input.update();
|
|
|
|
for action in one_shots {
|
|
match action {
|
|
Action::Quit => {
|
|
self.running = false;
|
|
return;
|
|
}
|
|
Action::Paint(brush) => {
|
|
let mat = brush.to_material();
|
|
let cx = self.cam_x + (vw as i32 / 2);
|
|
let cy = self.cam_y + (vh as i32 / 2);
|
|
let r = 2;
|
|
for dy in -r..=r {
|
|
for dx in -r..=r {
|
|
if dx * dx + dy * dy <= r * r + 1 {
|
|
if let Some(m) = mat {
|
|
self.grid.set_material(cx + dx, cy + dy, m);
|
|
} else {
|
|
self.grid.set(cx + dx, cy + dy, crate::world::cell::Cell::empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if !self.running {
|
|
return;
|
|
}
|
|
|
|
// Jump: only on press, not held
|
|
if self.input.jump_requested() {
|
|
let on_ground = self.check_on_ground();
|
|
self.player.jump(&mut self.entities, on_ground);
|
|
}
|
|
|
|
// Movement: applied every tick while held (vector-style, direct velocity)
|
|
let held = self.input.held_actions();
|
|
let moving_left = held.iter().any(|a| *a == Action::MoveLeft);
|
|
let moving_right = held.iter().any(|a| *a == Action::MoveRight);
|
|
|
|
if moving_left && !moving_right {
|
|
self.player.move_left(&mut self.entities);
|
|
} else if moving_right && !moving_left {
|
|
self.player.move_right(&mut self.entities);
|
|
} else {
|
|
self.player.stop_horizontal(&mut self.entities);
|
|
}
|
|
|
|
for action in &held {
|
|
match action {
|
|
Action::MoveCameraLeft => self.cam_x -= 2,
|
|
Action::MoveCameraRight => self.cam_x += 2,
|
|
Action::MoveCameraUp => self.cam_y -= 2,
|
|
Action::MoveCameraDown => self.cam_y += 2,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn check_on_ground(&self) -> bool {
|
|
if let Some(e) = self.player.entity(&self.entities) {
|
|
let bottom_y = e.cy + e.half_h;
|
|
let bottom_cell = bottom_y.floor() as i32;
|
|
let frac = bottom_y - bottom_cell as f32;
|
|
if frac > 0.05 {
|
|
return false;
|
|
}
|
|
let left = (e.cx - e.half_w) as i32;
|
|
let right = (e.cx + e.half_w) as i32;
|
|
for x in left..=right {
|
|
if self.grid.in_bounds(x, bottom_cell) && self.grid.get(x, bottom_cell).is_solid() {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn fixed_update(&mut self) {
|
|
self.tick += 1;
|
|
|
|
self.ca.step(&mut self.grid);
|
|
|
|
self.update_entities();
|
|
|
|
self.apply_world_damage();
|
|
|
|
if self.tick % 30 == 0 {
|
|
self.try_spawn_goblin();
|
|
}
|
|
}
|
|
|
|
fn update_entities(&mut self) {
|
|
let solver = self.verlet.clone();
|
|
let substeps = solver.substeps;
|
|
let gravity = self.verlet.gravity;
|
|
let damping = self.verlet.damping;
|
|
let max_vel = solver.max_vel;
|
|
|
|
let entity_count = self.entities.all().len();
|
|
|
|
for idx in 0..entity_count {
|
|
let is_rigid = self.entities.all()[idx].rigid;
|
|
|
|
if is_rigid {
|
|
self.update_rigid_entity(idx, gravity, damping, max_vel);
|
|
} else {
|
|
self.update_ragdoll_entity(idx, &solver, substeps);
|
|
}
|
|
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
let mut total_health = 0.0;
|
|
let mut alive_count = 0;
|
|
for b in &e.bodies {
|
|
if b.alive {
|
|
total_health += b.health;
|
|
alive_count += 1;
|
|
}
|
|
}
|
|
if alive_count > 0 {
|
|
let avg = total_health / alive_count as f32;
|
|
if avg < 0.0 && e.alive {
|
|
e.kill();
|
|
}
|
|
}
|
|
|
|
let any_on_fire = e.bodies.iter().any(|b| b.alive && b.on_fire);
|
|
e.on_fire = any_on_fire;
|
|
if e.on_fire {
|
|
e.apply_fire_damage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_rigid_entity(&mut self, idx: usize, gravity: f32, _damping: f32, max_vel: f32) {
|
|
let (cx, cy, cvx, cvy, half_w, half_h) = {
|
|
let e = &self.entities.all()[idx];
|
|
(e.cx, e.cy, e.cvx, e.cvy, e.half_w, e.half_h)
|
|
};
|
|
|
|
let mut nx = cx;
|
|
let mut ny = cy;
|
|
let mut nvx = cvx;
|
|
let mut nvy = cvy * 0.99;
|
|
nvy += gravity;
|
|
|
|
let v_mag = (nvx * nvx + nvy * nvy).sqrt();
|
|
if v_mag > max_vel {
|
|
nvx = nvx / v_mag * max_vel;
|
|
nvy = nvy / v_mag * max_vel;
|
|
}
|
|
|
|
// Step 1: Try horizontal movement with slope stepping
|
|
nx += nvx;
|
|
if self.aabb_overlaps_solid(nx, ny, half_w, half_h) {
|
|
// Try stepping up 1 cell
|
|
let step = 1.0;
|
|
if !self.aabb_overlaps_solid(nx, ny - step, half_w, half_h) {
|
|
// Can step up — snap to top of the obstacle
|
|
ny -= step;
|
|
} else {
|
|
// Blocked — resolve X
|
|
let (resolved_x, hit) = self.resolve_aabb_x(idx, nx, ny, half_w, half_h, nvx);
|
|
nx = resolved_x;
|
|
if hit { nvx = 0.0; }
|
|
}
|
|
}
|
|
|
|
// Step 2: Vertical movement
|
|
ny += nvy;
|
|
let (resolved_y, hit_floor, hit_ceiling) = self.resolve_aabb_y(idx, nx, ny, half_w, half_h, nvy > 0.0);
|
|
ny = resolved_y;
|
|
if hit_floor { nvy = 0.0; }
|
|
if hit_ceiling { nvy = 0.0; }
|
|
|
|
// Check material contacts
|
|
let (touching_lava, touching_fire, touching_acid, in_liquid) = {
|
|
let grid = &self.grid;
|
|
let mut tl = false;
|
|
let mut tf = false;
|
|
let mut ta = false;
|
|
let mut il = false;
|
|
let min_x = (nx - half_w).floor() as i32;
|
|
let max_x = (nx + half_w).ceil() as i32;
|
|
let min_y = (ny - half_h).floor() as i32;
|
|
let max_y = (ny + half_h).ceil() as i32;
|
|
for y in min_y..=max_y {
|
|
for x in min_x..=max_x {
|
|
if !grid.in_bounds(x, y) { continue; }
|
|
let cell = grid.get(x, y);
|
|
if cell.material == MaterialId::Lava { tl = true; }
|
|
if cell.material == MaterialId::Fire { tf = true; }
|
|
if cell.material == MaterialId::Acid { ta = true; }
|
|
if cell.is_liquid() { il = true; }
|
|
}
|
|
}
|
|
(tl, tf, ta, il)
|
|
};
|
|
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.cx = nx;
|
|
e.cy = ny;
|
|
e.cvx = nvx;
|
|
e.cvy = nvy;
|
|
if in_liquid {
|
|
e.cvy *= 0.6;
|
|
e.cvx *= 0.8;
|
|
}
|
|
e.sync_bodies_to_center();
|
|
|
|
if touching_lava {
|
|
for b in &mut e.bodies {
|
|
if b.alive {
|
|
b.health -= 0.5;
|
|
if !b.on_fire { b.on_fire = true; }
|
|
}
|
|
}
|
|
}
|
|
if touching_fire {
|
|
for b in &mut e.bodies {
|
|
if b.alive {
|
|
b.health -= 0.15;
|
|
if !b.on_fire && b.health < 80.0 { b.on_fire = true; }
|
|
}
|
|
}
|
|
}
|
|
if touching_acid {
|
|
for b in &mut e.bodies {
|
|
if b.alive { b.health -= 0.25; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn aabb_overlaps_solid(&self, cx: f32, cy: f32, hw: f32, hh: f32) -> bool {
|
|
let grid = &self.grid;
|
|
let left = cx - hw;
|
|
let right = cx + hw;
|
|
let top = cy - hh;
|
|
let bottom = cy + hh;
|
|
|
|
let min_x = left.floor() as i32;
|
|
let max_x = right.ceil() as i32;
|
|
let min_y = top.floor() as i32;
|
|
let max_y = bottom.ceil() as i32;
|
|
|
|
for y in min_y..=max_y {
|
|
for x in min_x..=max_x {
|
|
if !grid.in_bounds(x, y) { continue; }
|
|
let cell = grid.get(x, y);
|
|
if !cell.is_solid() { continue; }
|
|
let cl = x as f32;
|
|
let cr = (x + 1) as f32;
|
|
let ct = y as f32;
|
|
let cb = (y + 1) as f32;
|
|
if right > cl && left < cr && bottom > ct && top < cb {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn resolve_aabb_x(&self, _idx: usize, cx: f32, cy: f32, hw: f32, hh: f32, vx: f32) -> (f32, bool) {
|
|
let grid = &self.grid;
|
|
let left = cx - hw;
|
|
let right = cx + hw;
|
|
let top = cy - hh;
|
|
let bottom = cy + hh;
|
|
|
|
let min_x = left.floor() as i32;
|
|
let max_x = right.ceil() as i32;
|
|
let min_y = top.floor() as i32;
|
|
let max_y = bottom.ceil() as i32;
|
|
|
|
let mut new_cx = cx;
|
|
let mut hit = false;
|
|
|
|
for y in min_y..=max_y {
|
|
for x in min_x..=max_x {
|
|
if !grid.in_bounds(x, y) { continue; }
|
|
let cell = grid.get(x, y);
|
|
if !cell.is_solid() { continue; }
|
|
|
|
let cell_left = x as f32;
|
|
let cell_right = (x + 1) as f32;
|
|
let cell_top = y as f32;
|
|
let cell_bottom = (y + 1) as f32;
|
|
|
|
if bottom <= cell_top || top >= cell_bottom {
|
|
continue;
|
|
}
|
|
|
|
if vx > 0.0 {
|
|
let pen = right - cell_left;
|
|
if pen > 0.0 && pen < 1.5 {
|
|
new_cx -= pen;
|
|
hit = true;
|
|
}
|
|
} else if vx < 0.0 {
|
|
let pen = cell_right - left;
|
|
if pen > 0.0 && pen < 1.5 {
|
|
new_cx += pen;
|
|
hit = true;
|
|
}
|
|
} else {
|
|
let pen_left = right - cell_left;
|
|
let pen_right = cell_right - left;
|
|
if pen_left < pen_right && pen_left > 0.0 && pen_left < 1.5 {
|
|
new_cx -= pen_left;
|
|
hit = true;
|
|
} else if pen_right > 0.0 && pen_right < 1.5 {
|
|
new_cx += pen_right;
|
|
hit = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(new_cx, hit)
|
|
}
|
|
|
|
fn resolve_aabb_y(&self, _idx: usize, cx: f32, cy: f32, hw: f32, hh: f32, moving_down: bool) -> (f32, bool, bool) {
|
|
let grid = &self.grid;
|
|
let left = cx - hw;
|
|
let right = cx + hw;
|
|
let top = cy - hh;
|
|
let bottom = cy + hh;
|
|
|
|
let min_x = left.floor() as i32;
|
|
let max_x = right.ceil() as i32;
|
|
let min_y = top.floor() as i32;
|
|
let max_y = bottom.ceil() as i32;
|
|
|
|
let mut max_pen = 0.0f32;
|
|
let mut hit_floor = false;
|
|
let mut hit_ceiling = false;
|
|
|
|
for y in min_y..=max_y {
|
|
for x in min_x..=max_x {
|
|
if !grid.in_bounds(x, y) { continue; }
|
|
let cell = grid.get(x, y);
|
|
if !cell.is_solid() { continue; }
|
|
|
|
let cell_left = x as f32;
|
|
let cell_right = (x + 1) as f32;
|
|
let cell_top = y as f32;
|
|
let cell_bottom = (y + 1) as f32;
|
|
|
|
if right <= cell_left || left >= cell_right {
|
|
continue;
|
|
}
|
|
|
|
if bottom <= cell_top || top >= cell_bottom {
|
|
continue;
|
|
}
|
|
|
|
if moving_down {
|
|
let pen = bottom - cell_top;
|
|
if pen > max_pen {
|
|
max_pen = pen;
|
|
hit_floor = true;
|
|
}
|
|
} else {
|
|
let pen = cell_bottom - top;
|
|
if pen > max_pen {
|
|
max_pen = pen;
|
|
hit_ceiling = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_cy = if hit_floor {
|
|
cy - max_pen
|
|
} else if hit_ceiling {
|
|
cy + max_pen
|
|
} else {
|
|
cy
|
|
};
|
|
(new_cy, hit_floor, hit_ceiling)
|
|
}
|
|
|
|
fn update_ragdoll_entity(&mut self, idx: usize, solver: &crate::physics::verlet::VerletSolver, substeps: u32) {
|
|
let grid = &self.grid;
|
|
let mut bodies = self.entities.all()[idx].bodies.clone();
|
|
let constraints = self.entities.all()[idx].constraints.clone();
|
|
|
|
for b in &mut bodies {
|
|
if !b.alive {
|
|
continue;
|
|
}
|
|
if b.on_fire {
|
|
b.fire_timer += 1;
|
|
b.health -= 0.3;
|
|
if b.fire_timer > 120 {
|
|
b.on_fire = false;
|
|
b.fire_timer = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
for _ in 0..substeps {
|
|
solver.integrate(&mut bodies);
|
|
|
|
for b in &mut bodies {
|
|
if !b.alive {
|
|
continue;
|
|
}
|
|
let result = resolve_grid_collision(grid, b);
|
|
if result.touching_lava {
|
|
b.health -= 0.5;
|
|
if !b.on_fire { b.on_fire = true; }
|
|
}
|
|
if result.touching_fire {
|
|
b.health -= 0.15;
|
|
if !b.on_fire && b.health < 80.0 { b.on_fire = true; }
|
|
}
|
|
if result.touching_acid {
|
|
b.health -= 0.25;
|
|
}
|
|
}
|
|
|
|
for _ci in 0..4 {
|
|
solver.solve_constraints(&mut bodies, &constraints, 1);
|
|
for b in &mut bodies {
|
|
if !b.alive {
|
|
continue;
|
|
}
|
|
resolve_grid_collision(grid, b);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.bodies = bodies;
|
|
}
|
|
}
|
|
|
|
fn apply_world_damage(&mut self) {
|
|
let mut to_kill: Vec<usize> = Vec::new();
|
|
for (i, e) in self.entities.all().iter().enumerate() {
|
|
if !e.alive {
|
|
continue;
|
|
}
|
|
let mut dead_parts = 0;
|
|
for b in &e.bodies {
|
|
if !b.alive || b.health <= 0.0 {
|
|
dead_parts += 1;
|
|
}
|
|
}
|
|
if dead_parts == e.bodies.len() {
|
|
to_kill.push(i);
|
|
}
|
|
}
|
|
for i in to_kill {
|
|
if let Some(e) = self.entities.all_mut().get_mut(i) {
|
|
e.kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_spawn_goblin(&mut self) {
|
|
let alive_goblins = self.entities.all().iter().filter(|e| e.alive && e.kind == EntityKind::Goblin).count();
|
|
if alive_goblins >= 3 {
|
|
return;
|
|
}
|
|
|
|
let (px, _py) = self.player.center(&self.entities);
|
|
let spawn_x = px as i32 + if px as i32 % 2 == 0 { 15 } else { -15 };
|
|
if !self.grid.in_bounds(spawn_x, 0) {
|
|
return;
|
|
}
|
|
|
|
let mut surface_y = self.grid.height as i32 - 3;
|
|
for y in 0..self.grid.height as i32 {
|
|
let cell = self.grid.get(spawn_x, y);
|
|
if cell.is_solid() && cell.material != MaterialId::Stone {
|
|
surface_y = y;
|
|
break;
|
|
}
|
|
}
|
|
let spawn_y = surface_y - 5;
|
|
|
|
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
|
return;
|
|
}
|
|
|
|
let id = self.entities.spawn(EntityKind::Goblin);
|
|
if let Some(g) = self.entities.get_mut(id) {
|
|
g.build_humanoid(spawn_x as f32, spawn_y as f32);
|
|
}
|
|
}
|
|
}
|