lootastic
Version:
a loot rolling system
122 lines (93 loc) • 3.42 kB
text/typescript
import { select } from 'weighted-randomly-select';
import * as random from 'lodash.random';
export enum LootFunction {
WithReplacement = 'chooseWithReplacement',
WithoutReplacement = 'chooseWithoutReplacement',
EachItem = 'tryToDropEachItem'
}
export interface LootRollerConfig {
table: LootTable;
func: LootFunction;
args: number;
}
export interface LootTableOptions {
rollModifier: number;
seed?: number
}
export interface Rollable {
result: string;
chance: number;
maxChance?: number;
}
export class LootRoller {
static rollTables(lootTableConfigs: LootRollerConfig[]) {
const items = [];
lootTableConfigs.forEach(({ table, func, args }) => {
items.push(...table[func](args));
});
return items;
}
static rollTable(lootTableConfig: LootRollerConfig) {
return this.rollTables([lootTableConfig]);
}
};
export class LootTable {
private choices: Rollable[];
private rollModifier: number;
constructor(choices: string[]|Rollable[], { rollModifier }: LootTableOptions = { rollModifier: 0 }) {
this.choices = this.configureChoices(choices);
this.rollModifier = rollModifier || 0;
}
private getFormattedChoices(): { alwaysDrop: string[], choices: Rollable[] } {
const alwaysDrop = [];
const choices = this.choices.map(x => {
// if the chance < -1, it always drops
if(x.chance <= 0) {
alwaysDrop.push(x.result);
return null;
}
// if there is for some reason a negative total roll, this item can never drop
const totalChance = x.chance + this.rollModifier;
if(totalChance <= 0) {
return null;
}
return { result: x.result, chance: totalChance, maxChance: x.maxChance };
}).filter(Boolean);
return { alwaysDrop, choices };
}
private configureChoices(choices: string[]|Rollable[]): Rollable[] {
// pre-formatted with weights
if((choices[0] as Rollable).chance) return choices as Rollable[];
// assign a weight of 1 to everything
return (choices as string[]).map(item => ({ result: item, chance: 1 }));
}
public chooseWithReplacement(numItems = 1): string[] {
const { choices, alwaysDrop } = this.getFormattedChoices();
return alwaysDrop.concat(
Array(numItems).fill(null).map(() => select(choices))
);
}
public chooseWithoutReplacement(numItems = 1): string[] {
const { choices, alwaysDrop } = this.getFormattedChoices();
if(numItems > choices.length) throw new Error(`Cannot choose ${numItems} without replacement when array is only ${choices.length} choices.`);
return alwaysDrop.concat(
Array(numItems).fill(null).map(() => {
// choose something
const choice = select(choices);
// remove old choice
choices.splice(choices.indexOf(choice), 1);
return choice;
})
);
}
public tryToDropEachItem(comparisonValue: number): string[] {
const { choices, alwaysDrop } = this.getFormattedChoices();
return alwaysDrop.concat(
choices.map(({ result, chance, maxChance }) => {
const randomValue = random(0, maxChance || comparisonValue || 1);
if(chance >= randomValue) return result;
return null;
}).filter(Boolean)
);
}
};