roguelike-cli 1.3.2 → 1.3.4

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.
@@ -0,0 +1,177 @@
1
+ import { getDictionary, Dictionary } from './dictionaries';
2
+
3
+ export type Rarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
4
+
5
+ export interface LootItem {
6
+ name: string;
7
+ rarity: Rarity;
8
+ droppedAt: string;
9
+ source: 'task' | 'levelup' | 'achievement' | 'boss';
10
+ }
11
+
12
+ export interface LootDropResult {
13
+ dropped: boolean;
14
+ item?: LootItem;
15
+ }
16
+
17
+ // Rarity drop chances by player level
18
+ // Higher level = higher chance for rare items, but overall drop rate decreases
19
+ function getDropChances(level: number): { dropRate: number; rarityWeights: Record<Rarity, number> } {
20
+ // Base drop rate decreases with level (starts at 30%, min 5%)
21
+ const baseDropRate = Math.max(0.05, 0.30 - (level * 0.01));
22
+
23
+ // Rarity weights shift toward rarer items at higher levels
24
+ const legendaryWeight = Math.min(0.05, level * 0.002);
25
+ const epicWeight = Math.min(0.10, level * 0.004);
26
+ const rareWeight = Math.min(0.20, 0.05 + level * 0.005);
27
+ const uncommonWeight = Math.min(0.30, 0.15 + level * 0.005);
28
+ const commonWeight = 1 - legendaryWeight - epicWeight - rareWeight - uncommonWeight;
29
+
30
+ return {
31
+ dropRate: baseDropRate,
32
+ rarityWeights: {
33
+ common: commonWeight,
34
+ uncommon: uncommonWeight,
35
+ rare: rareWeight,
36
+ epic: epicWeight,
37
+ legendary: legendaryWeight,
38
+ },
39
+ };
40
+ }
41
+
42
+ // Bonus multipliers for different sources
43
+ const SOURCE_MULTIPLIERS: Record<string, number> = {
44
+ task: 1.0,
45
+ levelup: 3.0, // 3x chance on level up
46
+ achievement: 2.5, // 2.5x chance on achievement
47
+ boss: 2.0, // 2x chance on boss
48
+ };
49
+
50
+ // Select random rarity based on weights
51
+ function selectRarity(weights: Record<Rarity, number>): Rarity {
52
+ const roll = Math.random();
53
+ let cumulative = 0;
54
+
55
+ const rarities: Rarity[] = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
56
+ for (const rarity of rarities) {
57
+ cumulative += weights[rarity];
58
+ if (roll < cumulative) {
59
+ return rarity;
60
+ }
61
+ }
62
+
63
+ return 'common';
64
+ }
65
+
66
+ // Select random item from rarity pool
67
+ function selectItem(rarity: Rarity, dict: Dictionary): string {
68
+ const pool = dict.loot[rarity];
69
+ return pool[Math.floor(Math.random() * pool.length)];
70
+ }
71
+
72
+ // Main function to roll for loot
73
+ export function rollForLoot(
74
+ level: number,
75
+ source: 'task' | 'levelup' | 'achievement' | 'boss',
76
+ rulesPreset?: string
77
+ ): LootDropResult {
78
+ const dict = getDictionary(rulesPreset);
79
+ const { dropRate, rarityWeights } = getDropChances(level);
80
+
81
+ // Apply source multiplier
82
+ const adjustedDropRate = Math.min(0.95, dropRate * (SOURCE_MULTIPLIERS[source] || 1.0));
83
+
84
+ // Roll for drop
85
+ if (Math.random() > adjustedDropRate) {
86
+ return { dropped: false };
87
+ }
88
+
89
+ // Select rarity and item
90
+ const rarity = selectRarity(rarityWeights);
91
+ const name = selectItem(rarity, dict);
92
+
93
+ return {
94
+ dropped: true,
95
+ item: {
96
+ name,
97
+ rarity,
98
+ droppedAt: new Date().toISOString(),
99
+ source,
100
+ },
101
+ };
102
+ }
103
+
104
+ // Format loot drop message
105
+ export function formatLootDrop(item: LootItem, dict: Dictionary): string {
106
+ const rarityName = dict.rarities[item.rarity];
107
+ const raritySymbol = getRaritySymbol(item.rarity);
108
+
109
+ return `
110
+ === ${dict.messages.lootDropped} ===
111
+
112
+ ${raritySymbol} [${rarityName}] ${item.name}
113
+ `.trim();
114
+ }
115
+
116
+ // Get rarity symbol/color indicator
117
+ function getRaritySymbol(rarity: Rarity): string {
118
+ switch (rarity) {
119
+ case 'common': return '[.]';
120
+ case 'uncommon': return '[+]';
121
+ case 'rare': return '[*]';
122
+ case 'epic': return '[#]';
123
+ case 'legendary': return '[!]';
124
+ }
125
+ }
126
+
127
+ // Format inventory display
128
+ export function formatInventory(inventory: LootItem[], dict: Dictionary): string {
129
+ if (!inventory || inventory.length === 0) {
130
+ return `${dict.stats.inventory}: (empty)`;
131
+ }
132
+
133
+ // Group by rarity
134
+ const byRarity: Record<Rarity, LootItem[]> = {
135
+ legendary: [],
136
+ epic: [],
137
+ rare: [],
138
+ uncommon: [],
139
+ common: [],
140
+ };
141
+
142
+ for (const item of inventory) {
143
+ byRarity[item.rarity].push(item);
144
+ }
145
+
146
+ const lines: string[] = [`=== ${dict.stats.inventory} (${inventory.length} items) ===`, ''];
147
+
148
+ for (const rarity of ['legendary', 'epic', 'rare', 'uncommon', 'common'] as Rarity[]) {
149
+ const items = byRarity[rarity];
150
+ if (items.length > 0) {
151
+ const symbol = getRaritySymbol(rarity);
152
+ const rarityName = dict.rarities[rarity];
153
+ lines.push(`${rarityName}:`);
154
+ for (const item of items) {
155
+ lines.push(` ${symbol} ${item.name}`);
156
+ }
157
+ lines.push('');
158
+ }
159
+ }
160
+
161
+ return lines.join('\n').trim();
162
+ }
163
+
164
+ // Calculate inventory value (for stats)
165
+ export function calculateInventoryValue(inventory: LootItem[]): number {
166
+ if (!inventory) return 0;
167
+
168
+ const values: Record<Rarity, number> = {
169
+ common: 1,
170
+ uncommon: 5,
171
+ rare: 25,
172
+ epic: 100,
173
+ legendary: 500,
174
+ };
175
+
176
+ return inventory.reduce((sum, item) => sum + values[item.rarity], 0);
177
+ }
@@ -473,16 +473,27 @@ export async function processCommand(
473
473
 
474
474
  // Stats command
475
475
  if (command === 'stats') {
476
- return wrapResult({ output: formatStats() });
476
+ return wrapResult({ output: formatStats(config.rulesPreset) });
477
+ }
478
+
479
+ // Inventory command
480
+ if (command === 'inventory' || command === 'inv' || command === 'loot') {
481
+ const { formatInventoryDisplay } = await import('../storage/profile');
482
+ return wrapResult({ output: formatInventoryDisplay(config.rulesPreset) });
477
483
  }
478
484
 
479
485
  // Achievements command
480
486
  if (command === 'achievements' || command === 'ach') {
481
- return wrapResult({ output: formatAchievements() });
487
+ return wrapResult({ output: formatAchievements(config.rulesPreset) });
482
488
  }
483
489
 
484
490
  // Done command - mark current node as completed
485
491
  if (command === 'done') {
492
+ const { getDictionary } = await import('../data/dictionaries');
493
+ const { formatLootDrop } = await import('../data/loot');
494
+ const { getAchievementInfo } = await import('../storage/profile');
495
+ const dict = getDictionary(config.rulesPreset);
496
+
486
497
  const nodeConfig = readNodeConfig(currentPath);
487
498
 
488
499
  if (!nodeConfig) {
@@ -502,7 +513,8 @@ export async function processCommand(
502
513
  result.xpGained,
503
514
  nodeConfig.isBoss || false,
504
515
  depth,
505
- nodeConfig.createdAt
516
+ nodeConfig.createdAt,
517
+ config.rulesPreset
506
518
  );
507
519
 
508
520
  // Save to undo history
@@ -513,27 +525,33 @@ export async function processCommand(
513
525
  timestamp: new Date().toISOString(),
514
526
  });
515
527
 
516
- let output = `\n=== TASK COMPLETED ===\n`;
517
- output += `\nTasks completed: ${result.tasksCompleted}`;
528
+ let output = `\n=== ${dict.messages.questCompleted} ===\n`;
529
+ output += `\nTasks: ${result.tasksCompleted}`;
518
530
  if (result.bossesDefeated > 0) {
519
- output += `\nBosses defeated: ${result.bossesDefeated}`;
531
+ output += ` | Bosses: ${result.bossesDefeated}`;
520
532
  }
521
533
  output += `\n+${result.xpGained} XP`;
522
534
 
523
535
  if (taskResult.levelUp) {
524
- output += `\n\n*** LEVEL UP! ***`;
525
- output += `\nYou are now level ${taskResult.newLevel}!`;
536
+ output += `\n\n*** ${dict.messages.levelUp} ***`;
537
+ output += `\n${dict.stats.level} ${taskResult.newLevel}!`;
526
538
  }
527
539
 
528
540
  if (taskResult.newAchievements.length > 0) {
529
- output += `\n\n=== NEW ACHIEVEMENTS ===`;
530
- for (const ach of taskResult.newAchievements) {
531
- output += `\n[x] ${ach.name}: ${ach.description}`;
541
+ output += `\n\n=== ${dict.messages.newAchievement} ===`;
542
+ for (const achId of taskResult.newAchievements) {
543
+ const achInfo = getAchievementInfo(achId, dict);
544
+ if (achInfo) {
545
+ output += `\n[x] ${achInfo.name}: ${achInfo.desc}`;
546
+ }
532
547
  }
533
548
  }
534
549
 
535
- output += '\n';
536
- output += '[Type "undo" to revert]';
550
+ if (taskResult.lootDropped) {
551
+ output += `\n\n${formatLootDrop(taskResult.lootDropped, dict)}`;
552
+ }
553
+
554
+ output += '\n\n[Type "undo" to revert]';
537
555
 
538
556
  return wrapResult({ output });
539
557
  }
@@ -566,10 +584,10 @@ export async function processCommand(
566
584
  return wrapResult({ output });
567
585
  }
568
586
 
569
- // Deadline command
570
- if (command === 'deadline') {
587
+ // Deadline command (dl as alias)
588
+ if (command === 'deadline' || command === 'dl') {
571
589
  if (parts.length < 2) {
572
- return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
590
+ return wrapResult({ output: 'Usage: deadline <date> (or dl <date>)\nExamples: dl today, dl +3d, deadline Jan 15' });
573
591
  }
574
592
 
575
593
  const dateStr = parts.slice(1).join(' ');
@@ -1107,22 +1125,29 @@ export async function processCommand(
1107
1125
  }
1108
1126
 
1109
1127
  if (command === 'config') {
1110
- const { updateConfig, RULES_PRESETS } = await import('../config/config');
1128
+ const { updateConfig, SUPPORTED_MODELS } = await import('../config/config');
1111
1129
 
1112
- // Check for flags
1113
- const keyFlag = parts.find(p => p.startsWith('-k=') || p.startsWith('--key='));
1114
- const modelFlag = parts.find(p => p.startsWith('-m=') || p.startsWith('--model='));
1115
- const rulesFlag = parts.find(p => p.startsWith('-r=') || p.startsWith('--rules='));
1116
- const themeFlag = parts.find(p => p.startsWith('-t=') || p.startsWith('--theme='));
1130
+ // Check for flags (uppercase short, lowercase long)
1131
+ const keyFlag = parts.find(p => p.startsWith('-K=') || p.startsWith('--key='));
1132
+ const modelFlag = parts.find(p => p.startsWith('-M=') || p.startsWith('--model='));
1133
+ const rulesFlag = parts.find(p => p.startsWith('-R=') || p.startsWith('--rules='));
1117
1134
 
1118
1135
  if (keyFlag) {
1119
1136
  const value = keyFlag.split('=').slice(1).join('=');
1137
+ if (!value) {
1138
+ return wrapResult({ output: 'Error: API key cannot be empty' });
1139
+ }
1120
1140
  updateConfig({ apiKey: value });
1121
1141
  return wrapResult({ output: 'API key updated.' });
1122
1142
  }
1123
1143
 
1124
1144
  if (modelFlag) {
1125
1145
  const value = modelFlag.split('=').slice(1).join('=');
1146
+ if (!SUPPORTED_MODELS.includes(value)) {
1147
+ return wrapResult({
1148
+ output: `Error: Unknown model "${value}"\n\nSupported models:\n ${SUPPORTED_MODELS.join('\n ')}`
1149
+ });
1150
+ }
1126
1151
  updateConfig({ model: value });
1127
1152
  return wrapResult({ output: `Model updated: ${value}` });
1128
1153
  }
@@ -1130,21 +1155,7 @@ export async function processCommand(
1130
1155
  if (rulesFlag) {
1131
1156
  const value = rulesFlag.split('=').slice(1).join('=');
1132
1157
  updateConfig({ rules: value, rulesPreset: 'custom' });
1133
- return wrapResult({ output: 'Custom rules updated.' });
1134
- }
1135
-
1136
- if (themeFlag) {
1137
- const value = themeFlag.split('=').slice(1).join('=').toLowerCase();
1138
- if (RULES_PRESETS[value]) {
1139
- updateConfig({
1140
- rules: RULES_PRESETS[value].rules,
1141
- rulesPreset: value
1142
- });
1143
- return wrapResult({ output: `Theme updated: ${RULES_PRESETS[value].name}` });
1144
- } else {
1145
- const themes = Object.keys(RULES_PRESETS).join(', ');
1146
- return wrapResult({ output: `Unknown theme. Available: ${themes}` });
1147
- }
1158
+ return wrapResult({ output: 'Rules updated.' });
1148
1159
  }
1149
1160
 
1150
1161
  // Show config
@@ -1152,27 +1163,21 @@ export async function processCommand(
1152
1163
  ? config.apiKey.slice(0, 8) + '...' + config.apiKey.slice(-4)
1153
1164
  : '(not set)';
1154
1165
 
1155
- const themeName = config.rulesPreset
1156
- ? (RULES_PRESETS[config.rulesPreset]?.name || 'Custom')
1157
- : 'Default';
1158
-
1159
1166
  const rulesPreview = config.rules
1160
- ? (config.rules.length > 50 ? config.rules.substring(0, 50) + '...' : config.rules)
1161
- : '(none)';
1167
+ ? (config.rules.length > 60 ? config.rules.substring(0, 60) + '...' : config.rules)
1168
+ : '(default)';
1162
1169
 
1163
1170
  const output = `
1164
1171
  Provider: ${config.aiProvider}
1165
1172
  Model: ${config.model || '(default)'}
1166
1173
  API Key: ${maskedKey}
1167
1174
  Storage: ${config.storagePath}
1168
- Theme: ${themeName}
1169
1175
  Rules: ${rulesPreview}
1170
1176
 
1171
1177
  Set with flags:
1172
- config -k=<key> Set API key
1173
- config -m=<model> Set model
1174
- config -t=<theme> Set theme (fantasy, space, starwars, western, cyberpunk, pirate)
1175
- config -r="<rules>" Set custom rules
1178
+ config -K=<key> or --key=<key>
1179
+ config -M=<model> or --model=<model>
1180
+ config -R="<rules>" or --rules="<rules>"
1176
1181
  `.trim();
1177
1182
 
1178
1183
  return wrapResult({ output });
@@ -1187,49 +1192,51 @@ Navigation:
1187
1192
  ls List tasks and files
1188
1193
  tree [-A] [--depth=N] Show task tree
1189
1194
  cd <task> Navigate into task
1190
- cd .., ... Go back 1 or 2 levels
1191
- pwd Show current path
1192
- open Open folder in Finder
1195
+ .., ... Go up 1 or 2 levels
1196
+ pwd Current path
1197
+ open Open in Finder
1193
1198
 
1194
- Task Management:
1195
- mkdir <name> Create new task
1196
- done Complete task (earns XP)
1199
+ Tasks:
1200
+ mkdir <name> Create task
1201
+ done Complete (earns XP)
1197
1202
  undo Undo last done
1198
- deadline <date> Set deadline (today, +3d, Jan 15)
1199
- boss Toggle boss status (3x XP)
1200
- block [node] Block by task or text
1203
+ dl <date> Set deadline (dl +3d, dl Jan 15)
1204
+ boss Toggle boss (3x XP)
1205
+ block [node] Block by task
1201
1206
  unblock Remove block
1202
1207
  status Task details
1203
- check Upcoming deadlines
1208
+ check Deadline alerts
1204
1209
 
1205
1210
  Gamification:
1206
1211
  stats XP, level, streaks
1207
- achievements Achievement list
1208
- map [--ai] Dungeon map view
1212
+ achievements Achievement list (infinite)
1213
+ inventory Loot collection
1214
+ map Dungeon map
1215
+ map --ai AI-generated map
1209
1216
 
1210
- Configuration:
1217
+ Rules (AI style presets):
1218
+ Set via init or config -R="<rules>"
1219
+ Presets: fantasy, space, starwars, western, cyberpunk, pirate
1220
+
1221
+ Config:
1211
1222
  init Setup wizard
1212
1223
  config Show settings
1213
- config -k=<key> Set API key
1214
- config -m=<model> Set model
1215
- config -t=<theme> Set theme (fantasy, space, starwars, etc)
1216
- config -r="<rules>" Custom AI rules
1217
-
1218
- Themes:
1219
- default, fantasy, space, starwars, western, cyberpunk, pirate
1224
+ config -K=<key> or --key=<key>
1225
+ config -M=<model> or --model=<model>
1226
+ config -R="<rules>" or --rules="<rules>"
1220
1227
 
1221
- File Operations:
1222
- cp, mv, rm [-rf] Standard file operations
1223
- clean --yes Clear current folder
1228
+ Files:
1229
+ cp, mv, rm [-rf] Standard operations
1230
+ clean --yes Clear folder
1224
1231
 
1225
- AI Generation:
1226
- <description> AI generates preview
1227
- save Save to folders/file
1232
+ AI:
1233
+ <description> Generate preview
1234
+ save Save to folders
1228
1235
  cancel Discard
1229
1236
 
1230
1237
  Clipboard:
1231
- <cmd> | pbcopy Copy to clipboard (macOS)
1232
- <cmd> | clip Copy to clipboard (Windows)
1238
+ <cmd> | pbcopy macOS
1239
+ <cmd> | clip Windows
1233
1240
 
1234
1241
  www.rlc.rocks
1235
1242
  `.trim()
@@ -1254,17 +1261,17 @@ www.rlc.rocks
1254
1261
 
1255
1262
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
1256
1263
  } else {
1257
- const schemaPath = saveSchemaFile(
1258
- currentPath,
1264
+ const schemaPath = saveSchemaFile(
1265
+ currentPath,
1259
1266
  pending.title,
1260
1267
  pending.content
1261
- );
1262
- const filename = path.basename(schemaPath);
1263
-
1264
- sessionState.pending = null;
1265
- sessionState.history = [];
1266
-
1267
- return wrapResult({ output: `Saved: ${filename}` });
1268
+ );
1269
+ const filename = path.basename(schemaPath);
1270
+
1271
+ sessionState.pending = null;
1272
+ sessionState.history = [];
1273
+
1274
+ return wrapResult({ output: `Saved: ${filename}` });
1268
1275
  }
1269
1276
  }
1270
1277
 
@@ -17,16 +17,17 @@ const ASCII_ART = [
17
17
  '║ Roguelike CLI ║',
18
18
  '╚═════════════════════════╝',
19
19
  '',
20
- ' Tasks: done, undo, deadline, boss, block',
21
- ' Stats: stats, achievements, map, check',
22
- ' Config: init, config -t=<theme>',
20
+ ' Tasks: done, undo, dl <date>, boss, block',
21
+ ' Stats: stats, achievements, inventory, map',
23
22
  '',
24
- ' Themes: fantasy, space, starwars, cyberpunk',
23
+ ' Rules: fantasy, space, starwars, cyberpunk',
24
+ ' Config: init, config -R="<rules>"',
25
25
  '',
26
26
  ' TAB autocomplete, | pbcopy to copy',
27
27
  ' <description> -> refine -> save',
28
28
  '',
29
- ' help - commands, www.rlc.rocks',
29
+ ' help - commands',
30
+ ' www.rlc.rocks',
30
31
  '',
31
32
  ' Ready...',
32
33
  '',