push units apart when they overlap

collisions, from checkCollisions and checkCollision in the client.

a pair is considered when both are on the same plane - air with air,
ground with ground, decided by z. the radius is the unit's own
CollisionRadius, capped at 500 when the other side has no movement
component, which is what lets a unit squeeze past a building instead of
being shoved by it, plus the other's radius. the axis test comes before
the squared one, tangency counts as a hit, and two units standing exactly
on top of each other are separated along y by the owner's facing rather
than dividing by zero.

the push is clamp(sum - distance, 0, 300) scaled by the other's mass over
mine, plus one, capped at 300, spread along dx and dy over the distance.
Mass is clamped to one through twenty and a building counts as twenty.
the accumulator is drained the same tick it is filled, as it is in the
client.

what is still short of the client: the push is averaged over the pairs
rather than run through updateMovementTowards, avoidance steering is not
modelled, and LogicMath::sqrt is our exact root rather than the client's
table - which differs from the true root above 2147441940 and will have
to be reproduced bug for bug before checksums can agree.
This commit is contained in:
WiseDev 2026-08-23 14:15:32 +03:00
parent a355b77514
commit 8a4a81057b

View file

@ -21,6 +21,13 @@ 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;
@ -37,6 +44,7 @@ pub struct LogicCharacterStats {
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 {
@ -65,6 +73,7 @@ impl LogicCharacterStats {
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 {
@ -80,6 +89,7 @@ impl LogicCharacterStats {
attacks_ground: false,
target_only_buildings: false,
flying: false,
mass: MASS_MIN,
}
}
pub fn is_building(&self) -> bool {
@ -185,6 +195,7 @@ impl LogicBattle {
pub fn tick(&mut self, tick: i32) {
self.regenerate_mana(tick);
self.retarget();
self.resolve_collisions();
self.move_objects();
self.resolve_attacks();
self.remove_dead();
@ -354,6 +365,76 @@ impl LogicBattle {
base.position.y = from.1 + (dy * step / leg) as i32;
}
}
fn resolve_collisions(&mut self) {
let bodies: Vec<(bool, (i32, i32), LogicCharacterStats, i32, bool)> = self
.objects
.objects
.iter()
.map(|entry| {
(
entry.is_alive(),
entry.position(),
entry.stats(),
entry.owner_index(),
entry.movement().is_some(),
)
})
.collect();
for index in 0..self.objects.objects.len() {
let (alive, mine, stats, owner, moves) = &bodies[index];
if !alive || !moves || stats.collision_radius < 1 {
continue;
}
let mut push = (0i64, 0i64, 0i32);
for (other, (other_alive, theirs, other_stats, _, other_moves)) in
bodies.iter().enumerate()
{
if other == index || !other_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 = integer_sqrt(square as i64).max(1);
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 base = self.objects.objects[index].body.base_mut();
base.position.x += (push.0 / push.2 as i64) as i32;
base.position.y += (push.1 / push.2 as i64) as i32;
}
}
fn resolve_attacks(&mut self) {
let positions: Vec<(i32, i32)> = self
.objects