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.
Files changed (123) hide show
  1. package/dist/runescape/model/Item/all/Black2hSword.d.ts +3 -0
  2. package/dist/runescape/model/Item/all/Black2hSword.d.ts.map +1 -0
  3. package/dist/runescape/model/Item/all/Black2hSword.js +21 -0
  4. package/dist/runescape/model/Item/all/BlackChainbody.d.ts +3 -0
  5. package/dist/runescape/model/Item/all/BlackChainbody.d.ts.map +1 -0
  6. package/dist/runescape/model/Item/all/BlackChainbody.js +21 -0
  7. package/dist/runescape/model/Item/all/BlackKiteshield.d.ts +3 -0
  8. package/dist/runescape/model/Item/all/BlackKiteshield.d.ts.map +1 -0
  9. package/dist/runescape/model/Item/all/BlackKiteshield.js +21 -0
  10. package/dist/runescape/model/Item/all/BlackMace.d.ts +3 -0
  11. package/dist/runescape/model/Item/all/BlackMace.d.ts.map +1 -0
  12. package/dist/runescape/model/Item/all/BlackMace.js +21 -0
  13. package/dist/runescape/model/Item/all/BlackMedHelm.d.ts +3 -0
  14. package/dist/runescape/model/Item/all/BlackMedHelm.d.ts.map +1 -0
  15. package/dist/runescape/model/Item/all/BlackMedHelm.js +21 -0
  16. package/dist/runescape/model/Item/all/BlackRobe.d.ts +3 -0
  17. package/dist/runescape/model/Item/all/BlackRobe.d.ts.map +1 -0
  18. package/dist/runescape/model/Item/all/BlackRobe.js +21 -0
  19. package/dist/runescape/model/Item/all/BlackScimitar.d.ts +3 -0
  20. package/dist/runescape/model/Item/all/BlackScimitar.d.ts.map +1 -0
  21. package/dist/runescape/model/Item/all/BlackScimitar.js +21 -0
  22. package/dist/runescape/model/Item/all/BlackSword.d.ts +3 -0
  23. package/dist/runescape/model/Item/all/BlackSword.d.ts.map +1 -0
  24. package/dist/runescape/model/Item/all/BlackSword.js +21 -0
  25. package/dist/runescape/model/Item/all/BlackWarhammer.d.ts +3 -0
  26. package/dist/runescape/model/Item/all/BlackWarhammer.d.ts.map +1 -0
  27. package/dist/runescape/model/Item/all/BlackWarhammer.js +21 -0
  28. package/dist/runescape/model/Item/all/BlueWizardHat.d.ts +3 -0
  29. package/dist/runescape/model/Item/all/BlueWizardHat.d.ts.map +1 -0
  30. package/dist/runescape/model/Item/all/BlueWizardHat.js +21 -0
  31. package/dist/runescape/model/Item/all/BlueWizardRobe.d.ts +3 -0
  32. package/dist/runescape/model/Item/all/BlueWizardRobe.d.ts.map +1 -0
  33. package/dist/runescape/model/Item/all/BlueWizardRobe.js +21 -0
  34. package/dist/runescape/model/Item/all/BronzeArrow.d.ts +3 -0
  35. package/dist/runescape/model/Item/all/BronzeArrow.d.ts.map +1 -0
  36. package/dist/runescape/model/Item/all/BronzeArrow.js +21 -0
  37. package/dist/runescape/model/Item/all/DeathRune.d.ts +3 -0
  38. package/dist/runescape/model/Item/all/DeathRune.d.ts.map +1 -0
  39. package/dist/runescape/model/Item/all/DeathRune.js +21 -0
  40. package/dist/runescape/model/Item/all/DragonLongsword.d.ts +3 -0
  41. package/dist/runescape/model/Item/all/DragonLongsword.d.ts.map +1 -0
  42. package/dist/runescape/model/Item/all/DragonLongsword.js +21 -0
  43. package/dist/runescape/model/Item/all/GreenDHideBody.d.ts +1 -1
  44. package/dist/runescape/model/Item/all/GreenDHideBody.js +7 -7
  45. package/dist/runescape/model/Item/all/GreenDHideChaps.d.ts +3 -0
  46. package/dist/runescape/model/Item/all/GreenDHideChaps.d.ts.map +1 -0
  47. package/dist/runescape/model/Item/all/GreenDHideChaps.js +27 -0
  48. package/dist/runescape/model/Item/all/HardleatherBody.d.ts +3 -0
  49. package/dist/runescape/model/Item/all/HardleatherBody.d.ts.map +1 -0
  50. package/dist/runescape/model/Item/all/HardleatherBody.js +21 -0
  51. package/dist/runescape/model/Item/all/Herring.d.ts +3 -0
  52. package/dist/runescape/model/Item/all/Herring.d.ts.map +1 -0
  53. package/dist/runescape/model/Item/all/Herring.js +21 -0
  54. package/dist/runescape/model/Item/all/IronArrow.d.ts +3 -0
  55. package/dist/runescape/model/Item/all/IronArrow.d.ts.map +1 -0
  56. package/dist/runescape/model/Item/all/IronArrow.js +21 -0
  57. package/dist/runescape/model/Item/all/IronPickaxe.d.ts +3 -0
  58. package/dist/runescape/model/Item/all/IronPickaxe.d.ts.map +1 -0
  59. package/dist/runescape/model/Item/all/IronPickaxe.js +21 -0
  60. package/dist/runescape/model/Item/all/LeatherBody.d.ts +3 -0
  61. package/dist/runescape/model/Item/all/LeatherBody.d.ts.map +1 -0
  62. package/dist/runescape/model/Item/all/LeatherBody.js +21 -0
  63. package/dist/runescape/model/Item/all/LeatherCowl.d.ts +3 -0
  64. package/dist/runescape/model/Item/all/LeatherCowl.d.ts.map +1 -0
  65. package/dist/runescape/model/Item/all/LeatherCowl.js +21 -0
  66. package/dist/runescape/model/Item/all/LeatherVambraces.d.ts +3 -0
  67. package/dist/runescape/model/Item/all/LeatherVambraces.d.ts.map +1 -0
  68. package/dist/runescape/model/Item/all/LeatherVambraces.js +21 -0
  69. package/dist/runescape/model/Item/all/Longbow.d.ts +3 -0
  70. package/dist/runescape/model/Item/all/Longbow.d.ts.map +1 -0
  71. package/dist/runescape/model/Item/all/Longbow.js +21 -0
  72. package/dist/runescape/model/Item/all/OakLongbow.d.ts +3 -0
  73. package/dist/runescape/model/Item/all/OakLongbow.d.ts.map +1 -0
  74. package/dist/runescape/model/Item/all/OakLongbow.js +21 -0
  75. package/dist/runescape/model/Item/all/OakShortbow.d.ts +3 -0
  76. package/dist/runescape/model/Item/all/OakShortbow.d.ts.map +1 -0
  77. package/dist/runescape/model/Item/all/OakShortbow.js +21 -0
  78. package/dist/runescape/model/Item/all/Sardine.d.ts +3 -0
  79. package/dist/runescape/model/Item/all/Sardine.d.ts.map +1 -0
  80. package/dist/runescape/model/Item/all/Sardine.js +21 -0
  81. package/dist/runescape/model/Item/all/Shortbow.d.ts +3 -0
  82. package/dist/runescape/model/Item/all/Shortbow.d.ts.map +1 -0
  83. package/dist/runescape/model/Item/all/Shortbow.js +21 -0
  84. package/dist/runescape/model/Item/all/Shrimps.d.ts +3 -0
  85. package/dist/runescape/model/Item/all/Shrimps.d.ts.map +1 -0
  86. package/dist/runescape/model/Item/all/Shrimps.js +21 -0
  87. package/dist/runescape/model/Item/all/SoulRune.d.ts +3 -0
  88. package/dist/runescape/model/Item/all/SoulRune.d.ts.map +1 -0
  89. package/dist/runescape/model/Item/all/SoulRune.js +21 -0
  90. package/dist/runescape/model/Item/all/StaffOfEarth.d.ts +3 -0
  91. package/dist/runescape/model/Item/all/StaffOfEarth.d.ts.map +1 -0
  92. package/dist/runescape/model/Item/all/StaffOfEarth.js +21 -0
  93. package/dist/runescape/model/Item/all/StaffOfFire.d.ts +3 -0
  94. package/dist/runescape/model/Item/all/StaffOfFire.d.ts.map +1 -0
  95. package/dist/runescape/model/Item/all/StaffOfFire.js +21 -0
  96. package/dist/runescape/model/Item/all/StaffOfWater.d.ts +3 -0
  97. package/dist/runescape/model/Item/all/StaffOfWater.d.ts.map +1 -0
  98. package/dist/runescape/model/Item/all/StaffOfWater.js +21 -0
  99. package/dist/runescape/model/Item/all/SteelAxe.d.ts +3 -0
  100. package/dist/runescape/model/Item/all/SteelAxe.d.ts.map +1 -0
  101. package/dist/runescape/model/Item/all/SteelAxe.js +21 -0
  102. package/dist/runescape/model/Item/all/SteelDagger.d.ts +3 -0
  103. package/dist/runescape/model/Item/all/SteelDagger.d.ts.map +1 -0
  104. package/dist/runescape/model/Item/all/SteelDagger.js +21 -0
  105. package/dist/runescape/model/Item/all/SteelLongsword.d.ts +3 -0
  106. package/dist/runescape/model/Item/all/SteelLongsword.d.ts.map +1 -0
  107. package/dist/runescape/model/Item/all/SteelLongsword.js +21 -0
  108. package/dist/runescape/model/Item/all/SummerPie.d.ts +3 -0
  109. package/dist/runescape/model/Item/all/SummerPie.d.ts.map +1 -0
  110. package/dist/runescape/model/Item/all/SummerPie.js +21 -0
  111. package/dist/runescape/model/Item/all/TunaPotato.d.ts +3 -0
  112. package/dist/runescape/model/Item/all/TunaPotato.d.ts.map +1 -0
  113. package/dist/runescape/model/Item/all/TunaPotato.js +21 -0
  114. package/dist/runescape/model/Item/all/WizardHat.d.ts +3 -0
  115. package/dist/runescape/model/Item/all/WizardHat.d.ts.map +1 -0
  116. package/dist/runescape/model/Item/all/WizardHat.js +21 -0
  117. package/dist/runescape/model/clue/ClueScrollHelper.d.ts +1 -1
  118. package/dist/runescape/model/clue/ClueScrollHelper.d.ts.map +1 -1
  119. package/dist/runescape/model/clue/ClueScrollHelper.js +114 -197
  120. package/dist/runescape/model/clue/ClueScrollRewards.d.ts +17 -40
  121. package/dist/runescape/model/clue/ClueScrollRewards.d.ts.map +1 -1
  122. package/dist/runescape/model/clue/ClueScrollRewards.js +668 -501
  123. 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, ELITE_REWARDS, MASTER_REWARDS } from "./ClueScrollRewards";
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 - Weighted Selection
11
+ // CORE UTILITY METHODS - Wiki Rarity Driven Selection
14
12
  //======================================================================================
15
- /**
16
- * Select a random item from a reward table using probability weights
17
- * Higher rarity number = lower chance of selection
18
- * @param rewards Table mapping item names to {item, rarity} pairs
19
- * @returns A randomly selected Item based on rarity weights
20
- */
21
- function selectRandomReward(rewards) {
22
- const items = [];
23
- let totalWeight = 0;
24
- // Convert rarity values to probability weights
25
- for (const { item, rarity } of Object.values(rewards)) {
26
- const probability = 1 / rarity;
27
- items.push({ item, probability });
28
- totalWeight += probability;
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
- // Weighted random selection
31
- const random = Math.random() * totalWeight;
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 { item, probability } of items) {
34
- cumulative += probability;
35
- if (random < cumulative) {
36
- return item;
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
- // Fallback (should never reach here with valid data)
40
- return items[items.length - 1].item;
58
+ const [fallbackKey, fallbackReward] = entries[entries.length - 1];
59
+ return canonicalizeRewardItem(fallbackKey, fallbackReward);
41
60
  }
42
61
  /**
43
- * Select a reward table using weighted table selection
44
- * Each table has a weight representing its probability of being selected
45
- * @param tables Array of weighted reward tables for a tier
46
- * @returns The selected reward table
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 selectWeightedTable(tables) {
49
- const totalWeight = tables.reduce((sum, t) => sum + t.weight, 0);
50
- const random = Math.random() * totalWeight;
51
- let cumulative = 0;
52
- for (const table of tables) {
53
- cumulative += table.weight;
54
- if (random < cumulative) {
55
- return table.items;
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
- // Fallback
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
- const selectedTable = selectWeightedTable(tables);
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
- const selectedTable = selectWeightedTable(tables);
192
- const item = selectRandomReward(selectedTable);
193
- rewards.push(item);
224
+ rewards.push(rollTierPrimaryReward("easy"));
194
225
  }
195
- // Master clue: 1/50 chance (separate roll)
196
- const masterClueRoll = Math.random();
226
+ const easyRewards = getClueRewardsByTier("easy");
197
227
  let masterClue;
198
- if (masterClueRoll < 1 / 50) {
199
- // Get master clue from the master clue table
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
- const selectedTable = selectWeightedTable(tables);
223
- const item = selectRandomReward(selectedTable);
224
- rewards.push(item);
247
+ rewards.push(rollTierPrimaryReward("medium"));
225
248
  }
226
- return { items: rewards, count: rewardCount };
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
- const selectedTable = selectWeightedTable(tables);
240
- const item = selectRandomReward(selectedTable);
241
- rewards.push(item);
270
+ rewards.push(rollTierPrimaryReward("hard"));
242
271
  }
243
- return { items: rewards, count: rewardCount };
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(generateElitePrimaryRoll(uniqueTable, standardTable, megaRareTable));
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
- else {
276
- eliteCasketsSinceMimic += 1;
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(generateMasterPrimaryRoll(uniqueTable, standardTable, megaRareTable));
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 = generateMasterMimicBonusRoll(uniqueTable, standardTable, megaRareTable);
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 counters used for pity/guaranteed mechanics.
345
+ * Resets internal simulation state.
429
346
  */
430
347
  static resetSimulationState() {
431
- eliteCasketsSinceMimic = 0;
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":"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"}
1
+ {"version":3,"file":"ClueScrollRewards.d.ts","sourceRoot":"","sources":["../../../../source/runescape/model/clue/ClueScrollRewards.ts"],"names":[],"mappings":"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"}