diff --git a/plugins/examples/scum-server-plugin/features/page.ts b/plugins/examples/scum-server-plugin/features/page.ts index de39fc2..dd43e4f 100644 --- a/plugins/examples/scum-server-plugin/features/page.ts +++ b/plugins/examples/scum-server-plugin/features/page.ts @@ -36,6 +36,7 @@ type PointerEventLike = { pointerId?: number; clientX?: number; clientY?: number const scumMapLayerImages = { terrain: new URL("../assets/map/scum-map-terrain-4096.webp", import.meta.url).href, topo: new URL("../assets/map/scum-map-topo-4096.webp", import.meta.url).href, night: new URL("../assets/map/scum-map-night-4096.webp", import.meta.url).href, zones: new URL("../assets/map/scum-map-building-zones-4096.png", import.meta.url).href } as const; const scumMapBaseLayers: ReadonlyArray = [["terrain", "地形"], ["topo", "等高线"], ["night", "夜间"]]; const scumMapVehicleFilters: ReadonlyArray = [["all", "全部载具"], ["land", "陆地载具"], ["water", "水上载具"], ["air", "飞行类载具"], ["other", "其他载具"]]; +const scumFlightVehicleKeys = new Set(["kinglet_duster", "kinglet_mariner", "kinglet_scout"]); const scumCatalogKinds: ReadonlyArray = [["all", "全部"], ["item", "物品"], ["vehicle", "载具"], ["zombie", "丧尸"], ["animal", "动物"], ["armed-npc", "NPC"]]; const scumCatalogPickLimit = 48; const scumMapSize = 4096; @@ -1408,14 +1409,16 @@ function positionOf(row: RecordMap | undefined): RecordMap | undefined { const n function hasCoordinates(row: RecordMap | undefined): row is RecordMap { return Boolean(row) && Number.isFinite(Number(field(row, "x", "locationX"))) && Number.isFinite(Number(field(row, "y", "locationY"))); } function layerOf(point: RecordMap): MapLayer { const value = textField(point, "layer", "subjectType", "type").toLowerCase(); if (value.includes("player") || value.includes("user")) return "players"; if (value.includes("vehicle")) return "vehicles"; if (value.includes("flag")) return "flags"; if (value.includes("region") || value.includes("zone") || value === "base") return "regions"; return "other"; } function layerLabel(layer: MapLayer): string { return layer === "players" ? "用户" : layer === "vehicles" ? "载具" : layer === "flags" ? "旗帜" : layer === "regions" ? "区域" : "其他"; } -function mapVehicleCategory(point: RecordMap): MapVehicleFilter { +export function mapVehicleCategory(point: RecordMap): MapVehicleFilter { const entry = scumVehicleEntry(textField(point, "className", "vehicleClass", "entityClass", "vehicleType", "name", "label")); - const value = [textField(point, "vehicleType", "className", "vehicleClass", "entityClass", "name", "label"), entry?.code || "", entry?.name || ""].join(" ").toLowerCase(); - if (/kinglet|duster|mariner|scout|aircraft|airplane|aeroplane|helicopter|plane|flight|飞行/.test(value)) return "air"; + const values = [textField(point, "vehicleType", "className", "vehicleClass", "entityClass", "name", "label"), entry?.code || "", entry?.name || ""]; + if (values.some((value) => scumFlightVehicleKeys.has(mapVehicleKey(value)))) return "air"; + const value = values.join(" ").toLowerCase(); if (/boat|dinghy|raft|motorboat|barba|sup|water|船|水上/.test(value)) return "water"; if (/bike|bicycle|motorcycle|sidecar|cruiser|laika|rager|ris|wolfswagen|tractor|pickup|suv|quad|sportbike|wheelbarrow|car|land|陆地/.test(value)) return "land"; return "other"; } +function mapVehicleKey(value: string): string { return normalizedVehicleClass(value.replace(/^#spawnvehicle\s+/i, "")).replace(/^(?:bpc_|bp_)/i, "").replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toLowerCase(); } function pointTitle(point: RecordMap): string { return textField(point, "displayName", "name", "label", "subjectName") || textField(point, "subjectType", "type") || textField(point, "subjectId", "id") || "地图点"; } function mapPointIdentity(point: RecordMap): string { const subject = textField(point, "subjectId", "gamePlayerId", "vehicleId", "flagId", "regionId"); if (subject) return `${layerOf(point)}:${subject}`; const record = textField(point, "id", "_recordKey"); if (record) return `${layerOf(point)}:${record}`; return `${layerOf(point)}:${pointTitle(point).toLowerCase()}:${numField(point, "x", "locationX")}:${numField(point, "y", "locationY")}`; } export function mapScenePoint(point: RecordMap, bounds: RecordMap): { x: number; y: number } { diff --git a/plugins/tests/scum-feature-module.test.ts b/plugins/tests/scum-feature-module.test.ts index f524307..79a5726 100644 --- a/plugins/tests/scum-feature-module.test.ts +++ b/plugins/tests/scum-feature-module.test.ts @@ -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 { advanceMapPlayback, bundledVehicleImageUrl, collectMapPoints, mapPointStyle, playbackStartView, playbackFollowView } from "../examples/scum-server-plugin/features/page.js"; +import { advanceMapPlayback, bundledVehicleImageUrl, collectMapPoints, mapPointStyle, mapVehicleCategory, playbackStartView, playbackFollowView } from "../examples/scum-server-plugin/features/page.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"; @@ -346,6 +346,14 @@ describe("SCUM plugin feature module", () => { expect(view.texts).toContain("Mira · 4 点"); }); + it("recognizes only the three Kinglet aircraft names in the flight filter", () => { + expect(mapVehicleCategory({ className: "Kinglet_Duster_ES" })).toBe("air"); + expect(mapVehicleCategory({ className: "BPC_Kinglet_Mariner_C" })).toBe("air"); + expect(mapVehicleCategory({ label: "Kinglet Scout" })).toBe("air"); + expect(mapVehicleCategory({ className: "BPC_Laika_C" })).not.toBe("air"); + expect(mapVehicleCategory({ className: "Scout_ES" })).not.toBe("air"); + }); + it("keeps the extracted icon catalog closed until an editor asks for it", () => { const gifts = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" }); expect(gifts.texts).not.toContain("从 SCUM 目录选择礼包物品");