Skip to content

Commit 5302a1b

Browse files
authored
Fix inaccuracy of the DoTTime weapon stat (#6457)
Fixes the DoT effect taking less time than the stat indicates, and updates blueprints to the lower actual time.
1 parent c69792c commit 5302a1b

File tree

21 files changed

+128
-129
lines changed

21 files changed

+128
-129
lines changed

changelog/snippets/fix.6457.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- (#6457) Fix weapon projectiles dealing damage over time over a shorter duration than given by the `DoTTime` stat.
2+
- `DoTTime` for FAF units is adjusted to the old actual DoT duration, so balance is not changed.
3+
- Unit databases will now show the actual damage over time duration.

lua/sim/DefaultDamage.lua

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,66 +14,90 @@ local DamageArea = DamageArea
1414

1515
-- cache for performance
1616
local VectorCache = Vector(0, 0, 0)
17-
local MathFloor = math.floor
18-
local CoroutineYield = coroutine.yield
17+
local MathMod = math.mod
18+
local MATH_IRound = MATH_IRound
19+
local WaitTicks = WaitTicks
1920

2021
local EntityBeenDestroyed = _G.moho.entity_methods.BeenDestroyed
2122
local EntityGetPositionXYZ = _G.moho.entity_methods.GetPositionXYZ
2223

23-
--- Performs damage over time on a unit.
24+
--- Performs damage over time on a target, waiting the interval *before* dealing damage.
2425
---@param instigator Unit
25-
---@param unit Unit
26-
---@param pulses any
27-
---@param pulseTime integer
26+
---@param target Unit | Prop | Projectile
27+
---@param pulses number
28+
---@param pulseInterval number
2829
---@param damage number
29-
---@param damType DamageType
30-
---@param friendly boolean
31-
function UnitDoTThread (instigator, unit, pulses, pulseTime, damage, damType, friendly)
32-
30+
---@param damageType DamageType
31+
function UnitDoTThread(instigator, target, pulses, pulseInterval, damage, damageType)
3332
-- localize for performance
3433
local position = VectorCache
35-
local DamageArea = DamageArea
36-
local CoroutineYield = CoroutineYield
34+
local Damage = Damage
35+
local EntityGetPositionXYZ = EntityGetPositionXYZ
36+
local WaitTicks = WaitTicks
37+
local MathMod = MathMod
3738

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

4144
for i = 1, pulses do
42-
if unit and not EntityBeenDestroyed(unit) then
43-
position[1], position[2], position[3] = EntityGetPositionXYZ(unit)
44-
Damage(instigator, position, unit, damage, damType )
45+
if target and not EntityBeenDestroyed(target) then
46+
position[1], position[2], position[3] = EntityGetPositionXYZ(target)
47+
Damage(instigator, position, target, damage, damageType)
4548
else
4649
break
4750
end
48-
CoroutineYield(pulseTime)
51+
accum = accum + pulseInterval
52+
if accum > 1 then
53+
-- final accumulator value may be #.999 which needs to be rounded
54+
if i == pulses then
55+
WaitTicks(MATH_IRound(accum))
56+
else
57+
WaitTicks(accum)
58+
accum = MathMod(accum, 1)
59+
end
60+
end
4961
end
5062
end
5163

52-
--- Performs damage over time in a given area.
64+
--- Performs damage over time in a given area, waiting the interval *before* dealing damage.
5365
---@param instigator Unit
5466
---@param position Vector
5567
---@param pulses number
56-
---@param pulseTime number
68+
---@param pulseInterval number
5769
---@param radius number
5870
---@param damage number
59-
---@param damType DamageType
60-
---@param friendly boolean
61-
function AreaDoTThread (instigator, position, pulses, pulseTime, radius, damage, damType, friendly)
62-
71+
---@param damageType DamageType
72+
---@param damageFriendly boolean
73+
---@param damageSelf boolean
74+
function AreaDoTThread(instigator, position, pulses, pulseInterval, radius, damage, damageType, damageFriendly, damageSelf)
6375
-- localize for performance
6476
local DamageArea = DamageArea
65-
local CoroutineYield = CoroutineYield
77+
local WaitTicks = WaitTicks
78+
local MathMod = MathMod
6679

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

7085
for i = 1, pulses do
71-
DamageArea(instigator, position, radius, damage, damType, friendly)
72-
CoroutineYield(pulseTime)
86+
accum = accum + pulseInterval
87+
if accum > 1 then
88+
-- final accumulator value may be #.999 which needs to be rounded
89+
if i == pulses then
90+
WaitTicks(MATH_IRound(accum))
91+
else
92+
WaitTicks(accum)
93+
accum = MathMod(accum, 1)
94+
end
95+
end
96+
DamageArea(instigator, position, radius, damage, damageType, damageFriendly, damageSelf)
7397
end
7498
end
7599

76-
-- Deprecated functionality --
100+
--#region Deprecated functionality
77101

78102
-- SCALABLE RADIUS AREA DOT
79103
-- - Allows for a scalable damage radius that begins with DamageStartRadius and ends
@@ -101,4 +125,5 @@ function ScalableRadiusAreaDoT(entity)
101125
end
102126
end
103127
entity:Destroy()
104-
end
128+
end
129+
--#endregion

lua/sim/Projectile.lua

Lines changed: 51 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -612,16 +612,11 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) {
612612
end,
613613

614614
--- Called by Lua to process the damage logic of a projectile
615-
-- @param self The projectile itself
616-
-- @param instigator The launcher, and if it doesn't exist, the projectile itself
617-
-- @param DamageData The damage data passed by the weapon
618-
-- @param targetEntity The entity we hit, is nil if we hit terrain
619-
-- @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
620615
---@param self Projectile
621-
---@param instigator Unit
616+
---@param instigator Unit # The launcher, and if it doesn't exist, the projectile itself
622617
---@param DamageData WeaponDamageTable # passed by the weapon
623-
---@param targetEntity Unit | Prop | nil
624-
---@param cachedPosition Vector
618+
---@param targetEntity Unit | Prop | nil # nil if hitting terrain
619+
---@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
625620
DoDamage = function(self, instigator, DamageData, targetEntity, cachedPosition)
626621

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

640-
-- check for damage-over-time
641-
local DoTTime = DamageData.DoTTime
642-
if DoTTime <= 0 then
643-
-- no damage over time, do radius-based damage
635+
-- do initial damage in a radius
636+
DamageArea(
637+
instigator,
638+
cachedPosition,
639+
radius,
640+
damage + (DamageData.InitialDamageAmount or 0),
641+
damageType,
642+
damageFriendly,
643+
damageSelf
644+
)
645+
646+
local damageToShields = DamageData.DamageToShields
647+
if damageToShields then
644648
DamageArea(
645649
instigator,
646650
cachedPosition,
647651
radius,
648-
damage,
649-
damageType,
652+
damageToShields,
653+
"FAF_AntiShield",
650654
damageFriendly,
651655
damageSelf
652656
)
657+
end
653658

654-
local damageToShields = DamageData.DamageToShields
655-
if damageToShields then
656-
DamageArea(
657-
instigator,
658-
cachedPosition,
659-
radius,
660-
damageToShields,
661-
"FAF_AntiShield",
662-
damageFriendly,
663-
damageSelf
664-
)
665-
end
666-
else
667-
-- check for initial damage
668-
local initialDmg = DamageData.InitialDamageAmount
669-
if initialDmg > 0 then
670-
DamageArea(
659+
-- check for and deal damage over time
660+
local DoTTime = DamageData.DoTTime
661+
if DoTTime > 0 then
662+
-- initial damage pulse was already dealt so subtract 1
663+
local DoTPulses = DamageData.DoTPulses - 1
664+
if DoTPulses >= 1 then
665+
ForkThread(
666+
AreaDoTThread,
671667
instigator,
672-
cachedPosition,
668+
self:GetPosition(), -- can't use cachedPosition here: breaks invariant
669+
DoTPulses,
670+
(DoTTime / (DoTPulses)),
673671
radius,
674-
initialDmg,
672+
damage,
675673
damageType,
676-
damageFriendly,
677-
damageSelf
674+
damageFriendly
678675
)
679676
end
680-
681-
-- apply damage over time
682-
local DoTPulses = DamageData.DoTPulses or 1
683-
ForkThread(
684-
AreaDoTThread,
685-
instigator,
686-
self:GetPosition(), -- can't use cachedPosition here: breaks invariant
687-
DoTPulses,
688-
(DoTTime / (DoTPulses)),
689-
radius,
690-
damage,
691-
damageType,
692-
damageFriendly
693-
)
694677
end
695678

696679
-- damage a single entity
697680
elseif targetEntity then
698681
local damageType = DamageData.DamageType
699682

683+
-- do initial damage
684+
Damage(
685+
instigator,
686+
cachedPosition,
687+
targetEntity,
688+
damage + (DamageData.InitialDamageAmount or 0),
689+
damageType
690+
)
691+
700692
local damageToShields = DamageData.DamageToShields
701693
if damageToShields then
702694
Damage(
@@ -708,43 +700,22 @@ Projectile = ClassProjectile(ProjectileMethods, DebugProjectileComponent) {
708700
)
709701
end
710702

711-
-- check for damage-over-time
703+
-- check for and apply damage over time
712704
local DoTTime = DamageData.DoTTime
713-
if DoTTime <= 0 then
714-
715-
-- no damage over time, do single target damage
716-
Damage(
717-
instigator,
718-
cachedPosition,
719-
targetEntity,
720-
damage,
721-
damageType
722-
)
723-
else
724-
-- check for initial damage
725-
local initialDmg = DamageData.InitialDamageAmount or 0
726-
if initialDmg > 0 then
727-
Damage(
705+
if DoTTime > 0 then
706+
-- initial damage pulse was already dealt so subtract 1
707+
local DoTPulses = DamageData.DoTPulses - 1
708+
if DoTPulses >= 1 then
709+
ForkThread(
710+
UnitDoTThread,
728711
instigator,
729-
cachedPosition,
730712
targetEntity,
731-
initialDmg,
713+
DoTPulses,
714+
(DoTTime / (DoTPulses)),
715+
damage,
732716
damageType
733717
)
734718
end
735-
736-
-- apply damage over time
737-
local DoTPulses = DamageData.DoTPulses or 1
738-
ForkThread(
739-
UnitDoTThread,
740-
instigator,
741-
targetEntity,
742-
DoTPulses,
743-
(DoTTime / (DoTPulses)),
744-
damage,
745-
damageType,
746-
DamageData.DamageFriendly
747-
)
748719
end
749720
end
750721
end

units/DAA0206/DAA0206_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ UnitBlueprint{
170170
DamageType = "Normal",
171171
DisplayName = "Kamikaze",
172172
DoTPulses = 20,
173-
DoTTime = 10,
173+
DoTTime = 9.5,
174174
EffectiveRadius = 0,
175175
FireTargetLayerCapsTable = {
176176
Air = "Land|Seabed|Water",

units/DEA0202/DEA0202_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ UnitBlueprint{
346346
DamageType = "Normal",
347347
DisplayName = "Napalm Carpet Bomb",
348348
DoTPulses = 10,
349-
DoTTime = 6,
349+
DoTTime = 5.4,
350350
FireTargetLayerCapsTable = {
351351
Air = "Land|Seabed|Water",
352352
Land = "Land|Seabed|Water",

units/UAB2302/UAB2302_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ UnitBlueprint{
149149
DamageType = "Normal",
150150
DisplayName = "Sonance Artillery",
151151
DoTPulses = 2,
152-
DoTTime = 2,
152+
DoTTime = 1,
153153
EnergyDrainPerSecond = 4250,
154154
EnergyRequired = 17000,
155155
FireTargetLayerCapsTable = {

units/UAB2303/UAB2303_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ UnitBlueprint{
132132
DamageType = "Normal",
133133
DisplayName = "Miasma Artillery",
134134
DoTPulses = 5,
135-
DoTTime = 1,
135+
DoTTime = 0.8,
136136
EnergyDrainPerSecond = 145,
137137
EnergyRequired = 1450,
138138
FireTargetLayerCapsTable = {

units/UAL0304/UAL0304_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ UnitBlueprint{
148148
DamageType = "Normal",
149149
DisplayName = "Sonance Artillery",
150150
DoTPulses = 15,
151-
DoTTime = 5,
151+
DoTTime = 4.2,
152152
FireTargetLayerCapsTable = {
153153
Land = "Land|Water|Seabed",
154154
Water = "Land|Water|Seabed",

units/UEA0103/UEA0103_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ UnitBlueprint{
192192
DamageType = "Normal",
193193
DisplayName = "Napalm Carpet Bomb",
194194
DoTPulses = 10,
195-
DoTTime = 4.2,
195+
DoTTime = 3.6,
196196
FireTargetLayerCapsTable = {
197197
Air = "Land|Water|Seabed",
198198
Land = "Land|Water|Seabed",

units/URA0204/URA0204_unit.bp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ UnitBlueprint{
205205
DamageType = "Normal",
206206
DisplayName = "Nanite Torpedo",
207207
DoTPulses = 5,
208-
DoTTime = 1,
208+
DoTTime = 0.8,
209209
FireTargetLayerCapsTable = {
210210
Air = "Seabed|Sub|Water",
211211
Land = "Seabed|Sub|Water",

0 commit comments

Comments
 (0)