676 lines
20 KiB
Elixir
676 lines
20 KiB
Elixir
defmodule Odinsea.Game.SkillFactory do
|
|
@moduledoc """
|
|
Skill Factory - loads and caches skill data.
|
|
|
|
Ported from Java: client/SkillFactory.java
|
|
|
|
This module loads skill metadata from cached JSON files.
|
|
The JSON files should be exported from the Java server's WZ data providers.
|
|
|
|
Skill data is cached in ETS for fast lookups.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
alias Odinsea.Game.{Skill, StatEffect}
|
|
|
|
# ETS table names
|
|
@skill_cache :odinsea_skill_cache
|
|
@skill_names :odinsea_skill_names
|
|
@skills_by_job :odinsea_skills_by_job
|
|
@summon_skills :odinsea_summon_skills
|
|
|
|
# Data file paths (relative to priv directory)
|
|
@skill_data_file "data/skills.json"
|
|
@skill_strings_file "data/skill_strings.json"
|
|
|
|
defmodule SummonSkillEntry do
|
|
@moduledoc "Summon skill attack data"
|
|
|
|
@type t :: %__MODULE__{
|
|
skill_id: integer(),
|
|
type: integer(),
|
|
mob_count: integer(),
|
|
attack_count: integer(),
|
|
lt: {integer(), integer()},
|
|
rb: {integer(), integer()},
|
|
delay: integer()
|
|
}
|
|
|
|
defstruct [
|
|
:skill_id,
|
|
:type,
|
|
:mob_count,
|
|
:attack_count,
|
|
:lt,
|
|
:rb,
|
|
:delay
|
|
]
|
|
end
|
|
|
|
## Public API
|
|
|
|
@doc "Starts the SkillFactory GenServer"
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Gets a skill by ID.
|
|
Returns nil if not found.
|
|
"""
|
|
@spec get_skill(integer()) :: Skill.t() | nil
|
|
def get_skill(skill_id) do
|
|
case :ets.lookup(@skill_cache, skill_id) do
|
|
[{^skill_id, skill}] -> skill
|
|
[] -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets skill name by ID.
|
|
"""
|
|
@spec get_skill_name(integer()) :: String.t()
|
|
def get_skill_name(skill_id) do
|
|
case :ets.lookup(@skill_names, skill_id) do
|
|
[{^skill_id, name}] -> name
|
|
[] -> "UNKNOWN"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets all skills for a specific job.
|
|
"""
|
|
@spec get_skills_by_job(integer()) :: [integer()]
|
|
def get_skills_by_job(job_id) do
|
|
case :ets.lookup(@skills_by_job, job_id) do
|
|
[{^job_id, skills}] -> skills
|
|
[] -> []
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets summon skill entry for a skill ID.
|
|
"""
|
|
@spec get_summon_data(integer()) :: SummonSkillEntry.t() | nil
|
|
def get_summon_data(skill_id) do
|
|
case :ets.lookup(@summon_skills, skill_id) do
|
|
[{^skill_id, entry}] -> entry
|
|
[] -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if a skill exists.
|
|
"""
|
|
@spec skill_exists?(integer()) :: boolean()
|
|
def skill_exists?(skill_id) do
|
|
:ets.member(@skill_cache, skill_id)
|
|
end
|
|
|
|
@doc """
|
|
Gets all loaded skill IDs.
|
|
"""
|
|
@spec get_all_skill_ids() :: [integer()]
|
|
def get_all_skill_ids do
|
|
:ets.select(@skill_cache, [{{:"$1", :_}, [], [:"$1"]}])
|
|
end
|
|
|
|
@doc """
|
|
Gets skill effect for a specific level.
|
|
Convenience function that combines get_skill and Skill.get_effect.
|
|
"""
|
|
@spec get_effect(integer(), integer()) :: StatEffect.t() | nil
|
|
def get_effect(skill_id, level) do
|
|
case get_skill(skill_id) do
|
|
nil -> nil
|
|
skill -> Skill.get_effect(skill, level)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if a skill is a beginner skill.
|
|
"""
|
|
@spec is_beginner_skill?(integer()) :: boolean()
|
|
def is_beginner_skill?(skill_id) do
|
|
job_id = div(skill_id, 10000)
|
|
job_id in [0, 1000, 2000, 2001, 2002, 3000, 3001, 1]
|
|
end
|
|
|
|
@doc """
|
|
Gets the job ID for a skill.
|
|
"""
|
|
@spec get_skill_job(integer()) :: integer()
|
|
def get_skill_job(skill_id) do
|
|
div(skill_id, 10000)
|
|
end
|
|
|
|
@doc """
|
|
Reloads skill data from files.
|
|
"""
|
|
def reload do
|
|
GenServer.call(__MODULE__, :reload, :infinity)
|
|
end
|
|
|
|
## GenServer Callbacks
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Create ETS tables
|
|
:ets.new(@skill_cache, [:set, :public, :named_table, read_concurrency: true])
|
|
:ets.new(@skill_names, [:set, :public, :named_table, read_concurrency: true])
|
|
:ets.new(@skills_by_job, [:set, :public, :named_table, read_concurrency: true])
|
|
:ets.new(@summon_skills, [:set, :public, :named_table, read_concurrency: true])
|
|
|
|
# Load data
|
|
load_skill_data()
|
|
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:reload, _from, state) do
|
|
Logger.info("Reloading skill data...")
|
|
load_skill_data()
|
|
{:reply, :ok, state}
|
|
end
|
|
|
|
## Private Functions
|
|
|
|
defp load_skill_data do
|
|
priv_dir = :code.priv_dir(:odinsea) |> to_string()
|
|
|
|
# Try to load from JSON files
|
|
load_skill_strings(Path.join(priv_dir, @skill_strings_file))
|
|
load_skills(Path.join(priv_dir, @skill_data_file))
|
|
|
|
skill_count = :ets.info(@skill_cache, :size)
|
|
Logger.info("Loaded #{skill_count} skills")
|
|
end
|
|
|
|
defp load_skill_strings(file_path) do
|
|
case File.read(file_path) do
|
|
{:ok, content} ->
|
|
case Jason.decode(content) do
|
|
{:ok, data} when is_map(data) ->
|
|
Enum.each(data, fn {id_str, name} ->
|
|
case Integer.parse(id_str) do
|
|
{skill_id, ""} -> :ets.insert(@skill_names, {skill_id, name})
|
|
_ -> :ok
|
|
end
|
|
end)
|
|
|
|
{:error, reason} ->
|
|
Logger.warn("Failed to parse skill strings JSON: #{inspect(reason)}")
|
|
create_fallback_strings()
|
|
end
|
|
|
|
{:error, :enoent} ->
|
|
Logger.warn("Skill strings file not found: #{file_path}, using fallback data")
|
|
create_fallback_strings()
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to read skill strings: #{inspect(reason)}")
|
|
create_fallback_strings()
|
|
end
|
|
end
|
|
|
|
defp load_skills(file_path) do
|
|
case File.read(file_path) do
|
|
{:ok, content} ->
|
|
case Jason.decode(content, keys: :atoms) do
|
|
{:ok, skills} when is_list(skills) ->
|
|
Enum.each(skills, fn skill_data ->
|
|
skill = build_skill(skill_data)
|
|
:ets.insert(@skill_cache, {skill.id, skill})
|
|
|
|
# Index by job
|
|
job_id = div(skill.id, 10000)
|
|
|
|
existing =
|
|
case :ets.lookup(@skills_by_job, job_id) do
|
|
[{^job_id, list}] -> list
|
|
[] -> []
|
|
end
|
|
|
|
:ets.insert(@skills_by_job, {job_id, [skill.id | existing]})
|
|
|
|
# Check for summon data
|
|
if skill_data[:summon] do
|
|
entry = build_summon_entry(skill.id, skill_data[:summon])
|
|
:ets.insert(@summon_skills, {skill.id, entry})
|
|
end
|
|
end)
|
|
|
|
{:error, reason} ->
|
|
Logger.warn("Failed to parse skills JSON: #{inspect(reason)}")
|
|
create_fallback_skills()
|
|
end
|
|
|
|
{:error, :enoent} ->
|
|
Logger.warn("Skills file not found: #{file_path}, using fallback data")
|
|
create_fallback_skills()
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to read skills: #{inspect(reason)}")
|
|
create_fallback_skills()
|
|
end
|
|
end
|
|
|
|
defp build_skill(data) do
|
|
effects =
|
|
(data[:effects] || [])
|
|
|> Enum.map(&build_stat_effect/1)
|
|
|
|
pvp_effects =
|
|
if data[:pvp_effects] do
|
|
Enum.map(data[:pvp_effects], &build_stat_effect/1)
|
|
else
|
|
nil
|
|
end
|
|
|
|
%Skill{
|
|
id: data[:id] || data[:skill_id] || 0,
|
|
name: data[:name] || "",
|
|
element: parse_element(data[:element]),
|
|
max_level: data[:max_level] || 0,
|
|
true_max: data[:true_max] || data[:max_level] || 0,
|
|
master_level: data[:master_level] || 0,
|
|
effects: effects,
|
|
pvp_effects: pvp_effects,
|
|
required_skills: data[:required_skills] || [],
|
|
skill_type: data[:skill_type] || 0,
|
|
animation: data[:animation],
|
|
animation_time: data[:animation_time] || 0,
|
|
delay: data[:delay] || 0,
|
|
invisible: data[:invisible] || false,
|
|
time_limited: data[:time_limited] || false,
|
|
combat_orders: data[:combat_orders] || false,
|
|
charge_skill: data[:charge_skill] || false,
|
|
magic: data[:magic] || false,
|
|
caster_move: data[:caster_move] || false,
|
|
push_target: data[:push_target] || false,
|
|
pull_target: data[:pull_target] || false,
|
|
not_removed: data[:not_removed] || false,
|
|
pvp_disabled: data[:pvp_disabled] || false,
|
|
event_taming_mob: data[:event_taming_mob] || 0
|
|
}
|
|
end
|
|
|
|
defp build_stat_effect(data) do
|
|
%StatEffect{
|
|
source_id: data[:source_id] || 0,
|
|
level: data[:level] || 1,
|
|
is_skill: data[:is_skill] || true,
|
|
duration: data[:duration] || -1,
|
|
over_time: data[:over_time] || false,
|
|
hp: data[:hp] || 0,
|
|
mp: data[:mp] || 0,
|
|
hp_r: data[:hp_r] || 0.0,
|
|
mp_r: data[:mp_r] || 0.0,
|
|
mhp_r: data[:mhp_r] || 0,
|
|
mmp_r: data[:mmp_r] || 0,
|
|
watk: data[:watk] || data[:pad] || 0,
|
|
wdef: data[:wdef] || data[:pdd] || 0,
|
|
matk: data[:matk] || data[:mad] || 0,
|
|
mdef: data[:mdef] || data[:mdd] || 0,
|
|
acc: data[:acc] || 0,
|
|
avoid: data[:avoid] || data[:eva] || 0,
|
|
hands: data[:hands] || 0,
|
|
speed: data[:speed] || 0,
|
|
jump: data[:jump] || 0,
|
|
mastery: data[:mastery] || 0,
|
|
damage: data[:damage] || 100,
|
|
pdd_r: data[:pdd_r] || 0,
|
|
mdd_r: data[:mdd_r] || 0,
|
|
dam_r: data[:dam_r] || 0,
|
|
bd_r: data[:bd_r] || 0,
|
|
ignore_mob: data[:ignore_mob] || 0,
|
|
critical_damage_min: data[:critical_damage_min] || 0,
|
|
critical_damage_max: data[:critical_damage_max] || 0,
|
|
asr_r: data[:asr_r] || 0,
|
|
er: data[:er] || 0,
|
|
prop: data[:prop] || 100,
|
|
mob_count: data[:mob_count] || 1,
|
|
attack_count: data[:attack_count] || 1,
|
|
bullet_count: data[:bullet_count] || 1,
|
|
cooldown: data[:cooldown] || data[:cooltime] || 0,
|
|
interval: data[:interval] || 0,
|
|
mp_con: data[:mp_con] || 0,
|
|
hp_con: data[:hp_con] || 0,
|
|
force_con: data[:force_con] || 0,
|
|
mp_con_reduce: data[:mp_con_reduce] || 0,
|
|
move_to: data[:move_to] || -1,
|
|
morph_id: data[:morph] || data[:morph_id] || 0,
|
|
summon_movement_type: parse_summon_movement(data[:summon_movement]),
|
|
dot: data[:dot] || 0,
|
|
dot_time: data[:dot_time] || 0,
|
|
thaw: data[:thaw] || 0,
|
|
self_destruction: data[:self_destruction] || 0,
|
|
pvp_damage: data[:pvp_damage] || 0,
|
|
inc_pvp_damage: data[:inc_pvp_damage] || 0,
|
|
indie_pad: data[:indie_pad] || 0,
|
|
indie_mad: data[:indie_mad] || 0,
|
|
indie_mhp: data[:indie_mhp] || 0,
|
|
indie_mmp: data[:indie_mmp] || 0,
|
|
indie_speed: data[:indie_speed] || 0,
|
|
indie_jump: data[:indie_jump] || 0,
|
|
indie_acc: data[:indie_acc] || 0,
|
|
indie_eva: data[:indie_eva] || 0,
|
|
indie_pdd: data[:indie_pdd] || 0,
|
|
indie_mdd: data[:indie_mdd] || 0,
|
|
indie_all_stat: data[:indie_all_stat] || 0,
|
|
str: data[:str] || 0,
|
|
dex: data[:dex] || 0,
|
|
int: data[:int] || 0,
|
|
luk: data[:luk] || 0,
|
|
str_x: data[:str_x] || 0,
|
|
dex_x: data[:dex_x] || 0,
|
|
int_x: data[:int_x] || 0,
|
|
luk_x: data[:luk_x] || 0,
|
|
x: data[:x] || 0,
|
|
y: data[:y] || 0,
|
|
z: data[:z] || 0,
|
|
stat_ups: data[:stat_ups] || %{},
|
|
monster_status: data[:monster_status] || %{}
|
|
}
|
|
end
|
|
|
|
defp build_summon_entry(skill_id, summon_data) do
|
|
%SummonSkillEntry{
|
|
skill_id: skill_id,
|
|
type: summon_data[:type] || 0,
|
|
mob_count: summon_data[:mob_count] || 1,
|
|
attack_count: summon_data[:attack_count] || 1,
|
|
lt: parse_point(summon_data[:lt]) || {-100, -100},
|
|
rb: parse_point(summon_data[:rb]) || {100, 100},
|
|
delay: summon_data[:delay] || 0
|
|
}
|
|
end
|
|
|
|
defp parse_element(nil), do: :neutral
|
|
defp parse_element("f"), do: :fire
|
|
defp parse_element("i"), do: :ice
|
|
defp parse_element("l"), do: :lightning
|
|
defp parse_element("p"), do: :poison
|
|
defp parse_element("h"), do: :holy
|
|
defp parse_element("d"), do: :dark
|
|
defp parse_element("s"), do: :physical
|
|
defp parse_element(atom) when is_atom(atom), do: atom
|
|
defp parse_element(_), do: :neutral
|
|
|
|
defp parse_summon_movement(nil), do: nil
|
|
defp parse_summon_movement("follow"), do: :follow
|
|
defp parse_summon_movement("stationary"), do: :stationary
|
|
defp parse_summon_movement("circle_follow"), do: :circle_follow
|
|
defp parse_summon_movement(atom) when is_atom(atom), do: atom
|
|
defp parse_summon_movement(_), do: nil
|
|
|
|
defp parse_point(nil), do: nil
|
|
defp parse_point({x, y}), do: {x, y}
|
|
defp parse_point([x, y]), do: {x, y}
|
|
defp parse_point(%{x: x, y: y}), do: {x, y}
|
|
defp parse_point(_), do: nil
|
|
|
|
# Fallback data for basic testing without WZ exports
|
|
defp create_fallback_strings do
|
|
fallback_names = %{
|
|
# Beginner skills
|
|
1_000 => "Three Snails",
|
|
1_001 => "Recovery",
|
|
1_002 => "Nimble Feet",
|
|
1_003 => "Monster Rider",
|
|
1_004 => "Echo of Hero",
|
|
|
|
# Warrior 1st job
|
|
100_000 => "Power Strike",
|
|
100_001 => "Slash Blast",
|
|
100_002 => "Iron Body",
|
|
100_003 => "Iron Body",
|
|
100_004 => "Power Strike",
|
|
100_005 => "Slash Blast",
|
|
100_006 => "Iron Body",
|
|
100_007 => "Power Strike",
|
|
100_008 => "Slash Blast",
|
|
100_009 => "Iron Body",
|
|
100_010 => "Power Strike",
|
|
100_100 => "Power Strike",
|
|
100_101 => "Slash Blast",
|
|
100_102 => "Iron Body",
|
|
|
|
# Magician 1st job
|
|
200_000 => "Magic Claw",
|
|
200_001 => "Teleport",
|
|
200_002 => "Magic Guard",
|
|
200_003 => "Magic Armor",
|
|
200_004 => "Energy Bolt",
|
|
200_005 => "Magic Claw",
|
|
200_006 => "Teleport",
|
|
200_007 => "Magic Guard",
|
|
200_008 => "Magic Armor",
|
|
200_009 => "Energy Bolt",
|
|
200_100 => "Magic Claw",
|
|
200_101 => "Teleport",
|
|
200_102 => "Magic Guard",
|
|
200_103 => "Magic Armor",
|
|
200_104 => "Energy Bolt",
|
|
|
|
# Bowman 1st job
|
|
300_000 => "Arrow Blow",
|
|
300_001 => "Double Shot",
|
|
300_002 => "Critical Shot",
|
|
300_003 => "The Eye of Amazon",
|
|
300_004 => "Focus",
|
|
300_100 => "Arrow Blow",
|
|
300_101 => "Double Shot",
|
|
300_102 => "Critical Shot",
|
|
300_103 => "The Eye of Amazon",
|
|
300_104 => "Focus",
|
|
|
|
# Thief 1st job
|
|
400_000 => "Lucky Seven",
|
|
400_001 => "Double Stab",
|
|
400_002 => "Disorder",
|
|
400_003 => "Dark Sight",
|
|
400_004 => "Lucky Seven",
|
|
400_005 => "Double Stab",
|
|
400_100 => "Lucky Seven",
|
|
400_101 => "Double Stab",
|
|
400_102 => "Disorder",
|
|
400_103 => "Dark Sight",
|
|
400_104 => "Lucky Seven",
|
|
400_105 => "Double Stab",
|
|
|
|
# Pirate 1st job
|
|
500_000 => "Somersault Kick",
|
|
500_001 => "Double Fire",
|
|
500_002 => "Dash",
|
|
500_003 => "Shadow Heart",
|
|
500_004 => "Somersault Kick",
|
|
500_005 => "Double Fire",
|
|
500_100 => "Somersault Kick",
|
|
500_101 => "Double Fire",
|
|
500_102 => "Dash",
|
|
500_103 => "Shadow Heart",
|
|
500_104 => "Somersault Kick",
|
|
500_105 => "Double Fire",
|
|
|
|
# GM skills
|
|
9_001_000 => "Haste",
|
|
9_001_001 => "Dragon Roar",
|
|
9_001_002 => "Holy Symbol",
|
|
9_001_003 => "Heal",
|
|
9_001_004 => "Hide",
|
|
9_001_005 => "Resurrection",
|
|
9_001_006 => "Hyper Body",
|
|
9_001_007 => "Holy Shield",
|
|
9_001_008 => "Holy Shield",
|
|
|
|
# 4th job common
|
|
1_122_004 => "Hero's Will",
|
|
1_222_004 => "Hero's Will",
|
|
1_322_004 => "Hero's Will",
|
|
2_122_004 => "Hero's Will",
|
|
2_222_004 => "Hero's Will",
|
|
2_322_004 => "Hero's Will",
|
|
3_122_004 => "Hero's Will",
|
|
4_122_004 => "Hero's Will",
|
|
4_222_004 => "Hero's Will",
|
|
5_122_004 => "Hero's Will",
|
|
5_222_004 => "Hero's Will",
|
|
|
|
# Maple Warrior (all 4th jobs)
|
|
1_121_000 => "Maple Warrior",
|
|
1_221_000 => "Maple Warrior",
|
|
1_321_000 => "Maple Warrior",
|
|
2_121_000 => "Maple Warrior",
|
|
2_221_000 => "Maple Warrior",
|
|
2_321_000 => "Maple Warrior",
|
|
3_121_000 => "Maple Warrior",
|
|
3_221_000 => "Maple Warrior",
|
|
4_121_000 => "Maple Warrior",
|
|
4_221_000 => "Maple Warrior",
|
|
5_121_000 => "Maple Warrior",
|
|
5_221_000 => "Maple Warrior"
|
|
}
|
|
|
|
Enum.each(fallback_names, fn {skill_id, name} ->
|
|
:ets.insert(@skill_names, {skill_id, name})
|
|
end)
|
|
end
|
|
|
|
defp create_fallback_skills do
|
|
# Create some basic beginner skills as fallback
|
|
fallback_skills = [
|
|
%{
|
|
id: 1_000,
|
|
name: "Three Snails",
|
|
element: :physical,
|
|
max_level: 3,
|
|
true_max: 3,
|
|
effects: [
|
|
%{level: 1, damage: 150, mp_con: 10, mob_count: 1, x: 15},
|
|
%{level: 2, damage: 200, mp_con: 15, mob_count: 1, x: 30},
|
|
%{level: 3, damage: 250, mp_con: 20, mob_count: 1, x: 45}
|
|
]
|
|
},
|
|
%{
|
|
id: 1_001,
|
|
name: "Recovery",
|
|
element: :neutral,
|
|
max_level: 3,
|
|
true_max: 3,
|
|
effects: [
|
|
%{level: 1, duration: 30000, hp: 10, interval: 2000, x: 10},
|
|
%{level: 2, duration: 30000, hp: 20, interval: 1900, x: 20},
|
|
%{level: 3, duration: 30000, hp: 30, interval: 1800, x: 30}
|
|
]
|
|
},
|
|
%{
|
|
id: 1_002,
|
|
name: "Nimble Feet",
|
|
element: :neutral,
|
|
max_level: 3,
|
|
true_max: 3,
|
|
effects: [
|
|
%{level: 1, duration: 4000, speed: 10, x: 10},
|
|
%{level: 2, duration: 8000, speed: 15, x: 15},
|
|
%{level: 3, duration: 12000, speed: 20, x: 20}
|
|
]
|
|
},
|
|
%{
|
|
id: 1_004,
|
|
name: "Echo of Hero",
|
|
element: :neutral,
|
|
max_level: 1,
|
|
true_max: 1,
|
|
effects: [
|
|
%{level: 1, duration: 1200000, watk: 4, wdef: 4, matk: 4, mdef: 4, x: 4}
|
|
]
|
|
},
|
|
%{
|
|
id: 100_000,
|
|
name: "Power Strike",
|
|
element: :physical,
|
|
max_level: 20,
|
|
true_max: 20,
|
|
skill_type: 1,
|
|
effects: [
|
|
%{level: 1, damage: 145, mp_con: 8, mob_count: 1, attack_count: 1},
|
|
%{level: 10, damage: 190, mp_con: 16, mob_count: 1, attack_count: 1},
|
|
%{level: 20, damage: 245, mp_con: 24, mob_count: 1, attack_count: 1}
|
|
]
|
|
},
|
|
%{
|
|
id: 100_001,
|
|
name: "Slash Blast",
|
|
element: :physical,
|
|
max_level: 20,
|
|
true_max: 20,
|
|
skill_type: 1,
|
|
effects: [
|
|
%{level: 1, damage: 85, mp_con: 8, mob_count: 3, attack_count: 1},
|
|
%{level: 10, damage: 115, mp_con: 16, mob_count: 4, attack_count: 1},
|
|
%{level: 20, damage: 150, mp_con: 24, mob_count: 6, attack_count: 1}
|
|
]
|
|
},
|
|
%{
|
|
id: 200_000,
|
|
name: "Magic Claw",
|
|
element: :neutral,
|
|
max_level: 20,
|
|
true_max: 20,
|
|
magic: true,
|
|
skill_type: 1,
|
|
effects: [
|
|
%{level: 1, damage: 132, mp_con: 12, mob_count: 2, attack_count: 1, x: 22},
|
|
%{level: 10, damage: 156, mp_con: 24, mob_count: 2, attack_count: 1, x: 26},
|
|
%{level: 20, damage: 182, mp_con: 36, mob_count: 2, attack_count: 1, x: 30}
|
|
]
|
|
},
|
|
%{
|
|
id: 200_001,
|
|
name: "Teleport",
|
|
element: :neutral,
|
|
max_level: 20,
|
|
true_max: 20,
|
|
skill_type: 2,
|
|
effects: [
|
|
%{level: 1, mp_con: 40, x: 70},
|
|
%{level: 10, mp_con: 35, x: 115},
|
|
%{level: 20, mp_con: 30, x: 160}
|
|
]
|
|
},
|
|
%{
|
|
id: 200_002,
|
|
name: "Magic Guard",
|
|
element: :neutral,
|
|
max_level: 20,
|
|
true_max: 20,
|
|
skill_type: 2,
|
|
effects: [
|
|
%{level: 1, x: 15},
|
|
%{level: 10, x: 42},
|
|
%{level: 20, x: 70}
|
|
]
|
|
}
|
|
]
|
|
|
|
Enum.each(fallback_skills, fn skill_data ->
|
|
skill = build_skill(skill_data)
|
|
:ets.insert(@skill_cache, {skill.id, skill})
|
|
|
|
job_id = div(skill.id, 10000)
|
|
|
|
existing =
|
|
case :ets.lookup(@skills_by_job, job_id) do
|
|
[{^job_id, list}] -> list
|
|
[] -> []
|
|
end
|
|
|
|
:ets.insert(@skills_by_job, {job_id, [skill.id | existing]})
|
|
end)
|
|
end
|
|
end
|