Merge branch 'openfrontio:main' into custom-flag

This commit is contained in:
Aotumuri
2025-04-20 08:06:34 +09:00
committed by GitHub
118 changed files with 5948 additions and 1227 deletions
+10
View File
@@ -12,6 +12,7 @@ import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
import { OptionsMenu } from "./layers/OptionsMenu";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
@@ -125,6 +126,14 @@ export function createRenderer(
playerPanel.eventBus = eventBus;
playerPanel.emojiTable = emojiTable;
const multiTabModal = document.querySelector(
"multi-tab-modal",
) as MultiTabModal;
if (!(multiTabModal instanceof MultiTabModal)) {
console.error("multi-tab modal not found");
}
multiTabModal.game = game;
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus),
@@ -153,6 +162,7 @@ export function createRenderer(
optionsMenu,
topBar,
playerPanel,
multiTabModal,
];
return new GameRenderer(
+137
View File
@@ -0,0 +1,137 @@
import { Colord } from "colord";
import atomBombSprite from "../../../resources/sprites/atombomb.png";
import hydrogenBombSprite from "../../../resources/sprites/hydrogenbomb.png";
import mirvSprite from "../../../resources/sprites/mirv2.png";
import samMissileSprite from "../../../resources/sprites/samMissile.png";
import tradeShipSprite from "../../../resources/sprites/tradeship.png";
import transportShipSprite from "../../../resources/sprites/transportship.png";
import warshipSprite from "../../../resources/sprites/warship.png";
import { Theme } from "../../core/configuration/Config";
import { UnitType } from "../../core/game/Game";
import { UnitView } from "../../core/game/GameView";
const SPRITE_CONFIG: Partial<Record<UnitType, string>> = {
[UnitType.TransportShip]: transportShipSprite,
[UnitType.Warship]: warshipSprite,
[UnitType.SAMMissile]: samMissileSprite,
[UnitType.AtomBomb]: atomBombSprite,
[UnitType.HydrogenBomb]: hydrogenBombSprite,
[UnitType.TradeShip]: tradeShipSprite,
[UnitType.MIRV]: mirvSprite,
};
const spriteMap: Map<UnitType, ImageBitmap> = new Map();
// preload all images
export const loadAllSprites = async (): Promise<void> => {
const entries = Object.entries(SPRITE_CONFIG);
const totalSprites = entries.length;
let loadedCount = 0;
await Promise.all(
entries.map(async ([unitType, url]) => {
const typedUnitType = unitType as UnitType;
if (!url || url === "") {
console.warn(`No sprite URL for ${typedUnitType}, skipping...`);
return;
}
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = url;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = (err) => reject(err);
});
const bitmap = await createImageBitmap(img);
spriteMap.set(typedUnitType, bitmap);
loadedCount++;
if (loadedCount === totalSprites) {
console.log("All sprites loaded.");
}
} catch (err) {
console.error(`Failed to load sprite for ${typedUnitType}:`, err);
}
}),
);
};
const getSpriteForUnit = (unitType: UnitType): ImageBitmap | null => {
return spriteMap.get(unitType) ?? null;
};
export const isSpriteReady = (unitType: UnitType): boolean => {
return spriteMap.has(unitType);
};
const coloredSpriteCache: Map<string, HTMLCanvasElement> = new Map();
// puts the sprite in an canvas colors it and caches the colored canvas
export const getColoredSprite = (
unit: UnitView,
theme: Theme,
customTerritoryColor?: Colord,
customBorderColor?: Colord,
): HTMLCanvasElement => {
const owner = unit.owner();
const territoryColor = customTerritoryColor ?? theme.territoryColor(owner);
const borderColor = customBorderColor ?? theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const colorKey = customTerritoryColor
? customTerritoryColor.toRgbString()
: "";
const key = owner.id() + unit.type() + colorKey;
if (coloredSpriteCache.has(key)) {
return coloredSpriteCache.get(key)!;
}
const sprite = getSpriteForUnit(unit.type());
const territoryRgb = territoryColor.toRgb();
const borderRgb = borderColor.toRgb();
const spawnHighlightRgb = spawnHighlightColor.toRgb();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = sprite.width;
canvas.height = sprite.height;
ctx.drawImage(sprite, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (r === 180 && g === 180 && b === 180) {
data[i] = territoryRgb.r;
data[i + 1] = territoryRgb.g;
data[i + 2] = territoryRgb.b;
}
if (r === 70 && g === 70 && b === 70) {
data[i] = borderRgb.r;
data[i + 1] = borderRgb.g;
data[i + 2] = borderRgb.b;
}
if (r === 130 && g === 130 && b === 130) {
data[i] = spawnHighlightRgb.r;
data[i + 1] = spawnHighlightRgb.g;
data[i + 2] = spawnHighlightRgb.b;
}
}
ctx.putImageData(imageData, 0.5, 0.5);
coloredSpriteCache.set(key, canvas);
return canvas;
};
+8 -4
View File
@@ -97,6 +97,10 @@ export class TransformHandler {
}
screenBoundingRect(): [Cell, Cell] {
const canvasRect = this.boundingRect();
const canvasWidth = canvasRect.width;
const canvasHeight = canvasRect.height;
const LeftX = -this.game.width() / 2 / this.scale + this.offsetX;
const TopY = -this.game.height() / 2 / this.scale + this.offsetY;
@@ -104,12 +108,12 @@ export class TransformHandler {
const gameTopY = TopY + this.game.height() / 2;
const rightX =
(screen.width - this.game.width() / 2) / this.scale + this.offsetX;
const rightY =
(screen.height - this.game.height() / 2) / this.scale + this.offsetY;
(canvasWidth - this.game.width() / 2) / this.scale + this.offsetX;
const bottomY =
(canvasHeight - this.game.height() / 2) / this.scale + this.offsetY;
const gameRightX = rightX + this.game.width() / 2;
const gameBottomY = rightY + this.game.height() / 2;
const gameBottomY = bottomY + this.game.height() / 2;
return [
new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)),
+16 -1
View File
@@ -56,8 +56,16 @@ export class ControlPanel extends LitElement implements Layer {
private _popRateIsIncreasing: boolean = true;
private init_: boolean = false;
init() {
this.attackRatio = 0.2;
this.attackRatio = Number(
localStorage.getItem("settings.attackRatio") ?? "0.2",
);
this.targetTroopRatio = Number(
localStorage.getItem("settings.troopRatio") ?? "0.95",
);
this.init_ = true;
this.uiState.attackRatio = this.attackRatio;
this.currentTroopRatio = this.targetTroopRatio;
this.eventBus.on(AttackRatioEvent, (event) => {
@@ -87,6 +95,13 @@ export class ControlPanel extends LitElement implements Layer {
}
tick() {
if (this.init_) {
this.eventBus.emit(
new SendSetTargetTroopRatioEvent(this.targetTroopRatio),
);
this.init_ = false;
}
if (!this._isVisible && !this.game.inSpawnPhase()) {
this.setVisibile(true);
}
+1 -1
View File
@@ -282,7 +282,7 @@ export class EventsDisplay extends LitElement implements Layer {
});
} else if (betrayed === myPlayer) {
this.addEvent({
description: `${traitor.name()}, broke their alliance with you`,
description: `${traitor.name()} broke their alliance with you`,
type: MessageType.ERROR,
highlight: true,
createdAt: this.game.ticks(),
+131
View File
@@ -0,0 +1,131 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { GameType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { MultiTabDetector } from "../../MultiTabDetector";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
@customElement("multi-tab-modal")
export class MultiTabModal extends LitElement implements Layer {
public game: GameView;
private detector: MultiTabDetector;
@property({ type: Number }) duration: number = 5000;
@state() private countdown: number = 5;
@state() private isVisible: boolean = false;
private intervalId?: number;
// Disable shadow DOM to allow Tailwind classes to work
createRenderRoot() {
return this;
}
tick() {
if (
this.game.inSpawnPhase() ||
this.game.config().gameConfig().gameType == GameType.Singleplayer
) {
return;
}
if (!this.detector) {
this.detector = new MultiTabDetector();
this.detector.startMonitoring((duration: number) => {
this.show(duration);
});
}
}
// Show the modal with penalty information
public show(duration: number): void {
if (!this.game.myPlayer()?.isAlive()) {
return;
}
this.duration = duration;
this.countdown = Math.ceil(duration / 1000);
this.isVisible = true;
// Start countdown timer
this.intervalId = window.setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
this.hide();
}
}, 1000);
this.requestUpdate();
}
// Hide the modal
public hide(): void {
this.isVisible = false;
if (this.intervalId) {
window.clearInterval(this.intervalId);
this.intervalId = undefined;
}
// Dispatch event when modal is closed
this.dispatchEvent(
new CustomEvent("penalty-complete", {
bubbles: true,
composed: true,
}),
);
this.requestUpdate();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.intervalId) {
window.clearInterval(this.intervalId);
}
}
render() {
if (!this.isVisible) {
return html``;
}
return html`
<div
class="fixed inset-0 z-50 overflow-auto bg-red-500/20 flex items-center justify-center"
>
<div
class="relative p-6 bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full m-4 transition-all transform"
>
<h2 class="text-2xl font-bold mb-4 text-red-600 dark:text-red-400">
${translateText("multi_tab.warning")}
</h2>
<p class="mb-4 text-gray-800 dark:text-gray-200">
${translateText("multi_tab.detected")}
</p>
<p class="mb-4 text-gray-800 dark:text-gray-200">
${translateText("multi_tab.please_wait")}
<span class="font-bold text-xl">${this.countdown}</span>
${translateText("multi_tab.seconds")}
</p>
<div
class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-4"
>
<div
class="bg-red-600 dark:bg-red-500 h-2.5 rounded-full transition-all duration-1000 ease-linear"
style="width: ${(this.countdown / (this.duration / 1000)) * 100}%"
></div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
${translateText("multi_tab.explanation")}
</p>
</div>
</div>
`;
}
}
+8 -2
View File
@@ -145,8 +145,14 @@ export class PlayerPanel extends LitElement implements Layer {
this.eventBus.on(MouseUpEvent, (e: MouseEvent) => this.hide());
}
tick() {
this.requestUpdate();
async tick() {
if (this.isVisible && this.tile) {
const myPlayer = this.g.myPlayer();
if (myPlayer !== null && myPlayer.isAlive()) {
this.actions = await myPlayer.actions(this.tile);
this.requestUpdate();
}
}
}
getTotalNukesSent(otherId: PlayerID): number {
+35 -14
View File
@@ -8,7 +8,7 @@ import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import { consolex } from "../../../core/Consolex";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions } from "../../../core/game/Game";
import { Cell, PlayerActions, TerraNullius } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
@@ -44,6 +44,7 @@ export class RadialMenu implements Layer {
private clickedCell: Cell | null = null;
private lastClosed: number = 0;
private originalTileOwner: PlayerView | TerraNullius;
private menuElement: d3.Selection<HTMLDivElement, unknown, null, undefined>;
private isVisible: boolean = false;
private readonly menuItems = new Map([
@@ -138,6 +139,7 @@ export class RadialMenu implements Layer {
.style("touch-action", "none")
.on("contextmenu", (e) => {
e.preventDefault();
this.hideRadialMenu();
});
const svg = this.menuElement
@@ -266,8 +268,26 @@ export class RadialMenu implements Layer {
.style("pointer-events", "none");
}
tick() {
// Update logic if needed
async tick() {
// Only update when menu is visible
if (!this.isVisible || this.clickedCell === null) return;
const myPlayer = this.g.myPlayer();
if (myPlayer === null || !myPlayer.isAlive()) return;
const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y);
if (this.originalTileOwner.isPlayer()) {
if (this.g.owner(tile) != this.originalTileOwner) {
this.closeMenu();
return;
}
} else {
if (this.g.owner(tile).isPlayer() || this.g.owner(tile) == myPlayer) {
this.closeMenu();
return;
}
}
const actions = await myPlayer.actions(tile);
this.disableAllButtons();
this.handlePlayerActions(myPlayer, actions, tile);
}
renderLayer(context: CanvasRenderingContext2D) {
@@ -290,12 +310,7 @@ export class RadialMenu implements Layer {
} else {
this.showRadialMenu(event.x, event.y);
}
this.enableCenterButton(false);
for (const item of this.menuItems.values()) {
item.disabled = true;
this.updateMenuItemState(item);
}
this.disableAllButtons();
this.clickedCell = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
@@ -304,7 +319,7 @@ export class RadialMenu implements Layer {
return;
}
const tile = this.g.ref(this.clickedCell.x, this.clickedCell.y);
this.originalTileOwner = this.g.owner(tile);
if (this.g.inSpawnPhase()) {
if (this.g.isLand(tile) && !this.g.hasOwner(tile)) {
this.enableCenterButton(true);
@@ -312,10 +327,8 @@ export class RadialMenu implements Layer {
return;
}
const myPlayer = this.g
.playerViews()
.find((p) => p.clientID() == this.clientID);
if (!myPlayer) {
const myPlayer = this.g.myPlayer();
if (myPlayer === null) {
consolex.warn("my player not found");
return;
}
@@ -429,6 +442,14 @@ export class RadialMenu implements Layer {
this.hideRadialMenu();
}
private disableAllButtons() {
this.enableCenterButton(false);
for (const item of this.menuItems.values()) {
item.disabled = true;
this.updateMenuItemState(item);
}
}
private activateMenuElement(
slot: Slot,
color: string,
+49 -27
View File
@@ -1,13 +1,11 @@
import { blue, red } from "../../../core/configuration/Colors";
import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
export class SpawnTimer implements Layer {
private ratio = 0;
private leftColor = "rgba(0, 128, 255, 0.7)";
private rightColor = "rgba(0, 0, 0, 0.5)";
private ratios = [0];
private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"];
constructor(
private game: GameView,
@@ -18,27 +16,35 @@ export class SpawnTimer implements Layer {
tick() {
if (this.game.inSpawnPhase()) {
this.ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns();
this.ratios[0] =
this.game.ticks() / this.game.config().numSpawnPhaseTurns();
return;
}
this.ratios = [];
this.colors = [];
if (this.game.config().gameConfig().gameMode != GameMode.Team) {
this.ratio = 0;
return;
}
const numBlueTiles = this.game
.players()
.filter((p) => p.team() == Team.Blue)
.reduce((acc, p) => acc + p.numTilesOwned(), 0);
const teamTiles: Map<Team, number> = new Map();
for (const player of this.game.players()) {
const team = player.team();
const tiles = teamTiles.get(team) ?? 0;
const sum = tiles + player.numTilesOwned();
teamTiles.set(team, sum);
}
const numRedTiles = this.game
.players()
.filter((p) => p.team() == Team.Red)
.reduce((acc, p) => acc + p.numTilesOwned(), 0);
this.ratio = numBlueTiles / (numBlueTiles + numRedTiles);
this.leftColor = blue.toRgbString();
this.rightColor = red.toRgbString();
const theme = this.game.config().theme();
const total = sumIterator(teamTiles.values());
if (total === 0) return;
for (const [team, count] of teamTiles) {
const ratio = count / total;
const color = theme.teamColor(team).toRgbString();
this.ratios.push(ratio);
this.colors.push(color);
}
}
shouldTransform(): boolean {
@@ -46,18 +52,34 @@ export class SpawnTimer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.ratio == 0) {
return;
}
if (this.ratios === null) return;
if (this.ratios.length === 0) return;
if (this.colors.length === 0) return;
const barHeight = 10;
const barBackgroundWidth = this.transformHandler.width();
const barWidth = this.transformHandler.width();
// Draw bar background
context.fillStyle = this.rightColor;
context.fillRect(0, 0, barBackgroundWidth, barHeight);
let x = 0;
let filledRatio = 0;
for (let i = 0; i < this.ratios.length && i < this.colors.length; i++) {
const ratio = this.ratios[i];
const segmentWidth = barWidth * ratio;
context.fillStyle = this.leftColor;
context.fillRect(0, 0, barBackgroundWidth * this.ratio, barHeight);
context.fillStyle = this.colors[i];
context.fillRect(x, 0, segmentWidth, barHeight);
x += segmentWidth;
filledRatio += ratio;
}
}
}
function sumIterator(values: MapIterator<number>) {
// To use reduce, we'd need to allocate an array:
// return Array.from(values).reduce((sum, v) => sum + v, 0);
let total = 0;
for (const value of values) {
total += value;
}
return total;
}
+64 -149
View File
@@ -19,6 +19,8 @@ import { MoveWarshipIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { getColoredSprite, loadAllSprites } from "../SpriteLoader";
enum Relationship {
Self,
Ally,
@@ -77,6 +79,8 @@ export class UnitLayer implements Layer {
this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e));
this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e));
this.redraw();
loadAllSprites();
}
/**
@@ -199,6 +203,18 @@ export class UnitLayer implements Layer {
?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
this.boatToTrail.forEach((trail, unit) => {
for (const t of trail) {
this.paintCell(
this.game.x(t),
this.game.y(t),
this.relationship(unit),
this.theme.territoryColor(unit.owner()),
150,
this.transportShipTrailContext,
);
}
});
}
private relationship(unit: UnitView): Relationship {
@@ -258,56 +274,13 @@ export class UnitLayer implements Layer {
this.clearCell(this.game.x(t), this.game.y(t));
}
if (!unit.isActive()) {
return;
}
let outerColor = this.theme.territoryColor(unit.owner());
if (unit.warshipTargetId()) {
const targetOwner = this.game
.units()
.find((u) => u.id() == unit.warshipTargetId())
?.owner();
if (targetOwner == this.myPlayer) {
outerColor = colord({ r: 200, b: 0, g: 0 });
if (unit.isActive()) {
if (unit.warshipTargetId()) {
this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 }));
} else {
this.drawSprite(unit);
}
}
// Paint outer territory
for (const t of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), 5, false),
)) {
this.paintCell(this.game.x(t), this.game.y(t), rel, outerColor, 255);
}
// Paint border
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 4),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner()),
255,
);
}
// Paint inner territory
for (const t of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), 1, false),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner()),
255,
);
}
}
private handleShellEvent(unit: UnitView) {
@@ -355,32 +328,13 @@ export class UnitLayer implements Layer {
}
if (unit.isActive()) {
for (const t of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), range, false),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.spawnHighlightColor(),
255,
);
}
this.paintCell(
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner()),
255,
);
this.drawSprite(unit);
}
}
private handleNuke(unit: UnitView) {
const rel = this.relationship(unit);
let range = 0;
switch (unit.type()) {
case UnitType.AtomBomb:
range = 4;
@@ -393,7 +347,6 @@ export class UnitLayer implements Layer {
break;
}
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
euclDistFN(unit.lastTile(), range, false),
@@ -402,30 +355,7 @@ export class UnitLayer implements Layer {
}
if (unit.isActive()) {
for (const t of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), range, false),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.spawnHighlightColor(),
255,
);
}
for (const t of this.game.bfs(
unit.tile(),
euclDistFN(unit.tile(), 2, false),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner()),
255,
);
}
this.drawSprite(unit);
}
}
@@ -447,8 +377,6 @@ export class UnitLayer implements Layer {
}
private handleTradeShipEvent(unit: UnitView) {
const rel = this.relationship(unit);
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
@@ -458,33 +386,7 @@ export class UnitLayer implements Layer {
}
if (unit.isActive()) {
// Paint territory
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 2),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner()),
255,
);
}
// Paint border
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 1),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner()),
255,
);
}
this.drawSprite(unit);
}
}
@@ -500,7 +402,7 @@ export class UnitLayer implements Layer {
// Clear previous area
for (const t of this.game.bfs(
unit.lastTile(),
manhattanDistFN(unit.lastTile(), 2),
manhattanDistFN(unit.lastTile(), 4),
)) {
this.clearCell(this.game.x(t), this.game.y(t));
}
@@ -518,31 +420,7 @@ export class UnitLayer implements Layer {
);
}
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 2),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner()),
255,
);
}
for (const t of this.game.bfs(
unit.tile(),
manhattanDistFN(unit.tile(), 1),
)) {
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner()),
255,
);
}
this.drawSprite(unit);
} else {
for (const t of trail) {
this.clearCell(
@@ -606,4 +484,41 @@ export class UnitLayer implements Layer {
) {
context.clearRect(x, y, 1, 1);
}
drawSprite(unit: UnitView, customTerritoryColor?: Colord) {
const x = this.game.x(unit.tile());
const y = this.game.y(unit.tile());
let alternateViewColor = null;
if (this.alternateView) {
const rel = this.relationship(unit);
switch (rel) {
case Relationship.Self:
alternateViewColor = this.theme.selfColor();
break;
case Relationship.Ally:
alternateViewColor = this.theme.allyColor();
break;
case Relationship.Enemy:
alternateViewColor = this.theme.enemyColor();
break;
}
}
const sprite = getColoredSprite(
unit,
this.theme,
alternateViewColor ?? customTerritoryColor,
alternateViewColor,
);
this.context.drawImage(
sprite,
Math.round(x - sprite.width / 2),
Math.round(y - sprite.height / 2),
sprite.width,
sprite.width,
);
}
}
+3 -29
View File
@@ -149,7 +149,7 @@ export class WinModal extends LitElement implements Layer {
return html`
<div class="win-modal ${this.isVisible ? "visible" : ""}">
<h2>${this._title || ""}</h2>
${this.supportHTML()}
${this.innerHtml()}
<div class="button-container">
<button @click=${this._handleExit}>Exit Game</button>
<button @click=${this.hide}>Keep Playing</button>
@@ -158,35 +158,9 @@ export class WinModal extends LitElement implements Layer {
`;
}
updated(changedProperties) {
super.updated(changedProperties);
// Initialize ads if modal is visible and showing ads
if (changedProperties.has("isVisible") && this.isVisible && !this.won) {
try {
setTimeout(() => {
(adsbygoogle = window.adsbygoogle || []).push({});
}, 0);
} catch (error) {
console.error("Error initializing ad:", error);
}
}
}
supportHTML() {
innerHtml() {
return html`
<div style="text-align: center; margin: 15px 0;">
<p>
Like the game? Help make this my full-time project!
<a
href="https://discord.gg/k22YrnAzGp"
target="_blank"
rel="noopener noreferrer"
style="color: #0096ff; text-decoration: underline; display: block; margin-top: 5px;"
>
Support the game!
</a>
</p>
</div>
<div style="text-align: center; margin: 15px 0; line-height: 1.5;"></div>
`;
}