@@ -19,15 +19,20 @@ import {
type SCUMSurfaceData ,
type SCUMWorkspaceActions
} from "./page-data.js" ;
import { scumVehicleIconUrl } from "./scum-catalog.js" ;
import { scumCatalogEntries , scumCatalogEntryFor , scumCatalogIconUrl , scumVehicleIconUrl , type ScumCatalogEntry } from "./scum-catalog.js" ;
type StateSetter < T > = ( next : T | ( ( previous : T ) = > T ) ) = > void ;
type InputEvent = { target ? : { value? : string ; checked? : boolean ; open? : boolean } ; stopPropagation ? : ( ) = > void } ;
type GiftTab = "definitions" | "claims" | "deliveries" | "timed" ;
type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other" ;
type ScumMapLayer = "terrain" | "topo" | "night" ;
type CatalogPickTarget = "gift-items" | "produce-item" ;
type PlayerPanelKind = "closed" | "gifts" | "items" | "history" | "trajectory" ;
type PlayerPanelState = { kind : PlayerPanelKind ; playerId : string } ;
const scumMapBackground = new URL ( "../assets/map/scum-map-terrain-4096.webp" , import . meta . url ) . href ;
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 < readonly [ ScumMapLayer , string ] > = [ [ "terrain" , "地形" ] , [ "topo" , "等高线" ] , [ "night" , "夜间" ] ] ;
const scumCatalogKinds : ReadonlyArray < readonly [ string , string ] > = [ [ "all" , "全部" ] , [ "item" , "物品" ] , [ "vehicle" , "载具" ] , [ "zombie" , "丧尸" ] , [ "animal" , "动物" ] , [ "armed-npc" , "NPC" ] ] ;
const scumCatalogPickLimit = 48 ;
const scumMapSize = 4096 ;
const scumSurfaceRefreshMs = 15000 ;
@@ -106,6 +111,11 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
const [ produceEditorOpen , setProduceEditorOpen ] = usePluginState ( react , false ) ;
const [ giftEditorOpen , setGiftEditorOpen ] = usePluginState ( react , false ) ;
const [ mapSettingsOpen , setMapSettingsOpen ] = usePluginState ( react , false ) ;
const [ mapBaseLayer , setMapBaseLayer ] = usePluginState < ScumMapLayer | undefined > ( react , undefined ) ;
const [ mapZonesOverlay , setMapZonesOverlay ] = usePluginState < boolean | undefined > ( react , undefined ) ;
const [ catalogSearch , setCatalogSearch ] = usePluginState ( react , "" ) ;
const [ catalogKind , setCatalogKind ] = usePluginState ( react , "all" ) ;
const [ catalogOpen , setCatalogOpen ] = usePluginState ( react , false ) ;
const [ refreshSignal , setRefreshSignal ] = usePluginState ( react , 0 ) ;
const pageKey = input . pageKey ? ? "players" ;
if ( react . useEffect ) react . useEffect ( ( ) = > {
@@ -146,6 +156,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
deliveryGift , setDeliveryGift , deliveryPlayer , setDeliveryPlayer , mapSearch , setMapSearch , mapLayers , setMapLayers , selectedMapPoint , setSelectedMapPoint ,
mapCustomEnabled , setMapCustomEnabled , mapCenterX , setMapCenterX , mapCenterY , setMapCenterY , mapWidthKm , setMapWidthKm , mapHeightKm , setMapHeightKm ,
eventEditorOpen , setEventEditorOpen , produceEditorOpen , setProduceEditorOpen , giftEditorOpen , setGiftEditorOpen , mapSettingsOpen , setMapSettingsOpen ,
mapBaseLayer , setMapBaseLayer , mapZonesOverlay , setMapZonesOverlay , catalogSearch , setCatalogSearch , catalogKind , setCatalogKind , catalogOpen , setCatalogOpen ,
setAction , refresh : ( ) = > setRefreshSignal ( ( value ) = > value + 1 )
} ) : null
) ;
@@ -171,6 +182,8 @@ type ViewState = {
selectedMapPoint : string ; setSelectedMapPoint : StateSetter < string > ; setAction : StateSetter < ActionState > ; refresh : ( ) = > void ;
mapCustomEnabled : boolean | undefined ; setMapCustomEnabled : StateSetter < boolean | undefined > ; mapCenterX : string ; setMapCenterX : StateSetter < string > ; mapCenterY : string ; setMapCenterY : StateSetter < string > ; mapWidthKm : string ; setMapWidthKm : StateSetter < string > ; mapHeightKm : string ; setMapHeightKm : StateSetter < string > ;
eventEditorOpen : boolean ; setEventEditorOpen : StateSetter < boolean > ; produceEditorOpen : boolean ; setProduceEditorOpen : StateSetter < boolean > ; giftEditorOpen : boolean ; setGiftEditorOpen : StateSetter < boolean > ; mapSettingsOpen : boolean ; setMapSettingsOpen : StateSetter < boolean > ;
mapBaseLayer : ScumMapLayer | undefined ; setMapBaseLayer : StateSetter < ScumMapLayer | undefined > ; mapZonesOverlay : boolean | undefined ; setMapZonesOverlay : StateSetter < boolean | undefined > ;
catalogSearch : string ; setCatalogSearch : StateSetter < string > ; catalogKind : string ; setCatalogKind : StateSetter < string > ; catalogOpen : boolean ; setCatalogOpen : StateSetter < boolean > ;
} ;
function renderSurfaceBody ( e : ReactLike [ "createElement" ] , pageKey : string , data : SCUMSurfaceData , input : SCUMPageContext , view : ViewState ) {
@@ -291,7 +304,7 @@ function playerHistoryPanel(e: ReactLike["createElement"], player: RecordMap) {
function playerTrajectoryPanel ( e : ReactLike [ "createElement" ] , player : RecordMap , data : SCUMSurfaceData ) {
const rows = playerRecords ( data . trajectories , player ) . filter ( ( row ) = > layerOf ( row ) === "players" && hasCoordinates ( positionOf ( row ) ) ) . sort ( ( left , right ) = > trajectoryOrder ( right ) - trajectoryOrder ( left ) ) . slice ( 0 , 120 ) ;
return e ( "div" , { className : "console-record-list" } , e ( "p" , { className : "dialog-description" } , "用户轨迹来自平台 scum_user_trajectory 表;乘车状态使用轨迹记录声明的载具。" ) , e ( "div" , { className : "console-row-list" } , rows . length ? rows . map ( ( row , index ) = > { const ride = textField ( row , "riddenVehicleId" , "gameVehicleId" ) ; return e ( "div" , { key : idOf ( row , ` trajectory- ${ index } ` ) , className : "console-row" } , e ( "span" , null , dateField ( row , "sampledAt" , "observedAt" , "createdAt" ) ) , e ( "strong" , null , coords ( positionOf ( row ) ) ) , e ( "strong" , null , ride ? ` 乘坐 ${ ride } ` : textField ( row , "source" ) || "平台轨迹表" ) ) ; } ) : e ( "p" , { className : "page-status" } , "没有该用户的真实轨迹记录。" ) ) ) ;
return e ( "div" , { className : "console-record-list" } , e ( "p" , { className : "dialog-description" } , "用户轨迹来自平台 scum_user_trajectory 表;乘车状态使用轨迹记录声明的载具。" ) , e ( "div" , { className : "console-row-list" } , rows . length ? rows . map ( ( row , index ) = > { const ride = textField ( row , "riddenVehicleId" , "gameVehicleId" ) ; return e ( "div" , { key : idOf ( row , ` trajectory- ${ index } ` ) , className : "console-row" } , e ( "span" , null , dateField ( row , "sampledAt" , "observedAt" , "createdAt" ) ) , e ( "strong" , null , coords ( positionOf ( row ) ) ) , e ( "strong" , { className : "scum-trajectory-vehicle" } , trajectoryVehicleIcon ( row ) ? e ( "img" , { className : "scum-catalog-icon" , src : trajectoryVehicleIcon ( row ) , alt : "" } ) : null , ride ? ` 乘坐 ${ ride } ` : textField ( row , "source" ) || "平台轨迹表" ) ) ; } ) : e ( "p" , { className : "page-status" } , "没有该用户的真实轨迹记录。" ) ) ) ;
}
function playerRecords ( rows : RecordMap [ ] , player : RecordMap ) : RecordMap [ ] { const identities = playerIdentities ( player ) ; return rows . filter ( ( row ) = > identities . includes ( textField ( row , "steamId" , "playerId" , "gamePlayerId" , "userProfileId" , "profileId" ) ) ) ; }
@@ -394,6 +407,7 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
labeledField ( e , "物品编号" , e ( "input" , { value : view.produceTradeGoodsId , "aria-label" : "生成物品编号" , placeholder : "TradeGoods ID" , onChange : ( event : InputEvent ) = > view . setProduceTradeGoodsId ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "概率 (%)" , e ( "input" , { value : view.producePercent , "aria-label" : "生成概率" , type : "number" , placeholder : "100" , onChange : ( event : InputEvent ) = > view . setProducePercent ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "数量" , e ( "input" , { value : view.produceValue , "aria-label" : "生成数量" , type : "number" , placeholder : "1" , onChange : ( event : InputEvent ) = > view . setProduceValue ( inputValue ( event ) ) } ) ) ) ,
catalogPicker ( e , view , "produce-item" ) ,
e ( "div" , { className : "console-row-actions" } ,
labeledField ( e , "半径" , e ( "input" , { value : view.produceRadius , "aria-label" : "生成半径" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setProduceRadius ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "X" , e ( "input" , { value : view.produceX , "aria-label" : "生成 X" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setProduceX ( inputValue ( event ) ) } ) ) ,
@@ -401,6 +415,7 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
labeledField ( e , "Z" , e ( "input" , { value : view.produceZ , "aria-label" : "生成 Z" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setProduceZ ( inputValue ( event ) ) } ) ) ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : saveProduce } , "保存生成项" ) ) ,
e ( "div" , { className : "console-row-list" } , data . eventProduces . length ? data . eventProduces . map ( ( produce , index ) = > e ( "div" , { key : idOf ( produce , ` produce- ${ index } ` ) , className : "console-row" } ,
catalogIconStrip ( e , catalogIconsForValue ( textField ( produce , "tradeGoodsId" , "trade_goods_id" ) ) ) ,
e ( "span" , null , ` ${ textField ( produce , "eventId" , "event" ) } / ${ tradeGoodsLabel ( data . tradeGoods , textField ( produce , "tradeGoodsId" , "trade_goods_id" ) ) } ` ) ,
e ( "strong" , null , ` ${ numField ( produce , "percent" ) } % × ${ numField ( produce , "value" ) } ` ) ,
e ( "strong" , null , ` R ${ numField ( produce , "r" ) } · ${ coords ( produce ) } ` ) ,
@@ -474,6 +489,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
labeledField ( e , "发放次数" , e ( "input" , { value : view.giftNumber , "aria-label" : "发放次数" , type : "number" , placeholder : "1" , onChange : ( event : InputEvent ) = > view . setGiftNumber ( inputValue ( event ) ) } ) ) ,
e ( "div" , { className : "console-row-actions" } , labeledField ( e , "成就类型" , e ( "input" , { value : view.giftAchievement , "aria-label" : "成就类型" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setGiftAchievement ( inputValue ( event ) ) } ) ) , labeledField ( e , "成就值" , e ( "input" , { value : view.giftAchievementNumber , "aria-label" : "成就值" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setGiftAchievementNumber ( inputValue ( event ) ) } ) ) ) ,
labeledField ( e , "礼包物品" , e ( "input" , { value : view.giftItems , "aria-label" : "礼包物品" , placeholder : "SCUM 目录代码:数量" , onChange : ( event : InputEvent ) = > view . setGiftItems ( inputValue ( event ) ) } ) ) ,
catalogPicker ( e , view , "gift-items" ) ,
labeledField ( e , "礼包命令" , e ( "textarea" , { value : view.giftCommands , "aria-label" : "礼包命令" , placeholder : "每行一条命令" , onChange : ( event : InputEvent ) = > view . setGiftCommands ( inputValue ( event ) ) } ) ) ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : saveGift } , "保存礼包" )
) ,
@@ -483,6 +499,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( gift , "name" ) || key ) , e ( "span" , { className : "status-pill status-active" } , textField ( gift , "status" ) || "active" ) ) ,
e ( "div" , { className : "console-record-meta" } , e ( "span" , null , ` 周期 ${ giftClassLabel ( numField ( gift , "class" ) ) } ` ) , e ( "span" , null , ` 适用 ${ textField ( gift , "audience" ) || "all" } ` ) , e ( "span" , null , ` 次数 ${ numField ( gift , "number" ) } ` ) , e ( "span" , null , ` 成就 ${ numField ( gift , "achievement" ) } / ${ numField ( gift , "achievementNumber" , "achievement_number" ) } ` ) ) ,
e ( "span" , { className : "provider-id" } , giftItemsSummary ( gift ) ) ,
catalogIconStrip ( e , giftCatalogIcons ( gift ) ) ,
e ( "span" , { className : "provider-id" } , giftCommandsSummary ( gift ) ) ,
e ( "div" , { className : "console-row-actions" } , e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > view . setDeliveryGift ( key ) } , "选择发放" ) , e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > { view . setGiftEditorOpen ( true ) ; view . setGiftCode ( textField ( gift , "code" ) ) ; view . setGiftName ( textField ( gift , "name" ) ) ; view . setGiftClass ( numField ( gift , "class" ) ) ; view . setGiftAudience ( textField ( gift , "audience" ) || "all" ) ; view . setGiftNumber ( numField ( gift , "number" ) ) ; view . setGiftAchievement ( numField ( gift , "achievement" ) ) ; view . setGiftAchievementNumber ( numField ( gift , "achievementNumber" , "achievement_number" ) ) ; view . setGiftItems ( giftItemsInput ( gift ) ) ; view . setGiftCommands ( giftCommandsInput ( gift ) ) ; } } , "编辑" ) , e ( "button" , { type : "button" , className : "icon-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在删除礼包…" , async ( ) = > { await deleteGiftDefinition ( actions ? ? { } , key ) ; view . refresh ( ) ; return "礼包定义已删除。" ; } ) } , "删除" ) )
) ;
@@ -511,7 +528,6 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
function mapSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , view : ViewState ) {
const actions = input . workspaceActions ;
const points = collectMapPoints ( data ) ;
const vehicleCatalog = data . tradeGoods . filter ( ( item ) = > isVehicleCatalogItem ( item ) ) ;
const settings = data . mapSettings . find ( ( value ) = > textField ( value , "_recordKey" , "id" ) === "current" ) ? ? data . mapSettings [ 0 ] ;
const bounds = resolveMapBounds ( settings ) ;
const customEnabled = view . mapCustomEnabled ? ? Boolean ( settings && boolField ( settings , "customMapEnabled" ) ) ;
@@ -519,6 +535,8 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input:
const centerY = view . mapCenterY || textField ( settings , "centerY" , "mapY" ) || String ( ( bounds . worldMinY + bounds . worldMaxY ) / 2 ) ;
const widthKm = view . mapWidthKm || textField ( settings , "widthKm" , "mapWidth" ) || String ( ( bounds . worldMaxX - bounds . worldMinX ) / 100000 ) ;
const heightKm = view . mapHeightKm || textField ( settings , "heightKm" , "mapHeight" ) || String ( ( bounds . worldMaxY - bounds . worldMinY ) / 100000 ) ;
const baseLayer = view . mapBaseLayer ? ? mapLayerSetting ( settings ) ;
const zonesOverlay = view . mapZonesOverlay ? ? Boolean ( settings && boolField ( settings , "buildingZones" ) ) ;
const search = view . mapSearch . trim ( ) . toLowerCase ( ) ;
const visible = points . filter ( ( point ) = > view . mapLayers [ layerOf ( point ) ] && matchesText ( point , search , "name" , "label" , "subjectId" , "subjectType" , "layer" ) ) ;
const selected = visible . find ( ( point , index ) = > idOf ( point , ` point- ${ index } ` ) === view . selectedMapPoint ) ? ? visible [ 0 ] ;
@@ -526,24 +544,100 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input:
const selectedTrails = selected ? trajectoryRecordsForPoint ( data . trajectories , selected ) . sort ( ( left , right ) = > trajectoryOrder ( right ) - trajectoryOrder ( left ) ) . slice ( 0 , 8 ) : [ ] ;
const selectedLocks = selected && layerOf ( selected ) === "vehicles" ? vehicleLockRecordsForPoint ( data . vehicleLocks , selected ) . sort ( ( left , right ) = > rowTime ( right , [ "lockedAt" , "createdAt" ] ) - rowTime ( left , [ "lockedAt" , "createdAt" ] ) ) . slice ( 0 , 5 ) : [ ] ;
return e ( "div" , { className : "console-record-list" } ,
statsStrip ( e , [ [ "地图点" , points . length ] , [ "用户" , data . players . length ] , [ "载具" , data . vehicles . length ] , [ "载具目录" , vehicleCatalog . length ] ] ) ,
e ( "div" , { className : "resource-filter-bar scum-filter-bar" } ,
labeledField ( e , "筛选地图点" , e ( "input" , { value : view.mapSearch , "aria-label" : "筛选地图点" , placeholder : "名称 / 类型 / ID" , onChange : ( event : InputEvent ) = > view . setMapSearch ( inputValue ( event ) ) } ) ) ,
( [ "players" , "vehicles" , "flags" , "regions" , "other" ] as MapLayer [ ] ) . map ( ( layer ) = > e ( "label" , { key : layer , className : "scum-layer-toggle" } , e ( "input" , { type : "checkbox" , checked : view.mapLayers [ layer ] , onChange : ( event : InputEvent ) = > view . setMapLayers ( ( previous ) = > ( { . . . previous , [ layer ] : Boolean ( event . target ? . checked ) } ) ) } ) , layerLabel ( layer ) ) )
e ( "div" , { className : "scum-layer-group" , role : "group" , "aria-label" : "地图图层开关" } , ( [ "players" , "vehicles" , "flags" , "regions" , "other" ] as MapLayer [ ] ) . map ( ( layer ) = > e ( "label" , { key : layer , className : "scum-layer-toggle" } , e ( "input" , { type : "checkbox" , checked : view.mapLayers [ layer ] , onChange : ( event : InputEvent ) = > view . setMapLayers ( ( previous ) = > ( { . . . previous , [ layer ] : Boolean ( event . target ? . checked ) } ) ) } ) , layerLabel ( layer ) ) ) ) ,
e ( "div" , { className : "scum-layer-group" , role : "group" , "aria-label" : "地图底图" } ,
scumMapBaseLayers . map ( ( [ key , label ] ) = > e ( "button" , { key , type : "button" , className : baseLayer === key ? "icon-command is-active" : "icon-command" , "aria-pressed" : baseLayer === key , onClick : ( ) = > view . setMapBaseLayer ( key ) } , label ) ) ,
e ( "label" , { className : "scum-layer-toggle" } , e ( "input" , { type : "checkbox" , checked : zonesOverlay , onChange : ( event : InputEvent ) = > view . setMapZonesOverlay ( Boolean ( event . target ? . checked ) ) } ) , "建筑区" ) ) ,
e ( "span" , { className : "page-status scum-map-summary" } , ` ${ visible . length } / ${ points . length } 个可见点 ` )
) ,
e ( "details" , { className : "console-module scum-editor" , open : view.mapSettingsOpen , onToggle : ( event : InputEvent ) = > view . setMapSettingsOpen ( detailOpen ( event ) ) } , e ( "summary" , null , e ( "strong" , null , "地图范围" ) , e ( "span" , { className : "page-status" } , customEnabled ? "自定义范围" : "SCUM 默认范围" ) ) ,
e ( "details" , { className : "console-module scum-editor scum-map-settings " , open : view.mapSettingsOpen , onToggle : ( event : InputEvent ) = > view . setMapSettingsOpen ( detailOpen ( event ) ) } , e ( "summary" , null , e ( "strong" , null , "地图范围与图层 " ) , e ( "span" , { className : "page-status" } , ` ${ mapLayerLabel ( baseLayer ) } ${ zonesOverlay ? " + 建筑区" : "" } · ${ customEnabled ? "自定义范围" : "SCUM 默认范围" } ` ) ) ,
e ( "div" , { className : "console-row-actions" } ,
e ( "label" , null , e ( "input" , { type : "checkbox" , checked : customEnabled , "aria-label" : "启用自定义地图" , onChange : ( event : InputEvent ) = > view . setMapCustomEnabled ( Boolean ( event . target ? . checked ) ) } ) , "启用自定义地图" ) ,
labeledField ( e , "中心 X" , e ( "input" , { value : centerX , "aria-label" : "地图中心 X" , type : "number" , onChange : ( event : InputEvent ) = > view . setMapCenterX ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "中心 Y" , e ( "input" , { value : centerY , "aria-label" : "地图中心 Y" , type : "number" , onChange : ( event : InputEvent ) = > view . setMapCenterY ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "宽度 (公里)" , e ( "input" , { value : widthKm , "aria-label" : "地图宽度公里" , type : "number" , onChange : ( event : InputEvent ) = > view . setMapWidthKm ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "高度 (公里)" , e ( "input" , { value : heightKm , "aria-label" : "地图高度公里" , type : "number" , onChange : ( event : InputEvent ) = > view . setMapHeightKm ( inputValue ( event ) ) } ) ) ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在保存地图范围…" , async ( ) = > { await saveMapSettings ( actions ? ? { } , { customMapEnabled : customEnabled , centerX : numberInput ( centerX , 0 ) , centerY : numberInput ( centerY , 0 ) , widthKm : numberInput ( widthKm , 15.24 ) , heightKm : numberInput ( heightKm , 15.24 ) } ) ; view . refresh ( ) ; return "地图范围已保存。" ; } ) } , "保存地图范围" ) )
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在保存地图范围…" , async ( ) = > { await saveMapSettings ( actions ? ? { } , { customMapEnabled : customEnabled , centerX : numberInput ( centerX , 0 ) , centerY : numberInput ( centerY , 0 ) , widthKm : numberInput ( widthKm , 15.24 ) , heightKm : numberInput ( heightKm , 15.24 ) , baseLayer , buildingZones : zonesOverlay } ) ; view . refresh ( ) ; return "地图范围已保存。" ; } ) } , "保存地图范围" ) )
) ,
e ( "div" , { className : "overview-two-col" } ,
e ( "div" , { className : "map-surface-board" , "aria-label" : "SCUM 地图图层" , style : { backgroundImage : ` url( ${ scumMapBackground } ) ` } } , mapGridOverlay ( e ) , trails . map ( ( point , index ) = > e ( "span" , { key : ` trail- ${ index } - ${ idOf ( point , "sample" ) } ` , className : ` map-trajectory-dot map-layer- ${ layerOf ( point ) } ` , title : ` ${ pointTitle ( point ) } ${ dateField ( point , "sampledAt" , "observedAt" ) } ` , style : mapPointStyle ( point , bounds ) } ) ) , visible . map ( ( point , index ) = > { const ride = layerOf ( point ) === "players" ? textField ( point , "riddenVehicleId" , "gameVehicleId" ) : "" ; return e ( "button" , { key : idOf ( point , ` point- ${ index } ` ) , type : "button" , className : ` map-surface-dot map-layer- ${ layerOf ( point ) } ${ ride ? " map-surface-dot-riding" : "" } ` , title : ` ${ pointTitle ( point ) } ${ coords ( point ) } ${ ride ? ` · 乘坐 ${ ride } ` : "" } ` , "aria-label" : pointTitle ( point ) , style : mapPointStyle ( point , bounds ) , onClick : ( ) = > view . setSelectedMapPoint ( idOf ( point , ` point- ${ index } ` ) ) } , vehicleIconFor ( point ) ? e ( "img" , { src : vehicleIconFor ( point ) , alt : "" } ) : "" ) ; } ) ) ,
e ( "article" , { className : "console-module" } , e ( "div" , { className : "panel-header" } , e ( "h2" , null , "地图点详情" ) , e ( "span" , { className : "page-status" } , ` ${ visible . length } 个可见点 ` ) ) , selected ? e ( "div" , { className : "console-record" } , e ( "strong" , null , pointTitle ( selected ) ) , e ( "span" , { className : "status-pill status-active" } , layerLabel ( layerOf ( selected ) ) ) , e ( "span" , { className : "provider-id" } , coords ( selected ) ) , e ( "div" , { className : "console-record-meta" } , e ( "span" , null , ` ID ${ textField ( selected , "subjectId" , "id" , "_recordKey" ) || "unknown" } ` ) , e ( "span" , null , ` 来源 ${ textField ( selected , "source" ) || "平台表" } ` ) , e ( "span" , null , freshness ( selected ) ) ) , layerOf ( selected ) === "vehicles" ? e ( "div" , { className : "console-record-meta" } , e ( "span" , null , ` 类型 ${ textField ( selected , "className" , "vehicleClass" , "vehicleType" ) || "unknown" } ` ) , e ( "span" , null , ` 状态 ${ textField ( selected , "status" , "state" , "isFunctional" ) || "unknown" } ` ) , e ( "span" , null , ` 锁 ${ boolField ( selected , "locked" ) ? "已上锁" : "未上锁/未知" } ` ) , e ( "span" , null , ` 访问 ${ dateField ( selected , "lastAccessTime" , "vehicleObservedAt" , "sampledAt" ) } ` ) ) : null , selectedLocks . length ? e ( "div" , { className : "console-row-list" } , selectedLocks . map ( ( row , index ) = > e ( "div" , { key : ` selected-lock- ${ index } ` , className : "console-row" } , e ( "span" , null , dateField ( row , "lockedAt" , "createdAt" ) ) , e ( "strong" , null , textField ( row , "scumUserId" ) || "unknown" ) , e ( "strong" , null , textField ( row , "steamId" ) || "unknown" ) ) ) ) : null , selectedTrails . length ? e ( "div" , { className : "console-row-list" } , selectedTrails . map ( ( row , index ) = > e ( "div" , { key : ` selected-trail- ${ index } ` , className : "console-row" } , e ( "span" , null , dateField ( row , "sampledAt" , "observedAt" ) ) , e ( "strong" , null , coords ( row ) ) , e ( "strong" , null , textField ( row , "source" ) || "平台轨迹表" ) ) ) ) : null ) : e ( "p" , { className : "page-status" } , "当前图层和筛选条件下没有真实地图点。" ) )
)
e ( "div" , { className : "map-surface-board" , "aria-label" : "SCUM 地图图层" , style : mapBoardStyle ( baseLayer , zonesOverlay ) } , mapGridOverlay ( e ) , trails . map ( ( point , index ) = > e ( "span" , { key : ` trail- ${ index } - ${ idOf ( point , "sample" ) } ` , className : ` map-trajectory-dot map-layer- ${ layerOf ( point ) } ` , title : ` ${ pointTitle ( point ) } ${ dateField ( point , "sampledAt" , "observedAt" ) } ` , style : mapPointStyle ( point , bounds ) } ) ) , visible . map ( ( point , index ) = > { const ride = layerOf ( point ) === "players" ? textField ( point , "riddenVehicleId" , "gameVehicleId" ) : "" ; return e ( "button" , { key : idOf ( point , ` point- ${ index } ` ) , type : "button" , className : ` map-surface-dot map-layer- ${ layerOf ( point ) } ${ ride ? " map-surface-dot-riding" : "" } ` , title : ` ${ pointTitle ( point ) } ${ coords ( point ) } ${ ride ? ` · 乘坐 ${ ride } ` : "" } ` , "aria-label" : pointTitle ( point ) , style : mapPointStyle ( point , bounds ) , onClick : ( ) = > view . setSelectedMapPoint ( idOf ( point , ` point- ${ index } ` ) ) } , vehicleIconFor ( point ) ? e ( "img" , { src : vehicleIconFor ( point ) , alt : "" } ) : "" ) ; } ) ) ,
selected ? mapPointStrip ( e , selected , selectedTrails , selectedLocks ) : null
) ;
}
function mapPointStrip ( e : ReactLike [ "createElement" ] , point : RecordMap , trails : RecordMap [ ] , locks : RecordMap [ ] ) {
const icon = layerOf ( point ) === "vehicles" ? vehicleIconFor ( point ) : "" ;
return e ( "article" , { className : "console-module map-point-strip" } ,
e ( "div" , { className : "map-point-strip-head" } ,
e ( "h2" , null , "地图点详情" ) ,
icon ? e ( "img" , { className : "scum-catalog-icon" , src : icon , alt : "" } ) : null ,
e ( "strong" , null , pointTitle ( point ) ) ,
e ( "span" , { className : "status-pill status-active" } , layerLabel ( layerOf ( point ) ) ) ,
e ( "span" , { className : "provider-id" } , coords ( point ) ) ,
e ( "span" , { className : "provider-id" } , ` ID ${ textField ( point , "subjectId" , "id" , "_recordKey" ) || "unknown" } ` ) ,
e ( "span" , { className : "provider-id" } , ` 来源 ${ textField ( point , "source" ) || "平台表" } ` ) ,
e ( "span" , { className : "provider-id" } , freshness ( point ) ) ,
layerOf ( point ) === "vehicles" ? e ( "span" , { className : "provider-id" } , ` ${ textField ( point , "className" , "vehicleClass" , "vehicleType" ) || "unknown" } · ${ textField ( point , "status" , "state" , "isFunctional" ) || "unknown" } · ${ boolField ( point , "locked" ) ? "已上锁" : "未上锁/未知" } · 访问 ${ dateField ( point , "lastAccessTime" , "vehicleObservedAt" , "sampledAt" ) } ` ) : null ) ,
locks . length ? e ( "div" , { className : "console-row-list" } , locks . map ( ( row , index ) = > e ( "div" , { key : ` selected-lock- ${ index } ` , className : "console-row" } , e ( "span" , null , dateField ( row , "lockedAt" , "createdAt" ) ) , e ( "strong" , null , textField ( row , "scumUserId" ) || "unknown" ) , e ( "strong" , null , textField ( row , "steamId" ) || "unknown" ) ) ) ) : null ,
trails . length ? e ( "div" , { className : "console-row-list" } , trails . map ( ( row , index ) = > e ( "div" , { key : ` selected-trail- ${ index } ` , className : "console-row" } , e ( "span" , null , dateField ( row , "sampledAt" , "observedAt" ) ) , e ( "strong" , null , coords ( row ) ) , e ( "strong" , null , textField ( row , "source" ) || "平台轨迹表" ) ) ) ) : null
) ;
}
function mapBoardStyle ( base : ScumMapLayer , zones : boolean ) : Record < string , string > {
const layers = [ zones ? ` url( ${ scumMapLayerImages . zones } ) ` : "" , ` url( ${ scumMapLayerImages [ base ] } ) ` ] . filter ( Boolean ) ;
return { backgroundImage : layers.join ( ", " ) } ;
}
function mapLayerSetting ( settings : RecordMap | undefined ) : ScumMapLayer {
const value = textField ( settings , "baseLayer" , "base_layer" ) . toLowerCase ( ) ;
return value === "topo" || value === "night" ? value : "terrain" ;
}
function mapLayerLabel ( layer : ScumMapLayer ) : string { return layer === "topo" ? "等高线" : layer === "night" ? "夜间" : "地形" ; }
function catalogMatches ( query : string , kind : string ) : ScumCatalogEntry [ ] {
const search = query . trim ( ) . toLowerCase ( ) ;
return scumCatalogEntries . filter ( ( entry ) = > ( kind === "all" || entry . kind === kind ) && ( ! search || entry . name . toLowerCase ( ) . includes ( search ) || entry . code . toLowerCase ( ) . includes ( search ) || entry . command . toLowerCase ( ) . includes ( search ) ) ) . slice ( 0 , scumCatalogPickLimit ) ;
}
function appendCatalogItem ( current : string , code : string ) : string {
const parts = current . split ( "," ) . map ( ( part ) = > part . trim ( ) ) . filter ( Boolean ) ;
const index = parts . findIndex ( ( part ) = > part . split ( ":" ) [ 0 ] ? . trim ( ) . toLowerCase ( ) === code . toLowerCase ( ) ) ;
if ( index >= 0 ) { const [ key , quantity ] = parts [ index ] . split ( ":" ) ; parts [ index ] = ` ${ key . trim ( ) } : ${ Math . max ( 1 , Number ( quantity ) || 1 ) + 1 } ` ; }
else parts . push ( ` ${ code } :1 ` ) ;
return parts . join ( ", " ) ;
}
function catalogIconsForValue ( value : string ) : string [ ] {
const entry = scumCatalogEntryFor ( value ) ;
const url = entry ? scumCatalogIconUrl ( entry . icon ) : "" ;
return url ? [ url ] : [ ] ;
}
function catalogIconStrip ( e : ReactLike [ "createElement" ] , icons : string [ ] ) {
return icons . length ? e ( "span" , { className : "scum-catalog-icons" } , icons . map ( ( url , index ) = > e ( "img" , { key : ` ${ url } - ${ index } ` , className : "scum-catalog-icon" , src : url , alt : "" } ) ) ) : null ;
}
function giftCatalogIcons ( gift : RecordMap ) : string [ ] {
const items = field ( gift , "items" ) ;
return Array . isArray ( items ) ? items . flatMap ( ( item ) = > isRecord ( item ) ? catalogIconsForValue ( textField ( item , "catalogCode" , "catalogItemKey" , "key" , "className" ) ) : [ ] ) . slice ( 0 , 12 ) : [ ] ;
}
function trajectoryVehicleIcon ( row : RecordMap ) : string {
const code = textField ( row , "className" , "vehicleClass" , "entityClass" , "vehicleType" , "riddenVehicleId" , "gameVehicleId" , "vehicleId" ) ;
return code ? scumVehicleIconUrl ( code ) : "" ;
}
function catalogPicker ( e : ReactLike [ "createElement" ] , view : ViewState , target : CatalogPickTarget ) {
const matches = catalogMatches ( view . catalogSearch , view . catalogKind ) ;
return e ( "details" , { className : "console-module scum-editor scum-catalog-picker" , open : view.catalogOpen , onToggle : ( event : InputEvent ) = > view . setCatalogOpen ( detailOpen ( event ) ) } ,
e ( "summary" , null , e ( "strong" , null , target === "gift-items" ? "从 SCUM 目录选择礼包物品" : "从 SCUM 目录选择物品" ) , e ( "span" , { className : "page-status" } , "图标 / 名称 / 代码" ) ) ,
view . catalogOpen ? e ( "div" , { className : "scum-catalog-picker-body" } ,
e ( "input" , { type : "search" , value : view.catalogSearch , "aria-label" : target === "gift-items" ? "搜索礼包物品" : "搜索生成物品" , placeholder : "名称 / 代码,例如 Laika" , onChange : ( event : InputEvent ) = > view . setCatalogSearch ( inputValue ( event ) ) } ) ,
e ( "div" , { className : "scum-catalog-kinds" , role : "group" , "aria-label" : "目录类型" } , scumCatalogKinds . map ( ( [ key , label ] ) = > e ( "button" , { key , type : "button" , className : view.catalogKind === key ? "icon-command is-active" : "icon-command" , "aria-pressed" : view . catalogKind === key , onClick : ( ) = > view . setCatalogKind ( key ) } , label ) ) ) ,
e ( "div" , { className : "scum-catalog-grid" } , matches . length ? matches . map ( ( entry ) = > { const icon = scumCatalogIconUrl ( entry . icon ) ; return e ( "button" , { key : entry.code , type : "button" , className : "scum-catalog-chip" , title : ` ${ entry . name } · ${ entry . command } ` , onClick : ( ) = > target === "gift-items" ? view . setGiftItems ( appendCatalogItem ( view . giftItems , entry . code ) ) : view . setProduceTradeGoodsId ( entry . code ) } , icon ? e ( "img" , { className : "scum-catalog-icon" , src : icon , alt : "" } ) : e ( "span" , { className : "scum-catalog-icon scum-catalog-icon-empty" } ) , e ( "span" , { className : "scum-catalog-meta" } , e ( "strong" , null , entry . name ) , e ( "span" , { className : "provider-id" } , entry . code ) ) ) ; } ) : e ( "p" , { className : "page-status" } , "没有匹配的目录条目。" ) )
) : null
) ;
}