Initial commit: Game Maker phase 1

Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript.

- core: asset model (Scene/Camera/Sprite), scene objects with sparse
  overrides, property schema + resolve, command stack (undo/redo),
  serialization and project store interface (16 unit tests)
- ui: custom in-canvas widget toolkit (panels, fields, color picker,
  list/scroll, popup) with a hidden <input> keyboard bridge
- editor: asset/hierarchy panels, schema-driven inspector, toolbar
- viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan
- runtime: world/entity instantiation for play mode, input, camera follow
- api: PHP JSON project store with revision counter (409 on conflict)
- tools: CDP-based e2e smoke test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 10:07:06 +02:00
commit 5a46124454
65 files changed
+7193

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
import type { Application } from 'pixi.js';
import {
Button,
Checkbox,
ColorField,
Dialog,
HStack,
Label,
ListView,
Menu,
NumberField,
Panel,
ScrollContainer,
TextField,
Theme,
VStack,
toHex,
} from './ui';
/**
* Developer page (`?playground`) that shows every widget of the toolkit for manual
* testing: focus cycling, typing, scrubbing, color picking, scrolling, popups.
*/
export function mountPlayground(app: Application, root: VStack): void {
const status = new Label('Interact with the widgets – events are logged here.', { style: 'muted', padding: Theme.padding });
const log = (message: string) => status.setText(message);
const row = new HStack({ gap: Theme.padding, padding: Theme.padding });
// Column 1: inputs
const inputs = new Panel({ title: 'Inputs' });
inputs.content.add(new Label('Text field'));
inputs.content.add(new TextField('Crate', { placeholder: 'Name', onInput: (v) => log(`text input: ${v}`), onChange: (v) => log(`text change: ${v}`) }));
inputs.content.add(new Label('Number field (drag to scrub)'));
inputs.content.add(new NumberField(100, { min: 1, step: 1, onInput: (v) => log(`number input: ${v}`), onChange: (v) => log(`number change: ${v}`) }));
inputs.content.add(new NumberField(1, { min: 0.1, max: 10, step: 0.1, onChange: (v) => log(`zoom change: ${v}`) }));
inputs.content.add(new Label('Color field'));
inputs.content.add(new ColorField(0xe0553f, { onInput: (c) => log(`color input: ${toHex(c)}`), onChange: (c) => log(`color change: ${toHex(c)}`) }));
const checkRow = new HStack({ gap: Theme.gap, align: 'center' });
const checkbox = new Checkbox(true);
checkbox.onChange = (v) => log(`checkbox: ${v}`);
checkRow.add(checkbox);
checkRow.add(new Label('Checkbox'), 1);
inputs.content.add(checkRow);
row.add(inputs, 1);
// Column 2: buttons + popups
const actions = new Panel({ title: 'Buttons & popups' });
actions.content.add(new Button('Default', { onClick: () => log('default clicked') }));
actions.content.add(new Button('Primary', { variant: 'primary', onClick: () => log('primary clicked') }));
const toggle = new Button('Toggle me');
toggle.onClick = () => { toggle.setToggled(!toggle.toggled); log(`toggled: ${toggle.toggled}`); };
actions.content.add(toggle);
actions.content.add(new Button('Ghost', { variant: 'ghost', onClick: () => log('ghost clicked') }));
const disabled = new Button('Disabled');
disabled.setEnabled(false);
actions.content.add(disabled);
const menuButton = new Button('Open menu…');
menuButton.onClick = () => {
const g = menuButton.getGlobalPosition();
new Menu([
{ label: 'Scene', onSelect: () => log('menu: scene') },
{ label: 'Camera', onSelect: () => log('menu: camera') },
{ label: 'Sprite', onSelect: () => log('menu: sprite') },
{ label: 'Delete', danger: true, onSelect: () => log('menu: delete') },
]).open(g.x, g.y + menuButton.h + 2);
};
actions.content.add(menuButton);
actions.content.add(new Button('Open dialog…', {
onClick: () => new Dialog('Delete asset?', 'Every placed instance is removed as well.', [
{ label: 'Cancel' },
{ label: 'Delete', danger: true, onClick: () => log('dialog: deleted') },
]).openCentered(),
}));
row.add(actions, 1);
// Column 3: list in a scroll container
const listPanel = new Panel({ title: 'List (scroll)', padding: 0 });
const scroll = new ScrollContainer();
const list = new ListView();
list.setItems(Array.from({ length: 40 }, (_, i) => ({
id: `item-${i}`,
label: `Item ${i + 1}${i % 7 === 0 ? ' with a rather long name that must truncate' : ''}`,
hint: i % 3 === 0 ? 'sprite' : i % 3 === 1 ? 'camera' : 'scene',
swatch: i % 3 === 0 ? (0x3fa0e0 + i * 0x040201) & 0xffffff : undefined,
depth: i % 5 === 4 ? 1 : 0,
})));
list.onSelect = (id) => log(`list select: ${id}`);
list.onActivate = (id) => log(`list activate: ${id}`);
list.onContextMenu = (id, x, y) => new Menu([{ label: `Context for ${id}`, onSelect: () => log(`context: ${id}`) }]).open(x, y);
scroll.setContent(list);
listPanel.content.add(scroll, 1);
row.add(listPanel, 1);
root.add(new Label('Game Maker – UI playground', { style: 'bodyBold', padding: Theme.padding }));
root.add(row, 1);
root.add(status);
root.resize(app.screen.width, app.screen.height);
}