fix alternate view perf regression (#1734)

## Description:

Have the diplomacy view only draw border, not interior tiles. Drawing
the interior tiles is a very expensive operation and caused main thread
cpu usage to spike to close to 100%.

Also change the color scheme so that neutral players are gray, and
embargoed players are red. I think long term embargo should be more of a
war state.

Added embargo update and cleaned it up to use Player instead of
PlayerID. There's no reason to pass ids around.


<img width="493" height="466" alt="Screenshot 2025-08-07 at 6 25 55 PM"
src="https://github.com/user-attachments/assets/75552036-42f1-4103-9537-234ff1c0464f"
/>

## 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 have read and accepted the CLA agreement (only required once).

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

evan
This commit is contained in:
evanpelle
2025-08-08 16:37:17 -07:00
committed by GitHub
parent 7743c39ecb
commit 7de962eb5b
13 changed files with 118 additions and 65 deletions
+1 -5
View File
@@ -531,17 +531,13 @@ export class NameLayer implements Layer {
// Embargo icon
let existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
const hasEmbargo =
myPlayer &&
(render.player.hasEmbargoAgainst(myPlayer) ||
myPlayer.hasEmbargoAgainst(render.player));
const isThemeEmbargoIcon =
existingEmbargo?.getAttribute("dark-mode") === isDarkMode.toString();
const embargoIconImageSrc = isDarkMode
? this.embargoWhiteIconImage.src
: this.embargoBlackIconImage.src;
if (myPlayer && hasEmbargo) {
if (myPlayer?.hasEmbargo(render.player)) {
// Create new icon to match theme
if (existingEmbargo && !isThemeEmbargoIcon) {
existingEmbargo.remove();
+56 -37
View File
@@ -104,10 +104,8 @@ export class TerritoryLayer implements Layer {
if (myPlayer) {
updates?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
const territory = this.game.playerBySmallID(update.betrayedID);
console.log("betrayedID", update.betrayedID);
console.log("territory", territory);
if (territory && territory instanceof PlayerView) {
this.redrawTerritory(territory);
this.redrawBorder(territory);
}
});
@@ -123,10 +121,23 @@ export class TerritoryLayer implements Layer {
: update.request.requestorID;
const territory = this.game.playerBySmallID(territoryId);
if (territory && territory instanceof PlayerView) {
this.redrawTerritory(territory);
this.redrawBorder(territory);
}
}
});
updates?.[GameUpdateType.EmbargoEvent]?.forEach((update) => {
const player = this.game.playerBySmallID(update.playerID) as PlayerView;
const embargoed = this.game.playerBySmallID(
update.embargoedID,
) as PlayerView;
if (
player.id() === myPlayer?.id() ||
embargoed.id() === myPlayer?.id()
) {
this.redrawBorder(player, embargoed);
}
});
}
const focusedPlayer = this.game.focusedPlayer();
@@ -237,7 +248,7 @@ export class TerritoryLayer implements Layer {
if (this.highlightedTerritory) {
territories.push(this.highlightedTerritory);
}
this.redrawTerritory(territories);
this.redrawBorder(...territories);
}
}
@@ -298,16 +309,15 @@ export class TerritoryLayer implements Layer {
});
}
redrawTerritory(territory: PlayerView | PlayerView[]) {
const territories = Array.isArray(territory) ? territory : [territory];
const territorySet = new Set(territories);
this.game.forEachTile((t) => {
const owner = this.game.owner(t) as PlayerView;
if (territorySet.has(owner)) {
this.paintTerritory(t);
}
});
redrawBorder(...players: PlayerView[]) {
return Promise.all(
players.map(async (player) => {
const tiles = await player.borderTiles();
tiles.borderTiles.forEach((tile: TileRef) => {
this.paintTerritory(tile, true);
});
}),
);
}
initImageData() {
@@ -419,12 +429,7 @@ export class TerritoryLayer implements Layer {
if (this.game.isBorder(tile)) {
const playerIsFocused = owner && this.game.focusedPlayer() === owner;
if (myPlayer) {
let alternativeColor = owner.isFriendly(myPlayer)
? this.theme.allyColor()
: this.theme.enemyColor();
if (owner.smallID() === myPlayer.smallID()) {
alternativeColor = this.theme.selfColor();
}
const alternativeColor = this.alternateViewColor(owner);
this.paintTile(this.alternativeImageData, tile, alternativeColor, 255);
}
if (
@@ -449,25 +454,12 @@ export class TerritoryLayer implements Layer {
this.paintTile(this.imageData, tile, useBorderColor, 255);
}
} else {
// Interior tiles
const pattern = owner.cosmetics.pattern;
const patternsEnabled = this.cachedTerritoryPatternsEnabled ?? false;
if (myPlayer) {
let alternativeColor = owner.isFriendly(myPlayer)
? this.theme.allyColor()
: this.theme.enemyColor();
// If the current player is the owner
if (owner.smallID() === myPlayer.smallID()) {
alternativeColor = this.theme.selfColor();
}
// If the tile is on a ally territory, use the ally color
this.paintTile(
this.alternativeImageData,
tile,
alternativeColor,
isHighlighted ? 150 : 60,
);
}
// Alternative view only shows borders.
this.clearAlternativeTile(tile);
if (pattern === undefined || patternsEnabled === false) {
this.paintTile(
@@ -490,6 +482,28 @@ export class TerritoryLayer implements Layer {
}
}
alternateViewColor(other: PlayerView): Colord {
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
return this.theme.neutralColor();
}
if (other.smallID() === myPlayer.smallID()) {
return this.theme.selfColor();
}
if (other.isFriendly(myPlayer)) {
return this.theme.allyColor();
}
if (!other.hasEmbargo(myPlayer)) {
return this.theme.neutralColor();
}
return this.theme.enemyColor();
}
paintAlternateViewTile(tile: TileRef, other: PlayerView) {
const color = this.alternateViewColor(other);
this.paintTile(this.alternativeImageData, tile, color, 255);
}
paintTile(imageData: ImageData, tile: TileRef, color: Colord, alpha: number) {
const offset = tile * 4;
imageData.data[offset] = color.rgba.r;
@@ -504,6 +518,11 @@ export class TerritoryLayer implements Layer {
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
clearAlternativeTile(tile: TileRef) {
const offset = tile * 4;
this.alternativeImageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
enqueueTile(tile: TileRef) {
this.tileToRenderQueue.push({
tile: tile,
+1
View File
@@ -185,6 +185,7 @@ export interface Theme {
// unit color for alternate view
selfColor(): Colord;
allyColor(): Colord;
neutralColor(): Colord;
enemyColor(): Colord;
spawnHighlightColor(): Colord;
}
+4
View File
@@ -31,6 +31,7 @@ export class PastelTheme implements Theme {
private _selfColor = colord({ r: 0, g: 255, b: 0 });
private _allyColor = colord({ r: 255, g: 255, b: 0 });
private _neutralColor = colord({ r: 128, g: 128, b: 128 });
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
@@ -159,6 +160,9 @@ export class PastelTheme implements Theme {
allyColor(): Colord {
return this._allyColor;
}
neutralColor(): Colord {
return this._neutralColor;
}
enemyColor(): Colord {
return this._enemyColor;
}
@@ -31,6 +31,7 @@ export class PastelThemeDark implements Theme {
private _selfColor = colord({ r: 0, g: 255, b: 0 });
private _allyColor = colord({ r: 255, g: 255, b: 0 });
private _neutralColor = colord({ r: 128, g: 128, b: 128 });
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
@@ -161,6 +162,9 @@ export class PastelThemeDark implements Theme {
allyColor(): Colord {
return this._allyColor;
}
neutralColor(): Colord {
return this._neutralColor;
}
enemyColor(): Colord {
return this._enemyColor;
}
+1 -1
View File
@@ -69,7 +69,7 @@ export class AttackExecution implements Execution {
this._owner.type() !== PlayerType.Bot
) {
// Don't let bots embargo since they can't trade anyway.
targetPlayer.addEmbargo(this._owner.id(), true);
targetPlayer.addEmbargo(this._owner, true);
}
}
+5 -2
View File
@@ -3,6 +3,8 @@ import { Execution, Game, Player, PlayerID } from "../game/Game";
export class EmbargoExecution implements Execution {
private active = true;
private target: Player;
constructor(
private player: Player,
private targetID: PlayerID,
@@ -15,11 +17,12 @@ export class EmbargoExecution implements Execution {
this.active = false;
return;
}
this.target = mg.player(this.targetID);
}
tick(_: number): void {
if (this.action === "start") this.player.addEmbargo(this.targetID, false);
else this.player.stopEmbargo(this.targetID);
if (this.action === "start") this.player.addEmbargo(this.target, false);
else this.player.stopEmbargo(this.target);
this.active = false;
}
+2 -2
View File
@@ -100,12 +100,12 @@ export class FakeHumanExecution implements Execution {
player.relation(other) <= Relation.Hostile &&
!player.hasEmbargoAgainst(other)
) {
player.addEmbargo(other.id(), false);
player.addEmbargo(other, false);
} else if (
player.relation(other) >= Relation.Neutral &&
player.hasEmbargoAgainst(other)
) {
player.stopEmbargo(other.id());
player.stopEmbargo(other);
}
});
}
+4 -4
View File
@@ -487,7 +487,7 @@ export interface TerraNullius {
export interface Embargo {
createdAt: Tick;
isTemporary: boolean;
target: PlayerID;
target: Player;
}
export interface Player {
@@ -595,10 +595,10 @@ export interface Player {
// Embargo
hasEmbargoAgainst(other: Player): boolean;
tradingPartners(): Player[];
addEmbargo(other: PlayerID, isTemporary: boolean): void;
addEmbargo(other: Player, isTemporary: boolean): void;
getEmbargoes(): Embargo[];
stopEmbargo(other: PlayerID): void;
endTemporaryEmbargo(other: PlayerID): void;
stopEmbargo(other: Player): void;
endTemporaryEmbargo(other: Player): void;
canTrade(other: Player): boolean;
// Attacking.
+2 -2
View File
@@ -287,9 +287,9 @@ export class GameImpl implements Game {
// Automatically remove embargoes only if they were automatically created
if (requestor.hasEmbargoAgainst(recipient))
requestor.endTemporaryEmbargo(recipient.id());
requestor.endTemporaryEmbargo(recipient);
if (recipient.hasEmbargoAgainst(requestor))
recipient.endTemporaryEmbargo(requestor.id());
recipient.endTemporaryEmbargo(requestor);
this.addUpdate({
type: GameUpdateType.AllianceRequestReply,
+10 -1
View File
@@ -45,6 +45,7 @@ export enum GameUpdateType {
BonusEvent,
RailroadEvent,
ConquestEvent,
EmbargoEvent,
}
export type GameUpdate =
@@ -65,7 +66,8 @@ export type GameUpdate =
| AllianceExtensionUpdate
| BonusEventUpdate
| RailroadUpdate
| ConquestUpdate;
| ConquestUpdate
| EmbargoUpdate;
export interface BonusEventUpdate {
type: GameUpdateType.BonusEvent;
@@ -255,3 +257,10 @@ export interface UnitIncomingUpdate {
messageType: MessageType;
playerID: number;
}
export interface EmbargoUpdate {
type: GameUpdateType.EmbargoEvent;
event: "start" | "stop";
playerID: number;
embargoedID: number;
}
+4
View File
@@ -326,6 +326,10 @@ export class PlayerView {
return this.data.embargoes.has(other.id());
}
hasEmbargo(other: PlayerView): boolean {
return this.hasEmbargoAgainst(other) || other.hasEmbargoAgainst(this);
}
profile(): Promise<PlayerProfile> {
return this.game.worker.playerProfile(this.smallID());
}
+24 -11
View File
@@ -642,27 +642,40 @@ export class PlayerImpl implements Player {
return !embargo && other.id() !== this.id();
}
addEmbargo(other: PlayerID, isTemporary: boolean): void {
const embargo = this.embargoes.get(other);
getEmbargoes(): Embargo[] {
return [...this.embargoes.values()];
}
addEmbargo(other: Player, isTemporary: boolean): void {
const embargo = this.embargoes.get(other.id());
if (embargo !== undefined && !embargo.isTemporary) return;
this.embargoes.set(other, {
this.mg.addUpdate({
type: GameUpdateType.EmbargoEvent,
event: "start",
playerID: this.smallID(),
embargoedID: other.smallID(),
});
this.embargoes.set(other.id(), {
createdAt: this.mg.ticks(),
isTemporary: isTemporary,
target: other,
});
}
getEmbargoes(): Embargo[] {
return [...this.embargoes.values()];
stopEmbargo(other: Player): void {
this.embargoes.delete(other.id());
this.mg.addUpdate({
type: GameUpdateType.EmbargoEvent,
event: "stop",
playerID: this.smallID(),
embargoedID: other.smallID(),
});
}
stopEmbargo(other: PlayerID): void {
this.embargoes.delete(other);
}
endTemporaryEmbargo(other: PlayerID): void {
const embargo = this.embargoes.get(other);
endTemporaryEmbargo(other: Player): void {
const embargo = this.embargoes.get(other.id());
if (embargo !== undefined && !embargo.isTemporary) return;
this.stopEmbargo(other);