defmodule Odinsea.Constants.Game do @moduledoc """ Game constants ported from Java GameConstants. These define gameplay mechanics, limits, and rates. """ # Character limits @max_level 250 @max_ap 999 @max_hp_mp 999_999 @base_max_hp 50 @base_max_mp 50 # Inventory limits @max_inventory_slots 128 @equip_slots 96 @use_slots 96 @setup_slots 96 @etc_slots 96 @cash_slots 96 # Meso limits @max_meso 9_999_999_999 @max_storage_meso 9_999_999_999 # Guild limits @max_guild_name_length 12 @max_guild_members 100 # Party limits @max_party_members 6 @max_expedition_members 30 # GMS specific flag @gms true @doc """ Returns the maximum character level. """ def max_level, do: @max_level @doc """ Returns the maximum AP. """ def max_ap, do: @max_ap @doc """ Returns the maximum HP/MP. """ def max_hp_mp, do: @max_hp_mp @doc """ Returns the base max HP for new characters. """ def base_max_hp, do: @base_max_hp @doc """ Returns the base max MP for new characters. """ def base_max_mp, do: @base_max_mp @doc """ Returns the max inventory slots per type. """ def max_inventory_slots, do: @max_inventory_slots def equip_slots, do: @equip_slots def use_slots, do: @use_slots def setup_slots, do: @setup_slots def etc_slots, do: @etc_slots def cash_slots, do: @cash_slots @doc """ Returns the max meso a character can hold. """ def max_meso, do: @max_meso @doc """ Returns the max meso in storage. """ def max_storage_meso, do: @max_storage_meso @doc """ Returns true if this is a GMS server. """ def gms?, do: @gms @doc """ Returns the rates from configuration. """ def rates do Application.get_env(:odinsea, :rates, []) end @doc """ Returns the EXP rate. """ def exp_rate do rates()[:exp] || 1 end @doc """ Returns the meso rate. """ def meso_rate do rates()[:meso] || 1 end @doc """ Returns the drop rate. """ def drop_rate do rates()[:drop] || 1 end # Job classification helpers @doc """ Returns true if the job is a beginner job. """ def beginner?(job), do: div(job, 1000) == 0 && rem(job, 100) == 0 @doc """ Returns true if the job is a warrior class. """ def warrior?(job), do: div(job, 100) == 1 @doc """ Returns true if the job is a magician class. """ def magician?(job), do: div(job, 100) == 2 @doc """ Returns true if the job is a bowman class. """ def bowman?(job), do: div(job, 100) == 3 @doc """ Returns true if the job is a thief class. """ def thief?(job), do: div(job, 100) == 4 @doc """ Returns true if the job is a pirate class. """ def pirate?(job), do: div(job, 100) == 5 || div(job, 100) == 51 || div(job, 100) == 52 @doc """ Returns true if the job is a resistance class. """ def resistance?(job), do: div(job, 1000) == 3 @doc """ Returns true if the job is a cygnus class. """ def cygnus?(job), do: div(job, 1000) == 1 @doc """ Returns true if the job is an aran. """ def aran?(job), do: div(job, 100) == 21 @doc """ Returns true if the job is an evan. """ def evan?(job), do: div(job, 100) == 22 || job == 2001 @doc """ Returns the job name for a given job ID. """ def job_name(job) do case job do 0 -> "Beginner" 100 -> "Warrior" 110 -> "Fighter" 120 -> "Page" 130 -> "Spearman" 111 -> "Crusader" 121 -> "Knight" 131 -> "Dragon Knight" 112 -> "Hero" 122 -> "Paladin" 132 -> "Dark Knight" 200 -> "Magician" 210 -> "Wizard (F/P)" 220 -> "Wizard (I/L)" 230 -> "Cleric" 211 -> "Mage (F/P)" 221 -> "Mage (I/L)" 231 -> "Bishop" 212 -> "Arch Mage (F/P)" 222 -> "Arch Mage (I/L)" 232 -> "Arch Mage" 300 -> "Bowman" 310 -> "Hunter" 320 -> "Crossbowman" 311 -> "Ranger" 321 -> "Sniper" 312 -> "Bowmaster" 322 -> "Marksman" 400 -> "Thief" 410 -> "Assassin" 420 -> "Bandit" 411 -> "Hermit" 421 -> "Chief Bandit" 412 -> "Night Lord" 422 -> "Shadower" 500 -> "Pirate" 510 -> "Brawler" 520 -> "Gunslinger" 511 -> "Marauder" 521 -> "Outlaw" 512 -> "Buccaneer" 522 -> "Corsair" 1000 -> "Noblesse" 1100 -> "Dawn Warrior" 1200 -> "Blaze Wizard" 1300 -> "Wind Archer" 1400 -> "Night Walker" 1500 -> "Thunder Breaker" 2000 -> "Legend" 2100 -> "Aran" 2001 -> "Evan" 2002 -> "Mercedes" _ -> "Unknown" end end # ============================================================================= # Skill & Attack Constants (for Anti-Cheat) # ============================================================================= @doc """ Returns the attack delay for a skill (in ticks). Used for speed hack detection. """ def get_attack_delay(skill_id) do if skill_id == 0 do # Normal attack delay 300 else # Get from skill data or use default get_skill_delay(skill_id) || 300 end end @doc """ Returns the skill damage percentage. """ def get_skill_damage(skill_id) do # Default skill damage, would be loaded from WZ in production case skill_id do 0 -> 100 # Common skills 1000 -> 40 1009 -> 3000 1020 -> 1 # Warrior 1001004 -> 150 1001005 -> 200 1101006 -> 180 # Mage 2001008 -> 120 2101004 -> 130 2201004 -> 130 # Bowman 3101005 -> 150 3201005 -> 150 # Thief 4001334 -> 130 4101005 -> 140 4201005 -> 140 # Pirate 5001002 -> 160 5101004 -> 150 5201004 -> 150 # Aran 21000002 -> 180 21100001 -> 190 _ -> 100 end end @doc """ Returns the attack range for a skill. """ def get_attack_range(skill_id) do case skill_id do 0 -> 100 # Ranged skills 3101005 -> 500 3201005 -> 500 4001334 -> 250 4101005 -> 250 4121007 -> 300 4201005 -> 150 4221007 -> 600 # Melee skills _ -> 150 end end @doc """ Returns true if skill is a Mu Lung Dojo skill. """ def is_mulung_skill?(skill_id) do skill_id >= 10001000 && skill_id < 10002000 end @doc """ Returns true if skill is a Pyramid skill. """ def is_pyramid_skill?(skill_id) do skill_id >= 1020 && skill_id <= 1022 end @doc """ Returns true if skill has no delay. """ def is_no_delay_skill?(skill_id) do # Skills that bypass attack delay checks skill_id in [3101005, 1009, 1020] end @doc """ Returns true if skill is a magic charge skill. """ def is_magic_charge_skill?(skill_id) do skill_id in [2121001, 2221001, 2321001] end @doc """ Returns true if skill is an event skill. """ def is_event_skill?(skill_id) do # Skills only usable in specific event maps skill_id >= 90001000 && skill_id < 90002000 end @doc """ Returns the linked Aran skill (for skill linking). """ def get_linked_aran_skill(skill_id) do # Handle Aran skill linking if div(skill_id, 10000) == 21 do # Convert Aran skills to regular warrior equivalents for linking base = rem(skill_id, 10000) cond do base == 1005 -> 1101005 base == 1004 -> 1101006 true -> skill_id end else skill_id end end # ============================================================================= # Private Helpers # ============================================================================= defp get_skill_delay(skill_id) do case skill_id do 0 -> 300 1000 -> 600 1004 -> 600 1005 -> 900 1001004 -> 960 1001005 -> 1260 1101006 -> 1050 2001008 -> 810 2101004 -> 810 2201004 -> 810 3101005 -> 840 3201005 -> 840 4001334 -> 600 4101005 -> 720 4201005 -> 720 5001002 -> 600 5101004 -> 660 5201004 -> 660 _ -> nil end end end