- a card lands one tick after its command fires, since the client hashes after LogicTime::increaseTick, not before - answer RequestSectorState immediately except in the two ticks after a firing tick, where a full update would teleport the client past the command it just queued - the old blanket refusal left every card the player tapped silently deleted while it held - carry both pending commands and queued deploys in the same delivery ledger so the quiet window covers what the player actually paid for - add the regression tests for the birth tick and the deferred answer
100 lines
3.3 KiB
Rust
100 lines
3.3 KiB
Rust
use game_service::battle::BattleBuilder;
|
|
use logic::battle::{LogicComponent, LogicObjectBody, LogicVector2, COMPONENT_COMBAT};
|
|
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 a_ranged_shooter_updates_twice_on_the_tick_it_fires() {
|
|
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");
|
|
for entry in builder.summon(&spell, LogicVector2::new(14500, 23500), 0, 0, 100) {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
const ARCHER_LOAD_TIME: i32 = 1000;
|
|
let load_timer_of = |mode: &logic::battle::LogicGameMode, id: i32| -> Option<i32> {
|
|
mode.battle.objects.objects.iter().find_map(|entry| {
|
|
if entry.global_id.0?.instance_id != id {
|
|
return None;
|
|
}
|
|
match entry.components.get(COMPONENT_COMBAT)? {
|
|
Some(LogicComponent::Combat(combat)) => Some(combat.load_timer),
|
|
_ => None,
|
|
}
|
|
})
|
|
};
|
|
let archer_ref = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.data.name() == "Archer")
|
|
.map(|entry| entry.global_id)
|
|
.expect("the archer is on the board");
|
|
let archer = archer_ref
|
|
.0
|
|
.expect("the archer has a global id")
|
|
.instance_id;
|
|
let mut shots = 0usize;
|
|
let mut seen: Vec<logic::battle::LogicGameObjectRef> = Vec::new();
|
|
for tick in 0..(20 * 20) {
|
|
mode.battle.tick(tick);
|
|
let fresh: Vec<_> = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.filter_map(|entry| match &entry.body {
|
|
LogicObjectBody::Projectile(shot) if shot.source == archer_ref => {
|
|
Some(entry.global_id)
|
|
}
|
|
_ => None,
|
|
})
|
|
.filter(|id| !seen.contains(id))
|
|
.collect();
|
|
for id in &fresh {
|
|
seen.push(*id);
|
|
}
|
|
if fresh.is_empty() {
|
|
continue;
|
|
}
|
|
shots += 1;
|
|
let load_timer = load_timer_of(&mode, archer).expect("the archer has a combat component");
|
|
assert_eq!(
|
|
load_timer,
|
|
ARCHER_LOAD_TIME - logic::battle::TICK_MILLISECONDS,
|
|
"t={tick}: the shooter did not take its second update on the firing tick"
|
|
);
|
|
}
|
|
assert!(
|
|
shots > 1,
|
|
"the archer fired {shots} times, too few to prove anything"
|
|
);
|
|
}
|