Files
elysium/apps/web/src/components/game/clickArea.tsx
T
hikari 29c817230d
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint, Build & Test (push) Successful in 1m6s
feat: initial prototype — core game systems (#30)
## Summary

This PR represents the full v1 prototype, implementing the core game systems for Elysium.

- Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests
- Adventurer hiring with batch size selector and progressive tier cost scaling
- Prestige, transcendence, and apotheosis systems with auto-prestige support
- Character sheet, titles, leaderboards, companion system, and daily login bonuses
- Auto-quest and auto-boss toggles
- Discord webhook notifications on prestige/transcendence/apotheosis
- Discord role awarded on apotheosis
- Responsive design and overarching story/lore system
- In-game sound effects and browser notifications for key events
- Support link button in the resource bar
- Full test coverage (100% on `apps/api` and `packages/types`)
- CI pipeline: lint → build → test

## Closes

Closes #1
Closes #2
Closes #3
Closes #4
Closes #5
Closes #6
Closes #7
Closes #8
Closes #9
Closes #10
Closes #11
Closes #12
Closes #13
Closes #14
Closes #16
Closes #19
Closes #20
Closes #21
Closes #22
Closes #23
Closes #24
Closes #25
Closes #26
Closes #27
Closes #29

 This issue was created with help from Hikari~ 🌸

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #30
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-08 15:53:39 -07:00

137 lines
3.7 KiB
TypeScript

/**
* @file Click area component - the main guild hall click target.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Complex useCallback with float management */
import {
type JSX,
type MouseEvent,
useCallback,
useRef,
useState,
} from "react";
import { useGame } from "../../context/gameContext.js";
import { calculateClickPower } from "../../engine/tick.js";
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -- Vite define constant
declare const __WEB_VERSION__: string;
interface FloatText {
id: number;
x: number;
y: number;
text: string;
}
/**
* Renders the guild hall click area with floating gold text on click.
* @returns The JSX element.
*/
const ClickArea = (): JSX.Element => {
const {
state,
handleClick,
formatNumber,
saveSchemaVersion,
currentSchemaVersion,
} = useGame();
const [ floats, setFloats ] = useState<Array<FloatText>>([]);
const nextIdReference = useRef(0);
const handleClickWithFloat = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
if (state === null) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const id = nextIdReference.current;
nextIdReference.current = nextIdReference.current + 1;
const clickPower = calculateClickPower(state);
const text = `+${formatNumber(clickPower)}`;
setFloats((previous) => {
return [ ...previous, { id, text, x, y } ];
});
handleClick();
setTimeout(() => {
// eslint-disable-next-line max-nested-callbacks -- Float cleanup requires nesting within setTimeout
setFloats((previous) => {
// eslint-disable-next-line max-nested-callbacks -- Float cleanup requires nesting within setTimeout
return previous.filter((floatItem) => {
return floatItem.id !== id;
});
});
}, 900);
},
[ state, handleClick, formatNumber ],
);
if (state === null) {
return <div className="click-area-placeholder" />;
}
const clickPower = calculateClickPower(state);
return (
<section className="click-area">
<h1 className="game-title">{"Elysium"}</h1>
<p className="game-version">
{"v"}
{__WEB_VERSION__}
</p>
{currentSchemaVersion > 0
&& <p className="game-schema-version">
{"Save: v"}
{saveSchemaVersion}
{" / Latest: v"}
{currentSchemaVersion}
</p>
}
<h2>{"Guild Hall"}</h2>
<div className="click-button-wrapper">
<button
aria-label={`Click to earn ${formatNumber(clickPower)} gold`}
className="click-button"
onClick={handleClickWithFloat}
type="button"
>
<img
alt="Guild Hall"
className="click-button-image"
src="https://cdn.nhcarrigan.com/avatars/elysium.png"
/>
</button>
{floats.map((floatItem) => {
return (
<span
className="click-float"
key={floatItem.id}
style={{ left: floatItem.x, top: floatItem.y }}
>
{floatItem.text}
</span>
);
})}
</div>
<p className="click-power">
{"+"}
{formatNumber(clickPower)}
{" gold/click"}
</p>
<p className="early-access-notice">
{"⚠️ Early Access — this build is subject to change. "}
<strong>
{"All game progress WILL be reset upon v1.0.0 release."}
</strong>
</p>
</section>
);
};
export { ClickArea };