Release v0.1.3 2026-07-11

This commit is contained in:
Warren H
2026-07-11 23:23:02 -04:00
parent 076f6cf97c
commit b48b3a4f8f
103 changed files with 6708 additions and 454 deletions
+194
View File
@@ -0,0 +1,194 @@
import { BOSS_DEFINITIONS, BOSS_ORDER } 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;
coinPrefix: string;
healthMultiplier: number;
damageMultiplier: number;
}
export interface MaterialStack {
id: string;
name: string;
quantity: number;
rarity: LootRarity;
itemLevel: number;
glyph: string;
}
export interface BossCoinDrop {
kind: "coin";
id: string;
bossId: BossId;
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 BossDropTable {
bossId: BossId;
coins: Record<DifficultySlug, BossCoinDrop>;
pet: BossPetDrop;
entries: readonly (BossCoinDrop | BossPetDrop)[];
}
export interface CollectionLog {
dropsFound: Record<string, number>;
petsFound: Record<string, number>;
}
export interface BossRewardAward {
bossId: BossId;
coin: BossCoinDrop;
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", coinPrefix: "Raw", healthMultiplier: 1, damageMultiplier: 1 },
{ slug: "veteran", name: "Veteran", itemLevel: 10, rarity: "uncommon", glyph: "G", coinPrefix: "Green", healthMultiplier: 1.45, damageMultiplier: 1.25 },
{ slug: "champion", name: "Champion", itemLevel: 15, rarity: "rare", glyph: "B", coinPrefix: "Blue", healthMultiplier: 1.7, damageMultiplier: 1.45 },
{ slug: "mythic", name: "Mythic", itemLevel: 20, rarity: "epic", glyph: "P", coinPrefix: "Purple", healthMultiplier: 2.25, damageMultiplier: 1.85 },
{ slug: "ascendant", name: "Ascendant", itemLevel: 25, rarity: "legendary", glyph: "O", coinPrefix: "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 createBossDropTable(bossId: BossId): BossDropTable {
const boss = BOSS_DEFINITIONS[bossId];
const coins = Object.fromEntries(DIFFICULTIES.map((difficulty) => [
difficulty.slug,
{
kind: "coin" as const,
id: `${DIFFICULTY_ID_PREFIX[difficulty.slug]}-${bossId}-coin`,
bossId,
difficultySlug: difficulty.slug,
name: `${difficulty.coinPrefix} ${boss.name} Coin`,
rarity: difficulty.rarity,
itemLevel: difficulty.itemLevel,
glyph: difficulty.glyph,
chanceLabel: "Guaranteed 1-3",
},
])) as Record<DifficultySlug, BossCoinDrop>;
const pet: BossPetDrop = {
kind: "pet",
id: `${bossId}-pet`,
bossId,
name: `${boss.name} Pet`,
rarity: "legendary",
glyph: boss.icon,
dropRate: BOSS_PET_DROP_RATE,
chanceLabel: "1 in 500",
};
return {
bossId,
coins,
pet,
entries: [...DIFFICULTIES.map((difficulty) => coins[difficulty.slug]), pet],
};
}
export const BOSS_DROP_TABLES = Object.fromEntries(
BOSS_ORDER.map((bossId) => [bossId, createBossDropTable(bossId)]),
) as Record<BossId, BossDropTable>;
export function bossDropTable(bossId: BossId): BossDropTable {
return BOSS_DROP_TABLES[bossId];
}
export function bossCoinDrop(bossId: BossId, difficultySlug: DifficultySlug): BossCoinDrop {
return BOSS_DROP_TABLES[bossId].coins[difficultySlug];
}
export function coinDropQuantity(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 table = bossDropTable(bossId);
const coin = table.coins[difficultySlug];
const quantity = coinDropQuantity(random);
const existing = inventory.find((item) => item.id === coin.id);
const inventoryAfter = existing
? inventory.map((item) => item.id === coin.id ? { ...item, quantity: item.quantity + quantity } : { ...item })
: [...inventory.map((item) => ({ ...item })), { id: coin.id, name: coin.name, quantity, rarity: coin.rarity, itemLevel: coin.itemLevel, glyph: coin.glyph }];
const petAwarded = random() < table.pet.dropRate;
const petQuantityAfter = (collectionLog.petsFound[table.pet.id] ?? 0) + Number(petAwarded);
return {
award: {
bossId,
coin,
quantity,
quantityAfter: (existing?.quantity ?? 0) + quantity,
duplicate: Boolean(existing),
pet: petAwarded ? table.pet : null,
petQuantityAfter,
},
inventory: inventoryAfter,
collectionLog: {
dropsFound: {
...collectionLog.dropsFound,
[coin.id]: (collectionLog.dropsFound[coin.id] ?? 0) + quantity,
},
petsFound: petAwarded
? { ...collectionLog.petsFound, [table.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: {} };
}