@vibemancer/core 1.0.9 → 1.0.11
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/dist/{chunk-OLGKLCCB.js → chunk-EO7JO2RZ.js} +199 -24
- package/dist/chunk-EO7JO2RZ.js.map +1 -0
- package/dist/{index-browser-BTHlrB_s.d.ts → index-browser-CM0uuzWp.d.ts} +156 -18
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +5 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/bot-compute-budget.ts +10 -0
- package/src/engine/bot-error-capture.ts +179 -0
- package/src/engine/manual-match.ts +2 -0
- package/src/engine/physics.ts +76 -0
- package/src/engine/sandbox-harness.ts +46 -0
- package/src/engine/sandbox.ts +347 -341
- package/src/engine/simulation.ts +120 -10
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +112 -5
- package/src/hooks/state-hooks.ts +409 -407
- package/src/hooks/threat-analysis.ts +57 -18
- package/src/hooks/types.ts +16 -4
- package/src/rules.ts +8 -0
- package/src/types.ts +10 -6
- package/src/utils/combat.ts +379 -371
- package/dist/chunk-OLGKLCCB.js.map +0 -1
package/src/utils/combat.ts
CHANGED
|
@@ -1,371 +1,379 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VIBEMANCER - COMBAT UTILITIES
|
|
3
|
-
*
|
|
4
|
-
* Utilities for combat calculations.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {Position, Velocity, MissileConfig} from '../types.js';
|
|
8
|
-
import {
|
|
9
|
-
calculateMissileCastTime,
|
|
10
|
-
calculateWarmupMultiplier,
|
|
11
|
-
validateMissileConfig,
|
|
12
|
-
TICKS_PER_SECOND,
|
|
13
|
-
MOVEMENT_SPEED,
|
|
14
|
-
CASTING_MOVEMENT_MULT,
|
|
15
|
-
WIZARD_HEALTH,
|
|
16
|
-
MISSILE_BASE_CAST,
|
|
17
|
-
MISSILE_DAMAGE_SCALE,
|
|
18
|
-
MISSILE_DAMAGE_POWER,
|
|
19
|
-
MISSILE_HOMING_COEFF,
|
|
20
|
-
MISSILE_TURN_DURATION_COEFF,
|
|
21
|
-
MISSILE_SPEED_DURATION_COEFF,
|
|
22
|
-
MISSILE_SPEED_DURATION_BASELINE,
|
|
23
|
-
MISSILE_MIN_CAST_TIME,
|
|
24
|
-
MISSILE_MIN_DURATION,
|
|
25
|
-
MISSILE_MIN_DAMAGE,
|
|
26
|
-
WIZARD_RADIUS,
|
|
27
|
-
effectiveTurnRateCost,
|
|
28
|
-
} from '../rules.js';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get cast time in ticks for a missile configuration.
|
|
32
|
-
* Applies the same clamps/validation as the engine before calculating,
|
|
33
|
-
* so the result matches the actual cast time that will be used in-game.
|
|
34
|
-
*
|
|
35
|
-
* If lastMissileConfig is provided, includes warmup multiplier.
|
|
36
|
-
* Pass undefined for first cast (full warmup) or null for base time only.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
t = t2;
|
|
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
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
]
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
{
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - COMBAT UTILITIES
|
|
3
|
+
*
|
|
4
|
+
* Utilities for combat calculations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Position, Velocity, MissileConfig} from '../types.js';
|
|
8
|
+
import {
|
|
9
|
+
calculateMissileCastTime,
|
|
10
|
+
calculateWarmupMultiplier,
|
|
11
|
+
validateMissileConfig,
|
|
12
|
+
TICKS_PER_SECOND,
|
|
13
|
+
MOVEMENT_SPEED,
|
|
14
|
+
CASTING_MOVEMENT_MULT,
|
|
15
|
+
WIZARD_HEALTH,
|
|
16
|
+
MISSILE_BASE_CAST,
|
|
17
|
+
MISSILE_DAMAGE_SCALE,
|
|
18
|
+
MISSILE_DAMAGE_POWER,
|
|
19
|
+
MISSILE_HOMING_COEFF,
|
|
20
|
+
MISSILE_TURN_DURATION_COEFF,
|
|
21
|
+
MISSILE_SPEED_DURATION_COEFF,
|
|
22
|
+
MISSILE_SPEED_DURATION_BASELINE,
|
|
23
|
+
MISSILE_MIN_CAST_TIME,
|
|
24
|
+
MISSILE_MIN_DURATION,
|
|
25
|
+
MISSILE_MIN_DAMAGE,
|
|
26
|
+
WIZARD_RADIUS,
|
|
27
|
+
effectiveTurnRateCost,
|
|
28
|
+
} from '../rules.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get cast time in ticks for a missile configuration.
|
|
32
|
+
* Applies the same clamps/validation as the engine before calculating,
|
|
33
|
+
* so the result matches the actual cast time that will be used in-game.
|
|
34
|
+
*
|
|
35
|
+
* If lastMissileConfig is provided, includes warmup multiplier.
|
|
36
|
+
* Pass undefined for first cast (full warmup) or null for base time only.
|
|
37
|
+
*
|
|
38
|
+
* @returns cast time in TICKS.
|
|
39
|
+
*
|
|
40
|
+
* Note the unit, because there are two of these: `calculateMissileCastTime` in rules.ts has
|
|
41
|
+
* the identical signature and returns SECONDS. vibemancer_api lists both as
|
|
42
|
+
* `(config, last?) => number`, and the whole guide speaks in ticks — so a player budgeting
|
|
43
|
+
* ticks from the seconds one reads 2.747 as "about three ticks" when it is 275.
|
|
44
|
+
* This one is the one a bot wants.
|
|
45
|
+
*/
|
|
46
|
+
export function getMissileCastTime(config: MissileConfig, lastMissileConfig?: MissileConfig | null): number
|
|
47
|
+
{
|
|
48
|
+
const validated = validateMissileConfig(config);
|
|
49
|
+
return Math.ceil(calculateMissileCastTime(validated, lastMissileConfig) * TICKS_PER_SECOND);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate the position to aim at to hit a moving target.
|
|
54
|
+
* Returns the intercept point where a missile would hit the target.
|
|
55
|
+
*
|
|
56
|
+
* @param targetPos - Current target position
|
|
57
|
+
* @param targetVel - Target velocity (units per tick)
|
|
58
|
+
* @param missileSpeed - Missile speed (units per tick)
|
|
59
|
+
* @param myPos - Shooter position
|
|
60
|
+
* @returns The position to aim at
|
|
61
|
+
*/
|
|
62
|
+
export function getLeadPosition(
|
|
63
|
+
targetPos: Position,
|
|
64
|
+
targetVel: Velocity,
|
|
65
|
+
missileSpeed: number,
|
|
66
|
+
myPos: Position,
|
|
67
|
+
): Position
|
|
68
|
+
{
|
|
69
|
+
// Vector from shooter to target
|
|
70
|
+
const dx = targetPos.x - myPos.x;
|
|
71
|
+
const dy = targetPos.y - myPos.y;
|
|
72
|
+
|
|
73
|
+
// Target velocity
|
|
74
|
+
const vx = targetVel.x;
|
|
75
|
+
const vy = targetVel.y;
|
|
76
|
+
|
|
77
|
+
// Quadratic coefficients for intercept time
|
|
78
|
+
// |target + velocity * t - shooter|^2 = (missileSpeed * t)^2
|
|
79
|
+
// Expands to: (vx^2 + vy^2 - speed^2) * t^2 + 2*(vx*dx + vy*dy) * t + (dx^2 + dy^2) = 0
|
|
80
|
+
const a = vx * vx + vy * vy - missileSpeed * missileSpeed;
|
|
81
|
+
const b = 2 * (vx * dx + vy * dy);
|
|
82
|
+
const c = dx * dx + dy * dy;
|
|
83
|
+
|
|
84
|
+
let t: number | null = null;
|
|
85
|
+
|
|
86
|
+
// Handle the linear case (|velocity| === missileSpeed)
|
|
87
|
+
if (Math.abs(a) < 1e-10)
|
|
88
|
+
{
|
|
89
|
+
if (Math.abs(b) > 1e-10)
|
|
90
|
+
{
|
|
91
|
+
const linearT = -c / b;
|
|
92
|
+
if (linearT > 0)
|
|
93
|
+
{
|
|
94
|
+
t = linearT;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
{
|
|
100
|
+
// Quadratic case
|
|
101
|
+
const discriminant = b * b - 4 * a * c;
|
|
102
|
+
|
|
103
|
+
if (discriminant >= 0)
|
|
104
|
+
{
|
|
105
|
+
const sqrtD = Math.sqrt(discriminant);
|
|
106
|
+
const t1 = (-b + sqrtD) / (2 * a);
|
|
107
|
+
const t2 = (-b - sqrtD) / (2 * a);
|
|
108
|
+
|
|
109
|
+
if (t1 > 0 && t2 > 0)
|
|
110
|
+
{
|
|
111
|
+
t = Math.min(t1, t2);
|
|
112
|
+
}
|
|
113
|
+
else if (t1 > 0)
|
|
114
|
+
{
|
|
115
|
+
t = t1;
|
|
116
|
+
}
|
|
117
|
+
else if (t2 > 0)
|
|
118
|
+
{
|
|
119
|
+
t = t2;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// If no intercept solution, just aim at current position
|
|
125
|
+
if (t === null)
|
|
126
|
+
{
|
|
127
|
+
return {x: targetPos.x, y: targetPos.y};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate intercept position
|
|
131
|
+
return {
|
|
132
|
+
x: targetPos.x + vx * t,
|
|
133
|
+
y: targetPos.y + vy * t,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Calculate optimal missile configuration based on target behavior.
|
|
139
|
+
*/
|
|
140
|
+
export function getAdaptiveMissileConfig(
|
|
141
|
+
targetVelocity: Position,
|
|
142
|
+
distance: number,
|
|
143
|
+
): {speed: number; turnRate: number; damage: number; duration: number}
|
|
144
|
+
{
|
|
145
|
+
const targetSpeed = Math.sqrt(targetVelocity.x ** 2 + targetVelocity.y ** 2);
|
|
146
|
+
|
|
147
|
+
// Fast moving target = more homing
|
|
148
|
+
// Slow/stationary target = faster missile, light homing for correction
|
|
149
|
+
if (targetSpeed > 0.8)
|
|
150
|
+
{
|
|
151
|
+
// Moving target - use significant homing
|
|
152
|
+
const turnRate = Math.min(3, 1 + targetSpeed * 1.5);
|
|
153
|
+
return {
|
|
154
|
+
damage: 10,
|
|
155
|
+
speed: 5,
|
|
156
|
+
turnRate,
|
|
157
|
+
duration: Math.min(250, 100 + distance * 0.5),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
{
|
|
162
|
+
// Stationary/slow target - faster missile with light homing for accuracy
|
|
163
|
+
return {
|
|
164
|
+
damage: 10,
|
|
165
|
+
speed: 6,
|
|
166
|
+
turnRate: 1, // Light homing to correct any aiming errors
|
|
167
|
+
duration: 180,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Given a cast-time budget (in ticks) and a target distance, find the best
|
|
174
|
+
* missile config that fits. Maximizes damage while ensuring the missile
|
|
175
|
+
* can reach the target and finishes casting in time.
|
|
176
|
+
*
|
|
177
|
+
* Returns null if no useful missile fits in the budget.
|
|
178
|
+
*
|
|
179
|
+
* How it works: tries several speed/turnRate templates. For each, calculates
|
|
180
|
+
* the minimum duration to reach `distance`, then solves the cast-time formula
|
|
181
|
+
* for the maximum damage that fits within `budgetTicks`.
|
|
182
|
+
*
|
|
183
|
+
* If `lastMissileConfig` is provided, accounts for warmup bonus: similar
|
|
184
|
+
* missiles cast faster, so more damage can fit in the same budget.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Simulate a missile trajectory to find the minimum duration (ticks) needed
|
|
188
|
+
* to reach a target at the given distance. Works for all turnRate values:
|
|
189
|
+
* positive (homing) and zero (straight).
|
|
190
|
+
*
|
|
191
|
+
* The simulation starts the missile aimed directly at the target and steps
|
|
192
|
+
* through the trajectory tick by tick. For homing, the missile tracks the
|
|
193
|
+
* target each tick matching the engine's steering physics.
|
|
194
|
+
*/
|
|
195
|
+
/**
|
|
196
|
+
* Simulate a missile trajectory to find the minimum duration (ticks) needed
|
|
197
|
+
* to reach a target at the given distance. Works for straight (turnRate=0)
|
|
198
|
+
* and homing (turnRate>0) missiles.
|
|
199
|
+
*
|
|
200
|
+
* The missile starts aimed directly at the target at (dist, 0) and steps
|
|
201
|
+
* through the trajectory tick by tick. For homing, the missile tracks the
|
|
202
|
+
* target each tick matching the engine's steering physics.
|
|
203
|
+
*
|
|
204
|
+
* Returns 500 if the missile cannot reach the target within 500 ticks.
|
|
205
|
+
*/
|
|
206
|
+
export function simulateMinDuration(speed: number, turnRateDeg: number, dist: number, collisionRadius?: number): number
|
|
207
|
+
{
|
|
208
|
+
if (dist <= 0) return 1;
|
|
209
|
+
const hitR = collisionRadius ?? (WIZARD_RADIUS + 8);
|
|
210
|
+
if (turnRateDeg === 0)
|
|
211
|
+
{
|
|
212
|
+
return Math.max(1, Math.ceil((dist - hitR) / speed));
|
|
213
|
+
}
|
|
214
|
+
const maxTurn = Math.abs(turnRateDeg) * Math.PI / 180;
|
|
215
|
+
let angle = 0;
|
|
216
|
+
let x = 0;
|
|
217
|
+
let y = 0;
|
|
218
|
+
for (let t = 1; t <= 500; t++)
|
|
219
|
+
{
|
|
220
|
+
const targetAngle = Math.atan2(-y, dist - x);
|
|
221
|
+
let diff = targetAngle - angle;
|
|
222
|
+
while (diff > Math.PI) diff -= 2 * Math.PI;
|
|
223
|
+
while (diff < -Math.PI) diff += 2 * Math.PI;
|
|
224
|
+
const turn = Math.max(-maxTurn, Math.min(maxTurn, diff));
|
|
225
|
+
angle += turn;
|
|
226
|
+
x += Math.cos(angle) * speed;
|
|
227
|
+
y += Math.sin(angle) * speed;
|
|
228
|
+
const d = Math.sqrt((x - dist) * (x - dist) + y * y);
|
|
229
|
+
if (d <= hitR) return t;
|
|
230
|
+
}
|
|
231
|
+
return 500;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function fitMissileToBudget(
|
|
235
|
+
budgetTicks: number,
|
|
236
|
+
distance: number,
|
|
237
|
+
options?: {minTurnRate?: number; maxDamage?: number; lastMissileConfig?: MissileConfig},
|
|
238
|
+
): MissileConfig | null
|
|
239
|
+
{
|
|
240
|
+
|
|
241
|
+
const budgetSec = budgetTicks / TICKS_PER_SECOND;
|
|
242
|
+
const minTurnRate = options?.minTurnRate ?? 0;
|
|
243
|
+
const maxDamage = options?.maxDamage ?? WIZARD_HEALTH;
|
|
244
|
+
// Warmup is active when caller explicitly passes lastMissileConfig in options.
|
|
245
|
+
// Value can be undefined (first cast = full warmup) or MissileConfig (similarity-based).
|
|
246
|
+
// When key is absent (old callers), no warmup is applied.
|
|
247
|
+
const warmupActive = options !== undefined && 'lastMissileConfig' in options;
|
|
248
|
+
const lastMissile = options?.lastMissileConfig;
|
|
249
|
+
|
|
250
|
+
// Minimum useful cast time is 10 ticks (0.1s)
|
|
251
|
+
if (budgetSec < MISSILE_MIN_CAST_TIME)
|
|
252
|
+
{
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Templates: [speed, turnRate] combos to try, ordered by preference.
|
|
257
|
+
// Lower turnRate = cheaper cast. Higher speed = shorter duration needed.
|
|
258
|
+
const templates: [number, number][] = [
|
|
259
|
+
[7, 0], // Fast straight: cheapest cast, only hits stationary
|
|
260
|
+
[5, 0], // Medium straight
|
|
261
|
+
[6, 0.5], // Fast with light tracking
|
|
262
|
+
[5, 1], // Medium with tracking
|
|
263
|
+
[4, 1], // Slow with tracking (long range)
|
|
264
|
+
[4, 1.5], // Slow with strong tracking
|
|
265
|
+
[3, 2], // Very slow with heavy tracking (for aggressive homing)
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
let best: MissileConfig | null = null;
|
|
269
|
+
let bestDamage = 0;
|
|
270
|
+
|
|
271
|
+
for (const [speed, turnRate] of templates)
|
|
272
|
+
{
|
|
273
|
+
if (turnRate < minTurnRate) continue;
|
|
274
|
+
const minDurationTicks = Math.max(
|
|
275
|
+
MISSILE_MIN_DURATION,
|
|
276
|
+
simulateMinDuration(speed, turnRate, distance),
|
|
277
|
+
);
|
|
278
|
+
const durationSec = minDurationTicks / TICKS_PER_SECOND;
|
|
279
|
+
|
|
280
|
+
// Solve cast formula for damage:
|
|
281
|
+
// budgetSec = BASE + SCALE * d^POWER + delivery costs
|
|
282
|
+
// → d = ((budgetSec - fixedCost) / SCALE)^(1/POWER)
|
|
283
|
+
const turnCost = effectiveTurnRateCost(turnRate);
|
|
284
|
+
const fixedCost = MISSILE_BASE_CAST
|
|
285
|
+
+ MISSILE_HOMING_COEFF * turnCost
|
|
286
|
+
+ MISSILE_TURN_DURATION_COEFF * (turnCost * durationSec)
|
|
287
|
+
+ MISSILE_SPEED_DURATION_COEFF * (speed * durationSec - MISSILE_SPEED_DURATION_BASELINE);
|
|
288
|
+
|
|
289
|
+
// Estimate warmup multiplier for this template (depends on speed/turnRate/duration,
|
|
290
|
+
// not damage, so we can estimate before solving for damage)
|
|
291
|
+
let effectiveBudgetSec = budgetSec;
|
|
292
|
+
if (warmupActive && lastMissile)
|
|
293
|
+
{
|
|
294
|
+
const probe: MissileConfig = {damage: lastMissile.damage, speed, turnRate, duration: minDurationTicks};
|
|
295
|
+
const multiplier = calculateWarmupMultiplier(lastMissile, probe);
|
|
296
|
+
// multiplier < 1 = faster (more budget), > 1 = slower (less budget)
|
|
297
|
+
effectiveBudgetSec = budgetSec / multiplier;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const remaining = effectiveBudgetSec - fixedCost;
|
|
301
|
+
if (remaining <= 0)
|
|
302
|
+
{
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const maxDamageFloat = Math.pow(remaining / MISSILE_DAMAGE_SCALE, 1 / MISSILE_DAMAGE_POWER);
|
|
307
|
+
const damage = Math.min(maxDamage, Math.floor(maxDamageFloat));
|
|
308
|
+
|
|
309
|
+
if (damage < MISSILE_MIN_DAMAGE)
|
|
310
|
+
{
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Verify actual cast time with real warmup (using final damage)
|
|
315
|
+
const candidate: MissileConfig = {damage, speed, turnRate, duration: minDurationTicks};
|
|
316
|
+
const actualCastSec = (warmupActive && lastMissile)
|
|
317
|
+
? calculateMissileCastTime(candidate, lastMissile)
|
|
318
|
+
: calculateMissileCastTime(candidate);
|
|
319
|
+
const actualCastTicks = Math.max(1, Math.ceil(actualCastSec * TICKS_PER_SECOND));
|
|
320
|
+
|
|
321
|
+
if (actualCastTicks > budgetTicks)
|
|
322
|
+
{
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (damage > bestDamage)
|
|
327
|
+
{
|
|
328
|
+
bestDamage = damage;
|
|
329
|
+
best = candidate;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return best;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Fit a missile config that accounts for the enemy escaping during cast time.
|
|
338
|
+
*
|
|
339
|
+
* During casting, the caster moves at CASTING_MOVEMENT_MULT speed while
|
|
340
|
+
* the enemy moves at full MOVEMENT_SPEED. This means the effective distance
|
|
341
|
+
* at launch is larger than the current distance. This function iteratively
|
|
342
|
+
* converges on a missile config whose range covers the escape distance.
|
|
343
|
+
*
|
|
344
|
+
* @param currentDistance - Current distance to enemy
|
|
345
|
+
* @param budgetTicks - Maximum cast time budget in ticks
|
|
346
|
+
* @param options - Same options as fitMissileToBudget, plus:
|
|
347
|
+
* - enemyApproaching: if true, enemy is moving toward caster (reduces escape)
|
|
348
|
+
* - distanceBuffer: flat units added to target distance for safety margin (default 20)
|
|
349
|
+
* - maxIterations: convergence iterations (default 5)
|
|
350
|
+
*/
|
|
351
|
+
export function fitMissileForEscapingTarget(
|
|
352
|
+
currentDistance: number,
|
|
353
|
+
budgetTicks: number,
|
|
354
|
+
options?: {minTurnRate?: number; maxDamage?: number; lastMissileConfig?: MissileConfig; enemyApproaching?: boolean; distanceBuffer?: number; maxIterations?: number},
|
|
355
|
+
): MissileConfig | null
|
|
356
|
+
{
|
|
357
|
+
const maxIter = options?.maxIterations ?? 5;
|
|
358
|
+
const buffer = options?.distanceBuffer ?? 20;
|
|
359
|
+
const escapeRate = options?.enemyApproaching
|
|
360
|
+
? 0
|
|
361
|
+
: MOVEMENT_SPEED - MOVEMENT_SPEED * CASTING_MOVEMENT_MULT;
|
|
362
|
+
|
|
363
|
+
let targetDistance = currentDistance + buffer;
|
|
364
|
+
let lastCastTicks = 0;
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < maxIter; i++)
|
|
367
|
+
{
|
|
368
|
+
const config = fitMissileToBudget(budgetTicks, targetDistance, options);
|
|
369
|
+
if (!config) return null;
|
|
370
|
+
|
|
371
|
+
const castTicks = getMissileCastTime(config, options?.lastMissileConfig);
|
|
372
|
+
if (castTicks === lastCastTicks) return config;
|
|
373
|
+
lastCastTicks = castTicks;
|
|
374
|
+
|
|
375
|
+
targetDistance = currentDistance + castTicks * escapeRate + buffer;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return fitMissileToBudget(budgetTicks, targetDistance, options);
|
|
379
|
+
}
|