272 lines
8.2 KiB
Elixir
272 lines
8.2 KiB
Elixir
defmodule Odinsea.Game.Skill do
|
|
@moduledoc """
|
|
Skill struct and functions for MapleStory skills.
|
|
|
|
Ported from Java: client/Skill.java
|
|
|
|
Skills are abilities that characters can learn and use. Each skill has:
|
|
- Multiple levels with increasing effects
|
|
- Requirements (job, level, other skills)
|
|
- Effects (buffs, damage, healing, etc.)
|
|
- Animation data
|
|
- Cooldowns and durations
|
|
"""
|
|
|
|
alias Odinsea.Game.StatEffect
|
|
|
|
defstruct [
|
|
:id,
|
|
:name,
|
|
:element,
|
|
:max_level,
|
|
:true_max,
|
|
:master_level,
|
|
:effects,
|
|
:pvp_effects,
|
|
:required_skills,
|
|
:skill_type,
|
|
:animation,
|
|
:animation_time,
|
|
:delay,
|
|
:invisible,
|
|
:time_limited,
|
|
:combat_orders,
|
|
:charge_skill,
|
|
:magic,
|
|
:caster_move,
|
|
:push_target,
|
|
:pull_target,
|
|
:not_removed,
|
|
:pvp_disabled,
|
|
:event_taming_mob
|
|
]
|
|
|
|
@type element :: :neutral | :fire | :ice | :lightning | :poison | :holy | :dark | :physical
|
|
|
|
@type t :: %__MODULE__{
|
|
id: integer(),
|
|
name: String.t(),
|
|
element: element(),
|
|
max_level: integer(),
|
|
true_max: integer(),
|
|
master_level: integer(),
|
|
effects: [StatEffect.t()],
|
|
pvp_effects: [StatEffect.t()] | nil,
|
|
required_skills: [{integer(), integer()}],
|
|
skill_type: integer(),
|
|
animation: [{String.t(), integer()}] | nil,
|
|
animation_time: integer(),
|
|
delay: integer(),
|
|
invisible: boolean(),
|
|
time_limited: boolean(),
|
|
combat_orders: boolean(),
|
|
charge_skill: boolean(),
|
|
magic: boolean(),
|
|
caster_move: boolean(),
|
|
push_target: boolean(),
|
|
pull_target: boolean(),
|
|
not_removed: boolean(),
|
|
pvp_disabled: boolean(),
|
|
event_taming_mob: integer()
|
|
}
|
|
|
|
@doc """
|
|
Creates a new skill with the given ID and default values.
|
|
"""
|
|
@spec new(integer()) :: t()
|
|
def new(id) do
|
|
%__MODULE__{
|
|
id: id,
|
|
name: "",
|
|
element: :neutral,
|
|
max_level: 0,
|
|
true_max: 0,
|
|
master_level: 0,
|
|
effects: [],
|
|
pvp_effects: nil,
|
|
required_skills: [],
|
|
skill_type: 0,
|
|
animation: nil,
|
|
animation_time: 0,
|
|
delay: 0,
|
|
invisible: false,
|
|
time_limited: false,
|
|
combat_orders: false,
|
|
charge_skill: false,
|
|
magic: false,
|
|
caster_move: false,
|
|
push_target: false,
|
|
pull_target: false,
|
|
not_removed: false,
|
|
pvp_disabled: false,
|
|
event_taming_mob: 0
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Gets the effect for a specific skill level.
|
|
Returns the last effect if level exceeds max, or first effect if level <= 0.
|
|
"""
|
|
@spec get_effect(t(), integer()) :: StatEffect.t() | nil
|
|
def get_effect(skill, level) do
|
|
effects = skill.effects
|
|
|
|
cond do
|
|
length(effects) == 0 -> nil
|
|
level <= 0 -> List.first(effects)
|
|
level > length(effects) -> List.last(effects)
|
|
true -> Enum.at(effects, level - 1)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets the PVP effect for a specific skill level.
|
|
Falls back to regular effects if PVP effects not defined.
|
|
"""
|
|
@spec get_pvp_effect(t(), integer()) :: StatEffect.t() | nil
|
|
def get_pvp_effect(skill, level) do
|
|
if skill.pvp_effects do
|
|
cond do
|
|
level <= 0 -> List.first(skill.pvp_effects)
|
|
level > length(skill.pvp_effects) -> List.last(skill.pvp_effects)
|
|
true -> Enum.at(skill.pvp_effects, level - 1)
|
|
end
|
|
else
|
|
get_effect(skill, level)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if this skill can be learned by a specific job.
|
|
"""
|
|
@spec can_be_learned_by?(t(), integer()) :: boolean()
|
|
def can_be_learned_by?(skill, job_id) do
|
|
skill_job = div(skill.id, 10000)
|
|
|
|
# Special job exceptions
|
|
cond do
|
|
# Evan beginner skills
|
|
skill_job == 2001 -> is_evan_job?(job_id)
|
|
# Regular beginner skills (adventurer)
|
|
skill_job == 0 -> is_adventurer_job?(job_id)
|
|
# Cygnus beginner skills
|
|
skill_job == 1000 -> is_cygnus_job?(job_id)
|
|
# Aran beginner skills
|
|
skill_job == 2000 -> is_aran_job?(job_id)
|
|
# Resistance beginner skills
|
|
skill_job == 3000 -> is_resistance_job?(job_id)
|
|
# Cannon shooter beginner
|
|
skill_job == 1 -> is_cannon_job?(job_id)
|
|
# Demon beginner
|
|
skill_job == 3001 -> is_demon_job?(job_id)
|
|
# Mercedes beginner
|
|
skill_job == 2002 -> is_mercedes_job?(job_id)
|
|
# Wrong job category
|
|
div(job_id, 100) != div(skill_job, 100) -> false
|
|
div(job_id, 1000) != div(skill_job, 1000) -> false
|
|
# Class-specific restrictions
|
|
is_cannon_job?(skill_job) and not is_cannon_job?(job_id) -> false
|
|
is_demon_job?(skill_job) and not is_demon_job?(job_id) -> false
|
|
is_adventurer_job?(skill_job) and not is_adventurer_job?(job_id) -> false
|
|
is_cygnus_job?(skill_job) and not is_cygnus_job?(job_id) -> false
|
|
is_aran_job?(skill_job) and not is_aran_job?(job_id) -> false
|
|
is_evan_job?(skill_job) and not is_evan_job?(job_id) -> false
|
|
is_mercedes_job?(skill_job) and not is_mercedes_job?(job_id) -> false
|
|
is_resistance_job?(skill_job) and not is_resistance_job?(job_id) -> false
|
|
# Wrong 2nd job
|
|
rem(div(job_id, 10), 10) == 0 and rem(div(skill_job, 10), 10) > rem(div(job_id, 10), 10) -> false
|
|
rem(div(skill_job, 10), 10) != 0 and rem(div(skill_job, 10), 10) != rem(div(job_id, 10), 10) -> false
|
|
# Wrong 3rd/4th job
|
|
rem(skill_job, 10) > rem(job_id, 10) -> false
|
|
true -> true
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if this is a fourth job skill.
|
|
"""
|
|
@spec is_fourth_job?(t()) :: boolean()
|
|
def is_fourth_job?(skill) do
|
|
job_id = div(skill.id, 10000)
|
|
|
|
cond do
|
|
# All 10 skills for 2312 (Phantom)
|
|
job_id == 2312 -> true
|
|
# Skills with max level <= 15 and no master level
|
|
skill.max_level <= 15 and not skill.invisible and skill.master_level <= 0 -> false
|
|
# Specific exceptions
|
|
skill.id in [3_220_010, 3_120_011, 33_120_010, 32_120_009, 5_321_006, 21_120_011, 22_181_004, 4_340_010] -> false
|
|
# Evan skills
|
|
job_id >= 2212 and job_id < 3000 -> rem(job_id, 10) >= 7
|
|
# Dual Blade skills
|
|
job_id >= 430 and job_id <= 434 -> rem(job_id, 10) == 4 or skill.master_level > 0
|
|
# Standard 4th job detection
|
|
rem(job_id, 10) == 2 and skill.id < 90_000_000 and not is_beginner_skill?(skill) -> true
|
|
true -> false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if this is a beginner skill.
|
|
"""
|
|
@spec is_beginner_skill?(t()) :: boolean()
|
|
def is_beginner_skill?(skill) do
|
|
job_id = div(skill.id, 10000)
|
|
job_id in [0, 1000, 2000, 2001, 2002, 3000, 3001, 1]
|
|
end
|
|
|
|
@doc """
|
|
Checks if skill has required skills that must be learned first.
|
|
"""
|
|
@spec has_required_skill?(t()) :: boolean()
|
|
def has_required_skill?(skill) do
|
|
length(skill.required_skills) > 0
|
|
end
|
|
|
|
@doc """
|
|
Gets the default skill expiration time for time-limited skills.
|
|
Returns -1 for permanent skills, or 30 days from now for time-limited.
|
|
"""
|
|
@spec get_default_expiry(t()) :: integer()
|
|
def get_default_expiry(skill) do
|
|
if skill.time_limited do
|
|
# 30 days in milliseconds
|
|
System.system_time(:millisecond) + 30 * 24 * 60 * 60 * 1000
|
|
else
|
|
-1
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if this is a special skill (GM, admin, etc).
|
|
"""
|
|
@spec is_special_skill?(t()) :: boolean()
|
|
def is_special_skill?(skill) do
|
|
job_id = div(skill.id, 10000)
|
|
job_id in [900, 800, 9000, 9200, 9201, 9202, 9203, 9204]
|
|
end
|
|
|
|
@doc """
|
|
Gets a random animation from the skill's animation list.
|
|
"""
|
|
@spec get_animation(t()) :: integer() | nil
|
|
def get_animation(skill) do
|
|
if skill.animation && length(skill.animation) > 0 do
|
|
{_, delay} = Enum.random(skill.animation)
|
|
delay
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Job type checks
|
|
defp is_evan_job?(job_id), do: div(job_id, 100) == 22 or job_id == 2001
|
|
defp is_adventurer_job?(job_id), do: div(job_id, 1000) == 0 and job_id not in [1]
|
|
defp is_cygnus_job?(job_id), do: div(job_id, 1000) == 1
|
|
defp is_aran_job?(job_id), do: div(job_id, 100) == 21 or job_id == 2000
|
|
defp is_resistance_job?(job_id), do: div(job_id, 1000) == 3
|
|
defp is_cannon_job?(job_id), do: div(job_id, 100) == 53 or job_id == 1
|
|
defp is_demon_job?(job_id), do: div(job_id, 100) == 31 or job_id == 3001
|
|
defp is_mercedes_job?(job_id), do: div(job_id, 100) == 23 or job_id == 2002
|
|
end
|