vibemancer 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,4 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ findCoreSourceDir,
4
+ getBuiltinBotNames,
5
+ getCoreCompileOptions,
6
+ resolveOpponent
7
+ } from "./chunk-4DUS7IHW.js";
8
+ import {
9
+ FIREBASE_CONFIG
10
+ } from "./chunk-AA2UJPPN.js";
2
11
 
3
12
  // src/commands/dev.ts
4
13
  import { execFile } from "child_process";
@@ -40,13 +49,25 @@ async function discoverBot(projectDir, overridePath) {
40
49
  return { sourcePath: botPath, exportName };
41
50
  }
42
51
  }
52
+ const allBots = await discoverAllBots(absDir);
53
+ if (allBots.length === 1) {
54
+ return allBots[0];
55
+ }
56
+ if (allBots.length > 1) {
57
+ const list = allBots.map((b) => ` --bot ${path.relative(absDir, b.sourcePath).replace(/\\/g, "/")} (${b.exportName})`).join("\n");
58
+ throw new Error(
59
+ `Found ${allBots.length} bots. Pick one with --bot:
60
+
61
+ ${list}`
62
+ );
63
+ }
43
64
  const defaultPath = path.join(absDir, "src", "bot.ts");
44
65
  if (fs.existsSync(defaultPath)) {
45
66
  const exportName = await findExportName(defaultPath);
46
67
  return { sourcePath: defaultPath, exportName };
47
68
  }
48
69
  throw new Error(
49
- 'Could not find bot source file.\nCreate src/bot.ts or add a vibemancer.json with {"bot": "path/to/bot.ts"}'
70
+ "Could not find bot source file.\nCreate a .ts file in src/ with a PascalCase export, e.g.:\n export function MyWizard() { ... }"
50
71
  );
51
72
  }
52
73
  async function findExportName(filePath) {
@@ -142,100 +163,6 @@ import http from "http";
142
163
 
143
164
  // src/compile-single-bot.ts
144
165
  import { build } from "esbuild";
145
-
146
- // src/opponent-resolver.ts
147
- import path2 from "path";
148
- import fs2 from "fs";
149
- import { fileURLToPath } from "url";
150
- import { BotBundle } from "@vibemancer/core";
151
- function findCoreSourceDir() {
152
- const monorepoCore = path2.resolve(import.meta.dirname, "..", "..", "core", "src");
153
- if (fs2.existsSync(path2.join(monorepoCore, "engine", "simulation.ts"))) {
154
- return monorepoCore;
155
- }
156
- try {
157
- const coreIndex = import.meta.resolve("@vibemancer/core");
158
- const coreIndexPath = fileURLToPath(coreIndex);
159
- const coreRoot = path2.resolve(path2.dirname(coreIndexPath), "..");
160
- const srcDir = path2.join(coreRoot, "src");
161
- if (fs2.existsSync(path2.join(srcDir, "engine", "simulation.ts"))) {
162
- return srcDir;
163
- }
164
- } catch {
165
- }
166
- throw new Error(
167
- "Could not find @vibemancer/core source directory.\nEnsure @vibemancer/core is installed and includes its src/ directory."
168
- );
169
- }
170
- var BUILTIN_BOTS = {
171
- // Standalone
172
- TargetDummy: "standalone/TargetDummy.ts",
173
- Critter: "standalone/Critter.ts",
174
- Rookie: "standalone/Rookie.ts",
175
- Hogger: "standalone/Hogger.ts",
176
- Doombringer: "standalone/Doombringer.ts",
177
- // Defensive
178
- Turtle: "defensive/01_Turtle.ts",
179
- Sentinel: "defensive/02_Sentinel.ts",
180
- Golem: "defensive/03_Golem.ts",
181
- // Melee
182
- Shadowblade: "melee/01_Shadowblade.ts",
183
- Nightblade: "melee/02_Nightblade.ts",
184
- Voidblade: "melee/03_Voidblade.ts",
185
- // Homing
186
- Bonemancer: "homing/01_Bonemancer.ts",
187
- Lich: "homing/02_Lich.ts",
188
- Archlich: "homing/03_Archlich.ts",
189
- // Caster
190
- Flamecaller: "caster/01_Flamecaller.ts",
191
- Pyromancer: "caster/02_Pyromancer.ts",
192
- Infernalist: "caster/03_Infernalist.ts",
193
- // Sniper
194
- Spellshot: "sniper/01_Spellshot.ts",
195
- Spelltracer: "sniper/02_Spelltracer.ts",
196
- Spellseeker: "sniper/03_Spellseeker.ts",
197
- // Duelist
198
- Battlemage: "duelist/01_Battlemage.ts",
199
- Warmage: "duelist/02_Warmage.ts",
200
- Archmage: "duelist/03_Archmage.ts",
201
- // Berserker
202
- Stormchaser: "berserker/01_Stormchaser.ts",
203
- Stormcaller: "berserker/02_Stormcaller.ts",
204
- Stormforger: "berserker/03_Stormforger.ts",
205
- // Kiter
206
- Spellspinner: "kiter/01_Spellspinner.ts",
207
- Spellweaver: "kiter/02_Spellweaver.ts",
208
- Spellbinder: "kiter/03_Spellbinder.ts"
209
- };
210
- function getCoreCompileOptions() {
211
- const coreSourceDir = findCoreSourceDir();
212
- return {
213
- alias: {
214
- "@vibemancer/core": coreSourceDir + "/index-browser.ts"
215
- }
216
- };
217
- }
218
- function getBuiltinBotNames() {
219
- return Object.keys(BUILTIN_BOTS);
220
- }
221
- function resolveOpponent(name) {
222
- const relativePath = BUILTIN_BOTS[name];
223
- if (!relativePath) {
224
- const available = Object.keys(BUILTIN_BOTS).join(", ");
225
- throw new Error(
226
- `Unknown opponent "${name}".
227
- Available built-in bots: ${available}`
228
- );
229
- }
230
- const coreSourceDir = findCoreSourceDir();
231
- const sourcePath = path2.join(coreSourceDir, "bots", relativePath);
232
- if (!fs2.existsSync(sourcePath)) {
233
- throw new Error(`Built-in bot source not found: ${sourcePath}`);
234
- }
235
- return new BotBundle(sourcePath, name);
236
- }
237
-
238
- // src/compile-single-bot.ts
239
166
  async function compileSingleBotBundle(sourcePath, exportName) {
240
167
  const coreSourceDir = findCoreSourceDir();
241
168
  const result = await build({
@@ -355,7 +282,7 @@ async function runDev(options) {
355
282
  const bots = await discoverAllBots(projectDir);
356
283
  if (bots.length === 0) {
357
284
  console.log("No bots discovered in src/. Add a .ts file with a PascalCase export, e.g.:");
358
- console.log(" export function MyWizard(props) { return {move: {x: 0, y: 0}}; }");
285
+ console.log(" export function MyWizard() { return move(0, 0); }");
359
286
  } else {
360
287
  console.log(`Discovered ${bots.length} bot${bots.length === 1 ? "" : "s"}: ${bots.map((b) => b.exportName).join(", ")}`);
361
288
  }
@@ -402,9 +329,70 @@ async function runTest(_options) {
402
329
  }
403
330
 
404
331
  // src/commands/fight.ts
405
- import fs3 from "fs";
406
- import path3 from "path";
407
- import { BotBundle as BotBundle2, sandboxFight, scoreFight } from "@vibemancer/core";
332
+ import fs2 from "fs";
333
+ import path2 from "path";
334
+ import { BotBundle, sandboxFight, scoreFight, runBundleFight } from "@vibemancer/core";
335
+
336
+ // src/remote-opponent.ts
337
+ import { initializeApp, getApps } from "firebase/app";
338
+ import { getFirestore, collection, query, where, limit, getDocs, connectFirestoreEmulator } from "firebase/firestore";
339
+ import { getStorage, ref, getBytes, connectStorageEmulator } from "firebase/storage";
340
+ function isHandleSelector(opponent) {
341
+ const trimmed = opponent.trim();
342
+ const slash = trimmed.indexOf("/");
343
+ return slash > 0 && slash < trimmed.length - 1;
344
+ }
345
+ var cachedDb = null;
346
+ var cachedStorage = null;
347
+ function getApp() {
348
+ const apps = getApps();
349
+ return apps.length > 0 ? apps[0] : initializeApp(FIREBASE_CONFIG);
350
+ }
351
+ function getDb() {
352
+ if (!cachedDb) {
353
+ cachedDb = getFirestore(getApp());
354
+ if (process.env.VIBEMANCER_EMULATOR === "1") connectFirestoreEmulator(cachedDb, "127.0.0.1", 8085);
355
+ }
356
+ return cachedDb;
357
+ }
358
+ function getBucket() {
359
+ if (!cachedStorage) {
360
+ cachedStorage = getStorage(getApp());
361
+ if (process.env.VIBEMANCER_EMULATOR === "1") connectStorageEmulator(cachedStorage, "127.0.0.1", 9199);
362
+ }
363
+ return cachedStorage;
364
+ }
365
+ async function resolveRemoteOpponent(selector) {
366
+ const slash = selector.indexOf("/");
367
+ const handle = selector.slice(0, slash).trim().toLowerCase();
368
+ const botName = selector.slice(slash + 1).trim();
369
+ if (!handle || !botName) {
370
+ throw new Error(`Invalid opponent "${selector}" \u2014 expected handle/botname (e.g. happy-golden-banana/FireMage).`);
371
+ }
372
+ const db = getDb();
373
+ const snap = await getDocs(query(
374
+ collection(db, "wizards"),
375
+ where("ownerHandle", "==", handle),
376
+ where("nameLower", "==", botName.toLowerCase()),
377
+ where("active", "==", true),
378
+ limit(1)
379
+ ));
380
+ if (snap.empty) {
381
+ throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
382
+ }
383
+ const docSnap = snap.docs[0];
384
+ const data = docSnap.data();
385
+ const exportName = typeof data.exportName === "string" ? data.exportName : "";
386
+ if (!exportName) {
387
+ throw new Error(`Bot "${handle}/${botName}" is missing its export name.`);
388
+ }
389
+ const bundlePath = typeof data.bundlePath === "string" ? data.bundlePath : `bundles/${docSnap.id}.js`;
390
+ const bytes = await getBytes(ref(getBucket(), bundlePath));
391
+ const bundle = new TextDecoder().decode(bytes);
392
+ return { bundle, exportName, label: `${handle}/${botName}` };
393
+ }
394
+
395
+ // src/commands/fight.ts
408
396
  async function runFight(options) {
409
397
  if (options.opponent) {
410
398
  return runSingleFight({ ...options, opponent: options.opponent });
@@ -418,13 +406,21 @@ async function runSingleFight(options) {
418
406
  Bot: ${botInfo.exportName}`);
419
407
  console.log(` Opponent: ${options.opponent}
420
408
  `);
421
- const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
422
- const opponentBundle = resolveOpponent(options.opponent);
423
409
  const start = Date.now();
424
- const result = await sandboxFight(userBundle, opponentBundle, {
425
- seed: options.seed,
426
- compileOptions: getCoreCompileOptions()
427
- });
410
+ let result;
411
+ if (isHandleSelector(options.opponent)) {
412
+ console.log(" Resolving uploaded opponent...");
413
+ const remote = await resolveRemoteOpponent(options.opponent);
414
+ const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
415
+ result = await runBundleFight(userBundle, remote.bundle, { seed: options.seed ?? 1 });
416
+ } else {
417
+ const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
418
+ const opponentBundle = resolveOpponent(options.opponent);
419
+ result = await sandboxFight(userBundle, opponentBundle, {
420
+ seed: options.seed,
421
+ compileOptions: getCoreCompileOptions()
422
+ });
423
+ }
428
424
  const elapsed = Date.now() - start;
429
425
  const { wizard1Wins, wizard2Wins, draws } = result;
430
426
  const total = wizard1Wins + wizard2Wins + draws;
@@ -440,7 +436,7 @@ async function runSingleFight(options) {
440
436
  async function runFullFight(options) {
441
437
  const projectDir = process.cwd();
442
438
  const botInfo = await discoverBot(projectDir, options.bot);
443
- const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
439
+ const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
444
440
  const opponents = getBuiltinBotNames();
445
441
  console.log(`
446
442
  Fighting ${botInfo.exportName} against ${opponents.length} built-in bots...
@@ -494,13 +490,13 @@ async function runFullFight(options) {
494
490
  console.log("");
495
491
  }
496
492
  function getHistoryPath(projectDir) {
497
- return path3.join(projectDir, ".vibemancer", "history.json");
493
+ return path2.join(projectDir, ".vibemancer", "history.json");
498
494
  }
499
495
  function loadHistory(projectDir) {
500
496
  const historyPath = getHistoryPath(projectDir);
501
- if (!fs3.existsSync(historyPath)) return [];
497
+ if (!fs2.existsSync(historyPath)) return [];
502
498
  try {
503
- const raw = fs3.readFileSync(historyPath, "utf-8");
499
+ const raw = fs2.readFileSync(historyPath, "utf-8");
504
500
  return JSON.parse(raw);
505
501
  } catch {
506
502
  return [];
@@ -508,10 +504,10 @@ function loadHistory(projectDir) {
508
504
  }
509
505
  function saveHistory(projectDir, history, current) {
510
506
  const historyPath = getHistoryPath(projectDir);
511
- const dir = path3.dirname(historyPath);
512
- fs3.mkdirSync(dir, { recursive: true });
507
+ const dir = path2.dirname(historyPath);
508
+ fs2.mkdirSync(dir, { recursive: true });
513
509
  const updated = [...history.slice(-19), current];
514
- fs3.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
510
+ fs2.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
515
511
  }
516
512
  function showDiff(current, previous) {
517
513
  const prevMap = new Map(previous.map((e) => [e.opponent, e]));
@@ -542,8 +538,9 @@ function showDiff(current, previous) {
542
538
 
543
539
  // src/commands/trace.ts
544
540
  import {
545
- BotBundle as BotBundle3,
541
+ BotBundle as BotBundle2,
546
542
  sandboxSimulate,
543
+ runBundleSimulate,
547
544
  extractTraceEvents,
548
545
  summarizeTrace,
549
546
  formatTraceEvents,
@@ -556,18 +553,30 @@ import {
556
553
  async function runTrace(options) {
557
554
  const projectDir = process.cwd();
558
555
  const botInfo = await discoverBot(projectDir, options.bot);
559
- const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
560
- const opponentBundle = resolveOpponent(options.opponent);
561
556
  const distance = options.distance ?? 600;
562
557
  console.log(`
563
558
  Trace: ${botInfo.exportName} (W1) vs ${options.opponent} (W2) | distance: ${distance} | seed: ${options.seed ?? 1}
564
559
  `);
565
- const result = await sandboxSimulate(userBundle, opponentBundle, {
566
- seed: options.seed ?? 1,
567
- spawnDistance: distance,
568
- maxTicks: options.maxTicks ?? 3e3,
569
- compileOptions: getCoreCompileOptions()
570
- });
560
+ let result;
561
+ if (isHandleSelector(options.opponent)) {
562
+ console.log(" Resolving uploaded opponent...");
563
+ const remote = await resolveRemoteOpponent(options.opponent);
564
+ const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
565
+ result = await runBundleSimulate(userBundle, remote.bundle, {
566
+ seed: options.seed ?? 1,
567
+ spawnDistance: distance,
568
+ maxTicks: options.maxTicks ?? 3e3
569
+ });
570
+ } else {
571
+ const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
572
+ const opponentBundle = resolveOpponent(options.opponent);
573
+ result = await sandboxSimulate(userBundle, opponentBundle, {
574
+ seed: options.seed ?? 1,
575
+ spawnDistance: distance,
576
+ maxTicks: options.maxTicks ?? 3e3,
577
+ compileOptions: getCoreCompileOptions()
578
+ });
579
+ }
571
580
  const history = result.history;
572
581
  if (history.length === 0) {
573
582
  console.log(" No history available.\n");
@@ -589,11 +598,11 @@ async function runTrace(options) {
589
598
  }
590
599
 
591
600
  // src/commands/tournament.ts
592
- import { BotBundle as BotBundle4, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
601
+ import { BotBundle as BotBundle3, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
593
602
  async function runTournament(options) {
594
603
  const projectDir = process.cwd();
595
604
  const botInfo = await discoverBot(projectDir, options.bot);
596
- const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
605
+ const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
597
606
  const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
598
607
  const participants = [
599
608
  { name: botInfo.exportName, bundle: userBundle }
@@ -664,7 +673,7 @@ async function runTournament(options) {
664
673
 
665
674
  // src/commands/optimize.ts
666
675
  import { readFileSync, writeFileSync } from "fs";
667
- import { BotBundle as BotBundle5, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
676
+ import { BotBundle as BotBundle4, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
668
677
  var USEPAR_RE = /useParam\(\s*['"](\w+)['"]\s*,\s*([^,)]+?)\s*(?:,\s*\{([^}]*)\})?\s*\)/g;
669
678
  function parseNumValue(s) {
670
679
  const n = parseFloat(s.trim());
@@ -780,7 +789,7 @@ async function runOptimize(options) {
780
789
  const maxRounds = options.rounds ?? 3;
781
790
  const opponentNames = resolveOpponentNames(options.opponents);
782
791
  const opponentBundles = opponentNames.map((name) => resolveOpponent(name));
783
- const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
792
+ const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
784
793
  console.log(` Opponents: ${opponentBundles.length}`);
785
794
  console.log(` Steps per param: ${steps}`);
786
795
  console.log(` Max rounds: ${maxRounds}
@@ -848,14 +857,14 @@ function escapeRegex(s) {
848
857
  }
849
858
 
850
859
  // src/commands/build.ts
851
- import fs4 from "fs";
852
- import path4 from "path";
853
- import { BotBundle as BotBundle6, compileMatchBundle } from "@vibemancer/core";
860
+ import fs3 from "fs";
861
+ import path3 from "path";
862
+ import { BotBundle as BotBundle5, compileMatchBundle } from "@vibemancer/core";
854
863
  async function runBuild(options) {
855
864
  const projectDir = process.cwd();
856
865
  const botInfo = await discoverBot(projectDir, options.bot);
857
866
  const coreSourceDir = findCoreSourceDir();
858
- const userBundle = new BotBundle6(botInfo.sourcePath, botInfo.exportName);
867
+ const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
859
868
  const opponentBundle = resolveOpponent(options.opponent);
860
869
  console.log(`
861
870
  Compiling ${botInfo.exportName} vs ${options.opponent}...`);
@@ -867,9 +876,9 @@ async function runBuild(options) {
867
876
  });
868
877
  const elapsed = Date.now() - start;
869
878
  const outFile = options.output ?? `dist/${botInfo.exportName}-vs-${options.opponent}.js`;
870
- const outDir = path4.dirname(path4.resolve(outFile));
871
- fs4.mkdirSync(outDir, { recursive: true });
872
- fs4.writeFileSync(path4.resolve(outFile), bundle);
879
+ const outDir = path3.dirname(path3.resolve(outFile));
880
+ fs3.mkdirSync(outDir, { recursive: true });
881
+ fs3.writeFileSync(path3.resolve(outFile), bundle);
873
882
  const sizeKb = (bundle.length / 1024).toFixed(1);
874
883
  console.log(` Output: ${outFile} (${sizeKb} KB)`);
875
884
  console.log(` Compiled in ${elapsed}ms
@@ -966,7 +975,7 @@ function getStyleDescription(bot) {
966
975
 
967
976
  // src/commands/upload.ts
968
977
  import { createHash } from "crypto";
969
- import fs5 from "fs";
978
+ import fs4 from "fs";
970
979
  async function runUpload(options) {
971
980
  const projectDir = process.cwd();
972
981
  const botInfo = await discoverBot(projectDir, options.bot);
@@ -979,25 +988,21 @@ async function runUpload(options) {
979
988
  console.error(` Error: Bundle too large (${sizeKb} KB). Max 500 KB.`);
980
989
  process.exit(1);
981
990
  }
982
- const nameMatch = bundle.match(/export\s+(?:const|var|let)\s+name\s*=\s*["']([^"']+)["']/);
983
- const wizardName = nameMatch?.[1] ?? botInfo.exportName;
991
+ const wizardName = botInfo.exportName;
984
992
  const bundleHash = createHash("sha256").update(bundle).digest("hex");
985
993
  console.log(` Wizard: ${wizardName}`);
986
994
  console.log(` Hash: ${bundleHash.slice(0, 12)}...`);
987
995
  console.log(" Uploading to VibeMancer...");
988
996
  try {
989
- const { initializeApp } = await import("firebase/app");
997
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
990
998
  const { getFunctions, httpsCallable } = await import("firebase/functions");
991
- const app = initializeApp({
992
- apiKey: "AIzaSyDSUJ2jlk5_vlpGOypMtvqKjHhDmbgomIY",
993
- authDomain: "le-vibemancer.firebaseapp.com",
994
- projectId: "le-vibemancer"
995
- });
999
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1000
+ const app = initializeApp2(FIREBASE_CONFIG2);
996
1001
  const functions = getFunctions(app, "us-central1");
997
1002
  const uploadFn = httpsCallable(functions, "uploadWizard");
998
1003
  let sourceCode;
999
1004
  try {
1000
- sourceCode = fs5.readFileSync(botInfo.sourcePath, "utf-8");
1005
+ sourceCode = fs4.readFileSync(botInfo.sourcePath, "utf-8");
1001
1006
  } catch {
1002
1007
  }
1003
1008
  const result = await uploadFn({
@@ -1030,38 +1035,47 @@ async function runUpload(options) {
1030
1035
  }
1031
1036
 
1032
1037
  // src/commands/pull.ts
1033
- import fs6 from "fs";
1038
+ import fs5 from "fs";
1034
1039
  async function runPull(options) {
1035
1040
  const projectDir = process.cwd();
1036
1041
  const botInfo = await discoverBot(projectDir, options.bot);
1037
1042
  console.log(`
1038
1043
  Pulling latest source for ${botInfo.exportName}...`);
1039
1044
  try {
1040
- const { initializeApp } = await import("firebase/app");
1045
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
1041
1046
  const { getAuth, signInWithPopup, GoogleAuthProvider } = await import("firebase/auth");
1042
- const { getFirestore, collection, query, where, getDocs, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
1043
- const app = initializeApp({
1044
- apiKey: "AIzaSyDSUJ2jlk5_vlpGOypMtvqKjHhDmbgomIY",
1045
- authDomain: "le-vibemancer.firebaseapp.com",
1046
- projectId: "le-vibemancer"
1047
- });
1047
+ const { getFirestore: getFirestore2, collection: collection2, query: query2, where: where2, getDocs: getDocs2, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
1048
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1049
+ const app = initializeApp2(FIREBASE_CONFIG2);
1048
1050
  const auth = getAuth(app);
1049
1051
  console.log(" Signing in...");
1050
1052
  const cred = await signInWithPopup(auth, new GoogleAuthProvider());
1051
1053
  const userId = cred.user.uid;
1052
1054
  console.log(` Signed in as ${cred.user.displayName ?? cred.user.email ?? userId}`);
1053
- const db = getFirestore(app);
1054
- const wizardQuery = query(
1055
- collection(db, "wizards"),
1056
- where("ownerId", "==", userId),
1057
- where("name", "==", botInfo.exportName),
1058
- where("active", "==", true),
1055
+ const db = getFirestore2(app);
1056
+ const wizardSnap = await getDocs2(query2(
1057
+ collection2(db, "wizards"),
1058
+ where2("ownerId", "==", userId),
1059
+ where2("name", "==", botInfo.exportName),
1060
+ where2("active", "==", true),
1059
1061
  fbLimit(1)
1060
- );
1061
- const wizardSnap = await getDocs(wizardQuery);
1062
+ ));
1062
1063
  if (wizardSnap.empty) {
1063
- console.error(` No active wizard named "${botInfo.exportName}" found on your account.`);
1064
- console.error(" Upload first with: vibemancer upload");
1064
+ const allBots = await getDocs2(query2(
1065
+ collection2(db, "wizards"),
1066
+ where2("ownerId", "==", userId),
1067
+ where2("active", "==", true),
1068
+ fbLimit(20)
1069
+ ));
1070
+ if (allBots.empty) {
1071
+ console.error(" No active wizards found on your account.");
1072
+ console.error(" Upload first with: vibemancer upload");
1073
+ } else {
1074
+ const names = allBots.docs.map((d) => d.data().name).filter(Boolean);
1075
+ console.error(` No wizard named "${botInfo.exportName}" found. Your active wizards:`);
1076
+ for (const n of names) console.error(` - ${n}`);
1077
+ console.error("\n Rename your local export to match, or upload first.");
1078
+ }
1065
1079
  process.exit(1);
1066
1080
  }
1067
1081
  const wizardId = wizardSnap.docs[0].id;
@@ -1077,7 +1091,7 @@ async function runPull(options) {
1077
1091
  console.error(" Source code is empty. Re-upload to store it.");
1078
1092
  process.exit(1);
1079
1093
  }
1080
- fs6.writeFileSync(botInfo.sourcePath, sourceCode);
1094
+ fs5.writeFileSync(botInfo.sourcePath, sourceCode);
1081
1095
  console.log(` \u2713 Updated ${botInfo.sourcePath}`);
1082
1096
  console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1083
1097
  } catch (err) {
@@ -1090,6 +1104,60 @@ async function runPull(options) {
1090
1104
  }
1091
1105
  }
1092
1106
 
1107
+ // src/commands/feedback.ts
1108
+ async function runFeedback(options) {
1109
+ const message = options.message.trim();
1110
+ if (!message) {
1111
+ console.error(" Error: feedback message cannot be empty.");
1112
+ process.exit(1);
1113
+ }
1114
+ console.log("\n Submitting feedback...");
1115
+ try {
1116
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
1117
+ const { getFunctions, httpsCallable } = await import("firebase/functions");
1118
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1119
+ const app = initializeApp2(FIREBASE_CONFIG2);
1120
+ const functions = getFunctions(app, "us-central1");
1121
+ const submitFn = httpsCallable(functions, "submitFeedback");
1122
+ await submitFn({ message });
1123
+ console.log(" Sent! Thanks for the feedback.\n");
1124
+ } catch (err) {
1125
+ const msg = err instanceof Error ? err.message : String(err);
1126
+ console.error(` Failed to submit: ${msg}
1127
+ `);
1128
+ process.exit(1);
1129
+ }
1130
+ }
1131
+
1132
+ // src/commands/missile-calc.ts
1133
+ import { calculateMissileCastTime, GCD_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
1134
+ function runMissileCalc(options) {
1135
+ const config = {
1136
+ damage: options.damage,
1137
+ speed: options.speed,
1138
+ duration: options.duration,
1139
+ turnRate: options.turnRate
1140
+ };
1141
+ const castTimeSec = calculateMissileCastTime(config);
1142
+ const castFrames = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
1143
+ const gcdSec = GCD_DURATION / TICKS_PER_SECOND;
1144
+ const totalCycleSec = castTimeSec + gcdSec;
1145
+ const dps = config.damage / totalCycleSec;
1146
+ const range = config.speed * config.duration;
1147
+ const radius = calculateMissileRadius(config.damage);
1148
+ console.log(`
1149
+ Missile Calculator
1150
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1151
+ Config: damage=${config.damage} speed=${config.speed} duration=${config.duration} turnRate=${config.turnRate}
1152
+ Cast time: ${castTimeSec.toFixed(3)}s (${castFrames} frames)
1153
+ GCD: ${gcdSec}s (${GCD_DURATION} frames)
1154
+ Full cycle: ${totalCycleSec.toFixed(3)}s (cast + GCD)
1155
+ Eff. DPS: ${dps.toFixed(2)} HP/s
1156
+ Range: ${range} units (speed \xD7 duration)
1157
+ Hitbox: ${radius.toFixed(2)} radius
1158
+ `);
1159
+ }
1160
+
1093
1161
  // src/cli.ts
1094
1162
  var args = process.argv.slice(2);
1095
1163
  var command = args[0];
@@ -1128,6 +1196,8 @@ Commands:
1128
1196
  build Compile bot to a standalone bundle
1129
1197
  upload Upload bot to VibeMancer for online competition
1130
1198
  pull Download latest source code from VibeMancer
1199
+ feedback Submit a bug report or suggestion
1200
+ missile-calc Calculate missile cast time and DPS for a config
1131
1201
 
1132
1202
  Common options:
1133
1203
  --bot <path> Path to bot source file (default: auto-discover)
@@ -1136,11 +1206,12 @@ Dev options:
1136
1206
  --port <n> Server port (default: 4242)
1137
1207
 
1138
1208
  Fight options:
1139
- --opponent <name> Fight a single opponent instead of all bots
1209
+ --opponent <name> Fight a single opponent: a built-in name OR another
1210
+ user's uploaded bot as handle/botname
1140
1211
  --seed <n> Random seed
1141
1212
 
1142
1213
  Trace options:
1143
- --opponent <name> Opponent bot name (required)
1214
+ --opponent <name> Opponent: a built-in name OR handle/botname (required)
1144
1215
  --seed <n> Random seed (default: 1)
1145
1216
  --distance <n> Spawn distance (default: 600)
1146
1217
 
@@ -1163,6 +1234,8 @@ Examples:
1163
1234
  vibemancer tournament Battlemage Warmage Archmage
1164
1235
  vibemancer optimize
1165
1236
  vibemancer build --opponent Battlemage
1237
+ vibemancer feedback "missiles go through walls sometimes"
1238
+ vibemancer missile-calc --damage 15 --speed 7 --duration 200 --turnRate 3
1166
1239
  `);
1167
1240
  }
1168
1241
  async function main() {
@@ -1192,7 +1265,14 @@ async function main() {
1192
1265
  case "trace": {
1193
1266
  const opponent = parseFlag("--opponent");
1194
1267
  if (!opponent) {
1195
- console.error("Error: --opponent is required for trace command.");
1268
+ const { getBuiltinBotNames: getBuiltinBotNames2 } = await import("./opponent-resolver-U2UK2QEJ.js");
1269
+ const names = getBuiltinBotNames2();
1270
+ const list = names.map((n) => ` --opponent ${n}`).join("\n");
1271
+ console.error("Error: --opponent is required for trace.\n");
1272
+ console.error(`Available opponents:
1273
+
1274
+ ${list}
1275
+ `);
1196
1276
  console.error("Usage: vibemancer trace --opponent Battlemage");
1197
1277
  process.exit(1);
1198
1278
  }
@@ -1254,6 +1334,20 @@ async function main() {
1254
1334
  await runPull({ bot });
1255
1335
  break;
1256
1336
  }
1337
+ case "feedback": {
1338
+ const message = args.slice(1).join(" ");
1339
+ await runFeedback({ message });
1340
+ break;
1341
+ }
1342
+ case "missile-calc": {
1343
+ runMissileCalc({
1344
+ damage: parseIntFlag("--damage", 15),
1345
+ speed: parseIntFlag("--speed", 7),
1346
+ duration: parseIntFlag("--duration", 200),
1347
+ turnRate: parseIntFlag("--turnRate", 0)
1348
+ });
1349
+ break;
1350
+ }
1257
1351
  default:
1258
1352
  console.error(`Unknown command: ${command}`);
1259
1353
  printHelp();