@vibemancer/core 1.0.8 → 1.0.9

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.
@@ -1,266 +1,299 @@
1
- /**
2
- * VIBEMANCER - ACTION BUILDERS
3
- *
4
- * Fluent API for constructing bot actions with type-safe chaining.
5
- *
6
- * UNITS REFERENCE (100 ticks = 1 second):
7
- * Position: absolute world coordinates, 0-860 on each axis (860×860 arena, of which
8
- * the outer 30 units on every side are lethal lava — playfield is [30, 830])
9
- * Movement: direction vector, magnitude clamped to max 1 (speed in [0, 1])
10
- * Speed: units per tick (player moves at 1.5 units/tick = 150 units/sec)
11
- * Duration: ticks (divide by 100 for seconds)
12
- * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
13
- * Damage: raw HP removed on hit (wizard has 60 HP)
14
- * Turn rate: degrees per tick the missile can rotate
15
- */
16
-
17
- import {WizardActions, MissileConfig, MissileActions} from '../types.js';
18
- import type {FinalAction, ActionBuilder, MissileAction, MissileFunction} from './types.js';
19
-
20
- /**
21
- * Create a FinalAction (no further chaining possible).
22
- */
23
- function createFinalAction(action: WizardActions): FinalAction
24
- {
25
- return {
26
- _toAction: () => action,
27
- };
28
- }
29
-
30
- /**
31
- * Create an ActionBuilder (can chain .move()).
32
- */
33
- function createActionBuilder(baseAction: Omit<WizardActions, 'move'>): ActionBuilder
34
- {
35
- const action: WizardActions = {
36
- ...baseAction,
37
- move: {x: 0, y: 0},
38
- };
39
-
40
- return {
41
- _toAction: () => action,
42
- move(x: number, y: number): FinalAction
43
- {
44
- return createFinalAction({
45
- ...action,
46
- move: {x, y},
47
- });
48
- },
49
- };
50
- }
51
-
52
- // ============================================================
53
- // ACTION BUILDERS
54
- // ============================================================
55
-
56
- /**
57
- * Channel a shield that blocks incoming damage.
58
- *
59
- * Starts at 90% block, decays by 20% per second, minimum 30%.
60
- * Takes 20 ticks (0.2s) to activate. Movement is disabled while channeling.
61
- * Cancel anytime with cancel(). Triggers 100-tick (1s) GCD after cancel.
62
- *
63
- * Can chain .move() — movement applies during the 20-tick cast, NOT during channel.
64
- *
65
- * @example
66
- * return shield(); // shield and stay still
67
- * return shield().move(1, 0); // move right while cast starts
68
- */
69
- export function shield(): ActionBuilder
70
- {
71
- return createActionBuilder({
72
- startCast: {spell: 'shield'},
73
- });
74
- }
75
-
76
- /**
77
- * Cast a missile spell.
78
- *
79
- * Cast time scales with damage, speed, duration, and turn rate — bigger missiles
80
- * take longer to cast. While casting you move at 50% speed. After firing, 100-tick
81
- * (1s) GCD before next spell.
82
- *
83
- * Repeated similar missiles cast 20% faster (warmup bonus). Switching styles
84
- * incurs a 20% penalty.
85
- *
86
- * Can chain .move() for simultaneous movement while casting.
87
- *
88
- * @param config - Missile stats:
89
- * - damage: HP removed on hit (1-60 typical). Also sets hitbox: radius = 2 + 0.1×damage.
90
- * - speed: units/tick (min 1.5). Player moves at 1 u/t, so 5 = 5× player speed.
91
- * - duration: ticks the missile lives (min 10). Range ≈ speed × duration.
92
- * - turnRate: degrees/tick of homing (0 = straight line, 3 = moderate homing, 5+ = strong).
93
- * Negative = no homing + minor speed cost reduction.
94
- * @param ai - Called every tick to control missile steering. Use getMissileContext()
95
- * to read the missile's state (position, rotation, speed, etc.), the full game
96
- * state (worldState), and a seeded PRNG (random). Return turnToward(x, y) to home
97
- * toward a position, or flyStraight() to fly straight.
98
- * @param direction - Launch angle in degrees (0°=right, 90°=down, 180°=left, 270°=up).
99
- * Tip: use Math.atan2(dy, dx) * (180 / Math.PI) to aim at a target.
100
- *
101
- * @example
102
- * // Straight missile aimed at enemy
103
- * const angle = Math.atan2(dy, dx) * (180 / Math.PI);
104
- * return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => flyStraight(), angle);
105
- *
106
- * // Homing missile that tracks enemy
107
- * return missile(
108
- * {damage: 10, speed: 5, duration: 300, turnRate: 3},
109
- * () => {
110
- * const ctx = getMissileContext();
111
- * const enemy = ctx.worldState.enemies[0];
112
- * return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
113
- * },
114
- * angle,
115
- * );
116
- */
117
- export function missile(config: MissileConfig, ai: MissileFunction, direction: number): ActionBuilder
118
- {
119
- return createActionBuilder({
120
- startCast: {
121
- spell: 'missile',
122
- config,
123
- missileAI: ai,
124
- direction,
125
- },
126
- });
127
- }
128
-
129
- /**
130
- * Teleport to an absolute position on the arena.
131
- *
132
- * Max range: 300 units from current position (clamped by engine if further).
133
- * Cast time: 10 ticks (0.1s). Cooldown scales with distance:
134
- * - 100 units → 100 ticks (1s)
135
- * - 300 units → 2000 ticks (20s)
136
- *
137
- * Cannot chain .move() — blink IS the movement.
138
- *
139
- * @param x - Target X position (0-800, absolute world coordinate)
140
- * @param y - Target Y position (0-800, absolute world coordinate)
141
- *
142
- * @example
143
- * return blink(400, 400); // blink to center
144
- * return blink(enemy.position.x, enemy.position.y); // blink to enemy
145
- */
146
- export function blink(x: number, y: number): FinalAction
147
- {
148
- return createFinalAction({
149
- move: {x: 0, y: 0}, // No manual movement with blink
150
- startCast: {
151
- spell: 'blink',
152
- target: {x, y},
153
- },
154
- });
155
- }
156
-
157
- /**
158
- * Cancel current cast or channel (e.g. stop shielding to attack).
159
- *
160
- * Canceling a cast/channel triggers 100-tick (1s) GCD.
161
- * Can chain .move() for simultaneous movement.
162
- *
163
- * @example
164
- * return cancel().move(-1, 0); // cancel and dodge left
165
- */
166
- export function cancel(): ActionBuilder
167
- {
168
- return createActionBuilder({
169
- cancel: true,
170
- });
171
- }
172
-
173
- /**
174
- * Move in a direction without casting any spell.
175
- *
176
- * This is a **direction vector**, not a target position. Values are in the
177
- * range [-1, 1] where 1 = full speed. Larger values (like raw position
178
- * deltas) are clamped to full speed automatically.
179
- *
180
- * @param x - Horizontal direction (positive = right, negative = left)
181
- * @param y - Vertical direction (positive = down, negative = up)
182
- *
183
- * @example
184
- * return move(1, 0); // move right at full speed
185
- * return move(dx / dist, dy / dist); // normalized unit vector = full speed
186
- * return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // raw delta = clamped to full speed
187
- */
188
- export function move(x: number, y: number): FinalAction
189
- {
190
- return createFinalAction({
191
- move: {x, y},
192
- });
193
- }
194
-
195
- /**
196
- * Do nothing — no action, no movement.
197
- */
198
- export function idle(): FinalAction
199
- {
200
- return createFinalAction({
201
- move: {x: 0, y: 0},
202
- });
203
- }
204
-
205
- // ============================================================
206
- // MISSILE ACTION BUILDERS
207
- // ============================================================
208
-
209
- /**
210
- * Steer the missile toward a world position.
211
- * The engine clamps the actual turn to the missile's turnRate.
212
- *
213
- * @param x - Target X position (world coordinates)
214
- * @param y - Target Y position (world coordinates)
215
- */
216
- export function turnToward(x: number, y: number): MissileAction
217
- {
218
- return {
219
- _toMissileAction: () => ({turnToward: {x, y}}),
220
- };
221
- }
222
-
223
- /**
224
- * Steer the missile toward a specific angle (degrees).
225
- * The engine clamps the actual turn to the missile's turnRate.
226
- *
227
- * @param degrees - Target rotation in degrees (0=right, 90=down, 180=left, 270=up)
228
- */
229
- export function turnToAngle(degrees: number): MissileAction
230
- {
231
- return {
232
- _toMissileAction: () => ({turnToAngle: degrees}),
233
- };
234
- }
235
-
236
- /**
237
- * Fly straight — no steering this tick.
238
- */
239
- export function flyStraight(): MissileAction
240
- {
241
- return {
242
- _toMissileAction: () => ({}),
243
- };
244
- }
245
-
246
- /**
247
- * Extract MissileActions from a MissileAction.
248
- * Used by the engine to get the actual missile action.
249
- */
250
- export function extractMissileAction(action: MissileAction): MissileActions
251
- {
252
- return action._toMissileAction();
253
- }
254
-
255
- // ============================================================
256
- // WIZARD ACTION EXTRACTION
257
- // ============================================================
258
-
259
- /**
260
- * Extract WizardActions from a FinalAction.
261
- * Used by the engine to get the actual action.
262
- */
263
- export function extractAction(finalAction: FinalAction): WizardActions
264
- {
265
- return finalAction._toAction();
266
- }
1
+ /**
2
+ * VIBEMANCER - ACTION BUILDERS
3
+ *
4
+ * Fluent API for constructing bot actions with type-safe chaining.
5
+ *
6
+ * UNITS REFERENCE (100 ticks = 1 second):
7
+ * Position: absolute world coordinates, 0-860 on each axis (860×860 arena, of which
8
+ * the outer 30 units on every side are lethal lava — playfield is [30, 830])
9
+ * Movement: direction vector, magnitude clamped to max 1 (speed in [0, 1])
10
+ * Speed: units per tick (player moves at 1.5 units/tick = 150 units/sec)
11
+ * Duration: ticks (divide by 100 for seconds)
12
+ * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
13
+ * Damage: raw HP removed on hit (wizard has 60 HP)
14
+ * Turn rate: degrees per tick the missile can rotate
15
+ */
16
+
17
+ import {WizardActions, MissileConfig, MissileActions} from '../types.js';
18
+ import type {FinalAction, ActionBuilder, MissileAction, MissileFunction} from './types.js';
19
+
20
+ /**
21
+ * Create a FinalAction (no further chaining possible).
22
+ */
23
+ function createFinalAction(action: WizardActions): FinalAction
24
+ {
25
+ return {
26
+ _toAction: () => action,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Create an ActionBuilder (can chain .move()).
32
+ */
33
+ function createActionBuilder(baseAction: Omit<WizardActions, 'move'>): ActionBuilder
34
+ {
35
+ const action: WizardActions = {
36
+ ...baseAction,
37
+ move: {x: 0, y: 0},
38
+ };
39
+
40
+ return {
41
+ _toAction: () => action,
42
+ move(x: number, y: number): FinalAction
43
+ {
44
+ return createFinalAction({
45
+ ...action,
46
+ move: {x, y},
47
+ });
48
+ },
49
+ lockAim(): ActionBuilder
50
+ {
51
+ // Opting out of auto-aim for this action. The engine holds `aimDirection` for the
52
+ // duration of a missile cast, so the angle survives to launch — without this, the
53
+ // per-tick auto-aim overwrites it and the shot goes at the enemy instead.
54
+ //
55
+ // Nothing happens if the action names no direction: there is no angle to keep, and
56
+ // silently facing 0 degrees would be worse than doing nothing.
57
+ const cast = action.startCast;
58
+ const direction = cast && cast.spell === 'missile' ? cast.direction : undefined;
59
+ if (direction === undefined || !Number.isFinite(direction)) return createActionBuilder(baseAction);
60
+ return createActionBuilder({...baseAction, aimDirection: direction});
61
+ },
62
+ };
63
+ }
64
+
65
+ // ============================================================
66
+ // ACTION BUILDERS
67
+ // ============================================================
68
+
69
+ /**
70
+ * Channel a shield that blocks incoming damage.
71
+ *
72
+ * Starts at 90% block, decays by 20% per second, minimum 30%.
73
+ * Takes 20 ticks (0.2s) to activate. Movement is disabled while channeling.
74
+ * Cancel anytime with cancel(). Cancelling costs NO GCD — you are idle the next tick.
75
+ *
76
+ * Can chain .move() — movement applies during the 20-tick cast, NOT during channel.
77
+ *
78
+ * @example
79
+ * return shield(); // shield and stay still
80
+ * return shield().move(1, 0); // move right while cast starts
81
+ */
82
+ export function shield(): ActionBuilder
83
+ {
84
+ return createActionBuilder({
85
+ startCast: {spell: 'shield'},
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Cast a missile spell.
91
+ *
92
+ * Cast time scales with damage, speed, duration, and turn rate bigger missiles
93
+ * take longer to cast. While casting you move at 50% speed. After firing, 100-tick
94
+ * (1s) GCD before next spell.
95
+ *
96
+ * Repeated similar missiles cast 20% faster (warmup bonus). Switching styles
97
+ * incurs a 20% penalty.
98
+ *
99
+ * Can chain .move() for simultaneous movement while casting.
100
+ *
101
+ * @param config - Missile stats:
102
+ * - damage: HP removed on hit (1-60 typical). Also sets hitbox: radius = 2 + 0.1×damage.
103
+ * - speed: units/tick (min 1.5). Player moves at 1 u/t, so 5 = 5× player speed.
104
+ * - duration: ticks the missile lives (min 10). Range speed × duration.
105
+ * - turnRate: degrees/tick of homing (0 = straight line, 3 = moderate homing, 5+ = strong).
106
+ * Negative = no homing + minor speed cost reduction.
107
+ * @param ai - Called every tick to control missile steering. Use getMissileContext()
108
+ * to read the missile's state (position, rotation, speed, etc.), the full game
109
+ * state (worldState), and a seeded PRNG (random). Return turnToward(x, y) to home
110
+ * toward a position, or flyStraight() to fly straight.
111
+ * @param direction - Launch angle in degrees (0°=right, 90°=down, 180°=left, 270°=up).
112
+ * Tip: use Math.atan2(dy, dx) * (180 / Math.PI) to aim at a target.
113
+ *
114
+ * @example
115
+ * // Straight missile aimed at enemy
116
+ * const angle = Math.atan2(dy, dx) * (180 / Math.PI);
117
+ * return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => flyStraight(), angle);
118
+ *
119
+ * // Homing missile that tracks enemy
120
+ * return missile(
121
+ * {damage: 10, speed: 5, duration: 300, turnRate: 3},
122
+ * () => {
123
+ * const ctx = getMissileContext();
124
+ * const enemy = ctx.worldState.enemies[0];
125
+ * return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
126
+ * },
127
+ * angle,
128
+ * );
129
+ */
130
+ export function missile(config: MissileConfig, ai: MissileFunction, direction: number): ActionBuilder
131
+ {
132
+ return createActionBuilder({
133
+ startCast: {
134
+ spell: 'missile',
135
+ config,
136
+ missileAI: ai,
137
+ direction,
138
+ },
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Face a specific direction this tick, instead of auto-aiming at the enemy.
144
+ *
145
+ * Useful on its own (turning to cover an approach) and during a cast, where it keeps a
146
+ * locked shot pointed where you want it. Angles are degrees, 0 = right, 90 = down.
147
+ *
148
+ * A non-finite angle is ignored rather than applied, because a NaN rotation propagates into
149
+ * every position calculation that follows it.
150
+ */
151
+ export function aim(degrees: number): ActionBuilder
152
+ {
153
+ return createActionBuilder(Number.isFinite(degrees) ? {aimDirection: degrees} : {});
154
+ }
155
+
156
+ /**
157
+ * Teleport to an absolute position on the arena.
158
+ *
159
+ * Max range: 300 units from current position (clamped by engine if further).
160
+ * Cast time: 10 ticks (0.1s). Cooldown scales with distance:
161
+ * - 100 units 100 ticks (1s)
162
+ * - 300 units → 2000 ticks (20s)
163
+ *
164
+ * Cannot chain .move() blink IS the movement.
165
+ *
166
+ * @param x - Target X position (0-800, absolute world coordinate)
167
+ * @param y - Target Y position (0-800, absolute world coordinate)
168
+ *
169
+ * @example
170
+ * return blink(400, 400); // blink to center
171
+ * return blink(enemy.position.x, enemy.position.y); // blink to enemy
172
+ */
173
+ export function blink(x: number, y: number): FinalAction
174
+ {
175
+ return createFinalAction({
176
+ move: {x: 0, y: 0}, // No manual movement with blink
177
+ startCast: {
178
+ spell: 'blink',
179
+ target: {x, y},
180
+ },
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Cancel current cast or channel (e.g. stop shielding to attack).
186
+ *
187
+ * Cancelling costs NO GCD. `gcdRemaining` is set only when a spell COMPLETES; the cancel
188
+ * handlers just return you to idle, so you can act on the very next tick.
189
+ *
190
+ * This said "triggers 100-tick (1s) GCD" and was wrong for as long as anyone can tell. It
191
+ * matters more than a typo: it makes every defender look 100 ticks slower than they are, so
192
+ * attacks that appear unpunishable on paper are not. Cancel-then-shield really costs 1 tick
193
+ * plus the 20-tick shield cast, not 120.
194
+ * Can chain .move() for simultaneous movement.
195
+ *
196
+ * @example
197
+ * return cancel().move(-1, 0); // cancel and dodge left
198
+ */
199
+ export function cancel(): ActionBuilder
200
+ {
201
+ return createActionBuilder({
202
+ cancel: true,
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Move in a direction without casting any spell.
208
+ *
209
+ * This is a **direction vector**, not a target position. Values are in the
210
+ * range [-1, 1] where 1 = full speed. Larger values (like raw position
211
+ * deltas) are clamped to full speed automatically.
212
+ *
213
+ * @param x - Horizontal direction (positive = right, negative = left)
214
+ * @param y - Vertical direction (positive = down, negative = up)
215
+ *
216
+ * @example
217
+ * return move(1, 0); // move right at full speed
218
+ * return move(dx / dist, dy / dist); // normalized unit vector = full speed
219
+ * return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // raw delta = clamped to full speed
220
+ */
221
+ export function move(x: number, y: number): FinalAction
222
+ {
223
+ return createFinalAction({
224
+ move: {x, y},
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Do nothing — no action, no movement.
230
+ */
231
+ export function idle(): FinalAction
232
+ {
233
+ return createFinalAction({
234
+ move: {x: 0, y: 0},
235
+ });
236
+ }
237
+
238
+ // ============================================================
239
+ // MISSILE ACTION BUILDERS
240
+ // ============================================================
241
+
242
+ /**
243
+ * Steer the missile toward a world position.
244
+ * The engine clamps the actual turn to the missile's turnRate.
245
+ *
246
+ * @param x - Target X position (world coordinates)
247
+ * @param y - Target Y position (world coordinates)
248
+ */
249
+ export function turnToward(x: number, y: number): MissileAction
250
+ {
251
+ return {
252
+ _toMissileAction: () => ({turnToward: {x, y}}),
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Steer the missile toward a specific angle (degrees).
258
+ * The engine clamps the actual turn to the missile's turnRate.
259
+ *
260
+ * @param degrees - Target rotation in degrees (0=right, 90=down, 180=left, 270=up)
261
+ */
262
+ export function turnToAngle(degrees: number): MissileAction
263
+ {
264
+ return {
265
+ _toMissileAction: () => ({turnToAngle: degrees}),
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Fly straight — no steering this tick.
271
+ */
272
+ export function flyStraight(): MissileAction
273
+ {
274
+ return {
275
+ _toMissileAction: () => ({}),
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Extract MissileActions from a MissileAction.
281
+ * Used by the engine to get the actual missile action.
282
+ */
283
+ export function extractMissileAction(action: MissileAction): MissileActions
284
+ {
285
+ return action._toMissileAction();
286
+ }
287
+
288
+ // ============================================================
289
+ // WIZARD ACTION EXTRACTION
290
+ // ============================================================
291
+
292
+ /**
293
+ * Extract WizardActions from a FinalAction.
294
+ * Used by the engine to get the actual action.
295
+ */
296
+ export function extractAction(finalAction: FinalAction): WizardActions
297
+ {
298
+ return finalAction._toAction();
299
+ }