first commit

This commit is contained in:
2026-07-16 17:36:21 +02:00
commit a0d7163464
45 changed files with 7481 additions and 0 deletions
+166
View File
@@ -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;