diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index 10bf0c7..ed953fb 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -4,7 +4,8 @@ use logic::battle::{ LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager, LogicGameObjectRef, LogicHitpointComponent, LogicObjectBody, LogicSummoner, LogicSummonerDeck, LogicTilemap, - LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, DIRECTION_BOTTOM, + LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES, + DIRECTION_BOTTOM, DIRECTION_TOP, SUBTILE_UNITS, }; use logic::model::{LogicClientAvatar, LogicSpellDeck}; @@ -14,6 +15,17 @@ pub const CHARACTER_KING_TOWER: &str = "KingTower"; pub const CHARACTER_PRINCESS_TOWER: &str = "PrincessTower"; pub const BUFF_ARRAY_TABLE: i32 = table::DAMAGE_TYPES; pub const START_MANA_GLOBAL: &str = "START_MANA"; +pub const CHARACTER_HITPOINTS_COLUMN: &str = "Hitpoints"; +pub const CHARACTER_SPEED_COLUMN: &str = "Speed"; +pub const SPELL_SUMMON_CHARACTER_COLUMN: &str = "SummonCharacter"; +pub const SPELL_SUMMON_NUMBER_COLUMN: &str = "SummonNumber"; +pub struct CharacterSpec { + pub data: LogicDataRef, + pub instance: i32, + pub position: LogicVector2, + pub owner: i32, + pub level_index: i32, +} pub struct TowerSpec { pub data: LogicDataRef, pub instance: i32, @@ -74,6 +86,91 @@ impl BattleBuilder { } LogicTilemap::load(&self.root, &file_name) } + fn hitpoints_of(data: &LogicDataRef, level_index: i32) -> i32 { + data.data() + .map(|row| row.int_at(CHARACTER_HITPOINTS_COLUMN, level_index.max(0) as usize)) + .filter(|value| *value > 0) + .unwrap_or(1) + } + fn components_for(&self, hitpoints: i32) -> [Option; COMPONENT_PASSES] { + [ + Some(LogicComponent::Combat(LogicCombatComponent::default())), + None, + Some(LogicComponent::Hitpoint(LogicHitpointComponent::healthy( + hitpoints, + ))), + Some(LogicComponent::Buff(LogicCharacterBuffComponent::empty( + self.buff_type_count, + ))), + ] + } + pub fn character(&self, spec: CharacterSpec) -> LogicGameObjectEntry { + let CharacterSpec { + data, + instance, + position, + owner, + level_index, + } = spec; + let hitpoints = Self::hitpoints_of(&data, level_index); + let character = LogicCharacter { + level_index, + base: LogicGameObject { + position, + owner_index: owner, + ..LogicGameObject::default() + }, + direction: LogicVector2::new( + 0, + if owner == 0 { + DIRECTION_TOP + } else { + DIRECTION_BOTTOM + }, + ), + ..LogicCharacter::default() + }; + let components = self.components_for(hitpoints); + LogicGameObjectEntry::new( + data, + LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance), + LogicObjectBody::Character(Box::new(character)), + components, + ) + } + pub fn summon( + &self, + spell: &LogicDataRef, + position: LogicVector2, + owner: i32, + level_index: i32, + first_instance: i32, + ) -> Vec { + let Some(row) = spell.data() else { + return Vec::new(); + }; + let name = row.string(SPELL_SUMMON_CHARACTER_COLUMN).to_owned(); + if name.is_empty() { + return Vec::new(); + } + let data = character_data(&name); + if data.is_none() { + tracing::warn!(spell = %spell, character = name, "the spell summons a character the tables do not have"); + return Vec::new(); + } + let count = row.int(SPELL_SUMMON_NUMBER_COLUMN).max(1); + (0..count) + .map(|index| { + self.character(CharacterSpec { + data: data.clone(), + instance: first_instance + index, + position, + owner, + level_index, + }) + }) + .collect() + } fn tower(&self, spec: TowerSpec) -> LogicGameObjectEntry { let TowerSpec { data, @@ -85,11 +182,7 @@ impl BattleBuilder { summoner, deck_slots, } = spec; - let hitpoints = data - .data() - .map(|row| row.int_at("Hitpoints", level_index.max(0) as usize)) - .filter(|value| *value > 0) - .unwrap_or(1); + let hitpoints = Self::hitpoints_of(&data, level_index); let character = LogicCharacter { level_index, is_leader, @@ -122,16 +215,7 @@ impl BattleBuilder { data, LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance), body, - [ - Some(LogicComponent::Combat(LogicCombatComponent::default())), - None, - Some(LogicComponent::Hitpoint(LogicHitpointComponent::healthy( - hitpoints, - ))), - Some(LogicComponent::Buff(LogicCharacterBuffComponent::empty( - self.buff_type_count, - ))), - ], + self.components_for(hitpoints), ) } pub fn build( diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 5ae66aa..2930a43 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; -use logic::battle::{LogicGameMode, BATTLE_TICKS_PER_SECOND}; +use logic::battle::{LogicGameMode, LogicGameObjectEntry, BATTLE_TICKS_PER_SECOND}; +use logic::LogicDataRef; +use titan::LogicLong; use service_rpc::AccountRef; use tokio::sync::Mutex; pub struct BattleSession { @@ -31,6 +33,36 @@ impl BattleSession { pub fn stars(&self) -> (i32, i32) { (self.mode.battle.stars(0), self.mode.battle.stars(1)) } + 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 { @@ -54,12 +86,42 @@ impl BattleRegistry { seconds: session.seconds(), finished: session.is_finished(), stars: session.stars(), + objects: session.object_count(), }) } + pub async fn play( + &self, + account: AccountRef, + executor: LogicLong, + slot: i32, + position: logic::battle::LogicVector2, + summon: F, + ) -> usize + where + F: Fn(&LogicDataRef, logic::battle::LogicVector2, i32, i32) -> Vec, + { + let mut sessions = self.sessions.lock().await; + let Some(session) = sessions.get_mut(&account) else { + return 0; + }; + 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(); + for entry in entries { + session.push(entry); + } + spawned + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BattleProgress { pub seconds: i32, pub finished: bool, pub stars: (i32, i32), + pub objects: usize, } diff --git a/crates/game-service/src/service.rs b/crates/game-service/src/service.rs index 461bb8a..d665597 100644 --- a/crates/game-service/src/service.rs +++ b/crates/game-service/src/service.rs @@ -306,6 +306,27 @@ impl GameApi for GameService { (home.end_client_turn(&turn, &roller, &self.shop), in_battle) }; if in_battle { + let mut spawned = 0; + for command in &turn.commands { + let Some(played) = command + .as_any() + .downcast_ref::() + else { + continue; + }; + spawned += self + .running_battles + .play( + account, + played.header.executor_account_id, + played.deck_slot, + played.position, + |card, position, owner, instance| { + self.battles.summon(card, position, owner, 0, instance) + }, + ) + .await; + } let progress = self.running_battles.advance(account, turn.tick).await; tracing::info!( %account, @@ -313,6 +334,8 @@ impl GameApi for GameService { seconds = progress.map(|state| state.seconds), finished = progress.map(|state| state.finished), stars = ?progress.map(|state| state.stars), + objects = progress.map(|state| state.objects), + spawned, commands = ?turn .commands .iter() diff --git a/crates/logic/src/lib.rs b/crates/logic/src/lib.rs index 906dc3d..270eb6e 100644 --- a/crates/logic/src/lib.rs +++ b/crates/logic/src/lib.rs @@ -11,7 +11,8 @@ pub use commands::{ chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand, LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimAchievementRewardCommand, LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, - LogicCommand, LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand, + LogicCommand, LogicCommandHeader, LogicCommandManager, LogicCompleteTutorialBattleCommand, + LogicDoSpellCommand, LogicFuseSpellsCommand, LogicHelpOpenedCommand, LogicMoveSpellCommand, LogicPageOpenedCommand, LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand, LogicShopSeedChangedCommand, LogicSortCollectionCommand, LogicStartMatchmakeCommand,