generated from nhcarrigan/template
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:
@@ -1,15 +1,25 @@
|
||||
import type { Boss } from "@elysium/types";
|
||||
import { useState } from "react";
|
||||
import { useGame } from "../../context/GameContext.js";
|
||||
import { formatNumber } from "../../utils/format.js";
|
||||
|
||||
interface BossCardProps {
|
||||
boss: Boss;
|
||||
prestigeCount: number;
|
||||
onChallenge: (bossId: string) => void;
|
||||
isChallenging: boolean;
|
||||
}
|
||||
|
||||
const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element => {
|
||||
const { attackBoss } = useGame();
|
||||
const BossCard = ({
|
||||
boss,
|
||||
prestigeCount,
|
||||
onChallenge,
|
||||
isChallenging,
|
||||
}: BossCardProps): React.JSX.Element => {
|
||||
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
||||
const isLocked = boss.prestigeRequirement > prestigeCount;
|
||||
const canChallenge =
|
||||
(boss.status === "available" || boss.status === "in_progress") && !isChallenging;
|
||||
|
||||
return (
|
||||
<div className={`boss-card boss-${boss.status}`}>
|
||||
@@ -17,7 +27,9 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element =>
|
||||
<h3>{boss.name}</h3>
|
||||
<p>{boss.description}</p>
|
||||
{isLocked && boss.status === "locked" && (
|
||||
<p className="prestige-lock">🔒 Requires Prestige {boss.prestigeRequirement}</p>
|
||||
<p className="prestige-lock">
|
||||
🔒 Requires Prestige {boss.prestigeRequirement}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -30,26 +42,40 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element =>
|
||||
/>
|
||||
</div>
|
||||
<span className="hp-text">
|
||||
{boss.currentHp.toLocaleString()} / {boss.maxHp.toLocaleString()} HP
|
||||
{formatNumber(boss.currentHp)} / {formatNumber(boss.maxHp)} HP
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="boss-rewards">
|
||||
<span>🪙 {boss.goldReward.toLocaleString()}</span>
|
||||
{boss.essenceReward > 0 && <span>✨ {boss.essenceReward.toLocaleString()}</span>}
|
||||
{boss.crystalReward > 0 && <span>💎 {boss.crystalReward.toLocaleString()}</span>}
|
||||
<div className="boss-meta">
|
||||
<span className="boss-dps">💢 Boss DPS: {formatNumber(boss.damagePerSecond)}</span>
|
||||
</div>
|
||||
|
||||
{boss.status === "available" || boss.status === "in_progress" ? (
|
||||
<div className="boss-rewards">
|
||||
<span>🪙 {formatNumber(boss.goldReward)}</span>
|
||||
{boss.essenceReward > 0 && (
|
||||
<span>✨ {formatNumber(boss.essenceReward)}</span>
|
||||
)}
|
||||
{boss.crystalReward > 0 && (
|
||||
<span>💎 {formatNumber(boss.crystalReward)}</span>
|
||||
)}
|
||||
{(boss.equipmentRewards ?? []).length > 0 && (
|
||||
<span>🗡️ {boss.equipmentRewards.length} Equipment</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(boss.status === "available" || boss.status === "in_progress") && (
|
||||
<button
|
||||
className="attack-button"
|
||||
onClick={() => { void attackBoss(boss.id); }}
|
||||
disabled={!canChallenge}
|
||||
onClick={() => {
|
||||
onChallenge(boss.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
⚔️ Attack
|
||||
{isChallenging ? "⚔️ Battling…" : "⚔️ Challenge"}
|
||||
</button>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{boss.status === "defeated" && (
|
||||
<span className="boss-badge defeated">☠️ Defeated</span>
|
||||
@@ -59,19 +85,81 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element =>
|
||||
};
|
||||
|
||||
export const BossPanel = (): React.JSX.Element => {
|
||||
const { state } = useGame();
|
||||
const { state, challengeBoss } = useGame();
|
||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||
|
||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||
|
||||
// Calculate party combat stats including equipment multiplier
|
||||
let globalMultiplier = 1;
|
||||
for (const upgrade of state.upgrades) {
|
||||
if (upgrade.purchased && upgrade.target === "global") {
|
||||
globalMultiplier *= upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
const prestigeMultiplier = 1 + state.prestige.count * 0.1;
|
||||
const equipmentCombatMultiplier = (state.equipment ?? [])
|
||||
.filter((e) => e.equipped && e.bonus.combatMultiplier != null)
|
||||
.reduce((mult, e) => mult * (e.bonus.combatMultiplier ?? 1), 1);
|
||||
|
||||
let partyDPS = 0;
|
||||
let partyHP = 0;
|
||||
for (const adventurer of state.adventurers) {
|
||||
if (adventurer.count === 0) continue;
|
||||
let adventurerMultiplier = 1;
|
||||
for (const upgrade of state.upgrades) {
|
||||
if (
|
||||
upgrade.purchased &&
|
||||
upgrade.target === "adventurer" &&
|
||||
upgrade.adventurerId === adventurer.id
|
||||
) {
|
||||
adventurerMultiplier *= upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
partyDPS +=
|
||||
adventurer.combatPower *
|
||||
adventurer.count *
|
||||
adventurerMultiplier *
|
||||
globalMultiplier *
|
||||
prestigeMultiplier;
|
||||
partyHP += adventurer.level * 50 * adventurer.count;
|
||||
}
|
||||
partyDPS *= equipmentCombatMultiplier;
|
||||
|
||||
const handleChallenge = async (bossId: string): Promise<void> => {
|
||||
setChallengingBossId(bossId);
|
||||
try {
|
||||
await challengeBoss(bossId);
|
||||
} finally {
|
||||
setChallengingBossId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel boss-panel">
|
||||
<h2>Boss Encounters</h2>
|
||||
|
||||
<div className="party-combat-stats">
|
||||
<div className="combat-stat">
|
||||
<span className="stat-label">⚔️ Party DPS</span>
|
||||
<span className="stat-value">{formatNumber(partyDPS)}</span>
|
||||
</div>
|
||||
<div className="combat-stat">
|
||||
<span className="stat-label">❤️ Party HP</span>
|
||||
<span className="stat-value">{formatNumber(partyHP)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="boss-list">
|
||||
{state.bosses.map((boss) => (
|
||||
<BossCard
|
||||
key={boss.id}
|
||||
boss={boss}
|
||||
isChallenging={challengingBossId === boss.id}
|
||||
prestigeCount={state.prestige.count}
|
||||
onChallenge={(id) => {
|
||||
void handleChallenge(id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user