fix: redesign SCUM gift item lists

This commit is contained in:
npc0-hue
2026-09-20 18:33:31 +08:00
parent 5f2bac3280
commit 1488d5a43e
6 changed files with 267 additions and 83 deletions
+47 -10
View File
@@ -4,9 +4,10 @@ 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, parseGiftCommands, parseGiftItems, queueGiftDelivery, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, 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, 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";
import type { MapTrack, MapTrackPoint } from "../examples/scum-server-plugin/features/contracts.js";
import { renderPluginPage } from "../examples/scum-server-plugin/page-bundle/index.js";
import { configurationCatalog, validateConfigPatch, validateStatePatch, validateVehicleSpawn, vehicleSpawnCatalog } from "../examples/scum-server-plugin/features/schemas.js";
@@ -33,7 +34,7 @@ const surfaceData: SCUMSurfaceData = {
{ code: "#spawnvehicle BPC_Laika_C", className: "BPC_Laika_C", catalogType: "vehicle", type: "21", typeName: "其他载具", imagePath: "/original/BPC_Laika_C.webp", lastSeenAt: "2026-08-10T00:00:03Z" }
],
tradeEvents: [{ steamId: "76561198000000001", itemCode: "goods-1", tradeVerb: "purchased", quantity: "2", price: "120", observedAt: "2026-08-10T00:01:00Z" }],
gifts: [{ code: "starter-pack", name: "Starter Pack", class: 5, audience: "all", number: 1, achievement: 2, achievementNumber: 10, status: "active", items: [{ catalogCode: "BP_Cash_01", quantity: 2 }], commands: [{ command: "#announce Starter pack" }] }],
gifts: [{ code: "starter-pack", name: "Starter Pack", class: 5, audience: "all", number: 1, achievement: 2, achievementNumber: 10, status: "active", items: [{ catalogCode: "Cash", quantity: 2, probability: 100, durability: 65 }] }],
giftClaims: [{ id: "claim-1", playerId: "steam-1", giftCode: "starter-pack", status: "claimed", claimedAt: "2026-08-10T00:03:00Z" }],
pendingGifts: [{ id: "pending-1", playerId: "steam-1", giftCode: "starter-pack", status: "pending", createdAt: "2026-08-10T00:03:30Z" }],
giftDeliveries: [{ id: "delivery-1", playerId: "steam-1", giftCode: "starter-pack", status: "delivered", deliveredAt: "2026-08-10T00:04:00Z" }],
@@ -141,18 +142,30 @@ describe("SCUM plugin feature module", () => {
it("uses transaction, put, and delete for plugin-owned gift data", async () => {
const pluginData = pluginDataActions();
const actions = { pluginData };
expect(parseGiftItems("BP_Cash_01:2, Water-Bottle.01:1")).toEqual([{ catalogCode: "BP_Cash_01", quantity: 2 }, { catalogCode: "Water-Bottle.01", quantity: 1 }]);
expect(parseGiftCommands("#announce Hello\n#spawnitem BP_Cash_01 2")).toEqual([{ command: "#announce Hello" }, { command: "#spawnitem BP_Cash_01 2" }]);
expect(() => parseGiftItems("cash:0")).toThrow("格式无效");
expect(parseGiftItems([{ catalogCode: "BP_Cash_01", quantity: "2", probability: "80", durability: "65" }, { catalogCode: "Water-Bottle.01", quantity: 1, probability: 35 }])).toEqual([{ catalogCode: "BP_Cash_01", quantity: 2, probability: 80, durability: 65 }, { catalogCode: "Water-Bottle.01", quantity: 1, probability: 35 }]);
expect(parseGiftItems("BP_Cash_01:2, Water-Bottle.01:1")).toEqual([{ catalogCode: "BP_Cash_01", quantity: 2, probability: 100 }, { catalogCode: "Water-Bottle.01", quantity: 1, probability: 100 }]);
expect(() => parseGiftItems("cash:0")).toThrow("正整数");
expect(parseGiftItems("a:101,b:1,c:1,d:1,e:1,f:1,g:1,h:1,i:1")).toHaveLength(9);
await saveGiftDefinition(actions, { code: "starter", name: "Starter", items: [] });
await createGiftDelivery(actions, { id: "delivery-1", giftCode: "starter", playerId: "steam-1" });
await deleteGiftDefinition(actions, "starter");
expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.gifts, [{ operation: "put", key: "starter", value: { code: "starter", name: "Starter", items: [] } }]);
await saveGiftDefinition(actions, { code: "legacy", name: "Legacy", items: [{ catalogCode: "Cash", quantity: 1 }], commands: [{ command: "#announce ignored" }] });
expect(pluginData.transact).toHaveBeenLastCalledWith(scumCollections.gifts, [{ operation: "put", key: "legacy", value: { code: "legacy", name: "Legacy", items: [{ catalogCode: "Cash", quantity: 1, probability: 100 }] } }]);
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.giftDeliveries, "delivery-1", expect.objectContaining({ giftCode: "starter", playerId: "steam-1" }));
expect(pluginData.delete).toHaveBeenCalledWith(scumCollections.gifts, "starter");
});
it("selects gift items by probability and keeps the old default at 100 percent", () => {
const items = parseGiftItems([
{ catalogCode: "always", quantity: 1, probability: 100 },
{ catalogCode: "never", quantity: 1, probability: 0 },
{ catalogCode: "half", quantity: 1, probability: 50 }
]);
expect(selectGiftItems(items, () => 0.49).map((item) => item.catalogCode)).toEqual(["always", "half"]);
expect(selectGiftItems(items, () => 0.5).map((item) => item.catalogCode)).toEqual(["always"]);
});
it("persists event produces, event runs, and gift resets in plugin-owned collections", async () => {
const pluginData = pluginDataActions();
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
@@ -170,12 +183,12 @@ describe("SCUM plugin feature module", () => {
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.pendingGifts, "pending-1", expect.objectContaining({ status: "pending", receivedAt: null }));
});
it("queues gift and event commands through platform-mediated SCUM RCON", async () => {
it("queues gift items and event commands through platform-mediated SCUM RCON", async () => {
const pluginData = pluginDataActions();
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
const actions: SCUMWorkspaceActions = { pluginData, dispatch };
await queueGiftDelivery(actions, { ...surfaceData.gifts[0], operations: ["#announce Starter pack", "#SetFamePoints 250"] }, surfaceData.players[0]);
expect(dispatch.mock.calls.map(([request]) => request.payload?.["input.command"])).toEqual(["#SpawnItem BP_Cash_01 2", "#announce Starter pack", "#SetFamePoints 250"]);
await queueGiftDelivery(actions, { ...surfaceData.gifts[0], operations: ["#announce ignored"] }, surfaceData.players[0]);
expect(dispatch.mock.calls.map(([request]) => request.payload?.["input.command"])).toEqual(["#SpawnItem Cash 2 Health 65%"]);
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.giftDeliveries, expect.any(String), expect.objectContaining({ giftCode: "starter-pack", playerId: "steam-1", status: "queued" }));
await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-1" }) }));
@@ -290,7 +303,8 @@ describe("SCUM plugin feature module", () => {
expect(deliveries.texts).toContain("delivered");
expect(deliveries.buttons.find((button) => button.label === "立即发放")?.disabled).toBe(false);
const details = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理", giftDialog: { mode: "view", gift: surfaceData.gifts[0] } });
expect(details.texts).toEqual(expect.arrayContaining(["礼包配置详情", "礼包编号", "starter-pack", "领取周期", "一次", "礼包内容", "BP_Cash_01", "#announce Starter pack"]));
expect(details.texts).toEqual(expect.arrayContaining(["礼包配置详情", "礼包编号", "starter-pack", "领取周期", "一次", "礼包内容", "现金", "其他 · Cash", "数量 × 2", "概率 100%", "耐久 65%"]));
expect(details.texts).not.toContain("#announce Starter pack");
expect(details.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["关闭", "编辑礼包"]));
});
@@ -299,9 +313,12 @@ describe("SCUM plugin feature module", () => {
expect(scumGiftCatalogCategory(entry("1H_Cleaver"))).toBe("weapons");
expect(scumGiftCatalogCategory(entry("Aloe_Vera"))).toBe("food");
expect(scumGiftCatalogCategory(entry("Aspirin"))).toBe("medical");
expect(scumGiftCatalogCategory(entry("Animal_Skin"))).toBe("components");
expect(scumGiftCatalogCategory(entry("Animal_Skin"))).toBe("crafting");
expect(scumGiftCatalogCategory(entry("Air_Pump"))).toBe("tools");
expect(scumGiftCatalogCategory(entry("Backpack_02_01"))).toBe("gear");
expect(scumGiftCatalogCategory(entry("Bait_trap_for_small_animals"))).toBe("hunting");
expect(scumGiftCatalogCategory(entry("Beer"))).toBe("metabolism");
expect(scumGiftCatalogCategory(entry("Advanced_Standing_Torch_01"))).toBe("building");
expect(pageSource).toContain("礼包目录分类");
expect(scumGiftCatalogFilterKey(entry("BPC_Laika"))).toBe("vehicle");
expect(scumGiftCatalogFilterKey(entry("BP_Bear2"))).toBe("animal");
@@ -321,6 +338,14 @@ describe("SCUM plugin feature module", () => {
expect(tradeGoodsTypeLabel({ code: "unknown-item", typeName: "item" })).toBe("未知类型");
});
it("shows Chinese catalog names and searches through the translated name", () => {
const aloe = scumCatalogEntries.find((item) => item.code === "Aloe_Vera")!;
const vehicle = scumCatalogEntries.find((item) => item.code === "BPC_Laika")!;
expect(scumCatalogDisplayName(aloe)).toBe("芦荟");
expect(scumCatalogDisplayName(vehicle)).toBe("莱卡");
expect(scumCatalogSearchText(aloe)).toContain("芦荟");
});
it("uses catalog spawn commands for mixed gift contents", async () => {
const pluginData = pluginDataActions();
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
@@ -413,6 +438,18 @@ describe("SCUM plugin feature module", () => {
expect(workflows.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).not.toContain("giftCommands");
expect(pageSource).not.toContain("parseGiftCommands");
expect(pageSource).not.toContain("礼包命令");
expect(pageSource).not.toContain("命令清单");
expect(dataClientSource).not.toContain("parseGiftCommands");
});
it("focuses the live map on the user who was picked from the user list", () => {
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", pageQuery: { focus: "76561198000000001", from: "2026-08-09T23:00:00Z", to: "2026-08-10T01:00:00Z" } });
expect(view.texts).toContain("用户 1/1");