diff --git a/agents.md b/agents.md deleted file mode 100644 index 452c8bd1aa07fe524760a7ad22b84a20ba408577..0000000000000000000000000000000000000000 --- a/agents.md +++ /dev/null @@ -1,88 +0,0 @@ -# VibeGame Engine Context - -You are a VibeGame specialist. VibeGame is a 3D game engine with declarative XML syntax and ECS architecture. Your role is to help users create games efficiently using VibeGame's declarative approach. - -## Core Architecture - -**ECS Pattern**: Entities (IDs) + Components (data) + Systems (logic) -**Declarative XML**: Game entities defined in `` tags -**Auto-Creation**: Engine provides player, camera, lighting by default - -## Essential Syntax - -```xml - - - - - - - - - - -``` - -## Key Recipes & Components - -- `` - Immovable (grounds, walls) -- `` - Gravity-affected (balls, crates) -- `` - Script-controlled (moving platforms) -- ``, `` - Auto-created if missing -- `` - Base with custom components - -## Critical Rules - -⚠️ **Physics Override**: Body position overrides transform position. Always use `pos` on physics entities. - -```xml - - - - - -``` - -## Component Syntax - -```xml - - - - - -``` - -**Shorthands**: `pos`, `color`, `size` auto-expand to matching component properties. - -## Development Commands - -- `bun dev` - Start development server -- `bun run build` - Production build -- `bun run check` - TypeScript validation -- `bun test` - Run tests - -## Features Available - -✅ Physics (Rapier), rendering (Three.js), input, tweening, player controller, orbital camera, collision detection, respawn, post-processing - -❌ Audio, multiplayer, save/load, inventory, AI, particles, custom shaders - -## Documentation Access - -**For detailed information**: Use Context7 to fetch comprehensive docs: -1. `mcp__context7__resolve-library-id` with "/dylanebert/vibegame" -2. `mcp__context7__get-library-docs` with resolved ID - -**Quick Reference**: Shapes (`box`, `sphere`, `cylinder`, `capsule`), Physics (`static`, `dynamic`, `kinematic`), Easing functions, Loop modes (`once`, `loop`, `ping-pong`) - -## Best Practices - -1. Always include ground platforms -2. Use recipes over raw entities -3. Leverage auto-creation defaults -4. Set positions on physics bodies, not transforms -5. Query Context7 for detailed API references -6. Test incrementally - -This provides foundational VibeGame knowledge. Use Context7 for comprehensive documentation and examples. diff --git a/bun.lock b/bun.lock index c2a9ccbd36078498888531dc7ec9418ea950590c..d3504c61273914cd36c5b5bd379a1940217b7626 100644 --- a/bun.lock +++ b/bun.lock @@ -11,13 +11,12 @@ "@langchain/mcp-adapters": "^0.6.0", "@modelcontextprotocol/sdk": "^0.6.0", "@modelcontextprotocol/server-filesystem": "^0.6.2", - "@types/marked": "^6.0.0", "@types/node": "^24.3.3", "gsap": "^3.13.0", - "marked": "^16.2.1", + "htmlparser2": "^10.0.0", "monaco-editor": "^0.50.0", "svelte-splitpanes": "^8.0.5", - "vibegame": "^0.1.7", + "vibegame": "^0.1.9", "zod": "^4.1.8", }, "devDependencies": { @@ -117,7 +116,7 @@ "@huggingface/jinja": ["@huggingface/jinja@0.5.1", "", {}, "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng=="], - "@huggingface/tasks": ["@huggingface/tasks@0.19.45", "", {}, "sha512-lM3QOgbfkGZ5gAZOYWOmzMM6BbKcXOIHjgnUAoymTdZEcEcGSr0vy/LWGEiK+vBXC4vU+sCT+WNoA/JZ8TEWdA=="], + "@huggingface/tasks": ["@huggingface/tasks@0.19.46", "", {}, "sha512-c6F/r7zRQjmyo6Ji8c2TUbdeeu6WAdZxYLRd+G7Xxvfbadi6iDwk2szt/oinC5v5Ljyc2sjzesaqGB6hLWy/DA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -217,8 +216,6 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/marked": ["@types/marked@6.0.0", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="], - "@types/node": ["@types/node@24.5.0", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg=="], "@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="], @@ -373,6 +370,14 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -383,6 +388,8 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -529,6 +536,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], @@ -641,8 +650,6 @@ "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], - "marked": ["marked@16.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], @@ -915,7 +922,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vibegame": ["vibegame@0.1.7", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "postprocessing": "^6.37.8", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-KFzNGi+EnlEWt4R3QKPN5jVJhkgBgMg443Qq3muzSjfrp+GJ1fsHIB8ovYylRFHg8+9P3kSlWmiQ6PlDiZ8msQ=="], + "vibegame": ["vibegame@0.1.9", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.18.2", "gsap": "^3.13.0", "postprocessing": "^6.37.8", "zod": "^4.1.5" }, "peerDependencies": { "bitecs": ">=0.3.40", "three": ">=0.170.0" } }, "sha512-f83vO9BSuG6IjwI0n6xsPYwTg9ApEBmdQBTebf5UT/Si6lgLdX6EElU8CUORb0147IXyyxk5GXzJzLpF9S24NQ=="], "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], @@ -979,6 +986,8 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], diff --git a/layers/structure.md b/layers/structure.md index 5765cadc08d8a1a2278b726741b9b639e86ab10d..d34ce4db1d90ef1200594e0478eb820cd4a89e9c 100644 --- a/layers/structure.md +++ b/layers/structure.md @@ -54,7 +54,7 @@ vibegame/ ├── tsconfig.json # TypeScript configuration ├── vite.config.ts # Vite + Svelte + VibeGame config ├── bun.lock # Dependency lock file -├── agents.md # VibeGame documentation +├── llms.txt # VibeGame documentation └── README.md ``` diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc5acc4ec649363bc9914b4b2c9e80378d972f27 --- /dev/null +++ b/llms.txt @@ -0,0 +1,2430 @@ +# VibeGame Engine + +A 3D game engine with declarative XML syntax and ECS architecture. Start playing immediately with automatic player, camera, and lighting - just add a ground to prevent falling. + +## Core Architecture + +### ECS (Entity Component System) +- **Entities**: Just numbers (IDs), no data or behavior +- **Components**: Pure data containers (position, health, color) +- **Systems**: Functions that process entities with specific components +- **Queries**: Find entities with specific component combinations + +### Plugin System +Bevy-inspired modular architecture: +- **Components**: Data definitions using bitECS +- **Systems**: Logic organized by update phases (setup, fixed, simulation, draw) +- **Recipes**: XML entity templates with preset components +- **Config**: Defaults, shorthands, enums, validations, parsers + +### Update Phases +1. **SetupBatch**: Input gathering and frame setup +2. **FixedBatch**: Physics simulation at fixed timestep (50Hz) +3. **SimulationBatch**: Game logic and state updates +4. **DrawBatch**: Rendering and interpolation + +## Critical Rules + +⚠️ **Physics Position Override**: Physics bodies override transform positions. Always use `pos` attribute on physics entities, not transform position. + +⚠️ **Ground Required**: Always include ground/platforms or the player falls infinitely. + +⚠️ **Component Declaration**: Bare attributes mean "include with defaults", not "empty". + +## Essential Knowledge + +## Instant Playable Game + +```html + + + + + + + + + +``` + +This creates a complete game with: +- ✅ Player character (auto-created) +- ✅ Orbital camera (auto-created) +- ✅ Directional + ambient lighting (auto-created) +- ✅ Ground platform (you provide this) +- ✅ WASD movement, mouse camera, space to jump + +## Development Setup + +After installation with `npm create vibegame@latest my-game`: + +```bash +cd my-game +bun dev # Start dev server with hot reload +``` + +### Project Structure +- **TypeScript** - Full TypeScript support with strict type checking +- **src/main.ts** - Entry point for your game +- **index.html** - HTML template with canvas element +- **vite.config.ts** - Build configuration + +### Commands +- `bun dev` - Development server with hot reload +- `bun run build` - Production build +- `bun run preview` - Preview production build +- `bun run check` - TypeScript type checking +- `bun run lint` - Lint code with ESLint +- `bun run format` - Format code with Prettier + +## Physics Objects + +```xml + + + + + + + + + + + + + +``` + +## CRITICAL: Physics Position vs Transform Position + + +⚠️ **Physics bodies override transform positions!** +Always set position on the body, not the transform, for physics entities. + + +```xml + + + + + + + + +``` + +## ECS Architecture Explained + +Unlike traditional game engines with GameObjects, VibeGame uses Entity-Component-System: + +- **Entities**: Just numbers (IDs), no data or behavior +- **Components**: Pure data containers (position, health, color) +- **Systems**: Functions that process entities with specific components + +```typescript +// Component = Data only +const Health = GAME.defineComponent({ + current: GAME.Types.f32, + max: GAME.Types.f32 +}); + +// System = Logic only +const healthQuery = GAME.defineQuery([Health]); +const DamageSystem: GAME.System = { + update: (state) => { + const entities = healthQuery(state.world); + for (const entity of entities) { + Health.current[entity] -= 1 * state.time.deltaTime; + if (Health.current[entity] <= 0) { + state.destroyEntity(entity); + } + } + } +}; +``` + +## What's Auto-Created (Game Engine Defaults) + +The engine automatically creates these if missing: +1. **Player** - Character with physics, controls, and respawn (at 0, 1, 0) +2. **Camera** - Orbital camera following the player +3. **Lighting** - Ambient + directional light with shadows + +You only need to provide: +- **Ground/platforms** - Or the player falls forever +- **Game objects** - Whatever makes your game unique + +### Override Auto-Creation (When Needed) + +While auto-creation is recommended, you can manually create these for customization: + +```xml + + + + + + + + + + + + + +``` + +**Best Practice**: Use auto-creation unless you specifically need custom positions, properties, or multiple instances. The defaults are well-tuned for most games. + +## Post-Processing Effects + +```xml + + + + + + + + + + + +``` + +## Common Game Patterns + +### Basic Platformer +```xml + + + + + + + + + + + + + + + + + +``` + +### Collectible Coins (Collision-based) +```xml + + + + + + + + + + + + +``` + +### Physics Playground +```xml + + + + + + + + + + + + + + + + + + +``` + +## Recipe Reference + +| Recipe | Purpose | Key Attributes | Common Use | +|--------|---------|---------------|------------| +| `` | Immovable objects | `pos`, `shape`, `size`, `color` | Grounds, walls, platforms | +| `` | Gravity-affected objects | `pos`, `shape`, `size`, `color`, `mass` | Balls, crates, falling objects | +| `` | Script-controlled physics | `pos`, `shape`, `size`, `color` | Moving platforms, doors | +| `` | Player character | `pos`, `speed`, `jump-height` | Main character (auto-created) | +| `` | Base entity | Any components via attributes | Custom entities | + +### Shape Options +- `box` - Rectangular solid (default) +- `sphere` - Ball shape +- `cylinder` - Cylindrical shape +- `capsule` - Pill shape (good for characters) + +### Size Attribute +- Box: `size="width height depth"` or `size="2 1 2"` +- Sphere: `size="diameter"` or `size="1"` +- Cylinder: `size="diameter height"` or `size="1 2"` +- Broadcast: `size="2"` becomes `size="2 2 2"` + +## How Recipes and Shorthands Work + +### Everything is an Entity +Every XML tag creates an entity. Recipes like `` are just shortcuts for `` with preset components. + +```xml + + + + +``` + +### Component Attributes +Components are declared using bare attributes (no value means "use defaults"): + +```xml + + + + + + + + + + + +``` + +**Important**: Bare attributes like `transform` mean "include this component with default values", NOT "empty" or "disabled". + +### Automatic Shorthand Expansion +Shorthands expand to ANY component with matching properties: + +```xml + + + + + + + + + + + + + +``` + +### Recipe Internals +Recipes are registered component bundles with defaults: + +```xml + + + collider + renderer + respawn +> + + + + + + + collider + renderer="color: 0xff0000" + respawn +> +``` + +### Common Pitfall: Component Requirements +```xml + + + + + + + + +``` + +### Best Practices Summary +1. **Use recipes** (``, ``, etc.) instead of raw `` tags +2. **Use shorthands** (`pos`, `size`, `color`) for cleaner code +3. **Override only what you need** - recipes have good defaults +4. **Mix recipes with custom components** - e.g., `` + +## Currently Supported Features + +### ✅ What Works Well +- **Basic platforming** - Jump puzzles, obstacle courses +- **Physics interactions** - Balls, dominoes, stacking +- **Moving platforms** - Via kinematic bodies + tweening +- **Collectibles** - Using collision detection in systems +- **Third-person character control** - WASD + mouse camera +- **Gamepad support** - Xbox/PlayStation controllers +- **Visual effects** - Tweening colors, positions, rotations +- **Post-processing** - Bloom, dithering, and tonemapping effects for visual styling +- **Game UI/HUD** - HTML/CSS overlays with GSAP animations, ECS state integration + +### Example Prompts That Work +- "Create a platformer with moving platforms and collectible coins" +- "Make bouncing balls that collide with walls" +- "Build an obstacle course with rotating platforms" +- "Add falling crates that stack up" +- "Create a simple parkour level" +- "Add a score display and upgrade menu with animations" +- "Create a game with currency system and floating text effects" + +### Troubleshooting + +- **Physics not working?** → Check if ground exists, verify `` tag +- **Entity not appearing?** → Verify transform component, check position values +- **Movement feels wrong?** → Physics body position overrides transform position +- **Player falling forever?** → Add a ground/platform with `` + +## Plugin Development Pattern + +```typescript +// Component Definition +export const MyComponent = GAME.defineComponent({ + value: GAME.Types.f32, + enabled: GAME.Types.ui8 +}); + +// System Definition +const myQuery = GAME.defineQuery([MyComponent]); +export const MySystem: GAME.System = { + group: 'simulation', // or 'setup', 'fixed', 'draw' + update: (state) => { + const entities = myQuery(state.world); + for (const entity of entities) { + // System logic + MyComponent.value[entity] += state.time.deltaTime; + } + } +}; + +// Plugin Bundle +export const MyPlugin: GAME.Plugin = { + components: { MyComponent }, + systems: [MySystem], + config: { + defaults: { "my-component": { value: 1, enabled: 1 } }, + shorthands: { "my-val": "my-component.value" } + } +}; + +// Registration +GAME.withPlugin(MyPlugin).run(); +``` + +## State Management Patterns + +### Singleton Entities (Game State) +```typescript +function getOrCreateGameState(state: GAME.State): number { + const query = GAME.defineQuery([GameState]); + const entities = query(state.world); + if (entities.length > 0) return entities[0]; + + const entity = state.createEntity(); + state.addComponent(entity, GameState, { score: 0, level: 1 }); + return entity; +} +``` + +### Component Access (bitECS style) +```typescript +// Direct property access +GAME.Transform.posX[entity] = 10; +GAME.Transform.posY[entity] = 5; + +// Read values +const x = GAME.Transform.posX[entity]; +const health = GAME.Health.current[entity]; +``` + +## Features Not Yet Built-In + +### ❌ Engine Features Not Available +- **Multiplayer/Networking** - No server sync +- **Sound/Audio** - No audio system yet +- **Save/Load** - No persistence system +- **Inventory** - No item management system built-in (but easily implementable with UI) +- **Dialog/NPCs** - No conversation system built-in (but easily implementable with UI) +- **AI/Pathfinding** - No enemy AI +- **Particles** - No particle effects (though UI can create particle-like effects) +- **Custom shaders** - Fixed rendering pipeline +- **Terrain** - Use box platforms instead + +### Recommended Approaches +- **Complex UI** → HTML/CSS overlays (this is actually superior to most game engines) +- **Animations** → GSAP for smooth transitions and effects +- **Level progression** → Reload with different XML or hide/show worlds +- **Enemy behavior** → Tweened movement patterns +- **Interactions** → Collision detection in custom systems + +## Common Mistakes to Avoid + +### ❌ Forgetting the Ground +```xml + + + + +``` + +### ❌ Setting Transform Position on Physics Objects +```xml + + + + + + + + +``` + +### ❌ Missing World Tag +```xml + + + + + + + +``` + +### ❌ Wrong Physics Type +```xml + + + + + + + + + +``` + +## Custom Components and Systems + +### Creating a Health System +```typescript +import * as GAME from 'vibegame'; + +// Define the component +const Health = GAME.defineComponent({ + current: GAME.Types.f32, + max: GAME.Types.f32 +}); + +// Create the system +const HealthSystem: GAME.System = { + update: (state) => { + const entities = GAME.defineQuery([Health])(state.world); + for (const entity of entities) { + // Regenerate health over time + if (Health.current[entity] < Health.max[entity]) { + Health.current[entity] += 5 * state.time.deltaTime; + } + } + } +}; + +// Bundle as plugin +const HealthPlugin: GAME.Plugin = { + components: { Health }, + systems: [HealthSystem], + config: { + defaults: { + "health": { current: 100, max: 100 } + } + } +}; + +// Use in game +GAME.withPlugin(HealthPlugin).run(); +``` + +### Using in XML +```xml + + + + +``` + +## State API Reference + +Available in all systems via the `state` parameter: + +### Entity Management +- `createEntity(): number` - Create new entity +- `destroyEntity(entity: number)` - Remove entity +- `query(...Components): number[]` - Find entities with components + +### Component Operations +- `addComponent(entity, Component, data?)` - Add component +- `removeComponent(entity, Component)` - Remove component +- `hasComponent(entity, Component): boolean` - Check component +- `getComponent(name: string): Component | null` - Get by name + +### Time +- `time.delta: number` - Frame time in seconds +- `time.elapsed: number` - Total time in seconds +- `time.fixed: number` - Fixed timestep (1/50) + +### Physics Helpers +- `addComponent(entity, ApplyImpulse, {x, y, z})` - One-time push +- `addComponent(entity, ApplyForce, {x, y, z})` - Continuous force +- `addComponent(entity, KinematicMove, {x, y, z})` - Move kinematic + +## Plugin System + +### Using Specific Plugins +```typescript +import * as GAME from 'vibegame'; + +// Start with no plugins +GAME + .withoutDefaultPlugins() + .withPlugin(TransformsPlugin) // Just transforms + .withPlugin(RenderingPlugin) // Add rendering + .withPlugin(PhysicsPlugin) // Add physics + .run(); +``` + +### Default Plugin Bundle +- **RecipesPlugin** - XML parsing and entity creation +- **TransformsPlugin** - Position, rotation, scale, hierarchy +- **RenderingPlugin** - Three.js meshes, lights, camera +- **PhysicsPlugin** - Rapier physics simulation +- **InputPlugin** - Keyboard, mouse, gamepad input +- **OrbitCameraPlugin** - Third-person camera +- **PlayerPlugin** - Character controller +- **TweenPlugin** - Animation system +- **RespawnPlugin** - Fall detection and reset +- **StartupPlugin** - Auto-create player/camera/lights + +## Plugin Reference + +### Core + +Math utilities for interpolation and 3D transformations. + +### Functions + +#### lerp(a, b, t): number +Linear interpolation + +#### slerp(fromX, fromY, fromZ, fromW, toX, toY, toZ, toW, t): Quaternion +Quaternion spherical interpolation + +#### Examples + +## Usage Note + +Math utilities are used internally by systems like tweening and transforms. For animating properties, use the Tween system instead of directly calling interpolation functions. For transformations, use the Transform component's euler angles which are automatically converted to quaternions by the system. + +### Animation + +Procedural character animation with body parts that respond to movement states. + +### Components + +#### AnimatedCharacter +- headEntity: eid +- torsoEntity: eid +- leftArmEntity: eid +- rightArmEntity: eid +- leftLegEntity: eid +- rightLegEntity: eid +- phase: f32 - Walk cycle phase (0-1) +- jumpTime: f32 +- fallTime: f32 +- animationState: ui8 - 0=IDLE, 1=WALKING, 2=JUMPING, 3=FALLING, 4=LANDING +- stateTransition: f32 + +#### HasAnimator +Tag component (no properties) + +### Systems + +#### AnimatedCharacterInitializationSystem +- Group: setup +- Creates body part entities for AnimatedCharacter components + +#### AnimatedCharacterUpdateSystem +- Group: simulation +- Updates character animation based on movement and physics state + +#### Examples + +## Examples + +### Basic Usage + +```typescript +import * as GAME from 'vibegame'; + +// Add animated character to a player entity +const player = state.createEntity(); +state.addComponent(player, GAME.AnimatedCharacter); +state.addComponent(player, GAME.CharacterController); +state.addComponent(player, GAME.Transform); + +// The AnimatedCharacterInitializationSystem will automatically +// create body parts in the next setup phase +``` + +### Accessing Animation State + +```typescript +import * as GAME from 'vibegame'; + +const characterQuery = GAME.defineQuery([GAME.AnimatedCharacter]); +const MySystem: GAME.System = { + update: (state) => { + const characters = characterQuery(state.world); + for (const entity of characters) { + const animState = GAME.AnimatedCharacter.animationState[entity]; + if (animState === 2) { // JUMPING + console.log('Character is jumping!'); + } + } + } +}; +``` + +### XML Declaration + +```xml + + +``` + +### Input + +Focus-aware input handling for mouse, keyboard, and gamepad with buffered actions. Keyboard input only responds when canvas has focus. + +### Components + +#### InputState +- moveX: f32 - Horizontal axis (-1 left, 1 right) +- moveY: f32 - Forward/backward (-1 back, 1 forward) +- moveZ: f32 - Vertical axis (-1 down, 1 up) +- lookX: f32 - Mouse delta X +- lookY: f32 - Mouse delta Y +- scrollDelta: f32 - Mouse wheel delta +- jump: ui8 - Jump available (0/1) +- primaryAction: ui8 - Primary action (0/1) +- secondaryAction: ui8 - Secondary action (0/1) +- leftMouse: ui8 - Left button (0/1) +- rightMouse: ui8 - Right button (0/1) +- middleMouse: ui8 - Middle button (0/1) +- jumpBufferTime: f32 +- primaryBufferTime: f32 +- secondaryBufferTime: f32 + +### Systems + +#### InputSystem +- Group: simulation +- Updates InputState components with current input data + +### Functions + +#### setTargetCanvas(canvas: HTMLCanvasElement | null): void +Registers canvas for focus-based keyboard input + +#### consumeJump(): boolean +Consumes buffered jump input + +#### consumePrimary(): boolean +Consumes buffered primary action + +#### consumeSecondary(): boolean +Consumes buffered secondary action + +#### handleMouseMove(event: MouseEvent): void +Processes mouse movement + +#### handleMouseDown(event: MouseEvent): void +Processes mouse button press + +#### handleMouseUp(event: MouseEvent): void +Processes mouse button release + +#### handleWheel(event: WheelEvent): void +Processes mouse wheel + +### Constants + +#### INPUT_CONFIG +Default input mappings and sensitivity settings + +#### Examples + +## Examples + +### Basic Plugin Registration + +```typescript +import * as GAME from 'vibegame'; + +GAME + .withPlugin(GAME.InputPlugin) + .run(); +``` + +### Reading Input in a Custom System + +```typescript +import * as GAME from 'vibegame'; + +const playerQuery = GAME.defineQuery([GAME.Player, GAME.InputState]); +const PlayerControlSystem: GAME.System = { + update: (state) => { + const players = playerQuery(state.world); + + for (const player of players) { + // Read movement axes + const moveX = GAME.InputState.moveX[player]; + const moveY = GAME.InputState.moveY[player]; + + // Check for jump + if (GAME.InputState.jump[player]) { + // Jump is available this frame + } + + // Check mouse buttons + if (GAME.InputState.leftMouse[player]) { + // Left mouse is held + } + } + } +}; +``` + +### Consuming Buffered Actions + +```typescript +import * as GAME from 'vibegame'; + +const CombatSystem: GAME.System = { + update: (state) => { + // Consume jump if available (prevents double consumption) + if (GAME.consumeJump()) { + // Perform jump + velocity.y = JUMP_FORCE; + } + + // Consume primary action + if (GAME.consumePrimary()) { + // Fire weapon + spawnProjectile(); + } + } +}; +``` + +### Custom Input Mappings + +```typescript +import * as GAME from 'vibegame'; + +// Modify before starting the game +GAME.INPUT_CONFIG.mappings.jump = ['Space', 'KeyX']; +GAME.INPUT_CONFIG.mappings.moveForward = ['KeyW', 'KeyZ', 'ArrowUp']; +GAME.INPUT_CONFIG.mouseSensitivity.look = 0.3; + +GAME.run(); +``` + +### Manual Event Handling + +```typescript +import * as GAME from 'vibegame'; + +// Use the exported handlers directly if needed +canvas.addEventListener('mousedown', GAME.handleMouseDown); +canvas.addEventListener('mouseup', GAME.handleMouseUp); +``` + +### Orbit Camera + +Orbital camera controller for third-person views and smooth target following. + +### Components + +#### OrbitCamera +- target: eid (0) - Target entity ID +- current-yaw: f32 (π) - Current horizontal angle +- current-pitch: f32 (π/6) - Current vertical angle +- current-distance: f32 (4) - Current distance +- target-yaw: f32 (π) - Target horizontal angle +- target-pitch: f32 (π/6) - Target vertical angle +- target-distance: f32 (4) - Target distance +- min-distance: f32 (1) +- max-distance: f32 (25) +- min-pitch: f32 (0) +- max-pitch: f32 (π/2) +- smoothness: f32 (0.5) - Interpolation speed +- offset-x: f32 (0) +- offset-y: f32 (1.25) +- offset-z: f32 (0) + +### Systems + +#### OrbitCameraSystem +- Group: draw +- Updates camera position and rotation around target + +### Recipes + +#### camera +- Creates orbital camera with default settings +- Components: orbit-camera, transform, world-transform, main-camera + +#### Examples + +## Examples + +### Basic Camera + +```xml + + +``` + +### Camera Following Player + +```xml + + + + + + + +``` + +### Custom Orbit Settings + +```xml + +``` + +### Programmatic Usage + +```typescript +import * as GAME from 'vibegame'; + +const cameraQuery = GAME.defineQuery([GAME.OrbitCamera]); +const CameraControlSystem = { + update: (state) => { + const cameras = cameraQuery(state.world); + + for (const camera of cameras) { + // Rotate camera on input + if (state.input.mouse.deltaX) { + GAME.OrbitCamera.targetYaw[camera] += state.input.mouse.deltaX * 0.01; + } + + // Zoom on scroll + if (state.input.mouse.wheel) { + GAME.OrbitCamera.targetDistance[camera] = Math.max( + GAME.OrbitCamera.minDistance[camera], + Math.min( + GAME.OrbitCamera.maxDistance[camera], + GAME.OrbitCamera.targetDistance[camera] - state.input.mouse.wheel * 0.5 + ) + ); + } + } + } +}; +``` + +### Dynamic Target Switching + +```typescript +import * as GAME from 'vibegame'; + +// Switch camera target +const switchTarget = (state, cameraEntity, newTargetEntity) => { + GAME.OrbitCamera.target[cameraEntity] = newTargetEntity; +}; +``` + +### Physics + +3D physics simulation with Rapier including rigid bodies, collisions, and character controllers. + +### Constants + +- DEFAULT_GRAVITY: -60 + +### Enums + +#### BodyType +- Dynamic = 0 - Affected by forces +- Fixed = 1 - Immovable static +- KinematicPositionBased = 2 - Script position +- KinematicVelocityBased = 3 - Script velocity + +#### ColliderShape +- Box = 0 +- Sphere = 1 +- Capsule = 2 + +### Components + +#### PhysicsWorld +- gravityX: f32 (0) +- gravityY: f32 (-60) +- gravityZ: f32 (0) + +#### Body +- type: ui8 - BodyType enum (Fixed) +- mass: f32 (1) +- linearDamping: f32 (0) +- angularDamping: f32 (0) +- gravityScale: f32 (1) +- ccd: ui8 (0) +- lockRotX: ui8 (0) +- lockRotY: ui8 (0) +- lockRotZ: ui8 (0) +- posX, posY, posZ: f32 +- rotX, rotY, rotZ, rotW: f32 (rotW=1) +- eulerX, eulerY, eulerZ: f32 +- velX, velY, velZ: f32 +- rotVelX, rotVelY, rotVelZ: f32 +- lastPosX, lastPosY, lastPosZ: f32 + +#### Collider +- shape: ui8 - ColliderShape enum (Box) +- sizeX, sizeY, sizeZ: f32 (1) +- radius: f32 (0.5) +- height: f32 (1) +- friction: f32 (0.5) +- restitution: f32 (0) +- density: f32 (1) +- isSensor: ui8 (0) +- membershipGroups: ui16 (0xffff) +- filterGroups: ui16 (0xffff) +- posOffsetX, posOffsetY, posOffsetZ: f32 +- rotOffsetX, rotOffsetY, rotOffsetZ, rotOffsetW: f32 (rotOffsetW=1) + +#### CharacterController +- offset: f32 (0.08) +- maxSlope: f32 (45°) +- maxSlide: f32 (30°) +- snapDist: f32 (0.5) +- autoStep: ui8 (1) +- maxStepHeight: f32 (0.3) +- minStepWidth: f32 (0.05) +- upX, upY, upZ: f32 (upY=1) +- moveX, moveY, moveZ: f32 +- grounded: ui8 +- platform: eid - Entity the character is standing on +- platformVelX, platformVelY, platformVelZ: f32 +- platformDeltaX, platformDeltaY, platformDeltaZ: f32 + +#### CharacterMovement +- desiredVelX, desiredVelY, desiredVelZ: f32 +- velocityY: f32 +- actualMoveX, actualMoveY, actualMoveZ: f32 + +#### InterpolatedTransform +- prevPosX, prevPosY, prevPosZ: f32 +- prevRotX, prevRotY, prevRotZ, prevRotW: f32 +- posX, posY, posZ: f32 +- rotX, rotY, rotZ, rotW: f32 + +#### Force/Impulse Components +- ApplyForce: x, y, z (f32) +- ApplyTorque: x, y, z (f32) +- ApplyImpulse: x, y, z (f32) +- ApplyAngularImpulse: x, y, z (f32) +- SetLinearVelocity: x, y, z (f32) +- SetAngularVelocity: x, y, z (f32) +- KinematicMove: x, y, z (f32) +- KinematicRotate: x, y, z, w (f32) + +#### Collision Events +- CollisionEvents: activeEvents (ui8) +- TouchedEvent: other, handle1, handle2 (ui32) +- TouchEndedEvent: other, handle1, handle2 (ui32) + +### Systems + +- PhysicsWorldSystem - Initializes physics world +- PhysicsInitializationSystem - Creates bodies and colliders +- PhysicsCleanupSystem - Removes physics on entity destroy +- PlatformDeltaSystem - Tracks platform position changes +- CharacterMovementSystem - Character controller movement with platform sticking +- CollisionEventCleanupSystem - Clears collision events +- ApplyForcesSystem - Applies forces +- ApplyTorquesSystem - Applies torques +- ApplyImpulsesSystem - Applies impulses +- ApplyAngularImpulsesSystem - Applies angular impulses +- SetVelocitySystem - Sets velocities +- TeleportationSystem - Instant position changes +- KinematicMovementSystem - Kinematic movement +- PhysicsStepSystem - Steps simulation +- PhysicsRapierSyncSystem - Syncs Rapier to ECS +- PhysicsInterpolationSystem - Interpolates for rendering + +### Functions + +#### initializePhysics(): Promise +Initializes Rapier WASM physics engine + +### Recipes + +- static-part - Immovable physics objects +- dynamic-part - Gravity-affected objects +- kinematic-part - Script-controlled objects + +#### Examples + +## Examples + +### Basic Usage + +#### XML Recipes + +##### Static Floor +```xml + +``` + +##### Dynamic Ball +```xml + +``` + +##### Moving Platform +```xml + + + + +``` + +##### Character with Controller +```xml + +``` + +#### JavaScript API + +##### Create Physics Entity +```typescript +import * as GAME from 'vibegame'; + +// Create a dynamic physics box +const entity = state.createEntity(); + +state.addComponent(entity, GAME.Body, { + type: GAME.BodyType.Dynamic, + mass: 5, + posX: 0, posY: 10, posZ: 0 +}); + +state.addComponent(entity, GAME.Collider, { + shape: GAME.ColliderShape.Box, + sizeX: 1, sizeY: 1, sizeZ: 1, + friction: 0.7, + restitution: 0.3 +}); + +// Note: Physics body won't exist until next fixed update +// Transform will be overwritten by Body position after initialization +``` + +##### Moving Physics Bodies + +```typescript +import * as GAME from 'vibegame'; + +// Dynamic bodies - Use forces/impulses for movement +if (GAME.Body.type[entity] === GAME.BodyType.Dynamic) { + // Apply force for gradual acceleration + state.addComponent(entity, GAME.ApplyForce, { x: 10, y: 0, z: 0 }); + + // Apply impulse for instant velocity change + state.addComponent(entity, GAME.ApplyImpulse, { x: 0, y: 50, z: 0 }); + + // Direct position setting only for teleportation + GAME.Body.posX[entity] = 10; // Teleport - use sparingly +} + +// Kinematic bodies - Direct control via movement components +if (GAME.Body.type[entity] === GAME.BodyType.KinematicPositionBased) { + state.addComponent(entity, GAME.KinematicMove, { x: 5, y: 2, z: 0 }); +} + +if (GAME.Body.type[entity] === GAME.BodyType.KinematicVelocityBased) { + state.addComponent(entity, GAME.SetLinearVelocity, { x: 3, y: 0, z: 0 }); +} + +// Never modify Transform directly for physics entities +// GAME.Transform.posX[entity] = 10; // ❌ Will be overwritten by Body +``` + +##### Apply Forces +```typescript +import * as GAME from 'vibegame'; + +// Apply upward impulse (jump) +state.addComponent(entity, GAME.ApplyImpulse, { + x: 0, y: 50, z: 0 +}); + +// Apply continuous force +state.addComponent(entity, GAME.ApplyForce, { + x: 10, y: 0, z: 0 +}); + +// Set velocity directly +state.addComponent(entity, GAME.SetLinearVelocity, { + x: 0, y: 5, z: 0 +}); +``` + +##### Handle Collisions +```typescript +import * as GAME from 'vibegame'; + +const touchedQuery = GAME.defineQuery([GAME.TouchedEvent]); +const CollisionSystem: GAME.System = { + update: (state) => { + // Query entities with collision events + for (const entity of touchedQuery(state.world)) { + const otherEntity = GAME.TouchedEvent.other[entity]; + console.log(`Entity ${entity} collided with ${otherEntity}`); + + // React to collision + state.addComponent(entity, GAME.ApplyImpulse, { + x: 0, y: 10, z: 0 + }); + } + } +}; +``` + +##### Character Movement +```typescript +import * as GAME from 'vibegame'; + +const PlayerMovementSystem: GAME.System = { + update: (state) => { + const movementQuery = GAME.defineQuery([GAME.CharacterMovement, GAME.CharacterController]); + for (const entity of movementQuery(state.world)) { + // Set desired movement based on input + GAME.CharacterMovement.desiredVelX[entity] = input.x * 5; + GAME.CharacterMovement.desiredVelZ[entity] = input.z * 5; + + // Jump if grounded + if (GAME.CharacterController.grounded[entity] && input.jump) { + GAME.CharacterMovement.velocityY[entity] = 10; + } + } + } +}; +``` + +##### Custom Plugin Integration +```typescript +import * as GAME from 'vibegame'; + +// Initialize physics before running +await GAME.initializePhysics(); + +// Use with builder +GAME + .withPlugin(GAME.PhysicsPlugin) + .run(); +``` + +### Player + +Complete player character controller with physics movement, jumping, and platform momentum preservation. + +### Components + +#### Player +- speed: f32 (5.3) +- jumpHeight: f32 (2.3) +- rotationSpeed: f32 (10) +- canJump: ui8 (1) +- isJumping: ui8 (0) +- jumpCooldown: f32 (0) +- lastGroundedTime: f32 (0) +- jumpBufferTime: f32 (-10000) +- cameraSensitivity: f32 (0.007) +- cameraZoomSensitivity: f32 (1.5) +- cameraEntity: eid (0) +- inheritedVelX: f32 (0) - Horizontal momentum inherited from platform +- inheritedVelZ: f32 (0) - Horizontal momentum inherited from platform + +### Systems + +#### PlayerMovementSystem +- Group: fixed +- Handles movement, rotation, jumping with platform momentum preservation + +#### PlayerGroundedSystem +- Group: fixed +- Tracks grounded state, jump availability, and clears inherited momentum on landing + +#### PlayerCameraLinkingSystem +- Group: simulation +- Links player to orbit camera + +#### PlayerCameraControlSystem +- Group: simulation +- Camera control via mouse input + +### Recipes + +#### player +- Complete player setup with physics +- Components: player, character-movement, transform, world-transform, body, collider, character-controller, input-state, respawn + +### Functions + +#### processInput(moveForward, moveRight, cameraYaw): Vector3 +Converts input to world-space movement + +#### handleJump(entity, jumpPressed, currentTime): number +Processes jump with buffering + +#### updateRotation(entity, inputVector, deltaTime, rotationData): Quaternion +Smooth rotation towards movement + +#### Examples + +## Examples + +### Basic Player Usage (XML) + +```xml + + + + +``` + +### Custom Player Configuration (XML) + +```xml + + + +``` + +### Accessing Player Component (JavaScript) + +```typescript +import * as GAME from 'vibegame'; + +const playerQuery = GAME.defineQuery([GAME.Player]); +const MySystem: GAME.System = { + update: (state) => { + const players = playerQuery(state.world); + for (const entity of players) { + // Check if player is jumping + if (GAME.Player.isJumping[entity]) { + console.log('Player is airborne!'); + } + + // Modify player speed + GAME.Player.speed[entity] = 10; + } + } +}; +``` + +### Creating Player Programmatically + +```typescript +import * as GAME from 'vibegame'; + +const PlayerSpawnSystem: GAME.System = { + setup: (state) => { + // Create player entity + const player = state.createEntity(); + + // Add player recipe components + state.addComponent(player, GAME.Player, { + speed: 7, + jumpHeight: 3.5, + cameraSensitivity: 0.01 + }); + + // Add required components + state.addComponent(player, GAME.Transform, { posY: 5 }); + state.addComponent(player, GAME.Body, { type: GAME.BodyType.KinematicPositionBased }); + state.addComponent(player, GAME.CharacterController); + state.addComponent(player, GAME.InputState); + } +}; +``` + +### Movement Controls + +**Keyboard:** +- W/S or Arrow Up/Down - Move forward/backward +- A/D or Arrow Left/Right - Move left/right +- Space - Jump + +**Mouse:** +- Right-click + drag - Rotate camera +- Scroll wheel - Zoom in/out + +### Plugin Registration + +```typescript +import * as GAME from 'vibegame'; + +GAME + .withPlugin(GAME.PlayerPlugin) // Included in defaults + .run(); +``` + +### Rendering + +Three.js rendering pipeline with meshes, lights, cameras, and post-processing effects. + +### Components + +#### Renderer +- shape: ui8 - 0=box, 1=sphere, 2=cylinder, 3=plane +- sizeX, sizeY, sizeZ: f32 (1) +- color: ui32 (0xffffff) +- visible: ui8 (1) + +#### RenderContext +- clearColor: ui32 (0x000000) +- hasCanvas: ui8 + +#### MainCamera +Tag component (no properties) + +#### Ambient +- skyColor: ui32 (0x87ceeb) +- groundColor: ui32 (0x4a4a4a) +- intensity: f32 (0.6) + +#### Directional +- color: ui32 (0xffffff) +- intensity: f32 (1) +- castShadow: ui8 (1) +- shadowMapSize: ui32 (4096) +- directionX: f32 (-1) +- directionY: f32 (2) +- directionZ: f32 (-1) +- distance: f32 (30) + +#### Bloom +- intensity: f32 (1.0) - Bloom intensity +- luminanceThreshold: f32 (1.0) - Luminance threshold for bloom +- luminanceSmoothing: f32 (0.03) - Smoothness of luminance threshold +- mipmapBlur: ui8 (1) - Enable mipmap blur +- radius: f32 (0.85) - Blur radius for mipmap blur +- levels: ui8 (8) - Number of MIP levels for mipmap blur + +#### Dithering +- colorBits: ui8 (4) - Bits per color channel (1-8) +- intensity: f32 (1.0) - Effect intensity (0-1) +- grayscale: ui8 (0) - Enable grayscale mode (0/1) +- scale: f32 (1.0) - Pattern scale (higher = coarser dithering) +- noise: f32 (1.0) - Noise threshold intensity + +### Systems + +#### MeshInstanceSystem +- Group: draw +- Synchronizes transforms with Three.js meshes + +#### LightSyncSystem +- Group: draw +- Updates Three.js lights + +#### CameraSyncSystem +- Group: draw +- Updates Three.js camera and manages post-processing effects + +#### WebGLRenderSystem +- Group: draw (last) +- Renders scene through EffectComposer + +### Functions + +#### setCanvasElement(entity, canvas): void +Associates canvas with RenderContext + +### Recipes + +- ambient-light - Ambient hemisphere lighting +- directional-light - Directional light with shadows +- light - Both ambient and directional + +#### Examples + +## Examples + +### Basic Rendering Setup + +```xml + + + + + + + + + + + +``` + +### Custom Lighting + +```xml + + + + +``` + +### Imperative Usage + +```typescript +import * as GAME from 'vibegame'; + +// Create rendered entity programmatically +const entity = state.createEntity(); + +// Add transform for positioning +state.addComponent(entity, GAME.Transform, { + posX: 0, posY: 5, posZ: 0 +}); + +// Add renderer component +state.addComponent(entity, GAME.Renderer, { + shape: 1, // sphere + sizeX: 2, + sizeY: 2, + sizeZ: 2, + color: 0xff00ff, + visible: 1 +}); + +// Set canvas for rendering context +const contextQuery = GAME.defineQuery([GAME.RenderContext]); +const contextEntity = contextQuery(state.world)[0]; +const canvas = document.getElementById('game-canvas'); +GAME.setCanvasElement(contextEntity, canvas); +``` + +### Shape Types + +```typescript +import * as GAME from 'vibegame'; + +// Available shape enums +const shapes = { + box: 0, + sphere: 1, + cylinder: 2, + plane: 3 +}; + +// Use in XML + + +// Or with enum names + +``` + +### Visibility Control + +```typescript +import * as GAME from 'vibegame'; + +// Hide/show entities +GAME.Renderer.visible[entity] = 0; // Hide +GAME.Renderer.visible[entity] = 1; // Show + +// In XML + +``` + +### Post-Processing Effects + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Respawn + +Automatic respawn system that resets entities when falling below Y=-100. + +### Components + +#### Respawn +- posX, posY, posZ: f32 - Spawn position +- eulerX, eulerY, eulerZ: f32 - Spawn rotation (degrees) + +### Systems + +#### RespawnSystem +- Group: simulation +- Resets entities when Y < -100 + +#### Examples + +## Examples + +### Player with Respawn (Automatic) + +The `` recipe automatically includes respawn: + +```xml + + + + +``` + +### Manual Respawn Component + +```xml + + + +``` + +### Imperative Usage + +```typescript +import * as GAME from 'vibegame'; + +// Add respawn to an entity +const entity = state.createEntity(); + +// Set spawn point from current transform +state.addComponent(entity, GAME.Transform, { + posX: 0, posY: 10, posZ: 0, + eulerX: 0, eulerY: 0, eulerZ: 0 +}); + +state.addComponent(entity, GAME.Respawn, { + posX: 0, posY: 10, posZ: 0, + eulerX: 0, eulerY: 0, eulerZ: 0 +}); + +// Entity will respawn at (0,10,0) when falling +``` + +### Update Spawn Point + +```typescript +import * as GAME from 'vibegame'; + +// Change respawn position dynamically +GAME.Respawn.posX[entity] = 20; +GAME.Respawn.posY[entity] = 5; +GAME.Respawn.posZ[entity] = -10; +``` + +### XML with Transform Sync + +Position attributes automatically populate the respawn component: + +```xml + + +``` + +### Startup + +Auto-creates player, camera, and lighting entities at startup if missing. + +### Systems + +#### LightingStartupSystem +- Group: setup +- Creates default lighting if none exists + +#### CameraStartupSystem +- Group: setup +- Creates main camera if none exists + +#### PlayerStartupSystem +- Group: setup +- Creates player entity if none exists + +#### PlayerCharacterSystem +- Group: setup +- Adds animated character to players + +#### Examples + +## Examples + +### Basic Usage (Auto-Creation) + +```typescript +// The plugin automatically creates defaults when included +import * as GAME from 'vibegame'; + +// This will create player, camera, and lighting automatically +GAME.run(); // Uses DefaultPlugins which includes StartupPlugin +``` + +### Preventing Auto-Creation with XML + +```xml + + + + + + + +``` + +### Manual Plugin Registration + +```typescript +import * as GAME from 'vibegame'; + +// Use startup plugin without other defaults +GAME.withoutDefaultPlugins() + .withPlugin(GAME.TransformsPlugin) + .withPlugin(GAME.RenderingPlugin) + .withPlugin(GAME.StartupPlugin) + .run(); +``` + +### System Behavior + +The startup systems are idempotent - they check for existing entities before creating: + +```typescript +import * as GAME from 'vibegame'; + +// First run: Creates player, camera, lights +const playerQuery = GAME.defineQuery([GAME.Player]); +playerQuery(state.world).length // 0 -> creates player + +// Subsequent runs: Skips creation +playerQuery(state.world).length // 1 -> skips creation +``` + +### Transforms + +3D transforms with position, rotation, scale, and parent-child hierarchies. + +### Components + +#### Transform +- posX, posY, posZ: f32 (0) +- rotX, rotY, rotZ, rotW: f32 (rotW=1) - Quaternion +- eulerX, eulerY, eulerZ: f32 (0) - Degrees +- scaleX, scaleY, scaleZ: f32 (1) + +#### WorldTransform +- Same properties as Transform +- Auto-computed from hierarchy (read-only) + +### Systems + +#### TransformHierarchySystem +- Group: simulation (last) +- Syncs euler/quaternion and computes world transforms + +#### Examples + +## Examples + +### Basic Usage + +#### XML Position and Rotation +```xml + + + + + + + + + + + +``` + +#### JavaScript API +```typescript +import * as GAME from 'vibegame'; + +// In a system +const MySystem = { + update: (state) => { + const entity = state.createEntity(); + + // Add transform component with initial values + state.addComponent(entity, GAME.Transform, { + posX: 10, posY: 5, posZ: -3, + eulerX: 0, eulerY: 45, eulerZ: 0, + scaleX: 2, scaleY: 2, scaleZ: 2 + }); + + // Transform system automatically syncs euler to quaternion + } +}; +``` + +### Transform Hierarchy + +#### Parent-Child Relationships +```xml + + + + + + + + + + + + + +``` + +#### Accessing World Transform +```typescript +import * as GAME from 'vibegame'; + +const transformQuery = GAME.defineQuery([GAME.Transform, GAME.WorldTransform]); +const WorldTransformSystem = { + update: (state) => { + // Query entities with both transforms + const entities = transformQuery(state.world); + + for (const entity of entities) { + // Local position + const localX = GAME.Transform.posX[entity]; + + // World position (after parent transforms) + const worldX = GAME.WorldTransform.posX[entity]; + + console.log(`Local: ${localX}, World: ${worldX}`); + } + } +}; +``` + +### Common Patterns + +#### Setting Transform Values +```typescript +import * as GAME from 'vibegame'; + +// Direct property access (bitECS style) +GAME.Transform.posX[entity] = 10; +GAME.Transform.posY[entity] = 5; +GAME.Transform.posZ[entity] = -3; + +// Using euler angles for rotation +GAME.Transform.eulerX[entity] = 0; +GAME.Transform.eulerY[entity] = 45; +GAME.Transform.eulerZ[entity] = 0; +// Quaternion will be auto-synced by TransformHierarchySystem + +// Uniform scale +GAME.Transform.scaleX[entity] = 2; +GAME.Transform.scaleY[entity] = 2; +GAME.Transform.scaleZ[entity] = 2; +``` + +#### Transform Interpolation +```typescript +import * as GAME from 'vibegame'; + +// Interpolate between two positions +const t = 0.5; // 50% between start and end +GAME.Transform.posX[entity] = startX + (endX - startX) * t; +GAME.Transform.posY[entity] = startY + (endY - startY) * t; +GAME.Transform.posZ[entity] = startZ + (endZ - startZ) * t; +``` + +### Tweening + +Animates component properties with easing functions and loop modes. Kinematic velocity bodies use velocity-based tweening for smooth physics-correct movement. + +### Components + +#### Tween +- duration: f32 (1) - Seconds +- elapsed: f32 +- easingIndex: ui8 +- loopMode: ui8 - 0=Once, 1=Loop, 2=PingPong + +#### TweenValue +- source: ui32 - Tween entity +- target: ui32 - Target entity +- componentId: ui32 +- fieldIndex: ui32 +- from: f32 +- to: f32 +- value: f32 - Current value + +#### KinematicTween +- tweenEntity: ui32 - Associated tween entity +- targetEntity: ui32 - Kinematic body entity +- axis: ui8 - 0=X, 1=Y, 2=Z +- from: f32 - Start position +- to: f32 - End position +- lastPosition: f32 +- targetPosition: f32 + +### Systems + +#### KinematicTweenSystem +- Group: fixed +- Converts position tweens to velocity for kinematic bodies + +#### TweenSystem +- Group: simulation +- Interpolates values with easing and auto-cleanup + +### Functions + +#### createTween(state, entity, target, options): number | null +Animates component property + +### Easing Functions + +- linear +- sine-in, sine-out, sine-in-out +- quad-in, quad-out, quad-in-out +- cubic-in, cubic-out, cubic-in-out +- quart-in, quart-out, quart-in-out +- expo-in, expo-out, expo-in-out +- circ-in, circ-out, circ-in-out +- back-in, back-out, back-in-out +- elastic-in, elastic-out, elastic-in-out +- bounce-in, bounce-out, bounce-in-out + +### Loop Modes + +- once - Play once and destroy +- loop - Repeat indefinitely +- ping-pong - Alternate directions + +### Shorthand Targets + +- rotation - body.eulerX/Y/Z +- at - body.posX/Y/Z +- scale - transform.scaleX/Y/Z + +#### Examples + +## Examples + +### Basic XML Tween + +```xml + + + + +``` + +### Multiple Properties + +```xml + + + + +``` + +### Body Physics Properties + +```xml + + + + +``` + +### Programmatic Usage + +```typescript +import * as GAME from 'vibegame'; + +// In a system +const MySystem = { + setup: (state) => { + const entity = state.createEntity(); + state.addComponent(entity, GAME.Transform); + + // Create tween programmatically + GAME.createTween(state, entity, 'body.pos-y', { + from: 0, + to: 10, + duration: 2, + easing: 'bounce-out', + loop: 'once' + }); + } +}; +``` + +### Complex Animation Sequence + +```xml + + + + + + + + + + +``` + +### Ui + +Web-native UI system using HTML/CSS overlays positioned over the 3D canvas. Includes GSAP for animations, ECS state synchronization, and external library support. This provides capabilities superior to most game engines' built-in UI systems. + +### Components + +#### UIManager +- element: HTMLElement - Root UI container +- state: State - ECS state reference for updates +- visible: ui8 (1) - UI visibility toggle + +### Systems + +#### UIUpdateSystem +- Group: simulation +- Updates UI elements from ECS component state + +#### UIEventSystem +- Group: setup +- Handles UI event binding and canvas focus management + +### Functions + +#### createUIOverlay(canvas: HTMLCanvasElement): HTMLElement +Creates positioned UI overlay container + +#### bindUIToState(uiManager: UIManager, state: State): void +Connects UI updates to ECS state changes + +#### showFloatingText(x: number, y: number, text: string): void +Creates animated floating text at screen coordinates + +### Patterns + +#### Basic UI Setup +```html +
+
+ 0 + 100 +
+
+``` + +#### ECS Integration +```typescript +const UISystem = { + update: (state) => { + // Update UI from game state components + const scoreEl = document.getElementById('score'); + if (scoreEl) scoreEl.textContent = getScore(state); + } +}; +``` + +#### GSAP Animations +```typescript +gsap.to("#currency", { + scale: 1.2, + duration: 0.2, + yoyo: true, + repeat: 1 +}); +``` + +#### Examples + +## Examples + +### Basic Game HUD + +```html + + + + + + + + + + + + +
+
+
Score: 0
+
Coins: 0
+
+
+ + + + +``` + +### Animated Currency with GSAP + +```javascript +import gsap from 'gsap'; + +class AnimatedCounter { + constructor(elementId) { + this.element = document.getElementById(elementId); + this.currentValue = 0; + this.displayValue = 0; + } + + setValue(newValue) { + this.currentValue = newValue; + + gsap.to(this, { + displayValue: newValue, + duration: 0.8, + ease: "power2.out", + onUpdate: () => { + this.element.textContent = Math.floor(this.displayValue); + } + }); + } +} + +// Usage in ECS system +const coinCounter = new AnimatedCounter('coins'); + +const UISystem = { + update: (state) => { + const coins = getCoinsFromState(state); + coinCounter.setValue(coins); + } +}; +``` + +### Floating Damage Text + +```javascript +function showDamageText(worldX, worldY, worldZ, damage) { + // Convert 3D world position to screen coordinates + const camera = getMainCamera(state); + const screenPos = worldToScreen(worldX, worldY, worldZ, camera); + + const element = document.createElement('div'); + element.textContent = `-${damage}`; + element.style.cssText = ` + position: fixed; + left: ${screenPos.x}px; + top: ${screenPos.y}px; + color: #ff4444; + font-weight: bold; + font-size: 24px; + pointer-events: none; + z-index: 10000; + `; + + document.body.appendChild(element); + + gsap.timeline() + .to(element, { + y: screenPos.y - 100, + opacity: 0, + duration: 1.5, + ease: "power2.out" + }) + .call(() => element.remove()); +} + +// Usage in collision system +const DamageSystem = { + update: (state) => { + // When player takes damage + const playerPos = getPlayerPosition(state); + showDamageText(playerPos.x, playerPos.y + 2, playerPos.z, 25); + } +}; +``` + +### Menu System with State + +```javascript +class GameMenu { + constructor() { + this.isOpen = false; + this.element = document.getElementById('main-menu'); + } + + toggle() { + this.isOpen = !this.isOpen; + + if (this.isOpen) { + this.show(); + } else { + this.hide(); + } + } + + show() { + this.element.style.display = 'block'; + gsap.fromTo(this.element, + { opacity: 0, scale: 0.8 }, + { opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" } + ); + } + + hide() { + gsap.to(this.element, { + opacity: 0, + scale: 0.8, + duration: 0.2, + onComplete: () => { + this.element.style.display = 'none'; + } + }); + } +} + +// Integration with input system +const menu = new GameMenu(); + +const MenuSystem = { + update: (state) => { + // Check for escape key press + if (GAME.consumeInput(state, 'escape')) { + menu.toggle(); + } + } +}; +``` diff --git a/package.json b/package.json index bffbb402f5d7b66ef13253fdacc701e8fde42090..6c8b13a2fcceceb644de770aaf1085835be2c0f9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "check": "tsc --noEmit && svelte-check", "check:ts": "tsc --noEmit", "check:svelte": "svelte-check", + "update": "cp node_modules/vibegame/llms.txt ./llms.txt && echo '✓ Updated llms.txt from vibegame'", + "postinstall": "test -f node_modules/vibegame/llms.txt && cp node_modules/vibegame/llms.txt ./llms.txt || true", "test": "bun test", "test:watch": "bun test --watch", "format": "prettier --write .", @@ -43,13 +45,12 @@ "@langchain/mcp-adapters": "^0.6.0", "@modelcontextprotocol/sdk": "^0.6.0", "@modelcontextprotocol/server-filesystem": "^0.6.2", - "@types/marked": "^6.0.0", "@types/node": "^24.3.3", "gsap": "^3.13.0", - "marked": "^16.2.1", + "htmlparser2": "^10.0.0", "monaco-editor": "^0.50.0", "svelte-splitpanes": "^8.0.5", - "vibegame": "^0.1.7", + "vibegame": "^0.1.9", "zod": "^4.1.8" } } diff --git a/src/App.svelte b/src/App.svelte index d19a2c1475b18783eadf2b9fa61c5f5b5f786477..690273c2b1fc6f6bebe3503b6338c8dd2c44a10c 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,10 +2,12 @@ import { onMount, onDestroy } from 'svelte'; import { registerShortcuts, shortcuts } from './lib/config/shortcuts'; import { loadingStore } from './lib/stores/loading'; + import { uiStore } from './lib/stores/ui'; import { contentManager } from './lib/services/content-manager'; import AppHeader from './lib/components/layout/AppHeader.svelte'; import SplitView from './lib/components/layout/SplitView.svelte'; import LoadingScreen from './lib/components/layout/LoadingScreen.svelte'; + import About from './lib/components/about/About.svelte'; let unregisterShortcuts: () => void; @@ -39,7 +41,11 @@
- + {#if $uiStore.viewMode === 'about'} + + {:else} + + {/if}
\ No newline at end of file diff --git a/src/lib/components/about/context.md b/src/lib/components/about/context.md new file mode 100644 index 0000000000000000000000000000000000000000..6ede51798461f8025b9bc25fd8f4e3c5438a1402 --- /dev/null +++ b/src/lib/components/about/context.md @@ -0,0 +1,31 @@ +# About Component + +Project information display + +## Purpose + +- Display project overview and features +- Provide quick start example +- Link to resources (GitHub, NPM, demos) +- Modal-style overlay presentation + +## Structure + +``` +about/ +├── context.md # This file +└── About.svelte # Project info modal +``` + +## Features + +- Compact card layout with 860px max width +- Feature grid (3 columns) +- Full Quick Start code example +- Resource links to GitHub, NPM, Demo, JSFiddle +- Close via X button, About toggle, or title click + +## Dependencies + +- uiStore for view mode control +- GSAP for fade animation diff --git a/src/lib/components/chat/ChatPanel.svelte b/src/lib/components/chat/ChatPanel.svelte index 31edbe8fbb43e9df10fff97a44c1ebd393297279..d18e8530fb6754a3ba3298c469474141d79a8d36 100644 --- a/src/lib/components/chat/ChatPanel.svelte +++ b/src/lib/components/chat/ChatPanel.svelte @@ -4,6 +4,7 @@ import gsap from "gsap"; import MessageList from "./MessageList.svelte"; import MessageInput from "./MessageInput.svelte"; + import ExampleRow from "./ExampleRow.svelte"; import { isConnected, isProcessing, messages, error } from "../../stores/chat-store"; import { chatController } from "../../controllers/chat-controller"; import { authStore } from "../../services/auth"; @@ -118,6 +119,11 @@ {/if} + + - import { onMount, onDestroy } from "svelte"; import { fade } from "svelte/transition"; - import gsap from "gsap"; - - export let onSendMessage: (message: string) => void; - - const examples = [ - { icon: "🏀", text: "add another ball" }, - { icon: "📝", text: "explain what the code does" }, - { icon: "🎮", text: "make an obby" }, - { icon: "⬆️", text: "increase the player jump height" } - ]; - - let exampleCards: HTMLButtonElement[] = []; - - onMount(() => { - exampleCards.forEach((card, index) => { - if (!card) return; - - gsap.fromTo(card, { - opacity: 0, - y: 15, - scale: 0.97 - }, { - opacity: 1, - y: 0, - scale: 1, - duration: 0.2, - delay: index * 0.02, - ease: "power3.out" - }); - }); - }); + - onDestroy(() => { - exampleCards.forEach((card) => { - if (card) { - gsap.killTweensOf(card); - } - }); - }); +
+
+
+ ⚠️ + Model Limitations +
+

+ This demo uses Qwen3-Next-80B-A3B-Instruct. +
+ For complex tasks, run locally with frontier code agents like Claude Code. +

+ + Learn More + + + + +
+
- function handleClick(text: string) { - onSendMessage(text); + \ No newline at end of file + diff --git a/src/lib/components/chat/ExampleRow.svelte b/src/lib/components/chat/ExampleRow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..acbf7cc6c6e1a9599e2a805ce617ab75a273f131 --- /dev/null +++ b/src/lib/components/chat/ExampleRow.svelte @@ -0,0 +1,183 @@ + + +{#if visible} +
+ Try an example: +
+ {#each examples as example, i} + + {/each} +
+
+{/if} + + \ No newline at end of file diff --git a/src/lib/components/chat/Message.svelte b/src/lib/components/chat/Message.svelte index 533793696aaba666af2406f86ff27c11fcfb3c83..f5d917a182f7a98e14920d61222a36a06b950c15 100644 --- a/src/lib/components/chat/Message.svelte +++ b/src/lib/components/chat/Message.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/components/chat/MessageContent.svelte b/src/lib/components/chat/MessageContent.svelte index 0c97fe0d96d180dd58e768576fefdc968edb4119..f04b4fd6c451832b5b5027e34aa61378ac36d491 100644 --- a/src/lib/components/chat/MessageContent.svelte +++ b/src/lib/components/chat/MessageContent.svelte @@ -3,14 +3,15 @@ import TextRenderer from "./TextRenderer.svelte"; import ToolBlock from "./segments/ToolBlock.svelte"; import TodoSegment from "./segments/TodoSegment.svelte"; + import { filterToolCalls } from "../../utils/tool-call-parser"; export let message: ChatMessage; $: hasSegments = message.segments && message.segments.length > 0; $: isStreaming = message.streaming || (message.segments?.some(s => s.streaming)); - // Normalize content for consistent rendering - $: normalizedContent = hasSegments ? null : (message.content || ""); + // Normalize content for consistent rendering with tool call filtering + $: normalizedContent = hasSegments ? null : filterToolCalls(message.content || ""); // Filter segments for rendering $: segmentsToRender = hasSegments ? message.segments!.filter((segment) => { @@ -34,7 +35,7 @@ {#each segmentsToRender as segment (segment.id)} {#if segment.type === "text"} - + {:else if segment.type === "tool-invocation"} { - gsap.to(sendButtonRef, { - rotationY: 360, - duration: 0.6, - ease: "back.out(1.7)" - }); - } - }); + ease: "power2.in" + }) + .to(sendButtonRef, { + scale: 1.1, + rotation: 360, + duration: 0.4, + ease: "back.out(2)" + }) + .to(sendButtonRef, { + scale: 1, + duration: 0.2, + ease: "power2.out" + }, "-=0.1"); } - dispatch("send", value.trim()); - value = ""; - if (textareaRef) { - gsap.fromTo(textareaRef, - { scale: 1.02 }, - { scale: 1, duration: 0.3, ease: "power2.out" } + tl.to(textareaRef, + { + borderColor: "rgba(76, 175, 80, 0.3)", + backgroundColor: "rgba(76, 175, 80, 0.02)", + duration: 0.2 + }, + 0 + ) + .to(textareaRef, + { + borderColor: "rgba(139, 115, 85, 0.08)", + backgroundColor: "rgba(139, 115, 85, 0.03)", + duration: 0.3, + ease: "power2.out" + } ); } + + dispatch("send", value.trim()); + value = ""; } } @@ -60,6 +76,8 @@ } } + let isTyping = false; + onMount(() => { if (inputAreaRef) { gsap.fromTo(inputAreaRef, @@ -84,6 +102,30 @@ gsap.set(stopButtonRef, { scale: 1 }); } + $: if (value.length > 0 && !isTyping) { + isTyping = true; + if (sendButtonRef) { + gsap.to(sendButtonRef, { + scale: 1.05, + backgroundColor: "rgba(124, 152, 133, 0.12)", + duration: 0.3, + ease: "power2.out" + }); + } + } + + $: if (value.length === 0 && isTyping) { + isTyping = false; + if (sendButtonRef) { + gsap.to(sendButtonRef, { + scale: 1, + backgroundColor: "rgba(124, 152, 133, 0.08)", + duration: 0.3, + ease: "power2.out" + }); + } + } + function handleKeydown(event: KeyboardEvent) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); @@ -135,8 +177,8 @@ display: flex; gap: 0.5rem; padding: 12px 16px; - background: transparent; - border-top: 1px solid rgba(139, 115, 85, 0.06); + background: rgba(20, 19, 17, 0.5); + border-top: 1px solid rgba(139, 115, 85, 0.18); } .input-wrapper { @@ -161,9 +203,9 @@ textarea { width: 100%; - background: rgba(139, 115, 85, 0.03); - color: rgba(251, 248, 244, 0.9); - border: 1px solid rgba(139, 115, 85, 0.08); + background: rgba(30, 28, 26, 0.5); + color: rgba(251, 248, 244, 0.95); + border: 1px solid rgba(139, 115, 85, 0.15); border-radius: 4px; padding: 0.4rem 0.6rem; resize: none; @@ -177,9 +219,9 @@ textarea:focus { outline: none; - border-color: rgba(124, 152, 133, 0.2); - background: rgba(139, 115, 85, 0.06); - box-shadow: 0 0 0 1px rgba(124, 152, 133, 0.1); + border-color: rgba(124, 152, 133, 0.3); + background: rgba(35, 33, 30, 0.6); + box-shadow: 0 0 0 1px rgba(124, 152, 133, 0.15); } textarea:focus + .input-glow { @@ -197,9 +239,9 @@ display: flex; align-items: center; justify-content: center; - background: rgba(124, 152, 133, 0.08); - color: rgba(124, 152, 133, 0.8); - border: 1px solid rgba(124, 152, 133, 0.15); + background: rgba(124, 152, 133, 0.12); + color: rgba(124, 152, 133, 0.9); + border: 1px solid rgba(124, 152, 133, 0.2); border-radius: 4px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -219,10 +261,10 @@ } .send-btn:hover:not(:disabled) { - background: rgba(124, 152, 133, 0.15); - border-color: rgba(124, 152, 133, 0.3); + background: rgba(124, 152, 133, 0.2); + border-color: rgba(124, 152, 133, 0.35); transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(124, 152, 133, 0.2); + box-shadow: 0 4px 12px rgba(124, 152, 133, 0.25); color: rgba(124, 152, 133, 1); } @@ -246,9 +288,9 @@ display: flex; align-items: center; justify-content: center; - background: rgba(184, 84, 80, 0.08); - color: rgba(184, 84, 80, 0.8); - border: 1px solid rgba(184, 84, 80, 0.15); + background: rgba(184, 84, 80, 0.12); + color: rgba(184, 84, 80, 0.9); + border: 1px solid rgba(184, 84, 80, 0.2); border-radius: 4px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -281,10 +323,10 @@ } .stop-btn:hover { - background: rgba(184, 84, 80, 0.15); - border-color: rgba(184, 84, 80, 0.3); + background: rgba(184, 84, 80, 0.2); + border-color: rgba(184, 84, 80, 0.35); transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(184, 84, 80, 0.2); + box-shadow: 0 4px 12px rgba(184, 84, 80, 0.25); color: rgba(184, 84, 80, 1); } diff --git a/src/lib/components/chat/MessageList.svelte b/src/lib/components/chat/MessageList.svelte index 894870006f6ac76de3689e65c64d707c8af927b2..f6edc61759fdc667f33ee0a1f5ad7184b3ff432e 100644 --- a/src/lib/components/chat/MessageList.svelte +++ b/src/lib/components/chat/MessageList.svelte @@ -35,15 +35,27 @@ $: if ($typingIndicator && typingIndicatorRef) { gsap.fromTo(typingIndicatorRef, - { opacity: 0, y: 10 }, - { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" } + { opacity: 0, y: 10, scale: 0.95 }, + { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: "power2.out" } ); + + const dots = typingIndicatorRef.querySelectorAll('.dot'); + gsap.to(dots, { + y: -3, + duration: 0.6, + stagger: 0.15, + repeat: -1, + yoyo: true, + ease: "power1.inOut" + }); } $: if (!$typingIndicator && typingIndicatorRef) { + gsap.killTweensOf(typingIndicatorRef.querySelectorAll('.dot')); gsap.to(typingIndicatorRef, { opacity: 0, y: -5, + scale: 0.95, duration: 0.2, ease: "power2.in" }); @@ -53,7 +65,7 @@
{#if messages.length === 0 && isConnected && onSendMessage}
- +
{/if} @@ -101,44 +113,63 @@ display: flex; align-items: center; gap: 0.5rem; - padding: 0.375rem 0.5rem; - margin: 0.125rem 0; - border-left: 2px solid rgba(124, 152, 133, 0.2); - padding-left: 0.5rem; + padding: 0.5rem; + margin: 0.25rem 0; + border-left: 2px solid rgba(33, 150, 243, 0.3); + background: linear-gradient(90deg, + rgba(33, 150, 243, 0.05), + transparent); + border-radius: 0 4px 4px 0; + position: relative; + overflow: hidden; + } + + .typing-indicator::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(33, 150, 243, 0.1), + transparent); + animation: shimmer 2s ease-in-out infinite; + } + + @keyframes shimmer { + to { left: 100%; } } .typing-dots { display: flex; - gap: 0.25rem; + gap: 0.3rem; + align-items: center; } .dot { - width: 4px; - height: 4px; + width: 6px; + height: 6px; border-radius: 50%; - background: rgba(124, 152, 133, 0.6); - animation: typing-bounce 1.4s ease-in-out infinite both; - } - - .dot:nth-child(1) { animation-delay: -0.32s; } - .dot:nth-child(2) { animation-delay: -0.16s; } - .dot:nth-child(3) { animation-delay: 0s; } - - @keyframes typing-bounce { - 0%, 80%, 100% { - transform: scale(0); - opacity: 0.5; - } - 40% { - transform: scale(1); - opacity: 1; - } + background: linear-gradient(135deg, + rgba(33, 150, 243, 0.8), + rgba(100, 181, 246, 0.6)); + display: inline-block; + position: relative; } .typing-text { font-size: 11px; - color: rgba(251, 248, 244, 0.3); - font-style: italic; + color: rgba(251, 248, 244, 0.4); + font-style: normal; + letter-spacing: 0.3px; + animation: fade-pulse 2s ease-in-out infinite; + } + + @keyframes fade-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 0.7; } } .error-message { diff --git a/src/lib/components/chat/TextRenderer.svelte b/src/lib/components/chat/TextRenderer.svelte index 2671a5a7ac397a971f42afd0282ed4d13d300d7d..c94d2418e26ad5ef447e0bf408513807fe411dcb 100644 --- a/src/lib/components/chat/TextRenderer.svelte +++ b/src/lib/components/chat/TextRenderer.svelte @@ -1,22 +1,66 @@
- {content}{#if streaming}{/if} + {#if streaming && lastContentLength > 0} + {filteredContent.slice(0, lastContentLength)} + {filteredContent.slice(lastContentLength)} + {:else} + {filteredContent} + {/if} + {#if streaming}{/if}
diff --git a/src/lib/components/chat/context.md b/src/lib/components/chat/context.md index 57049d4c16432ec7b5b134e6068eaf5f45f03876..9597bdc33d3c2da256f8bf23f86a160849320248 100644 --- a/src/lib/components/chat/context.md +++ b/src/lib/components/chat/context.md @@ -1,30 +1,31 @@ # Chat Context -AI chat interface with enhanced feedback and micro-interactions. +AI chat interface with enhanced contrast dark theme and dopamine-driven micro-interactions. ## Components - `ChatPanel.svelte` - Main container with auth handling -- `MessageList.svelte` - Message container with typing indicators -- `Message.svelte` - Message wrapper with entrance animations -- `MessageContent.svelte` - Content normalizer and router -- `TextRenderer.svelte` - Text display with streaming cursors -- `MessageInput.svelte` - Input with enhanced button feedback -- `ExampleMessages.svelte` - Initial prompt suggestions -- `segments/ToolBlock.svelte` - Collapsible tool invocation/result -- `segments/TodoSegment.svelte` - Task list display +- `MessageList.svelte` - Message container with animated typing indicators +- `Message.svelte` - Message wrapper with coordinated entrance animations +- `MessageContent.svelte` - Content normalizer and router with segment-level filtering +- `TextRenderer.svelte` - Progressive text reveal with streaming-aware tool call filtering +- `MessageInput.svelte` - Input with charge-up animations and success feedback +- `ExampleMessages.svelte` - Model limitations warning when chat is empty +- `ExampleRow.svelte` - Horizontal example prompts above input +- `segments/ToolBlock.svelte` - Tool execution with icons, colors, and progress bars +- `segments/TodoSegment.svelte` - Task tracker with progress bar and milestone celebrations ## Architecture Clean MVC separation: -- **Model**: Enhanced chat store with feedback states +- **Model**: Chat store with state management - **View**: Component hierarchy with GSAP animations -- **Controller**: WebSocket handling with state management +- **Controller**: WebSocket handling with agent communication ## Design -- Consistent styling matches console/header color scheme -- GSAP micro-interactions provide constant user feedback -- Typing indicators and status management for engagement -- Single text renderer with streaming state animations +- Tool-specific icons and color coding for visual hierarchy +- Progress indicators with anticipation curves +- Success celebrations through subtle animations +- Consistent feedback loops for all interactions diff --git a/src/lib/components/chat/segments/TodoSegment.svelte b/src/lib/components/chat/segments/TodoSegment.svelte index da3111ddb667fbc2741ece559dd5122bf15bfcdf..614f94aefea167375053bacf330f704c413a9fe2 100644 --- a/src/lib/components/chat/segments/TodoSegment.svelte +++ b/src/lib/components/chat/segments/TodoSegment.svelte @@ -1,5 +1,5 @@ @@ -36,10 +82,25 @@
+
+
+
+
- {#each todoList.tasks as task} -
- {task.emoji} + {#each todoList.tasks as task, i} +
+ + {#if task.status === 'completed'} + + {:else if task.status === 'in_progress'} + + {:else} + + {/if} + {task.description}
{/each} @@ -81,6 +142,21 @@ border-radius: 10px; } + .progress-track { + height: 3px; + background: rgba(255, 255, 255, 0.05); + position: relative; + overflow: hidden; + } + + .progress-bar { + height: 100%; + background: linear-gradient(90deg, + rgba(76, 175, 80, 0.3), + rgba(76, 175, 80, 0.5)); + transition: width 0.6s ease-out; + } + .todo-list { padding: 0.375rem 0.5rem; } @@ -89,16 +165,48 @@ display: flex; align-items: center; gap: 0.375rem; - padding: 0.25rem 0; + padding: 0.3rem 0; font-size: 0.75rem; color: rgba(255, 255, 255, 0.7); + transition: all 0.2s ease; } - .task-emoji { - font-size: 0.9rem; + .task-indicator { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; flex-shrink: 0; } + .checkmark { + color: rgba(76, 175, 80, 0.9); + font-weight: bold; + animation: checkPop 0.3s ease-out; + } + + @keyframes checkPop { + 0% { transform: scale(0); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } + } + + .progress-dot { + color: rgba(33, 150, 243, 0.9); + animation: progressPulse 1.5s ease-in-out infinite; + } + + @keyframes progressPulse { + 0%, 100% { opacity: 0.4; transform: scale(0.8); } + 50% { opacity: 1; transform: scale(1.2); } + } + + .pending-circle { + color: rgba(255, 255, 255, 0.3); + font-size: 10px; + } + .task-text { flex: 1; } @@ -110,6 +218,10 @@ .todo-task.in_progress { color: rgba(255, 255, 255, 0.9); + background: rgba(33, 150, 243, 0.05); + border-left: 2px solid rgba(33, 150, 243, 0.3); + padding-left: 0.5rem; + margin-left: -0.5rem; } .todo-task.in_progress .task-text { diff --git a/src/lib/components/chat/segments/ToolBlock.svelte b/src/lib/components/chat/segments/ToolBlock.svelte index b1776b126ff198b107f27e4a4972258e2d992fec..5e148c31199964628c656594e8b5c85dc6bef6a2 100644 --- a/src/lib/components/chat/segments/ToolBlock.svelte +++ b/src/lib/components/chat/segments/ToolBlock.svelte @@ -1,24 +1,119 @@ -
+
+ {#if isRunning} +
+ {/if} +
+ + | + + |
-
- - -
+ {#if $uiStore.viewMode !== 'about'} +
+ + +
+ {/if}
@@ -146,14 +176,28 @@ .header-left { justify-content: flex-start; - gap: 0.75rem; + gap: 0.5rem; + align-items: center; } - .app-title { + .app-title-button { display: flex; align-items: center; gap: 0.5rem; user-select: none; + background: transparent; + border: none; + padding: 0; + cursor: default; + transition: all 0.2s; + } + + .app-title-button.clickable { + cursor: pointer; + } + + .app-title-button.clickable:hover .app-name { + color: rgba(251, 248, 244, 0.75); } .app-icon { @@ -168,6 +212,59 @@ letter-spacing: 0.025em; } + .header-separator { + color: rgba(251, 248, 244, 0.2); + font-size: 0.875rem; + user-select: none; + padding: 0 0.25rem; + } + + .nav-button { + background: transparent; + border: none; + color: rgba(251, 248, 244, 0.5); + font-size: 0.875rem; + font-weight: 500; + padding: 0.375rem 0.625rem; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + } + + .nav-button::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 0.25rem; + background: rgba(139, 115, 85, 0.08); + opacity: 0; + transition: opacity 0.2s ease-out; + } + + .nav-button:hover { + color: rgba(251, 248, 244, 0.9); + } + + .nav-button:hover::before { + opacity: 1; + } + + .nav-button:active { + transform: scale(0.98); + } + + .nav-button.active { + background: rgba(124, 152, 133, 0.12); + color: rgba(251, 248, 244, 0.95); + border-radius: 0.25rem; + } + + .nav-button.active::before { + opacity: 1; + background: rgba(124, 152, 133, 0.1); + } + .github-link { display: flex; align-items: center; diff --git a/src/lib/components/layout/SplitView.svelte b/src/lib/components/layout/SplitView.svelte index 5bbe4664a4c9f1c1539011cc1b62d1f21be05612..6d25007cd04f9f2cb21c4b3ed7f5007bd5f14ea8 100644 --- a/src/lib/components/layout/SplitView.svelte +++ b/src/lib/components/layout/SplitView.svelte @@ -37,7 +37,7 @@ }; }); - $: if (viewMode) { + $: if (viewMode && viewMode !== 'about') { handleViewModeAnimation(viewMode); } diff --git a/src/lib/components/layout/context.md b/src/lib/components/layout/context.md index 302a181abc7ef0b2c8e7518c3046b2f613707666..dff04e3f35ae04e0ca1358fe17b13cc6cdf5b1de 100644 --- a/src/lib/components/layout/context.md +++ b/src/lib/components/layout/context.md @@ -5,9 +5,9 @@ Application layout structure ## Purpose - Loading state management -- Header navigation +- Header navigation with view switching - Two-pane layout with nested splits -- View mode transitions +- View mode transitions (code/preview/about) ## Structure @@ -15,7 +15,7 @@ Application layout structure layout/ ├── context.md # This file ├── LoadingScreen.svelte # Initial loading with spinner -├── AppHeader.svelte # Top navigation bar +├── AppHeader.svelte # Top navigation bar with Title | About | Repo └── SplitView.svelte # Two-pane layout with nested splits ``` @@ -24,10 +24,11 @@ layout/ - Main split: Editor pane (left) | Preview pane (right) - Editor pane: Code editor (top) | Chat panel (bottom) - Preview pane: Game canvas (top) | Console (bottom) +- View modes: code, preview, or about page overlay ## Dependencies - loadingStore for loading state -- uiStore for view mode +- uiStore for view mode management - svelte-splitpanes for resizable panes - GSAP for animations diff --git a/src/lib/config/animations.ts b/src/lib/config/animations.ts deleted file mode 100644 index 9f0fd871a4b090a6765104a3140bd2821978fab4..0000000000000000000000000000000000000000 --- a/src/lib/config/animations.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const animationConfig = { - header: { - initial: { opacity: 0, y: -20 }, - animate: { opacity: 1, y: 0, duration: 0.6, ease: "power3.out" }, - }, - - editor: { - initial: { opacity: 0, x: -20 }, - animate: { - opacity: 1, - x: 0, - duration: 0.6, - delay: 0.1, - ease: "power3.out", - }, - }, - - preview: { - initial: { opacity: 0, x: 20 }, - animate: { - opacity: 1, - x: 0, - duration: 0.6, - delay: 0.2, - ease: "power3.out", - }, - }, - - consoleMessage: { - initial: { opacity: 0, y: -5 }, - animate: { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }, - }, - - error: { - initial: { opacity: 0, transform: "translateY(-10px)" }, - animate: { opacity: 1, transform: "translateY(0)", duration: 0.3 }, - }, - - viewTransition: { - duration: 0.2, - ease: "power2.inOut", - }, -}; diff --git a/src/lib/config/context.md b/src/lib/config/context.md index bb6a1018c08c639dac3ccc1fe331b1516a7f29e9..8d36a4ac76769ec0a6eb50a911c86c9f4c2b8342 100644 --- a/src/lib/config/context.md +++ b/src/lib/config/context.md @@ -5,16 +5,14 @@ Application configuration and constants ## Purpose - Centralize configuration values -- Define animation settings - Keyboard shortcuts registry ## Layout ``` config/ -├── context.md # This file -├── animations.ts # GSAP animation configs -└── shortcuts.ts # Keyboard shortcut definitions +├── context.md # This file +└── shortcuts.ts # Keyboard shortcut definitions ``` ## Scope @@ -24,7 +22,6 @@ config/ ## Entrypoints -- `animationConfig` - Animation presets - `shortcuts` - Keyboard shortcut definitions - `registerShortcuts()` - Setup keyboard handlers diff --git a/src/lib/controllers/animation-controller.ts b/src/lib/controllers/animation-controller.ts deleted file mode 100644 index 48d90d8090b9cfb4b2e75934d17c8c1f0bb8bd2c..0000000000000000000000000000000000000000 --- a/src/lib/controllers/animation-controller.ts +++ /dev/null @@ -1,190 +0,0 @@ -import gsap from "gsap"; - -type AnimationTarget = HTMLElement | null; -type AnimationId = string; - -interface AnimationConfig { - target: AnimationTarget; - animation: gsap.core.Tween | gsap.core.Timeline; -} - -export class AnimationController { - private animations = new Map(); - - register(id: AnimationId, target: AnimationTarget): void { - if (!target) return; - this.cleanup(id); - this.animations.set(id, { target, animation: null! }); - } - - cleanup(id: AnimationId): void { - const config = this.animations.get(id); - if (config?.animation) { - config.animation.kill(); - } - if (config?.target) { - gsap.killTweensOf(config.target); - } - this.animations.delete(id); - } - - cleanupAll(): void { - this.animations.forEach((_, id) => this.cleanup(id)); - } - - buttonPress(element: AnimationTarget): void { - if (!element) return; - gsap.to(element, { - scale: 0.9, - duration: 0.1, - ease: "power2.in", - onComplete: () => { - gsap.to(element, { - scale: 1, - duration: 0.2, - ease: "elastic.out(1, 0.5)", - }); - }, - }); - } - - buttonHover(element: AnimationTarget, entering: boolean): void { - if (!element) return; - gsap.to(element, { - scale: entering ? 1.05 : 1, - duration: 0.2, - ease: "power2.out", - }); - } - - fadeIn(element: AnimationTarget, duration = 0.3): void { - if (!element) return; - gsap.fromTo( - element, - { opacity: 0, y: 5 }, - { opacity: 1, y: 0, duration, ease: "power2.out" }, - ); - } - - fadeOut(element: AnimationTarget, duration = 0.2): Promise { - return new Promise((resolve) => { - if (!element) { - resolve(); - return; - } - gsap.to(element, { - opacity: 0, - duration, - ease: "power2.in", - onComplete: resolve, - }); - }); - } - - pulseGlow(id: AnimationId, element: AnimationTarget, color: string): void { - if (!element) return; - const animation = gsap.to(element, { - boxShadow: `0 0 25px ${color}`, - duration: 2.5, - repeat: -1, - yoyo: true, - ease: "sine.inOut", - }); - this.animations.set(id, { target: element, animation }); - } - - borderPulse(id: AnimationId, element: AnimationTarget, color: string): void { - if (!element) return; - const animation = gsap.to(element, { - borderColor: color, - duration: 2, - repeat: -1, - yoyo: true, - ease: "sine.inOut", - }); - this.animations.set(id, { target: element, animation }); - } - - smoothScroll( - element: AnimationTarget, - targetScroll: number, - duration?: number, - ): Promise { - return new Promise((resolve) => { - if (!element) { - resolve(); - return; - } - - const currentScroll = element.scrollTop; - const distance = Math.abs(targetScroll - currentScroll); - const calculatedDuration = duration ?? Math.min(0.5, distance / 1000); - - gsap.to(element, { - scrollTop: targetScroll, - duration: calculatedDuration, - ease: "power2.out", - onComplete: resolve, - }); - }); - } - - cursorBlink(id: AnimationId, element: AnimationTarget): void { - if (!element) return; - gsap.set(element, { display: "inline-block", opacity: 1 }); - const animation = gsap.to(element, { - opacity: 0, - duration: 0.5, - repeat: -1, - yoyo: true, - ease: "steps(1)", - }); - this.animations.set(id, { target: element, animation }); - } - - hideCursor(element: AnimationTarget): Promise { - return new Promise((resolve) => { - if (!element) { - resolve(); - return; - } - gsap.killTweensOf(element); - gsap.to(element, { - opacity: 0, - duration: 0.2, - onComplete: () => { - gsap.set(element, { display: "none" }); - resolve(); - }, - }); - }); - } - - slideDown(element: AnimationTarget, duration = 0.3): void { - if (!element) return; - gsap.from(element, { - height: 0, - opacity: 0, - duration, - ease: "power2.out", - }); - } - - slideUp(element: AnimationTarget, duration = 0.3): Promise { - return new Promise((resolve) => { - if (!element) { - resolve(); - return; - } - gsap.to(element, { - height: 0, - opacity: 0, - duration, - ease: "power2.in", - onComplete: resolve, - }); - }); - } -} - -export const animationController = new AnimationController(); diff --git a/src/lib/controllers/context.md b/src/lib/controllers/context.md index 0c6af7bfb3da914f594072b3e028aa1cab2ed294..b7f907b2d0137185e0e88e9ca8fbed4b08a06793 100644 --- a/src/lib/controllers/context.md +++ b/src/lib/controllers/context.md @@ -12,23 +12,20 @@ Business logic and orchestration layer ``` controllers/ -├── context.md # This file -├── chat-controller.ts # Chat operations and state management -└── animation-controller.ts # Centralized GSAP animations +├── context.md # This file +└── chat-controller.ts # Chat operations and state management ``` ## Scope -- In-scope: Business logic, state orchestration, animations +- In-scope: Business logic, state orchestration - Out-of-scope: UI rendering, direct store access from components ## Entrypoints - `chatController` - All chat operations -- `animationController` - UI animations ## Dependencies - Stores for state updates - Services for external operations -- GSAP for animations diff --git a/src/lib/models/context.md b/src/lib/models/context.md index 3d8c81d900d4b9c1aa0a1dd76e9dd36ba6e88814..8e0bb3abcb74367f7d6c6bffdc7ef545285b367c 100644 --- a/src/lib/models/context.md +++ b/src/lib/models/context.md @@ -10,8 +10,9 @@ Define data shapes, types, and factory functions for type safety ``` models/ -├── context.md # This file -└── chat-data.ts # Chat types and factories +├── context.md # This file +├── chat-data.ts # Chat types and factories +└── segment-view.ts # Segment visualization and todo parsing ``` ## Scope diff --git a/src/lib/models/segment-view.ts b/src/lib/models/segment-view.ts index 93e1735b220ef1c18350131a6281afa9a784119e..f31f8cb2844305c5398caf892a1f61e9f69fe8fb 100644 --- a/src/lib/models/segment-view.ts +++ b/src/lib/models/segment-view.ts @@ -77,9 +77,6 @@ function getSegmentTitle(segment: MessageSegment): string { update_task: "✏️ Update Task", view_tasks: "👀 View Tasks", observe_console: "📺 Console Output", - read_file: "📖 Read File", - write_file: "✍️ Write File", - edit_file: "✏️ Edit File", }; return toolNames[segment.toolName] || `🔧 ${segment.toolName}`; } diff --git a/src/lib/server/console-buffer.ts b/src/lib/server/console-buffer.ts index aa951e1b791dee8ebd2b864d14df3b47d1675a2d..55a658d6ce06255947b1455eee2cee4748b3d471 100644 --- a/src/lib/server/console-buffer.ts +++ b/src/lib/server/console-buffer.ts @@ -3,13 +3,16 @@ export interface ConsoleBufferMessage { type: "log" | "warn" | "error" | "info"; message: string; timestamp: number; + context?: string; } export class ConsoleBuffer { private static instance: ConsoleBuffer | null = null; private messages: ConsoleBufferMessage[] = []; - private maxMessages = 100; + private maxMessages = 200; private lastReadTimestamp = 0; + private executionContext: string | null = null; + private messagesSinceLastTool: ConsoleBufferMessage[] = []; private constructor() {} @@ -21,19 +24,39 @@ export class ConsoleBuffer { } addMessage(message: ConsoleBufferMessage): void { + if (this.executionContext) { + message.context = this.executionContext; + } + this.messages.push(message); + this.messagesSinceLastTool.push(message); if (this.messages.length > this.maxMessages) { this.messages = this.messages.slice(-this.maxMessages); } + + if (this.messagesSinceLastTool.length > 50) { + this.messagesSinceLastTool = this.messagesSinceLastTool.slice(-50); + } } - getRecentMessages(since?: number): ConsoleBufferMessage[] { + getRecentMessages(since?: number, limit?: number): ConsoleBufferMessage[] { const sinceTimestamp = since || this.lastReadTimestamp; - return this.messages.filter((msg) => msg.timestamp > sinceTimestamp); + const filtered = this.messages.filter( + (msg) => msg.timestamp > sinceTimestamp, + ); + + if (limit && limit > 0 && filtered.length > limit) { + return filtered.slice(-limit); + } + + return filtered; } - getAllMessages(): ConsoleBufferMessage[] { + getAllMessages(limit?: number): ConsoleBufferMessage[] { + if (limit && limit > 0 && this.messages.length > limit) { + return this.messages.slice(-limit); + } return [...this.messages]; } @@ -45,8 +68,40 @@ export class ConsoleBuffer { } clear(): void { - this.messages = []; this.lastReadTimestamp = Date.now(); + this.messagesSinceLastTool = []; + } + + onGameReloadStart(): void { + this.messagesSinceLastTool = []; + this.lastReadTimestamp = Date.now(); + this.addMessage({ + id: `reload-${Date.now()}`, + type: "info", + message: "🔄 Game reloading...", + timestamp: Date.now(), + }); + } + + onGameReloadComplete(): void { + this.addMessage({ + id: `reload-complete-${Date.now()}`, + type: "info", + message: "✅ Game reload complete", + timestamp: Date.now(), + }); + } + + setExecutionContext(context: string | null): void { + this.executionContext = context; + } + + getMessagesSinceLastTool(): ConsoleBufferMessage[] { + return [...this.messagesSinceLastTool]; + } + + clearToolMessages(): void { + this.messagesSinceLastTool = []; } getGameStateFromMessages(): { @@ -54,8 +109,21 @@ export class ConsoleBuffer { hasError: boolean; lastError?: string; isReady: boolean; + isReloading: boolean; + messageCount: number; } { - const recentMessages = this.getRecentMessages(Date.now() - 5000); + const recentMessages = + this.messagesSinceLastTool.length > 0 + ? this.messagesSinceLastTool + : this.getRecentMessages(Date.now() - 5000); + + const hasReloadStartMessage = recentMessages.some((msg) => + msg.message.includes("🔄 Game reloading"), + ); + + const hasReloadCompleteMessage = recentMessages.some((msg) => + msg.message.includes("✅ Game reload complete"), + ); const hasStartMessage = recentMessages.some( (msg) => @@ -70,6 +138,7 @@ export class ConsoleBuffer { msg.message.includes("✅ Game started") || msg.message.includes("Game started!") || msg.message.includes("Game script loaded!") || + msg.message.includes("✅ Game reload complete") || msg.message.includes("successfully"), ); @@ -87,12 +156,18 @@ export class ConsoleBuffer { ? errorMessages[errorMessages.length - 1].message : undefined; + const isReloading = hasReloadStartMessage && !hasReloadCompleteMessage; + return { isLoading: - hasStartMessage && !hasSuccessMessage && errorMessages.length === 0, + (hasStartMessage || isReloading) && + !hasSuccessMessage && + errorMessages.length === 0, hasError: errorMessages.length > 0, lastError, - isReady: hasSuccessMessage && errorMessages.length === 0, + isReady: hasSuccessMessage && errorMessages.length === 0 && !isReloading, + isReloading, + messageCount: recentMessages.length, }; } } diff --git a/src/lib/server/context.md b/src/lib/server/context.md index 5efda921aaed52e246ad8a17f9cb654327df0cd6..ebdc799ef3497eff7b427b9bb1ca22fd21c58c63 100644 --- a/src/lib/server/context.md +++ b/src/lib/server/context.md @@ -4,19 +4,21 @@ WebSocket server with LangGraph agent for AI-assisted game development. ## Key Components -- **api.ts** - WebSocket message routing with virtual file sync -- **langgraph-agent.ts** - LangGraph agent with MCP tools -- **mcp-client.ts** - MCP tools for virtual file operations and Context7 docs -- **tools.ts** - Console observation and task tracking -- **console-buffer.ts** - Console message buffering with game state analysis +- **api.ts** - WebSocket message routing and connection management +- **langgraph-agent.ts** - LangGraph agent with tool deduplication and conflict detection +- **mcp-client.ts** - Editor tools with pre-validation and standardized responses +- **tools.ts** - Console observation with tail limiting +- **task-tracker.ts** - Task planning and management tools +- **console-buffer.ts** - Console buffering with lifecycle events and tail limiting ## Architecture -LangGraph agent with bidirectional content synchronization: +LangGraph agent with robust tool execution: -- Virtual file system treats editor as single HTML file -- MCP tools operate directly on virtual file system -- Real-time bidirectional sync with ContentManager +- Virtual file system with version tracking and edit history +- Tool call deduplication to prevent redundant operations +- Sequential tool execution with state tracking +- Pre-validation for edit operations to detect conflicts - Context7 integration for external library documentation - Buffered streaming with segment handling - AbortController for canceling conversations diff --git a/src/lib/server/documentation.ts b/src/lib/server/documentation.ts index 2eb52cc0121901a8401f5d89d46132891891bad6..5a1bd13872e1f8995394ac55b27eb74e9d0814c0 100644 --- a/src/lib/server/documentation.ts +++ b/src/lib/server/documentation.ts @@ -5,7 +5,7 @@ export class DocumentationService { private cache: string | null = null; private readonly docsPath: string; - constructor(filename: string = "agents.md") { + constructor(filename: string = "llms.txt") { this.docsPath = join(process.cwd(), filename); } diff --git a/src/lib/server/langgraph-agent.ts b/src/lib/server/langgraph-agent.ts index 24cabfff72899eca86caa7695211097031ae4b7f..63d1398b9fdc2011bbadccc1350169fbbe1173bf 100644 --- a/src/lib/server/langgraph-agent.ts +++ b/src/lib/server/langgraph-agent.ts @@ -10,6 +10,11 @@ import { observeConsoleTool } from "./tools"; import { mcpClientManager, setMCPWebSocketConnection } from "./mcp-client"; import { planTasksTool, updateTaskTool, viewTasksTool } from "./task-tracker"; import { documentationService } from "./documentation"; +import { + StreamingToolCallParser, + extractToolCalls, +} from "../utils/tool-call-parser"; +import { virtualFileSystem } from "../services/virtual-fs"; import type { WebSocket } from "ws"; const AgentState = Annotation.Root({ @@ -58,146 +63,86 @@ export class LangGraphAgent { const messages = this.formatMessages(state.messages, systemPrompt); let fullResponse = ""; + const parser = new StreamingToolCallParser(); let currentSegmentId: string | null = null; - let currentSegmentContent = ""; - let buffer = ""; const messageId = config?.metadata?.messageId; const abortSignal = config?.metadata?.abortSignal as | AbortSignal | undefined; - const toolRegex = /TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/g; - for await (const token of this.streamModelResponse( messages, abortSignal, )) { fullResponse += token; config?.writer?.({ type: "token", content: token }); - buffer += token; - - let processedUpTo = 0; - let match; - toolRegex.lastIndex = 0; - - while ((match = toolRegex.exec(buffer)) !== null) { - const textBefore = buffer.substring(processedUpTo, match.index); - - if (textBefore.trim() && this.ws) { - if (!currentSegmentId) { - currentSegmentId = `seg_${Date.now()}_${Math.random()}`; - currentSegmentContent = ""; - this.ws.send( - JSON.stringify({ - type: "segment_start", - payload: { - segmentId: currentSegmentId, - segmentType: "text", - messageId, - }, - timestamp: Date.now(), - }), - ); - } - currentSegmentContent += textBefore; - this.ws.send( - JSON.stringify({ - type: "segment_token", - payload: { - segmentId: currentSegmentId, - token: textBefore, - messageId, - }, - timestamp: Date.now(), - }), - ); - } + // Process token through the parser + const parseResult = parser.process(token); - if (currentSegmentId && currentSegmentContent.trim() && this.ws) { + // Only stream safe text (without tool calls) + if (parseResult.safeText) { + // Start a text segment if needed + if (!currentSegmentId && parseResult.safeText.trim() && this.ws) { + currentSegmentId = `seg_${Date.now()}_${Math.random()}`; this.ws.send( JSON.stringify({ - type: "segment_end", + type: "segment_start", payload: { segmentId: currentSegmentId, - content: currentSegmentContent, + segmentType: "text", messageId, }, timestamp: Date.now(), }), ); - currentSegmentId = null; - currentSegmentContent = ""; } - processedUpTo = match.index + match[0].length; - } - - if (processedUpTo > 0) { - buffer = buffer.substring(processedUpTo); - } else if (buffer.length > 100 && !buffer.includes("TOOL:")) { - if (!currentSegmentId && buffer.trim() && this.ws) { - currentSegmentId = `seg_${Date.now()}_${Math.random()}`; - currentSegmentContent = ""; + // Stream the safe text + if (currentSegmentId && this.ws) { this.ws.send( JSON.stringify({ - type: "segment_start", + type: "segment_token", payload: { segmentId: currentSegmentId, - segmentType: "text", + token: parseResult.safeText, messageId, }, timestamp: Date.now(), }), ); } - - if (currentSegmentId) { - currentSegmentContent += buffer; - if (this.ws) { - this.ws.send( - JSON.stringify({ - type: "segment_token", - payload: { - segmentId: currentSegmentId, - token: buffer, - messageId, - }, - timestamp: Date.now(), - }), - ); - } - } - buffer = ""; } - } - if (buffer.trim() && !buffer.match(/TOOL:\s*(\w+)\s+ARGS:\s*({[^}]*})/)) { - if (!currentSegmentId && this.ws) { - currentSegmentId = `seg_${Date.now()}_${Math.random()}`; - currentSegmentContent = ""; + // If we detected a tool call start, end the current text segment + if (parseResult.pendingToolCall && currentSegmentId && this.ws) { this.ws.send( JSON.stringify({ - type: "segment_start", + type: "segment_end", payload: { segmentId: currentSegmentId, - segmentType: "text", messageId, }, timestamp: Date.now(), }), ); + currentSegmentId = null; } + } - if (currentSegmentId) { - currentSegmentContent += buffer; - if (this.ws) { + // Finalize any remaining text segment + if (currentSegmentId && this.ws) { + // Process any remaining buffered content + const remainingBuffer = parser.getBuffer(); + if (remainingBuffer && !parser.isInToolCall()) { + const finalResult = parser.process(""); + if (finalResult.safeText) { this.ws.send( JSON.stringify({ type: "segment_token", payload: { segmentId: currentSegmentId, - token: buffer, + token: finalResult.safeText, messageId, }, timestamp: Date.now(), @@ -205,15 +150,12 @@ export class LangGraphAgent { ); } } - } - if (currentSegmentId && currentSegmentContent.trim() && this.ws) { this.ws.send( JSON.stringify({ type: "segment_end", payload: { segmentId: currentSegmentId, - content: currentSegmentContent, messageId, }, timestamp: Date.now(), @@ -221,7 +163,7 @@ export class LangGraphAgent { ); } - const toolCalls = this.parseToolCalls(fullResponse); + const toolCalls = extractToolCalls(fullResponse); if (toolCalls.length > 0) { const toolResults = await this.executeToolsWithSegments( @@ -294,85 +236,156 @@ export class LangGraphAgent { } private buildSystemPrompt(): string { - return `You are an expert VibeGame developer assistant with MCP (Model Context Protocol) tools for direct code manipulation. - -## Core Principles -- ALWAYS use tools to complete tasks - never provide instructions without execution -- Use the EXACT format: TOOL: tool_name ARGS: {"param": "value"} -- Wait for tool results before proceeding to next step -- The game auto-reloads after each editor change - -## Tool Usage Strategy - -### 1. SEARCH FIRST -Always search before reading or editing to locate relevant code: -- search_editor: Find functions, components, or patterns by text/regex -- read_editor_lines: Examine specific sections after search -- read_editor: Only when you need the complete file context - -### 2. EDIT INCREMENTALLY -Keep edits small and focused: -- edit_editor: For targeted changes (max ~20 lines) -- write_editor: Only for complete file rewrites or new files -- Break large changes into multiple edit_editor calls - -### 3. TASK PLANNING -For complex work (3+ steps), use task management: -- plan_tasks: Decompose work into clear steps -- update_task: Track progress (in_progress → completed) -- view_tasks: Review current task list - -### 4. RESEARCH LIBRARIES -For external libraries/frameworks beyond VibeGame: -- resolve_library_id: Find Context7-compatible library ID first -- get_library_docs: Fetch current documentation with targeted search - -### 5. VERIFY CHANGES -- observe_console: Check for errors after edits -- Monitor game reload status in tool responses - -## MCP Tools Reference - -### Code Analysis -- search_editor: {"query": "text", "mode": "text|regex", "contextLines": 2} -- read_editor: {} -- read_editor_lines: {"startLine": 1, "endLine": 10} - -### Code Modification -- edit_editor: {"oldText": "exact text", "newText": "replacement"} -- write_editor: {"content": "complete file content"} - -### Task Management -- plan_tasks: {"tasks": ["step 1", "step 2", ...]} -- update_task: {"taskId": 1, "status": "in_progress|completed"} -- view_tasks: {} - - -### Documentation & Research -- resolve_library_id: {"libraryName": "gsap"} - Find Context7-compatible library ID -- get_library_docs: {"context7CompatibleLibraryID": "/greensock/gsap", "tokens": 5000, "topic": "animations"} - Fetch up-to-date docs - -### Debugging -- observe_console: {} - -## VibeGame Context + return `## Role & Primary Guidance +You are a VibeGame Engine Specialist operating in a single-file editor environment. You work with ONE game file that users can see and edit in real-time, similar to JSFiddle or CodePen. + +## Live Coding Environment Context +- **SINGLE EDITOR**: There is exactly ONE editor file containing the game's XML/HTML code +- **"The code" ALWAYS refers to**: The content currently in the editor +- **Live Preview**: Changes to the editor automatically reload the game +- **User's View**: Users see the same editor you're modifying +- **GAME is PRE-IMPORTED**: The GAME object is automatically available - NEVER write \`import * as GAME from 'vibegame'\` +- **Auto-run enabled**: GAME.run() is called automatically - use \`GAME.withPlugin(MyPlugin)\` NOT \`GAME.withPlugin(MyPlugin).run()\` + +## VibeGame Expert Knowledge ${this.documentation} -## Example Workflows - -### VibeGame Edit: -User: "Change the ball color to blue" -1. TOOL: search_editor ARGS: {"query": "ball"} -2. TOOL: read_editor_lines ARGS: {"startLine": 10, "endLine": 15} -3. TOOL: edit_editor ARGS: {"oldText": "color=\\"#ff4500\\"", "newText": "color=\\"#0000ff\\""} -4. TOOL: observe_console ARGS: {} - -### External Library Research: -User: "How do I create GSAP animations?" -1. TOOL: resolve_library_id ARGS: {"libraryName": "gsap"} -2. TOOL: get_library_docs ARGS: {"context7CompatibleLibraryID": "/greensock/gsap", "topic": "animations"} - -Remember: Execute immediately. Don't explain - just do.`; +## MCP Tool Execution Protocol + +### Format Requirements +- MANDATORY format: {"param": "value"} +- JSON arguments must be valid JSON (use double quotes for strings) +- **CRITICAL**: Execute ONE tool at a time and wait for the result +- The parser will handle unclosed tags gracefully for recovery +- **NEVER** generate multiple tool calls in a single response +- After each tool execution, analyze the result before deciding next action +- ALWAYS check console after making changes +- The game auto-reloads after editor changes + +### Tool Execution Rules +1. Execute a single tool +2. Read and analyze the tool's response +3. Only then decide if another tool is needed +4. If multiple edits are needed, use plan_tasks first to organize them + +### Available MCP Tools (All operate on the SINGLE editor file) + +#### Understanding the Editor Content +- search_editor: Find text/patterns in the current game code + Parameters: + - query: string - Text or regex pattern to search for + - mode: "text" | "regex" - Search mode (optional, default: "text") + - contextLines: number - Lines of context (optional, default: 2, max: 5) + Example: {"query": "dynamic-part", "mode": "text"} + +- read_editor_lines: Read specific lines from the current game code + Parameters: + - startLine: number - Starting line (1-indexed) + - endLine: number - Ending line (optional, defaults to startLine) + Example: {"startLine": 10, "endLine": 20} + +- read_editor: Read the complete game code currently in the editor + Parameters: none + Example: {} + Note: Use when user asks to "explain the code", "show the code", or needs full context + +#### Modifying the Game +- edit_editor: Make targeted changes to the game code + Parameters: + - oldText: string - Exact text to find and replace (max ~20 lines). MUST include actual content, not just whitespace + - newText: string - Replacement text + Example: {"oldText": "color='#ff0000'", "newText": "color='#00ff00'"} + IMPORTANT: Always include meaningful content (tags, comments, or code) in oldText, never just spaces/newlines + Note: Returns standardized response with game status and console output + +- write_editor: Replace all game code with new content + Parameters: + - content: string - Complete new game code + Example: {"content": "..."} + Note: Use ONLY for starting fresh or complete rewrites + +#### Task Management +- plan_tasks: Create task list for complex operations + Parameters: + - tasks: string[] - Array of task descriptions + Example: {"tasks": ["Add physics component", "Create gravity system", "Test gravity effect"]} + +- update_task: Update task status + Parameters: + - taskId: number - Task ID to update + - status: "pending" | "in_progress" | "completed" + Example: {"taskId": 1, "status": "completed"} + +- view_tasks: View current task list + Parameters: none + Example: {} + +#### Runtime Monitoring +- observe_console: Check console messages and game state + Parameters: none + Example: {} + Note: Returns recent console messages and game status + +## Tool Response Format +All editor modification tools return: +- Success indicator (✅ or ❌) +- Action description +- Game status (Running/Error/Loading) +- Console output from the game + +## Common User Requests (Context Clarification) +When users say: +- "explain the code" → Read and explain the CURRENT editor content +- "what's in the game?" → Describe the entities/elements in the editor +- "fix the error" → Check console, then modify the editor content +- "add a..." → Add new elements to the existing editor content +- "change the..." → Modify existing elements in the editor + +## Execution Patterns + +### Code Writing Rules (Live Environment) +- **NO IMPORTS**: GAME is pre-imported globally - NEVER write \`import * as GAME from 'vibegame'\` +- **NO .run()**: Auto-run is enabled - write \`GAME.withPlugin(MyPlugin)\` not \`GAME.withPlugin(MyPlugin).run()\` +- **Direct access**: Use GAME.defineComponent, GAME.Types, etc. directly +- **Script tags**: When adding JavaScript, use \`