update
This commit is contained in:
438
lib/odinsea/game/life_factory.ex
Normal file
438
lib/odinsea/game/life_factory.ex
Normal file
@@ -0,0 +1,438 @@
|
||||
defmodule Odinsea.Game.LifeFactory do
|
||||
@moduledoc """
|
||||
Life Factory - loads and caches monster and NPC data.
|
||||
|
||||
This module loads life metadata (monsters, NPCs, stats, skills) from cached JSON files.
|
||||
The JSON files should be exported from the Java server's WZ data providers.
|
||||
|
||||
Life data is cached in ETS for fast lookups.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
# ETS table names
|
||||
@monster_stats :odinsea_monster_stats
|
||||
@npc_data :odinsea_npc_data
|
||||
@mob_skills :odinsea_mob_skills
|
||||
|
||||
# Data file paths
|
||||
@monster_data_file "data/monsters.json"
|
||||
@npc_data_file "data/npcs.json"
|
||||
@mob_skill_file "data/mob_skills.json"
|
||||
|
||||
defmodule MonsterStats do
|
||||
@moduledoc "Monster statistics and properties"
|
||||
|
||||
@type element :: :physical | :ice | :fire | :poison | :lightning | :holy | :dark
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
mob_id: integer(),
|
||||
name: String.t(),
|
||||
level: integer(),
|
||||
hp: integer(),
|
||||
mp: integer(),
|
||||
exp: integer(),
|
||||
physical_attack: integer(),
|
||||
magic_attack: integer(),
|
||||
physical_defense: integer(),
|
||||
magic_defense: integer(),
|
||||
accuracy: integer(),
|
||||
evasion: integer(),
|
||||
speed: integer(),
|
||||
chase_speed: integer(),
|
||||
boss: boolean(),
|
||||
undead: boolean(),
|
||||
flying: boolean(),
|
||||
friendly: boolean(),
|
||||
public_reward: boolean(),
|
||||
explosive_reward: boolean(),
|
||||
invincible: boolean(),
|
||||
first_attack: boolean(),
|
||||
kb_recovery: integer(),
|
||||
fixed_damage: integer(),
|
||||
only_normal_attack: boolean(),
|
||||
self_destruction_hp: integer(),
|
||||
self_destruction_action: integer(),
|
||||
remove_after: integer(),
|
||||
tag_color: integer(),
|
||||
tag_bg_color: integer(),
|
||||
skills: [integer()],
|
||||
revives: [integer()],
|
||||
drop_item_period: integer(),
|
||||
elemental_attributes: %{element() => atom()}
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:mob_id,
|
||||
:name,
|
||||
:level,
|
||||
:hp,
|
||||
:mp,
|
||||
:exp,
|
||||
:physical_attack,
|
||||
:magic_attack,
|
||||
:physical_defense,
|
||||
:magic_defense,
|
||||
:accuracy,
|
||||
:evasion,
|
||||
:speed,
|
||||
:chase_speed,
|
||||
:boss,
|
||||
:undead,
|
||||
:flying,
|
||||
:friendly,
|
||||
:public_reward,
|
||||
:explosive_reward,
|
||||
:invincible,
|
||||
:first_attack,
|
||||
:kb_recovery,
|
||||
:fixed_damage,
|
||||
:only_normal_attack,
|
||||
:self_destruction_hp,
|
||||
:self_destruction_action,
|
||||
:remove_after,
|
||||
:tag_color,
|
||||
:tag_bg_color,
|
||||
:skills,
|
||||
:revives,
|
||||
:drop_item_period,
|
||||
:elemental_attributes
|
||||
]
|
||||
end
|
||||
|
||||
defmodule NPC do
|
||||
@moduledoc "NPC data"
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
npc_id: integer(),
|
||||
name: String.t(),
|
||||
has_shop: boolean(),
|
||||
shop_id: integer() | nil,
|
||||
script: String.t() | nil
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:npc_id,
|
||||
:name,
|
||||
:has_shop,
|
||||
:shop_id,
|
||||
:script
|
||||
]
|
||||
end
|
||||
|
||||
## Public API
|
||||
|
||||
@doc "Starts the LifeFactory GenServer"
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc "Gets monster stats by mob ID"
|
||||
@spec get_monster_stats(integer()) :: MonsterStats.t() | nil
|
||||
def get_monster_stats(mob_id) do
|
||||
case :ets.lookup(@monster_stats, mob_id) do
|
||||
[{^mob_id, stats}] -> stats
|
||||
[] -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets NPC data by NPC ID"
|
||||
@spec get_npc(integer()) :: NPC.t() | nil
|
||||
def get_npc(npc_id) do
|
||||
case :ets.lookup(@npc_data, npc_id) do
|
||||
[{^npc_id, npc}] -> npc
|
||||
[] -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets monster name by mob ID"
|
||||
@spec get_monster_name(integer()) :: String.t()
|
||||
def get_monster_name(mob_id) do
|
||||
case get_monster_stats(mob_id) do
|
||||
nil -> "UNKNOWN"
|
||||
stats -> stats.name || "UNKNOWN"
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets NPC name by NPC ID"
|
||||
@spec get_npc_name(integer()) :: String.t()
|
||||
def get_npc_name(npc_id) do
|
||||
case get_npc(npc_id) do
|
||||
nil -> "UNKNOWN"
|
||||
npc -> npc.name || "UNKNOWN"
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Checks if monster exists"
|
||||
@spec monster_exists?(integer()) :: boolean()
|
||||
def monster_exists?(mob_id) do
|
||||
:ets.member(@monster_stats, mob_id)
|
||||
end
|
||||
|
||||
@doc "Checks if NPC exists"
|
||||
@spec npc_exists?(integer()) :: boolean()
|
||||
def npc_exists?(npc_id) do
|
||||
:ets.member(@npc_data, npc_id)
|
||||
end
|
||||
|
||||
@doc "Gets all loaded monster IDs"
|
||||
@spec get_all_monster_ids() :: [integer()]
|
||||
def get_all_monster_ids do
|
||||
:ets.select(@monster_stats, [{{:"$1", :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc "Gets all loaded NPC IDs"
|
||||
@spec get_all_npc_ids() :: [integer()]
|
||||
def get_all_npc_ids do
|
||||
:ets.select(@npc_data, [{{:"$1", :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc "Reloads life 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(@monster_stats, [:set, :public, :named_table, read_concurrency: true])
|
||||
:ets.new(@npc_data, [:set, :public, :named_table, read_concurrency: true])
|
||||
:ets.new(@mob_skills, [:set, :public, :named_table, read_concurrency: true])
|
||||
|
||||
# Load data
|
||||
load_life_data()
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:reload, _from, state) do
|
||||
Logger.info("Reloading life data...")
|
||||
load_life_data()
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
## Private Functions
|
||||
|
||||
defp load_life_data do
|
||||
priv_dir = :code.priv_dir(:odinsea) |> to_string()
|
||||
|
||||
# Load monsters and NPCs
|
||||
load_monsters(Path.join(priv_dir, @monster_data_file))
|
||||
load_npcs(Path.join(priv_dir, @npc_data_file))
|
||||
|
||||
monster_count = :ets.info(@monster_stats, :size)
|
||||
npc_count = :ets.info(@npc_data, :size)
|
||||
Logger.info("Loaded #{monster_count} monsters and #{npc_count} NPCs")
|
||||
end
|
||||
|
||||
defp load_monsters(file_path) do
|
||||
case File.read(file_path) do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content, keys: :atoms) do
|
||||
{:ok, monsters} when is_list(monsters) ->
|
||||
Enum.each(monsters, fn monster_data ->
|
||||
stats = build_monster_stats(monster_data)
|
||||
:ets.insert(@monster_stats, {stats.mob_id, stats})
|
||||
end)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warn("Failed to parse monsters JSON: #{inspect(reason)}")
|
||||
create_fallback_monsters()
|
||||
end
|
||||
|
||||
{:error, :enoent} ->
|
||||
Logger.warn("Monsters file not found: #{file_path}, using fallback data")
|
||||
create_fallback_monsters()
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to read monsters: #{inspect(reason)}")
|
||||
create_fallback_monsters()
|
||||
end
|
||||
end
|
||||
|
||||
defp load_npcs(file_path) do
|
||||
case File.read(file_path) do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content, keys: :atoms) do
|
||||
{:ok, npcs} when is_list(npcs) ->
|
||||
Enum.each(npcs, fn npc_data ->
|
||||
npc = build_npc(npc_data)
|
||||
:ets.insert(@npc_data, {npc.npc_id, npc})
|
||||
end)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warn("Failed to parse NPCs JSON: #{inspect(reason)}")
|
||||
create_fallback_npcs()
|
||||
end
|
||||
|
||||
{:error, :enoent} ->
|
||||
Logger.warn("NPCs file not found: #{file_path}, using fallback data")
|
||||
create_fallback_npcs()
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to read NPCs: #{inspect(reason)}")
|
||||
create_fallback_npcs()
|
||||
end
|
||||
end
|
||||
|
||||
defp build_monster_stats(monster_data) do
|
||||
%MonsterStats{
|
||||
mob_id: monster_data[:mob_id] || monster_data[:id],
|
||||
name: monster_data[:name] || "UNKNOWN",
|
||||
level: monster_data[:level] || 1,
|
||||
hp: monster_data[:hp] || 100,
|
||||
mp: monster_data[:mp] || 0,
|
||||
exp: monster_data[:exp] || 0,
|
||||
physical_attack: monster_data[:physical_attack] || monster_data[:watk] || 10,
|
||||
magic_attack: monster_data[:magic_attack] || monster_data[:matk] || 10,
|
||||
physical_defense: monster_data[:physical_defense] || monster_data[:wdef] || 0,
|
||||
magic_defense: monster_data[:magic_defense] || monster_data[:mdef] || 0,
|
||||
accuracy: monster_data[:accuracy] || monster_data[:acc] || 10,
|
||||
evasion: monster_data[:evasion] || monster_data[:eva] || 5,
|
||||
speed: monster_data[:speed] || 100,
|
||||
chase_speed: monster_data[:chase_speed] || 100,
|
||||
boss: monster_data[:boss] || false,
|
||||
undead: monster_data[:undead] || false,
|
||||
flying: monster_data[:flying] || monster_data[:fly] || false,
|
||||
friendly: monster_data[:friendly] || false,
|
||||
public_reward: monster_data[:public_reward] || false,
|
||||
explosive_reward: monster_data[:explosive_reward] || false,
|
||||
invincible: monster_data[:invincible] || false,
|
||||
first_attack: monster_data[:first_attack] || false,
|
||||
kb_recovery: monster_data[:kb_recovery] || monster_data[:pushed] || 1,
|
||||
fixed_damage: monster_data[:fixed_damage] || 0,
|
||||
only_normal_attack: monster_data[:only_normal_attack] || false,
|
||||
self_destruction_hp: monster_data[:self_destruction_hp] || 0,
|
||||
self_destruction_action: monster_data[:self_destruction_action] || 0,
|
||||
remove_after: monster_data[:remove_after] || -1,
|
||||
tag_color: monster_data[:tag_color] || 0,
|
||||
tag_bg_color: monster_data[:tag_bg_color] || 0,
|
||||
skills: monster_data[:skills] || [],
|
||||
revives: monster_data[:revives] || [],
|
||||
drop_item_period: monster_data[:drop_item_period] || 0,
|
||||
elemental_attributes: monster_data[:elemental_attributes] || %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp build_npc(npc_data) do
|
||||
%NPC{
|
||||
npc_id: npc_data[:npc_id] || npc_data[:id],
|
||||
name: npc_data[:name] || "UNKNOWN",
|
||||
has_shop: npc_data[:has_shop] || false,
|
||||
shop_id: npc_data[:shop_id],
|
||||
script: npc_data[:script]
|
||||
}
|
||||
end
|
||||
|
||||
# Fallback data for basic testing
|
||||
defp create_fallback_monsters do
|
||||
# Common beginner monsters
|
||||
fallback_monsters = [
|
||||
# Blue Snail
|
||||
%{
|
||||
mob_id: 100100,
|
||||
name: "Blue Snail",
|
||||
level: 1,
|
||||
hp: 50,
|
||||
mp: 0,
|
||||
exp: 3,
|
||||
physical_attack: 8,
|
||||
magic_attack: 8,
|
||||
physical_defense: 10,
|
||||
magic_defense: 10,
|
||||
accuracy: 5,
|
||||
evasion: 3,
|
||||
speed: 50,
|
||||
boss: false,
|
||||
undead: false,
|
||||
flying: false
|
||||
},
|
||||
# Red Snail
|
||||
%{
|
||||
mob_id: 130101,
|
||||
name: "Red Snail",
|
||||
level: 3,
|
||||
hp: 80,
|
||||
mp: 0,
|
||||
exp: 5,
|
||||
physical_attack: 12,
|
||||
magic_attack: 12,
|
||||
physical_defense: 15,
|
||||
magic_defense: 15,
|
||||
accuracy: 8,
|
||||
evasion: 5,
|
||||
speed: 50,
|
||||
boss: false,
|
||||
undead: false,
|
||||
flying: false
|
||||
},
|
||||
# Green Mushroom
|
||||
%{
|
||||
mob_id: 1110100,
|
||||
name: "Green Mushroom",
|
||||
level: 7,
|
||||
hp: 200,
|
||||
mp: 0,
|
||||
exp: 12,
|
||||
physical_attack: 30,
|
||||
magic_attack: 30,
|
||||
physical_defense: 30,
|
||||
magic_defense: 30,
|
||||
accuracy: 20,
|
||||
evasion: 10,
|
||||
speed: 80,
|
||||
boss: false,
|
||||
undead: false,
|
||||
flying: false
|
||||
},
|
||||
# Orange Mushroom
|
||||
%{
|
||||
mob_id: 1210100,
|
||||
name: "Orange Mushroom",
|
||||
level: 10,
|
||||
hp: 300,
|
||||
mp: 0,
|
||||
exp: 20,
|
||||
physical_attack: 45,
|
||||
magic_attack: 45,
|
||||
physical_defense: 40,
|
||||
magic_defense: 40,
|
||||
accuracy: 25,
|
||||
evasion: 12,
|
||||
speed: 100,
|
||||
boss: false,
|
||||
undead: false,
|
||||
flying: false
|
||||
}
|
||||
]
|
||||
|
||||
Enum.each(fallback_monsters, fn monster_data ->
|
||||
stats = build_monster_stats(monster_data)
|
||||
:ets.insert(@monster_stats, {stats.mob_id, stats})
|
||||
end)
|
||||
end
|
||||
|
||||
defp create_fallback_npcs do
|
||||
# Common NPCs
|
||||
fallback_npcs = [
|
||||
# Henesys NPCs
|
||||
%{npc_id: 1012000, name: "Athena Pierce", has_shop: false},
|
||||
%{npc_id: 1012001, name: "Robin", has_shop: false},
|
||||
%{npc_id: 1012002, name: "Maya", has_shop: false},
|
||||
# General Shop
|
||||
%{npc_id: 9201045, name: "General Store", has_shop: true, shop_id: 1000},
|
||||
# Beginner instructors
|
||||
%{npc_id: 1002000, name: "Sera", has_shop: false},
|
||||
%{npc_id: 2007, name: "Peter", has_shop: false}
|
||||
]
|
||||
|
||||
Enum.each(fallback_npcs, fn npc_data ->
|
||||
npc = build_npc(npc_data)
|
||||
:ets.insert(@npc_data, {npc.npc_id, npc})
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user