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

@@ -11,7 +11,7 @@ defmodule Odinsea.Game.Character do
alias Odinsea.Database.Schema.Character, as: CharacterDB
alias Odinsea.Game.Map, as: GameMap
alias Odinsea.Game.{Inventory, InventoryType}
alias Odinsea.Game.{Inventory, InventoryType, Pet}
alias Odinsea.Net.Packet.Out
# ============================================================================
@@ -92,6 +92,8 @@ defmodule Odinsea.Game.Character do
:skin_color,
:hair,
:face,
# GM Level (0 = normal player, >0 = GM)
:gm,
# Stats
:stats,
# Position & Map
@@ -131,6 +133,7 @@ defmodule Odinsea.Game.Character do
skin_color: byte(),
hair: non_neg_integer(),
face: non_neg_integer(),
gm: non_neg_integer(),
stats: Stats.t(),
map_id: non_neg_integer(),
position: Position.t(),
@@ -513,6 +516,7 @@ defmodule Odinsea.Game.Character do
skin_color: db_char.skin_color,
hair: db_char.hair,
face: db_char.face,
gm: db_char.gm,
stats: stats,
map_id: db_char.map_id,
position: position,
@@ -592,10 +596,258 @@ defmodule Odinsea.Game.Character do
# Save character base data
result = Odinsea.Database.Context.update_character(state.character_id, attrs)
# Save inventories
Odinsea.Database.Context.save_character_inventory(state.character_id, state.inventories)
result
end
@doc """
Gives EXP to the character.
Handles level-up, EXP calculation, and packet broadcasting.
"""
def gain_exp(character_pid, exp_amount, is_highest_damage \\ false) when is_pid(character_pid) do
GenServer.cast(character_pid, {:gain_exp, exp_amount, is_highest_damage})
end
# ============================================================================
# Pet API
# ============================================================================
@doc """
Gets a pet by slot index (0-2 for the 3 pet slots).
"""
def get_pet(character_id, slot) do
GenServer.call(via_tuple(character_id), {:get_pet, slot})
end
@doc """
Gets all summoned pets.
"""
def get_summoned_pets(character_id) do
GenServer.call(via_tuple(character_id), :get_summoned_pets)
end
@doc """
Spawns a pet from inventory to the map.
"""
def spawn_pet(character_id, inventory_slot, lead \\ false) do
GenServer.call(via_tuple(character_id), {:spawn_pet, inventory_slot, lead})
end
@doc """
Despawns a pet from the map.
"""
def despawn_pet(character_id, slot) do
GenServer.call(via_tuple(character_id), {:despawn_pet, slot})
end
@doc """
Updates a pet's data (after feeding, command, etc.).
"""
def update_pet(character_id, pet) do
GenServer.cast(via_tuple(character_id), {:update_pet, pet})
end
@doc """
Updates a pet's position.
"""
def update_pet_position(character_id, slot, position) do
GenServer.cast(via_tuple(character_id), {:update_pet_position, slot, position})
end
def gain_exp(character_id, exp_amount, is_highest_damage) when is_integer(character_id) do
case Registry.lookup(Odinsea.CharacterRegistry, character_id) do
[{pid, _}] -> gain_exp(pid, exp_amount, is_highest_damage)
[] -> {:error, :character_not_found}
end
end
# ============================================================================
# GenServer Callbacks - EXP Gain
# ============================================================================
@impl true
def handle_cast({:gain_exp, exp_amount, is_highest_damage}, state) do
# Calculate EXP needed for next level
exp_needed = calculate_exp_needed(state.level)
# Add EXP
new_exp = state.exp + exp_amount
# Check for level up
{new_state, leveled_up} =
if new_exp >= exp_needed and state.level < 200 do
# Level up!
new_level = state.level + 1
# Calculate stat gains (simple formula for now)
# TODO: Use job-specific stat gain formulas
hp_gain = 10 + div(state.stats.str, 10)
mp_gain = 5 + div(state.stats.int, 10)
new_stats = %{
state.stats
| max_hp: state.stats.max_hp + hp_gain,
max_mp: state.stats.max_mp + mp_gain,
hp: state.stats.max_hp + hp_gain,
mp: state.stats.max_mp + mp_gain
}
updated_state = %{
state
| level: new_level,
exp: new_exp - exp_needed,
stats: new_stats,
remaining_ap: state.remaining_ap + 5
}
Logger.info("Character #{state.name} leveled up to #{new_level}!")
# TODO: Send level-up packet to client
# TODO: Broadcast level-up effect to map
{updated_state, true}
else
{%{state | exp: new_exp}, false}
end
# TODO: Send EXP gain packet to client
# TODO: If highest damage, send bonus message
Logger.debug(
"Character #{state.name} gained #{exp_amount} EXP (total: #{new_state.exp}, level: #{new_state.level})"
)
{:noreply, new_state}
end
# ============================================================================
# GenServer Callbacks - Pet Functions
# ============================================================================
@impl true
def handle_call({:get_pet, slot}, _from, state) do
# Find pet by slot (1, 2, or 3)
pet = Enum.find(state.pets, fn p -> p.summoned == slot end)
if pet do
{:reply, {:ok, pet}, state}
else
{:reply, {:error, :pet_not_found}, state}
end
end
@impl true
def handle_call(:get_summoned_pets, _from, state) do
# Return list of {slot, pet} tuples for all summoned pets
summoned = state.pets
|> Enum.filter(fn p -> p.summoned > 0 end)
|> Enum.map(fn p -> {p.summoned, p} end)
{:reply, summoned, state}
end
@impl true
def handle_call({:spawn_pet, inventory_slot, lead}, _from, state) do
# Get pet from cash inventory
cash_inv = Map.get(state.inventories, :cash, Inventory.new(:cash))
item = Inventory.get_item(cash_inv, inventory_slot)
cond do
is_nil(item) ->
{:reply, {:error, :item_not_found}, state}
is_nil(item.pet) ->
{:reply, {:error, :not_a_pet}, state}
true ->
# Find available slot (1, 2, or 3)
used_slots = state.pets |> Enum.map(& &1.summoned) |> Enum.filter(& &1 > 0)
available_slots = [1, 2, 3] -- used_slots
if available_slots == [] do
{:reply, {:error, :no_slots_available}, state}
else
slot = if lead, do: 1, else: List.first(available_slots)
# Update pet with summoned slot and position
pet = item.pet
|> Pet.set_summoned(slot)
|> Pet.set_inventory_position(inventory_slot)
|> Pet.update_position(state.position.x, state.position.y)
# Add or update pet in state
existing_index = Enum.find_index(state.pets, fn p -> p.unique_id == pet.unique_id end)
new_pets = if existing_index do
List.replace_at(state.pets, existing_index, pet)
else
[pet | state.pets]
end
new_state = %{state | pets: new_pets, updated_at: DateTime.utc_now()}
{:reply, {:ok, pet}, new_state}
end
end
end
@impl true
def handle_call({:despawn_pet, slot}, _from, state) do
case Enum.find(state.pets, fn p -> p.summoned == slot end) do
nil ->
{:reply, {:error, :pet_not_found}, state}
pet ->
# Set summoned to 0
updated_pet = Pet.set_summoned(pet, 0)
# Update in state
pet_index = Enum.find_index(state.pets, fn p -> p.unique_id == pet.unique_id end)
new_pets = List.replace_at(state.pets, pet_index, updated_pet)
new_state = %{state | pets: new_pets, updated_at: DateTime.utc_now()}
{:reply, {:ok, updated_pet}, new_state}
end
end
@impl true
def handle_cast({:update_pet, pet}, state) do
# Find and update pet
pet_index = Enum.find_index(state.pets, fn p -> p.unique_id == pet.unique_id end)
new_pets = if pet_index do
List.replace_at(state.pets, pet_index, pet)
else
[pet | state.pets]
end
{:noreply, %{state | pets: new_pets, updated_at: DateTime.utc_now()}}
end
@impl true
def handle_cast({:update_pet_position, slot, position}, state) do
# Find pet by slot and update position
case Enum.find_index(state.pets, fn p -> p.summoned == slot end) do
nil ->
{:noreply, state}
index ->
pet = Enum.at(state.pets, index)
updated_pet = Pet.update_position(pet, position.x, position.y, position.fh, position.stance)
new_pets = List.replace_at(state.pets, index, updated_pet)
{:noreply, %{state | pets: new_pets}}
end
end
# Calculate EXP needed to reach next level
defp calculate_exp_needed(level) when level >= 200, do: 0
defp calculate_exp_needed(level) do
# Simple formula: level^3 + 100 * level
# TODO: Use actual MapleStory EXP table
level * level * level + 100 * level
end
end