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

394 lines
9.7 KiB
Elixir

defmodule Odinsea.Game.Events.Coconut do
@moduledoc """
Coconut Event - Team-based coconut hitting competition.
Ported from Java `server.events.MapleCoconut`.
## Gameplay
- Two teams (Maple vs Story) compete to hit coconuts
- Coconuts spawn and fall when hit
- Team with most hits at end wins
- 5 minute time limit with potential 1 minute bonus time
## Map
- Single map: 109080000
## Win Condition
- Team with higher score after 5 minutes wins
- If tied, 1 minute bonus time is awarded
- If still tied after bonus, no winner
"""
alias Odinsea.Game.Event
alias Odinsea.Game.Timer.EventTimer
require Logger
# ============================================================================
# Types
# ============================================================================
@typedoc "Coconut struct representing a single coconut"
@type coconut :: %{
id: non_neg_integer(),
hits: non_neg_integer(),
hittable: boolean(),
stopped: boolean(),
hit_time: integer() # Unix timestamp ms
}
@typedoc "Coconut event state"
@type t :: %__MODULE__{
base: Event.t(),
coconuts: [coconut()],
maple_score: non_neg_integer(), # Team 0
story_score: non_neg_integer(), # Team 1
count_bombing: non_neg_integer(),
count_falling: non_neg_integer(),
count_stopped: non_neg_integer(),
schedules: [reference()]
}
defstruct [
:base,
coconuts: [],
maple_score: 0,
story_score: 0,
count_bombing: 80,
count_falling: 401,
count_stopped: 20,
schedules: []
]
# ============================================================================
# Constants
# ============================================================================
@map_ids [109080000]
@event_duration 300_000 # 5 minutes in ms
@bonus_duration 60_000 # 1 minute bonus time
@total_coconuts 506
@warp_out_delay 10_000 # 10 seconds after game end
# ============================================================================
# Event Implementation
# ============================================================================
@doc """
Creates a new Coconut event for the given channel.
"""
def new(channel_id) do
base = Event.new(:coconut, channel_id, @map_ids)
%__MODULE__{
base: base,
coconuts: initialize_coconuts(),
maple_score: 0,
story_score: 0,
count_bombing: 80,
count_falling: 401,
count_stopped: 20,
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
base = %{event.base | is_running: true, player_count: 0}
%__MODULE__{
event |
base: base,
coconuts: initialize_coconuts(),
maple_score: 0,
story_score: 0,
count_bombing: 80,
count_falling: 401,
count_stopped: 20,
schedules: []
}
end
@doc """
Cleans up the event after it ends.
"""
def unreset(%__MODULE__{} = event) do
# Cancel all schedules
Event.cancel_schedules(event.base)
base = %{event.base | is_running: false, player_count: 0}
%__MODULE__{
event |
base: base,
coconuts: [],
maple_score: 0,
story_score: 0,
schedules: []
}
end
@doc """
Called when a player finishes (reaches end map).
Coconut event doesn't use this - winners determined by time.
"""
def finished(_event, _character) do
:ok
end
@doc """
Called when a player loads into the event map.
Sends coconut score packet.
"""
def on_map_load(%__MODULE__{} = event, character) do
# Send coconut score packet
Logger.debug("Sending coconut score to #{character.name}: Maple #{event.maple_score}, Story #{event.story_score}")
# In real implementation: send packet with scores
# Packet format: coconutScore(maple_score, story_score)
:ok
end
@doc """
Starts the coconut event gameplay.
"""
def start_event(%__MODULE__{} = event) do
Logger.info("Starting Coconut event on channel #{event.base.channel_id}")
# Set coconuts hittable
event = set_hittable(event, true)
# Broadcast event start
Event.broadcast_to_event(event.base, :event_started)
Event.broadcast_to_event(event.base, :hit_coconut)
# Start 5-minute countdown
Event.broadcast_to_event(event.base, {:clock, div(@event_duration, 1000)})
# Schedule end check
schedule_ref = EventTimer.schedule(
fn -> check_winner(event) end,
@event_duration
)
%{event | schedules: [schedule_ref]}
end
@doc """
Gets a coconut by ID.
Returns nil if ID is out of range.
"""
def get_coconut(%__MODULE__{coconuts: coconuts}, id) when id >= 0 and id < length(coconuts) do
Enum.at(coconuts, id)
end
def get_coconut(_, _), do: nil
@doc """
Returns all coconuts.
"""
def get_all_coconuts(%__MODULE__{coconuts: coconuts}), do: coconuts
@doc """
Sets whether coconuts are hittable.
"""
def set_hittable(%__MODULE__{coconuts: coconuts} = event, hittable) do
updated_coconuts = Enum.map(coconuts, fn coconut ->
%{coconut | hittable: hittable}
end)
%{event | coconuts: updated_coconuts}
end
@doc """
Gets the number of available bombings.
"""
def get_bombings(%__MODULE__{count_bombing: count}), do: count
@doc """
Decrements bombing count.
"""
def bomb_coconut(%__MODULE__{count_bombing: count} = event) do
%{event | count_bombing: max(0, count - 1)}
end
@doc """
Gets the number of falling coconuts available.
"""
def get_falling(%__MODULE__{count_falling: count}), do: count
@doc """
Decrements falling count.
"""
def fall_coconut(%__MODULE__{count_falling: count} = event) do
%{event | count_falling: max(0, count - 1)}
end
@doc """
Gets the number of stopped coconuts.
"""
def get_stopped(%__MODULE__{count_stopped: count}), do: count
@doc """
Decrements stopped count.
"""
def stop_coconut(%__MODULE__{count_stopped: count} = event) do
%{event | count_stopped: max(0, count - 1)}
end
@doc """
Gets the current scores [maple, story].
"""
def get_coconut_score(%__MODULE__{} = event) do
[event.maple_score, event.story_score]
end
@doc """
Gets Team Maple score.
"""
def get_maple_score(%__MODULE__{maple_score: score}), do: score
@doc """
Gets Team Story score.
"""
def get_story_score(%__MODULE__{story_score: score}), do: score
@doc """
Adds a point to Team Maple.
"""
def add_maple_score(%__MODULE__{maple_score: score} = event) do
%{event | maple_score: score + 1}
end
@doc """
Adds a point to Team Story.
"""
def add_story_score(%__MODULE__{story_score: score} = event) do
%{event | story_score: score + 1}
end
@doc """
Records a hit on a coconut.
"""
def hit_coconut(%__MODULE__{coconuts: coconuts} = event, coconut_id, team) do
now = System.system_time(:millisecond)
updated_coconuts = List.update_at(coconuts, coconut_id, fn coconut ->
%{coconut |
hits: coconut.hits + 1,
hit_time: now + 1000 # 1 second cooldown
}
end)
# Add score to appropriate team
event = %{event | coconuts: updated_coconuts}
event = case team do
0 -> add_maple_score(event)
1 -> add_story_score(event)
_ -> event
end
event
end
# ============================================================================
# Private Functions
# ============================================================================
defp initialize_coconuts do
Enum.map(0..(@total_coconuts - 1), fn id ->
%{
id: id,
hits: 0,
hittable: false,
stopped: false,
hit_time: 0
}
end)
end
defp check_winner(%__MODULE__{} = event) do
if get_maple_score(event) == get_story_score(event) do
# Tie - bonus time
bonus_time(event)
else
# We have a winner
winner_team = if get_maple_score(event) > get_story_score(event), do: 0, else: 1
end_game(event, winner_team)
end
end
defp bonus_time(%__MODULE__{} = event) do
Logger.info("Coconut event tied! Starting bonus time...")
# Broadcast bonus time
Event.broadcast_to_event(event.base, {:clock, div(@bonus_duration, 1000)})
# Schedule final check
EventTimer.schedule(
fn ->
if get_maple_score(event) == get_story_score(event) do
# Still tied - no winner
end_game_no_winner(event)
else
winner_team = if get_maple_score(event) > get_story_score(event), do: 0, else: 1
end_game(event, winner_team)
end
end,
@bonus_duration
)
end
defp end_game(%__MODULE__{} = event, winner_team) do
team_name = if winner_team == 0, do: "Maple", else: "Story"
Logger.info("Coconut event ended! Team #{team_name} wins!")
# Broadcast winner
Event.broadcast_to_event(event.base, {:victory, winner_team})
# Schedule warp out
EventTimer.schedule(
fn -> warp_out(event, winner_team) end,
@warp_out_delay
)
end
defp end_game_no_winner(%__MODULE__{} = event) do
Logger.info("Coconut event ended with no winner (tie)")
# Broadcast no winner
Event.broadcast_to_event(event.base, :no_winner)
# Schedule warp out
EventTimer.schedule(
fn -> warp_out(event, nil) end,
@warp_out_delay
)
end
defp warp_out(%__MODULE__{} = event, winner_team) do
# Make coconuts unhittable
event = set_hittable(event, false)
# Give prizes to winners, warp everyone back
# In real implementation:
# - Get all characters on map
# - For each character:
# - If on winning team, give prize
# - Warp back to saved location
Logger.info("Warping out all players from coconut event")
# Unreset event
unreset(event)
end
end