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 AlwaysOn,
33}
34
35#[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 PreAssign,
43 PreMul,
45 PreDiv,
47 ModAdd,
49 ModSub,
51 PostMul,
53 PostDiv,
55 PostPercent,
57 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#[derive(Debug, Copy, Clone, PartialEq)]
74pub enum Origin {
75 Effect {
77 effect_id: i32,
78 source: Object,
79 source_category: EffectCategory,
80 attribute_id: i32,
81 },
82 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 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 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 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 pub fn runs_at(self, state: ItemState) -> bool {
173 match self.required_state() {
174 Some(required) => state >= required,
175 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 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 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}