import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS, BOSS_GROUP_BY_ID, bossGroupFor, type BossGroupId } from "../bossCatalog"; import type { BossId } from "../types"; export type LootRarity = "common" | "uncommon" | "rare" | "epic" | "legendary"; export type DifficultySlug = "initiate" | "veteran" | "champion" | "mythic" | "ascendant"; export interface DifficultyDefinition { slug: DifficultySlug; name: string; itemLevel: number; rarity: LootRarity; glyph: string; dropPrefix: string; healthMultiplier: number; damageMultiplier: number; } export interface MaterialStack { id: string; name: string; quantity: number; rarity: LootRarity; itemLevel: number; glyph: string; } export interface GroupDrop { kind: "group-drop"; id: string; groupId: BossGroupId; difficultySlug: DifficultySlug; name: string; rarity: LootRarity; itemLevel: number; glyph: string; chanceLabel: string; } export interface BossPetDrop { kind: "pet"; id: string; bossId: BossId; name: string; rarity: "legendary"; glyph: string; dropRate: number; chanceLabel: string; } export interface GroupDropTable { groupId: BossGroupId; drops: Record; entries: readonly GroupDrop[]; } export interface CollectionLog { dropsFound: Record; petsFound: Record; } export interface BossRewardAward { bossId: BossId; drop: GroupDrop; quantity: number; quantityAfter: number; duplicate: boolean; pet: BossPetDrop | null; petQuantityAfter: number; } export const BOSS_PET_DROP_RATE = 1 / 500; export const DIFFICULTIES: readonly DifficultyDefinition[] = [ { slug: "initiate", name: "Initiate", itemLevel: 1, rarity: "common", glyph: "R", dropPrefix: "Raw", healthMultiplier: 1, damageMultiplier: 1 }, { slug: "veteran", name: "Veteran", itemLevel: 10, rarity: "uncommon", glyph: "G", dropPrefix: "Green", healthMultiplier: 1.45, damageMultiplier: 1.25 }, { slug: "champion", name: "Champion", itemLevel: 15, rarity: "rare", glyph: "B", dropPrefix: "Blue", healthMultiplier: 1.7, damageMultiplier: 1.45 }, { slug: "mythic", name: "Mythic", itemLevel: 20, rarity: "epic", glyph: "P", dropPrefix: "Purple", healthMultiplier: 2.25, damageMultiplier: 1.85 }, { slug: "ascendant", name: "Ascendant", itemLevel: 25, rarity: "legendary", glyph: "O", dropPrefix: "Orange", healthMultiplier: 2.8, damageMultiplier: 2.25 }, ] as const; export const DIFFICULTY_BY_SLUG = Object.fromEntries( DIFFICULTIES.map((difficulty) => [difficulty.slug, difficulty]), ) as Record; const DIFFICULTY_ID_PREFIX: Record = { initiate: "raw", veteran: "green", champion: "blue", mythic: "purple", ascendant: "orange", }; function createGroupDropTable(groupId: BossGroupId): GroupDropTable { const group = BOSS_GROUP_BY_ID[groupId]; const drops = Object.fromEntries(DIFFICULTIES.map((difficulty) => [ difficulty.slug, { kind: "group-drop" as const, id: `${DIFFICULTY_ID_PREFIX[difficulty.slug]}-${groupId}-drop`, groupId, difficultySlug: difficulty.slug, name: `${difficulty.dropPrefix} Group ${group.letter} Drop`, rarity: difficulty.rarity, itemLevel: difficulty.itemLevel, glyph: difficulty.glyph, chanceLabel: "Guaranteed 1-3", }, ])) as Record; return { groupId, drops, entries: DIFFICULTIES.map((difficulty) => drops[difficulty.slug]), }; } function createBossPetDrop(bossId: BossId): BossPetDrop { const boss = BOSS_DEFINITIONS[bossId]; return { kind: "pet", id: `${bossId}-pet`, bossId, name: `${boss.name} Pet`, rarity: "legendary", glyph: boss.icon, dropRate: BOSS_PET_DROP_RATE, chanceLabel: "1 in 500", }; } export const GROUP_DROP_TABLES = Object.fromEntries( Object.keys(BOSS_GROUP_BY_ID).map((groupId) => [groupId, createGroupDropTable(groupId as BossGroupId)]), ) as Record; export const BOSS_PET_DROPS = Object.fromEntries( AVAILABLE_BOSS_IDS.map((bossId) => [bossId, createBossPetDrop(bossId)]), ) as Record; export function groupDropTable(groupId: BossGroupId): GroupDropTable { return GROUP_DROP_TABLES[groupId]; } export function groupDrop(groupId: BossGroupId, difficultySlug: DifficultySlug): GroupDrop { return GROUP_DROP_TABLES[groupId].drops[difficultySlug]; } export function bossGroupDrop(bossId: BossId, difficultySlug: DifficultySlug): GroupDrop { return groupDrop(bossGroupFor(bossId).id, difficultySlug); } export function bossPetDrop(bossId: BossId): BossPetDrop { return BOSS_PET_DROPS[bossId]; } export function groupDropQuantity(random: () => number = Math.random): number { const roll = random(); if (roll < 0.15) return 3; if (roll < 0.5) return 2; return 1; } export function rollBossReward( bossId: BossId, difficultySlug: DifficultySlug, inventory: readonly MaterialStack[], collectionLog: CollectionLog, random: () => number = Math.random, ): { award: BossRewardAward; inventory: MaterialStack[]; collectionLog: CollectionLog } { const drop = bossGroupDrop(bossId, difficultySlug); const pet = bossPetDrop(bossId); const quantity = groupDropQuantity(random); const existing = inventory.find((item) => item.id === drop.id); const inventoryAfter = existing ? inventory.map((item) => item.id === drop.id ? { ...item, quantity: item.quantity + quantity } : { ...item }) : [...inventory.map((item) => ({ ...item })), { id: drop.id, name: drop.name, quantity, rarity: drop.rarity, itemLevel: drop.itemLevel, glyph: drop.glyph }]; const petAwarded = random() < pet.dropRate; const petQuantityAfter = (collectionLog.petsFound[pet.id] ?? 0) + Number(petAwarded); return { award: { bossId, drop, quantity, quantityAfter: (existing?.quantity ?? 0) + quantity, duplicate: Boolean(existing), pet: petAwarded ? pet : null, petQuantityAfter, }, inventory: inventoryAfter, collectionLog: { dropsFound: { ...collectionLog.dropsFound, [drop.id]: (collectionLog.dropsFound[drop.id] ?? 0) + quantity, }, petsFound: petAwarded ? { ...collectionLog.petsFound, [pet.id]: petQuantityAfter } : { ...collectionLog.petsFound }, }, }; } export function normalizeDifficultySlug(value: unknown): DifficultySlug { return typeof value === "string" && value in DIFFICULTY_BY_SLUG ? value as DifficultySlug : "initiate"; } export function createEmptyCollectionLog(): CollectionLog { return { dropsFound: {}, petsFound: {} }; }