osrs-tools 2.8.0 → 2.8.1
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/dist/runescape/model/Item/all/Black2hSword.d.ts +3 -0
- package/dist/runescape/model/Item/all/Black2hSword.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Black2hSword.js +21 -0
- package/dist/runescape/model/Item/all/BlackChainbody.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackChainbody.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackChainbody.js +21 -0
- package/dist/runescape/model/Item/all/BlackKiteshield.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackKiteshield.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackKiteshield.js +21 -0
- package/dist/runescape/model/Item/all/BlackMace.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackMace.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackMace.js +21 -0
- package/dist/runescape/model/Item/all/BlackMedHelm.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackMedHelm.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackMedHelm.js +21 -0
- package/dist/runescape/model/Item/all/BlackRobe.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackRobe.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackRobe.js +21 -0
- package/dist/runescape/model/Item/all/BlackScimitar.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackScimitar.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackScimitar.js +21 -0
- package/dist/runescape/model/Item/all/BlackSword.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackSword.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackSword.js +21 -0
- package/dist/runescape/model/Item/all/BlackWarhammer.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlackWarhammer.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlackWarhammer.js +21 -0
- package/dist/runescape/model/Item/all/BlueWizardHat.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlueWizardHat.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlueWizardHat.js +21 -0
- package/dist/runescape/model/Item/all/BlueWizardRobe.d.ts +3 -0
- package/dist/runescape/model/Item/all/BlueWizardRobe.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BlueWizardRobe.js +21 -0
- package/dist/runescape/model/Item/all/BronzeArrow.d.ts +3 -0
- package/dist/runescape/model/Item/all/BronzeArrow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/BronzeArrow.js +21 -0
- package/dist/runescape/model/Item/all/DeathRune.d.ts +3 -0
- package/dist/runescape/model/Item/all/DeathRune.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/DeathRune.js +21 -0
- package/dist/runescape/model/Item/all/DragonLongsword.d.ts +3 -0
- package/dist/runescape/model/Item/all/DragonLongsword.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/DragonLongsword.js +21 -0
- package/dist/runescape/model/Item/all/GreenDHideBody.d.ts +1 -1
- package/dist/runescape/model/Item/all/GreenDHideBody.js +7 -7
- package/dist/runescape/model/Item/all/GreenDHideChaps.d.ts +3 -0
- package/dist/runescape/model/Item/all/GreenDHideChaps.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/GreenDHideChaps.js +27 -0
- package/dist/runescape/model/Item/all/HardleatherBody.d.ts +3 -0
- package/dist/runescape/model/Item/all/HardleatherBody.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/HardleatherBody.js +21 -0
- package/dist/runescape/model/Item/all/Herring.d.ts +3 -0
- package/dist/runescape/model/Item/all/Herring.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Herring.js +21 -0
- package/dist/runescape/model/Item/all/IronArrow.d.ts +3 -0
- package/dist/runescape/model/Item/all/IronArrow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/IronArrow.js +21 -0
- package/dist/runescape/model/Item/all/IronPickaxe.d.ts +3 -0
- package/dist/runescape/model/Item/all/IronPickaxe.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/IronPickaxe.js +21 -0
- package/dist/runescape/model/Item/all/LeatherBody.d.ts +3 -0
- package/dist/runescape/model/Item/all/LeatherBody.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/LeatherBody.js +21 -0
- package/dist/runescape/model/Item/all/LeatherCowl.d.ts +3 -0
- package/dist/runescape/model/Item/all/LeatherCowl.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/LeatherCowl.js +21 -0
- package/dist/runescape/model/Item/all/LeatherVambraces.d.ts +3 -0
- package/dist/runescape/model/Item/all/LeatherVambraces.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/LeatherVambraces.js +21 -0
- package/dist/runescape/model/Item/all/Longbow.d.ts +3 -0
- package/dist/runescape/model/Item/all/Longbow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Longbow.js +21 -0
- package/dist/runescape/model/Item/all/OakLongbow.d.ts +3 -0
- package/dist/runescape/model/Item/all/OakLongbow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/OakLongbow.js +21 -0
- package/dist/runescape/model/Item/all/OakShortbow.d.ts +3 -0
- package/dist/runescape/model/Item/all/OakShortbow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/OakShortbow.js +21 -0
- package/dist/runescape/model/Item/all/Sardine.d.ts +3 -0
- package/dist/runescape/model/Item/all/Sardine.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Sardine.js +21 -0
- package/dist/runescape/model/Item/all/Shortbow.d.ts +3 -0
- package/dist/runescape/model/Item/all/Shortbow.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Shortbow.js +21 -0
- package/dist/runescape/model/Item/all/Shrimps.d.ts +3 -0
- package/dist/runescape/model/Item/all/Shrimps.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/Shrimps.js +21 -0
- package/dist/runescape/model/Item/all/SoulRune.d.ts +3 -0
- package/dist/runescape/model/Item/all/SoulRune.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/SoulRune.js +21 -0
- package/dist/runescape/model/Item/all/StaffOfEarth.d.ts +3 -0
- package/dist/runescape/model/Item/all/StaffOfEarth.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/StaffOfEarth.js +21 -0
- package/dist/runescape/model/Item/all/StaffOfFire.d.ts +3 -0
- package/dist/runescape/model/Item/all/StaffOfFire.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/StaffOfFire.js +21 -0
- package/dist/runescape/model/Item/all/StaffOfWater.d.ts +3 -0
- package/dist/runescape/model/Item/all/StaffOfWater.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/StaffOfWater.js +21 -0
- package/dist/runescape/model/Item/all/SteelAxe.d.ts +3 -0
- package/dist/runescape/model/Item/all/SteelAxe.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/SteelAxe.js +21 -0
- package/dist/runescape/model/Item/all/SteelDagger.d.ts +3 -0
- package/dist/runescape/model/Item/all/SteelDagger.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/SteelDagger.js +21 -0
- package/dist/runescape/model/Item/all/SteelLongsword.d.ts +3 -0
- package/dist/runescape/model/Item/all/SteelLongsword.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/SteelLongsword.js +21 -0
- package/dist/runescape/model/Item/all/SummerPie.d.ts +3 -0
- package/dist/runescape/model/Item/all/SummerPie.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/SummerPie.js +21 -0
- package/dist/runescape/model/Item/all/TunaPotato.d.ts +3 -0
- package/dist/runescape/model/Item/all/TunaPotato.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/TunaPotato.js +21 -0
- package/dist/runescape/model/Item/all/WizardHat.d.ts +3 -0
- package/dist/runescape/model/Item/all/WizardHat.d.ts.map +1 -0
- package/dist/runescape/model/Item/all/WizardHat.js +21 -0
- package/dist/runescape/model/clue/ClueScrollHelper.d.ts +1 -1
- package/dist/runescape/model/clue/ClueScrollHelper.d.ts.map +1 -1
- package/dist/runescape/model/clue/ClueScrollHelper.js +114 -197
- package/dist/runescape/model/clue/ClueScrollRewards.d.ts +17 -40
- package/dist/runescape/model/clue/ClueScrollRewards.d.ts.map +1 -1
- package/dist/runescape/model/clue/ClueScrollRewards.js +668 -501
- package/package.json +1 -1
|
@@ -5,58 +5,96 @@
|
|
|
5
5
|
* WIKI REFERENCE: https://oldschool.runescape.wiki/w/Clue_scrolls
|
|
6
6
|
* Each tier has unique mechanics documented in the reward casket pages
|
|
7
7
|
*/
|
|
8
|
-
import { getClueRewardsByTier, getClueRewardTables
|
|
8
|
+
import { getClueRewardsByTier, getClueRewardTables } from "./ClueScrollRewards";
|
|
9
9
|
const ELITE_MIMIC_BASE_CHANCE = 1 / 35;
|
|
10
|
-
const ELITE_MIMIC_GUARANTEE_STREAK = 25;
|
|
11
|
-
let eliteCasketsSinceMimic = 0;
|
|
12
10
|
//======================================================================================
|
|
13
|
-
// CORE UTILITY METHODS -
|
|
11
|
+
// CORE UTILITY METHODS - Wiki Rarity Driven Selection
|
|
14
12
|
//======================================================================================
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
function cloneItemWithQuantity(item, quantity) {
|
|
14
|
+
const cloned = Object.assign(Object.create(Object.getPrototypeOf(item)), item);
|
|
15
|
+
cloned.quantity = quantity;
|
|
16
|
+
return cloned;
|
|
17
|
+
}
|
|
18
|
+
function toCanonicalRewardName(rewardKey) {
|
|
19
|
+
// Keep canonical suffixes that are real item names, but strip quantity-range descriptors.
|
|
20
|
+
if (/\((?:\d|\d+k|\d+-\d+|\d+k-\d+k)/i.test(rewardKey)) {
|
|
21
|
+
return rewardKey.replace(/\s*\((?:\d|\d+k|\d+-\d+|\d+k-\d+k)[^)]*\)$/i, "").trim();
|
|
22
|
+
}
|
|
23
|
+
return rewardKey;
|
|
24
|
+
}
|
|
25
|
+
function canonicalizeRewardItem(rewardKey, reward) {
|
|
26
|
+
const canonicalName = toCanonicalRewardName(rewardKey);
|
|
27
|
+
const canonicalized = cloneItemWithQuantity(reward.item, resolveRewardQuantity(reward));
|
|
28
|
+
canonicalized.name = canonicalName;
|
|
29
|
+
canonicalized.officialWikiUrl = `https://oldschool.runescape.wiki/w/${canonicalName.replace(/ /g, "_")}`;
|
|
30
|
+
return canonicalized;
|
|
31
|
+
}
|
|
32
|
+
function resolveRewardQuantity(reward) {
|
|
33
|
+
if (typeof reward.quantity === "number") {
|
|
34
|
+
return reward.quantity;
|
|
35
|
+
}
|
|
36
|
+
if (typeof reward.quantityMin === "number" && typeof reward.quantityMax === "number") {
|
|
37
|
+
const min = Math.ceil(reward.quantityMin);
|
|
38
|
+
const max = Math.floor(reward.quantityMax);
|
|
39
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
40
|
+
}
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
function rollTierReward(tier, excludeMasterClue = true) {
|
|
44
|
+
const rewards = getClueRewardsByTier(tier);
|
|
45
|
+
const entries = Object.entries(rewards).filter(([name]) => !(excludeMasterClue && name === "Clue scroll (master)"));
|
|
46
|
+
if (entries.length === 0) {
|
|
47
|
+
throw new Error(`No rewards configured for tier: ${tier}`);
|
|
29
48
|
}
|
|
30
|
-
|
|
31
|
-
const
|
|
49
|
+
const totalWeight = entries.reduce((sum, [, reward]) => sum + 1 / reward.rarity, 0);
|
|
50
|
+
const roll = Math.random() * totalWeight;
|
|
32
51
|
let cumulative = 0;
|
|
33
|
-
for (const
|
|
34
|
-
cumulative +=
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
52
|
+
for (const [rewardKey, reward] of entries) {
|
|
53
|
+
cumulative += 1 / reward.rarity;
|
|
54
|
+
if (roll < cumulative) {
|
|
55
|
+
return canonicalizeRewardItem(rewardKey, reward);
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
|
-
|
|
40
|
-
return
|
|
58
|
+
const [fallbackKey, fallbackReward] = entries[entries.length - 1];
|
|
59
|
+
return canonicalizeRewardItem(fallbackKey, fallbackReward);
|
|
41
60
|
}
|
|
42
61
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @param
|
|
46
|
-
*
|
|
62
|
+
* Rolls the primary reward for a tier, which may involve weighted table selection if multiple tables exist.
|
|
63
|
+
* This handles the multi-table mechanics for beginner clues and any future tiers that may have them.
|
|
64
|
+
* @param tier The clue tier to roll a reward for
|
|
65
|
+
*
|
|
66
|
+
* @returns An Item representing the rolled reward, with canonicalized name and resolved quantity
|
|
47
67
|
*/
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
function rollTierPrimaryReward(tier) {
|
|
69
|
+
const tierTables = getClueRewardTables(tier);
|
|
70
|
+
if (!tierTables) {
|
|
71
|
+
return rollTierReward(tier, true);
|
|
72
|
+
}
|
|
73
|
+
const primaryTables = tierTables.filter((table) => table.weight > 0);
|
|
74
|
+
if (primaryTables.length === 0) {
|
|
75
|
+
return rollTierReward(tier, true);
|
|
76
|
+
}
|
|
77
|
+
const totalWeight = primaryTables.reduce((sum, table) => sum + table.weight, 0);
|
|
78
|
+
const tableRoll = Math.random() * totalWeight;
|
|
79
|
+
let cumulativeWeight = 0;
|
|
80
|
+
for (const table of primaryTables) {
|
|
81
|
+
cumulativeWeight += table.weight;
|
|
82
|
+
if (tableRoll < cumulativeWeight) {
|
|
83
|
+
const itemEntries = Object.entries(table.items);
|
|
84
|
+
const itemWeightTotal = itemEntries.reduce((sum, [, reward]) => sum + 1 / reward.rarity, 0);
|
|
85
|
+
const itemRoll = Math.random() * itemWeightTotal;
|
|
86
|
+
let cumulativeItemWeight = 0;
|
|
87
|
+
for (const [rewardKey, reward] of itemEntries) {
|
|
88
|
+
cumulativeItemWeight += 1 / reward.rarity;
|
|
89
|
+
if (itemRoll < cumulativeItemWeight) {
|
|
90
|
+
return canonicalizeRewardItem(rewardKey, reward);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const [fallbackKey, fallbackReward] = itemEntries[itemEntries.length - 1];
|
|
94
|
+
return canonicalizeRewardItem(fallbackKey, fallbackReward);
|
|
56
95
|
}
|
|
57
96
|
}
|
|
58
|
-
|
|
59
|
-
return tables[0].items;
|
|
97
|
+
return rollTierReward(tier, true);
|
|
60
98
|
}
|
|
61
99
|
//======================================================================================
|
|
62
100
|
// TIER-SPECIFIC REWARD COUNT METHODS
|
|
@@ -161,11 +199,8 @@ function getMasterRewardCount() {
|
|
|
161
199
|
function openBeginnerCasket() {
|
|
162
200
|
const rewardCount = getBeginnerRewardCount();
|
|
163
201
|
const rewards = [];
|
|
164
|
-
const tables = getClueRewardTables("beginner");
|
|
165
202
|
for (let i = 0; i < rewardCount; i++) {
|
|
166
|
-
|
|
167
|
-
const item = selectRandomReward(selectedTable);
|
|
168
|
-
rewards.push(item);
|
|
203
|
+
rewards.push(rollTierPrimaryReward("beginner"));
|
|
169
204
|
}
|
|
170
205
|
return { items: rewards, count: rewardCount };
|
|
171
206
|
}
|
|
@@ -185,22 +220,13 @@ function openBeginnerCasket() {
|
|
|
185
220
|
function openEasyCasket() {
|
|
186
221
|
const rewardCount = getEasyRewardCount();
|
|
187
222
|
const rewards = [];
|
|
188
|
-
const tables = getClueRewardTables("easy");
|
|
189
|
-
// Main rewards using weighted table selection
|
|
190
223
|
for (let i = 0; i < rewardCount; i++) {
|
|
191
|
-
|
|
192
|
-
const item = selectRandomReward(selectedTable);
|
|
193
|
-
rewards.push(item);
|
|
224
|
+
rewards.push(rollTierPrimaryReward("easy"));
|
|
194
225
|
}
|
|
195
|
-
|
|
196
|
-
const masterClueRoll = Math.random();
|
|
226
|
+
const easyRewards = getClueRewardsByTier("easy");
|
|
197
227
|
let masterClue;
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
const masterTable = tables.find((t) => t.weight === 0 && "items" in t);
|
|
201
|
-
if (masterTable && "Clue scroll (master)" in masterTable.items) {
|
|
202
|
-
masterClue = masterTable.items["Clue scroll (master)"].item;
|
|
203
|
-
}
|
|
228
|
+
if (Math.random() < 1 / 50 && easyRewards["Clue scroll (master)"]) {
|
|
229
|
+
masterClue = cloneItemWithQuantity(easyRewards["Clue scroll (master)"].item, resolveRewardQuantity(easyRewards["Clue scroll (master)"]));
|
|
204
230
|
}
|
|
205
231
|
const result = { items: rewards, count: rewardCount };
|
|
206
232
|
if (masterClue) {
|
|
@@ -217,13 +243,19 @@ function openEasyCasket() {
|
|
|
217
243
|
function openMediumCasket() {
|
|
218
244
|
const rewardCount = getMediumRewardCount();
|
|
219
245
|
const rewards = [];
|
|
220
|
-
const tables = getClueRewardTables("medium");
|
|
221
246
|
for (let i = 0; i < rewardCount; i++) {
|
|
222
|
-
|
|
223
|
-
const item = selectRandomReward(selectedTable);
|
|
224
|
-
rewards.push(item);
|
|
247
|
+
rewards.push(rollTierPrimaryReward("medium"));
|
|
225
248
|
}
|
|
226
|
-
|
|
249
|
+
const mediumRewards = getClueRewardsByTier("medium");
|
|
250
|
+
let masterClue;
|
|
251
|
+
if (Math.random() < 1 / 30 && mediumRewards["Clue scroll (master)"]) {
|
|
252
|
+
masterClue = cloneItemWithQuantity(mediumRewards["Clue scroll (master)"].item, resolveRewardQuantity(mediumRewards["Clue scroll (master)"]));
|
|
253
|
+
}
|
|
254
|
+
const result = { items: rewards, count: rewardCount };
|
|
255
|
+
if (masterClue) {
|
|
256
|
+
result.masterClue = masterClue;
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
227
259
|
}
|
|
228
260
|
/**
|
|
229
261
|
* Hard casket opening
|
|
@@ -234,13 +266,19 @@ function openMediumCasket() {
|
|
|
234
266
|
function openHardCasket() {
|
|
235
267
|
const rewardCount = getHardRewardCount();
|
|
236
268
|
const rewards = [];
|
|
237
|
-
const tables = getClueRewardTables("hard");
|
|
238
269
|
for (let i = 0; i < rewardCount; i++) {
|
|
239
|
-
|
|
240
|
-
const item = selectRandomReward(selectedTable);
|
|
241
|
-
rewards.push(item);
|
|
270
|
+
rewards.push(rollTierPrimaryReward("hard"));
|
|
242
271
|
}
|
|
243
|
-
|
|
272
|
+
const hardRewards = getClueRewardsByTier("hard");
|
|
273
|
+
let masterClue;
|
|
274
|
+
if (Math.random() < 1 / 15 && hardRewards["Clue scroll (master)"]) {
|
|
275
|
+
masterClue = cloneItemWithQuantity(hardRewards["Clue scroll (master)"].item, resolveRewardQuantity(hardRewards["Clue scroll (master)"]));
|
|
276
|
+
}
|
|
277
|
+
const result = { items: rewards, count: rewardCount };
|
|
278
|
+
if (masterClue) {
|
|
279
|
+
result.masterClue = masterClue;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
244
282
|
}
|
|
245
283
|
/**
|
|
246
284
|
* Elite casket opening with master clue special mechanic
|
|
@@ -256,87 +294,21 @@ function openHardCasket() {
|
|
|
256
294
|
function openEliteCasket() {
|
|
257
295
|
const rewardCount = getEliteRewardCount();
|
|
258
296
|
const rewards = [];
|
|
259
|
-
const uniqueTable = ELITE_REWARDS.tables.find((t) => t.name === "unique")?.items;
|
|
260
|
-
const standardTable = ELITE_REWARDS.tables.find((t) => t.name === "standard")?.items;
|
|
261
|
-
const megaRareTable = ELITE_REWARDS.tables.find((t) => t.name === "mega-rare")?.items;
|
|
262
|
-
const masterBonusTable = ELITE_REWARDS.tables.find((t) => t.name === "master")?.items;
|
|
263
|
-
if (!uniqueTable || !standardTable || !megaRareTable) {
|
|
264
|
-
throw new Error("Elite reward tables are missing required unique/standard/mega-rare entries.");
|
|
265
|
-
}
|
|
266
|
-
// Main rewards (4-6): explicit hierarchy per roll.
|
|
267
297
|
for (let i = 0; i < rewardCount; i++) {
|
|
268
|
-
rewards.push(
|
|
269
|
-
}
|
|
270
|
-
const mimicGuaranteed = eliteCasketsSinceMimic >= ELITE_MIMIC_GUARANTEE_STREAK - 1;
|
|
271
|
-
const mimicTriggered = mimicGuaranteed || Math.random() < ELITE_MIMIC_BASE_CHANCE;
|
|
272
|
-
if (mimicTriggered) {
|
|
273
|
-
eliteCasketsSinceMimic = 0;
|
|
298
|
+
rewards.push(rollTierPrimaryReward("elite"));
|
|
274
299
|
}
|
|
275
|
-
|
|
276
|
-
|
|
300
|
+
const mimicTriggered = Math.random() < ELITE_MIMIC_BASE_CHANCE;
|
|
301
|
+
const eliteRewards = getClueRewardsByTier("elite");
|
|
302
|
+
if (Math.random() < 1 / 5 && eliteRewards["Clue scroll (master)"]) {
|
|
303
|
+
rewards.push(cloneItemWithQuantity(eliteRewards["Clue scroll (master)"].item, resolveRewardQuantity(eliteRewards["Clue scroll (master)"])));
|
|
277
304
|
}
|
|
278
|
-
// Master clue: 1/5 chance (20%), DOES NOT consume a slot
|
|
279
|
-
const masterClueRoll = Math.random();
|
|
280
|
-
let masterClue;
|
|
281
|
-
if (masterClueRoll < 1 / 5) {
|
|
282
|
-
if (masterBonusTable && "Clue scroll (master)" in masterBonusTable) {
|
|
283
|
-
masterClue = masterBonusTable["Clue scroll (master)"].item;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// Pity roll behavior is not fully public; this simulation uses a conservative
|
|
287
|
-
// threshold + proc chance to emulate the documented mechanic.
|
|
288
305
|
const result = {
|
|
289
306
|
items: rewards,
|
|
290
307
|
count: rewardCount,
|
|
291
308
|
mimicTriggered,
|
|
292
|
-
mimicGuaranteed: mimicTriggered ? mimicGuaranteed : undefined,
|
|
293
309
|
};
|
|
294
|
-
if (masterClue) {
|
|
295
|
-
result.masterClue = masterClue;
|
|
296
|
-
}
|
|
297
310
|
return result;
|
|
298
311
|
}
|
|
299
|
-
function generateElitePrimaryRoll(uniqueTable, standardTable, megaRareTable) {
|
|
300
|
-
// Elite mega-rare gate per roll.
|
|
301
|
-
if (Math.random() < 1 / 13616) {
|
|
302
|
-
return selectEliteMegaRareWithThirdAgeWeaponsBias(megaRareTable);
|
|
303
|
-
}
|
|
304
|
-
// Elite uniques are roughly 1/14 per roll.
|
|
305
|
-
if (Math.random() < 1 / 14) {
|
|
306
|
-
return selectRandomReward(uniqueTable);
|
|
307
|
-
}
|
|
308
|
-
// Default fallback.
|
|
309
|
-
return selectRandomReward(standardTable);
|
|
310
|
-
}
|
|
311
|
-
function selectEliteMegaRareWithThirdAgeWeaponsBias(megaRareTable) {
|
|
312
|
-
const weightedItems = [];
|
|
313
|
-
let totalWeight = 0;
|
|
314
|
-
for (const [name, { item, rarity }] of Object.entries(megaRareTable)) {
|
|
315
|
-
const key = name.toLowerCase();
|
|
316
|
-
const isDruidic = key.includes("druidic");
|
|
317
|
-
const isEliteFavoredThirdAge = key.includes("3rd age longsword") || key.includes("3rd age bow") || key.includes("3rd age cloak") || key.includes("3rd age wand");
|
|
318
|
-
// Elite tables emphasize 3rd age weapon/cloak outcomes and do not include druidic pieces.
|
|
319
|
-
if (isDruidic) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const baseWeight = 1 / rarity;
|
|
323
|
-
const boostedWeight = isEliteFavoredThirdAge ? baseWeight * 2 : baseWeight;
|
|
324
|
-
weightedItems.push({ item, weight: boostedWeight });
|
|
325
|
-
totalWeight += boostedWeight;
|
|
326
|
-
}
|
|
327
|
-
if (weightedItems.length === 0) {
|
|
328
|
-
return selectRandomReward(megaRareTable);
|
|
329
|
-
}
|
|
330
|
-
const roll = Math.random() * totalWeight;
|
|
331
|
-
let cumulative = 0;
|
|
332
|
-
for (const candidate of weightedItems) {
|
|
333
|
-
cumulative += candidate.weight;
|
|
334
|
-
if (roll < cumulative) {
|
|
335
|
-
return candidate.item;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return weightedItems[weightedItems.length - 1].item;
|
|
339
|
-
}
|
|
340
312
|
/**
|
|
341
313
|
* Master casket opening
|
|
342
314
|
* Standard weighted table selection with no special mechanics
|
|
@@ -346,26 +318,14 @@ function selectEliteMegaRareWithThirdAgeWeaponsBias(megaRareTable) {
|
|
|
346
318
|
function openMasterCasket() {
|
|
347
319
|
const rewardCount = getMasterRewardCount();
|
|
348
320
|
const rewards = [];
|
|
349
|
-
// Master casket hierarchy modeled from OSRS behavior:
|
|
350
|
-
// 1) Mega-rare gate
|
|
351
|
-
// 2) Unique gate
|
|
352
|
-
// 3) Standard fallback
|
|
353
|
-
const uniqueTable = MASTER_REWARDS.tables.find((t) => t.name === "unique")?.items;
|
|
354
|
-
const standardTable = MASTER_REWARDS.tables.find((t) => t.name === "standard")?.items;
|
|
355
|
-
const megaRareTable = MASTER_REWARDS.tables.find((t) => t.name === "mega-rare")?.items;
|
|
356
|
-
if (!uniqueTable || !standardTable || !megaRareTable) {
|
|
357
|
-
throw new Error("Master reward tables are missing required unique/standard/mega-rare entries.");
|
|
358
|
-
}
|
|
359
321
|
for (let i = 0; i < rewardCount; i++) {
|
|
360
|
-
rewards.push(
|
|
322
|
+
rewards.push(rollTierPrimaryReward("master"));
|
|
361
323
|
}
|
|
362
|
-
// Mimic encounter: 1/15 chance per casket opening.
|
|
363
|
-
// Defeating mimic grants one extra roll with improved 3rd-age odds.
|
|
364
324
|
const mimicTriggered = Math.random() < 1 / 15;
|
|
365
325
|
if (!mimicTriggered) {
|
|
366
326
|
return { items: rewards, count: rewardCount };
|
|
367
327
|
}
|
|
368
|
-
const mimicBonusItem =
|
|
328
|
+
const mimicBonusItem = rollTierPrimaryReward("master");
|
|
369
329
|
rewards.push(mimicBonusItem);
|
|
370
330
|
return {
|
|
371
331
|
items: rewards,
|
|
@@ -374,49 +334,6 @@ function openMasterCasket() {
|
|
|
374
334
|
mimicBonusItem,
|
|
375
335
|
};
|
|
376
336
|
}
|
|
377
|
-
function generateMasterPrimaryRoll(uniqueTable, standardTable, megaRareTable) {
|
|
378
|
-
// Mega-rare table gate per-roll.
|
|
379
|
-
if (Math.random() < 1 / 13616) {
|
|
380
|
-
return selectRandomReward(megaRareTable);
|
|
381
|
-
}
|
|
382
|
-
// Master unique table is approximately 1/10 per roll.
|
|
383
|
-
if (Math.random() < 0.1) {
|
|
384
|
-
return selectRandomReward(uniqueTable);
|
|
385
|
-
}
|
|
386
|
-
// Default fallback.
|
|
387
|
-
return selectRandomReward(standardTable);
|
|
388
|
-
}
|
|
389
|
-
function generateMasterMimicBonusRoll(uniqueTable, standardTable, megaRareTable) {
|
|
390
|
-
// Mimic bonus roll has boosted mega-rare access.
|
|
391
|
-
if (Math.random() < 1 / 6808) {
|
|
392
|
-
return selectMasterMegaRareWithThirdAgeBoost(megaRareTable);
|
|
393
|
-
}
|
|
394
|
-
// Keep the same unique/common hierarchy for non-mega outcomes.
|
|
395
|
-
if (Math.random() < 0.1) {
|
|
396
|
-
return selectRandomReward(uniqueTable);
|
|
397
|
-
}
|
|
398
|
-
return selectRandomReward(standardTable);
|
|
399
|
-
}
|
|
400
|
-
function selectMasterMegaRareWithThirdAgeBoost(megaRareTable) {
|
|
401
|
-
const weightedItems = [];
|
|
402
|
-
let totalWeight = 0;
|
|
403
|
-
for (const [name, { item, rarity }] of Object.entries(megaRareTable)) {
|
|
404
|
-
const baseWeight = 1 / rarity;
|
|
405
|
-
const isThirdAge = name.toLowerCase().includes("3rd age");
|
|
406
|
-
const boostedWeight = isThirdAge ? baseWeight * 2 : baseWeight;
|
|
407
|
-
weightedItems.push({ item, weight: boostedWeight });
|
|
408
|
-
totalWeight += boostedWeight;
|
|
409
|
-
}
|
|
410
|
-
const roll = Math.random() * totalWeight;
|
|
411
|
-
let cumulative = 0;
|
|
412
|
-
for (const candidate of weightedItems) {
|
|
413
|
-
cumulative += candidate.weight;
|
|
414
|
-
if (roll < cumulative) {
|
|
415
|
-
return candidate.item;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return weightedItems[weightedItems.length - 1].item;
|
|
419
|
-
}
|
|
420
337
|
//======================================================================================
|
|
421
338
|
// PUBLIC API
|
|
422
339
|
//======================================================================================
|
|
@@ -425,10 +342,10 @@ function selectMasterMegaRareWithThirdAgeBoost(megaRareTable) {
|
|
|
425
342
|
*/
|
|
426
343
|
export class ClueScrollHelper {
|
|
427
344
|
/**
|
|
428
|
-
* Resets internal simulation
|
|
345
|
+
* Resets internal simulation state.
|
|
429
346
|
*/
|
|
430
347
|
static resetSimulationState() {
|
|
431
|
-
|
|
348
|
+
// No persistent per-tier state is used in wiki-accurate roll mode.
|
|
432
349
|
}
|
|
433
350
|
/**
|
|
434
351
|
* Simulate opening a clue casket and return all rewards
|
|
@@ -21,11 +21,16 @@ import { Item } from "../Item/Item";
|
|
|
21
21
|
* Average casket value: 2,854 gp (2 rolls average)
|
|
22
22
|
* Average clues for all uniques: 597
|
|
23
23
|
*/
|
|
24
|
+
export interface RewardEntry {
|
|
25
|
+
item: Item;
|
|
26
|
+
rarity: number;
|
|
27
|
+
quantity?: number;
|
|
28
|
+
quantityMin?: number;
|
|
29
|
+
quantityMax?: number;
|
|
30
|
+
noted?: boolean;
|
|
31
|
+
}
|
|
24
32
|
interface RewardTable {
|
|
25
|
-
[itemName: string]:
|
|
26
|
-
item: Item;
|
|
27
|
-
rarity: number;
|
|
28
|
-
};
|
|
33
|
+
[itemName: string]: RewardEntry;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* Beginner rewards organized by table structure
|
|
@@ -44,10 +49,7 @@ export declare const BEGINNER_REWARDS: {
|
|
|
44
49
|
description?: string;
|
|
45
50
|
}>;
|
|
46
51
|
flattened: {
|
|
47
|
-
[x: string]:
|
|
48
|
-
item: Item;
|
|
49
|
-
rarity: number;
|
|
50
|
-
};
|
|
52
|
+
[x: string]: RewardEntry;
|
|
51
53
|
};
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
@@ -66,10 +68,7 @@ export declare const EASY_REWARDS: {
|
|
|
66
68
|
description?: string;
|
|
67
69
|
}>;
|
|
68
70
|
flattened: {
|
|
69
|
-
[x: string]:
|
|
70
|
-
item: Item;
|
|
71
|
-
rarity: number;
|
|
72
|
-
};
|
|
71
|
+
[x: string]: RewardEntry;
|
|
73
72
|
};
|
|
74
73
|
};
|
|
75
74
|
/**
|
|
@@ -84,10 +83,7 @@ export declare const MEDIUM_REWARDS: {
|
|
|
84
83
|
description?: string;
|
|
85
84
|
}>;
|
|
86
85
|
flattened: {
|
|
87
|
-
[x: string]:
|
|
88
|
-
item: Item;
|
|
89
|
-
rarity: number;
|
|
90
|
-
};
|
|
86
|
+
[x: string]: RewardEntry;
|
|
91
87
|
};
|
|
92
88
|
};
|
|
93
89
|
/**
|
|
@@ -102,10 +98,7 @@ export declare const HARD_REWARDS: {
|
|
|
102
98
|
description?: string;
|
|
103
99
|
}>;
|
|
104
100
|
flattened: {
|
|
105
|
-
[x: string]:
|
|
106
|
-
item: Item;
|
|
107
|
-
rarity: number;
|
|
108
|
-
};
|
|
101
|
+
[x: string]: RewardEntry;
|
|
109
102
|
};
|
|
110
103
|
};
|
|
111
104
|
/**
|
|
@@ -120,10 +113,7 @@ export declare const ELITE_REWARDS: {
|
|
|
120
113
|
description?: string;
|
|
121
114
|
}>;
|
|
122
115
|
flattened: {
|
|
123
|
-
[x: string]:
|
|
124
|
-
item: Item;
|
|
125
|
-
rarity: number;
|
|
126
|
-
};
|
|
116
|
+
[x: string]: RewardEntry;
|
|
127
117
|
};
|
|
128
118
|
};
|
|
129
119
|
/**
|
|
@@ -152,21 +142,13 @@ export declare const MASTER_REWARDS: {
|
|
|
152
142
|
description?: string;
|
|
153
143
|
}>;
|
|
154
144
|
flattened: {
|
|
155
|
-
[x: string]:
|
|
156
|
-
item: Item;
|
|
157
|
-
rarity: number;
|
|
158
|
-
};
|
|
145
|
+
[x: string]: RewardEntry;
|
|
159
146
|
};
|
|
160
147
|
};
|
|
161
148
|
/**
|
|
162
149
|
* Gets all reward odds for a specific clue tier
|
|
163
150
|
*/
|
|
164
|
-
export declare function getClueRewardsByTier(tier: "beginner" | "easy" | "medium" | "hard" | "elite" | "master"):
|
|
165
|
-
[itemName: string]: {
|
|
166
|
-
item: Item;
|
|
167
|
-
rarity: number;
|
|
168
|
-
};
|
|
169
|
-
};
|
|
151
|
+
export declare function getClueRewardsByTier(tier: "beginner" | "easy" | "medium" | "hard" | "elite" | "master"): RewardTable;
|
|
170
152
|
/**
|
|
171
153
|
* Gets the table structure for a specific clue tier (if available)
|
|
172
154
|
* Returns null for tiers that don't have table-based rewards
|
|
@@ -174,12 +156,7 @@ export declare function getClueRewardsByTier(tier: "beginner" | "easy" | "medium
|
|
|
174
156
|
export declare function getClueRewardTables(tier: "beginner" | "easy" | "medium" | "hard" | "elite" | "master"): Array<{
|
|
175
157
|
name: string;
|
|
176
158
|
weight: number;
|
|
177
|
-
items:
|
|
178
|
-
[itemName: string]: {
|
|
179
|
-
item: Item;
|
|
180
|
-
rarity: number;
|
|
181
|
-
};
|
|
182
|
-
};
|
|
159
|
+
items: RewardTable;
|
|
183
160
|
}> | null;
|
|
184
161
|
/**
|
|
185
162
|
* Gets all items for a specific tier as an array
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClueScrollRewards.d.ts","sourceRoot":"","sources":["../../../../source/runescape/model/clue/ClueScrollRewards.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"ClueScrollRewards.d.ts","sourceRoot":"","sources":["../../../../source/runescape/model/clue/ClueScrollRewards.ts"],"names":[],"mappings":"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"}
|