From 1f8c0d05d8ebf538c2d8a318031577261bebe51a Mon Sep 17 00:00:00 2001 From: Critical <48370698+CriticalXI@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:25:11 -0700 Subject: [PATCH] [lua, sql, c++] Ranger job adjustments --- scripts/actions/abilities/flashy_shot.lua | 20 ++++ scripts/actions/abilities/stealth_shot.lua | 20 ++++ scripts/effects/barrage.lua | 5 +- scripts/effects/camouflage.lua | 9 +- scripts/effects/flashy_shot.lua | 4 + scripts/effects/sharpshot.lua | 8 +- scripts/effects/stealth_shot.lua | 1 + scripts/effects/unlimited_shot.lua | 6 +- scripts/enum/mod.lua | 5 +- scripts/globals/combat/physical_utilities.lua | 5 + scripts/globals/job_utils/ranger.lua | 91 ++++++++++++------- scripts/globals/weaponskills.lua | 3 + sql/merits.sql | 2 +- src/map/entities/charentity.cpp | 10 +- src/map/modifier.h | 5 +- 15 files changed, 133 insertions(+), 61 deletions(-) create mode 100644 scripts/actions/abilities/flashy_shot.lua create mode 100644 scripts/actions/abilities/stealth_shot.lua diff --git a/scripts/actions/abilities/flashy_shot.lua b/scripts/actions/abilities/flashy_shot.lua new file mode 100644 index 00000000000..abe6703ae21 --- /dev/null +++ b/scripts/actions/abilities/flashy_shot.lua @@ -0,0 +1,20 @@ +----------------------------------- +-- Ability: Flashy Shot +-- Next attack will generate more enmity and ignore level difference penalties. +-- Obtained: Ranger Level 75 +-- Recast Time: 10:00 +-- Duration: 1:00 +-- Target: Self Only +----------------------------------- +---@type TAbility +local abilityObject = {} + +abilityObject.onAbilityCheck = function(player, target, ability) + return xi.job_utils.ranger.checkFlashyShot(player, target, ability) +end + +abilityObject.onUseAbility = function(player, target, ability, action) + return xi.job_utils.ranger.useFlashyShot(player, target, ability, action) +end + +return abilityObject diff --git a/scripts/actions/abilities/stealth_shot.lua b/scripts/actions/abilities/stealth_shot.lua new file mode 100644 index 00000000000..93f1f476300 --- /dev/null +++ b/scripts/actions/abilities/stealth_shot.lua @@ -0,0 +1,20 @@ +----------------------------------- +-- Ability: Stealth Shot +-- Your next attack will generate less enmity. +-- Obtained: Ranger Level 75 +-- Recast Time: 5:00 +-- Duration: 1:00 +-- Target: Self Only +----------------------------------- +---@type TAbility +local abilityObject = {} + +abilityObject.onAbilityCheck = function(player, target, ability) + return xi.job_utils.ranger.checkStealthShot(player, target, ability) +end + +abilityObject.onUseAbility = function(player, target, ability, action) + return xi.job_utils.ranger.useStealthShot(player, target, ability, action) +end + +return abilityObject diff --git a/scripts/effects/barrage.lua b/scripts/effects/barrage.lua index d05dd2c0670..359fe7bbc86 100644 --- a/scripts/effects/barrage.lua +++ b/scripts/effects/barrage.lua @@ -7,16 +7,13 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) local jpValue = target:getJobPointLevel(xi.jp.BARRAGE_EFFECT) - target:addMod(xi.mod.RATT, jpValue * 3) + effect:addMod(xi.mod.RATT, jpValue * 3) end effectObject.onEffectTick = function(target, effect) end effectObject.onEffectLose = function(target, effect) - local jpValue = target:getJobPointLevel(xi.jp.BARRAGE_EFFECT) - - target:delMod(xi.mod.RATT, jpValue * 3) end return effectObject diff --git a/scripts/effects/camouflage.lua b/scripts/effects/camouflage.lua index 6d20b02f146..d6877d20f6c 100644 --- a/scripts/effects/camouflage.lua +++ b/scripts/effects/camouflage.lua @@ -7,18 +7,15 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) local jpValue = target:getJobPointLevel(xi.jp.CAMOUFLAGE_EFFECT) - target:addMod(xi.mod.ENMITY, -25) - target:addMod(xi.mod.CRITHITRATE, jpValue) + effect:addMod(xi.mod.ENMITY, -25) + effect:addMod(xi.mod.CRITHITRATE, jpValue) + effect:addMod(xi.mod.RETAIN_CAMOUFLAGE, 1) end effectObject.onEffectTick = function(target, effect) end effectObject.onEffectLose = function(target, effect) - local jpValue = target:getJobPointLevel(xi.jp.CAMOUFLAGE_EFFECT) - - target:delMod(xi.mod.ENMITY, -25) - target:delMod(xi.mod.CRITHITRATE, jpValue) end return effectObject diff --git a/scripts/effects/flashy_shot.lua b/scripts/effects/flashy_shot.lua index 38d97b86fef..15845a4dd2a 100644 --- a/scripts/effects/flashy_shot.lua +++ b/scripts/effects/flashy_shot.lua @@ -5,6 +5,10 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) + local boost = target:getMerit(xi.merit.FLASHY_SHOT) + + effect:addMod(xi.mod.RATTP, boost) + effect:addMod(xi.mod.ENMITY, 50) end effectObject.onEffectTick = function(target, effect) diff --git a/scripts/effects/sharpshot.lua b/scripts/effects/sharpshot.lua index 924efa434e1..0196f4c1caf 100644 --- a/scripts/effects/sharpshot.lua +++ b/scripts/effects/sharpshot.lua @@ -7,18 +7,14 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) local jpValue = target:getJobPointLevel(xi.jp.SHARPSHOT_EFFECT) - target:addMod(xi.mod.RACC, effect:getPower()) - target:addMod(xi.mod.RATT, jpValue * 2) + effect:addMod(xi.mod.RACC, effect:getPower()) + effect:addMod(xi.mod.RATT, jpValue * 2) end effectObject.onEffectTick = function(target, effect) end effectObject.onEffectLose = function(target, effect) - local jpValue = target:getJobPointLevel(xi.jp.SHARPSHOT_EFFECT) - - target:delMod(xi.mod.RACC, effect:getPower()) - target:delMod(xi.mod.RATT, jpValue * 2) end return effectObject diff --git a/scripts/effects/stealth_shot.lua b/scripts/effects/stealth_shot.lua index a969ae6f4de..833916cfb07 100644 --- a/scripts/effects/stealth_shot.lua +++ b/scripts/effects/stealth_shot.lua @@ -5,6 +5,7 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) + effect:addMod(xi.mod.ENMITY, -target:getMerit(xi.merit.STEALTH_SHOT)) end effectObject.onEffectTick = function(target, effect) diff --git a/scripts/effects/unlimited_shot.lua b/scripts/effects/unlimited_shot.lua index c5eba432a09..3d5fdce6be4 100644 --- a/scripts/effects/unlimited_shot.lua +++ b/scripts/effects/unlimited_shot.lua @@ -7,16 +7,14 @@ local effectObject = {} effectObject.onEffectGain = function(target, effect) local jpValue = target:getJobPointLevel(xi.jp.UNLIMITED_SHOT_EFFECT) - target:addMod(xi.mod.ENMITY, -2 * jpValue) + effect:addMod(xi.mod.ENMITY, -2 * jpValue) + effect:addMod(xi.mod.RETAIN_UNLIMITED_SHOT, 1) end effectObject.onEffectTick = function(target, effect) end effectObject.onEffectLose = function(target, effect) - local jpValue = target:getJobPointLevel(xi.jp.UNLIMITED_SHOT_EFFECT) - - target:delMod(xi.mod.ENMITY, -2 * jpValue) end return effectObject diff --git a/scripts/enum/mod.lua b/scripts/enum/mod.lua index c0c1d4403a3..a4ae848cf9c 100644 --- a/scripts/enum/mod.lua +++ b/scripts/enum/mod.lua @@ -523,7 +523,10 @@ xi.mod = SHIELD_BARRIER = 1082, -- Grants a bonus to Protect spells cast by self while a shield is equipped. -- Ranger - BOUNTY_SHOT_TH_BONUS = 826, -- Boosts base TH level of bounty shot + BOUNTY_SHOT_TH_BONUS = 826, -- Boosts base TH level of bounty shot + RETAIN_CAMOUFLAGE = 1189, -- Camouflage may be retained after ranged attacks + RETAIN_UNLIMITED_SHOT = 1190, -- Unlimited Shot is retained if the ranged attack misses + RA_IGNORE_LVL_DIFF = 1191, -- Ranged attacks ignore pDIF level correction penalty -- Dark Knight ARCANE_CIRCLE_DURATION = 858, -- Arcane Circle extended duration in seconds diff --git a/scripts/globals/combat/physical_utilities.lua b/scripts/globals/combat/physical_utilities.lua index 337c7c623bb..498d43712fc 100644 --- a/scripts/globals/combat/physical_utilities.lua +++ b/scripts/globals/combat/physical_utilities.lua @@ -738,6 +738,11 @@ xi.combat.physical.calculateRangedPDIF = function(actor, target, weaponType, wsA ---------------------------------------- local levelDifFactor = 0 + -- Mod-based bypass for ranged level correction + if actor:isPC() and actor:getMod(xi.mod.RA_IGNORE_LVL_DIFF) > 0 then + applyLevelCorrection = false + end + if applyLevelCorrection then levelDifFactor = (actor:getMainLvl() - target:getMainLvl()) * 0.025 end diff --git a/scripts/globals/job_utils/ranger.lua b/scripts/globals/job_utils/ranger.lua index 242417ffaf2..0327d3393c9 100644 --- a/scripts/globals/job_utils/ranger.lua +++ b/scripts/globals/job_utils/ranger.lua @@ -5,6 +5,31 @@ xi = xi or {} xi.job_utils = xi.job_utils or {} xi.job_utils.ranger = xi.job_utils.ranger or {} +----------------------------------- +-- Helper Functions +----------------------------------- + +-- TODO: Remove this logic when Fire and Brimstone quest is converted to IF +xi.job_utils.ranger.tryScavengeQuestItem = function(player) + local fireAndBrimstoneCS = player:getCharVar('fireAndBrimstone') + + if + player:getZoneID() == xi.zone.CASTLE_OZTROJA and + fireAndBrimstoneCS == 5 and + not player:hasItem(xi.item.OLD_EARRING) and + player:getYPos() > -43 and player:getYPos() < -38 and + player:getXPos() > -85 and player:getXPos() < -73 and + player:getZPos() > -85 and player:getZPos() < -75 and + math.random(1, 100) <= 50 + then + npcUtil.giveItem(player, xi.item.OLD_EARRING) + + return true + end + + return false +end + ----------------------------------- -- Ability Check Functions ----------------------------------- @@ -128,6 +153,7 @@ xi.job_utils.ranger.useEagleEyeShot = function(player, target, ability, action) local params = {} params.numHits = 1 + params.ignoreShadows = true -- Eagle Eye Shot bypasses Utsusemi and Blink -- TP params. local tp = 1000 -- to ensure ftp multiplier is applied @@ -176,50 +202,40 @@ end xi.job_utils.ranger.useScavenge = function(player, target, ability, action) -- RNG AF2 quest check - local fireAndBrimstoneCS = player:getCharVar('fireAndBrimstone') + if xi.job_utils.ranger.tryScavengeQuestItem(player) then + return + end - if - player:getZoneID() == xi.zone.CASTLE_OZTROJA and fireAndBrimstoneCS == 5 and-- zone + quest match - not player:hasItem(xi.item.OLD_EARRING) and -- make sure player doesn't already have the earring - player:getYPos() > -43 and player:getYPos() < -38 and -- Y match - player:getXPos() > -85 and player:getXPos() < -73 and -- X match - player:getZPos() > -85 and player:getZPos() < -75 and -- Z match - math.random(1, 100) <= 50 - then - npcUtil.giveItem(player, xi.item.OLD_EARRING) + local bonuses = (player:getMod(xi.mod.SCAVENGE_EFFECT) + player:getMerit(xi.merit.SCAVENGE_EFFECT)) / 100 + local arrowsToReturn = math.floor(math.floor(player:getLocalVar('ArrowsUsed') % 10000) * (player:getMainLvl() / 200 + bonuses)) + local playerID = target:getID() + if arrowsToReturn == 0 then + action:messageID(playerID, 139) else - local bonuses = (player:getMod(xi.mod.SCAVENGE_EFFECT) + player:getMerit(xi.merit.SCAVENGE_EFFECT)) / 100 - local arrowsToReturn = math.floor(math.floor(player:getLocalVar('ArrowsUsed') % 10000) * (player:getMainLvl() / 200 + bonuses)) - local playerID = target:getID() - - if arrowsToReturn == 0 then - action:messageID(playerID, 139) - else - if arrowsToReturn > 99 then - arrowsToReturn = 99 - end - - local arrowID = math.floor(player:getLocalVar('ArrowsUsed') / 10000) - player:addItem(arrowID, arrowsToReturn) + if arrowsToReturn > 99 then + arrowsToReturn = 99 + end - if arrowsToReturn == 1 then - action:messageID(playerID, 140) - else - action:messageID(playerID, 674) - action:additionalEffect(playerID, 1) - action:addEffectParam(playerID, arrowsToReturn) - end + local arrowID = math.floor(player:getLocalVar('ArrowsUsed') / 10000) + player:addItem(arrowID, arrowsToReturn) - player:setLocalVar('ArrowsUsed', 0) - return arrowID + if arrowsToReturn == 1 then + action:messageID(playerID, 140) + else + action:messageID(playerID, 674) + action:additionalEffect(playerID, 1) + action:addEffectParam(playerID, arrowsToReturn) end + + player:setLocalVar('ArrowsUsed', 0) + return arrowID end end xi.job_utils.ranger.useCamouflage = function(player, target, ability, action) local duration = math.random(30, 300) * (1 + 0.01 * player:getMod(xi.mod.CAMOUFLAGE_DURATION)) - player:addStatusEffect(xi.effect.CAMOUFLAGE, 1 , 0, math.floor(duration * xi.settings.main.SNEAK_INVIS_DURATION_MULTIPLIER)) + player:addStatusEffect(xi.effect.CAMOUFLAGE, 1, 0, math.floor(duration * xi.settings.main.SNEAK_INVIS_DURATION_MULTIPLIER)) return xi.effect.CAMOUFLAGE end @@ -262,11 +278,16 @@ xi.job_utils.ranger.useUnlimitedShot = function(player, target, ability, action) end xi.job_utils.ranger.useFlashyShot = function(player, target, ability, action) - return 0, 0 -- Not implemented yet + -- TODO: Flashy Shot should add "D" damage to the next ranged attack + player:addStatusEffect(xi.effect.FLASHY_SHOT, 1, 0, 60) + + return xi.effect.FLASHY_SHOT end xi.job_utils.ranger.useStealthShot = function(player, target, ability, action) - return 0, 0 -- Not implemented yet + player:addStatusEffect(xi.effect.STEALTH_SHOT, 1, 0, 60) + + return xi.effect.STEALTH_SHOT end xi.job_utils.ranger.useDoubleShot = function(player, target, ability, action) diff --git a/scripts/globals/weaponskills.lua b/scripts/globals/weaponskills.lua index 716a3aafd9f..ade633e8ca4 100644 --- a/scripts/globals/weaponskills.lua +++ b/scripts/globals/weaponskills.lua @@ -152,6 +152,7 @@ local function getSingleHitDamage(attacker, target, dmg, ftp, wsParams, calcPara -- check shadows if not calcParams.guaranteedHit and + not wsParams.ignoreShadows and shadowAbsorb(target) then -- shadow absorb logic @@ -778,6 +779,8 @@ xi.weaponskills.doRangedWeaponskill = function(attacker, target, wsID, wsParams, -- Delete statuses that may have been spent by the WS attacker:delStatusEffectsByFlag(xi.effectFlag.DETECTABLE) + attacker:delStatusEffect(xi.effect.FLASHY_SHOT) + attacker:delStatusEffect(xi.effect.STEALTH_SHOT) -- Calculate reductions finaldmg = target:rangedDmgTaken(finaldmg) diff --git a/sql/merits.sql b/sql/merits.sql index 666180ec2f3..c8214fb2a80 100644 --- a/sql/merits.sql +++ b/sql/merits.sql @@ -256,7 +256,7 @@ INSERT INTO `merits` VALUES (2630,'adventurers_dirge',5,10,512,7,40); INSERT INTO `merits` VALUES (2632,'con_anima',5,1,512,7,40); INSERT INTO `merits` VALUES (2634,'con_brio',5,1,512,7,40); INSERT INTO `merits` VALUES (2688,'stealth_shot',5,10,1024,7,41); -INSERT INTO `merits` VALUES (2690,'flashy_shot',5,1,1024,7,41); +INSERT INTO `merits` VALUES (2690,'flashy_shot',5,5,1024,7,41); INSERT INTO `merits` VALUES (2692,'snapshot',5,2,1024,7,41); INSERT INTO `merits` VALUES (2694,'recycle',5,5,1024,7,41); INSERT INTO `merits` VALUES (2752,'shikikoyo',5,12,2048,7,42); diff --git a/src/map/entities/charentity.cpp b/src/map/entities/charentity.cpp index b33b80d5907..beb426e06a1 100644 --- a/src/map/entities/charentity.cpp +++ b/src/map/entities/charentity.cpp @@ -2047,13 +2047,17 @@ void CCharEntity::OnRangedAttack(CRangeState& state, action_t& action) { // Never consume ammo with Unlimited Shot active recycleChance = 100; - // Only remove unlimited shot on hit - if (hitOccured) + // Remove unlimited shot unless retained on miss via RETAIN_UNLIMITED_SHOT mod + if (hitOccured || this->getMod(Mod::RETAIN_UNLIMITED_SHOT) <= 0) { StatusEffectContainer->DelStatusEffect(EFFECT_UNLIMITED_SHOT); } } + // Flashy Shot / Stealth Shot: Consumed after the next ranged attack + StatusEffectContainer->DelStatusEffect(EFFECT_FLASHY_SHOT); + StatusEffectContainer->DelStatusEffect(EFFECT_STEALTH_SHOT); + if (PAmmo != nullptr && xirand::GetRandomNumber(100) > recycleChance) { ++ammoConsumed; @@ -2158,7 +2162,7 @@ void CCharEntity::OnRangedAttack(CRangeState& state, action_t& action) battleutils::RemoveAmmo(this, ammoConsumed); // Handle Camouflage effects - if (this->StatusEffectContainer->HasStatusEffect(EFFECT_CAMOUFLAGE, 0)) + if (getMod(Mod::RETAIN_CAMOUFLAGE) > 0) { int16 retainChance = 40; // Estimate base ~40% chance to keep Camouflage on a ranged attack uint8 rotAllowance = 25; // Allow for some slight variance in direction faced to be "behind" or "beside" the mob diff --git a/src/map/modifier.h b/src/map/modifier.h index f4075ad4200..eceaf3ee10b 100644 --- a/src/map/modifier.h +++ b/src/map/modifier.h @@ -599,6 +599,9 @@ enum class Mod TRUE_SHOT_EFFECT = 1053, // TODO: True Shot Ranged Damage increase (percent) DEAD_AIM_EFFECT = 1054, // TODO: Dead Aim Critical Damage increase (percent) BOUNTY_SHOT_TH_BONUS = 826, // Boosts base TH level of bounty shot + RETAIN_CAMOUFLAGE = 1189, // Enables retaining Camouflage after using a ranged attack + RETAIN_UNLIMITED_SHOT = 1190, // Unlimited Shot is retained if the ranged attack misses + RA_IGNORE_LVL_DIFF = 1191, // Ranged attacks ignore pDIF level correction penalty // Samurai WARDING_CIRCLE_DURATION = 95, // Warding Circle extended duration in seconds @@ -1143,7 +1146,7 @@ enum class Mod // The spares take care of finding the next ID to use so long as we don't forget to list IDs that have been freed up by refactoring. // 570 through 825 used by WS DMG mods these are not spares. // - // SPARE IDs: 1189 and onward + // SPARE IDs: 1192 and onward }; // temporary workaround for using enum class as unordered_map key until compilers support it