cool prestige update

This commit is contained in:
gpatruno
2026-07-19 00:29:46 +02:00
parent d0f868ffa5
commit 3a8871cbae
27 changed files with 1165 additions and 353 deletions
+9 -7
View File
@@ -37,10 +37,12 @@ export default function AdminDrops() {
[cases, selectedCaseId]
);
const percents = useMemo(
() => (selected ? dropPercents(selected.items) : []),
[selected]
);
const poolItems = useMemo(() => {
if (!selected) return [];
return [...selected.items].sort((a, b) => Number(a.dropRate) - Number(b.dropRate));
}, [selected]);
const percents = useMemo(() => dropPercents(poolItems), [poolItems]);
return (
<div>
@@ -116,12 +118,12 @@ export default function AdminDrops() {
</div>
<div className="admin-block">
<h3>Pool ({selected.items.length})</h3>
{selected.items.length === 0 ? (
<h3>Pool ({poolItems.length})</h3>
{poolItems.length === 0 ? (
<div className="muted">No drops yet.</div>
) : (
<div className="drop-list">
{selected.items.map((ci, idx) => (
{poolItems.map((ci, idx) => (
<div key={ci.id} className="drop-row">
<span className="drop-item-cell">
{ci.item.imageUrl ? (
+65 -2
View File
@@ -46,6 +46,17 @@ export const api = {
body: JSON.stringify({ count }),
}),
inventory: () => request('/api/inventory'),
vaultStake: (inventoryItemIds) =>
request('/api/vault/stake', {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
vaultUnstake: (inventoryItemIds) =>
request('/api/vault/unstake', {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
vaultClaim: () => request('/api/vault/claim', { method: 'POST' }),
sellInventory: (inventoryItemIds) =>
request('/api/inventory/sell', {
method: 'POST',
@@ -174,8 +185,13 @@ export const api = {
},
};
export function formatCredits(cents) {
const fixed = (Number(cents) / 100).toFixed(2);
/**
* Exact credit display (cents → "1234.56"), for tooltips / confirmations.
*/
export function formatCreditsExact(cents) {
const n = Number(cents);
if (!Number.isFinite(n)) return '0.00';
const fixed = (n / 100).toFixed(2);
const neg = fixed.startsWith('-');
const body = neg ? fixed.slice(1) : fixed;
const [intPart, dec] = body.split('.');
@@ -183,6 +199,53 @@ export function formatCredits(cents) {
return `${neg ? '-' : ''}${grouped}.${dec}`;
}
/** Alphabet suffixes after T: aa, ab, … az, ba, … (each step ×1000). */
function alphabetSuffix(index) {
let num = Math.max(0, Math.floor(index));
let result = '';
do {
result = String.fromCharCode(97 + (num % 26)) + result;
num = Math.floor(num / 26);
} while (num > 0);
while (result.length < 2) result = `a${result}`;
return result;
}
function tierSuffix(tier) {
if (tier <= 0) return '';
if (tier === 1) return 'K';
if (tier === 2) return 'M';
if (tier === 3) return 'B';
if (tier === 4) return 'T';
return alphabetSuffix(tier - 5);
}
/**
* Compact credit display for UI readability.
* K / M / B / T, then aa, ab, … (×1000 each). Input is cents.
*/
export function formatCredits(cents) {
const n = Number(cents);
if (!Number.isFinite(n)) return '0.00';
const neg = n < 0;
const credits = Math.abs(n) / 100;
if (credits < 1000) {
return `${neg ? '-' : ''}${credits.toFixed(2)}`;
}
let tier = 0;
let scaled = credits;
while (scaled >= 1000 && tier < 1000) {
scaled /= 1000;
tier += 1;
}
const suffix = tierSuffix(tier);
const shown = String(Number(scaled.toFixed(2)));
return `${neg ? '-' : ''}${shown}${suffix}`;
}
export function formatChance(pct) {
return `${Number(pct || 0).toFixed(1)}%`;
}
+11 -4
View File
@@ -3,10 +3,15 @@ import { rarityColor } from '../rarities';
const ITEM_WIDTH = 120;
const ITEM_WIDTH_COMPACT = 72;
const SPIN_DURATION_MS = 5500;
const BASE_SPIN_DURATION_MS = 5500;
const SPIN_SLOTS = 42;
const TAIL_SLOTS = 8;
export function spinDurationMs(animReduction = 0) {
const pct = Math.max(0, Math.min(90, Number(animReduction) || 0));
return Math.max(1000, Math.round(BASE_SPIN_DURATION_MS * (1 - pct / 100)));
}
function pickRandom(items, avoidId) {
if (!items.length) return null;
if (items.length === 1) return items[0];
@@ -78,6 +83,7 @@ export default function CaseReel({
onDone,
showOwners = false,
compact = false,
animReduction = 0,
}) {
const trackRef = useRef(null);
const [offset, setOffset] = useState(0);
@@ -85,6 +91,7 @@ export default function CaseReel({
const onDoneRef = useRef(onDone);
onDoneRef.current = onDone;
const itemWidth = compact ? ITEM_WIDTH_COMPACT : ITEM_WIDTH;
const durationMs = spinDurationMs(animReduction);
const spinKey =
winnerId && items?.length ? `${winnerId}:${items.map((i) => i.id).join(',')}` : '';
@@ -122,7 +129,7 @@ export default function CaseReel({
if (finished) return;
finished = true;
onDoneRef.current?.();
}, SPIN_DURATION_MS + 80);
}, durationMs + 80);
return () => {
cancelAnimationFrame(id);
@@ -131,7 +138,7 @@ export default function CaseReel({
};
// intentionally only re-run when a new spin starts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [spinning, winnerId, itemWidth]);
}, [spinning, winnerId, itemWidth, durationMs]);
return (
<div
@@ -144,7 +151,7 @@ export default function CaseReel({
style={{
transform: `translateX(-${offset}px)`,
transition: spinning
? `transform ${SPIN_DURATION_MS}ms cubic-bezier(0.12, 0.75, 0.12, 1)`
? `transform ${durationMs}ms cubic-bezier(0.12, 0.75, 0.12, 1)`
: 'none',
}}
>
+16 -5
View File
@@ -1,4 +1,4 @@
import { formatCredits } from '../api';
import { formatCredits, formatCreditsExact } from '../api';
import { rarityColor } from '../rarities';
/** Instance sell/bet price, else catalog FT reference */
@@ -22,6 +22,7 @@ export default function ItemTile({
showWear = true,
selected = false,
favorite = false,
staked = false,
onClick,
disabled = false,
}) {
@@ -37,16 +38,23 @@ export default function ItemTile({
}`
: wear || undefined;
const titleParts = [];
if (staked) titleParts.push('Vault');
if (favorite) titleParts.push('Favorite');
if (floatTitle) titleParts.push(floatTitle);
else titleParts.push(item.name);
if (showValue) titleParts.push(`${formatCreditsExact(value)} cr`);
return (
<div
className={`item-tile${selected ? ' selected' : ''}${favorite ? ' favorite' : ''}${
clickable ? ' selectable' : ''
}${disabled ? ' disabled' : ''}`}
staked ? ' staked' : ''
}${clickable ? ' selectable' : ''}${disabled ? ' disabled' : ''}`}
style={{ borderTopColor: color }}
onClick={disabled ? undefined : onClick}
role={clickable ? 'button' : undefined}
tabIndex={clickable && !disabled ? 0 : undefined}
title={favorite ? `Favorite · ${floatTitle || item.name}` : floatTitle}
title={titleParts.join(' · ')}
onKeyDown={
clickable && !disabled
? (e) => {
@@ -60,6 +68,7 @@ export default function ItemTile({
>
<div className={`item-visual${hasImage ? ' has-img' : ''}`} style={{ color }}>
{favorite ? <span className="item-favorite-badge" aria-label="Favorite"></span> : null}
{staked ? <span className="item-vault-badge" aria-label="Vault">V</span> : null}
{hasImage ? (
<img src={item.imageUrl} alt="" loading="lazy" />
) : (
@@ -76,7 +85,9 @@ export default function ItemTile({
</div>
) : null}
{showValue && (
<div className="item-meta">{formatCredits(value)} cr</div>
<div className="item-meta" title={`${formatCreditsExact(value)} cr`}>
{formatCredits(value)} cr
</div>
)}
</div>
);
+12 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { NavLink, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../AuthContext';
import { api, formatCredits } from '../api';
import { api, formatCredits, formatCreditsExact } from '../api';
import DropFeed from './DropFeed';
import SessionLimitOverlay from './SessionLimitOverlay';
import { onInventoryChanged } from '../inventoryEvents';
@@ -213,11 +213,20 @@ export default function Layout() {
{user?.role === 'admin' && <NavLink to="/admin">Admin</NavLink>}
{user && (
isPlayer ? (
<NavLink to="/shop" className="balance-pill balance-pill-link">
<NavLink
to="/shop"
className="balance-pill balance-pill-link"
title={`${formatCreditsExact(user.balance)} cr`}
>
{formatCredits(user.balance)} cr
</NavLink>
) : (
<span className="balance-pill">{formatCredits(user.balance)} cr</span>
<span
className="balance-pill"
title={`${formatCreditsExact(user.balance)} cr`}
>
{formatCredits(user.balance)} cr
</span>
)
)}
{user ? (
+52 -60
View File
@@ -1,8 +1,54 @@
import { formatCredits } from '../api';
export default function ProfilePrestigeStats({ prestige }) {
if (!prestige || !prestige.count) return null;
const rows = [
typeof prestige.prBalance === 'number' && prestige.prBalance > 0
? { label: 'Pr', value: <strong>{prestige.prBalance}</strong> }
: null,
{ label: 'Fortune', value: <strong>+{prestige.chanceBonus}%</strong> },
{ label: 'Yield', value: <strong>+{prestige.gainBonus}%</strong> },
(prestige.wearBonus || 0) > 0 && {
label: 'Polish',
value: <strong>+{prestige.wearBonus}</strong>,
},
(prestige.openBonus || 0) > 0 && {
label: 'Bulk',
value: <strong>+{prestige.openBonus} opens</strong>,
},
(prestige.caseDiscount || 0) > 0 && {
label: 'Discount',
value: <strong>{prestige.caseDiscount}%</strong>,
},
(prestige.qualityBonus || 0) > 0 && {
label: 'Quality',
value: <strong>+{prestige.qualityBonus}</strong>,
},
(prestige.animReduction || 0) > 0 && {
label: 'noTime',
value: <strong>{prestige.animReduction}%</strong>,
},
(prestige.refundChance || 0) > 0 && {
label: 'Refund',
value: <strong>{prestige.refundChance}%</strong>,
},
(prestige.respinChance || 0) > 0 && {
label: 'Respin',
value: <strong>{prestige.respinChance}%</strong>,
},
(prestige.vaultSlots || 0) > 0 && {
label: 'Vault Capacity',
value: <strong>+{prestige.vaultSlots}</strong>,
},
(prestige.offlineYield || 0) > 0 && {
label: 'Offline Yield',
value: <strong>+{prestige.offlineYield}%</strong>,
},
(prestige.onlineYield || 0) > 0 && {
label: 'Online Yield',
value: <strong>+{prestige.onlineYield}%</strong>,
},
].filter(Boolean);
return (
<div className="prestige-profile-block">
<div className="catalog-badges-head">
@@ -10,65 +56,11 @@ export default function ProfilePrestigeStats({ prestige }) {
<span className="catalog-badges-luck">×{prestige.count}</span>
</div>
<ul className="stat-list prestige-profile-stats">
{typeof prestige.prBalance === 'number' && prestige.prBalance > 0 && (
<li>
Pr <strong>{prestige.prBalance}</strong>
{rows.map((row) => (
<li key={row.label}>
{row.label} {row.value}
</li>
)}
<li>
Chance <strong>+{prestige.chanceBonus}%</strong>
</li>
<li>
Sell gains <strong>+{prestige.gainBonus}%</strong>
</li>
<li>
Key opens <strong>{prestige.keyOpenReduction}</strong>
</li>
{(prestige.startBonus || 0) > 0 && (
<li>
Vault <strong>{formatCredits(prestige.startBonus)} cr</strong>
</li>
)}
{(prestige.adBonus || 0) > 0 && (
<li>
Ad reward <strong>+{prestige.adBonus}%</strong>
</li>
)}
{(prestige.adCooldownReduction || 0) > 0 && (
<li>
Ad cooldown <strong>{prestige.adCooldownReduction}s</strong>
</li>
)}
{(prestige.wearBonus || 0) > 0 && (
<li>
Polish <strong>+{prestige.wearBonus}</strong>
</li>
)}
{(prestige.prBonus || 0) > 0 && (
<li>
Pr echo <strong>+{prestige.prBonus}%</strong>
</li>
)}
{(prestige.shopDiscount || 0) > 0 && (
<li>
Haggle <strong>{prestige.shopDiscount} osu</strong>
</li>
)}
{(prestige.startOsu || 0) > 0 && (
<li>
Purse <strong>+{prestige.startOsu} osu</strong>
</li>
)}
{(prestige.dropValueBonus || 0) > 0 && (
<li>
Magnet <strong>+{prestige.dropValueBonus}%</strong>
</li>
)}
{(prestige.openBonus || 0) > 0 && (
<li>
Bulk <strong>+{prestige.openBonus} opens</strong>
</li>
)}
))}
</ul>
</div>
);
+42
View File
@@ -1841,6 +1841,48 @@ a.balance-pill-link.active {
pointer-events: none;
}
.item-vault-badge {
position: absolute;
top: 0.35rem;
left: 0.35rem;
z-index: 1;
width: 1.15rem;
height: 1.15rem;
display: grid;
place-items: center;
border-radius: 0.2rem;
background: rgba(80, 160, 220, 0.35);
color: #9fd4ff;
font-size: 0.7rem;
font-weight: 700;
line-height: 1;
pointer-events: none;
}
.item-tile.staked {
box-shadow: inset 0 0 0 1px rgba(80, 160, 220, 0.4);
}
.vault-panel {
margin: 1.25rem 0 1.5rem;
}
.vault-panel .section-head {
margin-bottom: 0.75rem;
}
.vault-stats {
display: flex;
flex-wrap: wrap;
gap: 1rem 1.5rem;
margin-bottom: 0.85rem;
font-size: 0.92rem;
}
.vault-grid {
margin-bottom: 0.5rem;
}
.item-tile.disabled {
opacity: 0.4;
pointer-events: none;
+17 -2
View File
@@ -27,6 +27,8 @@ export default function CasePage() {
const [error, setError] = useState('');
const [keyMessage, setKeyMessage] = useState('');
const [catalogMessage, setCatalogMessage] = useState('');
const [openBonusMessage, setOpenBonusMessage] = useState('');
const [animReduction, setAnimReduction] = useState(0);
const [quantity, setQuantity] = useState(1);
const [opening, setOpening] = useState(false);
const [spins, setSpins] = useState([]);
@@ -243,6 +245,7 @@ export default function CasePage() {
setError('');
setKeyMessage('');
setCatalogMessage('');
setOpenBonusMessage('');
setSpins([]);
setOpening(true);
announcedRef.current = false;
@@ -253,6 +256,13 @@ export default function CasePage() {
updateBalance(result.user.balance);
notifyInventoryChanged();
persistQty(crate.id, quantity);
if (result.animReduction != null) {
setAnimReduction(result.animReduction);
}
const bonusParts = [];
if (result.refunded) bonusParts.push('Refund! Case cost returned');
if (result.respin) bonusParts.push('Respin! All drops re-rolled');
if (bonusParts.length) setOpenBonusMessage(bonusParts.join(' · '));
if (result.keyProgress) {
setCrate((prev) => (prev ? { ...prev, keyProgress: result.keyProgress } : prev));
}
@@ -352,14 +362,18 @@ export default function CasePage() {
{error && <div className="error">{error}</div>}
{keyMessage && <div className="ok-msg">{keyMessage}</div>}
{catalogMessage && <div className="ok-msg">{catalogMessage}</div>}
{openBonusMessage && <div className="ok-msg">{openBonusMessage}</div>}
{crate && (
<>
<div className="case-hero">
<h1>{crate.name}</h1>
<p className="muted">
Cost {formatCredits(crate.price)} cr each · Balance{' '}
{formatCredits(user.balance)} cr
Cost {formatCredits(crate.price)} cr each
{crate.basePrice != null && Number(crate.basePrice) !== Number(crate.price)
? ` (was ${formatCredits(crate.basePrice)})`
: ''}{' '}
· Balance {formatCredits(user.balance)} cr
{crate.catalogLuckPercent > 0
? ` · Catalog luck +${crate.catalogLuckPercent}%`
: ''}
@@ -435,6 +449,7 @@ export default function CasePage() {
winnerId={spin.item.id}
spinning={spin.spinning}
onDone={() => onReelDone(spin.key)}
animReduction={animReduction}
/>
{spin.done && (
<div
+157 -3
View File
@@ -11,6 +11,8 @@ const PAGE_SIZE = 50;
export default function Dashboard() {
const { user, loading, patchUser } = useAuth();
const [inventory, setInventory] = useState([]);
const [vault, setVault] = useState(null);
const [vaultMessage, setVaultMessage] = useState('');
const [error, setError] = useState('');
const [sort, setSort] = useState('value');
const [page, setPage] = useState(1);
@@ -37,6 +39,7 @@ export default function Dashboard() {
const load = async () => {
const inv = await api.inventory();
setInventory(inv.inventory);
setVault(inv.vault || null);
};
useEffect(() => {
@@ -109,6 +112,7 @@ export default function Dashboard() {
return inventory.filter(
(inv) =>
!inv.favorite &&
!inv.staked &&
(inv.valueCents ?? inv.item?.valueCents ?? 0) < thresholdCents
);
}, [inventory, thresholdCents]);
@@ -118,10 +122,17 @@ export default function Dashboard() {
[inventory, selected]
);
const stakedItems = useMemo(() => inventory.filter((inv) => inv.staked), [inventory]);
const selectedHasFavorite = selectedItems.some((inv) => inv.favorite);
const selectedAllFavorite =
selectedItems.length > 0 && selectedItems.every((inv) => inv.favorite);
const selectedCanSell = selectedItems.some((inv) => !inv.favorite);
const selectedCanSell = selectedItems.some((inv) => !inv.favorite && !inv.staked);
const selectedStaked = selectedItems.filter((inv) => inv.staked);
const selectedUnstaked = selectedItems.filter((inv) => !inv.staked);
const vaultFree = Math.max(0, (vault?.slots ?? 3) - (vault?.used ?? 0));
const canStake = selectedUnstaked.length > 0 && selectedUnstaked.length <= vaultFree;
const canUnstake = selectedStaked.length > 0;
const belowValue = useMemo(
() =>
@@ -155,11 +166,78 @@ export default function Dashboard() {
};
const sell = async () => {
const ids = selectedItems.filter((inv) => !inv.favorite).map((inv) => inv.id);
const ids = selectedItems
.filter((inv) => !inv.favorite && !inv.staked)
.map((inv) => inv.id);
if (!ids.length) return;
await sellIds(ids);
};
const stakeSelected = async () => {
const ids = selectedUnstaked.map((inv) => inv.id);
if (!ids.length) return;
setBusy(true);
setError('');
setVaultMessage('');
try {
const data = await api.vaultStake(ids);
if (data.user) patchUser({ balance: data.user.balance });
setSelected([]);
setVaultMessage(`Staked ${ids.length} item${ids.length === 1 ? '' : 's'} in vault`);
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
const unstakeSelected = async () => {
const ids = selectedStaked.map((inv) => inv.id);
if (!ids.length) return;
setBusy(true);
setError('');
setVaultMessage('');
try {
const data = await api.vaultUnstake(ids);
if (data.user) patchUser({ balance: data.user.balance });
setSelected([]);
const yieldPart =
data.yieldCredited > 0
? ` · +${formatCredits(data.yieldCredited)} cr yield`
: '';
setVaultMessage(
`Unstaked ${ids.length} item${ids.length === 1 ? '' : 's'}${yieldPart}`
);
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
const claimVault = async () => {
setBusy(true);
setError('');
setVaultMessage('');
try {
const data = await api.vaultClaim();
if (data.user) patchUser({ balance: data.user.balance });
if (data.vault) setVault(data.vault);
setVaultMessage(
data.credited > 0
? `Vault yield +${formatCredits(data.credited)} cr`
: 'No vault yield ready yet'
);
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
const setFavorite = async (favorite) => {
if (!selected.length) return;
setBusy(true);
@@ -303,10 +381,34 @@ export default function Dashboard() {
className="btn"
onClick={sell}
disabled={busy || !selectedCanSell}
title={selectedHasFavorite ? 'Favorites cannot be sold' : undefined}
title={
selectedHasFavorite || selectedStaked.length
? 'Favorites and vault items cannot be sold'
: undefined
}
>
Sell
</button>
{canStake && (
<button
type="button"
className="btn secondary"
onClick={stakeSelected}
disabled={busy}
>
Stake ({selectedUnstaked.length})
</button>
)}
{canUnstake && (
<button
type="button"
className="btn secondary"
onClick={unstakeSelected}
disabled={busy}
>
Unstake ({selectedStaked.length})
</button>
)}
</>
)}
</div>
@@ -381,6 +483,57 @@ export default function Dashboard() {
</div>
{error && <div className="error">{error}</div>}
{vaultMessage && <div className="ok-msg">{vaultMessage}</div>}
<div className="vault-panel admin-block">
<div className="section-head">
<div>
<h2>Vault</h2>
<p className="muted">
Stake up to {vault?.slots ?? 3} items. Online: 10% / 10 min · Offline: 1% / 10 min
(scales with wear &amp; rarity).
</p>
</div>
<button
type="button"
className="btn secondary"
onClick={claimVault}
disabled={busy || !(vault?.used > 0)}
>
Claim yield
</button>
</div>
<div className="vault-stats">
<span>
Slots <strong>{vault?.used ?? 0}/{vault?.slots ?? 3}</strong>
</span>
<span>
Online / tick{' '}
<strong className="gold">{formatCredits(vault?.onlinePerTick ?? 0)} cr</strong>
</span>
<span>
Offline / tick{' '}
<strong>{formatCredits(vault?.offlinePerTick ?? 0)} cr</strong>
</span>
</div>
{stakedItems.length > 0 ? (
<div className="grid vault-grid">
{stakedItems.map((inv) => (
<ItemTile
key={`vault-${inv.id}`}
item={inv.item}
favorite={inv.favorite}
staked
selected={selected.includes(inv.id)}
onClick={() => toggle(inv.id)}
/>
))}
</div>
) : (
<p className="muted">No items staked select items below and Stake.</p>
)}
</div>
{inventory.length === 0 ? (
<div className="empty">
Empty for now {' '}
@@ -397,6 +550,7 @@ export default function Dashboard() {
key={inv.id}
item={inv.item}
favorite={inv.favorite}
staked={inv.staked}
selected={selected.includes(inv.id)}
onClick={() => toggle(inv.id)}
/>
+36 -36
View File
@@ -162,53 +162,53 @@ export default function PrestigePage() {
{p && (
<div className="prestige-totals prestige-totals-wide">
<div>
<span className="muted">Chance</span>
<span className="muted">Fortune</span>
<strong>+{p.chanceBonus}%</strong>
</div>
<div>
<span className="muted">Gains</span>
<span className="muted">Yield</span>
<strong>+{p.gainBonus}%</strong>
</div>
<div>
<span className="muted">Key opens</span>
<strong>{p.keyOpenReduction}</strong>
</div>
<div>
<span className="muted">Vault</span>
<strong>{formatCredits(p.startBonus || 0)} cr</strong>
</div>
<div>
<span className="muted">Ad reward</span>
<strong>+{p.adBonus || 0}%</strong>
</div>
<div>
<span className="muted">Ad cooldown</span>
<strong>{p.adCooldownReduction || 0}s</strong>
</div>
<div>
<span className="muted">Polish</span>
<strong>+{p.wearBonus || 0}</strong>
</div>
<div>
<span className="muted">Pr echo</span>
<strong>+{p.prBonus || 0}%</strong>
</div>
<div>
<span className="muted">Haggle</span>
<strong>{p.shopDiscount || 0} osu</strong>
</div>
<div>
<span className="muted">Purse</span>
<strong>+{p.startOsu || 0} osu</strong>
</div>
<div>
<span className="muted">Magnet</span>
<strong>+{p.dropValueBonus || 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>
)}
@@ -256,8 +256,8 @@ export default function PrestigePage() {
<div>
<h3>You lose</h3>
<ul>
<li>Balance (minus Vault / Purse)</li>
<li>Inventory</li>
<li>Balance</li>
<li>Inventory &amp; vault stakes</li>
<li>Case keys &amp; catalog progress</li>
</ul>
</div>