use crate::battle::logic_battle::LogicBattle; use crate::battle::logic_battle::BATTLE_TICKS_PER_SECOND; use crate::battle::logic_component::{LogicComponent, COMPONENT_COMBAT, COMPONENT_HITPOINT}; use crate::battle::logic_game_object::{LogicGameObjectEntry, LogicObjectBody}; use crate::battle::logic_tilemap::SUBTILE_UNITS; use crate::data::LogicDataRef; pub const TICK_MILLISECONDS: i32 = 50; pub const CHARACTER_DEPLOY_TIME_COLUMN: &str = "DeployTime"; pub const CHARACTER_STATE_MOVING: i32 = 1; pub const MANA_ACCUMULATOR_STEP: i32 = 5000; pub const MANA_RATE_SCALE: i32 = 100; pub const GLOBAL_MAX_MANA: &str = "MAX_MANA"; pub const GLOBAL_MANA_REGEN: &str = "MANA_REGEN_MS"; pub const GLOBAL_MANA_REGEN_END: &str = "MANA_REGEN_MS_END"; pub const GLOBAL_MANA_SPEED_UP_SECONDS: &str = "MANA_SPEED_UP_WHEN_REMAINING_SECONDS"; pub const GLOBAL_KING_ACTIVATE_TIME_MS: &str = "KING_ACTIVATE_TIME_MS"; pub const KING_ACTIVATION_STEP: i32 = 50; pub const PRINCESS_TOWERS_TOTAL: i32 = 4; pub const COLUMN_SPEED: &str = "Speed"; pub const COLUMN_SIGHT_RANGE: &str = "SightRange"; pub const COLUMN_RANGE: &str = "Range"; pub const COLUMN_COLLISION_RADIUS: &str = "CollisionRadius"; pub const COLUMN_HIT_SPEED: &str = "HitSpeed"; pub const COLUMN_LOAD_TIME: &str = "LoadTime"; pub const COLUMN_DAMAGE: &str = "Damage"; pub const COLUMN_ATTACKS_AIR: &str = "AttacksAir"; pub const COLUMN_ATTACKS_GROUND: &str = "AttacksGround"; pub const COLUMN_TARGET_ONLY_BUILDINGS: &str = "TargetOnlyBuildings"; pub const COLUMN_FLYING_HEIGHT: &str = "FlyingHeight"; pub const COLUMN_MASS: &str = "Mass"; pub const COLLISION_RADIUS_CAP: i32 = 500; pub const COLLISION_OVERLAP_CAP: i32 = 300; pub const COLLISION_PUSH_CAP: i32 = 300; pub const MASS_MIN: i32 = 1; pub const MASS_MAX: i32 = 20; pub const MASS_FOR_STATIC: i32 = 20; pub const COLUMN_PROJECTILE: &str = "Projectile"; pub const DISTANCE_SATURATION: i32 = 46340; pub const WAYPOINT_REACHED: i32 = 250; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LogicCharacterStats { pub speed: i32, pub sight_range: i32, pub range: i32, pub collision_radius: i32, pub hit_speed: i32, pub load_time: i32, pub damage: i32, pub attacks_air: bool, pub attacks_ground: bool, pub target_only_buildings: bool, pub flying: bool, pub mass: i32, } impl LogicCharacterStats { pub fn of(data: &LogicDataRef, level_index: i32) -> Self { let Some(row) = data.data() else { return Self::none(); }; let level = level_index.max(0) as usize; let projectile = row.string(COLUMN_PROJECTILE); let damage = if projectile.is_empty() { row.int_at(COLUMN_DAMAGE, level) } else { LogicDataRef::by_name(crate::data::table::PROJECTILES, projectile) .data() .map(|shot| shot.int_at(COLUMN_DAMAGE, level)) .unwrap_or(0) }; Self { speed: row.int(COLUMN_SPEED), sight_range: row.int(COLUMN_SIGHT_RANGE), range: row.int(COLUMN_RANGE), collision_radius: row.int(COLUMN_COLLISION_RADIUS), hit_speed: row.int(COLUMN_HIT_SPEED), load_time: row.int(COLUMN_LOAD_TIME), damage, attacks_air: row.boolean(COLUMN_ATTACKS_AIR), attacks_ground: row.boolean(COLUMN_ATTACKS_GROUND), target_only_buildings: row.boolean(COLUMN_TARGET_ONLY_BUILDINGS), flying: row.int(COLUMN_FLYING_HEIGHT) > 0, mass: row.int(COLUMN_MASS).clamp(MASS_MIN, MASS_MAX), } } pub fn none() -> Self { Self { speed: 0, sight_range: 0, range: 0, collision_radius: 0, hit_speed: 0, load_time: 0, damage: 0, attacks_air: false, attacks_ground: false, target_only_buildings: false, flying: false, mass: MASS_MIN, } } pub fn is_building(&self) -> bool { self.speed == 0 } } pub fn distance_squared(from: (i32, i32), to: (i32, i32)) -> i32 { let dx = to.0 - from.0; let dy = to.1 - from.1; if dx.abs() > DISTANCE_SATURATION || dy.abs() > DISTANCE_SATURATION { return i32::MAX; } let square_x = dx * dx; if dy * dy >= (square_x ^ i32::MAX) { return i32::MAX; } square_x + dy * dy } impl LogicGameObjectEntry { pub fn position(&self) -> (i32, i32) { let base = self.body.base(); (base.position.x, base.position.y) } pub fn owner_index(&self) -> i32 { self.body.base().owner_index } pub fn level_index(&self) -> i32 { self.body.level_index() } pub fn stats(&self) -> LogicCharacterStats { LogicCharacterStats::of(&self.data, self.level_index()) } pub fn damage(&mut self, amount: i32) { if let Some(Some(LogicComponent::Hitpoint(component))) = self.components.get_mut(COMPONENT_HITPOINT) { component.hitpoints = (component.hitpoints - amount.max(0)).max(0); } } pub fn movement(&self) -> Option<&crate::battle::logic_component::LogicMovementComponent> { match self .components .get(crate::battle::logic_component::COMPONENT_MOVEMENT)? { Some(LogicComponent::Movement(component)) => Some(component), _ => None, } } pub fn movement_mut( &mut self, ) -> Option<&mut crate::battle::logic_component::LogicMovementComponent> { match self .components .get_mut(crate::battle::logic_component::COMPONENT_MOVEMENT)? { Some(LogicComponent::Movement(component)) => Some(component), _ => None, } } pub fn combat_mut( &mut self, ) -> Option<&mut crate::battle::logic_component::LogicCombatComponent> { match self.components.get_mut(COMPONENT_COMBAT)? { Some(LogicComponent::Combat(component)) => Some(component), _ => None, } } } struct CollisionBody { alive: bool, position: (i32, i32), stats: LogicCharacterStats, owner: i32, moves: bool, } impl LogicBattle { fn regenerate_mana(&mut self, tick: i32) { let max_mana = crate::LogicGlobals::number(GLOBAL_MAX_MANA).max(1); let seconds_left = (self.match_length_seconds() - tick / BATTLE_TICKS_PER_SECOND).max(0); let speed_up = crate::LogicGlobals::number(GLOBAL_MANA_SPEED_UP_SECONDS); let regen = if seconds_left <= speed_up { crate::LogicGlobals::number(GLOBAL_MANA_REGEN_END) } else { crate::LogicGlobals::number(GLOBAL_MANA_REGEN) }; let step = regen.saturating_mul(MANA_RATE_SCALE) / max_mana; if step < 1 { return; } for entry in self.objects.objects.iter_mut() { let LogicObjectBody::Summoner(summoner) = &mut entry.body else { continue; }; summoner.mana_regen_timer += MANA_ACCUMULATOR_STEP; let gained = summoner.mana_regen_timer / step; if gained < 1 { continue; } summoner.mana_regen_timer -= gained * step; summoner.mana = (summoner.mana + gained).clamp(0, max_mana); } } pub fn tick(&mut self, tick: i32) { self.activate_summoners(); self.advance_deploy(); self.regenerate_mana(tick); self.retarget(); self.resolve_collisions(); self.move_objects(); self.resolve_attacks(); self.remove_dead(); } fn activate_summoners(&mut self) { let activate_ms = crate::LogicGlobals::number(GLOBAL_KING_ACTIVATE_TIME_MS).max(0); let total = self .tilemap .as_ref() .map(|map| map.princess_towers().len() as i32) .filter(|count| *count > 0) .unwrap_or(PRINCESS_TOWERS_TOTAL); let standing = [ self.leader_towers.first().map(Vec::len).unwrap_or(0) as i32, self.leader_towers.get(1).map(Vec::len).unwrap_or(0) as i32, ]; for entry in self.objects.objects.iter_mut() { let full_health = match entry.components.get(COMPONENT_HITPOINT) { Some(Some(LogicComponent::Hitpoint(hp))) => hp.hitpoints >= hp.base_hitpoints, _ => true, }; let owner = entry.owner_index().clamp(0, 1) as usize; let LogicObjectBody::Summoner(summoner) = &mut entry.body else { continue; }; let princess_lost = standing[owner] < total / 2; if summoner.field_256 == 0 { if !full_health || princess_lost { summoner.field_256 = KING_ACTIVATION_STEP; } } else if summoner.field_256 <= activate_ms { summoner.field_256 += KING_ACTIVATION_STEP; } let active = summoner.field_256 > activate_ms; let mask = &mut summoner.character.base.component_mask; if active { *mask |= 1 << COMPONENT_COMBAT; } else { *mask &= !(1 << COMPONENT_COMBAT); } } } fn advance_deploy(&mut self) { for entry in self.objects.objects.iter_mut() { let deploy_time = entry .data .data() .map(|row| row.int(CHARACTER_DEPLOY_TIME_COLUMN)) .unwrap_or(0); let LogicObjectBody::Character(character) = &mut entry.body else { continue; }; if character.deploy_timer < deploy_time { character.deploy_timer = (character.deploy_timer + TICK_MILLISECONDS).min(deploy_time); } } } fn retarget(&mut self) { let snapshot: Vec<(i32, (i32, i32), LogicCharacterStats, bool)> = self .objects .objects .iter() .map(|entry| { ( entry.owner_index(), entry.position(), entry.stats(), entry.is_alive(), ) }) .collect(); for index in 0..self.objects.objects.len() { let (owner, position, stats, alive) = snapshot[index]; if !alive || stats.range < 1 { continue; } let mut best: Option<(i32, usize)> = None; for (other, (other_owner, other_position, other_stats, other_alive)) in snapshot.iter().enumerate() { if other == index || !other_alive || *other_owner == owner { continue; } if stats.target_only_buildings && !other_stats.is_building() { continue; } if other_stats.flying && !stats.attacks_air { continue; } if !other_stats.flying && !stats.attacks_ground { continue; } let reach = stats.sight_range + other_stats.collision_radius; let distance = distance_squared(position, *other_position); if distance > reach.saturating_mul(reach) { continue; } if best.map(|(closest, _)| distance < closest).unwrap_or(true) { best = Some((distance, other)); } } let target = best.map(|(_, other)| other); if let Some(component) = self.objects.objects[index].combat_mut() { component.target_index = target; } } } fn default_target(&self, index: usize) -> Option { let owner = self.objects.objects[index].owner_index(); let from = self.objects.objects[index].position(); let mut building: Option<(i32, usize)> = None; let mut anything: Option<(i32, usize)> = None; for (other, entry) in self.objects.objects.iter().enumerate() { if other == index || entry.owner_index() == owner || !entry.is_alive() { continue; } let distance = distance_squared(from, entry.position()); let closer = |best: &Option<(i32, usize)>| { best.map(|(closest, _)| distance < closest).unwrap_or(true) }; if entry.stats().is_building() && closer(&building) { building = Some((distance, other)); } if closer(&anything) { anything = Some((distance, other)); } } building.or(anything).map(|(_, other)| other) } fn next_waypoint( &mut self, index: usize, from: (i32, i32), goal: (i32, i32), flying: bool, ) -> (i32, i32) { let stale = match self.objects.objects[index].movement() { Some(movement) => movement.goal != Some(goal) || movement.path.is_empty(), None => return goal, }; if stale { let path = self .tilemap .as_ref() .map(|tilemap| crate::battle::find_path(tilemap, from, goal, flying)) .unwrap_or_default(); if let Some(movement) = self.objects.objects[index].movement_mut() { movement.goal = Some(goal); movement.route = path; } } let reached = self .objects .objects .get(index) .and_then(LogicGameObjectEntry::movement) .and_then(|movement| movement.route.first().copied()) .map(|node| { let close = distance_squared(from, node) <= WAYPOINT_REACHED * WAYPOINT_REACHED; (node, close) }); match reached { Some((node, true)) => { if let Some(movement) = self.objects.objects[index].movement_mut() { if !movement.route.is_empty() { movement.route.remove(0); } } let _ = node; self.objects .objects .get(index) .and_then(LogicGameObjectEntry::movement) .and_then(|movement| movement.route.first().copied()) .unwrap_or(goal) } Some((node, false)) => node, None => goal, } } fn move_objects(&mut self) { let positions: Vec<(i32, i32)> = self .objects .objects .iter() .map(LogicGameObjectEntry::position) .collect(); for index in 0..self.objects.objects.len() { let stats = self.objects.objects[index].stats(); if stats.speed < 1 || !self.objects.objects[index].is_alive() { continue; } let target = match self.objects.objects[index] .combat_mut() .and_then(|component| component.target_index) { Some(target) => target, None => match self.default_target(index) { Some(target) => target, None => continue, }, }; let Some(goal) = positions.get(target).copied() else { continue; }; let from = positions[index]; let stand_off = stats.range; let straight = crate::logic_sqrt(distance_squared(from, goal)) as i64; if straight <= stand_off as i64 || straight == 0 { continue; } let waypoint = self.next_waypoint(index, from, goal, stats.flying); let dx = (waypoint.0 - from.0) as i64; let dy = (waypoint.1 - from.1) as i64; let leg = crate::logic_sqrt(distance_squared(from, waypoint)) as i64; 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 / leg) as i32; base.position.y = from.1 + (dy * step / leg) as i32; } } fn resolve_collisions(&mut self) { let bodies: Vec = self .objects .objects .iter() .map(|entry| CollisionBody { alive: entry.is_alive(), position: entry.position(), stats: entry.stats(), owner: entry.owner_index(), moves: entry.movement().is_some(), }) .collect(); for index in 0..self.objects.objects.len() { let CollisionBody { alive, position: mine, stats, owner, moves, } = &bodies[index]; if !alive || !moves || stats.collision_radius < 1 { continue; } let mut push = (0i64, 0i64, 0i32); for (other, body) in bodies.iter().enumerate() { let (theirs, other_stats, other_moves) = (&body.position, &body.stats, body.moves); if other == index || !body.alive || other_stats.collision_radius < 1 { continue; } if stats.flying != other_stats.flying { continue; } let reach = if other_moves { stats.collision_radius } else { stats.collision_radius.min(COLLISION_RADIUS_CAP) }; let sum = reach + other_stats.collision_radius; let (mut dx, mut dy) = (mine.0 - theirs.0, mine.1 - theirs.1); if dx.abs() > sum || dy.abs() > sum { continue; } let mut square = dx * dx + dy * dy; if square == 0 { dx = 0; dy = if *owner == 0 { -1 } else { 1 }; square = 1; } if square > sum.saturating_mul(sum) { continue; } let distance = crate::logic_sqrt(square).max(1) as i64; let other_mass = if other_stats.is_building() { MASS_FOR_STATIC } else { other_stats.mass }; let overlap = (sum - distance as i32).clamp(0, COLLISION_OVERLAP_CAP); let strength = ((overlap * other_mass) / stats.mass.max(MASS_MIN) + 1).min(COLLISION_PUSH_CAP); push.0 += (strength as i64 * dx as i64) / distance; push.1 += (strength as i64 * dy as i64) / distance; push.2 += 1; } if push.2 == 0 { continue; } let mut shove = (push.0 / push.2 as i64, push.1 / push.2 as i64); let allowance = (stats.speed / 2).max(1) as i64; let travelled = crate::logic_sqrt(distance_squared((0, 0), (shove.0 as i32, shove.1 as i32))) as i64; if travelled > allowance { shove.0 = shove.0 * allowance / travelled; shove.1 = shove.1 * allowance / travelled; } let (limit_x, limit_y) = self .tilemap .as_ref() .map(|map| { ( map.width() * SUBTILE_UNITS - 1, map.height() * SUBTILE_UNITS - 1, ) }) .unwrap_or((i32::MAX, i32::MAX)); let base = self.objects.objects[index].body.base_mut(); base.position.x = (base.position.x + shove.0 as i32).clamp(0, limit_x); base.position.y = (base.position.y + shove.1 as i32).clamp(0, limit_y); } } fn resolve_attacks(&mut self) { let positions: Vec<(i32, i32)> = self .objects .objects .iter() .map(LogicGameObjectEntry::position) .collect(); let radii: Vec = self .objects .objects .iter() .map(|entry| entry.stats().collision_radius) .collect(); let mut hits: Vec<(usize, i32)> = Vec::new(); for index in 0..self.objects.objects.len() { let stats = self.objects.objects[index].stats(); if !self.objects.objects[index].is_alive() || stats.hit_speed < 1 { continue; } let Some(component) = self.objects.objects[index].combat_mut() else { continue; }; let Some(target) = component.target_index else { component.hit_timer = 0; continue; }; let reach = stats.range + radii.get(target).copied().unwrap_or(0); if distance_squared(positions[index], positions[target]) > reach.saturating_mul(reach) { component.hit_timer = 0; continue; } component.hit_timer += TICK_MILLISECONDS; let period = stats.hit_speed.max(TICK_MILLISECONDS); let wind_up = stats.load_time.max(0); if component.hit_timer >= wind_up + period { component.hit_timer = wind_up; hits.push((target, stats.damage)); } } for (target, amount) in hits { if let Some(entry) = self.objects.objects.get_mut(target) { entry.damage(amount); } } } fn remove_dead(&mut self) { let leaders = self.leaders; let dead: Vec = self .objects .objects .iter() .filter(|entry| !entry.is_alive() && !leaders.contains(&entry.global_id)) .map(|entry| entry.global_id) .collect(); if dead.is_empty() { return; } self.objects .objects .retain(|entry| entry.is_alive() || leaders.contains(&entry.global_id)); for towers in self.leader_towers.iter_mut() { towers.retain(|tower| !dead.contains(tower)); } for index in 0..self.objects.objects.len() { if let Some(component) = self.objects.objects[index].combat_mut() { component.target_index = None; } } } }