mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 18:45:06 +00:00
feat: structures cosmetic effect (hover-shown gradient/transition recolor) (#4492)
## Description: Adds a new `structures` cosmetic effect type: an equippable effect that recolors the owner's structure icons (City, Port, Factory, Defense Post, SAM Launcher, Missile Silo) with gradient or transition color styles. The effect is **shown while the owner's territory is hovered** — structures otherwise keep their normal player colors, so the map stays readable. **Cosmetics / selection** - `StructuresEffectAttributesSchema` (`CosmeticSchemas.ts`): its own discriminated union (`gradient` / `transition`) — structurally identical to the trail attributes today, but structures aren't trails, so it's a separate schema free to diverge. - Slot = the effectType itself: `effectTypeForSlot` is generalized to map any non-nukeExplosion effect type to itself, so server privilege checks (`Privilege.ts`), client selection, and persistence all work with no per-type code. - Effects tab, Default tile, and the store preview (shared color swatch) come from `EFFECT_TYPES`; the only UI addition is the `effects.type.structures` label in `en.json`. **Rendering** - The shared per-player effect palette grows from 2 to 3 blocks (`EFFECT_PALETTE_BLOCKS`; structures = block 2, pinned by a build-breaking guard). `syncPlayerEffects` resolves the `structures` selection through the same `writeEffectEntry` used by trails. - `StructurePass` binds the effect texture plus `uTime` and `uHoverOwner` (fed from the existing `HoverHighlightController` → `setHighlightOwner` path, now forwarded to the pass). - `structure.frag.glsl` recolors the **fill only** — the border keeps the player color for ownership legibility; alt view and construction gray bypass the effect entirely. - Style semantics: - `gradient` — the palette spans each icon's diagonal once (a visible gradient across the shape), sliding one full cycle every `colorSize · 4 · count / movementSpeed` seconds (the trail-equivalent pace; world-space banding like the trail's would put a whole icon inside one band and read as a flat color) - `transition` — the whole icon is one color at a time, cross-fading at `frequency` colors/s - Glyph contrast: the inner icon's black/white decision is now a smooth luminance fade (`smoothstep(0.25, 0.45)`) instead of a hard flip at 0.25, so animated fills cross-fade the glyph instead of snapping it between black and white. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -580,6 +580,91 @@ describe("nukeExplosion in the cosmetics catalog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("structures effects", () => {
|
||||
const gradient = {
|
||||
name: "rwb_structure_gradient",
|
||||
effectType: "structures",
|
||||
attributes: {
|
||||
type: "gradient",
|
||||
colors: ["#f00000", "#ffffff", "#1000f5"],
|
||||
colorSize: 5,
|
||||
movementSpeed: 5,
|
||||
},
|
||||
affiliateCode: null,
|
||||
product: null,
|
||||
rarity: "common",
|
||||
};
|
||||
const transition = {
|
||||
name: "rwb_structure_transistion",
|
||||
effectType: "structures",
|
||||
attributes: {
|
||||
type: "transition",
|
||||
colors: ["#ff0000", "#ffffff", "#0008ff"],
|
||||
frequency: 5,
|
||||
},
|
||||
affiliateCode: null,
|
||||
product: null,
|
||||
priceHard: 1,
|
||||
rarity: "common",
|
||||
};
|
||||
|
||||
it("parses the gradient and transition catalog entries", () => {
|
||||
const result = CosmeticsSchema.safeParse({
|
||||
patterns: {},
|
||||
flags: {},
|
||||
effects: {
|
||||
structures: {
|
||||
rwb_structure_gradient: gradient,
|
||||
rwb_structure_transistion: transition,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(
|
||||
result.data.effects?.structures?.rwb_structure_gradient?.attributes
|
||||
.type,
|
||||
).toBe("gradient");
|
||||
expect(
|
||||
result.data.effects?.structures?.rwb_structure_transistion?.attributes
|
||||
.type,
|
||||
).toBe("transition");
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves the structures slot (slot = effectType)", () => {
|
||||
expect(effectTypeForSlot("structures")).toBe("structures");
|
||||
const parsed = CosmeticsSchema.safeParse({
|
||||
patterns: {},
|
||||
flags: {},
|
||||
effects: { structures: { rwb_structure_gradient: gradient } },
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
if (!parsed.success) return;
|
||||
expect(
|
||||
findEffectForSlot(parsed.data, "structures", "rwb_structure_gradient")
|
||||
?.name,
|
||||
).toBe("rwb_structure_gradient");
|
||||
});
|
||||
|
||||
it("shares trail attribute shapes but is not a trail effect", () => {
|
||||
const eff = EffectSchema.parse(gradient);
|
||||
// Renders through the structures palette block, not a trail block.
|
||||
expect(isTrailEffect(eff)).toBe(false);
|
||||
expect(effectMatchesSlot(eff, "structures")).toBe(true);
|
||||
expect(effectMatchesSlot(eff, "transportShipTrail")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a structures effect with an unknown attribute type", () => {
|
||||
expect(
|
||||
EffectSchema.safeParse({
|
||||
...gradient,
|
||||
attributes: { type: "sparkle", colors: [] },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTrailEffect", () => {
|
||||
it("is true for a trail effect and false for a nukeExplosion", () => {
|
||||
const trail = EffectSchema.parse({
|
||||
|
||||
Reference in New Issue
Block a user