Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
252 lines
7.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|