Improve SCUM map playback and vehicle markers

This commit is contained in:
npc0-hue
2026-09-18 15:53:32 +08:00
parent 4009b7260e
commit 156392df1e
7 changed files with 432 additions and 55 deletions
+32 -3
View File
@@ -5,7 +5,7 @@ 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 { collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.js";
import { advanceMapPlayback, bundledVehicleImageUrl, collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.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";
import { scumMigrationParityFixtures } from "./fixtures/scum-migration-parity.js";
@@ -316,9 +316,11 @@ describe("SCUM plugin feature module", () => {
expect(view.nodes).toContain("details:轨迹回放");
expect(view.texts).not.toContain("轨迹列表");
expect(view.elements.map((element) => element.label)).toContain("轨迹颜色图例");
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["全选", "清空", "不限", "回放", "回到最新"]));
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["全选", "清空", "不限", "开始", "回到最新"]));
expect(pageSource).toContain('new URL("../assets/map/scum-map-building-zones-4096.png", import.meta.url).href');
expect(pageSource).toContain("mapBoardStyle(baseLayer, zonesOverlay)");
expect(pageSource).toContain("map-hover-card");
expect(pageSource).toContain("onPointerEnter");
});
it("draws ridden-vehicle trajectory segments with vehicle icons and per-user selection", () => {
@@ -405,6 +407,31 @@ describe("SCUM plugin feature module", () => {
expect(pageSource).toContain("setInterval(refresh, scumSurfaceRefreshMs)");
expect(pageSource).toContain("clearInterval(interval)");
});
it("never lets a collector dump path reach a map image", () => {
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" });
expect(bundledVehicleImageUrl("/original/Rager_ES.webp")).toBe("");
expect(bundledVehicleImageUrl("/@fs/tmp/scum-catalog/items/vehicles/ico_rager.webp")).toBe("/@fs/tmp/scum-catalog/items/vehicles/ico_rager.webp");
expect(bundledVehicleImageUrl("data:image/svg+xml,%3Csvg")).toBe("data:image/svg+xml,%3Csvg");
expect(view.images.some((src) => src.includes("/original/"))).toBe(false);
// vitest resolves bundled assets to file:// URLs; the browser sees /@fs/ or /assets/.
expect(view.images.every((src) => src === "" || /^(?:data|blob|file):/.test(src) || /^\/(?:@fs|assets)\//.test(src))).toBe(true);
expect(view.images.some((src) => src.includes("ico_"))).toBe(true);
});
it("does not assign a vehicle fallback icon to ordinary player markers", () => {
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: { ...surfaceData, vehicles: [], vehicleLocks: [], trajectories: [], mapPoints: [], flags: [], mapRegions: [] } });
expect(view.images.some((src) => src.startsWith("data:image/svg+xml"))).toBe(false);
});
it("runs playback on wall-clock time so the speed switch is instant", () => {
const half = advanceMapPlayback({ playing: true, value: 0, speed: 1, startedAt: 1_000, startValue: 0 }, 16_000);
expect(half.value).toBeCloseTo(0.5, 6);
const done = advanceMapPlayback({ playing: true, value: 0, speed: 4, startedAt: 0, startValue: 0 }, 7_500);
expect(done.playing).toBe(false);
expect(done.value).toBe(1);
const paused = advanceMapPlayback({ playing: false, value: 0.3, speed: 1, startedAt: 0, startValue: 0 }, 900_000);
expect(paused.value).toBeCloseTo(0.3, 6);
});
});
function pluginDataActions(overrides: Partial<{ list: (collection: string, key?: string) => Promise<unknown> }> = {}) {
@@ -434,6 +461,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
const buttons: Array<{ label: string; disabled: boolean; onClick?: () => void }> = [];
const inputs: Array<{ label: string; value: unknown; onChange?: (event: unknown) => void }> = [];
const elements: Array<{ label: string; className: string; style?: Record<string, unknown> }> = [];
const images: string[] = [];
const collectText = (value: unknown): void => { if (typeof value === "string") texts.push(value); else if (Array.isArray(value)) value.forEach(collectText); else if (value && typeof value === "object" && "children" in value) collectText((value as { children?: unknown }).children); };
let stateCall = 0;
const react = {
@@ -443,6 +471,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
children.forEach(collectText);
if (type === "button") buttons.push({ label: String(children[0]), disabled: Boolean(props?.disabled), onClick: props?.onClick as (() => void) | undefined });
if (type === "input" || type === "select" || type === "textarea") inputs.push({ label: String(props?.["aria-label"] ?? ""), value: props?.value ?? props?.checked, onChange: props?.onChange as ((event: unknown) => void) | undefined });
if (type === "img") images.push(String(props?.src ?? ""));
return { type, props, children };
},
useEffect: () => undefined,
@@ -462,5 +491,5 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
availability: { available: true, features: [{ key: "player.intelligence", available: true }] },
workspaceActions: actions
});
return { nodes, texts, buttons, inputs, elements, actions };
return { nodes, texts, buttons, inputs, elements, images, actions };
}