defmodule Odinsea.Game.StatEffect do @moduledoc """ StatEffect struct for skill and item effects. Ported from Java: server/MapleStatEffect.java StatEffects define what happens when a skill or item is used: - Stat changes (WATK, WDEF, MATK, MDEF, etc.) - HP/MP changes - Buffs and debuffs - Monster status effects - Cooldowns and durations """ alias Odinsea.Game.MonsterStatus defstruct [ # Basic info :source_id, :level, :is_skill, :duration, :over_time, # HP/MP :hp, :mp, :hp_r, :mp_r, :mhp_r, :mmp_r, # Combat stats :watk, :wdef, :matk, :mdef, :acc, :avoid, :hands, :speed, :jump, :mastery, # Damage modifiers :damage, :pdd_r, :mdd_r, :dam_r, :bd_r, :ignore_mob, :critical_damage_min, :critical_damage_max, :asr_r, :er, # Skill-specific :prop, :mob_count, :attack_count, :bullet_count, :cooldown, :interval, # MP/HP consumption :mp_con, :hp_con, :force_con, :mp_con_reduce, # Movement :move_to, # Morph :morph_id, # Summon :summon_movement_type, # DoT (Damage over Time) :dot, :dot_time, # Special effects :thaw, :self_destruction, :pvp_damage, :inc_pvp_damage, # Independent stats (angel buffs) :indie_pad, :indie_mad, :indie_mhp, :indie_mmp, :indie_speed, :indie_jump, :indie_acc, :indie_eva, :indie_pdd, :indie_mdd, :indie_all_stat, # Base stats :str, :dex, :int, :luk, :str_x, :dex_x, :int_x, :luk_x, # Enhanced stats :ehp, :emp, :ewatk, :ewdef, :emdef, # Misc :pad_x, :mad_x, :meso_r, :exp_r, # Item consumption :item_con, :item_con_no, :bullet_consume, :money_con, # Position/Range :lt, :rb, :range, # Buff stats (map of CharacterTemporaryStat => value) :stat_ups, # Monster status effects :monster_status, # Cure debuffs :cure_debuffs, # Other :expinc, :exp_buff, :itemup, :mesoup, :cashup, :berserk, :berserk2, :booster, :illusion, :life_id, :inflation, :imhp, :immp, :use_level, :char_color, :recipe, :recipe_use_count, :recipe_valid_day, :req_skill_level, :slot_count, :preventslip, :immortal, :type, :bs, :cr, :t, :u, :v, :w, :x, :y, :z, :mob_skill, :mob_skill_level, :familiar_target, :fatigue_change, :available_maps, :reward_meso, :reward_items, :pets_can_consume, :familiars, :random_pickup, :traits, :party_buff ] @type point :: {integer(), integer()} @type t :: %__MODULE__{ source_id: integer(), level: integer(), is_skill: boolean(), duration: integer(), over_time: boolean(), hp: integer(), mp: integer(), hp_r: float(), mp_r: float(), mhp_r: integer(), mmp_r: integer(), watk: integer(), wdef: integer(), matk: integer(), mdef: integer(), acc: integer(), avoid: integer(), hands: integer(), speed: integer(), jump: integer(), mastery: integer(), damage: integer(), pdd_r: integer(), mdd_r: integer(), dam_r: integer(), bd_r: integer(), ignore_mob: integer(), critical_damage_min: integer(), critical_damage_max: integer(), asr_r: integer(), er: integer(), prop: integer(), mob_count: integer(), attack_count: integer(), bullet_count: integer(), cooldown: integer(), interval: integer(), mp_con: integer(), hp_con: integer(), force_con: integer(), mp_con_reduce: integer(), move_to: integer(), morph_id: integer(), summon_movement_type: atom() | nil, dot: integer(), dot_time: integer(), thaw: integer(), self_destruction: integer(), pvp_damage: integer(), inc_pvp_damage: integer(), indie_pad: integer(), indie_mad: integer(), indie_mhp: integer(), indie_mmp: integer(), indie_speed: integer(), indie_jump: integer(), indie_acc: integer(), indie_eva: integer(), indie_pdd: integer(), indie_mdd: integer(), indie_all_stat: integer(), str: integer(), dex: integer(), int: integer(), luk: integer(), str_x: integer(), dex_x: integer(), int_x: integer(), luk_x: integer(), ehp: integer(), emp: integer(), ewatk: integer(), ewdef: integer(), emdef: integer(), pad_x: integer(), mad_x: integer(), meso_r: integer(), exp_r: integer(), item_con: integer(), item_con_no: integer(), bullet_consume: integer(), money_con: integer(), lt: point() | nil, rb: point() | nil, range: integer(), stat_ups: map(), monster_status: map(), cure_debuffs: [atom()], expinc: integer(), exp_buff: integer(), itemup: integer(), mesoup: integer(), cashup: integer(), berserk: integer(), berserk2: integer(), booster: integer(), illusion: integer(), life_id: integer(), inflation: integer(), imhp: integer(), immp: integer(), use_level: integer(), char_color: integer(), recipe: integer(), recipe_use_count: integer(), recipe_valid_day: integer(), req_skill_level: integer(), slot_count: integer(), preventslip: integer(), immortal: integer(), type: integer(), bs: integer(), cr: integer(), t: integer(), u: integer(), v: integer(), w: integer(), x: integer(), y: integer(), z: integer(), mob_skill: integer(), mob_skill_level: integer(), familiar_target: integer(), fatigue_change: integer(), available_maps: [{integer(), integer()}], reward_meso: integer(), reward_items: [{integer(), integer(), integer()}], pets_can_consume: [integer()], familiars: [integer()], random_pickup: [integer()], traits: map(), party_buff: boolean() } @doc """ Creates a new StatEffect with default values. """ @spec new(integer(), integer(), boolean()) :: t() def new(source_id, level, is_skill) do %__MODULE__{ source_id: source_id, level: level, is_skill: is_skill, duration: -1, over_time: false, hp: 0, mp: 0, hp_r: 0.0, mp_r: 0.0, mhp_r: 0, mmp_r: 0, watk: 0, wdef: 0, matk: 0, mdef: 0, acc: 0, avoid: 0, hands: 0, speed: 0, jump: 0, mastery: 0, damage: 100, pdd_r: 0, mdd_r: 0, dam_r: 0, bd_r: 0, ignore_mob: 0, critical_damage_min: 0, critical_damage_max: 0, asr_r: 0, er: 0, prop: 100, mob_count: 1, attack_count: 1, bullet_count: 1, cooldown: 0, interval: 0, mp_con: 0, hp_con: 0, force_con: 0, mp_con_reduce: 0, move_to: -1, morph_id: 0, summon_movement_type: nil, dot: 0, dot_time: 0, thaw: 0, self_destruction: 0, pvp_damage: 0, inc_pvp_damage: 0, indie_pad: 0, indie_mad: 0, indie_mhp: 0, indie_mmp: 0, indie_speed: 0, indie_jump: 0, indie_acc: 0, indie_eva: 0, indie_pdd: 0, indie_mdd: 0, indie_all_stat: 0, str: 0, dex: 0, int: 0, luk: 0, str_x: 0, dex_x: 0, int_x: 0, luk_x: 0, ehp: 0, emp: 0, ewatk: 0, ewdef: 0, emdef: 0, pad_x: 0, mad_x: 0, meso_r: 0, exp_r: 0, item_con: 0, item_con_no: 0, bullet_consume: 0, money_con: 0, lt: nil, rb: nil, range: 0, stat_ups: %{}, monster_status: %{}, cure_debuffs: [], expinc: 0, exp_buff: 0, itemup: 0, mesoup: 0, cashup: 0, berserk: 0, berserk2: 0, booster: 0, illusion: 0, life_id: 0, inflation: 0, imhp: 0, immp: 0, use_level: 0, char_color: 0, recipe: 0, recipe_use_count: 0, recipe_valid_day: 0, req_skill_level: 0, slot_count: 0, preventslip: 0, immortal: 0, type: 0, bs: 0, cr: 0, t: 0, u: 0, v: 0, w: 0, x: 0, y: 0, z: 0, mob_skill: 0, mob_skill_level: 0, familiar_target: 0, fatigue_change: 0, available_maps: [], reward_meso: 0, reward_items: [], pets_can_consume: [], familiars: [], random_pickup: [], traits: %{}, party_buff: true } end @doc """ Checks if this effect has a cooldown. """ @spec has_cooldown?(t()) :: boolean() def has_cooldown?(effect) do effect.cooldown > 0 end @doc """ Checks if this is a heal effect. """ @spec is_heal?(t()) :: boolean() def is_heal?(effect) do effect.source_id in [2_301_002, 9_101_002, 9_101_004] end @doc """ Checks if this is a resurrection effect. """ @spec is_resurrection?(t()) :: boolean() def is_resurrection?(effect) do effect.source_id == 2_321_006 end @doc """ Checks if this is a dispel effect. """ @spec is_dispel?(t()) :: boolean() def is_dispel?(effect) do effect.source_id == 2_311_001 end @doc """ Checks if this is a hero's will effect. """ @spec is_hero_will?(t()) :: boolean() def is_hero_will?(effect) do effect.source_id in [1_121_004, 1_221_004, 1_321_004, 2_122_004, 2_222_004, 2_322_004, 3_122_004, 4_122_004, 4_222_004, 5_122_004, 5_222_004, 2_217_004, 4_341_000, 3_221_007, 3_321_007] end @doc """ Checks if this is a time leap effect. """ @spec is_time_leap?(t()) :: boolean() def is_time_leap?(effect) do effect.source_id == 5_121_010 end @doc """ Checks if this is a mist effect. """ @spec is_mist?(t()) :: boolean() def is_mist?(effect) do effect.source_id in [2_111_003, 2_211_003, 1_211_005] end @doc """ Checks if this is a magic door effect. """ @spec is_magic_door?(t()) :: boolean() def is_magic_door?(effect) do effect.source_id == 2_311_002 end @doc """ Checks if this is a poison effect. """ @spec is_poison?(t()) :: boolean() def is_poison?(effect) do effect.dot > 0 and effect.dot_time > 0 end @doc """ Checks if this is a morph effect. """ @spec is_morph?(t()) :: boolean() def is_morph?(effect) do effect.morph_id > 0 end @doc """ Checks if this is a final attack effect. """ @spec is_final_attack?(t()) :: boolean() def is_final_attack?(effect) do effect.source_id in [1_100_002, 1_200_002, 1_300_002, 3_100_001, 3_200_001, 1_110_002, 1_310_002, 2_111_007, 2_221_007, 2_311_007, 3_211_010, 3_310_009, 2_215_004, 2_218_004, 1_120_013, 3_120_008, 2_310_006, 2_312_012] end @doc """ Checks if this is an energy charge effect. """ @spec is_energy_charge?(t()) :: boolean() def is_energy_charge?(effect) do effect.source_id in [5_110_001, 1_510_004] end @doc """ Checks if this effect makes the player invisible. """ @spec is_hide?(t()) :: boolean() def is_hide?(effect) do effect.source_id in [9_101_004, 9_001_004, 4_330_001] end @doc """ Checks if this is a shadow partner effect. """ @spec is_shadow_partner?(t()) :: boolean() def is_shadow_partner?(effect) do effect.source_id in [4_111_002, 1_411_000, 4_331_002, 4_211_008] end @doc """ Checks if this is a combo recharge effect. """ @spec is_combo_recharge?(t()) :: boolean() def is_combo_recharge?(effect) do effect.source_id == 2_111_009 end @doc """ Checks if this is a spirit claw effect. """ @spec is_spirit_claw?(t()) :: boolean() def is_spirit_claw?(effect) do effect.source_id == 4_121_006 end @doc """ Checks if this is a Mech door effect. """ @spec is_mech_door?(t()) :: boolean() def is_mech_door?(effect) do effect.source_id == 3_511_005 end @doc """ Checks if this is a mist eruption effect. """ @spec is_mist_eruption?(t()) :: boolean() def is_mist_eruption?(effect) do effect.source_id == 2_121_005 end @doc """ Checks if this effect affects monsters. """ @spec is_monster_buff?(t()) :: boolean() def is_monster_buff?(effect) do count = stat_size(effect.monster_status) count > 0 end @doc """ Checks if this is a party buff. """ @spec is_party_buff?(t()) :: boolean() def is_party_buff?(effect) do effect.party_buff end @doc """ Calculates the bounding box for this effect based on position. """ @spec calculate_bounding_box(t(), {integer(), integer()}, boolean()) :: {{integer(), integer()}, {integer(), integer()}} | nil def calculate_bounding_box(effect, {x, y}, facing_left) do case {effect.lt, effect.rb} do {nil, nil} -> # Default bounding box width = 200 + effect.range height = 100 + effect.range if facing_left do {{x - width, y - div(height, 2)}, {x, y + div(height, 2)}} else {{x, y - div(height, 2)}, {x + width, y + div(height, 2)}} end {{lt_x, lt_y}, {rb_x, rb_y}} -> if facing_left do {{x + lt_x - effect.range, y + lt_y}, {x + rb_x, y + rb_y}} else {{x - rb_x + effect.range, y + lt_y}, {x - lt_x, y + rb_y}} end _ -> nil end end @doc """ Makes a chance result check based on the effect's prop value. """ @spec make_chance_result?(t()) :: boolean() def make_chance_result?(effect) do effect.prop >= 100 or :rand.uniform(100) < effect.prop end @doc """ Gets the summon movement type if this effect summons something. """ @spec get_summon_movement_type(t()) :: atom() | nil def get_summon_movement_type(effect) do effect.summon_movement_type end @doc """ Gets the total stat change for a specific stat. """ @spec get_stat_change(t(), atom()) :: integer() def get_stat_change(effect, stat) do case stat do :str -> effect.str :dex -> effect.dex :int -> effect.int :luk -> effect.luk :max_hp -> effect.mhp_r :max_mp -> effect.mmp_r :watk -> effect.watk :wdef -> effect.wdef :matk -> effect.matk :mdef -> effect.mdef :acc -> effect.acc :avoid -> effect.avoid :speed -> effect.speed :jump -> effect.jump _ -> 0 end end @doc """ Applies this effect to HP calculation. Returns the HP change (can be negative). """ @spec calc_hp_change(t(), integer(), boolean()) :: integer() def calc_hp_change(effect, max_hp, _primary) do hp_change = effect.hp # Apply HP% recovery/consumption hp_change = hp_change + trunc(max_hp * effect.hp_r) # Cap recovery to max HP min(hp_change, max_hp) end @doc """ Applies this effect to MP calculation. Returns the MP change (can be negative). """ @spec calc_mp_change(t(), integer(), boolean()) :: integer() def calc_mp_change(effect, max_mp, _primary) do mp_change = effect.mp # Apply MP% recovery/consumption mp_change = mp_change + trunc(max_mp * effect.mp_r) # Cap recovery to max MP min(mp_change, max_mp) end # Helper for map size defp stat_size(nil), do: 0 defp stat_size(map) when is_map(map), do: stat_size(Map.keys(map)) defp stat_size(list) when is_list(list), do: length(list) end