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.
@@ -17,11 +17,14 @@ const ASCII_ART = [
17
17
  '║ Roguelike CLI ║',
18
18
  '╚═════════════════════════╝',
19
19
  '',
20
- ' Commands: ls, cd, mkdir, open, cp, mv, rm, tree, pwd, clean',
21
- ' TAB to autocomplete, | pbcopy to copy output',
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
- ' init - setup, config - settings, help - examples',
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
- return JSON.parse(data);
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
+