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>
143 lines
5.0 KiB
Rust
Executable File
143 lines
5.0 KiB
Rust
Executable File
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
|
||
}
|
||
}
|