← All decisions

Merge visual feedback: a whole-canvas brightness flash, not a per-position particle burst

merge-gameuifeedback

Context

Last item in the feature-gap backlog from the prior research pass: real Suika-style clones typically show some visual flourish (a particle burst, a scale pop) at the exact point two pieces merge, on top of a sound effect (decision 0035, already shipped). Investigated what a real per-position effect would require: resolve_all_collisions currently returns #(List(Entity), Int) (entities plus score gained) with no record of *where* any merge happened, only *that* score went up -- exposing merge positions would mean changing that function's return shape and threading it through update/tick, a real signature change cascading through pe.gleam's internals for a purely cosmetic feature.

Options considered

Change resolve_all_collisions (and update, and tick) to also return each merge's position, then spawn short-lived synthetic 'particle' entities (matching pe.preview_entity's pattern) that age and expire over a few frames, rendered via the existing WebGPU entity pipeline — rejected
The 'correct-feeling' version of this feature, but a real, invasive change to pe's internal collision-resolution signatures purely to support a cosmetic effect -- not proportionate to what a whole-canvas flash already delivers (a clear, immediate 'something happened' cue) at a fraction of the implementation cost. Left as a documented, real follow-up if the flash alone doesn't feel like enough, not built speculatively now.
No visual feedback at all -- the merge sound (decision 0035) alone is enough — rejected
Every researched real clone pairs an audible merge cue with a visible one; sound alone (especially if a player has audio muted, common in browser games) leaves merges without any confirmation beyond watching the entity count/score change, which is easy to miss during fast play.
A brief, whole-#glemy-canvas-wrap CSS brightness flash, triggered by the exact same pe.Merged sound event already wired up (decision 0035) -- no new pe-side state needed at all — chosen
Reuses Model.sound_events entirely as-is (zero new Core state, zero new pe.gleam surface), costs one small CSS @keyframes block and one non-branching FFI class-toggle, and gives real, immediate, hard-to-miss visual confirmation of a merge -- proportionate to the feature's actual value, matching this project's standing 'don't build speculative infrastructure ahead of a real, current need' practice (ARCHITECTURE.md's own 'what not to do' section, using glemy/cs as the cautionary example).

Decision

index.html gained a glemy-merge-flash CSS @keyframes (brightness pulse, 0.2s ease-out) applied via a .glemy-flash class on #glemy-canvas-wrap. game_ffi.mjs's triggerMergeFlash removes then re-adds that class (forcing a reflow via reading offsetWidth in between -- re-adding an already-present class doesn't restart a CSS animation on its own, which would make back-to-back merges within 0.2s only flash once instead of once each). game.gleam's play_sound_event now calls both play_merge_sound and trigger_merge_flash for pe.Merged, still the single place a SoundEvent becomes actual FFI calls.

Verification

161 Erlang / 184 JavaScript gleam test cases passing on both targets, unchanged (no new pure logic -- this is a direct extension of the already-tested Merged event path from decision 0035). Re-ran tools/browser_check.ts: still passes cleanly with zero page errors, confirming the new FFI call and CSS class manipulation don't throw or break the page; the existing click-and-hold sequence is too short (~500ms, decision 0035's cooldown is 0.5s) to reliably force a real merge, so this specifically confirms the code loads and runs without error rather than exercising the flash itself end-to-end -- a reasonable stopping point for a purely cosmetic feature, not extended further to force a merge scenario.

Consequences

Merges now have both audible and visible confirmation without any new pe.gleam state or signature changes. If a per-position particle effect is ever wanted, resolve_all_collisions/update/tick would need to be extended to expose merge positions -- a real, separate, larger decision, not something this change quietly paved the way for by half-building it.

References