Skip to content
Snippets Groups Projects
Verified Commit 94947743 authored by Louis's avatar Louis :fire:
Browse files

Crafting and hunger descriptions in taverns

parent 245c752e
No related branches found
No related tags found
No related merge requests found
Pipeline #254 failed with stages
in 3 minutes and 58 seconds
...@@ -4,12 +4,23 @@ ...@@ -4,12 +4,23 @@
use std::collections::HashMap; use std::collections::HashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::world::TradeGood; use crate::world::{CraftSpecialism, TradeGood};
#[derive(Serialize, Deserialize)]
struct RawSpecialisms {
specialism: Vec<CraftSpecialism>,
}
lazy_static! { lazy_static! {
pub static ref TRADE_GOODS: HashMap<String, TradeGood> = pub static ref TRADE_GOODS: HashMap<String, TradeGood> =
toml::from_slice(include_bytes!("../../../raw_assets/trade_goods.toml")).unwrap(); toml::from_slice(include_bytes!("../../../raw_assets/trade_goods.toml")).unwrap();
pub static ref SPECIALISMS: Vec<CraftSpecialism> = {
let initial: RawSpecialisms =
toml::from_slice(include_bytes!("../../../raw_assets/specialisms.toml")).unwrap();
initial.specialism
};
} }
/// Convert the name of a trade good into its representation. /// Convert the name of a trade good into its representation.
......
...@@ -6,7 +6,7 @@ use bevy::prelude::{Entity, Query, Res, ResMut, Resource, With}; ...@@ -6,7 +6,7 @@ use bevy::prelude::{Entity, Query, Res, ResMut, Resource, With};
use crate::const_data::{get_goods_from_name, get_goods_from_name_checked, TRADE_GOODS}; use crate::const_data::{get_goods_from_name, get_goods_from_name_checked, TRADE_GOODS};
use crate::states::Player; use crate::states::Player;
use crate::world::{ use crate::world::{
CurrentResidence, TownName, TradeGood, TradeManifest, TradingState, TravelTarget, CurrentResidence, HungerState, TownName, TradeGood, TradeManifest, TradingState, TravelTarget,
}; };
#[derive(Clone)] #[derive(Clone)]
...@@ -24,6 +24,7 @@ pub struct UITradeData { ...@@ -24,6 +24,7 @@ pub struct UITradeData {
pub player_gold: usize, pub player_gold: usize,
pub player_entity: Option<Entity>, pub player_entity: Option<Entity>,
pub shop_items: Vec<ItemTradeData>, pub shop_items: Vec<ItemTradeData>,
pub hunger_descriptor: String,
} }
pub fn sync_ui_trade_data( pub fn sync_ui_trade_data(
...@@ -36,7 +37,13 @@ pub fn sync_ui_trade_data( ...@@ -36,7 +37,13 @@ pub fn sync_ui_trade_data(
), ),
With<Player>, With<Player>,
>, >,
town_query: Query<(Entity, &TownName, &TradingState, &Handle<TradeManifest>)>, town_query: Query<(
Entity,
&TownName,
&TradingState,
&HungerState,
&Handle<TradeManifest>,
)>,
manifests: Res<Assets<TradeManifest>>, manifests: Res<Assets<TradeManifest>>,
mut trade_data: ResMut<UITradeData>, mut trade_data: ResMut<UITradeData>,
) { ) {
...@@ -57,9 +64,9 @@ pub fn sync_ui_trade_data( ...@@ -57,9 +64,9 @@ pub fn sync_ui_trade_data(
CurrentResidence::TravellingFrom(..) => String::new(), CurrentResidence::TravellingFrom(..) => String::new(),
}); });
let (town_entity, _, town_trading_state, manifest_handle) = match town_query let (town_entity, _, town_trading_state, hunger_state, manifest_handle) = match town_query
.into_iter() .into_iter()
.find(|(_, name, _, _)| town_name == *name.0) .find(|(_, name, _, _, _)| town_name == *name.0)
{ {
Some(v) => v, Some(v) => v,
None => { None => {
...@@ -71,6 +78,7 @@ pub fn sync_ui_trade_data( ...@@ -71,6 +78,7 @@ pub fn sync_ui_trade_data(
trade_data.town_entity = Some(town_entity); trade_data.town_entity = Some(town_entity);
trade_data.town_gold = town_trading_state.gold.max(0) as usize; trade_data.town_gold = town_trading_state.gold.max(0) as usize;
trade_data.hunger_descriptor = hunger_state.get_town_descriptor();
for (item_name, trade_good) in TRADE_GOODS.iter() { for (item_name, trade_good) in TRADE_GOODS.iter() {
let manifest = match manifests.get(manifest_handle) { let manifest = match manifests.get(manifest_handle) {
......
...@@ -249,6 +249,13 @@ pub mod context { ...@@ -249,6 +249,13 @@ pub mod context {
UITradeData, UITradeData,
render_shop_panel render_shop_panel
); );
register_widget_with_resource!(
widget_context,
TavernPanelProps,
EmptyState,
UITradeData,
render_tavern_panel
);
register_widget_with_resource!( register_widget_with_resource!(
widget_context, widget_context,
StatsPanelProps, StatsPanelProps,
......
mod encounter_panel; mod encounter_panel;
mod shop_panel; mod shop_panel;
mod stats_panel; mod stats_panel;
mod tavern_panel;
mod town_menu; mod town_menu;
mod transit_panel; mod transit_panel;
pub use encounter_panel::{render_encounter_panel, EncounterPanel, EncounterPanelProps}; pub use encounter_panel::{render_encounter_panel, EncounterPanel, EncounterPanelProps};
pub use shop_panel::{render_shop_panel, ShopPanel, ShopPanelProps}; pub use shop_panel::{render_shop_panel, ShopPanel, ShopPanelProps};
pub use stats_panel::{render_stats_panel, StatsPanel, StatsPanelProps}; pub use stats_panel::{render_stats_panel, StatsPanel, StatsPanelProps};
pub use tavern_panel::{render_tavern_panel, TavernPanel, TavernPanelProps};
pub use town_menu::{ pub use town_menu::{
render_town_menu_panel, TownMenuPanel, TownMenuPanelProps, TownMenuPanelState, render_town_menu_panel, TownMenuPanel, TownMenuPanelProps, TownMenuPanelState,
}; };
......
use bevy::prelude::*;
use kayak_ui::prelude::*;
use kayak_ui::widgets::{
ElementBundle, ScrollBoxBundle, ScrollBoxProps, ScrollContextProviderBundle, TextProps,
TextWidgetBundle,
};
use crate::assets::AssetHandles;
use crate::states::Player;
use crate::system::utilities::format_ui_distance;
use crate::ui::components::*;
use crate::ui::prelude::*;
use crate::ui::sync::{UITradeData, UITravelInfo};
use crate::ui::widgets::*;
use crate::world::{CurrentResidence, MapQuery, TownPaths};
use crate::{basic_widget, empty_props, on_button_click};
empty_props!(TavernPanelProps);
basic_widget!(TavernPanelProps => TavernPanel);
pub fn render_tavern_panel(
In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
mut commands: Commands,
ui_data: Res<UITradeData>,
query: Query<&TavernPanelProps>,
) -> bool {
let parent_id = Some(entity);
if let Ok(props) = query.get(entity) {
rsx! {
<ElementBundle>
<TextWidgetBundle
styles={KStyle {
left: stretch(1.0),
right: stretch(1.0),
color: value(Color::BLACK),
..Default::default()
}}
text={TextProps {
size: 32.0,
content: String::from("A strange bartender eyes you suspiciously"),
..Default::default()
}}
/>
<ScrollContextProviderBundle>
<ScrollBoxBundle>
<TextWidgetBundle
styles={KStyle {
top: px(20.0),
bottom: px(20.0),
left: stretch(1.0),
right: stretch(1.0),
color: value(Color::BLACK),
..Default::default()
}}
text={TextProps {
size: 28.0,
content: ui_data.hunger_descriptor.clone(),
..Default::default()
}}
/>
</ScrollBoxBundle>
</ScrollContextProviderBundle>
</ElementBundle>
};
}
true
}
...@@ -171,19 +171,7 @@ pub fn render_town_menu_panel( ...@@ -171,19 +171,7 @@ pub fn render_town_menu_panel(
} }
TownMenuTab::Tavern => { TownMenuTab::Tavern => {
constructor! { constructor! {
<TextWidgetBundle <TavernPanel />
styles={KStyle {
left: stretch(1.0),
right: stretch(1.0),
color: value(Color::BLACK),
..Default::default()
}}
text={TextProps {
size: 32.0,
content: String::from("A strange bartender eyes you suspiciously"),
..Default::default()
}}
/>
} }
} }
TownMenuTab::None => {} TownMenuTab::None => {}
......
...@@ -27,6 +27,23 @@ impl HungerState { ...@@ -27,6 +27,23 @@ impl HungerState {
starvation_ticks: 0.0, starvation_ticks: 0.0,
} }
} }
pub fn get_town_descriptor(&self) -> String {
let percent = self.sustenance as f32 / TOWN_FOOD_TARGET as f32;
if percent > 1.0 {
String::from("The townsfolk have never looked healthier. They are bustling and joyous, brimming with energy. You'll have one of whatever they've been having!")
} else if percent > 0.75 {
String::from("The atmosphere is generally lively and upbeat, but as you glance around you notice a few worried faces.")
} else if percent > 0.55 {
String::from("You find yourself unable to strike up a conversation with any of the local townsfolk. There's a certain desperation about the people you see, but you can't quite place why. In the background, you do not hear the usual clinking of crockery and cups.")
} else if percent > 0.15 {
String::from("The tavern is looking quite empty, with only a few elderly or especially scruffy patrons within sight. None have a beer or food in front of them; instead they count the rings in the wooden tables.")
} else if percent > 0.001 {
String::from("Every wretched soul whose path you have crossed to reach the tavern has had the same hungry look in their eyes. The atmosphere is tense, as if at any moment the whole town could erupt into chaos. You decide to make your stay short.")
} else {
String::from("You look around tavern and see only the emaciated corpses of the former townsfolk. Checking one pile of corpses, you notice some covered in several bite marks. Are those the marks of human teeth? You dare not think too much about it.")
}
}
} }
pub struct StarvationMarker; pub struct StarvationMarker;
......
...@@ -9,6 +9,7 @@ mod encounters; ...@@ -9,6 +9,7 @@ mod encounters;
mod generators; mod generators;
mod hunger; mod hunger;
mod spawning; mod spawning;
mod specialism;
mod towns; mod towns;
mod trading; mod trading;
mod travel; mod travel;
...@@ -16,8 +17,9 @@ mod utils; ...@@ -16,8 +17,9 @@ mod utils;
mod world_query; mod world_query;
pub use encounters::{EncounterState, WorldZones}; pub use encounters::{EncounterState, WorldZones};
pub use hunger::HungerState; pub use hunger::{HungerState, StarvationMarker};
pub use spawning::PendingLoadState; pub use spawning::PendingLoadState;
pub use specialism::CraftSpecialism;
pub use towns::{CurrentResidence, PathingResult, TownName, TownPaths, TravelPath, TravelTarget}; pub use towns::{CurrentResidence, PathingResult, TownName, TownPaths, TravelPath, TravelTarget};
pub use trading::{ pub use trading::{
ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState, TradeRoster, ItemName, RosterEntry, TradeGood, TradeManifest, TradeManifestTickState, TradeRoster,
...@@ -72,6 +74,7 @@ impl Plugin for WorldPlugin { ...@@ -72,6 +74,7 @@ impl Plugin for WorldPlugin {
ConditionSet::new() ConditionSet::new()
.run_in_state(AppState::InGame) .run_in_state(AppState::InGame)
.with_system(trading::tick_manifests) .with_system(trading::tick_manifests)
.with_system(specialism::check_specialisms)
.into(), .into(),
) )
.add_system_set_to_stage( .add_system_set_to_stage(
......
...@@ -14,6 +14,7 @@ use num_traits::AsPrimitive; ...@@ -14,6 +14,7 @@ use num_traits::AsPrimitive;
use serde_json::Value; use serde_json::Value;
use crate::assets::{AssetHandles, LdtkProject, LevelIndex, TilesetIndex}; use crate::assets::{AssetHandles, LdtkProject, LevelIndex, TilesetIndex};
use crate::const_data::SPECIALISMS;
use crate::persistance::PersistenceState; use crate::persistance::PersistenceState;
use crate::states::Player; use crate::states::Player;
use crate::system::camera::ChaseCam; use crate::system::camera::ChaseCam;
...@@ -362,6 +363,8 @@ pub fn populate_world( ...@@ -362,6 +363,8 @@ pub fn populate_world(
apply_skull_marker(&mut commands, &assets, ent); apply_skull_marker(&mut commands, &assets, ent);
}); });
log::info!("Spec {:#?}", *SPECIALISMS);
commands.insert_resource(trade_routes); commands.insert_resource(trade_routes);
commands.insert_resource(world_zones); commands.insert_resource(world_zones);
} }
......
use std::fmt::format;
use bevy::prelude::{Component, EventReader, Query};
use bevy::utils::HashMap;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::world::travel::WorldTickEvent;
use crate::world::TradingState;
#[derive(Clone, Debug, Default, Component, Serialize, Deserialize)]
pub struct CraftSpecialism {
pub requirements: HashMap<String, usize>,
pub production: HashMap<String, usize>,
}
lazy_static! {
static ref REQUIREMENT_TEMPLATES: Vec<String> = {
let mut items = Vec::with_capacity(4);
items.push(String::from("I hear that the artisans at [[TOWN]] are pretty good at working with [[ITEMS]]. A wonder to behold!",));
items.push(String::from("I remember the last time I was at [[TOWN]]. I saw a craftsman making something with [[ITEMS]]. I hope I get to see that again one day...",));
items.push(String::from("If you happen to have [[ITEMS]], consider popping over to [[TOWN]]. I hear there's high demand there!",));
items.push(String::from("I've heard that there are a lot of folk able to make use of [[ITEMS]] over in [[TOWN]]."));
items
};
static ref PRODUCTION_TEMPLATES: Vec<String> = {
let mut items = Vec::with_capacity(3);
items.push(String::from("Have you ever seen the craftsfolk over in [[TOWN]] making [[ITEMS]]? It's a wonder to see. I heard they need a few bits and bobs to get going though."));
items.push(String::from("Got a hankering for [[ITEMS]]? If you can get some supplies to [[TOWN]], you might be in luck."));
items.push(String::from("My cousin over in [[TOWN]] is a dab hand at making [[ITEMS]]. Doesn't usually have the supplies for it, though."));
items
};
}
impl CraftSpecialism {
pub fn requirements_satisfied(&self, inventory: &TradingState) -> bool {
self.requirements.iter().all(|(name, amount)| {
inventory
.items
.get(name)
.map(|quantity| quantity >= amount)
.unwrap_or(false)
})
}
pub fn apply_to_inventory(&self, inventory: &mut TradingState) -> bool {
if self.requirements_satisfied(inventory) {
for (name, amount) in self.requirements.iter() {
inventory.remove_items(name, *amount);
}
for (name, amount) in self.production.iter() {
inventory.add_items(name, *amount);
}
true
} else {
false
}
}
fn format_item_map(map: &HashMap<String, usize>) -> String {
match map.len() {
0 => "...oh, I forget".to_string(),
1 => format!("an {}", map.keys().next().unwrap()),
2 => format!(
"{} and {}",
map.keys().nth(0).unwrap(),
map.keys().nth(1).unwrap()
),
_ => {
let first = fastrand::usize(0..map.len());
let mut second = fastrand::usize(0..map.len());
while second == first {
second = fastrand::usize(0..map.len());
}
format!(
"{}, {}, and...some other things, I think",
map.keys().nth(first).unwrap(),
map.keys().nth(second).unwrap()
)
}
}
}
pub fn format_requirements(&self) -> String {
Self::format_item_map(&self.requirements)
}
pub fn format_production(&self) -> String {
Self::format_item_map(&self.production)
}
pub fn format_rumour(&self, town_name: impl ToString) -> String {
let is_production = fastrand::bool();
if is_production {
let stub = &PRODUCTION_TEMPLATES[fastrand::usize(0..PRODUCTION_TEMPLATES.len())];
let output = stub
.replace("[[TOWN]]", town_name.to_string().as_str())
.replace("[[ITEMS]]", self.format_production().as_str());
output
} else {
let stub = &REQUIREMENT_TEMPLATES[fastrand::usize(0..REQUIREMENT_TEMPLATES.len())];
let output = stub
.replace("[[TOWN]]", town_name.to_string().as_str())
.replace("[[ITEMS]]", self.format_requirements().as_str());
output
}
}
}
pub fn check_specialisms(
mut query: Query<(&mut TradingState, &CraftSpecialism)>,
events: EventReader<WorldTickEvent>,
) {
if !events.is_empty() {
for (mut trading_state, specialism) in &mut query {
specialism.apply_to_inventory(&mut trading_state);
}
}
}
[[specialism]]
[specialism.requirements]
Tomato = 3
Cheese = 2
Dough = 2
[specialism.production]
Pizza = 2
[[specialism]]
[specialism.requirements]
Coal = 4
"Iron Ore" = 3
[specialism.production]
"Iron Ingot" = 3
[[specialism]]
[specialism.requirements]
Lumber = 10
[specialism.production]
"Spare Wheel" = 3
[[specialism]]
[specialism.requirements]
Milk = 4
[specialism.production]
Cheese = 3
[[specialism]]
[specialism.requirements]
Wheat = 4
[specialism.production]
Dough = 3
[[specialism]]
[specialism.requirements]
Dough = 4
[specialism.production]
Bread = 3
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment