@@ -1,7 +1,6 @@
import {
deleteGiftDefinition ,
deleteEventDefinition ,
deleteEventProduce ,
emptySCUMSurfaceData ,
loadSCUMSurface ,
parseGiftItems ,
@@ -10,7 +9,6 @@ import {
resetPendingGift ,
resolveMapBounds ,
saveEventDefinition ,
saveEventProduce ,
saveGiftDefinition ,
saveMapSettings ,
startEvent ,
@@ -30,9 +28,10 @@ type GiftDialogState = { mode: "closed" } | { mode: "view" | "edit"; gift?: Reco
type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other" ;
type MapVehicleFilter = "all" | "land" | "water" | "air" | "other" ;
type ScumMapLayer = "terrain" | "topo" | "night" ;
type CatalogPickTarget = "gift-items" | "produce -item" ;
type CatalogPickTarget = "gift-items" | "activity -items " ;
type GiftItemDraft = { catalogCode : string ; quantity : string ; probability : string ; durability : string } ;
type GiftItemDraftField = keyof GiftItemDraft ;
type ActivityMapPointerEvent = { pointerId? : number ; clientX? : number ; clientY? : number ; currentTarget ? : { getBoundingClientRect ? : ( ) = > { left : number ; top : number ; width : number ; height : number } ; setPointerCapture ? : ( pointerId? : number ) = > void ; releasePointerCapture ? : ( pointerId? : number ) = > void } ; preventDefault ? : ( ) = > void ; stopPropagation ? : ( ) = > void } ;
type PlayerPanelKind = "closed" | "gifts" | "items" | "history" ;
type PlayerPanelState = { kind : PlayerPanelKind ; playerId : string } ;
type PointerEventLike = { pointerId? : number ; clientX? : number ; clientY? : number ; button? : number ; currentTarget ? : { setPointerCapture ? : ( pointerId? : number ) = > void ; releasePointerCapture ? : ( pointerId? : number ) = > void } ; preventDefault ? : ( ) = > void ; stopPropagation ? : ( ) = > void } ;
@@ -40,7 +39,6 @@ const scumMapLayerImages = { terrain: new URL("../assets/map/scum-map-terrain-40
const scumMapBaseLayers : ReadonlyArray < readonly [ ScumMapLayer , string ] > = [ [ "terrain" , "地形" ] , [ "topo" , "等高线" ] , [ "night" , "夜间" ] ] ;
const scumMapVehicleFilters : ReadonlyArray < readonly [ MapVehicleFilter , string ] > = [ [ "all" , "全部载具" ] , [ "land" , "陆地载具" ] , [ "water" , "水上载具" ] , [ "air" , "飞行类载具" ] , [ "other" , "其他载具" ] ] ;
const scumFlightVehicleKeys = new Set ( [ "kinglet_duster" , "kinglet_mariner" , "kinglet_scout" ] ) ;
const scumCatalogKinds : ReadonlyArray < readonly [ string , string ] > = [ [ "all" , "全部" ] , [ "item" , "物品" ] , [ "vehicle" , "载具" ] , [ "zombie" , "丧尸" ] , [ "animal" , "动物" ] , [ "armed-npc" , "NPC" ] ] ;
const scumGiftCatalogCategories : ReadonlyArray < readonly [ string , string ] > = [ [ "all" , "全部目录" ] , [ "hunting" , "狩猎" ] , [ "food" , "食物" ] , [ "metabolism" , "代谢" ] , [ "medical" , "医疗" ] , [ "weapons" , "武器" ] , [ "crafting" , "制作" ] , [ "gear" , "装备" ] , [ "fishing" , "钓鱼" ] , [ "building" , "建筑" ] , [ "tools" , "工具" ] , [ "farming" , "农业" ] , [ "misc" , "其他" ] , [ "vehicle" , "载具" ] , [ "animal" , "动物" ] , [ "zombie" , "丧尸" ] , [ "armed-npc" , "NPC" ] ] ;
const scumCatalogPickLimit = 48 ;
const scumMapSize = 4096 ;
@@ -58,6 +56,7 @@ const scumMapTrackColors = ["#4ECDC4", "#FF8A6B", "#F7DC6F", "#85C1E9", "#BB8FCE
const scumMapTrackColorCache = new Map < string , string > ( ) ;
const scumMapDefaultWindowMs = 60 * 60 * 1000 ;
const mapDragState = { active : false , pointerId : - 1 , startX : 0 , startY : 0 , originX : 0 , originY : 0 , moved : false } ;
const activityMapDragState = { active : false , pointerId : - 1 , startX : 0 , startY : 0 } ;
export type ReactLike = {
createElement : ( . . . args : any [ ] ) = > any ;
@@ -96,28 +95,16 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
const [ squadSearch , setSquadSearch ] = usePluginState ( react , "" ) ;
const [ selectedSquadId , setSelectedSquadId ] = usePluginState ( react , "" ) ;
const [ activityStatus , setActivityStatus ] = usePluginState ( react , "all" ) ;
const [ activityDetailId , setActivityDetailId ] = usePluginState ( react , "" ) ;
const [ eventId , setEventId ] = usePluginState ( react , "" ) ;
const [ eventName , setEventName ] = usePluginState ( react , "" ) ;
const [ eventType , setEventType ] = usePluginState ( react , "range" ) ;
const [ eventSchedule , setEventSchedule ] = usePluginState ( react , "" ) ;
const [ eventCommand , setEventCommand ] = usePluginState ( react , "" ) ;
const [ eventClass , setEventClass ] = usePluginState ( react , "1" ) ;
const [ eventPlacard , setEventPlacard ] = usePluginState ( react , "" ) ;
const [ eventPercent , setEventPercent ] = usePluginState ( react , "1 00" ) ;
const [ eventDuration , setEventDuration ] = usePluginState ( react , "1800 " ) ;
const [ eventNpc , setEventNpc ] = usePluginState ( react , "0 " ) ;
const [ eventItem , setEventItem ] = usePluginState ( react , "0" ) ;
const [ eventZombie , setEventZombie ] = usePluginState ( react , "0" ) ;
const [ eventAnimal , setEventAnimal ] = usePluginState ( react , "0" ) ;
const [ produceEventId , setProduceEventId ] = usePluginState ( react , "" ) ;
const [ produceId , setProduceId ] = usePluginState ( react , "" ) ;
const [ produceTradeGoodsId , setProduceTradeGoodsId ] = usePluginState ( react , "" ) ;
const [ producePercent , setProducePercent ] = usePluginState ( react , "100" ) ;
const [ produceValue , setProduceValue ] = usePluginState ( react , "1" ) ;
const [ produceRadius , setProduceRadius ] = usePluginState ( react , "0" ) ;
const [ produceX , setProduceX ] = usePluginState ( react , "0" ) ;
const [ produceY , setProduceY ] = usePluginState ( react , "0" ) ;
const [ produceZ , setProduceZ ] = usePluginState ( react , "0" ) ;
const [ eventItems , setEventItems ] = usePluginState < GiftItemDraft [ ] > ( react , [ ] ) ;
const [ eventRegionName , setEventRegionName ] = usePluginState ( react , "" ) ;
const [ eventRegionRadius , setEventRegionRadius ] = usePluginState ( react , "50 00" ) ;
const [ eventRegionX , setEventRegionX ] = usePluginState ( react , "" ) ;
const [ eventRegionY , setEventRegionY ] = usePluginState ( react , "" ) ;
const [ eventRegionZ , setEventRegionZ ] = usePluginState ( react , "0" ) ;
const [ giftTab , setGiftTab ] = usePluginState < GiftTab > ( react , "definitions" ) ;
const [ giftSearch , setGiftSearch ] = usePluginState ( react , "" ) ;
const [ giftStatus , setGiftStatus ] = usePluginState ( react , "all" ) ;
@@ -146,8 +133,6 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
const mapViewportRef = react . useRef ? react . useRef < HTMLElement | null > ( null ) : { current : null as HTMLElement | null } ;
const mapPlaybackFrameRef = react . useRef ? react . useRef < MapPlaybackFrame | null > ( null ) : { current : null as MapPlaybackFrame | null } ;
const mapFollowRef = react . useRef ? react . useRef ( true ) : { current : true } ;
const [ eventEditorOpen , setEventEditorOpen ] = usePluginState ( react , false ) ;
const [ produceEditorOpen , setProduceEditorOpen ] = usePluginState ( react , false ) ;
const [ giftDialog , setGiftDialog ] = usePluginState < GiftDialogState > ( react , { mode : "closed" } ) ;
const [ mapBaseLayer , setMapBaseLayer ] = usePluginState < ScumMapLayer | undefined > ( react , undefined ) ;
const [ mapZonesOverlay , setMapZonesOverlay ] = usePluginState < boolean | undefined > ( react , undefined ) ;
@@ -164,8 +149,8 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
if ( inFlight || disposed ) return ;
inFlight = true ;
const currentGeneration = ++ generation ;
if ( ! input . serverInstanceId || ! input . workspaceActions ? . scum || ! input . workspaceActions ? . pluginData ) {
setState ( { status : "error" , reason : "插件页面没有绑定服务器、平台 SCUM 数据能力 或通用 pluginData 能力。" } ) ;
if ( ! input . serverInstanceId || ! input . workspaceActions ? . pluginData ) {
setState ( { status : "error" , reason : "插件页面没有绑定服务器或通用 pluginData 能力。" } ) ;
inFlight = false ;
return ;
}
@@ -258,15 +243,14 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
const mapData = react . useMemo ? react . useMemo ( deriveMap , [ pageKey , data , mapTimeFrom , mapTimeTo , mapUsers , mapVehicleFilter ] ) : deriveMap ( ) ;
const view : ViewState = {
playerSearch , setPlayerSearch , playerStatus , setPlayerStatus , playerPanel , setPlayerPanel , squadSearch , setSquadSearch , selectedSquadId , setSelectedSquadId ,
activityStatus , setActivityStatus , eventId , setEventId , eventName , setEventName , eventType , setEventType , eventSchedule , setEventSchedule ,
eventCommand , setEventCommand , eventClass , setEventClass , eventPlacard , setEventPlacard , eventPercent , setEventPercent , eventDuration , setEventDuration , eventNpc , setEventNpc , eventItem , setEventItem , eventZombie , setEventZombie , eventAnimal , setEventAnimal ,
produceEventId , setProduceEventId , produceId , setProduceId , produceTradeGoodsId , setProduceTradeGoodsId , producePercent , setProducePercent , produceValue , setProduceValue , produceRadius , setProduceRadius , produceX , setProduceX , produceY , setProduceY , produceZ , setProduceZ ,
activityStatus , setActivityStatus , activityDetailId , setActivityDetailId , eventId , setEventId , eventName , setEventName ,
eventCommand , setEventCommand , eventItems , setEventItems , eventRegionName , setEventRegionName , eventRegionRadius , setEventRegionRadius , eventRegionX , setEventRegionX , eventRegionY , setEventRegionY , eventRegionZ , setEventRegionZ ,
giftTab , setGiftTab , giftSearch , setGiftSearch , giftStatus , setGiftStatus , giftRecordKey , setGiftRecordKey , giftCode , setGiftCode , giftName , setGiftName , giftItems , setGiftItems , giftClass , setGiftClass , giftAudience , setGiftAudience , giftNumber , setGiftNumber , giftAchievement , setGiftAchievement , giftAchievementNumber , setGiftAchievementNumber ,
deliveryGift , setDeliveryGift , deliveryPlayer , setDeliveryPlayer , mapLayers , setMapLayers , mapVehicleFilter , setMapVehicleFilter , selectedMapPoint , setSelectedMapPoint ,
hoverMapPoint , setHoverMapPoint ,
mapView , setMapView , mapViewportRef , mapPlaybackFrameRef , mapFollowRef , mapTimePreset , setMapTimePreset , mapTimeFrom , setMapTimeFrom , mapTimeTo , setMapTimeTo ,
mapUsers , setMapUsers , mapTrackKey , setMapTrackKey , mapPlayback , setMapPlayback ,
eventEditorOpen , setEventEditorOpen , produceEditorOpen , setProduceEditorOpen , giftDialog , setGiftDialog ,
giftDialog , setGiftDialog ,
mapBaseLayer , setMapBaseLayer , mapZonesOverlay , setMapZonesOverlay , catalogSearch , setCatalogSearch , catalogKind , setCatalogKind , catalogOpen , setCatalogOpen ,
setAction , refresh : ( ) = > setRefreshSignal ( ( value ) = > value + 1 ) ,
mapData , mapSceneContents : null , mapStaticContents : null , mapSelectedTrails : [ ] , mapSelectedLocks : [ ]
@@ -302,15 +286,10 @@ type ViewState = {
playerSearch : string ; setPlayerSearch : StateSetter < string > ; playerStatus : string ; setPlayerStatus : StateSetter < string > ;
playerPanel : PlayerPanelState ; setPlayerPanel : StateSetter < PlayerPanelState > ;
squadSearch : string ; setSquadSearch : StateSetter < string > ; selectedSquadId : string ; setSelectedSquadId : StateSetter < string > ;
activityStatus : string ; setActivityStatus : StateSetter < string > ; giftTab : GiftTab ; setGiftTab : StateSetter < GiftTab > ; giftSearch : string ; setGiftSearch : StateSetter < string > ; giftStatus : string ; setGiftStatus : StateSetter < string > ; giftRecordKey : string ; setGiftRecordKey : StateSetter < string > ;
activityStatus : string ; setActivityStatus : StateSetter < string > ; activityDetailId : string ; setActivityDetailId : StateSetter < string > ; giftTab : GiftTab ; setGiftTab : StateSetter < GiftTab > ; giftSearch : string ; setGiftSearch : StateSetter < string > ; giftStatus : string ; setGiftStatus : StateSetter < string > ; giftRecordKey : string ; setGiftRecordKey : StateSetter < string > ;
eventId : string ; setEventId : StateSetter < string > ; eventName : string ; setEventName : StateSetter < string > ;
eventType : string ; setEventType : StateSetter < string > ; eventSchedule : string ; setEventSchedule : StateSetter < string > ;
eventCommand : string ; setEventCommand : StateSetter < string > ;
eventClass : string ; setEventClass : StateSetter < string > ; eventPlacard : string ; setEventPlacard : StateSetter < string > ; eventPercent : string ; setEventPercent : StateSetter < string > ;
eventDuration : string ; setEventDuration : StateSetter < string > ;
eventNpc : string ; setEventNpc : StateSetter < string > ; eventItem : string ; setEventItem : StateSetter < string > ; eventZombie : string ; setEventZombie : StateSetter < string > ; eventAnimal : string ; setEventAnimal : StateSetter < string > ;
produceEventId : string ; setProduceEventId : StateSetter < string > ; produceId : string ; setProduceId : StateSetter < string > ; produceTradeGoodsId : string ; setProduceTradeGoodsId : StateSetter < string > ;
producePercent : string ; setProducePercent : StateSetter < string > ; produceValue : string ; setProduceValue : StateSetter < string > ; produceRadius : string ; setProduceRadius : StateSetter < string > ; produceX : string ; setProduceX : StateSetter < string > ; produceY : string ; setProduceY : StateSetter < string > ; produceZ : string ; setProduceZ : StateSetter < string > ;
eventItems : GiftItemDraft [ ] ; setEventItems : StateSetter < GiftItemDraft [ ] > ; eventRegionName : string ; setEventRegionName : StateSetter < string > ; eventRegionRadius : string ; setEventRegionRadius : StateSetter < string > ; eventRegionX : string ; setEventRegionX : StateSetter < string > ; eventRegionY : string ; setEventRegionY : StateSetter < string > ; eventRegionZ : string ; setEventRegionZ : StateSetter < string > ;
giftCode : string ; setGiftCode : StateSetter < string > ; giftName : string ; setGiftName : StateSetter < string > ; giftItems : GiftItemDraft [ ] ; setGiftItems : StateSetter < GiftItemDraft [ ] > ;
giftClass : string ; setGiftClass : StateSetter < string > ; giftAudience : string ; setGiftAudience : StateSetter < string > ; giftNumber : string ; setGiftNumber : StateSetter < string > ; giftAchievement : string ; setGiftAchievement : StateSetter < string > ; giftAchievementNumber : string ; setGiftAchievementNumber : StateSetter < string > ;
deliveryGift : string ; setDeliveryGift : StateSetter < string > ; deliveryPlayer : string ; setDeliveryPlayer : StateSetter < string > ;
@@ -321,7 +300,7 @@ type ViewState = {
mapFollowRef : { current : boolean } ;
mapTimePreset : string ; setMapTimePreset : StateSetter < string > ; mapTimeFrom : string ; setMapTimeFrom : StateSetter < string > ; mapTimeTo : string ; setMapTimeTo : StateSetter < string > ;
mapUsers : string [ ] | undefined ; setMapUsers : StateSetter < string [ ] | undefined > ; mapTrackKey : string ; setMapTrackKey : StateSetter < string > ; mapPlayback : MapPlaybackState ; setMapPlayback : StateSetter < MapPlaybackState > ;
eventEditorOpen : boolean ; setEventEditorOpen : StateSetter < boolean > ; produceEditorOpen : boolean ; setProduceEditorOpen : StateSetter < boolean > ; giftDialog : GiftDialogState ; setGiftDialog : StateSetter < GiftDialogState > ;
giftDialog : GiftDialogState ; setGiftDialog : StateSetter < GiftDialogState > ;
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 > ;
} ;
@@ -502,105 +481,165 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
return view . activityStatus === "all" || status === view . activityStatus ;
} ) ;
const statuses = unique ( data . events . map ( ( event ) = > textField ( runsByEvent . get ( textField ( event , "id" , "eventId" ) ) , "status" , "state" ) || textField ( event , "status" , "state" ) ) . filter ( Boolean ) ) ;
const tradeEvents = recentRows ( data . tradeEvents , "observedAt" , "occurredAt" , "createdAt" ) ;
const openNewActivity = ( ) = > {
view . setActivityDetailId ( "__new__" ) ; view . setEventId ( "" ) ; view . setEventName ( "" ) ; view . setEventCommand ( "" ) ; view . setEventItems ( [ ] ) ;
view . setEventRegionName ( "" ) ; view . setEventRegionRadius ( "5000" ) ; view . setEventRegionX ( "" ) ; view . setEventRegionY ( "" ) ; view . setEventRegionZ ( "0" ) ;
} ;
const openActivity = ( event : RecordMap ) = > {
const eventId = textField ( event , "id" , "eventId" ) ;
const produce = data . eventProduces . find ( ( row ) = > textField ( row , "eventId" , "event" ) === eventId ) ;
const region = activityRegionDraft ( event , produce ) ;
view . setActivityDetailId ( eventId ) ; view . setEventId ( eventId ) ; view . setEventName ( textField ( event , "title" , "name" ) ) ; view . setEventCommand ( textField ( event , "rconCommand" , "command" ) ) ;
view . setEventItems ( activityItemDrafts ( event , data . eventProduces ) ) ; view . setEventRegionName ( region . name ) ; view . setEventRegionRadius ( region . radius ) ; view . setEventRegionX ( region . x ) ; view . setEventRegionY ( region . y ) ; view . setEventRegionZ ( region . z ) ;
} ;
const saveEvent = ( ) = > runAction ( view . setAction , "正在保存活动定义…" , async ( ) = > {
const id = view . eventId . trim ( ) ;
const name = view . eventName . trim ( ) ;
if ( ! id || ! name ) throw new Error ( "活动编号和名称 不能为空。" ) ;
await saveEventDefinition ( actions ? ? { } , {
id , name , eventType : view.eventClass === "2" ? "fixed" : "range" , class : integerInput ( view . eventClass , 1 ) , schedule : view.eventSchedule.trim ( ) , corn : view.eventSchedule.trim ( ) ,
rconCommand : view.eventCommand.trim ( ) , command : view.eventCommand.trim ( ) ,
placard : view.eventPlacard.trim ( ) , announcement : view.eventPlacard.trim ( ) , percent : integerInput ( view . eventPercent , 100 ) , probability : integerInput ( view . eventPercent , 100 ) ,
npc : minimumIntegerInput ( view . eventNpc , 0 , "NPC 数量" ) , item : minimumIntegerInput ( view . eventItem , 0 , "物品数量" ) , zombie : minimumIntegerInput ( view . eventZombie , 0 , "僵尸数量" ) , animal : minimumIntegerInput ( view . eventAnimal , 0 , "动物数量" ) ,
status : "enabled" , announce : Boolean ( view . eventPlacard . trim ( ) ) , durationSeconds : minimumIntegerInput ( view . eventDuration , 1 , "活动持续秒数" ) , updatedAt : new Date ( ) . toISOString ( )
} ) ;
const title = view . eventName . trim ( ) ;
if ( ! id || ! title ) throw new Error ( "活动编号和活动标题 不能为空。" ) ;
if ( ! /^[A-Za-z0-9_.:-]{1,96}$/ . test ( id ) ) throw new Error ( "活动编号只能使用字母、数字、点、下划线、短横线或冒号。" ) ;
const items = parseGiftItems ( view . eventItems ) ;
if ( ! items . length ) throw new Error ( "活动至少要包含一项物品或 NPC。" ) ;
const region = activityRegionValue ( view ) ;
const previous = data . events . find ( ( event ) = > textField ( event , "id" , "eventId" ) === id ) ;
const legacy = previous ? {
eventType : textField ( previous , "eventType" ) || "range" , class : integerInput ( textField ( previous , "class" ) , 1 ) , schedule : textField ( previous , "schedule" , "corn" ) , corn : textField ( previous , "corn" , "schedule" ) ,
rconCommand : view.eventCommand.trim ( ) || textField ( previous , "rconCommand" , "command" ) , command : view.eventCommand.trim ( ) || textField ( previous , "command" , "rconCommand" ) ,
placard : textField ( previous , "placard" , "announcement" ) , announcement : textField ( previous , "announcement" , "placard" ) , percent : integerInput ( textField ( previous , "percent" , "probability" ) , 100 ) , probability : integerInput ( textField ( previous , "probability" , "percent" ) , 100 ) ,
npc : activityCount ( items , "armed-npc" , textField ( previous , "npc" ) ) , item : activityCount ( items , "item" , textField ( previous , "item" ) ) , zombie : activityCount ( items , "zombie" , textField ( previous , "zombie" ) ) , animal : activityCount ( items , "animal" , textField ( previous , "animal" ) ) ,
status : textField ( previous , "status" ) || "enabled" , announce : boolField ( previous , "announce" ) || Boolean ( textField ( previous , "placard" , "announcement" ) ) , durationSeconds : integerInput ( textField ( previous , "durationSeconds" ) , 1800 )
} : { eventType : "range" , class : 1 , rconCommand : view.eventCommand.trim ( ) , command : view.eventCommand.trim ( ) , percent : 100 , probability : 100 , npc : activityCount ( items , "armed-npc" ) , item : activityCount ( items , "item" ) , zombie : activityCount ( items , "zombie" ) , animal : activityCount ( items , "animal" ) , status : "enabled" , announce : false , durationSeconds : 1800 } ;
await saveEventDefinition ( actions ? ? { } , { . . . legacy , id , name : title , title , region , area : region , items , produces : activityProduceRecords ( id , items , region ) , updatedAt : new Date ( ) . toISOString ( ) } ) ;
view . setActivityDetailId ( id ) ;
view . refresh ( ) ;
return ` 活动 ${ name } 已保存。 ` ;
return ` 活动 ${ title } 已保存。 ` ;
} ) ;
const saveProduce = ( ) = > runAction ( view . setAction , "正在保存活动生成项…" , async ( ) = > {
const eventId = view . produceEventId . trim ( ) || view . eventId . trim ( ) || textField ( data . events [ 0 ] , "id" , "eventId" ) ;
const tradeGoodsId = view . produceTradeGoodsId . trim ( ) ;
if ( ! eventId || ! tradeGoodsId ) throw new Error ( "活动编号和物品编号不能为空。" ) ;
await saveEventProduce ( actions ? ? { } , {
id : view.produceId.trim ( ) , eventId , tradeGoodsId , percent : integerInput ( view . producePercent , 100 ) , value : integerInput ( view . produceValue , 1 ) ,
r : numberInput ( view . produceRadius , 0 ) , x : numberInput ( view . produceX , 0 ) , y : numberInput ( view . produceY , 0 ) , z : numberInput ( view . produceZ , 0 ) , updatedAt : new Date ( ) . toISOString ( )
} ) ;
view . refresh ( ) ;
return "活动生成项已保存。" ;
} ) ;
const activityHistory = data . activityEvents . filter ( ( event ) = > Boolean ( textField ( event , "occurredAt" , "createdAt" ) ) && textField ( event , "taskKind" ) . toLowerCase ( ) !== "active-task" ) ;
return e ( "div" , { className : "console-record-list" } ,
statsStrip ( e , [ [ "活动定义" , data . events . length ] , [ "运行记录" , data . eventRuns . length ] , [ "物品列表" , data . tradeGoods . length ] , [ "交易记录" , data . tradeEvents . length ] ] ) ,
e ( "div" , { className : "overview-two-col" } ,
e ( "details" , { className : "console-module scum-editor" , open : view.eventEditorOpen , onToggle : ( event : InputEvent ) = > view . setEventEditorOpen ( detailOpen ( event ) ) } , e ( "summary" , null , e ( "strong" , null , "新建或更新活动" ) , e ( "span" , { className : "page-status" } , view . eventId || view . eventName ? "编辑中" : "点击展开" ) ) ,
labeledField ( e , "活动编号" , e ( "input" , { value : view.eventId , "aria-label" : "活动编号" , placeholder : "例如 event_supply_drop" , onChange : ( event : InputEvent ) = > view . setEventId ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "活动名称" , e ( "input" , { value : view.eventName , "aria-label" : "活动名称" , placeholder : "例如 空投补给" , onChange : ( event : InputEvent ) = > view . setEventName ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "生成类型" , e ( "select" , { value : view.eventClass , "aria-label" : "生成类型" , onChange : ( event : InputEvent ) = > view . setEventClass ( inputValue ( event ) ) } , e ( "option" , { value : "1" } , "范围生成" ) , e ( "option" , { value : "2" } , "固定坐标生成" ) ) ) ,
labeledField ( e , "活动计划" , e ( "input" , { value : view.eventSchedule , "aria-label" : "活动计划" , placeholder : "Cron(留空为手动)" , onChange : ( event : InputEvent ) = > view . setEventSchedule ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "RCON 命令" , e ( "input" , { value : view.eventCommand , "aria-label" : "RCON 命令" , placeholder : "例如 #start_event event_supply_drop" , onChange : ( event : InputEvent ) = > view . setEventCommand ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "活动公告" , e ( "input" , { value : view.eventPlacard , "aria-label" : "活动公告" , placeholder : "活动开始时显示的公告" , onChange : ( event : InputEvent ) = > view . setEventPlacard ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "触发概率 (%)" , e ( "input" , { value : view.eventPercent , "aria-label" : "活动概率" , type : "number" , placeholder : "100" , onChange : ( event : InputEvent ) = > view . setEventPercent ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "持续秒数" , e ( "input" , { value : view.eventDuration , "aria-label" : "活动持续秒数" , type : "number" , min : 1 , placeholder : "1800" , onChange : ( event : InputEvent ) = > view . setEventDuration ( inputValue ( event ) ) } ) ) ,
e ( "div" , { className : "console-row-actions" } ,
labeledField ( e , "NPC 数量" , e ( "input" , { value : view.eventNpc , "aria-label" : "NPC 数量" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setEventNpc ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "物品数量" , e ( "input" , { value : view.eventItem , "aria-label" : "物品数量" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setEventItem ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "僵尸数量" , e ( "input" , { value : view.eventZombie , "aria-label" : "僵尸数量" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setEventZombie ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "动物数量" , e ( "input" , { value : view.eventAnimal , "aria-label" : "动物数量" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setEventAnimal ( inputValue ( event ) ) } ) ) ) ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : saveEvent } , "保存活动" )
const renderEditor = ( event? : RecordMap ) = > {
const eventId = event ? textField ( event , "id" , "eventId" ) : "__new__" ;
const isNew = eventId === "__new__" ;
return e ( "div" , { className : "activity-editor" } ,
labeledField ( e , "活动编号" , e ( "input" , { value : view.eventId , "aria-label" : "活动编号" , placeholder : "例如 event_supply_drop" , readOnly : ! isNew , onChange : ( event : InputEvent ) = > view . setEventId ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "活动标题" , e ( "input" , { value : view.eventName , "aria-label" : "活动标题" , placeholder : "例如 空投补给" , onChange : ( event : InputEvent ) = > view . setEventName ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "区域名称" , e ( "input" , { value : view.eventRegionName , "aria-label" : "活动区域名称" , placeholder : "例如 北部机场补给区" , onChange : ( event : InputEvent ) = > view . setEventRegionName ( inputValue ( event ) ) } ) ) ,
e ( "div" , { className : "activity-region-fields" } ,
labeledField ( e , "区域半径" , e ( "input" , { value : view.eventRegionRadius , "aria-label" : "活动区域半径" , type : "number" , min : "1" , step : "1" , placeholder : "5000" , onChange : ( event : InputEvent ) = > view . setEventRegionRadius ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "中心 Z" , e ( "input" , { value : view.eventRegionZ , "aria-label" : "活动区域 Z" , type : "number" , onChange : ( event : InputEvent ) = > view . setEventRegionZ ( inputValue ( event ) ) } ) )
) ,
e ( "article" , { className : "console-module" } , e ( "h2" , null , "状态筛选" ) ,
e ( "select " , { value : view.activityStatus , "aria-label" : "活动状态" , onChange : ( event : InputEvent ) = > view . setActivityStatus ( inputValue ( event ) ) } , e ( "option" , { value : "all" } , "全部状态" ) , statuses . map ( ( status ) = > e ( "option" , { key : status , value : status } , status ) ) ) ,
e ( "p " , { className : "page-status" } , "活动定义属于插件;SCUM 原生 event_round 和 quest/task 只作为运行事实展示。" )
activityMapPicker ( e , data , view ) ,
e ( "div " , { className : "activity-region-fields activity-region-coordinates" } ,
labeledField ( e , "中心 X" , e ( "input " , { value : view.eventRegionX , "aria-label" : "活动区域 X" , type : "number" , onChange : ( event : InputEvent ) = > view . setEventRegionX ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "中心 Y" , e ( "input" , { value : view.eventRegionY , "aria-label" : "活动区域 Y" , type : "number" , onChange : ( event : InputEvent ) = > view . setEventRegionY ( inputValue ( event ) ) } ) )
) ,
e ( "div" , { className : "gift-form-wide gift-form-section activity-items-section" } , e ( "div" , { className : "gift-form-section-title" } , e ( "strong" , null , "活动物品 / NPC" ) , e ( "span" , null , "复用礼包目录;每项配置数量、概率和耐久" ) ) , activityItemEditorList ( e , view ) , catalogPicker ( e , view , "activity-items" ) ) ,
e ( "div" , { className : "console-row-actions activity-editor-actions" } ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . pluginData , onClick : saveEvent } , "保存活动" ) ,
event && textField ( event , "rconCommand" , "command" ) ? e ( "button" , { type : "button" , className : "icon-command" , disabled : ! actions ? . dispatch || ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在启动活动…" , async ( ) = > { await startEvent ( actions ? ? { } , event , data . eventProduces . filter ( ( produce ) = > textField ( produce , "eventId" , "event" ) === eventId ) ) ; view . refresh ( ) ; return "活动已进入执行队列。" ; } ) } , "立即启动" ) : null ,
event ? e ( "button" , { type : "button" , className : "icon-command danger-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在删除活动…" , async ( ) = > { await deleteEventDefinition ( actions ? ? { } , eventId , data . eventProduces ) ; view . setActivityDetailId ( "" ) ; view . refresh ( ) ; return "活动已删除。" ; } ) } , "删除" ) : null
)
) ;
} ;
return e ( "div" , { className : "console-record-list activity-list" } ,
e ( "div" , { className : "resource-filter-bar scum-filter-bar activity-list-toolbar" } ,
e ( "span" , { className : "page-status" } , ` ${ events . length } / ${ data . events . length } 个活动 ` ) ,
labeledField ( e , "活动状态" , e ( "select" , { value : view.activityStatus , "aria-label" : "活动状态" , onChange : ( event : InputEvent ) = > view . setActivityStatus ( inputValue ( event ) ) } , e ( "option" , { value : "all" } , "全部状态" ) , statuses . map ( ( status ) = > e ( "option" , { key : status , value : status } , status ) ) ) ) ,
e ( "button" , { type : "button" , className : "primary-command activity-create-command" , disabled : ! actions ? . pluginData , onClick : openNewActivity } , "新建活动" )
) ,
e ( "details" , { className : "console-module scum-editor" , open : view.produceEditorOpen , onToggle : ( event : InputEvent ) = > view . setProduceEditorOpen ( detailOpen ( event ) ) } , e ( "summary" , null , e ( "strong" , null , "活动生成项" ) , e ( "span" , { className : "page-status" } , view . produceEventId || view . produceId ? "编辑中" : "点击展开" ) ) ,
e ( "div" , { className : "console-row-actions" } ,
labeledField ( e , "活动编号" , e ( "input" , { value : view.produceEventId , "aria-label" : "生成项活动编号" , placeholder : "默认使用上方活动" , onChange : ( event : InputEvent ) = > view . setProduceEventId ( inputValue ( event ) ) } ) ) ,
labeledField ( e , "生成项编号" , e ( "input" , { value : view.produceId , "aria-label" : "生成项编号" , placeholder : "更新时填写" , onChange : ( event : InputEvent ) = > view . setProduceId ( inputValue ( event ) ) } ) ) ,
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 ) ) } ) ) ,
labeledField ( e , "Y" , e ( "input" , { value : view.produceY , "aria-label" : "生成 Y" , type : "number" , placeholder : "0" , onChange : ( event : InputEvent ) = > view . setProduceY ( inputValue ( event ) ) } ) ) ,
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 ) } ` ) ,
e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > { view . setProduceEditorOpen ( true ) ; view . setProduceEventId ( textField ( produce , "eventId" , "event" ) ) ; view . setProduceId ( textField ( produce , "id" , "produceId" ) ) ; view . setProduceTradeGoodsId ( textField ( produce , "tradeGoodsId" , "trade_goods_id" ) ) ; view . setProducePercent ( numField ( produce , "percent" ) ) ; view . setProduceValue ( numField ( produce , "value" ) ) ; view . setProduceRadius ( numField ( produce , "r" ) ) ; view . setProduceX ( numField ( produce , "x" ) ) ; view . setProduceY ( numField ( produce , "y" ) ) ; view . setProduceZ ( numField ( produce , "z" ) ) ; } } , "编辑" ) ,
e ( "button" , { type : "button" , className : "icon-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在删除生成项…" , async ( ) = > { await deleteEventProduce ( actions ? ? { } , produce ) ; view . refresh ( ) ; return "生成项已删除。" ; } ) } , "删除" ) ) ) : e ( "p" , { className : "page-status" } , "暂无活动生成项。" ) )
e ( "details" , { className : "console-module activity-record activity-record-new" , open : view.activityDetailId === "__new__" , onToggle : ( event : InputEvent ) = > { if ( detailOpen ( event ) ) openNewActivity ( ) ; else if ( view . activityDetailId === "__new__" ) view . setActivityDetailId ( "" ) ; } } ,
e ( "summary" , null , e ( "strong" , null , "新建活动" ) , e ( "span" , { className : "page-status" } , "展开配置活动区域和发放清单" ) ) ,
view . activityDetailId === "__new__" ? renderEditor ( ) : null
) ,
events . length ? events . map ( ( event , index ) = > {
const eventId = textField ( event , "id" , "eventId" ) ;
const run = runsByEvent . get ( eventId ) ;
const status = textField ( run , "status" , "state" ) || textField ( event , "status" , "state" ) || "unknown" ;
return e ( "article" , { key : idOf ( event , ` event- ${ index } ` ) , className : "console-record" } ,
e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( event , "name" , "title" ) || eventId || "未命名活动" ) , e ( "span" , { className : ` status-pill ${ activeStatus ( status ) ? "status-active" : "status-disabled" } ` } , status ) ) ,
e ( "div " , { className : "console-record-meta" } , e ( "span" , null , ` 生成 ${ Number ( field ( event , "class" ) ) === 2 ? "固定坐标" : "范围" } ` ) , e ( "span" , null , ` 计划 ${ textField ( event , "schedule" , " corn ") || "手动" } ` ) , e ( "span" , null , ` 概率 ${ numField ( event , "percent" , "probability" ) } % ` ) , e ( "span" , null , ` NPC/物品/僵尸/动物 ${ numField ( event , "npc" ) } / ${ numField ( event , "item" ) } / ${ numField ( event , "zombie" ) } / ${ numField ( event , "animal" ) } ` ) , e ( "span" , null , textField ( event , "placard" , "announcement" ) || "无公告" ) ) ,
e ( "div" , { className : "console-row-actions" } ,
e ( "button" , { type : "button" , className : "primary-command" , disabled : ! actions ? . dispatch || ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在启动活动…" , async ( ) = > { await startEvent ( actions ? ? { } , event , data . eventProduces . filter ( ( produce ) = > textField ( produce , "eventId" , "event" ) === eventId ) ) ; view . refresh ( ) ; return "活动命令已进入执行队列。" ; } ) } , "立即启动" ) ,
e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > { view . setEventEditorOpen ( true ) ; view . setEventId ( eventId ) ; view . setEventName ( textField ( event , "name" ) ) ; view . setEventClass ( numField ( event , "class" ) === "--" ? "1" : numField ( event , "class" ) ) ; view . setEventSchedule ( textField ( event , "schedule" , "corn" ) ) ; view . setEventCommand ( textField ( event , "rconCommand" , "command" ) ) ; view . setEventPlacard ( textField ( event , "placard" , "announcement" ) ) ; view . setEventPercent ( numField ( event , "percent" , "probability" ) ) ; view . setEventDuration ( numField ( event , "durationSeconds" ) === "--" ? "1800" : numField ( event , "durationSeconds" ) ) ; view . setEventNpc ( numField ( event , "npc" ) ) ; view . setEventItem ( numField ( event , "item" ) ) ; view . setEventZombie ( numField ( event , "zombie" ) ) ; view . setEventAnimal ( numField ( event , "animal" ) ) ; } } , "编辑" ) ,
e ( "button" , { type : "button" , className : "icon-command" , disabled : ! actions ? . pluginData , onClick : ( ) = > runAction ( view . setAction , "正在删除活动…" , async ( ) = > { await deleteEventDefinition ( actions ? ? { } , eventId , data . eventProduces ) ; view . refresh ( ) ; return "活动定义已删除。" ; } ) } , "删除 ")
const region = activityRegionDraft ( event , data . eventProduces . find ( ( row ) = > textField ( row , "eventId" , "event" ) === eventId ) ) ;
const items = activityItemDrafts ( event , data . eventProduces ) ;
return e ( "details " , { key : idOf ( event , ` event- ${ index } ` ) , className : "console-module activity-re cord ", open : view.activityDetailId === eventId , onToggle : ( toggle : InputEvent ) = > { if ( detailOpen ( toggle ) ) openActivity ( event ) ; else if ( view . activityDetailId === eventId ) view . setActivityDetailId ( "" ) ; } } ,
e ( "summary" , null , e ( "strong" , null , textField ( event , "title" , "name" ) || eventId || "未命名活动" ) , e ( "span" , { className : ` status-pill ${ activeStatus ( status ) ? "status-active" : "status-disabled" } ` } , status ) , e ( "span" , { className : "page-status" } , ` ${ region . name || "未命名区域" } · ${ items . length } 项物品 / NPC ` ) ) ,
view . activityDetailId === eventId ? renderEditor ( event ) : null
) ;
} ) : e ( "p" , { className : "page-status" } , "暂无活动,请先新建活动。 ")
) ;
}
function activityItemEditorList ( e : ReactLike [ "createElement" ] , view : ViewState ) { return catalogItemEditorList ( e , view . eventItems , view . setEventItems , "活动物品" ) ; }
function activityRegionDraft ( event : RecordMap , produce? : RecordMap ) : { name : string ; radius : string ; x : string ; y : string ; z : string } {
const nested = field ( event , "region" , "area" , "location" ) ;
const source = isRecord ( nested ) ? nested : event ;
const fallback = produce ? positionOf ( produce ) : undefined ;
const value = ( keys : string [ ] , fallbackValue = "" ) = > { const direct = textField ( source , . . . keys ) ; return direct || ( fallback ? textField ( fallback , . . . keys ) : fallbackValue ) ; } ;
const name = isRecord ( nested ) ? value ( [ "name" , "label" , "regionName" , "areaName" ] ) : value ( [ "regionName" , "areaName" , "regionLabel" ] ) ;
return { name , radius : value ( [ "radius" , "r" , "size" ] , "5000" ) , x : value ( [ "x" , "locationX" ] ) , y : value ( [ "y" , "locationY" ] ) , z : value ( [ "z" , "locationZ" ] , "0" ) } ;
}
function activityItemDrafts ( event : RecordMap , produces : RecordMap [ ] ) : GiftItemDraft [ ] {
const direct = giftItemDrafts ( event ) ;
if ( direct . length ) return direct ;
const eventId = textField ( event , "id" , "eventId" ) ;
return produces . filter ( ( produce ) = > textField ( produce , "eventId" , "event" ) === eventId ) . map ( ( produce ) = > ( { catalogCode : textField ( produce , "catalogCode" , "tradeGoodsId" , "trade_goods_id" ) , quantity : numField ( produce , "quantity" , "value" ) === "--" ? "1" : numField ( produce , "quantity" , "value" ) , probability : numField ( produce , "probability" , "percent" ) === "--" ? "100" : numField ( produce , "probability" , "percent" ) , durability : giftItemRawDurability ( produce ) } ) ) ;
}
function activityRegionValue ( view : ViewState ) : { name : string ; radius : number ; x : number ; y : number ; z : number } {
const radius = Number ( view . eventRegionRadius ) ; const x = Number ( view . eventRegionX ) ; const y = Number ( view . eventRegionY ) ; const z = Number ( view . eventRegionZ ) ;
if ( ! view . eventRegionX . trim ( ) || ! view . eventRegionY . trim ( ) || ! [ radius , x , y , z ] . every ( Number . isFinite ) || radius <= 0 ) throw new Error ( "请在地图上选择活动区域,并填写有效的区域半径。" ) ;
return { name : view.eventRegionName.trim ( ) , radius , x , y , z } ;
}
function activityProduceRecords ( eventId : string , items : Array < { catalogCode : string ; quantity : number | string ; probability : number | string ; durability? : number | string } > , region : { radius : number ; x : number ; y : number ; z : number } ) : RecordMap [ ] {
return items . map ( ( item , index ) = > { const quantity = Number ( item . quantity ) ; const probability = Number ( item . probability ) ; const durability = Number ( item . durability ) ; return { id : ` produce- ${ index + 1 } ` , eventId , tradeGoodsId : item.catalogCode , catalogCode : item.catalogCode , percent : Number.isFinite ( probability ) ? probability : 100 , probability : Number.isFinite ( probability ) ? probability : 100 , value : Number.isSafeInteger ( quantity ) && quantity > 0 ? quantity : 1 , quantity : Number.isSafeInteger ( quantity ) && quantity > 0 ? quantity : 1 , . . . ( Number . isFinite ( durability ) ? { durability } : { } ) , r : region.radius , x : region.x , y : region.y , z : region.z } ; } ) ;
}
function activityCount ( items : Array < { catalogCode : string ; quantity : number | string } > , kind : ScumCatalogEntry [ "kind" ] , fallback = "0" ) : number {
const count = items . filter ( ( item ) = > scumCatalogEntryFor ( item . catalogCode ) ? . kind === kind ) . reduce ( ( total , item ) = > total + ( Number ( item . quantity ) || 0 ) , 0 ) ;
return count || Number ( fallback ) || 0 ;
}
function activityMapPicker ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , view : ViewState ) {
const settings = data . mapSettings . find ( ( value ) = > textField ( value , "_recordKey" , "id" ) === "current" ) ? ? data . mapSettings [ 0 ] ;
const bounds = resolveMapBounds ( settings ) ;
const selected = view . eventRegionX . trim ( ) && view . eventRegionY . trim ( ) && Number . isFinite ( Number ( view . eventRegionX ) ) && Number . isFinite ( Number ( view . eventRegionY ) ) ? { x : Number ( view . eventRegionX ) , y : Number ( view . eventRegionY ) } : undefined ;
const selectedStyle = selected ? activityMapSelectionStyle ( selected , Number ( view . eventRegionRadius ) || 5000 , bounds ) : undefined ;
const choose = ( event : ActivityMapPointerEvent ) = > {
const point = activityMapWorldPoint ( event , bounds ) ;
if ( ! point ) return ;
view . setEventRegionX ( String ( Math . round ( point . x ) ) ) ; view . setEventRegionY ( String ( Math . round ( point . y ) ) ) ;
const start = activityMapDragState ;
if ( start . active ) {
const startPoint = activityMapWorldPoint ( { . . . event , clientX : start.startX , clientY : start.startY , currentTarget : event.currentTarget } , bounds ) ;
if ( startPoint ) {
const radius = Math . round ( Math . hypot ( point . x - startPoint . x , point . y - startPoint . y ) ) ;
if ( radius > 0 ) view . setEventRegionRadius ( String ( radius ) ) ;
}
}
activityMapDragState . active = false ;
} ;
return e ( "div" , { className : "activity-map-picker" } ,
e ( "div" , { className : "activity-map-picker-head" } , e ( "strong" , null , "活动区域" ) , e ( "span" , { className : "page-status" } , "点击选择中心;从中心拖动可直接设置半径" ) ) ,
e ( "div" , { className : "activity-map-picker-board" , role : "application" , "aria-label" : "选择活动区域地图" , onPointerDown : ( event : ActivityMapPointerEvent ) = > { activityMapDragState . active = true ; activityMapDragState . pointerId = event . pointerId ? ? - 1 ; activityMapDragState . startX = event . clientX ? ? 0 ; activityMapDragState . startY = event . clientY ? ? 0 ; event . currentTarget ? . setPointerCapture ? . ( event . pointerId ) ; } , onPointerUp : choose , onPointerCancel : ( ) = > { activityMapDragState . active = false ; } } ,
e ( "div" , { className : "activity-map-picker-scene" , style : { backgroundImage : mapBoardStyle ( mapLayerSetting ( settings ) , false ) } } ,
mapGridOverlay ( e ) ,
data . mapRegions . map ( ( region , index ) = > { const position = positionOf ( region ) ; return hasCoordinates ( position ) ? e ( "button" , { key : idOf ( region , ` map-region- ${ index } ` ) , type : "button" , className : "activity-map-region" , style : mapPointStyle ( position , bounds ) , title : textField ( region , "name" , "label" , "id" ) || "已有地图区域" , onPointerDown : ( event : ActivityMapPointerEvent ) = > event . stopPropagation ? . ( ) , onClick : ( event : ActivityMapPointerEvent ) = > { event . stopPropagation ? . ( ) ; view . setEventRegionName ( textField ( region , "name" , "label" ) ) ; view . setEventRegionX ( String ( Number ( field ( position , "x" , "locationX" ) ) ) ) ; view . setEventRegionY ( String ( Number ( field ( position , "y" , "locationY" ) ) ) ) ; view . setEventRegionZ ( String ( Number ( field ( position , "z" , "locationZ" ) ? ? 0 ) ) ) ; view . setEventRegionRadius ( numField ( region , "radius" , "r" ) === "--" ? "5000" : numField ( region , "radius" , "r" ) ) ; } } , textField ( region , "name" , "label" ) || "区" ) : null ; } ) ,
selectedStyle ? e ( "span" , { className : "activity-map-selection" , style : selectedStyle , "aria-label" : "当前活动区域" } ) : null
)
) ;
} ) : e ( "p" , { className : "page-status" } , "没有符合状态筛选的真实活动。" ) ,
e ( "div" , { className : "overview-two-col" } ,
tablePanel ( e , "原生赛事轮次" , data . nativeEventRounds , ( event ) = > [ textField ( event , "eventId" ) || "event" , textField ( event , "state" ) || "unknown" , ` Kills ${ numField ( event , "enemyKills" ) } ` , dateField ( event , "startTime" ) ] ) ,
tablePanel ( e , "Quest / Task" , data . tasks , ( task ) = > [ textField ( task , "taskKind" ) || "task" , textField ( task , "dataAssetPath" ) || "unknown" , textField ( task , "state" ) || "unknown" , textField ( task , "userProfileId" ) || "unknown" ] )
) ,
e ( "div " , { className : "overview-two-col" } ,
itemCatalogPanel ( e , data . tradeGoods ) ,
tablePanel ( e , "最近商人交易" , tradeEvents , ( event ) = > [ tradeGoodsLabel ( data . tradeGoods , textField ( event , "itemCode" , "code" , "tradeGoodsId" ) ) , ` ${ tradeActionLabel ( event ) } × ${ numField ( event , "quantity" , "itemCount" ) } ` , ` 玩家 ${ shortHash ( textField ( event , "steamId" , "playerId" ) ) } ` , ` 价格 ${ numField ( event , "price" , "currencyDelta" ) } ` , dateField ( event , "observedAt" , "occurredAt" , "createdAt" ) ] )
) ,
tablePanel ( e , "最近活动记录" , [ . . . data . eventRuns , . . . activityHistory ] , ( event ) = > [ textField ( event , "eventName" , "type" , "kind" , "activityType" ) || "event" , textField ( event , "subjectName" , "eventId" , "subjectId" , "subject" ) || "unknown" , textField ( event , "status" , "result" , "state" ) || "unknown" , dateField ( event , "startedAt" , "occurredAt" , "createdAt" ) ] )
e ( "p " , { className : "page-status" } , ` 当前区域: ${ view . eventRegionName || "未命名" } · ${ view . eventRegionX || "未选择" } / ${ view . eventRegionY || "未选择" } · 半径 ${ view . eventRegionRadius || "未设置" } ` )
) ;
}
function activityMapWorldPoint ( event : ActivityMapPointerEvent , bounds : { worldMinX : number ; worldMinY : number ; worldMaxX : number ; worldMaxY : number } ) : { x : number ; y : number } | undefined {
const rect = event . currentTarget ? . getBoundingClientRect ? . ( ) ;
if ( ! rect || ! rect . width || ! rect . height || event . clientX === undefined || event . clientY === undefined ) return undefined ;
const horizontal = Math . max ( 0 , Math . min ( 1 , ( event . clientX - rect . left ) / rect . width ) ) ;
const vertical = Math . max ( 0 , Math . min ( 1 , ( event . clientY - rect . top ) / rect . height ) ) ;
return { x : bounds.worldMaxX - horizontal * ( bounds . worldMaxX - bounds . worldMinX ) , y : bounds.worldMaxY - vertical * ( bounds . worldMaxY - bounds . worldMinY ) } ;
}
function activityMapSelectionStyle ( point : { x : number ; y : number } , radius : number , bounds : { worldMinX : number ; worldMinY : number ; worldMaxX : number ; worldMaxY : number } ) : Record < string , string > {
const center = mapPointStyle ( point , bounds ) ;
const diameter = Math . max ( 2 , Math . min ( 80 , radius * 2 / Math . max ( bounds . worldMaxX - bounds . worldMinX , bounds . worldMaxY - bounds . worldMinY ) * 100 ) ) ;
return { . . . center , width : ` ${ diameter } % ` , height : ` ${ diameter } % ` } ;
}
function giftsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , view : ViewState ) {
const actions = input . workspaceActions ;
const openNewGift = ( ) = > {
@@ -706,9 +745,11 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
) ;
}
function giftItemEditorList ( e : ReactLike [ "createElement" ] , view : ViewState ) {
if ( ! view . giftItems . length ) return e ( "p" , { className : "page-status" } , "尚未添加物品,请使用目录选择器加入礼包内容。" ) ;
return e ( "div" , { className : "gift-item-editor-list" } , view . giftItems . map ( ( item , index ) = > {
function giftItemEditorList ( e : ReactLike [ "createElement" ] , view : ViewState ) { return catalogItemEditorList ( e , view . giftItems , view . setGiftItems , "礼包物品" ) ; }
function catalogItemEditorList ( e : ReactLike [ "createElement" ] , items : GiftItemDraft [ ] , setItems : StateSetter < GiftItemDraft [ ] > , labelPrefix : string ) {
if ( ! items . length ) return e ( "p" , { className : "page-status" } , ` 尚未添加物品,请使用目录选择器加入 ${ labelPrefix } 。 ` ) ;
return e ( "div" , { className : "gift-item-editor-list" } , items . map ( ( item , index ) = > {
const entry = scumCatalogEntryFor ( item . catalogCode ) ;
const supportsDurability = giftItemSupportsDurability ( entry ) ;
const itemLabel = entry ? scumCatalogDisplayName ( entry ) : item . catalogCode || "未命名物品" ;
@@ -718,17 +759,17 @@ function giftItemEditorList(e: ReactLike["createElement"], view: ViewState) {
catalogIconStrip ( e , catalogIconsForValue ( item . catalogCode ) ) ,
e ( "div" , { className : "gift-item-editor-info" } , e ( "strong" , null , itemLabel ) , e ( "span" , { className : "provider-id" } , ` ${ category } · ${ item . catalogCode || "未配置代码" } ` ) ) ,
e ( "div" , { className : "gift-item-editor-fields" } ,
labeledField ( e , "数量" , e ( "input" , { type : "number" , min : "1" , step : "1" , value : item.quantity , "aria-label" : ` 礼包物品 ${ itemNumber } 数量 ` , onChange : ( event : InputEvent ) = > updateGiftItemDraft ( view , index , "quantity" , inputValue ( event ) ) } ) ) ,
labeledField ( e , "概率" , e ( "input" , { type : "number" , min : "0" , max : "100" , step : "1" , value : item.probability , "aria-label" : ` 礼包物品 ${ itemNumber } 概率 ` , onChange : ( event : InputEvent ) = > updateGiftItemDraft ( view , index , "probability" , inputValue ( event ) ) } ) ) ,
labeledField ( e , "耐久" , e ( "input" , { type : "number" , min : "0" , max : "100" , step : "1" , value : item.durability , disabled : ! supportsDurability , placeholder : supportsDurability ? "默认" : "不适用" , "aria-label" : ` 礼包物品 ${ itemNumber } 耐久 ` , onChange : ( event : InputEvent ) = > updateGiftItemDraft ( view , index , "durability" , inputValue ( event ) ) } ) ) ,
e ( "button" , { type : "button" , className : "gift-item-editor-remove icon-command" , "aria-label" : ` 删除礼包物品 ${ itemNumber } ` , onClick : ( ) = > view . setGiftItems ( ( items ) = > items . filter ( ( _ , itemIndex ) = > itemIndex !== index ) ) } , "删除" )
labeledField ( e , "数量" , e ( "input" , { type : "number" , min : "1" , step : "1" , value : item.quantity , "aria-label" : ` ${ labelPrefix } ${ itemNumber } 数量 ` , onChange : ( event : InputEvent ) = > updateCatalogItemDraft ( setItems , index , "quantity" , inputValue ( event ) ) } ) ) ,
labeledField ( e , "概率" , e ( "input" , { type : "number" , min : "0" , max : "100" , step : "1" , value : item.probability , "aria-label" : ` ${ labelPrefix } ${ itemNumber } 概率 ` , onChange : ( event : InputEvent ) = > updateCatalogItemDraft ( setItems , index , "probability" , inputValue ( event ) ) } ) ) ,
labeledField ( e , "耐久" , e ( "input" , { type : "number" , min : "0" , max : "100" , step : "1" , value : item.durability , disabled : ! supportsDurability , placeholder : supportsDurability ? "默认" : "不适用" , "aria-label" : ` ${ labelPrefix } ${ itemNumber } 耐久 ` , onChange : ( event : InputEvent ) = > updateCatalogItemDraft ( setItems , index , "durability" , inputValue ( event ) ) } ) ) ,
e ( "button" , { type : "button" , className : "gift-item-editor-remove icon-command" , "aria-label" : ` 删除 ${ labelPrefix } ${ itemNumber } ` , onClick : ( ) = > setItems ( ( current ) = > current . filter ( ( _ , itemIndex ) = > itemIndex !== index ) ) } , "删除" )
)
) ;
} ) ) ;
}
function updateGiftItemDraft ( view : ViewState , index : number , key : GiftItemDraftField , value : string ) : void {
view . setGiftItems ( ( items ) = > items . map ( ( item , itemIndex ) = > itemIndex === index ? { . . . item , [ key ] : value } : item ) ) ;
function updateCatalogItemDraft ( setItems : StateSetter < GiftItemDraft [ ] > , index : number , key : GiftItemDraftField , value : string ) : void {
setItems ( ( items ) = > items . map ( ( item , itemIndex ) = > itemIndex === index ? { . . . item , [ key ] : value } : item ) ) ;
}
function giftDetailsDialog ( e : ReactLike [ "createElement" ] , gift : RecordMap , view : ViewState , editGift : ( ) = > void ) {
@@ -1387,7 +1428,7 @@ function mapLayerLabel(layer: ScumMapLayer): string { return layer === "topo" ?
function catalogMatches ( query : string , kind : string , target : CatalogPickTarget ) : ScumCatalogEntry [ ] {
const search = query . trim ( ) . toLowerCase ( ) ;
return scumCatalogEntries . filter ( ( entry ) = > {
const matchesKind = target === "gift-items" ? kind === "all" || scumGiftCatalogFilterKey ( entry ) === kind : kind === "all" || entry . kind === kind ;
const matchesKind = kind === "all" || scumGiftCatalogFilterKey ( entry ) === kind ;
return matchesKind && ( ! search || scumCatalogSearchText ( entry ) . includes ( search ) ) ;
} ) . slice ( 0 , scumCatalogPickLimit ) ;
}
@@ -1414,15 +1455,15 @@ function giftCatalogIcons(gift: RecordMap): string[] {
}
function catalogPicker ( e : ReactLike [ "createElement" ] , view : ViewState , target : CatalogPickTarget ) {
const filters = target === "gift-items" ? scumGiftCatalogCategories : scumCatalogKinds ;
const filters = scumGiftCatalogCategories ;
const activeFilter = filters . some ( ( [ key ] ) = > key === view . catalogKind ) ? view . catalogKind : "all" ;
const matches = catalogMatches ( view . catalogSearch , activeFilter , target ) ;
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" } , "图标 / 名称 / 代码" ) ) ,
e ( "summary" , null , e ( "strong" , null , target === "gift-items" ? "从 SCUM 目录选择礼包内容" : "从 SCUM 目录选择活动物品 / NPC " ) , 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" : target === "gift-items" ? "礼包目录分类" : "目录类型 " } , filters . map ( ( [ key , label ] ) = > e ( "button" , { key , type : "button" , className : activeFilter === key ? "icon-command is-active" : "icon-command" , "aria-pressed" : activeFilter === key , onClick : ( ) = > view . setCatalogKind ( key ) } , label ) ) ) ,
e ( "div" , { className : "scum-catalog-grid" } , matches . length ? matches . map ( ( entry ) = > { const icon = scumCatalogIconUrl ( entry . icon ) ; const displayName = scumCatalogDisplayName ( entry ) ; return e ( "button" , { key : entry.code , type : "button" , className : "scum-catalog-chip" , title : ` ${ displayName } · ${ scumCatalogCategoryLabel ( entry ) } · ${ entry . command } ` , onClick : ( ) = > target === "gift-items" ? view . setGiftItems ( ( items ) = > appendCatalogItem ( items , 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 , displayName ) , e ( "span" , { className : "provider-id" } , ` ${ scumCatalogCategoryLabel ( entry ) } · ${ entry . code } ` ) ) ) ; } ) : e ( "p" , { className : "page-status" } , "没有匹配的目录条目。" ) )
e ( "input" , { type : "search" , value : view.catalogSearch , "aria-label" : target === "gift-items" ? "搜索礼包目录" : "搜索活动物品目录 " , placeholder : "中文名 / 英文 名 / 代码,例如 莱卡 " , onChange : ( event : InputEvent ) = > view . setCatalogSearch ( inputValue ( event ) ) } ) ,
e ( "div" , { className : "scum-catalog-kinds" , role : "group" , "aria-label" : target === "gift-items" ? "礼包目录分类" : "活动目录分类 " } , filters . map ( ( [ key , label ] ) = > e ( "button" , { key , type : "button" , className : activeFilter === key ? "icon-command is-active" : "icon-command" , "aria-pressed" : activeFilter === key , onClick : ( ) = > view . setCatalogKind ( key ) } , label ) ) ) ,
e ( "div" , { className : "scum-catalog-grid" } , matches . length ? matches . map ( ( entry ) = > { const icon = scumCatalogIconUrl ( entry . icon ) ; const displayName = scumCatalogDisplayName ( entry ) ; return e ( "button" , { key : entry.code , type : "button" , className : "scum-catalog-chip" , title : ` ${ displayName } · ${ scumCatalogCategoryLabel ( entry ) } · ${ entry . command } ` , onClick : ( ) = > target === "gift-items" ? view . setGiftItems ( ( items ) = > appendCatalogItem ( items , entry . code ) ) : view . setEventItems ( ( items ) = > appendCatalogItem ( items , 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 , displayName ) , e ( "span" , { className : "provider-id" } , ` ${ scumCatalogCategoryLabel ( entry ) } · ${ entry . code } ` ) ) ) ; } ) : e ( "p" , { className : "page-status" } , "没有匹配的目录条目。" ) )
) : null
) ;
}