Rebuild the SCUM live map with zoom, pan, and trajectory tracking

This commit is contained in:
npc0-hue
2026-09-16 10:48:54 +08:00
parent f2a9c7b1ca
commit 0ce264836f
5 changed files with 401 additions and 56 deletions
+1 -1
View File
@@ -313,7 +313,7 @@ describe("plugin manifest validation", () => {
const serialized = JSON.stringify(manifest).toLowerCase();
expect(serialized).not.toContain("local-proof");
expect(manifest.version).toBe("0.1.18");
expect(manifest.version).toBe("0.1.19");
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
+31 -9
View File
@@ -282,30 +282,52 @@ describe("SCUM plugin feature module", () => {
it("renders map layers, filter controls, points, and selected-point details", () => {
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" });
expect(view.nodes).toContain("div:SCUM 地图图层");
expect(view.inputs.map((input) => input.label)).toContain("筛选地图点");
expect(view.nodes).toContain("div:SCUM 地图视图");
expect(view.inputs.map((input) => input.label)).toContain("轨迹时间范围");
expect(view.inputs.map((input) => input.label)).not.toContain("筛选地图点");
for (const layer of ["用户", "载具", "旗帜", "区域", "其他"]) expect(view.texts).toContain(layer);
expect(view.texts).toContain("地图点详情");
expect(view.texts).toContain("Airfield");
expect(view.texts.join("\n")).toContain("X 800 / Y 900 / Z 10");
expect(pageSource).toContain('new URL("../assets/map/scum-map-terrain-4096.webp", import.meta.url).href');
expect(view.elements.find((element) => element.label === "SCUM 地图图层")?.style?.backgroundImage).toContain("scum-map-terrain-4096.webp");
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["启用自定义地图", "地图中心 X", "地图中心 Y", "地图宽度公里", "地图高度公里"]));
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["", "", "复位", "适应轨迹"]));
const vehicleView = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: { ...surfaceData, players: [], mapPoints: [], flags: [], mapRegions: [] } });
expect(vehicleView.texts).toEqual(expect.arrayContaining(["BPC_Laika_C · unknown · 已上锁 · 访问 unknown", "scum-user-1", "76561198000000001"]));
});
it("keeps the live map full width with one compact filter row", () => {
it("keeps the live map full width with one compact filter row and dropdown menus", () => {
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" });
expect(view.texts).not.toContain("载具目录");
expect(view.texts).not.toContain("地图点");
expect(view.texts.some((text) => /^\d+ \/ \d+ 个可见点$/.test(text))).toBe(true);
expect(view.texts.some((text) => /\d+ \/ \d+ 个点 · \d+ 条轨迹/.test(text))).toBe(true);
for (const layer of ["地形", "等高线", "夜间", "建筑区"]) expect(view.texts).toContain(layer);
expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["地形", "等高线", "夜间"]));
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["筛选地图点"]));
expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["地形", "等高线", "夜间"]));
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹时间范围", "地图底图", "轨迹回放进度"]));
expect(view.inputs.map((input) => input.label)).not.toContain("启用自定义地图");
expect(view.texts).not.toContain("地图范围与图层");
expect(view.texts).not.toContain("自定义范围");
expect(view.elements.map((element) => element.label)).toEqual(expect.arrayContaining(["轨迹用户筛选", "地图图层筛选"]));
expect(view.texts).toContain("轨迹列表");
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)");
});
it("draws ridden-vehicle trajectory segments with vehicle icons and per-user selection", () => {
const riding: SCUMSurfaceData = { ...surfaceData, trajectories: [
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:01Z" },
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 60, y: 80, z: 3, sampledAt: "2026-08-10T00:00:02Z" },
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 120, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:03Z" },
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 180, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:04Z" }
] };
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: riding });
expect(view.nodes).toContain("polyline:");
expect(view.texts).toEqual(expect.arrayContaining(["用户 · 4 点 · 步行 2 / 乘车 2"]));
expect(view.elements.filter((element) => element.className.includes("map-ride-marker")).length).toBeGreaterThan(0);
expect(view.elements.filter((element) => element.className.includes("map-track-endpoint")).length).toBeGreaterThan(1);
expect(view.texts).toContain("Mira · 4");
});
it("keeps the extracted icon catalog closed until an editor asks for it", () => {
const gifts = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" });
expect(gifts.texts).toContain("从 SCUM 目录选择礼包物品");
@@ -378,13 +400,13 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
const texts: string[] = [];
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; style?: Record<string, unknown> }> = [];
const elements: Array<{ label: string; className: string; style?: Record<string, unknown> }> = [];
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 = {
createElement: (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => {
if (typeof type === "string") nodes.push(`${type}:${String(props?.["aria-label"] ?? "")}`);
if (typeof type === "string") elements.push({ label: String(props?.["aria-label"] ?? ""), style: props?.style as Record<string, unknown> | undefined });
if (typeof type === "string") elements.push({ label: String(props?.["aria-label"] ?? ""), className: String(props?.className ?? ""), style: props?.style as Record<string, unknown> | undefined });
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 });