838 lines
27 KiB
Rust
838 lines
27 KiB
Rust
use game_service::battle::BattleBuilder;
|
|
use game_service::battle_session::MAX_CATCH_UP_TICKS;
|
|
use logic::battle::{verify_snapshot, LogicVector2, BATTLE_TICKS_PER_SECOND};
|
|
use logic::data::{table, LogicDataRef, LogicDataTables};
|
|
use logic::model::LogicClientAvatar;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use titan::LogicLong;
|
|
fn assets() -> std::path::PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets")
|
|
}
|
|
fn avatar(low: i32) -> LogicClientAvatar {
|
|
let account = LogicLong::new(0, low);
|
|
LogicClientAvatar {
|
|
avatar_id: account,
|
|
account_id: account,
|
|
home_id: account,
|
|
name_change_state: -1,
|
|
..LogicClientAvatar::default()
|
|
}
|
|
}
|
|
#[test]
|
|
fn ranged_fire_roundtrips() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let spell = LogicDataRef::spell("Archer");
|
|
let entries = builder.summon(&spell, LogicVector2::new(14500, 23500), 0, 0, 100);
|
|
assert!(!entries.is_empty(), "the archer spell summons nothing");
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
for tick in 0..(15 * BATTLE_TICKS_PER_SECOND) {
|
|
mode.battle.tick(tick);
|
|
let snapshot = mode.snapshot(&[]).expect("snapshot encodes");
|
|
let report = verify_snapshot(&snapshot);
|
|
assert!(
|
|
report.is_ok(),
|
|
"t={tick}: snapshot did not read back: {:?} trailing={}",
|
|
report.error,
|
|
report.trailing
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn troop_walks_and_stops() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let towers: Vec<(i32, (i32, i32))> = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.map(|entry| (entry.owner_index(), entry.position()))
|
|
.collect();
|
|
println!("towers: {towers:?}");
|
|
let spell = LogicDataRef::spell("Knight");
|
|
let entries = builder.summon(&spell, LogicVector2::new(9000, 20000), 0, 0, 100);
|
|
assert!(!entries.is_empty(), "the knight spell summons nothing");
|
|
let id = entries[0].global_id;
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
let mut last = None;
|
|
for tick in 0..(30 * BATTLE_TICKS_PER_SECOND) {
|
|
mode.battle.tick(tick);
|
|
let here = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.global_id == id)
|
|
.map(|entry| entry.position());
|
|
if here.is_some() {
|
|
last = here;
|
|
}
|
|
if tick % 40 == 0 {
|
|
println!("t={tick:4} pos={here:?}");
|
|
}
|
|
for entry in mode.battle.objects.objects.iter() {
|
|
let (x, y) = entry.position();
|
|
assert!(
|
|
(0..=18000).contains(&x) && (0..=33000).contains(&y),
|
|
"t={tick}: an object left the arena at ({x}, {y})"
|
|
);
|
|
}
|
|
}
|
|
let end = last.expect("the knight never existed");
|
|
println!("last seen at {end:?}");
|
|
let tower = (14500, 25500);
|
|
let reach = ((end.0 - tower.0) as f64).hypot((end.1 - tower.1) as f64);
|
|
println!("distance to the near enemy tower: {reach:.0}");
|
|
assert!(
|
|
reach < 3000.0,
|
|
"the knight never reached the enemy tower, stopped {reach:.0} away at {end:?}"
|
|
);
|
|
}
|
|
#[test]
|
|
fn bot_deploys_at_towers() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut deck = logic::model::LogicSpellDeck::default();
|
|
for (slot, name) in ["Knight", "Cannon", "GoblinHut", "Giant"]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
deck.slots[slot] = Some(logic::model::LogicSpell {
|
|
data: LogicDataRef::spell(name),
|
|
..logic::model::LogicSpell::default()
|
|
});
|
|
}
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[Some(deck.clone()), Some(deck.clone())],
|
|
7,
|
|
)
|
|
.expect("battle");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
let mut plays = 0;
|
|
for tick in 0..(180 * BATTLE_TICKS_PER_SECOND) {
|
|
if tick % 20 == 0 {
|
|
if let Some((card, at, instance, level)) = session.bot_play() {
|
|
let entries = builder.summon(&card, at, 1, level, instance);
|
|
let entries_debug = entries.clone();
|
|
session.reserve_instances(entries.len());
|
|
for entry in entries {
|
|
session.push(entry);
|
|
}
|
|
assert!(
|
|
(17000..25000).contains(&at.y) && (at.x == 3500 || at.x == 14500),
|
|
"the bot deployed at {:?}, which is not in front of one of its towers",
|
|
(at.x, at.y)
|
|
);
|
|
plays += 1;
|
|
if plays <= 8 {
|
|
println!(
|
|
"bot played {:?} at {:?}, {} entries, speeds {:?}",
|
|
card,
|
|
(at.x, at.y),
|
|
entries_debug.len(),
|
|
entries_debug
|
|
.iter()
|
|
.map(|e| e.stats().speed)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
session.advance_to(tick);
|
|
for (side, leader) in session.mode().battle.leaders.iter().enumerate() {
|
|
let alive = session
|
|
.mode()
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.any(|entry| entry.global_id == *leader);
|
|
assert!(
|
|
alive,
|
|
"t={tick}: leader {leader:?} gone"
|
|
);
|
|
}
|
|
if session.is_finished() || session.towers_standing().0 == 0 {
|
|
println!("battle over at t={tick} after {plays} bot plays");
|
|
break;
|
|
}
|
|
for entry in session.mode().battle.objects.objects.iter() {
|
|
let (x, y) = entry.position();
|
|
if !((0..=17999).contains(&x) && (0..=31999).contains(&y)) {
|
|
println!(
|
|
"t={tick}: object owner={} speed={} alive={} at ({x}, {y})",
|
|
entry.owner_index(),
|
|
entry.stats().speed,
|
|
entry.is_alive()
|
|
);
|
|
for other in session.mode().battle.objects.objects.iter() {
|
|
println!(
|
|
" owner={} speed={} alive={} at {:?}",
|
|
other.owner_index(),
|
|
other.stats().speed,
|
|
other.is_alive(),
|
|
other.position()
|
|
);
|
|
}
|
|
panic!("a troop walked out of the arena");
|
|
}
|
|
}
|
|
}
|
|
assert!(plays > 0, "bot never played");
|
|
}
|
|
#[test]
|
|
fn bot_troop_walks() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let entries = builder.summon(
|
|
&LogicDataRef::spell("Knight"),
|
|
LogicVector2::new(14500, 23500),
|
|
1,
|
|
0,
|
|
200,
|
|
);
|
|
let id = entries[0].global_id;
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
let mut last = None;
|
|
for tick in 0..(20 * BATTLE_TICKS_PER_SECOND) {
|
|
mode.battle.tick(tick);
|
|
let here = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == id)
|
|
.map(|e| e.position());
|
|
if here.is_some() {
|
|
last = here;
|
|
}
|
|
if tick % 40 == 0 {
|
|
println!("t={tick:4} bot troop at {here:?}");
|
|
}
|
|
}
|
|
let end = last.expect("the bot troop never existed");
|
|
println!("bot troop finished at {end:?}");
|
|
assert!(
|
|
end.1 < 23500,
|
|
"bot troop at {end:?}"
|
|
);
|
|
}
|
|
#[test]
|
|
fn both_leaders_resolve_to_a_summoner() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
println!("leaders: {:?}", mode.battle.leaders);
|
|
for (side, leader) in mode.battle.leaders.iter().enumerate() {
|
|
assert!(
|
|
!leader.is_none(),
|
|
"side {side} has no leader; LogicBattle::resetSimulatedManaTimers reads both \
|
|
without checking for null and takes the client down with it"
|
|
);
|
|
let found = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.global_id == *leader);
|
|
assert!(
|
|
found.is_some(),
|
|
"leader {leader:?} not found"
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn finished_battle_stops() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
for _ in 0..400 {
|
|
session.advance_to(session.tick() + MAX_CATCH_UP_TICKS);
|
|
}
|
|
assert!(session.is_finished(), "battle never ended");
|
|
let settled = session.tick();
|
|
session.advance_to(settled + 10_000);
|
|
assert_eq!(
|
|
session.tick(),
|
|
settled,
|
|
"a finished battle carried on ticking"
|
|
);
|
|
}
|
|
#[test]
|
|
fn dormant_king_combat_bit() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
for tick in 1..80 {
|
|
mode.battle.tick(tick);
|
|
}
|
|
let mut kings = 0;
|
|
let mut princesses = 0;
|
|
for entry in mode.battle.objects.objects.iter() {
|
|
let mask = entry.body.base().component_mask;
|
|
match &entry.body {
|
|
logic::battle::LogicObjectBody::Summoner(_) => {
|
|
assert_eq!(mask, 12, "combat bit not cleared");
|
|
kings += 1;
|
|
}
|
|
logic::battle::LogicObjectBody::Character(_) => {
|
|
assert_eq!(mask, 13, "a princess tower keeps its combat bit");
|
|
princesses += 1;
|
|
}
|
|
logic::battle::LogicObjectBody::Projectile(_) => {}
|
|
}
|
|
}
|
|
assert_eq!(kings, 2);
|
|
assert_eq!(princesses, 4);
|
|
let a = mode.calculate_checksum().unwrap();
|
|
mode.time.tick = 200;
|
|
mode.battle.tick(200);
|
|
let b = mode.calculate_checksum().unwrap();
|
|
mode.time.tick = 200;
|
|
let c = mode.calculate_checksum().unwrap();
|
|
assert_eq!(b, c, "idle hash mismatch");
|
|
let _ = a;
|
|
}
|
|
#[test]
|
|
fn deploy_countdown_holds() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let entries = builder.summon(
|
|
&LogicDataRef::spell("Knight"),
|
|
LogicVector2::new(9000, 20000),
|
|
0,
|
|
0,
|
|
100,
|
|
);
|
|
let id = entries[0].global_id;
|
|
let deploy = entries[0]
|
|
.data
|
|
.data()
|
|
.map(|r| r.int("DeployTime"))
|
|
.unwrap_or(0);
|
|
assert!(deploy >= 1, "the knight has a deploy time");
|
|
let spawn_pos = entries[0].position();
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
let timer_of = |mode: &logic::battle::LogicGameMode| {
|
|
mode.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == id)
|
|
.and_then(|e| match &e.body {
|
|
logic::battle::LogicObjectBody::Character(c) => Some(c.deploy_timer),
|
|
_ => None,
|
|
})
|
|
};
|
|
mode.battle.tick(1);
|
|
assert_eq!(
|
|
timer_of(&mode),
|
|
Some(deploy - 50),
|
|
"deploy timer counts down"
|
|
);
|
|
let here = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == id)
|
|
.unwrap()
|
|
.position();
|
|
assert_eq!(here, spawn_pos, "a deploying troop holds still");
|
|
for tick in 2..(deploy / 50 + 40) {
|
|
mode.battle.tick(tick);
|
|
}
|
|
assert_eq!(timer_of(&mode), Some(0), "deploy finished");
|
|
let moved = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == id)
|
|
.unwrap()
|
|
.position();
|
|
assert_ne!(moved, spawn_pos, "a deployed troop moves");
|
|
}
|
|
#[test]
|
|
fn in_range_damage() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let (tower_id, tower_pos, start_hp) = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find_map(|e| match &e.body {
|
|
logic::battle::LogicObjectBody::Character(_) if e.owner_index() == 1 => {
|
|
let hp = e
|
|
.components
|
|
.get(2)
|
|
.and_then(|c| c.as_ref())
|
|
.and_then(|c| match c {
|
|
logic::battle::LogicComponent::Hitpoint(h) => Some(h.hitpoints),
|
|
_ => None,
|
|
})?;
|
|
Some((e.global_id, e.position(), hp))
|
|
}
|
|
_ => None,
|
|
})
|
|
.expect("an enemy princess tower");
|
|
let entries = builder.summon(
|
|
&LogicDataRef::spell("Knight"),
|
|
LogicVector2::new(tower_pos.0, tower_pos.1 - 800),
|
|
0,
|
|
0,
|
|
100,
|
|
);
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
for tick in 1..200 {
|
|
mode.battle.tick(tick);
|
|
}
|
|
let hp = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == tower_id)
|
|
.and_then(|e| e.components.get(2))
|
|
.and_then(|c| c.as_ref())
|
|
.and_then(|c| match c {
|
|
logic::battle::LogicComponent::Hitpoint(h) => Some(h.hitpoints),
|
|
_ => None,
|
|
})
|
|
.expect("tower still present");
|
|
assert!(
|
|
hp < start_hp,
|
|
"the knight's attacks should reduce tower hp ({hp} !< {start_hp})"
|
|
);
|
|
}
|
|
#[test]
|
|
fn heartbeat_every_turn() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
std::env::set_var("SCROLL_HEARTBEAT", "1");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
session.advance_to(25);
|
|
let out = session.take_outbound();
|
|
std::env::remove_var("SCROLL_HEARTBEAT");
|
|
let heartbeats: Vec<&service_rpc::WireMessage> =
|
|
out.iter().filter(|m| m.message_type == 21902).collect();
|
|
assert_eq!(
|
|
heartbeats.len(),
|
|
2,
|
|
"expected a heartbeat at ticks 10 and 20"
|
|
);
|
|
let mut reader = titan::ByteStreamReader::new(&heartbeats[0].payload);
|
|
let turn = reader.read_vint().unwrap();
|
|
let checksum = reader.read_vint().unwrap();
|
|
assert_eq!(turn, 1);
|
|
assert_eq!(Some(checksum), session.checksum_at(10));
|
|
}
|
|
#[test]
|
|
fn playing_a_card_cycles_the_summoner_hand() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let mut deck = logic::model::LogicSpellDeck::default();
|
|
let names = [
|
|
"Knight",
|
|
"Archers",
|
|
"Goblins",
|
|
"Giant",
|
|
"Fireball",
|
|
"Arrows",
|
|
"Minions",
|
|
"Musketeer",
|
|
];
|
|
for (slot, name) in names.iter().enumerate().take(deck.slots.len()) {
|
|
deck.slots[slot] = Some(logic::model::LogicSpell {
|
|
data: LogicDataRef::spell(name),
|
|
..logic::model::LogicSpell::default()
|
|
});
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[Some(deck.clone()), Some(deck.clone())],
|
|
7,
|
|
)
|
|
.expect("battle");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
let hand_of = |session: &game_service::battle_session::BattleSession| {
|
|
session
|
|
.mode()
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find_map(|e| match &e.body {
|
|
logic::battle::LogicObjectBody::Summoner(s) if e.owner_index() == 0 => s
|
|
.deck
|
|
.as_ref()
|
|
.map(|d| (d.hand, d.used_pile.clone(), d.spell_cooldown)),
|
|
_ => None,
|
|
})
|
|
.expect("player summoner with a deck")
|
|
};
|
|
let (hand0, used0, _) = hand_of(&session);
|
|
let filled = deck.filled_slot_count();
|
|
assert!(filled >= 5, "need a draw pile for this test");
|
|
let played = hand0[0];
|
|
assert!(played >= 0, "hand slot 0 holds a card");
|
|
session.play_from_hand(0, played);
|
|
let (hand1, used1, _) = hand_of(&session);
|
|
assert_eq!(hand1[0], -1, "the played slot is now empty");
|
|
assert!(used1.contains(&played), "the card went to the used pile");
|
|
assert_eq!(used1.len(), used0.len() + 1);
|
|
for _ in 0..6 {
|
|
session.advance_to(session.tick() + MAX_CATCH_UP_TICKS);
|
|
}
|
|
let (hand2, _, _) = hand_of(&session);
|
|
assert_ne!(hand2[0], -1, "the slot refilled from the draw pile");
|
|
assert_ne!(
|
|
hand2[0], played,
|
|
"wrong card"
|
|
);
|
|
}
|
|
#[test]
|
|
fn multi_unit_spread() {
|
|
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"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn snapshot_with_command_roundtrips() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let played = logic::LogicDoSpellCommand {
|
|
header: logic::LogicCommandHeader {
|
|
tick_when_given: 125,
|
|
execute_tick: 145,
|
|
executor_account_id: LogicLong::new(0, 5),
|
|
},
|
|
deck_slot: 0,
|
|
spell: Some(logic::model::LogicSpell {
|
|
data: LogicDataRef::spell("Knight"),
|
|
..Default::default()
|
|
}),
|
|
position: LogicVector2::new(10500, 12500),
|
|
};
|
|
let mut writer = titan::ByteStreamWriter::new();
|
|
logic::LogicCommandManager::encode_command(&mut writer, &played).expect("command encodes");
|
|
let encoded = writer.into_inner();
|
|
for commands in [
|
|
Vec::new(),
|
|
vec![encoded.clone()],
|
|
vec![encoded.clone(), encoded],
|
|
] {
|
|
let snapshot = mode.snapshot(&commands).expect("snapshot encodes");
|
|
let report = verify_snapshot(&snapshot);
|
|
assert!(
|
|
report.is_ok(),
|
|
"{} command(s): snapshot did not read back: {:?} trailing={}",
|
|
commands.len(),
|
|
report.error,
|
|
report.trailing
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn deploy_search_matches_client() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let tilemap = mode.battle.tilemap.as_ref().expect("tilemap");
|
|
let knight = LogicDataRef::by_name(table::CHARACTERS_COMBINED, "Knight");
|
|
let blockers = mode.battle.deploy_blockers(true);
|
|
assert_eq!(blockers.len(), 6);
|
|
assert!(blockers.contains(&logic::battle::DeployBlocker {
|
|
x: 3500,
|
|
y: 25500,
|
|
size_w: 11,
|
|
size_h: 21
|
|
}));
|
|
assert!(blockers.contains(&logic::battle::DeployBlocker {
|
|
x: 3500,
|
|
y: 6500,
|
|
size_w: 3,
|
|
size_h: 3
|
|
}));
|
|
for (raw, expected) in [
|
|
((0, 0), (500, 1500)),
|
|
((9000, 15900), (8500, 14500)),
|
|
((9000, 22000), (8500, 14500)),
|
|
((3500, 22000), (3500, 14500)),
|
|
((3500, 10000), (3500, 10500)),
|
|
((12500, 9500), (12500, 9500)),
|
|
] {
|
|
let found = logic::battle::find_position_for_spell(
|
|
knight.as_character().as_ref(),
|
|
LogicVector2::new(raw.0, raw.1),
|
|
tilemap,
|
|
&blockers,
|
|
false,
|
|
)
|
|
.unwrap_or_else(|| panic!("{raw:?} should resolve to a position"));
|
|
assert_eq!((found.x, found.y), expected, "drop at {raw:?} landed wrong");
|
|
}
|
|
}
|
|
#[test]
|
|
fn refused_drop_spawns_nothing() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let tilemap = mode.battle.tilemap.as_ref().expect("tilemap");
|
|
let knight = LogicDataRef::spell("Knight");
|
|
for (tile, raw) in [((25, 31), (12750, 15750)), ((0, 0), (0, 0))] {
|
|
assert!(
|
|
!tilemap.can_place_egg(tile.0, tile.1),
|
|
"tile {tile:?} should refuse a deploy"
|
|
);
|
|
let point = LogicVector2::new(raw.0, raw.1);
|
|
assert_eq!(
|
|
logic::battle::check_spell_position(tilemap, point.x, point.y, true),
|
|
5
|
|
);
|
|
assert!(mode
|
|
.battle
|
|
.resolve_spell_position(&knight, point, 0)
|
|
.is_none());
|
|
}
|
|
assert_eq!(
|
|
logic::battle::check_spell_position(tilemap, 12750, 15750, false),
|
|
0
|
|
);
|
|
}
|
|
#[test]
|
|
fn mana_already_committed_cannot_be_spent_twice() {
|
|
let root = assets();
|
|
if let Ok(t) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(t));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
session.advance_to(120);
|
|
let available = session.simulated_mana(0);
|
|
assert!(available > 0, "no mana by t120");
|
|
let entries = builder.summon(
|
|
&LogicDataRef::spell("Knight"),
|
|
LogicVector2::new(9500, 14500),
|
|
0,
|
|
0,
|
|
session.next_instance(),
|
|
);
|
|
session.queue(
|
|
120,
|
|
LogicVector2::new(9500, 14500),
|
|
0,
|
|
available,
|
|
0,
|
|
entries,
|
|
);
|
|
assert_eq!(
|
|
session.simulated_mana(0),
|
|
0,
|
|
"a card in flight has already claimed its mana"
|
|
);
|
|
}
|