Start repo, claude & kimi still vibing tho
This commit is contained in:
300
lib/odinsea/game/map.ex
Normal file
300
lib/odinsea/game/map.ex
Normal file
@@ -0,0 +1,300 @@
|
||||
defmodule Odinsea.Game.Map do
|
||||
@moduledoc """
|
||||
Represents a game map instance.
|
||||
|
||||
Each map is a GenServer that manages all objects on the map:
|
||||
- Players
|
||||
- Monsters (mobs)
|
||||
- NPCs
|
||||
- Items (drops)
|
||||
- Reactors
|
||||
- Portals
|
||||
|
||||
Maps are registered by map_id and belong to a specific channel.
|
||||
"""
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Game.Character
|
||||
alias Odinsea.Channel.Packets, as: ChannelPackets
|
||||
|
||||
# ============================================================================
|
||||
# Data Structures
|
||||
# ============================================================================
|
||||
|
||||
defmodule State do
|
||||
@moduledoc "Map instance state"
|
||||
defstruct [
|
||||
# Map identity
|
||||
:map_id,
|
||||
:channel_id,
|
||||
# Objects on map (by type)
|
||||
:players,
|
||||
# Map stores character_id => %{oid: integer(), character: Character.State}
|
||||
:monsters,
|
||||
# Map stores oid => Monster
|
||||
:npcs,
|
||||
# Map stores oid => NPC
|
||||
:items,
|
||||
# Map stores oid => Item
|
||||
:reactors,
|
||||
# Map stores oid => Reactor
|
||||
# Object ID counter
|
||||
:next_oid,
|
||||
# Map properties (TODO: load from WZ data)
|
||||
:return_map,
|
||||
:forced_return,
|
||||
:time_limit,
|
||||
:field_limit,
|
||||
:mob_rate,
|
||||
:drop_rate,
|
||||
# Timestamps
|
||||
:created_at
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
map_id: non_neg_integer(),
|
||||
channel_id: byte(),
|
||||
players: %{pos_integer() => map()},
|
||||
monsters: %{pos_integer() => any()},
|
||||
npcs: %{pos_integer() => any()},
|
||||
items: %{pos_integer() => any()},
|
||||
reactors: %{pos_integer() => any()},
|
||||
next_oid: pos_integer(),
|
||||
return_map: non_neg_integer() | nil,
|
||||
forced_return: non_neg_integer() | nil,
|
||||
time_limit: non_neg_integer() | nil,
|
||||
field_limit: non_neg_integer() | nil,
|
||||
mob_rate: float(),
|
||||
drop_rate: float(),
|
||||
created_at: DateTime.t()
|
||||
}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Client API
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Starts a map GenServer.
|
||||
"""
|
||||
def start_link(opts) do
|
||||
map_id = Keyword.fetch!(opts, :map_id)
|
||||
channel_id = Keyword.fetch!(opts, :channel_id)
|
||||
|
||||
GenServer.start_link(__MODULE__, opts, name: via_tuple(map_id, channel_id))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures a map is loaded for the given channel.
|
||||
"""
|
||||
def ensure_map(map_id, channel_id) do
|
||||
case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do
|
||||
[{pid, _}] ->
|
||||
{:ok, pid}
|
||||
|
||||
[] ->
|
||||
# Start map via DynamicSupervisor
|
||||
spec = {__MODULE__, map_id: map_id, channel_id: channel_id}
|
||||
|
||||
case DynamicSupervisor.start_child(Odinsea.MapSupervisor, spec) do
|
||||
{:ok, pid} -> {:ok, pid}
|
||||
{:error, {:already_started, pid}} -> {:ok, pid}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a player to the map.
|
||||
"""
|
||||
def add_player(map_id, character_id) do
|
||||
# TODO: Get channel_id from somewhere
|
||||
channel_id = 1
|
||||
{:ok, _pid} = ensure_map(map_id, channel_id)
|
||||
GenServer.call(via_tuple(map_id, channel_id), {:add_player, character_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a player from the map.
|
||||
"""
|
||||
def remove_player(map_id, character_id) do
|
||||
# TODO: Get channel_id from somewhere
|
||||
channel_id = 1
|
||||
|
||||
case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do
|
||||
[{_pid, _}] ->
|
||||
GenServer.call(via_tuple(map_id, channel_id), {:remove_player, character_id})
|
||||
|
||||
[] ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a packet to all players on the map.
|
||||
"""
|
||||
def broadcast(map_id, channel_id, packet) do
|
||||
GenServer.cast(via_tuple(map_id, channel_id), {:broadcast, packet})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a packet to all players except the specified character.
|
||||
"""
|
||||
def broadcast_except(map_id, channel_id, except_character_id, packet) do
|
||||
GenServer.cast(via_tuple(map_id, channel_id), {:broadcast_except, except_character_id, packet})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all players on the map.
|
||||
"""
|
||||
def get_players(map_id, channel_id) do
|
||||
GenServer.call(via_tuple(map_id, channel_id), :get_players)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# GenServer Callbacks
|
||||
# ============================================================================
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
map_id = Keyword.fetch!(opts, :map_id)
|
||||
channel_id = Keyword.fetch!(opts, :channel_id)
|
||||
|
||||
state = %State{
|
||||
map_id: map_id,
|
||||
channel_id: channel_id,
|
||||
players: %{},
|
||||
monsters: %{},
|
||||
npcs: %{},
|
||||
items: %{},
|
||||
reactors: %{},
|
||||
next_oid: 500_000,
|
||||
return_map: nil,
|
||||
forced_return: nil,
|
||||
time_limit: nil,
|
||||
field_limit: 0,
|
||||
mob_rate: 1.0,
|
||||
drop_rate: 1.0,
|
||||
created_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
Logger.debug("Map loaded: #{map_id} (channel #{channel_id})")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:add_player, character_id}, _from, state) do
|
||||
# Allocate OID for this player
|
||||
oid = state.next_oid
|
||||
|
||||
# Get character state
|
||||
case Character.get_state(character_id) do
|
||||
%Character.State{} = char_state ->
|
||||
# Add player to map
|
||||
player_entry = %{
|
||||
oid: oid,
|
||||
character: char_state
|
||||
}
|
||||
|
||||
new_players = Map.put(state.players, character_id, player_entry)
|
||||
|
||||
# Broadcast spawn packet to other players
|
||||
spawn_packet = ChannelPackets.spawn_player(oid, char_state)
|
||||
broadcast_to_players(new_players, spawn_packet, except: character_id)
|
||||
|
||||
# Send existing players to new player
|
||||
client_pid = char_state.client_pid
|
||||
|
||||
if client_pid do
|
||||
send_existing_players(client_pid, new_players, except: character_id)
|
||||
end
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
| players: new_players,
|
||||
next_oid: oid + 1
|
||||
}
|
||||
|
||||
{:reply, {:ok, oid}, new_state}
|
||||
|
||||
nil ->
|
||||
{:reply, {:error, :character_not_found}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:remove_player, character_id}, _from, state) do
|
||||
case Map.get(state.players, character_id) do
|
||||
nil ->
|
||||
{:reply, :ok, state}
|
||||
|
||||
player_entry ->
|
||||
# Broadcast despawn packet
|
||||
despawn_packet = ChannelPackets.remove_player(player_entry.oid)
|
||||
broadcast_to_players(state.players, despawn_packet, except: character_id)
|
||||
|
||||
# Remove from map
|
||||
new_players = Map.delete(state.players, character_id)
|
||||
new_state = %{state | players: new_players}
|
||||
|
||||
{:reply, :ok, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_players, _from, state) do
|
||||
{:reply, state.players, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:broadcast, packet}, state) do
|
||||
broadcast_to_players(state.players, packet)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:broadcast_except, except_character_id, packet}, state) do
|
||||
broadcast_to_players(state.players, packet, except: except_character_id)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
defp via_tuple(map_id, channel_id) do
|
||||
{:via, Registry, {Odinsea.MapRegistry, {map_id, channel_id}}}
|
||||
end
|
||||
|
||||
defp broadcast_to_players(players, packet, opts \\ []) do
|
||||
except_char_id = Keyword.get(opts, :except)
|
||||
|
||||
Enum.each(players, fn
|
||||
{char_id, %{character: char_state}} when char_id != except_char_id ->
|
||||
if char_state.client_pid do
|
||||
send_packet(char_state.client_pid, packet)
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
|
||||
defp send_existing_players(client_pid, players, opts) do
|
||||
except_char_id = Keyword.get(opts, :except)
|
||||
|
||||
Enum.each(players, fn
|
||||
{char_id, %{oid: oid, character: char_state}} when char_id != except_char_id ->
|
||||
spawn_packet = ChannelPackets.spawn_player(oid, char_state)
|
||||
send_packet(client_pid, spawn_packet)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
|
||||
defp send_packet(client_pid, packet) do
|
||||
send(client_pid, {:send_packet, packet})
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user