Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/components/game/Minimap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ export function Minimap() {
// Draw resources
const resources = worldAdapter.getEntitiesWith('Transform', 'Resource');
for (const entity of resources) {
const transform = entity.get('Transform') as { x: number; y: number } | undefined;
const transform = entity.get('Transform') as { x: number; y: number; z: number } | undefined;
const resource = entity.get('Resource') as { resourceType: string } | undefined;

if (!transform || !resource) continue;

const x = transform.x * scale;
const y = transform.y * scale;
const y = transform.z * scale; // Use z (depth) for minimap Y

ctx.fillStyle = resource.resourceType === 'minerals' ? '#00aaff' : '#00ff66';
ctx.beginPath();
Expand All @@ -191,7 +191,7 @@ export function Minimap() {
// Draw buildings
const buildings = worldAdapter.getEntitiesWith('Transform', 'Building', 'Selectable', 'Health');
for (const entity of buildings) {
const transform = entity.get('Transform') as { x: number; y: number } | undefined;
const transform = entity.get('Transform') as { x: number; y: number; z: number } | undefined;
const building = entity.get('Building') as { width: number; height: number; state: string; isComplete?: () => boolean } | undefined;
const selectable = entity.get('Selectable') as { playerId: string } | undefined;
const health = entity.get('Health') as { current: number; max: number } | undefined;
Expand All @@ -202,13 +202,13 @@ export function Minimap() {
// Skip enemy buildings that are not visible due to fog of war (unless spectator)
const fogOfWarEnabled = useGameSetupStore.getState().fogOfWar;
if (localPlayer && selectable.playerId !== localPlayer && fogOfWarEnabled && game.visionSystem && !isSpectating) {
if (!game.visionSystem.isVisible(localPlayer, transform.x, transform.y)) {
if (!game.visionSystem.isVisible(localPlayer, transform.x, transform.z)) {
continue;
}
}

const x = transform.x * scale;
const y = transform.y * scale;
const y = transform.z * scale; // Use z (depth) for minimap Y
const w = Math.max(building.width * scale, 4);
const h = Math.max(building.height * scale, 4);

Expand All @@ -229,7 +229,7 @@ export function Minimap() {
// Draw units
const units = worldAdapter.getEntitiesWith('Transform', 'Unit', 'Selectable', 'Health');
for (const entity of units) {
const transform = entity.get('Transform') as { x: number; y: number } | undefined;
const transform = entity.get('Transform') as { x: number; y: number; z: number } | undefined;
const selectable = entity.get('Selectable') as { playerId: string } | undefined;
const health = entity.get('Health') as { current: number; max: number } | undefined;

Expand All @@ -239,13 +239,13 @@ export function Minimap() {
// Skip enemy units that are not visible due to fog of war (unless spectator)
const fogEnabled = useGameSetupStore.getState().fogOfWar;
if (localPlayer && selectable.playerId !== localPlayer && fogEnabled && game.visionSystem && !isSpectating) {
if (!game.visionSystem.isVisible(localPlayer, transform.x, transform.y)) {
if (!game.visionSystem.isVisible(localPlayer, transform.x, transform.z)) {
continue;
}
}

const x = transform.x * scale;
const y = transform.y * scale;
const y = transform.z * scale; // Use z (depth) for minimap Y

// Unit color based on player's assigned color
const unitPlayerHex = getPlayerColor(selectable.playerId);
Expand Down
50 changes: 25 additions & 25 deletions src/engine/systems/VisionSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ export class VisionSystem extends System {

casters.push({
x: transform.x,
y: transform.y,
y: transform.z, // Use Z (depth) not Y (altitude) for 2D vision grid
sightRange: unit.sightRange,
playerId: playerIndex,
});
Expand All @@ -524,7 +524,7 @@ export class VisionSystem extends System {

casters.push({
x: transform.x,
y: transform.y,
y: transform.z, // Use Z (depth) not Y (altitude) for 2D vision grid
sightRange: building.sightRange,
playerId: playerIndex,
});
Expand Down Expand Up @@ -569,8 +569,8 @@ export class VisionSystem extends System {
if (!transform || !selectable) continue;

const dx = transform.x - tower.x;
const dy = transform.y - tower.y;
const distSq = dx * dx + dy * dy;
const dz = transform.z - tower.y; // tower.y is stored depth, compare with transform.z
const distSq = dx * dx + dz * dz;

if (distSq <= captureRadiusSq) {
tower.controllingPlayers.add(selectable.playerId);
Expand Down Expand Up @@ -699,7 +699,7 @@ export class VisionSystem extends System {
units.push({
id: entity.id,
x: transform.x,
y: transform.y,
y: transform.z, // Use Z (depth) for 2D vision grid
sightRange: unit.sightRange,
playerId: selectable.playerId,
});
Expand Down Expand Up @@ -729,7 +729,7 @@ export class VisionSystem extends System {
buildings.push({
id: entity.id,
x: transform.x,
y: transform.y,
y: transform.z, // Use Z (depth) for 2D vision grid
sightRange: building.sightRange,
playerId: selectable.playerId,
isOperational: building.isOperational(),
Expand Down Expand Up @@ -869,8 +869,8 @@ export class VisionSystem extends System {
if (!transform || !selectable) continue;

const dx = transform.x - tower.x;
const dy = transform.y - tower.y;
const distSq = dx * dx + dy * dy;
const dz = transform.z - tower.y; // tower.y is stored depth, compare with transform.z
const distSq = dx * dx + dz * dz;

if (distSq <= captureRadiusSq) {
tower.controllingPlayers.add(selectable.playerId);
Expand All @@ -883,7 +883,7 @@ export class VisionSystem extends System {
for (const tower of this.watchTowers) {
if (tower.isActive) {
for (const playerId of tower.controllingPlayers) {
this.revealArea(playerId, tower.x, tower.y, tower.radius);
this.revealArea(playerId, tower.x, tower.y, tower.radius); // tower.y is depth (Z)
}
}
}
Expand All @@ -896,7 +896,7 @@ export class VisionSystem extends System {

if (!transform || !unit || !selectable) return;

this.revealArea(selectable.playerId, transform.x, transform.y, unit.sightRange);
this.revealArea(selectable.playerId, transform.x, transform.z, unit.sightRange);
}

private updateBuildingVision(entity: Entity): void {
Expand All @@ -909,10 +909,10 @@ export class VisionSystem extends System {
// Only provide vision if building is operational
if (!building.isOperational()) return;

this.revealArea(selectable.playerId, transform.x, transform.y, building.sightRange);
this.revealArea(selectable.playerId, transform.x, transform.z, building.sightRange);
}

private revealArea(playerId: string, worldX: number, worldY: number, range: number): void {
private revealArea(playerId: string, worldX: number, worldZ: number, range: number): void {
this.ensurePlayerRegistered(playerId);

const visionGrid = this.visionMap.playerVision.get(playerId)!;
Expand All @@ -921,7 +921,7 @@ export class VisionSystem extends System {
const gridHeight = this.visionMap.height;

const cellX = Math.floor(worldX / this.cellSize);
const cellY = Math.floor(worldY / this.cellSize);
const cellY = Math.floor(worldZ / this.cellSize); // cellY maps to worldZ (depth)
const cellRange = Math.ceil(range / this.cellSize);

const cellRangeSq = cellRange * cellRange;
Expand All @@ -948,7 +948,7 @@ export class VisionSystem extends System {
*/
private detectCloakedUnitsInArea(
playerId: string,
position: { x: number; y: number },
position: { x: number; y: number }, // y here is actually Z (depth) in world space
radius: number
): void {
const units = this.world.getEntitiesWith('Unit', 'Transform', 'Selectable');
Expand All @@ -963,34 +963,34 @@ export class VisionSystem extends System {
if (selectable.playerId === playerId) continue;
if (!unit.isCloaked) continue;

// Check if within detection radius
// Check if within detection radius (position.y is depth/Z coordinate)
const dx = transform.x - position.x;
const dy = transform.y - position.y;
const distSq = dx * dx + dy * dy;
const dz = transform.z - position.y; // position.y is Z in 2D map space
const distSq = dx * dx + dz * dz;

if (distSq <= radiusSq) {
// Emit detection event
this.game.eventBus.emit('unit:detected', {
entityId: entity.id,
detectedBy: playerId,
position: { x: transform.x, y: transform.y },
position: { x: transform.x, y: transform.z }, // Report 2D map position
});

debugPathfinding.log(
`[VisionSystem] Detected cloaked unit ${entity.id} at (${transform.x.toFixed(1)}, ${transform.y.toFixed(1)})`
`[VisionSystem] Detected cloaked unit ${entity.id} at (${transform.x.toFixed(1)}, ${transform.z.toFixed(1)})`
);
}
}
}

// Public API for checking visibility

public getVisionState(playerId: string, worldX: number, worldY: number): VisionState {
public getVisionState(playerId: string, worldX: number, worldZ: number): VisionState {
const visionGrid = this.visionMap.playerVision.get(playerId);
if (!visionGrid) return 'unexplored';

const cellX = Math.floor(worldX / this.cellSize);
const cellY = Math.floor(worldY / this.cellSize);
const cellY = Math.floor(worldZ / this.cellSize); // cellY maps to worldZ (depth)

if (cellX < 0 || cellX >= this.visionMap.width || cellY < 0 || cellY >= this.visionMap.height) {
return 'unexplored';
Expand All @@ -999,12 +999,12 @@ export class VisionSystem extends System {
return visionGrid[cellY][cellX];
}

public isVisible(playerId: string, worldX: number, worldY: number): boolean {
return this.getVisionState(playerId, worldX, worldY) === 'visible';
public isVisible(playerId: string, worldX: number, worldZ: number): boolean {
return this.getVisionState(playerId, worldX, worldZ) === 'visible';
}

public isExplored(playerId: string, worldX: number, worldY: number): boolean {
const state = this.getVisionState(playerId, worldX, worldY);
public isExplored(playerId: string, worldX: number, worldZ: number): boolean {
const state = this.getVisionState(playerId, worldX, worldZ);
return state === 'visible' || state === 'explored';
}

Expand Down
2 changes: 1 addition & 1 deletion src/rendering/BuildingRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ export class BuildingRenderer {
// Check visibility for enemy buildings (skip in spectator mode - show all)
let shouldShow = true;
if (isEnemy && this.visionSystem && this.playerId) {
shouldShow = this.visionSystem.isExplored(this.playerId, transform.x, transform.y);
shouldShow = this.visionSystem.isExplored(this.playerId, transform.x, transform.z);
}

// PERF: Get cached terrain height (buildings rarely move)
Expand Down
2 changes: 1 addition & 1 deletion src/rendering/UnitRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ export class UnitRenderer {
// Check visibility for enemy units (skip in spectator mode - show all)
let shouldShow = true;
if (isEnemy && this.visionSystem && this.playerId) {
shouldShow = this.visionSystem.isVisible(this.playerId, transform.x, transform.y);
shouldShow = this.visionSystem.isVisible(this.playerId, transform.x, transform.z);
}

// Skip dead units
Expand Down
11 changes: 6 additions & 5 deletions src/rendering/compute/VisionCompute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,10 @@ export class VisionCompute {
return;
});

// Cell center in world coordinates
// Cell center in world coordinates (X horizontal, Z depth)
// Note: cellY maps to worldZ (depth), not worldY (altitude)
const worldX = cellX.toFloat().mul(cellSize).add(cellSize.mul(0.5));
const worldY = cellY.toFloat().mul(cellSize).add(cellSize.mul(0.5));
const worldZ = cellY.toFloat().mul(cellSize).add(cellSize.mul(0.5));

// Read previous frame's data for temporal accumulation
const prevUV = vec2(
Expand All @@ -218,13 +219,13 @@ export class VisionCompute {
Loop(casterCount, () => {
const caster = casterBuffer.element(i);
const casterX = caster.x;
const casterY = caster.y;
const casterZ = caster.y; // caster.y stores Z (depth) coordinate
const sightRange = caster.z;
const casterPlayer = caster.w.toInt();

const dx = worldX.sub(casterX);
const dy = worldY.sub(casterY);
const distSq = dx.mul(dx).add(dy.mul(dy));
const dz = worldZ.sub(casterZ);
const distSq = dx.mul(dx).add(dz.mul(dz));
const rangeSq = sightRange.mul(sightRange);

If(distSq.lessThanEqual(rangeSq).and(casterPlayer.equal(targetPlayerId)), () => {
Expand Down
12 changes: 6 additions & 6 deletions src/rendering/tsl/FogOfWar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ export class TSLFogOfWar {
// Update CPU texture from vision grid with temporal smoothing
for (let y = 0; y < this.gridHeight; y++) {
for (let x = 0; x < this.gridWidth; x++) {
// Flip Y for texture coordinates
const textureY = this.gridHeight - 1 - y;
const i = textureY * this.gridWidth + x;
// No Y-flip needed - grid y maps directly to texture row
// Grid y=0 (worldZ~0) → texture row 0 (UV v~0)
const i = y * this.gridWidth + x;
const state = visionGrid[y]?.[x] ?? 'unexplored';

// Current visibility (target)
Expand Down Expand Up @@ -281,9 +281,9 @@ export class TSLFogOfWar {
// Update CPU texture from serialized data with temporal smoothing
for (let y = 0; y < this.gridHeight; y++) {
for (let x = 0; x < this.gridWidth; x++) {
// Flip Y for texture coordinates
const textureY = this.gridHeight - 1 - y;
const textureIndex = textureY * this.gridWidth + x;
// No Y-flip needed - grid y maps directly to texture row
// Grid y=0 (worldZ~0) → texture row 0 (UV v~0)
const textureIndex = y * this.gridWidth + x;
const dataIndex = y * this.gridWidth + x;
const state = serializedData[dataIndex];

Expand Down
7 changes: 5 additions & 2 deletions src/rendering/tsl/PostProcessing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,10 +857,13 @@ export class RenderPipeline {

/**
* Set fog of war map dimensions for proper world-space calculations
* @param width Map width in world units
* @param height Map height in world units
* @param cellSize Optional cell size for vision grid (default: 2)
*/
setFogOfWarMapDimensions(width: number, height: number): void {
setFogOfWarMapDimensions(width: number, height: number, cellSize: number = 2): void {
if (this.fogOfWarPass) {
this.fogOfWarPass.uniforms.mapDimensions.value.set(width, height);
this.fogOfWarPass.setMapDimensions(width, height, cellSize);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/rendering/tsl/effects/EffectPasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ export interface FogOfWarPassResult {
updateTime: (t: number) => void;
updateCamera: (cam: THREE.PerspectiveCamera) => void;
applyConfig: (config: Partial<FogOfWarConfig>) => void;
setMapDimensions: (width: number, height: number, cellSize?: number) => void;
}

/**
Expand Down Expand Up @@ -1011,6 +1012,18 @@ export function createFogOfWarPass(
}
};

const setMapDimensions = (width: number, height: number, cellSize?: number) => {
uMapDimensions.value.set(width, height);
if (cellSize !== undefined) {
uCellSize.value = cellSize;
// Update grid dimensions based on map dimensions and cell size
uGridDimensions.value.set(
Math.ceil(width / cellSize),
Math.ceil(height / cellSize)
);
}
};

return {
node: fogOfWarNode(),
uniforms,
Expand All @@ -1019,6 +1032,7 @@ export function createFogOfWarPass(
updateTime,
updateCamera,
applyConfig,
setMapDimensions,
};
}

Expand Down
Loading