- 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
148 lines
4.7 KiB
Rust
148 lines
4.7 KiB
Rust
use game_service::battle::BattleBuilder;
|
|
use game_service::battle_session::{BattleSession, COMMAND_DELAY_TICKS, DEPLOY_BIRTH_TICKS};
|
|
use logic::battle::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 a_card_lands_one_tick_after_the_command_fires() {
|
|
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 mut session = BattleSession::new(mode, Vec::new());
|
|
let at_tick = 0;
|
|
let spell = LogicDataRef::spell("Knight");
|
|
let entries = builder.summon(&spell, LogicVector2::new(14500, 23500), 0, 0, 100);
|
|
assert!(!entries.is_empty(), "the knight spell summons nothing");
|
|
let before = session.object_count();
|
|
let born = session.queue(at_tick, LogicVector2::new(14500, 23500), 0, 3, 0, entries);
|
|
assert_eq!(
|
|
born,
|
|
at_tick + DEPLOY_BIRTH_TICKS,
|
|
"the birth tick must be one past the firing tick"
|
|
);
|
|
assert_eq!(
|
|
DEPLOY_BIRTH_TICKS,
|
|
COMMAND_DELAY_TICKS + 1,
|
|
"the command still fires at age twenty; only the label the troops are first hashed under moves"
|
|
);
|
|
session.advance_to(born - 1);
|
|
assert_eq!(
|
|
session.object_count(),
|
|
before,
|
|
"the troops appeared on the firing tick, a tick before the client hashes them"
|
|
);
|
|
session.advance_to(born);
|
|
assert!(
|
|
session.object_count() > before,
|
|
"the troops never arrived on the birth tick"
|
|
);
|
|
}
|
|
#[test]
|
|
fn a_state_request_waits_for_the_cards_in_the_air() {
|
|
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 mut session = BattleSession::new(mode, Vec::new());
|
|
assert!(
|
|
session.state_on_request().is_some(),
|
|
"with nothing in the air a request must be answered at once"
|
|
);
|
|
let first = session.queue(
|
|
0,
|
|
LogicVector2::new(14500, 23500),
|
|
0,
|
|
3,
|
|
0,
|
|
builder.summon(
|
|
&LogicDataRef::spell("Archer"),
|
|
LogicVector2::new(14500, 23500),
|
|
0,
|
|
0,
|
|
100,
|
|
),
|
|
);
|
|
session.advance_to(15);
|
|
let second = session.queue(
|
|
15,
|
|
LogicVector2::new(14000, 23500),
|
|
0,
|
|
3,
|
|
1,
|
|
builder.summon(
|
|
&LogicDataRef::spell("Bomber"),
|
|
LogicVector2::new(14000, 23500),
|
|
0,
|
|
0,
|
|
100,
|
|
),
|
|
);
|
|
assert!(second > first, "the second card must land after the first");
|
|
let mut refused = Vec::new();
|
|
for tick in 16..(second + 4) {
|
|
if session.state_on_request().is_none() {
|
|
refused.push(session.tick());
|
|
}
|
|
session.advance_to(tick);
|
|
}
|
|
for tick in &refused {
|
|
let near_a_firing_tick = [first, second]
|
|
.iter()
|
|
.any(|fires_at| tick > fires_at && tick <= &(fires_at + 2));
|
|
assert!(
|
|
near_a_firing_tick,
|
|
"t={tick}: a state was refused away from any firing tick ({first}, {second}); while a full update is pending that silently eats the player's cards"
|
|
);
|
|
}
|
|
assert!(
|
|
!refused.is_empty(),
|
|
"the firing ticks were never protected at all"
|
|
);
|
|
session.advance_to(second + 1);
|
|
let mut paid = false;
|
|
for _ in 0..8 {
|
|
if session.take_outbound().into_iter().count() > 0 {
|
|
paid = true;
|
|
break;
|
|
}
|
|
session.advance_to(session.tick() + 1);
|
|
}
|
|
assert!(paid, "the owed state was never sent after the cards landed");
|
|
}
|