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 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue