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