1use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Currency {
13 pub gold: u64,
14 pub silver: u64,
15 pub copper: u64,
16}
17
18impl Currency {
19 pub fn new(gold: u64, silver: u64, copper: u64) -> Self {
20 let mut c = Self { gold, silver, copper };
21 c.normalize();
22 c
23 }
24
25 pub fn from_copper(total: u64) -> Self {
27 let gold = total / 10_000;
28 let rem = total % 10_000;
29 let silver = rem / 100;
30 let copper = rem % 100;
31 Self { gold, silver, copper }
32 }
33
34 pub fn gold(amount: u64) -> Self {
36 Self { gold: amount, silver: 0, copper: 0 }
37 }
38
39 pub fn silver(amount: u64) -> Self {
41 Self::from_copper(amount * 100)
42 }
43
44 pub fn copper(amount: u64) -> Self {
46 Self::from_copper(amount)
47 }
48
49 pub fn zero() -> Self {
50 Self { gold: 0, silver: 0, copper: 0 }
51 }
52
53 pub fn normalize(&mut self) {
55 let carry_silver = self.copper / 100;
56 self.copper %= 100;
57 self.silver += carry_silver;
58
59 let carry_gold = self.silver / 100;
60 self.silver %= 100;
61 self.gold += carry_gold;
62 }
63
64 pub fn to_copper_total(&self) -> u64 {
66 self.gold * 10_000 + self.silver * 100 + self.copper
67 }
68
69 pub fn has_at_least(&self, amount: &Currency) -> bool {
71 self.to_copper_total() >= amount.to_copper_total()
72 }
73
74 pub fn try_subtract(&mut self, amount: &Currency) -> bool {
77 let total = self.to_copper_total();
78 let cost = amount.to_copper_total();
79 if total < cost {
80 return false;
81 }
82 *self = Self::from_copper(total - cost);
83 true
84 }
85
86 pub fn add(&mut self, amount: &Currency) {
88 let total = self.to_copper_total() + amount.to_copper_total();
89 *self = Self::from_copper(total);
90 }
91
92 pub fn multiply(&self, factor: u32) -> Self {
94 Self::from_copper(self.to_copper_total() * factor as u64)
95 }
96
97 pub fn scale(&self, factor: f32) -> Self {
99 let scaled = (self.to_copper_total() as f32 * factor).round() as u64;
100 Self::from_copper(scaled)
101 }
102
103 pub fn is_zero(&self) -> bool {
104 self.to_copper_total() == 0
105 }
106}
107
108impl Default for Currency {
109 fn default() -> Self {
110 Self::zero()
111 }
112}
113
114impl std::fmt::Display for Currency {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}g {}s {}c", self.gold, self.silver, self.copper)
117 }
118}
119
120impl PartialOrd for Currency {
121 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
122 self.to_copper_total().partial_cmp(&other.to_copper_total())
123 }
124}
125
126impl Ord for Currency {
127 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
128 self.to_copper_total().cmp(&other.to_copper_total())
129 }
130}
131
132#[derive(Debug, Clone)]
138pub struct PriceModifier {
139 pub base_price: u64,
140 pub demand_factor: f32,
142 pub supply_factor: f32,
144 pub player_rep_factor: f32,
146}
147
148impl PriceModifier {
149 pub fn new(base_price: u64) -> Self {
150 Self {
151 base_price,
152 demand_factor: 1.0,
153 supply_factor: 1.0,
154 player_rep_factor: 1.0,
155 }
156 }
157
158 pub fn final_price(&self) -> u64 {
160 let base = self.base_price as f32;
161 let rep_discount = 1.0 + (1.0 - self.player_rep_factor.clamp(0.5, 1.5)) * 0.3;
163 let modified = base
164 * self.demand_factor.clamp(0.5, 3.0)
165 * self.supply_factor.clamp(0.25, 2.0)
166 * rep_discount;
167 modified.round().max(1.0) as u64
168 }
169
170 pub fn final_currency(&self) -> Currency {
172 Currency::from_copper(self.final_price())
173 }
174}
175
176#[derive(Debug, Clone)]
182pub struct PlayerReputation {
183 pub faction_id: String,
184 pub value: f32,
186}
187
188impl PlayerReputation {
189 pub fn new(faction_id: impl Into<String>) -> Self {
190 Self { faction_id: faction_id.into(), value: 0.0 }
191 }
192
193 pub fn adjust(&mut self, delta: f32) {
195 self.value = (self.value + delta).clamp(-1.0, 1.0);
196 }
197
198 pub fn price_factor(&self) -> f32 {
201 1.0 - self.value * 0.3
203 }
204
205 pub fn label(&self) -> &'static str {
206 match self.value {
207 v if v >= 0.8 => "Exalted",
208 v if v >= 0.5 => "Revered",
209 v if v >= 0.2 => "Honored",
210 v if v >= -0.2 => "Neutral",
211 v if v >= -0.5 => "Unfriendly",
212 v if v >= -0.8 => "Hostile",
213 _ => "Hated",
214 }
215 }
216}
217
218#[derive(Debug, Clone)]
224pub struct TaxSystem {
225 region_taxes: HashMap<String, f32>,
227 pub black_market_premium: f32,
229}
230
231impl TaxSystem {
232 pub fn new() -> Self {
233 let mut t = Self {
234 region_taxes: HashMap::new(),
235 black_market_premium: 1.35,
236 };
237 t.region_taxes.insert("capital".into(), 0.08);
239 t.region_taxes.insert("frontier".into(), 0.03);
240 t.region_taxes.insert("merchant_district".into(), 0.12);
241 t.region_taxes.insert("black_market".into(), 0.00);
242 t
243 }
244
245 pub fn set_region_tax(&mut self, region_id: impl Into<String>, rate: f32) {
247 self.region_taxes.insert(region_id.into(), rate.clamp(0.0, 0.5));
248 }
249
250 pub fn apply_tax(&self, base_price: u64, region_id: &str) -> u64 {
252 let tax_rate = self.region_taxes.get(region_id).copied().unwrap_or(0.05);
253 let taxed = base_price as f32 * (1.0 + tax_rate);
254 let black_market = if region_id == "black_market" {
255 taxed * self.black_market_premium
256 } else {
257 taxed
258 };
259 black_market.round() as u64
260 }
261
262 pub fn tax_amount(&self, base_price: u64, region_id: &str) -> u64 {
264 let final_price = self.apply_tax(base_price, region_id);
265 final_price.saturating_sub(base_price)
266 }
267}
268
269impl Default for TaxSystem {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[derive(Debug, Clone)]
281pub struct ShopItem {
282 pub item_id: String,
283 pub stock: i32,
285 pub max_stock: i32,
287 pub price: Currency,
289 pub restock_rate: f32,
291 pub last_restock: f32,
293}
294
295impl ShopItem {
296 pub fn new(
297 item_id: impl Into<String>,
298 stock: i32,
299 max_stock: i32,
300 price: Currency,
301 restock_rate: f32,
302 ) -> Self {
303 Self {
304 item_id: item_id.into(),
305 stock,
306 max_stock,
307 price,
308 restock_rate,
309 last_restock: 0.0,
310 }
311 }
312
313 pub fn buy_back_price(&self) -> Currency {
315 self.price.scale(0.40)
316 }
317
318 pub fn is_in_stock(&self) -> bool {
319 self.stock == -1 || self.stock > 0
320 }
321
322 pub fn is_unlimited(&self) -> bool {
323 self.stock == -1
324 }
325}
326
327#[derive(Debug, Clone)]
333pub struct ShopInventory {
334 pub shop_id: String,
335 pub shop_name: String,
336 pub faction_id: String,
337 items: HashMap<String, ShopItem>,
338 pub region_id: String,
340 pub tax_system: TaxSystem,
341}
342
343impl ShopInventory {
344 pub fn new(
345 shop_id: impl Into<String>,
346 shop_name: impl Into<String>,
347 faction_id: impl Into<String>,
348 region_id: impl Into<String>,
349 ) -> Self {
350 Self {
351 shop_id: shop_id.into(),
352 shop_name: shop_name.into(),
353 faction_id: faction_id.into(),
354 items: HashMap::new(),
355 region_id: region_id.into(),
356 tax_system: TaxSystem::new(),
357 }
358 }
359
360 pub fn add_item(&mut self, item: ShopItem) {
362 self.items.insert(item.item_id.clone(), item);
363 }
364
365 pub fn get_item(&self, item_id: &str) -> Option<&ShopItem> {
367 self.items.get(item_id)
368 }
369
370 pub fn all_items(&self) -> Vec<&ShopItem> {
372 self.items.values().collect()
373 }
374
375 pub fn buy(
380 &mut self,
381 item_id: &str,
382 qty: u32,
383 player_currency: &mut Currency,
384 player_rep: f32,
385 ) -> Result<Currency, String> {
386 let item = self.items.get_mut(item_id)
387 .ok_or_else(|| format!("Item '{}' not found in shop", item_id))?;
388
389 if !item.is_unlimited() && (item.stock as u32) < qty {
390 return Err(format!("Not enough stock: have {}, need {}", item.stock, qty));
391 }
392
393 let rep_scale = 1.0 - player_rep.clamp(-1.0, 1.0) * 0.3;
395 let unit_price = item.price.scale(rep_scale);
396 let total_cost = unit_price.multiply(qty);
397
398 let taxed_total_copper = self.tax_system.apply_tax(total_cost.to_copper_total(), &self.region_id);
400 let taxed_total = Currency::from_copper(taxed_total_copper);
401
402 if !player_currency.has_at_least(&taxed_total) {
403 return Err(format!("Insufficient funds: need {}, have {}", taxed_total, player_currency));
404 }
405
406 player_currency.try_subtract(&taxed_total);
407 if !item.is_unlimited() {
408 item.stock -= qty as i32;
409 }
410
411 Ok(taxed_total)
412 }
413
414 pub fn sell(
419 &mut self,
420 item_id: &str,
421 qty: u32,
422 player_currency: &mut Currency,
423 ) -> Result<Currency, String> {
424 let item = self.items.get_mut(item_id)
425 .ok_or_else(|| format!("Shop does not buy '{}'", item_id))?;
426
427 if item.max_stock > 0 && item.stock >= item.max_stock {
429 return Err(format!("Shop is full for item '{}'", item_id));
430 }
431
432 let payout = item.buy_back_price().multiply(qty);
433 player_currency.add(&payout);
434
435 if item.max_stock > 0 {
437 item.stock = (item.stock + qty as i32).min(item.max_stock);
438 }
439
440 Ok(payout)
441 }
442
443 pub fn restock(&mut self, dt: f32, current_time: f32) {
445 for item in self.items.values_mut() {
446 if item.restock_rate <= 0.0 || item.max_stock <= 0 {
447 continue;
448 }
449 if item.stock >= item.max_stock {
450 continue;
451 }
452 let time_since = current_time - item.last_restock;
453 let units_to_add = (time_since * item.restock_rate).floor() as i32;
454 if units_to_add >= 1 {
455 item.stock = (item.stock + units_to_add).min(item.max_stock);
456 item.last_restock = current_time;
457 }
458 }
459 }
460}
461
462#[derive(Debug, Clone)]
468pub struct TradeOffer {
469 pub id: u64,
470 pub from_items: Vec<(String, u32)>,
472 pub to_items: Vec<(String, u32)>,
474 pub gold_delta: i64,
477 pub expires_at: f32,
479 pub proposer_id: String,
480 pub receiver_id: String,
481 pub accepted: bool,
482 pub rejected: bool,
483}
484
485impl TradeOffer {
486 pub fn new(
487 id: u64,
488 proposer_id: impl Into<String>,
489 receiver_id: impl Into<String>,
490 from_items: Vec<(String, u32)>,
491 to_items: Vec<(String, u32)>,
492 gold_delta: i64,
493 expires_at: f32,
494 ) -> Self {
495 Self {
496 id,
497 from_items,
498 to_items,
499 gold_delta,
500 expires_at,
501 proposer_id: proposer_id.into(),
502 receiver_id: receiver_id.into(),
503 accepted: false,
504 rejected: false,
505 }
506 }
507
508 pub fn is_expired(&self, current_time: f32) -> bool {
509 current_time > self.expires_at
510 }
511
512 pub fn is_pending(&self) -> bool {
513 !self.accepted && !self.rejected
514 }
515}
516
517const PRICE_HISTORY_SIZE: usize = 20;
522
523#[derive(Debug, Clone)]
525struct PriceRingBuffer {
526 values: [u64; PRICE_HISTORY_SIZE],
527 head: usize,
528 count: usize,
529}
530
531impl PriceRingBuffer {
532 fn new() -> Self {
533 Self { values: [0u64; PRICE_HISTORY_SIZE], head: 0, count: 0 }
534 }
535
536 fn push(&mut self, price: u64) {
537 self.values[self.head] = price;
538 self.head = (self.head + 1) % PRICE_HISTORY_SIZE;
539 if self.count < PRICE_HISTORY_SIZE {
540 self.count += 1;
541 }
542 }
543
544 fn moving_average(&self) -> f32 {
545 if self.count == 0 {
546 return 0.0;
547 }
548 let sum: u64 = self.values[..self.count].iter().sum();
549 sum as f32 / self.count as f32
550 }
551
552 fn last(&self) -> Option<u64> {
553 if self.count == 0 {
554 return None;
555 }
556 let last_idx = if self.head == 0 { PRICE_HISTORY_SIZE - 1 } else { self.head - 1 };
557 Some(self.values[last_idx])
558 }
559}
560
561#[derive(Debug, Clone)]
567struct ItemEconomy {
568 recent_sales: f32,
570 recent_supply: f32,
572 equilibrium_price: u64,
574 base_price: u64,
576 price_history: PriceRingBuffer,
577}
578
579impl ItemEconomy {
580 fn new(base_price: u64) -> Self {
581 Self {
582 recent_sales: 0.0,
583 recent_supply: 1.0,
584 equilibrium_price: base_price,
585 base_price,
586 price_history: PriceRingBuffer::new(),
587 }
588 }
589
590 fn demand_factor(&self) -> f32 {
592 if self.recent_supply <= 0.0 {
593 return 3.0;
594 }
595 (self.recent_sales / self.recent_supply).clamp(0.25, 3.0)
596 }
597
598 fn update_price(&mut self) {
600 let factor = self.demand_factor();
601 let target = (self.base_price as f32 * factor).round() as u64;
603 let t = 0.10_f32;
605 let eq = self.equilibrium_price as f32;
606 let new_eq = eq + t * (target as f32 - eq);
607 self.equilibrium_price = new_eq.round().max(1.0) as u64;
608 self.price_history.push(self.equilibrium_price);
609 }
610
611 fn decay(&mut self, dt: f32) {
613 let decay_rate = 0.02_f32 * dt;
614 self.recent_sales = (self.recent_sales * (1.0 - decay_rate)).max(0.0);
615 self.recent_supply = (self.recent_supply * (1.0 - decay_rate * 0.5)).max(0.01);
616 }
617}
618
619#[derive(Debug, Clone)]
621pub struct Economy {
622 items: HashMap<String, ItemEconomy>,
623 pub tax_system: TaxSystem,
624 update_accumulator: f32,
626 update_interval: f32,
627}
628
629impl Economy {
630 pub fn new() -> Self {
631 Self {
632 items: HashMap::new(),
633 tax_system: TaxSystem::new(),
634 update_accumulator: 0.0,
635 update_interval: 30.0,
636 }
637 }
638
639 pub fn register_item(&mut self, item_id: impl Into<String>, base_price_copper: u64) {
641 self.items.insert(item_id.into(), ItemEconomy::new(base_price_copper));
642 }
643
644 pub fn current_price(&self, item_id: &str) -> Option<u64> {
646 self.items.get(item_id).map(|e| e.equilibrium_price)
647 }
648
649 pub fn current_price_currency(&self, item_id: &str) -> Option<Currency> {
651 self.current_price(item_id).map(Currency::from_copper)
652 }
653
654 pub fn average_price(&self, item_id: &str) -> Option<f32> {
656 self.items.get(item_id).map(|e| e.price_history.moving_average())
657 }
658
659 pub fn demand_factor(&self, item_id: &str) -> f32 {
661 self.items.get(item_id).map(|e| e.demand_factor()).unwrap_or(1.0)
662 }
663
664 pub fn record_sale(&mut self, item_id: &str, qty: u32, _price_copper: u64) {
666 if let Some(item) = self.items.get_mut(item_id) {
667 item.recent_sales += qty as f32;
668 }
669 }
670
671 pub fn record_purchase(&mut self, item_id: &str, qty: u32) {
673 if let Some(item) = self.items.get_mut(item_id) {
674 item.recent_supply += qty as f32;
675 }
676 }
677
678 pub fn update_prices(&mut self, dt: f32) {
680 for item in self.items.values_mut() {
682 item.decay(dt);
683 }
684
685 self.update_accumulator += dt;
687 if self.update_accumulator >= self.update_interval {
688 self.update_accumulator = 0.0;
689 for item in self.items.values_mut() {
690 item.update_price();
691 }
692 }
693 }
694
695 pub fn price_modifier(&self, item_id: &str, player_rep_factor: f32) -> PriceModifier {
697 let item = self.items.get(item_id);
698 let base_price = item.map(|e| e.base_price).unwrap_or(100);
699 let demand_factor = item.map(|e| e.demand_factor()).unwrap_or(1.0);
700 let supply_factor = if let Some(e) = item {
701 if e.recent_sales > 0.0 {
702 (e.recent_supply / e.recent_sales).clamp(0.25, 2.0)
703 } else {
704 1.0
705 }
706 } else {
707 1.0
708 };
709 PriceModifier {
710 base_price,
711 demand_factor,
712 supply_factor,
713 player_rep_factor,
714 }
715 }
716
717 pub fn register_defaults(&mut self) {
719 self.register_item("iron_ingot", 200);
721 self.register_item("steel_ingot", 600);
722 self.register_item("coal", 30);
723 self.register_item("leather_strip", 50);
724 self.register_item("wooden_plank", 40);
725 self.register_item("iron_sword", 1500);
727 self.register_item("iron_shield", 1800);
728 self.register_item("steel_sword", 4500);
729 self.register_item("iron_helmet", 1200);
730 self.register_item("red_herb", 60);
732 self.register_item("blue_herb", 80);
733 self.register_item("clean_water", 10);
734 self.register_item("spring_water", 25);
735 self.register_item("golden_root", 250);
736 self.register_item("health_potion_minor", 500);
737 self.register_item("health_potion_major", 1500);
738 self.register_item("mana_potion", 700);
739 self.register_item("raw_meat", 80);
741 self.register_item("salt", 20);
742 self.register_item("roasted_meat", 180);
743 self.register_item("hearty_stew", 400);
744 self.register_item("magic_dust", 300);
746 self.register_item("fire_essence", 800);
747 self.register_item("light_rune", 600);
748 self.register_item("silver_ingot", 500);
750 self.register_item("gold_chain", 2000);
751 self.register_item("ruby_gem", 3000);
752 self.register_item("sapphire_gem", 3500);
753 self.register_item("silver_ring", 1200);
754 self.register_item("ruby_necklace", 8000);
755 }
756}
757
758impl Default for Economy {
759 fn default() -> Self {
760 let mut e = Self::new();
761 e.register_defaults();
762 e
763 }
764}