209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
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<DifficultySlug, GroupDrop>;
|
|
entries: readonly GroupDrop[];
|
|
}
|
|
|
|
export interface CollectionLog {
|
|
dropsFound: Record<string, number>;
|
|
petsFound: Record<string, number>;
|
|
}
|
|
|
|
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<DifficultySlug, DifficultyDefinition>;
|
|
|
|
const DIFFICULTY_ID_PREFIX: Record<DifficultySlug, string> = {
|
|
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<DifficultySlug, GroupDrop>;
|
|
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<BossGroupId, GroupDropTable>;
|
|
|
|
export const BOSS_PET_DROPS = Object.fromEntries(
|
|
AVAILABLE_BOSS_IDS.map((bossId) => [bossId, createBossPetDrop(bossId)]),
|
|
) as Record<BossId, BossPetDrop>;
|
|
|
|
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: {} };
|
|
}
|