Add bump-to-attack, death and a sim event queue
A step onto an occupied tile is now an attack: the geometry rule is checked against terrain only and the target tile decides between move and bump. Entities carry an attack stat; at zero hp they are removed. The sim reports Attacked/Died through an event queue the game drains after every movement window — the seam for sound, log, camera effects and spawns. Click-to-move treats an occupied goal as a target but detours around entities in the way; the player's death ends the game. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+50
-6
@@ -7,7 +7,7 @@ use crate::assets::{Image, Tile};
|
||||
use crate::input::{GameAction, InputState};
|
||||
use sim::entity::{Entity, EntityKind};
|
||||
use sim::map::TileMap;
|
||||
use sim::{delta_action, load_world, step_allowed, Sim, TICK_HZ, TICKS_PER_MOVE};
|
||||
use sim::{delta_action, load_world, step_allowed, Event, Sim, TICK_HZ, TICKS_PER_MOVE};
|
||||
|
||||
pub enum GameSignal {
|
||||
Quit,
|
||||
@@ -87,8 +87,8 @@ impl Game {
|
||||
println!("loaded {map_path}: {}×{} tiles in {:.1} ms",
|
||||
map.width, map.height, t0.elapsed().as_secs_f64() * 1e3);
|
||||
let mut sim = Sim::new(world);
|
||||
let player_id = sim.world.spawn_entity(EntityKind::Player, 0, (0, 0), 100);
|
||||
sim.world.spawn_entity(EntityKind::Npc, 1, (6, 6), 5);
|
||||
let player_id = sim.world.spawn_entity(EntityKind::Player, 0, (0, 0), 100, 3);
|
||||
sim.world.spawn_entity(EntityKind::Npc, 1, (6, 6), 5, 1);
|
||||
|
||||
let mut game = Game {
|
||||
tileset,
|
||||
@@ -127,6 +127,9 @@ impl Game {
|
||||
while self.tick_accum_ms >= TICK_MS {
|
||||
self.tick_accum_ms -= TICK_MS;
|
||||
if self.sim.step() {
|
||||
if self.handle_events() {
|
||||
return Some(GameSignal::Quit);
|
||||
}
|
||||
self.track_lerp();
|
||||
self.sync_route();
|
||||
self.step_movement(input);
|
||||
@@ -144,6 +147,27 @@ impl Game {
|
||||
None
|
||||
}
|
||||
|
||||
/// React to what the sim reported for the last window. Returns `true` once the
|
||||
/// player is dead — the one event the game cannot continue past.
|
||||
fn handle_events(&mut self) -> bool {
|
||||
let mut dead = false;
|
||||
for ev in self.sim.take_events() {
|
||||
match ev {
|
||||
Event::Attacked { by, target, damage } => {
|
||||
println!("#{by} hits #{target} for {damage}");
|
||||
}
|
||||
Event::Died { id, at } => {
|
||||
println!("#{id} dies at {at:?}");
|
||||
dead |= id == self.player_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
if dead {
|
||||
println!("You died.");
|
||||
}
|
||||
dead
|
||||
}
|
||||
|
||||
/// Where the camera wants to be: the viewport centered on the player's rendered
|
||||
/// (interpolated) position, not the coarser sim tile — otherwise the target itself
|
||||
/// jumps in discrete per-tile steps and the easing below has nothing smooth to chase.
|
||||
@@ -196,7 +220,7 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
|
||||
match pathfind::find_path(self.player_pos(), goal, |x, y| self.sim.world.blocked(x, y)) {
|
||||
match self.plan(goal) {
|
||||
Some(steps) => {
|
||||
self.route = steps.into();
|
||||
self.goal = Some(goal);
|
||||
@@ -241,7 +265,15 @@ impl Game {
|
||||
};
|
||||
let Some((nx, ny)) = step else { return };
|
||||
|
||||
if step_allowed(from, (nx, ny), |x, y| self.sim.world.blocked(x, y)) {
|
||||
// A keyboard step or the route's final step may bump into an entity (the sim
|
||||
// turns that into an attack); an entity blocking the route midway is walked
|
||||
// around instead.
|
||||
let may_bump = dir.is_some() || self.goal == Some((nx, ny));
|
||||
let legal = step_allowed(from, (nx, ny), |x, y| {
|
||||
if may_bump && (x, y) == (nx, ny) { self.sim.world.solid(x, y) }
|
||||
else { self.sim.world.blocked(x, y) }
|
||||
});
|
||||
if legal {
|
||||
self.sim.set_action(self.player_id, delta_action(nx - from.0, ny - from.1));
|
||||
} else if dir.is_none() {
|
||||
self.replan_route();
|
||||
@@ -271,12 +303,24 @@ impl Game {
|
||||
self.goal = None;
|
||||
return;
|
||||
}
|
||||
match pathfind::find_path(pos, goal, |x, y| self.sim.world.blocked(x, y)) {
|
||||
match self.plan(goal) {
|
||||
Some(steps) => self.route = steps.into(),
|
||||
None => self.goal = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Route from the player to `goal`. An entity standing on the goal does not make
|
||||
/// it unreachable — the route ends with a bump into it — but entities along the
|
||||
/// way are walked around.
|
||||
fn plan(&self, goal: (i32, i32)) -> Option<Vec<(i32, i32)>> {
|
||||
if self.sim.world.solid(goal.0, goal.1) {
|
||||
return None;
|
||||
}
|
||||
pathfind::find_path(self.player_pos(), goal, |x, y| {
|
||||
(x, y) != goal && self.sim.world.blocked(x, y)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fold the sim's entity positions into the interpolation table: a changed position
|
||||
/// starts a new lerp from the old one, a jump of more than one tile (Chebyshev — a
|
||||
/// teleport) snaps, and vanished entities are dropped.
|
||||
|
||||
Reference in New Issue
Block a user