Files
odinsea-elixir/lib/odinsea/game/events/survival.ex
2026-02-14 23:12:33 -07:00

248 lines
6.3 KiB
Elixir

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