Native Port - Behavior Coverage, the SCENE story machine, and the browser build SHIPPED

Changelog · 2026-06-24 · branch native-port. Picks up where the N1–N5 changelog left off. With the structural waves done (the engine plays the levels), this past day's work was about breadth: giving the individual game objects their real behavior, wiring the campaign's story-progression machine, and making the browser build actually expose both.

Summary

After waves N1–N5 the engine had the skeleton — bases, enemies, pickups, vehicles, and the game-flow/menu layer — but most of the ~90 distinct object types placed in the levels were still inert props. This past day closed almost all of that gap, driven by a single instrument: the new resolution/usage-aware Asset Catalog, whose strict query (“authored in a level, but no native behavior”, ranked by reach) gave an objective worklist. Working down it took the count of used-in-level FourCCs with a native behavior from the low-30s to 90 of 93 — the last three are documented resolver/positioning gaps, not behavior gaps. On top of that breadth, two structural pieces landed: the SCENE sequencer (the campaign's story-progress state machine, with both of its writers) and the first C3DGoddard companion slice. Finally, the browser build got a Campaign toggle and a real keyboard-input fix so the story and the talk system are reachable on the web. Every step held the same gate: tools/audit_faithfulness.py at 0 findings across all 35 levels, and a WASM rebuild verified by tools/qa_web_verify.py (16 browser checks).

areawhat landedresult
CoveragePer-FourCC behavior, worked off the Asset Catalog lensused-in-level native behaviors ~30 → 90 / 93
SCENETask-state story sequencer + both writers (triggers & talk)campaign story beats advance; gated actors appear/hide on cue
GoddardC3DGoddard companion + 3MEP fetch + scripted controlfollows Jimmy, fetches cans, obeys scripted sit/patrol triggers
WebCampaign toggle button + keyboard-input fix + touch-mode fixstory & talk now reachable in the browser; live

The instrument: a resolution/usage-aware Asset Catalog

tools/build_asset_catalog.py · the worklist behind everything below

Rather than guess what to port next, the catalog cross-references every FourCC's visual resolution (which mesh/sprite/canvas it draws) with its level usage (how many instances, in how many levels) and its native behavior (which vtable, if any). The behavior worklist is then a strict query — instances > 0 AND native_behavior IS NULL — ranked by instances × level_count so the highest-reach gaps come first. Each class is then read from its decompiled spec (docs/decomp/<Class>.md) and ported to match, not invented. The catalog is browsable at exentt.com/JN-assets/catalog.

catalog lens (end of this pass): 93 used FourCCs · 90 native vtables · 3 remaining
remaining 3 = 3ROK / 3SPR / 3DAI  (resolver/positioning gaps, not behavior gaps)

Behavior coverage — working down the lens DONE

Dozens of classes ported in themed passes, each one read from its decomp spec first and committed per class. Every pass ended on a clean faithfulness sweep.

passclasses (FourCC)shape
Placed actors (N2.x)3TUR turret, 3TES tesla, 3LAS laser, 3YSH, 3EYE, 3DIG, 3HOO, 3CIN Cindyactive hazards + patrol actors + the first friend NPC
NPC / friend cast (N2.y)vt_friend: Nick/Sheen/UltraLord/Libby/Hugh/Benny/Judy/Kitty; 3FLE→Yokian; 3SUM/3PIR escorts; 3AIT AITriggeridle/patrol NPCs that watch Jimmy; the AI/script mission-wiring volume
Actor-focus closeout3PHO phone booth, 3RCK placed patrol rocket, 3HUM Humphreysolid contact prop, autonomous toy rocket, hidden clone-controller
Base/effect tails (4 passes)3NEU/3RED/3ARR, 3LIO/3OMT/3CON, 3TRO/3LEA/3TAR/3AIO, 3LIG/3FIS/3GIR/3SPA/3STAneutron pickups, light/OMT data rows, decor billboards, set-dressing creatures
Animated sprite + door3ANI sprite-frame animator, 3SWN timed swing doorcarnival bottle animations; quarter-turn yaw doors
Long-tail close-outvt_creature (Dino/Camel/Sparrow…) + new vt_prop (3HYD/3CUB/3TOL/3SPH/3TEL/…)shared C3DAI creatures; one gated static-prop leaf, solidity strictly from authored HasCollision
Discipline throughout: where a class's gameplay method needs a subsystem the port hasn't reached yet (a scripted-trigger chain, the SCENE sequencer before it landed, an unported player slot), the minimal faithful port is the visibility/collision gate over the already-correct visual — documented per class — rather than an invented mechanic. A game-owner correction also landed: the shrink ray turns certain AI (Dino/Darwin/Humphrey…) into small moving pickups, which is why those classes carry a pickup base; the active transition is still deferred.

The SCENE sequencer — the campaign's story machine DONE

task_loader.c store · game_flow.c accessors · behavior_ai_trigger.c + behavior_friend.c writers · docs/decomp/_scene_sequencer.md

SCENE is one tag in the CTaskList task-state table — the campaign's story-progress counter, seeded to 30 at New Game. It has no autonomous driver; it only moves when a story event writes it. Reverse-engineering recovered the two writers and they are now both ported:

1. Player triggers. C3DAITrigger::ApplyAITriggerStoryProgress is a hardcoded ObjectTag × current-SCENE → new-SCENE patch table (~25 beats) run when the player trips a story-trigger volume. 2. Friend talks. Each friend's talk-pulse re-reads SCENE and writes the next beat, collapsed into one shared table driven from a single entry point (the T key → nearest friend).

Two previously-deferred consumers are now freed by the live store: 3FOW/C3DFowl (per-level SCENE windows) and 3YCA/C3DYokCargo (LEV5 SCENE>489), plus 3HUM's clone reveal at SCENE==0x5a. Every gate treats “no task loaded” as “show”, so a direct --level / audit launch is unchanged — the gates only bite in a real campaign run.

Validated against the visible result: New Game → tripping teleportexplanation advanced SCENE 0x1e→0x23; talking to friends advanced their beats (Carl 0x32→0x3c, …); level4c fowlinv closed the fowl window (visible→hidden); SCENE=0x5a revealed Humphrey + 4 clones.
[SCENE] 'teleportexplanation' advanced SCENE 0x1e -> 0x23
[TALK] 'C3DCARL' (3CAR) advanced SCENE 0x32 -> 0x3c

C3DGoddard — Jimmy's companion DONE

behavior_goddard.c · 3GOD (code-spawned) · docs/decomp/C3DGoddard.md

Goddard has no .gam rows — the original spawns him in code — so the native engine synthesizes one C3DGODDARD companion after level load, but only in a campaign run (or an explicit test probe) and only when the level data references him. He honors the documented disabled-level table (he stays hidden in level1c/1d/4a/6a/6/7/vr06), follows Jimmy at a short offset, and runs the first re-activated mechanic: the 3MEP metal-can beacon asks Goddard to fetch a can within range, after which he collects it and returns to follow.

The scripted-control tail then wired the C3DAITrigger branch that targets him: the ~30 3AIT rows with AITarget == C3DGODDARD (SITGODDARD, GOGODDARD, PUTGODDARD, …) now drive his mode — patrol an authored point chain, follow, or hold a scripted sit pose — so a cutscene that places or walks Goddard isn't instantly overridden by his follow loop.

New Game: GOGODDARD → Goddard walked GODDARDPAT1 → GODDARDPAT2; SITGODDARD → teleported to gstart and held; the 3MEP fetch/collect/return path was un-regressed.

The browser build — campaign + keyboard, made reachable LIVE

web/shell.html, gamestate.c, game_flow.c, input.c, behavior_player.c

All of the story work above was invisible in the browser, because campaign mode was only reachable from the command line and a real input bug silenced the talk key.

Campaign toggle. A new Campaign button (and a ?newgame=1 URL) begins the New Game task at runtime — campaign mode on, SCENE seeded — and swaps into level1b; toggling off returns to free-roam. So the story progression and the friend talk-rewards can now actually be played in-browser.

A real input bug, fixed: input_just_pressed() aliased SDL's live keyboard array, which the browser updates asynchronously between frames — so for any held key previous == current and the “just pressed” edge was never seen. Keyboard jump / throw / respawn / talk all silently did nothing in the browser (only held-key movement and the event-based B key worked). The input layer now keeps its own per-frame snapshot, so edge-triggered keys work in both the native and web builds. The T key (talk to the nearest friend) was also un-bound from a redundant turbo toggle (the Speed button keeps that), and now reports the nearest friend's distance so it's observable.
Also hardened the touch/mobile detection: it keyed only on (pointer: coarse), which false-positives on touchscreen laptops and got the page “stuck in mobile mode” (hiding the keyboard controls). It now requires a coarse primary pointer and no fine pointer, with a ?touch=0 / ?touch=1 override.

Verified against the live deployed build in headless Chromium: ?newgame=1 → Campaign: On at level1b; a real keydown fires [TALK] and does not toggle turbo; the Campaign button round-trips On/Off with the level swaps logged.

QA backlog campaign — 4 community tickets 18/24 CLOSED

2026-06-24/25 · branch native-port · workflow docs/qa_ticket_resolution_workflow.md · living handoff docs/qa/qa_backlog_campaign_handoff.md

With the behavior-coverage breadth above in place, this pass worked a backlog of four community QA tickets — sandmanfan (2026-06-24, 10 reports), two awefan tickets (level1 + level1b), and lu9 (2026-06-14) — deduped to 24 unique reports. Each was taken through the same contract: read the matching docs/decomp/<Class>.md first, prefer authored data and engine rules over one-off edits, and hold the tools/audit_faithfulness.py gate at 0 findings / 35 levels after every change. 18 reports are fixed-and-verified, 2 are resolved as faithful-to-original (WONTFIX-as-bug), and the remaining open work is Cindy location/pathing plus the documented Group I audio tail.

areawhat landedresult
Placement3SPH invisible; foot-anchor 3HUG/3CIN/3SHE; STRT spawn-yaw on level loadsfloating/mis-grounded actors seat correctly; Jimmy faces the authored way on lab-exit
Model/visualauthored button meshes + RGB pulse; enemy PROJ→missile; 3FLE→fleet commander; l3c tree04 glb; bigger bouncier red neutronsthe reported wrong/floating models now draw the authored asset
Logic gatel1b 3ARR/LOAD gated on authored RequiredTask + scoped Goddard-bowl ShowArrowcampaign-only arrow/teleport no longer fire out of sequence; bowl arrow drawn
Motion/pathl1 3SAI seats SailBoat.glb on the ncwater* surface; l3d 3CIN terminal-route loop mismatch investigatedboat no longer floats above the river; Cindy still needs location/pathing follow-up
Audiol6a doors loop soundeffects.omt[59] while moving, halt at fully-opendoor open-SFX no longer one-shots short of the motion
Faithfulapple-pie fruit bowl; house02 open-bottom facaderesolved WONTFIX-as-faithful — the “defect” is the original's authored data

Report ledger

#levelmodelissuestatus
1l13SPH ×2draws a stray “fan”fixed invisible
2l43SHEcharacter floatingfixed grounded
3l4a3HUGnot at ground levelfixed foot-anchor
4l3d3CINoff-ground + pathing/wall-clipground fixed / location deferred
5l13SAIboat floats above riverfixed water anchor + route audit
6l6a3BUTbuttons float + should flashfixed mesh + RGB pulse
7l6a3DORdoor SFX should loop until stopfixed loop+halt
8l6aPROJshould be plasma not baseballfixed enemy missile
9l6a3FLEshould be Yokian fleet commanderfixed
10l3ctree04wrong tree texturefixed per-level glb
11l53REDred neutrons too smallfixed size+pulse
12l1c3PICapple-pie shows fruit bowlWONTFIX-faithful
13l1house02floor under house missingWONTFIX-faithful
14l13JIMfaces lab not house on exitfixed STRT yaw
15l3c3BUTbutton orientation offfixed authored mesh
16l1b3ARRmisplaced arrow + wrong teleportfixed task-gate
17l1b3PICmissing coin pickupverified already drawn
18–21l1croomsfurniture proximity soundsopen audio feature
22l3a3JIMride-track sound stacks/never stopsopen emitter TBD
23l1a3JIMshrink-ray SFX plays as musicopen music-db resolution
24l1RocketPadmissing “find rockets” voice lineopen audio

Notable fixes this pass

behavior_door.c · report #7 (l6a halldoor01)

Door opening SFX now loops while the door moves. The rise-open door (3DOR/3DUD/3SCD/3GAT) played soundeffects.omt[59] — a handle literally named “Door opening loop” — as a one-shot, so it ended long before the door finished. It now plays looping (loops=-1) on trigger, stashes the mixer channel in unused door scratch, and halts it the moment the door latches fully-open. Audio-off builds stay safe (the play returns −1, the halt no-ops).

Verified the state machine end-to-end: spawning the player on halldoor01 logs [DOOR] opening → [DOOR] open, i.e. the loop starts on trigger and is halted on the fully-open transition. (xvfb has no audio device, so the loop is not audible headless — by-ear confirmation is a desktop-session job.)
behavior_base.c / behavior_arrow.c / behavior_load.c / main.c · report #16 (l1b)

level1b arrow + teleport gated on the authored story task. The level authors two arrow/load pairs keyed on campaign progress (RequiredTask=SCENE, RequiredLevel=60/65) plus a hidden gdish pickup trigger (ShowArrow=1) over Goddard's bowl. The native build ignored the task gate, so the campaign-only lab-water arrow/teleport showed up out of sequence and the authored bowl arrow never drew. A new behavior_required_task_gate_allows() evaluates the authored level window against the live SCENE task state (and leaves direct --level/audit runs unchanged); LOAD now uses its authored Radius, and a scoped pickup-arrow draws over the bowl trigger only — the global chunk-106 hidden rule (l1 hydrant/nests/pads) is untouched.

Audit confirms level1b reports pickup-arrow=1 while level1 keeps its hidden referent triggers with no arrow leak; the yellow arrow renders over the GODDARD dish.

Resolved as faithful-to-original

#12 apple-pie (l1c 3PIC). The authored SpriteIndex=157 is “FruitbowlEmty” — the fruit bowl is correct. The apple pie comes from the unimplemented fruit-fill mechanic (“Apple Pie%”), deferred to mechanic work, not a texture swap.
#13 house02 floor (l1). Deterministic glb analysis shows house02.glb is an open-bottomed facade: its only low/horizontal up-facing faces cover ~4% of the footprint (a corner step), and the coverage is identical in level2/level2b — not a level1 export bug. The 6 background houses are all open shells; only Jimmy's house ships a dedicated HOUSE BASE mesh. Adding a floor would invent geometry the original never had; the reporter saw the hollow interior only via the free QA inspection camera, which normal collision keeps the player out of.

Still open — documented Cindy + audio work

#5 boat (l1) is closed, but #4 Cindy (l3d) is incomplete/deferred because Cindy is still not in the correct location. The Group I audio set (l1c furniture proximity sounds #18–21, l3a ride-track stacking #22, l1a shrink-ray-as-music #23, l1 RocketPad voice line #24) is the remaining audio tail — 3SOU is confirmed clean (halts on exit), so the l3a stacking source is an as-yet-unidentified emitter. Audio cannot be verified by ear in the headless rig, so these are best finished against a desktop session. Full per-report evidence lives in the living handoff.

Validation gate (every step)

make                                        # native build
python3 tools/audit_faithfulness.py         # 0 findings across all 35 levels
python3 tools/build_asset_catalog.py        # refresh the coverage lens
make web
python3 tools/qa_web_verify.py              # 16/16 browser checks
./tools/deploy_wasm.sh                       # publish to exentt.com/jn-engine

Live build (with the QA-campaign fixes): jnengine.aef9117b.js, assets 63737d29 — deployed 2026-06-25. Each change is committed individually on the native-port branch.