Files
OpenOPC/opc/plugins/office_ui/frontend_src/game/PhaserGame.tsx
T
LZH-YS1998 ee79331d48 fix(office-ui): resolve blank/frozen office canvas and add sidebar collapse
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>
2026-07-07 11:47:04 +08:00

76 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}