json stores are gone, they nulled out every data ref on load and wrote the null straight back on save. shop json and the purchase commands ended up in here too.
246 lines
8.9 KiB
Rust
246 lines
8.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use logic::data::{RESOURCE_DIAMONDS, RESOURCE_GOLD};
|
|
use logic::{
|
|
chest_source, AvailableServerCommandMessage, CommandOutcome, EndClientTurnMessage,
|
|
LogicClaimRewardCommand, LogicDataRef, LogicHomeMode, LogicTimer, OutOfSyncMessage,
|
|
OwnHomeDataMessage,
|
|
};
|
|
use service_rpc::AccountRef;
|
|
use tokio::sync::{Mutex, RwLock};
|
|
use crate::config::ChestRewardConfig;
|
|
use crate::home::{build_avatar, build_home};
|
|
use crate::rewards::RewardRoller;
|
|
use crate::shop::{ShopCatalog, ShopOffer};
|
|
use crate::store::{OwnedCard, PlayerProfile, StoredChest};
|
|
pub const MAX_FAST_FORWARD_TICKS: i32 = 20 * 60 * 60;
|
|
#[derive(Debug, Default)]
|
|
pub struct TurnResult {
|
|
pub claims: Vec<LogicClaimRewardCommand>,
|
|
pub purchases: Vec<ShopOffer>,
|
|
pub out_of_sync: Option<OutOfSyncMessage>,
|
|
pub changed: bool,
|
|
}
|
|
pub struct HomeMode {
|
|
logic: LogicHomeMode,
|
|
}
|
|
impl HomeMode {
|
|
pub fn from_profile(profile: &PlayerProfile, current_timestamp: i32) -> Self {
|
|
Self {
|
|
logic: LogicHomeMode::new(
|
|
build_home(profile),
|
|
build_avatar(profile),
|
|
current_timestamp,
|
|
),
|
|
}
|
|
}
|
|
pub fn logic(&self) -> &LogicHomeMode {
|
|
&self.logic
|
|
}
|
|
pub fn own_home_data(&self, random_seed: i32) -> OwnHomeDataMessage {
|
|
OwnHomeDataMessage::new(
|
|
self.logic.home().clone(),
|
|
self.logic.avatar().clone(),
|
|
random_seed,
|
|
)
|
|
}
|
|
fn purchase(
|
|
&mut self,
|
|
data: &LogicDataRef,
|
|
count: i32,
|
|
shop: &ShopCatalog,
|
|
) -> Result<ShopOffer, &'static str> {
|
|
let offer = shop
|
|
.offer_for(data)
|
|
.filter(|offer| offer.give.count == count)
|
|
.ok_or("nothing in the shop sells that")?
|
|
.clone();
|
|
if !offer.cost.is_resource() {
|
|
return Err("the price must be a resource");
|
|
}
|
|
match offer.cost.data.name() {
|
|
RESOURCE_GOLD if self.logic.spend_gold(offer.cost.count) => {}
|
|
RESOURCE_DIAMONDS if self.logic.spend_diamonds(offer.cost.count) => {}
|
|
RESOURCE_GOLD | RESOURCE_DIAMONDS => return Err("the player cannot afford it"),
|
|
_ => return Err("only gold and diamonds are accepted"),
|
|
}
|
|
if offer.give.is_resource() {
|
|
match offer.give.data.name() {
|
|
RESOURCE_GOLD => self.logic.add_gold(offer.give.count),
|
|
RESOURCE_DIAMONDS => self.logic.add_diamonds(offer.give.count),
|
|
_ => return Err("only gold and diamonds can be sold"),
|
|
}
|
|
} else if offer.give.is_card() {
|
|
self.logic
|
|
.grant_card(offer.give.data.clone(), offer.give.count);
|
|
}
|
|
Ok(offer)
|
|
}
|
|
pub fn end_client_turn(
|
|
&mut self,
|
|
turn: &EndClientTurnMessage,
|
|
reward: &ChestRewardConfig,
|
|
roller: &RewardRoller,
|
|
shop: &ShopCatalog,
|
|
) -> TurnResult {
|
|
let mut result = TurnResult::default();
|
|
let ticked = self
|
|
.logic
|
|
.fast_forward_to_tick(turn.tick, MAX_FAST_FORWARD_TICKS);
|
|
if ticked > 0 {
|
|
result.changed = true;
|
|
}
|
|
let mut pending = Vec::new();
|
|
for command in &turn.commands {
|
|
match self.logic.execute(command.as_ref()) {
|
|
CommandOutcome::ClaimStarted { source, chest_id } => {
|
|
pending.push((source, chest_id));
|
|
}
|
|
CommandOutcome::Upgraded {
|
|
spell,
|
|
level,
|
|
gold_spent,
|
|
} => {
|
|
tracing::info!(card = %spell, level, gold = gold_spent, "card upgraded");
|
|
result.changed = true;
|
|
}
|
|
CommandOutcome::Rejected(reason) => {
|
|
tracing::warn!(
|
|
command = command.command_type(),
|
|
reason,
|
|
"client command rejected"
|
|
);
|
|
}
|
|
CommandOutcome::PurchaseRequested { data, count } => {
|
|
match self.purchase(&data, count, shop) {
|
|
Ok(offer) => {
|
|
if offer.give.is_chest() {
|
|
pending.push((chest_source::PURCHASED, 0));
|
|
}
|
|
result.changed = true;
|
|
result.purchases.push(offer);
|
|
}
|
|
Err(reason) => {
|
|
tracing::warn!(
|
|
command = command.command_type(),
|
|
item = %data,
|
|
reason,
|
|
"shop purchase rejected"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
CommandOutcome::Applied => result.changed = true,
|
|
CommandOutcome::Ignored => {}
|
|
}
|
|
}
|
|
let server_checksum = self.logic.checksum();
|
|
if server_checksum != turn.checksum {
|
|
result.out_of_sync = Some(OutOfSyncMessage {
|
|
server_checksum,
|
|
client_checksum: turn.checksum,
|
|
tick: turn.tick,
|
|
});
|
|
}
|
|
for (source, chest_id) in pending {
|
|
let rolled = roller.roll(reward);
|
|
self.logic.apply_reward(&rolled);
|
|
result.changed = true;
|
|
result
|
|
.claims
|
|
.push(LogicClaimRewardCommand::new(rolled, chest_id, source));
|
|
}
|
|
result
|
|
}
|
|
pub fn write_back(&self, profile: &mut PlayerProfile) {
|
|
let home = self.logic.home();
|
|
let avatar = self.logic.avatar();
|
|
profile.gold = self.logic.gold();
|
|
profile.diamonds = avatar.diamonds;
|
|
profile.free_diamonds = avatar.free_diamonds;
|
|
profile.exp_level = avatar.exp_level;
|
|
profile.exp_points = avatar.exp_points;
|
|
profile.trophies = avatar.score;
|
|
profile.arena = avatar.arena.clone();
|
|
profile.deck = home
|
|
.decks
|
|
.first()
|
|
.map(|deck| {
|
|
deck.slots
|
|
.iter()
|
|
.flatten()
|
|
.map(|spell| OwnedCard {
|
|
card: spell.data.clone(),
|
|
level_index: spell.level_index,
|
|
count: spell.count,
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
profile.collection = home
|
|
.spell_collection
|
|
.spells
|
|
.iter()
|
|
.map(|spell| OwnedCard {
|
|
card: spell.data.clone(),
|
|
level_index: spell.level_index,
|
|
count: spell.count,
|
|
})
|
|
.collect();
|
|
profile.chest_id_counter = home.chest_id_counter;
|
|
profile.free_chest_collect_count = home.free_chest_collect_count;
|
|
profile.crowns_towards_crown_chest = home.crowns_towards_crown_chest;
|
|
profile.chest_slot_count = home.chest_slots.len();
|
|
profile.chests = home
|
|
.chest_slots
|
|
.iter()
|
|
.flatten()
|
|
.map(|chest| {
|
|
let timer = chest.unlock_timer.unwrap_or(LogicTimer::idle());
|
|
StoredChest {
|
|
chest: chest.data.clone(),
|
|
chest_id: chest.chest_id,
|
|
slot_index: chest.slot_index,
|
|
source: chest.source,
|
|
unlocked: chest.unlocked,
|
|
claimed: chest.claimed,
|
|
remaining_ticks: timer.remaining_ticks,
|
|
total_ticks: timer.total_ticks,
|
|
end_timestamp: timer.end_timestamp,
|
|
}
|
|
})
|
|
.collect();
|
|
}
|
|
}
|
|
#[derive(Default)]
|
|
pub struct HomeModeRegistry {
|
|
sessions: RwLock<HashMap<AccountRef, Arc<Mutex<HomeMode>>>>,
|
|
}
|
|
impl HomeModeRegistry {
|
|
pub async fn get_or_open(
|
|
&self,
|
|
account: AccountRef,
|
|
profile: &PlayerProfile,
|
|
current_timestamp: i32,
|
|
) -> Arc<Mutex<HomeMode>> {
|
|
if let Some(session) = self.sessions.read().await.get(&account) {
|
|
return Arc::clone(session);
|
|
}
|
|
let mut sessions = self.sessions.write().await;
|
|
Arc::clone(sessions.entry(account).or_insert_with(|| {
|
|
Arc::new(Mutex::new(HomeMode::from_profile(
|
|
profile,
|
|
current_timestamp,
|
|
)))
|
|
}))
|
|
}
|
|
pub async fn close(&self, account: AccountRef) -> Option<Arc<Mutex<HomeMode>>> {
|
|
self.sessions.write().await.remove(&account)
|
|
}
|
|
pub async fn session_count(&self) -> usize {
|
|
self.sessions.read().await.len()
|
|
}
|
|
}
|
|
pub fn available_server_command(command: LogicClaimRewardCommand) -> AvailableServerCommandMessage {
|
|
AvailableServerCommandMessage::claim_reward(command)
|
|
}
|