# Explosion, smoke and flame recipes

[Recipe browser](../analysis/interface/effects/index.html) ·
[Structured export](../analysis/interface/effects/recipes.json) ·
[Native checks](../analysis/interface/effects/verification.json)

The executable's complete recipe-data interval `7014–783E` is now accounted
for: **21 referenced entry points**, **119 distinct 15-byte emitter records**,
**48 distinct six-byte sound records**, and record terminators. Entry points
share suffixes, so summing their individual record counts would double-count
assets. This interval is not the whole effects system: a separate single-sprite
trail path generates records procedurally, and rendering remains procedural.

The browser now includes a composite preview with orbit, elevation and distance
controls, alongside emitter timing, phase thumbnails, motion parameters, sound
layers and building bindings. The composite uses the native per-emitter
projection and drawing order described below. The table thumbnails remain
individual phase samples, not complete emitters.

## Recipe format and original constructor

`6820–689E` consumes a stream pointed to by SI, spawning particles relative to
DX/BX/CX (the two horizontal coordinates and height). The normal entry `680C`
loads those coordinates from an object; `6815` applies visibility/admission
checking. `67BF/67EA` are map-cell entry paths. The constructor restores its
parent BP and leaves SI pointing at the terminating FF byte.

Each emitter occupies 15 bytes:

| Offset | Size | Meaning / destination |
| --- | --- | --- |
| +0 | word | Start delay; runtime deadline `BF8 + delay` at object +2C |
| +2,+3 | signed bytes | Horizontal offsets from the supplied origin |
| +4 | signed word | Height offset |
| +6 | word | Initial radius/extent parameter at object +22 |
| +8 | byte | Initial angle high byte; low byte starts at zero, object +24 |
| +9 | word | Sampling word at object +26; low byte is sample count |
| +11 | signed byte | Radius rate at object +2A |
| +12 | signed byte | Height rate at object +2B |
| +13 | signed byte | Angle rate at object +29 |
| +14 | byte | Lifetime/bright flags; bit 40h ends the emitter section |

The constructor clears bit 40h before storing the runtime flags at +7.
Remaining low bits specify lifetime in **64-tick units**; bit 80h selects the
bright phase branch. Expiry occurs only when remaining lifetime is negative:
the exact zero point still draws phase 0. Likewise the initial maximum phase
may last only the initial tick; do not assign an unconditional 64 ticks to
every frame. Future start deadlines suppress both drawing and movement.

Emitter objects come from list `6FA0`, whose native pool has 25 records of 46
bytes. The largest recipe has 24 emitters. The native allocator can recycle
objects when a pool is exhausted; overlapping effects and pool pressure are
not simulated in the timeline or constructor comparisons.

## Multiple sprites and movement

`689F` draws each emitter through repeated projected sprite calls. Its sampling
word supplies a count and sine-table stride:

```
count = sampling_word & 255
stride = ((sampling_word >> 6) | 3) + 1
initial sine byte index = angle_word >> 5
```

The renderer combines sine/cosine samples with camera-transformed radius
vectors, clips and orders projected points, then calls the existing depth-based
sprite selector. This is more than a single billboard increasing in size.
The composite preview now reproduces this per-emitter projection and traversal
with integer arithmetic, checked against original execution. Camera setup,
whole-scene ordering and world visibility remain inspection substitutions,
as detailed below.

`6CD8–6D00` updates an active emitter with the signed low byte of the frame tick
increment:

- Radius word += signed radius rate × signed tick increment, wrapping at 16 bits.
- Angle word += signed angle rate × signed tick increment, wrapping at 16 bits.
- Height advances by signed height rate × tick increment / 8. Object +6 retains
  the fractional byte, so repeated negative or fractional changes must not be
  replaced by separately rounded floating-point additions.

The comparison uses whole object records, including the fractional byte, with
1, 16 and 63-tick steps for every distinct emitter. These checks do not establish
arbitrary large-frame scheduling behaviour.

## Attached sound layers

After the last emitter come six-byte sound records, terminated by a byte FF:

| Offset | Meaning |
| --- | --- |
| +0 | Duration in 16-tick units |
| +1 | FM patch ID |
| +2..3 | Pitch parameter |
| +4 | Behaviour flags |
| +5 | Level high byte; low byte is initialised to FF |

They allocate into list `6F90`, at the recipe's supplied origin. Their deadlines
are initialised during the same spawn call; emitter start delays do not delay
individual sound layers. The constructor uses clock BF8, while sound processing
at `362E` checks BF0. Their relationship during pauses/transitions is not
established by these isolated tests. Live admission, Doppler, panning and voice
competition also remain separate.

Patch references link to the existing reference-tone auditions. Those tones
are **not newly rendered recipe audio** and use different audition inputs;
the exact recipe pitch, level and duration are preserved in JSON and the table.
[Combined recipe auditions](combined-effect-audio.md) are now available separately
through the browser playback controls.

## Caller associations and shared entries

Names in the browser are descriptive associations, not recovered symbols.
They preserve pointers and caller addresses for further investigation.

- `7014`: player destruction path through `6F4F`; `7050` enters its suffix,
  omitting the first four emitters while sharing the sound tail.
- `70C3/70F0` and `716C/7199` are additional full/suffix pairs used by collision
  responses. Shared data does not mean the callers have identical gameplay roles.
- `75D4`: the Diffuser's building-marking path (`CE74–CE81`). Its **24 emitters**
  extend through tick 9926; tick 9927 is after the last lifetime. At the inspection
  rate of approximately 500 Hz, the sequence lasts nearly 20 seconds. Early
  bright emitters give way to numerous ordinary smoke emitters; the last starts
  at tick 9350 with a 576-tick lifetime.
- `75A3`: Diffuser unsuccessful-hit fallback; `77C5`: ground-vehicle route
  effect selected at `9028`.
- The remaining entries are associated with object hit/removal/damage paths
  or the map-effect directory. Their precise visible identity in every mission
  is not inferred merely from their position in the executable.

## Building-to-effect mapping

`67BF` advances map state by 20h, then reads **byte +5 of the eight-byte city
 type record** and indexes the seven-word directory at `783F`. The export maps
all **391 types across banks 30–32** to a recipe and links their model views.
Seven native tests verify directory selection and the state increment with
visibility admitted; this is not a test of all destruction eligibility rules.

| Effect index | Recipe |
| --- | --- |
| 0 | 7199 |
| 1 | 7749 |
| 2 | 7518 |
| 3 | 747E |
| 4 | 73C6 |
| 5 | 721C |
| 6 | 77FF |

For Delphi, type 11 streetlights select effect 5, type 76 skylight roofs select
1, and type 80 factory chimneys select 2. Underground types 111/112, the doors
previously identified on the Wrecker route, select effect 6. This links those
objects to their effect recipes; it does not establish that ordinary player
collisions can destroy every one of them.

## Separate damage-trail emission

`79C2–79E4` can call `676E` while updating objects. It reads the high byte of the
damage accumulator at +64/+65, skips object flags 18h, and gates emission with
bits from `C50`. Values below 80h produce no trail through this branch. Values
80h–9Fh use several cadence masks and the minimum lifetime; A0h upward uses
mask 40h and a clamped severity value 0–63. These masks are not labelled as
emission rates in hertz without tracing their clock producer.

`676E` emits one sprite into the distinct `6FA8` pool, at saved object position
+38/+3A/+3C with random horizontal offsets −16..15 and vertical offsets
−128..124 in steps of four. Flags are `88h + (severity >> 3)`, giving lifetimes
512..960 ticks. At the low end, the bright flag never reaches the bright phase
range: its starting phase is only 8. Higher severity can expose the fire phases.

These are stationary records after spawning, unlike the moving/expanding
recipe emitters. Their placement behind a moving object creates a trail.
There are 192 native emission checks using three supplied random words and all
64 severity values. Random-number generation and visibility are substituted;
allocation, offset arithmetic, flags and timestamp storage execute natively.
The impact path `6730/6742` also uses the single-sprite constructor `6795` and
adds sound. Its input-dependent impact classification remains to be expanded
into separately named recipes.

## Verification and reproduction

- **42 constructor cases:** all 21 entries at ordinary and wrapping clock/world
  coordinates, using the original pool initialiser and allocator. Checks cover
  every decoded field, sound record, parent BP restoration and stream boundary.
- **357 movement cases:** three frame increments for each of 119 unique emitters.
- **152 aging cases:** every stored flag value, waiting/initial/phase-boundary/
  last-live/expired states, including clock wrap.
- **192 trail cases** and **7 map-directory cases**, with substitutions stated above.
- Coverage assertion accounts for all **2091 bytes** in `7014–783E`.
- Browser checks compare its phase decisions with the native results, visit all
  21 entries, load their sprites and exercise playback/reset.

```sh
PYTHONPATH=/tmp/darker-python python3 tools/verify_effect_projection.py
PYTHONPATH=/tmp/darker-python python3 tools/extract_effect_recipes.py
NODE_PATH=/tmp/riotfire-browser/node_modules node tools/verify_effect_recipes.js
```

Original DOS files are untouched. The exported recipes are evidence for the
faithful reconstruction; the timeline is an inspection tool, not the start of
an engine implementation.


## Composite preview and native projection comparison

The browser's [projection translation](../tools/effect_projection.js) is checked
against **1091 native executions** of `689F`: all 119 emitter definitions at
initial, middle and final live ages under three supplied camera matrices, plus
20 cases exercising near-plane rejection, small sample counts and far depth.
The [captured calls](../analysis/interface/effects/projection-verification.json)
retain the synthetic inputs and the exact ordered `(screen X, screen Y, depth)`
arguments observed at `6AA6`. Browser verification compares every result,
including order and duplicate calls, rather than comparing just a bounding box.

The implementation preserves:

- Signed radius multiplication and high-word arithmetic before the additional
  two-bit shift used to build the projected radius coefficients.
- Separate 24-bit camera-space coordinates with eight fractional bits. Sine and
  cosine contributions are truncated independently before addition, including
  their carries and signed wraparound.
- Rejection below native integer depth 32, signed division truncating toward
  zero, and screen offsets (160,84) for the inspection viewport.
- The original two-direction stack traversal around the ring. It begins at the
  farthest accepted sample and walks the two arcs toward the nearest. Ties keep
  the first selected extremum. In some cases it draws the nearest sample twice;
  this redundant call is retained, not simplified away.
- Depth thresholds 1752, 2454, 3510, 4914 and 8190, selecting the extracted
  16/12/8/6/4-pixel sprites, then the phase-specific single-pixel colour.
- The even-X destination alignment and corresponding source alignment variant.
  Canvas clipping handles the inspection viewport edges without scaling sprites.

The composite uses these original-order calls to place the existing opaque,
masked PNGs. It does not substitute alpha-faded particles or interpolated sizes.
Changing distance therefore visibly changes size in the same discrete steps as
the native selector. CSS enlarges the finished 320×168 canvas with nearest-neighbour
sampling; it does not change the internal projection resolution.

### Inspection scene limits

The per-emitter comparison starts with a supplied camera-space centre and camera
coefficients. The browser's orbit/elevation controls construct a synthetic camera
and quantise those inputs; this does not verify the full original world-to-view
setup. The inspection camera aims at a fixed height of 100 raw height units,
uses horizontal scale 2 and height scale 1/4, and exposes depth independently.
These are explicit scene setup choices, not recovered player camera behaviour.

Between emitters, the browser draws by centre depth (ties by descending record
address). The game's complete render-list construction uses a different spatial
metric and also interleaves world objects; that is not reproduced here. Within
each emitter, the captured native ordering is retained exactly. City occlusion,
visibility admission and pool recycling are absent.

Motion is evaluated from ideal elapsed ticks rather than running the original
frame scheduler. The previously verified rates and wrapping radius/angle words
are retained; first-update timing and delayed-start frame overshoot remain
outside the comparison. Sound layers now have [synchronized combined auditions](combined-effect-audio.md),
with nonspatial voice assignment and a 500 Hz clock assumption. The separate single-sprite damage trails are not emitted
by this recipe-only preview.

Browser checks exercise all 21 recipes, compare all native projection fixtures
and existing phase fixtures, test playback/reset and the camera controls, and
retain composite screenshots beside the JSON. No original DOS files are changed.
