1
0
Fork 0
polaris-task-force/src/app/(frontend)/minigames/eod/actions.ts
2026-09-09 00:02:59 -04:00

252 lines
7.2 KiB
TypeScript

"use server";
import config from "@payload-config";
import { getPayload } from "payload";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
import { broadcastToUser } from "@/lib/realtime/bus";
interface ActionResult<T = undefined> {
success: boolean;
error?: string;
data?: T;
}
export interface EodStats {
gamesPlayed: number;
wins: number;
losses: number;
bestScore: number;
eodXp: number;
xpEarned: number;
bestTrainingTime: number;
bestPerimeterTime: number;
bestSupplyRoadTime: number;
}
const DIFFICULTIES = {
training: {
label: "Training Pad",
rows: 9,
cols: 9,
mines: 10,
baseScore: 100,
parSeconds: 60,
maxSeconds: 300,
},
perimeter: {
label: "FOB Perimeter",
rows: 16,
cols: 16,
mines: 40,
baseScore: 300,
parSeconds: 180,
maxSeconds: 900,
},
supplyRoad: {
label: "Main Supply Road",
rows: 16,
cols: 30,
mines: 99,
baseScore: 600,
parSeconds: 420,
maxSeconds: 1800,
},
} as const;
export type EodDifficulty = keyof typeof DIFFICULTIES;
const BEST_TIME_FIELDS = {
training: "bestTrainingTime",
perimeter: "bestPerimeterTime",
supplyRoad: "bestSupplyRoadTime",
} as const;
const BEST_FLAGS_FIELDS = {
training: "bestTrainingFlags",
perimeter: "bestPerimeterFlags",
supplyRoad: "bestSupplyRoadFlags",
} as const;
type EodTimeField = (typeof BEST_TIME_FIELDS)[EodDifficulty];
type EodFlagsField = (typeof BEST_FLAGS_FIELDS)[EodDifficulty];
function isEodDifficulty(value: string): value is EodDifficulty {
return Object.prototype.hasOwnProperty.call(DIFFICULTIES, value);
}
async function authenticate() {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
const { headers } = await import("next/headers");
const hdrs = await headers();
const { user } = await payload.auth({
headers: hdrs,
canSetHeaders: false,
});
if (!user) {
throw new Error("Unauthorized");
}
return { payload, user };
}
export async function submitEodResult(input: {
difficulty: EodDifficulty;
seconds: number;
flags: number;
won: boolean;
}): Promise<ActionResult<EodStats>> {
try {
const { payload, user } = await authenticate();
if (typeof input.difficulty !== "string" || !isEodDifficulty(input.difficulty)) {
return { success: false, error: "Invalid result." };
}
const difficulty = input.difficulty;
const difficultyConfig = DIFFICULTIES[difficulty];
if (
!Number.isFinite(input.seconds) ||
!Number.isInteger(input.seconds) ||
input.seconds <= 0 ||
input.seconds > difficultyConfig.maxSeconds
) {
return { success: false, error: "Invalid result." };
}
if (!Number.isInteger(input.flags) || input.flags < 0 || input.flags > difficultyConfig.mines) {
return { success: false, error: "Invalid result." };
}
const profileRes = await payload.find({
collection: "profiles",
where: { user: { equals: user.id as number } },
limit: 1,
depth: 0,
overrideAccess: true,
});
const profile = profileRes.docs[0];
if (!profile) {
return { success: false, error: "No profile found for this user." };
}
const progression = (profile.progression ?? {}) as {
xp?: unknown;
minigames?: {
eodMinesweeper?: {
gamesPlayed?: number;
wins?: number;
losses?: number;
bestScore?: number;
xpEarned?: number;
bestTrainingTime?: number;
bestPerimeterTime?: number;
bestSupplyRoadTime?: number;
bestTrainingFlags?: number;
bestPerimeterFlags?: number;
bestSupplyRoadFlags?: number;
};
};
};
const current = progression.minigames?.eodMinesweeper ?? {};
const score = input.won
? difficultyConfig.baseScore + Math.max(0, difficultyConfig.parSeconds - input.seconds)
: 0;
const runXp = input.won ? Math.max(1, Math.floor(score / 50)) : 0;
const gamesPlayed = (current.gamesPlayed ?? 0) + 1;
const wins = (current.wins ?? 0) + (input.won ? 1 : 0);
const losses = (current.losses ?? 0) + (input.won ? 0 : 1);
const bestScore = Math.max(current.bestScore ?? 0, score);
const eodXp = (current.xpEarned ?? 0) + runXp;
const bestTimeField: EodTimeField = BEST_TIME_FIELDS[difficulty];
const bestFlagsField: EodFlagsField = BEST_FLAGS_FIELDS[difficulty];
const previousBestTime = current[bestTimeField] ?? 0;
const previousBestFlags = current[bestFlagsField] ?? 0;
const isBestRun =
input.won &&
(previousBestTime === 0 ||
input.seconds < previousBestTime ||
(input.seconds === previousBestTime && input.flags < previousBestFlags));
const bestTime = isBestRun ? input.seconds : previousBestTime;
const bestFlags = isBestRun ? input.flags : previousBestFlags;
const { xp: _xp, ...progressionRest } = progression;
await payload.update({
collection: "profiles",
id: profile.id,
data: {
progression: {
...progressionRest,
minigames: {
...(progressionRest.minigames ?? {}),
eodMinesweeper: {
gamesPlayed,
wins,
losses,
bestScore,
xpEarned: eodXp,
bestTrainingTime: current.bestTrainingTime ?? 0,
bestPerimeterTime: current.bestPerimeterTime ?? 0,
bestSupplyRoadTime: current.bestSupplyRoadTime ?? 0,
bestTrainingFlags: current.bestTrainingFlags ?? 0,
bestPerimeterFlags: current.bestPerimeterFlags ?? 0,
bestSupplyRoadFlags: current.bestSupplyRoadFlags ?? 0,
[bestTimeField]: bestTime,
[bestFlagsField]: bestFlags,
},
},
},
},
overrideAccess: true,
depth: 0,
});
broadcastToUser("profile-update", Number(user.id), {
reason: "xp",
xpGained: runXp,
profileId: profile.id,
});
if (input.won) {
await emitGameEvent(payload, {
type: EventTypes.MinigameEod,
message: `Cleared ${difficultyConfig.label} in ${input.seconds}s (+${runXp} XP)`,
actor: user.id as number,
targetCollection: "users",
targetId: user.id as number,
data: {
difficulty,
seconds: input.seconds,
flags: input.flags,
score,
xpEarned: runXp,
},
});
}
return {
success: true,
data: {
gamesPlayed,
wins,
losses,
bestScore,
eodXp,
xpEarned: runXp,
bestTrainingTime:
bestTimeField === "bestTrainingTime" ? bestTime : (current.bestTrainingTime ?? 0),
bestPerimeterTime:
bestTimeField === "bestPerimeterTime" ? bestTime : (current.bestPerimeterTime ?? 0),
bestSupplyRoadTime:
bestTimeField === "bestSupplyRoadTime" ? bestTime : (current.bestSupplyRoadTime ?? 0),
},
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (message === "Unauthorized") {
return { success: false, error: "You must be logged in." };
}
return { success: false, error: message };
}
}