diff --git a/documentation/model_ids.txt b/documentation/model_ids.txt index ee19de00538..0f879124404 100644 --- a/documentation/model_ids.txt +++ b/documentation/model_ids.txt @@ -27,11 +27,11 @@ ID Number Costume Name 25 Diabolos 26 Odin 27 Alexander -28 Mandragora -29 Mandragora -30 Mandragora +28 Cait Sith +29 Atomos +30 Siren 31 Mandragora -32 Hume Male +32 Hume Male with hood 33 34 35 @@ -404,7 +404,7 @@ ID Number Costume Name 402 Manticore 403 Kirin 404 Behemoth -405 Elsamoth(Abys) +405 Elasamoth(Abys) 406 Crawler 3(Eruca) 407 Genbu 408 Beetle 1(green) @@ -1931,7 +1931,7 @@ ID Number Costume Name 1931 1932 Sturdy Pyxis 1933 Samursk(AbysNM) -1934 Leech (Black) Abys +1934 Leech (Black) Abys - Obdella 1935 Wyvern (Red) Abys 1936 Bee (Blue) Abys 1937 Rabbit(Orange) Abys @@ -2201,7 +2201,7 @@ ID Number Costume Name 2201 Dancer NPC2(HolidayDeco) 2202 Aphmau White (arm raised) 2203 Lion (dagger toss) -2204 Mandragora +2204 Tiny/Pygmy Mandragora 2205 Dark Ixion 2206 Ruszor 2207 Lynx @@ -2453,8 +2453,8 @@ ID Number Costume Name 2468 Altana??? 2500 2678 Arciela -2882 FF14 Spriggan 2881 DQ Slime +2882 FF14 Spriggan 2907 DQ She-Slime (Red Slime) 2908 DQ Metal Slime 2909 FF14 Spriggan diff --git a/scripts/actions/abilities/relinquish.lua b/scripts/actions/abilities/relinquish.lua new file mode 100644 index 00000000000..8de342cf029 --- /dev/null +++ b/scripts/actions/abilities/relinquish.lua @@ -0,0 +1,17 @@ +----------------------------------- +-- Ability: Relinquish +----------------------------------- +require('scripts/globals/monstrosity') +----------------------------------- +local abilityObject = {} + +abilityObject.onAbilityCheck = function(player, target, ability) + -- TODO: Block if being attacked + return 0, 0 +end + +abilityObject.onUseAbility = function(player, target, ability) + xi.monstrosity.relinquishOnAbility(player, target, ability) +end + +return abilityObject diff --git a/scripts/commands/addallmonstrosity.lua b/scripts/commands/addallmonstrosity.lua new file mode 100644 index 00000000000..6c4aa630822 --- /dev/null +++ b/scripts/commands/addallmonstrosity.lua @@ -0,0 +1,36 @@ +----------------------------------- +-- func: addallmonstrosity +-- desc: Adds all species, instincts, and variants for monstrosity +----------------------------------- +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 1, + parameters = 's' +} + +local function error(player, msg) + player:PrintToPlayer(msg) + player:PrintToPlayer('!addallmonstrosity (player)') +end + +commandObj.onTrigger = function(player, target) + -- validate target + local targ + if target == nil then + targ = player + else + targ = GetPlayerByName(target) + if targ == nil then + error(player, string.format('Player named "%s" not found!', target)) + return + end + end + + xi.monstrosity.unlockAll(targ) + + player:PrintToPlayer(string.format('%s now has all monstrosity data.', targ:getName())) +end + +return commandObj diff --git a/scripts/commands/costume2.lua b/scripts/commands/costume2.lua new file mode 100644 index 00000000000..788a7cd8928 --- /dev/null +++ b/scripts/commands/costume2.lua @@ -0,0 +1,29 @@ +----------------------------------- +-- func: costume2 +-- desc: Sets the players current costume2. +----------------------------------- +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 1, + parameters = 'i' +} + +local function error(player, msg) + player:PrintToPlayer(msg) + player:PrintToPlayer('!costume2 ') +end + +commandObj.onTrigger = function(player, costumeId) + -- validate costumeId + if costumeId == nil or costumeId < 0 then + error(player, 'Invalid costumeID.') + return + end + + -- put on costume + player:setCostume2(costumeId) +end + +return commandObj diff --git a/scripts/commands/monstrosity.lua b/scripts/commands/monstrosity.lua new file mode 100644 index 00000000000..e1a5a49ef55 --- /dev/null +++ b/scripts/commands/monstrosity.lua @@ -0,0 +1,27 @@ +----------------------------------- +-- func: monstrosity +-- desc: scripts/globals/monstrosity.lua for a general overview of how Monstrosity works and is designed. +----------------------------------- +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 1, + parameters = '' +} + +commandObj.onTrigger = function(player) + if player:getMainJob() ~= xi.job.MON then + local pos = player:getPos() + player:setMonstrosityEntryData(pos.x, pos.y, pos.z, pos.rot, player:getZoneID(), player:getMainJob(), player:getSubJob()) + player:changeJob(xi.job.MON) + else + local data = player:getMonstrosityData() + player:changeJob(data.entry_mjob) + player:changesJob(data.entry_sjob) + end + + player:setPos(player:getXPos(), player:getYPos(), player:getZPos(), player:getRotPos(), player:getZoneID()) +end + +return commandObj diff --git a/scripts/effects/gestation.lua b/scripts/effects/gestation.lua new file mode 100644 index 00000000000..17dfacb2808 --- /dev/null +++ b/scripts/effects/gestation.lua @@ -0,0 +1,34 @@ +----------------------------------- +-- xi.effect.GESTATION +-- https://ffxiclopedia.fandom.com/wiki/Gestation +-- +-- Effects +-- Gestation is a combination of the following: +-- - Completely undetectable, even to True Sight and True Hearing monsters +-- - Quickening +-- - Unable take any actions (attacks, spells, job abilities, etc.) +-- +-- Duration +-- - Outside Belligerency: 18 hours +-- - During Belligerency: 1 minute +-- +-- How to remove the effect +-- - Wait for the effect to wear off +-- - Remove manually +----------------------------------- +local effectObject = {} + +local boostAmount = 50 -- +50% movement speed + +effectObject.onEffectGain = function(target, effect) + target:addMod(xi.mod.MOVE, boostAmount) +end + +effectObject.onEffectTick = function(target, effect) +end + +effectObject.onEffectLose = function(target, effect) + target:delMod(xi.mod.MOVE, boostAmount) +end + +return effectObject diff --git a/scripts/enum/job.lua b/scripts/enum/job.lua index 6d38033cabd..b68e4f22738 100644 --- a/scripts/enum/job.lua +++ b/scripts/enum/job.lua @@ -25,6 +25,7 @@ xi.job = SCH = 20, GEO = 21, RUN = 22, + MON = 23, } -xi.MAX_JOB_TYPE = 23 +xi.MAX_JOB_TYPE = 24 diff --git a/scripts/enum/msg.lua b/scripts/enum/msg.lua index fcbd8850d94..1be86316b57 100644 --- a/scripts/enum/msg.lua +++ b/scripts/enum/msg.lua @@ -375,6 +375,9 @@ xi.msg.basic = -- TRUST & ALTER EGO TRUST_NO_CAST_TRUST = 700, -- You are unable to use Trust magic at this time. TRUST_NO_CALL_AE = 717, -- You cannot call forth alter egos here. + + -- Monstrosity + FERETORY_COUNTDOWN = 679, -- will return to the Feretory in } -- Used to modify certain basic messages. diff --git a/scripts/globals/monstrosity.lua b/scripts/globals/monstrosity.lua new file mode 100644 index 00000000000..469e6a3ba7a --- /dev/null +++ b/scripts/globals/monstrosity.lua @@ -0,0 +1,1585 @@ +----------------------------------- +-- Monstrosity (MON) +-- +-- === How does it work? === +-- +-- Monstrosity is enabled through two mechanisms: setting your job to JOB_MON (23) and zoning. +-- Currently, there are some details that seemingly can only be populated at zone-time, so switching in/out +-- of MON mode is reliant on zoning. +-- +-- When you zone your job will be checked, and if it is JOB_MON, then PChar->m_PMonstrosity will get +-- populated with your relevant Monstrosity data from table defined in char_monstrosity.sql. If you don't have +-- this information yet, it'll be created and saved for you with the defaults (the starting 3 MONs and the basic instincts). +-- +-- Most other logic for determining stats, exp, exp ranges, traits, etc. will check you are either JOB_MON +-- or have m_PMonstrosity populated, and then look up what main/sub job your current species is, and then +-- forward that information into the relevant code for working out stats, etc. +-- +-- IT IS VITAL that m_PMonstrosity is managed correctly, or that it's existance is constantly checked. +-- +-- There is _a lot_ of client-side validation for MON, but we have all the information available server-side, +-- so we make sure to validate everything that comes through the zone_in and MON equip packets. It's also important +-- to validate all things for MON, because if they're invalid the client will get stuck in a state where they can't change +-- jobs, species, instincts, or names without GM intervention. +-- +-- MONs main and subjob are in lock-step, so if you are a MNK15/NIN, the NIN will also be Lv15, and you'll get all the abilities, +-- traits, and stat contributions (TODO?) from both - except for the 2H which comes from the main job. +----------------------------------- +require('scripts/globals/npc_util') +require('scripts/globals/quests') +----------------------------------- +xi = xi or {} +xi.monstrosity = xi.monstrosity or {} + +----------------------------------- +-- Enums +----------------------------------- + +xi.monstrosity.species = +{ + RABBIT = 1, + BEHEMOTH = 2, + TIGER = 3, + SHEEP = 4, + RAM = 5, + DHALMEL = 6, + COEURL = 7, + OPO_OPO = 8, + MANTICORE = 9, + BUFFALO = 10, + MARID = 11, + CERBERUS = 12, + GNOLE = 13, + + FUNGUAR = 15, + TREANT_SAPLING = 16, + MORBOL = 17, + MANDRAGORA = 18, + SABOTENDER = 19, + FLYTRAP = 20, + GOOBBUE = 21, + RAFFLESIA = 22, + PANOPT = 23, + + BEE = 27, + BEETLE = 28, + CRAWLER = 29, + FLY = 30, + SCORPION = 31, + SPIDER = 32, + ANTLION = 33, + DIREMITE = 34, + CHIGOE = 35, + WAMOURACAMPA = 36, + LADYBUG = 37, + GNAT = 38, + + LIZARD = 43, + RAPTOR = 44, + ADAMANTOISE = 45, + BUGARD = 46, + EFT = 47, + WIVRE = 48, + PEISTE = 49, + + SLIME = 52, + HECTEYES = 53, + FLAN = 54, + SLUG = 56, + SANDWORM = 57, + LEECH = 58, + + CRAB = 60, + PUGIL = 61, + SEA_MONK = 62, + URAGNITE = 63, + OROBON = 64, + RUSZOR = 65, + TOAD = 66, + + BIRD = 69, + COCKATRICE = 70, + ROC = 71, + BAT = 72, + HIPPOGRYPH = 73, + APKALLU = 74, + COLIBRI = 75, + AMPHIPTERE = 76, + + ASTOLTIAN_SLIME = 126, + EORZEAN_SPRIGGAN = 127, +} + +xi.monstrosity.variants = +{ + -- Rabbit + ONYX_RABBIT = 0, + ALABASTER_RABBIT = 1, + LAPINION = 2, + + -- Behemoth + ELASMOTH = 3, + + -- Tiger + LEGENDARY_TIGER = 5, + SMILODON = 6, + + -- Sheep + KARAKUL = 7, + + -- Coeurl + LYNX = 10, + COLLARED_LYNX = 11, + + -- Manticore + LEGENDARY_MANTICORE = 12, + + -- Cerberus + ORTHRUS = 13, + + -- Gnole + BIPEDAL_GNOLE = 14, + + -- Funguar + COPPERCAP = 15, + + -- Treant Sapling + TREANT = 16, + FLOWERING_TREANT = 17, + SCARLET_TINGED_TREANT = 18, + BARREN_TREANT = 19, + NECKLACED_TREANT = 20, + + -- Morbol + PYGMY_MORBOL = 21, + SCARE_MORBOL = 22, + AMERETAT = 23, + PURBOL = 24, + + -- Mandragora + KORRIGAN = 25, + LYCOPODIUM = 26, + PYGMY_MANDRAGORA = 27, + ADENIUM = 28, + PACHYPODIUM = 29, + ENLIGHTENED_MANDRAGORA = 30, + NEW_YEAR_MANDRAGORA = 31, + + -- Sabotender + SABOTENDER_FLORIDO = 32, + + -- Rafflesia + MITRASTEMA = 33, + + -- Bee + VERMILLION_AND_ONYX_BEE = 34, + ZAFFRE_BEE = 35, + + -- Beetle + ONYX_BEETLE = 36, + GAMBOGE_BEETLE = 37, + + -- Crawler + ERUCA = 38, + EMERALD_CRAWLER = 39, + PYGMY_EMERALD_CRAWLER = 40, + + -- Fly + VERMILLION_FLY = 41, + + -- Scorpion + SCOLOPENDRID = 42, + UNUSUAL_SCOLOPENDRID = 43, + + -- Spider + RETICULATED_SPIDER = 44, + VERMILLION_AND_ONYX_SPIDER = 45, + + -- Antlion + ONYX_ANTLION = 46, + FORMICEROS = 47, + + -- Diremite + ARUNDIMITE = 48, + + -- Chigoe + AZURE_CHIGOE = 49, + + -- Wamouracampa + COILED_WAMOURACAMPA = 50, + + -- Wamoura + WAMOURA = 51, + CORAL_WAMOURA = 52, + + -- Ladybug + GOLD_LADYBUG = 53, + + -- Gnat + MIDGE = 54, + + -- Lizard + ASHEN_LIZARD = 59, + + -- Raptor + EMERALD_RAPTOR = 60, + VERMILLION_RAPTOR = 61, + + -- Adamantoise + PYGMY_ADAMANTOISE = 62, + LEGENDARY_ADAMANTOISE = 63, + FERROMANTOISE = 64, + + -- Bugard + ABYSSOBUGARD = 65, + + -- Eft + TARICHUK = 66, + + -- Wivre + UNUSUAL_WIVRE = 67, + + -- Peiste + SIBILUS = 68, + + -- Slime + CLOT = 73, + GOLD_SLIME = 74, + BOIL = 75, + + -- Flan + GOLD_FLAN = 76, + BLANCMANGE = 77, + + -- Sandworm + PYGMY_SANDWORM = 78, + GIGAWORM = 79, + + -- Leech + AZURE_LEECH = 80, + OBDELLA = 81, + + -- Crab + VERMILLION_CRAB = 84, + BASKET_BURDENED_CRAB = 85, + VERMILLION_BASKET_BURDENED_CRAB = 86, + PORTER_CRAB = 87, + + -- Pugil + JAGIL = 88, + + -- Sea Monk + AZURE_SEA_MONK = 89, + + -- Uragnite + LIMASCABRA = 90, + + -- Orobon + PYGMY_OROBON = 91, + OGREBON = 92, + + -- Toad + AZURE_TOAD = 93, + VERMILLION_TOAD = 94, + + -- Bird + ONYX_BIRD = 95, + + -- Cockatrice + ZIZ = 96, + + -- Roc + LEGENDARY_ROC = 97, + GAGANA = 98, + + -- Bat + BATS = 99, + VERMILLION_BAT = 100, + VERMILLION_BATS = 101, + + -- Apkallu + INGUZA = 102, + + -- Colibri + TOUCALIBRI = 103, + + -- Amphiptere + SANGUIPTERE = 104, + + -- Slime + SHE_SLIME = 252, + METAL_SLIME = 253, + + -- Spriggan + SPRIGGAN_C = 254, + SPRIGGAN_G = 255, +} + +xi.monstrosity.purchasableInstincts = +{ + -- Default (0x1F) + HUME_I = 0, + ELVAAN_I = 1, + TARU_I = 2, + MITHRA_I = 3, + GALKA_I = 4, + + HUME_II = 5, + ELVAAN_II = 6, + TARU_II = 7, + MITHRA_II = 8, + GALKA_II = 9, + + WAR = 10, + MNK = 11, + WHM = 12, + BLM = 13, + RDM = 14, + THF = 15, + PLD = 16, + DRK = 17, + BST = 18, + BRD = 19, + RNG = 20, + SAM = 21, + NIN = 22, + DRG = 23, + SMN = 24, + BLU = 25, + COR = 26, + PUP = 27, + DNC = 28, + SCH = 29, + GEO = 30, + RUN = 31, +} + +local limitBreakQuests = +{ + [xi.job.BLU] = { xi.quest.log_id.AHT_URHGAN, xi.quest.id.ahtUrhgan.THE_BEAST_WITHIN }, + [xi.job.COR] = { xi.quest.log_id.AHT_URHGAN, xi.quest.id.ahtUrhgan.BREAKING_THE_BONDS_OF_FATE }, + [xi.job.PUP] = { xi.quest.log_id.BASTOK, xi.quest.id.bastok.ACHIEVING_TRUE_POWER }, + [xi.job.DNC] = { xi.quest.log_id.JEUNO, xi.quest.id.jeuno.A_FURIOUS_FINALE }, + [xi.job.SCH] = { xi.quest.log_id.OTHER_AREAS, xi.quest.id.otherAreas.SURVIVAL_OF_THE_WISEST }, + [xi.job.GEO] = { xi.quest.log_id.ADOULIN, xi.quest.id.adoulin.ELEMENTARY_MY_DEAR_SYLVIE }, + [xi.job.RUN] = { xi.quest.log_id.ADOULIN, xi.quest.id.adoulin.ENDEAVORING_TO_AWAKEN }, +} + +-- NOTE: Cost and granted species/variant are hardcoded into Terynon's event; however, the requirements +-- to get each of these purchasable MONs is not displayed, and can be modified to a different set or +-- level. The requirements are limited to species! +local terynonMonData = +{ + [0] = -- Beasts + { + [0] = + { + monVariant = xi.monstrosity.variants.LAPINION, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.RABBIT, 90 }, + }, + }, + + [1] = + { + monSpecies = xi.monstrosity.species.SHEEP, + infamyCost = 3000, + }, + + [2] = + { + monSpecies = xi.monstrosity.species.BEHEMOTH, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.RABBIT, 75 }, + { xi.monstrosity.species.OPO_OPO, 75 }, + { xi.monstrosity.species.GNOLE, 75 }, + }, + }, + + [3] = + { + monVariant = xi.monstrosity.variants.ELASMOTH, + infamyCost = 25000, + requirements = + { + { xi.monstrosity.species.BEHEMOTH, 50 }, + }, + }, + + [4] = + { + monSpecies = xi.monstrosity.species.CERBERUS, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.BUFFALO, 60 }, + { xi.monstrosity.species.MANTICORE, 60 }, + { xi.monstrosity.species.MARID, 60 }, + { xi.monstrosity.species.SHEEP, 60 }, + { xi.monstrosity.species.DHALMEL, 60 }, + }, + }, + + [5] = + { + monVariant = xi.monstrosity.variants.ORTHRUS, + infamyCost = 25000, + requirements = + { + { xi.monstrosity.species.CERBERUS, 50 }, + }, + }, + }, + + [1] = -- Plantoids + { + [0] = + { + monVariant = xi.monstrosity.variants.PYGMY_MANDRAGORA, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.MANDRAGORA, 45 }, + }, + }, + + [1] = + { + monSpecies = xi.monstrosity.species.TREANT, + infamyCost = 3000, + }, + + [2] = + { + monVariant = xi.monstrosity.variants.PYGMY_MORBOL, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.MORBOL, 1 }, + }, + }, + + [3] = + { + monVariant = xi.monstrosity.variants.PURBOL, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.MORBOL, 75 }, + }, + }, + }, + + [2] = -- Vermin + { + [0] = + { + monVariant = xi.monstrosity.variants.GOLD_LADYBUG, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.LADYBUG, 50 }, + }, + }, + + [1] = + { + monSpecies = xi.monstrosity.species.BEETLE, + infamyCost = 3000, + }, + + [2] = + { + monVariant = xi.monstrosity.variants.UNUSUAL_SCOLOPENDRID, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.SCORPION, 60 }, + }, + }, + + [3] = + { + monSpecies = xi.monstrosity.species.ANTLION, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.SCORPION, 60 }, + }, + }, + + [4] = + { + monVariant = xi.monstrosity.variants.FORMICEROS, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.ANTLION, 60 }, + }, + }, + + [5] = + { + monVariant = xi.monstrosity.variants.PYGMY_EMERALD_CRAWLER, + infamyCost = 6000, + requirements = + { + { xi.monstrosity.species.CRAWLER, 60 }, + }, + }, + + [6] = + { + monVariant = xi.monstrosity.variants.CORAL_WAMOURA, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.WAMOURACAMPA, 60 }, + }, + }, + + [7] = + { + monSpecies = xi.monstrosity.species.GNAT, + infamyCost = 5000, + requirements = + { + { xi.monstrosity.species.LADYBUG, 50 }, + { xi.monstrosity.species.WAMOURACAMPA, 50 }, + }, + }, + }, + + [3] = -- Lizards + { + [0] = + { + monVariant = xi.monstrosity.species.UNUSUAL_WIVRE, + infamyCost = 7500, + requirements = + { + { xi.monstrosity.species.WIVRE, 60 }, + }, + }, + + [1] = + { + monSpecies = xi.monstrosity.species.ADAMANTOISE, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.BUGARD, 60 }, + { xi.monstrosity.species.LIZARD, 60 }, + { xi.monstrosity.species.WIVRE, 60 }, + }, + }, + + [2] = + { + monVariant = xi.monstrosity.variants.FERROMANTOISE, + infamyCost = 20000, + requirements = + { + { xi.monstrosity.species.ADAMANTOISE, 70 }, + }, + }, + + [3] = + { + monSpecies = xi.monstrosity.species.RAPTOR, + infamyCost = 3000, + }, + + [4] = + { + monSpecies = xi.monstrosity.species.PEISTE, + infamyCost = 8000, + requirements = + { + { xi.monstrosity.species.EFT, 50 }, + { xi.monstrosity.species.RAPTOR, 50 }, + }, + }, + + [5] = + { + monVariant = xi.monstrosity.variants.SIBILUS, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.PEISTE, 50 }, + }, + }, + }, + + [4] = -- Amorphs + { + [0] = + { + monSpecies = xi.monstrosity.species.SLIME, + infamyCost = 3000, + }, + + [1] = + { + monVariant = xi.monstrosity.variants.BOIL, + infamyCost = 25000, + requirements = + { + { xi.monstrosity.species.SLIME, 50 }, + }, + }, + + [2] = + { + monVariant = xi.monstrosity.variants.PYGMY_SANDWORM, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.SANDWORM, 1 }, + }, + }, + + [3] = + { + monVariant = xi.monstrosity.variants.GIGAWORM, + infamyCost = 25000, + requirements = + { + { xi.monstrosity.species.SANDWORM, 60 }, + }, + }, + + [4] = + { + monSpecies = xi.monstrosity.species.LEECH, + infamyCost = 2000, + }, + }, + + [5] = -- Aquans + { + [0] = + { + monSpecies = xi.monstrosity.species.CRAB, + infamyCost = 2000, + }, + + [1] = + { + monVariant = xi.monstrosity.variants.BASKET_BURDENED_CRAB, + infamyCost = 20000, + requirements = + { + { xi.monstrosity.species.CRAB, 1 }, + }, + }, + + [2] = + { + monVariant = xi.monstrosity.variants.VERMILLION_BASKET_BURDENED_CRAB, + infamyCost = 20000, + requirements = + { + { xi.monstrosity.species.CRAB, 15 }, + }, + }, + + [3] = + { + monVariant = xi.monstrosity.variants.PORTER_CRAB, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.CRAB, 60 }, + }, + }, + + [4] = + { + monSpecies = xi.monstrosity.species.PUGIL, + infamyCost = 3000, + }, + + [5] = + { + monVariant = xi.monstrosity.variants.LIMASCABRA, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.URAGNITE, 50 }, + }, + }, + + [6] = + { + monVariant = xi.monstrosity.variants.PYGMY_OROBON, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.OROBON, 1 }, + }, + }, + + [7] = + { + monVariant = xi.monstrosity.variants.OGREBON, + infamyCost = 18000, + requirements = + { + { xi.monstrosity.species.OROBON, 50 }, + }, + }, + + [8] = + { + monSpecies = xi.monstrosity.species.RUSZOR, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.OROBON, 75 }, + { xi.monstrosity.species.URAGNITE, 75 }, + }, + }, + }, + + [6] = -- Birds + { + [0] = + { + monSpecies = xi.monstrosity.species.COCKATRICE, + infamyCost = 3000, + }, + + [1] = + { + monVariant = xi.monstrosity.variants.GAGANA, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.ROC, 75 }, + }, + }, + + [2] = + { + monSpecies = xi.monstrosity.species.BAT, + infamyCost = 2000, + }, + + [3] = + { + monVariant = xi.monstrosity.variants.INGUZA, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.APKALLU, 50 }, + }, + }, + + [4] = + { + monSpecies = xi.monstrosity.species.COLIBRI, + infamyCost = 5000, + requirements = + { + { xi.monstrosity.species.BAT, 50 }, + { xi.monstrosity.species.BIRD, 45 }, + }, + }, + + [5] = + { + monVariant = xi.monstrosity.variants.TOUCALIBRI, + infamyCost = 15000, + requirements = + { + { xi.monstrosity.species.COLIBRI, 50 }, + }, + }, + + [6] = + { + monSpecies = xi.monstrosity.species.AMPHIPTERE, + infamyCost = 10000, + requirements = + { + { xi.monstrosity.species.COCKATRICE, 75 }, + { xi.monstrosity.species.ROC, 75 }, + { xi.monstrosity.species.HIPPOGRYPH, 75 }, + }, + }, + + [7] = + { + monVariant = xi.monstrosity.variants.SANGUIPTERE, + infamyCost = 20000, + requirements = + { + { xi.monstrosity.species.AMPHIPTERE, 50 }, + }, + }, + }, +} + +xi.monstrosity.teleports = +{ + [xi.zone.EAST_RONFAURE] = + { + { 120, 0.5, -530, 192 }, + { 115, -59.684, 247, 16 }, + }, + + [xi.zone.QUFIM_ISLAND] = + { + { -2, -20.001, 324, 64 }, + { 161, -20, 37, 192 }, + }, + + [xi.zone.VALKURM_DUNES] = + { + { 838, 0, -162, 64 }, + }, + + [xi.zone.WESTERN_ALTEPA_DESERT] = + { + { 685.548, -1.744, -50.395, 128 }, + }, +} + +-- NOTE: The zones in this list are not customisable, but the level caps are! +xi.monstrosity.belligerencyCaps = +{ + [xi.zone.BUBURIMU_PENINSULA] = 30, + [xi.zone.XARCABARD] = 60, + [xi.zone.ULEGUERAND_RANGE] = 90, +} + +----------------------------------- +-- Helpers +----------------------------------- +-- Use xi.monstrosity.species +xi.monstrosity.unlockStartingMONs = function(player, choice) + local data = + { + monstrosityId = choice, + species = choice, + } + + player:setMonstrosityData(data) +end + +-- Use xi.monstrosity.species +xi.monstrosity.getSpeciesLevel = function(player, species) + local data = player:getMonstrosityData() + return data['levels'][species] +end + +-- Use xi.monstrosity.species +xi.monstrosity.hasUnlockedSpecies = function(player, species) + return xi.monstrosity.getSpeciesLevel(player, species) > 0 +end + +-- Use xi.monstrosity.species +xi.monstrosity.setSpeciesLevel = function(player, species, level) + local data = player:getMonstrosityData() + data.levels[species] = level + player:setMonstrosityData(data) +end + +-- Use xi.monstrosity.species +xi.monstrosity.unlockSpecies = function(player, species) + if not xi.monstrosity.hasUnlockedSpecies(player, species) then + xi.monstrosity.setSpeciesLevel(player, species, 1) + end +end + +-- Use xi.monstrosity.variants +xi.monstrosity.hasUnlockedVariant = function(player, variant) + local data = player:getMonstrosityData() + + local byteOffset = math.floor(variant / 8) + local shiftAmount = variant % 8 + + if byteOffset < 32 then + return bit.band(data.variants[byteOffset] or 0, bit.lshift(0x01, shiftAmount)) > 0 + end + + return false +end + +-- Use xi.monstrosity.variants +xi.monstrosity.unlockVariant = function(player, variant) + if not xi.monstrosity.hasUnlockedVariant(player, variant) then + local data = player:getMonstrosityData() + + local byteOffset = math.floor(variant / 8) + local shiftAmount = variant % 8 + + if byteOffset < 32 then + data.variants[byteOffset] = bit.bor(data.variants[byteOffset] or 0, bit.lshift(0x01, shiftAmount)) + else + print('byteOffset out of range') + end + + player:setMonstrosityData(data) + end +end + +local function hasPurchasedInstinct(player, purchasableInstinctId) + local data = player:getMonstrosityData() + local byteOffset = 20 + math.floor(purchasableInstinctId / 8) + local shiftAmount = purchasableInstinctId % 8 + + if byteOffset >= 20 and byteOffset < 24 then + return bit.band(data.instincts[byteOffset], bit.lshift(1, shiftAmount)) > 0 + else + print('byteOffset out of range') + end +end + +local function getPurchasedInstinctsMask(player) + local instinctMask = 0 + + for _, purchasableInstinctId in pairs(xi.monstrosity.purchasableInstincts) do + if + purchasableInstinctId >= xi.monstrosity.purchasableInstincts.HUME_II and + hasPurchasedInstinct(player, purchasableInstinctId) + then + instinctMask = utils.mask.setBit(instinctMask, purchasableInstinctId - xi.monstrosity.purchasableInstincts.HUME_II, true) + end + end + + return instinctMask +end + +local function addPurchasedInstinct(player, purchasableInstinctId) + local data = player:getMonstrosityData() + local byteOffset = 20 + math.floor(purchasableInstinctId / 8) + local shiftAmount = purchasableInstinctId % 8 + + if byteOffset >= 20 and byteOffset < 24 then + data.instincts[byteOffset] = bit.bor(data.instincts[byteOffset] or 0, bit.lshift(0x01, shiftAmount)) + else + print('byteOffset out of range') + end + + player:setMonstrosityData(data) +end + +-- When generating Terynon's mask for discounts, we need a bitmask for +-- specific jobs. Since only one quest exists for pre-ToAU jobs, use +-- Maat's Cap tracking for those. +local function hasCompletedLimitBreak(player, jobId) + if jobId <= xi.job.SMN then + local maatsCap = player:getCharVar('maatsCap') + + return utils.mask.getBit(maatsCap, jobId - 1) + else + return player:hasCompletedQuest(unpack(limitBreakQuests[jobId])) + end +end + +local function getLimitBreakMask(player) + local limitMask = 0 + + for jobId = xi.job.WAR, xi.job.RUN do + if hasCompletedLimitBreak(player, jobId) then + limitMask = utils.mask.setBit(limitMask, jobId - 1, true) + end + end + + return limitMask +end + +local function hasPurchaseRequirements(player, monCategory, selectedMon) + local selectedMonData = terynonMonData[monCategory][selectedMon] + local eligibleSpecies = selectedMonData.monSpecies and xi.monstrosity.getSpeciesLevel(player, selectedMonData.monSpecies) == 0 + local eligibleVariant = selectedMonData.monVariant and not xi.monstrosity.hasUnlockedVariant(player, selectedMonData.monVariant) + + if + eligibleSpecies or + eligibleVariant + then + if selectedMonData.requirements then + for _, reqTable in ipairs(selectedMonData.requirements) do + if xi.monstrosity.getSpeciesLevel(player, reqTable[1]) < reqTable[2] then + return false + end + end + end + + return true + end + + return false +end + +local function getMonPageMask(player, monCategory) + local pageMask = 0 + + if terynonMonData[monCategory] then + local categoryTable = terynonMonData[monCategory] + + for bitPos, _ in pairs(categoryTable) do + if hasPurchaseRequirements(player, monCategory, bitPos) then + pageMask = utils.mask.setBit(pageMask, bitPos, true) + end + end + end + + return pageMask +end + +----------------------------------- +-- Bound by C++ (DO NOT CHANGE SIGNATURE) +----------------------------------- + +xi.monstrosity.onMonstrosityUpdate = function(player, data) + -- Tap level-based unlocks + + -- Instincts by MON level + -- NOTE: Since this is a bitfield, it's zero-indexed! + for _, val in pairs(xi.monstrosity.species) do + local speciesKey = val + local speciesLevel = data.levels[val] + local byteOffset = math.floor(speciesKey / 4) + local unlockAmount = math.floor(speciesLevel / 30) + local shiftAmount = (speciesKey * 2) % 8 + + -- Special case for writing Slime & Spriggan data at the end of the 64-byte array + if byteOffset == 31 then + byteOffset = 63 + end + + if byteOffset < 64 then + data.instincts[byteOffset] = bit.bor(data.instincts[byteOffset] or 0, bit.lshift(unlockAmount, shiftAmount)) + else + print('byteOffset out of range') + end + end + + -- TODO: Handle level-based variants here +end + +xi.monstrosity.onMonstrosityReturnToEntrance = function(player) + local data = player:getMonstrosityData() + + local x = data.entry_x + local y = data.entry_y + local z = data.entry_z + local rot = data.entry_rot + local zoneId = data.entry_zone_id + local mjob = data.entry_mjob + local sjob = data.entry_sjob + + -- TODO: Sanity check + + for _, effect in pairs(player:getStatusEffects()) do + player:delStatusEffectSilent(effect:getEffectType()) + end + + if xi.settings.main.MONSTROSITY_TELEPORT_TO_FERETORY == 1 then + if player:getZoneID() ~= xi.zone.FERETORY then + player:setPos(-358, -3.4, -440, 64, xi.zone.FERETORY) + return + end + + -- Otherwise fallthrough and exit as normal + end + + player:changeJob(mjob) + player:changesJob(sjob) + player:setPos(x, y, z, rot, zoneId) +end + +----------------------------------- +-- Relinquish +----------------------------------- + +xi.monstrosity.relinquishSteps = +{ + [0] = function(player) + player:messageBasic(xi.msg.basic.FERETORY_COUNTDOWN, 0, 4) + end, + + [1] = function(player) + player:messageBasic(xi.msg.basic.FERETORY_COUNTDOWN, 0, 3) + end, + + [2] = function(player) + player:messageBasic(xi.msg.basic.FERETORY_COUNTDOWN, 0, 2) + end, + + [3] = function(player) + player:messageBasic(xi.msg.basic.FERETORY_COUNTDOWN, 0, 1) + end, + + [4] = function(player) + xi.monstrosity.onMonstrosityReturnToEntrance(player) + end, +} + +xi.monstrosity.relinquishFuncBody = function(player) + -- TODO: Make this countdown interruptable + player:timer(1000, function(playerArg) + local step = utils.clamp(playerArg:getLocalVar('RELINQUISH_COUNTDOWN'), 0, 4) + xi.monstrosity.relinquishSteps[step](playerArg) + playerArg:setLocalVar('RELINQUISH_COUNTDOWN', step + 1) + xi.monstrosity.relinquishFuncBody(playerArg) + end) +end + +xi.monstrosity.relinquishOnAbility = function(player, target, ability) + xi.monstrosity.relinquishFuncBody(player) +end + +----------------------------------- +-- Debug +----------------------------------- + +xi.monstrosity.unlockAll = function(player) + -- Complete quest + local logId = xi.quest.log_id.OTHER_AREAS + player:completeQuest(logId, xi.quest.id[xi.quest.area[logId]].MONSTROSITY) + + -- Add Monstrosity key item + player:addKeyItem(xi.keyItem.RING_OF_SUPERNAL_DISJUNCTION) + + local data = player:getMonstrosityData() + + -- Set all levels to 99 + for _, val in pairs(xi.monstrosity.species) do + data.levels[val] = 99 + end + + -- Instincts by MON level + -- NOTE: Since this is a bitfield, it's zero-indexed! + for _, val in pairs(xi.monstrosity.species) do + local speciesKey = val + local speciesLevel = data.levels[val] + local byteOffset = math.floor(speciesKey / 4) + local unlockAmount = math.floor(speciesLevel / 30) + local shiftAmount = (speciesKey * 2) % 8 + + -- Special case for writing Slime & Spriggan data at the end of the 64-byte array + if byteOffset == 31 then + byteOffset = 63 + end + + if byteOffset < 64 then + data.instincts[byteOffset] = bit.bor(data.instincts[byteOffset] or 0, bit.lshift(unlockAmount, shiftAmount)) + else + print('byteOffset out of range') + end + end + + -- Instincts (Purchasable) + for _, val in pairs(xi.monstrosity.purchasableInstincts) do + local byteOffset = 20 + math.floor(val / 8) + local shiftAmount = val % 8 + + if byteOffset >= 20 and byteOffset < 24 then + data.instincts[byteOffset] = bit.bor(data.instincts[byteOffset] or 0, bit.lshift(0x01, shiftAmount)) + else + print('byteOffset out of range') + end + end + + -- Variants + -- Force unlock all + for _, val in pairs(xi.monstrosity.variants) do + local speciesKey = val + local byteOffset = math.floor(speciesKey / 8) + local shiftAmount = speciesKey % 8 + + if byteOffset < 32 then + data.variants[byteOffset] = bit.bor(data.variants[byteOffset] or 0, bit.lshift(0x01, shiftAmount)) + else + print('byteOffset out of range') + end + end + + -- Set data + player:setMonstrosityData(data) +end + +----------------------------------- +-- Odyssean Passage (Feretory Only) +----------------------------------- + +xi.monstrosity.odysseanPassageOnTrade = function(player, npc, trade) +end + +xi.monstrosity.odysseanPassageOnTrigger = function(player, npc) + local monSize = player:getMonstrositySize() + local hasBelligerency = player:getBelligerencyFlag() and 1 or 0 + + -- Show the full menu, not the restricted one + if xi.settings.main.MONSTROSITY_PVP_ZONE_BYPASS == 1 then + hasBelligerency = 0 + end + + -- NOTE: The list of available zones is built from the char's list of + -- visited zones. If you haven't visited any zones in a category it'll back + -- out immediately. + -- NOTE: Param5 is not consistent, Bee has seen 0, 1, and 2 so far + -- player:startEvent(5, 0, 0, 0, 0, 2, 0, 0, 0) -- Bee + player:startEvent(5, 0, monSize, hasBelligerency, 0, 0, 0, 0, 0) +end + +xi.monstrosity.odysseanPassageOnEventUpdate = function(player, csid, option, npc) + local zoneSelected = bit.rshift(option, 4) + player:updateEvent(xi.monstrosity.belligerencyCaps[zoneSelected], 0, 0, 0, 1, 0, 0, 0) +end + +xi.monstrosity.odysseanPassageOnEventFinish = function(player, csid, option, npc) + local eventOption = bit.band(option, 0xF) + local zoneSelected = bit.rshift(option, 4) + + if eventOption == 1 then + if zoneSelected == 0 then + xi.monstrosity.onMonstrosityReturnToEntrance(player) + else + if xi.monstrosity.teleports[zoneSelected] then + local teleportPos = xi.monstrosity.teleports[zoneSelected][math.random(1, #xi.monstrosity.teleports[zoneSelected])] + + player:setPos(teleportPos[1], + teleportPos[2], + teleportPos[3], + teleportPos[4], + zoneSelected + ) + else + print('Monstrosity Teleport - No Valid Entries for Zone ' .. zoneSelected .. '. Setting pos to (0, 0, 0)!') + player:setPos(0, 0, 0, 0, zoneSelected) + end + end + end +end + +----------------------------------- +-- Feretory +----------------------------------- + +xi.monstrosity.feretoryOnZoneIn = function(player, prevZone) + local cs = -1 + + if + player:getXPos() == 0 and + player:getYPos() == 0 and + player:getZPos() == 0 + then + player:setPos(-358.000, -3.400, -440.00, 63) + end + + if player:getMainJob() ~= xi.job.MON then + player:changeJob(xi.job.MON) + end + + for _, effect in pairs(player:getStatusEffects()) do + player:delStatusEffectSilent(effect:getEffectType()) + end + + return cs +end + +xi.monstrosity.feretoryOnZoneOut = function(player) + -- Mark all status effects so they'll survive zoning + -- (there are some routines that will force them off anyway) + for _, effect in pairs(player:getStatusEffects()) do + print(effect) + effect:delEffectFlag(xi.effectFlag.ON_ZONE) + effect:delEffectFlag(xi.effectFlag.LOGOUT) + end +end + +xi.monstrosity.feretoryOnEventUpdate = function(player, csid, option, npc) +end + +xi.monstrosity.feretoryOnEventFinish = function(player, csid, option, npc) +end + +----------------------------------- +-- Aengus (Feretory NPC) +----------------------------------- + +xi.monstrosity.aengusOnTrade = function(player, npc, trade) +end + +xi.monstrosity.aengusOnTrigger = function(player, npc) + local inBelligerency = player:getBelligerencyFlag() and 1 or 0 + player:startEvent(13, inBelligerency, player:getCurrency('infamy'), 0, 0, 0, 0, 0, 0) +end + +xi.monstrosity.aengusOnEventUpdate = function(player, csid, option, npc) +end + +xi.monstrosity.aengusOnEventFinish = function(player, csid, option, npc) + if csid == 13 and option == 1 then + -- Toggle + player:setBelligerencyFlag(not player:getBelligerencyFlag()) + end +end + +----------------------------------- +-- Teyrnon (Feretory NPC) +----------------------------------- + +xi.monstrosity.teyrnonOnTrade = function(player, npc, trade) +end + +xi.monstrosity.teyrnonOnTrigger = function(player, npc) + player:startEvent(7, player:getCurrency('infamy'), 0, 0, 0, 0, 0, 0, 0) +end + +xi.monstrosity.teyrnonOnEventUpdate = function(player, csid, option, npc) + if csid == 7 then + local optionType = bit.band(option, 0xFF) + + if optionType == 0 then + -- Monsters + + local monPage = bit.rshift(option, 16) + local availableMons = getMonPageMask(player, monPage) + + player:updateEvent(availableMons, 0, 0, 0, 0, 0, 0, 0) + elseif optionType == 1 then + -- Instincts + + local purchasedInstincts = getPurchasedInstinctsMask(player) + local completedLimits = getLimitBreakMask(player) + + player:updateEvent(purchasedInstincts, completedLimits, 0, 0, 0, 0, 0, 0) + end + end +end + +xi.monstrosity.teyrnonOnEventFinish = function(player, csid, option, npc) + local optionType = bit.band(option, 0xFF) + + if optionType == 1 then + local selectedCategory = bit.band(bit.rshift(option, 8), 0xF) - 1 + local selectedMon = bit.rshift(option, 16) + local monData = terynonMonData[selectedCategory][selectedMon] + + if not monData then + print(string.format('Invalid Event Finish Option received by Terynon! (%s:%d)', player:getName(), option)) + return + end + + if player:getCurrency('infamy') >= monData.infamyCost then + player:delCurrency('infamy', monData.infamyCost) + + if monData.monSpecies then + xi.monstrosity.unlockSpecies(player, monData.monSpecies) + elseif monData.monVariant then + xi.monstrosity.unlockVariant(player, monData.monVariant) + end + + player:messageSpecial(zones[xi.zone.FERETORY].text.MAY_POSSESS_BEASTS + 3 * selectedCategory, 0, selectedMon) + else + player:messageSpecial(zones[xi.zone.FERETORY].text.THY_BRAZEN_DISREGARD) + end + + elseif optionType == 2 then + -- Instincts: Costs are hardcoded, and adjusted based on having completed certain + -- prerequisites. This data is not tabled with Terynon, as it cannot be controlled. + + local selectedInstinct = bit.band(bit.rshift(option, 8), 0xFF) + local instinctPrice = selectedInstinct > xi.monstrosity.purchasableInstincts.GALKA_II and 10000 or 500 + local checkValue = bit.rshift(option, 16) + + if checkValue ~= 119 then + print(string.format('Invalid Event Finish Option received by Terynon! (%s:%d)', player:getName(), option)) + return + end + + if + selectedInstinct > xi.monstrosity.purchasableInstincts.GALKA_II and + hasCompletedLimitBreak(player, selectedInstinct - xi.monstrosity.purchasableInstincts.GALKA_II) + then + instinctPrice = instinctPrice / 2 + end + + if player:getCurrency('infamy') >= instinctPrice then + player:delCurrency('infamy', instinctPrice) + addPurchasedInstinct(player, selectedInstinct) + + -- NOTE: The offset below is the beginning parameter for purchased instincts used by this message, and + -- lower values will result in an item being placed in the message. Base offset for all instincts + -- is 29696 (29696 + 3 -> Rabbit Instinct I) + player:messageSpecial(zones[xi.zone.FERETORY].text.YOU_LEARNED_INSTINCT, 30464 + selectedInstinct) + else + player:messageSpecial(zones[xi.zone.FERETORY].text.THY_BRAZEN_DISREGARD) + end + + elseif optionType == 3 then + -- TODO: The casting effects and animations + + local tryPayCost = function(playerArg, cost) + if playerArg:getCurrency('infamy') < cost then + playerArg:messageSpecial(zones[xi.zone.FERETORY].text.THY_BRAZEN_DISREGARD) + return false + end + + playerArg:delCurrency('infamy', cost) + return true + end + + local selectedEffect = bit.rshift(option, 8) + switch(selectedEffect): caseof + { + -- 0: Dedication 1 + -- 50% experience bonus 60 minutes or until a maximum bonus of 10,000 EXP is gained + [0] = function() + if not tryPayCost(player, 3000) then + return + end + + local effect = xi.effect.DEDICATION + local power = 50 + local duration = utils.minutes(60) + local subpower = 10000 + player:delStatusEffectSilent(power) + xi.itemUtils.addItemExpEffect(player, effect, power, duration, subpower) + end, + + -- 1: Dedication 2 + -- 100% experience bonus 60 minutes or until a maximum bonus of 2,000 EXP is gained + [1] = function() + if not tryPayCost(player, 400) then + return + end + + local effect = xi.effect.DEDICATION + local power = 100 + local duration = utils.minutes(60) + local subpower = 2000 + player:delStatusEffectSilent(power) + xi.itemUtils.addItemExpEffect(player, effect, power, duration, subpower) + end, + + -- 2: Regen + [2] = function() + if not tryPayCost(player, 10) then + return + end + + player:delStatusEffectSilent(xi.effect.REGEN) + player:addStatusEffect(xi.effect.REGEN, 1, 3, 3600) + end, + + -- 3: Refresh + [3] = function() + if not tryPayCost(player, 10) then + return + end + + player:delStatusEffectSilent(xi.effect.REFRESH) + player:delStatusEffect(xi.effect.SUBLIMATION_COMPLETE) + player:delStatusEffect(xi.effect.SUBLIMATION_ACTIVATED) + player:addStatusEffect(xi.effect.REFRESH, 1, 3, 3600, 0, 3) + end, + + -- 4: Protect + [4] = function() + if not tryPayCost(player, 100) then + return + end + + local mLvl = player:getMainLvl() + local power = 220 + local tier = 5 + + if mLvl < 27 then + power = 20 + tier = 1 + elseif mLvl < 47 then + power = 50 + tier = 2 + elseif mLvl < 63 then + power = 90 + tier = 3 + elseif mLvl < 76 then + power = 140 + tier = 4 + end + + local bonus = 0 + if player:getMod(xi.mod.ENHANCES_PROT_SHELL_RCVD) > 0 then + bonus = 2 -- 2x Tier from MOD + end + + power = power + (bonus * tier) + player:delStatusEffectSilent(xi.effect.PROTECT) + player:addStatusEffect(xi.effect.PROTECT, power, 0, 1800, 0, 0, tier) + end, + + -- 5: Shell + [5] = function() + if not tryPayCost(player, 100) then + return + end + + local mLvl = player:getMainLvl() + + -- Shell V (75/256) + local power = 2930 + local tier = 5 + + if mLvl < 37 then + power = 1055 -- Shell I (27/256) + tier = 1 + elseif mLvl < 57 then + power = 1641 -- Shell II (42/256) + tier = 2 + elseif mLvl < 68 then + power = 2188 -- Shell III (56/256) + tier = 3 + elseif mLvl < 76 then + power = 2617 -- Shell IV (67/256) + tier = 4 + end + + local bonus = 0 + if player:getMod(xi.mod.ENHANCES_PROT_SHELL_RCVD) > 0 then + bonus = 39 -- (1/256 bonus buff per tier of spell) + end + + power = power + (bonus * tier) + player:delStatusEffectSilent(xi.effect.SHELL) + player:addStatusEffect(xi.effect.SHELL, power, 0, 1800, 0, 0, tier) + end, + + -- 6: Haste + [6] = function() + player:delStatusEffectSilent(xi.effect.HASTE) + player:addStatusEffect(xi.effect.HASTE, 1000, 0, 600) + end, + } + end +end + +----------------------------------- +-- Maccus (Feretory NPC) +----------------------------------- + +xi.monstrosity.maccusOnTrade = function(player, npc, trade) +end + +xi.monstrosity.maccusOnTrigger = function(player, npc) + player:startEvent(9, 285, 2, 2, 0, 0, 0, 0, 0) +end + +xi.monstrosity.maccusOnEventUpdate = function(player, csid, option, npc) + -- print('update', csid, option) +end + +xi.monstrosity.maccusOnEventFinish = function(player, csid, option, npc) + -- print('finish', csid, option) +end diff --git a/scripts/globals/quests.lua b/scripts/globals/quests.lua index 8bbd49fe024..cbb004a997f 100644 --- a/scripts/globals/quests.lua +++ b/scripts/globals/quests.lua @@ -547,6 +547,7 @@ xi.quest.id = PICTURE_PERFECT = 31, WAKING_THE_BEAST = 32, SURVIVAL_OF_THE_WISEST = 33, + MONSTROSITY = 34, -- + Converted A_HARD_DAYS_KNIGHT = 64, -- + Converted X_MARKS_THE_SPOT = 65, A_BITTER_PAST = 66, diff --git a/scripts/quests/hiddenQuests/Monstrosity_Bee.lua b/scripts/quests/hiddenQuests/Monstrosity_Bee.lua new file mode 100644 index 00000000000..fbfdd6a836a --- /dev/null +++ b/scripts/quests/hiddenQuests/Monstrosity_Bee.lua @@ -0,0 +1,82 @@ +----------------------------------- +-- Monstrosity: Suibhne Bee Guessing Game +----------------------------------- +-- Suibhne : !pos -366 -3.612 -466 285 +----------------------------------- +local feretoryID = zones[xi.zone.FERETORY] +----------------------------------- + +local quest = HiddenQuest:new('monstrosityBee') + +quest.reward = {} + +local choices = +{ + AENGUS = 1, + MACCUS = 2, + SUIBHNE = 3, + TERYNON = 4, +} + +local answerKey = +{ + [1] = { [0] = choices.TERYNON, [1] = choices.AENGUS, [2] = choices.SUIBHNE }, + [2] = { [0] = choices.TERYNON, [1] = choices.SUIBHNE, [2] = choices.AENGUS }, + [3] = { [0] = choices.AENGUS, [1] = choices.TERYNON, [2] = choices.SUIBHNE }, + [4] = { [0] = choices.AENGUS, [1] = choices.SUIBHNE, [2] = choices.TERYNON }, + [5] = { [0] = choices.SUIBHNE, [1] = choices.AENGUS, [2] = choices.TERYNON }, + [6] = { [0] = choices.SUIBHNE, [1] = choices.TERYNON, [2] = choices.AENGUS }, +} + +local function getCheckValue(choiceNum) + local checkValue = 0 + + for fieldPos, correctChoice in pairs(answerKey[choiceNum]) do + checkValue = checkValue + bit.lshift(correctChoice, 16 + 3 * fieldPos) + end + + return checkValue +end + +quest.sections = +{ + { + check = function(player, questVars, vars) + return quest:getVar(player, 'Timer') <= VanadielUniqueDay() and + not xi.monstrosity.hasUnlockedSpecies(player, xi.monstrosity.species.BEE) + end, + + [xi.zone.FERETORY] = + { + ['Suibhne'] = + { + onTrigger = function(player, npc) + local quizInfo = quest:getVar(player, 'Option') + + if quizInfo == 0 then + quizInfo = math.random(1, 6) + quest:setVar(player, 'Option', quizInfo) + end + + return quest:progressEvent(11, 1, quizInfo - 1) + end, + }, + + onEventFinish = + { + [11] = function(player, csid, option, npc) + if option == 1 + getCheckValue(quest:getVar(player, 'Option')) then + if quest:complete(player) then + xi.monstrosity.unlockSpecies(xi.monstrosity.species.BEE) + player:messageSpecial(feretoryID.text.MAY_POSSESS_BEES) + end + elseif option == 2 then + quest:setVar(player, 'Timer', VanadielUniqueDay() + 1) + end + end, + }, + }, + }, +} + +return quest diff --git a/scripts/quests/otherAreas/Monstrosity.lua b/scripts/quests/otherAreas/Monstrosity.lua new file mode 100644 index 00000000000..38f808fbc30 --- /dev/null +++ b/scripts/quests/otherAreas/Monstrosity.lua @@ -0,0 +1,249 @@ +----------------------------------- +-- Monstrosity +----------------------------------- +-- Log ID: 4, Quest ID: 34 +-- Suspicious Hume : !pos -491.817 24.988 -618.552 109 +-- Suspicious Elvaan : !pos 82.217 -0.199 42.682 231 +-- Suspicious Galka : !pos 81.610 8.499 -229.065 236 +-- Suspicious Tarutaru : !pos 221.286 -12.000 225.311 241 +-- Rabbit Hide : !additem 856 +-- Lizard Tail : !additem 926 +-- Two-leaf Mandy Bud : !additem 4368 +----------------------------------- + +local quest = Quest:new(xi.quest.log_id.OTHER_AREAS, xi.quest.id.otherAreas.MONSTROSITY) + +quest.reward = {} + +local baseNpcEvents = +{ + [xi.zone.PASHHOW_MARSHLANDS] = 40, + [xi.zone.NORTHERN_SAN_DORIA] = 882, + [xi.zone.PORT_BASTOK] = 418, + [xi.zone.PORT_WINDURST] = 880, +} + +local tradeItems = +{ + { xi.item.LIZARD_TAIL, xi.monstrosity.species.LIZARD }, + { xi.item.RABBIT_HIDE, xi.monstrosity.species.RABBIT }, + { xi.item.TWO_LEAF_MANDRAGORA_BUD, xi.monstrosity.species.MANDRAGORA }, +} + +local suspiciousCityNpc = +{ + onTrade = function(player, npc, trade) + if player:hasKeyItem(xi.ki.RING_OF_SUPERNAL_DISJUNCTION) then + return + end + + for _, entry in ipairs(tradeItems) do + local itemId = entry[1] + if npcUtil.tradeHasExactly(trade, itemId) then + player:setLocalVar('MONSTROSITY_UNLOCK', entry[2]) + return quest:progressEvent(baseNpcEvents[player:getZoneID()] + 1, itemId) + end + end + end, + + onTrigger = function(player, npc) + local baseEvent = baseNpcEvents[player:getZoneID()] + + if not player:hasKeyItem(xi.ki.RING_OF_SUPERNAL_DISJUNCTION) then + return quest:event(baseEvent) + else + return quest:event(baseEvent + 2) + end + end, +} + +local tradeEventFinish = function(player, csid, option, npc) + npcUtil.giveKeyItem(player, xi.ki.RING_OF_SUPERNAL_DISJUNCTION) + local species = player:getLocalVar('MONSTROSITY_UNLOCK') + if species > 0 then + xi.monstrosity.unlockStartingMONs(player, species) + end +end + +local odysseanPassageNpc = +{ + onTrigger = function(player, npc) + if player:hasKeyItem(xi.ki.RING_OF_SUPERNAL_DISJUNCTION) then + return quest:progressEvent(baseNpcEvents[player:getZoneID()] + 3) + end + end, +} + +local odysseanPassageOnEventFinish = function(player, csid, option, npc) + if option == 1 then + local pos = player:getPos() + player:setMonstrosityEntryData(pos.x, pos.y, pos.z, pos.rot, player:getZoneID(), player:getMainJob(), player:getSubJob()) + player:setPos(-358, -3.4, -440, 64, xi.zone.FERETORY) + end +end + +quest.sections = +{ + { + check = function(player, status, vars) + return xi.settings.main.ENABLE_MONSTROSITY == 1 and status == QUEST_AVAILABLE + end, + + [xi.zone.PASHHOW_MARSHLANDS] = + { + ['Suspicious_Hume'] = quest:progressEvent(40), + + onEventFinish = + { + [40] = function(player, csid, option, npc) + quest:begin(player) + end, + }, + }, + }, + + { + check = function(player, status, vars) + return status == QUEST_ACCEPTED + end, + + [xi.zone.NORTHERN_SAN_DORIA] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Elvaan'] = suspiciousCityNpc, + + onEventFinish = + { + [883] = tradeEventFinish, + [885] = odysseanPassageOnEventFinish, + }, + }, + + [xi.zone.PASHHOW_MARSHLANDS] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Hume'] = + { + onTrigger = function(player, npc) + if not player:hasKeyItem(xi.ki.RING_OF_SUPERNAL_DISJUNCTION) then + return quest:event(41) + else + return quest:event(42) + end + end, + }, + + onEventFinish = + { + [43] = odysseanPassageOnEventFinish, + }, + }, + + [xi.zone.PORT_BASTOK] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Galka'] = suspiciousCityNpc, + + onEventFinish = + { + [419] = tradeEventFinish, + [421] = odysseanPassageOnEventFinish, + }, + }, + + [xi.zone.PORT_WINDURST] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Tarutaru'] = suspiciousCityNpc, + + onEventFinish = + { + [881] = tradeEventFinish, + [883] = odysseanPassageOnEventFinish, + }, + }, + + [xi.zone.FERETORY] = + { + onZoneIn = + { + function(player, prevZone) + return 2 + end, + }, + + onEventUpdate = + { + [2] = function(player, csid, option, npc) + if option == 1 then + -- TODO: Character appearance has to be encoded here to make the CS show the right character + player:updateEvent(7, 10, 2, 1024, 2, 0, 0, 0) + end + end, + }, + + onEventFinish = + { + [2] = function(player, csid, option, npc) + quest:complete(player) + end, + }, + }, + }, + + -- NOTE: The default events for the Suspicious NPCs require additional work to support + -- the items that they can provide, and will most likely need support in the monstrosity + -- global, along with event update/finish wiring here. + { + check = function(player, status, vars) + return status == QUEST_COMPLETED + end, + + [xi.zone.PASHHOW_MARSHLANDS] = + { + + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Hume'] = quest:event(45):replaceDefault(), + + onEventFinish = + { + [43] = odysseanPassageOnEventFinish, + } + }, + + [xi.zone.NORTHERN_SAN_DORIA] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Elvaan'] = quest:event(886):replaceDefault(), -- TODO: 886 may be once per zone or once, 887 after + + onEventFinish = + { + [885] = odysseanPassageOnEventFinish, + } + }, + + [xi.zone.PORT_BASTOK] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Galka'] = quest:event(422):replaceDefault(), + + onEventFinish = + { + [421] = odysseanPassageOnEventFinish, + } + }, + + [xi.zone.PORT_WINDURST] = + { + ['Odyssean_Passage'] = odysseanPassageNpc, + ['Suspicious_Tarutaru'] = quest:event(884):replaceDefault(), + + onEventFinish = + { + [883] = odysseanPassageOnEventFinish, + } + }, + }, +} + +return quest diff --git a/scripts/zones/Feretory/DefaultActions.lua b/scripts/zones/Feretory/DefaultActions.lua new file mode 100644 index 00000000000..0de848f990a --- /dev/null +++ b/scripts/zones/Feretory/DefaultActions.lua @@ -0,0 +1,5 @@ +-- local ID = zones[xi.zone.FERETORY] + +return { + ['Suibhne'] = { event = 11 }, +} diff --git a/scripts/zones/Feretory/IDs.lua b/scripts/zones/Feretory/IDs.lua index 438d1708c66..7f9c02c4806 100644 --- a/scripts/zones/Feretory/IDs.lua +++ b/scripts/zones/Feretory/IDs.lua @@ -15,6 +15,10 @@ zones[xi.zone.FERETORY] = LOGIN_CAMPAIGN_UNDERWAY = 7002, -- The [/January/February/March/April/May/June/July/August/September/October/November/December] Login Campaign is currently underway! LOGIN_NUMBER = 7003, -- In celebration of your most recent login (login no. ), we have provided you with points! You currently have a total of points. MEMBERS_LEVELS_ARE_RESTRICTED = 7023, -- Your party is unable to participate because certain members' levels are restricted. + MAY_POSSESS_BEASTS = 7330, -- You may now possess [lapinions/sheep/behemoths/elasmoths/cerebruses/orthruses]! + THY_BRAZEN_DISREGARD = 7349, -- Thy brazen disregard to count correctly is an affront to monipulators everywhere. Return whenas thou hast the meet amount of infamy. + YOU_LEARNED_INSTINCT = 7354, -- You learned ! + MAY_POSSESS_BEES = 7382, -- You may now possess bees! }, mob = { diff --git a/scripts/zones/Feretory/Zone.lua b/scripts/zones/Feretory/Zone.lua index dbd7762b148..70316be4be8 100644 --- a/scripts/zones/Feretory/Zone.lua +++ b/scripts/zones/Feretory/Zone.lua @@ -1,26 +1,32 @@ ----------------------------------- -- Zone: Feretory ----------------------------------- +require('scripts/globals/monstrosity') +----------------------------------- local zoneObject = {} zoneObject.onInitialize = function(zone) + -- Unused end zoneObject.onZoneIn = function(player, prevZone) - local cs = -1 - - player:setPos(-358.000, -3.400, -440.00, 63) + return xi.monstrosity.feretoryOnZoneIn(player, prevZone) +end - return cs +zoneObject.onZoneOut = function(player) + xi.monstrosity.feretoryOnZoneOut(player) end zoneObject.onTriggerAreaEnter = function(player, triggerArea) + -- Unused end zoneObject.onEventUpdate = function(player, csid, option, npc) + xi.monstrosity.feretoryOnEventUpdate(player, csid, option, npc) end zoneObject.onEventFinish = function(player, csid, option, npc) + xi.monstrosity.feretoryOnEventFinish(player, csid, option, npc) end return zoneObject diff --git a/scripts/zones/Feretory/npcs/Aengus.lua b/scripts/zones/Feretory/npcs/Aengus.lua new file mode 100644 index 00000000000..53cc6a99171 --- /dev/null +++ b/scripts/zones/Feretory/npcs/Aengus.lua @@ -0,0 +1,26 @@ +----------------------------------- +-- Area: Feretory +-- NPC: Aengus +-- !pos TODO +----------------------------------- +require('scripts/globals/monstrosity') +----------------------------------- +local entity = {} + +entity.onTrade = function(player, npc, trade) + xi.monstrosity.aengusOnTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + xi.monstrosity.aengusOnTrigger(player, npc) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.monstrosity.aengusOnEventUpdate(player, csid, option, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + xi.monstrosity.aengusOnEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Feretory/npcs/Maccus.lua b/scripts/zones/Feretory/npcs/Maccus.lua new file mode 100644 index 00000000000..e56ee4d347c --- /dev/null +++ b/scripts/zones/Feretory/npcs/Maccus.lua @@ -0,0 +1,26 @@ +----------------------------------- +-- Area: Feretory +-- NPC: Maccus +-- !pos TODO +----------------------------------- +require('scripts/globals/monstrosity') +----------------------------------- +local entity = {} + +entity.onTrade = function(player, npc, trade) + xi.monstrosity.maccusOnTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + xi.monstrosity.maccusOnTrigger(player, npc) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.monstrosity.maccusOnEventUpdate(player, csid, option, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + xi.monstrosity.maccusOnEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Feretory/npcs/Odyssean_Passage.lua b/scripts/zones/Feretory/npcs/Odyssean_Passage.lua new file mode 100644 index 00000000000..59e8cfa4462 --- /dev/null +++ b/scripts/zones/Feretory/npcs/Odyssean_Passage.lua @@ -0,0 +1,24 @@ +----------------------------------- +-- Area: Feretory +-- NPC: Odyssean Passage +-- !pos TODO +----------------------------------- +local entity = {} + +entity.onTrade = function(player, npc, trade) + xi.monstrosity.odysseanPassageOnTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + xi.monstrosity.odysseanPassageOnTrigger(player, npc) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.monstrosity.odysseanPassageOnEventUpdate(player, csid, option, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + xi.monstrosity.odysseanPassageOnEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Feretory/npcs/Suibhne.lua b/scripts/zones/Feretory/npcs/Suibhne.lua new file mode 100644 index 00000000000..926eeb6e09e --- /dev/null +++ b/scripts/zones/Feretory/npcs/Suibhne.lua @@ -0,0 +1,20 @@ +----------------------------------- +-- Area: Feretory +-- NPC: Suibhne +-- !pos -366 -3.612 -466 285 +----------------------------------- +local entity = {} + +entity.onTrade = function(player, npc, trade) +end + +entity.onTrigger = function(player, npc) +end + +entity.onEventUpdate = function(player, csid, option, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Feretory/npcs/Teyrnon.lua b/scripts/zones/Feretory/npcs/Teyrnon.lua new file mode 100644 index 00000000000..1cfb270e35a --- /dev/null +++ b/scripts/zones/Feretory/npcs/Teyrnon.lua @@ -0,0 +1,26 @@ +----------------------------------- +-- Area: Feretory +-- NPC: Teyrnon +-- !pos TODO +----------------------------------- +require('scripts/globals/monstrosity') +----------------------------------- +local entity = {} + +entity.onTrade = function(player, npc, trade) + xi.monstrosity.teyrnonOnTrade(player, npc, trade) +end + +entity.onTrigger = function(player, npc) + xi.monstrosity.teyrnonOnTrigger(player, npc) +end + +entity.onEventUpdate = function(player, csid, option, npc) + xi.monstrosity.teyrnonOnEventUpdate(player, csid, option, npc) +end + +entity.onEventFinish = function(player, csid, option, npc) + xi.monstrosity.teyrnonOnEventFinish(player, csid, option, npc) +end + +return entity diff --git a/scripts/zones/Northern_San_dOria/DefaultActions.lua b/scripts/zones/Northern_San_dOria/DefaultActions.lua index 3e3c6ce353d..b03b160238d 100644 --- a/scripts/zones/Northern_San_dOria/DefaultActions.lua +++ b/scripts/zones/Northern_San_dOria/DefaultActions.lua @@ -1,32 +1,34 @@ local ID = zones[xi.zone.NORTHERN_SAN_DORIA] return { - ['Abeaule'] = { text = ID.text.ABEAULE_DIALOG_THANKS }, - ['Abioleget'] = { text = ID.text.ABIOLEGET_DIALOG }, - ['Ailbeche'] = { event = 868 }, - ['Aivedoir'] = { text = ID.text.AIVEDOIR_DIALOG }, - ['Arienh'] = { text = ID.text.ARIENH_DIALOG }, - ['Charlaimagnat'] = { event = 702 }, - ['Chasalvige'] = { event = 6 }, - ['Emilia'] = { text = ID.text.EMILIA_DIALOG }, - ['Eperdur'] = { event = 678 }, - ['Fittesegat'] = { text = ID.text.FITTESEGAT_DIALOG }, - ['Gilipese'] = { text = ID.text.GILIPESE_DIALOG }, - ['Guilerme'] = { text = ID.text.GUILERME_DIALOG }, - ['Helaku'] = { event = 541 }, - ['Hinaree'] = { event = 580 }, - ['Ishwar'] = { text = ID.text.ISHWAR_DIALOG }, - ['Kasaroro'] = { event = 548 }, - ['Malfine'] = { text = ID.text.MALFINE_DIALOG }, - ['Maurine'] = { text = ID.text.MAURINE_DIALOG }, -- NOTE: These are two different NPCs - ['Maurinne'] = { text = ID.text.MAURINNE_DIALOG }, - ['Miageau'] = { event = 517 }, - ['Morjean'] = { event = 601 }, - ['Nouveil'] = { event = 574 }, - ['Pellimie'] = { text = ID.text.PELLIMIE_DIALOG }, - ['Pepigort'] = { text = ID.text.PEPIGORT_DIALOG }, - ['Phaviane'] = { text = ID.text.PHAVIANE_DIALOG }, - ['Prerivon'] = { text = ID.text.PRERIVON_DIALOG }, - ['Rodaillece'] = { text = ID.text.RODAILLECE_DIALOG }, - ['Sochiene'] = { text = ID.text.SOCHIENE_DIALOG }, + ['Abeaule'] = { text = ID.text.ABEAULE_DIALOG_THANKS }, + ['Abioleget'] = { text = ID.text.ABIOLEGET_DIALOG }, + ['Ailbeche'] = { event = 868 }, + ['Aivedoir'] = { text = ID.text.AIVEDOIR_DIALOG }, + ['Arienh'] = { text = ID.text.ARIENH_DIALOG }, + ['Charlaimagnat'] = { event = 702 }, + ['Chasalvige'] = { event = 6 }, + ['Emilia'] = { text = ID.text.EMILIA_DIALOG }, + ['Eperdur'] = { event = 678 }, + ['Fittesegat'] = { text = ID.text.FITTESEGAT_DIALOG }, + ['Gilipese'] = { text = ID.text.GILIPESE_DIALOG }, + ['Guilerme'] = { text = ID.text.GUILERME_DIALOG }, + ['Helaku'] = { event = 541 }, + ['Hinaree'] = { event = 580 }, + ['Ishwar'] = { text = ID.text.ISHWAR_DIALOG }, + ['Kasaroro'] = { event = 548 }, + ['Malfine'] = { text = ID.text.MALFINE_DIALOG }, + ['Maurine'] = { text = ID.text.MAURINE_DIALOG }, -- NOTE: These are two different NPCs + ['Maurinne'] = { text = ID.text.MAURINNE_DIALOG }, + ['Miageau'] = { event = 517 }, + ['Morjean'] = { event = 601 }, + ['Nouveil'] = { event = 574 }, + ['Odyssean_Passage'] = { messageSpecial = ID.text.NOTHING_HAPPENS }, + ['Pellimie'] = { text = ID.text.PELLIMIE_DIALOG }, + ['Pepigort'] = { text = ID.text.PEPIGORT_DIALOG }, + ['Phaviane'] = { text = ID.text.PHAVIANE_DIALOG }, + ['Prerivon'] = { text = ID.text.PRERIVON_DIALOG }, + ['Rodaillece'] = { text = ID.text.RODAILLECE_DIALOG }, + ['Sochiene'] = { text = ID.text.SOCHIENE_DIALOG }, + ['Suspicious_Elvaan'] = { event = 881 }, } diff --git a/scripts/zones/Northern_San_dOria/IDs.lua b/scripts/zones/Northern_San_dOria/IDs.lua index efa80bb027c..91ced81add3 100644 --- a/scripts/zones/Northern_San_dOria/IDs.lua +++ b/scripts/zones/Northern_San_dOria/IDs.lua @@ -122,6 +122,7 @@ zones[xi.zone.NORTHERN_SAN_DORIA] = FRAGMENT_FAR_TOO_SMALL = 18119, -- You obtain . However, it is far too small to house an adequate amount of energy. Alone, it serves no purpose. FRAGMENTS_MELD = 18120, -- The tiny fragments of Lilisette's memory meld together to form ! RETRIEVE_DIALOG_ID = 18155, -- You retrieve from the porter moogle's care. + NOTHING_HAPPENS = 18321, -- Nothing Happens... COMMON_SENSE_SURVIVAL = 18501, -- It appears that you have arrived at a new survival guide provided by the Adventurers' Mutual Aid Network. Common sense dictates that you should now be able to teleport here from similar tomes throughout the world. MAP_MARKER_TUTORIAL = 18623, -- Selecting Map from the main menu opens the map of the area in which you currently reside. Select Markers and press the right arrow key to see all the markers placed on your map. }, diff --git a/scripts/zones/Pashhow_Marshlands/DefaultActions.lua b/scripts/zones/Pashhow_Marshlands/DefaultActions.lua index 34c7498830f..5be95baa3c8 100644 --- a/scripts/zones/Pashhow_Marshlands/DefaultActions.lua +++ b/scripts/zones/Pashhow_Marshlands/DefaultActions.lua @@ -1,5 +1,6 @@ local ID = zones[xi.zone.PASHHOW_MARSHLANDS] return { - ['Outpost_Gate'] = { messageSpecial = ID.text.GATE_IS_LOCKED }, + ['Odyssean_Passage'] = { messageSpecial = ID.text.NOTHING_HAPPENS }, + ['Outpost_Gate'] = { messageSpecial = ID.text.GATE_IS_LOCKED }, } diff --git a/scripts/zones/Port_Bastok/DefaultActions.lua b/scripts/zones/Port_Bastok/DefaultActions.lua index 28a70629286..61981605416 100644 --- a/scripts/zones/Port_Bastok/DefaultActions.lua +++ b/scripts/zones/Port_Bastok/DefaultActions.lua @@ -1,34 +1,36 @@ local ID = zones[xi.zone.PORT_BASTOK] return { - ['Agapito'] = { event = 17 }, - ['Bartolomeo'] = { text = ID.text.ARRIVING_PASSENGER_DIALOG }, - ['Carmelo'] = { event = 182 }, - ['Clarion_Star'] = { event = 442 }, - ['Corann'] = { event = 38 }, - ['Dehlner'] = { event = 46 }, - ['Ehrhard'] = { event = 47 }, - ['Ensetsu'] = { event = 27 }, - ['Evi'] = { event = 21 }, - ['Ferrol'] = { event = 254 }, - ['Gudav'] = { event = 31 }, - ['Hilda'] = { event = 48 }, - ['Juroro'] = { event = 253 }, - ['Kaede'] = { event = 26 }, - ['Kagetora'] = { event = 23 }, - ['Kurando'] = { event = 28 }, - ['Latifah'] = { event = 13 }, - ['Mine_Konte'] = { event = 42 }, - ['Oggbi'] = { event = 230 }, - ['Otto'] = { event = 20 }, - ['Panana'] = { event = 43 }, - ['Paujean'] = { event = 25 }, - ['Powhatan'] = { text = ID.text.POWHATAN_DIALOG_1 }, - ['Qiji'] = { event = 33 }, - ['Rafaela'] = { event = 22 }, - ['Romilda'] = { event = 34 }, - ['Ronan'] = { event = 37 }, - ['Steel_Bones'] = { event = 29 }, - ['Tete'] = { event = 35 }, - ['Yazan'] = { event = 190 }, + ['Agapito'] = { event = 17 }, + ['Bartolomeo'] = { text = ID.text.ARRIVING_PASSENGER_DIALOG }, + ['Carmelo'] = { event = 182 }, + ['Clarion_Star'] = { event = 442 }, + ['Corann'] = { event = 38 }, + ['Dehlner'] = { event = 46 }, + ['Ehrhard'] = { event = 47 }, + ['Ensetsu'] = { event = 27 }, + ['Evi'] = { event = 21 }, + ['Ferrol'] = { event = 254 }, + ['Gudav'] = { event = 31 }, + ['Hilda'] = { event = 48 }, + ['Juroro'] = { event = 253 }, + ['Kaede'] = { event = 26 }, + ['Kagetora'] = { event = 23 }, + ['Kurando'] = { event = 28 }, + ['Latifah'] = { event = 13 }, + ['Mine_Konte'] = { event = 42 }, + ['Odyssean_Passage'] = { messageSpecial = ID.text.NOTHING_HAPPENS }, + ['Oggbi'] = { event = 230 }, + ['Otto'] = { event = 20 }, + ['Panana'] = { event = 43 }, + ['Paujean'] = { event = 25 }, + ['Powhatan'] = { text = ID.text.POWHATAN_DIALOG_1 }, + ['Qiji'] = { event = 33 }, + ['Rafaela'] = { event = 22 }, + ['Romilda'] = { event = 34 }, + ['Ronan'] = { event = 37 }, + ['Steel_Bones'] = { event = 29 }, + ['Suspicious_Galka'] = { event = 417 }, + ['Tete'] = { event = 35 }, + ['Yazan'] = { event = 190 }, } diff --git a/scripts/zones/Port_Bastok/IDs.lua b/scripts/zones/Port_Bastok/IDs.lua index 7be0e34213a..8cd96c5655d 100644 --- a/scripts/zones/Port_Bastok/IDs.lua +++ b/scripts/zones/Port_Bastok/IDs.lua @@ -76,6 +76,7 @@ zones[xi.zone.PORT_BASTOK] = OBTAINED_GUILD_POINTS = 12691, -- Obtained: guild points. OBTAINED_NUM_KEYITEMS = 13084, -- Obtained key item: ! NOT_ACQUAINTED = 13086, -- I'm sorry, but I don't believe we're acquainted. Please leave me be. + NOTHING_HAPPENS = 13237, -- Nothing Happens... }, mob = { diff --git a/scripts/zones/Port_Windurst/DefaultActions.lua b/scripts/zones/Port_Windurst/DefaultActions.lua index 09e2b150068..fcfd6f487ac 100644 --- a/scripts/zones/Port_Windurst/DefaultActions.lua +++ b/scripts/zones/Port_Windurst/DefaultActions.lua @@ -1,18 +1,20 @@ --- local ID = zones[xi.zone.PORT_WINDURST] +local ID = zones[xi.zone.PORT_WINDURST] return { - ['Ada'] = { event = 44 }, - ['Eki_Kamalabi'] = { event = 10005 }, - ['Gold_Skull'] = { event = 43 }, - ['Gomada-Vulmada'] = { event = 363 }, - ['Hakkuru-Rinkuru'] = { event = 224 }, - ['Josef'] = { event = 45 }, - ['Kohlo-Lakolo'] = { event = 361 }, - ['Melek'] = { event = 42 }, - ['Papo-Hopo'] = { event = 362 }, - ['Pichichi'] = { event = 364 }, - ['Pyo_Nzon'] = { event = 366 }, - ['Shanruru'] = { event = 367 }, - ['Tokaka'] = { event = 207 }, - ['Yafa_Yaa'] = { event = 365 }, + ['Ada'] = { event = 44 }, + ['Eki_Kamalabi'] = { event = 10005 }, + ['Gold_Skull'] = { event = 43 }, + ['Gomada-Vulmada'] = { event = 363 }, + ['Hakkuru-Rinkuru'] = { event = 224 }, + ['Josef'] = { event = 45 }, + ['Kohlo-Lakolo'] = { event = 361 }, + ['Melek'] = { event = 42 }, + ['Odyssean_Passage'] = { messageSpecial = ID.text.NOTHING_HAPPENS }, + ['Papo-Hopo'] = { event = 362 }, + ['Pichichi'] = { event = 364 }, + ['Pyo_Nzon'] = { event = 366 }, + ['Shanruru'] = { event = 367 }, + ['Suspicious_Tarutaru'] = { event = 879 }, + ['Tokaka'] = { event = 207 }, + ['Yafa_Yaa'] = { event = 365 }, } diff --git a/scripts/zones/Port_Windurst/IDs.lua b/scripts/zones/Port_Windurst/IDs.lua index f036fd1993c..f2454559a78 100644 --- a/scripts/zones/Port_Windurst/IDs.lua +++ b/scripts/zones/Port_Windurst/IDs.lua @@ -72,6 +72,7 @@ zones[xi.zone.PORT_WINDURST] = RETRIEVE_DIALOG_ID = 15915, -- You retrieve from the porter moogle's care. OBTAINED_NUM_KEYITEMS = 15957, -- Obtained key item: ! NOT_ACQUAINTED = 15959, -- I'm sorry, but I don't believe we're acquainted. Please leave me be. + NOTHING_HAPPENS = 16117, -- Nothing Happens... COMMON_SENSE_SURVIVAL = 16329, -- It appears that you have arrived at a new survival guide provided by the Adventurers' Mutual Aid Network. Common sense dictates that you should now be able to teleport here from similar tomes throughout the world. }, mob = diff --git a/settings/default/main.lua b/settings/default/main.lua index bcad16499a4..45a57d316e0 100644 --- a/settings/default/main.lua +++ b/settings/default/main.lua @@ -64,11 +64,26 @@ xi.settings.main = CAP_CURRENCY_VALOR = 50000, -- Magian Trials - ENABLE_MAGIAN_TRIALS = 1, + ENABLE_MAGIAN_TRIALS = 1, -- VoidWalker ENABLE_VOIDWALKER = 1, + -- Monstrosity (Heavily in development, use at your own risk!) + ENABLE_MONSTROSITY = 0, + MONSTROSITY_INFAMY_RATIO = 0.1, -- (float) The ratio of exp gained to infamy gained on defeating a mob. + MONSTROSITY_INFAMY_MESSAGING = 0, -- Show a message when you gain infamy. + MONSTROSITY_TELEPORT_TO_FERETORY = 0, -- Return to Feretory instead of the zone where you entered Feretory when Relinquishing or after death. + MONSTROSITY_TRIGGER_NPCS = 0, -- Allow Monipulators to trigger NPCs outside of the Feretory. + MONSTROSITY_DONT_WIPE_BUFFS = 0, -- If set, buffs won't be wiped when changing species in the Feretory. + + -- Monstrosity PVP Mode + -- 0: Retail (fully restricted): Monipulators and Players must both be flagged for Beligerency before they can fight + -- 1: (partially restricted): Players do not need to be flagged to fight, but Monipulators do. + -- 2: (open): Belligerency is not needed for Players and Monipulators to fight. + MONSTROSITY_PVP_MODE = 0, + MONSTROSITY_PVP_ZONE_BYPASS = 0, -- Show the full zone teleport menu from Feretory while Belligerency is flagged. + -- TREASURE CASKETS -- Retail droprate = 0.1 (10%) with no other effects active -- Set to 0 to disable caskets. diff --git a/sql/abilities.sql b/sql/abilities.sql index d7e07bce7cd..9f7bc9c13cb 100644 --- a/sql/abilities.sql +++ b/sql/abilities.sql @@ -386,7 +386,7 @@ INSERT INTO `abilities` VALUES (378,'odyllic_subterfuge',22,96,4,3600,131,0,0,27 INSERT INTO `abilities` VALUES (379,'ward',22,1,1,0,142,0,0,0,2000,0,6,0.0,0,0,0,0,0,'SOA'); INSERT INTO `abilities` VALUES (380,'effusion',22,1,1,0,143,0,0,0,2000,0,6,0.0,0,0,0,0,0,'SOA'); INSERT INTO `abilities` VALUES (381,'chocobo_jig_ii',19,70,1,60,218,126,0,13,2000,0,14,10.0,1,1,300,0,0,'SOA'); --- INSERT INTO `abilities` VALUES (382,'relinquish',23,1,1,60,253,0,0,0,0,0,6,0.0,0,0,0,0,0,NULL); +INSERT INTO `abilities` VALUES (382,'relinquish',23,1,1,60,253,0,0,312,0,0,6,0.0,0,0,0,0,0,NULL); INSERT INTO `abilities` VALUES (383,'vivacious_pulse',22,65,1,60,242,102,0,327,2000,0,6,0.0,0,0,0,0,0,'SOA'); INSERT INTO `abilities` VALUES (384,'contradance',19,50,1,300,229,0,0,329,2000,0,6,0.0,0,0,0,0,0,NULL); -- check animation INSERT INTO `abilities` VALUES (385,'apogee',15,70,1,180,108,100,0,94,2000,0,6,0.0,0,1,80,0,0,'SOA'); diff --git a/sql/char_monstrosity.sql b/sql/char_monstrosity.sql new file mode 100644 index 00000000000..ecadc8f99d4 --- /dev/null +++ b/sql/char_monstrosity.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS `char_monstrosity`; +CREATE TABLE `char_monstrosity` ( + `charid` int(10) unsigned NOT NULL, + `current_monstrosity_id` smallint(3) unsigned NOT NULL DEFAULT 0, + `current_monstrosity_species` smallint(3) unsigned NOT NULL DEFAULT 0, + `current_monstrosity_name_prefix_1` smallint(3) unsigned NOT NULL DEFAULT 0, + `current_monstrosity_name_prefix_2` smallint(3) unsigned NOT NULL DEFAULT 0, + `current_exp` int(3) unsigned NOT NULL DEFAULT 0, + `equip` blob DEFAULT NULL, + `levels` blob DEFAULT NULL, + `instincts` blob DEFAULT NULL, + `variants` blob DEFAULT NULL, + `belligerency` tinyint(3) unsigned NOT NULL DEFAULT 0, + `entry_x` float(7,3) NOT NULL DEFAULT '0.000', + `entry_y` float(7,3) NOT NULL DEFAULT '0.000', + `entry_z` float(7,3) NOT NULL DEFAULT '0.000', + `entry_rot` tinyint(3) unsigned NOT NULL DEFAULT '0', + `entry_zone_id` smallint(3) unsigned NOT NULL DEFAULT '0', + `entry_mjob` tinyint(2) unsigned NOT NULL DEFAULT '0', + `entry_sjob` tinyint(2) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`charid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/sql/monstrosity_exp_table.sql b/sql/monstrosity_exp_table.sql new file mode 100644 index 00000000000..57751cf4a0e --- /dev/null +++ b/sql/monstrosity_exp_table.sql @@ -0,0 +1,109 @@ +DROP TABLE IF EXISTS `monstrosity_exp_table`; +CREATE TABLE IF NOT EXISTS `monstrosity_exp_table` ( + `level` tinyint(2) NOT NULL, + `amount` smallint(4) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`level`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +-- https://www.bg-wiki.com/ffxi/Category:Monstrosity +INSERT INTO `monstrosity_exp_table` VALUES (1, 300); +INSERT INTO `monstrosity_exp_table` VALUES (2, 400); +INSERT INTO `monstrosity_exp_table` VALUES (3, 500); +INSERT INTO `monstrosity_exp_table` VALUES (4, 700); +INSERT INTO `monstrosity_exp_table` VALUES (5, 800); +INSERT INTO `monstrosity_exp_table` VALUES (6, 800); +INSERT INTO `monstrosity_exp_table` VALUES (7, 900); +INSERT INTO `monstrosity_exp_table` VALUES (8, 1000); +INSERT INTO `monstrosity_exp_table` VALUES (9, 1100); +INSERT INTO `monstrosity_exp_table` VALUES (10, 1200); +INSERT INTO `monstrosity_exp_table` VALUES (11, 1350); +INSERT INTO `monstrosity_exp_table` VALUES (12, 1500); +INSERT INTO `monstrosity_exp_table` VALUES (13, 1650); +INSERT INTO `monstrosity_exp_table` VALUES (14, 1800); +INSERT INTO `monstrosity_exp_table` VALUES (15, 1950); +INSERT INTO `monstrosity_exp_table` VALUES (16, 2100); +INSERT INTO `monstrosity_exp_table` VALUES (17, 2250); +INSERT INTO `monstrosity_exp_table` VALUES (18, 2400); +INSERT INTO `monstrosity_exp_table` VALUES (19, 2550); +INSERT INTO `monstrosity_exp_table` VALUES (20, 2700); +INSERT INTO `monstrosity_exp_table` VALUES (21, 2850); +INSERT INTO `monstrosity_exp_table` VALUES (22, 3000); +INSERT INTO `monstrosity_exp_table` VALUES (23, 3100); +INSERT INTO `monstrosity_exp_table` VALUES (24, 3200); +INSERT INTO `monstrosity_exp_table` VALUES (25, 3300); +INSERT INTO `monstrosity_exp_table` VALUES (26, 3400); +INSERT INTO `monstrosity_exp_table` VALUES (27, 3500); +INSERT INTO `monstrosity_exp_table` VALUES (28, 3600); +INSERT INTO `monstrosity_exp_table` VALUES (29, 3700); +INSERT INTO `monstrosity_exp_table` VALUES (30, 3800); +INSERT INTO `monstrosity_exp_table` VALUES (31, 3900); +INSERT INTO `monstrosity_exp_table` VALUES (32, 4000); +INSERT INTO `monstrosity_exp_table` VALUES (33, 4100); +INSERT INTO `monstrosity_exp_table` VALUES (34, 4200); +INSERT INTO `monstrosity_exp_table` VALUES (35, 4300); +INSERT INTO `monstrosity_exp_table` VALUES (36, 4400); +INSERT INTO `monstrosity_exp_table` VALUES (37, 4500); +INSERT INTO `monstrosity_exp_table` VALUES (38, 4600); +INSERT INTO `monstrosity_exp_table` VALUES (39, 4700); +INSERT INTO `monstrosity_exp_table` VALUES (40, 4800); +INSERT INTO `monstrosity_exp_table` VALUES (41, 4900); +INSERT INTO `monstrosity_exp_table` VALUES (42, 5000); +INSERT INTO `monstrosity_exp_table` VALUES (43, 5100); +INSERT INTO `monstrosity_exp_table` VALUES (44, 5200); +INSERT INTO `monstrosity_exp_table` VALUES (45, 5300); +INSERT INTO `monstrosity_exp_table` VALUES (46, 5400); +INSERT INTO `monstrosity_exp_table` VALUES (47, 5500); +INSERT INTO `monstrosity_exp_table` VALUES (48, 5600); +INSERT INTO `monstrosity_exp_table` VALUES (49, 5700); +INSERT INTO `monstrosity_exp_table` VALUES (50, 5800); +INSERT INTO `monstrosity_exp_table` VALUES (51, 6000); +INSERT INTO `monstrosity_exp_table` VALUES (52, 6400); +INSERT INTO `monstrosity_exp_table` VALUES (53, 6800); +INSERT INTO `monstrosity_exp_table` VALUES (54, 7200); +INSERT INTO `monstrosity_exp_table` VALUES (55, 7600); +INSERT INTO `monstrosity_exp_table` VALUES (56, 8000); +INSERT INTO `monstrosity_exp_table` VALUES (57, 8400); +INSERT INTO `monstrosity_exp_table` VALUES (58, 8800); +INSERT INTO `monstrosity_exp_table` VALUES (59, 9200); +INSERT INTO `monstrosity_exp_table` VALUES (60, 9600); +INSERT INTO `monstrosity_exp_table` VALUES (61, 10000); +INSERT INTO `monstrosity_exp_table` VALUES (62, 10500); +INSERT INTO `monstrosity_exp_table` VALUES (63, 11000); +INSERT INTO `monstrosity_exp_table` VALUES (64, 11500); +INSERT INTO `monstrosity_exp_table` VALUES (65, 12000); +INSERT INTO `monstrosity_exp_table` VALUES (66, 12500); +INSERT INTO `monstrosity_exp_table` VALUES (67, 13000); +INSERT INTO `monstrosity_exp_table` VALUES (68, 13500); +INSERT INTO `monstrosity_exp_table` VALUES (69, 14000); +INSERT INTO `monstrosity_exp_table` VALUES (70, 14500); +INSERT INTO `monstrosity_exp_table` VALUES (71, 15000); +INSERT INTO `monstrosity_exp_table` VALUES (72, 15500); +INSERT INTO `monstrosity_exp_table` VALUES (73, 16000); +INSERT INTO `monstrosity_exp_table` VALUES (74, 16500); +INSERT INTO `monstrosity_exp_table` VALUES (75, 17000); +INSERT INTO `monstrosity_exp_table` VALUES (76, 17500); +INSERT INTO `monstrosity_exp_table` VALUES (77, 18000); +INSERT INTO `monstrosity_exp_table` VALUES (78, 18500); +INSERT INTO `monstrosity_exp_table` VALUES (79, 19000); +INSERT INTO `monstrosity_exp_table` VALUES (80, 19500); +INSERT INTO `monstrosity_exp_table` VALUES (81, 20000); +INSERT INTO `monstrosity_exp_table` VALUES (82, 20500); +INSERT INTO `monstrosity_exp_table` VALUES (83, 21000); +INSERT INTO `monstrosity_exp_table` VALUES (84, 21500); +INSERT INTO `monstrosity_exp_table` VALUES (85, 22000); +INSERT INTO `monstrosity_exp_table` VALUES (86, 22500); +INSERT INTO `monstrosity_exp_table` VALUES (87, 23000); +INSERT INTO `monstrosity_exp_table` VALUES (88, 23500); +INSERT INTO `monstrosity_exp_table` VALUES (89, 24000); + +-- TODO: These are guessed, what are the exp requirements for these? +INSERT INTO `monstrosity_exp_table` VALUES (90, 24500); +INSERT INTO `monstrosity_exp_table` VALUES (91, 25000); +INSERT INTO `monstrosity_exp_table` VALUES (92, 25500); +INSERT INTO `monstrosity_exp_table` VALUES (93, 26000); +INSERT INTO `monstrosity_exp_table` VALUES (94, 26500); +INSERT INTO `monstrosity_exp_table` VALUES (95, 27000); +INSERT INTO `monstrosity_exp_table` VALUES (96, 27500); +INSERT INTO `monstrosity_exp_table` VALUES (97, 28000); +INSERT INTO `monstrosity_exp_table` VALUES (98, 28500); +INSERT INTO `monstrosity_exp_table` VALUES (99, 29000); diff --git a/sql/monstrosity_instinct_mods.sql b/sql/monstrosity_instinct_mods.sql new file mode 100644 index 00000000000..80f0995f93c --- /dev/null +++ b/sql/monstrosity_instinct_mods.sql @@ -0,0 +1,94 @@ +DROP TABLE IF EXISTS `monstrosity_instinct_mods`; +CREATE TABLE `monstrosity_instinct_mods` ( + `monstrosity_instinct_id` smallint(5) unsigned NOT NULL, + `modId` smallint(5) unsigned NOT NULL, + `value` smallint(5) NOT NULL DEFAULT 0, + PRIMARY KEY (`monstrosity_instinct_id`,`modId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +-- Rabbit instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (3,3,2); -- HPP: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (3,11,5); -- AGI: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (3,23,10); -- ATT: 10 + +-- Rabbit instinct II +INSERT INTO `monstrosity_instinct_mods` VALUES (4,3,3); -- HPP: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (4,9,5); -- DEX: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (4,68,10); -- EVA: 15 + +-- Rabbit instinct III +INSERT INTO `monstrosity_instinct_mods` VALUES (5,1,20); -- DEF: 20 +INSERT INTO `monstrosity_instinct_mods` VALUES (5,9,5); -- DEX: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (5,11,5); -- AGI: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (5,288,2); -- DOUBLE_ATTACK: 2 + +-- Mandragora instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (54,9,5); -- DEX: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (54,25,10); -- ACC: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (54,384,100); -- HASTE_GEAR: 100 + +-- Mandragora instinct II +INSERT INTO `monstrosity_instinct_mods` VALUES (55,1,10); -- DEF: 10 +INSERT INTO `monstrosity_instinct_mods` VALUES (55,13,5); -- MND: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (55,23,10); -- ATT: 10 +INSERT INTO `monstrosity_instinct_mods` VALUES (55,288,1); -- DOUBLE_ATTACK: 1 + +-- Mandragora instinct III +INSERT INTO `monstrosity_instinct_mods` VALUES (56,3,3); -- HPP: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (56,289,3); -- SUBTLE_BLOW: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (56,384,300); -- HASTE_GEAR: 300 + +-- Bee instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (81,11,5); -- AGI: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (81,23,10); -- ATT: 10 +INSERT INTO `monstrosity_instinct_mods` VALUES (81,68,5); -- EVA: 5 + +-- Bee instinct II +INSERT INTO `monstrosity_instinct_mods` VALUES (82,3,2); -- HPP: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (82,5,50); -- MP: 50 +INSERT INTO `monstrosity_instinct_mods` VALUES (82,12,5); -- INT: 5 + +-- Bee instinct III +INSERT INTO `monstrosity_instinct_mods` VALUES (83,8,10); -- STR: 10 +INSERT INTO `monstrosity_instinct_mods` VALUES (83,10,5); -- VIT: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (83,384,300); -- HASTE_GEAR: 300 + +-- Lizard instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (129,3,1); -- HPP: 1 +INSERT INTO `monstrosity_instinct_mods` VALUES (129,10,5); -- VIT: 5 +INSERT INTO `monstrosity_instinct_mods` VALUES (129,1,20); -- DEF: 20 + +-- Lizard instinct II +INSERT INTO `monstrosity_instinct_mods` VALUES (130,8,4); -- STR: 4 +INSERT INTO `monstrosity_instinct_mods` VALUES (130,23,14); -- ATT: 14 +INSERT INTO `monstrosity_instinct_mods` VALUES (130,73,3); -- STORETP: 3 + +-- Lizard instinct III +INSERT INTO `monstrosity_instinct_mods` VALUES (131,3,10); -- HPP: 10 +INSERT INTO `monstrosity_instinct_mods` VALUES (131,62,1); -- ATTP: 1 +INSERT INTO `monstrosity_instinct_mods` VALUES (131,1,30); -- DEF: 30 + +-- Hume's instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (768,8,2); -- STR: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,9,2); -- DEX: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,10,2); -- VIT: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,11,2); -- AGI: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,12,2); -- INT: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,13,2); -- MND: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (768,14,2); -- CHR: 2 + +-- Elvaan's instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (769,8,3); -- STR: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (769,13,3); -- MND: 3 + +-- Tarutaru's instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (770,6,2); -- MPP: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (770,12,3); -- INT: 3 + +-- Mithra's instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (771,9,3); -- DEX: 3 +INSERT INTO `monstrosity_instinct_mods` VALUES (771,11,3); -- AGI: 3 + +-- Galka's instinct I +INSERT INTO `monstrosity_instinct_mods` VALUES (772,3,2); -- HPP: 2 +INSERT INTO `monstrosity_instinct_mods` VALUES (772,10,3); -- VIT: 3 diff --git a/sql/monstrosity_instincts.sql b/sql/monstrosity_instincts.sql new file mode 100644 index 00000000000..8f08a38a11d --- /dev/null +++ b/sql/monstrosity_instincts.sql @@ -0,0 +1,298 @@ +DROP TABLE IF EXISTS `monstrosity_instincts`; +CREATE TABLE `monstrosity_instincts` ( + `monstrosity_instinct_id` smallint(30) unsigned NOT NULL, + `cost` smallint(30) unsigned NOT NULL, + `name` varchar(60) DEFAULT NULL, + PRIMARY KEY (`monstrosity_instinct_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +INSERT INTO `monstrosity_instincts` VALUES (3, 2, "Rabbit instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (4, 4, "Rabbit instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (5, 6, "Rabbit instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (6, 10, "Behemoth instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (7, 12, "Behemoth instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (8, 14, "Behemoth instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES ( 9, 4, "Tiger instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (10, 6, "Tiger instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (11, 8, "Tiger instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (12, 4, "Sheep instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (13, 6, "Sheep instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (14, 8, "Sheep instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (15, 4, "Ram instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (16, 6, "Ram instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (17, 8, "Ram instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (18, 4, "Dhalmel instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (19, 6, "Dhalmel instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (20, 8, "Dhalmel instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (21, 6, "Coeurl instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (22, 8, "Coeurl instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (23, 10, "Coeurl instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (24, 8, "Opo-opo instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (25, 10, "Opo-opo instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (26, 12, "Opo-opo instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (27, 6, "Manticore instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (28, 8, "Manticore instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (29, 10, "Manticore instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (30, 6, "Buffalo instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (31, 8, "Buffalo instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (32, 10, "Buffalo instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (33, 8, "Marid instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (34, 10, "Marid instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (35, 12, "Marid instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (36, 10, "Cerberus instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (37, 12, "Cerberus instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (38, 14, "Cerberus instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (39, 8, "Gnole instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (40, 10, "Gnole instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (41, 12, "Gnole instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (45, 4, "Funguar instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (46, 6, "Funguar instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (47, 8, "Funguar instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (48, 2, "Treant instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (49, 4, "Treant instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (50, 6, "Treant instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (51, 6, "Morbol instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (52, 8, "Morbol instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (53, 10, "Morbol instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (54, 2, "Mandragora instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (55, 4, "Mandragora instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (56, 6, "Mandragora instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (57, 4, "Sabotender instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (58, 6, "Sabotender instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (59, 8, "Sabotender instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (60, 6, "Flytrap instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (61, 8, "Flytrap instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (62, 10, "Flytrap instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (63, 4, "Goobbue instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (64, 6, "Goobbue instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (65, 8, "Goobbue instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (66, 8, "Rafflesia instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (67, 10, "Rafflesia instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (68, 12, "Rafflesia instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (69, 8, "Panopt instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (70, 10, "Panopt instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (71, 12, "Panopt instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (81, 2, "Bee instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (82, 4, "Bee instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (83, 6, "Bee instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (84, 2, "Beetle instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (85, 4, "Beetle instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (86, 6, "Beetle instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (87, 4, "Crawler instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (88, 6, "Crawler instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (89, 8, "Crawler instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (90, 4, "Fly instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (91, 6, "Fly instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (92, 8, "Fly instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (93, 4, "Scorpion instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (94, 6, "Scorpion instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (95, 8, "Scorpion instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (96, 6, "Spider instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (97, 8, "Spider instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (98, 10, "Spider instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES ( 99, 6, "Antlion instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (100, 8, "Antlion instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (101, 10, "Antlion instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (102, 8, "Diremite instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (103, 10, "Diremite instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (104, 12, "Diremite instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (105, 6, "Chigoe instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (106, 8, "Chigoe instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (107, 10, "Chigoe instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (108, 6, "Wamoura instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (109, 8, "Wamoura instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (110, 10, "Wamoura instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (111, 6, "Ladybug instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (112, 8, "Ladybug instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (113, 10, "Ladybug instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (114, 8, "Gnat instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (115, 10, "Gnat instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (116, 12, "Gnat instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (129, 2, "Lizard instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (130, 4, "Lizard instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (131, 6, "Lizard instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (132, 2, "Raptor instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (133, 4, "Raptor instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (134, 6, "Raptor instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (135, 8, "Adamantoise instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (136, 10, "Adamantoise instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (137, 12, "Adamantoise instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (138, 4, "Bugard instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (139, 6, "Bugard instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (140, 8, "Bugard instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (141, 4, "Eft instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (142, 6, "Eft instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (143, 8, "Eft instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (144, 6, "Wivre instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (145, 8, "Wivre instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (146, 10, "Wivre instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (147, 6, "Peiste instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (148, 8, "Peiste instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (149, 10, "Peiste instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (156, 4, "Slime instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (157, 6, "Slime instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (158, 8, "Slime instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (159, 4, "Hecteyes instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (160, 6, "Hecteyes instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (161, 8, "Hecteyes instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (162, 6, "Flan instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (163, 8, "Flan instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (164, 10, "Flan instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (168, 4, "Slug instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (169, 6, "Slug instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (170, 20, "Slug instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (171, 6, "Sandworm instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (172, 8, "Sandworm instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (173, 10, "Sandworm instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (174, 2, "Leech instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (175, 4, "Leech instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (176, 6, "Leech instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (180, 4, "Crab instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (181, 5, "Crab instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (182, 6, "Crab instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (183, 3, "Pugil instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (184, 4, "Pugil instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (185, 5, "Pugil instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (186, 5, "Sea Monk instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (187, 7, "Sea Monk instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (188, 9, "Sea Monk instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (189, 4, "Uragnite instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (190, 8, "Uragnite instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (191, 16, "Uragnite instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (192, 7, "Orobon instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (193, 8, "Orobon instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (194, 10, "Orobon instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (195, 8, "Ruszor instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (196, 10, "Ruszor instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (197, 15, "Ruszor instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (198, 2, "Toad instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (199, 5, "Toad instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (200, 30, "Toad instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (207, 3, "Bird instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (208, 5, "Bird instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (209, 7, "Bird instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (210, 2, "Cockatrice instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (211, 4, "Cockatrice instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (212, 6, "Cockatrice instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (213, 5, "Roc instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (214, 7, "Roc instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (215, 9, "Roc instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (216, 2, "Bat instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (217, 4, "Bat instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (218, 6, "Bat instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (219, 6, "Hippogryph instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (220, 8, "Hippogryph instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (221, 10, "Hippogryph instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (222, 8, "Apkallu instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (223, 10, "Apkallu instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (224, 12, "Apkallu instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (225, 6, "Colibri instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (226, 8, "Colibri instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (227, 10, "Colibri instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (228, 8, "Amphiptere instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (229, 10, "Amphiptere instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (230, 12, "Amphiptere instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (762, 1, "Astoltian slime instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (763, 2, "Astoltian slime instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (764, 3, "Astoltian slime instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (765, 1, "Eorzean spriggan instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (766, 2, "Eorzean spriggan instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (767, 3, "Eorzean spriggan instinct III"); + +INSERT INTO `monstrosity_instincts` VALUES (768, 3, "Hume's instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (769, 3, "Elvaan's instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (770, 3, "Tarutaru's instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (771, 3, "Mithra's instinct I"); +INSERT INTO `monstrosity_instincts` VALUES (772, 3, "Galka's instinct I"); + +INSERT INTO `monstrosity_instincts` VALUES (773, 3, "Hume's instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (774, 3, "Elvaan's instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (775, 3, "Tarutaru's instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (776, 3, "Mithra's instinct II"); +INSERT INTO `monstrosity_instincts` VALUES (777, 3, "Galka's instinct II"); + +INSERT INTO `monstrosity_instincts` VALUES (778, 15, "Warrior's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (779, 15, "Monk's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (780, 15, "White mage's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (781, 15, "Black mage's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (782, 15, "Red mage's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (783, 15, "Thief's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (784, 15, "Paladin's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (785, 15, "Dark knight's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (786, 15, "Beastmaster's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (787, 15, "Bard's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (788, 15, "Ranger's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (789, 15, "Samurai's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (790, 15, "Ninja's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (791, 15, "Dragoon's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (792, 15, "Summoner's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (793, 15, "Blue mage's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (794, 15, "Corsair's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (795, 15, "Puppetmaster's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (796, 15, "Dancer's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (797, 15, "Scholar's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (798, 15, "Geomancer's instinct"); +INSERT INTO `monstrosity_instincts` VALUES (799, 15, "Rune fencer's instinct"); diff --git a/sql/monstrosity_species.sql b/sql/monstrosity_species.sql new file mode 100644 index 00000000000..1e6ac6c7697 --- /dev/null +++ b/sql/monstrosity_species.sql @@ -0,0 +1,246 @@ +DROP TABLE IF EXISTS `monstrosity_species`; +CREATE TABLE `monstrosity_species` ( + `monstrosity_id` smallint(30) unsigned NOT NULL, + `monstrosity_species_code` smallint(30) unsigned NOT NULL, + `name` varchar(60) DEFAULT NULL, + `mjob` tinyint(3) unsigned NOT NULL, + `sjob` tinyint(3) unsigned NOT NULL, + `size` tinyint(3) unsigned NOT NULL, -- 0: small, 1: medium, 2: large + `look` smallint(3) unsigned NOT NULL, + PRIMARY KEY (`monstrosity_id`, `monstrosity_species_code`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +-- TODO: Double check the mjob/sjob values for everything using both resources: +-- https://ffxiclopedia.fandom.com/wiki/Category:Monipulators +-- https://www.bg-wiki.com/ffxi/Category:Monstrosity/Species + +-- NOTE: The mjob/sjob of MONs rise at the same level, so the only difference between which +-- : order you specify them is is which 2H ability you get. + +-- NOTE: Since there are so many variants of model that SEEM THE SAME BUT ACT DIFFERENTLY, +-- : guessing the model IDs isn't always good enough - they all need to eventually be +-- : properly captured. + +INSERT INTO `monstrosity_species` VALUES (1, 1, "Rabbit", 1, 1, 0, 0x010C); +INSERT INTO `monstrosity_species` VALUES (1, 256, "Onyx Rabbit", 1, 4, 0, 0x010D); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (1, 257, "Alabaster Rabbit", 1, 3, 0, 0x010E); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (1, 258, "Lapinion (Rabbit)", 3, 4, 0, 0x0791); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (2, 2, "Behemoth", 1, 6, 2, 0x0194); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (2, 259, "Elasamoth (Behemoth)", 8, 6, 2, 0x0195); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (3, 3, "Tiger", 1, 1, 1, 0x0134); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (3, 261, "Legendary Tiger", 1, 6, 1, 0x0135); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (3, 262, "Smilodon (Tiger)", 1, 1, 1, 0x08C8); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (4, 4, "Sheep", 1, 1, 1, 0x0154); +INSERT INTO `monstrosity_species` VALUES (4, 263, "Karakul (Sheep)", 1, 1, 1, 0x0951); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (5, 5, "Ram (Sheep)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (6, 6, "Dhalmel", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (7, 7, "Coeurl", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (7, 266, "Lynx (Coeurl)", 1, 4, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (7, 267, "Collared Lynx (Coeurl)", 5, 4, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (8, 8, "Opo-opo", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (9, 9, "Manticore", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (9, 268, "Legendary Manticore", 5, 4, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (10, 10, "Buffalo", 7, 7, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (11, 11, "Marid", 1, 1, 2, 0x06CA); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (12, 12, "Cerberus", 1, 1, 2, 0x0701); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (12, 269, "Orthrus (Cerberus)", 1, 4, 2, 0x0702); -- TODO + +INSERT INTO `monstrosity_species` VALUES (13, 13, "Gnole", 2, 2, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (13, 270, "Bipedal Gnole", 2, 2, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (15, 15, "Funguar", 1, 1, 0, 0x0178); +INSERT INTO `monstrosity_species` VALUES (15, 271, "Coppercap (Funguar)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (16, 16, "Treant Sapling", 1, 1, 0, 0x0188); +INSERT INTO `monstrosity_species` VALUES (16, 272, "Treant", 1, 4, 2, 0x0B3C); +INSERT INTO `monstrosity_species` VALUES (16, 273, "Flowering Treant", 1, 4, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (16, 274, "Scarlet-tinged Treant", 1, 4, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (16, 275, "Barren Treant", 1, 4, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (16, 276, "Necklaced Treant", 1, 4, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (17, 17, "Morbol", 1, 1, 2, 0x017C); +INSERT INTO `monstrosity_species` VALUES (17, 277, "Pygmy Morbol", 1, 1, 1, 0x0950); +INSERT INTO `monstrosity_species` VALUES (17, 278, "Scarce Morbol", 1, 1, 2, 0x00017D); +INSERT INTO `monstrosity_species` VALUES (17, 279, "Ameretat (Morbol)", 1, 1, 2, 0x08B6); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (17, 280, "Purbol (Morbol)", 1, 4, 2, 0x017F); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (18, 18, "Mandragora", 2, 2, 0, 0x012C); +INSERT INTO `monstrosity_species` VALUES (18, 281, "Korrigan (Mandragora)", 2, 4, 0, 0x012D); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (18, 282, "Lycopodium (Mandragora)", 2, 3, 0, 0x08C7); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (18, 283, "Pygmy Mandragora", 2, 2, 0, 0x089C); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (18, 284, "Adenium (Mandragora)", 2, 5, 0, 0x0950); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (18, 285, "Pachypodium (Mandragora)", 2, 2, 0, 0x0949); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (18, 286, "Enlightened Mandragora", 2, 2, 0, 0x0930); -- TODO: Look guessed not capped, this is the wrong id? +INSERT INTO `monstrosity_species` VALUES (18, 287, "New Year Mandragora", 2, 13, 0, 0x0B48); + +INSERT INTO `monstrosity_species` VALUES (19, 19, "Sabotender", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (19, 288, "Sabotender Florido", 1, 5, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (20, 20, "Flytrap", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (21, 21, "Goobbue", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (22, 22, "Rafflesia", 8, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (22, 289, "Mitrastema (Rafflesia)", 8, 4, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (23, 23, "Panopt", 4, 4, 0, 0x0205); + +INSERT INTO `monstrosity_species` VALUES (27, 27, "Bee", 1, 1, 0, 0x0110); +INSERT INTO `monstrosity_species` VALUES (27, 290, "Vermillion and Onyx Bee", 1, 5, 0, 0x0111); +INSERT INTO `monstrosity_species` VALUES (27, 291, "Zaffre Bee", 1, 4, 0, 0x0790); + +INSERT INTO `monstrosity_species` VALUES (28, 28, "Beetle", 7, 7, 0, 0x0198); +INSERT INTO `monstrosity_species` VALUES (28, 292, "Onyx Beetle", 7, 4, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (28, 293, "Gamboge Beetle", 7, 5, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (29, 29, "Crawler", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (29, 294, "Eruca (Crawler)", 1, 5, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (29, 295, "Emerald Crawler", 3, 4, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (29, 296, "Pygmy Emerald Crawler", 3, 4, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (30, 30, "Fly", 1, 1, 1, 0x01C0); +INSERT INTO `monstrosity_species` VALUES (30, 297, "Vermillion Fly", 1, 5, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (31, 31, "Scorpion", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (31, 298, "Scolopendrid (Scorpion)", 1, 4, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (31, 299, "Unusual Scolopendrid (Scorpion)", 8, 4, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (32, 32, "Spider", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (32, 300, "Reticulated Spider", 1, 8, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (32, 301, "Vermillion and Onyx Spider", 1, 5, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (33, 33, "Antlion", 1, 1, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (33, 302, "Onyx Antlion", 1, 4, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (33, 303, "Formiceros (Antlion)", 8, 4, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (34, 34, "Diremite", 8, 8, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (34, 304, "Arundemite (Diremite)", 8, 8, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (35, 35, "Chigoe", 6, 6, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (35, 305, "Azure Chigoe", 6, 4, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (36, 36, "Wamouracampa (Wamoura larva)", 7, 7, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (36, 306, "Coiled Wamouracampa (Wamoura larva)", 7, 7, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (36, 307, "Wamoura", 1, 7, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (36, 308, "Coral Wamoura", 4, 7, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (37, 37, "Ladybug", 6, 6, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (37, 309, "Gold Ladybug", 6, 5, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (38, 38, "Gnat", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (38, 310, "Midge (Gnat)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (43, 43, "Lizard", 1, 1, 0, 0x0148); +INSERT INTO `monstrosity_species` VALUES (43, 315, "Ashen Lizard", 1, 4, 0, 0x0149); + +INSERT INTO `monstrosity_species` VALUES (44, 44, "Raptor", 1, 1, 1, 0x013C); +INSERT INTO `monstrosity_species` VALUES (44, 316, "Emerald Raptor", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (44, 317, "Vermillion Raptor", 1, 1, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (45, 45, "Adamantoise", 1, 1, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (45, 318, "Pygmy Adamantoise", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (45, 319, "Legendary Adamantoise", 1, 1, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (45, 320, "Ferromantoise (Adamantoise)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (46, 46, "Bugard", 1, 1, 2, 0x0547); +INSERT INTO `monstrosity_species` VALUES (46, 321, "Abyssobugard (Bugard)", 1, 1, 2, 0x0548); + +INSERT INTO `monstrosity_species` VALUES (47, 47, "Eft", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (47, 322, "Tarichuk (Eft)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (48, 48, "Wivre", 1, 1, 2, 0x08B9); +INSERT INTO `monstrosity_species` VALUES (48, 323, "Unusual Wivre", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (49, 49, "Peiste", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (49, 324, "Sibilus (Peiste)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (52, 52, "Slime", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (52, 329, "Clot (Slime)", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (52, 330, "Gold Slime", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (52, 331, "Boil (Slime)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (53, 53, "Hecteyes", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (54, 54, "Flan", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (54, 332, "Gold Flan", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (54, 333, "Blancmange (Flan)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (56, 56, "Slug", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (57, 57, "Sandworm", 1, 1, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (57, 334, "Pygmy Sandworm", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (57, 335, "Gigaworm (Sandworm)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (58, 58, "Leech", 1, 1, 0, 0x0114); +INSERT INTO `monstrosity_species` VALUES (58, 336, "Azure Leech", 1, 1, 0, 0x0115); +INSERT INTO `monstrosity_species` VALUES (58, 337, "Obdella (Leech)", 1, 1, 0, 0x078E); -- TODO: Look guessed not capped + +INSERT INTO `monstrosity_species` VALUES (60, 60, "Crab", 1, 1, 0, 0x0164); +INSERT INTO `monstrosity_species` VALUES (60, 340, "Vermillion Crab", 1, 1, 0, 0x0165); -- TODO: Look guessed not capped +INSERT INTO `monstrosity_species` VALUES (60, 341, "Basket-burdened Crab", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (60, 342, "Vermillion Basket-burdened Crab", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (60, 343, "Porter Crab (Crab)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (61, 61, "Pugil", 1, 1, 0, 0x015C); +INSERT INTO `monstrosity_species` VALUES (61, 344, "Jagil (Pugil)", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (62, 62, "Sea Monk", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (62, 345, "Azure Sea Monk", 1, 1, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (63, 63, "Uragnite", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (63, 346, "Limascabra (Uragnite)", 1, 1, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (64, 64, "Orobon", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (64, 347, "Pygmy Orobon", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (64, 348, "Ogrebon (Orobon)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (65, 65, "Ruszor", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (66, 66, "Toad", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (66, 349, "Azure Toad", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (66, 350, "Vermillion Toad", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (69, 69, "Bird", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (69, 351, "Onyx Bird", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (70, 70, "Cockatrice", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (70, 352, "Ziz (Cockatrice)", 1, 1, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (71, 71, "Roc", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (71, 353, "Legendary Roc", 1, 1, 1, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (71, 354, "Gagana (Roc)", 1, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (72, 72, "Bat", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (72, 355, "Bats", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (72, 356, "Vermillion Bat", 1, 1, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (72, 357, "Vermillion Bats", 1, 1, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (73, 73, "Hippogryph", 1, 1, 1, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (74, 74, "Apkallu", 2, 2, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (74, 358, "Inguza (Apkallu)", 13, 2, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (75, 75, "Colibri", 5, 5, 0, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (75, 359, "Toucalibri (Colibri)", 10, 5, 0, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (76, 76, "Amphiptere", 1, 4, 2, 0x0000); -- TODO +INSERT INTO `monstrosity_species` VALUES (76, 360, "Sanguiptere (Amphiptere)", 4, 1, 2, 0x0000); -- TODO + +INSERT INTO `monstrosity_species` VALUES (126, 254, "Astoltian Slime", 1, 1, 0, 0x0B41); +INSERT INTO `monstrosity_species` VALUES (126, 508, "Astoltian She-Slime", 1, 1, 0, 0x0B5B); +INSERT INTO `monstrosity_species` VALUES (126, 509, "Astoltian Metal Slime", 4, 1, 0, 0x0B5C); + +INSERT INTO `monstrosity_species` VALUES (127, 255, "Eorzean Spriggan", 1, 4, 0, 0x0B42); +INSERT INTO `monstrosity_species` VALUES (127, 510, "Eorzean Spriggan.C", 1, 4, 0, 0x0B5D); +INSERT INTO `monstrosity_species` VALUES (127, 511, "Eorzean Spriggan.G", 4, 1, 0, 0x0B5E); diff --git a/sql/npc_list.sql b/sql/npc_list.sql index c3d68e95624..ba2b426be04 100644 --- a/sql/npc_list.sql +++ b/sql/npc_list.sql @@ -35758,7 +35758,7 @@ INSERT INTO `npc_list` VALUES (17940524,'Achieve_Master','Achieve Master',0,0.00 INSERT INTO `npc_list` VALUES (17940526,'csnpc','',0,0.000,0.000,0.000,0,50,50,0,0,0,2,2051,0x0000340000000000000000000000000000000000,0,'SOA',1); -- ------------------------------------------------------------ --- Ferretory (Zone 285) +-- Feretory (Zone 285) -- ------------------------------------------------------------ INSERT INTO `npc_list` VALUES (17944579,'Moogle','Moogle',0,0.000,0.000,0.000,0,40,40,0,0,0,2,3,0x0000520000000000000000000000000000000000,0,'SOA',0); diff --git a/sql/triggers.sql b/sql/triggers.sql index 6d166cce4eb..b0d61a485d9 100644 --- a/sql/triggers.sql +++ b/sql/triggers.sql @@ -64,6 +64,7 @@ BEGIN DELETE FROM `char_job_points` WHERE `charid` = OLD.charid; DELETE FROM `char_look` WHERE `charid` = OLD.charid; DELETE FROM `char_merit` WHERE `charid` = OLD.charid; + DELETE FROM `char_monstrosity` WHERE `charid` = OLD.charid; DELETE FROM `char_pet` WHERE `charid` = OLD.charid; DELETE FROM `char_points` WHERE `charid` = OLD.charid; DELETE FROM `char_profile` WHERE `charid` = OLD.charid; diff --git a/src/common/sql.h b/src/common/sql.h index a18ea533383..8100890b288 100644 --- a/src/common/sql.h +++ b/src/common/sql.h @@ -185,6 +185,27 @@ class SqlConnection std::string GetStringData(size_t col); + template + void GetBlobData(size_t col, T* destination) + { + size_t length = 0; + char* buffer = nullptr; + GetData(col, &buffer, &length); + std::memcpy(destination, buffer, (length > sizeof(T) ? sizeof(T) : length)); + } + + template + std::string ObjectToBlobString(T* destination) + { + char buffer[sizeof(T) * 2 + 1]; + { + char dataBlob[sizeof(T)]; + std::memcpy(dataBlob, destination, sizeof(dataBlob)); + EscapeStringLen(buffer, dataBlob, sizeof(dataBlob)); + } + return std::string(buffer); + } + /// Frees the result of the query. void FreeResult(); diff --git a/src/map/CMakeLists.txt b/src/map/CMakeLists.txt index 2d0d14b2f73..fcdbdd1102a 100644 --- a/src/map/CMakeLists.txt +++ b/src/map/CMakeLists.txt @@ -88,6 +88,8 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/mobskill.h ${CMAKE_CURRENT_SOURCE_DIR}/modifier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/modifier.h + ${CMAKE_CURRENT_SOURCE_DIR}/monstrosity.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/monstrosity.h ${CMAKE_CURRENT_SOURCE_DIR}/navmesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/navmesh.h ${CMAKE_CURRENT_SOURCE_DIR}/notoriety_container.cpp diff --git a/src/map/ai/ai_container.cpp b/src/map/ai/ai_container.cpp index e8caed61783..1a8457382bf 100644 --- a/src/map/ai/ai_container.cpp +++ b/src/map/ai/ai_container.cpp @@ -226,7 +226,7 @@ bool CAIContainer::Internal_Cast(uint16 targetid, SpellID spellid) auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) { return false; } @@ -269,7 +269,7 @@ bool CAIContainer::Internal_WeaponSkill(uint16 targid, uint16 wsid) auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) { return false; } @@ -280,10 +280,10 @@ bool CAIContainer::Internal_WeaponSkill(uint16 targid, uint16 wsid) bool CAIContainer::Internal_MobSkill(uint16 targid, uint16 wsid) { - auto* entity = dynamic_cast(PEntity); + auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) { return false; } @@ -297,7 +297,7 @@ bool CAIContainer::Internal_PetSkill(uint16 targid, uint16 abilityid) auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targid); target && target->PAI->IsUntargetable()) { return false; } @@ -311,7 +311,7 @@ bool CAIContainer::Internal_Ability(uint16 targetid, uint16 abilityid) auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) { return false; } @@ -325,7 +325,7 @@ bool CAIContainer::Internal_RangedAttack(uint16 targetid) auto* entity = dynamic_cast(PEntity); if (entity) { - if (auto target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) + if (auto* target = entity->GetEntity(targetid); target && target->PAI->IsUntargetable()) { return false; } diff --git a/src/map/ai/states/mobskill_state.cpp b/src/map/ai/states/mobskill_state.cpp index 4a2c85a223d..9fb0fbf3fee 100644 --- a/src/map/ai/states/mobskill_state.cpp +++ b/src/map/ai/states/mobskill_state.cpp @@ -22,13 +22,14 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "mobskill_state.h" #include "ai/ai_container.h" #include "enmity_container.h" +#include "entities/battleentity.h" #include "entities/mobentity.h" #include "mobskill.h" #include "packets/action.h" #include "status_effect_container.h" #include "utils/battleutils.h" -CMobSkillState::CMobSkillState(CMobEntity* PEntity, uint16 targid, uint16 wsid) +CMobSkillState::CMobSkillState(CBattleEntity* PEntity, uint16 targid, uint16 wsid) : CState(PEntity, targid) , m_PEntity(PEntity) , m_spentTP(0) diff --git a/src/map/ai/states/mobskill_state.h b/src/map/ai/states/mobskill_state.h index 0b640ab8203..25232eee384 100644 --- a/src/map/ai/states/mobskill_state.h +++ b/src/map/ai/states/mobskill_state.h @@ -25,12 +25,12 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "mobskill.h" #include "state.h" -class CMobEntity; +class CBattleEntity; class CMobSkillState : public CState { public: - CMobSkillState(CMobEntity* PEntity, uint16 targid, uint16 wsid); + CMobSkillState(CBattleEntity* PEntity, uint16 targid, uint16 wsid); CMobSkill* GetSkill(); @@ -57,7 +57,7 @@ class CMobSkillState : public CState void SpendCost(); private: - CMobEntity* const m_PEntity; + CBattleEntity* const m_PEntity; std::unique_ptr m_PSkill; time_point m_finishTime; duration m_castTime{}; diff --git a/src/map/entities/battleentity.cpp b/src/map/entities/battleentity.cpp index 3477233ffc0..adc2ee477a4 100644 --- a/src/map/entities/battleentity.cpp +++ b/src/map/entities/battleentity.cpp @@ -30,6 +30,7 @@ #include "ai/states/despawn_state.h" #include "ai/states/inactive_state.h" #include "ai/states/magic_state.h" +#include "ai/states/mobskill_state.h" #include "ai/states/raise_state.h" #include "ai/states/weaponskill_state.h" #include "attack.h" @@ -1644,6 +1645,254 @@ void CBattleEntity::OnWeaponSkillFinished(CWeaponSkillState& state, action_t& ac action.actionid = PWeaponskill->getID(); } +void CBattleEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) +{ + auto* PSkill = state.GetSkill(); + auto* PTarget = dynamic_cast(state.GetTarget()); + + if (PTarget == nullptr) + { + ShowWarning("CMobEntity::OnMobSkillFinished: PTarget is null"); + return; + } + + if (auto* PMob = dynamic_cast(this)) + { + // store the skill used + PMob->m_UsedSkillIds[PSkill->getID()] = GetMLevel(); + } + + PAI->TargetFind->reset(); + + float distance = PSkill->getDistance(); + uint8 findFlags = 0; + if (PSkill->getFlag() & SKILLFLAG_HIT_ALL) + { + findFlags |= FINDFLAGS_HIT_ALL; + } + + // Mob buff abilities also hit monster's pets + if (PSkill->getValidTargets() == TARGET_SELF) + { + findFlags |= FINDFLAGS_PET; + } + + if ((PSkill->getValidTargets() & TARGET_IGNORE_BATTLEID) == TARGET_IGNORE_BATTLEID) + { + findFlags |= FINDFLAGS_IGNORE_BATTLEID; + } + + action.id = id; + if (objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR) + { + action.actiontype = ACTION_PET_MOBABILITY_FINISH; + } + else if (PSkill->getID() < 256) + { + action.actiontype = ACTION_WEAPONSKILL_FINISH; + } + else + { + action.actiontype = ACTION_MOBABILITY_FINISH; + } + action.actionid = PSkill->getID(); + + if (PAI->TargetFind->isWithinRange(&PTarget->loc.p, distance)) + { + if (PSkill->isAoE()) + { + PAI->TargetFind->findWithinArea(PTarget, static_cast(PSkill->getAoe()), PSkill->getRadius(), findFlags); + } + else if (PSkill->isConal()) + { + float angle = 45.0f; + PAI->TargetFind->findWithinCone(PTarget, distance, angle, findFlags); + } + else + { + if (this->objtype == TYPE_MOB && PTarget->objtype == TYPE_PC) + { + CBattleEntity* PCoverAbilityUser = battleutils::GetCoverAbilityUser(PTarget, this); + if (PCoverAbilityUser != nullptr) + { + PTarget = PCoverAbilityUser; + } + } + + PAI->TargetFind->findSingleTarget(PTarget, findFlags); + } + } + else // Out of range + { + action.actiontype = ACTION_MOBABILITY_INTERRUPT; + action.actionid = 0; + actionList_t& actionList = action.getNewActionList(); + actionList.ActionTargetID = PTarget->id; + + actionTarget_t& actionTarget = actionList.getNewActionTarget(); + actionTarget.animation = 0x1FC; // Hardcoded magic sent from the server + actionTarget.messageID = MSGBASIC_TOO_FAR_AWAY; + actionTarget.speceffect = SPECEFFECT::BLOOD; + return; + } + + uint16 targets = static_cast(PAI->TargetFind->m_targets.size()); + + // No targets, perhaps something like Super Jump or otherwise untargetable + if (targets == 0) + { + action.actiontype = ACTION_MOBABILITY_INTERRUPT; + action.actionid = 28787; // Some hardcoded magic for interrupts + actionList_t& actionList = action.getNewActionList(); + actionList.ActionTargetID = id; + + actionTarget_t& actionTarget = actionList.getNewActionTarget(); + actionTarget.animation = 0x1FC; // Hardcoded magic sent from the server + actionTarget.messageID = 0; + actionTarget.reaction = REACTION::ABILITY | REACTION::HIT; + + return; + } + + PSkill->setTotalTargets(targets); + PSkill->setTP(state.GetSpentTP()); + PSkill->setHPP(GetHPP()); + + uint16 msg = 0; + uint16 defaultMessage = PSkill->getMsg(); + + bool first{ true }; + for (auto&& PTargetFound : PAI->TargetFind->m_targets) + { + actionList_t& list = action.getNewActionList(); + + list.ActionTargetID = PTargetFound->id; + + actionTarget_t& target = list.getNewActionTarget(); + + list.ActionTargetID = PTargetFound->id; + target.reaction = REACTION::HIT; + target.speceffect = SPECEFFECT::HIT; + target.animation = PSkill->getAnimationID(); + target.messageID = PSkill->getMsg(); + + // reset the skill's message back to default + PSkill->setMsg(defaultMessage); + int32 damage = 0; + if (objtype == TYPE_PET && static_cast(this)->getPetType() != PET_TYPE::JUG_PET) + { + PET_TYPE petType = static_cast(this)->getPetType(); + + if (static_cast(this)->getPetType() == PET_TYPE::AVATAR || static_cast(this)->getPetType() == PET_TYPE::WYVERN) + { + target.animation = PSkill->getPetAnimationID(); + } + + if (petType == PET_TYPE::AUTOMATON) + { + damage = luautils::OnAutomatonAbility(PTargetFound, this, PSkill, PMaster, &action); + } + else + { + damage = luautils::OnPetAbility(PTargetFound, this, PSkill, PMaster, &action); + } + } + else + { + damage = luautils::OnMobWeaponSkill(PTargetFound, this, PSkill, &action); + this->PAI->EventHandler.triggerListener("WEAPONSKILL_USE", CLuaBaseEntity(this), CLuaBaseEntity(PTargetFound), PSkill->getID(), state.GetSpentTP(), CLuaAction(&action), damage); + PTarget->PAI->EventHandler.triggerListener("WEAPONSKILL_TAKE", CLuaBaseEntity(PTargetFound), CLuaBaseEntity(this), PSkill->getID(), state.GetSpentTP(), CLuaAction(&action)); + } + + if (msg == 0) + { + msg = PSkill->getMsg(); + } + else + { + msg = PSkill->getAoEMsg(); + } + + if (damage < 0) + { + msg = MSGBASIC_SKILL_RECOVERS_HP; // TODO: verify this message does/does not vary depending on mob/avatar/automaton use + target.param = std::clamp(-damage, 0, PTargetFound->GetMaxHP() - PTargetFound->health.hp); + } + else + { + target.param = damage; + } + + target.messageID = msg; + + if (PSkill->hasMissMsg()) + { + target.reaction = REACTION::MISS; + target.speceffect = SPECEFFECT::NONE; + if (msg == PSkill->getAoEMsg()) + { + msg = 282; + } + } + else + { + target.reaction = REACTION::HIT; + target.speceffect = SPECEFFECT::HIT; + } + + // TODO: Should this be reaction and not speceffect? + if (target.speceffect == SPECEFFECT::HIT) // Formerly bitwise and, though nothing in this function adds additional bits to the field + { + target.speceffect = SPECEFFECT::RECOIL; + target.knockback = PSkill->getKnockback(); + if (first && (PSkill->getPrimarySkillchain() != 0)) + { + SUBEFFECT effect = battleutils::GetSkillChainEffect(PTargetFound, PSkill->getPrimarySkillchain(), PSkill->getSecondarySkillchain(), + PSkill->getTertiarySkillchain()); + if (effect != SUBEFFECT_NONE) + { + int32 skillChainDamage = battleutils::TakeSkillchainDamage(this, PTargetFound, target.param, nullptr); + if (skillChainDamage < 0) + { + target.addEffectParam = -skillChainDamage; + target.addEffectMessage = 384 + effect; + } + else + { + target.addEffectParam = skillChainDamage; + target.addEffectMessage = 287 + effect; + } + target.additionalEffect = effect; + } + + first = false; + } + } + + if (PSkill->getValidTargets() & TARGET_ENEMY) + { + PTargetFound->StatusEffectContainer->DelStatusEffectsByFlag(EFFECTFLAG_DETECTABLE); + } + + if (PTargetFound->isDead()) + { + battleutils::ClaimMob(PTargetFound, this); + } + battleutils::DirtyExp(PTargetFound, this); + } + + PTarget = dynamic_cast(state.GetTarget()); // TODO: why is this recast here? can state change between now and the original cast? + + if (PTarget) + { + if (PTarget->objtype == TYPE_MOB && (PTarget->isDead() || (objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR))) + { + battleutils::ClaimMob(PTarget, this); + } + battleutils::DirtyExp(PTarget, this); + } +} + bool CBattleEntity::CanAttack(CBattleEntity* PTarget, std::unique_ptr& errMsg) { TracyZoneScoped; diff --git a/src/map/entities/battleentity.h b/src/map/entities/battleentity.h index aaed5b16ef0..eb6166640b4 100644 --- a/src/map/entities/battleentity.h +++ b/src/map/entities/battleentity.h @@ -92,10 +92,11 @@ enum JOBTYPE JOB_DNC = 19, JOB_SCH = 20, JOB_GEO = 21, - JOB_RUN = 22 + JOB_RUN = 22, + JOB_MON = 23, // NOTE: MON is not a full job }; -#define MAX_JOBTYPE 23 +#define MAX_JOBTYPE 24 enum SKILLTYPE { @@ -520,6 +521,7 @@ class CSpell; class CItemEquipment; class CAbilityState; class CAttackState; +class CMobSkillState; class CWeaponSkillState; class CMagicState; class CDespawnState; @@ -700,6 +702,7 @@ class CBattleEntity : public CBaseEntity virtual void OnCastInterrupted(CMagicState&, action_t&, MSGBASIC_ID msg, bool blockedCast); /* Weaponskill */ virtual void OnWeaponSkillFinished(CWeaponSkillState& state, action_t& action); + virtual void OnMobSkillFinished(CMobSkillState& state, action_t& action); virtual void OnChangeTarget(CBattleEntity* PTarget); // Used to set an action to an "interrupted" state diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index 774f3ddb9d5..94304811414 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -173,8 +173,10 @@ CCharEntity::CCharEntity() m_mkeCurrent = 0; m_asaCurrent = 0; + m_PMonstrosity = nullptr; + m_Costume = 0; - m_Monstrosity = 0; + m_Costume2 = 0; m_hasTractor = 0; m_hasRaise = 0; m_weaknessLvl = 0; diff --git a/src/map/entities/charentity.h b/src/map/entities/charentity.h index 8650e0707dc..f9a43856189 100644 --- a/src/map/entities/charentity.h +++ b/src/map/entities/charentity.h @@ -23,6 +23,7 @@ along with this program. If not, see http://www.gnu.org/licenses/ #define _CHARENTITY_H #include "event_info.h" +#include "monstrosity.h" #include "packets/char.h" #include "packets/entity_update.h" @@ -447,10 +448,12 @@ class CCharEntity : public CBattleEntity EntityID_t BazaarID{}; // Pointer to the bazaar we are browsing. BazaarList_t BazaarCustomers; // Array holding the IDs of the current customers + std::unique_ptr m_PMonstrosity; + uint32 m_InsideTriggerAreaID; // The ID of the trigger area the character is inside uint8 m_LevelRestriction; // Character level limit uint16 m_Costume; - uint16 m_Monstrosity; // Monstrosity model ID + uint16 m_Costume2; uint32 m_AHHistoryTimestamp; uint32 m_DeathTimestamp; time_point m_deathSyncTime; // Timer used for sending an update packet at a regular interval while the character is dead diff --git a/src/map/entities/mobentity.cpp b/src/map/entities/mobentity.cpp index 949ecfda538..b08eec7cb3e 100644 --- a/src/map/entities/mobentity.cpp +++ b/src/map/entities/mobentity.cpp @@ -591,249 +591,10 @@ void CMobEntity::OnWeaponSkillFinished(CWeaponSkillState& state, action_t& actio void CMobEntity::OnMobSkillFinished(CMobSkillState& state, action_t& action) { TracyZoneScoped; - auto* PSkill = state.GetSkill(); - auto* PTarget = dynamic_cast(state.GetTarget()); - if (PTarget == nullptr) - { - ShowWarning("CMobEntity::OnMobSkillFinished: PTarget is null"); - return; - } + CBattleEntity::OnMobSkillFinished(state, action); TapDeaggroTime(); - - // store the skill used - m_UsedSkillIds[PSkill->getID()] = GetMLevel(); - - PAI->TargetFind->reset(); - - float distance = PSkill->getDistance(); - uint8 findFlags = 0; - if (PSkill->getFlag() & SKILLFLAG_HIT_ALL) - { - findFlags |= FINDFLAGS_HIT_ALL; - } - - // Mob buff abilities also hit monster's pets - if (PSkill->getValidTargets() == TARGET_SELF) - { - findFlags |= FINDFLAGS_PET; - } - - if ((PSkill->getValidTargets() & TARGET_IGNORE_BATTLEID) == TARGET_IGNORE_BATTLEID) - { - findFlags |= FINDFLAGS_IGNORE_BATTLEID; - } - - action.id = id; - if (objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR) - { - action.actiontype = ACTION_PET_MOBABILITY_FINISH; - } - else if (PSkill->getID() < 256) - { - action.actiontype = ACTION_WEAPONSKILL_FINISH; - } - else - { - action.actiontype = ACTION_MOBABILITY_FINISH; - } - action.actionid = PSkill->getID(); - - if (PAI->TargetFind->isWithinRange(&PTarget->loc.p, distance)) - { - if (PSkill->isAoE()) - { - PAI->TargetFind->findWithinArea(PTarget, static_cast(PSkill->getAoe()), PSkill->getRadius(), findFlags); - } - else if (PSkill->isConal()) - { - float angle = 45.0f; - PAI->TargetFind->findWithinCone(PTarget, distance, angle, findFlags); - } - else - { - if (this->objtype == TYPE_MOB && PTarget->objtype == TYPE_PC) - { - CBattleEntity* PCoverAbilityUser = battleutils::GetCoverAbilityUser(PTarget, this); - if (PCoverAbilityUser != nullptr) - { - PTarget = PCoverAbilityUser; - } - } - - PAI->TargetFind->findSingleTarget(PTarget, findFlags); - } - } - else // Out of range - { - action.actiontype = ACTION_MOBABILITY_INTERRUPT; - action.actionid = 0; - actionList_t& actionList = action.getNewActionList(); - actionList.ActionTargetID = PTarget->id; - - actionTarget_t& actionTarget = actionList.getNewActionTarget(); - actionTarget.animation = 0x1FC; // Hardcoded magic sent from the server - actionTarget.messageID = MSGBASIC_TOO_FAR_AWAY; - actionTarget.speceffect = SPECEFFECT::BLOOD; - return; - } - - uint16 targets = static_cast(PAI->TargetFind->m_targets.size()); - - // No targets, perhaps something like Super Jump or otherwise untargetable - if (targets == 0) - { - action.actiontype = ACTION_MOBABILITY_INTERRUPT; - action.actionid = 28787; // Some hardcoded magic for interrupts - actionList_t& actionList = action.getNewActionList(); - actionList.ActionTargetID = id; - - actionTarget_t& actionTarget = actionList.getNewActionTarget(); - actionTarget.animation = 0x1FC; // Hardcoded magic sent from the server - actionTarget.messageID = 0; - actionTarget.reaction = REACTION::ABILITY | REACTION::HIT; - - return; - } - - PSkill->setTotalTargets(targets); - PSkill->setTP(state.GetSpentTP()); - PSkill->setHPP(GetHPP()); - - uint16 msg = 0; - uint16 defaultMessage = PSkill->getMsg(); - - bool first{ true }; - for (auto&& PTargetFound : PAI->TargetFind->m_targets) - { - actionList_t& list = action.getNewActionList(); - - list.ActionTargetID = PTargetFound->id; - - actionTarget_t& target = list.getNewActionTarget(); - - list.ActionTargetID = PTargetFound->id; - target.reaction = REACTION::HIT; - target.speceffect = SPECEFFECT::HIT; - target.animation = PSkill->getAnimationID(); - target.messageID = PSkill->getMsg(); - - // reset the skill's message back to default - PSkill->setMsg(defaultMessage); - int32 damage = 0; - if (objtype == TYPE_PET && static_cast(this)->getPetType() != PET_TYPE::JUG_PET) - { - PET_TYPE petType = static_cast(this)->getPetType(); - - if (static_cast(this)->getPetType() == PET_TYPE::AVATAR || static_cast(this)->getPetType() == PET_TYPE::WYVERN) - { - target.animation = PSkill->getPetAnimationID(); - } - - if (petType == PET_TYPE::AUTOMATON) - { - damage = luautils::OnAutomatonAbility(PTargetFound, this, PSkill, PMaster, &action); - } - else - { - damage = luautils::OnPetAbility(PTargetFound, this, PSkill, PMaster, &action); - } - } - else - { - damage = luautils::OnMobWeaponSkill(PTargetFound, this, PSkill, &action); - this->PAI->EventHandler.triggerListener("WEAPONSKILL_USE", CLuaBaseEntity(this), CLuaBaseEntity(PTargetFound), PSkill->getID(), state.GetSpentTP(), CLuaAction(&action), damage); - PTarget->PAI->EventHandler.triggerListener("WEAPONSKILL_TAKE", CLuaBaseEntity(PTargetFound), CLuaBaseEntity(this), PSkill->getID(), state.GetSpentTP(), CLuaAction(&action)); - } - - if (msg == 0) - { - msg = PSkill->getMsg(); - } - else - { - msg = PSkill->getAoEMsg(); - } - - if (damage < 0) - { - msg = MSGBASIC_SKILL_RECOVERS_HP; // TODO: verify this message does/does not vary depending on mob/avatar/automaton use - target.param = std::clamp(-damage, 0, PTargetFound->GetMaxHP() - PTargetFound->health.hp); - } - else - { - target.param = damage; - } - - target.messageID = msg; - - if (PSkill->hasMissMsg()) - { - target.reaction = REACTION::MISS; - target.speceffect = SPECEFFECT::NONE; - if (msg == PSkill->getAoEMsg()) - { - msg = 282; - } - } - else - { - target.reaction = REACTION::HIT; - target.speceffect = SPECEFFECT::HIT; - } - - // TODO: Should this be reaction and not speceffect? - if (target.speceffect == SPECEFFECT::HIT) // Formerly bitwise and, though nothing in this function adds additional bits to the field - { - target.speceffect = SPECEFFECT::RECOIL; - target.knockback = PSkill->getKnockback(); - if (first && (PSkill->getPrimarySkillchain() != 0)) - { - SUBEFFECT effect = battleutils::GetSkillChainEffect(PTargetFound, PSkill->getPrimarySkillchain(), PSkill->getSecondarySkillchain(), - PSkill->getTertiarySkillchain()); - if (effect != SUBEFFECT_NONE) - { - int32 skillChainDamage = battleutils::TakeSkillchainDamage(this, PTargetFound, target.param, nullptr); - if (skillChainDamage < 0) - { - target.addEffectParam = -skillChainDamage; - target.addEffectMessage = 384 + effect; - } - else - { - target.addEffectParam = skillChainDamage; - target.addEffectMessage = 287 + effect; - } - target.additionalEffect = effect; - } - - first = false; - } - } - - if (PSkill->getValidTargets() & TARGET_ENEMY) - { - PTargetFound->StatusEffectContainer->DelStatusEffectsByFlag(EFFECTFLAG_DETECTABLE); - } - - if (PTargetFound->isDead()) - { - battleutils::ClaimMob(PTargetFound, this); - } - battleutils::DirtyExp(PTargetFound, this); - } - - PTarget = dynamic_cast(state.GetTarget()); // TODO: why is this recast here? can state change between now and the original cast? - - if (PTarget) - { - if (PTarget->objtype == TYPE_MOB && (PTarget->isDead() || (objtype == TYPE_PET && static_cast(this)->getPetType() == PET_TYPE::AVATAR))) - { - battleutils::ClaimMob(PTarget, this); - } - battleutils::DirtyExp(PTarget, this); - } } void CMobEntity::DistributeRewards() diff --git a/src/map/entities/mobentity.h b/src/map/entities/mobentity.h index 6bab4b17453..c01edc04e2f 100644 --- a/src/map/entities/mobentity.h +++ b/src/map/entities/mobentity.h @@ -158,7 +158,7 @@ class CMobEntity : public CBattleEntity virtual void Die() override; virtual void OnWeaponSkillFinished(CWeaponSkillState&, action_t&) override; - virtual void OnMobSkillFinished(CMobSkillState&, action_t&); + virtual void OnMobSkillFinished(CMobSkillState&, action_t&) override; virtual void OnEngage(CAttackState&) override; virtual bool OnAttack(CAttackState&, action_t&) override; diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index f038895f80d..1f1fe1a4c67 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -5149,53 +5149,52 @@ void CLuaBaseEntity::setModelId(uint16 modelId, sol::object const& slotObj) } /************************************************************************ - * Function: setCostume() - * Purpose : Updates the PC's appearance - * Example : player:setCostume( costumeId ) + * Function: getCostume() + * Purpose : Returns the PC's appearance + * Example : player:getCostume() ************************************************************************/ -void CLuaBaseEntity::setCostume(uint16 costume) +uint16 CLuaBaseEntity::getCostume() { if (m_PBaseEntity->objtype != TYPE_PC) { ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->GetName()); - return; + return 0; } auto* PChar = static_cast(m_PBaseEntity); - if (PChar->m_Costume != costume && PChar->status != STATUS_TYPE::SHUTDOWN && PChar->status != STATUS_TYPE::DISAPPEAR) - { - PChar->m_Costume = costume; - PChar->updatemask |= UPDATE_LOOK; - PChar->pushPacket(new CCharUpdatePacket(PChar)); - } + return PChar->m_Costume; } /************************************************************************ - * Function: getCostume() - * Purpose : Returns the PC's appearance - * Example : player:getCostume() + * Function: setCostume() + * Purpose : Updates the PC's appearance + * Example : player:setCostume( costumeId ) ************************************************************************/ -uint16 CLuaBaseEntity::getCostume() +void CLuaBaseEntity::setCostume(uint16 costume) { if (m_PBaseEntity->objtype != TYPE_PC) { ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->GetName()); - return 0; + return; } auto* PChar = static_cast(m_PBaseEntity); - return PChar->m_Costume; + if (PChar->m_Costume != costume && PChar->status != STATUS_TYPE::SHUTDOWN && PChar->status != STATUS_TYPE::DISAPPEAR) + { + PChar->m_Costume = costume; + PChar->updatemask |= UPDATE_LOOK; + PChar->pushPacket(new CCharUpdatePacket(PChar)); + } } /************************************************************************ * Function: getCostume2() - * Purpose : Sets or returns a monstrosity costume - * Example : player:costume2( costumeId ) - * Notes : Not currently implemented + * Purpose : Returns the PC's appearance + * Example : player:getCostume() ************************************************************************/ uint16 CLuaBaseEntity::getCostume2() @@ -5207,14 +5206,14 @@ uint16 CLuaBaseEntity::getCostume2() } auto* PChar = static_cast(m_PBaseEntity); - return PChar->m_Monstrosity; + + return PChar->m_Costume2; } /************************************************************************ * Function: setCostume2() - * Purpose : Sets or returns a monstrosity costume - * Example : player:costume2( costumeId ) - * Notes : Not currently implemented + * Purpose : Updates the PC's appearance + * Example : player:setCostume2( costumeId ) ************************************************************************/ void CLuaBaseEntity::setCostume2(uint16 costume) @@ -5227,14 +5226,13 @@ void CLuaBaseEntity::setCostume2(uint16 costume) auto* PChar = static_cast(m_PBaseEntity); - if (PChar->m_Monstrosity != costume && PChar->status != STATUS_TYPE::SHUTDOWN && PChar->status != STATUS_TYPE::DISAPPEAR) + if (PChar->m_Costume2 != costume && PChar->status != STATUS_TYPE::SHUTDOWN && PChar->status != STATUS_TYPE::DISAPPEAR) { - PChar->m_Monstrosity = costume; + PChar->m_Costume2 = costume; PChar->updatemask |= UPDATE_LOOK; PChar->pushPacket(new CCharAppearancePacket(PChar)); } } - /************************************************************************ * Function: getAnimation() * Purpose : Returns the assigned default animation of an entity @@ -6350,6 +6348,136 @@ void CLuaBaseEntity::addJobTraits(uint8 jobID, uint8 level) } } +sol::table CLuaBaseEntity::getMonstrosityData() +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return sol::lua_nil; + } + + bool startsWithMonstrosityData = PChar->m_PMonstrosity != nullptr; + if (!startsWithMonstrosityData) + { + monstrosity::ReadMonstrosityData(PChar); + } + + auto table = luautils::GetMonstrosityLuaTable(PChar); + + // If we didn't start with Monstrosity data, we should wipe it out now so we + // don't change modes + if (!startsWithMonstrosityData) + { + PChar->m_PMonstrosity = nullptr; + } + + return table; +} + +void CLuaBaseEntity::setMonstrosityData(sol::table table) +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return; + } + + bool startsWithMonstrosityData = PChar->m_PMonstrosity != nullptr; + + // NOTE: This will populate m_PMonstrosity if it doesn't exist + monstrosity::ReadMonstrosityData(PChar); + + luautils::SetMonstrosityLuaTable(PChar, table); + + monstrosity::WriteMonstrosityData(PChar); + + // If we didn't start with Monstrosity data, we should wipe it out now so we + // don't change modes + if (!startsWithMonstrosityData) + { + PChar->m_PMonstrosity = nullptr; + } + else + { + monstrosity::SendFullMonstrosityUpdate(PChar); + } +} + +bool CLuaBaseEntity::getBelligerencyFlag() +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return false; + } + + if (PChar->m_PMonstrosity == nullptr) + { + return false; + } + + return PChar->m_PMonstrosity->Belligerency; +} + +void CLuaBaseEntity::setBelligerencyFlag(bool flag) +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return; + } + + monstrosity::SetBelligerencyFlag(PChar, flag); +} + +auto CLuaBaseEntity::getMonstrositySize() -> uint8 +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return 0; + } + + if (PChar->m_PMonstrosity == nullptr) + { + return 0; + } + + return PChar->m_PMonstrosity->Size; +} + +void CLuaBaseEntity::setMonstrosityEntryData(float x, float y, float z, uint8 rot, uint16 zoneId, uint8 mjob, uint8 sjob) +{ + auto* PChar = dynamic_cast(m_PBaseEntity); + if (PChar == nullptr) + { + return; + } + + bool startsWithMonstrosityData = PChar->m_PMonstrosity != nullptr; + if (!startsWithMonstrosityData) + { + monstrosity::ReadMonstrosityData(PChar); + } + + PChar->m_PMonstrosity->EntryPos.x = x; + PChar->m_PMonstrosity->EntryPos.y = y; + PChar->m_PMonstrosity->EntryPos.z = z; + PChar->m_PMonstrosity->EntryPos.rotation = rot; + PChar->m_PMonstrosity->EntryZoneId = zoneId; + PChar->m_PMonstrosity->EntryMainJob = mjob; + PChar->m_PMonstrosity->EntrySubJob = sjob; + + monstrosity::WriteMonstrosityData(PChar); + + // If we didn't start with Monstrosity data, we should wipe it out now so we + // don't change modes + if (!startsWithMonstrosityData) + { + PChar->m_PMonstrosity = nullptr; + } +} + /************************************************************************ * Function: getTitle() * Purpose : Returns the integer value of the player's current title @@ -17094,11 +17222,10 @@ void CLuaBaseEntity::Register() SOL_REGISTER("checkNameFlags", CLuaBaseEntity::checkNameFlags); SOL_REGISTER("getModelId", CLuaBaseEntity::getModelId); SOL_REGISTER("setModelId", CLuaBaseEntity::setModelId); - SOL_REGISTER("setCostume", CLuaBaseEntity::setCostume); SOL_REGISTER("getCostume", CLuaBaseEntity::getCostume); + SOL_REGISTER("setCostume", CLuaBaseEntity::setCostume); SOL_REGISTER("getCostume2", CLuaBaseEntity::getCostume2); SOL_REGISTER("setCostume2", CLuaBaseEntity::setCostume2); - SOL_REGISTER("getAnimation", CLuaBaseEntity::getAnimation); SOL_REGISTER("setAnimation", CLuaBaseEntity::setAnimation); SOL_REGISTER("getAnimationSub", CLuaBaseEntity::getAnimationSub); @@ -17156,6 +17283,14 @@ void CLuaBaseEntity::Register() SOL_REGISTER("levelRestriction", CLuaBaseEntity::levelRestriction); SOL_REGISTER("addJobTraits", CLuaBaseEntity::addJobTraits); + // Monstrosity + SOL_REGISTER("getMonstrosityData", CLuaBaseEntity::getMonstrosityData); + SOL_REGISTER("setMonstrosityData", CLuaBaseEntity::setMonstrosityData); + SOL_REGISTER("getBelligerencyFlag", CLuaBaseEntity::getBelligerencyFlag); + SOL_REGISTER("setBelligerencyFlag", CLuaBaseEntity::setBelligerencyFlag); + SOL_REGISTER("getMonstrositySize", CLuaBaseEntity::getMonstrositySize); + SOL_REGISTER("setMonstrosityEntryData", CLuaBaseEntity::setMonstrosityEntryData); + // Player Titles and Fame SOL_REGISTER("getTitle", CLuaBaseEntity::getTitle); SOL_REGISTER("hasTitle", CLuaBaseEntity::hasTitle); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index bba583a83d8..fd39de465e5 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -269,9 +269,9 @@ class CLuaBaseEntity bool checkNameFlags(uint32 flags); // this is check and not get because it tests for a flag, it doesn't return all flags uint16 getModelId(); void setModelId(uint16 modelId, sol::object const& slotObj); - void setCostume(uint16 costume); uint16 getCostume(); - uint16 getCostume2(); // monstrosity costume + void setCostume(uint16 costume); + uint16 getCostume2(); void setCostume2(uint16 costume); uint8 getAnimation(); void setAnimation(uint8 animation); @@ -331,6 +331,14 @@ class CLuaBaseEntity uint8 levelRestriction(sol::object const& level); // Establish/return current level restriction void addJobTraits(uint8 jobID, uint8 level); + // Monstrosity + auto getMonstrosityData() -> sol::table; + void setMonstrosityData(sol::table table); + bool getBelligerencyFlag(); + void setBelligerencyFlag(bool flag); + auto getMonstrositySize() -> uint8; + void setMonstrosityEntryData(float x, float y, float z, uint8 rot, uint16 zoneId, uint8 mjob, uint8 sjob); + // Player Titles and Fame uint16 getTitle(); bool hasTitle(uint16 titleID); diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index 3ae49df0c07..de5f74336bc 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -75,6 +75,7 @@ #include "map.h" #include "message.h" #include "mobskill.h" +#include "monstrosity.h" #include "packets/action.h" #include "packets/char_emotion.h" #include "packets/char_update.h" @@ -3888,6 +3889,179 @@ namespace luautils return result.get_type(0) == sol::type::number ? result.get(0) : 0; } + auto GetMonstrosityLuaTable(CCharEntity* PChar) -> sol::table + { + TracyZoneScoped; + + // TODO: lua_monstrosity.cpp? + auto table = lua.create_table(); + + table["monstrosityId"] = PChar->m_PMonstrosity->MonstrosityId; + table["species"] = PChar->m_PMonstrosity->Species; + table["flags"] = PChar->m_PMonstrosity->Flags; + table["entry_x"] = PChar->m_PMonstrosity->EntryPos.x; + table["entry_y"] = PChar->m_PMonstrosity->EntryPos.y; + table["entry_z"] = PChar->m_PMonstrosity->EntryPos.z; + table["entry_rot"] = PChar->m_PMonstrosity->EntryPos.rotation; + table["entry_zone_id"] = PChar->m_PMonstrosity->EntryZoneId; + table["entry_mjob"] = PChar->m_PMonstrosity->EntryMainJob; + table["entry_sjob"] = PChar->m_PMonstrosity->EntrySubJob; + + { + std::size_t idx = 0; + table["levels"] = lua.create_table(); + for (auto entry : PChar->m_PMonstrosity->levels) + { + table["levels"][idx++] = entry; + } + } + + { + std::size_t idx = 0; + table["instincts"] = lua.create_table(); + for (auto entry : PChar->m_PMonstrosity->instincts) + { + table["instincts"][idx++] = entry; + } + } + + { + std::size_t idx = 0; + table["variants"] = lua.create_table(); + for (auto entry : PChar->m_PMonstrosity->variants) + { + table["variants"][idx++] = entry; + } + } + + return table; + } + + void SetMonstrosityLuaTable(CCharEntity* PChar, sol::table table) + { + TracyZoneScoped; + + if (table["monstrosityId"].valid()) + { + PChar->m_PMonstrosity->MonstrosityId = table.get("monstrosityId"); + } + + if (table["species"].valid()) + { + PChar->m_PMonstrosity->Species = table.get("species"); + } + + if (table["flags"].valid()) + { + PChar->m_PMonstrosity->Flags = table.get("flags"); + } + + if (table["entry_x"].valid()) + { + PChar->m_PMonstrosity->EntryPos.x = table.get("entry_x"); + } + + if (table["entry_y"].valid()) + { + PChar->m_PMonstrosity->EntryPos.y = table.get("entry_y"); + } + + if (table["entry_z"].valid()) + { + PChar->m_PMonstrosity->EntryPos.z = table.get("entry_z"); + } + + if (table["entry_rot"].valid()) + { + PChar->m_PMonstrosity->EntryPos.rotation = table.get("entry_rot"); + } + + if (table["entry_zone_id"].valid()) + { + PChar->m_PMonstrosity->EntryZoneId = table.get("entry_zone_id"); + } + + if (table["entry_mjob"].valid()) + { + PChar->m_PMonstrosity->EntryMainJob = table.get("entry_mjob"); + } + + if (table["entry_sjob"].valid()) + { + PChar->m_PMonstrosity->EntrySubJob = table.get("entry_sjob"); + } + + if (table["levels"].valid()) + { + for (auto const& [keyObj, valObj] : table.get("levels")) + { + uint8 key = keyObj.as(); + uint8 val = valObj.as(); + PChar->m_PMonstrosity->levels[key] |= val; + } + } + + if (table["instincts"].valid()) + { + for (auto const& [keyObj, valObj] : table.get("instincts")) + { + uint8 key = keyObj.as(); + uint8 val = valObj.as(); + PChar->m_PMonstrosity->instincts[key] |= val; + } + } + + if (table["variants"].valid()) + { + for (auto const& [keyObj, valObj] : table.get("variants")) + { + uint8 key = keyObj.as(); + uint8 val = valObj.as(); + PChar->m_PMonstrosity->variants[key] |= val; + } + } + } + + void OnMonstrosityUpdate(CCharEntity* PChar) + { + TracyZoneScoped; + + sol::function onMonstrosityUpdate = lua["xi"]["monstrosity"]["onMonstrosityUpdate"]; + if (!onMonstrosityUpdate.valid()) + { + ShowError("luautils::OnMonstrosityUpdate"); + return; + } + + auto result = onMonstrosityUpdate(CLuaBaseEntity(PChar), GetMonstrosityLuaTable(PChar)); + if (!result.valid()) + { + sol::error err = result; + ShowError("luautils::OnMonstrosityUpdate: %s", err.what()); + return; + } + } + + void OnMonstrosityReturnToEntrance(CCharEntity* PChar) + { + TracyZoneScoped; + + sol::function onMonstrosityReturnToEntrance = lua["xi"]["monstrosity"]["onMonstrosityReturnToEntrance"]; + if (!onMonstrosityReturnToEntrance.valid()) + { + ShowError("luautils::retuonMonstrosityReturnToEntrancernToEntrance"); + return; + } + + auto result = onMonstrosityReturnToEntrance(CLuaBaseEntity(PChar)); + if (!result.valid()) + { + sol::error err = result; + ShowError("luautils::onMonstrosityReturnToEntrance: %s", err.what()); + return; + } + } + int32 OnMagicCastingCheck(CBaseEntity* PChar, CBaseEntity* PTarget, CSpell* PSpell) { TracyZoneScoped; diff --git a/src/map/lua/luautils.h b/src/map/lua/luautils.h index 3f2fdaf70c7..b9dcdc9a90b 100644 --- a/src/map/lua/luautils.h +++ b/src/map/lua/luautils.h @@ -285,6 +285,11 @@ namespace luautils int32 OnAutomatonAbilityCheck(CBaseEntity* PChar, CAutomatonEntity* PAutomaton, CMobSkill* PMobSkill); int32 OnAutomatonAbility(CBaseEntity* PTarget, CBaseEntity* PMob, CMobSkill* PMobSkill, CBaseEntity* PMobMaster, action_t* action); + auto GetMonstrosityLuaTable(CCharEntity* PChar) -> sol::table; + void SetMonstrosityLuaTable(CCharEntity* PChar, sol::table data); + void OnMonstrosityUpdate(CCharEntity* PChar); + void OnMonstrosityReturnToEntrance(CCharEntity* PChar); + int32 OnAbilityCheck(CBaseEntity* PChar, CBaseEntity* PTarget, CAbility* PAbility, CBaseEntity** PMsgTarget); int32 OnPetAbility(CBaseEntity* PTarget, CBaseEntity* PMob, CMobSkill* PMobSkill, CBaseEntity* PPetMaster, action_t* action); int32 OnPetAbility(CBaseEntity* PTarget, CPetEntity* PPet, CPetSkill* PMobSkill, CBaseEntity* PPetMaster, action_t* action); // triggers when pet uses an ability, specialized for pets diff --git a/src/map/map.cpp b/src/map/map.cpp index b3c8734d421..e44b7405668 100755 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -43,6 +43,7 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "linkshell.h" #include "message.h" #include "mob_spell_list.h" +#include "monstrosity.h" #include "packet_guard.h" #include "packet_system.h" #include "roe.h" @@ -274,6 +275,8 @@ int32 do_init(int32 argc, char** argv) fishingutils::InitializeFishingSystem(); instanceutils::LoadInstanceList(); + monstrosity::LoadStaticData(); + ShowInfo("do_init: server is binding with port %u", map_port == 0 ? settings::get("network.MAP_PORT") : map_port); map_fd = makeBind_udp(INADDR_ANY, map_port == 0 ? settings::get("network.MAP_PORT") : map_port); diff --git a/src/map/monstrosity.cpp b/src/map/monstrosity.cpp new file mode 100644 index 00000000000..26a469f8fb5 --- /dev/null +++ b/src/map/monstrosity.cpp @@ -0,0 +1,778 @@ +/* +=========================================================================== + + Copyright (c) 2023 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +// === +// See scripts/globals/monstrosity.lua for a general overview of how Monstrosity works and is designed. +// === + +#include "monstrosity.h" + +#include "ai/ai_container.h" + +#include "common/logging.h" +#include "common/sql.h" + +#include "entities/charentity.h" + +#include "lua/luautils.h" + +#include "packets/char_abilities.h" +#include "packets/char_appearance.h" +#include "packets/char_job_extra.h" +#include "packets/char_jobs.h" +#include "packets/char_stats.h" +#include "packets/monipulator1.h" +#include "packets/monipulator2.h" + +#include "utils/charutils.h" +#include "utils/zoneutils.h" + +#include "status_effect.h" +#include "status_effect_container.h" + +extern std::unique_ptr sql; + +struct MonstrositySpeciesRow +{ + uint8 monstrosityId; + uint16 monstrositySpeciesCode; + std::string name; + JOBTYPE mjob; + JOBTYPE sjob; + uint8 size; + uint16 look; +}; + +struct MonstrosityInstinctRow +{ + uint16 monstrosityInstinctId; + uint8 cost; + std::string name; + std::vector mods; +}; + +namespace +{ + std::unordered_map gMonstrositySpeciesMap; + std::unordered_map gMonstrosityInstinctMap; +} // namespace + +monstrosity::MonstrosityData_t::MonstrosityData_t() +: MonstrosityId(0x01) // Rabbit +, Species(0x0001) // Rabbit +, Flags(0x0B44) // ? +, Look(0x010C) // Rabbit +, Size(0x00) // Size (0: Small, 1: Medium, 2: Large) +, NamePrefix1(0x00) // Nothing +, NamePrefix2(0x00) // Nothing +, MainJob(JOB_WAR) // +, SubJob(JOB_WAR) // +, CurrentExp(0) // No exp +, Belligerency(false) // +, EntryZoneId(0) // +, EntryMainJob(0) // +, EntrySubJob(0) // +{ + levels[1] = 1; // Rabbit + levels[18] = 1; // Mandragora + levels[43] = 1; // Lizard + + instincts[20] = 0x1F; // Default player race instincts +} + +void monstrosity::LoadStaticData() +{ + ShowInfo("Loading Monstrosity data"); + + int32 ret = sql->Query("SELECT monstrosity_id, monstrosity_species_code, name, mjob, sjob, size, look FROM monstrosity_species;"); + if (ret != SQL_ERROR && sql->NumRows() != 0) + { + while (sql->NextRow() == SQL_SUCCESS) + { + MonstrositySpeciesRow row; + + row.monstrosityId = static_cast(sql->GetUIntData(0)); + row.monstrositySpeciesCode = static_cast(sql->GetUIntData(1)); + row.name = sql->GetStringData(2); + row.mjob = static_cast(sql->GetUIntData(3)); + row.sjob = static_cast(sql->GetUIntData(4)); + row.size = static_cast(sql->GetUIntData(5)); + row.look = static_cast(sql->GetUIntData(6)); + + gMonstrositySpeciesMap[row.monstrositySpeciesCode] = row; + } + } + + ret = sql->Query("SELECT monstrosity_instinct_id, cost, name FROM monstrosity_instincts;"); + if (ret != SQL_ERROR && sql->NumRows() != 0) + { + while (sql->NextRow() == SQL_SUCCESS) + { + MonstrosityInstinctRow row; + + row.monstrosityInstinctId = static_cast(sql->GetUIntData(0)); + row.cost = static_cast(sql->GetUIntData(1)); + row.name = sql->GetStringData(2); + + gMonstrosityInstinctMap[row.monstrosityInstinctId] = row; + } + } + + for (auto& [_, entry] : gMonstrosityInstinctMap) + { + ret = sql->Query("SELECT monstrosity_instinct_id, modId, value FROM monstrosity_instinct_mods WHERE monstrosity_instinct_id = %d;", entry.monstrosityInstinctId); + if (ret != SQL_ERROR && sql->NumRows() != 0) + { + while (sql->NextRow() == SQL_SUCCESS) + { + std::ignore = static_cast(sql->GetUIntData(0)); // id + auto mod = static_cast(sql->GetUIntData(1)); + auto val = static_cast(sql->GetIntData(2)); + entry.mods.emplace_back(CModifier(mod, val)); + } + } + } +} + +void monstrosity::ReadMonstrosityData(CCharEntity* PChar) +{ + auto data = std::make_unique(); + + // clang-format off + auto ret = sql->Query("SELECT " + "charid, " + "current_monstrosity_id, " + "current_monstrosity_species, " + "current_monstrosity_name_prefix_1, " + "current_monstrosity_name_prefix_2, " + "current_exp, " + "equip, " + "levels, " + "instincts, " + "variants, " + "belligerency, " + "entry_x, " + "entry_y, " + "entry_z, " + "entry_rot, " + "entry_zone_id, " + "entry_mjob, " + "entry_sjob " + "FROM char_monstrosity WHERE charid = %d LIMIT 1;", + PChar->id); + // clang-format on + + if (ret != SQL_ERROR && sql->NumRows() != 0) + { + while (sql->NextRow() == SQL_SUCCESS) + { + // charid: 0 + data->MonstrosityId = static_cast(sql->GetUIntData(1)); + data->Species = static_cast(sql->GetUIntData(2)); + data->Look = gMonstrositySpeciesMap[data->Species].look; + + data->NamePrefix1 = static_cast(sql->GetUIntData(3)); + data->NamePrefix2 = static_cast(sql->GetUIntData(4)); + data->CurrentExp = static_cast(sql->GetUIntData(5)); + + sql->GetBlobData(6, &data->EquippedInstincts); + sql->GetBlobData(7, &data->levels); + sql->GetBlobData(8, &data->instincts); + sql->GetBlobData(9, &data->variants); + + data->Belligerency = static_cast(sql->GetUIntData(10)); + + data->EntryPos.x = sql->GetFloatData(11); + data->EntryPos.y = sql->GetFloatData(12); + data->EntryPos.z = sql->GetFloatData(13); + data->EntryPos.rotation = static_cast(sql->GetUIntData(14)); + data->EntryZoneId = static_cast(sql->GetUIntData(15)); + data->EntryMainJob = static_cast(sql->GetUIntData(16)); + data->EntrySubJob = static_cast(sql->GetUIntData(17)); + + // Build additional data from lookups + data->MainJob = gMonstrositySpeciesMap[data->Species].mjob; + data->SubJob = gMonstrositySpeciesMap[data->Species].sjob; + data->Size = gMonstrositySpeciesMap[data->Species].size; + + // TODO: + auto level = data->levels[data->MonstrosityId]; + std::ignore = level; + } + } + + PChar->m_PMonstrosity = std::move(data); +} + +void monstrosity::WriteMonstrosityData(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + const char* Query = "REPLACE INTO char_monstrosity SET " + "charid = '%u', " + "current_monstrosity_id = '%d', " + "current_monstrosity_species = '%d', " + "current_monstrosity_name_prefix_1 = '%d', " + "current_monstrosity_name_prefix_2 = '%d', " + "current_exp = '%d', " + "equip = '%s', " + "levels = '%s', " + "instincts = '%s', " + "variants = '%s', " + "belligerency = '%d', " + "entry_x = '%.3f', " + "entry_y = '%.3f', " + "entry_z = '%.3f', " + "entry_rot = '%u', " + "entry_zone_id = '%d', " + "entry_mjob = '%d', " + "entry_sjob = '%d';"; + + auto equipEscaped = sql->ObjectToBlobString(&PChar->m_PMonstrosity->EquippedInstincts); + auto levelsEscaped = sql->ObjectToBlobString(&PChar->m_PMonstrosity->levels); + auto instinctsEscaped = sql->ObjectToBlobString(&PChar->m_PMonstrosity->instincts); + auto variantsEscaped = sql->ObjectToBlobString(&PChar->m_PMonstrosity->variants); + + sql->Query(Query, + PChar->id, + PChar->m_PMonstrosity->MonstrosityId, + PChar->m_PMonstrosity->Species, + PChar->m_PMonstrosity->NamePrefix1, + PChar->m_PMonstrosity->NamePrefix2, + PChar->m_PMonstrosity->CurrentExp, + equipEscaped.c_str(), + levelsEscaped.c_str(), + instinctsEscaped.c_str(), + variantsEscaped.c_str(), + static_cast(PChar->m_PMonstrosity->Belligerency), + PChar->m_PMonstrosity->EntryPos.x, + PChar->m_PMonstrosity->EntryPos.y, + PChar->m_PMonstrosity->EntryPos.z, + PChar->m_PMonstrosity->EntryPos.rotation, + PChar->m_PMonstrosity->EntryZoneId, + PChar->m_PMonstrosity->EntryMainJob, + PChar->m_PMonstrosity->EntrySubJob); +} + +void monstrosity::TryPopulateMonstrosityData(CCharEntity* PChar) +{ + if (PChar->GetMJob() == JOB_MON) + { + // Populates PChar->m_PMonstrosity + ReadMonstrosityData(PChar); + + // This handles !monstrosity GM command, is this needed? + WriteMonstrosityData(PChar); + } +} + +void monstrosity::HandleZoneIn(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + // Add stats from equipped instincts + for (auto instinctId : PChar->m_PMonstrosity->EquippedInstincts) + { + auto maybeInstinct = gMonstrosityInstinctMap.find(instinctId); + if (maybeInstinct != gMonstrosityInstinctMap.end()) + { + auto instinct = (*maybeInstinct).second; + for (auto const& mod : instinct.mods) + { + PChar->addModifier(mod.getModID(), mod.getModAmount()); + } + } + } + + // NOTE: Whenever you log in as a MON, you'll have Gestation - even if you've previously clicked it off. + // TODO: Check this is true in Belligerency. + // TODO: There are more conditions to handle here? + if (PChar->loc.zone->GetID() != ZONE_FERETORY) + { + uint32 duration = PChar->m_PMonstrosity->Belligerency ? 60 : 64800 /* 18 hours */; + + CStatusEffect* PEffect = new CStatusEffect(EFFECT::EFFECT_GESTATION, EFFECT::EFFECT_GESTATION, 0, 0, duration); + + // TODO: Move these into the db + PEffect->AddEffectFlag(EFFECTFLAG_INVISIBLE); + PEffect->AddEffectFlag(EFFECTFLAG_DEATH); + PEffect->AddEffectFlag(EFFECTFLAG_ATTACK); + PEffect->AddEffectFlag(EFFECTFLAG_MAGIC_BEGIN); + PEffect->AddEffectFlag(EFFECTFLAG_DETECTABLE); + PEffect->AddEffectFlag(EFFECTFLAG_ON_ZONE); + + // PEffect->AddEffectFlag(EFFECTFLAG_LOGOUT); + + // NOTE: It DOES say the effect wears off + // PEffect->AddEffectFlag(EFFECTFLAG_NO_LOSS_MESSAGE); + + PChar->StatusEffectContainer->AddStatusEffect(PEffect, true); + } + + SendFullMonstrosityUpdate(PChar); + + PChar->updatemask |= UPDATE_LOOK; +} + +uint32 monstrosity::GetPackedMonstrosityName(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return 0x00000000; + } + + // NOTE: Changing this 0x8000 to 0xC000 will hide the species name. + // : This looks to be a quirk of the client and not intended. + uint16 a = 0x8000 | PChar->m_PMonstrosity->Species; + uint8 b = PChar->m_PMonstrosity->NamePrefix1; + uint8 c = PChar->m_PMonstrosity->NamePrefix2; + + // Packed as LE + return (c << 24) + (b << 16) + (a << 0); +} + +void monstrosity::SendFullMonstrosityUpdate(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + // Make sure look is up to date before we send packets + PChar->m_PMonstrosity->Look = gMonstrositySpeciesMap[PChar->m_PMonstrosity->Species].look; + + // TODO: Safety checks: + // : The species box on the UI should never be empty - everything breaks if that happens. + // : We should detect a bad state and fall back to being a Lv1 Bunny if that happens. + + // TODO: Only send model change packets when the model actually changes - otherwise it disappears! + + charutils::BuildingCharTraitsTable(PChar); + + luautils::OnMonstrosityUpdate(PChar); + + PChar->pushPacket(new CMonipulatorPacket1(PChar)); + PChar->pushPacket(new CMonipulatorPacket2(PChar)); + PChar->pushPacket(new CCharJobsPacket(PChar)); + PChar->pushPacket(new CCharJobExtraPacket(PChar, true)); + PChar->pushPacket(new CCharJobExtraPacket(PChar, false)); + PChar->pushPacket(new CCharAppearancePacket(PChar)); + PChar->pushPacket(new CCharStatsPacket(PChar)); + PChar->pushPacket(new CCharAbilitiesPacket(PChar)); + + PChar->updatemask |= UPDATE_LOOK; +} + +void monstrosity::HandleMonsterSkillActionPacket(CCharEntity* PChar, CBasicPacket& data) +{ + if (PChar->GetMJob() != JOB_MON) + { + return; + } + + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + // uint16 other = data.ref(0x0A); // Always 25? + + uint16 targId = data.ref(0x08); + uint16 skillId = data.ref(0x0C); + + // TODO: Validate that this move is available at this level, for this species, and that + // we're capable of using it (state, TP, etc.). + + PChar->PAI->Internal_MobSkill(targId, skillId); +} + +void monstrosity::HandleEquipChangePacket(CCharEntity* PChar, CBasicPacket& data) +{ + if (PChar->loc.zone->GetID() != ZONE_FERETORY || PChar->m_PMonstrosity == nullptr) + { + return; + } + + // NOTE: The amount of pointer per level is level + 10, this is set in the client + + // clang-format off + auto getTotalInstinctsCost = [&](std::array input) -> uint8 + { + uint8 total = 0; + + for (auto const& idx : input) + { + total += gMonstrosityInstinctMap[idx].cost; + } + + return total; + }; + + auto instinctsContainDuplicates = [&](std::array input) -> bool + { + std::unordered_set set; + for (auto const& idx : input) + { + if (set.find(idx) != set.end()) + { + // Found dupe + return true; + } + } + return false; + }; + // clang-format on + + uint8 flag = data.ref(0x0A); + if (flag == 0x01) // Species Change + { + auto previousId = PChar->m_PMonstrosity->MonstrosityId; + + auto newSpecies = data.ref(0x0C); + + // Invalid species + if (gMonstrositySpeciesMap.find(newSpecies) == gMonstrositySpeciesMap.end()) + { + return; + } + + auto data = gMonstrositySpeciesMap[newSpecies]; + + // Not unlocked + if (PChar->m_PMonstrosity->levels[data.monstrosityId] == 0) + { + return; + } + + // If is a variant, and isn't unlocked, bail + if (newSpecies >= 256 && !IsVariantUnlocked(PChar, newSpecies - 256)) + { + return; + } + + PChar->m_PMonstrosity->Species = newSpecies; + + PChar->m_PMonstrosity->MonstrosityId = data.monstrosityId; + PChar->m_PMonstrosity->MainJob = data.mjob; + PChar->m_PMonstrosity->SubJob = data.sjob; + PChar->m_PMonstrosity->Size = data.size; + PChar->m_PMonstrosity->Look = data.look; + + // If changing "family" of species + if (PChar->m_PMonstrosity->MonstrosityId != previousId) + { + // Unequip all instincts + for (std::size_t idx = 0; idx < 12; ++idx) + { + PChar->m_PMonstrosity->EquippedInstincts[idx] = 0x0000; + } + + if (!settings::get("main.MONSTROSITY_DONT_WIPE_BUFFS")) + { + PChar->StatusEffectContainer->EraseAllStatusEffect(); + } + } + } + else if (flag == 0x04) // Instinct Change + { + auto previousEquipped = PChar->m_PMonstrosity->EquippedInstincts; + + // NOTE: This is set by the client + auto maxPoints = PChar->m_PMonstrosity->levels[PChar->m_PMonstrosity->MonstrosityId] + 10; + + // Remove All + if (data.ref(0x16) == 0xFFFF) + { + for (std::size_t idx = 0; idx < 12; ++idx) + { + uint16 value = data.ref(0x10 + (idx * 2)); + if (value != 0) + { + PChar->m_PMonstrosity->EquippedInstincts[idx] = 0x0000; + } + } + } + else // Set + { + for (std::size_t idx = 0; idx < 12; ++idx) + { + uint16 value = data.ref(0x10 + (idx * 2)); + if (value != 0) + { + if (value == 0xFFFF) + { + // Remove + PChar->m_PMonstrosity->EquippedInstincts[idx] = 0x0000; + + for (auto const& mod : gMonstrosityInstinctMap[previousEquipped[idx]].mods) + { + PChar->delModifier(mod.getModID(), mod.getModAmount()); + } + } + else + { + auto maybeInstinct = gMonstrosityInstinctMap.find(value); + if (maybeInstinct != gMonstrosityInstinctMap.end()) + { + if (!IsInstinctUnlocked(PChar, value)) + { + return; + } + + PChar->m_PMonstrosity->EquippedInstincts[idx] = value; + + // Validate cost + if (getTotalInstinctsCost(PChar->m_PMonstrosity->EquippedInstincts) > maxPoints || + instinctsContainDuplicates(PChar->m_PMonstrosity->EquippedInstincts)) + { + // Reset to what it was before and don't handle mods + PChar->m_PMonstrosity->EquippedInstincts = previousEquipped; + } + else + { + auto instinct = (*maybeInstinct).second; + for (auto const& mod : instinct.mods) + { + PChar->addModifier(mod.getModID(), mod.getModAmount()); + } + } + } + } + } + } + } + } + else if (flag == 0x08) // Name Change 1 + { + PChar->m_PMonstrosity->NamePrefix1 = data.ref(0x28); + } + else if (flag == 0x10) // Name Change 2 + { + PChar->m_PMonstrosity->NamePrefix2 = data.ref(0x29); + } + + WriteMonstrosityData(PChar); + + // TODO: Is this too much traffic? + SendFullMonstrosityUpdate(PChar); +} + +void monstrosity::SetLevel(CCharEntity* PChar, uint8 id, uint8 level) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + // TODO: Validate id and level + // TODO: If not unlocked, unlock whatever id is + PChar->m_PMonstrosity->levels[id] = level; +} + +void monstrosity::HandleDeathMenu(CCharEntity* PChar, uint8 type) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + PChar->health.hp = PChar->GetMaxHP(); + PChar->health.mp = PChar->GetMaxMP(); + PChar->animation = ANIMATION_NONE; + + PChar->updatemask |= UPDATE_HP; + + // Monstrosity death menu: + // 2: Retry + // 1: Cancel + if (type == 1) + { + luautils::OnMonstrosityReturnToEntrance(PChar); + } + else if (type == 2) + { + // TODO: Pick a location from the starting points list + + PChar->loc.p.x = 0.0f; + PChar->loc.p.y = 0.0f; + PChar->loc.p.z = 0.0f; + + PChar->SetDeathTimestamp(0); + + PChar->status = STATUS_TYPE::DISAPPEAR; + + PChar->clearPacketList(); + + // Restart this zone with Gestation effect + PChar->loc.destination = PChar->loc.zone->GetID(); + charutils::SendToZone(PChar, 2, zoneutils::GetZoneIPP(PChar->loc.destination)); + } +} + +bool monstrosity::IsInstinctUnlocked(CCharEntity* PChar, uint16 instinct) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return false; + } + + // Purchasable instincts are 768 onwards + if (instinct >= 768) + { + auto idx = instinct - 768; + uint8 byteOffset = 20 + (idx / 8); + uint8 shiftAmount = idx % 8; + + // There is a gap in the instincts bitpack, so we put the purchase information + // for these instincts in there. Sneaky sneaky. + if (byteOffset >= 20 && byteOffset < 24) + { + return PChar->m_PMonstrosity->instincts[byteOffset] & (0x01 << shiftAmount); + } + } + else + { + // TODO: Level-based instincts + } + + return false; +} + +bool monstrosity::IsVariantUnlocked(CCharEntity* PChar, uint8 variant) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return false; + } + + uint8 byteOffset = static_cast(variant) / 8; + uint8 shiftAmount = static_cast(variant) % 8; + + if (byteOffset < 32) + { + return PChar->m_PMonstrosity->variants[byteOffset] & (0x01 << shiftAmount); + } + + return false; +} + +void monstrosity::SetBelligerencyFlag(CCharEntity* PChar, bool flag) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + PChar->m_PMonstrosity->Belligerency = flag; + + WriteMonstrosityData(PChar); +} + +void monstrosity::MaxAllLevels(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + for (auto const& [_, entry] : gMonstrositySpeciesMap) + { + SetLevel(PChar, entry.monstrosityId, 99); + } +} + +void monstrosity::UnlockAllInstincts(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + // Level based + for (auto const& [_, entry] : gMonstrositySpeciesMap) + { + uint8 level = 99; + uint8 byteOffset = entry.monstrosityId / 4; + uint8 unlockAmount = level / 30; + uint8 shiftAmount = (entry.monstrosityId * 2) % 8; + + // Special case for writing Slime & Spriggan data at the end of the 64-byte array + if (byteOffset == 31) + { + byteOffset = 63; + } + + if (byteOffset < 64) + { + PChar->m_PMonstrosity->instincts[byteOffset] |= (unlockAmount << shiftAmount); + } + else + { + ShowError("byteOffset out of range"); + } + } + + // Instincts (Purchasable) + for (uint8 idx = 0; idx < 32; ++idx) + { + uint8 byteOffset = 20 + (idx / 8); + uint8 shiftAmount = idx % 8; + + // There is a gap in the instincts bitpack, so we put the purchase information + // for these instincts in there. Sneaky sneaky. + if (byteOffset >= 20 && byteOffset < 24) + { + PChar->m_PMonstrosity->instincts[byteOffset] |= (0x01 << shiftAmount); + } + else + { + ShowError("byteOffset out of range"); + } + } +} + +void monstrosity::UnlockAllVariants(CCharEntity* PChar) +{ + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + for (std::size_t idx = 0; idx < 256; ++idx) + { + uint8 byteOffset = static_cast(idx) / 8; + uint8 shiftAmount = static_cast(idx) % 8; + + if (byteOffset < 32) + { + PChar->m_PMonstrosity->variants[byteOffset] |= (0x01 << shiftAmount); + } + else + { + ShowError("byteOffset out of range"); + } + } +} diff --git a/src/map/monstrosity.h b/src/map/monstrosity.h new file mode 100644 index 00000000000..33ffa9a2d6a --- /dev/null +++ b/src/map/monstrosity.h @@ -0,0 +1,96 @@ +/* +=========================================================================== + + Copyright (c) 2023 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "common/cbasetypes.h" + +#include "packets/basic.h" + +#include "entities/battleentity.h" + +#include + +class CCharEntity; + +// === +// See scripts/globals/monstrosity.lua for a general overview of how Monstrosity works and is designed. +// === +namespace monstrosity +{ + struct MonstrosityData_t + { + public: + MonstrosityData_t(); + + uint8 MonstrosityId; + uint16 Species; + uint16 Flags; + uint16 Look; + uint8 Size; + + uint8 NamePrefix1; + uint8 NamePrefix2; + + JOBTYPE MainJob; + JOBTYPE SubJob; + uint32 CurrentExp; + + std::array EquippedInstincts{ 0 }; + std::array levels{ 0 }; + std::array instincts{ 0 }; + std::array variants{ 0 }; + + bool Belligerency; + + position_t EntryPos{}; + uint16 EntryZoneId; + uint8 EntryMainJob; + uint8 EntrySubJob; + }; + + void LoadStaticData(); + + void ReadMonstrosityData(CCharEntity* PChar); + void WriteMonstrosityData(CCharEntity* PChar); + + void TryPopulateMonstrosityData(CCharEntity* PChar); + void HandleZoneIn(CCharEntity* PChar); + uint32 GetPackedMonstrosityName(CCharEntity* PChar); + void SendFullMonstrosityUpdate(CCharEntity* PChar); + + void HandleMonsterSkillActionPacket(CCharEntity* PChar, CBasicPacket& data); + void HandleEquipChangePacket(CCharEntity* PChar, CBasicPacket& data); + + void SetLevel(CCharEntity* PChar, uint8 id, uint8 level); + + void HandleDeathMenu(CCharEntity* PChar, uint8 type); + + bool IsInstinctUnlocked(CCharEntity* PChar, uint16 instinct); + bool IsVariantUnlocked(CCharEntity* PChar, uint8 variant); + + void SetBelligerencyFlag(CCharEntity* PChar, bool flag); + + // Debug + void MaxAllLevels(CCharEntity* PChar); + void UnlockAllInstincts(CCharEntity* PChar); + void UnlockAllVariants(CCharEntity* PChar); +} // namespace monstrosity diff --git a/src/map/packet_system.cpp b/src/map/packet_system.cpp index 9ac21b3859b..2879d28228f 100644 --- a/src/map/packet_system.cpp +++ b/src/map/packet_system.cpp @@ -49,6 +49,7 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "map.h" #include "message.h" #include "mob_modifier.h" +#include "monstrosity.h" #include "notoriety_container.h" #include "packet_system.h" #include "party.h" @@ -80,6 +81,7 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "lua/luautils.h" #include "packets/auction_house.h" +#include "packets/basic.h" #include "packets/bazaar_check.h" #include "packets/bazaar_close.h" #include "packets/bazaar_confirmation.h" @@ -791,15 +793,19 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh { TracyZoneScoped; - uint16 TargID = data.ref(0x08); - uint8 action = data.ref(0x0A); - position_t actionOffset = { + uint16 TargID = data.ref(0x08); + uint8 action = data.ref(0x0A); + + // clang-format off + position_t actionOffset = + { data.ref(0x10), data.ref(0x14), data.ref(0x18), - 0, // packet only contains x/y/z - 0, // + 0, // moving (packet only contains x/y/z) + 0, // rotation (packet only contains x/y/z) }; + // clang-format on constexpr auto actionToStr = [](uint8 actionIn) { @@ -847,6 +853,8 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh return "Ballista - Scout"; case 0x18: return "Blockaid"; + case 0x19: + return "Monstrosity Monster Skill"; case 0x1A: return "Mounts"; default: @@ -854,6 +862,13 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh } }; + // Monstrosity: Can't really do anything while under Gestation until you click it off. + // : MONs can trigger doors, so we'll handle that later. + if (PChar->StatusEffectContainer->HasStatusEffect(EFFECT_GESTATION) && action == 0x00) + { + return; + } + auto actionStr = fmt::format("Player Action: {}: {} (0x{:02X}) -> targid: {}", PChar->GetName(), actionToStr(action), action, TargID); TracyZoneString(actionStr); ShowTrace(actionStr); @@ -877,6 +892,16 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh CBaseEntity* PNpc = nullptr; PNpc = PChar->GetEntity(TargID, TYPE_NPC | TYPE_MOB); + // MONs are allowed to use doors, but nothing else + if (PChar->m_PMonstrosity != nullptr && + PNpc->look.size != 0x02 && + PChar->getZone() != ZONEID::ZONE_FERETORY && + !settings::get("main.MONSTROSITY_TRIGGER_NPCS")) + { + PChar->pushPacket(new CReleasePacket(PChar, RELEASE_TYPE::STANDARD)); + return; + } + // NOTE: Moogles inside of mog houses are the exception for not requiring Spawned or Status checks. if (PNpc != nullptr && distance(PNpc->loc.p, PChar->loc.p) <= 10 && ((PNpc->PAI->IsSpawned() && PNpc->status == STATUS_TYPE::NORMAL) || PChar->m_moghouseID != 0)) { @@ -1005,6 +1030,14 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh { return; } + + if (PChar->m_PMonstrosity != nullptr) + { + auto type = data.ref(0x0C); + monstrosity::HandleDeathMenu(PChar, type); + return; + } + PChar->setCharVar("expLost", 0); charutils::HomePoint(PChar); } @@ -1173,6 +1206,11 @@ void SmallPacket0x01A(map_session_data_t* const PSession, CCharEntity* const PCh } } break; + case 0x19: // Monstrosity Monster Skill + { + monstrosity::HandleMonsterSkillActionPacket(PChar, data); + } + break; case 0x1A: // mounts { uint8 MountID = data.ref(0x0C); @@ -1497,6 +1535,13 @@ void SmallPacket0x029(map_session_data_t* const PSession, CCharEntity* const PCh void SmallPacket0x032(map_session_data_t* const PSession, CCharEntity* const PChar, CBasicPacket& data) { TracyZoneScoped; + + // MONs can't trade + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + uint32 charid = data.ref(0x04); uint16 targid = data.ref(0x08); @@ -1600,6 +1645,13 @@ void SmallPacket0x032(map_session_data_t* const PSession, CCharEntity* const PCh void SmallPacket0x033(map_session_data_t* const PSession, CCharEntity* const PChar, CBasicPacket& data) { TracyZoneScoped; + + // MONs can't trade + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + CCharEntity* PTarget = (CCharEntity*)PChar->GetEntity(PChar->TradePending.targid, TYPE_PC); if (PTarget != nullptr && PChar->TradePending.id == PTarget->id) @@ -1701,6 +1753,13 @@ void SmallPacket0x033(map_session_data_t* const PSession, CCharEntity* const PCh void SmallPacket0x034(map_session_data_t* const PSession, CCharEntity* const PChar, CBasicPacket& data) { TracyZoneScoped; + + // MONs can't trade + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + uint32 quantity = data.ref(0x04); uint16 itemID = data.ref(0x08); uint8 invSlotID = data.ref(0x0A); @@ -1805,6 +1864,12 @@ void SmallPacket0x036(map_session_data_t* const PSession, CCharEntity* const PCh return; } + // MONs can't trade + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + uint32 npcid = data.ref(0x04); uint16 targid = data.ref(0x3A); @@ -1854,6 +1919,12 @@ void SmallPacket0x037(map_session_data_t* const PSession, CCharEntity* const PCh { TracyZoneScoped; + // MONs can't use usable items + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + uint16 TargetID = data.ref(0x0C); uint8 SlotID = data.ref(0x0E); uint8 StorageID = data.ref(0x10); @@ -3947,6 +4018,16 @@ void SmallPacket0x05E(map_session_data_t* const PSession, CCharEntity* const PCh PChar->status = STATUS_TYPE::NORMAL; return; } + else if (PChar->m_PMonstrosity != nullptr) // Not allowed to use zonelines while MON + { + PChar->loc.p.rotation += 128; + + PChar->pushPacket(new CMessageSystemPacket(0, 0, 2)); // You could not enter the next area. + PChar->pushPacket(new CCSPositionPacket(PChar)); + + PChar->status = STATUS_TYPE::NORMAL; + return; + } else { // Ensure the destination exists @@ -6234,6 +6315,13 @@ void SmallPacket0x0DD(map_session_data_t* const PSession, CCharEntity* const PCh { CCharEntity* PTarget = (CCharEntity*)PEntity; + if (PTarget->m_PMonstrosity) + { + PChar->pushPacket(new CMessageStandardPacket(PTarget, 0, 0, MsgStd::MonstrosityCheckOut)); + PTarget->pushPacket(new CMessageStandardPacket(PChar, 0, 0, MsgStd::MonstrosityCheckIn)); + return; + } + if (!PChar->m_isGMHidden || (PChar->m_isGMHidden && PTarget->m_GMlevel >= PChar->m_GMlevel)) { PTarget->pushPacket(new CMessageStandardPacket(PChar, 0, 0, MsgStd::Examine)); @@ -7218,8 +7306,12 @@ void SmallPacket0x100(map_session_data_t* const PSession, CCharEntity* const PCh PChar->StatusEffectContainer->DelStatusEffectsByFlag(EFFECTFLAG_DISPELABLE | EFFECTFLAG_ROLL | EFFECTFLAG_ON_JOBCHANGE); + // clang-format off PChar->ForParty([](CBattleEntity* PMember) - { ((CCharEntity*)PMember)->PLatentEffectContainer->CheckLatentsPartyJobs(); }); + { + ((CCharEntity*)PMember)->PLatentEffectContainer->CheckLatentsPartyJobs(); + }); + // clang-format on PChar->UpdateHealth(); @@ -7246,7 +7338,7 @@ void SmallPacket0x100(map_session_data_t* const PSession, CCharEntity* const PCh /************************************************************************ * * - * Set Blue Magic Spells * + * Set Blue Magic Spells / PUP Attachments / MON equip * * * ************************************************************************/ @@ -7389,6 +7481,10 @@ void SmallPacket0x102(map_session_data_t* const PSession, CCharEntity* const PCh PChar->pushPacket(new CCharJobExtraPacket(PChar, false)); puppetutils::SaveAutomaton(PChar); } + else if (PChar->loc.zone->GetID() == ZONE_FERETORY && PChar->m_PMonstrosity != nullptr) + { + monstrosity::HandleEquipChangePacket(PChar, data); + } } /************************************************************************ diff --git a/src/map/packets/char.cpp b/src/map/packets/char.cpp index 0e42b7ed3dc..5109a4d7a36 100644 --- a/src/map/packets/char.cpp +++ b/src/map/packets/char.cpp @@ -163,7 +163,7 @@ void CCharPacket::updateWith(CCharEntity* PChar, ENTITYUPDATE type, uint8 update ref(0x42) = 0x50 + PChar->StatusEffectContainer->GetStatusEffect(EFFECT_COLURE_ACTIVE)->GetPower(); } - ref(0x43) = 0x04; + ref(0x43) = 0x04; // Seen as 0x0C and 0x06 in Monstrosity? if (updatemask & UPDATE_LOOK) { @@ -179,9 +179,9 @@ void CCharPacket::updateWith(CCharEntity* PChar, ENTITYUPDATE type, uint8 update ref(0x56) = look->sub + 0x7000; ref(0x58) = look->ranged + 0x8000; - if (PChar->m_Monstrosity != 0) + if (PChar->m_Costume2 != 0) { - ref(0x48) = PChar->m_Monstrosity; + ref(0x48) = PChar->m_Costume2; ref(0x58) = 0xFFFF; } } @@ -190,6 +190,13 @@ void CCharPacket::updateWith(CCharEntity* PChar, ENTITYUPDATE type, uint8 update { memcpy(data + (0x5A), PChar->GetName().c_str(), PChar->GetName().size()); } + + if (PChar->m_PMonstrosity != nullptr && (updatemask & UPDATE_HP || updatemask & UPDATE_LOOK)) + { + ref(0x3E) = monstrosity::GetPackedMonstrosityName(PChar); + ref(0x48) = PChar->m_PMonstrosity->Look; + ref(0x58) = 0xFFFF; + } } break; default: diff --git a/src/map/packets/char_appearance.cpp b/src/map/packets/char_appearance.cpp index e891fcbd5e7..b0f9da09241 100644 --- a/src/map/packets/char_appearance.cpp +++ b/src/map/packets/char_appearance.cpp @@ -41,9 +41,15 @@ CCharAppearancePacket::CCharAppearancePacket(CCharEntity* PChar) ref(0x12) = look->sub + 0x7000; ref(0x14) = look->ranged + 0x8000; - if (PChar->m_Monstrosity != 0) + if (PChar->m_Costume2 != 0) { - ref(0x04) = PChar->m_Monstrosity; + ref(0x04) = PChar->m_Costume2; + ref(0x14) = 0xFFFF; + } + + if (PChar->m_PMonstrosity != nullptr) + { + ref(0x04) = PChar->m_PMonstrosity->Look; ref(0x14) = 0xFFFF; } } diff --git a/src/map/packets/char_health.cpp b/src/map/packets/char_health.cpp index 60b9120ffcc..823414f2230 100644 --- a/src/map/packets/char_health.cpp +++ b/src/map/packets/char_health.cpp @@ -26,6 +26,8 @@ #include "entities/charentity.h" #include "entities/trustentity.h" +#include "monstrosity.h" + CCharHealthPacket::CCharHealthPacket(CCharEntity* PChar) { this->setType(0xDF); @@ -42,6 +44,11 @@ CCharHealthPacket::CCharHealthPacket(CCharEntity* PChar) ref(0x16) = PChar->GetHPP(); ref(0x17) = PChar->GetMPP(); + if (PChar->m_PMonstrosity != nullptr) + { + ref(0x1C) = monstrosity::GetPackedMonstrosityName(PChar); + } + if (!(PChar->nameflags.flags & FLAG_ANON)) { ref(0x20) = PChar->GetMJob(); diff --git a/src/map/packets/char_job_extra.cpp b/src/map/packets/char_job_extra.cpp index 0f1df2bc4fc..f6f128e1888 100644 --- a/src/map/packets/char_job_extra.cpp +++ b/src/map/packets/char_job_extra.cpp @@ -29,6 +29,7 @@ #include "entities/automatonentity.h" #include "entities/charentity.h" #include "merit.h" +#include "monstrosity.h" CCharJobExtraPacket::CCharJobExtraPacket(CCharEntity* PChar, bool mjob) { @@ -46,6 +47,11 @@ CCharJobExtraPacket::CCharJobExtraPacket(CCharEntity* PChar, bool mjob) job = PChar->GetSJob(); } + if (PChar->m_PMonstrosity != nullptr) + { + job = JOB_MON; + } + ref(0x04) = job; if (!mjob) { @@ -127,4 +133,13 @@ CCharJobExtraPacket::CCharJobExtraPacket(CCharEntity* PChar, bool mjob) ref(0x9C) = PChar->getMod(Mod::AUTO_ELEM_CAPACITY); } + else if (PChar->m_PMonstrosity != nullptr) + { + ref(0x08) = PChar->m_PMonstrosity->Species; + + for (std::size_t idx = 0; idx < 12; ++idx) + { + ref(0x0C + (idx * 2)) = PChar->m_PMonstrosity->EquippedInstincts[idx]; + } + } } diff --git a/src/map/packets/char_jobs.cpp b/src/map/packets/char_jobs.cpp index 69a0a398deb..5e715a0ebef 100644 --- a/src/map/packets/char_jobs.cpp +++ b/src/map/packets/char_jobs.cpp @@ -25,6 +25,7 @@ #include "char_jobs.h" #include "entities/charentity.h" +#include "monstrosity.h" CCharJobsPacket::CCharJobsPacket(CCharEntity* PChar) { @@ -56,4 +57,14 @@ CCharJobsPacket::CCharJobsPacket(CCharEntity* PChar) ref(0x68) = 0; // Is job mastered, and has Master Breaker KI ref(0x6D) = 0; // Master Level + + if (PChar->m_PMonstrosity != nullptr) + { + ref(0x08) = static_cast(JOB_MON); + ref(0x0B) = static_cast(JOB_MON); + + // ref(0x10) = 0x01; // ? + + // ref(0x5F) = 0x10; // MON level ? + } } diff --git a/src/map/packets/char_sync.cpp b/src/map/packets/char_sync.cpp index f74063d3d4e..db4db991bc1 100644 --- a/src/map/packets/char_sync.cpp +++ b/src/map/packets/char_sync.cpp @@ -37,14 +37,14 @@ CCharSyncPacket::CCharSyncPacket(CCharEntity* PChar) ref(0x08) = PChar->id; // ref(0x0C) = PChar->PFellow ? PChar->PFellow->targid : 0 - ref(0x10) = PChar->StatusEffectContainer->HasStatusEffect(EFFECT_ALLIED_TAGS) ? 2 : 0; // 0x02 - Campaign Battle, 0x04 - Level Sync + ref(0x10) = PChar->StatusEffectContainer->HasStatusEffect(EFFECT_ALLIED_TAGS) ? 0x02 : 0x00; // 0x02 - Campaign Battle, 0x04 - Level Sync if (PChar->m_LevelRestriction && PChar->StatusEffectContainer->HasStatusEffect(EFFECT_LEVEL_SYNC)) { if (PChar->PBattlefield == nullptr) { // Only display the level sync icon outside of BCNMs - ref(0x10) |= 4; + ref(0x10) |= 0x04; } ref(0x26) = PChar->m_LevelRestriction; diff --git a/src/map/packets/char_update.cpp b/src/map/packets/char_update.cpp index d0f71c2b05f..3d50146f5a6 100644 --- a/src/map/packets/char_update.cpp +++ b/src/map/packets/char_update.cpp @@ -52,6 +52,7 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) { ref(0x2D) = 0x80; } + if (PChar->StatusEffectContainer->HasStatusEffect(EFFECT_SNEAK)) { ref(0x38) = 0x04; @@ -61,6 +62,7 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) { ref(0x38) |= 0x10; // Mentor flag. } + if (PChar->isNewPlayer()) { ref(0x38) |= 0x08; // New player ? @@ -81,10 +83,12 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) ref(0x32) = (LSColor.G << 4) + 15; ref(0x33) = (LSColor.B << 4) + 15; } + if (PChar->PPet != nullptr) { ref(0x34) = PChar->PPet->targid << 3; } + // Status flag: bit 4: frozen anim (terror), // bit 6/7/8 related to Ballista (6 set - normal, 7 set san d'oria, 6+7 set bastok, 8 set windurst) uint8 flag = (static_cast(PChar->allegiance) << 5); @@ -96,6 +100,9 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) ref(0x36) = flag; + // Sword & Shield Icon (Campaign battles, etc?) + // ref(0x37) = 0x01; + uint32 timeRemainingToForcedHomepoint = PChar->GetTimeRemainingUntilDeathHomepoint(); ref(0x3C) = timeRemainingToForcedHomepoint; @@ -107,6 +114,7 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) { ref(0x4A) = PChar->hookDelay; } + ref(0x4C) = PChar->StatusEffectContainer->m_Flags; // GEO bubble effects, changes bubble effect depending on what effect is activated. @@ -125,4 +133,15 @@ CCharUpdatePacket::CCharUpdatePacket(CCharEntity* PChar) ref(0x29) |= static_cast(PChar->StatusEffectContainer->GetStatusEffect(EFFECT_MOUNTED)->GetSubPower()); ref(0x5B) = PChar->StatusEffectContainer->GetStatusEffect(EFFECT_MOUNTED)->GetPower(); } + + if (PChar->m_PMonstrosity != nullptr) + { + ref(0x54) = monstrosity::GetPackedMonstrosityName(PChar); + + // Sword & Shield icon only shows outside of the Feretory + if (PChar->m_PMonstrosity->Belligerency && PChar->loc.zone->GetID() != ZONE_FERETORY) + { + ref(0x37) |= 0x01; + } + } } diff --git a/src/map/packets/message_basic.h b/src/map/packets/message_basic.h index 227e6253b4c..e747e2e8f30 100644 --- a/src/map/packets/message_basic.h +++ b/src/map/packets/message_basic.h @@ -171,12 +171,14 @@ enum MSGBASIC_ID : uint16 MSGBASIC_MERIT_INCREASE = 380, // Your modification has risen to level MSGBASIC_MERIT_DECREASE = 381, // Your modification has dropped to level /* DEBUG MESSAGES */ - MSGBASIC_DEBUG_RESISTED_SPELL = 66, /* Debug: Resisted spell! */ - MSGBASIC_DEBUG_RECEIVED_STATUS = 73, /* Debug: 's status is now .. */ - MSGBASIC_DEBUG_RECOVERED_STATUS = 74, /* Debug: recovers from .. */ - MSGBASIC_DEBUG_DBLATK_PROC = 79, /* Debug: uses Double Attack (..%) */ - MSGBASIC_DEBUG_TRPATK_PROC = 80, /* Debug: uses Triple Attack (..%) */ - MSGBASIC_DEBUG_SUCCESS_CHANCE = 255 /* DEBUG: ..% chance of success */ + MSGBASIC_DEBUG_RESISTED_SPELL = 66, /* Debug: Resisted spell! */ + MSGBASIC_DEBUG_RECEIVED_STATUS = 73, /* Debug: 's status is now .. */ + MSGBASIC_DEBUG_RECOVERED_STATUS = 74, /* Debug: recovers from .. */ + MSGBASIC_DEBUG_DBLATK_PROC = 79, /* Debug: uses Double Attack (..%) */ + MSGBASIC_DEBUG_TRPATK_PROC = 80, /* Debug: uses Triple Attack (..%) */ + MSGBASIC_DEBUG_SUCCESS_CHANCE = 255, /* DEBUG: ..% chance of success */ + /* Monstrosity */ + MSGBASIC_FERETORY_COUNTDOWN = 679, // will return to the Feretory in }; class CBaseEntity; diff --git a/src/map/packets/message_standard.cpp b/src/map/packets/message_standard.cpp index fd53a9d2b16..972bcb2b99b 100644 --- a/src/map/packets/message_standard.cpp +++ b/src/map/packets/message_standard.cpp @@ -81,6 +81,14 @@ CMessageStandardPacket::CMessageStandardPacket(CCharEntity* PChar, uint32 param0 ref(0x0C) = 0x10; + snprintf((char*)data + (0x0D), 24, "string2 %s", PChar->GetName().c_str()); + } + else if (MessageID == MsgStd::MonstrosityCheckIn || MessageID == MsgStd::MonstrosityCheckOut) + { + this->setSize(0x20); + + ref(0x0A) = static_cast(MessageID); + snprintf((char*)data + (0x0D), 24, "string2 %s", PChar->GetName().c_str()); } } diff --git a/src/map/packets/message_standard.h b/src/map/packets/message_standard.h index 2098c7f259a..75d346d2a48 100644 --- a/src/map/packets/message_standard.h +++ b/src/map/packets/message_standard.h @@ -99,6 +99,8 @@ enum class MsgStd CannotHere = 256, // You cannot use that command in this area. HeadgearShow = 260, HeadgearHide = 261, + MonstrosityCheckOut = 263, // This monster is currently possessed by ! + MonstrosityCheckIn = 264, // stares at you intently, evidently aware that you have discovered its true form. TrustCannotJoinParty = 265, // You are unable to join a party whose leader currently has an alter ego present. TrustCannotJoinAlliance = 266, // You are unable to join an alliance whose leader currently has an alter ego present. StyleLockOn = 267, // Style lock mode enabled. diff --git a/src/map/packets/monipulator1.cpp b/src/map/packets/monipulator1.cpp index 53226c21b12..4d0bcc32bc1 100644 --- a/src/map/packets/monipulator1.cpp +++ b/src/map/packets/monipulator1.cpp @@ -19,24 +19,52 @@ =========================================================================== */ -#include "common/socket.h" - #include "monipulator1.h" +#include "common/socket.h" +#include "entities/charentity.h" +#include "monstrosity.h" +#include "utils/charutils.h" + CMonipulatorPacket1::CMonipulatorPacket1(CCharEntity* PChar) { this->setType(0x63); this->setSize(0xDC); + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + ref(0x04) = 0x03; // Update Type ref(0x06) = 0xD8; // Variable Data Size - ref(0x08) = 0; // Species - ref(0x0A) = 0; // Flags? - ref(0x0C) = 0; // Monstrosity Rank (0 = Mon, 1 = NM, 2 = HNM) + ref(0x08) = PChar->m_PMonstrosity->Species; + ref(0x0A) = PChar->m_PMonstrosity->Flags; + + int32 infamy = charutils::GetPoints(PChar, "infamy"); + + // Monstrosity Rank (0 = Mon, 1 = NM, 2 = HNM) + // TODO: The ranks are listed as: + // 0~10,000 Mon. (Monster) + // 10,001~20,000 NM (Notorious Monster) + // 20,001+ HNM (Highly Notorious Monster) + // But this integer division gives the next rank on 10'000 etc. + // FIXME + ref(0x0C) = static_cast(infamy / 10000); + + // Unknown + ref(0x10) = 0xEC; + ref(0x11) = 0x00; + + ref(0x12) = infamy; + + // Unknown + ref(0x14) = 0x2C; - ref(0x12) = 0; // Infamy + // Bitpacked 2-bit values. 0 = no instincts from that species, 1 == first instinct, 2 == first and second instinct, 3 == first, second, and third instinct. + std::memcpy(data + 0x1C, PChar->m_PMonstrosity->instincts.data(), 64); // Instinct Bitfield 1 - std::memset(data + 0x1C, 0, 64); // Instinct Battlefield 1 - std::memset(data + 0x5C, 0, 128); // Monster Level Char Field + // Mapped onto the item ID for these creatures. (00 doesn't exist, 01 is rabbit, 02 is behemoth, etc.) + std::memcpy(data + 0x5C, PChar->m_PMonstrosity->levels.data(), 128); // Monster Level Bitfield } diff --git a/src/map/packets/monipulator2.cpp b/src/map/packets/monipulator2.cpp index 05782b01e78..a3cef1ebf78 100644 --- a/src/map/packets/monipulator2.cpp +++ b/src/map/packets/monipulator2.cpp @@ -19,17 +19,39 @@ =========================================================================== */ -#include "common/socket.h" - #include "monipulator2.h" +#include "common/socket.h" +#include "entities/charentity.h" +#include "monstrosity.h" + CMonipulatorPacket2::CMonipulatorPacket2(CCharEntity* PChar) { this->setType(0x63); this->setSize(0xB4); - memset(data + 4, 0, PACKET_SIZE - 4); + if (PChar->m_PMonstrosity == nullptr) + { + return; + } + + std::memset(data + 4, 0, PACKET_SIZE - 4); + // TODO: What are these? std::array packet2 = { 0x04, 0x00, 0xB0 }; - memcpy(data + (0x04), &packet2, sizeof(packet2)); + std::memcpy(data + 0x04, &packet2, sizeof(packet2)); + + // NOTE: SE added these after-the-fact, so they're not sent in Monipulator1 and they're at the end of the array! + // Slime Level + ref(0x86) = PChar->m_PMonstrosity->levels[126]; + + // Spriggan Level + ref(0x87) = PChar->m_PMonstrosity->levels[127]; + + // Contains job/race instincts from the 0x03 set. Has 8 unused bytes. This is a 1:1 mapping. + // Since this has 8 unused bytes, we're only going to use 4 from instincts[20:23] + std::memcpy(data + 0x88, PChar->m_PMonstrosity->instincts.data() + 20, 4); // Instinct Bitfield 3 + + // Does not show normal monsters, only variants. Bit is 1 if the variant is owned. Length is an estimation including the possible padding. + std::memcpy(data + 0x94, PChar->m_PMonstrosity->variants.data(), 32); // Variants Bitfield } diff --git a/src/map/packets/zone_in.cpp b/src/map/packets/zone_in.cpp index 3a45e95a472..7ffe0a10327 100644 --- a/src/map/packets/zone_in.cpp +++ b/src/map/packets/zone_in.cpp @@ -118,7 +118,7 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) this->setType(0x0A); this->setSize(0x104); - // It is necessary to work Manaklipper + // It is necessary to work Manaclipper // The last 8 bytes are similar for a while // unsigned char packet [] = { // 0x0D, 0x3A, 0x0C, 0x00, 0x11, 0x00, 0x19, 0x00, 0x02, 0xE4, 0x93, 0x10, 0x91, 0xE5, 0x93, 0x10}; // 0x2a = 0x10 @@ -130,18 +130,24 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) ref(0x04) = PChar->id; ref(0x08) = PChar->targid; - memcpy(data + (0x84), PChar->GetName().c_str(), PChar->GetName().size()); + // 0x0A = Padding ref(0x0B) = PChar->loc.p.rotation; ref(0x0C) = PChar->loc.p.x; ref(0x10) = PChar->loc.p.y; ref(0x14) = PChar->loc.p.z; + // 0x18 = Run Count + + // 0x1A = Target Index + ref(0x1C) = PChar->GetSpeed(); ref(0x1D) = PChar->speedsub; ref(0x1E) = PChar->GetHPP(); ref(0x1F) = PChar->animation; + // 0x20 = Character Gender and Size + if (PChar->StatusEffectContainer->HasStatusEffect(EFFECT_MOUNTED)) { ref(0x20) = static_cast(PChar->StatusEffectContainer->GetStatusEffect(EFFECT_MOUNTED)->GetSubPower()); @@ -149,6 +155,10 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) ref(0x21) = PChar->GetGender() * 128 + (1 << PChar->look.size); + ref(0x28) = 0x0100; // "Always 0x100" + + // 0x2A = Zone Animation + look_t* look = (PChar->getStyleLocked() ? &PChar->mainlook : &PChar->look); ref(0x44) = look->face; ref(0x45) = look->race; @@ -161,12 +171,6 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) ref(0x52) = look->sub + 0x7000; ref(0x54) = look->ranged + 0x8000; - if (PChar->m_Monstrosity != 0) - { - ref(0x44) = PChar->m_Monstrosity; - ref(0x54) = 0xFFFF; - } - ref(0x56) = PChar->PInstance ? PChar->PInstance->GetBackgroundMusicDay() : PChar->loc.zone->GetBackgroundMusicDay(); ref(0x58) = PChar->PInstance ? PChar->PInstance->GetBackgroundMusicNight() : PChar->loc.zone->GetBackgroundMusicNight(); ref(0x5A) = PChar->PInstance ? PChar->PInstance->GetSoloBattleMusic() : PChar->loc.zone->GetSoloBattleMusic(); @@ -200,7 +204,7 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) if (PChar->m_moghouseID != 0) { - ref(0x80) = 1; + ref(0x80) = 0x01; if (PChar->profile.mhflag & 0x0040) // On MH2F { @@ -212,7 +216,7 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) } else { - ref(0x80) = 2; + ref(0x80) = 0x02; ref(0xAA) = 0x01FF; // TODO: This has also been seen as 0x04 and 0x07 @@ -220,6 +224,9 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) ref(0xAF) = PChar->loc.zone->CanUseMisc(MISC_MOGMENU); // flag allows you to use Mog Menu outside Mog House } + auto const& nameStr = PChar->GetName(); + std::memcpy(data + 0x84, nameStr.data(), nameStr.size()); + ref(0xA0) = PChar->GetPlayTime(); // time spent by the character in the game from the moment of creation ref(0xAE) = GetMogHouseLeavingFlag(PChar); @@ -249,4 +256,30 @@ CZoneInPacket::CZoneInPacket(CCharEntity* PChar, const EventInfo* currentEvent) ref(0xF8) = PChar->chatFilterFlags; ref(0x100) = 0x01; // observed: RoZ = 3, CoP = 5, ToAU = 9, WoTG = 11, SoA/original areas = 1 + + if (PChar->GetMJob() == JOB_MON) + { + monstrosity::ReadMonstrosityData(PChar); + } + + if (PChar->loc.zone->GetID() == ZONE_FERETORY) + { + // This disables the zone model, but also disables abilities etc. + ref(0x80) = 0x01; + + // Zone Model + ref(0xAA) = 0x02D9; // 729 + } + + if (PChar->m_PMonstrosity != nullptr) + { + // look_t data from above + ref(0x44) = PChar->m_PMonstrosity->Look; + ref(0x54) = 0xFFFF; + + // Enable Monstrosity menu options + ref(0x7E) = 0x1F; + + ref(0x98) = monstrosity::GetPackedMonstrosityName(PChar); + } } diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index d16c7313506..55a3d39d4ed 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -125,13 +125,14 @@ namespace battleutils ret = sql->Query(fmtQuery); + // NOTE: Skip over Monstrosity, they re-use other jobs ranks if (ret != SQL_ERROR && sql->NumRows() != 0) { - for (uint32 x = 0; x < MAX_SKILLTYPE && sql->NextRow() == SQL_SUCCESS; ++x) + for (uint32 x = 0; x < JOB_MON && sql->NextRow() == SQL_SUCCESS; ++x) { - auto SkillID = std::clamp(sql->GetIntData(0), 0, MAX_SKILLTYPE - 1); + auto SkillID = std::clamp(sql->GetIntData(0), 0, JOB_MON - 1); - for (uint32 y = 1; y < MAX_JOBTYPE; ++y) + for (uint32 y = 1; y < JOB_MON; ++y) { g_SkillRanks[SkillID][y] = std::clamp(sql->GetIntData(y), 0, 11); } diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index d4d2e14ad2b..5f51e8c52d3 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -146,6 +146,23 @@ namespace charutils JOBTYPE sjob = PChar->GetSJob(); MERIT_TYPE statMerit[] = { MERIT_STR, MERIT_DEX, MERIT_VIT, MERIT_AGI, MERIT_INT, MERIT_MND, MERIT_CHR }; + // We have to make sure we don't leave the job as JOB_MON - we CANNOT generate stats for it. + if (mjob == JOB_MON || sjob == JOB_MON) + { + mjob = JOB_WAR; + sjob = JOB_WAR; + } + + // NOTE: Monstrosity (MON) is treated as its own job, but each species is it's own + // : combination of main/sub job for stats, traits and abilities. + if (PChar->m_PMonstrosity != nullptr) + { + mjob = PChar->m_PMonstrosity->MainJob; + sjob = PChar->m_PMonstrosity->SubJob; + mlvl = PChar->m_PMonstrosity->levels[PChar->m_PMonstrosity->MonstrosityId]; + slvl = mlvl; + } + uint8 race = 0; // Hume switch (PChar->look.race) @@ -856,6 +873,8 @@ namespace charutils PChar->m_FieldChocobo = sql->GetUIntData(0); } + monstrosity::TryPopulateMonstrosityData(PChar); + charutils::LoadInventory(PChar); CalculateStats(PChar); @@ -1611,11 +1630,17 @@ namespace charutils bool CanTrade(CCharEntity* PChar, CCharEntity* PTarget) { + if (PChar->m_PMonstrosity != nullptr || PTarget->m_PMonstrosity != nullptr) + { + return false; + } + if (PTarget->getStorage(LOC_INVENTORY)->GetFreeSlotsCount() < PChar->UContainer->GetItemsCount()) { ShowDebug("Unable to trade, %s doesn't have enough inventory space", PTarget->GetName()); return false; } + for (uint8 slotid = 0; slotid <= 8; ++slotid) { CItem* PItem = PChar->UContainer->GetItem(slotid); @@ -1629,6 +1654,7 @@ namespace charutils } } } + return true; } @@ -2926,19 +2952,15 @@ namespace charutils void BuildingCharAbilityTable(CCharEntity* PChar) { - std::vector AbilitiesList; - if (PChar == nullptr) { ShowWarning("charutils::BuildingCharAbilityTable() - PChar was null."); return; } - memset(&PChar->m_Abilities, 0, sizeof(PChar->m_Abilities)); + std::memset(&PChar->m_Abilities, 0, sizeof(PChar->m_Abilities)); - AbilitiesList = ability::GetAbilities(PChar->GetMJob()); - - for (auto PAbility : AbilitiesList) + for (auto PAbility : ability::GetAbilities(PChar->GetMJob())) { if (PAbility == nullptr) { @@ -2976,9 +2998,7 @@ namespace charutils return; } - AbilitiesList = ability::GetAbilities(PChar->GetSJob()); - - for (auto PAbility : AbilitiesList) + for (auto PAbility : ability::GetAbilities(PChar->GetSJob())) { if (PChar->GetSLevel() >= PAbility->getLevel()) { @@ -3214,17 +3234,32 @@ namespace charutils PChar->TraitList.clear(); memset(&PChar->m_TraitList, 0, sizeof(PChar->m_TraitList)); - battleutils::AddTraits(PChar, traits::GetTraits(PChar->GetMJob()), PChar->GetMLevel()); - battleutils::AddTraits(PChar, traits::GetTraits(PChar->GetSJob()), PChar->GetSLevel()); + auto mjob = PChar->GetMJob(); + auto sjob = PChar->GetSJob(); + auto mlvl = PChar->GetMLevel(); + auto slvl = PChar->GetSLevel(); - if (PChar->GetMJob() == JOB_BLU || PChar->GetSJob() == JOB_BLU) + // NOTE: Monstrosity (MON) is treated as its own job, but each species is it's own + // : combination of main/sub job for stats, traits and abilities. + if (PChar->m_PMonstrosity != nullptr) + { + mjob = PChar->m_PMonstrosity->MainJob; + sjob = PChar->m_PMonstrosity->SubJob; + mlvl = PChar->m_PMonstrosity->levels[PChar->m_PMonstrosity->MonstrosityId]; + slvl = mlvl; + } + + battleutils::AddTraits(PChar, traits::GetTraits(mjob), mlvl); + battleutils::AddTraits(PChar, traits::GetTraits(sjob), slvl); + + if (mjob == JOB_BLU || sjob == JOB_BLU) { blueutils::CalculateTraits(PChar); } PChar->delModifier(Mod::MEVA, PChar->m_magicEvasion); - PChar->m_magicEvasion = battleutils::GetMaxSkill(12, PChar->GetMLevel()); // Player MEVA is Rank G + PChar->m_magicEvasion = battleutils::GetMaxSkill(12, mlvl); // Player MEVA is Rank G PChar->addModifier(Mod::MEVA, PChar->m_magicEvasion); } @@ -4621,6 +4656,12 @@ namespace charutils return; } + // MONs don't lose exp on death + if (PChar->m_PMonstrosity != nullptr) + { + return; + } + uint8 mLevel = (PChar->m_LevelRestriction != 0 && PChar->m_LevelRestriction < PChar->GetMLevel()) ? PChar->m_LevelRestriction : PChar->GetMLevel(); uint16 exploss = mLevel <= 67 ? (GetExpNEXTLevel(mLevel) * 8) / 100 : 2400; @@ -5423,6 +5464,12 @@ namespace charutils return; } + // Monstrosity job and level data is handled elsewhere, bail out now + if (job == JOB_MON) + { + return; + } + const char* fmtQuery = ""; switch (job) @@ -5510,6 +5557,12 @@ namespace charutils return; } + // Monstrosity exp data is handled elsewhere, bail out now + if (job == JOB_MON) + { + return; + } + const char* Query = ""; switch (job) diff --git a/src/map/zone.cpp b/src/map/zone.cpp index a8760573ea2..ac9be93dae3 100644 --- a/src/map/zone.cpp +++ b/src/map/zone.cpp @@ -40,6 +40,7 @@ #include "linkshell.h" #include "map.h" #include "message.h" +#include "monstrosity.h" #include "notoriety_container.h" #include "party.h" #include "spell.h" @@ -1010,7 +1011,6 @@ void CZone::createZoneTimers() void CZone::CharZoneIn(CCharEntity* PChar) { TracyZoneScoped; - // ищем свободный targid для входящего в зону персонажа PChar->loc.zone = this; PChar->loc.zoning = false; @@ -1093,6 +1093,8 @@ void CZone::CharZoneIn(CCharEntity* PChar) } } + monstrosity::HandleZoneIn(PChar); + PChar->PLatentEffectContainer->CheckLatentsZone(); charutils::ReadHistory(PChar); diff --git a/src/map/zone_entities.cpp b/src/map/zone_entities.cpp index 222cb272ddd..501ce5b6f5a 100644 --- a/src/map/zone_entities.cpp +++ b/src/map/zone_entities.cpp @@ -740,6 +740,19 @@ void CZoneEntities::SpawnPCs(CCharEntity* PChar) continue; } + // TODO: This is a temporary fix so that Feretory _seems_ like a solo zone. + // We need a better solution for this that properly supports: + // - Shared moghouse (both floors) + // - Mog Garden + // - Feretory + // and the NPCs that exist per-player in those zones (Garden, Music Items in MH, etc.) + // Despawn character if it's a hidden GM, is in a different mog house, or if player is in a conflict while other is not, or too far up/down + if (PChar->loc.zone->GetID() == ZONE_FERETORY) + { + toRemove.emplace_back(pc); + continue; + } + // Despawn character if it's currently spawned and is far away float charDistance = distance(PChar->loc.p, pc->loc.p); if (charDistance >= CHARACTER_DESPAWN_DISTANCE) diff --git a/src/search/data_loader.cpp b/src/search/data_loader.cpp index 043e0dae870..a2bbf9dcd95 100644 --- a/src/search/data_loader.cpp +++ b/src/search/data_loader.cpp @@ -30,6 +30,11 @@ along with this program. If not, see http://www.gnu.org/licenses/ #include "data_loader.h" #include "search.h" +namespace +{ + uint8 JOB_MON = 23; +} // namespace + CDataLoader::CDataLoader() : sql(std::make_unique()) { @@ -287,30 +292,37 @@ std::list CDataLoader::GetPlayersList(search_req sr, int* count) { PPlayer->flags1 |= 0x0001; } + if (partyid == PPlayer->id) { PPlayer->flags1 |= 0x0008; } + if (PPlayer->seacom_type) { PPlayer->flags1 |= 0x0010; } + if (nameflag & FLAG_AWAY) { PPlayer->flags1 |= 0x0100; } + if (nameflag & FLAG_DC) { PPlayer->flags1 |= 0x0800; } + if (partyid != 0) { PPlayer->flags1 |= 0x2000; } + if (nameflag & FLAG_ANON) { PPlayer->flags1 |= 0x4000; } + if (nameflag & FLAG_INVITE) { PPlayer->flags1 |= 0x8000; @@ -318,6 +330,12 @@ std::list CDataLoader::GetPlayersList(search_req sr, int* count) PPlayer->flags2 = PPlayer->flags1; + if (PPlayer->mjob == JOB_MON || PPlayer->sjob == JOB_MON) + { + PPlayer->mjob = 0; + PPlayer->sjob = 0; + } + // filter by job if (sr.jobid > 0 && sr.jobid != PPlayer->mjob) { diff --git a/tools/dbtool.py b/tools/dbtool.py index 3b31aa91ff3..62f390ba5fc 100644 --- a/tools/dbtool.py +++ b/tools/dbtool.py @@ -196,6 +196,7 @@ def load_into_dict(filename, settings): "char_job_points.sql", "char_look.sql", "char_merit.sql", + "char_monstrosity.sql", "char_pet.sql", "char_points.sql", "char_profile.sql",