b3d-warhead
The explosion — the scene-side bridge to the pure AOE math in warhead.ts (see
COMBAT-DESIGN.md). On detonate(center) it gathers every b3d-destroyable
in the scene, resolves the blast (full damage inside fullRadius, falling linearly to
1 at blastRadius, 0 beyond), line-of-sight gates each target with a raycast (a
wall between the blast and a target spares it), and applies the damage as an outward
shockwave: each target's hit lands on a short delay proportional to its distance, so
the effect ripples out (near things die first, far last) in step with the expanding
flash — cheap (amounts + delays computed instantly, only application is staggered), not
an all-at-once kill. Single-use — a projectile/bomb owns one and fires it on impact;
here you can also place one and detonate it directly.
Demo
Steer the reticle with A/D + W/S (left stick), pull the right trigger (or F) to
detonate there — the standard controller. Cubes inside the
radius take falloff damage (they flash, and die at 0 hp).
The wall blocks line of sight, and it only runs half the width. So one blast on the near side shows both halves of the rule at once: cubes on the far side behind the wall survive, while cubes on the far side past its end die — same distance, different cover. Slide the blast along the wall and watch the shadow it casts. Turn line of sight off in the ⚙ panel and the wall stops mattering.
import { b3d, b3dController, b3dWarhead, b3dDestroyable, b3dLight, b3dSkybox, b3dGround, label3d, slider3d, toggle3d } from 'tosijs-3d'
import { orbitCam } from 'tosijs-3d/demo-utils'
import { tosi } from 'tosijs'
const { s } = tosi({ s: { damage: 20, fullRadius: 1.5, blastRadius: 5, los: true } })
const warhead = b3dWarhead({ y: 0.4, damage: s.damage, fullRadius: s.fullRadius, blastRadius: s.blastRadius })
// Two blocks, one either side of the wall (which sits at z = 0). The far block is
// the interesting one: its left half is sheltered, its right half is past the wall's
// end and fully exposed.
const ROWS = [-2.6, -1.2, 1.2, 2.6]
const targets = []
for (let i = 0; i < 24; i++) {
targets.push(b3dDestroyable({ x: (i % 6) * 1.4 - 3.5, y: 0.4, z: ROWS[Math.floor(i / 6)], size: 0.8, capacity: 8, color: '#cc4444' }))
}
// Shared reticle position + shoot edge, reachable by both sceneCreated and drive.
const state = { rx: -2.1, rz: -2, shootWas: false, cam: null }
const scene = b3d(
{
gamepad: 'left_stick,right_trigger',
scenePanelOpen: true,
scenePanel: () => [
label3d({ text: 'Warhead', bold: true }),
slider3d({ label: 'damage', value: s.damage, min: 1, max: 60, step: 1 }),
slider3d({ label: 'full radius', value: s.fullRadius, min: 0, max: 5, step: 0.1 }),
slider3d({ label: 'blast radius', value: s.blastRadius, min: 1, max: 12, step: 0.5 }),
toggle3d({ label: 'line of sight', value: s.los }),
],
sceneCreated(el, BABYLON) {
state.cam = orbitCam(el, { alpha: -Math.PI / 2.3, beta: Math.PI / 3, radius: 16, target: [0, 0.5, 0] })
// A wall that runs only HALF the width, between the two blocks of cubes.
// Stopping short is the point: it gives you sheltered and exposed targets
// at the same range, so one blast demonstrates the rule instead of the
// absence of an effect.
const wall = BABYLON.MeshBuilder.CreateBox('wall', { width: 4.2, height: 2.5, depth: 0.4 }, el.scene)
wall.position.set(-2.1, 1.25, 0)
const wm = new BABYLON.StandardMaterial('wm', el.scene)
wm.diffuseColor = new BABYLON.Color3(0.4, 0.42, 0.48)
wall.material = wm
// a glowing reticle you steer across the ground
const reticle = BABYLON.MeshBuilder.CreateTorus('reticle', { diameter: 1.3, thickness: 0.12 }, el.scene)
reticle.isPickable = false
const rmat = new BABYLON.StandardMaterial('rmat', el.scene)
rmat.emissiveColor = new BABYLON.Color3(1, 0.85, 0.2)
rmat.disableLighting = true
reticle.material = rmat
el.scene.onBeforeRenderObservable.add(() => reticle.position.set(state.rx, 0.05, state.rz))
},
},
b3dLight({ y: 1, intensity: 0.85 }),
b3dSkybox({ timeOfDay: 10 }),
b3dGround({ width: 30, height: 30, color: '#5a6b52' }),
b3dController({
mapping: 'biped',
drive(input, dt) {
// CAMERA-RELATIVE steering. World-axis steering is unusable here for a
// reason specific to this demo: its subject is the shadow the wall casts,
// which you can only read by orbiting — and once the camera has swung,
// "forward" was pushing the reticle sideways.
//
// ArcRotate puts the camera at target + r*(cos a*sin b, cos b, sin a*sin b),
// so camera->target in XZ is -(cos a, sin a) = screen "up", and screen
// "right" is up x forward = (-sin a, cos a).
const a = state.cam ? state.cam.alpha : -Math.PI / 2
const fx = -Math.cos(a), fz = -Math.sin(a)
const sx = -Math.sin(a), sz = Math.cos(a)
const step = 7 * dt
state.rx = Math.max(-6, Math.min(6, state.rx + (sx * input.turn + fx * input.forward) * step)) // A/D + W/S
state.rz = Math.max(-5, Math.min(5, state.rz + (sz * input.turn + fz * input.forward) * step))
const shoot = input.shoot > 0.5 || input.sprint > 0.5
if (shoot && !state.shootWas) {
warhead.damage = s.damage.value
warhead.fullRadius = s.fullRadius.value
warhead.blastRadius = s.blastRadius.value
warhead.los = s.los.value ? 'on' : 'off'
warhead.x = state.rx
warhead.z = state.rz
warhead.detonate() // blast at the reticle (F · glass button · XR trigger)
}
state.shootWas = shoot
},
}),
warhead,
...targets,
)
preview.append(scene)
tosi-b3d { width: 100%; height: 100%; }
Attributes
| Attribute | Default | Description |
|---|---|---|
damage |
20 |
Full damage D (direct hit + within fullRadius) |
fullRadius |
1.5 |
Within this distance the blast deals full D |
blastRadius |
5 |
Falloff from D (at fullRadius) to 1 here; 0 beyond |
los |
'on' |
Line-of-sight occlusion (a wall between blast + target spares it) |
x,y,z |
0 |
Detonation point when detonate() is called with no argument |