Files
CaseGambling/client/src/pages/PrestigePage.jsx
T
2026-07-19 00:29:46 +02:00

350 lines
11 KiB
React
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.
import { useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { api, formatCredits } from '../api';
import { useAuth } from '../AuthContext';
function formatSkillValue(skill, value) {
const prefix = skill.prefix || '';
if (skill.unit === 'cr-cents') {
return `${prefix}${formatCredits(value)} cr`.replace(/^\+\+/, '+');
}
if (skill.unit === '%') return `${prefix || '+'}${value}%`;
if (skill.unit === 's') return `${prefix || ''}${value}s`;
if (skill.unit === ' osu') return `${prefix || '+'}${value} osu`;
if (skill.prefix === '') return `${value}`;
if (value === 0 && !prefix) return `${value}`;
return `${prefix || '+'}${value}`;
}
export default function PrestigePage() {
const { user, loading, patchUser, refresh } = useAuth();
const [data, setData] = useState(null);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const [busy, setBusy] = useState(false);
const load = async () => {
const result = await api.prestige();
setData(result);
};
useEffect(() => {
if (!user || user.role === 'admin') return undefined;
let cancelled = false;
(async () => {
try {
const result = await api.prestige();
if (!cancelled) setData(result);
} catch (err) {
if (!cancelled) setError(err.message);
}
})();
return () => {
cancelled = true;
};
}, [user?.id, user?.role]);
if (loading) return <p className="muted">Loading</p>;
if (!user) return <Navigate to="/login" replace />;
if (user.role === 'admin') return <Navigate to="/admin" replace />;
if (!data && !error) return <p className="muted">Loading</p>;
if (data && data.pageVisible === false) {
return <Navigate to="/dashboard" replace />;
}
const cost = data?.prestige?.costCents ?? 0;
const p = data?.prestige;
const prBalance = p?.prBalance ?? user.prBalance ?? 0;
const canClick = Boolean(data?.canPrestige) && !busy;
const onPrestige = async () => {
if (!canClick) return;
setBusy(true);
setError('');
setMessage('');
try {
const result = await api.doPrestige();
patchUser(result.user);
await refresh();
setMessage(
`Prestige #${result.prestige.count} · +${result.prGranted} Pr. Balance, inventory, keys & catalog reset.`
);
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
const onUpgrade = async (skillId) => {
setBusy(true);
setError('');
setMessage('');
try {
const result = await api.prestigeUpgrade(skillId);
patchUser(result.user);
setMessage(`Upgraded ${skillId}.`);
setData((prev) =>
prev
? {
...prev,
prestige: result.prestige,
skills: result.skills,
}
: prev
);
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
const shortfall = Math.max(0, cost - (Number(user.balance) || 0));
const statusLabel = !data?.canAfford
? `Need ${formatCredits(shortfall)} cr more`
: data?.lockedItems > 0
? `Withdraw ${data.lockedItems} locked item${data.lockedItems === 1 ? '' : 's'} first`
: `Ready · ~${data.previewPr} Pr`;
return (
<section className="section prestige-page">
<div className="section-head prestige-head">
<div className="prestige-head-copy">
<h1>Prestige</h1>
<p className="prestige-lead">
Reset your progress for <strong className="gold">Pr</strong>, then spend Pr on permanent
skills.
</p>
</div>
<div className="prestige-pills">
{p && (
<div className="prestige-count-pill">
Prestige <strong>{p.count}</strong>
</div>
)}
<div className="prestige-pr-pill">
<strong>{prBalance}</strong> Pr
</div>
</div>
</div>
<ol className="prestige-steps">
<li>
<span className="prestige-step-num">1</span>
<div>
<strong>Pay the cost</strong>
<p className="muted">{formatCredits(cost)} cr from your balance</p>
</div>
</li>
<li>
<span className="prestige-step-num">2</span>
<div>
<strong>Earn Pr</strong>
<p className="muted">
+{p?.prBaseReward ?? 1000} base, more if you are over the cost
</p>
</div>
</li>
<li>
<span className="prestige-step-num">3</span>
<div>
<strong>Upgrade skills</strong>
<p className="muted">Permanent bonuses they survive every reset</p>
</div>
</li>
</ol>
{error && <div className="error">{error}</div>}
{message && <div className="ok-msg">{message}</div>}
{p && (
<div className="prestige-totals prestige-totals-wide">
<div>
<span className="muted">Fortune</span>
<strong>+{p.chanceBonus}%</strong>
</div>
<div>
<span className="muted">Yield</span>
<strong>+{p.gainBonus}%</strong>
</div>
<div>
<span className="muted">Polish</span>
<strong>+{p.wearBonus || 0}</strong>
</div>
<div>
<span className="muted">Bulk</span>
<strong>+{p.openBonus || 0}</strong>
</div>
<div>
<span className="muted">Discount</span>
<strong>{p.caseDiscount || 0}%</strong>
</div>
<div>
<span className="muted">Quality</span>
<strong>+{p.qualityBonus || 0}</strong>
</div>
<div>
<span className="muted">noTime</span>
<strong>{p.animReduction || 0}%</strong>
</div>
<div>
<span className="muted">Refund</span>
<strong>{p.refundChance || 0}%</strong>
</div>
<div>
<span className="muted">Respin</span>
<strong>{p.respinChance || 0}%</strong>
</div>
<div>
<span className="muted">Vault slots</span>
<strong>+{p.vaultSlots || 0}</strong>
</div>
<div>
<span className="muted">Offline Yield</span>
<strong>+{p.offlineYield || 0}%</strong>
</div>
<div>
<span className="muted">Online Yield</span>
<strong>+{p.onlineYield || 0}%</strong>
</div>
</div>
)}
{data?.lockedItems > 0 && (
<div className="error">
Withdraw {data.lockedItems} locked item{data.lockedItems === 1 ? '' : 's'} from battles
before prestiging.
</div>
)}
<div className="prestige-action admin-block">
<div className="prestige-action-head">
<h2>Reset now</h2>
<span className={`prestige-status${data?.canPrestige ? ' ready' : ''}`}>
{statusLabel}
</span>
</div>
<div className="prestige-action-stats">
<div>
<span className="muted">Cost</span>
<strong className="gold">{formatCredits(cost)} cr</strong>
</div>
<div>
<span className="muted">Your balance</span>
<strong>{formatCredits(user.balance)} cr</strong>
</div>
<div>
<span className="muted">Est. Pr reward</span>
<strong className="prestige-pr-est">
{data?.canAfford ? `~${data.previewPr} Pr` : '—'}
</strong>
</div>
</div>
<div className="prestige-action-split">
<div>
<h3>You keep</h3>
<ul>
<li>All Pr already earned</li>
<li>Skill-tree bonuses</li>
<li>Account &amp; prestige count</li>
</ul>
</div>
<div>
<h3>You lose</h3>
<ul>
<li>Balance</li>
<li>Inventory &amp; vault stakes</li>
<li>Case keys &amp; catalog progress</li>
</ul>
</div>
</div>
<p className="muted prestige-pr-hint">
Reward: +{p?.prBaseReward ?? 1000} Pr base. Every{' '}
{formatCredits(p?.excessChunkCents ?? 0)} cr above the cost adds ~{p?.prPerExcessChunk ?? 1000}{' '}
Pr.
</p>
<button
type="button"
className="btn prestige-action-btn"
disabled={!canClick}
onClick={onPrestige}
title={
!data?.canAfford
? `Need ${formatCredits(cost)} cr`
: data?.lockedItems > 0
? 'Withdraw locked items first'
: 'Prestige now'
}
>
{busy ? 'Prestiging…' : 'Prestige now'}
</button>
</div>
<div className="prestige-tree-section">
<div className="section-head">
<div>
<h2>Skill tree</h2>
<p className="muted">Spend Pr on permanent branches. Costs rise with each purchase.</p>
</div>
</div>
<div className="prestige-tree">
<div className="prestige-tree-root" aria-hidden>
<span>Core</span>
<strong>{prBalance} Pr</strong>
</div>
<div className="prestige-tree-branches">
{(data?.skills || []).map((skill) => (
<div key={skill.id} className="prestige-tree-branch">
<div className="prestige-tree-connector" aria-hidden />
<article className="prestige-skill-node">
<h3>{skill.title}</h3>
<p className="muted">{skill.description}</p>
<div className="prestige-skill-meta">
<span>
Lv {skill.level} · now{' '}
<strong>{formatSkillValue(skill, skill.current)}</strong>
</span>
<span className="prestige-delta">
{formatSkillValue(skill, skill.nextValue)}
</span>
</div>
<button
type="button"
className="btn secondary"
disabled={busy || !skill.canAfford}
onClick={() => onUpgrade(skill.id)}
>
Buy · {skill.cost} Pr
</button>
</article>
</div>
))}
</div>
</div>
</div>
{data?.history?.length > 0 && (
<div className="prestige-history">
<h2>History</h2>
<ul className="stat-list">
{data.history.map((h) => (
<li key={h.id}>
Prestige reset
{h.prGranted > 0 ? ` · +${h.prGranted} Pr` : ''}{' '}
<span className="muted">· {new Date(h.createdAt).toLocaleString()}</span>
</li>
))}
</ul>
</div>
)}
</section>
);
}