roguelike-cli 1.2.6 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -208
- package/dist/ai/claude.js +53 -42
- package/dist/interactive/commands.js +383 -181
- package/dist/interactive/startup.js +6 -3
- package/dist/storage/nodeConfig.js +35 -1
- package/dist/storage/profile.js +276 -0
- package/package.json +1 -1
- package/src/ai/claude.ts +60 -43
- package/src/interactive/commands.ts +470 -187
- package/src/interactive/startup.ts +6 -3
- package/src/storage/nodeConfig.ts +48 -2
- package/src/storage/profile.ts +281 -0
|
@@ -17,11 +17,14 @@ const ASCII_ART = [
|
|
|
17
17
|
'║ Roguelike CLI ║',
|
|
18
18
|
'╚═════════════════════════╝',
|
|
19
19
|
'',
|
|
20
|
-
'
|
|
21
|
-
'
|
|
20
|
+
' Navigation: ls, cd, tree, pwd, open',
|
|
21
|
+
' Tasks: done, deadline, boss, block, status',
|
|
22
|
+
' Gamification: stats, achievements, map',
|
|
22
23
|
'',
|
|
24
|
+
' TAB to autocomplete, | pbcopy to copy',
|
|
23
25
|
' Workflow: <description> -> refine -> save',
|
|
24
|
-
'
|
|
26
|
+
'',
|
|
27
|
+
' help - all commands, init - setup',
|
|
25
28
|
'',
|
|
26
29
|
' www.rlc.rocks',
|
|
27
30
|
'',
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
|
|
4
|
+
export type NodeStatus = 'open' | 'done' | 'blocked';
|
|
5
|
+
|
|
4
6
|
export interface NodeConfig {
|
|
5
7
|
name: string;
|
|
8
|
+
status: NodeStatus;
|
|
6
9
|
deadline?: string;
|
|
10
|
+
completedAt?: string;
|
|
11
|
+
xp: number;
|
|
12
|
+
isBoss?: boolean;
|
|
13
|
+
blockedBy?: string[];
|
|
7
14
|
branch?: string;
|
|
8
15
|
zone?: string;
|
|
9
16
|
description?: string;
|
|
@@ -23,13 +30,19 @@ export function readNodeConfig(nodePath: string): NodeConfig | null {
|
|
|
23
30
|
|
|
24
31
|
try {
|
|
25
32
|
const data = fs.readFileSync(configPath, 'utf-8');
|
|
26
|
-
|
|
33
|
+
const config = JSON.parse(data);
|
|
34
|
+
// Ensure defaults for older configs
|
|
35
|
+
return {
|
|
36
|
+
status: 'open',
|
|
37
|
+
xp: 10,
|
|
38
|
+
...config,
|
|
39
|
+
};
|
|
27
40
|
} catch {
|
|
28
41
|
return null;
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
export function writeNodeConfig(nodePath: string, config: NodeConfig): void {
|
|
45
|
+
export function writeNodeConfig(nodePath: string, config: Partial<NodeConfig>): void {
|
|
33
46
|
if (!fs.existsSync(nodePath)) {
|
|
34
47
|
fs.mkdirSync(nodePath, { recursive: true });
|
|
35
48
|
}
|
|
@@ -38,6 +51,9 @@ export function writeNodeConfig(nodePath: string, config: NodeConfig): void {
|
|
|
38
51
|
const existing = readNodeConfig(nodePath);
|
|
39
52
|
|
|
40
53
|
const updated: NodeConfig = {
|
|
54
|
+
name: config.name || existing?.name || path.basename(nodePath),
|
|
55
|
+
status: config.status ?? existing?.status ?? 'open',
|
|
56
|
+
xp: config.xp ?? existing?.xp ?? 10,
|
|
41
57
|
...existing,
|
|
42
58
|
...config,
|
|
43
59
|
updatedAt: new Date().toISOString(),
|
|
@@ -55,6 +71,8 @@ export function createNode(
|
|
|
55
71
|
branch?: string;
|
|
56
72
|
zone?: string;
|
|
57
73
|
description?: string;
|
|
74
|
+
isBoss?: boolean;
|
|
75
|
+
xp?: number;
|
|
58
76
|
metadata?: Record<string, any>;
|
|
59
77
|
}
|
|
60
78
|
): string {
|
|
@@ -67,7 +85,10 @@ export function createNode(
|
|
|
67
85
|
|
|
68
86
|
const config: NodeConfig = {
|
|
69
87
|
name,
|
|
88
|
+
status: 'open',
|
|
89
|
+
xp: options?.xp || 10,
|
|
70
90
|
deadline: options?.deadline,
|
|
91
|
+
isBoss: options?.isBoss,
|
|
71
92
|
branch: options?.branch,
|
|
72
93
|
zone: options?.zone,
|
|
73
94
|
description: options?.description,
|
|
@@ -81,6 +102,14 @@ export function createNode(
|
|
|
81
102
|
return nodePath;
|
|
82
103
|
}
|
|
83
104
|
|
|
105
|
+
// Calculate XP based on depth (deeper = more XP)
|
|
106
|
+
export function calculateXP(depth: number, isBoss: boolean = false): number {
|
|
107
|
+
const baseXP = 10;
|
|
108
|
+
const depthBonus = depth * 5;
|
|
109
|
+
const bossMultiplier = isBoss ? 3 : 1;
|
|
110
|
+
return (baseXP + depthBonus) * bossMultiplier;
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
// Save schema content to .rlc.schema file
|
|
85
114
|
export function saveSchemaFile(dirPath: string, filename: string, content: string): string {
|
|
86
115
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -98,6 +127,23 @@ export function saveSchemaFile(dirPath: string, filename: string, content: strin
|
|
|
98
127
|
return schemaPath;
|
|
99
128
|
}
|
|
100
129
|
|
|
130
|
+
// Save map content to .rlc.map file
|
|
131
|
+
export function saveMapFile(dirPath: string, filename: string, content: string): string {
|
|
132
|
+
if (!fs.existsSync(dirPath)) {
|
|
133
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const safeName = filename
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
139
|
+
.replace(/^-+|-+$/g, '');
|
|
140
|
+
|
|
141
|
+
const mapPath = path.join(dirPath, `${safeName}.rlc.map`);
|
|
142
|
+
fs.writeFileSync(mapPath, content, 'utf-8');
|
|
143
|
+
|
|
144
|
+
return mapPath;
|
|
145
|
+
}
|
|
146
|
+
|
|
101
147
|
// Read schema file
|
|
102
148
|
export function readSchemaFile(filePath: string): string | null {
|
|
103
149
|
if (!fs.existsSync(filePath)) {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface Achievement {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
unlockedAt?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Profile {
|
|
13
|
+
totalXP: number;
|
|
14
|
+
level: number;
|
|
15
|
+
tasksCompleted: number;
|
|
16
|
+
bossesDefeated: number;
|
|
17
|
+
currentStreak: number;
|
|
18
|
+
longestStreak: number;
|
|
19
|
+
lastCompletionDate?: string;
|
|
20
|
+
achievements: string[];
|
|
21
|
+
stats: {
|
|
22
|
+
completedByDay: Record<string, number>;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PROFILE_FILE = path.join(os.homedir(), '.rlc', 'profile.json');
|
|
28
|
+
|
|
29
|
+
// Achievement definitions
|
|
30
|
+
export const ACHIEVEMENTS: Achievement[] = [
|
|
31
|
+
{ id: 'first_blood', name: 'First Blood', description: 'Complete your first task' },
|
|
32
|
+
{ id: 'ten_tasks', name: 'Getting Started', description: 'Complete 10 tasks' },
|
|
33
|
+
{ id: 'fifty_tasks', name: 'Productive', description: 'Complete 50 tasks' },
|
|
34
|
+
{ id: 'hundred_tasks', name: 'Centurion', description: 'Complete 100 tasks' },
|
|
35
|
+
{ id: 'deep_nesting', name: 'Deep Diver', description: 'Complete a task at depth 5+' },
|
|
36
|
+
{ id: 'boss_slayer', name: 'Boss Slayer', description: 'Complete a boss task' },
|
|
37
|
+
{ id: 'five_bosses', name: 'Boss Hunter', description: 'Defeat 5 bosses' },
|
|
38
|
+
{ id: 'speedrunner', name: 'Speedrunner', description: 'Complete a task on the same day it was created' },
|
|
39
|
+
{ id: 'streak_3', name: 'On a Roll', description: '3 day completion streak' },
|
|
40
|
+
{ id: 'streak_7', name: 'Streak Master', description: '7 day completion streak' },
|
|
41
|
+
{ id: 'streak_30', name: 'Unstoppable', description: '30 day completion streak' },
|
|
42
|
+
{ id: 'level_5', name: 'Adventurer', description: 'Reach level 5' },
|
|
43
|
+
{ id: 'level_10', name: 'Veteran', description: 'Reach level 10' },
|
|
44
|
+
{ id: 'level_25', name: 'Legend', description: 'Reach level 25' },
|
|
45
|
+
{ id: 'xp_1000', name: 'XP Collector', description: 'Earn 1000 XP' },
|
|
46
|
+
{ id: 'xp_10000', name: 'XP Hoarder', description: 'Earn 10000 XP' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// XP required for each level (cumulative)
|
|
50
|
+
export function xpForLevel(level: number): number {
|
|
51
|
+
return Math.floor(100 * Math.pow(1.5, level - 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function levelFromXP(xp: number): number {
|
|
55
|
+
let level = 1;
|
|
56
|
+
let totalRequired = 0;
|
|
57
|
+
while (totalRequired + xpForLevel(level) <= xp) {
|
|
58
|
+
totalRequired += xpForLevel(level);
|
|
59
|
+
level++;
|
|
60
|
+
}
|
|
61
|
+
return level;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function xpToNextLevel(xp: number): { current: number; required: number; progress: number } {
|
|
65
|
+
const level = levelFromXP(xp);
|
|
66
|
+
let totalForCurrentLevel = 0;
|
|
67
|
+
for (let i = 1; i < level; i++) {
|
|
68
|
+
totalForCurrentLevel += xpForLevel(i);
|
|
69
|
+
}
|
|
70
|
+
const xpInCurrentLevel = xp - totalForCurrentLevel;
|
|
71
|
+
const required = xpForLevel(level);
|
|
72
|
+
return {
|
|
73
|
+
current: xpInCurrentLevel,
|
|
74
|
+
required,
|
|
75
|
+
progress: Math.floor((xpInCurrentLevel / required) * 100),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readProfile(): Profile {
|
|
80
|
+
if (!fs.existsSync(PROFILE_FILE)) {
|
|
81
|
+
return createDefaultProfile();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const data = fs.readFileSync(PROFILE_FILE, 'utf-8');
|
|
86
|
+
const profile = JSON.parse(data);
|
|
87
|
+
return {
|
|
88
|
+
...createDefaultProfile(),
|
|
89
|
+
...profile,
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return createDefaultProfile();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function saveProfile(profile: Profile): void {
|
|
97
|
+
const dir = path.dirname(PROFILE_FILE);
|
|
98
|
+
if (!fs.existsSync(dir)) {
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Update level based on XP
|
|
103
|
+
profile.level = levelFromXP(profile.totalXP);
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2), 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createDefaultProfile(): Profile {
|
|
109
|
+
return {
|
|
110
|
+
totalXP: 0,
|
|
111
|
+
level: 1,
|
|
112
|
+
tasksCompleted: 0,
|
|
113
|
+
bossesDefeated: 0,
|
|
114
|
+
currentStreak: 0,
|
|
115
|
+
longestStreak: 0,
|
|
116
|
+
achievements: [],
|
|
117
|
+
stats: {
|
|
118
|
+
completedByDay: {},
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function addXP(amount: number): { newXP: number; levelUp: boolean; newLevel: number } {
|
|
125
|
+
const profile = readProfile();
|
|
126
|
+
const oldLevel = profile.level;
|
|
127
|
+
|
|
128
|
+
profile.totalXP += amount;
|
|
129
|
+
profile.level = levelFromXP(profile.totalXP);
|
|
130
|
+
|
|
131
|
+
saveProfile(profile);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
newXP: profile.totalXP,
|
|
135
|
+
levelUp: profile.level > oldLevel,
|
|
136
|
+
newLevel: profile.level,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function completeTask(xp: number, isBoss: boolean, depth: number, createdAt: string): {
|
|
141
|
+
xpGained: number;
|
|
142
|
+
levelUp: boolean;
|
|
143
|
+
newLevel: number;
|
|
144
|
+
newAchievements: Achievement[];
|
|
145
|
+
} {
|
|
146
|
+
const profile = readProfile();
|
|
147
|
+
const oldLevel = profile.level;
|
|
148
|
+
const today = new Date().toISOString().split('T')[0];
|
|
149
|
+
const createdDate = createdAt.split('T')[0];
|
|
150
|
+
|
|
151
|
+
// Add XP
|
|
152
|
+
profile.totalXP += xp;
|
|
153
|
+
profile.tasksCompleted += 1;
|
|
154
|
+
|
|
155
|
+
if (isBoss) {
|
|
156
|
+
profile.bossesDefeated += 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update streak
|
|
160
|
+
if (profile.lastCompletionDate) {
|
|
161
|
+
const lastDate = new Date(profile.lastCompletionDate);
|
|
162
|
+
const todayDate = new Date(today);
|
|
163
|
+
const diffDays = Math.floor((todayDate.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
164
|
+
|
|
165
|
+
if (diffDays === 1) {
|
|
166
|
+
profile.currentStreak += 1;
|
|
167
|
+
} else if (diffDays > 1) {
|
|
168
|
+
profile.currentStreak = 1;
|
|
169
|
+
}
|
|
170
|
+
// Same day - streak continues but doesn't increment
|
|
171
|
+
} else {
|
|
172
|
+
profile.currentStreak = 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
profile.longestStreak = Math.max(profile.longestStreak, profile.currentStreak);
|
|
176
|
+
profile.lastCompletionDate = today;
|
|
177
|
+
|
|
178
|
+
// Update daily stats
|
|
179
|
+
profile.stats.completedByDay[today] = (profile.stats.completedByDay[today] || 0) + 1;
|
|
180
|
+
|
|
181
|
+
// Update level
|
|
182
|
+
profile.level = levelFromXP(profile.totalXP);
|
|
183
|
+
|
|
184
|
+
// Check for new achievements
|
|
185
|
+
const newAchievements: Achievement[] = [];
|
|
186
|
+
|
|
187
|
+
const checkAchievement = (id: string) => {
|
|
188
|
+
if (!profile.achievements.includes(id)) {
|
|
189
|
+
profile.achievements.push(id);
|
|
190
|
+
const achievement = ACHIEVEMENTS.find(a => a.id === id);
|
|
191
|
+
if (achievement) newAchievements.push(achievement);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Task count achievements
|
|
196
|
+
if (profile.tasksCompleted >= 1) checkAchievement('first_blood');
|
|
197
|
+
if (profile.tasksCompleted >= 10) checkAchievement('ten_tasks');
|
|
198
|
+
if (profile.tasksCompleted >= 50) checkAchievement('fifty_tasks');
|
|
199
|
+
if (profile.tasksCompleted >= 100) checkAchievement('hundred_tasks');
|
|
200
|
+
|
|
201
|
+
// Boss achievements
|
|
202
|
+
if (isBoss) checkAchievement('boss_slayer');
|
|
203
|
+
if (profile.bossesDefeated >= 5) checkAchievement('five_bosses');
|
|
204
|
+
|
|
205
|
+
// Depth achievement
|
|
206
|
+
if (depth >= 5) checkAchievement('deep_nesting');
|
|
207
|
+
|
|
208
|
+
// Speedrunner achievement
|
|
209
|
+
if (createdDate === today) checkAchievement('speedrunner');
|
|
210
|
+
|
|
211
|
+
// Streak achievements
|
|
212
|
+
if (profile.currentStreak >= 3) checkAchievement('streak_3');
|
|
213
|
+
if (profile.currentStreak >= 7) checkAchievement('streak_7');
|
|
214
|
+
if (profile.currentStreak >= 30) checkAchievement('streak_30');
|
|
215
|
+
|
|
216
|
+
// Level achievements
|
|
217
|
+
if (profile.level >= 5) checkAchievement('level_5');
|
|
218
|
+
if (profile.level >= 10) checkAchievement('level_10');
|
|
219
|
+
if (profile.level >= 25) checkAchievement('level_25');
|
|
220
|
+
|
|
221
|
+
// XP achievements
|
|
222
|
+
if (profile.totalXP >= 1000) checkAchievement('xp_1000');
|
|
223
|
+
if (profile.totalXP >= 10000) checkAchievement('xp_10000');
|
|
224
|
+
|
|
225
|
+
saveProfile(profile);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
xpGained: xp,
|
|
229
|
+
levelUp: profile.level > oldLevel,
|
|
230
|
+
newLevel: profile.level,
|
|
231
|
+
newAchievements,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatStats(): string {
|
|
236
|
+
const profile = readProfile();
|
|
237
|
+
const nextLevel = xpToNextLevel(profile.totalXP);
|
|
238
|
+
|
|
239
|
+
const lines: string[] = [
|
|
240
|
+
'',
|
|
241
|
+
'=== PLAYER STATS ===',
|
|
242
|
+
'',
|
|
243
|
+
`Level: ${profile.level}`,
|
|
244
|
+
`XP: ${profile.totalXP} (${nextLevel.current}/${nextLevel.required} to next level)`,
|
|
245
|
+
`Progress: [${'#'.repeat(Math.floor(nextLevel.progress / 5))}${'.'.repeat(20 - Math.floor(nextLevel.progress / 5))}] ${nextLevel.progress}%`,
|
|
246
|
+
'',
|
|
247
|
+
`Tasks Completed: ${profile.tasksCompleted}`,
|
|
248
|
+
`Bosses Defeated: ${profile.bossesDefeated}`,
|
|
249
|
+
`Current Streak: ${profile.currentStreak} days`,
|
|
250
|
+
`Longest Streak: ${profile.longestStreak} days`,
|
|
251
|
+
'',
|
|
252
|
+
`Achievements: ${profile.achievements.length}/${ACHIEVEMENTS.length}`,
|
|
253
|
+
'',
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
return lines.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function formatAchievements(): string {
|
|
260
|
+
const profile = readProfile();
|
|
261
|
+
|
|
262
|
+
const lines: string[] = [
|
|
263
|
+
'',
|
|
264
|
+
'=== ACHIEVEMENTS ===',
|
|
265
|
+
'',
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
for (const achievement of ACHIEVEMENTS) {
|
|
269
|
+
const unlocked = profile.achievements.includes(achievement.id);
|
|
270
|
+
const status = unlocked ? '[x]' : '[ ]';
|
|
271
|
+
lines.push(`${status} ${achievement.name}`);
|
|
272
|
+
lines.push(` ${achievement.description}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push(`Unlocked: ${profile.achievements.length}/${ACHIEVEMENTS.length}`);
|
|
277
|
+
lines.push('');
|
|
278
|
+
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|