b3d-spawner

Keeps the world populated. Spawns prefabs — usually encounters: a mothership with escorts, a base with ground defences and air cover — around the player, and refills them as you destroy them.

An encounter is not a special kind of thing. It's a prefab whose members are placed in a formation. That's the whole idea:

import { definePrefab, escorts, at, b3dSpawner, b3dDestroyable, b3dRadarBlip } from 'tosijs-3d'

definePrefab('mothership-group', ({ position }) => [
  // The prize. A waypoint blip so the player can FIND it — and it dies with the ship,
  // because the blip follows the mesh and a destroyed mesh reports no position.
  b3dDestroyable(
    { meshName: 'mothership', size: 14, capacity: 400, ...position },
    b3dRadarBlip({ faction: 'hostile', profile: -1 }) // -1 = always detectable
  ),
  // The screen.
  ...at(position, escorts(5, 45, { y: 8 })).map((p) =>
    b3dDestroyable({ meshName: 'escort', size: 3, capacity: 30, ...p })
  ),
])

b3dSpawner({ prefab: 'mothership-group', maxAlive: 1, minDistance: 800, maxDistance: 1600 })

Demo — hunt the mothership

Two hostile groups are out there, each a mothership ringed by escorts. The motherships carry a waypoint blip (profile: -1 — always detectable), so the HUD shows you where to go; the escorts don't, so you only see them when your radar picks them up.

Kill a mothership and its waypoint vanishes — not because anything cleans it up, but because the blip follows that mesh, and a destroyed mesh reports no position. The marker stops existing because the thing it marked stopped existing.

Clear a group and the spawner puts a fresh one somewhere else.

import { b3d, b3dAircraft, b3dRadar, b3dRadarBlip, b3dHud, b3dLibrary, b3dDestroyable, b3dSpawner, b3dDeath, b3dLight, b3dSun, b3dSkybox, b3dGround, definePrefab, escorts, at, gameController, inputFocus } from 'tosijs-3d'

// An ENCOUNTER is just a prefab whose members are placed in a formation.
definePrefab('mothership-group', ({ position }) => [
  b3dDestroyable(
    { meshName: 'mothership', size: 10, color: '#b05050', capacity: 120,
      explode: 'on', explodeForce: 10, ...position, y: position.y + 30 },
    // The prize, and the thing you can FIND: always-detectable waypoint blip.
    b3dRadarBlip({ faction: 'hostile', profile: -1 }),
  ),
  // The screen — no waypoint of their own; radar has to find them.
  ...at({ ...position, y: position.y + 30 }, escorts(5, 55, { y: 6 })).map((p) =>
    b3dDestroyable(
      { meshName: 'escort', size: 3, color: '#d07070', capacity: 20, explode: 'on', ...p },
      b3dRadarBlip({ faction: 'hostile', profile: 1 }),
    )
  ),
])

const plane = () => b3dAircraft(
  { library: 'vehicles', meshName: 'scout', player: true, y: 120, vtolSpeed: 6, maxSpeed: 60 },
  b3dRadar({ range: 900, coneDeg: 90, lockTime: 1.2, maxLocks: 2 }),
)
const focus = inputFocus(gameController(), plane())

const scene = b3d(
  { gamepad: true },
  b3dLight({ y: 1, intensity: 0.5 }),
  b3dSun({ intensity: 0.9 }),
  b3dSkybox({ timeOfDay: 10 }),
  b3dGround({ meshName: 'ground_nocast', width: 6000, height: 6000, color: '#6b7f5e' }),
  b3dLibrary({ url: '/test-2.glb', type: 'vehicles' }),
  b3dHud({}),
  // Two groups at a time, seeded — the same battles in the same places, every run.
  b3dSpawner({
    prefab: 'mothership-group',
    maxAlive: 2, interval: 6,
    minDistance: 700, maxDistance: 1500,
    seed: 42,
  }),
  b3dDeath({ title: 'DOWN', respawn() { focus.appendChild(plane()) } }),
  focus,
)
preview.append(scene)
tosi-b3d { width: 100%; height: 100%; }

The rules it follows

Why the prefab is a NAME

Because a narrative driver can ask for one. spawn('mothership-group', at) is a string, and a string crosses a worker membrane; a closure does not. So "find and destroy the enemy mothership" is a GM composing pieces the simulation already has — with no narrative vocabulary anywhere in the sim.

Attributes

Attribute Default Description
prefab '' Prefab name to spawn (usually an encounter)
maxAlive 2 How many groups may exist at once
interval 8 Seconds between spawn attempts
minDistance 600 Nearest a group may appear (from the player)
maxDistance 1400 Furthest a group may appear
y 0 Base height for the spawn point (the prefab places its own members)
seed 1 Deterministic placement — same seed, same battles
disabled false Stop spawning (a boolean can't default to true — see CLAUDE.md)

Events

spawned — bubbles, detail: { prefab, position, elements }.