@ -1,6 +1,7 @@
import { BaseScene } from "@/scene/BaseScene" ;
import { sceneManager } from "@/scene/SceneManager" ;
import { SceneType } from "@/enums/SceneType" ;
import { appRuntime } from "@/kernel/AppRuntime" ;
import { ChaseGameModel } from "@/game/chase/model" ;
import type { Difficulty , NodeId } from "@/game/chase/types" ;
import { Container , Graphics , Text , TextStyle , type Ticker } from "pixi.js" ;
@ -8,6 +9,11 @@ import { Container, Graphics, Text, TextStyle, type Ticker } from "pixi.js";
const NODE_RADIUS = 18 ;
const NODE_GAP = 58 ;
const END_ACTION_COOLDOWN_MS = 400 ;
const PAD_X = 20 ;
const PAD_Y = 12 ;
/** 顶栏 + 开局控件占用高度(用于把地图放到下方安全区) */
const TOP_CHROME_H = 168 ;
const SEED_INPUT_WIDTH = 220 ;
type ChaseModelLike = Pick <
ChaseGameModel ,
@ -59,6 +65,9 @@ export default class ChaseScene extends BaseScene {
private unsubscribeStageChange ? : ( ) = > void ;
private endActionLocked = false ;
private lastEndActionAt = 0 ;
private readonly onWindowResize = ( ) : void = > {
this . relayoutChrome ( ) ;
} ;
constructor ( options : ChaseSceneOptions = { } ) {
super ( "chase" , SceneType . Normal ) ;
@ -74,59 +83,54 @@ export default class ChaseScene extends BaseScene {
this . hudText = new Text ( {
text : "Chase: loading..." ,
style : new TextStyle ( {
fontSize : 20 ,
fill : 0xffffff ,
fontSize : 16 ,
fill : 0xe2e8f0 ,
fontFamily : "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif" ,
} ) ,
} ) ;
this . hudText . position . set ( 24 , 24 ) ;
this . stage . addChild ( this . hudText ) ;
this . winCountText = new Text ( {
text : "" ,
style : new TextStyle ( {
fontSize : 18 ,
fontSize : 17 ,
fill : 0xffffff ,
fontFamily : "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif" ,
} ) ,
} ) ;
this . winCountText . position . set ( 24 , 6 ) ;
this . stage . addChild ( this . winCountText ) ;
this . seedText = new Text ( {
text : "" ,
style : new TextStyle ( {
fontSize : 18 ,
fontSize : 17 ,
fill : 0xffffff ,
fontFamily : "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif" ,
} ) ,
} ) ;
this . seedText . anchor . set ( 1 , 0 ) ;
this . seedText . position . set ( 760 , 6 ) ;
this . stage . addChild ( this . seedText ) ;
this . setupText = new Text ( {
text : "" ,
style : new TextStyle ( {
fontSize : 16 ,
fill : 0xdbeafe ,
fontSize : 14 ,
fill : 0x93c5fd ,
fontFamily : "'Microsoft YaHei', 'PingFang SC', system-ui, sans-serif" ,
} ) ,
} ) ;
this . setupText . position . set ( 24 , 54 ) ;
this . stage . addChild ( this . setupText ) ;
this . setupLayer . eventMode = "passive" ;
this . setupLayer . position . set ( 24 , 84 ) ;
this . stage . addChild ( this . setupLayer ) ;
this . buildSetupControls ( ) ;
this . graphLayer . eventMode = "passive" ;
this . graphLayer . position . set ( 140 , 140 ) ;
this . stage . addChild ( this . graphLayer ) ;
this . buildResultOverlay ( ) ;
this . stage . addChild ( this . resultOverlay ) ;
this . relayoutChrome ( ) ;
if ( ! this . model ) {
this . model =
this . modelFactory ? . ( ) ? ?
@ -139,6 +143,9 @@ export default class ChaseScene extends BaseScene {
}
protected onSceneEnter ( ) : void {
if ( typeof globalThis !== "undefined" && "addEventListener" in globalThis ) {
globalThis . addEventListener ( "resize" , this . onWindowResize ) ;
}
if ( ! this . unsubscribeStageChange ) {
this . unsubscribeStageChange = sceneManager . onStageChange ( ( current ) = > {
if ( current . name !== this . name ) {
@ -152,6 +159,9 @@ export default class ChaseScene extends BaseScene {
}
protected onSceneExit ( ) : void {
if ( typeof globalThis !== "undefined" && "removeEventListener" in globalThis ) {
globalThis . removeEventListener ( "resize" , this . onWindowResize ) ;
}
this . unsubscribeStageChange ? . ( ) ;
this . unsubscribeStageChange = undefined ;
this . detachSeedInputBridge ( ) ;
@ -179,6 +189,9 @@ export default class ChaseScene extends BaseScene {
child . destroy ( { children : true } ) ;
}
const nodes = Object . values ( state . snapshot . graph . nodes ) ;
const { offsetX , offsetY } = this . computeGraphCenterOffset ( nodes ) ;
const thiefNode = state . snapshot . graph . nodes [ state . snapshot . thiefNodeId ] ;
const availableMoves = thiefNode
? thiefNode . neighbors . filter ( ( id ) = > id !== state . snapshot . guardNodeId )
@ -192,11 +205,10 @@ export default class ChaseScene extends BaseScene {
highlight . label = "move-highlight" ;
highlight . circle ( 0 , 0 , NODE_RADIUS + 8 ) ;
highlight . stroke ( { width : 3 , color : 0xfacc15 , alpha : 0.9 } ) ;
highlight . position . set ( node . q * NODE_GAP , node . r * NODE_GAP ) ;
highlight . position . set ( node . q * NODE_GAP + offsetX , node . r * NODE_GAP + offsetY ) ;
this . graphLayer . addChild ( highlight ) ;
}
const nodes = Object . values ( state . snapshot . graph . nodes ) ;
for ( const node of nodes ) {
const nodeView = new Graphics ( ) ;
nodeView . circle ( 0 , 0 , NODE_RADIUS ) ;
@ -210,7 +222,7 @@ export default class ChaseScene extends BaseScene {
: 0x64748b ;
nodeView . fill ( { color , alpha : 0.95 } ) ;
nodeView . stroke ( { width : 2 , color : 0xe2e8f0 , alpha : 0.9 } ) ;
nodeView . position . set ( node . q * NODE_GAP , node . r * NODE_GAP ) ;
nodeView . position . set ( node . q * NODE_GAP + offsetX , node . r * NODE_GAP + offsetY ) ;
nodeView . eventMode = "static" ;
nodeView . cursor = "pointer" ;
nodeView . on ( "pointerdown" , ( ) = > this . handleNodeClick ( node . id ) ) ;
@ -218,6 +230,58 @@ export default class ChaseScene extends BaseScene {
}
}
/** 将节点包络中心移到局部原点,避免负坐标画进顶栏 */
private computeGraphCenterOffset (
nodes : Array < { q : number ; r : number } > ,
) : { offsetX : number ; offsetY : number } {
if ( nodes . length === 0 ) {
return { offsetX : 0 , offsetY : 0 } ;
}
let minX = Infinity ;
let maxX = - Infinity ;
let minY = Infinity ;
let maxY = - Infinity ;
for ( const node of nodes ) {
const x = node . q * NODE_GAP ;
const y = node . r * NODE_GAP ;
minX = Math . min ( minX , x ) ;
maxX = Math . max ( maxX , x ) ;
minY = Math . min ( minY , y ) ;
maxY = Math . max ( maxY , y ) ;
}
const cx = ( minX + maxX ) / 2 ;
const cy = ( minY + maxY ) / 2 ;
return { offsetX : - cx , offsetY : - cy } ;
}
private relayoutChrome ( ) : void {
const { width , height } = appRuntime . game . getInfo ( ) ;
const safeTop = PAD_Y ;
this . winCountText ? . position . set ( PAD_X , safeTop ) ;
this . seedText ? . position . set ( width - PAD_X , safeTop ) ;
this . hudText ? . position . set ( PAD_X , safeTop + 26 ) ;
this . setupText ? . position . set ( PAD_X , safeTop + 52 ) ;
this . setupLayer . position . set ( PAD_X , safeTop + 78 ) ;
const playTop = Math . min ( TOP_CHROME_H , height * 0.28 ) ;
const playCenterY = playTop + ( height - playTop ) / 2 ;
this . graphLayer . position . set ( width / 2 , playCenterY ) ;
this . resultOverlay . position . set ( width / 2 - 120 , playTop + 8 ) ;
this . positionSeedInputDom ( ) ;
}
private positionSeedInputDom ( ) : void {
if ( ! this . seedInputElement ) {
return ;
}
const topPx = this . setupLayer . position . y + 48 ;
this . seedInputElement . style . left = ` ${ PAD_X } px ` ;
this . seedInputElement . style . top = ` ${ topPx } px ` ;
this . seedInputElement . style . width = ` ${ SEED_INPUT_WIDTH } px ` ;
}
private handleNodeClick ( targetNodeId : NodeId ) : void {
if ( ! this . model ) {
return ;
@ -363,7 +427,6 @@ export default class ChaseScene extends BaseScene {
private buildResultOverlay ( ) : void {
this . resultOverlay . visible = false ;
this . resultOverlay . eventMode = "passive" ;
this . resultOverlay . position . set ( 24 , 170 ) ;
this . resultText = new Text ( {
text : "" ,
@ -449,14 +512,17 @@ export default class ChaseScene extends BaseScene {
input . placeholder = "Seed (optional)" ;
input . value = this . seedInput ;
input . style . position = "fixed" ;
input . style . left = "24px" ;
input . style . top = "130px" ;
input . style . zIndex = "10" ;
input . style . width = "180px" ;
input . style . boxSizing = "border-box" ;
input . style . padding = "6px 10px" ;
input . style . fontSize = "14px" ;
input . style . borderRadius = "6px" ;
input . style . border = "1px solid #94a3b8" ;
input . addEventListener ( "input" , this . handleSeedInputEvent ) ;
doc . body . appendChild ( input ) ;
this . seedInputElement = input ;
this . inputAttached = true ;
this . positionSeedInputDom ( ) ;
}
private detachSeedInputBridge ( ) : void {