@vibemancer/core 1.0.7 → 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,200 +1,216 @@
1
- /**
2
- * VIBEMANCER - HOOKS API TYPES
3
- *
4
- * Types for the hooks-based bot API.
5
- *
6
- * UNITS REFERENCE (100 ticks = 1 second):
7
- * Position: absolute world coordinates, 0-860 on each axis. The arena is 860x860,
8
- * but only [30, 830] is safe — the outer 30 units are lethal lava.
9
- * Velocity: units per tick on each axis (player max speed = 1 u/t)
10
- * Health: hit points (max 60)
11
- * Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
12
- * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
13
- */
14
-
15
- import {Position, Velocity, ProjectileState, MissileConfig, WizardActions, GameState, MissileActions} from '../types.js';
16
-
17
- /**
18
- * Enemy wizard state as seen by your bot.
19
- *
20
- * Note: you cannot see the enemy's missile configs, cooldown timers, or
21
- * damage history — only what's visible on the battlefield.
22
- */
23
- export interface EnemyState
24
- {
25
- /** Enemy position in world coordinates (0-800). */
26
- position: Position;
27
- /** Enemy velocity in units/tick. */
28
- velocity: Velocity;
29
- /** Enemy current HP (0-60). */
30
- health: number;
31
- /** Enemy status: 'idle', 'casting', 'channeling' (shield), or 'gcd_locked'. */
32
- status: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
33
- /** Which spell enemy is casting, or null. */
34
- castingSpell: 'missile' | 'shield' | 'blink' | null;
35
- /** Cast progress in ticks (0 if not casting). */
36
- castProgress: number;
37
- /** Total cast duration in ticks (0 if not casting). */
38
- castDuration: number;
39
- /** Remaining GCD ticks (0 if not in GCD). */
40
- gcdRemaining: number;
41
- /** Duration the enemy has been channeling in ticks (0 if not channeling). */
42
- channelDuration: number;
43
- /** Enemy shield block multiplier (0 if not shielding, 0.3-0.9 if shielding). */
44
- shieldStrength: number;
45
- }
46
-
47
- /**
48
- * Pre-computed analysis of an incoming enemy projectile.
49
- *
50
- * All timing values are in ticks (100 ticks = 1 second).
51
- * Dodge directions are relative to missile heading, not world axes.
52
- */
53
- export interface AnalyzedThreat
54
- {
55
- /** Unique projectile ID. */
56
- id: string;
57
- /** Raw projectile state (position, rotation in degrees, speed in u/t, turnRate, remainingTicks). */
58
- projectile: ProjectileState;
59
-
60
- /** Ticks until missile hits your current position. Infinity if predicted to miss. */
61
- ticksToImpact: number;
62
- /** Whether the missile will hit if you stand still. */
63
- willHit: boolean;
64
-
65
- /** Whether strafing left (perpendicular to missile heading) avoids it. */
66
- canDodgeLeft: boolean;
67
- /** Whether strafing right (perpendicular to missile heading) avoids it. */
68
- canDodgeRight: boolean;
69
- /** Whether moving directly away from the missile avoids it. */
70
- canOutrun: boolean;
71
- /** Optimal dodge direction as a unit vector {x, y}, or null if undodgeable. */
72
- bestDodgeDirection: Position | null;
73
-
74
- /** Whether you can channel shield before the missile arrives. */
75
- canBlockInTime: boolean;
76
- /** Ticks from now when you should START channeling shield to block in time. */
77
- ticksToStartShield: number;
78
- }
79
-
80
- /**
81
- * Final action that can be returned from a bot.
82
- * Cannot be further chained.
83
- */
84
- export interface FinalAction
85
- {
86
- /** Internal: extract the WizardActions */
87
- readonly _toAction: () => WizardActions;
88
- }
89
-
90
- /**
91
- * Action builder that allows chaining .move() for simultaneous movement.
92
- * Returned by shield(), missile(), and cancel().
93
- */
94
- export interface ActionBuilder extends FinalAction
95
- {
96
- /**
97
- * Add movement to this action (e.g., move while casting).
98
- * Direction vector, not absolute position. Auto-normalized.
99
- * Positive X = right, positive Y = down.
100
- */
101
- move(x: number, y: number): FinalAction;
102
- }
103
-
104
- /**
105
- * Wizard function type for the hooks API.
106
- * Called every tick. Read state with hooks, return an action.
107
- */
108
- export type WizardFunction = () => FinalAction;
109
-
110
- /**
111
- * Internal context for game state hooks.
112
- */
113
- export interface WizardContext
114
- {
115
- entityId: string;
116
- tick: number;
117
- position: Position;
118
- velocity: Velocity;
119
- health: number;
120
- maxHealth: number;
121
- state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
122
- castingSpell?: 'missile' | 'shield' | 'blink';
123
- castProgress?: number;
124
- castDuration?: number;
125
- channelingSpell?: 'shield';
126
- channelDuration?: number;
127
- gcdRemaining?: number;
128
- blinkCooldown: number;
129
- lastMissileConfig?: MissileConfig;
130
- enemies: Array<{
131
- id: string;
132
- position: Position;
133
- velocity: Velocity;
134
- health: number;
135
- state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
136
- castingSpell?: 'missile' | 'shield' | 'blink';
137
- castProgress?: number;
138
- castDuration?: number;
139
- gcdRemaining?: number;
140
- channelingSpell?: 'shield';
141
- channelDuration?: number;
142
- }>;
143
- projectiles: ProjectileState[];
144
- myProjectiles: ProjectileState[];
145
- arenaWidth: number;
146
- arenaHeight: number;
147
- damageDealt: number;
148
- damageTaken: number;
149
- lastHitTick: number;
150
- random: () => number;
151
- }
152
-
153
- /**
154
- * Context available to missile AI functions via getMissileContext().
155
- * Provides missile state, the full game state from the owner's perspective,
156
- * and a seeded PRNG.
157
- */
158
- export interface MissileContext
159
- {
160
- /** Missile position in world coordinates. */
161
- position: Position;
162
- /** Missile heading in degrees (0=right, 90=down). */
163
- rotation: number;
164
- /** Missile speed in units/tick. */
165
- speed: number;
166
- /** Missile turn rate in degrees/tick. */
167
- turnRate: number;
168
- /** Missile damage on hit. */
169
- damage: number;
170
- /** Ticks remaining before the missile expires. */
171
- remainingTicks: number;
172
- /** ID of the wizard who owns this missile. */
173
- ownerId: string;
174
- /** Full game state from the missile owner's perspective. */
175
- worldState: GameState;
176
- /** Seeded PRNG [0, 1). Deterministic per missile per tick. */
177
- random: () => number;
178
- }
179
-
180
- /**
181
- * Action returned by a missile AI function.
182
- * Call turnToward(x, y) to steer, or flyStraight() to coast.
183
- */
184
- export interface MissileAction
185
- {
186
- /** Internal: extract the MissileActions */
187
- readonly _toMissileAction: () => MissileActions;
188
- }
189
-
190
- /**
191
- * Missile AI function type for the hooks API.
192
- * Called every tick for each in-flight missile.
193
- * Read state with getMissileContext(), return a MissileAction.
194
- */
195
- export type MissileFunction = () => MissileAction;
196
-
197
- /**
198
- * Missile configuration for action builders.
199
- */
200
- export type {MissileConfig};
1
+ /**
2
+ * VIBEMANCER - HOOKS API TYPES
3
+ *
4
+ * Types for the hooks-based bot API.
5
+ *
6
+ * UNITS REFERENCE (100 ticks = 1 second):
7
+ * Position: absolute world coordinates, 0-860 on each axis. The arena is 860x860,
8
+ * but only [30, 830] is safe — the outer 30 units are lethal lava.
9
+ * Velocity: units per tick on each axis (player max speed = 1 u/t)
10
+ * Health: hit points (max 60)
11
+ * Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
12
+ * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
13
+ */
14
+
15
+ import {Position, Velocity, ProjectileState, MissileConfig, WizardActions, GameState, MissileActions} from '../types.js';
16
+
17
+ /**
18
+ * Enemy wizard state as seen by your bot.
19
+ *
20
+ * Note: you cannot see the enemy's missile configs, cooldown timers, or
21
+ * damage history — only what's visible on the battlefield.
22
+ */
23
+ export interface EnemyState
24
+ {
25
+ /** Enemy position in world coordinates (0-800). */
26
+ position: Position;
27
+ /** Enemy velocity in units/tick. */
28
+ velocity: Velocity;
29
+ /** Enemy current HP (0-60). */
30
+ health: number;
31
+ /** Enemy status: 'idle', 'casting', 'channeling' (shield), or 'gcd_locked'. */
32
+ status: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
33
+ /** Which spell enemy is casting, or null. */
34
+ castingSpell: 'missile' | 'shield' | 'blink' | null;
35
+ /** Cast progress in ticks (0 if not casting). */
36
+ castProgress: number;
37
+ /** Total cast duration in ticks (0 if not casting). */
38
+ castDuration: number;
39
+ /** Remaining GCD ticks (0 if not in GCD). */
40
+ gcdRemaining: number;
41
+ /** Duration the enemy has been channeling in ticks (0 if not channeling). */
42
+ channelDuration: number;
43
+ /** Enemy shield block multiplier (0 if not shielding, 0.3-0.9 if shielding). */
44
+ shieldStrength: number;
45
+ }
46
+
47
+ /**
48
+ * Pre-computed analysis of an incoming enemy projectile.
49
+ *
50
+ * All timing values are in ticks (100 ticks = 1 second).
51
+ * Dodge directions are relative to missile heading, not world axes.
52
+ */
53
+ export interface AnalyzedThreat
54
+ {
55
+ /** Unique projectile ID. */
56
+ id: string;
57
+ /** Raw projectile state (position, rotation in degrees, speed in u/t, turnRate, remainingTicks). */
58
+ projectile: ProjectileState;
59
+
60
+ /** Ticks until missile hits your current position. Infinity if predicted to miss. */
61
+ ticksToImpact: number;
62
+ /** Whether the missile will hit if you stand still. */
63
+ willHit: boolean;
64
+
65
+ /** Whether strafing left (perpendicular to missile heading) avoids it. */
66
+ canDodgeLeft: boolean;
67
+ /** Whether strafing right (perpendicular to missile heading) avoids it. */
68
+ canDodgeRight: boolean;
69
+ /** Whether moving directly away from the missile avoids it. */
70
+ canOutrun: boolean;
71
+ /** Optimal dodge direction as a unit vector {x, y}, or null if undodgeable. */
72
+ bestDodgeDirection: Position | null;
73
+
74
+ /** Whether you can channel shield before the missile arrives. */
75
+ canBlockInTime: boolean;
76
+ /** Ticks from now when you should START channeling shield to block in time. */
77
+ ticksToStartShield: number;
78
+ }
79
+
80
+ /**
81
+ * Final action that can be returned from a bot.
82
+ * Cannot be further chained.
83
+ */
84
+ export interface FinalAction
85
+ {
86
+ /** Internal: extract the WizardActions */
87
+ readonly _toAction: () => WizardActions;
88
+ }
89
+
90
+ /**
91
+ * Action builder that allows chaining .move() for simultaneous movement.
92
+ * Returned by shield(), missile(), and cancel().
93
+ */
94
+ export interface ActionBuilder extends FinalAction
95
+ {
96
+ /**
97
+ * Add movement to this action (e.g., move while casting).
98
+ * Direction vector, not absolute position. Auto-normalized.
99
+ * Positive X = right, positive Y = down.
100
+ */
101
+ move(x: number, y: number): FinalAction;
102
+
103
+ /**
104
+ * Fire at the angle you asked for, instead of auto-aiming at the enemy.
105
+ *
106
+ * By default a wizard re-aims at the enemy's CURRENT position every tick, and a missile
107
+ * launches with the rotation it has when the cast finishes — so the `direction` you pass
108
+ * to `missile()` is overwritten before the shot leaves. That makes the enemy impossible
109
+ * to LEAD: you always fire where they are, never where they will be.
110
+ *
111
+ * `lockAim()` holds your angle for the whole cast, which is what makes `getLeadPosition`
112
+ * and `interceptAngle` worth using. It is opt-in precisely because auto-aim is the right
113
+ * default for most bots: lock your aim and a target that moves during the cast is missed.
114
+ *
115
+ * Chainable — `missile(cfg, ai, angle).lockAim().move(0, 1)` aims, fires and kites.
116
+ */
117
+ lockAim(): ActionBuilder;
118
+ }
119
+
120
+ /**
121
+ * Wizard function type for the hooks API.
122
+ * Called every tick. Read state with hooks, return an action.
123
+ */
124
+ export type WizardFunction = () => FinalAction;
125
+
126
+ /**
127
+ * Internal context for game state hooks.
128
+ */
129
+ export interface WizardContext
130
+ {
131
+ entityId: string;
132
+ tick: number;
133
+ position: Position;
134
+ velocity: Velocity;
135
+ health: number;
136
+ maxHealth: number;
137
+ state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
138
+ castingSpell?: 'missile' | 'shield' | 'blink';
139
+ castProgress?: number;
140
+ castDuration?: number;
141
+ channelingSpell?: 'shield';
142
+ channelDuration?: number;
143
+ gcdRemaining?: number;
144
+ blinkCooldown: number;
145
+ lastMissileConfig?: MissileConfig;
146
+ enemies: Array<{
147
+ id: string;
148
+ position: Position;
149
+ velocity: Velocity;
150
+ health: number;
151
+ state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
152
+ castingSpell?: 'missile' | 'shield' | 'blink';
153
+ castProgress?: number;
154
+ castDuration?: number;
155
+ gcdRemaining?: number;
156
+ channelingSpell?: 'shield';
157
+ channelDuration?: number;
158
+ }>;
159
+ projectiles: ProjectileState[];
160
+ myProjectiles: ProjectileState[];
161
+ arenaWidth: number;
162
+ arenaHeight: number;
163
+ damageDealt: number;
164
+ damageTaken: number;
165
+ lastHitTick: number;
166
+ random: () => number;
167
+ }
168
+
169
+ /**
170
+ * Context available to missile AI functions via getMissileContext().
171
+ * Provides missile state, the full game state from the owner's perspective,
172
+ * and a seeded PRNG.
173
+ */
174
+ export interface MissileContext
175
+ {
176
+ /** Missile position in world coordinates. */
177
+ position: Position;
178
+ /** Missile heading in degrees (0=right, 90=down). */
179
+ rotation: number;
180
+ /** Missile speed in units/tick. */
181
+ speed: number;
182
+ /** Missile turn rate in degrees/tick. */
183
+ turnRate: number;
184
+ /** Missile damage on hit. */
185
+ damage: number;
186
+ /** Ticks remaining before the missile expires. */
187
+ remainingTicks: number;
188
+ /** ID of the wizard who owns this missile. */
189
+ ownerId: string;
190
+ /** Full game state from the missile owner's perspective. */
191
+ worldState: GameState;
192
+ /** Seeded PRNG [0, 1). Deterministic per missile per tick. */
193
+ random: () => number;
194
+ }
195
+
196
+ /**
197
+ * Action returned by a missile AI function.
198
+ * Call turnToward(x, y) to steer, or flyStraight() to coast.
199
+ */
200
+ export interface MissileAction
201
+ {
202
+ /** Internal: extract the MissileActions */
203
+ readonly _toMissileAction: () => MissileActions;
204
+ }
205
+
206
+ /**
207
+ * Missile AI function type for the hooks API.
208
+ * Called every tick for each in-flight missile.
209
+ * Read state with getMissileContext(), return a MissileAction.
210
+ */
211
+ export type MissileFunction = () => MissileAction;
212
+
213
+ /**
214
+ * Missile configuration for action builders.
215
+ */
216
+ export type {MissileConfig};
package/src/rules.ts CHANGED
@@ -61,6 +61,11 @@ export const RULES = {
61
61
  MISSILE_MIN_CAST_TIME: 0.1, // seconds
62
62
  MISSILE_BASE_RADIUS: 2, // base hitbox radius in units
63
63
  MISSILE_DAMAGE_RADIUS_SCALE: 0.1, // additional radius per damage point
64
+ // The real hitbox is THREE TIMES the base+scale figure. This lived as a bare `* 3` inside
65
+ // calculateMissileRadius, outside RULES, so no document could interpolate it — and five
66
+ // of them consequently stated the unmultiplied formula, understating the hit window by
67
+ // 43-57%. It is a rule, so it lives with the rules.
68
+ MISSILE_RADIUS_MULTIPLIER: 3,
64
69
  MISSILE_BASE_CAST: 0.1,
65
70
  MISSILE_DAMAGE_SCALE: 0.226,
66
71
  MISSILE_DAMAGE_POWER: 2 / 3, // sublinear: DPS always increases with damage
@@ -133,7 +138,7 @@ export const MISSILE_DAMAGE_RADIUS_SCALE = RULES.MISSILE_DAMAGE_RADIUS_SCALE;
133
138
  */
134
139
  export function calculateMissileRadius(damage: number): number
135
140
  {
136
- return (RULES.MISSILE_BASE_RADIUS + damage * RULES.MISSILE_DAMAGE_RADIUS_SCALE) * 3;
141
+ return (RULES.MISSILE_BASE_RADIUS + damage * RULES.MISSILE_DAMAGE_RADIUS_SCALE) * RULES.MISSILE_RADIUS_MULTIPLIER;
137
142
  }
138
143
 
139
144
  // === MISSILE CAST TIME FORMULA ===