scroll.server/crates/game-service/tests/projectile_references.rs
2026-08-28 23:54:30 +03:00

161 lines
5.2 KiB
Rust

use game_service::battle::BattleBuilder;
use logic::battle::{LogicObjectBody, LogicVector2};
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 projectile_drops_dead_ref() {
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("SkeletonArmy");
for entry in builder.summon(&spell, LogicVector2::new(14500, 22000), 0, 0, 100) {
mode.battle.objects.push(entry);
}
let mut projectiles_seen = 0usize;
let mut stale = Vec::new();
for tick in 0..600 {
mode.battle.tick(tick);
let alive: Vec<_> = mode
.battle
.objects
.objects
.iter()
.map(|entry| entry.global_id)
.collect();
for entry in &mode.battle.objects.objects {
let LogicObjectBody::Projectile(projectile) = &entry.body else {
continue;
};
projectiles_seen += 1;
for (name, reference) in [("target", projectile.target), ("source", projectile.source)]
{
if reference.is_none() {
continue;
}
if !alive.contains(&reference) {
stale.push(format!(
"t={tick}: stale {name} ref {:?} -> {:?}",
entry.global_id.0, reference.0
));
}
}
}
}
assert!(
projectiles_seen > 0,
"no projectiles seen"
);
assert!(
stale.is_empty(),
"a projectile outlived one of its references:\n{}",
stale.join("\n")
);
}
#[test]
fn pending_damage_matches_shots() {
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");
for spell in ["Archer", "SkeletonArmy"] {
for entry in builder.summon(
&LogicDataRef::spell(spell),
LogicVector2::new(14500, 22000),
0,
0,
100,
) {
mode.battle.objects.push(entry);
}
}
let mut ever_positive = false;
for tick in 0..800 {
mode.battle.tick(tick);
let mut owed: Vec<(logic::battle::LogicGameObjectRef, i32)> = Vec::new();
for entry in &mode.battle.objects.objects {
let LogicObjectBody::Projectile(shot) = &entry.body else {
continue;
};
if shot.destroyed || shot.target.is_none() {
continue;
}
let Some(data) = entry.data.as_projectile() else {
continue;
};
if !data.uses_pending_physical_damage() {
continue;
}
let damage = data.damage(shot.level_index.max(0) as usize);
match owed.iter_mut().find(|(t, _)| *t == shot.target) {
Some((_, total)) => *total += damage,
None => owed.push((shot.target, damage)),
}
}
for entry in &mode.battle.objects.objects {
let held = match &entry.body {
LogicObjectBody::Character(character) => character.pending_physical_damage,
LogicObjectBody::Summoner(summoner) => summoner.character.pending_physical_damage,
LogicObjectBody::Projectile(_) => continue,
};
assert!(
held >= 0,
"t={tick}: {:?} negative pending {held}",
entry.global_id.0
);
let want = owed
.iter()
.find(|(t, _)| *t == entry.global_id)
.map(|(_, total)| *total)
.unwrap_or(0);
assert_eq!(
held, want,
"t={tick}: {:?} pending {held} != {want}",
entry.global_id.0
);
if held > 0 {
ever_positive = true;
}
}
}
assert!(
ever_positive,
"no pending damage seen"
);
}