mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
Color nuke telegraph circles by launcher relation (self/ally/enemy)
The blast-radius warning circle was always red, so players couldn't tell who launched an incoming nuke. Now it's green for your own nukes, yellow for ally/teammate nukes, and red for everyone else's. Each telegraph carries a relation (0=self, 1=friendly, 2=enemy), classified from the per-tick relation matrix — the same friend/foe logic alt-view uses — and passed to the shader as a per-instance attribute. Replay/spectator mode (no local player) stays all red. Colors are tunable via the nukeTelegraph slice in render-settings.json.
This commit is contained in:
@@ -1,22 +1,56 @@
|
||||
import type { NukeTelegraphData, UnitState } from "../../types";
|
||||
import { NUKE_MAGNITUDES } from "../../types";
|
||||
|
||||
// Must match RelationMatrix.ts
|
||||
const RELATION_FRIENDLY = 1;
|
||||
|
||||
export const TELEGRAPH_SELF = 0;
|
||||
export const TELEGRAPH_FRIENDLY = 1;
|
||||
export const TELEGRAPH_ENEMY = 2;
|
||||
|
||||
/**
|
||||
* Classify a nuke owner relative to the local player:
|
||||
* 0 = own nuke, 1 = ally/teammate, 2 = everyone else.
|
||||
* No local player (replay / spectator) → everything is 2 (enemy color).
|
||||
*/
|
||||
function classifyOwner(
|
||||
ownerID: number,
|
||||
localPlayerID: number,
|
||||
relationMatrix: Uint8Array | undefined,
|
||||
relationSize: number,
|
||||
): number {
|
||||
if (localPlayerID <= 0) return TELEGRAPH_ENEMY;
|
||||
if (ownerID === localPlayerID) return TELEGRAPH_SELF;
|
||||
if (
|
||||
relationMatrix &&
|
||||
ownerID > 0 &&
|
||||
ownerID < relationSize &&
|
||||
localPlayerID < relationSize &&
|
||||
relationMatrix[localPlayerID * relationSize + ownerID] === RELATION_FRIENDLY
|
||||
) {
|
||||
return TELEGRAPH_FRIENDLY;
|
||||
}
|
||||
return TELEGRAPH_ENEMY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract nuke telegraph circles for active nukes with targets.
|
||||
*
|
||||
* When `friendlyIDs` is provided, only nukes owned by those players are
|
||||
* included (live game — you see your own + teammates' telegraphs).
|
||||
* When omitted, all nukes are included (replay / spectator).
|
||||
* Each telegraph carries a `relation` (self / friendly / enemy) so the
|
||||
* renderer can color it by who launched the nuke. Pass the local player's
|
||||
* smallID plus the relation matrix from RelationMatrix.ts; omit them in
|
||||
* replay / spectator mode to color everything as enemy.
|
||||
*/
|
||||
export function extractNukeTelegraphs(
|
||||
units: ReadonlyMap<number, UnitState>,
|
||||
mapW: number,
|
||||
friendlyIDs?: ReadonlySet<number>,
|
||||
localPlayerID = 0,
|
||||
relationMatrix?: Uint8Array,
|
||||
relationSize = 0,
|
||||
): NukeTelegraphData[] {
|
||||
const telegraphs: NukeTelegraphData[] = [];
|
||||
for (const u of units.values()) {
|
||||
if (u.targetTile === null || !u.isActive) continue;
|
||||
if (friendlyIDs && !friendlyIDs.has(u.ownerID)) continue;
|
||||
const mag = NUKE_MAGNITUDES[u.unitType];
|
||||
if (!mag) continue;
|
||||
telegraphs.push({
|
||||
@@ -24,6 +58,12 @@ export function extractNukeTelegraphs(
|
||||
y: (u.targetTile - (u.targetTile % mapW)) / mapW,
|
||||
innerRadius: mag.inner,
|
||||
outerRadius: mag.outer,
|
||||
relation: classifyOwner(
|
||||
u.ownerID,
|
||||
localPlayerID,
|
||||
relationMatrix,
|
||||
relationSize,
|
||||
),
|
||||
});
|
||||
}
|
||||
return telegraphs;
|
||||
@@ -37,13 +77,14 @@ export function extractNukeTelegraphsFromIds(
|
||||
nukeIds: readonly number[],
|
||||
units: ReadonlyMap<number, UnitState>,
|
||||
mapW: number,
|
||||
friendlyIDs?: ReadonlySet<number>,
|
||||
localPlayerID = 0,
|
||||
relationMatrix?: Uint8Array,
|
||||
relationSize = 0,
|
||||
): NukeTelegraphData[] {
|
||||
const telegraphs: NukeTelegraphData[] = [];
|
||||
for (const id of nukeIds) {
|
||||
const u = units.get(id);
|
||||
if (!u || u.targetTile === null || !u.isActive) continue;
|
||||
if (friendlyIDs && !friendlyIDs.has(u.ownerID)) continue;
|
||||
const mag = NUKE_MAGNITUDES[u.unitType];
|
||||
if (!mag) continue;
|
||||
telegraphs.push({
|
||||
@@ -51,6 +92,12 @@ export function extractNukeTelegraphsFromIds(
|
||||
y: (u.targetTile - (u.targetTile % mapW)) / mapW,
|
||||
innerRadius: mag.inner,
|
||||
outerRadius: mag.outer,
|
||||
relation: classifyOwner(
|
||||
u.ownerID,
|
||||
localPlayerID,
|
||||
relationMatrix,
|
||||
relationSize,
|
||||
),
|
||||
});
|
||||
}
|
||||
return telegraphs;
|
||||
|
||||
@@ -299,9 +299,15 @@ export interface RenderSettings {
|
||||
pulseAmplitude: number; // alpha pulse ±
|
||||
pulseSpeed: number; // pulse frequency (radians/sec)
|
||||
fillAlphaOffset: number; // inner fill is baseAlpha minus this
|
||||
colorR: number; // circle color
|
||||
colorR: number; // circle color — enemy nukes
|
||||
colorG: number;
|
||||
colorB: number;
|
||||
selfColorR: number; // circle color — own nukes
|
||||
selfColorG: number;
|
||||
selfColorB: number;
|
||||
allyColorR: number; // circle color — ally/teammate nukes
|
||||
allyColorG: number;
|
||||
allyColorB: number;
|
||||
};
|
||||
moveIndicator: {
|
||||
startRadius: number; // screen px — initial distance from center
|
||||
|
||||
@@ -14,8 +14,8 @@ import { createProgram } from "../utils/GlUtils";
|
||||
import fragSrc from "../shaders/nuke-telegraph/nuke-telegraph.frag.glsl?raw";
|
||||
import vertSrc from "../shaders/nuke-telegraph/nuke-telegraph.vert.glsl?raw";
|
||||
|
||||
// Per-instance: x, y, innerRadius, outerRadius
|
||||
const FLOATS_PER_INSTANCE = 4;
|
||||
// Per-instance: x, y, innerRadius, outerRadius, relation
|
||||
const FLOATS_PER_INSTANCE = 5;
|
||||
|
||||
export class NukeTelegraphPass {
|
||||
private gl: WebGL2RenderingContext;
|
||||
@@ -28,7 +28,9 @@ export class NukeTelegraphPass {
|
||||
private uTime: WebGLUniformLocation;
|
||||
private uTelegraphStyle: WebGLUniformLocation;
|
||||
private uTelegraphAlpha: WebGLUniformLocation;
|
||||
private uTelegraphColor: WebGLUniformLocation;
|
||||
private uColorSelf: WebGLUniformLocation;
|
||||
private uColorAlly: WebGLUniformLocation;
|
||||
private uColorEnemy: WebGLUniformLocation;
|
||||
|
||||
private instanceCount = 0;
|
||||
private startTime = performance.now();
|
||||
@@ -48,10 +50,9 @@ export class NukeTelegraphPass {
|
||||
this.program,
|
||||
"uTelegraphAlpha",
|
||||
)!;
|
||||
this.uTelegraphColor = gl.getUniformLocation(
|
||||
this.program,
|
||||
"uTelegraphColor",
|
||||
)!;
|
||||
this.uColorSelf = gl.getUniformLocation(this.program, "uColorSelf")!;
|
||||
this.uColorAlly = gl.getUniformLocation(this.program, "uColorAlly")!;
|
||||
this.uColorEnemy = gl.getUniformLocation(this.program, "uColorEnemy")!;
|
||||
|
||||
// VAO
|
||||
this.vao = gl.createVertexArray()!;
|
||||
@@ -69,6 +70,7 @@ export class NukeTelegraphPass {
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Attribute 1: per-instance vec4 (x, y, innerR, outerR)
|
||||
// Attribute 2: per-instance float (relation: 0=self, 1=ally, 2=enemy)
|
||||
const glBuf = gl.createBuffer()!;
|
||||
this.instanceBuf = new DynamicInstanceBuffer(
|
||||
gl,
|
||||
@@ -76,10 +78,14 @@ export class NukeTelegraphPass {
|
||||
16,
|
||||
FLOATS_PER_INSTANCE,
|
||||
);
|
||||
const stride = FLOATS_PER_INSTANCE * 4;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, glBuf);
|
||||
gl.enableVertexAttribArray(1);
|
||||
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
|
||||
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, stride, 0);
|
||||
gl.vertexAttribDivisor(1, 1);
|
||||
gl.enableVertexAttribArray(2);
|
||||
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, stride, 16);
|
||||
gl.vertexAttribDivisor(2, 1);
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
@@ -96,6 +102,7 @@ export class NukeTelegraphPass {
|
||||
buf[off + 1] = d.y;
|
||||
buf[off + 2] = d.innerRadius;
|
||||
buf[off + 3] = d.outerRadius;
|
||||
buf[off + 4] = d.relation;
|
||||
}
|
||||
|
||||
this.instanceCount = count;
|
||||
@@ -137,7 +144,9 @@ export class NukeTelegraphPass {
|
||||
s.pulseSpeed,
|
||||
s.fillAlphaOffset,
|
||||
);
|
||||
gl.uniform3f(this.uTelegraphColor, s.colorR, s.colorG, s.colorB);
|
||||
gl.uniform3f(this.uColorSelf, s.selfColorR, s.selfColorG, s.selfColorB);
|
||||
gl.uniform3f(this.uColorAlly, s.allyColorR, s.allyColorG, s.allyColorB);
|
||||
gl.uniform3f(this.uColorEnemy, s.colorR, s.colorG, s.colorB);
|
||||
|
||||
gl.bindVertexArray(this.vao);
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.instanceCount);
|
||||
|
||||
@@ -260,7 +260,13 @@
|
||||
"fillAlphaOffset": 0.6,
|
||||
"colorR": 1,
|
||||
"colorG": 0,
|
||||
"colorB": 0
|
||||
"colorB": 0,
|
||||
"selfColorR": 0,
|
||||
"selfColorG": 1,
|
||||
"selfColorB": 0,
|
||||
"allyColorR": 1,
|
||||
"allyColorG": 1,
|
||||
"allyColorB": 0
|
||||
},
|
||||
"moveIndicator": {
|
||||
"startRadius": 13,
|
||||
|
||||
@@ -4,11 +4,14 @@ precision highp float;
|
||||
in vec2 vLocal;
|
||||
flat in float vInnerRadius;
|
||||
flat in float vOuterRadius;
|
||||
flat in float vRelation; // 0 = self, 1 = ally, 2 = enemy
|
||||
|
||||
uniform float uTime; // seconds
|
||||
uniform vec4 uTelegraphStyle; // (strokeWidth, dashLen, gapLen, rotationSpeed)
|
||||
uniform vec4 uTelegraphAlpha; // (baseAlpha, pulseAmplitude, pulseSpeed, fillAlphaOffset)
|
||||
uniform vec3 uTelegraphColor;
|
||||
uniform vec3 uColorSelf;
|
||||
uniform vec3 uColorAlly;
|
||||
uniform vec3 uColorEnemy;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
@@ -52,5 +55,8 @@ void main() {
|
||||
float alpha = max(max(fillAlpha, strokeAlpha), outerAlpha);
|
||||
if (alpha < 0.01) discard;
|
||||
|
||||
fragColor = vec4(uTelegraphColor, alpha);
|
||||
vec3 color = vRelation < 0.5 ? uColorSelf
|
||||
: vRelation < 1.5 ? uColorAlly
|
||||
: uColorEnemy;
|
||||
fragColor = vec4(color, alpha);
|
||||
}
|
||||
|
||||
@@ -5,17 +5,21 @@ precision highp float;
|
||||
layout(location = 0) in vec2 aPos;
|
||||
// Per-instance: x, y, innerRadius, outerRadius
|
||||
layout(location = 1) in vec4 aInstance;
|
||||
// Per-instance: 0 = self, 1 = ally, 2 = enemy
|
||||
layout(location = 2) in float aRelation;
|
||||
|
||||
uniform mat3 uCamera;
|
||||
|
||||
out vec2 vLocal; // [-1, +1] local coords
|
||||
flat out float vInnerRadius;
|
||||
flat out float vOuterRadius;
|
||||
flat out float vRelation;
|
||||
|
||||
void main() {
|
||||
vLocal = aPos * 2.0 - 1.0;
|
||||
vInnerRadius = aInstance.z;
|
||||
vOuterRadius = aInstance.w;
|
||||
vRelation = aRelation;
|
||||
|
||||
// Expand quad to cover outer circle bbox + padding
|
||||
float r = aInstance.w + 2.0;
|
||||
|
||||
@@ -205,6 +205,8 @@ export interface NukeTelegraphData {
|
||||
y: number;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
/** Launcher vs local player: 0 = self, 1 = ally/teammate, 2 = enemy. */
|
||||
relation: number;
|
||||
}
|
||||
|
||||
/** Lean config for constructing the GPU renderer — no replay-specific fields. */
|
||||
|
||||
@@ -485,6 +485,9 @@ export class GameView implements GameMap {
|
||||
f.nukeTelegraphs = extractNukeTelegraphs(
|
||||
this._unitStates,
|
||||
this._map.width(),
|
||||
this._myPlayer?.smallID() ?? 0,
|
||||
rel.matrix,
|
||||
rel.size,
|
||||
);
|
||||
f.attackRings = this._myPlayer
|
||||
? extractAttackRings(
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* extractNukeTelegraphs colors each telegraph by who launched the nuke:
|
||||
*
|
||||
* - relation 0 (self): the local player owns the nuke
|
||||
* - relation 1 (friendly): an ally or teammate owns it (via relation matrix)
|
||||
* - relation 2 (enemy): everyone else, and everything in replay / spectator
|
||||
* mode (no local player)
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
extractNukeTelegraphs,
|
||||
TELEGRAPH_ENEMY,
|
||||
TELEGRAPH_FRIENDLY,
|
||||
TELEGRAPH_SELF,
|
||||
} from "../../../../../src/client/render/frame/derive/NukeTelegraphs";
|
||||
import { buildRelationMatrix } from "../../../../../src/client/render/frame/derive/RelationMatrix";
|
||||
import type {
|
||||
PlayerState,
|
||||
UnitState,
|
||||
} from "../../../../../src/client/render/types";
|
||||
import {
|
||||
UT_ATOM_BOMB,
|
||||
UT_WARSHIP,
|
||||
} from "../../../../../src/client/render/types";
|
||||
|
||||
const MAP_W = 100;
|
||||
|
||||
function ps(overrides: Partial<PlayerState> = {}): PlayerState {
|
||||
return {
|
||||
smallID: 1,
|
||||
isAlive: true,
|
||||
isDisconnected: false,
|
||||
tilesOwned: 0,
|
||||
gold: 0,
|
||||
troops: 0,
|
||||
isTraitor: false,
|
||||
traitorRemainingTicks: 0,
|
||||
betrayals: 0,
|
||||
hasSpawned: true,
|
||||
lastDeleteUnitTick: 0,
|
||||
allies: [],
|
||||
embargoes: [],
|
||||
targets: [],
|
||||
outgoingAttacks: [],
|
||||
incomingAttacks: [],
|
||||
outgoingAllianceRequests: [],
|
||||
alliances: [],
|
||||
outgoingEmojis: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function nuke(overrides: Partial<UnitState> = {}): UnitState {
|
||||
return {
|
||||
id: 1,
|
||||
unitType: UT_ATOM_BOMB,
|
||||
ownerID: 1,
|
||||
lastOwnerID: null,
|
||||
pos: 0,
|
||||
lastPos: 0,
|
||||
isActive: true,
|
||||
reachedTarget: false,
|
||||
retreating: false,
|
||||
targetable: true,
|
||||
markedForDeletion: false,
|
||||
health: null,
|
||||
underConstruction: false,
|
||||
targetUnitId: null,
|
||||
targetTile: 305,
|
||||
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]));
|
||||
}
|
||||
|
||||
describe("extractNukeTelegraphs", () => {
|
||||
it("computes target x/y and blast radii", () => {
|
||||
const [t] = extractNukeTelegraphs(units(nuke({ targetTile: 305 })), MAP_W);
|
||||
expect(t).toMatchObject({ x: 5, y: 3, innerRadius: 12, outerRadius: 30 });
|
||||
});
|
||||
|
||||
it("skips inactive nukes, nukes without a target, and non-nuke units", () => {
|
||||
const result = extractNukeTelegraphs(
|
||||
units(
|
||||
nuke({ id: 1, isActive: false }),
|
||||
nuke({ id: 2, targetTile: null }),
|
||||
nuke({ id: 3, unitType: UT_WARSHIP }),
|
||||
),
|
||||
MAP_W,
|
||||
);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("marks the local player's own nukes as self", () => {
|
||||
const rel = buildRelationMatrix(new Map([[1, ps({ smallID: 1 })]]));
|
||||
const [t] = extractNukeTelegraphs(
|
||||
units(nuke({ ownerID: 1 })),
|
||||
MAP_W,
|
||||
1,
|
||||
rel.matrix,
|
||||
rel.size,
|
||||
);
|
||||
expect(t.relation).toBe(TELEGRAPH_SELF);
|
||||
});
|
||||
|
||||
it("marks an ally's nuke as friendly", () => {
|
||||
const rel = buildRelationMatrix(
|
||||
new Map([
|
||||
[1, ps({ smallID: 1, allies: [2] })],
|
||||
[2, ps({ smallID: 2 })],
|
||||
]),
|
||||
);
|
||||
const [t] = extractNukeTelegraphs(
|
||||
units(nuke({ ownerID: 2 })),
|
||||
MAP_W,
|
||||
1,
|
||||
rel.matrix,
|
||||
rel.size,
|
||||
);
|
||||
expect(t.relation).toBe(TELEGRAPH_FRIENDLY);
|
||||
});
|
||||
|
||||
it("marks a teammate's nuke as friendly", () => {
|
||||
const rel = buildRelationMatrix(
|
||||
new Map([
|
||||
[1, ps({ smallID: 1 })],
|
||||
[2, ps({ smallID: 2 })],
|
||||
]),
|
||||
new Map([
|
||||
[1, "red"],
|
||||
[2, "red"],
|
||||
]),
|
||||
);
|
||||
const [t] = extractNukeTelegraphs(
|
||||
units(nuke({ ownerID: 2 })),
|
||||
MAP_W,
|
||||
1,
|
||||
rel.matrix,
|
||||
rel.size,
|
||||
);
|
||||
expect(t.relation).toBe(TELEGRAPH_FRIENDLY);
|
||||
});
|
||||
|
||||
it("marks everyone else's nukes as enemy", () => {
|
||||
const rel = buildRelationMatrix(
|
||||
new Map([
|
||||
[1, ps({ smallID: 1 })],
|
||||
[2, ps({ smallID: 2 })],
|
||||
]),
|
||||
);
|
||||
const [t] = extractNukeTelegraphs(
|
||||
units(nuke({ ownerID: 2 })),
|
||||
MAP_W,
|
||||
1,
|
||||
rel.matrix,
|
||||
rel.size,
|
||||
);
|
||||
expect(t.relation).toBe(TELEGRAPH_ENEMY);
|
||||
});
|
||||
|
||||
it("marks everything as enemy without a local player (replay/spectator)", () => {
|
||||
const [t] = extractNukeTelegraphs(units(nuke({ ownerID: 1 })), MAP_W);
|
||||
expect(t.relation).toBe(TELEGRAPH_ENEMY);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user