kimi gone wild
This commit is contained in:
271
lib/odinsea/game/skill.ex
Normal file
271
lib/odinsea/game/skill.ex
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
Reference in New Issue
Block a user