defmodule Odinsea.Game.Events.OlaOla do @moduledoc """ Ola Ola Event - Portal guessing game (similar to Survival but with portals). Ported from Java `server.events.MapleOla`. ## Gameplay - 3 stages with random correct portals - Players must guess which portal leads forward - Wrong portals send players back or eliminate them - Fastest to finish wins ## Maps - Stage 1: 109030001 (5 portals: ch00-ch04) - Stage 2: 109030002 (8 portals: ch00-ch07) - Stage 3: 109030003 (16 portals: ch00-ch15) ## Win Condition - Reach the finish map by choosing correct portals - First to finish gets best prize, all finishers get prize """ alias Odinsea.Game.Event alias Odinsea.Game.Timer.EventTimer alias Odinsea.Game.Character require Logger # ============================================================================ # Types # ============================================================================ @typedoc "OlaOla event state" @type t :: %__MODULE__{ base: Event.t(), stages: [non_neg_integer()], # Correct portal indices for each stage time_started: integer() | nil, event_duration: non_neg_integer(), schedules: [reference()] } defstruct [ :base, stages: [0, 0, 0], # Will be randomized on start time_started: nil, event_duration: 360_000, # 6 minutes schedules: [] ] # ============================================================================ # Constants # ============================================================================ @map_ids [109030001, 109030002, 109030003] @event_duration 360_000 # 6 minutes in ms # Stage configurations @stage_config [ %{map: 109030001, portals: 5, prefix: "ch"}, # Stage 1: 5 portals %{map: 109030002, portals: 8, prefix: "ch"}, # Stage 2: 8 portals %{map: 109030003, portals: 16, prefix: "ch"} # Stage 3: 16 portals ] # ============================================================================ # Event Implementation # ============================================================================ @doc """ Creates a new OlaOla event for the given channel. """ def new(channel_id) do base = Event.new(:ola_ola, channel_id, @map_ids) %__MODULE__{ base: base, stages: [0, 0, 0], time_started: nil, event_duration: @event_duration, schedules: [] } end @doc """ Returns the map IDs for this event type. """ def map_ids, do: @map_ids @doc """ Resets the event state for a new game. """ def reset(%__MODULE__{} = event) do # Cancel existing schedules cancel_schedules(event) base = %{event.base | is_running: true, player_count: 0} %__MODULE__{ event | base: base, stages: [0, 0, 0], time_started: nil, schedules: [] } end @doc """ Cleans up the event after it ends. Randomizes correct portals for next game. """ def unreset(%__MODULE__{} = event) do cancel_schedules(event) # Randomize correct portals for each stage stages = [ random_stage_portal(0), # Stage 1: 0-4 random_stage_portal(1), # Stage 2: 0-7 random_stage_portal(2) # Stage 3: 0-15 ] # Hack check: stage 1 portal 2 is inaccessible stages = if Enum.at(stages, 0) == 2 do List.replace_at(stages, 0, 3) else stages end # Open entry portal set_portal_state(event, "join00", true) base = %{event.base | is_running: false, player_count: 0} %__MODULE__{ event | base: base, stages: stages, time_started: nil, schedules: [] } end @doc """ Called when a player finishes (reaches end map). Gives prize and achievement. """ def finished(%__MODULE__{} = event, character) do Logger.info("Player #{character.name} finished Ola Ola event!") # Give prize Event.give_prize(character) # Give achievement (ID 21) Character.finish_achievement(character, 21) :ok end @doc """ Called when a player loads into an event map. Sends clock if timer is running. """ def on_map_load(%__MODULE__{} = event, character) do if is_timer_started(event) do time_left = get_time_left(event) Logger.debug("Sending Ola Ola clock to #{character.name}: #{div(time_left, 1000)}s remaining") end :ok end @doc """ Starts the Ola Ola event gameplay. """ def start_event(%__MODULE__{} = event) do Logger.info("Starting Ola Ola event on channel #{event.base.channel_id}") now = System.system_time(:millisecond) # Close entry portal set_portal_state(event, "join00", false) # Broadcast start Event.broadcast_to_event(event.base, :event_started) Event.broadcast_to_event(event.base, {:clock, div(@event_duration, 1000)}) Event.broadcast_to_event(event.base, {:server_notice, "The portal has now opened. Press the up arrow key at the portal to enter."}) Event.broadcast_to_event(event.base, {:server_notice, "Fall down once, and never get back up again! Get to the top without falling down!"}) # Schedule event end end_ref = EventTimer.schedule( fn -> end_event(event) end, @event_duration ) %__MODULE__{ event | time_started: now, schedules: [end_ref] } end @doc """ Checks if a character chose the correct portal for their current stage. ## Parameters - event: The OlaOla event state - portal_name: The portal name (e.g., "ch00", "ch05") - map_id: Current map ID ## Returns - true if correct portal - false if wrong portal """ def correct_portal?(%__MODULE__{stages: stages}, portal_name, map_id) do # Get stage index from map ID stage_index = get_stage_index(map_id) if stage_index == nil do false else # Get correct portal for this stage correct = Enum.at(stages, stage_index) # Format correct portal name correct_name = format_portal_name(correct) portal_name == correct_name end end @doc """ Gets the correct portal name for a stage. """ def get_correct_portal(%__MODULE__{stages: stages}, stage_index) when stage_index in 0..2 do correct = Enum.at(stages, stage_index) format_portal_name(correct) end def get_correct_portal(_, _), do: nil @doc """ Checks if the timer has started. """ def is_timer_started(%__MODULE__{time_started: nil}), do: false def is_timer_started(%__MODULE__{}), do: true @doc """ Gets the total event duration in milliseconds. """ def get_time(%__MODULE__{event_duration: duration}), do: duration @doc """ Gets the time remaining in milliseconds. """ def get_time_left(%__MODULE__{time_started: nil}), do: @event_duration def get_time_left(%__MODULE__{time_started: started, event_duration: duration}) do elapsed = System.system_time(:millisecond) - started max(0, duration - elapsed) end @doc """ Gets the current stage (0-2) for a map ID. """ def get_stage_index(map_id) do Enum.find_index(@map_ids, &(&1 == map_id)) end @doc """ Gets the stage configuration. """ def stage_config, do: @stage_config @doc """ Handles a player attempting to use a portal. Returns {:ok, destination_map} for correct portal, :error for wrong portal. """ def attempt_portal(%__MODULE__{} = event, portal_name, current_map_id) do if correct_portal?(event, portal_name, current_map_id) do # Correct portal - advance to next stage stage = get_stage_index(current_map_id) if stage < 2 do next_map = Enum.at(@map_ids, stage + 1) {:ok, next_map} else # Finished all stages {:finished, 109050000} # Finish map end else # Wrong portal - fail :error end end # ============================================================================ # Private Functions # ============================================================================ defp random_stage_portal(stage_index) do portal_count = Enum.at(@stage_config, stage_index).portals :rand.uniform(portal_count) - 1 # 0-based end defp format_portal_name(portal_num) do # Format as ch00, ch01, etc. if portal_num < 10 do "ch0#{portal_num}" else "ch#{portal_num}" end end defp cancel_schedules(%__MODULE__{schedules: schedules} = event) do Enum.each(schedules, fn ref -> EventTimer.cancel(ref) end) %{event | schedules: []} end defp set_portal_state(%__MODULE__{}, _portal_name, _state) do # In real implementation, update map portal state :ok end defp end_event(%__MODULE__{} = event) do Logger.info("Ola Ola event ended on channel #{event.base.channel_id}") # Warp out all remaining players # In real implementation, get all players on event maps and warp them # Unreset event unreset(event) end end