defmodule Odinsea.Game.Events.Survival do @moduledoc """ Survival Event - Last-man-standing platform challenge. Ported from Java `server.events.MapleSurvival`. ## Gameplay - Players must navigate platforms without falling - Fall once = elimination - Last players to survive win ## Maps - Stage 1: 809040000 - Stage 2: 809040100 ## Win Condition - Survive until time runs out - Last players standing win """ alias Odinsea.Game.Event alias Odinsea.Game.Timer.EventTimer alias Odinsea.Game.Character require Logger # ============================================================================ # Types # ============================================================================ @typedoc "Survival event state" @type t :: %__MODULE__{ base: Event.t(), time_started: integer() | nil, event_duration: non_neg_integer(), schedules: [reference()] } defstruct [ :base, time_started: nil, event_duration: 360_000, # 6 minutes default schedules: [] ] # ============================================================================ # Constants # ============================================================================ @map_ids [809040000, 809040100] @default_duration 360_000 # 6 minutes in ms # ============================================================================ # Event Implementation # ============================================================================ @doc """ Creates a new Survival event for the given channel. """ def new(channel_id) do base = Event.new(:survival, channel_id, @map_ids) %__MODULE__{ base: base, time_started: nil, event_duration: @default_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) # Close entry portal set_portal_state(event, "join00", false) base = %{event.base | is_running: true, player_count: 0} %__MODULE__{ event | base: base, time_started: nil, schedules: [] } end @doc """ Cleans up the event after it ends. """ def unreset(%__MODULE__{} = event) do cancel_schedules(event) # Open entry portal set_portal_state(event, "join00", true) base = %{event.base | is_running: false, player_count: 0} %__MODULE__{ event | base: base, 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 Survival event!") # Give prize Event.give_prize(character) # Give achievement (ID 25) Character.finish_achievement(character, 25) :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 Survival clock to #{character.name}: #{div(time_left, 1000)}s remaining") # Send clock packet end :ok end @doc """ Starts the Survival event gameplay. """ def start_event(%__MODULE__{} = event) do Logger.info("Starting Survival 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.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.event_duration ) %__MODULE__{ event | time_started: now, schedules: [end_ref] } end @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, event_duration: duration}), do: 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 """ Handles a player falling (elimination). """ def player_fell(%__MODULE__{} = event, character) do Logger.info("Player #{character.name} fell and was eliminated from Survival event") # Warp player out Event.warp_back(character) # Unregister from event base = Event.unregister_player(event.base, character.id) %{event | base: base} end @doc """ Checks if a player position is valid (on platform). Falling below a certain Y coordinate = elimination. """ def valid_position?(%__MODULE__{}, %{y: y}) do # Y threshold for falling (map-specific) y > -500 # Example threshold end # ============================================================================ # Private Functions # ============================================================================ 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("Survival event ended on channel #{event.base.channel_id}") # Warp out all remaining players # In real implementation: # - Get all players on event maps # - Give prizes to survivors # - Warp each back to saved location # Unreset event unreset(event) end end