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:
parent
9d10cc38f9
commit
573d70ff08
7 changed files with 297 additions and 7 deletions
37
crates/logic/src/commands/achievement.rs
Normal file
37
crates/logic/src/commands/achievement.rs
Normal 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
|
||||
}
|
||||
}
|
||||
171
crates/logic/src/commands/deck.rs
Normal file
171
crates/logic/src/commands/deck.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
self.home.free_chest_timer.remaining_ticks <= 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<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)]
|
||||
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<LogicSpell> {
|
||||
if index >= self.spells.len() {
|
||||
return None;
|
||||
}
|
||||
Some(self.spells.remove(index))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue