This commit is contained in:
gpatruno
2026-07-18 16:26:08 +02:00
parent 49c444e8b9
commit 2614d679c8
85 changed files with 8074 additions and 710 deletions
+52
View File
@@ -0,0 +1,52 @@
/**
* Backfill CaseKeyProgress from open_case transactions.
* Run after: npx prisma db push
*
* npm run db:backfill-case-keys --prefix server
*/
import { prisma } from '../src/db.js';
import { progressFromTotalOpens } from '../src/caseKeys.js';
async function main() {
const txs = await prisma.transaction.findMany({
where: { type: 'open_case' },
select: { userId: true, meta: true },
orderBy: { createdAt: 'asc' },
});
const counts = new Map();
for (const tx of txs) {
let meta;
try {
meta = JSON.parse(tx.meta || '{}');
} catch {
continue;
}
const caseId = Number(meta.caseId);
if (!caseId) continue;
const key = `${tx.userId}:${caseId}`;
counts.set(key, (counts.get(key) || 0) + 1);
}
let upserted = 0;
for (const [key, totalOpens] of counts) {
const [userId, caseId] = key.split(':').map(Number);
const { keys, opensSinceLastKey } = progressFromTotalOpens(totalOpens);
await prisma.caseKeyProgress.upsert({
where: { userId_caseId: { userId, caseId } },
create: { userId, caseId, keys, opensSinceLastKey, totalOpens },
update: { keys, opensSinceLastKey, totalOpens },
});
upserted += 1;
}
console.log(`Backfilled ${upserted} user/case key progress rows from ${txs.length} open_case transactions`);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+66
View File
@@ -0,0 +1,66 @@
/**
* Backfill CatalogEntry from open_case transactions.
* Run after: npx prisma db push
*
* npm run db:backfill-catalog --prefix server
*/
import { prisma } from '../src/db.js';
async function main() {
const txs = await prisma.transaction.findMany({
where: { type: 'open_case' },
select: { userId: true, meta: true, createdAt: true },
orderBy: { createdAt: 'asc' },
});
const pairs = new Map();
for (const tx of txs) {
let meta;
try {
meta = JSON.parse(tx.meta || '{}');
} catch {
continue;
}
const caseId = Number(meta.caseId);
const itemId = Number(meta.itemId);
if (!caseId || !itemId) continue;
const key = `${tx.userId}:${caseId}:${itemId}`;
if (!pairs.has(key)) {
pairs.set(key, {
userId: tx.userId,
caseId,
itemId,
filledAt: tx.createdAt,
});
}
}
let created = 0;
let skipped = 0;
for (const row of pairs.values()) {
try {
await prisma.catalogEntry.create({
data: {
userId: row.userId,
caseId: row.caseId,
itemId: row.itemId,
filledAt: row.filledAt,
},
});
created += 1;
} catch {
skipped += 1;
}
}
console.log(
`Backfilled catalog: ${created} new entries, ${skipped} already present (${pairs.size} unique drops from ${txs.length} opens)`
);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+38
View File
@@ -0,0 +1,38 @@
/**
* Backfill existing InventoryItem rows with FT mid float + catalog marketValue.
* Run after: npx prisma db push
*
* npm run db:backfill-wear --prefix server
*/
import { prisma } from '../src/db.js';
import { FT_MID_FLOAT } from '../src/wear.js';
async function main() {
const rows = await prisma.inventoryItem.findMany({
include: { item: true },
});
let updated = 0;
for (const inv of rows) {
if (inv.valueCents !== 0) continue;
await prisma.inventoryItem.update({
where: { id: inv.id },
data: {
floatValue: FT_MID_FLOAT,
wear: 'Field-Tested',
paintSeed: 0,
valueCents: inv.item.marketValue,
},
});
updated += 1;
}
console.log(`Backfilled ${updated} / ${rows.length} inventory items`);
}
main()
.catch((err) => {
console.error(err);
process.exit(1);
})
.finally(() => prisma.$disconnect());
+90 -14
View File
@@ -11,12 +11,20 @@ model User {
id Int @id @default(autoincrement())
username String @unique
passwordHash String
balance Int @default(0)
balance BigInt @default(0)
osuBalance Int @default(0)
lastAdAt DateTime?
role String @default("user")
avatarUrl String @default("")
bio String @default("")
autoSellEnabled Boolean @default(false)
autoSellThresholdCents BigInt @default(1000)
prestigeCount Int @default(0)
prestigeChanceBonus Int @default(0)
prestigeGainBonus Int @default(0)
prestigeKeyOpenReduction Int @default(0)
lastConnection DateTime?
playTimeSeconds BigInt @default(0)
createdAt DateTime @default(now())
inventory InventoryItem[]
transactions Transaction[]
@@ -25,16 +33,22 @@ model User {
duelRoomsCreated DuelRoom[] @relation("DuelCreator")
duelWins DuelRoom[] @relation("DuelWinner")
duelDeposits DuelDeposit[]
caseKeyProgress CaseKeyProgress[]
catalogEntries CatalogEntry[]
prestigeLogs PrestigeLog[]
achievements UserAchievement[]
}
model Case {
id Int @id @default(autoincrement())
name String
imageUrl String @default("")
price Int
price BigInt
active Boolean @default(true)
createdAt DateTime @default(now())
items CaseItem[]
keyProgress CaseKeyProgress[]
catalogEntries CatalogEntry[]
}
model Item {
@@ -42,10 +56,11 @@ model Item {
name String
imageUrl String @default("")
rarity String
marketValue Int
marketValue BigInt
createdAt DateTime @default(now())
cases CaseItem[]
inventory InventoryItem[]
catalogEntries CatalogEntry[]
}
model CaseItem {
@@ -60,13 +75,18 @@ model CaseItem {
}
model InventoryItem {
id Int @id @default(autoincrement())
userId Int
itemId Int
locked Boolean @default(false)
obtainedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
userId Int
itemId Int
locked Boolean @default(false)
floatValue Float @default(0.265)
wear String @default("Field-Tested")
paintSeed Int @default(0)
valueCents BigInt @default(0)
favorite Boolean @default(false)
obtainedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
jackpotDeposits JackpotDeposit[]
duelDeposits DuelDeposit[]
}
@@ -75,7 +95,7 @@ model Transaction {
id Int @id @default(autoincrement())
userId Int
type String
amount Int
amount BigInt
meta String @default("{}")
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -97,7 +117,7 @@ model JackpotDeposit {
roundId Int
userId Int
inventoryItemId Int
valueCents Int
valueCents BigInt
createdAt DateTime @default(now())
round JackpotRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -112,7 +132,7 @@ model DuelRoom {
status String @default("open")
maxPlayers Int
maxItemsPerPlayer Int
minItemValue Int @default(0)
minItemValue BigInt @default(0)
minPlayersToStart Int @default(2)
winnerUserId Int?
createdAt DateTime @default(now())
@@ -128,7 +148,7 @@ model DuelDeposit {
roomId Int
userId Int
inventoryItemId Int
valueCents Int
valueCents BigInt
createdAt DateTime @default(now())
room DuelRoom @relation(fields: [roomId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -136,3 +156,59 @@ model DuelDeposit {
@@unique([roomId, inventoryItemId])
}
model CaseKeyProgress {
id Int @id @default(autoincrement())
userId Int
caseId Int
keys Int @default(1)
opensSinceLastKey Int @default(0)
totalOpens Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
case Case @relation(fields: [caseId], references: [id], onDelete: Cascade)
@@unique([userId, caseId])
}
model CatalogEntry {
id Int @id @default(autoincrement())
userId Int
caseId Int
itemId Int
filledAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
case Case @relation(fields: [caseId], references: [id], onDelete: Cascade)
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
@@unique([userId, caseId, itemId])
}
model PrestigeLog {
id Int @id @default(autoincrement())
userId Int
choice String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Achievement {
id Int @id @default(autoincrement())
key String @unique
name String
description String
icon String @default("")
sortOrder Int @default(0)
createdAt DateTime @default(now())
unlocks UserAchievement[]
}
model UserAchievement {
id Int @id @default(autoincrement())
userId Int
achievementId Int
unlockedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
achievement Achievement @relation(fields: [achievementId], references: [id], onDelete: Cascade)
@@unique([userId, achievementId])
}
+13 -3
View File
@@ -25,6 +25,8 @@ const ITEMS = [
];
async function main() {
await prisma.userAchievement.deleteMany();
await prisma.achievement.deleteMany();
await prisma.transaction.deleteMany();
await prisma.inventoryItem.deleteMany();
await prisma.caseItem.deleteMany();
@@ -32,12 +34,19 @@ async function main() {
await prisma.item.deleteMany();
await prisma.user.deleteMany();
const adminHash = await bcrypt.hash('admin123', 10);
const { ACHIEVEMENT_DEFS } = await import('../src/achievements.js');
for (const def of ACHIEVEMENT_DEFS) {
await prisma.achievement.create({ data: def });
}
const adminUsername = String(process.env.ADMIN_USERNAME || 'admin').trim() || 'admin';
const adminPassword = String(process.env.ADMIN_PASSWORD || 'admin123');
const adminHash = await bcrypt.hash(adminPassword, 10);
const userHash = await bcrypt.hash('player123', 10);
await prisma.user.create({
data: {
username: 'admin',
username: adminUsername,
passwordHash: adminHash,
balance: 0,
role: 'admin',
@@ -144,8 +153,9 @@ async function main() {
}
console.log('Seed complete.');
console.log(' admin / admin123');
console.log(` admin: ${adminUsername} / ${adminPassword}`);
console.log(' player / player123 (balance: 100.00, osu: 100)');
console.log(` achievements: ${ACHIEVEMENT_DEFS.length} definitions`);
}
main()