update nice visuel

This commit is contained in:
gpatruno
2026-07-25 20:04:34 +02:00
parent 3a8871cbae
commit 810e4ac922
78 changed files with 6742 additions and 3116 deletions
+145 -39
View File
@@ -35,10 +35,10 @@ export const api = {
catalog: () => request('/api/catalog'),
prestige: () => request('/api/prestige'),
doPrestige: () => request('/api/prestige', { method: 'POST', body: JSON.stringify({}) }),
prestigeUpgrade: (skillId) =>
prestigeUpgrade: (skillId, count = 1) =>
request('/api/prestige/upgrade', {
method: 'POST',
body: JSON.stringify({ skillId }),
body: JSON.stringify({ skillId, count }),
}),
openCase: (id, count = 1) =>
request(`/api/cases/${id}/open`, {
@@ -56,7 +56,6 @@ export const api = {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
vaultClaim: () => request('/api/vault/claim', { method: 'POST' }),
sellInventory: (inventoryItemIds) =>
request('/api/inventory/sell', {
method: 'POST',
@@ -83,41 +82,65 @@ export const api = {
method: 'POST',
body: JSON.stringify({ packageId }),
}),
shopOsuCheckout: (packId) =>
request('/api/shop/osu/checkout', {
method: 'POST',
body: JSON.stringify({ packId }),
}),
shopOsuConfirm: (orderId) =>
request('/api/shop/osu/confirm', {
method: 'POST',
body: JSON.stringify({ orderId }),
}),
shopAd: () => request('/api/shop/ad', { method: 'POST' }),
feed: () => request('/api/feed'),
feedAnnounce: (inventoryItemIds, caseName) => {
feedAnnounce: (inventoryItemIds, caseName, opts = {}) => {
const ids = Array.isArray(inventoryItemIds)
? inventoryItemIds
: [inventoryItemIds];
: inventoryItemIds != null
? [inventoryItemIds]
: [];
const body = {
inventoryItemIds: ids.filter(Boolean),
caseName,
respin: Boolean(opts.respin),
};
if (opts.snapshotBest) {
body.snapshotBest = opts.snapshotBest;
body.openCount = opts.openCount;
body.count = opts.count;
} else if (opts.snapshotItems) {
body.snapshotItems = opts.snapshotItems;
}
return request('/api/feed/announce', {
method: 'POST',
body: JSON.stringify({ inventoryItemIds: ids, caseName }),
body: JSON.stringify(body),
});
},
leaderboard: () => request('/api/leaderboard'),
jackpot: () => request('/api/jackpot'),
jackpotBet: (inventoryItemIds) =>
jackpotRound: (id) => request(`/api/jackpot/${id}`),
jackpotCreateBet: (inventoryItemIds) =>
request('/api/jackpot/bet', {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
jackpotWithdraw: (inventoryItemIds) =>
request('/api/jackpot/withdraw', {
jackpotBet: (id, inventoryItemIds) =>
request(`/api/jackpot/${id}/bet`, {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
duels: () => request('/api/duels'),
duel: (id) => request(`/api/duels/${id}`),
createDuel: (body) =>
request('/api/duels', { method: 'POST', body: JSON.stringify(body) }),
duelBet: (id, inventoryItemIds) =>
request(`/api/duels/${id}/bet`, {
jackpotWithdraw: (id, inventoryItemIds) =>
request(`/api/jackpot/${id}/withdraw`, {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
// legacy alias
jackpotCreate: (inventoryItemIds) =>
request('/api/jackpot/bet', {
method: 'POST',
body: JSON.stringify({ inventoryItemIds }),
}),
duelLeave: (id) => request(`/api/duels/${id}/leave`, { method: 'POST' }),
duelCancel: (id) => request(`/api/duels/${id}/cancel`, { method: 'POST' }),
duelStart: (id) => request(`/api/duels/${id}/start`, { method: 'POST' }),
profile: () => request('/api/profile'),
playerProfile: (username) =>
request(`/api/profile/player/${encodeURIComponent(username)}`),
@@ -140,6 +163,12 @@ export const api = {
body: JSON.stringify({ username, password }),
}),
adminStats: () => request('/api/admin/stats'),
adminPrestigeServerPreview: () => request('/api/admin/prestige-server'),
adminPrestigeServer: (body) =>
request('/api/admin/prestige-server', {
method: 'POST',
body: JSON.stringify(body),
}),
adminUsers: () => request('/api/admin/users'),
adminUpdateUser: (id, body) =>
request(`/api/admin/users/${id}`, {
@@ -150,6 +179,17 @@ export const api = {
adminItems: () => request('/api/admin/items'),
adminRarities: () => request('/api/admin/rarities'),
adminCategories: () => request('/api/admin/categories'),
adminShopOsuPacks: () => request('/api/admin/shop/osu-packs'),
adminShopOsuSection: (enabled) =>
request('/api/admin/shop/osu-section', {
method: 'PUT',
body: JSON.stringify({ enabled }),
}),
adminUpdateShopOsuPack: (id, body) =>
request(`/api/admin/shop/osu-packs/${encodeURIComponent(id)}`, {
method: 'PUT',
body: JSON.stringify(body),
}),
createCategory: (body) =>
request('/api/admin/categories', { method: 'POST', body: JSON.stringify(body) }),
updateCategory: (id, body) =>
@@ -186,17 +226,77 @@ export const api = {
};
/**
* Exact credit display (cents → "1234.56"), for tooltips / confirmations.
* Parse money amounts (cents) to BigInt — mirror of server `cents.js`.
* Accepts bigint, number, or decimal/int string. Truncates toward zero.
*/
export function toCentsBigInt(value) {
if (typeof value === 'bigint') return value;
if (value == null || value === '') return 0n;
if (typeof value === 'number') {
if (!Number.isFinite(value)) return 0n;
return BigInt(Math.trunc(value));
}
const s = String(value).trim();
if (!s) return 0n;
const m = s.match(/^([+-]?\d+)/);
if (!m) return 0n;
try {
return BigInt(m[1]);
} catch {
return 0n;
}
}
/** JSON-safe cents string. */
export function centsJson(value) {
return toCentsBigInt(value).toString();
}
/** Sort/compare helper: negative if a < b, 0 if equal, positive if a > b. */
export function compareCents(a, b) {
const aa = toCentsBigInt(a);
const bb = toCentsBigInt(b);
if (aa < bb) return -1;
if (aa > bb) return 1;
return 0;
}
/** 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('.');
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, '\u202f');
return `${neg ? '-' : ''}${grouped}.${dec}`;
let bi = toCentsBigInt(cents);
const neg = bi < 0n;
if (neg) bi = -bi;
const whole = bi / 100n;
const frac = (bi % 100n).toString().padStart(2, '0');
const grouped = whole.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '\u202f');
return `${neg ? '-' : ''}${grouped}.${frac}`;
}
/** Parse credits input (decimal string) → integer cents string. Null if invalid. */
export function creditsToCentsString(raw) {
if (raw == null || raw === '') return null;
const s = String(raw).trim().replace(',', '.');
const m = s.match(/^(\d+)(?:\.(\d{1,2})\d*)?$/);
if (!m) return null;
try {
const whole = BigInt(m[1]);
const frac = m[2] ? m[2].padEnd(2, '0') : '00';
return (whole * 100n + BigInt(frac)).toString();
} catch {
return null;
}
}
/** Cents → credits decimal string without Number precision loss. */
export function centsToCreditsString(cents) {
let bi = toCentsBigInt(cents);
const neg = bi < 0n;
const abs = neg ? -bi : bi;
const whole = abs / 100n;
const frac = (abs % 100n).toString().padStart(2, '0');
const body =
frac === '00' ? whole.toString() : `${whole}.${frac}`.replace(/0+$/, '').replace(/\.$/, '');
return `${neg ? '-' : ''}${body}`;
}
/** Alphabet suffixes after T: aa, ab, … az, ba, … (each step ×1000). */
@@ -225,25 +325,31 @@ function tierSuffix(tier) {
* 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;
let bi = toCentsBigInt(cents);
const neg = bi < 0n;
if (neg) bi = -bi;
if (credits < 1000) {
return `${neg ? '-' : ''}${credits.toFixed(2)}`;
const creditsWhole = bi / 100n;
const creditsFrac = bi % 100n;
if (creditsWhole < 1000n) {
return `${neg ? '-' : ''}${creditsWhole}.${creditsFrac.toString().padStart(2, '0')}`;
}
let tier = 0;
let scaled = credits;
while (scaled >= 1000 && tier < 1000) {
scaled /= 1000;
let scaled = creditsWhole;
let rem = 0n;
while (scaled >= 1000n) {
rem = scaled % 1000n;
scaled /= 1000n;
tier += 1;
}
const suffix = tierSuffix(tier);
const shown = String(Number(scaled.toFixed(2)));
return `${neg ? '-' : ''}${shown}${suffix}`;
// scaled < 1000 → safe as Number; rem gives ~3 decimal digits of precision
const approx = Number(scaled) + Number(rem) / 1000;
if (!Number.isFinite(approx)) return `${neg ? '-' : ''}0${tierSuffix(tier)}`;
const shown = String(Number(approx.toFixed(2)));
return `${neg ? '-' : ''}${shown}${tierSuffix(tier)}`;
}
export function formatChance(pct) {