create-vibemancer 0.1.0

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 ADDED
@@ -0,0 +1,30 @@
1
+ # create-vibemancer
2
+
3
+ Scaffold a new [Vibemancer](https://vibemancer.com) wizard bot project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create vibemancer@latest my-wizard
9
+ cd my-wizard
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Creates a project with:
15
+
16
+ - `src/bot.ts` — starter wizard bot
17
+ - `tests/bot.test.ts` — vitest suite
18
+ - `AGENTS.md` — context file for AI coding assistants (Claude Code, Cursor, Copilot CLI, Codex)
19
+ - `README.md` — full development workflow guide
20
+ - `vibemancer.json` — CLI config
21
+
22
+ Prefer pnpm, yarn, or bun? They all work — swap `npm` for your favourite.
23
+
24
+ ## What's next
25
+
26
+ See the full walkthrough at [vibemancer.com](https://vibemancer.com) (Build a Bot tab), or just run `npm run dev` and start editing.
27
+
28
+ ## License
29
+
30
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs2 from "fs";
5
+ import path2 from "path";
6
+
7
+ // src/scaffolder.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ function toPascalCase(str) {
11
+ let result = str.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).replace(/\s+/g, "");
12
+ if (/^\d/.test(result)) {
13
+ result = "_" + result;
14
+ }
15
+ return result;
16
+ }
17
+ function isValidIdentifier(name) {
18
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
19
+ }
20
+ function isValidProjectName(name) {
21
+ return /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(name);
22
+ }
23
+ function packageJson(projectName) {
24
+ return JSON.stringify({
25
+ name: projectName,
26
+ version: "0.1.0",
27
+ private: true,
28
+ type: "module",
29
+ scripts: {
30
+ dev: "vibemancer dev",
31
+ test: "vibemancer test",
32
+ typecheck: "tsc --noEmit",
33
+ fight: "vibemancer fight",
34
+ trace: "vibemancer trace --opponent TargetDummy",
35
+ optimize: "vibemancer optimize",
36
+ build: "vibemancer build --opponent Battlemage"
37
+ },
38
+ dependencies: {
39
+ "@vibemancer/core": "~0.1.0"
40
+ },
41
+ devDependencies: {
42
+ "vibemancer": "~0.1.0",
43
+ "typescript": "^5.9.0",
44
+ "vitest": "^3.0.0"
45
+ }
46
+ }, null, " ") + "\n";
47
+ }
48
+ function tsconfigJson() {
49
+ return JSON.stringify({
50
+ compilerOptions: {
51
+ target: "ESNext",
52
+ module: "ESNext",
53
+ lib: ["ESNext"],
54
+ skipLibCheck: true,
55
+ moduleResolution: "Bundler",
56
+ strict: true,
57
+ noUnusedLocals: true,
58
+ noUnusedParameters: true,
59
+ isolatedModules: true,
60
+ noEmit: true
61
+ },
62
+ include: ["src", "tests"]
63
+ }, null, " ") + "\n";
64
+ }
65
+ function vibemancerJson(botName) {
66
+ return JSON.stringify({
67
+ bot: "src/bot.ts",
68
+ export: botName
69
+ }, null, " ") + "\n";
70
+ }
71
+ function botTemplate(botName) {
72
+ return `import {
73
+ usePosition, useEnemy, useTicksUntilReady, useThreats,
74
+ missile, shield, move,
75
+ // More actions (all importable from '@vibemancer/core'):
76
+ // blink, cancel, idle,
77
+ // More hooks:
78
+ // useHealth, useBlinkCooldown, useStatus, useVelocity,
79
+ // useCastProgress, useMyProjectiles, useMyThreatsToEnemy,
80
+ // useClosestThreat, useShieldStrength, useCastingSpell,
81
+ // useTick, useArenaSize, useDamageDealt, useDamageTaken,
82
+ // Utility functions:
83
+ // distanceTo, angleTo, directionTo, directionAway,
84
+ // getLeadPosition, fitMissileToBudget, getMissileCastTime,
85
+ // predictPosition, interceptAngle, clampPositionToArena,
86
+ // Persistence hooks (React-style, state preserved across ticks):
87
+ // useState, useRef, useMemo, useEffect
88
+ // Parameter tuning (for optimizer):
89
+ // useParam
90
+ } from '@vibemancer/core';
91
+
92
+ /**
93
+ * ${botName} - Your custom wizard bot!
94
+ *
95
+ * === HOW IT WORKS ===
96
+ * Your function is called EVERY TICK (100x per second). Each call you:
97
+ * 1. Read game state using hooks (usePosition, useEnemy, etc.)
98
+ * 2. Return ONE action (missile, shield, blink, move, cancel, or idle)
99
+ *
100
+ * The function must ALWAYS return an action. During cooldowns (GCD), return
101
+ * move() to keep moving \u2014 casting spells during GCD is silently ignored.
102
+ * Check useTicksUntilReady() > 0 to know when you're on cooldown.
103
+ *
104
+ * === GAME RULES ===
105
+ * Arena: 800x800, positions clamped to [5, 795]
106
+ * Health: 60 HP, wizard dies at 0
107
+ * Timing: 100 ticks = 1 second, match lasts 30,000 ticks (5 min)
108
+ * Movement: 1 unit/tick (100 u/s), 50% speed while casting, 0% while shielding
109
+ * GCD: 100 ticks (1s) lockout after any spell completes or is canceled
110
+ *
111
+ * === SPELLS ===
112
+ * missile(config, ai, direction)
113
+ * config: { damage, speed, duration, turnRate }
114
+ * - damage: HP removed on hit (1-60). Hitbox radius = 2 + 0.1 x damage.
115
+ * - speed: units/tick (min 1.5, player moves at 1). Range = speed x duration.
116
+ * - duration: ticks the missile lives (min 10). Longer = more range but slower cast.
117
+ * - turnRate: degrees/tick of homing (0 = straight, 3 = moderate, 5+ = strong).
118
+ * ai: called every tick to steer -> return { turnToward: {x,y} } or {} for straight.
119
+ * direction: launch angle in degrees (0=right, 90=down, 180=left, 270=up).
120
+ * Cast time scales with all stats. Repeated similar missiles cast 20% faster (warmup).
121
+ * Chain .move(dx, dy) to walk at 50% speed while casting.
122
+ *
123
+ * shield()
124
+ * Channeled. Blocks 90% initially, decays 20%/sec, minimum 30%.
125
+ * 0.2s cast time. Immobile while channeling. Cancel with cancel().
126
+ *
127
+ * blink(x, y) \u2014 Teleport to ABSOLUTE position (not direction). Max 300 units.
128
+ * move(dx, dy) \u2014 Move in DIRECTION (not position). Auto-normalized to max speed.
129
+ * cancel() \u2014 Cancel current cast/channel. Chain .move() to dodge simultaneously.
130
+ * idle() \u2014 Do nothing this tick.
131
+ *
132
+ * === KEY TYPES ===
133
+ * useEnemy() returns: { position: {x, y}, velocity: {x, y}, health, state, ... }
134
+ * useThreats() returns array of:
135
+ * { ticksToImpact, damage, position, velocity, canDodge, bestDodgeDirection, ... }
136
+ * fitMissileToBudget(budget, opts) \u2014 finds optimal missile config for a cast-time budget
137
+ * getLeadPosition(pos, vel, time) \u2014 predicts where a moving target will be
138
+ *
139
+ * All imports have full JSDoc \u2014 hover or jump-to-definition to see docs.
140
+ */
141
+ export function ${botName}()
142
+ {
143
+ const myPos = usePosition();
144
+ const enemy = useEnemy();
145
+ const ready = useTicksUntilReady();
146
+ const threats = useThreats();
147
+
148
+ // Direction toward enemy (used for movement and aiming)
149
+ const dx = enemy.position.x - myPos.x;
150
+ const dy = enemy.position.y - myPos.y;
151
+ const dist = Math.sqrt(dx * dx + dy * dy);
152
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI);
153
+
154
+ // While on cooldown, move toward the enemy
155
+ // move() takes a direction vector, not an absolute position
156
+ if (ready > 0) return move(dx, dy);
157
+
158
+ // Shield if a missile is about to hit (within 30 ticks = 0.3s)
159
+ const closestThreat = threats[0];
160
+ if (closestThreat && closestThreat.ticksToImpact < 30)
161
+ {
162
+ return shield();
163
+ }
164
+
165
+ // Fire a homing missile at the enemy
166
+ // Duration scales with distance so missiles always have enough range
167
+ // Chain .move() to walk toward enemy at 50% speed while casting
168
+ return missile(
169
+ {damage: 15, speed: 6, duration: Math.max(60, Math.ceil(dist / 6) + 20), turnRate: 3},
170
+ ({worldState}) =>
171
+ {
172
+ // Missile AI: called every tick to steer the missile
173
+ // Return { turnToward: position } to home, or {} to fly straight
174
+ const target = worldState.enemies[0];
175
+ if (!target) return {};
176
+ return {turnToward: target.position};
177
+ },
178
+ angle, // Launch angle in degrees toward enemy
179
+ ).move(dx, dy);
180
+ }
181
+ `;
182
+ }
183
+ function readmeMd(botName) {
184
+ return `# ${botName} \u2014 Vibemancer Bot
185
+
186
+ This is a Vibemancer wizard bot project. The bot fights 1v1 against other wizard bots in an 800x800 arena.
187
+
188
+ ## Quick Start
189
+
190
+ \`\`\`bash
191
+ npm test # Verify everything works
192
+ npm run dev # Start dev server + open browser (refresh to see changes)
193
+ npm run fight # Fight against all 29 built-in bots
194
+ npm run trace # Per-tick debug trace against TargetDummy
195
+ npm run typecheck # Check for type errors (catches bugs esbuild misses)
196
+ npm run optimize # Auto-tune useParam() values via optimizer
197
+ \`\`\`
198
+
199
+ To trace/fight a specific opponent:
200
+ \`\`\`bash
201
+ npx vibemancer trace --opponent Battlemage
202
+ npx vibemancer fight --opponent Nightblade
203
+ npx vibemancer bots # list all bots with descriptions
204
+ npx vibemancer bots Battlemage # details about a specific bot
205
+ \`\`\`
206
+
207
+ ## Project Structure
208
+
209
+ - \`src/bot.ts\` \u2014 Bot source code (edit this!)
210
+ - \`tests/bot.test.ts\` \u2014 Automated tests for your bot
211
+ - \`vibemancer.json\` \u2014 Config: \`{ "bot": "src/bot.ts", "export": "${botName}" }\`
212
+
213
+ ## Development Workflow
214
+
215
+ 1. Edit \`src/bot.ts\`
216
+ 2. Run \`npm run typecheck\` to catch type errors early
217
+ 3. Run \`npm test\` to check your bot doesn't crash and beats weak bots
218
+ 4. Run \`npm run fight\` to see how you rank against all 29 built-in bots
219
+ 5. Run \`npm run trace\` to see per-tick events when something isn't working
220
+ 6. Run \`npm run optimize\` to auto-tune useParam() values
221
+
222
+ For the browser viewer: run \`npm run dev\`, then refresh the browser after editing your bot.
223
+
224
+ ## Difficulty Progression
225
+
226
+ Start by beating the weakest bots and work your way up:
227
+
228
+ | Milestone | Bots to Beat | What You Need |
229
+ |-----------|-------------|---------------|
230
+ | Beginner | TargetDummy, Critter | Basic missile firing |
231
+ | Easy | Rookie, Hogger, Bonemancer | Homing missiles + movement |
232
+ | Medium | Turtle, Sentinel, Flamecaller | Shielding + dodge awareness |
233
+ | Hard | Battlemage (#11), Stormchaser | Cast timing + positioning |
234
+ | Expert | Archmage, Stormforger | Adaptive missiles + vulnerability punish |
235
+ | Master | Nightblade (#29) | Everything at once |
236
+
237
+ ## Debugging
238
+
239
+ When your bot isn't working:
240
+ 1. \`npm test\` \u2014 check for runtime errors (captured in test results)
241
+ 2. \`vibemancer trace --opponent TargetDummy\` \u2014 watch exactly what happens tick by tick
242
+ 3. Look at the trace stats: hit rate, shield usage, idle time
243
+ 4. Look for ERROR and WARNING events in the trace output
244
+
245
+ ## Common Pitfalls
246
+
247
+ - \`move(dx, dy)\` is a **direction vector**, NOT a target position
248
+ - \`blink(x, y)\` is an **absolute position**, NOT a direction
249
+ - Calling \`missile()\` during GCD is **silently ignored** \u2014 check \`useTicksUntilReady()\` first
250
+ - Missile range = speed x duration. If range < distance, missiles expire before reaching the enemy
251
+ - All angles are in **degrees** (not radians). Use: \`Math.atan2(dy, dx) * (180 / Math.PI)\`
252
+ - All time values are in **ticks** (100 ticks = 1 second)
253
+ - Your bot runs every tick \u2014 always return an action, even during cooldowns (\`move()\`)
254
+ - \`fitMissileToBudget()\` and \`getLeadPosition()\` return **null** when no solution exists
255
+
256
+ ## Built-in Bots (29 total, ranked weakest -> strongest)
257
+
258
+ TargetDummy, Critter, Bonemancer, Hogger, Rookie, Turtle, Sentinel, Flamecaller,
259
+ Golem, Stormchaser, Battlemage, Spellspinner, Doombringer, Lich, Stormcaller,
260
+ Spellbinder, Shadowblade, Pyromancer, Warmage, Spelltracer, Archmage, Spellseeker,
261
+ Stormforger, Spellweaver, Spellshot, Voidblade, Infernalist, Archlich, Nightblade
262
+
263
+ Run \`vibemancer bots\` to see descriptions, or \`vibemancer bots Battlemage\` for details.
264
+
265
+ ## Strategy Tips
266
+
267
+ - Shield is strongest at the start (90% block) \u2014 cancel early to minimize GCD waste
268
+ - Repeated similar missiles cast 20% faster (warmup). Switching styles costs 20%
269
+ - Higher damage = longer cast time, but DPS always increases with damage
270
+ - Homing missiles (turnRate > 0) are harder to dodge but cost more cast time
271
+ - Straight missiles (turnRate: 0) are fast to cast. Use \`getLeadPosition()\` to aim
272
+ - Use \`useThreats()\` to decide: dodge (best), shield (good), or eat the hit (risky)
273
+ `;
274
+ }
275
+ function botTestTemplate(botName) {
276
+ return `import {expect, test} from 'vitest';
277
+ import {testBot} from '@vibemancer/core';
278
+ import {${botName}} from '../src/bot';
279
+
280
+ // Run with: npm test
281
+ // These tests are ordered by difficulty \u2014 fix them top to bottom.
282
+
283
+ test('no runtime errors', () =>
284
+ {
285
+ const result = testBot(${botName}).simulate('TargetDummy');
286
+ expect(result.errors).toHaveLength(0);
287
+ });
288
+
289
+ test('beats TargetDummy', () =>
290
+ {
291
+ const result = testBot(${botName}).fight('TargetDummy');
292
+ expect(result.won).toBe(true);
293
+ });
294
+
295
+ test('beats Critter', () =>
296
+ {
297
+ const result = testBot(${botName}).fight('Critter');
298
+ expect(result.won).toBe(true);
299
+ });
300
+
301
+ test('kills TargetDummy within 15 seconds', () =>
302
+ {
303
+ const result = testBot(${botName}).simulate('TargetDummy', {maxTicks: 1500});
304
+ expect(result.won).toBe(true);
305
+ });
306
+
307
+ test('lands at least 40% of missiles', () =>
308
+ {
309
+ const result = testBot(${botName}).simulate('Rookie', {maxTicks: 3000});
310
+ if (result.stats.missilesLaunched > 0)
311
+ {
312
+ expect(result.stats.hitRate).toBeGreaterThan(0.4);
313
+ }
314
+ });
315
+ `;
316
+ }
317
+ function agentsMd(botName) {
318
+ return `# AGENTS.md \u2014 context for AI assistants
319
+
320
+ This is a [Vibemancer](https://vibemancer.com) wizard-bot project. The goal
321
+ is to write a TypeScript bot that wins 1v1 fights against other wizards in
322
+ an 800x800 arena.
323
+
324
+ ## Layout
325
+
326
+ - \`src/bot.ts\` \u2014 the bot. Edit this. Export must be a single PascalCase
327
+ function (currently \`${botName}\`).
328
+ - \`tests/bot.test.ts\` \u2014 vitest suite. Add cases as you discover edge
329
+ cases the bot should handle.
330
+ - \`vibemancer.json\` \u2014 \`{ "bot": "src/bot.ts", "export": "${botName}" }\`.
331
+ Don't move the bot without updating this.
332
+ - \`README.md\` \u2014 a more thorough developer guide. Skim it once.
333
+
334
+ ## How to verify a change
335
+
336
+ \`\`\`bash
337
+ npm test # vitest \u2014 fast, runs every change
338
+ npm run typecheck # catches typos esbuild would silently bundle
339
+ npm run fight # round-robin against all 29 built-ins
340
+ npx vibemancer trace --opponent <Bot> # per-tick debug trace
341
+ \`\`\`
342
+
343
+ Treat \`npm test\` as the smallest verifying step. Run it before claiming a
344
+ change is done.
345
+
346
+ ## The bot API
347
+
348
+ Imported from \`@vibemancer/core\`. The bot is called every tick (100 ticks
349
+ per second). It returns an action object \u2014 typically built by helpers:
350
+
351
+ - **Movement:** \`move(dx, dy)\` \u2014 direction vector (NOT a target). Always
352
+ return *something* even on cooldown \u2014 bare \`move(0, 0)\` if you have
353
+ nothing else to do.
354
+ - **Spells (only one per cast):**
355
+ - \`missile({damage, speed, duration, turnRate}, ai, angle)\` \u2014 fire a
356
+ projectile. Range = speed \xD7 duration. Higher damage = longer cast time.
357
+ - \`shield()\` \u2014 channel a shield. Strongest at start (90% block),
358
+ decays. Cancel early to free up GCD.
359
+ - \`blink(x, y)\` \u2014 teleport to **absolute position** (NOT a direction).
360
+ Range cap is 300; further targets are clamped along the line.
361
+ - **Hooks** (read state):
362
+ - \`usePosition()\` \u2014 your current \`{x, y}\`.
363
+ - \`useEnemy()\` \u2014 opponent's last-known state.
364
+ - \`useThreats()\` \u2014 incoming missiles you might want to dodge/shield.
365
+ - \`useTicksUntilReady()\` \u2014 GCD remaining. Cast attempts during GCD are
366
+ silently ignored \u2014 gate on this.
367
+ - \`useParam(name, default, {min, max, step})\` \u2014 declare a tunable; the
368
+ optimizer's coordinate-descent will sweep it.
369
+
370
+ Read the full surface: \`packages/core/src/index-browser.ts\` if you have
371
+ the monorepo, or jump-to-definition from any \`@vibemancer/core\` import.
372
+
373
+ ## Common pitfalls
374
+
375
+ - All angles are in **degrees**, not radians. Use \`Math.atan2(dy, dx) * (180 / Math.PI)\`.
376
+ - All time values are in **ticks**, not seconds. 100 ticks = 1 second.
377
+ - \`getLeadPosition()\` and \`fitMissileToBudget()\` return **null** when no
378
+ solution exists \u2014 handle that, don't assume non-null.
379
+ - Returning nothing freezes the bot for that tick. Always return at least
380
+ \`move(0, 0)\`.
381
+ - The bot runs in a sandboxed Web Worker / isolated-vm \u2014 no \`fetch\`,
382
+ no DOM, no \`Math.random()\` (use the seeded RNG passed via hooks if you
383
+ need randomness).
384
+
385
+ ## Don't
386
+
387
+ - Don't add new top-level files outside \`src/\` and \`tests/\` without a
388
+ reason \u2014 the dev server scans \`src/\` for bots.
389
+ - Don't add npm dependencies that pull in Node-only modules. Bots are
390
+ bundled for browser/sandbox execution.
391
+ - Don't mutate globals or the prototype chain \u2014 the sandbox freezes them
392
+ and crashes will be silent.
393
+
394
+ ## Publishing
395
+
396
+ Once \`npm run fight\` shows acceptable wins:
397
+
398
+ \`\`\`bash
399
+ npx vibemancer upload
400
+ \`\`\`
401
+
402
+ This signs in (Google), compiles, and ships the bundle to vibemancer.com.
403
+ The matchmaker auto-pairs your wizard against other active uploads. Watch
404
+ the leaderboard / your wizard's match history for results, find a
405
+ weakness, edit, re-upload.
406
+ `;
407
+ }
408
+ function gitignore() {
409
+ return `node_modules/
410
+ dist/
411
+ .vibemancer/
412
+ *.tgz
413
+ `;
414
+ }
415
+ function scaffoldProject(projectDir, projectName, botName) {
416
+ const files = [
417
+ ["package.json", packageJson(projectName)],
418
+ ["tsconfig.json", tsconfigJson()],
419
+ ["vibemancer.json", vibemancerJson(botName)],
420
+ ["src/bot.ts", botTemplate(botName)],
421
+ ["tests/bot.test.ts", botTestTemplate(botName)],
422
+ ["README.md", readmeMd(botName)],
423
+ ["AGENTS.md", agentsMd(botName)],
424
+ [".gitignore", gitignore()]
425
+ ];
426
+ fs.mkdirSync(path.join(projectDir, "src"), { recursive: true });
427
+ fs.mkdirSync(path.join(projectDir, "tests"), { recursive: true });
428
+ for (const [filePath, content] of files) {
429
+ const fullPath = path.join(projectDir, filePath);
430
+ fs.writeFileSync(fullPath, content);
431
+ }
432
+ }
433
+
434
+ // src/index.ts
435
+ var args = process.argv.slice(2);
436
+ function printHelp() {
437
+ console.log(`
438
+ create-vibemancer - Create a new Vibemancer wizard bot project
439
+
440
+ Usage:
441
+ npx create-vibemancer <project-name> [--name <BotName>]
442
+
443
+ Options:
444
+ --name <name> Bot export name (default: derived from project name)
445
+
446
+ Examples:
447
+ npx create-vibemancer my-wizard
448
+ npx create-vibemancer fire-mage --name FireMage
449
+ `);
450
+ }
451
+ function parseArgs() {
452
+ const projectName = args.find((a) => !a.startsWith("-"));
453
+ if (!projectName) {
454
+ printHelp();
455
+ process.exit(1);
456
+ }
457
+ if (!isValidProjectName(projectName)) {
458
+ console.error(`Error: "${projectName}" is not a valid project name.`);
459
+ console.error("Project names must start and end with alphanumeric characters");
460
+ console.error("and may contain letters, digits, dots, hyphens, and underscores.");
461
+ process.exit(1);
462
+ }
463
+ const nameIdx = args.indexOf("--name");
464
+ if (nameIdx !== -1 && !args[nameIdx + 1]) {
465
+ console.error("Error: --name requires a value.");
466
+ console.error("Example: npx create-vibemancer my-bot --name MyBot");
467
+ process.exit(1);
468
+ }
469
+ const botName = nameIdx !== -1 && args[nameIdx + 1] ? args[nameIdx + 1] : toPascalCase(projectName);
470
+ if (!isValidIdentifier(botName)) {
471
+ console.error(`Error: "${botName}" is not a valid JavaScript identifier.`);
472
+ console.error("Bot names must start with a letter, underscore, or $");
473
+ console.error("and contain only letters, digits, underscores, and $.");
474
+ process.exit(1);
475
+ }
476
+ return { projectName, botName };
477
+ }
478
+ function main() {
479
+ const { projectName, botName } = parseArgs();
480
+ const projectDir = path2.resolve(projectName);
481
+ if (fs2.existsSync(projectDir)) {
482
+ console.error(`Error: Directory "${projectName}" already exists.`);
483
+ process.exit(1);
484
+ }
485
+ console.log(`
486
+ Creating Vibemancer project: ${projectName}`);
487
+ console.log(`Bot name: ${botName}
488
+ `);
489
+ try {
490
+ scaffoldProject(projectDir, projectName, botName);
491
+ const files = ["package.json", "tsconfig.json", "vibemancer.json", "src/bot.ts", "tests/bot.test.ts", "README.md", ".gitignore"];
492
+ for (const filePath of files) {
493
+ console.log(` Created ${filePath}`);
494
+ }
495
+ } catch (err) {
496
+ console.error(`
497
+ Error creating project: ${err instanceof Error ? err.message : String(err)}`);
498
+ try {
499
+ fs2.rmSync(projectDir, { recursive: true, force: true });
500
+ } catch {
501
+ }
502
+ process.exit(1);
503
+ }
504
+ console.log(`
505
+ Done! To get started:
506
+
507
+ cd ${projectName}
508
+ npm install
509
+ npm test (verify everything works)
510
+ npm run dev (start dev server + open browser)
511
+
512
+ Other commands:
513
+ npm run fight Fight against all 29 built-in bots
514
+ npm run trace Per-tick debug trace (see exactly what your bot does)
515
+ npm run optimize Auto-tune bot parameters
516
+
517
+ Edit src/bot.ts to change your bot. Refresh the browser to see changes.
518
+ `);
519
+ }
520
+ main();
521
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/scaffolder.ts"],"sourcesContent":["/**\r\n * create-vibemancer\r\n *\r\n * Scaffolds a new Vibemancer wizard bot project.\r\n *\r\n * Usage:\r\n * npx create-vibemancer my-wizard\r\n * npx create-vibemancer my-wizard --name MyWizard\r\n */\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport {\r\n\ttoPascalCase,\r\n\tisValidIdentifier,\r\n\tisValidProjectName,\r\n\tscaffoldProject,\r\n} from './scaffolder.js';\r\n\r\nconst args = process.argv.slice(2);\r\n\r\nfunction printHelp(): void\r\n{\r\n\tconsole.log(`\r\ncreate-vibemancer - Create a new Vibemancer wizard bot project\r\n\r\nUsage:\r\n npx create-vibemancer <project-name> [--name <BotName>]\r\n\r\nOptions:\r\n --name <name> Bot export name (default: derived from project name)\r\n\r\nExamples:\r\n npx create-vibemancer my-wizard\r\n npx create-vibemancer fire-mage --name FireMage\r\n`);\r\n}\r\n\r\nfunction parseArgs(): {projectName: string; botName: string}\r\n{\r\n\tconst projectName = args.find((a) => !a.startsWith('-'));\r\n\tif (!projectName)\r\n\t{\r\n\t\tprintHelp();\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\tif (!isValidProjectName(projectName))\r\n\t{\r\n\t\tconsole.error(`Error: \"${projectName}\" is not a valid project name.`);\r\n\t\tconsole.error('Project names must start and end with alphanumeric characters');\r\n\t\tconsole.error('and may contain letters, digits, dots, hyphens, and underscores.');\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\tconst nameIdx = args.indexOf('--name');\r\n\tif (nameIdx !== -1 && !args[nameIdx + 1])\r\n\t{\r\n\t\tconsole.error('Error: --name requires a value.');\r\n\t\tconsole.error('Example: npx create-vibemancer my-bot --name MyBot');\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\tconst botName = nameIdx !== -1 && args[nameIdx + 1]\r\n\t\t? args[nameIdx + 1]\r\n\t\t: toPascalCase(projectName);\r\n\r\n\tif (!isValidIdentifier(botName))\r\n\t{\r\n\t\tconsole.error(`Error: \"${botName}\" is not a valid JavaScript identifier.`);\r\n\t\tconsole.error('Bot names must start with a letter, underscore, or $');\r\n\t\tconsole.error('and contain only letters, digits, underscores, and $.');\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\treturn {projectName, botName};\r\n}\r\n\r\nfunction main(): void\r\n{\r\n\tconst {projectName, botName} = parseArgs();\r\n\tconst projectDir = path.resolve(projectName);\r\n\r\n\tif (fs.existsSync(projectDir))\r\n\t{\r\n\t\tconsole.error(`Error: Directory \"${projectName}\" already exists.`);\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\tconsole.log(`\\nCreating Vibemancer project: ${projectName}`);\r\n\tconsole.log(`Bot name: ${botName}\\n`);\r\n\r\n\ttry\r\n\t{\r\n\t\tscaffoldProject(projectDir, projectName, botName);\r\n\r\n\t\tconst files = ['package.json', 'tsconfig.json', 'vibemancer.json', 'src/bot.ts', 'tests/bot.test.ts', 'README.md', '.gitignore'];\r\n\t\tfor (const filePath of files)\r\n\t\t{\r\n\t\t\tconsole.log(` Created ${filePath}`);\r\n\t\t}\r\n\t}\r\n\tcatch(err)\r\n\t{\r\n\t\tconsole.error(`\\nError creating project: ${err instanceof Error ? err.message : String(err)}`);\r\n\t\ttry\r\n\t\t{\r\n\t\t\tfs.rmSync(projectDir, {recursive: true, force: true});\r\n\t\t}\r\n\t\tcatch\r\n\t\t{\r\n\t\t\t// Best-effort cleanup\r\n\t\t}\r\n\t\tprocess.exit(1);\r\n\t}\r\n\r\n\tconsole.log(`\r\nDone! To get started:\r\n\r\n cd ${projectName}\r\n npm install\r\n npm test (verify everything works)\r\n npm run dev (start dev server + open browser)\r\n\r\nOther commands:\r\n npm run fight Fight against all 29 built-in bots\r\n npm run trace Per-tick debug trace (see exactly what your bot does)\r\n npm run optimize Auto-tune bot parameters\r\n\r\nEdit src/bot.ts to change your bot. Refresh the browser to see changes.\r\n`);\r\n}\r\n\r\nmain();\r\n","/**\r\n * Scaffolder - Core logic for creating a Vibemancer project.\r\n *\r\n * Pure functions for validation, naming, and template generation.\r\n * Separated from index.ts for testability.\r\n */\r\n\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\n\r\nexport function toPascalCase(str: string): string\r\n{\r\n\tlet result = str\r\n\t\t.replace(/[-_]+/g, ' ')\r\n\t\t.replace(/\\b\\w/g, (c) => c.toUpperCase())\r\n\t\t.replace(/\\s+/g, '');\r\n\r\n\t// Ensure starts with a letter (prepend underscore if starts with digit)\r\n\tif (/^\\d/.test(result))\r\n\t{\r\n\t\tresult = '_' + result;\r\n\t}\r\n\r\n\treturn result;\r\n}\r\n\r\n/**\r\n * Validate that a string is a valid JavaScript identifier.\r\n */\r\nexport function isValidIdentifier(name: string): boolean\r\n{\r\n\treturn /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);\r\n}\r\n\r\n/**\r\n * Validate that a project name is safe for use as a directory name.\r\n */\r\nexport function isValidProjectName(name: string): boolean\r\n{\r\n\treturn /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(name);\r\n}\r\n\r\nexport function packageJson(projectName: string): string\r\n{\r\n\treturn JSON.stringify({\r\n\t\tname: projectName,\r\n\t\tversion: '0.1.0',\r\n\t\tprivate: true,\r\n\t\ttype: 'module',\r\n\t\tscripts: {\r\n\t\t\tdev: 'vibemancer dev',\r\n\t\t\ttest: 'vibemancer test',\r\n\t\t\ttypecheck: 'tsc --noEmit',\r\n\t\t\tfight: 'vibemancer fight',\r\n\t\t\ttrace: 'vibemancer trace --opponent TargetDummy',\r\n\t\t\toptimize: 'vibemancer optimize',\r\n\t\t\tbuild: 'vibemancer build --opponent Battlemage',\r\n\t\t},\r\n\t\tdependencies: {\r\n\t\t\t'@vibemancer/core': '~0.1.0',\r\n\t\t},\r\n\t\tdevDependencies: {\r\n\t\t\t'vibemancer': '~0.1.0',\r\n\t\t\t'typescript': '^5.9.0',\r\n\t\t\t'vitest': '^3.0.0',\r\n\t\t},\r\n\t}, null, '\\t') + '\\n';\r\n}\r\n\r\nexport function tsconfigJson(): string\r\n{\r\n\treturn JSON.stringify({\r\n\t\tcompilerOptions: {\r\n\t\t\ttarget: 'ESNext',\r\n\t\t\tmodule: 'ESNext',\r\n\t\t\tlib: ['ESNext'],\r\n\t\t\tskipLibCheck: true,\r\n\t\t\tmoduleResolution: 'Bundler',\r\n\t\t\tstrict: true,\r\n\t\t\tnoUnusedLocals: true,\r\n\t\t\tnoUnusedParameters: true,\r\n\t\t\tisolatedModules: true,\r\n\t\t\tnoEmit: true,\r\n\t\t},\r\n\t\tinclude: ['src', 'tests'],\r\n\t}, null, '\\t') + '\\n';\r\n}\r\n\r\nexport function vibemancerJson(botName: string): string\r\n{\r\n\treturn JSON.stringify({\r\n\t\tbot: 'src/bot.ts',\r\n\t\texport: botName,\r\n\t}, null, '\\t') + '\\n';\r\n}\r\n\r\nexport function botTemplate(botName: string): string\r\n{\r\n\treturn `import {\r\n\tusePosition, useEnemy, useTicksUntilReady, useThreats,\r\n\tmissile, shield, move,\r\n\t// More actions (all importable from '@vibemancer/core'):\r\n\t// blink, cancel, idle,\r\n\t// More hooks:\r\n\t// useHealth, useBlinkCooldown, useStatus, useVelocity,\r\n\t// useCastProgress, useMyProjectiles, useMyThreatsToEnemy,\r\n\t// useClosestThreat, useShieldStrength, useCastingSpell,\r\n\t// useTick, useArenaSize, useDamageDealt, useDamageTaken,\r\n\t// Utility functions:\r\n\t// distanceTo, angleTo, directionTo, directionAway,\r\n\t// getLeadPosition, fitMissileToBudget, getMissileCastTime,\r\n\t// predictPosition, interceptAngle, clampPositionToArena,\r\n\t// Persistence hooks (React-style, state preserved across ticks):\r\n\t// useState, useRef, useMemo, useEffect\r\n\t// Parameter tuning (for optimizer):\r\n\t// useParam\r\n} from '@vibemancer/core';\r\n\r\n/**\r\n * ${botName} - Your custom wizard bot!\r\n *\r\n * === HOW IT WORKS ===\r\n * Your function is called EVERY TICK (100x per second). Each call you:\r\n * 1. Read game state using hooks (usePosition, useEnemy, etc.)\r\n * 2. Return ONE action (missile, shield, blink, move, cancel, or idle)\r\n *\r\n * The function must ALWAYS return an action. During cooldowns (GCD), return\r\n * move() to keep moving — casting spells during GCD is silently ignored.\r\n * Check useTicksUntilReady() > 0 to know when you're on cooldown.\r\n *\r\n * === GAME RULES ===\r\n * Arena: 800x800, positions clamped to [5, 795]\r\n * Health: 60 HP, wizard dies at 0\r\n * Timing: 100 ticks = 1 second, match lasts 30,000 ticks (5 min)\r\n * Movement: 1 unit/tick (100 u/s), 50% speed while casting, 0% while shielding\r\n * GCD: 100 ticks (1s) lockout after any spell completes or is canceled\r\n *\r\n * === SPELLS ===\r\n * missile(config, ai, direction)\r\n * config: { damage, speed, duration, turnRate }\r\n * - damage: HP removed on hit (1-60). Hitbox radius = 2 + 0.1 x damage.\r\n * - speed: units/tick (min 1.5, player moves at 1). Range = speed x duration.\r\n * - duration: ticks the missile lives (min 10). Longer = more range but slower cast.\r\n * - turnRate: degrees/tick of homing (0 = straight, 3 = moderate, 5+ = strong).\r\n * ai: called every tick to steer -> return { turnToward: {x,y} } or {} for straight.\r\n * direction: launch angle in degrees (0=right, 90=down, 180=left, 270=up).\r\n * Cast time scales with all stats. Repeated similar missiles cast 20% faster (warmup).\r\n * Chain .move(dx, dy) to walk at 50% speed while casting.\r\n *\r\n * shield()\r\n * Channeled. Blocks 90% initially, decays 20%/sec, minimum 30%.\r\n * 0.2s cast time. Immobile while channeling. Cancel with cancel().\r\n *\r\n * blink(x, y) — Teleport to ABSOLUTE position (not direction). Max 300 units.\r\n * move(dx, dy) — Move in DIRECTION (not position). Auto-normalized to max speed.\r\n * cancel() — Cancel current cast/channel. Chain .move() to dodge simultaneously.\r\n * idle() — Do nothing this tick.\r\n *\r\n * === KEY TYPES ===\r\n * useEnemy() returns: { position: {x, y}, velocity: {x, y}, health, state, ... }\r\n * useThreats() returns array of:\r\n * { ticksToImpact, damage, position, velocity, canDodge, bestDodgeDirection, ... }\r\n * fitMissileToBudget(budget, opts) — finds optimal missile config for a cast-time budget\r\n * getLeadPosition(pos, vel, time) — predicts where a moving target will be\r\n *\r\n * All imports have full JSDoc — hover or jump-to-definition to see docs.\r\n */\r\nexport function ${botName}()\r\n{\r\n\tconst myPos = usePosition();\r\n\tconst enemy = useEnemy();\r\n\tconst ready = useTicksUntilReady();\r\n\tconst threats = useThreats();\r\n\r\n\t// Direction toward enemy (used for movement and aiming)\r\n\tconst dx = enemy.position.x - myPos.x;\r\n\tconst dy = enemy.position.y - myPos.y;\r\n\tconst dist = Math.sqrt(dx * dx + dy * dy);\r\n\tconst angle = Math.atan2(dy, dx) * (180 / Math.PI);\r\n\r\n\t// While on cooldown, move toward the enemy\r\n\t// move() takes a direction vector, not an absolute position\r\n\tif (ready > 0) return move(dx, dy);\r\n\r\n\t// Shield if a missile is about to hit (within 30 ticks = 0.3s)\r\n\tconst closestThreat = threats[0];\r\n\tif (closestThreat && closestThreat.ticksToImpact < 30)\r\n\t{\r\n\t\treturn shield();\r\n\t}\r\n\r\n\t// Fire a homing missile at the enemy\r\n\t// Duration scales with distance so missiles always have enough range\r\n\t// Chain .move() to walk toward enemy at 50% speed while casting\r\n\treturn missile(\r\n\t\t{damage: 15, speed: 6, duration: Math.max(60, Math.ceil(dist / 6) + 20), turnRate: 3},\r\n\t\t({worldState}) =>\r\n\t\t{\r\n\t\t\t// Missile AI: called every tick to steer the missile\r\n\t\t\t// Return { turnToward: position } to home, or {} to fly straight\r\n\t\t\tconst target = worldState.enemies[0];\r\n\t\t\tif (!target) return {};\r\n\t\t\treturn {turnToward: target.position};\r\n\t\t},\r\n\t\tangle, // Launch angle in degrees toward enemy\r\n\t).move(dx, dy);\r\n}\r\n`;\r\n}\r\n\r\nexport function readmeMd(botName: string): string\r\n{\r\n\treturn `# ${botName} — Vibemancer Bot\r\n\r\nThis is a Vibemancer wizard bot project. The bot fights 1v1 against other wizard bots in an 800x800 arena.\r\n\r\n## Quick Start\r\n\r\n\\`\\`\\`bash\r\nnpm test # Verify everything works\r\nnpm run dev # Start dev server + open browser (refresh to see changes)\r\nnpm run fight # Fight against all 29 built-in bots\r\nnpm run trace # Per-tick debug trace against TargetDummy\r\nnpm run typecheck # Check for type errors (catches bugs esbuild misses)\r\nnpm run optimize # Auto-tune useParam() values via optimizer\r\n\\`\\`\\`\r\n\r\nTo trace/fight a specific opponent:\r\n\\`\\`\\`bash\r\nnpx vibemancer trace --opponent Battlemage\r\nnpx vibemancer fight --opponent Nightblade\r\nnpx vibemancer bots # list all bots with descriptions\r\nnpx vibemancer bots Battlemage # details about a specific bot\r\n\\`\\`\\`\r\n\r\n## Project Structure\r\n\r\n- \\`src/bot.ts\\` — Bot source code (edit this!)\r\n- \\`tests/bot.test.ts\\` — Automated tests for your bot\r\n- \\`vibemancer.json\\` — Config: \\`{ \"bot\": \"src/bot.ts\", \"export\": \"${botName}\" }\\`\r\n\r\n## Development Workflow\r\n\r\n1. Edit \\`src/bot.ts\\`\r\n2. Run \\`npm run typecheck\\` to catch type errors early\r\n3. Run \\`npm test\\` to check your bot doesn't crash and beats weak bots\r\n4. Run \\`npm run fight\\` to see how you rank against all 29 built-in bots\r\n5. Run \\`npm run trace\\` to see per-tick events when something isn't working\r\n6. Run \\`npm run optimize\\` to auto-tune useParam() values\r\n\r\nFor the browser viewer: run \\`npm run dev\\`, then refresh the browser after editing your bot.\r\n\r\n## Difficulty Progression\r\n\r\nStart by beating the weakest bots and work your way up:\r\n\r\n| Milestone | Bots to Beat | What You Need |\r\n|-----------|-------------|---------------|\r\n| Beginner | TargetDummy, Critter | Basic missile firing |\r\n| Easy | Rookie, Hogger, Bonemancer | Homing missiles + movement |\r\n| Medium | Turtle, Sentinel, Flamecaller | Shielding + dodge awareness |\r\n| Hard | Battlemage (#11), Stormchaser | Cast timing + positioning |\r\n| Expert | Archmage, Stormforger | Adaptive missiles + vulnerability punish |\r\n| Master | Nightblade (#29) | Everything at once |\r\n\r\n## Debugging\r\n\r\nWhen your bot isn't working:\r\n1. \\`npm test\\` — check for runtime errors (captured in test results)\r\n2. \\`vibemancer trace --opponent TargetDummy\\` — watch exactly what happens tick by tick\r\n3. Look at the trace stats: hit rate, shield usage, idle time\r\n4. Look for ERROR and WARNING events in the trace output\r\n\r\n## Common Pitfalls\r\n\r\n- \\`move(dx, dy)\\` is a **direction vector**, NOT a target position\r\n- \\`blink(x, y)\\` is an **absolute position**, NOT a direction\r\n- Calling \\`missile()\\` during GCD is **silently ignored** — check \\`useTicksUntilReady()\\` first\r\n- Missile range = speed x duration. If range < distance, missiles expire before reaching the enemy\r\n- All angles are in **degrees** (not radians). Use: \\`Math.atan2(dy, dx) * (180 / Math.PI)\\`\r\n- All time values are in **ticks** (100 ticks = 1 second)\r\n- Your bot runs every tick — always return an action, even during cooldowns (\\`move()\\`)\r\n- \\`fitMissileToBudget()\\` and \\`getLeadPosition()\\` return **null** when no solution exists\r\n\r\n## Built-in Bots (29 total, ranked weakest -> strongest)\r\n\r\nTargetDummy, Critter, Bonemancer, Hogger, Rookie, Turtle, Sentinel, Flamecaller,\r\nGolem, Stormchaser, Battlemage, Spellspinner, Doombringer, Lich, Stormcaller,\r\nSpellbinder, Shadowblade, Pyromancer, Warmage, Spelltracer, Archmage, Spellseeker,\r\nStormforger, Spellweaver, Spellshot, Voidblade, Infernalist, Archlich, Nightblade\r\n\r\nRun \\`vibemancer bots\\` to see descriptions, or \\`vibemancer bots Battlemage\\` for details.\r\n\r\n## Strategy Tips\r\n\r\n- Shield is strongest at the start (90% block) — cancel early to minimize GCD waste\r\n- Repeated similar missiles cast 20% faster (warmup). Switching styles costs 20%\r\n- Higher damage = longer cast time, but DPS always increases with damage\r\n- Homing missiles (turnRate > 0) are harder to dodge but cost more cast time\r\n- Straight missiles (turnRate: 0) are fast to cast. Use \\`getLeadPosition()\\` to aim\r\n- Use \\`useThreats()\\` to decide: dodge (best), shield (good), or eat the hit (risky)\r\n`;\r\n}\r\n\r\nexport function botTestTemplate(botName: string): string\r\n{\r\n\treturn `import {expect, test} from 'vitest';\r\nimport {testBot} from '@vibemancer/core';\r\nimport {${botName}} from '../src/bot';\r\n\r\n// Run with: npm test\r\n// These tests are ordered by difficulty — fix them top to bottom.\r\n\r\ntest('no runtime errors', () =>\r\n{\r\n\tconst result = testBot(${botName}).simulate('TargetDummy');\r\n\texpect(result.errors).toHaveLength(0);\r\n});\r\n\r\ntest('beats TargetDummy', () =>\r\n{\r\n\tconst result = testBot(${botName}).fight('TargetDummy');\r\n\texpect(result.won).toBe(true);\r\n});\r\n\r\ntest('beats Critter', () =>\r\n{\r\n\tconst result = testBot(${botName}).fight('Critter');\r\n\texpect(result.won).toBe(true);\r\n});\r\n\r\ntest('kills TargetDummy within 15 seconds', () =>\r\n{\r\n\tconst result = testBot(${botName}).simulate('TargetDummy', {maxTicks: 1500});\r\n\texpect(result.won).toBe(true);\r\n});\r\n\r\ntest('lands at least 40% of missiles', () =>\r\n{\r\n\tconst result = testBot(${botName}).simulate('Rookie', {maxTicks: 3000});\r\n\tif (result.stats.missilesLaunched > 0)\r\n\t{\r\n\t\texpect(result.stats.hitRate).toBeGreaterThan(0.4);\r\n\t}\r\n});\r\n`;\r\n}\r\n\r\nexport function agentsMd(botName: string): string\r\n{\r\n\treturn `# AGENTS.md — context for AI assistants\r\n\r\nThis is a [Vibemancer](https://vibemancer.com) wizard-bot project. The goal\r\nis to write a TypeScript bot that wins 1v1 fights against other wizards in\r\nan 800x800 arena.\r\n\r\n## Layout\r\n\r\n- \\`src/bot.ts\\` — the bot. Edit this. Export must be a single PascalCase\r\n function (currently \\`${botName}\\`).\r\n- \\`tests/bot.test.ts\\` — vitest suite. Add cases as you discover edge\r\n cases the bot should handle.\r\n- \\`vibemancer.json\\` — \\`{ \"bot\": \"src/bot.ts\", \"export\": \"${botName}\" }\\`.\r\n Don't move the bot without updating this.\r\n- \\`README.md\\` — a more thorough developer guide. Skim it once.\r\n\r\n## How to verify a change\r\n\r\n\\`\\`\\`bash\r\nnpm test # vitest — fast, runs every change\r\nnpm run typecheck # catches typos esbuild would silently bundle\r\nnpm run fight # round-robin against all 29 built-ins\r\nnpx vibemancer trace --opponent <Bot> # per-tick debug trace\r\n\\`\\`\\`\r\n\r\nTreat \\`npm test\\` as the smallest verifying step. Run it before claiming a\r\nchange is done.\r\n\r\n## The bot API\r\n\r\nImported from \\`@vibemancer/core\\`. The bot is called every tick (100 ticks\r\nper second). It returns an action object — typically built by helpers:\r\n\r\n- **Movement:** \\`move(dx, dy)\\` — direction vector (NOT a target). Always\r\n return *something* even on cooldown — bare \\`move(0, 0)\\` if you have\r\n nothing else to do.\r\n- **Spells (only one per cast):**\r\n - \\`missile({damage, speed, duration, turnRate}, ai, angle)\\` — fire a\r\n projectile. Range = speed × duration. Higher damage = longer cast time.\r\n - \\`shield()\\` — channel a shield. Strongest at start (90% block),\r\n decays. Cancel early to free up GCD.\r\n - \\`blink(x, y)\\` — teleport to **absolute position** (NOT a direction).\r\n Range cap is 300; further targets are clamped along the line.\r\n- **Hooks** (read state):\r\n - \\`usePosition()\\` — your current \\`{x, y}\\`.\r\n - \\`useEnemy()\\` — opponent's last-known state.\r\n - \\`useThreats()\\` — incoming missiles you might want to dodge/shield.\r\n - \\`useTicksUntilReady()\\` — GCD remaining. Cast attempts during GCD are\r\n silently ignored — gate on this.\r\n - \\`useParam(name, default, {min, max, step})\\` — declare a tunable; the\r\n optimizer's coordinate-descent will sweep it.\r\n\r\nRead the full surface: \\`packages/core/src/index-browser.ts\\` if you have\r\nthe monorepo, or jump-to-definition from any \\`@vibemancer/core\\` import.\r\n\r\n## Common pitfalls\r\n\r\n- All angles are in **degrees**, not radians. Use \\`Math.atan2(dy, dx) * (180 / Math.PI)\\`.\r\n- All time values are in **ticks**, not seconds. 100 ticks = 1 second.\r\n- \\`getLeadPosition()\\` and \\`fitMissileToBudget()\\` return **null** when no\r\n solution exists — handle that, don't assume non-null.\r\n- Returning nothing freezes the bot for that tick. Always return at least\r\n \\`move(0, 0)\\`.\r\n- The bot runs in a sandboxed Web Worker / isolated-vm — no \\`fetch\\`,\r\n no DOM, no \\`Math.random()\\` (use the seeded RNG passed via hooks if you\r\n need randomness).\r\n\r\n## Don't\r\n\r\n- Don't add new top-level files outside \\`src/\\` and \\`tests/\\` without a\r\n reason — the dev server scans \\`src/\\` for bots.\r\n- Don't add npm dependencies that pull in Node-only modules. Bots are\r\n bundled for browser/sandbox execution.\r\n- Don't mutate globals or the prototype chain — the sandbox freezes them\r\n and crashes will be silent.\r\n\r\n## Publishing\r\n\r\nOnce \\`npm run fight\\` shows acceptable wins:\r\n\r\n\\`\\`\\`bash\r\nnpx vibemancer upload\r\n\\`\\`\\`\r\n\r\nThis signs in (Google), compiles, and ships the bundle to vibemancer.com.\r\nThe matchmaker auto-pairs your wizard against other active uploads. Watch\r\nthe leaderboard / your wizard's match history for results, find a\r\nweakness, edit, re-upload.\r\n`;\r\n}\r\n\r\nexport function gitignore(): string\r\n{\r\n\treturn `node_modules/\r\ndist/\r\n.vibemancer/\r\n*.tgz\r\n`;\r\n}\r\n\r\n/**\r\n * Scaffold a new project directory with all template files.\r\n */\r\nexport function scaffoldProject(projectDir: string, projectName: string, botName: string): void\r\n{\r\n\tconst files: [string, string][] = [\r\n\t\t['package.json', packageJson(projectName)],\r\n\t\t['tsconfig.json', tsconfigJson()],\r\n\t\t['vibemancer.json', vibemancerJson(botName)],\r\n\t\t['src/bot.ts', botTemplate(botName)],\r\n\t\t['tests/bot.test.ts', botTestTemplate(botName)],\r\n\t\t['README.md', readmeMd(botName)],\r\n\t\t['AGENTS.md', agentsMd(botName)],\r\n\t\t['.gitignore', gitignore()],\r\n\t];\r\n\r\n\tfs.mkdirSync(path.join(projectDir, 'src'), {recursive: true});\r\n\tfs.mkdirSync(path.join(projectDir, 'tests'), {recursive: true});\r\n\r\n\tfor (const [filePath, content] of files)\r\n\t{\r\n\t\tconst fullPath = path.join(projectDir, filePath);\r\n\t\tfs.writeFileSync(fullPath, content);\r\n\t}\r\n}\r\n"],"mappings":"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","names":["fs","path","path","fs"]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-vibemancer",
3
+ "version": "0.1.0",
4
+ "description": "Create a new Vibemancer wizard bot project",
5
+ "type": "module",
6
+ "author": "Low Entry",
7
+ "license": "MIT",
8
+ "homepage": "https://vibemancer.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/LowEntry/lowentry-app-vibemancer.git"
12
+ },
13
+ "bin": {
14
+ "create-vibemancer": "./dist/index.js"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "devDependencies": {
21
+ "@stylistic/eslint-plugin": "^5.7.1",
22
+ "@types/node": "^25.3.0",
23
+ "eslint": "^9.39.2",
24
+ "tsup": "^8.5.1",
25
+ "typescript": "^5.9.3",
26
+ "typescript-eslint": "^8.53.1",
27
+ "vitest": "^4.0.18"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "lint": "eslint .",
32
+ "format": "eslint . --fix",
33
+ "type-check": "tsc --noEmit",
34
+ "test": "pnpm run type-check && pnpm run lint && vitest run",
35
+ "clean": "rm -rf dist"
36
+ }
37
+ }