Collision physics is now mass-aware (proportional to area), superseding the equal-mass simplification
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
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.