← All decisions

Collision physics is now mass-aware (proportional to area), superseding the equal-mass simplification

merge-gamecollisionmassdecision-0007-superseded

Context

Phase 1 of the approved merge-puzzler plan. The original collision.resolve (decision 0007) assumed all entities have equal mass regardless of radius -- a reasonable simplification when every entity in the sandbox was roughly the same size, but wrong for a merge game where tier sizes differ substantially (radius 4 up to 24 across 6 tiers): a tiny entity striking a huge one should barely move it, not push it around as if they were equal weight.

Options considered

Keep the equal-mass simplification — rejected
Would look physically wrong for a game whose entire mechanic depends on circles of very different sizes colliding constantly -- exactly the scenario the simplification was never validated against.
Mass proportional to area (mass = radius^2, uniform 2D density), general unequal-mass elastic-impulse formula — chosen
Standard, textbook physics for 2D circular bodies. The general formula (impulse_scalar = -(1+e)*velocity_along_normal / (1/mass_a + 1/mass_b), applied as impulse_scalar/mass to each side) provably reduces to the exact old formula when mass_a == mass_b -- verified directly, not just claimed: the pre-existing head_on_equal_mass_collision_swaps_velocities_test (equal radii, hence equal mass) passes byte-for-byte unchanged.

Decision

glemy/pe/collision.gleam's resolve now computes mass(e) = e.radius^2 for each entity and uses it both for the velocity impulse (general unequal-mass formula above) and for position-separation weighting (each entity's share of separating an overlap is proportional to the OTHER entity's mass -- heavier entities move less). overlap(a, b) -> Float was also extracted as its own public function (previously inlined), needed by Phase 2's merge-eligibility check. mass is a private helper, not a stored Entity field -- entities of the same tier always share the same radius (glemy/pe/tier), so it's always cheaply derivable, and storing it separately would just be one more place for it to drift out of sync with radius.

Verification

The pre-existing equal-mass regression test (head_on_equal_mass_collision_swaps_velocities_test) passes completely unchanged -- the actual proof the generalization is correct in the equal-mass case, not just an algebraic claim. Added two new invariant tests using float.loosely_equals (the first tolerance-based comparison in this test file, since dividing by 1/mass_a + 1/mass_b routinely produces inexact decimals for arbitrary mass ratios): momentum conservation and kinetic-energy conservation, both for an unequal-mass (16 vs 1) collision -- independent physical checks, not just 'trust the formula'. Rewrote asymmetric_radii_still_separate_positions_evenly_test (which explicitly pinned down the OLD equal-split behavior) into asymmetric_radii_separate_positions_weighted_by_mass_test, with expected values computed via the same arithmetic expressions as the source (not hand-typed decimal literals, since the true values are repeating fractions like 1/13 that can't be represented exactly as finite decimals) -- confirmed to match the actual resolve() output exactly on the first run after the rewrite. 92 Erlang / 125 JS tests passing.

Consequences

Explicitly supersedes decision 0007. Any future code relying on the old 'equal mass regardless of radius' assumption (none currently exists outside collision.gleam itself) would need updating. This unblocks Phase 2 (the merge algorithm), which needs collision.overlap as an independent function to decide merge-eligibility without triggering a full physics resolve.

References