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;