Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/api/src/routes/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { conflict, notFound, parse } from "../errors.js";
import { deleteAvatarImage, getAvatarImage, saveAvatarImage } from "../services/avatars.js";
import { clearBannerCard, deleteBanner, getBannerImage, saveBannerImage, setBannerCard } from "../services/banners.js";
import { answeringQuestion, queueWaiting } from "../services/battles.js";
import { freeCases } from "../services/free-cases.js";
import { arthropodFlags, cardSheet, catalog, imageFlags } from "../services/cards.js";
import {
bulkTag,
Expand Down Expand Up @@ -89,6 +90,7 @@ export const FEATURE_ANNOUNCEMENTS: { key: string; until: string }[] = [
{ key: "suggestions", until: "2026-12-01T00:00:00+01:00" },
{ key: "updates", until: "2026-12-01T00:00:00+01:00" },
{ key: "wheels", until: "2026-12-01T00:00:00+01:00" },
{ key: "cases", until: "2026-12-15T00:00:00+01:00" },
{ key: "battle-decks", until: "2026-12-15T00:00:00+01:00" },
{ key: "boss-v2", until: "2026-12-31T00:00:00+01:00" },
{ key: "leaderboard-achievements", until: "2026-12-31T00:00:00+01:00" },
Expand Down Expand Up @@ -150,6 +152,7 @@ export async function me(
quests: { done: quests.filter((q) => q.completedAt).length, total: quests.length },
hlTriesLeft: await hlTriesLeft(ctx, user.id),
boss: await bossSummary(ctx, user.id),
freeCases: Object.values(await freeCases(ctx.db, user.id)).reduce((a, b) => a + b, 0),
};
}

Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/routes/economy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
upgradeSeriesHistory,
upgradeSeriesPreview,
} from "../services/upgrade.js";
import { casesState, openCase } from "../services/cases.js";
import { spinWheel, wheelState } from "../services/wheel.js";

const id = z.coerce.number().int().positive();
Expand Down Expand Up @@ -240,6 +241,16 @@ export function economyRoutes(api: FastifyInstance, ctx: Ctx) {
api.get("/upgrade/series/history", auth, async (req) => upgradeSeriesHistory(ctx, req.user.id));
api.get("/wheel", auth, async (req) => wheelState(ctx, req.user.id));
api.post("/wheel/spin", limited(10), async (req) => spinWheel(ctx, req.user.id));
// Caisses (suggestion n° 126) : contenu visible, une issue par ouverture, ×5 d'un coup.
api.get("/cases", auth, async (req) => casesState(ctx, req.user.id));
api.post("/cases/:id/open", limited(60), async (req) => {
const { id } = parse(idParams, req.params);
const b = parse(
z.object({ count: z.union([z.literal(1), z.literal(5)]).default(1), free: z.boolean().default(false) }),
req.body ?? {},
);
return openCase(ctx, req.user.id, id, b.count, b.free);
});
// Limite serrée : pas de devinette de codes à la chaîne.
api.post("/codes/redeem", limited(10), async (req) => {
const { code } = parse(z.object({ code: z.string().trim().min(1).max(40) }), req.body);
Expand Down
59 changes: 58 additions & 1 deletion apps/api/src/routes/test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { and, eq, schema, sql } from "@palacards/db";
import { TITLE_BOARDS, TITLE_MAX_RANK } from "@palacards/game";
import { CASE_CONTENT, CASE_TIERS, TITLE_BOARDS, TITLE_MAX_RANK } from "@palacards/game";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { requireUser, type Ctx } from "../context.js";
Expand Down Expand Up @@ -84,6 +84,63 @@ export function testRoutes(api: FastifyInstance, ctx: Ctx) {
return { balance: p.balance };
});

/**
* Caisses en E2E : les quatre niveaux d'un thème, remplis des cartes les plus lues de la saison (comme
* l'atelier), en vente une semaine, et des caisses gratuites pour le joueur connecté.
*/
api.post("/test/case-seed", auth, async (req) => {
const body = parse(
z.object({ collection: z.string().min(1).max(60), free: z.number().int().min(0).max(20).default(0) }),
req.body,
);
const season = await activeSeason(ctx.db);
const ids: number[] = [];
for (const tier of CASE_TIERS) {
const [box] = await ctx.db
.insert(schema.caseBoxes)
.values({
collection: body.collection,
tier,
season,
startsAt: new Date(ctx.now().getTime() - 60_000),
endsAt: new Date(ctx.now().getTime() + 7 * 86_400_000),
})
.returning({ id: schema.caseBoxes.id });
let pos = 0;
for (const [rarity, n] of Object.entries(CASE_CONTENT[tier])) {
const rows = await ctx.db.execute<{ id: string }>(sql`
select id from cards where season = ${season} and rarity = ${rarity}::rarity
order by views_12m desc, id limit ${n}
`);
if (rows.length)
await ctx.db
.insert(schema.caseCards)
.values(rows.map((r) => ({ caseId: box!.id, cardId: Number(r.id), rarity: rarity as "UR", pos: pos++ })));
}
ids.push(box!.id);
}
if (body.free)
await ctx.db
.insert(schema.playerFreeCases)
.values(CASE_TIERS.map((tier) => ({ userId: req.user.id, tier, count: body.free })))
.onConflictDoUpdate({
target: [schema.playerFreeCases.userId, schema.playerFreeCases.tier],
set: { count: body.free },
});
return { ids };
});

/** Prochaine ouverture de caisse forcée sur la dernière issue (le plus gros lot) : captures et parcours E2E. */
api.post("/test/case-jackpot", auth, async () => {
const random = ctx.random;
ctx.random = (max) => {
if (max !== 10_000) return random(max);
ctx.random = random;
return 9_999;
};
return { ok: true };
});

/** Donne un titre de fin de saison au joueur connecté (sans l'afficher : c'est à lui de le choisir). */
api.post("/test/grant-title", auth, async (req) => {
const body = parse(
Expand Down
Loading
Loading