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(&current) {
-				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 = &params.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