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

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

```
add name=3d-world title="<Game Name>"
```

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

## Project Structure

After 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:

- `game.js` - Main orchestrator. Start here.
- `config.js` - Project metadata (title, room, features).
- `settings.js` - Tunable gameplay values (colors, speeds, sizes, camera).
- `strings.js` - User-facing display text.
- `scene.js` - Demo scene setup. Replace with your own world.
- `core.js` - Engine entry point. Exports all engine modules.

Read the files before making changes - the comments explain what each one does.

## 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 } from './core.js';
```

### Lifecycle

```js
// settings.js - tunable values
export const settings = {
  colors: { player: 0xf26522, ground: 0x4CAF50, objects: 0x2196F3 },
  world: { groundSize: 30 },
  gameplay: { objectCount: 5, spawnRange: 20, messageDuration: 3000 },
};

// strings.js - user-facing text
export const strings = { welcome: 'My Game' };

// objects.js - entity factories
import { world, assets, physics } from './core.js';
import { settings } from './settings.js';
export function createBlock(x, y, z, color = settings.colors.objects) { ... }

// game.js - orchestrator
import { setConfig, onInit, onUpdate, world, assets, physics, player, ui, THREE } from './core.js';
import { config } from './config.js';
import { settings } from './settings.js';
import { strings } from './strings.js';
import { createBlock } from './objects.js';

setConfig(config);

onInit(async () => {
  player.initPlayer({ color: settings.colors.player });

  const { groundSize } = settings.world;
  const ground = assets.createVoxelGround(groundSize, groundSize, settings.colors.ground);
  world.scene.add(ground);
  physics.addStaticBox({ x: 0, y: -0.5, z: 0 }, { x: groundSize / 2, y: 0.5, z: groundSize / 2 });

  const { objectCount, spawnRange } = settings.gameplay;
  for (let i = 0; i < objectCount; i++) {
    createBlock(Math.random() * spawnRange - spawnRange / 2, 0.5, Math.random() * spawnRange - spawnRange / 2);
  }

  ui.showMessage(strings.welcome, settings.gameplay.messageDuration);
});

onUpdate((dt) => {
  // Game update loop - runs every frame
});
```

### World (`world`)
- `world.scene` - Three.js Scene (add objects here)
- `world.camera` - PerspectiveCamera (auto-follows player)
- `world.renderer` - WebGLRenderer
- `world.clock` - Three.js Clock

### Assets (`assets`)
- `assets.spawn(name, {x,y,z}, scale)` - Load and add a model to the scene
- `assets.despawn(model)` - Remove a model
- `assets.loadModel(name)` - Load a model (returns clone)
- `assets.createVoxelBox(color, size)` - Simple colored cube
- `assets.createVoxelGround(width, depth, color)` - Instanced voxel ground plane
- `assets.playSound(name, {volume})` - Play a sound effect
- `assets.getTexture(name)` - Load a texture

### Physics (`physics`)
- `physics.addStaticBox(pos, halfExtents)` - Static collider (floor, wall)
- `physics.addDynamicBox(pos, halfExtents, mass)` - Dynamic physics body
- `physics.addKinematicBody(pos, halfExtents)` - Kinematic controller
- `physics.addTrigger(pos, halfExtents, {onEnter, onExit})` - Sensor zone
- `physics.removeBody(body)` - Remove a body
- `physics.castRay(origin, dir, maxDist)` - Raycast

### World Primitives (`primitives`) - v13+

Parts are the universal 3D building block. **Dynamic (gravity-on) by default.** Each Part is a 3x3x3 sub-voxel grid for detailed shapes.

```js
// Create parts - they fall with gravity by default
const crate = primitives.createPart({ position: {x:0, y:10, z:0}, color: 0x8B4513, material: 'wood' });

// Anchored = no gravity, stays in place
const floor = primitives.createPart({ position: {x:0, y:0, z:0}, size: {x:20, y:1, z:20}, anchored: true, material: 'metal' });

// Sub-voxel shapes: FULL, SLAB, HALF, STAIR, SLOPE, CORNER, PILLAR, ARCH
const stair = primitives.createPart({ position: {x:3, y:1, z:0}, shape: primitives.SHAPES.STAIR, color: 0x4CAF50 });

// Runtime property changes
primitives.setProperty(crate, 'anchored', true);   // freeze in place
primitives.setProperty(crate, 'material', 'ice');   // change material (updates physics + visual)
primitives.setProperty(crate, 'color', 0xff0000);

// Query and remove
const redParts = primitives.queryParts({ color: 0xff0000 });
primitives.removePart(crate);
```

**Part properties:** position, rotation (quaternion), size, anchored, canCollide, mass, friction, elasticity, linearDamping, angularDamping, color, material, transparency, shape, castShadow, receiveShadow

**Materials:** plastic (default), metal, wood, glass, neon, ice, grass, sand - each sets visual + physics defaults

**Spawn points:**
```js
primitives.createSpawnPoint({ position: {x:0, y:2, z:0}, teamColor: 0xff0000 });
```

### Compound Blocks (`primitives.createCompoundBlock`)

Destructible blocks - a grid of welded sub-blocks that shatter on impact:

```js
// 3x3x3 destructible block (27 welded 1x1x1 parts)
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,           // blocks per axis (default 3 → 27 blocks)
  blockSize: 1,          // size of each sub-block (default 1)
  material: 'wood',      // material preset
  colorVariation: true,  // slight brightness variation per block (default true)
});

block.break(block.parts[0]);  // free a specific sub-block
block.breakAll();              // shatter everything
block.isIntact();              // any welds remaining?
block.parts;                   // array of all sub-block Parts
```

### Constraints (`constraints`) - v13+

Connect Parts with physical joints:

```js
// Weld - rigid lock (structures, attached parts)
constraints.weld(partA, partB);

// Hinge - rotation on one axis (doors, wheels, levers)
constraints.hinge(frame, door, { axis: {x:0,y:1,z:0}, limits: [-90, 90] });

// Spring - elastic (suspension, ropes, bouncy platforms)
constraints.spring(partA, partB, { stiffness: 100, damping: 10 });

// Management
constraints.getAll(part);     // all constraints on a part
constraints.remove(c);        // remove one
constraints.removeAll(part);  // remove all
```

### Workspace (`workspace`) - v13+

World-level settings:

```js
workspace.gravity = {x: 0, y: -30, z: 0};
workspace.snapEnabled = true;        // auto-snap nearby parts (creates Weld)
workspace.snapDistance = 0.15;
workspace.lighting.timeOfDay = 18;   // sunset (0-24)
workspace.lighting.fogEnabled = true;
workspace.lighting.fogNear = 40;
workspace.onSnap((a, b) => console.log('snapped'));
```

### Camera Modes - v13+

```js
player.cameraControl.mode = 'orbit';       // default third-person
player.cameraControl.mode = 'firstPerson'; // FPS
player.cameraControl.mode = 'topDown';     // birds-eye
player.cameraControl.mode = 'fixed';       // scriptable
player.cameraControl.setFixedPosition({x:10, y:8, z:10});
player.cameraControl.setFixedLookAt({x:0, y:0, z:0});
```

### Player (`player`)
- `player.initPlayer({color, x, y, z})` - Create the player character
- `player.getPosition()` - Get {x, y, z}
- `player.setPosition(x, y, z)` - Teleport player
- `player.inputState` - Current input: {forward, right, jump, action}
- `player.cameraControl` - Camera mode and settings (see above)

### Network (`network`)

Multiplayer rides on the engine-agnostic `@gipity/realtime` kit (`packages/realtime/`). The `network` module is a thin 3D facade over it:

- `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.
- `network.channel(name, { sync })` - open a custom channel. `sync: 'messages'` gives pub/sub (`.send(type, data)` / `.on(type, cb)`) for game events.
- `network.enableWorldSync()` - host-authoritative shared-world sync (or just set `sync.worldState` on the multiplayer feature).
- `network.rt` - the underlying realtime instance: `.on(event)`, `.metrics()`.

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

### UI (`ui`)
- `ui.setHud(slot, html)` - Set HUD content. Slots: top-left, top-right, bottom-left, bottom-right, center
- `ui.clearHud(slot)` - Clear a slot
- `ui.showMessage(text, duration)` - Centered message (0 = sticky)
- `ui.debug(msg)` - Log to the in-game debug panel

### InfoPanel (`ui.InfoPanel`) - v13+

Reusable 3D World-styled info display. Use for stats, inventories, leaderboards, dialogs, etc.

```js
// Create a panel
const stats = new ui.InfoPanel({ title: 'Player Stats', position: 'top-right' });
stats.addRow('Health', '100', { color: '#0f0' });
stats.addRow('Score', '0', { bold: true });
stats.addRow('Ammo', '30');

// Update values (call in onUpdate or on events)
stats.setRow('Health', '75', { color: '#ff0' });
stats.setRow('Score', '1500');

// Remove a row
stats.removeRow('Ammo');

// Toggle with a key
const inv = new ui.InfoPanel({ title: 'Inventory', position: 'bottom-right', toggleKey: 'KeyI', visible: false });

// Custom position
const custom = new ui.InfoPanel({ position: 'custom', top: 100, right: 20, width: 200 });

// Raw HTML content
stats.setContent('<table>...</table>');

// Cleanup
stats.destroy();
```

**Options:** title, position (top-left/top-right/bottom-left/bottom-right/custom), width, visible, compact, toggleKey

**Built-in debug panel:** F3 toggles version info + FPS + console logs (ON by default)

### Debug Panel
Press **`** (backtick) to toggle the built-in debug panel. Shows:
- FPS counter
- All `console.log`, `console.warn`, `console.error` output
- Use `ui.debug('message')` to log from game code

## Asset Catalog

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

**Note:** The asset pack is being built. For now, use the built-in helpers:
- `assets.createVoxelBox(color, size)` - colored cube for any object
- `assets.createVoxelGround(width, depth, color)` - terrain plane
- Create custom geometry with THREE.js directly

## Genre Recipes

### Obby / Parkour
- Platforms at varying heights with physics.addStaticBox
- 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: write save/load functions in `functions/`

### 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: set in config.js
- Crosshair in HUD center
- Projectile: spawn small box, apply velocity, 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, add 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 for you, 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)
}
```

## 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_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2',
    [ctx.auth.userId, data]
  );
  return { ok: true };
}

// functions/load-progress.js
export default async function (ctx, { db }) {
  const row = await db.findOne('saves', { user_id: ctx.auth.userId });
  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 Support

Touch controls are automatic:
- Left side: virtual joystick (movement)
- Right side: Jump + Action buttons
- Template detects mobile and shows controls automatically

## Features (Opt-in Gameplay Modules)

Enable built-in gameplay features via `config.features`. Features are template-level modules that auto-initialize during boot.

### Enabling a Feature

In `config.js`:
```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)
  },
}
```

### Interacting with Features in 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 */ });
  }
});
```

### Available Features

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

### Multiplayer in Depth

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

Solo / single-player game:
```js
features: { 'multiplayer': false }
```

Basic multiplayer (default):
```js
features: { 'multiplayer': { room: 'my-arena' } }
```

Authoritative 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:
```js
features: { 'multiplayer': { room: 'lobby', sync: { worldState: true } } }
```

With `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).

To 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/`.

## Performance Tips

- Use `assets.createVoxelGround()` - it uses InstancedMesh (one draw call for entire ground)
- For many identical objects, use THREE.InstancedMesh instead of individual meshes
- Keep total triangle count under 100K for mobile
- Limit shadow-casting objects (only player + key objects)
- Use fog to hide pop-in at draw distance

## Deploy Verification

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

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

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

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

## Related Skills

- **app-development** - Functions, database & API for persistence, leaderboards
- **app-realtime** - advanced Colyseus room configuration
- **app-auth** - Sign in with Gipity for user identity
- **app-llm** - AI-powered NPCs using LLM service

