mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 03:32:02 +00:00
2794ab1270
## 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>
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
import type {
|
|
AttackRingInput,
|
|
BonusEvent,
|
|
ConquestFx,
|
|
DeadUnitFx,
|
|
FrameData,
|
|
NameEntry,
|
|
NukeTelegraphData,
|
|
PlayerState,
|
|
PlayerStatusData,
|
|
TilePair,
|
|
UnitState,
|
|
} from "../types";
|
|
|
|
/**
|
|
* Structural interface for the GPU view target.
|
|
* Satisfied by GameView through TypeScript structural typing.
|
|
*/
|
|
export interface FrameUploadTarget {
|
|
uploadTileAndTrailState(
|
|
tileState: Uint16Array,
|
|
trailState: Uint16Array,
|
|
): void;
|
|
uploadLiveDelta(tileState: Uint16Array, changedTiles: TilePair[]): void;
|
|
uploadLiveTrailDelta(
|
|
trailState: Uint16Array,
|
|
dirtyRowMin: number,
|
|
dirtyRowMax: number,
|
|
): void;
|
|
uploadRailroadState(data: Uint8Array): void;
|
|
applyRailroadDust(tileRefs: number[]): void;
|
|
updateUnits(units: ReadonlyMap<number, UnitState>, gameTick: number): void;
|
|
updateStructures(units: ReadonlyMap<number, UnitState>): void;
|
|
applyDeadUnits(deadUnits: DeadUnitFx[]): void;
|
|
applyConquestEvents(events: ConquestFx[]): void;
|
|
applyBonusEvents(events: BonusEvent[]): void;
|
|
updateAttackRings(rings: AttackRingInput[]): void;
|
|
updateNukeTelegraphs(data: NukeTelegraphData[]): void;
|
|
updateNames(
|
|
names: ReadonlyMap<string, NameEntry>,
|
|
players: ReadonlyMap<number, PlayerState>,
|
|
snap: boolean,
|
|
statusData?: ReadonlyMap<number, PlayerStatusData>,
|
|
): void;
|
|
updateRelations(data: Uint8Array, size: number): void;
|
|
setSAMAllianceClusters(clusters: ReadonlyMap<number, number>): void;
|
|
}
|
|
|
|
/**
|
|
* Upload a FrameData snapshot to the GPU view.
|
|
*
|
|
* A straightforward dispatch loop: pushes tile/trail deltas, then all the
|
|
* conditional railroad/ephemeral uploads, to the view's update*() methods.
|
|
*/
|
|
export function uploadFrameData(
|
|
view: FrameUploadTarget,
|
|
frame: FrameData,
|
|
): void {
|
|
// --- Tiles + Trails ---
|
|
// changedTiles[] means "only these tiles changed" (empty = nothing changed,
|
|
// skip upload). null means "no delta info" (first tick — full upload needed).
|
|
if (frame.changedTiles) {
|
|
if (frame.changedTiles.length > 0) {
|
|
view.uploadLiveDelta(frame.tileState, frame.changedTiles);
|
|
}
|
|
// Trail dirty rows come from TrailManager, independent of tile deltas
|
|
if (frame.trailDirtyRowMax >= 0) {
|
|
view.uploadLiveTrailDelta(
|
|
frame.trailState,
|
|
frame.trailDirtyRowMin,
|
|
frame.trailDirtyRowMax,
|
|
);
|
|
}
|
|
} else {
|
|
view.uploadTileAndTrailState(frame.tileState, frame.trailState);
|
|
}
|
|
|
|
// --- Railroads ---
|
|
if (frame.railroadDirty) {
|
|
view.uploadRailroadState(frame.railroadState);
|
|
if (frame.revealedRailTiles.length > 0) {
|
|
view.applyRailroadDust(frame.revealedRailTiles);
|
|
}
|
|
}
|
|
|
|
// --- Units + structures ---
|
|
view.updateUnits(frame.units, frame.tick);
|
|
if (frame.structuresDirty) {
|
|
view.updateStructures(frame.units);
|
|
}
|
|
|
|
// --- Ephemeral effects ---
|
|
if (frame.events.deadUnits.length > 0) {
|
|
view.applyDeadUnits(frame.events.deadUnits);
|
|
}
|
|
if (frame.events.conquestEvents.length > 0) {
|
|
view.applyConquestEvents(frame.events.conquestEvents);
|
|
}
|
|
if (frame.events.bonusEvents.length > 0) {
|
|
view.applyBonusEvents(frame.events.bonusEvents);
|
|
}
|
|
|
|
// --- Attack rings + nuke telegraphs ---
|
|
view.updateAttackRings(frame.attackRings);
|
|
view.updateNukeTelegraphs(frame.nukeTelegraphs);
|
|
|
|
// --- Names + player status ---
|
|
view.updateNames(frame.names, frame.players, false, frame.playerStatus);
|
|
|
|
// --- Relations ---
|
|
// Gated: updateRelations triggers a full-map border recompute downstream,
|
|
// so only push when the matrix was actually rebuilt this tick.
|
|
if (frame.relationsDirty) {
|
|
view.updateRelations(frame.relationMatrix, frame.relationSize);
|
|
}
|
|
|
|
// --- Alliance clusters (SAM pass) ---
|
|
view.setSAMAllianceClusters(frame.allianceClusters);
|
|
}
|