@mauricode/token-derby 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/bin.js +951 -138
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -113,27 +113,64 @@ function tagColor(tag, colors) {
|
|
|
113
113
|
return null;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// src/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
116
|
+
// src/hats/render.ts
|
|
117
|
+
function composeHatGrid(baseSprite, hat, variantIdx, horseColors) {
|
|
118
|
+
const horseH = baseSprite.length;
|
|
119
|
+
const horseW = baseSprite[0]?.length ?? 0;
|
|
120
|
+
const hatH = hat.rows.length;
|
|
121
|
+
const hatW = hat.width;
|
|
122
|
+
const ext = Math.max(0, hatH - 4);
|
|
123
|
+
const canvasLeft = Math.min(0, hat.anchor_x);
|
|
124
|
+
const canvasRight = Math.max(horseW, hat.anchor_x + hatW);
|
|
125
|
+
const canvasW = canvasRight - canvasLeft;
|
|
126
|
+
const horseOffsetX = -canvasLeft;
|
|
127
|
+
const grid = Array.from(
|
|
128
|
+
{ length: horseH + ext },
|
|
129
|
+
() => Array(canvasW).fill(null)
|
|
130
|
+
);
|
|
131
|
+
for (let y = 0; y < horseH; y++) {
|
|
132
|
+
for (let x = 0; x < horseW; x++) {
|
|
133
|
+
const tag = baseSprite[y][x];
|
|
134
|
+
if (tag === null) continue;
|
|
135
|
+
grid[y + ext][x + horseOffsetX] = tagToColor(tag, horseColors);
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
|
-
|
|
138
|
+
const hatColors = hatColorsFor(hat, variantIdx);
|
|
139
|
+
for (let y = 0; y < hatH; y++) {
|
|
140
|
+
const row = hat.rows[y];
|
|
141
|
+
for (let x = 0; x < hatW; x++) {
|
|
142
|
+
const ch = row[x];
|
|
143
|
+
if (ch === "." || ch === void 0) continue;
|
|
144
|
+
const gx = hat.anchor_x + x + horseOffsetX;
|
|
145
|
+
if (gx < 0 || gx >= canvasW) continue;
|
|
146
|
+
const color = ch === "A" ? hatColors.A : hatColors.Q ?? hatColors.A;
|
|
147
|
+
grid[y][gx] = color;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { grid, offsetX: canvasLeft };
|
|
151
|
+
}
|
|
152
|
+
function tagToColor(tag, c) {
|
|
153
|
+
switch (tag) {
|
|
154
|
+
case "B":
|
|
155
|
+
return c.body;
|
|
156
|
+
case "M":
|
|
157
|
+
return c.mane;
|
|
158
|
+
case "T":
|
|
159
|
+
return c.tail;
|
|
160
|
+
case "S":
|
|
161
|
+
return c.saddle;
|
|
162
|
+
case "E":
|
|
163
|
+
return FIXED_COLORS.E;
|
|
164
|
+
case "H":
|
|
165
|
+
return FIXED_COLORS.H;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function hatColorsFor(hat, variantIdx) {
|
|
169
|
+
if (hat.rarity === "legendary") return hat.colors;
|
|
170
|
+
return hat.variants[variantIdx] ?? hat.variants[0];
|
|
136
171
|
}
|
|
172
|
+
|
|
173
|
+
// src/ui/half-blocks.ts
|
|
137
174
|
var RESET = "\x1B[0m";
|
|
138
175
|
function hexToRgb(hex) {
|
|
139
176
|
const h = hex.replace("#", "");
|
|
@@ -151,6 +188,52 @@ function ansiBg(hex) {
|
|
|
151
188
|
const [r, g, b] = hexToRgb(hex);
|
|
152
189
|
return `\x1B[48;2;${r};${g};${b}m`;
|
|
153
190
|
}
|
|
191
|
+
function hexGridToHalfBlocks(grid) {
|
|
192
|
+
const W = grid[0]?.length ?? 0;
|
|
193
|
+
const padded = grid.length % 2 === 0 ? grid : [...grid, Array(W).fill(null)];
|
|
194
|
+
const lines = [];
|
|
195
|
+
for (let y = 0; y < padded.length; y += 2) {
|
|
196
|
+
let line = "";
|
|
197
|
+
for (let x = 0; x < W; x++) {
|
|
198
|
+
const top = padded[y][x] ?? null;
|
|
199
|
+
const bot = padded[y + 1][x] ?? null;
|
|
200
|
+
if (top === null && bot === null) line += " ";
|
|
201
|
+
else if (top !== null && bot !== null) line += ansiFg(top) + ansiBg(bot) + "\u2580" + RESET;
|
|
202
|
+
else if (top !== null) line += ansiFg(top) + "\u2580" + RESET;
|
|
203
|
+
else line += ansiFg(bot) + "\u2584" + RESET;
|
|
204
|
+
}
|
|
205
|
+
lines.push(line);
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/ui/HorseSprite.tsx
|
|
211
|
+
import { jsx } from "react/jsx-runtime";
|
|
212
|
+
function HorseSprite({ sprite, colors, hat }) {
|
|
213
|
+
if (!hat) {
|
|
214
|
+
const grid2 = renderSprite(sprite, colors);
|
|
215
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: grid2.map((row, y) => /* @__PURE__ */ jsx(Text, { children: rowToAnsi(row) }, y)) });
|
|
216
|
+
}
|
|
217
|
+
const { grid } = composeHatGrid(sprite, hat.hat, hat.variant ?? 0, colors);
|
|
218
|
+
const lines = hexGridToHalfBlocks(grid);
|
|
219
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { children: line }, i)) });
|
|
220
|
+
}
|
|
221
|
+
var RESET2 = "\x1B[0m";
|
|
222
|
+
function rowToAnsi(row) {
|
|
223
|
+
let out = "";
|
|
224
|
+
for (const cell of row) {
|
|
225
|
+
if (cell.top === null && cell.bottom === null) {
|
|
226
|
+
out += " ";
|
|
227
|
+
} else if (cell.top !== null && cell.bottom !== null) {
|
|
228
|
+
out += ansiFg(cell.top) + ansiBg(cell.bottom) + "\u2580" + RESET2;
|
|
229
|
+
} else if (cell.top !== null) {
|
|
230
|
+
out += ansiFg(cell.top) + "\u2580" + RESET2;
|
|
231
|
+
} else {
|
|
232
|
+
out += ansiFg(cell.bottom) + "\u2584" + RESET2;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
154
237
|
|
|
155
238
|
// src/ui/palette.ts
|
|
156
239
|
var SLOTS = ["body", "mane", "tail", "saddle"];
|
|
@@ -337,8 +420,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
|
337
420
|
// src/version.ts
|
|
338
421
|
import { createRequire } from "module";
|
|
339
422
|
function readVersion() {
|
|
340
|
-
if ("2.
|
|
341
|
-
return "2.
|
|
423
|
+
if ("2.5.0".length > 0) {
|
|
424
|
+
return "2.5.0";
|
|
342
425
|
}
|
|
343
426
|
try {
|
|
344
427
|
const req = createRequire(import.meta.url);
|
|
@@ -358,6 +441,32 @@ var USER_NAME_MAX_LENGTH = 40;
|
|
|
358
441
|
var ORG_NAME_MAX_LENGTH = 12;
|
|
359
442
|
var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
|
|
360
443
|
|
|
444
|
+
// ../shared/dist/version-match.js
|
|
445
|
+
var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
446
|
+
function parseSemver(v) {
|
|
447
|
+
if (typeof v !== "string")
|
|
448
|
+
return null;
|
|
449
|
+
const m = SEMVER_RE.exec(v.trim());
|
|
450
|
+
if (!m)
|
|
451
|
+
return null;
|
|
452
|
+
return {
|
|
453
|
+
major: Number(m[1]),
|
|
454
|
+
minor: Number(m[2]),
|
|
455
|
+
patch: Number(m[3])
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function gteSemver(a, b) {
|
|
459
|
+
const pa = parseSemver(a);
|
|
460
|
+
const pb = parseSemver(b);
|
|
461
|
+
if (!pa || !pb)
|
|
462
|
+
return false;
|
|
463
|
+
if (pa.major !== pb.major)
|
|
464
|
+
return pa.major > pb.major;
|
|
465
|
+
if (pa.minor !== pb.minor)
|
|
466
|
+
return pa.minor > pb.minor;
|
|
467
|
+
return pa.patch >= pb.patch;
|
|
468
|
+
}
|
|
469
|
+
|
|
361
470
|
// ../shared/dist/levels.js
|
|
362
471
|
var MAX_LEVEL = 30;
|
|
363
472
|
function xpForLevel(n) {
|
|
@@ -404,6 +513,91 @@ function overtakeDescription(positionsClimbed) {
|
|
|
404
513
|
return "Overtook another horse";
|
|
405
514
|
return `Overtook ${positionsClimbed} horses`;
|
|
406
515
|
}
|
|
516
|
+
var TOKEN_INPUT_MULTIPLIER = 10;
|
|
517
|
+
function tokenMultiplier(race) {
|
|
518
|
+
return race.counts_input ? TOKEN_INPUT_MULTIPLIER : 1;
|
|
519
|
+
}
|
|
520
|
+
function describeAchievement(event, race) {
|
|
521
|
+
if (event.name === "Overtake!") {
|
|
522
|
+
return overtakeDescription(Math.floor(event.xp / 3));
|
|
523
|
+
}
|
|
524
|
+
const m = tokenMultiplier(race);
|
|
525
|
+
if (event.name === "Stampede!") {
|
|
526
|
+
return `Gained ${(MIDRACE_THRESHOLDS.stampede_tokens * m).toLocaleString("en-US")}+ tokens in a single minute`;
|
|
527
|
+
}
|
|
528
|
+
if (event.name === "Pulled Away!") {
|
|
529
|
+
return `Grew the lead by ${(MIDRACE_THRESHOLDS.pulled_away_gap * m).toLocaleString("en-US")}+ tokens in a minute`;
|
|
530
|
+
}
|
|
531
|
+
return ACHIEVEMENT_DESCRIPTIONS[event.name];
|
|
532
|
+
}
|
|
533
|
+
var MIDRACE_THRESHOLDS = {
|
|
534
|
+
warm_up_fraction: 0.08,
|
|
535
|
+
// first 8% of race time
|
|
536
|
+
streak_hour_ms: 36e5,
|
|
537
|
+
// 1 hour for Racer!/Pacesetter!
|
|
538
|
+
racer_dt_cap_ms: 9e4,
|
|
539
|
+
// single-tick credit cap for Racer!
|
|
540
|
+
stampede_tokens: 7e3,
|
|
541
|
+
// tokens-in-a-minute threshold
|
|
542
|
+
stampede_cooldown_ms: 72e5,
|
|
543
|
+
// 2 hours
|
|
544
|
+
pulled_away_gap: 5e3,
|
|
545
|
+
// gap-growth threshold per minute
|
|
546
|
+
pulled_away_cooldown_ms: 72e5,
|
|
547
|
+
// 2 hours
|
|
548
|
+
recent_events_retention_ms: 9e4
|
|
549
|
+
// sliding window for recent_events
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// ../shared/dist/hats.js
|
|
553
|
+
function hatById(id) {
|
|
554
|
+
return HATS.find((h) => h.id === id);
|
|
555
|
+
}
|
|
556
|
+
var HATS = [
|
|
557
|
+
// ── COMMON (18) ────────────────────────────────────────────────────────
|
|
558
|
+
{ id: "flat_cap", name: "Flat Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...........", "....AAAA...", "...AAAAAA..", "..AAAAAAA.."], variants: [{ A: "#8B6914" }, { A: "#12301b" }, { A: "#aab5cb" }, { A: "#bdcbaa" }] },
|
|
559
|
+
{ id: "bucket_hat", name: "Bucket Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "...AAAAA...", "...AAAAA...", ".AAAAAAAAA."], variants: [{ A: "#CC2200", Q: "#FFFFFF" }, { A: "#0007cc", Q: "#FFFFFF" }, { A: "#becc00", Q: "#4d3dc7" }, { A: "#cc00c5", Q: "#d6d6dc" }] },
|
|
560
|
+
{ id: "beanie", name: "Beanie", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#4A7C59" }, { A: "#b21f35" }, { A: "#ec9418" }, { A: "#3a17e6" }] },
|
|
561
|
+
{ id: "stetson", name: "Stetson", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AA.AA...", "...AAAAA...", "...AAAAA...", "A..AAAAA..A", "AAAAAAAAAAA", "AAAAAAAAAAA"], variants: [{ A: "#C49A00" }, { A: "#272004" }, { A: "#040404" }, { A: "#373606" }] },
|
|
562
|
+
{ id: "party_hat", name: "Party Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "....AQA....", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FF69B4", Q: "#FFD700" }, { A: "#c46bff", Q: "#ff000d" }, { A: "#fdff6b", Q: "#0400ff" }, { A: "#6bff97", Q: "#05050a" }] },
|
|
563
|
+
{ id: "fez", name: "Fez", rarity: "common", width: 11, anchor_x: 22, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....Q......", "....AAA....", "....AAA....", "....AAA...."], variants: [{ A: "#CC0000", Q: "#8B0000" }, { A: "#CC0000", Q: "#f5e5e5" }, { A: "#CC0000", Q: "#f1e209" }, { A: "#CC0000", Q: "#0956f1" }] },
|
|
564
|
+
{ id: "sailor_hat", name: "Sailor Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...QQQQQ...", "..AAAAAAA..", "..QQQQQQQ..", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#000080" }, { A: "#FFFFFF", Q: "#c0c0ed" }, { A: "#FFFFFF", Q: "#00000a" }, { A: "#FFFFFF", Q: "#0505ef" }] },
|
|
565
|
+
{ id: "newsboy_cap", name: "Newsboy Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA..", "..AAAAAA..."], variants: [{ A: "#5C4033", Q: "#3D2B1F" }, { A: "#e96020", Q: "#131211" }, { A: "#342c28", Q: "#131211" }, { A: "#0a0300", Q: "#ddab78" }] },
|
|
566
|
+
{ id: "tam_o_shanter", name: "Tam O'Shanter", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "....AAA....", "..AAAAAAA..", "..AAAAAAA..", "...AAAAA..."], variants: [{ A: "#006400", Q: "#FF0000" }, { A: "#ff0000", Q: "#016400" }] },
|
|
567
|
+
{ id: "trucker_cap", name: "Trucker Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAQQ....", "...AAQQ....", "..AAQQQAA..", "..AAQQQAA.."], variants: [{ A: "#2196F3", Q: "#FFFFFF" }, { A: "#f41fe3", Q: "#FFFFFF" }, { A: "#1ff45f", Q: "#FFFFFF" }] },
|
|
568
|
+
{ id: "hard_hat", name: "Hard Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#FFD600" }, { A: "#d74c1d" }] },
|
|
569
|
+
{ id: "chef_toque", name: "Chef's Toque", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...AAAAA...", "..AAQAQAA..", "...AQAQA...", "...AQAQA...", "...AQAQA...", "...AAAAA..."], variants: [{ A: "#FFFFFF", Q: "#f9f9f9" }] },
|
|
570
|
+
{ id: "bobble_hat", name: "Bobble Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....Q.....", "...AAAAA...", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#C62828", Q: "#FFFFFF" }, { A: "#293bc7", Q: "#FFFFFF" }, { A: "#29c775", Q: "#231f1f" }, { A: "#a529c7", Q: "#d50707" }] },
|
|
571
|
+
{ id: "cowboy_hat", name: "Cowboy Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "A.AAAAAAA.A", ".AAAAAAAAA."], variants: [{ A: "#8B4513" }, { A: "#0a0400" }, { A: "#956c50" }] },
|
|
572
|
+
{ id: "baseball_cap", name: "Baseball Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "...AAAAA...", "..AAAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#1565C0" }, { A: "#8515c1" }, { A: "#15c157" }, { A: "#ef2d0b" }] },
|
|
573
|
+
{ id: "tinfoil_hat", name: "Tinfoil Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#B0BEC5" }, { A: "#6a6c6c" }] },
|
|
574
|
+
{ id: "dunce_cap", name: "Dunce Cap", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....Q.....", "....QAQ....", "...QAAAQ...", "...QAAAQ...", "..QAAAAAQ..", "..AAAAAAA.."], variants: [{ A: "#F8F8F8", Q: "#F44336" }] },
|
|
575
|
+
{ id: "mini_top_hat", name: "Mini Top Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...AAAAA...", "...AAAAA...", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#212121" }, { A: "#626060" }] },
|
|
576
|
+
// ── RARE (10) ──────────────────────────────────────────────────────────
|
|
577
|
+
{ id: "bicorne", name: "Bicorne", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AAAQQQQ..", "..AAAAAAA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#1A237E", Q: "#FFD700" }, { A: "#01052d", Q: "#544803" }] },
|
|
578
|
+
{ id: "viking_helmet", name: "Viking Helmet", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..A.AAA.A..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#9E9E9E", Q: "#8D6E63" }, { A: "#dcdbdb", Q: "#bf3908" }] },
|
|
579
|
+
{ id: "jesters_cap", name: "Jester's Cap", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "...AAAAA...", "...AAAAA..."], variants: [{ A: "#E53935", Q: "#FFD600" }, { A: "#e5a434", Q: "#0040ff" }, { A: "#34e537", Q: "#eeff00" }, { A: "#e234e5", Q: "#00ff40" }] },
|
|
580
|
+
{ id: "plague_doctor", name: "Plague Doctor Beak", rarity: "rare", width: 11, anchor_x: 24, rows: ["...........", "...........", "...........", "...........", "....QQQ....", "...QAAAQQ..", "...QQQQQQQ.", "..QQQQQ.QQQ", "..QQQQ....Q", "..QQQQ....."], variants: [{ A: "#F5F5F5", Q: "#795548" }, { A: "#e1dbdb", Q: "#080707" }] },
|
|
581
|
+
{ id: "morion", name: "Conquistador Morion", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....Q.....", "...QQQQQ...", "...QAAAQ...", ".A..AAA..A.", "..AAQQQAA.."], variants: [{ A: "#B0BEC5", Q: "#FFD600" }, { A: "#0a4768", Q: "#e6c10a" }] },
|
|
582
|
+
{ id: "shako", name: "Shako", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "........Q..", "........Q..", "........Q..", "........Q..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#45464f", Q: "#00ff2a" }, { A: "#45464f", Q: "#ff4d00" }, { A: "#45464f", Q: "#ff0019" }, { A: "#45464f", Q: "#fff700" }] },
|
|
583
|
+
{ id: "centurion_helm", name: "Centurion Helm", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "....QQQ....", "...QQQQQQ..", "..QQQQQQQQ.", ".QQ..A...Q.", ".Q..AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#2f3131", Q: "#C62828" }] },
|
|
584
|
+
{ id: "papal_mitre", name: "Papal Mitre", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....QQQ....", "...QQQQQ...", "...QAAAQ...", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }] },
|
|
585
|
+
{ id: "headdress", name: "Headdress", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#FF8F00", Q: "#1565C0" }] },
|
|
586
|
+
{ id: "sombrero", name: "Sombrero", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "...AQAQA...", "AAAAAAAAAAA"], variants: [{ A: "#F57F17", Q: "#BF360C" }, { A: "#f8b377", Q: "#3e1204" }, { A: "#d6f877", Q: "#04133e" }, { A: "#d6f877", Q: "#04133e" }] },
|
|
587
|
+
// ── EPIC (6) ───────────────────────────────────────────────────────────
|
|
588
|
+
{ id: "papal_tiara", name: "Papal Tiara", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "....QQ.....", "...QQQQQ...", "...QAAQQ...", "...QQQQQ...", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }, { A: "#b18e10", Q: "#f3f3f1" }] },
|
|
589
|
+
{ id: "samurai_kabuto", name: "Samurai Kabuto", rarity: "epic", width: 11, anchor_x: 23, rows: ["....Q...Q..", "....QQ.QQ..", ".....QQQ...", ".......Q...", "...AAAAQ...", "..AAAAAQQ..", "..AAAAAQQ..", ".AAAAAAAQ..", "AAAAAAAAQ..", "AAAAAAAAQ.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#050505", Q: "#C62828" }, { A: "#675f5f", Q: "#c4c729" }] },
|
|
590
|
+
{ id: "gladiator_galea", name: "Gladiator Galea", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "....QQQQQQ.", "...QQQQQ...", "...QQQQQ...", "...QAAA....", "..AAAAAAA..", "..AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#C62828" }, { A: "#ffe907", Q: "#C62828" }] },
|
|
591
|
+
{ id: "pharaoh_nemes", name: "Pharaoh Nemes", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "..AAAAAAA..", "..AAQAQAA..", "..QAQAQAQ..", "..QAQAQAQ..", "..QQAAAQQ..", "..QQAAAAA..", "...QAQA...."], variants: [{ A: "#FFD700", Q: "#1565C0" }, { A: "#8d342a", Q: "#63686e" }] },
|
|
592
|
+
{ id: "spartan_helmet", name: "Spartan Helmet", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "........Q..", ".......QQ..", ".....QQQQ..", "....QQQQQ..", "..QQQQQQ...", ".QQQAA.....", "QQAAAAAAA..", "Q.AAAAAAA..", "..AQQQQQA.."], variants: [{ A: "#B0BEC5", Q: "#B71C1C" }, { A: "#B0BEC5", Q: "#b2b51c" }, { A: "#B0BEC5", Q: "#b5611c" }] },
|
|
593
|
+
{ id: "conquistador_full", name: "Conquistador Helm", rarity: "epic", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA..", "...AQQQA...", "...AQQQA..."], variants: [{ A: "#B0BEC5", Q: "#FFD700" }, { A: "#B0BEC5", Q: "#0040ff" }, { A: "#B0BEC5", Q: "#36123b" }] },
|
|
594
|
+
// ── LEGENDARY (5) ──────────────────────────────────────────────────────
|
|
595
|
+
{ id: "rainbow_crown", name: "Rainbow Crown", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", ".....A.....", "....AQA....", "....AQA....", "....AQA....", "...AAQAA...", "...AQQQA...", "..AAQQQAA..", "..AAAAAAA.."], colors: { A: "#FFD700", Q: "#553f3f" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF"], fps: 8 } },
|
|
596
|
+
{ id: "inferno_cap", name: "Inferno Cap", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "....AAA....", "...AAAAA...", "....AAA....", ".....Q.....", "A....Q....A", "A...QQQ...A", ".A..QQQ..A.", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF4500", Q: "#FFD700" }, animation: { type: "cycle", frames: ["#FF0000", "#FF2200", "#FF4500", "#FF6600", "#FF8C00", "#FFA500"], fps: 12 } },
|
|
597
|
+
{ id: "void_hood", name: "Void Hood", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...AAA.....", "..AAAAAA...", ".AAAAAAA...", ".AAAAAAAA..", "AAAAAAAAA..", "AAAAQQQAA..", "AAAAQQQAA.."], colors: { A: "#1A0033", Q: "#d9cfe3" }, animation: { type: "cycle", frames: ["#0D0019", "#1A0033", "#2D004D", "#3D0066", "#2D004D", "#1A0033"], fps: 3 } },
|
|
598
|
+
{ id: "prismatic_jester", name: "Prismatic Jester", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "..Q.Q.Q.Q..", "Q..A.A.A..Q", ".Q.A.A.A.Q.", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA..", "..AAAAAAA.."], colors: { A: "#FF0000", Q: "#0000FF" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF", "#FF00FF", "#00FFFF"], fps: 15 } },
|
|
599
|
+
{ id: "aurora_helm", name: "Aurora Helm", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...QQQQQ...", "...QQQQQ...", "..AAAAAAA..", "..AAAAAAA..", "..AQAAAQA..", "..AQAAAQA.."], colors: { A: "#00CED1", Q: "#00FF7F" }, animation: { type: "cycle", frames: ["#0000FF", "#0066FF", "#00BFFF", "#00CED1", "#00FF7F", "#7CFC00", "#00FF7F", "#00CED1"], fps: 4 } }
|
|
600
|
+
];
|
|
407
601
|
|
|
408
602
|
// src/identity/identity.ts
|
|
409
603
|
import { promises as fs } from "fs";
|
|
@@ -607,6 +801,12 @@ function deleteOrgWebhook(orgName) {
|
|
|
607
801
|
void 0
|
|
608
802
|
);
|
|
609
803
|
}
|
|
804
|
+
function rollHat(stableHorseId) {
|
|
805
|
+
return request("POST", `/jockey/me/horses/${encodeURIComponent(stableHorseId)}/roll`, void 0, void 0);
|
|
806
|
+
}
|
|
807
|
+
function equipHat(stableHorseId, body) {
|
|
808
|
+
return request("POST", `/jockey/me/horses/${encodeURIComponent(stableHorseId)}/equip`, body, void 0);
|
|
809
|
+
}
|
|
610
810
|
|
|
611
811
|
// src/commands/stable-create.ts
|
|
612
812
|
async function stableCreateCommand() {
|
|
@@ -739,8 +939,114 @@ async function stableDeleteCommand(name) {
|
|
|
739
939
|
}
|
|
740
940
|
|
|
741
941
|
// src/commands/stable-edit.ts
|
|
742
|
-
import
|
|
942
|
+
import React6 from "react";
|
|
743
943
|
import { render as render3 } from "ink";
|
|
944
|
+
|
|
945
|
+
// src/ui/HatPicker.tsx
|
|
946
|
+
import { useState as useState3 } from "react";
|
|
947
|
+
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
948
|
+
|
|
949
|
+
// src/ui/AnimatedHorseSprite.tsx
|
|
950
|
+
import { useEffect, useState as useState2 } from "react";
|
|
951
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
952
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
953
|
+
function AnimatedHorseSprite({ sprite, colors, hat }) {
|
|
954
|
+
const isLegendary = hat.rarity === "legendary";
|
|
955
|
+
const frames = isLegendary ? hat.animation.frames : [];
|
|
956
|
+
const fps = isLegendary ? hat.animation.fps : 1;
|
|
957
|
+
const [idx, setIdx] = useState2(0);
|
|
958
|
+
useEffect(() => {
|
|
959
|
+
if (!isLegendary || frames.length <= 1) return;
|
|
960
|
+
const interval = setInterval(
|
|
961
|
+
() => setIdx((i) => (i + 1) % frames.length),
|
|
962
|
+
Math.max(1, Math.round(1e3 / fps))
|
|
963
|
+
);
|
|
964
|
+
return () => clearInterval(interval);
|
|
965
|
+
}, [isLegendary, frames.length, fps]);
|
|
966
|
+
const renderedHat = isLegendary && frames[idx] ? { ...hat, colors: { ...hat.colors, A: frames[idx] } } : hat;
|
|
967
|
+
const { grid } = composeHatGrid(sprite, renderedHat, 0, colors);
|
|
968
|
+
const lines = hexGridToHalfBlocks(grid);
|
|
969
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx4(Text4, { children: line }, i)) });
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/ui/HatPicker.tsx
|
|
973
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
974
|
+
function HatPicker({ hats, equipped, colors, onPick, onCancel }) {
|
|
975
|
+
const entries = [
|
|
976
|
+
{ kind: "unequip" },
|
|
977
|
+
...hats.map((c, idx) => ({ kind: "hat", idx, collected: c }))
|
|
978
|
+
];
|
|
979
|
+
const [cursor, setCursor] = useState3(0);
|
|
980
|
+
useInput2((input, key) => {
|
|
981
|
+
if (key.escape) {
|
|
982
|
+
onCancel();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (key.upArrow) {
|
|
986
|
+
setCursor((cursor - 1 + entries.length) % entries.length);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (key.downArrow) {
|
|
990
|
+
setCursor((cursor + 1) % entries.length);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (key.return) {
|
|
994
|
+
const e = entries[cursor];
|
|
995
|
+
onPick(e.kind === "unequip" ? null : e.idx);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
const focused = entries[cursor];
|
|
1000
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
|
|
1001
|
+
/* @__PURE__ */ jsx5(Text5, { children: "Pick a hat to equip:" }),
|
|
1002
|
+
entries.map((e, i) => {
|
|
1003
|
+
const isCursor = i === cursor;
|
|
1004
|
+
if (e.kind === "unequip") {
|
|
1005
|
+
const isEquipped2 = equipped == null;
|
|
1006
|
+
return /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", children: /* @__PURE__ */ jsxs3(Text5, { children: [
|
|
1007
|
+
isCursor ? "\u25BA" : " ",
|
|
1008
|
+
" ",
|
|
1009
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Unequip" }),
|
|
1010
|
+
isEquipped2 ? " \u2713" : ""
|
|
1011
|
+
] }) }, "unequip");
|
|
1012
|
+
}
|
|
1013
|
+
const hat = hatById(e.collected.id);
|
|
1014
|
+
const name = hat?.name ?? e.collected.id;
|
|
1015
|
+
const variantSuffix = hat && hat.rarity !== "legendary" && e.collected.variant !== void 0 ? ` #${e.collected.variant + 1}` : "";
|
|
1016
|
+
const isEquipped = equipped === e.idx;
|
|
1017
|
+
const rarityColor = hat ? hat.rarity === "legendary" ? "yellow" : hat.rarity === "epic" ? "magenta" : hat.rarity === "rare" ? "blue" : "gray" : "gray";
|
|
1018
|
+
return /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", children: /* @__PURE__ */ jsxs3(Text5, { children: [
|
|
1019
|
+
isCursor ? "\u25BA" : " ",
|
|
1020
|
+
" ",
|
|
1021
|
+
name,
|
|
1022
|
+
variantSuffix,
|
|
1023
|
+
" ",
|
|
1024
|
+
/* @__PURE__ */ jsxs3(Text5, { color: rarityColor, children: [
|
|
1025
|
+
"[",
|
|
1026
|
+
hat?.rarity ?? "?",
|
|
1027
|
+
"]"
|
|
1028
|
+
] }),
|
|
1029
|
+
isEquipped ? " \u2713" : ""
|
|
1030
|
+
] }) }, `hat-${e.idx}`);
|
|
1031
|
+
}),
|
|
1032
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Preview:" }) }),
|
|
1033
|
+
/* @__PURE__ */ jsx5(PreviewArea, { focused, colors }),
|
|
1034
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
1035
|
+
] });
|
|
1036
|
+
}
|
|
1037
|
+
function PreviewArea({ focused, colors }) {
|
|
1038
|
+
if (focused.kind === "unequip") {
|
|
1039
|
+
return /* @__PURE__ */ jsx5(HorseSprite, { sprite: MAIN_SPRITE, colors });
|
|
1040
|
+
}
|
|
1041
|
+
const hat = hatById(focused.collected.id);
|
|
1042
|
+
if (!hat) return /* @__PURE__ */ jsx5(HorseSprite, { sprite: MAIN_SPRITE, colors });
|
|
1043
|
+
if (hat.rarity === "legendary") {
|
|
1044
|
+
return /* @__PURE__ */ jsx5(AnimatedHorseSprite, { sprite: MAIN_SPRITE, colors, hat });
|
|
1045
|
+
}
|
|
1046
|
+
return /* @__PURE__ */ jsx5(HorseSprite, { sprite: MAIN_SPRITE, colors, hat: { hat, variant: focused.collected.variant ?? 0 } });
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/commands/stable-edit.ts
|
|
744
1050
|
async function stableEditCommand(name) {
|
|
745
1051
|
if (!name) {
|
|
746
1052
|
console.error("Usage: token-derby stable edit <name>");
|
|
@@ -754,8 +1060,9 @@ async function stableEditCommand(name) {
|
|
|
754
1060
|
return 1;
|
|
755
1061
|
}
|
|
756
1062
|
let exitCode = 0;
|
|
1063
|
+
let liveColors = existing.colors;
|
|
757
1064
|
const app = render3(
|
|
758
|
-
|
|
1065
|
+
React6.createElement(HorseCreator, {
|
|
759
1066
|
initialColors: existing.colors,
|
|
760
1067
|
initialName: existing.name,
|
|
761
1068
|
lockName: true,
|
|
@@ -763,6 +1070,7 @@ async function stableEditCommand(name) {
|
|
|
763
1070
|
onSubmit: async (_name, colors) => {
|
|
764
1071
|
try {
|
|
765
1072
|
await updateStableHorse(existing.stable_horse_id, { colors });
|
|
1073
|
+
liveColors = colors;
|
|
766
1074
|
app.unmount();
|
|
767
1075
|
console.log(`\u2713 Updated "${existing.name}".`);
|
|
768
1076
|
} catch (e) {
|
|
@@ -783,6 +1091,34 @@ async function stableEditCommand(name) {
|
|
|
783
1091
|
})
|
|
784
1092
|
);
|
|
785
1093
|
await app.waitUntilExit();
|
|
1094
|
+
if (exitCode === 0 && existing.hats && existing.hats.length > 0) {
|
|
1095
|
+
const equipResult = await new Promise((resolve) => {
|
|
1096
|
+
const app2 = render3(
|
|
1097
|
+
React6.createElement(HatPicker, {
|
|
1098
|
+
hats: existing.hats,
|
|
1099
|
+
equipped: existing.equipped_hat ?? null,
|
|
1100
|
+
colors: liveColors,
|
|
1101
|
+
onPick: (idx) => {
|
|
1102
|
+
app2.unmount();
|
|
1103
|
+
resolve({ done: true, idx });
|
|
1104
|
+
},
|
|
1105
|
+
onCancel: () => {
|
|
1106
|
+
app2.unmount();
|
|
1107
|
+
resolve({ done: false, idx: null });
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
if (equipResult.done) {
|
|
1113
|
+
try {
|
|
1114
|
+
await equipHat(existing.stable_horse_id, { hat_index: equipResult.idx });
|
|
1115
|
+
console.log(equipResult.idx === null ? "Hat unequipped." : "Hat equipped.");
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
if (e instanceof ApiError) console.error(`Equip failed: ${e.code} ${e.message}`);
|
|
1118
|
+
else throw e;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
786
1122
|
return exitCode;
|
|
787
1123
|
}
|
|
788
1124
|
async function fetchStable() {
|
|
@@ -839,13 +1175,16 @@ async function createRaceCommand(organisationName) {
|
|
|
839
1175
|
console.error("Organisation name must be 1\u201312 alphanumeric characters.");
|
|
840
1176
|
return 1;
|
|
841
1177
|
}
|
|
1178
|
+
const countInputRaw = (await rl.question("Count input tokens (fresh input + cache creation) toward race totals? [y/N]: ")).trim().toLowerCase();
|
|
1179
|
+
const counts_input = countInputRaw === "y" || countInputRaw === "yes";
|
|
842
1180
|
const resp = await createRace({
|
|
843
1181
|
name,
|
|
844
1182
|
start_time: start,
|
|
845
1183
|
end_time: end,
|
|
846
1184
|
tz,
|
|
847
1185
|
...max !== void 0 ? { max_participants: max } : {},
|
|
848
|
-
...org ? { organisation_name: org } : {}
|
|
1186
|
+
...org ? { organisation_name: org } : {},
|
|
1187
|
+
...counts_input ? { counts_input: true } : {}
|
|
849
1188
|
});
|
|
850
1189
|
console.log("");
|
|
851
1190
|
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
@@ -858,6 +1197,9 @@ async function createRaceCommand(organisationName) {
|
|
|
858
1197
|
if (org) {
|
|
859
1198
|
console.log(` Restricted to organisation: ${org}`);
|
|
860
1199
|
}
|
|
1200
|
+
if (counts_input) {
|
|
1201
|
+
console.log(" Counting input + output tokens (excluding cache reads).");
|
|
1202
|
+
}
|
|
861
1203
|
console.log(` Share with participants: token-derby join ${resp.join_code}`);
|
|
862
1204
|
return 0;
|
|
863
1205
|
} catch (e) {
|
|
@@ -877,16 +1219,16 @@ function isIso(s) {
|
|
|
877
1219
|
}
|
|
878
1220
|
|
|
879
1221
|
// src/commands/join.ts
|
|
880
|
-
import
|
|
1222
|
+
import React9 from "react";
|
|
881
1223
|
import { render as render4 } from "ink";
|
|
882
1224
|
|
|
883
1225
|
// src/ui/HorsePicker.tsx
|
|
884
|
-
import { useState as
|
|
885
|
-
import { Box as
|
|
886
|
-
import { jsx as
|
|
1226
|
+
import { useState as useState4 } from "react";
|
|
1227
|
+
import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
|
|
1228
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
887
1229
|
function HorsePicker({ horses, onPick, onCancel }) {
|
|
888
|
-
const [idx, setIdx] =
|
|
889
|
-
|
|
1230
|
+
const [idx, setIdx] = useState4(0);
|
|
1231
|
+
useInput3((input, key) => {
|
|
890
1232
|
if (key.escape) {
|
|
891
1233
|
onCancel();
|
|
892
1234
|
return;
|
|
@@ -906,31 +1248,31 @@ function HorsePicker({ horses, onPick, onCancel }) {
|
|
|
906
1248
|
}
|
|
907
1249
|
});
|
|
908
1250
|
if (horses.length === 0) {
|
|
909
|
-
return /* @__PURE__ */
|
|
910
|
-
/* @__PURE__ */
|
|
911
|
-
/* @__PURE__ */
|
|
1251
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
1252
|
+
/* @__PURE__ */ jsx6(Text6, { children: "No horses in your stable." }),
|
|
1253
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Run `token-derby stable create` to make one." })
|
|
912
1254
|
] });
|
|
913
1255
|
}
|
|
914
|
-
return /* @__PURE__ */
|
|
915
|
-
/* @__PURE__ */
|
|
916
|
-
horses.map((h, i) => /* @__PURE__ */
|
|
917
|
-
/* @__PURE__ */
|
|
1256
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
1257
|
+
/* @__PURE__ */ jsx6(Text6, { children: "Pick a horse to race:" }),
|
|
1258
|
+
horses.map((h, i) => /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
|
|
1259
|
+
/* @__PURE__ */ jsx6(Box6, { flexDirection: "row", children: /* @__PURE__ */ jsxs4(Text6, { children: [
|
|
918
1260
|
i === idx ? "\u25BA" : " ",
|
|
919
1261
|
" ",
|
|
920
1262
|
h.name,
|
|
921
1263
|
" ",
|
|
922
|
-
/* @__PURE__ */
|
|
1264
|
+
/* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
|
|
923
1265
|
"[Lvl. ",
|
|
924
1266
|
levelFromXp(h.xp),
|
|
925
1267
|
"]"
|
|
926
1268
|
] })
|
|
927
1269
|
] }) }),
|
|
928
|
-
/* @__PURE__ */
|
|
929
|
-
/* @__PURE__ */
|
|
930
|
-
/* @__PURE__ */
|
|
1270
|
+
/* @__PURE__ */ jsxs4(Box6, { flexDirection: "row", children: [
|
|
1271
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
1272
|
+
/* @__PURE__ */ jsx6(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
931
1273
|
] })
|
|
932
1274
|
] }, h.stable_horse_id)),
|
|
933
|
-
/* @__PURE__ */
|
|
1275
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
934
1276
|
] });
|
|
935
1277
|
}
|
|
936
1278
|
|
|
@@ -956,45 +1298,45 @@ async function saveActiveRace(active) {
|
|
|
956
1298
|
}
|
|
957
1299
|
|
|
958
1300
|
// src/runtime/run-race.tsx
|
|
959
|
-
import { useEffect, useRef, useState as
|
|
960
|
-
import { Box as
|
|
1301
|
+
import { useEffect as useEffect2, useRef, useState as useState5 } from "react";
|
|
1302
|
+
import { Box as Box8, Text as Text8, useApp } from "ink";
|
|
961
1303
|
|
|
962
1304
|
// src/ui/StatusScreen.tsx
|
|
963
|
-
import { Box as
|
|
964
|
-
import { jsx as
|
|
1305
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
1306
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
965
1307
|
function StatusScreen(props) {
|
|
966
1308
|
const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk } = props;
|
|
967
1309
|
if (!race) {
|
|
968
|
-
return /* @__PURE__ */
|
|
1310
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
|
|
969
1311
|
}
|
|
970
1312
|
const own = race.horses.find((h) => h.horse_id === ownHorseId);
|
|
971
1313
|
const leader = race.horses[0];
|
|
972
1314
|
const elapsedPct = elapsed(race);
|
|
973
1315
|
const timeLeft = formatDuration(race.time_left_seconds);
|
|
974
1316
|
const lvl = levelInfo((own?.xp ?? 0) + (own?.live_xp ?? 0));
|
|
975
|
-
return /* @__PURE__ */
|
|
976
|
-
/* @__PURE__ */
|
|
1317
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1318
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
977
1319
|
"\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
|
|
978
|
-
/* @__PURE__ */
|
|
1320
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: race.name }),
|
|
979
1321
|
" \u2500\u2500\u2500 status: ",
|
|
980
|
-
/* @__PURE__ */
|
|
1322
|
+
/* @__PURE__ */ jsx7(Text7, { color: statusColor(race.status), children: race.status })
|
|
981
1323
|
] }),
|
|
982
|
-
/* @__PURE__ */
|
|
983
|
-
/* @__PURE__ */
|
|
984
|
-
/* @__PURE__ */
|
|
985
|
-
/* @__PURE__ */
|
|
1324
|
+
/* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "row", children: [
|
|
1325
|
+
/* @__PURE__ */ jsx7(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
|
|
1326
|
+
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
1327
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
986
1328
|
" ",
|
|
987
1329
|
ownHorseName,
|
|
988
1330
|
" ",
|
|
989
|
-
/* @__PURE__ */
|
|
1331
|
+
/* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
|
|
990
1332
|
"[Lvl. ",
|
|
991
1333
|
lvl.level,
|
|
992
1334
|
"]"
|
|
993
1335
|
] })
|
|
994
1336
|
] }),
|
|
995
|
-
/* @__PURE__ */
|
|
1337
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
996
1338
|
" ",
|
|
997
|
-
/* @__PURE__ */
|
|
1339
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
998
1340
|
"(",
|
|
999
1341
|
ownUserName,
|
|
1000
1342
|
")"
|
|
@@ -1002,43 +1344,43 @@ function StatusScreen(props) {
|
|
|
1002
1344
|
] })
|
|
1003
1345
|
] })
|
|
1004
1346
|
] }),
|
|
1005
|
-
/* @__PURE__ */
|
|
1006
|
-
/* @__PURE__ */
|
|
1347
|
+
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
1348
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1007
1349
|
"Tokens (race): ",
|
|
1008
1350
|
own?.current_tokens ?? 0
|
|
1009
1351
|
] }),
|
|
1010
|
-
/* @__PURE__ */
|
|
1352
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1011
1353
|
"Position: ",
|
|
1012
1354
|
own?.rank ?? "\u2014",
|
|
1013
1355
|
" of ",
|
|
1014
1356
|
race.horses.length
|
|
1015
1357
|
] }),
|
|
1016
|
-
/* @__PURE__ */
|
|
1358
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1017
1359
|
"Leader: ",
|
|
1018
1360
|
leader ? `${leader.name}${leader.user_name ? ` (${leader.user_name})` : ""} \u2014 ${leader.current_tokens}` : "\u2014"
|
|
1019
1361
|
] }),
|
|
1020
|
-
/* @__PURE__ */
|
|
1362
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1021
1363
|
"Race elapsed: ",
|
|
1022
1364
|
(elapsedPct * 100).toFixed(0),
|
|
1023
1365
|
"% ",
|
|
1024
1366
|
bar(elapsedPct, 20)
|
|
1025
1367
|
] }),
|
|
1026
|
-
/* @__PURE__ */
|
|
1368
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1027
1369
|
"Time left: ",
|
|
1028
1370
|
timeLeft
|
|
1029
1371
|
] }),
|
|
1030
|
-
/* @__PURE__ */
|
|
1372
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1031
1373
|
"XP: ",
|
|
1032
1374
|
lvl.next_level_xp === null ? `${lvl.xp} (max level) ${bar(1, 20)}` : `${lvl.xp_into_level}/${lvl.xp_for_level} \u2192 Lvl. ${lvl.level + 1} ${bar(lvl.progress, 20)}`
|
|
1033
1375
|
] }),
|
|
1034
|
-
/* @__PURE__ */
|
|
1376
|
+
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1035
1377
|
"Last heartbeat: ",
|
|
1036
1378
|
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1037
1379
|
" ",
|
|
1038
|
-
/* @__PURE__ */
|
|
1380
|
+
/* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1039
1381
|
] })
|
|
1040
1382
|
] }),
|
|
1041
|
-
/* @__PURE__ */
|
|
1383
|
+
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
|
|
1042
1384
|
] });
|
|
1043
1385
|
}
|
|
1044
1386
|
function elapsed(race) {
|
|
@@ -1066,27 +1408,6 @@ function formatDuration(seconds) {
|
|
|
1066
1408
|
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${ss.toString().padStart(2, "0")}`;
|
|
1067
1409
|
}
|
|
1068
1410
|
|
|
1069
|
-
// src/ui/AchievementToast.tsx
|
|
1070
|
-
import { Box as Box6, Text as Text6 } from "ink";
|
|
1071
|
-
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1072
|
-
function AchievementToast({ horseName, name, description, xp }) {
|
|
1073
|
-
return /* @__PURE__ */ jsxs5(Box6, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [
|
|
1074
|
-
/* @__PURE__ */ jsx6(Box6, { flexDirection: "column", justifyContent: "center", marginRight: 1, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, color: "yellow", children: [
|
|
1075
|
-
"+",
|
|
1076
|
-
xp,
|
|
1077
|
-
" XP"
|
|
1078
|
-
] }) }),
|
|
1079
|
-
/* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
|
|
1080
|
-
/* @__PURE__ */ jsxs5(Text6, { bold: true, children: [
|
|
1081
|
-
horseName,
|
|
1082
|
-
" gained ",
|
|
1083
|
-
name
|
|
1084
|
-
] }),
|
|
1085
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: description })
|
|
1086
|
-
] })
|
|
1087
|
-
] });
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1411
|
// src/runtime/heartbeat-loop.ts
|
|
1091
1412
|
function runHeartbeatLoop(opts) {
|
|
1092
1413
|
let timer = null;
|
|
@@ -1140,15 +1461,9 @@ async function sumTokens() {
|
|
|
1140
1461
|
}
|
|
1141
1462
|
return { input, output };
|
|
1142
1463
|
}
|
|
1143
|
-
async function
|
|
1464
|
+
async function sumTokensForRace(race) {
|
|
1144
1465
|
const { input, output } = await sumTokens();
|
|
1145
|
-
return
|
|
1146
|
-
}
|
|
1147
|
-
function countInputTokens() {
|
|
1148
|
-
const v = process.env.TOKEN_DERBY_COUNT_INPUT_TOKENS;
|
|
1149
|
-
if (!v) return false;
|
|
1150
|
-
const s = v.toLowerCase();
|
|
1151
|
-
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
1466
|
+
return race.counts_input ? input + output : output;
|
|
1152
1467
|
}
|
|
1153
1468
|
async function listJsonlFiles(root) {
|
|
1154
1469
|
let projects;
|
|
@@ -1202,7 +1517,7 @@ async function sumFile(file) {
|
|
|
1202
1517
|
}
|
|
1203
1518
|
const usage = parsed?.message?.usage;
|
|
1204
1519
|
if (!usage) continue;
|
|
1205
|
-
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens)
|
|
1520
|
+
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1206
1521
|
output += addNum(usage.output_tokens);
|
|
1207
1522
|
}
|
|
1208
1523
|
return { input, output };
|
|
@@ -1214,33 +1529,33 @@ function initialBaseline(args) {
|
|
|
1214
1529
|
}
|
|
1215
1530
|
|
|
1216
1531
|
// src/runtime/run-race.tsx
|
|
1217
|
-
import { jsx as
|
|
1532
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1218
1533
|
function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
1219
1534
|
const { exit } = useApp();
|
|
1220
|
-
const [race, setRace] =
|
|
1221
|
-
const [lastHbAt, setLastHbAt] =
|
|
1222
|
-
const [lastHbOk, setLastHbOk] =
|
|
1223
|
-
const [tickNow, setTickNow] =
|
|
1224
|
-
const [fatalError, setFatalError] =
|
|
1225
|
-
const [
|
|
1226
|
-
const
|
|
1535
|
+
const [race, setRace] = useState5(null);
|
|
1536
|
+
const [lastHbAt, setLastHbAt] = useState5(null);
|
|
1537
|
+
const [lastHbOk, setLastHbOk] = useState5(true);
|
|
1538
|
+
const [tickNow, setTickNow] = useState5(/* @__PURE__ */ new Date());
|
|
1539
|
+
const [fatalError, setFatalError] = useState5(null);
|
|
1540
|
+
const [achievements, setAchievements] = useState5([]);
|
|
1541
|
+
const shownAchievementAtRef = useRef(0);
|
|
1227
1542
|
const baselineRef = useRef(startingBaseline);
|
|
1228
1543
|
const pendingRef = useRef(pendingMode);
|
|
1229
1544
|
const lastTokenSampleRef = useRef(startingBaseline);
|
|
1230
1545
|
const ctrl = useRef(new AbortController());
|
|
1231
|
-
|
|
1546
|
+
useEffect2(() => {
|
|
1232
1547
|
const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
|
|
1233
1548
|
return () => clearInterval(t);
|
|
1234
1549
|
}, []);
|
|
1235
|
-
|
|
1550
|
+
useEffect2(() => {
|
|
1236
1551
|
if (pendingRef.current && race?.status === "live") {
|
|
1237
|
-
|
|
1552
|
+
sumTokensForRace(active).then((total) => {
|
|
1238
1553
|
baselineRef.current = total;
|
|
1239
1554
|
pendingRef.current = false;
|
|
1240
1555
|
});
|
|
1241
1556
|
}
|
|
1242
1557
|
}, [race?.status]);
|
|
1243
|
-
|
|
1558
|
+
useEffect2(() => {
|
|
1244
1559
|
runHeartbeatLoop({
|
|
1245
1560
|
sendHeartbeat: async (currentTokens) => {
|
|
1246
1561
|
const resp = await heartbeat(
|
|
@@ -1268,14 +1583,11 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1268
1583
|
setLastHbOk(true);
|
|
1269
1584
|
setRace(raceViewFrom(resp));
|
|
1270
1585
|
const own = resp.horses.find((h) => h.horse_id === active.horse_id);
|
|
1271
|
-
const candidates = (own?.recent_events ?? []).filter((e) => e.at >
|
|
1586
|
+
const candidates = (own?.recent_events ?? []).filter((e) => e.at > shownAchievementAtRef.current);
|
|
1272
1587
|
if (candidates.length > 0) {
|
|
1273
|
-
|
|
1588
|
+
shownAchievementAtRef.current = Math.max(...candidates.map((e) => e.at));
|
|
1274
1589
|
const fresh = candidates.map((e) => ({ key: `${e.at}-${e.name}`, event: e }));
|
|
1275
|
-
|
|
1276
|
-
for (const { key } of fresh) {
|
|
1277
|
-
setTimeout(() => setToasts((prev) => prev.filter((t) => t.key !== key)), 1e4);
|
|
1278
|
-
}
|
|
1590
|
+
setAchievements((prev) => [...prev, ...fresh]);
|
|
1279
1591
|
}
|
|
1280
1592
|
if (resp.race_status === "finished") exit();
|
|
1281
1593
|
},
|
|
@@ -1293,12 +1605,12 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1293
1605
|
});
|
|
1294
1606
|
const sampler = setInterval(async () => {
|
|
1295
1607
|
try {
|
|
1296
|
-
lastTokenSampleRef.current = await
|
|
1608
|
+
lastTokenSampleRef.current = await sumTokensForRace(active);
|
|
1297
1609
|
} catch (e) {
|
|
1298
1610
|
console.error("[token-derby] token sampler failed:", e);
|
|
1299
1611
|
}
|
|
1300
1612
|
}, 5e3);
|
|
1301
|
-
|
|
1613
|
+
sumTokensForRace(active).then((t) => {
|
|
1302
1614
|
lastTokenSampleRef.current = t;
|
|
1303
1615
|
}).catch((e) => console.error("[token-derby] token sampler prime failed:", e));
|
|
1304
1616
|
const controller = ctrl.current;
|
|
@@ -1309,13 +1621,13 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1309
1621
|
}, []);
|
|
1310
1622
|
const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
|
|
1311
1623
|
if (fatalError) {
|
|
1312
|
-
return /* @__PURE__ */ jsxs6(
|
|
1313
|
-
/* @__PURE__ */
|
|
1314
|
-
/* @__PURE__ */
|
|
1624
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", padding: 1, children: [
|
|
1625
|
+
/* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
|
|
1626
|
+
/* @__PURE__ */ jsx8(Text8, { children: fatalError })
|
|
1315
1627
|
] });
|
|
1316
1628
|
}
|
|
1317
|
-
return /* @__PURE__ */ jsxs6(
|
|
1318
|
-
/* @__PURE__ */
|
|
1629
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
|
|
1630
|
+
/* @__PURE__ */ jsx8(
|
|
1319
1631
|
StatusScreen,
|
|
1320
1632
|
{
|
|
1321
1633
|
race,
|
|
@@ -1327,18 +1639,38 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
|
|
|
1327
1639
|
lastHeartbeatOk: lastHbOk
|
|
1328
1640
|
}
|
|
1329
1641
|
),
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
{
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1642
|
+
achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
|
|
1643
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: "Achievements" }),
|
|
1644
|
+
achievements.map(({ key, event }) => {
|
|
1645
|
+
const description = describeAchievement(event, active);
|
|
1646
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
|
|
1647
|
+
/* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
1648
|
+
" ",
|
|
1649
|
+
formatClockTime(event.at),
|
|
1650
|
+
" "
|
|
1651
|
+
] }),
|
|
1652
|
+
/* @__PURE__ */ jsxs6(Text8, { color: "yellow", bold: true, children: [
|
|
1653
|
+
"+",
|
|
1654
|
+
event.xp,
|
|
1655
|
+
" XP "
|
|
1656
|
+
] }),
|
|
1657
|
+
/* @__PURE__ */ jsx8(Text8, { children: event.name }),
|
|
1658
|
+
/* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
|
|
1659
|
+
" \u2014 ",
|
|
1660
|
+
description
|
|
1661
|
+
] })
|
|
1662
|
+
] }, key);
|
|
1663
|
+
})
|
|
1664
|
+
] })
|
|
1340
1665
|
] });
|
|
1341
1666
|
}
|
|
1667
|
+
function formatClockTime(at) {
|
|
1668
|
+
const d = new Date(at);
|
|
1669
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
1670
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
1671
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
1672
|
+
return `${h}:${m}:${s}`;
|
|
1673
|
+
}
|
|
1342
1674
|
function raceViewFrom(resp) {
|
|
1343
1675
|
return {
|
|
1344
1676
|
...resp.race,
|
|
@@ -1349,7 +1681,7 @@ function raceViewFrom(resp) {
|
|
|
1349
1681
|
};
|
|
1350
1682
|
}
|
|
1351
1683
|
async function buildInitialState(args) {
|
|
1352
|
-
const runningTotal = await
|
|
1684
|
+
const runningTotal = await sumTokensForRace(args.active);
|
|
1353
1685
|
if (args.rejoin) {
|
|
1354
1686
|
return {
|
|
1355
1687
|
startingBaseline: Math.max(0, runningTotal - args.active.last_race_tokens),
|
|
@@ -1400,6 +1732,17 @@ async function joinCommand(joinCode) {
|
|
|
1400
1732
|
chosenColors = ownHorse.colors;
|
|
1401
1733
|
isResume = true;
|
|
1402
1734
|
} else {
|
|
1735
|
+
if (race.org_id) {
|
|
1736
|
+
try {
|
|
1737
|
+
const { organisations } = await listOrganisations();
|
|
1738
|
+
if (!organisations.some((o) => o.org_id === race.org_id)) {
|
|
1739
|
+
const label = race.organisation_name ?? race.org_id;
|
|
1740
|
+
console.error(`This race is restricted to members of "${label}".`);
|
|
1741
|
+
return 1;
|
|
1742
|
+
}
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1403
1746
|
let horses;
|
|
1404
1747
|
try {
|
|
1405
1748
|
horses = (await listStable()).horses;
|
|
@@ -1454,18 +1797,19 @@ async function joinCommand(joinCode) {
|
|
|
1454
1797
|
horse_colors: chosenColors,
|
|
1455
1798
|
joined_at: ownHorse?.joined_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1456
1799
|
last_race_tokens: lastTokens,
|
|
1457
|
-
last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1800
|
+
last_heartbeat_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1801
|
+
...race.counts_input ? { counts_input: true } : {}
|
|
1458
1802
|
};
|
|
1459
1803
|
await saveActiveRace(active);
|
|
1460
1804
|
const initial = await buildInitialState({ active, raceStatus: status, rejoin: isResume });
|
|
1461
|
-
const app = render4(
|
|
1805
|
+
const app = render4(React9.createElement(RunRace, { active, ...initial, ownUserName: identity.display_name }));
|
|
1462
1806
|
await app.waitUntilExit();
|
|
1463
1807
|
return 0;
|
|
1464
1808
|
}
|
|
1465
1809
|
async function pickHorse(horses) {
|
|
1466
1810
|
return new Promise((resolve) => {
|
|
1467
1811
|
const app = render4(
|
|
1468
|
-
|
|
1812
|
+
React9.createElement(HorsePicker, {
|
|
1469
1813
|
horses,
|
|
1470
1814
|
onPick: (h) => {
|
|
1471
1815
|
app.unmount();
|
|
@@ -1590,11 +1934,471 @@ async function initCommand(reset = false) {
|
|
|
1590
1934
|
}
|
|
1591
1935
|
}
|
|
1592
1936
|
|
|
1593
|
-
// src/commands/
|
|
1937
|
+
// src/commands/update.ts
|
|
1594
1938
|
import * as readline5 from "readline/promises";
|
|
1595
1939
|
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
1596
|
-
|
|
1940
|
+
import { spawn } from "child_process";
|
|
1941
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@mauricode/token-derby/latest";
|
|
1942
|
+
var UPGRADE_CMD = "npm install -g @mauricode/token-derby@latest";
|
|
1943
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
1944
|
+
async function updateCommand(deps = {}) {
|
|
1945
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1946
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
1947
|
+
const promptYesNo2 = deps.promptYesNo ?? defaultPromptYesNo;
|
|
1948
|
+
let latest;
|
|
1949
|
+
try {
|
|
1950
|
+
latest = await fetchLatestVersion(fetchImpl);
|
|
1951
|
+
} catch (e) {
|
|
1952
|
+
console.error(`Could not reach the npm registry${e?.message ? ` (${e.message})` : ""}.`);
|
|
1953
|
+
console.error(`To upgrade manually: ${UPGRADE_CMD}`);
|
|
1954
|
+
return 1;
|
|
1955
|
+
}
|
|
1956
|
+
if (gteSemver(CLI_VERSION, latest)) {
|
|
1957
|
+
console.log(`You're on the latest version (${CLI_VERSION}).`);
|
|
1958
|
+
return 0;
|
|
1959
|
+
}
|
|
1960
|
+
console.log(`Current: ${CLI_VERSION} Latest: ${latest}`);
|
|
1961
|
+
const yes = await promptYesNo2("Run upgrade now? [y/N]: ");
|
|
1962
|
+
if (!yes) {
|
|
1963
|
+
console.log(`To upgrade manually: ${UPGRADE_CMD}`);
|
|
1964
|
+
return 0;
|
|
1965
|
+
}
|
|
1966
|
+
return runNpmUpgrade(spawnImpl);
|
|
1967
|
+
}
|
|
1968
|
+
async function fetchLatestVersion(fetchImpl) {
|
|
1969
|
+
const controller = new AbortController();
|
|
1970
|
+
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1971
|
+
try {
|
|
1972
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: controller.signal });
|
|
1973
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1974
|
+
const body = await res.json();
|
|
1975
|
+
if (typeof body.version !== "string" || !/^\d+\.\d+\.\d+/.test(body.version)) {
|
|
1976
|
+
throw new Error("unexpected registry response");
|
|
1977
|
+
}
|
|
1978
|
+
return body.version;
|
|
1979
|
+
} finally {
|
|
1980
|
+
clearTimeout(t);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function defaultPromptYesNo(question) {
|
|
1597
1984
|
const rl = readline5.createInterface({ input: stdin5, output: stdout5 });
|
|
1985
|
+
try {
|
|
1986
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
1987
|
+
return answer === "y" || answer === "yes";
|
|
1988
|
+
} finally {
|
|
1989
|
+
rl.close();
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function runNpmUpgrade(spawnImpl) {
|
|
1993
|
+
return new Promise((resolve) => {
|
|
1994
|
+
const child = spawnImpl("npm", ["install", "-g", "@mauricode/token-derby@latest"], {
|
|
1995
|
+
stdio: "inherit"
|
|
1996
|
+
});
|
|
1997
|
+
child.on("error", (e) => {
|
|
1998
|
+
if (e.code === "ENOENT") {
|
|
1999
|
+
console.error("Could not find `npm` on PATH.");
|
|
2000
|
+
console.error(`To upgrade manually: ${UPGRADE_CMD}`);
|
|
2001
|
+
} else {
|
|
2002
|
+
console.error(`npm failed to start: ${e.message}`);
|
|
2003
|
+
}
|
|
2004
|
+
resolve(1);
|
|
2005
|
+
});
|
|
2006
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/commands/roll.ts
|
|
2011
|
+
import React13 from "react";
|
|
2012
|
+
import { render as render5 } from "ink";
|
|
2013
|
+
|
|
2014
|
+
// src/ui/RollReveal.tsx
|
|
2015
|
+
import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
|
|
2016
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
2017
|
+
|
|
2018
|
+
// src/ui/HatSprite.tsx
|
|
2019
|
+
import { useEffect as useEffect3, useState as useState6 } from "react";
|
|
2020
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
2021
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2022
|
+
function HatSprite({ hat, variant, centerIn }) {
|
|
2023
|
+
const colors = hatColorsFor2(hat, variant ?? 0);
|
|
2024
|
+
const grid = makeHatGrid(hat, colors, centerIn);
|
|
2025
|
+
const lines = hexGridToHalfBlocks(grid);
|
|
2026
|
+
return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
|
|
2027
|
+
}
|
|
2028
|
+
function AnimatedHatSprite({ hat, variant, centerIn }) {
|
|
2029
|
+
if (hat.rarity !== "legendary") {
|
|
2030
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
|
|
2031
|
+
}
|
|
2032
|
+
const frames = hat.animation.frames;
|
|
2033
|
+
const fps = hat.animation.fps;
|
|
2034
|
+
const [idx, setIdx] = useState6(0);
|
|
2035
|
+
useEffect3(() => {
|
|
2036
|
+
if (frames.length <= 1) return;
|
|
2037
|
+
const interval = setInterval(
|
|
2038
|
+
() => setIdx((i) => (i + 1) % frames.length),
|
|
2039
|
+
Math.max(1, Math.round(1e3 / fps))
|
|
2040
|
+
);
|
|
2041
|
+
return () => clearInterval(interval);
|
|
2042
|
+
}, [frames.length, fps]);
|
|
2043
|
+
const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
|
|
2044
|
+
return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
|
|
2045
|
+
}
|
|
2046
|
+
function hatColorsFor2(hat, variantIdx) {
|
|
2047
|
+
if (hat.rarity === "legendary") return hat.colors;
|
|
2048
|
+
return hat.variants[variantIdx] ?? hat.variants[0];
|
|
2049
|
+
}
|
|
2050
|
+
function makeHatGrid(hat, colors, centerIn) {
|
|
2051
|
+
const w = centerIn?.w ?? hat.width;
|
|
2052
|
+
const h = centerIn?.h ?? hat.rows.length;
|
|
2053
|
+
const offX = Math.floor((w - hat.width) / 2);
|
|
2054
|
+
const offY = Math.floor((h - hat.rows.length) / 2);
|
|
2055
|
+
const grid = Array.from({ length: h }, () => Array(w).fill(null));
|
|
2056
|
+
for (let y = 0; y < hat.rows.length; y++) {
|
|
2057
|
+
const row = hat.rows[y];
|
|
2058
|
+
for (let x = 0; x < hat.width; x++) {
|
|
2059
|
+
const ch = row[x];
|
|
2060
|
+
if (ch === "." || ch === void 0) continue;
|
|
2061
|
+
const gx = x + offX;
|
|
2062
|
+
const gy = y + offY;
|
|
2063
|
+
if (gx < 0 || gx >= w || gy < 0 || gy >= h) continue;
|
|
2064
|
+
grid[gy][gx] = ch === "A" ? colors.A : colors.Q ?? colors.A;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return grid;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/ui/RollReveal.tsx
|
|
2071
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
2072
|
+
var RESET3 = "\x1B[0m";
|
|
2073
|
+
var BOX_COLOR = "#E5C76B";
|
|
2074
|
+
var TIER_PALETTE = {
|
|
2075
|
+
common: ["#FFFFFF", "#DDDDDD", "#AAAAAA", "#9E9E9E"],
|
|
2076
|
+
rare: ["#42A5F5", "#1E88E5", "#90CAF9", "#0277BD"],
|
|
2077
|
+
epic: ["#AB47BC", "#8E24AA", "#CE93D8", "#6A1B9A"],
|
|
2078
|
+
legendary: ["#FFD700", "#FF7F00", "#FF0000", "#FF00FF", "#00BFFF", "#7CFC00", "#8B00FF"]
|
|
2079
|
+
};
|
|
2080
|
+
var CONFETTI_CHARS = ["\u2726", "\u2727", "\u22C6", "\u2605", "\u2606", "\u2728", "*", "\u2022", "\u25C6", "\u25C7"];
|
|
2081
|
+
var SCENE_W = 32;
|
|
2082
|
+
var SCENE_H = 10;
|
|
2083
|
+
function pad(line) {
|
|
2084
|
+
const visible = stripAnsi(line).length;
|
|
2085
|
+
const lead = Math.max(0, Math.floor((SCENE_W - visible) / 2));
|
|
2086
|
+
return " ".repeat(lead) + line + " ".repeat(Math.max(0, SCENE_W - lead - visible));
|
|
2087
|
+
}
|
|
2088
|
+
function stripAnsi(s) {
|
|
2089
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2090
|
+
}
|
|
2091
|
+
var BOX_CLOSED = [
|
|
2092
|
+
"",
|
|
2093
|
+
"",
|
|
2094
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2095
|
+
"\u2551 \u2591\u2591\u2591\u2591\u2591 \u2551",
|
|
2096
|
+
"\u2551\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2551",
|
|
2097
|
+
"\u2551 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2551",
|
|
2098
|
+
"\u2551\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2551",
|
|
2099
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2100
|
+
"",
|
|
2101
|
+
""
|
|
2102
|
+
].map(pad);
|
|
2103
|
+
var BOX_OPENING_1 = [
|
|
2104
|
+
"",
|
|
2105
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2106
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2107
|
+
"",
|
|
2108
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2109
|
+
"\u2551 \u2726 \u2551",
|
|
2110
|
+
"\u2551\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2551",
|
|
2111
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2112
|
+
"",
|
|
2113
|
+
""
|
|
2114
|
+
].map(pad);
|
|
2115
|
+
var BOX_OPENING_2 = [
|
|
2116
|
+
" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2117
|
+
" \u2551 \u2551",
|
|
2118
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2119
|
+
"",
|
|
2120
|
+
" \u2728 \u2605 \u2726",
|
|
2121
|
+
"",
|
|
2122
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2123
|
+
"\u2551 \u2551",
|
|
2124
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2125
|
+
""
|
|
2126
|
+
].map(pad);
|
|
2127
|
+
var BOX_EMPTY = [
|
|
2128
|
+
"",
|
|
2129
|
+
"",
|
|
2130
|
+
"",
|
|
2131
|
+
"",
|
|
2132
|
+
"",
|
|
2133
|
+
"",
|
|
2134
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2135
|
+
"\u2551 \u2551",
|
|
2136
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2137
|
+
""
|
|
2138
|
+
].map(pad);
|
|
2139
|
+
function GiftBox({ frame, color }) {
|
|
2140
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
|
|
2141
|
+
}
|
|
2142
|
+
function printClosedBox() {
|
|
2143
|
+
for (const line of BOX_CLOSED) {
|
|
2144
|
+
const colored = line.trim().length > 0 ? `${ansiFg(BOX_COLOR)}${line}${RESET3}` : line;
|
|
2145
|
+
process.stdout.write(colored + "\n");
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function spawnParticles(tier, count, cx, cy) {
|
|
2149
|
+
const palette = TIER_PALETTE[tier];
|
|
2150
|
+
const out = [];
|
|
2151
|
+
for (let i = 0; i < count; i++) {
|
|
2152
|
+
const angle = i / count * Math.PI * 2 + (Math.random() - 0.5) * 0.6;
|
|
2153
|
+
const speed = 0.8 + Math.random() * 1.6;
|
|
2154
|
+
out.push({
|
|
2155
|
+
x: cx,
|
|
2156
|
+
y: cy,
|
|
2157
|
+
vx: Math.cos(angle) * speed,
|
|
2158
|
+
vy: Math.sin(angle) * speed * 0.5,
|
|
2159
|
+
char: CONFETTI_CHARS[Math.floor(Math.random() * CONFETTI_CHARS.length)],
|
|
2160
|
+
color: palette[Math.floor(Math.random() * palette.length)]
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
return out;
|
|
2164
|
+
}
|
|
2165
|
+
function ConfettiBurst({ tier }) {
|
|
2166
|
+
const cx = Math.floor(SCENE_W / 2);
|
|
2167
|
+
const cy = Math.floor(SCENE_H / 2);
|
|
2168
|
+
const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
|
|
2169
|
+
const [tick, setTick] = useState7(0);
|
|
2170
|
+
useEffect4(() => {
|
|
2171
|
+
const i = setInterval(() => setTick((t) => t + 1), 70);
|
|
2172
|
+
return () => clearInterval(i);
|
|
2173
|
+
}, []);
|
|
2174
|
+
const grid = Array.from({ length: SCENE_H }, () => Array(SCENE_W).fill(" "));
|
|
2175
|
+
for (const p of particles) {
|
|
2176
|
+
const x = Math.round(p.x + p.vx * tick);
|
|
2177
|
+
const y = Math.round(p.y + p.vy * tick);
|
|
2178
|
+
if (x >= 0 && x < SCENE_W && y >= 0 && y < SCENE_H) {
|
|
2179
|
+
grid[y][x] = ansiFg(p.color) + p.char + RESET3;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
|
|
2183
|
+
}
|
|
2184
|
+
function RollReveal({ outcome, onDone }) {
|
|
2185
|
+
const isNoHat = outcome.kind === "no_hat";
|
|
2186
|
+
const isLegendary = outcome.kind !== "no_hat" && outcome.hat.rarity === "legendary";
|
|
2187
|
+
const [phase, setPhase] = useState7("open1");
|
|
2188
|
+
useEffect4(() => {
|
|
2189
|
+
const timers = [];
|
|
2190
|
+
timers.push(setTimeout(() => setPhase("open2"), 350));
|
|
2191
|
+
if (isNoHat) {
|
|
2192
|
+
timers.push(setTimeout(() => setPhase("empty"), 700));
|
|
2193
|
+
timers.push(setTimeout(onDone, 1500));
|
|
2194
|
+
} else {
|
|
2195
|
+
timers.push(setTimeout(() => setPhase("burst"), 700));
|
|
2196
|
+
timers.push(setTimeout(() => setPhase("reveal"), 1650));
|
|
2197
|
+
timers.push(setTimeout(onDone, isLegendary ? 4650 : 2650));
|
|
2198
|
+
}
|
|
2199
|
+
return () => timers.forEach(clearTimeout);
|
|
2200
|
+
}, [isNoHat, isLegendary, onDone]);
|
|
2201
|
+
if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
|
|
2202
|
+
if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
|
|
2203
|
+
if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
2204
|
+
if (phase === "burst" && outcome.kind !== "no_hat") {
|
|
2205
|
+
return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
|
|
2206
|
+
}
|
|
2207
|
+
if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
|
|
2208
|
+
return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx10(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx10(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/ui/RollHorsePicker.tsx
|
|
2212
|
+
import { useState as useState8 } from "react";
|
|
2213
|
+
import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
|
|
2214
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2215
|
+
function RollHorsePicker({ horses, onPick, onCancel }) {
|
|
2216
|
+
const [idx, setIdx] = useState8(0);
|
|
2217
|
+
useInput4((input, key) => {
|
|
2218
|
+
if (key.escape) {
|
|
2219
|
+
onCancel();
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (horses.length === 0) return;
|
|
2223
|
+
if (key.upArrow) {
|
|
2224
|
+
setIdx((idx - 1 + horses.length) % horses.length);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (key.downArrow) {
|
|
2228
|
+
setIdx((idx + 1) % horses.length);
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (key.return) {
|
|
2232
|
+
onPick(horses[idx]);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
2236
|
+
return /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
|
|
2237
|
+
/* @__PURE__ */ jsx11(Text11, { children: "Pick a horse to roll for:" }),
|
|
2238
|
+
horses.map((h, i) => /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
|
|
2239
|
+
/* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text11, { children: [
|
|
2240
|
+
i === idx ? "\u25BA" : " ",
|
|
2241
|
+
" ",
|
|
2242
|
+
h.name,
|
|
2243
|
+
" ",
|
|
2244
|
+
/* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
|
|
2245
|
+
"[Lvl. ",
|
|
2246
|
+
levelFromXp(h.xp),
|
|
2247
|
+
"]"
|
|
2248
|
+
] }),
|
|
2249
|
+
" ",
|
|
2250
|
+
/* @__PURE__ */ jsxs7(Text11, { color: "yellow", children: [
|
|
2251
|
+
"\u2014 ",
|
|
2252
|
+
h.pending,
|
|
2253
|
+
" roll",
|
|
2254
|
+
h.pending === 1 ? "" : "s"
|
|
2255
|
+
] })
|
|
2256
|
+
] }) }),
|
|
2257
|
+
/* @__PURE__ */ jsxs7(Box11, { flexDirection: "row", children: [
|
|
2258
|
+
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
2259
|
+
/* @__PURE__ */ jsx11(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
|
|
2260
|
+
] })
|
|
2261
|
+
] }, h.stable_horse_id)),
|
|
2262
|
+
/* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
|
|
2263
|
+
] });
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/commands/roll.ts
|
|
2267
|
+
function pendingFor(horse) {
|
|
2268
|
+
const level = levelFromXp(horse.xp);
|
|
2269
|
+
const lastRolled = horse.last_rolled_level ?? Math.max(0, level - 1);
|
|
2270
|
+
return level - lastRolled;
|
|
2271
|
+
}
|
|
2272
|
+
async function promptYesNo(question) {
|
|
2273
|
+
const readline7 = await import("readline/promises");
|
|
2274
|
+
const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
|
|
2275
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
2276
|
+
rl.close();
|
|
2277
|
+
return a === "" || a === "y" || a === "yes";
|
|
2278
|
+
}
|
|
2279
|
+
async function runReveal(outcome) {
|
|
2280
|
+
printClosedBox();
|
|
2281
|
+
const readline7 = await import("readline/promises");
|
|
2282
|
+
const rl = readline7.createInterface({ input: process.stdin, output: process.stdout });
|
|
2283
|
+
await rl.question("Press Enter to open the box\u2026 ");
|
|
2284
|
+
rl.close();
|
|
2285
|
+
await new Promise((resolve) => {
|
|
2286
|
+
const app = render5(React13.createElement(RollReveal, {
|
|
2287
|
+
outcome,
|
|
2288
|
+
onDone: () => {
|
|
2289
|
+
app.unmount();
|
|
2290
|
+
resolve();
|
|
2291
|
+
}
|
|
2292
|
+
}));
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
async function rollCommand() {
|
|
2296
|
+
let stable;
|
|
2297
|
+
try {
|
|
2298
|
+
stable = await listStable();
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
if (e instanceof ApiError) {
|
|
2301
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
2302
|
+
return 1;
|
|
2303
|
+
}
|
|
2304
|
+
throw e;
|
|
2305
|
+
}
|
|
2306
|
+
const eligible = stable.horses.map((h) => ({ ...h, pending: pendingFor(h) })).filter((h) => h.pending > 0);
|
|
2307
|
+
if (eligible.length === 0) {
|
|
2308
|
+
console.log("No rolls available. Level up a horse to earn a roll!");
|
|
2309
|
+
return 0;
|
|
2310
|
+
}
|
|
2311
|
+
let chosen = eligible[0];
|
|
2312
|
+
if (eligible.length > 1) {
|
|
2313
|
+
const picked = await new Promise((resolve) => {
|
|
2314
|
+
const app = render5(React13.createElement(RollHorsePicker, {
|
|
2315
|
+
horses: eligible,
|
|
2316
|
+
onPick: (h) => {
|
|
2317
|
+
app.unmount();
|
|
2318
|
+
resolve(h);
|
|
2319
|
+
},
|
|
2320
|
+
onCancel: () => {
|
|
2321
|
+
app.unmount();
|
|
2322
|
+
resolve(null);
|
|
2323
|
+
}
|
|
2324
|
+
}));
|
|
2325
|
+
});
|
|
2326
|
+
if (!picked) {
|
|
2327
|
+
console.log("Cancelled.");
|
|
2328
|
+
return 0;
|
|
2329
|
+
}
|
|
2330
|
+
chosen = picked;
|
|
2331
|
+
}
|
|
2332
|
+
while (true) {
|
|
2333
|
+
let result;
|
|
2334
|
+
try {
|
|
2335
|
+
result = await rollHat(chosen.stable_horse_id);
|
|
2336
|
+
} catch (e) {
|
|
2337
|
+
if (e instanceof ApiError) {
|
|
2338
|
+
if (e.code === "INSUFFICIENT_ROLLS") {
|
|
2339
|
+
console.log("No more rolls available.");
|
|
2340
|
+
return 0;
|
|
2341
|
+
}
|
|
2342
|
+
console.error(`Error: ${e.code} ${e.message}`);
|
|
2343
|
+
return 1;
|
|
2344
|
+
}
|
|
2345
|
+
throw e;
|
|
2346
|
+
}
|
|
2347
|
+
const outcome = (() => {
|
|
2348
|
+
if (result.result === "hat") {
|
|
2349
|
+
const hat = hatById(result.collected.id);
|
|
2350
|
+
if (!hat) return null;
|
|
2351
|
+
return { kind: "hat", hat, variant: result.collected.variant };
|
|
2352
|
+
}
|
|
2353
|
+
if (result.result === "duplicate") {
|
|
2354
|
+
const hat = hatById(result.hat_id);
|
|
2355
|
+
if (!hat) return null;
|
|
2356
|
+
return { kind: "duplicate", hat, variant: result.variant };
|
|
2357
|
+
}
|
|
2358
|
+
return { kind: "no_hat" };
|
|
2359
|
+
})();
|
|
2360
|
+
if (!outcome) {
|
|
2361
|
+
console.error("Server returned an unknown hat id \u2014 catalog mismatch.");
|
|
2362
|
+
return 1;
|
|
2363
|
+
}
|
|
2364
|
+
await runReveal(outcome);
|
|
2365
|
+
if (result.result === "hat") {
|
|
2366
|
+
const hat = hatById(result.collected.id);
|
|
2367
|
+
const variantSuffix = hat.rarity !== "legendary" && result.collected.variant !== void 0 ? ` #${result.collected.variant + 1}` : "";
|
|
2368
|
+
console.log(`
|
|
2369
|
+
\u2728 ${hat.name}${variantSuffix} [${hat.rarity.toUpperCase()}]
|
|
2370
|
+
`);
|
|
2371
|
+
if (await promptYesNo("Equip now? [Y/n] ")) {
|
|
2372
|
+
try {
|
|
2373
|
+
await equipHat(chosen.stable_horse_id, { hat_index: result.hat_index });
|
|
2374
|
+
console.log(`Equipped on ${chosen.name}.`);
|
|
2375
|
+
} catch (e) {
|
|
2376
|
+
if (e instanceof ApiError) {
|
|
2377
|
+
console.error(`Equip failed: ${e.code} ${e.message}`);
|
|
2378
|
+
} else throw e;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
} else if (result.result === "duplicate") {
|
|
2382
|
+
const hat = hatById(result.hat_id);
|
|
2383
|
+
const variantSuffix = result.variant !== void 0 ? ` #${result.variant + 1}` : "";
|
|
2384
|
+
console.log(`
|
|
2385
|
+
You already have ${hat?.name ?? result.hat_id}${variantSuffix}. +${result.xp_awarded} XP.
|
|
2386
|
+
`);
|
|
2387
|
+
} else {
|
|
2388
|
+
console.log(`
|
|
2389
|
+
No hat this time. +${result.xp_awarded} XP toward your next level.
|
|
2390
|
+
`);
|
|
2391
|
+
}
|
|
2392
|
+
if (result.remaining_rolls <= 0) return 0;
|
|
2393
|
+
if (!await promptYesNo(`${result.remaining_rolls} more roll${result.remaining_rolls === 1 ? "" : "s"} available. Roll again? [Y/n] `)) return 0;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/commands/org-create.ts
|
|
2398
|
+
import * as readline6 from "readline/promises";
|
|
2399
|
+
import { stdin as stdin6, stdout as stdout6 } from "process";
|
|
2400
|
+
async function orgCreateCommand() {
|
|
2401
|
+
const rl = readline6.createInterface({ input: stdin6, output: stdout6 });
|
|
1598
2402
|
try {
|
|
1599
2403
|
const name = (await rl.question(`Organisation name (1\u2013${ORG_NAME_MAX_LENGTH} alphanumeric chars): `)).trim();
|
|
1600
2404
|
if (!ORG_NAME_PATTERN.test(name)) {
|
|
@@ -1773,6 +2577,9 @@ Identity:
|
|
|
1773
2577
|
token-derby init --reset Wipe local identity and create a fresh account.
|
|
1774
2578
|
Your previous stable is abandoned on the server.
|
|
1775
2579
|
|
|
2580
|
+
Maintenance:
|
|
2581
|
+
token-derby update Check for and install the latest CLI version
|
|
2582
|
+
|
|
1776
2583
|
Stable management:
|
|
1777
2584
|
token-derby stable create Make a new horse (interactive)
|
|
1778
2585
|
token-derby stable list Show your saved horses
|
|
@@ -1801,6 +2608,10 @@ Races:
|
|
|
1801
2608
|
token-derby join <join-code> Join (or resume) a race
|
|
1802
2609
|
token-derby end <admin-code> End a race early
|
|
1803
2610
|
|
|
2611
|
+
Cosmetics:
|
|
2612
|
+
token-derby roll Spend a pending roll to try for a hat.
|
|
2613
|
+
Earn rolls by leveling up horses.
|
|
2614
|
+
|
|
1804
2615
|
Environment:
|
|
1805
2616
|
TOKEN_DERBY_API_BASE Override API base URL (default: production)
|
|
1806
2617
|
TOKEN_DERBY_HOME Override identity/stable directory
|
|
@@ -1820,6 +2631,7 @@ async function main() {
|
|
|
1820
2631
|
const reset = argv.slice(1).includes("--reset");
|
|
1821
2632
|
return initCommand(reset);
|
|
1822
2633
|
}
|
|
2634
|
+
if (cmd === "update") return updateCommand();
|
|
1823
2635
|
const identity = await loadIdentity();
|
|
1824
2636
|
if (!identity) {
|
|
1825
2637
|
console.error("Run `token-derby init` to set up your identity before using any other command.");
|
|
@@ -1860,6 +2672,7 @@ async function main() {
|
|
|
1860
2672
|
}
|
|
1861
2673
|
if (cmd === "join") return joinCommand(argv[1]);
|
|
1862
2674
|
if (cmd === "end") return endCommand(argv[1]);
|
|
2675
|
+
if (cmd === "roll") return rollCommand();
|
|
1863
2676
|
console.error(`Unknown command: ${cmd}`);
|
|
1864
2677
|
console.error(HELP);
|
|
1865
2678
|
return 2;
|