Files
forgotten_caves/sim/src/world.rs
T
irrlichtandClaude Opus 5 7ea351f8e5 Add entity blocking and a wandering NPC
Occupied tiles now block movement through World::blocked, the one predicate
the sim and the pathfinder share. Entities carry an EntityKind so the sim
knows which ones to think for; Sim::think gives each NPC a random king move
one window in four. Intents execute in id order so contested tiles resolve
deterministically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 10:51:44 +02:00

143 lines
5.0 KiB
Rust
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashMap;
use crate::chunk::{Chunk, TileFlags};
use crate::entity::{Entity, EntityKind};
pub fn tile_to_chunk(tx: i16, ty: i16) -> (i16, i16) {
(tx >> 5, ty >> 5)
}
pub fn tile_to_local(tx: i16, ty: i16) -> (u8, u8) {
((tx & 31) as u8, (ty & 31) as u8)
}
pub fn local_to_tile(cx: i16, cy: i16, lx: u8, ly: u8) -> (i16, i16) {
((cx << 5) | lx as i16, (cy << 5) | ly as i16)
}
pub struct World {
chunks: HashMap<(i16, i16), Chunk>,
pub entities: HashMap<u32, Entity>,
chunk_entities: HashMap<(i16, i16), Vec<u32>>,
next_entity_id: u32,
}
impl World {
pub fn new() -> Self {
Self {
chunks: HashMap::new(),
entities: HashMap::new(),
chunk_entities: HashMap::new(),
next_entity_id: 1,
}
}
pub fn chunk(&self, cx: i16, cy: i16) -> Option<&Chunk> {
self.chunks.get(&(cx, cy))
}
pub fn set_chunk(&mut self, cx: i16, cy: i16, chunk: Chunk) {
self.chunks.insert((cx, cy), chunk);
}
pub fn tile_flags(&self, wx: i16, wy: i16) -> Option<TileFlags> {
let (cx, cy) = tile_to_chunk(wx, wy);
let (lx, ly) = tile_to_local(wx, wy);
self.chunks.get(&(cx, cy)).map(|c| c.tile_flags(lx, ly))
}
pub fn set_tile(&mut self, wx: i16, wy: i16, id: u16) {
let (cx, cy) = tile_to_chunk(wx, wy);
let (lx, ly) = tile_to_local(wx, wy);
let chunk = self.chunks.entry((cx, cy)).or_insert_with(Chunk::new);
chunk.set_tile(lx, ly, id);
}
pub fn spawn_entity(&mut self, kind: EntityKind, type_id: u16, pos: (i16, i16), hp: u16) -> u32 {
let id = self.next_entity_id;
self.next_entity_id += 1;
let entity = Entity::new(id, kind, type_id, pos, hp);
self.add_entity(entity);
id
}
/// True if any entity stands on `(wx, wy)`. Goes through the chunk index, so it is
/// cheap enough to call per pathfinder expansion.
pub fn occupied(&self, wx: i16, wy: i16) -> bool {
let (cx, cy) = tile_to_chunk(wx, wy);
self.entities_in_chunk(cx, cy).iter()
.any(|id| self.entities[id].pos == (wx, wy))
}
/// The movement blocker predicate: a tile blocks if it is solid, outside the loaded
/// world, or occupied by an entity. Both the sim and the pathfinder plan with this.
pub fn blocked(&self, wx: i32, wy: i32) -> bool {
let Ok(wx) = i16::try_from(wx) else { return true };
let Ok(wy) = i16::try_from(wy) else { return true };
self.tile_flags(wx, wy).map_or(true, |f| f.collidable()) || self.occupied(wx, wy)
}
pub fn add_entity(&mut self, entity: Entity) {
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
let id = entity.id;
self.entities.insert(id, entity);
self.chunk_entities.entry(chunk_key).or_default().push(id);
}
pub fn remove_entity(&mut self, id: u32) {
if let Some(entity) = self.entities.remove(&id) {
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
if let Some(list) = self.chunk_entities.get_mut(&chunk_key) {
list.retain(|&eid| eid != id);
}
}
}
pub fn move_entity(&mut self, id: u32, new_pos: (i16, i16)) {
if let Some(entity) = self.entities.get_mut(&id) {
let old_chunk = tile_to_chunk(entity.pos.0, entity.pos.1);
let new_chunk = tile_to_chunk(new_pos.0, new_pos.1);
entity.pos = new_pos;
if old_chunk != new_chunk {
if let Some(list) = self.chunk_entities.get_mut(&old_chunk) {
list.retain(|&eid| eid != id);
}
self.chunk_entities.entry(new_chunk).or_default().push(id);
}
}
}
pub fn entities_in_chunk(&self, cx: i16, cy: i16) -> &[u32] {
self.chunk_entities.get(&(cx, cy)).map_or(&[], Vec::as_slice)
}
/// Returns all entities within a 30×30 tile viewport centered on `center`.
pub fn entities_in_viewport(&self, center: (i16, i16)) -> Vec<&Entity> {
let half: i16 = 15;
let min_x = center.0 - half;
let max_x = center.0 + half - 1;
let min_y = center.1 - half;
let max_y = center.1 + half - 1;
let min_cx = tile_to_chunk(min_x, min_y).0;
let max_cx = tile_to_chunk(max_x, max_y).0;
let min_cy = tile_to_chunk(min_x, min_y).1;
let max_cy = tile_to_chunk(max_x, max_y).1;
let mut result = Vec::new();
for cy in min_cy..=max_cy {
for cx in min_cx..=max_cx {
for &eid in self.entities_in_chunk(cx, cy) {
if let Some(e) = self.entities.get(&eid) {
if e.pos.0 >= min_x && e.pos.0 <= max_x
&& e.pos.1 >= min_y && e.pos.1 <= max_y
{
result.push(e);
}
}
}
}
}
result
}
}