mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-12 05:43:52 +00:00
671 lines
19 KiB
TypeScript
671 lines
19 KiB
TypeScript
import * as PIXI from "pixi.js";
|
|
import { assetUrl } from "../../../core/AssetUrls";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import { PseudoRandom } from "../../../core/PseudoRandom";
|
|
import { Config, Theme } from "../../../core/configuration/Config";
|
|
import { Cell } from "../../../core/game/Game";
|
|
import { GameView, PlayerView } from "../../../core/game/GameView";
|
|
import { UserSettings } from "../../../core/game/UserSettings";
|
|
import { AlternateViewEvent } from "../../InputHandler";
|
|
import { renderTroops } from "../../Utils";
|
|
import {
|
|
ALLIANCE_ICON_ID,
|
|
computeAllianceTopCutPercent,
|
|
EMOJI_ICON_KIND,
|
|
getFirstPlacePlayer,
|
|
getPlayerIcons,
|
|
IMAGE_ICON_KIND,
|
|
PlayerIconDescriptor,
|
|
PlayerIconId,
|
|
TRAITOR_ICON_ID,
|
|
} from "../PlayerIcons";
|
|
import { TransformHandler } from "../TransformHandler";
|
|
import { Layer } from "./Layer";
|
|
import { NameLayerAssets } from "./NameLayerAssets";
|
|
import {
|
|
computeNameLayerFontSize,
|
|
computeNameLayerLayout,
|
|
computeNameLayerScale,
|
|
computeNameLayerVisible,
|
|
computeTraitorFlashAlpha,
|
|
replaceUnsupportedNameGlyphs,
|
|
} from "./NameLayerLayout";
|
|
|
|
const allianceIconFaded = assetUrl("images/AllianceIconFaded.svg");
|
|
const questionMarkIcon = assetUrl("images/QuestionMarkIcon.svg");
|
|
|
|
type PixiRenderer = PIXI.Renderer | PIXI.WebGLRenderer | PIXI.WebGPURenderer;
|
|
|
|
interface PixiIconRender {
|
|
container: PIXI.Container;
|
|
centered: boolean;
|
|
src?: string;
|
|
sprite?: PIXI.Sprite;
|
|
text?: PIXI.Text;
|
|
alliance?: {
|
|
base: PIXI.Sprite;
|
|
colored: PIXI.Sprite;
|
|
questionMark: PIXI.Sprite;
|
|
mask: PIXI.Graphics;
|
|
};
|
|
}
|
|
|
|
class RenderInfo {
|
|
public icons: Map<PlayerIconId, PixiIconRender> = new Map();
|
|
public location: Cell | null = null;
|
|
public baseSize = 1;
|
|
public fontSize = 0;
|
|
public fontColor = "";
|
|
public flagSrc = "";
|
|
public flagSprite: PIXI.Sprite | null = null;
|
|
public lastDisplayName = "";
|
|
public lastTroopsText = "";
|
|
|
|
constructor(
|
|
public player: PlayerView,
|
|
public lastRenderCalc: number,
|
|
public container: PIXI.Container,
|
|
public nameText: PIXI.BitmapText,
|
|
public troopsText: PIXI.BitmapText,
|
|
) {}
|
|
}
|
|
|
|
export class NameLayer implements Layer {
|
|
private config: Config;
|
|
private lastChecked = 0;
|
|
private readonly renderCheckRate = 100;
|
|
private readonly renderRefreshRate = 500;
|
|
private readonly rand = new PseudoRandom(10);
|
|
private readonly renders: RenderInfo[] = [];
|
|
private readonly seenPlayers: Set<PlayerView> = new Set();
|
|
private readonly rootStage: PIXI.Container = new PIXI.Container();
|
|
private readonly labelStage: PIXI.Container = new PIXI.Container();
|
|
private readonly assets = new NameLayerAssets();
|
|
private theme: Theme;
|
|
private userSettings: UserSettings = new UserSettings();
|
|
private isVisible = true;
|
|
private firstPlace: PlayerView | null = null;
|
|
private allianceDuration: number;
|
|
private alliancesDisabled = false;
|
|
private myPlayer: PlayerView | null = null;
|
|
private pixicanvas: HTMLCanvasElement;
|
|
private renderer: PixiRenderer | null = null;
|
|
private rendererInitialized = false;
|
|
private rebuildPending = false;
|
|
|
|
constructor(
|
|
private game: GameView,
|
|
private transformHandler: TransformHandler,
|
|
private eventBus: EventBus,
|
|
) {}
|
|
|
|
shouldTransform(): boolean {
|
|
return false;
|
|
}
|
|
|
|
async init() {
|
|
this.myPlayer = this.game.myPlayer();
|
|
this.config = this.game.config();
|
|
this.theme = this.config.theme();
|
|
this.alliancesDisabled = this.config.disableAlliances();
|
|
this.allianceDuration = Math.max(1, this.config.allianceDuration());
|
|
|
|
this.rootStage.addChild(this.labelStage);
|
|
this.rootStage.position.set(0, 0);
|
|
|
|
this.eventBus.on(AlternateViewEvent, (e) => this.onAlternateViewChange(e));
|
|
window.addEventListener("resize", () => this.resizeCanvas());
|
|
|
|
await this.setupRenderer();
|
|
this.resizeCanvas();
|
|
}
|
|
|
|
async redraw() {
|
|
if (this.rebuildPending || this.rendererOrGLContextLost()) {
|
|
return;
|
|
}
|
|
this.rebuildPending = true;
|
|
try {
|
|
if (this.renderer?.name === "webgpu") {
|
|
this.rendererInitialized = false;
|
|
await this.setupRenderer();
|
|
}
|
|
this.resizeCanvas();
|
|
for (const render of this.renders) {
|
|
render.container.destroy({ children: true });
|
|
}
|
|
this.renders.length = 0;
|
|
this.seenPlayers.clear();
|
|
} finally {
|
|
this.rebuildPending = false;
|
|
}
|
|
}
|
|
|
|
getTickIntervalMs() {
|
|
return 1000;
|
|
}
|
|
|
|
tick() {
|
|
this.firstPlace = getFirstPlacePlayer(this.game);
|
|
|
|
for (const player of this.game.playerViews()) {
|
|
if (player.isAlive() && !this.seenPlayers.has(player)) {
|
|
this.seenPlayers.add(player);
|
|
this.renders.push(this.createPlayerRender(player));
|
|
}
|
|
}
|
|
}
|
|
|
|
renderLayer(mainContext: CanvasRenderingContext2D) {
|
|
if (this.rendererOrGLContextLost()) {
|
|
return;
|
|
}
|
|
|
|
this.myPlayer ??= this.game.myPlayer();
|
|
this.updateTransformsAndVisibility();
|
|
|
|
const now = Date.now();
|
|
if (now > this.lastChecked + this.renderCheckRate) {
|
|
this.lastChecked = now;
|
|
const transitiveTargets = this.myPlayer?.transitiveTargets() ?? [];
|
|
for (const render of [...this.renders]) {
|
|
this.renderPlayerInfo(render, transitiveTargets, now);
|
|
}
|
|
}
|
|
|
|
this.renderer?.render(this.rootStage);
|
|
if (this.renderer) {
|
|
mainContext.drawImage(this.renderer.canvas, 0, 0);
|
|
}
|
|
}
|
|
|
|
private async setupRenderer() {
|
|
if (this.renderer) {
|
|
this.renderer.destroy(true);
|
|
this.labelStage.removeChildren();
|
|
}
|
|
|
|
await this.assets.preload();
|
|
|
|
this.pixicanvas = document.createElement("canvas");
|
|
this.pixicanvas.width = window.innerWidth;
|
|
this.pixicanvas.height = window.innerHeight;
|
|
|
|
const renderer = await PIXI.autoDetectRenderer({
|
|
canvas: this.pixicanvas,
|
|
resolution: 1,
|
|
width: this.pixicanvas.width,
|
|
height: this.pixicanvas.height,
|
|
antialias: false,
|
|
clearBeforeRender: true,
|
|
backgroundAlpha: 0,
|
|
backgroundColor: 0x00000000,
|
|
});
|
|
|
|
console.info(`Using ${renderer.name} for name layer`);
|
|
this.renderer = renderer;
|
|
|
|
if (this.renderer.name === "webgpu") {
|
|
const gpuRenderer = this.renderer as PIXI.WebGPURenderer;
|
|
gpuRenderer.gpu.device.lost.then(() => {
|
|
this.redraw();
|
|
});
|
|
}
|
|
|
|
if (this.renderer.name === "webgl") {
|
|
this.renderer.runners.contextChange.add({
|
|
contextChange: () => {
|
|
requestAnimationFrame(() => {
|
|
this.redraw();
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
this.rendererInitialized = true;
|
|
}
|
|
|
|
private rendererOrGLContextLost(): boolean {
|
|
if (!this.renderer || !this.rendererInitialized) return true;
|
|
if (this.renderer.name === "webgl") {
|
|
return (this.renderer as PIXI.WebGLRenderer).context?.isLost === true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private resizeCanvas() {
|
|
if (this.rendererOrGLContextLost()) {
|
|
return;
|
|
}
|
|
this.pixicanvas.width = window.innerWidth;
|
|
this.pixicanvas.height = window.innerHeight;
|
|
this.renderer?.resize(window.innerWidth, window.innerHeight, 1);
|
|
}
|
|
|
|
private onAlternateViewChange(event: AlternateViewEvent) {
|
|
this.isVisible = !event.alternateView;
|
|
this.updateTransformsAndVisibility();
|
|
}
|
|
|
|
private createPlayerRender(player: PlayerView): RenderInfo {
|
|
const container = new PIXI.Container();
|
|
container.visible = false;
|
|
|
|
const nameText = this.createBitmapText("");
|
|
const troopsText = this.createBitmapText("");
|
|
|
|
container.addChild(nameText, troopsText);
|
|
this.labelStage.addChild(container);
|
|
|
|
const render = new RenderInfo(player, 0, container, nameText, troopsText);
|
|
this.updateFlag(render);
|
|
return render;
|
|
}
|
|
|
|
private createBitmapText(text: string): PIXI.BitmapText {
|
|
const bitmapText = new PIXI.BitmapText({
|
|
text,
|
|
style: {
|
|
fontFamily: this.assets.fontFamily,
|
|
fontSize: 12,
|
|
fill: "#ffffff",
|
|
},
|
|
});
|
|
bitmapText.anchor.set(0.5);
|
|
return bitmapText;
|
|
}
|
|
|
|
private updateTransformsAndVisibility() {
|
|
const now = performance.now();
|
|
for (const render of this.renders) {
|
|
const nameLocation = render.player.nameLocation();
|
|
if (!nameLocation || !render.player.isAlive()) {
|
|
render.container.visible = false;
|
|
continue;
|
|
}
|
|
|
|
render.baseSize = Math.max(1, Math.floor(nameLocation.size));
|
|
render.location = new Cell(nameLocation.x, nameLocation.y);
|
|
const isOnScreen = this.transformHandler.isOnScreen(render.location);
|
|
render.container.visible = computeNameLayerVisible({
|
|
isLayerVisible: this.isVisible,
|
|
transformScale: this.transformHandler.scale,
|
|
baseSize: render.baseSize,
|
|
isOnScreen,
|
|
});
|
|
|
|
if (!render.container.visible) {
|
|
continue;
|
|
}
|
|
|
|
const screenPos = this.transformHandler.worldToCanvasCoordinates(
|
|
render.location,
|
|
);
|
|
render.container.position.set(
|
|
Math.round(screenPos.x),
|
|
Math.round(screenPos.y),
|
|
);
|
|
render.container.scale.set(computeNameLayerScale(render.baseSize));
|
|
this.updateTraitorAlpha(render, now);
|
|
}
|
|
}
|
|
|
|
private renderPlayerInfo(
|
|
render: RenderInfo,
|
|
transitiveTargets: PlayerView[],
|
|
now: number,
|
|
) {
|
|
if (!render.player.nameLocation()) {
|
|
return;
|
|
}
|
|
if (!render.player.isAlive()) {
|
|
this.deleteRender(render);
|
|
return;
|
|
}
|
|
if (!render.container.visible) {
|
|
return;
|
|
}
|
|
if (now - render.lastRenderCalc <= this.renderRefreshRate) {
|
|
return;
|
|
}
|
|
render.lastRenderCalc = now + this.rand.nextInt(0, 100);
|
|
|
|
render.fontSize = computeNameLayerFontSize(render.baseSize);
|
|
this.updateText(render);
|
|
this.updateFlag(render);
|
|
|
|
const iconSize = Math.min(render.fontSize * 1.5, 48);
|
|
const icons = getPlayerIcons({
|
|
game: this.game,
|
|
player: render.player,
|
|
includeAllianceIcon: true,
|
|
firstPlace: this.firstPlace,
|
|
darkMode: this.userSettings.darkMode(),
|
|
alliancesDisabled: this.alliancesDisabled,
|
|
transitiveTargets,
|
|
});
|
|
|
|
this.updateIcons(render, icons, iconSize);
|
|
this.layoutRender(render, iconSize);
|
|
}
|
|
|
|
private updateText(render: RenderInfo) {
|
|
const displayName = replaceUnsupportedNameGlyphs(
|
|
render.player.displayName(),
|
|
);
|
|
const troopsText = replaceUnsupportedNameGlyphs(
|
|
renderTroops(render.player.troops()),
|
|
);
|
|
const fontColor = this.theme.textColor(render.player);
|
|
|
|
if (
|
|
render.lastDisplayName !== displayName ||
|
|
render.fontColor !== fontColor ||
|
|
render.nameText.style.fontSize !== render.fontSize ||
|
|
render.nameText.style.fontFamily !== this.assets.fontFamily
|
|
) {
|
|
render.nameText.text = displayName;
|
|
render.nameText.style = {
|
|
fontFamily: this.assets.fontFamily,
|
|
fontSize: render.fontSize,
|
|
fill: fontColor,
|
|
};
|
|
render.lastDisplayName = displayName;
|
|
}
|
|
|
|
if (
|
|
render.lastTroopsText !== troopsText ||
|
|
render.fontColor !== fontColor ||
|
|
render.troopsText.style.fontSize !== render.fontSize ||
|
|
render.troopsText.style.fontFamily !== this.assets.fontFamily
|
|
) {
|
|
render.troopsText.text = troopsText;
|
|
render.troopsText.style = {
|
|
fontFamily: this.assets.fontFamily,
|
|
fontSize: render.fontSize,
|
|
fill: fontColor,
|
|
};
|
|
render.lastTroopsText = troopsText;
|
|
}
|
|
|
|
render.fontColor = fontColor;
|
|
}
|
|
|
|
private updateFlag(render: RenderInfo) {
|
|
const flag = render.player.cosmetics.flag;
|
|
const src = flag ? assetUrl(flag) : "";
|
|
if (!src) {
|
|
render.flagSprite?.destroy();
|
|
render.flagSprite = null;
|
|
render.flagSrc = "";
|
|
return;
|
|
}
|
|
|
|
if (src !== render.flagSrc) {
|
|
render.flagSprite?.destroy();
|
|
render.flagSprite = null;
|
|
render.flagSrc = src;
|
|
}
|
|
|
|
const texture = this.assets.getTexture(src);
|
|
if (!texture) {
|
|
if (render.flagSprite) {
|
|
render.flagSprite.visible = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!render.flagSprite) {
|
|
render.flagSprite = new PIXI.Sprite(texture);
|
|
render.flagSprite.anchor.set(0.5);
|
|
render.flagSprite.alpha = 0.8;
|
|
render.container.addChild(render.flagSprite);
|
|
} else if (render.flagSprite.texture !== texture) {
|
|
render.flagSprite.texture = texture;
|
|
}
|
|
|
|
render.flagSprite.visible = true;
|
|
}
|
|
|
|
private updateIcons(
|
|
render: RenderInfo,
|
|
icons: PlayerIconDescriptor[],
|
|
size: number,
|
|
) {
|
|
const desiredIds = new Set(icons.map((icon) => icon.id));
|
|
for (const [id, iconRender] of render.icons) {
|
|
if (!desiredIds.has(id)) {
|
|
iconRender.container.destroy({ children: true });
|
|
render.icons.delete(id);
|
|
}
|
|
}
|
|
|
|
for (const icon of icons) {
|
|
if (icon.kind === EMOJI_ICON_KIND) {
|
|
this.updateEmojiIcon(render, icon, size);
|
|
} else if (icon.id === ALLIANCE_ICON_ID) {
|
|
this.updateAllianceIcon(render, icon, size);
|
|
} else if (icon.kind === IMAGE_ICON_KIND && icon.src) {
|
|
this.updateImageIcon(render, icon, size);
|
|
}
|
|
}
|
|
}
|
|
|
|
private updateImageIcon(
|
|
render: RenderInfo,
|
|
icon: PlayerIconDescriptor,
|
|
size: number,
|
|
) {
|
|
const src = icon.src;
|
|
if (!src) {
|
|
return;
|
|
}
|
|
|
|
let iconRender = render.icons.get(icon.id);
|
|
if (!iconRender || iconRender.src !== src || !iconRender.sprite) {
|
|
iconRender?.container.destroy({ children: true });
|
|
const container = new PIXI.Container();
|
|
container.alpha = 0.8;
|
|
const sprite = new PIXI.Sprite();
|
|
sprite.anchor.set(0.5);
|
|
container.addChild(sprite);
|
|
render.container.addChild(container);
|
|
iconRender = {
|
|
container,
|
|
centered: icon.center ?? false,
|
|
src,
|
|
sprite,
|
|
};
|
|
render.icons.set(icon.id, iconRender);
|
|
}
|
|
|
|
iconRender.centered = icon.center ?? false;
|
|
const texture = this.assets.getTexture(src);
|
|
iconRender.container.visible = texture !== null;
|
|
if (!texture) {
|
|
return;
|
|
}
|
|
|
|
iconRender.sprite!.texture = texture;
|
|
iconRender.sprite!.width = size;
|
|
iconRender.sprite!.height = size;
|
|
}
|
|
|
|
private updateEmojiIcon(
|
|
render: RenderInfo,
|
|
icon: PlayerIconDescriptor,
|
|
size: number,
|
|
) {
|
|
let iconRender = render.icons.get(icon.id);
|
|
if (!iconRender || !iconRender.text) {
|
|
iconRender?.container.destroy({ children: true });
|
|
const container = new PIXI.Container();
|
|
container.alpha = 0.8;
|
|
const text = new PIXI.Text({
|
|
text: icon.text ?? "",
|
|
style: {
|
|
fontFamily: "sans-serif",
|
|
fontSize: size,
|
|
fill: "#ffffff",
|
|
},
|
|
});
|
|
text.anchor.set(0.5);
|
|
container.addChild(text);
|
|
render.container.addChild(container);
|
|
iconRender = { container, centered: icon.center ?? false, text };
|
|
render.icons.set(icon.id, iconRender);
|
|
}
|
|
|
|
iconRender.centered = icon.center ?? false;
|
|
iconRender.text!.text = icon.text ?? "";
|
|
iconRender.text!.style = {
|
|
fontFamily: "sans-serif",
|
|
fontSize: size,
|
|
fill: "#ffffff",
|
|
};
|
|
iconRender.container.visible = true;
|
|
}
|
|
|
|
private updateAllianceIcon(
|
|
render: RenderInfo,
|
|
icon: PlayerIconDescriptor,
|
|
size: number,
|
|
) {
|
|
let iconRender = render.icons.get(icon.id);
|
|
if (!iconRender || !iconRender.alliance) {
|
|
iconRender?.container.destroy({ children: true });
|
|
const container = new PIXI.Container();
|
|
container.alpha = 0.8;
|
|
const base = new PIXI.Sprite();
|
|
const colored = new PIXI.Sprite();
|
|
const questionMark = new PIXI.Sprite();
|
|
const mask = new PIXI.Graphics();
|
|
for (const sprite of [base, colored, questionMark]) {
|
|
sprite.anchor.set(0.5);
|
|
container.addChild(sprite);
|
|
}
|
|
colored.mask = mask;
|
|
container.addChild(mask);
|
|
render.container.addChild(container);
|
|
iconRender = {
|
|
container,
|
|
centered: false,
|
|
src: icon.src,
|
|
alliance: { base, colored, questionMark, mask },
|
|
};
|
|
render.icons.set(icon.id, iconRender);
|
|
}
|
|
|
|
const baseTexture = this.assets.getTexture(allianceIconFaded);
|
|
const coloredTexture = icon.src ? this.assets.getTexture(icon.src) : null;
|
|
const questionTexture = this.assets.getTexture(questionMarkIcon);
|
|
iconRender.container.visible =
|
|
baseTexture !== null && coloredTexture !== null;
|
|
if (!baseTexture || !coloredTexture) {
|
|
return;
|
|
}
|
|
|
|
const refs = iconRender.alliance!;
|
|
refs.base.texture = baseTexture;
|
|
refs.colored.texture = coloredTexture;
|
|
refs.base.width = size;
|
|
refs.base.height = size;
|
|
refs.colored.width = size;
|
|
refs.colored.height = size;
|
|
|
|
this.myPlayer ??= this.game.myPlayer();
|
|
const allianceView = this.myPlayer
|
|
?.alliances()
|
|
.find((a) => a.other === render.player.id());
|
|
const remaining = allianceView
|
|
? Math.max(0, allianceView.expiresAt - this.game.ticks())
|
|
: 0;
|
|
const fraction = Math.max(
|
|
0,
|
|
Math.min(1, remaining / this.allianceDuration),
|
|
);
|
|
const topCut = (computeAllianceTopCutPercent(fraction) / 100) * size;
|
|
refs.mask.clear();
|
|
refs.mask
|
|
.rect(-size / 2, -size / 2 + topCut, size, Math.max(0, size - topCut))
|
|
.fill(0xffffff);
|
|
|
|
refs.questionMark.visible =
|
|
allianceView?.hasExtensionRequest === true && questionTexture !== null;
|
|
if (questionTexture) {
|
|
refs.questionMark.texture = questionTexture;
|
|
refs.questionMark.width = size;
|
|
refs.questionMark.height = size;
|
|
}
|
|
}
|
|
|
|
private layoutRender(render: RenderInfo, iconSize: number) {
|
|
const regularIcons = Array.from(render.icons.values()).filter(
|
|
(icon) => !icon.centered && icon.container.visible,
|
|
);
|
|
const centeredIcons = Array.from(render.icons.values()).filter(
|
|
(icon) => icon.centered && icon.container.visible,
|
|
);
|
|
const flagTexture = render.flagSprite?.visible
|
|
? render.flagSprite.texture
|
|
: null;
|
|
const flagAspectRatio =
|
|
flagTexture && flagTexture.height > 0
|
|
? flagTexture.width / flagTexture.height
|
|
: 1;
|
|
|
|
const layout = computeNameLayerLayout({
|
|
fontSize: render.fontSize,
|
|
iconSize,
|
|
iconCount: regularIcons.length,
|
|
centeredIconCount: centeredIcons.length,
|
|
hasFlag: render.flagSprite?.visible === true,
|
|
flagAspectRatio,
|
|
nameWidth: render.nameText.width,
|
|
troopWidth: render.troopsText.width,
|
|
});
|
|
|
|
regularIcons.forEach((icon, index) => {
|
|
const pos = layout.iconPositions[index];
|
|
icon.container.position.set(pos.x, pos.y);
|
|
});
|
|
centeredIcons.forEach((icon, index) => {
|
|
const pos = layout.centeredIconPositions[index];
|
|
icon.container.position.set(pos.x, pos.y);
|
|
});
|
|
|
|
if (render.flagSprite && layout.flag) {
|
|
render.flagSprite.position.set(layout.flag.x, layout.flag.y);
|
|
render.flagSprite.width = layout.flag.width;
|
|
render.flagSprite.height = layout.flag.height;
|
|
render.flagSprite.visible = true;
|
|
} else if (render.flagSprite) {
|
|
render.flagSprite.visible = false;
|
|
}
|
|
|
|
render.nameText.position.set(layout.nameText.x, layout.nameText.y);
|
|
render.troopsText.position.set(layout.troopText.x, layout.troopText.y);
|
|
}
|
|
|
|
private updateTraitorAlpha(render: RenderInfo, nowMs: number) {
|
|
const traitorIcon = render.icons.get(TRAITOR_ICON_ID);
|
|
if (!traitorIcon) {
|
|
return;
|
|
}
|
|
traitorIcon.container.alpha =
|
|
computeTraitorFlashAlpha(
|
|
render.player.getTraitorRemainingTicks(),
|
|
nowMs,
|
|
) * 0.8;
|
|
}
|
|
|
|
private deleteRender(render: RenderInfo) {
|
|
const index = this.renders.indexOf(render);
|
|
if (index >= 0) {
|
|
this.renders.splice(index, 1);
|
|
}
|
|
this.seenPlayers.delete(render.player);
|
|
render.container.destroy({ children: true });
|
|
}
|
|
}
|