kimi gone wild
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user