From baf573ae9b8138de47e4d823949d4a240eaeea74 Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov Date: Tue, 18 Jun 2019 01:49:25 +0300 Subject: [PATCH] Upgrade player's fighters between battles --- src/core/campaign.rs | 148 ++++++++++++++++++++++++++++++++--------- src/main.rs | 2 +- src/screen/campaign.rs | 27 +++++--- 3 files changed, 136 insertions(+), 41 deletions(-) diff --git a/src/core/campaign.rs b/src/core/campaign.rs index eb047ddf..a64d2710 100644 --- a/src/core/campaign.rs +++ b/src/core/campaign.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; + +use rand::{seq::SliceRandom, thread_rng}; use serde::{Deserialize, Serialize}; use crate::core::battle::{component::ObjType, scenario::Scenario, state::BattleResult, PlayerId}; @@ -23,6 +26,12 @@ pub struct Award { pub recruits: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum Action { + Recruit(ObjType), + Upgrade { from: ObjType, to: ObjType }, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct CampaignNode { pub scenario: Scenario, @@ -50,15 +59,14 @@ pub struct State { scenarios: Vec, current_scenario_index: i32, mode: Mode, - agents: Vec, - last_battle_casualties: Vec, - recruits: Vec, + upgrades: HashMap>, + actions: Vec, } impl State { - pub fn from_plan(plan: Plan) -> Self { + pub fn new(plan: Plan, upgrades: HashMap>) -> Self { assert!(!plan.nodes.is_empty(), "No scenarios"); Self { current_scenario_index: 0, @@ -66,7 +74,8 @@ impl State { mode: Mode::ReadyForBattle, agents: plan.initial_agents, last_battle_casualties: vec![], - recruits: vec![], + actions: vec![], + upgrades, } } @@ -96,19 +105,26 @@ impl State { &self.agents } - pub fn recruit(&mut self, typename: ObjType) { - assert_eq!(self.mode(), Mode::PreparingForBattle); - assert!(self.recruits.contains(&typename)); - self.agents.push(typename); - self.recruits = Vec::new(); - self.mode = Mode::ReadyForBattle; + pub fn available_actions(&self) -> &[Action] { + &self.actions } - pub fn available_recruits(&self) -> &[ObjType] { - if self.mode != Mode::PreparingForBattle { - assert!(self.recruits.is_empty()); + pub fn exectute_action(&mut self, action: Action) { + assert_eq!(self.mode(), Mode::PreparingForBattle); + assert!(self.actions.contains(&action)); + match action { + Action::Recruit(typename) => self.agents.push(typename), + Action::Upgrade { from, to } => { + self.agents + .iter() + .position(|a| a == &from) + .map(|e| self.agents.remove(e)) + .unwrap_or_else(|| panic!("Can't find agent {:?}", from)); + self.agents.push(to); + } } - &self.recruits + self.actions = Vec::new(); + self.mode = Mode::ReadyForBattle; } pub fn report_battle_results(&mut self, result: &BattleResult) -> Result<(), ()> { @@ -140,12 +156,34 @@ impl State { self.mode = Mode::Won; } else { let i = self.current_scenario_index as usize; - self.recruits = self.scenarios[i].award.recruits.clone(); + + for recruit in &self.scenarios[i].award.recruits { + self.actions.push(Action::Recruit(recruit.clone())); + } + + // Add one random upgrade (if available). + { + let mut upgrade_candidates = Vec::new(); + for agent in &self.agents { + for (from, upgrades) in &self.upgrades { + if from == agent { + for upgrade in upgrades { + let from = agent.clone(); + let to = upgrade.clone(); + upgrade_candidates.push(Action::Upgrade { from, to }); + } + } + } + } + if let Some(final_action) = upgrade_candidates.choose(&mut thread_rng()) { + self.actions.push(final_action.clone()); + } + } self.current_scenario_index += 1; self.mode = Mode::PreparingForBattle; - if self.available_recruits().is_empty() { + if self.available_actions().is_empty() { // Skip the preparation step. self.mode = Mode::ReadyForBattle; } @@ -157,6 +195,8 @@ impl State { #[cfg(test)] mod tests { + use std::collections::HashMap; + use crate::core::{ battle::{ component::ObjType, @@ -164,7 +204,7 @@ mod tests { state::BattleResult, PlayerId, }, - campaign::{Award, CampaignNode, Mode, Plan, State}, + campaign::{Action, Award, CampaignNode, Mode, Plan, State}, }; type GroupTuple<'a> = (Option, &'a str, Option, i32); @@ -186,6 +226,10 @@ mod tests { vec!["swordsman".into(), "alchemist".into()] } + fn upgrades() -> HashMap> { + HashMap::new() + } + fn campaign_plan_short() -> Plan { let initial_agents = initial_agents(); let nodes = { @@ -251,13 +295,13 @@ mod tests { nodes: Vec::new(), initial_agents: Vec::new(), }; - let _state = State::from_plan(empty_plan); + let _state = State::new(empty_plan, upgrades()); } #[test] fn short_happy_path() { - let mut state = State::from_plan(campaign_plan_short()); - assert!(state.available_recruits().is_empty()); + let mut state = State::new(campaign_plan_short(), upgrades()); + assert!(state.available_actions().is_empty()); assert_eq!(state.mode(), Mode::ReadyForBattle); let battle_result = BattleResult { winner_id: PlayerId(0), @@ -269,8 +313,8 @@ mod tests { #[test] fn short_fail_path() { - let mut state = State::from_plan(campaign_plan_short()); - assert!(state.available_recruits().is_empty()); + let mut state = State::new(campaign_plan_short(), upgrades()); + assert!(state.available_actions().is_empty()); assert_eq!(state.mode(), Mode::ReadyForBattle); let battle_result = BattleResult { winner_id: PlayerId(1), @@ -283,7 +327,7 @@ mod tests { #[test] fn bad_survivors() { - let mut state = State::from_plan(campaign_plan_short()); + let mut state = State::new(campaign_plan_short(), upgrades()); let battle_result = BattleResult { winner_id: PlayerId(1), survivor_types: vec!["imp".into()], @@ -293,7 +337,7 @@ mod tests { #[test] fn bad_battle_win_no_survivors() { - let mut state = State::from_plan(campaign_plan_short()); + let mut state = State::new(campaign_plan_short(), upgrades()); let battle_result = BattleResult { winner_id: PlayerId(0), survivor_types: vec![], @@ -302,9 +346,9 @@ mod tests { } #[test] - fn upgrade() { - let mut state = State::from_plan(campaign_plan_two_battles()); - assert!(state.available_recruits().is_empty()); + fn recruit_and_casualty() { + let mut state = State::new(campaign_plan_two_battles(), upgrades()); + assert!(state.available_actions().is_empty()); assert_eq!(state.mode(), Mode::ReadyForBattle); { let battle_result = BattleResult { @@ -313,11 +357,14 @@ mod tests { }; state.report_battle_results(&battle_result).unwrap(); } - assert_eq!(state.available_recruits(), &["spearman".into()]); + assert_eq!( + state.available_actions(), + &[Action::Recruit("spearman".into())] + ); assert!(state.last_battle_casualties().is_empty()); assert_eq!(state.mode(), Mode::PreparingForBattle); - state.recruit("spearman".into()); - assert!(state.available_recruits().is_empty()); + state.exectute_action(Action::Recruit("spearman".into())); + assert!(state.available_actions().is_empty()); assert_eq!(state.mode(), Mode::ReadyForBattle); { let battle_result = BattleResult { @@ -329,4 +376,43 @@ mod tests { assert_eq!(state.mode(), Mode::Won); assert_eq!(state.last_battle_casualties(), &["spearman".into()]); } + + #[test] + fn upgrade_and_casualty() { + let mut upgrades = HashMap::new(); + upgrades.insert("swordsman".into(), vec!["heavy_swordsman".into()]); + let mut state = State::new(campaign_plan_two_battles(), upgrades); + assert!(state.available_actions().is_empty()); + assert_eq!(state.mode(), Mode::ReadyForBattle); + { + let battle_result = BattleResult { + winner_id: PlayerId(0), + survivor_types: initial_agents(), + }; + state.report_battle_results(&battle_result).unwrap(); + } + let action_upgrade = Action::Upgrade { + from: "swordsman".into(), + to: "heavy_swordsman".into(), + }; + let action_reqruit = Action::Recruit("spearman".into()); + assert_eq!( + state.available_actions(), + &[action_reqruit, action_upgrade.clone()] + ); + assert!(state.last_battle_casualties().is_empty()); + assert_eq!(state.mode(), Mode::PreparingForBattle); + state.exectute_action(action_upgrade); + assert!(state.available_actions().is_empty()); + assert_eq!(state.mode(), Mode::ReadyForBattle); + { + let battle_result = BattleResult { + winner_id: PlayerId(0), + survivor_types: vec!["alchemist".into()], + }; + state.report_battle_results(&battle_result).unwrap(); + } + assert_eq!(state.mode(), Mode::Won); + assert_eq!(state.last_battle_casualties(), &["heavy_swordsman".into()]); + } } diff --git a/src/main.rs b/src/main.rs index 71130522..83b87752 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,7 @@ fn main() -> ZResult { const APP_ID: &str = "zemeroth"; const APP_AUTHOR: &str = "ozkriff"; const ASSETS_DIR_NAME: &str = "assets"; - const ASSETS_HASHSUM: &str = "2b90cce9970532b3a2e5678e650e38e8"; + const ASSETS_HASHSUM: &str = "95a1bd6209b1b2b33cead60f40873c58"; fn enable_backtrace() { if std::env::var("RUST_BACKTRACE").is_err() { diff --git a/src/screen/campaign.rs b/src/screen/campaign.rs index bb1a14fa..0019c1eb 100644 --- a/src/screen/campaign.rs +++ b/src/screen/campaign.rs @@ -19,7 +19,7 @@ use crate::{ state::BattleResult, PlayerId, }, - campaign::{Mode, State}, + campaign::{Action, Mode, State}, }, screen::{self, Screen, Transition}, utils, ZResult, @@ -30,7 +30,7 @@ enum Message { Menu, StartBattle, AgentInfo(ObjType), - Recruit(ObjType), + Action(Action), } const FONT_SIZE: f32 = utils::font_size(); @@ -119,7 +119,8 @@ pub struct Campaign { impl Campaign { pub fn new(context: &mut Context) -> ZResult { let plan = utils::deserialize_from_file(context, "/campaign_01.ron")?; - let state = State::from_plan(plan); + let upgrades = utils::deserialize_from_file(context, "/agent_upgrades.ron")?; + let state = State::new(plan, upgrades); let font = utils::default_font(context); let gui = basic_gui(context, font)?; let mut this = Self { @@ -175,13 +176,18 @@ impl Campaign { let text = Box::new(Text::new(("Choose:", self.font, FONT_SIZE))); layout.add(Box::new(ui::Label::new(context, text, h)?)); } - for agent_type in self.state.available_recruits() { + for action in self.state.available_actions() { let mut line = ui::HLayout::new(); { - let text = format!("- [Recruit {}]", agent_type.0); + let text = match action { + Action::Recruit(agent_type) => format!("- [Recruit {}]", agent_type.0), + Action::Upgrade { from, to } => { + format!("- [Upgrade {} to {}]", from.0, to.0) + } + }; let text = Box::new(Text::new((text.as_str(), self.font, FONT_SIZE))); let sender = self.gui.sender(); - let message = Message::Recruit(agent_type.clone()); + let message = Message::Action(action.clone()); let button = ui::Button::new(context, text, h, sender, message)?; line.add(Box::new(button)); } @@ -194,7 +200,10 @@ impl Campaign { } { let text = Box::new(Text::new(("[i]", self.font, FONT_SIZE))); - let message = Message::AgentInfo(agent_type.clone()); + let message = match action { + Action::Recruit(agent_type) => Message::AgentInfo(agent_type.clone()), + Action::Upgrade { to, .. } => Message::AgentInfo(to.clone()), + }; let sender = self.gui.sender(); let button = ui::Button::new(context, text, h, sender, message)?; line.add(Box::new(button)); @@ -328,8 +337,8 @@ impl Screen for Campaign { let screen = self.start_battle(context)?; Ok(Transition::Push(screen)) } - Some(Message::Recruit(typename)) => { - self.state.recruit(typename); + Some(Message::Action(action)) => { + self.state.exectute_action(action); let new_mode = self.state.mode(); self.set_mode(context, new_mode)?; Ok(Transition::None)