{
  "name": "3d-world",
  "title": "3D World - 3D Multiplayer Games",
  "description": "Build 3D World games (3D multiplayer): template API, genre recipes (obby, tycoon, simulator, PvP, shooter, horror, racing), assets, physics, networking",
  "guid": "sk_plat_game",
  "category": "Templates",
  "requiredTools": [
    "add",
    "realtime_room",
    "file_write",
    "project_deploy"
  ],
  "content": "# 3D World - 3D Multiplayer Starter\n\n**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.\n\n**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`.\n\n## Quick Start - Start Here\n\n**STRONGLY RECOMMENDED:** Begin every 3D World game by adding the `3d-world` template with `add`. It sets up Three.js, Rapier physics, Colyseus multiplayer, player controls, and the full engine layer for you. Only hand-roll files if the user explicitly tells you to skip the template.\n\n```\nadd name=3d-world title=\"<Game Name>\"\n```\n\n**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.\n\n**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.\n\nThis creates a playable game immediately - ground, player character, physics, camera, mobile controls. Then edit `config.js` and `game.js` to build your game.\n\n## Project Structure\n\nAfter installing the template, list all files with `file_list` and read them to understand the project. All files are in `src/` and fully editable. Key files:\n\n- `game.js` - Main orchestrator. Start here.\n- `config.js` - Project metadata (title, room, features).\n- `settings.js` - Tunable gameplay values (colors, speeds, sizes, camera).\n- `strings.js` - User-facing display text.\n- `scene.js` - Demo scene setup. Replace with your own world.\n- `core.js` - Engine entry point. Exports all engine modules.\n\nRead the files before making changes - the comments explain what each one does.\n\n## Engine API\n\nAll engine modules are available via a single import:\n\n```js\nimport { world, assets, physics, player, network, ui, THREE, onInit, onUpdate, setConfig, primitives, constraints, workspace, features } from './core.js';\n```\n\n### Lifecycle\n\n```js\n// settings.js - tunable values\nexport const settings = {\n  colors: { player: 0xf26522, ground: 0x4CAF50, objects: 0x2196F3 },\n  world: { groundSize: 30 },\n  gameplay: { objectCount: 5, spawnRange: 20, messageDuration: 3000 },\n};\n\n// strings.js - user-facing text\nexport const strings = { welcome: 'My Game' };\n\n// objects.js - entity factories\nimport { world, assets, physics } from './core.js';\nimport { settings } from './settings.js';\nexport function createBlock(x, y, z, color = settings.colors.objects) { ... }\n\n// game.js - orchestrator\nimport { setConfig, onInit, onUpdate, world, assets, physics, player, ui, THREE } from './core.js';\nimport { config } from './config.js';\nimport { settings } from './settings.js';\nimport { strings } from './strings.js';\nimport { createBlock } from './objects.js';\n\nsetConfig(config);\n\nonInit(async () => {\n  player.initPlayer({ color: settings.colors.player });\n\n  const { groundSize } = settings.world;\n  const ground = assets.createVoxelGround(groundSize, groundSize, settings.colors.ground);\n  world.scene.add(ground);\n  physics.addStaticBox({ x: 0, y: -0.5, z: 0 }, { x: groundSize / 2, y: 0.5, z: groundSize / 2 });\n\n  const { objectCount, spawnRange } = settings.gameplay;\n  for (let i = 0; i < objectCount; i++) {\n    createBlock(Math.random() * spawnRange - spawnRange / 2, 0.5, Math.random() * spawnRange - spawnRange / 2);\n  }\n\n  ui.showMessage(strings.welcome, settings.gameplay.messageDuration);\n});\n\nonUpdate((dt) => {\n  // Game update loop - runs every frame\n});\n```\n\n### World (`world`)\n- `world.scene` - Three.js Scene (add objects here)\n- `world.camera` - PerspectiveCamera (auto-follows player)\n- `world.renderer` - WebGLRenderer\n- `world.clock` - Three.js Clock\n\n### Assets (`assets`)\n- `assets.spawn(name, {x,y,z}, scale)` - Load and add a model to the scene\n- `assets.despawn(model)` - Remove a model\n- `assets.loadModel(name)` - Load a model (returns clone)\n- `assets.createVoxelBox(color, size)` - Simple colored cube\n- `assets.createVoxelGround(width, depth, color)` - Instanced voxel ground plane\n- `assets.playSound(name, {volume})` - Play a sound effect\n- `assets.getTexture(name)` - Load a texture\n\n### Physics (`physics`)\n- `physics.addStaticBox(pos, halfExtents)` - Static collider (floor, wall)\n- `physics.addDynamicBox(pos, halfExtents, mass)` - Dynamic physics body\n- `physics.addKinematicBody(pos, halfExtents)` - Kinematic controller\n- `physics.addTrigger(pos, halfExtents, {onEnter, onExit})` - Sensor zone\n- `physics.removeBody(body)` - Remove a body\n- `physics.castRay(origin, dir, maxDist)` - Raycast\n\n### World Primitives (`primitives`) - v13+\n\nParts are the universal 3D building block. **Dynamic (gravity-on) by default.** Each Part is a 3x3x3 sub-voxel grid for detailed shapes.\n\n```js\n// Create parts - they fall with gravity by default\nconst crate = primitives.createPart({ position: {x:0, y:10, z:0}, color: 0x8B4513, material: 'wood' });\n\n// Anchored = no gravity, stays in place\nconst floor = primitives.createPart({ position: {x:0, y:0, z:0}, size: {x:20, y:1, z:20}, anchored: true, material: 'metal' });\n\n// Sub-voxel shapes: FULL, SLAB, HALF, STAIR, SLOPE, CORNER, PILLAR, ARCH\nconst stair = primitives.createPart({ position: {x:3, y:1, z:0}, shape: primitives.SHAPES.STAIR, color: 0x4CAF50 });\n\n// Runtime property changes\nprimitives.setProperty(crate, 'anchored', true);   // freeze in place\nprimitives.setProperty(crate, 'material', 'ice');   // change material (updates physics + visual)\nprimitives.setProperty(crate, 'color', 0xff0000);\n\n// Query and remove\nconst redParts = primitives.queryParts({ color: 0xff0000 });\nprimitives.removePart(crate);\n```\n\n**Part properties:** position, rotation (quaternion), size, anchored, canCollide, mass, friction, elasticity, linearDamping, angularDamping, color, material, transparency, shape, castShadow, receiveShadow\n\n**Materials:** plastic (default), metal, wood, glass, neon, ice, grass, sand - each sets visual + physics defaults\n\n**Spawn points:**\n```js\nprimitives.createSpawnPoint({ position: {x:0, y:2, z:0}, teamColor: 0xff0000 });\n```\n\n### Compound Blocks (`primitives.createCompoundBlock`)\n\nDestructible blocks - a grid of welded sub-blocks that shatter on impact:\n\n```js\n// 3x3x3 destructible block (27 welded 1x1x1 parts)\nconst block = primitives.createCompoundBlock({\n  position: { x: 5, y: 1.5, z: 0 },\n  color: 0xff0000,\n  breakForce: 8,         // velocity delta threshold (higher = harder to break)\n  gridSize: 3,           // blocks per axis (default 3 → 27 blocks)\n  blockSize: 1,          // size of each sub-block (default 1)\n  material: 'wood',      // material preset\n  colorVariation: true,  // slight brightness variation per block (default true)\n});\n\nblock.break(block.parts[0]);  // free a specific sub-block\nblock.breakAll();              // shatter everything\nblock.isIntact();              // any welds remaining?\nblock.parts;                   // array of all sub-block Parts\n```\n\n### Constraints (`constraints`) - v13+\n\nConnect Parts with physical joints:\n\n```js\n// Weld - rigid lock (structures, attached parts)\nconstraints.weld(partA, partB);\n\n// Hinge - rotation on one axis (doors, wheels, levers)\nconstraints.hinge(frame, door, { axis: {x:0,y:1,z:0}, limits: [-90, 90] });\n\n// Spring - elastic (suspension, ropes, bouncy platforms)\nconstraints.spring(partA, partB, { stiffness: 100, damping: 10 });\n\n// Management\nconstraints.getAll(part);     // all constraints on a part\nconstraints.remove(c);        // remove one\nconstraints.removeAll(part);  // remove all\n```\n\n### Workspace (`workspace`) - v13+\n\nWorld-level settings:\n\n```js\nworkspace.gravity = {x: 0, y: -30, z: 0};\nworkspace.snapEnabled = true;        // auto-snap nearby parts (creates Weld)\nworkspace.snapDistance = 0.15;\nworkspace.lighting.timeOfDay = 18;   // sunset (0-24)\nworkspace.lighting.fogEnabled = true;\nworkspace.lighting.fogNear = 40;\nworkspace.onSnap((a, b) => console.log('snapped'));\n```\n\n### Camera Modes - v13+\n\n```js\nplayer.cameraControl.mode = 'orbit';       // default third-person\nplayer.cameraControl.mode = 'firstPerson'; // FPS\nplayer.cameraControl.mode = 'topDown';     // birds-eye\nplayer.cameraControl.mode = 'fixed';       // scriptable\nplayer.cameraControl.setFixedPosition({x:10, y:8, z:10});\nplayer.cameraControl.setFixedLookAt({x:0, y:0, z:0});\n```\n\n### Player (`player`)\n- `player.initPlayer({color, x, y, z})` - Create the player character\n- `player.getPosition()` - Get {x, y, z}\n- `player.setPosition(x, y, z)` - Teleport player\n- `player.inputState` - Current input: {forward, right, jump, action}\n- `player.cameraControl` - Camera mode and settings (see above)\n\n### Network (`network`)\n\nMultiplayer rides on the engine-agnostic `@gipity/realtime` kit (`packages/realtime/`). The `network` module is a thin 3D facade over it:\n\n- `network.avatars` - presence channel of remote players. `.peers()` returns a Map of `{position, rotation}`; `.onJoin(cb)` / `.onLeave(cb)` fire on membership changes. The `multiplayer` feature already spawns and moves a mesh per peer.\n- `network.channel(name, { sync })` - open a custom channel. `sync: 'messages'` gives pub/sub (`.send(type, data)` / `.on(type, cb)`) for game events.\n- `network.enableWorldSync()` - host-authoritative shared-world sync (or just set `sync.worldState` on the multiplayer feature).\n- `network.rt` - the underlying realtime instance: `.on(event)`, `.metrics()`.\n\nThe room is already declared in the template's `gipity.yaml` (a `realtime` deploy phase), so `gipity deploy` provisions it automatically - no separate step. To add or manage rooms: `gipity realtime room create|list|info|delete`, or the `realtime_room` tool. See the `app-realtime` skill.\n\n### UI (`ui`)\n- `ui.setHud(slot, html)` - Set HUD content. Slots: top-left, top-right, bottom-left, bottom-right, center\n- `ui.clearHud(slot)` - Clear a slot\n- `ui.showMessage(text, duration)` - Centered message (0 = sticky)\n- `ui.debug(msg)` - Log to the in-game debug panel\n\n### InfoPanel (`ui.InfoPanel`) - v13+\n\nReusable 3D World-styled info display. Use for stats, inventories, leaderboards, dialogs, etc.\n\n```js\n// Create a panel\nconst stats = new ui.InfoPanel({ title: 'Player Stats', position: 'top-right' });\nstats.addRow('Health', '100', { color: '#0f0' });\nstats.addRow('Score', '0', { bold: true });\nstats.addRow('Ammo', '30');\n\n// Update values (call in onUpdate or on events)\nstats.setRow('Health', '75', { color: '#ff0' });\nstats.setRow('Score', '1500');\n\n// Remove a row\nstats.removeRow('Ammo');\n\n// Toggle with a key\nconst inv = new ui.InfoPanel({ title: 'Inventory', position: 'bottom-right', toggleKey: 'KeyI', visible: false });\n\n// Custom position\nconst custom = new ui.InfoPanel({ position: 'custom', top: 100, right: 20, width: 200 });\n\n// Raw HTML content\nstats.setContent('<table>...</table>');\n\n// Cleanup\nstats.destroy();\n```\n\n**Options:** title, position (top-left/top-right/bottom-left/bottom-right/custom), width, visible, compact, toggleKey\n\n**Built-in debug panel:** F3 toggles version info + FPS + console logs (ON by default)\n\n### Debug Panel\nPress **`** (backtick) to toggle the built-in debug panel. Shows:\n- FPS counter\n- All `console.log`, `console.warn`, `console.error` output\n- Use `ui.debug('message')` to log from game code\n\n## Asset Catalog\n\nModels, sounds, and textures are loaded by name from the shared CDN. Use `assets.spawn('name')` for models, `assets.playSound('name')` for sounds, `assets.getTexture('name')` for textures.\n\n**Note:** The asset pack is being built. For now, use the built-in helpers:\n- `assets.createVoxelBox(color, size)` - colored cube for any object\n- `assets.createVoxelGround(width, depth, color)` - terrain plane\n- Create custom geometry with THREE.js directly\n\n## Genre Recipes\n\n### Obby / Parkour\n- Platforms at varying heights with physics.addStaticBox\n- Checkpoints as triggers (save spawn point)\n- Kill zones below platforms (trigger → respawn at last checkpoint)\n- Timer in HUD (top-right)\n- Finish trigger → show completion time\n\n### Tycoon\n- Resource nodes (triggers that give currency on proximity)\n- Shop system (ui.setHud for buy menu)\n- Upgrades stored in game state\n- Auto-generation timer\n- Persist progress with App API functions: write save/load functions in `functions/`\n\n### Simulator (Collect & Sell)\n- Collectibles scattered as voxel boxes with triggers\n- Inventory count in HUD\n- Sell zone (trigger → convert items to currency)\n- Upgrade tiers (speed, capacity, multiplier)\n- Leaderboard via App API\n\n### PvP Combat\n- Health bar in HUD\n- Weapon hitbox via raycast (physics.castRay)\n- Damage events via a `network.channel('combat', { sync: 'messages' })` channel\n- Respawn timer + invincibility frames\n- Score tracking\n\n### Shooter (FPS/TPS)\n- Camera mode: set in config.js\n- Crosshair in HUD center\n- Projectile: spawn small box, apply velocity, raycast for hit detection\n- Ammo count in HUD\n- Network: broadcast shots, validate hits server-side\n\n### Tower Defense\n- Path defined as waypoint array\n- Enemy spawner on interval\n- Tower placement on grid (snap to voxel)\n- Projectile system (tower → nearest enemy)\n- Wave counter + health in HUD\n\n### Horror\n- Override world lighting: dim the sun, add fog closer\n- Flashlight: spotlight attached to camera\n- Jump scare: trigger zones that play sounds + show images\n- Inventory: key-item tracking\n- Narrative: text messages via ui.showMessage\n\n### Racing\n- Checkpoints as triggers around a track\n- Lap counter + timer in HUD\n- Speed boost zones (triggers that increase velocity)\n- Vehicle: replace player model, adjust move speed\n- Multiplayer: position sync shows other racers\n\n## Multiplayer Patterns\n\n### Custom game events\nOpen a `messages` channel and send/receive typed events. Each channel namespaces its own wire types, so use as many as you like:\n```js\nimport { network } from './core.js';\n\nconst events = network.channel('events', { sync: 'messages' });\nevents.send('item_collected', { itemId: 'coin-3', points: 10 });\nevents.on('item_collected', (data) => {\n  removeItem(data.itemId);\n  updateScore(data.points);\n});\n```\n\n### Remote players\nThe `multiplayer` feature already renders remote-player avatars for you, driven by the `network.avatars` presence channel. To react to joins/leaves yourself:\n```js\nnetwork.avatars.onJoin((sid) => console.log('joined', sid));\nnetwork.avatars.onLeave((sid) => console.log('left', sid));\nfor (const [sid, peer] of network.avatars.peers()) {\n  // peer.position {x,y,z}, peer.rotation (y radians)\n}\n```\n\n## Persistence\n\nUse App API functions to save/load player data:\n\n```js\n// functions/save-progress.js\nexport default async function (ctx, { db }) {\n  const { data } = ctx.body;\n  await db.execute(\n    'INSERT INTO saves (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2',\n    [ctx.auth.userId, data]\n  );\n  return { ok: true };\n}\n\n// functions/load-progress.js\nexport default async function (ctx, { db }) {\n  const row = await db.findOne('saves', { user_id: ctx.auth.userId });\n  return { data: row?.data ?? null };\n}\n```\n\nDeclare in `gipity.yaml`:\n```yaml\nfunctions:\n  save-progress:\n    auth_level: user\n    tables: [saves]\n  load-progress:\n    auth_level: user\n    tables: [saves]\n```\n\n## Mobile Support\n\nTouch controls are automatic:\n- Left side: virtual joystick (movement)\n- Right side: Jump + Action buttons\n- Template detects mobile and shows controls automatically\n\n## Features (Opt-in Gameplay Modules)\n\nEnable built-in gameplay features via `config.features`. Features are template-level modules that auto-initialize during boot.\n\n### Enabling a Feature\n\nIn `config.js`:\n```js\nexport const config = {\n  title: 'My Game',\n  features: {\n    'rocket-launcher': true,  // enable with defaults\n  },\n};\n```\n\nWith custom settings:\n```js\nfeatures: {\n  'rocket-launcher': {\n    speed: 200,       // projectile speed (default: 120)\n    cooldown: 1.0,    // seconds between shots (default: 0.15)\n    blastRadius: 5,   // explosion radius (default: 10)\n    blastForce: 60,   // knockback strength (default: 40)\n    maxDistance: 300,  // max range (default: 150)\n    size: 3.0,        // rocket model scale (default: 2.0)\n  },\n}\n```\n\n### Interacting with Features in game.js\n\n```js\nimport { features } from './core.js';\n\nonInit(() => {\n  const rl = features.get('rocket-launcher');\n  if (rl) {\n    rl.onHit((pos) => { /* rocket hit something at pos */ });\n    rl.onExplode((pos) => { /* explosion at pos */ });\n    rl.onFire((origin, dir) => { /* rocket fired */ });\n  }\n});\n```\n\n### Available Features\n\n| Feature | Key | Description |\n|---------|-----|-------------|\n| 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. |\n| Rocket Launcher | `rocket-launcher` | Projectile weapon with physics explosions. Left-click to fire, B for debug traces. |\n\n### Multiplayer in Depth\n\nMultiplayer is a feature like rocket-launcher - flip it on or off. It runs on the engine-agnostic `@gipity/realtime` kit in `packages/realtime/` (that package's `README.md` and `examples/` show non-game uses too). By default it connects, broadcasts the local player at ~20 Hz on the `avatars` presence channel, and renders a mesh for every remote peer.\n\nSolo / single-player game:\n```js\nfeatures: { 'multiplayer': false }\n```\n\nBasic multiplayer (default):\n```js\nfeatures: { 'multiplayer': { room: 'my-arena' } }\n```\n\nAuthoritative world state (host-elected, drift-corrected). Use when every client must see the same blocks/objects in the same place - e.g. shared sandbox or destructible level:\n```js\nfeatures: { 'multiplayer': { room: 'lobby', sync: { worldState: true } } }\n```\n\nWith `sync.worldState` on, 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 - no game-side registration needed. The first client to join becomes host (3-phase claim election); if the host drops, another client takes over automatically (a client holding world data is promoted instantly, else alphabetical tiebreaker).\n\nTo sync your own non-Part state, open an `entities` channel directly and supply an adapter - see `packages/realtime/contracts/adapter.contract.md` and the worked `examples/`.\n\n## Performance Tips\n\n- Use `assets.createVoxelGround()` - it uses InstancedMesh (one draw call for entire ground)\n- For many identical objects, use THREE.InstancedMesh instead of individual meshes\n- Keep total triangle count under 100K for mobile\n- Limit shadow-casting objects (only player + key objects)\n- Use fog to hide pop-in at draw distance\n\n## Deploy Verification\n\nUse the browser tool to verify deploys when it matters - first deploy, structural changes (new pages, new frameworks, changed imports), or when something might have broken. Skip verification for trivial changes (copy tweaks, style adjustments, config values).\n\nTo verify: `browser action=open url=<deployed-url>` - waits for async modules, captures console errors automatically. Check output for `[Console errors captured after page load]`. Use `browser action=screenshot` to confirm visual correctness.\n\n**Debugging in production:** Add `console.error()` calls to app code for diagnostics, redeploy, then use `browser action=console` to read the output. Remove debug logging when done.\n\n**The version line** `[3D World] Game Title v1.0 (build 2026-...)` appears in console on every boot - use it to confirm the correct build is deployed.\n\n## Related Skills\n\n- **app-development** - Functions, database & API for persistence, leaderboards\n- **app-realtime** - advanced Colyseus room configuration\n- **app-auth** - Sign in with Gipity for user identity\n- **app-llm** - AI-powered NPCs using LLM service\n"
}
