first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
@@ -0,0 +1,54 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import session from 'express-session';
|
||||
|
||||
import authRoutes from './routes/auth.js';
|
||||
import casesRoutes from './routes/cases.js';
|
||||
import inventoryRoutes from './routes/inventory.js';
|
||||
import leaderboardRoutes from './routes/leaderboard.js';
|
||||
import adminRoutes from './routes/admin.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = Number(process.env.PORT || 3001);
|
||||
const CLIENT_ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:5173';
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: CLIENT_ORIGIN,
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
app.use(express.json());
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET || 'dev-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: false,
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/cases', casesRoutes);
|
||||
app.use('/api/inventory', inventoryRoutes);
|
||||
app.use('/api/leaderboard', leaderboardRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`API listening on http://localhost:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
export function requireAuth(req, res, next) {
|
||||
if (!req.session?.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export function requireAdmin(req, res, next) {
|
||||
if (!req.session?.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
if (req.session.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
balance: user.balance,
|
||||
role: user.role,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const RARITIES = [
|
||||
'Consumer',
|
||||
'Industrial',
|
||||
'MilSpec',
|
||||
'Restricted',
|
||||
'Classified',
|
||||
'Covert',
|
||||
'Extraordinary',
|
||||
];
|
||||
|
||||
export const RARITY_COLORS = {
|
||||
Consumer: '#b0c3d9',
|
||||
Industrial: '#5e98d9',
|
||||
MilSpec: '#4b69ff',
|
||||
Restricted: '#8847ff',
|
||||
Classified: '#d32ce6',
|
||||
Covert: '#eb4b4b',
|
||||
Extraordinary: '#ffd700',
|
||||
};
|
||||
|
||||
export function isValidRarity(rarity) {
|
||||
return RARITIES.includes(rarity);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { prisma } from '../db.js';
|
||||
import { requireAdmin, publicUser } from '../middleware.js';
|
||||
import { isValidRarity, RARITIES } from '../rarities.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const username = String(req.body.username || '').trim();
|
||||
const password = String(req.body.password || '');
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user || user.role !== 'admin') {
|
||||
return res.status(401).json({ error: 'Invalid admin credentials' });
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!ok) {
|
||||
return res.status(401).json({ error: 'Invalid admin credentials' });
|
||||
}
|
||||
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
|
||||
res.json({ user: publicUser(user) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Admin login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/rarities', requireAdmin, (_req, res) => {
|
||||
res.json({ rarities: RARITIES });
|
||||
});
|
||||
|
||||
// —— Cases ——
|
||||
router.get('/cases', requireAdmin, async (_req, res) => {
|
||||
try {
|
||||
const cases = await prisma.case.findMany({
|
||||
include: {
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
res.json({
|
||||
cases: cases.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
imageUrl: c.imageUrl,
|
||||
price: c.price,
|
||||
active: c.active,
|
||||
items: c.items.map((ci) => ({
|
||||
id: ci.id,
|
||||
dropRate: ci.dropRate,
|
||||
item: {
|
||||
id: ci.item.id,
|
||||
name: ci.item.name,
|
||||
rarity: ci.item.rarity,
|
||||
marketValue: ci.item.marketValue,
|
||||
imageUrl: ci.item.imageUrl,
|
||||
},
|
||||
})),
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load cases' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/cases', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
const imageUrl = String(req.body.imageUrl || '');
|
||||
const price = Number(req.body.price);
|
||||
const active = req.body.active !== false;
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Name is required' });
|
||||
if (!Number.isInteger(price) || price < 0) {
|
||||
return res.status(400).json({ error: 'Price must be a non-negative integer (cents)' });
|
||||
}
|
||||
|
||||
const created = await prisma.case.create({
|
||||
data: { name, imageUrl, price, active },
|
||||
});
|
||||
res.status(201).json({ case: created });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to create case' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/cases/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const data = {};
|
||||
if (req.body.name !== undefined) data.name = String(req.body.name).trim();
|
||||
if (req.body.imageUrl !== undefined) data.imageUrl = String(req.body.imageUrl);
|
||||
if (req.body.price !== undefined) {
|
||||
const price = Number(req.body.price);
|
||||
if (!Number.isInteger(price) || price < 0) {
|
||||
return res.status(400).json({ error: 'Invalid price' });
|
||||
}
|
||||
data.price = price;
|
||||
}
|
||||
if (req.body.active !== undefined) data.active = Boolean(req.body.active);
|
||||
|
||||
const updated = await prisma.case.update({ where: { id }, data });
|
||||
res.json({ case: updated });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to update case' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/cases/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await prisma.case.delete({ where: { id } });
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to delete case' });
|
||||
}
|
||||
});
|
||||
|
||||
// —— Items ——
|
||||
router.get('/items', requireAdmin, async (_req, res) => {
|
||||
try {
|
||||
const items = await prisma.item.findMany({ orderBy: { id: 'asc' } });
|
||||
res.json({ items });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load items' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/items', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
const imageUrl = String(req.body.imageUrl || '');
|
||||
const rarity = String(req.body.rarity || '');
|
||||
const marketValue = Number(req.body.marketValue);
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Name is required' });
|
||||
if (!isValidRarity(rarity)) {
|
||||
return res.status(400).json({ error: `Invalid rarity. Use: ${RARITIES.join(', ')}` });
|
||||
}
|
||||
if (!Number.isInteger(marketValue) || marketValue < 0) {
|
||||
return res.status(400).json({ error: 'marketValue must be a non-negative integer (cents)' });
|
||||
}
|
||||
|
||||
const created = await prisma.item.create({
|
||||
data: { name, imageUrl, rarity, marketValue },
|
||||
});
|
||||
res.status(201).json({ item: created });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to create item' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/items/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const data = {};
|
||||
if (req.body.name !== undefined) data.name = String(req.body.name).trim();
|
||||
if (req.body.imageUrl !== undefined) data.imageUrl = String(req.body.imageUrl);
|
||||
if (req.body.rarity !== undefined) {
|
||||
if (!isValidRarity(req.body.rarity)) {
|
||||
return res.status(400).json({ error: 'Invalid rarity' });
|
||||
}
|
||||
data.rarity = req.body.rarity;
|
||||
}
|
||||
if (req.body.marketValue !== undefined) {
|
||||
const marketValue = Number(req.body.marketValue);
|
||||
if (!Number.isInteger(marketValue) || marketValue < 0) {
|
||||
return res.status(400).json({ error: 'Invalid marketValue' });
|
||||
}
|
||||
data.marketValue = marketValue;
|
||||
}
|
||||
|
||||
const updated = await prisma.item.update({ where: { id }, data });
|
||||
res.json({ item: updated });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to update item' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/items/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await prisma.item.delete({ where: { id } });
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to delete item' });
|
||||
}
|
||||
});
|
||||
|
||||
// —— Case drops ——
|
||||
router.post('/cases/:caseId/items', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const caseId = Number(req.params.caseId);
|
||||
const itemId = Number(req.body.itemId);
|
||||
const dropRate = Number(req.body.dropRate);
|
||||
|
||||
if (!Number.isInteger(itemId)) {
|
||||
return res.status(400).json({ error: 'itemId required' });
|
||||
}
|
||||
if (!(dropRate > 0)) {
|
||||
return res.status(400).json({ error: 'dropRate must be > 0' });
|
||||
}
|
||||
|
||||
const created = await prisma.caseItem.create({
|
||||
data: { caseId, itemId, dropRate },
|
||||
include: { item: true },
|
||||
});
|
||||
res.status(201).json({ caseItem: created });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.code === 'P2002') {
|
||||
return res.status(409).json({ error: 'Item already in this case' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to add case item' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/case-items/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const dropRate = Number(req.body.dropRate);
|
||||
if (!(dropRate > 0)) {
|
||||
return res.status(400).json({ error: 'dropRate must be > 0' });
|
||||
}
|
||||
const updated = await prisma.caseItem.update({
|
||||
where: { id },
|
||||
data: { dropRate },
|
||||
include: { item: true },
|
||||
});
|
||||
res.json({ caseItem: updated });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to update drop rate' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/case-items/:id', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await prisma.caseItem.delete({ where: { id } });
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to remove case item' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { prisma } from '../db.js';
|
||||
import { publicUser, requireAuth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
const STARTING_BALANCE = Number(process.env.STARTING_BALANCE || 10000);
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const username = String(req.body.username || '').trim();
|
||||
const password = String(req.body.password || '');
|
||||
|
||||
if (username.length < 3 || username.length > 32) {
|
||||
return res.status(400).json({ error: 'Username must be 3–32 characters' });
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
return res.status(400).json({ error: 'Username may only contain letters, numbers, and underscores' });
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { username } });
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Username already taken' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
passwordHash,
|
||||
balance: STARTING_BALANCE,
|
||||
role: 'user',
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.transaction.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
type: 'seed_credit',
|
||||
amount: STARTING_BALANCE,
|
||||
meta: JSON.stringify({ reason: 'starting_balance' }),
|
||||
},
|
||||
});
|
||||
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
|
||||
res.status(201).json({ user: publicUser(user) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Registration failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const username = String(req.body.username || '').trim();
|
||||
const password = String(req.body.password || '');
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!ok) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
|
||||
res.json({ user: publicUser(user) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/me', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({ where: { id: req.session.userId } });
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
res.json({ user: publicUser(user) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load user' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../db.js';
|
||||
import { requireAuth, publicUser } from '../middleware.js';
|
||||
import { normalizeRates, pickWeighted } from '../weighted.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function formatCase(c, includeRates = false) {
|
||||
const items = (c.items || []).map((ci) => ({
|
||||
id: ci.item.id,
|
||||
name: ci.item.name,
|
||||
imageUrl: ci.item.imageUrl,
|
||||
rarity: ci.item.rarity,
|
||||
marketValue: ci.item.marketValue,
|
||||
dropRate: ci.dropRate,
|
||||
...(includeRates ? {} : {}),
|
||||
}));
|
||||
|
||||
const withChance = normalizeRates(
|
||||
items.map((i) => ({ ...i, dropRate: i.dropRate }))
|
||||
);
|
||||
|
||||
return {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
imageUrl: c.imageUrl,
|
||||
price: c.price,
|
||||
active: c.active,
|
||||
items: withChance.map(({ dropRate, chance, ...rest }) => ({
|
||||
...rest,
|
||||
dropRate,
|
||||
chance: Number(chance.toFixed(4)),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const cases = await prisma.case.findMany({
|
||||
where: { active: true },
|
||||
include: {
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
orderBy: { price: 'asc' },
|
||||
});
|
||||
res.json({ cases: cases.map((c) => formatCase(c)) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load cases' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const c = await prisma.case.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
});
|
||||
if (!c || !c.active) {
|
||||
return res.status(404).json({ error: 'Case not found' });
|
||||
}
|
||||
res.json({ case: formatCase(c, true) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load case' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/open', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const caseId = Number(req.params.id);
|
||||
const userId = req.session.userId;
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const c = await tx.case.findUnique({
|
||||
where: { id: caseId },
|
||||
include: {
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!c || !c.active) {
|
||||
const err = new Error('Case not found');
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
if (!c.items.length) {
|
||||
const err = new Error('Case has no items');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const user = await tx.user.findUnique({ where: { id: userId } });
|
||||
if (!user) {
|
||||
const err = new Error('User not found');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
if (user.balance < c.price) {
|
||||
const err = new Error('Insufficient balance');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const winner = pickWeighted(
|
||||
c.items.map((ci) => ({
|
||||
caseItem: ci,
|
||||
dropRate: ci.dropRate,
|
||||
}))
|
||||
);
|
||||
|
||||
const item = winner.caseItem.item;
|
||||
|
||||
const updatedUser = await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: { balance: { decrement: c.price } },
|
||||
});
|
||||
|
||||
const inventoryItem = await tx.inventoryItem.create({
|
||||
data: {
|
||||
userId,
|
||||
itemId: item.id,
|
||||
},
|
||||
include: { item: true },
|
||||
});
|
||||
|
||||
await tx.transaction.create({
|
||||
data: {
|
||||
userId,
|
||||
type: 'open_case',
|
||||
amount: -c.price,
|
||||
meta: JSON.stringify({
|
||||
caseId: c.id,
|
||||
caseName: c.name,
|
||||
itemId: item.id,
|
||||
itemName: item.name,
|
||||
inventoryItemId: inventoryItem.id,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
user: publicUser(updatedUser),
|
||||
item: {
|
||||
inventoryId: inventoryItem.id,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
imageUrl: item.imageUrl,
|
||||
rarity: item.rarity,
|
||||
marketValue: item.marketValue,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const status = err.status || 500;
|
||||
res.status(status).json({ error: err.message || 'Failed to open case' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../db.js';
|
||||
import { requireAuth } from '../middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const items = await prisma.inventoryItem.findMany({
|
||||
where: { userId: req.session.userId },
|
||||
include: { item: true },
|
||||
orderBy: { obtainedAt: 'desc' },
|
||||
});
|
||||
|
||||
res.json({
|
||||
inventory: items.map((inv) => ({
|
||||
id: inv.id,
|
||||
obtainedAt: inv.obtainedAt,
|
||||
item: {
|
||||
id: inv.item.id,
|
||||
name: inv.item.name,
|
||||
imageUrl: inv.item.imageUrl,
|
||||
rarity: inv.item.rarity,
|
||||
marketValue: inv.item.marketValue,
|
||||
},
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load inventory' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import { prisma } from '../db.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { role: 'user' },
|
||||
include: {
|
||||
inventory: {
|
||||
include: { item: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ranked = users
|
||||
.map((u) => {
|
||||
const inventoryValue = u.inventory.reduce(
|
||||
(sum, inv) => sum + inv.item.marketValue,
|
||||
0
|
||||
);
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
balance: u.balance,
|
||||
inventoryValue,
|
||||
totalWealth: u.balance + inventoryValue,
|
||||
itemCount: u.inventory.length,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalWealth - a.totalWealth)
|
||||
.slice(0, 50)
|
||||
.map((entry, index) => ({ rank: index + 1, ...entry }));
|
||||
|
||||
res.json({ leaderboard: ranked });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to load leaderboard' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,24 @@
|
||||
/** Pick one entry from a weighted list using dropRate as relative weight. */
|
||||
export function pickWeighted(entries) {
|
||||
const total = entries.reduce((sum, e) => sum + e.dropRate, 0);
|
||||
if (total <= 0) {
|
||||
throw new Error('No valid drop rates');
|
||||
}
|
||||
let roll = Math.random() * total;
|
||||
for (const entry of entries) {
|
||||
roll -= entry.dropRate;
|
||||
if (roll <= 0) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return entries[entries.length - 1];
|
||||
}
|
||||
|
||||
export function normalizeRates(entries) {
|
||||
const total = entries.reduce((sum, e) => sum + e.dropRate, 0);
|
||||
if (total <= 0) return entries.map((e) => ({ ...e, chance: 0 }));
|
||||
return entries.map((e) => ({
|
||||
...e,
|
||||
chance: (e.dropRate / total) * 100,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user