@vibemancer/core 0.1.0 → 0.1.1
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/README.md +28 -28
- package/dist/{chunk-L7Z7OFXD.js → chunk-7VDJHO22.js} +3 -3
- package/dist/chunk-7VDJHO22.js.map +1 -0
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +79 -78
- package/src/bots/berserker/01_Stormchaser.ts +457 -457
- package/src/bots/berserker/02_Stormcaller.ts +417 -417
- package/src/bots/berserker/03_Stormforger.ts +481 -481
- package/src/bots/caster/01_Flamecaller.ts +286 -286
- package/src/bots/caster/02_Pyromancer.ts +350 -350
- package/src/bots/caster/03_Infernalist.ts +492 -492
- package/src/bots/defensive/01_Turtle.ts +151 -151
- package/src/bots/defensive/02_Sentinel.ts +134 -134
- package/src/bots/defensive/03_Golem.ts +357 -357
- package/src/bots/duelist/01_Battlemage.ts +433 -433
- package/src/bots/duelist/02_Warmage.ts +438 -438
- package/src/bots/duelist/03_Archmage.ts +588 -588
- package/src/bots/homing/01_Bonemancer.ts +67 -67
- package/src/bots/homing/02_Lich.ts +356 -356
- package/src/bots/homing/03_Archlich.ts +220 -220
- package/src/bots/kiter/01_Spellspinner.ts +398 -398
- package/src/bots/kiter/02_Spellweaver.ts +378 -378
- package/src/bots/kiter/03_Spellbinder.ts +448 -448
- package/src/bots/melee/01_Shadowblade.ts +270 -270
- package/src/bots/melee/02_Nightblade.ts +437 -437
- package/src/bots/melee/03_Voidblade.ts +582 -582
- package/src/bots/sniper/01_Spellshot.ts +385 -385
- package/src/bots/sniper/02_Spelltracer.ts +441 -441
- package/src/bots/sniper/03_Spellseeker.ts +546 -546
- package/src/bots/standalone/Critter.ts +89 -89
- package/src/bots/standalone/Doombringer.ts +91 -91
- package/src/bots/standalone/Hogger.ts +228 -228
- package/src/bots/standalone/Rookie.ts +50 -50
- package/src/bots/standalone/TargetDummy.ts +21 -21
- package/src/bots/test/cheater.ts +405 -405
- package/src/bots/test/crasher.ts +81 -81
- package/src/engine/hooks-runtime.ts +394 -394
- package/src/engine/manual-match.ts +289 -289
- package/src/engine/missile-templates.ts +155 -155
- package/src/engine/physics.ts +143 -143
- package/src/engine/simulation.ts +828 -828
- package/src/engine/spells.ts +128 -128
- package/src/engine-version.ts +1 -1
- package/src/index.ts +23 -23
- package/src/rules.ts +254 -254
- package/src/types.ts +193 -193
- package/src/utils/index.ts +6 -6
- package/dist/chunk-L7Z7OFXD.js.map +0 -1
|
@@ -1,357 +1,357 @@
|
|
|
1
|
-
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
-
import {distanceTo} from '../../utils/distance.js';
|
|
3
|
-
import {angleTo} from '../../utils/angles.js';
|
|
4
|
-
import {
|
|
5
|
-
GCD_DURATION,
|
|
6
|
-
SHIELD_CAST_TIME,
|
|
7
|
-
BLINK_CAST_TIME,
|
|
8
|
-
} from '../../rules.js';
|
|
9
|
-
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
10
|
-
import {
|
|
11
|
-
getThreats,
|
|
12
|
-
getMostUrgentThreat,
|
|
13
|
-
getBlinkToCenter,
|
|
14
|
-
fitMissileToBudget,
|
|
15
|
-
isSafeToAttack,
|
|
16
|
-
MAX_CAST_BUDGET,
|
|
17
|
-
MIN_USEFUL_DAMAGE,
|
|
18
|
-
getEnemyVulnerabilityTicks,
|
|
19
|
-
} from '../shared.js';
|
|
20
|
-
import {useParam} from '../../engine/params-runtime.js';
|
|
21
|
-
|
|
22
|
-
const SHIELD_BUFFER = 3;
|
|
23
|
-
const SHIELD_HOLD_TICKS = 5;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Standard homing missile AI — tracks enemy position.
|
|
27
|
-
*/
|
|
28
|
-
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
29
|
-
{
|
|
30
|
-
const enemy = worldState.enemies[0];
|
|
31
|
-
return {turnToward: enemy?.position};
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Straight missile AI — no homing, flies in initial direction.
|
|
36
|
-
*/
|
|
37
|
-
const StraightAI: MissileAIFunction = () => ({});
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Bot: Golem
|
|
41
|
-
*
|
|
42
|
-
* BEHAVIOR: Stationary fortress with perfect shield timing and devastating
|
|
43
|
-
* counterattacks during enemy vulnerability windows. Reads enemy cast/GCD
|
|
44
|
-
* state to time punish missiles that land when the enemy can't shield.
|
|
45
|
-
* Handles multi-missile volleys by holding shield through consecutive impacts.
|
|
46
|
-
* Uses progressive cast-cancel thresholds for optimal damage trading.
|
|
47
|
-
*
|
|
48
|
-
* KEY IMPROVEMENTS OVER SENTINEL:
|
|
49
|
-
* - Counterattack punish: fires during enemy cast/GCD recovery
|
|
50
|
-
* - Multi-threat volley awareness: holds shield through consecutive hits
|
|
51
|
-
* - Progressive cast-cancel: graduated damage thresholds
|
|
52
|
-
* - Perfect shield timing: uses ticksToStartShield precisely
|
|
53
|
-
*
|
|
54
|
-
* PROGRESSION LINE: Turtle → Sentinel → Golem
|
|
55
|
-
* TIER: 3 (elite Defensive line)
|
|
56
|
-
*/
|
|
57
|
-
export const Golem: WizardFunction = ({state}) =>
|
|
58
|
-
{
|
|
59
|
-
// Positive turn rate ensures homing missiles that hit moving targets.
|
|
60
|
-
// Hardcoded — optimizer incorrectly pushes this negative for stationary matchups.
|
|
61
|
-
const minTurnRate = 0.5;
|
|
62
|
-
const preemptiveShieldRange = useParam('preemptiveShieldRange', 150, {
|
|
63
|
-
range: 85,
|
|
64
|
-
min: 0,
|
|
65
|
-
steps: 7,
|
|
66
|
-
});
|
|
67
|
-
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
68
|
-
range: 75,
|
|
69
|
-
min: 10,
|
|
70
|
-
});
|
|
71
|
-
const punishMinVulnerability = useParam('punishMinVulnerability', 310, {
|
|
72
|
-
range: 30,
|
|
73
|
-
min: 10,
|
|
74
|
-
steps: 5,
|
|
75
|
-
});
|
|
76
|
-
const preferBlinkDodge =
|
|
77
|
-
useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
78
|
-
const cancelHeavyDmg = useParam('cancelHeavyDmg', 14, {
|
|
79
|
-
range: 15,
|
|
80
|
-
min: 5,
|
|
81
|
-
max: 40,
|
|
82
|
-
steps: 5,
|
|
83
|
-
});
|
|
84
|
-
const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
|
|
85
|
-
range: 10,
|
|
86
|
-
min: 3,
|
|
87
|
-
max: 25,
|
|
88
|
-
steps: 5,
|
|
89
|
-
});
|
|
90
|
-
const cancelCastRatio = useParam('cancelCastRatio', 0.8, {
|
|
91
|
-
range: 0.4,
|
|
92
|
-
min: 0.3,
|
|
93
|
-
max: 1.0,
|
|
94
|
-
steps: 5,
|
|
95
|
-
});
|
|
96
|
-
const cancelLightDmg = useParam('cancelLightDmg', 8, {
|
|
97
|
-
range: 5,
|
|
98
|
-
min: 1,
|
|
99
|
-
max: 15,
|
|
100
|
-
steps: 5,
|
|
101
|
-
});
|
|
102
|
-
const cancelEarlyCastRatio = useParam('cancelEarlyCastRatio', 0.7, {
|
|
103
|
-
range: 0.3,
|
|
104
|
-
min: 0.0,
|
|
105
|
-
max: 0.7,
|
|
106
|
-
steps: 5,
|
|
107
|
-
});
|
|
108
|
-
const blinkWindowMax = useParam('blinkWindowMax', 50, {
|
|
109
|
-
range: 40,
|
|
110
|
-
min: 20,
|
|
111
|
-
max: 120,
|
|
112
|
-
steps: 7,
|
|
113
|
-
});
|
|
114
|
-
const preemptiveShieldBuffer = useParam('preemptiveShieldBuffer', 11.3, {
|
|
115
|
-
range: 5,
|
|
116
|
-
min: 0,
|
|
117
|
-
max: 15,
|
|
118
|
-
steps: 5,
|
|
119
|
-
});
|
|
120
|
-
const flightTimeDivisor = useParam('flightTimeDivisor', 9.5, {
|
|
121
|
-
range: 4,
|
|
122
|
-
min: 2,
|
|
123
|
-
max: 12,
|
|
124
|
-
steps: 5,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const enemy = state.enemies[0];
|
|
128
|
-
const threats = getThreats(state);
|
|
129
|
-
const urgentThreat = getMostUrgentThreat(threats);
|
|
130
|
-
const hittingThreats = threats.filter((t) => t.willHit);
|
|
131
|
-
|
|
132
|
-
// === STEP 1: CHANNELING — hold through volleys, cancel when safe ===
|
|
133
|
-
if (state.state === 'channeling' && state.channelingSpell === 'shield')
|
|
134
|
-
{
|
|
135
|
-
// Hold if any missile is about to hit
|
|
136
|
-
const imminentThreat = hittingThreats.find(
|
|
137
|
-
(t) => t.ticksToImpact <= SHIELD_HOLD_TICKS,
|
|
138
|
-
);
|
|
139
|
-
if (imminentThreat)
|
|
140
|
-
{
|
|
141
|
-
return {move: {x: 0, y: 0}};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Hold if next threat will arrive before we can reshield
|
|
145
|
-
const nextHitting = hittingThreats[0];
|
|
146
|
-
const ticksToReshield = SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
147
|
-
if (nextHitting && nextHitting.ticksToImpact <= ticksToReshield)
|
|
148
|
-
{
|
|
149
|
-
return {move: {x: 0, y: 0}};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Cancel shield if no threat or threat is far enough
|
|
153
|
-
if (!nextHitting || nextHitting.ticksToImpact > shieldCancelThreshold)
|
|
154
|
-
{
|
|
155
|
-
return {move: {x: 0, y: 0}, cancel: true};
|
|
156
|
-
}
|
|
157
|
-
return {move: {x: 0, y: 0}};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// If casting shield, let it finish
|
|
161
|
-
if (state.state === 'casting' && state.castingSpell === 'shield')
|
|
162
|
-
{
|
|
163
|
-
return {move: {x: 0, y: 0}};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// === STEP 2: PROGRESSIVE CAST-CANCEL — evaluate damage tradeoff ===
|
|
167
|
-
if (
|
|
168
|
-
state.state === 'casting' &&
|
|
169
|
-
state.castingSpell === 'missile' &&
|
|
170
|
-
urgentThreat
|
|
171
|
-
)
|
|
172
|
-
{
|
|
173
|
-
// Sentinel is stationary — can't dodge. Must always consider canceling.
|
|
174
|
-
const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
|
|
175
|
-
const defenseTime =
|
|
176
|
-
state.blinkCooldown === 0
|
|
177
|
-
? BLINK_CAST_TIME
|
|
178
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
179
|
-
const willArriveInWindow =
|
|
180
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
181
|
-
|
|
182
|
-
if (willArriveInWindow)
|
|
183
|
-
{
|
|
184
|
-
const incomingDamage = urgentThreat.projectile.damage;
|
|
185
|
-
const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
|
|
186
|
-
|
|
187
|
-
// Always cancel for lethal or heavy damage
|
|
188
|
-
if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
|
|
189
|
-
{
|
|
190
|
-
return {move: {x: 0, y: 0}, cancel: true};
|
|
191
|
-
}
|
|
192
|
-
// Significant damage + early/mid cast: cancel
|
|
193
|
-
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
194
|
-
{
|
|
195
|
-
return {move: {x: 0, y: 0}, cancel: true};
|
|
196
|
-
}
|
|
197
|
-
// Medium damage + very early cast: cancel
|
|
198
|
-
if (
|
|
199
|
-
incomingDamage >= cancelLightDmg &&
|
|
200
|
-
castRatio < cancelEarlyCastRatio
|
|
201
|
-
)
|
|
202
|
-
{
|
|
203
|
-
return {move: {x: 0, y: 0}, cancel: true};
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
|
|
209
|
-
if (
|
|
210
|
-
preferBlinkDodge &&
|
|
211
|
-
state.state === 'idle' &&
|
|
212
|
-
urgentThreat &&
|
|
213
|
-
state.blinkCooldown === 0 &&
|
|
214
|
-
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
215
|
-
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
216
|
-
)
|
|
217
|
-
{
|
|
218
|
-
return {
|
|
219
|
-
move: {x: 0, y: 0},
|
|
220
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// === STEP 3b: SHIELD — fallback when blink unavailable (90% block) ===
|
|
225
|
-
if (
|
|
226
|
-
state.state === 'idle' &&
|
|
227
|
-
urgentThreat &&
|
|
228
|
-
urgentThreat.canBlockInTime &&
|
|
229
|
-
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
230
|
-
)
|
|
231
|
-
{
|
|
232
|
-
return {
|
|
233
|
-
move: {x: 0, y: 0},
|
|
234
|
-
startCast: {spell: 'shield'},
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// === STEP 4: PREEMPTIVE SHIELD — enemy casting at close range ===
|
|
239
|
-
if (
|
|
240
|
-
state.state === 'idle' &&
|
|
241
|
-
enemy &&
|
|
242
|
-
enemy.state === 'casting' &&
|
|
243
|
-
enemy.castingSpell === 'missile'
|
|
244
|
-
)
|
|
245
|
-
{
|
|
246
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
247
|
-
if (distance < preemptiveShieldRange)
|
|
248
|
-
{
|
|
249
|
-
const remainingCast =
|
|
250
|
-
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
251
|
-
const estimatedFlightTicks = distance / flightTimeDivisor;
|
|
252
|
-
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
253
|
-
if (
|
|
254
|
-
ticksUntilHit <
|
|
255
|
-
SHIELD_CAST_TIME + SHIELD_BUFFER + preemptiveShieldBuffer
|
|
256
|
-
)
|
|
257
|
-
{
|
|
258
|
-
return {
|
|
259
|
-
move: {x: 0, y: 0},
|
|
260
|
-
startCast: {spell: 'shield'},
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// === STEP 5: OFFENSE — punish mode + standard counterattack ===
|
|
267
|
-
if (state.state === 'idle' && enemy)
|
|
268
|
-
{
|
|
269
|
-
const offenseDefenseTime =
|
|
270
|
-
state.blinkCooldown === 0
|
|
271
|
-
? BLINK_CAST_TIME
|
|
272
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
273
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
274
|
-
const enemyHP = Math.ceil(enemy.health);
|
|
275
|
-
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
276
|
-
|
|
277
|
-
// --- PUNISH MODE: exploit enemy cast/GCD vulnerability ---
|
|
278
|
-
if (vulnerabilityTicks >= punishMinVulnerability)
|
|
279
|
-
{
|
|
280
|
-
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
281
|
-
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
282
|
-
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
283
|
-
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
284
|
-
const effectiveBudget = Math.min(
|
|
285
|
-
punishBudget,
|
|
286
|
-
safeBudget,
|
|
287
|
-
MAX_CAST_BUDGET,
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
if (effectiveBudget > 0)
|
|
291
|
-
{
|
|
292
|
-
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
293
|
-
minTurnRate: 0,
|
|
294
|
-
maxDamage: enemyHP,
|
|
295
|
-
lastMissileConfig: state.lastMissileConfig,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
299
|
-
{
|
|
300
|
-
const castTime = getMissileCastTime(config, state.lastMissileConfig);
|
|
301
|
-
const actualFlight = distance / config.speed;
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
castTime + actualFlight < vulnerabilityTicks &&
|
|
305
|
-
isSafeToAttack(threats, castTime)
|
|
306
|
-
)
|
|
307
|
-
{
|
|
308
|
-
const aimTarget =
|
|
309
|
-
config.turnRate > 0
|
|
310
|
-
? enemy.position
|
|
311
|
-
: getLeadPosition(
|
|
312
|
-
enemy.position,
|
|
313
|
-
enemy.velocity,
|
|
314
|
-
config.speed,
|
|
315
|
-
state.position,
|
|
316
|
-
);
|
|
317
|
-
return {
|
|
318
|
-
move: {x: 0, y: 0},
|
|
319
|
-
startCast: {
|
|
320
|
-
spell: 'missile',
|
|
321
|
-
config,
|
|
322
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
323
|
-
direction: angleTo(state.position, aimTarget),
|
|
324
|
-
},
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// --- STANDARD MODE: adaptive homing in safe windows ---
|
|
332
|
-
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
333
|
-
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
334
|
-
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
335
|
-
|
|
336
|
-
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
337
|
-
minTurnRate,
|
|
338
|
-
maxDamage: enemyHP,
|
|
339
|
-
lastMissileConfig: state.lastMissileConfig,
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
if (config && config.damage >= Math.min(MIN_USEFUL_DAMAGE, enemyHP))
|
|
343
|
-
{
|
|
344
|
-
return {
|
|
345
|
-
move: {x: 0, y: 0},
|
|
346
|
-
startCast: {
|
|
347
|
-
spell: 'missile',
|
|
348
|
-
config,
|
|
349
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
350
|
-
direction: angleTo(state.position, enemy.position),
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return {move: {x: 0, y: 0}};
|
|
357
|
-
};
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
3
|
+
import {angleTo} from '../../utils/angles.js';
|
|
4
|
+
import {
|
|
5
|
+
GCD_DURATION,
|
|
6
|
+
SHIELD_CAST_TIME,
|
|
7
|
+
BLINK_CAST_TIME,
|
|
8
|
+
} from '../../rules.js';
|
|
9
|
+
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
10
|
+
import {
|
|
11
|
+
getThreats,
|
|
12
|
+
getMostUrgentThreat,
|
|
13
|
+
getBlinkToCenter,
|
|
14
|
+
fitMissileToBudget,
|
|
15
|
+
isSafeToAttack,
|
|
16
|
+
MAX_CAST_BUDGET,
|
|
17
|
+
MIN_USEFUL_DAMAGE,
|
|
18
|
+
getEnemyVulnerabilityTicks,
|
|
19
|
+
} from '../shared.js';
|
|
20
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
21
|
+
|
|
22
|
+
const SHIELD_BUFFER = 3;
|
|
23
|
+
const SHIELD_HOLD_TICKS = 5;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard homing missile AI — tracks enemy position.
|
|
27
|
+
*/
|
|
28
|
+
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
29
|
+
{
|
|
30
|
+
const enemy = worldState.enemies[0];
|
|
31
|
+
return {turnToward: enemy?.position};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Straight missile AI — no homing, flies in initial direction.
|
|
36
|
+
*/
|
|
37
|
+
const StraightAI: MissileAIFunction = () => ({});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bot: Golem
|
|
41
|
+
*
|
|
42
|
+
* BEHAVIOR: Stationary fortress with perfect shield timing and devastating
|
|
43
|
+
* counterattacks during enemy vulnerability windows. Reads enemy cast/GCD
|
|
44
|
+
* state to time punish missiles that land when the enemy can't shield.
|
|
45
|
+
* Handles multi-missile volleys by holding shield through consecutive impacts.
|
|
46
|
+
* Uses progressive cast-cancel thresholds for optimal damage trading.
|
|
47
|
+
*
|
|
48
|
+
* KEY IMPROVEMENTS OVER SENTINEL:
|
|
49
|
+
* - Counterattack punish: fires during enemy cast/GCD recovery
|
|
50
|
+
* - Multi-threat volley awareness: holds shield through consecutive hits
|
|
51
|
+
* - Progressive cast-cancel: graduated damage thresholds
|
|
52
|
+
* - Perfect shield timing: uses ticksToStartShield precisely
|
|
53
|
+
*
|
|
54
|
+
* PROGRESSION LINE: Turtle → Sentinel → Golem
|
|
55
|
+
* TIER: 3 (elite Defensive line)
|
|
56
|
+
*/
|
|
57
|
+
export const Golem: WizardFunction = ({state}) =>
|
|
58
|
+
{
|
|
59
|
+
// Positive turn rate ensures homing missiles that hit moving targets.
|
|
60
|
+
// Hardcoded — optimizer incorrectly pushes this negative for stationary matchups.
|
|
61
|
+
const minTurnRate = 0.5;
|
|
62
|
+
const preemptiveShieldRange = useParam('preemptiveShieldRange', 150, {
|
|
63
|
+
range: 85,
|
|
64
|
+
min: 0,
|
|
65
|
+
steps: 7,
|
|
66
|
+
});
|
|
67
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
68
|
+
range: 75,
|
|
69
|
+
min: 10,
|
|
70
|
+
});
|
|
71
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 310, {
|
|
72
|
+
range: 30,
|
|
73
|
+
min: 10,
|
|
74
|
+
steps: 5,
|
|
75
|
+
});
|
|
76
|
+
const preferBlinkDodge =
|
|
77
|
+
useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
78
|
+
const cancelHeavyDmg = useParam('cancelHeavyDmg', 14, {
|
|
79
|
+
range: 15,
|
|
80
|
+
min: 5,
|
|
81
|
+
max: 40,
|
|
82
|
+
steps: 5,
|
|
83
|
+
});
|
|
84
|
+
const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
|
|
85
|
+
range: 10,
|
|
86
|
+
min: 3,
|
|
87
|
+
max: 25,
|
|
88
|
+
steps: 5,
|
|
89
|
+
});
|
|
90
|
+
const cancelCastRatio = useParam('cancelCastRatio', 0.8, {
|
|
91
|
+
range: 0.4,
|
|
92
|
+
min: 0.3,
|
|
93
|
+
max: 1.0,
|
|
94
|
+
steps: 5,
|
|
95
|
+
});
|
|
96
|
+
const cancelLightDmg = useParam('cancelLightDmg', 8, {
|
|
97
|
+
range: 5,
|
|
98
|
+
min: 1,
|
|
99
|
+
max: 15,
|
|
100
|
+
steps: 5,
|
|
101
|
+
});
|
|
102
|
+
const cancelEarlyCastRatio = useParam('cancelEarlyCastRatio', 0.7, {
|
|
103
|
+
range: 0.3,
|
|
104
|
+
min: 0.0,
|
|
105
|
+
max: 0.7,
|
|
106
|
+
steps: 5,
|
|
107
|
+
});
|
|
108
|
+
const blinkWindowMax = useParam('blinkWindowMax', 50, {
|
|
109
|
+
range: 40,
|
|
110
|
+
min: 20,
|
|
111
|
+
max: 120,
|
|
112
|
+
steps: 7,
|
|
113
|
+
});
|
|
114
|
+
const preemptiveShieldBuffer = useParam('preemptiveShieldBuffer', 11.3, {
|
|
115
|
+
range: 5,
|
|
116
|
+
min: 0,
|
|
117
|
+
max: 15,
|
|
118
|
+
steps: 5,
|
|
119
|
+
});
|
|
120
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 9.5, {
|
|
121
|
+
range: 4,
|
|
122
|
+
min: 2,
|
|
123
|
+
max: 12,
|
|
124
|
+
steps: 5,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const enemy = state.enemies[0];
|
|
128
|
+
const threats = getThreats(state);
|
|
129
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
130
|
+
const hittingThreats = threats.filter((t) => t.willHit);
|
|
131
|
+
|
|
132
|
+
// === STEP 1: CHANNELING — hold through volleys, cancel when safe ===
|
|
133
|
+
if (state.state === 'channeling' && state.channelingSpell === 'shield')
|
|
134
|
+
{
|
|
135
|
+
// Hold if any missile is about to hit
|
|
136
|
+
const imminentThreat = hittingThreats.find(
|
|
137
|
+
(t) => t.ticksToImpact <= SHIELD_HOLD_TICKS,
|
|
138
|
+
);
|
|
139
|
+
if (imminentThreat)
|
|
140
|
+
{
|
|
141
|
+
return {move: {x: 0, y: 0}};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Hold if next threat will arrive before we can reshield
|
|
145
|
+
const nextHitting = hittingThreats[0];
|
|
146
|
+
const ticksToReshield = SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
147
|
+
if (nextHitting && nextHitting.ticksToImpact <= ticksToReshield)
|
|
148
|
+
{
|
|
149
|
+
return {move: {x: 0, y: 0}};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Cancel shield if no threat or threat is far enough
|
|
153
|
+
if (!nextHitting || nextHitting.ticksToImpact > shieldCancelThreshold)
|
|
154
|
+
{
|
|
155
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
156
|
+
}
|
|
157
|
+
return {move: {x: 0, y: 0}};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If casting shield, let it finish
|
|
161
|
+
if (state.state === 'casting' && state.castingSpell === 'shield')
|
|
162
|
+
{
|
|
163
|
+
return {move: {x: 0, y: 0}};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// === STEP 2: PROGRESSIVE CAST-CANCEL — evaluate damage tradeoff ===
|
|
167
|
+
if (
|
|
168
|
+
state.state === 'casting' &&
|
|
169
|
+
state.castingSpell === 'missile' &&
|
|
170
|
+
urgentThreat
|
|
171
|
+
)
|
|
172
|
+
{
|
|
173
|
+
// Sentinel is stationary — can't dodge. Must always consider canceling.
|
|
174
|
+
const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
|
|
175
|
+
const defenseTime =
|
|
176
|
+
state.blinkCooldown === 0
|
|
177
|
+
? BLINK_CAST_TIME
|
|
178
|
+
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
179
|
+
const willArriveInWindow =
|
|
180
|
+
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
181
|
+
|
|
182
|
+
if (willArriveInWindow)
|
|
183
|
+
{
|
|
184
|
+
const incomingDamage = urgentThreat.projectile.damage;
|
|
185
|
+
const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
|
|
186
|
+
|
|
187
|
+
// Always cancel for lethal or heavy damage
|
|
188
|
+
if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
|
|
189
|
+
{
|
|
190
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
191
|
+
}
|
|
192
|
+
// Significant damage + early/mid cast: cancel
|
|
193
|
+
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
194
|
+
{
|
|
195
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
196
|
+
}
|
|
197
|
+
// Medium damage + very early cast: cancel
|
|
198
|
+
if (
|
|
199
|
+
incomingDamage >= cancelLightDmg &&
|
|
200
|
+
castRatio < cancelEarlyCastRatio
|
|
201
|
+
)
|
|
202
|
+
{
|
|
203
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
|
|
209
|
+
if (
|
|
210
|
+
preferBlinkDodge &&
|
|
211
|
+
state.state === 'idle' &&
|
|
212
|
+
urgentThreat &&
|
|
213
|
+
state.blinkCooldown === 0 &&
|
|
214
|
+
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
215
|
+
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
216
|
+
)
|
|
217
|
+
{
|
|
218
|
+
return {
|
|
219
|
+
move: {x: 0, y: 0},
|
|
220
|
+
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// === STEP 3b: SHIELD — fallback when blink unavailable (90% block) ===
|
|
225
|
+
if (
|
|
226
|
+
state.state === 'idle' &&
|
|
227
|
+
urgentThreat &&
|
|
228
|
+
urgentThreat.canBlockInTime &&
|
|
229
|
+
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
230
|
+
)
|
|
231
|
+
{
|
|
232
|
+
return {
|
|
233
|
+
move: {x: 0, y: 0},
|
|
234
|
+
startCast: {spell: 'shield'},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// === STEP 4: PREEMPTIVE SHIELD — enemy casting at close range ===
|
|
239
|
+
if (
|
|
240
|
+
state.state === 'idle' &&
|
|
241
|
+
enemy &&
|
|
242
|
+
enemy.state === 'casting' &&
|
|
243
|
+
enemy.castingSpell === 'missile'
|
|
244
|
+
)
|
|
245
|
+
{
|
|
246
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
247
|
+
if (distance < preemptiveShieldRange)
|
|
248
|
+
{
|
|
249
|
+
const remainingCast =
|
|
250
|
+
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
251
|
+
const estimatedFlightTicks = distance / flightTimeDivisor;
|
|
252
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
253
|
+
if (
|
|
254
|
+
ticksUntilHit <
|
|
255
|
+
SHIELD_CAST_TIME + SHIELD_BUFFER + preemptiveShieldBuffer
|
|
256
|
+
)
|
|
257
|
+
{
|
|
258
|
+
return {
|
|
259
|
+
move: {x: 0, y: 0},
|
|
260
|
+
startCast: {spell: 'shield'},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// === STEP 5: OFFENSE — punish mode + standard counterattack ===
|
|
267
|
+
if (state.state === 'idle' && enemy)
|
|
268
|
+
{
|
|
269
|
+
const offenseDefenseTime =
|
|
270
|
+
state.blinkCooldown === 0
|
|
271
|
+
? BLINK_CAST_TIME
|
|
272
|
+
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
273
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
274
|
+
const enemyHP = Math.ceil(enemy.health);
|
|
275
|
+
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
276
|
+
|
|
277
|
+
// --- PUNISH MODE: exploit enemy cast/GCD vulnerability ---
|
|
278
|
+
if (vulnerabilityTicks >= punishMinVulnerability)
|
|
279
|
+
{
|
|
280
|
+
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
281
|
+
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
282
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
283
|
+
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
284
|
+
const effectiveBudget = Math.min(
|
|
285
|
+
punishBudget,
|
|
286
|
+
safeBudget,
|
|
287
|
+
MAX_CAST_BUDGET,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (effectiveBudget > 0)
|
|
291
|
+
{
|
|
292
|
+
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
293
|
+
minTurnRate: 0,
|
|
294
|
+
maxDamage: enemyHP,
|
|
295
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
299
|
+
{
|
|
300
|
+
const castTime = getMissileCastTime(config, state.lastMissileConfig);
|
|
301
|
+
const actualFlight = distance / config.speed;
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
castTime + actualFlight < vulnerabilityTicks &&
|
|
305
|
+
isSafeToAttack(threats, castTime)
|
|
306
|
+
)
|
|
307
|
+
{
|
|
308
|
+
const aimTarget =
|
|
309
|
+
config.turnRate > 0
|
|
310
|
+
? enemy.position
|
|
311
|
+
: getLeadPosition(
|
|
312
|
+
enemy.position,
|
|
313
|
+
enemy.velocity,
|
|
314
|
+
config.speed,
|
|
315
|
+
state.position,
|
|
316
|
+
);
|
|
317
|
+
return {
|
|
318
|
+
move: {x: 0, y: 0},
|
|
319
|
+
startCast: {
|
|
320
|
+
spell: 'missile',
|
|
321
|
+
config,
|
|
322
|
+
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
323
|
+
direction: angleTo(state.position, aimTarget),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- STANDARD MODE: adaptive homing in safe windows ---
|
|
332
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
333
|
+
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
334
|
+
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
335
|
+
|
|
336
|
+
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
337
|
+
minTurnRate,
|
|
338
|
+
maxDamage: enemyHP,
|
|
339
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (config && config.damage >= Math.min(MIN_USEFUL_DAMAGE, enemyHP))
|
|
343
|
+
{
|
|
344
|
+
return {
|
|
345
|
+
move: {x: 0, y: 0},
|
|
346
|
+
startCast: {
|
|
347
|
+
spell: 'missile',
|
|
348
|
+
config,
|
|
349
|
+
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
350
|
+
direction: angleTo(state.position, enemy.position),
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {move: {x: 0, y: 0}};
|
|
357
|
+
};
|