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
+2 -2
View File
@@ -28,8 +28,8 @@ const HOW_TO_PLAY = [
body: "New zones unlock when you defeat the final boss AND complete the final quest of the previous zone. Each zone contains new bosses and quests with progressively greater rewards.",
},
{
title: "🗡️ Equipment",
body: "Earn equipment from boss drops and quest rewards. Each piece of equipment provides bonuses to gold income, click power, or adventurer output. Rarer equipment provides stronger bonuses.",
title: "🗡️ Equipment & Sets",
body: "Earn equipment from boss drops and quest rewards. Each piece provides bonuses to gold income, click power, or combat. Rarer equipment provides stronger bonuses. Equip matching set pieces (2 or 3 of a named set) to unlock escalating set bonuses shown at the top of the Equipment panel.",
},
{
title: "⭐ Prestige",
@@ -133,6 +133,9 @@ export const BattleModal = ({
{result.rewards.crystals > 0 && (
<span>💎 {formatNumber(result.rewards.crystals)} crystals</span>
)}
{result.rewards.bountyRunestones > 0 && (
<span className="battle-bounty">🔮 {formatNumber(result.rewards.bountyRunestones)} runestones (first kill!)</span>
)}
</div>
)}
</>
@@ -70,6 +70,9 @@ const BossCard = ({
{(boss.equipmentRewards ?? []).length > 0 && (
<span>🗡 {boss.equipmentRewards.length} Equipment</span>
)}
{boss.status !== "defeated" && boss.bountyRunestones > 0 && (
<span className="boss-bounty">🔮 {boss.bountyRunestones} (first kill)</span>
)}
</div>
{(boss.status === "available" || boss.status === "in_progress") && (
@@ -1,6 +1,7 @@
import type { Equipment, EquipmentType } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { EQUIPMENT_SETS } from "../../data/equipmentSets.js";
import { LockToggle } from "../ui/LockToggle.js";
const RARITY_LABEL: Record<string, string> = {
@@ -36,6 +37,7 @@ interface EquipmentCardProps {
essence: number;
crystals: number;
dropBossName?: string | undefined;
setName?: string | undefined;
}
const costLabel = (cost: { gold: number; essence: number; crystals: number }): string => {
@@ -46,7 +48,7 @@ const costLabel = (cost: { gold: number; essence: number; crystals: number }): s
return parts.join(" ");
};
const EquipmentCard = ({ item, gold, essence, crystals, dropBossName }: EquipmentCardProps): React.JSX.Element => {
const EquipmentCard = ({ item, gold, essence, crystals, dropBossName, setName }: EquipmentCardProps): React.JSX.Element => {
const { equipItem, buyEquipment } = useGame();
const canAfford = item.cost
@@ -63,6 +65,7 @@ const EquipmentCard = ({ item, gold, essence, crystals, dropBossName }: Equipmen
</div>
<p className="equipment-description">{item.description}</p>
<p className="equipment-bonus">{bonusDescription(item)}</p>
{setName && <span className="equipment-set-badge">🔗 {setName}</span>}
{!item.owned && item.cost && (
<p className="equipment-cost">{costLabel(item.cost)}</p>
)}
@@ -121,6 +124,31 @@ export const EquipmentPanel = (): React.JSX.Element => {
}
}
// Build set name lookup for card badges
const setNameById = new Map<string, string>(
EQUIPMENT_SETS.map((s) => [s.id, s.name]),
);
// Compute active set bonuses for the summary strip
const equippedItemIds = equipment.filter((e) => e.equipped).map((e) => e.id);
const activeSets = EQUIPMENT_SETS.map((set) => {
const count = set.pieces.filter((id) => equippedItemIds.includes(id)).length;
return { set, count };
}).filter(({ count }) => count >= 2);
const setBonusDescription = (set: typeof EQUIPMENT_SETS[number], count: number): string => {
const parts: string[] = [];
for (const threshold of [2, 3] as const) {
if (count >= threshold) {
const bonus = set.bonuses[threshold];
if (bonus.goldMultiplier) parts.push(`+${Math.round((bonus.goldMultiplier - 1) * 100)}% Gold/s (${threshold}pc)`);
if (bonus.combatMultiplier) parts.push(`+${Math.round((bonus.combatMultiplier - 1) * 100)}% Combat (${threshold}pc)`);
if (bonus.clickMultiplier) parts.push(`+${Math.round((bonus.clickMultiplier - 1) * 100)}% Click (${threshold}pc)`);
}
}
return parts.join(", ");
};
return (
<section className="panel equipment-panel">
<div className="panel-header">
@@ -132,9 +160,21 @@ export const EquipmentPanel = (): React.JSX.Element => {
/>
</div>
<p className="equipment-intro">
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.
Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time. Equip matching set pieces for bonus effects!
</p>
{activeSets.length > 0 && (
<div className="active-sets">
<h3 className="active-sets-heading"> Active Set Bonuses</h3>
{activeSets.map(({ set, count }) => (
<div key={set.id} className="active-set-row">
<span className="active-set-name">{set.name} ({count}/{set.pieces.length})</span>
<span className="active-set-bonus">{setBonusDescription(set, count)}</span>
</div>
))}
</div>
)}
{SLOT_ORDER.map((slotType) => {
const items = equipment.filter(
(e) => e.type === slotType && (showLocked || e.owned),
@@ -151,6 +191,7 @@ export const EquipmentPanel = (): React.JSX.Element => {
essence={state.resources.essence}
crystals={state.resources.crystals}
dropBossName={equipmentDropSources.get(item.id)}
setName={item.setId ? setNameById.get(item.setId) : undefined}
/>
))}
{items.length === 0 && (