give the server its own battle clock and end condition
LogicBattle::isEndConditionMatched, transcribed: the battle is over when end_counter is positive, when either king is dead, when tick/20 seconds reach MatchLength + OvertimeSeconds, or - once past MatchLength - when the crowns differ. the divisor is the 0x66666667/2^35 multiply in the client, which is a divide by twenty, so the battle runs at 20 ticks a second. crowns come from LogicSummoner::getStars: three when the enemy king is down, otherwise two minus the enemy princess towers still standing, which is exactly what leader_towers holds. BattleRegistry keeps the LogicGameMode we built for each account and advances it on the tick the client reports in its turn message, so the server now tracks the clock, the crowns and whether the battle is over, and drops the session when the player goes home. the two isSummoner guards in the overtime branch are left out: they only fire for an object that is not a summoner, which a king tower always is.
This commit is contained in:
parent
a8dcd683b2
commit
7a0ba1b652
6 changed files with 165 additions and 4 deletions
65
crates/game-service/src/battle_session.rs
Normal file
65
crates/game-service/src/battle_session.rs
Normal file
|
|
@ -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<HashMap<AccountRef, BattleSession>>,
|
||||
}
|
||||
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<BattleSession> {
|
||||
self.sessions.lock().await.remove(&account)
|
||||
}
|
||||
pub async fn advance(&self, account: AccountRef, tick: i32) -> Option<BattleProgress> {
|
||||
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),
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<Catalog>,
|
||||
shop: Arc<ShopCatalog>,
|
||||
battles: Arc<BattleBuilder>,
|
||||
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<RewardRoller> {
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,21 @@ pub struct LogicGameObjectEntry {
|
|||
pub components: [Option<crate::battle::logic_component::LogicComponent>; COMPONENT_PASSES],
|
||||
}
|
||||
impl LogicGameObjectEntry {
|
||||
pub fn hitpoints(&self) -> Option<i32> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue