b3d-destroyable

A thing that can take damage and be destroyed — the scene-side bridge to the pure CombatWorld (see destroyable.ts / COMBAT-DESIGN.md). Drop it into a <tosi-b3d> and it registers a Destroyable in the scene's combat world, drives a placeholder cube mesh (real meshes/GLB later), and runs its death outcome when killed — whether by a direct hit or a chain reaction resolved in the combat tick.

Demo

Click any cube to damage it (1/click) — it flashes on a hit and dies at 0 hp. The front grid are independent targets; the back row is chain-linked, so killing the leftmost cascades the reaction down the line (chains are wired after mount, since they reference combat ids that only exist once the targets have mounted).

import { b3d, b3dDestroyable, b3dLight, b3dSkybox, b3dGround } from 'tosijs-3d'

const grid = []
for (let i = 0; i < 12; i++) {
  grid.push(b3dDestroyable({ x: (i % 4) * 1.6 - 2.4, y: 0.5, z: Math.floor(i / 4) * 1.6, capacity: 3, color: '#cc4444' }))
}
const chainRow = []
for (let i = 0; i < 6; i++) {
  chainRow.push(b3dDestroyable({ x: i * 1.6 - 4, y: 0.5, z: -3, capacity: 4, color: '#e0a020' }))
}

const scene = b3d(
  {
    sceneCreated(el, BABYLON) {
      const cam = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, Math.PI / 3.2, 15, new BABYLON.Vector3(0, 0.5, -0.5), el.scene)
      cam.attachControl(el.querySelector('canvas'), true)
      el.setActiveCamera(cam)
      // click a cube → damage it
      el.scene.onPointerDown = (_evt, pick) => {
        if (!pick.hit || !pick.pickedMesh) return
        const t = [...grid, ...chainRow].find((c) => c.mesh === pick.pickedMesh)
        if (t) t.damage(1)
      }
    },
  },
  b3dLight({ y: 1, intensity: 0.85 }),
  b3dSkybox({ timeOfDay: 10 }),
  b3dGround({ width: 24, height: 24, color: '#5a6b52' }),
  ...grid,
  ...chainRow,
)
preview.append(scene)

// Wire the chain row once every target has mounted (its combat id exists then).
const wireChain = () => {
  if (chainRow.some((c) => !c.combatId)) { requestAnimationFrame(wireChain); return }
  for (let i = 0; i < chainRow.length - 1; i++) {
    chainRow[i].setChain([{ target: chainRow[i + 1].combatId, amount: 99, delay: 0.15 }])
  }
}
wireChain()
tosi-b3d { width: 100%; height: 100%; }

Death outcomes

What happens when it dies is configurable — the default just removes the mesh, but you can compose:

Demo — a field of fuel drums that chain-explode

Click any drum — it explodes, and the blast sets off its neighbours in a spreading chain reaction (each drum both explodes and fires a deathBlast).

import { b3d, b3dDestroyable, b3dLight, b3dSkybox, b3dGround } from 'tosijs-3d'

const drums = []
for (let i = 0; i < 48; i++) {
  drums.push(b3dDestroyable({
    x: (i % 8) * 1.5 - 5.25, y: 0.5, z: Math.floor(i / 8) * 1.5 - 3.75,
    size: 0.9, capacity: 6, color: '#d06020',
    explode: 'on', explodeForce: 7,
    deathBlast: 'on', blastDamage: 30, blastFullRadius: 1.2, blastRadius: 2.4,
  }))
}

const scene = b3d(
  {
    sceneCreated(el, BABYLON) {
      const cam = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, Math.PI / 3, 18, new BABYLON.Vector3(0, 0.5, 0), el.scene)
      cam.attachControl(el.querySelector('canvas'), true)
      el.setActiveCamera(cam)
      el.scene.onPointerDown = (_evt, pick) => {
        if (!pick.hit || !pick.pickedMesh) return
        const t = drums.find((d) => d.mesh === pick.pickedMesh)
        if (t) t.damage(99)
      }
    },
  },
  b3dLight({ y: 1, intensity: 0.85 }),
  b3dSkybox({ timeOfDay: 10 }),
  b3dGround({ width: 30, height: 30, color: '#5a6b52' }),
  ...drums,
)
preview.append(scene)
tosi-b3d { width: 100%; height: 100%; }

It participates in the floating origin: because AbstractMesh treats the x/y/z attributes as the source of truth for the mesh position, this uses onOriginShift to shift BOTH the mesh node and its x/z attributes on a rebase (NOT registerWorldRoot, which would leave the attributes stale so a later render would un-shift the mesh).

Attributes: capacity, armor, regenRate, regenDelay, protectedBy, protection, the death-outcome knobs (explode/explodeForce, deathBlast + blastDamage/blastFullRadius/blastRadius/blastDelay), plus size/color for the placeholder cube and the usual x/y/z/meshName. Set .chain (a ChainLink[]) in code for direct-transfer chain reactions, or whenDestroyed for a death hook. Call .damage(n) to hurt it (a warhead will do this on contact).