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