From 573d70ff0834740116c21d4c696e084756c7f1ef Mon Sep 17 00:00:00 2001 From: WiseDev <83840010+wisedevik@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:24:30 +0300 Subject: [PATCH] deck swapping and achievement claims 500 LogicSwapSpellsCommand does the real work, 520 and 521 are no-ops in this build so they just decode. 535 puts its data reference BEFORE the base header, unlike every other command. collection only shrinks when a card moves into an empty deck slot, that is the one case where the home checksum moves. --- crates/logic/src/commands/achievement.rs | 37 +++++ crates/logic/src/commands/deck.rs | 171 +++++++++++++++++++++++ crates/logic/src/commands/mod.rs | 8 ++ crates/logic/src/commands/spells.rs | 5 +- crates/logic/src/home/logic_home_mode.rs | 38 +++++ crates/logic/src/lib.rs | 10 +- crates/logic/src/model/spell.rs | 35 +++++ 7 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 crates/logic/src/commands/achievement.rs create mode 100644 crates/logic/src/commands/deck.rs diff --git a/crates/logic/src/commands/achievement.rs b/crates/logic/src/commands/achievement.rs new file mode 100644 index 0000000..3e0a8b9 --- /dev/null +++ b/crates/logic/src/commands/achievement.rs @@ -0,0 +1,37 @@ +use logic_derive::Command; +use titan::Payload; +use crate::commands::command::{CommandOutcome, Execute}; +use crate::commands::command_type; +use crate::commands::header::LogicCommandHeader; +use crate::data::LogicDataRef; +use crate::home::LogicHomeMode; +pub const COMMODITY_ACHIEVEMENT_PROGRESS: usize = 2; +pub const COMMODITY_ACHIEVEMENT_CLAIMED: usize = 3; +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::CLAIM_ACHIEVEMENT_REWARD)] +pub struct LogicClaimAchievementRewardCommand { + pub achievement: LogicDataRef, + pub header: LogicCommandHeader, +} +impl Execute for LogicClaimAchievementRewardCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + let Some(data) = self.achievement.data().cloned() else { + return CommandOutcome::Rejected("no achievement was named"); + }; + let required = data.int("ActionCount"); + let progress = mode.commodity_count(COMMODITY_ACHIEVEMENT_PROGRESS, &self.achievement); + if progress < required { + return CommandOutcome::Rejected("the achievement is not completed"); + } + if mode.commodity_count(COMMODITY_ACHIEVEMENT_CLAIMED, &self.achievement) > 0 { + return CommandOutcome::Rejected("the reward was already claimed"); + } + let diamonds = data.int("DiamondReward"); + mode.add_exp(data.int("ExpReward")); + if diamonds >= 1 { + mode.add_free_diamonds(diamonds); + } + mode.commodity_change(COMMODITY_ACHIEVEMENT_CLAIMED, &self.achievement, 1); + CommandOutcome::Applied + } +} diff --git a/crates/logic/src/commands/deck.rs b/crates/logic/src/commands/deck.rs new file mode 100644 index 0000000..cd6b2e8 --- /dev/null +++ b/crates/logic/src/commands/deck.rs @@ -0,0 +1,171 @@ +use logic_derive::Command; +use titan::Payload; +use crate::commands::command::{CommandOutcome, Execute}; +use crate::commands::command_type; +use crate::commands::header::LogicCommandHeader; +use crate::home::LogicHomeMode; +use crate::model::{LogicSpell, DECK_SLOT_COUNT}; +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::SWAP_SPELLS)] +pub struct LogicSwapSpellsCommand { + pub header: LogicCommandHeader, + #[codec(vint)] + pub index1: i32, + #[codec(vint)] + pub index2: i32, + #[codec(bool)] + pub index1_in_deck: bool, + #[codec(bool)] + pub index2_in_deck: bool, +} +struct Slot { + index: usize, + in_deck: bool, + spell: Option, +} +impl LogicSwapSpellsCommand { + fn resolve( + &self, + mode: &LogicHomeMode, + index: i32, + in_deck: bool, + ) -> Result { + let deck = mode + .home() + .decks + .get(mode.selected_deck_index()) + .ok_or(CommandOutcome::Rejected("no selected deck"))?; + let collection = &mode.home().spell_collection; + if in_deck { + let slot = usize::try_from(index) + .ok() + .filter(|slot| *slot < DECK_SLOT_COUNT) + .ok_or(CommandOutcome::Rejected("deck slot out of range"))?; + Ok(Slot { + index: slot, + in_deck, + spell: deck.spell(slot).cloned(), + }) + } else { + let slot = usize::try_from(index) + .ok() + .filter(|slot| *slot < collection.spells.len()) + .ok_or(CommandOutcome::Rejected("collection slot out of range"))?; + Ok(Slot { + index: slot, + in_deck, + spell: collection.spell(slot).cloned(), + }) + } + } +} +impl Execute for LogicSwapSpellsCommand { + fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { + if self.index1 == self.index2 && self.index1_in_deck == self.index2_in_deck { + return CommandOutcome::Rejected("both sides name the same slot"); + } + if self.index1 < 0 && self.index2 < 0 { + return CommandOutcome::Rejected("neither slot is addressable"); + } + let first = match self.resolve(mode, self.index1, self.index1_in_deck) { + Ok(slot) => slot, + Err(outcome) => return outcome, + }; + let second = match self.resolve(mode, self.index2, self.index2_in_deck) { + Ok(slot) => slot, + Err(outcome) => return outcome, + }; + let (held, other) = if first.spell.is_some() { + (first, second) + } else { + (second, first) + }; + let Some(moving) = held.spell.clone() else { + return CommandOutcome::Rejected("both slots are empty"); + }; + match (held.in_deck, other.in_deck) { + (true, true) => { + let Some(deck) = mode.selected_deck_mut() else { + return CommandOutcome::Rejected("no selected deck"); + }; + deck.set_spell(other.index, Some(moving)); + deck.set_spell(held.index, other.spell); + CommandOutcome::Applied + } + (false, false) => { + let collection = mode.collection_mut(); + if let Some(displaced) = other.spell { + collection.set_spell(other.index, moving); + collection.set_spell(held.index, displaced); + } + CommandOutcome::Applied + } + _ => self.mixed(mode, held, other, moving), + } + } +} +impl LogicSwapSpellsCommand { + fn mixed( + &self, + mode: &mut LogicHomeMode, + held: Slot, + other: Slot, + moving: LogicSpell, + ) -> CommandOutcome { + let (deck_slot, collection_slot, incoming, displaced) = if held.in_deck { + if other.spell.is_none() { + return CommandOutcome::Rejected( + "a deck card cannot go to an empty collection slot", + ); + } + ( + held.index, + other.index, + other.spell.clone().unwrap(), + Some(moving), + ) + } else { + (other.index, held.index, moving, other.spell.clone()) + }; + if let Some(displaced) = displaced.as_ref() { + if displaced.level_index == incoming.level_index && displaced.data == incoming.data { + return CommandOutcome::Rejected("that swap is a card upgrade"); + } + } + let can_insert = mode + .home() + .decks + .get(mode.selected_deck_index()) + .map(|deck| deck.can_be_inserted(&incoming, deck_slot)) + .unwrap_or(false); + if !can_insert { + return CommandOutcome::Rejected("the deck already holds that card"); + } + let Some(deck) = mode.selected_deck_mut() else { + return CommandOutcome::Rejected("no selected deck"); + }; + deck.set_spell(deck_slot, Some(incoming)); + let collection = mode.collection_mut(); + match displaced { + Some(displaced) => collection.set_spell(collection_slot, displaced), + None => { + collection.remove_spell(collection_slot); + } + } + CommandOutcome::Applied + } +} +#[derive(Debug, Default, Payload, Command)] +#[command(id = command_type::MOVE_SPELL)] +pub struct LogicMoveSpellCommand { + pub header: LogicCommandHeader, + #[codec(vint)] + pub index: i32, + #[codec(bool)] + pub in_deck: bool, +} +impl Execute for LogicMoveSpellCommand { + fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome { + CommandOutcome::Ignored + } +} diff --git a/crates/logic/src/commands/mod.rs b/crates/logic/src/commands/mod.rs index 4842026..4217303 100644 --- a/crates/logic/src/commands/mod.rs +++ b/crates/logic/src/commands/mod.rs @@ -1,16 +1,23 @@ +mod achievement; mod chest; mod command; +mod deck; mod header; mod manager; mod reward; mod shop; mod spells; mod ui; +pub use achievement::{ + LogicClaimAchievementRewardCommand, COMMODITY_ACHIEVEMENT_CLAIMED, + COMMODITY_ACHIEVEMENT_PROGRESS, +}; pub use chest::{ LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, LogicStartRewardClaimCommand, }; pub use command::{CommandMeta, CommandOutcome, CommandRegistryEntry, Execute, LogicCommand}; +pub use deck::{LogicMoveSpellCommand, LogicSwapSpellsCommand}; pub use header::LogicCommandHeader; pub use manager::LogicCommandManager; pub use reward::LogicReward; @@ -46,6 +53,7 @@ pub mod command_type { pub const BUY_CARD: i32 = 530; pub const HELP_OPENED: i32 = 531; pub const SHOP_OPENED: i32 = 532; + pub const CLAIM_ACHIEVEMENT_REWARD: i32 = 535; pub const REFRESH_ACHIEVEMENTS: i32 = 538; pub const PAGE_OPENED: i32 = 539; } diff --git a/crates/logic/src/commands/spells.rs b/crates/logic/src/commands/spells.rs index 48a6945..9b69d4a 100644 --- a/crates/logic/src/commands/spells.rs +++ b/crates/logic/src/commands/spells.rs @@ -24,8 +24,7 @@ pub struct LogicSortCollectionCommand { pub sort_mode: i32, } impl Execute for LogicSortCollectionCommand { - fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { - mode.home_mut().spell_collection.current_sort = self.sort_mode; - CommandOutcome::Applied + fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome { + CommandOutcome::Ignored } } diff --git a/crates/logic/src/home/logic_home_mode.rs b/crates/logic/src/home/logic_home_mode.rs index f8f1abf..19840e9 100644 --- a/crates/logic/src/home/logic_home_mode.rs +++ b/crates/logic/src/home/logic_home_mode.rs @@ -108,6 +108,44 @@ impl LogicHomeMode { pub fn begin_claim(&mut self) { self.claiming_reward = true; } + pub fn selected_deck_index(&self) -> usize { + self.home.selected_deck_index.max(0) as usize + } + pub fn selected_deck_mut(&mut self) -> Option<&mut crate::model::LogicSpellDeck> { + let index = self.selected_deck_index(); + self.home.decks.get_mut(index) + } + pub fn collection_mut(&mut self) -> &mut crate::model::LogicSpellCollection { + &mut self.home.spell_collection + } + pub fn commodity_mut(&mut self, kind: usize) -> Option<&mut Vec> { + self.avatar.commodities.types.get_mut(kind) + } + pub fn commodity_count(&self, kind: usize, data: &LogicDataRef) -> i32 { + self.avatar + .commodities + .types + .get(kind) + .and_then(|slots| slots.iter().find(|slot| &slot.data == data)) + .map(|slot| slot.count) + .unwrap_or(0) + } + pub fn commodity_change(&mut self, kind: usize, data: &LogicDataRef, delta: i32) { + let Some(slots) = self.avatar.commodities.types.get_mut(kind) else { + return; + }; + match slots.iter_mut().find(|slot| &slot.data == data) { + Some(slot) => slot.count = slot.count.saturating_add(delta).max(0), + None => slots.push(LogicDataSlot::new(data.clone(), delta.max(0))), + } + } + pub fn add_exp(&mut self, amount: i32) { + self.avatar.exp_points = self.avatar.exp_points.saturating_add(amount); + } + pub fn add_free_diamonds(&mut self, amount: i32) { + self.avatar.diamonds = self.avatar.diamonds.saturating_add(amount); + self.avatar.free_diamonds = self.avatar.free_diamonds.saturating_add(amount); + } pub fn free_chest_ready(&self) -> bool { self.home.free_chest_timer.remaining_ticks <= 0 } diff --git a/crates/logic/src/lib.rs b/crates/logic/src/lib.rs index 8712947..dc66123 100644 --- a/crates/logic/src/lib.rs +++ b/crates/logic/src/lib.rs @@ -7,11 +7,13 @@ pub mod messages; pub mod model; pub use commands::{ chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand, - LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimRewardCommand, - LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, LogicCommand, - LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand, LogicHelpOpenedCommand, - LogicPageOpenedCommand, LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand, + LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimAchievementRewardCommand, + LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, + LogicCommand, LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand, + LogicHelpOpenedCommand, LogicMoveSpellCommand, LogicPageOpenedCommand, + LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand, LogicShopSeedChangedCommand, LogicSortCollectionCommand, LogicStartRewardClaimCommand, + LogicSwapSpellsCommand, }; pub use data::{ table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable, diff --git a/crates/logic/src/model/spell.rs b/crates/logic/src/model/spell.rs index 6b979d3..d912da8 100644 --- a/crates/logic/src/model/spell.rs +++ b/crates/logic/src/model/spell.rs @@ -65,6 +65,24 @@ impl LogicSpellDeck { pub fn filled_slot_count(&self) -> usize { self.slots.iter().flatten().count() } + pub fn spell(&self, index: usize) -> Option<&LogicSpell> { + self.slots.get(index)?.as_ref() + } + pub fn set_spell(&mut self, index: usize, spell: Option) { + if let Some(slot) = self.slots.get_mut(index) { + *slot = spell; + } + } + pub fn can_be_inserted(&self, spell: &LogicSpell, index: usize) -> bool { + if self.spell(index).map(|held| held.data == spell.data) == Some(true) { + return true; + } + !self + .slots + .iter() + .flatten() + .any(|held| held.data == spell.data) + } } #[derive(Debug, Default, Clone, PartialEq, Eq, Payload)] pub struct LogicSpellCollection { @@ -79,4 +97,21 @@ impl LogicSpellCollection { current_sort: 0, } } + pub fn spell(&self, index: usize) -> Option<&LogicSpell> { + self.spells.get(index) + } + pub fn set_spell(&mut self, index: usize, spell: LogicSpell) { + if let Some(slot) = self.spells.get_mut(index) { + *slot = spell; + } + } + pub fn add_spell(&mut self, spell: LogicSpell) { + self.spells.push(spell); + } + pub fn remove_spell(&mut self, index: usize) -> Option { + if index >= self.spells.len() { + return None; + } + Some(self.spells.remove(index)) + } }