osrs-tools
Version:
A comprehensive TypeScript library for Old School RuneScape (OSRS) data and utilities, including quest data, skill requirements, and game item information
382 lines (381 loc) • 12.3 kB
JavaScript
/**
* Enhanced NPC Drop System for Complex Drop Mechanics
* Handles drop tables, rare drops, secondary rolls, and weighted systems
* This file shows how to extend the basic NpcDrop model for scalability
*/
import { NpcDrop } from './NpcDrop';
/**
* Weighted drop - An item with a weight in a weighted selection
* Useful for "rare drop table" mechanics where one of several items is chosen
*/
export class WeightedDrop {
drop;
weight;
constructor(drop, weight) {
this.drop = drop;
this.weight = weight;
if (weight <= 0) {
throw new Error('Weight must be positive');
}
}
/**
* Calculate the probability of this drop being selected
* Only valid when used in a WeightedDropTable
*/
getProbabilityInTable(totalWeight) {
return this.weight / totalWeight;
}
}
/**
* Weighted drop table - Roll a single drop from weighted options
* Example: Boss drops either Sword (weight 30), Shield (weight 15), or Helm (weight 5)
* Chance to get Sword = 30/50 = 60%
*/
export class WeightedDropTable {
drops = [];
totalWeight = 0;
/**
* Add a drop to the weighted table
*/
addDrop(drop, weight) {
const weighted = new WeightedDrop(drop, weight);
this.drops.push(weighted);
this.totalWeight += weight;
}
/**
* Get all drops in this table
*/
getDrops() {
return [...this.drops];
}
/**
* Sort by weight (most common first)
*/
sortByWeight(descending = true) {
this.drops.sort((a, b) => (descending ? b.weight - a.weight : a.weight - b.weight));
}
/**
* Calculate probability of each drop
*/
getDropProbabilities() {
const probs = new Map();
for (const weighted of this.drops) {
probs.set(weighted.drop, weighted.getProbabilityInTable(this.totalWeight));
}
return probs;
}
/**
* Get the most likely drop
*/
getMostLikelyDrop() {
return this.drops.length > 0
? this.drops.reduce((max, curr) => (curr.weight > max.weight ? curr : max))
: undefined;
}
toString() {
return `WeightedDropTable(${this.drops.length} options, total weight: ${this.totalWeight})`;
}
}
/**
* Drop roll - Represents one "roll" in a multi-roll system
* Example: Zulrah drops coins (always) THEN rolls rare drop table
*/
export class DropRoll {
name;
drops;
chanceString;
constructor(name, // e.g., "Primary", "Secondary", "Tertiary", "Rare"
drops, chanceString = 'Always') {
this.name = name;
this.drops = drops;
this.chanceString = chanceString;
}
/**
* Check if this roll is guaranteed
*/
get isGuaranteed() {
return this.chanceString === 'Always';
}
/**
* Check if drops are weighted or a flat list
*/
get isWeighted() {
return this.drops instanceof WeightedDropTable;
}
getDrop(itemId) {
if (this.isWeighted) {
return this.drops
.getDrops()
.find(w => w.drop.item === itemId)?.drop;
}
else {
return this.drops.find(d => d.item === itemId);
}
}
toString() {
const dropInfo = this.isWeighted
? `Weighted(${this.drops.getDrops().length} options)`
: `${this.drops.length} drops`;
return `${this.name} Roll (${this.chanceString}): ${dropInfo}`;
}
}
/**
* Complete NPC drop table with multiple rolls
* Replaces the old flat drop array
*
* OSRS Example - Zulrah Boss:
* - Roll 1 (Always): 2-3 drops from primary table
* - Roll 2 (Always): 1 drop from secondary table
* - Roll 3 (4/128): 1 drop from rare table
*
* OSRS Example - Woman NPC:
* - Roll 1 (Always): Bones (100%)
* (Note: Woman only has one simple roll)
*/
export class CompleteDropTable {
rolls = [];
name;
constructor(npcName = 'Unknown NPC') {
this.name = npcName;
}
/**
* Add a roll to the drop table
*/
addRoll(roll) {
this.rolls.push(roll);
}
/**
* Add a simple guaranteed roll with flat drops
*/
addSimpleRoll(name, drops) {
this.addRoll(new DropRoll(name, drops, 'Always'));
}
/**
* Add a weighted roll (e.g., rare drop table)
*/
addWeightedRoll(name, table, chance = 'Always') {
const chanceStr = typeof chance === 'string' ? chance : `1/${chance}`;
this.addRoll(new DropRoll(name, table, chanceStr));
}
/**
* Get all rolls
*/
getRolls() {
return [...this.rolls];
}
/**
* Get all possible drops (flattened)
*/
getAllPossibleDrops() {
const allDrops = [];
for (const roll of this.rolls) {
if (roll.isWeighted) {
roll.drops
.getDrops()
.forEach((w) => {
if (!allDrops.find(d => d.item === w.drop.item)) {
allDrops.push(w.drop);
}
});
}
else {
roll.drops.forEach(drop => {
if (!allDrops.find(d => d.item === drop.item)) {
allDrops.push(drop);
}
});
}
}
return allDrops;
}
/**
* Find a specific drop by item ID
*/
findDrop(itemId) {
for (const roll of this.rolls) {
const drop = roll.getDrop(itemId);
if (drop)
return drop;
}
return undefined;
}
/**
* Get conditional drops only
*/
getConditionalDrops() {
return this.getAllPossibleDrops().filter(d => d.conditional);
}
/**
* Estimate average drops per kill (simplified)
*/
estimateAverageDrops() {
let average = 0;
for (const roll of this.rolls) {
// Parse chance string to fraction
let rollChance = 1.0;
if (roll.chanceString !== 'Always') {
const parts = roll.chanceString.split('/');
if (parts.length === 2) {
rollChance = parseFloat(parts[0]) / parseFloat(parts[1]);
}
}
if (roll.isWeighted) {
// Average weighted item
const weighted = roll.drops;
const probs = weighted.getDropProbabilities();
let rollAverage = 0;
for (const [drop] of probs) {
const qty = drop.quantity;
const min = Array.isArray(qty) ? qty[0] : qty;
rollAverage += min; // Use min as estimate
}
average += rollAverage * rollChance;
}
else {
// Average flat drops
const flat = roll.drops;
let rollAverage = flat.reduce((sum, drop) => {
const qty = drop.quantity;
const min = Array.isArray(qty) ? qty[0] : qty;
return sum + min;
}, 0);
average += rollAverage * rollChance;
}
}
return average;
}
toString() {
return `${this.name} Drop Table\n${this.rolls.map(r => ` - ${r.toString()}`).join('\n')}`;
}
}
/**
* Shared rare drop table - Used by multiple NPCs
* OSRS Example: Many bosses share the same rare drop table
*/
export class SharedRareDropTable {
rarities = new Map();
/**
* Add a weighted drop table for a specific rarity tier
* Example: tier 1 = very rare (1/512), tier 2 = rare (1/128), etc.
*/
addTier(tier, table) {
this.rarities.set(tier, table);
}
/**
* Get a specific tier
*/
getTier(tier) {
return this.rarities.get(tier);
}
/**
* Get all tiers
*/
getAllTiers() {
return Array.from(this.rarities.keys()).sort();
}
toString() {
return `SharedRareDropTable(${this.rarities.size} tiers)`;
}
}
// ============================================================================
// PRACTICAL EXAMPLES
// ============================================================================
/**
* Example 1: Simple NPC (Woman)
* - Always drops Bones (100%)
*/
export function createWomanDropTable() {
const table = new CompleteDropTable('Woman');
table.addSimpleRoll('Standard', [new NpcDrop('Bones', 1, 'Always')]);
return table;
}
/**
* Example 2: Boss with multiple rolls (Zulrah simplified)
* - Always get 2-3 primary drops
* - Always get 1 secondary drop
* - 4/128 chance for 1 rare drop
*/
export function createZulrahDropTable() {
const table = new CompleteDropTable('Zulrah');
// Primary roll - always happens, multiple drops
const primaryDrops = [
new NpcDrop('Zulrah\'s Scales', [20, 40], 'Always'),
new NpcDrop('Snake Hides', [10, 20], 'Always'),
];
table.addSimpleRoll('Primary', primaryDrops);
// Secondary roll - always happens, single drop
const secondaryDrops = [
new NpcDrop('Torstol', [2, 4], 'Always'),
new NpcDrop('Snapdragon', [3, 5], 'Always'),
];
table.addSimpleRoll('Secondary', secondaryDrops);
// Rare roll - 4/128 chance, weighted drops
const rareTable = new WeightedDropTable();
rareTable.addDrop(new NpcDrop('Magic Fang', 1, 'Always'), 20); // Least rare
rareTable.addDrop(new NpcDrop('Tanzanite Fang', 1, 'Always'), 20);
rareTable.addDrop(new NpcDrop('Serpentine Helm', 1, 'Always'), 10); // Most rare
table.addWeightedRoll('Rare', rareTable, '4/128');
return table;
}
/**
* Example 3: Boss with shared rare drop table
* Many GWD bosses share a common rare table
*/
export function createSharedGWDRareTable() {
const shared = new SharedRareDropTable();
// Very rare tier (1/512)
const veryRareTable = new WeightedDropTable();
veryRareTable.addDrop(new NpcDrop('Bandos Godsword', 1, 'Always'), 1);
veryRareTable.addDrop(new NpcDrop('Zaff Godsword', 1, 'Always'), 1);
shared.addTier(1, veryRareTable);
// Rare tier (1/128)
const rareTable = new WeightedDropTable();
rareTable.addDrop(new NpcDrop('Bandos Hilt', 1, 'Always'), 1);
rareTable.addDrop(new NpcDrop('Zaff Hilt', 1, 'Always'), 1);
shared.addTier(2, rareTable);
return shared;
}
/**
* Example 4: Using shared rare table in an NPC
*/
export function createGeneralGraardorDropTable(sharedRareTable) {
const table = new CompleteDropTable('General Graardor');
// Standard drops
const standardDrops = [
new NpcDrop('Coins', [1000, 5000], 'Always'),
new NpcDrop('Bandos Boots', 1, '1/50'),
];
table.addSimpleRoll('Standard', standardDrops);
// Share rare table (1/512 for unique)
const rareTable = sharedRareTable.getTier(1);
if (rareTable) {
table.addWeightedRoll('Rare', rareTable, '1/512');
}
return table;
}
// ============================================================================
// DEMONSTRATION
// ============================================================================
export function demonstrateComplexDropSystems() {
console.log('\n=== WOMAN (Simple) ===');
const woman = createWomanDropTable();
console.log(woman.toString());
console.log('Possible drops:', woman.getAllPossibleDrops().map(d => d.item));
console.log('\n=== ZULRAH (Complex Multi-Roll) ===');
const zulrah = createZulrahDropTable();
console.log(zulrah.toString());
console.log('Estimated drops per kill:', zulrah.estimateAverageDrops().toFixed(2));
console.log('\n=== SHARED RARE TABLE ===');
const sharedRare = createSharedGWDRareTable();
console.log(sharedRare.toString());
console.log('Tiers:', sharedRare.getAllTiers());
console.log('\n=== GENERAL GRAARDOR (Uses Shared Rare) ===');
const graardor = createGeneralGraardorDropTable(sharedRare);
console.log(graardor.toString());
console.log('Possible drops:', graardor.getAllPossibleDrops().map(d => d.item));
// Find a specific drop
const bandosHilt = graardor.findDrop('Bandos Hilt');
if (bandosHilt) {
console.log(`\nFound Bandos Hilt: ${bandosHilt.toString()}`);
}
}