feat(locker): add locker and loadout server actions
This commit is contained in:
parent
0fc898d227
commit
d32f36a39c
1 changed files with 735 additions and 0 deletions
735
src/app/(frontend)/locker/actions.ts
Normal file
735
src/app/(frontend)/locker/actions.ts
Normal file
|
|
@ -0,0 +1,735 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import type { Asset, Loadout, LockerStorage, User } from "@/payload-types";
|
||||
import hasRoles from "@/utils/access-control/hasRoles";
|
||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
import {
|
||||
autoArrangeLockerItems,
|
||||
ensureLockerStorage,
|
||||
findLockerEmptySpot,
|
||||
getLockerGridDimensions,
|
||||
getLockerItemFootprint,
|
||||
getLoadoutSlot,
|
||||
isLockerPlacementValid,
|
||||
lockerIsRotatable,
|
||||
lockerIsStackable,
|
||||
lockerMaxStack,
|
||||
type LockerGridItem,
|
||||
} from "@/lib/locker";
|
||||
|
||||
interface ActionResult<T = undefined> {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
function isLockerManager(user: User): boolean {
|
||||
return hasRoles(["admin", "developer"], user);
|
||||
}
|
||||
|
||||
function toGridItems(items: LockerStorage["items"]): LockerGridItem[] {
|
||||
return (items ?? [])
|
||||
.filter((e) => e.asset && typeof e.asset === "object" && "id" in e.asset)
|
||||
.map((e) => {
|
||||
const asset = e.asset as Asset;
|
||||
return {
|
||||
id: e.id ?? `${asset.id}-${e.gridX}-${e.gridY}`,
|
||||
assetId: asset.id,
|
||||
quantity: e.quantity,
|
||||
gridX: e.gridX ?? 0,
|
||||
gridY: e.gridY ?? 0,
|
||||
rotated: e.rotated ?? false,
|
||||
asset,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAssetId(asset: NonNullable<LockerStorage["items"]>[number]["asset"]): number | null {
|
||||
if (asset == null) return null;
|
||||
if (typeof asset === "object" && "id" in asset) return asset.id as number;
|
||||
return asset as number;
|
||||
}
|
||||
|
||||
async function getLockerWithItems(
|
||||
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||
userId: number,
|
||||
): Promise<LockerStorage> {
|
||||
const locker = await ensureLockerStorage(payload, userId);
|
||||
return (await payload.findByID({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
depth: 2,
|
||||
overrideAccess: true,
|
||||
})) as unknown as LockerStorage;
|
||||
}
|
||||
|
||||
async function getAssetOrThrow(
|
||||
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||
assetId: number,
|
||||
): Promise<Asset> {
|
||||
const asset = (await payload
|
||||
.findByID({ collection: "assets", id: assetId, depth: 0, overrideAccess: true })
|
||||
.catch(() => null)) as unknown as Asset | null;
|
||||
if (!asset) throw new Error("Asset not found.");
|
||||
return asset;
|
||||
}
|
||||
|
||||
export async function ensureLocker(): Promise<ActionResult<number>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await ensureLockerStorage(payload, user.id as number);
|
||||
const existing = await payload.find({
|
||||
collection: "game-event-logs",
|
||||
where: {
|
||||
and: [
|
||||
{ type: { equals: EventTypes.LockerCreate } },
|
||||
{ targetCollection: { equals: "locker-storages" } },
|
||||
{ targetId: { equals: locker.id } },
|
||||
],
|
||||
},
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
});
|
||||
if (existing.docs.length === 0) {
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerCreate,
|
||||
message: `Personal locker provisioned`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
});
|
||||
}
|
||||
return { success: true, data: locker.id };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function addItemToLocker(
|
||||
assetId: number,
|
||||
quantity: number,
|
||||
): Promise<ActionResult<LockerStorage>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!isLockerManager(user)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Insufficient permissions. Admin or Developer role required to add items.",
|
||||
};
|
||||
}
|
||||
if (!Number.isInteger(quantity) || quantity <= 0) {
|
||||
return { success: false, error: "Quantity must be a positive integer." };
|
||||
}
|
||||
|
||||
const asset = await getAssetOrThrow(payload, assetId);
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const grid = await getLockerGridDimensions(payload);
|
||||
|
||||
const existing = [...(locker.items ?? [])];
|
||||
const gridItems = toGridItems(existing);
|
||||
const stackable = lockerIsStackable(asset);
|
||||
const maxStack = lockerMaxStack(asset);
|
||||
const fp = getLockerItemFootprint(asset, false);
|
||||
|
||||
let remaining = quantity;
|
||||
|
||||
if (stackable) {
|
||||
const sameType = gridItems.filter((e) => e.assetId === assetId);
|
||||
for (const entry of sameType) {
|
||||
if (remaining <= 0) break;
|
||||
const space =
|
||||
maxStack === Infinity
|
||||
? remaining
|
||||
: Math.max(0, Math.min(maxStack - entry.quantity, remaining));
|
||||
if (space <= 0) continue;
|
||||
const idx = existing.findIndex((e) => e.id === entry.id);
|
||||
if (idx >= 0) {
|
||||
existing[idx] = { ...existing[idx], quantity: existing[idx].quantity + space };
|
||||
remaining -= space;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (remaining > 0) {
|
||||
const stackAmount = Math.min(remaining, maxStack === Infinity ? remaining : maxStack);
|
||||
const spot = findLockerEmptySpot(gridItems, fp.width, fp.height, grid.width, grid.height);
|
||||
if (!spot) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No space in locker for ${asset.name}. Need ${fp.width}×${fp.height} free cells.`,
|
||||
};
|
||||
}
|
||||
existing.push({
|
||||
asset: assetId,
|
||||
quantity: stackAmount,
|
||||
gridX: spot.x,
|
||||
gridY: spot.y,
|
||||
rotated: false,
|
||||
});
|
||||
gridItems.push({
|
||||
id: `new-${existing.length}`,
|
||||
assetId,
|
||||
quantity: stackAmount,
|
||||
gridX: spot.x,
|
||||
gridY: spot.y,
|
||||
rotated: false,
|
||||
asset,
|
||||
});
|
||||
remaining -= stackAmount;
|
||||
}
|
||||
|
||||
const added = quantity - remaining;
|
||||
const updated = (await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items: existing },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as LockerStorage;
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerAddItem,
|
||||
message: `Added ${added.toLocaleString()}× ${asset.name} to locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { assetId, assetName: asset.name, quantity: added },
|
||||
});
|
||||
|
||||
return { success: true, data: updated };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeLockerItem(entryId: string): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const items = [...(locker.items ?? [])];
|
||||
const idx = items.findIndex((e) => e.id === entryId);
|
||||
if (idx < 0) return { success: false, error: "Item not found in locker." };
|
||||
|
||||
const removed = items[idx];
|
||||
const asset = removed.asset as Asset;
|
||||
const removedName = typeof asset === "object" && asset ? asset.name : `asset #${resolveAssetId(removed.asset)}`;
|
||||
|
||||
items.splice(idx, 1);
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerRemoveItem,
|
||||
message: `Removed ${removed.quantity}× ${removedName} from locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: resolveAssetId(removed.asset), assetName: removedName, quantity: removed.quantity },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveLockerItem(
|
||||
entryId: string,
|
||||
gridX: number,
|
||||
gridY: number,
|
||||
rotated?: boolean,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const grid = await getLockerGridDimensions(payload);
|
||||
const items = [...(locker.items ?? [])];
|
||||
const idx = items.findIndex((e) => e.id === entryId);
|
||||
if (idx < 0) return { success: false, error: "Item not found in locker." };
|
||||
|
||||
const entry = items[idx];
|
||||
const asset = entry.asset as Asset;
|
||||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||||
|
||||
const effectiveRotated = rotated ?? entry.rotated ?? false;
|
||||
const fp = getLockerItemFootprint(asset, effectiveRotated);
|
||||
|
||||
if (gridX < 0 || gridY < 0 || gridX + fp.width > grid.width || gridY + fp.height > grid.height) {
|
||||
return { success: false, error: "Placement is outside grid bounds." };
|
||||
}
|
||||
|
||||
const gridItems = toGridItems(items);
|
||||
if (!isLockerPlacementValid(gridItems, entryId, gridX, gridY, fp.width, fp.height, grid.width, grid.height)) {
|
||||
return { success: false, error: "Cannot move here — overlaps with another item." };
|
||||
}
|
||||
|
||||
items[idx] =
|
||||
rotated !== undefined
|
||||
? { ...entry, gridX, gridY, rotated }
|
||||
: { ...entry, gridX, gridY };
|
||||
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerGridMove,
|
||||
message: `Moved ${asset.name} to (${gridX}, ${gridY}) in locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: asset.id, assetName: asset.name, gridX, gridY, rotated: effectiveRotated },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function rotateLockerItem(entryId: string): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const grid = await getLockerGridDimensions(payload);
|
||||
const items = [...(locker.items ?? [])];
|
||||
const idx = items.findIndex((e) => e.id === entryId);
|
||||
if (idx < 0) return { success: false, error: "Item not found in locker." };
|
||||
|
||||
const entry = items[idx];
|
||||
const asset = entry.asset as Asset;
|
||||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||||
if (!lockerIsRotatable(asset)) return { success: false, error: "This asset cannot be rotated." };
|
||||
|
||||
const newRotated = !(entry.rotated ?? false);
|
||||
const fp = getLockerItemFootprint(asset, newRotated);
|
||||
|
||||
if (entry.gridX + fp.width > grid.width || entry.gridY + fp.height > grid.height) {
|
||||
return { success: false, error: "Rotation would push the item out of bounds." };
|
||||
}
|
||||
|
||||
const gridItems = toGridItems(items);
|
||||
if (!isLockerPlacementValid(gridItems, entryId, entry.gridX, entry.gridY, fp.width, fp.height, grid.width, grid.height)) {
|
||||
return { success: false, error: "Rotation would overlap with another item." };
|
||||
}
|
||||
|
||||
items[idx] = { ...entry, rotated: newRotated };
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerGridRotate,
|
||||
message: `Rotated ${asset.name} in locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: asset.id, assetName: asset.name, rotated: newRotated },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function mergeLockerStacks(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const items = [...(locker.items ?? [])];
|
||||
|
||||
const srcIdx = items.findIndex((e) => e.id === sourceId);
|
||||
const tgtIdx = items.findIndex((e) => e.id === targetId);
|
||||
if (srcIdx < 0 || tgtIdx < 0) return { success: false, error: "Item not found." };
|
||||
|
||||
const src = items[srcIdx];
|
||||
const tgt = items[tgtIdx];
|
||||
const srcAsset = src.asset as Asset;
|
||||
const tgtAsset = tgt.asset as Asset;
|
||||
if (!srcAsset || typeof srcAsset === "number" || !tgtAsset || typeof tgtAsset === "number") {
|
||||
return { success: false, error: "Invalid asset." };
|
||||
}
|
||||
if (srcAsset.id !== tgtAsset.id) {
|
||||
return { success: false, error: "Cannot merge different asset types." };
|
||||
}
|
||||
if (!lockerIsStackable(tgtAsset)) return { success: false, error: "This asset cannot be stacked." };
|
||||
|
||||
const maxStack = lockerMaxStack(tgtAsset);
|
||||
const spaceInTarget = maxStack === Infinity ? Infinity : Math.max(0, maxStack - tgt.quantity);
|
||||
if (spaceInTarget <= 0) return { success: false, error: "Target stack is full." };
|
||||
|
||||
const transferAmount = Math.min(src.quantity, spaceInTarget);
|
||||
items[tgtIdx] = { ...tgt, quantity: tgt.quantity + transferAmount };
|
||||
|
||||
if (transferAmount >= src.quantity) {
|
||||
items.splice(srcIdx, 1);
|
||||
} else {
|
||||
items[srcIdx] = { ...src, quantity: src.quantity - transferAmount };
|
||||
}
|
||||
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerGridMerge,
|
||||
message: `Merged ${transferAmount}× ${tgtAsset.name} stacks in locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { sourceId, targetId, assetId: srcAsset.id, assetName: tgtAsset.name, transferAmount },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function splitLockerStack(entryId: string, splitAmount: number): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!Number.isInteger(splitAmount) || splitAmount <= 0) {
|
||||
return { success: false, error: "Split amount must be a positive integer." };
|
||||
}
|
||||
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const grid = await getLockerGridDimensions(payload);
|
||||
const items = [...(locker.items ?? [])];
|
||||
const idx = items.findIndex((e) => e.id === entryId);
|
||||
if (idx < 0) return { success: false, error: "Item not found in locker." };
|
||||
|
||||
const entry = items[idx];
|
||||
if (splitAmount >= entry.quantity) {
|
||||
return { success: false, error: "Split amount must be less than the stack size." };
|
||||
}
|
||||
const asset = entry.asset as Asset;
|
||||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||||
if (!lockerIsStackable(asset)) return { success: false, error: "This asset cannot be stacked." };
|
||||
|
||||
const gridItems = toGridItems(items);
|
||||
const fp = getLockerItemFootprint(asset, entry.rotated ?? false);
|
||||
const spot = findLockerEmptySpot(gridItems, fp.width, fp.height, grid.width, grid.height);
|
||||
if (!spot) return { success: false, error: "No empty spot available for the split stack." };
|
||||
|
||||
items[idx] = { ...entry, quantity: entry.quantity - splitAmount };
|
||||
items.push({
|
||||
asset: entry.asset,
|
||||
quantity: splitAmount,
|
||||
gridX: spot.x,
|
||||
gridY: spot.y,
|
||||
rotated: entry.rotated ?? false,
|
||||
});
|
||||
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerGridSplit,
|
||||
message: `Split ${splitAmount}× ${asset.name} from stack in locker`,
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { entryId, assetId: asset.id, assetName: asset.name, splitAmount, remainingAmount: entry.quantity - splitAmount },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function autoArrangeLocker(): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const grid = await getLockerGridDimensions(payload);
|
||||
|
||||
const items = [...(locker.items ?? [])];
|
||||
const rearranged = autoArrangeLockerItems(toGridItems(items), grid.width, grid.height);
|
||||
|
||||
const updatedItems = items.map((entry) => {
|
||||
const match = rearranged.find((r) => r.id === entry.id);
|
||||
return match ? { ...entry, gridX: match.gridX, gridY: match.gridY } : entry;
|
||||
});
|
||||
|
||||
await payload.update({
|
||||
collection: "locker-storages",
|
||||
id: locker.id,
|
||||
data: { items: updatedItems },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LockerGridArrange,
|
||||
message: "Arranged locker grid",
|
||||
actor: user.id,
|
||||
targetCollection: "locker-storages",
|
||||
targetId: locker.id,
|
||||
data: { itemCount: updatedItems.length },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Loadouts ---
|
||||
|
||||
export async function createLoadout(input: {
|
||||
name: string;
|
||||
description?: string;
|
||||
}): Promise<ActionResult<number>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
if (!input.name?.trim()) {
|
||||
return { success: false, error: "A loadout name is required." };
|
||||
}
|
||||
const loadout = (await payload.create({
|
||||
collection: "loadouts",
|
||||
data: {
|
||||
name: input.name.trim(),
|
||||
description: input.description?.trim() || null,
|
||||
ownerUser: user.id as number,
|
||||
slots: {},
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as Loadout;
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LoadoutCreate,
|
||||
message: `Created loadout "${loadout.name}"`,
|
||||
actor: user.id,
|
||||
targetCollection: "loadouts",
|
||||
targetId: loadout.id,
|
||||
data: { loadoutName: loadout.name },
|
||||
});
|
||||
|
||||
return { success: true, data: loadout.id };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLoadout(
|
||||
id: number,
|
||||
input: { name?: string; description?: string },
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const loadout = await getOwnedLoadout(payload, user, id);
|
||||
if (!loadout) return { success: false, error: "Loadout not found." };
|
||||
|
||||
const data: { name?: string; description?: string | null } = {};
|
||||
if (input.name?.trim()) data.name = input.name.trim();
|
||||
if (input.description !== undefined) data.description = input.description?.trim() || null;
|
||||
|
||||
const updated = (await payload.update({
|
||||
collection: "loadouts",
|
||||
id,
|
||||
data,
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as Loadout;
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LoadoutUpdate,
|
||||
message: `Updated loadout "${updated.name}"`,
|
||||
actor: user.id,
|
||||
targetCollection: "loadouts",
|
||||
targetId: id,
|
||||
data: { loadoutName: updated.name },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLoadout(id: number): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const loadout = await getOwnedLoadout(payload, user, id);
|
||||
if (!loadout) return { success: false, error: "Loadout not found." };
|
||||
|
||||
await payload.delete({ collection: "loadouts", id, overrideAccess: true });
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LoadoutDelete,
|
||||
message: `Deleted loadout "${loadout.name}"`,
|
||||
actor: user.id,
|
||||
targetCollection: "loadouts",
|
||||
targetId: id,
|
||||
data: { loadoutName: loadout.name },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function setLoadoutSlot(
|
||||
loadoutId: number,
|
||||
slot: string,
|
||||
assetId: number | null,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
const slotConfig = getLoadoutSlot(slot);
|
||||
if (!slotConfig) return { success: false, error: "Unknown loadout slot." };
|
||||
|
||||
const loadout = await getOwnedLoadout(payload, user, loadoutId, 1);
|
||||
if (!loadout) return { success: false, error: "Loadout not found." };
|
||||
|
||||
if (assetId !== null) {
|
||||
const asset = await getAssetOrThrow(payload, assetId);
|
||||
const allowed = slotConfig.assetTypes.includes(asset.assetType);
|
||||
if (!allowed) {
|
||||
return {
|
||||
success: false,
|
||||
error: `"${asset.name}" cannot be equipped in the ${slotConfig.label} slot.`,
|
||||
};
|
||||
}
|
||||
const locker = await getLockerWithItems(payload, user.id as number);
|
||||
const owned = (locker.items ?? []).some((e) => resolveAssetId(e.asset) === assetId);
|
||||
if (!owned) {
|
||||
return { success: false, error: "You can only equip assets from your own locker." };
|
||||
}
|
||||
}
|
||||
|
||||
const currentSlots = (loadout.slots as Record<string, unknown> | null) ?? {};
|
||||
const updatedSlots = { ...currentSlots, [slot]: assetId ?? null };
|
||||
|
||||
await payload.update({
|
||||
collection: "loadouts",
|
||||
id: loadoutId,
|
||||
data: { slots: updatedSlots },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
const equippedName = assetId
|
||||
? (await getAssetOrThrow(payload, assetId).catch(() => null))?.name ?? `asset #${assetId}`
|
||||
: null;
|
||||
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.LoadoutEquip,
|
||||
message: equippedName
|
||||
? `Equipped ${equippedName} to "${loadout.name}" ${slotConfig.label} slot`
|
||||
: `Cleared ${slotConfig.label} slot on "${loadout.name}"`,
|
||||
actor: user.id,
|
||||
targetCollection: "loadouts",
|
||||
targetId: loadoutId,
|
||||
data: { loadoutId, slot, assetId, assetName: equippedName },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return message === "Unauthorized"
|
||||
? { success: false, error: "You must be logged in." }
|
||||
: { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
async function getOwnedLoadout(
|
||||
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||
user: User,
|
||||
id: number,
|
||||
depth = 0,
|
||||
): Promise<Loadout | null> {
|
||||
const loadout = (await payload
|
||||
.findByID({ collection: "loadouts", id, depth, overrideAccess: true })
|
||||
.catch(() => null)) as unknown as Loadout | null;
|
||||
if (!loadout) return null;
|
||||
|
||||
const ownerId =
|
||||
typeof loadout.ownerUser === "object" && loadout.ownerUser
|
||||
? (loadout.ownerUser as { id: number }).id
|
||||
: (loadout.ownerUser as number);
|
||||
|
||||
if (ownerId !== user.id && !isLockerManager(user)) return null;
|
||||
return loadout;
|
||||
}
|
||||
Loading…
Reference in a new issue