1use std::collections::{HashMap, VecDeque};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct FactionId(pub u32);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct TradeRouteId(pub u32);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct TributeAgreementId(pub u32);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct EspionageOperationId(pub u32);
24
25#[derive(Debug, Clone)]
31pub struct FactionTreasury {
32 pub faction: FactionId,
33 pub gold: f64,
35 pub total_income: f64,
37 pub total_expenditure: f64,
39 income_log: HashMap<String, VecDeque<f64>>,
41 expenditure_log: HashMap<String, VecDeque<f64>>,
43}
44
45impl FactionTreasury {
46 pub fn new(faction: FactionId, starting_gold: f64) -> Self {
47 Self {
48 faction,
49 gold: starting_gold,
50 total_income: starting_gold,
51 total_expenditure: 0.0,
52 income_log: HashMap::new(),
53 expenditure_log: HashMap::new(),
54 }
55 }
56
57 pub fn receive(&mut self, amount: f64, category: &str) {
59 self.gold += amount;
60 self.total_income += amount;
61 let log = self.income_log.entry(category.to_string()).or_insert_with(|| VecDeque::with_capacity(32));
62 log.push_back(amount);
63 if log.len() > 32 { log.pop_front(); }
64 }
65
66 pub fn spend(&mut self, amount: f64, category: &str) -> bool {
68 let had_enough = self.gold >= amount;
69 self.gold -= amount;
70 self.total_expenditure += amount;
71 let log = self.expenditure_log.entry(category.to_string()).or_insert_with(|| VecDeque::with_capacity(32));
72 log.push_back(amount);
73 if log.len() > 32 { log.pop_front(); }
74 had_enough
75 }
76
77 pub fn avg_income(&self, category: &str, n: usize) -> f64 {
79 match self.income_log.get(category) {
80 None => 0.0,
81 Some(log) => {
82 let slice: Vec<_> = log.iter().rev().take(n).collect();
83 if slice.is_empty() { return 0.0; }
84 slice.iter().copied().sum::<f64>() / slice.len() as f64
85 }
86 }
87 }
88
89 pub fn net_income_rate(&self) -> f64 {
91 let income: f64 = self.income_log.values()
92 .filter_map(|log| {
93 if log.is_empty() { return None; }
94 Some(log.iter().sum::<f64>() / log.len() as f64)
95 })
96 .sum();
97 let expenditure: f64 = self.expenditure_log.values()
98 .filter_map(|log| {
99 if log.is_empty() { return None; }
100 Some(log.iter().sum::<f64>() / log.len() as f64)
101 })
102 .sum();
103 income - expenditure
104 }
105}
106
107#[derive(Debug, Clone)]
113pub struct TaxPolicy {
114 pub faction: FactionId,
115 pub flat_rate: f64,
117 pub brackets: Vec<(f64, f64)>,
119 pub trade_tax_rate: f64,
121 pub import_duty: f64,
123 pub export_duty: f64,
125 pub luxury_multiplier: f64,
127 pub evasion_rate: f64,
129}
130
131impl TaxPolicy {
132 pub fn default_for(faction: FactionId) -> Self {
133 Self {
134 faction,
135 flat_rate: 0.15,
136 brackets: vec![(1000.0, 0.10), (5000.0, 0.20), (20000.0, 0.35)],
137 trade_tax_rate: 0.10,
138 import_duty: 0.05,
139 export_duty: 0.03,
140 luxury_multiplier: 1.5,
141 evasion_rate: 0.05,
142 }
143 }
144
145 pub fn compute_tax(&self, income: f64) -> f64 {
147 let mut tax = 0.0;
148 let mut remaining = income;
149 let mut prev_threshold = 0.0;
150 for &(threshold, rate) in &self.brackets {
151 if remaining <= 0.0 { break; }
152 let bracket_income = (threshold - prev_threshold).min(remaining);
153 tax += bracket_income * rate;
154 remaining -= bracket_income;
155 prev_threshold = threshold;
156 }
157 if remaining > 0.0 {
158 tax += remaining * self.flat_rate;
159 }
160 tax * (1.0 - self.evasion_rate)
162 }
163
164 pub fn trade_tax(&self, value: f64, is_import: bool, is_luxury: bool) -> f64 {
166 let duty = if is_import { self.import_duty } else { self.export_duty };
167 let base = value * (self.trade_tax_rate + duty);
168 let lux = if is_luxury { base * self.luxury_multiplier } else { base };
169 lux * (1.0 - self.evasion_rate)
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum TradeRouteStatus {
179 Active,
180 Disrupted,
181 Embargoed,
182 Closed,
183}
184
185#[derive(Debug, Clone)]
187pub struct TradeRoute {
188 pub id: TradeRouteId,
189 pub from_faction: FactionId,
190 pub to_faction: FactionId,
191 pub status: TradeRouteStatus,
192 pub base_volume: f64,
194 pub current_volume: f64,
196 pub disruption: f64,
198 pub export_share_from: f64,
200 pub import_share_to: f64,
202 pub age_ticks: u64,
204 pub volume_history: VecDeque<f64>,
206 pub escorted: bool,
208 pub infrastructure: f64,
210}
211
212impl TradeRoute {
213 pub fn new(
214 id: TradeRouteId,
215 from: FactionId,
216 to: FactionId,
217 base_volume: f64,
218 export_share: f64,
219 import_share: f64,
220 ) -> Self {
221 Self {
222 id,
223 from_faction: from,
224 to_faction: to,
225 status: TradeRouteStatus::Active,
226 base_volume,
227 current_volume: base_volume,
228 disruption: 1.0,
229 export_share_from: export_share,
230 import_share_to: import_share,
231 age_ticks: 0,
232 volume_history: VecDeque::with_capacity(64),
233 escorted: false,
234 infrastructure: 0.0,
235 }
236 }
237
238 pub fn apply_disruption(&mut self, severity: f64) {
240 let mitigation = if self.escorted { 0.5 } else { 0.0 };
241 let infra_mitigation = (self.infrastructure / 200.0).min(0.3);
242 self.disruption = (self.disruption - severity * (1.0 - mitigation - infra_mitigation)).max(0.0);
243 self.update_status();
244 }
245
246 pub fn recover(&mut self, rate: f64) {
248 self.disruption = (self.disruption + rate).min(1.0);
249 self.update_status();
250 }
251
252 fn update_status(&mut self) {
253 if self.status == TradeRouteStatus::Embargoed || self.status == TradeRouteStatus::Closed {
254 return;
255 }
256 self.status = if self.disruption < 0.1 {
257 TradeRouteStatus::Disrupted
258 } else {
259 TradeRouteStatus::Active
260 };
261 }
262
263 pub fn tick(&mut self) -> (f64, f64) {
265 self.age_ticks += 1;
266 if self.status == TradeRouteStatus::Embargoed || self.status == TradeRouteStatus::Closed {
267 self.current_volume = 0.0;
268 self.volume_history.push_back(0.0);
269 if self.volume_history.len() > 64 { self.volume_history.pop_front(); }
270 return (0.0, 0.0);
271 }
272 let infra_bonus = 1.0 + (self.infrastructure / 100.0) * 0.05;
274 self.current_volume = self.base_volume * self.disruption * infra_bonus;
275 self.volume_history.push_back(self.current_volume);
276 if self.volume_history.len() > 64 { self.volume_history.pop_front(); }
277 let export_income = self.current_volume * self.export_share_from;
278 let import_income = self.current_volume * self.import_share_to;
279 (export_income, import_income)
280 }
281
282 pub fn avg_volume(&self, n: usize) -> f64 {
284 let slice: Vec<_> = self.volume_history.iter().rev().take(n).collect();
285 if slice.is_empty() { return 0.0; }
286 slice.iter().copied().sum::<f64>() / slice.len() as f64
287 }
288}
289
290#[derive(Debug, Clone)]
296pub struct EmbargoPenalty {
297 pub imposing_faction: FactionId,
298 pub target_faction: FactionId,
299 pub blocks_trade: bool,
301 pub asset_freeze: bool,
303 pub extra_tariff: f64,
305 pub imposed_tick: u64,
307 pub lifted_tick: Option<u64>,
309 pub reason: String,
310}
311
312#[derive(Debug, Clone)]
317pub struct WarReparations {
318 pub paying_faction: FactionId,
319 pub receiving_faction: FactionId,
320 pub total_amount: f64,
322 pub paid: f64,
324 pub installment: f64,
326 pub imposed_tick: u64,
327 pub completed: bool,
328}
329
330impl WarReparations {
331 pub fn new(payer: FactionId, receiver: FactionId, total: f64, installment: f64, tick: u64) -> Self {
332 Self {
333 paying_faction: payer,
334 receiving_faction: receiver,
335 total_amount: total,
336 paid: 0.0,
337 installment,
338 imposed_tick: tick,
339 completed: false,
340 }
341 }
342
343 pub fn process(&mut self) -> f64 {
345 if self.completed { return 0.0; }
346 let amount = self.installment.min(self.total_amount - self.paid);
347 self.paid += amount;
348 if self.paid >= self.total_amount - 1e-9 { self.completed = true; }
349 amount
350 }
351
352 pub fn remaining(&self) -> f64 {
353 self.total_amount - self.paid
354 }
355}
356
357#[derive(Debug, Clone)]
363pub struct TributeAgreement {
364 pub id: TributeAgreementId,
365 pub paying_faction: FactionId,
366 pub receiving_faction: FactionId,
367 pub amount_per_tick: f64,
369 pub start_tick: u64,
371 pub duration: Option<u64>,
373 pub total_paid: f64,
374 pub active: bool,
375 pub coercion_level: f64,
377 pub rebellion_chance: f64,
379}
380
381impl TributeAgreement {
382 pub fn process(&mut self, current_tick: u64, rng_val: f64) -> f64 {
384 if !self.active { return 0.0; }
385 if let Some(dur) = self.duration {
386 if current_tick >= self.start_tick + dur {
387 self.active = false;
388 return 0.0;
389 }
390 }
391 if self.coercion_level < 0.5 && rng_val < self.rebellion_chance {
393 self.active = false;
394 return 0.0;
395 }
396 self.total_paid += self.amount_per_tick;
397 self.amount_per_tick
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum EspionageObjective {
407 IntelTreasury,
409 SabotageRoute,
411 StealTaxPolicy,
413 Disinformation,
415 CorruptProduction,
417}
418
419#[derive(Debug, Clone)]
420pub struct EspionageOperation {
421 pub id: EspionageOperationId,
422 pub instigator: FactionId,
423 pub target: FactionId,
424 pub objective: EspionageObjective,
425 pub started_tick: u64,
426 pub completion_tick: u64,
427 pub success_probability: f64,
428 pub resolved: bool,
429 pub succeeded: Option<bool>,
430}
431
432#[derive(Debug, Clone)]
434pub struct EspionageReport {
435 pub operation_id: EspionageOperationId,
436 pub instigator: FactionId,
437 pub target: FactionId,
438 pub objective: EspionageObjective,
439 pub succeeded: bool,
440 pub resolved_tick: u64,
441 pub intel: Option<EspionageIntel>,
442}
443
444#[derive(Debug, Clone)]
445pub enum EspionageIntel {
446 TreasuryBalance(f64),
447 TaxRates { flat: f64, trade: f64, import_duty: f64 },
448 RouteDisrupted { route_id: TradeRouteId, disruption: f64 },
449 DisinformationPlanted,
450 ProductionCorrupted,
451}
452
453#[derive(Debug, Clone)]
458pub struct WealthRanking {
459 pub ranked: Vec<(FactionId, f64)>,
461 pub computed_tick: u64,
462}
463
464impl WealthRanking {
465 pub fn richest(&self) -> Option<(FactionId, f64)> {
467 self.ranked.first().copied()
468 }
469
470 pub fn poorest(&self) -> Option<(FactionId, f64)> {
472 self.ranked.last().copied()
473 }
474
475 pub fn rank_of(&self, faction: FactionId) -> Option<usize> {
477 self.ranked.iter().position(|(id, _)| *id == faction).map(|i| i + 1)
478 }
479
480 pub fn gini(&self) -> f64 {
482 let n = self.ranked.len();
483 if n == 0 { return 0.0; }
484 let mut sorted: Vec<f64> = self.ranked.iter().map(|(_, w)| w.max(0.0)).collect();
485 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
486 let total: f64 = sorted.iter().sum();
487 if total < 1e-9 { return 0.0; }
488 let sum_of_ranks: f64 = sorted.iter().enumerate()
489 .map(|(i, w)| (2 * (i as i64 + 1) - n as i64 - 1) as f64 * w)
490 .sum();
491 sum_of_ranks / (n as f64 * total)
492 }
493}
494
495#[derive(Debug, Clone)]
501pub struct Faction {
502 pub id: FactionId,
503 pub name: String,
504 pub treasury: FactionTreasury,
505 pub tax_policy: TaxPolicy,
506 pub trade_route_ids: Vec<TradeRouteId>,
508 pub embargoes: Vec<EmbargoPenalty>,
510 pub reparations_paying: Vec<WarReparations>,
512 pub reparations_receiving: Vec<WarReparations>,
514 pub tribute_paying: Vec<TributeAgreementId>,
516 pub tribute_receiving: Vec<TributeAgreementId>,
518 pub espionage_ops: Vec<EspionageOperationId>,
520 pub disinformation_penalty: f64,
522 pub gdp_proxy: f64,
524 pub at_war_with: Vec<FactionId>,
526}
527
528impl Faction {
529 pub fn new(id: FactionId, name: &str, starting_gold: f64) -> Self {
530 Self {
531 id,
532 name: name.to_string(),
533 treasury: FactionTreasury::new(id, starting_gold),
534 tax_policy: TaxPolicy::default_for(id),
535 trade_route_ids: Vec::new(),
536 embargoes: Vec::new(),
537 reparations_paying: Vec::new(),
538 reparations_receiving: Vec::new(),
539 tribute_paying: Vec::new(),
540 tribute_receiving: Vec::new(),
541 espionage_ops: Vec::new(),
542 disinformation_penalty: 0.0,
543 gdp_proxy: 0.0,
544 at_war_with: Vec::new(),
545 }
546 }
547
548 pub fn net_worth(&self) -> f64 {
549 self.treasury.gold
550 }
551}
552
553pub struct FactionEconomy {
560 next_faction_id: u32,
561 next_route_id: u32,
562 next_tribute_id: u32,
563 next_espionage_id: u32,
564 pub current_tick: u64,
565
566 pub factions: HashMap<FactionId, Faction>,
567 pub trade_routes: HashMap<TradeRouteId, TradeRoute>,
568 pub tribute_agreements: HashMap<TributeAgreementId, TributeAgreement>,
569 pub espionage_ops: HashMap<EspionageOperationId, EspionageOperation>,
570 pub espionage_reports: Vec<EspionageReport>,
571 pub wealth_ranking: Option<WealthRanking>,
572
573 rng_state: u64,
575}
576
577impl FactionEconomy {
578 pub fn new() -> Self {
579 Self {
580 next_faction_id: 1,
581 next_route_id: 1,
582 next_tribute_id: 1,
583 next_espionage_id: 1,
584 current_tick: 0,
585 factions: HashMap::new(),
586 trade_routes: HashMap::new(),
587 tribute_agreements: HashMap::new(),
588 espionage_ops: HashMap::new(),
589 espionage_reports: Vec::new(),
590 wealth_ranking: None,
591 rng_state: 0xDEAD_BEEF_CAFE_1234,
592 }
593 }
594
595 fn next_rand(&mut self) -> f64 {
597 let mut x = self.rng_state;
598 x ^= x << 13;
599 x ^= x >> 7;
600 x ^= x << 17;
601 self.rng_state = x;
602 (x as f64) / (u64::MAX as f64)
603 }
604
605 pub fn add_faction(&mut self, name: &str, starting_gold: f64) -> FactionId {
610 let id = FactionId(self.next_faction_id);
611 self.next_faction_id += 1;
612 self.factions.insert(id, Faction::new(id, name, starting_gold));
613 id
614 }
615
616 pub fn faction(&self, id: FactionId) -> Option<&Faction> {
617 self.factions.get(&id)
618 }
619
620 pub fn faction_mut(&mut self, id: FactionId) -> Option<&mut Faction> {
621 self.factions.get_mut(&id)
622 }
623
624 pub fn declare_war(&mut self, faction_a: FactionId, faction_b: FactionId) {
626 if let Some(f) = self.factions.get_mut(&faction_a) {
627 if !f.at_war_with.contains(&faction_b) { f.at_war_with.push(faction_b); }
628 }
629 if let Some(f) = self.factions.get_mut(&faction_b) {
630 if !f.at_war_with.contains(&faction_a) { f.at_war_with.push(faction_a); }
631 }
632 let tick = self.current_tick;
634 for route in self.trade_routes.values_mut() {
635 let involves_a = route.from_faction == faction_a || route.to_faction == faction_a;
636 let involves_b = route.from_faction == faction_b || route.to_faction == faction_b;
637 if involves_a && involves_b {
638 route.status = TradeRouteStatus::Closed;
639 let _ = tick; }
641 }
642 }
643
644 pub fn end_war(&mut self, faction_a: FactionId, faction_b: FactionId) {
646 if let Some(f) = self.factions.get_mut(&faction_a) {
647 f.at_war_with.retain(|&id| id != faction_b);
648 }
649 if let Some(f) = self.factions.get_mut(&faction_b) {
650 f.at_war_with.retain(|&id| id != faction_a);
651 }
652 }
653
654 pub fn add_trade_route(
659 &mut self,
660 from: FactionId,
661 to: FactionId,
662 base_volume: f64,
663 export_share: f64,
664 import_share: f64,
665 ) -> TradeRouteId {
666 let id = TradeRouteId(self.next_route_id);
667 self.next_route_id += 1;
668 let route = TradeRoute::new(id, from, to, base_volume, export_share, import_share);
669 self.trade_routes.insert(id, route);
670 if let Some(f) = self.factions.get_mut(&from) { f.trade_route_ids.push(id); }
671 if let Some(f) = self.factions.get_mut(&to) { f.trade_route_ids.push(id); }
672 id
673 }
674
675 pub fn disrupt_route(&mut self, id: TradeRouteId, severity: f64) {
676 if let Some(route) = self.trade_routes.get_mut(&id) {
677 route.apply_disruption(severity);
678 }
679 }
680
681 pub fn close_route(&mut self, id: TradeRouteId) {
682 if let Some(route) = self.trade_routes.get_mut(&id) {
683 route.status = TradeRouteStatus::Closed;
684 }
685 }
686
687 pub fn reopen_route(&mut self, id: TradeRouteId) {
688 if let Some(route) = self.trade_routes.get_mut(&id) {
689 if route.status == TradeRouteStatus::Closed {
690 route.status = TradeRouteStatus::Active;
691 }
692 }
693 }
694
695 pub fn invest_infrastructure(&mut self, id: TradeRouteId, amount: f64) {
696 if let Some(route) = self.trade_routes.get_mut(&id) {
697 route.infrastructure += amount;
698 }
699 }
700
701 pub fn impose_embargo(
706 &mut self,
707 imposing: FactionId,
708 target: FactionId,
709 extra_tariff: f64,
710 blocks_trade: bool,
711 asset_freeze: bool,
712 reason: &str,
713 ) {
714 let tick = self.current_tick;
715 let embargo = EmbargoPenalty {
716 imposing_faction: imposing,
717 target_faction: target,
718 blocks_trade,
719 asset_freeze,
720 extra_tariff,
721 imposed_tick: tick,
722 lifted_tick: None,
723 reason: reason.to_string(),
724 };
725 if blocks_trade {
726 for route in self.trade_routes.values_mut() {
728 let involves_i = route.from_faction == imposing || route.to_faction == imposing;
729 let involves_t = route.from_faction == target || route.to_faction == target;
730 if involves_i && involves_t {
731 route.status = TradeRouteStatus::Embargoed;
732 }
733 }
734 }
735 if let Some(f) = self.factions.get_mut(&imposing) {
736 f.embargoes.push(embargo);
737 }
738 }
739
740 pub fn lift_embargo(&mut self, imposing: FactionId, target: FactionId) {
741 let tick = self.current_tick;
742 if let Some(f) = self.factions.get_mut(&imposing) {
743 for emb in f.embargoes.iter_mut() {
744 if emb.target_faction == target && emb.lifted_tick.is_none() {
745 emb.lifted_tick = Some(tick);
746 }
747 }
748 }
749 for route in self.trade_routes.values_mut() {
751 let involves_i = route.from_faction == imposing || route.to_faction == imposing;
752 let involves_t = route.from_faction == target || route.to_faction == target;
753 if involves_i && involves_t && route.status == TradeRouteStatus::Embargoed {
754 route.status = TradeRouteStatus::Active;
755 }
756 }
757 }
758
759 pub fn impose_reparations(
764 &mut self,
765 payer: FactionId,
766 receiver: FactionId,
767 total: f64,
768 installment: f64,
769 ) {
770 let tick = self.current_tick;
771 let reps = WarReparations::new(payer, receiver, total, installment, tick);
772 if let Some(f) = self.factions.get_mut(&payer) {
773 f.reparations_paying.push(reps.clone());
774 }
775 if let Some(f) = self.factions.get_mut(&receiver) {
776 f.reparations_receiving.push(reps);
777 }
778 }
779
780 pub fn add_tribute(
785 &mut self,
786 payer: FactionId,
787 receiver: FactionId,
788 amount_per_tick: f64,
789 duration: Option<u64>,
790 coercion_level: f64,
791 rebellion_chance: f64,
792 ) -> TributeAgreementId {
793 let id = TributeAgreementId(self.next_tribute_id);
794 self.next_tribute_id += 1;
795 let tick = self.current_tick;
796 let agreement = TributeAgreement {
797 id,
798 paying_faction: payer,
799 receiving_faction: receiver,
800 amount_per_tick,
801 start_tick: tick,
802 duration,
803 total_paid: 0.0,
804 active: true,
805 coercion_level,
806 rebellion_chance,
807 };
808 self.tribute_agreements.insert(id, agreement);
809 if let Some(f) = self.factions.get_mut(&payer) { f.tribute_paying.push(id); }
810 if let Some(f) = self.factions.get_mut(&receiver) { f.tribute_receiving.push(id); }
811 id
812 }
813
814 pub fn launch_espionage(
820 &mut self,
821 instigator: FactionId,
822 target: FactionId,
823 objective: EspionageObjective,
824 success_probability: f64,
825 duration_ticks: u64,
826 ) -> EspionageOperationId {
827 let id = EspionageOperationId(self.next_espionage_id);
828 self.next_espionage_id += 1;
829 let tick = self.current_tick;
830 let op = EspionageOperation {
831 id,
832 instigator,
833 target,
834 objective,
835 started_tick: tick,
836 completion_tick: tick + duration_ticks,
837 success_probability,
838 resolved: false,
839 succeeded: None,
840 };
841 self.espionage_ops.insert(id, op);
842 if let Some(f) = self.factions.get_mut(&instigator) {
843 f.espionage_ops.push(id);
844 }
845 id
846 }
847
848 fn resolve_espionage(&mut self) {
850 let tick = self.current_tick;
851 let ready: Vec<EspionageOperationId> = self.espionage_ops.iter()
852 .filter(|(_, op)| !op.resolved && op.completion_tick <= tick)
853 .map(|(id, _)| *id)
854 .collect();
855
856 for op_id in ready {
857 let roll = self.next_rand();
858 let op = match self.espionage_ops.get_mut(&op_id) {
859 Some(o) => o,
860 None => continue,
861 };
862 let success = roll < op.success_probability;
863 op.resolved = true;
864 op.succeeded = Some(success);
865
866 let instigator = op.instigator;
867 let target = op.target;
868 let objective = op.objective;
869
870 let intel = if success {
871 match objective {
872 EspionageObjective::IntelTreasury => {
873 let balance = self.factions.get(&target)
874 .map(|f| f.treasury.gold)
875 .unwrap_or(0.0);
876 Some(EspionageIntel::TreasuryBalance(balance))
877 }
878 EspionageObjective::StealTaxPolicy => {
879 let (flat, trade, import) = self.factions.get(&target)
880 .map(|f| (f.tax_policy.flat_rate, f.tax_policy.trade_tax_rate, f.tax_policy.import_duty))
881 .unwrap_or((0.0, 0.0, 0.0));
882 Some(EspionageIntel::TaxRates { flat, trade, import_duty: import })
883 }
884 EspionageObjective::SabotageRoute => {
885 let route_ids: Vec<TradeRouteId> = self.trade_routes.keys()
887 .filter(|&&rid| {
888 let r = &self.trade_routes[&rid];
889 r.from_faction == target || r.to_faction == target
890 })
891 .copied()
892 .collect();
893 if let Some(&rid) = route_ids.first() {
894 let disruption_applied = 0.4;
895 if let Some(route) = self.trade_routes.get_mut(&rid) {
896 route.apply_disruption(disruption_applied);
897 Some(EspionageIntel::RouteDisrupted { route_id: rid, disruption: route.disruption })
898 } else {
899 None
900 }
901 } else {
902 None
903 }
904 }
905 EspionageObjective::Disinformation => {
906 if let Some(f) = self.factions.get_mut(&target) {
907 f.disinformation_penalty = (f.disinformation_penalty + 0.15).min(0.5);
908 }
909 Some(EspionageIntel::DisinformationPlanted)
910 }
911 EspionageObjective::CorruptProduction => {
912 Some(EspionageIntel::ProductionCorrupted)
913 }
914 }
915 } else {
916 None
917 };
918
919 self.espionage_reports.push(EspionageReport {
920 operation_id: op_id,
921 instigator,
922 target,
923 objective,
924 succeeded: success,
925 resolved_tick: tick,
926 intel,
927 });
928 }
929 if self.espionage_reports.len() > 128 {
931 let drain = self.espionage_reports.len() - 128;
932 self.espionage_reports.drain(0..drain);
933 }
934 }
935
936 pub fn compute_wealth_ranking(&mut self) {
941 let tick = self.current_tick;
942 let mut ranked: Vec<(FactionId, f64)> = self.factions.iter()
943 .map(|(&id, f)| (id, f.net_worth()))
944 .collect();
945 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
946 self.wealth_ranking = Some(WealthRanking { ranked, computed_tick: tick });
947 }
948
949 fn collect_taxes(&mut self, route_incomes: &HashMap<FactionId, f64>) {
955 for (&faction_id, &income) in route_incomes {
956 let tax = self.factions.get(&faction_id)
957 .map(|f| f.tax_policy.trade_tax(income, false, false))
958 .unwrap_or(0.0);
959 if let Some(f) = self.factions.get_mut(&faction_id) {
960 f.treasury.receive(tax, "trade_tax");
961 }
962 }
963 }
964
965 pub fn tick(&mut self) {
979 self.current_tick += 1;
980 let tick = self.current_tick;
981
982 let route_ids: Vec<TradeRouteId> = self.trade_routes.keys().copied().collect();
984 let mut route_incomes: HashMap<FactionId, f64> = HashMap::new();
985 for &rid in &route_ids {
986 let (from_id, to_id, export_income, import_income) = {
987 let route = self.trade_routes.get_mut(&rid).unwrap();
988 let (exp, imp) = route.tick();
989 (route.from_faction, route.to_faction, exp, imp)
990 };
991 let dis_penalty = self.factions.get(&from_id).map(|f| f.disinformation_penalty).unwrap_or(0.0);
993 let actual_export = export_income * (1.0 - dis_penalty);
994 let actual_import = import_income;
995
996 if let Some(f) = self.factions.get_mut(&from_id) {
997 f.treasury.receive(actual_export, "trade_export");
998 f.gdp_proxy += actual_export;
999 }
1000 if let Some(f) = self.factions.get_mut(&to_id) {
1001 f.treasury.receive(actual_import, "trade_import");
1002 f.gdp_proxy += actual_import;
1003 }
1004 *route_incomes.entry(from_id).or_insert(0.0) += actual_export;
1005 *route_incomes.entry(to_id).or_insert(0.0) += actual_import;
1006 if let Some(route) = self.trade_routes.get_mut(&rid) {
1008 route.recover(0.01);
1009 }
1010 }
1011
1012 self.collect_taxes(&route_incomes);
1014
1015 let paying_faction_ids: Vec<FactionId> = self.factions.keys().copied().collect();
1018 for fid in &paying_faction_ids {
1019 let mut to_transfer: Vec<(FactionId, f64)> = Vec::new();
1021 if let Some(f) = self.factions.get_mut(fid) {
1022 for rep in f.reparations_paying.iter_mut() {
1023 if rep.completed { continue; }
1024 let amount = rep.process();
1025 if amount > 0.0 {
1026 f.treasury.spend(amount, "reparations");
1027 to_transfer.push((rep.receiving_faction, amount));
1028 }
1029 }
1030 }
1031 for (receiver_id, amount) in to_transfer {
1032 if let Some(rf) = self.factions.get_mut(&receiver_id) {
1033 rf.treasury.receive(amount, "reparations_received");
1034 }
1035 }
1036 }
1037
1038 let rng_val = self.next_rand();
1040 let tribute_ids: Vec<TributeAgreementId> = self.tribute_agreements.keys().copied().collect();
1041 for &tid in &tribute_ids {
1042 let (payer, receiver, amount) = {
1043 let agr = self.tribute_agreements.get_mut(&tid).unwrap();
1044 let amount = agr.process(tick, rng_val);
1045 (agr.paying_faction, agr.receiving_faction, amount)
1046 };
1047 if amount > 0.0 {
1048 if let Some(f) = self.factions.get_mut(&payer) {
1049 f.treasury.spend(amount, "tribute");
1050 }
1051 if let Some(f) = self.factions.get_mut(&receiver) {
1052 f.treasury.receive(amount, "tribute_received");
1053 }
1054 }
1055 }
1056
1057 for f in self.factions.values_mut() {
1059 f.disinformation_penalty = (f.disinformation_penalty - 0.005).max(0.0);
1060 }
1061
1062 self.resolve_espionage();
1064
1065 self.compute_wealth_ranking();
1067 }
1068
1069 pub fn routes_for_faction(&self, faction: FactionId) -> Vec<&TradeRoute> {
1075 self.trade_routes.values()
1076 .filter(|r| r.from_faction == faction || r.to_faction == faction)
1077 .collect()
1078 }
1079
1080 pub fn global_gdp(&self) -> f64 {
1082 self.factions.values().map(|f| f.gdp_proxy).sum()
1083 }
1084
1085 pub fn dominant_faction(&self) -> Option<FactionId> {
1087 self.wealth_ranking.as_ref()?.richest().map(|(id, _)| id)
1088 }
1089
1090 pub fn active_embargoes(&self, faction: FactionId) -> Vec<&EmbargoPenalty> {
1092 self.factions.get(&faction)
1093 .map(|f| f.embargoes.iter().filter(|e| e.lifted_tick.is_none()).collect())
1094 .unwrap_or_default()
1095 }
1096
1097 pub fn reports_for(&self, faction: FactionId) -> Vec<&EspionageReport> {
1099 self.espionage_reports.iter()
1100 .filter(|r| r.instigator == faction || r.target == faction)
1101 .collect()
1102 }
1103}
1104
1105impl Default for FactionEconomy {
1106 fn default() -> Self {
1107 Self::new()
1108 }
1109}
1110
1111#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_trade_route_income() {
1121 let mut fe = FactionEconomy::new();
1122 let a = fe.add_faction("Alpha", 10_000.0);
1123 let b = fe.add_faction("Beta", 8_000.0);
1124 let _rid = fe.add_trade_route(a, b, 1000.0, 0.4, 0.3);
1125 fe.tick();
1126 let alpha = fe.faction(a).unwrap();
1127 assert!(alpha.treasury.gold > 10_000.0);
1129 }
1130
1131 #[test]
1132 fn test_embargo_blocks_route() {
1133 let mut fe = FactionEconomy::new();
1134 let a = fe.add_faction("Aland", 5_000.0);
1135 let b = fe.add_faction("Bland", 5_000.0);
1136 let rid = fe.add_trade_route(a, b, 500.0, 0.5, 0.5);
1137 fe.impose_embargo(a, b, 0.2, true, false, "Political dispute");
1138 let route = &fe.trade_routes[&rid];
1139 assert_eq!(route.status, TradeRouteStatus::Embargoed);
1140 fe.tick();
1141 let fa = fe.faction(a).unwrap();
1143 assert!(fa.treasury.gold <= 5_001.0, "embargoed route should not produce income");
1144 }
1145
1146 #[test]
1147 fn test_reparations() {
1148 let mut fe = FactionEconomy::new();
1149 let loser = fe.add_faction("Loser", 10_000.0);
1150 let winner = fe.add_faction("Winner", 5_000.0);
1151 fe.impose_reparations(loser, winner, 1_000.0, 100.0);
1152 for _ in 0..10 { fe.tick(); }
1153 let w = fe.faction(winner).unwrap();
1154 assert!(w.treasury.gold > 5_000.0 + 999.0);
1155 let l = fe.faction(loser).unwrap();
1156 assert!(l.treasury.gold < 10_000.0);
1157 }
1158
1159 #[test]
1160 fn test_wealth_ranking_gini() {
1161 let mut fe = FactionEconomy::new();
1162 fe.add_faction("Rich", 100_000.0);
1163 fe.add_faction("Poor", 1_000.0);
1164 fe.compute_wealth_ranking();
1165 let wk = fe.wealth_ranking.as_ref().unwrap();
1166 let gini = wk.gini();
1167 assert!(gini > 0.3, "highly unequal distribution should yield gini > 0.3: {}", gini);
1168 }
1169
1170 #[test]
1171 fn test_progressive_tax() {
1172 let f = FactionId(1);
1173 let policy = TaxPolicy {
1174 faction: f,
1175 flat_rate: 0.40,
1176 brackets: vec![(1_000.0, 0.10), (5_000.0, 0.25)],
1177 trade_tax_rate: 0.10,
1178 import_duty: 0.05,
1179 export_duty: 0.03,
1180 luxury_multiplier: 1.5,
1181 evasion_rate: 0.0,
1182 };
1183 let tax = policy.compute_tax(6_000.0);
1184 let expected = 100.0 + 1000.0 + 400.0;
1188 assert!((tax - expected).abs() < 1e-6, "tax={} expected={}", tax, expected);
1189 }
1190
1191 #[test]
1192 fn test_tribute_rebellion() {
1193 let mut fe = FactionEconomy::new();
1194 let payer = fe.add_faction("Vassal", 3_000.0);
1195 let recvr = fe.add_faction("Overlord", 10_000.0);
1196 fe.add_tribute(payer, recvr, 100.0, None, 0.0, 1.0);
1198 fe.tick();
1200 let agreement = fe.tribute_agreements.values().next().unwrap();
1201 assert!(!agreement.active, "tribute should have been rebelled against");
1202 }
1203
1204 #[test]
1205 fn test_war_closes_routes() {
1206 let mut fe = FactionEconomy::new();
1207 let a = fe.add_faction("North", 5_000.0);
1208 let b = fe.add_faction("South", 5_000.0);
1209 fe.add_trade_route(a, b, 500.0, 0.4, 0.4);
1210 fe.declare_war(a, b);
1211 let route = fe.trade_routes.values().next().unwrap();
1212 assert_eq!(route.status, TradeRouteStatus::Closed);
1213 }
1214}