742 lines
16 KiB
Elixir
742 lines
16 KiB
Elixir
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
|