mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 20:06:46 +00:00
748 lines
22 KiB
TypeScript
748 lines
22 KiB
TypeScript
import { Theme } from "../../../../core/configuration/Config";
|
|
import { UnitType } from "../../../../core/game/Game";
|
|
import { GameView } from "../../../../core/game/GameView";
|
|
|
|
/**
|
|
* Alignment helper for texture uploads.
|
|
*/
|
|
function align(value: number, alignment: number): number {
|
|
return Math.ceil(value / alignment) * alignment;
|
|
}
|
|
|
|
/**
|
|
* Manages authoritative GPU textures and buffers (ground truth data).
|
|
* All compute and render passes read from this data.
|
|
*/
|
|
export class GroundTruthData {
|
|
public static readonly PALETTE_RESERVED_SLOTS = 10;
|
|
public static readonly PALETTE_FALLOUT_INDEX = 0;
|
|
|
|
// Textures
|
|
public readonly stateTexture: GPUTexture;
|
|
public readonly terrainTexture: GPUTexture;
|
|
public readonly terrainDataTexture: GPUTexture;
|
|
public readonly paletteTexture: GPUTexture;
|
|
public readonly defendedTexture: GPUTexture;
|
|
|
|
// Buffers
|
|
public readonly uniformBuffer: GPUBuffer;
|
|
public readonly defenseParamsBuffer: GPUBuffer;
|
|
public readonly terrainParamsBuffer: GPUBuffer;
|
|
public updatesBuffer: GPUBuffer | null = null;
|
|
public defensePostsBuffer: GPUBuffer | null = null;
|
|
|
|
// Staging arrays for buffer uploads
|
|
private updatesStaging: Uint32Array | null = null;
|
|
private defensePostsStaging: Uint32Array | null = null;
|
|
|
|
// Buffer capacities
|
|
private updatesCapacity = 0;
|
|
private defensePostsCapacity = 0;
|
|
|
|
// State tracking
|
|
private readonly mapWidth: number;
|
|
private readonly mapHeight: number;
|
|
private readonly state: Uint16Array;
|
|
private readonly terrainData: Uint8Array;
|
|
private needsStateUpload = true;
|
|
private needsPaletteUpload = true;
|
|
private needsTerrainDataUpload = true;
|
|
private needsTerrainParamsUpload = true;
|
|
private paletteWidth = 1;
|
|
private defensePostsCount = 0;
|
|
private needsDefensePostsUpload = true;
|
|
|
|
// Uniform data arrays
|
|
private readonly uniformData = new Float32Array(12);
|
|
private readonly defenseParamsData = new Uint32Array(4);
|
|
private readonly terrainParamsData = new Float32Array(24); // 6 vec4f: shore, water, shorelineWater, plainsBase, highlandBase, mountainBase
|
|
|
|
// View state (updated by renderer)
|
|
private viewWidth = 1;
|
|
private viewHeight = 1;
|
|
private viewScale = 1;
|
|
private viewOffsetX = 0;
|
|
private viewOffsetY = 0;
|
|
private alternativeView = false;
|
|
private highlightedOwnerId = -1;
|
|
|
|
// Defense state
|
|
private defendedEpoch = 1;
|
|
private lastDefenseRange = -1;
|
|
private lastDefensePostsCount = -1;
|
|
|
|
private constructor(
|
|
private readonly device: GPUDevice,
|
|
private readonly game: GameView,
|
|
private readonly theme: Theme,
|
|
state: Uint16Array,
|
|
terrainData: Uint8Array,
|
|
mapWidth: number,
|
|
mapHeight: number,
|
|
) {
|
|
this.state = state;
|
|
this.terrainData = terrainData;
|
|
this.mapWidth = mapWidth;
|
|
this.mapHeight = mapHeight;
|
|
|
|
const GPUBufferUsage = (globalThis as any).GPUBufferUsage;
|
|
const GPUTextureUsage = (globalThis as any).GPUTextureUsage;
|
|
const UNIFORM = GPUBufferUsage?.UNIFORM ?? 0x40;
|
|
const COPY_DST_BUF = GPUBufferUsage?.COPY_DST ?? 0x8;
|
|
const COPY_DST_TEX = GPUTextureUsage?.COPY_DST ?? 0x2;
|
|
const TEXTURE_BINDING = GPUTextureUsage?.TEXTURE_BINDING ?? 0x4;
|
|
const STORAGE_BINDING = GPUTextureUsage?.STORAGE_BINDING ?? 0x8;
|
|
|
|
// Render uniforms: 3x vec4f = 48 bytes
|
|
this.uniformBuffer = device.createBuffer({
|
|
size: 48,
|
|
usage: UNIFORM | COPY_DST_BUF,
|
|
});
|
|
|
|
// Defense params: 4x u32 = 16 bytes
|
|
this.defenseParamsBuffer = device.createBuffer({
|
|
size: 16,
|
|
usage: UNIFORM | COPY_DST_BUF,
|
|
});
|
|
|
|
// Terrain params: 6x vec4f = 96 bytes (shore, water, shorelineWater, plainsBase, highlandBase, mountainBase)
|
|
this.terrainParamsBuffer = device.createBuffer({
|
|
size: 96,
|
|
usage: UNIFORM | COPY_DST_BUF,
|
|
});
|
|
|
|
// State texture (r32uint)
|
|
this.stateTexture = device.createTexture({
|
|
size: { width: mapWidth, height: mapHeight },
|
|
format: "r32uint",
|
|
usage: COPY_DST_TEX | TEXTURE_BINDING | STORAGE_BINDING,
|
|
});
|
|
|
|
// Defended texture (r32uint)
|
|
this.defendedTexture = device.createTexture({
|
|
size: { width: mapWidth, height: mapHeight },
|
|
format: "r32uint",
|
|
usage: TEXTURE_BINDING | STORAGE_BINDING,
|
|
});
|
|
|
|
// Palette texture (rgba8unorm)
|
|
this.paletteTexture = device.createTexture({
|
|
size: { width: 1, height: 1 },
|
|
format: "rgba8unorm",
|
|
usage: COPY_DST_TEX | TEXTURE_BINDING,
|
|
});
|
|
|
|
// Terrain texture (rgba8unorm) - output of terrain compute shader
|
|
this.terrainTexture = device.createTexture({
|
|
size: { width: mapWidth, height: mapHeight },
|
|
format: "rgba8unorm",
|
|
usage: COPY_DST_TEX | TEXTURE_BINDING | STORAGE_BINDING,
|
|
});
|
|
|
|
// Terrain data texture (r8uint) - input terrain data (read-only in compute shader)
|
|
this.terrainDataTexture = device.createTexture({
|
|
size: { width: mapWidth, height: mapHeight },
|
|
format: "r8uint",
|
|
usage: COPY_DST_TEX | TEXTURE_BINDING,
|
|
});
|
|
}
|
|
|
|
static create(
|
|
device: GPUDevice,
|
|
game: GameView,
|
|
theme: Theme,
|
|
state: Uint16Array,
|
|
): GroundTruthData {
|
|
return new GroundTruthData(
|
|
device,
|
|
game,
|
|
theme,
|
|
state,
|
|
game.terrainDataView(),
|
|
game.width(),
|
|
game.height(),
|
|
);
|
|
}
|
|
|
|
// =====================
|
|
// View state setters
|
|
// =====================
|
|
|
|
setViewSize(width: number, height: number): void {
|
|
this.viewWidth = Math.max(1, Math.floor(width));
|
|
this.viewHeight = Math.max(1, Math.floor(height));
|
|
}
|
|
|
|
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
|
this.viewScale = scale;
|
|
this.viewOffsetX = offsetX;
|
|
this.viewOffsetY = offsetY;
|
|
}
|
|
|
|
setAlternativeView(enabled: boolean): void {
|
|
this.alternativeView = enabled;
|
|
}
|
|
|
|
setHighlightedOwnerId(ownerSmallId: number | null): void {
|
|
this.highlightedOwnerId = ownerSmallId ?? -1;
|
|
}
|
|
|
|
// =====================
|
|
// Upload methods
|
|
// =====================
|
|
|
|
uploadState(): void {
|
|
if (!this.needsStateUpload) {
|
|
return;
|
|
}
|
|
this.needsStateUpload = false;
|
|
|
|
// Convert 16-bit CPU state to 32-bit array
|
|
const u32State = new Uint32Array(this.state.length);
|
|
for (let i = 0; i < this.state.length; i++) {
|
|
u32State[i] = this.state[i];
|
|
}
|
|
|
|
const bytesPerTexel = Uint32Array.BYTES_PER_ELEMENT;
|
|
const fullBytesPerRow = this.mapWidth * bytesPerTexel;
|
|
|
|
if (fullBytesPerRow % 256 === 0) {
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.stateTexture },
|
|
u32State,
|
|
{ bytesPerRow: fullBytesPerRow, rowsPerImage: this.mapHeight },
|
|
{
|
|
width: this.mapWidth,
|
|
height: this.mapHeight,
|
|
depthOrArrayLayers: 1,
|
|
},
|
|
);
|
|
} else {
|
|
// Fallback: upload row-by-row with padding
|
|
const paddedBytesPerRow = align(fullBytesPerRow, 256);
|
|
const scratch = new Uint32Array(paddedBytesPerRow / 4);
|
|
for (let y = 0; y < this.mapHeight; y++) {
|
|
const start = y * this.mapWidth;
|
|
scratch.set(u32State.subarray(start, start + this.mapWidth), 0);
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.stateTexture, origin: { x: 0, y } },
|
|
scratch,
|
|
{ bytesPerRow: paddedBytesPerRow, rowsPerImage: 1 },
|
|
{ width: this.mapWidth, height: 1, depthOrArrayLayers: 1 },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use terrain compute shader instead. This method is kept for fallback.
|
|
*/
|
|
uploadTerrain(): void {
|
|
const bytesPerRow = this.mapWidth * 4;
|
|
const paddedBytesPerRow = align(bytesPerRow, 256);
|
|
const row = new Uint8Array(paddedBytesPerRow);
|
|
|
|
const toByte = (value: number): number =>
|
|
Math.max(0, Math.min(255, Math.round(value)));
|
|
|
|
for (let y = 0; y < this.mapHeight; y++) {
|
|
row.fill(0);
|
|
for (let x = 0; x < this.mapWidth; x++) {
|
|
const tile = y * this.mapWidth + x;
|
|
const rgba = this.theme.terrainColor(this.game, tile).rgba;
|
|
const idx = x * 4;
|
|
row[idx] = toByte(rgba.r);
|
|
row[idx + 1] = toByte(rgba.g);
|
|
row[idx + 2] = toByte(rgba.b);
|
|
row[idx + 3] = 255;
|
|
}
|
|
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.terrainTexture, origin: { x: 0, y } },
|
|
row,
|
|
{ bytesPerRow: paddedBytesPerRow, rowsPerImage: 1 },
|
|
{ width: this.mapWidth, height: 1, depthOrArrayLayers: 1 },
|
|
);
|
|
}
|
|
}
|
|
|
|
uploadTerrainData(): void {
|
|
if (!this.needsTerrainDataUpload) {
|
|
return;
|
|
}
|
|
this.needsTerrainDataUpload = false;
|
|
|
|
const bytesPerRow = this.mapWidth;
|
|
const paddedBytesPerRow = align(bytesPerRow, 256);
|
|
|
|
if (paddedBytesPerRow === bytesPerRow) {
|
|
// Direct upload if already aligned
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.terrainDataTexture },
|
|
this.terrainData,
|
|
{ bytesPerRow, rowsPerImage: this.mapHeight },
|
|
{
|
|
width: this.mapWidth,
|
|
height: this.mapHeight,
|
|
depthOrArrayLayers: 1,
|
|
},
|
|
);
|
|
} else {
|
|
// Row-by-row upload with padding
|
|
const row = new Uint8Array(paddedBytesPerRow);
|
|
for (let y = 0; y < this.mapHeight; y++) {
|
|
row.fill(0);
|
|
const start = y * this.mapWidth;
|
|
row.set(this.terrainData.subarray(start, start + this.mapWidth), 0);
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.terrainDataTexture, origin: { x: 0, y } },
|
|
row,
|
|
{ bytesPerRow: paddedBytesPerRow, rowsPerImage: 1 },
|
|
{ width: this.mapWidth, height: 1, depthOrArrayLayers: 1 },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
uploadTerrainParams(): void {
|
|
if (!this.needsTerrainParamsUpload) {
|
|
return;
|
|
}
|
|
this.needsTerrainParamsUpload = false;
|
|
|
|
// Sample theme colors by finding representative tiles
|
|
// We'll search for a shore tile, water tile, and compute base terrain colors
|
|
let shoreColor = { r: 204, g: 203, b: 158, a: 255 }; // Default pastel
|
|
let waterColor = { r: 70, g: 132, b: 180, a: 255 }; // Default pastel
|
|
let shorelineWaterColor = { r: 100, g: 143, b: 255, a: 255 }; // Default pastel
|
|
|
|
// Find a shore tile (land adjacent to water)
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (this.game.isShore(i)) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
shoreColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find a deep water tile (magnitude > 5) and shoreline water
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (this.game.isWater(i)) {
|
|
if (this.game.isShoreline(i)) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
shorelineWaterColor = color.rgba;
|
|
} else if (this.game.magnitude(i) > 5) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
waterColor = color.rgba;
|
|
}
|
|
if (waterColor.r !== 70 || shorelineWaterColor.r !== 100) {
|
|
// Found both, can break
|
|
if (this.game.isShoreline(i) && this.game.magnitude(i) > 5) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute terrain base colors by sampling at magnitude 0, 10, 20
|
|
// Find a plains tile (magnitude < 10, land, not shore)
|
|
let plainsColor = { r: 190, g: 220, b: 138, a: 255 };
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (
|
|
this.game.isLand(i) &&
|
|
!this.game.isShore(i) &&
|
|
this.game.magnitude(i) < 10
|
|
) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
plainsColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find a highland tile at magnitude 10 (for accurate formula computation)
|
|
let highlandColor = { r: 200, g: 183, b: 138, a: 255 };
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (
|
|
this.game.isLand(i) &&
|
|
!this.game.isShore(i) &&
|
|
this.game.magnitude(i) === 10
|
|
) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
highlandColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
// If no mag 10 found, try any highland tile
|
|
if (highlandColor.r === 200 && highlandColor.g === 183) {
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (
|
|
this.game.isLand(i) &&
|
|
!this.game.isShore(i) &&
|
|
this.game.magnitude(i) >= 10 &&
|
|
this.game.magnitude(i) < 20
|
|
) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
highlandColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store colors as vec4f (RGBA normalized to 0-1)
|
|
// Index 0-3: shore color
|
|
this.terrainParamsData[0] = shoreColor.r / 255;
|
|
this.terrainParamsData[1] = shoreColor.g / 255;
|
|
this.terrainParamsData[2] = shoreColor.b / 255;
|
|
this.terrainParamsData[3] = 1.0;
|
|
|
|
// Index 4-7: water base color
|
|
this.terrainParamsData[4] = waterColor.r / 255;
|
|
this.terrainParamsData[5] = waterColor.g / 255;
|
|
this.terrainParamsData[6] = waterColor.b / 255;
|
|
this.terrainParamsData[7] = 1.0;
|
|
|
|
// Index 8-11: shoreline water color
|
|
this.terrainParamsData[8] = shorelineWaterColor.r / 255;
|
|
this.terrainParamsData[9] = shorelineWaterColor.g / 255;
|
|
this.terrainParamsData[10] = shorelineWaterColor.b / 255;
|
|
this.terrainParamsData[11] = 1.0;
|
|
|
|
// Find a mountain tile at magnitude 20 (for accurate formula computation)
|
|
let mountainColor = { r: 230, g: 230, b: 230, a: 255 };
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (
|
|
this.game.isLand(i) &&
|
|
!this.game.isShore(i) &&
|
|
this.game.magnitude(i) === 20
|
|
) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
mountainColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
// If no mag 20 found, try any mountain tile
|
|
if (mountainColor.r === 230 && mountainColor.g === 230) {
|
|
for (let i = 0; i < Math.min(1000, this.mapWidth * this.mapHeight); i++) {
|
|
if (
|
|
this.game.isLand(i) &&
|
|
!this.game.isShore(i) &&
|
|
this.game.magnitude(i) >= 20
|
|
) {
|
|
const color = this.theme.terrainColor(this.game, i);
|
|
mountainColor = color.rgba;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Index 12-15: plains base color (magnitude 0)
|
|
this.terrainParamsData[12] = plainsColor.r / 255;
|
|
this.terrainParamsData[13] = plainsColor.g / 255;
|
|
this.terrainParamsData[14] = plainsColor.b / 255;
|
|
this.terrainParamsData[15] = 1.0;
|
|
|
|
// Index 16-19: highland base color (magnitude 10)
|
|
this.terrainParamsData[16] = highlandColor.r / 255;
|
|
this.terrainParamsData[17] = highlandColor.g / 255;
|
|
this.terrainParamsData[18] = highlandColor.b / 255;
|
|
this.terrainParamsData[19] = 1.0;
|
|
|
|
// Index 20-23: mountain base color (magnitude 20)
|
|
this.terrainParamsData[20] = mountainColor.r / 255;
|
|
this.terrainParamsData[21] = mountainColor.g / 255;
|
|
this.terrainParamsData[22] = mountainColor.b / 255;
|
|
this.terrainParamsData[23] = 1.0;
|
|
|
|
this.device.queue.writeBuffer(
|
|
this.terrainParamsBuffer,
|
|
0,
|
|
this.terrainParamsData,
|
|
);
|
|
}
|
|
|
|
markTerrainParamsDirty(): void {
|
|
this.needsTerrainParamsUpload = true;
|
|
}
|
|
|
|
uploadPalette(): boolean {
|
|
if (!this.needsPaletteUpload) {
|
|
return false;
|
|
}
|
|
this.needsPaletteUpload = false;
|
|
|
|
let maxSmallId = 0;
|
|
for (const player of this.game.playerViews()) {
|
|
maxSmallId = Math.max(maxSmallId, player.smallID());
|
|
}
|
|
const nextPaletteWidth =
|
|
GroundTruthData.PALETTE_RESERVED_SLOTS + Math.max(1, maxSmallId + 1);
|
|
|
|
let textureRecreated = false;
|
|
if (nextPaletteWidth !== this.paletteWidth) {
|
|
this.paletteWidth = nextPaletteWidth;
|
|
(this.paletteTexture as any).destroy?.();
|
|
const GPUTextureUsage = (globalThis as any).GPUTextureUsage;
|
|
const COPY_DST_TEX = GPUTextureUsage?.COPY_DST ?? 0x2;
|
|
const TEXTURE_BINDING = GPUTextureUsage?.TEXTURE_BINDING ?? 0x4;
|
|
(this as any).paletteTexture = this.device.createTexture({
|
|
size: { width: this.paletteWidth, height: 1 },
|
|
format: "rgba8unorm",
|
|
usage: COPY_DST_TEX | TEXTURE_BINDING,
|
|
});
|
|
textureRecreated = true;
|
|
}
|
|
|
|
const bytes = new Uint8Array(this.paletteWidth * 4);
|
|
|
|
// Store special colors in reserved slots (0-9)
|
|
const falloutIdx = GroundTruthData.PALETTE_FALLOUT_INDEX * 4;
|
|
bytes[falloutIdx] = 120;
|
|
bytes[falloutIdx + 1] = 255;
|
|
bytes[falloutIdx + 2] = 71;
|
|
bytes[falloutIdx + 3] = 255;
|
|
|
|
// Store player colors starting at index 10
|
|
for (const player of this.game.playerViews()) {
|
|
const id = player.smallID();
|
|
if (id <= 0) continue;
|
|
const rgba = player.territoryColor().rgba;
|
|
const idx = (GroundTruthData.PALETTE_RESERVED_SLOTS + id) * 4;
|
|
bytes[idx] = rgba.r;
|
|
bytes[idx + 1] = rgba.g;
|
|
bytes[idx + 2] = rgba.b;
|
|
bytes[idx + 3] = 255;
|
|
}
|
|
|
|
const bytesPerRow = align(this.paletteWidth * 4, 256);
|
|
const padded =
|
|
bytesPerRow === this.paletteWidth * 4
|
|
? bytes
|
|
: (() => {
|
|
const tmp = new Uint8Array(bytesPerRow);
|
|
tmp.set(bytes);
|
|
return tmp;
|
|
})();
|
|
|
|
this.device.queue.writeTexture(
|
|
{ texture: this.paletteTexture },
|
|
padded,
|
|
{ bytesPerRow, rowsPerImage: 1 },
|
|
{ width: this.paletteWidth, height: 1, depthOrArrayLayers: 1 },
|
|
);
|
|
|
|
return textureRecreated;
|
|
}
|
|
|
|
uploadDefensePosts(): void {
|
|
if (!this.needsDefensePostsUpload) {
|
|
return;
|
|
}
|
|
this.needsDefensePostsUpload = false;
|
|
|
|
const posts = this.collectDefensePosts();
|
|
this.defensePostsCount = posts.length;
|
|
|
|
if (this.defensePostsCount > 0) {
|
|
this.ensureDefensePostsBuffer(this.defensePostsCount);
|
|
}
|
|
|
|
if (
|
|
this.defensePostsCount > 0 &&
|
|
this.defensePostsStaging &&
|
|
this.defensePostsBuffer
|
|
) {
|
|
for (let i = 0; i < this.defensePostsCount; i++) {
|
|
const p = posts[i];
|
|
this.defensePostsStaging[i * 3] = p.x >>> 0;
|
|
this.defensePostsStaging[i * 3 + 1] = p.y >>> 0;
|
|
this.defensePostsStaging[i * 3 + 2] = p.ownerId >>> 0;
|
|
}
|
|
this.device.queue.writeBuffer(
|
|
this.defensePostsBuffer,
|
|
0,
|
|
this.defensePostsStaging.subarray(0, this.defensePostsCount * 3),
|
|
);
|
|
}
|
|
}
|
|
|
|
private collectDefensePosts(): Array<{
|
|
x: number;
|
|
y: number;
|
|
ownerId: number;
|
|
}> {
|
|
const posts: Array<{ x: number; y: number; ownerId: number }> = [];
|
|
const units = this.game.units(UnitType.DefensePost) as any[];
|
|
for (const u of units) {
|
|
if (!u.isActive() || u.isUnderConstruction()) {
|
|
continue;
|
|
}
|
|
const tile = u.tile();
|
|
posts.push({
|
|
x: this.game.x(tile),
|
|
y: this.game.y(tile),
|
|
ownerId: u.owner().smallID(),
|
|
});
|
|
}
|
|
return posts;
|
|
}
|
|
|
|
private ensureDefensePostsBuffer(capacity: number): void {
|
|
if (this.defensePostsBuffer && capacity <= this.defensePostsCapacity) {
|
|
return;
|
|
}
|
|
|
|
const GPUBufferUsage = (globalThis as any).GPUBufferUsage;
|
|
const STORAGE = GPUBufferUsage?.STORAGE ?? 0x10;
|
|
const COPY_DST_BUF = GPUBufferUsage?.COPY_DST ?? 0x8;
|
|
|
|
this.defensePostsCapacity = Math.max(
|
|
8,
|
|
Math.pow(2, Math.ceil(Math.log2(Math.max(1, capacity)))),
|
|
);
|
|
|
|
const bytesPerPost = 12; // 3 * u32
|
|
const bufferSize = this.defensePostsCapacity * bytesPerPost;
|
|
|
|
if (this.defensePostsBuffer) {
|
|
(this.defensePostsBuffer as any).destroy?.();
|
|
}
|
|
|
|
(this as any).defensePostsBuffer = this.device.createBuffer({
|
|
size: bufferSize,
|
|
usage: STORAGE | COPY_DST_BUF,
|
|
});
|
|
|
|
this.defensePostsStaging = new Uint32Array(this.defensePostsCapacity * 3);
|
|
}
|
|
|
|
ensureUpdatesBuffer(capacity: number): GPUBuffer {
|
|
if (this.updatesBuffer && capacity <= this.updatesCapacity) {
|
|
return this.updatesBuffer;
|
|
}
|
|
|
|
const GPUBufferUsage = (globalThis as any).GPUBufferUsage;
|
|
const STORAGE = GPUBufferUsage?.STORAGE ?? 0x10;
|
|
const COPY_DST_BUF = GPUBufferUsage?.COPY_DST ?? 0x8;
|
|
|
|
this.updatesCapacity = Math.max(
|
|
256,
|
|
Math.pow(2, Math.ceil(Math.log2(capacity))),
|
|
);
|
|
const bufferSize = this.updatesCapacity * 8; // Each update is 8 bytes
|
|
|
|
if (this.updatesBuffer) {
|
|
(this.updatesBuffer as any).destroy?.();
|
|
}
|
|
|
|
(this as any).updatesBuffer = this.device.createBuffer({
|
|
size: bufferSize,
|
|
usage: STORAGE | COPY_DST_BUF,
|
|
});
|
|
|
|
this.updatesStaging = new Uint32Array(this.updatesCapacity * 2);
|
|
return this.updatesBuffer;
|
|
}
|
|
|
|
getUpdatesStaging(): Uint32Array {
|
|
this.updatesStaging ??= new Uint32Array(this.updatesCapacity * 2);
|
|
return this.updatesStaging;
|
|
}
|
|
|
|
// =====================
|
|
// Uniform buffer updates
|
|
// =====================
|
|
|
|
writeUniformBuffer(timeSec: number): void {
|
|
this.uniformData[0] = this.mapWidth;
|
|
this.uniformData[1] = this.mapHeight;
|
|
this.uniformData[2] = this.viewScale;
|
|
this.uniformData[3] = timeSec;
|
|
this.uniformData[4] = this.viewOffsetX;
|
|
this.uniformData[5] = this.viewOffsetY;
|
|
this.uniformData[6] = this.alternativeView ? 1 : 0;
|
|
this.uniformData[7] = this.highlightedOwnerId;
|
|
this.uniformData[8] = this.viewWidth;
|
|
this.uniformData[9] = this.viewHeight;
|
|
this.uniformData[10] = 0;
|
|
this.uniformData[11] = 0;
|
|
|
|
this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData);
|
|
}
|
|
|
|
writeDefenseParamsBuffer(): void {
|
|
const range = this.game.config().defensePostRange() >>> 0;
|
|
this.defenseParamsData[0] = range;
|
|
this.defenseParamsData[1] = this.defensePostsCount >>> 0;
|
|
this.defenseParamsData[2] = this.defendedEpoch >>> 0;
|
|
this.defenseParamsData[3] = 0;
|
|
this.device.queue.writeBuffer(
|
|
this.defenseParamsBuffer,
|
|
0,
|
|
this.defenseParamsData,
|
|
);
|
|
}
|
|
|
|
// =====================
|
|
// State getters/setters
|
|
// =====================
|
|
|
|
getDefendedEpoch(): number {
|
|
return this.defendedEpoch;
|
|
}
|
|
|
|
incrementDefendedEpoch(): void {
|
|
this.defendedEpoch = (this.defendedEpoch + 1) >>> 0;
|
|
if (this.defendedEpoch === 0) {
|
|
this.defendedEpoch = 1;
|
|
}
|
|
}
|
|
|
|
getDefensePostsCount(): number {
|
|
return this.defensePostsCount;
|
|
}
|
|
|
|
getLastDefenseRange(): number {
|
|
return this.lastDefenseRange;
|
|
}
|
|
|
|
setLastDefenseRange(range: number): void {
|
|
this.lastDefenseRange = range;
|
|
}
|
|
|
|
getLastDefensePostsCount(): number {
|
|
return this.lastDefensePostsCount;
|
|
}
|
|
|
|
setLastDefensePostsCount(count: number): void {
|
|
this.lastDefensePostsCount = count;
|
|
}
|
|
|
|
markPaletteDirty(): void {
|
|
this.needsPaletteUpload = true;
|
|
}
|
|
|
|
markDefensePostsDirty(): void {
|
|
this.needsDefensePostsUpload = true;
|
|
}
|
|
|
|
getState(): Uint16Array {
|
|
return this.state;
|
|
}
|
|
|
|
getMapWidth(): number {
|
|
return this.mapWidth;
|
|
}
|
|
|
|
getMapHeight(): number {
|
|
return this.mapHeight;
|
|
}
|
|
|
|
getGame(): GameView {
|
|
return this.game;
|
|
}
|
|
|
|
getTheme(): Theme {
|
|
return this.theme;
|
|
}
|
|
}
|