mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 22:56:06 +00:00
## What Adds a **`nukeTrail`** cosmetic effectType alongside `transportShipTrail`, so nukes leave a trail colored by their own gradient/transition effect — independent of the boat-trail effect (a player can run both). Also reorganizes the effects picker and store into per-effectType **tabs**. ## Rendering Boat and nuke trails are stamped into **one** trail texture keyed only by owner, so independent coloring needs a per-tile unit-class signal: - **Trail texture** `R8UI` → `R16UI`: texel = `ownerID(bits 0-11) | nukeBit(bit 12)`. `TrailManager` stamps the bit (and preserves it when repainting on unit death); the `Uint8Array`→`Uint16Array` ripple + `UNSIGNED_SHORT` uploads flow through `GpuResources`, `TrailPass`, `Upload`, `MapRenderer`, `Renderer`, `FrameData`. - **Effect texture** widened to two stacked blocks (`TRAIL_EFFECT_BLOCKS`): rows 0–7 = transportShipTrail, rows 8–15 = nukeTrail. `writeEffectEntry(…, rowBase)`; `syncPlayerEffects` resolves both effectTypes. - **Shader** masks the owner, derives `rowBase` from the nuke bit, offsets every row, and reuses the gradient/transition decode. - Bonus: the 12-bit owner mask lifts the old `R8UI` >255-player truncation. ## Schema / server / UI - Shared attributes schema renamed `TransportShipTrail…` → **`TrailEffectAttributesSchema`** (it's no longer ship-specific); `NukeTrailEffectSchema` added to `EffectSchema` + `CosmeticsSchema.effects`. `EFFECT_TYPES = [transportShipTrail, nukeTrail]`. - Server `Privilege`, selection, and the picker grid all iterate `EFFECT_TYPES`, so they handle the new type with **no per-type code**. - **Tabs:** the selection modal uses one tab per effectType (`BaseModal`'s native tabs); the **store's** EFFECTS panel gets an internal sub-tab bar (its top-level PACKS/EFFECTS tabs can't nest). Tabs are always present, so a type you own entirely still appears as an empty tab (previously the boat-trail section vanished from the store when you owned everything). ## Review A 3-angle adversarial review (bit-packing, type-ripple, GLSL/data-flow) **refuted** the correctness concerns — the R16UI format, masking, and block layout agree across `TrailManager` / shader / builder. The minor survivors (a preview that only resolved boat trails, stale comments) were fixed. ## Testing - `tsc --noEmit`, ESLint, Prettier, `build-prod` — all clean. - Schema/`Privilege` tests updated for `nukeTrail` (96 tests pass). - The GL trail + tab UI are visual — not yet verified in a running game. - The catalog (`cosmetics.json`, closed-source API) must ship the `effects.nukeTrail` block for the effect to appear in production. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
/**
|
|
* TrailManager stamps a unit's path into the per-tile "last owner" texture.
|
|
*
|
|
* Smoothed nukes are interpolated lastPos→pos per render frame (UnitPass), so
|
|
* their trail must stamp only up to `lastPos` — otherwise the tail would lead
|
|
* the smoothly-moving missile sprite. Every other unit stamps up to `pos`.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
NUKE_TRAIL_BIT,
|
|
TrailManager,
|
|
} from "../../../../src/client/render/frame/TrailManager";
|
|
import type { UnitState } from "../../../../src/client/render/types";
|
|
import {
|
|
UT_ATOM_BOMB,
|
|
UT_TRADE_SHIP,
|
|
} from "../../../../src/client/render/types";
|
|
|
|
const MAP_W = 50;
|
|
const MAP_H = 50;
|
|
|
|
function unit(overrides: Partial<UnitState> = {}): UnitState {
|
|
return {
|
|
id: 1,
|
|
unitType: UT_ATOM_BOMB,
|
|
ownerID: 7,
|
|
lastOwnerID: null,
|
|
pos: 0,
|
|
lastPos: 0,
|
|
isActive: true,
|
|
reachedTarget: false,
|
|
retreating: false,
|
|
targetable: true,
|
|
markedForDeletion: false,
|
|
health: null,
|
|
underConstruction: false,
|
|
targetUnitId: null,
|
|
targetTile: null,
|
|
troops: 0,
|
|
missileTimerQueue: [],
|
|
level: 1,
|
|
hasTrainStation: false,
|
|
trainType: null,
|
|
loaded: null,
|
|
constructionStartTick: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function units(...us: UnitState[]): Map<number, UnitState> {
|
|
return new Map(us.map((u) => [u.id, u]));
|
|
}
|
|
|
|
const ref = (x: number, y: number) => y * MAP_W + x;
|
|
|
|
describe("TrailManager", () => {
|
|
it("stamps a smoothed nuke's trail only up to lastPos, not pos", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
// A nuke's texel carries the owner smallID plus the nuke bit (bit 12).
|
|
const nukeTexel = 7 | NUKE_TRAIL_BIT;
|
|
|
|
// First sighting: lastPos === pos at spawn.
|
|
tm.update(units(unit({ pos: ref(2, 2), lastPos: ref(2, 2) })), [1]);
|
|
expect(trail[ref(2, 2)]).toBe(nukeTexel);
|
|
|
|
// Move: lastPos trails pos by a tile. The trail head must reach lastPos
|
|
// (3,2) but NOT the current pos (4,2) — the smoothed sprite occupies the
|
|
// lastPos→pos span this frame.
|
|
tm.update(units(unit({ pos: ref(4, 2), lastPos: ref(3, 2) })), [1]);
|
|
expect(trail[ref(3, 2)]).toBe(nukeTexel);
|
|
expect(trail[ref(4, 2)]).toBe(0);
|
|
});
|
|
|
|
it("stamps a non-smoothed unit's trail up to pos", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
tm.update(
|
|
units(
|
|
unit({ unitType: UT_TRADE_SHIP, pos: ref(2, 2), lastPos: ref(2, 2) }),
|
|
),
|
|
[1],
|
|
);
|
|
tm.update(
|
|
units(
|
|
unit({ unitType: UT_TRADE_SHIP, pos: ref(4, 2), lastPos: ref(3, 2) }),
|
|
),
|
|
[1],
|
|
);
|
|
|
|
// Trade ships are not interpolated, so the trail reaches the current pos.
|
|
expect(trail[ref(4, 2)]).toBe(7);
|
|
});
|
|
|
|
it("clears a unit's trail when it disappears", () => {
|
|
const tm = new TrailManager(MAP_W, MAP_H);
|
|
const trail = tm.getTrailState();
|
|
|
|
const nukeTexel = 7 | NUKE_TRAIL_BIT;
|
|
tm.update(units(unit({ pos: ref(5, 5), lastPos: ref(5, 5) })), [1]);
|
|
tm.update(units(unit({ pos: ref(7, 5), lastPos: ref(6, 5) })), [1]);
|
|
expect(trail[ref(5, 5)]).toBe(nukeTexel);
|
|
expect(trail[ref(6, 5)]).toBe(nukeTexel);
|
|
|
|
// Unit gone from the map → its tiles are cleared.
|
|
tm.update(new Map(), []);
|
|
expect(trail[ref(5, 5)]).toBe(0);
|
|
expect(trail[ref(6, 5)]).toBe(0);
|
|
});
|
|
});
|