1
0
Fork 0
polaris-task-force/src/collections/users/Profiles.ts
Z8MB1E f83301a923 feat(voiceover): add voice script and submission collections with profile XP
Add VoiceScripts and VoiceSubmissions collections, voiceover event types and event-log targets, the voice-submissions migration, and a voiceover XP group on Profiles counted toward total XP.

Ultraworked with [Sisyphus](https://github.com/code-yeongju/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 17:22:50 -04:00

586 lines
18 KiB
TypeScript

import { CollectionConfig } from "payload";
import { Award } from "@/payload-types";
import { broadcastToUser } from "@/lib/realtime/bus";
import { notifyAwardGrants } from "@/lib/awards";
import { hasPermission } from "@/utils/access-control/hasPermission";
import { lexicalPlaintextLength } from "@/lib/lexical/text";
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 });
}
},
async ({ doc, previousDoc, req }) => {
await notifyAwardGrants(req.payload, previousDoc, doc);
},
],
},
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",
label: "Personal Excerpt",
type: "richText",
validate: (value: unknown) => {
if (lexicalPlaintextLength(value) > 1000) {
return "Personal excerpt must be 1000 characters or fewer.";
}
return true;
},
},
{
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<string, number | null>;
minigames?: {
flappyBird?: { xpEarned?: number | null };
eodMinesweeper?: { xpEarned?: number | null };
radioTraffic?: { xpEarned?: number | null };
aimTrainer?: { xpEarned?: number | null };
};
voiceover?: { 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;
const radioTrafficXp = progressionData?.minigames?.radioTraffic?.xpEarned ?? 0;
const aimTrainerXp = progressionData?.minigames?.aimTrainer?.xpEarned ?? 0;
const voiceoverXp = progressionData?.voiceover?.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;
xpAcc += radioTrafficXp;
xpAcc += aimTrainerXp;
// Voiceover submissions: XP awarded per accepted submission
xpAcc += voiceoverXp;
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",
admin: {
components: {
Field: {
path: "@/components/admin/groups/CollapsibleGroupField",
clientProps: { initCollapsed: true },
},
},
},
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,
},
{
name: "bestTrainingFlags",
label: "Flags on Best Training Pad Run",
type: "number",
defaultValue: 0,
},
{
name: "bestPerimeterFlags",
label: "Flags on Best FOB Perimeter Run",
type: "number",
defaultValue: 0,
},
{
name: "bestSupplyRoadFlags",
label: "Flags on Best Main Supply Road Run",
type: "number",
defaultValue: 0,
},
],
},
{
name: "radioTraffic",
label: "Radio Traffic",
type: "group",
fields: [
{
name: "gamesPlayed",
type: "number",
defaultValue: 0,
},
{
name: "totalMessages",
label: "Total Messages",
type: "number",
defaultValue: 0,
},
{
name: "incorrectMessages",
label: "Incorrect Messages",
type: "number",
defaultValue: 0,
},
{
name: "bestScore",
label: "Best Score",
type: "number",
defaultValue: 0,
},
{
name: "bestAccuracy",
label: "Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "bestSprintTimeMs",
label: "Best Sprint Time (ms)",
type: "number",
defaultValue: 0,
},
{
name: "sprintGamesPlayed",
label: "Sprint Games Played",
type: "number",
defaultValue: 0,
},
{
name: "sprintTotalMessages",
label: "Sprint Total Messages",
type: "number",
defaultValue: 0,
},
{
name: "sprintIncorrectMessages",
label: "Sprint Incorrect Messages",
type: "number",
defaultValue: 0,
},
{
name: "sprintBestScore",
label: "Sprint Best Score",
type: "number",
defaultValue: 0,
},
{
name: "sprintBestAccuracy",
label: "Sprint Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "watchGamesPlayed",
label: "Watch Games Played",
type: "number",
defaultValue: 0,
},
{
name: "watchTotalMessages",
label: "Watch Total Messages",
type: "number",
defaultValue: 0,
},
{
name: "watchIncorrectMessages",
label: "Watch Incorrect Messages",
type: "number",
defaultValue: 0,
},
{
name: "watchBestScore",
label: "Watch Best Score",
type: "number",
defaultValue: 0,
},
{
name: "watchBestAccuracy",
label: "Watch Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "xpEarned",
label: "XP Earned",
type: "number",
defaultValue: 0,
},
],
},
{
name: "aimTrainer",
label: "Aim Trainer",
type: "group",
fields: [
{
name: "gamesPlayed",
type: "number",
defaultValue: 0,
},
{
name: "bestScore",
label: "Best Score",
type: "number",
defaultValue: 0,
},
{
name: "recruitBestScore",
label: "Recruit Best Score",
type: "number",
defaultValue: 0,
},
{
name: "veteranBestScore",
label: "Veteran Best Score",
type: "number",
defaultValue: 0,
},
{
name: "eliteBestScore",
label: "Elite Best Score",
type: "number",
defaultValue: 0,
},
{
name: "recruitBestAccuracy",
label: "Recruit Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "veteranBestAccuracy",
label: "Veteran Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "eliteBestAccuracy",
label: "Elite Best Accuracy",
type: "number",
defaultValue: 0,
},
{
name: "xpEarned",
label: "XP Earned",
type: "number",
defaultValue: 0,
},
],
},
],
},
{
name: "voiceover",
label: "Voiceover",
type: "group",
fields: [
{
name: "xpEarned",
label: "XP Earned",
type: "number",
defaultValue: 0,
},
],
},
],
},
],
};