diff --git a/assets/encounters/grasslands.encounters.toml b/assets/encounters/grasslands.encounters.toml index 3f51b782ea06629e9ea2c9104de1c2d30d299bc6..4630ab0598e4c9024987872c35900f980a6783af 100644 --- a/assets/encounters/grasslands.encounters.toml +++ b/assets/encounters/grasslands.encounters.toml @@ -1,16 +1,124 @@ [[encounters]] chance = 90 -title = "You hear a rustle in the bushes" +title = "A rustle in the bushes" description = """ +While taking a rest along the road, you hear a suspicious rustle in the nearby bushes. All sorts of creatures and cretins live in the wilds; do you with to investigate? +It will take some time to do so +""" +[[encounters.options]] +label = "Investigate" +outcomes = [ + { type = "Description", text = """You carefully poke through the bushes, keeping a close eye on your footing. Eventually, you enter a small clearing where some creatures must have been storing stolen food. +You fill one of your bags before making your getaway.""" }, + { type = "GainResource", resource_type = "Corn", amount = [25, 35] } +# ,{ type = "TickWorld", amount = 1 } + +] +[[encounters.options]] +label = "Ignore And Move On" +outcomes = [ + { type = "Continue" } +] + +[[encounters]] +chance = 90 +title = "A rustle in the bushes" +description = """ +While taking a rest along the road, you hear a suspicious rustle in the nearby bushes. All sorts of creatures and cretins live in the wilds; do you with to investigate? +It will take some time to do so +""" +[[encounters.options]] +label = "Investigate" +outcomes = [ + { type = "Description", text = """You carefully poke through the bushes, keeping a close eye on your footing. Eventually, you enter a small clearing where some creatures must have been storing stolen food. +You fill one of your bags before making your getaway.""" }, + { type = "GainResource", resource_type = "Wheat", amount = [25, 35] } +# ,{ type = "TickWorld", amount = 1 } + +] +[[encounters.options]] +label = "Ignore And Move On" +outcomes = [ + { type = "Continue" } +] + +[[encounters]] +chance = 90 +title = "A rustle in the bushes" +description = """ +While taking a rest along the road, you hear a suspicious rustle in the nearby bushes. All sorts of creatures and cretins live in the wilds; do you with to investigate? +It will take some time to do so +""" +[[encounters.options]] +label = "Investigate" +outcomes = [ + { type = "Description", text = """You carefully poke through the bushes, keeping a close eye on your footing. Eventually, you enter a small clearing where some creatures must have been storing stolen food. +You fill one of your bags before making your getaway.""" }, + { type = "GainResource", resource_type = "Berries", amount = [25, 35] } +# ,{ type = "TickWorld", amount = 1 } +] +[[encounters.options]] +label = "Ignore And Move On" +outcomes = [ + { type = "Continue" } +] +[[encounters]] +chance = 90 +title = "A rustle in the bushes" +description = """ +While taking a rest along the road, you hear a suspicious rustle in the nearby bushes. All sorts of creatures and cretins live in the wilds; do you with to investigate? +It will take some time to do so """ +[[encounters.options]] +label = "Investigate" +outcomes = [ + { type = "Description", text = """You carefully poke through the bushes, keeping a close eye on your footing. Despite your searching, you can't seem to find the source. +When you return to your caravan, you discover that some of your supplies have been pilfered!""" }, + { type = "LoseGold", amount = [45, 90] } +# ,{ type = "TickWorld", amount = 1 } + +] +[[encounters.options]] +label = "Ignore And Move On" +outcomes = [ + { type = "Continue" } +] [[encounters]] chance = 10 title = "You see a small shrine" description = """ Off to the side of the path is located a small offering shrine to some forgotten god, chipped and covered in moss. - Around the base of the shrine, you see some small totems and other miscellanious offerings. Do you want to leave anything for these ancient gods?""" -[[encounters.options]] \ No newline at end of file +[[encounters.options]] +label = "Leave 100 Gold" +hide_missing_reqs = true +outcomes = [ + { type = "Description", text = "After depositing the gold at the shrine, a quiet thud sound catches your attention. Looking around for the source of the noise, you notice a small golden statue nestled within some shrubs. You pick it up and place it in your pack." }, + { type = "GainResource", resource_type = "Religious Artifact", amount = 1 }, + { type = "LoseGold", amount = 100 } +] +requirements = [ + { type = "MinimumGoldAmount", amount = 100 }, +] + +[[encounters.options]] +label = "Leave a Gold Piece" +hide_missing_reqs = false +outcomes = [ + { type = "Description", text = "You walk up to the shrine and leave a single gold piece. As you walk away, you feel a warmth inside." }, + { type = "GainSustenance", amount = [2, 4] }, + { type = "LoseGold", amount = 1 } +] +requirements = [ + { type = "MinimumGoldAmount", amount = 1 }, +] + +[[encounters.options]] +label = "Ignore The Shrine" +outcomes = [ + { type = "Continue" } +] +requirements = [] diff --git a/assets/resources.apack b/assets/resources.apack index 3ad723412d4cdd5561d095a77ba3bb17e1554d75..e6ebb1425b826aa9b947ff3fbe6264e2c440b47f 100644 --- a/assets/resources.apack +++ b/assets/resources.apack @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08fbd17915eea98e0e3e077c32249781bb0b33d821aeab9c9dac5196eaab6527 -size 1955812 +oid sha256:6451023934a9b45a940ff0ec5f5a75bedcbaa34b66f349514ee3136f080355ce +size 1955948 diff --git a/assets/trade_manifests/monastery.manifest.toml b/assets/trade_manifests/monastery.manifest.toml index 88bc5033b02cde1a5f7d4e63bf2d3f91aec36cd0..d412d6073533e6ecb8b00accc8339726a2fbad05 100644 --- a/assets/trade_manifests/monastery.manifest.toml +++ b/assets/trade_manifests/monastery.manifest.toml @@ -151,6 +151,7 @@ cost_multipliers = [ [99999, 1] ] oversupply_trend = "halt" +undersupply_trend = "halt" [Wheat] tick_decay = 1.5 @@ -161,4 +162,18 @@ cost_multipliers = [ [30, 1.1], [99999, 1] ] -oversupply_trend = "halt" \ No newline at end of file +oversupply_trend = "halt" + +["Religious Artifact"] +tick_decay = 0.0 +natural_limit = 0 +oversupply_trend = "halt" +undersupply_trend = "halt" +cost_multipliers = [ + [5, 2], + [10, 1.75], + [20, 1.5], + [30, 1.25], + [100, 1], + [99999, 0.75], +] \ No newline at end of file diff --git a/game_core/src/assets/asset_types/encounter_set.rs b/game_core/src/assets/asset_types/encounter_set.rs new file mode 100644 index 0000000000000000000000000000000000000000..cebbd1b52a9441a575688cd5ba0953fedcce5778 --- /dev/null +++ b/game_core/src/assets/asset_types/encounter_set.rs @@ -0,0 +1,58 @@ +use anyhow::Error; +use bevy::asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset}; +use bevy::reflect::TypeUuid; +use serde::{Deserialize, Serialize}; + +use crate::world::Encounter; + +pub struct EncounterSetJsonLoader; +pub struct EncounterSetTomlLoader; + +#[derive(Debug, Default, Clone, TypeUuid, Serialize, Deserialize)] +#[uuid = "87953518-7969-11ed-9ec9-d7c0b4b5e357"] +pub struct EncounterSet { + pub encounters: Vec<Encounter>, +} + +impl EncounterSet { + pub fn select(&self) -> Encounter { + let val = fastrand::usize(0..self.encounters.len()); + self.encounters[val].clone() + } +} + +impl AssetLoader for EncounterSetJsonLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let manifest: EncounterSet = serde_json::from_slice(bytes)?; + load_context.set_default_asset(LoadedAsset::new(manifest)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["encounters.json"] + } +} + +impl AssetLoader for EncounterSetTomlLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let manifest: EncounterSet = toml::from_slice(bytes)?; + load_context.set_default_asset(LoadedAsset::new(manifest)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["encounters.toml"] + } +} diff --git a/game_core/src/assets/asset_types/mod.rs b/game_core/src/assets/asset_types/mod.rs index ecb2c415973c934243541dcee86696b6eaaa967d..dd19a0f1bc3b13a382761770d01e5c1458ab100a 100644 --- a/game_core/src/assets/asset_types/mod.rs +++ b/game_core/src/assets/asset_types/mod.rs @@ -1,2 +1,3 @@ +pub mod encounter_set; pub mod ldtk_project; pub mod trade_manifest; diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs index 01abd6336892755a98e4896a4ea57341332fbc9a..2c3ab81fb097cfb0c7efa30c00973b5f7195c44a 100644 --- a/game_core/src/assets/loader.rs +++ b/game_core/src/assets/loader.rs @@ -8,6 +8,7 @@ use kayak_font::KayakFont; use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; +use crate::assets::asset_types::encounter_set::EncounterSet; use crate::assets::asset_types::ldtk_project::LdtkProject; use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig}; use crate::world::TradeManifest; @@ -64,6 +65,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> { load_basic_type!(load_ldtk, LdtkProject => ldtk); load_basic_type!(load_kayak_font, KayakFont => kayak_fonts); load_basic_type!(load_trade_manifest, TradeManifest => trade_manifests); + load_basic_type!(load_encounter_set, EncounterSet => encounter_sets); pub fn load_spritesheet( &mut self, diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs index bd42743e9bf00651ded6a64ae54284c076f4e670..3a6933b60320a1f83657c8cc0cad99c3e7159d4f 100644 --- a/game_core/src/assets/mod.rs +++ b/game_core/src/assets/mod.rs @@ -4,6 +4,7 @@ mod loader; mod resources; mod startup; +pub use asset_types::encounter_set::EncounterSet; pub use asset_types::ldtk_project::{LdtkLoader, LdtkProject, LevelIndex, TilesetIndex}; use bevy::app::{App, Plugin}; use bevy::prelude::AddAsset; @@ -13,6 +14,7 @@ pub use loader::AssetTypeLoader; pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig}; use crate::assets::apack_handler::handle_apack_process_events; +use crate::assets::asset_types::encounter_set::{EncounterSetJsonLoader, EncounterSetTomlLoader}; use crate::assets::asset_types::ldtk_project::handle_ldtk_project_events; use crate::assets::asset_types::trade_manifest::{ TradeManifestJsonLoader, TradeManifestTomlLoader, @@ -28,9 +30,12 @@ impl Plugin for AssetsPlugin { .init_resource::<TilesetIndex>() .add_asset::<LdtkProject>() .add_asset::<TradeManifest>() + .add_asset::<EncounterSet>() .add_asset_loader(LdtkLoader) .add_asset_loader(TradeManifestJsonLoader) .add_asset_loader(TradeManifestTomlLoader) + .add_asset_loader(EncounterSetJsonLoader) + .add_asset_loader(EncounterSetTomlLoader) .add_enter_system(AppState::Preload, startup::start_preload_resources) .add_enter_system(AppState::Preload, startup::start_load_resources) .add_system(handle_apack_process_events) diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs index ba4ed40f931b47a75f7e3cab1c53e4e7e4f3b35b..e4a87b2cac912b8f6906feed9e63cd151f54c638 100644 --- a/game_core/src/assets/resources.rs +++ b/game_core/src/assets/resources.rs @@ -5,6 +5,7 @@ use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; use micro_musicbox::utilities::{SuppliesAudio, TrackType}; +use crate::assets::asset_types::encounter_set::EncounterSet; use crate::assets::asset_types::ldtk_project::LdtkProject; use crate::world::TradeManifest; @@ -46,6 +47,7 @@ pub struct AssetHandles { pub ldtk: HashMap<String, Handle<LdtkProject>>, pub kayak_fonts: HashMap<String, Handle<KayakFont>>, pub trade_manifests: HashMap<String, Handle<TradeManifest>>, + pub encounter_sets: HashMap<String, Handle<EncounterSet>>, } macro_rules! fetch_wrapper { @@ -79,6 +81,7 @@ impl AssetHandles { fetch_wrapper!(ldtk, LdtkProject => ldtk); fetch_wrapper!(kayak_font, KayakFont => kayak_fonts); fetch_wrapper!(trade_manifest, TradeManifest => trade_manifests); + fetch_wrapper!(encounter_set, EncounterSet => encounter_sets); } impl SuppliesAudio for AssetHandles { diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index 74d4a5bcdc7813fa7dc8343a20cce5a9ac15fbfe..69c4b20e8ea77029d17d684792df4b4a33255046 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -20,6 +20,7 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) { ("trade_manifests/monastery.manifest.toml", "monastery"), ("trade_manifests/ship.manifest.toml", "ship"), ]); + loader.load_encounter_set(&[("encounters/grasslands.encounters.toml", "grasslands")]); } pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) { diff --git a/game_core/src/ui/widgets/encounter_panel.rs b/game_core/src/ui/widgets/encounter_panel.rs index c0583bf423419324a064ee45e9c9e6d6bfe14422..8d6a38d4ef6b28486e26e81c128b05dc7571bf0f 100644 --- a/game_core/src/ui/widgets/encounter_panel.rs +++ b/game_core/src/ui/widgets/encounter_panel.rs @@ -8,7 +8,7 @@ use kayak_ui::widgets::{ use crate::ui::components::*; use crate::ui::prelude::*; use crate::ui::widgets::*; -use crate::world::EncounterState; +use crate::world::{EncounterOption, EncounterOutcome, EncounterState, SelectEncounterOptionEvent}; use crate::{basic_widget, empty_props, on_button_click}; empty_props!(EncounterPanelProps); @@ -47,113 +47,129 @@ pub fn render_encounter_panel( ..Default::default() }; - match &*ui_data { - EncounterState::Choice(encounter) => { - rsx! { - <ElementBundle styles={panel_style}> - <PanelWidget> - <BackgroundBundle - styles={KStyle { + let encounter_data = match &*ui_data { + EncounterState::NoEncounter => None, + EncounterState::Choice(encounter) => Some(( + encounter.title.clone(), + encounter.description.clone(), + encounter.options.clone(), + )), + EncounterState::Consequence(encounter, choice) => Some(( + encounter.title.clone(), + choice.describe(), + vec![EncounterOption { + label: String::from("Continue"), + requirements: vec![], + hide_missing_reqs: false, + outcomes: vec![EncounterOutcome::Continue], + }], + )), + }; + + if let Some((title, description, options)) = encounter_data { + rsx! { + <ElementBundle styles={panel_style}> + <PanelWidget> + <BackgroundBundle + styles={KStyle { - height: value(Units::Auto), + height: value(Units::Auto), + color: value(Color::BLACK), + padding_left: px(20.0), + padding_right: px(20.0), + padding_top: px(10.0), + padding_bottom: px(10.0), + ..Default::default() + }} + > + <TextWidgetBundle + text={TextProps { + font: Some(String::from("header")), + content: format!("Trepidation! {}", title), + size: 36.0, + ..Default::default() + }} + styles={KStyle { color: value(Color::BLACK), - padding_left: px(20.0), - padding_right: px(20.0), - padding_top: px(10.0), - padding_bottom: px(10.0), + left: stretch(1.0), + right: stretch(1.0), ..Default::default() }} - > - <TextWidgetBundle - text={TextProps { - font: Some(String::from("header")), - content: format!("Trepidation! {}", encounter.title), - size: 36.0, - ..Default::default() - }} - styles={KStyle { - color: value(Color::BLACK), - left: stretch(1.0), - right: stretch(1.0), - ..Default::default() - }} - /> - </BackgroundBundle> - <VDividerWidget props={VDividerWidgetProps { height: 4.0, padding: 10.0, color: Color::rgb(0.52, 0.369, 0.18)}} /> - <ScrollContextProviderBundle> - <ScrollBoxBundle - scroll_box_props={ScrollBoxProps { - track_color: Some(Color::rgb(0.827, 0.482, 0.353)), - thumb_color: Some(Color::rgb(0.451, 0.224, 0.063)), - ..Default::default() - } } styles={KStyle { padding: edge_px(5.0), ..Default::default() }}> - { for line in encounter.description.lines() { - constructor! { - <BackgroundBundle - styles={KStyle { + /> + </BackgroundBundle> + <VDividerWidget props={VDividerWidgetProps { height: 4.0, padding: 10.0, color: Color::rgb(0.52, 0.369, 0.18)}} /> + <ScrollContextProviderBundle> + <ScrollBoxBundle + scroll_box_props={ScrollBoxProps { + track_color: Some(Color::rgb(0.827, 0.482, 0.353)), + thumb_color: Some(Color::rgb(0.451, 0.224, 0.063)), + ..Default::default() + } } styles={KStyle { padding: edge_px(5.0), ..Default::default() }}> + { for line in description.lines() { + constructor! { + <BackgroundBundle + styles={KStyle { - pointer_events: value(PointerEvents::None), - render_command: value(RenderCommand::Quad), - layout_type: value(LayoutType::Row), - height: value(Units::Auto), - width: pct(90.0), - left: stretch(1.0), - right: stretch(1.0), - bottom: px(15.0), + pointer_events: value(PointerEvents::None), + render_command: value(RenderCommand::Quad), + layout_type: value(LayoutType::Row), + height: value(Units::Auto), + width: pct(90.0), + left: stretch(1.0), + right: stretch(1.0), + bottom: px(15.0), + ..Default::default() + }} + > + <TextWidgetBundle + text={TextProps { + content: line.to_string(), + line_height: Some(34.0), + size: 32.0, + ..Default::default() + }} + styles={KStyle { + color: value(Color::BLACK), ..Default::default() }} - > - <TextWidgetBundle - text={TextProps { - content: line.to_string(), - line_height: Some(34.0), - size: 32.0, - ..Default::default() - }} - styles={KStyle { - color: value(Color::BLACK), - ..Default::default() - }} - /> - </BackgroundBundle> - } - } } - </ScrollBoxBundle> - </ScrollContextProviderBundle> - </PanelWidget> + /> + </BackgroundBundle> + } + } } + </ScrollBoxBundle> + </ScrollContextProviderBundle> + </PanelWidget> - <ElementBundle styles={KStyle { - layout_type: value(LayoutType::Row), - padding_top: stretch(1.0), - padding_bottom: stretch(1.0), - padding_left: stretch(1.0), - padding_right: stretch(1.0), - height: px(60.0), - col_between: px(15.0), - ..Default::default() - }}> - { - for option in &encounter.options { - let label = format!("{}", option.label); - let on_click = on_button_click!(ResMut<EncounterState>, |mut state: ResMut<EncounterState>| { - *state = EncounterState::NoEncounter; - }); + <ElementBundle styles={KStyle { + layout_type: value(LayoutType::Row), + padding_top: stretch(1.0), + padding_bottom: stretch(1.0), + padding_left: stretch(1.0), + padding_right: stretch(1.0), + height: px(60.0), + col_between: px(15.0), + ..Default::default() + }}> + { + for option in &options { + let label = format!("{}", option.label); + let owned = option.clone(); + let on_click = on_button_click!(EventWriter<SelectEncounterOptionEvent>, |mut state: EventWriter<SelectEncounterOptionEvent>| { + state.send(SelectEncounterOptionEvent { option: owned.clone() }); + }); - constructor! { - <ButtonWidget - styles={KStyle { ..Default::default() }} - props={ButtonWidgetProps::text(label, 28.0)} - on_event={on_click} - /> - } + constructor! { + <ButtonWidget + styles={KStyle { ..Default::default() }} + props={ButtonWidgetProps::text(label, 28.0)} + on_event={on_click} + /> } } - </ElementBundle> + } </ElementBundle> - }; - } - EncounterState::Consequence(..) => {} - EncounterState::NoEncounter => {} + </ElementBundle> + }; } true diff --git a/game_core/src/ui/widgets/shop_panel.rs b/game_core/src/ui/widgets/shop_panel.rs index 074d0868a7dc9bb292470ff2c2359029a83a3da5..c0579259ee8ec5c3bf7983e68388b2f9789fc219 100644 --- a/game_core/src/ui/widgets/shop_panel.rs +++ b/game_core/src/ui/widgets/shop_panel.rs @@ -145,8 +145,8 @@ pub fn render_shop_panel( }} > <InsetIconWidget props={InsetIconProps { image: icon.clone(), size: 32.0}} /> - <TextWidgetBundle styles={KStyle { width: stretch(1.5), ..Default::default() }} text={TextProps { content: name.clone(), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> - <TextWidgetBundle styles={KStyle { width: stretch(1.0), ..Default::default() }} text={TextProps { content: format!("{}g", current_cost), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> + <TextWidgetBundle styles={KStyle { width: stretch(1.75), ..Default::default() }} text={TextProps { content: name.clone(), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> + <TextWidgetBundle styles={KStyle { width: stretch(0.6), ..Default::default() }} text={TextProps { content: format!("{}g", current_cost), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> <TextWidgetBundle styles={KStyle { width: stretch(0.5), ..Default::default() }} text={TextProps { content: format!("{}", town_amount), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> <TextWidgetBundle styles={KStyle { width: stretch(0.5), ..Default::default() }} text={TextProps { content: format!("{}", player_amount), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> <ButtonWidget on_event={buysell_button_factory(name.clone(), true)} props={ButtonWidgetProps { font_size: 20.0, is_disabled: ui_data.player_gold < *current_cost || town_amount == &0, text: String::from("Buy"), ..Default::default() }} /> diff --git a/game_core/src/world/encounters.rs b/game_core/src/world/encounters.rs index 66ac733d46d6353e5fba62cbe28ec76d2e46d1ab..8a3ac8fc5d1cfb82f1c2510138be67eb226f2d72 100644 --- a/game_core/src/world/encounters.rs +++ b/game_core/src/world/encounters.rs @@ -8,8 +8,11 @@ use num_traits::AsPrimitive; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::assets::{AssetHandles, EncounterSet}; use crate::states::Player; +use crate::world::travel::WorldTickEvent; use crate::world::utils::entity_to_worldspace; +use crate::world::{HungerState, TradingState}; #[derive(Copy, Clone, PartialEq, Debug)] pub enum EncounterType { @@ -37,6 +40,9 @@ impl EncounterType { _ => None, } } + pub fn get_asset_name(&self) -> String { + String::from("grasslands") + } } impl TryFrom<String> for EncounterType { @@ -133,22 +139,42 @@ impl From<Vec<EncounterZone>> for WorldZones { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RewardAmount { + Exact(usize), + Between(usize, usize), +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum EncounterOutcome { Continue, Description { - description: String, + text: String, }, GainResource { - description: String, resource_type: String, - amount: (usize, usize), + amount: RewardAmount, + }, + GainSustenance { + amount: RewardAmount, + }, + LoseSustenance { + amount: RewardAmount, + }, + GainGold { + amount: RewardAmount, + }, + LoseGold { + amount: RewardAmount, }, LoseResource { - description: String, resource_type: String, - amount: (usize, usize), + amount: RewardAmount, + }, + TickWorld { + amount: RewardAmount, }, Ambush { description: String, @@ -170,8 +196,8 @@ pub struct ChanceOutcome { pub enum EncounterRequirement { MinimumItemAmount { item: String, amount: usize }, MaximumItemAmount { item: String, amount: usize }, - MinimumGoldAmount { item: String, amount: usize }, - MaximumGoldAmount { item: String, amount: usize }, + MinimumGoldAmount { amount: usize }, + MaximumGoldAmount { amount: usize }, AnyFoodLessThan { amount: usize, or_equal: bool }, AnyFoodGreaterThan { amount: usize, or_equal: bool }, } @@ -190,6 +216,44 @@ pub struct EncounterOption { pub hide_missing_reqs: bool, } +impl EncounterOption { + pub fn describe(&self) -> String { + let mut description = Vec::with_capacity(self.outcomes.len()); + for outcome in &self.outcomes { + match outcome { + EncounterOutcome::Continue => {} + EncounterOutcome::Description { text } => { + description.push(text.clone()); + } + EncounterOutcome::GainResource { resource_type, .. } => { + description.push(format!("Gained {}", resource_type)); + } + EncounterOutcome::GainSustenance { .. } => { + description.push(String::from("Your Hunger Has Decreased")); + } + EncounterOutcome::LoseSustenance { .. } => { + description.push(String::from("Your Hunger Has Decreased")); + } + EncounterOutcome::GainGold { .. } => { + description.push(String::from("Gained Some Gold")); + } + EncounterOutcome::LoseGold { .. } => { + description.push(String::from("Lost Some Gold")); + } + EncounterOutcome::LoseResource { resource_type, .. } => { + description.push(format!("Gained {}", resource_type)); + } + EncounterOutcome::TickWorld { .. } => { + description.push(String::from("Some time has passed")) + } + EncounterOutcome::Ambush { .. } => {} + } + } + + description.join("\n") + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Encounter { pub title: String, @@ -205,7 +269,6 @@ pub fn gen_encounter() -> Encounter { EncounterOption { label: BsVerb().fake(), outcomes: vec![EncounterOutcome::GainResource { - description: Paragraph(3..5).fake(), resource_type: format!( "{} {}", BsAdj().fake::<String>(), @@ -214,7 +277,7 @@ pub fn gen_encounter() -> Encounter { amount: { let b = fastrand::usize(2..5); let inc = fastrand::usize(2..5); - (b, b + inc) + RewardAmount::Between(b, b + inc) }, }], requirements: Vec::new(), @@ -223,7 +286,6 @@ pub fn gen_encounter() -> Encounter { EncounterOption { label: BsVerb().fake(), outcomes: vec![EncounterOutcome::LoseResource { - description: Paragraph(3..5).fake(), resource_type: format!( "{} {}", BsAdj().fake::<String>(), @@ -232,7 +294,7 @@ pub fn gen_encounter() -> Encounter { amount: { let b = fastrand::usize(2..5); let inc = fastrand::usize(2..5); - (b, b + inc) + RewardAmount::Between(b, b + inc) }, }], requirements: Vec::new(), @@ -256,11 +318,116 @@ impl EncounterState { } } +#[derive(Clone, Debug)] +pub struct SelectEncounterOptionEvent { + pub option: EncounterOption, +} + +pub fn handle_encounter_option_event( + mut events: ResMut<Events<SelectEncounterOptionEvent>>, + mut time_ticks: EventWriter<WorldTickEvent>, + mut player_query: Query<(&mut TradingState, &mut HungerState), With<Player>>, + mut encounter_state: ResMut<EncounterState>, +) { + for event in events.drain() { + for (mut trading, mut hunger) in &mut player_query { + for outcome in &event.option.outcomes { + log::info!("Processing outcome: {:?}", outcome); + + match outcome { + EncounterOutcome::Continue => { + *encounter_state = EncounterState::NoEncounter; + } + EncounterOutcome::Description { .. } => {} + EncounterOutcome::GainResource { + resource_type, + amount, + } => match amount { + RewardAmount::Exact(number) => { + trading.add_items(resource_type, *number); + } + RewardAmount::Between(min, max) => { + trading.add_items(resource_type, fastrand::usize(*min..*max)); + } + }, + EncounterOutcome::LoseResource { + resource_type, + amount, + } => match amount { + RewardAmount::Exact(number) => { + trading.force_remove_items(resource_type, *number); + } + RewardAmount::Between(min, max) => { + trading.force_remove_items(resource_type, fastrand::usize(*min..*max)); + } + }, + EncounterOutcome::GainSustenance { amount } => match amount { + RewardAmount::Exact(number) => { + hunger.sustenance += *number; + } + RewardAmount::Between(min, max) => { + hunger.sustenance += fastrand::usize(*min..*max); + } + }, + EncounterOutcome::LoseSustenance { amount } => match amount { + RewardAmount::Exact(number) => { + hunger.sustenance = hunger.sustenance.saturating_sub(*number); + } + RewardAmount::Between(min, max) => { + hunger.sustenance = hunger + .sustenance + .saturating_sub(fastrand::usize(*min..*max)); + } + }, + EncounterOutcome::GainGold { amount } => match amount { + RewardAmount::Exact(number) => { + trading.adjust_gold(*number); + } + RewardAmount::Between(min, max) => { + trading.adjust_gold(fastrand::usize(*min..*max)); + } + }, + EncounterOutcome::LoseGold { amount } => match amount { + RewardAmount::Exact(number) => { + trading.adjust_gold(-(*number as isize)); + } + RewardAmount::Between(min, max) => { + trading.adjust_gold(-(fastrand::usize(*min..*max) as isize)); + } + }, + EncounterOutcome::TickWorld { amount } => { + // match amount { + // RewardAmount::Exact(number) => { + // for _ in 0..*number { + // time_ticks.send(WorldTickEvent); + // } + // } + // RewardAmount::Between(min, max) => { + // for _ in 0..fastrand::usize(*min..*max) { + // time_ticks.send(WorldTickEvent); + // } + // } + // } + } + EncounterOutcome::Ambush { .. } => {} + } + } + } + + if let EncounterState::Choice(encounter) = &*encounter_state { + *encounter_state = EncounterState::Consequence(encounter.clone(), event.option.clone()); + } + log::info!("Finished processing"); + } +} + pub fn notify_new_zone( zones: Res<WorldZones>, player_query: Query<&Transform, With<Player>>, mut last_zone: Local<Option<EncounterZone>>, mut encounter_state: ResMut<EncounterState>, + assets: Res<AssetHandles>, + sets: Res<Assets<EncounterSet>>, ) { for position in &player_query { if let Some(zone) = zones.get_container(position.translation.xy()) { @@ -275,9 +442,13 @@ pub fn notify_new_zone( *encounter_state = EncounterState::Choice(encounter); } None => { + let asset_name = zone.zone_type.get_asset_name(); + let handle = assets.encounter_set(asset_name); + let set = sets.get(&handle).unwrap(); + log::info!("New Zone: {:?}", zone.zone_type); *last_zone = Some(zone); - *encounter_state = EncounterState::Choice(gen_encounter()); + *encounter_state = EncounterState::Choice(set.select()); } } } @@ -290,9 +461,13 @@ pub fn notify_new_zone( *encounter_state = EncounterState::Choice(encounter); } None => { + let asset_name = zone.zone_type.get_asset_name(); + let handle = assets.encounter_set(asset_name); + let set = sets.get(&handle).unwrap(); + log::info!("New Zone: {:?}", zone.zone_type); *last_zone = Some(zone); - *encounter_state = EncounterState::Choice(gen_encounter()); + *encounter_state = EncounterState::Choice(set.select()); } }, } diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index 23131befa50b65ee9efb40bfeda2e3b8cf6b2da4..e8fe8f908a5cb382837ee9d4319dc666721e21db 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -16,7 +16,10 @@ mod travel; mod utils; mod world_query; -pub use encounters::{EncounterState, WorldZones}; +pub use encounters::{ + Encounter, EncounterOption, EncounterOutcome, EncounterRequirement, EncounterState, + EncounterType, SelectEncounterOptionEvent, WorldZones, +}; pub use hunger::{HungerState, StarvationMarker}; pub use spawning::PendingLoadState; pub use specialism::CraftSpecialism; @@ -46,6 +49,7 @@ impl Plugin for WorldPlugin { .init_resource::<EncounterState>() .add_event::<PopulateWorldEvent>() .add_event::<WorldTickEvent>() + .add_event::<SelectEncounterOptionEvent>() .add_enter_system(AppState::InGame, |mut commands: Commands| { commands.insert_resource(ActiveLevel { map: String::from("Grantswaith"), @@ -91,6 +95,7 @@ impl Plugin for WorldPlugin { .run_in_state(AppState::InGame) .with_system(encounters::notify_new_zone) .with_system(hunger::handle_entity_starvation) + .with_system(encounters::handle_encounter_option_event) .with_system(|mut events: ResMut<Events<WorldTickEvent>>| { events.clear(); }) diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs index 73fa009fe038187eaf61b8481c8a733dfa2301c1..c680d3559ae6006eef6642521eef26904d520424 100644 --- a/game_core/src/world/spawning.rs +++ b/game_core/src/world/spawning.rs @@ -368,7 +368,7 @@ pub fn populate_world( } }) .for_each(|(maybe_spec, bundle)| { - let is_royal_lampoon = &bundle.town_name.0 == &String::from("The Royal Lampoon"); + let is_royal_lampoon = bundle.town_name.0 == String::from("The Royal Lampoon"); let ent = commands.spawn((bundle, VisibilityBundle::default())).id(); if let Some(spec) = maybe_spec { commands.entity(ent).insert(spec); diff --git a/game_core/src/world/trading.rs b/game_core/src/world/trading.rs index 643fab9c6850c83b0724d77ac6275d28978f1e6f..8d16d76a0befae2dc637a624898b271699475212 100644 --- a/game_core/src/world/trading.rs +++ b/game_core/src/world/trading.rs @@ -144,6 +144,7 @@ impl TradingState { pub fn adjust_gold(&mut self, adjustment: impl AsPrimitive<isize>) { self.gold += adjustment.as_(); + self.gold = self.gold.max(0); } pub fn remove_items( @@ -168,6 +169,29 @@ impl TradingState { } } + pub fn force_remove_items( + &mut self, + identifier: impl ToString, + amount: impl AsPrimitive<usize>, + ) -> bool { + match self.items.entry(identifier.to_string()) { + Entry::Occupied(mut e) => { + let amount = amount.as_(); + if e.get() >= &amount { + *e.get_mut() -= amount; + if e.get() == &0 { + e.remove(); + } + true + } else { + e.remove(); + true + } + } + Entry::Vacant(_) => false, + } + } + pub fn add_items(&mut self, identifier: impl ToString, amount: impl AsPrimitive<usize>) { *self.items.entry(identifier.to_string()).or_insert(0) += amount.as_() } diff --git a/raw_assets/trade_goods.toml b/raw_assets/trade_goods.toml index f7ad6a676fd43b6d5c9a6c94b382ca9124437fce..f50a002393bd8958a8a7e04ecc72bd09f7e6927c 100644 --- a/raw_assets/trade_goods.toml +++ b/raw_assets/trade_goods.toml @@ -133,14 +133,14 @@ gold_value = 19 ["Religious Artifact"] name = "Religious Artifact" -icon = ["icons", 34] +icon = ["icons", 40] food_value = 0 gold_value = 125 ["Strange Doll"] name = "Polished Gemstone" -icon = ["icons", 34] +icon = ["icons", 41] food_value = 0 gold_value = 19