spread multi-unit cards on the client's cos/sin ring
audit finding #13, the last one. summon() gave every unit of a SummonNumber>1 card the identical command point, so goblins/archers stacked on one spot and their positions - which are hashed - diverged from the client, which places them on a ring. ported LogicMath::sin/cos: SIN_TABLE (91 entries, sin(deg)*1024) extracted verbatim from the binary, with the same quadrant folding, plus sin/cos scaled helpers. then LogicBattle::getSpawnOffset: the per-count ring (radius = 1000*collisionRadius / sin(180/n,1000), angle = base + 360*index/divisor + 90), with the bottom player's y mirrored. summon() adds the offset per unit. tests: sin(0/90/180/270)=0/1024/0/-1024, cos(0)=full scale, a single unit gets (0,0) and a pair splits across x; goblins no longer share a point. residual: the per-unit deploy stagger (charData[+200]*index/count % DeployTime) is not applied - all units still deploy_timer=DeployTime; that field's source column is unidentified, left for later. this closes the 17-item audit's actionable list. remaining known gaps are the deploy stagger above and the heartbeat follow-up (bot commands in-band + drop periodic snapshots).
This commit is contained in:
parent
ea9d9be4c4
commit
447cc091bc
4 changed files with 127 additions and 2 deletions
|
|
@ -179,12 +179,18 @@ impl BattleBuilder {
|
|||
return Vec::new();
|
||||
}
|
||||
let count = row.int(SPELL_SUMMON_NUMBER_COLUMN).max(1);
|
||||
let collision_radius = data
|
||||
.data()
|
||||
.map(|row| row.int("CollisionRadius"))
|
||||
.unwrap_or(0);
|
||||
(0..count)
|
||||
.map(|index| {
|
||||
let (dx, dy) =
|
||||
logic::spawn_offset(index, count, collision_radius, owner == 0);
|
||||
self.character(CharacterSpec {
|
||||
data: data.clone(),
|
||||
instance: first_instance + index,
|
||||
position,
|
||||
position: LogicVector2::new(position.x + dx, position.y + dy),
|
||||
owner,
|
||||
level_index,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -569,3 +569,19 @@ fn playing_a_card_cycles_the_summoner_hand() {
|
|||
assert_ne!(hand2[0], -1, "the slot refilled from the draw pile");
|
||||
assert_ne!(hand2[0], played, "with a different card than the one played");
|
||||
}
|
||||
#[test]
|
||||
fn a_multi_unit_card_spreads_its_units() {
|
||||
let root = assets();
|
||||
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
||||
LogicDataTables::install(Arc::new(t));
|
||||
}
|
||||
let builder = BattleBuilder::new(&root);
|
||||
let entries = builder.summon(&LogicDataRef::spell("Goblins"), LogicVector2::new(9000, 14500), 0, 0, 100);
|
||||
assert!(entries.len() >= 3, "goblins summon multiple units");
|
||||
let positions: Vec<(i32, i32)> = entries.iter().map(|e| e.position()).collect();
|
||||
for i in 0..positions.len() {
|
||||
for j in (i + 1)..positions.len() {
|
||||
assert_ne!(positions[i], positions[j], "units must not stack on one point");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub mod logic_random;
|
|||
pub mod messages;
|
||||
pub mod logic_math;
|
||||
pub mod model;
|
||||
pub use logic_math::{logic_sqrt, SQRT_TABLE};
|
||||
pub use logic_math::{logic_cos_scaled, logic_sin, logic_sin_scaled, logic_sqrt, spawn_offset, SIN_TABLE, SQRT_TABLE};
|
||||
pub use commands::{
|
||||
chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand,
|
||||
LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimAchievementRewardCommand,
|
||||
|
|
|
|||
|
|
@ -72,3 +72,106 @@ pub fn logic_sqrt(value: i32) -> i32 {
|
|||
let root = refined >> 1;
|
||||
root - i32::from(root.wrapping_mul(root) > value)
|
||||
}
|
||||
pub const SIN_TABLE: [i32; 91] = [
|
||||
0, 18, 36, 54, 71, 89, 107, 125, 143, 160,
|
||||
178, 195, 213, 230, 248, 265, 282, 299, 316, 333,
|
||||
350, 367, 384, 400, 416, 433, 449, 465, 481, 496,
|
||||
512, 527, 543, 558, 573, 587, 602, 616, 630, 644,
|
||||
658, 672, 685, 698, 711, 724, 737, 749, 761, 773,
|
||||
784, 796, 807, 818, 828, 839, 849, 859, 868, 878,
|
||||
887, 896, 904, 912, 920, 928, 935, 943, 949, 956,
|
||||
962, 968, 974, 979, 984, 989, 994, 998, 1002, 1005,
|
||||
1008, 1011, 1014, 1016, 1018, 1020, 1022, 1023, 1023, 1024,
|
||||
1024,
|
||||
];
|
||||
pub fn logic_sin(deg: i32) -> i32 {
|
||||
let mut v = deg % 360;
|
||||
if v < 0 {
|
||||
v += 360;
|
||||
}
|
||||
if v > 179 {
|
||||
let v3 = v - 180;
|
||||
let v4 = if v3 <= 90 { v3 } else { 360 - v };
|
||||
-SIN_TABLE[v4 as usize]
|
||||
} else {
|
||||
let v1 = if v > 90 { 180 - v } else { v };
|
||||
SIN_TABLE[v1 as usize]
|
||||
}
|
||||
}
|
||||
pub fn logic_sin_scaled(deg: i32, scale: i32) -> i32 {
|
||||
logic_sin(deg).wrapping_mul(scale) / 1024
|
||||
}
|
||||
pub fn logic_cos_scaled(deg: i32, scale: i32) -> i32 {
|
||||
logic_sin_scaled(deg + 90, scale)
|
||||
}
|
||||
pub fn spawn_offset(index: i32, count: i32, radius: i32, mirror: bool) -> (i32, i32) {
|
||||
if count <= 1 {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut idx = index;
|
||||
let mut n = count;
|
||||
let mut base_angle = 90;
|
||||
let mut divisor = 2;
|
||||
let mut r = radius;
|
||||
let use_ring = match count {
|
||||
2 => false,
|
||||
3 | 5 => true,
|
||||
4 => {
|
||||
n = 4;
|
||||
base_angle = 45;
|
||||
true
|
||||
}
|
||||
7 => {
|
||||
if index != 0 {
|
||||
base_angle = 0;
|
||||
idx -= 1;
|
||||
n = 6;
|
||||
true
|
||||
} else {
|
||||
return (0, 0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
base_angle = 0;
|
||||
count >= 3
|
||||
}
|
||||
};
|
||||
if use_ring {
|
||||
r = 1000i32.wrapping_mul(radius) / logic_sin_scaled(180 / n, 1000).max(1);
|
||||
if n >= 7 {
|
||||
r = (3 * idx % 7).wrapping_mul(r) / 6;
|
||||
}
|
||||
divisor = n;
|
||||
} else if count != 2 {
|
||||
divisor = count;
|
||||
}
|
||||
let angle = base_angle + 360 * idx / divisor.max(1) + 90;
|
||||
let dx = logic_cos_scaled(angle, r);
|
||||
let dy = logic_sin_scaled(angle, r);
|
||||
(dx, if mirror { -dy } else { dy })
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod trig_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn sin_matches_the_client_table() {
|
||||
assert_eq!(logic_sin(0), 0);
|
||||
assert_eq!(logic_sin(90), 1024);
|
||||
assert_eq!(logic_sin(180), 0);
|
||||
assert_eq!(logic_sin(270), -1024);
|
||||
assert_eq!(logic_sin(360), 0);
|
||||
assert_eq!(logic_sin(-90), -1024);
|
||||
assert_eq!(logic_cos_scaled(0, 1000), 1000);
|
||||
assert_eq!(logic_sin_scaled(90, 1000), 1000);
|
||||
}
|
||||
#[test]
|
||||
fn a_single_unit_has_no_offset_and_pairs_split() {
|
||||
assert_eq!(spawn_offset(0, 1, 300, false), (0, 0));
|
||||
let a = spawn_offset(0, 2, 300, false);
|
||||
let b = spawn_offset(1, 2, 300, false);
|
||||
assert_eq!(a.1, 0);
|
||||
assert_eq!(b.1, 0);
|
||||
assert_eq!(a.0, -b.0);
|
||||
assert_ne!(a.0, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue