at type 0 the client stopped working the elixir bar out for itself and started reading it from the summoner in our snapshot - which never moved, so the bar sat where the opening state left it and no card could be afforded. the rule from LogicSummoner::tick: an accumulator gains five thousand a tick and one mana is granted for every MANA_REGEN_MS * 100 / MAX_MANA it holds, the remainder carried rather than dropped. that works out at 2.8 seconds a mana with the shipped globals, and halves in the last sixty seconds through MANA_REGEN_MS_END, which is the speed-up the game has always had.
364 lines
13 KiB
Rust
364 lines
13 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20;
|
|
use std::time::Instant;
|
|
use logic::battle::{
|
|
LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP,
|
|
};
|
|
use logic::battle::{verify_snapshot, LogicBattleEvent};
|
|
use logic::SectorStateMessage;
|
|
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
|
use titan::LogicLong;
|
|
use service_rpc::{AccountRef, WireMessage};
|
|
use tokio::sync::Mutex;
|
|
pub struct BattleSession {
|
|
mode: LogicGameMode,
|
|
tick: i32,
|
|
queued: Vec<(i32, Vec<LogicGameObjectEntry>)>,
|
|
outbound: Vec<WireMessage>,
|
|
started_at: Instant,
|
|
random: LogicRandom,
|
|
taunts: Vec<LogicDataRef>,
|
|
next_bot_emote: i32,
|
|
next_snapshot: i32,
|
|
next_bot_play: i32,
|
|
pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>,
|
|
}
|
|
impl BattleSession {
|
|
pub fn new(mode: LogicGameMode, taunts: Vec<LogicDataRef>) -> Self {
|
|
let mut random = LogicRandom::new(mode.random_seed);
|
|
let next_bot_emote = Self::roll_emote_tick(&mut random, 0);
|
|
Self {
|
|
mode,
|
|
tick: 0,
|
|
queued: Vec::new(),
|
|
outbound: Vec::new(),
|
|
started_at: Instant::now(),
|
|
random,
|
|
taunts,
|
|
next_bot_emote,
|
|
next_snapshot: SNAPSHOT_INTERVAL_TICKS,
|
|
next_bot_play: 0,
|
|
pending_bot_play: None,
|
|
}
|
|
}
|
|
pub fn take_bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
|
|
self.pending_bot_play.take()
|
|
}
|
|
pub fn bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
|
|
let deck = self.mode.battle.decks.get(1)?.as_ref()?;
|
|
let filled: Vec<LogicDataRef> = deck
|
|
.slots
|
|
.iter()
|
|
.flatten()
|
|
.map(|spell| spell.data.clone())
|
|
.collect();
|
|
if filled.is_empty() {
|
|
return None;
|
|
}
|
|
let card = filled
|
|
.get(self.random.next(filled.len() as i32).max(0) as usize)?
|
|
.clone();
|
|
let towers: Vec<(i32, i32)> = self
|
|
.mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.filter(|entry| entry.owner_index() == crate::bot::BOT_OWNER_INDEX)
|
|
.map(|entry| entry.position())
|
|
.collect();
|
|
let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?;
|
|
let (king_x, king_y) = leader.position();
|
|
let ahead = towers
|
|
.iter()
|
|
.map(|(_, y)| *y)
|
|
.fold(king_y, |best, y| if (y - king_y).abs() > (best - king_y).abs() { y } else { best });
|
|
let lane_x = towers
|
|
.iter()
|
|
.filter(|(_, y)| *y == ahead)
|
|
.map(|(x, _)| *x)
|
|
.collect::<Vec<_>>();
|
|
let x = lane_x
|
|
.get(self.random.next(lane_x.len().max(1) as i32).max(0) as usize)
|
|
.copied()
|
|
.unwrap_or(king_x);
|
|
Some((card, LogicVector2::new(x, ahead), self.next_instance()))
|
|
}
|
|
pub fn pushes_snapshots(&self) -> bool {
|
|
self.mode.battle.battle_type == BATTLE_TYPE_PVP
|
|
}
|
|
fn snapshot_message(&mut self) -> Option<WireMessage> {
|
|
let snapshot = self.mode.snapshot().ok()?;
|
|
let report = verify_snapshot(&snapshot);
|
|
if !report.is_ok() {
|
|
tracing::error!(
|
|
bytes = snapshot.len(),
|
|
trailing = report.trailing,
|
|
error = report.error.as_deref().unwrap_or("-"),
|
|
"the battle snapshot does not read back, not pushing it"
|
|
);
|
|
return None;
|
|
}
|
|
crate::wire::encode(&SectorStateMessage::new(snapshot)).ok()
|
|
}
|
|
fn roll_emote_tick(random: &mut LogicRandom, from: i32) -> i32 {
|
|
let span = crate::bot::BOT_EMOTE_MAX_SECONDS - crate::bot::BOT_EMOTE_MIN_SECONDS;
|
|
let seconds = crate::bot::BOT_EMOTE_MIN_SECONDS + random.next(span.max(1));
|
|
from + seconds * BATTLE_TICKS_PER_SECOND
|
|
}
|
|
pub fn opponent_account(&self) -> LogicLong {
|
|
self.mode
|
|
.battle
|
|
.account_ids
|
|
.get(1)
|
|
.copied()
|
|
.unwrap_or_default()
|
|
}
|
|
pub fn bot_emote(&mut self) -> Option<LogicBattleEvent> {
|
|
if self.taunts.is_empty() {
|
|
return None;
|
|
}
|
|
let index = self.random.next(self.taunts.len() as i32).max(0) as usize;
|
|
let taunt = self.taunts.get(index)?;
|
|
let instance = taunt.global_id().map(|id| id.instance_id).unwrap_or(0);
|
|
Some(LogicBattleEvent {
|
|
event_type: 1,
|
|
account_id: self.opponent_account(),
|
|
ticks: vec![self.tick],
|
|
coords: Vec::new(),
|
|
params: vec![instance],
|
|
})
|
|
}
|
|
pub fn elapsed_tick(&self) -> i32 {
|
|
let millis = self.started_at.elapsed().as_millis() as i64;
|
|
(millis * BATTLE_TICKS_PER_SECOND as i64 / 1000) as i32
|
|
}
|
|
pub fn advance_to_now(&mut self) {
|
|
let tick = self.elapsed_tick();
|
|
self.advance_to(tick);
|
|
}
|
|
pub fn push_outbound(&mut self, message: WireMessage) {
|
|
self.outbound.push(message);
|
|
}
|
|
pub fn take_outbound(&mut self) -> Vec<WireMessage> {
|
|
std::mem::take(&mut self.outbound)
|
|
}
|
|
pub fn mode(&self) -> &LogicGameMode {
|
|
&self.mode
|
|
}
|
|
pub fn tick(&self) -> i32 {
|
|
self.tick
|
|
}
|
|
pub fn seconds(&self) -> i32 {
|
|
self.tick / BATTLE_TICKS_PER_SECOND
|
|
}
|
|
pub fn queue(&mut self, at_tick: i32, entries: Vec<LogicGameObjectEntry>) {
|
|
if entries.is_empty() {
|
|
return;
|
|
}
|
|
self.queued.push((at_tick.max(self.tick + 1), entries));
|
|
}
|
|
pub fn advance_to(&mut self, tick: i32) {
|
|
while self.tick < tick {
|
|
self.tick += 1;
|
|
self.release_queued(self.tick);
|
|
self.mode.battle.tick(self.tick);
|
|
if self.pushes_snapshots() && self.tick >= self.next_snapshot {
|
|
self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS;
|
|
if let Some(message) = self.snapshot_message() {
|
|
self.outbound.push(message);
|
|
}
|
|
}
|
|
if self.tick >= self.next_bot_play {
|
|
let span = crate::bot::BOT_PLAY_MAX_SECONDS - crate::bot::BOT_PLAY_MIN_SECONDS;
|
|
let wait = crate::bot::BOT_PLAY_MIN_SECONDS + self.random.next(span.max(1));
|
|
self.next_bot_play = self.tick + wait * BATTLE_TICKS_PER_SECOND;
|
|
if let Some((card, position, instance)) = self.bot_play() {
|
|
self.pending_bot_play = Some((card, position, instance));
|
|
}
|
|
}
|
|
if self.tick >= self.next_bot_emote {
|
|
self.next_bot_emote = Self::roll_emote_tick(&mut self.random, self.tick);
|
|
if let Some(event) = self.bot_emote() {
|
|
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
|
|
self.outbound.push(message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.mode.time.tick = self.tick;
|
|
}
|
|
fn release_queued(&mut self, tick: i32) {
|
|
let mut due = Vec::new();
|
|
self.queued.retain(|(at, entries)| {
|
|
if *at <= tick {
|
|
due.extend(entries.iter().cloned());
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
for entry in due {
|
|
self.mode.battle.objects.push(entry);
|
|
}
|
|
}
|
|
pub fn is_finished(&self) -> bool {
|
|
self.mode.battle.is_end_condition_matched(self.tick)
|
|
}
|
|
pub fn stars(&self) -> (i32, i32) {
|
|
(self.mode.battle.stars(0), self.mode.battle.stars(1))
|
|
}
|
|
pub fn checksum(&self) -> Option<i32> {
|
|
self.mode.calculate_checksum().ok()
|
|
}
|
|
pub fn towers_standing(&self) -> (usize, usize) {
|
|
(
|
|
self.mode.battle.leader_towers[0].len(),
|
|
self.mode.battle.leader_towers[1].len(),
|
|
)
|
|
}
|
|
pub fn owner_of(&self, account: LogicLong) -> Option<i32> {
|
|
self.mode
|
|
.battle
|
|
.account_ids
|
|
.iter()
|
|
.position(|id| *id == account)
|
|
.map(|index| index as i32)
|
|
}
|
|
pub fn next_instance(&self) -> i32 {
|
|
self.mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.filter_map(|entry| entry.global_id.0.map(|id| id.instance_id))
|
|
.max()
|
|
.map(|last| last + 1)
|
|
.unwrap_or(0)
|
|
}
|
|
pub fn deck_card(&self, owner: i32, slot: i32) -> Option<LogicDataRef> {
|
|
let deck = self.mode.battle.decks.get(owner.max(0) as usize)?.as_ref()?;
|
|
let spell = deck.slots.get(slot.max(0) as usize)?.as_ref()?;
|
|
Some(spell.data.clone())
|
|
}
|
|
pub fn push(&mut self, entry: LogicGameObjectEntry) {
|
|
self.mode.battle.objects.push(entry);
|
|
}
|
|
pub fn object_count(&self) -> usize {
|
|
self.mode.battle.objects.objects.len()
|
|
}
|
|
}
|
|
#[derive(Default)]
|
|
pub struct BattleRegistry {
|
|
sessions: Mutex<HashMap<AccountRef, Arc<Mutex<BattleSession>>>>,
|
|
}
|
|
impl BattleRegistry {
|
|
pub async fn start(
|
|
&self,
|
|
players: &[AccountRef],
|
|
mode: LogicGameMode,
|
|
taunts: Vec<LogicDataRef>,
|
|
) {
|
|
let session = Arc::new(Mutex::new(BattleSession::new(mode, taunts)));
|
|
let mut sessions = self.sessions.lock().await;
|
|
for account in players {
|
|
sessions.insert(*account, Arc::clone(&session));
|
|
}
|
|
}
|
|
async fn session(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
|
|
self.sessions.lock().await.get(&account).cloned()
|
|
}
|
|
pub async fn is_running(&self, account: AccountRef) -> bool {
|
|
self.sessions.lock().await.contains_key(&account)
|
|
}
|
|
pub async fn finish(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
|
|
self.sessions.lock().await.remove(&account)
|
|
}
|
|
pub async fn tick<F>(&self, account: AccountRef, summon: F) -> Vec<WireMessage>
|
|
where
|
|
F: Fn(&LogicDataRef, LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
|
|
{
|
|
let Some(handle) = self.session(account).await else {
|
|
return Vec::new();
|
|
};
|
|
let mut session = handle.lock().await;
|
|
session.advance_to_now();
|
|
if let Some((card, position, instance)) = session.take_bot_play() {
|
|
let entries = summon(&card, position, crate::bot::BOT_OWNER_INDEX, instance);
|
|
if !entries.is_empty() {
|
|
tracing::debug!(card = %card, count = entries.len(), "the bot played a card");
|
|
for entry in entries {
|
|
session.push(entry);
|
|
}
|
|
}
|
|
}
|
|
session.take_outbound()
|
|
}
|
|
pub async fn answer_emote(&self, account: AccountRef) {
|
|
let Some(handle) = self.session(account).await else {
|
|
return;
|
|
};
|
|
let mut session = handle.lock().await;
|
|
let Some(event) = session.bot_emote() else {
|
|
return;
|
|
};
|
|
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
|
|
session.push_outbound(message);
|
|
}
|
|
}
|
|
pub async fn send(&self, account: AccountRef, message: WireMessage) {
|
|
if let Some(handle) = self.session(account).await {
|
|
handle.lock().await.push_outbound(message);
|
|
}
|
|
}
|
|
pub async fn advance(&self, account: AccountRef, tick: i32) -> Option<BattleProgress> {
|
|
let handle = self.session(account).await?;
|
|
let mut session = handle.lock().await;
|
|
session.advance_to(tick);
|
|
Some(BattleProgress {
|
|
seconds: session.seconds(),
|
|
finished: session.is_finished(),
|
|
stars: session.stars(),
|
|
objects: session.object_count(),
|
|
towers: session.towers_standing(),
|
|
checksum: session.checksum(),
|
|
})
|
|
}
|
|
pub async fn play<F>(
|
|
&self,
|
|
account: AccountRef,
|
|
executor: LogicLong,
|
|
slot: i32,
|
|
position: logic::battle::LogicVector2,
|
|
at_tick: i32,
|
|
summon: F,
|
|
) -> usize
|
|
where
|
|
F: Fn(&LogicDataRef, logic::battle::LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
|
|
{
|
|
let Some(handle) = self.session(account).await else {
|
|
return 0;
|
|
};
|
|
let mut session = handle.lock().await;
|
|
let Some(owner) = session.owner_of(executor) else {
|
|
return 0;
|
|
};
|
|
let Some(card) = session.deck_card(owner, slot) else {
|
|
return 0;
|
|
};
|
|
let entries = summon(&card, position, owner, session.next_instance());
|
|
let spawned = entries.len();
|
|
session.queue(at_tick, entries);
|
|
spawned
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct BattleProgress {
|
|
pub seconds: i32,
|
|
pub finished: bool,
|
|
pub stars: (i32, i32),
|
|
pub objects: usize,
|
|
pub towers: (usize, usize),
|
|
pub checksum: Option<i32>,
|
|
}
|