walk units around the river instead of through it

units now path. A* over the tilemap grid, water costing 800 against 1
for ground so the route runs to a bridge, or 20 when the unit flies and
crosses anywhere. bit 5 of a map cell is the water flag, which is what
makes 48 water and leaves the bridge cells - carrying the lane ids 1 and
2 - dry. the unit walks to the next node rather than at its target, and
still stops at Range.

also stops pushing snapshots while the battle type is 1. the client is
simulating the same battle itself there, and our state does not match it
yet, so every push yanked the units back to where the server thought
they were. the snapshots are still built and verified each second, ready
for the switch to type 0 once the two simulations agree; they are simply
not sent.
This commit is contained in:
WiseDev 2026-08-23 13:23:58 +03:00
parent ba235a9b4c
commit 9084163f71
7 changed files with 168 additions and 9 deletions

View file

@ -302,6 +302,7 @@ impl BattleBuilder {
.unwrap_or_default(), .unwrap_or_default(),
]; ];
let battle = LogicBattle { let battle = LogicBattle {
tilemap: Some(tilemap),
location, location,
npc, npc,
arena, arena,

View file

@ -2,7 +2,9 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20; pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20;
use std::time::Instant; use std::time::Instant;
use logic::battle::{LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND}; use logic::battle::{
LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
};
use logic::battle::{verify_snapshot, LogicBattleEvent}; use logic::battle::{verify_snapshot, LogicBattleEvent};
use logic::SectorStateMessage; use logic::SectorStateMessage;
use logic::{BattleEventMessage, LogicDataRef, LogicRandom}; use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
@ -83,6 +85,9 @@ impl BattleSession {
.unwrap_or(king_x); .unwrap_or(king_x);
Some((card, LogicVector2::new(x, ahead), self.next_instance())) Some((card, LogicVector2::new(x, ahead), self.next_instance()))
} }
pub fn pushes_snapshots(&self) -> bool {
self.mode.battle.battle_type == BATTLE_TYPE_PVP
}
fn snapshot_message(&mut self) -> Option<WireMessage> { fn snapshot_message(&mut self) -> Option<WireMessage> {
let snapshot = self.mode.snapshot().ok()?; let snapshot = self.mode.snapshot().ok()?;
let report = verify_snapshot(&snapshot); let report = verify_snapshot(&snapshot);
@ -159,7 +164,7 @@ impl BattleSession {
self.tick += 1; self.tick += 1;
self.release_queued(self.tick); self.release_queued(self.tick);
self.mode.battle.tick(); self.mode.battle.tick();
if self.tick >= self.next_snapshot { if self.pushes_snapshots() && self.tick >= self.next_snapshot {
self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS; self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS;
if let Some(message) = self.snapshot_message() { if let Some(message) = self.snapshot_message() {
self.outbound.push(message); self.outbound.push(message);

View file

@ -39,6 +39,7 @@ pub struct LogicBattle {
pub winner_score_change: i32, pub winner_score_change: i32,
pub loser_score_change: i32, pub loser_score_change: i32,
pub trailing: [i32; BATTLE_TRAILING_INTS], pub trailing: [i32; BATTLE_TRAILING_INTS],
pub tilemap: Option<crate::battle::LogicTilemap>,
} }
impl Default for LogicBattle { impl Default for LogicBattle {
fn default() -> Self { fn default() -> Self {
@ -66,6 +67,7 @@ impl Default for LogicBattle {
winner_score_change: 0, winner_score_change: 0,
loser_score_change: 0, loser_score_change: 0,
trailing: [0; BATTLE_TRAILING_INTS], trailing: [0; BATTLE_TRAILING_INTS],
tilemap: None,
} }
} }
} }
@ -257,6 +259,7 @@ impl Payload for LogicBattle {
winner_score_change, winner_score_change,
loser_score_change, loser_score_change,
trailing, trailing,
tilemap: None,
}) })
} }
} }

View file

@ -0,0 +1,116 @@
use std::collections::BinaryHeap;
use crate::battle::logic_tilemap::{
LogicTilemap, GROUND_COST, SUBTILE_UNITS, WATER_COST, WATER_COST_JUMPING,
};
pub const PATH_STEP_LIMIT: usize = 4096;
fn tile_of(units: i32) -> i32 {
units.div_euclid(SUBTILE_UNITS)
}
fn centre_of(tile: i32) -> i32 {
tile * SUBTILE_UNITS + SUBTILE_UNITS / 2
}
#[derive(PartialEq, Eq)]
struct Step {
estimate: i32,
cost: i32,
index: usize,
}
impl Ord for Step {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.estimate
.cmp(&self.estimate)
.then_with(|| other.index.cmp(&self.index))
}
}
impl PartialOrd for Step {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub fn find_path(
tilemap: &LogicTilemap,
from: (i32, i32),
to: (i32, i32),
jumps: bool,
) -> Vec<(i32, i32)> {
let start = (tile_of(from.0), tile_of(from.1));
let goal = (tile_of(to.0), tile_of(to.1));
if start == goal || !tilemap.is_inside(goal.0, goal.1) {
return Vec::new();
}
let width = tilemap.width().max(1) as usize;
let height = tilemap.height().max(1) as usize;
let index_of = |x: i32, y: i32| y as usize * width + x as usize;
let mut cost = vec![i32::MAX; width * height];
let mut came_from = vec![usize::MAX; width * height];
let mut open = BinaryHeap::new();
let start_index = index_of(start.0, start.1);
cost[start_index] = 0;
open.push(Step {
estimate: 0,
cost: 0,
index: start_index,
});
let goal_index = index_of(goal.0, goal.1);
let mut visited = 0;
while let Some(step) = open.pop() {
if step.index == goal_index {
break;
}
if step.cost > cost[step.index] {
continue;
}
visited += 1;
if visited > PATH_STEP_LIMIT {
break;
}
let x = (step.index % width) as i32;
let y = (step.index / width) as i32;
for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
let (nx, ny) = (x + dx, y + dy);
if !tilemap.is_inside(nx, ny) {
continue;
}
let water = tilemap.is_water(nx, ny);
let step_cost = if water {
if jumps {
WATER_COST_JUMPING
} else {
WATER_COST
}
} else {
GROUND_COST
};
let next = index_of(nx, ny);
let total = step.cost.saturating_add(step_cost);
if total >= cost[next] {
continue;
}
cost[next] = total;
came_from[next] = step.index;
let heuristic = (goal.0 - nx).abs() + (goal.1 - ny).abs();
open.push(Step {
estimate: total.saturating_add(heuristic),
cost: total,
index: next,
});
}
}
if came_from[goal_index] == usize::MAX {
return Vec::new();
}
let mut path = Vec::new();
let mut cursor = goal_index;
while cursor != start_index {
let x = (cursor % width) as i32;
let y = (cursor / width) as i32;
path.push((centre_of(x), centre_of(y)));
cursor = came_from[cursor];
if cursor == usize::MAX {
return Vec::new();
}
}
path.reverse();
path
}

View file

@ -210,16 +210,28 @@ impl LogicBattle {
}; };
let from = positions[index]; let from = positions[index];
let stand_off = stats.range; let stand_off = stats.range;
let dx = (goal.0 - from.0) as i64; let straight = integer_sqrt(
let dy = (goal.1 - from.1) as i64; ((goal.0 - from.0) as i64).pow(2) + ((goal.1 - from.1) as i64).pow(2),
let distance = integer_sqrt(dx * dx + dy * dy); );
if distance <= stand_off as i64 || distance == 0 { if straight <= stand_off as i64 || straight == 0 {
continue; continue;
} }
let step = (stats.speed as i64).min(distance - stand_off as i64); let waypoint = self
.tilemap
.as_ref()
.map(|tilemap| crate::battle::find_path(tilemap, from, goal, stats.flying))
.and_then(|path| path.first().copied())
.unwrap_or(goal);
let dx = (waypoint.0 - from.0) as i64;
let dy = (waypoint.1 - from.1) as i64;
let leg = integer_sqrt(dx * dx + dy * dy);
if leg == 0 {
continue;
}
let step = (stats.speed as i64).min(straight - stand_off as i64).min(leg);
let base = self.objects.objects[index].body.base_mut(); let base = self.objects.objects[index].body.base_mut();
base.position.x = from.0 + (dx * step / distance) as i32; base.position.x = from.0 + (dx * step / leg) as i32;
base.position.y = from.1 + (dy * step / distance) as i32; base.position.y = from.1 + (dy * step / leg) as i32;
} }
} }
fn resolve_attacks(&mut self) { fn resolve_attacks(&mut self) {

View file

@ -1,6 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
pub const SUBTILE_UNITS: i32 = 500; pub const SUBTILE_UNITS: i32 = 500;
pub const WATER_BIT: i32 = 5;
pub const WATER_COST: i32 = 800;
pub const WATER_COST_JUMPING: i32 = 20;
pub const GROUND_COST: i32 = 1;
pub const SECTION_OBJECTS: &str = "Objects"; pub const SECTION_OBJECTS: &str = "Objects";
pub const SECTION_MAP: &str = "Map"; pub const SECTION_MAP: &str = "Map";
pub const OBJECT_KING_TOWER: &str = "KingTower"; pub const OBJECT_KING_TOWER: &str = "KingTower";
@ -82,6 +86,22 @@ impl LogicTilemap {
let source = std::fs::read_to_string(&path)?; let source = std::fs::read_to_string(&path)?;
Ok(Self::parse(&source)) Ok(Self::parse(&source))
} }
pub fn tile_at(&self, x: i32, y: i32) -> i32 {
if x < 0 || y < 0 {
return 0;
}
self.tiles
.get(y as usize)
.and_then(|row| row.get(x as usize))
.copied()
.unwrap_or(0)
}
pub fn is_water(&self, x: i32, y: i32) -> bool {
(self.tile_at(x, y) >> WATER_BIT) & 1 == 1
}
pub fn is_inside(&self, x: i32, y: i32) -> bool {
x >= 0 && y >= 0 && x < self.width() && y < self.height()
}
pub fn height(&self) -> i32 { pub fn height(&self) -> i32 {
self.tiles.len() as i32 self.tiles.len() as i32
} }

View file

@ -6,6 +6,7 @@ mod logic_game_mode;
mod logic_game_object; mod logic_game_object;
mod logic_game_object_manager; mod logic_game_object_manager;
mod logic_game_object_ref; mod logic_game_object_ref;
mod logic_pathfinder;
mod logic_simulation; mod logic_simulation;
mod logic_tilemap; mod logic_tilemap;
mod logic_time; mod logic_time;
@ -31,6 +32,7 @@ pub use logic_game_object::{
}; };
pub use logic_game_object_manager::LogicGameObjectManager; pub use logic_game_object_manager::LogicGameObjectManager;
pub use logic_game_object_ref::LogicGameObjectRef; pub use logic_game_object_ref::LogicGameObjectRef;
pub use logic_pathfinder::find_path;
pub use logic_simulation::{LogicCharacterStats, TICK_MILLISECONDS}; pub use logic_simulation::{LogicCharacterStats, TICK_MILLISECONDS};
pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS}; pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS};
pub use logic_time::LogicTime; pub use logic_time::LogicTime;