Files
odinsea-elixir/lib/odinsea/game/event_manager.ex
2026-02-14 23:12:33 -07:00

607 lines
20 KiB
Elixir

defmodule Odinsea.Game.EventManager do
@moduledoc """
Event Manager for scheduling and managing in-game events.
Ported from Java `server.events` scheduling functionality.
## Responsibilities
- Event scheduling per channel
- Player registration for events
- Event state management
- Event coordination across channels
## Event Types
- Coconut - Team coconut hitting
- Fitness - Obstacle course
- OlaOla - Portal guessing
- OxQuiz - True/False quiz
- Snowball - Team snowball rolling
- Survival - Last man standing
## Usage
Event scheduling is typically done by GM commands or automated system:
# Schedule an event
EventManager.schedule_event(channel_id, :coconut)
# Player joins event
EventManager.join_event(channel_id, character_id, :coconut)
# Start the event
EventManager.start_event(channel_id, :coconut)
"""
use GenServer
alias Odinsea.Game.Events
alias Odinsea.Game.Event
alias Odinsea.Game.Timer.EventTimer
require Logger
# ============================================================================
# Types
# ============================================================================
@typedoc "Channel event state"
@type channel_events :: %{
optional(Events.t()) => Event.t() | struct()
}
@typedoc "Manager state"
@type state :: %{
channels: %{optional(non_neg_integer()) => channel_events()},
schedules: %{optional(reference()) => {:auto_start, non_neg_integer(), Events.t()}}
}
# ============================================================================
# Client API
# ============================================================================
@doc """
Starts the Event Manager.
"""
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Schedules an event to run on a specific channel.
## Parameters
- channel_id: Channel to run event on
- event_type: Type of event (:coconut, :fitness, etc.)
## Returns
- :ok on success
- {:error, reason} on failure
"""
def schedule_event(channel_id, event_type) do
GenServer.call(__MODULE__, {:schedule_event, channel_id, event_type})
end
@doc """
Starts a scheduled event immediately.
## Parameters
- channel_id: Channel running the event
- event_type: Type of event
"""
def start_event(channel_id, event_type) do
GenServer.call(__MODULE__, {:start_event, channel_id, event_type})
end
@doc """
Cancels a scheduled event.
## Parameters
- channel_id: Channel with the event
- event_type: Type of event
"""
def cancel_event(channel_id, event_type) do
GenServer.call(__MODULE__, {:cancel_event, channel_id, event_type})
end
@doc """
Registers a player for an event.
## Parameters
- channel_id: Channel with the event
- character_id: Character joining
- event_type: Type of event
"""
def join_event(channel_id, character_id, event_type) do
GenServer.call(__MODULE__, {:join_event, channel_id, character_id, event_type})
end
@doc """
Unregisters a player from an event.
## Parameters
- channel_id: Channel with the event
- character_id: Character leaving
- event_type: Type of event
"""
def leave_event(channel_id, character_id, event_type) do
GenServer.call(__MODULE__, {:leave_event, channel_id, character_id, event_type})
end
@doc """
Handles a player loading into an event map.
Called by map load handlers.
## Parameters
- channel_id: Channel the player is on
- character: Character struct
- map_id: Map ID player loaded into
"""
def on_map_load(channel_id, character, map_id) do
GenServer.cast(__MODULE__, {:on_map_load, channel_id, character, map_id})
end
@doc """
Handles a GM manually starting an event.
"""
def on_start_event(channel_id, character, map_id) do
GenServer.cast(__MODULE__, {:on_start_event, channel_id, character, map_id})
end
@doc """
Gets the active event on a channel.
"""
def get_active_event(channel_id) do
GenServer.call(__MODULE__, {:get_active_event, channel_id})
end
@doc """
Gets all events for a channel.
"""
def get_channel_events(channel_id) do
GenServer.call(__MODULE__, {:get_channel_events, channel_id})
end
@doc """
Checks if an event is running on a channel.
"""
def event_running?(channel_id, event_type) do
GenServer.call(__MODULE__, {:event_running?, channel_id, event_type})
end
@doc """
Sets the active event map for a channel.
This is the map where players should go to join.
"""
def set_event_map(channel_id, map_id) do
GenServer.cast(__MODULE__, {:set_event_map, channel_id, map_id})
end
@doc """
Gets the event map for a channel (where players join).
"""
def get_event_map(channel_id) do
GenServer.call(__MODULE__, {:get_event_map, channel_id})
end
@doc """
Lists all available event types.
"""
def list_event_types do
Events.all()
end
@doc """
Gets event info for a type.
"""
def event_info(event_type) do
%{
type: event_type,
name: Events.display_name(event_type),
map_ids: Events.map_ids(event_type),
entry_map: Events.entry_map_id(event_type),
stages: Events.stage_count(event_type),
is_race: Events.race_event?(event_type),
is_team: Events.team_event?(event_type)
}
end
# ============================================================================
# Server Callbacks
# ============================================================================
@impl true
def init(_) do
Logger.info("EventManager started")
state = %{
channels: %{},
schedules: %{},
event_maps: %{} # channel_id => map_id
}
{:ok, state}
end
@impl true
def handle_call({:schedule_event, channel_id, event_type}, _from, state) do
case do_schedule_event(state, channel_id, event_type) do
{:ok, new_state} ->
broadcast_event_notice(channel_id, event_type)
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_call({:start_event, channel_id, event_type}, _from, state) do
case do_start_event(state, channel_id, event_type) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_call({:cancel_event, channel_id, event_type}, _from, state) do
new_state = do_cancel_event(state, channel_id, event_type)
{:reply, :ok, new_state}
end
@impl true
def handle_call({:join_event, channel_id, character_id, event_type}, _from, state) do
{reply, new_state} = do_join_event(state, channel_id, character_id, event_type)
{:reply, reply, new_state}
end
@impl true
def handle_call({:leave_event, channel_id, character_id, event_type}, _from, state) do
{reply, new_state} = do_leave_event(state, channel_id, character_id, event_type)
{:reply, reply, new_state}
end
@impl true
def handle_call({:get_active_event, channel_id}, _from, state) do
event = get_active_event_impl(state, channel_id)
{:reply, event, state}
end
@impl true
def handle_call({:get_channel_events, channel_id}, _from, state) do
events = Map.get(state.channels, channel_id, %{})
{:reply, events, state}
end
@impl true
def handle_call({:event_running?, channel_id, event_type}, _from, state) do
running = event_running_impl?(state, channel_id, event_type)
{:reply, running, state}
end
@impl true
def handle_call({:get_event_map, channel_id}, _from, state) do
map_id = Map.get(state.event_maps, channel_id)
{:reply, map_id, state}
end
@impl true
def handle_cast({:on_map_load, channel_id, character, map_id}, state) do
new_state = do_on_map_load(state, channel_id, character, map_id)
{:noreply, new_state}
end
@impl true
def handle_cast({:on_start_event, channel_id, character, map_id}, state) do
new_state = do_on_start_event(state, channel_id, character, map_id)
{:noreply, new_state}
end
@impl true
def handle_cast({:set_event_map, channel_id, map_id}, state) do
new_maps = Map.put(state.event_maps, channel_id, map_id)
{:noreply, %{state | event_maps: new_maps}}
end
@impl true
def handle_info({:auto_start, channel_id, event_type}, state) do
Logger.info("Auto-starting event #{event_type} on channel #{channel_id}")
# Start the event
case do_start_event(state, channel_id, event_type) do
{:ok, new_state} ->
# Clear event map
new_maps = Map.delete(new_state.event_maps, channel_id)
{:noreply, %{new_state | event_maps: new_maps}}
{:error, _} ->
{:noreply, state}
end
end
# ============================================================================
# Private Functions
# ============================================================================
defp do_schedule_event(state, channel_id, event_type) do
# Check if event type is valid
if event_type not in Events.all() do
{:error, "Invalid event type"}
else
# Check if event is already running
if event_running_impl?(state, channel_id, event_type) do
{:error, "Event already running"}
else
# Create event instance
event = create_event(event_type, channel_id)
# Reset event
event = reset_event(event, event_type)
# Store event
channel_events = Map.get(state.channels, channel_id, %{})
channel_events = Map.put(channel_events, event_type, event)
channels = Map.put(state.channels, channel_id, channel_events)
# Set event map (entry map)
entry_map = Events.entry_map_id(event_type)
event_maps = Map.put(state.event_maps, channel_id, entry_map)
{:ok, %{state | channels: channels, event_maps: event_maps}}
end
end
end
defp do_start_event(state, channel_id, event_type) do
with {:ok, event} <- get_event(state, channel_id, event_type) do
# Start the event
new_event = start_event_impl(event, event_type)
# Update state
channel_events = Map.get(state.channels, channel_id, %{})
channel_events = Map.put(channel_events, event_type, new_event)
channels = Map.put(state.channels, channel_id, channel_events)
# Clear event map
event_maps = Map.delete(state.event_maps, channel_id)
{:ok, %{state | channels: channels, event_maps: event_maps}}
else
nil -> {:error, "Event not found"}
end
end
defp do_cancel_event(state, channel_id, event_type) do
with {:ok, event} <- get_event(state, channel_id, event_type) do
# Unreset event (cleanup)
unreset_event(event, event_type)
# Remove from state
channel_events = Map.get(state.channels, channel_id, %{})
channel_events = Map.delete(channel_events, event_type)
channels = Map.put(state.channels, channel_id, channel_events)
# Clear event map
event_maps = Map.delete(state.event_maps, channel_id)
%{state | channels: channels, event_maps: event_maps}
else
nil -> state
end
end
defp do_join_event(state, channel_id, character_id, event_type) do
with {:ok, event} <- get_event(state, channel_id, event_type) do
if event.base.is_running do
# Register player
new_event = Event.register_player(event.base, character_id)
new_event = %{event | base: new_event}
# Check if we should auto-start (250 players)
new_event = Event.increment_player_count(new_event.base)
new_event = %{event | base: new_event}
# Update state
channel_events = Map.get(state.channels, channel_id, %{})
channel_events = Map.put(channel_events, event_type, new_event)
channels = Map.put(state.channels, channel_id, channel_events)
{{:ok, :joined}, %{state | channels: channels}}
else
{{:error, "Event not running"}, state}
end
else
nil -> {{:error, "Event not found"}, state}
end
end
defp do_leave_event(state, channel_id, character_id, event_type) do
with {:ok, event} <- get_event(state, channel_id, event_type) do
# Unregister player
new_base = Event.unregister_player(event.base, character_id)
new_event = %{event | base: new_base}
# Update state
channel_events = Map.get(state.channels, channel_id, %{})
channel_events = Map.put(channel_events, event_type, new_event)
channels = Map.put(state.channels, channel_id, channel_events)
{{:ok, :left}, %{state | channels: channels}}
else
nil -> {{:error, "Event not found"}, state}
end
end
defp do_on_map_load(state, channel_id, character, map_id) do
# Check if any event is running on this channel
channel_events = Map.get(state.channels, channel_id, %{})
Enum.reduce(channel_events, state, fn {event_type, event}, acc_state ->
if event.base.is_running do
# Check if this is the finish map
if map_id == 109050000 do
# Call finished callback
finished_event(event, event_type, character)
end
# Check if this is an event map
if Event.event_map?(event.base, map_id) do
# Call on_map_load callback
on_map_load_event(event, event_type, character)
# If first map, increment player count
if Event.map_index(event.base, map_id) == 0 do
new_base = Event.increment_player_count(event.base)
# Check if we hit 250 players
if new_base.player_count >= 250 do
# Auto-start
schedule_auto_start(channel_id, event_type)
end
# Update event in state
new_event = put_event_base(event, event_type, new_base)
channel_events = Map.put(acc_state.channels[channel_id], event_type, new_event)
channels = Map.put(acc_state.channels, channel_id, channel_events)
%{acc_state | channels: channels}
else
acc_state
end
else
acc_state
end
else
acc_state
end
end)
end
defp do_on_start_event(state, channel_id, character, map_id) do
channel_events = Map.get(state.channels, channel_id, %{})
Enum.find_value(channel_events, state, fn {event_type, event} ->
if event.base.is_running and Event.event_map?(event.base, map_id) do
# Start the event
new_event = start_event_impl(event, event_type)
# Update state
channel_events = Map.put(channel_events, event_type, new_event)
channels = Map.put(state.channels, channel_id, channel_events)
# Clear event map
event_maps = Map.delete(state.event_maps, channel_id)
%{state | channels: channels, event_maps: event_maps}
end
end) || state
end
defp get_event(state, channel_id, event_type) do
channel_events = Map.get(state.channels, channel_id, %{})
case Map.get(channel_events, event_type) do
nil -> nil
event -> {:ok, event}
end
end
defp get_active_event_impl(state, channel_id) do
channel_events = Map.get(state.channels, channel_id, %{})
Enum.find_value(channel_events, fn {event_type, event} ->
if event.base.is_running do
{event_type, event}
end
end)
end
defp event_running_impl?(state, channel_id, event_type) do
case get_event(state, channel_id, event_type) do
{:ok, event} -> event.base.is_running
nil -> false
end
end
defp create_event(:coconut, channel_id), do: Odinsea.Game.Events.Coconut.new(channel_id)
defp create_event(:fitness, channel_id), do: Odinsea.Game.Events.Fitness.new(channel_id)
defp create_event(:ola_ola, channel_id), do: Odinsea.Game.Events.OlaOla.new(channel_id)
defp create_event(:ox_quiz, channel_id), do: Odinsea.Game.Events.OxQuiz.new(channel_id)
defp create_event(:snowball, channel_id), do: Odinsea.Game.Events.Snowball.new(channel_id)
defp create_event(:survival, channel_id), do: Odinsea.Game.Events.Survival.new(channel_id)
defp create_event(_, _), do: nil
defp reset_event(event, :coconut), do: Odinsea.Game.Events.Coconut.reset(event)
defp reset_event(event, :fitness), do: Odinsea.Game.Events.Fitness.reset(event)
defp reset_event(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.reset(event)
defp reset_event(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.reset(event)
defp reset_event(event, :snowball), do: Odinsea.Game.Events.Snowball.reset(event)
defp reset_event(event, :survival), do: Odinsea.Game.Events.Survival.reset(event)
defp reset_event(event, _), do: event
defp unreset_event(event, :coconut), do: Odinsea.Game.Events.Coconut.unreset(event)
defp unreset_event(event, :fitness), do: Odinsea.Game.Events.Fitness.unreset(event)
defp unreset_event(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.unreset(event)
defp unreset_event(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.unreset(event)
defp unreset_event(event, :snowball), do: Odinsea.Game.Events.Snowball.unreset(event)
defp unreset_event(event, :survival), do: Odinsea.Game.Events.Survival.unreset(event)
defp unreset_event(event, _), do: event
defp start_event_impl(event, :coconut), do: Odinsea.Game.Events.Coconut.start_event(event)
defp start_event_impl(event, :fitness), do: Odinsea.Game.Events.Fitness.start_event(event)
defp start_event_impl(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.start_event(event)
defp start_event_impl(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.start_event(event)
defp start_event_impl(event, :snowball), do: Odinsea.Game.Events.Snowball.start_event(event)
defp start_event_impl(event, :survival), do: Odinsea.Game.Events.Survival.start_event(event)
defp start_event_impl(event, _), do: event
defp finished_event(event, :coconut, character), do: Odinsea.Game.Events.Coconut.finished(event, character)
defp finished_event(event, :fitness, character), do: Odinsea.Game.Events.Fitness.finished(event, character)
defp finished_event(event, :ola_ola, character), do: Odinsea.Game.Events.OlaOla.finished(event, character)
defp finished_event(event, :ox_quiz, character), do: Odinsea.Game.Events.OxQuiz.finished(event, character)
defp finished_event(event, :snowball, character), do: Odinsea.Game.Events.Snowball.finished(event, character)
defp finished_event(event, :survival, character), do: Odinsea.Game.Events.Survival.finished(event, character)
defp finished_event(_, _, _), do: :ok
defp on_map_load_event(event, :coconut, character), do: Odinsea.Game.Events.Coconut.on_map_load(event, character)
defp on_map_load_event(event, :fitness, character), do: Odinsea.Game.Events.Fitness.on_map_load(event, character)
defp on_map_load_event(event, :ola_ola, character), do: Odinsea.Game.Events.OlaOla.on_map_load(event, character)
defp on_map_load_event(event, :ox_quiz, character), do: Odinsea.Game.Events.OxQuiz.on_map_load(event, character)
defp on_map_load_event(event, :snowball, character), do: Odinsea.Game.Events.Snowball.on_map_load(event, character)
defp on_map_load_event(event, :survival, character), do: Odinsea.Game.Events.Survival.on_map_load(event, character)
defp on_map_load_event(_, _, _), do: :ok
defp put_event_base(event, :coconut, base), do: %{event | base: base}
defp put_event_base(event, :fitness, base), do: %{event | base: base}
defp put_event_base(event, :ola_ola, base), do: %{event | base: base}
defp put_event_base(event, :ox_quiz, base), do: %{event | base: base}
defp put_event_base(event, :snowball, base), do: %{event | base: base}
defp put_event_base(event, :survival, base), do: %{event | base: base}
defp put_event_base(event, _, _), do: event
defp schedule_auto_start(channel_id, event_type) do
EventTimer.schedule(
fn ->
send(__MODULE__, {:auto_start, channel_id, event_type})
end,
30_000 # 30 seconds
)
broadcast_server_notice(channel_id, "The event will start in 30 seconds!")
end
defp broadcast_event_notice(channel_id, event_type) do
event_name = Events.display_name(event_type)
broadcast_server_notice(
channel_id,
"Hello! Let's play a #{event_name} event in channel #{channel_id}! " <>
"Change to channel #{channel_id} and use @event command!"
)
end
defp broadcast_server_notice(channel_id, message) do
# In real implementation, broadcast to channel
Logger.info("[Channel #{channel_id}] #{message}")
end
end