diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs new file mode 100644 index 0000000..5ae66aa --- /dev/null +++ b/crates/game-service/src/battle_session.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use logic::battle::{LogicGameMode, BATTLE_TICKS_PER_SECOND}; +use service_rpc::AccountRef; +use tokio::sync::Mutex; +pub struct BattleSession { + mode: LogicGameMode, + tick: i32, +} +impl BattleSession { + pub fn new(mode: LogicGameMode) -> Self { + Self { mode, tick: 0 } + } + 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 advance_to(&mut self, tick: i32) { + if tick > self.tick { + self.tick = tick; + self.mode.time.tick = tick; + } + } + 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)) + } +} +#[derive(Default)] +pub struct BattleRegistry { + sessions: Mutex>, +} +impl BattleRegistry { + pub async fn start(&self, account: AccountRef, mode: LogicGameMode) { + self.sessions + .lock() + .await + .insert(account, BattleSession::new(mode)); + } + pub async fn finish(&self, account: AccountRef) -> Option { + self.sessions.lock().await.remove(&account) + } + pub async fn advance(&self, account: AccountRef, tick: i32) -> Option { + let mut sessions = self.sessions.lock().await; + let session = sessions.get_mut(&account)?; + session.advance_to(tick); + Some(BattleProgress { + seconds: session.seconds(), + finished: session.is_finished(), + stars: session.stars(), + }) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BattleProgress { + pub seconds: i32, + pub finished: bool, + pub stars: (i32, i32), +} diff --git a/crates/game-service/src/lib.rs b/crates/game-service/src/lib.rs index febc3cb..0569391 100644 --- a/crates/game-service/src/lib.rs +++ b/crates/game-service/src/lib.rs @@ -1,4 +1,5 @@ pub mod battle; +pub mod battle_session; pub mod bot; pub mod catalog; pub mod config; @@ -10,6 +11,7 @@ pub mod shop; pub mod store; pub mod time; pub use battle::BattleBuilder; +pub use battle_session::{BattleProgress, BattleRegistry, BattleSession}; pub use bot::BotPlayer; pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE}; pub use config::{CardRef, DataSelector, GameConfig, StarterProfile}; diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index 5ebba46..461bb8a 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -11,6 +11,7 @@ use service_rpc::{ }; use titan::{Message, MessageMeta, Payload}; use crate::battle::BattleBuilder; +use crate::battle_session::BattleRegistry; use crate::catalog::Catalog; use crate::config::GameConfig; use crate::bot::BotPlayer; @@ -33,6 +34,7 @@ pub struct GameService { catalog: Arc, shop: Arc, battles: Arc, + running_battles: BattleRegistry, sessions: HomeModeRegistry, } impl GameService { @@ -63,6 +65,7 @@ impl GameService { catalog, shop, battles, + running_battles: BattleRegistry::default(), sessions: HomeModeRegistry::default(), })) } @@ -150,6 +153,7 @@ impl GameService { format!("{} trailing byte(s)", report.trailing) }))); } + self.running_battles.start(account, battle).await; Ok(vec![encode(&SectorStateMessage::new(snapshot))?]) } async fn roller(&self, account: AccountRef) -> RpcResult { @@ -197,6 +201,7 @@ impl GameApi for GameService { .sessions .get_or_open(account, &profile, unix_seconds() as i32) .await; + let mut came_back_from_battle = false; let (home_data, checksum, cycle, on_sale): ( OwnHomeDataMessage, i32, @@ -205,8 +210,8 @@ impl GameApi for GameService { ) = { let mut home = session.lock().await; if kind == HomeRequestKind::GoHome && home.is_home_logic_stopped() { - tracing::info!(%account, "client came back from the battle, resuming home logic"); home.resume_home_logic(); + came_back_from_battle = true; } ( home.own_home_data(self.config.random_seed), @@ -219,6 +224,16 @@ impl GameApi for GameService { .collect(), ) }; + if came_back_from_battle { + let battle = self.running_battles.finish(account).await; + tracing::info!( + %account, + seconds = battle.as_ref().map(|state| state.seconds()), + finished = battle.as_ref().map(|state| state.is_finished()), + stars = ?battle.as_ref().map(|state| state.stars()), + "client came back from the battle, resuming home logic" + ); + } tracing::info!( %account, ?kind, @@ -291,10 +306,13 @@ impl GameApi for GameService { (home.end_client_turn(&turn, &roller, &self.shop), in_battle) }; if in_battle { + let progress = self.running_battles.advance(account, turn.tick).await; tracing::info!( %account, tick = turn.tick, - checksum = turn.checksum, + seconds = progress.map(|state| state.seconds), + finished = progress.map(|state| state.finished), + stars = ?progress.map(|state| state.stars), commands = ?turn .commands .iter() diff --git a/crates/logic/src/battle/logic_battle.rs b/crates/logic/src/battle/logic_battle.rs index b1540d3..3efbf6c 100644 --- a/crates/logic/src/battle/logic_battle.rs +++ b/crates/logic/src/battle/logic_battle.rs @@ -1,4 +1,5 @@ use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, Payload, Result}; +use crate::battle::logic_game_object::LogicGameObjectEntry; use crate::battle::logic_game_object_manager::LogicGameObjectManager; use crate::battle::logic_game_object_ref::LogicGameObjectRef; use crate::data::LogicDataRef; @@ -7,6 +8,11 @@ pub const BATTLE_TYPE_PVP: i32 = 0; pub const BATTLE_TYPE_NPC: i32 = 1; pub const BATTLE_TYPE_REPLAY: i32 = 3; pub const BATTLE_INT_ARRAY: usize = 8; +pub const BATTLE_TICKS_PER_SECOND: i32 = 20; +pub const LEADER_TOWER_COUNT: i32 = 2; +pub const CROWNS_FOR_LEADER: i32 = 3; +pub const LOCATION_MATCH_LENGTH_COLUMN: &str = "MatchLength"; +pub const LOCATION_OVERTIME_COLUMN: &str = "OvertimeSeconds"; pub const BATTLE_TRAILING_INTS: usize = 6; #[derive(Debug, Clone, PartialEq, Eq)] pub struct LogicBattle { @@ -67,6 +73,61 @@ impl LogicBattle { pub fn is_npc_battle(&self) -> bool { self.battle_type == BATTLE_TYPE_NPC && !self.npc.is_none() } + pub fn match_length_seconds(&self) -> i32 { + self.location + .data() + .map(|row| row.int(LOCATION_MATCH_LENGTH_COLUMN)) + .unwrap_or(0) + } + pub fn overtime_length_seconds(&self) -> i32 { + self.location + .data() + .map(|row| row.int(LOCATION_OVERTIME_COLUMN)) + .unwrap_or(0) + } + pub fn leader(&self, index: usize) -> Option<&LogicGameObjectEntry> { + let id = self.leaders.get(index)?; + self.objects + .objects + .iter() + .find(|entry| entry.global_id == *id) + } + pub fn is_leader_alive(&self, index: usize) -> bool { + self.leader(index).map(|entry| entry.is_alive()).unwrap_or(false) + } + pub fn stars(&self, index: usize) -> i32 { + let other = 1 - index.min(1); + if !self.is_leader_alive(other) { + return CROWNS_FOR_LEADER; + } + let standing = self + .leader_towers + .get(other) + .map(|towers| towers.len() as i32) + .unwrap_or(0) + .min(LEADER_TOWER_COUNT); + LEADER_TOWER_COUNT - standing + } + pub fn is_end_condition_matched(&self, tick: i32) -> bool { + if self.end_counter > 0 { + return true; + } + if !self.is_leader_alive(0) || !self.is_leader_alive(1) { + return true; + } + let match_length = self.match_length_seconds(); + if match_length < 1 { + return false; + } + let seconds = tick / BATTLE_TICKS_PER_SECOND; + if seconds >= match_length + self.overtime_length_seconds() { + return true; + } + if seconds < match_length { + return false; + } + self.stars(0) != self.stars(1) + } } impl Payload for LogicBattle { fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { diff --git a/crates/logic/src/battle/logic_game_object.rs b/crates/logic/src/battle/logic_game_object.rs index 41e6876..7fed6a3 100644 --- a/crates/logic/src/battle/logic_game_object.rs +++ b/crates/logic/src/battle/logic_game_object.rs @@ -58,6 +58,21 @@ pub struct LogicGameObjectEntry { pub components: [Option; COMPONENT_PASSES], } impl LogicGameObjectEntry { + pub fn hitpoints(&self) -> Option { + match self + .components + .get(crate::battle::logic_component::COMPONENT_HITPOINT)? + .as_ref()? + { + crate::battle::logic_component::LogicComponent::Hitpoint(component) => { + Some(component.hitpoints) + } + _ => None, + } + } + pub fn is_alive(&self) -> bool { + self.hitpoints().map(|value| value > 0).unwrap_or(false) + } pub fn new( data: LogicDataRef, global_id: LogicGameObjectRef, diff --git a/crates/logic/src/battle/mod.rs b/crates/logic/src/battle/mod.rs index ba3a5e4..40dc47c 100644 --- a/crates/logic/src/battle/mod.rs +++ b/crates/logic/src/battle/mod.rs @@ -11,8 +11,8 @@ mod logic_time; mod logic_tutorial_manager; mod verify; pub use logic_battle::{ - LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP, - BATTLE_TYPE_REPLAY, + LogicBattle, BATTLE_INT_ARRAY, BATTLE_TICKS_PER_SECOND, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, + BATTLE_TYPE_PVP, BATTLE_TYPE_REPLAY, CROWNS_FOR_LEADER, LEADER_TOWER_COUNT, }; pub use logic_character::{ LogicCharacter, LogicSummoner, LogicSummonerDeck, DEFAULT_SIZE, DIRECTION_BOTTOM, DIRECTION_TOP,