import { Text } from 'pixi.js'; import type { World } from '../runtime/World'; import { Theme, textStyle, ui } from '../ui'; import { cameraOfWorld } from '../viewport/activeCamera'; import { routePointer } from '../viewport/gamePointer'; import { collectWorldItems } from '../viewport/items'; import { cameraView, type View } from '../viewport/view'; import { type Frame, type ViewSource, WorldViewport } from '../viewport/WorldViewport'; /** * A released game on screen (#266): the running world through its active camera, at the largest * zoom step that fits (Auto), letterboxed in the page colour. Before the game runs, and after it * stopped itself, it shows a line of text instead – "Click to play", or why there is nothing to play. */ export class GameScreen extends WorldViewport { private readonly caption = new Text({ text: '', style: textStyle('title') }); private readonly hint = new Text({ text: 'No active camera', style: textStyle('muted') }); private running: World | null = null; /** Asked when the caption is clicked while it offers to start. */ onStart: (() => void) | null = null; private startable = false; constructor(source: ViewSource) { super(source); for (const text of [this.caption, this.hint]) { text.eventMode = 'none'; text.anchor.set(0.5); this.addChild(text); } routePointer( this, () => this.running, () => this.currentView(), ); this.on('pointerdown', (e) => { if (e.button === 0 && !this.running && this.startable) this.onStart?.(); }); } /** What the screen says while no game runs; `startable` makes a click on it start one. */ say(text: string, startable = false): void { this.caption.text = text; this.startable = startable; this.cursor = startable ? 'pointer' : 'default'; this.redraw(); } get canStart(): boolean { return this.startable && !this.running; } /** null = not running. */ setWorld(world: World | null): void { this.running = world; this.redraw(); } /** * What the screen shows, for the check scripts: the caption while no game runs, and the camera's * picture as it lies on screen – world units per CSS pixel and the frame in global CSS pixels. */ info(): { caption: string | null; scale: number | null; frame: { x: number; y: number; w: number; h: number } | null; } { const view = this.currentView(); const origin = this.getGlobalPosition(); return { caption: this.running ? null : this.caption.text, scale: view?.scale ?? null, frame: view ? { ...view.rect, x: origin.x + view.rect.x, y: origin.y + view.rect.y } : null, }; } protected override currentView(): View | null { const camera = this.running ? cameraOfWorld(this.running) : null; return camera ? cameraView(camera, this.w, this.h, 'auto', ui().app.renderer.resolution) : null; } protected override collectFrame(): Frame { return this.running ? collectWorldItems(this.running) : { items: [], background: this.baseColor }; } protected override get baseColor(): number { return Theme.colors.background; } protected override beforeItems(view: View | null): void { this.caption.visible = this.running === null; this.caption.position.set(this.w / 2, this.h / 2); this.hint.visible = this.running !== null && view === null; this.hint.position.set(this.w / 2, this.h / 2); } }