source: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js@ 25013

Last change on this file since 25013 was 25013, checked in by Freagarach, 3 years ago

Combine attacker data in "data"-object in attack-related code.

Allowing for easier introduction of new parameters.
Split from D781, (D368).

Differential revision: D2269
Comments by: @Angen, @Stan, @wraitii
Idea accepted by: @wraitii

  • Property svn:eol-style set to native
File size: 16.2 KB
Line 
1AttackEffects = class AttackEffects
2{
3 constructor() {}
4 Receivers()
5 {
6 return [{
7 "type": "Damage",
8 "IID": "IID_Health",
9 "method": "TakeDamage"
10 }];
11 }
12};
13
14Engine.LoadHelperScript("Attacking.js");
15Engine.LoadHelperScript("Player.js");
16Engine.LoadHelperScript("Position.js");
17Engine.LoadHelperScript("ValueModification.js");
18Engine.LoadComponentScript("interfaces/DelayedDamage.js");
19Engine.LoadComponentScript("interfaces/Health.js");
20Engine.LoadComponentScript("interfaces/Loot.js");
21Engine.LoadComponentScript("interfaces/Promotion.js");
22Engine.LoadComponentScript("interfaces/ModifiersManager.js");
23Engine.LoadComponentScript("interfaces/Resistance.js");
24Engine.LoadComponentScript("interfaces/Timer.js");
25Engine.LoadComponentScript("interfaces/UnitAI.js");
26Engine.LoadComponentScript("Attack.js");
27Engine.LoadComponentScript("DelayedDamage.js");
28Engine.LoadComponentScript("Timer.js");
29
30function Test_Generic()
31{
32 ResetState();
33
34 let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
35 cmpTimer.OnUpdate({ "turnLength": 1 });
36 let attacker = 11;
37 let atkPlayerEntity = 1;
38 let attackerOwner = 6;
39 let cmpAttack = ConstructComponent(attacker, "Attack",
40 {
41 "Ranged": {
42 "Damage": {
43 "Crush": 5,
44 },
45 "MaxRange": 50,
46 "MinRange": 0,
47 "Delay": 0,
48 "Projectile": {
49 "Speed": 75.0,
50 "Spread": 0.5,
51 "Gravity": 9.81,
52 "FriendlyFire": "false",
53 "LaunchPoint": { "@y": 3 }
54 }
55 }
56 });
57 let damage = 5;
58 let target = 21;
59 let targetOwner = 7;
60 let targetPos = new Vector3D(3, 0, 3);
61
62 let type = "Melee";
63 let damageTaken = false;
64
65 cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage });
66
67 let data = {
68 "type": "Melee",
69 "attackData": {
70 "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage },
71 },
72 "target": target,
73 "attacker": attacker,
74 "attackerOwner": attackerOwner,
75 "position": targetPos,
76 "projectileId": 9,
77 "direction": new Vector3D(1, 0, 0)
78 };
79
80 AddMock(atkPlayerEntity, IID_Player, {
81 "GetEnemies": () => [targetOwner]
82 });
83
84 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
85 "GetPlayerByID": id => atkPlayerEntity,
86 "GetAllPlayers": () => [0, 1, 2, 3, 4]
87 });
88
89 AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
90 "RemoveProjectile": () => {},
91 "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
92 });
93
94 AddMock(target, IID_Position, {
95 "GetPosition": () => targetPos,
96 "GetPreviousPosition": () => targetPos,
97 "GetPosition2D": () => Vector2D.From(targetPos),
98 "GetHeightAt": () => 0,
99 "IsInWorld": () => true,
100 });
101
102 AddMock(target, IID_Health, {
103 "TakeDamage": (amount, __, ___) => {
104 damageTaken = true;
105 return { "healthChange": -amount };
106 },
107 });
108
109 AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
110 "MissileHit": () => {
111 damageTaken = true;
112 },
113 });
114
115 Engine.PostMessage = function(ent, iid, message)
116 {
117 TS_ASSERT_UNEVAL_EQUALS({
118 "type": type,
119 "target": target,
120 "attacker": attacker,
121 "attackerOwner": attackerOwner,
122 "damage": damage,
123 "capture": 0,
124 "statusEffects": [],
125 "fromStatusEffect": false
126 }, message);
127 };
128
129 AddMock(target, IID_Footprint, {
130 "GetShape": () => ({ "type": "circle", "radius": 20 }),
131 });
132
133 AddMock(attacker, IID_Ownership, {
134 "GetOwner": () => attackerOwner,
135 });
136
137 AddMock(attacker, IID_Position, {
138 "GetPosition": () => new Vector3D(2, 0, 3),
139 "GetRotation": () => new Vector3D(1, 2, 3),
140 "IsInWorld": () => true,
141 });
142
143 function TestDamage()
144 {
145 cmpTimer.OnUpdate({ "turnLength": 1 });
146 TS_ASSERT(damageTaken);
147 damageTaken = false;
148 }
149
150 Attacking.HandleAttackEffects(target, data);
151 TestDamage();
152
153 data.type = "Ranged";
154 type = data.type;
155 Attacking.HandleAttackEffects(target, data);
156 TestDamage();
157
158 // Check for damage still being dealt if the attacker dies
159 cmpAttack.PerformAttack("Ranged", target);
160 Engine.DestroyEntity(attacker);
161 TestDamage();
162
163 atkPlayerEntity = 1;
164 AddMock(atkPlayerEntity, IID_Player, {
165 "GetEnemies": () => [2, 3]
166 });
167 TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]);
168 TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]);
169}
170
171Test_Generic();
172
173function TestLinearSplashDamage()
174{
175 ResetState();
176 Engine.PostMessage = (ent, iid, message) => {};
177
178 const attacker = 50;
179 const attackerOwner = 1;
180
181 const origin = new Vector2D(0, 0);
182
183 let data = {
184 "type": "Ranged",
185 "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
186 "attacker": attacker,
187 "attackerOwner": attackerOwner,
188 "origin": origin,
189 "radius": 10,
190 "shape": "Linear",
191 "direction": new Vector3D(1, 747, 0),
192 "friendlyFire": false,
193 };
194
195 let fallOff = function(x, y)
196 {
197 return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius));
198 };
199
200 let hitEnts = new Set();
201
202 AddMock(attackerOwner, IID_Player, {
203 "GetEnemies": () => [2]
204 });
205
206 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
207 "GetPlayerByID": id => attackerOwner,
208 "GetAllPlayers": () => [0, 1, 2]
209 });
210
211 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
212 "ExecuteQueryAroundPos": () => [60, 61, 62],
213 });
214
215 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
216 "DistanceToPoint": (ent) => ({
217 "60": Math.sqrt(9.25),
218 "61": 0,
219 "62": Math.sqrt(29)
220 }[ent])
221 });
222
223 AddMock(60, IID_Position, {
224 "GetPosition2D": () => new Vector2D(3, -0.5),
225 });
226
227 AddMock(61, IID_Position, {
228 "GetPosition2D": () => new Vector2D(0, 0),
229 });
230
231 AddMock(62, IID_Position, {
232 "GetPosition2D": () => new Vector2D(5, 2),
233 });
234
235 AddMock(60, IID_Health, {
236 "TakeDamage": (amount, __, ___) => {
237 hitEnts.add(60);
238 TS_ASSERT_EQUALS(amount, 100 * fallOff(3, -0.5));
239 return { "healthChange": -amount };
240 }
241 });
242
243 AddMock(61, IID_Health, {
244 "TakeDamage": (amount, __, ___) => {
245 hitEnts.add(61);
246 TS_ASSERT_EQUALS(amount, 100 * fallOff(0, 0));
247 return { "healthChange": -amount };
248 }
249 });
250
251 AddMock(62, IID_Health, {
252 "TakeDamage": (amount, __, ___) => {
253 hitEnts.add(62);
254 // Minor numerical precision issues make this necessary
255 TS_ASSERT(amount < 0.00001);
256 return { "healthChange": -amount };
257 }
258 });
259
260 Attacking.CauseDamageOverArea(data);
261 TS_ASSERT(hitEnts.has(60));
262 TS_ASSERT(hitEnts.has(61));
263 TS_ASSERT(hitEnts.has(62));
264 hitEnts.clear();
265
266 data.direction = new Vector3D(0.6, 747, 0.8);
267
268 AddMock(60, IID_Health, {
269 "TakeDamage": (amount, __, ___) => {
270 hitEnts.add(60);
271 TS_ASSERT_EQUALS(amount, 100 * fallOff(1, 2));
272 return { "healthChange": -amount };
273 }
274 });
275
276 Attacking.CauseDamageOverArea(data);
277 TS_ASSERT(hitEnts.has(60));
278 TS_ASSERT(hitEnts.has(61));
279 TS_ASSERT(hitEnts.has(62));
280 hitEnts.clear();
281}
282
283TestLinearSplashDamage();
284
285function TestCircularSplashDamage()
286{
287 ResetState();
288 Engine.PostMessage = (ent, iid, message) => {};
289
290 const radius = 10;
291 let attackerOwner = 1;
292
293 let fallOff = function(r)
294 {
295 return 1 - r * r / (radius * radius);
296 };
297
298 AddMock(attackerOwner, IID_Player, {
299 "GetEnemies": () => [2]
300 });
301
302 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
303 "GetPlayerByID": id => attackerOwner,
304 "GetAllPlayers": () => [0, 1, 2]
305 });
306
307 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
308 "ExecuteQueryAroundPos": () => [60, 61, 62, 64, 65],
309 });
310
311 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
312 "DistanceToPoint": (ent, x, z) => ({
313 "60": 0,
314 "61": 5,
315 "62": 1,
316 "63": Math.sqrt(85),
317 "64": 10,
318 "65": 2
319 }[ent])
320 });
321
322 AddMock(60, IID_Position, {
323 "GetPosition2D": () => new Vector2D(3, 4),
324 });
325
326 AddMock(61, IID_Position, {
327 "GetPosition2D": () => new Vector2D(0, 0),
328 });
329
330 AddMock(62, IID_Position, {
331 "GetPosition2D": () => new Vector2D(3.6, 3.2),
332 });
333
334 AddMock(63, IID_Position, {
335 "GetPosition2D": () => new Vector2D(10, -10),
336 });
337
338 // Target on the frontier of the shape (see distance above).
339 AddMock(64, IID_Position, {
340 "GetPosition2D": () => new Vector2D(9, -4),
341 });
342
343 // Big target far away (see distance above).
344 AddMock(65, IID_Position, {
345 "GetPosition2D": () => new Vector2D(23, 4),
346 });
347
348 AddMock(60, IID_Health, {
349 "TakeDamage": (amount, __, ___) => {
350 TS_ASSERT_EQUALS(amount, 100 * fallOff(0));
351 return { "healthChange": -amount };
352 }
353 });
354
355 AddMock(61, IID_Health, {
356 "TakeDamage": (amount, __, ___) => {
357 TS_ASSERT_EQUALS(amount, 100 * fallOff(5));
358 return { "healthChange": -amount };
359 }
360 });
361
362 AddMock(62, IID_Health, {
363 "TakeDamage": (amount, __, ___) => {
364 TS_ASSERT_EQUALS(amount, 100 * fallOff(1));
365 return { "healthChange": -amount };
366 }
367 });
368
369 AddMock(63, IID_Health, {
370 "TakeDamage": (amount, __, ___) => {
371 TS_ASSERT(false);
372 }
373 });
374
375 let cmphealth64 = AddMock(64, IID_Health, {
376 "TakeDamage": (amount, __, ___) => {
377 TS_ASSERT_EQUALS(amount, 0);
378 return { "healthChange": -amount };
379 }
380 });
381 let spy64 = new Spy(cmphealth64, "TakeDamage");
382
383 let cmpHealth65 = AddMock(65, IID_Health, {
384 "TakeDamage": (amount, __, ___) => {
385 TS_ASSERT_EQUALS(amount, 100 * fallOff(2));
386 return { "healthChange": -amount };
387 }
388 });
389 let spy65 = new Spy(cmpHealth65, "TakeDamage");
390
391 Attacking.CauseDamageOverArea({
392 "type": "Ranged",
393 "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
394 "attacker": 50,
395 "attackerOwner": attackerOwner,
396 "origin": new Vector2D(3, 4),
397 "radius": radius,
398 "shape": "Circular",
399 "friendlyFire": false,
400 });
401
402 TS_ASSERT_EQUALS(spy64._called, 1);
403 TS_ASSERT_EQUALS(spy65._called, 1);
404}
405
406TestCircularSplashDamage();
407
408function Test_MissileHit()
409{
410 ResetState();
411 Engine.PostMessage = (ent, iid, message) => {};
412
413 let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage");
414
415 let target = 60;
416 let targetOwner = 1;
417 let targetPos = new Vector3D(3, 10, 0);
418 let hitEnts = new Set();
419
420 AddMock(SYSTEM_ENTITY, IID_Timer, {
421 "GetLatestTurnLength": () => 500
422 });
423
424 const radius = 10;
425
426 let data = {
427 "type": "Ranged",
428 "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } },
429 "target": 60,
430 "attacker": 70,
431 "attackerOwner": 1,
432 "position": targetPos,
433 "direction": new Vector3D(1, 0, 0),
434 "projectileId": 9,
435 "friendlyFire": "false",
436 };
437
438 AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
439 "GetPlayerByID": id => id == 1 ? 10 : 11,
440 "GetAllPlayers": () => [0, 1]
441 });
442
443 AddMock(SYSTEM_ENTITY, IID_ProjectileManager, {
444 "RemoveProjectile": () => {},
445 "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {},
446 });
447
448 AddMock(60, IID_Position, {
449 "GetPosition": () => targetPos,
450 "GetPreviousPosition": () => targetPos,
451 "GetPosition2D": () => Vector2D.From(targetPos),
452 "IsInWorld": () => true,
453 });
454
455 AddMock(60, IID_Health, {
456 "TakeDamage": (amount, __, ___) => {
457 hitEnts.add(60);
458 TS_ASSERT_EQUALS(amount, 100);
459 return { "healthChange": -amount };
460 }
461 });
462
463 AddMock(60, IID_Footprint, {
464 "GetShape": () => ({ "type": "circle", "radius": 20 }),
465 });
466
467 AddMock(70, IID_Ownership, {
468 "GetOwner": () => 1,
469 });
470
471 AddMock(70, IID_Position, {
472 "GetPosition": () => new Vector3D(0, 0, 0),
473 "GetRotation": () => new Vector3D(0, 0, 0),
474 "IsInWorld": () => true,
475 });
476
477 AddMock(10, IID_Player, {
478 "GetEnemies": () => [2]
479 });
480
481 cmpDelayedDamage.MissileHit(data, 0);
482 TS_ASSERT(hitEnts.has(60));
483 hitEnts.clear();
484
485 // Target is a mirage: hit the parent.
486 AddMock(60, IID_Mirage, {
487 "GetParent": () => 61
488 });
489
490 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
491 "DistanceToPoint": (ent) => 0
492 });
493
494 AddMock(61, IID_Position, {
495 "GetPosition": () => targetPos,
496 "GetPreviousPosition": () => targetPos,
497 "GetPosition2D": () => Vector2D.from3D(targetPos),
498 "IsInWorld": () => true
499 });
500
501 AddMock(61, IID_Health, {
502 "TakeDamage": (amount, __, ___) => {
503 hitEnts.add(61);
504 TS_ASSERT_EQUALS(amount, 100);
505 return { "healthChange": -amount };
506 }
507 });
508
509 AddMock(61, IID_Footprint, {
510 "GetShape": () => ({ "type": "circle", "radius": 20 })
511 });
512
513 cmpDelayedDamage.MissileHit(data, 0);
514 TS_ASSERT(hitEnts.has(61));
515 hitEnts.clear();
516
517 // Make sure we don't corrupt other tests.
518 DeleteMock(60, IID_Mirage);
519 cmpDelayedDamage.MissileHit(data, 0);
520 TS_ASSERT(hitEnts.has(60));
521 hitEnts.clear();
522
523 // The main target is not hit but another one is hit.
524 AddMock(60, IID_Position, {
525 "GetPosition": () => new Vector3D(900, 10, 0),
526 "GetPreviousPosition": () => new Vector3D(900, 10, 0),
527 "GetPosition2D": () => new Vector2D(900, 0),
528 "IsInWorld": () => true
529 });
530
531 AddMock(60, IID_Health, {
532 "TakeDamage": (amount, __, ___) => {
533 TS_ASSERT_EQUALS(false);
534 return { "healthChange": -amount };
535 }
536 });
537
538 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
539 "ExecuteQueryAroundPos": () => [61]
540 });
541
542 cmpDelayedDamage.MissileHit(data, 0);
543 TS_ASSERT(hitEnts.has(61));
544 hitEnts.clear();
545
546 // Add a splash damage.
547 data.splash = {};
548 data.splash.friendlyFire = false;
549 data.splash.radius = 10;
550 data.splash.shape = "Circular";
551 data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } };
552
553 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
554 "ExecuteQueryAroundPos": () => [61, 62]
555 });
556
557 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
558 "DistanceToPoint": (ent) => ({
559 "61": 0,
560 "62": 5
561 }[ent])
562 });
563
564 let dealtDamage = 0;
565 AddMock(61, IID_Health, {
566 "TakeDamage": (amount, __, ___) => {
567 hitEnts.add(61);
568 dealtDamage += amount;
569 return { "healthChange": -amount };
570 }
571 });
572
573 AddMock(62, IID_Position, {
574 "GetPosition": () => new Vector3D(8, 10, 0),
575 "GetPreviousPosition": () => new Vector3D(8, 10, 0),
576 "GetPosition2D": () => new Vector2D(8, 0),
577 "IsInWorld": () => true,
578 });
579
580 AddMock(62, IID_Health, {
581 "TakeDamage": (amount, __, ___) => {
582 hitEnts.add(62);
583 TS_ASSERT_EQUALS(amount, 200 * 0.75);
584 return { "healthChange": -amount };
585 }
586 });
587
588 AddMock(62, IID_Footprint, {
589 "GetShape": () => ({ "type": "circle", "radius": 20 }),
590 });
591
592 cmpDelayedDamage.MissileHit(data, 0);
593 TS_ASSERT(hitEnts.has(61));
594 TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
595 dealtDamage = 0;
596 TS_ASSERT(hitEnts.has(62));
597 hitEnts.clear();
598
599 // Add some hard counters bonus.
600
601 Engine.DestroyEntity(62);
602 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
603 "ExecuteQueryAroundPos": () => [61]
604 });
605
606 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
607 "DistanceToPoint": (ent) => 0
608 });
609
610 let bonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } };
611 let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } };
612
613 AddMock(61, IID_Identity, {
614 "GetClassesList": () => ["Cavalry"],
615 "GetCiv": () => "civ"
616 });
617
618 data.attackData.Bonuses = bonus;
619 cmpDelayedDamage.MissileHit(data, 0);
620 TS_ASSERT(hitEnts.has(61));
621 TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200);
622 dealtDamage = 0;
623 hitEnts.clear();
624
625 data.splash.attackData.Bonuses = splashBonus;
626 cmpDelayedDamage.MissileHit(data, 0);
627 TS_ASSERT(hitEnts.has(61));
628 TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200);
629 dealtDamage = 0;
630 hitEnts.clear();
631
632 data.attackData.Bonuses = undefined;
633 cmpDelayedDamage.MissileHit(data, 0);
634 TS_ASSERT(hitEnts.has(61));
635 TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
636 dealtDamage = 0;
637 hitEnts.clear();
638
639 data.attackData.Bonuses = null;
640 cmpDelayedDamage.MissileHit(data, 0);
641 TS_ASSERT(hitEnts.has(61));
642 TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
643 dealtDamage = 0;
644 hitEnts.clear();
645
646 data.attackData.Bonuses = {};
647 cmpDelayedDamage.MissileHit(data, 0);
648 TS_ASSERT(hitEnts.has(61));
649 TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
650 dealtDamage = 0;
651 hitEnts.clear();
652
653 // Test splash damage with friendly fire.
654 data.splash = {};
655 data.splash.friendlyFire = true;
656 data.splash.radius = 10;
657 data.splash.shape = "Circular";
658 data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } };
659
660 AddMock(SYSTEM_ENTITY, IID_RangeManager, {
661 "ExecuteQueryAroundPos": () => [61, 62]
662 });
663
664 AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
665 "DistanceToPoint": (ent) => ({
666 "61": 0,
667 "62": 5
668 }[ent])
669 });
670
671 dealtDamage = 0;
672 AddMock(61, IID_Health, {
673 "TakeDamage": (amount, __, ___) => {
674 hitEnts.add(61);
675 dealtDamage += amount;
676 return { "healthChange": -amount };
677 }
678 });
679
680 AddMock(62, IID_Position, {
681 "GetPosition": () => new Vector3D(8, 10, 0),
682 "GetPreviousPosition": () => new Vector3D(8, 10, 0),
683 "GetPosition2D": () => new Vector2D(8, 0),
684 "IsInWorld": () => true,
685 });
686
687 AddMock(62, IID_Health, {
688 "TakeDamage": (amount, __, ___) => {
689 hitEnts.add(62);
690 TS_ASSERT_EQUALS(amount, 200 * 0.75);
691 return { "healtChange": -amount };
692 }
693 });
694
695 AddMock(62, IID_Footprint, {
696 "GetShape": () => ({ "type": "circle", "radius": 20 }),
697 });
698
699 cmpDelayedDamage.MissileHit(data, 0);
700 TS_ASSERT(hitEnts.has(61));
701 TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
702 dealtDamage = 0;
703 TS_ASSERT(hitEnts.has(62));
704 hitEnts.clear();
705}
706
707Test_MissileHit();
Note: See TracBrowser for help on using the repository browser.