CRITICAL: - Fix use_item duplicating weapons/armor (not removed from inventory) - Fix unwrap() panic in update_ragdoll_entity - Fix descend proceeding without stairs when player entity missing HIGH: - Fix projectile hardcoded id==0 preventing enemy projectiles hitting player - Fix previously equipped item lost on re-equip (returned to inventory) - Fix i32::abs() overflow panic in background_color (use wrapping_abs) MEDIUM: - Fix slime AI ignoring vertical direction to player - Fix combat damage applied to dead player - Fix camera movement keys non-functional (added cam_offset_x/y) - Fix Unicode emoji status icons (replaced with ASCII F/P/I/B) - Fix Unicode UI chars (█░·─│┌┐└┘◆■ → ASCII #-./|++++*#) - Add missing char_bitmap glyphs (+, =, >, <, *, %, #, |, @, _) LOW: - Fix draw_messages writing to negative y coordinates - Fix WindowInput losing key state on focus loss (clear_keys on Focused(false)) - Fix compute_lighting using full grid scan instead of gather_sources_in_range All 171 tests + 14 scenarios pass.
1387 lines
45 KiB
Rust
1387 lines
45 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use crate::entity::player::Player;
|
|
use crate::entity::{EntityKind, EntityManager, ItemManager, ItemType};
|
|
use crate::input::{Action, InputHandler};
|
|
use crate::physics::collision::resolve_grid_collision;
|
|
use crate::physics::projectile::{ProjectileManager, ProjectileType};
|
|
use crate::physics::verlet::VerletSolver;
|
|
use crate::render::lighting;
|
|
use crate::render::Renderer;
|
|
use crate::ui::UiLayer;
|
|
use crate::world::cell::MaterialId;
|
|
use crate::world::cellular::CellularAutomaton;
|
|
use crate::world::grid::Grid;
|
|
|
|
pub struct Game {
|
|
pub grid: Grid,
|
|
pub ca: CellularAutomaton,
|
|
pub verlet: VerletSolver,
|
|
pub entities: EntityManager,
|
|
pub projectiles: ProjectileManager,
|
|
pub items: ItemManager,
|
|
pub player: Player,
|
|
pub input: InputHandler,
|
|
pub ui: UiLayer,
|
|
pub cam_x: i32,
|
|
pub cam_y: i32,
|
|
pub cam_offset_x: i32,
|
|
pub cam_offset_y: i32,
|
|
pub running: bool,
|
|
pub tick: u64,
|
|
pub fixed_dt: Duration,
|
|
pub accumulator: Duration,
|
|
pub last_time: Instant,
|
|
pub last_shot_tick: u64,
|
|
pub shot_cooldown: u64,
|
|
pub fireball_mode: bool,
|
|
pub corpse_decomp_timer: u64,
|
|
pub kills: u32,
|
|
pub score: u32,
|
|
pub depth: u32,
|
|
pub fps: f32,
|
|
}
|
|
|
|
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,
|
|
projectiles: ProjectileManager::new(),
|
|
items: ItemManager::new(),
|
|
player,
|
|
input: InputHandler::new(),
|
|
ui: UiLayer::new(),
|
|
cam_x: 100,
|
|
cam_y: 100,
|
|
cam_offset_x: 0,
|
|
cam_offset_y: 0,
|
|
running: true,
|
|
tick: 0,
|
|
fixed_dt: Duration::from_millis(16),
|
|
accumulator: Duration::ZERO,
|
|
last_time: Instant::now(),
|
|
last_shot_tick: 0,
|
|
shot_cooldown: 8,
|
|
fireball_mode: false,
|
|
corpse_decomp_timer: 0,
|
|
kills: 0,
|
|
score: 0,
|
|
depth: 1,
|
|
fps: 0.0,
|
|
}
|
|
}
|
|
|
|
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 stair_y = (h as i32 - 2).max(surface_y + 2);
|
|
self.grid
|
|
.set_material(surface_x, stair_y, MaterialId::Stairs);
|
|
|
|
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);
|
|
|
|
self.items
|
|
.spawn(ItemType::Sword, px as i32 - 6, py as i32 + 1);
|
|
self.items
|
|
.spawn(ItemType::HealthPotion, px as i32 + 6, py as i32 + 1);
|
|
self.items
|
|
.spawn(ItemType::LeatherArmor, px as i32 - 3, py as i32 - 8);
|
|
}
|
|
|
|
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();
|
|
let mut frame_count = 0u32;
|
|
let mut frame_time_acc = Duration::ZERO;
|
|
let mut last_fps_print = 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;
|
|
frame_time_acc += frame_time;
|
|
frame_count += 1;
|
|
if last_fps_print.elapsed() >= Duration::from_secs(1) {
|
|
let avg_ms = frame_time_acc.as_secs_f32() * 1000.0 / frame_count as f32;
|
|
self.fps = 1000.0 / avg_ms;
|
|
frame_count = 0;
|
|
frame_time_acc = Duration::ZERO;
|
|
last_fps_print = Instant::now();
|
|
}
|
|
|
|
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_offset_x;
|
|
self.cam_y = py as i32 - (vh as i32 / 2) + self.cam_offset_y;
|
|
|
|
self.build_ui(vw, vh);
|
|
|
|
let light = lighting::compute_lighting(
|
|
&self.grid,
|
|
self.cam_x,
|
|
self.cam_y,
|
|
vw,
|
|
vh,
|
|
lighting::ambient_light(),
|
|
);
|
|
|
|
if let Err(e) = renderer.render(
|
|
&self.grid,
|
|
&self.entities,
|
|
&self.items,
|
|
&self.ui,
|
|
self.cam_x,
|
|
self.cam_y,
|
|
Some(&light),
|
|
) {
|
|
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::ShootLeft => self.player_shoot(-1.0, 0.0),
|
|
Action::ShootRight => self.player_shoot(1.0, 0.0),
|
|
Action::ShootUp => self.player_shoot(0.0, -1.0),
|
|
Action::ShootDown => self.player_shoot(0.0, 1.0),
|
|
Action::ToggleFireball => self.fireball_mode = !self.fireball_mode,
|
|
Action::Descend => self.descend(),
|
|
Action::UseItem => self.use_item(0),
|
|
Action::DropItem => self.drop_item(0),
|
|
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);
|
|
}
|
|
|
|
let mut held = held;
|
|
for action in &mut 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,
|
|
Action::ShootLeft => self.player_shoot(-1.0, 0.0),
|
|
Action::ShootRight => self.player_shoot(1.0, 0.0),
|
|
Action::ShootUp => self.player_shoot(0.0, -1.0),
|
|
Action::ShootDown => self.player_shoot(0.0, 1.0),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn build_ui(&mut self, vw: usize, vh: usize) {
|
|
self.ui.clear();
|
|
self.ui.tick_messages();
|
|
|
|
let player = self.player.entity(&self.entities).cloned();
|
|
let brush = self
|
|
.input
|
|
.paint_brush
|
|
.to_material()
|
|
.unwrap_or(crate::world::cell::MaterialId::Empty);
|
|
let player_alive = player.as_ref().map(|e| e.alive).unwrap_or(false);
|
|
let ui_w = (vw as i32) * crate::ui::UI_SCALE;
|
|
let ui_h = (vh as i32) * crate::ui::UI_SCALE;
|
|
self.ui
|
|
.draw_character_panel(1, 1, player.as_ref(), &self.player);
|
|
self.ui.draw_hud(
|
|
ui_w as usize,
|
|
ui_h as usize,
|
|
player.as_ref(),
|
|
self.tick,
|
|
brush,
|
|
self.kills,
|
|
self.score,
|
|
self.depth,
|
|
&self.player,
|
|
self.fps,
|
|
);
|
|
let msg_x = (ui_w / 2) - 24;
|
|
self.ui.draw_messages(msg_x, 4);
|
|
self.ui.draw_damage_numbers(self.cam_x, self.cam_y);
|
|
self.ui.draw_edge_indicators(
|
|
ui_w as usize,
|
|
ui_h as usize,
|
|
self.entities.all(),
|
|
self.cam_x,
|
|
self.cam_y,
|
|
);
|
|
self.ui
|
|
.draw_entity_labels(self.entities.all(), self.cam_x, self.cam_y);
|
|
self.ui
|
|
.draw_status_icons(self.entities.all(), self.cam_x, self.cam_y);
|
|
self.ui.draw_minimap(
|
|
ui_w as usize,
|
|
ui_h as usize,
|
|
&self.grid,
|
|
self.entities.all(),
|
|
self.cam_x,
|
|
self.cam_y,
|
|
);
|
|
if !player_alive {
|
|
self.ui
|
|
.draw_death_screen(ui_w as usize, ui_h as usize, self.kills, self.score);
|
|
}
|
|
|
|
for e in self.entities.all() {
|
|
if !e.alive || e.kind == EntityKind::Corpse {
|
|
continue;
|
|
}
|
|
let (sx, sy) = crate::ui::entity_screen_pos_ui(e, self.cam_x, self.cam_y);
|
|
let top = sy - (e.half_h as i32 * crate::ui::UI_SCALE) - 2;
|
|
self.ui
|
|
.draw_health_bar(sx - 6, top, e.health, e.max_health, 12);
|
|
}
|
|
|
|
for p in self.projectiles.all() {
|
|
let sx = (p.x as i32 - self.cam_x) * crate::ui::UI_SCALE;
|
|
let sy = (p.y as i32 - self.cam_y) * crate::ui::UI_SCALE;
|
|
if sx >= 0 && sx < ui_w && sy >= 0 && sy < ui_h {
|
|
self.ui
|
|
.set(sx, sy, p.draw_char(), p.draw_color(), [0, 0, 0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
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.update_active_chunks();
|
|
self.ca.step(&mut self.grid);
|
|
|
|
self.update_entities();
|
|
self.update_slime_ai();
|
|
self.update_goblin_ai();
|
|
self.update_combat();
|
|
self.update_projectiles();
|
|
self.apply_world_damage();
|
|
self.decompose_corpses();
|
|
self.update_status_effects();
|
|
self.update_score();
|
|
self.update_item_pickup();
|
|
|
|
if self.tick % 30 == 0 {
|
|
self.try_spawn_goblin();
|
|
}
|
|
if self.tick % 45 == 0 {
|
|
self.try_spawn_slime();
|
|
}
|
|
|
|
self.grid.swap_modified_flags();
|
|
}
|
|
|
|
fn update_score(&mut self) {
|
|
let mut new_kills = 0;
|
|
for e in self.entities.all_mut() {
|
|
if !e.alive && e.kind == EntityKind::Corpse && !e.counted_for_score {
|
|
e.counted_for_score = true;
|
|
new_kills += 1;
|
|
}
|
|
}
|
|
if new_kills > 0 {
|
|
self.kills += new_kills;
|
|
self.score += new_kills * 10;
|
|
if let Some(player) = self.player.entity_mut(&mut self.entities) {
|
|
player.add_xp(new_kills * 25);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_status_effects(&mut self) {
|
|
for e in self.entities.all_mut() {
|
|
if e.alive {
|
|
e.apply_status_effects();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_item_pickup(&mut self) {
|
|
let (px, py) = self.player.center(&self.entities);
|
|
let ix = px as i32;
|
|
let iy = py as i32;
|
|
let mut picked = Vec::new();
|
|
for (idx, item) in self.items.all().iter().enumerate() {
|
|
if (item.x - ix).abs() <= 1 && (item.y - iy).abs() <= 1 {
|
|
picked.push(idx);
|
|
}
|
|
}
|
|
for idx in picked.into_iter().rev() {
|
|
let item = self.items.all_mut().remove(idx);
|
|
self.ui.add_message(&format!("Picked up {}", item.name()));
|
|
self.player.inventory.push(item);
|
|
}
|
|
}
|
|
|
|
pub fn descend(&mut self) {
|
|
let can_descend = match self.player.entity(&self.entities) {
|
|
Some(e) => {
|
|
let foot_x = e.cx as i32;
|
|
let foot_y = (e.cy + e.half_h).ceil() as i32;
|
|
self.grid.get(foot_x, foot_y).material == MaterialId::Stairs
|
|
}
|
|
None => false,
|
|
};
|
|
if !can_descend {
|
|
return;
|
|
}
|
|
self.depth += 1;
|
|
self.grid = Grid::new();
|
|
self.entities = EntityManager::new();
|
|
self.player = Player::new(&mut self.entities);
|
|
self.projectiles = ProjectileManager::new();
|
|
self.items = ItemManager::new();
|
|
self.corpse_decomp_timer = 0;
|
|
self.init_world();
|
|
self.ui
|
|
.add_message(&format!("Descended to depth {}", self.depth));
|
|
}
|
|
|
|
pub fn use_item(&mut self, index: usize) {
|
|
if index >= self.player.inventory.len() {
|
|
return;
|
|
}
|
|
let item = self.player.inventory[index].clone();
|
|
let name = item.name();
|
|
if item.is_weapon() {
|
|
if let Some(old) = self.player.weapon.take() {
|
|
self.player.inventory.push(old);
|
|
}
|
|
self.player.weapon = Some(item);
|
|
self.player.inventory.remove(index);
|
|
self.ui.add_message(&format!("Equipped {}", name));
|
|
} else if item.is_armor() {
|
|
if let Some(old) = self.player.armor.take() {
|
|
self.player.inventory.push(old);
|
|
}
|
|
self.player.armor = Some(item);
|
|
self.player.inventory.remove(index);
|
|
self.ui.add_message(&format!("Equipped {}", name));
|
|
} else if item.is_consumable() {
|
|
let heal = item.heal_amount();
|
|
if let Some(p) = self.player.entity_mut(&mut self.entities) {
|
|
p.health = (p.health + heal).min(p.max_health);
|
|
}
|
|
self.player.inventory.remove(index);
|
|
self.ui.add_message(&format!("Consumed {}", name));
|
|
}
|
|
}
|
|
|
|
pub fn drop_item(&mut self, index: usize) {
|
|
if index >= self.player.inventory.len() {
|
|
return;
|
|
}
|
|
let item = self.player.inventory.remove(index);
|
|
let (px, py) = self.player.center(&self.entities);
|
|
self.items.spawn(item.typ, px as i32, py as i32 + 1);
|
|
self.ui.add_message(&format!("Dropped {}", item.name()));
|
|
}
|
|
|
|
fn update_active_chunks(&mut self) {
|
|
self.grid.deactivate_all();
|
|
|
|
for e in self.entities.all() {
|
|
let (cx, cy) = e.center();
|
|
self.grid.activate_around(cx as i32, cy as i32, 2);
|
|
}
|
|
|
|
for p in self.projectiles.all() {
|
|
self.grid.activate_around(p.x as i32, p.y as i32, 1);
|
|
}
|
|
|
|
for item in self.items.all() {
|
|
self.grid.activate_around(item.x, item.y, 1);
|
|
}
|
|
|
|
let chunk_size = self.grid.chunk_size as i32;
|
|
for cy in 0..self.grid.chunks_y as i32 {
|
|
for cx in 0..self.grid.chunks_x as i32 {
|
|
let idx = self.grid.chunk_index(cx, cy);
|
|
if self.grid.chunks[idx].modified || self.grid.chunks[idx].was_modified {
|
|
self.grid
|
|
.activate_around(cx * chunk_size, cy * chunk_size, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_projectiles(&mut self) {
|
|
self.projectiles.update(&self.grid);
|
|
self.projectiles
|
|
.resolve_hits(&mut self.grid, self.entities.all_mut(), &mut self.ui);
|
|
self.projectiles.cull_dead();
|
|
}
|
|
|
|
pub fn player_shoot(&mut self, dir_x: f32, dir_y: f32) {
|
|
if self.tick > self.last_shot_tick && self.tick - self.last_shot_tick < self.shot_cooldown {
|
|
return;
|
|
}
|
|
let (px, py) = self.player.center(&self.entities);
|
|
let owner = self.player.entity_id;
|
|
let speed = if self.fireball_mode { 2.2 } else { 3.0 };
|
|
let vx = dir_x * speed;
|
|
let vy = dir_y * speed;
|
|
let typ = if self.fireball_mode {
|
|
ProjectileType::Fireball
|
|
} else if self.tick % 4 == 0 {
|
|
ProjectileType::MagicBolt
|
|
} else {
|
|
ProjectileType::Arrow
|
|
};
|
|
let spawn_x = px + dir_x * 3.0;
|
|
let spawn_y = py + dir_y * 3.0;
|
|
let damage_bonus = self
|
|
.player
|
|
.weapon
|
|
.as_ref()
|
|
.map(|w| w.damage_bonus())
|
|
.unwrap_or(0.0);
|
|
self.projectiles
|
|
.spawn(typ, spawn_x, spawn_y, vx, vy, owner, damage_bonus);
|
|
self.last_shot_tick = self.tick;
|
|
}
|
|
|
|
fn update_goblin_ai(&mut self) {
|
|
let (px, py) = self.player.center(&self.entities);
|
|
let goblin_data: Vec<(usize, f32, f32, f32)> = self
|
|
.entities
|
|
.all()
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, e)| e.alive && e.kind == EntityKind::Goblin)
|
|
.map(|(i, e)| (i, e.cx, e.cy, e.health))
|
|
.collect();
|
|
|
|
for (idx, gx, gy, health) in goblin_data {
|
|
if !self.entities.all()[idx].rigid {
|
|
continue;
|
|
}
|
|
let dx = px - gx;
|
|
let dy = py - gy;
|
|
let dist_sq = dx * dx + dy * dy;
|
|
let dist = dist_sq.sqrt();
|
|
if dist < 0.5 {
|
|
continue;
|
|
}
|
|
let dir_x = dx / dist;
|
|
let _dir_y = dy / dist;
|
|
if health < 10.0 {
|
|
let flee = self.entities.all()[idx].cx < px;
|
|
let move_dir = if flee { -1.0 } else { 1.0 };
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.set_horizontal_vel(move_dir * 0.6);
|
|
}
|
|
} else if dist > 6.0 {
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.set_horizontal_vel(dir_x * 0.45);
|
|
}
|
|
} else if dist < 4.0 {
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.set_horizontal_vel(-dir_x * 0.45);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn decompose_corpses(&mut self) {
|
|
self.corpse_decomp_timer += 1;
|
|
if self.corpse_decomp_timer < 40 {
|
|
return;
|
|
}
|
|
self.corpse_decomp_timer = 0;
|
|
let mut drops: Vec<(i32, i32, usize, usize)> = Vec::new();
|
|
for (idx, e) in self.entities.all().iter().enumerate() {
|
|
if e.alive || e.kind != EntityKind::Corpse {
|
|
continue;
|
|
}
|
|
if e.bodies.is_empty() {
|
|
continue;
|
|
}
|
|
let alive_indices: Vec<usize> = e
|
|
.bodies
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, b)| b.alive)
|
|
.map(|(i, _)| i)
|
|
.collect();
|
|
if alive_indices.is_empty() {
|
|
continue;
|
|
}
|
|
let pick = self.ca.random_usize(alive_indices.len());
|
|
let bi = alive_indices[pick];
|
|
let b = &e.bodies[bi];
|
|
let gx = b.x.floor() as i32;
|
|
let gy = b.y.floor() as i32;
|
|
if self.grid.in_bounds(gx, gy) && self.grid.get(gx, gy).is_empty() {
|
|
drops.push((gx, gy, idx, bi));
|
|
}
|
|
}
|
|
for (gx, gy, eidx, bidx) in drops {
|
|
self.grid.set_material(gx, gy, MaterialId::Flesh);
|
|
if let Some(e) = self.entities.all_mut().get_mut(eidx) {
|
|
if let Some(b) = e.bodies.get_mut(bidx) {
|
|
b.alive = false;
|
|
b.health = 0.0;
|
|
}
|
|
let alive_count = e.bodies.iter().filter(|b| b.alive).count();
|
|
if alive_count == 0 {
|
|
e.health = 0.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_slime_ai(&mut self) {
|
|
let (px, py) = self.player.center(&self.entities);
|
|
let tick = self.tick;
|
|
|
|
let slime_data: Vec<(usize, f32, f32, bool, f32)> = self
|
|
.entities
|
|
.all()
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, e)| e.alive && e.kind == EntityKind::Slime)
|
|
.map(|(i, e)| (i, e.cx, e.cy, e.rigid, e.health))
|
|
.collect();
|
|
|
|
for (idx, sx, sy, rigid, _health) in slime_data {
|
|
if !rigid {
|
|
continue;
|
|
}
|
|
let dx = px - sx;
|
|
let dy = py - sy;
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
if dist < 0.5 {
|
|
continue;
|
|
}
|
|
|
|
let jump_phase = tick % 60;
|
|
if jump_phase == 0 && dist < 40.0 {
|
|
let dir_x = dx / dist;
|
|
let dir_y = dy / dist;
|
|
let jump_power = 0.8 + (1.0 - dist / 40.0).min(0.5) * 0.5;
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.set_horizontal_vel(dir_x * jump_power);
|
|
let vy = if dy < -1.0 {
|
|
-jump_power * 0.8
|
|
} else {
|
|
-jump_power * 0.6
|
|
};
|
|
e.set_vertical_vel(vy + dir_y * jump_power * 0.3);
|
|
}
|
|
} else if jump_phase == 30 {
|
|
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
|
e.set_horizontal_vel(0.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_combat(&mut self) {
|
|
let player_id = self.player.entity_id;
|
|
let player_alive = self
|
|
.entities
|
|
.get(player_id)
|
|
.map(|e| e.alive)
|
|
.unwrap_or(false);
|
|
if !player_alive {
|
|
return;
|
|
}
|
|
let player_center = self.player.center(&self.entities);
|
|
let player_half_w = self
|
|
.entities
|
|
.get(player_id)
|
|
.map(|e| e.half_w)
|
|
.unwrap_or(3.0);
|
|
let player_half_h = self
|
|
.entities
|
|
.get(player_id)
|
|
.map(|e| e.half_h)
|
|
.unwrap_or(6.0);
|
|
|
|
let enemy_data: Vec<(usize, EntityKind, f32, f32, f32, f32)> = self
|
|
.entities
|
|
.all()
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, e)| {
|
|
e.alive && e.kind != EntityKind::Player && e.kind != EntityKind::Corpse
|
|
})
|
|
.map(|(i, e)| (i, e.kind, e.cx, e.cy, e.half_w, e.half_h))
|
|
.collect();
|
|
|
|
for (_idx, kind, ex, ey, ew, eh) in enemy_data {
|
|
let dx = (ex - player_center.0).abs();
|
|
let dy = (ey - player_center.1).abs();
|
|
if dx < ew + player_half_w && dy < eh + player_half_h {
|
|
if self.tick % 20 == 0 {
|
|
let base = match kind {
|
|
EntityKind::Goblin => 8.0,
|
|
EntityKind::Slime => 5.0,
|
|
_ => 0.0,
|
|
};
|
|
let armor = self
|
|
.player
|
|
.armor
|
|
.as_ref()
|
|
.map(|a| a.armor_bonus())
|
|
.unwrap_or(0.0);
|
|
let damage = (base - armor).max(0.0);
|
|
if damage > 0.0 {
|
|
let player_before = self
|
|
.entities
|
|
.get(player_id)
|
|
.map(|e| e.health)
|
|
.unwrap_or(0.0);
|
|
if let Some(p) = self.entities.get_mut(player_id) {
|
|
p.take_damage(damage);
|
|
}
|
|
let player_after = self
|
|
.entities
|
|
.get(player_id)
|
|
.map(|e| e.health)
|
|
.unwrap_or(0.0);
|
|
self.ui.add_damage_number(
|
|
player_center.0,
|
|
player_center.1 - player_half_h - 2.0,
|
|
&format!("-{:.0}", player_before - player_after),
|
|
);
|
|
let knockback_dir = if player_center.0 < ex { -1.0 } else { 1.0 };
|
|
if let Some(p) = self.entities.get_mut(player_id) {
|
|
p.set_horizontal_vel(knockback_dir * 0.5);
|
|
}
|
|
let msg = match kind {
|
|
EntityKind::Goblin => "Goblin hits you!",
|
|
EntityKind::Slime => "Slime burns you!",
|
|
_ => "Enemy hits you!",
|
|
};
|
|
self.ui.add_message(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
e.health -= 1.0;
|
|
for b in &mut e.bodies {
|
|
if b.alive {
|
|
b.health -= 1.0;
|
|
if !b.on_fire {
|
|
b.on_fire = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if touching_fire {
|
|
e.health -= 0.15;
|
|
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 {
|
|
e.health -= 0.25;
|
|
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 e = match self.entities.all_mut().get_mut(idx) {
|
|
Some(e) => e,
|
|
None => return,
|
|
};
|
|
let alive = e.alive;
|
|
let bodies = &mut e.bodies;
|
|
let constraints = &e.constraints;
|
|
|
|
let effective_substeps = if alive { substeps } else { 1 };
|
|
let constraint_iters = if alive { 4 } else { 1 };
|
|
|
|
for b in bodies.iter_mut() {
|
|
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..effective_substeps {
|
|
solver.integrate(bodies);
|
|
|
|
for b in bodies.iter_mut() {
|
|
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..constraint_iters {
|
|
solver.solve_constraints(bodies, constraints, 1);
|
|
for b in bodies.iter_mut() {
|
|
if !b.alive {
|
|
continue;
|
|
}
|
|
resolve_grid_collision(grid, b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
fn try_spawn_slime(&mut self) {
|
|
let alive_slimes = self
|
|
.entities
|
|
.all()
|
|
.iter()
|
|
.filter(|e| e.alive && e.kind == EntityKind::Slime)
|
|
.count();
|
|
if alive_slimes >= 2 {
|
|
return;
|
|
}
|
|
|
|
let (px, _py) = self.player.center(&self.entities);
|
|
let spawn_x = px as i32 + if px as i32 % 2 == 0 { -18 } else { 18 };
|
|
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 - 3;
|
|
|
|
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
|
return;
|
|
}
|
|
|
|
let id = self.entities.spawn(EntityKind::Slime);
|
|
if let Some(s) = self.entities.get_mut(id) {
|
|
s.build_humanoid(spawn_x as f32, spawn_y as f32);
|
|
}
|
|
}
|
|
}
|