vibemancer 0.1.4 → 0.1.6

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,74 @@ 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
+ import { isBannedBotName } from "@vibemancer/core";
341
+ function isHandleSelector(opponent) {
342
+ const trimmed = opponent.trim();
343
+ const slash = trimmed.indexOf("/");
344
+ return slash > 0 && slash < trimmed.length - 1;
345
+ }
346
+ var cachedDb = null;
347
+ var cachedStorage = null;
348
+ function getApp() {
349
+ const apps = getApps();
350
+ return apps.length > 0 ? apps[0] : initializeApp(FIREBASE_CONFIG);
351
+ }
352
+ function getDb() {
353
+ if (!cachedDb) {
354
+ cachedDb = getFirestore(getApp());
355
+ if (process.env.VIBEMANCER_EMULATOR === "1") connectFirestoreEmulator(cachedDb, "127.0.0.1", 8085);
356
+ }
357
+ return cachedDb;
358
+ }
359
+ function getBucket() {
360
+ if (!cachedStorage) {
361
+ cachedStorage = getStorage(getApp());
362
+ if (process.env.VIBEMANCER_EMULATOR === "1") connectStorageEmulator(cachedStorage, "127.0.0.1", 9199);
363
+ }
364
+ return cachedStorage;
365
+ }
366
+ async function resolveRemoteOpponent(selector) {
367
+ const slash = selector.indexOf("/");
368
+ const handle = selector.slice(0, slash).trim().toLowerCase();
369
+ const botName = selector.slice(slash + 1).trim();
370
+ if (!handle || !botName) {
371
+ throw new Error(`Invalid opponent "${selector}" \u2014 expected handle/botname (e.g. happy-golden-banana/FireMage).`);
372
+ }
373
+ if (isBannedBotName(botName)) {
374
+ throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
375
+ }
376
+ const db = getDb();
377
+ const snap = await getDocs(query(
378
+ collection(db, "wizards"),
379
+ where("ownerHandle", "==", handle),
380
+ where("nameLower", "==", botName.toLowerCase()),
381
+ where("active", "==", true),
382
+ limit(1)
383
+ ));
384
+ if (snap.empty) {
385
+ throw new Error(`No active bot "${botName}" found for handle "${handle}". Check the handle + bot name (both case-insensitive) on the leaderboard.`);
386
+ }
387
+ const docSnap = snap.docs[0];
388
+ const data = docSnap.data();
389
+ const exportName = typeof data.exportName === "string" ? data.exportName : "";
390
+ if (!exportName) {
391
+ throw new Error(`Bot "${handle}/${botName}" is missing its export name.`);
392
+ }
393
+ const bundlePath = typeof data.bundlePath === "string" ? data.bundlePath : `bundles/${docSnap.id}.js`;
394
+ const bytes = await getBytes(ref(getBucket(), bundlePath));
395
+ const bundle = new TextDecoder().decode(bytes);
396
+ return { bundle, exportName, label: `${handle}/${botName}` };
397
+ }
398
+
399
+ // src/commands/fight.ts
408
400
  async function runFight(options) {
409
401
  if (options.opponent) {
410
402
  return runSingleFight({ ...options, opponent: options.opponent });
@@ -418,13 +410,21 @@ async function runSingleFight(options) {
418
410
  Bot: ${botInfo.exportName}`);
419
411
  console.log(` Opponent: ${options.opponent}
420
412
  `);
421
- const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
422
- const opponentBundle = resolveOpponent(options.opponent);
423
413
  const start = Date.now();
424
- const result = await sandboxFight(userBundle, opponentBundle, {
425
- seed: options.seed,
426
- compileOptions: getCoreCompileOptions()
427
- });
414
+ let result;
415
+ if (isHandleSelector(options.opponent)) {
416
+ console.log(" Resolving uploaded opponent...");
417
+ const remote = await resolveRemoteOpponent(options.opponent);
418
+ const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
419
+ result = await runBundleFight(userBundle, remote.bundle, { seed: options.seed ?? 1 });
420
+ } else {
421
+ const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
422
+ const opponentBundle = resolveOpponent(options.opponent);
423
+ result = await sandboxFight(userBundle, opponentBundle, {
424
+ seed: options.seed,
425
+ compileOptions: getCoreCompileOptions()
426
+ });
427
+ }
428
428
  const elapsed = Date.now() - start;
429
429
  const { wizard1Wins, wizard2Wins, draws } = result;
430
430
  const total = wizard1Wins + wizard2Wins + draws;
@@ -440,7 +440,7 @@ async function runSingleFight(options) {
440
440
  async function runFullFight(options) {
441
441
  const projectDir = process.cwd();
442
442
  const botInfo = await discoverBot(projectDir, options.bot);
443
- const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
443
+ const userBundle = new BotBundle(botInfo.sourcePath, botInfo.exportName);
444
444
  const opponents = getBuiltinBotNames();
445
445
  console.log(`
446
446
  Fighting ${botInfo.exportName} against ${opponents.length} built-in bots...
@@ -494,13 +494,13 @@ async function runFullFight(options) {
494
494
  console.log("");
495
495
  }
496
496
  function getHistoryPath(projectDir) {
497
- return path3.join(projectDir, ".vibemancer", "history.json");
497
+ return path2.join(projectDir, ".vibemancer", "history.json");
498
498
  }
499
499
  function loadHistory(projectDir) {
500
500
  const historyPath = getHistoryPath(projectDir);
501
- if (!fs3.existsSync(historyPath)) return [];
501
+ if (!fs2.existsSync(historyPath)) return [];
502
502
  try {
503
- const raw = fs3.readFileSync(historyPath, "utf-8");
503
+ const raw = fs2.readFileSync(historyPath, "utf-8");
504
504
  return JSON.parse(raw);
505
505
  } catch {
506
506
  return [];
@@ -508,10 +508,10 @@ function loadHistory(projectDir) {
508
508
  }
509
509
  function saveHistory(projectDir, history, current) {
510
510
  const historyPath = getHistoryPath(projectDir);
511
- const dir = path3.dirname(historyPath);
512
- fs3.mkdirSync(dir, { recursive: true });
511
+ const dir = path2.dirname(historyPath);
512
+ fs2.mkdirSync(dir, { recursive: true });
513
513
  const updated = [...history.slice(-19), current];
514
- fs3.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
514
+ fs2.writeFileSync(historyPath, JSON.stringify(updated, null, " ") + "\n");
515
515
  }
516
516
  function showDiff(current, previous) {
517
517
  const prevMap = new Map(previous.map((e) => [e.opponent, e]));
@@ -542,8 +542,9 @@ function showDiff(current, previous) {
542
542
 
543
543
  // src/commands/trace.ts
544
544
  import {
545
- BotBundle as BotBundle3,
545
+ BotBundle as BotBundle2,
546
546
  sandboxSimulate,
547
+ runBundleSimulate,
547
548
  extractTraceEvents,
548
549
  summarizeTrace,
549
550
  formatTraceEvents,
@@ -556,18 +557,30 @@ import {
556
557
  async function runTrace(options) {
557
558
  const projectDir = process.cwd();
558
559
  const botInfo = await discoverBot(projectDir, options.bot);
559
- const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
560
- const opponentBundle = resolveOpponent(options.opponent);
561
560
  const distance = options.distance ?? 600;
562
561
  console.log(`
563
562
  Trace: ${botInfo.exportName} (W1) vs ${options.opponent} (W2) | distance: ${distance} | seed: ${options.seed ?? 1}
564
563
  `);
565
- const result = await sandboxSimulate(userBundle, opponentBundle, {
566
- seed: options.seed ?? 1,
567
- spawnDistance: distance,
568
- maxTicks: options.maxTicks ?? 3e3,
569
- compileOptions: getCoreCompileOptions()
570
- });
564
+ let result;
565
+ if (isHandleSelector(options.opponent)) {
566
+ console.log(" Resolving uploaded opponent...");
567
+ const remote = await resolveRemoteOpponent(options.opponent);
568
+ const userBundle = await compileSingleBotBundle(botInfo.sourcePath, botInfo.exportName);
569
+ result = await runBundleSimulate(userBundle, remote.bundle, {
570
+ seed: options.seed ?? 1,
571
+ spawnDistance: distance,
572
+ maxTicks: options.maxTicks ?? 3e3
573
+ });
574
+ } else {
575
+ const userBundle = new BotBundle2(botInfo.sourcePath, botInfo.exportName);
576
+ const opponentBundle = resolveOpponent(options.opponent);
577
+ result = await sandboxSimulate(userBundle, opponentBundle, {
578
+ seed: options.seed ?? 1,
579
+ spawnDistance: distance,
580
+ maxTicks: options.maxTicks ?? 3e3,
581
+ compileOptions: getCoreCompileOptions()
582
+ });
583
+ }
571
584
  const history = result.history;
572
585
  if (history.length === 0) {
573
586
  console.log(" No history available.\n");
@@ -589,11 +602,11 @@ async function runTrace(options) {
589
602
  }
590
603
 
591
604
  // src/commands/tournament.ts
592
- import { BotBundle as BotBundle4, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
605
+ import { BotBundle as BotBundle3, sandboxFight as sandboxFight2, scoreFight as scoreFight2, scoreFightAsWizard2 } from "@vibemancer/core";
593
606
  async function runTournament(options) {
594
607
  const projectDir = process.cwd();
595
608
  const botInfo = await discoverBot(projectDir, options.bot);
596
- const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
609
+ const userBundle = new BotBundle3(botInfo.sourcePath, botInfo.exportName);
597
610
  const opponentNames = options.opponents && options.opponents.length > 0 ? options.opponents : getBuiltinBotNames();
598
611
  const participants = [
599
612
  { name: botInfo.exportName, bundle: userBundle }
@@ -664,7 +677,7 @@ async function runTournament(options) {
664
677
 
665
678
  // src/commands/optimize.ts
666
679
  import { readFileSync, writeFileSync } from "fs";
667
- import { BotBundle as BotBundle5, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
680
+ import { BotBundle as BotBundle4, MatchSandbox, scoreFight as scoreFight3, generateCandidates, getEffectiveRange } from "@vibemancer/core";
668
681
  var USEPAR_RE = /useParam\(\s*['"](\w+)['"]\s*,\s*([^,)]+?)\s*(?:,\s*\{([^}]*)\})?\s*\)/g;
669
682
  function parseNumValue(s) {
670
683
  const n = parseFloat(s.trim());
@@ -780,7 +793,7 @@ async function runOptimize(options) {
780
793
  const maxRounds = options.rounds ?? 3;
781
794
  const opponentNames = resolveOpponentNames(options.opponents);
782
795
  const opponentBundles = opponentNames.map((name) => resolveOpponent(name));
783
- const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
796
+ const userBundle = new BotBundle4(botInfo.sourcePath, botInfo.exportName);
784
797
  console.log(` Opponents: ${opponentBundles.length}`);
785
798
  console.log(` Steps per param: ${steps}`);
786
799
  console.log(` Max rounds: ${maxRounds}
@@ -848,14 +861,14 @@ function escapeRegex(s) {
848
861
  }
849
862
 
850
863
  // src/commands/build.ts
851
- import fs4 from "fs";
852
- import path4 from "path";
853
- import { BotBundle as BotBundle6, compileMatchBundle } from "@vibemancer/core";
864
+ import fs3 from "fs";
865
+ import path3 from "path";
866
+ import { BotBundle as BotBundle5, compileMatchBundle } from "@vibemancer/core";
854
867
  async function runBuild(options) {
855
868
  const projectDir = process.cwd();
856
869
  const botInfo = await discoverBot(projectDir, options.bot);
857
870
  const coreSourceDir = findCoreSourceDir();
858
- const userBundle = new BotBundle6(botInfo.sourcePath, botInfo.exportName);
871
+ const userBundle = new BotBundle5(botInfo.sourcePath, botInfo.exportName);
859
872
  const opponentBundle = resolveOpponent(options.opponent);
860
873
  console.log(`
861
874
  Compiling ${botInfo.exportName} vs ${options.opponent}...`);
@@ -867,9 +880,9 @@ async function runBuild(options) {
867
880
  });
868
881
  const elapsed = Date.now() - start;
869
882
  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);
883
+ const outDir = path3.dirname(path3.resolve(outFile));
884
+ fs3.mkdirSync(outDir, { recursive: true });
885
+ fs3.writeFileSync(path3.resolve(outFile), bundle);
873
886
  const sizeKb = (bundle.length / 1024).toFixed(1);
874
887
  console.log(` Output: ${outFile} (${sizeKb} KB)`);
875
888
  console.log(` Compiled in ${elapsed}ms
@@ -966,7 +979,7 @@ function getStyleDescription(bot) {
966
979
 
967
980
  // src/commands/upload.ts
968
981
  import { createHash } from "crypto";
969
- import fs5 from "fs";
982
+ import fs4 from "fs";
970
983
  async function runUpload(options) {
971
984
  const projectDir = process.cwd();
972
985
  const botInfo = await discoverBot(projectDir, options.bot);
@@ -979,25 +992,21 @@ async function runUpload(options) {
979
992
  console.error(` Error: Bundle too large (${sizeKb} KB). Max 500 KB.`);
980
993
  process.exit(1);
981
994
  }
982
- const nameMatch = bundle.match(/export\s+(?:const|var|let)\s+name\s*=\s*["']([^"']+)["']/);
983
- const wizardName = nameMatch?.[1] ?? botInfo.exportName;
995
+ const wizardName = botInfo.exportName;
984
996
  const bundleHash = createHash("sha256").update(bundle).digest("hex");
985
997
  console.log(` Wizard: ${wizardName}`);
986
998
  console.log(` Hash: ${bundleHash.slice(0, 12)}...`);
987
999
  console.log(" Uploading to VibeMancer...");
988
1000
  try {
989
- const { initializeApp } = await import("firebase/app");
1001
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
990
1002
  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
- });
1003
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1004
+ const app = initializeApp2(FIREBASE_CONFIG2);
996
1005
  const functions = getFunctions(app, "us-central1");
997
1006
  const uploadFn = httpsCallable(functions, "uploadWizard");
998
1007
  let sourceCode;
999
1008
  try {
1000
- sourceCode = fs5.readFileSync(botInfo.sourcePath, "utf-8");
1009
+ sourceCode = fs4.readFileSync(botInfo.sourcePath, "utf-8");
1001
1010
  } catch {
1002
1011
  }
1003
1012
  const result = await uploadFn({
@@ -1030,38 +1039,47 @@ async function runUpload(options) {
1030
1039
  }
1031
1040
 
1032
1041
  // src/commands/pull.ts
1033
- import fs6 from "fs";
1042
+ import fs5 from "fs";
1034
1043
  async function runPull(options) {
1035
1044
  const projectDir = process.cwd();
1036
1045
  const botInfo = await discoverBot(projectDir, options.bot);
1037
1046
  console.log(`
1038
1047
  Pulling latest source for ${botInfo.exportName}...`);
1039
1048
  try {
1040
- const { initializeApp } = await import("firebase/app");
1049
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
1041
1050
  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
- });
1051
+ const { getFirestore: getFirestore2, collection: collection2, query: query2, where: where2, getDocs: getDocs2, doc, getDoc, limit: fbLimit } = await import("firebase/firestore");
1052
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1053
+ const app = initializeApp2(FIREBASE_CONFIG2);
1048
1054
  const auth = getAuth(app);
1049
1055
  console.log(" Signing in...");
1050
1056
  const cred = await signInWithPopup(auth, new GoogleAuthProvider());
1051
1057
  const userId = cred.user.uid;
1052
1058
  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),
1059
+ const db = getFirestore2(app);
1060
+ const wizardSnap = await getDocs2(query2(
1061
+ collection2(db, "wizards"),
1062
+ where2("ownerId", "==", userId),
1063
+ where2("name", "==", botInfo.exportName),
1064
+ where2("active", "==", true),
1059
1065
  fbLimit(1)
1060
- );
1061
- const wizardSnap = await getDocs(wizardQuery);
1066
+ ));
1062
1067
  if (wizardSnap.empty) {
1063
- console.error(` No active wizard named "${botInfo.exportName}" found on your account.`);
1064
- console.error(" Upload first with: vibemancer upload");
1068
+ const allBots = await getDocs2(query2(
1069
+ collection2(db, "wizards"),
1070
+ where2("ownerId", "==", userId),
1071
+ where2("active", "==", true),
1072
+ fbLimit(20)
1073
+ ));
1074
+ if (allBots.empty) {
1075
+ console.error(" No active wizards found on your account.");
1076
+ console.error(" Upload first with: vibemancer upload");
1077
+ } else {
1078
+ const names = allBots.docs.map((d) => d.data().name).filter(Boolean);
1079
+ console.error(` No wizard named "${botInfo.exportName}" found. Your active wizards:`);
1080
+ for (const n of names) console.error(` - ${n}`);
1081
+ console.error("\n Rename your local export to match, or upload first.");
1082
+ }
1065
1083
  process.exit(1);
1066
1084
  }
1067
1085
  const wizardId = wizardSnap.docs[0].id;
@@ -1077,7 +1095,7 @@ async function runPull(options) {
1077
1095
  console.error(" Source code is empty. Re-upload to store it.");
1078
1096
  process.exit(1);
1079
1097
  }
1080
- fs6.writeFileSync(botInfo.sourcePath, sourceCode);
1098
+ fs5.writeFileSync(botInfo.sourcePath, sourceCode);
1081
1099
  console.log(` \u2713 Updated ${botInfo.sourcePath}`);
1082
1100
  console.log(` ${(sourceCode.length / 1024).toFixed(1)} KB written`);
1083
1101
  } catch (err) {
@@ -1090,6 +1108,60 @@ async function runPull(options) {
1090
1108
  }
1091
1109
  }
1092
1110
 
1111
+ // src/commands/feedback.ts
1112
+ async function runFeedback(options) {
1113
+ const message = options.message.trim();
1114
+ if (!message) {
1115
+ console.error(" Error: feedback message cannot be empty.");
1116
+ process.exit(1);
1117
+ }
1118
+ console.log("\n Submitting feedback...");
1119
+ try {
1120
+ const { initializeApp: initializeApp2 } = await import("firebase/app");
1121
+ const { getFunctions, httpsCallable } = await import("firebase/functions");
1122
+ const { FIREBASE_CONFIG: FIREBASE_CONFIG2 } = await import("./firebase-config-F6COE3NY.js");
1123
+ const app = initializeApp2(FIREBASE_CONFIG2);
1124
+ const functions = getFunctions(app, "us-central1");
1125
+ const submitFn = httpsCallable(functions, "submitFeedback");
1126
+ await submitFn({ message });
1127
+ console.log(" Sent! Thanks for the feedback.\n");
1128
+ } catch (err) {
1129
+ const msg = err instanceof Error ? err.message : String(err);
1130
+ console.error(` Failed to submit: ${msg}
1131
+ `);
1132
+ process.exit(1);
1133
+ }
1134
+ }
1135
+
1136
+ // src/commands/missile-calc.ts
1137
+ import { calculateMissileCastTime, GCD_DURATION, TICKS_PER_SECOND, calculateMissileRadius } from "@vibemancer/core";
1138
+ function runMissileCalc(options) {
1139
+ const config = {
1140
+ damage: options.damage,
1141
+ speed: options.speed,
1142
+ duration: options.duration,
1143
+ turnRate: options.turnRate
1144
+ };
1145
+ const castTimeSec = calculateMissileCastTime(config);
1146
+ const castFrames = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
1147
+ const gcdSec = GCD_DURATION / TICKS_PER_SECOND;
1148
+ const totalCycleSec = castTimeSec + gcdSec;
1149
+ const dps = config.damage / totalCycleSec;
1150
+ const range = config.speed * config.duration;
1151
+ const radius = calculateMissileRadius(config.damage);
1152
+ console.log(`
1153
+ Missile Calculator
1154
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1155
+ Config: damage=${config.damage} speed=${config.speed} duration=${config.duration} turnRate=${config.turnRate}
1156
+ Cast time: ${castTimeSec.toFixed(3)}s (${castFrames} frames)
1157
+ GCD: ${gcdSec}s (${GCD_DURATION} frames)
1158
+ Full cycle: ${totalCycleSec.toFixed(3)}s (cast + GCD)
1159
+ Eff. DPS: ${dps.toFixed(2)} HP/s
1160
+ Range: ${range} units (speed \xD7 duration)
1161
+ Hitbox: ${radius.toFixed(2)} radius
1162
+ `);
1163
+ }
1164
+
1093
1165
  // src/cli.ts
1094
1166
  var args = process.argv.slice(2);
1095
1167
  var command = args[0];
@@ -1128,6 +1200,8 @@ Commands:
1128
1200
  build Compile bot to a standalone bundle
1129
1201
  upload Upload bot to VibeMancer for online competition
1130
1202
  pull Download latest source code from VibeMancer
1203
+ feedback Submit a bug report or suggestion
1204
+ missile-calc Calculate missile cast time and DPS for a config
1131
1205
 
1132
1206
  Common options:
1133
1207
  --bot <path> Path to bot source file (default: auto-discover)
@@ -1136,11 +1210,12 @@ Dev options:
1136
1210
  --port <n> Server port (default: 4242)
1137
1211
 
1138
1212
  Fight options:
1139
- --opponent <name> Fight a single opponent instead of all bots
1213
+ --opponent <name> Fight a single opponent: a built-in name OR another
1214
+ user's uploaded bot as handle/botname
1140
1215
  --seed <n> Random seed
1141
1216
 
1142
1217
  Trace options:
1143
- --opponent <name> Opponent bot name (required)
1218
+ --opponent <name> Opponent: a built-in name OR handle/botname (required)
1144
1219
  --seed <n> Random seed (default: 1)
1145
1220
  --distance <n> Spawn distance (default: 600)
1146
1221
 
@@ -1163,6 +1238,8 @@ Examples:
1163
1238
  vibemancer tournament Battlemage Warmage Archmage
1164
1239
  vibemancer optimize
1165
1240
  vibemancer build --opponent Battlemage
1241
+ vibemancer feedback "missiles go through walls sometimes"
1242
+ vibemancer missile-calc --damage 15 --speed 7 --duration 200 --turnRate 3
1166
1243
  `);
1167
1244
  }
1168
1245
  async function main() {
@@ -1192,7 +1269,14 @@ async function main() {
1192
1269
  case "trace": {
1193
1270
  const opponent = parseFlag("--opponent");
1194
1271
  if (!opponent) {
1195
- console.error("Error: --opponent is required for trace command.");
1272
+ const { getBuiltinBotNames: getBuiltinBotNames2 } = await import("./opponent-resolver-U2UK2QEJ.js");
1273
+ const names = getBuiltinBotNames2();
1274
+ const list = names.map((n) => ` --opponent ${n}`).join("\n");
1275
+ console.error("Error: --opponent is required for trace.\n");
1276
+ console.error(`Available opponents:
1277
+
1278
+ ${list}
1279
+ `);
1196
1280
  console.error("Usage: vibemancer trace --opponent Battlemage");
1197
1281
  process.exit(1);
1198
1282
  }
@@ -1254,6 +1338,20 @@ async function main() {
1254
1338
  await runPull({ bot });
1255
1339
  break;
1256
1340
  }
1341
+ case "feedback": {
1342
+ const message = args.slice(1).join(" ");
1343
+ await runFeedback({ message });
1344
+ break;
1345
+ }
1346
+ case "missile-calc": {
1347
+ runMissileCalc({
1348
+ damage: parseIntFlag("--damage", 15),
1349
+ speed: parseIntFlag("--speed", 7),
1350
+ duration: parseIntFlag("--duration", 200),
1351
+ turnRate: parseIntFlag("--turnRate", 0)
1352
+ });
1353
+ break;
1354
+ }
1257
1355
  default:
1258
1356
  console.error(`Unknown command: ${command}`);
1259
1357
  printHelp();