diff --git a/src/components/game/Minimap.tsx b/src/components/game/Minimap.tsx index 2ee10797..7976efa0 100644 --- a/src/components/game/Minimap.tsx +++ b/src/components/game/Minimap.tsx @@ -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(); @@ -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; @@ -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); @@ -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; @@ -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); diff --git a/src/engine/systems/VisionSystem.ts b/src/engine/systems/VisionSystem.ts index fd5f5772..279e742a 100644 --- a/src/engine/systems/VisionSystem.ts +++ b/src/engine/systems/VisionSystem.ts @@ -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, }); @@ -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, }); @@ -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); @@ -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, }); @@ -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(), @@ -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); @@ -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) } } } @@ -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 { @@ -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)!; @@ -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; @@ -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'); @@ -963,21 +963,21 @@ 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)})` ); } } @@ -985,12 +985,12 @@ export class VisionSystem extends System { // 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'; @@ -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'; } diff --git a/src/rendering/BuildingRenderer.ts b/src/rendering/BuildingRenderer.ts index 62a3251c..15757ca7 100644 --- a/src/rendering/BuildingRenderer.ts +++ b/src/rendering/BuildingRenderer.ts @@ -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) diff --git a/src/rendering/UnitRenderer.ts b/src/rendering/UnitRenderer.ts index 805f9ddd..63693afb 100644 --- a/src/rendering/UnitRenderer.ts +++ b/src/rendering/UnitRenderer.ts @@ -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 diff --git a/src/rendering/compute/VisionCompute.ts b/src/rendering/compute/VisionCompute.ts index becf166d..5e3046fb 100644 --- a/src/rendering/compute/VisionCompute.ts +++ b/src/rendering/compute/VisionCompute.ts @@ -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( @@ -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)), () => { diff --git a/src/rendering/tsl/FogOfWar.ts b/src/rendering/tsl/FogOfWar.ts index 6904c53e..b1703a10 100644 --- a/src/rendering/tsl/FogOfWar.ts +++ b/src/rendering/tsl/FogOfWar.ts @@ -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) @@ -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]; diff --git a/src/rendering/tsl/PostProcessing.ts b/src/rendering/tsl/PostProcessing.ts index 6277829e..eb218efa 100644 --- a/src/rendering/tsl/PostProcessing.ts +++ b/src/rendering/tsl/PostProcessing.ts @@ -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); } } diff --git a/src/rendering/tsl/effects/EffectPasses.ts b/src/rendering/tsl/effects/EffectPasses.ts index 52cd3859..c0f65566 100644 --- a/src/rendering/tsl/effects/EffectPasses.ts +++ b/src/rendering/tsl/effects/EffectPasses.ts @@ -659,6 +659,7 @@ export interface FogOfWarPassResult { updateTime: (t: number) => void; updateCamera: (cam: THREE.PerspectiveCamera) => void; applyConfig: (config: Partial) => void; + setMapDimensions: (width: number, height: number, cellSize?: number) => void; } /** @@ -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, @@ -1019,6 +1032,7 @@ export function createFogOfWarPass( updateTime, updateCamera, applyConfig, + setMapDimensions, }; }