Problem: there is no 2D path. Verified on main: packages/shell/src/world/GridWorldScene.tsx:33 renders tiles as BoxGeometry instances; packages/core/src/assets/spriteSheet.ts has no consumers; packages/shell/src/world/SpriteBatch.tsx takes a raw frame index; EntitySpriteConfig in packages/core/src/game/playableGame.ts:77-82 is { url, width, height, y }; SideScrollCameraConfig in packages/core/src/game/cameraConfig.ts has no bounds, deadzone, or lookahead (the bounds field at :96 belongs to the RTS rig); no MeshToonMaterial anywhere; grid layer commands exist in packages/core/src/editor/commands.ts:113-116 but no editor panel calls them (only packages/editor/src/handlers/grids.ts). Related art pipeline issues: #1421 to #1425.
Tasks, one PR each, in order. Tasks 2, 3, 5, 6, 7 need before/after shots.
-
Sprite atlas and clips (core, no rendering). New packages/core/src/assets/spriteAtlas.ts: export interface SpriteAtlas { image: string; size: [number, number]; frames: Record<string, { x: number; y: number; w: number; h: number; pivot?: [number, number] }>; animations: Record<string, { frames: string[]; fps: number; loop?: boolean }> }, fromAseprite(json: unknown): SpriteAtlas (Aseprite "hash" and "array" JSON export), fromTexturePacker(json: unknown): SpriteAtlas. New packages/core/src/render/sprite2d.ts: SpriteClipState { animation: string; frameIndex: number; elapsed: number; done: boolean }, createSpriteClipPlayer(atlas) with play(name), advance(dt), frame(), snapshot(), restore(), retune({ speed }); SortingLayers as an ordered readonly string[] with sortingOrder(layers, layer, offset). Tests for both files with a small hand-written atlas. bun run gen. CHANGELOG Added.
-
Sprite clips in the shell. Add clip?: { atlas: SpriteAtlas; animation: string } to EntitySpriteConfig; in SpriteBatch.tsx (and EntitySprite in packages/shell/src/render/SceneModels.tsx) drive the frame UV from a createSpriteClipPlayer advanced in useFrame, keep the raw-frame path when clip is absent. Use nearest filtering when pixelPerfect (task 4) is set. Shots: a two-frame procedurally drawn test atlas (draw it with canvas in a fixture; no external assets) animating on an entity.
-
Textured tile layers. In packages/core/src/world/features.ts extend the tilemap() feature with tileSet?: { atlas: SpriteAtlas; tiles: Record<string, string> } (cell value id → atlas frame id). New packages/shell/src/world/TileLayerRenderer.tsx: one InstancedMesh of unit quads per layer with per-instance UV offsets (copy the attribute approach from SpriteBatch.tsx), renderOrder from the layer's sorting layer, optional parallax: [number, number] scaled against the camera position. GridWorldScene.tsx uses it when tileSet is present and keeps the box path otherwise. Test: instance count and UV offsets for a 3x3 map. Shots.
-
Pixel-perfect orthographic camera. Add pixelPerfect?: { pixelsPerUnit: number; integerScale?: boolean } to the camera projection config in cameraConfig.ts; in the orthographic branch of packages/shell/src/Shell3dPresentation.tsx and the side-scroll rig, set the ortho frustum from viewport size / pixelsPerUnit, choose the largest integer zoom that fits when integerScale, and snap the camera position to the pixel grid each frame. Tests for the pure frustum/zoom math (put it in packages/core/src/game/pixelPerfect.ts). Shots at 1x and 2x.
-
2D framing. Add to SideScrollCameraConfig: bounds?: { minX; maxX; minY; maxY }, deadzone?: { width; height }, lookahead?: { seconds; max }. Implement in the side-scroll rig math (find it under packages/shell/src/camera/; if the math is inline, extract it to packages/core/src/game/sideScrollCamera.ts first so it is testable). Tests: camera stays inside bounds, does not move inside the deadzone, leads a moving target. Shots.
-
2D collision and platformer movement. New packages/core/src/physics/shapes2d.ts: AABB, circle, convex polygon overlap and swept AABB (return toi and normal). New packages/core/src/movement/platformer.ts: createPlatformerController({ width, height, gravity, jumpVelocity, coyoteMs, jumpBufferMs, maxFallSpeed }) with move(state, input, dt, solids: Aabb2[] , oneWay: Aabb2[]), snapshot/restore/retune. Tests: coyote jump after leaving a ledge within the window, buffered jump on landing, one-way platform passable from below. bun run gen.
-
Toon shading. Add shading?: "standard" | "toon" | "unlit" and outline?: { width: number; color: string } to ModelConfig in playableGame.ts. New packages/shell/src/render/toonMaterial.ts: convert a cloned model's standard materials to MeshToonMaterial with a 3-step gradient map, and add an inverted-hull outline mesh (BackSide, scaled by width) when outline is set. Hook it in SceneModels.tsx after cloneModelScene. Shots: a KayKit model toon vs standard.
-
Editor tiles workspace. In packages/editor/src/shell/WorkspaceRail.tsx add a Tiles workspace; new packages/editor/src/TilesPanel.tsx with a palette bound to the layer's tileSet and paint / fill / erase tools that dispatch paintGridCells (see packages/editor/src/handlers/grids.ts); add an orthographic toggle to the editor camera in packages/editor/src/camera/. Shots of the editor.
-
Probe. examples/platformer-2d/: a small side-scroller using tasks 1 to 7 (procedurally drawn atlas, one tile layer with parallax, pixel-perfect camera, platformer controller). Must pass bun run shoot and check-game-shape. Closes #1679.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1679; bun run gen after export changes (JSDoc on every new export; create* factories need snapshot/restore); CHANGELOG bullet; check-types and test on core, shell, editor as touched plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; merge origin/main before pushing; shots via bun run games:clone, bun run shoot daemon start, bun run shoot <game> --mode play --size half --out shots/x.png (for the example use bun run shoot --url with data-jg-capture="ready"), bun run pr-shots --dir <branch> shots/*.png; squash auto-merge; fix CI on the same branch.
Problem: there is no 2D path. Verified on main:
packages/shell/src/world/GridWorldScene.tsx:33renders tiles asBoxGeometryinstances;packages/core/src/assets/spriteSheet.tshas no consumers;packages/shell/src/world/SpriteBatch.tsxtakes a raw frame index;EntitySpriteConfiginpackages/core/src/game/playableGame.ts:77-82is{ url, width, height, y };SideScrollCameraConfiginpackages/core/src/game/cameraConfig.tshas no bounds, deadzone, or lookahead (theboundsfield at:96belongs to the RTS rig); noMeshToonMaterialanywhere; grid layer commands exist inpackages/core/src/editor/commands.ts:113-116but no editor panel calls them (onlypackages/editor/src/handlers/grids.ts). Related art pipeline issues: #1421 to #1425.Tasks, one PR each, in order. Tasks 2, 3, 5, 6, 7 need before/after shots.
Sprite atlas and clips (core, no rendering). New
packages/core/src/assets/spriteAtlas.ts:export interface SpriteAtlas { image: string; size: [number, number]; frames: Record<string, { x: number; y: number; w: number; h: number; pivot?: [number, number] }>; animations: Record<string, { frames: string[]; fps: number; loop?: boolean }> },fromAseprite(json: unknown): SpriteAtlas(Aseprite "hash" and "array" JSON export),fromTexturePacker(json: unknown): SpriteAtlas. Newpackages/core/src/render/sprite2d.ts:SpriteClipState { animation: string; frameIndex: number; elapsed: number; done: boolean },createSpriteClipPlayer(atlas)withplay(name),advance(dt),frame(),snapshot(),restore(),retune({ speed });SortingLayersas an orderedreadonly string[]withsortingOrder(layers, layer, offset). Tests for both files with a small hand-written atlas.bun run gen. CHANGELOG Added.Sprite clips in the shell. Add
clip?: { atlas: SpriteAtlas; animation: string }toEntitySpriteConfig; inSpriteBatch.tsx(andEntitySpriteinpackages/shell/src/render/SceneModels.tsx) drive the frame UV from acreateSpriteClipPlayeradvanced inuseFrame, keep the raw-frame path whenclipis absent. Use nearest filtering whenpixelPerfect(task 4) is set. Shots: a two-frame procedurally drawn test atlas (draw it with canvas in a fixture; no external assets) animating on an entity.Textured tile layers. In
packages/core/src/world/features.tsextend thetilemap()feature withtileSet?: { atlas: SpriteAtlas; tiles: Record<string, string> }(cell value id → atlas frame id). Newpackages/shell/src/world/TileLayerRenderer.tsx: oneInstancedMeshof unit quads per layer with per-instance UV offsets (copy the attribute approach fromSpriteBatch.tsx),renderOrderfrom the layer's sorting layer, optionalparallax: [number, number]scaled against the camera position.GridWorldScene.tsxuses it whentileSetis present and keeps the box path otherwise. Test: instance count and UV offsets for a 3x3 map. Shots.Pixel-perfect orthographic camera. Add
pixelPerfect?: { pixelsPerUnit: number; integerScale?: boolean }to the camera projection config incameraConfig.ts; in the orthographic branch ofpackages/shell/src/Shell3dPresentation.tsxand the side-scroll rig, set the ortho frustum from viewport size / pixelsPerUnit, choose the largest integer zoom that fits whenintegerScale, and snap the camera position to the pixel grid each frame. Tests for the pure frustum/zoom math (put it inpackages/core/src/game/pixelPerfect.ts). Shots at 1x and 2x.2D framing. Add to
SideScrollCameraConfig:bounds?: { minX; maxX; minY; maxY },deadzone?: { width; height },lookahead?: { seconds; max }. Implement in the side-scroll rig math (find it underpackages/shell/src/camera/; if the math is inline, extract it topackages/core/src/game/sideScrollCamera.tsfirst so it is testable). Tests: camera stays inside bounds, does not move inside the deadzone, leads a moving target. Shots.2D collision and platformer movement. New
packages/core/src/physics/shapes2d.ts: AABB, circle, convex polygon overlap and swept AABB (return toi and normal). Newpackages/core/src/movement/platformer.ts:createPlatformerController({ width, height, gravity, jumpVelocity, coyoteMs, jumpBufferMs, maxFallSpeed })withmove(state, input, dt, solids: Aabb2[] , oneWay: Aabb2[]), snapshot/restore/retune. Tests: coyote jump after leaving a ledge within the window, buffered jump on landing, one-way platform passable from below.bun run gen.Toon shading. Add
shading?: "standard" | "toon" | "unlit"andoutline?: { width: number; color: string }toModelConfiginplayableGame.ts. Newpackages/shell/src/render/toonMaterial.ts: convert a cloned model's standard materials toMeshToonMaterialwith a 3-step gradient map, and add an inverted-hull outline mesh (BackSide, scaled by width) whenoutlineis set. Hook it inSceneModels.tsxaftercloneModelScene. Shots: a KayKit model toon vs standard.Editor tiles workspace. In
packages/editor/src/shell/WorkspaceRail.tsxadd a Tiles workspace; newpackages/editor/src/TilesPanel.tsxwith a palette bound to the layer'stileSetand paint / fill / erase tools that dispatchpaintGridCells(seepackages/editor/src/handlers/grids.ts); add an orthographic toggle to the editor camera inpackages/editor/src/camera/. Shots of the editor.Probe.
examples/platformer-2d/: a small side-scroller using tasks 1 to 7 (procedurally drawn atlas, one tile layer with parallax, pixel-perfect camera, platformer controller). Must passbun run shootandcheck-game-shape.Closes #1679.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1679;bun run genafter export changes (JSDoc on every new export;create*factories need snapshot/restore); CHANGELOG bullet;check-typesandteston core, shell, editor as touched pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet; mergeorigin/mainbefore pushing; shots viabun run games:clone,bun run shoot daemon start,bun run shoot <game> --mode play --size half --out shots/x.png(for the example usebun run shoot --urlwithdata-jg-capture="ready"),bun run pr-shots --dir <branch> shots/*.png; squash auto-merge; fix CI on the same branch.