← All decisions

Render the drop preview circle Model.preview_x was tracking but never actually drawing

merge-gamerenderingpredictabilityui

Context

Investigating the user's 'hard to predict' complaint (alongside decision 0029's restitution fix) turned up a second, independent gap: pe.gleam's Model has carried a `preview_x` field since Phase 5 (decision-documented as existing specifically 'for rendering a preview'), and `tick` already computes and clamps it every frame from the cursor position -- but render.gleam only ever draws `model.entities`, never anything derived from `preview_x`. The feature was half-built: the state existed, but nothing ever reached the screen. Cross-referenced against research on real Suika-style clones (multiple independent implementations were found to include this, described as 'preview for where the fruit will drop') -- this is a standard, expected feature of the genre, not a nice-to-have. Without it, a player has no way to see where the next piece will land, or even what size/tier it is, before committing to a drop; they can only aim by trial and error, which independently compounds the 'hard to predict' feeling decision 0029's restitution fix targets from the physics side.

Options considered

Leave preview_x tracked but unrendered (status quo) — rejected
A real, user-facing gap directly relevant to the 'hard to predict' complaint being investigated, and a feature every researched real clone includes -- not implementing it once found would be leaving known, cheap, high-value work undone.
Extend the WebGPU shader/FFI to support a distinct 'ghost' rendering mode (transparency, outline-only, etc.) for the preview — rejected
Would require touching render_ffi.mjs's WGSL shader and per-entity buffer layout -- real, but avoidable, complexity and FFI surface area (decision 0016's standing rule: keep FFI minimal). The core value (seeing *where* and *what size* the next piece is before dropping) doesn't depend on the preview looking visually distinct from a real entity, only on it existing on screen at all.
Add a pure `pe.preview_entity(model) -> Entity` (a synthetic, stationary Entity at preview_x/spawn_y/current_tier's radius, never added to model.entities or touched by tick's physics) and have game.gleam's tick_and_render prepend it to the entities list passed to the existing, unmodified render pipeline — chosen
Reuses 100% of the existing per-entity draw pipeline (position/radius/tier-color) with zero shader or FFI changes -- the preview is genuinely just another circle from the renderer's perspective. Keeps the computation in pe.gleam (pure, gleam-test-covered) rather than game.gleam, matching this project's established pattern of pe owning game-state/policy decisions. Suppressed once game_over (nothing more will ever drop).

Decision

Added `pub fn preview_entity(model: Model) -> Entity` to pe.gleam, constructing a stationary Entity at (preview_x, spawn_y) with current_tier's radius/tier. game.gleam's tick_and_render now renders `[pe.preview_entity(next_model), ..next_model.entities]` instead of just `next_model.entities`, unless `next_model.game_over` is True. tools/browser_check.ts's real-browser regression check was updated: the starting demo scene now correctly renders 4 circles per frame (3 real entities + 1 preview), not 3 -- a genuine, expected change to what the check verifies, not a loosened assertion.

Verification

146 Erlang / 180 JavaScript gleam test cases passing on both targets, unaffected (game_test.gleam's existing tick_and_render tests still pass unchanged -- none of them asserted an exact entity/pixel count that the extra preview circle would invalidate). Re-ran tools/browser_check.ts against a real headless-Chromium session after updating its entity-count expectation: passes cleanly, confirming the preview circle is really being drawn every frame (4 entities/frame at rest, 5 immediately after a real click-and-hold spawns a new one) with zero page errors.

Consequences

Every drop now has a visible, accurate preview (position and size/tier) before the player commits to it, matching standard genre convention and directly supporting predictability. The preview currently renders identically to a real entity (same solid color, no transparency/outline) since that was out of scope for this pass -- a distinct 'ghost' visual treatment remains an open, purely cosmetic follow-up if wanted, requiring the shader/FFI work explicitly deferred above.

References