From 75a10045532351130916de1106870f1da2f848c5 Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Fri, 9 Dec 2022 14:57:01 +0000 Subject: [PATCH] Actual trading with actual inventories --- game_core/src/ui/sync/mod.rs | 4 + game_core/src/ui/sync/sync_trade.rs | 125 ++++++++++++++++++++ game_core/src/ui/utilities.rs | 4 +- game_core/src/ui/widgets/shop_panel.rs | 152 +++++++++++++------------ game_core/src/ui/widgets/town_menu.rs | 1 - game_core/src/world/trading.rs | 3 +- 6 files changed, 210 insertions(+), 79 deletions(-) create mode 100644 game_core/src/ui/sync/sync_trade.rs diff --git a/game_core/src/ui/sync/mod.rs b/game_core/src/ui/sync/mod.rs index 0dc64da..27dd7b0 100644 --- a/game_core/src/ui/sync/mod.rs +++ b/game_core/src/ui/sync/mod.rs @@ -4,6 +4,7 @@ use iyes_loopless::prelude::ConditionSet; use crate::system::flow::AppState; mod sync_stats; +mod sync_trade; mod sync_travel; pub struct UISyncPlugin; @@ -11,15 +12,18 @@ impl Plugin for UISyncPlugin { fn build(&self, app: &mut App) { app.init_resource::<UITravelInfo>() .init_resource::<UIStatsData>() + .init_resource::<UITradeData>() .add_system_set( ConditionSet::new() .run_in_state(AppState::InGame) .with_system(sync_travel::sync_player_travel_info_to_ui) .with_system(sync_stats::sync_player_stats_info_to_ui) + .with_system(sync_trade::sync_ui_trade_data) .into(), ); } } pub use sync_stats::UIStatsData; +pub use sync_trade::{ItemTradeData, UITradeData}; pub use sync_travel::UITravelInfo; diff --git a/game_core/src/ui/sync/sync_trade.rs b/game_core/src/ui/sync/sync_trade.rs new file mode 100644 index 0000000..660dc62 --- /dev/null +++ b/game_core/src/ui/sync/sync_trade.rs @@ -0,0 +1,125 @@ +use std::collections::HashMap; + +use bevy::asset::{Assets, Handle}; +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::states::Player; +use crate::world::{ + CurrentResidence, TownName, TradeGood, TradeManifest, TradingState, TravelTarget, +}; + +#[derive(Clone)] +pub struct ItemTradeData { + pub definition: TradeGood, + pub current_cost: usize, + pub town_amount: usize, + pub player_amount: usize, +} + +#[derive(Resource, Default)] +pub struct UITradeData { + pub town_gold: usize, + pub town_entity: Option<Entity>, + pub player_gold: usize, + pub player_entity: Option<Entity>, + pub shop_items: Vec<ItemTradeData>, +} + +pub fn sync_ui_trade_data( + player_query: Query< + ( + Entity, + &TradingState, + Option<&TravelTarget>, + &CurrentResidence, + ), + With<Player>, + >, + town_query: Query<(Entity, &TownName, &TradingState, &Handle<TradeManifest>)>, + manifests: Res<Assets<TradeManifest>>, + mut trade_data: ResMut<UITradeData>, +) { + let mut trade_entries = HashMap::with_capacity(15); + + let (entity, player_trading_state, target, residence) = match player_query.get_single() { + Ok(p) => p, + Err(_) => return, + }; + + trade_data.player_entity = Some(entity); + trade_data.player_gold = player_trading_state.gold.max(0) as usize; + + let town_name = target + .map(|target| target.0.clone()) + .unwrap_or_else(|| match residence { + CurrentResidence::RestingAt(place) => place.clone(), + CurrentResidence::TravellingFrom(..) => String::new(), + }); + + let (town_entity, _, town_trading_state, manifest_handle) = match town_query + .into_iter() + .find(|(_, name, _, _)| town_name == *name.0) + { + Some(v) => v, + None => { + trade_data.shop_items = vec![]; + trade_data.town_entity = None; + return; + } + }; + + trade_data.town_entity = Some(town_entity); + trade_data.town_gold = town_trading_state.gold.max(0) as usize; + + for (item_name, trade_good) in TRADE_GOODS.iter() { + let manifest = match manifests.get(manifest_handle) { + Some(m) => m, + None => continue, + }; + + let roster_entry = match manifest.get(&trade_good.name) { + Some(e) => e, + None => continue, + }; + + let town_amount = match town_trading_state.items.get(&trade_good.name) { + Some(q) => *q, + None => 0, + }; + + let player_amount = match player_trading_state.items.get(&trade_good.name) { + Some(q) => *q, + None => 0, + }; + + let mut cost_multiplier = -1.0; + + for (limit, multiplier) in &roster_entry.cost_multipliers { + cost_multiplier = *multiplier; + if town_amount < *limit { + break; + } + } + + let current_fractional_cost = cost_multiplier * trade_good.gold_value as f32; + let current_cost = current_fractional_cost.ceil() as usize; + + trade_entries.insert( + item_name.clone(), + ItemTradeData { + definition: trade_good.clone(), + current_cost, + town_amount, + player_amount, + }, + ); + } + + let mut entries = trade_entries + .into_values() + .filter(|listing| listing.town_amount > 0 || listing.player_amount > 0) + .collect::<Vec<ItemTradeData>>(); + entries.sort_by(|a, b| a.definition.name.cmp(&b.definition.name)); + trade_data.shop_items = entries; +} diff --git a/game_core/src/ui/utilities.rs b/game_core/src/ui/utilities.rs index bf32736..f48ce1c 100644 --- a/game_core/src/ui/utilities.rs +++ b/game_core/src/ui/utilities.rs @@ -164,7 +164,7 @@ pub mod context { use crate::register_widget; use crate::ui::components::*; - use crate::ui::sync::{UIStatsData, UITravelInfo}; + use crate::ui::sync::{UIStatsData, UITradeData, UITravelInfo}; use crate::ui::widgets::*; use crate::world::EncounterState; @@ -246,7 +246,7 @@ pub mod context { widget_context, ShopPanelProps, EmptyState, - UITravelInfo, + UITradeData, render_shop_panel ); register_widget_with_resource!( diff --git a/game_core/src/ui/widgets/shop_panel.rs b/game_core/src/ui/widgets/shop_panel.rs index 93ed7cc..bca69ba 100644 --- a/game_core/src/ui/widgets/shop_panel.rs +++ b/game_core/src/ui/widgets/shop_panel.rs @@ -8,78 +8,74 @@ use kayak_ui::widgets::{ use crate::states::Player; use crate::ui::components::*; use crate::ui::prelude::*; -use crate::ui::sync::UIStatsData; +use crate::ui::sync::{ItemTradeData, UIStatsData, UITradeData}; use crate::ui::widgets::*; -use crate::world::{CurrentResidence, MapQuery, TownPaths}; +use crate::world::{CurrentResidence, MapQuery, TownName, TownPaths, TradeGood, TradingState}; use crate::{basic_widget, empty_props, on_button_click}; empty_props!(ShopPanelProps); basic_widget!(ShopPanelProps => ShopPanel); -pub fn buysell_button_factory(target: String) -> OnEvent { +pub fn buysell_button_factory(target: String, is_buy: bool) -> OnEvent { let target = target.clone(); on_button_click!( - ParamSet<( - Commands, - Res<TownPaths>, - Query<(Entity, &CurrentResidence), With<Player>>, - MapQuery, - )>, - |mut params: ParamSet<( - Commands, - Res<TownPaths>, - Query<(Entity, &CurrentResidence), With<Player>>, - MapQuery, - )>| { - let target = target.clone(); - let (entity, current) = { - match params.p2().get_single() { - Ok((entity, current)) => (entity.clone(), (current.get_location()).clone()), - _ => return, - } - }; - - let places = match params.p1().routes.get(¤t) { - Some(places) => places.clone(), - None => return, + ParamSet<(Query<&mut TradingState>, Res<UITradeData>,)>, + |mut params: ParamSet<(Query<&mut TradingState>, Res<UITradeData>,)>| { + let player_entity = params.p1().player_entity.clone(); + let town_entity = params.p1().town_entity.clone(); + let entry = { + let shop_items = ¶ms.p1().shop_items; + shop_items + .iter() + .find(|en| en.definition.name == target) + .cloned() }; - let bundle = match params.p3().get_active_level() { - Some(level) => places.create_route_bundle_for(target, level).unwrap(), - None => return, - }; + if let (Some(player), Some(town), Some(entry)) = (player_entity, town_entity, entry) { + let mut query = params.p0(); - params.p0().entity(entity).insert(bundle); + if query.contains(player) && query.contains(town) { + let mut has_purchased = false; + if is_buy { + if let Ok(mut player_inv) = query.get_mut(player) { + has_purchased = + player_inv.try_buy_items(entry.current_cost, target.clone(), 1); + } + if has_purchased { + if let Ok(mut town_inv) = query.get_mut(town) { + town_inv.try_sell_items(entry.current_cost, target.clone(), 1); + } + } + } else { + let adjusted_sell_price = adjust_player_sell_price(entry.current_cost); + if let Ok(mut town_inv) = query.get_mut(town) { + has_purchased = + town_inv.try_buy_items(adjusted_sell_price, target.clone(), 1); + } + if has_purchased { + if let Ok(mut player_inv) = query.get_mut(player) { + player_inv.try_sell_items(adjusted_sell_price, target.clone(), 1); + } + } + } + } + } } ) } +fn adjust_player_sell_price(base: usize) -> usize { + (base as f32 * 0.8).ceil() as usize +} + pub fn render_shop_panel( In((widget_context, entity)): In<(KayakWidgetContext, Entity)>, mut commands: Commands, query: Query<&ShopPanelProps>, - ui_data: Res<UIStatsData>, + ui_data: Res<UITradeData>, ) -> bool { let parent_id = Some(entity); - let items = vec![ - ("Armour", 12usize, 12, 14, 2), - ("Weapons", 1, 13, 14, 2), - ("Luxury Goods", 13, 27, 14, 2), - ("Ale", 11, 6, 14, 2), - ("Milk", 2, 5, 14, 2), - ("Cheese", 3, 6, 14, 2), - ("Spare Wheel", 28, 125, 14, 2), - ("Berries", 27, 4, 14, 2), - ("Corn", 26, 5, 14, 2), - ("Tomato", 25, 6, 14, 2), - ("Chili", 24, 8, 14, 2), - ("Grapes", 23, 9, 14, 2), - ("Wheat", 22, 3, 14, 2), - ]; - - log::info!("{:?}", items); - if let Ok(props) = query.get(entity) { rsx! { <ElementBundle> @@ -106,31 +102,37 @@ pub fn render_shop_panel( ..Default::default() } } styles={KStyle { pointer_events: value(PointerEvents::None), padding: edge_px(5.0), ..Default::default() }}> { - for (name, idx, cost, shop_amount, player_amount) in items.iter() { - constructor! { - <BackgroundBundle - styles={KStyle { - pointer_events: value(PointerEvents::ChildrenOnly), - layout_type: value(LayoutType::Row), - col_between: stretch(0.5), - height: px(40.0), - bottom: px(15.0), - padding_top: px(4.0), - padding_bottom: px(4.0), - color: value(Color::BLACK), - ..Default::default() - }} - > - <InsetIconWidget props={InsetIconProps { image: IconContent::Atlas(String::from("icons"), *idx), size: 32.0}} /> - <TextWidgetBundle styles={KStyle { width: stretch(1.5), ..Default::default() }} text={TextProps { content: String::from(*name), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> - <TextWidgetBundle styles={KStyle { width: stretch(1.0), ..Default::default() }} text={TextProps { content: format!("{}g", cost), size: 30.0, line_height: Some(32.0), ..Default::default() } }/> - <TextWidgetBundle styles={KStyle { width: stretch(0.5), ..Default::default() }} text={TextProps { content: format!("{}", shop_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 props={ButtonWidgetProps { font_size: 20.0, text: String::from("Buy"), ..Default::default() }} /> - <ButtonWidget props={ButtonWidgetProps { font_size: 20.0, text: String::from("Sell"), ..Default::default() }} /> - </BackgroundBundle> - }; - } + for ItemTradeData { + town_amount, + player_amount, + current_cost, + definition: TradeGood { name, icon, .. }, + } in &ui_data.shop_items + { + constructor! { + <BackgroundBundle + styles={KStyle { + pointer_events: value(PointerEvents::ChildrenOnly), + layout_type: value(LayoutType::Row), + col_between: stretch(0.5), + height: px(40.0), + bottom: px(15.0), + padding_top: px(4.0), + padding_bottom: px(4.0), + color: value(Color::BLACK), + ..Default::default() + }} + > + <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(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() }} /> + <ButtonWidget on_event={buysell_button_factory(name.clone(), false)} props={ButtonWidgetProps { font_size: 20.0, is_disabled: ui_data.town_gold < adjust_player_sell_price(*current_cost) || player_amount == &0, text: String::from("Sell"), ..Default::default() }} /> + </BackgroundBundle> + } + } } </ScrollBoxBundle> </ScrollContextProviderBundle> diff --git a/game_core/src/ui/widgets/town_menu.rs b/game_core/src/ui/widgets/town_menu.rs index b4240a7..c3460e4 100644 --- a/game_core/src/ui/widgets/town_menu.rs +++ b/game_core/src/ui/widgets/town_menu.rs @@ -158,7 +158,6 @@ pub fn render_town_menu_panel( { - log::info!("Town menu is showing: {:?}", &state.tab); match state.tab { TownMenuTab::Travel => { constructor! { diff --git a/game_core/src/world/trading.rs b/game_core/src/world/trading.rs index cc43cc5..5f8f9b4 100644 --- a/game_core/src/world/trading.rs +++ b/game_core/src/world/trading.rs @@ -30,6 +30,7 @@ pub struct TradeGood { pub name: String, pub icon: IconContent, pub food_value: usize, + pub gold_value: usize, } impl PartialEq for TradeGood { @@ -124,7 +125,7 @@ impl TradingState { if self.gold < amount.as_() { false } else { - self.adjust_gold(amount); + self.adjust_gold(-amount.as_()); true } } -- GitLab