use std::collections::BTreeSet;
use esf_data::eve;
use super::attribute_ids::{
ATTRIBUTE_CAPACITOR_NEED_ID, ATTRIBUTE_REMOTE_RESISTANCE_ID, ATTRIBUTE_SKILLS,
};
use super::item::{
Attribute, Effect, EffectCategory, EffectOperator, Item, ItemState, Object, Origin,
};
use super::{Info, Objects, Pass};
use crate::projection::ProjectedBuff;
const EXEMPT_PENALTY_CATEGORY_IDS: [i32; 6] = [6, 8, 16, 20, 32, 65];
pub struct PassTwo {}
#[allow(clippy::enum_variant_names)]
#[derive(Debug)]
enum Modifier {
LocationRequiredSkillModifier(i32),
LocationGroupModifier(i32),
LocationModifier(),
OwnerRequiredSkillModifier(i32),
ItemModifier(),
}
#[derive(Debug)]
struct Pass2Effect {
origin: Origin,
modifier: Modifier,
operator: EffectOperator,
source_quantity: u32,
target: Object,
target_attribute_id: i32,
resistance: Option<i32>,
}
fn get_modifier_func(
func: eve::ModifierFunc,
skill_type_id: i32,
group_id: i32,
) -> Option<Modifier> {
match func {
eve::ModifierFunc::LocationRequiredSkillModifier => {
Some(Modifier::LocationRequiredSkillModifier(skill_type_id))
}
eve::ModifierFunc::LocationGroupModifier => Some(Modifier::LocationGroupModifier(group_id)),
eve::ModifierFunc::LocationModifier => Some(Modifier::LocationModifier()),
eve::ModifierFunc::ItemModifier => Some(Modifier::ItemModifier()),
eve::ModifierFunc::OwnerRequiredSkillModifier => {
Some(Modifier::OwnerRequiredSkillModifier(skill_type_id))
}
eve::ModifierFunc::EffectStopper => None,
func => panic!("Unknown modifier func: {:?}", func),
}
}
const STRUCTURE_CATEGORY_ID: i32 = 65;
fn get_target_object(domain: eve::ModifierDomain, origin: Object) -> Option<Object> {
match domain {
eve::ModifierDomain::ShipID => Some(Object::Ship),
eve::ModifierDomain::CharID => Some(Object::Char),
eve::ModifierDomain::OtherID => match origin {
Object::Item(index) => Some(Object::Charge(index)),
Object::Charge(index) => Some(Object::Item(index)),
_ => panic!("Invalid origin for OtherID domain"),
},
eve::ModifierDomain::StructureID => Some(Object::Ship),
eve::ModifierDomain::ItemID => Some(origin),
eve::ModifierDomain::TargetID | eve::ModifierDomain::Target => None,
domain => panic!("Unknown modifier domain: {:?}", domain),
}
}
fn get_projected_object(domain: eve::ModifierDomain) -> Option<Object> {
match domain {
eve::ModifierDomain::ShipID
| eve::ModifierDomain::StructureID
| eve::ModifierDomain::TargetID
| eve::ModifierDomain::Target => Some(Object::Ship),
eve::ModifierDomain::CharID => Some(Object::Char),
eve::ModifierDomain::ItemID | eve::ModifierDomain::OtherID => None,
domain => panic!("Unknown modifier domain: {:?}", domain),
}
}
fn collect_projected_effects(
info: &impl Info,
item: &Item,
origin: Object,
effect_id: i32,
effects: &mut Vec<Pass2Effect>,
) {
let Some(effect) = info.get_dogma_effect(effect_id) else {
return;
};
let category = get_effect_category(effect.effect_category());
let resistance = get_resistance(item, &effect);
for modifier in effect.modifiers().into_iter().flatten() {
let Some(target) = get_projected_object(modifier.domain()) else {
continue;
};
let modifier_func = get_modifier_func(
modifier.func(),
modifier.skill_type_id(),
modifier.group_id(),
);
let Some(effect_modifier) = modifier_func else {
continue;
};
let Some(operator) = get_effect_operator(modifier.operation()) else {
continue;
};
effects.push(Pass2Effect {
origin: Origin::Effect {
effect_id,
source: origin,
source_category: category,
attribute_id: modifier.modifying_attribute_id(),
},
modifier: effect_modifier,
operator,
source_quantity: 1,
target,
target_attribute_id: modifier.modified_attribute_id(),
resistance,
});
}
}
fn get_resistance(item: &Item, effect: &eve::DogmaEffect) -> Option<i32> {
let named = item
.attributes
.get(&ATTRIBUTE_REMOTE_RESISTANCE_ID)
.map_or(0, |attribute| attribute.base_value as i32);
match (named, effect.resistance_attribute_id()) {
(0, 0) => None,
(0, from_effect) => Some(from_effect),
(named, _) => Some(named),
}
}
fn for_each_in_location(
objects: &mut Objects,
location: Object,
mut apply: impl FnMut(Object, &mut Item),
) {
match location {
Object::Ship => {
apply(Object::Ship, &mut objects.ship);
for (index, item) in objects.items.iter_mut().enumerate() {
if !item.is_in_ship() {
continue;
}
apply(Object::Item(index), item);
if let Some(charge) = &mut item.charge {
apply(Object::Charge(index), charge);
}
}
}
Object::Char => {
apply(Object::Char, &mut objects.char);
for (index, skill) in objects.skills.iter_mut().enumerate() {
apply(Object::Skill(index), skill);
}
for (index, item) in objects.items.iter_mut().enumerate() {
if item.is_on_char() {
apply(Object::Item(index), item);
}
}
}
Object::Mode
| Object::Item(_)
| Object::Charge(_)
| Object::Skill(_)
| Object::Projected(_) => apply(location, objects.get_mut(location).unwrap()),
}
}
pub(super) fn get_effect_category(category: eve::EffectCategory) -> EffectCategory {
match category {
eve::EffectCategory::Passive => EffectCategory::Passive,
eve::EffectCategory::Active => EffectCategory::Active,
eve::EffectCategory::Target => EffectCategory::Target,
eve::EffectCategory::Area => EffectCategory::Area,
eve::EffectCategory::Online => EffectCategory::Online,
eve::EffectCategory::Overload => EffectCategory::Overload,
eve::EffectCategory::Dungeon => EffectCategory::Dungeon,
eve::EffectCategory::System => EffectCategory::System,
category => panic!("Unknown effect category: {:?}", category),
}
}
fn get_effect_operator(operation: eve::ModifierOperation) -> Option<EffectOperator> {
match operation {
eve::ModifierOperation::PreAssign => Some(EffectOperator::PreAssign),
eve::ModifierOperation::PreMul => Some(EffectOperator::PreMul),
eve::ModifierOperation::PreDiv => Some(EffectOperator::PreDiv),
eve::ModifierOperation::ModAdd => Some(EffectOperator::ModAdd),
eve::ModifierOperation::ModSub => Some(EffectOperator::ModSub),
eve::ModifierOperation::PostMul => Some(EffectOperator::PostMul),
eve::ModifierOperation::PostDiv => Some(EffectOperator::PostDiv),
eve::ModifierOperation::PostPercent => Some(EffectOperator::PostPercent),
eve::ModifierOperation::PostAssign => Some(EffectOperator::PostAssign),
eve::ModifierOperation(9) => None,
eve::ModifierOperation::Unset => panic!("Effect operation is not set"),
operation => panic!("Unknown effect operation: {:?}", operation),
}
}
fn collect_buff_effects(info: &impl Info, buffs: &[ProjectedBuff], effects: &mut Vec<Pass2Effect>) {
for buff in buffs {
let Some(collection) = info.get_dbuff_collection(buff.id) else {
continue;
};
let Some(operator) = get_effect_operator(collection.operation()) else {
continue;
};
for modifier in collection.modifiers().into_iter().flatten() {
let effect_modifier = get_modifier_func(
modifier.func(),
modifier.skill_type_id(),
modifier.group_id(),
);
let Some(effect_modifier) = effect_modifier else {
continue;
};
effects.push(Pass2Effect {
origin: Origin::Buff {
buff_id: buff.id,
value: buff.value,
},
modifier: effect_modifier,
operator,
source_quantity: 1,
target: Object::Ship,
target_attribute_id: modifier.modified_attribute_id(),
resistance: None,
});
}
}
}
impl Item {
fn required_skills(&self) -> BTreeSet<i32> {
ATTRIBUTE_SKILLS
.iter()
.filter_map(|attribute_skill_id| self.attributes.get(attribute_skill_id))
.map(|attribute| attribute.base_value as i32)
.collect()
}
fn add_effect(
&mut self,
info: &impl Info,
target: Object,
attribute_id: i32,
source_category_id: i32,
effect: &Pass2Effect,
) {
let attr = info.get_dogma_attribute(attribute_id);
let stackable = attr.is_some_and(|attr| attr.stackable());
let penalty = !stackable && !EXEMPT_PENALTY_CATEGORY_IDS.contains(&source_category_id);
let attribute = self.attributes.entry(attribute_id).or_insert_with(|| {
Attribute::new(attr.map_or(0.0, |attr| attr.default_value() as f64))
});
attribute.effects.push(Effect {
origin: effect.origin,
operator: effect.operator,
penalty,
quantity: match effect.origin {
Origin::Effect { source, .. } if source == target => 1,
_ => effect.source_quantity,
},
resistance: effect.resistance,
});
}
fn collect_effects(
&mut self,
info: &impl Info,
origin: Object,
skip_ship_domain: bool,
effects: &mut Vec<Pass2Effect>,
) {
let own_effects = info.get_dogma_effects(self.type_id);
let base_effects = self
.mutation_base
.and_then(|base| info.get_dogma_effects(base))
.into_iter()
.flatten()
.filter(|base_effect| {
!own_effects.is_some_and(|own_effects| {
own_effects
.iter()
.any(|effect| effect.effect_id() == base_effect.effect_id())
})
});
for dogma_effect in own_effects.into_iter().flatten().chain(base_effects) {
let Some(type_dogma_effect) = info.get_dogma_effect(dogma_effect.effect_id()) else {
continue;
};
let category = get_effect_category(type_dogma_effect.effect_category());
if let Some(state) = category.required_state() {
self.max_state = self.max_state.max(state);
}
if self.is_fighter() && category != EffectCategory::Passive {
let used = match &self.fighter_abilities {
Some(abilities) => abilities.contains(&dogma_effect.effect_id()),
None => dogma_effect.is_default(),
};
if !used {
continue;
}
}
if type_dogma_effect.fitting_usage_chance_attribute_id() != 0
&& !self
.booster_side_effects
.contains(&dogma_effect.effect_id())
{
continue;
}
let modifiers = type_dogma_effect
.modifiers()
.filter(|modifiers| !modifiers.is_empty());
let Some(modifiers) = modifiers else {
self.effects.push(dogma_effect.effect_id());
continue;
};
for modifier in modifiers {
let effect_modifier = get_modifier_func(
modifier.func(),
modifier.skill_type_id(),
modifier.group_id(),
);
let Some(effect_modifier) = effect_modifier else {
continue;
};
let Some(operator) = get_effect_operator(modifier.operation()) else {
continue;
};
if matches!(origin, Object::Item(_))
&& modifier.domain() == eve::ModifierDomain::OtherID
&& self.charge.is_none()
{
continue;
}
if skip_ship_domain && modifier.domain() == eve::ModifierDomain::ShipID {
continue;
}
let Some(target) = get_target_object(modifier.domain(), origin) else {
continue;
};
effects.push(Pass2Effect {
origin: Origin::Effect {
effect_id: dogma_effect.effect_id(),
source: origin,
source_category: category,
attribute_id: modifier.modifying_attribute_id(),
},
modifier: effect_modifier,
operator,
source_quantity: self.quantity,
target,
target_attribute_id: modifier.modified_attribute_id(),
resistance: None,
});
}
}
if self.attributes.contains_key(&ATTRIBUTE_CAPACITOR_NEED_ID)
&& self.max_state < ItemState::Active
{
self.max_state = ItemState::Active;
}
if self.state > self.max_state {
self.state = self.max_state;
}
}
}
impl Pass for PassTwo {
fn pass(info: &impl Info, objects: &mut Objects) {
let mut effects = Vec::new();
objects
.ship
.collect_effects(info, Object::Ship, false, &mut effects);
if let Some(mode) = &mut objects.mode {
mode.collect_effects(info, Object::Mode, false, &mut effects);
}
objects
.char
.collect_effects(info, Object::Char, false, &mut effects);
for index in 0..objects.projected.len() {
let projected = &objects.projected[index];
collect_projected_effects(
info,
&projected.item,
Object::Projected(index),
projected.effect_id,
&mut effects,
);
}
collect_buff_effects(info, &objects.buffs, &mut effects);
let structure_fit = objects.ship.category_id == STRUCTURE_CATEGORY_ID;
for (index, item) in objects.items.iter_mut().enumerate() {
if structure_fit && item.is_on_char() {
item.state = ItemState::Passive;
continue;
}
if !item.is_calculated() {
continue;
}
item.collect_effects(info, Object::Item(index), false, &mut effects);
if let Some(charge) = &mut item.charge {
charge.collect_effects(info, Object::Charge(index), false, &mut effects);
}
}
for (index, skill) in objects.skills.iter_mut().enumerate() {
skill.collect_effects(info, Object::Skill(index), structure_fit, &mut effects);
}
let ship_skills = objects.ship.required_skills();
let item_skills: Vec<_> = objects
.items
.iter()
.map(|item| {
let charge_skills = item.charge.as_ref().map(|charge| charge.required_skills());
(item.required_skills(), charge_skills.unwrap_or_default())
})
.collect();
for effect in effects {
let (source_type_id, category_id) = match effect.origin {
Origin::Effect { source, .. } => {
let source = objects.get(source).unwrap();
(source.type_id, source.category_id)
}
Origin::Buff { .. } => (0, 0),
};
match effect.modifier {
Modifier::ItemModifier() => {
let target = objects.get_mut(effect.target).unwrap();
target.add_effect(
info,
effect.target,
effect.target_attribute_id,
category_id,
&effect,
);
}
Modifier::LocationModifier() => {
for_each_in_location(objects, effect.target, |target, item| {
item.add_effect(
info,
target,
effect.target_attribute_id,
category_id,
&effect,
);
});
}
Modifier::LocationGroupModifier(group_id) => {
for_each_in_location(objects, effect.target, |target, item| {
if item.group_id == group_id {
item.add_effect(
info,
target,
effect.target_attribute_id,
category_id,
&effect,
);
}
});
}
Modifier::OwnerRequiredSkillModifier(skill_type_id)
| Modifier::LocationRequiredSkillModifier(skill_type_id) => {
let skill_type_id = if skill_type_id == -1 {
source_type_id
} else {
skill_type_id
};
let location_only =
matches!(effect.modifier, Modifier::LocationRequiredSkillModifier(_));
let char_location = location_only && effect.target == Object::Char;
let in_location = |item: &Item| match char_location {
true => item.is_on_char(),
false => item.is_in_ship(),
};
if !char_location && ship_skills.contains(&skill_type_id) {
objects.ship.add_effect(
info,
Object::Ship,
effect.target_attribute_id,
category_id,
&effect,
);
}
for (index, (item, (skills, charge_skills))) in objects
.items
.iter_mut()
.zip(&item_skills)
.enumerate()
.filter(|(_, (item, _))| !location_only || in_location(item))
{
if skills.contains(&skill_type_id) {
item.add_effect(
info,
Object::Item(index),
effect.target_attribute_id,
category_id,
&effect,
);
}
if let Some(charge) = &mut item.charge
&& charge_skills.contains(&skill_type_id)
{
charge.add_effect(
info,
Object::Charge(index),
effect.target_attribute_id,
category_id,
&effect,
);
}
}
}
}
}
}
}