Skip to content

[FEATURE] 2D presentation path: sprite clips, textured tile layers, pixel-perfect camera, 2D framing and collision #1679

Description

@Noisemaker111

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions