roguelike-cli 1.3.3 → 1.3.5
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 +52 -11
- package/dist/config/config.js +8 -0
- package/dist/data/dictionaries.js +574 -0
- package/dist/data/loot.js +141 -0
- package/dist/interactive/commands.js +30 -15
- package/dist/interactive/startup.js +1 -1
- package/dist/storage/profile.js +222 -97
- package/package.json +1 -1
- package/src/config/config.ts +8 -0
- package/src/data/dictionaries.ts +647 -0
- package/src/data/loot.ts +177 -0
- package/src/interactive/commands.ts +43 -24
- package/src/interactive/startup.ts +1 -1
- package/src/storage/profile.ts +253 -94
package/src/storage/profile.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
id: string;
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
unlockedAt?: string;
|
|
10
|
-
}
|
|
4
|
+
import { getDictionary, Dictionary } from '../data/dictionaries';
|
|
5
|
+
import { LootItem, rollForLoot, formatLootDrop, formatInventory, calculateInventoryValue } from '../data/loot';
|
|
11
6
|
|
|
12
7
|
export interface UndoEntry {
|
|
13
8
|
path: string;
|
|
@@ -24,35 +19,35 @@ export interface Profile {
|
|
|
24
19
|
currentStreak: number;
|
|
25
20
|
longestStreak: number;
|
|
26
21
|
lastCompletionDate?: string;
|
|
27
|
-
achievements: string[];
|
|
22
|
+
achievements: string[]; // Achievement IDs that were unlocked
|
|
23
|
+
inventory: LootItem[]; // Collected loot items
|
|
28
24
|
undoHistory: UndoEntry[];
|
|
29
25
|
stats: {
|
|
30
26
|
completedByDay: Record<string, number>;
|
|
31
27
|
createdAt: string;
|
|
28
|
+
deepestDepth: number;
|
|
29
|
+
speedruns: number;
|
|
30
|
+
nightOwlTasks: number;
|
|
31
|
+
earlyBirdTasks: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const PROFILE_FILE = path.join(os.homedir(), '.rlc', 'profile.json');
|
|
36
36
|
|
|
37
|
-
// Achievement
|
|
38
|
-
export const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
{ id: 'level_10', name: 'Veteran', description: 'Reach level 10' },
|
|
52
|
-
{ id: 'level_25', name: 'Legend', description: 'Reach level 25' },
|
|
53
|
-
{ id: 'xp_1000', name: 'XP Collector', description: 'Earn 1000 XP' },
|
|
54
|
-
{ id: 'xp_10000', name: 'XP Hoarder', description: 'Earn 10000 XP' },
|
|
55
|
-
];
|
|
37
|
+
// Achievement thresholds (infinite, level-based)
|
|
38
|
+
export const ACHIEVEMENT_THRESHOLDS = {
|
|
39
|
+
tasks: [1, 10, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
|
|
40
|
+
bosses: [1, 5, 10, 25, 50, 100, 250, 500],
|
|
41
|
+
streaks: [3, 7, 14, 30, 60, 90, 180, 365],
|
|
42
|
+
depths: [3, 5, 7, 10, 15, 20],
|
|
43
|
+
levels: [5, 10, 25, 50, 100, 150, 200],
|
|
44
|
+
xp: [1000, 5000, 10000, 50000, 100000, 500000, 1000000],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Map threshold to achievement ID
|
|
48
|
+
function getAchievementId(type: string, threshold: number): string {
|
|
49
|
+
return `${type}_${threshold}`;
|
|
50
|
+
}
|
|
56
51
|
|
|
57
52
|
// XP required for each level (cumulative)
|
|
58
53
|
export function xpForLevel(level: number): number {
|
|
@@ -95,6 +90,10 @@ export function readProfile(): Profile {
|
|
|
95
90
|
return {
|
|
96
91
|
...createDefaultProfile(),
|
|
97
92
|
...profile,
|
|
93
|
+
stats: {
|
|
94
|
+
...createDefaultProfile().stats,
|
|
95
|
+
...profile.stats,
|
|
96
|
+
},
|
|
98
97
|
};
|
|
99
98
|
} catch {
|
|
100
99
|
return createDefaultProfile();
|
|
@@ -122,40 +121,40 @@ function createDefaultProfile(): Profile {
|
|
|
122
121
|
currentStreak: 0,
|
|
123
122
|
longestStreak: 0,
|
|
124
123
|
achievements: [],
|
|
124
|
+
inventory: [],
|
|
125
125
|
undoHistory: [],
|
|
126
126
|
stats: {
|
|
127
127
|
completedByDay: {},
|
|
128
128
|
createdAt: new Date().toISOString(),
|
|
129
|
+
deepestDepth: 0,
|
|
130
|
+
speedruns: 0,
|
|
131
|
+
nightOwlTasks: 0,
|
|
132
|
+
earlyBirdTasks: 0,
|
|
129
133
|
},
|
|
130
134
|
};
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
export
|
|
134
|
-
const profile = readProfile();
|
|
135
|
-
const oldLevel = profile.level;
|
|
136
|
-
|
|
137
|
-
profile.totalXP += amount;
|
|
138
|
-
profile.level = levelFromXP(profile.totalXP);
|
|
139
|
-
|
|
140
|
-
saveProfile(profile);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
newXP: profile.totalXP,
|
|
144
|
-
levelUp: profile.level > oldLevel,
|
|
145
|
-
newLevel: profile.level,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function completeTask(xp: number, isBoss: boolean, depth: number, createdAt: string): {
|
|
137
|
+
export interface CompletionResult {
|
|
150
138
|
xpGained: number;
|
|
151
139
|
levelUp: boolean;
|
|
140
|
+
oldLevel: number;
|
|
152
141
|
newLevel: number;
|
|
153
|
-
newAchievements:
|
|
154
|
-
|
|
142
|
+
newAchievements: string[]; // Achievement IDs
|
|
143
|
+
lootDropped?: LootItem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function completeTask(
|
|
147
|
+
xp: number,
|
|
148
|
+
isBoss: boolean,
|
|
149
|
+
depth: number,
|
|
150
|
+
createdAt: string,
|
|
151
|
+
rulesPreset?: string
|
|
152
|
+
): CompletionResult {
|
|
155
153
|
const profile = readProfile();
|
|
156
154
|
const oldLevel = profile.level;
|
|
157
155
|
const today = new Date().toISOString().split('T')[0];
|
|
158
156
|
const createdDate = createdAt.split('T')[0];
|
|
157
|
+
const hour = new Date().getHours();
|
|
159
158
|
|
|
160
159
|
// Add XP
|
|
161
160
|
profile.totalXP += xp;
|
|
@@ -165,6 +164,18 @@ export function completeTask(xp: number, isBoss: boolean, depth: number, created
|
|
|
165
164
|
profile.bossesDefeated += 1;
|
|
166
165
|
}
|
|
167
166
|
|
|
167
|
+
// Track special completions
|
|
168
|
+
if (createdDate === today) {
|
|
169
|
+
profile.stats.speedruns = (profile.stats.speedruns || 0) + 1;
|
|
170
|
+
}
|
|
171
|
+
if (hour >= 0 && hour < 6) {
|
|
172
|
+
profile.stats.earlyBirdTasks = (profile.stats.earlyBirdTasks || 0) + 1;
|
|
173
|
+
}
|
|
174
|
+
if (hour >= 0 && hour < 5) {
|
|
175
|
+
profile.stats.nightOwlTasks = (profile.stats.nightOwlTasks || 0) + 1;
|
|
176
|
+
}
|
|
177
|
+
profile.stats.deepestDepth = Math.max(profile.stats.deepestDepth || 0, depth);
|
|
178
|
+
|
|
168
179
|
// Update streak
|
|
169
180
|
if (profile.lastCompletionDate) {
|
|
170
181
|
const lastDate = new Date(profile.lastCompletionDate);
|
|
@@ -176,7 +187,6 @@ export function completeTask(xp: number, isBoss: boolean, depth: number, created
|
|
|
176
187
|
} else if (diffDays > 1) {
|
|
177
188
|
profile.currentStreak = 1;
|
|
178
189
|
}
|
|
179
|
-
// Same day - streak continues but doesn't increment
|
|
180
190
|
} else {
|
|
181
191
|
profile.currentStreak = 1;
|
|
182
192
|
}
|
|
@@ -189,84 +199,185 @@ export function completeTask(xp: number, isBoss: boolean, depth: number, created
|
|
|
189
199
|
|
|
190
200
|
// Update level
|
|
191
201
|
profile.level = levelFromXP(profile.totalXP);
|
|
202
|
+
const levelUp = profile.level > oldLevel;
|
|
192
203
|
|
|
193
204
|
// Check for new achievements
|
|
194
|
-
const newAchievements:
|
|
205
|
+
const newAchievements: string[] = [];
|
|
195
206
|
|
|
196
207
|
const checkAchievement = (id: string) => {
|
|
197
208
|
if (!profile.achievements.includes(id)) {
|
|
198
209
|
profile.achievements.push(id);
|
|
199
|
-
|
|
200
|
-
if (achievement) newAchievements.push(achievement);
|
|
210
|
+
newAchievements.push(id);
|
|
201
211
|
}
|
|
202
212
|
};
|
|
203
213
|
|
|
204
|
-
// Task count achievements
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
214
|
+
// Task count achievements (infinite)
|
|
215
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.tasks) {
|
|
216
|
+
if (profile.tasksCompleted >= threshold) {
|
|
217
|
+
checkAchievement(getAchievementId('tasks', threshold));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Boss achievements (infinite)
|
|
222
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.bosses) {
|
|
223
|
+
if (profile.bossesDefeated >= threshold) {
|
|
224
|
+
checkAchievement(getAchievementId('bosses', threshold));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Depth achievements (infinite)
|
|
229
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.depths) {
|
|
230
|
+
if (profile.stats.deepestDepth >= threshold) {
|
|
231
|
+
checkAchievement(getAchievementId('depth', threshold));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Streak achievements (infinite)
|
|
236
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.streaks) {
|
|
237
|
+
if (profile.currentStreak >= threshold) {
|
|
238
|
+
checkAchievement(getAchievementId('streak', threshold));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Level achievements (infinite)
|
|
243
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.levels) {
|
|
244
|
+
if (profile.level >= threshold) {
|
|
245
|
+
checkAchievement(getAchievementId('level', threshold));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
209
248
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
// XP achievements (infinite)
|
|
250
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.xp) {
|
|
251
|
+
if (profile.totalXP >= threshold) {
|
|
252
|
+
checkAchievement(getAchievementId('xp', threshold));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
213
255
|
|
|
214
|
-
//
|
|
215
|
-
if (
|
|
256
|
+
// Special achievements
|
|
257
|
+
if (createdDate === today) {
|
|
258
|
+
checkAchievement('speedrun');
|
|
259
|
+
}
|
|
260
|
+
if (hour >= 0 && hour < 5) {
|
|
261
|
+
checkAchievement('nightowl');
|
|
262
|
+
}
|
|
263
|
+
if (hour >= 5 && hour < 7) {
|
|
264
|
+
checkAchievement('earlybird');
|
|
265
|
+
}
|
|
216
266
|
|
|
217
|
-
//
|
|
218
|
-
|
|
267
|
+
// Roll for loot
|
|
268
|
+
let lootDropped: LootItem | undefined;
|
|
219
269
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
270
|
+
// Roll from task completion
|
|
271
|
+
const taskLoot = rollForLoot(profile.level, isBoss ? 'boss' : 'task', rulesPreset);
|
|
272
|
+
if (taskLoot.dropped && taskLoot.item) {
|
|
273
|
+
lootDropped = taskLoot.item;
|
|
274
|
+
profile.inventory = profile.inventory || [];
|
|
275
|
+
profile.inventory.push(taskLoot.item);
|
|
276
|
+
}
|
|
224
277
|
|
|
225
|
-
//
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
278
|
+
// Additional roll on level up
|
|
279
|
+
if (levelUp) {
|
|
280
|
+
const levelLoot = rollForLoot(profile.level, 'levelup', rulesPreset);
|
|
281
|
+
if (levelLoot.dropped && levelLoot.item) {
|
|
282
|
+
if (!lootDropped) {
|
|
283
|
+
lootDropped = levelLoot.item;
|
|
284
|
+
}
|
|
285
|
+
profile.inventory.push(levelLoot.item);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
229
288
|
|
|
230
|
-
//
|
|
231
|
-
if (
|
|
232
|
-
|
|
289
|
+
// Additional roll on new achievements
|
|
290
|
+
if (newAchievements.length > 0) {
|
|
291
|
+
const achievementLoot = rollForLoot(profile.level, 'achievement', rulesPreset);
|
|
292
|
+
if (achievementLoot.dropped && achievementLoot.item) {
|
|
293
|
+
if (!lootDropped) {
|
|
294
|
+
lootDropped = achievementLoot.item;
|
|
295
|
+
}
|
|
296
|
+
profile.inventory.push(achievementLoot.item);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
233
299
|
|
|
234
300
|
saveProfile(profile);
|
|
235
301
|
|
|
236
302
|
return {
|
|
237
303
|
xpGained: xp,
|
|
238
|
-
levelUp
|
|
304
|
+
levelUp,
|
|
305
|
+
oldLevel,
|
|
239
306
|
newLevel: profile.level,
|
|
240
307
|
newAchievements,
|
|
308
|
+
lootDropped,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get achievement display info from dictionary
|
|
313
|
+
export function getAchievementInfo(achievementId: string, dict: Dictionary): { name: string; desc: string } | null {
|
|
314
|
+
// Parse achievement ID
|
|
315
|
+
const parts = achievementId.split('_');
|
|
316
|
+
const type = parts[0];
|
|
317
|
+
const threshold = parseInt(parts[1]);
|
|
318
|
+
|
|
319
|
+
// Map to dictionary keys
|
|
320
|
+
const keyMap: Record<string, Record<number, string>> = {
|
|
321
|
+
tasks: { 1: 'firstTask', 10: 'tasks10', 50: 'tasks50', 100: 'tasks100', 500: 'tasks500', 1000: 'tasks1000' },
|
|
322
|
+
bosses: { 1: 'boss1', 5: 'boss5', 10: 'boss10', 25: 'boss25' },
|
|
323
|
+
streak: { 3: 'streak3', 7: 'streak7', 14: 'streak14', 30: 'streak30' },
|
|
324
|
+
depth: { 3: 'depth3', 5: 'depth5', 10: 'depth10' },
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Special achievements
|
|
328
|
+
if (achievementId === 'speedrun') {
|
|
329
|
+
return dict.achievements.speedrun;
|
|
330
|
+
}
|
|
331
|
+
if (achievementId === 'nightowl') {
|
|
332
|
+
return dict.achievements.nightOwl;
|
|
333
|
+
}
|
|
334
|
+
if (achievementId === 'earlybird') {
|
|
335
|
+
return dict.achievements.earlyBird;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Get from map
|
|
339
|
+
if (keyMap[type] && keyMap[type][threshold]) {
|
|
340
|
+
const key = keyMap[type][threshold] as keyof Dictionary['achievements'];
|
|
341
|
+
return dict.achievements[key];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Generate for achievements beyond dictionary
|
|
345
|
+
return {
|
|
346
|
+
name: `${type.charAt(0).toUpperCase() + type.slice(1)} ${threshold}`,
|
|
347
|
+
desc: `Reach ${threshold} ${type}`,
|
|
241
348
|
};
|
|
242
349
|
}
|
|
243
350
|
|
|
244
|
-
export function formatStats(): string {
|
|
351
|
+
export function formatStats(rulesPreset?: string): string {
|
|
245
352
|
const profile = readProfile();
|
|
353
|
+
const dict = getDictionary(rulesPreset);
|
|
246
354
|
const nextLevel = xpToNextLevel(profile.totalXP);
|
|
247
355
|
|
|
248
356
|
const lines: string[] = [
|
|
249
357
|
'',
|
|
250
358
|
'=== PLAYER STATS ===',
|
|
251
359
|
'',
|
|
252
|
-
|
|
253
|
-
|
|
360
|
+
`${dict.stats.level}: ${profile.level}`,
|
|
361
|
+
`${dict.stats.xp}: ${profile.totalXP} (${nextLevel.current}/${nextLevel.required} to next)`,
|
|
254
362
|
`Progress: [${'#'.repeat(Math.floor(nextLevel.progress / 5))}${'.'.repeat(20 - Math.floor(nextLevel.progress / 5))}] ${nextLevel.progress}%`,
|
|
255
363
|
'',
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
364
|
+
`${dict.stats.tasksCompleted}: ${profile.tasksCompleted}`,
|
|
365
|
+
`${dict.stats.bossesDefeated}: ${profile.bossesDefeated}`,
|
|
366
|
+
`${dict.stats.currentStreak}: ${profile.currentStreak} days`,
|
|
367
|
+
`${dict.stats.longestStreak}: ${profile.longestStreak} days`,
|
|
260
368
|
'',
|
|
261
|
-
`Achievements: ${profile.achievements.length}
|
|
369
|
+
`Achievements: ${profile.achievements.length}`,
|
|
370
|
+
`${dict.stats.inventory}: ${(profile.inventory || []).length} items`,
|
|
371
|
+
`Inventory Value: ${calculateInventoryValue(profile.inventory || [])}`,
|
|
262
372
|
'',
|
|
263
373
|
];
|
|
264
374
|
|
|
265
375
|
return lines.join('\n');
|
|
266
376
|
}
|
|
267
377
|
|
|
268
|
-
export function formatAchievements(): string {
|
|
378
|
+
export function formatAchievements(rulesPreset?: string): string {
|
|
269
379
|
const profile = readProfile();
|
|
380
|
+
const dict = getDictionary(rulesPreset);
|
|
270
381
|
|
|
271
382
|
const lines: string[] = [
|
|
272
383
|
'',
|
|
@@ -274,24 +385,76 @@ export function formatAchievements(): string {
|
|
|
274
385
|
'',
|
|
275
386
|
];
|
|
276
387
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
lines.push(
|
|
388
|
+
// Show unlocked achievements
|
|
389
|
+
if (profile.achievements.length === 0) {
|
|
390
|
+
lines.push('No achievements yet. Complete tasks to unlock!');
|
|
391
|
+
} else {
|
|
392
|
+
lines.push('Unlocked:');
|
|
393
|
+
for (const id of profile.achievements) {
|
|
394
|
+
const info = getAchievementInfo(id, dict);
|
|
395
|
+
if (info) {
|
|
396
|
+
lines.push(`[x] ${info.name}`);
|
|
397
|
+
lines.push(` ${info.desc}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Show next achievements to unlock
|
|
403
|
+
lines.push('');
|
|
404
|
+
lines.push('Next to unlock:');
|
|
405
|
+
|
|
406
|
+
// Find next task achievement
|
|
407
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.tasks) {
|
|
408
|
+
const id = getAchievementId('tasks', threshold);
|
|
409
|
+
if (!profile.achievements.includes(id)) {
|
|
410
|
+
const info = getAchievementInfo(id, dict);
|
|
411
|
+
if (info) {
|
|
412
|
+
lines.push(`[ ] ${info.name} (${profile.tasksCompleted}/${threshold})`);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Find next boss achievement
|
|
419
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.bosses) {
|
|
420
|
+
const id = getAchievementId('bosses', threshold);
|
|
421
|
+
if (!profile.achievements.includes(id)) {
|
|
422
|
+
const info = getAchievementInfo(id, dict);
|
|
423
|
+
if (info) {
|
|
424
|
+
lines.push(`[ ] ${info.name} (${profile.bossesDefeated}/${threshold})`);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Find next streak achievement
|
|
431
|
+
for (const threshold of ACHIEVEMENT_THRESHOLDS.streaks) {
|
|
432
|
+
const id = getAchievementId('streak', threshold);
|
|
433
|
+
if (!profile.achievements.includes(id)) {
|
|
434
|
+
const info = getAchievementInfo(id, dict);
|
|
435
|
+
if (info) {
|
|
436
|
+
lines.push(`[ ] ${info.name} (${profile.currentStreak}/${threshold})`);
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
282
440
|
}
|
|
283
441
|
|
|
284
442
|
lines.push('');
|
|
285
|
-
lines.push(`
|
|
443
|
+
lines.push(`Total unlocked: ${profile.achievements.length}`);
|
|
286
444
|
lines.push('');
|
|
287
445
|
|
|
288
446
|
return lines.join('\n');
|
|
289
447
|
}
|
|
290
448
|
|
|
449
|
+
export function formatInventoryDisplay(rulesPreset?: string): string {
|
|
450
|
+
const profile = readProfile();
|
|
451
|
+
const dict = getDictionary(rulesPreset);
|
|
452
|
+
return formatInventory(profile.inventory || [], dict);
|
|
453
|
+
}
|
|
454
|
+
|
|
291
455
|
export function addToUndoHistory(entry: UndoEntry): void {
|
|
292
456
|
const profile = readProfile();
|
|
293
457
|
|
|
294
|
-
// Keep only last 10 undo entries
|
|
295
458
|
profile.undoHistory = profile.undoHistory || [];
|
|
296
459
|
profile.undoHistory.unshift(entry);
|
|
297
460
|
if (profile.undoHistory.length > 10) {
|
|
@@ -318,7 +481,6 @@ export function performUndo(): { success: boolean; entry: UndoEntry | null; mess
|
|
|
318
481
|
|
|
319
482
|
const entry = profile.undoHistory.shift()!;
|
|
320
483
|
|
|
321
|
-
// Subtract XP
|
|
322
484
|
profile.totalXP = Math.max(0, profile.totalXP - entry.xpLost);
|
|
323
485
|
profile.tasksCompleted = Math.max(0, profile.tasksCompleted - 1);
|
|
324
486
|
|
|
@@ -326,10 +488,8 @@ export function performUndo(): { success: boolean; entry: UndoEntry | null; mess
|
|
|
326
488
|
profile.bossesDefeated = Math.max(0, profile.bossesDefeated - 1);
|
|
327
489
|
}
|
|
328
490
|
|
|
329
|
-
// Update level
|
|
330
491
|
profile.level = levelFromXP(profile.totalXP);
|
|
331
492
|
|
|
332
|
-
// Update daily stats
|
|
333
493
|
const today = new Date().toISOString().split('T')[0];
|
|
334
494
|
if (profile.stats.completedByDay[today]) {
|
|
335
495
|
profile.stats.completedByDay[today] = Math.max(0, profile.stats.completedByDay[today] - 1);
|
|
@@ -343,4 +503,3 @@ export function performUndo(): { success: boolean; entry: UndoEntry | null; mess
|
|
|
343
503
|
message: `Undo: -${entry.xpLost} XP`
|
|
344
504
|
};
|
|
345
505
|
}
|
|
346
|
-
|