Skip to main content

esf_dogma_engine/calculate/
item.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, BTreeSet};
3
4use serde::Serialize;
5use strum_macros::EnumIter;
6
7#[cfg(feature = "typescript")]
8use tsify::Tsify;
9
10use super::output::Source;
11use crate::fit::{FitItem, Slot, State};
12
13#[derive(Debug, Copy, Clone, PartialEq, Eq)]
14pub enum EffectCategory {
15    Passive,
16    Online,
17    Active,
18    Overload,
19    Target,
20    Area,
21    Dungeon,
22    System,
23}
24
25#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub enum ItemState {
27    Passive,
28    Online,
29    Active,
30    Overload,
31    /// Not a state an item is put in; whatever holds it always runs.
32    AlwaysOn,
33}
34
35/* Declaration order is the order pass 3 applies operators in; do not reorder. */
36/// How a modifier changes an attribute. They are applied in this order.
37#[cfg_attr(feature = "typescript", derive(Tsify))]
38#[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, EnumIter)]
39#[serde(rename_all = "snake_case")]
40pub enum EffectOperator {
41    /// Set the value, before anything else.
42    PreAssign,
43    /// Multiply the value.
44    PreMul,
45    /// Divide the value.
46    PreDiv,
47    /// Add to the value.
48    ModAdd,
49    /// Subtract from the value.
50    ModSub,
51    /// Multiply the value, after adding.
52    PostMul,
53    /// Divide the value, after adding.
54    PostDiv,
55    /// Change the value by a percentage.
56    PostPercent,
57    /// Set the value, after everything else.
58    PostAssign,
59}
60
61#[derive(Debug, Copy, Clone, PartialEq)]
62pub enum Object {
63    Ship,
64    Mode,
65    Item(usize),
66    Charge(usize),
67    Skill(usize),
68    Char,
69    Projected(usize),
70}
71
72/// Where a modifier comes from, and where its strength is read.
73#[derive(Debug, Copy, Clone, PartialEq)]
74pub enum Origin {
75    /// A dogma effect on an object, reading its strength off an attribute there.
76    Effect {
77        effect_id: i32,
78        source: Object,
79        source_category: EffectCategory,
80        attribute_id: i32,
81    },
82    /// A buff, which carries its own strength and has already won.
83    Buff { buff_id: i32, value: f64 },
84}
85
86#[derive(Debug)]
87pub struct Effect {
88    pub origin: Origin,
89    pub operator: EffectOperator,
90    pub penalty: bool,
91    pub quantity: u32,
92    pub resistance: Option<i32>,
93}
94
95impl Origin {
96    /// The effect that holds the modifier; `None` for a buff, which has none.
97    pub fn effect_id(self) -> Option<i32> {
98        match self {
99            Origin::Effect { effect_id, .. } => Some(effect_id),
100            Origin::Buff { .. } => None,
101        }
102    }
103
104    /// The attribute the strength is read off; `None` for a buff, which carries it.
105    pub fn source_attribute_id(self) -> Option<i32> {
106        match self {
107            Origin::Effect { attribute_id, .. } => Some(attribute_id),
108            Origin::Buff { .. } => None,
109        }
110    }
111}
112
113#[derive(Debug)]
114pub struct Attribute {
115    pub base_value: f64,
116    pub value: Cell<Option<f64>>,
117    pub effects: Vec<Effect>,
118    /* Only filled when the calculation asks for sources. */
119    pub sources: RefCell<Vec<Source>>,
120}
121
122#[derive(Debug)]
123pub struct Item {
124    pub type_id: i32,
125    pub group_id: i32,
126    pub category_id: i32,
127
128    pub slot: Option<Slot>,
129    pub quantity: u32,
130    pub charge: Option<Box<Item>>,
131    pub state: ItemState,
132    pub max_state: ItemState,
133    pub attributes: BTreeMap<i32, Attribute>,
134    pub effects: Vec<i32>,
135    pub fighter_abilities: Option<BTreeSet<i32>>,
136    pub booster_side_effects: BTreeSet<i32>,
137    pub mutation_base: Option<i32>,
138}
139
140impl Attribute {
141    pub fn new(value: f64) -> Attribute {
142        Attribute {
143            base_value: value,
144            value: Cell::new(None),
145            effects: Vec::new(),
146            sources: RefCell::new(Vec::new()),
147        }
148    }
149}
150
151impl ItemState {
152    pub fn is_active(self) -> bool {
153        self >= ItemState::Active
154    }
155}
156
157impl EffectCategory {
158    pub fn required_state(self) -> Option<ItemState> {
159        match self {
160            EffectCategory::Passive => Some(ItemState::Passive),
161            EffectCategory::Online => Some(ItemState::Online),
162            EffectCategory::Active => Some(ItemState::Active),
163            EffectCategory::Overload => Some(ItemState::Overload),
164            EffectCategory::Target
165            | EffectCategory::Area
166            | EffectCategory::Dungeon
167            | EffectCategory::System => None,
168        }
169    }
170
171    /// Whether an effect of this category runs on an item in `state`.
172    pub fn runs_at(self, state: ItemState) -> bool {
173        match self.required_state() {
174            Some(required) => state >= required,
175            /* Asks for no state, so it runs the moment the item is active. */
176            None => state.is_active(),
177        }
178    }
179}
180
181impl From<State> for ItemState {
182    fn from(state: State) -> ItemState {
183        match state {
184            State::Offline => ItemState::Passive,
185            State::Online => ItemState::Online,
186            State::Active => ItemState::Active,
187            State::Overload => ItemState::Overload,
188        }
189    }
190}
191
192impl From<ItemState> for State {
193    fn from(state: ItemState) -> State {
194        match state {
195            ItemState::Passive => State::Offline,
196            ItemState::Online => State::Online,
197            ItemState::Active => State::Active,
198            ItemState::Overload => State::Overload,
199            /* Only a beacon is always-on, and beacons are not in the result. */
200            ItemState::AlwaysOn => unreachable!("an always-on item has no fit state"),
201        }
202    }
203}
204
205impl Item {
206    pub fn is_module(&self) -> bool {
207        matches!(
208            self.slot,
209            Some(
210                Slot::High(_) | Slot::Medium(_) | Slot::Low(_) | Slot::Rig(_) | Slot::Subsystem(_)
211            )
212        )
213    }
214
215    /* Drones are owned by the character, but not located in the ship. */
216    pub fn is_in_ship(&self) -> bool {
217        self.is_module() || matches!(self.slot, Some(Slot::Service(_)))
218    }
219
220    pub fn is_fighter(&self) -> bool {
221        matches!(self.slot, Some(Slot::FighterTube(_) | Slot::FighterBay))
222    }
223
224    pub fn is_on_char(&self) -> bool {
225        matches!(self.slot, Some(Slot::Implant(_) | Slot::Booster(_)))
226    }
227
228    pub fn is_calculated(&self) -> bool {
229        self.is_in_ship()
230            || self.is_fighter()
231            || self.is_on_char()
232            || self.slot == Some(Slot::DroneBay)
233    }
234
235    pub fn new_charge(type_id: i32) -> Item {
236        Item {
237            type_id,
238            group_id: 0,
239            category_id: 0,
240            slot: None,
241            quantity: 1,
242            charge: None,
243            state: ItemState::Active,
244            max_state: ItemState::Active,
245            attributes: BTreeMap::new(),
246            effects: Vec::new(),
247            fighter_abilities: None,
248            booster_side_effects: BTreeSet::new(),
249            mutation_base: None,
250        }
251    }
252
253    pub fn new_fit(fit_item: &FitItem) -> Item {
254        let mut item = Item {
255            type_id: fit_item.type_id,
256            group_id: 0,
257            category_id: 0,
258            slot: Some(fit_item.slot),
259            quantity: fit_item.quantity,
260            charge: fit_item
261                .charge
262                .as_ref()
263                .map(|charge| Box::new(Item::new_charge(charge.type_id))),
264            state: fit_item.state.into(),
265            max_state: ItemState::Passive,
266            attributes: BTreeMap::new(),
267            effects: Vec::new(),
268            fighter_abilities: fit_item.fighter_abilities.clone(),
269            booster_side_effects: fit_item.booster_side_effects.clone(),
270            mutation_base: fit_item.mutation.as_ref().map(|mutation| mutation.base),
271        };
272
273        match item.slot {
274            Some(Slot::DroneBay | Slot::FighterTube(_)) => {
275                if item.state != ItemState::Passive {
276                    item.state = ItemState::Active;
277                }
278                item.max_state = ItemState::Active;
279            }
280            Some(Slot::FighterBay) => item.state = ItemState::Passive,
281            _ if !item.is_calculated() => item.state = ItemState::Passive,
282            _ => {}
283        }
284
285        item
286    }
287
288    pub fn new_projected(type_id: i32) -> Item {
289        Item {
290            state: ItemState::AlwaysOn,
291            max_state: ItemState::AlwaysOn,
292            ..Item::new_fake(type_id)
293        }
294    }
295
296    pub fn new_fake(type_id: i32) -> Item {
297        Item {
298            type_id,
299            group_id: 0,
300            category_id: 0,
301            slot: None,
302            quantity: 1,
303            charge: None,
304            state: ItemState::Active,
305            max_state: ItemState::Active,
306            attributes: BTreeMap::new(),
307            effects: Vec::new(),
308            fighter_abilities: None,
309            booster_side_effects: BTreeSet::new(),
310            mutation_base: None,
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::EffectOperator;
318    use strum::IntoEnumIterator;
319
320    #[test]
321    fn effect_operator_iterates_in_application_order() {
322        assert_eq!(
323            EffectOperator::iter().collect::<Vec<_>>(),
324            [
325                EffectOperator::PreAssign,
326                EffectOperator::PreMul,
327                EffectOperator::PreDiv,
328                EffectOperator::ModAdd,
329                EffectOperator::ModSub,
330                EffectOperator::PostMul,
331                EffectOperator::PostDiv,
332                EffectOperator::PostPercent,
333                EffectOperator::PostAssign,
334            ]
335        );
336    }
337}