

Shipped a cozy sim on Phaser 3.90: one scene, zero sprite files, and React doing all the UI
I've been building Wild Willows, a nature-restoration sim where you replant a damaged preserve and animals return as the habitat starts supporting them. It's at wildwillows.app, on the Mac App Store and itch, with a free browser demo. A few architecture decisions that mattered more than I expected:
One scene, forever. The scene list is just [WorldScene]. No title scene, no menu scene, no UI scene. Every menu, panel, the journal, crafting, settings, the tutorial: all React DOM on top of the canvas. Phaser draws the world and nothing else.
I expected to regret this and haven't. DOM gives me real text layout, real focus management, and accessibility (five interface fonts, four text sizes, three colorblind modes, fully rebindable keys), none of which I'd have enjoyed building in-canvas.
A 60-line bridge between them. React owns authoritative state. Phaser reads it synchronously off a shared object and re-renders when told the world is dirty. It's a Map of handlers and a shared blob, no observables, no state library spanning both worlds. React to Phaser: world-dirty, enter-placement, area-changed. Phaser to React: collect-node, open-crafting, animal-clicked, place-at.
Zero sprite files. Every sprite is drawn at boot with Phaser.Graphics and generateTexture(). Plants, animals, the player, habitat objects. About 8,600 lines of drawing code and no image assets at all. Shapes are authored in logical 32px pixels and rasterized 4x (2x on low graphics quality), with every sprite scaled back down by 1/4, so they stay crisp under camera zoom and HiDPI.
A frame budget for world rebuilds. Every action used to rebuild the dynamic layer synchronously, so dragging a volume slider tore down and rebuilt the animal layer about 60 times a second for a preference the world doesn't even draw from. Now that work is keyed and coalesced to at most one run per animation frame, each run is timed, and any task consistently overrunning half a frame automatically drops to every Nth frame, then recovers on its own once it's cheap again. A big save degrades to a lower redraw rate instead of locking up.
Smaller stuff: Scale.NONE plus manual zoom so I can render at native device pixels and let CSS scale the canvas back down, and a screen-space RenderTexture used as a bitmap mask to punch light holes in the night tint.
Happy to go into any of it. The two worst bugs were both Phaser internals: TimeStep.resetDelta making the player walk at a fifth speed for ten seconds after every load, and repeat: -1 tweens outliving the sprites they drove.