diff --git a/crates/game-service/src/home.rs b/crates/game-service/src/home.rs index 830aa67..7f3eeeb 100644 --- a/crates/game-service/src/home.rs +++ b/crates/game-service/src/home.rs @@ -5,6 +5,9 @@ use logic::model::{ use titan::LogicLong; use logic::data::RESOURCE_GOLD; use logic::{table, LogicDataRef}; +use logic::data::LogicDataTables; +use logic::commands::COMMODITY_ACHIEVEMENT_PROGRESS; +use std::sync::Arc; use crate::catalog::GOLD_RESOURCE_FALLBACK_INSTANCE; use crate::store::{OwnedCard, PlayerProfile, StoredChest}; pub const COMMODITY_RESOURCES: usize = 0; @@ -110,6 +113,42 @@ fn gold_resource(profile: &PlayerProfile) -> LogicDataRef { } LogicDataRef::of(table::RESOURCES, GOLD_RESOURCE_FALLBACK_INSTANCE) } +fn achievement_progress(profile: &PlayerProfile) -> Vec { + let tables = LogicDataTables::instance(); + let Some(achievements) = tables.table(table::ACHIEVEMENTS) else { + return Vec::new(); + }; + let distinct_cards = { + let mut seen: Vec = Vec::new(); + for card in profile.deck.iter().chain(profile.collection.iter()) { + if let Some(id) = card.card.global_id() { + if !seen.contains(&id) { + seen.push(id); + } + } + } + seen.len() as i32 + }; + let arenas_reached = profile.arena.instance_id().unwrap_or(0) + 1; + let mut slots = Vec::new(); + for index in 0..achievements.count() { + let Some(row) = achievements.get_at(index) else { + continue; + }; + let progress = match row.string("Action") { + "findcard" => distinct_cards, + "reacharena" => arenas_reached, + _ => 0, + }; + if progress > 0 { + slots.push(LogicDataSlot::new( + LogicDataRef::Resolved(Arc::clone(row)), + progress, + )); + } + } + slots +} pub fn build_avatar(profile: &PlayerProfile) -> LogicClientAvatar { let account = LogicLong::new(profile.account.high, profile.account.low); let mut commodities = LogicCommodityStore::default(); @@ -117,6 +156,7 @@ pub fn build_avatar(profile: &PlayerProfile) -> LogicClientAvatar { COMMODITY_RESOURCES, vec![LogicDataSlot::new(gold_resource(profile), profile.gold)], ); + commodities.set(COMMODITY_ACHIEVEMENT_PROGRESS, achievement_progress(profile)); LogicClientAvatar { avatar_id: account, account_id: account, diff --git a/crates/game-service/tests/achievements_and_levels.rs b/crates/game-service/tests/achievements_and_levels.rs new file mode 100644 index 0000000..142070c --- /dev/null +++ b/crates/game-service/tests/achievements_and_levels.rs @@ -0,0 +1,79 @@ +use std::path::Path; +use std::sync::Arc; +use game_service::home::build_avatar; +use logic::commands::COMMODITY_ACHIEVEMENT_PROGRESS; +use logic::{CommandOutcome, LogicClaimAchievementRewardCommand, LogicCommandHeader}; +use logic::data::{table, LogicDataRef, LogicDataTables}; +use logic::home::LogicHomeMode; +use logic::model::LogicClientHome; +fn tables() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets"); + if let Ok(t) = LogicDataTables::load_from_dir(&root) { + LogicDataTables::install(Arc::new(t)); + } +} +#[test] +fn adding_exp_rolls_over_into_levels() { + tables(); + let mut home = LogicHomeMode::new(LogicClientHome::default(), Default::default(), 0); + home.avatar_mut().exp_level = 1; + home.avatar_mut().exp_points = 0; + home.add_exp(25); + assert_eq!(home.avatar().exp_level, 2, "25 exp should reach level 2"); + assert_eq!(home.avatar().exp_points, 5, "5 exp should carry into level 2"); + home.add_exp(50); + assert_eq!(home.avatar().exp_level, 3, "another 50 should reach level 3"); + assert_eq!(home.avatar().exp_points, 5); +} +#[test] +fn a_completed_findcard_achievement_can_be_claimed() { + tables(); + let tabs = LogicDataTables::instance(); + let achievements = tabs.table(table::ACHIEVEMENTS).unwrap(); + let (name, required) = (0..achievements.count()) + .filter_map(|i| achievements.get_at(i)) + .find(|row| row.string("Action") == "findcard") + .map(|row| (row.string("Name").to_owned(), row.int("ActionCount"))) + .expect("a findcard achievement exists"); + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets"); + let catalog = game_service::Catalog::load(Some(&root)); + let mut profile = game_service::store::PlayerProfile::starter( + service_rpc::AccountRef::new(0, 5), + &game_service::StarterProfile::default(), + &catalog, + ); + profile.collection.clear(); + profile.deck.clear(); + let tabs2 = LogicDataTables::instance(); + let cards = tabs2.table(table::CHARACTERS).unwrap(); + for i in 0..(required.max(1) as usize) { + let row = cards.get_at(i % cards.count()).unwrap(); + profile.collection.push(game_service::store::OwnedCard { + card: LogicDataRef::Resolved(Arc::clone(row)), + level_index: 0, + count: 1, + }); + } + let avatar = build_avatar(&profile); + let progress = avatar + .commodities + .types + .get(COMMODITY_ACHIEVEMENT_PROGRESS) + .map(|slots| slots.iter().filter(|s| s.count >= required).count()) + .unwrap_or(0); + assert!(progress > 0, "the served progress should complete a findcard tier"); + let mut home = LogicHomeMode::new(LogicClientHome::default(), avatar, 0); + let before_level = home.avatar().exp_level; + let before_points = home.avatar().exp_points; + let command = LogicClaimAchievementRewardCommand { + achievement: LogicDataRef::by_name(table::ACHIEVEMENTS, &name), + header: LogicCommandHeader::default(), + }; + let outcome = home.execute(&command); + assert_eq!(outcome, CommandOutcome::Applied, "the claim should be accepted"); + assert!( + home.avatar().exp_points != before_points || home.avatar().exp_level != before_level, + "claiming should have granted exp" + ); + assert!(matches!(home.execute(&command), CommandOutcome::Rejected(_))); +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index 9406aca..3607106 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -1,7 +1,7 @@ use crate::battle::logic_battle::LogicBattle; +use crate::battle::logic_battle::BATTLE_TICKS_PER_SECOND; use crate::battle::logic_component::{LogicComponent, COMPONENT_COMBAT, COMPONENT_HITPOINT}; use crate::battle::logic_game_object::{LogicGameObjectEntry, LogicObjectBody}; -use crate::battle::logic_battle::BATTLE_TICKS_PER_SECOND; use crate::battle::logic_tilemap::SUBTILE_UNITS; use crate::data::LogicDataRef; pub const TICK_MILLISECONDS: i32 = 50; @@ -133,7 +133,10 @@ impl LogicGameObjectEntry { } } pub fn movement(&self) -> Option<&crate::battle::logic_component::LogicMovementComponent> { - match self.components.get(crate::battle::logic_component::COMPONENT_MOVEMENT)? { + match self + .components + .get(crate::battle::logic_component::COMPONENT_MOVEMENT)? + { Some(LogicComponent::Movement(component)) => Some(component), _ => None, } @@ -149,7 +152,9 @@ impl LogicGameObjectEntry { _ => None, } } - pub fn combat_mut(&mut self) -> Option<&mut crate::battle::logic_component::LogicCombatComponent> { + pub fn combat_mut( + &mut self, + ) -> Option<&mut crate::battle::logic_component::LogicCombatComponent> { match self.components.get_mut(COMPONENT_COMBAT)? { Some(LogicComponent::Combat(component)) => Some(component), _ => None, @@ -376,7 +381,9 @@ impl LogicBattle { if leg == 0 { continue; } - let step = (stats.speed as i64).min(straight - stand_off as i64).min(leg); + let step = (stats.speed as i64) + .min(straight - stand_off as i64) + .min(leg); let base = self.objects.objects[index].body.base_mut(); base.position.x = from.0 + (dx * step / leg) as i32; base.position.y = from.1 + (dy * step / leg) as i32; @@ -453,7 +460,8 @@ impl LogicBattle { let mut shove = (push.0 / push.2 as i64, push.1 / push.2 as i64); let allowance = (stats.speed / 2).max(1) as i64; let travelled = - crate::logic_sqrt(distance_squared((0, 0), (shove.0 as i32, shove.1 as i32))) as i64; + crate::logic_sqrt(distance_squared((0, 0), (shove.0 as i32, shove.1 as i32))) + as i64; if travelled > allowance { shove.0 = shove.0 * allowance / travelled; shove.1 = shove.1 * allowance / travelled; @@ -461,7 +469,12 @@ impl LogicBattle { let (limit_x, limit_y) = self .tilemap .as_ref() - .map(|map| (map.width() * SUBTILE_UNITS - 1, map.height() * SUBTILE_UNITS - 1)) + .map(|map| { + ( + map.width() * SUBTILE_UNITS - 1, + map.height() * SUBTILE_UNITS - 1, + ) + }) .unwrap_or((i32::MAX, i32::MAX)); let base = self.objects.objects[index].body.base_mut(); base.position.x = (base.position.x + shove.0 as i32).clamp(0, limit_x); diff --git a/crates/logic/src/home/logic_home_mode.rs b/crates/logic/src/home/logic_home_mode.rs index 19840e9..6b7a6e2 100644 --- a/crates/logic/src/home/logic_home_mode.rs +++ b/crates/logic/src/home/logic_home_mode.rs @@ -1,5 +1,5 @@ use crate::commands::{CommandOutcome, LogicCommand, LogicReward}; -use crate::data::{LogicDataRef, LogicRarityData}; +use crate::data::{table, LogicDataRef, LogicDataTables, LogicRarityData}; use crate::model::{ ChestSource, LogicChest, LogicClientAvatar, LogicClientHome, LogicDataSlot, LogicSpell, LogicTimer, TICKS_PER_SECOND, @@ -141,6 +141,23 @@ impl LogicHomeMode { } pub fn add_exp(&mut self, amount: i32) { self.avatar.exp_points = self.avatar.exp_points.saturating_add(amount); + let tables = LogicDataTables::instance(); + let Some(levels) = tables.table(table::EXP_LEVELS) else { + return; + }; + let max_level = levels.count() as i32; + while self.avatar.exp_level < max_level { + let index = (self.avatar.exp_level - 1).max(0) as usize; + let Some(row) = levels.get_at(index) else { + break; + }; + let threshold = row.int("ExpToNextLevel"); + if threshold < 1 || self.avatar.exp_points < threshold { + break; + } + self.avatar.exp_points -= threshold; + self.avatar.exp_level += 1; + } } pub fn add_free_diamonds(&mut self, amount: i32) { self.avatar.diamonds = self.avatar.diamonds.saturating_add(amount); diff --git a/crates/service-rpc/src/game.rs b/crates/service-rpc/src/game.rs index a265177..329a74d 100644 --- a/crates/service-rpc/src/game.rs +++ b/crates/service-rpc/src/game.rs @@ -18,7 +18,7 @@ impl AccountRef { } impl std::fmt::Display for AccountRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{}", self.high, self.low) + write!(f, "LogicLong({}-{})", self.high, self.low) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/titan/src/logic_long.rs b/crates/titan/src/logic_long.rs index 3eb5925..dad48a5 100644 --- a/crates/titan/src/logic_long.rs +++ b/crates/titan/src/logic_long.rs @@ -24,7 +24,7 @@ impl LogicLong { } impl fmt::Display for LogicLong { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}-{}", self.high, self.low) + write!(f, "LogicLong({}-{})", self.high, self.low) } } impl From for LogicLong {