Add SCUM trade catalog projections
This commit is contained in:
@@ -456,6 +456,8 @@ describe("plugin manifest validation", () => {
|
||||
maxPayloadBytes: number;
|
||||
}>;
|
||||
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
||||
queryTemplates: Array<{ key: string; projections?: Array<{ collection?: string; fixedValues?: Record<string, string>; mergeExisting?: boolean }> }>;
|
||||
logProjections?: Array<{ key: string; streamKeys?: string[]; target?: { collection?: string; upsertKeys?: string[]; captureMappings?: Record<string, string> } }>;
|
||||
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||
};
|
||||
pages: Array<{ key: string; permissions?: string[] }>;
|
||||
@@ -496,6 +498,12 @@ describe("plugin manifest validation", () => {
|
||||
"maintenance.prepare"
|
||||
]));
|
||||
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]));
|
||||
expect(manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.vehicles")?.projections).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ collection: "scum_trade_goods", mergeExisting: true, fixedValues: expect.objectContaining({ catalogType: "vehicle", type: "21", typeName: "其他载具" }) })
|
||||
]));
|
||||
expect(manifest.gameClientBridge.logProjections?.map((projection) => projection.key)).toEqual(expect.arrayContaining(["scum.trade.catalog", "scum.trade.events"]));
|
||||
expect(manifest.gameClientBridge.logProjections?.find((projection) => projection.key === "scum.trade.catalog")).toMatchObject({ streamKeys: ["scum.trade"], target: { collection: "scum_trade_goods", upsertKeys: ["code"], captureMappings: { code: "itemCode" } } });
|
||||
expect(manifest.gameClientBridge.logProjections?.find((projection) => projection.key === "scum.trade.events")?.target?.collection).toBe("scum_trade_events");
|
||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||
@@ -507,7 +515,7 @@ describe("plugin manifest validation", () => {
|
||||
expect(manifest.fileWorkspace?.files.map((file) => file.key)).toEqual(expect.arrayContaining(["scum-server-settings", "scum-admin-users", "scum-chat-log", "scum-performance-log"]));
|
||||
expect(manifest.fileWorkspace?.configFields.map((field) => field.key)).toEqual(expect.arrayContaining(["server-name", "game-port", "query-port", "max-players", "welcome-message"]));
|
||||
expect(manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "scum-client")?.capabilities).not.toContain("remote.run.rcon.command");
|
||||
expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-client-events"]));
|
||||
expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-trade-events", "scum-client-events"]));
|
||||
});
|
||||
|
||||
it("declares bounded and permissioned SCUM bridge commands", () => {
|
||||
|
||||
@@ -24,6 +24,11 @@ const surfaceData: SCUMSurfaceData = {
|
||||
nativeEventRounds: [{ eventRecordId: "native-1", eventId: "native-event", state: "active", startTime: "2026-08-10T00:00:00Z", enemyKills: 2 }],
|
||||
tasks: [{ taskRecordId: "task-1", taskKind: "active-task", state: "active", userProfileId: "profile-1" }],
|
||||
activityEvents: [{ id: "activity-1", type: "reward", subjectName: "Mira", status: "delivered", occurredAt: "2026-08-10T00:02:00Z" }],
|
||||
tradeGoods: [
|
||||
{ code: "goods-1", name: "Cargo Drop", catalogType: "item", lastSeenAt: "2026-08-10T00:01:00Z" },
|
||||
{ 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" }] }],
|
||||
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" }],
|
||||
@@ -32,7 +37,7 @@ const surfaceData: SCUMSurfaceData = {
|
||||
mapPoints: [{ id: "poi-1", name: "Airfield", layer: "other", x: 800, y: 900, z: 10, source: "plugin-map" }],
|
||||
mapRegions: [{ id: "region-1", name: "Safe Zone", x: 500, y: 600, z: 0, source: "server-config" }],
|
||||
mapSettings: [],
|
||||
vehicles: [{ vehicleId: "veh-1", label: "Laika", position: { x: 400, y: 200, z: 0 }, freshness: { status: "fresh" } }],
|
||||
vehicles: [{ vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", position: { x: 400, y: 200, z: 0 }, freshness: { status: "fresh" } }],
|
||||
trajectories: [
|
||||
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.positions" },
|
||||
{ subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.vehicles" }
|
||||
@@ -84,7 +89,7 @@ describe("SCUM plugin feature module", () => {
|
||||
it("loads page data only through scoped plugin collections", async () => {
|
||||
const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 }));
|
||||
const data = await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "gifts");
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents, scumCollections.players]);
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents, scumCollections.players, scumCollections.tradeGoods]);
|
||||
expect(data.gifts[0]).toMatchObject({ collection: scumCollections.gifts, _recordKey: `${scumCollections.gifts}-1` });
|
||||
});
|
||||
|
||||
@@ -104,9 +109,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 }) }, "workflows");
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents]);
|
||||
expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents, scumCollections.tradeGoods, scumCollections.tradeEvents]);
|
||||
await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "activity");
|
||||
expect(list.mock.calls.slice(-6).map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents]);
|
||||
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]);
|
||||
});
|
||||
|
||||
it("uses transaction, put, and delete for plugin-owned gift data", async () => {
|
||||
@@ -239,6 +244,7 @@ describe("SCUM plugin feature module", () => {
|
||||
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)");
|
||||
});
|
||||
@@ -246,7 +252,9 @@ describe("SCUM plugin feature module", () => {
|
||||
it("renders gift definitions, claims, and delivery records", () => {
|
||||
const definitions = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" });
|
||||
expect(definitions.texts).toContain("礼包定义");
|
||||
expect(definitions.texts).toContain("物品列表");
|
||||
expect(definitions.texts).toContain("Starter Pack");
|
||||
expect(definitions.texts).toContain("Cargo Drop");
|
||||
expect(definitions.buttons.find((button) => button.label === "保存礼包")?.disabled).toBe(false);
|
||||
expect(definitions.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["礼包周期", "适用玩家", "发放次数", "成就类型", "成就值", "礼包物品", "礼包命令"]));
|
||||
const claims = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理", giftTab: "claims" });
|
||||
@@ -274,12 +282,15 @@ describe("SCUM plugin feature module", () => {
|
||||
it("deduplicates map entities, keeps every point, and computes custom map bounds", async () => {
|
||||
const duplicateData: SCUMSurfaceData = { ...surfaceData, mapPoints: [{ id: "direct-player", subjectType: "player", subjectId: "76561198000000001", name: "Mira", x: 10, y: 20, z: 3 }, ...Array.from({ length: 260 }, (_, index) => ({ id: `poi-${index}`, name: `POI ${index}`, layer: "other", x: index * 10, y: index * 10 }))] };
|
||||
expect(collectMapPoints(duplicateData)).toHaveLength(264);
|
||||
expect(collectMapPoints(surfaceData).find((point) => point.vehicleId === "veh-1")).toMatchObject({ imagePath: "/original/BPC_Laika_C.webp", spawnCommand: "#spawnvehicle BPC_Laika_C" });
|
||||
const bounds = resolveMapBounds({ customMapEnabled: true, centerX: 100000, centerY: 200000, widthKm: 4, heightKm: 2 });
|
||||
expect(bounds).toEqual({ worldMinX: -100000, worldMinY: 100000, worldMaxX: 300000, worldMaxY: 300000 });
|
||||
expect(mapPointStyle({ x: -100000, y: 100000 }, bounds)).toEqual({ left: "99%", top: "99%" });
|
||||
const pluginData = pluginDataActions();
|
||||
await saveMapSettings({ pluginData }, { customMapEnabled: true, centerX: 100000, centerY: 200000, widthKm: 4, heightKm: 2 });
|
||||
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.mapSettings, "current", expect.objectContaining(bounds));
|
||||
expect(pageSource).toContain('textField(point, "imagePath", "image_path")');
|
||||
expect(pageSource).toContain('"className", "vehicleClass", "entityClass", "vehicleType"');
|
||||
expect(pageSource).not.toContain("visible.slice(0, 240)");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user