forked from FAForever/fa
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDefaultProjectiles.lua
More file actions
487 lines (414 loc) · 18.6 KB
/
DefaultProjectiles.lua
File metadata and controls
487 lines (414 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
-----------------------------------------------------------------
-- File : /lua/defaultprojectiles.lua
-- Author(s): John Comes, Gordon Duclos
-- Summary : Script for default projectiles
-- Copyright © 2005 Gas Powered Games, Inc. All rights reserved.
-----------------------------------------------------------------
local Projectile = import('/lua/sim/Projectile.lua').Projectile
local UnitsInSphere = import('/lua/utilities.lua').GetTrueEnemyUnitsInSphere
local GetDistanceBetweenTwoEntities = import('/lua/utilities.lua').GetDistanceBetweenTwoEntities
local OCProjectiles = {}
-----------------------------------------------------------------
-- Null Shell
-----------------------------------------------------------------
NullShell = Class(Projectile) {}
-----------------------------------------------------------------
-- PROJECTILE WITH ATTACHED EFFECT EMITTERS
-----------------------------------------------------------------
EmitterProjectile = Class(Projectile) {
FxTrails = {'/effects/emitters/missile_munition_trail_01_emit.bp',},
FxTrailScale = 1,
FxTrailOffset = 0,
OnCreate = function(self)
Projectile.OnCreate(self)
for i in self.FxTrails do
CreateEmitterOnEntity(self, self.Army, self.FxTrails[i]):ScaleEmitter(self.FxTrailScale):OffsetEmitter(0, 0, self.FxTrailOffset)
end
end,
}
-----------------------------------------------------------------
-- BEAM PROJECTILES
-----------------------------------------------------------------
SingleBeamProjectile = Class(EmitterProjectile) {
BeamName = '/effects/emitters/default_beam_01_emit.bp',
FxTrails = {},
OnCreate = function(self)
EmitterProjectile.OnCreate(self)
if self.BeamName then
CreateBeamEmitterOnEntity(self, -1, self.Army, self.BeamName)
end
end,
}
MultiBeamProjectile = Class(EmitterProjectile) {
Beams = {'/effects/emitters/default_beam_01_emit.bp',},
FxTrails = {},
OnCreate = function(self)
EmitterProjectile.OnCreate(self)
local beam = nil
for k, v in self.Beams do
CreateBeamEmitterOnEntity(self, -1, self.Army, v)
end
end,
}
-- Nukes
NukeProjectile = Class(NullShell) {
MovementThread = function(self)
local launcher = self:GetLauncher()
self.Nuke = true
self.CreateEffects(self, self.InitialEffects, self.Army, 1)
self:TrackTarget(false)
WaitSeconds(2.5) -- Height
self:SetCollision(true)
self.CreateEffects(self, self.LaunchEffects, self.Army, 1)
WaitSeconds(2.5)
self.CreateEffects(self, self.ThrustEffects, self.Army, 3)
WaitSeconds(2.5)
self:TrackTarget(true) -- Turn ~90 degrees towards target
self:SetDestroyOnWater(true)
self:SetTurnRate(45)
WaitSeconds(2) -- Now set turn rate to zero so nuke flies straight
self:SetTurnRate(0)
self:SetAcceleration(0.001)
self.WaitTime = 0.5
while not self:BeenDestroyed() do
self:SetTurnRateByDist()
WaitSeconds(self.WaitTime)
end
end,
SetTurnRateByDist = function(self)
local dist = self:GetDistanceToTarget()
-- Get the nuke as close to 90 deg as possible
if dist > 150 then
-- Freeze the turn rate as to prevent steep angles at long distance targets
self:SetTurnRate(0)
elseif dist > 75 and dist <= 150 then
-- Increase check intervals
self.WaitTime = 0.3
elseif dist > 32 and dist <= 75 then
-- Further increase check intervals
self.WaitTime = 0.1
elseif dist < 32 then
-- Turn the missile down
self:SetTurnRate(50)
end
end,
GetDistanceToTarget = function(self)
local tpos = self:GetCurrentTargetPosition()
local mpos = self:GetPosition()
local dist = VDist2(mpos[1], mpos[3], tpos[1], tpos[3])
return dist
end,
CreateEffects = function(self, EffectTable, army, scale)
if not EffectTable then return end
for k, v in EffectTable do
self.Trash:Add(CreateAttachedEmitter(self, -1, army, v):ScaleEmitter(scale))
end
end,
ForceThread = function(self)
-- Knockdown force rings
local position = self:GetPosition()
DamageRing(self, position, 0.1, 45, 1, 'Force', true)
WaitSeconds(0.1)
DamageRing(self, position, 0.1, 45, 1, 'Force', true)
end,
OnImpact = function(self, TargetType, TargetEntity)
if not TargetEntity or not EntityCategoryContains(categories.PROJECTILE, TargetEntity) then
-- Play the explosion sound
local myBlueprint = self:GetBlueprint()
if myBlueprint.Audio.NukeExplosion then
self:PlaySound(myBlueprint.Audio.NukeExplosion)
end
self.effectEntity = self:CreateProjectile(self.effectEntityPath, 0, 0, 0, nil, nil, nil):SetCollision(false)
self.effectEntity:ForkThread(self.effectEntity.EffectThread)
self:ForkThread(self.ForceThread)
end
NullShell.OnImpact(self, TargetType, TargetEntity)
end,
LauncherCallbacks = function(self)
local launcher = self:GetLauncher()
if launcher and not launcher.Dead and launcher.EventCallbacks.ProjectileDamaged then
self.ProjectileDamaged = {}
for k,v in launcher.EventCallbacks.ProjectileDamaged do
table.insert(self.ProjectileDamaged, v)
end
end
self:SetCollisionShape('Sphere', 0, 0, 0, 2.0)
self:ForkThread(self.MovementThread)
end,
DoTakeDamage = function(self, instigator, amount, vector, damageType)
if self.ProjectileDamaged then
for k,v in self.ProjectileDamaged do
v(self)
end
end
NullShell.DoTakeDamage(self, instigator, amount, vector, damageType)
end,
OnDamage = function(self, instigator, amount, vector, damageType)
local bp = self:GetBlueprint().Defense.MaxHealth
if bp then
self:DoTakeDamage(instigator, amount, vector, damageType)
else
self:OnKilled(instigator, damageType)
end
end,
}
-----------------------------------------------------------------
-- POLY-TRAIL PROJECTILES
-----------------------------------------------------------------
SinglePolyTrailProjectile = Class(EmitterProjectile) {
PolyTrail = '/effects/emitters/test_missile_trail_emit.bp',
PolyTrailOffset = 0,
FxTrails = {},
OnCreate = function(self)
EmitterProjectile.OnCreate(self)
if self.PolyTrail ~= '' then
CreateTrail(self, -1, self.Army, self.PolyTrail):OffsetEmitter(0, 0, self.PolyTrailOffset)
end
end,
}
MultiPolyTrailProjectile = Class(EmitterProjectile) {
PolyTrails = {'/effects/emitters/test_missile_trail_emit.bp'},
PolyTrailOffset = {0},
FxTrails = {},
RandomPolyTrails = 0, -- Count of how many are selected randomly for PolyTrail table
OnCreate = function(self)
EmitterProjectile.OnCreate(self)
if self.PolyTrails then
local NumPolyTrails = table.getn(self.PolyTrails)
if self.RandomPolyTrails ~= 0 then
local index = nil
for i = 1, self.RandomPolyTrails do
index = math.floor(Random(1, NumPolyTrails))
CreateTrail(self, -1, self.Army, self.PolyTrails[index]):OffsetEmitter(0, 0, self.PolyTrailOffset[index])
end
else
for i = 1, NumPolyTrails do
CreateTrail(self, -1, self.Army, self.PolyTrails[i]):OffsetEmitter(0, 0, self.PolyTrailOffset[i])
end
end
end
end,
}
-----------------------------------------------------------------
-- COMPOSITE EMITTER PROJECTILES - MULTIPURPOSE PROJECTILES
-- - THAT COMBINES BEAMS, POLYTRAILS, AND NORMAL EMITTERS
-----------------------------------------------------------------
-- LIGHTWEIGHT VERSION THAT LIMITS USE TO 1 BEAM, 1 POLYTRAIL, AND STANDARD EMITTERS
SingleCompositeEmitterProjectile = Class(SinglePolyTrailProjectile) {
BeamName = '/effects/emitters/default_beam_01_emit.bp',
FxTrails = {},
OnCreate = function(self)
SinglePolyTrailProjectile.OnCreate(self)
if self.BeamName ~= '' then
CreateBeamEmitterOnEntity(self, -1, self.Army, self.BeamName)
end
end,
}
-- HEAVYWEIGHT VERSION, ALLOWS FOR MULTIPLE BEAMS, POLYTRAILS, AND STANDARD EMITTERS
MultiCompositeEmitterProjectile = Class(MultiPolyTrailProjectile) {
Beams = {'/effects/emitters/default_beam_01_emit.bp',},
PolyTrails = {'/effects/emitters/test_missile_trail_emit.bp'},
PolyTrailOffset = {0},
RandomPolyTrails = 0, -- Count of how many are selected randomly for PolyTrail table
FxTrails = {},
OnCreate = function(self)
MultiPolyTrailProjectile.OnCreate(self)
local beam = nil
for k, v in self.Beams do
CreateBeamEmitterOnEntity(self, -1, self.Army, v)
end
end,
}
-----------------------------------------------------------------
-- TRAIL ON ENTERING WATER PROJECTILE
-----------------------------------------------------------------
OnWaterEntryEmitterProjectile = Class(Projectile) {
FxTrails = {'/effects/emitters/torpedo_munition_trail_01_emit.bp',},
FxTrailScale = 1,
FxTrailOffset = 0,
PolyTrail = '',
PolyTrailOffset = 0,
TrailDelay = 5,
EnterWaterSound = 'Torpedo_Enter_Water_01',
OnCreate = function(self, inWater)
Projectile.OnCreate(self, inWater)
if inWater then
for i in self.FxTrails do
CreateEmitterOnEntity(self, self.Army, self.FxTrails[i]):ScaleEmitter(self.FxTrailScale):OffsetEmitter(0, 0, self.FxTrailOffset)
end
if self.PolyTrail ~= '' then
CreateTrail(self, -1, self.Army, self.PolyTrail):OffsetEmitter(0, 0, self.PolyTrailOffset)
end
end
end,
EnterWaterThread = function(self)
WaitTicks(self.TrailDelay)
for i in self.FxTrails do
CreateEmitterOnEntity(self, self.Army, self.FxTrails[i]):ScaleEmitter(self.FxTrailScale):OffsetEmitter(0, 0, self.FxTrailOffset)
end
if self.PolyTrail ~= '' then
CreateTrail(self, -1, self.Army, self.PolyTrail):OffsetEmitter(0, 0, self.PolyTrailOffset)
end
end,
OnEnterWater = function(self)
Projectile.OnEnterWater(self)
self:TrackTarget(true)
self:StayUnderwater(true)
self.TTT1 = self:ForkThread(self.EnterWaterThread)
end,
OnImpact = function(self, TargetType, TargetEntity)
Projectile.OnImpact(self, TargetType, TargetEntity)
KillThread(self.TTT1)
end,
}
-----------------------------------------------------------------
-- GENERIC DEBRIS PROJECTILE
-----------------------------------------------------------------
BaseGenericDebris = Class(EmitterProjectile){
FxUnitHitScale = 0.25,
FxWaterHitScale = 0.25,
FxUnderWaterHitScale = 0.25,
FxNoneHitScale = 0.25,
FxImpactLand = false,
FxLandHitScale = 0.5,
FxTrails = false,
FxTrailScale = 1,
}
-----------------------------------------------------------
-- PROJECTILE THAT ADJUSTS DAMAGE AND ENERGY COST ON IMPACT
-----------------------------------------------------------
OverchargeProjectile = Class() {
OnImpact = function(self, targetType, targetEntity)
--[[WARN('Inside OCPROJ OnImpact')
LOG(targetType)
LOG(targetEntity)
if targetEntity and IsUnit(targetEntity) then
LOG(targetEntity.UnitId)
end]]
-- Stop us doing blueprint damage in the other OnImpact call if we ditch this one without resetting self.DamageData
self.DamageData.DamageAmount = 0
local launcher = self:GetLauncher()
if not launcher then return end
local wep = launcher:GetWeaponByLabel('OverCharge')
if not wep then return end
-- Table layout for Overcharge data section
-- Overcharge = {
-- energyMult = _, -- What proportion of current storage are we allowed to spend?
-- commandDamage = _, -- Takes effect in ACUUnit DoTakeDamage()
-- structureDamage = _, -- Takes effect in StructureUnit DoTakeDamage() & Shield ApplyDamage()
-- maxDamage = _,
-- minDamage = _,
-- },
local data = wep:GetBlueprint().Overcharge
if not data then return end
-- Set the damage dealt by the projectile for hitting the floor or an ACUUnit
-- Energy drained is calculated by the relationship equations
local damage = data.minDamage
if targetEntity then
-- Handle hitting shields. We want the unit underneath, not the shield itself
if not IsUnit(targetEntity) then
if not targetEntity.Owner then -- We hit something odd, not a shield
WARN('Overcharge hit something that was not the ground, a shield, or a unit')
LOG(targetType)
return
end
targetEntity = targetEntity.Owner
end
-- Get max energy available to drain according to how much we have
local energyAvailable = launcher:GetAIBrain():GetEconomyStored('ENERGY')
local energyLimit = energyAvailable * data.energyMult
if OCProjectiles[self.Army] > 1 then
energyLimit = energyLimit / OCProjectiles[self.Army]
end
local energyLimitDamage = self:EnergyAsDamage(energyLimit)
-- Find max available damage
damage = math.min(data.maxDamage, energyLimitDamage)
-- How much damage do we actually need to kill the unit?
local idealDamage = targetEntity:GetHealth()
local maxHP = self:UnitsDetection(targetType, targetEntity)
idealDamage = maxHP or data.minDamage
-----SHIELDS------
if targetEntity.MyShield and targetEntity.MyShield.ShieldType == 'Bubble' then
if EntityCategoryContains(categories.STRUCTURE, targetEntity) then
idealDamage = data.minDamage
else
idealDamage = targetEntity.MyShield:GetMaxHealth()
end
--MaxHealth instead of GetHealth because with getHealth OC won't kill bubble shield which is in AoE range but has more hp than targetEntity.MyShield.
--good against group of mobile shields
end
------ ACU -------
if EntityCategoryContains(categories.COMMAND, targetEntity) and not maxHP then -- no units around ACU - min.damage
idealDamage = data.minDamage
end
damage = math.min(damage, idealDamage)
damage = math.max(data.minDamage, damage)
-- prevents radars blinks if there is less than 5k e in storage when OC hits the target
if energyAvailable < 5000 then
damage = energyLimitDamage
end
end
-- Turn the final damage into energy
local drain = self:DamageAsEnergy(damage)
--LOG('Drain is ' .. drain)
--LOG('Damage is ' .. damage)
self.DamageData.DamageAmount = damage
if drain > 0 then
launcher.EconDrain = CreateEconomyEvent(launcher, drain, 0, 0)
launcher:ForkThread(function()
WaitFor(launcher.EconDrain)
RemoveEconomyEvent(launcher, launcher.EconDrain)
OCProjectiles[self.Army] = OCProjectiles[self.Army] - 1
launcher.EconDrain = nil
end)
end
end,
-- y = 3000e^(0.000095(x+15500))-10090
-- https://www.desmos.com/calculator/yyetmwyf0d
DamageAsEnergy = function(self, damage)
return (3000 * math.exp(0.000095 * (damage + 15500))) - 10090
end,
EnergyAsDamage = function(self, energy)
return (math.log((energy + 10090) / 3000) / 0.000095) - 15500
end,
UnitsDetection = function(self, targetType, targetEntity)
-- looking for units around target which are in splash range
local launcher = self:GetLauncher()
local maxHP = 0
for _, unit in UnitsInSphere(launcher, self:GetPosition(), 2.7, categories.MOBILE -categories.COMMAND) or {} do
if unit.MyShield and unit:GetHealth() + unit.MyShield:GetHealth() > maxHP then
maxHP = unit:GetHealth() + unit.MyShield:GetHealth()
elseif unit:GetHealth() > maxHP then
maxHP = unit:GetHealth()
end
end
for _, unit in UnitsInSphere(launcher, self:GetPosition(), 13.2, categories.EXPERIMENTAL*categories.LAND*categories.MOBILE) or {} do
-- Special for fatty's shield
if EntityCategoryContains(categories.UEF, unit) and unit.MyShield._IsUp and unit.MyShield:GetMaxHealth() > maxHP then
maxHP = unit.MyShield:GetMaxHealth()
elseif unit:GetHealth() > maxHP then
local distance = math.min(unit:GetBlueprint().SizeX, unit:GetBlueprint().SizeZ)
if GetDistanceBetweenTwoEntities(unit, self) < distance + self.DamageData.DamageRadius then
maxHP = unit:GetHealth()
end
end
end
if EntityCategoryContains(categories.EXPERIMENTAL, targetEntity) and targetEntity:GetHealth() > maxHP then
maxHP = targetEntity:GetHealth()
--[[ we need this because if OC shell hitted top part of GC model its health won't be in our table
Bug appeared since we use shell.pos in getUnitsInSphere instead of target.pos.
Shell is too far from actual target.pos(target pos is somewhere near land and shell is near GC's head)
and getUnits returns nothing. Same to GetDistance. Distance between shell and GC pos > than math.min (x,z) size]]
end
if maxHP ~= 0 then
return maxHP
end
end,
OnCreate = function(self)
self.Army = self:GetArmy()
if not OCProjectiles[self.Army] then
OCProjectiles[self.Army] = 0
end
OCProjectiles[self.Army] = OCProjectiles[self.Army] + 1
end,
}