kimi gone wild
This commit is contained in:
298
lib/odinsea/game/events/fitness.ex
Normal file
298
lib/odinsea/game/events/fitness.ex
Normal file
@@ -0,0 +1,298 @@
|
||||
defmodule Odinsea.Game.Events.Fitness do
|
||||
@moduledoc """
|
||||
Fitness Event - Maple Physical Fitness Test obstacle course.
|
||||
Ported from Java `server.events.MapleFitness`.
|
||||
|
||||
## Gameplay
|
||||
- 4 stage obstacle course that players must navigate
|
||||
- Time limit of 10 minutes
|
||||
- Players who reach the end within time limit get prize
|
||||
- Death during event results in elimination
|
||||
|
||||
## Maps
|
||||
- Stage 1: 109040000 (Start - monkeys throwing bananas)
|
||||
- Stage 2: 109040001 (Stage 2 - monkeys)
|
||||
- Stage 3: 109040002 (Stage 3 - traps)
|
||||
- Stage 4: 109040003 (Stage 4 - last stage)
|
||||
- Finish: 109040004
|
||||
|
||||
## Win Condition
|
||||
- Reach the finish map within 10 minutes
|
||||
- All finishers get prize regardless of order
|
||||
"""
|
||||
|
||||
alias Odinsea.Game.Event
|
||||
alias Odinsea.Game.Timer.EventTimer
|
||||
alias Odinsea.Game.Character
|
||||
|
||||
require Logger
|
||||
|
||||
# ============================================================================
|
||||
# Types
|
||||
# ============================================================================
|
||||
|
||||
@typedoc "Fitness event state"
|
||||
@type t :: %__MODULE__{
|
||||
base: Event.t(),
|
||||
time_started: integer() | nil, # Unix timestamp ms
|
||||
event_duration: non_neg_integer(),
|
||||
schedules: [reference()]
|
||||
}
|
||||
|
||||
defstruct [
|
||||
:base,
|
||||
time_started: nil,
|
||||
event_duration: 600_000, # 10 minutes
|
||||
schedules: []
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Constants
|
||||
# ============================================================================
|
||||
|
||||
@map_ids [109040000, 109040001, 109040002, 109040003, 109040004]
|
||||
@event_duration 600_000 # 10 minutes in ms
|
||||
@message_interval 60_000 # Broadcast messages every minute
|
||||
|
||||
# Message schedule based on time remaining
|
||||
@messages [
|
||||
{10_000, "You have 10 sec left. Those of you unable to beat the game, we hope you beat it next time! Great job everyone!! See you later~"},
|
||||
{110_000, "Alright, you don't have much time remaining. Please hurry up a little!"},
|
||||
{210_000, "The 4th stage is the last one for [The Maple Physical Fitness Test]. Please don't give up at the last minute and try your best. The reward is waiting for you at the very top!"},
|
||||
{310_000, "The 3rd stage offers traps where you may see them, but you won't be able to step on them. Please be careful of them as you make your way up."},
|
||||
{400_000, "For those who have heavy lags, please make sure to move slowly to avoid falling all the way down because of lags."},
|
||||
{500_000, "Please remember that if you die during the event, you'll be eliminated from the game. If you're running out of HP, either take a potion or recover HP first before moving on."},
|
||||
{600_000, "The most important thing you'll need to know to avoid the bananas thrown by the monkeys is *Timing* Timing is everything in this!"},
|
||||
{660_000, "The 2nd stage offers monkeys throwing bananas. Please make sure to avoid them by moving along at just the right timing."},
|
||||
{700_000, "Please remember that if you die during the event, you'll be eliminated from the game. You still have plenty of time left, so either take a potion or recover HP first before moving on."},
|
||||
{780_000, "Everyone that clears [The Maple Physical Fitness Test] on time will be given an item, regardless of the order of finish, so just relax, take your time, and clear the 4 stages."},
|
||||
{840_000, "There may be a heavy lag due to many users at stage 1 all at once. It won't be difficult, so please make sure not to fall down because of heavy lag."},
|
||||
{900_000, "[MapleStory Physical Fitness Test] consists of 4 stages, and if you happen to die during the game, you'll be eliminated from the game, so please be careful of that."}
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Event Implementation
|
||||
# ============================================================================
|
||||
|
||||
@doc """
|
||||
Creates a new Fitness event for the given channel.
|
||||
"""
|
||||
def new(channel_id) do
|
||||
base = Event.new(:fitness, channel_id, @map_ids)
|
||||
|
||||
%__MODULE__{
|
||||
base: base,
|
||||
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,
|
||||
time_started: nil,
|
||||
schedules: []
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cleans up the event after it ends.
|
||||
"""
|
||||
def unreset(%__MODULE__{} = event) do
|
||||
cancel_schedules(event)
|
||||
|
||||
# Close entry portal
|
||||
set_portal_state(event, "join00", false)
|
||||
|
||||
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 Fitness event!")
|
||||
|
||||
# Give prize
|
||||
Event.give_prize(character)
|
||||
|
||||
# Give achievement (ID 20)
|
||||
Character.finish_achievement(character, 20)
|
||||
|
||||
: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 fitness clock to #{character.name}: #{div(time_left, 1000)}s remaining")
|
||||
# Send clock packet with time left
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Starts the fitness event gameplay.
|
||||
"""
|
||||
def start_event(%__MODULE__{} = event) do
|
||||
Logger.info("Starting Fitness event on channel #{event.base.channel_id}")
|
||||
|
||||
now = System.system_time(:millisecond)
|
||||
|
||||
# Open entry portal
|
||||
set_portal_state(event, "join00", true)
|
||||
|
||||
# Broadcast start
|
||||
Event.broadcast_to_event(event.base, :event_started)
|
||||
Event.broadcast_to_event(event.base, {:clock, div(@event_duration, 1000)})
|
||||
|
||||
# Schedule event end
|
||||
end_ref = EventTimer.schedule(
|
||||
fn -> end_event(event) end,
|
||||
@event_duration
|
||||
)
|
||||
|
||||
# Start message broadcasting
|
||||
msg_ref = start_message_schedule(event)
|
||||
|
||||
%__MODULE__{
|
||||
event |
|
||||
time_started: now,
|
||||
schedules: [end_ref, msg_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}), 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 time elapsed in milliseconds.
|
||||
"""
|
||||
def get_time_elapsed(%__MODULE__{time_started: nil}), do: 0
|
||||
def get_time_elapsed(%__MODULE__{time_started: started}) do
|
||||
System.system_time(:millisecond) - started
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a player is eliminated (died during event).
|
||||
"""
|
||||
def eliminated?(character) do
|
||||
# Check if character died while on event maps
|
||||
# This would check character state
|
||||
character.hp <= 0
|
||||
end
|
||||
|
||||
@doc """
|
||||
Eliminates a player from the event.
|
||||
"""
|
||||
def eliminate_player(%__MODULE__{} = event, character) do
|
||||
Logger.info("Player #{character.name} eliminated from Fitness event")
|
||||
|
||||
# Warp player out
|
||||
Event.warp_back(character)
|
||||
|
||||
# Unregister from event
|
||||
base = Event.unregister_player(event.base, character.id)
|
||||
|
||||
%{event | base: base}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Functions
|
||||
# ============================================================================
|
||||
|
||||
defp cancel_schedules(%__MODULE__{schedules: schedules} = event) do
|
||||
Enum.each(schedules, fn ref ->
|
||||
EventTimer.cancel(ref)
|
||||
end)
|
||||
|
||||
%{event | schedules: []}
|
||||
end
|
||||
|
||||
defp start_message_schedule(%__MODULE__{} = event) do
|
||||
# Register recurring task for message broadcasting
|
||||
{:ok, ref} = EventTimer.register(
|
||||
fn -> check_and_broadcast_messages(event) end,
|
||||
@message_interval,
|
||||
0
|
||||
)
|
||||
|
||||
ref
|
||||
end
|
||||
|
||||
defp check_and_broadcast_messages(%__MODULE__{} = event) do
|
||||
time_left = get_time_left(event)
|
||||
|
||||
# Find messages that should be broadcast based on time left
|
||||
messages_to_send = Enum.filter(@messages, fn {threshold, _} ->
|
||||
time_left <= threshold and time_left > threshold - @message_interval
|
||||
end)
|
||||
|
||||
Enum.each(messages_to_send, fn {_, message} ->
|
||||
Event.broadcast_to_event(event.base, {:server_notice, message})
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_portal_state(%__MODULE__{}, _portal_name, _state) do
|
||||
# In real implementation, this would update the map portal state
|
||||
# allowing or preventing players from entering
|
||||
:ok
|
||||
end
|
||||
|
||||
defp end_event(%__MODULE__{} = event) do
|
||||
Logger.info("Fitness event ended on channel #{event.base.channel_id}")
|
||||
|
||||
# Warp out all remaining players
|
||||
# In real implementation:
|
||||
# - Get all players on event maps
|
||||
# - Warp each back to saved location
|
||||
|
||||
# Unreset event
|
||||
unreset(event)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user