fix: correct equipment balance and sort items by stat power (#69)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s
CI / Lint, Build & Test (push) Successful in 1m10s

## Summary

Two improvements to the equipment system in one PR:

### Balance fixes (closes #54)

Full equipment audit revealed 9 items with duplicated stats, regressions, or purchasable items weaker than free boss drops:

| Item | Change | Reason |
|---|---|---|
| Void Conduit | 4x → 7x combat | 100M essence sink was equal to a zone-6 boss drop |
| Void Edge | 2.75x → 3.25x combat | Purchasable was weaker than free Celestial Blade (3x) |
| Astral Robe | 2.25x → 2.75x gold | Boss drop was weaker than purchasable Titan's Aegis (2.5x) |
| Philosopher's Stone | 2x → 2.25x click | Duplicated Frost Crystal's click multiplier |
| Eternal Flame | 1.15x → 1.25x gold | Gold regressed vs Philosopher's Stone (1.25x) |
| Celestial Focus | 2.5x → 3x click | 20M essence sink was weaker than free Angel's Halo (2.75x click + 1.3x gold) |
| Abyssal Tome | 3x → 3.75x gold | 50M essence sink was equal to free Heaven's Mantle (3x) |
| Crystal Matrix | 4x → 4.75x gold | 20M crystal sink was equal to free Sinslayer Aegis (4x) |
| Infernal Gem | 3.5x → 4x click | 5M crystal sink was identical to free Prism Eye |

### Equipment sorting (closes #55)

Equipment cards within each slot now render in ascending order of combined bonus power — the sum of all multiplier bonuses — so stronger items always appear further down the list. Hybrid items such as Volcanic Plate sort correctly without needing a per-slot primary stat.

## Test plan

- [ ] All purchasable weapons/armour/trinkets now exceed the stats of the highest free boss drop at their tier
- [ ] No duplicate stat values between adjacent items in the same progression track
- [ ] Equipment cards within each slot render weakest → strongest
- [ ] Hybrid multi-stat items sort sensibly alongside single-stat items
- [ ] Full pipeline green (lint + build + tests at 100% coverage)

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #69
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #69.
This commit is contained in:
2026-03-19 08:51:08 -07:00
committed by Naomi Carrigan
parent cfcf763ce3
commit ca2edb090e
2 changed files with 26 additions and 9 deletions
+9 -9
View File
@@ -101,7 +101,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "weapon", type: "weapon",
}, },
{ {
bonus: { combatMultiplier: 2.75 }, bonus: { combatMultiplier: 3.25 },
cost: { crystals: 500, essence: 2000, gold: 0 }, cost: { crystals: 500, essence: 2000, gold: 0 },
description: description:
"A blade made of compressed nothingness. It does not cut — it simply unmakes.", "A blade made of compressed nothingness. It does not cut — it simply unmakes.",
@@ -204,7 +204,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "armour", type: "armour",
}, },
{ {
bonus: { goldMultiplier: 2.25 }, bonus: { goldMultiplier: 2.75 },
description: description:
"Woven from threads of pure starlight harvested by the Astral Wraith. Income flows like cosmic energy.", "Woven from threads of pure starlight harvested by the Astral Wraith. Income flows like cosmic energy.",
equipped: false, equipped: false,
@@ -305,7 +305,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "trinket", type: "trinket",
}, },
{ {
bonus: { clickMultiplier: 2, goldMultiplier: 1.25 }, bonus: { clickMultiplier: 2.25, goldMultiplier: 1.25 },
description: description:
"The legendary stone that grants mastery over gold and combat alike.", "The legendary stone that grants mastery over gold and combat alike.",
equipped: false, equipped: false,
@@ -316,7 +316,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "trinket", type: "trinket",
}, },
{ {
bonus: { clickMultiplier: 2.25, goldMultiplier: 1.15 }, bonus: { clickMultiplier: 2.25, goldMultiplier: 1.25 },
description: description:
"A flame that has never been extinguished, sealed in crystal by the Phoenix Lord. It burns with the power of rebirth.", "A flame that has never been extinguished, sealed in crystal by the Phoenix Lord. It burns with the power of rebirth.",
equipped: false, equipped: false,
@@ -697,7 +697,7 @@ export const defaultEquipment: Array<Equipment> = [
}, },
// ── Purchasable endgame sinks ───────────────────────────────────────────── // ── Purchasable endgame sinks ─────────────────────────────────────────────
{ {
bonus: { clickMultiplier: 2.5 }, bonus: { clickMultiplier: 3 },
cost: { crystals: 0, essence: 20_000_000, gold: 0 }, cost: { crystals: 0, essence: 20_000_000, gold: 0 },
description: description:
"A lens of compressed celestial light that sharpens every strike with divine precision.", "A lens of compressed celestial light that sharpens every strike with divine precision.",
@@ -709,7 +709,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "trinket", type: "trinket",
}, },
{ {
bonus: { goldMultiplier: 3 }, bonus: { goldMultiplier: 3.75 },
cost: { crystals: 0, essence: 50_000_000, gold: 0 }, cost: { crystals: 0, essence: 50_000_000, gold: 0 },
description: description:
"A book written in the language of the deep — reading it aligns your guild's operations with abyssal efficiency.", "A book written in the language of the deep — reading it aligns your guild's operations with abyssal efficiency.",
@@ -721,7 +721,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "armour", type: "armour",
}, },
{ {
bonus: { combatMultiplier: 4 }, bonus: { combatMultiplier: 7 },
cost: { crystals: 0, essence: 100_000_000, gold: 0 }, cost: { crystals: 0, essence: 100_000_000, gold: 0 },
description: description:
"A weapon that channels void energy — the absence of resistance makes every strike devastating.", "A weapon that channels void energy — the absence of resistance makes every strike devastating.",
@@ -733,7 +733,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "weapon", type: "weapon",
}, },
{ {
bonus: { clickMultiplier: 3.5, goldMultiplier: 1.5 }, bonus: { clickMultiplier: 4, goldMultiplier: 1.5 },
cost: { crystals: 5_000_000, essence: 0, gold: 0 }, cost: { crystals: 5_000_000, essence: 0, gold: 0 },
description: description:
"A gem forged in the heart of the Infernal Court — it burns with productivity and righteous fury in equal measure.", "A gem forged in the heart of the Infernal Court — it burns with productivity and righteous fury in equal measure.",
@@ -745,7 +745,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "trinket", type: "trinket",
}, },
{ {
bonus: { goldMultiplier: 4 }, bonus: { goldMultiplier: 4.75 },
cost: { crystals: 20_000_000, essence: 0, gold: 0 }, cost: { crystals: 20_000_000, essence: 0, gold: 0 },
description: description:
"Armour structured around a crystalline lattice of optimal income calculations. Every gold piece finds you faster.", "Armour structured around a crystalline lattice of optimal income calculations. Every gold piece finds you faster.",
@@ -7,6 +7,7 @@
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */ /* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
/* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */
/* eslint-disable complexity -- Complex component with many conditional render paths */ /* eslint-disable complexity -- Complex component with many conditional render paths */
/* eslint-disable max-lines -- Equipment panel with set bonus display and sort logic */
import { type JSX, useState } from "react"; import { type JSX, useState } from "react";
import { useGame } from "../../context/gameContext.js"; import { useGame } from "../../context/gameContext.js";
import { EQUIPMENT_SETS } from "../../data/equipmentSets.js"; import { EQUIPMENT_SETS } from "../../data/equipmentSets.js";
@@ -188,6 +189,20 @@ const EquipmentCard = ({
); );
}; };
/**
* Computes a combined power score for sorting — sum of all bonus multipliers.
* Using the sum (rather than a single stat) keeps hybrid items in sensible order.
* @param item - The equipment piece whose bonus multipliers are summed.
* @returns The combined bonus value.
*/
const equipmentPower = (item: Equipment): number => {
return (
(item.bonus.combatMultiplier ?? 1)
+ (item.bonus.goldMultiplier ?? 1)
+ (item.bonus.clickMultiplier ?? 1)
);
};
const slotOrder: Array<EquipmentType> = [ "weapon", "armour", "trinket" ]; const slotOrder: Array<EquipmentType> = [ "weapon", "armour", "trinket" ];
const slotLabel: Record<EquipmentType, string> = { const slotLabel: Record<EquipmentType, string> = {
armour: "🛡️ Armour", armour: "🛡️ Armour",
@@ -320,6 +335,8 @@ const EquipmentPanel = (): JSX.Element => {
{slotOrder.map((slotType) => { {slotOrder.map((slotType) => {
const items = equipment.filter((item) => { const items = equipment.filter((item) => {
return item.type === slotType && (showLocked || item.owned); return item.type === slotType && (showLocked || item.owned);
}).sort((a, b) => {
return equipmentPower(a) - equipmentPower(b);
}); });
return ( return (
<div className="equipment-slot-section" key={slotType}> <div className="equipment-slot-section" key={slotType}>