255 lines
6.1 KiB
Elixir
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
|