BYTEPATH++ Vector Style and Destructible Terrain
Fable 5
Opus 4.8
Summary
A long build session that took BYTEPATH++ from a single-screen combat slice to a 3x3-screen procedurally generated destructible cave with a vector/neon-derived monochrome art style, MOBA-style click-to-move controls, flow-field enemy navigation, and a full render bloom pipeline. Four planned "increments" (bloom, static outline terrain, destruction, combat integration) plus an unplanned but larger cave-map + camera + pathfinding arc driven by owner requests mid-session.
Reference video analysis (vector art-style feasibility):
- Owner supplied
BYTEPATH++/reference/Home - X_2.mp4(a neon run-and-gun concept, "stylish like Vib-Ribbon from PS1 era") and asked how hard the style would be, especially given planned destruction mechanics. - Extracted all 90s at 1fps into 10 3x3 contact sheets with ffmpeg, plus 15fps bursts of two destruction moments and pixel-level crops (
ffmpeg -vf fps=1,hstack/vstack,crop+scale=...:flags=neighbor). - Findings: terrain is OUTLINE-ONLY (bright core + colored halo, interiors pure black, no fill); destruction deforms the outline with right-angle crenellations (suggesting grid-cell removal + outline extraction, Worms-style, not polygon CSG); fresh cuts glow WARM (yellow/orange edge tint cooling back to terrain color) and spit sparks + detached line-fragment debris; characters are hand-animated glowing stick figures; everything else is particles + heavy bloom + slight chromatic fringing; occasional full-screen color washes.
- Verdict: very feasible and CHEAPER for BYTEPATH++ than for the video's author, because the hardest thing in the clip (skeletal stick-figure animation) doesn't apply — cursor-is-the-ship combat already uses geometric entities. The style deletes the whole category of problems that makes destructible terrain ugly (seams, texturing, lighting cut faces) because interiors are never rendered.
- Presented three directions: (A) adopt neon wholesale, (B) monochrome vector hybrid — destruction language + bloom but keeping BYTEPATH's white-on-black identity with color as rare accent, (C) vector under the existing CRT stack. Recommended B; owner chose B.
Increment 1 — Bloom pipeline (owner-tuned):
- New
bloom_layer = layer_new('bloom', 'smooth', width, height)— SMOOTH filter so the effect ping-pong buffer inherits linear filtering (verified inanchor.c: layer_ensure_effect_buffer), making blur taps gaussian-correct and letting game-res glow upsample smoothly at window res over crisp 3x-nearest geometry. - Three new branch-free shaders (branch-only uniforms have been stripped by this GPU before — the
u_text_gammaincident):bright_pass(luma threshold + knee ramp),blur(9-tap separable gaussian using linear-sampling offsets, one shader retargeted between H and V passes via interleaved uniform commands),bloom_combine(additive, bloom on the unit-1 aux sampler like the displacement map). - Pipeline: game layer renders (displacement included) -> copy to bloom layer -> queued threshold + 2x separable blur applies (ping-pong in command order) ->
final_layerdoesdraw_from(game)then a queued additivebloom_combineapply, then CRT. Key engine facts verified:apply_shadercommands chain in queue order with interleaved uniform sets;layer_draw_fromHARDCODES alpha blend, so additive compositing must go through an apply pass. - F1 panel grew bloom rows (on/off chip, threshold/knee/spread/intensity sliders, export button writing
tuner_export.txt). - Owner tuned and baked: spread 3, intensity 1.33 (threshold 0.35, knee 0.3).
Increment 2 — Static outline terrain:
- New
terrain.lua: occupancy grid atTERRAIN_CELL = 3px,terrain_stamp_rect/terrain_stamp_trianglefilling by cell-center rule. - Contour extraction: boundary trace walking cell edges with the solid side on the walker's right, right-turn priority at saddle points (so diagonally-touching regions come out as separate loops), turn points emitted directly (built-in collinear merge), then Douglas-Peucker at 0.75*cell (straightens stamp staircases, keeps >=1-cell carve notches).
- Physics: one static body per contour loop via the raw
physics_add_chainbinding (pixel coords, flat vertex table, tag name). Chain winding for solid-inside loops = visually CW in y-down screen px (px->m does NOT flip y; Box2D chain normals = RightPerp of segment direction, verified against vendoredmanifold.c). - Projectiles use grid marching (swept, half-cell steps) rather than physics — exact impact cell, no tunneling at shotgun speeds.
- Engine bug found and fixed (
Anchor3/engine/src/anchor.c):l_physics_destroy_bodyrecycled shape-pool user-data slots through a FIXED 32-entry stack array; a terrain body carries hundreds of chain-segment shapes, so every future carve rebuild would leak slots until pool exhaustion. Now heap-allocates past 32. - Physics behavior found: Box2D v3 one-sided chain segments POP THROUGH under sustained horde shove (contact vanishes once the body center crosses the segment). Winding was correct — it's inherent. Fix: chains handle everyday contact/sliding + a per-enemy grid containment guard reverting crossings to the last valid position.
- Verify harness gained a two-act structure and a "verify wall" (full-height stamp between a pinned player and the seeker spawns) asserting containment.
Increment 3 — Destruction:
terrain_carve(x, y, radius)clears a circular blob where each cell rolls its OWN radius jitter (which is what makes bites read as chewed rather than cookie-cuttered); returns cleared count intoterrain.cells_carved— the lifetime ledger the dust economy will read.- Carving only marks chunks dirty;
terrain_updatebatches re-extraction + chain rebuild to at most once per frame (a 12-pellet shotgun blast = one rebuild). - Heat: cleared cells stamp their surviving neighbors (crater rim) with a timestamp;
terrain_drawoverlays those cells' exposed edges over the white contour through a precomputed 12-step yellow->white ramp across ~1s. Cell-indexed (not contour-indexed) so rebuilds never disturb it; both ramp endpoints sit above the bloom threshold so rims glow through the cooldown. - Debris:
line_fragmenteffect class (tumbling jagged polyline sliver, cooling + fading) + warm-tinted sparks per impact. (Owner later CUTline_fragmententirely.) - Owner reaction: "This is extremely good and changes the design of the game a lot actually, but we'll get to that later" — destruction may be getting promoted from mechanic to pillar; design implications deferred by owner.
Cave map + camera + packs (owner-requested, larger arc):
- 3x3-screen map (owner chose 3x3 over 2x2), cellular-automata cave generation, unbreakable map edges, everything inside destructible.
- Chunked terrain — forced by map size and built as the band-scale architecture: grid stays global, but extraction/physics/draw partition into
CHUNK_CELLS = 40(120px) chunks, each owning its contours + static body. Contour walker REWRITTEN to be cell-state-driven (next/prev boundary edge derived from the 4 cells at a grid point + incoming direction, no global edge set), so a chunk can stop exactly where a contour leaves it. Cross-chunk contours become OPEN Box2D chains padded with their TRUE continuation points as ghost anchors -> seamless collision and rendering across seams. Carving dirties only touched chunks (bbox + 1 cell) -> 1-4 chunk rebuilds regardless of map size. - Engine change:
MAX_SHAPE_USER_DATA4096 -> 16384 (cave chains alone are thousands of segments). - CA generation: initially fine-grid at 3px cells -> owner: "the cave is too noisy, there should be way more open spaces and more solid spaces". REWORKED to generate 3x COARSER (
CAVE_COARSE = 3, ~9px features) then upscale + one fine smoothing pass + prune solid islands underTERRAIN_MIN_ISLAND = 80+ keep largest open region. Density swept empirically (standalone Lua scripts): fine-grid 0.45 -> 0.66 open / 0.48 -> 0.49 / 0.50 -> fragmenting cliff; coarse pipeline 0.47 -> 0.45-0.57 open with 15-30 large rock masses. Solid mass now draws as a dark fill (color(34,34,34), per-chunk cached horizontal run rects) vs the pure-black void — the owner-requested open/solid differentiation, kept monochrome. - Generation perf bug: flood fills used hash-backed Lua tables with
#stack/table.removechurn over 130k cells and took SECONDS per attempt. Rewritten with dense preallocated region arrays + manual stack-top pointers -> ~60-110ms total. - Silent hang bug: update() ran exactly twice then froze with no error. The new swept wall collision costs O(distance) in substeps, and in headless mode the garbage mouse position is ~1e8 px away, so frame 2 attempted a ~50-million-iteration movement loop. Fixed by clamping the aim target to the map + a hard substep cap. Diagnosis required a FILE-based probe because killed pipes silently swallow buffered stdout.
- Bright-edge-bands bug (owner screenshot): two causes. (1) The map-perimeter contour line is a bright 1px line sitting exactly on texture row/col 0 whenever the camera clamps to a map edge; (2) the bloom blur's CLAMP_TO_EDGE sampling repeats that edge pixel across all taps, smearing it into a wide band. Fixed both: perimeter contour segments are no longer drawn (rock reads as extending forever), and the bright-pass now fades glow to zero over the outer 3px (new
u_resolutionuniform). - Camera: framework
camera_follow(0.9/0.3, F1-tunable) + map-clamped bounds, snapped to the player at boot; mouse aim converted to WORLD space viacamera.mouse; F1/F3 tuner windows de-parented from the camera (screen-fixed). - Seeker packs: 3-6 members per pack, 8 packs scattered at open spots >=250px from spawn; idle members wander with a home leash; the WHOLE pack aggros when the player nears any member (
SEEKER_AGGRO_RADIUS = 130) or when any member is hit (including a dart poke).
MOBA movement scheme (owner-chosen):
- Owner asked for one-handed mouse-only movement, NOT cursor-led. I presented four schemes (hold-to-thrust / auto-forward-steer / gravity+thrust / click-impulse); owner instead proposed MOBA click-to-move + cursor-aimed firing, and asked for prior art. Prior art given: skillshot MOBA heroes (Invoker/Ezreal), Lost Ark, Albion Online, Nox (1998 Westwood), Halls of Torment's mouse-only mode — the scheme is near-untried in top-down shooters proper.
- Flagged the consequence: click-to-move makes dodging a DECLARED INTENTION with latency rather than twitch, so seeker speed/density will need retuning toward "readable and orb-walkable".
- Player pathfinding (
terrain.lua):terrain.nav= clearance-dilated blocked map (solid OR any 8-neighbor solid), recomputed per chunk alongside contours so pathing tracks carving automatically.terrain_find_path= A* 8-connected, no corner cutting, octile heuristic, STAMP-VERSIONED dense scratch arrays + binary heap with lazy deletion (no per-query clearing — applying the flood-fill perf lesson preemptively), endpoints snapped to nearest nav-open cell, then greedy string-pulling via nav line-of-sight into a few waypoints. - Enemy navigation = FLOW FIELD, not per-enemy A*: one BFS from the player over the coarse grid (
FLOW_MAX_DIST = 80coarse cells), refreshed ~3x/s while anything is hunting; every aggro'd seeker readsterrain_flow_dirat its cell. One computation serves the whole horde, and because it reads the live nav data, a carved breach reroutes the horde within a third of a second. - Controls: RMB click/hold (repaths at ~7Hz) paths the ship; the cursor angles the ship; LMB fires toward it. Old RMB-flip /
attack_mfacing removed.
Movement feel iterations (several owner-driven rounds):
- Round 1: turn-rate caps (
turn_toward(cur, target, p, t, dt, rate)— a rad/s cap layered ON TOP of the existing p/t lerps), residual glide with damping when a path ends/drops (the ship had been stopping dead), and a faint dashed path preview + destination diamond. - Round 2 (owner): merge visual and attack angle into ONE real facing (
face_r— drawn AND fired along), with a separate turn-rate-limited LOCOMOTION heading (move_r) that motion always travels along, so movement ARCS. Two rates: attack (near-instant) + movement (low, produces circling). Engineered around the classic failure:PLAYER_ARRIVE_RADIUSmust exceed the turn radiusmove_speed/move_turn_rateor a heading-locked ship ORBITS its destination forever. - Owner-tuned and baked: move_speed 100, move_turn_rate 9, attack_turn_rate 40, plus "the first attack should always turn instantly towards the mouse position" — a fresh LMB press (including click-spam) snaps
face_rto the cursor instantly (placed after the facing block so the same-frame shot fires dead-on); only held-fire follow-up shots track at the attack turn rate. - Polish batch: soft wall bounce for both units,
line_fragmentdebris REMOVED, path preview alpha 32->48 with the destination diamond filled black under its outline, and carve-time dissolution of orphaned speck islands (TERRAIN_MIN_CARVE_ISLAND = 5, window-bounded flood around each bite).
Wall bounce — research and correction:
- Owner: "Copy the wall bounce from SNKRX-update/Orblike/Invoker." Research found the canonical mechanism is NOT velocity reflection (what I had built) — it's
seeker:wall_bump->seeker:push: steering fully SUSPENDS, damping jumps 3->6 so the impulse drains visibly, the body TUMBLES (random spin tweened to 0), spring-pops, flashes white 0.15s, then control resumes.wall_bumpruns a STREAK: contacts within a 0.5s window escalate force 3/6/9/12 and duration 0.25/0.5/0.75/1.0 (capped at 4x) — a graze is a nudge, grinding gets flung off harder each time. - Ported verbatim (constants copied). Since terrain chains emit no contact events (
enableContactEvents = falsein the binding), a half-body grid probe serves as contact-begin, and the pushed-state gate gives the streak its cadence. Player got a grid-mover-flavored version (deflect + speed FLOOR rather than only shedding + 0.18s steering suspension) since the source games never bump the player.
Seeker movement — floaty regression and verbatim re-port:
- Owner: "Something about the way the enemies are moving makes them look wrong and floaty. Copy them exactly from SNKRX-update/062026" (visuals only adapted — sharp rects, not the source's rounded bodies).
- Diagnosis: the heading-lock I had added for the enemy turn rate made velocity DIRECTION a smoothed state variable on top of already force-limited steering — two layers of turn inertia, so they ice-skated in wide curves and their motion decoupled from their intent. In the source, the force budget (200) against damping (3) IS the turn inertia.
- Reverted to the verbatim SNKRX-update/062026 pipeline: forces ->
apply_force-> velocity clamped to max_speed -> set;self.ris VISUAL only, plainlerp_angle_dt(0.99, 0.1)toward the velocity direction. Removed the heading lock, the rotation turn cap, theseeker_move_turn_rateglobal and its F1 slider.
Enemy wall avoidance tune (final change):
- Owner: "They don't seem to be avoiding the walls that well. Are they also using the separate behavior amongst themselves?" — yes,
steering_separate(16, enemies, ...)verbatim from source, in both branches. - Diagnosed three compounding causes, all tracing to the source's steering being ARENA-tuned and dropped into a cave: (1)
steering_wander(64, 32, 16)at full force parity with seek is a wall magnet in corridors and dilutes the flow field's guidance with noise a third of its strength; (2) the 48px flow "carrot" chords across corners the field routes around; (3)coarse_opencounted a coarse cell walkable if ANY 1 of its 9 fine cells was open, so flow could route through cells 8/9 solid. - Fixed all three:
coarse_opennow requires >=3 of 9 fine cells open; the carrot shortened 48 -> 24px; wander force drops 200 -> 70 while the field is guiding (full only in direct-seek mode). Steering math itself stays byte-identical to the source — only the seek TARGET and one force CAP changed. Noted the source toolkit'ssteering_wall_avoidance(Reynolds 3-feeler) exists but was deliberately rejected by the source games (snkrx-template decision #6) in favor of collision-side wall_bump; held a grid-native wall-repulsion force in reserve rather than adding a new force to the verbatim mix.
Verify harness (grew throughout):
- Two acts: (1) VERIFY-CAVE — generates the real 3x3 cave and asserts open fraction, exactly 1 open region, sealed rim, border carve refused / interior carve bites, plus gen and full-rebuild timings; (2) VERIFY — re-inits with the legacy flat arena for the deterministic physics/carve/path asserts, which double as a regression test for the chunked-pipeline rewrite. Asserts: wall containment (
180 < min_enemy_x < 240, non-vacuous),cells_carved > 0,path_ok. Player pinned at (30,135) withaim_r = 0so scripted shots stay deterministic (the headless mouse garbage has random sign).
Model guidance (asked mid-session): For this session's work — Opus 4.8 for forensic debugging/engine work and architecture briefs; Sonnet 5 is excellent for implementation against an agreed brief, tuning, and polish. Recommendation: default to Sonnet 5, switch to Opus 4.8 when something is weird (silent hang, physics misbehaving, engine C changes).
🔒 Only the summary of this log is public. Private because this is BYTEPATH++.