diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts index 8e0a973c5..29d0a10c4 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/graphics/layers/StructureIconsLayer.ts @@ -20,11 +20,12 @@ class StructureRenderInfo { public owner: PlayerID, public pixiContainer: PIXI.Container, public level: number = 0, + public underConstruction: boolean = true, ) {} } -const ZOOM_THRESHOLD = 2.8; // below this zoom level, structures are not rendered +const ZOOM_THRESHOLD = 2.5; const ICON_SIZE = 24; -const OFFSET_ZOOM_Y = 15; // offset for the y position of the icon to avoid hiding the structure beneath +const OFFSET_ZOOM_Y = 12; // offset for the y position of the icon to avoid hiding the structure beneath export class StructureIconsLayer implements Layer { private pixicanvas: HTMLCanvasElement; @@ -110,7 +111,7 @@ export class StructureIconsLayer implements Layer { } resizeCanvas() { - if (this.renderer.view) { + if (this.renderer) { this.pixicanvas.width = window.innerWidth; this.pixicanvas.height = window.innerHeight; this.renderer.resize(innerWidth, innerHeight, 1); @@ -143,10 +144,14 @@ export class StructureIconsLayer implements Layer { if (this.seenUnits.has(unitView)) { const render = this.findRenderByUnit(unitView); if (render) { + this.checkForConstructionState(render, unitView); this.checkForOwnershipChange(render, unitView); this.checkForLevelChange(render, unitView); } - } else if (this.structures.has(unitView.type())) { + } else if ( + this.structures.has(unitView.type()) || + unitView.type() === UnitType.Construction + ) { this.addNewStructure(unitView); } } @@ -159,8 +164,23 @@ export class StructureIconsLayer implements Layer { } } + private checkForConstructionState( + render: StructureRenderInfo, + unit: UnitView, + ) { + if ( + render.underConstruction && + render.unit.type() !== UnitType.Construction + ) { + render.underConstruction = false; + render.pixiContainer?.destroy(); + render.pixiContainer = this.createPixiSprite(unit); + this.shouldRedraw = true; + } + } + private checkForOwnershipChange(render: StructureRenderInfo, unit: UnitView) { - if (render && render.owner !== unit.owner().id()) { + if (render.owner !== unit.owner().id()) { render.owner = unit.owner().id(); render.pixiContainer?.destroy(); render.pixiContainer = this.createPixiSprite(unit); @@ -169,7 +189,7 @@ export class StructureIconsLayer implements Layer { } private checkForLevelChange(render: StructureRenderInfo, unit: UnitView) { - if (render && render.level !== unit.level()) { + if (render.level !== unit.level()) { render.level = unit.level(); render.pixiContainer?.destroy(); render.pixiContainer = this.createPixiSprite(unit); @@ -200,22 +220,41 @@ export class StructureIconsLayer implements Layer { } private createTexture(unit: UnitView): PIXI.Texture { - const cacheKey = `${unit.owner().id()}-${unit.type()}`; + const isConstruction = unit.type() === UnitType.Construction; + const constructionType = unit.constructionType(); + if (isConstruction && constructionType === undefined) { + console.warn( + `Unit ${unit.id()} is a construction but has no construction type.`, + ); + return PIXI.Texture.EMPTY; + } + const structureType = isConstruction ? constructionType! : unit.type(); + const cacheKey = isConstruction + ? `construction-${structureType}` + : `${unit.owner().id()}-${structureType}`; if (this.textureCache.has(cacheKey)) { return this.textureCache.get(cacheKey)!; } + const structureCanvas = document.createElement("canvas"); structureCanvas.width = ICON_SIZE; structureCanvas.height = ICON_SIZE; const context = structureCanvas.getContext("2d")!; - context.fillStyle = this.theme - .territoryColor(unit.owner()) - .lighten(0.1) - .toRgbString(); - const borderColor = this.theme - .borderColor(unit.owner()) - .darken(0.2) - .toRgbString(); + + let borderColor: string; + if (isConstruction) { + context.fillStyle = "rgb(198, 198, 198)"; + borderColor = "rgb(128, 127, 127)"; + } else { + context.fillStyle = this.theme + .territoryColor(unit.owner()) + .lighten(0.06) + .toRgbString(); + borderColor = this.theme + .borderColor(unit.owner()) + .darken(0.08) + .toRgbString(); + } context.strokeStyle = borderColor; context.beginPath(); context.arc( @@ -228,9 +267,9 @@ export class StructureIconsLayer implements Layer { context.fill(); context.lineWidth = 1; context.stroke(); - const structureInfo = this.structures.get(unit.type()); + const structureInfo = this.structures.get(structureType); if (!structureInfo?.image) { - console.warn(`Image not loaded for unit type: ${unit.type()}`); + console.warn(`Image not loaded for unit type: ${structureType}`); return PIXI.Texture.from(structureCanvas); } context.drawImage( @@ -266,10 +305,17 @@ export class StructureIconsLayer implements Layer { text.position.y = -ICON_SIZE / 2 - 2; parentContainer.addChild(text); } - parentContainer.position.set( - Math.round(screenPos.x), - Math.round(screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y), - ); + const posX = Math.round(screenPos.x); + let posY = Math.round(screenPos.y); + if (this.transformHandler.scale >= ZOOM_THRESHOLD) { + // Adjust the y position based on zoom level to avoid hiding the structure beneath + posY = Math.round( + screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y, + ); + } else { + posY = Math.round(screenPos.y); + } + parentContainer.position.set(posX, posY); parentContainer.scale.set(Math.min(1, this.transformHandler.scale)); this.stage.addChild(parentContainer); return parentContainer; @@ -298,9 +344,14 @@ export class StructureIconsLayer implements Layer { new Cell(worldX, worldY), ); screenPos.x = Math.round(screenPos.x); - screenPos.y = Math.round( - screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y, - ); + if (this.transformHandler.scale >= ZOOM_THRESHOLD) { + // Adjust the y position based on zoom level to avoid hiding the structure beneath + screenPos.y = Math.round( + screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y, + ); + } else { + screenPos.y = Math.round(screenPos.y); + } // Check if the sprite is on screen (with margin for partial visibility) const margin = ICON_SIZE; @@ -329,6 +380,7 @@ export class StructureIconsLayer implements Layer { unitView.owner().id(), this.createPixiSprite(unitView), unitView.level(), + unitView.type() === UnitType.Construction, ); this.renders.push(render); this.computeNewLocation(render); diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 4c68f222b..2f769183c 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -24,6 +24,7 @@ const selectedUnitColor = colord({ r: 0, g: 255, b: 255 }); const BASE_BORDER_RADIUS = 16.5; const BASE_TERRITORY_RADIUS = 13.5; const RADIUS_SCALE_FACTOR = 0.5; +const ZOOM_THRESHOLD = 2.5; // below this zoom level, structures are not rendered interface UnitRenderConfig { icon: string; @@ -151,6 +152,9 @@ export class StructureLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { + if (this.transformHandler.scale <= ZOOM_THRESHOLD) { + return; + } context.drawImage( this.canvas, -this.game.width() / 2,