diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 116bbb5..54f2efb 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20; use std::time::Instant; -use logic::battle::{LogicGameMode, LogicGameObjectEntry, BATTLE_TICKS_PER_SECOND}; +use logic::battle::{LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND}; use logic::battle::{verify_snapshot, LogicBattleEvent}; use logic::SectorStateMessage; use logic::{BattleEventMessage, LogicDataRef, LogicRandom}; @@ -18,6 +18,8 @@ pub struct BattleSession { 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 { @@ -33,8 +35,53 @@ impl BattleSession { 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())) + } fn snapshot_message(&mut self) -> Option { let snapshot = self.mode.snapshot().ok()?; let report = verify_snapshot(&snapshot); @@ -117,6 +164,14 @@ impl BattleSession { 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() { @@ -193,12 +248,24 @@ impl BattleRegistry { pub async fn finish(&self, account: AccountRef) -> Option { self.sessions.lock().await.remove(&account) } - pub async fn tick(&self, account: AccountRef) -> Vec { + pub async fn tick(&self, account: AccountRef, summon: F) -> Vec + where + F: Fn(&LogicDataRef, LogicVector2, i32, i32) -> Vec, + { let mut sessions = self.sessions.lock().await; let Some(session) = sessions.get_mut(&account) else { return Vec::new(); }; 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) { diff --git a/crates/game-service/src/bot.rs b/crates/game-service/src/bot.rs index 9433506..6355d6e 100644 --- a/crates/game-service/src/bot.rs +++ b/crates/game-service/src/bot.rs @@ -5,6 +5,9 @@ pub const BOT_NAME: &str = "Bot"; pub const TAUNT_MENU_COLUMN: &str = "TauntMenu"; pub const BOT_EMOTE_MIN_SECONDS: i32 = 12; pub const BOT_EMOTE_MAX_SECONDS: i32 = 30; +pub const BOT_PLAY_MIN_SECONDS: i32 = 4; +pub const BOT_PLAY_MAX_SECONDS: i32 = 11; +pub const BOT_OWNER_INDEX: i32 = 1; pub const DECK_SPELLS_COLUMN: &str = "Spells"; pub const DECK_LEVEL_COLUMN: &str = "SpellLevel"; pub fn menu_taunts() -> Vec { diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index c691e3d..25d44e5 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -394,7 +394,12 @@ impl GameApi for GameService { Ok(replies) } async fn battle_tick(&self, account: AccountRef) -> RpcResult> { - Ok(self.running_battles.tick(account).await) + Ok(self + .running_battles + .tick(account, |card, position, owner, instance| { + self.battles.summon(card, position, owner, 0, instance) + }) + .await) } async fn battle_event(&self, account: AccountRef, payload: Vec) -> RpcResult<()> { let Ok(sent) = SendBattleEventMessage::from_bytes(&payload) else {