# Gipity platform documentation (full) # Source of truth: https://docs.gipity.ai - per-doc index at https://docs.gipity.ai/llms.txt # What Gipity Can Do The full-stack platform tuned for AI agents. Ask an AI agent to build something real - an app, a dashboard, an API, an agent - and it does one of two things: rebuilds the backend from scratch as fragile one-off code, or stitches together a dozen third parties, each with its own API keys, billing, dashboards, and SDK to learn. Both burn tokens, take more turns, and ship more bugs. Gipity is one full stack, and every layer is tuned for the agent. Low-level primitives - database, storage, CDN, auth, functions, deploy. Kits for the harder parts - multiplayer, vision, media - with no server setup. The most popular third-party APIs are built in, no keys to wire. It's not a walled garden. Gipity's agent-tuned tools are the easy path most builders will want - but you're never boxed in. Bring your own API key for any integration, point at your own AWS, run whatever you want. Great defaults, real exits. The result: fewer tokens, fewer turns, fewer bugs. And Gipity runs what it builds - hosting, monitoring, rollback included. No IDE, no code to read. Use Gipity in your browser, or pair it with your coding agent - Claude Code, Codex, or Grok - in one command. *This is the capability map. For the mission and principles behind Gipity, see [about-gipity](about-gipity.md).* ## Build Apps & Websites Tell the agent what you want and it builds it - HTML, CSS, JavaScript, deployed to a live URL. No IDE, no terminal, no Git. - "Build me a portfolio site" - built, styled, deployed in minutes - "Make a todo app with a database" - full-stack with persistent storage - "Create a multiplayer trivia game" - real-time WebSocket rooms, shared state - "Build a dashboard that shows my sales data" - charts, tables, auto-refresh Apps deploy to `dev.gipity.ai` (testing) or `app.gipity.ai` (production). Auto-deploy mode pushes changes live on every file edit. *Load `web-app-basics` for best practices, `app-development` for backend APIs.* ## 3D World **3D World** is the 3D multiplayer game template on Gipity. All 3D World games share the same visual style, physics engine (Rapier), and multiplayer backend (Gipity Realtime). All files are fully editable. Add a 3D World project with `add name=3d-world` (web agent) or `gipity add 3d-world` (CLI). This creates a playable 3D game with Three.js + Rapier physics + Gipity Realtime multiplayer. Key files: `config.js` (metadata), `settings.js` (tunable values), `strings.js` (display text), `objects.js` (entity factories), `game.js` (orchestrator), plus engine files (`core.js`, `world.js`, `physics.js`, etc.). **Genres:** obby/parkour, tycoon, simulator, PvP combat, shooter, tower defense, horror, racing, RPG, social. **Features:** Opt-in gameplay modules enabled via `config.features`. Available: `rocket-launcher` (projectile weapon with physics explosions). Example: `features: { 'rocket-launcher': true }` in config.js. Features auto-initialize during boot. Regular game requests ("make a wordle", "build a quiz") should use the standard web template - they don't need the 3D template. *Load `3d-engine` for the engine API and a blank-slate template, or `3d-world` for the playable starter, genre recipes, and multiplayer patterns.* ## Backend Services - No API Keys Needed Your deployed apps can call AI models, generate images, create speech, and play sound effects through built-in API services - no third-party accounts or API keys required. - **LLM**: GPT-5, Claude, Gemini - streaming or non-streaming - **Image generation**: on-demand AI images, multiple styles, aspect ratios, and resolution tiers - **Video generation**: AI-generated short videos with audio - **Text-to-speech**: 30 voices, multi-speaker, 60+ languages - **Audio**: Sound effects, music generation, transcription - **Video understanding**: Analyze video content with multimodal AI Two billing modes: you pay (simple) or your app users pay from their own credits. *Load `app-llm`, `app-image`, `app-video`, `app-tts`, `app-audio`, or `app-files` for integration details.* ## Databases Per-project PostgreSQL schemas. Full SQL - create tables, query data, build APIs on top. The agent handles the SQL for you. - "Track my expenses in a database" - schema designed, tables created, data entry via chat - "Build an API for my app's leaderboard" - serverless functions callable from deployed apps Database count is per-plan (call `credits_products` to see your limit). *Load `app-development` for functions, database setup, and API patterns.* ## Code Execution Sandboxed Docker containers with JavaScript (Node 20), Python 3, Bash, and 60+ pre-installed CLI tools. Data analysis, file conversion, image processing, document generation - all without leaving chat. - "Analyze this CSV and make a chart" - pandas + matplotlib, chart saved to files - "Convert this PDF to Word" - LibreOffice headless - "Resize all these images to 800px wide" - ImageMagick one-liner - "Cross-compile this C program for Windows" - mingw-w64 *Load `sandbox-tools` for the full tool and library list.* ## Automate Anything Multi-step workflows triggered by schedule, webhook, or manual run. Each step is an AI call with full tool access. - "Send me a daily weather briefing at 7am" - scheduled workflow - "Monitor this website and alert me if the price drops" - scheduled check + email - "Every Monday, summarize my calendar and email me" - Gmail + Calendar + email Human-in-the-loop approval steps for anything that needs sign-off before proceeding. *Load `workflow` for cron syntax and patterns, `approvals` for approval flows.* ## Generate Media Images, speech, sound effects, and music - generated on demand and saved to your files. - "Generate a logo for my app" - AI image generation - "Read this article aloud" - text-to-speech - "Make a thunder sound effect" - sound effect generation - "Create 30 seconds of lo-fi background music" - music generation *Load `tts` for voice and audio details.* ## Search the Web & Social Media Real-time web search and Twitter/X search (last 7 days). The agent can research current events, find documentation, check prices, and monitor social media. *Load `web-search` for operators and tips.* ## Connect External Services Gmail, Google Calendar, Slack, GitHub, Todoist, Notion, Telegram, Discord - connect accounts and the agent can read, send, and manage on your behalf. *Load `google-services`, `service-call`, `telegram-connector`, or `discord-connector` for setup.* ## Persistent Memory The agent remembers what matters across conversations. Two scopes: - **Agent memory** - personal, available in all projects (preferences, facts, context) - **Project memory** - shared across agents in a project (specs, decisions, architecture) You can ask the agent to remember or forget anything. Memory is also auto-extracted from conversations. ## File Management & Recovery Per-project file workspaces with full CRUD. Files generated by tools (images, audio, video, sandbox outputs) are saved as project files automatically. Every file write is tracked with immutable version history - you can view versions, restore individual files, or roll back entire directories to any point in time. *Load `version-history` for recovery options.* ## Multi-Model AI Claude (Sonnet, Opus, Haiku) and GPT (GPT-4.1, GPT-5) families. Switch models per agent. Extended thinking mode for complex reasoning. Cross-model queries to get second opinions. *Load `query-llm` for cross-model queries.* ## User Authentication for Apps Apps on Gipity can authenticate Gipity users with zero setup - session cookies, no OAuth configuration needed. *Load `app-auth` for implementation details.* ## Real-Time Multiplayer WebSocket rooms for games and collaborative apps. Relay (message broker) or state (server-authoritative) modes. Up to 200 clients per room. *Load `app-realtime` for room types and client code.* ## Relays: drive your coding agent from the web CLI A **relay** is what runs your coding agent (Claude Code, Codex, Grok, or opencode) for your web-CLI chats. Two kinds: - **On your own computer** (cheapest - uses your existing Claude, Codex, or Grok subscription, not per-token API billing; opencode instead runs the curated open coding models - DeepSeek, Qwen, Kimi, GLM families - billed via your Gipity credits, no third-party account needed). After installing the `gipity` CLI locally, run `gipity connect` - one command connects this computer to gipity.ai so the web CLI can drive it: it logs you in, pairs this machine, starts the relay in the background, and installs the OS login service so it auto-starts, then stops WITHOUT launching a coding agent. (`gipity build` runs the same pairing as its first-run prompts and then launches a session with your chosen project and agent; `gipity relay install` sets up just the login service on an already-paired machine.) - **Cloud relay** (Gipity-hosted - bring your own Anthropic API key, billed per token). For when you have no spare computer to run on. Add your key in the web CLI: run `/relay` (or `/chat claude`) with no relay yet and it offers an inline prompt to paste your `sk-ant-api…` key; `/relay key` manages it later, and the Settings page (gear icon) has an "API keys" section with the same Cloud Relay Anthropic key. Then in the web CLI start a chat with `/relay` (or `/chat claude`) - your prompt switches to `claude>` and each message dispatches to the relay, runs your coding agent (`gipity build -p ""`), and streams the conversation back into the terminal view. Every prompt, tool call, and assistant turn is recorded to your Gipity project - from Claude Code, Codex, Grok, and opencode alike - and viewable live in the web client. `exit` returns to the normal Gipity prompt. Check pairing state anytime with `gipity relay status`; `gipity relay revoke` unpairs and removes the login service. **Switch the model for a relay chat with `/model `** (or any catalog id) right from the web CLI `claude>` prompt - the choice is stored per-chat and forwarded to your local coding agent as `--model` on your next message. Relay chats run on your own agent subscription, so there are no Gipity credits involved. Locally, `gipity build --model ` works the same way - the flag passes straight through to the underlying agent binary (it is NOT a Gipity-only flag), so `gipity build --model opus` is valid and launches the session on Opus. ## Credits & Billing Gipity runs on **credits**. Use this to answer the common "wait, am I going to get charged / will everything break?" worry plainly: - **What spends credits:** the AI and media work - chatting with the agent, and an app's calls to the LLM, image, video, TTS, audio, and transcribe services. Each call shows its `credits_used`. - **What's free (never credit-gated):** deploying, databases, file storage, code execution, `location`, and `notify` (web push). Running out of credits stops the AI calls **only** - it does not take your app, data, or site offline. So the honest answer to "will other things break?" is no. - **Free credits refresh** on the Free plan, so a free user who hits zero gets more automatically after a short wait (the out-of-credits message says when). - **Who pays for an app's AI calls** is the app's billing mode: `user_pays` (default - each signed-in end user spends their own credits, so publishing never bills you for what users consume) or `owner_pays` (you subsidize it). See `app-llm`. - **Check balance & top up:** `gipity credits` shows the balance; `gipity credits buy` opens the purchase page; the `credits_products` tool lists plans/packs. The Monitor dashboard's **Plan** tab shows balance, burn rate, and days-until-empty. ## Platform Limits Storage, database count, concurrent chats, and deploy rate are all per-plan. Run the `credits_products` tool to show the user's plan and what it includes. ## Where to Start Just ask. "Build me a website." "Analyze this data." "Set up a daily email." "What can you help me with?" The agent will figure out the tools, the code, and the deployment. You describe what you want - the AI handles the how. --- # 2D Game - Phaser 3 Games **2D Game** is the Phaser-based game template on Gipity. All files are fully editable - no locked template layer. Uses Phaser 3.80.1 via CDN with arcade physics. **When to use this:** When the user asks for a 2D game - platformer, side-scroller, arcade, puzzle, endless runner, top-down, shooter, or RPG. For simple games (wordle, quiz, card games), use `web-simple`. For 3D multiplayer apps, use `3d-engine` for a blank template or `3d-world` for a playable rocket-launcher starter. ## Quick Start - Start Here **STRONGLY RECOMMENDED:** Begin every 2D game by adding the `2d-game` template with `add`. It sets up Phaser 3, boot/game scenes, config, settings, plus generated icons, a PWA manifest, and a social share card. Only hand-roll files if the user explicitly tells you to skip the template. ``` add name=2d-game title="" ``` **Starting over in an existing project:** If `src/` already exists and the user wants a clean rebuild, call `file_delete` on `src` first, then run `add` normally. Or pass `force=true` to `add` to overwrite in one step - destructive, so confirm with the user first. Unrelated content (media, data, notes) is preserved either way. **Naming:** Use the user's name verbatim if given. If they didn't specify, blend "Gip" or "Gipity" into the name (e.g. "Gipity Racer", "Gip Tac Toe") - be creative but don't force it. This creates a playable game immediately - colored rectangle player, arcade physics, ground platform, and controls that work on desktop AND mobile out of the box (WASD/arrows + Space on keyboard; a floating virtual joystick, Jump button, and fullscreen toggle on touch devices). Then edit `scenes/game.js` and `settings.js` to build your game. ## Project Structure ``` src/ index.html - Phaser CDN, game container, module entry js/ config.js - Phaser.Game config, scene registration controls.js - Touch controls overlay (virtual joystick, buttons, fullscreen) settings.js - Tunable values (canvas, colors, physics, player, gameplay) strings.js - User-facing display text scenes/ boot.js - Preloader with progress bar game.js - Main game scene (gameplay logic) css/ styles.css - Page layout, canvas styling images/ - generated app icons + share card (favicon-*.png/.ico, apple-touch-icon.png, og-image.png); customize with `gipity brand set --emoji `, then redeploy ``` ## Phaser Essentials ### Scene Lifecycle Every scene has three key methods: - `preload()` - load assets (images, spritesheets, audio) - `create()` - set up game objects, physics, input - `update()` - runs every frame, game logic here ### Loading Assets In `boot.js` preload(): ```js this.load.image('player', './images/player.png'); this.load.spritesheet('hero', './images/hero.png', { frameWidth: 32, frameHeight: 48 }); this.load.audio('jump', './audio/jump.mp3'); ``` ### Creating Game Objects ```js // Sprite (from loaded image) this.player = this.physics.add.sprite(400, 300, 'player'); // Rectangle (no image needed) this.player = this.add.rectangle(400, 300, 32, 48, 0xf26522); this.physics.add.existing(this.player); // Static group (platforms) this.platforms = this.physics.add.staticGroup(); this.platforms.create(400, 568, 'ground'); ``` ### Physics (Arcade) ```js // Gravity is set in config.js via settings.physics.gravity body.setVelocityX(speed); body.setVelocityY(jumpForce); // negative = up body.setBounce(0.2); body.setCollideWorldBounds(true); // Collisions this.physics.add.collider(player, platforms); this.physics.add.overlap(player, coins, collectCoin, null, this); ``` ### Input ```js // Keyboard this.cursors = this.input.keyboard.createCursorKeys(); // up, down, left, right if (this.cursors.left.isDown) body.setVelocityX(-speed); // Specific keys this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); // Touch/pointer this.input.on('pointerdown', (pointer) => { ... }); ``` ### Text ```js this.add.text(400, 30, 'Score: 0', { fontSize: '24px', color: '#ffffff', fontStyle: 'bold', }).setOrigin(0.5); ``` ### Camera ```js this.cameras.main.startFollow(this.player, true, 0.1, 0.1); this.cameras.main.setBounds(0, 0, worldWidth, worldHeight); ``` ## Common Phaser 3 Pitfalls These are the failure modes that most often turn the whole screen black. Read before writing a game scene. - **`Graphics.fillEllipse` does NOT exist.** Phaser 3 Graphics has `fillCircle(x, y, r)`, `fillRect`, `fillTriangle`, `fillRoundedRect`, and `beginPath` + `arc` + `fillPath` - but no ellipse drawcall. For an oval, use `fillCircle` and set `gfx.scaleX` / `gfx.scaleY`, or draw it with `beginPath`/`arc`/`fillPath`. - **Don't attach physics to a raw `Graphics` object.** Graphics has no width/height for the body to size against, so the body ends up with `{0,0}` dimensions or drifts from the drawn shape. Use a `Sprite` (from a loaded image) or a `Rectangle` / `Zone` for the physics body, and draw a follower `Graphics` that tracks `body.x`/`body.y` each frame. - **Set world bounds for scrolling worlds.** The default physics world is the canvas size. For a side-scroller or large map, call `this.physics.world.setBounds(0, 0, worldWidth, height)` in `create()` - otherwise platforms and objects beyond the canvas get ignored by physics. - **Use object hashes, not sparse arrays, for spatial maps.** `this.segments = {}` with string/number keys works; `this.segments = []` with large indices becomes sparse and breaks `Object.keys`/`values` enumeration in subtle ways. - **Don't double-create physics bodies.** Either call `this.physics.add.existing(obj)` OR add `obj` to a `staticGroup` with `group.add(obj)` - not both. Double-creation silently misplaces the body relative to the visual. - **DOM inputs never receive the game's captured keys unless you shield them.** Phaser registers its keys (WASD, Space, arrows via `addKeys`/`createCursorKeys`) as "captured": its window-level listener calls `preventDefault()` on them no matter where focus is, so a player literally cannot type W/A/S/D or Space into a high-score name field or chat box overlaid on the canvas. The template ships a shield in `js/config.js` (a document-level `keydown`/`keyup` listener that `stopPropagation()`s events targeting `input`/`textarea`/`select`/contenteditable, plus `resetKeys()` on focus) - keep it, and if you add your OWN `window`/`document` key listeners, start them with a guard: `if (e.target.closest('input, textarea, select') || e.target.isContentEditable) return;`. Test any name-entry UI by actually typing the movement keys into it. - **Always verify renders, not just console.** A missing API or a bad draw can throw once during init and leave a black canvas with nothing further to report. After deploying, capture a screenshot and look at it - `gipity page screenshot ` from the CLI, or the `browser` agent tool's `screenshot` action in chat - don't trust "clean console" as proof. If gameplay only starts after a "play" click, drive it in the same shot so you capture the game and not the menu: `gipity page screenshot --action "document.getElementById('play').click()"` (use your start button's selector). ## Build Incrementally For anything non-trivial, don't write the whole game in one `Write` call. Work in small, verified steps: 1. Add the template (`gipity add 2d-game`) and deploy - confirm the starter game renders. 2. Customize ONE element (e.g. replace the player rectangle with your sprite). Deploy, screenshot, confirm it renders. 3. Add the next element (ground, enemies, collectibles) one at a time, deploying and verifying between each. 4. Only after the core loop works, layer on polish (parallax, particles, HUD, touch controls). A 500-line rewrite of `scenes/game.js` is very hard to debug when something breaks - a single bad API call turns the whole screen black with no useful error. Small steps keep the failure surface tiny. ## Verification After Deploy After every `gipity deploy dev`: - Run `gipity page inspect ` to surface console errors. - **On the first deploy of a new game**, and any time you've made significant visual changes, also capture a screenshot and look at the image - `gipity page screenshot ` (CLI) or the `browser` agent tool's `screenshot` action (chat). To capture actual gameplay rather than the title screen, start the game in the same shot: `gipity page screenshot --action "document.getElementById('play').click()"`. A clean console is NOT sufficient proof for Canvas/WebGL apps - render failures are often silent. - If you see a black screen with a clean console: assume a sync error fired during Phaser init (most commonly a missing API like `fillEllipse`, or physics attached to a raw Graphics). Re-read the "Common Phaser 3 Pitfalls" section above before rewriting. ### Asserting on real game state (score, lives, collisions, win/lose) A screenshot proves the game *renders*; it can't prove the ball bounces, the score increments, or the win screen ever fires. Drive the **live game object** instead. `js/config.js` exports the Phaser game, and `index.html` loads it as a module - so a dynamic `import()` from `gipity page eval` resolves out of the browser's module cache and hands you **the running instance** (relative specifiers resolve against the page URL). No second game boots. **Never ship a `window.game` debug hook to do this.** That leaves instrumentation in your production bundle and costs an extra deploy. ```bash gipity page eval "" " const { game } = await import('./js/config.js'); const s = game.scene.getScene('Game'); s.startGame(); // call your own scene methods return JSON.stringify({ score: s.score, lives: s.lives, state: s.state }); " ``` For a longer driver (play a full round, force a win, assert the game-over overlay), put the script in a file and pass `--file` - no shell quoting, and the body may use `await` and `return`: ```bash gipity page eval "" --file ./tests/drive-game.js --json ``` The eval body has a **~20s in-page budget**, so split a long sequence into one call per state you're verifying. **Never wait wall-clock time for physics/animation to play out** - the headless browser can paint at ~15 fps, so `setTimeout(2000)` advances far less than 2s of game time and assertions report false negatives. `config.js` also exports `advance(seconds)`: it pauses the browser loop and ticks Phaser's TimeStep by hand, so N simulated seconds (physics, timers, collisions) run in milliseconds, deterministically: ```bash gipity page eval "" " const { game, advance } = await import('./js/config.js'); const s = game.scene.getScene('Game'); s.startGame(); advance(5); // 5s of game time, instantly return JSON.stringify({ score: s.score, bricks: s.bricks.countActive() }); " ``` To *capture* a driven state visually, `page screenshot --action ""` runs the same kind of script before the shot - same async body, same app-relative `import()`. If the action throws, you still get the image, plus a `⚠ --action failed:` line telling you it shows the **undriven** page: ```bash gipity page screenshot "" -o win.png \ --action "const { game } = await import('./js/config.js'); game.scene.getScene('Game').winGame();" ``` Write throwaway driver scripts and screenshots **outside the project directory** (e.g. `/tmp`) - the project auto-syncs to Gipity, so scratch files land in the user's storage. If they must live in the project, add the path to `.gipityignore`. ### Animations ```js this.anims.create({ key: 'walk', frames: this.anims.generateFrameNumbers('hero', { start: 0, end: 3 }), frameRate: 10, repeat: -1, }); this.player.anims.play('walk', true); ``` ## Adding a New Scene 1. Create `src/js/scenes/myScene.js`: ```js export class MyScene extends Phaser.Scene { constructor() { super('MyScene'); } create() { /* ... */ } update() { /* ... */ } } ``` 2. Register in `config.js`: ```js import { MyScene } from './scenes/myScene.js'; // Add to scene array: scene: [Boot, Game, MyScene], ``` 3. Switch scenes: `this.scene.start('MyScene');` ## Genre Recipes ### Side-Scroller / Platformer - Keep gravity enabled (default) - Add platforms as static physics bodies - Camera follows player: `this.cameras.main.startFollow(player)` - Set world bounds larger than canvas: `this.physics.world.setBounds(0, 0, 3200, 600)` ### Top-Down (Zelda-style) - Disable gravity: set `settings.physics.gravity = 0` - Add 4-way movement (up/down/left/right) - Use `body.setVelocity(x, y)` for diagonal movement ### Endless Runner - Auto-scroll: move obstacles left each frame, spawn new ones off-screen right - Single input: jump on tap/space - Increase speed over time: `speed += dt * acceleration` ### Arcade / Shooter - Bullets: `this.physics.add.group()` with `body.setVelocity()` - Enemy spawning: `this.time.addEvent({ delay: 1000, callback: spawnEnemy, loop: true })` ### Puzzle - Disable gravity, disable physics or use minimal physics - Grid-based: snap positions to grid `Math.round(x / tileSize) * tileSize` - Input: pointer clicks to select/move pieces ## Mobile / Touch The template is mobile-ready by default via `js/controls.js` - a DOM overlay above the canvas that renders only on touch devices (nothing shows on desktop). It gives the standard mobile-game layout: a **floating virtual joystick** on the left half (the pad appears wherever the thumb lands), **round action buttons** bottom-right, and a **fullscreen toggle** top-right (auto-hidden where the Fullscreen API is unavailable, e.g. iPhone Safari - there, "Add to Home Screen" runs fullscreen via the template's PWA meta tags). Multi-touch is tracked per pointer, so joystick + buttons work simultaneously. ```js import { touch, initTouchControls } from '../controls.js'; // create(): declare the buttons your game needs (first = primary, biggest) initTouchControls({ buttons: [{ id: 'jump', label: 'Jump' }, { id: 'fire', label: 'Fire' }] }); // tap-only game? disable the joystick so it doesn't swallow lower-left taps: // initTouchControls({ joystick: false, buttons: [...] }); // update(): merge with keyboard - joystick is analog (-1..1) const keyX = (cursors.right.isDown ? 1 : 0) - (cursors.left.isDown ? 1 : 0); const moveX = keyX !== 0 ? keyX : touch.x; // touch.y for vertical body.setVelocityX(moveX * speed); if (touch.isDown('jump')) { /* held */ } if (touch.justPressed('fire')) { /* once per press */ } ``` Always keep desktop AND touch input working: keyboard = WASD + arrows (both), plus the matching touch buttons. `touch.enabled` tells you touch controls are active (e.g. to swap instruction text). Phaser still handles in-canvas taps via pointer events (`this.input.on('pointerdown', ...)`), with 3 active pointers configured. For mobile-responsive canvas, the template uses `Phaser.Scale.FIT` + `CENTER_BOTH` by default - the game keeps its `settings.canvas` coordinate system and letterboxes to fit any screen or orientation. The page is hardened for games (no pinch-zoom, no overscroll, no text selection; `dvh` viewport). ## Deploy Verification Verify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values). `gipity deploy dev --inspect` deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture `gipity page screenshot ` and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass. Full loop - reading function logs, calling a function directly, driving the page: the `app-debugging` skill. --- # 3D Engine - Minimal 3D Multiplayer Template **3D Engine** is the blank-slate 3D multiplayer template on Gipity. It boots a Three.js scene, a Rapier physics world, a Gipity Realtime multiplayer client, and exposes the engine API - with no gameplay. Use it when the user wants to build a fresh 3D app without inheriting a demo's logic. **When to use this:** When the user describes a new 3D or multiplayer app (e.g. "3D Boggle", "voxel painting", "3D chat room") and you want a clean starting point. If they want a playable reference with rockets, a demo scene, and spawn logic already wired, add the `3d-world` starter instead. ## Quick Start ``` add name=3d-engine title="" ``` Out of the box: a controllable character on a lit ground plane, multiplayer connected. No gameplay - that's yours. **Naming:** Use the user's name verbatim if given. If they didn't specify, blend "Gip" or "Gipity" into the name. ## Project Structure After adding the template, all files are in `src/` and fully editable. Two-layer split: **Your code (edit these):** - `game.js` - entry point. Calls `setConfig`, `onInit`, `onUpdate`. Start here. - `config.js` - project metadata and feature flags. - `settings.js` - tunable values (world size, colors). Add your own. - `strings.js` - user-facing text. Localized via `translations.js`. - `scene.js` - scene builder. Currently creates one ground plane. Replace with your world. - `css/game.css` - game-specific styles. Empty stub. **Engine (leave as-is unless you know what you're doing):** - `core.js` - boot sequence and render loop. Re-exports every engine module. - `world.js` - Three.js scene, camera, renderer, lighting. - `physics.js` - Rapier physics world, raycasting, triggers. - `network.js` - thin 3D facade over the `@gipity/realtime` kit (presence + world-state channels). - `network/adapter-3d.js` - the only 3D-aware networking code: bridges `@gipity/realtime` to Three.js / Rapier. - `packages/realtime/` - the engine-agnostic `@gipity/realtime` kit (transport, channels, host election, sync). Self-describing: see its `README.md`, `contracts/`, and `examples/`. Reusable by any app, not just games. - `player.js` - character controller, booted by `game.js` via `player.initPlayer()`. A dynamic rigid body (added mass 2, rotation locked): it knocks over dynamic Parts on contact and never topples. - `primitives.js` - Part system and workspace. `PART_DEFAULTS` at the top lists every Part property with its exact default. - `constraints.js` - joint/constraint system. - `assets.js` - geometry builders and `MATERIAL_PRESETS` (per-material friction/elasticity + visual settings). - `shapes.js` - voxel shape library (cubes, spheres, arches, towers). - `utils.js` - helpers (randInt, onCircle, placeVoxels). - `features.js` - opt-in feature registry. Empty by default. - `ui.js` - HUD, loading screen, message overlay. - `mobile.js` - touch controls overlay (floating joystick, Jump/Action buttons, camera drag, fullscreen toggle). Auto-shown on touch devices; wired by `player.js`. - `css/engine.css` - engine styles. Each file's top comment explains what it does; read one only when you need a detail this skill doesn't already give you. The full engine API surface - every call, its arguments, and its return shape - is documented below, so you should be able to build without grepping the source. ## Engine API All engine modules are available via a single import: ```js import { world, assets, physics, player, network, ui, THREE, onInit, onUpdate, setConfig, primitives, constraints, workspace, features, advance, whenReady } from './core.js'; ``` ### Verifying it headlessly `advance(seconds)` steps the world at a fixed timestep with rendering skipped, and `whenReady()` resolves once boot finishes. Drive them from `gipity page eval` instead of waiting on real time: the headless browser paints at ~2-3 fps and the loop caps its frame delta, so a wall-clock wait advances the simulation ~12x slower than it looks and reports collisions that never happened. ```js const core = await import('./js/core.js'); await core.whenReady(); core.advance(3); // 3s of world time, ~110ms real, bit-identical every run ``` Both are exported from `core.js`. See the `app-debugging` skill. ### Lifecycle ```js setConfig(config); // set once at import time onInit(async () => { ... }); // runs after engine boots onUpdate((dt) => { ... }); // runs every frame ``` The starter `game.js` already wires these up - edit its bodies. ### Before you build: two defaults that will surprise you The template ships wired for a multiplayer building toy. If you're building something else - especially a solo physics sandbox - decide on each of these before writing your scene: 1. **Multiplayer is ON.** `config.js` ships `features: { multiplayer: {...} }` enabled, which opens a Gipity Realtime connection. Going solo is a **one-line edit**: set `features: {}` in `config.js` and no network client loads. **Leave `gipity.yaml` alone** - its `realtime` phase just registers a room name (no server, no cost, no plan limit), and an unconnected room does nothing. Don't rewrite the manifest to strip it. 2. **Part `mass` is the body's TOTAL mass in kg.** Leave it unset and a Part weighs its volume at density 1 (a 2x2x2 crate = 8 kg); set it and the body weighs exactly that (`mass: 0.5` really is 0.5 kg). The player is a dynamic body with locked rotation and ~2 added mass (`player.js`), so the mass ratio decides contact physics: light Parts scatter when run into, a wall of heavy Parts barely budges. If the headline interaction is "run into stuff and scatter it", set Part `mass` at or below ~1 - or shove via `player.applyKnockback` / an impulse (see Physics below) rather than relying on body-to-body contact alone. ### The player `game.js` already calls `player.initPlayer()` in `onInit` - WASD/arrows, space to jump, mouse look, scroll zoom, E / left-click for action. Pass options to tune it: ```js player.initPlayer({ speed: 12, jumpForce: 18, color: 0xf26522, camera: { distance: 20, sensitivity: 0.003, invertY: false }, }); ``` **Camera comfort:** the player module bakes in **Invert-Y** and adjustable **sensitivity**, persisted per-browser in `localStorage` (`player.setInvertY`/`getInvertY`, `player.setSensitivity`/`getSensitivity`; players can toggle invert live with the **I** key). For any first-person app, wire a **visible** toggle button + sensitivity slider to these — bad mouse-look is the top "controls feel wrong" complaint, so treat it as a default, not an extra. **Mobile is automatic:** on touch devices `initPlayer` shows the standard mobile layout via `mobile.js` — floating joystick (left half) for movement, drag-anywhere camera, Jump/Action buttons bottom-right (Action = `inputState.action`, same as the E key), and a fullscreen toggle top-right. Nothing renders on mouse-only desktops. To stay mobile-compatible, read input from `player.inputState` instead of raw key events, and trigger game actions from `inputState.action`/`inputState.jump`. **Typing into form fields is already shielded:** the template's own key handlers (movement, panel hotkeys) ignore key events targeting an `input`/`textarea`/`select`/contenteditable and clear held keys when a field gains focus, so players can type W/A/S/D/Space into a name-entry or chat box without the avatar moving. If you add your OWN `window`/`document` key listener, keep that property: start it with `if (e.target.closest('input, textarea, select') || e.target.isContentEditable) return;` (note `freezePlayer()` does NOT do this for you - it freezes movement integration, not key handling). **`initPlayer` config** (all optional): `speed`, `jumpForce`, `color`, `crosshair` (bool), `x`/`y`/`z` spawn (a `createSpawnPoint`, if one exists, wins over these), and a `camera` object: `{ distance, minDistance, maxDistance, heightOffset, sensitivity, invertY, scrollSpeed, mode, topDownHeight }`. **Player runtime API:** ```js player.getPosition(); // -> { x, y, z } player.setPosition(x, y, z); // teleport player.getAimDirection(); // -> normalized { x, y, z } the camera faces player.getAimOrigin(); // -> { x, y, z } CAMERA position - in third-person up to ~25 units BEHIND the player player.aimRaycast(reach); // -> { point, distance, collider, body, part } under the crosshair within reach OF THE PLAYER, or null. part is null for ground/scenery player.grab(part); // carry a Part in front of the crosshair; false for anchored/scenery. opts: { distance } player.release({ throwSpeed: 40 }); // drop (omit throwSpeed) or throw; -> the released Part or null player.getHeldPart(); // -> the carried Part or null player.inputState; // live { forward, right, jump, action, mouseDown, sprint } player.velocity; // live velocity vector player.isGrounded(); // bool player.applyKnockback(dir, strength); // shove the player: direction { x, y, z } + strength scalar (capped at 40) player.cameraControl.mode = 'firstPerson';// 'orbit' (default) | 'firstPerson' | 'topDown' | 'fixed' player.cameraControl.setFixedPosition({ x, y, z }); // for mode 'fixed' player.cameraControl.setFixedLookAt({ x, y, z }); player.freezePlayer(); player.unfreezePlayer(); player.isFrozen(); player.setPlayerColor(0xff0000); player.getPlayerColor(); ``` ### Building a scene: Parts `primitives.createPart(...)` is the workhorse - the universal 3D primitive (3D World's equivalent of a Roblox BasePart). **Parts are dynamic (gravity-on) by default.** Set `anchored: true` to fix one in place (floors, walls, static structures). `createPart` returns the Part object; its internal Rapier body is on `part._body` (needed for the impulse calls below). ```js import { primitives } from './core.js'; // Dynamic block - falls, collides, can be knocked around const crate = primitives.createPart({ position: { x: 0, y: 5, z: 0 }, size: { x: 3, y: 3, z: 3 }, color: 0x2196F3, material: 'wood', }); // Anchored floor - no gravity, immovable primitives.createPart({ position: { x: 0, y: 0, z: 0 }, size: { x: 40, y: 1, z: 40 }, anchored: true, material: 'concrete' }); // Sub-voxel shape (each Part is a 3x3x3 sub-voxel grid) primitives.createPart({ position: { x: 3, y: 1, z: 0 }, shape: primitives.SHAPES.STAIR, color: 0x4CAF50 }); ``` **Part properties** (pass any subset to `createPart`; defaults from `PART_DEFAULTS` in `primitives.js`): | Property | Default | Notes | |----------|---------|-------| | `position` | `{0,0,0}` | world position | | `rotation` | `{0,0,0,1}` | quaternion | | `size` | `{1,1,1}` | world units | | `anchored` | `false` | `true` = static, no gravity | | `canCollide` | `true` | | | `mass` | auto | TOTAL kg; unset = volume at density 1 - see the mass note above | | `friction` | `0.5` | overridden by `material` preset | | `elasticity` | `0.3` | overridden by `material` preset | | `linearDamping` | `0.1` | | | `angularDamping` | `0.5` | | | `color` | `0x888888` | | | `material` | `'plastic'` | sets friction+elasticity+look | | `transparency` | `0.0` | 0 opaque … 1 invisible | | `shape` | `SHAPES.FULL` | sub-voxel shape mask | | `castShadow` / `receiveShadow` | `true` | | **Materials** (`material:` name, from `MATERIAL_PRESETS` in `assets.js`; each sets friction + elasticity + visual style): | Material | friction | elasticity | Feel | |----------|----------|-----------|------| | `plastic` (default) | 0.5 | 0.3 | neutral | | `metal` | 0.4 | 0.2 | shiny, heavy-looking | | `wood` | 0.6 | 0.2 | matte | | `glass` | 0.2 | 0.1 | translucent | | `neon` | 0.3 | 0.2 | emissive | | `ice` | 0.05 | 0.1 | slippery | | `grass` | 0.7 | 0.1 | high-grip | | `sand` | 0.8 | 0.05 | high-grip, dead bounce | | `concrete` | 0.8 | 0.1 | high-grip | An unknown material name warns in the console and falls back to `plastic`. **Sub-voxel shapes** (`primitives.SHAPES`): `FULL`, `SLAB`, `HALF`, `STAIR`, `SLOPE`, `CORNER`, `PILLAR`, `ARCH`. **Runtime changes, queries, spawn points:** ```js primitives.setProperty(crate, 'anchored', true); // freeze in place primitives.setProperty(crate, 'material', 'ice'); // updates physics + visual primitives.setProperty(crate, 'color', 0xff0000); primitives.queryParts({ color: 0xff0000, anchored: false }); // -> matching Parts primitives.getPart(id); primitives.getParts(); // lookup / all primitives.removePart(crate); primitives.createSpawnPoint({ position: { x: 0, y: 2, z: 0 }, teamColor: 0xff0000 }); ``` **Compound blocks** - a grid of welded sub-Parts that shatter on impact (destructible crates, breakable walls): ```js const block = primitives.createCompoundBlock({ position: { x: 5, y: 1.5, z: 0 }, color: 0xff0000, breakForce: 8, // velocity-delta threshold; higher = harder to break gridSize: 3, // sub-blocks per axis (3 -> 27) blockSize: 1, material: 'wood', colorVariation: true, }); block.break(block.parts[0]); // free one sub-block block.breakAll(); // shatter everything block.isIntact(); // any welds left? ``` For quick voxel structures there's also a shapes library: ```js import { placeVoxels } from './utils.js'; import { solidCube, voxelSphere, arch } from './shapes.js'; placeVoxels(arch(5, 6), { x: 0, y: 0, z: 10 }, 0x9C27B0); ``` ### Assets: geometry, models, sounds ```js assets.createVoxelGround(width, depth, color); // instanced ground plane - ONE draw call, use it for terrain assets.createVoxelBox(color, size); // simple colored cube assets.spawn(name, { x, y, z }, scale); // load a model and add it to the scene assets.despawn(model); assets.loadModel(name); // remove / load (returns a clone) assets.playSound(name, { volume }); // sound effect assets.getTexture(name); // texture ``` Models, sounds, and textures load by name from the shared CDN. For anything the pack doesn't have, build geometry with `primitives.createPart` or THREE.js directly. ### Physics: moving, ray-testing, and detecting Import `physics` from `./core.js`. The impulse calls take a Rapier **body** - use `part._body` from a Part, or `player`-side helpers. ```js // Push a body. applyImpulseAtPoint adds torque from the offset, so edge hits spin. physics.applyImpulse(crate._body, { x: 0, y: 0, z: 40 }); // pure shove, no spin physics.applyImpulseAtPoint(crate._body, { x: 0, y: 2, z: 40 }, hitPoint); // shove + realistic spin // Raycast. Returns { point, distance, collider, body, part } or null; hit.part is // the Part that was hit (null for ground/scenery), already resolved for you. // 4th arg excludes a body (pass the player's own so a shot doesn't hit the // shooter). For a reach-limited crosshair ray use player.aimRaycast(reach). const hit = physics.castRay(player.getAimOrigin(), player.getAimDirection(), 100, /* excludeBody */ null); if (hit?.part) primitives.removePart(hit.part); // Broadphase proximity query. Returns [{ collider, body }] within radius (explosions, triggers). const nearby = physics.queryNearby({ x: 0, y: 0, z: 0 }, 5); for (const { body } of nearby) physics.applyImpulse(body, { x: 0, y: 10, z: 0 }); ``` **Carry / stack / throw** - `grab`/`release` are the whole loop; the engine pins the held Part in front of the crosshair each frame: ```js // on action (player.inputState.action / mouseDown): toggle carry, else place a block if (player.getHeldPart()) player.release({ throwSpeed: 40 }); else { const hit = player.aimRaycast(10); if (hit && !player.grab(hit.part)) // grab() is false for the ground / anchored scenery primitives.createPart({ position: hit.point, size: { x: 3, y: 3, z: 3 } }); } ``` Lower-level colliders (most apps use Parts instead, but these exist): `physics.addStaticBox(pos, halfExtents)`, `physics.addDynamicBox(pos, halfExtents, mass)`, `physics.addKinematicBody(pos, halfExtents)`, `physics.addTrigger(pos, halfExtents, { onEnter, onExit })` (sensor zone), `physics.removeBody(body)`, and the fully-optioned `physics.addBody(pos, halfExtents, { type, rotation, mass, friction, restitution, linearDamping, angularDamping, isSensor, convexHullPoints })`. ### Constraints: joints between Parts Import `constraints` from `./core.js`: ```js constraints.weld(partA, partB); // rigid lock constraints.hinge(frame, door, { axis: { x:0,y:1,z:0 }, limits: [-90, 90] }); // one-axis rotation constraints.spring(partA, partB, { stiffness: 100, damping: 10 }); // elastic constraints.getAll(part); constraints.remove(c); constraints.removeAll(part); ``` ### Workspace: gravity, snap, lighting ```js workspace.gravity = { x: 0, y: -20, z: 0 }; // default { x: 0, y: -40, z: 0 }; live - takes effect immediately and wakes sleeping bodies. Applies to everything dynamic, including the player. workspace.snapEnabled = true; // opt in to Roblox-style snap-together building: dynamic Parts within snapDistance weld into one rigid body (default false) workspace.snapDistance = 0.15; // weld radius when snap is on workspace.onSnap((a, b) => { /* two Parts just welded */ }); ``` `workspace.lighting` is a live proxy - assigning any field updates the Three.js lights immediately. The complete set, with defaults: ```js workspace.lighting.timeOfDay = 18; // 12; 0-24 (0 midnight, 6 sunrise, 12 noon, 18 sunset) - drives sun angle + color workspace.lighting.sunIntensity = 2.0; // 2.0 workspace.lighting.sunColor = 0xfff5e6; // 0xfff5e6 workspace.lighting.ambientIntensity = 0.55; // 0.55 - raise to lift shadow detail workspace.lighting.hemisphereIntensity = 0.85; // 0.85 - sky/ground bounce workspace.lighting.skyColor = 0x87ceeb; // also sets background + fog color workspace.lighting.groundColor = 0x556b2f; workspace.lighting.fogEnabled = true; // true workspace.lighting.fogNear = 40; workspace.lighting.fogFar = 120; ``` ### UI: HUD, panels, messages Import `ui` from `./core.js`: ```js ui.setHud('top-left', 'Score: 0'); // slots: top-left|top-right|bottom-left|bottom-right|center ui.clearHud('top-left'); ui.showMessage('Nice tower!', 3000); // transient centered toast // A self-placing stats panel with labeled rows (auto-appends + positions itself; optional toggle key): const panel = new ui.InfoPanel({ title: 'Stats', position: 'top-right', toggleKey: 'KeyP' }); panel.addRow('Blocks', '0'); panel.setRow('Blocks', String(count)); // update a row in place panel.removeRow('Blocks'); panel.destroy(); ``` ### Multiplayer Multiplayer runs on the engine-agnostic `@gipity/realtime` kit (`packages/realtime/`); `network.js` is a thin 3D facade over it. The `multiplayer` feature connects to the Gipity Realtime room named in `config.js`, broadcasts the local player on the `avatars` presence channel, and renders remote peers. The room is declared in the template's `gipity.yaml` (a `realtime` deploy phase), so `gipity deploy` provisions it automatically. For game events, open a channel: `network.channel('events', { sync: 'messages' })` → `.send(type, data)` / `.on(type, cb)`. For host-authoritative shared world state, enable `sync.worldState`. Multiplayer patterns in depth - custom event channels, remote-player joins/leaves, host-authoritative world state: the `3d-world` skill. ### Features `features.js` ships empty. To add a feature module: 1. Create `js/features/my-feature.js` exporting `DEFAULTS` and `create({...settings}, deps)`. 2. Register it in `featureLoaders` in `features.js`. 3. Enable it in `config.js`: `features: { 'my-feature': true }`. Each feature receives the full engine deps: `world, scene, camera, renderer, physics, player, network, ui, assets, primitives, constraints, THREE`. ## Reference: the 3d-world starter The `3d-world` starter uses this same engine, plus: - A demo scene with voxel structures in two rings - A `rocket-launcher` feature with projectile + explosion + audio - Block-collision tick sounds Add one in a throwaway project if you want to see a fully-wired implementation: ``` add name=3d-world title="Rocket Demo" ``` ## Deploy Same flow as every Gipity app: ``` project_deploy target=dev ``` ## Deploy Verification Verify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values). `gipity deploy dev --inspect` deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture `gipity page screenshot ` and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass. Full loop - reading function logs, calling a function directly, driving the page: the `app-debugging` skill. --- # 3D World - 3D Multiplayer Starter **3D World** is a playable starter app on Gipity - a multiplayer rocket-launcher demo built on the `3d-engine` template. Use it when you want a working reference or a fun playground. All 3D World games share the same visual style, physics, and multiplayer backend. **Read [3d-engine](3d-engine.md) for the engine API.** 3D World *is* the 3d-engine template plus a demo, so every call you'll make - Parts, physics, constraints, workspace, player, camera modes, assets, UI/HUD - is documented once, there. This skill covers only what the starter adds on top: the demo it ships, the rocket-launcher feature, genre recipes, multiplayer patterns, and persistence. **When to use this:** When the user asks for a 3D World game, a playable 3D reference, or a multiplayer shooter. For a fresh build without rocket-launcher / demo-scene content to strip out, add `3d-engine` instead and build your own features. For 2D games (platformer, puzzle, arcade) add `2d-game`. For non-game web apps (wordle, quiz, card games), use `web-simple` or `web-fullstack`. ## Quick Start - Start Here **STRONGLY RECOMMENDED:** Begin every 3D World game by adding the `3d-world` template with `add`. It sets up Three.js, Rapier physics, Gipity Realtime multiplayer, player controls, and the full engine layer for you. Only hand-roll files if the user explicitly tells you to skip the template. ``` add name=3d-world title="" ``` **Starting over in an existing project:** If `src/` already exists and the user wants a clean rebuild, call `file_delete` on `src` first, then run `add` normally. Or pass `force=true` to `add` to overwrite in one step - destructive, so confirm with the user first. Unrelated content (media, data, notes) is preserved either way. **Naming:** Use the user's name verbatim if given. If they didn't specify, blend "Gip" or "Gipity" into the name (e.g. "Gipity World", "GipCraft") - be creative but don't force it. This creates a playable game immediately - ground, player character, physics, camera, mobile controls. Then edit `config.js` and `game.js` to build your game. ## What the starter ships All files are in `src/` and fully editable. The engine layer is identical to `3d-engine` (see its "Project Structure"). What 3D World adds on top: - `game.js` - main orchestrator, already wired to the demo. **Start here.** - `scene.js` - the demo scene (voxel structures in two rings). Replace with your own world. - `config.js` - project metadata + `features` flags. `settings.js` - tunable gameplay values. `strings.js` - display text. - A `rocket-launcher` feature (projectile + explosion + audio) and block-collision tick sounds. - A player controller wired with an orbit + aim camera. Read the files before changing them - each opens with a header comment listing its exports and invariants. Press **`** (backtick) or **U** to toggle the built-in debug panel: FPS, version line, and all `console.log`/`warn`/`error` output. Log to it from game code with `ui.debug('message')`. ## Features (Opt-in Gameplay Modules) Enable built-in gameplay features via `config.features`. Features are template-level modules that auto-initialize during boot. ```js // config.js export const config = { title: 'My Game', features: { 'rocket-launcher': true, // enable with defaults }, }; ``` With custom settings: ```js features: { 'rocket-launcher': { speed: 200, // projectile speed (default: 120) cooldown: 1.0, // seconds between shots (default: 0.15) blastRadius: 5, // explosion radius (default: 10) blastForce: 60, // knockback strength (default: 40) maxDistance: 300, // max range (default: 150) size: 3.0, // rocket model scale (default: 2.0) }, } ``` Hook a feature's events from `game.js`: ```js import { features } from './core.js'; onInit(() => { const rl = features.get('rocket-launcher'); if (rl) { rl.onHit((pos) => { /* rocket hit something at pos */ }); rl.onExplode((pos) => { /* explosion at pos */ }); rl.onFire((origin, dir) => { /* rocket fired */ }); } }); ``` | Feature | Key | Description | |---------|-----|-------------| | Multiplayer | `multiplayer` | `@gipity/realtime` transport + remote-player avatars rendered automatically. Optional host-authoritative world-state sync via `sync.worldState: true`. Disable with `'multiplayer': false` for solo games. | | Rocket Launcher | `rocket-launcher` | Projectile weapon with physics explosions. Left-click to fire, B for debug traces. | To write your own feature, see "Features" in [3d-engine](3d-engine.md). ## Genre Recipes ### Obby / Parkour - Platforms at varying heights with anchored Parts - Checkpoints as triggers (save spawn point) - Kill zones below platforms (trigger → respawn at last checkpoint) - Timer in HUD (top-right) - Finish trigger → show completion time ### Tycoon - Resource nodes (triggers that give currency on proximity) - Shop system (`ui.setHud` for buy menu) - Upgrades stored in game state - Auto-generation timer - Persist progress with App API functions (below) ### Simulator (Collect & Sell) - Collectibles scattered as voxel boxes with triggers - Inventory count in HUD - Sell zone (trigger → convert items to currency) - Upgrade tiers (speed, capacity, multiplier) - Leaderboard via App API ### PvP Combat - Health bar in HUD - Weapon hitbox via raycast (`physics.castRay`) - Damage events via a `network.channel('combat', { sync: 'messages' })` channel - Respawn timer + invincibility frames - Score tracking ### Shooter (FPS/TPS) - Camera mode: `player.cameraControl.mode = 'firstPerson'` - Crosshair in HUD center - Projectile: spawn small Part, apply impulse, raycast for hit detection - Ammo count in HUD - Network: broadcast shots, validate hits server-side ### Tower Defense - Path defined as waypoint array - Enemy spawner on interval - Tower placement on grid (snap to voxel) - Projectile system (tower → nearest enemy) - Wave counter + health in HUD ### Horror - Override world lighting: dim the sun (`workspace.lighting.timeOfDay`), pull fog closer - Flashlight: spotlight attached to camera - Jump scare: trigger zones that play sounds + show images - Inventory: key-item tracking - Narrative: text messages via `ui.showMessage` ### Racing - Checkpoints as triggers around a track - Lap counter + timer in HUD - Speed boost zones (triggers that increase velocity) - Vehicle: replace player model, adjust move speed - Multiplayer: position sync shows other racers ## Multiplayer Patterns ### Custom game events Open a `messages` channel and send/receive typed events. Each channel namespaces its own wire types, so use as many as you like: ```js import { network } from './core.js'; const events = network.channel('events', { sync: 'messages' }); events.send('item_collected', { itemId: 'coin-3', points: 10 }); events.on('item_collected', (data) => { removeItem(data.itemId); updateScore(data.points); }); ``` ### Remote players The `multiplayer` feature already renders remote-player avatars, driven by the `network.avatars` presence channel. To react to joins/leaves yourself: ```js network.avatars.onJoin((sid) => console.log('joined', sid)); network.avatars.onLeave((sid) => console.log('left', sid)); for (const [sid, peer] of network.avatars.peers()) { // peer.position {x,y,z}, peer.rotation (y radians) } ``` ### Solo, basic, and authoritative world state Multiplayer is a feature like rocket-launcher - flip it on or off in `config.js`: ```js features: { 'multiplayer': false } // solo game features: { 'multiplayer': { room: 'my-arena' } } // default: presence + avatars features: { 'multiplayer': { room: 'lobby', sync: { worldState: true } } } // host-authoritative world ``` Solo is that one line and nothing else: **leave `gipity.yaml` as shipped.** Its `realtime` phase only registers a room name (no server, no cost, no plan limit) and an unconnected room does nothing, so there is no manifest to strip out. Use `sync.worldState` when every client must see the same blocks/objects in the same place - a shared sandbox or a destructible level. The feature creates a host-authoritative `world` entities channel; the 3D adapter (`js/network/adapter-3d.js`) already knows how to serialize and apply Parts, so there's no game-side registration. The first client to join becomes host (3-phase claim election); if the host drops, another takes over automatically (a client holding world data is promoted instantly, else alphabetical tiebreaker). The room is declared in the template's `gipity.yaml` (a `realtime` deploy phase), so `gipity deploy` provisions it - no separate step. To sync your own non-Part state, open an `entities` channel and supply an adapter - see `packages/realtime/contracts/adapter.contract.md` and the worked `examples/`. Room management and advanced config: the `app-realtime` skill. ## Persistence Use App API functions to save/load player data: ```js // functions/save-progress.js export default async function (ctx, { db }) { const { data } = ctx.body; await db.execute( 'INSERT INTO saves (user_guid, data) VALUES ($1, $2) ON CONFLICT (user_guid) DO UPDATE SET data = $2', [ctx.auth.userGuid, data] ); return { ok: true }; } // functions/load-progress.js export default async function (ctx, { db }) { const row = await db.findOne('saves', { user_guid: ctx.auth.userGuid }); return { data: row?.data ?? null }; } ``` Declare in `gipity.yaml`: ```yaml functions: save-progress: auth_level: user tables: [saves] load-progress: auth_level: user tables: [saves] ``` ## Mobile and Performance Touch controls are automatic - floating joystick, Jump/Action buttons, camera drag, fullscreen toggle (details in [3d-engine](3d-engine.md)). To stay playable on both inputs, read movement from `player.inputState` and trigger actions from `inputState.action` / `inputState.jump` rather than raw key events. Typing into form fields (leaderboard name entry, chat) is already shielded: the template's key handlers ignore events targeting an `input`/`textarea`/`select`/contenteditable and clear held keys on focus, so W/A/S/D/Space type normally without moving the avatar. If you add your OWN global key listener, start it with `if (e.target.closest('input, textarea, select') || e.target.isContentEditable) return;`. - Use `assets.createVoxelGround()` for terrain - it's an InstancedMesh (one draw call for the whole ground) - For many identical objects, use `THREE.InstancedMesh` instead of individual meshes - Keep total triangle count under 100K for mobile - Limit shadow-casting objects (player + key objects only) - Use fog to hide pop-in at draw distance ## Deploy Verification Verify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values). `gipity deploy dev --inspect` deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture `gipity page screenshot ` and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass. Full loop - reading function logs, calling a function directly, driving the page: the `app-debugging` skill. **The version line** `[3D World] Game Title v1.0 (build 2026-...)` appears in the console on every boot - use it to confirm the correct build is deployed. ## Related Skills - **3d-engine** - the engine API this starter is built on: Parts, physics, constraints, workspace, player, camera, assets, UI. **Read it first.** - **app-debugging** - verifying a 3D app headlessly: `advance(seconds)` instead of wall-clock waits, `page inspect`, screenshots - **app-development** - functions, database & API for persistence and leaderboards - **app-realtime** - advanced Gipity Realtime room configuration - **app-auth** - Sign in with Gipity for user identity - **app-llm** - AI-powered NPCs using the LLM service --- # About Gipity ## What Is This? The full-stack platform tuned for AI agents. This is the *why* behind Gipity - the mission and the principles. For *what you can build* and the full capability list, see [gipity-platform](gipity-platform.md). ## Why We Built This The internet got worse. Platforms that started by serving users now serve advertisers and shareholders. Cory Doctorow calls it "enshittification" - platforms attract users, attract business customers, then extract value from both until the thing barely works for anyone. We've all watched it happen. We're building the opposite. **We believe in deshittification.** Making things that work for the people using them. No ads. No dark patterns. No bait-and-switch. No investor telling us to "maximize engagement" at the expense of the product. **We believe AI should democratize creation, not gatekeep it.** If you can describe what you want, you should be able to have it - a website, an app, an automation, a tool. You shouldn't need to be a developer, learn a framework, or pay for five different SaaS subscriptions to build something useful. **We believe you should own what you build.** Your apps, your data, your databases, your files. Not trapped in someone else's ecosystem. Not held hostage by a platform that might change its pricing or shut down your account. **We believe the agent is the machinery.** Enterprise platforms (ServiceNow, Salesforce) make change easy by operating a giant metadata engine - form builders, field editors, workflow designers - which is most of their codebase, licensed by the seat, and run by certified admins. They are now bolting AI agents on top of that engine. Gipity deletes the engine instead: an app's whole definition is plain text on normal software (Postgres, REST, static files), and any coding agent - Claude Code, Codex, Gip - is the admin. Ask for a new field in plain English and the schema, screens, and API follow in seconds, as a reviewable diff rather than platform rows migrated with update sets. Your "admin" is swappable and gets smarter for free every time the models do. ## Who We Are A small team. No massive corporate backing dictating our roadmap. We answer to users, not a board room. That's not a limitation - it's the point. It means every feature exists because it's genuinely useful, not because it looks good in a pitch deck. We show you exactly how your data is stored. We tell you what things cost. We don't hide behind corporate speak. The agent is transparent about what it's doing and why. ## The Bet The full-stack platform tuned for AI agents. **Your AI builds it. Your AI runs it.** --- # Headless Deploy with Agent API Tokens The normal `gipity login` is interactive: it emails a 6-digit code that a human reads and pastes back. An autonomous agent, a cron job, or a CI runner has no inbox to check - and a fresh/ephemeral container has no saved session on disk. For those, use a long-lived **agent API token** (`gip_at_*`) instead. It authenticates from an environment variable, so it survives fresh containers, never expires unless you set `--expires`, bills to your account like any other usage, and can be revoked instantly. This is the path for **self-hosted coding agents (e.g. Hermes, OpenClaw), scripts, and CI** that want to build, deploy, and operate Gipity apps without a human in the loop. ## When to use this - You're an agent/script running unattended - no human to read an email code. - You run in an ephemeral or rebuilt container (no persistent `~/.gipity/auth.json`). - You're wiring Gipity into CI/CD or a scheduled job. If a human is present at a terminal, plain `gipity login` is simpler - see [getting-started](getting-started.md). Use a token when login can't be completed interactively. ## 1. Mint a token (one time, needs an interactive session) From a machine where you're already logged in - your own CLI **or** the web CLI in the browser: ```bash gipity token create --name "Hermes on my VPS" # name is a label you choose gipity token create --name "CI deploy" --expires 90 # optional expiry, in days gipity token create --name "ci" --expires 90 --json # machine-readable ``` It prints the token **once** - copy it immediately, it can't be retrieved later: ``` gip_at_eygYHKVRkBP8UXY5dfQzZV_xMH6t1x4Bynnhv4GD128 ``` Only a hash is stored server-side, so a lost token can't be recovered - only revoked and replaced. With `--json` the output is `{ "token": "gip_at_…", "shortGuid": "at_…", "expiresAt": "…"|null }`. ## 2. Use it: set GIPITY_TOKEN Put the token in the agent's environment. The CLI picks it up automatically, skips login entirely, and acts as your account: ```bash export GIPITY_TOKEN=gip_at_eygYHKVRkBP8UXY5dfQzZV_xMH6t1x4Bynnhv4GD128 ``` That's the whole auth step. `GIPITY_TOKEN` takes precedence over any saved session, so the same command works identically in a fresh container with no `auth.json` and on a logged-in workstation. Every CLI command now works unattended: ```bash gipity status # confirms authenticated, no login prompt gipity init # link the cwd as a project (or `gipity add