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();
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
emptySCUMSurfaceData,
|
||||
loadSCUMSurface,
|
||||
parseGiftItems,
|
||||
parseGiftCommands,
|
||||
queueGiftDelivery,
|
||||
resetGiftClaim,
|
||||
resetPendingGift,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
type SCUMWorkspaceActions
|
||||
} from "./page-data.js";
|
||||
import { scumCatalogEntries, scumCatalogEntryFor, scumCatalogIconUrl, scumVehicleEntry, type ScumCatalogEntry } from "./scum-catalog.js";
|
||||
import { scumCatalogDisplayName, scumCatalogSearchText } from "./scum-catalog-zh.js";
|
||||
import { scumMapVehicleIconUrl } from "./map-vehicle-icons.js";
|
||||
import type { MapPlaybackFrame, MapPlaybackState, MapSceneData, MapTimeWindow, MapTrack, MapTrackPoint, MapTrackSegment, MapViewState } from "./contracts.js";
|
||||
|
||||
@@ -31,6 +31,8 @@ type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other";
|
||||
type MapVehicleFilter = "all" | "land" | "water" | "air" | "other";
|
||||
type ScumMapLayer = "terrain" | "topo" | "night";
|
||||
type CatalogPickTarget = "gift-items" | "produce-item";
|
||||
type GiftItemDraft = { catalogCode: string; quantity: string; probability: string; durability: string };
|
||||
type GiftItemDraftField = keyof GiftItemDraft;
|
||||
type PlayerPanelKind = "closed" | "gifts" | "items" | "history";
|
||||
type PlayerPanelState = { kind: PlayerPanelKind; playerId: string };
|
||||
type PointerEventLike = { pointerId?: number; clientX?: number; clientY?: number; button?: number; currentTarget?: { setPointerCapture?: (pointerId?: number) => void; releasePointerCapture?: (pointerId?: number) => void }; preventDefault?: () => void; stopPropagation?: () => void };
|
||||
@@ -39,7 +41,7 @@ const scumMapBaseLayers: ReadonlyArray<readonly [ScumMapLayer, string]> = [["ter
|
||||
const scumMapVehicleFilters: ReadonlyArray<readonly [MapVehicleFilter, string]> = [["all", "全部载具"], ["land", "陆地载具"], ["water", "水上载具"], ["air", "飞行类载具"], ["other", "其他载具"]];
|
||||
const scumFlightVehicleKeys = new Set(["kinglet_duster", "kinglet_mariner", "kinglet_scout"]);
|
||||
const scumCatalogKinds: ReadonlyArray<readonly [string, string]> = [["all", "全部"], ["item", "物品"], ["vehicle", "载具"], ["zombie", "丧尸"], ["animal", "动物"], ["armed-npc", "NPC"]];
|
||||
const scumGiftCatalogCategories: ReadonlyArray<readonly [string, string]> = [["all", "全部目录"], ["weapons", "武器"], ["fishing", "钓鱼"], ["gear", "装备"], ["food", "食物"], ["medical", "医疗"], ["components", "组件"], ["blueprints", "蓝图"], ["tools", "工具"], ["farming", "农业"], ["misc", "其他"], ["vehicle", "载具"], ["animal", "动物"], ["zombie", "丧尸"], ["armed-npc", "NPC"]];
|
||||
const scumGiftCatalogCategories: ReadonlyArray<readonly [string, string]> = [["all", "全部目录"], ["hunting", "狩猎"], ["food", "食物"], ["metabolism", "代谢"], ["medical", "医疗"], ["weapons", "武器"], ["crafting", "制作"], ["gear", "装备"], ["fishing", "钓鱼"], ["building", "建筑"], ["tools", "工具"], ["farming", "农业"], ["misc", "其他"], ["vehicle", "载具"], ["animal", "动物"], ["zombie", "丧尸"], ["armed-npc", "NPC"]];
|
||||
const scumCatalogPickLimit = 48;
|
||||
const scumMapSize = 4096;
|
||||
const scumSurfaceRefreshMs = 15000;
|
||||
@@ -122,8 +124,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const [giftRecordKey, setGiftRecordKey] = usePluginState(react, "");
|
||||
const [giftCode, setGiftCode] = usePluginState(react, "");
|
||||
const [giftName, setGiftName] = usePluginState(react, "");
|
||||
const [giftItems, setGiftItems] = usePluginState(react, "");
|
||||
const [giftCommands, setGiftCommands] = usePluginState(react, "");
|
||||
const [giftItems, setGiftItems] = usePluginState<GiftItemDraft[]>(react, []);
|
||||
const [giftClass, setGiftClass] = usePluginState(react, "5");
|
||||
const [giftAudience, setGiftAudience] = usePluginState(react, "all");
|
||||
const [giftNumber, setGiftNumber] = usePluginState(react, "1");
|
||||
@@ -260,7 +261,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
activityStatus, setActivityStatus, eventId, setEventId, eventName, setEventName, eventType, setEventType, eventSchedule, setEventSchedule,
|
||||
eventCommand, setEventCommand, eventClass, setEventClass, eventPlacard, setEventPlacard, eventPercent, setEventPercent, eventDuration, setEventDuration, eventNpc, setEventNpc, eventItem, setEventItem, eventZombie, setEventZombie, eventAnimal, setEventAnimal,
|
||||
produceEventId, setProduceEventId, produceId, setProduceId, produceTradeGoodsId, setProduceTradeGoodsId, producePercent, setProducePercent, produceValue, setProduceValue, produceRadius, setProduceRadius, produceX, setProduceX, produceY, setProduceY, produceZ, setProduceZ,
|
||||
giftTab, setGiftTab, giftSearch, setGiftSearch, giftStatus, setGiftStatus, giftRecordKey, setGiftRecordKey, giftCode, setGiftCode, giftName, setGiftName, giftItems, setGiftItems, giftCommands, setGiftCommands, giftClass, setGiftClass, giftAudience, setGiftAudience, giftNumber, setGiftNumber, giftAchievement, setGiftAchievement, giftAchievementNumber, setGiftAchievementNumber,
|
||||
giftTab, setGiftTab, giftSearch, setGiftSearch, giftStatus, setGiftStatus, giftRecordKey, setGiftRecordKey, giftCode, setGiftCode, giftName, setGiftName, giftItems, setGiftItems, giftClass, setGiftClass, giftAudience, setGiftAudience, giftNumber, setGiftNumber, giftAchievement, setGiftAchievement, giftAchievementNumber, setGiftAchievementNumber,
|
||||
deliveryGift, setDeliveryGift, deliveryPlayer, setDeliveryPlayer, mapLayers, setMapLayers, mapVehicleFilter, setMapVehicleFilter, selectedMapPoint, setSelectedMapPoint,
|
||||
hoverMapPoint, setHoverMapPoint,
|
||||
mapView, setMapView, mapViewportRef, mapPlaybackFrameRef, mapFollowRef, mapTimePreset, setMapTimePreset, mapTimeFrom, setMapTimeFrom, mapTimeTo, setMapTimeTo,
|
||||
@@ -310,7 +311,7 @@ type ViewState = {
|
||||
eventNpc: string; setEventNpc: StateSetter<string>; eventItem: string; setEventItem: StateSetter<string>; eventZombie: string; setEventZombie: StateSetter<string>; eventAnimal: string; setEventAnimal: StateSetter<string>;
|
||||
produceEventId: string; setProduceEventId: StateSetter<string>; produceId: string; setProduceId: StateSetter<string>; produceTradeGoodsId: string; setProduceTradeGoodsId: StateSetter<string>;
|
||||
producePercent: string; setProducePercent: StateSetter<string>; produceValue: string; setProduceValue: StateSetter<string>; produceRadius: string; setProduceRadius: StateSetter<string>; produceX: string; setProduceX: StateSetter<string>; produceY: string; setProduceY: StateSetter<string>; produceZ: string; setProduceZ: StateSetter<string>;
|
||||
giftCode: string; setGiftCode: StateSetter<string>; giftName: string; setGiftName: StateSetter<string>; giftItems: string; setGiftItems: StateSetter<string>; giftCommands: string; setGiftCommands: StateSetter<string>;
|
||||
giftCode: string; setGiftCode: StateSetter<string>; giftName: string; setGiftName: StateSetter<string>; giftItems: GiftItemDraft[]; setGiftItems: StateSetter<GiftItemDraft[]>;
|
||||
giftClass: string; setGiftClass: StateSetter<string>; giftAudience: string; setGiftAudience: StateSetter<string>; giftNumber: string; setGiftNumber: StateSetter<string>; giftAchievement: string; setGiftAchievement: StateSetter<string>; giftAchievementNumber: string; setGiftAchievementNumber: StateSetter<string>;
|
||||
deliveryGift: string; setDeliveryGift: StateSetter<string>; deliveryPlayer: string; setDeliveryPlayer: StateSetter<string>;
|
||||
mapLayers: Record<MapLayer, boolean>; setMapLayers: StateSetter<Record<MapLayer, boolean>>; mapVehicleFilter: MapVehicleFilter; setMapVehicleFilter: StateSetter<MapVehicleFilter>;
|
||||
@@ -603,10 +604,10 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
|
||||
function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, view: ViewState) {
|
||||
const actions = input.workspaceActions;
|
||||
const openNewGift = () => {
|
||||
view.setGiftRecordKey(""); view.setGiftCode(""); view.setGiftName(""); view.setGiftItems(""); view.setGiftCommands(""); view.setGiftClass("5"); view.setGiftAudience("all"); view.setGiftNumber("1"); view.setGiftAchievement("0"); view.setGiftAchievementNumber("0"); view.setGiftDialog({ mode: "edit" });
|
||||
view.setGiftRecordKey(""); view.setGiftCode(""); view.setGiftName(""); view.setGiftItems([]); view.setGiftClass("5"); view.setGiftAudience("all"); view.setGiftNumber("1"); view.setGiftAchievement("0"); view.setGiftAchievementNumber("0"); view.setGiftDialog({ mode: "edit" });
|
||||
};
|
||||
const openGiftDialog = (gift: RecordMap, mode: "view" | "edit") => {
|
||||
view.setGiftRecordKey(textField(gift, "_recordKey", "code", "id")); view.setGiftCode(textField(gift, "code")); view.setGiftName(textField(gift, "name")); view.setGiftClass(numField(gift, "class")); view.setGiftAudience(textField(gift, "audience") || "all"); view.setGiftNumber(numField(gift, "number")); view.setGiftAchievement(numField(gift, "achievement")); view.setGiftAchievementNumber(numField(gift, "achievementNumber", "achievement_number")); view.setGiftItems(giftItemsInput(gift)); view.setGiftCommands(giftCommandsInput(gift)); view.setGiftDialog({ mode, gift });
|
||||
view.setGiftRecordKey(textField(gift, "_recordKey", "code", "id")); view.setGiftCode(textField(gift, "code")); view.setGiftName(textField(gift, "name")); view.setGiftClass(numField(gift, "class")); view.setGiftAudience(textField(gift, "audience") || "all"); view.setGiftNumber(numField(gift, "number")); view.setGiftAchievement(numField(gift, "achievement")); view.setGiftAchievementNumber(numField(gift, "achievementNumber", "achievement_number")); view.setGiftItems(giftItemDrafts(gift)); view.setGiftDialog({ mode, gift });
|
||||
};
|
||||
const saveGift = () => runAction(view.setAction, "正在保存礼包定义…", async () => {
|
||||
const code = view.giftCode.trim();
|
||||
@@ -614,14 +615,13 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
if (!code || !name) throw new Error("礼包编号和名称不能为空。");
|
||||
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(code)) throw new Error("礼包编号只能使用字母、数字、点、下划线或短横线。");
|
||||
const items = parseGiftItems(view.giftItems);
|
||||
const commands = parseGiftCommands(view.giftCommands);
|
||||
if (!items.length && !commands.length) throw new Error("礼包至少需要一项物品或命令。");
|
||||
if (!items.length) throw new Error("礼包至少要包含一项物品。");
|
||||
const number = minimumIntegerInput(view.giftNumber, 1, "发放次数");
|
||||
const achievement = minimumIntegerInput(view.giftAchievement, 0, "成就类型");
|
||||
const achievementNumber = minimumIntegerInput(view.giftAchievementNumber, 0, "成就值");
|
||||
await saveGiftDefinition(actions ?? {}, {
|
||||
code, name, class: integerInput(view.giftClass, 5), audience: view.giftAudience, number,
|
||||
achievement, achievementNumber, items, commands,
|
||||
achievement, achievementNumber, items,
|
||||
status: "active", updatedAt: new Date().toISOString()
|
||||
});
|
||||
if (view.giftRecordKey && view.giftRecordKey !== code) await deleteGiftDefinition(actions ?? {}, view.giftRecordKey);
|
||||
@@ -638,7 +638,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
if (!gift || !player) throw new Error("请选择礼包和用户。");
|
||||
await queueGiftDelivery(actions ?? {}, gift, player);
|
||||
view.refresh();
|
||||
return "礼包发放命令已进入执行队列。";
|
||||
return "礼包发放已进入执行队列。";
|
||||
});
|
||||
const search = view.giftSearch.trim().toLowerCase();
|
||||
const gifts = data.gifts.filter((gift) => {
|
||||
@@ -666,7 +666,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
const status = textField(gift, "status").toLowerCase() || "active";
|
||||
return e("tr", { key: idOf(gift, `gift-${index}`) },
|
||||
e("td", null, e("strong", null, textField(gift, "name") || key), e("span", { className: "provider-id scum-gift-code" }, key)),
|
||||
e("td", null, e("div", { className: "scum-gift-content-cell" }, catalogIconStrip(e, giftCatalogIcons(gift)), e("span", null, giftItemsSummary(gift))), e("span", { className: "provider-id" }, giftCommandsSummary(gift))),
|
||||
e("td", null, e("div", { className: "scum-gift-content-cell" }, catalogIconStrip(e, giftCatalogIcons(gift)), e("span", null, giftItemsSummary(gift)))),
|
||||
e("td", null, e("span", { className: "gift-rule-primary" }, `${giftClassLabel(numField(gift, "class"))} · ${giftAudienceLabel(textField(gift, "audience"))}`), e("span", { className: "provider-id" }, `发放 ${numField(gift, "number")} 次 · 成就 ${numField(gift, "achievement")} / ${numField(gift, "achievementNumber", "achievement_number")}`)),
|
||||
e("td", null, e("div", { className: "gift-status-cell" }, e("span", { className: `status-pill ${activeStatus(status) ? "status-active" : "status-disabled"}` }, giftStatusLabel(status)), e("span", { className: "provider-id" }, dateField(gift, "updatedAt", "createdAt")))),
|
||||
e("td", { className: "provider-actions-cell" }, e("div", { className: "scum-gift-table-actions" },
|
||||
@@ -706,9 +706,33 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
);
|
||||
}
|
||||
|
||||
function giftItemEditorList(e: ReactLike["createElement"], view: ViewState) {
|
||||
if (!view.giftItems.length) return e("p", { className: "page-status" }, "尚未添加物品,请使用目录选择器加入礼包内容。");
|
||||
return e("div", { className: "gift-item-editor-list" }, view.giftItems.map((item, index) => {
|
||||
const entry = scumCatalogEntryFor(item.catalogCode);
|
||||
const supportsDurability = giftItemSupportsDurability(entry);
|
||||
const itemLabel = entry ? scumCatalogDisplayName(entry) : item.catalogCode || "未命名物品";
|
||||
const category = entry ? scumCatalogCategoryLabel(entry) : "物品";
|
||||
const itemNumber = index + 1;
|
||||
return e("div", { key: `${item.catalogCode}-${index}`, className: "gift-item-editor-row" },
|
||||
catalogIconStrip(e, catalogIconsForValue(item.catalogCode)),
|
||||
e("div", { className: "gift-item-editor-info" }, e("strong", null, itemLabel), e("span", { className: "provider-id" }, `${category} · ${item.catalogCode || "未配置代码"}`)),
|
||||
e("div", { className: "gift-item-editor-fields" },
|
||||
labeledField(e, "数量", e("input", { type: "number", min: "1", step: "1", value: item.quantity, "aria-label": `礼包物品 ${itemNumber} 数量`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "quantity", inputValue(event)) })),
|
||||
labeledField(e, "概率", e("input", { type: "number", min: "0", max: "100", step: "1", value: item.probability, "aria-label": `礼包物品 ${itemNumber} 概率`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "probability", inputValue(event)) })),
|
||||
labeledField(e, "耐久", e("input", { type: "number", min: "0", max: "100", step: "1", value: item.durability, disabled: !supportsDurability, placeholder: supportsDurability ? "默认" : "不适用", "aria-label": `礼包物品 ${itemNumber} 耐久`, onChange: (event: InputEvent) => updateGiftItemDraft(view, index, "durability", inputValue(event)) })),
|
||||
e("button", { type: "button", className: "gift-item-editor-remove icon-command", "aria-label": `删除礼包物品 ${itemNumber}`, onClick: () => view.setGiftItems((items) => items.filter((_, itemIndex) => itemIndex !== index)) }, "删除")
|
||||
)
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
function updateGiftItemDraft(view: ViewState, index: number, key: GiftItemDraftField, value: string): void {
|
||||
view.setGiftItems((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item));
|
||||
}
|
||||
|
||||
function giftDetailsDialog(e: ReactLike["createElement"], gift: RecordMap, view: ViewState, editGift: () => void) {
|
||||
const items = giftItemRecords(gift);
|
||||
const commands = giftCommandValues(gift);
|
||||
const status = textField(gift, "status").toLowerCase() || "active";
|
||||
return e("div", { className: "confirm-backdrop management-dialog-backdrop gift-dialog-backdrop", role: "presentation", onClick: () => view.setGiftDialog({ mode: "closed" }) },
|
||||
e("div", { className: "drawer-panel management-dialog-panel management-dialog-wide", role: "dialog", "aria-modal": "true", "aria-label": "查看礼包配置", onClick: (event: InputEvent) => event.stopPropagation?.() },
|
||||
@@ -723,13 +747,12 @@ function giftDetailsDialog(e: ReactLike["createElement"], gift: RecordMap, view:
|
||||
giftConfigEntry(e, "成就门槛", `${numField(gift, "achievement")} / ${numField(gift, "achievementNumber", "achievement_number")}`)
|
||||
),
|
||||
e("section", { className: "gift-form-section gift-config-section", "aria-label": "礼包内容" },
|
||||
e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包内容"), e("span", null, `${items.length} 项物品,${commands.length} 条命令`)),
|
||||
e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包内容"), e("span", null, `${items.length} 项物品`)),
|
||||
items.length ? e("div", { className: "gift-config-list" }, items.map((item, index) => {
|
||||
const code = textField(item, "catalogCode", "catalogItemKey", "key", "className");
|
||||
const entry = scumCatalogEntryFor(code);
|
||||
return e("div", { key: `${code}-${index}`, className: "gift-config-row" }, catalogIconStrip(e, catalogIconsForValue(code)), e("span", null, entry?.name || textField(item, "label") || code || "未命名物品"), e("code", null, code || "unknown"), e("strong", null, `× ${numField(item, "quantity")}`));
|
||||
return e("div", { key: `${code}-${index}`, className: "gift-config-row" }, catalogIconStrip(e, catalogIconsForValue(code)), e("div", { className: "gift-config-item-info" }, e("strong", null, entry ? scumCatalogDisplayName(entry) : textField(item, "label") || code || "未命名物品"), e("span", { className: "provider-id" }, `${entry ? scumCatalogCategoryLabel(entry) : "物品"} · ${code || "unknown"}`)), e("span", null, `数量 × ${numField(item, "quantity")}`), e("span", null, `概率 ${giftItemProbability(item)}%`), e("span", null, `耐久 ${giftItemDurability(item, entry)}`));
|
||||
})) : e("p", { className: "page-status" }, "未配置物品。"),
|
||||
commands.length ? e("div", { className: "gift-command-list", "aria-label": "礼包命令" }, commands.map((command, index) => e("code", { key: `${command}-${index}` }, command))) : e("p", { className: "page-status" }, "未配置附加命令。")
|
||||
),
|
||||
e("div", { className: "confirm-actions gift-dialog-actions" }, e("button", { type: "button", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "关闭"), e("button", { type: "button", className: "confirm-primary", onClick: editGift }, "编辑礼包"))
|
||||
)
|
||||
@@ -743,7 +766,7 @@ function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, action
|
||||
e("div", { className: "drawer-panel management-dialog-panel management-dialog-wide", role: "dialog", "aria-modal": "true", "aria-label": editing ? "编辑礼包" : "新建礼包", onClick: (event: InputEvent) => event.stopPropagation?.() },
|
||||
e("div", { className: "panel-header dialog-panel-header" }, e("div", { className: "gift-dialog-heading" }, e("h2", null, editing ? "编辑礼包" : "新建礼包"), e("span", { className: "page-status" }, editing ? "更新现有礼包定义" : "创建可领取的运营礼包")), e("button", { type: "button", className: "theme-upload drawer-close", "aria-label": "关闭礼包编辑器", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "关闭")),
|
||||
e("div", { className: "gift-form dialog-form management-form" },
|
||||
e("div", { className: "form-guidance management-form-wide" }, e("strong", null, "先定义规则,再配置内容"), e("span", null, "礼包编号用于数据主键,只支持字母、数字、点、下划线和短横线;礼包至少要包含一项物品或一条命令。")),
|
||||
e("div", { className: "form-guidance management-form-wide" }, e("strong", null, "先定义规则,再配置内容"), e("span", null, "礼包编号用于数据主键,只支持字母、数字、点、下划线和短横线;礼包至少要包含一项物品。")),
|
||||
labeledField(e, "礼包编号", e("input", { value: view.giftCode, "aria-label": "礼包编号", placeholder: "例如 welcome_pack", onChange: (event: InputEvent) => view.setGiftCode(inputValue(event)) })),
|
||||
labeledField(e, "礼包名称", e("input", { value: view.giftName, "aria-label": "礼包名称", placeholder: "例如 新人礼包", onChange: (event: InputEvent) => view.setGiftName(inputValue(event)) })),
|
||||
labeledField(e, "礼包周期", e("select", { value: view.giftClass, "aria-label": "礼包周期", onChange: (event: InputEvent) => view.setGiftClass(inputValue(event)) }, [["1", "每日"], ["2", "每周"], ["3", "每月"], ["4", "每年"], ["5", "一次"], ["6", "每日五次"]].map(([value, label]) => e("option", { key: value, value }, label)))),
|
||||
@@ -751,8 +774,7 @@ function giftEditorDialog(e: ReactLike["createElement"], view: ViewState, action
|
||||
labeledField(e, "发放次数", e("input", { value: view.giftNumber, "aria-label": "发放次数", type: "number", min: "1", placeholder: "1", onChange: (event: InputEvent) => view.setGiftNumber(inputValue(event)) })),
|
||||
labeledField(e, "成就类型", e("input", { value: view.giftAchievement, "aria-label": "成就类型", type: "number", min: "0", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievement(inputValue(event)) })),
|
||||
labeledField(e, "成就值", e("input", { value: view.giftAchievementNumber, "aria-label": "成就值", type: "number", min: "0", placeholder: "0", onChange: (event: InputEvent) => view.setGiftAchievementNumber(inputValue(event)) })),
|
||||
e("div", { className: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包物品"), e("span", null, "按 SCUM 目录分类筛选(物品、载具、动物、丧尸、NPC);格式为 目录代码:数量")), labeledField(e, "物品清单", e("input", { value: view.giftItems, "aria-label": "礼包物品", placeholder: "例如 Aloe_Vera:2, BPC_Laika:1, BP_Bear2:2", onChange: (event: InputEvent) => view.setGiftItems(inputValue(event)) })), catalogPicker(e, view, "gift-items")),
|
||||
e("div", { className: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包命令"), e("span", null, "每行一条 SCUM RCON 命令,可选")), labeledField(e, "命令清单", e("textarea", { value: view.giftCommands, "aria-label": "礼包命令", placeholder: "例如 #announce Welcome\n每行一条命令", onChange: (event: InputEvent) => view.setGiftCommands(inputValue(event)) }))),
|
||||
e("div", { className: "gift-form-wide gift-form-section" }, e("div", { className: "gift-form-section-title" }, e("strong", null, "礼包物品"), e("span", null, "可用目录选择器快速加入;每项配置数量、概率和耐久")), giftItemEditorList(e, view), catalogPicker(e, view, "gift-items")),
|
||||
e("div", { className: "confirm-actions gift-dialog-actions" }, e("button", { type: "button", onClick: () => view.setGiftDialog({ mode: "closed" }) }, "取消"), e("button", { type: "button", className: "confirm-primary", disabled: !actions?.pluginData, onClick: saveGift }, "保存礼包"))
|
||||
)
|
||||
)
|
||||
@@ -1366,16 +1388,14 @@ function catalogMatches(query: string, kind: string, target: CatalogPickTarget):
|
||||
const search = query.trim().toLowerCase();
|
||||
return scumCatalogEntries.filter((entry) => {
|
||||
const matchesKind = target === "gift-items" ? kind === "all" || scumGiftCatalogFilterKey(entry) === kind : kind === "all" || entry.kind === kind;
|
||||
return matchesKind && (!search || entry.name.toLowerCase().includes(search) || entry.code.toLowerCase().includes(search) || entry.command.toLowerCase().includes(search));
|
||||
return matchesKind && (!search || scumCatalogSearchText(entry).includes(search));
|
||||
}).slice(0, scumCatalogPickLimit);
|
||||
}
|
||||
|
||||
function appendCatalogItem(current: string, code: string): string {
|
||||
const parts = current.split(",").map((part) => part.trim()).filter(Boolean);
|
||||
const index = parts.findIndex((part) => part.split(":")[0]?.trim().toLowerCase() === code.toLowerCase());
|
||||
if (index >= 0) { const [key, quantity] = parts[index].split(":"); parts[index] = `${key.trim()}:${Math.max(1, Number(quantity) || 1) + 1}`; }
|
||||
else parts.push(`${code}:1`);
|
||||
return parts.join(", ");
|
||||
function appendCatalogItem(items: GiftItemDraft[], code: string): GiftItemDraft[] {
|
||||
const index = items.findIndex((item) => item.catalogCode.toLowerCase() === code.toLowerCase());
|
||||
if (index >= 0) return items.map((item, itemIndex) => itemIndex === index ? { ...item, quantity: String(Math.max(1, Number(item.quantity) || 1) + 1) } : item);
|
||||
return [...items, { catalogCode: code, quantity: "1", probability: "100", durability: "" }];
|
||||
}
|
||||
|
||||
function catalogIconsForValue(value: string): string[] {
|
||||
@@ -1402,20 +1422,23 @@ function catalogPicker(e: ReactLike["createElement"], view: ViewState, target: C
|
||||
view.catalogOpen ? e("div", { className: "scum-catalog-picker-body" },
|
||||
e("input", { type: "search", value: view.catalogSearch, "aria-label": target === "gift-items" ? "搜索礼包目录" : "搜索生成物品", placeholder: "名称 / 代码,例如 Laika", onChange: (event: InputEvent) => view.setCatalogSearch(inputValue(event)) }),
|
||||
e("div", { className: "scum-catalog-kinds", role: "group", "aria-label": target === "gift-items" ? "礼包目录分类" : "目录类型" }, filters.map(([key, label]) => e("button", { key, type: "button", className: activeFilter === key ? "icon-command is-active" : "icon-command", "aria-pressed": activeFilter === key, onClick: () => view.setCatalogKind(key) }, label))),
|
||||
e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => { const icon = scumCatalogIconUrl(entry.icon); return e("button", { key: entry.code, type: "button", className: "scum-catalog-chip", title: `${entry.name} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems(appendCatalogItem(view.giftItems, entry.code)) : view.setProduceTradeGoodsId(entry.code) }, icon ? e("img", { className: "scum-catalog-icon", src: icon, alt: "" }) : e("span", { className: "scum-catalog-icon scum-catalog-icon-empty" }), e("span", { className: "scum-catalog-meta" }, e("strong", null, entry.name), e("span", { className: "provider-id" }, entry.code))); }) : e("p", { className: "page-status" }, "没有匹配的目录条目。"))
|
||||
e("div", { className: "scum-catalog-grid" }, matches.length ? matches.map((entry) => { const icon = scumCatalogIconUrl(entry.icon); const displayName = scumCatalogDisplayName(entry); return e("button", { key: entry.code, type: "button", className: "scum-catalog-chip", title: `${displayName} · ${scumCatalogCategoryLabel(entry)} · ${entry.command}`, onClick: () => target === "gift-items" ? view.setGiftItems((items) => appendCatalogItem(items, entry.code)) : view.setProduceTradeGoodsId(entry.code) }, icon ? e("img", { className: "scum-catalog-icon", src: icon, alt: "" }) : e("span", { className: "scum-catalog-icon scum-catalog-icon-empty" }), e("span", { className: "scum-catalog-meta" }, e("strong", null, displayName), e("span", { className: "provider-id" }, `${scumCatalogCategoryLabel(entry)} · ${entry.code}`))); }) : e("p", { className: "page-status" }, "没有匹配的目录条目。"))
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export function scumGiftCatalogCategory(entry: ScumCatalogEntry): string {
|
||||
const icon = entry.icon.toLowerCase();
|
||||
if (icon.includes("/weapons/") || icon.includes("/crafting/traps/")) return "weapons";
|
||||
const name = `${entry.name} ${entry.code}`.toLowerCase();
|
||||
if (icon.includes("/crafting/traps/") || icon.includes("/weapons/explosives/traps/") || /\b(hunting|bait|trap)\b/.test(name)) return "hunting";
|
||||
if (icon.includes("/fishing/")) return "fishing";
|
||||
if (icon.includes("/food/drinks/")) return "metabolism";
|
||||
if (icon.includes("/medicals/")) return "medical";
|
||||
if (icon.includes("/weapons/")) return "weapons";
|
||||
if (icon.includes("/clothes/")) return "gear";
|
||||
if (icon.includes("/food/")) return "food";
|
||||
if (icon.includes("/medicals/")) return "medical";
|
||||
if (icon.includes("/crafting/basebuilding/")) return "blueprints";
|
||||
if (icon.includes("/crafting/components/") || icon.includes("/misc/components/")) return "components";
|
||||
if (icon.includes("/crafting/basebuilding/") || icon.includes("/crafting/fortifications/")) return "building";
|
||||
if (icon.includes("/crafting/components/") || icon.includes("/misc/components/") || icon.includes("/crafting/recipes/")) return "crafting";
|
||||
if (icon.includes("/crafting/tools/") || icon.includes("/misc/tools/")) return "tools";
|
||||
if (icon.includes("/farming/")) return "farming";
|
||||
return "misc";
|
||||
@@ -1423,7 +1446,7 @@ export function scumGiftCatalogCategory(entry: ScumCatalogEntry): string {
|
||||
|
||||
export function scumGiftCatalogFilterKey(entry: ScumCatalogEntry): string { return entry.kind === "item" ? scumGiftCatalogCategory(entry) : entry.kind; }
|
||||
|
||||
export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "NPC"; return ({ weapons: "武器", fishing: "钓鱼", gear: "装备", food: "食物", medical: "医疗", components: "组件", blueprints: "蓝图", tools: "工具", farming: "农业", misc: "其他" } as Record<string, string>)[scumGiftCatalogCategory(entry)] ?? "其他"; }
|
||||
export function scumCatalogCategoryLabel(entry: ScumCatalogEntry): string { if (entry.kind === "vehicle") return "载具"; if (entry.kind === "animal") return "动物"; if (entry.kind === "zombie") return "丧尸"; if (entry.kind === "armed-npc") return "NPC"; return ({ hunting: "狩猎", food: "食物", metabolism: "代谢", medical: "医疗", weapons: "武器", crafting: "制作", gear: "装备", fishing: "钓鱼", building: "建筑", tools: "工具", farming: "农业", misc: "其他" } as Record<string, string>)[scumGiftCatalogCategory(entry)] ?? "其他"; }
|
||||
function labeledField(e: ReactLike["createElement"], label: string, control: unknown) { return e("label", { className: "console-field" }, e("span", null, label), control); }
|
||||
function detailOpen(event: InputEvent): boolean { return Boolean(event.target?.open); }
|
||||
function giftTabButton(e: ReactLike["createElement"], view: ViewState, tab: GiftTab, label: string) { return e("button", { type: "button", role: "tab", "aria-selected": view.giftTab === tab, className: view.giftTab === tab ? "primary-command" : "icon-command", onClick: () => view.setGiftTab(tab) }, label); }
|
||||
@@ -1563,12 +1586,13 @@ function dateField(row: RecordMap | undefined, ...keys: string[]): string { cons
|
||||
function matchesText(row: RecordMap, search: string, ...keys: string[]): boolean { return !search || keys.some((key) => textField(row, key).toLowerCase().includes(search)); }
|
||||
function unique(values: string[]): string[] { return [...new Set(values)]; }
|
||||
function activeStatus(status: string): boolean { return ["active", "running", "scheduled", "enabled", "queued"].includes(status.toLowerCase()); }
|
||||
function giftItemsSummary(gift: RecordMap): string { const items = field(gift, "items"); if (!Array.isArray(items) || !items.length) return "未配置物品"; return items.map((item) => { if (!isRecord(item)) return String(item); const code = textField(item, "catalogCode", "catalogItemKey", "key", "className"); const entry = scumCatalogEntryFor(code); const label = entry?.name || textField(item, "label") || code || "未命名物品"; return `${label} × ${numField(item, "quantity")}`; }).join(" · "); }
|
||||
function giftCommandsSummary(gift: RecordMap): string { const commands = field(gift, "commands"); return Array.isArray(commands) && commands.length ? `${commands.length} 条命令` : "无命令"; }
|
||||
function giftItemsInput(gift: RecordMap): string { const items = field(gift, "items"); return Array.isArray(items) ? items.map((item) => isRecord(item) ? `${textField(item, "catalogCode", "catalogItemKey", "key")}:${numField(item, "quantity")}` : "").filter(Boolean).join(", ") : ""; }
|
||||
function giftCommandsInput(gift: RecordMap): string { const commands = field(gift, "commands"); return Array.isArray(commands) ? commands.map((item) => isRecord(item) ? textField(item, "command", "value") : String(item)).filter(Boolean).join("\n") : ""; }
|
||||
function giftItemRecords(gift: RecordMap): RecordMap[] { const items = field(gift, "items"); return Array.isArray(items) ? items.filter(isRecord) : []; }
|
||||
function giftCommandValues(gift: RecordMap): string[] { const commands = field(gift, "commands"); return Array.isArray(commands) ? commands.map((item) => isRecord(item) ? textField(item, "command", "value") : String(item)).filter(Boolean) : []; }
|
||||
function giftItemsSummary(gift: RecordMap): string { const items = giftItemRecords(gift); if (!items.length) return "未配置物品"; return items.map((item) => { const code = textField(item, "catalogCode", "catalogItemKey", "key", "className"); const entry = scumCatalogEntryFor(code); const label = entry ? scumCatalogDisplayName(entry) : textField(item, "label") || code || "未命名物品"; return `${label} × ${numField(item, "quantity")} · 概率 ${giftItemProbability(item)}%`; }).join(" · "); }
|
||||
function giftItemDrafts(gift: RecordMap): GiftItemDraft[] { return giftItemRecords(gift).map((item) => ({ catalogCode: textField(item, "catalogCode", "catalogItemKey", "key", "className"), quantity: String(field(item, "quantity") ?? "1"), probability: giftItemProbability(item), durability: giftItemRawDurability(item) })); }
|
||||
function giftItemRecords(gift: RecordMap): RecordMap[] { try { return parseGiftItems(field(gift, "items")).map((item) => ({ ...item })); } catch { return []; } }
|
||||
function giftItemProbability(item: RecordMap): string { const value = Number(field(item, "probability")); return Number.isFinite(value) && value >= 0 && value <= 100 ? String(value) : "100"; }
|
||||
function giftItemRawDurability(item: RecordMap): string { const value = field(item, "durability"); return value === undefined || value === null ? "" : String(value); }
|
||||
function giftItemDurability(item: RecordMap, entry: ScumCatalogEntry | undefined): string { if (!giftItemSupportsDurability(entry)) return "不适用"; const value = giftItemRawDurability(item); return value ? `${value}%` : "默认"; }
|
||||
function giftItemSupportsDurability(entry: ScumCatalogEntry | undefined): boolean { return !entry || entry.kind === "item"; }
|
||||
function giftConfigEntry(e: ReactLike["createElement"], label: string, value: string) { return e("div", { className: "gift-config-entry" }, e("dt", null, label), e("dd", null, value)); }
|
||||
function giftClassLabel(value: string): string { return ({ "1": "每日", "2": "每周", "3": "每月", "4": "每年", "5": "一次", "6": "每日五次" } as Record<string, string>)[value] ?? value; }
|
||||
function giftAudienceLabel(value: string): string { return (({ all: "全部玩家", pve: "PVE 玩家", pvp: "PVP 玩家" } as Record<string, string>)[value] ?? value) || "全部玩家"; }
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { ScumCatalogEntry } from "./scum-catalog.js";
|
||||
|
||||
const directNames: Record<string, string> = {
|
||||
BP_Bear2: "熊",
|
||||
BP_Boar: "野猪",
|
||||
BP_Chicken: "鸡",
|
||||
BP_Crow: "乌鸦",
|
||||
BP_Deer2: "鹿",
|
||||
BP_Donkey2: "驴",
|
||||
BP_Goat2: "山羊",
|
||||
BP_Horse2: "马",
|
||||
BP_Rabbit2: "兔子",
|
||||
BP_Seagull: "海鸥",
|
||||
BP_Wolf3: "狼",
|
||||
WildDuck: "鹿(目录别名)",
|
||||
BP_Drifter_Lvl_1: "武装 NPC 1 级",
|
||||
BP_Drifter_Lvl_2: "武装 NPC 2 级",
|
||||
BP_Drifter_Lvl_3: "武装 NPC 3 级",
|
||||
BP_Drifter_Lvl_4: "武装 NPC 4 级",
|
||||
BP_Drifter_Lvl_5: "武装 NPC 5 级",
|
||||
BP_Guard_Lvl_1: "武装守卫 1 级",
|
||||
BP_Guard_Lvl_2: "武装守卫 2 级",
|
||||
BP_Guard_Lvl_3: "武装守卫 3 级",
|
||||
BP_Guard_Lvl_4: "武装守卫 4 级",
|
||||
BP_Guard_Lvl_5: "武装守卫 5 级",
|
||||
BP_Zombie_Hospital_Normal: "医院普通丧尸",
|
||||
BPC_Laika: "莱卡",
|
||||
BPC_WolfsWagen: "沃尔夫斯瓦根",
|
||||
BPC_SidecarBike: "边车摩托",
|
||||
BPC_Motorcycle: "摩托车",
|
||||
Aloe_Vera: "芦荟",
|
||||
Aspirin: "阿司匹林",
|
||||
Animal_Skin: "动物皮",
|
||||
Air_Pump: "打气筒",
|
||||
Backpack_02_01: "红色背包",
|
||||
"1H_Cleaver": "菜刀",
|
||||
Water_05l: "水",
|
||||
BP_Cash_01: "现金",
|
||||
Cash: "现金",
|
||||
Adrenaline_Shot: "肾上腺素注射剂",
|
||||
Antibiotic_Pill_Single: "抗生素药片",
|
||||
AntiburnGel: "烧伤凝胶",
|
||||
Bandage: "绷带",
|
||||
Bread: "面包",
|
||||
Burger: "汉堡",
|
||||
Canteen: "水壶",
|
||||
Fish_trap: "捕鱼陷阱",
|
||||
FishingRod: "鱼竿",
|
||||
FishingHook: "鱼钩",
|
||||
Bait_trap_for_small_animals: "小型动物诱饵陷阱",
|
||||
Watermelon: "西瓜"
|
||||
};
|
||||
|
||||
const wordNames: Record<string, string> = {
|
||||
absinthe: "苦艾酒", animal: "动物", animals: "动物", apple: "苹果", armor: "护甲", arrow: "箭", arrows: "箭矢", aspirin: "阿司匹林", advanced: "高级", automatic: "自动",
|
||||
backpack: "背包", bait: "诱饵", ball: "球", banana: "香蕉", bandage: "绷带", baseball: "棒球", battery: "电池", bear: "熊", beer: "啤酒", beet: "甜菜", berries: "浆果", blackberry: "黑莓", blueberries: "蓝莓", bolt: "弩箭", bottle: "瓶", bow: "弓", bread: "面包", brick: "砖块", broccoli: "西兰花", buck: "雄鹿", bullet: "子弹", bulletproof: "防弹", burger: "汉堡", burn: "烧伤", cake: "蛋糕", canned: "罐装", carbon: "碳纤维", carrot: "胡萝卜", cartridge: "弹药", cash: "现金", cheese: "奶酪", chicken: "鸡", civilian: "平民", cleaver: "菜刀", cola: "可乐", coffee: "咖啡", compound: "复合", construction: "施工", cooked: "熟制", cooking: "烹饪", crow: "乌鸦", crossbow: "弩", crucian: "鲫鱼", cup: "杯", dark: "深色", deer: "鹿", detonator: "雷管", dildo: "假阳具", donkey: "驴", double: "双头", drink: "饮料", drinks: "饮料", eagle: "鹰", edible: "可食用", energy: "能量", explosive: "爆炸", female: "女性", fish: "鱼", fishing: "钓鱼", floater: "浮漂", freshwater: "淡水", fruit: "水果", gas: "燃气", gasoline: "汽油", goat: "山羊", green: "绿色", grenade: "手雷", grey: "灰色", guts: "内脏", hammer: "锤子", hands: "手部", hat: "帽子", head: "头部", helmet: "头盔", hospital: "医院", hook: "鱼钩", horse: "马", human: "人类", improvised: "简易", injection: "注射剂", iron: "铁制", juice: "果汁", katana: "武士刀", knife: "刀", large: "大型", launcher: "发射器", leather: "皮革", level: "等级", lime: "青柠", lock: "锁", lockpick: "撬锁工具", magazine: "弹匣", male: "男性", medical: "医疗", medium: "中型", metal: "金属", milk: "牛奶", mushroom: "蘑菇", muzzle: "枪口", necklace: "项链", nylon: "尼龙", orange: "橙色", pack: "包", packaged: "包装", painkiller: "止痛药", pepper: "胡椒", pill: "药片", pills: "药片", pizza: "披萨", police: "警用", potato: "土豆", red: "红色", repair: "维修", rifle: "步枪", rope: "绳子", rabbit: "兔子", rice: "米饭", revolver: "左轮手枪", salad: "沙拉", sardine: "沙丁鱼", shotgun: "霰弹枪", shovel: "铲子", small: "小型", soda: "汽水", soup: "汤", stone: "石制", suppressor: "消音器", sword: "剑", syringe: "注射器", tea: "茶", tuna: "金枪鱼", vegetables: "蔬菜", vehicle: "载具", vest: "背心", vitamins: "维生素", vodka: "伏特加", water: "水", watermelon: "西瓜", whiskey: "威士忌", white: "白色", wolf: "狼", wooden: "木制", yellow: "黄色", zombie: "丧尸"
|
||||
};
|
||||
|
||||
const additionalWordNames: Record<string, string> = {
|
||||
activated: "活性炭", alcohol: "酒精", alcoholic: "酒精类", anti: "抗", antibiotic: "抗生素", antimalaria: "抗疟疾", antiparasitic: "抗寄生虫", antipsychotic: "抗精神病", ammunition: "弹药", attachment: "配件", back: "后部", body: "身体", boots: "靴子", box: "盒", bucket: "桶", camp: "营地", charcoal: "木炭", clothes: "服装", component: "组件", crafted: "制作的", crafting: "制作", dirty: "脏污", door: "车门", electrodes: "电极", farming: "农业", feet: "脚部", front: "前部", gear: "装备", gloves: "手套", hiking: "徒步", hood: "引擎盖", inventory: "背包栏", item: "物品", legs: "腿部", light: "轻型", normal: "普通", other: "其他", parts: "零件", rear: "后部", rubber: "橡胶", security: "安全", seat: "座椅", side: "侧面", single: "单发", skin: "皮肤", spare: "备用", standing: "立式", steel: "钢制", tactical: "战术", top: "顶部", trap: "陷阱", underwear: "内衣", weapon: "武器", wheel: "车轮"
|
||||
};
|
||||
|
||||
const phraseNames: Array<readonly [RegExp, string]> = [
|
||||
[/^Armed Guard NPC Level (\d+)$/i, "武装守卫 $1 级"],
|
||||
[/^Armed NPC Level (\d+)$/i, "武装 NPC $1 级"],
|
||||
[/^Zombie (.+) Female$/i, "女性丧尸 $1"],
|
||||
[/^Zombie (.+) Male$/i, "男性丧尸 $1"],
|
||||
[/^Zombie (.+)$/i, "丧尸 $1"],
|
||||
[/^(.+) Fish Bait$/i, "$1 鱼饵"],
|
||||
[/^(.+) Cookbook$/i, "$1 烹饪书"],
|
||||
[/^(.+) Magazine$/i, "$1 弹匣"],
|
||||
[/^(.+) Crossbow Bolt$/i, "$1 弩箭"],
|
||||
[/^(.+) Arrow$/i, "$1 箭"],
|
||||
[/^(.+) Seed Bag$/i, "$1 种子袋"],
|
||||
[/^(.+) Seeds$/i, "$1 种子"],
|
||||
[/^(.+) Juice$/i, "$1 果汁"],
|
||||
[/^(.+) Beer$/i, "$1 啤酒"],
|
||||
[/^(.+) Vodka$/i, "$1 伏特加"],
|
||||
[/^(.+) Water$/i, "$1 水"],
|
||||
[/^(.+) Level (\d+)$/i, "$1 $2 级"]
|
||||
];
|
||||
|
||||
const catalogKindNames: Record<string, string> = { item: "物品", vehicle: "载具", animal: "动物", zombie: "丧尸", "armed-npc": "NPC", razor: "其他" };
|
||||
|
||||
export function scumCatalogNameZh(entry: ScumCatalogEntry): string {
|
||||
const direct = directNames[entry.code];
|
||||
if (direct) return direct;
|
||||
const source = usableName(entry);
|
||||
for (const [pattern, replacement] of phraseNames) if (pattern.test(source)) return translateWords(source.replace(pattern, replacement));
|
||||
const translated = translateWords(source);
|
||||
return translated === source ? `${catalogKindNames[entry.kind] ?? "物品"} · ${source}` : translated;
|
||||
}
|
||||
|
||||
export function scumCatalogDisplayName(entry: ScumCatalogEntry): string { return scumCatalogNameZh(entry); }
|
||||
|
||||
export function scumCatalogSearchText(entry: ScumCatalogEntry): string {
|
||||
return [scumCatalogNameZh(entry), entry.name, entry.code, entry.command].join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function usableName(entry: ScumCatalogEntry): string {
|
||||
return /^ICO[_ .]/i.test(entry.name) || /\.(?:png|webp)$/i.test(entry.name) ? entry.code.replace(/[_-]+/g, " ") : entry.name;
|
||||
}
|
||||
|
||||
function translateWords(value: string): string {
|
||||
const spaced = value.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||
return spaced.replace(/[A-Za-z][A-Za-z'-]*/g, (word) => additionalWordNames[word.toLowerCase()] ?? wordNames[word.toLowerCase()] ?? word);
|
||||
}
|
||||
+4
-2
@@ -3,7 +3,7 @@
|
||||
"title": "SCUMRewardDeliverPayload",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["grantId", "playerId", "items", "operations"],
|
||||
"required": ["grantId", "playerId", "items"],
|
||||
"properties": {
|
||||
"playerId": {
|
||||
"type": "string",
|
||||
@@ -23,7 +23,9 @@
|
||||
"required": ["catalogCode", "quantity"],
|
||||
"properties": {
|
||||
"catalogCode": { "type": "string", "maxLength": 128, "pattern": "^[A-Za-z0-9_.-]{1,128}$" },
|
||||
"quantity": { "type": "integer", "minimum": 1 }
|
||||
"quantity": { "type": "integer", "minimum": 1 },
|
||||
"probability": { "type": "number", "minimum": 0, "maximum": 100 },
|
||||
"durability": { "type": "number", "minimum": 0, "maximum": 100 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user