mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 04:23:24 +00:00
## Summary Adds a **spiral** `nukeTrail` effect type — a 3D vortex of glowing helix strands projected onto the map, trailing behind nukes whose owner has the cosmetic equipped. Catalog attributes (`spiral_tail` shape): | attribute | meaning | | --- | --- | | `colors` | palette, wrapped once around the vortex circumference | | `radius` | helix amplitude in tiles | | `strands` | helix strand count (renderer clamps to 8) | | `rotationSpeed` | vortex spin, radians/sec | ## How it works The per-tile trail texture holds persistent state; the vortex is a transient animation that follows a path — so it renders as **ribbon geometry**, not tile stamps. **Path recording** (`SpiralTrails`): each live spiral-owner nuke gets an append-only centerline polyline — ~2 samples per tile of travel carrying position, a smoothed perpendicular (blended across tick segments so curved paths don't kink), and cumulative distance. Params are pushed once per player by `WebGLFrameBuilder` when the cosmetics catalog resolves; a ribbon is dropped the moment its unit disappears, matching stamped-trail cleanup. **Rendering** (`SpiralRibbonPass`): one triangle-strip VBO per nuke (2 verts per sample, streamed append-only via `bufferSubData`, grown by doubling); the vertex shader swings each sample sideways by the helix offset and evaluates the head-cone convergence as a function of `uHeadDist − d`, so uploaded vertices are immutable — the cone feeding the strands into the missile needs no rewriting as the nuke flies. One draw per strand reuses the same strip with a different phase offset (`uPhase0`). The glow look is a bloom-style split: - **halo** — wide quadratic falloff, rendered premultiplied into a quarter-resolution buffer (`mapOverlay.spiralResolutionScale = 0.25`, ~16× cheaper fragments) and composited **additively** over the scene, so it reads as emitted light and the bilinear upsample keeps it soft; - **core** — sharp full-resolution ribbons on top, with a white-hot center on segments facing the viewer (neon-tube look). Shading spins the helix angle with time: a `cos` depth cue brightens facing segments and darkens receding ones, and the palette cross-fades around the circumference. The spiral nuke still stamps its plain centerline through the unchanged `TrailManager` — `trail.frag` styleId 2 draws it flat in the first color as the missile's spine, so alt view, death cleanup, and trail overlap behave identically to non-cosmetic nukes. Ribbons draw above the plain trails, below the missiles, and are skipped in alt view. **Perf**: both ribbon stages are skipped entirely (CPU-side, before any GL work) while no spiral nuke is in flight — games without the cosmetic, and frames without a spiral nuke, pay nothing new. Vertex uploads stream only newly appended samples; the halo's fragment cost is capped by the quarter-res buffer. MIRV warheads are explicitly excluded from ribbons (one MIRV splits into 350 of them). **Store preview**: `TrailSwatch` mirrors the bloom split in SVG — a screen-blended blurred halo, a crisp colored core, and a white-hot center line — with a phase-offset per-strand fade matching the in-game depth-shaded spin. Note: requires the `spiral_tail` catalog entry on the API side to be purchasable/selectable; without it nothing changes visually and the schema tolerates its absence. ## Testing - New `tests/SpiralTrails.test.ts`: ribbon gating by owner/unit type, sample spacing + monotonic distances, strand clamp + pitch-derived twist, death cleanup mutating the live array, params staying fixed for in-flight ribbons - New `tests/TrailManager.test.ts`: baseline stamping behavior (plain boat trails, nuke-bit stamping up to lastPos, death cleanup with overlap repaint) - `tests/CosmeticSchemas.test.ts`: spiral attribute parsing incl. the exact catalog shape, required-field/positivity rejections - Full suite green; `tsc` and lint clean - Verified visually: a standalone WebGL harness drove the real ribbon shaders (glow split, palette colors, spin, cone convergence), and a real solo game boots with zero GL errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
406 lines
8.6 KiB
JSON
406 lines
8.6 KiB
JSON
{
|
|
"passEnabled": {
|
|
"terrain": true,
|
|
"territory": true,
|
|
"borderCompute": true,
|
|
"borderStamp": true,
|
|
"trail": true,
|
|
"territoryPatterns": true,
|
|
"structure": true,
|
|
"unit": true,
|
|
"name": true,
|
|
"falloutBloom": true,
|
|
"falloutLight": true,
|
|
"railroad": true,
|
|
"fx": true,
|
|
"bar": true,
|
|
"nameDebug": false
|
|
},
|
|
"terrain": {
|
|
"oceanColor": "#4785b5",
|
|
"sandColor": "#CCCB9E",
|
|
"plainsColor": "#BEDC8A",
|
|
"highlandColor": "#DCCB9E",
|
|
"mountainColor": "#e6e6e6"
|
|
},
|
|
"falloutBloom": {
|
|
"broilSpeedCold": 0.0018,
|
|
"broilSpeedHot": 0,
|
|
"noiseFreq1": 0.059,
|
|
"noiseFreq2": 0.171,
|
|
"contrastLoCold": 0.52,
|
|
"contrastLoHot": 0,
|
|
"contrastHiCold": 1,
|
|
"contrastHiHot": 0,
|
|
"metaFreq": 0.02,
|
|
"intensityCold": 0.15,
|
|
"intensityHot": 1.8,
|
|
"metaInfluenceCold": 1,
|
|
"metaInfluenceHot": 0,
|
|
"opacityFadeEnd": 1,
|
|
"bloomR": 0.054901960784313725,
|
|
"bloomG": 0.8196078431372549,
|
|
"bloomB": 0,
|
|
"bloomCoverage": 1.1,
|
|
"heatDecayPerTick": 1,
|
|
"particleColorDarkR": 0.05,
|
|
"particleColorDarkG": 0.4,
|
|
"particleColorDarkB": 0.05,
|
|
"particleColorBrightR": 0.2,
|
|
"particleColorBrightG": 1,
|
|
"particleColorBrightB": 0.2,
|
|
"particleThresholdUnowned": 0.85,
|
|
"particleThresholdOwned": 0.875,
|
|
"particleFlickerSpeed": 0.2,
|
|
"particleStrength": 1,
|
|
"particleFreshScale": 0.2
|
|
},
|
|
"lighting": {
|
|
"ambient": 1,
|
|
"enabled": false,
|
|
"falloffPower": 2,
|
|
"falloutLightR": 0.15,
|
|
"falloutLightG": 0.95,
|
|
"falloutLightB": 0.15,
|
|
"falloutLightIntensity": 5.2,
|
|
"falloutLightThreshold": 0.01,
|
|
"emberLightR": 1,
|
|
"emberLightG": 0.4,
|
|
"emberLightB": 0.05,
|
|
"emberLightIntensity": 3,
|
|
"blurZoomDivisor": 4,
|
|
"lightRadiusMultiplier": 1
|
|
},
|
|
"mapOverlay": {
|
|
"trailAlpha": 0.588,
|
|
"spiralResolutionScale": 0.25,
|
|
"defenseCheckerDarken": 0.7,
|
|
"territoryDefenseDarken": 0.85,
|
|
"territorySaturation": 0.85,
|
|
"territoryAlpha": 0.588,
|
|
"coordinateGridOpacity": 0.5,
|
|
"staleNukeBase": 0,
|
|
"staleNukeVariation": 0.05,
|
|
"staleNukeAlpha": 1,
|
|
"staleNukeR": 0.05,
|
|
"staleNukeG": 0.55,
|
|
"staleNukeB": 0.07,
|
|
"highlightBrighten": 0.25,
|
|
"highlightFillBrighten": 0.15,
|
|
"highlightThicken": 1,
|
|
"defensePostRange": 30,
|
|
"embargoTintRatio": 0.35,
|
|
"friendlyTintRatio": 0.35,
|
|
"embargoTintR": 1,
|
|
"embargoTintG": 0,
|
|
"embargoTintB": 0,
|
|
"friendlyTintR": 0,
|
|
"friendlyTintG": 1,
|
|
"friendlyTintB": 0
|
|
},
|
|
"affiliation": {
|
|
"selfR": 0,
|
|
"selfG": 1,
|
|
"selfB": 0,
|
|
"allyR": 1,
|
|
"allyG": 1,
|
|
"allyB": 0,
|
|
"neutralR": 0.502,
|
|
"neutralG": 0.502,
|
|
"neutralB": 0.502,
|
|
"enemyR": 1,
|
|
"enemyG": 0,
|
|
"enemyB": 0
|
|
},
|
|
"railroad": {
|
|
"railMinZoom": 3,
|
|
"railFadeRange": 2,
|
|
"railDetailZoom": 6,
|
|
"railAlpha": 1,
|
|
"railThickness": 1
|
|
},
|
|
"structure": {
|
|
"iconSize": 60,
|
|
"dotsZoomThreshold": 1.2,
|
|
"dotScale": 0.3,
|
|
"iconScaleFactorZoomedOut": 3,
|
|
"iconGrowZoom": 7,
|
|
"shapes": {
|
|
"City": {
|
|
"scale": 1,
|
|
"iconFill": 0.85
|
|
},
|
|
"Port": {
|
|
"scale": 1.08,
|
|
"iconFill": 0.85
|
|
},
|
|
"Factory": {
|
|
"scale": 1.08,
|
|
"iconFill": 0.85
|
|
},
|
|
"Defense Post": {
|
|
"scale": 1,
|
|
"iconFill": 0.8
|
|
},
|
|
"SAM Launcher": {
|
|
"scale": 1.4,
|
|
"iconFill": 1
|
|
},
|
|
"Missile Silo": {
|
|
"scale": 1.55,
|
|
"iconFill": 0.85
|
|
}
|
|
},
|
|
"highlightOutlineWidth": 0.04,
|
|
"highlightDimAlpha": 0.3,
|
|
"fillDarken": 0.65,
|
|
"borderDarken": 0.1,
|
|
"iconAlpha": 1.0,
|
|
"iconR": 1.0,
|
|
"iconG": 1.0,
|
|
"iconB": 1.0,
|
|
"iconDarken": 0
|
|
},
|
|
"structureLevel": {
|
|
"scale": 1,
|
|
"outlineWidth": 8,
|
|
"offsetY": -0.4,
|
|
"classicFont": true
|
|
},
|
|
"bar": {
|
|
"healthBarW": 11,
|
|
"healthBarH": 3,
|
|
"healthBarOffsetY": -6,
|
|
"progressBarW": 14,
|
|
"progressBarH": 3,
|
|
"progressBarOffsetY": 6,
|
|
"borderWidth": 1,
|
|
"threshold1": 0.25,
|
|
"threshold2": 0.5,
|
|
"threshold3": 0.75,
|
|
"colorRedR": 0.91,
|
|
"colorRedG": 0.098,
|
|
"colorRedB": 0.098,
|
|
"colorOrangeR": 0.941,
|
|
"colorOrangeG": 0.478,
|
|
"colorOrangeB": 0.098,
|
|
"colorYellowR": 0.792,
|
|
"colorYellowG": 0.906,
|
|
"colorYellowB": 0.059,
|
|
"colorGreenR": 0.173,
|
|
"colorGreenG": 0.937,
|
|
"colorGreenB": 0.071,
|
|
"veterancyPipW": 4,
|
|
"veterancyPipH": 1,
|
|
"veterancyPipGap": 1,
|
|
"veterancyPipOffsetX": 1.5,
|
|
"veterancyPipOffsetY": 3.5,
|
|
"veterancyR": 1.0,
|
|
"veterancyG": 0.84,
|
|
"veterancyB": 0.0
|
|
},
|
|
"unit": {
|
|
"unitSize": 13,
|
|
"flickerSpeed": 0.3,
|
|
"angryR": 0.784,
|
|
"angryG": 0,
|
|
"angryB": 0,
|
|
"hBombGlowScale": 2.2,
|
|
"hBombGlowR": 1.0,
|
|
"hBombGlowG": 0.72,
|
|
"hBombGlowB": 0.15,
|
|
"hBombGlowStrength": 0.5,
|
|
"hBombGlowInner": 0.45,
|
|
"untargetableAlpha": 0.6
|
|
},
|
|
"name": {
|
|
"lerpSpeed": 10,
|
|
"cullThreshold": 0.008,
|
|
"nameScaleFactor": 0.4,
|
|
"nameScaleCap": 3,
|
|
"troopSizeMultiplier": 0.6,
|
|
"outlineWidth": 1.4,
|
|
"outlineR": 0.0,
|
|
"outlineG": 0.0,
|
|
"outlineB": 0.0,
|
|
"outlineUsePlayerColor": true,
|
|
"fillUsePlayerColor": false,
|
|
"nameShadeNation": 0.3,
|
|
"nameShadeBot": 0.4,
|
|
"emojiRowOffset": 1.4,
|
|
"statusRowOffset": 1.4,
|
|
"statusOutlineWidth": 6,
|
|
"hoverFadeAlpha": 0.5,
|
|
"hoverGlowWidth": 5,
|
|
"hoverGlowAlpha": 0.75
|
|
},
|
|
"fx": {
|
|
"shockwaveRingWidth": 0.04,
|
|
"attackRingScreenPx": 30,
|
|
"nukeShockwaveDurationMs": 1500,
|
|
"nukeShockwaveRadiusFactor": 1.5,
|
|
"samShockwaveDurationMs": 800,
|
|
"samShockwaveRadius": 40,
|
|
"debrisLifetimeMs": 6000,
|
|
"debrisFadeIn": 0.1,
|
|
"debrisFadeOut": 0.8,
|
|
"conquestLifetimeMs": 2500,
|
|
"conquestFadeIn": 0.1,
|
|
"conquestFadeOut": 0.6
|
|
},
|
|
"nukeTrajectory": {
|
|
"lineWidth": 2.5,
|
|
"outlineWidth": 1.5,
|
|
"dashTargetable": 8,
|
|
"gapTargetable": 4,
|
|
"dashUntargetable": 2,
|
|
"gapUntargetable": 6,
|
|
"lineR": 1,
|
|
"lineG": 1,
|
|
"lineB": 1,
|
|
"interceptR": 1,
|
|
"interceptG": 0.314,
|
|
"interceptB": 0.314,
|
|
"outlineR": 0.549,
|
|
"outlineG": 0.549,
|
|
"outlineB": 0.549,
|
|
"interceptOutlineR": 0.588,
|
|
"interceptOutlineG": 0.353,
|
|
"interceptOutlineB": 0.353,
|
|
"markerCircleRadius": 6,
|
|
"markerXRadius": 40
|
|
},
|
|
"nukeTelegraph": {
|
|
"strokeWidth": 1.5,
|
|
"dashLen": 12,
|
|
"gapLen": 6,
|
|
"rotationSpeed": 20,
|
|
"baseAlpha": 0.85,
|
|
"pulseAmplitude": 0.1,
|
|
"pulseSpeed": 3,
|
|
"fillAlphaOffset": 0.6,
|
|
"colorR": 1,
|
|
"colorG": 0,
|
|
"colorB": 0,
|
|
"selfColorR": 0,
|
|
"selfColorG": 1,
|
|
"selfColorB": 0,
|
|
"allyColorR": 1,
|
|
"allyColorG": 1,
|
|
"allyColorB": 0
|
|
},
|
|
"moveIndicator": {
|
|
"startRadius": 13,
|
|
"chevronSize": 5,
|
|
"lineWidth": 2,
|
|
"duration": 800,
|
|
"converge": 0.7
|
|
},
|
|
"samRadius": {
|
|
"strokeWidth": 1.5,
|
|
"dashLen": 12,
|
|
"gapLen": 6,
|
|
"rotationSpeed": 14,
|
|
"alpha": 0.8,
|
|
"outlineWidth": 0.4,
|
|
"outlineSoftness": 0.15
|
|
},
|
|
"bonusPopup": {
|
|
"scale": 6,
|
|
"lifetimeMs": 1500,
|
|
"riseSpeed": 3,
|
|
"yOffset": -3,
|
|
"outlineWidth": 2,
|
|
"colorR": 1,
|
|
"colorG": 1,
|
|
"colorB": 1,
|
|
"minScreenScale": 0.15,
|
|
"cullZoom": 0.3
|
|
},
|
|
"ghostCost": {
|
|
"screenScale": 18,
|
|
"screenYOffset": 25
|
|
},
|
|
"spawnOverlay": {
|
|
"highlightRadius": 9,
|
|
"highlightAlpha": 1.0,
|
|
"selfMinRad": 10,
|
|
"selfMaxRad": 30,
|
|
"mateMinRad": 5,
|
|
"mateMaxRad": 14,
|
|
"animSpeed": 0.008,
|
|
"gradientInnerEdge": 0.01,
|
|
"gradientSolidEnd": 0.1
|
|
},
|
|
"smallPlayerGlow": {
|
|
"color": [1.0, 0.12, 0.1],
|
|
"alpha": 0.9,
|
|
"pulseSpeed": 0.003
|
|
},
|
|
"altView": {
|
|
"gridFontSize": 24,
|
|
"recolorStructures": true
|
|
},
|
|
"tileDrip": {
|
|
"bucketCount": 9
|
|
},
|
|
"lightConfigs": {
|
|
"City": {
|
|
"radius": 18,
|
|
"intensity": 1.2
|
|
},
|
|
"Port": {
|
|
"radius": 12,
|
|
"intensity": 1
|
|
},
|
|
"Factory": {
|
|
"radius": 12,
|
|
"intensity": 1
|
|
},
|
|
"Defense Post": {
|
|
"radius": 10,
|
|
"intensity": 0.9
|
|
},
|
|
"SAM Launcher": {
|
|
"radius": 10,
|
|
"intensity": 0.9
|
|
},
|
|
"Missile Silo": {
|
|
"radius": 10,
|
|
"intensity": 0.9
|
|
},
|
|
"Transport": {
|
|
"radius": 6,
|
|
"intensity": 2.7
|
|
},
|
|
"Trade Ship": {
|
|
"radius": 6,
|
|
"intensity": 2.7
|
|
},
|
|
"Warship": {
|
|
"radius": 10,
|
|
"intensity": 2.8
|
|
},
|
|
"Atom Bomb": {
|
|
"radius": 16,
|
|
"intensity": 1.1
|
|
},
|
|
"Hydrogen Bomb": {
|
|
"radius": 22,
|
|
"intensity": 1.3
|
|
},
|
|
"MIRV": {
|
|
"radius": 18,
|
|
"intensity": 1.2
|
|
},
|
|
"MIRV Warhead": {
|
|
"radius": 12,
|
|
"intensity": 1
|
|
},
|
|
"Train": {
|
|
"radius": 6,
|
|
"intensity": 0.5
|
|
}
|
|
}
|
|
}
|