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)>, outbound: Vec, started_at: Instant, random: LogicRandom, taunts: Vec, 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) -> 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 = 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::>(); 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 { 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 { 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 { 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) { 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 { 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 { 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 { 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>>>, } impl BattleRegistry { pub async fn start( &self, players: &[AccountRef], mode: LogicGameMode, taunts: Vec, ) { 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>> { 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>> { self.sessions.lock().await.remove(&account) } pub async fn tick(&self, account: AccountRef, summon: F) -> Vec where F: Fn(&LogicDataRef, LogicVector2, i32, i32) -> Vec, { 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 { 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( &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, { 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, }