scroll.server/crates/game-service/tests/combat_timer_phase.rs
WiseDev a585b0c18d resolve the deploy birth tick and stop wedging the client's own commands
- 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
2026-08-28 16:27:16 +03:00

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"
);
}