roguelike-cli 1.3.3 → 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
  }
@@ -1191,7 +1209,8 @@ Tasks:
1191
1209
 
1192
1210
  Gamification:
1193
1211
  stats XP, level, streaks
1194
- achievements Achievement list
1212
+ achievements Achievement list (infinite)
1213
+ inventory Loot collection
1195
1214
  map Dungeon map
1196
1215
  map --ai AI-generated map
1197
1216
 
@@ -1242,17 +1261,17 @@ www.rlc.rocks
1242
1261
 
1243
1262
  return wrapResult({ output: `Created todo folder: ${safeName}/` });
1244
1263
  } else {
1245
- const schemaPath = saveSchemaFile(
1246
- currentPath,
1264
+ const schemaPath = saveSchemaFile(
1265
+ currentPath,
1247
1266
  pending.title,
1248
1267
  pending.content
1249
- );
1250
- const filename = path.basename(schemaPath);
1251
-
1252
- sessionState.pending = null;
1253
- sessionState.history = [];
1254
-
1255
- 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}` });
1256
1275
  }
1257
1276
  }
1258
1277
 
@@ -18,7 +18,7 @@ const ASCII_ART = [
18
18
  '╚═════════════════════════╝',
19
19
  '',
20
20
  ' Tasks: done, undo, dl <date>, boss, block',
21
- ' Stats: stats, achievements, map --ai',
21
+ ' Stats: stats, achievements, inventory, map',
22
22
  '',
23
23
  ' Rules: fantasy, space, starwars, cyberpunk',
24
24
  ' Config: init, config -R="<rules>"',