update
This commit is contained in:
473
lib/odinsea/game/map_factory.ex
Normal file
473
lib/odinsea/game/map_factory.ex
Normal file
@@ -0,0 +1,473 @@
|
||||
defmodule Odinsea.Game.MapFactory do
|
||||
@moduledoc """
|
||||
Map Factory - loads and caches map templates and data.
|
||||
|
||||
This module loads map metadata (portals, footholds, spawns, properties) from cached JSON files.
|
||||
The JSON files should be exported from the Java server's WZ data providers.
|
||||
|
||||
Map templates are cached in ETS for fast lookups.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
# ETS table names
|
||||
@map_templates :odinsea_map_templates
|
||||
@portal_cache :odinsea_portal_cache
|
||||
@foothold_cache :odinsea_foothold_cache
|
||||
|
||||
# Data file paths
|
||||
@map_data_file "data/maps.json"
|
||||
@portal_data_file "data/portals.json"
|
||||
@foothold_data_file "data/footholds.json"
|
||||
|
||||
defmodule Portal do
|
||||
@moduledoc "Represents a portal on a map"
|
||||
|
||||
@type portal_type ::
|
||||
:spawn
|
||||
| :invisible
|
||||
| :visible
|
||||
| :collision
|
||||
| :changeable
|
||||
| :changeable_invisible
|
||||
| :town_portal_point
|
||||
| :script
|
||||
| :sp
|
||||
| :pi
|
||||
| :pv
|
||||
| :tp
|
||||
| :ps
|
||||
| :psi
|
||||
| :hidden
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: integer(),
|
||||
name: String.t(),
|
||||
type: portal_type(),
|
||||
x: integer(),
|
||||
y: integer(),
|
||||
target_map: integer(),
|
||||
target_portal: String.t(),
|
||||
script: String.t() | nil
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:id,
|
||||
:name,
|
||||
:type,
|
||||
:x,
|
||||
:y,
|
||||
:target_map,
|
||||
:target_portal,
|
||||
:script
|
||||
]
|
||||
|
||||
@doc "Converts portal type integer to atom"
|
||||
def type_from_int(type_int) do
|
||||
case type_int do
|
||||
0 -> :spawn
|
||||
1 -> :invisible
|
||||
2 -> :visible
|
||||
3 -> :collision
|
||||
4 -> :changeable
|
||||
5 -> :changeable_invisible
|
||||
6 -> :town_portal_point
|
||||
7 -> :script
|
||||
8 -> :sp
|
||||
9 -> :pi
|
||||
10 -> :pv
|
||||
11 -> :tp
|
||||
12 -> :ps
|
||||
13 -> :psi
|
||||
14 -> :hidden
|
||||
_ -> :invisible
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Converts portal type string to atom"
|
||||
def type_from_string(type_str) do
|
||||
case type_str do
|
||||
"sp" -> :spawn
|
||||
"pi" -> :invisible
|
||||
"pv" -> :visible
|
||||
"pc" -> :collision
|
||||
"pg" -> :changeable
|
||||
"pgi" -> :changeable_invisible
|
||||
"tp" -> :town_portal_point
|
||||
"ps" -> :script
|
||||
"psi" -> :script
|
||||
"hidden" -> :hidden
|
||||
_ -> :invisible
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Foothold do
|
||||
@moduledoc "Represents a foothold (platform) on a map"
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: integer(),
|
||||
x1: integer(),
|
||||
y1: integer(),
|
||||
x2: integer(),
|
||||
y2: integer(),
|
||||
prev: integer(),
|
||||
next: integer()
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:id,
|
||||
:x1,
|
||||
:y1,
|
||||
:x2,
|
||||
:y2,
|
||||
:prev,
|
||||
:next
|
||||
]
|
||||
end
|
||||
|
||||
defmodule FieldTemplate do
|
||||
@moduledoc "Map field template containing all map data"
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
map_id: integer(),
|
||||
map_name: String.t(),
|
||||
street_name: String.t(),
|
||||
return_map: integer(),
|
||||
forced_return: integer(),
|
||||
mob_rate: float(),
|
||||
field_limit: integer(),
|
||||
time_limit: integer(),
|
||||
dec_hp: integer(),
|
||||
dec_hp_interval: integer(),
|
||||
portal_map: %{String.t() => Portal.t()},
|
||||
portals: [Portal.t()],
|
||||
spawn_points: [Portal.t()],
|
||||
footholds: [Foothold.t()],
|
||||
top: integer(),
|
||||
bottom: integer(),
|
||||
left: integer(),
|
||||
right: integer(),
|
||||
bgm: String.t(),
|
||||
first_user_enter: String.t(),
|
||||
user_enter: String.t(),
|
||||
clock: boolean(),
|
||||
everlast: boolean(),
|
||||
town: boolean(),
|
||||
mount_allowed: boolean(),
|
||||
recovery_rate: float(),
|
||||
create_mob_interval: integer(),
|
||||
fixed_mob_capacity: integer()
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:map_id,
|
||||
:map_name,
|
||||
:street_name,
|
||||
:return_map,
|
||||
:forced_return,
|
||||
:mob_rate,
|
||||
:field_limit,
|
||||
:time_limit,
|
||||
:dec_hp,
|
||||
:dec_hp_interval,
|
||||
:portal_map,
|
||||
:portals,
|
||||
:spawn_points,
|
||||
:footholds,
|
||||
:top,
|
||||
:bottom,
|
||||
:left,
|
||||
:right,
|
||||
:bgm,
|
||||
:first_user_enter,
|
||||
:user_enter,
|
||||
:clock,
|
||||
:everlast,
|
||||
:town,
|
||||
:mount_allowed,
|
||||
:recovery_rate,
|
||||
:create_mob_interval,
|
||||
:fixed_mob_capacity
|
||||
]
|
||||
end
|
||||
|
||||
## Public API
|
||||
|
||||
@doc "Starts the MapFactory GenServer"
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc "Gets map template by map ID"
|
||||
@spec get_map_template(integer()) :: FieldTemplate.t() | nil
|
||||
def get_map_template(map_id) do
|
||||
case :ets.lookup(@map_templates, map_id) do
|
||||
[{^map_id, template}] -> template
|
||||
[] -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets portal by name on a map"
|
||||
@spec get_portal(integer(), String.t()) :: Portal.t() | nil
|
||||
def get_portal(map_id, portal_name) do
|
||||
case get_map_template(map_id) do
|
||||
nil -> nil
|
||||
template -> Map.get(template.portal_map, portal_name)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets random spawn portal on a map"
|
||||
@spec get_random_spawn_portal(integer()) :: Portal.t() | nil
|
||||
def get_random_spawn_portal(map_id) do
|
||||
case get_map_template(map_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
template ->
|
||||
spawn_points = template.spawn_points
|
||||
|
||||
if Enum.empty?(spawn_points) do
|
||||
nil
|
||||
else
|
||||
Enum.random(spawn_points)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets map name"
|
||||
@spec get_map_name(integer()) :: String.t()
|
||||
def get_map_name(map_id) do
|
||||
case get_map_template(map_id) do
|
||||
nil -> "UNKNOWN"
|
||||
template -> template.map_name || "UNKNOWN"
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets return map ID"
|
||||
@spec get_return_map(integer()) :: integer()
|
||||
def get_return_map(map_id) do
|
||||
case get_map_template(map_id) do
|
||||
nil -> 999_999_999
|
||||
template -> template.return_map || 999_999_999
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Checks if map exists"
|
||||
@spec map_exists?(integer()) :: boolean()
|
||||
def map_exists?(map_id) do
|
||||
:ets.member(@map_templates, map_id)
|
||||
end
|
||||
|
||||
@doc "Gets all loaded map IDs"
|
||||
@spec get_all_map_ids() :: [integer()]
|
||||
def get_all_map_ids do
|
||||
:ets.select(@map_templates, [{{:"$1", :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc "Reloads map 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(@map_templates, [:set, :public, :named_table, read_concurrency: true])
|
||||
:ets.new(@portal_cache, [:set, :public, :named_table, read_concurrency: true])
|
||||
:ets.new(@foothold_cache, [:set, :public, :named_table, read_concurrency: true])
|
||||
|
||||
# Load data
|
||||
load_map_data()
|
||||
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:reload, _from, state) do
|
||||
Logger.info("Reloading map data...")
|
||||
load_map_data()
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
## Private Functions
|
||||
|
||||
defp load_map_data do
|
||||
priv_dir = :code.priv_dir(:odinsea) |> to_string()
|
||||
|
||||
# Load maps
|
||||
load_maps(Path.join(priv_dir, @map_data_file))
|
||||
|
||||
map_count = :ets.info(@map_templates, :size)
|
||||
Logger.info("Loaded #{map_count} map templates")
|
||||
end
|
||||
|
||||
defp load_maps(file_path) do
|
||||
case File.read(file_path) do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content, keys: :atoms) do
|
||||
{:ok, maps} when is_list(maps) ->
|
||||
Enum.each(maps, fn map_data ->
|
||||
template = build_field_template(map_data)
|
||||
:ets.insert(@map_templates, {template.map_id, template})
|
||||
end)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warn("Failed to parse maps JSON: #{inspect(reason)}")
|
||||
create_fallback_maps()
|
||||
end
|
||||
|
||||
{:error, :enoent} ->
|
||||
Logger.warn("Maps file not found: #{file_path}, using fallback data")
|
||||
create_fallback_maps()
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to read maps: #{inspect(reason)}")
|
||||
create_fallback_maps()
|
||||
end
|
||||
end
|
||||
|
||||
defp build_field_template(map_data) do
|
||||
# Parse portals
|
||||
portals =
|
||||
(map_data[:portals] || [])
|
||||
|> Enum.map(&build_portal/1)
|
||||
|
||||
portal_map =
|
||||
portals
|
||||
|> Enum.map(fn portal -> {portal.name, portal} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
spawn_points =
|
||||
Enum.filter(portals, fn portal ->
|
||||
portal.type == :spawn || portal.name == "sp"
|
||||
end)
|
||||
|
||||
# Parse footholds
|
||||
footholds =
|
||||
(map_data[:footholds] || [])
|
||||
|> Enum.map(&build_foothold/1)
|
||||
|
||||
%FieldTemplate{
|
||||
map_id: map_data[:map_id],
|
||||
map_name: map_data[:map_name] || "",
|
||||
street_name: map_data[:street_name] || "",
|
||||
return_map: map_data[:return_map] || 999_999_999,
|
||||
forced_return: map_data[:forced_return] || 999_999_999,
|
||||
mob_rate: map_data[:mob_rate] || 1.0,
|
||||
field_limit: map_data[:field_limit] || 0,
|
||||
time_limit: map_data[:time_limit] || -1,
|
||||
dec_hp: map_data[:dec_hp] || 0,
|
||||
dec_hp_interval: map_data[:dec_hp_interval] || 10000,
|
||||
portal_map: portal_map,
|
||||
portals: portals,
|
||||
spawn_points: spawn_points,
|
||||
footholds: footholds,
|
||||
top: map_data[:top] || 0,
|
||||
bottom: map_data[:bottom] || 0,
|
||||
left: map_data[:left] || 0,
|
||||
right: map_data[:right] || 0,
|
||||
bgm: map_data[:bgm] || "",
|
||||
first_user_enter: map_data[:first_user_enter] || "",
|
||||
user_enter: map_data[:user_enter] || "",
|
||||
clock: map_data[:clock] || false,
|
||||
everlast: map_data[:everlast] || false,
|
||||
town: map_data[:town] || false,
|
||||
mount_allowed: map_data[:mount_allowed] || true,
|
||||
recovery_rate: map_data[:recovery_rate] || 1.0,
|
||||
create_mob_interval: map_data[:create_mob_interval] || 4000,
|
||||
fixed_mob_capacity: map_data[:fixed_mob_capacity] || 0
|
||||
}
|
||||
end
|
||||
|
||||
defp build_portal(portal_data) do
|
||||
type =
|
||||
cond do
|
||||
is_integer(portal_data[:type]) -> Portal.type_from_int(portal_data[:type])
|
||||
is_binary(portal_data[:type]) -> Portal.type_from_string(portal_data[:type])
|
||||
true -> :invisible
|
||||
end
|
||||
|
||||
%Portal{
|
||||
id: portal_data[:id] || 0,
|
||||
name: portal_data[:name] || "sp",
|
||||
type: type,
|
||||
x: portal_data[:x] || 0,
|
||||
y: portal_data[:y] || 0,
|
||||
target_map: portal_data[:target_map] || 999_999_999,
|
||||
target_portal: portal_data[:target_portal] || "",
|
||||
script: portal_data[:script]
|
||||
}
|
||||
end
|
||||
|
||||
defp build_foothold(foothold_data) do
|
||||
%Foothold{
|
||||
id: foothold_data[:id] || 0,
|
||||
x1: foothold_data[:x1] || 0,
|
||||
y1: foothold_data[:y1] || 0,
|
||||
x2: foothold_data[:x2] || 0,
|
||||
y2: foothold_data[:y2] || 0,
|
||||
prev: foothold_data[:prev] || 0,
|
||||
next: foothold_data[:next] || 0
|
||||
}
|
||||
end
|
||||
|
||||
# Fallback data for basic testing
|
||||
defp create_fallback_maps do
|
||||
# Common beginner maps
|
||||
fallback_maps = [
|
||||
# Maple Island - Southperry
|
||||
%{
|
||||
map_id: 60000,
|
||||
map_name: "Southperry",
|
||||
street_name: "Maple Island",
|
||||
return_map: 60000,
|
||||
forced_return: 60000,
|
||||
portals: [
|
||||
%{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 60000, target_portal: ""}
|
||||
]
|
||||
},
|
||||
# Victoria Island - Henesys
|
||||
%{
|
||||
map_id: 100000000,
|
||||
map_name: "Henesys",
|
||||
street_name: "Victoria Island",
|
||||
return_map: 100000000,
|
||||
forced_return: 100000000,
|
||||
portals: [
|
||||
%{id: 0, name: "sp", type: "sp", x: -1283, y: 86, target_map: 100000000, target_portal: ""}
|
||||
]
|
||||
},
|
||||
# Henesys Hunting Ground I
|
||||
%{
|
||||
map_id: 100010000,
|
||||
map_name: "Henesys Hunting Ground I",
|
||||
street_name: "Victoria Island",
|
||||
return_map: 100000000,
|
||||
forced_return: 100000000,
|
||||
portals: [
|
||||
%{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 100010000, target_portal: ""}
|
||||
]
|
||||
},
|
||||
# Hidden Street - FM Entrance
|
||||
%{
|
||||
map_id: 910000000,
|
||||
map_name: "Free Market Entrance",
|
||||
street_name: "Free Market",
|
||||
return_map: 100000000,
|
||||
forced_return: 100000000,
|
||||
portals: [
|
||||
%{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 910000000, target_portal: ""}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Enum.each(fallback_maps, fn map_data ->
|
||||
template = build_field_template(map_data)
|
||||
:ets.insert(@map_templates, {template.map_id, template})
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user