import { CollectionConfig } from "payload"; import { Award } from "@/payload-types"; import { broadcastToUser } from "@/lib/realtime/bus"; import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const Profiles: CollectionConfig = { slug: "profiles", admin: { useAsTitle: "profileTitle", group: "Users", defaultColumns: ["profileTitle", "user", "rank"], }, access: { read: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:read"); }, create: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:create"); }, update: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:update"); }, delete: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:delete"); }, }, hooks: { afterChange: [ async ({ doc }) => { const userId = typeof doc.user === "object" ? doc.user?.id : doc.user; if (typeof userId === "number") { broadcastToUser("profile-update", userId, { profileId: doc.id }); } }, ], }, fields: [ { name: "user", type: "relationship", relationTo: "users", required: true, }, { name: "rank", type: "relationship", relationTo: "ranks", required: true, }, { name: "profileTitle", type: "text", virtual: "user.displayName", admin: { hidden: true, }, label: "Operator", }, { name: "dossier", label: "Personnel Dossier", type: "group", fields: [ { name: "enlistmentDate", type: "date", required: true, defaultValue: ({ user }) => user?.createdAt, }, { name: "personalExcerpt", type: "textarea", }, { name: "intelExcerpt", label: "Intelligence Record", type: "richText", access: { create: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:intel_excerpt:update"); }, update: async ({ req }) => { return await hasPermission(req.payload, req.user, "profiles:intel_excerpt:update"); }, }, }, ], }, { name: "progression", label: "Progression & XP", type: "group", fields: [ { name: "xp", label: "Experience Points (XP)", type: "number", virtual: true, defaultValue: 0, admin: { readOnly: true, }, hooks: { // beforeChange: [({ siblingData }) => delete siblingData["xp"]], afterRead: [ async ({ req, siblingData }) => { let xpAcc = 0; const progressionData = siblingData as { awards?: Array< | { award?: number | Award; } | number >; scoreboard?: Record; minigames?: { flappyBird?: { xpEarned?: number | null }; eodMinesweeper?: { xpEarned?: number | null }; }; }; const awardsList = progressionData?.awards || []; const awardIds = awardsList.flatMap((entry) => { const award = typeof entry === "object" ? entry.award : entry; if (typeof award === "number") return [award]; if (award && typeof award.id === "number") return [award.id]; return []; }); const scoreboard = progressionData?.scoreboard || {}; const flappyXp = progressionData?.minigames?.flappyBird?.xpEarned ?? 0; const eodXp = progressionData?.minigames?.eodMinesweeper?.xpEarned ?? 0; if (awardIds.length > 0) { await req.payload .find({ collection: "awards", where: { id: { in: awardIds, }, }, }) .then((res) => { const awards: Award[] = res.docs; xpAcc += awards.reduce((prev, cur) => prev + (cur.experiencePoints ?? 0), 0); }); } Object.entries(scoreboard).forEach(([k, v]) => { if (!v) return; switch (k) { case "vehicleKills": xpAcc += v * 20; break; case "armorKills": case "airKills": xpAcc += v * 30; break; case "infantryKills": default: xpAcc += v * 10; break; } }); // Flappy Bird training sim: 1 XP per 15 pipes passed in a single run xpAcc += flappyXp; // EOD Minesweeper: XP from cleared minefields xpAcc += eodXp; return xpAcc; }, ], }, }, { name: "awards", type: "array", fields: [ { name: "award", type: "relationship", relationTo: "awards", required: true, admin: { appearance: "drawer", }, }, { name: "awardedBy", type: "relationship", relationTo: "profiles", // This is a self-relation. Do not recursively populate the // issuer's progression/awards when a profile is read. maxDepth: 0, admin: { description: 'Who issued the award. If no one, the award will be attributed as "on behalf of the unit."', }, }, { name: "awardedAt", type: "date", required: true, defaultValue: () => new Date().toISOString(), admin: { description: "The issuing date of the award.", }, }, { name: "awardReason", type: "textarea", admin: { description: "An optional reason for the award.", }, }, ], }, { name: "qualifications", type: "relationship", relationTo: "qualifications", hasMany: true, admin: { appearance: "drawer", }, }, { name: "scoreboard", type: "group", admin: { readOnly: true, }, fields: [ { name: "infantryKills", type: "number", defaultValue: 0, }, { name: "vehicleKills", type: "number", defaultValue: 0, }, { name: "armorKills", type: "number", defaultValue: 0, }, { name: "airKills", type: "number", defaultValue: 0, }, { name: "deaths", type: "number", defaultValue: 0, }, ], }, { name: "minigames", label: "Minigames", type: "group", fields: [ { name: "flappyBird", label: "Flappy Bird", type: "group", fields: [ { name: "gamesPlayed", type: "number", defaultValue: 0, }, { name: "totalPasses", label: "Total Pipes Passed", type: "number", defaultValue: 0, }, { name: "bestScore", label: "Best Run (Pipes)", type: "number", defaultValue: 0, }, { name: "xpEarned", label: "XP Earned", type: "number", defaultValue: 0, }, ], }, { name: "eodMinesweeper", label: "EOD Minesweeper", type: "group", fields: [ { name: "gamesPlayed", type: "number", defaultValue: 0, }, { name: "wins", label: "Wins", type: "number", defaultValue: 0, }, { name: "losses", label: "Losses", type: "number", defaultValue: 0, }, { name: "bestScore", label: "Best Score", type: "number", defaultValue: 0, }, { name: "xpEarned", label: "XP Earned", type: "number", defaultValue: 0, }, { name: "bestTrainingTime", label: "Best Training Pad Time (s)", type: "number", defaultValue: 0, }, { name: "bestPerimeterTime", label: "Best FOB Perimeter Time (s)", type: "number", defaultValue: 0, }, { name: "bestSupplyRoadTime", label: "Best Main Supply Road Time (s)", type: "number", defaultValue: 0, }, ], }, ], }, ], }, ], };