use std::path::{Path, PathBuf}; use logic::battle::{ LogicBattle, LogicCharacter, LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent, LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager, LogicGameObjectRef, LogicHitpointComponent, LogicMovementComponent, LogicObjectBody, LogicSummoner, LogicSummonerDeck, LogicTilemap, LogicTime, LogicVector2, BATTLE_TYPE_PVP, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES, DIRECTION_BOTTOM, DIRECTION_TOP, SUBTILE_UNITS, }; use logic::model::{LogicClientAvatar, LogicSpellDeck}; use logic::{table, LogicDataRef, LogicDataTables, LogicGlobals, LogicRandom}; pub const LOCATION_FILE_COLUMN: &str = "FileName"; 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, pub tile: (i32, i32), pub owner: i32, pub level_index: i32, pub is_leader: bool, pub summoner: bool, pub deck_slots: usize, } pub struct BattleBuilder { root: PathBuf, buff_type_count: usize, start_mana: i32, } fn tile_to_units(tile: i32) -> i32 { tile.saturating_mul(SUBTILE_UNITS) } fn character_data(name: &str) -> LogicDataRef { let combined = LogicDataRef::by_name(table::CHARACTERS_COMBINED, name); if !combined.is_none() { return combined; } let building = LogicDataRef::by_name(table::BUILDINGS, name); if !building.is_none() { return building; } LogicDataRef::by_name(table::CHARACTERS, name) } impl BattleBuilder { pub fn new(root: impl AsRef) -> Self { let buff_type_count = LogicDataTables::instance() .table(BUFF_ARRAY_TABLE) .map(|rows| rows.count()) .unwrap_or(0); tracing::info!( buff_type_count, king_id = ?character_data(CHARACTER_KING_TOWER).global_id(), princess_id = ?character_data(CHARACTER_PRINCESS_TOWER).global_id(), "battle builder ready" ); Self { root: root.as_ref().to_path_buf(), buff_type_count, start_mana: LogicGlobals::number(START_MANA_GLOBAL), } } pub fn buff_type_count(&self) -> usize { self.buff_type_count } pub fn tilemap_for(&self, location: &LogicDataRef) -> std::io::Result { let file_name = location .data() .map(|data| data.string(LOCATION_FILE_COLUMN).to_owned()) .unwrap_or_default(); if file_name.is_empty() { return Err(std::io::Error::other("the location names no tilemap")); } 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, moves: bool) -> [Option; COMPONENT_PASSES] { [ Some(LogicComponent::Combat(LogicCombatComponent::default())), moves.then(|| LogicComponent::Movement(LogicMovementComponent::default())), 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 moves = data .data() .map(|row| row.int(CHARACTER_SPEED_COLUMN) > 0) .unwrap_or(false); 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, moves); 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, instance, tile, owner, level_index, is_leader, summoner, deck_slots, } = spec; let hitpoints = Self::hitpoints_of(&data, level_index); let character = LogicCharacter { level_index, is_leader, base: LogicGameObject { position: LogicVector2::new(tile_to_units(tile.0), tile_to_units(tile.1)), owner_index: owner, ..LogicGameObject::default() }, direction: LogicVector2::new( 0, if owner == 0 { DIRECTION_TOP } else { DIRECTION_BOTTOM }, ), ..LogicCharacter::default() }; let body = if summoner { LogicObjectBody::Summoner(Box::new(LogicSummoner { character, deck: Some(LogicSummonerDeck::starting(deck_slots)), mana: self.start_mana, ..LogicSummoner::default() })) } else { LogicObjectBody::Character(Box::new(character)) }; LogicGameObjectEntry::new( data, LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance), body, self.components_for(hitpoints, false), ) } pub fn build( &self, location: LogicDataRef, npc: LogicDataRef, arena: LogicDataRef, avatars: Vec, decks: [Option; 2], random_seed: i32, ) -> std::io::Result { let tilemap = self.tilemap_for(&location)?; let height = tilemap.height(); let middle = tile_to_units(height) / 2; let king = character_data(CHARACTER_KING_TOWER); let princess = character_data(CHARACTER_PRINCESS_TOWER); if king.is_none() { return Err(std::io::Error::other( "no KingTower row in the character tables", )); } let deck_slots = [ decks[0].as_ref().map(LogicSpellDeck::filled_slot_count).unwrap_or(0), decks[1].as_ref().map(LogicSpellDeck::filled_slot_count).unwrap_or(0), ]; let mut objects = LogicGameObjectManager::default(); let mut leaders = [LogicGameObjectRef::NONE; 2]; let mut towers: [Vec; 2] = [Vec::new(), Vec::new()]; let mut instance = 0; for tile in tilemap.king_towers() { let owner = usize::from(tile_to_units(tile.1) >= middle); let entry = self.tower(TowerSpec { data: king.clone(), instance, tile: *tile, owner: owner as i32, level_index: 0, is_leader: true, summoner: true, deck_slots: deck_slots[owner], }); leaders[owner] = entry.global_id; objects.push(entry); instance += 1; } if !princess.is_none() { for tile in tilemap.princess_towers() { let owner = usize::from(tile_to_units(tile.1) >= middle); let entry = self.tower(TowerSpec { data: princess.clone(), instance, tile: *tile, owner: owner as i32, level_index: 0, is_leader: false, summoner: false, deck_slots: 0, }); towers[owner].push(entry.global_id); objects.push(entry); instance += 1; } } if leaders.iter().any(LogicGameObjectRef::is_none) { return Err(std::io::Error::other(format!( "{} has {} king towers, the client needs exactly two", location, tilemap.king_towers().len() ))); } let account_ids = [ avatars .first() .map(|avatar| avatar.account_id) .unwrap_or_default(), avatars .get(1) .map(|avatar| avatar.account_id) .unwrap_or_default(), ]; let battle = LogicBattle { tilemap: Some(tilemap), location, npc, arena, account_ids, battle_type: BATTLE_TYPE_PVP, decks, objects, leaders, leader_towers: towers, ..LogicBattle::default() }; Ok(LogicGameMode { random: LogicRandom::new(random_seed), random_seed, time: LogicTime::default(), battle, avatars, ..LogicGameMode::default() }) } }