-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: transport-ship trail transition effect + animated store swatch #4455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,78 @@ | ||
| import { html, TemplateResult } from "lit"; | ||
| import { html, LitElement, TemplateResult } from "lit"; | ||
| import { customElement, property } from "lit/decorators.js"; | ||
| import { TransportShipTrailAttributes } from "../../core/CosmeticSchemas"; | ||
|
|
||
| // Neutral fallback when a trail has no usable colors. | ||
| const EMPTY_BG = "#444"; | ||
|
|
||
| /** | ||
| * Render a swatch preview of a transport-ship-trail's attributes, filling its | ||
| * container. A trail is a list of colors: one color renders as a flat swatch, | ||
| * two or more as a left-to-right gradient (a multi-color list reads as a | ||
| * rainbow). An empty list renders a neutral swatch. | ||
| * Swatch preview of a transport-ship-trail effect, filling its container. | ||
| * | ||
| * - gradient / single color: a static swatch (flat color or left-to-right | ||
| * gradient — a multi-color list reads as a rainbow). | ||
| * - transition: cross-fades through the colors over time, mirroring the trail | ||
| * (each color step lasts 1/frequency seconds, matching the shader). | ||
| */ | ||
| export function renderTransportShipTrailSwatch( | ||
| attributes: TransportShipTrailAttributes, | ||
| ): TemplateResult { | ||
| 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};" | ||
| ></div>`; | ||
| @customElement("trail-swatch") | ||
| export class TrailSwatch extends LitElement { | ||
| // Named `trail` (not `attributes`) to avoid clashing with Element.attributes. | ||
| @property({ attribute: false }) | ||
| trail: TransportShipTrailAttributes | null = null; | ||
|
|
||
| private animation: Animation | null = null; | ||
|
|
||
| // Light DOM so the shared Tailwind classes apply. | ||
| createRenderRoot(): HTMLElement { | ||
| return this; | ||
| } | ||
|
|
||
| render(): TemplateResult { | ||
| const colors = this.trail?.colors ?? []; | ||
| let background: string; | ||
| if (colors.length === 0) { | ||
| background = EMPTY_BG; | ||
| } else if (this.trail?.type === "transition") { | ||
| // The animation (see updated) cross-fades from here through the list. | ||
| background = colors[0]; | ||
| } else if (colors.length === 1) { | ||
| background = colors[0]; | ||
| } else { | ||
| background = `linear-gradient(90deg,${colors.join(",")})`; | ||
| } | ||
| return html`<div | ||
| class="w-full h-full rounded-md" | ||
| style="background:${background};" | ||
| ></div>`; | ||
| } | ||
|
|
||
| updated(changed: Map<string, unknown>): void { | ||
| if (!changed.has("trail")) return; | ||
| this.animation?.cancel(); | ||
| this.animation = null; | ||
|
|
||
| const attrs = this.trail; | ||
| if (attrs?.type !== "transition") return; | ||
| const colors = attrs.colors; | ||
| if (colors.length < 2 || attrs.frequency <= 0) return; | ||
|
|
||
| const fill = this.querySelector<HTMLElement>("div"); | ||
| if (!fill) return; | ||
|
|
||
| // Cross-fade color0 → color1 → … → color0; each step lasts 1/frequency s, | ||
| // matching the shader's i = floor(uTime * frequency) mod count. | ||
| const keyframes = [...colors, colors[0]].map((c) => ({ | ||
| backgroundColor: c, | ||
| })); | ||
| this.animation = fill.animate(keyframes, { | ||
| duration: (colors.length / attrs.frequency) * 1000, | ||
| iterations: Infinity, | ||
| easing: "linear", | ||
| }); | ||
| } | ||
|
|
||
| disconnectedCallback(): void { | ||
| super.disconnectedCallback(); | ||
| this.animation?.cancel(); | ||
| this.animation = null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -102,20 +102,28 @@ export const SkinSchema = CosmeticSchema.extend({ | |||||||||||||||||
| export const EFFECT_TYPES = ["transportShipTrail"] as const; | ||||||||||||||||||
| export const EffectTypeSchema = z.enum(EFFECT_TYPES); | ||||||||||||||||||
|
|
||||||||||||||||||
| // A boat trail is a gradient of one or more colors, cycled along the trail. The | ||||||||||||||||||
| // old solid/rainbow styles are just color lists now: solid = a single color, | ||||||||||||||||||
| // rainbow = the spectrum, gradient = two or more. The server only ships this | ||||||||||||||||||
| // "gradient" shape. Colors are unvalidated strings here; the renderer drops any | ||||||||||||||||||
| // it can't parse (and an empty list falls back to the player's territory color). | ||||||||||||||||||
| // `colorSize` is how wide each color band is, in tiles (larger = bigger bands); | ||||||||||||||||||
| // `movementSpeed` is how fast the bands scroll along the trail, in tiles per | ||||||||||||||||||
| // second (0 = static). | ||||||||||||||||||
| export const TransportShipTrailAttributesSchema = z.object({ | ||||||||||||||||||
| type: z.literal("gradient"), | ||||||||||||||||||
| colors: z.array(z.string()), | ||||||||||||||||||
| colorSize: z.number(), | ||||||||||||||||||
| movementSpeed: z.number(), | ||||||||||||||||||
| }); | ||||||||||||||||||
| // A boat trail effect, discriminated on `type`: | ||||||||||||||||||
| // - "gradient": the colors form a spatial gradient banded along the trail. | ||||||||||||||||||
| // `colorSize` = band width in tiles (larger = bigger bands); `movementSpeed` | ||||||||||||||||||
| // = how fast the bands scroll, in tiles/sec (0 = static). | ||||||||||||||||||
| // - "transition": the whole trail is one color at a time, cross-fading through | ||||||||||||||||||
| // the color list over time. `frequency` = color changes per second. | ||||||||||||||||||
| // solid = a single-color list; rainbow = the spectrum as a gradient. Colors are | ||||||||||||||||||
| // unvalidated strings here; the renderer drops any it can't parse (and an empty | ||||||||||||||||||
| // list falls back to the player's territory color). | ||||||||||||||||||
| export const TransportShipTrailAttributesSchema = z.discriminatedUnion("type", [ | ||||||||||||||||||
| z.object({ | ||||||||||||||||||
| type: z.literal("gradient"), | ||||||||||||||||||
| colors: z.array(z.string()), | ||||||||||||||||||
| colorSize: z.number(), | ||||||||||||||||||
| movementSpeed: z.number(), | ||||||||||||||||||
| }), | ||||||||||||||||||
| z.object({ | ||||||||||||||||||
| type: z.literal("transition"), | ||||||||||||||||||
| colors: z.array(z.string()), | ||||||||||||||||||
| frequency: z.number(), | ||||||||||||||||||
|
Comment on lines
+121
to
+124
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Reject negative transition frequencies. Line 124 currently accepts negative values. 💡 Proposed fix z.object({
type: z.literal("transition"),
colors: z.array(z.string()),
- frequency: z.number(),
+ frequency: z.number().nonnegative(),
}),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| }), | ||||||||||||||||||
| ]); | ||||||||||||||||||
|
|
||||||||||||||||||
| const TransportShipTrailEffectSchema = CosmeticSchema.extend({ | ||||||||||||||||||
| effectType: z.literal("transportShipTrail"), | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Filter preview colors the same way the renderer does.
TransportShipTrailAttributesSchemaallows raw strings, andWebGLFrameBuilder.writeEffectEntry()drops any color it can't parse. Here Lines 30-40 and Lines 55-66 use those raw values directly in CSS/WAAPI, so one bad entry can make the swatch blank or disagree with the actual trail. Normalize/filter first, then fall back toEMPTY_BGwhen nothing valid remains.💡 Proposed fix
Also applies to: 53-69
🤖 Prompt for AI Agents