DARKER Research archive

Explosion, smoke and flame recipes

Recipe browser · Structured export · Native checks

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:

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

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

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

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