Files
i-want-to-heal-mmo/src/game/progression/loot.ts
T
2026-07-12 13:49:46 -04:00

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: {} };
}