@vibemancer/core 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/{chunk-NR326XEY.js → chunk-MUU3ELH5.js} +9 -4
- package/dist/chunk-MUU3ELH5.js.map +1 -0
- package/dist/{index-browser-iiIpqpDl.d.ts → index-browser-ClmR9SkR.d.ts} +2 -1
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/engine/manual-match.ts +3 -1
- package/src/engine-version.ts +1 -1
- package/src/hooks/threat-analysis.ts +396 -389
- package/src/rules.ts +6 -1
- package/dist/chunk-NR326XEY.js.map +0 -1
|
@@ -1,389 +1,396 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VIBEMANCER - THREAT ANALYSIS
|
|
3
|
-
*
|
|
4
|
-
* Pre-computes threat information for incoming projectiles.
|
|
5
|
-
* This handles the "subconscious" perception of missile trajectories.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {Position, ProjectileState} from '../types.js';
|
|
9
|
-
import {
|
|
10
|
-
RULES,
|
|
11
|
-
COLLISION_RADIUS,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from '../
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param
|
|
31
|
-
* @param
|
|
32
|
-
* @param
|
|
33
|
-
* @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(projectile.position.
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Stryker disable
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
else if (canDodgeLeft)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - THREAT ANALYSIS
|
|
3
|
+
*
|
|
4
|
+
* Pre-computes threat information for incoming projectiles.
|
|
5
|
+
* This handles the "subconscious" perception of missile trajectories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Position, ProjectileState} from '../types.js';
|
|
9
|
+
import {
|
|
10
|
+
RULES,
|
|
11
|
+
COLLISION_RADIUS,
|
|
12
|
+
calculateMissileRadius,
|
|
13
|
+
ARENA_SIZE,
|
|
14
|
+
ARENA_MIN,
|
|
15
|
+
ARENA_MAX,
|
|
16
|
+
ARENA_WATER_BUFFER,
|
|
17
|
+
} from '../rules.js';
|
|
18
|
+
import {distanceTo} from '../utils/distance.js';
|
|
19
|
+
import {angleTo, normalizeAngle, angleDiff} from '../utils/angles.js';
|
|
20
|
+
import type {AnalyzedThreat} from './types.js';
|
|
21
|
+
|
|
22
|
+
// Maximum distance at which a missile is still tracked as a potential threat
|
|
23
|
+
// (even if simulation says it will miss — homing missiles can change course)
|
|
24
|
+
const THREAT_RELEVANCE_DISTANCE = 500;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Analyze all threats from enemy projectiles.
|
|
28
|
+
*
|
|
29
|
+
* @param myPos - Current position of the wizard
|
|
30
|
+
* @param projectiles - All projectiles in the game
|
|
31
|
+
* @param myProjectiles - Only the bot's own projectiles (used for filtering)
|
|
32
|
+
* @param ticksUntilReady - Ticks until wizard can start a new action
|
|
33
|
+
* @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
|
|
34
|
+
*/
|
|
35
|
+
export function analyzeThreats(
|
|
36
|
+
myPos: Position,
|
|
37
|
+
projectiles: ProjectileState[],
|
|
38
|
+
myProjectiles: ProjectileState[],
|
|
39
|
+
ticksUntilReady: number,
|
|
40
|
+
): AnalyzedThreat[]
|
|
41
|
+
{
|
|
42
|
+
// Filter to enemy projectiles by excluding our own
|
|
43
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
44
|
+
const enemyProjectiles = projectiles.filter((p) => !myProjectileIds.has(p.id));
|
|
45
|
+
|
|
46
|
+
// Analyze each projectile
|
|
47
|
+
const threats: AnalyzedThreat[] = [];
|
|
48
|
+
|
|
49
|
+
for (const projectile of enemyProjectiles)
|
|
50
|
+
{
|
|
51
|
+
const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady);
|
|
52
|
+
// Only include if it will hit or missile is within reasonable distance
|
|
53
|
+
const missileDistance = Math.sqrt(
|
|
54
|
+
(projectile.position.x - myPos.x) ** 2 +
|
|
55
|
+
(projectile.position.y - myPos.y) ** 2,
|
|
56
|
+
);
|
|
57
|
+
// Stryker disable next-line EqualityOperator: < vs <= equivalent (exact distance never equals constant in practice)
|
|
58
|
+
if (analysis.willHit || missileDistance < THREAT_RELEVANCE_DISTANCE)
|
|
59
|
+
{
|
|
60
|
+
threats.push(analysis);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sort by ticksToImpact (soonest first)
|
|
65
|
+
threats.sort((a, b) => a.ticksToImpact - b.ticksToImpact);
|
|
66
|
+
|
|
67
|
+
return threats;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Analyze a single projectile threat.
|
|
72
|
+
*/
|
|
73
|
+
function analyzeOneThreat(
|
|
74
|
+
targetPos: Position,
|
|
75
|
+
projectile: ProjectileState,
|
|
76
|
+
ticksUntilReady: number,
|
|
77
|
+
): AnalyzedThreat
|
|
78
|
+
{
|
|
79
|
+
// Ask the ENGINE for the collision radius rather than re-deriving it.
|
|
80
|
+
//
|
|
81
|
+
// This used to compute `MISSILE_BASE_RADIUS + damage * MISSILE_RADIUS_PER_DAMAGE`, which
|
|
82
|
+
// is the formula every document states — and which is not what the engine collides with.
|
|
83
|
+
// `calculateMissileRadius` multiplies that by 3, and `simulation.ts` feeds its result to
|
|
84
|
+
// the swept-circle check. So for a 10-damage missile this modelled a collision at 8 units
|
|
85
|
+
// while the real one happened at 14, and `useThreats()` — the game's own defensive tool —
|
|
86
|
+
// reported "will miss" for missiles that hit. A bot dodging into a gap between 8 and 14
|
|
87
|
+
// was walking into the missile on the engine's own advice.
|
|
88
|
+
const missileRadius = calculateMissileRadius(projectile.damage);
|
|
89
|
+
const collisionDist = COLLISION_RADIUS + missileRadius;
|
|
90
|
+
|
|
91
|
+
// Dodge simulation uses a larger collision distance to account for swept circle collision.
|
|
92
|
+
// The real game checks the entire missile path each tick (swept), but the simulation only
|
|
93
|
+
// checks endpoints. Adding half the missile speed compensates for this gap.
|
|
94
|
+
// Stryker disable next-line ArithmeticOperator: +speed*0.5 vs -speed*0.5 or /0.5 — only affects tight-margin dodge outcomes which are inherently stochastic
|
|
95
|
+
const dodgeCollisionDist = collisionDist + projectile.speed * 0.5;
|
|
96
|
+
|
|
97
|
+
// Simulate missile trajectory toward stationary target
|
|
98
|
+
const {willHit, ticksToImpact} = simulateMissileToTarget(
|
|
99
|
+
projectile,
|
|
100
|
+
targetPos,
|
|
101
|
+
collisionDist,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Only simulate dodges for missiles that will actually hit (saves ~75% simulation work)
|
|
105
|
+
let canDodgeLeft = true;
|
|
106
|
+
let canDodgeRight = true;
|
|
107
|
+
let canOutrun = true;
|
|
108
|
+
let bestDodgeDirection: Position | null = null;
|
|
109
|
+
|
|
110
|
+
if (willHit)
|
|
111
|
+
{
|
|
112
|
+
canDodgeLeft = simulateDodge(projectile, targetPos, 'left', dodgeCollisionDist);
|
|
113
|
+
canDodgeRight = simulateDodge(projectile, targetPos, 'right', dodgeCollisionDist);
|
|
114
|
+
canOutrun = simulateDodge(projectile, targetPos, 'away', dodgeCollisionDist);
|
|
115
|
+
|
|
116
|
+
bestDodgeDirection = calculateBestDodgeDirection(
|
|
117
|
+
projectile,
|
|
118
|
+
targetPos,
|
|
119
|
+
canDodgeLeft,
|
|
120
|
+
canDodgeRight,
|
|
121
|
+
canOutrun,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Calculate shield timing
|
|
126
|
+
// Can block if: ticksUntilReady + shield cast time < ticksToImpact
|
|
127
|
+
// Stryker disable next-line EqualityOperator,ArithmeticOperator: ±1 tick boundary is intentional buffer, not precisely testable
|
|
128
|
+
const canBlockInTime = ticksToImpact > ticksUntilReady + RULES.SHIELD_CAST_TIME + 1;
|
|
129
|
+
|
|
130
|
+
// When to start casting shield (leave 1 tick buffer)
|
|
131
|
+
const ticksToStartShield = Math.max(0, ticksToImpact - RULES.SHIELD_CAST_TIME - 1);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: projectile.id,
|
|
135
|
+
projectile,
|
|
136
|
+
ticksToImpact,
|
|
137
|
+
willHit,
|
|
138
|
+
canDodgeLeft,
|
|
139
|
+
canDodgeRight,
|
|
140
|
+
canOutrun,
|
|
141
|
+
bestDodgeDirection,
|
|
142
|
+
canBlockInTime,
|
|
143
|
+
ticksToStartShield,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Simulate a missile moving toward a stationary target.
|
|
149
|
+
* Accounts for homing (turn rate) and expiration.
|
|
150
|
+
*/
|
|
151
|
+
function simulateMissileToTarget(
|
|
152
|
+
projectile: ProjectileState,
|
|
153
|
+
targetPos: Position,
|
|
154
|
+
collisionDist: number,
|
|
155
|
+
): {willHit: boolean; ticksToImpact: number}
|
|
156
|
+
{
|
|
157
|
+
let pos = {...projectile.position};
|
|
158
|
+
let rotation = projectile.rotation;
|
|
159
|
+
const maxTicks = projectile.remainingTicks;
|
|
160
|
+
const speed = projectile.speed;
|
|
161
|
+
const turnRate = projectile.turnRate;
|
|
162
|
+
|
|
163
|
+
// Stryker disable next-line EqualityOperator: <= vs < equivalent (off-by-one at max tick boundary is non-observable)
|
|
164
|
+
for (let tick = 1; tick <= maxTicks; tick++)
|
|
165
|
+
{
|
|
166
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: turnRate>=0 equivalent (homing with rate 0 is no-op)
|
|
167
|
+
if (turnRate > 0)
|
|
168
|
+
{
|
|
169
|
+
const desiredAngle = angleTo(pos, targetPos);
|
|
170
|
+
const diff = angleDiff(rotation, desiredAngle);
|
|
171
|
+
const maxTurn = turnRate;
|
|
172
|
+
|
|
173
|
+
// Stryker disable all: homing snap — equivalent mutations (removing snap or changing boundary doesn't affect hit outcome)
|
|
174
|
+
if (Math.abs(diff) <= maxTurn)
|
|
175
|
+
{
|
|
176
|
+
rotation = desiredAngle;
|
|
177
|
+
}
|
|
178
|
+
// Stryker restore all
|
|
179
|
+
else
|
|
180
|
+
{
|
|
181
|
+
rotation = normalizeAngle(rotation + Math.sign(diff) * maxTurn);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Move forward
|
|
186
|
+
const rad = rotation * (Math.PI / 180);
|
|
187
|
+
pos = {
|
|
188
|
+
x: pos.x + Math.cos(rad) * speed,
|
|
189
|
+
y: pos.y + Math.sin(rad) * speed,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Check if missile is out of bounds (with water buffer)
|
|
193
|
+
if (isOutOfBounds(pos))
|
|
194
|
+
{
|
|
195
|
+
return {willHit: false, ticksToImpact: Infinity};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check collision
|
|
199
|
+
const dist = distanceTo(pos, targetPos);
|
|
200
|
+
// Stryker disable next-line EqualityOperator: <= vs < equivalent (float distance never exactly equals collisionDist)
|
|
201
|
+
if (dist <= collisionDist)
|
|
202
|
+
{
|
|
203
|
+
return {willHit: true, ticksToImpact: tick};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Missile expired without hitting
|
|
208
|
+
return {willHit: false, ticksToImpact: Infinity};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Simulate a dodge attempt.
|
|
213
|
+
* Returns true if the dodge avoids the missile.
|
|
214
|
+
*/
|
|
215
|
+
function simulateDodge(
|
|
216
|
+
projectile: ProjectileState,
|
|
217
|
+
startPos: Position,
|
|
218
|
+
direction: 'left' | 'right' | 'away',
|
|
219
|
+
collisionDist: number,
|
|
220
|
+
): boolean
|
|
221
|
+
{
|
|
222
|
+
// Calculate dodge direction vector
|
|
223
|
+
const dodgeDir = getDodgeDirection(projectile, startPos, direction);
|
|
224
|
+
|
|
225
|
+
// Stryker disable all: guard is equivalent — zero vector means zero movement, missile still hits, returns false either way
|
|
226
|
+
if (dodgeDir.x === 0 && dodgeDir.y === 0)
|
|
227
|
+
{
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
// Stryker restore all
|
|
231
|
+
|
|
232
|
+
let targetPos = {...startPos};
|
|
233
|
+
let missilePos = {...projectile.position};
|
|
234
|
+
let missileRotation = projectile.rotation;
|
|
235
|
+
const maxTicks = projectile.remainingTicks;
|
|
236
|
+
const speed = projectile.speed;
|
|
237
|
+
const turnRate = projectile.turnRate;
|
|
238
|
+
|
|
239
|
+
// Stryker disable next-line EqualityOperator,UpdateOperator: <= vs < off-by-one equivalent; tick-- causes timeout not assertion failure
|
|
240
|
+
for (let tick = 1; tick <= maxTicks; tick++)
|
|
241
|
+
{
|
|
242
|
+
// Stryker disable next-line ArithmeticOperator: RULES.MOVEMENT_SPEED=1 makes * and / identical; sign flip equivalent when wizard is wall-clamped
|
|
243
|
+
const newX = targetPos.x + dodgeDir.x * RULES.MOVEMENT_SPEED;
|
|
244
|
+
// Stryker disable next-line ArithmeticOperator: same as above
|
|
245
|
+
const newY = targetPos.y + dodgeDir.y * RULES.MOVEMENT_SPEED;
|
|
246
|
+
|
|
247
|
+
// Stryker disable all: arena clamp sign — +/- COLLISION_RADIUS equivalent (wizard rarely reaches exact wall boundary during dodge)
|
|
248
|
+
targetPos = {
|
|
249
|
+
x: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newX)),
|
|
250
|
+
y: Math.max(ARENA_MIN + COLLISION_RADIUS, Math.min(ARENA_MAX - COLLISION_RADIUS, newY)),
|
|
251
|
+
};
|
|
252
|
+
// Stryker restore all
|
|
253
|
+
|
|
254
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: turnRate>=0 equivalent (homing with rate 0 is no-op)
|
|
255
|
+
if (turnRate > 0)
|
|
256
|
+
{
|
|
257
|
+
const desiredAngle = angleTo(missilePos, targetPos);
|
|
258
|
+
const diff = angleDiff(missileRotation, desiredAngle);
|
|
259
|
+
const maxTurn = turnRate;
|
|
260
|
+
|
|
261
|
+
// Stryker disable all: homing snap/incremental — equivalent mutations (removing either branch doesn't change dodge outcome)
|
|
262
|
+
if (Math.abs(diff) <= maxTurn)
|
|
263
|
+
{
|
|
264
|
+
missileRotation = desiredAngle;
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
{
|
|
268
|
+
missileRotation = normalizeAngle(missileRotation + Math.sign(diff) * maxTurn);
|
|
269
|
+
}
|
|
270
|
+
// Stryker restore all
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Move missile
|
|
274
|
+
const rad = missileRotation * (Math.PI / 180);
|
|
275
|
+
missilePos = {
|
|
276
|
+
x: missilePos.x + Math.cos(rad) * speed,
|
|
277
|
+
y: missilePos.y + Math.sin(rad) * speed,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Stryker disable all: OOB early-return equivalent — removing it means missile expires instead, still returns true
|
|
281
|
+
if (isOutOfBounds(missilePos))
|
|
282
|
+
{
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
// Stryker restore all
|
|
286
|
+
|
|
287
|
+
// Check collision
|
|
288
|
+
const dist = distanceTo(missilePos, targetPos);
|
|
289
|
+
// Stryker disable next-line EqualityOperator: <= vs < equivalent (float dist never exactly equals collisionDist)
|
|
290
|
+
if (dist <= collisionDist)
|
|
291
|
+
{
|
|
292
|
+
return false; // Got hit while dodging
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Missile expired without hitting
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the dodge direction vector based on direction type.
|
|
302
|
+
*/
|
|
303
|
+
function getDodgeDirection(
|
|
304
|
+
projectile: ProjectileState,
|
|
305
|
+
targetPos: Position,
|
|
306
|
+
direction: 'left' | 'right' | 'away',
|
|
307
|
+
): Position
|
|
308
|
+
{
|
|
309
|
+
// Get missile direction vector
|
|
310
|
+
const missileAngleRad = projectile.rotation * (Math.PI / 180);
|
|
311
|
+
const missileDir = {
|
|
312
|
+
x: Math.cos(missileAngleRad),
|
|
313
|
+
y: Math.sin(missileAngleRad),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
switch (direction)
|
|
317
|
+
{
|
|
318
|
+
case 'left':
|
|
319
|
+
// Perpendicular to missile direction (left = counter-clockwise)
|
|
320
|
+
return {x: missileDir.y, y: -missileDir.x};
|
|
321
|
+
|
|
322
|
+
case 'right':
|
|
323
|
+
// Perpendicular to missile direction (right = clockwise)
|
|
324
|
+
return {x: -missileDir.y, y: missileDir.x};
|
|
325
|
+
|
|
326
|
+
case 'away':
|
|
327
|
+
{
|
|
328
|
+
// Away from missile current position
|
|
329
|
+
const dx = targetPos.x - projectile.position.x;
|
|
330
|
+
const dy = targetPos.y - projectile.position.y;
|
|
331
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
332
|
+
if (len === 0) return {x: 0, y: 0};
|
|
333
|
+
return {x: dx / len, y: dy / len};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
default:
|
|
337
|
+
return {x: 0, y: 0};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Calculate the best dodge direction based on available options.
|
|
343
|
+
*/
|
|
344
|
+
function calculateBestDodgeDirection(
|
|
345
|
+
projectile: ProjectileState,
|
|
346
|
+
targetPos: Position,
|
|
347
|
+
canDodgeLeft: boolean,
|
|
348
|
+
canDodgeRight: boolean,
|
|
349
|
+
canOutrun: boolean,
|
|
350
|
+
): Position | null
|
|
351
|
+
{
|
|
352
|
+
// Stryker disable next-line ConditionalExpression: replacing with false just falls through to the `else if (canDodgeLeft)` which returns the same value
|
|
353
|
+
if (canDodgeLeft && canDodgeRight)
|
|
354
|
+
{
|
|
355
|
+
// Both sides work - pick the one closer to where we want to be
|
|
356
|
+
// For simplicity, just return left
|
|
357
|
+
return getDodgeDirection(projectile, targetPos, 'left');
|
|
358
|
+
}
|
|
359
|
+
else if (canDodgeLeft)
|
|
360
|
+
{
|
|
361
|
+
return getDodgeDirection(projectile, targetPos, 'left');
|
|
362
|
+
}
|
|
363
|
+
else if (canDodgeRight)
|
|
364
|
+
{
|
|
365
|
+
return getDodgeDirection(projectile, targetPos, 'right');
|
|
366
|
+
}
|
|
367
|
+
else if (canOutrun)
|
|
368
|
+
{
|
|
369
|
+
return getDodgeDirection(projectile, targetPos, 'away');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// No dodge available
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if a position is out of bounds (past the water buffer).
|
|
378
|
+
*/
|
|
379
|
+
function isOutOfBounds(pos: Position): boolean
|
|
380
|
+
{
|
|
381
|
+
return (
|
|
382
|
+
pos.x < -ARENA_WATER_BUFFER ||
|
|
383
|
+
pos.x > ARENA_SIZE + ARENA_WATER_BUFFER ||
|
|
384
|
+
pos.y < -ARENA_WATER_BUFFER ||
|
|
385
|
+
pos.y > ARENA_SIZE + ARENA_WATER_BUFFER
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @internal Exposed for mutation testing only. */
|
|
390
|
+
export const _testing = {
|
|
391
|
+
simulateMissileToTarget,
|
|
392
|
+
simulateDodge,
|
|
393
|
+
getDodgeDirection,
|
|
394
|
+
calculateBestDodgeDirection,
|
|
395
|
+
isOutOfBounds,
|
|
396
|
+
};
|