From 3cba764841f768bc1048276ce18781deea4778a2 Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:39:49 +0300 Subject: [PATCH] deal the summoner a starting hand the card bar was empty because the hand lives on the king tower, not on the battle. LogicSummoner::decode gates a whole block behind one boolean - setEncodeDeckDataEnabled on the client side - and we always wrote it false, so the client kept the hand and the draw queue it was born with, which is nothing. the block holds four deck indices for the hand, then two int lists: the draw queue getNextSpell walks and the used pile reshuffleDeck folds back into it. we now hand out the first four slots and queue the rest, leave the used pile empty and keep last used at -1, which is what getLastUsedSpell reads as "none". starting mana comes from the START_MANA global instead of zero, and the field after last-used is named after clearSpellCooldowns, which is the only thing that writes it. --- crates/game-service/src/battle.rs | 18 ++++++++++++++-- crates/logic/src/battle/logic_character.rs | 24 +++++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index 57da1ec..10bf0c7 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -2,16 +2,18 @@ use std::path::{Path, PathBuf}; use logic::battle::{ LogicBattle, LogicCharacter, LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent, LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager, - LogicGameObjectRef, LogicHitpointComponent, LogicObjectBody, LogicSummoner, LogicTilemap, + LogicGameObjectRef, LogicHitpointComponent, LogicObjectBody, LogicSummoner, LogicSummonerDeck, + LogicTilemap, LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, DIRECTION_BOTTOM, DIRECTION_TOP, SUBTILE_UNITS, }; use logic::model::{LogicClientAvatar, LogicSpellDeck}; -use logic::{table, LogicDataRef, LogicDataTables, LogicRandom}; +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 struct TowerSpec { pub data: LogicDataRef, pub instance: i32, @@ -20,10 +22,12 @@ pub struct TowerSpec { 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) @@ -54,6 +58,7 @@ impl BattleBuilder { 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 { @@ -78,6 +83,7 @@ impl BattleBuilder { level_index, is_leader, summoner, + deck_slots, } = spec; let hitpoints = data .data() @@ -105,6 +111,8 @@ impl BattleBuilder { let body = if summoner { LogicObjectBody::Summoner(Box::new(LogicSummoner { character, + deck: Some(LogicSummonerDeck::starting(deck_slots)), + mana: self.start_mana, ..LogicSummoner::default() })) } else { @@ -145,6 +153,10 @@ impl BattleBuilder { "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()]; @@ -159,6 +171,7 @@ impl BattleBuilder { level_index: 0, is_leader: true, summoner: true, + deck_slots: deck_slots[owner], }); leaders[owner] = entry.global_id; objects.push(entry); @@ -175,6 +188,7 @@ impl BattleBuilder { level_index: 0, is_leader: false, summoner: false, + deck_slots: 0, }); towers[owner].push(entry.global_id); objects.push(entry); diff --git a/crates/logic/src/battle/logic_character.rs b/crates/logic/src/battle/logic_character.rs index 186d446..dd6e58d 100644 --- a/crates/logic/src/battle/logic_character.rs +++ b/crates/logic/src/battle/logic_character.rs @@ -3,6 +3,7 @@ use crate::battle::logic_game_object::{LogicGameObject, LogicVector2}; pub const DIRECTION_TOP: i32 = 256; pub const DIRECTION_BOTTOM: i32 = -256; pub const DEFAULT_SIZE: i32 = 100; +pub const SUMMONER_HAND_SIZE: usize = 4; #[derive(Debug, Clone, PartialEq, Eq)] pub struct LogicCharacter { pub level_index: i32, @@ -92,26 +93,39 @@ pub struct LogicSummoner { #[derive(Debug, Clone, PartialEq, Eq)] pub struct LogicSummonerDeck { pub field_220: i32, - pub hand: [i32; 4], + pub hand: [i32; SUMMONER_HAND_SIZE], pub draw_pile: Vec, pub used_pile: Vec, pub last_used_index: i32, - pub field_248: i32, + pub spell_cooldown: i32, pub field_240: i32, } impl Default for LogicSummonerDeck { fn default() -> Self { Self { field_220: 0, - hand: [-1; 4], + hand: [-1; SUMMONER_HAND_SIZE], draw_pile: Vec::new(), used_pile: Vec::new(), last_used_index: -1, - field_248: 0, + spell_cooldown: 0, field_240: 0, } } } +impl LogicSummonerDeck { + pub fn starting(slot_count: usize) -> Self { + let mut hand = [-1; SUMMONER_HAND_SIZE]; + for (slot, index) in hand.iter_mut().zip(0..slot_count as i32) { + *slot = index; + } + Self { + hand, + draw_pile: (SUMMONER_HAND_SIZE as i32..slot_count as i32).collect(), + ..Self::default() + } + } +} impl LogicSummoner { pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> { self.character.encode(writer)?; @@ -132,7 +146,7 @@ impl LogicSummoner { writer.write_vint(*value); } writer.write_vint(deck.last_used_index); - writer.write_vint(deck.field_248); + writer.write_vint(deck.spell_cooldown); writer.write_vint(deck.field_240); } }