394 lines
9.7 KiB
Elixir
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
|