create-vibemancer 1.0.2 → 1.0.4

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/dist/index.js CHANGED
@@ -54,7 +54,7 @@ function packageJson(projectName, scaffolderVersion = SCAFFOLDER_VERSION) {
54
54
  test: "vibemancer test",
55
55
  typecheck: "tsc --noEmit",
56
56
  fight: "vibemancer fight",
57
- trace: "vibemancer trace",
57
+ trace: "vibemancer trace --opponent TargetDummy",
58
58
  optimize: "vibemancer optimize",
59
59
  upload: "vibemancer upload",
60
60
  pull: "vibemancer pull",
@@ -130,23 +130,34 @@ function botTemplate(botName) {
130
130
  * Check useTicksUntilReady() > 0 to know when you're on cooldown.
131
131
  *
132
132
  * === GAME RULES ===
133
- * Arena: 800x800, positions clamped to [5, 795]
133
+ * Arena: 860x860 total. The PLAYFIELD is [30, 830] \u2014 the outer 30 units are LAVA.
134
+ * LAVA: Standing in it KILLS YOU. Positions are NOT clamped to the playfield: you
135
+ * can walk into lava, and knockback can push you in. Many losses are this.
134
136
  * Health: 60 HP, wizard dies at 0
135
137
  * Timing: 100 ticks = 1 second, match lasts 30,000 ticks (5 min)
136
- * Movement: 1 unit/tick (100 u/s), 50% speed while casting, 0% while shielding
138
+ * Movement: 1.5 units/tick (150 u/s), 33% speed while casting, 0% while shielding
139
+ * Knockback: missiles above 10 damage shove the target. Yours push them; theirs push you
140
+ * \u2014 possibly into the lava.
137
141
  * GCD: 100 ticks (1s) lockout after any spell completes or is canceled
142
+ * Thinking: 45 SECONDS of total thinking time per fight (all ten matches share it).
143
+ * Spend it all and your bot stops being asked for the rest of the fight \u2014 it
144
+ * just stands there. That is NOT a crash: a slow bot loses fights, it does
145
+ * not get removed from the ladder. There is no per-tick limit, so one slow
146
+ * tick costs you nothing but the time itself. You will not come close to
147
+ * this by accident \u2014 the worst built-in bot uses about 5.6s of its 45.
138
148
  *
139
149
  * === SPELLS ===
140
150
  * missile(config, ai, direction)
141
151
  * config: { damage, speed, duration, turnRate }
142
152
  * - damage: HP removed on hit (1-60). Hitbox radius = 2 + 0.1 x damage.
143
- * - speed: units/tick (min 1.5, player moves at 1). Range = speed x duration.
153
+ * - speed: units/tick (min 1.5, and a wizard moves at 1.5 too \u2014 so a minimum-speed
154
+ * missile does NOT outrun a fleeing target). Range = speed x duration.
144
155
  * - duration: ticks the missile lives (min 10). Longer = more range but slower cast.
145
156
  * - turnRate: degrees/tick of homing (0 = straight, 3 = moderate, 5+ = strong).
146
157
  * ai: hooks-style function called every tick to steer. Use getMissileContext() to read state, return turnToward(x,y), turnToAngle(deg), or flyStraight().
147
158
  * direction: launch angle in degrees (0=right, 90=down, 180=left, 270=up).
148
159
  * Cast time scales with all stats. Repeated similar missiles cast 20% faster (warmup).
149
- * Chain .move(dx, dy) to walk at 50% speed while casting.
160
+ * Chain .move(dx, dy) to walk at 33% speed while casting.
150
161
  *
151
162
  * shield()
152
163
  * Channeled. Blocks 90% initially, decays 20%/sec, minimum 30%.
@@ -159,8 +170,10 @@ function botTemplate(botName) {
159
170
  *
160
171
  * === KEY TYPES ===
161
172
  * useEnemy() returns: { position: {x, y}, velocity: {x, y}, health, state, ... }
162
- * useThreats() returns array of:
163
- * { ticksToImpact, damage, position, velocity, canDodge, bestDodgeDirection, ... }
173
+ * useThreats() returns array of AnalyzedThreat:
174
+ * { id, projectile, ticksToImpact, willHit }
175
+ * - projectile carries the incoming missile's own position/velocity/damage.
176
+ * - willHit is the one to branch on: a threat that misses needs no reaction.
164
177
  * fitMissileToBudget(budget, opts) \u2014 finds optimal missile config for a cast-time budget
165
178
  * getLeadPosition(pos, vel, time) \u2014 predicts where a moving target will be
166
179
  *
@@ -192,7 +205,7 @@ export function ${botName}()
192
205
 
193
206
  // Fire a homing missile at the enemy
194
207
  // Duration scales with distance so missiles always have enough range
195
- // Chain .move() to walk toward enemy at 50% speed while casting
208
+ // Chain .move() to walk toward enemy at 33% speed while casting
196
209
  return missile(
197
210
  {damage: 25, speed: 6, duration: Math.max(60, Math.ceil(dist / 6) + 20), turnRate: 3},
198
211
  () =>
@@ -212,7 +225,7 @@ export function ${botName}()
212
225
  function readmeMd(botName) {
213
226
  return `# ${botName} \u2014 Vibemancer Bot
214
227
 
215
- This is a Vibemancer wizard bot project. The bot fights 1v1 against other wizard bots in an 800x800 arena.
228
+ This is a Vibemancer wizard bot project. The bot fights 1v1 against other wizard bots in an 860x860 arena whose outer 30 units are instant-death lava (playfield [30, 830]).
216
229
 
217
230
  ## Quick Start
218
231
 
@@ -351,7 +364,7 @@ function agentsMd(botName) {
351
364
 
352
365
  This is a [Vibemancer](https://vibemancer.com) wizard-bot project. The goal
353
366
  is to write a TypeScript bot that wins 1v1 fights against other wizards in
354
- an 800x800 arena.
367
+ an 860x860 arena whose outer 30 units are instant-death lava (playfield [30, 830]).
355
368
 
356
369
  ## Layout
357
370
 
@@ -466,6 +479,7 @@ function scaffoldProject(projectDir, projectName, botName) {
466
479
  const fullPath = path.join(projectDir, filePath);
467
480
  fs.writeFileSync(fullPath, content);
468
481
  }
482
+ return files.map(([filePath]) => filePath);
469
483
  }
470
484
 
471
485
  // src/index.ts
@@ -524,8 +538,7 @@ Creating Vibemancer project: ${projectName}`);
524
538
  console.log(`Bot name: ${botName}
525
539
  `);
526
540
  try {
527
- scaffoldProject(projectDir, projectName, botName);
528
- const files = ["package.json", "tsconfig.json", "vibemancer.json", "src/bot.ts", "tests/bot.test.ts", "README.md", ".gitignore"];
541
+ const files = scaffoldProject(projectDir, projectName, botName);
529
542
  for (const filePath of files) {
530
543
  console.log(` Created ${filePath}`);
531
544
  }
package/dist/index.js.map CHANGED
@@ -1 +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 function name (also the leaderboard 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\nimport {fileURLToPath} from 'node:url';\r\n\r\n/**\r\n * This package's own version, read from its manifest at load time.\r\n *\r\n * Read rather than hardcoded so a release bump cannot leave scaffolded projects pinned to\r\n * an older toolchain — the exact bug this replaced. Falls back to a permissive range if\r\n * the manifest cannot be found (e.g. an unusual install layout), which is better than\r\n * emitting a version that does not exist.\r\n */\r\nfunction readOwnVersion(): string\r\n{\r\n\ttry\r\n\t{\r\n\t\tconst here = path.dirname(fileURLToPath(import.meta.url));\r\n\t\tfor (const rel of ['../package.json', '../../package.json'])\r\n\t\t{\r\n\t\t\tconst candidate = path.resolve(here, rel);\r\n\t\t\tif (!fs.existsSync(candidate)) continue;\r\n\t\t\tconst parsed: unknown = JSON.parse(fs.readFileSync(candidate, 'utf8'));\r\n\t\t\tif (typeof parsed !== 'object' || parsed === null) continue;\r\n\t\t\tif (!('name' in parsed) || !('version' in parsed)) continue;\r\n\t\t\tconst {name, version} = parsed;\r\n\t\t\tif (name === 'create-vibemancer' && typeof version === 'string') return version;\r\n\t\t}\r\n\t}\r\n\tcatch\r\n\t{\r\n\t\t// fall through\r\n\t}\r\n\treturn '';\r\n}\r\n\r\nexport const SCAFFOLDER_VERSION = readOwnVersion();\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\n/**\r\n * The version range new projects pin their Vibemancer toolchain to.\r\n *\r\n * Derived from THIS package's own version rather than hardcoded: all five packages are\r\n * released in lockstep on a single version, so a scaffolder shipped at 0.2.1 must hand out\r\n * ~0.2.1. It was previously frozen at `~0.1.0` while the published packages had moved to\r\n * 0.2.1 — and `~0.1.0` means `>=0.1.0 <0.2.0`, so it could never reach them. Every new\r\n * player was scaffolded onto a stale core and CLI.\r\n */\r\nexport function toolchainRange(scaffolderVersion: string = SCAFFOLDER_VERSION): string\r\n{\r\n\tif (!scaffolderVersion) return 'latest';\r\n\treturn `~${scaffolderVersion}`;\r\n}\r\n\r\nexport function packageJson(projectName: string, scaffolderVersion: string = SCAFFOLDER_VERSION): string\r\n{\r\n\tconst toolchain = toolchainRange(scaffolderVersion);\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',\r\n\t\t\toptimize: 'vibemancer optimize',\r\n\t\t\tupload: 'vibemancer upload',\r\n\t\t\tpull: 'vibemancer pull',\r\n\t\t\tfeedback: 'vibemancer feedback',\r\n\t\t\tbuild: 'vibemancer build --opponent Battlemage',\r\n\t\t},\r\n\t\tdependencies: {\r\n\t\t\t'@vibemancer/core': toolchain,\r\n\t\t},\r\n\t\tdevDependencies: {\r\n\t\t\t'vibemancer': toolchain,\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\tgetMissileContext, turnToward, flyStraight,\r\n\t// More actions (all importable from '@vibemancer/core'):\r\n\t// blink, cancel, idle, turnToAngle,\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// useRandom,\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: hooks-style function called every tick to steer. Use getMissileContext() to read state, return turnToward(x,y), turnToAngle(deg), or flyStraight().\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). Speed clamped to [-1, 1].\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: 25, speed: 6, duration: Math.max(60, Math.ceil(dist / 6) + 20), turnRate: 3},\r\n\t\t() =>\r\n\t\t{\r\n\t\t\t// Missile AI: hooks-style function called every tick to steer the missile\r\n\t\t\t// Use getMissileContext() to read state, return turnToward/flyStraight\r\n\t\t\tconst ctx = getMissileContext();\r\n\t\t\tconst target = ctx.worldState.enemies[0];\r\n\t\t\tif (!target) return flyStraight();\r\n\t\t\treturn turnToward(target.position.x, target.position.y);\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\nnpm run upload # Upload to vibemancer.com for rated competition\r\nnpm run pull # Download latest source from server (after MCP edits)\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 all pass on the starter bot. Keep them green as you change it, and add\r\n// your own — they are the fastest way to know you have not broken anything.\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 The \\`ai\\` is a hooks-style function \\`() => MissileAction\\` — use\r\n \\`getMissileContext()\\` to read state, return \\`turnToward(x, y)\\`,\r\n \\`turnToAngle(degrees)\\`, or \\`flyStraight()\\`.\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 - \\`useRandom()\\` — seeded PRNG (\\`() => number\\`). Deterministic. Use\r\n instead of \\`Math.random()\\` (blocked in sandbox).\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 \\`useRandom()\\` for a seeded,\r\n deterministic PRNG).\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"]}
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 function name (also the leaderboard 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\t// Print what was ACTUALLY written, not a hardcoded list — the old one claimed seven\r\n\t\t// files while eight were created, silently omitting AGENTS.md.\r\n\t\tconst files = scaffoldProject(projectDir, projectName, botName);\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\nimport {fileURLToPath} from 'node:url';\r\n\r\n/**\r\n * This package's own version, read from its manifest at load time.\r\n *\r\n * Read rather than hardcoded so a release bump cannot leave scaffolded projects pinned to\r\n * an older toolchain — the exact bug this replaced. Falls back to a permissive range if\r\n * the manifest cannot be found (e.g. an unusual install layout), which is better than\r\n * emitting a version that does not exist.\r\n */\r\nfunction readOwnVersion(): string\r\n{\r\n\ttry\r\n\t{\r\n\t\tconst here = path.dirname(fileURLToPath(import.meta.url));\r\n\t\tfor (const rel of ['../package.json', '../../package.json'])\r\n\t\t{\r\n\t\t\tconst candidate = path.resolve(here, rel);\r\n\t\t\tif (!fs.existsSync(candidate)) continue;\r\n\t\t\tconst parsed: unknown = JSON.parse(fs.readFileSync(candidate, 'utf8'));\r\n\t\t\tif (typeof parsed !== 'object' || parsed === null) continue;\r\n\t\t\tif (!('name' in parsed) || !('version' in parsed)) continue;\r\n\t\t\tconst {name, version} = parsed;\r\n\t\t\tif (name === 'create-vibemancer' && typeof version === 'string') return version;\r\n\t\t}\r\n\t}\r\n\tcatch\r\n\t{\r\n\t\t// fall through\r\n\t}\r\n\treturn '';\r\n}\r\n\r\nexport const SCAFFOLDER_VERSION = readOwnVersion();\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\n/**\r\n * The version range new projects pin their Vibemancer toolchain to.\r\n *\r\n * Derived from THIS package's own version rather than hardcoded: all five packages are\r\n * released in lockstep on a single version, so a scaffolder shipped at 0.2.1 must hand out\r\n * ~0.2.1. It was previously frozen at `~0.1.0` while the published packages had moved to\r\n * 0.2.1 — and `~0.1.0` means `>=0.1.0 <0.2.0`, so it could never reach them. Every new\r\n * player was scaffolded onto a stale core and CLI.\r\n */\r\nexport function toolchainRange(scaffolderVersion: string = SCAFFOLDER_VERSION): string\r\n{\r\n\tif (!scaffolderVersion) return 'latest';\r\n\treturn `~${scaffolderVersion}`;\r\n}\r\n\r\nexport function packageJson(projectName: string, scaffolderVersion: string = SCAFFOLDER_VERSION): string\r\n{\r\n\tconst toolchain = toolchainRange(scaffolderVersion);\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\tupload: 'vibemancer upload',\r\n\t\t\tpull: 'vibemancer pull',\r\n\t\t\tfeedback: 'vibemancer feedback',\r\n\t\t\tbuild: 'vibemancer build --opponent Battlemage',\r\n\t\t},\r\n\t\tdependencies: {\r\n\t\t\t'@vibemancer/core': toolchain,\r\n\t\t},\r\n\t\tdevDependencies: {\r\n\t\t\t'vibemancer': toolchain,\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\tgetMissileContext, turnToward, flyStraight,\r\n\t// More actions (all importable from '@vibemancer/core'):\r\n\t// blink, cancel, idle, turnToAngle,\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// useRandom,\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: 860x860 total. The PLAYFIELD is [30, 830] — the outer 30 units are LAVA.\r\n * LAVA: Standing in it KILLS YOU. Positions are NOT clamped to the playfield: you\r\n * can walk into lava, and knockback can push you in. Many losses are this.\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.5 units/tick (150 u/s), 33% speed while casting, 0% while shielding\r\n * Knockback: missiles above 10 damage shove the target. Yours push them; theirs push you\r\n * — possibly into the lava.\r\n * GCD: 100 ticks (1s) lockout after any spell completes or is canceled\r\n * Thinking: 45 SECONDS of total thinking time per fight (all ten matches share it).\r\n * Spend it all and your bot stops being asked for the rest of the fight — it\r\n * just stands there. That is NOT a crash: a slow bot loses fights, it does\r\n * not get removed from the ladder. There is no per-tick limit, so one slow\r\n * tick costs you nothing but the time itself. You will not come close to\r\n * this by accident — the worst built-in bot uses about 5.6s of its 45.\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, and a wizard moves at 1.5 too — so a minimum-speed\r\n * missile does NOT outrun a fleeing target). 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: hooks-style function called every tick to steer. Use getMissileContext() to read state, return turnToward(x,y), turnToAngle(deg), or flyStraight().\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 33% 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). Speed clamped to [-1, 1].\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 AnalyzedThreat:\r\n * { id, projectile, ticksToImpact, willHit }\r\n * - projectile carries the incoming missile's own position/velocity/damage.\r\n * - willHit is the one to branch on: a threat that misses needs no reaction.\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 33% speed while casting\r\n\treturn missile(\r\n\t\t{damage: 25, speed: 6, duration: Math.max(60, Math.ceil(dist / 6) + 20), turnRate: 3},\r\n\t\t() =>\r\n\t\t{\r\n\t\t\t// Missile AI: hooks-style function called every tick to steer the missile\r\n\t\t\t// Use getMissileContext() to read state, return turnToward/flyStraight\r\n\t\t\tconst ctx = getMissileContext();\r\n\t\t\tconst target = ctx.worldState.enemies[0];\r\n\t\t\tif (!target) return flyStraight();\r\n\t\t\treturn turnToward(target.position.x, target.position.y);\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 860x860 arena whose outer 30 units are instant-death lava (playfield [30, 830]).\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\nnpm run upload # Upload to vibemancer.com for rated competition\r\nnpm run pull # Download latest source from server (after MCP edits)\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 all pass on the starter bot. Keep them green as you change it, and add\r\n// your own — they are the fastest way to know you have not broken anything.\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 860x860 arena whose outer 30 units are instant-death lava (playfield [30, 830]).\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 The \\`ai\\` is a hooks-style function \\`() => MissileAction\\` — use\r\n \\`getMissileContext()\\` to read state, return \\`turnToward(x, y)\\`,\r\n \\`turnToAngle(degrees)\\`, or \\`flyStraight()\\`.\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 - \\`useRandom()\\` — seeded PRNG (\\`() => number\\`). Deterministic. Use\r\n instead of \\`Math.random()\\` (blocked in sandbox).\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 \\`useRandom()\\` for a seeded,\r\n deterministic PRNG).\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\n/**\r\n * Write the project and RETURN the files written.\r\n *\r\n * The caller used to print a hardcoded list of seven while this wrote eight — AGENTS.md,\r\n * the file the website advertises as the AI-onboarding hook, was created silently and left\r\n * out of the scaffolder's own output. Returning the list makes that drift impossible.\r\n */\r\nexport function scaffoldProject(projectDir: string, projectName: string, botName: string): string[]\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\treturn files.map(([filePath]) => filePath);\r\n}\r\n"],"mappings":";;;AAUA,OAAOA,SAAQ;AACf,OAAOC,WAAU;;;ACJjB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAQ,qBAAoB;AAU5B,SAAS,iBACT;AACC,MACA;AACC,UAAM,OAAO,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxD,eAAW,OAAO,CAAC,mBAAmB,oBAAoB,GAC1D;AACC,YAAM,YAAY,KAAK,QAAQ,MAAM,GAAG;AACxC,UAAI,CAAC,GAAG,WAAW,SAAS,EAAG;AAC/B,YAAM,SAAkB,KAAK,MAAM,GAAG,aAAa,WAAW,MAAM,CAAC;AACrE,UAAI,OAAO,WAAW,YAAY,WAAW,KAAM;AACnD,UAAI,EAAE,UAAU,WAAW,EAAE,aAAa,QAAS;AACnD,YAAM,EAAC,MAAM,QAAO,IAAI;AACxB,UAAI,SAAS,uBAAuB,OAAO,YAAY,SAAU,QAAO;AAAA,IACzE;AAAA,EACD,QAEA;AAAA,EAEA;AACA,SAAO;AACR;AAEO,IAAM,qBAAqB,eAAe;AAE1C,SAAS,aAAa,KAC7B;AACC,MAAI,SAAS,IACX,QAAQ,UAAU,GAAG,EACrB,QAAQ,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,EACvC,QAAQ,QAAQ,EAAE;AAGpB,MAAI,MAAM,KAAK,MAAM,GACrB;AACC,aAAS,MAAM;AAAA,EAChB;AAEA,SAAO;AACR;AAKO,SAAS,kBAAkB,MAClC;AACC,SAAO,6BAA6B,KAAK,IAAI;AAC9C;AAKO,SAAS,mBAAmB,MACnC;AACC,SAAO,6CAA6C,KAAK,IAAI;AAC9D;AAWO,SAAS,eAAe,oBAA4B,oBAC3D;AACC,MAAI,CAAC,kBAAmB,QAAO;AAC/B,SAAO,IAAI,iBAAiB;AAC7B;AAEO,SAAS,YAAY,aAAqB,oBAA4B,oBAC7E;AACC,QAAM,YAAY,eAAe,iBAAiB;AAClD,SAAO,KAAK,UAAU;AAAA,IACrB,MAAM;AAAA,IACN,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,MACR,KAAK;AAAA,MACL,MAAM;AAAA,MACN,WAAW;AAAA,MACX,OAAO;AAAA,MACP,OAAO;AAAA,MACP,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO;AAAA,IACR;AAAA,IACA,cAAc;AAAA,MACb,oBAAoB;AAAA,IACrB;AAAA,IACA,iBAAiB;AAAA,MAChB,cAAc;AAAA,MACd,cAAc;AAAA,MACd,UAAU;AAAA,IACX;AAAA,EACD,GAAG,MAAM,GAAI,IAAI;AAClB;AAEO,SAAS,eAChB;AACC,SAAO,KAAK,UAAU;AAAA,IACrB,iBAAiB;AAAA,MAChB,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,KAAK,CAAC,QAAQ;AAAA,MACd,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,oBAAoB;AAAA,MACpB,iBAAiB;AAAA,MACjB,QAAQ;AAAA,IACT;AAAA,IACA,SAAS,CAAC,OAAO,OAAO;AAAA,EACzB,GAAG,MAAM,GAAI,IAAI;AAClB;AAEO,SAAS,eAAe,SAC/B;AACC,SAAO,KAAK,UAAU;AAAA,IACrB,KAAK;AAAA,IACL,QAAQ;AAAA,EACT,GAAG,MAAM,GAAI,IAAI;AAClB;AAEO,SAAS,YAAY,SAC5B;AACC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAuBH,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBA6DM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CzB;AAEO,SAAS,SAAS,SACzB;AACC,SAAO,KAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2EA6BkD,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+D7E;AAEO,SAAS,gBAAgB,SAChC;AACC,SAAO;AAAA;AAAA,UAEE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAQS,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAMP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAMP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAMP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAMP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOjC;AAEO,SAAS,SAAS,SACzB;AACC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BASkB,OAAO;AAAA;AAAA;AAAA,mEAG6B,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkFrE;AAEO,SAAS,YAChB;AACC,SAAO;AAAA;AAAA;AAAA;AAAA;AAKR;AAYO,SAAS,gBAAgB,YAAoB,aAAqB,SACzE;AACC,QAAM,QAA4B;AAAA,IACjC,CAAC,gBAAgB,YAAY,WAAW,CAAC;AAAA,IACzC,CAAC,iBAAiB,aAAa,CAAC;AAAA,IAChC,CAAC,mBAAmB,eAAe,OAAO,CAAC;AAAA,IAC3C,CAAC,cAAc,YAAY,OAAO,CAAC;AAAA,IACnC,CAAC,qBAAqB,gBAAgB,OAAO,CAAC;AAAA,IAC9C,CAAC,aAAa,SAAS,OAAO,CAAC;AAAA,IAC/B,CAAC,aAAa,SAAS,OAAO,CAAC;AAAA,IAC/B,CAAC,cAAc,UAAU,CAAC;AAAA,EAC3B;AAEA,KAAG,UAAU,KAAK,KAAK,YAAY,KAAK,GAAG,EAAC,WAAW,KAAI,CAAC;AAC5D,KAAG,UAAU,KAAK,KAAK,YAAY,OAAO,GAAG,EAAC,WAAW,KAAI,CAAC;AAE9D,aAAW,CAAC,UAAU,OAAO,KAAK,OAClC;AACC,UAAM,WAAW,KAAK,KAAK,YAAY,QAAQ;AAC/C,OAAG,cAAc,UAAU,OAAO;AAAA,EACnC;AAEA,SAAO,MAAM,IAAI,CAAC,CAAC,QAAQ,MAAM,QAAQ;AAC1C;;;AD7hBA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAEjC,SAAS,YACT;AACC,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAYZ;AACD;AAEA,SAAS,YACT;AACC,QAAM,cAAc,KAAK,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AACvD,MAAI,CAAC,aACL;AACC,cAAU;AACV,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,MAAI,CAAC,mBAAmB,WAAW,GACnC;AACC,YAAQ,MAAM,WAAW,WAAW,gCAAgC;AACpE,YAAQ,MAAM,+DAA+D;AAC7E,YAAQ,MAAM,kEAAkE;AAChF,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,UAAU,KAAK,QAAQ,QAAQ;AACrC,MAAI,YAAY,MAAM,CAAC,KAAK,UAAU,CAAC,GACvC;AACC,YAAQ,MAAM,iCAAiC;AAC/C,YAAQ,MAAM,oDAAoD;AAClE,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,UAAU,YAAY,MAAM,KAAK,UAAU,CAAC,IAC/C,KAAK,UAAU,CAAC,IAChB,aAAa,WAAW;AAE3B,MAAI,CAAC,kBAAkB,OAAO,GAC9B;AACC,YAAQ,MAAM,WAAW,OAAO,yCAAyC;AACzE,YAAQ,MAAM,sDAAsD;AACpE,YAAQ,MAAM,uDAAuD;AACrE,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,SAAO,EAAC,aAAa,QAAO;AAC7B;AAEA,SAAS,OACT;AACC,QAAM,EAAC,aAAa,QAAO,IAAI,UAAU;AACzC,QAAM,aAAaC,MAAK,QAAQ,WAAW;AAE3C,MAAIC,IAAG,WAAW,UAAU,GAC5B;AACC,YAAQ,MAAM,qBAAqB,WAAW,mBAAmB;AACjE,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,UAAQ,IAAI;AAAA,+BAAkC,WAAW,EAAE;AAC3D,UAAQ,IAAI,aAAa,OAAO;AAAA,CAAI;AAEpC,MACA;AAGC,UAAM,QAAQ,gBAAgB,YAAY,aAAa,OAAO;AAC9D,eAAW,YAAY,OACvB;AACC,cAAQ,IAAI,aAAa,QAAQ,EAAE;AAAA,IACpC;AAAA,EACD,SACM,KACN;AACC,YAAQ,MAAM;AAAA,0BAA6B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC7F,QACA;AACC,MAAAA,IAAG,OAAO,YAAY,EAAC,WAAW,MAAM,OAAO,KAAI,CAAC;AAAA,IACrD,QAEA;AAAA,IAEA;AACA,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,UAAQ,IAAI;AAAA;AAAA;AAAA,OAGN,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAWjB;AACD;AAEA,KAAK;","names":["fs","path","path","fs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vibemancer",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Create a new Vibemancer wizard bot project",
5
5
  "type": "module",
6
6
  "author": "Low Entry",