Files
OpenOPC/opc/plugins/office_ui/frontend_src/game/config.ts
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

131 lines
4.2 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 Phaser from 'phaser'
export const TILE_SIZE = 32
export const OFFICE_COLS = 20
export const OFFICE_ROWS = 25
export const GAP_COLS = 2
export const OFFICE_COUNT = 3
export const WORLD_COLS = OFFICE_COLS * OFFICE_COUNT + GAP_COLS * (OFFICE_COUNT - 1) // 64
export const WORLD_ROWS = OFFICE_ROWS // 25
export const MAP_COLS = WORLD_COLS
export const MAP_ROWS = WORLD_ROWS
export const OUTDOOR_MARGIN_X = TILE_SIZE * 8
export const OUTDOOR_MARGIN_TOP = TILE_SIZE * 4
export const OUTDOOR_MARGIN_BOTTOM = TILE_SIZE * 20
export const CHAR_SCALE = 1.8
export const CHAR_SCALE_X = CHAR_SCALE
export const CHAR_SCALE_Y = CHAR_SCALE
export const CHAR_SHADOW_WIDTH = 30
export const CHAR_SHADOW_HEIGHT = 8
export const CHAR_SHADOW_Y = -2
export const CHAR_SHADOW_ALPHA = 0.22
/** Daytime if local hour is in [DAYTIME_START_HOUR, DAYTIME_END_HOUR] inclusive. */
export const DAYTIME_START_HOUR = 5
/** 23 → day through 23:59; 0:004:59 is night when mode is Auto (no URL/storage override). */
export const DAYTIME_END_HOUR = 23
/** Parse `?day=1` / `#?day=1` / `#/path?day=1` for outdoor preview. */
function readOutdoorOverrideFromUrl(): 'day' | 'night' | null {
if (typeof window === 'undefined') return null
const parse = (raw: string): 'day' | 'night' | null => {
const q = new URLSearchParams(raw)
if (q.get('day') === '1' || q.get('daytime') === '1') return 'day'
if (q.get('night') === '1') return 'night'
return null
}
let o = parse(window.location.search || '')
if (o) return o
const hash = window.location.hash
if (!hash) return null
const qm = hash.indexOf('?')
if (qm >= 0) {
o = parse(hash.slice(qm + 1))
if (o) return o
}
const h = hash.replace(/^#/, '')
if (h.includes('=')) {
o = parse(h)
if (o) return o
}
return null
}
/**
* Day vs night for the outdoor skyline. Clock: local 5:0023:59 = day, 0:004:59 = night (Auto mode).
* Override (browser): URL `?day=1` / `?daytime=1` (also after `#…?`), or `localStorage opc_outdoor_override` = `day`|`night`.
* Legacy: `opc_outdoor_day` / `opc_outdoor_night` = `1`.
*/
export function isLocalDaytime(now = new Date()): boolean {
if (typeof window !== 'undefined') {
try {
const url = readOutdoorOverrideFromUrl()
if (url === 'day') return true
if (url === 'night') return false
const om = window.localStorage?.getItem('opc_outdoor_override')
if (om === 'day') return true
if (om === 'night') return false
if (window.localStorage?.getItem('opc_outdoor_day') === '1') return true
if (window.localStorage?.getItem('opc_outdoor_night') === '1') return false
} catch {
/* private mode / SSR */
}
}
const h = now.getHours()
return h >= DAYTIME_START_HOUR && h <= DAYTIME_END_HOUR
}
/** Phaser camera clear color to match sky / lawn edge. */
export const SCENE_CLEAR_DAY = 0xa8d4ec
export const SCENE_CLEAR_NIGHT = 0x31453a
export const WALK_SPEED_URGENT = 200
export const WALK_SPEED_NORMAL = 100
export const WALK_SPEED_RELAXED = 60
export const WANDER_PAUSE_MIN = 2.0
export const WANDER_PAUSE_MAX = 20.0
export const WANDER_MOVES_BEFORE_REST_MIN = 3
export const WANDER_MOVES_BEFORE_REST_MAX = 6
export const SEAT_REST_MIN = 120.0
export const SEAT_REST_MAX = 240.0
export const CELEBRATE_DURATION = 2.5
export const COFFEE_DURATION_MIN = 8.0
export const COFFEE_DURATION_MAX = 15.0
export const CHAT_DURATION_MIN = 5.0
export const CHAT_DURATION_MAX = 10.0
export const STATUS_BUBBLE_DURATION = 5.0
export const INACTIVE_SEAT_TIMER_MIN = 3.0
export const INACTIVE_SEAT_TIMER_RANGE = 2.0
export function createGameConfig(parent: HTMLElement, width?: number, height?: number): Phaser.Types.Core.GameConfig {
const w = width || parent.clientWidth || window.innerWidth - 380
const h = height || parent.clientHeight || window.innerHeight - 48
const skyHex = isLocalDaytime() ? '#a8d4ec' : '#31453a'
return {
type: Phaser.CANVAS,
parent,
width: w,
height: h,
pixelArt: true,
backgroundColor: skyHex,
physics: {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 0 },
debug: false,
},
},
scale: {
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.NONE,
parent,
},
render: {
antialias: false,
pixelArt: true,
},
}
}