feat: add equipment, achievements, and visual polish

- Equipment system: 12 items across weapon/armour/trinket slots with
  common/rare/epic/legendary rarities; starter commons auto-equipped,
  higher tiers drop from boss victories
- Achievement system: 15 milestones with typed conditions; checked
  each tick and crystal rewards applied automatically
- Achievement toast: slide-in notification, auto-dismisses after 4s
- Floating click text: +X gold floats on each manual click
- Expanded quests (9 total) and upgrades (12 total)
- Upgrade panel now shows locked upgrades so players can see their
  progression path
- formatNumber utility (K/M/B/T) used consistently across all panels
- Backfill logic for existing saves to add new content gracefully
- types package now emits .d.ts declarations
This commit is contained in:
2026-03-06 13:27:48 -08:00
committed by Naomi Carrigan
parent a3daed1683
commit e9e0df31fd
33 changed files with 2066 additions and 133 deletions
+50 -9
View File
@@ -1,8 +1,38 @@
import { useCallback, useRef, useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { calculateClickPower } from "../../engine/tick.js";
import { formatNumber } from "../../utils/format.js";
interface FloatText {
id: number;
x: number;
y: number;
text: string;
}
export const ClickArea = (): React.JSX.Element => {
const { state, handleClick } = useGame();
const [floats, setFloats] = useState<FloatText[]>([]);
const nextIdRef = useRef(0);
const handleClickWithFloat = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
if (!state) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const id = nextIdRef.current++;
const clickPower = calculateClickPower(state);
setFloats((prev) => [...prev, { id, x, y, text: `+${formatNumber(clickPower)}` }]);
handleClick();
setTimeout(() => {
setFloats((prev) => prev.filter((f) => f.id !== id));
}, 900);
},
[state, handleClick],
);
if (!state) return <div className="click-area-placeholder" />;
@@ -11,15 +41,26 @@ export const ClickArea = (): React.JSX.Element => {
return (
<section className="click-area">
<h2>Guild Hall</h2>
<button
className="click-button"
onClick={handleClick}
type="button"
aria-label={`Click to earn ${clickPower.toFixed(1)} gold`}
>
</button>
<p className="click-power">+{clickPower.toFixed(1)} gold per click</p>
<div className="click-button-wrapper">
<button
className="click-button"
onClick={handleClickWithFloat}
type="button"
aria-label={`Click to earn ${formatNumber(clickPower)} gold`}
>
</button>
{floats.map((float) => (
<span
key={float.id}
className="click-float"
style={{ left: float.x, top: float.y }}
>
{float.text}
</span>
))}
</div>
<p className="click-power">+{formatNumber(clickPower)} gold/click</p>
</section>
);
};