## Description:

Make the unit display bar a proper unit build bar
Add shortcuts for all structures and units
Add ranges for ranged structures and units
Changed the shortcuts to use the key instead of the code for
internationalization purposes


![buildbar](https://github.com/user-attachments/assets/6407dc9c-14b4-40cc-8faa-cdd9e88c9fd2)
<img width="285" height="517" alt="image"
src="https://github.com/user-attachments/assets/91bb01e6-e48c-4255-ace1-306af9cdc25b"
/>

## 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

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

Mr.Box

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
Co-authored-by: icslucas <carolinacarazolli@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Vivacious Box
2025-10-02 21:38:28 +02:00
committed by GitHub
parent 6061c97d78
commit 311d43ab4f
17 changed files with 1283 additions and 515 deletions
+304 -396
View File
@@ -3,25 +3,44 @@ import a11yPlugin from "colord/plugins/a11y";
import { OutlineFilter } from "pixi-filters";
import * as PIXI from "pixi.js";
import bitmapFont from "../../../../resources/fonts/round_6x6_modified.xml";
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 { EventBus } from "../../../core/EventBus";
import { Cell, PlayerID, UnitType } from "../../../core/game/Game";
import {
BuildableUnit,
Cell,
PlayerActions,
PlayerID,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ToggleStructureEvent } from "../../InputHandler";
import { GameView, UnitView } from "../../../core/game/GameView";
import {
MouseMoveEvent,
MouseUpEvent,
ToggleStructureEvent as ToggleStructuresEvent,
} from "../../InputHandler";
import {
BuildUnitIntentEvent,
SendUpgradeStructureIntentEvent,
} from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
import {
DOTS_ZOOM_THRESHOLD,
ICON_SCALE_FACTOR_ZOOMED_IN,
ICON_SCALE_FACTOR_ZOOMED_OUT,
ICON_SIZE,
LEVEL_SCALE_FACTOR,
OFFSET_ZOOM_Y,
SpriteFactory,
STRUCTURE_SHAPES,
ZOOM_THRESHOLD,
} from "./StructureDrawingUtils";
extend([a11yPlugin]);
type ShapeType = "triangle" | "square" | "pentagon" | "octagon" | "circle";
class StructureRenderInfo {
public isOnScreen: boolean = false;
constructor(
@@ -35,68 +54,50 @@ class StructureRenderInfo {
) {}
}
const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
[UnitType.City]: "circle",
[UnitType.Port]: "pentagon",
[UnitType.Factory]: "circle",
[UnitType.DefensePost]: "octagon",
[UnitType.SAMLauncher]: "square",
[UnitType.MissileSilo]: "triangle",
};
const LEVEL_SCALE_FACTOR = 3;
const ICON_SCALE_FACTOR_ZOOMED_IN = 3.5;
const ICON_SCALE_FACTOR_ZOOMED_OUT = 1.4;
const DOTS_ZOOM_THRESHOLD = 0.5;
const ZOOM_THRESHOLD = 4.3;
const ICON_SIZE = {
circle: 28,
octagon: 28,
pentagon: 30,
square: 28,
triangle: 28,
};
const OFFSET_ZOOM_Y = 4; // offset for the y position of the level over the sprite
export class StructureIconsLayer implements Layer {
private ghostUnit: {
container: PIXI.Container;
range: PIXI.Container | null;
buildableUnit: BuildableUnit;
} | null = null;
private pixicanvas: HTMLCanvasElement;
private iconsStage: PIXI.Container;
private ghostStage: PIXI.Container;
private levelsStage: PIXI.Container;
private rootStage: PIXI.Container = new PIXI.Container();
public playerActions: PlayerActions | null = null;
private dotsStage: PIXI.Container;
private shouldRedraw: boolean = true;
private textureCache: Map<string, PIXI.Texture> = new Map();
private theme: Theme;
private readonly theme: Theme;
private renderer: PIXI.Renderer;
private renders: StructureRenderInfo[] = [];
private seenUnits: Set<UnitView> = new Set();
private structures: Map<
UnitType,
{ visible: boolean; iconPath: string; image: HTMLImageElement | null }
> = new Map([
[UnitType.City, { visible: true, iconPath: cityIcon, image: null }],
[UnitType.Factory, { visible: true, iconPath: factoryIcon, image: null }],
[
UnitType.DefensePost,
{ visible: true, iconPath: shieldIcon, image: null },
],
[UnitType.Port, { visible: true, iconPath: anchorIcon, image: null }],
[
UnitType.MissileSilo,
{ visible: true, iconPath: missileSiloIcon, image: null },
],
[
UnitType.SAMLauncher,
{ visible: true, iconPath: SAMMissileIcon, image: null },
],
]);
private readonly seenUnits: Set<UnitView> = new Set();
private readonly mousePos = { x: 0, y: 0 };
private renderSprites = true;
private factory: SpriteFactory;
private readonly structures: Map<UnitType, { visible: boolean }> = new Map([
[UnitType.City, { visible: true }],
[UnitType.Factory, { visible: true }],
[UnitType.DefensePost, { visible: true }],
[UnitType.Port, { visible: true }],
[UnitType.MissileSilo, { visible: true }],
[UnitType.SAMLauncher, { visible: true }],
]);
private lastGhostQueryAt: number;
potentialUpgrade: StructureRenderInfo | undefined;
constructor(
private game: GameView,
private eventBus: EventBus,
public uiState: UIState,
private transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
this.structures.forEach((u, unitType) => this.loadIcon(u, unitType));
this.factory = new SpriteFactory(
this.theme,
game,
transformHandler,
this.renderSprites,
);
}
async setupRenderer() {
@@ -114,6 +115,10 @@ export class StructureIconsLayer implements Layer {
this.iconsStage.position.set(0, 0);
this.iconsStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
this.ghostStage = new PIXI.Container();
this.ghostStage.position.set(0, 0);
this.ghostStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
this.levelsStage = new PIXI.Container();
this.levelsStage.position.set(0, 0);
this.levelsStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
@@ -122,6 +127,15 @@ export class StructureIconsLayer implements Layer {
this.dotsStage.position.set(0, 0);
this.dotsStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
this.rootStage.addChild(
this.dotsStage,
this.iconsStage,
this.levelsStage,
this.ghostStage,
);
this.rootStage.position.set(0, 0);
this.rootStage.setSize(this.pixicanvas.width, this.pixicanvas.height);
await this.renderer.init({
canvas: this.pixicanvas,
resolution: 1,
@@ -134,33 +148,18 @@ export class StructureIconsLayer implements Layer {
});
}
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() {
this.eventBus.on(ToggleStructureEvent, (e) =>
this.toggleStructure(e.structureType),
this.eventBus.on(ToggleStructuresEvent, (e) =>
this.toggleStructures(e.structureTypes),
);
this.eventBus.on(MouseMoveEvent, (e) => this.moveGhost(e));
this.eventBus.on(MouseUpEvent, (e) => this.createStructure(e));
window.addEventListener("resize", () => this.resizeCanvas());
await this.setupRenderer();
this.redraw();
@@ -171,11 +170,10 @@ export class StructureIconsLayer implements Layer {
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
this.renderer.resize(innerWidth, innerHeight, 1);
this.shouldRedraw = true;
}
}
public tick() {
tick() {
this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
@@ -192,15 +190,216 @@ export class StructureIconsLayer implements Layer {
this.game.config().userSettings()?.structureSprites() ?? true;
}
private toggleStructure(toggleStructureType: UnitType | null): void {
redraw() {
this.resizeCanvas();
}
renderLayer(mainContext: CanvasRenderingContext2D) {
if (!this.renderer) {
return;
}
if (this.ghostUnit) {
if (this.uiState.ghostStructure === null) {
this.removeGhostStructure();
} else if (
this.uiState.ghostStructure !== this.ghostUnit.buildableUnit.type
) {
this.clearGhostStructure();
}
} else if (this.uiState.ghostStructure !== null) {
this.createGhostStructure(this.uiState.ghostStructure);
}
this.renderGhost();
if (this.transformHandler.hasChanged()) {
for (const render of this.renders) {
this.computeNewLocation(render);
}
}
const scale = this.transformHandler.scale;
this.dotsStage!.visible = scale <= DOTS_ZOOM_THRESHOLD;
this.iconsStage!.visible =
scale > DOTS_ZOOM_THRESHOLD &&
(scale <= ZOOM_THRESHOLD || !this.renderSprites);
this.levelsStage!.visible = scale > ZOOM_THRESHOLD && this.renderSprites;
this.renderer.render(this.rootStage);
mainContext.drawImage(this.renderer.canvas, 0, 0);
}
renderGhost() {
if (!this.ghostUnit) return;
const now = performance.now();
if (now - this.lastGhostQueryAt < 50) {
return;
}
const rect = this.transformHandler.boundingRect();
if (!rect) return;
const localX = this.mousePos.x - rect.left;
const localY = this.mousePos.y - rect.top;
this.lastGhostQueryAt = now;
let tileRef: TileRef | undefined;
const tile = this.transformHandler.screenToWorldCoordinates(localX, localY);
if (this.game.isValidCoord(tile.x, tile.y)) {
tileRef = this.game.ref(tile.x, tile.y);
}
this.game
?.myPlayer()
?.actions(tileRef)
.then((actions) => {
if (this.potentialUpgrade) {
this.potentialUpgrade.iconContainer.filters = [];
this.potentialUpgrade.dotContainer.filters = [];
}
this.ghostUnit?.container && (this.ghostUnit.container.filters = []);
if (!this.ghostUnit) return;
const unit = actions.buildableUnits.find(
(u) => u.type === this.ghostUnit!.buildableUnit.type,
);
if (!unit) {
Object.assign(this.ghostUnit.buildableUnit, {
canBuild: false,
canUpgrade: false,
});
this.ghostUnit.container.filters = [
new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }),
];
return;
}
this.ghostUnit.buildableUnit = unit;
if (unit.canUpgrade) {
this.potentialUpgrade = this.renders.find(
(r) => r.unit.id() === unit.canUpgrade,
);
if (this.potentialUpgrade) {
this.potentialUpgrade.iconContainer.filters = [
new OutlineFilter({ thickness: 2, color: "rgba(0, 255, 0, 1)" }),
];
this.potentialUpgrade.dotContainer.filters = [
new OutlineFilter({ thickness: 2, color: "rgba(0, 255, 0, 1)" }),
];
}
} else if (unit.canBuild === false) {
this.ghostUnit.container.filters = [
new OutlineFilter({ thickness: 2, color: "rgba(255, 0, 0, 1)" }),
];
}
const scale = this.transformHandler.scale;
const s =
scale >= ZOOM_THRESHOLD
? Math.max(1, scale / ICON_SCALE_FACTOR_ZOOMED_IN)
: Math.min(1, scale / ICON_SCALE_FACTOR_ZOOMED_OUT);
this.ghostUnit.container.scale.set(s);
this.ghostUnit.range?.scale.set(this.transformHandler.scale);
});
}
private createStructure(e: MouseUpEvent) {
if (!this.ghostUnit) return;
if (
this.ghostUnit.buildableUnit.canBuild === false &&
this.ghostUnit.buildableUnit.canUpgrade === false
) {
this.removeGhostStructure();
return;
}
const rect = this.transformHandler.boundingRect();
if (!rect) return;
const x = e.x - rect.left;
const y = e.y - rect.top;
const tile = this.transformHandler.screenToWorldCoordinates(x, y);
if (this.ghostUnit.buildableUnit.canUpgrade !== false) {
this.eventBus.emit(
new SendUpgradeStructureIntentEvent(
this.ghostUnit.buildableUnit.canUpgrade,
this.ghostUnit.buildableUnit.type,
),
);
} else if (this.ghostUnit.buildableUnit.canBuild) {
this.eventBus.emit(
new BuildUnitIntentEvent(
this.ghostUnit.buildableUnit.type,
this.game.ref(tile.x, tile.y),
),
);
}
this.removeGhostStructure();
}
private moveGhost(e: MouseMoveEvent) {
this.mousePos.x = e.x;
this.mousePos.y = e.y;
if (!this.ghostUnit) return;
const rect = this.transformHandler.boundingRect();
if (!rect) return;
const localX = e.x - rect.left;
const localY = e.y - rect.top;
this.ghostUnit.container.position.set(localX, localY);
this.ghostUnit.range?.position.set(localX, localY);
}
private createGhostStructure(type: UnitType | null) {
const player = this.game.myPlayer();
if (!player) return;
if (type === null) {
return;
}
const rect = this.transformHandler.boundingRect();
const localX = this.mousePos.x - rect.left;
const localY = this.mousePos.y - rect.top;
this.ghostUnit = {
container: this.factory.createGhostContainer(
player,
this.ghostStage,
{ x: localX, y: localY },
type,
),
range: this.factory.createRange(type, this.ghostStage, {
x: localX,
y: localY,
}),
buildableUnit: { type, canBuild: false, canUpgrade: false, cost: 0n },
};
}
private clearGhostStructure() {
if (this.ghostUnit) {
this.ghostUnit.container.destroy();
this.ghostUnit.range?.destroy();
this.ghostUnit = null;
}
if (this.potentialUpgrade) {
this.potentialUpgrade.iconContainer.filters = [];
this.potentialUpgrade.dotContainer.filters = [];
this.potentialUpgrade = undefined;
}
}
private removeGhostStructure() {
this.clearGhostStructure();
this.uiState.ghostStructure = null;
}
private toggleStructures(toggleStructureType: UnitType[] | null): void {
for (const [structureType, infos] of this.structures) {
infos.visible =
structureType === toggleStructureType || toggleStructureType === null;
toggleStructureType?.indexOf(structureType) !== -1 ||
toggleStructureType === null;
}
for (const render of this.renders) {
this.modifyVisibility(render);
}
this.shouldRedraw = true;
}
private findRenderByUnit(
@@ -229,7 +428,6 @@ export class StructureIconsLayer implements Layer {
const render = this.findRenderByUnit(unitView);
if (render) {
this.deleteStructure(render);
this.shouldRedraw = true;
}
}
@@ -278,7 +476,6 @@ export class StructureIconsLayer implements Layer {
render.iconContainer = this.createIconSprite(unit);
render.dotContainer = this.createDotSprite(unit);
this.modifyVisibility(render);
this.shouldRedraw = true;
}
}
@@ -290,7 +487,6 @@ export class StructureIconsLayer implements Layer {
render.iconContainer = this.createIconSprite(unit);
render.dotContainer = this.createDotSprite(unit);
this.modifyVisibility(render);
this.shouldRedraw = true;
}
}
@@ -304,317 +500,9 @@ export class StructureIconsLayer implements Layer {
render.levelContainer = this.createLevelSprite(unit);
render.dotContainer = this.createDotSprite(unit);
this.modifyVisibility(render);
this.shouldRedraw = true;
}
}
redraw() {
this.resizeCanvas();
}
renderLayer(mainContext: CanvasRenderingContext2D) {
if (!this.renderer) {
return;
}
if (this.transformHandler.hasChanged()) {
for (const render of this.renders) {
this.computeNewLocation(render);
}
}
if (this.transformHandler.hasChanged() || this.shouldRedraw) {
if (this.transformHandler.scale > ZOOM_THRESHOLD && this.renderSprites) {
this.renderer.render(this.levelsStage);
} else if (this.transformHandler.scale > DOTS_ZOOM_THRESHOLD) {
this.renderer.render(this.iconsStage);
} else {
this.renderer.render(this.dotsStage);
}
this.shouldRedraw = false;
}
mainContext.drawImage(this.renderer.canvas, 0, 0);
}
private createTexture(unit: UnitView, renderIcon: boolean): PIXI.Texture {
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}` + (renderIcon ? "-icon" : "")
: `${unit.owner().territoryColor().toRgbString()}-${structureType}` +
(renderIcon ? "-icon" : "");
if (this.textureCache.has(cacheKey)) {
return this.textureCache.get(cacheKey)!;
}
const shape = STRUCTURE_SHAPES[structureType];
const texture = shape
? this.createIcon(
unit.owner(),
structureType,
isConstruction,
shape,
renderIcon,
)
: PIXI.Texture.EMPTY;
this.textureCache.set(cacheKey, texture);
return texture;
}
private createIcon(
owner: PlayerView,
structureType: UnitType,
isConstruction: boolean,
shape: ShapeType,
renderIcon: boolean,
): PIXI.Texture {
const structureCanvas = document.createElement("canvas");
let iconSize = ICON_SIZE[shape];
if (!renderIcon) {
iconSize /= 2.5;
}
structureCanvas.width = Math.ceil(iconSize);
structureCanvas.height = Math.ceil(iconSize);
const context = structureCanvas.getContext("2d")!;
const tc = owner.territoryColor();
const bc = owner.borderColor();
const darker = bc.luminance() < tc.luminance() ? bc : tc;
const lighter = bc.luminance() < tc.luminance() ? tc : bc;
let borderColor: string;
if (isConstruction) {
context.fillStyle = "rgb(198, 198, 198)";
borderColor = "rgb(128, 127, 127)";
} else {
context.fillStyle = lighter
.lighten(0.13)
.alpha(renderIcon ? 0.65 : 1)
.toRgbString();
const darken = darker.isLight() ? 0.17 : 0.15;
borderColor = darker.darken(darken).toRgbString();
}
context.strokeStyle = borderColor;
context.lineWidth = 1;
const halfIconSize = iconSize / 2;
switch (shape) {
case "triangle":
context.beginPath();
context.moveTo(halfIconSize, 1); // Top
context.lineTo(iconSize - 1, iconSize - 1); // Bottom right
context.lineTo(0, iconSize - 1); // Bottom left
context.closePath();
context.fill();
context.stroke();
break;
case "square":
context.fillRect(1, 1, iconSize - 2, iconSize - 2);
context.strokeRect(1, 1, iconSize - 3, iconSize - 3);
break;
case "octagon":
{
const cx = halfIconSize;
const cy = halfIconSize;
const r = halfIconSize - 1;
const step = (Math.PI * 2) / 8;
context.beginPath();
for (let i = 0; i < 8; i++) {
const angle = step * i - Math.PI / 8; // slight rotation for flat top
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.closePath();
context.fill();
context.stroke();
}
break;
case "pentagon":
{
const cx = halfIconSize;
const cy = halfIconSize;
const r = halfIconSize - 1;
const step = (Math.PI * 2) / 5;
context.beginPath();
for (let i = 0; i < 5; i++) {
const angle = step * i - Math.PI / 2; // rotate to have flat base or point up
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.closePath();
context.fill();
context.stroke();
}
break;
case "circle":
context.beginPath();
context.arc(
halfIconSize,
halfIconSize,
halfIconSize - 1,
0,
Math.PI * 2,
);
context.fill();
context.stroke();
break;
default:
throw new Error(`Unknown shape: ${shape}`);
}
const structureInfo = this.structures.get(structureType);
if (!structureInfo?.image) {
console.warn(`Image not loaded for unit type: ${structureType}`);
return PIXI.Texture.from(structureCanvas);
}
if (renderIcon) {
const SHAPE_OFFSETS = {
triangle: [6, 11],
square: [5, 5],
octagon: [6, 6],
pentagon: [7, 7],
circle: [6, 6],
};
const [offsetX, offsetY] = SHAPE_OFFSETS[shape] || [0, 0];
context.drawImage(
this.getImageColored(structureInfo.image, borderColor),
offsetX,
offsetY,
);
}
return PIXI.Texture.from(structureCanvas);
}
private createLevelSprite(unit: UnitView): PIXI.Container {
return this.createUnitContainer(unit, {
type: "level",
stage: this.levelsStage,
});
}
private createDotSprite(unit: UnitView): PIXI.Container {
return this.createUnitContainer(unit, {
type: "dot",
stage: this.dotsStage,
});
}
private createIconSprite(unit: UnitView): PIXI.Container {
return this.createUnitContainer(unit, {
type: "icon",
stage: this.iconsStage,
});
}
private createUnitContainer(
unit: UnitView,
options: { type?: "icon" | "dot" | "level"; stage: PIXI.Container },
): PIXI.Container {
const parentContainer = new PIXI.Container();
const tile = unit.tile();
const worldPos = new Cell(this.game.x(tile), this.game.y(tile));
const screenPos = this.transformHandler.worldToScreenCoordinates(worldPos);
const { type, stage } = options;
const scale = this.transformHandler.scale;
const spritesEnabled = this.game
.config()
.userSettings()
?.structureSprites?.();
// Add sprite if needed
if (type === "icon" || type === "dot") {
const texture = this.createTexture(unit, type === "icon");
const sprite = new PIXI.Sprite(texture);
sprite.anchor.set(0.5);
parentContainer.addChild(sprite);
}
// Add level text if needed
if ((type === "icon" || type === "level") && unit.level() > 1) {
const text = new PIXI.BitmapText({
text: unit.level().toString(),
style: {
fontFamily: "round_6x6_modified",
fontSize: 14,
},
});
text.anchor.set(0.5);
const unitType =
unit.type() === UnitType.Construction
? unit.constructionType()
: unit.type();
const shape = STRUCTURE_SHAPES[unitType!];
if (shape !== undefined) {
text.position.y = Math.round(-ICON_SIZE[shape] / 2 - 2);
}
parentContainer.addChild(text);
}
// Positioning
const posX = Math.round(screenPos.x);
let posY = Math.round(screenPos.y);
if (type === "level" && scale >= ZOOM_THRESHOLD && spritesEnabled) {
posY = Math.round(screenPos.y - scale * OFFSET_ZOOM_Y);
}
parentContainer.position.set(posX, posY);
// Scaling
if (type === "icon") {
const s =
scale >= ZOOM_THRESHOLD && !spritesEnabled
? Math.max(1, scale / ICON_SCALE_FACTOR_ZOOMED_IN)
: Math.min(1, scale / ICON_SCALE_FACTOR_ZOOMED_OUT);
parentContainer.scale.set(s);
} else if (type === "level") {
parentContainer.scale.set(Math.max(1, scale / LEVEL_SCALE_FACTOR));
}
stage.addChild(parentContainer);
return parentContainer;
}
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 worldPos = new Cell(this.game.x(tile), this.game.y(tile));
@@ -691,7 +579,27 @@ export class StructureIconsLayer implements Layer {
this.renders.push(render);
this.computeNewLocation(render);
this.modifyVisibility(render);
this.shouldRedraw = true;
}
private createLevelSprite(unit: UnitView): PIXI.Container {
return this.factory.createUnitContainer(unit, {
type: "level",
stage: this.levelsStage,
});
}
private createDotSprite(unit: UnitView): PIXI.Container {
return this.factory.createUnitContainer(unit, {
type: "dot",
stage: this.dotsStage,
});
}
private createIconSprite(unit: UnitView): PIXI.Container {
return this.factory.createUnitContainer(unit, {
type: "icon",
stage: this.iconsStage,
});
}
private deleteStructure(render: StructureRenderInfo) {