mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 14:24:38 +00:00
496f1008bb
## Description: Fix for all structure icons suddenly being displaced from the actual structure location. And in some cases, structure itself created at wrong location, or coordinate grid, nuke trajectory preview target, nuke circles, naval landing target, or pop-up text of gold earned from train or tradeship. - Was triggered on iOS almost exclusively, but was also possible on other devices when a top/left margin was present. Like when an ad was shown. Why noticed almost only on iOS? Because of different behaviors where it shifts the DOM elements around relative to the screen temporarily, so we get a top/left offset on getBoundingClientRect for the canvas. Possible culprit is overscroll which lets you scroll outside of the viewport for several hundred pixels before snapping back. Which was triggered by mistake by dragging instead of tapping somewhere, or so. - Some of the bug reports: https://discord.com/channels/1284581928254701718/1451393982159523982, https://discord.com/channels/1284581928254701718/1463548362526822649, https://discord.com/channels/1284581928254701718/1378672255336189964, fixes https://github.com/openfrontio/OpenFrontIO/issues/3406 - The fix brings a little performance win as well because we need to be doing less calculations. It is basically "if drawing on a canvas, work with canvas coordinates and not with screen coordinates". Was stress tested by two players and me, see below for reproduction. - (BTW. When researching if the current logic was intended in any way, I found CodeRabbit had already noticed part of the bug twice. One of them was [resolved](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2396277710), the other [left open](https://github.com/openfrontio/OpenFrontIO/pull/2059#discussion_r2370413213). Another reminder that we need to heed Rabbits calls!) **CONTAINS** - StructureIconsLayer > computeNewLocation and StructureDrawingUtils > createUnitContainer. In renderLayer, when TransformHandler.hasChanged (after onZoom, goTo, onMove), computeNewLocation recalculates the position of all structure icons. Sometimes all icons would suddenly be displaced, not above their map position but somewhere else. New single icons/levels/sprites would be placed wrongly too by computeNewLocation and createUnitContainer. -- Fix: don't use TransformHandler > worldToScreenCoordinates but the new worldToCanvasCoordinates. Because worldToScreenCoordinates adds the canvas boundingRect top/left offset. When the main canvas is already shifted down with a margin, the icons also get shifted within the canvas. So they're moved twice as much as they should be. We only need to know at what canvas location to put the icons, the main GameRender canvas already handles the overall displacement. - TransformHandler > worldToCanvasCoordinates -- Use the new worldToCanvasCoordinates too instead of worldToScreenCoordinates in CoordinateGridLayer, MoveIncicatorUI, NavalTarget, NukeTeleGraph, TextIndicator. They were affected by the same bug as the shifting Structure icons because the boundingRect top/left offset was applied twice, but it was noticed less. I have seen reports of NavalTarget or MobveIndicatorUI (for warships) not being in the correct spot though iirc. And just like for StructureIconsLayer/StructureDrawingUtils, it's only logical. Since we're drawing on the canvas, we only need to know where to place things within that canvas. - TransformHandler > worldToScreenCoordinates -- Split in two sub-functions. New seperate function worldToCanvasCoordinates was needed for the above fix. For canvasToScreenCoordinates the reason is explained below. - TransformHandler > screenToWorldCoordinates: this function already subtracts the canvas boundingRect top/left offset. Some callers were themselves getting the boundaryRect and subtracting top/left, before calling screenToWorldCoordinates. Not only unnecessary. But also, when there was more than 0 top/left offset, it would be subtracted twice from the mouse/tap position. Meaning a (ghost) structure or nuke trajectory preview target would not be put where the mouse/tap was. Same bug as above but reversed. -- Checked all callers. Most did it right. Fixes where needed in StructureIconsLayer > createStructure and renderGhost, and in NukeTrajectoryPreviewLayer > updateTrajectoryPreview and updateTrajectoryPath. -- Removed comment in screenToWorldCoordinates that talked about zoom. It doesn't belong there because we do more than zooming there, it was probably copied once from onZoom() which has the exact same comment and it belongs in that function. -- Small fix in caller BuildMenu when checking all callers of screenToWorldCoordinates: it checked if clickedCell was null, but screenToWorldCoordinates never returns null. - TransformHandler > added public helper functions screenToCanvasCoordinates and canvasToScreenCoordinates to make code re-usable -- screenToCanvasCoordinates is used in screenToWorldCoordinates and onZoom in TransformHandler itself -- screenToCanvasCoordinates is now also used also in moveGhost and createGhostStructure in StructureIconsLayer. No bugs there, but the same calculation was done (getting boundingRect, subtracting left/top from mouse/tap position). So they now use the centralized function which also adds to their readability. -- canvasToScreenCoordinates is (for now) only used in worldToScreenCoordinates in TransformHandler. It makes the function more readable imo. And, since it has such a similar calculation to screenToWorldCoordinates, it only seemed neat to have them both have their own function. **BEFORE** (only showing "all structure icons get displaced", but the cause and fix is basically the same for all) https://youtu.be/CfDdUwIRQeE **AFTER** https://youtu.be/w5w_wvh5V0g ## 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: tryout33
521 lines
16 KiB
TypeScript
521 lines
16 KiB
TypeScript
import * as PIXI from "pixi.js";
|
|
import { assetUrl } from "../../../core/AssetUrls";
|
|
import { Theme } from "../../../core/configuration/Config";
|
|
import {
|
|
Cell,
|
|
PlayerBuildableUnitType,
|
|
UnitType,
|
|
} from "../../../core/game/Game";
|
|
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
|
import { TransformHandler } from "../TransformHandler";
|
|
const anchorIcon = assetUrl("images/AnchorIcon.png");
|
|
const cityIcon = assetUrl("images/CityIcon.png");
|
|
const factoryIcon = assetUrl("images/FactoryUnit.png");
|
|
const missileSiloIcon = assetUrl("images/MissileSiloUnit.png");
|
|
const SAMMissileIcon = assetUrl("images/SamLauncherUnit.png");
|
|
const shieldIcon = assetUrl("images/ShieldIcon.png");
|
|
|
|
export 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",
|
|
[UnitType.Warship]: "cross",
|
|
[UnitType.AtomBomb]: "cross",
|
|
[UnitType.HydrogenBomb]: "cross",
|
|
[UnitType.MIRV]: "cross",
|
|
};
|
|
export const LEVEL_SCALE_FACTOR = 3;
|
|
export const ICON_SCALE_FACTOR_ZOOMED_IN = 3.5;
|
|
export const ICON_SCALE_FACTOR_ZOOMED_OUT = 1.4;
|
|
export const DOTS_ZOOM_THRESHOLD = 0.5;
|
|
export const ZOOM_THRESHOLD = 4.3;
|
|
export const ICON_SIZE = {
|
|
circle: 28,
|
|
octagon: 28,
|
|
pentagon: 30,
|
|
square: 28,
|
|
triangle: 28,
|
|
cross: 20,
|
|
};
|
|
export const OFFSET_ZOOM_Y = 4;
|
|
|
|
export type ShapeType =
|
|
| "triangle"
|
|
| "square"
|
|
| "pentagon"
|
|
| "octagon"
|
|
| "circle"
|
|
| "cross";
|
|
|
|
export class SpriteFactory {
|
|
private theme: Theme;
|
|
private game: GameView;
|
|
private transformHandler: TransformHandler;
|
|
private renderSprites: boolean;
|
|
private readonly textureCache: Map<string, PIXI.Texture> = new Map();
|
|
|
|
private readonly structuresInfos: 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(
|
|
theme: Theme,
|
|
game: GameView,
|
|
transformHandler: TransformHandler,
|
|
renderSprites: boolean,
|
|
) {
|
|
this.theme = theme;
|
|
this.game = game;
|
|
this.transformHandler = transformHandler;
|
|
this.renderSprites = renderSprites;
|
|
this.structuresInfos.forEach((u, unitType) => this.loadIcon(u, unitType));
|
|
}
|
|
|
|
private loadIcon(
|
|
unitInfo: {
|
|
iconPath: string;
|
|
image: HTMLImageElement | null;
|
|
},
|
|
unitType: UnitType,
|
|
) {
|
|
const image = new Image();
|
|
image.src = unitInfo.iconPath;
|
|
image.onload = () => {
|
|
unitInfo.image = image;
|
|
this.invalidateTextureCache(unitType);
|
|
};
|
|
image.onerror = () => {
|
|
console.error(
|
|
`Failed to load icon for ${unitType}: ${unitInfo.iconPath}`,
|
|
);
|
|
};
|
|
}
|
|
|
|
private invalidateTextureCache(unitType: UnitType) {
|
|
for (const key of Array.from(this.textureCache.keys())) {
|
|
if (key.includes(`-${unitType}`)) {
|
|
this.textureCache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
createGhostContainer(
|
|
player: PlayerView,
|
|
ghostStage: PIXI.Container,
|
|
pos: { x: number; y: number },
|
|
structureType: PlayerBuildableUnitType,
|
|
): {
|
|
container: PIXI.Container;
|
|
priceText: PIXI.BitmapText;
|
|
priceBg: PIXI.Graphics;
|
|
priceGroup: PIXI.Container;
|
|
priceBox: { height: number; y: number; paddingX: number; minWidth: number };
|
|
} {
|
|
const parentContainer = new PIXI.Container();
|
|
const texture = this.createTexture(
|
|
structureType,
|
|
player,
|
|
false,
|
|
false,
|
|
true,
|
|
);
|
|
const sprite = new PIXI.Sprite(texture);
|
|
sprite.anchor.set(0.5);
|
|
sprite.alpha = 0.5;
|
|
parentContainer.addChild(sprite);
|
|
|
|
const priceText = new PIXI.BitmapText({
|
|
text: "125K",
|
|
style: { fontFamily: "round_6x6_modified", fontSize: 12 },
|
|
});
|
|
priceText.anchor.set(0.5);
|
|
const priceGroup = new PIXI.Container();
|
|
const boxHeight = 18;
|
|
const boxY =
|
|
(sprite.height > 0 ? sprite.height / 2 : 16) + boxHeight / 2 + 4;
|
|
|
|
// a way to resize the pill horizontally based on the text width
|
|
const paddingX = 8;
|
|
const minWidth = 32;
|
|
const textWidth = priceText.width;
|
|
const boxWidth = Math.max(minWidth, textWidth + paddingX * 2);
|
|
|
|
const priceBg = new PIXI.Graphics();
|
|
priceBg
|
|
.roundRect(-boxWidth / 2, boxY - boxHeight / 2, boxWidth, boxHeight, 4)
|
|
.fill({ color: 0x000000, alpha: 0.65 });
|
|
|
|
priceText.position.set(0, boxY);
|
|
|
|
priceGroup.addChild(priceBg);
|
|
priceGroup.addChild(priceText);
|
|
parentContainer.addChild(priceGroup);
|
|
|
|
parentContainer.position.set(pos.x, pos.y);
|
|
parentContainer.scale.set(
|
|
Math.min(1, this.transformHandler.scale / ICON_SCALE_FACTOR_ZOOMED_OUT),
|
|
);
|
|
ghostStage.addChild(parentContainer);
|
|
return {
|
|
container: parentContainer,
|
|
priceText,
|
|
priceBg,
|
|
priceGroup,
|
|
priceBox: { height: boxHeight, y: boxY, paddingX, minWidth },
|
|
};
|
|
}
|
|
|
|
// --- internal helpers ---
|
|
|
|
public 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.worldToCanvasCoordinates(worldPos);
|
|
|
|
const isMarkedForDeletion = unit.markedForDeletion() !== false;
|
|
const isConstruction = unit.isUnderConstruction();
|
|
const structureType = unit.type();
|
|
const { type, stage } = options;
|
|
const { scale } = this.transformHandler;
|
|
|
|
this.renderSprites =
|
|
this.game.config().userSettings()?.structureSprites() ?? true;
|
|
|
|
if (type === "icon" || type === "dot") {
|
|
const texture = this.createTexture(
|
|
structureType,
|
|
unit.owner(),
|
|
isConstruction,
|
|
isMarkedForDeletion,
|
|
type === "icon",
|
|
);
|
|
const sprite = new PIXI.Sprite(texture);
|
|
sprite.anchor.set(0.5);
|
|
parentContainer.addChild(sprite);
|
|
}
|
|
|
|
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 shape = STRUCTURE_SHAPES[structureType];
|
|
if (shape !== undefined) {
|
|
text.position.y = Math.round(-ICON_SIZE[shape] / 2 - 2);
|
|
}
|
|
parentContainer.addChild(text);
|
|
}
|
|
|
|
const posX = Math.round(screenPos.x);
|
|
let posY = Math.round(screenPos.y);
|
|
if (type === "level" && scale >= ZOOM_THRESHOLD && this.renderSprites) {
|
|
posY = Math.round(screenPos.y - scale * OFFSET_ZOOM_Y);
|
|
}
|
|
parentContainer.position.set(posX, posY);
|
|
|
|
if (type === "icon") {
|
|
const s =
|
|
scale >= ZOOM_THRESHOLD && !this.renderSprites
|
|
? 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 createTexture(
|
|
type: UnitType,
|
|
owner: PlayerView,
|
|
isConstruction: boolean,
|
|
isMarkedForDeletion: boolean,
|
|
renderIcon: boolean,
|
|
): PIXI.Texture {
|
|
const cacheKeyBase = isConstruction
|
|
? `construction-${type}`
|
|
: `${this.theme.territoryColor(owner).toRgbString()}-${type}`;
|
|
const cacheKey =
|
|
cacheKeyBase +
|
|
(renderIcon ? "-icon" : "") +
|
|
(isMarkedForDeletion ? "-deleted" : "");
|
|
|
|
if (this.textureCache.has(cacheKey)) {
|
|
return this.textureCache.get(cacheKey)!;
|
|
}
|
|
const shape = STRUCTURE_SHAPES[type];
|
|
const texture = shape
|
|
? this.createIcon(
|
|
owner,
|
|
type,
|
|
isConstruction,
|
|
isMarkedForDeletion,
|
|
shape,
|
|
renderIcon,
|
|
)
|
|
: PIXI.Texture.EMPTY;
|
|
this.textureCache.set(cacheKey, texture);
|
|
return texture;
|
|
}
|
|
|
|
private createIcon(
|
|
owner: PlayerView,
|
|
structureType: UnitType,
|
|
isConstruction: boolean,
|
|
isMarkedForDeletion: boolean,
|
|
shape: string,
|
|
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")!;
|
|
|
|
// Use structureColors defined from the PlayerView.
|
|
context.fillStyle = isConstruction
|
|
? "rgb(198,198,198)"
|
|
: owner.structureColors().light.toRgbString();
|
|
context.strokeStyle = isConstruction
|
|
? "rgb(127,127, 127)"
|
|
: owner.structureColors().dark.toRgbString();
|
|
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 "cross": {
|
|
context.strokeStyle = "rgba(0, 0, 0, 1)";
|
|
context.fillStyle = "rgba(0, 0, 0, 1)";
|
|
|
|
const gap = iconSize * 0.18; // gap at center
|
|
const lineLen = iconSize / 2;
|
|
context.save();
|
|
context.translate(halfIconSize, halfIconSize);
|
|
// Up
|
|
context.beginPath();
|
|
context.moveTo(0, -gap);
|
|
context.lineTo(0, -lineLen);
|
|
context.stroke();
|
|
// Down
|
|
context.beginPath();
|
|
context.moveTo(0, gap);
|
|
context.lineTo(0, lineLen);
|
|
context.stroke();
|
|
// Left
|
|
context.beginPath();
|
|
context.moveTo(-gap, 0);
|
|
context.lineTo(-lineLen, 0);
|
|
context.stroke();
|
|
// Right
|
|
context.beginPath();
|
|
context.moveTo(gap, 0);
|
|
context.lineTo(lineLen, 0);
|
|
context.stroke();
|
|
context.restore();
|
|
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.structuresInfos.get(structureType);
|
|
|
|
if (structureInfo?.image && renderIcon) {
|
|
const SHAPE_OFFSETS = {
|
|
triangle: [6, 11],
|
|
square: [5, 5],
|
|
octagon: [6, 6],
|
|
pentagon: [7, 7],
|
|
circle: [6, 6],
|
|
cross: [0, 0],
|
|
};
|
|
const [offsetX, offsetY] = SHAPE_OFFSETS[shape] || [0, 0];
|
|
context.drawImage(
|
|
this.getImageColored(
|
|
structureInfo.image,
|
|
owner.structureColors().dark.toRgbString(),
|
|
),
|
|
offsetX,
|
|
offsetY,
|
|
);
|
|
}
|
|
|
|
if (isMarkedForDeletion) {
|
|
context.save();
|
|
context.strokeStyle = "rgba(255, 64, 64, 0.95)";
|
|
context.lineWidth = Math.max(2, Math.round(iconSize * 0.12));
|
|
context.lineCap = "round";
|
|
const padding = Math.max(2, iconSize * 0.12);
|
|
context.beginPath();
|
|
context.moveTo(padding, padding);
|
|
context.lineTo(iconSize - padding, iconSize - padding);
|
|
context.moveTo(iconSize - padding, padding);
|
|
context.lineTo(padding, iconSize - padding);
|
|
context.stroke();
|
|
context.restore();
|
|
}
|
|
|
|
return PIXI.Texture.from(structureCanvas);
|
|
}
|
|
|
|
public createRange(
|
|
type: UnitType,
|
|
stage: PIXI.Container,
|
|
pos: { x: number; y: number },
|
|
level?: number,
|
|
targetingAlly: boolean = false,
|
|
): PIXI.Container | null {
|
|
if (stage === undefined) throw new Error("Not initialized");
|
|
const parentContainer = new PIXI.Container();
|
|
const circle = new PIXI.Graphics();
|
|
let radius = 0;
|
|
switch (type) {
|
|
case UnitType.SAMLauncher:
|
|
radius = this.game.config().samRange(level ?? 1);
|
|
break;
|
|
case UnitType.Factory:
|
|
radius = this.game.config().trainStationMaxRange();
|
|
break;
|
|
case UnitType.DefensePost:
|
|
radius = this.game.config().defensePostRange();
|
|
break;
|
|
case UnitType.AtomBomb:
|
|
radius = this.game.config().nukeMagnitudes(UnitType.AtomBomb).outer;
|
|
break;
|
|
case UnitType.HydrogenBomb:
|
|
radius = this.game.config().nukeMagnitudes(UnitType.HydrogenBomb).outer;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
// Add warning colors (red/orange) when targeting an ally to indicate alliance will break
|
|
const isNuke = type === UnitType.AtomBomb || type === UnitType.HydrogenBomb;
|
|
const fillColor = targetingAlly && isNuke ? 0xff6b35 : 0xffffff;
|
|
const fillAlpha = targetingAlly && isNuke ? 0.35 : 0.2;
|
|
const strokeColor = targetingAlly && isNuke ? 0xff4444 : 0xffffff;
|
|
const strokeAlpha = targetingAlly && isNuke ? 0.8 : 0.5;
|
|
const strokeWidth = targetingAlly && isNuke ? 2 : 1;
|
|
|
|
circle
|
|
.circle(0, 0, radius)
|
|
.fill({ color: fillColor, alpha: fillAlpha })
|
|
.stroke({ width: strokeWidth, color: strokeColor, alpha: strokeAlpha });
|
|
parentContainer.addChild(circle);
|
|
parentContainer.position.set(pos.x, pos.y);
|
|
parentContainer.scale.set(this.transformHandler.scale);
|
|
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;
|
|
}
|
|
}
|