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.
- package/README.md +49 -10
- package/dist/data/dictionaries.js +450 -0
- package/dist/data/loot.js +141 -0
- package/dist/interactive/commands.js +29 -14
- package/dist/interactive/startup.js +1 -1
- package/dist/storage/profile.js +222 -97
- package/package.json +1 -1
- package/src/data/dictionaries.ts +521 -0
- package/src/data/loot.ts +177 -0
- package/src/interactive/commands.ts +42 -23
- package/src/interactive/startup.ts +1 -1
- package/src/storage/profile.ts +253 -94
package/src/data/loot.ts
ADDED
|
@@ -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===
|
|
517
|
-
output += `\nTasks
|
|
528
|
+
let output = `\n=== ${dict.messages.questCompleted} ===\n`;
|
|
529
|
+
output += `\nTasks: ${result.tasksCompleted}`;
|
|
518
530
|
if (result.bossesDefeated > 0) {
|
|
519
|
-
output +=
|
|
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***
|
|
525
|
-
output += `\
|
|
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===
|
|
530
|
-
for (const
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
1246
|
-
|
|
1264
|
+
const schemaPath = saveSchemaFile(
|
|
1265
|
+
currentPath,
|
|
1247
1266
|
pending.title,
|
|
1248
1267
|
pending.content
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
|
21
|
+
' Stats: stats, achievements, inventory, map',
|
|
22
22
|
'',
|
|
23
23
|
' Rules: fantasy, space, starwars, cyberpunk',
|
|
24
24
|
' Config: init, config -R="<rules>"',
|