@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,286 +1,286 @@
|
|
|
1
|
-
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
-
import {angleTo} from '../../utils/angles.js';
|
|
3
|
-
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
-
import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
|
|
5
|
-
import {getMissileCastTime} from '../../utils/combat.js';
|
|
6
|
-
import {
|
|
7
|
-
getThreats,
|
|
8
|
-
getMostUrgentThreat,
|
|
9
|
-
getDodgeDirectionForMovement,
|
|
10
|
-
isSafeToAttack,
|
|
11
|
-
getBlinkToCenter,
|
|
12
|
-
shouldForceShield,
|
|
13
|
-
} from '../shared.js';
|
|
14
|
-
import {useParam} from '../../engine/params-runtime.js';
|
|
15
|
-
|
|
16
|
-
const WALL_BUFFER = 60;
|
|
17
|
-
|
|
18
|
-
const SHIELD_BUFFER = 3;
|
|
19
|
-
|
|
20
|
-
// Fixed homing missile: damage 12, speed 5, turnRate 1, duration 180
|
|
21
|
-
// Range: 5 × 180 = 900 units (covers arena)
|
|
22
|
-
const MISSILE_CONFIG = {damage: 12, speed: 5, turnRate: 1, duration: 180};
|
|
23
|
-
|
|
24
|
-
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
25
|
-
{
|
|
26
|
-
const enemy = worldState.enemies[0];
|
|
27
|
-
return {turnToward: enemy?.position};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function isNearWall(position: {x: number; y: number}): boolean
|
|
31
|
-
{
|
|
32
|
-
return (
|
|
33
|
-
position.x < WALL_BUFFER ||
|
|
34
|
-
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
35
|
-
position.y < WALL_BUFFER ||
|
|
36
|
-
position.y > ARENA_SIZE - WALL_BUFFER
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function smartStrafe(
|
|
41
|
-
position: {x: number; y: number},
|
|
42
|
-
enemy: {position: {x: number; y: number}},
|
|
43
|
-
dodgeDirection: {x: number; y: number} | null,
|
|
44
|
-
tick: number,
|
|
45
|
-
strafeCyclePeriod: number,
|
|
46
|
-
strafeIntensity: number,
|
|
47
|
-
): {x: number; y: number}
|
|
48
|
-
{
|
|
49
|
-
const deltaX = enemy.position.x - position.x;
|
|
50
|
-
const deltaY = enemy.position.y - position.y;
|
|
51
|
-
const normalizedDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
52
|
-
if (normalizedDistance === 0) return {x: 0, y: 0};
|
|
53
|
-
|
|
54
|
-
const strafeClockwise = {
|
|
55
|
-
x: -deltaY / normalizedDistance,
|
|
56
|
-
y: deltaX / normalizedDistance,
|
|
57
|
-
};
|
|
58
|
-
const strafeCounterClockwise = {
|
|
59
|
-
x: deltaY / normalizedDistance,
|
|
60
|
-
y: -deltaX / normalizedDistance,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
let strafeDir: {x: number; y: number};
|
|
64
|
-
|
|
65
|
-
if (dodgeDirection)
|
|
66
|
-
{
|
|
67
|
-
strafeDir = dodgeDirection;
|
|
68
|
-
}
|
|
69
|
-
else if (isNearWall(position))
|
|
70
|
-
{
|
|
71
|
-
const futureClockwise = {
|
|
72
|
-
x: position.x + strafeClockwise.x * 100,
|
|
73
|
-
y: position.y + strafeClockwise.y * 100,
|
|
74
|
-
};
|
|
75
|
-
const futureCounterClockwise = {
|
|
76
|
-
x: position.x + strafeCounterClockwise.x * 100,
|
|
77
|
-
y: position.y + strafeCounterClockwise.y * 100,
|
|
78
|
-
};
|
|
79
|
-
const scoreClockwise = Math.min(
|
|
80
|
-
futureClockwise.x,
|
|
81
|
-
ARENA_SIZE - futureClockwise.x,
|
|
82
|
-
futureClockwise.y,
|
|
83
|
-
ARENA_SIZE - futureClockwise.y,
|
|
84
|
-
);
|
|
85
|
-
const scoreCounterClockwise = Math.min(
|
|
86
|
-
futureCounterClockwise.x,
|
|
87
|
-
ARENA_SIZE - futureCounterClockwise.x,
|
|
88
|
-
futureCounterClockwise.y,
|
|
89
|
-
ARENA_SIZE - futureCounterClockwise.y,
|
|
90
|
-
);
|
|
91
|
-
strafeDir =
|
|
92
|
-
scoreClockwise > scoreCounterClockwise
|
|
93
|
-
? strafeClockwise
|
|
94
|
-
: strafeCounterClockwise;
|
|
95
|
-
}
|
|
96
|
-
else
|
|
97
|
-
{
|
|
98
|
-
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
99
|
-
strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const intensity = dodgeDirection ? 100 : strafeIntensity;
|
|
103
|
-
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Bot: Flamecaller
|
|
108
|
-
*
|
|
109
|
-
* BEHAVIOR: Long-range homing missile caster with fixed missile config.
|
|
110
|
-
* Maintains 350u distance, strafes to dodge, and fires standard homing
|
|
111
|
-
* missiles (d=10, s=5, t=1, dur=180). Shields undodgeable threats,
|
|
112
|
-
* emergency blinks. A straightforward ranged caster that trades
|
|
113
|
-
* consistency for adaptability.
|
|
114
|
-
*
|
|
115
|
-
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
116
|
-
* - Flamecaller (tier 1): Fixed homing missiles, basic strafe and defense
|
|
117
|
-
* - Pyromancer (tier 2): + adaptive fitting, cast canceling, smart fallbacks
|
|
118
|
-
* - Infernalist (tier 3): Future — overwhelming adaptive fire
|
|
119
|
-
*
|
|
120
|
-
* TIER: 1 (base)
|
|
121
|
-
*/
|
|
122
|
-
export const Flamecaller: WizardFunction = ({state}) =>
|
|
123
|
-
{
|
|
124
|
-
const targetDistance = useParam('targetDistance', 753, {
|
|
125
|
-
range: 150,
|
|
126
|
-
min: 0,
|
|
127
|
-
});
|
|
128
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod', 273, {
|
|
129
|
-
range: 75,
|
|
130
|
-
min: 80,
|
|
131
|
-
max: 300,
|
|
132
|
-
});
|
|
133
|
-
const strafeIntensity = useParam('strafeIntensity', 20, {
|
|
134
|
-
range: 40,
|
|
135
|
-
min: 20,
|
|
136
|
-
max: 100,
|
|
137
|
-
steps: 7,
|
|
138
|
-
});
|
|
139
|
-
const approachSpeed = useParam('approachSpeed', 40, {
|
|
140
|
-
range: 30,
|
|
141
|
-
min: 10,
|
|
142
|
-
max: 70,
|
|
143
|
-
steps: 7,
|
|
144
|
-
});
|
|
145
|
-
const distanceDeadZone = useParam('distanceDeadZone', 56, {
|
|
146
|
-
range: 30,
|
|
147
|
-
min: 5,
|
|
148
|
-
max: 80,
|
|
149
|
-
steps: 7,
|
|
150
|
-
});
|
|
151
|
-
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
152
|
-
range: 75,
|
|
153
|
-
min: 50,
|
|
154
|
-
max: 300,
|
|
155
|
-
});
|
|
156
|
-
const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
|
|
157
|
-
range: 25,
|
|
158
|
-
min: 10,
|
|
159
|
-
max: 80,
|
|
160
|
-
steps: 5,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const enemy = state.enemies[0];
|
|
164
|
-
if (!enemy)
|
|
165
|
-
{
|
|
166
|
-
return {move: {x: 0, y: 0}};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
170
|
-
|
|
171
|
-
const threats = getThreats(state);
|
|
172
|
-
const urgentThreat = getMostUrgentThreat(threats);
|
|
173
|
-
|
|
174
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
175
|
-
const strafe = smartStrafe(
|
|
176
|
-
state.position,
|
|
177
|
-
enemy,
|
|
178
|
-
dodgeDirection,
|
|
179
|
-
state.tick,
|
|
180
|
-
strafeCyclePeriod,
|
|
181
|
-
strafeIntensity,
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
// Distance management (retreat if too close, approach if too far)
|
|
185
|
-
let moveX = strafe.x;
|
|
186
|
-
let moveY = strafe.y;
|
|
187
|
-
|
|
188
|
-
if (!dodgeDirection)
|
|
189
|
-
{
|
|
190
|
-
const deltaX = enemy.position.x - state.position.x;
|
|
191
|
-
const deltaY = enemy.position.y - state.position.y;
|
|
192
|
-
const normalizedDistance = distance > 0 ? distance : 1;
|
|
193
|
-
|
|
194
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
195
|
-
{
|
|
196
|
-
moveX += (deltaX / normalizedDistance) * approachSpeed;
|
|
197
|
-
moveY += (deltaY / normalizedDistance) * approachSpeed;
|
|
198
|
-
}
|
|
199
|
-
else if (distance < targetDistance - distanceDeadZone)
|
|
200
|
-
{
|
|
201
|
-
moveX -= (deltaX / normalizedDistance) * approachSpeed;
|
|
202
|
-
moveY -= (deltaY / normalizedDistance) * approachSpeed;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
207
|
-
if (magnitude > 100)
|
|
208
|
-
{
|
|
209
|
-
moveX = (moveX / magnitude) * 100;
|
|
210
|
-
moveY = (moveY / magnitude) * 100;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const move = {x: moveX, y: moveY};
|
|
214
|
-
|
|
215
|
-
// === DEFENSE: Handle channeling ===
|
|
216
|
-
if (state.state === 'channeling')
|
|
217
|
-
{
|
|
218
|
-
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
219
|
-
{
|
|
220
|
-
return {move, cancel: true};
|
|
221
|
-
}
|
|
222
|
-
return {move: {x: 0, y: 0}};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// === DEFENSE: Let shield cast finish ===
|
|
226
|
-
if (state.state === 'casting' && state.castingSpell === 'shield')
|
|
227
|
-
{
|
|
228
|
-
return {move: {x: 0, y: 0}};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// === DEFENSE: Shield undodgeable or high-damage homing threats ===
|
|
232
|
-
if (
|
|
233
|
-
state.state === 'idle' &&
|
|
234
|
-
urgentThreat &&
|
|
235
|
-
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
236
|
-
)
|
|
237
|
-
{
|
|
238
|
-
if (urgentThreat.canBlockInTime)
|
|
239
|
-
{
|
|
240
|
-
if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
241
|
-
{
|
|
242
|
-
return {
|
|
243
|
-
move: {x: 0, y: 0},
|
|
244
|
-
startCast: {spell: 'shield'},
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (state.blinkCooldown === 0)
|
|
249
|
-
{
|
|
250
|
-
return {
|
|
251
|
-
move: {x: 0, y: 0},
|
|
252
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// === OFFENSE: Fire fixed homing missile when safe ===
|
|
258
|
-
if (state.state === 'idle')
|
|
259
|
-
{
|
|
260
|
-
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
261
|
-
const castTime = getMissileCastTime(
|
|
262
|
-
MISSILE_CONFIG,
|
|
263
|
-
state.lastMissileConfig,
|
|
264
|
-
);
|
|
265
|
-
const safeWindow =
|
|
266
|
-
castTime + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
nextThreatTime > safeWindow &&
|
|
270
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
271
|
-
)
|
|
272
|
-
{
|
|
273
|
-
return {
|
|
274
|
-
move,
|
|
275
|
-
startCast: {
|
|
276
|
-
spell: 'missile',
|
|
277
|
-
config: MISSILE_CONFIG,
|
|
278
|
-
missileAI: HomingAI,
|
|
279
|
-
direction: angleTo(state.position, enemy.position),
|
|
280
|
-
},
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return {move};
|
|
286
|
-
};
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
+
import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
|
|
5
|
+
import {getMissileCastTime} from '../../utils/combat.js';
|
|
6
|
+
import {
|
|
7
|
+
getThreats,
|
|
8
|
+
getMostUrgentThreat,
|
|
9
|
+
getDodgeDirectionForMovement,
|
|
10
|
+
isSafeToAttack,
|
|
11
|
+
getBlinkToCenter,
|
|
12
|
+
shouldForceShield,
|
|
13
|
+
} from '../shared.js';
|
|
14
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
15
|
+
|
|
16
|
+
const WALL_BUFFER = 60;
|
|
17
|
+
|
|
18
|
+
const SHIELD_BUFFER = 3;
|
|
19
|
+
|
|
20
|
+
// Fixed homing missile: damage 12, speed 5, turnRate 1, duration 180
|
|
21
|
+
// Range: 5 × 180 = 900 units (covers arena)
|
|
22
|
+
const MISSILE_CONFIG = {damage: 12, speed: 5, turnRate: 1, duration: 180};
|
|
23
|
+
|
|
24
|
+
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
25
|
+
{
|
|
26
|
+
const enemy = worldState.enemies[0];
|
|
27
|
+
return {turnToward: enemy?.position};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isNearWall(position: {x: number; y: number}): boolean
|
|
31
|
+
{
|
|
32
|
+
return (
|
|
33
|
+
position.x < WALL_BUFFER ||
|
|
34
|
+
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
35
|
+
position.y < WALL_BUFFER ||
|
|
36
|
+
position.y > ARENA_SIZE - WALL_BUFFER
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function smartStrafe(
|
|
41
|
+
position: {x: number; y: number},
|
|
42
|
+
enemy: {position: {x: number; y: number}},
|
|
43
|
+
dodgeDirection: {x: number; y: number} | null,
|
|
44
|
+
tick: number,
|
|
45
|
+
strafeCyclePeriod: number,
|
|
46
|
+
strafeIntensity: number,
|
|
47
|
+
): {x: number; y: number}
|
|
48
|
+
{
|
|
49
|
+
const deltaX = enemy.position.x - position.x;
|
|
50
|
+
const deltaY = enemy.position.y - position.y;
|
|
51
|
+
const normalizedDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
52
|
+
if (normalizedDistance === 0) return {x: 0, y: 0};
|
|
53
|
+
|
|
54
|
+
const strafeClockwise = {
|
|
55
|
+
x: -deltaY / normalizedDistance,
|
|
56
|
+
y: deltaX / normalizedDistance,
|
|
57
|
+
};
|
|
58
|
+
const strafeCounterClockwise = {
|
|
59
|
+
x: deltaY / normalizedDistance,
|
|
60
|
+
y: -deltaX / normalizedDistance,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let strafeDir: {x: number; y: number};
|
|
64
|
+
|
|
65
|
+
if (dodgeDirection)
|
|
66
|
+
{
|
|
67
|
+
strafeDir = dodgeDirection;
|
|
68
|
+
}
|
|
69
|
+
else if (isNearWall(position))
|
|
70
|
+
{
|
|
71
|
+
const futureClockwise = {
|
|
72
|
+
x: position.x + strafeClockwise.x * 100,
|
|
73
|
+
y: position.y + strafeClockwise.y * 100,
|
|
74
|
+
};
|
|
75
|
+
const futureCounterClockwise = {
|
|
76
|
+
x: position.x + strafeCounterClockwise.x * 100,
|
|
77
|
+
y: position.y + strafeCounterClockwise.y * 100,
|
|
78
|
+
};
|
|
79
|
+
const scoreClockwise = Math.min(
|
|
80
|
+
futureClockwise.x,
|
|
81
|
+
ARENA_SIZE - futureClockwise.x,
|
|
82
|
+
futureClockwise.y,
|
|
83
|
+
ARENA_SIZE - futureClockwise.y,
|
|
84
|
+
);
|
|
85
|
+
const scoreCounterClockwise = Math.min(
|
|
86
|
+
futureCounterClockwise.x,
|
|
87
|
+
ARENA_SIZE - futureCounterClockwise.x,
|
|
88
|
+
futureCounterClockwise.y,
|
|
89
|
+
ARENA_SIZE - futureCounterClockwise.y,
|
|
90
|
+
);
|
|
91
|
+
strafeDir =
|
|
92
|
+
scoreClockwise > scoreCounterClockwise
|
|
93
|
+
? strafeClockwise
|
|
94
|
+
: strafeCounterClockwise;
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
{
|
|
98
|
+
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
99
|
+
strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const intensity = dodgeDirection ? 100 : strafeIntensity;
|
|
103
|
+
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Bot: Flamecaller
|
|
108
|
+
*
|
|
109
|
+
* BEHAVIOR: Long-range homing missile caster with fixed missile config.
|
|
110
|
+
* Maintains 350u distance, strafes to dodge, and fires standard homing
|
|
111
|
+
* missiles (d=10, s=5, t=1, dur=180). Shields undodgeable threats,
|
|
112
|
+
* emergency blinks. A straightforward ranged caster that trades
|
|
113
|
+
* consistency for adaptability.
|
|
114
|
+
*
|
|
115
|
+
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
116
|
+
* - Flamecaller (tier 1): Fixed homing missiles, basic strafe and defense
|
|
117
|
+
* - Pyromancer (tier 2): + adaptive fitting, cast canceling, smart fallbacks
|
|
118
|
+
* - Infernalist (tier 3): Future — overwhelming adaptive fire
|
|
119
|
+
*
|
|
120
|
+
* TIER: 1 (base)
|
|
121
|
+
*/
|
|
122
|
+
export const Flamecaller: WizardFunction = ({state}) =>
|
|
123
|
+
{
|
|
124
|
+
const targetDistance = useParam('targetDistance', 753, {
|
|
125
|
+
range: 150,
|
|
126
|
+
min: 0,
|
|
127
|
+
});
|
|
128
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 273, {
|
|
129
|
+
range: 75,
|
|
130
|
+
min: 80,
|
|
131
|
+
max: 300,
|
|
132
|
+
});
|
|
133
|
+
const strafeIntensity = useParam('strafeIntensity', 20, {
|
|
134
|
+
range: 40,
|
|
135
|
+
min: 20,
|
|
136
|
+
max: 100,
|
|
137
|
+
steps: 7,
|
|
138
|
+
});
|
|
139
|
+
const approachSpeed = useParam('approachSpeed', 40, {
|
|
140
|
+
range: 30,
|
|
141
|
+
min: 10,
|
|
142
|
+
max: 70,
|
|
143
|
+
steps: 7,
|
|
144
|
+
});
|
|
145
|
+
const distanceDeadZone = useParam('distanceDeadZone', 56, {
|
|
146
|
+
range: 30,
|
|
147
|
+
min: 5,
|
|
148
|
+
max: 80,
|
|
149
|
+
steps: 7,
|
|
150
|
+
});
|
|
151
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
152
|
+
range: 75,
|
|
153
|
+
min: 50,
|
|
154
|
+
max: 300,
|
|
155
|
+
});
|
|
156
|
+
const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
|
|
157
|
+
range: 25,
|
|
158
|
+
min: 10,
|
|
159
|
+
max: 80,
|
|
160
|
+
steps: 5,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const enemy = state.enemies[0];
|
|
164
|
+
if (!enemy)
|
|
165
|
+
{
|
|
166
|
+
return {move: {x: 0, y: 0}};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
170
|
+
|
|
171
|
+
const threats = getThreats(state);
|
|
172
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
173
|
+
|
|
174
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
175
|
+
const strafe = smartStrafe(
|
|
176
|
+
state.position,
|
|
177
|
+
enemy,
|
|
178
|
+
dodgeDirection,
|
|
179
|
+
state.tick,
|
|
180
|
+
strafeCyclePeriod,
|
|
181
|
+
strafeIntensity,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Distance management (retreat if too close, approach if too far)
|
|
185
|
+
let moveX = strafe.x;
|
|
186
|
+
let moveY = strafe.y;
|
|
187
|
+
|
|
188
|
+
if (!dodgeDirection)
|
|
189
|
+
{
|
|
190
|
+
const deltaX = enemy.position.x - state.position.x;
|
|
191
|
+
const deltaY = enemy.position.y - state.position.y;
|
|
192
|
+
const normalizedDistance = distance > 0 ? distance : 1;
|
|
193
|
+
|
|
194
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
195
|
+
{
|
|
196
|
+
moveX += (deltaX / normalizedDistance) * approachSpeed;
|
|
197
|
+
moveY += (deltaY / normalizedDistance) * approachSpeed;
|
|
198
|
+
}
|
|
199
|
+
else if (distance < targetDistance - distanceDeadZone)
|
|
200
|
+
{
|
|
201
|
+
moveX -= (deltaX / normalizedDistance) * approachSpeed;
|
|
202
|
+
moveY -= (deltaY / normalizedDistance) * approachSpeed;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
207
|
+
if (magnitude > 100)
|
|
208
|
+
{
|
|
209
|
+
moveX = (moveX / magnitude) * 100;
|
|
210
|
+
moveY = (moveY / magnitude) * 100;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const move = {x: moveX, y: moveY};
|
|
214
|
+
|
|
215
|
+
// === DEFENSE: Handle channeling ===
|
|
216
|
+
if (state.state === 'channeling')
|
|
217
|
+
{
|
|
218
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
219
|
+
{
|
|
220
|
+
return {move, cancel: true};
|
|
221
|
+
}
|
|
222
|
+
return {move: {x: 0, y: 0}};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// === DEFENSE: Let shield cast finish ===
|
|
226
|
+
if (state.state === 'casting' && state.castingSpell === 'shield')
|
|
227
|
+
{
|
|
228
|
+
return {move: {x: 0, y: 0}};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// === DEFENSE: Shield undodgeable or high-damage homing threats ===
|
|
232
|
+
if (
|
|
233
|
+
state.state === 'idle' &&
|
|
234
|
+
urgentThreat &&
|
|
235
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
236
|
+
)
|
|
237
|
+
{
|
|
238
|
+
if (urgentThreat.canBlockInTime)
|
|
239
|
+
{
|
|
240
|
+
if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
241
|
+
{
|
|
242
|
+
return {
|
|
243
|
+
move: {x: 0, y: 0},
|
|
244
|
+
startCast: {spell: 'shield'},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (state.blinkCooldown === 0)
|
|
249
|
+
{
|
|
250
|
+
return {
|
|
251
|
+
move: {x: 0, y: 0},
|
|
252
|
+
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// === OFFENSE: Fire fixed homing missile when safe ===
|
|
258
|
+
if (state.state === 'idle')
|
|
259
|
+
{
|
|
260
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
261
|
+
const castTime = getMissileCastTime(
|
|
262
|
+
MISSILE_CONFIG,
|
|
263
|
+
state.lastMissileConfig,
|
|
264
|
+
);
|
|
265
|
+
const safeWindow =
|
|
266
|
+
castTime + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
nextThreatTime > safeWindow &&
|
|
270
|
+
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
271
|
+
)
|
|
272
|
+
{
|
|
273
|
+
return {
|
|
274
|
+
move,
|
|
275
|
+
startCast: {
|
|
276
|
+
spell: 'missile',
|
|
277
|
+
config: MISSILE_CONFIG,
|
|
278
|
+
missileAI: HomingAI,
|
|
279
|
+
direction: angleTo(state.position, enemy.position),
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {move};
|
|
286
|
+
};
|