feat: add equipment set bonuses and boss bounty runestones

- Define EquipmentSet type + computeSetBonuses utility in packages/types
- Add setId field to Equipment type and assign sets to 27 equipment items
- Create 9 named equipment sets (Iron Vanguard → Eternal Throne) with 2pc/3pc bonuses
- Apply set combat multiplier in boss route
- Apply set gold/click multipliers in tick engine and click handler
- Include set bonuses in anti-cheat delta validation
- Show active set bonus strip + set badge per card in EquipmentPanel
- Add boss first-kill bounty runestones (scaling 1–10 per boss tier)
- Update AboutPanel and IDEAS.md
This commit is contained in:
2026-03-06 23:56:45 -08:00
committed by Naomi Carrigan
parent 48bf74e713
commit 078ae50e69
19 changed files with 488 additions and 20 deletions
+15 -2
View File
@@ -1,10 +1,14 @@
import type { BossChallengeResponse, GameState } from "@elysium/types";
import { computeSetBonuses } from "@elysium/types";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
import { DEFAULT_BOSSES } from "../data/bosses.js";
import { DEFAULT_EQUIPMENT_SETS } from "../data/equipmentSets.js";
import { authMiddleware } from "../middleware/auth.js";
import { updateChallengeProgress } from "../services/dailyChallenges.js";
export const bossRouter = new Hono();
export const bossRouter = new Hono<HonoEnv>();
bossRouter.use("*", authMiddleware);
@@ -25,6 +29,9 @@ const calculatePartyStats = (
.filter((e) => e.equipped && e.bonus.combatMultiplier != null)
.reduce((mult, e) => mult * (e.bonus.combatMultiplier ?? 1), 1);
const equippedItemIds = (state.equipment ?? []).filter((e) => e.equipped).map((e) => e.id);
const setCombatMultiplier = computeSetBonuses(equippedItemIds, DEFAULT_EQUIPMENT_SETS).combatMultiplier;
let partyDPS = 0;
let partyMaxHp = 0;
@@ -52,7 +59,7 @@ const calculatePartyStats = (
partyMaxHp += adventurer.level * 50 * adventurer.count;
}
partyDPS *= equipmentCombatMultiplier;
partyDPS *= equipmentCombatMultiplier * setCombatMultiplier;
return { partyDPS, partyMaxHp };
};
@@ -185,12 +192,18 @@ bossRouter.post("/challenge", async (context) => {
state.resources.crystals += crystalsAwarded;
}
// First-kill bounty — look up authoritative bounty from static data
const staticBoss = DEFAULT_BOSSES.find((b) => b.id === body.bossId);
const bountyRunestones = staticBoss?.bountyRunestones ?? 0;
state.prestige.runestones += bountyRunestones;
rewards = {
gold: boss.goldReward,
essence: boss.essenceReward,
crystals: boss.crystalReward,
upgradeIds: boss.upgradeRewards,
equipmentIds: equipmentRewards,
bountyRunestones,
};
} else {
bossHpAtBattleEnd = Math.max(
+16 -7
View File
@@ -1,11 +1,14 @@
import type { GameState, SaveRequest } from "@elysium/types";
import { computeSetBonuses } from "@elysium/types";
import { createHmac } from "node:crypto";
import { Hono } from "hono";
import type { HonoEnv } from "../types/hono.js";
import { prisma } from "../db/client.js";
import { DEFAULT_ACHIEVEMENTS } from "../data/achievements.js";
import { DEFAULT_ADVENTURERS } from "../data/adventurers.js";
import { DEFAULT_BOSSES } from "../data/bosses.js";
import { DEFAULT_EQUIPMENT } from "../data/equipment.js";
import { DEFAULT_EQUIPMENT_SETS } from "../data/equipmentSets.js";
import { DEFAULT_QUESTS } from "../data/quests.js";
import { authMiddleware } from "../middleware/auth.js";
import { getOrResetDailyChallenges } from "../services/dailyChallenges.js";
@@ -44,6 +47,8 @@ const computeMaxPassiveIncome = (
(mult, e) => mult * (e.bonus.goldMultiplier ?? 1),
1,
);
const equippedItemIds = equippedItems.map((e) => e.id);
const setGoldMultiplier = computeSetBonuses(equippedItemIds, DEFAULT_EQUIPMENT_SETS).goldMultiplier;
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
@@ -70,7 +75,8 @@ const computeMaxPassiveIncome = (
upgradeMultiplier *
prestige *
runestonesIncome *
equipmentGoldMultiplier;
equipmentGoldMultiplier *
setGoldMultiplier;
essencePerSecond +=
adventurer.essencePerSecond *
@@ -93,9 +99,11 @@ const computeMaxClickGoldPerSecond = (state: GameState): number => {
.filter((u) => u.purchased && u.target === "click")
.reduce((mult, u) => mult * u.multiplier, 1);
const equipmentClickMultiplier = (state.equipment ?? [])
.filter((e) => e.equipped && e.bonus.clickMultiplier != null)
const equippedItems = (state.equipment ?? []).filter((e) => e.equipped);
const equipmentClickMultiplier = equippedItems
.filter((e) => e.bonus.clickMultiplier != null)
.reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1);
const setClickMultiplier = computeSetBonuses(equippedItems.map((e) => e.id), DEFAULT_EQUIPMENT_SETS).clickMultiplier;
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
@@ -104,7 +112,8 @@ const computeMaxClickGoldPerSecond = (state: GameState): number => {
clickMultiplier *
state.prestige.productionMultiplier *
runestonesClick *
equipmentClickMultiplier;
equipmentClickMultiplier *
setClickMultiplier;
return clickPower * CLICK_BUFFER_CPS;
};
@@ -284,7 +293,7 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
return { ...incoming, resources, bosses, quests, achievements, prestige };
};
export const gameRouter = new Hono();
export const gameRouter = new Hono<HonoEnv>();
gameRouter.use("*", authMiddleware);
@@ -610,8 +619,8 @@ gameRouter.post("/save", async (context) => {
await prisma.gameState.upsert({
where: { discordId },
create: { discordId, state: stateToSave, updatedAt: now },
update: { state: stateToSave, updatedAt: now },
create: { discordId, state: stateToSave as unknown as never, updatedAt: now },
update: { state: stateToSave as unknown as never, updatedAt: now },
});
const signature = secret ? computeHmac(JSON.stringify(stateToSave), secret) : undefined;