feat: simplify SCUM activity management

This commit is contained in:
npc0-hue
2026-09-20 21:22:15 +08:00
parent 1488d5a43e
commit ab1cd01351
5 changed files with 217 additions and 157 deletions
+26 -17
View File
@@ -4,7 +4,7 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { migrateConfigurationRecord, migrateGiftGrantRecord, migratePlayerProfileRecord, migratePlayerRecord, migrateStatePatchRecord, migrateTrajectoryHistoryRecord, migrateTrajectoryRecord, migrationStatus } from "../examples/scum-server-plugin/features/migration.js";
import { createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, parseGiftItems, queueGiftDelivery, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, selectGiftItems, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
import { createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, parseGiftItems, queueGiftDelivery, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventDefinition, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, selectGiftItems, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
import { advanceMapPlayback, bundledVehicleImageUrl, collectMapPoints, mapPointStyle, mapVehicleCategory, playbackStartView, playbackFollowView, scumCatalogCategoryLabel, scumGiftCatalogCategory, scumGiftCatalogFilterKey, tradeGoodsTypeLabel } from "../examples/scum-server-plugin/features/page.js";
import { scumCatalogEntries } from "../examples/scum-server-plugin/features/scum-catalog.js";
import { scumCatalogDisplayName, scumCatalogSearchText } from "../examples/scum-server-plugin/features/scum-catalog-zh.js";
@@ -134,9 +134,9 @@ describe("SCUM plugin feature module", () => {
it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => {
const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 }));
await loadSCUMSurface({ pluginData: pluginDataActions({ list }), scum: scumActions() }, "workflows");
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents, scumCollections.tradeGoods, scumCollections.tradeEvents]);
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.mapRegions, scumCollections.mapSettings]);
await loadSCUMSurface({ pluginData: pluginDataActions({ list }), scum: scumActions() }, "activity");
expect(list.mock.calls.slice(-8).map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents, scumCollections.tradeGoods, scumCollections.tradeEvents]);
expect(list.mock.calls.slice(-5).map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.mapRegions, scumCollections.mapSettings]);
});
it("uses transaction, put, and delete for plugin-owned gift data", async () => {
@@ -170,6 +170,8 @@ describe("SCUM plugin feature module", () => {
const pluginData = pluginDataActions();
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
const actions: SCUMWorkspaceActions = { pluginData, dispatch };
await saveEventDefinition(actions, { id: "event-list", title: "补给活动", items: [{ catalogCode: "Cash", quantity: "2", probability: "65", durability: "80" }] });
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.events, "event-list", expect.objectContaining({ title: "补给活动", items: [{ catalogCode: "Cash", quantity: 2, probability: 65, durability: 80 }] }));
await saveEventProduce(actions, { id: "produce-1", eventId: "event-1", tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 });
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.eventProduces, "event-1:produce-1", expect.objectContaining({ eventId: "event-1", tradeGoodsId: "goods-1" }));
await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces);
@@ -268,18 +270,21 @@ describe("SCUM plugin feature module", () => {
expect(view.texts.join("\n")).toContain("verified");
});
it("renders activity definitions, status filters, runs, and records", () => {
it("renders only a collapsed activity list and expands into area and reward configuration", () => {
const view = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理" });
expect(view.inputs.map((input) => input.label)).toContain("活动状态");
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["生成类型", "RCON 命令", "活动公告", "活动概率", "活动持续秒数", "生成物品编号", "生成半径", "生成 X", "生成 Y", "生成 Z"]));
expect(view.inputs.map((input) => input.label)).not.toEqual(expect.arrayContaining(["RCON 命令", "活动公告", "生成物品编号", "生成 X", "生成 Y", "生成 Z"]));
expect(view.texts).toContain("Friday Range");
expect(view.texts).toContain("running");
expect(view.texts).toContain("最近活动记录");
expect(view.texts).toContain("Mira");
expect(view.texts).toContain("活动生成项");
expect(view.texts).toEqual(expect.arrayContaining(["物品列表", "最近商人交易", "Cargo Drop", "买入 × 2"]));
expect(pageSource).toContain("setEventEditorOpen(detailOpen(event))");
expect(pageSource).not.toContain("open: Boolean(view.eventId || view.eventName)");
expect(view.texts).not.toEqual(expect.arrayContaining(["最近活动记录", "活动生成项", "物品列表", "最近商人交易", "Quest / Task", "原生赛事轮次"]));
expect(view.buttons.map((button) => button.label)).toContain("新建活动");
expect(pageSource).toContain("activityMapPicker(e, data, view)");
expect(pageSource).toContain("活动物品 / NPC");
expect(pageSource).not.toContain("setEventEditorOpen(detailOpen(event))");
const expanded = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理", activityDetailId: "event-1" });
expect(expanded.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["活动标题", "活动区域名称", "活动区域半径", "活动区域 X", "活动区域 Y", "活动区域 Z"]));
expect(expanded.nodes).toContain("div:选择活动区域地图");
expect(expanded.texts).toContain("从 SCUM 目录选择活动物品 / NPC");
});
it("renders gift definitions, claims, and delivery records", () => {
@@ -434,15 +439,18 @@ describe("SCUM plugin feature module", () => {
expect(gifts.texts).not.toContain("从 SCUM 目录选择礼包物品");
expect(gifts.inputs.map((input) => input.label)).not.toContain("搜索礼包物品");
const workflows = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理" });
expect(workflows.texts).toContain("从 SCUM 目录选择物品");
expect(workflows.inputs.map((input) => input.label)).not.toContain("搜索生成物品");
expect(workflows.texts).not.toContain("从 SCUM 目录选择活动物品 / NPC");
const expanded = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理", activityDetailId: "event-1" });
expect(expanded.texts).toContain("从 SCUM 目录选择活动物品 / NPC");
expect(expanded.inputs.map((input) => input.label)).not.toContain("搜索生成物品");
});
it("keeps gifts as an item list with probability and durability instead of command text", () => {
expect(pageSource).toContain("礼包物品");
expect(pageSource).toContain("礼包物品 ${itemNumber} 数量");
expect(pageSource).toContain("礼包物品 ${itemNumber} 概率");
expect(pageSource).toContain("礼包物品 ${itemNumber} 耐久");
expect(pageSource).toContain("${labelPrefix} ${itemNumber} 数量");
expect(pageSource).toContain("${labelPrefix} ${itemNumber} 概率");
expect(pageSource).toContain("${labelPrefix} ${itemNumber} 耐久");
expect(pageSource).toContain("activityItemEditorList");
expect(pageSource).not.toContain("giftCommands");
expect(pageSource).not.toContain("parseGiftCommands");
expect(pageSource).not.toContain("礼包命令");
@@ -591,7 +599,7 @@ function scumActions(overrides: Partial<NonNullable<SCUMWorkspaceActions["scum"]
};
}
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; giftDialog?: { mode: "closed" | "view" | "edit"; gift?: RecordMap }; playerSearch?: string; pageQuery?: Record<string, string> } = {}) {
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; giftDialog?: { mode: "closed" | "view" | "edit"; gift?: RecordMap }; activityDetailId?: string; playerSearch?: string; pageQuery?: Record<string, string> } = {}) {
const nodes: string[] = [];
const texts: string[] = [];
const buttons: Array<{ label: string; disabled: boolean; onClick?: () => void }> = [];
@@ -615,6 +623,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
stateCall += 1;
if (stateCall === 1) return [{ status: "ready", data: options.data ?? surfaceData } as T, () => undefined];
if (stateCall === 3 && options.playerSearch !== undefined) return [options.playerSearch as T, () => undefined];
if (stateCall === 9 && options.activityDetailId !== undefined) return [options.activityDetailId as T, () => undefined];
if (options.giftDialog && typeof initial === "object" && initial !== null && "mode" in initial && (initial as { mode?: string }).mode === "closed") return [options.giftDialog as T, () => undefined];
const value = typeof initial === "function" ? (initial as () => T)() : initial;
if (options.giftTab && value === "definitions") return [options.giftTab as T, () => undefined];