1331 lines
42 KiB
TypeScript
1331 lines
42 KiB
TypeScript
"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 {
|
||
ATTACHMENT_CATEGORIES,
|
||
autoArrangeLockerItems,
|
||
ensureLockerStorage,
|
||
findLockerEmptySpot,
|
||
getLoadoutSlot,
|
||
getLockerGridDimensions,
|
||
getLockerItemFootprint,
|
||
isAttachmentCompatible,
|
||
isLockerPlacementValid,
|
||
isSkinAsset,
|
||
lockerIsRotatable,
|
||
lockerIsStackable,
|
||
lockerMaxStack,
|
||
resolveAssetId,
|
||
skinAppliesTo,
|
||
toLockerGridItems,
|
||
} from "@/lib/locker";
|
||
|
||
export 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);
|
||
}
|
||
|
||
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 = toLockerGridItems(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 = toLockerGridItems(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 = toLockerGridItems(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 = toLockerGridItems(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(toLockerGridItems(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;
|
||
}
|
||
|
||
// --- Wardrobe / equipped loadout ---
|
||
|
||
export async function equipLoadout(loadoutId: number, equipped = true): Promise<ActionResult> {
|
||
try {
|
||
const { payload, user } = await authenticate();
|
||
const loadout = await getOwnedLoadout(payload, user, loadoutId, 0);
|
||
if (!loadout) return { success: false, error: "Loadout not found." };
|
||
|
||
const ownedRes = (await payload.find({
|
||
collection: "loadouts",
|
||
where: { ownerUser: { equals: user.id } },
|
||
limit: 100,
|
||
depth: 0,
|
||
overrideAccess: true,
|
||
})) as unknown as { docs: Loadout[] };
|
||
|
||
for (const doc of ownedRes.docs) {
|
||
const isEquipped = doc.equipped === true;
|
||
if (doc.id === loadoutId) {
|
||
if (isEquipped !== equipped) {
|
||
await payload.update({
|
||
collection: "loadouts",
|
||
id: doc.id,
|
||
data: { equipped },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
}
|
||
} else if (isEquipped && equipped) {
|
||
await payload.update({
|
||
collection: "loadouts",
|
||
id: doc.id,
|
||
data: { equipped: false },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
}
|
||
}
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LoadoutSetActive,
|
||
message: equipped
|
||
? `Set "${loadout.name}" as the equipped loadout`
|
||
: `Un-equipped "${loadout.name}"`,
|
||
actor: user.id,
|
||
targetCollection: "loadouts",
|
||
targetId: loadoutId,
|
||
data: { loadoutName: loadout.name, active: equipped },
|
||
});
|
||
|
||
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 };
|
||
}
|
||
}
|
||
|
||
// --- Equipment customization (attachments & skins) ---
|
||
|
||
export async function setItemAttachment(entryId: string, assetId: number): Promise<ActionResult> {
|
||
try {
|
||
const { payload, user } = await authenticate();
|
||
const locker = await getLockerWithItems(payload, user.id as number);
|
||
const items = [...(locker.items ?? [])];
|
||
|
||
const targetIdx = items.findIndex((e) => e.id === entryId);
|
||
if (targetIdx < 0) return { success: false, error: "Item not found in locker." };
|
||
|
||
const target = items[targetIdx];
|
||
const weapon = target.asset as Asset;
|
||
if (!weapon || typeof weapon === "number" || weapon.assetType !== "weapon") {
|
||
return { success: false, error: "Attachments can only be installed on weapons." };
|
||
}
|
||
|
||
const attachment = await getAssetOrThrow(payload, assetId);
|
||
if (!isAttachmentCompatible(weapon, attachment)) {
|
||
return {
|
||
success: false,
|
||
error: `"${attachment.name}" is not compatible with ${weapon.name}.`,
|
||
};
|
||
}
|
||
|
||
const already = (target.equipment?.attachments ?? []).some(
|
||
(a) => resolveAssetId(a.asset) === assetId,
|
||
);
|
||
if (already) return { success: false, error: "This attachment is already installed." };
|
||
|
||
const srcIdx = items.findIndex(
|
||
(e) => e.id !== entryId && resolveAssetId(e.asset) === assetId && e.quantity >= 1,
|
||
);
|
||
if (srcIdx < 0) {
|
||
return { success: false, error: `You do not have ${attachment.name} in your locker.` };
|
||
}
|
||
|
||
const src = items[srcIdx];
|
||
const updatedItems = [...items];
|
||
if (src.quantity > 1) {
|
||
updatedItems[srcIdx] = { ...src, quantity: src.quantity - 1 };
|
||
} else {
|
||
updatedItems.splice(srcIdx, 1);
|
||
}
|
||
|
||
const tIdx = updatedItems.findIndex((e) => e.id === entryId);
|
||
const currentAttachments = target.equipment?.attachments ?? [];
|
||
updatedItems[tIdx] = {
|
||
...updatedItems[tIdx],
|
||
equipment: {
|
||
...(updatedItems[tIdx].equipment ?? {}),
|
||
attachments: [...currentAttachments, { asset: assetId }],
|
||
},
|
||
};
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items: updatedItems },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemAttach,
|
||
message: `Installed ${attachment.name} on ${weapon.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: {
|
||
entryId,
|
||
weaponId: weapon.id,
|
||
weaponName: weapon.name,
|
||
attachmentId: assetId,
|
||
attachmentName: attachment.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 removeItemAttachment(
|
||
entryId: string,
|
||
attachmentId: number,
|
||
): 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 targetIdx = items.findIndex((e) => e.id === entryId);
|
||
if (targetIdx < 0) return { success: false, error: "Item not found in locker." };
|
||
|
||
const target = items[targetIdx];
|
||
const weapon = target.asset as Asset;
|
||
if (!weapon || typeof weapon === "number") return { success: false, error: "Invalid asset." };
|
||
|
||
const attachments = target.equipment?.attachments ?? [];
|
||
const attIdx = attachments.findIndex((a) => resolveAssetId(a.asset) === attachmentId);
|
||
if (attIdx < 0) return { success: false, error: "Attachment not installed on this item." };
|
||
|
||
const attachment = await getAssetOrThrow(payload, attachmentId);
|
||
const remainingAttachments = attachments.filter((_, i) => i !== attIdx);
|
||
|
||
const gridItems = toLockerGridItems(items);
|
||
const fp = getLockerItemFootprint(attachment, false);
|
||
const spot = findLockerEmptySpot(gridItems, fp.width, fp.height, grid.width, grid.height);
|
||
if (!spot) {
|
||
return { success: false, error: `No space in locker to return ${attachment.name}.` };
|
||
}
|
||
|
||
const updatedItems = [...items];
|
||
updatedItems[targetIdx] = {
|
||
...target,
|
||
equipment: { ...(target.equipment ?? {}), attachments: remainingAttachments },
|
||
};
|
||
updatedItems.push({
|
||
asset: attachmentId,
|
||
quantity: 1,
|
||
gridX: spot.x,
|
||
gridY: spot.y,
|
||
rotated: false,
|
||
});
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items: updatedItems },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemDetach,
|
||
message: `Removed ${attachment.name} from ${weapon.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: {
|
||
entryId,
|
||
weaponId: weapon.id,
|
||
weaponName: weapon.name,
|
||
attachmentId,
|
||
attachmentName: attachment.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 setItemSkin(entryId: string, assetId: number): Promise<ActionResult> {
|
||
try {
|
||
const { payload, user } = await authenticate();
|
||
const locker = await getLockerWithItems(payload, user.id as number);
|
||
const grid = await getLockerGridDimensions(payload);
|
||
let items = [...(locker.items ?? [])];
|
||
|
||
const targetIdx = items.findIndex((e) => e.id === entryId);
|
||
if (targetIdx < 0) return { success: false, error: "Item not found in locker." };
|
||
|
||
const target = items[targetIdx];
|
||
const equipment = target.asset as Asset;
|
||
if (!equipment || typeof equipment === "number")
|
||
return { success: false, error: "Invalid asset." };
|
||
|
||
const skin = await getAssetOrThrow(payload, assetId);
|
||
if (!isSkinAsset(skin)) return { success: false, error: `"${skin.name}" is not a skin.` };
|
||
if (!skinAppliesTo(skin, equipment)) {
|
||
return { success: false, error: `"${skin.name}" cannot be applied to ${equipment.name}.` };
|
||
}
|
||
|
||
const currentSkinId = resolveAssetId(target.equipment?.skin);
|
||
if (currentSkinId != null && currentSkinId !== assetId) {
|
||
const oldSkin = await getAssetOrThrow(payload, currentSkinId).catch(() => null);
|
||
if (oldSkin) {
|
||
const fp = getLockerItemFootprint(oldSkin, false);
|
||
const spot = findLockerEmptySpot(
|
||
toLockerGridItems(items),
|
||
fp.width,
|
||
fp.height,
|
||
grid.width,
|
||
grid.height,
|
||
);
|
||
if (!spot) {
|
||
return {
|
||
success: false,
|
||
error: "No space in locker to return the currently applied skin.",
|
||
};
|
||
}
|
||
items = [
|
||
...items,
|
||
{ asset: currentSkinId, quantity: 1, gridX: spot.x, gridY: spot.y, rotated: false },
|
||
];
|
||
}
|
||
}
|
||
|
||
const srcIdx = items.findIndex(
|
||
(e) => e.id !== entryId && resolveAssetId(e.asset) === assetId && e.quantity >= 1,
|
||
);
|
||
if (srcIdx < 0) {
|
||
return { success: false, error: `You do not have ${skin.name} in your locker.` };
|
||
}
|
||
|
||
const src = items[srcIdx];
|
||
const updatedItems = [...items];
|
||
if (src.quantity > 1) {
|
||
updatedItems[srcIdx] = { ...src, quantity: src.quantity - 1 };
|
||
} else {
|
||
updatedItems.splice(srcIdx, 1);
|
||
}
|
||
|
||
const tIdx = updatedItems.findIndex((e) => e.id === entryId);
|
||
updatedItems[tIdx] = {
|
||
...updatedItems[tIdx],
|
||
equipment: { ...(updatedItems[tIdx].equipment ?? {}), skin: assetId },
|
||
};
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items: updatedItems },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemSkin,
|
||
message: `Applied ${skin.name} to ${equipment.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: {
|
||
entryId,
|
||
equipmentId: equipment.id,
|
||
equipmentName: equipment.name,
|
||
skinId: assetId,
|
||
skinName: skin.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 removeItemSkin(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 targetIdx = items.findIndex((e) => e.id === entryId);
|
||
if (targetIdx < 0) return { success: false, error: "Item not found in locker." };
|
||
|
||
const target = items[targetIdx];
|
||
const skinId = resolveAssetId(target.equipment?.skin);
|
||
if (skinId == null) return { success: false, error: "No skin applied to this item." };
|
||
|
||
const skin = await getAssetOrThrow(payload, skinId);
|
||
const fp = getLockerItemFootprint(skin, false);
|
||
const spot = findLockerEmptySpot(
|
||
toLockerGridItems(items),
|
||
fp.width,
|
||
fp.height,
|
||
grid.width,
|
||
grid.height,
|
||
);
|
||
if (!spot) return { success: false, error: `No space in locker to return ${skin.name}.` };
|
||
|
||
const updatedItems = [...items];
|
||
updatedItems[targetIdx] = {
|
||
...target,
|
||
equipment: { ...(target.equipment ?? {}), skin: null },
|
||
};
|
||
updatedItems.push({ asset: skinId, quantity: 1, gridX: spot.x, gridY: spot.y, rotated: false });
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items: updatedItems },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemSkinClear,
|
||
message: `Removed ${skin.name} from ${(target.asset as Asset).name ?? "item"}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: { entryId, skinId, skinName: skin.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 renameLockerItem(entryId: string, customName: 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 entry = items[idx];
|
||
const asset = entry.asset as Asset;
|
||
if (!asset || typeof asset === "number") return { success: false, error: "Invalid asset." };
|
||
|
||
const name = customName.trim();
|
||
items[idx] = { ...entry, equipment: { ...(entry.equipment ?? {}), customName: name || null } };
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemRename,
|
||
message: name ? `Renamed ${asset.name} to "${name}"` : `Cleared custom name on ${asset.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: { entryId, assetId: asset.id, assetName: asset.name, customName: name || null },
|
||
});
|
||
|
||
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 attachItemFromGrid(
|
||
weaponEntryId: string,
|
||
attachmentEntryId: 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 weaponIdx = items.findIndex((e) => e.id === weaponEntryId);
|
||
const attIdx = items.findIndex((e) => e.id === attachmentEntryId);
|
||
if (weaponIdx < 0) return { success: false, error: "Weapon not found in locker." };
|
||
if (attIdx < 0) return { success: false, error: "Item not found in locker." };
|
||
|
||
const weapon = items[weaponIdx].asset as Asset;
|
||
if (!weapon || typeof weapon === "number" || weapon.assetType !== "weapon") {
|
||
return { success: false, error: "Attachments can only be installed on weapons." };
|
||
}
|
||
|
||
const attachment = items[attIdx].asset as Asset;
|
||
if (!attachment || typeof attachment === "number") {
|
||
return { success: false, error: "Invalid item." };
|
||
}
|
||
|
||
const category = ATTACHMENT_CATEGORIES.find((c) => c.assetType === attachment.assetType);
|
||
if (!category) {
|
||
return { success: false, error: "This item type cannot be attached to a weapon." };
|
||
}
|
||
|
||
if (!isAttachmentCompatible(weapon, attachment)) {
|
||
return {
|
||
success: false,
|
||
error: `"${attachment.name}" is not compatible with ${weapon.name}.`,
|
||
};
|
||
}
|
||
|
||
const already = (items[weaponIdx].equipment?.attachments ?? []).some(
|
||
(a) => resolveAssetId(a.asset) === attachment.id,
|
||
);
|
||
if (already) {
|
||
return { success: false, error: "This attachment is already installed on this weapon." };
|
||
}
|
||
|
||
let working = [...items];
|
||
const dragged = working[attIdx];
|
||
if (dragged.quantity > 1) {
|
||
working[attIdx] = { ...dragged, quantity: dragged.quantity - 1 };
|
||
} else {
|
||
working.splice(attIdx, 1);
|
||
}
|
||
|
||
let existingAsset: Asset | null = null;
|
||
for (const a of items[weaponIdx].equipment?.attachments ?? []) {
|
||
const id = resolveAssetId(a.asset);
|
||
if (id == null) continue;
|
||
const existing = await getAssetOrThrow(payload, id);
|
||
if (existing.assetType === category.assetType) {
|
||
existingAsset = existing;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (existingAsset) {
|
||
const fp = getLockerItemFootprint(existingAsset, false);
|
||
const spot = findLockerEmptySpot(
|
||
toLockerGridItems(working),
|
||
fp.width,
|
||
fp.height,
|
||
grid.width,
|
||
grid.height,
|
||
);
|
||
if (!spot) {
|
||
return { success: false, error: `No space in locker to return ${existingAsset.name}.` };
|
||
}
|
||
working.push({
|
||
asset: existingAsset.id,
|
||
quantity: 1,
|
||
gridX: spot.x,
|
||
gridY: spot.y,
|
||
rotated: false,
|
||
});
|
||
}
|
||
|
||
const wIdx = working.findIndex((e) => e.id === weaponEntryId);
|
||
if (wIdx < 0) return { success: false, error: "Weapon not found in locker." };
|
||
const kept = (working[wIdx].equipment?.attachments ?? []).filter(
|
||
(a) => resolveAssetId(a.asset) !== existingAsset?.id,
|
||
);
|
||
working[wIdx] = {
|
||
...working[wIdx],
|
||
equipment: {
|
||
...(working[wIdx].equipment ?? {}),
|
||
attachments: [...kept, { asset: attachment.id }],
|
||
},
|
||
};
|
||
|
||
await payload.update({
|
||
collection: "locker-storages",
|
||
id: locker.id,
|
||
data: { items: working },
|
||
overrideAccess: true,
|
||
depth: 0,
|
||
});
|
||
|
||
if (existingAsset) {
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemDetach,
|
||
message: `Removed ${existingAsset.name} from ${weapon.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: {
|
||
entryId: weaponEntryId,
|
||
weaponId: weapon.id,
|
||
weaponName: weapon.name,
|
||
attachmentId: existingAsset.id,
|
||
attachmentName: existingAsset.name,
|
||
},
|
||
});
|
||
}
|
||
|
||
await emitGameEvent(payload, {
|
||
type: EventTypes.LockerItemAttach,
|
||
message: `Installed ${attachment.name} on ${weapon.name}`,
|
||
actor: user.id,
|
||
targetCollection: "locker-storages",
|
||
targetId: locker.id,
|
||
data: {
|
||
entryId: weaponEntryId,
|
||
weaponId: weapon.id,
|
||
weaponName: weapon.name,
|
||
attachmentId: attachment.id,
|
||
attachmentName: attachment.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 };
|
||
}
|
||
}
|