Files
elysium/apps/web/src/components/game/upgradePanel.tsx
T
hikari 11e97325cb
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m2s
CI / Lint, Build & Test (push) Successful in 1m6s
feat: integrate art assets across all game panels (#43)
## Summary

- Adds `apps/web/src/utils/cdn.ts` with a `cdnImage(folder, id)` helper that builds URLs from `https://cdn.nhcarrigan.com/elysium/`
- Wires CDN art into all 13 game panels (bosses, quests, adventurers, companions, equipment, upgrades, prestige, transcendence, achievements, explorations, crafting, story, codex)
- Zone selector tabs now display 16:9 zone art thumbnails in place of emoji icons
- Adds a fixed background image at 15% opacity via `body::before`
- Documents the art generation and CDN upload process in `CLAUDE.md` for future expansions

Resolves #15

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #43
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-09 16:21:44 -07:00

288 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Upgrade panel component for purchasing game upgrades.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* 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 complexity -- UpgradeCard has many conditional render paths for states */
import { type JSX, useState } from "react";
import { useGame } from "../../context/gameContext.js";
import { cdnImage } from "../../utils/cdn.js";
import { LockToggle } from "../ui/lockToggle.js";
import type { Upgrade } from "@elysium/types";
interface UpgradeCardProperties {
readonly upgrade: Upgrade;
readonly currentGold: number;
readonly currentEssence: number;
readonly currentCrystals: number;
readonly unlockHint: string | undefined;
readonly formatNumber: (n: number)=> string;
}
/**
* Renders a single upgrade card.
* @param props - The upgrade card properties.
* @param props.upgrade - The upgrade data.
* @param props.currentGold - The current gold amount.
* @param props.currentEssence - The current essence amount.
* @param props.currentCrystals - The current crystals amount.
* @param props.unlockHint - Optional hint for how to unlock this upgrade.
* @param props.formatNumber - The number formatting utility function.
* @returns The JSX element.
*/
const UpgradeCard = ({
upgrade,
currentGold,
currentEssence,
currentCrystals,
unlockHint,
formatNumber,
}: UpgradeCardProperties): JSX.Element => {
const { buyUpgrade } = useGame();
const canAfford
= currentGold >= upgrade.costGold
&& currentEssence >= upgrade.costEssence
&& currentCrystals >= upgrade.costCrystals;
function handleBuy(): void {
buyUpgrade(upgrade.id);
}
if (upgrade.unlocked && upgrade.purchased) {
return (
<div className="upgrade-card purchased">
<img
alt={upgrade.name}
className="card-thumbnail"
src={cdnImage("upgrades", upgrade.id)}
/>
<span className="upgrade-name">
{"✅ "}
{upgrade.name}
</span>
<span className="upgrade-desc">{upgrade.description}</span>
</div>
);
}
if (upgrade.unlocked) {
return (
<div className="upgrade-card">
<img
alt={upgrade.name}
className="card-thumbnail"
src={cdnImage("upgrades", upgrade.id)}
/>
<div className="upgrade-info">
<h3>{upgrade.name}</h3>
<p>{upgrade.description}</p>
<p className="upgrade-multiplier">
{"×"}
{upgrade.multiplier}
{" multiplier"}
</p>
</div>
<div className="upgrade-cost">
{upgrade.costGold > 0
&& <span>
{"🪙 "}
{formatNumber(upgrade.costGold)}
</span>
}
{upgrade.costEssence > 0
&& <span>
{"✨ "}
{formatNumber(upgrade.costEssence)}
</span>
}
{upgrade.costCrystals > 0
&& <span>
{"💎 "}
{formatNumber(upgrade.costCrystals)}
</span>
}
</div>
<button
className="buy-button"
disabled={!canAfford}
onClick={handleBuy}
type="button"
>
{"Buy"}
</button>
</div>
);
}
return (
<div className="upgrade-card locked">
<img
alt={upgrade.name}
className="card-thumbnail"
src={cdnImage("upgrades", upgrade.id)}
/>
<div className="upgrade-info">
<h3>
{"🔒 "}
{upgrade.name}
</h3>
<p>{upgrade.description}</p>
<p className="upgrade-multiplier">
{"×"}
{upgrade.multiplier}
{" multiplier"}
</p>
</div>
<div className="upgrade-cost">
{upgrade.costGold > 0
&& <span>
{"🪙 "}
{formatNumber(upgrade.costGold)}
</span>
}
{upgrade.costEssence > 0
&& <span>
{"✨ "}
{formatNumber(upgrade.costEssence)}
</span>
}
{upgrade.costCrystals > 0
&& <span>
{"💎 "}
{formatNumber(upgrade.costCrystals)}
</span>
}
</div>
<span className="upgrade-locked-label">{"Locked"}</span>
{unlockHint === undefined
? null
: <p className="unlock-hint">{unlockHint}</p>
}
</div>
);
};
/**
* Renders the upgrade panel with all available, locked, and purchased upgrades.
* @returns The JSX element.
*/
const UpgradePanel = (): JSX.Element => {
const { state, formatNumber } = useGame();
const [ showLocked, setShowLocked ] = useState(true);
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { bosses, quests, upgrades, resources } = state;
const purchased = upgrades.filter((upgrade) => {
return upgrade.purchased;
});
const available = upgrades.filter((upgrade) => {
return upgrade.unlocked && !upgrade.purchased;
});
const locked = upgrades.filter((upgrade) => {
return !upgrade.unlocked;
});
const upgradeUnlockHints = new Map<string, string>();
for (const { upgradeRewards, name: bossName } of bosses) {
for (const upgradeId of upgradeRewards) {
upgradeUnlockHints.set(upgradeId, `⚔️ Defeat: ${bossName}`);
}
}
for (const { rewards, name: questName } of quests) {
for (const reward of rewards) {
if (
reward.type === "upgrade"
&& reward.targetId !== undefined
&& !upgradeUnlockHints.has(reward.targetId)
) {
upgradeUnlockHints.set(reward.targetId, `📜 Complete: ${questName}`);
}
}
}
function handleToggle(): void {
setShowLocked((current) => {
return !current;
});
}
return (
<section className="panel upgrade-panel">
<div className="panel-header">
<h2>{"Upgrades"}</h2>
<LockToggle
lockedCount={locked.length}
onToggle={handleToggle}
showLocked={showLocked}
/>
</div>
<p className="upgrade-progress">
{purchased.length}
{" / "}
{upgrades.length}
{" purchased"}
</p>
{upgrades.length === 0
? <p className="empty-state">
{"No upgrades available yet — keep adventuring!"}
</p>
: <div className="upgrade-list">
{available.map((upgrade) => {
return (
<UpgradeCard
currentCrystals={resources.crystals}
currentEssence={resources.essence}
currentGold={resources.gold}
formatNumber={formatNumber}
key={upgrade.id}
unlockHint={undefined}
upgrade={upgrade}
/>
);
})}
{purchased.map((upgrade) => {
return (
<UpgradeCard
currentCrystals={resources.crystals}
currentEssence={resources.essence}
currentGold={resources.gold}
formatNumber={formatNumber}
key={upgrade.id}
unlockHint={undefined}
upgrade={upgrade}
/>
);
})}
{showLocked
? locked.map((upgrade) => {
return (
<UpgradeCard
currentCrystals={resources.crystals}
currentEssence={resources.essence}
currentGold={resources.gold}
formatNumber={formatNumber}
key={upgrade.id}
unlockHint={upgradeUnlockHints.get(upgrade.id)}
upgrade={upgrade}
/>
);
})
: null}
</div>
}
</section>
);
};
export { UpgradePanel };