mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 10:08:11 +00:00
Implement territory defense and relation management in GameMap and renderers; update WebGL shader
This commit is contained in:
@@ -273,28 +273,31 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy {
|
||||
? (rawOwner as PlayerView)
|
||||
: null;
|
||||
const isBorderTile = this.game.isBorder(tile);
|
||||
let isDefended = false;
|
||||
|
||||
// Update defended and relation state in the shared buffer
|
||||
if (owner && isBorderTile) {
|
||||
isDefended = this.game.hasUnitNearby(
|
||||
const isDefended = this.game.hasUnitNearby(
|
||||
tile,
|
||||
this.game.config().defensePostRange(),
|
||||
UnitType.DefensePost,
|
||||
owner.id(),
|
||||
);
|
||||
const { hasEmbargo, hasFriendly } = owner.borderRelationFlags(tile);
|
||||
let relation = 0; // neutral
|
||||
if (hasFriendly) {
|
||||
relation = 1; // friendly
|
||||
} else if (hasEmbargo) {
|
||||
relation = 2; // embargo
|
||||
}
|
||||
this.game.setDefended(tile, isDefended);
|
||||
this.game.setRelation(tile, relation);
|
||||
} else {
|
||||
// Clear defended/relation state for non-border tiles
|
||||
this.game.setDefended(tile, false);
|
||||
this.game.setRelation(tile, 0);
|
||||
}
|
||||
|
||||
this.renderer.markTile(tile);
|
||||
if (!owner || !isBorderTile) {
|
||||
this.renderer.clearBorderColor(tile);
|
||||
} else {
|
||||
const borderCol = owner.borderColor(tile, isDefended).rgba;
|
||||
this.renderer.setBorderColor(tile, {
|
||||
r: borderCol.r,
|
||||
g: borderCol.g,
|
||||
b: borderCol.b,
|
||||
a: Math.round((borderCol.a ?? 1) * 255),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
|
||||
@@ -53,6 +53,16 @@ export class TerritoryWebGLRenderer {
|
||||
hoverPulseStrength: WebGLUniformLocation | null;
|
||||
hoverPulseSpeed: WebGLUniformLocation | null;
|
||||
time: WebGLUniformLocation | null;
|
||||
// Border color uniforms for shader-computed borders
|
||||
borderNeutral: WebGLUniformLocation | null;
|
||||
borderFriendly: WebGLUniformLocation | null;
|
||||
borderEmbargo: WebGLUniformLocation | null;
|
||||
borderDefendedNeutralLight: WebGLUniformLocation | null;
|
||||
borderDefendedNeutralDark: WebGLUniformLocation | null;
|
||||
borderDefendedFriendlyLight: WebGLUniformLocation | null;
|
||||
borderDefendedFriendlyDark: WebGLUniformLocation | null;
|
||||
borderDefendedEmbargoLight: WebGLUniformLocation | null;
|
||||
borderDefendedEmbargoDark: WebGLUniformLocation | null;
|
||||
};
|
||||
|
||||
private readonly state: Uint16Array;
|
||||
@@ -117,6 +127,15 @@ export class TerritoryWebGLRenderer {
|
||||
hoverPulseStrength: null,
|
||||
hoverPulseSpeed: null,
|
||||
time: null,
|
||||
borderNeutral: null,
|
||||
borderFriendly: null,
|
||||
borderEmbargo: null,
|
||||
borderDefendedNeutralLight: null,
|
||||
borderDefendedNeutralDark: null,
|
||||
borderDefendedFriendlyLight: null,
|
||||
borderDefendedFriendlyDark: null,
|
||||
borderDefendedEmbargoLight: null,
|
||||
borderDefendedEmbargoDark: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -149,6 +168,15 @@ export class TerritoryWebGLRenderer {
|
||||
hoverPulseStrength: null,
|
||||
hoverPulseSpeed: null,
|
||||
time: null,
|
||||
borderNeutral: null,
|
||||
borderFriendly: null,
|
||||
borderEmbargo: null,
|
||||
borderDefendedNeutralLight: null,
|
||||
borderDefendedNeutralDark: null,
|
||||
borderDefendedFriendlyLight: null,
|
||||
borderDefendedFriendlyDark: null,
|
||||
borderDefendedEmbargoLight: null,
|
||||
borderDefendedEmbargoDark: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -181,6 +209,33 @@ export class TerritoryWebGLRenderer {
|
||||
),
|
||||
hoverPulseSpeed: gl.getUniformLocation(this.program, "u_hoverPulseSpeed"),
|
||||
time: gl.getUniformLocation(this.program, "u_time"),
|
||||
borderNeutral: gl.getUniformLocation(this.program, "u_borderNeutral"),
|
||||
borderFriendly: gl.getUniformLocation(this.program, "u_borderFriendly"),
|
||||
borderEmbargo: gl.getUniformLocation(this.program, "u_borderEmbargo"),
|
||||
borderDefendedNeutralLight: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedNeutralLight",
|
||||
),
|
||||
borderDefendedNeutralDark: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedNeutralDark",
|
||||
),
|
||||
borderDefendedFriendlyLight: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedFriendlyLight",
|
||||
),
|
||||
borderDefendedFriendlyDark: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedFriendlyDark",
|
||||
),
|
||||
borderDefendedEmbargoLight: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedEmbargoLight",
|
||||
),
|
||||
borderDefendedEmbargoDark: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_borderDefendedEmbargoDark",
|
||||
),
|
||||
};
|
||||
|
||||
// Vertex data: two triangles covering the full map (pixel-perfect).
|
||||
@@ -665,16 +720,24 @@ export class TerritoryWebGLRenderer {
|
||||
const maxId = players.reduce((max, p) => Math.max(max, p.smallID()), 0) + 1;
|
||||
this.paletteWidth = Math.max(maxId, 1);
|
||||
|
||||
const paletteData = new Uint8Array(this.paletteWidth * 4);
|
||||
const paletteData = new Uint8Array(this.paletteWidth * 8); // 8 bytes per player: territory RGBA + border RGBA
|
||||
const relationData = new Uint8Array(this.paletteWidth);
|
||||
|
||||
for (const p of players) {
|
||||
const id = p.smallID();
|
||||
const rgba = p.territoryColor().rgba;
|
||||
paletteData[id * 4] = rgba.r;
|
||||
paletteData[id * 4 + 1] = rgba.g;
|
||||
paletteData[id * 4 + 2] = rgba.b;
|
||||
paletteData[id * 4 + 3] = Math.round((rgba.a ?? 1) * 255);
|
||||
// Territory color (first 4 bytes)
|
||||
const territoryRgba = p.territoryColor().rgba;
|
||||
paletteData[id * 8] = territoryRgba.r;
|
||||
paletteData[id * 8 + 1] = territoryRgba.g;
|
||||
paletteData[id * 8 + 2] = territoryRgba.b;
|
||||
paletteData[id * 8 + 3] = Math.round((territoryRgba.a ?? 1) * 255);
|
||||
|
||||
// Base border color (next 4 bytes)
|
||||
const borderRgba = p.borderColor().rgba; // Get base border color without relation/defended
|
||||
paletteData[id * 8 + 4] = borderRgba.r;
|
||||
paletteData[id * 8 + 5] = borderRgba.g;
|
||||
paletteData[id * 8 + 6] = borderRgba.b;
|
||||
paletteData[id * 8 + 7] = Math.round((borderRgba.a ?? 1) * 255);
|
||||
|
||||
relationData[id] = this.resolveRelationCode(p, myPlayer);
|
||||
}
|
||||
@@ -690,7 +753,7 @@ export class TerritoryWebGLRenderer {
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RGBA8,
|
||||
this.paletteWidth,
|
||||
this.paletteWidth * 2, // 2 pixels per player (territory + border)
|
||||
1,
|
||||
0,
|
||||
gl.RGBA,
|
||||
@@ -765,6 +828,15 @@ export class TerritoryWebGLRenderer {
|
||||
uniform vec4 u_altNeutral;
|
||||
uniform vec4 u_altEnemy;
|
||||
uniform float u_alpha;
|
||||
uniform vec4 u_borderNeutral;
|
||||
uniform vec4 u_borderFriendly;
|
||||
uniform vec4 u_borderEmbargo;
|
||||
uniform vec4 u_borderDefendedNeutralLight;
|
||||
uniform vec4 u_borderDefendedNeutralDark;
|
||||
uniform vec4 u_borderDefendedFriendlyLight;
|
||||
uniform vec4 u_borderDefendedFriendlyDark;
|
||||
uniform vec4 u_borderDefendedEmbargoLight;
|
||||
uniform vec4 u_borderDefendedEmbargoDark;
|
||||
uniform bool u_alternativeView;
|
||||
uniform float u_hoveredPlayerId;
|
||||
uniform vec3 u_hoverHighlightColor;
|
||||
@@ -792,6 +864,8 @@ export class TerritoryWebGLRenderer {
|
||||
uint state = texelFetch(u_state, texCoord, 0).r;
|
||||
uint owner = state & 0xFFFu;
|
||||
bool hasFallout = (state & 0x2000u) != 0u; // bit 13
|
||||
bool isDefended = (state & 0x1000u) != 0u; // bit 12
|
||||
uint relation = (state & 0xC000u) >> 14u; // bits 14-15
|
||||
|
||||
if (owner == 0u) {
|
||||
if (hasFallout) {
|
||||
@@ -836,17 +910,40 @@ export class TerritoryWebGLRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner), 0), 0);
|
||||
vec4 borderColor = texelFetch(u_borderColor, texCoord, 0);
|
||||
vec4 base = texelFetch(u_palette, ivec2(int(owner) * 2, 0), 0); // territory color
|
||||
vec4 baseBorder = texelFetch(u_palette, ivec2(int(owner) * 2 + 1, 0), 0); // base border color
|
||||
vec3 color = base.rgb;
|
||||
float a = u_alpha;
|
||||
|
||||
if (isBorder && borderColor.a > 0.0) {
|
||||
color = borderColor.rgb;
|
||||
a = borderColor.a;
|
||||
}
|
||||
if (isBorder && borderColor.a <= 0.0) {
|
||||
a = 1.0;
|
||||
if (isBorder) {
|
||||
// Start with base border color and apply relation tint
|
||||
vec3 borderColor = baseBorder.rgb;
|
||||
|
||||
// Apply relation-based tinting (same logic as PlayerView.borderColor)
|
||||
const float BORDER_TINT_RATIO = 0.35;
|
||||
const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0); // green
|
||||
const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0); // red
|
||||
|
||||
if (relation == 1u) { // friendly
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
} else if (relation == 2u) { // embargo
|
||||
borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) +
|
||||
EMBARGO_TINT_TARGET * BORDER_TINT_RATIO;
|
||||
}
|
||||
// relation == 0u (neutral) uses base border color as-is
|
||||
|
||||
// Apply defended checkerboard pattern
|
||||
if (isDefended) {
|
||||
bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2));
|
||||
// Simple checkerboard: alternate between lighter and darker versions
|
||||
const float LIGHT_FACTOR = 1.2;
|
||||
const float DARK_FACTOR = 0.8;
|
||||
borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;
|
||||
}
|
||||
|
||||
color = borderColor;
|
||||
a = baseBorder.a; // Already in 0-1 range from RGBA8 texture
|
||||
}
|
||||
|
||||
if (u_hoveredPlayerId >= 0.0 && abs(float(owner) - u_hoveredPlayerId) < 0.5) {
|
||||
|
||||
@@ -852,6 +852,22 @@ export class GameImpl implements Game {
|
||||
hasFallout(ref: TileRef): boolean {
|
||||
return this._map.hasFallout(ref);
|
||||
}
|
||||
|
||||
isDefended(ref: TileRef): boolean {
|
||||
return this._map.isDefended(ref);
|
||||
}
|
||||
|
||||
setDefended(ref: TileRef, value: boolean): void {
|
||||
return this._map.setDefended(ref, value);
|
||||
}
|
||||
|
||||
getRelation(ref: TileRef): number {
|
||||
return this._map.getRelation(ref);
|
||||
}
|
||||
|
||||
setRelation(ref: TileRef, relation: number): void {
|
||||
return this._map.setRelation(ref, relation);
|
||||
}
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this._map.isBorder(ref);
|
||||
}
|
||||
|
||||
+38
-14
@@ -27,6 +27,10 @@ export interface GameMap {
|
||||
setOwnerID(ref: TileRef, playerId: number): void;
|
||||
hasFallout(ref: TileRef): boolean;
|
||||
setFallout(ref: TileRef, value: boolean): void;
|
||||
isDefended(ref: TileRef): boolean;
|
||||
setDefended(ref: TileRef, value: boolean): void;
|
||||
getRelation(ref: TileRef): number;
|
||||
setRelation(ref: TileRef, relation: number): void;
|
||||
isOnEdgeOfMap(ref: TileRef): boolean;
|
||||
isBorder(ref: TileRef): boolean;
|
||||
neighbors(ref: TileRef): TileRef[];
|
||||
@@ -72,8 +76,13 @@ export class GameMapImpl implements GameMap {
|
||||
// State bits (Uint16Array)
|
||||
private static readonly PLAYER_ID_MASK = 0xfff;
|
||||
private static readonly FALLOUT_BIT = 13;
|
||||
private static readonly DEFENSE_BONUS_BIT = 14;
|
||||
// Bit 15 still reserved
|
||||
private static readonly DEFENDED_BIT = 12;
|
||||
private static readonly RELATION_MASK = 0xc000; // bits 14-15
|
||||
private static readonly RELATION_SHIFT = 14;
|
||||
// Relation values (stored in bits 14-15)
|
||||
private static readonly RELATION_NEUTRAL = 0;
|
||||
private static readonly RELATION_FRIENDLY = 1;
|
||||
private static readonly RELATION_EMBARGO = 2;
|
||||
|
||||
constructor(
|
||||
width: number,
|
||||
@@ -217,6 +226,33 @@ export class GameMapImpl implements GameMap {
|
||||
}
|
||||
}
|
||||
|
||||
isDefended(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENDED_BIT));
|
||||
}
|
||||
|
||||
setDefended(ref: TileRef, value: boolean): void {
|
||||
if (value) {
|
||||
this.state[ref] |= 1 << GameMapImpl.DEFENDED_BIT;
|
||||
} else {
|
||||
this.state[ref] &= ~(1 << GameMapImpl.DEFENDED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
getRelation(ref: TileRef): number {
|
||||
return (
|
||||
(this.state[ref] & GameMapImpl.RELATION_MASK) >>
|
||||
GameMapImpl.RELATION_SHIFT
|
||||
);
|
||||
}
|
||||
|
||||
setRelation(ref: TileRef, relation: number): void {
|
||||
// Clear existing relation bits
|
||||
this.state[ref] &= ~GameMapImpl.RELATION_MASK;
|
||||
// Set new relation bits
|
||||
this.state[ref] |=
|
||||
(relation << GameMapImpl.RELATION_SHIFT) & GameMapImpl.RELATION_MASK;
|
||||
}
|
||||
|
||||
isOnEdgeOfMap(ref: TileRef): boolean {
|
||||
const x = this.x(ref);
|
||||
const y = this.y(ref);
|
||||
@@ -231,18 +267,6 @@ export class GameMapImpl implements GameMap {
|
||||
);
|
||||
}
|
||||
|
||||
hasDefenseBonus(ref: TileRef): boolean {
|
||||
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENSE_BONUS_BIT));
|
||||
}
|
||||
|
||||
setDefenseBonus(ref: TileRef, value: boolean): void {
|
||||
if (value) {
|
||||
this.state[ref] |= 1 << GameMapImpl.DEFENSE_BONUS_BIT;
|
||||
} else {
|
||||
this.state[ref] &= ~(1 << GameMapImpl.DEFENSE_BONUS_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
isWater(ref: TileRef): boolean {
|
||||
return !this.isLand(ref);
|
||||
|
||||
@@ -854,6 +854,22 @@ export class GameView implements GameMap {
|
||||
setFallout(ref: TileRef, value: boolean): void {
|
||||
return this._map.setFallout(ref, value);
|
||||
}
|
||||
|
||||
isDefended(ref: TileRef): boolean {
|
||||
return this._map.isDefended(ref);
|
||||
}
|
||||
|
||||
setDefended(ref: TileRef, value: boolean): void {
|
||||
return this._map.setDefended(ref, value);
|
||||
}
|
||||
|
||||
getRelation(ref: TileRef): number {
|
||||
return this._map.getRelation(ref);
|
||||
}
|
||||
|
||||
setRelation(ref: TileRef, relation: number): void {
|
||||
return this._map.setRelation(ref, relation);
|
||||
}
|
||||
isBorder(ref: TileRef): boolean {
|
||||
return this._map.isBorder(ref);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user