cycle the summoner deck on play and draw, RNG-free
audit finding #3 (deck), after verifying the RNG stream is not involved: addSpellsInRandomOrder shuffles the INITIAL order with a separate Mersenne Twister, but the server is authoritative (ships the deck in the snapshot; the client decodes it, never reshuffles), and the in-match cycle is fully deterministic. so no RNG matching is needed. play: the command's slot is the DECK index (LogicDoSpellCommand::execute maps it to a hand slot via getSpellIndex, rejecting if it is not in hand). play_from_hand finds the hand slot holding that index, moves the card to the used pile and empties the slot - useSpellFromHand. spawn is untouched; it already reads the card by the deck index. draw: cycle_summoner_decks decrements spell_cooldown each tick and, when it hits 0 with an empty hand slot, slides the front of the draw pile in (refilling draw from used IN ORDER when it empties) and resets the timer to NEXT_SPELL_COOLDOWN_MILLISECONDS (2000 / _BOOST / _OVERTIME). field_240 (reshuffle) just counts down and stays 0 in normal play. harness: playing hand slot 0 empties it and banks the card in used; ticking past the cooldown refills the slot with a different card.
This commit is contained in:
parent
7ead67d219
commit
ea9d9be4c4
3 changed files with 126 additions and 1 deletions
|
|
@ -78,6 +78,25 @@ impl BattleSession {
|
||||||
self.next_snapshot = self.tick;
|
self.next_snapshot = self.tick;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
pub fn play_from_hand(&mut self, owner: i32, deck_index: i32) {
|
||||||
|
for entry in self.mode.battle.objects.objects.iter_mut() {
|
||||||
|
if entry.owner_index() != owner {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let logic::battle::LogicObjectBody::Summoner(summoner) = &mut entry.body else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(deck) = summoner.deck.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(slot) = deck.hand.iter().position(|card| *card == deck_index) {
|
||||||
|
deck.hand[slot] = -1;
|
||||||
|
deck.used_pile.push(deck_index);
|
||||||
|
deck.last_used_index = deck_index;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn spend_mana(&mut self, owner: i32, cost: i32) {
|
pub fn spend_mana(&mut self, owner: i32, cost: i32) {
|
||||||
for entry in self.mode.battle.objects.objects.iter_mut() {
|
for entry in self.mode.battle.objects.objects.iter_mut() {
|
||||||
if entry.owner_index() != owner {
|
if entry.owner_index() != owner {
|
||||||
|
|
@ -467,6 +486,7 @@ impl BattleRegistry {
|
||||||
};
|
};
|
||||||
let cost = card.data().map(|row| row.int("ManaCost")).unwrap_or(0);
|
let cost = card.data().map(|row| row.int("ManaCost")).unwrap_or(0);
|
||||||
session.spend_mana(owner, cost);
|
session.spend_mana(owner, cost);
|
||||||
|
session.play_from_hand(owner, slot);
|
||||||
let entries = summon(&card, position, owner, session.next_instance());
|
let entries = summon(&card, position, owner, session.next_instance());
|
||||||
session.reserve_instances(entries.len());
|
session.reserve_instances(entries.len());
|
||||||
let spawned = entries.len();
|
let spawned = entries.len();
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
|
||||||
LogicDataRef::None,
|
LogicDataRef::None,
|
||||||
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
||||||
vec![avatar(5), avatar(0)],
|
vec![avatar(5), avatar(0)],
|
||||||
[Some(deck.clone()), Some(deck)],
|
[Some(deck.clone()), Some(deck.clone())],
|
||||||
7,
|
7,
|
||||||
)
|
)
|
||||||
.expect("battle");
|
.expect("battle");
|
||||||
|
|
@ -509,3 +509,63 @@ fn the_session_emits_a_heartbeat_every_turn() {
|
||||||
assert_eq!(turn, 1);
|
assert_eq!(turn, 1);
|
||||||
assert_eq!(Some(checksum), session.checksum_at(10));
|
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, "with a different card than the one played");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ pub const GLOBAL_MAX_MANA: &str = "MAX_MANA";
|
||||||
pub const GLOBAL_MANA_REGEN: &str = "MANA_REGEN_MS";
|
pub const GLOBAL_MANA_REGEN: &str = "MANA_REGEN_MS";
|
||||||
pub const GLOBAL_MANA_REGEN_END: &str = "MANA_REGEN_MS_END";
|
pub const GLOBAL_MANA_REGEN_END: &str = "MANA_REGEN_MS_END";
|
||||||
pub const GLOBAL_MANA_REGEN_OVERTIME: &str = "MANA_REGEN_MS_OVERTIME";
|
pub const GLOBAL_MANA_REGEN_OVERTIME: &str = "MANA_REGEN_MS_OVERTIME";
|
||||||
|
pub const GLOBAL_NEXT_SPELL_COOLDOWN: &str = "NEXT_SPELL_COOLDOWN_MILLISECONDS";
|
||||||
|
pub const GLOBAL_NEXT_SPELL_COOLDOWN_BOOST: &str = "NEXT_SPELL_COOLDOWN_MILLISECONDS_BOOST";
|
||||||
|
pub const GLOBAL_NEXT_SPELL_COOLDOWN_OVERTIME: &str = "NEXT_SPELL_COOLDOWN_MILLISECONDS_OVERTIME";
|
||||||
pub const MAX_MOVE_STEP: i64 = 250;
|
pub const MAX_MOVE_STEP: i64 = 250;
|
||||||
pub const COLLISION_PUSH_LIMIT: i64 = 150;
|
pub const COLLISION_PUSH_LIMIT: i64 = 150;
|
||||||
pub const GLOBAL_MANA_SPEED_UP_SECONDS: &str = "MANA_SPEED_UP_WHEN_REMAINING_SECONDS";
|
pub const GLOBAL_MANA_SPEED_UP_SECONDS: &str = "MANA_SPEED_UP_WHEN_REMAINING_SECONDS";
|
||||||
|
|
@ -224,12 +227,54 @@ impl LogicBattle {
|
||||||
self.activate_summoners();
|
self.activate_summoners();
|
||||||
self.advance_deploy();
|
self.advance_deploy();
|
||||||
self.regenerate_mana(tick);
|
self.regenerate_mana(tick);
|
||||||
|
self.cycle_summoner_decks(tick);
|
||||||
self.retarget();
|
self.retarget();
|
||||||
self.resolve_collisions();
|
self.resolve_collisions();
|
||||||
self.move_objects();
|
self.move_objects();
|
||||||
self.resolve_attacks();
|
self.resolve_attacks();
|
||||||
self.remove_dead();
|
self.remove_dead();
|
||||||
}
|
}
|
||||||
|
fn cycle_summoner_decks(&mut self, tick: i32) {
|
||||||
|
let elapsed = tick / BATTLE_TICKS_PER_SECOND;
|
||||||
|
let cooldown = if self.is_on_overtime {
|
||||||
|
crate::LogicGlobals::number(GLOBAL_NEXT_SPELL_COOLDOWN_OVERTIME)
|
||||||
|
} else {
|
||||||
|
let seconds_left = (self.match_length_seconds() - elapsed).max(0);
|
||||||
|
let speed_up = crate::LogicGlobals::number(GLOBAL_MANA_SPEED_UP_SECONDS);
|
||||||
|
if seconds_left <= speed_up {
|
||||||
|
crate::LogicGlobals::number(GLOBAL_NEXT_SPELL_COOLDOWN_BOOST)
|
||||||
|
} else {
|
||||||
|
crate::LogicGlobals::number(GLOBAL_NEXT_SPELL_COOLDOWN)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for entry in self.objects.objects.iter_mut() {
|
||||||
|
let LogicObjectBody::Summoner(summoner) = &mut entry.body else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(deck) = summoner.deck.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if deck.field_240 > 0 {
|
||||||
|
deck.field_240 = (deck.field_240 - TICK_MILLISECONDS).max(0);
|
||||||
|
}
|
||||||
|
deck.spell_cooldown = (deck.spell_cooldown - TICK_MILLISECONDS).max(0);
|
||||||
|
if deck.spell_cooldown != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(slot) = deck.hand.iter().position(|card| *card == -1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if deck.draw_pile.is_empty() {
|
||||||
|
let refill = std::mem::take(&mut deck.used_pile);
|
||||||
|
deck.draw_pile = refill;
|
||||||
|
}
|
||||||
|
if deck.draw_pile.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deck.hand[slot] = deck.draw_pile.remove(0);
|
||||||
|
deck.spell_cooldown = cooldown;
|
||||||
|
}
|
||||||
|
}
|
||||||
fn activate_summoners(&mut self) {
|
fn activate_summoners(&mut self) {
|
||||||
let activate_ms = crate::LogicGlobals::number(GLOBAL_KING_ACTIVATE_TIME_MS).max(0);
|
let activate_ms = crate::LogicGlobals::number(GLOBAL_KING_ACTIVATE_TIME_MS).max(0);
|
||||||
let total = self
|
let total = self
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue