From b13c3b840da17950a6d8b20be6fcf55253b6ec4f Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Fri, 9 Dec 2022 12:24:27 +0000 Subject: [PATCH] Generate resources --- Cargo.lock | 1 + .../trade_manifests/whitestone.manifest.toml | 169 ++++++++++++++++++ game_core/Cargo.toml | 1 + game_core/src/assets/asset_types/mod.rs | 1 + .../src/assets/asset_types/trade_manifest.rs | 45 +++++ game_core/src/assets/loader.rs | 2 + game_core/src/assets/mod.rs | 7 + game_core/src/assets/resources.rs | 3 + game_core/src/assets/startup.rs | 1 + game_core/src/const_data/mod.rs | 25 +++ game_core/src/lib.rs | 1 + game_core/src/persistance/save_file.rs | 37 +++- game_core/src/system/utilities.rs | 3 + game_core/src/ui/components/inset_icon.rs | 1 + game_core/src/world/mod.rs | 14 +- game_core/src/world/spawning.rs | 65 ++++++- game_core/src/world/towns.rs | 16 +- game_core/src/world/trading.rs | 163 ++++++++++++++++- game_core/src/world/travel.rs | 38 +++- raw_assets/trade_goods.toml | 77 ++++++++ 20 files changed, 650 insertions(+), 20 deletions(-) create mode 100644 assets/trade_manifests/whitestone.manifest.toml create mode 100644 game_core/src/assets/asset_types/trade_manifest.rs create mode 100644 game_core/src/const_data/mod.rs create mode 100644 raw_assets/trade_goods.toml diff --git a/Cargo.lock b/Cargo.lock index 72e7089..d9294e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,7 @@ dependencies = [ "kayak_font", "kayak_ui", "kira", + "lazy_static", "ldtk_rust", "log", "micro_asset_io", diff --git a/assets/trade_manifests/whitestone.manifest.toml b/assets/trade_manifests/whitestone.manifest.toml new file mode 100644 index 0000000..dec88c9 --- /dev/null +++ b/assets/trade_manifests/whitestone.manifest.toml @@ -0,0 +1,169 @@ +["Spare Wheel"] +# It takes 5 world ticks to produce a new spare wheel +tick_decay = 0.2 +# Spare wheels will be produced up to a natural maximum of 2. +# When under this number, the undersupply_trend will be applied +# to the tick_decay. When equal or greater, the oversupply_trend +# will be applied +natural_limit = 2 +# Each entry lists the cost multiplier from the previous amount +# up to and including the specified quantity (left part). +# E.g. (1, 1.4) means that if the town has 0 or 1 wheels, the base +# price is multiplied by 1.4. +cost_multipliers = [ + [1, 1.4], + [2, 1.3], + [3, 1.2], + [99999, 1], +] +# When there are more wheels than the natural limit (e.g. player sells a lot of wheels) +# This strategy will be applied to the inventory. "Halt" means nothing will be done +oversupply_trend = "halt" + +[Armour] +# It takes 10 world ticks to consume one unit of armour. +# 10 ticks = 50km +tick_decay = 0.1 +natural_limit = 10 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] +oversupply_trend = "halt" + +[Weapons] +tick_decay = 0.1 +natural_limit = 10 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +["Luxury Goods"] +tick_decay = 0.05 +natural_limit = 5 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] +oversupply_trend = "halt" + +[Ale] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Milk] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Cheese] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Berries] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Corn] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Tomato] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Chili] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Grapes] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] + +[Wheat] +tick_decay = 0.25 +natural_limit = 0 +cost_multipliers = [ + [5, 1.4], + [9, 1.25], + [10, 1], + [15, 0.8], + [30, 0.7], + [99999, 0.6] +] diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml index 69a0773..47be554 100644 --- a/game_core/Cargo.toml +++ b/game_core/Cargo.toml @@ -38,6 +38,7 @@ kayak_font.workspace = true fake = "2.5.0" directories = "4.0.1" bevy_prototype_lyon = "0.7.2" +lazy_static = "1.4.0" #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"} diff --git a/game_core/src/assets/asset_types/mod.rs b/game_core/src/assets/asset_types/mod.rs index fc46ae2..ecb2c41 100644 --- a/game_core/src/assets/asset_types/mod.rs +++ b/game_core/src/assets/asset_types/mod.rs @@ -1 +1,2 @@ pub mod ldtk_project; +pub mod trade_manifest; diff --git a/game_core/src/assets/asset_types/trade_manifest.rs b/game_core/src/assets/asset_types/trade_manifest.rs new file mode 100644 index 0000000..e31a42f --- /dev/null +++ b/game_core/src/assets/asset_types/trade_manifest.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; + +use anyhow::Error; +use bevy::asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset}; + +use crate::world::{RosterEntry, TradeManifest}; + +pub struct TradeManifestJsonLoader; +pub struct TradeManifestTomlLoader; + +impl AssetLoader for TradeManifestJsonLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let manifest: TradeManifest = serde_json::from_slice(bytes)?; + load_context.set_default_asset(LoadedAsset::new(manifest)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["manifest.json"] + } +} + +impl AssetLoader for TradeManifestTomlLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let manifest: TradeManifest = toml::from_slice(bytes)?; + load_context.set_default_asset(LoadedAsset::new(manifest)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["manifest.toml"] + } +} diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs index cb7994a..01abd63 100644 --- a/game_core/src/assets/loader.rs +++ b/game_core/src/assets/loader.rs @@ -10,6 +10,7 @@ use micro_musicbox::prelude::AudioSource; use crate::assets::asset_types::ldtk_project::LdtkProject; use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig}; +use crate::world::TradeManifest; #[derive(SystemParam)] pub struct AssetTypeLoader<'w, 's> { @@ -62,6 +63,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> { load_basic_type!(load_apack, APack => apacks); 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); pub fn load_spritesheet( &mut self, diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs index 33fdbf7..bd42743 100644 --- a/game_core/src/assets/mod.rs +++ b/game_core/src/assets/mod.rs @@ -14,7 +14,11 @@ pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, Sprit use crate::assets::apack_handler::handle_apack_process_events; use crate::assets::asset_types::ldtk_project::handle_ldtk_project_events; +use crate::assets::asset_types::trade_manifest::{ + TradeManifestJsonLoader, TradeManifestTomlLoader, +}; use crate::system::flow::AppState; +use crate::world::TradeManifest; pub struct AssetsPlugin; impl Plugin for AssetsPlugin { @@ -23,7 +27,10 @@ impl Plugin for AssetsPlugin { .init_resource::<LevelIndex>() .init_resource::<TilesetIndex>() .add_asset::<LdtkProject>() + .add_asset::<TradeManifest>() .add_asset_loader(LdtkLoader) + .add_asset_loader(TradeManifestJsonLoader) + .add_asset_loader(TradeManifestTomlLoader) .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 459d6ef..ba4ed40 100644 --- a/game_core/src/assets/resources.rs +++ b/game_core/src/assets/resources.rs @@ -6,6 +6,7 @@ use micro_musicbox::prelude::AudioSource; use micro_musicbox::utilities::{SuppliesAudio, TrackType}; use crate::assets::asset_types::ldtk_project::LdtkProject; +use crate::world::TradeManifest; #[derive(Copy, Clone, Debug)] pub struct SpriteSheetConfig { @@ -44,6 +45,7 @@ pub struct AssetHandles { pub apacks: HashMap<String, Handle<APack>>, pub ldtk: HashMap<String, Handle<LdtkProject>>, pub kayak_fonts: HashMap<String, Handle<KayakFont>>, + pub trade_manifests: HashMap<String, Handle<TradeManifest>>, } macro_rules! fetch_wrapper { @@ -76,6 +78,7 @@ impl AssetHandles { fetch_wrapper!(apack, APack => apacks); fetch_wrapper!(ldtk, LdtkProject => ldtk); fetch_wrapper!(kayak_font, KayakFont => kayak_fonts); + fetch_wrapper!(trade_manifest, TradeManifest => trade_manifests); } impl SuppliesAudio for AssetHandles { diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index 8fd4020..e98c997 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -15,6 +15,7 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) { loader.load_images(&[("splash.png", "splash")]); loader.load_audio(&[("splash_sting.mp3", "splash_sting")]); loader.load_apack(&[("resources.apack", "resources")]); + loader.load_trade_manifest(&[("trade_manifests/whitestone.manifest.toml", "whitestone")]); } pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) { diff --git a/game_core/src/const_data/mod.rs b/game_core/src/const_data/mod.rs new file mode 100644 index 0000000..94d05af --- /dev/null +++ b/game_core/src/const_data/mod.rs @@ -0,0 +1,25 @@ +//! Data embedded in the game for the sake of convenience. Future iterations +//! should load this info as assets, to make it easier to customise & reload + +use std::collections::HashMap; + +use lazy_static::lazy_static; + +use crate::world::TradeGood; + +lazy_static! { + pub static ref TRADE_GOODS: HashMap<String, TradeGood> = + toml::from_slice(include_bytes!("../../../raw_assets/trade_goods.toml")).unwrap(); +} + +/// Convert the name of a trade good into its representation. +/// Will panic if the name is incorrect; usually useful in a +/// static context +pub fn get_goods_from_name(name: impl ToString) -> TradeGood { + (*TRADE_GOODS)[&name.to_string()].clone() +} +/// Convert the name of a trade good into its representation. +/// Preferred in a dynamic context where the `Option` can be checked +pub fn get_goods_from_name_checked(name: impl ToString) -> Option<TradeGood> { + (*TRADE_GOODS).get(&name.to_string()).cloned() +} diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs index 1d04c08..0bc9aa3 100644 --- a/game_core/src/lib.rs +++ b/game_core/src/lib.rs @@ -1,6 +1,7 @@ #![feature(result_option_inspect)] pub mod assets; +pub mod const_data; pub mod multiplayer; pub mod persistance; pub mod splash_screen; diff --git a/game_core/src/persistance/save_file.rs b/game_core/src/persistance/save_file.rs index a409640..681702d 100644 --- a/game_core/src/persistance/save_file.rs +++ b/game_core/src/persistance/save_file.rs @@ -1,12 +1,9 @@ +use std::collections::HashMap; use std::fs; use std::fs::File; -use std::io::Write; -use std::ops::Deref; -use std::path::Path; use bevy::math::Vec3; use bevy::prelude::*; -use bevy::utils::HashMap; use iyes_loopless::state::NextState; use serde::{Deserialize, Serialize}; @@ -14,12 +11,15 @@ use crate::persistance::fs_utils::{get_root_save_dir, AUTOSAVE_NAME}; use crate::states::Player; use crate::system::flow::AppState; use crate::world::{ - CurrentResidence, DistanceTravelled, EncounterState, HungerState, PendingLoadState, - TradingState, TravelPath, TravelTarget, + ActiveLevel, CurrentResidence, DistanceTravelled, EncounterState, HungerState, + PendingLoadState, TownName, TradeManifestTickState, TradingState, TravelPath, TravelTarget, }; +pub type TownPersistenceData = HashMap<String, (TradingState, HungerState, TradeManifestTickState)>; + #[derive(Serialize, Deserialize, Debug, Resource)] pub struct PersistenceState { + pub map_name: String, pub player_location: Vec3, pub player_inventory: TradingState, pub player_hunger: HungerState, @@ -31,7 +31,7 @@ pub struct PersistenceState { pub player_total_distance: DistanceTravelled, pub previous_location: CurrentResidence, pub encounter_state: EncounterState, - pub town_states: HashMap<String, (TradingState, HungerState)>, + pub town_states: TownPersistenceData, } #[derive(Clone, Debug)] @@ -75,8 +75,25 @@ pub fn sync_state_to_persistence( mut commands: Commands, mut state: Option<ResMut<PersistenceState>>, player_query: PlayerInfoQuery, + town_query: Query<( + &TownName, + &TradingState, + &HungerState, + &TradeManifestTickState, + )>, encounters: Res<EncounterState>, + active_map: Res<ActiveLevel>, ) { + let town_states = town_query + .iter() + .map(|(name, trading, hunger, manifest)| { + ( + name.0.clone(), + (trading.clone(), hunger.clone(), manifest.clone()), + ) + }) + .collect::<TownPersistenceData>(); + match state { Some(mut state) => { if let Some(( @@ -90,6 +107,7 @@ pub fn sync_state_to_persistence( )) = player_query.iter().next() { *state = PersistenceState { + map_name: active_map.map.clone(), player_location: transform.translation, player_inventory: trading.clone(), player_hunger: hunger.clone(), @@ -98,7 +116,7 @@ pub fn sync_state_to_persistence( travel_target: maybe_target.cloned(), previous_location: residence.clone(), encounter_state: encounters.clone(), - town_states: Default::default(), + town_states, }; } } @@ -114,6 +132,7 @@ pub fn sync_state_to_persistence( )) = player_query.iter().next() { commands.insert_resource(PersistenceState { + map_name: active_map.map.clone(), player_location: transform.translation, player_inventory: trading.clone(), player_hunger: hunger.clone(), @@ -122,7 +141,7 @@ pub fn sync_state_to_persistence( travel_target: maybe_target.cloned(), previous_location: residence.clone(), encounter_state: encounters.clone(), - town_states: Default::default(), + town_states, }) } } diff --git a/game_core/src/system/utilities.rs b/game_core/src/system/utilities.rs index 1c5028c..9356630 100644 --- a/game_core/src/system/utilities.rs +++ b/game_core/src/system/utilities.rs @@ -144,6 +144,9 @@ where } } +pub fn format_distance(distance: f32) -> f32 { + distance / 10.0 +} pub fn format_ui_distance(distance: f32) -> String { format!("{:.0} km", distance / 10.0) } diff --git a/game_core/src/ui/components/inset_icon.rs b/game_core/src/ui/components/inset_icon.rs index e05e25d..3d080d9 100644 --- a/game_core/src/ui/components/inset_icon.rs +++ b/game_core/src/ui/components/inset_icon.rs @@ -8,6 +8,7 @@ use crate::parent_widget; use crate::ui::prelude::*; #[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] pub enum IconContent { Image(String), Atlas(String, usize), diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index 91dbefc..971f5d4 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -14,14 +14,18 @@ mod world_query; pub use encounters::{EncounterState, WorldZones}; pub use spawning::PendingLoadState; -pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget}; -pub use trading::{HungerState, ItemName, TradeGood, TradingState}; +pub use towns::{CurrentResidence, PathingResult, TownName, TownPaths, TravelPath, TravelTarget}; +pub use trading::{ + HungerState, ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState, + TradeRoster, TradingState, +}; pub use travel::DistanceTravelled; +pub use utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked}; pub use world_query::{CameraBounds, MapQuery}; use crate::system::flow::AppState; use crate::world::spawning::PopulateWorldEvent; -use crate::world::utils::ActiveLevel; +use crate::world::travel::WorldTickEvent; pub struct WorldPlugin; impl Plugin for WorldPlugin { @@ -30,6 +34,7 @@ impl Plugin for WorldPlugin { .init_resource::<WorldZones>() .init_resource::<EncounterState>() .add_event::<PopulateWorldEvent>() + .add_event::<WorldTickEvent>() .add_enter_system(AppState::InGame, |mut commands: Commands| { commands.insert_resource(ActiveLevel { map: String::from("Grantswaith"), @@ -44,8 +49,9 @@ impl Plugin for WorldPlugin { .with_system(spawning::spawn_world_data) .with_system(spawning::populate_world) .with_system(travel::tick_travelling_merchant) + .with_system(travel::track_travel) .with_system(encounters::notify_new_zone) - + .with_system(trading::tick_manifests) .into(), ); } diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs index 73240a8..58e64ef 100644 --- a/game_core/src/world/spawning.rs +++ b/game_core/src/world/spawning.rs @@ -1,5 +1,8 @@ +use std::collections::HashMap; + use bevy::prelude::*; use bevy_ecs_tilemap::prelude::*; +use ldtk_rust::EntityInstance; use micro_banimate::definitions::SimpleAnimationBundle; use num_traits::AsPrimitive; use serde_json::Value; @@ -9,11 +12,14 @@ use crate::persistance::PersistenceState; use crate::states::Player; use crate::system::camera::ChaseCam; use crate::world::encounters::WorldZones; -use crate::world::towns::{CurrentResidence, TownPaths}; +use crate::world::towns::{CurrentResidence, TownBundle, TownPaths}; use crate::world::travel::DistanceTravelled; use crate::world::utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32}; use crate::world::world_query::MapQuery; -use crate::world::{EncounterState, HungerState, TradingState, TravelPath, TravelTarget}; +use crate::world::{ + EncounterState, HungerState, TownName, TradeManifestTickState, TradingState, TravelPath, + TravelTarget, +}; #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] pub struct PopulateWorldEvent; @@ -280,6 +286,61 @@ pub fn populate_world( } let world_zones = WorldZones::from_entities(level.px_hei, MapQuery::get_entities_of(level)); + MapQuery::get_entities_of(level) + .into_iter() + .filter(|instance| instance.identifier == *"Town") + .map(|instance| { + let mut name = String::new(); + let mut manifest_name = String::new(); + + instance + .field_instances + .iter() + .for_each(|inst| match &*inst.identifier { + "name" => name = inst.value.as_ref().unwrap().as_str().unwrap().to_string(), + "trade_manifest" => { + manifest_name = inst + .value + .as_ref() + .map(|val| String::from(val.as_str().unwrap())) + .unwrap_or_default() + } + _ => {} + }); + + if manifest_name.is_empty() { + (name, String::from("whitestone")) + } else { + (name, manifest_name) + } + }) + .map(|(name, manifest)| { + match pending_load + .as_ref() + .and_then(|pl| pl.0.town_states.get(&name)) + { + Some((trade, hunger, tick)) => TownBundle { + town_name: TownName(name.clone()), + manifest: assets.trade_manifest(manifest), + trade_state: trade.clone(), + hunger_state: hunger.clone(), + manifest_state: tick.clone(), + }, + None => TownBundle { + town_name: TownName(name.clone()), + manifest: assets.trade_manifest(manifest), + trade_state: TradingState { + gold: 250, + items: HashMap::default(), + }, + hunger_state: HungerState::default(), + manifest_state: TradeManifestTickState::default(), + }, + } + }) + .for_each(|bundle| { + commands.spawn((WorldLinked, bundle)); + }); commands.insert_resource(trade_routes); commands.insert_resource(world_zones); diff --git a/game_core/src/world/towns.rs b/game_core/src/world/towns.rs index 39921b8..d696048 100644 --- a/game_core/src/world/towns.rs +++ b/game_core/src/world/towns.rs @@ -1,11 +1,13 @@ use bevy::math::{vec2, Vec2}; -use bevy::prelude::{Bundle, Component, Resource}; +use bevy::prelude::{Bundle, Component, Handle, Resource}; use bevy::utils::HashMap; use ldtk_rust::{EntityInstance, Level}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::world::trading::TradeManifestTickState; use crate::world::utils::{grid_to_px, px_to_grid}; +use crate::world::{HungerState, TradeManifest, TradingState}; pub type Town = String; @@ -34,6 +36,9 @@ impl CurrentResidence { #[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct TravelTarget(pub String); +#[derive(Component, Serialize, Deserialize, Debug, Clone)] +pub struct TownName(pub String); + #[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct TravelPath { pub path: Vec<Vec2>, @@ -257,3 +262,12 @@ impl From<Vec<&EntityInstance>> for TownPaths { TownPaths { routes } } } + +#[derive(Bundle)] +pub struct TownBundle { + pub town_name: TownName, + pub trade_state: TradingState, + pub hunger_state: HungerState, + pub manifest: Handle<TradeManifest>, + pub manifest_state: TradeManifestTickState, +} diff --git a/game_core/src/world/trading.rs b/game_core/src/world/trading.rs index 6b70332..cc43cc5 100644 --- a/game_core/src/world/trading.rs +++ b/game_core/src/world/trading.rs @@ -1,10 +1,15 @@ -use bevy::prelude::{Component, Resource}; -use bevy::utils::hashbrown::hash_map::Entry; -use bevy::utils::HashMap; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::ops::{Deref, DerefMut}; + +use bevy::prelude::{Assets, Component, EventReader, Handle, Query, Res, Resource}; +use bevy::reflect::TypeUuid; use num_traits::AsPrimitive; use serde::{Deserialize, Serialize}; use crate::ui::components::IconContent; +use crate::world::travel::WorldTickEvent; pub type ItemName = String; @@ -24,6 +29,94 @@ pub struct HungerState { pub struct TradeGood { pub name: String, pub icon: IconContent, + pub food_value: usize, +} + +impl PartialEq for TradeGood { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} +impl PartialEq<String> for TradeGood { + fn eq(&self, other: &String) -> bool { + &self.name == other + } +} +impl Eq for TradeGood {} +impl Hash for TradeGood { + fn hash<H: Hasher>(&self, state: &mut H) { + self.name.hash(state) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RosterSupplyTrend { + #[serde(alias = "converge")] + #[default] + Converge, + #[serde(alias = "consume")] + Consume, + #[serde(alias = "produce")] + Produce, + #[serde(alias = "halt")] + Halt, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RosterEntry { + pub tick_decay: f32, + pub natural_limit: usize, + pub cost_multipliers: Vec<(usize, f32)>, + #[serde(default)] + pub undersupply_trend: RosterSupplyTrend, + #[serde(default)] + pub oversupply_trend: RosterSupplyTrend, +} + +#[derive(Debug, Clone, PartialEq, Default, TypeUuid, Serialize, Deserialize)] +#[serde(from = "HashMap<String, RosterEntry>")] +#[uuid = "7e395a74-7745-11ed-8b4e-ef01db57341b"] +pub struct TradeManifest(HashMap<String, RosterEntry>); +impl From<HashMap<String, RosterEntry>> for TradeManifest { + fn from(value: HashMap<String, RosterEntry>) -> Self { + Self(value) + } +} +impl Deref for TradeManifest { + type Target = HashMap<String, RosterEntry>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for TradeManifest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Component)] +pub struct TradeManifestTickState(HashMap<String, f32>); +impl From<HashMap<String, f32>> for TradeManifestTickState { + fn from(value: HashMap<String, f32>) -> Self { + Self(value) + } +} +impl Deref for TradeManifestTickState { + type Target = HashMap<String, f32>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for TradeManifestTickState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Component)] +pub struct TradeRoster { + pub goods_relations: HashMap<TradeGood, RosterEntry>, + pub production_ticks: HashMap<TradeGood, f32>, } impl TradingState { @@ -94,3 +187,67 @@ impl TradingState { } } } + +pub fn tick_manifests( + events: EventReader<WorldTickEvent>, + mut town_query: Query<( + &mut TradingState, + &Handle<TradeManifest>, + &mut TradeManifestTickState, + )>, + manifests: Res<Assets<TradeManifest>>, +) { + if events.is_empty() { + return; + } + log::info!("TICK MANIFESTS"); + + for (mut state, manifest, mut tick_state) in &mut town_query { + if let Some(manifest) = manifests.get(manifest) { + for (good, entry) in manifest.iter() { + let quantity = *state.items.get(good).to_owned().unwrap_or(&0); + + let ts = tick_state.entry(good.clone()).or_default(); + + match if quantity >= entry.natural_limit { + &entry.oversupply_trend + } else { + &entry.undersupply_trend + } { + RosterSupplyTrend::Converge => { + if quantity != entry.natural_limit { + *ts += entry.tick_decay; + while ts > &mut 1.0 { + *ts -= 1.0; + if quantity > entry.natural_limit { + let mut item = state.items.entry(good.clone()).or_insert(0); + *item = item.saturating_sub(1); + } else { + let mut item = state.items.entry(good.clone()).or_insert(0); + *item = item.saturating_add(1); + } + } + } + } + RosterSupplyTrend::Consume => { + *ts += entry.tick_decay; + while ts > &mut 1.0 { + *ts -= 1.0; + let mut item = state.items.entry(good.clone()).or_insert(0); + *item = item.saturating_sub(1); + } + } + RosterSupplyTrend::Produce => { + *ts += entry.tick_decay; + while ts > &mut 1.0 { + *ts -= 1.0; + let mut item = state.items.entry(good.clone()).or_insert(0); + *item = item.saturating_add(1); + } + } + RosterSupplyTrend::Halt => {} + } + } + } + } +} diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs index 6d58b11..e9e6e37 100644 --- a/game_core/src/world/travel.rs +++ b/game_core/src/world/travel.rs @@ -1,16 +1,29 @@ -use std::ops::SubAssign; +use std::ops::{Deref, DerefMut, SubAssign}; use bevy::math::Vec3Swizzles; use bevy::prelude::*; use serde::{Deserialize, Serialize}; use crate::persistance::SaveFileEvent; +use crate::states::Player; +use crate::system::utilities::format_distance; use crate::world::encounters::EncounterState; use crate::world::towns::{CurrentResidence, TravelPath, TravelTarget}; use crate::world::PathingResult; #[derive(Component, Serialize, Deserialize, Debug, Copy, Clone, Default)] pub struct DistanceTravelled(pub f32); +impl Deref for DistanceTravelled { + type Target = f32; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for DistanceTravelled { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} pub fn tick_travelling_merchant( mut commands: Commands, @@ -56,3 +69,26 @@ pub fn tick_travelling_merchant( } } } + +pub struct WorldTickEvent; + +pub fn track_travel( + mut tracked: Local<DistanceTravelled>, + player_query: Query<&DistanceTravelled, With<Player>>, + mut events: EventWriter<WorldTickEvent>, +) { + let before = **tracked.deref(); + let after = { + if let Ok(player_dist) = player_query.get_single() { + *tracked = *player_dist; + *player_dist.deref() + } else { + before + } + }; + + if format_distance(before) % 5.0 > format_distance(after) % 5.0 { + log::info!("ANOTHER 5KM HAS PASSED"); + events.send(WorldTickEvent); + } +} diff --git a/raw_assets/trade_goods.toml b/raw_assets/trade_goods.toml new file mode 100644 index 0000000..a92a5d5 --- /dev/null +++ b/raw_assets/trade_goods.toml @@ -0,0 +1,77 @@ +["Armour"] +name = "Armour" +icon = ["icons", 12] +food_value = 0 +gold_value = 12 + +["Weapons"] +name = "Weapons" +icon = ["icons", 1] +food_value = 0 +gold_value = 13 + +["Luxury Goods"] +name = "Luxury Goods" +icon = ["icons", 13] +food_value = 0 +gold_value = 27 + +["Ale"] +name = "Ale" +icon = ["icons", 11] +food_value = 0 +gold_value = 6 + +["Milk"] +name = "Milk" +icon = ["icons", 2] +food_value = 2 +gold_value = 5 + +["Cheese"] +name = "Cheese" +icon = ["icons", 3] +food_value = 3 +gold_value = 6 + +["Spare Wheel"] +name = "Spare Wheel" +icon = ["icons", 28] +food_value = 0 +gold_value = 125 + +["Berries"] +name = "Berries" +icon = ["icons", 27] +food_value = 1 +gold_value = 4 + +["Corn"] +name = "Corn" +icon = ["icons", 26] +food_value = 2 +gold_value = 6 + +["Tomato"] +name = "Tomato" +icon = ["icons", 25] +food_value = 2 +gold_value = 6 + +["Chili"] +name = "Chili" +icon = ["icons", 24] +food_value = 2 +gold_value = 8 + +["Grapes"] +name = "Grapes" +icon = ["icons", 23] +food_value = 1 +gold_value = 9 + +["Wheat"] +name = "Wheat" +icon = ["icons", 22] +food_value = 1 +gold_value = 3 -- GitLab