diff --git a/scripts/effects/visitant.lua b/scripts/effects/visitant.lua index 764bd9f72ba..230892a0acc 100644 --- a/scripts/effects/visitant.lua +++ b/scripts/effects/visitant.lua @@ -1,11 +1,41 @@ ----------------------------------- -- xi.effect.VISITANT ----------------------------------- +require('scripts/globals/abyssea') ---@type TEffect local effectObject = {} local remainingTimeLimits = { 300, 240, 180, 120, 60, 30, 10, 5, 4, 3, 2, 1 } +local function getGoldenExpBonusPct(player) + local cap = 200 + local lightTable = xi.abyssea.getLightsTable(player) + local golden = math.min(lightTable[xi.abyssea.lightType.GOLDEN] or 0, cap) + local ebon = math.min(lightTable[xi.abyssea.lightType.EBON] or 0, cap) + + local goldenBonus = (golden / cap) * 50 + local ebonFactor = 1 + (ebon / cap) + + return math.floor(goldenBonus * ebonFactor) +end + +local function updateGoldenExpBonus(player) + local applied = player:getLocalVar('abysseaGoldenExpBonusApplied') + local newBonus = getGoldenExpBonusPct(player) + + if applied ~= newBonus then + if applied > 0 then + player:delMod(xi.mod.EXP_BONUS, applied) + end + + if newBonus > 0 then + player:addMod(xi.mod.EXP_BONUS, newBonus) + end + + player:setLocalVar('abysseaGoldenExpBonusApplied', newBonus) + end +end + -- NOTE: Update the last local reportTimeRemaining reportTimeRemaining = function(player, effect) @@ -74,6 +104,7 @@ effectObject.onEffectGain = function(target, effect) effect:addEffectFlag(xi.effectFlag.HIDE_TIMER) target:setLocalVar('lastTimeUpdate', effect:getTimeRemaining() / 1000 + 1) + updateGoldenExpBonus(target) end effectObject.onEffectTick = function(target, effect) @@ -87,6 +118,8 @@ effectObject.onEffectTick = function(target, effect) xi.abyssea.searingWardTimer(target) end + updateGoldenExpBonus(target) + -- Handle Time Remaining Messages. This will no longer be called if the time -- remaining is less that 30s, as then we move to timers set on the player to -- ensure that they're displayed at the appropriate timings. @@ -119,9 +152,20 @@ effectObject.onEffectLose = function(target, effect) target:setCharVar('abysseaTimeStored', timeRemaining) end + local appliedGoldenExpBonus = target:getLocalVar('abysseaGoldenExpBonusApplied') + if appliedGoldenExpBonus > 0 then + target:delMod(xi.mod.EXP_BONUS, appliedGoldenExpBonus) + target:setLocalVar('abysseaGoldenExpBonusApplied', 0) + end + -- Reset Abyssea Lights target:setCharVar('abysseaLights1', 0) target:setCharVar('abysseaLights2', 0) + + -- Reset Cruor reverse-chain progress when visitant status is lost. + target:setCharVar('AbysseaCruorChainCount', 0) + target:setCharVar('AbysseaCruorLastIdentity', 0) + target:setCharVar('AbysseaCruorLastKillTime', 0) end return effectObject diff --git a/scripts/globals/abyssea.lua b/scripts/globals/abyssea.lua index 339a996235f..320687993fb 100644 --- a/scripts/globals/abyssea.lua +++ b/scripts/globals/abyssea.lua @@ -1056,7 +1056,7 @@ local function setLightsFromTable(player, lightTable) if k <= 4 then lightMaskFirst = lightMaskFirst + bit.lshift(lightTable[k], (k - 1) * 8) else - lightMaskSecond = lightMaskSecond + bit.lshift(lightTable[k], (k - 1) * 8) + lightMaskSecond = lightMaskSecond + bit.lshift(lightTable[k], (k - 5) * 8) end end @@ -1126,7 +1126,9 @@ xi.abyssea.addPlayerLights = function(player, light, amount) end xi.abyssea.getLightValue = function(player, light) - return bit.band(bit.rshift(player:getCharVar('abysseaLights'), (light - 1) * 2), 0xFF) + local lightTable = xi.abyssea.getLightsTable(player) + + return lightTable[light] or 0 end xi.abyssea.canEnterAbyssea = function(player) diff --git a/scripts/globals/abyssea/cruor.lua b/scripts/globals/abyssea/cruor.lua new file mode 100644 index 00000000000..4272cfae1c5 --- /dev/null +++ b/scripts/globals/abyssea/cruor.lua @@ -0,0 +1,155 @@ +----------------------------------- +-- Abyssea Cruor Rewards +----------------------------------- +require('scripts/globals/abyssea') +xi = xi or {} +xi.abyssea = xi.abyssea or {} +xi.abyssea.cruor = xi.abyssea.cruor or {} + +local function getConfig(name, default) + local value = xi.settings.main[name] + if value == nil then + return default + end + + return value +end + +local function getLightBonus(player) + local silverCap = getConfig('ABYSSEA_CRUOR_SILVER_CAP', 200) + local ebonCap = getConfig('ABYSSEA_CRUOR_EBON_CAP', 200) + local silver = math.min(xi.abyssea.getLightValue(player, xi.abyssea.lightType.SILVERY) or 0, silverCap) + local ebon = math.min(xi.abyssea.getLightValue(player, xi.abyssea.lightType.EBON) or 0, ebonCap) + + local silverMaxBonus = getConfig('ABYSSEA_CRUOR_SILVER_MAX_BONUS', 0.6) + local ebonAmplifier = getConfig('ABYSSEA_CRUOR_EBON_SILVER_AMPLIFIER', 1.0) + + local silverRatio = silver / silverCap + local ebonRatio = ebon / ebonCap + + return 1 + silverRatio * silverMaxBonus * (1 + ebonRatio * ebonAmplifier) +end + +local function getIdentity(mob) + local identityMode = getConfig('ABYSSEA_CRUOR_CHAIN_IDENTITY_MODE', 'pool') + + if identityMode == 'name' then + return mob:getName() + end + + return mob:getPool() +end + +local function getFamilyModifier(mob) + local modifier = 1.0 + + -- Ephemeral mobs are documented by community resources as high Cruor/EXP outliers. + if string.find(mob:getName(), 'Ephemeral_', 1, true) then + modifier = modifier * getConfig('ABYSSEA_CRUOR_EPHEMERAL_MULTIPLIER', 3.0) + end + + return modifier +end + +local function getBaseCruor(mob) + if mob:isNM() then + local size = mob:getModelSize() or 0 + local smallThreshold = getConfig('ABYSSEA_CRUOR_NM_SMALL_SIZE', 4) + local mediumThreshold = getConfig('ABYSSEA_CRUOR_NM_MEDIUM_SIZE', 7) + + if size <= smallThreshold then + return getConfig('ABYSSEA_CRUOR_NM_BASE_T1', 50) + elseif size <= mediumThreshold then + return getConfig('ABYSSEA_CRUOR_NM_BASE_T2', 65) + end + + return getConfig('ABYSSEA_CRUOR_NM_BASE_T3', 80) + end + + local levelStep = getConfig('ABYSSEA_CRUOR_LEVEL_STEP', 0) + local minBase = getConfig('ABYSSEA_CRUOR_KILL_BASE_MIN', 10) + local maxBase = getConfig('ABYSSEA_CRUOR_KILL_BASE_MAX', 20) + + if maxBase < minBase then + maxBase = minBase + end + + local randomizedBase = math.random(minBase, maxBase) + + return math.max(1, math.floor((randomizedBase + mob:getMainLvl() * levelStep) * getFamilyModifier(mob))) +end + +local function getEligibleMembers(killer) + local members = {} + + for _, member in pairs(killer:getAlliance()) do + local visitantEffect = member:getStatusEffect(xi.effect.VISITANT) + if + member:isPC() and + member:getZoneID() == killer:getZoneID() and + visitantEffect and + visitantEffect:getIcon() == xi.effect.VISITANT + then + table.insert(members, member) + end + end + + return members +end + +local function getKiller(player) + if player and not player:isPC() and player:getAllegiance() == 1 then + local master = player:getMaster() + if master then + return master + end + end + + return player +end + +xi.abyssea.cruor.onMobDefeat = function(killer, mob) + killer = getKiller(killer) + if not killer or not killer:isPC() then + return + end + + local timeout = getConfig('ABYSSEA_CRUOR_CHAIN_TIMEOUT_SEC', 0) + local chainStep = getConfig('ABYSSEA_CRUOR_CHAIN_STEP', 9) + local chainCap = getConfig('ABYSSEA_CRUOR_CHAIN_CAP', 180) + local reaperStep = getConfig('ABYSSEA_CRUOR_REAPER_BONUS_STEP', 0.2) + + local now = GetSystemTime() + local identity = tostring(getIdentity(mob)) + + for _, member in pairs(getEligibleMembers(killer)) do + local lastIdentity = tostring(member:getCharVar('AbysseaCruorLastIdentity')) + local lastKillTime = member:getCharVar('AbysseaCruorLastKillTime') + local chainCount = member:getCharVar('AbysseaCruorChainCount') + + if timeout > 0 and lastKillTime > 0 and now - lastKillTime > timeout then + chainCount = 0 + end + + if lastIdentity == identity then + chainCount = 0 + else + chainCount = chainCount + 1 + end + + local baseCruor = getBaseCruor(mob) + local chainBonus = math.min(chainCount * chainStep, chainCap) + local reaperBonus = 1 + xi.abyssea.getAbyssiteTotal(member, xi.abyssea.abyssiteType.THE_REAPER) * reaperStep + local lightBonus = getLightBonus(member) + local cruorReward = math.floor((baseCruor + chainBonus) * reaperBonus * lightBonus) + + member:addCurrency('cruor', cruorReward) + member:messageSpecial(zones[member:getZoneID()].text.CRUOR_OBTAINED, cruorReward) + + member:setCharVar('AbysseaCruorChainCount', chainCount) + member:setCharVar('AbysseaCruorLastIdentity', identity) + member:setCharVar('AbysseaCruorLastKillTime', now) + end +end + +return xi.abyssea.cruor diff --git a/scripts/globals/abyssea/lights.lua b/scripts/globals/abyssea/lights.lua index facfffc7a79..30bdbdb80c9 100644 --- a/scripts/globals/abyssea/lights.lua +++ b/scripts/globals/abyssea/lights.lua @@ -2,6 +2,7 @@ -- Abyssea Lights Global ----------------------------------- require('scripts/globals/abyssea') +require('scripts/globals/abyssea/cruor') ----------------------------------- xi = xi or {} xi.abyssea = xi.abyssea or {} @@ -523,6 +524,12 @@ local lightTypes = [xi.abyssea.deathType.WS_MAGICAL] = { light = xi.abyssea.lightType.AMBER, lightType = 'amber' }, -- Amber } +local function isCruorKillRewardsEnabled() + local value = xi.settings.main.ABYSSEA_ENABLE_CRUOR_KILL_REWARDS + + return value == true or value == 1 +end + xi.abyssea.RemoveDeathListeners = function(mob) mob:removeListener('ABYSSEA_PHYSICAL_DEATH_CHECK') mob:removeListener('ABYSSEA_MAGIC_DEATH_CHECK') @@ -571,6 +578,10 @@ xi.abyssea.AddDeathListeners = function(mob) deathType = xi.abyssea.deathType.PHYSICAL end + if isCruorKillRewardsEnabled() then + xi.abyssea.cruor.onMobDefeat(player, mobArg) + end + xi.abyssea.DropLights(player, mobArg:getName(), deathType, mobArg) xi.abyssea.RemoveDeathListeners(mobArg) diff --git a/settings/default/main.lua b/settings/default/main.lua index 152b03246a9..b66db486e5e 100644 --- a/settings/default/main.lua +++ b/settings/default/main.lua @@ -109,6 +109,29 @@ xi.settings.main = -- recomended amount 0 - 100, some lights will cap at 255 while others are less, these are capped automatically ABYSSEA_BONUSLIGHT_AMOUNT = 0, + -- Abyssea cruor kill rewards enable toggle (true/1 = enabled or false/0 = disabled) + ABYSSEA_ENABLE_CRUOR_KILL_REWARDS = 1, -- Enable or disable cruor rewards in Abyssea zones + -- Base cruor implementation: reward = floor((base + chainBonus) * (silver effect * (ebon bonus)) * reaperBonus) + ABYSSEA_CRUOR_KILL_BASE_MIN = 10, -- Minimum of randomized base cruor reward + ABYSSEA_CRUOR_KILL_BASE_MAX = 20, -- Maximum of randomized base cruor reward + ABYSSEA_CRUOR_LEVEL_STEP = 0, -- Set > 0 only if your server wants explicit level-based scaling for cruor rewards + ABYSSEA_CRUOR_CHAIN_STEP = 9, -- This is the bonus earned each time a mob of a different family from the last is defeated + ABYSSEA_CRUOR_CHAIN_CAP = 180, -- This is the maximum chain bonus value + ABYSSEA_CRUOR_CHAIN_TIMEOUT_SEC = 0, -- 0 disables timeout; reverse-chain persists until same family/name kill or visitant loss (retail-like) + ABYSSEA_CRUOR_CHAIN_IDENTITY_MODE = 'pool', -- accepts pool|name for identifying valid targets to maintain the chain, default pool (retail-like) + ABYSSEA_CRUOR_REAPER_BONUS_STEP = 0.1, -- Multiplier at the end of the calculation; eg. 0.2 = 120% and 0.15 = 115%. Default (0.1) + ABYSSEA_CRUOR_EPHEMERAL_MULTIPLIER = 3.0, -- Ephemeral mob cruor multiplier, multiplies the result of the randomized base before chain, lights, and atma bonuses + + ABYSSEA_CRUOR_SILVER_CAP = 200, -- Maximum silver light; determines how rapidly you can reach the cap and effectiveness per light + ABYSSEA_CRUOR_EBON_CAP = 200, -- Maximum ebon light; determines how rapidly you can reach the cap and effectiveness per light + ABYSSEA_CRUOR_SILVER_MAX_BONUS = 0.6, -- Maximum bonus multiplier to base+chain cruor; 0.6 = 1.6 multiplier (160%) + ABYSSEA_CRUOR_EBON_SILVER_AMPLIFIER = 1.0, -- Maximum bonus multiplier to silver light multiplier; 1.0 doubles silver effectiveness at cap (0.6 -> 1.2) + ABYSSEA_CRUOR_NM_BASE_T1 = 50, -- Cruor base reward for T1 NM sizes (default 0-4, set by ABYSSEA_CRUOR_NM_SMALL_SIZE) + ABYSSEA_CRUOR_NM_BASE_T2 = 65, -- Cruor base reward for T2 NM sizes (default 5-7, set by ABYSSEA_CRUOR_NM_MEDIUM_SIZE) + ABYSSEA_CRUOR_NM_BASE_T3 = 80, -- Cruor base reward for T3 NM sizes (default 8+, will always be everything larger than ABYSSEA_CRUOR_NM_MEDIUM_SIZE) + ABYSSEA_CRUOR_NM_SMALL_SIZE = 4, -- Determines maximum small size threshold for NM rewards + ABYSSEA_CRUOR_NM_MEDIUM_SIZE = 7, -- Determines maximum medium size threshold for NM rewards + -- CHARACTER CONFIG INITIAL_LEVEL_CAP = 50, -- The initial level cap for new players. There seems to be a hardcap of 255. MAX_LEVEL = 99, -- Level max of the server, lowers the attainable cap by disabling Limit Break quests. diff --git a/sql/char_unlocks.sql b/sql/char_unlocks.sql index e17e123b5da..126c60b9714 100644 --- a/sql/char_unlocks.sql +++ b/sql/char_unlocks.sql @@ -13,7 +13,7 @@ CREATE TABLE `char_unlocks` ( `campaign_windy` int(10) unsigned NOT NULL DEFAULT 0, `homepoints` blob DEFAULT NULL, `survivals` blob DEFAULT NULL, - `traverser_start` TIMESTAMP DEFAULT 0, + `traverser_start` TIMESTAMP NULL DEFAULT NULL, `traverser_claimed` int(10) unsigned NOT NULL DEFAULT 0, `abyssea_conflux` blob DEFAULT NULL, `waypoints` blob DEFAULT NULL,