kimi gone wild
This commit is contained in:
437
lib/odinsea/game/events/snowball.ex
Normal file
437
lib/odinsea/game/events/snowball.ex
Normal file
@@ -0,0 +1,437 @@
|
||||
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
|
||||
Reference in New Issue
Block a user