## Description:

Add a new pixi layer for rendering structure icons
Add new sprites for structures

![image](https://github.com/user-attachments/assets/d5171b31-c83b-431a-a0f6-87b85b460a3f)

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Vivacious Box

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Vivacious Box
2025-06-28 02:00:01 +02:00
committed by GitHub
parent 09bd6312e3
commit 77dddbf3ee
38 changed files with 1312 additions and 231 deletions
+27 -15
View File
@@ -28,6 +28,7 @@ import { RailroadLayer } from "./layers/RailroadLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SpawnAd } from "./layers/SpawnAd";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
@@ -227,6 +228,7 @@ export function createRenderer(
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
new RailroadLayer(game),
structureLayer,
new StructureIconsLayer(game, transformHandler),
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
@@ -312,6 +314,7 @@ export class GameRenderer {
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.transformHandler.updateCanvasBoundingRect();
//this.redraw()
}
@@ -325,24 +328,33 @@ export class GameRenderer {
.toHex();
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Save the current context state
this.context.save();
this.transformHandler.handleTransform(this.context);
this.layers.forEach((l) => {
if (l.shouldTransform?.()) {
l.renderLayer?.(this.context);
const handleTransformState = (
needsTransform: boolean,
active: boolean,
): boolean => {
if (needsTransform && !active) {
this.context.save();
this.transformHandler.handleTransform(this.context);
return true;
} else if (!needsTransform && active) {
this.context.restore();
return false;
}
});
return active;
};
this.context.restore();
let isTransformActive = false;
this.layers.forEach((l) => {
if (!l.shouldTransform?.()) {
l.renderLayer?.(this.context);
}
});
for (const layer of this.layers) {
const needsTransform = layer.shouldTransform?.() ?? false;
isTransformActive = handleTransformState(
needsTransform,
isTransformActive,
);
layer.renderLayer?.(this.context);
}
handleTransformState(false, isTransformActive); // Ensure context is clean after rendering
this.transformHandler.resetChanged();
requestAnimationFrame(() => this.renderGame());
+10 -2
View File
@@ -14,6 +14,7 @@ export const CAMERA_SMOOTHING = 0.03;
export class TransformHandler {
public scale: number = 1.8;
private _boundingRect: DOMRect;
private offsetX: number = -350;
private offsetY: number = -200;
private lastGoToCallTime: number | null = null;
@@ -27,6 +28,7 @@ export class TransformHandler {
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
) {
this._boundingRect = this.canvas.getBoundingClientRect();
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
this.eventBus.on(DragEvent, (e) => this.onMove(e));
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e));
@@ -35,8 +37,12 @@ export class TransformHandler {
this.eventBus.on(CenterCameraEvent, () => this.centerCamera());
}
public updateCanvasBoundingRect() {
this._boundingRect = this.canvas.getBoundingClientRect();
}
boundingRect(): DOMRect {
return this.canvas.getBoundingClientRect();
return this._boundingRect;
}
width(): number {
@@ -45,6 +51,9 @@ export class TransformHandler {
hasChanged(): boolean {
return this.changed;
}
resetChanged() {
this.changed = false;
}
handleTransform(context: CanvasRenderingContext2D) {
// Disable image smoothing for pixelated effect
@@ -59,7 +68,6 @@ export class TransformHandler {
this.game.width() / 2 - this.offsetX * this.scale,
this.game.height() / 2 - this.offsetY * this.scale,
);
this.changed = false;
}
worldToScreenCoordinates(cell: Cell): { x: number; y: number } {
+2 -2
View File
@@ -5,12 +5,12 @@ import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
import mirvIcon from "../../../../resources/images/MIRVIcon.svg";
import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg";
import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import missileSiloIcon from "../../../../resources/non-commercial/svg/MissileSiloIconWhite.svg";
import samlauncherIcon from "../../../../resources/non-commercial/svg/SamLauncherIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Cell, Gold, PlayerActions, UnitType } from "../../../core/game/Game";
@@ -0,0 +1,300 @@
import * as PIXI from "pixi.js";
import anchorIcon from "../../../../resources/images/AnchorIcon.png";
import cityIcon from "../../../../resources/images/CityIcon.png";
import factoryIcon from "../../../../resources/images/FactoryUnit.png";
import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import SAMMissileIcon from "../../../../resources/images/SamLauncherUnit.png";
import shieldIcon from "../../../../resources/images/ShieldIcon.png";
import { Theme } from "../../../core/configuration/Config";
import { Cell, PlayerID, UnitType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
class StructureRenderInfo {
public isOnScreen: boolean = false;
constructor(
public unit: UnitView,
public owner: PlayerID,
public pixiSprite: PIXI.Sprite,
) {}
}
const ZOOM_THRESHOLD = 2.8; // below this zoom level, structures are not rendered
const ICON_SIZE = 24;
const OFFSET_ZOOM_Y = 15; // offset for the y position of the icon to avoid hiding the structure beneath
export class StructureIconsLayer implements Layer {
private pixicanvas: HTMLCanvasElement;
private stage: PIXI.Container;
private shouldRedraw: boolean = true;
private textureCache: Map<string, PIXI.Texture> = new Map();
private theme: Theme;
private renderer: PIXI.Renderer;
private renders: StructureRenderInfo[] = [];
private seenUnits: Set<UnitView> = new Set();
private structures: Map<
UnitType,
{ iconPath: string; image: HTMLImageElement | null }
> = new Map([
[UnitType.City, { iconPath: cityIcon, image: null }],
[UnitType.Factory, { iconPath: factoryIcon, image: null }],
[UnitType.DefensePost, { iconPath: shieldIcon, image: null }],
[UnitType.Port, { iconPath: anchorIcon, image: null }],
[UnitType.MissileSilo, { iconPath: missileSiloIcon, image: null }],
[UnitType.SAMLauncher, { iconPath: SAMMissileIcon, image: null }],
]);
constructor(
private game: GameView,
private transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
this.structures.forEach((u, unitType) => this.loadIcon(u, unitType));
}
async setupRenderer() {
this.renderer = new PIXI.WebGLRenderer();
this.pixicanvas = document.createElement("canvas");
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
this.stage = new PIXI.Container();
this.stage.position.set(0, 0);
this.stage.width = this.pixicanvas.width;
this.stage.height = this.pixicanvas.height;
await this.renderer.init({
canvas: this.pixicanvas,
resolution: 1,
width: this.pixicanvas.width,
height: this.pixicanvas.height,
clearBeforeRender: true,
backgroundAlpha: 0,
backgroundColor: 0x00000000,
});
}
private loadIcon(
unitInfo: {
iconPath: string;
image: HTMLImageElement | null;
},
unitType: UnitType,
) {
const image = new Image();
image.src = unitInfo.iconPath;
image.onload = () => {
unitInfo.image = image;
};
image.onerror = () => {
console.error(
`Failed to load icon for ${unitType}: ${unitInfo.iconPath}`,
);
};
}
shouldTransform(): boolean {
return false;
}
async init() {
window.addEventListener("resize", () => this.resizeCanvas());
await this.setupRenderer();
this.redraw();
}
resizeCanvas() {
if (this.renderer.view) {
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
this.renderer.resize(innerWidth, innerHeight, 1);
this.shouldRedraw = true;
}
}
public tick() {
this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
?.forEach((unitView) => {
if (unitView === undefined) return;
if (unitView.isActive()) {
if (this.seenUnits.has(unitView)) {
// check if owner has changed
const render = this.renders.find(
(r) => r.unit.id() === unitView.id(),
);
if (render) {
this.ownerChangeCheck(render, unitView);
}
} else if (this.structures.has(unitView.type())) {
// new unit, create render info
this.seenUnits.add(unitView);
const render = new StructureRenderInfo(
unitView,
unitView.owner().id(),
this.createPixiSprite(unitView),
);
this.renders.push(render);
this.computeNewLocation(render);
this.shouldRedraw = true;
}
}
if (!unitView.isActive() && this.seenUnits.has(unitView)) {
const render = this.renders.find(
(r) => r.unit.id() === unitView.id(),
);
if (render) {
this.deleteStructure(render);
}
this.shouldRedraw = true;
return;
}
});
}
redraw() {
this.resizeCanvas();
}
renderLayer(mainContext: CanvasRenderingContext2D) {
if (!this.renderer || this.transformHandler.scale > ZOOM_THRESHOLD) {
return;
}
if (this.transformHandler.hasChanged()) {
for (const render of this.renders) {
this.computeNewLocation(render);
}
}
if (this.transformHandler.hasChanged() || this.shouldRedraw) {
this.renderer.render(this.stage);
this.shouldRedraw = false;
}
mainContext.drawImage(this.renderer.canvas, 0, 0);
}
private ownerChangeCheck(render: StructureRenderInfo, unit: UnitView) {
if (render.owner !== unit.owner().id()) {
render.owner = unit.owner().id();
render.pixiSprite?.destroy();
render.pixiSprite = this.createPixiSprite(unit);
this.shouldRedraw = true;
}
}
private createTexture(unit: UnitView): PIXI.Texture {
const cacheKey = `${unit.owner().id()}-${unit.type()}`;
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();
context.strokeStyle = borderColor;
context.beginPath();
context.arc(
ICON_SIZE / 2,
ICON_SIZE / 2,
ICON_SIZE / 2 - 1,
0,
Math.PI * 2,
);
context.fill();
context.lineWidth = 1;
context.stroke();
const structureInfo = this.structures.get(unit.type());
if (!structureInfo?.image) {
console.warn(`Image not loaded for unit type: ${unit.type()}`);
return PIXI.Texture.from(structureCanvas);
}
context.drawImage(
this.getImageColored(structureInfo.image, borderColor),
4,
4,
);
const texture = PIXI.Texture.from(structureCanvas);
this.textureCache.set(cacheKey, texture);
return texture;
}
private createPixiSprite(unit: UnitView): PIXI.Sprite {
const sprite = new PIXI.Sprite(this.createTexture(unit));
sprite.anchor.set(0.5, 0.5);
const tile = unit.tile();
const worldX = this.game.x(tile);
const worldY = this.game.y(tile);
const screenPos = this.transformHandler.worldToScreenCoordinates(
new Cell(worldX, worldY),
);
sprite.x = screenPos.x;
sprite.y = screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y;
sprite.scale.set(Math.min(1, this.transformHandler.scale));
this.stage.addChild(sprite);
return sprite;
}
private getImageColored(
image: HTMLImageElement,
color: string,
): HTMLCanvasElement {
const imageCanvas = document.createElement("canvas");
imageCanvas.width = image.width;
imageCanvas.height = image.height;
const ctx = imageCanvas.getContext("2d")!;
ctx.fillStyle = color;
ctx.fillRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(image, 0, 0);
return imageCanvas;
}
private computeNewLocation(render: StructureRenderInfo) {
const tile = render.unit.tile();
const worldX = this.game.x(tile);
const worldY = this.game.y(tile);
const screenPos = this.transformHandler.worldToScreenCoordinates(
new Cell(worldX, worldY),
);
screenPos.x = Math.round(screenPos.x);
screenPos.y = Math.round(
screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y,
);
// Check if the sprite is on screen (with margin for partial visibility)
const margin = ICON_SIZE;
const onScreen =
screenPos.x + margin > 0 &&
screenPos.x - margin < this.pixicanvas.width &&
screenPos.y + margin > 0 &&
screenPos.y - margin < this.pixicanvas.height;
if (onScreen) {
render.pixiSprite.x = screenPos.x;
render.pixiSprite.y = screenPos.y;
render.pixiSprite.scale.set(Math.min(1, this.transformHandler.scale));
}
if (render.isOnScreen !== onScreen) {
// prevent unnecessary updates
render.isOnScreen = onScreen;
render.pixiSprite.visible = onScreen;
}
}
private deleteStructure(render: StructureRenderInfo) {
render.pixiSprite?.destroy();
this.renders = this.renders.filter((r) => r.unit !== render.unit);
this.seenUnits.delete(render.unit);
}
}
+15 -78
View File
@@ -6,26 +6,18 @@ import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { UnitInfoModal } from "./UnitInfoModal";
import cityIcon from "../../../../resources/images/buildings/cityAlt1.png";
import factoryIcon from "../../../../resources/images/buildings/factoryAlt1.png";
import shieldIcon from "../../../../resources/images/buildings/fortAlt2.png";
import anchorIcon from "../../../../resources/images/buildings/port1.png";
import MissileSiloReloadingIcon from "../../../../resources/images/buildings/silo1-reloading.png";
import missileSiloIcon from "../../../../resources/images/buildings/silo1.png";
import SAMMissileReloadingIcon from "../../../../resources/images/buildings/silo4-reloading.png";
import SAMMissileIcon from "../../../../resources/images/buildings/silo4.png";
import cityIcon from "../../../../resources/non-commercial/images/buildings/cityAlt1.png";
import factoryIcon from "../../../../resources/non-commercial/images/buildings/factoryAlt1.png";
import shieldIcon from "../../../../resources/non-commercial/images/buildings/fortAlt3.png";
import anchorIcon from "../../../../resources/non-commercial/images/buildings/port1.png";
import missileSiloIcon from "../../../../resources/non-commercial/images/buildings/silo1.png";
import SAMMissileIcon from "../../../../resources/non-commercial/images/buildings/silo4.png";
import { Cell, UnitType } from "../../../core/game/Game";
import {
euclDistFN,
hexDistFN,
manhattanDistFN,
rectDistFN,
} from "../../../core/game/GameMap";
import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
const underConstructionColor = colord({ r: 150, g: 150, b: 150 });
const reloadingColor = colord({ r: 255, g: 0, b: 0 });
const selectedUnitColor = colord({ r: 0, g: 255, b: 255 });
// Base radius values and scaling factor for unit borders and territories
@@ -33,20 +25,10 @@ const BASE_BORDER_RADIUS = 16.5;
const BASE_TERRITORY_RADIUS = 13.5;
const RADIUS_SCALE_FACTOR = 0.5;
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 {
@@ -65,37 +47,31 @@ export class StructureLayer implements Layer {
icon: anchorIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Round,
},
[UnitType.City]: {
icon: cityIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Round,
},
[UnitType.Factory]: {
icon: factoryIcon,
borderRadius: 8.525,
territoryRadius: 6.525,
borderType: UnitBorderType.Round,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
},
[UnitType.MissileSilo]: {
icon: missileSiloIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Square,
},
[UnitType.DefensePost]: {
icon: shieldIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Hexagon,
},
[UnitType.SAMLauncher]: {
icon: SAMMissileIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Square,
},
};
@@ -117,18 +93,6 @@ export class StructureLayer implements Layer {
if (tempContext === null) throw new Error("2d context not supported");
this.tempContext = tempContext;
this.loadIconData();
this.loadIcon("reloadingSam", {
icon: SAMMissileReloadingIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Square,
});
this.loadIcon("reloadingSilo", {
icon: MissileSiloReloadingIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
borderType: UnitBorderType.Square,
});
}
private loadIcon(unitType: string, config: UnitRenderConfig) {
@@ -204,12 +168,11 @@ export class StructureLayer implements Layer {
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),
isometricDistFN(unit.tile(), config.borderRadius, true),
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
@@ -220,7 +183,7 @@ export class StructureLayer implements Layer {
for (const tile of this.game.bfs(
unit.tile(),
distanceFN(unit.tile(), config.territoryRadius, true),
isometricDistFN(unit.tile(), config.territoryRadius, true),
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
@@ -232,19 +195,6 @@ export class StructureLayer implements Layer {
}
}
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();
const iconType = unitType;
@@ -255,13 +205,7 @@ export class StructureLayer implements Layer {
let borderColor = this.theme.borderColor(unit.owner());
// Handle cooldown states and special icons
if (unitType === UnitType.SAMLauncher && unit.isInCooldown()) {
icon = this.unitIcons.get("reloadingSam");
borderColor = reloadingColor;
} else if (unitType === UnitType.MissileSilo && unit.isInCooldown()) {
icon = this.unitIcons.get("reloadingSilo");
borderColor = reloadingColor;
} else if (unit.type() === UnitType.Construction) {
if (unit.type() === UnitType.Construction) {
icon = this.unitIcons.get(iconType);
borderColor = underConstructionColor;
} else {
@@ -270,11 +214,10 @@ 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(),
drawFunction(unit.tile(), config.borderRadius, true),
euclDistFN(unit.tile(), config.borderRadius + 1, true),
)) {
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
}
@@ -284,8 +227,7 @@ export class StructureLayer implements Layer {
if (this.selectedStructureUnit === unit) {
borderColor = selectedUnitColor;
}
this.drawBorder(unit, borderColor, config, drawFunction);
this.drawBorder(unit, borderColor, config);
// Render icon at 1/2 scale for better quality
const scaledWidth = icon.width >> 1;
@@ -293,7 +235,7 @@ export class StructureLayer implements Layer {
const startX = this.game.x(unit.tile()) - (scaledWidth >> 1);
const startY = this.game.y(unit.tile()) - (scaledHeight >> 1);
this.renderIcon(icon, startX, startY, scaledWidth, scaledHeight, unit);
this.renderIcon(icon, startX, startY - 4, scaledWidth, scaledHeight, unit);
}
private renderIcon(
@@ -320,11 +262,6 @@ export class StructureLayer implements Layer {
// Draw the image at final size with high quality scaling
this.tempContext.drawImage(image, 0, 0, width * 2, height * 2);
// Apply color tinting using multiply blend mode
this.tempContext.globalCompositeOperation = "multiply";
this.tempContext.fillStyle = color.toRgbString();
this.tempContext.fillRect(0, 0, width * 2, height * 2);
// Restore the alpha channel
this.tempContext.globalCompositeOperation = "destination-in";
this.tempContext.drawImage(image, 0, 0, width * 2, height * 2);
+3 -3
View File
@@ -19,7 +19,7 @@ const COLOR_PROGRESSION = [
"rgb(44, 239, 18)",
];
const HEALTHBAR_WIDTH = 11; // Width of the health bar
const LOADINGBAR_WIDTH = 18; // Width of the loading bar
const LOADINGBAR_WIDTH = 14; // Width of the loading bar
const PROGRESSBAR_HEIGHT = 3; // Height of a bar
/**
@@ -378,8 +378,8 @@ export class UILayer implements Layer {
const progressBar = new ProgressBar(
COLOR_PROGRESSION,
this.context,
this.game.x(unit.tile()) - 8,
this.game.y(unit.tile()) - 10,
this.game.x(unit.tile()) - 6,
this.game.y(unit.tile()) + 6,
LOADINGBAR_WIDTH,
PROGRESSBAR_HEIGHT,
0,
+4 -4
View File
@@ -376,13 +376,13 @@ label.option-card:hover {
}
#helpModal .missile-silo-icon {
mask: url("../../resources/images/MissileSiloIconWhite.svg") no-repeat
center / cover;
mask: url("../../resources/non-commercial/svg/MissileSiloIconWhite.svg")
no-repeat center / cover;
}
#helpModal .sam-launcher-icon {
mask: url("../../resources/images/SamLauncherIconWhite.svg") no-repeat
center / cover;
mask: url("../../resources/non-commercial/svg/SamLauncherIconWhite.svg")
no-repeat center / cover;
}
#helpModal .atom-bomb-icon {
+33
View File
@@ -401,6 +401,39 @@ export function rectDistFN(
}
}
function isInIsometricTile(
center: { x: number; y: number },
tile: { x: number; y: number },
yOffset: number,
distance: number,
): boolean {
const dx = Math.abs(tile.x - center.x);
const dy = Math.abs(tile.y - (center.y + yOffset));
return dx + dy * 2 <= distance + 1;
}
export function isometricDistFN(
root: TileRef,
dist: number,
center: boolean = false,
): (gm: GameMap, tile: TileRef) => boolean {
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;
return isInIsometricTile(
{ x: rootX, y: rootY },
{ x: gm.x(n), y: gm.y(n) },
0,
dist,
);
};
}
}
export function hexDistFN(
root: TileRef,
dist: number,