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)
This commit is contained in:
MRH
2025-03-24 23:34:14 +01:00
committed by GitHub
parent 5a1451b430
commit 6a82b8eefc
2 changed files with 134 additions and 37 deletions
+77 -35
View File
@@ -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);
+57 -2
View File
@@ -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(