607 lines
20 KiB
Elixir
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
|