From 9084163f71f5f4d713304cd8f7362f042fd0b3a3 Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:23:58 +0300 Subject: [PATCH] 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. --- crates/game-service/src/battle.rs | 1 + crates/game-service/src/battle_session.rs | 9 +- crates/logic/src/battle/logic_battle.rs | 3 + crates/logic/src/battle/logic_pathfinder.rs | 116 ++++++++++++++++++++ crates/logic/src/battle/logic_simulation.rs | 26 +++-- crates/logic/src/battle/logic_tilemap.rs | 20 ++++ crates/logic/src/battle/mod.rs | 2 + 7 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 crates/logic/src/battle/logic_pathfinder.rs diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index f8813cf..817c725 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -302,6 +302,7 @@ impl BattleBuilder { .unwrap_or_default(), ]; let battle = LogicBattle { + tilemap: Some(tilemap), location, npc, arena, diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 0c1f427..bd31a79 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::sync::Arc; pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20; 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::SectorStateMessage; use logic::{BattleEventMessage, LogicDataRef, LogicRandom}; @@ -83,6 +85,9 @@ impl BattleSession { .unwrap_or(king_x); 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 { let snapshot = self.mode.snapshot().ok()?; let report = verify_snapshot(&snapshot); @@ -159,7 +164,7 @@ impl BattleSession { self.tick += 1; self.release_queued(self.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; if let Some(message) = self.snapshot_message() { self.outbound.push(message); diff --git a/crates/logic/src/battle/logic_battle.rs b/crates/logic/src/battle/logic_battle.rs index 3efbf6c..d9eabe8 100644 --- a/crates/logic/src/battle/logic_battle.rs +++ b/crates/logic/src/battle/logic_battle.rs @@ -39,6 +39,7 @@ pub struct LogicBattle { pub winner_score_change: i32, pub loser_score_change: i32, pub trailing: [i32; BATTLE_TRAILING_INTS], + pub tilemap: Option, } impl Default for LogicBattle { fn default() -> Self { @@ -66,6 +67,7 @@ impl Default for LogicBattle { winner_score_change: 0, loser_score_change: 0, trailing: [0; BATTLE_TRAILING_INTS], + tilemap: None, } } } @@ -257,6 +259,7 @@ impl Payload for LogicBattle { winner_score_change, loser_score_change, trailing, + tilemap: None, }) } } diff --git a/crates/logic/src/battle/logic_pathfinder.rs b/crates/logic/src/battle/logic_pathfinder.rs new file mode 100644 index 0000000..d07f6f8 --- /dev/null +++ b/crates/logic/src/battle/logic_pathfinder.rs @@ -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 { + 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 +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index e093725..f6f72d7 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -210,16 +210,28 @@ impl LogicBattle { }; let from = positions[index]; let stand_off = stats.range; - let dx = (goal.0 - from.0) as i64; - let dy = (goal.1 - from.1) as i64; - let distance = integer_sqrt(dx * dx + dy * dy); - if distance <= stand_off as i64 || distance == 0 { + let straight = integer_sqrt( + ((goal.0 - from.0) as i64).pow(2) + ((goal.1 - from.1) as i64).pow(2), + ); + if straight <= stand_off as i64 || straight == 0 { 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(); - base.position.x = from.0 + (dx * step / distance) as i32; - base.position.y = from.1 + (dy * step / distance) as i32; + base.position.x = from.0 + (dx * step / leg) as i32; + base.position.y = from.1 + (dy * step / leg) as i32; } } fn resolve_attacks(&mut self) { diff --git a/crates/logic/src/battle/logic_tilemap.rs b/crates/logic/src/battle/logic_tilemap.rs index 8a728a5..96d258f 100644 --- a/crates/logic/src/battle/logic_tilemap.rs +++ b/crates/logic/src/battle/logic_tilemap.rs @@ -1,6 +1,10 @@ use std::collections::HashMap; use std::path::Path; 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_MAP: &str = "Map"; pub const OBJECT_KING_TOWER: &str = "KingTower"; @@ -82,6 +86,22 @@ impl LogicTilemap { let source = std::fs::read_to_string(&path)?; 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 { self.tiles.len() as i32 } diff --git a/crates/logic/src/battle/mod.rs b/crates/logic/src/battle/mod.rs index 062c34f..265d922 100644 --- a/crates/logic/src/battle/mod.rs +++ b/crates/logic/src/battle/mod.rs @@ -6,6 +6,7 @@ mod logic_game_mode; mod logic_game_object; mod logic_game_object_manager; mod logic_game_object_ref; +mod logic_pathfinder; mod logic_simulation; mod logic_tilemap; mod logic_time; @@ -31,6 +32,7 @@ pub use logic_game_object::{ }; pub use logic_game_object_manager::LogicGameObjectManager; pub use logic_game_object_ref::LogicGameObjectRef; +pub use logic_pathfinder::find_path; pub use logic_simulation::{LogicCharacterStats, TICK_MILLISECONDS}; pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS}; pub use logic_time::LogicTime;