Let the example-game smokes read texts in the game again
game-maker / gate (push) Successful in 4m8s
game-maker / suite (push) Successful in 12m10s

Since 89aa92d (#169) a text in the game is drawn as a picture of its
glyphs on a sprite mesh, not as a Pixi `Text`. The test hook
`__gm.locate` only looked for `Text` nodes, so every game text was
invisible to it. The smokes have been red in CI since then (tasks 385
to 395): 5 checks in jumprun, 1 in tetris. The e2e part stayed green,
because it reads editor labels only.

- `textPicture.ts` remembers, per mesh, the text a `TextPicture`
  shows, and hands it out through `pictureText(node)`.
- `textProbe.eachText` reads both kinds and passes the text as a
  string instead of the Pixi node. `locate`, `locateAll` and
  `overlayTexts` use that string.

Tests:
- ./check.sh: 195 files, 2815 tests, CHECK EXIT 0
- smoke-jumprun: ALL PASSED, 24 checks
- smoke-tetris: ALL PASSED, 19 checks
- full e2e run (local): ALL PASSED, 1580 checks, the sum is unchanged
  although `locate` now also sees texts in the scene
- counter-check: CI task 395 with the old code, same checks, 6 red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-29 15:19:42 +02:00
1 parent cab1c0177f
commit dded62b9f9
3 files changed
+26 -8

No files matched your search

+2 -2
View File
@@ -64,8 +64,8 @@ export function uiHooks({ app, state, editor }: HookContext) {
*/
const overlayTexts = (): string[] => {
const out: string[] = [];
eachText(ui().overlay, (node) => {
if (node.text) out.push(node.text);
eachText(ui().overlay, (text) => {
if (text) out.push(text);
});
return out;
};
+12 -6
View File
@@ -1,5 +1,6 @@
import { type Application, type Container, Graphics, Rectangle, Text } from 'pixi.js';
import { Button, Widget } from '../ui';
import { pictureText } from '../viewport/textPicture';
/** The global box of a widget: where it starts, and how large it is. */
export function rectOf(widget: { getGlobalPosition(): { x: number; y: number }; w: number; h: number }): {
@@ -36,7 +37,7 @@ export interface IconHit {
* it the sign-in screen needs, since it exists before there is an editor to ask.
*/
export function textProbe(app: Application): {
eachText: (root: Container, visit: (node: Text, point: { x: number; y: number }) => void) => void;
eachText: (root: Container, visit: (text: string, point: { x: number; y: number }) => void) => void;
locateAll: (text: string) => { x: number; y: number }[];
locate: (text: string, last?: boolean) => { x: number; y: number } | null;
locateIcons: (name: string) => IconHit[];
@@ -76,14 +77,19 @@ export function textProbe(app: Application): {
};
walk(root, null, null);
};
const eachText = (root: Container, visit: (node: Text, point: { x: number; y: number }) => void): void => {
const eachText = (
root: Container,
visit: (text: string, point: { x: number; y: number }) => void,
): void => {
eachShown(root, (node, clip) => {
if (!(node instanceof Text)) return false;
// A text object in the game is a picture of its glyphs (#169), not a Pixi `Text`.
const text = node instanceof Text ? node.text : pictureText(node);
if (text === undefined) return false;
// Global bounds, not `width`/`height`: those are unscaled, and inside the graph canvas
// (which is scaled by the zoom) that put the point right of the text it was meant to hit.
const box = node.getBounds();
const point = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
if (!clip || clip.contains(point.x, point.y)) visit(node, point);
if (!clip || clip.contains(point.x, point.y)) visit(text, point);
return true;
});
};
@@ -105,8 +111,8 @@ export function textProbe(app: Application): {
/** Centres of every visible text equal to `text`, in drawing order (the topmost last). */
const locateAll = (text: string): { x: number; y: number }[] => {
const hits: { x: number; y: number }[] = [];
eachText(app.stage, (node, point) => {
if (node.text === text) hits.push(point);
eachText(app.stage, (shown, point) => {
if (shown === text) hits.push(point);
});
return hits;
};
+12
View File
@@ -70,6 +70,17 @@ export function sharpCoverage(
return { data, width: w, height: h };
}
/**
* The text each picture shows, by its mesh. The mesh is a sprite quad and carries no text, so the test
* hook reads a game text through here – otherwise `locate` finds only the editor's own labels.
*/
const shownTexts = new WeakMap<Mesh, string>();
/** The text a node on screen draws as a `TextPicture`, or undefined for any other node. */
export function pictureText(node: object): string | undefined {
return shownTexts.get(node as Mesh);
}
export class TextPicture {
readonly mesh: Mesh = newSpriteMesh();
private readonly style = new TextStyle({ fontFamily: Theme.font, fontSize: 32, fill: 0xffffff });
@@ -88,6 +99,7 @@ export class TextPicture {
const color = Number(item.props.color ?? 0xffffff);
const resolution = textResolution(scale, item.fontSmooth);
const key = JSON.stringify([text, family, size, color, resolution]);
shownTexts.set(this.mesh, text);
const stale = key !== this.rasterKey;
const previous = stale ? this.texture : null;
if (stale) {