rpfm_extensions/startpos/world/
unit_bonuses.rs1use std::collections::HashMap;
22
23use rpfm_lib::error::Result;
24
25use crate::startpos::tables::StartposTables;
26
27#[cfg(test)] #[path = "unit_bonuses_test.rs"] mod unit_bonuses_test;
28
29const STAT_BONUS_IDS: [&str; 5] = ["unit_stat_bonus_armour", "unit_stat_bonus_shield", "unit_stat_bonus_weapon", "unit_stat_bonus_ammunition", "unit_stat_bonus_horses"];
31
32const PROVINCE_SCOPES: [&str; 2] = ["province_to_province_own", "region_to_region_own_provincewide"];
34
35const REGION_SCOPE: &str = "region_to_region_own";
37
38#[derive(Debug, Clone, PartialEq, Eq)]
44enum Target {
45
46 Basic,
48
49 Category(String),
51
52 Class(String),
54}
55
56#[derive(Debug, Clone, PartialEq)]
58struct StatEffect {
59
60 stat: u32,
62
63 target: Target,
65
66 value: i64,
68
69 provincewide: bool,
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct StatBonusRules {
76
77 effects: HashMap<String, Vec<StatEffect>>,
79
80 kinds: HashMap<String, (String, String)>,
82}
83
84impl StatBonusRules {
89
90 pub fn new(tables: &StartposTables) -> Result<Self> {
96 let sources = [
97 ("effect_bonus_value_basic_junction", None),
98 ("effect_bonus_value_unit_category_junction", Some("unit_category")),
99 ("effect_bonus_value_unit_class_junction", Some("unit_class")),
100 ];
101 let mut targets = HashMap::<String, Vec<(u32, Target)>>::new();
102 for (table, column) in sources {
103 for row in tables.table(table)?.rows() {
104 let bonus = row.str("bonus_value_id")?;
105 let Some(stat) = STAT_BONUS_IDS.iter().position(|id| *id == bonus) else { continue };
106 let target = match column {
107 Some("unit_category") => Target::Category(row.str("unit_category")?.into_owned()),
108 Some(column) => Target::Class(row.str(column)?.into_owned()),
109 None => Target::Basic,
110 };
111 targets.entry(row.str("effect")?.into_owned()).or_default().push((stat as u32, target));
112 }
113 }
114
115 let mut effects = HashMap::<String, Vec<StatEffect>>::new();
116 for row in tables.table("building_effects_junction")?.rows() {
117 let Some(effect_targets) = targets.get(row.str("effect")?.as_ref()) else { continue };
118 let scope = row.str("effect_scope")?;
119 let provincewide = PROVINCE_SCOPES.contains(&scope.as_ref());
120 if !provincewide && scope != REGION_SCOPE {
121 continue;
122 }
123 let value = row.number("value")? as i64;
124 let building = effects.entry(row.str("building")?.into_owned()).or_default();
125 building.extend(effect_targets.iter().map(|(stat, target)| StatEffect { stat: *stat, target: target.clone(), value, provincewide }));
126 }
127
128 let mut unit_kinds = HashMap::new();
129 for table in ["land_units", "naval_units"] {
130 for row in tables.table(table)?.rows() {
131 unit_kinds.insert(row.str("key")?.into_owned(), (row.str("category")?.into_owned(), row.str("class")?.into_owned()));
132 }
133 }
134 let mut kinds = HashMap::new();
135 for row in tables.table("main_units")?.rows() {
136 let column = if row.bool("is_naval")? { "naval_unit" } else { "land_unit" };
137 if let Some(kind) = unit_kinds.get(row.str(column)?.as_ref()) {
138 kinds.insert(row.str("unit")?.into_owned(), kind.clone());
139 }
140 }
141
142 Ok(Self { effects, kinds })
143 }
144
145 pub fn bonuses<'b>(&self, unit: &str, buildings: impl IntoIterator<Item = (&'b str, bool)>) -> Vec<(u32, i64)> {
157 let Some((category, class)) = self.kinds.get(unit) else { return vec![] };
158 let mut sums = HashMap::<(u32, u8), i64>::new();
159 for (building, own_region) in buildings {
160 for effect in self.effects.get(building).into_iter().flatten().filter(|effect| effect.provincewide || own_region) {
161 let source = match &effect.target {
162 Target::Basic => 0,
163 Target::Category(target) if target == category => 1,
164 Target::Class(target) if target == class => 2,
165 _ => continue,
166 };
167 *sums.entry((effect.stat, source)).or_default() += effect.value;
168 }
169 }
170
171 (0..STAT_BONUS_IDS.len() as u32)
172 .filter_map(|stat| (0..3).filter_map(|source| sums.get(&(stat, source)).copied()).max().filter(|value| *value > 0).map(|value| (stat, value)))
173 .collect()
174 }
175}