defmodule Odinsea.Game.Events.Snowball do @moduledoc """ Snowball Event - Team-based snowball rolling competition. Ported from Java `server.events.MapleSnowball`. ## Gameplay - Two teams (Story and Maple) compete to roll snowballs - Players hit snowballs to move them forward - Snowmen block the opposing team's snowball - First team to reach position 899 wins ## Maps - Single map: 109060000 ## Teams - Team 0 (Story): Bottom snowball, y > -80 - Team 1 (Maple): Top snowball, y <= -80 ## Win Condition - First team to push snowball past position 899 wins """ alias Odinsea.Game.Event alias Odinsea.Game.Timer.EventTimer require Logger # ============================================================================ # Types # ============================================================================ @typedoc "Snowball state struct" @type snowball :: %{ team: 0 | 1, position: non_neg_integer(), # 0-899 start_point: non_neg_integer(), # Stage progress invis: boolean(), hittable: boolean(), snowman_hp: non_neg_integer(), schedule: reference() | nil } @typedoc "Snowball event state" @type t :: %__MODULE__{ base: Event.t(), snowballs: %{0 => snowball(), 1 => snowball()}, game_active: boolean() } defstruct [ :base, snowballs: %{}, game_active: false ] # ============================================================================ # Constants # ============================================================================ @map_ids [109060000] @finish_position 899 @snowman_max_hp 7500 @snowman_invincible_time 10_000 # 10 seconds # Stage positions @stage_positions [255, 511, 767] # Damage values @damage_normal 10 @damage_snowman 15 @damage_snowman_crit 45 @damage_snowman_miss 0 # ============================================================================ # Event Implementation # ============================================================================ @doc """ Creates a new Snowball event for the given channel. """ def new(channel_id) do base = Event.new(:snowball, channel_id, @map_ids) %__MODULE__{ base: base, snowballs: %{}, game_active: false } 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} # Initialize snowballs for both teams snowballs = %{ 0 => create_snowball(0), 1 => create_snowball(1) } %__MODULE__{ event | base: base, snowballs: snowballs, game_active: false } end @doc """ Cleans up the event after it ends. """ def unreset(%__MODULE__{} = event) do # Cancel all snowball schedules Enum.each(event.snowballs, fn {_, ball} -> if ball.schedule do EventTimer.cancel(ball.schedule) end end) base = %{event.base | is_running: false, player_count: 0} %__MODULE__{ event | base: base, snowballs: %{}, game_active: false } end @doc """ Called when a player finishes. Snowball doesn't use this - winner determined by position. """ def finished(_event, _character) do :ok end @doc """ Starts the snowball event gameplay. """ def start_event(%__MODULE__{} = event) do Logger.info("Starting Snowball event on channel #{event.base.channel_id}") # Initialize snowballs snowballs = %{ 0 => %{create_snowball(0) | invis: false, hittable: true}, 1 => %{create_snowball(1) | invis: false, hittable: true} } event = %{event | snowballs: snowballs, game_active: true} # Broadcast start Event.broadcast_to_event(event.base, :event_started) Event.broadcast_to_event(event.base, :enter_snowball) # Broadcast initial snowball state broadcast_snowball_update(event, 0) broadcast_snowball_update(event, 1) event end @doc """ Gets a snowball by team. """ def get_snowball(%__MODULE__{snowballs: balls}, team) when team in [0, 1] do Map.get(balls, team) end def get_snowball(_, _), do: nil @doc """ Gets both snowballs. """ def get_all_snowballs(%__MODULE__{snowballs: balls}), do: balls @doc """ Handles a player hitting a snowball. ## Parameters - event: Snowball event state - character: The character hitting - position: Character position %{x, y} """ def hit_snowball(%__MODULE__{game_active: false}, _, _) do # Game not active :ok end def hit_snowball(%__MODULE__{} = event, character, %{x: x, y: y}) do # Determine team based on Y position team = if y > -80, do: 0, else: 1 ball = get_snowball(event, team) if ball == nil or ball.invis do :ok else # Check if hitting snowman or snowball snowman = x < -360 and x > -560 if not snowman do # Hitting the snowball handle_snowball_hit(event, ball, character, x) else # Hitting the snowman handle_snowman_hit(event, ball, team) end end end @doc """ Updates a snowball's position. """ def update_position(%__MODULE__{} = event, team, new_position) when team in [0, 1] do ball = get_snowball(event, team) if ball do updated_ball = %{ball | position: new_position} snowballs = Map.put(event.snowballs, team, updated_ball) # Check for stage transitions if new_position in @stage_positions do updated_ball = %{updated_ball | start_point: updated_ball.start_point + 1} broadcast_message(event, team, updated_ball.start_point) end # Check for finish if new_position >= @finish_position do end_game(event, team) else broadcast_roll(event) %{event | snowballs: snowballs} end else event end end @doc """ Sets a snowball's hittable state. """ def set_hittable(%__MODULE__{} = event, team, hittable) when team in [0, 1] do ball = get_snowball(event, team) if ball do updated_ball = %{ball | hittable: hittable} snowballs = Map.put(event.snowballs, team, updated_ball) %{event | snowballs: snowballs} else event end end @doc """ Sets a snowball's visibility. """ def set_invis(%__MODULE__{} = event, team, invis) when team in [0, 1] do ball = get_snowball(event, team) if ball do updated_ball = %{ball | invis: invis} snowballs = Map.put(event.snowballs, team, updated_ball) %{event | snowballs: snowballs} else event end end # ============================================================================ # Private Functions # ============================================================================ defp create_snowball(team) do %{ team: team, position: 0, start_point: 0, invis: true, hittable: true, snowman_hp: @snowman_max_hp, schedule: nil } end defp handle_snowball_hit(event, ball, character, char_x) do # Calculate damage damage = calculate_snowball_damage(ball, char_x) if damage > 0 and ball.hittable do # Move snowball new_position = ball.position + 1 update_position(event, ball.team, new_position) else # Knockback chance (20%) if :rand.uniform() < 0.2 do # Send knockback packet send_knockback(character) end end :ok end defp handle_snowman_hit(event, ball, team) do # Calculate damage roll = :rand.uniform() damage = cond do roll < 0.05 -> @damage_snowman_crit # 5% crit roll < 0.35 -> @damage_snowman_miss # 30% miss true -> @damage_snowman # 65% normal end if damage > 0 do new_hp = ball.snowman_hp - damage if new_hp <= 0 do # Snowman destroyed - make enemy ball unhittable new_hp = @snowman_max_hp enemy_team = if team == 0, do: 1, else: 0 event = set_hittable(event, enemy_team, false) # Broadcast message broadcast_message(event, enemy_team, 4) # Schedule re-hittable schedule_ref = EventTimer.schedule( fn -> set_hittable(event, enemy_team, true) broadcast_message(event, enemy_team, 5) end, @snowman_invincible_time ) # Update ball with schedule enemy_ball = get_snowball(event, enemy_team) if enemy_ball do updated_enemy = %{enemy_ball | schedule: schedule_ref} snowballs = Map.put(event.snowballs, enemy_team, updated_enemy) event = %{event | snowballs: snowballs} end # Apply seduce debuff to enemy team apply_seduce(event, enemy_team) end # Update snowman HP updated_ball = %{ball | snowman_hp: new_hp} snowballs = Map.put(event.snowballs, team, updated_ball) %{event | snowballs: snowballs} end :ok end defp calculate_snowball_damage(ball, char_x) do left_x = get_left_x(ball) right_x = get_right_x(ball) # 1% chance for damage, or if in hit zone if :rand.uniform() < 0.01 or (char_x > left_x and char_x < right_x) do @damage_normal else 0 end end defp get_left_x(%{position: pos}) do pos * 3 + 175 end defp get_right_x(ball) do get_left_x(ball) + 275 end defp broadcast_snowball_update(%__MODULE__{} = event, team) do ball = get_snowball(event, team) if ball do # Broadcast snowball state Event.broadcast_to_event(event.base, {:snowball_message, team, ball.start_point}) end end defp broadcast_message(%__MODULE__{} = event, team, message) do Event.broadcast_to_event(event.base, {:snowball_message, team, message}) end defp broadcast_roll(%__MODULE__{} = event) do ball0 = get_snowball(event, 0) ball1 = get_snowball(event, 1) Event.broadcast_to_event(event.base, {:roll_snowball, ball0, ball1}) end defp send_knockback(_character) do # Send knockback packet to character :ok end defp apply_seduce(_event, _team) do # Apply seduce debuff to enemy team # This would use MobSkillFactory to apply debuff :ok end defp end_game(%__MODULE__{} = event, winner_team) do team_name = if winner_team == 0, do: "Story", else: "Maple" Logger.info("Snowball event ended! Team #{team_name} wins!") # Make both snowballs invisible event = set_invis(event, 0, true) event = set_invis(event, 1, true) # Broadcast winner Event.broadcast_to_event( event.base, {:server_notice, "Congratulations! Team #{team_name} has won the Snowball Event!"} ) # Give prizes to winners # In real implementation: # - Get all players on map # - Winners (based on Y position) get prize # - Everyone gets warped back # Unreset event unreset(%{event | game_active: false}) end end