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;
|
||||
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) {
|
||||
for entry in self.mode.battle.objects.objects.iter_mut() {
|
||||
if entry.owner_index() != owner {
|
||||
|
|
@ -467,6 +486,7 @@ impl BattleRegistry {
|
|||
};
|
||||
let cost = card.data().map(|row| row.int("ManaCost")).unwrap_or(0);
|
||||
session.spend_mana(owner, cost);
|
||||
session.play_from_hand(owner, slot);
|
||||
let entries = summon(&card, position, owner, session.next_instance());
|
||||
session.reserve_instances(entries.len());
|
||||
let spawned = entries.len();
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
|
|||
LogicDataRef::None,
|
||||
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
||||
vec![avatar(5), avatar(0)],
|
||||
[Some(deck.clone()), Some(deck)],
|
||||
[Some(deck.clone()), Some(deck.clone())],
|
||||
7,
|
||||
)
|
||||
.expect("battle");
|
||||
|
|
@ -509,3 +509,63 @@ fn the_session_emits_a_heartbeat_every_turn() {
|
|||
assert_eq!(turn, 1);
|
||||
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_END: &str = "MANA_REGEN_MS_END";
|
||||
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 COLLISION_PUSH_LIMIT: i64 = 150;
|
||||
pub const GLOBAL_MANA_SPEED_UP_SECONDS: &str = "MANA_SPEED_UP_WHEN_REMAINING_SECONDS";
|
||||
|
|
@ -224,12 +227,54 @@ impl LogicBattle {
|
|||
self.activate_summoners();
|
||||
self.advance_deploy();
|
||||
self.regenerate_mana(tick);
|
||||
self.cycle_summoner_decks(tick);
|
||||
self.retarget();
|
||||
self.resolve_collisions();
|
||||
self.move_objects();
|
||||
self.resolve_attacks();
|
||||
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) {
|
||||
let activate_ms = crate::LogicGlobals::number(GLOBAL_KING_ACTIVATE_TIME_MS).max(0);
|
||||
let total = self
|
||||
|
|
|
|||
Loading…
Reference in a new issue