kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View File

@@ -0,0 +1,741 @@
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