kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View 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