kimi gone wild
This commit is contained in:
444
lib/odinsea/game/event.ex
Normal file
444
lib/odinsea/game/event.ex
Normal file
@@ -0,0 +1,444 @@
|
||||
defmodule Odinsea.Game.Event do
|
||||
@moduledoc """
|
||||
Base behaviour and common functions for in-game events.
|
||||
Ported from Java `server.events.MapleEvent`.
|
||||
|
||||
Events are scheduled activities that players can participate in for rewards.
|
||||
Each event type has specific gameplay mechanics, maps, and win conditions.
|
||||
|
||||
## Event Lifecycle
|
||||
1. Schedule - Event is scheduled on a channel
|
||||
2. Registration - Players register/join the event
|
||||
3. Start - Event begins with gameplay
|
||||
4. Gameplay - Event-specific mechanics run
|
||||
5. Finish - Winners receive prizes, all players warped out
|
||||
6. Reset - Event state is cleared for next run
|
||||
|
||||
## Implemented Events
|
||||
- Coconut - Team-based coconut hitting competition
|
||||
- Fitness - Obstacle course race (4 stages)
|
||||
- OlaOla - Portal guessing game (3 stages)
|
||||
- OxQuiz - True/False quiz with position-based answers
|
||||
- Snowball - Team snowball rolling competition
|
||||
- Survival - Last-man-standing platform challenge
|
||||
"""
|
||||
|
||||
alias Odinsea.Game.Timer.EventTimer
|
||||
alias Odinsea.Game.Character
|
||||
|
||||
require Logger
|
||||
|
||||
# ============================================================================
|
||||
# Types
|
||||
# ============================================================================
|
||||
|
||||
@typedoc "Event type identifier"
|
||||
@type event_type :: :coconut | :coke_play | :fitness | :ola_ola | :ox_quiz | :survival | :snowball
|
||||
|
||||
@typedoc "Event state struct"
|
||||
@type t :: %__MODULE__{
|
||||
type: event_type(),
|
||||
channel_id: non_neg_integer(),
|
||||
map_ids: [non_neg_integer()],
|
||||
is_running: boolean(),
|
||||
player_count: non_neg_integer(),
|
||||
registered_players: MapSet.t(),
|
||||
schedules: [reference()]
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:type,
|
||||
:channel_id,
|
||||
:map_ids,
|
||||
is_running: false,
|
||||
player_count: 0,
|
||||
registered_players: MapSet.new(),
|
||||
schedules: []
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Behaviour Callbacks
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Called when a player finishes the event (reaches end map).
|
||||
Override to implement finish logic (give prizes, achievements, etc.)
|
||||
"""
|
||||
@callback finished(t(), Character.t()) :: :ok
|
||||
|
||||
@doc """
|
||||
Called to start the event gameplay.
|
||||
Override to implement event start logic (timers, broadcasts, etc.)
|
||||
"""
|
||||
@callback start_event(t()) :: t()
|
||||
|
||||
@doc """
|
||||
Called when a player loads into an event map.
|
||||
Override to send event-specific packets (clock, instructions, etc.)
|
||||
Default implementation sends event instructions.
|
||||
"""
|
||||
@callback on_map_load(t(), Character.t()) :: :ok
|
||||
|
||||
@doc """
|
||||
Resets the event state for a new run.
|
||||
Override to reset event-specific state (scores, stages, etc.)
|
||||
"""
|
||||
@callback reset(t()) :: t()
|
||||
|
||||
@doc """
|
||||
Cleans up the event after it ends.
|
||||
Override to cancel timers and reset state.
|
||||
"""
|
||||
@callback unreset(t()) :: t()
|
||||
|
||||
@doc """
|
||||
Returns the map IDs associated with this event type.
|
||||
"""
|
||||
@callback map_ids() :: [non_neg_integer()]
|
||||
|
||||
# ============================================================================
|
||||
# Behaviour Definition
|
||||
# ============================================================================
|
||||
|
||||
@optional_callbacks [on_map_load: 2]
|
||||
|
||||
# ============================================================================
|
||||
# Common Functions
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Creates a new event struct.
|
||||
"""
|
||||
def new(type, channel_id, map_ids) do
|
||||
%__MODULE__{
|
||||
type: type,
|
||||
channel_id: channel_id,
|
||||
map_ids: map_ids,
|
||||
is_running: false,
|
||||
player_count: 0,
|
||||
registered_players: MapSet.new(),
|
||||
schedules: []
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Increments the player count. If count reaches 250, automatically starts the event.
|
||||
Returns updated event state.
|
||||
"""
|
||||
def increment_player_count(%__MODULE__{} = event) do
|
||||
new_count = event.player_count + 1
|
||||
event = %{event | player_count: new_count}
|
||||
|
||||
if new_count == 250 do
|
||||
Logger.info("Event #{event.type} reached 250 players, auto-starting...")
|
||||
set_event_auto_start(event.channel_id)
|
||||
end
|
||||
|
||||
event
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers a player for the event.
|
||||
"""
|
||||
def register_player(%__MODULE__{} = event, character_id) do
|
||||
%{event | registered_players: MapSet.put(event.registered_players, character_id)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unregisters a player from the event.
|
||||
"""
|
||||
def unregister_player(%__MODULE__{} = event, character_id) do
|
||||
%{event | registered_players: MapSet.delete(event.registered_players, character_id)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a player is registered for the event.
|
||||
"""
|
||||
def registered?(%__MODULE__{} = event, character_id) do
|
||||
MapSet.member?(event.registered_players, character_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the first map ID (entry map) for the event.
|
||||
"""
|
||||
def entry_map_id(%__MODULE__{map_ids: [first | _]}), do: first
|
||||
def entry_map_id(%__MODULE__{map_ids: []}), do: nil
|
||||
|
||||
@doc """
|
||||
Checks if the given map ID is part of this event.
|
||||
"""
|
||||
def event_map?(%__MODULE__{} = event, map_id) do
|
||||
map_id in event.map_ids
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the map index for the given map ID (0-based).
|
||||
Returns nil if map is not part of this event.
|
||||
"""
|
||||
def map_index(%__MODULE__{} = event, map_id) do
|
||||
case Enum.find_index(event.map_ids, &(&1 == map_id)) do
|
||||
nil -> nil
|
||||
index -> index
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Default implementation for on_map_load callback.
|
||||
Sends event instructions to the player if they're on an event map.
|
||||
"""
|
||||
def on_map_load_default(_event, character) do
|
||||
# Send event instructions packet
|
||||
# This would typically show instructions UI
|
||||
# For now, just log
|
||||
Logger.debug("Player #{character.name} loaded event map")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Warps a character back to their saved location or default town.
|
||||
"""
|
||||
def warp_back(character) do
|
||||
# Get saved location or use default (Henesys: 104000000)
|
||||
return_map = character.saved_location || 104000000
|
||||
|
||||
# This would typically call Character.change_map/2
|
||||
# For now, just log
|
||||
Logger.info("Warping player #{character.name} back to map #{return_map}")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gives a random event prize to a character.
|
||||
Prizes include: mesos, cash, vote points, fame, or items.
|
||||
"""
|
||||
def give_prize(character) do
|
||||
reward_type = random_reward_type()
|
||||
|
||||
case reward_type do
|
||||
:meso ->
|
||||
amount = :rand.uniform(9_000_000) + 1_000_000
|
||||
# Character.gain_meso(character, amount)
|
||||
Logger.info("Event prize: #{character.name} gained #{amount} mesos")
|
||||
|
||||
:cash ->
|
||||
amount = :rand.uniform(4000) + 1000
|
||||
# Character.modify_cash_points(character, amount)
|
||||
Logger.info("Event prize: #{character.name} gained #{amount} NX")
|
||||
|
||||
:vote_points ->
|
||||
# Character.add_vote_points(character, 1)
|
||||
Logger.info("Event prize: #{character.name} gained 1 vote point")
|
||||
|
||||
:fame ->
|
||||
# Character.add_fame(character, 10)
|
||||
Logger.info("Event prize: #{character.name} gained 10 fame")
|
||||
|
||||
:none ->
|
||||
Logger.info("Event prize: #{character.name} got no reward")
|
||||
|
||||
{:item, item_id, quantity} ->
|
||||
# Check inventory space and add item
|
||||
Logger.info("Event prize: #{character.name} got item #{item_id} x#{quantity}")
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
# Random reward weights
|
||||
defp random_reward_type do
|
||||
roll = :rand.uniform(100)
|
||||
|
||||
cond do
|
||||
roll <= 25 -> :meso # 25% mesos
|
||||
roll <= 50 -> :cash # 25% cash
|
||||
roll <= 60 -> :vote_points # 10% vote points
|
||||
roll <= 70 -> :fame # 10% fame
|
||||
roll <= 75 -> :none # 5% no reward
|
||||
true -> random_item_reward() # 25% items
|
||||
end
|
||||
end
|
||||
|
||||
defp random_item_reward do
|
||||
# Item pool with quantities
|
||||
items = [
|
||||
{5062000, 1..3}, # Premium Miracle Cube (1-3)
|
||||
{5220000, 1..25}, # Gachapon Ticket (1-25)
|
||||
{4031307, 1..5}, # Piece of Statue (1-5)
|
||||
{5050000, 1..5}, # AP Reset Scroll (1-5)
|
||||
{2022121, 1..10}, # Chewy Rice Cake (1-10)
|
||||
]
|
||||
|
||||
{item_id, qty_range} = Enum.random(items)
|
||||
quantity = Enum.random(qty_range)
|
||||
|
||||
{:item, item_id, quantity}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Schedules the event to auto-start after player count threshold.
|
||||
"""
|
||||
def set_event_auto_start(channel_id) do
|
||||
# Schedule 30 second countdown before start
|
||||
EventTimer.schedule(
|
||||
fn ->
|
||||
broadcast_to_channel(channel_id, "The event will start in 30 seconds!")
|
||||
# Start clock countdown
|
||||
EventTimer.schedule(
|
||||
fn -> start_scheduled_event(channel_id) end,
|
||||
30_000
|
||||
)
|
||||
end,
|
||||
0
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a server notice to all players on a channel.
|
||||
"""
|
||||
def broadcast_to_channel(channel_id, message) do
|
||||
# This would call ChannelServer.broadcast
|
||||
Logger.info("[Channel #{channel_id}] Broadcast: #{message}")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a packet to all players in all event maps.
|
||||
"""
|
||||
def broadcast_to_event(%__MODULE__{} = event, _packet) do
|
||||
# This would broadcast to all maps in event.map_ids
|
||||
Logger.debug("Broadcasting to event #{event.type} on channel #{event.channel_id}")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles when a player loads into any map.
|
||||
Checks if they're on an event map and calls appropriate callbacks.
|
||||
"""
|
||||
def on_map_load(events, character, map_id, channel_id) do
|
||||
Enum.each(events, fn {event_type, event} ->
|
||||
if event.channel_id == channel_id and event.is_running do
|
||||
if map_id == 109050000 do
|
||||
# Finished map - call finished callback
|
||||
apply(event_module(event_type), :finished, [event, character])
|
||||
end
|
||||
|
||||
if event_map?(event, map_id) do
|
||||
# Event map - call on_map_load callback
|
||||
if function_exported?(event_module(event_type), :on_map_load, 2) do
|
||||
apply(event_module(event_type), :on_map_load, [event, character])
|
||||
else
|
||||
on_map_load_default(event, character)
|
||||
end
|
||||
|
||||
# If first map, increment player count
|
||||
if map_index(event, map_id) == 0 do
|
||||
increment_player_count(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles manual event start command from a GM.
|
||||
"""
|
||||
def on_start_event(events, character, map_id) do
|
||||
Enum.each(events, fn {event_type, event} ->
|
||||
if event.is_running and event_map?(event, map_id) do
|
||||
new_event = apply(event_module(event_type), :start_event, [event])
|
||||
set_event(character.channel_id, -1)
|
||||
Logger.info("GM #{character.name} started event #{event_type}")
|
||||
new_event
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Schedules an event to run on a channel.
|
||||
Returns {:ok, updated_events} or {:error, reason}.
|
||||
"""
|
||||
def schedule_event(events, event_type, channel_id) do
|
||||
event = Map.get(events, event_type)
|
||||
|
||||
cond do
|
||||
is_nil(event) ->
|
||||
{:error, "Event type not found"}
|
||||
|
||||
event.is_running ->
|
||||
{:error, "The event is already running."}
|
||||
|
||||
true ->
|
||||
# Check if maps have players (simplified check)
|
||||
# In real implementation, check all map_ids
|
||||
entry_map = entry_map_id(event)
|
||||
|
||||
# Reset and activate event
|
||||
event = apply(event_module(event_type), :reset, [event])
|
||||
event = %{event | is_running: true}
|
||||
|
||||
# Broadcast to channel
|
||||
event_name = humanize_event_name(event_type)
|
||||
broadcast_to_channel(
|
||||
channel_id,
|
||||
"Hello! Let's play a #{event_name} event in channel #{channel_id}! " <>
|
||||
"Change to channel #{channel_id} and use @event command!"
|
||||
)
|
||||
|
||||
{:ok, Map.put(events, event_type, event)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the channel's active event map.
|
||||
"""
|
||||
def set_event(channel_id, map_id) do
|
||||
# This would update ChannelServer state
|
||||
Logger.debug("Set channel #{channel_id} event map to #{map_id}")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancels all scheduled timers for an event.
|
||||
"""
|
||||
def cancel_schedules(%__MODULE__{schedules: schedules} = event) do
|
||||
Enum.each(schedules, fn ref ->
|
||||
EventTimer.cancel(ref)
|
||||
end)
|
||||
|
||||
%{event | schedules: []}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a schedule reference to the event.
|
||||
"""
|
||||
def add_schedule(%__MODULE__{} = event, schedule_ref) do
|
||||
%{event | schedules: [schedule_ref | event.schedules]}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Functions
|
||||
# ============================================================================
|
||||
|
||||
defp start_scheduled_event(_channel_id) do
|
||||
# Find the scheduled event and start it
|
||||
Logger.info("Auto-starting scheduled event")
|
||||
:ok
|
||||
end
|
||||
|
||||
defp event_module(:coconut), do: Odinsea.Game.Events.Coconut
|
||||
defp event_module(:fitness), do: Odinsea.Game.Events.Fitness
|
||||
defp event_module(:ola_ola), do: Odinsea.Game.Events.OlaOla
|
||||
defp event_module(:ox_quiz), do: Odinsea.Game.Events.OxQuiz
|
||||
defp event_module(:snowball), do: Odinsea.Game.Events.Snowball
|
||||
defp event_module(:survival), do: Odinsea.Game.Events.Survival
|
||||
defp event_module(_), do: nil
|
||||
|
||||
defp humanize_event_name(type) do
|
||||
type
|
||||
|> Atom.to_string()
|
||||
|> String.replace("_", " ")
|
||||
|> String.split()
|
||||
|> Enum.map(&String.capitalize/1)
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user