112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import type { Payload } from "payload";
|
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
|
import { broadcastToUser } from "@/lib/realtime/bus";
|
|
|
|
type ScoreboardField = "infantryKills" | "vehicleKills" | "armorKills" | "airKills" | "deaths";
|
|
|
|
const VEHICLE_CLASS_AIR = /(^|[_])(Heli|Plane|VTOL|UAV)(_|$)/i;
|
|
const VEHICLE_CLASS_ARMOR = /(^|[_])(MBT|APC|IFV|Tank|LT[_]?)(_|$)/i;
|
|
const VEHICLE_CLASS_INFANTRY = /(^|[_])(Soldier|Crew|Officer|Sniper|Spotter|Medic|Engineer|Rifleman|Grenadier|MachineGunner)(_|$)/i;
|
|
|
|
export function classifyKillCategory(victimVehicle: string | null | undefined): ScoreboardField {
|
|
if (!victimVehicle) return "infantryKills";
|
|
if (VEHICLE_CLASS_AIR.test(victimVehicle)) return "airKills";
|
|
if (VEHICLE_CLASS_ARMOR.test(victimVehicle)) return "armorKills";
|
|
if (VEHICLE_CLASS_INFANTRY.test(victimVehicle)) return "infantryKills";
|
|
return "vehicleKills";
|
|
}
|
|
|
|
interface KillPayload {
|
|
victimName: string;
|
|
killerName: string;
|
|
killerSteamId: string;
|
|
victimVehicle: string | null;
|
|
killerVehicle: string | null;
|
|
}
|
|
|
|
export function parseKillPayload(payload: unknown): KillPayload | null {
|
|
if (!Array.isArray(payload) || payload.length < 6) return null;
|
|
|
|
const victimName = String(payload[0] ?? "");
|
|
const killerName = String(payload[2] ?? "");
|
|
const killerSteamId = String(payload[3] ?? "");
|
|
const victimVehicle = payload[6] ? String(payload[6]) : null;
|
|
const killerVehicle = payload[7] ? String(payload[7]) : null;
|
|
|
|
if (!killerSteamId) return null;
|
|
|
|
return { victimName, killerName, killerSteamId, victimVehicle, killerVehicle };
|
|
}
|
|
|
|
export async function processPlayerKill(
|
|
payload: Payload,
|
|
eventDoc: { id: number; payload: unknown; occurredAt: string },
|
|
): Promise<void> {
|
|
const kill = parseKillPayload(eventDoc.payload);
|
|
if (!kill) return;
|
|
|
|
const users = await payload.find({
|
|
collection: "users",
|
|
where: { steamId: { equals: kill.killerSteamId } },
|
|
limit: 1,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
});
|
|
const user = users.docs[0];
|
|
if (!user) {
|
|
payload.logger.warn(
|
|
`[KillTracker] No user found for Steam ID ${kill.killerSteamId}; kill not recorded.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const profiles = await payload.find({
|
|
collection: "profiles",
|
|
where: { user: { equals: user.id } },
|
|
limit: 1,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
});
|
|
const profile = profiles.docs[0];
|
|
if (!profile) {
|
|
payload.logger.warn(
|
|
`[KillTracker] No profile found for user ${user.id} (${user.username}); kill not recorded.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const field = classifyKillCategory(kill.victimVehicle);
|
|
const current = (profile.progression?.scoreboard as Record<string, number | undefined>)?.[field] ?? 0;
|
|
|
|
await payload.update({
|
|
collection: "profiles",
|
|
id: profile.id,
|
|
data: {
|
|
progression: {
|
|
scoreboard: {
|
|
...((profile.progression?.scoreboard as Record<string, number>) ?? {}),
|
|
[field]: current + 1,
|
|
},
|
|
},
|
|
},
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
});
|
|
|
|
await emitGameEvent(payload, {
|
|
type: EventTypes.PlayerKill,
|
|
actor: user.id,
|
|
message: `${kill.killerName} eliminated ${kill.victimName} (${field.replace("Kills", "")})`,
|
|
targetCollection: "users",
|
|
targetId: user.id,
|
|
data: {
|
|
victim: kill.victimName,
|
|
category: field,
|
|
victimVehicle: kill.victimVehicle,
|
|
killerVehicle: kill.killerVehicle,
|
|
},
|
|
});
|
|
|
|
broadcastToUser("profile-update", user.id, { profileId: profile.id });
|
|
}
|