From acbb4e9d983f85d37641d12313ef74134d1d10dc Mon Sep 17 00:00:00 2001 From: Errilaz Date: Mon, 24 Aug 2026 15:27:29 +0200 Subject: [PATCH] chore: move project artifacts elsewhere --- .agents/commands/sketch.md | 14 - .agents/plans/gpu.md | 714 ------------------ .agents/plans/levels-and-editor.md | 261 ------- .agents/resources/commitlint.md | 214 ------ .agents/resources/conventional-commit.md | 128 ---- .agents/rules/big-red-dog.md | 7 - .agents/rules/caveman.md | 9 - .agents/rules/commits.md | 11 - .agents/rules/quality.md | 34 - .agents/skills/caveman/README.md | 48 -- .agents/skills/caveman/SKILL.md | 78 -- .agents/skills/domain-modeling/ADR-FORMAT.md | 47 -- .../skills/domain-modeling/CONTEXT-FORMAT.md | 60 -- .agents/skills/domain-modeling/SKILL.md | 74 -- .../skills/domain-modeling/agents/openai.yaml | 3 - .agents/skills/forge-sync/SKILL.md | 94 --- .agents/skills/grill-with-docs/SKILL.md | 7 - .../skills/grill-with-docs/agents/openai.yaml | 5 - .agents/skills/grilling/SKILL.md | 12 - .agents/skills/grilling/agents/openai.yaml | 3 - .agents/skills/jerklint/SKILL.md | 119 --- .gitignore | 7 +- AGENTS.md | 334 -------- CONTEXT.md | 29 - LICENSE | 7 + docs/adr/0001-engine-owns-world-concepts.md | 3 - opencode.json | 24 - 27 files changed, 13 insertions(+), 2333 deletions(-) delete mode 100644 .agents/commands/sketch.md delete mode 100644 .agents/plans/gpu.md delete mode 100644 .agents/plans/levels-and-editor.md delete mode 100644 .agents/resources/commitlint.md delete mode 100644 .agents/resources/conventional-commit.md delete mode 100644 .agents/rules/big-red-dog.md delete mode 100644 .agents/rules/caveman.md delete mode 100644 .agents/rules/commits.md delete mode 100644 .agents/rules/quality.md delete mode 100644 .agents/skills/caveman/README.md delete mode 100644 .agents/skills/caveman/SKILL.md delete mode 100644 .agents/skills/domain-modeling/ADR-FORMAT.md delete mode 100644 .agents/skills/domain-modeling/CONTEXT-FORMAT.md delete mode 100644 .agents/skills/domain-modeling/SKILL.md delete mode 100644 .agents/skills/domain-modeling/agents/openai.yaml delete mode 100644 .agents/skills/forge-sync/SKILL.md delete mode 100644 .agents/skills/grill-with-docs/SKILL.md delete mode 100644 .agents/skills/grill-with-docs/agents/openai.yaml delete mode 100644 .agents/skills/grilling/SKILL.md delete mode 100644 .agents/skills/grilling/agents/openai.yaml delete mode 100644 .agents/skills/jerklint/SKILL.md delete mode 100644 AGENTS.md delete mode 100644 CONTEXT.md create mode 100644 LICENSE delete mode 100644 docs/adr/0001-engine-owns-world-concepts.md delete mode 100644 opencode.json diff --git a/.agents/commands/sketch.md b/.agents/commands/sketch.md deleted file mode 100644 index bde484d..0000000 --- a/.agents/commands/sketch.md +++ /dev/null @@ -1,14 +0,0 @@ -Execute an approved OpenSpec change. - -Arguments: -- `$1` = OpenSpec change id. -- `--sync` = use Forgejo sync mode. -- `--status` = summarize state only; do not edit files. - -Load and follow the OpenSpec apply workflow and the `forge-sync` skill. - -Rules: -- Treat mode as explicit. Do not ask whether git workflow should be agent-managed. -- Apply tasks from `openspec/changes//tasks.md`. -- After each completed numbered task section, run a `forge-sync` checkpoint. -- Do not archive the OpenSpec change. diff --git a/.agents/plans/gpu.md b/.agents/plans/gpu.md deleted file mode 100644 index 1cc2ec4..0000000 --- a/.agents/plans/gpu.md +++ /dev/null @@ -1,714 +0,0 @@ -# GPU renderer migration direction note - -Status: **options open; migration seam agreed.** This note records the current -problem, viable approaches, expected scaling, proposed architecture, migration -order, and decisions still needed. It is not approval to delete the software -renderer or to commit to WebGL2, WebGPU rasterization, or WebGPU compute. - -## Goal - -Move triangle transformation and pixel rasterization off the CPU so adding more -world geometry and moving actors does not collapse frame rate. - -The GPU path must preserve the configurable PS1 look rather than replace it with -a fixed visual preset. `RenderConfig` remains the source of live settings for -internal resolution, vertex snap, texture filtering, lighting, fog, color depth, -dither, and LOD distance. - -The current software renderer remains playable during migration. A GPU backend -is added beside it and selected at startup or by a debug option until parity and -performance are proven. - -## Decisions already made - -- Build a parallel backend instead of replacing the software renderer in place. -- Keep engine world/content boundaries unchanged. GPU work must not reintroduce - content kinds, registries, or renderer knowledge of frogs, trees, texture names, - or other game content. -- Keep `RenderScene` as the clone-safe, content-agnostic render projection of a - live `Level`. -- Keep main-thread frustum culling and scene-local render prototype indexes at - first. GPU culling is not required to obtain the expected performance gain. -- Optimize for scaling as content grows, not merely for improving the current - benchmark by a small constant factor. -- Preserve low-resolution rendering and the live PS1 controls. - -## Decisions not made - -- WebGPU versus WebGL2 as the first GPU backend. -- Fixed-function GPU rasterization versus a custom WebGPU compute rasterizer. -- Whether WebGL2 and WebGPU should both exist long-term. -- Whether the software renderer remains a permanent fallback after a GPU backend - becomes default. -- Whether GPU output must be pixel-identical to software output or only visually - equivalent under the same `RenderConfig`. -- Required browser, OS, and device support. -- Whether GPU timing statistics are required for the first usable backend. - -## Current baseline - -Current final browser benchmark at 384x216: - -| Backend | Work median | Work p95 | Frame p95 | -| --- | ---: | ---: | ---: | -| Software, single thread | 30.07 ms | 32.19 ms | 32.60 ms | -| Software, 3 workers | 11.16 ms | 13.35 ms | 16.88 ms | - -These numbers are machine- and scene-specific. They are useful as the baseline -for this repository, not as universal predictions. - -The software cost grows with transformed triangles, covered pixels, and texture -samples. Workers divide framebuffer rows, but every worker still loops over the -same visible draw groups and transforms the same triangles for its band. More -content therefore keeps increasing CPU work even when screen resolution stays -fixed. - -## Remaining implementation options - -### Option A: standard WebGPU rasterization - -Use WebGPU render pipelines, vertex/index buffers, textures, samplers, a depth -texture, actor instancing, and WGSL shaders. Recreate PS1 traits in shaders and a -post-process pass. - -Strengths: - -- Uses hardware raster units for the work they are designed to perform. -- Best route to large geometry and actor-count headroom. -- Explicit resource and command model fits immutable compiled `RenderScene` data. -- Storage buffers and compute remain available later without changing API. -- Device-loss handling can fall back to the software backend. - -Costs: - -- More setup and resource-lifecycle code than WebGL2. -- Uniform/storage alignment and bind-group layout require care. -- Texture upload row pitch must satisfy WebGPU alignment requirements. -- Browser/device support must be checked against actual release targets. -- GPU work timing needs optional timestamp queries or coarse CPU submission timing. - -Expected scaling: - -Current geometry should normally fit in low single-digit GPU milliseconds on a -reasonable desktop GPU. At this low internal resolution, 50k triangles is small -for fixed-function GPU rasterization. Hundreds of thousands of visible triangles -should remain practical if draw count, state changes, and overdraw stay controlled. -These are estimates, not acceptance results. - -### Option B: standard WebGL2 rasterization - -Use a WebGL2 context, VAOs, vertex/index buffers, textures, samplers, an offscreen -framebuffer, a depth attachment, GLSL ES 3 shaders, and instanced actor draws. - -Strengths: - -- Simpler first standard-raster backend. -- Broad and mature browser implementation history. -- All current PS1 effects can be implemented without compute shaders. -- Hardware triangle throughput should be enough for this engine by a wide margin. -- Easier shader/program debugging in many browser tools. - -Costs: - -- More implicit global state than WebGPU. -- Resource binding and synchronization are less explicit. -- No general compute path if exact software rasterization becomes necessary. -- Large dynamic instance data is less flexible than WebGPU storage buffers. -- A later WebGPU backend would duplicate substantial platform code. - -Expected scaling: - -For this renderer, standard WebGL2 and standard WebGPU should have similar basic -triangle throughput because both use hardware rasterization. JavaScript draw-call -overhead, material changes, and actor submission are more likely to distinguish -them than raw fill rate at 384x216. - -### Option C: WebGPU compute software rasterizer - -Port the custom rasterizer to WGSL compute rather than using render pipelines. -This is the path with the best chance of preserving exact custom edge, depth, and -pixel rules, but it does not automatically scale like hardware rasterization. - -A serious implementation needs multiple stages: - -1. Transform vertices and clip triangles. -2. Compute screen bounds and assign triangles to 8x8 or 16x16 pixel tiles. -3. Store per-tile triangle lists with overflow handling. -4. Rasterize each tile in a workgroup, using workgroup memory where useful. -5. Resolve depth, alpha cutout, texture sampling, lighting, and color. -6. Run color quantization/dither and copy to the canvas. - -Expected speed: - -A properly tiled compute renderer could plausibly reduce the current scene to -roughly 1-5 ms on a decent desktop GPU. A naive port can be slower than the current -worker renderer. Exact performance depends on tile occupancy, atomics, overdraw, -texture access, and hardware. - -Scaling limitations: - -- Vertex transformation and binning still grow with triangle count. -- Dense tiles grow with local triangle count and overdraw. -- Triangle-parallel writes contend on depth and color. -- Pixel-parallel loops over every triangle are catastrophically expensive. -- Correct alpha-cutout depth ordering complicates packed atomic updates. -- Reproducing deterministic software ordering can serialize work. - -Use this option when exact software-raster behavior is more important than maximum -content headroom. It is not the recommended first answer to the current scaling -problem, but it remains open. - -### Option D: CPU rasterizer with GPU presentation - -Keep software rendering and upload the completed CPU framebuffer into a GPU texture -for presentation. - -Strengths: - -- Small migration step. -- Preserves exact current pixels. -- Can establish canvas, texture-upload, scaling, and backend-selection plumbing. -- May simplify or replace Canvas2D presentation. - -Limitations: - -- Does not move triangle or pixel work off the CPU. -- Does not solve frame-rate collapse as content grows. -- Adds an upload every frame. - -This remains useful as an intermediate bridge or diagnostic backend, not as the -destination for the stated performance goal. - -### Option E: both WebGPU and WebGL2 - -Implement a common backend contract, then supply WebGPU, WebGL2, and software -implementations. - -Strengths: - -- Best runtime coverage while retaining modern WebGPU capabilities. -- Allows direct performance comparison on the same machine and scene. -- Software remains the reference renderer. - -Costs: - -- Three renderers, shader languages, resource systems, and failure paths. -- PS1 feature fixes must be maintained in multiple implementations. -- Highest test and debugging burden. - -Do not start both GPU backends simultaneously unless supported-browser requirements -make that necessary. The common seam should permit a second backend without making -it mandatory. - -## Current architecture that should survive - -No game-content rewrite is needed. - -- `engine/world/Level.ts` owns live actors, collision, terrain, and render projection. -- `engine/render/RenderScene.ts` owns static groups, chunks, billboards, prototypes, - sky configuration, culling, and scene-local instance data. -- `engine/render/Chunk.ts` owns LOD choice. -- `Mesh` already provides flat position/UV vertices and indexed triangles. -- `Material` already binds a concrete `Texture` and cull setting. -- `ChunkBuilder` already batches static geometry by material object. -- Actor definitions already expose material-bound render prototypes. -- Main-thread frustum culling already prevents invisible chunks and actors from - reaching the renderer. - -GPU compilation should consume those objects directly. Backend-local maps may -deduplicate resources by `Mesh`, `Texture`, `Material`, or `RenderPrototype` object -identity. Such maps are compiled-resource caches, not semantic content registries: -they have no content names, stable IDs, or dispatch behavior. - -## Proposed backend seam - -Current `app/renderer.ts` exposes software-specific `fb` and `parallel` fields. -Presentation also lives partly in `app/main.ts`. A GPU backend needs a backend-neutral -surface. - -Candidate frame input: - -```ts -export type RenderFrame = { - camera: Camera - viewProjection: Mat4 - visibleChunks: readonly number[] - instances: readonly RenderInstance[] - time: number -} -``` - -Candidate renderer contract: - -```ts -export type Renderer = { - readonly width: number - readonly height: number - readonly backend: "software" | "webgl2" | "webgpu" - reconfigure: (config: RenderConfig) => void - dispatch: (frame: RenderFrame) => void - done: () => boolean - present: () => void - workMs: () => number | null - dispose: () => void -} -``` - -This is a candidate, not a final API. Important properties: - -- Main loop no longer reads a software `Framebuffer` to discover dimensions. -- Backend owns presentation. Software calls `putImageData`; WebGL/WebGPU finish a - pass targeting the canvas. -- Software `done()` polls workers; standard GPU backends can initially report true - after command submission. -- `workMs()` may be unavailable until GPU timing support exists. -- Renderer owns teardown for workers, buffers, textures, and device/context loss. -- Scene replacement or level reload recompiles backend resources explicitly. - -## Module ownership options - -Core engine must remain DOM-free and usable by the server. - -Recommended initial split: - -- `engine/render/RenderScene.ts` keeps backend-neutral scene and frame contracts. -- `app/renderers/SoftwareRenderer.ts` wraps the current worker renderer. -- `app/renderers/WebGpuRenderer.ts` or `app/renderers/WebGlRenderer.ts` owns browser - API calls and canvas contexts. -- `app/main.ts` selects a backend and uses only the common renderer contract. - -If a GPU backend grows too large for `app/`, another viable option is a separate -platform project such as `renderers/webgpu/` that depends on `engine/` but is not -part of DOM-free engine core. Do not add DOM/WebGPU globals to `tsconfig.engine.json` -merely to make placement convenient. - -## Standard GPU resource compilation - -Compile resources once per `RenderScene` or level load. - -### Meshes - -- Convert `Mesh.verts` into one `Float32Array` with stride 5: x, y, z, u, v. -- Convert `Mesh.indices` into `Uint16Array` when safe or `Uint32Array` otherwise. -- Create one backend mesh resource per distinct `Mesh` object. -- Preserve separate chunk meshes; merging all chunks would defeat CPU culling. -- Release resources when replacing the scene. - -### Textures - -- Upload existing packed RGBA bytes through a `Uint8Array` view. -- Keep repeat wrapping. -- Create nearest and linear samplers without mipmaps. -- Handle WebGPU `bytesPerRow` alignment with padded staging rows when needed. -- Deduplicate by `Texture` object identity. - -### Materials - -- Bind texture resources by `Material.texture` object identity. -- Keep culling as finite renderer capability, not content identity. -- Standard GPU paths will probably need two world pipeline variants: culled and - double-sided. -- Avoid one shader program or pipeline per game material. - -### Prototypes and instances - -- Compile each `RenderPrototype` once. -- Group visible instances by prototype each frame. -- Upload transform data once per frame. -- Draw repeated actors with instancing rather than one command per actor. -- Keep prototype indexes local to the compiled scene. - -### Billboards - -- Replace per-frame CPU quad construction with one static unit quad. -- Supply position/size as instance data. -- Derive camera-right direction in the vertex shader so billboards stay upright. - -## Standard GPU frame structure - -### Pass 1: scene color and depth - -Render at `RenderConfig.internalWidth` x `RenderConfig.internalHeight` into an -offscreen RGBA color texture plus depth attachment. - -Suggested order: - -1. Draw full-screen sky with depth writes disabled. -2. Draw static groups. -3. Draw visible chunks using CPU-selected near/far groups. -4. Draw billboards with alpha cutout and no culling. -5. Draw actor prototype instances. - -### Pass 2: quantization and presentation - -Sample the offscreen color texture and apply color-depth quantization plus Bayer -dither in a full-screen pass. Write to the canvas texture. Keep CSS integer scaling -and `image-rendering` behavior where useful. - -This two-pass shape matches current behavior better than quantizing each material -fragment independently because software quantization runs after the complete frame, -including sky. - -## Mapping current raster features to shaders - -| Current feature | Standard GPU implementation | Parity risk | -| --- | --- | --- | -| Perspective-correct UV | Native interpolation | Low; this is already desired behavior | -| Internal low resolution | Offscreen render target at configured size | Low | -| Nearest/linear texture filter | Select sampler from `RenderConfig` | Low | -| No mipmaps | Allocate only base level and use non-mipmap sampler | Low | -| Vertex snap | Snap projected screen coordinates in vertex shader, then rebuild clip xy | Medium; edge rules differ | -| Flat lighting | Fragment derivatives of world/view position, or baked face normals | Medium | -| Distance fog | Fragment shader using view-space distance | Low | -| Alpha cutout | Fragment `discard` below alpha threshold | Low | -| Backface culling | Culled and double-sided pipeline variants | Low; front-face sign must be checked | -| Color depth | Full-screen post-process quantization | Low | -| Bayer dither | Full-screen integer pixel-coordinate lookup | Low | -| 1/w depth behavior | Native perspective depth or explicit fragment depth | Medium; exact values differ | -| Near clipping | Native clipping | Low visually, not pixel-identical | -| Procedural sky | Full-screen sky shader port | Medium; noise parity must be tested | -| Chunk frustum culling | Keep existing CPU `RenderScene.visibleChunks` | Low | -| Actor culling | Keep existing `Level.visibleInstances` | Low | -| Chunk LOD | Keep existing CPU `Chunk.isFar` selection | Low | - -Vertex snap sketch: - -```text -clip = projection * view * model * position -ndc = clip.xy / clip.w -pixel = (ndc * 0.5 + 0.5) * internalResolution -pixel = round(pixel / snap) * snap -clip.xy = ((pixel / internalResolution) * 2.0 - 1.0) * clip.w -``` - -Y orientation and half-pixel conventions differ between APIs and need screenshot -tests. Do not guess front-face or snap signs from the software renderer. - -## Flat-lighting options - -### Fragment derivatives - -Calculate a face normal from `cross(dpdx(position), dpdy(position))` in the fragment -shader. This preserves flat faceted lighting without changing mesh data. - -Pros: - -- Smallest geometry change. -- Naturally one normal per rasterized face. -- Available in WebGL2 fragment shaders and WGSL fragment shaders. - -Cons: - -- Exact shade may differ from the CPU cross-product calculation. -- Orientation and two-sided handling need validation. - -### Baked face normals - -Duplicate shared vertices per triangle and store one face normal per vertex. - -Pros: - -- Explicit and predictable. -- Can reproduce CPU directional-light math closely. - -Cons: - -- Increases vertex memory. -- Requires mesh compilation to expand indexed geometry. -- Changes cache behavior. - -Both options remain open. Derivatives are the recommended first implementation. - -## WebGPU-specific notes - -- Request adapter/device once and handle failed acquisition cleanly. -- Configure the canvas using the preferred canvas format. -- Use an offscreen `rgba8unorm` scene texture and `depth24plus` initially. -- Keep frame uniforms in a uniform buffer. -- Use dynamic uniform offsets, instance vertex attributes, or storage buffers for - model transforms. Do not repeatedly overwrite one model uniform before submit. -- Respect 256-byte dynamic-uniform alignment. -- Group actors by prototype and use `instance_index` for transform lookup. -- Keep bind-group layouts stable across materials. -- Separate frame data, texture views, and samplers so changing texture filtering - does not force rebuilding every texture resource. -- Listen for device loss and switch to software rather than leaving a dead canvas. -- Treat timestamp queries as optional capability. -- Recreate size-dependent color/depth textures after `RenderConfig` resolution - changes. - -## WebGL2-specific notes - -- Request `webgl2` with alpha and antialias settings chosen explicitly. -- Use one VAO per compiled mesh. -- Use an FBO with RGBA color texture and depth renderbuffer/texture. -- Use GLSL ES 3 vertex/fragment programs for world, billboard, sky, and post passes. -- Use `drawElementsInstanced` and per-instance matrix attributes for actors. -- Use `dFdx`/`dFdy` fragment derivatives for flat lighting if selected. -- Use `EXT_disjoint_timer_query_webgl2` only when available. -- Handle context loss/restoration by releasing and recompiling scene resources. -- Reset or centralize state changes; implicit stale state is a major WebGL failure - mode. - -## WebGPU compute-raster details - -Avoid these naive designs: - -- One compute invocation per pixel looping over every triangle. -- One invocation per triangle writing non-atomic color and depth. -- A global atomic lock per pixel around full shading. -- Unbounded fixed-size tile lists with silent overflow. -- Recreating CPU row-band splitting on the GPU. - -Open design questions for compute: - -- Tile size and maximum triangle references per tile. -- Multi-pass prefix sums versus fixed tile capacity. -- Depth encoding suitable for atomic comparison. -- How alpha-cutout fragments update depth and color consistently. -- Whether exact triangle order is required for equal-depth fragments. -- Whether perspective-correct UV and texture filtering use native sampling. -- How clipping-generated triangles enter tile lists. -- Whether post-process quantization remains a separate render pass. - -Do not implement compute first unless pixel parity is declared a hard requirement -after testing a standard raster prototype. - -## Draw-call and batching strategy - -Current chunks are already grouped by material, which is suitable for GPU upload. -Culling should remain more important than globally merging geometry. - -Initial strategy: - -- Draw each visible chunk group separately. -- Skip invisible chunks on CPU. -- Select near/far groups on CPU. -- Sort or bucket visible groups by cull pipeline and material only if profiling - shows draw-call overhead matters. -- Instance actors by prototype. -- Use one unit-quad mesh for billboards. - -Possible later optimizations: - -- Merge neighboring chunk groups only if visibility remains acceptably granular. -- Multi-draw or indirect draws where API support and complexity justify them. -- GPU frustum/occlusion culling only after CPU submission becomes measured cost. -- Texture atlases or arrays only after material binding becomes measured cost. - -Do not add batching abstractions before a profile identifies the bottleneck. - -## Performance expectations and growth testing - -Standard GPU rasterization is recommended for the content-scaling goal because -hardware raster units remove the CPU per-triangle/per-pixel loop. However, GPU -rendering can still lose frame rate through excessive draws, overdraw, huge texture -uploads, per-frame allocation, or synchronization. - -Benchmark growth levels, not only the current scene: - -| Scenario | Purpose | -| --- | --- | -| 1x current content | Parity and baseline | -| 2x static props | Early scaling slope | -| 5x static props | Dense-world target | -| 10x static props | Stress and culling behavior | -| 2x/5x moving actors | Instance upload and draw scaling | -| Worst forest camera | Fill, overdraw, and visible draw count | -| Empty clearing | Fixed per-frame overhead | - -Record: - -- CPU simulation time. -- CPU render submission time. -- GPU time when available. -- Median, p95, and max frame time. -- Visible triangles, draw count, and visible actor count. -- GPU memory and scene compilation time where practical. - -Candidate budget, not yet agreed: - -- Keep renderer p95 below roughly 10 ms at the chosen content target, leaving - room inside a 16.67 ms frame for simulation, input, browser, and presentation. -- Demonstrate a substantially flatter frame-time slope than software at 2x and 5x - content. - -## Migration sequence - -### Phase 1: isolate backend contract - -1. Introduce `RenderFrame` and backend-neutral renderer dimensions/lifecycle. -2. Wrap current software renderer without changing output. -3. Move Canvas2D presentation behind the software backend. -4. Keep `?bench=st|mt` working. -5. Add explicit backend query selection such as `?renderer=software|webgl2|webgpu`. - -Acceptance: - -- Software screenshots and benchmark remain unchanged. -- Preset switching, resize, worker fallback, and presentation still work. - -### Phase 2: GPU scene compiler - -1. Traverse `RenderScene` once. -2. Deduplicate meshes, textures, materials, and prototypes by object identity. -3. Upload immutable resources. -4. Record compile time and resource counts. -5. Add disposal and level-reload behavior. - -Acceptance: - -- Compiling a scene does not mutate engine scene data. -- No content names or registries enter backend code. -- Repeated object references produce one GPU resource. - -### Phase 3: minimum standard-raster world - -1. Render static groups and visible chunk groups. -2. Add depth testing and material culling. -3. Add texture sampling. -4. Keep CPU culling and LOD. -5. Use a simple clear color before sky parity exists. - -Acceptance: - -- Geometry, UVs, winding, depth, and near/far LOD are visibly correct. -- Current scene already shows major software-to-GPU work-time reduction. - -### Phase 4: actors and billboards - -1. Compile render prototypes. -2. Group visible actors by prototype. -3. Upload per-frame transforms. -4. Draw actors with instancing. -5. Draw billboards from a shared unit quad. -6. Add alpha cutout. - -Acceptance: - -- No actor content dispatch exists in backend. -- Worker scene-local prototype semantics remain backend-neutral. -- Dynamic actor count scaling is measured. - -### Phase 5: PS1 look parity - -1. Implement vertex snap. -2. Implement nearest/linear sampler selection. -3. Implement flat lighting. -4. Implement fog. -5. Implement color-depth quantization and Bayer dither in post-processing. -6. Validate `standard`, `soft`, `clean`, and `ps1` presets. - -Acceptance: - -- Every current `RenderConfig` knob works live. -- No PS1 trait is silently hardcoded into GPU shaders. - -### Phase 6: sky parity - -1. Port gradient and sun. -2. Port skybox sampling. -3. Port basic cumulus. -4. Port fancy cumulus. -5. Preserve reduced cloud sampling where still useful. - -Acceptance: - -- Sky animation and panorama orientation match visually. -- Cloud cost is profiled separately at each internal resolution. - -### Phase 7: reliability and selection - -1. Handle WebGPU device loss or WebGL context loss. -2. Fall back to software when GPU initialization fails. -3. Verify preset switching and resize during active rendering. -4. Add screenshot comparison and growth benchmarks. -5. Decide default backend only from support and measured results. - -## Validation plan - -### Visual scenes - -Capture stable views that expose different failure modes: - -- Room edge crossing near plane. -- Terrain horizon with fog. -- Dense trees and boulders at LOD transition. -- Flowers and sprite alpha cutout. -- Frog/bee/robin close and far. -- Skybox poles and seam. -- Basic and fancy clouds. -- Backface-culling winding checks. - -### Comparison policy - -If standard GPU rasterization is selected, require visual equivalence rather than -bit-identical frames unless exact parity is later declared. Native clipping, depth, -edge fill, derivatives, and floating-point behavior will differ. - -If compute rasterization is selected specifically for exactness, define which -parts must match bit-for-bit before implementation. "Exact" cannot remain vague. - -### Automated checks - -- GPU scene compiler imports no game modules. -- Renderer backends import no concrete game modules. -- GPU compilation creates no stable content IDs. -- Software tests continue passing. -- Shader compilation errors fail backend initialization clearly. -- Scene replacement releases old resources. -- Config switch and resize recreate only size/config-dependent resources. -- Device/context loss reaches a working fallback. - -## Backend choice guide - -Choose standard WebGPU first when: - -- Target browsers/devices support WebGPU adequately. -- Future compute or storage-buffer work matters. -- Explicit resource management is worth the larger initial implementation. -- Software fallback is acceptable where WebGPU is absent. - -Choose WebGL2 first when: - -- Broad browser/device coverage is the priority. -- Fastest path to hardware rasterization is the priority. -- Current feature set is enough and compute is not required. -- Maintaining software fallback covers correctness/reference needs. - -Choose WebGPU compute first only when: - -- Exact custom raster behavior is a hard product requirement. -- Standard GPU raster tests prove shader emulation insufficient. -- The project accepts a tile-binning/atomic rasterizer as a major subsystem. -- WebGPU-only support is acceptable. - -Choose both standard GPU APIs only when: - -- Browser support requirements cannot be met by one GPU API plus software fallback. -- The maintenance cost is explicitly accepted. - -## Current recommendation - -Start with the backend seam, then prototype **standard WebGPU rasterization** while -keeping software fully operational. Standard WebGL2 remains a valid first backend -if target-browser research favors it. Measure one textured static world pass before -porting every visual feature. - -Do not begin with compute unless exact edge/depth behavior is declared mandatory. -For the stated problem, standard hardware rasterization offers the strongest chance -of adding substantially more content without frame time growing like the current -CPU rasterizer. - -## Questions required before implementation - -1. Which browsers, OS versions, and device classes must run the GPU path? -2. Is software-renderer pixel identity required, or is visual equivalence enough? -3. What content-growth target must hold 60 fps: 2x, 5x, 10x, or a concrete level? -4. Is the software backend a permanent supported mode or only migration/reference? -5. Is GPU timing required in the HUD for the first version? -6. Should backend selection be automatic, query-driven, or user-configurable? -7. Is WebGPU compute still desired after a standard-raster visual prototype exists? - -Until these are answered, keep all options above open and avoid API choices that -make WebGL2, WebGPU rasterization, or WebGPU compute unnecessarily impossible. diff --git a/.agents/plans/levels-and-editor.md b/.agents/plans/levels-and-editor.md deleted file mode 100644 index 05acc47..0000000 --- a/.agents/plans/levels-and-editor.md +++ /dev/null @@ -1,261 +0,0 @@ -# Levels & the in-game editor — direction note - -Status: **historical proposal, partially superseded.** Captures a brainstorm so the next -session starts from the conclusion, not a cold read. Nothing here is committed -code; it's the shape we want and *why*. - -> **Architecture note (2026-08-20):** Naming and ownership sections below are -> superseded by `docs/adr/0001-engine-owns-world-concepts.md` and `CONTEXT.md`. -> Canonical terms are now engine `LevelDefinition` (live-level creation input), -> engine `Level` (live runtime), and clone-safe engine `RenderScene`; game owns -> concrete definitions and values. A future serializable editor document needs its -> own name rather than overloading these runtime terms. References below to game-owned world contracts, content -> `kind` fields/registries, `game/Terrain.ts`, or `game/renderScene.ts` describe the -> old code only. Editor goals remain valid but must use the canonical boundary. - -## The pivot - -The game is heading toward a **multiplayer twitch shooter**. Each match loads a -**fresh, isolated level**. Consequences that shape everything below: - -- **Maps are hand-made, not procedural.** Procedural scatter is demoted to a - *tool* (a starting-block generator), never the runtime map path. This is the - whole reason the editor matters — the editor *is* the map pipeline. -- **Real authoritative server.** The server bakes the same map the clients do and - runs the authoritative sim. -- **No in-world level switching.** You don't walk through a portal to another - world; the match lifecycle loads a level at start. → **portals / transition - triggers are explicitly not needed.** -- **Persistence and player carry-over are YAGNI** for now — but must not be walled - off (see hedges). - -## Historical naming proposal: `Level` (data) vs `Scene` (runtime) - -Agreed vocabulary — reads as **"bake a `Level` into a `Scene`."** Fits the -type + namespace convention. - -- **`Level`** = the authored, serializable **data document** (what an earlier draft - called `LevelSpec`). Pure plain data. The editor's output; the network's payload; - the server's and client's input. -- **`Scene`** = the baked **runtime instance** — the assembled world you render and - simulate. - -**Repurposing note (so nobody trips):** today the names mean something narrower — -`Level` (`game/level.ts:69`) is a *bake output* and `Scene` (`renderScene.ts:19`) -is a *render-only, worker-cloneable subset*. Under the new vocabulary: - -- The name **`Level` is freed** for the authored data document. -- **`Scene` grows** to be the whole runtime instance — it **absorbs** the colliders - + live mob array + terrain sampler that currently hang off `Level`. The thing - **cloned to workers stays a render-subset projection** of the Scene (no colliders, - no live mobs — those are main-thread only), exactly as the current `Scene` already - is. This collapses today's incidental `Level`/`Scene` split (main.ts copies - fields across) into two clear things. - -So the pipeline is: - -``` -generator(params) → Level (data) → bake(level, textures) → Scene (runtime) - (a TOOL: script / editor / never runtime) (the runtime path) -``` - -(`bake` is today's `buildLevel`, renamed and re-typed to take a `Level` document. -Namespace home — `Scene.from(level)` vs `Level.bake()` — is an open detail.) - -## The spine: the `Level` document - -One serializable, explicit map document is the center of gravity. Four consumers, -one artifact: - -- **Editor** *writes* it. -- **Server** *reads* it — bakes headless, runs authoritative collision/sim. -- **Client** *reads* it — bakes the identical `Scene` to render + predict. -- **Network** ships it (or its id) at match start. - -Because the bake is deterministic, server and every client bake **byte-identical -geometry and collision** from the same `Level` — so only the document (or its id) -crosses the wire, never geometry. - -### The one real refactor - -Today the map *definition* is module-level constants welded **inside** -`buildLevel()`. Split generation from baking: the generator *produces* an explicit -`Level`; `bake` consumes it. `bake` should treat the **explicit** document (a list -of placements) as canonical — the generator emits that form. This one cut serves -three futures at once: multiplayer sync, the editor, and (later) persistence. - -## Build order - -1. **`Level` document + `bake(level)` + the gen/bake split.** Pure `game/`, - headless, unit-testable, **no UI, no netcode**. The whole foundation — do it - first, on its own. -2. **Then, in parallel — both ride the document, neither blocks the other:** - - **Editor** — in-engine, browser, `app/`. Emits a `Level` file. - - **Server** — real authoritative, imports `game/`, bakes the `Level` headless. - -## The editor shape - -- **In-game, single-player, edit↔play toggle.** Same running client: an edit mode - to author the `Level` live, and a button that flips to **play mode** and drops you - into the environment you're building. "Everything works except multiplayer" — - full local sim (player, mobs, collision, later weapons) runs in play mode; no - server, no netcode in the editor. -- The play toggle is a **re-bake + mode switch** — a loading beat, not a hot path. - So it may freely re-bake and even reuse the existing worker respawn/`reconfigure` - path; there is **no match, so no live-worker-resync problem** (the one genuinely - hard sub-problem, thereby dodged). -- The engine already provides the hard parts: live first-person view, deterministic - bake, and `camera` + `Terrain.height` for "where's the ground under the cursor" - (raycast/picking). The remaining weight is **UI/interaction**. AGENTS.md notes - "no DOM-built UI yet (deliberate)" — the editor is where that ends. - -## Editor features (the brainstorm) - -Three feature buckets. For each: what's already there, the real new work, the catch. - -### 1. Sidebar tree — level config + contents - -A **property inspector over the `Level` document.** If the document is clean data, -the tree is literally a view of it: each node = a field or an instance; edit a -field = mutate the document + re-bake. - -- **World config already exists as data**, just scattered: `SkyConfig`, the - `CloudLayer` union (basicCumulus/fancyCumulus + params), `TERRAIN` - (amplitude/frequency/peaks/inner/blend), `GROUND_UV`, room dims. Parameterizing = - moving these constants into the `Level`. -- **The tree forces one categorization: world property vs client look-preset.** - `RenderConfig` (internalWidth, colorDepth, dither, lodDistance, filters) is a - *per-viewer PS1 dial* — stays client-side. Sky/clouds/terrain = per-map, in the - document. But **fog + lighting live in `RenderConfig` today**, and in a shooter - fog density = sightlines = a *gameplay/map* property → fog probably moves into the - `Level`. Decide the world-vs-viewer line once, up front. -- **Re-bake blast radius varies wildly:** sky color = re-run sky, no chunk touch; - one tree = re-bake 1–2 chunks; terrain amplitude = re-bake *every* chunk. For a - tool, full re-bake per edit is fine to start; scope to dirty cells only if it janks. -- **Anti-overengineering line:** "everything parameterized" = *document completeness* - (every knob is data — yes). It is **not** a mandate for a generic reflection-UI - framework. Hand-wire panels for the handful of config sections; generalize to - type-driven widgets only if it hurts. - -### 2. Object tools — add / select / move / rotate - -- **Add** — palette pick + raycast ground (`camera` + `Terrain.height`) → drop an - instance record → re-bake. Easy. -- **Select / pick** — **no GPU picking needed.** Props bake to anonymous triangles, - but the document retains the instance list with positions → **ray-vs-instance-bounds - test in JS**, nearest hit wins. (Another reason instances must live on the document.) -- **Move** — drag → raycast ground → update `position` → re-bake touched cell(s). - Crossing a chunk boundary re-bakes 2 (cell membership is by base position). -- **Rotate — the one real engine gap.** Props have **no orientation today** (Tree/ - Boulder/Bush/Flower carry position + seed + size, no yaw; only *mobs* have - `heading`, applied live via `Mat4.compose`). Fix by **separating the per-instance - transform from geometry-gen**: `build` emits the instance's **unique local-space** - geometry (keep seed/growth = shape), and the **baker applies position + rotation + - scale** when appending to the chunk mesh. This mirrors what mobs already do (TRS - via `Mat4.compose`) — props become "unique local mesh × transform," baked-in - (static) where mobs are live (dynamic). This is the non-trivial refactor the object - tools demand; do it early since it touches every prop builder. -- **Gizmos** — the rasterizer draws textured tris only, **no line primitive.** A - move/rotate gizmo (and selection highlight / wireframe AABB) needs either thin-box - tris or a new line-draw path. **Start gizmo-less** (drag on ground = XZ move, - scroll/key = yaw; tinted re-draw for selection) and add handles later. - -### 3. Terrain brushes — elevation + texture (the biggest new capability) - -- **Elevation needs a stored, editable heightfield** — the thing that doesn't exist - yet. `Terrain.height` is a **pure analytic function** of noise params; a brush has - nowhere to write. So: **the `Level` owns a height grid**; procedural gen - *initializes* it; brushes edit it directly; `Terrain.height` bilinear-samples the - grid. ("Seeding is a tool," made concrete — noise seeds the grid, then you sculpt.) - - **Collision comes free** — player + mobs already sample `Terrain.height`, so once - it samples the grid, gameplay collision follows. No separate collision bake. - - **Welds still hold** — patches weld by sampling shared height at shared world - positions; grid-as-source keeps that. Re-bake only the brushed cells. - - Side effect: the room "hole" + flat-inner-clearing special cases **dissolve into - authored terrain**. An arena may drop the room concept entirely — the map *is* the - sculpted terrain. -- **Texture brush fits the engine shockingly well** — the chunk baker **already - accumulates one mesh per material key** and the renderer draws by DrawGroup list. - Ground is one `grass` material today; painting = ground goes to **N materials, each - painted tri routed to its material's mesh by a per-vertex/per-cell material id** — - the *exact* routing the baker already does for props. **No rasterizer change.** Hard - edges between materials (no blend) suit the PS1 look; soft splat-blending would need - per-pixel multi-texture rasterizer work — skip it, add later only if missed. - -### Threads that cut across - -1. **The `Level` document gains three responsibilities:** retain instance lists - (pick/move), own an editable **height grid** (elevation), own a **ground-material - map** (texture paint). All still pure data — the document grew, the architecture - didn't strain. -2. **One engine refactor unlocks the object tools:** pull per-instance transform - (pos/rot/scale) out of prop `build` into the baker — props become "unique local - mesh × transform," matching mobs. -3. **Terrain flips from function to data** — analytic → stored grid. Biggest single - change, but it's what "hand-made maps" *means*, and collision + welds fall out free. -4. **Almost none of this needs new *rendering*** — picking is JS, texture paint - reuses per-material DrawGroups, terrain is more patch bake. The only genuinely new - render bit is small + optional: **line-draw for gizmos/selection**. - -## Why the architecture already fits (the two bets that pre-paid for this) - -- **Deterministic bake** — the world is byte-identical from a `Level` every run → - multiplayer world-sync is nearly free (ship the document, everyone bakes the same). -- **DOM-free `engine`/`game`, layering test-enforced** — the authoritative server - runs `bake(level)` + sim with no renderer; `app/` is client-only glue. The layering - seam already enforced (`tests/layering.test.ts`) *is* the client/server seam. Keep - the `Level` document + `bake` in `game/`; editor UI in `app/`; server imports `game/`. - -## Cheap hedges (do now, save pain later) - -- The `Level` document is **pure serializable data** — no closures, no behavior baked - in (behavior stays code, imported per-side, as `Entity` already does it). -- A `Level` has an **id/name** — "load level X" is a reference; later persistence keys - off it for free. -- **Player state stays out of the `Level`/`Scene`** (already true) — carry-over later - touches the player, never the map. -- `bake` eats the **explicit** document as canonical — keeps the recipe-vs-explicit - network choice open. -- **No hardcoded buffer sizes** tied to today's world — mob/framebuffer sizing already - re-runs on `setup`; keep it so odd-shaped match maps just work. - -## Historical current-state facts (before 2026-08-20) - -- `buildLevel(textures): Level` (`game/level.ts:175`) is the single entry, called once - in `app/main.ts`. Today's `Level` (`game/level.ts:69-81`) is a **bake result** - (chunks/meshes/colliders/mobs) — under the new naming it becomes part of **`Scene`**; - the authored **`Level` document** is the missing *input* half. `buildLevel` becomes - `bake(level, textures) → Scene`. -- Placement (`placeTrees/placeBoulders/placeBushes/placeFlowers/placeMobs`, - `game/level.ts:359+`) is procedural from fixed seeds + counts, fully deterministic. - These become the **generator** (emit a `Level`) rather than running inside the bake. -- **Prop instances are discarded after bake** — only anonymous triangles survive in - per-chunk per-material meshes (`buildChunks`, `game/level.ts:251`). No `level.trees` - to iterate. The editor edits the **document**, then re-bakes — never mutates baked - meshes in place. -- **Mobs are the exception** — retained live, simulated each frame, crossing to workers - as packed floats. Add/move/delete is cheap, no worker resync (caveat: mob shared - buffer sized once to `scene.mobCount` in `app/renderer.ts` — growing past it needs a - re-`setup`). -- Instance shapes are already small plain records (position + seed + kind/size/color/ - growth) — editor-friendly, directly serializable. See - `game/actors/{Tree,Boulder,Bush,Flower,Mob}.ts`. -- Prop geometry currently bakes **position into world-space verts inside `build`**, with - seed/growth driving unique shape — hence the "separate transform from geometry-gen" - refactor needed for rotate/scale (see Object tools). -- `Terrain.height` (`game/Terrain.ts`) is a **pure analytic function**, no stored - heightfield — hence the elevation-brush needs a stored grid (see Terrain brushes). -- The render `Scene` (`renderScene.ts:19`) is the **render-only subset** already; the - chunk baker already routes geometry to **one mesh per material key** — the mechanism - the texture brush reuses. -- Workers hold a **one-time structured clone** of the render subset from init - (`app/renderer.ts` `setup`); per-frame only camera/matrix/visible/mob buffers cross. - Chunk-geometry edits reach workers only via a re-send (`reconfigure`→`setup`, respawns - them — the same path keys 1/2/3 use). Fine for a mode-switch re-bake; would be the wall - for live in-match editing, which we are **not** doing. - -## Explicitly out of scope / dropped - -Portals & in-world transitions · persistence · player carry-over · procedural as the -primary map path · any live-edit-during-a-multiplayer-match capability · GPU picking · -soft terrain-texture blending (hard-edged materials first). diff --git a/.agents/resources/commitlint.md b/.agents/resources/commitlint.md deleted file mode 100644 index ba4ee7e..0000000 --- a/.agents/resources/commitlint.md +++ /dev/null @@ -1,214 +0,0 @@ -@commitlint/config-conventional -=============================== - -Lint your conventional commits - -Shareable `commitlint` config enforcing [conventional commits](https://conventionalcommits.org/). Use with [@commitlint/cli](https://npm.im/@commitlint/cli) and [@commitlint/prompt-cli](https://npm.im/@commitlint/prompt-cli). - -Getting started ---------------- - -``` -npm install --save-dev @commitlint/config-conventional @commitlint/cli -echo "export default {extends: \['@commitlint/config-conventional'\]};" \> commitlint.config.js -``` - -Rules ------ - -### Problems - -The following rules are considered problems for `@commitlint/config-conventional` and will yield a non-zero exit code when not met. - -Consult [Rules reference](https://commitlint.js.org/reference/rules) for a list of available rules. - -#### type-enum - -- **condition**: `type` is found in value - -- **rule**: `always` - -- **level**: `error` - -- **value** - - ``` - [ - 'build', - 'chore', - 'ci', - 'docs', - 'feat', - 'fix', - 'perf', - 'refactor', - 'revert', - 'style', - 'test' - ]; - - ``` - -``` -echo "foo: some message" # fails -echo "fix: some message" # passes -``` - -#### type-case - -- **description**: `type` is in case `value` -- **rule**: `always` -- **level**: `error` -- **value** - ``` - 'lowerCase' - - ``` - -``` -echo "FIX: some message" # fails -echo "fix: some message" # passes -``` - -#### type-empty - -- **condition**: `type` is empty -- **rule**: `never` -- **level**: `error` - -``` -echo ": some message" # fails -echo "fix: some message" # passes -``` - -#### subject-case - -- **condition**: `subject` is in one of the cases `['sentence-case', 'start-case', 'pascal-case', 'upper-case']` -- **rule**: `never` -- **level**: `error` - -``` -echo "fix(SCOPE): Some message" # fails -echo "fix(SCOPE): Some Message" # fails -echo "fix(SCOPE): SomeMessage" # fails -echo "fix(SCOPE): SOMEMESSAGE" # fails -echo "fix(scope): some message" # passes -echo "fix(scope): some Message" # passes -``` - -#### subject-empty - -- **condition**: `subject` is empty -- **rule**: `never` -- **level**: `error` - -``` -echo "fix:" # fails -echo "fix: some message" # passes -``` - -#### subject-full-stop - -- **condition**: `subject` ends with `value` -- **rule**: `never` -- **level**: `error` -- **value** - -``` -'.' - -``` - -``` -echo "fix: some message." # fails -echo "fix: some message" # passes -``` - -#### header-max-length - -- **condition**: `header` has `value` or less characters -- **rule**: `always` -- **level**: `error` -- **value** - -``` -100 - -``` - -``` -echo "fix: some message that is way too long and breaks the line max-length by several characters" # fails -echo "fix: some message" # passes -``` - -#### footer-leading-blank - -- **condition**: `footer` should have a leading blank line -- **rule**: `always` -- **level**: `warning` - -``` -echo "fix: some message -BREAKING CHANGE: It will be significant" # warning - -echo "fix: some message -BREAKING CHANGE: It will be significant" # passes -``` - -#### footer-max-line-length - -- **condition**: `footer` each line has `value` or less characters -- **rule**: `always` -- **level**: `error` -- **value** - -``` -100 - -``` - -``` -echo "fix: some message -BREAKING CHANGE: footer with multiple lines -has a message that is way too long and will break the line rule 'line-max-length' by several characters" # fails - -echo "fix: some message -BREAKING CHANGE: footer with multiple lines -but still no line is too long" # passes -``` - -#### body-leading-blank - -- **condition**: `body` should have a leading blank line -- **rule**: `always` -- **level**: `warning` - -``` -echo "fix: some message -body" # warning - -echo "fix: some message -body" # passes -``` - -#### body-max-line-length - -- **condition**: `body` each line has `value` or less characters -- **rule**: `always` -- **level**: `error` -- **value** - -``` -100 - -``` - -``` -echo "fix: some message -body with multiple lines -has a message that is way too long and will break the line rule 'line-max-length' by several characters" # fails - -echo "fix: some message -body with multiple lines -but still no line is too long" # passes -``` \ No newline at end of file diff --git a/.agents/resources/conventional-commit.md b/.agents/resources/conventional-commit.md deleted file mode 100644 index a0e771c..0000000 --- a/.agents/resources/conventional-commit.md +++ /dev/null @@ -1,128 +0,0 @@ -Conventional Commits 1.0.0 -========================== - -Summary -------- - -The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with [SemVer](http://semver.org/), by describing the features, fixes, and breaking changes made in commit messages. - -The commit message should be structured as follows: - -* * * * - -``` -[optional scope]: - -[optional body] - -[optional footer(s)] - -``` - -* * * * - -The commit contains the following structural elements, to communicate intent to the consumers of your library: - -1. **fix:** a commit of the *type* `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning). -2. **feat:** a commit of the *type* `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning). -3. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning). A BREAKING CHANGE can be part of commits of any *type*. -4. *types* other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)) recommends `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. -5. *footers* other than `BREAKING CHANGE: ` may be provided and follow a convention similar to [git trailer format](https://git-scm.com/docs/git-interpret-trailers). - -Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`. - -Examples --------- - -### Commit message with description and breaking change footer - -``` -feat: allow provided config object to extend other configs - -BREAKING CHANGE: `extends` key in config file is now used for extending other config files - -``` - -### Commit message with `!` to draw attention to breaking change - -``` -feat!: send an email to the customer when a product is shipped - -``` - -### Commit message with scope and `!` to draw attention to breaking change - -``` -feat(api)!: send an email to the customer when a product is shipped - -``` - -### Commit message with both `!` and BREAKING CHANGE footer - -``` -feat!: drop support for Node 6 - -BREAKING CHANGE: use JavaScript features not available in Node 6. - -``` - -### Commit message with no body - -``` -docs: correct spelling of CHANGELOG - -``` - -### Commit message with scope - -``` -feat(lang): add Polish language - -``` - -### Commit message with multi-paragraph body and multiple footers - -``` -fix: prevent racing of requests - -Introduce a request id and a reference to latest request. Dismiss -incoming responses other than from latest request. - -Remove timeouts which were used to mitigate the racing issue but are -obsolete now. - -Reviewed-by: Z -Refs: #123 - -``` - -Specification -------------- - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). - -1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space. -2. The type `feat` MUST be used when a commit adds a new feature to your application or library. -3. The type `fix` MUST be used when a commit represents a bug fix for your application. -4. A scope MAY be provided after a type. A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):` -5. A description MUST immediately follow the colon and space after the type/scope prefix. The description is a short summary of the code changes, e.g., *fix: array parsing issue when multiple spaces were contained in string*. -6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. -7. A commit body is free-form and MAY consist of any number of newline separated paragraphs. -8. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of a word token, followed by either a `:` or `#` separator, followed by a string value (this is inspired by the [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)). -9. A footer's token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate the footer section from a multi-paragraph body). An exception is made for `BREAKING CHANGE`, which MAY also be used as a token. -10. A footer's value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed. -11. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer. -12. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., *BREAKING CHANGE: environment variables now take precedence over config files*. -13. If included in the type/scope prefix, breaking changes MUST be indicated by a `!` immediately before the `:`. If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change. -14. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., *docs: update ref docs.* -15. The units of information that make up Conventional Commits MUST NOT be treated as case-sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. -16. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. - -Why Use Conventional Commits ----------------------------- - -- Automatically generating CHANGELOGs. -- Automatically determining a semantic version bump (based on the types of commits landed). -- Communicating the nature of changes to teammates, the public, and other stakeholders. -- Triggering build and publish processes. -- Making it easier for people to contribute to your projects, by allowing them to explore a more structured commit history. \ No newline at end of file diff --git a/.agents/rules/big-red-dog.md b/.agents/rules/big-red-dog.md deleted file mode 100644 index 5015141..0000000 --- a/.agents/rules/big-red-dog.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -alwaysApply: true ---- - -# A way to check whether my instructions are loaded into context automatically - -the big red dog is named Salamander diff --git a/.agents/rules/caveman.md b/.agents/rules/caveman.md deleted file mode 100644 index 923185e..0000000 --- a/.agents/rules/caveman.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Always load the caveman skill for concise technical responses. ---- - -# Caveman Mode - -Use the `caveman` skill for every response. - -Stop only when the user explicitly says `stop caveman` or `normal mode`. diff --git a/.agents/rules/commits.md b/.agents/rules/commits.md deleted file mode 100644 index e1fea2c..0000000 --- a/.agents/rules/commits.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -condition: "(?i)git commit|commit message|conventional commit" ---- - -# Commits - -When commiting changes, follow these rules to write the messages: - -- Conventional Commit (reference available at .agents/resources/conventional-commit.md) -- `@commitlint/config-conventional` (reference available at .agents/resources/commitlint.md) - diff --git a/.agents/rules/quality.md b/.agents/rules/quality.md deleted file mode 100644 index a1aeb74..0000000 --- a/.agents/rules/quality.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -alwaysApply: true ---- - -# Quality Guardrails - -Use these while building. Do not turn every task into a full review; apply as lightweight pressure before adding or changing structure. - -## Module Shape - -- Filenames should match the primary export exactly, including casing. -- Exports and higher level functions should appear before private functions in a file. -- Prefer a primary type plus matching namespace for behavior tied to that type. -- Functions that operate on a domain type should live in that type's namespace. -- Avoid scattered single-name function exports for domain-specific behavior. -- Truly generic helpers may stay standalone when they do not naturally belong to a domain type. - -## Design Pressure - -- DRY: centralize rules and policy, not incidental similarity. -- KISS: prefer the smallest clear structure that solves the actual problem. -- YAGNI: do not add speculative extension points, options, compatibility layers, or abstractions. -- SOC: keep parsing, validation, IO, orchestration, and policy separate when mixing them creates change pressure. -- Cohesion: each module/type/function should have one clear job and one clear reason to change. -- Coupling: avoid making callers know protocol internals, nested implementation details, or unrelated runtime policy. -- Locality: one behavior should not require excessive jumping across unrelated files or helpers. -- Naming/API Clarity: names should expose behavior and domain meaning; avoid vague wrappers, false promises, and boolean-blind APIs. - -## Refactor Bias - -- Prefer moving behavior to the domain owner over creating utility bags. -- Prefer local private helpers over exported helpers until another real caller exists. -- Prefer deleting compatibility code when there are no shipped consumers, persisted data, or explicit requirements. -- Prefer small reshapes that preserve behavior over broad rewrites. diff --git a/.agents/skills/caveman/README.md b/.agents/skills/caveman/README.md deleted file mode 100644 index 696a4e3..0000000 --- a/.agents/skills/caveman/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# caveman - -Talk like smart caveman. Same brain, fewer tokens. - -## What it does - -Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts 65% of output tokens (measured) with full accuracy preserved. Mode persists for the whole session until changed or stopped. - -Six intensity levels: - -| Level | What change | -|-------|-------------| -| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | -| `full` | Default. Drop articles, fragments OK, short synonyms. | -| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | -| `wenyan-lite` | Classical Chinese register, light compression. | -| `wenyan-full` | Maximum 文言文. 80-90% character reduction. | -| `wenyan-ultra` | Extreme classical compression. | - -Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. - -## How to invoke - -``` -/caveman # full mode (default) -/caveman lite # lighter compression -/caveman ultra # extreme compression -/caveman wenyan # classical Chinese -stop caveman # back to normal prose -``` - -## Example output - -Question: "Why does my React component re-render?" - -Normal prose: -> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. - -Caveman (full): -> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. - -Caveman (ultra): -> Inline obj prop → new ref → re-render. `useMemo`. - -## See also - -- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions -- [Caveman README](../../README.md) — repo overview, install, benchmarks diff --git a/.agents/skills/caveman/SKILL.md b/.agents/skills/caveman/SKILL.md deleted file mode 100644 index adf8bc5..0000000 --- a/.agents/skills/caveman/SKILL.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: caveman -description: > - Ultra-compressed communication mode. Cuts output tokens 65% (measured) by speaking like caveman - while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, - wenyan-lite, wenyan-full, wenyan-ultra. - Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", - "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. ---- - -Respond terse like smart caveman. All technical substance stay. Only fluff die. - -## Persistence - -ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". - -Default: **full**. Switch: `/caveman lite|full|ultra`. - -## Rules - -Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) — tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either — own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact. - -Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. User write Spanish → reply Spanish caveman. Compress the style, not the language. No forced English openings or status phrases. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation. - -No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is. - -Pattern: `[thing] [action] [reason]. [next step].` - -Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." -Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" - -## Intensity - -| Level | What change | -|-------|------------| -| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | -| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations | -| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) — measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch | -| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | -| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | -| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | - -Example — "Why React component re-render?" -- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." -- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." -- ultra: "Inline obj prop, new ref, re-render. `useMemo`." -- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" -- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。" -- wenyan-ultra: "新參照則重繪。useMemo 包之。" - -Example — "Explain database connection pooling." -- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." -- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." -- ultra: "Pool reuse open DB connections. No per-request handshake." -- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。" -- wenyan-ultra: "池蓄連,免逐請新開,省握手。" - -## Auto-Clarity - -Drop caveman when: -- Security warnings -- Irreversible action confirmations -- Multi-step sequences where fragment order or omitted conjunctions risk misread -- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) -- User asks to clarify or repeats question - -Resume caveman after clear part done. - -Example — destructive op: -> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. -> ```sql -> DROP TABLE users; -> ``` -> Caveman resume. Verify backup exist first. - -## Boundaries - -Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. \ No newline at end of file diff --git a/.agents/skills/domain-modeling/ADR-FORMAT.md b/.agents/skills/domain-modeling/ADR-FORMAT.md deleted file mode 100644 index da7e78e..0000000 --- a/.agents/skills/domain-modeling/ADR-FORMAT.md +++ /dev/null @@ -1,47 +0,0 @@ -# ADR Format - -ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. - -Create the `docs/adr/` directory lazily — only when the first ADR is needed. - -## Template - -```md -# {Short title of the decision} - -{1-3 sentences: what's the context, what did we decide, and why.} -``` - -That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. - -## Optional sections - -Only include these when they add genuine value. Most ADRs won't need them. - -- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited -- **Considered Options** — only when the rejected alternatives are worth remembering -- **Consequences** — only when non-obvious downstream effects need to be called out - -## Numbering - -Scan `docs/adr/` for the highest existing number and increment by one. - -## When to offer an ADR - -All three of these must be true: - -1. **Hard to reverse** — the cost of changing your mind later is meaningful -2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" -3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons - -If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." - -### What qualifies - -- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." -- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." -- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. -- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. -- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. -- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." -- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/domain-modeling/CONTEXT-FORMAT.md b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md deleted file mode 100644 index eaf2a18..0000000 --- a/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +++ /dev/null @@ -1,60 +0,0 @@ -# CONTEXT.md Format - -## Structure - -```md -# {Context Name} - -{One or two sentence description of what this context is and why it exists.} - -## Language - -**Order**: -{A one or two sentence description of the term} -_Avoid_: Purchase, transaction - -**Invoice**: -A request for payment sent to a customer after delivery. -_Avoid_: Bill, payment request - -**Customer**: -A person or organization that places orders. -_Avoid_: Client, buyer, account -``` - -## Rules - -- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. -- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. -- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. -- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. - -## Single vs multi-context repos - -**Single context (most repos):** One `CONTEXT.md` at the repo root. - -**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: - -```md -# Context Map - -## Contexts - -- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders -- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments -- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping - -## Relationships - -- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking -- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices -- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` -``` - -The skill infers which structure applies: - -- If `CONTEXT-MAP.md` exists, read it to find contexts -- If only a root `CONTEXT.md` exists, single context -- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved - -When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/domain-modeling/SKILL.md b/.agents/skills/domain-modeling/SKILL.md deleted file mode 100644 index d0f7e1a..0000000 --- a/.agents/skills/domain-modeling/SKILL.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: domain-modeling -description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model. ---- - -# Domain Modeling - -Actively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.) - -## File structure - -Most repos have a single context: - -``` -/ -├── CONTEXT.md -├── docs/ -│ └── adr/ -│ ├── 0001-event-sourced-orders.md -│ └── 0002-postgres-for-write-model.md -└── src/ -``` - -If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: - -``` -/ -├── CONTEXT-MAP.md -├── docs/ -│ └── adr/ ← system-wide decisions -├── src/ -│ ├── ordering/ -│ │ ├── CONTEXT.md -│ │ └── docs/adr/ ← context-specific decisions -│ └── billing/ -│ ├── CONTEXT.md -│ └── docs/adr/ -``` - -Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. - -## During the session - -### Challenge against the glossary - -When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" - -### Sharpen fuzzy language - -When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." - -### Discuss concrete scenarios - -When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. - -### Cross-reference with code - -When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" - -### Update CONTEXT.md inline - -When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). - -`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. - -### Offer ADRs sparingly - -Only offer to create an ADR when all three are true: - -1. **Hard to reverse** — the cost of changing your mind later is meaningful -2. **Surprising without context** — a future reader will wonder "why did they do it this way?" -3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons - -If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). diff --git a/.agents/skills/domain-modeling/agents/openai.yaml b/.agents/skills/domain-modeling/agents/openai.yaml deleted file mode 100644 index 7f1522d..0000000 --- a/.agents/skills/domain-modeling/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "Domain Modeling" - short_description: "Build and sharpen a domain model" diff --git a/.agents/skills/forge-sync/SKILL.md b/.agents/skills/forge-sync/SKILL.md deleted file mode 100644 index 84885bf..0000000 --- a/.agents/skills/forge-sync/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: forge-sync -description: Use for `sketch --sync`, `sketch-sync`, or workflows needing shared local-vs-forge git handling; centralizes branch, commit, push, PR, and forge comment policy. ---- - -# Forge Sync - -Forge Sync = shared git + forge workflow policy. It decides how completed workflow units become commits, pushes, PRs, and PR comments. It does not decide when code work is complete. - -## Inputs - -Caller must provide or make clear: - -- Mode: `local` or `sync`. -- Change ID. -- Workflow: `sketch`, `archive`, or other. -- Unit: task section, archive, or named completed work unit. -- Commit message. -- Optional PR comment body. - -If mode is explicit, do not ask git workflow questions. - -## Universal Rules - -- Never force-push. -- Never delete branches. -- Never run destructive git commands. -- Never amend unless explicitly requested. -- Before committing, inspect status and diff. -- Do not include unrelated user changes. -- Commit messages must follow Conventional Commit and commitlint rules. - -## Local Mode - -Local mode means: - -- Do not push. -- Do not create, update, or comment on PRs. -- Do not call forge write tools. -- Create local commits only when caller requests a completed unit commit. -- If unrelated changes exist, commit only files/hunks belonging to current unit. - -## Sync Mode - -Sync mode means: - -- Ensure work happens on a feature branch for the change when caller has not already selected one. -- Commit completed unit. -- Push current branch after each unit commit. -- Create PR after first push if no PR exists. -- Reuse existing PR on later pushes. -- Add or update PR comment when caller provides comment body. - -Default branch name when creating one: - -```txt -feat/ -``` - -Default PR base: - -```txt -main -``` - -Do not create duplicate PRs. Check branch/PR state first when feasible. - -## Checkpoint Procedure - -When caller says a unit is complete: - -1. Inspect git status and diff. -2. Stage only relevant files. -3. Commit with caller-provided message. -4. If mode is local, stop. -5. If mode is sync, push current branch. -6. If PR comment body provided, comment or update as directed by caller. -7. Report commit SHA, push status, PR URL/number, and comment status. - -## PR Comment Shape - -Use caller-provided body when available. If caller asks for a default comment: - -```md -## - -Change: `` - -Commits: -- `` - -Verification: -- -``` diff --git a/.agents/skills/grill-with-docs/SKILL.md b/.agents/skills/grill-with-docs/SKILL.md deleted file mode 100644 index bed05d2..0000000 --- a/.agents/skills/grill-with-docs/SKILL.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: grill-with-docs -description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go. -disable-model-invocation: true ---- - -Run a `/grilling` session, using the `/domain-modeling` skill. diff --git a/.agents/skills/grill-with-docs/agents/openai.yaml b/.agents/skills/grill-with-docs/agents/openai.yaml deleted file mode 100644 index 5dbe278..0000000 --- a/.agents/skills/grill-with-docs/agents/openai.yaml +++ /dev/null @@ -1,5 +0,0 @@ -interface: - display_name: "Grill with Docs" - short_description: "Grill a design and write its docs" -policy: - allow_implicit_invocation: false diff --git a/.agents/skills/grilling/SKILL.md b/.agents/skills/grilling/SKILL.md deleted file mode 100644 index 52d8eb3..0000000 --- a/.agents/skills/grilling/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: grilling -description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. ---- - -Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. - -Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering. - -If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer. - -Do not act on it until I confirm we have reached a shared understanding. diff --git a/.agents/skills/grilling/agents/openai.yaml b/.agents/skills/grilling/agents/openai.yaml deleted file mode 100644 index 85b1260..0000000 --- a/.agents/skills/grilling/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "Grilling" - short_description: "Stress-test thinking one question at a time" diff --git a/.agents/skills/jerklint/SKILL.md b/.agents/skills/jerklint/SKILL.md deleted file mode 100644 index d46a6eb..0000000 --- a/.agents/skills/jerklint/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: jerklint -description: Use when user asks for "jerklint", "jerk lint", or strict code-quality review. Reviews code against DRY, KISS, YAGNI, SOC, cohesion, coupling, dependency direction, Law of Demeter, immutability, declarative shape, implicit contracts, abstraction pressure, naming/API clarity, and locality. ---- - -# Jerklint - -Jerklint = strict code-quality review. Be blunt, specific, fair. Findings must be actionable, not taste fights. - -## Trigger - -Use this skill when user asks for: -- `jerklint` -- `jerk lint` -- strict code-quality review -- review against DRY, KISS, YAGNI, SOC, cohesion, coupling, dependency direction, Law of Demeter, immutability, declarative programming, or maintainability principles - -## Goal - -Find code smells, design pressure, and maintainability risk. This is not normal bug review. Bugs matter only when they reveal deeper code-quality failure. - -## Axes - -- DRY: flag duplicated policy, copy-paste structure, repeated literals, or drift-prone validation. Do not centralize incidental similarity. -- KISS: flag needless indirection, clever control flow, over-generalization, and logic that is harder than its problem. -- YAGNI: flag speculative extension points, options, abstractions, or compatibility layers with no concrete need. -- SOC: flag mixed responsibilities, especially parsing + validation + IO + orchestration + policy in one unit. -- Cohesion: flag functions/types/modules that do not have one clear job or reason to change. -- Coupling: flag unnecessary knowledge between layers, callers, protocols, domains, or runtime details. -- Dependency Direction: flag lower-level code depending on higher-level policy, domain declarations depending on adapters, or circular conceptual flow. -- Law of Demeter: flag long object walks, dependency spelunking, and callers that know too much about nested internals. -- Immutability / State Discipline: prefer immutable boundaries and local mutation only. Flag shared mutable state, hidden mutation, aliasing risk, and mutation that creates temporal coupling. -- Declarative Shape: favor data/config descriptions for policy and protocol surfaces; keep execution/IO separate. Flag imperative branching where a small table/schema/declaration would clarify rules. -- Implicit Contracts / Temporal Coupling: flag hidden ordering requirements, call rituals, required prior validation, or invariants not encoded in type/name/API. -- Abstraction Pressure: flag both over-centralization and under-centralization. Centralize rules, not whole workflows. -- Naming/API Clarity: flag names that hide behavior, false promises, vague abstractions, boolean blindness, or weak error messages. -- Locality: flag code that requires excessive jumping across files/functions to understand one behavior. -- Testability: flag structure that forces brittle tests, excessive mocking, or untestable policy logic. -- File/API Shape: flag domain-specific function bags, filenames that do not match primary exports, and behavior that should live under a matching type namespace. -- Predicate Accuracy: flag boolean predicates/guards that check partial or wrong shape. Flag `in` checks without type narrowing, truthiness checks that miss falsy valid values, and type guards that accept broader input than their name promises. Prefer predicates that validate the full claimed shape. -- Construction Phase Separation: flag builder/factory functions that execute more than three distinct sequential phases in one function body without named phase boundaries. Flag protocol creation, config extraction, normalization, wiring, and return shaping collapsed into one function. Prefer named phase functions even when each is small. - -## Project Style Conventions - -Apply these conventions when reviewing module/API shape: -- File/Export Match: filenames should match the primary export exactly, including casing. Example: `Field.ts` exports `Field`. -- Type Namespace Cohesion: prefer a primary type plus matching namespace for behavior tied to that type. Example: `Field` + `namespace Field`. -- Domain Function Locality: functions that operate on a domain type should live in that type's namespace instead of as loose exports. -- Avoid Function Bags: avoid scattered single-name function exports for domain-specific behavior. Group them under the relevant domain type. -- Generic Helper Exception: truly generic helpers may remain standalone when they do not naturally belong to a domain type. - -## Non-Goals - -Do not focus on: -- formatting nits -- style preferences without maintenance impact -- security bugs unless structure caused them -- correctness bugs unless they reveal quality smell -- broad rewrites without concrete pressure -- purity dogma: mutation and imperative code are fine when local, clear, and bounded - -## Method - -1. Read target file first. -2. Read direct collaborators only when needed to validate design pressure. -3. Prefer evidence from code over principle recitation. -4. Rank findings by maintainability impact. -5. Cite file/line refs. -6. Suggest smallest useful direction, not full rewrites. -7. If no findings, say so and name strongest qualities. - -## Output - -Findings first. Keep summary secondary. - -```markdown -**Jerklint Findings** -1. **High** `path:line`: Smell. Why it hurts. Better direction. - -**Scorecard** -- DRY: pass/concern/fail - one phrase. -- KISS: pass/concern/fail - one phrase. -- YAGNI: pass/concern/fail - one phrase. -- SOC: pass/concern/fail - one phrase. -- Cohesion: pass/concern/fail - one phrase. -- Coupling: pass/concern/fail - one phrase. -- Dependency Direction: pass/concern/fail - one phrase. -- Law of Demeter: pass/concern/fail - one phrase. -- Immutability: pass/concern/fail - one phrase. -- Declarative Shape: pass/concern/fail - one phrase. -- Implicit Contracts: pass/concern/fail - one phrase. -- Abstraction Pressure: pass/concern/fail - one phrase. -- Naming/API Clarity: pass/concern/fail - one phrase. -- Locality: pass/concern/fail - one phrase. -- Predicate Accuracy: pass/concern/fail - one phrase. -- Construction Phase Separation: pass/concern/fail - one phrase. - -**Verdict** -Keep / minor refactor / refactor soon / rethink. -``` - -## Severity - -- High: smell creates likely drift, hard-to-change design, boundary violation, or hidden invariant across callers. -- Medium: smell adds avoidable complexity or makes future work risky but is local. -- Low: polish-level maintainability concern worth noting only if concrete. - -## Calibration - -- Do not chant DRY. Duplication can be clearer than wrong abstraction. -- Do not chant KISS. Simpler locally can be worse globally if it duplicates policy. -- Do not chant YAGNI. Keep extension points when existing architecture already requires them. -- Do not chant immutability. Local accumulators are fine when ownership is clear. -- Do not chant declarative programming. Imperative steps are fine when sequencing is core behavior. -- Prefer "centralize this rule" over "make a manager". -- Prefer "split parsing from execution" over "add layers". -- Do not limit DRY to textual duplication. Flag structural duplication: two functions that recursively traverse the same tree shape with different leaf transforms. -- Under Implicit Contracts, flag magic DI object shapes where binder and consumer agree on structure by convention without an exported type or binding token. -- Under Implicit Contracts, flag whitelist-vs-blocklist asymmetry where two code paths encoding the same conceptual boundary use different inclusion strategies. diff --git a/.gitignore b/.gitignore index dccad5e..79b05a5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,9 @@ dist .opencode builds tmp -out \ No newline at end of file +out +/.agents +/AGENTS.md +/CONTEXT.md +/docs +/opencode.json diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index fb5325f..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,334 +0,0 @@ -# meat - -A from-scratch browser **first-person-shooter game engine** with a configurable -**PS1 aesthetic**. Everything is hand-written: a **software rasterizer** draws -textured triangles into a low-res CPU framebuffer, then Canvas2D blits it -upscaled. No WebGL/WebGPU, no game-engine dependencies. - -This file is the durable project brief (checked in, auto-loaded). The per-topic -rules live in `.agents/rules/*.md`. - -## Guiding decisions (the "why", not derivable from code) - -- **PS1 look is a set of live knobs**, dial-able from full-PS1 to clean. See - `RenderConfig`. Nothing about the era is hardcoded into the renderer. -- **Software rasterizer on purpose** — the PS1 look leans on rasterizer traits - (vertex snap, low-res + dither/banding, no mipmaps). Raytracing was considered - and cut (it removes the very artifacts we want). Note: **affine texture "swim" - was dropped** — texturing is always perspective-correct now (it warped badly on - the big outdoor terrain); the other era knobs stay. -- **Minimal architecture.** Scene-graph-lite / plain data + functions. - Deliberately **not** ECS or any "Big Game Architecture." Prefer the smallest - clear structure; add knobs to experiment rather than abstractions. -- **Culling beats batching here.** A "draw call" is just a JS loop (no GPU state), - so merging the world into big meshes only defeats visibility skipping. Instead - the outdoor world is stored as spatial **chunks** that are frustum-culled per - frame; on-screen solids also **backface-cull**. This is what keeps a dense world - (thousands of trees/rocks) affordable — off-screen content costs ~nothing. -- **2D assets only.** Sprites/billboards (PS1-style), **no 3D model loading**. -- **Three layers, one-way deps: `engine` ← `game` ← `app`.** `engine/` owns - reusable, content-agnostic **concepts and mechanisms**: levels, terrain, actors, - prefabs, collision, render scenes, chunking/LOD, rasterization, and worker render - transport. `game/` owns **this game's concrete content**: actual level values, - placements, materials, and frog/bee/robin or oak/spruce/birch definitions. - `app/` is browser glue (canvas, input mapping, image decode, workers, frame loop). - Enforced: `engine/` imports nothing from `game`/`app`, `game/` nothing from `app` - (`tests/layering.test.ts`). Both `engine` and `game` are DOM-free (tsconfig). -- **No closed game-content kinds or registries.** Instances reference concrete - `ActorDefinition`/`Prefab` objects directly. Do not add `MobKind`, `TreeKind`, - `*_KINDS`, content dispatch switches, or stable content-order protocols. Numeric - prototype indexes are scene-local engine transport details only. Closed unions - remain valid for finite engine capabilities such as collider or cloud shape. - -## Stack & tooling - -- **Bun** runtime + `bun test`. **TypeScript 7** (native `tsc`), strict, - `moduleResolution: bundler`. **Vite 8** serves/builds the client (ES-module - workers; dev/preview serve COOP/COEP headers so the multi-threaded renderer's - `SharedArrayBuffer` works — a static prod host must send them too, or the app - falls back to single-threaded). -- **oxc** toolchain: `oxlint` + `oxfmt` (no eslint/prettier). -- commitlint + husky (Conventional Commits), OpenSpec change workflow, - forge-sync (Forgejo). Templates generated by `regime` from a `sigitex:` source. - -## Commands - -- `bun start` — Vite dev server; open the printed URL. Edits hot-reload. -- `bun run build` — production build to `dist/`. -- `bun run assets` — regenerate the placeholder PNGs in `/assets`. -- `bun run bench:browser` — Playwright: drive headless Chromium through the - `?bench=st`/`?bench=mt` flythrough, print single-thread vs worker frame timings - (median/p95/max). The real-browser profiler; run it on the target machine. -- `bunx tsc --build tsconfig.app.json` — **typecheck the engine+game+app graph. Use - this**, not `bun run check` (see Caveats). -- `bunx oxlint engine game app` — lint. -- `bun test` — tests (world compilation, render protocol, boundary guards). -- `bun run serve` — Bun server (`server/server.ts`, a stub for now). - -## Layout - -- `engine/` — content-agnostic mechanism (no DOM, no game content); consumed by - `game/` then `app/` via tsconfig project refs. - - `math/` — `Vec2`, `Vec3`, `Mat4` (column-major, OpenGL-style; verified). - - `render/` — `Color` (packed RGBA, little-endian = canvas ImageData order), - `Framebuffer` (Uint32 color + Float32 1/w depth; `quantize` = color-depth + - Bayer dither), `RenderConfig` (the look dials + presets), `Rasterizer` - (optional backface cull per draw), `Frustum` (6 planes from the viewProj + - AABB test, for chunk culling), `Texture` (nearest/bilinear, wrapping, no - mipmaps), `Material` (texture + cull flag; a `DrawGroup` pairs a mesh with one, - so the renderer draws by list, not by named texture), `Chunk`/`ChunkBuilder` - (spatial batches + two-level LOD), `RenderScene` (clone-safe scene projection, - culling + band rendering), `RenderProtocol` (shared worker frame layout), and - `Sky` (full-resolution gradient + sun, procedural clouds sampled at 1/`step`). - - `scene/` — `Camera` (fps yaw/pitch; far plane reaches the outdoor peaks), - `Mesh` (indexed tris; verts stored flat: `STRIDE` floats x,y,z,u,v per vertex, - no per-vertex objects — cache-friendly + alloc-free to draw), `Sprite` - (Y-axis billboard), `MeshBuilder` (generic quad/slab/box construction), `Actor` - (open behavior + render/collider contract), and `Prefab` (open static-content - contract + type-erased placed value). - - `world/` — `Terrain` (height contract, built-in rolling generator, generic - patch meshing), `Collider`/`CollisionWorld`, `CharacterController`, and `Level` - (live actor/collision state + clone-safe `RenderScene` projection). -- `game/` — this game's definitions and level data, on the engine interfaces - (headless: no DOM, imports nothing from `app`). - - `actors/` — the placeable things. `Mob` (a **roaming** creature — `frog` hops - the ground, `bee` hovers/darts, `robin` mostly hops but now and then takes a - short powered flight — the only moving geometry; each module exports an - `ActorDefinition` factory and instances hold the resulting object directly. - `Tree` is shared placement state; Oak/Spruce/Birch modules export material-bound - `Prefab` factories with no species registry. `Boulder` - (squashed jittered part-buried sphere), `Bush` (leaf-blob cluster, shares the - leaf mesh), `Flower` (stem + colored bloom, 2x2 atlas, double-sided). Baked props - append into shared per-material meshes; mobs draw live. - - `level.ts` — concrete playground values and placement policy: room dimensions, - rolling-terrain parameters, counts/seeds/reach, material bindings, sky, and - spawn definition lists. It places direct `Prefab` objects, submits the resulting - values to engine `ChunkBuilder`, then creates engine `Level`; no game renderer or material-key - registry exists. Trees + boulders bake full and far-impostor geometry; bushes - and flowers provide near geometry only. - - `player.ts` — concrete player tuning only; movement and collision live in - engine `CharacterController`. -- `app/` — browser glue only (top layer; depends on `game` + `engine`). - - `main.ts` — game loop: input, sim, preset switching, per-frame culling, then - the non-blocking pump (`renderer.dispatch`/`done`) + `present` (GPU/CSS upscale) - + a multi-line frame HUD (`work + present` critical-path ms, vsync, visible - chunks / LOD-aware tris). Calls engine `Level` simulation/collider extraction, - maps browser keys into `CharacterInput`, and dispatches visible render instances. - - `renderer.ts` — the render driver. When the page is cross-origin-isolated it runs - a pool of `render-worker.ts` threads (`MAX_WORKERS`) over a `SharedArrayBuffer` - framebuffer, each owning a disjoint row band, synced by a lock-free `Atomics` - barrier; otherwise inline. `dispatch`/`done` are non-blocking so the caller paces - on rAF. Per-frame inputs ride shared arrays: camera/matrix/visible-chunk list + - visible engine instance prototype indexes + transforms. `?bench=st|mt` A/Bs - the paths. Renderer and worker import no game modules. - - `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes are - already the `Color` layout); returns the `game` `Textures` palette. -- `index.html` — Vite entry at repo root; holds the `#screen` canvas and the - `#fps` meter div (styled inline). -- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and - writes PNGs (hand-rolled encoder via `node:zlib`). Run via `bun run assets`. -- `assets/` — generated `floor/grass/bark/birch/leaf/needle/rock/flower/wall/crate/npc/frog/bee/robin` - PNGs (`floor` = room stone, `grass` = outdoor ground, `bark`/`birch`/`leaf`/`needle` = - brown trunk / white birch trunk / oak leaf / spruce needle, `rock` = boulders, `flower` = 2x2 bloom-color atlas, - `frog`/`bee`/`robin` = mob skin atlases: frog green + eye tone; bee stripe bands + - head-dark + wing-pale; robin brown back + orange breast + dark eye/beak). - Swap for real art anytime; - filenames are the contract. -- `server/` — Bun server stub. `shared/` — isomorphic slot. - -## Frame pipeline (`app/main.ts` `tick`) - -`Level.update` (all actors) + `Level.refreshActorColliders` → -`CharacterController.update` → build `Camera` → `Camera.viewProjection` → -`RenderScene.visibleChunks` + `Level.visibleInstances` (frustum-cull, once on the -main thread) → `renderer.dispatch` (non-blocking) → -next rAF: `renderer.done()` ? `present` : skip this vsync. Frame N is presented -while N+1 is dispatched; the pump never blocks or async-awaits, so it can't -desync from rAF. - -**`present` is a GPU/CSS upscale, not a CPU blit.** The `#screen` canvas backing -store *is* the internal render resolution, so `present` is one internal-res -`putImageData` (viewport-independent, ~fixed cost). The browser compositor scales -the element to the display via CSS — `layout()` sets the element's pixel size to -an integer multiple (crisp letterbox, centered) once per resize/config, and -`image-rendering` follows `upscaleFilter` (`pixelated` for `nearest`, `auto` for -`linear`). This replaced a per-frame main-thread `drawImage` that scaled to the -whole window (cost grew with window size); present is now ~0.2ms. - -`renderBand` runs `RenderScene.renderBand` for rows [y0,y1): `Sky.render` with -clouds sampled at 1/`SKY_STEP` res (fills color + resets depth, replaces a clear) → `Rasterizer.draw` -floor/walls/crate (room, always) → for each visible `Chunk`, loop its draw-groups — -`near` or `far` chosen by the pure `Chunk.isFar` test (dist² from camera to the chunk -AABB vs `lodDistance²`): `near` is grass + full trees/rocks + flowers, `far` is grass -+ the cheap impostors (foliage/flowers dropped). Each group draws with its own -`Material` (cull per-material, so solids backface-cull and flowers stay double-sided) -→ each billboard → each visible actor prototype instance (shared local draw groups × -its `Mat4.compose` model matrix) → `Framebuffer.quantize`. `Chunk.isFar` is pure (camera + baked bounds + config -only), so every worker band picks the same LOD for a chunk → no horizontal seam. -Multi-threaded: N workers each run `renderBand` over -their band of the shared framebuffer in parallel; single-threaded: one call over -the full height. Bands are disjoint (no two workers touch a pixel) and their -interior edges snap to `SKY_STEP` so the sky's block grid stays seamless. - -Rasterizer specifics: near-plane clip (Sutherland-Hodgman), **1/w z-buffer**, -perspective-correct UVs, screen-space vertex snap, flat directional lighting, -distance fog, **alpha cutout** (discard texel alpha < 128, for sprites). -**Backface culling is opt-in** (`draw(..., cull)`, default off = double-sided): -on for solid world chunks, off for sprites and the room. It relies on winding, so -generators feeding culled draws (terrain patch, tree/boulder builders) must wind -front-out — a culled mesh that renders inside-out has its index order flipped -(see `Terrain.patch`). The cull sign: back-facing == positive screen area here. - -## The look — where to tune - -- **`engine/render/RenderConfig.ts`** — per-frame render dials + presets: - `standard` (384×216, the startup default), `soft`, `clean`, plus an unbound - `ps1` (320×240). In-app keys **1/2/3** switch standard/soft/clean live. - Knobs: `internalWidth/Height`, `upscaleFilter`, `colorDepth`, `dither`, - `vertexSnap`, `textureFilter`, `lighting`, `fog`, `lodDistance`. (Texturing is - always perspective-correct — the affine-swim dial was removed.) -- **`RenderConfig.lodDistance`** — beyond this many world units, a chunk's trees - + boulders draw as cheap impostors (see Performance). ~60 for standard/soft/ps1 - (well inside `fog.far`, so far detail is already fog-dimmed at the switch), - `Infinity` on `clean` to disable LOD. Lower it for more headroom (more pop), - raise it for more far detail (more tris). -- **`game/level.ts` `GROUND_UV`** (0.25) — outdoor ground texture tiles per world - unit. Lower = the stone tiles bigger and less busy = less far-distance moire - (there are no mipmaps); higher = finer but shimmerier. -- **`game/level.ts` `CHUNK_GRID`** (12) / `TERRAIN_SUBDIV` (5) — spatial-cull - granularity and terrain resolution. World terrain divisions = `CHUNK_GRID * - TERRAIN_SUBDIV`. Smaller cells cull tighter (draw less off-screen) but cost more - per-cell tests/bounds. This is the lever if a dense world still lags. - -## Performance / where the frame goes - -The world is dense (hundreds of trees + boulders, ~50k tris) but most of it is -off-screen or fogged each frame, so several things keep it cheap: -- **Frustum culling** (`Frustum` + per-`Chunk` AABB test in `main`) — skips whole - chunks that fall outside the view. Behind you + off to the sides = free. -- **Backface culling** (`draw(..., true)`) — ~halves fill on solid geometry - (terrain, foliage, rock). See the Rasterizer note re winding. -- **Half-res clouds** (`SKY_STEP` in `app/renderer.ts`, default 2) — cloud fbm runs per - pixel and dominated the frame; sampling once per 2×2 block quarters it. -- **Distance LOD** (`RenderConfig.lodDistance`, `Chunk.isFar`) — - past `lodDistance` a chunk's trees + boulders swap to pre-baked low-poly - impostors and its bushes/flowers drop; both meshes are baked once at load, and - the near/far pick is a pure function of camera + chunk bounds, so it costs - nothing per frame and stays worker-safe (no seam). In a dense forest view this - is what tips the per-frame work under the 16.67ms (60fps) vsync budget — it cut - ~1.4x of the triangles in-forest and drops far-tree/rock detail that fog is - already dimming anyway. -- **Flat geometry + zero-alloc raster** — `Mesh` is a flat float array and the - whole per-triangle path uses reused scratch, so a frame allocates ~0 bytes - (measured). Buys frame *consistency* (no GC-pause spikes; worst/mean ~1.3x) and - makes geometry shareable across worker threads. It did **not** raise mean fps — - allocation was never the bottleneck; the mean is the transform+fill **compute**. -- **Multi-threaded rasterization** (`renderer.ts` + `render-worker.ts`) — split the - framebuffer into row bands, one worker each, over a `SharedArrayBuffer`. The - barrier is **lock-free**: workers `Atomics.wait` on a frame counter (no per-frame - messages), main writes camera/matrix/visible-list into shared arrays, `dispatch` - is non-blocking, and `main` polls `done()` on its rAF and presents the finished - frame (present frame N, dispatch N+1). Measured real-browser (`bun run - bench:browser`): **~1.5x median AND ~1.2x p95** vs single-thread, frame time - pinned near vsync. The gotcha is **oversubscription** — too many workers (main + - browser + OS competing) wrecks the p95 tail even as the median improves (6 - workers were far worse than 3); `MAX_WORKERS` caps it, retune per machine. - Requires a cross-origin-isolated page (COOP/COEP; Vite serves them) — else it - falls back to single-thread, so the app never breaks. - -Frustum + backface + half-res sky give ~1.5–2x, workers ~1.5x more, distance LOD -another ~1.4x of the tris in dense views. Together they get the heavy in-forest -view (the worst case) under the 60fps vsync budget on the worker path; the -single-thread fallback still lands ~30fps there. The blunt content dials if it -still lags are `TREE_COUNT`/`BOULDER_COUNT` (less world) and `lodDistance` (more -aggressive impostor swap). - -**Profiling**: `bun run bench:browser` (Playwright) starts Vite, drives headless -Chromium through `?bench=st` and `?bench=mt` (a scripted flythrough with a fixed -camera path), and prints median/p95/max **work time** (critical-path band time) -and **frame time** for both. Headless absolute fps ≠ a real display, but the -single-vs-workers *relative* result and the *tail* (p95/max = jitter) are real — -that's how the worker path was actually validated instead of guessed. - -## Clouds - -Procedural, moving, in the sky pass (`engine/render/Sky.ts`). Two styles picked -by a `CloudLayer` discriminated union `kind`: -- **`basicCumulus`** — flat hard-thresholded white puffs, 1 noise lookup/pixel. -- **`fancyCumulus`** — domain-warped + heightfield-shaded fake volume, - ~5 lookups/pixel (pricier; watch the FPS meter). - -Both are exported presets in `game/level.ts`; the active one is set in -`buildLevel`'s `sky.clouds`. Add new cloud types by extending the union and -branching in the cloud shader. Cost scales with sky resolution — fine at -`standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky). - -## Trees (`game/actors/Tree.ts` + `game/actors/trees/`) - -Procedural low-poly geometry, faceted flat-shaded like everything else. `Tree.ts` -contains only shared placement state. Oak, Spruce, and Birch each expose a concrete -`Prefab` factory from their own module; level data references those definition -objects directly, and shared geometry primitives live in `trees/treekit.ts`. -- **`oak`** — short tapered trunk, a couple of branches, a broad cluster of - lumpy canopy `blob`s (wider than tall, bushy). -- **`spruce`** — tall thin trunk under stacked narrowing `cone` tiers pointing - to a tip (taller than wide, conical). -- **`birch`** — silver birch: tall, slender, near-straight trunk with an airy, - high, slightly drooping canopy (lean silhouette). Uses a separate **white** - `birch` bark texture (brown bark can't stand in), and shares the oak `leaf` - foliage. - -`growth` (0..1) runs **sapling → full grown**: it scales height/girth and adds -canopy blobs (oak/birch) / tiers (spruce); `seed` gives each tree its own wobble. -Each factory receives concrete trunk/foliage `Material` objects and closes over -them. `ChunkBuilder` accumulates meshes by material object, so a forest still -batches into a few draw groups without string keys or a registry. `game/level.ts` -`placeTrees` selects from a weighted list of prefab objects. **Add a species** = -add one concrete prefab module and include its object in level data. - -**Boulders** (`game/actors/Boulder.ts`) work the same way: `Boulder.build` -appends a squashed, per-vertex-jittered low-poly sphere (seam/pole-safe so it -never cracks) into one shared rock mesh, sunk partway into the ground. -`scatterBoulders` sizes them small→big (biased small) and drops colliders on the -big ones. `Bush` (leaf-blob clusters) and `Flower` (stem + colored bloom, atlas -UVs) are the same again — ground detail scattered near the play area, no -colliders; add a new prop type by cloning the pattern (generator + scatter). - -## Controls - -WASD move · **Shift** run (speed from `Player.config` in `game/player.ts`) · mouse -look (click canvas to pointer-lock) · **Space** jump · **1/2/3** switch look -presets. FPS shown bottom-right. The room's north wall is open — walk out onto -the terrain and toward the peaks. - -## Code style (oxlint-enforced — match it) - -No semicolons, double quotes, 2-space indent, trailing commas. **`type`, never -`interface`.** Function declarations (arrows allowed). Uppercase hex (`0xFF`). -Curly braces required on all `if`. No `void` operator. `T[]` not `Array`. -Prefer `globalThis` over `window`. Domain behavior lives in a `type` + -matching `namespace` (see `Vec3`, `Color`, `Rasterizer`). - -## Caveats - -- No mipmaps, so distant textures shimmer (period-correct); far cloud/geometry - is hidden by fog. - -## Debugging the renderer without a browser (key workflow) - -The engine is pure, so you can **render headless to a PNG and inspect it**. A -throwaway `bun` script can: `buildLevel()`, decode `assets/*.png` (they're -filter-0 RGBA — trivial to inflate), set a `Camera` pose, run -`Sky.render` + `Rasterizer.draw` into a `Framebuffer`, encode the color buffer -to PNG (same hand-rolled encoder as `scripts/gen-assets.ts`), write it, and read -it back. Sampling individual pixels this way tuned the clouds, framed the terrain -peaks, and confirmed the ground textures flat (no swim). Put temp scripts in the -job tmp dir, not the repo. - -## Roadmap / not yet built - -In-browser RenderConfig slider panel; mipmaps; `painter` depth mode; gouraud -lighting; more cloud types; more props / a weapon; more mob definitions + smarter mob -behavior (they wander + block/stand-on today, but don't yet react to the player). `shared/` -is nearly empty. The FPS meter is static HTML + `textContent` writes only — no -DOM-built UI yet (deliberate). diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 6091c46..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,29 +0,0 @@ -# Meat Engine - -Meat separates reusable first-person game capabilities from one game's concrete world and creatures. - -## Language - -**Level Definition**: -A complete engine-shaped input for creating one live Level. Game code produces it from concrete values and definitions; it is not the worker rendering payload. -_Avoid_: Scene assembly - -**Level**: -A live playable world created from a Level Definition, including terrain, actors, collision, and rendering state. -_Avoid_: Map, scene - -**Terrain**: -A bounded ground surface that can report height at any horizontal world position. -_Avoid_: Ground mesh - -**Actor Definition**: -A concrete reusable definition of one moving thing's behavior, representation, transform, and physical presence. Actor instances reference definitions directly. -_Avoid_: Mob kind, actor kind, type registry - -**Prefab**: -A concrete reusable definition for static level content, including its geometry and optional physical presence. Placed prefabs reference definitions directly. -_Avoid_: Prop kind, tree kind, species registry - -**Render Scene**: -The behavior-free rendering projection of a Level. -_Avoid_: Level diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..924b5b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright © 2026 Dan Finch + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/adr/0001-engine-owns-world-concepts.md b/docs/adr/0001-engine-owns-world-concepts.md deleted file mode 100644 index bcda3b7..0000000 --- a/docs/adr/0001-engine-owns-world-concepts.md +++ /dev/null @@ -1,3 +0,0 @@ -# Engine owns world concepts - -Engine owns contracts and reusable mechanisms for levels, terrain, actors, prefabs, collision, scene rendering, chunking, LOD, and render transport; game owns concrete level values and concrete actor or prefab definitions. Game content is referenced through definition objects, never closed `MobKind`, `TreeKind`, or equivalent content registries; finite engine capability unions remain allowed. A live `Level` may contain behavior, while its `RenderScene` projection contains only clone-safe data for workers, with numeric prototype indexes confined to engine transport. diff --git a/opencode.json b/opencode.json deleted file mode 100644 index 54d672b..0000000 --- a/opencode.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "instructions": [ - "AGENTS.md", - ".agents/rules/**/*.md" - ], - "skills": { - "paths": [ - ".agents/skills" - ] - }, - "command": { - "sketch": { - "description": "Apply an approved OpenSpec change", - "template": "@.agents/commands/sketch.md\n\nArguments: $ARGUMENTS", - "agent": "build" - }, - "sketch-sync": { - "description": "Apply an approved OpenSpec change and sync Forgejo", - "template": "@.agents/commands/sketch.md\n\nMode: sync\nArguments: $ARGUMENTS", - "agent": "build" - } - } -}