Files
Verbatim/src/game.rs
T
Emil 68fe0b87dd refactor: code cleanup — dead code, warnings, duplication
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
2026-06-21 01:36:25 +03:00

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);
}
}
}