From 6a82b8eefcd68d6e5b00eeb3eeea2fdde90a2c3b Mon Sep 17 00:00:00 2001 From: MRH Date: Mon, 24 Mar 2025 23:34:14 +0100 Subject: [PATCH] Improved structure legibility (#332) Updated structure icon border rendering to improve legibility, I often find that I miss important enemy structures like SAM launchers as they share a similar icon to many other structures. Supported shapes: - Square (SAM, Missile) - Round (City, Port) - Hexagon (Defense post) - Diamond (not used, square rotated 45deg) ![image](https://github.com/user-attachments/assets/85e70f6a-b9d1-4837-b3fb-230d752675db) --- src/client/graphics/layers/StructureLayer.ts | 112 +++++++++++++------ src/core/game/GameMap.ts | 59 +++++++++- 2 files changed, 134 insertions(+), 37 deletions(-) diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 087644746..3bb61bba6 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -12,15 +12,30 @@ import cityIcon from "../../../../resources/images/buildings/cityAlt1.png"; import { GameView, UnitView } from "../../../core/game/GameView"; import { Cell, UnitType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; -import { euclDistFN } from "../../../core/game/GameMap"; +import { + euclDistFN, + manhattanDistFN, + rectDistFN, + hexDistFN, +} from "../../../core/game/GameMap"; const underConstructionColor = colord({ r: 150, g: 150, b: 150 }); const reloadingColor = colord({ r: 255, g: 0, b: 0 }); +type DistanceFunction = typeof euclDistFN; + +enum UnitBorderType { + Round, + Diamond, + Square, + Hexagon, +} + interface UnitRenderConfig { icon: string; borderRadius: number; territoryRadius: number; + borderType: UnitBorderType; } export class StructureLayer implements Layer { @@ -35,26 +50,31 @@ export class StructureLayer implements Layer { icon: anchorIcon, borderRadius: 8.525, territoryRadius: 6.525, + borderType: UnitBorderType.Round, }, [UnitType.City]: { icon: cityIcon, borderRadius: 8.525, territoryRadius: 6.525, + borderType: UnitBorderType.Round, }, [UnitType.MissileSilo]: { icon: missileSiloIcon, - borderRadius: 9.5, - territoryRadius: 6, + borderRadius: 8.525, + territoryRadius: 6.525, + borderType: UnitBorderType.Square, }, [UnitType.DefensePost]: { icon: shieldIcon, - borderRadius: 9.5, - territoryRadius: 6, + borderRadius: 8.525, + territoryRadius: 6.525, + borderType: UnitBorderType.Hexagon, }, [UnitType.SAMLauncher]: { icon: SAMMissileIcon, - borderRadius: 10, - territoryRadius: 6, + borderRadius: 8.525, + territoryRadius: 6.525, + borderType: UnitBorderType.Square, }, }; @@ -68,6 +88,7 @@ export class StructureLayer implements Layer { icon: SAMMissileReloadingIcon, borderRadius: 8.525, territoryRadius: 6.525, + borderType: UnitBorderType.Square, }); } @@ -141,6 +162,51 @@ export class StructureLayer implements Layer { return unitType in this.unitConfigs; } + private drawBorder( + unit: UnitView, + borderColor: Colord, + config: UnitRenderConfig, + distanceFN: DistanceFunction, + ) { + // Draw border and territory + for (const tile of this.game.bfs( + unit.tile(), + distanceFN(unit.tile(), config.borderRadius, true), + )) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + borderColor, + 255, + ); + } + + for (const tile of this.game.bfs( + unit.tile(), + distanceFN(unit.tile(), config.territoryRadius, true), + )) { + this.paintCell( + new Cell(this.game.x(tile), this.game.y(tile)), + unit.type() == UnitType.Construction + ? underConstructionColor + : this.theme.territoryColor(unit.owner().info()), + 130, + ); + } + } + + private getDrawFN(type: UnitBorderType) { + switch (type) { + case UnitBorderType.Round: + return euclDistFN; + case UnitBorderType.Diamond: + return manhattanDistFN; + case UnitBorderType.Square: + return rectDistFN; + case UnitBorderType.Hexagon: + return hexDistFN; + } + } + private handleUnitRendering(unit: UnitView) { const unitType = unit.constructionType() ?? unit.type(); let iconType = unitType; @@ -157,17 +223,16 @@ export class StructureLayer implements Layer { if (!config || !icon) return; + const drawFunction = this.getDrawFN(config.borderType); // Clear previous rendering for (const tile of this.game.bfs( unit.tile(), - euclDistFN(unit.tile(), config.borderRadius, true), + drawFunction(unit.tile(), config.borderRadius, true), )) { this.clearCell(new Cell(this.game.x(tile), this.game.y(tile))); } - if (!unit.isActive()) { - return; - } + if (!unit.isActive()) return; let borderColor = this.theme.borderColor(unit.owner().info()); if (unitType == UnitType.SAMLauncher && unit.isSamCooldown()) { @@ -176,30 +241,7 @@ export class StructureLayer implements Layer { borderColor = underConstructionColor; } - // Draw border and territory - for (const tile of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), config.borderRadius, true), - )) { - this.paintCell( - new Cell(this.game.x(tile), this.game.y(tile)), - borderColor, - 255, - ); - } - - for (const tile of this.game.bfs( - unit.tile(), - euclDistFN(unit.tile(), config.territoryRadius, true), - )) { - this.paintCell( - new Cell(this.game.x(tile), this.game.y(tile)), - unit.type() == UnitType.Construction - ? underConstructionColor - : this.theme.territoryColor(unit.owner().info()), - 130, - ); - } + this.drawBorder(unit, borderColor, config, drawFunction); const startX = this.game.x(unit.tile()) - Math.floor(icon.width / 2); const startY = this.game.y(unit.tile()) - Math.floor(icon.height / 2); diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index 53bb75aa7..e9392fe5f 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -320,7 +320,7 @@ export class GameMapImpl implements GameMap { export function euclDistFN( root: TileRef, dist: number, - center: boolean, + center: boolean = false, ): (gm: GameMap, tile: TileRef) => boolean { if (!center) { return (gm: GameMap, n: TileRef) => gm.euclideanDist(root, n) <= dist; @@ -341,8 +341,63 @@ export function euclDistFN( export function manhattanDistFN( root: TileRef, dist: number, + center: boolean = false, ): (gm: GameMap, tile: TileRef) => boolean { - return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist; + if (!center) { + return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist; + } else { + return (gm: GameMap, n: TileRef) => { + const rootX = gm.x(root) - 0.5; + const rootY = gm.y(root) - 0.5; + const dx = Math.abs(gm.x(n) - rootX); + const dy = Math.abs(gm.y(n) - rootY); + return dx + dy <= dist; + }; + } +} + +export function rectDistFN( + root: TileRef, + dist: number, + center: boolean = false, +): (gm: GameMap, tile: TileRef) => boolean { + if (!center) { + return (gm: GameMap, n: TileRef) => { + const dx = Math.abs(gm.x(n) - gm.x(root)); + const dy = Math.abs(gm.y(n) - gm.y(root)); + return dx <= dist && dy <= dist; + }; + } else { + return (gm: GameMap, n: TileRef) => { + const rootX = gm.x(root) - 0.5; + const rootY = gm.y(root) - 0.5; + const dx = Math.abs(gm.x(n) - rootX); + const dy = Math.abs(gm.y(n) - rootY); + return dx <= dist && dy <= dist; + }; + } +} + +export function hexDistFN( + root: TileRef, + dist: number, + center: boolean = false, +): (gm: GameMap, tile: TileRef) => boolean { + if (!center) { + return (gm: GameMap, n: TileRef) => { + const dx = Math.abs(gm.x(n) - gm.x(root)); + const dy = Math.abs(gm.y(n) - gm.y(root)); + return dx <= dist && dy <= dist && dx + dy <= dist * 1.5; + }; + } else { + return (gm: GameMap, n: TileRef) => { + const rootX = gm.x(root) - 0.5; + const rootY = gm.y(root) - 0.5; + const dx = Math.abs(gm.x(n) - rootX); + const dy = Math.abs(gm.y(n) - rootY); + return dx <= dist && dy <= dist && dx + dy <= dist * 1.5; + }; + } } export function andFN(