defmodule Odinsea.Game.MonsterStatus do @moduledoc """ Monster status effects (buffs/debuffs) for MapleStory. Ported from Java: client/status/MobStat.java MonsterStatus represents status effects that can be applied to monsters: - PAD (Physical Attack Damage) - PDD (Physical Defense) - MAD (Magic Attack Damage) - MDD (Magic Defense) - ACC (Accuracy) - EVA (Evasion) - Speed - Stun - Freeze - Poison - Seal - And more... """ import Bitwise @type t :: :pad | :pdd | :mad | :mdd | :acc | :eva | :speed | :stun | :freeze | :poison | :seal | :darkness | :power_up | :magic_up | :p_guard_up | :m_guard_up | :doom | :web | :p_immune | :m_immune | :showdown | :hard_skin | :ambush | :damaged_elem_attr | :venom | :blind | :seal_skill | :burned | :dazzle | :p_counter | :m_counter | :disable | :rise_by_toss | :body_pressure | :weakness | :time_bomb | :magic_crash | :exchange_attack | :heal_by_damage | :invincible @doc """ All monster status effects with their bit values and positions. Format: {status_name, bit_value, position} Position 1 = first int, Position 2 = second int """ def all_statuses do [ # Position 1 (first int) {:pad, 0x1, 1}, {:pdd, 0x2, 1}, {:mad, 0x4, 1}, {:mdd, 0x8, 1}, {:acc, 0x10, 1}, {:eva, 0x20, 1}, {:speed, 0x40, 1}, {:stun, 0x80, 1}, {:freeze, 0x100, 1}, {:poison, 0x200, 1}, {:seal, 0x400, 1}, {:darkness, 0x800, 1}, {:power_up, 0x1000, 1}, {:magic_up, 0x2000, 1}, {:p_guard_up, 0x4000, 1}, {:m_guard_up, 0x8000, 1}, {:doom, 0x10000, 1}, {:web, 0x20000, 1}, {:p_immune, 0x40000, 1}, {:m_immune, 0x80000, 1}, {:showdown, 0x100000, 1}, {:hard_skin, 0x200000, 1}, {:ambush, 0x400000, 1}, {:damaged_elem_attr, 0x800000, 1}, {:venom, 0x1000000, 1}, {:blind, 0x2000000, 1}, {:seal_skill, 0x4000000, 1}, {:burned, 0x8000000, 1}, {:dazzle, 0x10000000, 1}, {:p_counter, 0x20000000, 1}, {:m_counter, 0x40000000, 1}, {:disable, 0x80000000, 1}, # Position 2 (second int) {:rise_by_toss, 0x1, 2}, {:body_pressure, 0x2, 2}, {:weakness, 0x4, 2}, {:time_bomb, 0x8, 2}, {:magic_crash, 0x10, 2}, {:exchange_attack, 0x20, 2}, {:heal_by_damage, 0x40, 2}, {:invincible, 0x80, 2} ] end @doc """ Gets the bit value for a status effect. """ @spec get_bit(t()) :: integer() def get_bit(status) do case List.keyfind(all_statuses(), status, 0) do {_, bit, _} -> bit nil -> 0 end end @doc """ Gets the position (1 or 2) for a status effect. """ @spec get_position(t()) :: integer() def get_position(status) do case List.keyfind(all_statuses(), status, 0) do {_, _, pos} -> pos nil -> 1 end end @doc """ Checks if a status is in position 1. """ @spec position_1?(t()) :: boolean() def position_1?(status) do get_position(status) == 1 end @doc """ Checks if a status is in position 2. """ @spec position_2?(t()) :: boolean() def position_2?(status) do get_position(status) == 2 end @doc """ Gets all statuses in position 1. """ @spec position_1_statuses() :: [t()] def position_1_statuses do all_statuses() |> Enum.filter(fn {_, _, pos} -> pos == 1 end) |> Enum.map(fn {status, _, _} -> status end) end @doc """ Gets all statuses in position 2. """ @spec position_2_statuses() :: [t()] def position_2_statuses do all_statuses() |> Enum.filter(fn {_, _, pos} -> pos == 2 end) |> Enum.map(fn {status, _, _} -> status end) end @doc """ Encodes a map of status effects to bitmasks. Returns {mask1, mask2} where each is an integer bitmask. """ @spec encode_statuses(%{t() => integer()}) :: {integer(), integer()} def encode_statuses(statuses) when is_map(statuses) do mask1 = statuses |> Enum.filter(fn {status, _} -> position_1?(status) end) |> Enum.reduce(0, fn {status, _}, acc -> acc ||| get_bit(status) end) mask2 = statuses |> Enum.filter(fn {status, _} -> position_2?(status) end) |> Enum.reduce(0, fn {status, _}, acc -> acc ||| get_bit(status) end) {mask1, mask2} end def encode_statuses(_), do: {0, 0} @doc """ Decodes bitmasks to a list of status effects. """ @spec decode_statuses(integer(), integer()) :: [t()] def decode_statuses(mask1, mask2) do pos1 = position_1_statuses() |> Enum.filter(fn status -> (mask1 &&& get_bit(status)) != 0 end) pos2 = position_2_statuses() |> Enum.filter(fn status -> (mask2 &&& get_bit(status)) != 0 end) pos1 ++ pos2 end @doc """ Gets the linked disease for a monster status. Used when converting monster debuffs to player diseases. """ @spec get_linked_disease(t()) :: atom() | nil def get_linked_disease(status) do case status do :stun -> :stun :web -> :stun :poison -> :poison :venom -> :poison :seal -> :seal :magic_crash -> :seal :freeze -> :freeze :blind -> :darkness :speed -> :slow _ -> nil end end @doc """ Gets the monster status from a Pokemon-style skill ID. Used for familiar/capture card mechanics. """ @spec from_pokemon_skill(integer()) :: t() | nil def from_pokemon_skill(skill_id) do case skill_id do 120 -> :seal 121 -> :blind 123 -> :stun 125 -> :poison 126 -> :speed 137 -> :freeze _ -> nil end end @doc """ Checks if the status is a stun effect (prevents movement). """ @spec is_stun?(t()) :: boolean() def is_stun?(status) do status in [:stun, :freeze] end @doc """ Checks if the status is a damage over time effect. """ @spec is_dot?(t()) :: boolean() def is_dot?(status) do status in [:poison, :venom, :burned] end @doc """ Checks if the status is a debuff (negative effect). """ @spec is_debuff?(t()) :: boolean() def is_debuff?(status) do status in [ :stun, :freeze, :poison, :seal, :darkness, :doom, :web, :blind, :seal_skill, :burned, :dazzle, :speed, :weakness, :time_bomb, :magic_crash, :disable, :rise_by_toss ] end @doc """ Checks if the status is a buff (positive effect). """ @spec is_buff?(t()) :: boolean() def is_buff?(status) do status in [ :pad, :pdd, :mad, :mdd, :acc, :eva, :power_up, :magic_up, :p_guard_up, :m_guard_up, :hard_skin, :invincible, :heal_by_damage ] end @doc """ Gets the default duration for a status effect in milliseconds. """ @spec default_duration(t()) :: integer() def default_duration(status) do case status do :stun -> 3000 :freeze -> 5000 :poison -> 8000 :seal -> 5000 :darkness -> 8000 :doom -> 10000 :web -> 8000 :blind -> 8000 :speed -> 8000 :weakness -> 8000 _ -> 5000 end end end