Skip to content
3 changes: 3 additions & 0 deletions changelog/snippets/fix.6457.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- (#6457) Fix weapon projectiles dealing damage over time over a shorter duration than given by the `DoTTime` stat.
- `DoTTime` for FAF units is adjusted to the old actual DoT duration, so balance is not changed.
- Unit databases will now show the actual damage over time duration.
87 changes: 56 additions & 31 deletions lua/sim/DefaultDamage.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,66 +14,90 @@ local DamageArea = DamageArea

-- cache for performance
local VectorCache = Vector(0, 0, 0)
local MathFloor = math.floor
local CoroutineYield = coroutine.yield
local MathMod = math.mod
local MATH_IRound = MATH_IRound
local WaitTicks = WaitTicks

local EntityBeenDestroyed = _G.moho.entity_methods.BeenDestroyed
local EntityGetPositionXYZ = _G.moho.entity_methods.GetPositionXYZ

--- Performs damage over time on a unit.
--- Performs damage over time on a target, waiting the interval *before* dealing damage.
---@param instigator Unit
---@param unit Unit
---@param pulses any
---@param pulseTime integer
---@param target Unit | Prop | Projectile
---@param pulses number
---@param pulseInterval number
---@param damage number
---@param damType DamageType
---@param friendly boolean
function UnitDoTThread (instigator, unit, pulses, pulseTime, damage, damType, friendly)

---@param damageType DamageType
function UnitDoTThread(instigator, target, pulses, pulseInterval, damage, damageType)
-- localize for performance
local position = VectorCache
local DamageArea = DamageArea
local CoroutineYield = CoroutineYield
local Damage = Damage
local EntityGetPositionXYZ = EntityGetPositionXYZ
local WaitTicks = WaitTicks
local MathMod = MathMod

-- convert time to ticks
pulseTime = 10 * pulseTime + 1
-- convert seconds to ticks, have to "wait" 1 extra tick to get to the end of the current tick
pulseInterval = 10 * pulseInterval + 1
-- accumulator to compensate for error caused by `WaitTicks` only working with integers
local accum = 0

for i = 1, pulses do
if unit and not EntityBeenDestroyed(unit) then
position[1], position[2], position[3] = EntityGetPositionXYZ(unit)
Damage(instigator, position, unit, damage, damType )
if target and not EntityBeenDestroyed(target) then
position[1], position[2], position[3] = EntityGetPositionXYZ(target)
Damage(instigator, position, target, damage, damageType)
else
break
end
CoroutineYield(pulseTime)
accum = accum + pulseInterval
if accum > 1 then
-- final accumulator value may be #.999 which needs to be rounded
if i == pulses then
WaitTicks(MATH_IRound(accum))
else
WaitTicks(accum)
accum = MathMod(accum, 1)
end
end
end
end

--- Performs damage over time in a given area.
--- Performs damage over time in a given area, waiting the interval *before* dealing damage.
---@param instigator Unit
---@param position Vector
---@param pulses number
---@param pulseTime number
---@param pulseInterval number
---@param radius number
---@param damage number
---@param damType DamageType
---@param friendly boolean
function AreaDoTThread (instigator, position, pulses, pulseTime, radius, damage, damType, friendly)

---@param damageType DamageType
---@param damageFriendly boolean
---@param damageSelf boolean
function AreaDoTThread(instigator, position, pulses, pulseInterval, radius, damage, damageType, damageFriendly, damageSelf)
-- localize for performance
local DamageArea = DamageArea
local CoroutineYield = CoroutineYield
local WaitTicks = WaitTicks
local MathMod = MathMod

-- compute ticks between pulses
pulseTime = 10 * pulseTime + 1
-- convert seconds to ticks, have to "wait" 1 extra tick to get to the end of the current tick
pulseInterval = 10 * pulseInterval + 1
-- accumulator to compensate for error caused by `WaitTicks` only working with integers
local accum = 0

for i = 1, pulses do
DamageArea(instigator, position, radius, damage, damType, friendly)
CoroutineYield(pulseTime)
accum = accum + pulseInterval
if accum > 1 then
-- final accumulator value may be #.999 which needs to be rounded
if i == pulses then
WaitTicks(MATH_IRound(accum))
else
WaitTicks(accum)
accum = MathMod(accum, 1)
end
end
DamageArea(instigator, position, radius, damage, damageType, damageFriendly, damageSelf)
end
end

-- Deprecated functionality --
--#region Deprecated functionality

-- SCALABLE RADIUS AREA DOT
-- - Allows for a scalable damage radius that begins with DamageStartRadius and ends
Expand Down Expand Up @@ -101,4 +125,5 @@ function ScalableRadiusAreaDoT(entity)
end
end
entity:Destroy()
end
end
--#endregion
131 changes: 51 additions & 80 deletions lua/sim/Projectile.lua
Original file line number Diff line number Diff line change
Expand Up @@ -612,16 +612,11 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) {
end,

--- Called by Lua to process the damage logic of a projectile
-- @param self The projectile itself
-- @param instigator The launcher, and if it doesn't exist, the projectile itself
-- @param DamageData The damage data passed by the weapon
-- @param targetEntity The entity we hit, is nil if we hit terrain
-- @param cachedPosition A cached position that is passed to prevent table allocations, can not be used in fork threads and / or after a yield statement
---@param self Projectile
---@param instigator Unit
---@param instigator Unit # The launcher, and if it doesn't exist, the projectile itself
---@param DamageData WeaponDamageTable # passed by the weapon
---@param targetEntity Unit | Prop | nil
---@param cachedPosition Vector
---@param targetEntity Unit | Prop | nil # nil if hitting terrain
---@param cachedPosition Vector # A cached position that is passed to prevent table allocations, can not be used in fork threads and / or after a yield statement
DoDamage = function(self, instigator, DamageData, targetEntity, cachedPosition)

-- this may be a cached vector, we can not send this to threads or use after waiting statements!
Expand All @@ -637,66 +632,63 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) {
local damageFriendly = DamageData.DamageFriendly
local damageSelf = DamageData.DamageSelf or false

-- check for damage-over-time
local DoTTime = DamageData.DoTTime
if DoTTime <= 0 then
-- no damage over time, do radius-based damage
-- do initial damage in a radius
DamageArea(
instigator,
cachedPosition,
radius,
damage + (DamageData.InitialDamageAmount or 0),
damageType,
damageFriendly,
damageSelf
)

local damageToShields = DamageData.DamageToShields
if damageToShields then
DamageArea(
instigator,
cachedPosition,
radius,
damage,
damageType,
damageToShields,
"FAF_AntiShield",
damageFriendly,
damageSelf
)
end

local damageToShields = DamageData.DamageToShields
if damageToShields then
DamageArea(
instigator,
cachedPosition,
radius,
damageToShields,
"FAF_AntiShield",
damageFriendly,
damageSelf
)
end
else
-- check for initial damage
local initialDmg = DamageData.InitialDamageAmount
if initialDmg > 0 then
DamageArea(
-- check for and deal damage over time
local DoTTime = DamageData.DoTTime
if DoTTime > 0 then
-- initial damage pulse was already dealt so subtract 1
local DoTPulses = DamageData.DoTPulses - 1
if DoTPulses >= 1 then
ForkThread(
AreaDoTThread,
instigator,
cachedPosition,
self:GetPosition(), -- can't use cachedPosition here: breaks invariant
DoTPulses,
(DoTTime / (DoTPulses)),
radius,
initialDmg,
damage,
damageType,
damageFriendly,
damageSelf
damageFriendly
)
end

-- apply damage over time
local DoTPulses = DamageData.DoTPulses or 1
ForkThread(
AreaDoTThread,
instigator,
self:GetPosition(), -- can't use cachedPosition here: breaks invariant
DoTPulses,
(DoTTime / (DoTPulses)),
radius,
damage,
damageType,
damageFriendly
)
end

-- damage a single entity
elseif targetEntity then
local damageType = DamageData.DamageType

-- do initial damage
Damage(
instigator,
cachedPosition,
targetEntity,
damage + (DamageData.InitialDamageAmount or 0),
damageType
)

local damageToShields = DamageData.DamageToShields
if damageToShields then
Damage(
Expand All @@ -708,43 +700,22 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) {
)
end

-- check for damage-over-time
-- check for and apply damage over time
local DoTTime = DamageData.DoTTime
if DoTTime <= 0 then

-- no damage over time, do single target damage
Damage(
instigator,
cachedPosition,
targetEntity,
damage,
damageType
)
else
-- check for initial damage
local initialDmg = DamageData.InitialDamageAmount or 0
if initialDmg > 0 then
Damage(
if DoTTime > 0 then
-- initial damage pulse was already dealt so subtract 1
local DoTPulses = DamageData.DoTPulses - 1
if DoTPulses >= 1 then
ForkThread(
UnitDoTThread,
instigator,
cachedPosition,
targetEntity,
initialDmg,
DoTPulses,
(DoTTime / (DoTPulses)),
damage,
damageType
)
end

-- apply damage over time
local DoTPulses = DamageData.DoTPulses or 1
ForkThread(
UnitDoTThread,
instigator,
targetEntity,
DoTPulses,
(DoTTime / (DoTPulses)),
damage,
damageType,
DamageData.DamageFriendly
)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion units/DAA0206/DAA0206_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Kamikaze",
DoTPulses = 20,
DoTTime = 10,
DoTTime = 9.5,
EffectiveRadius = 0,
FireTargetLayerCapsTable = {
Air = "Land|Seabed|Water",
Expand Down
2 changes: 1 addition & 1 deletion units/DEA0202/DEA0202_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Napalm Carpet Bomb",
DoTPulses = 10,
DoTTime = 6,
DoTTime = 5.4,
FireTargetLayerCapsTable = {
Air = "Land|Seabed|Water",
Land = "Land|Seabed|Water",
Expand Down
2 changes: 1 addition & 1 deletion units/UAB2302/UAB2302_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Sonance Artillery",
DoTPulses = 2,
DoTTime = 2,
DoTTime = 1,
EnergyDrainPerSecond = 4250,
EnergyRequired = 17000,
FireTargetLayerCapsTable = {
Expand Down
2 changes: 1 addition & 1 deletion units/UAB2303/UAB2303_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Miasma Artillery",
DoTPulses = 5,
DoTTime = 1,
DoTTime = 0.8,
EnergyDrainPerSecond = 145,
EnergyRequired = 1450,
FireTargetLayerCapsTable = {
Expand Down
2 changes: 1 addition & 1 deletion units/UAL0304/UAL0304_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Sonance Artillery",
DoTPulses = 15,
DoTTime = 5,
DoTTime = 4.2,
FireTargetLayerCapsTable = {
Land = "Land|Water|Seabed",
Water = "Land|Water|Seabed",
Expand Down
2 changes: 1 addition & 1 deletion units/UEA0103/UEA0103_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Napalm Carpet Bomb",
DoTPulses = 10,
DoTTime = 4.2,
DoTTime = 3.6,
FireTargetLayerCapsTable = {
Air = "Land|Water|Seabed",
Land = "Land|Water|Seabed",
Expand Down
2 changes: 1 addition & 1 deletion units/URA0204/URA0204_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Nanite Torpedo",
DoTPulses = 5,
DoTTime = 1,
DoTTime = 0.8,
FireTargetLayerCapsTable = {
Air = "Seabed|Sub|Water",
Land = "Seabed|Sub|Water",
Expand Down
2 changes: 1 addition & 1 deletion units/URB2109/URB2109_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Nanite Torpedo",
DoTPulses = 5,
DoTTime = 1,
DoTTime = 0.8,
FireTargetLayerCapsTable = { Water = "Seabed|Sub|Water" },
FiringTolerance = 30,
Label = "Turret01",
Expand Down
2 changes: 1 addition & 1 deletion units/URB2205/URB2205_unit.bp
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ UnitBlueprint{
DamageType = "Normal",
DisplayName = "Nanite Torpedo",
DoTPulses = 5,
DoTTime = 0.5,
DoTTime = 0.4,
FireTargetLayerCapsTable = { Water = "Seabed|Sub|Water" },
FiringTolerance = 60,
Label = "Turret01",
Expand Down
Loading