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