feat: render transport-ship trail cosmetic as a gradient#4454
Conversation
Transport ships already left a trail, but it was always drawn in the
player's territory color. This wires the selected `transportShipTrail`
cosmetic through to the renderer so the trail shows the player's chosen
gradient.
Rendering: a per-player effect texture (mirroring the palette) is keyed by
smallID and sampled by the trail fragment shader. The shader cycles a
flowing gradient through the player's color list (1 color = flat, 2+ =
animated bands); players with no effect fall back to the territory color,
and alt-view keeps affiliation colors. WebGLFrameBuilder resolves each
player's catalog attributes (the in-game cosmetic is just {name,
effectType}) and encodes them; resolution retries until the catalog loads
and drops unparseable colors so bad data degrades gracefully.
Schema: collapses the trail attributes to a single gradient shape —
`{ type: "gradient", colors, colorSize, movementSpeed }`. solid/rainbow are
expressed as color lists; colorSize sets band width and movementSpeed the
scroll rate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough
ChangesTransportShipTrail Gradient Pipeline
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/client/WebGLFrameBuilder.ts (1)
72-79: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winCache reset can leave stale trail gradients alive.
clearCaches()says the nextupdate()will fully rebuild renderer state, but the trail-effect path does not actually clear or re-upload fallback rows. A player with notransportShipTrail,colors: [], or only invalid colors never guaranteesdirty = true, so the old entry can stay ineffectPalette/effectTexand keep rendering the previous gradient instead of falling back to territory color.Suggested fix
clearCaches(): void { this.knownSmallIDs.clear(); this.effectResolved.clear(); + this.effectPalette.fill(0); this.lastSpawnTile.clear(); this.localPlayerSmallID = 0; this.skinsInitialized = false; }const trailEffect = p.cosmetics.effects?.["transportShipTrail"]; - if (!trailEffect) continue; // No effect — nothing to write or upload. + if (!trailEffect) { + // Zero this player's slot so the shader falls back to territory color. + dirty = this.clearEffectEntry(smallID) || dirty; + continue; + } const effect = findEffect( catalog, "transportShipTrail", trailEffect.name, ); if (effect?.effectType !== "transportShipTrail") continue; - if (this.writeEffectEntry(smallID, effect.attributes)) dirty = true; + dirty = this.writeEffectEntry(smallID, effect.attributes) || dirty;Add a small helper that zeros one player's
MAX_TRAIL_COLORSrows and returns whether anything changed, so fallback state is uploaded too.Also applies to: 288-308, 320-343
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/WebGLFrameBuilder.ts` around lines 72 - 79, `clearCaches()` in `WebGLFrameBuilder` leaves stale trail palette data behind because the trail-effect update path can skip setting `dirty` when `transportShipTrail`, `colors`, or color validation fall back to territory state. Update the trail rendering flow by adding a small helper used from the relevant effect/texture update code to zero a player’s `MAX_TRAIL_COLORS` rows and report whether anything changed, then make `update()`/the trail upload path mark `dirty` and re-upload the fallback rows when needed so `effectPalette` and `effectTex` are refreshed instead of reusing the previous gradient.
🧹 Nitpick comments (1)
src/client/render/gl/Renderer.ts (1)
95-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare
MAX_TRAIL_COLORSfrom one module.
src/client/render/gl/Renderer.tsandsrc/client/WebGLFrameBuilder.tsboth hardcode8and rely on comments to stay in sync. If one side drifts, the upload layout and shader sampling will silently disagree and every trail effect row will be read wrong. Export one shared constant and import it in both places.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/render/gl/Renderer.ts` around lines 95 - 98, `Renderer` and `WebGLFrameBuilder` both hardcode the trail gradient row count, so move `MAX_TRAIL_COLORS` into a single shared module and import it in both `Renderer.ts` and `WebGLFrameBuilder.ts`. Update the existing uses in `Renderer` and the frame-building/upload logic so they reference the shared constant instead of duplicating the value, keeping the texture layout and shader sampling aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client/components/EffectPreview.ts`:
- Around line 16-25: The preview background is built from raw attributes.colors
in EffectPreview, which can diverge from WebGLFrameBuilder.writeEffectEntry()
because invalid entries and colors beyond MAX_TRAIL_COLORS are not filtered the
same way. Update the EffectPreview color handling to apply the same validation
and truncation rules before constructing the CSS background, then build the
gradient from the normalized list so the preview matches the renderer.
---
Outside diff comments:
In `@src/client/WebGLFrameBuilder.ts`:
- Around line 72-79: `clearCaches()` in `WebGLFrameBuilder` leaves stale trail
palette data behind because the trail-effect update path can skip setting
`dirty` when `transportShipTrail`, `colors`, or color validation fall back to
territory state. Update the trail rendering flow by adding a small helper used
from the relevant effect/texture update code to zero a player’s
`MAX_TRAIL_COLORS` rows and report whether anything changed, then make
`update()`/the trail upload path mark `dirty` and re-upload the fallback rows
when needed so `effectPalette` and `effectTex` are refreshed instead of reusing
the previous gradient.
---
Nitpick comments:
In `@src/client/render/gl/Renderer.ts`:
- Around line 95-98: `Renderer` and `WebGLFrameBuilder` both hardcode the trail
gradient row count, so move `MAX_TRAIL_COLORS` into a single shared module and
import it in both `Renderer.ts` and `WebGLFrameBuilder.ts`. Update the existing
uses in `Renderer` and the frame-building/upload logic so they reference the
shared constant instead of duplicating the value, keeping the texture layout and
shader sampling aligned.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5547b6fb-7bf3-4e57-9d89-4b07084de042
⛔ Files ignored due to path filters (1)
src/client/render/gl/shaders/map-overlay/trail.frag.glslis excluded by!**/*.glsl
📒 Files selected for processing (8)
src/client/WebGLFrameBuilder.tssrc/client/components/EffectPreview.tssrc/client/render/gl/MapRenderer.tssrc/client/render/gl/Renderer.tssrc/client/render/gl/passes/TrailPass.tssrc/core/CosmeticSchemas.tstests/CosmeticSchemas.test.tstests/Privilege.test.ts
| const colors = attributes.colors; | ||
| const background = | ||
| colors.length === 0 | ||
| ? EMPTY_BG | ||
| : colors.length === 1 | ||
| ? colors[0] | ||
| : `linear-gradient(90deg,${colors.join(",")})`; | ||
| return html`<div | ||
| class="w-full h-full rounded-md" | ||
| style="background:${background};" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether colord and MAX_TRAIL_COLORS are already importable from nearby client code.
rg -n "from \"colord\"|from 'colord'|MAX_TRAIL_COLORS" src/client src/coreRepository: openfrontio/OpenFrontIO
Length of output: 1661
🏁 Script executed:
# First, locate and read EffectPreview.ts to see current state and imports
fd "EffectPreview.ts" src/client --exec cat -n {} \;Repository: openfrontio/OpenFrontIO
Length of output: 1266
🏁 Script executed:
# Then examine WebGLFrameBuilder.ts to see the writeEffectEntry implementation
ast-grep outline src/client/WebGLFrameBuilder.ts --view compactRepository: openfrontio/OpenFrontIO
Length of output: 312
🏁 Script executed:
# Get proper outline of WebGLFrameBuilder.ts
ast-grep outline src/client/WebGLFrameBuilder.ts --view expandedRepository: openfrontio/OpenFrontIO
Length of output: 1453
🏁 Script executed:
# Also search for writeEffectEntry specifically to understand color validation
rg -A 20 "writeEffectEntry" src/client/WebGLFrameBuilder.tsRepository: openfrontio/OpenFrontIO
Length of output: 1856
🏁 Script executed:
# Check if MAX_TRAIL_COLORS is exported from WebGLFrameBuilder.ts
rg -B 2 -A 2 "const MAX_TRAIL_COLORS" src/client/WebGLFrameBuilder.tsRepository: openfrontio/OpenFrontIO
Length of output: 349
🏁 Script executed:
# Check if there are other imports of colord in EffectPreview or nearby files
rg "colord" src/client/components/ src/client/hud/Repository: openfrontio/OpenFrontIO
Length of output: 372
🏁 Script executed:
# Check whether MAX_TRAIL_COLORS or similar color validation is exported elsewhere
rg "export.*MAX_TRAIL_COLORS|export.*colord" src/client/Repository: openfrontio/OpenFrontIO
Length of output: 161
🏁 Script executed:
# Check where MAX_TRAIL_COLORS is defined and if it's exported
rg "MAX_TRAIL_COLORS" src/client/ --type ts --type tsx -B 1 -A 1Repository: openfrontio/OpenFrontIO
Length of output: 189
🏁 Script executed:
# Check imports in files that use colord to understand pattern
head -20 src/client/components/PatternPreview.tsRepository: openfrontio/OpenFrontIO
Length of output: 871
🏁 Script executed:
# Check if MAX_TRAIL_COLORS is exported from WebGLFrameBuilder
rg "export.*MAX_TRAIL_COLORS" src/client/Repository: openfrontio/OpenFrontIO
Length of output: 161
🏁 Script executed:
# Check the CosmeticSchemas to see if colors are validated there
head -50 src/core/CosmeticSchemas.tsRepository: openfrontio/OpenFrontIO
Length of output: 1799
🏁 Script executed:
# Look at how EffectPreview is used - is it showing a preview for user input or catalog data?
rg -B 5 -A 5 "renderTransportShipTrailSwatch" src/client/Repository: openfrontio/OpenFrontIO
Length of output: 4151
🏁 Script executed:
# Check if there's a shared constants file or utilities where MAX_TRAIL_COLORS should be
find src/client -name "*[Cc]onst*" -o -name "*[Cc]onstant*" | head -20Repository: openfrontio/OpenFrontIO
Length of output: 161
🏁 Script executed:
# Check what the actual CSS gradient behavior is with invalid colors
python3 - <<'PY'
# Test how CSS gradient handles invalid colors
test_cases = [
"linear-gradient(90deg,`#red`,`#blue`)", # Invalid first color
"linear-gradient(90deg,`#ff0000`,invalid,`#0000ff`)", # Invalid middle color
"linear-gradient(90deg,`#ff0000`,`#00ff00`)", # Both valid
]
for gradient in test_cases:
# CSS ignores invalid colors; the gradient still renders with valid ones
print(f"Gradient: {gradient}")
print(f" -> CSS will render with only valid colors")
print()
PYRepository: openfrontio/OpenFrontIO
Length of output: 446
🏁 Script executed:
# Verify if the schema allows unvalidated color strings
rg -A 10 "TransportShipTrailAttributesSchema" src/core/CosmeticSchemas.tsRepository: openfrontio/OpenFrontIO
Length of output: 1199
🏁 Script executed:
# Check if MAX_TRAIL_COLORS should be exported - look at similar constants
rg -B 3 "export const" src/client/WebGLFrameBuilder.ts | head -20Repository: openfrontio/OpenFrontIO
Length of output: 161
🏁 Script executed:
# Check whether EffectPreview is used for user input or catalog display by examining EffectsInput more carefully
sed -n '1,100p' src/client/EffectsInput.tsRepository: openfrontio/OpenFrontIO
Length of output: 3527
🏁 Script executed:
# Verify the actual CSS gradient behavior with test
python3 - <<'PY'
# CSS gradient actual behavior with invalid colors
# According to CSS spec, invalid color values in gradient are simply skipped
print("CSS linear-gradient behavior with invalid colors:")
print("- Invalid colors are silently ignored")
print("- linear-gradient(90deg, invalid, `#ff0000`) renders as if only `#ff0000` is present")
print("- No CSS error or broken gradient occurs")
print()
print("However:")
print("- WebGLFrameBuilder expects validated colors only")
print("- EffectPreview uses raw colors from schema (which allows z.string())")
print("- Divergence: EffectPreview may show more colors than shader uses")
PYRepository: openfrontio/OpenFrontIO
Length of output: 567
🏁 Script executed:
# Check the comment around MAX_TRAIL_COLORS to understand design intent
sed -n '17,25p' src/client/WebGLFrameBuilder.tsRepository: openfrontio/OpenFrontIO
Length of output: 519
🏁 Script executed:
# Check if there's a shared utility module for constants
ls -la src/client/*.ts | grep -i "const\|util\|helper"Repository: openfrontio/OpenFrontIO
Length of output: 231
🏁 Script executed:
# Verify: does the gradient actually break with invalid colors in a real browser test?
python3 - <<'PY'
# CSS spec compliance check
print("CSS linear-gradient spec behavior:")
print("Invalid color values are simply dropped/ignored")
print("Example: linear-gradient(90deg, red, invalid, blue)")
print(" -> Renders as: linear-gradient(90deg, red, blue)")
print(" -> NO error, NO broken gradient")
print()
print("Conclusion: The 'breaks the whole CSS gradient' claim is incorrect")
print("The real issue is truncation at MAX_TRAIL_COLORS (8)")
print("If colors array has > 8 items, preview shows all but shader uses only 8")
PYRepository: openfrontio/OpenFrontIO
Length of output: 574
🏁 Script executed:
# Final check: what would need to be imported to implement the suggested fix?
cat src/client/WebGLFrameBuilder.ts | head -5Repository: openfrontio/OpenFrontIO
Length of output: 305
🏁 Script executed:
# Verify whether MAX_TRAIL_COLORS can/should be exported
ast-grep run --pattern 'const MAX_TRAIL_COLORS' src/client/WebGLFrameBuilder.tsRepository: openfrontio/OpenFrontIO
Length of output: 224
Normalize colors before building the CSS background and respect the MAX_TRAIL_COLORS limit.
Line 16 uses raw attributes.colors, but the renderer validates and filters invalid colors in WebGLFrameBuilder.writeEffectEntry() before use. The preview can diverge in two ways: (1) if the array exceeds 8 colors, the preview shows all of them but the shader renders only 8; (2) invalid color strings are silently ignored by CSS, but the shader filters them. This divergence between preview and in-game rendering can confuse users.
Filter colors with the same validation and truncation rules as the renderer:
+import { colord } from "colord";
+
const EMPTY_BG = "`#444`";
+const MAX_TRAIL_COLORS = 8;
export function renderTransportShipTrailSwatch(
attributes: TransportShipTrailAttributes,
): TemplateResult {
- const colors = attributes.colors;
+ const colors = attributes.colors
+ .map((color) => colord(color))
+ .filter((color) => color.isValid())
+ .slice(0, MAX_TRAIL_COLORS)
+ .map((color) => color.toHex());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const colors = attributes.colors; | |
| const background = | |
| colors.length === 0 | |
| ? EMPTY_BG | |
| : colors.length === 1 | |
| ? colors[0] | |
| : `linear-gradient(90deg,${colors.join(",")})`; | |
| return html`<div | |
| class="w-full h-full rounded-md" | |
| style="background:${background};" | |
| import { colord } from "colord"; | |
| const EMPTY_BG = "`#444`"; | |
| const MAX_TRAIL_COLORS = 8; | |
| export function renderTransportShipTrailSwatch( | |
| attributes: TransportShipTrailAttributes, | |
| ): TemplateResult { | |
| const colors = attributes.colors | |
| .map((color) => colord(color)) | |
| .filter((color) => color.isValid()) | |
| .slice(0, MAX_TRAIL_COLORS) | |
| .map((color) => color.toHex()); | |
| const background = | |
| colors.length === 0 | |
| ? EMPTY_BG | |
| : colors.length === 1 | |
| ? colors[0] | |
| : `linear-gradient(90deg,${colors.join(",")})`; | |
| return html`<div | |
| class="w-full h-full rounded-md" | |
| style="background:${background};" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/components/EffectPreview.ts` around lines 16 - 25, The preview
background is built from raw attributes.colors in EffectPreview, which can
diverge from WebGLFrameBuilder.writeEffectEntry() because invalid entries and
colors beyond MAX_TRAIL_COLORS are not filtered the same way. Update the
EffectPreview color handling to apply the same validation and truncation rules
before constructing the CSS background, then build the gradient from the
normalized list so the preview matches the renderer.
…ader CRLF - CosmeticSchemas: make effects[effectType] a lenient record so a newer effect entry the client can't parse is dropped instead of failing the whole catalog parse (patterns/flags/skins and other effects still load). New effectType keys were already tolerated by z.object stripping; this extends that one level down. - Extract getMaxTrailColors() into ColorUtils (next to getPaletteSize) and use it in Renderer + WebGLFrameBuilder instead of two hand-synced MAX_TRAIL_COLORS=8 literals. - Restore trail.frag.glsl to CRLF (its base line ending) so the diff shows only the functional uEffect/uTime additions instead of a full-file rewrite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
The getMaxTrailColors() wrapper was pointless indirection — export the const directly and import it where needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/client/render/gl/utils/ColorUtils.ts (1)
20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake this the only
MAX_TRAIL_COLORSdefinition.The new comment says this constant keeps CPU and GPU code in sync, but the provided graph context still shows separate
export const MAX_TRAIL_COLORS = 8declarations insrc/client/render/gl/Renderer.tsandsrc/client/WebGLFrameBuilder.ts. Please import this shared constant there too, or the contract can still drift on the next edit.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/render/gl/utils/ColorUtils.ts` around lines 20 - 25, `MAX_TRAIL_COLORS` is still duplicated across the renderer code, so make `ColorUtils.ts` the single source of truth and remove the local constant definitions in `Renderer` and `WebGLFrameBuilder`. Update the affected code paths to import and use the shared `MAX_TRAIL_COLORS` export from `ColorUtils` instead of redeclaring it. Verify any trail texture sizing/allocation logic in `Renderer` and `WebGLFrameBuilder` now references the shared constant so CPU/GPU limits stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/client/render/gl/utils/ColorUtils.ts`:
- Around line 20-25: `MAX_TRAIL_COLORS` is still duplicated across the renderer
code, so make `ColorUtils.ts` the single source of truth and remove the local
constant definitions in `Renderer` and `WebGLFrameBuilder`. Update the affected
code paths to import and use the shared `MAX_TRAIL_COLORS` export from
`ColorUtils` instead of redeclaring it. Verify any trail texture
sizing/allocation logic in `Renderer` and `WebGLFrameBuilder` now references the
shared constant so CPU/GPU limits stay aligned.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 138e3447-5ca9-4222-b4fd-09cbae5daedf
⛔ Files ignored due to path filters (1)
src/client/render/gl/shaders/map-overlay/trail.frag.glslis excluded by!**/*.glsl
📒 Files selected for processing (6)
src/client/WebGLFrameBuilder.tssrc/client/render/gl/Renderer.tssrc/client/render/gl/index.tssrc/client/render/gl/utils/ColorUtils.tssrc/core/CosmeticSchemas.tstests/CosmeticSchemas.test.ts
✅ Files skipped from review due to trivial changes (1)
- src/client/render/gl/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/client/WebGLFrameBuilder.ts
- src/client/render/gl/Renderer.ts
Lift the drop-invalid-entries loop out of the CosmeticsSchema literal into a named, reusable lenientRecord() helper so the schema reads declaratively. Same forward-compat behavior; reusable for future effectTypes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4455) ## What Adds a second transport-ship trail style, **transition**, alongside the existing **gradient** (#4454). Where `gradient` paints a spatial band of colors along the trail, `transition` makes the whole trail one color at a time, cross-fading through the color list over time. ```json "attributes": { "type": "transition", "colors": ["#002aff", "#4805ff"], "frequency": 1 } ``` ## How - **Schema** ([CosmeticSchemas.ts](src/core/CosmeticSchemas.ts)) — `TransportShipTrailAttributesSchema` is now a discriminated union on `type`: - `gradient`: `{ colors, colorSize, movementSpeed }` - `transition`: `{ colors, frequency }` — `frequency` = color changes per second. - **Renderer** — the effect texture gained a `styleId` discriminator (row 1's alpha; 0 = gradient, 1 = transition), with the gradient scalars shifted down a row. - [WebGLFrameBuilder.ts](src/client/WebGLFrameBuilder.ts) encodes `styleId` + the style's scalars. - [trail.frag.glsl](src/client/render/gl/shaders/map-overlay/trail.frag.glsl): for `transition`, the trail color is `mix(colors[i], colors[i+1], fract(t))` with `i = floor(uTime · frequency) mod count` — one color step every `1/frequency` seconds. - **Store/picker swatch** ([EffectPreview.ts](src/client/components/EffectPreview.ts)) — the swatch is now a `<trail-swatch>` Lit element. For `transition` it cross-fades through the colors via the Web Animations API, timed to match the shader (each step `1/frequency` s); gradient/solid stay static. The animation is canceled on disconnect. ## Notes - Animation is render-only (local time) — no simulation/determinism impact. - `gradient` swatches remain static (they don't scroll like the in-game trail) — easy to add later if wanted. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` all clean. - Schema tests cover the transition member (parse + required `frequency`); 95 tests pass. - The animated swatch is visual-only (no automated coverage) and not yet verified in a running store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Renders the
transportShipTrailcosmetic effect in-game. Transport ships already left a trail, but it was always drawn in the player's territory color — this wires the selected effect through to the renderer so the trail shows the player's chosen gradient.How
RGBA32F, mirrors the palette texture) keyed bysmallID, sampled by the trail fragment shader. Each row holds a gradient color; spare alpha channels carry the color count,colorSize, andmovementSpeed.{ name, effectType }; the style/colors live in the catalog) and encodes them. Resolution is decoupled from the first-seen palette path so it retries until the catalog loads, and unparseable colors are dropped so bad catalog data degrades to the territory color rather than rendering black.Schema
Collapses the trail attributes to a single gradient shape:
colors— solid = one color, rainbow = the spectrum, gradient = two or more.colorSize— band width (tiles per color band;1is the default, ~4 tiles).movementSpeed— scroll rate along the trail (tiles/sec;0= static).Notes
cosmetics.json, served by the closed-source API) must ship effects in this{ type: "gradient", colors, colorSize, movementSpeed }shape.4.0base in the shader) and the gradient frequency are visual constants picked without in-game verification — easy to tune.Testing
tsc --noEmit, ESLint, Prettier,build-prodall clean.🤖 Generated with Claude Code