ee79331d48
Office canvas fixes: - Lazy-create the Phaser game via ResizeObserver on the first non-zero layout instead of creating it inside the display:none office page with inline px fallback sizes. Phaser's RESIZE-mode 500ms parent poll has no zero guard, so the old path shrank the canvas to 0x0 (blue-black screen) and later restored it to a stale wrong size (clipped office). - On unhide, re-measure with scale.getParentBounds() before scale.refresh(); plain scale.resize() is clobbered by the stale cached parentSize in RESIZE mode. - Fix whole-game freeze when clicking an office card: camera effects resolve ease names via EaseMap, which has no 'Cubic.Out' key, leaving effect.ease undefined and killing the RAF loop with a per-frame TypeError. Use 'Cubic.easeOut' for cam.pan in panToOffice/resetCameraView. - Bound GameBridge queues (latest snapshot supersedes, event queue capped) since game creation is now deferred until the Office page is first opened. Sidebar: - Add a collapse/expand handle on the canvas/sidebar boundary with a 220ms grid transition; state persists in localStorage. The canvas follows the column change automatically through the ResizeObserver path. Stacked (<=1024px) layout collapses the bottom panel and moves the handle to the bottom edge. README: - Add Simplified Chinese translation (README.zh-CN.md) with a language switcher in both files; fix stale TOC entries in the English README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
||
import Phaser from 'phaser'
|
||
import { createGameConfig } from './config'
|
||
import type { GameBridge } from './GameBridge'
|
||
import { BootScene } from './scenes/BootScene'
|
||
import { OfficeScene } from './scenes/OfficeScene'
|
||
|
||
interface Props {
|
||
bridge: GameBridge
|
||
}
|
||
|
||
export function PhaserGame({ bridge }: Props) {
|
||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
const gameRef = useRef<Phaser.Game | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (!wrapperRef.current || !containerRef.current) return
|
||
|
||
const wrapper = wrapperRef.current
|
||
const container = containerRef.current
|
||
|
||
const createGame = (w: number, h: number) => {
|
||
console.log('[PhaserGame] Creating Phaser game', w, '×', h)
|
||
const config = createGameConfig(container, w, h)
|
||
config.scene = [BootScene, OfficeScene]
|
||
const game = new Phaser.Game(config)
|
||
game.registry.set('bridge', bridge)
|
||
gameRef.current = game
|
||
}
|
||
|
||
if (typeof ResizeObserver === 'undefined') {
|
||
createGame(wrapper.clientWidth || window.innerWidth - 380, wrapper.clientHeight || window.innerHeight - 48)
|
||
return () => {
|
||
gameRef.current?.destroy(true)
|
||
gameRef.current = null
|
||
}
|
||
}
|
||
|
||
// The office page can start hidden (display:none → 0×0). Never create or
|
||
// resize the game at zero size; wait for the first real layout instead.
|
||
const observer = new ResizeObserver((entries) => {
|
||
const rect = entries[entries.length - 1].contentRect
|
||
const w = Math.floor(rect.width)
|
||
const h = Math.floor(rect.height)
|
||
if (w < 1 || h < 1) return // hidden — keep last known size
|
||
if (!gameRef.current) {
|
||
createGame(w, h)
|
||
} else {
|
||
// In RESIZE scale mode the game follows parentSize, which Phaser only
|
||
// re-measures on its 500ms poll — and scale.resize() gets clobbered by
|
||
// that stale value. Re-measure the parent, then refresh.
|
||
const scale = gameRef.current.scale
|
||
scale.getParentBounds()
|
||
scale.refresh()
|
||
}
|
||
})
|
||
observer.observe(wrapper)
|
||
|
||
return () => {
|
||
observer.disconnect()
|
||
gameRef.current?.destroy(true)
|
||
gameRef.current = null
|
||
}
|
||
}, [bridge]) // bridge is a stable ref, effect runs once
|
||
|
||
return (
|
||
// Wrapper fills the CSS grid cell
|
||
<div ref={wrapperRef} style={{ width: '100%', height: '100%' }}>
|
||
{/* Phaser mounts its canvas inside this div; it must track the wrapper
|
||
so Phaser's own parent-bounds polling reads the true size. */}
|
||
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
|
||
</div>
|
||
)
|
||
}
|