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.
This commit is contained in:
WiseDev 2026-08-23 09:24:30 +03:00
parent 9d10cc38f9
commit 573d70ff08
7 changed files with 297 additions and 7 deletions

View file

@ -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
}
}

View file

@ -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<LogicSpell>,
}
impl LogicSwapSpellsCommand {
fn resolve(
&self,
mode: &LogicHomeMode,
index: i32,
in_deck: bool,
) -> Result<Slot, CommandOutcome> {
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
}
}

View file

@ -1,16 +1,23 @@
mod achievement;
mod chest; mod chest;
mod command; mod command;
mod deck;
mod header; mod header;
mod manager; mod manager;
mod reward; mod reward;
mod shop; mod shop;
mod spells; mod spells;
mod ui; mod ui;
pub use achievement::{
LogicClaimAchievementRewardCommand, COMMODITY_ACHIEVEMENT_CLAIMED,
COMMODITY_ACHIEVEMENT_PROGRESS,
};
pub use chest::{ pub use chest::{
LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand,
LogicStartRewardClaimCommand, LogicStartRewardClaimCommand,
}; };
pub use command::{CommandMeta, CommandOutcome, CommandRegistryEntry, Execute, LogicCommand}; pub use command::{CommandMeta, CommandOutcome, CommandRegistryEntry, Execute, LogicCommand};
pub use deck::{LogicMoveSpellCommand, LogicSwapSpellsCommand};
pub use header::LogicCommandHeader; pub use header::LogicCommandHeader;
pub use manager::LogicCommandManager; pub use manager::LogicCommandManager;
pub use reward::LogicReward; pub use reward::LogicReward;
@ -46,6 +53,7 @@ pub mod command_type {
pub const BUY_CARD: i32 = 530; pub const BUY_CARD: i32 = 530;
pub const HELP_OPENED: i32 = 531; pub const HELP_OPENED: i32 = 531;
pub const SHOP_OPENED: i32 = 532; pub const SHOP_OPENED: i32 = 532;
pub const CLAIM_ACHIEVEMENT_REWARD: i32 = 535;
pub const REFRESH_ACHIEVEMENTS: i32 = 538; pub const REFRESH_ACHIEVEMENTS: i32 = 538;
pub const PAGE_OPENED: i32 = 539; pub const PAGE_OPENED: i32 = 539;
} }

View file

@ -24,8 +24,7 @@ pub struct LogicSortCollectionCommand {
pub sort_mode: i32, pub sort_mode: i32,
} }
impl Execute for LogicSortCollectionCommand { impl Execute for LogicSortCollectionCommand {
fn execute(&self, mode: &mut LogicHomeMode) -> CommandOutcome { fn execute(&self, _mode: &mut LogicHomeMode) -> CommandOutcome {
mode.home_mut().spell_collection.current_sort = self.sort_mode; CommandOutcome::Ignored
CommandOutcome::Applied
} }
} }

View file

@ -108,6 +108,44 @@ impl LogicHomeMode {
pub fn begin_claim(&mut self) { pub fn begin_claim(&mut self) {
self.claiming_reward = true; 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<crate::model::LogicDataSlot>> {
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 { pub fn free_chest_ready(&self) -> bool {
self.home.free_chest_timer.remaining_ticks <= 0 self.home.free_chest_timer.remaining_ticks <= 0
} }

View file

@ -7,11 +7,13 @@ pub mod messages;
pub mod model; pub mod model;
pub use commands::{ pub use commands::{
chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand, chest_source, command_type, CommandMeta, CommandOutcome, Execute, LogicBuyCardCommand,
LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimRewardCommand, LogicBuyChestCommand, LogicBuyResourcePackCommand, LogicClaimAchievementRewardCommand,
LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand, LogicCommand, LogicClaimRewardCommand, LogicCollectFreeChestCommand, LogicCollectMultiWinChestCommand,
LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand, LogicHelpOpenedCommand, LogicCommand, LogicCommandHeader, LogicCommandManager, LogicFuseSpellsCommand,
LogicPageOpenedCommand, LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand, LogicHelpOpenedCommand, LogicMoveSpellCommand, LogicPageOpenedCommand,
LogicRefreshAchievementsCommand, LogicReward, LogicShopOpenedCommand,
LogicShopSeedChangedCommand, LogicSortCollectionCommand, LogicStartRewardClaimCommand, LogicShopSeedChangedCommand, LogicSortCollectionCommand, LogicStartRewardClaimCommand,
LogicSwapSpellsCommand,
}; };
pub use data::{ pub use data::{
table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable, table, DataError, LogicArenaData, LogicData, LogicDataRef, LogicDataTable,

View file

@ -65,6 +65,24 @@ impl LogicSpellDeck {
pub fn filled_slot_count(&self) -> usize { pub fn filled_slot_count(&self) -> usize {
self.slots.iter().flatten().count() 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<LogicSpell>) {
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)] #[derive(Debug, Default, Clone, PartialEq, Eq, Payload)]
pub struct LogicSpellCollection { pub struct LogicSpellCollection {
@ -79,4 +97,21 @@ impl LogicSpellCollection {
current_sort: 0, 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<LogicSpell> {
if index >= self.spells.len() {
return None;
}
Some(self.spells.remove(index))
}
} }