diff --git a/scripts/enum/effect_flag.lua b/scripts/enum/effect_flag.lua index db36351e351..357198572e5 100644 --- a/scripts/enum/effect_flag.lua +++ b/scripts/enum/effect_flag.lua @@ -6,35 +6,36 @@ xi = xi or {} ---@enum xi.effectFlag xi.effectFlag = { - NONE = 0x00000000, - DISPELABLE = 0x00000001, - ERASABLE = 0x00000002, - ATTACK = 0x00000004, - EMPATHY = 0x00000008, - DAMAGE = 0x00000010, - DEATH = 0x00000020, - MAGIC_BEGIN = 0x00000040, - MAGIC_END = 0x00000080, - ON_ZONE = 0x00000100, - NO_LOSS_MESSAGE = 0x00000200, - INVISIBLE = 0x00000400, - DETECTABLE = 0x00000800, - NO_REST = 0x00001000, - PREVENT_ACTION = 0x00002000, - WALTZABLE = 0x00004000, - FOOD = 0x00008000, - SONG = 0x00010000, - ROLL = 0x00020000, - SYNTH_SUPPORT = 0x00040000, - CONFRONTATION = 0x00080000, - LOGOUT = 0x00100000, - BLOODPACT = 0x00200000, - ON_JOBCHANGE = 0x00400000, - NO_CANCEL = 0x00800000, - INFLUENCE = 0x01000000, - OFFLINE_TICK = 0x02000000, - AURA = 0x04000000, - HIDE_TIMER = 0x08000000, - ON_ZONE_PATHOS = 0x10000000, - ALWAYS_EXPIRING = 0x20000000, + NONE = 0x00000000, + DISPELABLE = 0x00000001, + ERASABLE = 0x00000002, + ATTACK = 0x00000004, + EMPATHY = 0x00000008, + DAMAGE = 0x00000010, + DEATH = 0x00000020, + MAGIC_BEGIN = 0x00000040, + MAGIC_END = 0x00000080, + ON_ZONE = 0x00000100, + NO_LOSS_MESSAGE = 0x00000200, + INVISIBLE = 0x00000400, + DETECTABLE = 0x00000800, + NO_REST = 0x00001000, + PREVENT_ACTION = 0x00002000, + WALTZABLE = 0x00004000, + FOOD = 0x00008000, + SONG = 0x00010000, + ROLL = 0x00020000, + SYNTH_SUPPORT = 0x00040000, + CONFRONTATION = 0x00080000, + LOGOUT = 0x00100000, + BLOODPACT = 0x00200000, + ON_JOBCHANGE = 0x00400000, + NO_CANCEL = 0x00800000, + INFLUENCE = 0x01000000, + OFFLINE_TICK = 0x02000000, + AURA = 0x04000000, + HIDE_TIMER = 0x08000000, + ON_ZONE_PATHOS = 0x10000000, + ALWAYS_EXPIRING = 0x20000000, + EXP_FROM_ACTUAL_LEVEL = 0x80000000, } diff --git a/scripts/enum/item.lua b/scripts/enum/item.lua index 7b3a4eb29b1..2b6cfb12554 100644 --- a/scripts/enum/item.lua +++ b/scripts/enum/item.lua @@ -2571,7 +2571,13 @@ xi.item = MEIFU_GOMA = 4185, AIRBORNE = 4186, BASTOK_MINES_GLYPH = 4187, + BASTOK_MARKETS_GLYPH = 4188, + PORT_BASTOK_GLYPH = 4189, EAST_SANDORIA_GLYPH = 4190, + WEST_SANDORIA_GLYPH = 4191, + NORTH_SANDORIA_GLYPH = 4192, + WINDURST_WATERS_GLYPH = 4193, + PORT_WINDURST_GLYPH = 4194, WINDURST_WOODS_GLYPH = 4195, ROTTEN_QUIVER = 4196, RUSTY_BOLT_CASE = 4197, diff --git a/scripts/globals/conquest.lua b/scripts/globals/conquest.lua index 2777be5cd03..942195bba51 100644 --- a/scripts/globals/conquest.lua +++ b/scripts/globals/conquest.lua @@ -6,6 +6,7 @@ require('scripts/globals/garrison') require('scripts/globals/teleports') require('scripts/globals/missions') require('scripts/globals/npc_util') +require('scripts/globals/expeditionary_force') ----------------------------------- xi = xi or {} xi.conquest = xi.conquest or {} @@ -22,33 +23,316 @@ local conquestConstants = } ----------------------------------- --- (LOCAL) expeditionary forces --- TODO: implement this menu +-- (LOCAL) Signet ----------------------------------- ---[[ + +-- Bestow the nation's Signet. +local function bestowSignet(player, pNation, pRank, mOffset) + local duration = (pRank + GetNationRank(pNation) + 3) * 3600 + player:delStatusEffectsByFlag(xi.effectFlag.INFLUENCE, true) + player:addStatusEffect(xi.effect.SIGNET, { duration = duration, origin = player }) + player:messageSpecial(mOffset + 1) -- 'You've received your nation's Signet!' + + if player:getEminenceProgress(3367) then + xi.roe.onRecordTrigger(player, 3367) -- Complete Weekly Signet, brb objective. This might be able to move to a status effect trigger + end +end + +----------------------------------- +-- (LOCAL) Expeditionary Forces +----------------------------------- + +-- Keep in this order as it is necessary to mimic retail during the removal of the key items. local exForceMenuData = { - 0x20006, ZULK_EF, 103, 0x000040, 20, xi.ki.ZULKHEIM_EF_INSIGNIA, - 0x20007, NORV_EF, 104, 0x000080, 25, xi.ki.NORVALLEN_EF_INSIGNIA, - 0x20009, DERF_EF, 109, 0x000200, 25, xi.ki.DERFLAND_EF_INSIGNIA, - 0x2000B, KOLS_EF, 118, 0x000800, 20, xi.ki.KOLSHUSHU_EF_INSIGNIA, - 0x2000C, ARAG_EF, 119, 0x001000, 25, xi.ki.ARAGONEU_EF_INSIGNIA, - 0x2000D, FAUR_EF, 111, 0x002000, 35, xi.ki.FAUREGANDI_EF_INSIGNIA, - 0x2000E, VALD_EF, 112, 0x004000, 40, xi.ki.VALDEAUNIA_EF_INSIGNIA, - 0x2000F, QUFI_EF, 126, 0x008000, 25, xi.ki.QUFIM_EF_INSIGNIA, - 0x20010, LITE_EF, 121, 0x010000, 35, xi.ki.LITELOR_EF_INSIGNIA, - 0x20011, KUZO_EF, 114, 0x020000, 40, xi.ki.KUZOTZ_EF_INSIGNIA, - 0x20012, VOLL_EF, 113, 0x040000, 65, xi.ki.VOLLBOW_EF_INSIGNIA, - 0x20013, ELLO_EF, 123, 0x080000, 35, xi.ki.ELSHIMO_LOWLANDS_EF_INSIGNIA, - 0x20014, ELUP_EF, 124, 0x100000, 45, xi.ki.ELSHIMO_UPLANDS_EF_INSIGNIA + [xi.region.ZULKHEIM] = { option = 0x20006, zone = xi.zone.VALKURM_DUNES, menuBit = 0x000040, lvl = 20, ki = xi.ki.ZULKHEIM_EF_INSIGNIA }, + [xi.region.NORVALLEN] = { option = 0x20007, zone = xi.zone.JUGNER_FOREST, menuBit = 0x000080, lvl = 25, ki = xi.ki.NORVALLEN_EF_INSIGNIA }, + [xi.region.DERFLAND] = { option = 0x20009, zone = xi.zone.PASHHOW_MARSHLANDS, menuBit = 0x000200, lvl = 25, ki = xi.ki.DERFLAND_EF_INSIGNIA }, + [xi.region.KOLSHUSHU] = { option = 0x2000B, zone = xi.zone.BUBURIMU_PENINSULA, menuBit = 0x000800, lvl = 20, ki = xi.ki.KOLSHUSHU_EF_INSIGNIA }, + [xi.region.ARAGONEU] = { option = 0x2000C, zone = xi.zone.MERIPHATAUD_MOUNTAINS, menuBit = 0x001000, lvl = 25, ki = xi.ki.ARAGONEU_EF_INSIGNIA }, + [xi.region.FAUREGANDI] = { option = 0x2000D, zone = xi.zone.BEAUCEDINE_GLACIER, menuBit = 0x002000, lvl = 35, ki = xi.ki.FAUREGANDI_EF_INSIGNIA }, + [xi.region.VALDEAUNIA] = { option = 0x2000E, zone = xi.zone.XARCABARD, menuBit = 0x004000, lvl = 40, ki = xi.ki.VALDEAUNIA_EF_INSIGNIA }, + [xi.region.QUFIMISLAND] = { option = 0x2000F, zone = xi.zone.QUFIM_ISLAND, menuBit = 0x008000, lvl = 25, ki = xi.ki.QUFIM_EF_INSIGNIA }, + [xi.region.LITELOR] = { option = 0x20010, zone = xi.zone.THE_SANCTUARY_OF_ZITAH, menuBit = 0x010000, lvl = 35, ki = xi.ki.LITELOR_EF_INSIGNIA }, + [xi.region.KUZOTZ] = { option = 0x20011, zone = xi.zone.EASTERN_ALTEPA_DESERT, menuBit = 0x020000, lvl = 40, ki = xi.ki.KUZOTZ_EF_INSIGNIA }, + [xi.region.VOLLBOW] = { option = 0x20012, zone = xi.zone.CAPE_TERIGGAN, menuBit = 0x040000, lvl = 65, ki = xi.ki.VOLLBOW_EF_INSIGNIA }, + [xi.region.ELSHIMO_LOWLANDS] = { option = 0x20013, zone = xi.zone.YUHTUNGA_JUNGLE, menuBit = 0x080000, lvl = 35, ki = xi.ki.ELSHIMO_LOWLANDS_EF_INSIGNIA }, + [xi.region.ELSHIMO_UPLANDS] = { option = 0x20014, zone = xi.zone.YHOATOR_JUNGLE, menuBit = 0x100000, lvl = 45, ki = xi.ki.ELSHIMO_UPLANDS_EF_INSIGNIA }, +} + +local exForceGateGlyphTable = +{ + -- [overseerNpcName] = glyphItemId + ['Crying_Wind_IM'] = xi.item.BASTOK_MINES_GLYPH, + ['Rabid_Wolf_IM'] = xi.item.BASTOK_MARKETS_GLYPH, + ['Flying_Axe_IM'] = xi.item.PORT_BASTOK_GLYPH, + ['Achantere_TK'] = xi.item.NORTH_SANDORIA_GLYPH, + ['Aravoge_TK'] = xi.item.WEST_SANDORIA_GLYPH, + ['Arpevion_TK'] = xi.item.EAST_SANDORIA_GLYPH, + ['Harara_WW'] = xi.item.WINDURST_WOODS_GLYPH, + ['Milma-Hapilma_WW'] = xi.item.PORT_WINDURST_GLYPH, + ['Puroiko-Maiko_WW'] = xi.item.WINDURST_WATERS_GLYPH, +} + +local exForceCityGlyphTable = +{ + -- [nation] = that nation's three glyphs + [xi.nation.SANDORIA] = { xi.item.NORTH_SANDORIA_GLYPH, xi.item.WEST_SANDORIA_GLYPH, xi.item.EAST_SANDORIA_GLYPH }, + [xi.nation.BASTOK] = { xi.item.BASTOK_MINES_GLYPH, xi.item.BASTOK_MARKETS_GLYPH, xi.item.PORT_BASTOK_GLYPH }, + [xi.nation.WINDURST] = { xi.item.WINDURST_WOODS_GLYPH, xi.item.PORT_WINDURST_GLYPH, xi.item.WINDURST_WATERS_GLYPH }, +} + +local exForceNumberRequiredTable = +{ + -- [Standing] = partySize + [0] = 4, -- No standing + [1] = 6, + [2] = 5, + [3] = 4, +} + +-- CP awarded on collection, by count of participated regions the nation controls. +-- When looking at the wiki, the data appears to follow a cubic. Extrapolating that would put 13 to be 27,175 CP. This seems unrealistic. +-- Instead the data from https://ffxiclopedia.fandom.com/wiki/Talk:Expeditionary_Force is used as it is more conservative. +local exForceCPRewardTable = +{ + -- [regionsControlled] = cp + [0] = 0, + [1] = 3000, -- Verified in capture + [2] = 4200, -- Verified in capture + [3] = 4680, -- +480 per region + [4] = 5160, + [5] = 5640, + [6] = 6120, + [7] = 6600, + [8] = 7080, + [9] = 7560, + [10] = 8040, + [11] = 8520, + [12] = 9000, + [13] = 9480, } -]]-- -local function getExForceAvailable(player, guardNation) + +-- Helper to parse region out of the exForceMenu data +local function getExForceRegion(option) + for regionId, data in pairs(exForceMenuData) do + if data.option == option then + return regionId + end + end +end + +-- Validate the sign-up party. +-- Returns the overseer result code for the first failed check (1-4), or nil if every check passes. +local function exForceValidateSignup(player, guardNation, minLevel, numRequired) + -- Get all party members currently in zone for the check + local zoneId = player:getZoneID() + local inZone = {} + for _, member in pairs(player:getParty()) do + if member:getZoneID() == zoneId then + table.insert(inZone, member) + end + end + + -- 1: not enough members present in the overseer's zone + if #inZone < numRequired then + return 1 + end + + -- 2: a party member is not a citizen of the overseer's nation + for _, member in ipairs(inZone) do + if member:getNation() ~= guardNation then + return 2 + end + end + + -- 3: a member is below the Conquest rank requirement + for _, member in ipairs(inZone) do + if member:getRank(guardNation) < 3 then + return 3 + end + end + + -- 4: a member is below the region's minimum level + for _, member in ipairs(inZone) do + if member:getMainLvl() < minLevel then + return 4 + end + end + return 0 end +-- Bitmask of every region this player can sign up for. 0 = the EF menu does not appear. +local function getExForceAvailable(player, npc, guardNation) + -- TODO: Delete this check when feature is fully implemented + if not xi.expeditionaryForce.enabled then + return 0 + end + + -- Only one of the nine gate guards can trigger this + if + exForceGateGlyphTable[npc:getName()] == nil or + player:getNation() ~= guardNation + then + return 0 + end + + local mask = 0 + + -- Setup the bit mask for all the available regions + -- The bit masks are stored in the exForceMenuData. + for regionId, data in pairs(exForceMenuData) do + local owner = GetRegionOwner(regionId) + + -- Region is available if: + -- The player's nation does not own the region. + -- The player's ally does not own the region. + -- The player has visited the region's outpost. + if + owner ~= guardNation and + not xi.conquest.areAllies(guardNation, owner) and + player:hasVisitedZone(data.zone) and + xi.expeditionaryForce.enabledTable[data.zone] -- TODO: Remove this after all zones have been implemented + then + mask = bit.bor(mask, data.menuBit) + end + end + + return mask +end + +-- This is to display the warp information after getting the badge, quitting EF, or giving the CP reward. local function getExForceReward(player, guardNation) - return 0 + -- This is to catch instances where the player gets the badge and goes to a guard at another nation's embassy. + if player:getNation() ~= guardNation then + return 0 + end + + -- Only show menu if the player has the EF badge + local badge = player:getStatusEffect(xi.effect.EF_BADGE) + if badge == nil then + -- A paid reward is stashed and waiting + if player:getCharVar('[ExpForce]AwardCP') > 0 then + return 0x400 + end + + return 0 + end + + local region = exForceMenuData[badge:getPower()] + -- This is a guard. The starter regions exist in the client but were removed from Retail. + if region == nil then + return 0 + end + + -- regionRow 6-20 = eventOption (0x20006-0x20014) minus the 0x20000 base. + local regionRow = region.option - 0x20000 + return 0x80000000 + bit.lshift(regionRow, 5) +end + +-- Check if we are in the next tally and remove insignia and calculate CP reward +local function collectExForceInsignia(player) + local stamp = player:getCharVar('[ExpForce]NextConquestTally') + if stamp == 0 or stamp >= NextConquestTally() then + return + end + + -- Dispose of every expired insignia on the way in (paid and unpaid both do this). + local mOffset = zones[player:getZoneID()].text.CONQUEST + for _, data in pairs(exForceMenuData) do + if player:hasKeyItem(data.ki) then + player:messageSpecial(mOffset + 121, data.ki) + player:delKeyItem(data.ki) + end + end + + -- Calculate the CP reward + local participation = player:getCharVar('[ExpForce]Participation') + local controlled = 0 + + for regionId in pairs(exForceMenuData) do + if + bit.band(participation, bit.lshift(1, regionId)) ~= 0 and + GetRegionOwner(regionId) == player:getNation() + then + controlled = controlled + 1 + end + end + + player:setCharVar('[ExpForce]AwardCP', exForceCPRewardTable[controlled]) + player:setCharVar('[ExpForce]Participation', 0) + player:setCharVar('[ExpForce]NextConquestTally', 0) +end + +-- Handle the Expeditionary Force options for the overseer menu. +-- Returns true when the option was an EF one so the caller can stop there. +local function exForceOnEventFinish(player, option, pNation, pRank, mOffset) + -- EXPEDITIONARY FORCE - Region Selected + if + option >= 131078 and + option <= 131092 + then + local regionId = getExForceRegion(option) + player:delStatusEffect(xi.effect.EF_BADGE) -- We can get here if we already have the badge. No need for check as delStatusEffect covers it. + player:addStatusEffect(xi.effect.EF_BADGE, { power = regionId, origin = player, flag = xi.effectFlag.ON_ZONE }) + + -- EXPEDITIONARY FORCE - Teleport + -- Order is: Signet, Remove badge, Obtain KI, obtain Glyph. + elseif option == 5 then + local badge = player:getStatusEffect(xi.effect.EF_BADGE) + local regionId = badge:getPower() + local overseer = player:getEventTarget() + + -- Only stamp when starting a fresh batch in case of a tally that lands mid-menu. + if player:getCharVar('[ExpForce]NextConquestTally') == 0 then + -- Needed to know when the key item expires. Okay to overwrite as cleanup occurs when initiating conversation. + player:setCharVar('[ExpForce]NextConquestTally', NextConquestTally()) + end + + bestowSignet(player, pNation, pRank, mOffset) + + -- Replace badge with key item + player:delStatusEffect(xi.effect.EF_BADGE) + npcUtil.giveKeyItem(player, exForceMenuData[regionId].ki) + + -- If you have a glyph from the city, you cannot get a second one. + local cityGlyphs = exForceCityGlyphTable[pNation] + if + not player:hasItem(cityGlyphs[1]) and + not player:hasItem(cityGlyphs[2]) and + not player:hasItem(cityGlyphs[3]) + then + npcUtil.giveItem(player, exForceGateGlyphTable[overseer:getName()]) + end + + -- Outpost warp player + player:addStatusEffect(xi.effect.TELEPORT, { + power = xi.teleport.id.OUTPOST, + duration = 1, + origin = player, + icon = 0, + subPower = regionId, + }) + + -- EXPEDITIONARY FORCE - Grant CP + elseif option == 7 then + local cp = player:getCharVar('[ExpForce]AwardCP') + player:addCP(cp) + player:messageSpecial(mOffset + 124, 0, cp) -- "You received x conquest points!" + player:messageSpecial(mOffset + 122) -- "Your invalid insignias have been disposed of."" + player:setCharVar('[ExpForce]AwardCP', 0) + + -- EXPEDITIONARY FORCE - Quit + elseif option == 8 then + player:delStatusEffect(xi.effect.EF_BADGE) + + -- Not an EF option. Let the caller keep going. + else + return false + end + + return true end ----------------------------------- @@ -1183,6 +1467,15 @@ xi.conquest.overseerOnTrigger = function(player, npc, guardNation, guardType, gu return end + -- EXPEDITIONARY FORCE: Collect expired insignia and give awards + -- Any of the 3 gate guards or the embassy guards for the player's nation. + if + pNation == guardNation and + guardType <= xi.conquest.guard.FOREIGN + then + collectExForceInsignia(player) + end + -- SUPPLY RUNS if pNation == guardNation and @@ -1208,7 +1501,7 @@ xi.conquest.overseerOnTrigger = function(player, npc, guardNation, guardType, gu -- CITY AND FOREIGN OVERSEERS elseif guardType <= xi.conquest.guard.FOREIGN then local a1 = getArg1(player, guardNation, guardType) - local a2 = getExForceAvailable(player, guardNation) + local a2 = getExForceAvailable(player, npc, guardNation) local a3 = conquestRanking() local a4 = suppliesAvailableBitmask(player, guardNation) local a5 = player:getTeleport(guardNation) @@ -1238,7 +1531,30 @@ xi.conquest.overseerOnEventUpdate = function(player, csid, option, guardNation) local stock = getStock(player, guardNation, option) - if stock ~= nil then + -- EXPEDITIONARY FORCE - Region select + if + option >= 131078 and + option <= 131092 + then + local regionId = getExForceRegion(option) + local numberRequired = exForceNumberRequiredTable[GetNationRank(guardNation)] + local minLevel = exForceMenuData[regionId].lvl + local failCode = exForceValidateSignup(player, guardNation, minLevel, numberRequired) + + -- One or more party members are below the minimum required level + if failCode == 4 then + player:updateEvent(4, 0, 0, 0, 0, 0, minLevel) + + -- All other fail codes + elseif failCode ~= 0 then + player:updateEvent(failCode) + + -- Badge granted in overseerOnEventFinish + else + player:updateEvent(5) + end + + elseif stock ~= nil then local pRank = GetNationRank(pNation) local u1 = 2 -- default: player is correct job and level to equip item local u2 = 0 -- default: player has enough CP for item @@ -1349,16 +1665,13 @@ xi.conquest.overseerOnEventFinish = function(player, csid, option, guardNation, return end + if exForceOnEventFinish(player, option, pNation, pRank, mOffset) then + return + end + -- SIGNET if option == 1 then - local duration = (pRank + GetNationRank(pNation) + 3) * 3600 - player:delStatusEffectsByFlag(xi.effectFlag.INFLUENCE, true) - player:addStatusEffect(xi.effect.SIGNET, { duration = duration, origin = player }) - player:messageSpecial(mOffset + 1) -- 'You've received your nation's Signet!' - - if player:getEminenceProgress(3367) then - xi.roe.onRecordTrigger(player, 3367) -- Complete Weekly Signet, brb objective. This might be able to move to a status effect trigger - end + bestowSignet(player, pNation, pRank, mOffset) -- BEGIN SUPPLY RUN elseif diff --git a/scripts/globals/expeditionary_force.lua b/scripts/globals/expeditionary_force.lua new file mode 100644 index 00000000000..84e4bfb0117 --- /dev/null +++ b/scripts/globals/expeditionary_force.lua @@ -0,0 +1,796 @@ +----------------------------------- +-- Expeditionary Force +----------------------------------- +require('scripts/globals/npc_util') +----------------------------------- +xi = xi or {} +xi.expeditionaryForce = xi.expeditionaryForce or {} + +----------------------------------- +-- Feature Enable +----------------------------------- +-- This is temporary and only used to slowly enable zones as data is filled in. +-- This is accessed only by conquest.lua +-- TODO: Remove these checks after all zones have been implemented + +xi.expeditionaryForce.enabled = true + +xi.expeditionaryForce.enabledTable = +{ + [xi.zone.BEAUCEDINE_GLACIER] = false, + [xi.zone.BUBURIMU_PENINSULA] = true, + [xi.zone.CAPE_TERIGGAN] = false, + [xi.zone.EASTERN_ALTEPA_DESERT] = false, + [xi.zone.JUGNER_FOREST] = false, + [xi.zone.MERIPHATAUD_MOUNTAINS] = false, + [xi.zone.PASHHOW_MARSHLANDS] = false, + [xi.zone.QUFIM_ISLAND] = false, + [xi.zone.THE_SANCTUARY_OF_ZITAH] = false, + [xi.zone.VALKURM_DUNES] = false, + [xi.zone.XARCABARD] = false, + [xi.zone.YHOATOR_JUNGLE] = false, + [xi.zone.YUHTUNGA_JUNGLE] = false, +} + +----------------------------------- +-- Retail differences and unknowns +----------------------------------- +-- Was unable to verify behavrior with two groups from different nations in the same zone so relied on Wiki. +-- Mobs in older zones spawn at a random 0 - 360 degrees from the banner. Newer zones use 0 - 180 degrees equally spaced. I used 0 - 360 degrees for all zones. +-- Was unable to verify if giving a title had a distance restriction. Set this to a standard exp distance. +-- Was unable to verify the conditionas that participation recording occured. +-- For treasure opening, was unable to verify if non-treasure (like maps or quest items) also awarded influence. Opted to not include those to be conservative. +-- CP rewards in conquest.lua were verified for 0 - 2 regions, but external references were used for 3 - 13 regions. +-- Glyph positions for Bastok have been confirmed. The other 6 glyphs were estimated at 3.5 yalms in front of the gate guards. +-- The order of how text appears in the log for treasure opening is technically wrong. The influence message should appear after the loot message. +-- Mobs do not use confrontation on retail. The way it works is only level synched players can engage the mobs. Mobs can engage anyone. Other mobs can engage level synched players. +-- Avatar levels were not verified on retail, so they were set to pet levels. + +----------------------------------- +-- Data +----------------------------------- +-- Runtime state. One record per EF zone, built lazily on zone initialize. +-- If you modify this lua file during runtime, you must relaunch map as expForceZoneData is erased. +local expForceZoneData = {} + +----------------------------------- +-- Enums +----------------------------------- +local bannerState = +{ + IDLE = 0, + ACTIVE = 1, + CLEARED = 2, + HIDDEN = 3, +} + +local mobFamilies = +{ + DEMISAHAGIN = 0, + GIGAS = 1, + HALFORC = 2, + HOBGOBLIN = 3, + METAQUADAV = 4, + NOCTONBERRY = 5, + THEOYAGUDO = 6, + CONTANTICAN = 7, +} + +----------------------------------- +-- Tables +----------------------------------- + +local levelTable = +{ + -- [zoneId] = level_cap + [xi.zone.BEAUCEDINE_GLACIER] = 40, + [xi.zone.BUBURIMU_PENINSULA] = 30, + [xi.zone.CAPE_TERIGGAN] = 99, -- Uncapped + [xi.zone.EASTERN_ALTEPA_DESERT] = 50, + [xi.zone.JUGNER_FOREST] = 30, + [xi.zone.MERIPHATAUD_MOUNTAINS] = 30, + [xi.zone.PASHHOW_MARSHLANDS] = 30, + [xi.zone.QUFIM_ISLAND] = 30, + [xi.zone.THE_SANCTUARY_OF_ZITAH] = 40, + [xi.zone.VALKURM_DUNES] = 30, + [xi.zone.XARCABARD] = 50, + [xi.zone.YHOATOR_JUNGLE] = 50, + [xi.zone.YUHTUNGA_JUNGLE] = 40, +} + +local bannerTable = +{ + [xi.zone.BEAUCEDINE_GLACIER] = + { + -- position = { x, y, z, rot } + { position = { 193.614, -0.307, -35.663, 255 }, mobFamily = mobFamilies.GIGAS }, -- I-8 + { position = { 20.169, -80.061, 180.063, 224 }, mobFamily = mobFamilies.GIGAS }, -- H-7 + { position = { -326.264, -99.694, 140.523, 220 }, mobFamily = mobFamilies.GIGAS }, -- F-7 + { position = { 255.402, 0.072, 382.940, 110 }, mobFamily = mobFamilies.HOBGOBLIN }, -- J-6 + { position = { -173.299, -81.847, 150.200, 246 }, mobFamily = mobFamilies.HOBGOBLIN }, -- G-7 + }, + + [xi.zone.BUBURIMU_PENINSULA] = + { + { position = { 101.491, -23.090, 199.798, 218 }, mobFamily = mobFamilies.HOBGOBLIN }, + { position = { 527.885, 0.486, -40.241, 157 }, mobFamily = mobFamilies.HOBGOBLIN }, + { position = { 315.895, -0.025, 361.453, 17 }, mobFamily = mobFamilies.THEOYAGUDO }, + { position = { -132.589, 20.000, -314.261, 230 }, mobFamily = mobFamilies.THEOYAGUDO }, + { position = { -446.510, -8.799, -282.799, 240 }, mobFamily = mobFamilies.THEOYAGUDO }, + }, + + [xi.zone.CAPE_TERIGGAN] = + { + { position = { 126.583, -0.194, -117.367, 75 }, mobFamily = mobFamilies.HOBGOBLIN }, -- I-9 + { position = { -213.169, -3.320, 254.085, 181 }, mobFamily = mobFamilies.HOBGOBLIN }, -- G-6 + { position = { 251.977, 5.241, 50.698, 128 }, mobFamily = mobFamilies.HOBGOBLIN }, -- J-8 + { position = { -29.071, -9.694, 224.300, 46 }, mobFamily = mobFamilies.HOBGOBLIN }, -- H-7 + { position = { 162.059, -0.740, 250.538, 139 }, mobFamily = mobFamilies.HOBGOBLIN }, -- I-6 + }, + + [xi.zone.EASTERN_ALTEPA_DESERT] = + { + { position = { -63.319, -10.629, 408.180, 77 }, mobFamily = mobFamilies.CONTANTICAN }, -- G-5 + { position = { 463.219, -10.608, 248.849, 212 }, mobFamily = mobFamilies.CONTANTICAN }, -- J-6 + { position = { 329.054, 6.684, -330.958, 201 }, mobFamily = mobFamilies.CONTANTICAN }, -- J-10 + { position = { -332.218, -1.203, 126.229, 60 }, mobFamily = mobFamilies.HOBGOBLIN }, -- E-7 + { position = { 27.934, -10.019, 398.640, 126 }, mobFamily = mobFamilies.HOBGOBLIN }, -- H-6 + + }, + + [xi.zone.JUGNER_FOREST] = + { + { position = { 279.408, -15.592, -547.181, 176 }, mobFamily = mobFamilies.HALFORC }, -- J-11 + { position = { -159.588, 0.647, 386.042, 17 }, mobFamily = mobFamilies.HALFORC }, -- G-6 + { position = { 3.419, -16.000, -642.232, 7 }, mobFamily = mobFamilies.HALFORC }, -- H-12 + { position = { 448.240, 0.212, -157.228, 225 }, mobFamily = mobFamilies.HOBGOBLIN }, -- K-9 + { position = { 600.809, 0.873, 217.453, 130 }, mobFamily = mobFamilies.HOBGOBLIN }, -- L-7 + }, + + [xi.zone.MERIPHATAUD_MOUNTAINS] = + { + { position = { 199.396, -0.723, -527.072, 169 }, mobFamily = mobFamilies.HOBGOBLIN }, -- H-11 + { position = { 342.918, -1.109, 529.219, 226 }, mobFamily = mobFamilies.HOBGOBLIN }, -- I-5 + { position = { 592.850, -16.741, -518.802, 227 }, mobFamily = mobFamilies.THEOYAGUDO }, -- K-11 + { position = { -536.930, 4.317, 338.845, 200 }, mobFamily = mobFamilies.THEOYAGUDO }, -- D-6 + { position = { -559.025, -16.761, 47.233, 72 }, mobFamily = mobFamilies.THEOYAGUDO }, -- D-8 + }, + + [xi.zone.PASHHOW_MARSHLANDS] = + { + { position = { -172.764, 25.125, 93.640, 154 }, mobFamily = mobFamilies.HOBGOBLIN }, -- G-8 + { position = { 261.910, 24.213, 211.070, 85 }, mobFamily = mobFamilies.HOBGOBLIN }, -- J-7 + { position = { 140.080, 23.971, -411.951, 112 }, mobFamily = mobFamilies.METAQUADAV }, -- I-11 + { position = { -447.851, 24.305, -219.899, 113 }, mobFamily = mobFamilies.METAQUADAV }, -- E-10 + { position = { -460.959, 24.203, 469.851, 223 }, mobFamily = mobFamilies.METAQUADAV }, -- E-5 + }, + + [xi.zone.QUFIM_ISLAND] = + { + {}, + {}, + {}, + {}, + {}, + }, + + [xi.zone.THE_SANCTUARY_OF_ZITAH] = + { + { position = { 643.619, 0.842, -176.843, 128 }, mobFamily = mobFamilies.HOBGOBLIN }, -- L-10 + { position = { 174.336, -1.015, -413.606, 59 }, mobFamily = mobFamilies.HOBGOBLIN }, -- I-11 + { position = { -512.058, -0.975, 253.275, 37 }, mobFamily = mobFamilies.HOBGOBLIN }, -- E-7 + { position = { 429.298, 0.084, -604.489, 231 }, mobFamily = mobFamilies.HOBGOBLIN }, -- J-12 + { position = { -399.822, 0.162, -168.998, 174 }, mobFamily = mobFamilies.HOBGOBLIN }, -- E-10 + }, + + [xi.zone.VALKURM_DUNES] = + { + { position = { -522.404, -8.175, 113.667, 141 }, mobFamily = mobFamilies.HALFORC }, + { position = { 643.175, -0.592, 8.854, 10 }, mobFamily = mobFamilies.HALFORC }, + { position = { 478.713, -16.140, 365.873, 28 }, mobFamily = mobFamilies.HOBGOBLIN }, -- J-6 + { position = { -352.679, -8.856, 327.661, 18 }, mobFamily = mobFamilies.METAQUADAV }, + { position = { -116.204, 4.000, -113.608, 160 }, mobFamily = mobFamilies.METAQUADAV }, + }, + + [xi.zone.XARCABARD] = + { + { position = { 32.788, -24.162, -205.200, 6 }, mobFamily = mobFamilies.GIGAS }, -- G-9 + { position = { -160.590, -24.169, -87.061, 174 }, mobFamily = mobFamilies.GIGAS }, -- F-8 + { position = { 153.000, -36.438, 23.500, 16 }, mobFamily = mobFamilies.GIGAS }, -- H-7 + { position = { 47.461, -36.500, 66.281, 201 }, mobFamily = mobFamilies.HOBGOBLIN }, -- G-7 + { position = { 320.399, -8.190, 167.796, 52 }, mobFamily = mobFamilies.HOBGOBLIN }, -- I-6 + }, + + [xi.zone.YHOATOR_JUNGLE] = + { + { position = { -54.134, 0.344, -405.397, 199 }, mobFamily = mobFamilies.HOBGOBLIN }, -- H-10 + { position = { -196.704, 0.000, -149.953, 75 }, mobFamily = mobFamilies.HOBGOBLIN }, -- G-9 + { position = { -289.835, 0.000, -357.025, 5 }, mobFamily = mobFamilies.NOCTONBERRY }, -- F-10 + { position = { 366.014, -0.176, -394.801, 96 }, mobFamily = mobFamilies.NOCTONBERRY }, -- J-10 + { position = { -176.760, 0.162, 26.774, 40 }, mobFamily = mobFamilies.NOCTONBERRY }, -- G-8 + }, + + [xi.zone.YUHTUNGA_JUNGLE] = + { + { position = { -63.927, -0.042, -126.052, 153 }, mobFamily = mobFamilies.DEMISAHAGIN }, -- H-9 + { position = { 102.301, 0.600, 442.978, 17 }, mobFamily = mobFamilies.DEMISAHAGIN }, -- I-6 + { position = { -305.061, 16.186, -438.904, 132 }, mobFamily = mobFamilies.DEMISAHAGIN }, -- G-11 + { position = { 381.229, 3.908, 148.721, 115 }, mobFamily = mobFamilies.HOBGOBLIN }, -- K-8 + { position = { -647.367, 0.000, 42.053, 28 }, mobFamily = mobFamilies.HOBGOBLIN }, -- E-8 + }, +} + +-- Fill out by mob species. +local nmPoolTable = +{ + [xi.zone.BEAUCEDINE_GLACIER] = + { + + }, + + [xi.zone.BUBURIMU_PENINSULA] = + { + [mobFamilies.HOBGOBLIN] = + { + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_BEASTMASTER, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_BLACK_MAGE, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_DARK_KNIGHT, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_RANGER, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_RED_MAGE, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_THIEF, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_WARRIOR, + zones[xi.zone.BUBURIMU_PENINSULA].mob.HOBGOBLIN_WHITE_MAGE, + }, + + [mobFamilies.THEOYAGUDO] = + { + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_BARD, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_BLACK_MAGE, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_MONK, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_NINJA, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_SAMURAI, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_SUMMONER, + zones[xi.zone.BUBURIMU_PENINSULA].mob.THEOYAGUDO_WHITE_MAGE, + }, + }, + + [xi.zone.CAPE_TERIGGAN] = + { + + }, + + [xi.zone.EASTERN_ALTEPA_DESERT] = + { + + }, + + [xi.zone.JUGNER_FOREST] = + { + + }, + + [xi.zone.MERIPHATAUD_MOUNTAINS] = + { + + }, + + [xi.zone.PASHHOW_MARSHLANDS] = + { + + }, + + [xi.zone.QUFIM_ISLAND] = + { + + }, + + [xi.zone.THE_SANCTUARY_OF_ZITAH] = + { + + }, + + [xi.zone.VALKURM_DUNES] = + { + + }, + + [xi.zone.XARCABARD] = + { + + }, + + [xi.zone.YHOATOR_JUNGLE] = + { + + }, + + [xi.zone.YUHTUNGA_JUNGLE] = + { + + }, +} + +local regionKITable = +{ + [xi.region.ARAGONEU] = xi.ki.ARAGONEU_EF_INSIGNIA, + [xi.region.DERFLAND] = xi.ki.DERFLAND_EF_INSIGNIA, + [xi.region.ELSHIMO_LOWLANDS] = xi.ki.ELSHIMO_LOWLANDS_EF_INSIGNIA, + [xi.region.ELSHIMO_UPLANDS] = xi.ki.ELSHIMO_UPLANDS_EF_INSIGNIA, + [xi.region.FAUREGANDI] = xi.ki.FAUREGANDI_EF_INSIGNIA, + [xi.region.KOLSHUSHU] = xi.ki.KOLSHUSHU_EF_INSIGNIA, + [xi.region.KUZOTZ] = xi.ki.KUZOTZ_EF_INSIGNIA, + [xi.region.LITELOR] = xi.ki.LITELOR_EF_INSIGNIA, + [xi.region.NORVALLEN] = xi.ki.NORVALLEN_EF_INSIGNIA, + [xi.region.QUFIMISLAND] = xi.ki.QUFIM_EF_INSIGNIA, + [xi.region.VALDEAUNIA] = xi.ki.VALDEAUNIA_EF_INSIGNIA, + [xi.region.VOLLBOW] = xi.ki.VOLLBOW_EF_INSIGNIA, + [xi.region.ZULKHEIM] = xi.ki.ZULKHEIM_EF_INSIGNIA, +} + +----------------------------------- +-- Local functions +----------------------------------- + +-- Apply the EF level restriction to one player. +-- ON_ZONE makes it wear when the player zones out. +-- CONFRONTATION hard-gates the NMs to capped players. +-- EXP_FROM_ACTUAL_LEVEL makes it so xp rate is applied to your actual level, not your restricted level. +local function addLevelRestriction(player, levelCap) + local cap = levelCap + if levelCap == 99 then + cap = xi.settings.main.MAX_LEVEL + end + + player:addStatusEffect(xi.effect.LEVEL_RESTRICTION, { + power = cap, + duration = 900, -- 15 min if not removed at the banner or zone + origin = player, + flag = xi.effectFlag.ON_ZONE + xi.effectFlag.CONFRONTATION + xi.effectFlag.EXP_FROM_ACTUAL_LEVEL, + }) +end + +-- Add the CONFRONTATION to the NMs. The level restriction already won't apply to mobs. I just need the CONFRONTATION flag and matching power. +local function addConfrontationGate(mob, levelCap) + local cap = levelCap + if levelCap == 99 then + cap = xi.settings.main.MAX_LEVEL + end + + mob:addStatusEffect(xi.effect.LEVEL_RESTRICTION, { + power = cap, + origin = mob, + flag = xi.effectFlag.CONFRONTATION, + }) +end + +-- Spawn 4 NMs at the banner +local function spawnBattleNMs(player, banner, zoneData) + local zoneId = banner:getZoneID() + local levelCap = levelTable[zoneId] + local bannerPool = bannerTable[zoneId] + local bannerInfo = bannerPool[zoneData.bannerIndex] + local mobFamily = bannerInfo.mobFamily + local zoneNMPool = nmPoolTable[zoneId] + local nmPool = zoneNMPool[mobFamily] + + local bannerPosition = bannerInfo.position + local bx, by, bz = bannerPosition[1], bannerPosition[2], bannerPosition[3] + + -- Create a new table with the pool shuffled + local candidates = utils.shuffle(nmPool) + + for i = 1, 4 do + local mobId = candidates[i] + -- Catch case when pool has fewer than 4 mobs + if mobId == nil then + break + end + + -- Spawn is a normal distribution with a mean of 3.5 and standard deviation of 1.5. + -- The spawn distance is also restricted to [2.0, 7.5]. This is based on 234 samples. + local distance = utils.randomNormal(3.5, 1.5, 2.0, 7.5) + + -- Scatter around the banner in random direction. + local angle = math.random() * 2 * math.pi -- 0 to 360 degrees + local pos = GetFurthestValidPosition(banner, distance, angle) -- Drops mob on valid ground and snaps closer if terrain blocks the distance. + + local mob = GetMobByID(mobId) + if mob == nil then + break + end + + if pos ~= nil then + mob:setSpawn(pos.x, pos.y, pos.z, 0) + + -- Account for weird situation where the GetFurthestValidPosition can't find any position due to navmesh + else + mob:setSpawn(bx, by, bz, 0) + end + + mob:spawn() + mob:lookAt(player:getPos()) -- face whoever triggered the banner + addConfrontationGate(mob, levelCap) + mob:updateClaim(player) + table.insert(zoneData.nms, mobId) + + zoneData.numAlive = zoneData.numAlive + 1 + end +end + +-- CLEARED -> HIDDEN. This is called from a 60-second timer callback. +local function hideBanner(zoneId, banner) + local zoneData = expForceZoneData[zoneId] + + -- Make the banner disappear + banner:setStatus(xi.status.DISAPPEAR) + + -- Clean up data and set HIDDEN state + zoneData.nms = {} + zoneData.gone = {} + zoneData.numAlive = 0 + zoneData.creditNation = nil + zoneData.state = bannerState.HIDDEN + + -- Respawn the banner in 5 minutes + banner:timer(5 * 60 * 1000, function(npcArg) + xi.expeditionaryForce.initZone(npcArg:getZone()) + end) +end + +-- Safety check every 30s while banner is active. Catches the case where a DESPAWN listener misses. +local function watchDog(npc) + local zoneId = npc:getZoneID() + local zoneData = expForceZoneData[zoneId] + + -- Only continue running if the state is ACTIVE + if zoneData.state ~= bannerState.ACTIVE then + return + end + + -- Any NM still in the world (alive or corpse)? + local anyPresent = false + for _, mobId in ipairs(zoneData.nms) do + local mob = GetMobByID(mobId) + if mob ~= nil and mob:isSpawned() then + anyPresent = true + break + end + end + + -- None are present but we never ended + if not anyPresent then + zoneData.state = bannerState.CLEARED + + -- The banner will disappear after 60 seconds. + npc:timer(60 * 1000, function(npcArg) + hideBanner(npcArg:getZoneID(), npcArg) + end) + + -- Check again in 60 seconds + else + npc:timer(60 * 1000, function(npcArg) + watchDog(npcArg) + end) + end +end + +-- Log EF participation for a region by setting its bit. +local function recordParticipation(player, regionId) + local participation = player:getCharVar('[ExpForce]Participation') + player:setCharVar('[ExpForce]Participation', bit.bor(participation, bit.lshift(1, regionId))) +end + +-- Mark a mob as gone. When all NMs are accounted for, transition to CLEARED. +-- This fires during onDeath and onDespawn as the flag needs to update on the death of the last mob or despawn of the last mob if the mob is not killed. +local function removeNMFromList(mob) + local zoneId = mob:getZoneID() + local zoneData = expForceZoneData[zoneId] + + local mobId = mob:getID() + + -- Check if mob is already in list + if not zoneData.gone[mobId] then + -- Add the mob to the gone list + zoneData.gone[mobId] = true + zoneData.numAlive = zoneData.numAlive - 1 + + -- Battle is cleared + if zoneData.numAlive <= 0 then + zoneData.state = bannerState.CLEARED + + -- The banner will disappear after 60 seconds. + local ID = zones[zoneId] + local banner = GetNPCByID(ID.npc.BEASTMENS_BANNER) + if banner == nil then + return + end + + banner:timer(60 * 1000, function(npcArg) + hideBanner(npcArg:getZoneID(), npcArg) + end) + end + end +end + +-- This checks if the expeditionary force is allowed to be spawned. +-- A player must have the KI and the region must not be owned by the player's nation or ally. +-- All checks are necessary just in case the player is holding the key item after tally update. +local function expForceAvailableToPlayer(player, region) + local ownerNation = GetRegionOwner(region) + local playerNation = player:getNation() + + return + player:hasKeyItem(regionKITable[region]) and + ownerNation ~= playerNation and + not xi.conquest.areAllies(playerNation, ownerNation) +end + +----------------------------------- +-- Public functions +----------------------------------- + +-- This code runs whenever the zone is initialized as well as every time the Expeditionary Force has been reset. +xi.expeditionaryForce.initZone = function(zone) + local zoneId = zone:getID() + local ID = zones[zoneId] + + -- Build the zone's runtime record + local zoneData = expForceZoneData[zoneId] + -- This branch is for when the zone is initialized + if zoneData == nil then + zoneData = + { + state = bannerState.IDLE, + nms = {}, + gone = {}, + numAlive = 0, + creditNation = nil, + bannerIndex = nil, + } + expForceZoneData[zoneId] = zoneData + + -- This branch runs when respawning a banner + else + zoneData.state = bannerState.IDLE + zoneData.nms = {} + zoneData.gone = {} + zoneData.numAlive = 0 + zoneData.creditNation = nil + end + + -- Set the banner to a random position and set the status to normal + local banner = GetNPCByID(ID.npc.BEASTMENS_BANNER) + local bannerOptions = bannerTable[zoneId] + local lastBannerIndex = zoneData.bannerIndex + local newBannerIndex + + if banner == nil then + return + end + + -- When the zone loads, there are no previous positions so we can pick from any of the available options. + if lastBannerIndex == nil then + newBannerIndex = math.random(#bannerOptions) + + -- We will just roll 1 less number. If we happen to land on the previous index, we just select the last value in the position table. + -- Note: If you only have one banner position, this will break! This should never happen as all zones have more than 1 banner position. + else + newBannerIndex = math.random(#bannerOptions - 1) + if newBannerIndex == lastBannerIndex then + newBannerIndex = #bannerOptions + end + end + + local pos = bannerOptions[newBannerIndex].position + banner:setPos(pos[1], pos[2], pos[3], pos[4]) + banner:setStatus(xi.status.NORMAL) -- forces visible + + -- Store the position index for later to make sure the banner does not spawn in the same place twice in a row + zoneData.bannerIndex = newBannerIndex +end + +-- Handle all states of the Beastmen's Banner +-- The flow of Expeditionary Force goes from IDLE to ACTIVE to CLEARED to HIDDEN then back to IDLE +xi.expeditionaryForce.onBannerTrigger = function(player, npc) + local zoneId = npc:getZoneID() + local ID = zones[zoneId] + local zoneData = expForceZoneData[zoneId] + + -- IDLE + if zoneData.state == bannerState.IDLE then + local region = npc:getCurrentRegion() + + -- Find out if an alliance member in the zone has level sync already. If so, they can not activate the flag. + local allianceMemberCapped = false + for _, member in pairs(player:getAlliance()) do + if + member:getZoneID() == zoneId and + member:hasStatusEffect(xi.effect.LEVEL_RESTRICTION) + then + allianceMemberCapped = true + break -- No need to keep checking + end + end + + -- Gate is if no alliance member is level synched, player has key item, nation does not hold the region, nation's ally does not hold the region. + if + expForceAvailableToPlayer(player, region) and + not allianceMemberCapped + then + -- Credit nation is based on the player who clicked the banner, not the player who killed the nm. + zoneData.creditNation = player:getNation() + + -- Level cap every alliance member in zone + -- Get members in-range + for _, member in pairs(player:getAlliance()) do + if member:getZoneID() == zoneId then + -- Add level restriction if in zone + addLevelRestriction(member, levelTable[zoneId]) + + -- Display banner message to all members who have been level restricted + member:messageSpecial(ID.text.BEASTMEN_BANNER_CURSE) -- There was a curse on the beastmen's banner! + end + end + + -- Spawn 4 NMs at the banner + spawnBattleNMs(player, npc, zoneData) + zoneData.state = bannerState.ACTIVE + + -- Launch Watch Dog function: this will catch if an NM's despawn doesn't trigger. + watchDog(npc) + + -- If the player is ineligable to initiate, just send a message. + else + player:messageSpecial(ID.text.BEASTMEN_BANNER) -- There is a beastmen's banner. + end + + -- ACTIVE: Mobs exist + elseif zoneData.state == bannerState.ACTIVE then + -- Remove level restriction if it exists. + if player:hasStatusEffect(xi.effect.LEVEL_RESTRICTION) then + player:delStatusEffect(xi.effect.LEVEL_RESTRICTION) + end + + -- Anyone not level restricted can click to get level restriction. No checks. + addLevelRestriction(player, levelTable[zoneId]) + + -- Display banner message + player:messageSpecial(ID.text.BEASTMEN_BANNER_CURSE) -- There was a curse on the beastmen's banner! + + -- CLEARED: All mobs despawned + elseif zoneData.state == bannerState.CLEARED then + -- Remove clicker's level cap + if player:hasStatusEffect(xi.effect.LEVEL_RESTRICTION) then + player:delStatusEffect(xi.effect.LEVEL_RESTRICTION) + player:messageSpecial(ID.text.BEASTMEN_BANNER_LIFTED) -- The curse of the beastmen's banner has been lifted! + + -- Default banner text. + else + player:messageSpecial(ID.text.BEASTMEN_BANNER) -- There is a beastmen's banner. + end + end + + -- HIDDEN: Banner is invisible and not clickable +end + +-- Called from mob lua files. Fires once per alliance member in zone. +xi.expeditionaryForce.onMobDeath = function(mob, player, optParams) + -- This should never happen as pets are attributed to owner, but just in case pet kills during player zone. + if not player then + return + end + + local zoneId = mob:getZoneID() + local ID = zones[zoneId] + local zoneData = expForceZoneData[zoneId] + local creditNation = zoneData.creditNation + + -- These occur once per kill. + if optParams.isKiller then + removeNMFromList(mob) + + -- AWARD INFLUENCE + AddConquestInfluence(xi.settings.main.EXP_FORCE_MOBKILL_INFLUENCE, creditNation, mob:getCurrentRegion()) + + -- SEND ZONE MESSAGE + for _, person in pairs(mob:getZone():getPlayers()) do + if creditNation == xi.nation.SANDORIA then + person:messageText(person, ID.text.EXP_FORCE_KILL_SANDORIA, 5) -- 5 = Grey: messageText event + + elseif creditNation == xi.nation.BASTOK then + person:messageText(person, ID.text.EXP_FORCE_KILL_BASTOK, 5) -- 5 = Grey: messageText event + + elseif creditNation == xi.nation.WINDURST then + person:messageText(person, ID.text.EXP_FORCE_KILL_WINDURST, 5) -- 5 = Grey: messageText event + end + end + end + + -- AWARD TITLE AND PARTICIPATION + -- You don't need to be participating in Expeditionary Force or even be the right level to get the title. + if player:checkDistance(mob) <= 50 then -- TODO: Verify that there is a distance based restriction. Set it to standard xp restriction. + -- Award all alliance members title + player:addTitle(xi.title.EXPEDITIONARY_TROOPER) + + -- Mark all alliance members participating in Expeditionary Force with participation + -- TODO: Verify what conditions require recording participation. + local regionId = mob:getCurrentRegion() + if + player:hasKeyItem(regionKITable[regionId]) and + player:hasStatusEffect(xi.effect.LEVEL_RESTRICTION) + then + recordParticipation(player, regionId) + end + end + + -- MESSAGE + -- "x's region points have increased" + if creditNation == xi.nation.SANDORIA then + player:messageSpecial(ID.text.REGION_POINTS_SANDORIA) -- showText event + + elseif creditNation == xi.nation.BASTOK then + player:messageSpecial(ID.text.REGION_POINTS_BASTOK) -- showText event + + elseif creditNation == xi.nation.WINDURST then + player:messageSpecial(ID.text.REGION_POINTS_WINDURST) -- showText event + end +end + +-- Fires on despawn. This is for if mobs despawn naturally without death. 3 minute depsawn timer. +xi.expeditionaryForce.onMobDespawn = function(mob) + removeNMFromList(mob) +end + +-- Award influence for opening a chest/coffer with the region's insignia. +-- Caller checks the insignia and resolves the regionId. +-- TODO: This is only wired to give influence when a non-quest item and non-map key item is obtained. Double check this. +xi.expeditionaryForce.onChestOpen = function(player) + local regionId = player:getCurrentRegion() + local insignia = regionKITable[regionId] + + -- Only give influence if this is a EF region, the player has the KI, and the player's nation or allied nation does not control the region. + if + insignia ~= nil and + expForceAvailableToPlayer(player, regionId) + then + + player:gainInfluencePoints(xi.settings.main.EXP_FORCE_TREASURE_INFLUENCE) + + -- Nation flavor text + local ID = zones[player:getZoneID()] + local playerNation = player:getNation() + + if playerNation == xi.nation.SANDORIA then + player:messageSpecial(ID.text.EXP_FORCE_CHEST_SANDORIA) + + elseif playerNation == xi.nation.BASTOK then + player:messageSpecial(ID.text.EXP_FORCE_CHEST_BASTOK) + + elseif playerNation == xi.nation.WINDURST then + player:messageSpecial(ID.text.EXP_FORCE_CHEST_WINDURST) + end + + recordParticipation(player, regionId) + end +end + +-- Dispose of every Expeditionary Force insignia the player is holding. +-- Called on a nation change, since insignias are tied to the player's old allegiance. +-- Returns true if at least one insignia was removed. +xi.expeditionaryForce.disposeInsigniaNationSwap = function(player) + local removed = false + + for _, ki in pairs(regionKITable) do + if player:hasKeyItem(ki) then + player:delKeyItem(ki) + removed = true + end + end + + return removed +end + +-- Pets called mid-fight (call beast, astral flow) miss the gate at spawn. Need to apply manually. +xi.expeditionaryForce.gatePet = function(mob) + addConfrontationGate(mob, levelTable[mob:getZoneID()]) +end diff --git a/scripts/globals/teleports.lua b/scripts/globals/teleports.lua index 5b40e913adf..cc65dbe2f4d 100644 --- a/scripts/globals/teleports.lua +++ b/scripts/globals/teleports.lua @@ -90,11 +90,17 @@ local ids = SAFEHOLD_EARRING = 79, NORG_EARRING = 80, NASHMAU_EARRING = 81, - EAST_SANDY_GLYPH = 82, - BASTOK_MINES_GLYPH = 83, - WINDY_WOODS_GLYPH = 84, - CUMULUS_MASQUE = 85, - WYRMKING_SUIT = 86, + BASTOK_MINES_GLYPH = 82, + BASTOK_MARKETS_GLYPH = 83, + PORT_BASTOK_GLYPH = 84, + EAST_SANDY_GLYPH = 85, + WEST_SANDY_GLYPH = 86, + NORTH_SANDY_GLYPH = 87, + WINDY_WATERS_GLYPH = 88, + PORT_WINDY_GLYPH = 89, + WINDY_WOODS_GLYPH = 90, + CUMULUS_MASQUE = 91, + WYRMKING_SUIT = 92, } xi.teleport.id = ids @@ -177,9 +183,15 @@ xi.teleport.destination = [ids.SAFEHOLD_EARRING] = { -7.737, -28.012, 111.883, 128, 26 }, -- Tavnazian Safehold [ids.NORG_EARRING] = { -24.375, 0.389, -48.209, 207, 252 }, -- Norg [ids.NASHMAU_EARRING] = { -7.710, 0.000, -43.301, 193, 53 }, -- Nashmau - [ids.EAST_SANDY_GLYPH] = { 101.292, 1.000, -48.889, 31, 230 }, -- Southern San d'Oria East Gate [ids.BASTOK_MINES_GLYPH] = { -2.658, -1.001, -120.508, 70, 234 }, -- Bastok Mines Gate - [ids.WINDY_WOODS_GLYPH] = { 108.726, -5.000, -43.588, 0, 241 }, -- Windurst Woods Gate + [ids.BASTOK_MARKETS_GLYPH] = { -344.331, -10.001, -181.252, 88, 235 }, -- Bastok Markets South Gate + [ids.PORT_BASTOK_GLYPH] = { 127.048, 8.500, -0.777, 223, 236 }, -- Port Bastok Gate + [ids.EAST_SANDY_GLYPH] = { 102.877, 0.000, -50.330, 35, 230 }, -- Southern San d'Oria East Gate -- TODO: Get retail coordinates. + [ids.WEST_SANDY_GLYPH] = { -102.522, 0.000, -50.585, 95, 230 }, -- Southern San d'Oria West Gate -- TODO: Get retail coordinates. + [ids.NORTH_SANDY_GLYPH] = { -243.710, 6.999, 41.196, 125, 231 }, -- Northern San d'Oria West Gate -- TODO: Get retail coordinates. + [ids.WINDY_WATERS_GLYPH] = { -30.192, -4.920, 223.038, 186, 238 }, -- Windurst Waters Gate -- TODO: Get retail coordinates. + [ids.PORT_WINDY_GLYPH] = { -223.678, -7.999, 209.021, 131, 240 }, -- Port Windurst Gate -- TODO: Get retail coordinates. + [ids.WINDY_WOODS_GLYPH] = { 103.500, -5.000, -48.693, 5, 241 }, -- Windurst Woods Gate -- TODO: Get retail coordinates. [ids.CUMULUS_MASQUE] = { 260.000, -87.000, 86.000, 192, 291 }, -- Reisenjima Place of Parting [ids.WYRMKING_SUIT] = { -506.157, -8.500, -384.025, 220, 30 }, -- Riverne Site #B-01 } diff --git a/scripts/globals/treasure.lua b/scripts/globals/treasure.lua index ead666e10cd..46edb20f66c 100644 --- a/scripts/globals/treasure.lua +++ b/scripts/globals/treasure.lua @@ -2004,6 +2004,9 @@ xi.treasure.onTrade = function(player, npc, trade, bypassType, bypassReward) reward = itemId end + -- Award Expeditionary Force credit when a chest or coffer gives loot/gil + xi.expeditionaryForce.onChestOpen(player) + -- Handle illusion timers. if containerType == treasureType.CHEST then npc:setLocalVar('illusionCooldown', GetSystemTime() + math.random(xi.settings.main.CHEST_MIN_ILLUSION_TIME, xi.settings.main.CHEST_MAX_ILLUSION_TIME)) diff --git a/scripts/items/bastok_markets_gate_glyph.lua b/scripts/items/bastok_markets_gate_glyph.lua new file mode 100644 index 00000000000..b0f16fafe2f --- /dev/null +++ b/scripts/items/bastok_markets_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4188 +-- Bastok Markets Gate Glyph +-- Transports the user to the Bastok Markets gate +-- (near Rabid Wolf, I.M.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.BASTOK_MARKETS_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/items/northern_san_doria_gate_glyph.lua b/scripts/items/northern_san_doria_gate_glyph.lua new file mode 100644 index 00000000000..7022f2a2a68 --- /dev/null +++ b/scripts/items/northern_san_doria_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4192 +-- Northern San d'Oria Gate Glyph +-- Transports the user to the Northern San d'Oria gate +-- (near Achantere, T.K.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.NORTH_SANDY_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/items/port_bastok_gate_glyph.lua b/scripts/items/port_bastok_gate_glyph.lua new file mode 100644 index 00000000000..c9c2539051c --- /dev/null +++ b/scripts/items/port_bastok_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4189 +-- Port Bastok Gate Glyph +-- Transports the user to the Port Bastok gate +-- (near Flying Axe, I.M.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.PORT_BASTOK_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/items/port_windurst_gate_glyph.lua b/scripts/items/port_windurst_gate_glyph.lua new file mode 100644 index 00000000000..ecae5653b34 --- /dev/null +++ b/scripts/items/port_windurst_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4194 +-- Port Windurst Gate Glyph +-- Transports the user to the Port Windurst gate +-- (near Milma-Hapilma, W.W.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.PORT_WINDY_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/items/western_san_doria_gate_glyph.lua b/scripts/items/western_san_doria_gate_glyph.lua new file mode 100644 index 00000000000..3228f5680c5 --- /dev/null +++ b/scripts/items/western_san_doria_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4191 +-- Western San d'Oria Gate Glyph +-- Transports the user to the western Southern San d'Oria gate +-- (near Aravoge, T.K.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.WEST_SANDY_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/items/windurst_waters_gate_glyph.lua b/scripts/items/windurst_waters_gate_glyph.lua new file mode 100644 index 00000000000..ef836046d93 --- /dev/null +++ b/scripts/items/windurst_waters_gate_glyph.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- ID: 4193 +-- Windurst Waters Gate Glyph +-- Transports the user to the Windurst Waters gate +-- (near Puroiko-Maiko, W.W.) +----------------------------------- +---@type TItem +local itemObject = {} + +itemObject.onItemCheck = function(target, item, caster) + return 0 +end + +itemObject.onItemUse = function(target, user) + target:addStatusEffect(xi.effect.TELEPORT, { power = xi.teleport.id.WINDY_WATERS_GLYPH, duration = 3, origin = user, icon = 0 }) +end + +return itemObject diff --git a/scripts/specs/core/Globals.lua b/scripts/specs/core/Globals.lua index 08fc4b0856a..98441f9b10a 100644 --- a/scripts/specs/core/Globals.lua +++ b/scripts/specs/core/Globals.lua @@ -71,6 +71,12 @@ end function GetRegionOwner(type) end +---@param amount integer +---@param nation integer +---@param region integer +function AddConquestInfluence(amount, nation, region) +end + ---@nodiscard ---@param type integer ---@return integer diff --git a/scripts/utils/utils.lua b/scripts/utils/utils.lua index d1b74ccd5ce..ec37b701443 100644 --- a/scripts/utils/utils.lua +++ b/scripts/utils/utils.lua @@ -283,6 +283,49 @@ function utils.clamp(input, minValue, maxValue) return input end +-- Generates a number from a normal distribution. If lower and/or upper is defined, +-- then the number is resampled up to 5 total times to follow the normal distribution. +-- Examples: +-- utils.randomNormal(3.5, 1.5, 2, 7) : Mean of 3.5, standard deviation of 1.5, 2 lower limit, 7 upper limit. +-- utils.randomNormal(3.5, 1.5) : Mean of 3.5, standard deviation of 1.5, no lower or upper limit. +-- utils.randomNormal(3.5, 1.5, 0) : Mean of 3.5, standard deviation of 1.5, 0 lower limit, no upper limit. +-- utils.randomNormal(3.5, 1.5, nil, 7) : Mean of 3.5, standard deviation of 1.5, no lower limit, 7 upper limit. +---@nodiscard +---@param mean number +---@param stddev number +---@param lower number? Optional lower bound; draws below it are rejected. +---@param upper number? Optional upper bound; draws above it are rejected. +---@return number +function utils.randomNormal(mean, stddev, lower, upper) + local value = 0 + + -- Try up to 5 draws, rejecting any that land outside the optional bounds. + for _ = 1, 5 do + -- Box-Muller equation to create a normal distribution from two uniform distributions. + -- 1 - math.random() gives (0, 1], so math.log never sees 0. + local u1 = 1 - math.random() + local u2 = math.random() + value = mean + stddev * math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2) + + local belowLower = lower and value < lower + local aboveUpper = upper and value > upper + if not belowLower and not aboveUpper then + return value + end + end + + -- All 5 draws missed. Clamp. + if lower and value < lower then + return lower + end + + if upper and value > upper then + return upper + end + + return value +end + -- Returns a table containing all the elements in the specified range. -- Source: https://github.com/mebens/range ---@nodiscard diff --git a/scripts/zones/Buburimu_Peninsula/DefaultActions.lua b/scripts/zones/Buburimu_Peninsula/DefaultActions.lua index 7891b5f8495..dd7d3c6b0c1 100644 --- a/scripts/zones/Buburimu_Peninsula/DefaultActions.lua +++ b/scripts/zones/Buburimu_Peninsula/DefaultActions.lua @@ -1,7 +1,6 @@ local ID = zones[xi.zone.BUBURIMU_PENINSULA] return { - ['Beastmens_Banner'] = { messageSpecial = ID.text.BEASTMEN_BANNER }, ['qm1'] = { messageSpecial = ID.text.SHIP_SANK_NEAR_HERE }, ['Song_Runes'] = { messageSpecial = ID.text.SONG_RUNES_DEFAULT }, ['Stone_Monument'] = { event = 900 }, diff --git a/scripts/zones/Buburimu_Peninsula/IDs.lua b/scripts/zones/Buburimu_Peninsula/IDs.lua index 65de576468a..d8390b156cd 100644 --- a/scripts/zones/Buburimu_Peninsula/IDs.lua +++ b/scripts/zones/Buburimu_Peninsula/IDs.lua @@ -19,6 +19,14 @@ zones[xi.zone.BUBURIMU_PENINSULA] = LOGIN_NUMBER = 7042, -- 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 = 7062, -- Your party is unable to participate because certain members' levels are restricted. CONQUEST_BASE = 7107, -- Tallying conquest results... + REGION_POINTS_SANDORIA = 7172, -- San d'Oria's region points have increased! + REGION_POINTS_BASTOK = 7173, -- Bastok's region points have increased! + REGION_POINTS_WINDURST = 7174, -- Windurst's region points have increased! + EXP_FORCE_KILL_SANDORIA = 7175, -- San d'Orian E.F. defeats beastmen hordes... Maintain current momentum. + EXP_FORCE_KILL_BASTOK = 7176, -- Bastokan E.F. defeats beastmen hordes...Maintain current momentum. + EXP_FORCE_KILL_WINDURST = 7177, -- Windurstian E.F. defeats beastmen hordes...Maintain current momentum. + BEASTMEN_BANNER_CURSE = 7186, -- There was a curse on the beastmen's banner! + BEASTMEN_BANNER_LIFTED = 7187, -- The curse of the beastmen's banner has been lifted! BEASTMEN_BANNER = 7188, -- There is a beastmen's banner. FIVEOFSPADES_DIALOG = 7266, -- GiMme★fIvE! FiVe is★A cArdIan★OF WiN-DuRst! FIvE★iS On★pA-tRol! FISHING_MESSAGE_OFFSET = 7272, -- You can't fish here. @@ -64,9 +72,27 @@ zones[xi.zone.BUBURIMU_PENINSULA] = BACKOO = GetFirstID('Backoo'), BUBURIMBOO = GetFirstID('Buburimboo'), HELLDIVER = GetFirstID('Helldiver'), + + -- Expeditionary Force + HOBGOBLIN_BEASTMASTER = GetFirstID('Hobgoblin_Beastmaster'), + HOBGOBLIN_BLACK_MAGE = GetFirstID('Hobgoblin_Black_Mage'), + HOBGOBLIN_DARK_KNIGHT = GetFirstID('Hobgoblin_Dark_Knight'), + HOBGOBLIN_RANGER = GetFirstID('Hobgoblin_Ranger'), + HOBGOBLIN_RED_MAGE = GetFirstID('Hobgoblin_Red_Mage'), + HOBGOBLIN_THIEF = GetFirstID('Hobgoblin_Thief'), + HOBGOBLIN_WARRIOR = GetFirstID('Hobgoblin_Warrior'), + HOBGOBLIN_WHITE_MAGE = GetFirstID('Hobgoblin_White_Mage'), + THEOYAGUDO_BARD = GetFirstID('Theoyagudo_Bard'), + THEOYAGUDO_BLACK_MAGE = GetFirstID('Theoyagudo_Black_Mage'), + THEOYAGUDO_MONK = GetFirstID('Theoyagudo_Monk'), + THEOYAGUDO_NINJA = GetFirstID('Theoyagudo_Ninja'), + THEOYAGUDO_SAMURAI = GetFirstID('Theoyagudo_Samurai'), + THEOYAGUDO_SUMMONER = GetFirstID('Theoyagudo_Summoner'), + THEOYAGUDO_WHITE_MAGE = GetFirstID('Theoyagudo_White_Mage'), }, npc = { + BEASTMENS_BANNER = GetFirstID('Beastmens_Banner'), BRIGAND_CHART_HUME = GetFirstID('Brigand_Chart_Hume'), BRIGAND_CHART_QM = GetFirstID('qm1'), JADE_ETUI_TABLE = GetTableOfIDs('Jade_Etui'), diff --git a/scripts/zones/Buburimu_Peninsula/Zone.lua b/scripts/zones/Buburimu_Peninsula/Zone.lua index 96d97e5fc17..d0554d8d195 100644 --- a/scripts/zones/Buburimu_Peninsula/Zone.lua +++ b/scripts/zones/Buburimu_Peninsula/Zone.lua @@ -11,6 +11,8 @@ zoneObject.onInitialize = function(zone) xi.conquest.setRegionalConquestOverseers(zone:getRegionID()) xi.helm.initZone(zone, xi.helmType.LOGGING) + + xi.expeditionaryForce.initZone(zone) end zoneObject.onZoneIn = function(player, prevZone) diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Goblins_Rabbit.lua b/scripts/zones/Buburimu_Peninsula/mobs/Goblins_Rabbit.lua new file mode 100644 index 00000000000..16bcdcf43fc --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Goblins_Rabbit.lua @@ -0,0 +1,12 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Goblin's Rabbit +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobSpawn = function(mob) + xi.expeditionaryForce.gatePet(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Beastmaster.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Beastmaster.lua new file mode 100644 index 00000000000..c3d2562daf8 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Beastmaster.lua @@ -0,0 +1,36 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Beastmaster +-- Job: BST +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + xi.pet.setMobPet(mob, 1, 'Goblins_Rabbit') + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) + + -- BST pets aren't auto-killed on death (only SMN pets are), so clean it up + local pet = mob:getPet() + if pet then + DespawnMob(pet:getID()) + end +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) + + -- Idle despawn doesn't clean up the pet, so do it here + local pet = mob:getPet() + if pet then + DespawnMob(pet:getID()) + end +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Black_Mage.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Black_Mage.lua new file mode 100644 index 00000000000..70924c3b2a0 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Black_Mage.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Black Mage +-- Job: BLM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Dark_Knight.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Dark_Knight.lua new file mode 100644 index 00000000000..5694e19d5ef --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Dark_Knight.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Dark Knight +-- Job: DRK +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Ranger.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Ranger.lua new file mode 100644 index 00000000000..ff2ef513b1b --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Ranger.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Ranger +-- Job: RNG +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Red_Mage.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Red_Mage.lua new file mode 100644 index 00000000000..30be5ffc2a1 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Red_Mage.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Red Mage +-- Job: RDM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Thief.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Thief.lua new file mode 100644 index 00000000000..43ccee16cba --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Thief.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Thief +-- Job: THF +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Warrior.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Warrior.lua new file mode 100644 index 00000000000..1d54d4e1d5d --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_Warrior.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin Warrior +-- Job: WAR +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_White_Mage.lua b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_White_Mage.lua new file mode 100644 index 00000000000..8b825618e88 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Hobgoblin_White_Mage.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Hobgoblin White Mage +-- Job: WHM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Bard.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Bard.lua new file mode 100644 index 00000000000..9ddbffc365c --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Bard.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Bard +-- Job: BRD +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Black_Mage.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Black_Mage.lua new file mode 100644 index 00000000000..f1bcb6f59c8 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Black_Mage.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Black Mage +-- Job: BLM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Monk.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Monk.lua new file mode 100644 index 00000000000..9c2777ac385 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Monk.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Monk +-- Job: MNK +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Ninja.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Ninja.lua new file mode 100644 index 00000000000..59c423ea47d --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Ninja.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Ninja +-- Job: NIN +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Samurai.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Samurai.lua new file mode 100644 index 00000000000..eee4bb426b5 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Samurai.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Samurai +-- Job: SAM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Summoner.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Summoner.lua new file mode 100644 index 00000000000..7ab1f6f83d0 --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_Summoner.lua @@ -0,0 +1,31 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo Summoner +-- Job: SMN +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + xi.pet.setMobPet(mob, 1, 'Yagudos_Elemental') + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn + mob:setMobMod(xi.mobMod.ASTRAL_PET_OFFSET, 2) +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) + + -- Idle despawn doesn't clean up the pet, so do it here (death is handled by the engine) + local pet = mob:getPet() + if pet then + DespawnMob(pet:getID()) + end +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_White_Mage.lua b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_White_Mage.lua new file mode 100644 index 00000000000..400eb62880d --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Theoyagudo_White_Mage.lua @@ -0,0 +1,23 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Theoyagudo White Mage +-- Job: WHM +----------------------------------- +mixins = { require('scripts/mixins/job_special') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobInitialize = function(mob) + mob:setMobMod(xi.mobMod.IDLE_DESPAWN, 180) -- 3 minute idle despawn +end + +entity.onMobDeath = function(mob, player, optParams) + xi.expeditionaryForce.onMobDeath(mob, player, optParams) +end + +entity.onMobDespawn = function(mob) + xi.expeditionaryForce.onMobDespawn(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/mobs/Yagudos_Avatar.lua b/scripts/zones/Buburimu_Peninsula/mobs/Yagudos_Avatar.lua new file mode 100644 index 00000000000..30d2b0e69cd --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/mobs/Yagudos_Avatar.lua @@ -0,0 +1,14 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- Mob: Yagudo's Avatar +----------------------------------- +mixins = { require('scripts/mixins/families/avatar') } +----------------------------------- +---@type TMobEntity +local entity = {} + +entity.onMobSpawn = function(mob) + xi.expeditionaryForce.gatePet(mob) +end + +return entity diff --git a/scripts/zones/Buburimu_Peninsula/npcs/Beastmens_Banner.lua b/scripts/zones/Buburimu_Peninsula/npcs/Beastmens_Banner.lua new file mode 100644 index 00000000000..b950c7316db --- /dev/null +++ b/scripts/zones/Buburimu_Peninsula/npcs/Beastmens_Banner.lua @@ -0,0 +1,12 @@ +----------------------------------- +-- Area: Buburimu Peninsula +-- NPC: Beastmen's Banner +----------------------------------- +---@type TNpcEntity +local entity = {} + +entity.onTrigger = function(player, npc) + xi.expeditionaryForce.onBannerTrigger(player, npc) +end + +return entity diff --git a/scripts/zones/Heavens_Tower/IDs.lua b/scripts/zones/Heavens_Tower/IDs.lua index 7db19730e29..7684abe11e9 100644 --- a/scripts/zones/Heavens_Tower/IDs.lua +++ b/scripts/zones/Heavens_Tower/IDs.lua @@ -24,6 +24,7 @@ zones[xi.zone.HEAVENS_TOWER] = CALL_MULTIPLE_ALTER_EGO = 7199, -- You are now able to call multiple alter egos. YOU_ACCEPT_THE_MISSION = 7336, -- You have accepted the mission. FISHING_MESSAGE_OFFSET = 7389, -- You can't fish here. + INVALID_ENSIGNIAS = 7621, -- Your invalid ensignias have been disposed of. CELEBRATORY_GOODS = 9123, -- An assortment of celebratory goods is available for purchase. OBTAINED_NUM_KEYITEMS = 9201, -- Obtained key item: ! NOT_ACQUAINTED = 9203, -- I'm sorry, but I don't believe we're acquainted. Please leave me be. diff --git a/scripts/zones/Heavens_Tower/npcs/Rakano-Marukano.lua b/scripts/zones/Heavens_Tower/npcs/Rakano-Marukano.lua index d9a4cfaecae..9baeaa48080 100644 --- a/scripts/zones/Heavens_Tower/npcs/Rakano-Marukano.lua +++ b/scripts/zones/Heavens_Tower/npcs/Rakano-Marukano.lua @@ -56,6 +56,15 @@ entity.onEventFinish = function(player, csid, option, npc) player:setNation(newNation) player:setGil(player:getGil() - cost) player:setRankPoints(0) + + -- Remove Expeditionary Force insignias + if xi.expeditionaryForce.disposeInsigniaNationSwap(player) then + local ID = zones[xi.zone.HEAVENS_TOWER] + player:messageSpecial(ID.text.INVALID_ENSIGNIAS) + player:setCharVar('[ExpForce]Participation', 0) + player:setCharVar('[ExpForce]NextConquestTally', 0) + player:setCharVar('[ExpForce]AwardCP', 0) + end end end diff --git a/scripts/zones/Labyrinth_of_Onzozo/IDs.lua b/scripts/zones/Labyrinth_of_Onzozo/IDs.lua index e493d77a93c..0266b6e6a20 100644 --- a/scripts/zones/Labyrinth_of_Onzozo/IDs.lua +++ b/scripts/zones/Labyrinth_of_Onzozo/IDs.lua @@ -19,6 +19,9 @@ zones[xi.zone.LABYRINTH_OF_ONZOZO] = GEOMAGNETRON_ATTUNED = 7016, -- Your has been attuned to a geomagnetic fount in the corresponding locale. MEMBERS_LEVELS_ARE_RESTRICTED = 7027, -- Your party is unable to participate because certain members' levels are restricted. CONQUEST_BASE = 7072, -- Tallying conquest results... + EXP_FORCE_CHEST_SANDORIA = 7137, -- San d'Oria's region points have increased! + EXP_FORCE_CHEST_BASTOK = 7138, -- Bastok's region points have increased! + EXP_FORCE_CHEST_WINDURST = 7139, -- Windurst's region points have increased! FISHING_MESSAGE_OFFSET = 7231, -- You can't fish here. CHEST_UNLOCKED = 7340, -- You unlock the chest! NEST_OF_LARGE_BIRD = 7348, -- It looks like the nest of a very large bird. diff --git a/scripts/zones/Maze_of_Shakhrami/IDs.lua b/scripts/zones/Maze_of_Shakhrami/IDs.lua index baabed4ee2e..11ed1a30170 100644 --- a/scripts/zones/Maze_of_Shakhrami/IDs.lua +++ b/scripts/zones/Maze_of_Shakhrami/IDs.lua @@ -31,6 +31,9 @@ zones[xi.zone.MAZE_OF_SHAKHRAMI] = WATER_POOL = 7097, -- Water forms a pool here. WAIT_A_BIT_LONGER = 7098, -- It does not seem to have become yet. You need to wait a bit longer. CONQUEST_BASE = 7100, -- Tallying conquest results... + EXP_FORCE_CHEST_SANDORIA = 7165, -- San d'Oria's region points have increased! + EXP_FORCE_CHEST_BASTOK = 7166, -- Bastok's region points have increased! + EXP_FORCE_CHEST_WINDURST = 7167, -- Windurst's region points have increased! DEVICE_NOT_WORKING = 7273, -- The device is not working. SYS_OVERLOAD = 7282, -- Warning! Sys...verload! Enterin...fety mode. ID eras...d. YOU_LOST_THE = 7287, -- You lost the . diff --git a/scripts/zones/Metalworks/IDs.lua b/scripts/zones/Metalworks/IDs.lua index 02be4c26828..97b31c580f8 100644 --- a/scripts/zones/Metalworks/IDs.lua +++ b/scripts/zones/Metalworks/IDs.lua @@ -44,6 +44,7 @@ zones[xi.zone.METALWORKS] = CONQUEST = 8232, -- You've earned conquest points! GLAROCIQUET_DIALOG = 8234, -- I am , a Temple Knight. I am one of the guards charged with overseeing San d'Oria's conquest campaign. LEXUN_MARIXUN_DIALOG = 8236, -- I am , a War Warlock. I am one of the guards charged with overseeing Windurst's conquest campaign. + INVALID_ENSIGNIAS = 8354, -- Your invalid ensignias have been disposed of. EXTENDED_MISSION_OFFSET = 8622, -- Go to Ore Street and talk to Medicine Eagle. He says he was there when the commotion started. STEEL_CYCLONE_LEARNED = 9040, -- You have learned the weapon skill Steel Cyclone! DETONATOR_LEARNED = 9065, -- You have learned the weapon skill Detonator! diff --git a/scripts/zones/Metalworks/npcs/Mythily.lua b/scripts/zones/Metalworks/npcs/Mythily.lua index 070978d008b..3c439ad7ded 100644 --- a/scripts/zones/Metalworks/npcs/Mythily.lua +++ b/scripts/zones/Metalworks/npcs/Mythily.lua @@ -56,6 +56,15 @@ entity.onEventFinish = function(player, csid, option, npc) player:setNation(newNation) player:setGil(player:getGil() - cost) player:setRankPoints(0) + + -- Remove Expeditionary Force insignias + if xi.expeditionaryForce.disposeInsigniaNationSwap(player) then + local ID = zones[xi.zone.METALWORKS] + player:messageSpecial(ID.text.INVALID_ENSIGNIAS) + player:setCharVar('[ExpForce]Participation', 0) + player:setCharVar('[ExpForce]NextConquestTally', 0) + player:setCharVar('[ExpForce]AwardCP', 0) + end end end diff --git a/scripts/zones/Northern_San_dOria/IDs.lua b/scripts/zones/Northern_San_dOria/IDs.lua index 7d76c11aad8..0a8b281d5b5 100644 --- a/scripts/zones/Northern_San_dOria/IDs.lua +++ b/scripts/zones/Northern_San_dOria/IDs.lua @@ -104,6 +104,7 @@ zones[xi.zone.NORTHERN_SAN_DORIA] = EUGBALLION_OPEN_DIALOG = 11680, -- Have a look at these goods imported direct from Qufim Island! CHAUPIRE_SHOP_DIALOG = 11681, -- San d'Orian woodcraft is the finest in the land! CONQUEST = 11747, -- You've earned conquest points! + INVALID_ENSIGNIAS = 11869, -- Your invalid ensignias have been disposed of. FFR_BONCORT = 12094, -- Hmm... With magic, I could get hold of materials a mite easier. I'll have to check this mart out. FFR_CAPIRIA = 12095, -- A flyer? For me? Some reading material would be a welcome change of pace, indeed! FFR_VILLION = 12096, -- Opening a shop of magic, without consulting me first? I must pay this Regine a visit! diff --git a/scripts/zones/Northern_San_dOria/npcs/Beriphaule.lua b/scripts/zones/Northern_San_dOria/npcs/Beriphaule.lua index c6c9718f0e5..25b9442fdb0 100644 --- a/scripts/zones/Northern_San_dOria/npcs/Beriphaule.lua +++ b/scripts/zones/Northern_San_dOria/npcs/Beriphaule.lua @@ -56,6 +56,15 @@ entity.onEventFinish = function(player, csid, option, npc) player:setNation(newNation) player:setGil(player:getGil() - cost) player:setRankPoints(0) + + -- Remove Expeditionary Force insignias + if xi.expeditionaryForce.disposeInsigniaNationSwap(player) then + local ID = zones[xi.zone.NORTHERN_SAN_DORIA] + player:messageSpecial(ID.text.INVALID_ENSIGNIAS) + player:setCharVar('[ExpForce]Participation', 0) + player:setCharVar('[ExpForce]NextConquestTally', 0) + player:setCharVar('[ExpForce]AwardCP', 0) + end end end diff --git a/settings/default/main.lua b/settings/default/main.lua index 77db4637a61..f9f593ab5c3 100644 --- a/settings/default/main.lua +++ b/settings/default/main.lua @@ -221,6 +221,10 @@ xi.settings.main = GARRISON_NATION_BYPASS = false, -- Set to true to bypass the nation requirement. GARRISON_RANK = 2, -- Set to minumum Nation Rank to start Garrison (default: 2). + -- EXPEDITIONARY FORCE SETTINGS + EXP_FORCE_MOBKILL_INFLUENCE = 700, + EXP_FORCE_TREASURE_INFLUENCE = 700, + -- DYNAMIS SETTINGS BETWEEN_2DYNA_WAIT_TIME = 24, -- Hours before player can re-enter Dynamis. Default is 1 Earthday (24 hours). DYNA_MIDNIGHT_RESET = true, -- If true, makes the wait time count by number of server midnights instead of full 24 hour intervals diff --git a/sql/mob_spawn_points.sql b/sql/mob_spawn_points.sql index 71dbac0a6b4..b05cd02d326 100644 --- a/sql/mob_spawn_points.sql +++ b/sql/mob_spawn_points.sql @@ -44890,24 +44890,26 @@ INSERT INTO `mob_spawn_points` VALUES (17261004,0,'Bogy','Bogy',15,24,25,415.000 INSERT INTO `mob_spawn_points` VALUES (17261005,0,'Bogy','Bogy',15,24,25,362.000,15.000,170.000,97); INSERT INTO `mob_spawn_points` VALUES (17261006,0,'Water_Elemental','Water Elemental',23,28,30,440.067,19.499,133.811,46); INSERT INTO `mob_spawn_points` VALUES (17261007,0,'Goblin_Digger','Goblin Digger',27,20,21,220.690,-9.785,103.254,91); -INSERT INTO `mob_spawn_points` VALUES (17261008,0,'Hobgoblin_Warrior','Hobgoblin Warrior',28,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261009,0,'Hobgoblin_White_Mage','Hobgoblin White Mage',29,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261010,0,'Hobgoblin_Black_Mage','Hobgoblin Black Mage',30,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261011,0,'Hobgoblin_Red_Mage','Hobgoblin Red Mage',31,0,0,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261012,0,'Hobgoblin_Thief','Hobgoblin Thief',32,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261013,0,'Hobgoblin_Dark_Knight','Hobgoblin Dark Knight',33,0,0,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261014,0,'Hobgoblin_Ranger','Hobgoblin Ranger',34,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261015,0,'Hobgoblin_Beastmaster','Hobgoblin Beastmaster',35,30,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261016,0,'Goblins_Rabbit','Goblin\'s Rabbit',36,23,25,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261017,0,'Theoyagudo_Monk','Theoyagudo Monk',37,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261018,0,'Theoyagudo_White_Mage','Theoyagudo White Mage',38,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261019,0,'Theoyagudo_Black_Mage','Theoyagudo Black Mage',39,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261020,0,'Theoyagudo_Bard','Theoyagudo Bard',40,35,35,73.001,-20.281,-62.986,114); -INSERT INTO `mob_spawn_points` VALUES (17261021,0,'Theoyagudo_Samurai','Theoyagudo Samurai',41,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261022,0,'Theoyagudo_Ninja','Theoyagudo Ninja',42,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261023,0,'Theoyagudo_Summoner','Theoyagudo Summoner',43,35,35,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261024,0,'Yagudos_Elemental','Yagudo\'s Elemental',44,28,30,0.000,0.000,0.000,0); -INSERT INTO `mob_spawn_points` VALUES (17261025,0,'Yagudos_Avatar','Yagudo\'s Avatar',45,28,30,0.000,0.000,0.000,0); + +-- Expeditionary Force +INSERT INTO `mob_spawn_points` VALUES (17261008,0,'Hobgoblin_Warrior','Hobgoblin Warrior',28,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261009,0,'Hobgoblin_White_Mage','Hobgoblin White Mage',29,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261010,0,'Hobgoblin_Black_Mage','Hobgoblin Black Mage',30,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261011,0,'Hobgoblin_Red_Mage','Hobgoblin Red Mage',31,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261012,0,'Hobgoblin_Thief','Hobgoblin Thief',32,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261013,0,'Hobgoblin_Dark_Knight','Hobgoblin Dark Knight',33,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261014,0,'Hobgoblin_Ranger','Hobgoblin Ranger',34,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261015,0,'Hobgoblin_Beastmaster','Hobgoblin Beastmaster',35,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261016,0,'Goblins_Rabbit','Goblin\'s Rabbit',36,28,30,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261017,0,'Theoyagudo_Monk','Theoyagudo Monk',37,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261018,0,'Theoyagudo_White_Mage','Theoyagudo White Mage',38,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261019,0,'Theoyagudo_Black_Mage','Theoyagudo Black Mage',39,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261020,0,'Theoyagudo_Bard','Theoyagudo Bard',40,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261021,0,'Theoyagudo_Samurai','Theoyagudo Samurai',41,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261022,0,'Theoyagudo_Ninja','Theoyagudo Ninja',42,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261023,0,'Theoyagudo_Summoner','Theoyagudo Summoner',43,30,33,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261024,0,'Yagudos_Elemental','Yagudo\'s Elemental',44,28,30,1.000,1.000,1.000,0); +INSERT INTO `mob_spawn_points` VALUES (17261025,0,'Yagudos_Avatar','Yagudo\'s Avatar',45,28,30,1.000,1.000,1.000,0); -- Garrison INSERT INTO `mob_spawn_points` VALUES (17261026,0,'Goblin_Swordmaker','Goblin Swordmaker',46,30,35,-494.333,-29.776,58.614,120); diff --git a/src/map/conquest_system.h b/src/map/conquest_system.h index fe64facef87..10a3d8621a7 100644 --- a/src/map/conquest_system.h +++ b/src/map/conquest_system.h @@ -51,9 +51,10 @@ ConquestData& GetConquestData(); // Cached data with influences / region control void HandleMessage(ConquestMessage type, const std::span data); -void UpdateConquestGM(ConquestUpdate type); // Update conquest system by GM (modify in the DB and use @updateconquest) -void GainInfluencePoints(CCharEntity* PChar, uint32 points); // Gain influence for player's nation (+1) -void LoseInfluencePoints(CCharEntity* PChar); // Lose influence for player's nation and gain for beastmen influence +void UpdateConquestGM(ConquestUpdate type); // Update conquest system by GM (modify in the DB and use @updateconquest) +void GainInfluencePoints(CCharEntity* PChar, uint32 points); // Gain influence for player's nation (+1) +void LoseInfluencePoints(CCharEntity* PChar); // Lose influence for player's nation and gain for beastmen influence +void AddInfluencePoints(int points, unsigned int nation, REGION_TYPE region); // Add influence to a nation in a region (sends to world server) uint8 GetInfluenceGraphics(int32 san_inf, int32 bas_inf, int32 win_inf, int32 bst_inf); // Get number for graphics in conquest menu (arrows) uint8 GetInfluenceGraphics(REGION_TYPE RegionID); // Get number for graphics in conquest menu (arrows) diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index a5225a36b4d..af41f3c917e 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -42,6 +42,7 @@ #include "alliance.h" #include "aman.h" #include "battlefield.h" +#include "conquest_system.h" #include "daily_system.h" #include "enmity_container.h" #include "fishingcontest.h" @@ -9785,6 +9786,32 @@ void CLuaBaseEntity::delCP(int32 cp) PChar->pushPacket(PChar); } +/************************************************************************ + * Function: gainInfluencePoints() + * Purpose : Adds conquest influence to the player's nation and current region + * Example : player:gainInfluencePoints(50) + * Notes : Applies the player's Moghancement region bonus. + ************************************************************************/ + +void CLuaBaseEntity::gainInfluencePoints(int32 points) +{ + if (m_PBaseEntity->objtype != TYPE_PC) + { + ShowWarning("Invalid entity type calling function (%s).", m_PBaseEntity->getName()); + return; + } + + if (points <= 0) + { + ShowWarning("gainInfluencePoints: non-positive amount (%d) ignored.", points); + return; + } + + auto* PChar = static_cast(m_PBaseEntity); + + conquest::GainInfluencePoints(PChar, static_cast(points)); +} + /************************************************************************ * Function: getSeals() * Purpose : Returns the current seal balance for a player @@ -20512,6 +20539,7 @@ void CLuaBaseEntity::Register() SOL_REGISTER("getCP", CLuaBaseEntity::getCP); SOL_REGISTER("addCP", CLuaBaseEntity::addCP); SOL_REGISTER("delCP", CLuaBaseEntity::delCP); + SOL_REGISTER("gainInfluencePoints", CLuaBaseEntity::gainInfluencePoints); SOL_REGISTER("getSeals", CLuaBaseEntity::getSeals); SOL_REGISTER("addSeals", CLuaBaseEntity::addSeals); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 041ad922930..2034b862513 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -490,6 +490,7 @@ class CLuaBaseEntity int32 getCP(); // Conquest points, not to be confused with Capacity Points void addCP(int32 cp); void delCP(int32 cp); + void gainInfluencePoints(int32 points); int32 getSeals(uint8 sealType); void addSeals(int32 points, uint8 sealType); diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index 4930e81c936..202cbf86fd4 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -270,6 +270,7 @@ void init(IPP mapIPP, bool isRunningInCI) lua.set_function("GetNationRank", &luautils::GetNationRank); lua.set_function("GetConquestBalance", &luautils::GetConquestBalance); lua.set_function("IsConquestAlliance", &luautils::IsConquestAlliance); + lua.set_function("AddConquestInfluence", &luautils::AddConquestInfluence); lua.set_function("SpawnMob", &luautils::SpawnMob); lua.set_function("DespawnMob", &luautils::DespawnMob); lua.set_function("GetPlayerByName", &luautils::GetPlayerByName); @@ -1526,6 +1527,25 @@ uint8 GetConquestBalance() return conquest::GetBalance(); } +void AddConquestInfluence(int32 amount, uint8 nation, uint8 region) +{ + TracyZoneScoped; + + if (nation > NATION_BEASTMEN) + { + ShowError("Lua::AddConquestInfluence: invalid nation %d (expected 0-3).", nation); + return; + } + + if (amount <= 0) + { + ShowWarning("Lua::AddConquestInfluence: non-positive amount %d ignored.", amount); + return; + } + + conquest::AddInfluencePoints(amount, nation, static_cast(region)); +} + bool IsConquestAlliance() { TracyZoneScoped; diff --git a/src/map/lua/luautils.h b/src/map/lua/luautils.h index d25ebfd5211..99bdfae92ed 100644 --- a/src/map/lua/luautils.h +++ b/src/map/lua/luautils.h @@ -228,6 +228,7 @@ uint8 GetRegionOwner(uint8 type); uint8 GetRegionInfluence(uint8 type); // Return influence graphics uint8 GetNationRank(uint8 nation); uint8 GetConquestBalance(); +void AddConquestInfluence(int32 amount, uint8 nation, uint8 region); // Add influence to a nation in a region bool IsConquestAlliance(); void SetRegionalConquestOverseers(uint8 regionID); // Update NPC Conquest Guard void SendLuaFuncStringToZone(uint16 requestingZoneId, uint16 executorZoneId, const std::string& str); diff --git a/src/map/status_effect.h b/src/map/status_effect.h index df59625695c..1b9d779286b 100644 --- a/src/map/status_effect.h +++ b/src/map/status_effect.h @@ -42,38 +42,39 @@ DECLARE_FORMAT_AS_UNDERLYING(EFFECTOVERWRITE); enum EFFECTFLAG : uint32 { - EFFECTFLAG_NONE = 0x00000000, - EFFECTFLAG_DISPELABLE = 0x00000001, - EFFECTFLAG_ERASABLE = 0x00000002, - EFFECTFLAG_ATTACK = 0x00000004, // disappears upon attacking - EFFECTFLAG_EMPATHY = 0X00000008, // effect can be copied to wyvern by use of merited Spirit Link - EFFECTFLAG_DAMAGE = 0x00000010, // disappears upon being attacked - EFFECTFLAG_DEATH = 0x00000020, // disappears upon death/KO - EFFECTFLAG_MAGIC_BEGIN = 0x00000040, // disappears upon spellcasting start - EFFECTFLAG_MAGIC_END = 0x00000080, // disappears upon spellcasting complete - EFFECTFLAG_ON_ZONE = 0x00000100, - EFFECTFLAG_NO_LOSS_MESSAGE = 0x00000200, // Suppress effect worn off message. - EFFECTFLAG_INVISIBLE = 0x00000400, // invisible effect - EFFECTFLAG_DETECTABLE = 0x00000800, // invisible, sneak, deo - EFFECTFLAG_NO_REST = 0x00001000, // prevents resting, curse II, plague, disease - EFFECTFLAG_PREVENT_ACTION = 0x00002000, // sleep, lullaby, stun, petrify - EFFECTFLAG_WALTZABLE = 0x00004000, // for healing waltzable spells - EFFECTFLAG_FOOD = 0x00008000, - EFFECTFLAG_SONG = 0x00010000, // bard songs - EFFECTFLAG_ROLL = 0x00020000, // corsair rolls - EFFECTFLAG_SYNTH_SUPPORT = 0x00040000, // Synthesis Image Support - EFFECTFLAG_CONFRONTATION = 0x00080000, - EFFECTFLAG_LOGOUT = 0x00100000, - EFFECTFLAG_BLOODPACT = 0x00200000, - EFFECTFLAG_ON_JOBCHANGE = 0x00400000, // Removes effect when you change jobs - EFFECTFLAG_NO_CANCEL = 0x00800000, // CAN NOT CLICK IT OFF IN CLIENT - EFFECTFLAG_INFLUENCE = 0x01000000, // Influence effects - e.g. Signet, Sanction, Sigil, Ionis - EFFECTFLAG_OFFLINE_TICK = 0x02000000, // Duration elapses while offline - EFFECTFLAG_AURA = 0x04000000, // Is an aura type effect - EFFECTFLAG_HIDE_TIMER = 0x08000000, // Sends "Always" in the packet, even though timer is tracked - EFFECTFLAG_ON_ZONE_PATHOS = 0x10000000, // removes the effect zoning into a non instanced zone - EFFECTFLAG_ALWAYS_EXPIRING = 0x20000000, // Timer is always 4 seconds from now to have an illusion permanent "expiring", used for Auras - EFFECTFLAG_ON_ATTACK = 0x40000000, // Removes effect upon receiving an attack, regardless of hit/dmg + EFFECTFLAG_NONE = 0x00000000, + EFFECTFLAG_DISPELABLE = 0x00000001, + EFFECTFLAG_ERASABLE = 0x00000002, + EFFECTFLAG_ATTACK = 0x00000004, // disappears upon attacking + EFFECTFLAG_EMPATHY = 0X00000008, // effect can be copied to wyvern by use of merited Spirit Link + EFFECTFLAG_DAMAGE = 0x00000010, // disappears upon being attacked + EFFECTFLAG_DEATH = 0x00000020, // disappears upon death/KO + EFFECTFLAG_MAGIC_BEGIN = 0x00000040, // disappears upon spellcasting start + EFFECTFLAG_MAGIC_END = 0x00000080, // disappears upon spellcasting complete + EFFECTFLAG_ON_ZONE = 0x00000100, + EFFECTFLAG_NO_LOSS_MESSAGE = 0x00000200, // Suppress effect worn off message. + EFFECTFLAG_INVISIBLE = 0x00000400, // invisible effect + EFFECTFLAG_DETECTABLE = 0x00000800, // invisible, sneak, deo + EFFECTFLAG_NO_REST = 0x00001000, // prevents resting, curse II, plague, disease + EFFECTFLAG_PREVENT_ACTION = 0x00002000, // sleep, lullaby, stun, petrify + EFFECTFLAG_WALTZABLE = 0x00004000, // for healing waltzable spells + EFFECTFLAG_FOOD = 0x00008000, + EFFECTFLAG_SONG = 0x00010000, // bard songs + EFFECTFLAG_ROLL = 0x00020000, // corsair rolls + EFFECTFLAG_SYNTH_SUPPORT = 0x00040000, // Synthesis Image Support + EFFECTFLAG_CONFRONTATION = 0x00080000, + EFFECTFLAG_LOGOUT = 0x00100000, + EFFECTFLAG_BLOODPACT = 0x00200000, + EFFECTFLAG_ON_JOBCHANGE = 0x00400000, // Removes effect when you change jobs + EFFECTFLAG_NO_CANCEL = 0x00800000, // CAN NOT CLICK IT OFF IN CLIENT + EFFECTFLAG_INFLUENCE = 0x01000000, // Influence effects - e.g. Signet, Sanction, Sigil, Ionis + EFFECTFLAG_OFFLINE_TICK = 0x02000000, // Duration elapses while offline + EFFECTFLAG_AURA = 0x04000000, // Is an aura type effect + EFFECTFLAG_HIDE_TIMER = 0x08000000, // Sends "Always" in the packet, even though timer is tracked + EFFECTFLAG_ON_ZONE_PATHOS = 0x10000000, // removes the effect zoning into a non instanced zone + EFFECTFLAG_ALWAYS_EXPIRING = 0x20000000, // Timer is always 4 seconds from now to have an illusion permanent "expiring", used for Auras + EFFECTFLAG_ON_ATTACK = 0x40000000, // Removes effect upon receiving an attack, regardless of hit/dmg + EFFECTFLAG_EXP_FROM_ACTUAL_LEVEL = 0x80000000, // EXP uses the player's true level, not the level-restricted one }; DECLARE_FORMAT_AS_UNDERLYING(EFFECTFLAG); diff --git a/src/map/utils/charutils.cpp b/src/map/utils/charutils.cpp index b769083b33d..7d080d234b9 100644 --- a/src/map/utils/charutils.cpp +++ b/src/map/utils/charutils.cpp @@ -4749,6 +4749,26 @@ uint32 GetExpNEXTLevel(uint8 charlvl) return 0; } +/************************************************************************ + * * + * Level used to calculate EXP. * + * * + ************************************************************************/ + +uint8 GetExpLevel(CBattleEntity* PMember) +{ + if (auto* PChar = dynamic_cast(PMember)) + { + CStatusEffect* PRestriction = PChar->StatusEffectContainer->GetStatusEffect(EFFECT_LEVEL_RESTRICTION); + if (PRestriction && PRestriction->HasEffectFlag(EFFECTFLAG_EXP_FROM_ACTUAL_LEVEL)) + { + return PChar->jobs.job[PChar->GetMJob()]; + } + } + + return PMember->GetMLevel(); +} + /************************************************************************ * * * Distributes gil to party members. * @@ -4916,7 +4936,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) uint8 pcinzone = 0; uint8 minlevel = 0; - uint8 maxlevel = PChar->GetMLevel(); + uint8 maxlevel = GetExpLevel(PChar); REGION_TYPE region = PChar->loc.zone->GetRegionID(); if (PChar->PParty) @@ -4952,13 +4972,14 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) { maxlevel = PMember->PPet->GetMLevel(); } - if (PMember->GetMLevel() > maxlevel) + const uint8 memberExpLevel = GetExpLevel(PMember); + if (memberExpLevel > maxlevel) { - maxlevel = PMember->GetMLevel(); + maxlevel = memberExpLevel; } - else if (PMember->GetMLevel() < minlevel) + else if (memberExpLevel < minlevel) { - minlevel = PMember->GetMLevel(); + minlevel = memberExpLevel; } pcinzone++; } @@ -4982,7 +5003,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) bool chainactive = false; const int16 moblevel = PMob->GetMLevel() + PMob->getMod(Mod::EXP_LVL_MOD); - const uint8 memberlevel = PMember->GetMLevel(); + const uint8 memberlevel = GetExpLevel(PMember); EMobDifficulty mobCheck = CheckMob(maxlevel, PMob); float exp = static_cast(GetBaseExp(maxlevel, moblevel)); @@ -5028,11 +5049,11 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) } // Per monster caps pulled from: https://ffxiclopedia.fandom.com/wiki/Experience_Points - if (PMember->GetMLevel() <= 50) + if (memberlevel <= 50) { exp = std::fmin(exp, 400.0f); } - else if (PMember->GetMLevel() <= 60) + else if (memberlevel <= 60) { exp = std::fmin(exp, 500.0f); } @@ -5073,27 +5094,27 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) } else { - if (PMember->GetMLevel() <= 10) + if (memberlevel <= 10) { PMember->expChain.chainTime = timer::now() + 50s; } - else if (PMember->GetMLevel() <= 20) + else if (memberlevel <= 20) { PMember->expChain.chainTime = timer::now() + 100s; } - else if (PMember->GetMLevel() <= 30) + else if (memberlevel <= 30) { PMember->expChain.chainTime = timer::now() + 150s; } - else if (PMember->GetMLevel() <= 40) + else if (memberlevel <= 40) { PMember->expChain.chainTime = timer::now() + 200s; } - else if (PMember->GetMLevel() <= 50) + else if (memberlevel <= 50) { PMember->expChain.chainTime = timer::now() + 250s; } - else if (PMember->GetMLevel() <= 60) + else if (memberlevel <= 60) { PMember->expChain.chainTime = timer::now() + 300s; } @@ -5104,7 +5125,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) PMember->expChain.chainNumber = 1; } - if (chainactive && PMember->GetMLevel() <= 10) + if (chainactive && memberlevel <= 10) { switch (PMember->expChain.chainNumber) { @@ -5131,7 +5152,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) break; } } - else if (chainactive && PMember->GetMLevel() <= 20) + else if (chainactive && memberlevel <= 20) { switch (PMember->expChain.chainNumber) { @@ -5158,7 +5179,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) break; } } - else if (chainactive && PMember->GetMLevel() <= 30) + else if (chainactive && memberlevel <= 30) { switch (PMember->expChain.chainNumber) { @@ -5185,7 +5206,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) break; } } - else if (chainactive && PMember->GetMLevel() <= 40) + else if (chainactive && memberlevel <= 40) { switch (PMember->expChain.chainNumber) { @@ -5212,7 +5233,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) break; } } - else if (chainactive && PMember->GetMLevel() <= 50) + else if (chainactive && memberlevel <= 50) { switch (PMember->expChain.chainNumber) { @@ -5239,7 +5260,7 @@ void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob) break; } } - else if (chainactive && PMember->GetMLevel() <= 60) + else if (chainactive && memberlevel <= 60) { switch (PMember->expChain.chainNumber) { diff --git a/src/map/utils/charutils.h b/src/map/utils/charutils.h index baaea045909..c9e61a88472 100644 --- a/src/map/utils/charutils.h +++ b/src/map/utils/charutils.h @@ -99,6 +99,7 @@ EMobDifficulty CheckMob(uint8 charlvl, CBattleEntity* PMob); uint32 GetBaseExp(uint8 charlvl, int16 moblvl); uint32 GetExpNEXTLevel(uint8 charlvl); +uint8 GetExpLevel(CBattleEntity* PMember); void DelExperiencePoints(CCharEntity* PChar, float retainpct, uint16 forcedXpLoss); void DistributeExperiencePoints(CCharEntity* PChar, CMobEntity* PMob);