@vibemancer/core 0.1.3 → 0.1.5

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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-W7HWJFQ4.js +10599 -0
  3. package/dist/chunk-W7HWJFQ4.js.map +1 -0
  4. package/dist/index-browser.d.ts +1300 -1051
  5. package/dist/index-browser.js +55 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +200 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/bots/Hero.ts +867 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  12. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  13. package/src/bots/berserker/03_Stormforger.ts +162 -129
  14. package/src/bots/caster/01_Flamecaller.ts +126 -84
  15. package/src/bots/caster/02_Pyromancer.ts +148 -101
  16. package/src/bots/caster/03_Infernalist.ts +180 -160
  17. package/src/bots/defensive/01_Turtle.ts +48 -44
  18. package/src/bots/defensive/02_Sentinel.ts +57 -53
  19. package/src/bots/defensive/03_Golem.ts +85 -97
  20. package/src/bots/duelist/01_Battlemage.ts +157 -122
  21. package/src/bots/duelist/02_Warmage.ts +166 -121
  22. package/src/bots/duelist/03_Archmage.ts +198 -178
  23. package/src/bots/homing/01_Bonemancer.ts +20 -32
  24. package/src/bots/homing/02_Lich.ts +145 -93
  25. package/src/bots/homing/03_Archlich.ts +129 -60
  26. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  27. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  28. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  29. package/src/bots/melee/01_Shadowblade.ts +131 -75
  30. package/src/bots/melee/02_Nightblade.ts +174 -130
  31. package/src/bots/melee/03_Voidblade.ts +266 -168
  32. package/src/bots/registry.ts +44 -37
  33. package/src/bots/shared.ts +185 -25
  34. package/src/bots/sniper/01_Spellshot.ts +151 -98
  35. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  36. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  37. package/src/bots/standalone/Critter.ts +46 -52
  38. package/src/bots/standalone/Doombringer.ts +36 -40
  39. package/src/bots/standalone/Hogger.ts +120 -89
  40. package/src/bots/standalone/Rookie.ts +21 -23
  41. package/src/bots/standalone/TargetDummy.ts +5 -6
  42. package/src/bots/test/cheater.ts +91 -85
  43. package/src/bots/test/crasher.ts +26 -28
  44. package/src/engine/bundle-fight.ts +242 -0
  45. package/src/engine/hooks-runtime.ts +58 -18
  46. package/src/engine/manual-match.ts +33 -25
  47. package/src/engine/missile-templates.ts +25 -43
  48. package/src/engine/optimizer.ts +7 -7
  49. package/src/engine/params-runtime.ts +3 -3
  50. package/src/engine/physics.ts +33 -23
  51. package/src/engine/sandbox-browser.ts +8 -7
  52. package/src/engine/sandbox-compile.ts +1 -1
  53. package/src/engine/sandbox-harness.ts +299 -367
  54. package/src/engine/sandbox.ts +3 -3
  55. package/src/engine/simulation.ts +291 -76
  56. package/src/engine/spells.ts +9 -12
  57. package/src/engine-version.ts +1 -1
  58. package/src/hooks/action-builders.ts +76 -21
  59. package/src/hooks/index.ts +17 -9
  60. package/src/hooks/state-hooks.ts +61 -25
  61. package/src/hooks/threat-analysis.ts +44 -20
  62. package/src/hooks/types.ts +62 -5
  63. package/src/index.ts +2 -0
  64. package/src/rules.ts +209 -63
  65. package/src/stats.ts +2 -2
  66. package/src/testing.ts +6 -30
  67. package/src/trace.ts +63 -3
  68. package/src/types.ts +127 -14
  69. package/src/utils/angles.ts +1 -0
  70. package/src/utils/combat.ts +98 -6
  71. package/src/utils/spatial.ts +3 -3
  72. package/dist/chunk-B7YYYBEC.js +0 -9140
  73. package/dist/chunk-B7YYYBEC.js.map +0 -1
  74. package/src/hooks/bot-wrapper.ts +0 -84
@@ -1,5 +1,7 @@
1
- import {WizardFunction} from '../../types.js';
2
- import {ARENA_WIDTH, ARENA_HEIGHT} from '../../rules.js';
1
+ import type {FinalAction} from '../../hooks/index.js';
2
+ import {useStatus, useBlinkCooldown, useRandom, usePosition, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
3
+ import {avoidLava} from '../shared.js';
4
+ import {ARENA_MIN, ARENA_MAX} from '../../rules.js';
3
5
 
4
6
  /**
5
7
  * Bot: Critter
@@ -19,71 +21,63 @@ import {ARENA_WIDTH, ARENA_HEIGHT} from '../../rules.js';
19
21
  * "Hogger" bot could be an elite critter: same chaotic spirit but actually dangerous
20
22
  * (like the famous WoW elite that wipes unprepared lowbies).
21
23
  */
22
- export const Critter: WizardFunction = ({state, random}) =>
24
+ export function Critter(): FinalAction
23
25
  {
24
- // Use the engine-provided random function (seeded, isolated, deterministic)
25
- // World-space movement: random x/y in [-100, 100] range
26
- const move = {
27
- x: random() * 200 - 100, // -100 to 100
28
- y: random() * 200 - 100, // -100 to 100
29
- };
26
+ const status = useStatus();
27
+ const blinkCooldown = useBlinkCooldown();
28
+ const random = useRandom();
29
+ const position = usePosition();
30
30
 
31
- if (state.state === 'idle' && random() > 0.95)
31
+ const rawMove = {x: random() * 2 - 1, y: random() * 2 - 1};
32
+ const safe = avoidLava(position, rawMove);
33
+ const mx = safe.x;
34
+ const my = safe.y;
35
+
36
+ if (status === 'idle' && random() > 0.95)
32
37
  {
33
38
  const spellRoll = random();
34
- if (spellRoll > 0.6)
39
+ if (spellRoll > 0.6)
35
40
  {
36
- return {
37
- move,
38
- startCast: {
39
- spell: 'missile',
40
- config: {
41
- damage: Math.floor(random() * 20) + 5,
42
- speed: random() * 5 + 2,
43
- turnRate: random() * 3,
44
- duration: Math.floor(random() * 200) + 100,
45
- },
46
- // Missile AI gets its own random from engine - use it for random targeting
47
- missileAI: ({random: missileRandom}) => ({
48
- turnToward:
49
- missileRandom() > 0.5
50
- ? {
51
- x: missileRandom() * ARENA_WIDTH,
52
- y: missileRandom() * ARENA_HEIGHT,
53
- }
54
- : undefined,
55
- }),
56
- direction: random() * 360,
41
+ return missile(
42
+ {
43
+ damage: Math.floor(random() * 20) + 5,
44
+ speed: random() * 5 + 2,
45
+ turnRate: random() * 3,
46
+ duration: Math.floor(random() * 200) + 100,
47
+ },
48
+ // Missile AI gets its own random from engine - use it for random targeting
49
+ () =>
50
+ {
51
+ const ctx = getMissileContext();
52
+ if (ctx.random() > 0.5)
53
+ {
54
+ return turnToward(ARENA_MIN + ctx.random() * (ARENA_MAX - ARENA_MIN), ARENA_MIN + ctx.random() * (ARENA_MAX - ARENA_MIN));
55
+ }
56
+ return flyStraight();
57
57
  },
58
- };
58
+ random() * 360,
59
+ ).move(mx, my);
59
60
  }
60
- else if (spellRoll > 0.3)
61
+ else if (spellRoll > 0.3)
61
62
  {
62
- return {
63
- move,
64
- startCast: {spell: 'shield'},
65
- };
63
+ return shield().move(mx, my);
66
64
  }
67
- else if (state.blinkCooldown === 0)
65
+ else if (blinkCooldown === 0)
68
66
  {
69
- return {
70
- move,
71
- startCast: {
72
- spell: 'blink',
73
- target: {x: random() * ARENA_WIDTH, y: random() * ARENA_HEIGHT},
74
- },
75
- };
67
+ const margin = 50;
68
+ return blink(ARENA_MIN + margin + random() * (ARENA_MAX - ARENA_MIN - margin * 2), ARENA_MIN + margin + random() * (ARENA_MAX - ARENA_MIN - margin * 2));
76
69
  }
77
70
  }
78
71
 
79
72
  if (
80
- (state.state === 'casting' || state.state === 'channeling') &&
73
+ (status === 'casting' || status === 'channeling') &&
81
74
  random() > 0.98
82
- )
75
+ )
83
76
  {
84
- return {move, cancel: true};
77
+ return cancel().move(mx, my);
85
78
  }
86
79
 
87
- // Override auto-aim with random direction — Critter doesn't know where anything is
88
- return {move, aimDirection: random() * 360};
89
- };
80
+ // Critter doesn't know where anything is — just move randomly
81
+ // Note: aimDirection override is not supported by hooks API, so we just move randomly
82
+ return moveAction(mx, my);
83
+ }
@@ -1,17 +1,18 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
1
+ import type {FinalAction, MissileFunction} from '../../hooks/index.js';
2
+ import {usePosition, useEnemy, useStatus, useBlinkCooldown, useThreats, idle, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
2
3
  import {angleTo} from '../../utils/angles.js';
3
4
  import {distanceTo} from '../../utils/distance.js';
4
5
  import {fitMissileToBudget} from '../../utils/combat.js';
5
6
  import {
6
- getThreats,
7
7
  getMostUrgentThreat,
8
8
  getBlinkToCenter,
9
9
  } from '../shared.js';
10
10
 
11
- const HomingAI: MissileAIFunction = ({worldState}) =>
11
+ const HomingAI: MissileFunction = () =>
12
12
  {
13
- const enemy = worldState.enemies[0];
14
- return {turnToward: enemy?.position};
13
+ const ctx = getMissileContext();
14
+ const enemy = ctx.worldState.enemies[0];
15
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
15
16
  };
16
17
 
17
18
  /**
@@ -24,68 +25,63 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
24
25
  *
25
26
  * STANDALONE — no tier progression. Benchmark/test bot.
26
27
  */
27
- export const Doombringer: WizardFunction = ({state}) =>
28
+ export function Doombringer(): FinalAction
28
29
  {
29
- const enemy = state.enemies[0];
30
- if (!enemy)
30
+ const position = usePosition();
31
+ const enemy = useEnemy();
32
+ const status = useStatus();
33
+ const blinkCooldown = useBlinkCooldown();
34
+ const threats = useThreats();
35
+
36
+ if (enemy.health <= 0)
31
37
  {
32
- return {move: {x: 0, y: 0}};
38
+ return idle();
33
39
  }
34
40
 
35
- const distance = distanceTo(state.position, enemy.position);
36
- const threats = getThreats(state);
41
+ const distance = distanceTo(position, enemy.position);
37
42
  const urgentThreat = getMostUrgentThreat(threats);
38
43
 
39
44
  // === DEFENSE: Handle channeling ===
40
- if (state.state === 'channeling')
45
+ if (status === 'channeling')
41
46
  {
42
- if (!urgentThreat || urgentThreat.ticksToImpact > 150)
47
+ if (!urgentThreat || urgentThreat.ticksToImpact > 150)
43
48
  {
44
- return {move: {x: 0, y: 0}, cancel: true};
49
+ return cancel();
45
50
  }
46
- return {move: {x: 0, y: 0}};
51
+ return idle();
47
52
  }
48
53
 
49
54
  // === DEFENSE: Shield undodgeable threats ===
50
55
  if (
51
- state.state === 'idle' &&
56
+ status === 'idle' &&
52
57
  urgentThreat &&
53
58
  !urgentThreat.bestDodgeDirection
54
- )
59
+ )
55
60
  {
56
- if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
61
+ if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
57
62
  {
58
- return {
59
- move: {x: 0, y: 0},
60
- startCast: {spell: 'shield'},
61
- };
63
+ return shield();
62
64
  }
63
- else if (!urgentThreat.canBlockInTime && state.blinkCooldown === 0)
65
+ else if (!urgentThreat.canBlockInTime && blinkCooldown === 0)
64
66
  {
65
- return {
66
- move: {x: 0, y: 0},
67
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
68
- };
67
+ const t = getBlinkToCenter(position);
68
+ return blink(t.x, t.y);
69
69
  }
70
70
  }
71
71
 
72
72
  // === OFFENSE: Fire max-damage homing missile (no damage cap) ===
73
- if (state.state === 'idle')
73
+ if (status === 'idle')
74
74
  {
75
75
  const config = fitMissileToBudget(Infinity, distance, {minTurnRate: 0.5});
76
- if (config)
76
+ if (config)
77
77
  {
78
- return {
79
- move: {x: 0, y: 0},
80
- startCast: {
81
- spell: 'missile',
82
- config,
83
- missileAI: HomingAI,
84
- direction: angleTo(state.position, enemy.position),
85
- },
86
- };
78
+ return missile(
79
+ config,
80
+ HomingAI,
81
+ angleTo(position, enemy.position),
82
+ ).move(0, 0);
87
83
  }
88
84
  }
89
85
 
90
- return {move: {x: 0, y: 0}};
91
- };
86
+ return idle();
87
+ }
@@ -1,28 +1,31 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
2
- import {angleTo} from '../../utils/angles.js';
1
+ import type {FinalAction, MissileFunction} from '../../hooks/index.js';
2
+ import {usePosition, useEnemy, useStatus, useBlinkCooldown, useRandom, useThreats, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
3
3
  import {distanceTo} from '../../utils/distance.js';
4
- import {ARENA_SIZE} from '../../rules.js';
4
+ import {angleTo} from '../../utils/angles.js';
5
+ import {ARENA_MIN, ARENA_MAX} from '../../rules.js';
5
6
  import {
6
- getThreats,
7
7
  getMostUrgentThreat,
8
8
  getDodgeDirectionForMovement,
9
9
  getBlinkToCenter,
10
+ avoidLava,
10
11
  } from '../shared.js';
12
+ import {useParam} from '../../engine/params-runtime.js';
11
13
  const WALL_BUFFER = 60;
12
- const WALL_PUSH = 30; // How hard to push away from walls
14
+ const WALL_PUSH = 0.3; // How hard to push away from walls
13
15
 
14
16
  /**
15
17
  * Homing AI that sometimes forgets what it's doing.
16
18
  */
17
- const ChaoticHoming: MissileAIFunction = ({worldState, random}) =>
19
+ const ChaoticHoming: MissileFunction = () =>
18
20
  {
19
- const enemy = worldState.enemies[0];
21
+ const ctx = getMissileContext();
22
+ const enemy = ctx.worldState.enemies[0];
20
23
  // 80% chance to actually track, 20% chance to just fly straight
21
- if (random() < 0.8)
24
+ if (ctx.random() < 0.8)
22
25
  {
23
- return {turnToward: enemy?.position};
26
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
24
27
  }
25
- return {};
28
+ return flyStraight();
26
29
  };
27
30
 
28
31
  /**
@@ -45,140 +48,173 @@ const ChaoticHoming: MissileAIFunction = ({worldState, random}) =>
45
48
  *
46
49
  * STANDALONE — no tier progression. There's only one Hogger.
47
50
  */
48
- export const Hogger: WizardFunction = ({state, random}) =>
51
+ export function Hogger(): FinalAction
49
52
  {
50
- const enemy = state.enemies[0];
51
- if (!enemy)
53
+ const position = usePosition();
54
+ const enemy = useEnemy();
55
+ const status = useStatus();
56
+ const blinkCooldown = useBlinkCooldown();
57
+ const random = useRandom();
58
+ const threats = useThreats();
59
+
60
+
61
+ const centerBias = useParam('centerBias', 0.71, {
62
+ range: 0.4,
63
+ min: 0,
64
+ max: 0.8,
65
+ steps: 5,
66
+ });
67
+ const maxBlinkDistance = useParam('maxBlinkDistance', 224, {
68
+ range: 100,
69
+ min: 50,
70
+ max: 300,
71
+ steps: 7,
72
+ });
73
+
74
+ function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
52
75
  {
53
- return {move: {x: 0, y: 0}};
76
+ const dx = target.x - position.x;
77
+ const dy = target.y - position.y;
78
+ const dist = Math.sqrt(dx * dx + dy * dy);
79
+ if (dist <= maxBlinkDistance) return target;
80
+ const scale = maxBlinkDistance / dist;
81
+ return {x: position.x + dx * scale, y: position.y + dy * scale};
54
82
  }
55
83
 
56
- const distance = distanceTo(state.position, enemy.position);
57
- const threats = getThreats(state);
84
+ if (enemy.health <= 0)
85
+ {
86
+ return idle();
87
+ }
88
+
89
+ const distance = distanceTo(position, enemy.position);
58
90
  const urgentThreat = getMostUrgentThreat(threats);
59
91
 
60
92
  // Erratic movement: mostly toward/away from enemy with random strafe
61
- const deltaX = enemy.position.x - state.position.x;
62
- const deltaY = enemy.position.y - state.position.y;
93
+ const deltaX = enemy.position.x - position.x;
94
+ const deltaY = enemy.position.y - position.y;
63
95
  const normalizedDistance = distance > 0 ? distance : 1;
64
96
 
65
97
  // Dodge if there's a real threat
66
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
98
+ const dodgeDirection = getDodgeDirectionForMovement(threats, position);
67
99
 
68
100
  let moveX: number;
69
101
  let moveY: number;
70
102
 
71
- if (dodgeDirection)
103
+ if (dodgeDirection)
72
104
  {
73
105
  // Actually dodge (Hogger isn't stupid)
74
- moveX = dodgeDirection.x * 100;
75
- moveY = dodgeDirection.y * 100;
106
+ moveX = dodgeDirection.x;
107
+ moveY = dodgeDirection.y;
76
108
  }
77
- else
109
+ else
78
110
  {
79
111
  // Random strafe component
80
112
  const perpX = -deltaY / normalizedDistance;
81
113
  const perpY = deltaX / normalizedDistance;
82
- const strafeIntensity = random() * 80 - 40; // -40 to 40
114
+ const strafeIntensity = random() * 0.8 - 0.4; // -0.4 to 0.4
83
115
 
84
116
  // Approach/retreat: prefer medium range (250-400), but sometimes just wander
85
- let approachIntensity = 0;
86
- if (distance > 400)
117
+ let approachIntensity: number;
118
+ if (distance > 400)
87
119
  {
88
- approachIntensity = 40 + random() * 30; // approach
120
+ approachIntensity = 0.4 + random() * 0.3; // approach
89
121
  }
90
- else if (distance < 200)
122
+ else if (distance < 200)
91
123
  {
92
- approachIntensity = -(30 + random() * 30); // retreat
124
+ approachIntensity = -(0.3 + random() * 0.3); // retreat
93
125
  }
94
- else
126
+ else
95
127
  {
96
- approachIntensity = random() * 60 - 30; // random walk
128
+ approachIntensity = random() * 0.6 - 0.3; // random walk
97
129
  }
98
130
 
131
+ // Approach the center-side of the enemy so knockback pushes toward center, not lava.
132
+ let approachX = deltaX / normalizedDistance;
133
+ let approachY = deltaY / normalizedDistance;
134
+ if (approachIntensity > 0)
135
+ {
136
+ const center = (ARENA_MIN + ARENA_MAX) / 2;
137
+ const toCenterX = center - enemy.position.x;
138
+ const toCenterY = center - enemy.position.y;
139
+ const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
140
+ const adX = approachX + (toCenterX / toCenterDist) * centerBias;
141
+ const adY = approachY + (toCenterY / toCenterDist) * centerBias;
142
+ const adMag = Math.sqrt(adX * adX + adY * adY) || 1;
143
+ approachX = adX / adMag;
144
+ approachY = adY / adMag;
145
+ }
99
146
  moveX =
100
147
  perpX * strafeIntensity +
101
- (deltaX / normalizedDistance) * approachIntensity;
148
+ approachX * approachIntensity;
102
149
  moveY =
103
150
  perpY * strafeIntensity +
104
- (deltaY / normalizedDistance) * approachIntensity;
151
+ approachY * approachIntensity;
105
152
 
106
153
  // Wall avoidance (Hogger doesn't like corners)
107
- if (state.position.x < WALL_BUFFER) moveX += WALL_PUSH;
108
- if (state.position.x > ARENA_SIZE - WALL_BUFFER) moveX -= WALL_PUSH;
109
- if (state.position.y < WALL_BUFFER) moveY += WALL_PUSH;
110
- if (state.position.y > ARENA_SIZE - WALL_BUFFER) moveY -= WALL_PUSH;
154
+ if (position.x < ARENA_MIN + WALL_BUFFER) moveX += WALL_PUSH;
155
+ if (position.x > ARENA_MAX - WALL_BUFFER) moveX -= WALL_PUSH;
156
+ if (position.y < ARENA_MIN + WALL_BUFFER) moveY += WALL_PUSH;
157
+ if (position.y > ARENA_MAX - WALL_BUFFER) moveY -= WALL_PUSH;
111
158
  }
112
159
 
113
160
  const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
114
- if (magnitude > 100)
161
+ if (magnitude > 1)
115
162
  {
116
- moveX = (moveX / magnitude) * 100;
117
- moveY = (moveY / magnitude) * 100;
163
+ moveX = moveX / magnitude;
164
+ moveY = moveY / magnitude;
118
165
  }
119
166
 
120
167
  const move = {x: moveX, y: moveY};
168
+ const safeMove = avoidLava(position, move);
121
169
 
122
170
  // === DEFENSE: Shield if in real danger ===
123
- if (state.state === 'channeling')
171
+ if (status === 'channeling')
124
172
  {
125
- if (!urgentThreat || urgentThreat.ticksToImpact > 120)
173
+ if (!urgentThreat || urgentThreat.ticksToImpact > 120)
126
174
  {
127
- return {move, cancel: true};
175
+ return cancel().move(safeMove.x, safeMove.y);
128
176
  }
129
- return {move: {x: 0, y: 0}};
177
+ return idle();
130
178
  }
131
179
 
132
180
  if (
133
- state.state === 'idle' &&
181
+ status === 'idle' &&
134
182
  urgentThreat &&
135
183
  !urgentThreat.bestDodgeDirection
136
- )
184
+ )
137
185
  {
138
- if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
186
+ if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
139
187
  {
140
- return {
141
- move: {x: 0, y: 0},
142
- startCast: {spell: 'shield'},
143
- };
188
+ return shield();
144
189
  }
145
- else if (!urgentThreat.canBlockInTime && state.blinkCooldown === 0)
190
+ else if (!urgentThreat.canBlockInTime && blinkCooldown === 0)
146
191
  {
147
- return {
148
- move: {x: 0, y: 0},
149
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
150
- };
192
+ const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
193
+ return blink(blinkTarget.x, blinkTarget.y);
151
194
  }
152
195
  }
153
196
 
154
197
  // === OFFENSE: Random blink (sometimes offensive, sometimes random) ===
155
- if (state.state === 'idle' && state.blinkCooldown === 0 && random() < 0.03)
198
+ if (status === 'idle' && blinkCooldown === 0 && random() < 0.03)
156
199
  {
157
200
  // 60% chance blink toward enemy, 40% chance random blink
158
- if (random() < 0.6 && distance > 300)
201
+ if (random() < 0.6 && distance > 300)
159
202
  {
160
- return {
161
- move: {x: 0, y: 0},
162
- startCast: {spell: 'blink', target: enemy.position},
163
- };
203
+ const capped = capBlinkDistance(enemy.position);
204
+ return blink(capped.x, capped.y);
164
205
  }
165
- else
206
+ else
166
207
  {
167
- return {
168
- move: {x: 0, y: 0},
169
- startCast: {
170
- spell: 'blink',
171
- target: {
172
- x: 100 + random() * 600,
173
- y: 100 + random() * 600,
174
- },
175
- },
176
- };
208
+ const capped = capBlinkDistance({
209
+ x: 100 + random() * 600,
210
+ y: 100 + random() * 600,
211
+ });
212
+ return blink(capped.x, capped.y);
177
213
  }
178
214
  }
179
215
 
180
216
  // === OFFENSE: Chaotic missiles ===
181
- if (state.state === 'idle' && distance < 500)
217
+ if (status === 'idle' && distance < 500)
182
218
  {
183
219
  // Random missile config each time
184
220
  const roll = random();
@@ -187,7 +223,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
187
223
  let turnRate: number;
188
224
  let duration: number;
189
225
 
190
- if (roll < 0.3)
226
+ if (roll < 0.3)
191
227
  {
192
228
  // Fast snipe
193
229
  damage = 8 + Math.floor(random() * 5); // 8-12
@@ -195,7 +231,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
195
231
  turnRate = 0;
196
232
  duration = 80 + Math.floor(random() * 40); // 80-120
197
233
  }
198
- else if (roll < 0.7)
234
+ else if (roll < 0.7)
199
235
  {
200
236
  // Homing tracker
201
237
  damage = 7 + Math.floor(random() * 5); // 7-11
@@ -203,7 +239,7 @@ export const Hogger: WizardFunction = ({state, random}) =>
203
239
  turnRate = 1 + random() * 2; // 1-3
204
240
  duration = 150 + Math.floor(random() * 100); // 150-250
205
241
  }
206
- else
242
+ else
207
243
  {
208
244
  // Big slow bomb
209
245
  damage = 12 + Math.floor(random() * 4); // 12-15
@@ -212,17 +248,12 @@ export const Hogger: WizardFunction = ({state, random}) =>
212
248
  duration = 200 + Math.floor(random() * 100); // 200-300
213
249
  }
214
250
 
215
- return {
216
- move,
217
- startCast: {
218
- spell: 'missile',
219
- config: {damage, speed, turnRate, duration},
220
- missileAI: ChaoticHoming,
221
- direction:
222
- angleTo(state.position, enemy.position) + (random() * 20 - 10), // ±10° aim wobble
223
- },
224
- };
251
+ return missile(
252
+ {damage, speed, turnRate, duration},
253
+ ChaoticHoming,
254
+ angleTo(position, enemy.position) + (random() * 20 - 10), // ±10° aim wobble
255
+ ).move(safeMove.x, safeMove.y);
225
256
  }
226
257
 
227
- return {move};
228
- };
258
+ return moveAction(safeMove.x, safeMove.y);
259
+ }
@@ -1,4 +1,5 @@
1
- import {WizardFunction} from '../../types.js';
1
+ import type {FinalAction} from '../../hooks/index.js';
2
+ import {usePosition, useEnemy, useStatus, idle, missile, flyStraight} from '../../hooks/index.js';
2
3
  import {angleTo} from '../../utils/angles.js';
3
4
 
4
5
  /**
@@ -18,33 +19,30 @@ import {angleTo} from '../../utils/angles.js';
18
19
  * STANDALONE — no tier progression. Rookies either learn to play a real class
19
20
  * or quit. This bot represents the rock-bottom of "at least it shoots."
20
21
  */
21
- export const Rookie: WizardFunction = ({state}) =>
22
+ export function Rookie(): FinalAction
22
23
  {
23
- const enemy = state.enemies[0];
24
- if (!enemy)
24
+ const position = usePosition();
25
+ const enemy = useEnemy();
26
+ const status = useStatus();
27
+
28
+ if (enemy.health <= 0)
25
29
  {
26
- return {move: {x: 0, y: 0}};
30
+ return idle();
27
31
  }
28
32
 
29
- if (state.state === 'idle')
33
+ if (status === 'idle')
30
34
  {
31
- return {
32
- move: {x: 0, y: 0},
33
- startCast: {
34
- spell: 'missile',
35
- config: {
36
- damage: 10,
37
- speed: 5,
38
- turnRate: 0,
39
- duration: 200,
40
- },
41
- missileAI: () => ({}),
42
- direction: angleTo(state.position, enemy.position),
35
+ return missile(
36
+ {
37
+ damage: 10,
38
+ speed: 5,
39
+ turnRate: 0,
40
+ duration: 200,
43
41
  },
44
- };
42
+ () => flyStraight(),
43
+ angleTo(position, enemy.position),
44
+ ).move(0, 0);
45
45
  }
46
46
 
47
- return {
48
- move: {x: 0, y: 0},
49
- };
50
- };
47
+ return idle();
48
+ }
@@ -1,4 +1,5 @@
1
- import {WizardFunction} from '../../types.js';
1
+ import type {FinalAction} from '../../hooks/index.js';
2
+ import {idle} from '../../hooks/index.js';
2
3
 
3
4
  /**
4
5
  * Bot: TargetDummy
@@ -13,9 +14,7 @@ import {WizardFunction} from '../../types.js';
13
14
  *
14
15
  * STANDALONE — no tier progression. It's a test fixture, not a combatant.
15
16
  */
16
- export const TargetDummy: WizardFunction = (_props) =>
17
+ export function TargetDummy(): FinalAction
17
18
  {
18
- return {
19
- move: {x: 0, y: 0},
20
- };
21
- };
19
+ return idle();
20
+ }