Files
odinsea-elixir/lib/odinsea/game/monster.ex
2026-02-14 19:36:59 -07:00

255 lines
6.1 KiB
Elixir

defmodule Odinsea.Game.Monster do
@moduledoc """
Represents a monster (mob) instance on a map.
Monsters are managed by the Map GenServer, not as separate processes.
Each monster has stats, position, HP tracking, and controller assignment.
"""
alias Odinsea.Game.LifeFactory
@type t :: %__MODULE__{
oid: integer(),
mob_id: integer(),
stats: LifeFactory.MonsterStats.t(),
hp: integer(),
mp: integer(),
max_hp: integer(),
max_mp: integer(),
position: %{x: integer(), y: integer(), fh: integer()},
stance: integer(),
controller_id: integer() | nil,
controller_has_aggro: boolean(),
spawn_effect: integer(),
team: integer(),
fake: boolean(),
link_oid: integer(),
status_effects: %{atom() => any()},
poisons: [any()],
attackers: %{integer() => %{damage: integer(), last_hit: integer()}},
last_attack: integer(),
last_move: integer(),
last_skill_use: integer(),
killed: boolean(),
drops_disabled: boolean(),
create_time: integer()
}
defstruct [
:oid,
:mob_id,
:stats,
:hp,
:mp,
:max_hp,
:max_mp,
:position,
:stance,
:controller_id,
:controller_has_aggro,
:spawn_effect,
:team,
:fake,
:link_oid,
:status_effects,
:poisons,
:attackers,
:last_attack,
:last_move,
:last_skill_use,
:killed,
:drops_disabled,
:create_time
]
@doc """
Creates a new monster instance.
"""
def new(mob_id, oid, position) do
stats = LifeFactory.get_monster_stats(mob_id)
if stats do
%__MODULE__{
oid: oid,
mob_id: mob_id,
stats: stats,
hp: stats.hp,
mp: stats.mp,
max_hp: stats.hp,
max_mp: stats.mp,
position: position,
stance: 5,
controller_id: nil,
controller_has_aggro: false,
spawn_effect: 0,
team: -1,
fake: false,
link_oid: 0,
status_effects: %{},
poisons: [],
attackers: %{},
last_attack: System.system_time(:millisecond),
last_move: System.system_time(:millisecond),
last_skill_use: 0,
killed: false,
drops_disabled: false,
create_time: System.system_time(:millisecond)
}
else
nil
end
end
@doc """
Damages the monster.
Returns {:ok, monster, damage_dealt} or {:dead, monster, damage_dealt}
"""
def damage(%__MODULE__{} = monster, damage_amount, attacker_id) do
# Track attacker
attacker_entry = Map.get(monster.attackers, attacker_id, %{damage: 0, last_hit: 0})
new_attacker_entry = %{
attacker_entry
| damage: attacker_entry.damage + damage_amount,
last_hit: System.system_time(:millisecond)
}
new_attackers = Map.put(monster.attackers, attacker_id, new_attacker_entry)
# Apply damage
new_hp = max(0, monster.hp - damage_amount)
new_monster = %{monster | hp: new_hp, attackers: new_attackers}
if new_hp <= 0 do
{:dead, %{new_monster | killed: true}, damage_amount}
else
{:ok, new_monster, damage_amount}
end
end
@doc """
Heals the monster.
"""
def heal(%__MODULE__{} = monster, heal_amount) do
new_hp = min(monster.max_hp, monster.hp + heal_amount)
%{monster | hp: new_hp}
end
@doc """
Sets the monster's controller.
"""
def set_controller(%__MODULE__{} = monster, controller_id) do
%{monster | controller_id: controller_id, controller_has_aggro: true}
end
@doc """
Removes the monster's controller.
"""
def clear_controller(%__MODULE__{} = monster) do
%{monster | controller_id: nil, controller_has_aggro: false}
end
@doc """
Updates the monster's position.
"""
def update_position(%__MODULE__{} = monster, position) do
%{monster | position: position, last_move: System.system_time(:millisecond)}
end
@doc """
Checks if the monster is boss.
"""
def boss?(%__MODULE__{} = monster) do
monster.stats.boss
end
@doc """
Checks if the monster is dead.
"""
def dead?(%__MODULE__{} = monster) do
monster.hp <= 0 || monster.killed
end
@doc """
Gets the monster's name.
"""
def name(%__MODULE__{} = monster) do
monster.stats.name
end
@doc """
Gets the monster's level.
"""
def level(%__MODULE__{} = monster) do
monster.stats.level
end
@doc """
Calculates EXP drop for this monster.
"""
def calculate_exp(%__MODULE__{} = monster, exp_rate \\ 1.0) do
base_exp = monster.stats.exp
trunc(base_exp * exp_rate)
end
@doc """
Gets the top attacker (highest damage dealer).
"""
def get_top_attacker(%__MODULE__{} = monster) do
if Enum.empty?(monster.attackers) do
nil
else
{attacker_id, _entry} =
Enum.max_by(monster.attackers, fn {_id, entry} -> entry.damage end)
attacker_id
end
end
@doc """
Gets all attackers sorted by damage (descending).
"""
def get_attackers_sorted(%__MODULE__{} = monster) do
monster.attackers
|> Enum.sort_by(fn {_id, entry} -> entry.damage end, :desc)
|> Enum.map(fn {id, entry} -> {id, entry.damage} end)
end
@doc """
Applies a status effect to the monster.
"""
def apply_status_effect(%__MODULE__{} = monster, effect_name, effect_data) do
new_effects = Map.put(monster.status_effects, effect_name, effect_data)
%{monster | status_effects: new_effects}
end
@doc """
Removes a status effect from the monster.
"""
def remove_status_effect(%__MODULE__{} = monster, effect_name) do
new_effects = Map.delete(monster.status_effects, effect_name)
%{monster | status_effects: new_effects}
end
@doc """
Checks if monster has a status effect.
"""
def has_status_effect?(%__MODULE__{} = monster, effect_name) do
Map.has_key?(monster.status_effects, effect_name)
end
@doc """
Disables drops for this monster.
"""
def disable_drops(%__MODULE__{} = monster) do
%{monster | drops_disabled: true}
end
@doc """
Checks if drops are disabled.
"""
def drops_disabled?(%__MODULE__{} = monster) do
monster.drops_disabled
end
end