import type { BossGroupCollection, GameModeId, GameSettings, HunterSave, SaveSlotId } from "./types"; import { AVAILABLE_BOSS_IDS, BOSS_DEFINITIONS, BOSS_GROUPS } from "../game/bossCatalog"; import { createClassInventory, HEALER_CLASS_ORDER } from "../game/healers"; import type { BossId } from "../game/types"; import { createDefaultGearProgress } from "../game/progression/gear"; import { BOSS_PET_DROPS, GROUP_DROP_TABLES, createEmptyCollectionLog, type CollectionLog, type LootRarity, type MaterialStack } from "../game/progression/loot"; import { BLOCKBREAKER_BREACH_DAMAGE } from "../game/blockbreaker"; import { createDefaultHealerAppearance } from "../game/healerVisuals"; export const DEFAULT_SETTINGS: GameSettings = { masterVolume: 80, reducedMotion: false, damageNumbers: true, largeText: false, }; const RARITY_LABELS: Record = { common: "Common", uncommon: "Uncommon", rare: "Rare", epic: "Epic", legendary: "Legendary", }; export function buildCollections(collectionLog: CollectionLog, bossKills: Record): BossGroupCollection[] { return BOSS_GROUPS.map((group) => { const table = GROUP_DROP_TABLES[group.id]; const bosses = group.bossIds.map((bossId) => { const pet = BOSS_PET_DROPS[bossId]; return { bossId, bossName: BOSS_DEFINITIONS[bossId].name, kills: bossKills[bossId] ?? 0, pet: { id: pet.id, name: pet.name, icon: pet.glyph, rarity: RARITY_LABELS[pet.rarity], count: collectionLog.petsFound[pet.id] ?? 0, chance: pet.chanceLabel, kind: pet.kind, }, }; }); return { groupId: group.id, groupLetter: group.letter, groupName: group.name, coreMechanic: group.coreMechanic, defeated: bosses.some((boss) => boss.kills > 0), drops: table.entries.map((drop) => ({ id: drop.id, name: drop.name, icon: drop.glyph, rarity: RARITY_LABELS[drop.rarity], count: collectionLog.dropsFound[drop.id] ?? 0, chance: drop.chanceLabel, itemLevel: drop.itemLevel, kind: drop.kind, })), bosses, }; }); } export const DEFAULT_COLLECTION_LOG: CollectionLog = createEmptyCollectionLog(); export const DEFAULT_COLLECTIONS: BossGroupCollection[] = buildCollections(DEFAULT_COLLECTION_LOG, {}); export const MODE_COPY: Record = { "roguelike-pve": { eyebrow: "Solo healer · drafted RPG expedition", title: "RPG Roguelike", description: "Draft a random four-companion party and mixed healing spellbook, clear escalating arcade hallways, defeat ten bosses, and build a run-only loadout.", detail: "Three acts, three shops, and a final guardian", status: "Playable now", }, "rogue-trials": { eyebrow: "1–4 hunters · five-round PVE trial", title: "Rogue Trials", description: "Build through four randomized dual-boss rounds, defeat an unseen trio, then leave with the clear or continue into endless combat.", detail: "Endless mode replaces every fallen boss and tracks your best kill count", status: "Playable now", }, dungeons: { eyebrow: "1–4 hunters · chosen encounter", title: "Dungeons", description: "Choose a guardian, review its mechanics, and bring a prepared healing loadout into a focused encounter.", detail: `${AVAILABLE_BOSS_IDS.length} animated guardians available`, status: "Playable now", }, "hockey-healing": { eyebrow: "1–4 hunters · endless healer pressure", title: "Hockey Healing", description: "Defend a wide goal while healing through two active bosses. Aim returns past a moving Pong paddle that strikes back.", detail: "Every fallen boss drops loot, rolls its pet chance, then receives a replacement", status: "Playable now", }, "hockey-healing-pvp": { eyebrow: "1v1 healer duel · online queue", title: "Healing Hockey PVP", description: "Defend your net, keep your party alive, and race a rival through the same endless boss order in a mirrored hockey arena.", detail: "Goals deal 45 partywide damage · 5% Dampening per boss · normalized base gear", status: "Playable now", }, blockbreaker: { eyebrow: "1–4 hunters · endless color-break PVE", title: "Blockbreaker", description: "Heal through two endless bosses while aiming a puck into advancing rows of linked color bricks.", detail: `Breaches deal ${BLOCKBREAKER_BREACH_DAMAGE} damage to every party member · survive while four allies stand`, status: "Playable now", }, "aether-assault": { eyebrow: "1–4 hunters · endless movement-only arcade PVE", title: "Aether Assault", description: "Move freely and heal through endless bosses while automatic spellfire cuts through arcane ship formations.", detail: "No extra buttons · fixed-forward auto-fire · ship strikes damage only the healer", status: "Playable now", }, "roguelike-pvp": { eyebrow: "3v3 · mirrored expeditions", title: "Roguelike PvP", description: "Race a rival squad through shifting rooms. Send hazards across the veil while keeping your own formation alive.", detail: "Draft order, normalized base gear, rival pressure, and sudden-death rules", status: "Mode shell ready", }, "stadium-pvp": { eyebrow: "5v5 · objective arena", title: "Stadium PvP", description: "Bring a prepared loadout into short team battles where positioning, interrupts, and clutch healing decide the round.", detail: "Best of five rounds / normalized gear", status: "Mode shell ready", }, }; export function selectRandomBoss(random: () => number = Math.random): BossId { return AVAILABLE_BOSS_IDS[Math.floor(random() * AVAILABLE_BOSS_IDS.length)] ?? AVAILABLE_BOSS_IDS[0]; } export const MAX_HUNTER_NAME_LENGTH = 20; export function normalizeHunterName(value: string): string { return value .replace(/[\u0000-\u001f\u007f]/g, "") .replace(/\s+/g, " ") .trim() .slice(0, MAX_HUNTER_NAME_LENGTH); } export function createHunterSave(slotId: SaveSlotId, now: string, hunterName: string): HunterSave { const normalizedName = normalizeHunterName(hunterName); if (!normalizedName) throw new Error("Hunter name is required."); return { schemaVersion: 6, slotId, hunterName: normalizedName, activeClassId: "priest", healers: Object.fromEntries(HEALER_CLASS_ORDER.map((classId) => [classId, { level: 1, inventory: createClassInventory(classId), appearance: createDefaultHealerAppearance(classId), }])) as HunterSave["healers"], location: "Ember Vault Approach", playSeconds: 0, updatedAt: now, stats: { totalBossKills: 0, flawlessClears: 0, alliesSaved: 0, healingDone: 0, bossKills: {}, highestRoguelikeRound: 0, highestRogueTrialsEndlessKills: 0, highestHockeyHealingReturns: 0, longestHockeyHealingSecondsAtBest: 0, hockeyHealingPvpWins: 0, hockeyHealingPvpLosses: 0, hockeyHealingPvpBossKills: 0, highestBlockbreakerBricks: 0, longestBlockbreakerSeconds: 0, highestBlockbreakerScore: 0, highestAetherAssaultScore: 0, highestAetherAssaultWaveAtBest: 0, longestAetherAssaultSecondsAtBest: 0, }, materials: [] as MaterialStack[], collectionLog: createEmptyCollectionLog(), gearProgress: createDefaultGearProgress(), }; }