Files
i-want-to-heal-mmo/src/frontend/data.ts
T
2026-07-17 12:47:42 -04:00

190 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<LootRarity, BossGroupCollection["drops"][number]["rarity"]> = {
common: "Common",
uncommon: "Uncommon",
rare: "Rare",
epic: "Epic",
legendary: "Legendary",
};
export function buildCollections(collectionLog: CollectionLog, bossKills: Record<string, number>): 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<GameModeId, { eyebrow: string; title: string; description: string; detail: string; status: string }> = {
"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: "14 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: "14 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: "14 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: "14 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: "14 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(),
};
}