@mauricode/token-derby 2.5.4 → 2.5.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/bin.js CHANGED
@@ -1,14 +1,180 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/stable-create.ts
4
- import React2 from "react";
4
+ import React3 from "react";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/ui/HorseCreator.tsx
8
- import { useState } from "react";
9
- import { Box as Box2, Text as Text2, useInput } from "ink";
8
+ import { useState as useState2 } from "react";
9
+ import { Box as Box3, Text as Text3, useInput } from "ink";
10
10
  import TextInput from "ink-text-input";
11
11
 
12
+ // ../shared/dist/constants.js
13
+ var CLI_VERSION_HEADER = "x-cli-version";
14
+ var USER_ID_HEADER = "x-user-id";
15
+ var USER_TOKEN_HEADER = "x-user-token";
16
+ var USER_NAME_MAX_LENGTH = 40;
17
+ var ORG_NAME_MAX_LENGTH = 12;
18
+ var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
19
+
20
+ // ../shared/dist/version-match.js
21
+ var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
22
+ function parseSemver(v) {
23
+ if (typeof v !== "string")
24
+ return null;
25
+ const m = SEMVER_RE.exec(v.trim());
26
+ if (!m)
27
+ return null;
28
+ return {
29
+ major: Number(m[1]),
30
+ minor: Number(m[2]),
31
+ patch: Number(m[3])
32
+ };
33
+ }
34
+ function gteSemver(a, b) {
35
+ const pa = parseSemver(a);
36
+ const pb = parseSemver(b);
37
+ if (!pa || !pb)
38
+ return false;
39
+ if (pa.major !== pb.major)
40
+ return pa.major > pb.major;
41
+ if (pa.minor !== pb.minor)
42
+ return pa.minor > pb.minor;
43
+ return pa.patch >= pb.patch;
44
+ }
45
+
46
+ // ../shared/dist/levels.js
47
+ var MAX_LEVEL = 30;
48
+ function xpForLevel(n) {
49
+ return 1.8 * n ** 3 + 18 * n ** 2 + 50 * n - 19.8;
50
+ }
51
+ function thresholdForLevel(level) {
52
+ if (level <= 1)
53
+ return 0;
54
+ return Math.round(xpForLevel(level - 1));
55
+ }
56
+ var XP_THRESHOLDS = Array.from({ length: MAX_LEVEL }, (_, i) => thresholdForLevel(i + 1));
57
+ function levelFromXp(xp) {
58
+ const v = Math.max(0, Math.floor(xp));
59
+ let level = 1;
60
+ while (level < MAX_LEVEL && v >= thresholdForLevel(level + 1)) {
61
+ level++;
62
+ }
63
+ return level;
64
+ }
65
+ function levelInfo(xp) {
66
+ const v = Math.max(0, Math.floor(xp));
67
+ const level = levelFromXp(v);
68
+ const level_start_xp = thresholdForLevel(level);
69
+ const isMax = level >= MAX_LEVEL;
70
+ const next_level_xp = isMax ? null : thresholdForLevel(level + 1);
71
+ const xp_into_level = v - level_start_xp;
72
+ const xp_for_level = isMax ? null : next_level_xp - level_start_xp;
73
+ const progress = isMax ? 1 : Math.min(1, xp_into_level / Math.max(1, xp_for_level));
74
+ return { level, xp: v, level_start_xp, next_level_xp, xp_into_level, xp_for_level, progress };
75
+ }
76
+
77
+ // ../shared/dist/midrace.js
78
+ var ACHIEVEMENT_DESCRIPTIONS = {
79
+ "Racer!": "Raced continuously for an hour",
80
+ "Overtake!": "Overtook another horse",
81
+ "Pacesetter!": "Led the race for an hour straight",
82
+ "Stampede!": "Gained 7,000+ tokens in a single minute",
83
+ "Took the lead!": "Charged into first place",
84
+ "Comeback!": "Climbed from last place to the top half",
85
+ "Pulled Away!": "Grew the lead by 5,000+ tokens in a minute"
86
+ };
87
+ function overtakeDescription(positionsClimbed) {
88
+ if (positionsClimbed <= 1)
89
+ return "Overtook another horse";
90
+ return `Overtook ${positionsClimbed} horses`;
91
+ }
92
+ var TOKEN_INPUT_MULTIPLIER = 10;
93
+ function tokenMultiplier(race) {
94
+ return race.counts_input ? TOKEN_INPUT_MULTIPLIER : 1;
95
+ }
96
+ function describeAchievement(event, race) {
97
+ if (event.name === "Overtake!") {
98
+ return overtakeDescription(Math.floor(event.xp / 3));
99
+ }
100
+ const m = tokenMultiplier(race);
101
+ if (event.name === "Stampede!") {
102
+ return `Gained ${(MIDRACE_THRESHOLDS.stampede_tokens * m).toLocaleString("en-US")}+ tokens in a single minute`;
103
+ }
104
+ if (event.name === "Pulled Away!") {
105
+ return `Grew the lead by ${(MIDRACE_THRESHOLDS.pulled_away_gap * m).toLocaleString("en-US")}+ tokens in a minute`;
106
+ }
107
+ return ACHIEVEMENT_DESCRIPTIONS[event.name];
108
+ }
109
+ var MIDRACE_THRESHOLDS = {
110
+ warm_up_fraction: 0.08,
111
+ // first 8% of race time
112
+ streak_hour_ms: 36e5,
113
+ // 1 hour for Racer!/Pacesetter!
114
+ racer_dt_cap_ms: 9e4,
115
+ // single-tick credit cap for Racer!
116
+ stampede_tokens: 7e3,
117
+ // tokens-in-a-minute threshold
118
+ stampede_cooldown_ms: 72e5,
119
+ // 2 hours
120
+ pulled_away_gap: 5e3,
121
+ // gap-growth threshold per minute
122
+ pulled_away_cooldown_ms: 72e5,
123
+ // 2 hours
124
+ recent_events_retention_ms: 9e4
125
+ // sliding window for recent_events
126
+ };
127
+
128
+ // ../shared/dist/hats.js
129
+ function hatById(id) {
130
+ return HATS.find((h) => h.id === id);
131
+ }
132
+ var HATS = [
133
+ // ── COMMON (18) ────────────────────────────────────────────────────────
134
+ { 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" }] },
135
+ { 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" }] },
136
+ { 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" }] },
137
+ { 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" }] },
138
+ { 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" }] },
139
+ { 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" }] },
140
+ { 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" }] },
141
+ { 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" }] },
142
+ { 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" }] },
143
+ { 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" }] },
144
+ { id: "hard_hat", name: "Hard Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....AAA....", "...AAAAA...", "..AAAAAAA..", "..AAAAAAAAA"], variants: [{ A: "#FFD600" }, { A: "#d74c1d" }] },
145
+ { 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" }] },
146
+ { 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" }] },
147
+ { 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" }] },
148
+ { 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" }] },
149
+ { id: "tinfoil_hat", name: "Tinfoil Hat", rarity: "common", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", ".....A.....", "....AAA....", "...AAAAA...", "..AAAAAAA.."], variants: [{ A: "#B0BEC5" }, { A: "#6a6c6c" }] },
150
+ { 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" }] },
151
+ { 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" }] },
152
+ // ── RARE (10) ──────────────────────────────────────────────────────────
153
+ { 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" }] },
154
+ { 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" }] },
155
+ { 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" }] },
156
+ { 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" }] },
157
+ { 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" }] },
158
+ { 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" }] },
159
+ { 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" }] },
160
+ { id: "papal_mitre", name: "Papal Mitre", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "....QQQ....", "...QQQQQ...", "...QAAAQ...", "..AAAAAAA.."], variants: [{ A: "#FFFFFF", Q: "#FFD700" }] },
161
+ { id: "headdress", name: "Headdress", rarity: "rare", width: 11, anchor_x: 23, rows: ["...........", "...........", "...........", "...........", "...........", "...........", "..AQAQAQA..", "..AQAQAQA..", "..AAAAAAA..", "...QQQQQ..."], variants: [{ A: "#FF8F00", Q: "#1565C0" }] },
162
+ { 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" }] },
163
+ // ── EPIC (6) ───────────────────────────────────────────────────────────
164
+ { 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" }] },
165
+ { 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" }] },
166
+ { 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" }] },
167
+ { 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" }] },
168
+ { 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" }] },
169
+ { 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" }] },
170
+ // ── LEGENDARY (5) ──────────────────────────────────────────────────────
171
+ { id: "rainbow_crown", name: "Rainbow Crown", rarity: "legendary", width: 11, anchor_x: 23, rows: ["...........", "...........", ".....A.....", "....AAA....", "....AQA....", "....AAA....", "...AAQAA...", "...AAAAA...", "..AAAQAAA..", "..AAAAAAA.."], colors: { A: "#FFD700", Q: "#553f3f" }, animation: { type: "cycle", frames: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#8B00FF"], fps: 8 } },
172
+ { 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 } },
173
+ { 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 } },
174
+ { 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 } },
175
+ { 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 } }
176
+ ];
177
+
12
178
  // src/ui/HorseSprite.tsx
13
179
  import { Box, Text } from "ink";
14
180
 
@@ -235,6 +401,29 @@ function rowToAnsi(row) {
235
401
  return out;
236
402
  }
237
403
 
404
+ // src/ui/AnimatedHorseSprite.tsx
405
+ import { useEffect, useState } from "react";
406
+ import { Box as Box2, Text as Text2 } from "ink";
407
+ import { jsx as jsx2 } from "react/jsx-runtime";
408
+ function AnimatedHorseSprite({ sprite, colors, hat }) {
409
+ const isLegendary = hat.rarity === "legendary";
410
+ const frames = isLegendary ? hat.animation.frames : [];
411
+ const fps = isLegendary ? hat.animation.fps : 1;
412
+ const [idx, setIdx] = useState(0);
413
+ useEffect(() => {
414
+ if (!isLegendary || frames.length <= 1) return;
415
+ const interval = setInterval(
416
+ () => setIdx((i) => (i + 1) % frames.length),
417
+ Math.max(1, Math.round(1e3 / fps))
418
+ );
419
+ return () => clearInterval(interval);
420
+ }, [isLegendary, frames.length, fps]);
421
+ const renderedHat = isLegendary && frames[idx] ? { ...hat, colors: { ...hat.colors, A: frames[idx] } } : hat;
422
+ const { grid } = composeHatGrid(sprite, renderedHat, 0, colors);
423
+ const lines = hexGridToHalfBlocks(grid);
424
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx2(Text2, { children: line }, i)) });
425
+ }
426
+
238
427
  // src/ui/palette.ts
239
428
  var SLOTS = ["body", "mane", "tail", "saddle"];
240
429
  var PALETTES = {
@@ -332,14 +521,37 @@ function defaultColors() {
332
521
  }
333
522
 
334
523
  // src/ui/HorseCreator.tsx
335
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
336
- function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName, initialLevel }) {
337
- const [colors, setColors] = useState(initialColors ?? defaultColors());
338
- const [slotIdx, setSlotIdx] = useState(0);
339
- const [namingMode, setNamingMode] = useState(false);
340
- const [name, setName] = useState(initialName ?? "");
341
- const [error, setError] = useState(null);
342
- const slot = SLOTS[slotIdx];
524
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
525
+ function HorseCreator({
526
+ onSubmit,
527
+ onCancel,
528
+ initialColors,
529
+ initialName,
530
+ lockName,
531
+ initialLevel,
532
+ hats,
533
+ initialEquipped
534
+ }) {
535
+ const [colors, setColors] = useState2(initialColors ?? defaultColors());
536
+ const [hatChoice, setHatChoice] = useState2(initialEquipped ?? null);
537
+ const [rowIdx, setRowIdx] = useState2(0);
538
+ const [namingMode, setNamingMode] = useState2(false);
539
+ const [name, setName] = useState2(initialName ?? "");
540
+ const [error, setError] = useState2(null);
541
+ const hatRowEnabled = !!hats && hats.length > 0;
542
+ const rows = [
543
+ ...SLOTS.map((s) => ({ kind: "color", slot: s })),
544
+ ...hatRowEnabled ? [{ kind: "hat" }] : []
545
+ ];
546
+ const row = rows[rowIdx];
547
+ const cycleHat = (dir) => {
548
+ if (!hats || hats.length === 0) return;
549
+ const entries = [null, ...hats.map((_, i) => i)];
550
+ const cur = entries.findIndex((e) => e === hatChoice);
551
+ const start = cur < 0 ? 0 : cur;
552
+ const next = (start + dir + entries.length) % entries.length;
553
+ setHatChoice(entries[next]);
554
+ };
343
555
  useInput((input, key) => {
344
556
  if (namingMode) return;
345
557
  if (key.escape) {
@@ -347,67 +559,115 @@ function HorseCreator({ onSubmit, onCancel, initialColors, initialName, lockName
347
559
  return;
348
560
  }
349
561
  if (key.upArrow) {
350
- setSlotIdx((slotIdx - 1 + SLOTS.length) % SLOTS.length);
562
+ setRowIdx((rowIdx - 1 + rows.length) % rows.length);
351
563
  return;
352
564
  }
353
565
  if (key.downArrow) {
354
- setSlotIdx((slotIdx + 1) % SLOTS.length);
566
+ setRowIdx((rowIdx + 1) % rows.length);
355
567
  return;
356
568
  }
357
569
  if (key.leftArrow) {
358
- setColors({ ...colors, [slot]: prevColor(slot, colors[slot]) });
570
+ if (row.kind === "color") setColors({ ...colors, [row.slot]: prevColor(row.slot, colors[row.slot]) });
571
+ else cycleHat(-1);
359
572
  return;
360
573
  }
361
574
  if (key.rightArrow) {
362
- setColors({ ...colors, [slot]: nextColor(slot, colors[slot]) });
575
+ if (row.kind === "color") setColors({ ...colors, [row.slot]: nextColor(row.slot, colors[row.slot]) });
576
+ else cycleHat(1);
363
577
  return;
364
578
  }
365
579
  if (key.return) {
366
580
  if (lockName) {
367
- onSubmit(initialName ?? "", colors);
581
+ submit(initialName ?? "");
368
582
  return;
369
583
  }
370
584
  setNamingMode(true);
371
585
  return;
372
586
  }
373
587
  });
588
+ const submit = (finalName) => {
589
+ if (hatRowEnabled) onSubmit(finalName, colors, hatChoice);
590
+ else onSubmit(finalName, colors);
591
+ };
374
592
  const handleNameSubmit = (value) => {
375
593
  if (!value.trim()) {
376
594
  setError("Name required");
377
595
  return;
378
596
  }
379
- onSubmit(value.trim(), colors);
597
+ submit(value.trim());
380
598
  };
381
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", children: [
382
- lockName && initialName && /* @__PURE__ */ jsxs(Box2, { marginBottom: 1, children: [
383
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: initialName }),
384
- typeof initialLevel === "number" && /* @__PURE__ */ jsxs(Text2, { color: "cyan", children: [
599
+ const previewHat = hatRowEnabled && hatChoice !== null && hats ? hats[hatChoice] : void 0;
600
+ const previewHatDef = previewHat ? hatById(previewHat.id) : void 0;
601
+ return /* @__PURE__ */ jsxs(Box3, { flexDirection: "column", children: [
602
+ lockName && initialName && /* @__PURE__ */ jsxs(Box3, { marginBottom: 1, children: [
603
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: initialName }),
604
+ typeof initialLevel === "number" && /* @__PURE__ */ jsxs(Text3, { color: "cyan", children: [
385
605
  " [Lvl. ",
386
606
  initialLevel,
387
607
  "]"
388
608
  ] })
389
609
  ] }),
390
- /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(HorseSprite, { sprite: MAIN_SPRITE, colors }) }),
391
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: SLOTS.map((s, i) => /* @__PURE__ */ jsxs(Text2, { children: [
392
- i === slotIdx ? "\u25BA" : " ",
393
- " ",
394
- s.padEnd(7),
395
- " ",
396
- /* @__PURE__ */ jsx2(Text2, { color: colors[s], children: "\u2588\u2588" }),
397
- " ",
398
- colors[s]
399
- ] }, s)) }),
400
- !namingMode && /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191/\u2193 select slot \xB7 \u2190/\u2192 cycle color \xB7 Enter accept \xB7 Esc cancel" }) }),
401
- namingMode && /* @__PURE__ */ jsxs(Box2, { marginTop: 1, flexDirection: "column", children: [
402
- /* @__PURE__ */ jsx2(Text2, { children: "Name your horse: " }),
403
- /* @__PURE__ */ jsx2(TextInput, { value: name, onChange: (v) => {
610
+ /* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: previewHatDef ? previewHatDef.rarity === "legendary" ? /* @__PURE__ */ jsx3(AnimatedHorseSprite, { sprite: MAIN_SPRITE, colors, hat: previewHatDef }) : /* @__PURE__ */ jsx3(HorseSprite, { sprite: MAIN_SPRITE, colors, hat: { hat: previewHatDef, variant: previewHat?.variant ?? 0 } }) : /* @__PURE__ */ jsx3(HorseSprite, { sprite: MAIN_SPRITE, colors }) }),
611
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: rows.map((r, i) => {
612
+ const cursor = i === rowIdx ? "\u25BA" : " ";
613
+ if (r.kind === "color") {
614
+ return /* @__PURE__ */ jsxs(Text3, { children: [
615
+ cursor,
616
+ " ",
617
+ r.slot.padEnd(7),
618
+ " ",
619
+ /* @__PURE__ */ jsx3(Text3, { color: colors[r.slot], children: "\u2588\u2588" }),
620
+ " ",
621
+ colors[r.slot]
622
+ ] }, r.slot);
623
+ }
624
+ return /* @__PURE__ */ jsxs(Text3, { children: [
625
+ cursor,
626
+ " ",
627
+ "hat".padEnd(7),
628
+ " ",
629
+ renderHatLabel(hats, hatChoice, initialEquipped ?? null)
630
+ ] }, "hat");
631
+ }) }),
632
+ !namingMode && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2191/\u2193 select row \xB7 \u2190/\u2192 cycle \xB7 Enter accept \xB7 Esc cancel" }) }),
633
+ namingMode && /* @__PURE__ */ jsxs(Box3, { marginTop: 1, flexDirection: "column", children: [
634
+ /* @__PURE__ */ jsx3(Text3, { children: "Name your horse: " }),
635
+ /* @__PURE__ */ jsx3(TextInput, { value: name, onChange: (v) => {
404
636
  setName(v);
405
637
  setError(null);
406
638
  }, onSubmit: handleNameSubmit }),
407
- error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error })
639
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error })
408
640
  ] })
409
641
  ] });
410
642
  }
643
+ function renderHatLabel(hats, hatChoice, initialEquipped) {
644
+ if (!hats || hats.length === 0) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "none" });
645
+ if (hatChoice === null) {
646
+ const equippedMark2 = initialEquipped === null ? " \u2713" : "";
647
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
648
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Unequip" }),
649
+ equippedMark2
650
+ ] });
651
+ }
652
+ const collected = hats[hatChoice];
653
+ if (!collected) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "?" });
654
+ const hat = hatById(collected.id);
655
+ const name = hat?.name ?? collected.id;
656
+ const variantSuffix = hat && hat.rarity !== "legendary" && collected.variant !== void 0 ? ` #${collected.variant + 1}` : "";
657
+ const rarityColor = hat ? hat.rarity === "legendary" ? "yellow" : hat.rarity === "epic" ? "magenta" : hat.rarity === "rare" ? "blue" : "gray" : "gray";
658
+ const equippedMark = initialEquipped === hatChoice ? " \u2713" : "";
659
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
660
+ name,
661
+ variantSuffix,
662
+ " ",
663
+ /* @__PURE__ */ jsxs(Text3, { color: rarityColor, children: [
664
+ "[",
665
+ hat?.rarity ?? "?",
666
+ "]"
667
+ ] }),
668
+ equippedMark
669
+ ] });
670
+ }
411
671
 
412
672
  // src/config.ts
413
673
  var DEFAULT_API_BASE = "https://token-derby.mauricode.co.uk/api";
@@ -420,8 +680,8 @@ var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
420
680
  // src/version.ts
421
681
  import { createRequire } from "module";
422
682
  function readVersion() {
423
- if ("2.5.4".length > 0) {
424
- return "2.5.4";
683
+ if ("2.5.6".length > 0) {
684
+ return "2.5.6";
425
685
  }
426
686
  try {
427
687
  const req = createRequire(import.meta.url);
@@ -433,172 +693,6 @@ function readVersion() {
433
693
  }
434
694
  var CLI_VERSION = readVersion();
435
695
 
436
- // ../shared/dist/constants.js
437
- var CLI_VERSION_HEADER = "x-cli-version";
438
- var USER_ID_HEADER = "x-user-id";
439
- var USER_TOKEN_HEADER = "x-user-token";
440
- var USER_NAME_MAX_LENGTH = 40;
441
- var ORG_NAME_MAX_LENGTH = 12;
442
- var ORG_NAME_PATTERN = /^[A-Za-z0-9]{1,12}$/;
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
-
470
- // ../shared/dist/levels.js
471
- var MAX_LEVEL = 30;
472
- function xpForLevel(n) {
473
- return 1.8 * n ** 3 + 18 * n ** 2 + 50 * n - 19.8;
474
- }
475
- function thresholdForLevel(level) {
476
- if (level <= 1)
477
- return 0;
478
- return Math.round(xpForLevel(level - 1));
479
- }
480
- var XP_THRESHOLDS = Array.from({ length: MAX_LEVEL }, (_, i) => thresholdForLevel(i + 1));
481
- function levelFromXp(xp) {
482
- const v = Math.max(0, Math.floor(xp));
483
- let level = 1;
484
- while (level < MAX_LEVEL && v >= thresholdForLevel(level + 1)) {
485
- level++;
486
- }
487
- return level;
488
- }
489
- function levelInfo(xp) {
490
- const v = Math.max(0, Math.floor(xp));
491
- const level = levelFromXp(v);
492
- const level_start_xp = thresholdForLevel(level);
493
- const isMax = level >= MAX_LEVEL;
494
- const next_level_xp = isMax ? null : thresholdForLevel(level + 1);
495
- const xp_into_level = v - level_start_xp;
496
- const xp_for_level = isMax ? null : next_level_xp - level_start_xp;
497
- const progress = isMax ? 1 : Math.min(1, xp_into_level / Math.max(1, xp_for_level));
498
- return { level, xp: v, level_start_xp, next_level_xp, xp_into_level, xp_for_level, progress };
499
- }
500
-
501
- // ../shared/dist/midrace.js
502
- var ACHIEVEMENT_DESCRIPTIONS = {
503
- "Racer!": "Raced continuously for an hour",
504
- "Overtake!": "Overtook another horse",
505
- "Pacesetter!": "Led the race for an hour straight",
506
- "Stampede!": "Gained 7,000+ tokens in a single minute",
507
- "Took the lead!": "Charged into first place",
508
- "Comeback!": "Climbed from last place to the top half",
509
- "Pulled Away!": "Grew the lead by 5,000+ tokens in a minute"
510
- };
511
- function overtakeDescription(positionsClimbed) {
512
- if (positionsClimbed <= 1)
513
- return "Overtook another horse";
514
- return `Overtook ${positionsClimbed} horses`;
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.....", "....AAA....", "....AQA....", "....AAA....", "...AAQAA...", "...AAAAA...", "..AAAQAAA..", "..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
- ];
601
-
602
696
  // src/identity/identity.ts
603
697
  import { promises as fs } from "fs";
604
698
  import * as path2 from "path";
@@ -812,7 +906,7 @@ function equipHat(stableHorseId, body) {
812
906
  async function stableCreateCommand() {
813
907
  let exitCode = 0;
814
908
  const app = render(
815
- React2.createElement(HorseCreator, {
909
+ React3.createElement(HorseCreator, {
816
910
  onSubmit: async (name, colors) => {
817
911
  try {
818
912
  await createStableHorse({ name, colors });
@@ -844,9 +938,9 @@ async function stableCreateCommand() {
844
938
  }
845
939
 
846
940
  // src/commands/stable-list.tsx
847
- import React3 from "react";
848
- import { render as render2, Box as Box3, Text as Text3 } from "ink";
849
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
941
+ import React4 from "react";
942
+ import { render as render2, Box as Box4, Text as Text4 } from "ink";
943
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
850
944
  async function stableListCommand() {
851
945
  let horses;
852
946
  try {
@@ -864,28 +958,28 @@ async function stableListCommand() {
864
958
  return 0;
865
959
  }
866
960
  const app = render2(
867
- React3.createElement(StableList, { horses })
961
+ React4.createElement(StableList, { horses })
868
962
  );
869
963
  await app.waitUntilExit();
870
964
  return 0;
871
965
  }
872
966
  function StableList({ horses }) {
873
- React3.useEffect(() => {
967
+ React4.useEffect(() => {
874
968
  setImmediate(() => process.exit(0));
875
969
  }, []);
876
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", children: [
877
- /* @__PURE__ */ jsxs2(Text3, { bold: true, children: [
970
+ return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
971
+ /* @__PURE__ */ jsxs2(Text4, { bold: true, children: [
878
972
  "Your stable (",
879
973
  horses.length,
880
974
  "):"
881
975
  ] }),
882
- horses.map((h) => /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", marginTop: 1, children: [
883
- /* @__PURE__ */ jsx3(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors }),
884
- /* @__PURE__ */ jsxs2(Text3, { children: [
976
+ horses.map((h) => /* @__PURE__ */ jsxs2(Box4, { flexDirection: "row", marginTop: 1, children: [
977
+ /* @__PURE__ */ jsx4(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors }),
978
+ /* @__PURE__ */ jsxs2(Text4, { children: [
885
979
  " ",
886
980
  h.name,
887
981
  " ",
888
- /* @__PURE__ */ jsxs2(Text3, { color: "cyan", children: [
982
+ /* @__PURE__ */ jsxs2(Text4, { color: "cyan", children: [
889
983
  "[Lvl. ",
890
984
  levelFromXp(h.xp),
891
985
  "]"
@@ -939,120 +1033,16 @@ async function stableDeleteCommand(name) {
939
1033
  }
940
1034
 
941
1035
  // src/commands/stable-edit.ts
942
- import React7 from "react";
1036
+ import React6 from "react";
943
1037
  import { render as render3 } from "ink";
944
1038
 
945
- // src/ui/HatPicker.tsx
1039
+ // src/ui/HorsePicker.tsx
946
1040
  import { useState as useState3 } from "react";
947
1041
  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
1042
  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/ui/HorsePicker.tsx
1050
- import { useState as useState4 } from "react";
1051
- import { Box as Box6, Text as Text6, useInput as useInput3 } from "ink";
1052
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1053
1043
  function HorsePicker({ horses, onPick, onCancel }) {
1054
- const [idx, setIdx] = useState4(0);
1055
- useInput3((input, key) => {
1044
+ const [idx, setIdx] = useState3(0);
1045
+ useInput2((input, key) => {
1056
1046
  if (key.escape) {
1057
1047
  onCancel();
1058
1048
  return;
@@ -1072,31 +1062,31 @@ function HorsePicker({ horses, onPick, onCancel }) {
1072
1062
  }
1073
1063
  });
1074
1064
  if (horses.length === 0) {
1075
- return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1076
- /* @__PURE__ */ jsx6(Text6, { children: "No horses in your stable." }),
1077
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Run `token-derby stable create` to make one." })
1065
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
1066
+ /* @__PURE__ */ jsx5(Text5, { children: "No horses in your stable." }),
1067
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Run `token-derby stable create` to make one." })
1078
1068
  ] });
1079
1069
  }
1080
- return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1081
- /* @__PURE__ */ jsx6(Text6, { children: "Pick a horse to race:" }),
1082
- horses.map((h, i) => /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1083
- /* @__PURE__ */ jsx6(Box6, { flexDirection: "row", children: /* @__PURE__ */ jsxs4(Text6, { children: [
1070
+ return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
1071
+ /* @__PURE__ */ jsx5(Text5, { children: "Pick a horse to race:" }),
1072
+ horses.map((h, i) => /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
1073
+ /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", children: /* @__PURE__ */ jsxs3(Text5, { children: [
1084
1074
  i === idx ? "\u25BA" : " ",
1085
1075
  " ",
1086
1076
  h.name,
1087
1077
  " ",
1088
- /* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
1078
+ /* @__PURE__ */ jsxs3(Text5, { color: "cyan", children: [
1089
1079
  "[Lvl. ",
1090
1080
  levelFromXp(h.xp),
1091
1081
  "]"
1092
1082
  ] })
1093
1083
  ] }) }),
1094
- /* @__PURE__ */ jsxs4(Box6, { flexDirection: "row", children: [
1095
- /* @__PURE__ */ jsx6(Text6, { children: " " }),
1096
- /* @__PURE__ */ jsx6(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
1084
+ /* @__PURE__ */ jsxs3(Box5, { flexDirection: "row", children: [
1085
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
1086
+ /* @__PURE__ */ jsx5(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
1097
1087
  ] })
1098
1088
  ] }, h.stable_horse_id)),
1099
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
1089
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
1100
1090
  ] });
1101
1091
  }
1102
1092
 
@@ -1117,20 +1107,31 @@ async function stableEditCommand(name) {
1117
1107
  console.log("Cancelled.");
1118
1108
  return 1;
1119
1109
  }
1110
+ const initialEquipped = existing.equipped_hat ?? null;
1120
1111
  let exitCode = 0;
1121
- let liveColors = existing.colors;
1122
1112
  const app = render3(
1123
- React7.createElement(HorseCreator, {
1113
+ React6.createElement(HorseCreator, {
1124
1114
  initialColors: existing.colors,
1125
1115
  initialName: existing.name,
1126
1116
  lockName: true,
1127
1117
  initialLevel: levelFromXp(existing.xp),
1128
- onSubmit: async (_name, colors) => {
1118
+ hats: existing.hats,
1119
+ initialEquipped,
1120
+ onSubmit: async (_name, colors, hatChoice) => {
1121
+ const colorsChanged = !sameColors(colors, existing.colors);
1122
+ const hatChanged = hatChoice !== void 0 && hatChoice !== initialEquipped;
1129
1123
  try {
1130
- await updateStableHorse(existing.stable_horse_id, { colors });
1131
- liveColors = colors;
1124
+ if (colorsChanged) await updateStableHorse(existing.stable_horse_id, { colors });
1125
+ if (hatChanged) await equipHat(existing.stable_horse_id, { hat_index: hatChoice });
1132
1126
  app.unmount();
1133
- console.log(`\u2713 Updated "${existing.name}".`);
1127
+ if (!colorsChanged && !hatChanged) {
1128
+ console.log(`No changes for "${existing.name}".`);
1129
+ } else {
1130
+ const parts = [];
1131
+ if (colorsChanged) parts.push("colors");
1132
+ if (hatChanged) parts.push(hatChoice === null ? "hat unequipped" : "hat equipped");
1133
+ console.log(`\u2713 Updated "${existing.name}" (${parts.join(", ")}).`);
1134
+ }
1134
1135
  } catch (e) {
1135
1136
  app.unmount();
1136
1137
  if (e instanceof ApiError) {
@@ -1149,36 +1150,11 @@ async function stableEditCommand(name) {
1149
1150
  })
1150
1151
  );
1151
1152
  await app.waitUntilExit();
1152
- if (exitCode === 0 && existing.hats && existing.hats.length > 0) {
1153
- const equipResult = await new Promise((resolve) => {
1154
- const app2 = render3(
1155
- React7.createElement(HatPicker, {
1156
- hats: existing.hats,
1157
- equipped: existing.equipped_hat ?? null,
1158
- colors: liveColors,
1159
- onPick: (idx) => {
1160
- app2.unmount();
1161
- resolve({ done: true, idx });
1162
- },
1163
- onCancel: () => {
1164
- app2.unmount();
1165
- resolve({ done: false, idx: null });
1166
- }
1167
- })
1168
- );
1169
- });
1170
- if (equipResult.done) {
1171
- try {
1172
- await equipHat(existing.stable_horse_id, { hat_index: equipResult.idx });
1173
- console.log(equipResult.idx === null ? "Hat unequipped." : "Hat equipped.");
1174
- } catch (e) {
1175
- if (e instanceof ApiError) console.error(`Equip failed: ${e.code} ${e.message}`);
1176
- else throw e;
1177
- }
1178
- }
1179
- }
1180
1153
  return exitCode;
1181
1154
  }
1155
+ function sameColors(a, b) {
1156
+ return a.body === b.body && a.mane === b.mane && a.tail === b.tail && a.saddle === b.saddle;
1157
+ }
1182
1158
  async function fetchStable() {
1183
1159
  try {
1184
1160
  const resp = await listStable();
@@ -1199,7 +1175,7 @@ async function pickHorseToEdit(horses, name) {
1199
1175
  if (horses.length === 0) return "empty";
1200
1176
  const picked = await new Promise((resolve) => {
1201
1177
  const app = render3(
1202
- React7.createElement(HorsePicker, {
1178
+ React6.createElement(HorsePicker, {
1203
1179
  horses,
1204
1180
  onPick: (h) => {
1205
1181
  app.unmount();
@@ -1300,7 +1276,7 @@ function isIso(s) {
1300
1276
  }
1301
1277
 
1302
1278
  // src/commands/join.ts
1303
- import React9 from "react";
1279
+ import React8 from "react";
1304
1280
  import { render as render4 } from "ink";
1305
1281
 
1306
1282
  // src/stable/active-race.ts
@@ -1325,45 +1301,45 @@ async function saveActiveRace(active) {
1325
1301
  }
1326
1302
 
1327
1303
  // src/runtime/run-race.tsx
1328
- import { useEffect as useEffect2, useRef, useState as useState5 } from "react";
1329
- import { Box as Box8, Text as Text8, useApp } from "ink";
1304
+ import { useEffect as useEffect2, useRef, useState as useState4 } from "react";
1305
+ import { Box as Box7, Text as Text7, useApp } from "ink";
1330
1306
 
1331
1307
  // src/ui/StatusScreen.tsx
1332
- import { Box as Box7, Text as Text7 } from "ink";
1333
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1308
+ import { Box as Box6, Text as Text6 } from "ink";
1309
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1334
1310
  function StatusScreen(props) {
1335
1311
  const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk } = props;
1336
1312
  if (!race) {
1337
- return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
1313
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx6(Text6, { children: "Joining race\u2026" }) });
1338
1314
  }
1339
1315
  const own = race.horses.find((h) => h.horse_id === ownHorseId);
1340
1316
  const leader = race.horses[0];
1341
1317
  const elapsedPct = elapsed(race);
1342
1318
  const timeLeft = formatDuration(race.time_left_seconds);
1343
1319
  const lvl = levelInfo((own?.xp ?? 0) + (own?.live_xp ?? 0));
1344
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
1345
- /* @__PURE__ */ jsxs5(Text7, { children: [
1320
+ return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
1321
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1346
1322
  "\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
1347
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: race.name }),
1323
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: race.name }),
1348
1324
  " \u2500\u2500\u2500 status: ",
1349
- /* @__PURE__ */ jsx7(Text7, { color: statusColor(race.status), children: race.status })
1325
+ /* @__PURE__ */ jsx6(Text6, { color: statusColor(race.status), children: race.status })
1350
1326
  ] }),
1351
- /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "row", children: [
1352
- /* @__PURE__ */ jsx7(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
1353
- /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
1354
- /* @__PURE__ */ jsxs5(Text7, { children: [
1327
+ /* @__PURE__ */ jsxs4(Box6, { marginTop: 1, flexDirection: "row", children: [
1328
+ /* @__PURE__ */ jsx6(HorseSprite, { sprite: MINI_SPRITE, colors: ownColors }),
1329
+ /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1330
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1355
1331
  " ",
1356
1332
  ownHorseName,
1357
1333
  " ",
1358
- /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
1334
+ /* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
1359
1335
  "[Lvl. ",
1360
1336
  lvl.level,
1361
1337
  "]"
1362
1338
  ] })
1363
1339
  ] }),
1364
- /* @__PURE__ */ jsxs5(Text7, { children: [
1340
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1365
1341
  " ",
1366
- /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
1342
+ /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1367
1343
  "(",
1368
1344
  ownUserName,
1369
1345
  ")"
@@ -1371,43 +1347,43 @@ function StatusScreen(props) {
1371
1347
  ] })
1372
1348
  ] })
1373
1349
  ] }),
1374
- /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1375
- /* @__PURE__ */ jsxs5(Text7, { children: [
1350
+ /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", marginTop: 1, children: [
1351
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1376
1352
  "Tokens (race): ",
1377
1353
  own?.current_tokens ?? 0
1378
1354
  ] }),
1379
- /* @__PURE__ */ jsxs5(Text7, { children: [
1355
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1380
1356
  "Position: ",
1381
1357
  own?.rank ?? "\u2014",
1382
1358
  " of ",
1383
1359
  race.horses.length
1384
1360
  ] }),
1385
- /* @__PURE__ */ jsxs5(Text7, { children: [
1361
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1386
1362
  "Leader: ",
1387
1363
  leader ? `${leader.name}${leader.user_name ? ` (${leader.user_name})` : ""} \u2014 ${leader.current_tokens}` : "\u2014"
1388
1364
  ] }),
1389
- /* @__PURE__ */ jsxs5(Text7, { children: [
1365
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1390
1366
  "Race elapsed: ",
1391
1367
  (elapsedPct * 100).toFixed(0),
1392
1368
  "% ",
1393
1369
  bar(elapsedPct, 20)
1394
1370
  ] }),
1395
- /* @__PURE__ */ jsxs5(Text7, { children: [
1371
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1396
1372
  "Time left: ",
1397
1373
  timeLeft
1398
1374
  ] }),
1399
- /* @__PURE__ */ jsxs5(Text7, { children: [
1375
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1400
1376
  "XP: ",
1401
1377
  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)}`
1402
1378
  ] }),
1403
- /* @__PURE__ */ jsxs5(Text7, { children: [
1379
+ /* @__PURE__ */ jsxs4(Text6, { children: [
1404
1380
  "Last heartbeat: ",
1405
1381
  lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
1406
1382
  " ",
1407
- /* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1383
+ /* @__PURE__ */ jsx6(Text6, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
1408
1384
  ] })
1409
1385
  ] }),
1410
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1386
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1411
1387
  ] });
1412
1388
  }
1413
1389
  function elapsed(race) {
@@ -1556,15 +1532,15 @@ function initialBaseline(args) {
1556
1532
  }
1557
1533
 
1558
1534
  // src/runtime/run-race.tsx
1559
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1535
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1560
1536
  function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
1561
1537
  const { exit } = useApp();
1562
- const [race, setRace] = useState5(null);
1563
- const [lastHbAt, setLastHbAt] = useState5(null);
1564
- const [lastHbOk, setLastHbOk] = useState5(true);
1565
- const [tickNow, setTickNow] = useState5(/* @__PURE__ */ new Date());
1566
- const [fatalError, setFatalError] = useState5(null);
1567
- const [achievements, setAchievements] = useState5([]);
1538
+ const [race, setRace] = useState4(null);
1539
+ const [lastHbAt, setLastHbAt] = useState4(null);
1540
+ const [lastHbOk, setLastHbOk] = useState4(true);
1541
+ const [tickNow, setTickNow] = useState4(/* @__PURE__ */ new Date());
1542
+ const [fatalError, setFatalError] = useState4(null);
1543
+ const [achievements, setAchievements] = useState4([]);
1568
1544
  const shownAchievementAtRef = useRef(0);
1569
1545
  const baselineRef = useRef(startingBaseline);
1570
1546
  const pendingRef = useRef(pendingMode);
@@ -1648,13 +1624,13 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
1648
1624
  }, []);
1649
1625
  const lastHeartbeatAgoSec = lastHbAt ? Math.max(0, Math.floor((tickNow.getTime() - lastHbAt.getTime()) / 1e3)) : null;
1650
1626
  if (fatalError) {
1651
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", padding: 1, children: [
1652
- /* @__PURE__ */ jsx8(Text8, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
1653
- /* @__PURE__ */ jsx8(Text8, { children: fatalError })
1627
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", padding: 1, children: [
1628
+ /* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "CLI version mismatch \u2014 disconnected" }),
1629
+ /* @__PURE__ */ jsx7(Text7, { children: fatalError })
1654
1630
  ] });
1655
1631
  }
1656
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
1657
- /* @__PURE__ */ jsx8(
1632
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
1633
+ /* @__PURE__ */ jsx7(
1658
1634
  StatusScreen,
1659
1635
  {
1660
1636
  race,
@@ -1666,23 +1642,23 @@ function RunRace({ active, startingBaseline, pendingMode, ownUserName }) {
1666
1642
  lastHeartbeatOk: lastHbOk
1667
1643
  }
1668
1644
  ),
1669
- achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [
1670
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Achievements" }),
1645
+ achievements.length > 0 && /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1646
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Achievements" }),
1671
1647
  achievements.map(({ key, event }) => {
1672
1648
  const description = describeAchievement(event, active);
1673
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "row", children: [
1674
- /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
1649
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", children: [
1650
+ /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
1675
1651
  " ",
1676
1652
  formatClockTime(event.at),
1677
1653
  " "
1678
1654
  ] }),
1679
- /* @__PURE__ */ jsxs6(Text8, { color: "yellow", bold: true, children: [
1655
+ /* @__PURE__ */ jsxs5(Text7, { color: "yellow", bold: true, children: [
1680
1656
  "+",
1681
1657
  event.xp,
1682
1658
  " XP "
1683
1659
  ] }),
1684
- /* @__PURE__ */ jsx8(Text8, { children: event.name }),
1685
- /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
1660
+ /* @__PURE__ */ jsx7(Text7, { children: event.name }),
1661
+ /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
1686
1662
  " \u2014 ",
1687
1663
  description
1688
1664
  ] })
@@ -1829,14 +1805,14 @@ async function joinCommand(joinCode) {
1829
1805
  };
1830
1806
  await saveActiveRace(active);
1831
1807
  const initial = await buildInitialState({ active, raceStatus: status, rejoin: isResume });
1832
- const app = render4(React9.createElement(RunRace, { active, ...initial, ownUserName: identity.display_name }));
1808
+ const app = render4(React8.createElement(RunRace, { active, ...initial, ownUserName: identity.display_name }));
1833
1809
  await app.waitUntilExit();
1834
1810
  return 0;
1835
1811
  }
1836
1812
  async function pickHorse(horses) {
1837
1813
  return new Promise((resolve) => {
1838
1814
  const app = render4(
1839
- React9.createElement(HorsePicker, {
1815
+ React8.createElement(HorsePicker, {
1840
1816
  horses,
1841
1817
  onPick: (h) => {
1842
1818
  app.unmount();
@@ -2048,30 +2024,30 @@ function runNpmUpgrade(spawnImpl) {
2048
2024
  }
2049
2025
 
2050
2026
  // src/commands/roll.ts
2051
- import React13 from "react";
2027
+ import React12 from "react";
2052
2028
  import { render as render5 } from "ink";
2053
2029
 
2054
2030
  // src/ui/RollReveal.tsx
2055
- import { useState as useState7, useEffect as useEffect4, useMemo } from "react";
2056
- import { Box as Box10, Text as Text10 } from "ink";
2031
+ import { useState as useState6, useEffect as useEffect4, useMemo } from "react";
2032
+ import { Box as Box9, Text as Text9 } from "ink";
2057
2033
 
2058
2034
  // src/ui/HatSprite.tsx
2059
- import { useEffect as useEffect3, useState as useState6 } from "react";
2060
- import { Box as Box9, Text as Text9 } from "ink";
2061
- import { jsx as jsx9 } from "react/jsx-runtime";
2035
+ import { useEffect as useEffect3, useState as useState5 } from "react";
2036
+ import { Box as Box8, Text as Text8 } from "ink";
2037
+ import { jsx as jsx8 } from "react/jsx-runtime";
2062
2038
  function HatSprite({ hat, variant, centerIn }) {
2063
2039
  const colors = hatColorsFor2(hat, variant ?? 0);
2064
2040
  const grid = makeHatGrid(hat, colors, centerIn);
2065
2041
  const lines = hexGridToHalfBlocks(grid);
2066
- return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line }, i)) });
2042
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { children: line }, i)) });
2067
2043
  }
2068
2044
  function AnimatedHatSprite({ hat, variant, centerIn }) {
2069
2045
  if (hat.rarity !== "legendary") {
2070
- return /* @__PURE__ */ jsx9(HatSprite, { hat, variant, centerIn });
2046
+ return /* @__PURE__ */ jsx8(HatSprite, { hat, variant, centerIn });
2071
2047
  }
2072
2048
  const frames = hat.animation.frames;
2073
2049
  const fps = hat.animation.fps;
2074
- const [idx, setIdx] = useState6(0);
2050
+ const [idx, setIdx] = useState5(0);
2075
2051
  useEffect3(() => {
2076
2052
  if (frames.length <= 1) return;
2077
2053
  const interval = setInterval(
@@ -2081,7 +2057,7 @@ function AnimatedHatSprite({ hat, variant, centerIn }) {
2081
2057
  return () => clearInterval(interval);
2082
2058
  }, [frames.length, fps]);
2083
2059
  const framed = { ...hat, colors: { ...hat.colors, A: frames[idx] } };
2084
- return /* @__PURE__ */ jsx9(HatSprite, { hat: framed, variant, centerIn });
2060
+ return /* @__PURE__ */ jsx8(HatSprite, { hat: framed, variant, centerIn });
2085
2061
  }
2086
2062
  function hatColorsFor2(hat, variantIdx) {
2087
2063
  if (hat.rarity === "legendary") return hat.colors;
@@ -2108,7 +2084,7 @@ function makeHatGrid(hat, colors, centerIn) {
2108
2084
  }
2109
2085
 
2110
2086
  // src/ui/RollReveal.tsx
2111
- import { jsx as jsx10 } from "react/jsx-runtime";
2087
+ import { jsx as jsx9 } from "react/jsx-runtime";
2112
2088
  var RESET3 = "\x1B[0m";
2113
2089
  var BOX_COLOR = "#E5C76B";
2114
2090
  var TIER_PALETTE = {
@@ -2177,7 +2153,7 @@ var BOX_EMPTY = [
2177
2153
  ""
2178
2154
  ].map(pad);
2179
2155
  function GiftBox({ frame, color }) {
2180
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx10(Text10, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
2156
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx9(Text9, { children: line ? ansiFg(color) + line + RESET3 : line }, i)) });
2181
2157
  }
2182
2158
  function spawnParticles(tier, count, cx, cy) {
2183
2159
  const palette = TIER_PALETTE[tier];
@@ -2200,7 +2176,7 @@ function ConfettiBurst({ tier }) {
2200
2176
  const cx = Math.floor(SCENE_W / 2);
2201
2177
  const cy = Math.floor(SCENE_H / 2);
2202
2178
  const particles = useMemo(() => spawnParticles(tier, 36, cx, cy), [tier, cx, cy]);
2203
- const [tick, setTick] = useState7(0);
2179
+ const [tick, setTick] = useState6(0);
2204
2180
  useEffect4(() => {
2205
2181
  const i = setInterval(() => setTick((t) => t + 1), 70);
2206
2182
  return () => clearInterval(i);
@@ -2213,13 +2189,13 @@ function ConfettiBurst({ tier }) {
2213
2189
  grid[y][x] = ansiFg(p.color) + p.char + RESET3;
2214
2190
  }
2215
2191
  }
2216
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx10(Text10, { children: row.join("") }, y)) });
2192
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: grid.map((row, y) => /* @__PURE__ */ jsx9(Text9, { children: row.join("") }, y)) });
2217
2193
  }
2218
2194
  var CLOSED_HOLD_MS = 3e3;
2219
2195
  function RollReveal({ outcome, onDone }) {
2220
2196
  const isNoHat = outcome.kind === "no_hat";
2221
2197
  const isLegendary = outcome.kind !== "no_hat" && outcome.hat.rarity === "legendary";
2222
- const [phase, setPhase] = useState7("closed");
2198
+ const [phase, setPhase] = useState6("closed");
2223
2199
  useEffect4(() => {
2224
2200
  const timers = [];
2225
2201
  timers.push(setTimeout(() => setPhase("open1"), CLOSED_HOLD_MS));
@@ -2234,24 +2210,24 @@ function RollReveal({ outcome, onDone }) {
2234
2210
  }
2235
2211
  return () => timers.forEach(clearTimeout);
2236
2212
  }, [isNoHat, isLegendary, onDone]);
2237
- if (phase === "closed") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
2238
- if (phase === "open1") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
2239
- if (phase === "open2") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
2240
- if (phase === "empty") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2213
+ if (phase === "closed") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_CLOSED, color: BOX_COLOR });
2214
+ if (phase === "open1") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_OPENING_1, color: BOX_COLOR });
2215
+ if (phase === "open2") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_OPENING_2, color: BOX_COLOR });
2216
+ if (phase === "empty") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2241
2217
  if (phase === "burst" && outcome.kind !== "no_hat") {
2242
- return /* @__PURE__ */ jsx10(ConfettiBurst, { tier: outcome.hat.rarity });
2218
+ return /* @__PURE__ */ jsx9(ConfettiBurst, { tier: outcome.hat.rarity });
2243
2219
  }
2244
- if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx10(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2245
- 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 } });
2220
+ if (outcome.kind === "no_hat") return /* @__PURE__ */ jsx9(GiftBox, { frame: BOX_EMPTY, color: BOX_COLOR });
2221
+ return outcome.hat.rarity === "legendary" ? /* @__PURE__ */ jsx9(AnimatedHatSprite, { hat: outcome.hat, centerIn: { w: SCENE_W, h: SCENE_H } }) : /* @__PURE__ */ jsx9(HatSprite, { hat: outcome.hat, variant: outcome.variant, centerIn: { w: SCENE_W, h: SCENE_H } });
2246
2222
  }
2247
2223
 
2248
2224
  // src/ui/RollHorsePicker.tsx
2249
- import { useState as useState8 } from "react";
2250
- import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
2251
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2225
+ import { useState as useState7 } from "react";
2226
+ import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
2227
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2252
2228
  function RollHorsePicker({ horses, onPick, onCancel }) {
2253
- const [idx, setIdx] = useState8(0);
2254
- useInput4((input, key) => {
2229
+ const [idx, setIdx] = useState7(0);
2230
+ useInput3((input, key) => {
2255
2231
  if (key.escape) {
2256
2232
  onCancel();
2257
2233
  return;
@@ -2270,33 +2246,33 @@ function RollHorsePicker({ horses, onPick, onCancel }) {
2270
2246
  return;
2271
2247
  }
2272
2248
  });
2273
- return /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
2274
- /* @__PURE__ */ jsx11(Text11, { children: "Pick a horse to roll for:" }),
2275
- horses.map((h, i) => /* @__PURE__ */ jsxs7(Box11, { flexDirection: "column", children: [
2276
- /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs7(Text11, { children: [
2249
+ return /* @__PURE__ */ jsxs6(Box10, { flexDirection: "column", children: [
2250
+ /* @__PURE__ */ jsx10(Text10, { children: "Pick a horse to roll for:" }),
2251
+ horses.map((h, i) => /* @__PURE__ */ jsxs6(Box10, { flexDirection: "column", children: [
2252
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "row", children: /* @__PURE__ */ jsxs6(Text10, { children: [
2277
2253
  i === idx ? "\u25BA" : " ",
2278
2254
  " ",
2279
2255
  h.name,
2280
2256
  " ",
2281
- /* @__PURE__ */ jsxs7(Text11, { color: "cyan", children: [
2257
+ /* @__PURE__ */ jsxs6(Text10, { color: "cyan", children: [
2282
2258
  "[Lvl. ",
2283
2259
  levelFromXp(h.xp),
2284
2260
  "]"
2285
2261
  ] }),
2286
2262
  " ",
2287
- /* @__PURE__ */ jsxs7(Text11, { color: "yellow", children: [
2263
+ /* @__PURE__ */ jsxs6(Text10, { color: "yellow", children: [
2288
2264
  "\u2014 ",
2289
2265
  h.pending,
2290
2266
  " roll",
2291
2267
  h.pending === 1 ? "" : "s"
2292
2268
  ] })
2293
2269
  ] }) }),
2294
- /* @__PURE__ */ jsxs7(Box11, { flexDirection: "row", children: [
2295
- /* @__PURE__ */ jsx11(Text11, { children: " " }),
2296
- /* @__PURE__ */ jsx11(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
2270
+ /* @__PURE__ */ jsxs6(Box10, { flexDirection: "row", children: [
2271
+ /* @__PURE__ */ jsx10(Text10, { children: " " }),
2272
+ /* @__PURE__ */ jsx10(HorseSprite, { sprite: MINI_SPRITE, colors: h.colors })
2297
2273
  ] })
2298
2274
  ] }, h.stable_horse_id)),
2299
- /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
2275
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2191/\u2193 choose \xB7 Enter pick \xB7 Esc cancel" }) })
2300
2276
  ] });
2301
2277
  }
2302
2278
 
@@ -2321,10 +2297,11 @@ function resetStdinAfterInk() {
2321
2297
  while (process.stdin.read() !== null) {
2322
2298
  }
2323
2299
  process.stdin.pause();
2300
+ process.stdin.ref();
2324
2301
  }
2325
2302
  async function runReveal(outcome) {
2326
2303
  await new Promise((resolve) => {
2327
- const app = render5(React13.createElement(RollReveal, {
2304
+ const app = render5(React12.createElement(RollReveal, {
2328
2305
  outcome,
2329
2306
  onDone: () => {
2330
2307
  app.unmount();
@@ -2350,7 +2327,7 @@ async function rollCommand() {
2350
2327
  return 0;
2351
2328
  }
2352
2329
  const picked = await new Promise((resolve) => {
2353
- const app = render5(React13.createElement(RollHorsePicker, {
2330
+ const app = render5(React12.createElement(RollHorsePicker, {
2354
2331
  horses: eligible,
2355
2332
  onPick: (h) => {
2356
2333
  app.unmount();