1use std::cmp::Ordering;
2
3use crate::{
4 comp::inventory::{
5 Inventory,
6 item::{AbilityMap, Item, ItemDefinitionIdOwned, MaterialStatManifest, Quality},
7 slot::InvSlotId,
8 trade_pricing::TradePricing,
9 },
10 terrain::BiomeKind,
11 uid::Uid,
12};
13use hashbrown::HashMap;
14use serde::{Deserialize, Serialize};
15use strum::EnumIter;
16use tracing::{trace, warn};
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub enum TradePhase {
20 Mutate,
21 Review,
22 Complete,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub enum TradeAction {
30 AddItem {
31 item: InvSlotId,
32 quantity: u32,
33 ours: bool,
34 },
35 RemoveItem {
36 item: InvSlotId,
37 quantity: u32,
38 ours: bool,
39 },
40 Accept(TradePhase),
44 Decline,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub enum TradeResult {
49 Completed,
50 Declined,
51 NotEnoughSpace,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
80pub struct PendingTrade {
81 pub parties: [Uid; 2],
84 pub offers: [HashMap<InvSlotId, u32>; 2],
87 pub phase: TradePhase,
89 pub accept_flags: [bool; 2],
92}
93
94impl TradePhase {
95 fn next(self) -> TradePhase {
96 match self {
97 TradePhase::Mutate => TradePhase::Review,
98 TradePhase::Review => TradePhase::Complete,
99 TradePhase::Complete => TradePhase::Complete,
100 }
101 }
102}
103
104impl TradeAction {
105 pub fn item(item: InvSlotId, delta: i32, ours: bool) -> Option<Self> {
106 match delta.cmp(&0) {
107 Ordering::Equal => None,
108 Ordering::Less => Some(TradeAction::RemoveItem {
109 item,
110 ours,
111 quantity: -delta as u32,
112 }),
113 Ordering::Greater => Some(TradeAction::AddItem {
114 item,
115 ours,
116 quantity: delta as u32,
117 }),
118 }
119 }
120}
121
122impl PendingTrade {
123 pub fn new(party: Uid, counterparty: Uid) -> PendingTrade {
124 PendingTrade {
125 parties: [party, counterparty],
126 offers: [HashMap::new(), HashMap::new()],
127 phase: TradePhase::Mutate,
128 accept_flags: [false, false],
129 }
130 }
131
132 pub fn phase(&self) -> TradePhase { self.phase }
133
134 pub fn should_commit(&self) -> bool { matches!(self.phase, TradePhase::Complete) }
135
136 pub fn which_party(&self, party: Uid) -> Option<usize> {
137 self.parties
138 .iter()
139 .enumerate()
140 .find(|(_, x)| **x == party)
141 .map(|(i, _)| i)
142 }
143
144 pub fn is_empty_trade(&self) -> bool { self.offers[0].is_empty() && self.offers[1].is_empty() }
145
146 pub fn process_trade_action(
153 &mut self,
154 mut who: usize,
155 action: TradeAction,
156 inventories: &[&Inventory],
157 ) {
158 use TradeAction::*;
159 match action {
160 AddItem {
161 item,
162 quantity: delta,
163 ours,
164 } => {
165 if self.phase() == TradePhase::Mutate && delta > 0 {
166 if !ours {
167 who = 1 - who;
168 }
169 let total = self.offers[who].entry(item).or_insert(0);
170 let owned_quantity =
171 inventories[who].get(item).map(|i| i.amount()).unwrap_or(0);
172 *total = total.saturating_add(delta).min(owned_quantity);
173 self.accept_flags = [false, false];
174 }
175 },
176 RemoveItem {
177 item,
178 quantity: delta,
179 ours,
180 } => {
181 if self.phase() == TradePhase::Mutate {
182 if !ours {
183 who = 1 - who;
184 }
185 self.offers[who]
186 .entry(item)
187 .and_replace_entry_with(|_, mut total| {
188 total = total.saturating_sub(delta);
189 if total > 0 { Some(total) } else { None }
190 });
191 self.accept_flags = [false, false];
192 }
193 },
194 Accept(phase) => {
195 if self.phase == phase && !self.is_empty_trade() {
196 self.accept_flags[who] = true;
197 }
198 if self.accept_flags[0] && self.accept_flags[1] {
199 self.phase = self.phase.next();
200 self.accept_flags = [false, false];
201 }
202 },
203 Decline => {},
204 }
205 }
206}
207
208#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
209pub struct TradeId(usize);
210
211pub struct Trades {
212 pub next_id: TradeId,
213 pub trades: HashMap<TradeId, PendingTrade>,
214 pub entity_trades: HashMap<Uid, TradeId>,
215}
216
217impl Trades {
218 pub fn begin_trade(&mut self, party: Uid, counterparty: Uid) -> TradeId {
219 let id = self.next_id;
220 self.next_id = TradeId(id.0.wrapping_add(1));
221 self.trades
222 .insert(id, PendingTrade::new(party, counterparty));
223 self.entity_trades.insert(party, id);
224 self.entity_trades.insert(counterparty, id);
225 id
226 }
227
228 pub fn process_trade_action<'a, F: Fn(Uid) -> Option<&'a Inventory>>(
229 &mut self,
230 id: TradeId,
231 who: Uid,
232 action: TradeAction,
233 get_inventory: F,
234 ) {
235 trace!("for trade id {:?}, message {:?}", id, action);
236 if let Some(trade) = self.trades.get_mut(&id) {
237 if let Some(party) = trade.which_party(who) {
238 let mut inventories = Vec::new();
239 for party in trade.parties.iter() {
240 match get_inventory(*party) {
241 Some(inventory) => inventories.push(inventory),
242 None => return,
243 }
244 }
245 trade.process_trade_action(party, action, &inventories);
246 } else {
247 warn!(
248 "An entity who is not a party to trade {:?} tried to modify it",
249 id
250 );
251 }
252 } else {
253 warn!("Attempt to modify nonexistent trade id {:?}", id);
254 }
255 }
256
257 pub fn decline_trade(&mut self, id: TradeId, who: Uid) -> Option<Uid> {
258 let mut to_notify = None;
259 if let Some(trade) = self.trades.remove(&id) {
260 match trade.which_party(who) {
261 Some(i) => {
262 self.entity_trades.remove(&trade.parties[0]);
263 self.entity_trades.remove(&trade.parties[1]);
264 to_notify = Some(trade.parties[1 - i])
266 },
267 None => {
268 warn!(
269 "An entity who is not a party to trade {:?} tried to decline it",
270 id
271 );
272 self.trades.insert(id, trade);
274 },
275 }
276 } else {
277 warn!("Attempt to decline nonexistent trade id {:?}", id);
278 }
279 to_notify
280 }
281
282 pub fn in_trade_with_property<F: FnOnce(&PendingTrade) -> bool>(
285 &self,
286 uid: &Uid,
287 f: F,
288 ) -> bool {
289 self.entity_trades
290 .get(uid)
291 .and_then(|trade_id| self.trades.get(trade_id))
292 .map(f)
293 .unwrap_or(false)
295 }
296
297 pub fn in_immutable_trade(&self, uid: &Uid) -> bool {
298 self.in_trade_with_property(uid, |trade| trade.phase() != TradePhase::Mutate)
299 }
300
301 pub fn in_mutable_trade(&self, uid: &Uid) -> bool {
302 self.in_trade_with_property(uid, |trade| trade.phase() == TradePhase::Mutate)
303 }
304
305 pub fn implicit_mutation_occurred(&mut self, uid: &Uid) {
306 if let Some(trade_id) = self.entity_trades.get(uid) {
307 self.trades
308 .get_mut(trade_id)
309 .map(|trade| trade.accept_flags = [false, false]);
310 }
311 }
312}
313
314impl Default for Trades {
315 fn default() -> Trades {
316 Trades {
317 next_id: TradeId(0),
318 trades: HashMap::new(),
319 entity_trades: HashMap::new(),
320 }
321 }
322}
323
324#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, EnumIter)]
328pub enum Good {
329 Territory(BiomeKind),
330 Flour,
331 Meat,
332 Terrain(BiomeKind),
333 Transportation,
334 Food,
335 Wood,
336 Stone,
337 Tools, Armor,
339 Ingredients, Potions,
341 Coin, RoadSecurity,
343 Recipe,
344}
345
346impl Default for Good {
347 fn default() -> Self {
348 Good::Terrain(BiomeKind::Void) }
350}
351
352impl Good {
353 pub fn sell_discount(&self, quality: Quality) -> f32 {
355 let res = match self {
356 Good::Tools | Good::Armor => 0.9,
358 Good::Ingredients | Good::Wood | Good::Stone => 0.9,
360 Good::Food | Good::Potions => 1.0,
362 Good::Coin | Good::Recipe => return 1.0,
364 _ => return 0.0,
370 };
371
372 res * Good::quality_sell_discount(quality)
373 }
374
375 fn quality_sell_discount(quality: Quality) -> f32 {
380 match quality {
381 Quality::Low => 1.0,
382 Quality::Common => 0.9,
383 Quality::Moderate => 0.8,
384 Quality::High => 0.6,
385 Quality::Epic => 0.5,
386 Quality::Legendary => 0.5,
387 Quality::Artifact | Quality::Debug => 0.0,
388 }
389 }
390}
391
392pub type SiteId = u64;
394
395#[derive(Clone, Debug)]
396pub struct SiteInformation {
397 pub id: SiteId,
398 pub unconsumed_stock: HashMap<Good, f32>,
399}
400
401#[derive(Clone, Debug, Default, Serialize, Deserialize)]
402pub struct SitePrices {
403 pub values: HashMap<Good, f32>,
404}
405
406impl SitePrices {
407 pub fn balance(
408 &self,
409 offers: &[HashMap<InvSlotId, u32>; 2],
410 inventories: &[Option<ReducedInventory>; 2],
411 who: usize,
412 reduce: bool,
413 ) -> Option<f32> {
414 offers[who]
415 .iter()
416 .map(|(slot, amount)| {
417 inventories[who]
418 .as_ref()
419 .map(|ri| {
420 let item = ri.inventory.get(slot)?;
421 let vec = TradePricing::get_materials(&item.name.as_ref())?;
422 let good = TradePricing::good_from_item(&item.name);
423 Some(
424 vec.iter()
425 .map(|(amount2, material)| {
426 self.values.get(material).copied().unwrap_or_default()
427 * *amount2
428 })
429 .sum::<f32>()
430 * (*amount as f32)
431 * (if reduce {
432 good.sell_discount(
433 Item::new_from_item_definition_id(
436 item.name.as_ref(),
437 &AbilityMap::load().read(),
438 &MaterialStatManifest::load().read(),
439 )
440 .map_or(Quality::Low, |i| i.quality()),
441 )
442 } else {
443 1.0
444 }),
445 )
446 })
447 .unwrap_or(Some(0.0))
448 })
449 .try_fold(0.0, |a, p| Some(a + p?))
450 }
451}
452
453#[derive(Clone, Debug)]
454pub struct ReducedInventoryItem {
455 pub name: ItemDefinitionIdOwned,
456 pub amount: u32,
457}
458
459#[derive(Clone, Debug, Default)]
460pub struct ReducedInventory {
461 pub inventory: HashMap<InvSlotId, ReducedInventoryItem>,
462}
463
464impl ReducedInventory {
465 pub fn from(inventory: &Inventory) -> Self {
466 let items = inventory
467 .slots_with_id()
468 .filter(|(_, it)| it.is_some())
469 .map(|(sl, it)| {
470 (sl, ReducedInventoryItem {
471 name: it.as_ref().unwrap().item_definition_id().to_owned(),
472 amount: it.as_ref().unwrap().amount(),
473 })
474 })
475 .collect();
476 Self { inventory: items }
477 }
478}