kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

444
lib/odinsea/game/event.ex Normal file
View 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