1
0
Fork 0

feat(qualification): record aim trainer results

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-03 23:36:22 -04:00
parent a6a7a5639d
commit 282e23edc2
2 changed files with 240 additions and 0 deletions

View file

@ -0,0 +1,239 @@
"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";
import {
isAimTrainerDifficulty,
calculateAimTrainerAccuracy,
calculateAimTrainerScore,
calculateAimTrainerXp,
MAX_SHOTS_FIRED,
MAX_HITS,
MAX_DESPAWNED_TARGETS,
} from "@/lib/aim-trainer";
interface ActionResult<T = undefined> {
success: boolean;
error?: string;
data?: T;
}
export interface AimTrainerStats {
gamesPlayed: number;
bestScore: number;
recruitBestScore: number;
veteranBestScore: number;
eliteBestScore: number;
recruitBestAccuracy: number;
veteranBestAccuracy: number;
eliteBestAccuracy: number;
aimTrainerXp: number;
xpEarned: number;
score: number;
accuracy: number;
}
export interface AimTrainerResultInput {
difficulty: string;
headshots: number;
torsoHits: number;
shotsFired: number;
despawnedUnhitTargets: number;
}
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 submitAimTrainerResult(
input: AimTrainerResultInput,
): Promise<ActionResult<AimTrainerStats>> {
try {
const { payload, user } = await authenticate();
if (!isAimTrainerDifficulty(input.difficulty)) {
return { success: false, error: "Invalid difficulty." };
}
const { headshots, torsoHits, shotsFired, despawnedUnhitTargets } = input;
if (
!Number.isInteger(headshots) ||
headshots < 0 ||
!Number.isInteger(torsoHits) ||
torsoHits < 0 ||
!Number.isInteger(shotsFired) ||
shotsFired < 0 ||
!Number.isInteger(despawnedUnhitTargets) ||
despawnedUnhitTargets < 0
) {
return { success: false, error: "Invalid result." };
}
if (shotsFired > MAX_SHOTS_FIRED) {
return { success: false, error: "Too many shots fired." };
}
const hits = headshots + torsoHits;
if (hits > MAX_HITS) {
return { success: false, error: "Too many hits." };
}
if (hits > shotsFired) {
return { success: false, error: "Hits cannot exceed shots fired." };
}
if (despawnedUnhitTargets > MAX_DESPAWNED_TARGETS) {
return { success: false, error: "Too many despawned targets." };
}
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?: {
aimTrainer?: {
gamesPlayed?: number;
bestScore?: number;
recruitBestScore?: number;
veteranBestScore?: number;
eliteBestScore?: number;
recruitBestAccuracy?: number;
veteranBestAccuracy?: number;
eliteBestAccuracy?: number;
xpEarned?: number;
};
};
};
const current = progression.minigames?.aimTrainer ?? {};
const accuracy = calculateAimTrainerAccuracy(hits, shotsFired, despawnedUnhitTargets);
const score = calculateAimTrainerScore(headshots, torsoHits, shotsFired, despawnedUnhitTargets);
const difficultyBestScoreKey = `${input.difficulty}BestScore` as
| "recruitBestScore"
| "veteranBestScore"
| "eliteBestScore";
const difficultyBestAccuracyKey = `${input.difficulty}BestAccuracy` as
| "recruitBestAccuracy"
| "veteranBestAccuracy"
| "eliteBestAccuracy";
const isNewTierBest = score > (current[difficultyBestScoreKey] ?? 0);
const xp = calculateAimTrainerXp(score, isNewTierBest);
const gamesPlayed = (current.gamesPlayed ?? 0) + 1;
const bestScore = Math.max(current.bestScore ?? 0, score);
const difficultyBestScore = Math.max(current[difficultyBestScoreKey] ?? 0, score);
const difficultyBestAccuracy = Math.max(current[difficultyBestAccuracyKey] ?? 0, accuracy);
const aimTrainerXp = (current.xpEarned ?? 0) + xp;
const { xp: _xp, ...progressionRest } = progression;
const recruitBestScore = input.difficulty === "recruit" ? difficultyBestScore : (current.recruitBestScore ?? 0);
const veteranBestScore = input.difficulty === "veteran" ? difficultyBestScore : (current.veteranBestScore ?? 0);
const eliteBestScore = input.difficulty === "elite" ? difficultyBestScore : (current.eliteBestScore ?? 0);
const recruitBestAccuracy =
input.difficulty === "recruit" ? difficultyBestAccuracy : (current.recruitBestAccuracy ?? 0);
const veteranBestAccuracy =
input.difficulty === "veteran" ? difficultyBestAccuracy : (current.veteranBestAccuracy ?? 0);
const eliteBestAccuracy =
input.difficulty === "elite" ? difficultyBestAccuracy : (current.eliteBestAccuracy ?? 0);
await payload.update({
collection: "profiles",
id: profile.id,
data: {
progression: {
...progressionRest,
minigames: {
...(progressionRest.minigames ?? {}),
aimTrainer: {
gamesPlayed,
bestScore,
recruitBestScore,
veteranBestScore,
eliteBestScore,
recruitBestAccuracy,
veteranBestAccuracy,
eliteBestAccuracy,
xpEarned: aimTrainerXp,
},
},
},
},
overrideAccess: true,
depth: 0,
});
broadcastToUser("profile-update", Number(user.id), {
reason: "xp",
xpGained: xp,
profileId: profile.id,
});
if (score > 0) {
const pct = Math.round(accuracy * 100);
await emitGameEvent(payload, {
type: EventTypes.MinigameAimTrainer,
message: `completed the Qualification Course (${input.difficulty}) with a score of ${score} (${pct}% accuracy)`,
actor: user.id as number,
targetCollection: "users",
targetId: user.id as number,
data: {
difficulty: input.difficulty,
score,
accuracy,
xpEarned: xp,
bestScore,
difficultyBestScore,
aimTrainerXp,
},
});
}
return {
success: true,
data: {
gamesPlayed,
bestScore,
recruitBestScore,
veteranBestScore,
eliteBestScore,
recruitBestAccuracy,
veteranBestAccuracy,
eliteBestAccuracy,
aimTrainerXp,
xpEarned: xp,
score,
accuracy,
},
};
} 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 };
}
}

View file

@ -64,6 +64,7 @@ export const EventTypes = {
MinigameFlappy: "minigame:flappy",
MinigameEod: "minigame:eod",
MinigameRadioTraffic: "minigame:radio-traffic",
MinigameAimTrainer: "minigame:aim-trainer",
// Market
MarketListingCreate: "market:listing-create",