diff --git a/public/missiles/bladefury.json b/public/missiles/bladefury.json new file mode 100644 index 0000000..ae4e0e0 --- /dev/null +++ b/public/missiles/bladefury.json @@ -0,0 +1,69 @@ +{ + "name": "bladefury", + "celFile": "Expansion/bladefury", + "width": 512, + "height": 6, + "directions": 1, + "framesPerDirection": 7, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 13, + 6, + -6, + -34 + ], + [ + 14, + 0, + 13, + 6, + -6, + -34 + ], + [ + 28, + 0, + 13, + 6, + -6, + -34 + ], + [ + 42, + 0, + 13, + 6, + -6, + -34 + ], + [ + 56, + 0, + 13, + 6, + -6, + -34 + ], + [ + 70, + 0, + 13, + 6, + -6, + -34 + ], + [ + 84, + 0, + 13, + 6, + -6, + -34 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/bladefury.png b/public/missiles/bladefury.png new file mode 100644 index 0000000..6c29829 Binary files /dev/null and b/public/missiles/bladefury.png differ diff --git a/public/missiles/bladefuryb.json b/public/missiles/bladefuryb.json new file mode 100644 index 0000000..ef3d665 --- /dev/null +++ b/public/missiles/bladefuryb.json @@ -0,0 +1,69 @@ +{ + "name": "bladefuryb", + "celFile": "Expansion/bladefury_B", + "width": 512, + "height": 9, + "directions": 1, + "framesPerDirection": 7, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 20, + 9, + -10, + -35 + ], + [ + 21, + 0, + 20, + 9, + -10, + -35 + ], + [ + 42, + 0, + 20, + 9, + -10, + -35 + ], + [ + 63, + 0, + 20, + 9, + -10, + -35 + ], + [ + 84, + 0, + 20, + 9, + -10, + -35 + ], + [ + 105, + 0, + 20, + 9, + -10, + -35 + ], + [ + 126, + 0, + 20, + 9, + -10, + -35 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/bladefuryb.png b/public/missiles/bladefuryb.png new file mode 100644 index 0000000..01e9e03 Binary files /dev/null and b/public/missiles/bladefuryb.png differ diff --git a/public/missiles/bladefuryc.json b/public/missiles/bladefuryc.json new file mode 100644 index 0000000..d0a634a --- /dev/null +++ b/public/missiles/bladefuryc.json @@ -0,0 +1,69 @@ +{ + "name": "bladefuryc", + "celFile": "Expansion/bladefury_C", + "width": 512, + "height": 11, + "directions": 1, + "framesPerDirection": 7, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 24, + 11, + -12, + -36 + ], + [ + 25, + 0, + 24, + 11, + -12, + -36 + ], + [ + 50, + 0, + 24, + 11, + -12, + -36 + ], + [ + 75, + 0, + 24, + 11, + -12, + -36 + ], + [ + 100, + 0, + 24, + 11, + -12, + -36 + ], + [ + 125, + 0, + 24, + 11, + -12, + -36 + ], + [ + 150, + 0, + 24, + 11, + -12, + -36 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/bladefuryc.png b/public/missiles/bladefuryc.png new file mode 100644 index 0000000..0311b73 Binary files /dev/null and b/public/missiles/bladefuryc.png differ diff --git a/public/missiles/bladeshieldmissile.json b/public/missiles/bladeshieldmissile.json new file mode 100644 index 0000000..6091bde --- /dev/null +++ b/public/missiles/bladeshieldmissile.json @@ -0,0 +1,53 @@ +{ + "name": "bladeshieldmissile", + "celFile": "Expansion/blade_shield_missile", + "width": 512, + "height": 11, + "directions": 1, + "framesPerDirection": 5, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 20, + 11, + -10, + -7 + ], + [ + 21, + 0, + 20, + 11, + -10, + -7 + ], + [ + 42, + 0, + 20, + 11, + -10, + -7 + ], + [ + 63, + 0, + 20, + 11, + -10, + -7 + ], + [ + 84, + 0, + 20, + 11, + -10, + -7 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/bladeshieldmissile.png b/public/missiles/bladeshieldmissile.png new file mode 100644 index 0000000..83332c5 Binary files /dev/null and b/public/missiles/bladeshieldmissile.png differ diff --git a/public/missiles/deathsentryexplode.json b/public/missiles/deathsentryexplode.json new file mode 100644 index 0000000..a714aeb --- /dev/null +++ b/public/missiles/deathsentryexplode.json @@ -0,0 +1,109 @@ +{ + "name": "deathsentryexplode", + "celFile": "Expansion/dethsentry02", + "width": 512, + "height": 275, + "directions": 1, + "framesPerDirection": 12, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 115, + 91, + -66, + -66 + ], + [ + 116, + 0, + 115, + 91, + -66, + -66 + ], + [ + 232, + 0, + 115, + 91, + -66, + -66 + ], + [ + 348, + 0, + 115, + 91, + -66, + -66 + ], + [ + 0, + 92, + 115, + 91, + -66, + -66 + ], + [ + 116, + 92, + 115, + 91, + -66, + -66 + ], + [ + 232, + 92, + 115, + 91, + -66, + -66 + ], + [ + 348, + 92, + 115, + 91, + -66, + -66 + ], + [ + 0, + 184, + 115, + 91, + -66, + -66 + ], + [ + 116, + 184, + 115, + 91, + -66, + -66 + ], + [ + 232, + 184, + 115, + 91, + -66, + -66 + ], + [ + 348, + 184, + 115, + 91, + -66, + -66 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/deathsentryexplode.png b/public/missiles/deathsentryexplode.png new file mode 100644 index 0000000..eca33e7 Binary files /dev/null and b/public/missiles/deathsentryexplode.png differ diff --git a/public/missiles/firetrauma.json b/public/missiles/firetrauma.json new file mode 100644 index 0000000..cb6198e --- /dev/null +++ b/public/missiles/firetrauma.json @@ -0,0 +1,173 @@ +{ + "name": "firetrauma", + "celFile": "expansion/FireTraumaMissile", + "width": 512, + "height": 10, + "directions": 1, + "framesPerDirection": 20, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 12, + 10, + -6, + -13 + ], + [ + 13, + 0, + 12, + 10, + -6, + -13 + ], + [ + 26, + 0, + 12, + 10, + -6, + -13 + ], + [ + 39, + 0, + 12, + 10, + -6, + -13 + ], + [ + 52, + 0, + 12, + 10, + -6, + -13 + ], + [ + 65, + 0, + 12, + 10, + -6, + -13 + ], + [ + 78, + 0, + 12, + 10, + -6, + -13 + ], + [ + 91, + 0, + 12, + 10, + -6, + -13 + ], + [ + 104, + 0, + 12, + 10, + -6, + -13 + ], + [ + 117, + 0, + 12, + 10, + -6, + -13 + ], + [ + 130, + 0, + 12, + 10, + -6, + -13 + ], + [ + 143, + 0, + 12, + 10, + -6, + -13 + ], + [ + 156, + 0, + 12, + 10, + -6, + -13 + ], + [ + 169, + 0, + 12, + 10, + -6, + -13 + ], + [ + 182, + 0, + 12, + 10, + -6, + -13 + ], + [ + 195, + 0, + 12, + 10, + -6, + -13 + ], + [ + 208, + 0, + 12, + 10, + -6, + -13 + ], + [ + 221, + 0, + 12, + 10, + -6, + -13 + ], + [ + 234, + 0, + 12, + 10, + -6, + -13 + ], + [ + 247, + 0, + 12, + 10, + -6, + -13 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/firetrauma.png b/public/missiles/firetrauma.png new file mode 100644 index 0000000..3dc65d4 Binary files /dev/null and b/public/missiles/firetrauma.png differ diff --git a/public/missiles/firetraumaexplode.json b/public/missiles/firetraumaexplode.json new file mode 100644 index 0000000..fdd2801 --- /dev/null +++ b/public/missiles/firetraumaexplode.json @@ -0,0 +1,173 @@ +{ + "name": "firetraumaexplode", + "celFile": "expansion/FireTraumaXplosion", + "width": 512, + "height": 451, + "directions": 1, + "framesPerDirection": 20, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 87, + 112, + -45, + -111 + ], + [ + 88, + 0, + 87, + 112, + -45, + -111 + ], + [ + 176, + 0, + 87, + 112, + -45, + -111 + ], + [ + 264, + 0, + 87, + 112, + -45, + -111 + ], + [ + 352, + 0, + 87, + 112, + -45, + -111 + ], + [ + 0, + 113, + 87, + 112, + -45, + -111 + ], + [ + 88, + 113, + 87, + 112, + -45, + -111 + ], + [ + 176, + 113, + 87, + 112, + -45, + -111 + ], + [ + 264, + 113, + 87, + 112, + -45, + -111 + ], + [ + 352, + 113, + 87, + 112, + -45, + -111 + ], + [ + 0, + 226, + 87, + 112, + -45, + -111 + ], + [ + 88, + 226, + 87, + 112, + -45, + -111 + ], + [ + 176, + 226, + 87, + 112, + -45, + -111 + ], + [ + 264, + 226, + 87, + 112, + -45, + -111 + ], + [ + 352, + 226, + 87, + 112, + -45, + -111 + ], + [ + 0, + 339, + 87, + 112, + -45, + -111 + ], + [ + 88, + 339, + 87, + 112, + -45, + -111 + ], + [ + 176, + 339, + 87, + 112, + -45, + -111 + ], + [ + 264, + 339, + 87, + 112, + -45, + -111 + ], + [ + 352, + 339, + 87, + 112, + -45, + -111 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/firetraumaexplode.png b/public/missiles/firetraumaexplode.png new file mode 100644 index 0000000..8163fe0 Binary files /dev/null and b/public/missiles/firetraumaexplode.png differ diff --git a/public/missiles/firetraumaground.json b/public/missiles/firetraumaground.json new file mode 100644 index 0000000..ef7fec5 --- /dev/null +++ b/public/missiles/firetraumaground.json @@ -0,0 +1,21 @@ +{ + "name": "firetraumaground", + "celFile": "expansion/FireTraumaGround", + "width": 512, + "height": 10, + "directions": 1, + "framesPerDirection": 1, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 11, + 10, + -5, + -13 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/firetraumaground.png b/public/missiles/firetraumaground.png new file mode 100644 index 0000000..3395ab7 Binary files /dev/null and b/public/missiles/firetraumaground.png differ diff --git a/public/missiles/shockfieldair.json b/public/missiles/shockfieldair.json new file mode 100644 index 0000000..e2e28c7 --- /dev/null +++ b/public/missiles/shockfieldair.json @@ -0,0 +1,2123 @@ +{ + "name": "shockfieldair", + "celFile": "expansion/caltrops", + "width": 512, + "height": 65, + "directions": 32, + "framesPerDirection": 8, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 11, + 10, + -5, + -1 + ], + [ + 12, + 0, + 11, + 10, + -5, + -1 + ], + [ + 24, + 0, + 11, + 10, + -5, + -1 + ], + [ + 36, + 0, + 11, + 10, + -5, + -1 + ], + [ + 48, + 0, + 11, + 10, + -5, + -1 + ], + [ + 60, + 0, + 11, + 10, + -5, + -1 + ], + [ + 72, + 0, + 11, + 10, + -5, + -1 + ], + [ + 84, + 0, + 11, + 10, + -5, + -1 + ] + ], + [ + [ + 96, + 0, + 10, + 10, + -2, + 0 + ], + [ + 107, + 0, + 10, + 10, + -2, + 0 + ], + [ + 118, + 0, + 10, + 10, + -2, + 0 + ], + [ + 129, + 0, + 10, + 10, + -2, + 0 + ], + [ + 140, + 0, + 10, + 10, + -2, + 0 + ], + [ + 151, + 0, + 10, + 10, + -2, + 0 + ], + [ + 162, + 0, + 10, + 10, + -2, + 0 + ], + [ + 173, + 0, + 10, + 10, + -2, + 0 + ] + ], + [ + [ + 184, + 0, + 9, + 9, + -5, + 2 + ], + [ + 194, + 0, + 9, + 9, + -5, + 2 + ], + [ + 204, + 0, + 9, + 9, + -5, + 2 + ], + [ + 214, + 0, + 9, + 9, + -5, + 2 + ], + [ + 224, + 0, + 9, + 9, + -5, + 2 + ], + [ + 234, + 0, + 9, + 9, + -5, + 2 + ], + [ + 244, + 0, + 9, + 9, + -5, + 2 + ], + [ + 254, + 0, + 9, + 9, + -5, + 2 + ] + ], + [ + [ + 264, + 0, + 11, + 10, + -8, + 0 + ], + [ + 276, + 0, + 11, + 10, + -8, + 0 + ], + [ + 288, + 0, + 11, + 10, + -8, + 0 + ], + [ + 300, + 0, + 11, + 10, + -8, + 0 + ], + [ + 312, + 0, + 11, + 10, + -8, + 0 + ], + [ + 324, + 0, + 11, + 10, + -8, + 0 + ], + [ + 336, + 0, + 11, + 10, + -8, + 0 + ], + [ + 348, + 0, + 11, + 10, + -8, + 0 + ] + ], + [ + [ + 360, + 0, + 7, + 10, + -5, + -1 + ], + [ + 368, + 0, + 7, + 10, + -5, + -1 + ], + [ + 376, + 0, + 7, + 10, + -5, + -1 + ], + [ + 384, + 0, + 7, + 10, + -5, + -1 + ], + [ + 392, + 0, + 7, + 10, + -5, + -1 + ], + [ + 400, + 0, + 7, + 10, + -5, + -1 + ], + [ + 408, + 0, + 7, + 10, + -5, + -1 + ], + [ + 416, + 0, + 7, + 10, + -5, + -1 + ] + ], + [ + [ + 424, + 0, + 10, + 9, + -3, + 0 + ], + [ + 435, + 0, + 10, + 9, + -3, + 0 + ], + [ + 446, + 0, + 10, + 9, + -3, + 0 + ], + [ + 457, + 0, + 10, + 9, + -3, + 0 + ], + [ + 468, + 0, + 10, + 9, + -3, + 0 + ], + [ + 479, + 0, + 10, + 9, + -3, + 0 + ], + [ + 490, + 0, + 10, + 9, + -3, + 0 + ], + [ + 501, + 0, + 10, + 9, + -3, + 0 + ] + ], + [ + [ + 0, + 11, + 8, + 10, + -2, + 1 + ], + [ + 9, + 11, + 8, + 10, + -2, + 1 + ], + [ + 18, + 11, + 8, + 10, + -2, + 1 + ], + [ + 27, + 11, + 8, + 10, + -2, + 1 + ], + [ + 36, + 11, + 8, + 10, + -2, + 1 + ], + [ + 45, + 11, + 8, + 10, + -2, + 1 + ], + [ + 54, + 11, + 8, + 10, + -2, + 1 + ], + [ + 63, + 11, + 8, + 10, + -2, + 1 + ] + ], + [ + [ + 72, + 11, + 10, + 10, + -7, + 1 + ], + [ + 83, + 11, + 10, + 10, + -7, + 1 + ], + [ + 94, + 11, + 10, + 10, + -7, + 1 + ], + [ + 105, + 11, + 10, + 10, + -7, + 1 + ], + [ + 116, + 11, + 10, + 10, + -7, + 1 + ], + [ + 127, + 11, + 10, + 10, + -7, + 1 + ], + [ + 138, + 11, + 10, + 10, + -7, + 1 + ], + [ + 149, + 11, + 10, + 10, + -7, + 1 + ] + ], + [ + [ + 160, + 11, + 9, + 10, + -5, + -1 + ], + [ + 170, + 11, + 9, + 10, + -5, + -1 + ], + [ + 180, + 11, + 9, + 10, + -5, + -1 + ], + [ + 190, + 11, + 9, + 10, + -5, + -1 + ], + [ + 200, + 11, + 9, + 10, + -5, + -1 + ], + [ + 210, + 11, + 9, + 10, + -5, + -1 + ], + [ + 220, + 11, + 9, + 10, + -5, + -1 + ], + [ + 230, + 11, + 9, + 10, + -5, + -1 + ] + ], + [ + [ + 240, + 11, + 10, + 10, + -3, + -1 + ], + [ + 251, + 11, + 10, + 10, + -3, + -1 + ], + [ + 262, + 11, + 10, + 10, + -3, + -1 + ], + [ + 273, + 11, + 10, + 10, + -3, + -1 + ], + [ + 284, + 11, + 10, + 10, + -3, + -1 + ], + [ + 295, + 11, + 10, + 10, + -3, + -1 + ], + [ + 306, + 11, + 10, + 10, + -3, + -1 + ], + [ + 317, + 11, + 10, + 10, + -3, + -1 + ] + ], + [ + [ + 328, + 11, + 11, + 10, + -3, + 0 + ], + [ + 340, + 11, + 11, + 10, + -3, + 0 + ], + [ + 352, + 11, + 11, + 10, + -3, + 0 + ], + [ + 364, + 11, + 11, + 10, + -3, + 0 + ], + [ + 376, + 11, + 11, + 10, + -3, + 0 + ], + [ + 388, + 11, + 11, + 10, + -3, + 0 + ], + [ + 400, + 11, + 11, + 10, + -3, + 0 + ], + [ + 412, + 11, + 11, + 10, + -3, + 0 + ] + ], + [ + [ + 424, + 11, + 10, + 10, + -3, + 1 + ], + [ + 435, + 11, + 10, + 10, + -3, + 1 + ], + [ + 446, + 11, + 10, + 10, + -3, + 1 + ], + [ + 457, + 11, + 10, + 10, + -3, + 1 + ], + [ + 468, + 11, + 10, + 10, + -3, + 1 + ], + [ + 479, + 11, + 10, + 10, + -3, + 1 + ], + [ + 490, + 11, + 10, + 10, + -3, + 1 + ], + [ + 501, + 11, + 10, + 10, + -3, + 1 + ] + ], + [ + [ + 0, + 22, + 9, + 10, + -3, + 1 + ], + [ + 10, + 22, + 9, + 10, + -3, + 1 + ], + [ + 20, + 22, + 9, + 10, + -3, + 1 + ], + [ + 30, + 22, + 9, + 10, + -3, + 1 + ], + [ + 40, + 22, + 9, + 10, + -3, + 1 + ], + [ + 50, + 22, + 9, + 10, + -3, + 1 + ], + [ + 60, + 22, + 9, + 10, + -3, + 1 + ], + [ + 70, + 22, + 9, + 10, + -3, + 1 + ] + ], + [ + [ + 80, + 22, + 10, + 10, + -6, + 1 + ], + [ + 91, + 22, + 10, + 10, + -6, + 1 + ], + [ + 102, + 22, + 10, + 10, + -6, + 1 + ], + [ + 113, + 22, + 10, + 10, + -6, + 1 + ], + [ + 124, + 22, + 10, + 10, + -6, + 1 + ], + [ + 135, + 22, + 10, + 10, + -6, + 1 + ], + [ + 146, + 22, + 10, + 10, + -6, + 1 + ], + [ + 157, + 22, + 10, + 10, + -6, + 1 + ] + ], + [ + [ + 168, + 22, + 10, + 9, + -8, + 1 + ], + [ + 179, + 22, + 10, + 9, + -8, + 1 + ], + [ + 190, + 22, + 10, + 9, + -8, + 1 + ], + [ + 201, + 22, + 10, + 9, + -8, + 1 + ], + [ + 212, + 22, + 10, + 9, + -8, + 1 + ], + [ + 223, + 22, + 10, + 9, + -8, + 1 + ], + [ + 234, + 22, + 10, + 9, + -8, + 1 + ], + [ + 245, + 22, + 10, + 9, + -8, + 1 + ] + ], + [ + [ + 256, + 22, + 9, + 9, + -6, + 0 + ], + [ + 266, + 22, + 9, + 9, + -6, + 0 + ], + [ + 276, + 22, + 9, + 9, + -6, + 0 + ], + [ + 286, + 22, + 9, + 9, + -6, + 0 + ], + [ + 296, + 22, + 9, + 9, + -6, + 0 + ], + [ + 306, + 22, + 9, + 9, + -6, + 0 + ], + [ + 316, + 22, + 9, + 9, + -6, + 0 + ], + [ + 326, + 22, + 9, + 9, + -6, + 0 + ] + ], + [ + [ + 336, + 22, + 8, + 10, + -5, + -1 + ], + [ + 345, + 22, + 8, + 10, + -5, + -1 + ], + [ + 354, + 22, + 8, + 10, + -5, + -1 + ], + [ + 363, + 22, + 8, + 10, + -5, + -1 + ], + [ + 372, + 22, + 8, + 10, + -5, + -1 + ], + [ + 381, + 22, + 8, + 10, + -5, + -1 + ], + [ + 390, + 22, + 8, + 10, + -5, + -1 + ], + [ + 399, + 22, + 8, + 10, + -5, + -1 + ] + ], + [ + [ + 408, + 22, + 9, + 10, + -5, + -1 + ], + [ + 418, + 22, + 9, + 10, + -5, + -1 + ], + [ + 428, + 22, + 9, + 10, + -5, + -1 + ], + [ + 438, + 22, + 9, + 10, + -5, + -1 + ], + [ + 448, + 22, + 9, + 10, + -5, + -1 + ], + [ + 458, + 22, + 9, + 10, + -5, + -1 + ], + [ + 468, + 22, + 9, + 10, + -5, + -1 + ], + [ + 478, + 22, + 9, + 10, + -5, + -1 + ] + ], + [ + [ + 488, + 22, + 10, + 10, + -4, + -1 + ], + [ + 499, + 22, + 10, + 10, + -4, + -1 + ], + [ + 0, + 33, + 10, + 10, + -4, + -1 + ], + [ + 11, + 33, + 10, + 10, + -4, + -1 + ], + [ + 22, + 33, + 10, + 10, + -4, + -1 + ], + [ + 33, + 33, + 10, + 10, + -4, + -1 + ], + [ + 44, + 33, + 10, + 10, + -4, + -1 + ], + [ + 55, + 33, + 10, + 10, + -4, + -1 + ] + ], + [ + [ + 66, + 33, + 10, + 9, + -3, + 0 + ], + [ + 77, + 33, + 10, + 9, + -3, + 0 + ], + [ + 88, + 33, + 10, + 9, + -3, + 0 + ], + [ + 99, + 33, + 10, + 9, + -3, + 0 + ], + [ + 110, + 33, + 10, + 9, + -3, + 0 + ], + [ + 121, + 33, + 10, + 9, + -3, + 0 + ], + [ + 132, + 33, + 10, + 9, + -3, + 0 + ], + [ + 143, + 33, + 10, + 9, + -3, + 0 + ] + ], + [ + [ + 154, + 33, + 9, + 10, + -2, + 0 + ], + [ + 164, + 33, + 9, + 10, + -2, + 0 + ], + [ + 174, + 33, + 9, + 10, + -2, + 0 + ], + [ + 184, + 33, + 9, + 10, + -2, + 0 + ], + [ + 194, + 33, + 9, + 10, + -2, + 0 + ], + [ + 204, + 33, + 9, + 10, + -2, + 0 + ], + [ + 214, + 33, + 9, + 10, + -2, + 0 + ], + [ + 224, + 33, + 9, + 10, + -2, + 0 + ] + ], + [ + [ + 234, + 33, + 9, + 10, + -2, + 0 + ], + [ + 244, + 33, + 9, + 10, + -2, + 0 + ], + [ + 254, + 33, + 9, + 10, + -2, + 0 + ], + [ + 264, + 33, + 9, + 10, + -2, + 0 + ], + [ + 274, + 33, + 9, + 10, + -2, + 0 + ], + [ + 284, + 33, + 9, + 10, + -2, + 0 + ], + [ + 294, + 33, + 9, + 10, + -2, + 0 + ], + [ + 304, + 33, + 9, + 10, + -2, + 0 + ] + ], + [ + [ + 314, + 33, + 10, + 10, + -3, + 0 + ], + [ + 325, + 33, + 10, + 10, + -3, + 0 + ], + [ + 336, + 33, + 10, + 10, + -3, + 0 + ], + [ + 347, + 33, + 10, + 10, + -3, + 0 + ], + [ + 358, + 33, + 10, + 10, + -3, + 0 + ], + [ + 369, + 33, + 10, + 10, + -3, + 0 + ], + [ + 380, + 33, + 10, + 10, + -3, + 0 + ], + [ + 391, + 33, + 10, + 10, + -3, + 0 + ] + ], + [ + [ + 402, + 33, + 9, + 10, + -2, + 1 + ], + [ + 412, + 33, + 9, + 10, + -2, + 1 + ], + [ + 422, + 33, + 9, + 10, + -2, + 1 + ], + [ + 432, + 33, + 9, + 10, + -2, + 1 + ], + [ + 442, + 33, + 9, + 10, + -2, + 1 + ], + [ + 452, + 33, + 9, + 10, + -2, + 1 + ], + [ + 462, + 33, + 9, + 10, + -2, + 1 + ], + [ + 472, + 33, + 9, + 10, + -2, + 1 + ] + ], + [ + [ + 482, + 33, + 8, + 10, + -3, + 1 + ], + [ + 491, + 33, + 8, + 10, + -3, + 1 + ], + [ + 500, + 33, + 8, + 10, + -3, + 1 + ], + [ + 0, + 44, + 8, + 10, + -3, + 1 + ], + [ + 9, + 44, + 8, + 10, + -3, + 1 + ], + [ + 18, + 44, + 8, + 10, + -3, + 1 + ], + [ + 27, + 44, + 8, + 10, + -3, + 1 + ], + [ + 36, + 44, + 8, + 10, + -3, + 1 + ] + ], + [ + [ + 45, + 44, + 10, + 10, + -5, + 1 + ], + [ + 56, + 44, + 10, + 10, + -5, + 1 + ], + [ + 67, + 44, + 10, + 10, + -5, + 1 + ], + [ + 78, + 44, + 10, + 10, + -5, + 1 + ], + [ + 89, + 44, + 10, + 10, + -5, + 1 + ], + [ + 100, + 44, + 10, + 10, + -5, + 1 + ], + [ + 111, + 44, + 10, + 10, + -5, + 1 + ], + [ + 122, + 44, + 10, + 10, + -5, + 1 + ] + ], + [ + [ + 133, + 44, + 10, + 10, + -6, + 1 + ], + [ + 144, + 44, + 10, + 10, + -6, + 1 + ], + [ + 155, + 44, + 10, + 10, + -6, + 1 + ], + [ + 166, + 44, + 10, + 10, + -6, + 1 + ], + [ + 177, + 44, + 10, + 10, + -6, + 1 + ], + [ + 188, + 44, + 10, + 10, + -6, + 1 + ], + [ + 199, + 44, + 10, + 10, + -6, + 1 + ], + [ + 210, + 44, + 10, + 10, + -6, + 1 + ] + ], + [ + [ + 221, + 44, + 10, + 9, + -7, + 1 + ], + [ + 232, + 44, + 10, + 9, + -7, + 1 + ], + [ + 243, + 44, + 10, + 9, + -7, + 1 + ], + [ + 254, + 44, + 10, + 9, + -7, + 1 + ], + [ + 265, + 44, + 10, + 9, + -7, + 1 + ], + [ + 276, + 44, + 10, + 9, + -7, + 1 + ], + [ + 287, + 44, + 10, + 9, + -7, + 1 + ], + [ + 298, + 44, + 10, + 9, + -7, + 1 + ] + ], + [ + [ + 309, + 44, + 9, + 10, + -7, + 1 + ], + [ + 319, + 44, + 9, + 10, + -7, + 1 + ], + [ + 329, + 44, + 9, + 10, + -7, + 1 + ], + [ + 339, + 44, + 9, + 10, + -7, + 1 + ], + [ + 349, + 44, + 9, + 10, + -7, + 1 + ], + [ + 359, + 44, + 9, + 10, + -7, + 1 + ], + [ + 369, + 44, + 9, + 10, + -7, + 1 + ], + [ + 379, + 44, + 9, + 10, + -7, + 1 + ] + ], + [ + [ + 389, + 44, + 10, + 10, + -8, + 0 + ], + [ + 400, + 44, + 10, + 10, + -8, + 0 + ], + [ + 411, + 44, + 10, + 10, + -8, + 0 + ], + [ + 422, + 44, + 10, + 10, + -8, + 0 + ], + [ + 433, + 44, + 10, + 10, + -8, + 0 + ], + [ + 444, + 44, + 10, + 10, + -8, + 0 + ], + [ + 455, + 44, + 10, + 10, + -8, + 0 + ], + [ + 466, + 44, + 10, + 10, + -8, + 0 + ] + ], + [ + [ + 477, + 44, + 9, + 10, + -7, + -1 + ], + [ + 487, + 44, + 9, + 10, + -7, + -1 + ], + [ + 497, + 44, + 9, + 10, + -7, + -1 + ], + [ + 0, + 55, + 9, + 10, + -7, + -1 + ], + [ + 10, + 55, + 9, + 10, + -7, + -1 + ], + [ + 20, + 55, + 9, + 10, + -7, + -1 + ], + [ + 30, + 55, + 9, + 10, + -7, + -1 + ], + [ + 40, + 55, + 9, + 10, + -7, + -1 + ] + ], + [ + [ + 50, + 55, + 9, + 10, + -6, + -1 + ], + [ + 60, + 55, + 9, + 10, + -6, + -1 + ], + [ + 70, + 55, + 9, + 10, + -6, + -1 + ], + [ + 80, + 55, + 9, + 10, + -6, + -1 + ], + [ + 90, + 55, + 9, + 10, + -6, + -1 + ], + [ + 100, + 55, + 9, + 10, + -6, + -1 + ], + [ + 110, + 55, + 9, + 10, + -6, + -1 + ], + [ + 120, + 55, + 9, + 10, + -6, + -1 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/shockfieldair.png b/public/missiles/shockfieldair.png new file mode 100644 index 0000000..4c744fa Binary files /dev/null and b/public/missiles/shockfieldair.png differ diff --git a/public/missiles/shockfieldground.json b/public/missiles/shockfieldground.json new file mode 100644 index 0000000..5fa1de5 --- /dev/null +++ b/public/missiles/shockfieldground.json @@ -0,0 +1,667 @@ +{ + "name": "shockfieldground", + "celFile": "Expansion/ShockField", + "width": 1024, + "height": 647, + "directions": 8, + "framesPerDirection": 10, + "animSpeed": 16, + "groups": [ + [ + [ + 0, + 0, + 102, + 60, + -56, + -32 + ], + [ + 103, + 0, + 102, + 60, + -56, + -32 + ], + [ + 206, + 0, + 102, + 60, + -56, + -32 + ], + [ + 309, + 0, + 102, + 60, + -56, + -32 + ], + [ + 412, + 0, + 102, + 60, + -56, + -32 + ], + [ + 515, + 0, + 102, + 60, + -56, + -32 + ], + [ + 618, + 0, + 102, + 60, + -56, + -32 + ], + [ + 721, + 0, + 102, + 60, + -56, + -32 + ], + [ + 824, + 0, + 102, + 60, + -56, + -32 + ], + [ + 0, + 61, + 102, + 60, + -56, + -32 + ] + ], + [ + [ + 103, + 61, + 95, + 60, + -40, + -31 + ], + [ + 199, + 61, + 95, + 60, + -40, + -31 + ], + [ + 295, + 61, + 95, + 60, + -40, + -31 + ], + [ + 391, + 61, + 95, + 60, + -40, + -31 + ], + [ + 487, + 61, + 95, + 60, + -40, + -31 + ], + [ + 583, + 61, + 95, + 60, + -40, + -31 + ], + [ + 679, + 61, + 95, + 60, + -40, + -31 + ], + [ + 775, + 61, + 95, + 60, + -40, + -31 + ], + [ + 871, + 61, + 95, + 60, + -40, + -31 + ], + [ + 0, + 122, + 95, + 60, + -40, + -31 + ] + ], + [ + [ + 96, + 122, + 100, + 77, + -43, + -31 + ], + [ + 197, + 122, + 100, + 77, + -43, + -31 + ], + [ + 298, + 122, + 100, + 77, + -43, + -31 + ], + [ + 399, + 122, + 100, + 77, + -43, + -31 + ], + [ + 500, + 122, + 100, + 77, + -43, + -31 + ], + [ + 601, + 122, + 100, + 77, + -43, + -31 + ], + [ + 702, + 122, + 100, + 77, + -43, + -31 + ], + [ + 803, + 122, + 100, + 77, + -43, + -31 + ], + [ + 904, + 122, + 100, + 77, + -43, + -31 + ], + [ + 0, + 200, + 100, + 77, + -43, + -31 + ] + ], + [ + [ + 101, + 200, + 96, + 78, + -49, + -33 + ], + [ + 198, + 200, + 96, + 78, + -49, + -33 + ], + [ + 295, + 200, + 96, + 78, + -49, + -33 + ], + [ + 392, + 200, + 96, + 78, + -49, + -33 + ], + [ + 489, + 200, + 96, + 78, + -49, + -33 + ], + [ + 586, + 200, + 96, + 78, + -49, + -33 + ], + [ + 683, + 200, + 96, + 78, + -49, + -33 + ], + [ + 780, + 200, + 96, + 78, + -49, + -33 + ], + [ + 877, + 200, + 96, + 78, + -49, + -33 + ], + [ + 0, + 279, + 96, + 78, + -49, + -33 + ] + ], + [ + [ + 97, + 279, + 95, + 77, + -41, + -34 + ], + [ + 193, + 279, + 95, + 77, + -41, + -34 + ], + [ + 289, + 279, + 95, + 77, + -41, + -34 + ], + [ + 385, + 279, + 95, + 77, + -41, + -34 + ], + [ + 481, + 279, + 95, + 77, + -41, + -34 + ], + [ + 577, + 279, + 95, + 77, + -41, + -34 + ], + [ + 673, + 279, + 95, + 77, + -41, + -34 + ], + [ + 769, + 279, + 95, + 77, + -41, + -34 + ], + [ + 865, + 279, + 95, + 77, + -41, + -34 + ], + [ + 0, + 358, + 95, + 77, + -41, + -34 + ] + ], + [ + [ + 96, + 358, + 105, + 56, + -50, + -27 + ], + [ + 202, + 358, + 105, + 56, + -50, + -27 + ], + [ + 308, + 358, + 105, + 56, + -50, + -27 + ], + [ + 414, + 358, + 105, + 56, + -50, + -27 + ], + [ + 520, + 358, + 105, + 56, + -50, + -27 + ], + [ + 626, + 358, + 105, + 56, + -50, + -27 + ], + [ + 732, + 358, + 105, + 56, + -50, + -27 + ], + [ + 838, + 358, + 105, + 56, + -50, + -27 + ], + [ + 0, + 436, + 105, + 56, + -50, + -27 + ], + [ + 106, + 436, + 105, + 56, + -50, + -27 + ] + ], + [ + [ + 212, + 436, + 93, + 70, + -53, + -33 + ], + [ + 306, + 436, + 93, + 70, + -53, + -33 + ], + [ + 400, + 436, + 93, + 70, + -53, + -33 + ], + [ + 494, + 436, + 93, + 70, + -53, + -33 + ], + [ + 588, + 436, + 93, + 70, + -53, + -33 + ], + [ + 682, + 436, + 93, + 70, + -53, + -33 + ], + [ + 776, + 436, + 93, + 70, + -53, + -33 + ], + [ + 870, + 436, + 93, + 70, + -53, + -33 + ], + [ + 0, + 507, + 93, + 70, + -53, + -33 + ], + [ + 94, + 507, + 93, + 70, + -53, + -33 + ] + ], + [ + [ + 188, + 507, + 101, + 69, + -55, + -32 + ], + [ + 290, + 507, + 101, + 69, + -55, + -32 + ], + [ + 392, + 507, + 101, + 69, + -55, + -32 + ], + [ + 494, + 507, + 101, + 69, + -55, + -32 + ], + [ + 596, + 507, + 101, + 69, + -55, + -32 + ], + [ + 698, + 507, + 101, + 69, + -55, + -32 + ], + [ + 800, + 507, + 101, + 69, + -55, + -32 + ], + [ + 902, + 507, + 101, + 69, + -55, + -32 + ], + [ + 0, + 578, + 101, + 69, + -55, + -32 + ], + [ + 102, + 578, + 101, + 69, + -55, + -32 + ] + ] + ] +} \ No newline at end of file diff --git a/public/missiles/shockfieldground.png b/public/missiles/shockfieldground.png new file mode 100644 index 0000000..7568a33 Binary files /dev/null and b/public/missiles/shockfieldground.png differ diff --git a/public/overlays/Expansion/blade_shield.json b/public/overlays/Expansion/blade_shield.json new file mode 100644 index 0000000..2bfaa40 --- /dev/null +++ b/public/overlays/Expansion/blade_shield.json @@ -0,0 +1,188 @@ +{ + "name": "blade_shield", + "celFile": "Expansion/blade_shield", + "width": 512, + "height": 200, + "directions": 1, + "frames": 20, + "framesPerDirection": 20, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -33, + "top": -68, + "width": 64, + "height": 66 + }, + "groups": [ + [ + [ + 0, + 0, + 64, + 66, + -33, + -68 + ], + [ + 65, + 0, + 64, + 66, + -33, + -68 + ], + [ + 130, + 0, + 64, + 66, + -33, + -68 + ], + [ + 195, + 0, + 64, + 66, + -33, + -68 + ], + [ + 260, + 0, + 64, + 66, + -33, + -68 + ], + [ + 325, + 0, + 64, + 66, + -33, + -68 + ], + [ + 390, + 0, + 64, + 66, + -33, + -68 + ], + [ + 0, + 67, + 64, + 66, + -33, + -68 + ], + [ + 65, + 67, + 64, + 66, + -33, + -68 + ], + [ + 130, + 67, + 64, + 66, + -33, + -68 + ], + [ + 195, + 67, + 64, + 66, + -33, + -68 + ], + [ + 260, + 67, + 64, + 66, + -33, + -68 + ], + [ + 325, + 67, + 64, + 66, + -33, + -68 + ], + [ + 390, + 67, + 64, + 66, + -33, + -68 + ], + [ + 0, + 134, + 64, + 66, + -33, + -68 + ], + [ + 65, + 134, + 64, + 66, + -33, + -68 + ], + [ + 130, + 134, + 64, + 66, + -33, + -68 + ], + [ + 195, + 134, + 64, + 66, + -33, + -68 + ], + [ + 260, + 134, + 64, + 66, + -33, + -68 + ], + [ + 325, + 134, + 64, + 66, + -33, + -68 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/Expansion/blade_shield.png b/public/overlays/Expansion/blade_shield.png new file mode 100644 index 0000000..497328c Binary files /dev/null and b/public/overlays/Expansion/blade_shield.png differ diff --git a/public/overlays/blade_shield.json b/public/overlays/blade_shield.json new file mode 100644 index 0000000..2bfaa40 --- /dev/null +++ b/public/overlays/blade_shield.json @@ -0,0 +1,188 @@ +{ + "name": "blade_shield", + "celFile": "Expansion/blade_shield", + "width": 512, + "height": 200, + "directions": 1, + "frames": 20, + "framesPerDirection": 20, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -33, + "top": -68, + "width": 64, + "height": 66 + }, + "groups": [ + [ + [ + 0, + 0, + 64, + 66, + -33, + -68 + ], + [ + 65, + 0, + 64, + 66, + -33, + -68 + ], + [ + 130, + 0, + 64, + 66, + -33, + -68 + ], + [ + 195, + 0, + 64, + 66, + -33, + -68 + ], + [ + 260, + 0, + 64, + 66, + -33, + -68 + ], + [ + 325, + 0, + 64, + 66, + -33, + -68 + ], + [ + 390, + 0, + 64, + 66, + -33, + -68 + ], + [ + 0, + 67, + 64, + 66, + -33, + -68 + ], + [ + 65, + 67, + 64, + 66, + -33, + -68 + ], + [ + 130, + 67, + 64, + 66, + -33, + -68 + ], + [ + 195, + 67, + 64, + 66, + -33, + -68 + ], + [ + 260, + 67, + 64, + 66, + -33, + -68 + ], + [ + 325, + 67, + 64, + 66, + -33, + -68 + ], + [ + 390, + 67, + 64, + 66, + -33, + -68 + ], + [ + 0, + 134, + 64, + 66, + -33, + -68 + ], + [ + 65, + 134, + 64, + 66, + -33, + -68 + ], + [ + 130, + 134, + 64, + 66, + -33, + -68 + ], + [ + 195, + 134, + 64, + 66, + -33, + -68 + ], + [ + 260, + 134, + 64, + 66, + -33, + -68 + ], + [ + 325, + 134, + 64, + 66, + -33, + -68 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/blade_shield.png b/public/overlays/blade_shield.png new file mode 100644 index 0000000..497328c Binary files /dev/null and b/public/overlays/blade_shield.png differ diff --git a/public/overlays/bladeshield.json b/public/overlays/bladeshield.json new file mode 100644 index 0000000..2bfaa40 --- /dev/null +++ b/public/overlays/bladeshield.json @@ -0,0 +1,188 @@ +{ + "name": "blade_shield", + "celFile": "Expansion/blade_shield", + "width": 512, + "height": 200, + "directions": 1, + "frames": 20, + "framesPerDirection": 20, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -33, + "top": -68, + "width": 64, + "height": 66 + }, + "groups": [ + [ + [ + 0, + 0, + 64, + 66, + -33, + -68 + ], + [ + 65, + 0, + 64, + 66, + -33, + -68 + ], + [ + 130, + 0, + 64, + 66, + -33, + -68 + ], + [ + 195, + 0, + 64, + 66, + -33, + -68 + ], + [ + 260, + 0, + 64, + 66, + -33, + -68 + ], + [ + 325, + 0, + 64, + 66, + -33, + -68 + ], + [ + 390, + 0, + 64, + 66, + -33, + -68 + ], + [ + 0, + 67, + 64, + 66, + -33, + -68 + ], + [ + 65, + 67, + 64, + 66, + -33, + -68 + ], + [ + 130, + 67, + 64, + 66, + -33, + -68 + ], + [ + 195, + 67, + 64, + 66, + -33, + -68 + ], + [ + 260, + 67, + 64, + 66, + -33, + -68 + ], + [ + 325, + 67, + 64, + 66, + -33, + -68 + ], + [ + 390, + 67, + 64, + 66, + -33, + -68 + ], + [ + 0, + 134, + 64, + 66, + -33, + -68 + ], + [ + 65, + 134, + 64, + 66, + -33, + -68 + ], + [ + 130, + 134, + 64, + 66, + -33, + -68 + ], + [ + 195, + 134, + 64, + 66, + -33, + -68 + ], + [ + 260, + 134, + 64, + 66, + -33, + -68 + ], + [ + 325, + 134, + 64, + 66, + -33, + -68 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/bladeshield.png b/public/overlays/bladeshield.png new file mode 100644 index 0000000..497328c Binary files /dev/null and b/public/overlays/bladeshield.png differ diff --git a/public/overlays/deathsentry.json b/public/overlays/deathsentry.json new file mode 100644 index 0000000..0fb1569 --- /dev/null +++ b/public/overlays/deathsentry.json @@ -0,0 +1,124 @@ +{ + "name": "deathsentry_explode", + "celFile": "expansion/dethsentry01", + "width": 512, + "height": 547, + "directions": 1, + "frames": 12, + "framesPerDirection": 12, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -73, + "top": -93, + "width": 130, + "height": 136 + }, + "groups": [ + [ + [ + 0, + 0, + 130, + 136, + -73, + -93 + ], + [ + 131, + 0, + 130, + 136, + -73, + -93 + ], + [ + 262, + 0, + 130, + 136, + -73, + -93 + ], + [ + 0, + 137, + 130, + 136, + -73, + -93 + ], + [ + 131, + 137, + 130, + 136, + -73, + -93 + ], + [ + 262, + 137, + 130, + 136, + -73, + -93 + ], + [ + 0, + 274, + 130, + 136, + -73, + -93 + ], + [ + 131, + 274, + 130, + 136, + -73, + -93 + ], + [ + 262, + 274, + 130, + 136, + -73, + -93 + ], + [ + 0, + 411, + 130, + 136, + -73, + -93 + ], + [ + 131, + 411, + 130, + 136, + -73, + -93 + ], + [ + 262, + 411, + 130, + 136, + -73, + -93 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/deathsentry.png b/public/overlays/deathsentry.png new file mode 100644 index 0000000..808ff55 Binary files /dev/null and b/public/overlays/deathsentry.png differ diff --git a/public/overlays/deathsentry_explode.json b/public/overlays/deathsentry_explode.json new file mode 100644 index 0000000..0fb1569 --- /dev/null +++ b/public/overlays/deathsentry_explode.json @@ -0,0 +1,124 @@ +{ + "name": "deathsentry_explode", + "celFile": "expansion/dethsentry01", + "width": 512, + "height": 547, + "directions": 1, + "frames": 12, + "framesPerDirection": 12, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -73, + "top": -93, + "width": 130, + "height": 136 + }, + "groups": [ + [ + [ + 0, + 0, + 130, + 136, + -73, + -93 + ], + [ + 131, + 0, + 130, + 136, + -73, + -93 + ], + [ + 262, + 0, + 130, + 136, + -73, + -93 + ], + [ + 0, + 137, + 130, + 136, + -73, + -93 + ], + [ + 131, + 137, + 130, + 136, + -73, + -93 + ], + [ + 262, + 137, + 130, + 136, + -73, + -93 + ], + [ + 0, + 274, + 130, + 136, + -73, + -93 + ], + [ + 131, + 274, + 130, + 136, + -73, + -93 + ], + [ + 262, + 274, + 130, + 136, + -73, + -93 + ], + [ + 0, + 411, + 130, + 136, + -73, + -93 + ], + [ + 131, + 411, + 130, + 136, + -73, + -93 + ], + [ + 262, + 411, + 130, + 136, + -73, + -93 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/deathsentry_explode.png b/public/overlays/deathsentry_explode.png new file mode 100644 index 0000000..808ff55 Binary files /dev/null and b/public/overlays/deathsentry_explode.png differ diff --git a/public/overlays/dethsentry01.json b/public/overlays/dethsentry01.json new file mode 100644 index 0000000..0fb1569 --- /dev/null +++ b/public/overlays/dethsentry01.json @@ -0,0 +1,124 @@ +{ + "name": "deathsentry_explode", + "celFile": "expansion/dethsentry01", + "width": 512, + "height": 547, + "directions": 1, + "frames": 12, + "framesPerDirection": 12, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -73, + "top": -93, + "width": 130, + "height": 136 + }, + "groups": [ + [ + [ + 0, + 0, + 130, + 136, + -73, + -93 + ], + [ + 131, + 0, + 130, + 136, + -73, + -93 + ], + [ + 262, + 0, + 130, + 136, + -73, + -93 + ], + [ + 0, + 137, + 130, + 136, + -73, + -93 + ], + [ + 131, + 137, + 130, + 136, + -73, + -93 + ], + [ + 262, + 137, + 130, + 136, + -73, + -93 + ], + [ + 0, + 274, + 130, + 136, + -73, + -93 + ], + [ + 131, + 274, + 130, + 136, + -73, + -93 + ], + [ + 262, + 274, + 130, + 136, + -73, + -93 + ], + [ + 0, + 411, + 130, + 136, + -73, + -93 + ], + [ + 131, + 411, + 130, + 136, + -73, + -93 + ], + [ + 262, + 411, + 130, + 136, + -73, + -93 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/dethsentry01.png b/public/overlays/dethsentry01.png new file mode 100644 index 0000000..808ff55 Binary files /dev/null and b/public/overlays/dethsentry01.png differ diff --git a/public/overlays/expansion/dethsentry01.json b/public/overlays/expansion/dethsentry01.json new file mode 100644 index 0000000..0fb1569 --- /dev/null +++ b/public/overlays/expansion/dethsentry01.json @@ -0,0 +1,124 @@ +{ + "name": "deathsentry_explode", + "celFile": "expansion/dethsentry01", + "width": 512, + "height": 547, + "directions": 1, + "frames": 12, + "framesPerDirection": 12, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -73, + "top": -93, + "width": 130, + "height": 136 + }, + "groups": [ + [ + [ + 0, + 0, + 130, + 136, + -73, + -93 + ], + [ + 131, + 0, + 130, + 136, + -73, + -93 + ], + [ + 262, + 0, + 130, + 136, + -73, + -93 + ], + [ + 0, + 137, + 130, + 136, + -73, + -93 + ], + [ + 131, + 137, + 130, + 136, + -73, + -93 + ], + [ + 262, + 137, + 130, + 136, + -73, + -93 + ], + [ + 0, + 274, + 130, + 136, + -73, + -93 + ], + [ + 131, + 274, + 130, + 136, + -73, + -93 + ], + [ + 262, + 274, + 130, + 136, + -73, + -93 + ], + [ + 0, + 411, + 130, + 136, + -73, + -93 + ], + [ + 131, + 411, + 130, + 136, + -73, + -93 + ], + [ + 262, + 411, + 130, + 136, + -73, + -93 + ] + ] + ] +} \ No newline at end of file diff --git a/public/overlays/expansion/dethsentry01.png b/public/overlays/expansion/dethsentry01.png new file mode 100644 index 0000000..808ff55 Binary files /dev/null and b/public/overlays/expansion/dethsentry01.png differ diff --git a/scripts/pack-missiles.ts b/scripts/pack-missiles.ts index 55a3d59..7f96dc1 100644 --- a/scripts/pack-missiles.ts +++ b/scripts/pack-missiles.ts @@ -181,6 +181,18 @@ const MISSILE_TARGETS = [ { name: 'hurricaneswoosh', celFile: 'expansion/hurricane_swoosh', animSpeed: 8, trans: 1 }, { name: 'hurricanerock', celFile: 'expansion/hurricane_rocks', animSpeed: 16, trans: 0 }, { name: 'hurricanetree', celFile: 'expansion/hurricane_tree', animSpeed: 16, trans: 0 }, + + // Assassin Traps + { name: 'firetrauma', celFile: 'expansion/FireTraumaMissile', animSpeed: 16, trans: 1 }, + { name: 'firetraumaground', celFile: 'expansion/FireTraumaGround', animSpeed: 16, trans: 1 }, + { name: 'firetraumaexplode', celFile: 'expansion/FireTraumaXplosion', animSpeed: 16, trans: 1 }, + { name: 'shockfieldair', celFile: 'expansion/caltrops', animSpeed: 16, trans: 0 }, + { name: 'shockfieldground', celFile: 'Expansion/ShockField', animSpeed: 16, trans: 1 }, + { name: 'bladefury', celFile: 'Expansion/bladefury', animSpeed: 16, trans: 0 }, + { name: 'bladefuryb', celFile: 'Expansion/bladefury_B', animSpeed: 16, trans: 0 }, + { name: 'bladefuryc', celFile: 'Expansion/bladefury_C', animSpeed: 16, trans: 0 }, + { name: 'deathsentryexplode', celFile: 'Expansion/dethsentry02', animSpeed: 16, trans: 1 }, + { name: 'bladeshieldmissile', celFile: 'Expansion/blade_shield_missile', animSpeed: 16, trans: 1 }, ] as const const PAGE_SIZE = 4096 diff --git a/scripts/pack-overlays.ts b/scripts/pack-overlays.ts index dc8429c..6117777 100644 --- a/scripts/pack-overlays.ts +++ b/scripts/pack-overlays.ts @@ -1479,6 +1479,30 @@ export const OVERLAY_TARGETS: readonly OverlayTarget[] = [ lightRadius: 0, lightColor: [255, 255, 255] as const, }, + + // --------------------------------------------------------------------------- + // Authentic Diablo II v1.13c Assassin Trap Overlays + // --------------------------------------------------------------------------- + { + name: 'blade_shield', + celFile: 'Expansion/blade_shield', + aliases: ['bladeshield', 'Expansion/blade_shield'], + animRate: 16, + trans: 3, + preDraw: false, + lightRadius: 0, + lightColor: [255, 255, 255] as const, + }, + { + name: 'deathsentry_explode', + celFile: 'expansion/dethsentry01', + aliases: ['deathsentry', 'dethsentry01', 'expansion/dethsentry01'], + animRate: 16, + trans: 3, + preDraw: false, + lightRadius: 0, + lightColor: [255, 255, 255] as const, + }, ] as const export async function bakeOverlays( diff --git a/src/game/skills/assassin-traps.ts b/src/game/skills/assassin-traps.ts new file mode 100644 index 0000000..aa40a6a --- /dev/null +++ b/src/game/skills/assassin-traps.ts @@ -0,0 +1,951 @@ +/** + * Diablo II: Lord of Destruction v1.13c — Assassin Traps Module + * + * Dedicated pure calculations, stationary sentry limits, NextDelay tracking, + * Death Sentry dual-mode corpse explosion AI, blade skill weapon scaling (3/8, 3/4, 1/4), + * and adversarial kinematics for the 10 Assassin Trap Tree Skills: + * - Skill 251: Fire Trauma / Fire Blast (ReqLevel 1) + * - Skill 256: Shock Field / Shock Web (ReqLevel 6) + * - Skill 257: Blade Sentinel (ReqLevel 6) + * - Skill 261: Charged Bolt Sentry (ReqLevel 12) + * - Skill 262: Wake of Fire Sentry (ReqLevel 12) + * - Skill 266: Blade Fury (ReqLevel 18) + * - Skill 271: Lightning Sentry (ReqLevel 24) + * - Skill 272: Inferno Sentry / Wake of Inferno (ReqLevel 24) + * - Skill 276: Death Sentry (ReqLevel 30) + * - Skill 277: Blade Shield (ReqLevel 30) + * + * 1.13c Ground Truth: + * - Skills.txt, Missiles.txt, MonStats.txt, PetType.txt (Patch_D2.mpq). + * - Global Trap Cap: + * PetType `assassintrap` has global max = 5 across all stationary sentries + * (Charged Bolt, Wake of Fire, Lightning, Inferno, Death Sentry) and Blade Sentinel. + * Summoning a 6th trap causes strict FIFO eviction of the oldest active trap. + * - NextDelay Mechanics: + * Wake of Fire (`wake of destruction maker`): NextDelay = 4 frames (0.16s). + * Shock Web (`shock field on ground`): NextDelay = 25 frames (1.0s). + * Blade Sentinel (`blade creeper`): NextDelay = 25 frames (1.0s). + * Entities taking damage enter a temporary immunity window for missiles sharing that NextDelay. + * - Weapon Damage Scaling (`srcDam` in 128ths): + * Blade Sentinel: srcDam = 48 -> 48 / 128 = 3/8 (37.5%). + * Blade Fury: srcDam = 96 -> 96 / 128 = 3/4 (75.0%). + * Blade Shield: srcDam = 32 -> 32 / 128 = 1/4 (25.0%). + * - Death Sentry Dual-Mode AI: + * Prioritizes valid corpses within radius: 40% to 80% corpse max HP explosion (50% phys / 50% fire). + * Radius: 10 + (slvl - 1) subtiles (3.3 yards base, +0.33 yards/lvl). + * If no corpse in radius, fires piercing lightning bolt at live targets. + * Shots: 5 base + floor(Fire Blast blvl / 3). + * - Zero runtime MPQ reading. + */ + +import { compute5BandScaling } from '../engine/calc-ast.ts' + +// --- Skill IDs --- + +export const ASSASSIN_TRAP_SKILL_IDS = { + FIRE_BLAST: 251, + SHOCK_WEB: 256, + BLADE_SENTINEL: 257, + CHARGED_BOLT_SENTRY: 261, + WAKE_OF_FIRE: 262, + BLADE_FURY: 266, + LIGHTNING_SENTRY: 271, + WAKE_OF_INFERNO: 272, + DEATH_SENTRY: 276, + BLADE_SHIELD: 277, +} as const + +// Pet sub-skill IDs for sentries +export const ASSASSIN_PET_SUB_SKILL_IDS = { + WAKE_OF_DESTRUCTION_MAKER: 281, + BOLT_SENTRY: 306, + MON_INFERNO_SENTRY: 311, + MON_DEATH_SENTRY: 312, + SENTRY_LIGHTNING: 313, + DEATH_SENTRY_LTNG: 324, +} as const + +// --- Constants --- + +export const MAX_ACTIVE_TRAPS = 5 +export const TRAP_PET_TYPE = 'assassintrap' + +export const NEXT_DELAY_FRAMES = { + WAKE_OF_FIRE: 4, + SHOCK_WEB: 25, + BLADE_SENTINEL: 25, + DRAGON_FLIGHT: 4, +} as const + +export const BLADE_SRC_DAM = { + BLADE_SENTINEL: 48, // 48 / 128 = 3/8 + BLADE_FURY: 96, // 96 / 128 = 3/4 + BLADE_SHIELD: 32, // 32 / 128 = 1/4 +} as const + +// --- Interfaces --- + +export interface ActiveTrap { + readonly id: string + readonly ownerId: string | number + readonly skillId: number + readonly summonMon: string + readonly x: number + readonly y: number + readonly slvl: number + remainingShots: number + readonly createdAtFrame: number + readonly maxLifetimeFrames: number + lastAttackFrame: number +} + +export interface NextDelayRecord { + readonly entityId: string | number + readonly channelId: string | number + expiresAtFrame: number +} + +// --- NextDelay Engine --- + +export class NextDelayTracker { + private readonly records = new Map() + + private makeKey(entityId: string | number, channel: string | number): string { + return `${entityId}:${channel}` + } + + canTakeDamage(entityId: string | number, channel: string | number, currentFrame: number): boolean { + const key = this.makeKey(entityId, channel) + const expiresAt = this.records.get(key) + if (expiresAt === undefined) return true + return currentFrame >= expiresAt + } + + recordHit(entityId: string | number, channel: string | number, currentFrame: number, delayFrames: number): void { + const key = this.makeKey(entityId, channel) + this.records.set(key, currentFrame + delayFrames) + } + + cleanExpired(currentFrame: number): void { + for (const [key, expiresAt] of this.records.entries()) { + if (currentFrame >= expiresAt) { + this.records.delete(key) + } + } + } + + clear(): void { + this.records.clear() + } +} + +// --- Trap Manager Engine --- + +export class TrapManager { + private readonly traps: ActiveTrap[] = [] + private nextTrapId = 1 + + getActiveTraps(ownerId?: string | number): readonly ActiveTrap[] { + if (ownerId === undefined) return this.traps + return this.traps.filter(t => t.ownerId === ownerId) + } + + getActiveTrapCount(ownerId: string | number): number { + return this.traps.filter(t => t.ownerId === ownerId).length + } + + registerTrap(trapData: { + ownerId: string | number + skillId: number + summonMon: string + x: number + y: number + slvl: number + shots: number + currentFrame: number + lifetimeFrames?: number + }): { trap: ActiveTrap; evictedTrap?: ActiveTrap | undefined } { + let evictedTrap: ActiveTrap | undefined + + const ownerTraps = this.traps.filter(t => t.ownerId === trapData.ownerId) + if (ownerTraps.length >= MAX_ACTIVE_TRAPS) { + // FIFO: evict the oldest trap + evictedTrap = ownerTraps[0] + const idx = this.traps.indexOf(evictedTrap!) + if (idx !== -1) { + this.traps.splice(idx, 1) + } + } + + const trap: ActiveTrap = { + id: `trap_${this.nextTrapId++}`, + ownerId: trapData.ownerId, + skillId: trapData.skillId, + summonMon: trapData.summonMon, + x: trapData.x, + y: trapData.y, + slvl: trapData.slvl, + remainingShots: trapData.shots, + createdAtFrame: trapData.currentFrame, + maxLifetimeFrames: trapData.lifetimeFrames ?? 3000, + lastAttackFrame: -1000, + } + + this.traps.push(trap) + return { trap, evictedTrap } + } + + consumeShot(trapId: string): { remaining: number; destroyed: boolean } { + const idx = this.traps.findIndex(t => t.id === trapId) + if (idx === -1) return { remaining: 0, destroyed: true } + + const trap = this.traps[idx]! + trap.remainingShots -= 1 + if (trap.remainingShots <= 0) { + this.traps.splice(idx, 1) + return { remaining: 0, destroyed: true } + } + return { remaining: trap.remainingShots, destroyed: false } + } + + removeTrap(trapId: string): boolean { + const idx = this.traps.findIndex(t => t.id === trapId) + if (idx === -1) return false + this.traps.splice(idx, 1) + return true + } + + clear(ownerId?: string | number): void { + if (ownerId === undefined) { + this.traps.length = 0 + } else { + for (let i = this.traps.length - 1; i >= 0; i--) { + if (this.traps[i]!.ownerId === ownerId) { + this.traps.splice(i, 1) + } + } + } + } +} + +// --- Pure Damage & Stats Calculators --- + +/** + * 1.13c Weapon damage scaling for Assassin Blade skills. + * srcDam is in 128ths. + */ +export function computeBladeWeaponDamage(baseWeaponDamage: number, skillId: number): number { + switch (skillId) { + case ASSASSIN_TRAP_SKILL_IDS.BLADE_SENTINEL: + // 48 / 128 = 3/8 = 37.5% + return Math.floor((baseWeaponDamage * BLADE_SRC_DAM.BLADE_SENTINEL) / 128) + case ASSASSIN_TRAP_SKILL_IDS.BLADE_FURY: + // 96 / 128 = 3/4 = 75.0% + return Math.floor((baseWeaponDamage * BLADE_SRC_DAM.BLADE_FURY) / 128) + case ASSASSIN_TRAP_SKILL_IDS.BLADE_SHIELD: + // 32 / 128 = 1/4 = 25.0% + return Math.floor((baseWeaponDamage * BLADE_SRC_DAM.BLADE_SHIELD) / 128) + default: + return baseWeaponDamage + } +} + +/** + * Skill #251: Fire Blast / Fire Trauma + * 1.13c Parity: + * - 5-band Fire damage: base 3-4, +2..+7 per band (HitShift 8) + * - Synergies: +9% per hard point of CBS, WoF, LS, WoI, DS, Shock Web + * - Radius: 5 subtiles base + floor(allTrapSynergies / 3) + */ +export function evaluateFireBlastStats( + slvl: number, + hardPoints: { + shockWeb?: number + chargedBoltSentry?: number + wakeOfFire?: number + lightningSentry?: number + wakeOfInferno?: number + deathSentry?: number + } = {} +): { + minDamage: number + maxDamage: number + minDamage256: number + maxDamage256: number + radiusSubtiles: number + manaCost: number + manaCost256: number + synergyPct: number +} { + const minBase = compute5BandScaling(slvl, 3, 2, 3, 4, 6, 7) + const maxBase = compute5BandScaling(slvl, 4, 3, 4, 6, 8, 9) + + const synCount = + (hardPoints.shockWeb ?? 0) + + (hardPoints.chargedBoltSentry ?? 0) + + (hardPoints.wakeOfFire ?? 0) + + (hardPoints.lightningSentry ?? 0) + + (hardPoints.wakeOfInferno ?? 0) + + (hardPoints.deathSentry ?? 0) + + const synergyPct = synCount * 9 + const mult = 1 + synergyPct / 100 + + const minDamage = Math.floor(minBase * mult) + const maxDamage = Math.floor(maxBase * mult) + + const manaCost = Math.max(1, 3 + Math.floor((slvl - 1) / 8)) + const radiusSubtiles = 5 + Math.floor(synCount / 3) + + return { + minDamage, + maxDamage, + minDamage256: minDamage * 256, + maxDamage256: maxDamage * 256, + radiusSubtiles, + manaCost, + manaCost256: manaCost * 256, + synergyPct, + } +} + +/** + * Skill #256: Shock Web / Shock Field + * 1.13c Parity: + * - Missiles: 6 + floor(slvl / 4) + floor(fireBlastBlvl / 3) + * - 5-band Lightning damage: 2-20 base, max scales +6, +12, +20, +30, +42 (HitShift 7 -> /2) + * - Synergies: +11% per hard point of CBS, LS, DS + * - NextDelay: 25 frames + */ +export function evaluateShockWebStats( + slvl: number, + hardPoints: { + fireBlast?: number + chargedBoltSentry?: number + lightningSentry?: number + deathSentry?: number + } = {} +): { + missileCount: number + minDamage: number + maxDamage: number + minDamage256: number + maxDamage256: number + manaCost: number + synergyPct: number + nextDelayFrames: number +} { + const missileCount = 6 + Math.floor(slvl / 4) + Math.floor((hardPoints.fireBlast ?? 0) / 3) + + const minBase = 1 // 2 with hitshift 7 = 1 + const maxBase = Math.floor(compute5BandScaling(slvl, 20, 6, 12, 20, 30, 42) / 2) + + const synCount = + (hardPoints.chargedBoltSentry ?? 0) + + (hardPoints.lightningSentry ?? 0) + + (hardPoints.deathSentry ?? 0) + const synergyPct = synCount * 11 + const mult = 1 + synergyPct / 100 + + const minDamage = Math.max(1, Math.floor(minBase * mult)) + const maxDamage = Math.max(minDamage, Math.floor(maxBase * mult)) + + return { + missileCount, + minDamage, + maxDamage, + minDamage256: minDamage * 256, + maxDamage256: maxDamage * 256, + manaCost: 6, + synergyPct, + nextDelayFrames: NEXT_DELAY_FRAMES.SHOCK_WEB, + } +} + +/** + * Skill #257: Blade Sentinel + * 1.13c Parity: + * - Weapon Damage: 3/8 (srcDam = 48) + * - Flat Physical Damage: 6-10 base, +3/+4/+5 per band + * - Duration: 100 + 12 * (slvl - 1) frames (4s to 13.12s) + * - NextDelay: 25 frames + * - PetType: assassintrap (max 5) + */ +export function evaluateBladeSentinelStats( + slvl: number, + weaponDamage = 0 +): { + flatMinPhys: number + flatMaxPhys: number + scaledWeaponPhys: number + totalMinPhys: number + totalMaxPhys: number + durationFrames: number + manaCost: number + attackRatingBonusPct: number + nextDelayFrames: number + petMax: number +} { + const flatMinPhys = compute5BandScaling(slvl, 6, 3, 4, 5, 5, 5) + const flatMaxPhys = compute5BandScaling(slvl, 10, 3, 4, 5, 5, 5) + const scaledWeaponPhys = computeBladeWeaponDamage(weaponDamage, ASSASSIN_TRAP_SKILL_IDS.BLADE_SENTINEL) + + const durationFrames = 100 + 12 * (slvl - 1) + const attackRatingBonusPct = slvl * 5 + + return { + flatMinPhys, + flatMaxPhys, + scaledWeaponPhys, + totalMinPhys: flatMinPhys + scaledWeaponPhys, + totalMaxPhys: flatMaxPhys + scaledWeaponPhys, + durationFrames, + manaCost: 7, + attackRatingBonusPct, + nextDelayFrames: NEXT_DELAY_FRAMES.BLADE_SENTINEL, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #261: Charged Bolt Sentry + * 1.13c Parity: + * - Shots: 5 + floor(LightningSentry.blvl / 4) + * - Bolts per shot: 5 + floor(ShockWeb.blvl / 3) + * - Lightning Damage: 1 base min, max scales +1, +2, +3, +4, +5 (HitShift 8) + * - Synergies: +6% per hard point of Fire Blast, Shock Web, LS, DS + * - PetType: assassintrap (max 5) + */ +export function evaluateChargedBoltSentryStats( + slvl: number, + hardPoints: { + fireBlast?: number + shockWeb?: number + lightningSentry?: number + deathSentry?: number + } = {} +): { + shots: number + boltsPerShot: number + minDamage: number + maxDamage: number + minDamage256: number + maxDamage256: number + manaCost: number + synergyPct: number + petMax: number +} { + const shots = 5 + Math.floor((hardPoints.lightningSentry ?? 0) / 4) + const boltsPerShot = 5 + Math.floor((hardPoints.shockWeb ?? 0) / 3) + + const minBase = 1 + const maxBase = compute5BandScaling(slvl, 7, 1, 2, 3, 4, 5) + + const synCount = + (hardPoints.fireBlast ?? 0) + + (hardPoints.shockWeb ?? 0) + + (hardPoints.lightningSentry ?? 0) + + (hardPoints.deathSentry ?? 0) + const synergyPct = synCount * 6 + const mult = 1 + synergyPct / 100 + + const minDamage = minBase + const maxDamage = Math.max(minDamage, Math.floor(maxBase * mult)) + + return { + shots, + boltsPerShot, + minDamage, + maxDamage, + minDamage256: minDamage * 256, + maxDamage256: maxDamage * 256, + manaCost: 13, + synergyPct, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #262: Wake of Fire Sentry + * 1.13c Parity: + * - Shots: 5 waves + * - NextDelay: 4 frames + * - Fire Damage: 5-10 base, scales +2, +3, +4, +5, +6 (HitShift 8) + * - Synergies: +8% per hard point of Fire Blast, Wake of Inferno + * - PetType: assassintrap (max 5) + */ +export function evaluateWakeOfFireStats( + slvl: number, + hardPoints: { + fireBlast?: number + wakeOfInferno?: number + } = {} +): { + shots: number + minDamage: number + maxDamage: number + minDamage256: number + maxDamage256: number + manaCost: number + synergyPct: number + nextDelayFrames: number + petMax: number +} { + const minBase = compute5BandScaling(slvl, 5, 2, 3, 4, 5, 6) + const maxBase = compute5BandScaling(slvl, 10, 2, 3, 4, 5, 6) + + const synCount = (hardPoints.fireBlast ?? 0) + (hardPoints.wakeOfInferno ?? 0) + const synergyPct = synCount * 8 + const mult = 1 + synergyPct / 100 + + const minDamage = Math.floor(minBase * mult) + const maxDamage = Math.floor(maxBase * mult) + + return { + shots: 5, + minDamage, + maxDamage, + minDamage256: minDamage * 256, + maxDamage256: maxDamage * 256, + manaCost: 13, + synergyPct, + nextDelayFrames: NEXT_DELAY_FRAMES.WAKE_OF_FIRE, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #266: Blade Fury + * 1.13c Parity: + * - Weapon Damage: 3/4 (srcDam = 96) + * - Flat Physical Damage: 8-10 base, +3/+5/+8 + * - Channeled tick rate: fires every 6 frames (5 frame delay + 1 action) + * - Mana: 3 to start, 1 per shot + */ +export function evaluateBladeFuryStats( + slvl: number, + weaponDamage = 0 +): { + flatMinPhys: number + flatMaxPhys: number + scaledWeaponPhys: number + totalMinPhys: number + totalMaxPhys: number + startManaCost: number + perShotManaCost: number + framesPerShot: number +} { + const flatMinPhys = compute5BandScaling(slvl, 8, 3, 5, 8, 8, 8) + const flatMaxPhys = compute5BandScaling(slvl, 10, 3, 5, 8, 8, 8) + const scaledWeaponPhys = computeBladeWeaponDamage(weaponDamage, ASSASSIN_TRAP_SKILL_IDS.BLADE_FURY) + + return { + flatMinPhys, + flatMaxPhys, + scaledWeaponPhys, + totalMinPhys: flatMinPhys + scaledWeaponPhys, + totalMaxPhys: flatMaxPhys + scaledWeaponPhys, + startManaCost: 3, + perShotManaCost: 1, + framesPerShot: 6, + } +} + +/** + * Skill #271: Lightning Sentry + * 1.13c Parity: + * - Shots: 10 piercing lightning beams + * - Lightning Damage: 1 base min, max scales +7, +10, +13, +16, +19 (HitShift 8) + * - Synergies: +12% per hard point of Shock Web, CBS, DS + * - PetType: assassintrap (max 5) + */ +export function evaluateLightningSentryStats( + slvl: number, + hardPoints: { + shockWeb?: number + chargedBoltSentry?: number + deathSentry?: number + } = {} +): { + shots: number + minDamage: number + maxDamage: number + minDamage256: number + maxDamage256: number + manaCost: number + synergyPct: number + petMax: number +} { + const minBase = 1 + const maxBase = compute5BandScaling(slvl, 20, 10, 16, 24, 34, 44) + + const synCount = + (hardPoints.shockWeb ?? 0) + + (hardPoints.chargedBoltSentry ?? 0) + + (hardPoints.deathSentry ?? 0) + const synergyPct = synCount * 12 + const mult = 1 + synergyPct / 100 + + const minDamage = minBase + const maxDamage = Math.max(minDamage, Math.floor(maxBase * mult)) + + return { + shots: 10, + minDamage, + maxDamage, + minDamage256: minDamage * 256, + maxDamage256: maxDamage * 256, + manaCost: 20, + synergyPct, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #272: Inferno Sentry / Wake of Inferno + * 1.13c Parity: + * - Attacks: 10 flamethrower bursts + * - Fire Damage: 1.25 - 3.125 base per tick, scales with level + * - Synergies: +7% per hard point of Fire Blast, Wake of Fire, DS + * - PetType: assassintrap (max 5) + */ +export function evaluateWakeOfInfernoStats( + slvl: number, + hardPoints: { + fireBlast?: number + wakeOfFire?: number + deathSentry?: number + } = {} +): { + attacks: number + minDamage256: number + maxDamage256: number + minDamage: number + maxDamage: number + manaCost: number + synergyPct: number + petMax: number +} { + const minBase256 = compute5BandScaling(slvl, 320, 96, 128, 160, 192, 224) + const maxBase256 = compute5BandScaling(slvl, 800, 144, 192, 240, 288, 336) + + const synCount = + (hardPoints.fireBlast ?? 0) + + (hardPoints.wakeOfFire ?? 0) + + (hardPoints.deathSentry ?? 0) + const synergyPct = synCount * 7 + const mult = 1 + synergyPct / 100 + + const minDamage256 = Math.floor(minBase256 * mult) + const maxDamage256 = Math.floor(maxBase256 * mult) + + return { + attacks: 10, + minDamage256, + maxDamage256, + minDamage: Math.floor(minDamage256 / 256), + maxDamage: Math.floor(maxDamage256 / 256), + manaCost: 20, + synergyPct, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #276: Death Sentry + * 1.13c Parity: + * - Shots: 5 + floor(FireBlast.blvl / 3) + * - Lightning Damage: 1-50 base, max scales +11, +15, +19, +23, +27 (HitShift 8) + * - Lightning Synergy: +12% per hard point of Lightning Sentry + * - Corpse Explosion: + * 40% to 80% of corpse base HP + * 50% Physical, 50% Fire + * Radius: 10 + (slvl - 1) subtiles (3.3 yards base, +0.33 yards/lvl) + * - PetType: assassintrap (max 5) + */ +export function evaluateDeathSentryStats( + slvl: number, + hardPoints: { + fireBlast?: number + lightningSentry?: number + } = {} +): { + shots: number + minLtngDamage: number + maxLtngDamage: number + minLtngDamage256: number + maxLtngDamage256: number + corpseExplosionMinPct: number + corpseExplosionMaxPct: number + corpseExplosionFireRatio: number + corpseExplosionPhysRatio: number + corpseExplosionRadiusSubtiles: number + manaCost: number + ltngSynergyPct: number + petMax: number +} { + const shots = 5 + Math.floor((hardPoints.fireBlast ?? 0) / 3) + + const minBase = 1 + const maxBase = compute5BandScaling(slvl, 50, 11, 15, 19, 23, 27) + const ltngSynergyPct = (hardPoints.lightningSentry ?? 0) * 12 + const mult = 1 + ltngSynergyPct / 100 + + const minLtngDamage = minBase + const maxLtngDamage = Math.max(minLtngDamage, Math.floor(maxBase * mult)) + + const corpseExplosionRadiusSubtiles = 10 + (slvl - 1) + + return { + shots, + minLtngDamage, + maxLtngDamage, + minLtngDamage256: minLtngDamage * 256, + maxLtngDamage256: maxLtngDamage * 256, + corpseExplosionMinPct: 40, + corpseExplosionMaxPct: 80, + corpseExplosionFireRatio: 0.5, + corpseExplosionPhysRatio: 0.5, + corpseExplosionRadiusSubtiles, + manaCost: 20, + ltngSynergyPct, + petMax: MAX_ACTIVE_TRAPS, + } +} + +/** + * Skill #277: Blade Shield + * 1.13c Parity: + * - Weapon Damage: 1/4 (srcDam = 32) + * - Flat Physical Damage: 1-30 base, +5/+6/+7 + * - Duration: 500 + 125 * (slvl - 1) frames (20s to 115s at slvl 20) + * - Periodic Tick: every 25 frames (1.0s) + * - Radius: 6 subtiles (2.0 yards = 64 px) + * - Mana: 27 + 2 * (slvl - 1) + */ +export function evaluateBladeShieldStats( + slvl: number, + weaponDamage = 0 +): { + flatMinPhys: number + flatMaxPhys: number + scaledWeaponPhys: number + totalMinPhys: number + totalMaxPhys: number + durationFrames: number + tickRateFrames: number + radiusSubtiles: number + manaCost: number +} { + const flatMinPhys = compute5BandScaling(slvl, 1, 5, 6, 7, 7, 7) + const flatMaxPhys = compute5BandScaling(slvl, 30, 5, 6, 7, 7, 7) + const scaledWeaponPhys = computeBladeWeaponDamage(weaponDamage, ASSASSIN_TRAP_SKILL_IDS.BLADE_SHIELD) + + const durationFrames = 500 + 125 * (slvl - 1) + const manaCost = 27 + 2 * (slvl - 1) + + return { + flatMinPhys, + flatMaxPhys, + scaledWeaponPhys, + totalMinPhys: flatMinPhys + scaledWeaponPhys, + totalMaxPhys: flatMaxPhys + scaledWeaponPhys, + durationFrames, + tickRateFrames: 25, + radiusSubtiles: 6, + manaCost, + } +} + +// --- Kinematics & Simulation Helpers --- + +export interface SimulationCorpse { + readonly id: string + readonly x: number + readonly y: number + readonly baseMaxHp: number + consumed: boolean +} + +export interface SimulationEnemy { + readonly id: string + readonly x: number + readonly y: number + currentHp: number + readonly maxHp: number +} + +/** + * Simulate Death Sentry AI target selection: + * 1. If valid unconsumed corpse within explosion radius -> triggers Corpse Explosion (50% fire, 50% phys). + * 2. Else if enemy within lightning attack range (e.g. 15 yards / 480px) -> triggers Lightning Bolt. + * 3. Else -> idle. + */ +export function simulateDeathSentryAI( + trap: ActiveTrap, + corpses: SimulationCorpse[], + enemies: SimulationEnemy[], + options: { + fireBlastBlvl?: number + lightningSentryBlvl?: number + subtilePixelSize?: number + } = {} +): { + action: 'corpse_explosion' | 'lightning' | 'idle' + targetId?: string + damageDealt?: { phys: number; fire: number; ltng: number } + affectedEnemyIds?: string[] +} { + const stats = evaluateDeathSentryStats(trap.slvl, { + fireBlast: options.fireBlastBlvl ?? 0, + lightningSentry: options.lightningSentryBlvl ?? 0, + }) + + const pixelSize = options.subtilePixelSize ?? 16 + const ceRadiusPx = stats.corpseExplosionRadiusSubtiles * pixelSize + + // 1. Search for nearest corpse in CE radius + let bestCorpse: SimulationCorpse | null = null + let bestCorpseDistSq = Infinity + + for (const c of corpses) { + if (c.consumed) continue + const dx = c.x - trap.x + const dy = c.y - trap.y + const distSq = dx * dx + dy * dy + if (distSq <= ceRadiusPx * ceRadiusPx && distSq < bestCorpseDistSq) { + bestCorpseDistSq = distSq + bestCorpse = c + } + } + + if (bestCorpse) { + // Consume corpse + bestCorpse.consumed = true + // Calculate 40-80% corpse life damage (average 60%) + const pct = (stats.corpseExplosionMinPct + stats.corpseExplosionMaxPct) / 200 + const rawDamage = Math.floor(bestCorpse.baseMaxHp * pct) + const physDmg = Math.floor(rawDamage * stats.corpseExplosionPhysRatio) + const fireDmg = rawDamage - physDmg + + const affectedEnemyIds: string[] = [] + for (const e of enemies) { + const dx = e.x - bestCorpse.x + const dy = e.y - bestCorpse.y + if (dx * dx + dy * dy <= ceRadiusPx * ceRadiusPx) { + affectedEnemyIds.push(e.id) + e.currentHp = Math.max(0, e.currentHp - rawDamage) + } + } + + return { + action: 'corpse_explosion', + targetId: bestCorpse.id, + damageDealt: { phys: physDmg, fire: fireDmg, ltng: 0 }, + affectedEnemyIds, + } + } + + // 2. Search for nearest live enemy within lightning range (~480 px) + const maxLtngDistPx = 480 + let bestEnemy: SimulationEnemy | null = null + let bestEnemyDistSq = Infinity + + for (const e of enemies) { + if (e.currentHp <= 0) continue + const dx = e.x - trap.x + const dy = e.y - trap.y + const distSq = dx * dx + dy * dy + if (distSq <= maxLtngDistPx * maxLtngDistPx && distSq < bestEnemyDistSq) { + bestEnemyDistSq = distSq + bestEnemy = e + } + } + + if (bestEnemy) { + const avgLtng = Math.floor((stats.minLtngDamage + stats.maxLtngDamage) / 2) + bestEnemy.currentHp = Math.max(0, bestEnemy.currentHp - avgLtng) + return { + action: 'lightning', + targetId: bestEnemy.id, + damageDealt: { phys: 0, fire: 0, ltng: avgLtng }, + affectedEnemyIds: [bestEnemy.id], + } + } + + return { action: 'idle' } +} + +/** + * Simulate Wake of Fire wave emission with NextDelay protection: + * Each target can only be damaged once per 4 frames by Wake of Fire. + */ +export function simulateWakeOfFireWaveHit( + trap: ActiveTrap, + enemies: SimulationEnemy[], + currentFrame: number, + nextDelayTracker: NextDelayTracker, + options: { + fireBlastBlvl?: number + wakeOfInfernoBlvl?: number + waveRangePx?: number + } = {} +): { + enemiesHit: string[] + totalDamageDealt: number +} { + const stats = evaluateWakeOfFireStats(trap.slvl, { + fireBlast: options.fireBlastBlvl ?? 0, + wakeOfInferno: options.wakeOfInfernoBlvl ?? 0, + }) + + const rangePx = options.waveRangePx ?? 300 + const enemiesHit: string[] = [] + let totalDamageDealt = 0 + + const avgDamage = Math.floor((stats.minDamage + stats.maxDamage) / 2) + + for (const e of enemies) { + if (e.currentHp <= 0) continue + const dx = e.x - trap.x + const dy = e.y - trap.y + if (dx * dx + dy * dy <= rangePx * rangePx) { + // Check NextDelay + if (nextDelayTracker.canTakeDamage(e.id, 'wake_of_fire', currentFrame)) { + enemiesHit.push(e.id) + e.currentHp = Math.max(0, e.currentHp - avgDamage) + totalDamageDealt += avgDamage + nextDelayTracker.recordHit(e.id, 'wake_of_fire', currentFrame, stats.nextDelayFrames) + } + } + } + + return { enemiesHit, totalDamageDealt } +} + +/** + * Calculate Shock Web cluster points dispersed around the target impact point. + */ +export function computeShockWebClusterPoints( + targetX: number, + targetY: number, + slvl: number, + fireBlastBlvl = 0, + radiusPx = 64 +): { x: number; y: number }[] { + const stats = evaluateShockWebStats(slvl, { fireBlast: fireBlastBlvl }) + const points: { x: number; y: number }[] = [] + + // Deterministic circular / hexagonal distribution + for (let i = 0; i < stats.missileCount; i++) { + const angle = (2 * Math.PI * i) / stats.missileCount + // Varied distances for natural cluster density + const r = radiusPx * (0.3 + 0.7 * ((i % 3) / 2)) + points.push({ + x: Math.round(targetX + r * Math.cos(angle)), + y: Math.round(targetY + r * Math.sin(angle)), + }) + } + + return points +} diff --git a/src/render/missiles-meta.ts b/src/render/missiles-meta.ts index eabeb1a..1b35d0c 100644 --- a/src/render/missiles-meta.ts +++ b/src/render/missiles-meta.ts @@ -16697,6 +16697,637 @@ export const HURRICANETREE_META: MissileMeta = { ], } +export const FIRETRAUMA_META: MissileMeta = { + name: "firetrauma", + celFile: "expansion/FireTraumaMissile", + width: 512, + height: 10, + directions: 1, + framesPerDirection: 20, + animSpeed: 16, + groups: [ + [ + [0, 0, 12, 10, -6, -13], + [13, 0, 12, 10, -6, -13], + [26, 0, 12, 10, -6, -13], + [39, 0, 12, 10, -6, -13], + [52, 0, 12, 10, -6, -13], + [65, 0, 12, 10, -6, -13], + [78, 0, 12, 10, -6, -13], + [91, 0, 12, 10, -6, -13], + [104, 0, 12, 10, -6, -13], + [117, 0, 12, 10, -6, -13], + [130, 0, 12, 10, -6, -13], + [143, 0, 12, 10, -6, -13], + [156, 0, 12, 10, -6, -13], + [169, 0, 12, 10, -6, -13], + [182, 0, 12, 10, -6, -13], + [195, 0, 12, 10, -6, -13], + [208, 0, 12, 10, -6, -13], + [221, 0, 12, 10, -6, -13], + [234, 0, 12, 10, -6, -13], + [247, 0, 12, 10, -6, -13] + ] + ], +} + +export const FIRETRAUMAGROUND_META: MissileMeta = { + name: "firetraumaground", + celFile: "expansion/FireTraumaGround", + width: 512, + height: 10, + directions: 1, + framesPerDirection: 1, + animSpeed: 16, + groups: [ + [ + [0, 0, 11, 10, -5, -13] + ] + ], +} + +export const FIRETRAUMAEXPLODE_META: MissileMeta = { + name: "firetraumaexplode", + celFile: "expansion/FireTraumaXplosion", + width: 512, + height: 451, + directions: 1, + framesPerDirection: 20, + animSpeed: 16, + groups: [ + [ + [0, 0, 87, 112, -45, -111], + [88, 0, 87, 112, -45, -111], + [176, 0, 87, 112, -45, -111], + [264, 0, 87, 112, -45, -111], + [352, 0, 87, 112, -45, -111], + [0, 113, 87, 112, -45, -111], + [88, 113, 87, 112, -45, -111], + [176, 113, 87, 112, -45, -111], + [264, 113, 87, 112, -45, -111], + [352, 113, 87, 112, -45, -111], + [0, 226, 87, 112, -45, -111], + [88, 226, 87, 112, -45, -111], + [176, 226, 87, 112, -45, -111], + [264, 226, 87, 112, -45, -111], + [352, 226, 87, 112, -45, -111], + [0, 339, 87, 112, -45, -111], + [88, 339, 87, 112, -45, -111], + [176, 339, 87, 112, -45, -111], + [264, 339, 87, 112, -45, -111], + [352, 339, 87, 112, -45, -111] + ] + ], +} + +export const SHOCKFIELDAIR_META: MissileMeta = { + name: "shockfieldair", + celFile: "expansion/caltrops", + width: 512, + height: 65, + directions: 32, + framesPerDirection: 8, + animSpeed: 16, + groups: [ + [ + [0, 0, 11, 10, -5, -1], + [12, 0, 11, 10, -5, -1], + [24, 0, 11, 10, -5, -1], + [36, 0, 11, 10, -5, -1], + [48, 0, 11, 10, -5, -1], + [60, 0, 11, 10, -5, -1], + [72, 0, 11, 10, -5, -1], + [84, 0, 11, 10, -5, -1] + ], + [ + [96, 0, 10, 10, -2, 0], + [107, 0, 10, 10, -2, 0], + [118, 0, 10, 10, -2, 0], + [129, 0, 10, 10, -2, 0], + [140, 0, 10, 10, -2, 0], + [151, 0, 10, 10, -2, 0], + [162, 0, 10, 10, -2, 0], + [173, 0, 10, 10, -2, 0] + ], + [ + [184, 0, 9, 9, -5, 2], + [194, 0, 9, 9, -5, 2], + [204, 0, 9, 9, -5, 2], + [214, 0, 9, 9, -5, 2], + [224, 0, 9, 9, -5, 2], + [234, 0, 9, 9, -5, 2], + [244, 0, 9, 9, -5, 2], + [254, 0, 9, 9, -5, 2] + ], + [ + [264, 0, 11, 10, -8, 0], + [276, 0, 11, 10, -8, 0], + [288, 0, 11, 10, -8, 0], + [300, 0, 11, 10, -8, 0], + [312, 0, 11, 10, -8, 0], + [324, 0, 11, 10, -8, 0], + [336, 0, 11, 10, -8, 0], + [348, 0, 11, 10, -8, 0] + ], + [ + [360, 0, 7, 10, -5, -1], + [368, 0, 7, 10, -5, -1], + [376, 0, 7, 10, -5, -1], + [384, 0, 7, 10, -5, -1], + [392, 0, 7, 10, -5, -1], + [400, 0, 7, 10, -5, -1], + [408, 0, 7, 10, -5, -1], + [416, 0, 7, 10, -5, -1] + ], + [ + [424, 0, 10, 9, -3, 0], + [435, 0, 10, 9, -3, 0], + [446, 0, 10, 9, -3, 0], + [457, 0, 10, 9, -3, 0], + [468, 0, 10, 9, -3, 0], + [479, 0, 10, 9, -3, 0], + [490, 0, 10, 9, -3, 0], + [501, 0, 10, 9, -3, 0] + ], + [ + [0, 11, 8, 10, -2, 1], + [9, 11, 8, 10, -2, 1], + [18, 11, 8, 10, -2, 1], + [27, 11, 8, 10, -2, 1], + [36, 11, 8, 10, -2, 1], + [45, 11, 8, 10, -2, 1], + [54, 11, 8, 10, -2, 1], + [63, 11, 8, 10, -2, 1] + ], + [ + [72, 11, 10, 10, -7, 1], + [83, 11, 10, 10, -7, 1], + [94, 11, 10, 10, -7, 1], + [105, 11, 10, 10, -7, 1], + [116, 11, 10, 10, -7, 1], + [127, 11, 10, 10, -7, 1], + [138, 11, 10, 10, -7, 1], + [149, 11, 10, 10, -7, 1] + ], + [ + [160, 11, 9, 10, -5, -1], + [170, 11, 9, 10, -5, -1], + [180, 11, 9, 10, -5, -1], + [190, 11, 9, 10, -5, -1], + [200, 11, 9, 10, -5, -1], + [210, 11, 9, 10, -5, -1], + [220, 11, 9, 10, -5, -1], + [230, 11, 9, 10, -5, -1] + ], + [ + [240, 11, 10, 10, -3, -1], + [251, 11, 10, 10, -3, -1], + [262, 11, 10, 10, -3, -1], + [273, 11, 10, 10, -3, -1], + [284, 11, 10, 10, -3, -1], + [295, 11, 10, 10, -3, -1], + [306, 11, 10, 10, -3, -1], + [317, 11, 10, 10, -3, -1] + ], + [ + [328, 11, 11, 10, -3, 0], + [340, 11, 11, 10, -3, 0], + [352, 11, 11, 10, -3, 0], + [364, 11, 11, 10, -3, 0], + [376, 11, 11, 10, -3, 0], + [388, 11, 11, 10, -3, 0], + [400, 11, 11, 10, -3, 0], + [412, 11, 11, 10, -3, 0] + ], + [ + [424, 11, 10, 10, -3, 1], + [435, 11, 10, 10, -3, 1], + [446, 11, 10, 10, -3, 1], + [457, 11, 10, 10, -3, 1], + [468, 11, 10, 10, -3, 1], + [479, 11, 10, 10, -3, 1], + [490, 11, 10, 10, -3, 1], + [501, 11, 10, 10, -3, 1] + ], + [ + [0, 22, 9, 10, -3, 1], + [10, 22, 9, 10, -3, 1], + [20, 22, 9, 10, -3, 1], + [30, 22, 9, 10, -3, 1], + [40, 22, 9, 10, -3, 1], + [50, 22, 9, 10, -3, 1], + [60, 22, 9, 10, -3, 1], + [70, 22, 9, 10, -3, 1] + ], + [ + [80, 22, 10, 10, -6, 1], + [91, 22, 10, 10, -6, 1], + [102, 22, 10, 10, -6, 1], + [113, 22, 10, 10, -6, 1], + [124, 22, 10, 10, -6, 1], + [135, 22, 10, 10, -6, 1], + [146, 22, 10, 10, -6, 1], + [157, 22, 10, 10, -6, 1] + ], + [ + [168, 22, 10, 9, -8, 1], + [179, 22, 10, 9, -8, 1], + [190, 22, 10, 9, -8, 1], + [201, 22, 10, 9, -8, 1], + [212, 22, 10, 9, -8, 1], + [223, 22, 10, 9, -8, 1], + [234, 22, 10, 9, -8, 1], + [245, 22, 10, 9, -8, 1] + ], + [ + [256, 22, 9, 9, -6, 0], + [266, 22, 9, 9, -6, 0], + [276, 22, 9, 9, -6, 0], + [286, 22, 9, 9, -6, 0], + [296, 22, 9, 9, -6, 0], + [306, 22, 9, 9, -6, 0], + [316, 22, 9, 9, -6, 0], + [326, 22, 9, 9, -6, 0] + ], + [ + [336, 22, 8, 10, -5, -1], + [345, 22, 8, 10, -5, -1], + [354, 22, 8, 10, -5, -1], + [363, 22, 8, 10, -5, -1], + [372, 22, 8, 10, -5, -1], + [381, 22, 8, 10, -5, -1], + [390, 22, 8, 10, -5, -1], + [399, 22, 8, 10, -5, -1] + ], + [ + [408, 22, 9, 10, -5, -1], + [418, 22, 9, 10, -5, -1], + [428, 22, 9, 10, -5, -1], + [438, 22, 9, 10, -5, -1], + [448, 22, 9, 10, -5, -1], + [458, 22, 9, 10, -5, -1], + [468, 22, 9, 10, -5, -1], + [478, 22, 9, 10, -5, -1] + ], + [ + [488, 22, 10, 10, -4, -1], + [499, 22, 10, 10, -4, -1], + [0, 33, 10, 10, -4, -1], + [11, 33, 10, 10, -4, -1], + [22, 33, 10, 10, -4, -1], + [33, 33, 10, 10, -4, -1], + [44, 33, 10, 10, -4, -1], + [55, 33, 10, 10, -4, -1] + ], + [ + [66, 33, 10, 9, -3, 0], + [77, 33, 10, 9, -3, 0], + [88, 33, 10, 9, -3, 0], + [99, 33, 10, 9, -3, 0], + [110, 33, 10, 9, -3, 0], + [121, 33, 10, 9, -3, 0], + [132, 33, 10, 9, -3, 0], + [143, 33, 10, 9, -3, 0] + ], + [ + [154, 33, 9, 10, -2, 0], + [164, 33, 9, 10, -2, 0], + [174, 33, 9, 10, -2, 0], + [184, 33, 9, 10, -2, 0], + [194, 33, 9, 10, -2, 0], + [204, 33, 9, 10, -2, 0], + [214, 33, 9, 10, -2, 0], + [224, 33, 9, 10, -2, 0] + ], + [ + [234, 33, 9, 10, -2, 0], + [244, 33, 9, 10, -2, 0], + [254, 33, 9, 10, -2, 0], + [264, 33, 9, 10, -2, 0], + [274, 33, 9, 10, -2, 0], + [284, 33, 9, 10, -2, 0], + [294, 33, 9, 10, -2, 0], + [304, 33, 9, 10, -2, 0] + ], + [ + [314, 33, 10, 10, -3, 0], + [325, 33, 10, 10, -3, 0], + [336, 33, 10, 10, -3, 0], + [347, 33, 10, 10, -3, 0], + [358, 33, 10, 10, -3, 0], + [369, 33, 10, 10, -3, 0], + [380, 33, 10, 10, -3, 0], + [391, 33, 10, 10, -3, 0] + ], + [ + [402, 33, 9, 10, -2, 1], + [412, 33, 9, 10, -2, 1], + [422, 33, 9, 10, -2, 1], + [432, 33, 9, 10, -2, 1], + [442, 33, 9, 10, -2, 1], + [452, 33, 9, 10, -2, 1], + [462, 33, 9, 10, -2, 1], + [472, 33, 9, 10, -2, 1] + ], + [ + [482, 33, 8, 10, -3, 1], + [491, 33, 8, 10, -3, 1], + [500, 33, 8, 10, -3, 1], + [0, 44, 8, 10, -3, 1], + [9, 44, 8, 10, -3, 1], + [18, 44, 8, 10, -3, 1], + [27, 44, 8, 10, -3, 1], + [36, 44, 8, 10, -3, 1] + ], + [ + [45, 44, 10, 10, -5, 1], + [56, 44, 10, 10, -5, 1], + [67, 44, 10, 10, -5, 1], + [78, 44, 10, 10, -5, 1], + [89, 44, 10, 10, -5, 1], + [100, 44, 10, 10, -5, 1], + [111, 44, 10, 10, -5, 1], + [122, 44, 10, 10, -5, 1] + ], + [ + [133, 44, 10, 10, -6, 1], + [144, 44, 10, 10, -6, 1], + [155, 44, 10, 10, -6, 1], + [166, 44, 10, 10, -6, 1], + [177, 44, 10, 10, -6, 1], + [188, 44, 10, 10, -6, 1], + [199, 44, 10, 10, -6, 1], + [210, 44, 10, 10, -6, 1] + ], + [ + [221, 44, 10, 9, -7, 1], + [232, 44, 10, 9, -7, 1], + [243, 44, 10, 9, -7, 1], + [254, 44, 10, 9, -7, 1], + [265, 44, 10, 9, -7, 1], + [276, 44, 10, 9, -7, 1], + [287, 44, 10, 9, -7, 1], + [298, 44, 10, 9, -7, 1] + ], + [ + [309, 44, 9, 10, -7, 1], + [319, 44, 9, 10, -7, 1], + [329, 44, 9, 10, -7, 1], + [339, 44, 9, 10, -7, 1], + [349, 44, 9, 10, -7, 1], + [359, 44, 9, 10, -7, 1], + [369, 44, 9, 10, -7, 1], + [379, 44, 9, 10, -7, 1] + ], + [ + [389, 44, 10, 10, -8, 0], + [400, 44, 10, 10, -8, 0], + [411, 44, 10, 10, -8, 0], + [422, 44, 10, 10, -8, 0], + [433, 44, 10, 10, -8, 0], + [444, 44, 10, 10, -8, 0], + [455, 44, 10, 10, -8, 0], + [466, 44, 10, 10, -8, 0] + ], + [ + [477, 44, 9, 10, -7, -1], + [487, 44, 9, 10, -7, -1], + [497, 44, 9, 10, -7, -1], + [0, 55, 9, 10, -7, -1], + [10, 55, 9, 10, -7, -1], + [20, 55, 9, 10, -7, -1], + [30, 55, 9, 10, -7, -1], + [40, 55, 9, 10, -7, -1] + ], + [ + [50, 55, 9, 10, -6, -1], + [60, 55, 9, 10, -6, -1], + [70, 55, 9, 10, -6, -1], + [80, 55, 9, 10, -6, -1], + [90, 55, 9, 10, -6, -1], + [100, 55, 9, 10, -6, -1], + [110, 55, 9, 10, -6, -1], + [120, 55, 9, 10, -6, -1] + ] + ], +} + +export const SHOCKFIELDGROUND_META: MissileMeta = { + name: "shockfieldground", + celFile: "Expansion/ShockField", + width: 1024, + height: 647, + directions: 8, + framesPerDirection: 10, + animSpeed: 16, + groups: [ + [ + [0, 0, 102, 60, -56, -32], + [103, 0, 102, 60, -56, -32], + [206, 0, 102, 60, -56, -32], + [309, 0, 102, 60, -56, -32], + [412, 0, 102, 60, -56, -32], + [515, 0, 102, 60, -56, -32], + [618, 0, 102, 60, -56, -32], + [721, 0, 102, 60, -56, -32], + [824, 0, 102, 60, -56, -32], + [0, 61, 102, 60, -56, -32] + ], + [ + [103, 61, 95, 60, -40, -31], + [199, 61, 95, 60, -40, -31], + [295, 61, 95, 60, -40, -31], + [391, 61, 95, 60, -40, -31], + [487, 61, 95, 60, -40, -31], + [583, 61, 95, 60, -40, -31], + [679, 61, 95, 60, -40, -31], + [775, 61, 95, 60, -40, -31], + [871, 61, 95, 60, -40, -31], + [0, 122, 95, 60, -40, -31] + ], + [ + [96, 122, 100, 77, -43, -31], + [197, 122, 100, 77, -43, -31], + [298, 122, 100, 77, -43, -31], + [399, 122, 100, 77, -43, -31], + [500, 122, 100, 77, -43, -31], + [601, 122, 100, 77, -43, -31], + [702, 122, 100, 77, -43, -31], + [803, 122, 100, 77, -43, -31], + [904, 122, 100, 77, -43, -31], + [0, 200, 100, 77, -43, -31] + ], + [ + [101, 200, 96, 78, -49, -33], + [198, 200, 96, 78, -49, -33], + [295, 200, 96, 78, -49, -33], + [392, 200, 96, 78, -49, -33], + [489, 200, 96, 78, -49, -33], + [586, 200, 96, 78, -49, -33], + [683, 200, 96, 78, -49, -33], + [780, 200, 96, 78, -49, -33], + [877, 200, 96, 78, -49, -33], + [0, 279, 96, 78, -49, -33] + ], + [ + [97, 279, 95, 77, -41, -34], + [193, 279, 95, 77, -41, -34], + [289, 279, 95, 77, -41, -34], + [385, 279, 95, 77, -41, -34], + [481, 279, 95, 77, -41, -34], + [577, 279, 95, 77, -41, -34], + [673, 279, 95, 77, -41, -34], + [769, 279, 95, 77, -41, -34], + [865, 279, 95, 77, -41, -34], + [0, 358, 95, 77, -41, -34] + ], + [ + [96, 358, 105, 56, -50, -27], + [202, 358, 105, 56, -50, -27], + [308, 358, 105, 56, -50, -27], + [414, 358, 105, 56, -50, -27], + [520, 358, 105, 56, -50, -27], + [626, 358, 105, 56, -50, -27], + [732, 358, 105, 56, -50, -27], + [838, 358, 105, 56, -50, -27], + [0, 436, 105, 56, -50, -27], + [106, 436, 105, 56, -50, -27] + ], + [ + [212, 436, 93, 70, -53, -33], + [306, 436, 93, 70, -53, -33], + [400, 436, 93, 70, -53, -33], + [494, 436, 93, 70, -53, -33], + [588, 436, 93, 70, -53, -33], + [682, 436, 93, 70, -53, -33], + [776, 436, 93, 70, -53, -33], + [870, 436, 93, 70, -53, -33], + [0, 507, 93, 70, -53, -33], + [94, 507, 93, 70, -53, -33] + ], + [ + [188, 507, 101, 69, -55, -32], + [290, 507, 101, 69, -55, -32], + [392, 507, 101, 69, -55, -32], + [494, 507, 101, 69, -55, -32], + [596, 507, 101, 69, -55, -32], + [698, 507, 101, 69, -55, -32], + [800, 507, 101, 69, -55, -32], + [902, 507, 101, 69, -55, -32], + [0, 578, 101, 69, -55, -32], + [102, 578, 101, 69, -55, -32] + ] + ], +} + +export const BLADEFURY_META: MissileMeta = { + name: "bladefury", + celFile: "Expansion/bladefury", + width: 512, + height: 6, + directions: 1, + framesPerDirection: 7, + animSpeed: 16, + groups: [ + [ + [0, 0, 13, 6, -6, -34], + [14, 0, 13, 6, -6, -34], + [28, 0, 13, 6, -6, -34], + [42, 0, 13, 6, -6, -34], + [56, 0, 13, 6, -6, -34], + [70, 0, 13, 6, -6, -34], + [84, 0, 13, 6, -6, -34] + ] + ], +} + +export const BLADEFURYB_META: MissileMeta = { + name: "bladefuryb", + celFile: "Expansion/bladefury_B", + width: 512, + height: 9, + directions: 1, + framesPerDirection: 7, + animSpeed: 16, + groups: [ + [ + [0, 0, 20, 9, -10, -35], + [21, 0, 20, 9, -10, -35], + [42, 0, 20, 9, -10, -35], + [63, 0, 20, 9, -10, -35], + [84, 0, 20, 9, -10, -35], + [105, 0, 20, 9, -10, -35], + [126, 0, 20, 9, -10, -35] + ] + ], +} + +export const BLADEFURYC_META: MissileMeta = { + name: "bladefuryc", + celFile: "Expansion/bladefury_C", + width: 512, + height: 11, + directions: 1, + framesPerDirection: 7, + animSpeed: 16, + groups: [ + [ + [0, 0, 24, 11, -12, -36], + [25, 0, 24, 11, -12, -36], + [50, 0, 24, 11, -12, -36], + [75, 0, 24, 11, -12, -36], + [100, 0, 24, 11, -12, -36], + [125, 0, 24, 11, -12, -36], + [150, 0, 24, 11, -12, -36] + ] + ], +} + +export const DEATHSENTRYEXPLODE_META: MissileMeta = { + name: "deathsentryexplode", + celFile: "Expansion/dethsentry02", + width: 512, + height: 275, + directions: 1, + framesPerDirection: 12, + animSpeed: 16, + groups: [ + [ + [0, 0, 115, 91, -66, -66], + [116, 0, 115, 91, -66, -66], + [232, 0, 115, 91, -66, -66], + [348, 0, 115, 91, -66, -66], + [0, 92, 115, 91, -66, -66], + [116, 92, 115, 91, -66, -66], + [232, 92, 115, 91, -66, -66], + [348, 92, 115, 91, -66, -66], + [0, 184, 115, 91, -66, -66], + [116, 184, 115, 91, -66, -66], + [232, 184, 115, 91, -66, -66], + [348, 184, 115, 91, -66, -66] + ] + ], +} + +export const BLADESHIELDMISSILE_META: MissileMeta = { + name: "bladeshieldmissile", + celFile: "Expansion/blade_shield_missile", + width: 512, + height: 11, + directions: 1, + framesPerDirection: 5, + animSpeed: 16, + groups: [ + [ + [0, 0, 20, 11, -10, -7], + [21, 0, 20, 11, -10, -7], + [42, 0, 20, 11, -10, -7], + [63, 0, 20, 11, -10, -7], + [84, 0, 20, 11, -10, -7] + ] + ], +} + export const BLIZZARD4_META: MissileMeta = { ...BLIZZARD3_META, name: 'blizzard4', @@ -16939,6 +17570,16 @@ export const MISSILE_METAS: Readonly> = Object.freez 'hurricaneswoosh': HURRICANESWOOSH_META, 'hurricanerock': HURRICANEROCK_META, 'hurricanetree': HURRICANETREE_META, + 'firetrauma': FIRETRAUMA_META, + 'firetraumaground': FIRETRAUMAGROUND_META, + 'firetraumaexplode': FIRETRAUMAEXPLODE_META, + 'shockfieldair': SHOCKFIELDAIR_META, + 'shockfieldground': SHOCKFIELDGROUND_META, + 'bladefury': BLADEFURY_META, + 'bladefuryb': BLADEFURYB_META, + 'bladefuryc': BLADEFURYC_META, + 'deathsentryexplode': DEATHSENTRYEXPLODE_META, + 'bladeshieldmissile': BLADESHIELDMISSILE_META, 'revivesmall': REVIVESMALL_META, 'revivemedium': REVIVEMEDIUM_META, 'revivelarge': REVIVELARGE_META, diff --git a/src/render/overlays-meta.ts b/src/render/overlays-meta.ts index a2d3d1c..122b343 100644 --- a/src/render/overlays-meta.ts +++ b/src/render/overlays-meta.ts @@ -22058,6 +22058,320 @@ export const DIREWOLFCHARGED_OVERLAY_META: OverlayMeta = { ] } +export const BLADE_SHIELD_META: OverlayMeta = { + "name": "blade_shield", + "celFile": "Expansion/blade_shield", + "width": 512, + "height": 200, + "directions": 1, + "frames": 20, + "framesPerDirection": 20, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -33, + "top": -68, + "width": 64, + "height": 66 + }, + "groups": [ + [ + [ + 0, + 0, + 64, + 66, + -33, + -68 + ], + [ + 65, + 0, + 64, + 66, + -33, + -68 + ], + [ + 130, + 0, + 64, + 66, + -33, + -68 + ], + [ + 195, + 0, + 64, + 66, + -33, + -68 + ], + [ + 260, + 0, + 64, + 66, + -33, + -68 + ], + [ + 325, + 0, + 64, + 66, + -33, + -68 + ], + [ + 390, + 0, + 64, + 66, + -33, + -68 + ], + [ + 0, + 67, + 64, + 66, + -33, + -68 + ], + [ + 65, + 67, + 64, + 66, + -33, + -68 + ], + [ + 130, + 67, + 64, + 66, + -33, + -68 + ], + [ + 195, + 67, + 64, + 66, + -33, + -68 + ], + [ + 260, + 67, + 64, + 66, + -33, + -68 + ], + [ + 325, + 67, + 64, + 66, + -33, + -68 + ], + [ + 390, + 67, + 64, + 66, + -33, + -68 + ], + [ + 0, + 134, + 64, + 66, + -33, + -68 + ], + [ + 65, + 134, + 64, + 66, + -33, + -68 + ], + [ + 130, + 134, + 64, + 66, + -33, + -68 + ], + [ + 195, + 134, + 64, + 66, + -33, + -68 + ], + [ + 260, + 134, + 64, + 66, + -33, + -68 + ], + [ + 325, + 134, + 64, + 66, + -33, + -68 + ] + ] + ] +} + +export const DEATHSENTRY_EXPLODE_META: OverlayMeta = { + "name": "deathsentry_explode", + "celFile": "expansion/dethsentry01", + "width": 512, + "height": 547, + "directions": 1, + "frames": 12, + "framesPerDirection": 12, + "animRate": 16, + "trans": 3, + "preDraw": false, + "lightRadius": 0, + "lightColor": [ + 255, + 255, + 255 + ], + "box": { + "left": -73, + "top": -93, + "width": 130, + "height": 136 + }, + "groups": [ + [ + [ + 0, + 0, + 130, + 136, + -73, + -93 + ], + [ + 131, + 0, + 130, + 136, + -73, + -93 + ], + [ + 262, + 0, + 130, + 136, + -73, + -93 + ], + [ + 0, + 137, + 130, + 136, + -73, + -93 + ], + [ + 131, + 137, + 130, + 136, + -73, + -93 + ], + [ + 262, + 137, + 130, + 136, + -73, + -93 + ], + [ + 0, + 274, + 130, + 136, + -73, + -93 + ], + [ + 131, + 274, + 130, + 136, + -73, + -93 + ], + [ + 262, + 274, + 130, + 136, + -73, + -93 + ], + [ + 0, + 411, + 130, + 136, + -73, + -93 + ], + [ + 131, + 411, + 130, + 136, + -73, + -93 + ], + [ + 262, + 411, + 130, + 136, + -73, + -93 + ] + ] + ] +} + // Backward compatibility aliases for primary aura constants export const MIGHT_AURA_META: OverlayMeta = MIGHT_FRONT_AURA_META @@ -22297,6 +22611,8 @@ export const ALL_OVERLAY_NAMES: readonly string[] = [ "feralrage4back", "feralrage5back", "direwolfcharged", + "blade_shield", + "deathsentry_explode", ] export const OVERLAY_METAS: Readonly> = { @@ -22899,4 +23215,12 @@ export const OVERLAY_METAS: Readonly> = { "expansion/direwolfcharged": DIREWOLFCHARGED_OVERLAY_META, "dire_wolf_charged": DIREWOLFCHARGED_OVERLAY_META, "direwolf_charged": DIREWOLFCHARGED_OVERLAY_META, + "blade_shield": BLADE_SHIELD_META, + "Expansion/blade_shield": BLADE_SHIELD_META, + "expansion/blade_shield": BLADE_SHIELD_META, + "bladeshield": BLADE_SHIELD_META, + "deathsentry_explode": DEATHSENTRY_EXPLODE_META, + "expansion/dethsentry01": DEATHSENTRY_EXPLODE_META, + "deathsentry": DEATHSENTRY_EXPLODE_META, + "dethsentry01": DEATHSENTRY_EXPLODE_META, } diff --git a/tests/skills/ass/adv-ass-traps-stress.test.ts b/tests/skills/ass/adv-ass-traps-stress.test.ts new file mode 100644 index 0000000..5d3a6ad --- /dev/null +++ b/tests/skills/ass/adv-ass-traps-stress.test.ts @@ -0,0 +1,560 @@ +/** + * Adversarial Stress & Ground Truth Parity Test Suite — Assassin Traps (10 skills) + * + * Validates 1.13c Diablo II mechanics: + * 1. Global 5-trap cap on PetType `assassintrap` with strict FIFO eviction. + * 2. NextDelay mechanics (Wake of Fire 4-frame immunity, Shock Web 25-frame immunity). + * 3. Death Sentry dual-mode AI (corpse prioritization, 50/50 phys/fire split, radius progression, shot scaling). + * 4. Blade skills weapon damage scaling (3/8 for Sentinel, 3/4 for Fury, 1/4 for Shield). + * 5. Shock Web missile cluster dispersion and synergy scaling. + * 6. Sentry shot depletion and life cycles. + * + * Gitea Milestone: M26 (Issue #473) + */ + +import { describe, expect, it } from 'vitest' +import { + ASSASSIN_TRAP_SKILL_IDS, + MAX_ACTIVE_TRAPS, + BLADE_SRC_DAM, + NEXT_DELAY_FRAMES, + TrapManager, + NextDelayTracker, + computeBladeWeaponDamage, + evaluateFireBlastStats, + evaluateShockWebStats, + evaluateBladeSentinelStats, + evaluateChargedBoltSentryStats, + evaluateWakeOfFireStats, + evaluateBladeFuryStats, + evaluateLightningSentryStats, + evaluateWakeOfInfernoStats, + evaluateDeathSentryStats, + evaluateBladeShieldStats, + simulateDeathSentryAI, + simulateWakeOfFireWaveHit, + computeShockWebClusterPoints, + type SimulationCorpse, + type SimulationEnemy, +} from '../../../src/game/skills/assassin-traps.ts' + +describe('Assassin Traps Ground Truth & Adversarial Suite (Milestone M26)', () => { + describe('1. Global Trap Cap & FIFO Eviction (PetType: assassintrap, Limit: 5)', () => { + it('allows up to 5 traps without eviction', () => { + const manager = new TrapManager() + const ownerId = 'assassin_1' + + // Summon 5 different traps + const trap1 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.CHARGED_BOLT_SENTRY, + summonMon: 'chargeboltsentry', + x: 100, + y: 100, + slvl: 20, + shots: 5, + currentFrame: 10, + }) + expect(trap1.evictedTrap).toBeUndefined() + expect(manager.getActiveTrapCount(ownerId)).toBe(1) + + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.WAKE_OF_FIRE, + summonMon: 'wakeofdestruction', + x: 120, + y: 100, + slvl: 20, + shots: 5, + currentFrame: 20, + }) + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, + summonMon: 'lightningsentry', + x: 140, + y: 100, + slvl: 20, + shots: 10, + currentFrame: 30, + }) + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.WAKE_OF_INFERNO, + summonMon: 'infernosentry', + x: 160, + y: 100, + slvl: 20, + shots: 10, + currentFrame: 40, + }) + const trap5 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, + summonMon: 'deathsentry', + x: 180, + y: 100, + slvl: 20, + shots: 5, + currentFrame: 50, + }) + + expect(trap5.evictedTrap).toBeUndefined() + expect(manager.getActiveTrapCount(ownerId)).toBe(5) + }) + + it('strictly evicts the oldest trap (FIFO) when a 6th trap is summoned', () => { + const manager = new TrapManager() + const ownerId = 'assassin_1' + + // Register 5 traps + const t1 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, + summonMon: 'lightningsentry', + x: 10, + y: 10, + slvl: 20, + shots: 10, + currentFrame: 10, + }).trap + + const t2 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, + summonMon: 'lightningsentry', + x: 20, + y: 10, + slvl: 20, + shots: 10, + currentFrame: 20, + }).trap + + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, + summonMon: 'lightningsentry', + x: 30, + y: 10, + slvl: 20, + shots: 10, + currentFrame: 30, + }) + + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, + summonMon: 'deathsentry', + x: 40, + y: 10, + slvl: 20, + shots: 5, + currentFrame: 40, + }) + + manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, + summonMon: 'deathsentry', + x: 50, + y: 10, + slvl: 20, + shots: 5, + currentFrame: 50, + }) + + expect(manager.getActiveTrapCount(ownerId)).toBe(5) + + // Summon 6th trap -> t1 (the oldest) MUST be evicted! + const res6 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.WAKE_OF_FIRE, + summonMon: 'wakeofdestruction', + x: 60, + y: 10, + slvl: 20, + shots: 5, + currentFrame: 60, + }) + + expect(res6.evictedTrap).toBeDefined() + expect(res6.evictedTrap?.id).toBe(t1.id) + expect(manager.getActiveTrapCount(ownerId)).toBe(5) + + // Active traps should NOT contain t1 + const activeIds = manager.getActiveTraps(ownerId).map(t => t.id) + expect(activeIds).not.toContain(t1.id) + expect(activeIds).toContain(res6.trap.id) + + // Summon 7th trap -> t2 MUST be evicted! + const res7 = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.CHARGED_BOLT_SENTRY, + summonMon: 'chargeboltsentry', + x: 70, + y: 10, + slvl: 20, + shots: 5, + currentFrame: 70, + }) + expect(res7.evictedTrap?.id).toBe(t2.id) + expect(manager.getActiveTrapCount(ownerId)).toBe(5) + }) + + it('isolates trap quotas per player/owner in multiplayer arena', () => { + const manager = new TrapManager() + const p1 = 'assassin_alice' + const p2 = 'assassin_bob' + + for (let i = 0; i < 5; i++) { + manager.registerTrap({ + ownerId: p1, + skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, + summonMon: 'lightningsentry', + x: i * 10, + y: 0, + slvl: 20, + shots: 10, + currentFrame: 0, + }) + manager.registerTrap({ + ownerId: p2, + skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, + summonMon: 'deathsentry', + x: i * 10, + y: 50, + slvl: 20, + shots: 5, + currentFrame: 0, + }) + } + + expect(manager.getActiveTrapCount(p1)).toBe(5) + expect(manager.getActiveTrapCount(p2)).toBe(5) + + // Summon 6th trap for p1; only p1's trap is evicted, p2's remain intact + const res = manager.registerTrap({ + ownerId: p1, + skillId: ASSASSIN_TRAP_SKILL_IDS.WAKE_OF_FIRE, + summonMon: 'wakeofdestruction', + x: 999, + y: 0, + slvl: 20, + shots: 5, + currentFrame: 100, + }) + + expect(res.evictedTrap?.ownerId).toBe(p1) + expect(manager.getActiveTrapCount(p1)).toBe(5) + expect(manager.getActiveTrapCount(p2)).toBe(5) + }) + + it('automatically destroys trap when shot count reaches zero', () => { + const manager = new TrapManager() + const ownerId = 'assassin_1' + + const { trap } = manager.registerTrap({ + ownerId, + skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, + summonMon: 'deathsentry', + x: 0, + y: 0, + slvl: 1, + shots: 3, + currentFrame: 0, + }) + + expect(manager.getActiveTrapCount(ownerId)).toBe(1) + + const s1 = manager.consumeShot(trap.id) + expect(s1.remaining).toBe(2) + expect(s1.destroyed).toBe(false) + expect(manager.getActiveTrapCount(ownerId)).toBe(1) + + const s2 = manager.consumeShot(trap.id) + expect(s2.remaining).toBe(1) + expect(s2.destroyed).toBe(false) + + const s3 = manager.consumeShot(trap.id) + expect(s3.remaining).toBe(0) + expect(s3.destroyed).toBe(true) + expect(manager.getActiveTrapCount(ownerId)).toBe(0) + }) + }) + + describe('2. NextDelay Mechanics (Wake of Fire & Shock Web)', () => { + it('enforces 4-frame damage immunity for Wake of Fire', () => { + const tracker = new NextDelayTracker() + const enemyId = 'monster_1' + + // At frame 100, monster is eligible + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 100)).toBe(true) + + // Hit monster at frame 100 with NextDelay 4 + tracker.recordHit(enemyId, 'wake_of_fire', 100, NEXT_DELAY_FRAMES.WAKE_OF_FIRE) + + // Monster must be immune at frame 100, 101, 102, 103 + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 100)).toBe(false) + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 101)).toBe(false) + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 102)).toBe(false) + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 103)).toBe(false) + + // At frame 104, NextDelay window has expired (100 + 4 = 104) + expect(tracker.canTakeDamage(enemyId, 'wake_of_fire', 104)).toBe(true) + }) + + it('enforces 25-frame damage immunity for Shock Web', () => { + const tracker = new NextDelayTracker() + const enemyId = 'monster_boss' + + expect(tracker.canTakeDamage(enemyId, 'shock_web', 500)).toBe(true) + tracker.recordHit(enemyId, 'shock_web', 500, NEXT_DELAY_FRAMES.SHOCK_WEB) + + // Immune throughout frames 500..524 + expect(tracker.canTakeDamage(enemyId, 'shock_web', 510)).toBe(false) + expect(tracker.canTakeDamage(enemyId, 'shock_web', 524)).toBe(false) + + // Vulnerable again at frame 525 (500 + 25 = 525) + expect(tracker.canTakeDamage(enemyId, 'shock_web', 525)).toBe(true) + }) + + it('simulates Wake of Fire wave hit against grouped enemies with NextDelay', () => { + const tracker = new NextDelayTracker() + const enemies: SimulationEnemy[] = [ + { id: 'm1', x: 100, y: 100, currentHp: 500, maxHp: 500 }, + { id: 'm2', x: 150, y: 100, currentHp: 500, maxHp: 500 }, + ] + + const trap: any = { + id: 'wof_1', + x: 0, + y: 100, + slvl: 20, + } + + // First wave at frame 0 + const hit0 = simulateWakeOfFireWaveHit(trap, enemies, 0, tracker) + expect(hit0.enemiesHit).toContain('m1') + expect(hit0.enemiesHit).toContain('m2') + expect(enemies[0]!.currentHp).toBeLessThan(500) + + // Rapid overlap wave at frame 2 (within 4-frame NextDelay window) -> deals 0 damage! + const hpBefore = enemies[0]!.currentHp + const hit2 = simulateWakeOfFireWaveHit(trap, enemies, 2, tracker) + expect(hit2.enemiesHit).toHaveLength(0) + expect(hit2.totalDamageDealt).toBe(0) + expect(enemies[0]!.currentHp).toBe(hpBefore) + + // Next wave at frame 5 (> 4 frames) -> hits again! + const hit5 = simulateWakeOfFireWaveHit(trap, enemies, 5, tracker) + expect(hit5.enemiesHit).toContain('m1') + expect(enemies[0]!.currentHp).toBeLessThan(hpBefore) + }) + }) + + describe('3. Death Sentry Dual-Mode Corpse Explosion AI', () => { + it('prioritizes Corpse Explosion over Lightning when a corpse is in radius', () => { + const trap: any = { id: 'ds_1', x: 100, y: 100, slvl: 20 } + const corpses: SimulationCorpse[] = [ + { id: 'corpse_1', x: 120, y: 100, baseMaxHp: 1000, consumed: false }, + ] + const enemies: SimulationEnemy[] = [ + { id: 'live_mon', x: 130, y: 100, currentHp: 800, maxHp: 800 }, + ] + + const decision = simulateDeathSentryAI(trap, corpses, enemies) + expect(decision.action).toBe('corpse_explosion') + expect(decision.targetId).toBe('corpse_1') + expect(corpses[0]!.consumed).toBe(true) + // 40-80% of 1000 HP -> 600 damage avg (300 phys, 300 fire) + expect(decision.damageDealt?.phys).toBe(300) + expect(decision.damageDealt?.fire).toBe(300) + expect(decision.damageDealt?.ltng).toBe(0) + expect(enemies[0]!.currentHp).toBe(200) + }) + + it('falls back to piercing lightning bolt when no valid corpse exists in radius', () => { + const trap: any = { id: 'ds_1', x: 0, y: 0, slvl: 20 } + const corpses: SimulationCorpse[] = [ + // Corpse is far away outside radius (> 29 subtiles * 16px = 464px) + { id: 'corpse_far', x: 900, y: 900, baseMaxHp: 1000, consumed: false }, + ] + const enemies: SimulationEnemy[] = [ + { id: 'near_mon', x: 100, y: 0, currentHp: 500, maxHp: 500 }, + ] + + const decision = simulateDeathSentryAI(trap, corpses, enemies) + expect(decision.action).toBe('lightning') + expect(decision.targetId).toBe('near_mon') + expect(decision.damageDealt?.ltng).toBeGreaterThan(0) + expect(decision.damageDealt?.phys).toBe(0) + expect(decision.damageDealt?.fire).toBe(0) + expect(corpses[0]!.consumed).toBe(false) + }) + + it('evaluates Death Sentry shot count scaling with Fire Blast synergy', () => { + // Base shots: 5. +1 shot per 3 hard points of Fire Blast + const s0 = evaluateDeathSentryStats(20, { fireBlast: 0 }) + expect(s0.shots).toBe(5) + + const s2 = evaluateDeathSentryStats(20, { fireBlast: 2 }) + expect(s2.shots).toBe(5) + + const s3 = evaluateDeathSentryStats(20, { fireBlast: 3 }) + expect(s3.shots).toBe(6) + + const s9 = evaluateDeathSentryStats(20, { fireBlast: 9 }) + expect(s9.shots).toBe(8) + + const s20 = evaluateDeathSentryStats(20, { fireBlast: 20 }) + expect(s20.shots).toBe(11) // 5 + floor(20 / 3) = 5 + 6 = 11 shots + }) + + it('verifies Corpse Explosion radius progression: 10 + (slvl - 1) subtiles', () => { + const s1 = evaluateDeathSentryStats(1) + expect(s1.corpseExplosionRadiusSubtiles).toBe(10) // 10 subtiles = 3.3 yards + + const s10 = evaluateDeathSentryStats(10) + expect(s10.corpseExplosionRadiusSubtiles).toBe(19) // 19 subtiles = 6.3 yards + + const s20 = evaluateDeathSentryStats(20) + expect(s20.corpseExplosionRadiusSubtiles).toBe(29) // 29 subtiles = 9.6 yards + }) + }) + + describe('4. Blade Skills Weapon Damage Scaling (srcDam 128ths)', () => { + it('scales Blade Sentinel weapon damage to exact 3/8 (37.5%)', () => { + expect(BLADE_SRC_DAM.BLADE_SENTINEL).toBe(48) + // 48 / 128 = 3/8 + + // Weapon damage 80 -> 80 * 3/8 = 30 + expect(computeBladeWeaponDamage(80, ASSASSIN_TRAP_SKILL_IDS.BLADE_SENTINEL)).toBe(30) + // Weapon damage 240 -> 240 * 3/8 = 90 + expect(computeBladeWeaponDamage(240, ASSASSIN_TRAP_SKILL_IDS.BLADE_SENTINEL)).toBe(90) + // Weapon damage 100 -> floor(100 * 48 / 128) = floor(37.5) = 37 + expect(computeBladeWeaponDamage(100, ASSASSIN_TRAP_SKILL_IDS.BLADE_SENTINEL)).toBe(37) + }) + + it('scales Blade Fury weapon damage to exact 3/4 (75.0%)', () => { + expect(BLADE_SRC_DAM.BLADE_FURY).toBe(96) + // 96 / 128 = 3/4 + + // Weapon damage 80 -> 80 * 3/4 = 60 + expect(computeBladeWeaponDamage(80, ASSASSIN_TRAP_SKILL_IDS.BLADE_FURY)).toBe(60) + // Weapon damage 240 -> 240 * 3/4 = 180 + expect(computeBladeWeaponDamage(240, ASSASSIN_TRAP_SKILL_IDS.BLADE_FURY)).toBe(180) + // Weapon damage 100 -> floor(100 * 96 / 128) = 75 + expect(computeBladeWeaponDamage(100, ASSASSIN_TRAP_SKILL_IDS.BLADE_FURY)).toBe(75) + }) + + it('scales Blade Shield weapon damage to exact 1/4 (25.0%)', () => { + expect(BLADE_SRC_DAM.BLADE_SHIELD).toBe(32) + // 32 / 128 = 1/4 + + // Weapon damage 80 -> 80 * 1/4 = 20 + expect(computeBladeWeaponDamage(80, ASSASSIN_TRAP_SKILL_IDS.BLADE_SHIELD)).toBe(20) + // Weapon damage 240 -> 240 * 1/4 = 60 + expect(computeBladeWeaponDamage(240, ASSASSIN_TRAP_SKILL_IDS.BLADE_SHIELD)).toBe(60) + // Weapon damage 100 -> floor(100 * 32 / 128) = 25 + expect(computeBladeWeaponDamage(100, ASSASSIN_TRAP_SKILL_IDS.BLADE_SHIELD)).toBe(25) + }) + + it('combines flat physical damage with scaled weapon damage on Blade skills', () => { + const sentStats = evaluateBladeSentinelStats(20, 200) + // flat min = 79, max = 83. scaled weapon (200 * 3/8) = 75 + expect(sentStats.scaledWeaponPhys).toBe(75) + expect(sentStats.totalMinPhys).toBe(79 + 75) + expect(sentStats.totalMaxPhys).toBe(83 + 75) + + const furyStats = evaluateBladeFuryStats(20, 200) + // flat min = 101, max = 103. scaled weapon (200 * 3/4) = 150 + expect(furyStats.scaledWeaponPhys).toBe(150) + expect(furyStats.totalMinPhys).toBe(101 + 150) + expect(furyStats.totalMaxPhys).toBe(103 + 150) + + const shieldStats = evaluateBladeShieldStats(20, 200) + // flat min = 112, max = 141. scaled weapon (200 * 1/4) = 50 + expect(shieldStats.scaledWeaponPhys).toBe(50) + expect(shieldStats.totalMinPhys).toBe(112 + 50) + expect(shieldStats.totalMaxPhys).toBe(141 + 50) + }) + }) + + describe('5. Shock Web Cluster & Synergy Mechanics', () => { + it('calculates exact missile count: 6 + floor(slvl / 4) + floor(FireBlast.blvl / 3)', () => { + const sw1 = evaluateShockWebStats(1, { fireBlast: 0 }) + expect(sw1.missileCount).toBe(6) + + const sw4 = evaluateShockWebStats(4, { fireBlast: 0 }) + expect(sw4.missileCount).toBe(7) + + const sw20 = evaluateShockWebStats(20, { fireBlast: 0 }) + expect(sw20.missileCount).toBe(11) // 6 + 5 = 11 + + const sw20_syn = evaluateShockWebStats(20, { fireBlast: 20 }) + expect(sw20_syn.missileCount).toBe(17) // 6 + 5 + 6 = 17 + }) + + it('generates distributed cluster points around impact origin', () => { + const points = computeShockWebClusterPoints(200, 200, 20, 20, 64) + expect(points).toHaveLength(17) + // Ensure all points lie within max cluster radius (64 + epsilon) + for (const pt of points) { + const dist = Math.hypot(pt.x - 200, pt.y - 200) + expect(dist).toBeLessThanOrEqual(65) + } + }) + + it('applies +11% synergy per hard point of CBS, LS, DS', () => { + const base = evaluateShockWebStats(20, { chargedBoltSentry: 0, lightningSentry: 0, deathSentry: 0 }) + const syn = evaluateShockWebStats(20, { chargedBoltSentry: 20, lightningSentry: 20, deathSentry: 20 }) + // 60 hard points * 11% = 660% bonus damage + expect(syn.synergyPct).toBe(660) + expect(syn.maxDamage).toBe(Math.floor(base.maxDamage * (1 + 6.6))) + }) + }) + + describe('6. Sentry Lifecycles & Synergies', () => { + it('evaluates Charged Bolt Sentry shots and bolt count scaling', () => { + const cbs = evaluateChargedBoltSentryStats(20, { + lightningSentry: 20, // +5 shots + shockWeb: 20, // +6 bolts + }) + // Base shots: 5 + floor(20/4) = 10 shots + expect(cbs.shots).toBe(10) + // Base bolts: 5 + floor(20/3) = 11 bolts per shot + expect(cbs.boltsPerShot).toBe(11) + }) + + it('evaluates Lightning Sentry piercing damage and 12% synergy scaling', () => { + const lsBase = evaluateLightningSentryStats(20, { shockWeb: 0, chargedBoltSentry: 0, deathSentry: 0 }) + expect(lsBase.shots).toBe(10) + expect(lsBase.minDamage).toBe(1) + expect(lsBase.maxDamage).toBe(314) // 5-band level 20 max + + const lsMaxSyn = evaluateLightningSentryStats(20, { shockWeb: 20, chargedBoltSentry: 20, deathSentry: 20 }) + // 60 * 12% = 720% bonus + expect(lsMaxSyn.synergyPct).toBe(720) + expect(lsMaxSyn.maxDamage).toBe(Math.floor(314 * (1 + 7.2))) + }) + + it('evaluates Wake of Inferno 10 attacks and 7% synergies', () => { + const woi = evaluateWakeOfInfernoStats(20, { fireBlast: 20, wakeOfFire: 20, deathSentry: 20 }) + expect(woi.attacks).toBe(10) + // 60 * 7% = 420% synergy + expect(woi.synergyPct).toBe(420) + }) + + it('evaluates Blade Shield 20s-115s duration and 25-frame tick rate', () => { + const bs1 = evaluateBladeShieldStats(1) + expect(bs1.durationFrames).toBe(500) // 20s + expect(bs1.tickRateFrames).toBe(25) // 1.0s + expect(bs1.radiusSubtiles).toBe(6) + + const bs20 = evaluateBladeShieldStats(20) + expect(bs20.durationFrames).toBe(2875) // 500 + 125 * 19 = 2875 frames = 115s + expect(bs20.tickRateFrames).toBe(25) + expect(bs20.manaCost).toBe(65) // 27 + 2 * 19 = 65 + }) + }) +})