Skip to content

feat: render transport-ship trail cosmetic as a gradient#4454

Merged
evanpelle merged 4 commits into
mainfrom
transport-ship-trail-rendering
Jun 30, 2026
Merged

feat: render transport-ship trail cosmetic as a gradient#4454
evanpelle merged 4 commits into
mainfrom
transport-ship-trail-rendering

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

What

Renders the transportShipTrail cosmetic 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

  • Per-player effect texture (RGBA32F, mirrors the palette texture) keyed by smallID, sampled by the trail fragment shader. Each row holds a gradient color; spare alpha channels carry the color count, colorSize, and movementSpeed.
  • Shader (trail.frag.glsl) cycles a flowing gradient through the color list: 1 color → flat, 2+ → animated bands scrolling along the trail. No effect (count 0) falls back to the territory color; alt-view keeps affiliation colors.
  • WebGLFrameBuilder resolves each player's catalog attributes (the in-game cosmetic is only { 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:

{ type: "gradient", colors: string[], colorSize: number, movementSpeed: number }
  • colors — solid = one color, rainbow = the spectrum, gradient = two or more.
  • colorSize — band width (tiles per color band; 1 is the default, ~4 tiles).
  • movementSpeed — scroll rate along the trail (tiles/sec; 0 = static).

Notes

  • Animation is render-only (local time), no simulation/determinism impact.
  • The catalog (cosmetics.json, served by the closed-source API) must ship effects in this { type: "gradient", colors, colorSize, movementSpeed } shape.
  • Band thickness (4.0 base in the shader) and the gradient frequency are visual constants picked without in-game verification — easy to tune.

Testing

  • tsc --noEmit, ESLint, Prettier, build-prod all clean.
  • Schema + Privilege test suites updated for the gradient shape (92 tests pass).
  • Not yet visually verified in a running game (effect selection is flare-gated).

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a11924f9-9dea-4236-8a81-20849b562024

📥 Commits

Reviewing files that changed from the base of the PR and between 610ddd4 and bb43be7.

📒 Files selected for processing (1)
  • src/core/CosmeticSchemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/CosmeticSchemas.ts

Walkthrough

transportShipTrail is narrowed to gradient attributes, then resolved per player into a palette buffer that is uploaded to a new GPU texture for trail rendering. The preview swatch now renders directly from the colors array.

Changes

TransportShipTrail Gradient Pipeline

Layer / File(s) Summary
Gradient schema and fixtures
src/core/CosmeticSchemas.ts, tests/CosmeticSchemas.test.ts, tests/Privilege.test.ts
transportShipTrail parsing now accepts only the gradient attribute shape, and tests plus fixtures use gradient-only effect data and catalog filtering cases.
Per-player effect resolution
src/client/WebGLFrameBuilder.ts
Per-player trail effects are resolved from the cached cosmetics catalog, packed into a float palette buffer, and uploaded once new entries are written.
Trail effect texture and shader binding
src/client/render/gl/ColorUtils.ts, src/client/render/gl/index.ts, src/client/render/gl/Renderer.ts, src/client/render/gl/MapRenderer.ts, src/client/render/gl/passes/TrailPass.ts
The renderer stack adds MAX_TRAIL_COLORS, allocates and uploads effectTex, forwards palette updates, binds the texture in TrailPass, and updates uTime during draw.
EffectPreview swatch rewrite
src/client/components/EffectPreview.ts
The swatch background is computed from the number of colors instead of switching on effect type.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • openfrontio/OpenFrontIO#3973: Shares the same WebGL trail rendering path, including Renderer.ts and TrailPass.ts, with closely related wiring changes.
  • openfrontio/OpenFrontIO#4418: Changes the same transportShipTrail schema and preview-swatch area, with matching gradient-oriented effect shape updates.

Poem

🌈 Old trail shapes drifted out of sight,
A gradient path now paints the light.
Palette rows and shader time,
Move together, line by line.
Trails now shimmer, neat and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: rendering the transport-ship trail cosmetic as a gradient.
Description check ✅ Passed The description matches the change set and explains the renderer, schema, and testing updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Cache reset can leave stale trail gradients alive.

clearCaches() says the next update() will fully rebuild renderer state, but the trail-effect path does not actually clear or re-upload fallback rows. A player with no transportShipTrail, colors: [], or only invalid colors never guarantees dirty = true, so the old entry can stay in effectPalette/effectTex and 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_COLORS rows 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 win

Share MAX_TRAIL_COLORS from one module.

src/client/render/gl/Renderer.ts and src/client/WebGLFrameBuilder.ts both hardcode 8 and 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

📥 Commits

Reviewing files that changed from the base of the PR and between f4b47ce and 9c7ca96.

⛔ Files ignored due to path filters (1)
  • src/client/render/gl/shaders/map-overlay/trail.frag.glsl is excluded by !**/*.glsl
📒 Files selected for processing (8)
  • src/client/WebGLFrameBuilder.ts
  • src/client/components/EffectPreview.ts
  • src/client/render/gl/MapRenderer.ts
  • src/client/render/gl/Renderer.ts
  • src/client/render/gl/passes/TrailPass.ts
  • src/core/CosmeticSchemas.ts
  • tests/CosmeticSchemas.test.ts
  • tests/Privilege.test.ts

Comment on lines +16 to +25
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};"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/core

Repository: 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 compact

Repository: openfrontio/OpenFrontIO

Length of output: 312


🏁 Script executed:

# Get proper outline of WebGLFrameBuilder.ts
ast-grep outline src/client/WebGLFrameBuilder.ts --view expanded

Repository: openfrontio/OpenFrontIO

Length of output: 1453


🏁 Script executed:

# Also search for writeEffectEntry specifically to understand color validation
rg -A 20 "writeEffectEntry" src/client/WebGLFrameBuilder.ts

Repository: 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.ts

Repository: 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 1

Repository: openfrontio/OpenFrontIO

Length of output: 189


🏁 Script executed:

# Check imports in files that use colord to understand pattern
head -20 src/client/components/PatternPreview.ts

Repository: 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.ts

Repository: 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 -20

Repository: 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()
PY

Repository: openfrontio/OpenFrontIO

Length of output: 446


🏁 Script executed:

# Verify if the schema allows unvalidated color strings
rg -A 10 "TransportShipTrailAttributesSchema" src/core/CosmeticSchemas.ts

Repository: 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 -20

Repository: 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.ts

Repository: 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")
PY

Repository: 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.ts

Repository: 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")
PY

Repository: 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 -5

Repository: 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.ts

Repository: 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.

Suggested change
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.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jun 30, 2026
…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>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/client/render/gl/utils/ColorUtils.ts (1)

20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make this the only MAX_TRAIL_COLORS definition.

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 = 8 declarations in src/client/render/gl/Renderer.ts and src/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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c7ca96 and 610ddd4.

⛔ Files ignored due to path filters (1)
  • src/client/render/gl/shaders/map-overlay/trail.frag.glsl is excluded by !**/*.glsl
📒 Files selected for processing (6)
  • src/client/WebGLFrameBuilder.ts
  • src/client/render/gl/Renderer.ts
  • src/client/render/gl/index.ts
  • src/client/render/gl/utils/ColorUtils.ts
  • src/core/CosmeticSchemas.ts
  • tests/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>
@evanpelle evanpelle added this to the v33 milestone Jun 30, 2026
@evanpelle evanpelle merged commit 7c151e7 into main Jun 30, 2026
13 of 16 checks passed
@evanpelle evanpelle deleted the transport-ship-trail-rendering branch June 30, 2026 03:28
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management Jun 30, 2026
evanpelle added a commit that referenced this pull request Jun 30, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant