mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-11 02:24:15 +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:
@@ -10,7 +10,8 @@ export type Skin = z.infer<typeof SkinSchema>;
|
||||
export type Pack = z.infer<typeof PackSchema>;
|
||||
export type Subscription = z.infer<typeof SubscriptionSchema>;
|
||||
// An effect cosmetic of any type — discriminated on effectType (today
|
||||
// transportShipTrail + nukeTrail + nukeExplosion; gains a member per effectType).
|
||||
// transportShipTrail + nukeTrail + nukeExplosion + structures; gains a member
|
||||
// per effectType).
|
||||
export type Effect = z.infer<typeof EffectSchema>;
|
||||
export type EffectType = z.infer<typeof EffectTypeSchema>;
|
||||
// Shared by every trail effectType (transportShipTrail, nukeTrail, …).
|
||||
@@ -19,6 +20,10 @@ export type TrailEffectAttributes = z.infer<typeof TrailEffectAttributesSchema>;
|
||||
export type NukeExplosionAttributes = z.infer<
|
||||
typeof NukeExplosionAttributesSchema
|
||||
>;
|
||||
// Attributes of a structures effect (recolors structure icons, not a trail).
|
||||
export type StructuresEffectAttributes = z.infer<
|
||||
typeof StructuresEffectAttributesSchema
|
||||
>;
|
||||
export type PatternName = z.infer<typeof CosmeticNameSchema>;
|
||||
export type Product = z.infer<typeof ProductSchema>;
|
||||
export type ColorPalette = z.infer<typeof ColorPaletteSchema>;
|
||||
@@ -106,6 +111,7 @@ export const EFFECT_TYPES = [
|
||||
"transportShipTrail",
|
||||
"nukeTrail",
|
||||
"nukeExplosion",
|
||||
"structures",
|
||||
] as const;
|
||||
export const EffectTypeSchema = z.enum(EFFECT_TYPES);
|
||||
|
||||
@@ -199,11 +205,45 @@ const NukeExplosionEffectSchema = CosmeticSchema.extend({
|
||||
url: z.string().optional(),
|
||||
});
|
||||
|
||||
// Structures-effect attributes, discriminated on `type`. Structurally the
|
||||
// same shapes as the trail attributes today, but structures are not trails —
|
||||
// separate schema, and the spatial semantics differ:
|
||||
// - "gradient": the palette spans each structure icon's diagonal once (a
|
||||
// visible gradient across the shape), sliding one full cycle every
|
||||
// colorSize · 4 · count / movementSpeed seconds (the trail-equivalent pace).
|
||||
// - "transition": the whole icon is one color at a time, cross-fading through
|
||||
// the list. `frequency` = color changes per second.
|
||||
// Colors are unvalidated strings; the renderer drops any it can't parse (and
|
||||
// an empty list leaves the structure on its normal player color).
|
||||
export const StructuresEffectAttributesSchema = 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(),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Recolors the owner's structures (city, port, factory, defense post, SAM,
|
||||
// silo) with gradient / transition styles. Shown while the owner's territory
|
||||
// is hovered; structures otherwise keep their normal player colors.
|
||||
const StructuresEffectSchema = CosmeticSchema.extend({
|
||||
effectType: z.literal("structures"),
|
||||
attributes: StructuresEffectAttributesSchema,
|
||||
url: z.string().optional(),
|
||||
});
|
||||
|
||||
// Any catalog effect, discriminated on effectType. Add a member per effectType.
|
||||
export const EffectSchema = z.discriminatedUnion("effectType", [
|
||||
TransportShipTrailEffectSchema,
|
||||
NukeTrailEffectSchema,
|
||||
NukeExplosionEffectSchema,
|
||||
StructuresEffectSchema,
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -226,17 +266,21 @@ export function isNukeExplosionEffect(
|
||||
}
|
||||
|
||||
/**
|
||||
* A player selects one effect per "slot". A slot is the effectType for trails
|
||||
* (transportShipTrail, nukeTrail) and the nukeType for nuke explosions (atom,
|
||||
* hydro, mirvWarhead) — so a player can equip a distinct explosion per bomb.
|
||||
* Returns the effectType a slot resolves to for catalog lookup, or undefined for
|
||||
* an unknown/stale slot (e.g. a bare "nukeExplosion" key from before this split).
|
||||
* A player selects one effect per "slot". A slot is the effectType itself for
|
||||
* per-type effects (transportShipTrail, nukeTrail, structures) and the
|
||||
* nukeType for nuke explosions (atom, hydro, mirvWarhead) — so a player can
|
||||
* equip a distinct explosion per bomb. Returns the effectType a slot resolves
|
||||
* to for catalog lookup, or undefined for an unknown/stale slot (e.g. a bare
|
||||
* "nukeExplosion" key from before the per-nukeType split).
|
||||
*/
|
||||
export function effectTypeForSlot(slot: string): EffectType | undefined {
|
||||
if ((NUKE_EXPLOSION_TYPES as readonly string[]).includes(slot)) {
|
||||
return "nukeExplosion";
|
||||
}
|
||||
if ((TRAIL_EFFECT_TYPES as readonly string[]).includes(slot)) {
|
||||
if (
|
||||
(EFFECT_TYPES as readonly string[]).includes(slot) &&
|
||||
slot !== "nukeExplosion"
|
||||
) {
|
||||
return slot as EffectType;
|
||||
}
|
||||
return undefined;
|
||||
@@ -330,6 +374,7 @@ export const CosmeticsSchema = z.object({
|
||||
).optional(),
|
||||
nukeTrail: lenientRecord(NukeTrailEffectSchema).optional(),
|
||||
nukeExplosion: lenientRecord(NukeExplosionEffectSchema).optional(),
|
||||
structures: lenientRecord(StructuresEffectSchema).optional(),
|
||||
})
|
||||
.optional(),
|
||||
currencyPacks: z.record(z.string(), PackSchema).optional(),
|
||||
|
||||
Reference in New Issue
Block a user