diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index 3e6c808..6eda1a5 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -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, }) diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs index f5bf1d0..f4e4aca 100644 --- a/crates/game-service/tests/walk_to_tower.rs +++ b/crates/game-service/tests/walk_to_tower.rs @@ -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"); + } + } +} diff --git a/crates/logic/src/lib.rs b/crates/logic/src/lib.rs index ad93664..254b45e 100644 --- a/crates/logic/src/lib.rs +++ b/crates/logic/src/lib.rs @@ -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, diff --git a/crates/logic/src/logic_math.rs b/crates/logic/src/logic_math.rs index d88d896..16d5420 100644 --- a/crates/logic/src/logic_math.rs +++ b/crates/logic/src/logic_math.rs @@ -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); + } +}