@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.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/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
- package/LICENSE +1 -1
- package/README.md +102 -72
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +59 -44
- package/cli/biomes.ts +309 -0
- package/cli/buddy-shell.ts +818 -0
- package/{adapters/claude/install → cli}/disable.ts +14 -32
- package/{adapters/claude/install → cli}/doctor.ts +55 -57
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +36 -25
- package/{adapters/claude/install → cli}/install.ts +100 -145
- package/{adapters/claude/install → cli}/pick.ts +111 -50
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
- package/cli/tui.tsx +2333 -0
- package/{adapters/claude/install → cli}/uninstall.ts +23 -38
- package/cli/upgrade.ts +213 -0
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +113 -59
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +56 -0
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -0
- package/server/art.ts +450 -0
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/server/index.ts +1340 -0
- package/server/mcp-launcher.sh +26 -0
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +82 -0
- package/server/shift-mood.ts +43 -0
- package/{adapters/claude/storage → server}/state.ts +200 -7
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
- package/statusline/buddy-status.sh +397 -0
- package/statusline/combined-status.sh +134 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -204
- package/adapters/claude/popup/buddy-popup.sh +0 -92
- package/adapters/claude/popup/buddy-render.sh +0 -540
- package/adapters/claude/popup/popup-manager.sh +0 -355
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/index.ts +0 -752
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -389
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -24
- package/adapters/claude/storage/settings.ts +0 -41
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { buddyStateDir } from "./path.ts";
|
|
4
|
+
import { awardXp } from "./xp.ts";
|
|
5
|
+
|
|
6
|
+
const STATE_DIR = buddyStateDir();
|
|
7
|
+
const EVENTS_FILE = join(STATE_DIR, "events.json");
|
|
8
|
+
const DAYS_FILE = join(STATE_DIR, "active_days.json");
|
|
9
|
+
const UNLOCKED_FILE = join(STATE_DIR, "unlocked.json");
|
|
10
|
+
|
|
11
|
+
function slotEventsFile(slot: string): string {
|
|
12
|
+
return join(STATE_DIR, `events.${slot}.json`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureDir(): void {
|
|
16
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function atomicWrite(path: string, data: string): void {
|
|
20
|
+
ensureDir();
|
|
21
|
+
const tmp = path + ".tmp";
|
|
22
|
+
writeFileSync(tmp, data);
|
|
23
|
+
renameSync(tmp, path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GlobalCounters {
|
|
27
|
+
errors_seen: number;
|
|
28
|
+
tests_failed: number;
|
|
29
|
+
large_diffs: number;
|
|
30
|
+
sessions: number;
|
|
31
|
+
commands_run: number;
|
|
32
|
+
days_active: number;
|
|
33
|
+
turns: number;
|
|
34
|
+
// From PR #68 (achievement expansion)
|
|
35
|
+
buddies_collected: number;
|
|
36
|
+
renames: number;
|
|
37
|
+
personalities_set: number;
|
|
38
|
+
mutes: number;
|
|
39
|
+
unmutes: number;
|
|
40
|
+
summons: number;
|
|
41
|
+
dismissals: number;
|
|
42
|
+
shows: number;
|
|
43
|
+
helps: number;
|
|
44
|
+
achievement_views: number;
|
|
45
|
+
saves: number;
|
|
46
|
+
lists: number;
|
|
47
|
+
achievements_unlocked: number;
|
|
48
|
+
// From PR #71 (reaction contexts expansion)
|
|
49
|
+
commits_made: number;
|
|
50
|
+
pushes_made: number;
|
|
51
|
+
conflicts_resolved: number;
|
|
52
|
+
branches_created: number;
|
|
53
|
+
rebases_done: number;
|
|
54
|
+
late_night_sessions: number;
|
|
55
|
+
early_sessions: number;
|
|
56
|
+
marathon_sessions: number;
|
|
57
|
+
weekend_sessions: number;
|
|
58
|
+
type_errors: number;
|
|
59
|
+
lint_fails: number;
|
|
60
|
+
build_fails: number;
|
|
61
|
+
security_warnings: number;
|
|
62
|
+
deprecations_seen: number;
|
|
63
|
+
all_green: number;
|
|
64
|
+
deploys: number;
|
|
65
|
+
releases: number;
|
|
66
|
+
late_night_commits: number;
|
|
67
|
+
friday_pushes: number;
|
|
68
|
+
marathon_errors: number;
|
|
69
|
+
weekend_conflicts: number;
|
|
70
|
+
recoveries: number;
|
|
71
|
+
marathon_recoveries: number;
|
|
72
|
+
max_error_streak: number;
|
|
73
|
+
holiday_sessions: number;
|
|
74
|
+
spooky_sessions: number;
|
|
75
|
+
april_fools_errors: number;
|
|
76
|
+
bugs_resolved: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SlotCounters {
|
|
80
|
+
pets: number;
|
|
81
|
+
reactions_given: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface EventCounters extends GlobalCounters {
|
|
85
|
+
pets: number;
|
|
86
|
+
reactions_given: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const GLOBAL_KEYS: (keyof GlobalCounters)[] = [
|
|
90
|
+
"errors_seen", "tests_failed", "large_diffs",
|
|
91
|
+
"sessions", "commands_run", "days_active", "turns",
|
|
92
|
+
"buddies_collected", "renames", "personalities_set",
|
|
93
|
+
"mutes", "unmutes", "summons", "dismissals",
|
|
94
|
+
"shows", "helps", "achievement_views", "saves", "lists",
|
|
95
|
+
"achievements_unlocked",
|
|
96
|
+
"commits_made", "pushes_made", "conflicts_resolved",
|
|
97
|
+
"branches_created", "rebases_done",
|
|
98
|
+
"late_night_sessions", "early_sessions", "marathon_sessions", "weekend_sessions",
|
|
99
|
+
"type_errors", "lint_fails", "build_fails",
|
|
100
|
+
"security_warnings", "deprecations_seen",
|
|
101
|
+
"all_green", "deploys", "releases",
|
|
102
|
+
"late_night_commits", "friday_pushes", "marathon_errors", "weekend_conflicts",
|
|
103
|
+
"recoveries", "marathon_recoveries", "max_error_streak",
|
|
104
|
+
"holiday_sessions", "spooky_sessions", "april_fools_errors",
|
|
105
|
+
"bugs_resolved",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export const SLOT_KEYS: (keyof SlotCounters)[] = [
|
|
109
|
+
"pets", "reactions_given",
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
export const COUNTER_KEYS: (keyof EventCounters)[] = [
|
|
113
|
+
...GLOBAL_KEYS, ...SLOT_KEYS,
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const EMPTY_GLOBAL: GlobalCounters = {
|
|
117
|
+
errors_seen: 0, tests_failed: 0, large_diffs: 0,
|
|
118
|
+
sessions: 0, commands_run: 0, days_active: 0, turns: 0,
|
|
119
|
+
// From PR #68
|
|
120
|
+
buddies_collected: 0, renames: 0, personalities_set: 0,
|
|
121
|
+
mutes: 0, unmutes: 0, summons: 0, dismissals: 0,
|
|
122
|
+
shows: 0, helps: 0, achievement_views: 0, saves: 0, lists: 0,
|
|
123
|
+
achievements_unlocked: 0,
|
|
124
|
+
// From PR #71
|
|
125
|
+
commits_made: 0, pushes_made: 0, conflicts_resolved: 0,
|
|
126
|
+
branches_created: 0, rebases_done: 0,
|
|
127
|
+
late_night_sessions: 0, early_sessions: 0, marathon_sessions: 0, weekend_sessions: 0,
|
|
128
|
+
type_errors: 0, lint_fails: 0, build_fails: 0,
|
|
129
|
+
security_warnings: 0, deprecations_seen: 0,
|
|
130
|
+
all_green: 0, deploys: 0, releases: 0,
|
|
131
|
+
late_night_commits: 0, friday_pushes: 0, marathon_errors: 0, weekend_conflicts: 0,
|
|
132
|
+
recoveries: 0, marathon_recoveries: 0, max_error_streak: 0,
|
|
133
|
+
holiday_sessions: 0, spooky_sessions: 0, april_fools_errors: 0,
|
|
134
|
+
bugs_resolved: 0,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const EMPTY_SLOT: SlotCounters = {
|
|
138
|
+
pets: 0, reactions_given: 0,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export function loadGlobalEvents(): GlobalCounters {
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(readFileSync(EVENTS_FILE, "utf8"));
|
|
144
|
+
return { ...EMPTY_GLOBAL, ...parsed };
|
|
145
|
+
} catch {
|
|
146
|
+
return { ...EMPTY_GLOBAL };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function saveGlobalEvents(events: GlobalCounters): void {
|
|
151
|
+
atomicWrite(EVENTS_FILE, JSON.stringify(events, null, 2));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function loadSlotEvents(slot: string): SlotCounters {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(readFileSync(slotEventsFile(slot), "utf8"));
|
|
157
|
+
return { ...EMPTY_SLOT, ...parsed };
|
|
158
|
+
} catch {
|
|
159
|
+
return { ...EMPTY_SLOT };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function saveSlotEvents(slot: string, events: SlotCounters): void {
|
|
164
|
+
atomicWrite(slotEventsFile(slot), JSON.stringify(events, null, 2));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function loadEvents(slot?: string): EventCounters {
|
|
168
|
+
const global = loadGlobalEvents();
|
|
169
|
+
if (!slot) {
|
|
170
|
+
return { ...global, pets: 0, reactions_given: 0 };
|
|
171
|
+
}
|
|
172
|
+
const slotEvents = loadSlotEvents(slot);
|
|
173
|
+
return {
|
|
174
|
+
...global,
|
|
175
|
+
pets: slotEvents.pets,
|
|
176
|
+
reactions_given: slotEvents.reactions_given,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function incrementEvent(key: keyof EventCounters, amount: number = 1, slot?: string): EventCounters {
|
|
181
|
+
if ((SLOT_KEYS as string[]).includes(key) && slot) {
|
|
182
|
+
const slotEvents = loadSlotEvents(slot);
|
|
183
|
+
(slotEvents as any)[key] += amount;
|
|
184
|
+
saveSlotEvents(slot, slotEvents);
|
|
185
|
+
} else {
|
|
186
|
+
const global = loadGlobalEvents();
|
|
187
|
+
if ((GLOBAL_KEYS as string[]).includes(key)) {
|
|
188
|
+
(global as any)[key] += amount;
|
|
189
|
+
}
|
|
190
|
+
saveGlobalEvents(global);
|
|
191
|
+
}
|
|
192
|
+
return loadEvents(slot);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { loadEvents as loadGlobalEventsCompat, loadGlobalEvents as loadGlobalEventsDirect };
|
|
196
|
+
|
|
197
|
+
interface DayTracker {
|
|
198
|
+
lastDate: string;
|
|
199
|
+
totalDays: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function trackActiveDay(): void {
|
|
203
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
204
|
+
let tracker: DayTracker;
|
|
205
|
+
try {
|
|
206
|
+
tracker = JSON.parse(readFileSync(DAYS_FILE, "utf8"));
|
|
207
|
+
} catch {
|
|
208
|
+
tracker = { lastDate: "", totalDays: 0 };
|
|
209
|
+
}
|
|
210
|
+
if (tracker.lastDate === today) return;
|
|
211
|
+
|
|
212
|
+
tracker.lastDate = today;
|
|
213
|
+
tracker.totalDays += 1;
|
|
214
|
+
atomicWrite(DAYS_FILE, JSON.stringify(tracker, null, 2));
|
|
215
|
+
|
|
216
|
+
const events = loadGlobalEvents();
|
|
217
|
+
events.days_active = tracker.totalDays;
|
|
218
|
+
saveGlobalEvents(events);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface Achievement {
|
|
222
|
+
id: string;
|
|
223
|
+
name: string;
|
|
224
|
+
description: string;
|
|
225
|
+
icon: string;
|
|
226
|
+
check: (events: EventCounters) => boolean;
|
|
227
|
+
secret: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const ACHIEVEMENTS: Achievement[] = [
|
|
231
|
+
{
|
|
232
|
+
id: "first_steps",
|
|
233
|
+
name: "First Steps",
|
|
234
|
+
description: "Hatch your buddy for the first time",
|
|
235
|
+
icon: "\ud83c\udf1f",
|
|
236
|
+
check: () => true,
|
|
237
|
+
secret: false,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "good_boy",
|
|
241
|
+
name: "Good Buddy",
|
|
242
|
+
description: "Pet your companion 10 times",
|
|
243
|
+
icon: "\ud83e\uddf9",
|
|
244
|
+
check: (e) => e.pets >= 10,
|
|
245
|
+
secret: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "best_friend",
|
|
249
|
+
name: "Best Friend",
|
|
250
|
+
description: "Pet your companion 50 times",
|
|
251
|
+
icon: "\ud83d\udc95",
|
|
252
|
+
check: (e) => e.pets >= 50,
|
|
253
|
+
secret: false,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "bug_spotter",
|
|
257
|
+
name: "Bug Spotter",
|
|
258
|
+
description: "Witness your first error together",
|
|
259
|
+
icon: "\ud83d\udc1b",
|
|
260
|
+
check: (e) => e.errors_seen >= 1,
|
|
261
|
+
secret: false,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "error_whisperer",
|
|
265
|
+
name: "Error Whisperer",
|
|
266
|
+
description: "Survive 25 errors as a team",
|
|
267
|
+
icon: "\ud83d\udd27",
|
|
268
|
+
check: (e) => e.errors_seen >= 25,
|
|
269
|
+
secret: false,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "battle_scarred",
|
|
273
|
+
name: "Battle-Scarred",
|
|
274
|
+
description: "Survive 100 errors together",
|
|
275
|
+
icon: "\ud83d\udc80",
|
|
276
|
+
check: (e) => e.errors_seen >= 100,
|
|
277
|
+
secret: true,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "test_witness",
|
|
281
|
+
name: "Test Witness",
|
|
282
|
+
description: "See your first test failure",
|
|
283
|
+
icon: "\u274c",
|
|
284
|
+
check: (e) => e.tests_failed >= 1,
|
|
285
|
+
secret: false,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "test_veteran",
|
|
289
|
+
name: "Test Veteran",
|
|
290
|
+
description: "Witness 50 test failures",
|
|
291
|
+
icon: "\ud83d\udcca",
|
|
292
|
+
check: (e) => e.tests_failed >= 50,
|
|
293
|
+
secret: false,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "big_mover",
|
|
297
|
+
name: "Big Mover",
|
|
298
|
+
description: "Make a diff with 80+ lines",
|
|
299
|
+
icon: "\ud83d\udce6",
|
|
300
|
+
check: (e) => e.large_diffs >= 1,
|
|
301
|
+
secret: false,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "refactor_machine",
|
|
305
|
+
name: "Refactor Machine",
|
|
306
|
+
description: "Make 10 large diffs",
|
|
307
|
+
icon: "\ud83d\udd28",
|
|
308
|
+
check: (e) => e.large_diffs >= 10,
|
|
309
|
+
secret: false,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "chatterbox",
|
|
313
|
+
name: "Chatterbox",
|
|
314
|
+
description: "Your buddy reacts 100 times",
|
|
315
|
+
icon: "\ud83d\udcac",
|
|
316
|
+
check: (e) => e.reactions_given >= 100,
|
|
317
|
+
secret: false,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "week_streak",
|
|
321
|
+
name: "Week Streak",
|
|
322
|
+
description: "Code with your buddy for 7 days",
|
|
323
|
+
icon: "\ud83d\udd25",
|
|
324
|
+
check: (e) => e.days_active >= 7,
|
|
325
|
+
secret: false,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "month_streak",
|
|
329
|
+
name: "Month Streak",
|
|
330
|
+
description: "Code with your buddy for 30 days",
|
|
331
|
+
icon: "\ud83d\udc51",
|
|
332
|
+
check: (e) => e.days_active >= 30,
|
|
333
|
+
secret: true,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "power_user",
|
|
337
|
+
name: "Power User",
|
|
338
|
+
description: "Run 50 buddy commands",
|
|
339
|
+
icon: "\u26a1",
|
|
340
|
+
check: (e) => e.commands_run >= 50,
|
|
341
|
+
secret: false,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "dedicated",
|
|
345
|
+
name: "Dedicated Companion",
|
|
346
|
+
description: "Complete 200 turns together",
|
|
347
|
+
icon: "\ud83c\udfc5",
|
|
348
|
+
check: (e) => e.turns >= 200,
|
|
349
|
+
secret: false,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: "thousand_turns",
|
|
353
|
+
name: "Thousand Turns",
|
|
354
|
+
description: "Reach 1000 turns together",
|
|
355
|
+
icon: "\ud83c\udf96",
|
|
356
|
+
check: (e) => e.turns >= 1000,
|
|
357
|
+
secret: true,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "first_commit",
|
|
361
|
+
name: "First Blood",
|
|
362
|
+
description: "Make your first commit",
|
|
363
|
+
icon: "\ud83c\udfaf",
|
|
364
|
+
check: (e) => e.commits_made >= 1,
|
|
365
|
+
secret: false,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "commit_machine",
|
|
369
|
+
name: "Commit Machine",
|
|
370
|
+
description: "Make 50 commits",
|
|
371
|
+
icon: "\ud83c\udfed",
|
|
372
|
+
check: (e) => e.commits_made >= 50,
|
|
373
|
+
secret: false,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "centurion",
|
|
377
|
+
name: "Centurion",
|
|
378
|
+
description: "Make 100 commits",
|
|
379
|
+
icon: "\ud83d\udcaf",
|
|
380
|
+
check: (e) => e.commits_made >= 100,
|
|
381
|
+
secret: true,
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: "conflict_resolver",
|
|
385
|
+
name: "Diplomat",
|
|
386
|
+
description: "Resolve your first merge conflict",
|
|
387
|
+
icon: "\ud83d\udd4a\ufe0f",
|
|
388
|
+
check: (e) => e.conflicts_resolved >= 1,
|
|
389
|
+
secret: false,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: "peacekeeper",
|
|
393
|
+
name: "Peacekeeper",
|
|
394
|
+
description: "Resolve 10 merge conflicts",
|
|
395
|
+
icon: "\u2696\ufe0f",
|
|
396
|
+
check: (e) => e.conflicts_resolved >= 10,
|
|
397
|
+
secret: false,
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: "war_hero",
|
|
401
|
+
name: "War Hero",
|
|
402
|
+
description: "Resolve 25 merge conflicts",
|
|
403
|
+
icon: "\u2694\ufe0f",
|
|
404
|
+
check: (e) => e.conflicts_resolved >= 25,
|
|
405
|
+
secret: true,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "frequent_pusher",
|
|
409
|
+
name: "Ship It",
|
|
410
|
+
description: "Push 20 times",
|
|
411
|
+
icon: "\ud83d\ude80",
|
|
412
|
+
check: (e) => e.pushes_made >= 20,
|
|
413
|
+
secret: false,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "branch_hopper",
|
|
417
|
+
name: "Multiverse",
|
|
418
|
+
description: "Create 10 branches",
|
|
419
|
+
icon: "\ud83d\udd00",
|
|
420
|
+
check: (e) => e.branches_created >= 10,
|
|
421
|
+
secret: false,
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "rebase_master",
|
|
425
|
+
name: "Time Traveler",
|
|
426
|
+
description: "Complete 10 rebases",
|
|
427
|
+
icon: "\u23f3",
|
|
428
|
+
check: (e) => e.rebases_done >= 10,
|
|
429
|
+
secret: false,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: "night_owl",
|
|
433
|
+
name: "Night Owl",
|
|
434
|
+
description: "Code past 2am",
|
|
435
|
+
icon: "\ud83e\udd89",
|
|
436
|
+
check: (e) => e.late_night_sessions >= 1,
|
|
437
|
+
secret: false,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "vampire",
|
|
441
|
+
name: "Vampire",
|
|
442
|
+
description: "Code past 4am (3 sessions)",
|
|
443
|
+
icon: "\ud83e\udddb",
|
|
444
|
+
check: (e) => e.late_night_sessions >= 3,
|
|
445
|
+
secret: true,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: "marathoner",
|
|
449
|
+
name: "Marathoner",
|
|
450
|
+
description: "3+ hour coding session",
|
|
451
|
+
icon: "\ud83c\udfc3",
|
|
452
|
+
check: (e) => e.marathon_sessions >= 1,
|
|
453
|
+
secret: false,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "weekend_warrior",
|
|
457
|
+
name: "Weekend Warrior",
|
|
458
|
+
description: "Code on a weekend",
|
|
459
|
+
icon: "\u2694\ufe0f",
|
|
460
|
+
check: (e) => e.weekend_sessions >= 1,
|
|
461
|
+
secret: false,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: "early_bird",
|
|
465
|
+
name: "Early Bird",
|
|
466
|
+
description: "Code before 7am",
|
|
467
|
+
icon: "\ud83d\udc26",
|
|
468
|
+
check: (e) => e.early_sessions >= 1,
|
|
469
|
+
secret: false,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "type_warrior",
|
|
473
|
+
name: "Type Warrior",
|
|
474
|
+
description: "Survive 10 TypeScript errors",
|
|
475
|
+
icon: "\ud83d\udee1\ufe0f",
|
|
476
|
+
check: (e) => e.type_errors >= 10,
|
|
477
|
+
secret: false,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "type_master",
|
|
481
|
+
name: "Type Master",
|
|
482
|
+
description: "Survive 50 TypeScript errors",
|
|
483
|
+
icon: "\ud83e\uddd9",
|
|
484
|
+
check: (e) => e.type_errors >= 50,
|
|
485
|
+
secret: true,
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: "lint_scholar",
|
|
489
|
+
name: "Lint Scholar",
|
|
490
|
+
description: "See your first lint error",
|
|
491
|
+
icon: "\ud83d\udccf",
|
|
492
|
+
check: (e) => e.lint_fails >= 1,
|
|
493
|
+
secret: false,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: "security_conscious",
|
|
497
|
+
name: "Security Mind",
|
|
498
|
+
description: "Encounter a vulnerability warning",
|
|
499
|
+
icon: "\ud83d\udd12",
|
|
500
|
+
check: (e) => e.security_warnings >= 1,
|
|
501
|
+
secret: false,
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: "security_expert",
|
|
505
|
+
name: "Security Expert",
|
|
506
|
+
description: "Fix 10 vulnerability warnings",
|
|
507
|
+
icon: "\ud83c\udfc6",
|
|
508
|
+
check: (e) => e.security_warnings >= 10,
|
|
509
|
+
secret: false,
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "build_breaker",
|
|
513
|
+
name: "Build Breaker",
|
|
514
|
+
description: "Break the build 5 times",
|
|
515
|
+
icon: "\ud83d\udca5",
|
|
516
|
+
check: (e) => e.build_fails >= 5,
|
|
517
|
+
secret: false,
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: "antique_collector",
|
|
521
|
+
name: "Antique Collector",
|
|
522
|
+
description: "See 10 deprecation warnings",
|
|
523
|
+
icon: "\ud83c\udfdb\ufe0f",
|
|
524
|
+
check: (e) => e.deprecations_seen >= 10,
|
|
525
|
+
secret: false,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "green_machine",
|
|
529
|
+
name: "Green Machine",
|
|
530
|
+
description: "All tests pass for the first time",
|
|
531
|
+
icon: "\u2705",
|
|
532
|
+
check: (e) => e.all_green >= 1,
|
|
533
|
+
secret: false,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: "deployer",
|
|
537
|
+
name: "Ship to Prod",
|
|
538
|
+
description: "Deploy for the first time",
|
|
539
|
+
icon: "\ud83d\udea2",
|
|
540
|
+
check: (e) => e.deploys >= 1,
|
|
541
|
+
secret: false,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "veteran_deployer",
|
|
545
|
+
name: "Veteran Deployer",
|
|
546
|
+
description: "Deploy 10 times",
|
|
547
|
+
icon: "\u2693",
|
|
548
|
+
check: (e) => e.deploys >= 10,
|
|
549
|
+
secret: false,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "releaser",
|
|
553
|
+
name: "Release Manager",
|
|
554
|
+
description: "Create your first release",
|
|
555
|
+
icon: "\ud83d\udce6",
|
|
556
|
+
check: (e) => e.releases >= 1,
|
|
557
|
+
secret: false,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "midnight_oil",
|
|
561
|
+
name: "Burning the Midnight Oil",
|
|
562
|
+
description: "Commit past 3am",
|
|
563
|
+
icon: "\ud83d\udd6f\ufe0f",
|
|
564
|
+
check: (e) => e.late_night_commits >= 1,
|
|
565
|
+
secret: false,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "friday_deploy",
|
|
569
|
+
name: "Living Dangerously",
|
|
570
|
+
description: "Push on a Friday",
|
|
571
|
+
icon: "\ud83c\udfb0",
|
|
572
|
+
check: (e) => e.friday_pushes >= 1,
|
|
573
|
+
secret: false,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
id: "iron_will",
|
|
577
|
+
name: "Iron Will",
|
|
578
|
+
description: "Fix an error after 3+ hour session",
|
|
579
|
+
icon: "\ud83d\udcaa",
|
|
580
|
+
check: (e) => e.marathon_errors >= 1,
|
|
581
|
+
secret: false,
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
id: "weekend_warrior_deluxe",
|
|
585
|
+
name: "No Rest for the Wicked",
|
|
586
|
+
description: "Resolve a merge conflict on a weekend",
|
|
587
|
+
icon: "\ud83d\ude08",
|
|
588
|
+
check: (e) => e.weekend_conflicts >= 1,
|
|
589
|
+
secret: true,
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
id: "comeback_kid",
|
|
593
|
+
name: "Comeback Kid",
|
|
594
|
+
description: "Fix an error within 10 minutes of seeing it",
|
|
595
|
+
icon: "\ud83d\udd04",
|
|
596
|
+
check: (e) => e.recoveries >= 1,
|
|
597
|
+
secret: false,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: "phoenix",
|
|
601
|
+
name: "Phoenix Rising",
|
|
602
|
+
description: "Recover from 5 failures",
|
|
603
|
+
icon: "\ud83d\udd25",
|
|
604
|
+
check: (e) => e.recoveries >= 5,
|
|
605
|
+
secret: false,
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "iron_resolve",
|
|
609
|
+
name: "Iron Resolve",
|
|
610
|
+
description: "Recover from a failure after 3+ hour session",
|
|
611
|
+
icon: "\ud83d\udc8e",
|
|
612
|
+
check: (e) => e.marathon_recoveries >= 1,
|
|
613
|
+
secret: true,
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
id: "unlucky_streak",
|
|
617
|
+
name: "Snake Eyes",
|
|
618
|
+
description: "5 errors in a row",
|
|
619
|
+
icon: "\ud83c\udfb2",
|
|
620
|
+
check: (e) => e.max_error_streak >= 5,
|
|
621
|
+
secret: false,
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "cursed",
|
|
625
|
+
name: "Cursed",
|
|
626
|
+
description: "10 errors in a row",
|
|
627
|
+
icon: "\ud83d\udc80",
|
|
628
|
+
check: (e) => e.max_error_streak >= 10,
|
|
629
|
+
secret: true,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: "groundhog_day",
|
|
633
|
+
name: "Groundhog Day",
|
|
634
|
+
description: "20 errors in a row",
|
|
635
|
+
icon: "\ud83d\udd04",
|
|
636
|
+
check: (e) => e.max_error_streak >= 20,
|
|
637
|
+
secret: true,
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: "holiday_coder",
|
|
641
|
+
name: "Holiday Spirit",
|
|
642
|
+
description: "Code on a holiday",
|
|
643
|
+
icon: "\ud83c\udf84",
|
|
644
|
+
check: (e) => e.holiday_sessions >= 1,
|
|
645
|
+
secret: false,
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
id: "spooky_dev",
|
|
649
|
+
name: "Spooky Developer",
|
|
650
|
+
description: "Code during spooky season",
|
|
651
|
+
icon: "\ud83c\udf83",
|
|
652
|
+
check: (e) => e.spooky_sessions >= 1,
|
|
653
|
+
secret: false,
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "april_fool",
|
|
657
|
+
name: "Fool Me Once",
|
|
658
|
+
description: "Encounter an error on April 1st",
|
|
659
|
+
icon: "\ud83e\udd21",
|
|
660
|
+
check: (e) => e.april_fools_errors >= 1,
|
|
661
|
+
secret: true,
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: "session_regular",
|
|
665
|
+
name: "Regular",
|
|
666
|
+
description: "Start 10 coding sessions",
|
|
667
|
+
icon: "\ud83d\udd04",
|
|
668
|
+
check: (e) => e.sessions >= 10,
|
|
669
|
+
secret: false,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: "session_veteran",
|
|
673
|
+
name: "Session Veteran",
|
|
674
|
+
description: "Start 50 coding sessions",
|
|
675
|
+
icon: "\ud83c\udfe2",
|
|
676
|
+
check: (e) => e.sessions >= 50,
|
|
677
|
+
secret: false,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: "session_centurion",
|
|
681
|
+
name: "Centurion",
|
|
682
|
+
description: "Start 100 coding sessions",
|
|
683
|
+
icon: "\ud83d\udcaf",
|
|
684
|
+
check: (e) => e.sessions >= 100,
|
|
685
|
+
secret: false,
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
id: "collector",
|
|
689
|
+
name: "Collector",
|
|
690
|
+
description: "Save 3 buddies to your menagerie",
|
|
691
|
+
icon: "\ud83d\udcda",
|
|
692
|
+
check: (e) => e.buddies_collected >= 3,
|
|
693
|
+
secret: false,
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
id: "zookeeper",
|
|
697
|
+
name: "Zookeeper",
|
|
698
|
+
description: "Save 5 buddies to your menagerie",
|
|
699
|
+
icon: "\ud83d\udc12",
|
|
700
|
+
check: (e) => e.buddies_collected >= 5,
|
|
701
|
+
secret: false,
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "identity_crisis",
|
|
705
|
+
name: "Identity Crisis",
|
|
706
|
+
description: "Rename your buddy for the first time",
|
|
707
|
+
icon: "\ud83c\udd94",
|
|
708
|
+
check: (e) => e.renames >= 1,
|
|
709
|
+
secret: false,
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
id: "method_acting",
|
|
713
|
+
name: "Method Acting",
|
|
714
|
+
description: "Give your buddy a custom personality",
|
|
715
|
+
icon: "\ud83c\udfad",
|
|
716
|
+
check: (e) => e.personalities_set >= 1,
|
|
717
|
+
secret: false,
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
id: "pet_overflow",
|
|
721
|
+
name: "Century of Pets",
|
|
722
|
+
description: "Pet your companion 100 times",
|
|
723
|
+
icon: "\ud83d\udc3e",
|
|
724
|
+
check: (e) => e.pets >= 100,
|
|
725
|
+
secret: false,
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: "pet_legend",
|
|
729
|
+
name: "Legendary Petter",
|
|
730
|
+
description: "Pet your companion 250 times",
|
|
731
|
+
icon: "\ud83e\udd1e",
|
|
732
|
+
check: (e) => e.pets >= 250,
|
|
733
|
+
secret: false,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
id: "error_titan",
|
|
737
|
+
name: "Error Titan",
|
|
738
|
+
description: "Survive 500 errors together",
|
|
739
|
+
icon: "\ud83c\udf0b",
|
|
740
|
+
check: (e) => e.errors_seen >= 500,
|
|
741
|
+
secret: false,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
id: "error_god",
|
|
745
|
+
name: "Error God",
|
|
746
|
+
description: "Survive 1000 errors together",
|
|
747
|
+
icon: "\ud83c\udf20",
|
|
748
|
+
check: (e) => e.errors_seen >= 1000,
|
|
749
|
+
secret: false,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: "test_survivor",
|
|
753
|
+
name: "Test Survivor",
|
|
754
|
+
description: "Witness 200 test failures",
|
|
755
|
+
icon: "\ud83e\uddea",
|
|
756
|
+
check: (e) => e.tests_failed >= 200,
|
|
757
|
+
secret: false,
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
id: "test_masochist",
|
|
761
|
+
name: "Test Masochist",
|
|
762
|
+
description: "Witness 500 test failures",
|
|
763
|
+
icon: "\ud83d\udc80",
|
|
764
|
+
check: (e) => e.tests_failed >= 500,
|
|
765
|
+
secret: false,
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
id: "massive_mover",
|
|
769
|
+
name: "Massive Mover",
|
|
770
|
+
description: "Make 25 large diffs",
|
|
771
|
+
icon: "\ud83d\udea7",
|
|
772
|
+
check: (e) => e.large_diffs >= 25,
|
|
773
|
+
secret: false,
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
id: "earth_mover",
|
|
777
|
+
name: "Earth Mover",
|
|
778
|
+
description: "Make 50 large diffs",
|
|
779
|
+
icon: "\ud83c\udf0d",
|
|
780
|
+
check: (e) => e.large_diffs >= 50,
|
|
781
|
+
secret: false,
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
id: "social_butterfly",
|
|
785
|
+
name: "Social Butterfly",
|
|
786
|
+
description: "Your buddy reacts 250 times",
|
|
787
|
+
icon: "\ud83e\udd8b",
|
|
788
|
+
check: (e) => e.reactions_given >= 250,
|
|
789
|
+
secret: false,
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
id: "hypersocial",
|
|
793
|
+
name: "Hypersocial",
|
|
794
|
+
description: "Your buddy reacts 500 times",
|
|
795
|
+
icon: "\ud83d\udce3",
|
|
796
|
+
check: (e) => e.reactions_given >= 500,
|
|
797
|
+
secret: false,
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
id: "never_shuts_up",
|
|
801
|
+
name: "Never Shuts Up",
|
|
802
|
+
description: "Your buddy reacts 1000 times",
|
|
803
|
+
icon: "\ud83e\udd2b",
|
|
804
|
+
check: (e) => e.reactions_given >= 1000,
|
|
805
|
+
secret: false,
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: "hundred_days",
|
|
809
|
+
name: "Hundred Days",
|
|
810
|
+
description: "Code with your buddy for 100 days",
|
|
811
|
+
icon: "\ud83d\udcc5",
|
|
812
|
+
check: (e) => e.days_active >= 100,
|
|
813
|
+
secret: false,
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
id: "year_streak",
|
|
817
|
+
name: "Year Streak",
|
|
818
|
+
description: "Code with your buddy for 365 days",
|
|
819
|
+
icon: "\ud83d\udcc6",
|
|
820
|
+
check: (e) => e.days_active >= 365,
|
|
821
|
+
secret: false,
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
id: "commander",
|
|
825
|
+
name: "Commander",
|
|
826
|
+
description: "Run 200 buddy commands",
|
|
827
|
+
icon: "\ud83c\udf96",
|
|
828
|
+
check: (e) => e.commands_run >= 200,
|
|
829
|
+
secret: false,
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
id: "command_overlord",
|
|
833
|
+
name: "Command Overlord",
|
|
834
|
+
description: "Run 500 buddy commands",
|
|
835
|
+
icon: "\ud83c\udff9",
|
|
836
|
+
check: (e) => e.commands_run >= 500,
|
|
837
|
+
secret: false,
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
id: "five_thousand_turns",
|
|
841
|
+
name: "Five Thousand Turns",
|
|
842
|
+
description: "Reach 5000 turns together",
|
|
843
|
+
icon: "\ud83d\udd2e",
|
|
844
|
+
check: (e) => e.turns >= 5000,
|
|
845
|
+
secret: false,
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "ten_thousand_turns",
|
|
849
|
+
name: "Ten Thousand Turns",
|
|
850
|
+
description: "Reach 10000 turns together",
|
|
851
|
+
icon: "\ud83c\udf00",
|
|
852
|
+
check: (e) => e.turns >= 10000,
|
|
853
|
+
secret: false,
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
id: "menagerie",
|
|
857
|
+
name: "Menagerie",
|
|
858
|
+
description: "Save 10 buddies to your menagerie",
|
|
859
|
+
icon: "\ud83c\udfaa",
|
|
860
|
+
check: (e) => e.buddies_collected >= 10,
|
|
861
|
+
secret: false,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
id: "name_chameleon",
|
|
865
|
+
name: "Name Chameleon",
|
|
866
|
+
description: "Rename your buddy 5 times",
|
|
867
|
+
icon: "\ud83e\udd8e",
|
|
868
|
+
check: (e) => e.renames >= 5,
|
|
869
|
+
secret: false,
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: "fashionista",
|
|
873
|
+
name: "Fashionista",
|
|
874
|
+
description: "Change your buddy's personality 3 times",
|
|
875
|
+
icon: "\ud83d\udc85",
|
|
876
|
+
check: (e) => e.personalities_set >= 3,
|
|
877
|
+
secret: false,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: "silent_treatment",
|
|
881
|
+
name: "Silent Treatment",
|
|
882
|
+
description: "Mute your buddy for the first time",
|
|
883
|
+
icon: "\ud83d\udd07",
|
|
884
|
+
check: (e) => e.mutes >= 1,
|
|
885
|
+
secret: false,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
id: "prodigal",
|
|
889
|
+
name: "Prodigal",
|
|
890
|
+
description: "Summon a buddy from your menagerie",
|
|
891
|
+
icon: "\ud83d\udd19",
|
|
892
|
+
check: (e) => e.summons >= 1,
|
|
893
|
+
secret: false,
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
id: "menagerie_hop",
|
|
897
|
+
name: "Menagerie Hop",
|
|
898
|
+
description: "Summon buddies 10 times",
|
|
899
|
+
icon: "\ud83d\udd00",
|
|
900
|
+
check: (e) => e.summons >= 10,
|
|
901
|
+
secret: false,
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
id: "heartbreaker",
|
|
905
|
+
name: "Heartbreaker",
|
|
906
|
+
description: "Dismiss your first buddy",
|
|
907
|
+
icon: "\ud83d\udc94",
|
|
908
|
+
check: (e) => e.dismissals >= 1,
|
|
909
|
+
secret: false,
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: "pet_obsessed",
|
|
913
|
+
name: "Pet Obsessed",
|
|
914
|
+
description: "Pet your companion 500 times",
|
|
915
|
+
icon: "\ud83e\udd11",
|
|
916
|
+
check: (e) => e.pets >= 500,
|
|
917
|
+
secret: false,
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
id: "pet_god",
|
|
921
|
+
name: "Pet God",
|
|
922
|
+
description: "Pet your companion 1000 times",
|
|
923
|
+
icon: "\ud83d\udc51",
|
|
924
|
+
check: (e) => e.pets >= 1000,
|
|
925
|
+
secret: false,
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
id: "error_apocalypse",
|
|
929
|
+
name: "Error Apocalypse",
|
|
930
|
+
description: "Survive 5000 errors together",
|
|
931
|
+
icon: "\ud83c\udf0b",
|
|
932
|
+
check: (e) => e.errors_seen >= 5000,
|
|
933
|
+
secret: false,
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
id: "test_immortal",
|
|
937
|
+
name: "Test Immortal",
|
|
938
|
+
description: "Witness 1000 test failures",
|
|
939
|
+
icon: "\ud83e\uddd8",
|
|
940
|
+
check: (e) => e.tests_failed >= 1000,
|
|
941
|
+
secret: false,
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
id: "continental_drift",
|
|
945
|
+
name: "Continental Drift",
|
|
946
|
+
description: "Make 100 large diffs",
|
|
947
|
+
icon: "\ud83c\udf0f",
|
|
948
|
+
check: (e) => e.large_diffs >= 100,
|
|
949
|
+
secret: false,
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
id: "tectonic_shift",
|
|
953
|
+
name: "Tectonic Shift",
|
|
954
|
+
description: "Make 250 large diffs",
|
|
955
|
+
icon: "\ud83c\udf0b",
|
|
956
|
+
check: (e) => e.large_diffs >= 250,
|
|
957
|
+
secret: false,
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
id: "chatterbox_elite",
|
|
961
|
+
name: "Chatterbox Elite",
|
|
962
|
+
description: "Your buddy reacts 2500 times",
|
|
963
|
+
icon: "\ud83c\udf0a",
|
|
964
|
+
check: (e) => e.reactions_given >= 2500,
|
|
965
|
+
secret: false,
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
id: "no_off_switch",
|
|
969
|
+
name: "No Off Switch",
|
|
970
|
+
description: "Your buddy reacts 5000 times",
|
|
971
|
+
icon: "\ud83d\udd0a",
|
|
972
|
+
check: (e) => e.reactions_given >= 5000,
|
|
973
|
+
secret: false,
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
id: "two_week_streak",
|
|
977
|
+
name: "Two Week Warrior",
|
|
978
|
+
description: "Code with your buddy for 14 days",
|
|
979
|
+
icon: "\u270a",
|
|
980
|
+
check: (e) => e.days_active >= 14,
|
|
981
|
+
secret: false,
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
id: "quarter_streak",
|
|
985
|
+
name: "Quarter Streak",
|
|
986
|
+
description: "Code with your buddy for 90 days",
|
|
987
|
+
icon: "\ud83d\udcc8",
|
|
988
|
+
check: (e) => e.days_active >= 90,
|
|
989
|
+
secret: false,
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
id: "command_addict",
|
|
993
|
+
name: "Command Addict",
|
|
994
|
+
description: "Run 1000 buddy commands",
|
|
995
|
+
icon: "\ud83d\udcbb",
|
|
996
|
+
check: (e) => e.commands_run >= 1000,
|
|
997
|
+
secret: false,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
id: "command_deity",
|
|
1001
|
+
name: "Command Deity",
|
|
1002
|
+
description: "Run 2500 buddy commands",
|
|
1003
|
+
icon: "\ud83d\udd31",
|
|
1004
|
+
check: (e) => e.commands_run >= 2500,
|
|
1005
|
+
secret: false,
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: "twenty_five_k_turns",
|
|
1009
|
+
name: "25K Turns",
|
|
1010
|
+
description: "Reach 25000 turns together",
|
|
1011
|
+
icon: "\ud83d\udcb0",
|
|
1012
|
+
check: (e) => e.turns >= 25000,
|
|
1013
|
+
secret: false,
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
id: "fifty_k_turns",
|
|
1017
|
+
name: "50K Turns",
|
|
1018
|
+
description: "Reach 50000 turns together",
|
|
1019
|
+
icon: "\ud83d\udc8e",
|
|
1020
|
+
check: (e) => e.turns >= 50000,
|
|
1021
|
+
secret: false,
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "session_addict",
|
|
1025
|
+
name: "Session Addict",
|
|
1026
|
+
description: "Start 250 coding sessions",
|
|
1027
|
+
icon: "\ud83d\udcb8",
|
|
1028
|
+
check: (e) => e.sessions >= 250,
|
|
1029
|
+
secret: false,
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
id: "session_machine",
|
|
1033
|
+
name: "Session Machine",
|
|
1034
|
+
description: "Start 500 coding sessions",
|
|
1035
|
+
icon: "\ud83e\udd16",
|
|
1036
|
+
check: (e) => e.sessions >= 500,
|
|
1037
|
+
secret: false,
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: "buddy_hoarder",
|
|
1041
|
+
name: "Buddy Hoarder",
|
|
1042
|
+
description: "Save 20 buddies to your menagerie",
|
|
1043
|
+
icon: "\ud83c\udf81",
|
|
1044
|
+
check: (e) => e.buddies_collected >= 20,
|
|
1045
|
+
secret: false,
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
id: "buddy_tycoon",
|
|
1049
|
+
name: "Buddy Tycoon",
|
|
1050
|
+
description: "Save 50 buddies to your menagerie",
|
|
1051
|
+
icon: "\ud83c\udfe6",
|
|
1052
|
+
check: (e) => e.buddies_collected >= 50,
|
|
1053
|
+
secret: false,
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
id: "serial_renamer",
|
|
1057
|
+
name: "Serial Renamer",
|
|
1058
|
+
description: "Rename your buddy 10 times",
|
|
1059
|
+
icon: "\ud83d\udcdb",
|
|
1060
|
+
check: (e) => e.renames >= 10,
|
|
1061
|
+
secret: false,
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
id: "identity_thief",
|
|
1065
|
+
name: "Identity Thief",
|
|
1066
|
+
description: "Rename your buddy 25 times",
|
|
1067
|
+
icon: "\ud83d\ude08",
|
|
1068
|
+
check: (e) => e.renames >= 25,
|
|
1069
|
+
secret: false,
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
id: "personality_crisis",
|
|
1073
|
+
name: "Personality Crisis",
|
|
1074
|
+
description: "Change your buddy's personality 10 times",
|
|
1075
|
+
icon: "\ud83e\uddd8",
|
|
1076
|
+
check: (e) => e.personalities_set >= 10,
|
|
1077
|
+
secret: false,
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
id: "menagerie_hopper",
|
|
1081
|
+
name: "Menagerie Hopper",
|
|
1082
|
+
description: "Summon buddies 25 times",
|
|
1083
|
+
icon: "\ud83c\udfb0",
|
|
1084
|
+
check: (e) => e.summons >= 25,
|
|
1085
|
+
secret: false,
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
id: "summoner",
|
|
1089
|
+
name: "Summoner",
|
|
1090
|
+
description: "Summon buddies 50 times",
|
|
1091
|
+
icon: "\u2728",
|
|
1092
|
+
check: (e) => e.summons >= 50,
|
|
1093
|
+
secret: false,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
id: "serial_dumper",
|
|
1097
|
+
name: "Serial Dumper",
|
|
1098
|
+
description: "Dismiss 5 buddies",
|
|
1099
|
+
icon: "\ud83d\udca9",
|
|
1100
|
+
check: (e) => e.dismissals >= 5,
|
|
1101
|
+
secret: false,
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
id: "cold_blooded",
|
|
1105
|
+
name: "Cold Blooded",
|
|
1106
|
+
description: "Dismiss 10 buddies",
|
|
1107
|
+
icon: "\ud83e\udd82",
|
|
1108
|
+
check: (e) => e.dismissals >= 10,
|
|
1109
|
+
secret: false,
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
id: "on_off",
|
|
1113
|
+
name: "On Off",
|
|
1114
|
+
description: "Mute and unmute your buddy",
|
|
1115
|
+
icon: "\ud83d\udd1c",
|
|
1116
|
+
check: (e) => e.mutes >= 1 && e.unmutes >= 1,
|
|
1117
|
+
secret: false,
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
id: "indecisive",
|
|
1121
|
+
name: "Indecisive",
|
|
1122
|
+
description: "Mute and unmute 5 times each",
|
|
1123
|
+
icon: "\ud83e\udee3",
|
|
1124
|
+
check: (e) => e.mutes >= 5 && e.unmutes >= 5,
|
|
1125
|
+
secret: false,
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
id: "show_off",
|
|
1129
|
+
name: "Show Off",
|
|
1130
|
+
description: "Show your buddy 10 times",
|
|
1131
|
+
icon: "\ud83d\udc40",
|
|
1132
|
+
check: (e) => e.shows >= 10,
|
|
1133
|
+
secret: false,
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: "exhibitionist",
|
|
1137
|
+
name: "Exhibitionist",
|
|
1138
|
+
description: "Show your buddy 50 times",
|
|
1139
|
+
icon: "\ud83c\udfa9",
|
|
1140
|
+
check: (e) => e.shows >= 50,
|
|
1141
|
+
secret: false,
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
id: "help_me",
|
|
1145
|
+
name: "Help Me",
|
|
1146
|
+
description: "Ask for help for the first time",
|
|
1147
|
+
icon: "\u2753",
|
|
1148
|
+
check: (e) => e.helps >= 1,
|
|
1149
|
+
secret: false,
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
id: "help_addict",
|
|
1153
|
+
name: "Help Addict",
|
|
1154
|
+
description: "Ask for help 10 times",
|
|
1155
|
+
icon: "\ud83d\udcd6",
|
|
1156
|
+
check: (e) => e.helps >= 10,
|
|
1157
|
+
secret: false,
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: "achievement_hunter",
|
|
1161
|
+
name: "Achievement Hunter",
|
|
1162
|
+
description: "Check your achievements 5 times",
|
|
1163
|
+
icon: "\ud83d\udd0d",
|
|
1164
|
+
check: (e) => e.achievement_views >= 5,
|
|
1165
|
+
secret: false,
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
id: "achievement_stalker",
|
|
1169
|
+
name: "Achievement Stalker",
|
|
1170
|
+
description: "Check your achievements 25 times",
|
|
1171
|
+
icon: "\ud83d\udd2d",
|
|
1172
|
+
check: (e) => e.achievement_views >= 25,
|
|
1173
|
+
secret: false,
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
id: "pack_rat",
|
|
1177
|
+
name: "Pack Rat",
|
|
1178
|
+
description: "Save a buddy to a slot",
|
|
1179
|
+
icon: "\ud83d\udcbe",
|
|
1180
|
+
check: (e) => e.saves >= 1,
|
|
1181
|
+
secret: false,
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
id: "compulsive_saver",
|
|
1185
|
+
name: "Compulsive Saver",
|
|
1186
|
+
description: "Save buddies 10 times",
|
|
1187
|
+
icon: "\ud83d\udd04",
|
|
1188
|
+
check: (e) => e.saves >= 10,
|
|
1189
|
+
secret: false,
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
id: "roster_check",
|
|
1193
|
+
name: "Roster Check",
|
|
1194
|
+
description: "List your buddies for the first time",
|
|
1195
|
+
icon: "\ud83d\udccb",
|
|
1196
|
+
check: (e) => e.lists >= 1,
|
|
1197
|
+
secret: false,
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
id: "roster_obsessed",
|
|
1201
|
+
name: "Roster Obsessed",
|
|
1202
|
+
description: "List your buddies 10 times",
|
|
1203
|
+
icon: "\ud83d\udcdd",
|
|
1204
|
+
check: (e) => e.lists >= 10,
|
|
1205
|
+
secret: false,
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
id: "troubled",
|
|
1209
|
+
name: "Troubled",
|
|
1210
|
+
description: "See an error AND a test failure",
|
|
1211
|
+
icon: "\ud83d\ude2d",
|
|
1212
|
+
check: (e) => e.errors_seen >= 1 && e.tests_failed >= 1,
|
|
1213
|
+
secret: false,
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
id: "disaster_zone",
|
|
1217
|
+
name: "Disaster Zone",
|
|
1218
|
+
description: "See 50 errors AND 50 test failures",
|
|
1219
|
+
icon: "\ud83c\udf0a",
|
|
1220
|
+
check: (e) => e.errors_seen >= 50 && e.tests_failed >= 50,
|
|
1221
|
+
secret: false,
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: "apocalypse_survivor",
|
|
1225
|
+
name: "Apocalypse Survivor",
|
|
1226
|
+
description: "See 500 errors AND 200 test failures",
|
|
1227
|
+
icon: "\ud83d\udc7e",
|
|
1228
|
+
check: (e) => e.errors_seen >= 500 && e.tests_failed >= 200,
|
|
1229
|
+
secret: true,
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "well_rounded",
|
|
1233
|
+
name: "Well Rounded",
|
|
1234
|
+
description: "Pet, rename, and customize your buddy",
|
|
1235
|
+
icon: "\ud83c\udfaf",
|
|
1236
|
+
check: (e) => e.pets >= 1 && e.renames >= 1 && e.personalities_set >= 1,
|
|
1237
|
+
secret: false,
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
id: "renaissance",
|
|
1241
|
+
name: "Renaissance",
|
|
1242
|
+
description: "Use every buddy feature at least once",
|
|
1243
|
+
icon: "\ud83c\udfa8",
|
|
1244
|
+
check: (e) =>
|
|
1245
|
+
e.pets >= 1 && e.renames >= 1 && e.personalities_set >= 1 &&
|
|
1246
|
+
e.mutes >= 1 && e.unmutes >= 1 && e.summons >= 1 &&
|
|
1247
|
+
e.saves >= 1 && e.lists >= 1 && e.helps >= 1 &&
|
|
1248
|
+
e.achievement_views >= 1,
|
|
1249
|
+
secret: true,
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
id: "big_and_broken",
|
|
1253
|
+
name: "Big and Broken",
|
|
1254
|
+
description: "Make a large diff AND see a test failure",
|
|
1255
|
+
icon: "\ud83d\udca5",
|
|
1256
|
+
check: (e) => e.large_diffs >= 1 && e.tests_failed >= 1,
|
|
1257
|
+
secret: false,
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
id: "collector_and_destroyer",
|
|
1261
|
+
name: "Collector & Destroyer",
|
|
1262
|
+
description: "Collect 5 buddies AND dismiss one",
|
|
1263
|
+
icon: "\ud83d\udea8",
|
|
1264
|
+
check: (e) => e.buddies_collected >= 5 && e.dismissals >= 1,
|
|
1265
|
+
secret: false,
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
id: "bug_squasher",
|
|
1269
|
+
name: "Bug Squasher",
|
|
1270
|
+
description: "Resolve a remembered bug from memory",
|
|
1271
|
+
icon: "\ud83d\udc1f",
|
|
1272
|
+
check: (e) => e.bugs_resolved >= 1,
|
|
1273
|
+
secret: false,
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
id: "completionist",
|
|
1277
|
+
name: "Completionist",
|
|
1278
|
+
description: "Unlock every other achievement",
|
|
1279
|
+
icon: "\ud83c\udf08",
|
|
1280
|
+
check: (e) => e.achievements_unlocked >= ACHIEVEMENTS.length - 1,
|
|
1281
|
+
secret: true,
|
|
1282
|
+
},
|
|
1283
|
+
];
|
|
1284
|
+
|
|
1285
|
+
export interface UnlockedAchievement {
|
|
1286
|
+
id: string;
|
|
1287
|
+
unlockedAt: number;
|
|
1288
|
+
slot?: string;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
export function loadUnlocked(): UnlockedAchievement[] {
|
|
1292
|
+
try {
|
|
1293
|
+
return JSON.parse(readFileSync(UNLOCKED_FILE, "utf8"));
|
|
1294
|
+
} catch {
|
|
1295
|
+
return [];
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export function saveUnlocked(unlocked: UnlockedAchievement[]): void {
|
|
1300
|
+
atomicWrite(UNLOCKED_FILE, JSON.stringify(unlocked, null, 2));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
export function checkAndAward(slot?: string): Achievement[] {
|
|
1304
|
+
const e = loadEvents(slot);
|
|
1305
|
+
const unlocked = loadUnlocked();
|
|
1306
|
+
const unlockedIds = new Set(unlocked.map((u) => u.id));
|
|
1307
|
+
|
|
1308
|
+
const newlyUnlocked: Achievement[] = [];
|
|
1309
|
+
|
|
1310
|
+
for (const ach of ACHIEVEMENTS) {
|
|
1311
|
+
if (unlockedIds.has(ach.id)) continue;
|
|
1312
|
+
if (ach.check(e)) {
|
|
1313
|
+
unlocked.push({ id: ach.id, unlockedAt: Date.now(), slot: slot ?? undefined });
|
|
1314
|
+
newlyUnlocked.push(ach);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (newlyUnlocked.length > 0) {
|
|
1319
|
+
saveUnlocked(unlocked);
|
|
1320
|
+
for (const ach of newlyUnlocked) {
|
|
1321
|
+
awardXp("achievement_unlocked", slot);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const global = loadGlobalEvents();
|
|
1326
|
+
if (global.achievements_unlocked !== unlocked.length) {
|
|
1327
|
+
global.achievements_unlocked = unlocked.length;
|
|
1328
|
+
saveGlobalEvents(global);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return newlyUnlocked;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const GOLD = "\x1b[38;2;255;193;7m";
|
|
1335
|
+
const NC = "\x1b[0m";
|
|
1336
|
+
const BOLD = "\x1b[1m";
|
|
1337
|
+
const DIM = "\x1b[2m";
|
|
1338
|
+
|
|
1339
|
+
export function renderAchievementsCard(): string {
|
|
1340
|
+
const unlocked = loadUnlocked();
|
|
1341
|
+
const unlockedIds = new Set(unlocked.map((u) => u.id));
|
|
1342
|
+
|
|
1343
|
+
const W = 40;
|
|
1344
|
+
const hr = "\u2500".repeat(W - 2);
|
|
1345
|
+
const sep = `\u251c${"\u254c".repeat(W - 2)}\u2524`;
|
|
1346
|
+
const lines: string[] = [];
|
|
1347
|
+
|
|
1348
|
+
const total = ACHIEVEMENTS.length;
|
|
1349
|
+
const earned = unlockedIds.size;
|
|
1350
|
+
|
|
1351
|
+
lines.push(`${GOLD}\u256d${hr}\u256e${NC}`);
|
|
1352
|
+
|
|
1353
|
+
const header = "\ud83c\udfc6 ACHIEVEMENTS";
|
|
1354
|
+
lines.push(`${GOLD}\u2502${NC} ${BOLD}${header}${NC}${"".padEnd(W - header.length - 4)}${GOLD}\u2502${NC}`);
|
|
1355
|
+
|
|
1356
|
+
const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
|
|
1357
|
+
const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(20 - barFilled);
|
|
1358
|
+
const barText = `${bar} ${earned}/${total}`;
|
|
1359
|
+
lines.push(`${GOLD}\u2502${NC} ${barText}${"".padEnd(W - barText.length - 4)}${GOLD}\u2502${NC}`);
|
|
1360
|
+
|
|
1361
|
+
lines.push(`${GOLD}${sep}${NC}`);
|
|
1362
|
+
|
|
1363
|
+
for (const ach of ACHIEVEMENTS) {
|
|
1364
|
+
if (ach.secret && !unlockedIds.has(ach.id)) continue;
|
|
1365
|
+
|
|
1366
|
+
const done = unlockedIds.has(ach.id);
|
|
1367
|
+
const status = done ? "\u2705" : "\u2610";
|
|
1368
|
+
const content = ` ${ach.icon}${status} ${ach.name}`;
|
|
1369
|
+
const descContent = ` ${ach.description}`;
|
|
1370
|
+
|
|
1371
|
+
if (done) {
|
|
1372
|
+
lines.push(`${GOLD}\u2502${NC} ${BOLD}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}\u2502${NC}`);
|
|
1373
|
+
} else {
|
|
1374
|
+
lines.push(`${GOLD}\u2502${NC} ${DIM}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}\u2502${NC}`);
|
|
1375
|
+
}
|
|
1376
|
+
lines.push(`${GOLD}\u2502${NC} ${DIM}${descContent}${NC}${"".padEnd(W - descContent.length - 3)}${GOLD}\u2502${NC}`);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (earned > 0 && earned === ACHIEVEMENTS.length) {
|
|
1380
|
+
lines.push(`${GOLD}${sep}${NC}`);
|
|
1381
|
+
const complete = "\u2728 ALL ACHIEVEMENTS UNLOCKED! \u2728";
|
|
1382
|
+
lines.push(`${GOLD}\u2502${NC} ${BOLD}${complete}${NC}${"".padEnd(W - complete.length - 4)}${GOLD}\u2502${NC}`);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
lines.push(`${GOLD}\u2570${hr}\u256f${NC}`);
|
|
1386
|
+
|
|
1387
|
+
return lines.join("\n");
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
export function renderAchievementsCardMarkdown(): string {
|
|
1391
|
+
const unlocked = loadUnlocked();
|
|
1392
|
+
const unlockedIds = new Set(unlocked.map((u) => u.id));
|
|
1393
|
+
const total = ACHIEVEMENTS.length;
|
|
1394
|
+
const earned = unlockedIds.size;
|
|
1395
|
+
|
|
1396
|
+
const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
|
|
1397
|
+
const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(20 - barFilled);
|
|
1398
|
+
|
|
1399
|
+
const parts: string[] = [];
|
|
1400
|
+
parts.push(`### \ud83c\udfc6 Achievements \u2014 ${earned}/${total}`);
|
|
1401
|
+
parts.push("");
|
|
1402
|
+
parts.push(`\`${bar}\``);
|
|
1403
|
+
parts.push("");
|
|
1404
|
+
|
|
1405
|
+
for (const ach of ACHIEVEMENTS) {
|
|
1406
|
+
if (ach.secret && !unlockedIds.has(ach.id)) continue;
|
|
1407
|
+
const done = unlockedIds.has(ach.id);
|
|
1408
|
+
const status = done ? "\u2705" : "\u2610";
|
|
1409
|
+
const line = `${ach.icon}${status} **${ach.name}** \u2014 ${ach.description}`;
|
|
1410
|
+
parts.push(line);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (earned > 0 && earned === ACHIEVEMENTS.length) {
|
|
1414
|
+
parts.push("");
|
|
1415
|
+
parts.push("\u2728 **ALL ACHIEVEMENTS UNLOCKED!** \u2728");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return parts.join("\n");
|
|
1419
|
+
}
|