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 logic::{table, LogicGlobals}; use service_rpc::AccountRef; use tokio::sync::{Mutex, RwLock}; use crate::home::{build_avatar, build_home}; use crate::rewards::RewardRoller; use crate::shop::{Purchase, ShopCatalog, ShopCycle, ShopEntry}; use crate::store::{OwnedCard, PlayerProfile, StoredChest}; pub const MAX_FAST_FORWARD_TICKS: i32 = 20 * 60 * 60; pub const CHEST_FREE: &str = "Free"; pub const CHEST_CROWN: &str = "Star"; fn gold() -> LogicDataRef { LogicDataRef::by_name(table::RESOURCES, RESOURCE_GOLD) } fn diamonds() -> LogicDataRef { LogicDataRef::by_name(table::RESOURCES, RESOURCE_DIAMONDS) } #[derive(Debug, Default)] pub struct TurnResult { pub claims: Vec, pub purchases: Vec, pub matchmake: bool, pub out_of_sync: Option, pub changed: bool, } pub struct HomeMode { logic: LogicHomeMode, home_logic_stopped: bool, } impl HomeMode { pub fn from_profile(profile: &PlayerProfile, current_timestamp: i32) -> Self { let mut logic = LogicHomeMode::new( build_home(profile), build_avatar(profile), current_timestamp, ); logic.restore_free_chest_timer(profile.free_chest_end_timestamp); let cycle = ShopCycle::at(current_timestamp); logic.set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index); Self { logic, home_logic_stopped: false, } } pub fn stop_home_logic(&mut self) { self.home_logic_stopped = true; } pub fn shop_cycle(&self) -> ShopCycle { ShopCycle::at(self.logic.current_timestamp()) } pub fn refresh_shop(&mut self) -> Option { let cycle = self.shop_cycle(); if self.logic.home().shop_seed == cycle.seed { return None; } self.logic .set_shop(cycle.seed, cycle.seconds_to_cycle, cycle.weekday_index); Some(cycle) } 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 chest_for(&self, source: i32, chest_id: i32) -> LogicDataRef { let arena = &self.logic.avatar().arena; match source { chest_source::SLOT => self .logic .chest_with_id(chest_id) .map(|chest| chest.data.clone()) .unwrap_or_default(), chest_source::CROWN => LogicDataRef::treasure_chest_for_arena(CHEST_CROWN, arena), _ => LogicDataRef::treasure_chest_for_arena(CHEST_FREE, arena), } } fn market_cost(&self, give: &ShopEntry) -> Result { if let Some(chest) = give.data.as_treasure_chest() { let price = chest.shop_price(); if price < 1 { return Err("that chest is not sold in the shop"); } return Ok(ShopEntry::new(diamonds(), price)); } if give.is_resource() { return Ok(ShopEntry::new( diamonds(), LogicGlobals::resource_diamond_cost(give.count), )); } Err("that item has no market price") } fn pay(&mut self, cost: &ShopEntry) -> Result<(), &'static str> { if !cost.is_resource() { return Err("the price must be a resource"); } match cost.data.name() { RESOURCE_GOLD if self.logic.spend_gold(cost.count) => Ok(()), RESOURCE_DIAMONDS if self.logic.spend_diamonds(cost.count) => Ok(()), RESOURCE_GOLD | RESOURCE_DIAMONDS => Err("the player cannot afford it"), _ => Err("only gold and diamonds are accepted"), } } fn buy_card(&mut self, card: &LogicDataRef) -> Result { let spell = card.as_spell().ok_or("that is not a card")?; let buy_times = self .logic .shop_buy_times(card) .ok_or("that card is not on sale right now")?; if buy_times >= spell.shop_buy_limit() { return Err("the buy limit for that card is reached"); } let cost = ShopEntry::new(gold(), spell.cost_in_shop(buy_times)); self.pay(&cost)?; self.logic.increase_sold_spells(card); self.logic.grant_card(card.clone(), 1); Ok(Purchase { offer_id: 0, give: ShopEntry::new(card.clone(), 1), cost, }) } fn purchase( &mut self, data: &LogicDataRef, count: i32, shop: &ShopCatalog, ) -> Result { if data.as_spell().is_some() { return self.buy_card(data); } let offer = shop .offer_for(data) .filter(|offer| offer.give.count == count) .ok_or("nothing in the shop sells that")? .clone(); let cost = match &offer.cost { Some(cost) => cost.clone(), None => self.market_cost(&offer.give)?, }; self.pay(&cost)?; 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"), } } Ok(Purchase { offer_id: offer.id, give: offer.give, cost, }) } pub fn end_client_turn( &mut self, turn: &EndClientTurnMessage, roller: &RewardRoller, shop: &ShopCatalog, ) -> TurnResult { let mut result = TurnResult::default(); if self.home_logic_stopped { return result; } 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 } => { let chest = self.chest_for(source, chest_id); pending.push((source, chest_id, chest)); } 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(purchase) => { if purchase.give.is_chest() { pending.push(( chest_source::PURCHASED, 0, purchase.give.data.clone(), )); } result.changed = true; result.purchases.push(purchase); } Err(reason) => { tracing::warn!( command = command.command_type(), item = %data, reason, "shop purchase rejected" ); } } } CommandOutcome::MatchmakeStarted => result.matchmake = true, 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, }); } let arena = self.logic.avatar().arena.clone(); for (source, chest_id, chest) in pending { let rolled = roller.roll(&chest, &arena); tracing::info!( chest = %chest, gold = rolled.gold, cards = rolled.spell_count(), copies = rolled.card_copies(), "chest opened" ); 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.free_chest_end_timestamp = self.logic.free_chest_end_timestamp(); 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>>>, } impl HomeModeRegistry { pub async fn get_or_open( &self, account: AccountRef, profile: &PlayerProfile, current_timestamp: i32, ) -> Arc> { 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>> { 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) }