Release v0.1.3 2026-07-11
This commit is contained in:
@@ -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: {} };
|
||||
}
|
||||
Reference in New Issue
Block a user