fix(render): standardize gradient colorSize to game tiles (#4633)

## Summary

Effect cosmetic attributes are meant to be standardized on **game
tiles** for spatial values and **per-second** for rates. An audit of
every effect attribute against its renderer consumer found one
violation: the gradient trail's `colorSize`.

- The trail shader computed the palette cycle as `colorSize * 4.0 *
count`, so `colorSize: 1` produced ~4-tile bands — while
`CosmeticSchemas.ts` already documented `colorSize` as "band width in
tiles".
- This PR removes the hidden ×4 in `trail.frag.glsl` and the mirrored
factor in the structures gradient pace in `structure.frag.glsl`, so
`colorSize` is now exactly the band width in tiles, and updates the
pace-formula doc comment in `CosmeticSchemas.ts`.

All other attributes were verified already standard: gradient
`movementSpeed` (tiles/s), transition `frequency` (colors/s), spiral
`radius` (tiles) / `rotationSpeed` (rad/s), explosion `size`/`thickness`
(tiles), `speed` (tiles/s), `transitionSpeed` (colors/s). Every animated
path runs off a `uTime` uniform that is genuinely in seconds.

## Catalog impact

- Production `cosmetics.json` ships **zero** effects today, so there is
no live migration.
- Dev catalog gradient entries need `colorSize` ×4 to preserve their
current look (`rainbow_trail` 0.2→0.8, `red_black_trail` 1→4,
`tiel_red_gradient_nuke_trail` 0.5→2, `rwb_structure_gradient` 5→20).

## Test plan

- `npx vitest tests/CosmeticSchemas.test.ts --run` — 51 passed (schema
shape unchanged; this is a shader-semantics fix).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-17 18:27:58 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent cd75d54c5f
commit 1f97f2b252
3 changed files with 5 additions and 7 deletions
@@ -68,12 +68,10 @@ void main() {
} else {
// gradient — cyclic gradient banded across the map (world-space diagonal),
// scrolling over time so a moving trail shifts hue along it. colorSize
// scales the band width (colorSize = 1 ≈ 4 tiles per band); movementSpeed
// = tiles/sec the bands travel.
// = band width in tiles; movementSpeed = tiles/sec the bands travel.
float colorSize = max(texelFetch(uEffect, ivec2(o, rowBase + 2), 0).a, 0.001);
float movementSpeed = texelFetch(uEffect, ivec2(o, rowBase + 3), 0).a;
// 4.0 = tiles per band at colorSize 1; tune for default band thickness.
float cycle = colorSize * 4.0 * float(count);
float cycle = colorSize * float(count);
float phase =
fract((vWorldPos.x + vWorldPos.y - uTime * movementSpeed) / cycle);
float f = phase * float(count);
@@ -84,13 +84,13 @@ bool structuresEffectColor(int owner, out vec3 color) {
// gradient is visible across the shape (world-space banding like the
// trail's would put the entire icon inside one band and read as a flat
// color). It scrolls along the diagonal at the trail-equivalent pace:
// one full slide every colorSize · 4 · count / movementSpeed seconds,
// one full slide every colorSize · count / movementSpeed seconds,
// so both knobs keep their trail timing semantics.
float colorSize = max(texelFetch(uEffect, ivec2(owner, rowBase + 2), 0).a, 0.001);
float movementSpeed = texelFetch(uEffect, ivec2(owner, rowBase + 3), 0).a;
float dn = (vLocalPos.x + vLocalPos.y) * 0.5; // icon diagonal, -0.5..0.5
float phase =
fract(dn - uTime * movementSpeed / (colorSize * 4.0 * float(count)));
fract(dn - uTime * movementSpeed / (colorSize * float(count)));
float f = phase * float(count);
int i = int(f) % count;
int j = (i + 1) % count;