fix: redesign SCUM gift item lists
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { scumCatalogEntryFor } from "./scum-catalog.js";
|
||||
|
||||
export type RecordMap = Record<string, unknown>;
|
||||
export type GiftItemDefinition = { catalogCode: string; quantity: number; probability: number; durability?: number };
|
||||
|
||||
const giftDefinitionFields = ["code", "name", "class", "audience", "number", "achievement", "achievementNumber", "items", "status", "createdAt", "updatedAt"] as const;
|
||||
|
||||
export type PluginDataMutation = { operation: "put" | "delete"; key: string; value?: RecordMap };
|
||||
export type PluginDataActions = {
|
||||
@@ -130,7 +133,10 @@ async function loadPlatformSCUMTables(actions: SCUMWorkspaceActions, data: SCUMS
|
||||
|
||||
export async function saveGiftDefinition(actions: SCUMWorkspaceActions, gift: RecordMap): Promise<unknown> {
|
||||
const key = requiredKey(gift, "code", "礼包编号");
|
||||
return requirePluginData(actions).transact(scumCollections.gifts, [{ operation: "put", key, value: gift }]);
|
||||
const storedGift: RecordMap = {};
|
||||
for (const field of giftDefinitionFields) if (gift[field] !== undefined) storedGift[field] = gift[field];
|
||||
storedGift.items = parseGiftItems(storedGift.items);
|
||||
return requirePluginData(actions).transact(scumCollections.gifts, [{ operation: "put", key, value: storedGift }]);
|
||||
}
|
||||
|
||||
export async function deleteGiftDefinition(actions: SCUMWorkspaceActions, key: string): Promise<void> { return requirePluginData(actions).delete(scumCollections.gifts, key); }
|
||||
@@ -157,18 +163,19 @@ export async function queueGiftDelivery(actions: SCUMWorkspaceActions, gift: Rec
|
||||
const giftCode = requiredKey(gift, "code", "礼包编号");
|
||||
const playerId = firstText(player, "gamePlayerId", "playerId", "steamId", "id");
|
||||
if (!playerId) throw new Error("用户编号不能为空。");
|
||||
const items = normalizeGiftItems(gift.items);
|
||||
const operations = [...new Set([...normalizeGiftOperations(gift.commands), ...normalizeGiftOperations(gift.operations)])];
|
||||
if (!items.length && !operations.length) throw new Error("礼包必须包含物品或命令。");
|
||||
const configuredItems = parseGiftItems(gift.items);
|
||||
if (!configuredItems.length) throw new Error("礼包必须包含物品。");
|
||||
const items = selectGiftItems(configuredItems);
|
||||
if (!items.length) throw new Error("本次礼包没有命中任何物品,请检查物品概率配置。");
|
||||
const now = Date.now();
|
||||
const grantId = safeCommandId(`gift:${giftCode}:${playerId}:${now}`);
|
||||
const commands = [...giftCatalogCommands(items), ...operations].map(normalizeRCONCommand);
|
||||
const commands = giftCatalogCommands(items).map(normalizeRCONCommand);
|
||||
const results = [];
|
||||
for (const [index, command] of commands.entries()) {
|
||||
results.push(await dispatchSCUMRCONCommand(actions, command, `${grantId}:${index + 1}`));
|
||||
}
|
||||
const commandIds = results.map((result) => textValue(result.result?.jobId)).filter(Boolean);
|
||||
const record = { id: grantId, giftCode, giftName: textValue(gift.name), playerId, playerName: firstText(player, "displayName", "playerName", "name"), status: "queued", commandId: commandIds[0] ?? "", commandIds, createdAt: new Date(now).toISOString() };
|
||||
const record = { id: grantId, giftCode, giftName: textValue(gift.name), playerId, playerName: firstText(player, "displayName", "playerName", "name"), status: "queued", items, commandId: commandIds[0] ?? "", commandIds, createdAt: new Date(now).toISOString() };
|
||||
await createGiftDelivery(actions, record);
|
||||
return { status: "queued", result: { jobIds: commandIds.join(",") } };
|
||||
}
|
||||
@@ -207,29 +214,32 @@ export async function startEvent(actions: SCUMWorkspaceActions, event: RecordMap
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
export function parseGiftItems(input: string): Array<{ catalogCode: string; quantity: number }> {
|
||||
if (!input.trim()) return [];
|
||||
const items = input.split(",").map((part) => {
|
||||
const [rawKey, rawQuantity, ...extra] = part.split(":").map((value) => value.trim());
|
||||
const quantity = Number(rawQuantity);
|
||||
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(rawKey) || !rawQuantity || extra.length || !Number.isSafeInteger(quantity) || quantity < 1) throw new Error("礼包物品格式无效,请使用 SCUM 目录代码:正整数数量。");
|
||||
return { catalogCode: rawKey, quantity };
|
||||
});
|
||||
return items;
|
||||
export function parseGiftItems(input: unknown): GiftItemDefinition[] {
|
||||
if (input === undefined || input === null || input === "") return [];
|
||||
if (typeof input === "string") {
|
||||
if (!input.trim()) return [];
|
||||
return input.split(",").map((part) => {
|
||||
const [rawKey = "", rawQuantity = "", ...extra] = part.split(":").map((value) => value.trim());
|
||||
if (extra.length) throw new Error("礼包物品格式无效,请从目录选择物品并配置数量、概率和耐久。");
|
||||
return parseGiftItem({ catalogCode: rawKey, quantity: rawQuantity });
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(input)) throw new Error("礼包物品格式无效,请使用物品列表。");
|
||||
return input.map(parseGiftItem);
|
||||
}
|
||||
|
||||
function giftCatalogCommands(items: Array<{ catalogCode: string; quantity: number }>): string[] {
|
||||
export function selectGiftItems(items: GiftItemDefinition[], random: () => number = Math.random): GiftItemDefinition[] {
|
||||
return items.filter((item) => item.probability >= 100 || (item.probability > 0 && random() * 100 < item.probability));
|
||||
}
|
||||
|
||||
function giftCatalogCommands(items: GiftItemDefinition[]): string[] {
|
||||
return items.flatMap((item) => {
|
||||
const entry = scumCatalogEntryFor(item.catalogCode);
|
||||
if (!entry || entry.kind === "item") return [`#SpawnItem ${item.catalogCode} ${item.quantity}`];
|
||||
if (!entry || entry.kind === "item") return [`#SpawnItem ${item.catalogCode} ${item.quantity}${item.durability === undefined ? "" : ` Health ${item.durability}%`}`];
|
||||
return Array.from({ length: item.quantity }, () => entry.command);
|
||||
});
|
||||
}
|
||||
|
||||
export function parseGiftCommands(input: string): Array<{ command: string }> {
|
||||
return input.split(/\r?\n/).map((command) => command.trim()).filter(Boolean).map((command) => ({ command }));
|
||||
}
|
||||
|
||||
export type SCUMMapBounds = { worldMinX: number; worldMinY: number; worldMaxX: number; worldMaxY: number };
|
||||
|
||||
export function resolveMapBounds(settings?: RecordMap): SCUMMapBounds {
|
||||
@@ -283,19 +293,21 @@ function requiredKey(value: RecordMap, key: string, label: string): string {
|
||||
|
||||
function requiredRecordKey(value: RecordMap, label: string): string { const key = firstText(value, "_recordKey", "id", "produceId"); if (!key) throw new Error(`${label}编号不能为空。`); return key.includes(":") ? key : `${firstText(value, "eventId", "event")}:${key}`; }
|
||||
|
||||
function normalizeGiftItems(value: unknown): Array<{ catalogCode: string; quantity: number }> {
|
||||
if (value === undefined || value === null) return [];
|
||||
if (!Array.isArray(value)) throw new Error("礼包物品格式无效。");
|
||||
return value.map((item) => {
|
||||
if (!isRecord(item)) throw new Error("礼包物品格式无效。");
|
||||
const catalogCode = firstText(item, "catalogCode", "key");
|
||||
const quantity = Number(item.quantity);
|
||||
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(catalogCode) || !Number.isSafeInteger(quantity) || quantity < 1) throw new Error("礼包物品不符合 SCUM 目录代码或正整数数量约束。");
|
||||
return { catalogCode, quantity };
|
||||
});
|
||||
function parseGiftItem(value: unknown): GiftItemDefinition {
|
||||
if (!isRecord(value)) throw new Error("礼包物品格式无效,请使用物品列表。");
|
||||
const catalogCode = firstText(value, "catalogCode", "catalogItemKey", "key", "className");
|
||||
const quantity = Number(value.quantity);
|
||||
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(catalogCode) || !Number.isSafeInteger(quantity) || quantity < 1) throw new Error("礼包物品数量必须是正整数,目录代码格式无效。");
|
||||
const probability = value.probability === undefined || value.probability === null ? 100 : percentValue(value.probability, "物品概率");
|
||||
const durability = value.durability === undefined || value.durability === null || value.durability === "" ? undefined : percentValue(value.durability, "物品耐久");
|
||||
return durability === undefined ? { catalogCode, quantity, probability } : { catalogCode, quantity, probability, durability };
|
||||
}
|
||||
|
||||
function normalizeGiftOperations(value: unknown): string[] { if (value === undefined || value === null) return []; if (!Array.isArray(value)) throw new Error("礼包命令格式无效。"); return value.map((item) => { const command = isRecord(item) ? firstText(item, "command", "value") : textValue(item); if (!command.trim()) throw new Error("礼包命令不能为空。"); return command.trim(); }); }
|
||||
function percentValue(value: unknown, label: string): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100) throw new Error(`${label}必须在 0 到 100 之间。`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeRCONCommand(command: string): string {
|
||||
const normalized = command.trim();
|
||||
|
||||
Reference in New Issue
Block a user