Fow test 3 (based on files shared on 2 Jan 2026. Misses files like Game.ts that where present in the previously shared files)

This commit is contained in:
variablevince
2026-01-02 22:32:23 +01:00
parent 4877e202f6
commit baf866d099
14 changed files with 1850 additions and 98 deletions
+15 -12
View File
@@ -84,8 +84,6 @@
"radial_attack": "Open the Attack menu.",
"radial_info": "Open the Info menu.",
"radial_boat": "Send a Boat (transport ship) to attack at the selected location. Only available if you have access to water.",
"radial_donate_troops": "Donate troops equivalent to your attack ratio slider percentage to the ally you opened the radial menu on.",
"radial_donate_gold": "Opens the gold donation slider menu so you can quickly send allies gold.",
"radial_close": "Close the menu.",
"info_title": "Info menu",
"info_enemy_desc": "Contains information such as the selected player's name, gold, troops, stopped trading with you, nukes sent to you, and if the player is a traitor. Stopped trading means you won't receive gold from them and they won't sent you gold via trade ships. Manually (if the player clicked \"Stop trading\", which lasts until you both click \"Start trading\") or automatically (if you betrayed your alliance, which lasts until you become allies again or after 5 minutes). Traitor displays Yes for 30 seconds when the player betrayed and attacked a player who was in an alliance with them. The icons below represent the following interactions:",
@@ -276,11 +274,11 @@
"public_lobby": {
"join": "Join next Game",
"waiting": "players waiting",
"teams_Duos": "{team_count} of 2 (Duos)",
"teams_Trios": "{team_count} of 3 (Trios)",
"teams_Quads": "{team_count} of 4 (Quads)",
"waiting_for_players": "Waiting for players",
"starting_game": "Starting game…",
"teams_Duos": "of 2 (Duos)",
"teams_Trios": "of 3 (Trios)",
"teams_Quads": "of 4 (Quads)",
"teams_hvn": "Humans vs Nations",
"teams_hvn_detailed": "{num} Humans vs {num} Nations",
"teams": "{num} teams",
@@ -348,7 +346,7 @@
"code_license": "Code licensed under AGPL-3.0 (no warranty)"
},
"difficulty": {
"difficulty": "Nation difficulty",
"difficulty": "Difficulty",
"easy": "Easy",
"medium": "Medium",
"hard": "Hard",
@@ -356,7 +354,8 @@
},
"game_mode": {
"ffa": "Free for All",
"teams": "Teams"
"teams": "Teams",
"fog_of_war": "Fog of War"
},
"select_lang": {
"title": "Select Language"
@@ -603,7 +602,13 @@
"warships": "Warships",
"cities": "Cities",
"show_control": "Show Control",
"show_units": "Show Units"
"show_units": "Show Units",
"local_mode": "Local Mode",
"global_mode": "Global Mode",
"eliminated": "Eliminated",
"view": "View",
"switch_to_global": "Switch to Global",
"switch_to_local": "Switch to Local"
},
"player_info_overlay": {
"type": "Type",
@@ -702,10 +707,7 @@
"send_alliance": "Send Alliance",
"send_troops": "Send Troops",
"send_gold": "Send Gold",
"emotes": "Emojis",
"arc_up": "Upward arc",
"arc_down": "Downward arc",
"flip_rocket_trajectory": "Flip rocket trajectory"
"emotes": "Emojis"
},
"send_troops_modal": {
"title_with_name": "Send Troops to {name}",
@@ -862,3 +864,4 @@
"mode_team": "Team"
}
}
+49 -1
View File
@@ -12,7 +12,7 @@ import {
import { createPartialGameRecord, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
import { PlayerActions, UnitType } from "../core/game/Game";
import { GameMode, PlayerActions, UnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
@@ -501,6 +501,22 @@ export class ClientGameRunner {
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (this.gameView.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogOfWarLayer = this.renderer.getFogOfWarLayer();
if (fogOfWarLayer && typeof fogOfWarLayer.getFogValueAt === 'function') {
const tileX = this.gameView.x(tile);
const tileY = this.gameView.y(tile);
const idx = tileY * this.gameView.width() + tileX;
const fogValue = fogOfWarLayer.getFogValueAt(idx);
// If it's exactly fog = 1, don't allow any type of attack (ground or naval)
if (fogValue >= 1.0) {
return;
}
}
}
this.myPlayer.actions(tile).then((actions) => {
if (this.myPlayer === null) return;
if (actions.canAttack) {
@@ -598,6 +614,22 @@ export class ClientGameRunner {
this.myPlayer = myPlayer;
}
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (this.gameView.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogOfWarLayer = this.renderer.getFogOfWarLayer();
if (fogOfWarLayer && typeof fogOfWarLayer.getFogValueAt === 'function') {
const tileX = this.gameView.x(tile);
const tileY = this.gameView.y(tile);
const idx = tileY * this.gameView.width() + tileX;
const fogValue = fogOfWarLayer.getFogValueAt(idx);
// If it's exactly fog = 1, don't allow the boat attack
if (fogValue >= 1.0) {
return;
}
}
}
this.myPlayer.actions(tile).then((actions) => {
if (this.canBoatAttack(actions) !== false) {
this.sendBoatAttackIntent(tile);
@@ -617,6 +649,22 @@ export class ClientGameRunner {
this.myPlayer = myPlayer;
}
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (this.gameView.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogOfWarLayer = this.renderer.getFogOfWarLayer();
if (fogOfWarLayer && typeof fogOfWarLayer.getFogValueAt === 'function') {
const tileX = this.gameView.x(tile);
const tileY = this.gameView.y(tile);
const idx = tileY * this.gameView.width() + tileX;
const fogValue = fogOfWarLayer.getFogValueAt(idx);
// If it's exactly fog = 1, don't allow the attack (naval or ground invasion)
if (fogValue >= 1.0) {
return;
}
}
}
this.myPlayer.actions(tile).then((actions) => {
if (this.myPlayer === null) return;
if (actions.canAttack) {
+17 -2
View File
@@ -190,10 +190,20 @@ export class SinglePlayerModal extends LitElement {
${translateText("game_mode.teams")}
</div>
</div>
<div
class="option-card ${this.gameMode === GameMode.FogOfWar
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.FogOfWar)}
>
<div class="option-card-title">
${translateText("game_mode.fog_of_war")}
</div>
</div>
</div>
</div>
${this.gameMode === GameMode.FFA
${this.gameMode === GameMode.FFA || this.gameMode === GameMode.FogOfWar
? ""
: html`
<!-- Team Count Selection -->
@@ -218,7 +228,7 @@ export class SinglePlayerModal extends LitElement {
<div
class="option-card ${this.teamCount === o
? "selected"
: ""}"
: ""}
@click=${() => this.handleTeamCountSelection(o)}
>
<div class="option-card-title">
@@ -498,6 +508,11 @@ export class SinglePlayerModal extends LitElement {
private handleGameModeSelection(value: GameMode) {
this.gameMode = value;
// When Fog of War mode is selected, always enable random spawn
if (value === GameMode.FogOfWar) {
this.randomSpawn = true;
}
}
private handleTeamCountSelection(value: TeamCountConfig) {
+67 -13
View File
@@ -19,7 +19,7 @@ import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { Leaderboard, ViewPlayerVisionEvent } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
@@ -41,6 +41,7 @@ import { UILayer } from "./layers/UILayer";
import { UnitDisplay } from "./layers/UnitDisplay";
import { UnitLayer } from "./layers/UnitLayer";
import { WinModal } from "./layers/WinModal";
import { FogOfWarLayer } from "./layers/FogOfWarLayer";
export function createRenderer(
canvas: HTMLCanvasElement,
@@ -209,7 +210,10 @@ export function createRenderer(
}
headsUpMessage.game = game;
// Create FogOfWarLayer first so we can pass it to other layers
// Camada responsável por gerenciar o efeito visual do Fog of War
const structureLayer = new StructureLayer(game, eventBus, transformHandler);
const fogOfWarLayer = new FogOfWarLayer(game, transformHandler);
const samRadiusLayer = new SAMRadiusLayer(game, eventBus, uiState);
const performanceOverlay = document.querySelector(
@@ -236,6 +240,22 @@ export function createRenderer(
// When updating these layers please be mindful of the order.
// Try to group layers by the return value of shouldTransform.
// Create MainRadialMenu first to set references
const mainRadialMenu = new MainRadialMenu(
eventBus,
game,
transformHandler,
emojiTable as EmojiTable,
buildMenu,
uiState,
playerPanel,
);
// Set references to FogOfWarLayer
const nameLayer = new NameLayer(game, transformHandler, eventBus, fogOfWarLayer);
mainRadialMenu.setFogOfWarLayer(fogOfWarLayer);
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
@@ -243,24 +263,17 @@ export function createRenderer(
new RailroadLayer(game, eventBus, transformHandler),
structureLayer,
samRadiusLayer,
new UnitLayer(game, eventBus, transformHandler),
new UnitLayer(game, eventBus, transformHandler, fogOfWarLayer),
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
new UILayer(game, eventBus, transformHandler, fogOfWarLayer),
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new NameLayer(game, transformHandler, eventBus),
nameLayer,
fogOfWarLayer,
eventsDisplay,
chatDisplay,
buildMenu,
new MainRadialMenu(
eventBus,
game,
transformHandler,
emojiTable as EmojiTable,
buildMenu,
uiState,
playerPanel,
),
mainRadialMenu,
spawnTimer,
leaderboard,
gameLeftSidebar,
@@ -280,6 +293,27 @@ export function createRenderer(
performanceOverlay,
];
// Set reference to FogOfWarLayer for Leaderboard
const fogOfWarLayerRef = layers.find(layer => layer.constructor.name === 'FogOfWarLayer') as any;
if (fogOfWarLayerRef && leaderboard) {
leaderboard.fogOfWarLayer = fogOfWarLayerRef;
}
// Set reference to FogOfWarLayer for PlayerInfoOverlay
if (fogOfWarLayerRef && playerInfo) {
playerInfo.fogOfWarLayer = fogOfWarLayerRef;
}
// Set reference to FogOfWarLayer for MainRadialMenu
if (fogOfWarLayerRef && mainRadialMenu) {
mainRadialMenu.setFogOfWarLayer(fogOfWarLayerRef);
}
// Set reference to FogOfWarLayer for Leaderboard
if (fogOfWarLayerRef && leaderboard) {
leaderboard.fogOfWarLayer = fogOfWarLayerRef;
}
return new GameRenderer(
game,
eventBus,
@@ -310,6 +344,22 @@ export class GameRenderer {
initialize() {
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
// Handle ViewPlayerVisionEvent to switch between player views in Fog of War mode
this.eventBus.on(ViewPlayerVisionEvent, (event: ViewPlayerVisionEvent) => {
// Find the FogOfWarLayer and set the observed player
const fogOfWarLayer = this.layers.find(l => l instanceof FogOfWarLayer) as FogOfWarLayer;
if (fogOfWarLayer) {
fogOfWarLayer.setObservedPlayer(event.player);
}
// Also update the Leaderboard if available
const leaderboard = this.layers.find(l => l instanceof Leaderboard) as Leaderboard;
if (leaderboard) {
leaderboard.setViewingPlayerVision(event.player);
}
});
this.layers.forEach((l) => l.init?.());
// only append the canvas if it's not already in the document to avoid reparenting side-effects
@@ -407,6 +457,10 @@ export class GameRenderer {
this.layers.forEach((l) => l.tick?.());
}
getFogOfWarLayer() {
return this.layers.find(layer => layer.constructor.name === 'FogOfWarLayer') as any;
}
resize(width: number, height: number): void {
this.canvas.width = Math.ceil(width / window.devicePixelRatio);
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
+619
View File
@@ -0,0 +1,619 @@
import { GameView } from "../../../core/game/GameView";
import { GameMode } from "../../../core/game/Game";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
interface FogChunk {
x: number; y: number; isDirty: boolean;
startX: number; startY: number; endX: number; endY: number;
}
/**
* Camada gráfica responsável por renderizar o efeito visual do Fog of War (nevoeiro de guerra).
* Controla a visibilidade dos elementos gráficos baseado na lógica do servidor.
*/
export class FogOfWarLayer implements Layer {
private fullW: number;
private fullH: number;
private scale: number = 1;
private lowW: number;
private lowH: number;
private fogMap: Uint8ClampedArray;
private visionBuffer: Uint8ClampedArray;
private territoryMap: Uint8Array;
private fogCanvas: HTMLCanvasElement;
private fogCtx: CanvasRenderingContext2D;
private fogImageData: ImageData;
private chunks: FogChunk[] = [];
private readonly CHUNK_SIZE = 16;
private lastVisionUpdate = 0;
private readonly VISION_INTERVAL = 100;
private _observedPlayer: any = null;
// Cache to track units and borders that have already granted vision
private visionCache: Map<string, {centerTile: number, radius: number}> = new Map();
private borderVisionCache: Map<string, Set<number>> = new Map();
private MOBILE_FOG_THRESHOLD = 204; // Represents fog 0.8 (204/255 ≈ 0.8)
// Track previous alliances to detect changes
private previousAllies: Set<string> = new Set();
constructor(
private game: GameView,
private transformHandler: TransformHandler,
) {
this.fullW = game.width();
this.fullH = game.height();
const area = this.fullW * this.fullH;
if (area > 3_000_000) this.scale = 4;
else if (area > 1_200_000) this.scale = 2;
else if (area > 500_000) this.scale = 2;
else this.scale = 1;
this.lowW = Math.ceil(this.fullW / this.scale);
this.lowH = Math.ceil(this.fullH / this.scale);
const lowSize = this.lowW * this.lowH;
this.fogMap = new Uint8ClampedArray(lowSize).fill(255);
this.visionBuffer = new Uint8ClampedArray(lowSize).fill(255);
this.territoryMap = new Uint8Array(lowSize);
this.setupChunks();
}
private setupChunks() {
const chunksX = Math.ceil(this.lowW / this.CHUNK_SIZE);
const chunksY = Math.ceil(this.lowH / this.CHUNK_SIZE);
for (let cy = 0; cy < chunksY; cy++) {
for (let cx = 0; cx < chunksX; cx++) {
this.chunks.push({
x: cx, y: cy, isDirty: true,
startX: cx * this.CHUNK_SIZE,
startY: cy * this.CHUNK_SIZE,
endX: Math.min((cx + 1) * this.CHUNK_SIZE, this.lowW),
endY: Math.min((cy + 1) * this.CHUNK_SIZE, this.lowH),
});
}
}
}
shouldTransform(): boolean { return true; }
init() {
this.fogCanvas = document.createElement("canvas");
this.fogCanvas.width = this.lowW;
this.fogCanvas.height = this.lowH;
const ctx = this.fogCanvas.getContext("2d", { alpha: true });
if (!ctx) throw new Error("2D context not supported");
this.fogCtx = ctx;
this.fogImageData = this.fogCtx.createImageData(this.lowW, this.lowH);
this.markAllDirty();
// Initialize the vision buffer
this.visionBuffer.fill(255);
}
redraw() { this.markAllDirty(); this.renderFog(); }
async tick() {
if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) return;
const now = performance.now();
if (now - this.lastVisionUpdate >= this.VISION_INTERVAL) {
await this.updateVision();
this.lastVisionUpdate = now;
}
this.handleAllianceChanges();
this.applyPermanentTerritory();
this.updateMobileUnitFogVisibility();
if (this.hasDirtyChunks()) this.renderFog();
}
renderLayer(ctx: CanvasRenderingContext2D) {
if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) return;
if (this.hasDirtyChunks()) this.renderFog();
ctx.drawImage(this.fogCanvas, -this.fullW / 2, -this.fullH / 2, this.fullW, this.fullH);
}
// UNITS AND BORDERS GIVE VISION
private async updateVision() {
// Clear the vision buffer completely on each update
// This ensures that only areas with active vision sources remain visible
this.visionBuffer.fill(255);
const player = this._observedPlayer || this.game.myPlayer();
if (!player) return;
const allies = [player];
this.game.playerViews().forEach(p => {
if (p.id() !== player.id() && player.isAlliedWith(p)) allies.push(p);
});
// Create a new cache to compare with the previous one
const newVisionCache: Map<string, {centerTile: number, radius: number, type: string}> = new Map();
const newBorderVisionCache: Map<string, Set<number>> = new Map();
// First apply vision from fixed units and borders
const fixedEntities: Array<{player: any, unit: any, radius: number}> = [];
const mobileEntities: Array<{player: any, unit: any, radius: number}> = [];
// Separate fixed units from mobile ones
for (const p of allies) {
for (const unit of p.units()) {
const radius = this.getUnitVisionRange(unit);
const unitType = unit.type?.() || "";
const isFixed = ["City", "Port", "Defense Post", "Missile Silo", "SAM Launcher", "Factory"].includes(unitType);
if (isFixed) {
fixedEntities.push({player: p, unit, radius});
} else {
mobileEntities.push({player: p, unit, radius});
}
}
}
// Apply vision from fixed units first
for (const {player, unit, radius} of fixedEntities) {
const unitKey = `${player.id()}-${unit.id()}`;
newVisionCache.set(unitKey, {centerTile: unit.tile(), radius, type: 'fixed'});
this.applyVisionCircle(unit.tile(), radius);
}
// Apply vision from borders
await this.applyBorderVision(allies, newBorderVisionCache);
// Now apply vision from mobile units only where necessary
for (const {player, unit, radius} of mobileEntities) {
const unitKey = `${player.id()}-${unit.id()}`;
newVisionCache.set(unitKey, {centerTile: unit.tile(), radius, type: 'mobile'});
// Check if the mobile unit can reveal at least 25% of its vision range
const tileRef = unit.tile();
const canActivateVision = this.canUnitActivateVision(tileRef, radius);
// Activate unit vision if it can reveal 25% or more of its field
if (canActivateVision) {
this.applyVisionCircle(unit.tile(), radius);
}
}
// Update caches
this.visionCache = newVisionCache;
this.borderVisionCache = newBorderVisionCache;
this.applyVisionToFogMap();
}
// New method to apply vision from border tiles
private async applyBorderVision(alliedPlayers: any[], newBorderVisionCache: Map<string, Set<number>>) {
// Process all borders at once to avoid inconsistencies
const borderData: Array<{playerId: string, borderTiles: number[]}> = [];
// Collect all borders first
for (const player of alliedPlayers) {
try {
const borderTiles = await player.borderTiles();
borderData.push({
playerId: player.id(),
borderTiles: borderTiles.borderTiles
});
} catch (e) {
// In case of error, continue with other players
console.warn("Failed to get border tiles for player", player.id(), e);
}
}
// Now apply vision from borders
for (const {playerId, borderTiles} of borderData) {
newBorderVisionCache.set(playerId, new Set(borderTiles));
// Apply vision from borders only where necessary
for (const tile of borderTiles) {
// Check if the border is in an area already visible
const lx = Math.floor(this.game.x(tile) / this.scale);
const ly = Math.floor(this.game.y(tile) / this.scale);
const idx = ly * this.lowW + lx;
// If the area is not visible (buffer is still 255), apply vision from the border
if (this.visionBuffer[idx] === 255) {
this.applyVisionCircle(tile, 20); // Fixed radius of 20 tiles for border vision
}
// If the area is already visible, we don't need to apply additional vision
}
}
}
private applyVisionCircle(centerTile: number, radius: number) {
const cx = this.game.x(centerTile);
const cy = this.game.y(centerTile);
const r2 = radius * radius;
for (let dy = -radius; dy <= radius; dy++) {
const wy = cy + dy;
if (wy < 0 || wy >= this.fullH) continue;
const maxDx = Math.floor(Math.sqrt(r2 - dy * dy));
for (let dx = -maxDx; dx <= maxDx; dx++) {
const wx = cx + dx;
if (wx < 0 || wx >= this.fullW) continue;
const lx = Math.floor(wx / this.scale);
const ly = Math.floor(wy / this.scale);
if (lx >= 0 && lx < this.lowW && ly >= 0 && ly < this.lowH) {
this.visionBuffer[ly * this.lowW + lx] = 0;
}
}
}
}
private applyVisionToFogMap() {
let changed = false;
for (let i = 0; i < this.fogMap.length; i++) {
// Check if this tile is owned by a player
const lowX = i % this.lowW;
const lowY = Math.floor(i / this.lowW);
const fullX = lowX * this.scale;
const fullY = lowY * this.scale;
// Get the owner of this tile
const ownerID = this.getTileOwnerID(fullX, fullY);
const myPlayer = this._observedPlayer || this.game.myPlayer();
// Skip processing for player's own territory tiles
if (ownerID !== 0 && myPlayer && ownerID === myPlayer.smallID()) {
// Player's own territory tiles never go to fog 0.8
if (this.fogMap[i] !== 0) {
this.fogMap[i] = 0;
this.markTileDirty(i);
changed = true;
}
continue;
}
// Handle ally territory tiles - they should always be fog 0 as long as they are still allies
if (ownerID !== 0 && myPlayer && this.isAlly(ownerID, myPlayer)) {
// Ally's territory tiles should always be fog 0
if (this.fogMap[i] !== 0) {
this.fogMap[i] = 0;
this.markTileDirty(i);
changed = true;
}
continue;
}
if (this.territoryMap[i]) continue;
const newFog = this.visionBuffer[i] === 0 ? 0 : (this.fogMap[i] <= 204 ? 204 : 255);
if (this.fogMap[i] !== newFog) {
this.fogMap[i] = newFog;
this.markTileDirty(i);
changed = true;
}
}
if (changed) this.renderFog();
}
private applyPermanentTerritory() {
let changed = false;
for (let i = 0; i < this.territoryMap.length; i++) {
if (this.territoryMap[i] && this.fogMap[i] !== 0) {
this.fogMap[i] = 0;
this.markTileDirty(i);
changed = true;
}
}
if (changed) this.renderFog();
}
public claimTerritory(x: number, y: number) {
if (x < 0 || y < 0 || x >= this.fullW || y >= this.fullH) return;
const fullIdx = y * this.fullW + x;
const lowIdx = this.fullToLow(fullIdx);
this.territoryMap[lowIdx] = 1;
this.fogMap[lowIdx] = 0;
this.markTileDirty(lowIdx);
}
private renderFog() {
const data = this.fogImageData.data;
for (const chunk of this.chunks) {
if (!chunk.isDirty) continue;
for (let y = chunk.startY; y < chunk.endY; y++) {
for (let x = chunk.startX; x < chunk.endX; x++) {
const i = y * this.lowW + x;
const p = i * 4;
// Check if this tile is owned by a player
const fullX = x * this.scale;
const fullY = y * this.scale;
// Get the owner of this tile
const ownerID = this.getTileOwnerID(fullX, fullY);
const myPlayer = this._observedPlayer || this.game.myPlayer();
if (this.territoryMap[i]) {
data[p] = 0; data[p+1] = 128; data[p+2] = 0; data[p+3] = 70;
} else if (ownerID !== 0) {
// This tile is owned by some player
// Territory tiles always have fog 0, even if not in vision range
if (myPlayer && (ownerID === myPlayer.smallID() || this.isAlly(ownerID, myPlayer))) {
// Player's own territory or ally's territory - no fog (fog 0)
data[p] = data[p+1] = data[p+2] = data[p+3] = 0;
} else {
// Enemy territory - also fog 0 (no fog) as per requirement
data[p] = data[p+1] = data[p+2] = data[p+3] = 0;
}
} else {
// No owner (neutral territory) - normal fog behavior
data[p] = data[p+1] = data[p+2] = data[p+3] = 0;
}
const fog = this.fogMap[i];
// Player's own territory tiles and ally territory tiles never show fog 0.8
if (ownerID !== 0 && myPlayer && (ownerID === myPlayer.smallID() || this.isAlly(ownerID, myPlayer))) {
// Keep fog at 0 for player's own territory and ally territory
data[p+3] = 0;
} else if (fog >= 204) {
const gray = 12 + (i & 15);
data[p] = gray; data[p+1] = gray; data[p+2] = gray;
data[p+3] = fog;
}
}
}
chunk.isDirty = false;
}
this.fogCtx.putImageData(this.fogImageData, 0, 0);
}
private fullToLow(fullIdx: number): number {
const x = fullIdx % this.fullW;
const y = Math.floor(fullIdx / this.fullW);
const lx = Math.floor(x / this.scale);
const ly = Math.floor(y / this.scale);
return Math.min(ly, this.lowH - 1) * this.lowW + Math.min(lx, this.lowW - 1);
}
private markTileDirty(lowIdx: number) {
const x = lowIdx % this.lowW;
const y = Math.floor(lowIdx / this.lowW);
const cx = Math.floor(x / this.CHUNK_SIZE);
const cy = Math.floor(y / this.CHUNK_SIZE);
const idx = cy * Math.ceil(this.lowW / this.CHUNK_SIZE) + cx;
if (idx < this.chunks.length) this.chunks[idx].isDirty = true;
}
private markAllDirty() { this.chunks.forEach(c => c.isDirty = true); }
private hasDirtyChunks(): boolean { return this.chunks.some(c => c.isDirty); }
public getFogValueAt(fullIdx: number): number {
const low = this.fullToLow(fullIdx);
return this.fogMap[low] / 255;
}
// Helper method to get tile owner ID
private getTileOwnerID(x: number, y: number): number {
// Convert screen coordinates to game coordinates
const gameX = Math.floor(x);
const gameY = Math.floor(y);
// Check if coordinates are valid
if (gameX < 0 || gameX >= this.game.width() || gameY < 0 || gameY >= this.game.height()) {
return 0;
}
// Get the tile reference
const tileRef = this.game.ref(gameX, gameY);
// Return the owner ID
return this.game.ownerID(tileRef);
}
// Helper method to check if a player ID belongs to an ally
private isAlly(ownerID: number, myPlayer: any): boolean {
if (ownerID === myPlayer.smallID()) {
return true; // Own player is considered an ally
}
// Check if the owner is an ally
const ownerPlayer = this.game.playerBySmallID(ownerID);
if (ownerPlayer && ownerPlayer.isPlayer()) {
return myPlayer.isAlliedWith(ownerPlayer);
}
return false;
}
// Method to check if a unit can reveal at least 25% of its vision field
private canUnitActivateVision(centerTile: number, radius: number): boolean {
const cx = this.game.x(centerTile);
const cy = this.game.y(centerTile);
const r2 = radius * radius;
let totalTiles = 0;
let visibleTiles = 0;
// Count how many tiles of the vision field are already visible vs total
for (let dy = -radius; dy <= radius; dy++) {
const wy = cy + dy;
if (wy < 0 || wy >= this.fullH) continue;
const maxDx = Math.floor(Math.sqrt(r2 - dy * dy));
for (let dx = -maxDx; dx <= maxDx; dx++) {
const wx = cx + dx;
if (wx < 0 || wx >= this.fullW) continue;
const lx = Math.floor(wx / this.scale);
const ly = Math.floor(wy / this.scale);
if (lx >= 0 && lx < this.lowW && ly >= 0 && ly < this.lowH) {
totalTiles++;
const idx = ly * this.lowW + lx;
// Count as visible if the buffer is 0 (fog 0)
if (this.visionBuffer[idx] === 0) {
visibleTiles++;
}
}
}
}
const visibilityRatio = visibleTiles / totalTiles;
return visibilityRatio < 0.75;
}
private getUnitVisionRange(unit: any): number {
const type = unit.type?.() || "";
const level = unit.level?.() ?? 1;
const boost = 1 + (level - 1) * 0.2;
const base: Record<string, number> = {
"City": 30, "Port": 80, "Defense Post": 70, "Warship": 140,
"Missile Silo": 200, "SAM Launcher": 400, "Factory": 35
};
const range = base[type] ?? 15;
const upgradable = ["City", "Port", "Missile Silo", "SAM Launcher", "Factory"];
return upgradable.includes(type) ? Math.round(range * boost) : range;
}
private handleAllianceChanges() {
const player = this._observedPlayer || this.game.myPlayer();
if (!player) return;
// Create current allies set
const currentAllies = new Set<string>();
currentAllies.add(player.id());
this.game.playerViews().forEach(p => {
if (p.id() !== player.id() && player.isAlliedWith(p)) {
currentAllies.add(p.id());
}
});
// Check for alliance changes
const oldAlliesArray = Array.from(this.previousAllies);
const newAlliesArray = Array.from(currentAllies);
// If alliance composition changed, we could add logic here if needed
if (oldAlliesArray.length !== newAlliesArray.length ||
oldAlliesArray.some(id => !currentAllies.has(id)) ||
newAlliesArray.some(id => !this.previousAllies.has(id))) {
// Update the previous allies set
this.previousAllies = currentAllies;
}
}
public setObservedPlayer(player: any) {
this._observedPlayer = player;
this.fogMap.fill(255);
this.visionBuffer.fill(255);
this.territoryMap.fill(0);
this.markAllDirty();
// Clear caches when the observed player changes
this.visionCache.clear();
this.borderVisionCache.clear();
// Reset alliance tracking
this.previousAllies.clear();
if (player) {
this.previousAllies.add(player.id());
this.game.playerViews().forEach(p => {
if (p.id() !== player.id() && player.isAlliedWith(p)) {
this.previousAllies.add(p.id());
}
});
}
}
private updateMobileUnitFogVisibility() {
// This method is now simplified since we're not tracking units with distance anymore
// The visibility is now determined in real-time based on fog level
}
// Method to check if a mobile unit should be rendered as opacued or invisible
public getMobileUnitFogEffect(unitId: number): { isOpacued: boolean, isInvisible: boolean } {
// Find the unit in the game
const unit = this.game.unit(unitId);
if (!unit) {
// Unit doesn't exist, should be visible (or not rendered at all)
return { isOpacued: false, isInvisible: false };
}
const player = this._observedPlayer || this.game.myPlayer();
if (!player) {
return { isOpacued: false, isInvisible: false };
}
// Check if unit owner is an ally
const unitOwner = unit.owner?.();
if (unitOwner && player.isAlliedWith(unitOwner)) {
// Units from allies should always be visible
return { isOpacued: false, isInvisible: false };
}
// Get the fog value at the unit's position
const unitTile = unit.tile();
const unitX = this.game.x(unitTile);
const unitY = this.game.y(unitTile);
const lowX = Math.floor(unitX / this.scale);
const lowY = Math.floor(unitY / this.scale);
if (lowX >= 0 && lowX < this.lowW && lowY >= 0 && lowY < this.lowH) {
const idx = lowY * this.lowW + lowX;
const fogValue = this.fogMap[idx];
// If fog is 0.8 or higher (204/255), the mobile unit should be opacued
if (fogValue >= this.MOBILE_FOG_THRESHOLD) {
return { isOpacued: true, isInvisible: false };
}
}
return { isOpacued: false, isInvisible: false };
}
// Method to check if a fixed unit should be rendered based on fog level
public getFixedUnitFogVisibility(unit: any): { isVisible: boolean } {
const player = this._observedPlayer || this.game.myPlayer();
if (!player) {
return { isVisible: true };
}
// Check if unit owner is an ally
const unitOwner = unit.owner?.();
if (unitOwner && player.isAlliedWith(unitOwner)) {
// Units from allies should always be visible
return { isVisible: true };
}
// Get the fog value at the unit's position
const unitTile = unit.tile();
const unitX = this.game.x(unitTile);
const unitY = this.game.y(unitTile);
const lowX = Math.floor(unitX / this.scale);
const lowY = Math.floor(unitY / this.scale);
if (lowX >= 0 && lowX < this.lowW && lowY >= 0 && lowY < this.lowH) {
const idx = lowY * this.lowW + lowX;
const fogValue = this.fogMap[idx];
// If fog is 0.8 or higher (204/255), the fixed unit should not be visible
if (fogValue >= this.MOBILE_FOG_THRESHOLD) {
return { isVisible: false };
}
}
return { isVisible: true };
}
public clearObservedPlayer() { this._observedPlayer = null; }
}
+304 -30
View File
@@ -3,9 +3,11 @@ import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { renderTroops, translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
import { FogOfWarLayer } from "./FogOfWarLayer";
interface Entry {
name: string;
@@ -33,12 +35,23 @@ export class GoToUnitEvent implements GameEvent {
constructor(public unit: UnitView) {}
}
// Event to view another player's vision (for eliminated players)
export class ViewPlayerVisionEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
public eventBus: EventBus | null = null;
/**
* Referência à camada de Fog of War para controlar visibilidade no leaderboard
*/
public fogOfWarLayer: FogOfWarLayer | null = null; // Reference to FogOfWarLayer
players: Entry[] = [];
// Track players that have been seen in fog 0 in local mode
private seenPlayersInLocalMode = new Set<string>();
@property({ type: Boolean }) visible = false;
private showTopFive = true;
@@ -48,12 +61,22 @@ export class Leaderboard extends LitElement implements Layer {
@state()
private _sortOrder: "asc" | "desc" = "desc";
// Leaderboard mode: 'local' for visible only, 'global' for all players
@state()
private _leaderboardMode: "local" | "global" = "local";
// Track which player's vision is currently being viewed
private viewingPlayerVision: PlayerView | null = null;
createRenderRoot() {
return this; // use light DOM for Tailwind support
}
init() {}
init() {
// Clear the seen players when initializing a new game
this.seenPlayersInLocalMode.clear();
}
tick() {
if (this.game === null) throw new Error("Not initialized");
@@ -72,6 +95,83 @@ export class Leaderboard extends LitElement implements Layer {
}
this.updateLeaderboard();
}
// Check if the player can access the global leaderboard mode
private canAccessGlobalMode(): boolean {
// In Fog of War mode, only eliminated players can access global mode
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
return myPlayer !== null && !myPlayer.isAlive();
}
// In other modes, everyone can access global mode
return true;
}
// Method to set the player whose vision we want to view
public setViewingPlayerVision(player: PlayerView | null) {
// If we have a FogOfWarLayer reference, update it
if (this.fogOfWarLayer && typeof this.fogOfWarLayer.setObservedPlayer === 'function') {
this.fogOfWarLayer.setObservedPlayer(player);
}
// Update our local tracking
this.viewingPlayerVision = player;
}
// Method to get the currently viewed player
public getViewingPlayerVision(): PlayerView | null {
return this.viewingPlayerVision;
}
// Toggle between leaderboard modes
private toggleLeaderboardMode() {
// Check if the player can access global mode
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
if (!this.canAccessGlobalMode() && this._leaderboardMode === "local") {
// If the player is alive and trying to switch to global mode, don't allow
return;
}
}
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
this._leaderboardMode = this._leaderboardMode === "local" ? "global" : "local";
this.updateLeaderboard();
}
}
// Check if a player is visible in Fog of War mode
private isPlayerVisible(player: PlayerView): boolean {
// If we're not in Fog of War mode, all players are visible
if (!this.game || this.game.config().gameConfig().gameMode !== GameMode.FogOfWar || !this.fogOfWarLayer) {
return true;
}
// If the player is eliminated, they are not visible in the normal leaderboard
if (!player.isAlive()) {
return false;
}
// Get the player's position
const nameLocation = player.nameLocation();
if (!nameLocation) {
return false;
}
const x = nameLocation.x;
const y = nameLocation.y;
// Check if coordinates are valid
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// Consider visible only if fog is 0 (completely visible area)
return fogValue === 0;
}
return false;
}
private updateLeaderboard() {
if (this.game === null) throw new Error("Not initialized");
@@ -102,21 +202,105 @@ export class Leaderboard extends LitElement implements Layer {
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
const alivePlayers = sorted.filter((player) => player.isAlive());
// In Fog of War mode, filter players based on visibility and player state
let filteredPlayers = sorted;
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
if (this._leaderboardMode === "local") {
// Local mode: show all players but hide information for non-visible players
// Always include my player and allies
filteredPlayers = sorted.filter((player) => {
// Always show my player
if (myPlayer && player === myPlayer) {
return true;
}
// Always show allies
if (myPlayer && myPlayer.isAlliedWith(player)) {
return true;
}
// For others, check if they are alive and have been seen before
if (player.isAlive()) {
// Check if player has been seen in fog 0 before
if (!this.seenPlayersInLocalMode.has(player.id())) {
// Only add to seen players if currently visible (fog 0)
if (this.isPlayerVisible(player)) {
this.seenPlayersInLocalMode.add(player.id());
return true;
}
return false; // Not visible and not seen before, don't add
} else {
// Player has been seen before, always include in local leaderboard
return true;
}
} else {
// Player is not alive, remove from seen players if they were there
this.seenPlayersInLocalMode.delete(player.id());
return false; // Don't show dead players in local leaderboard
}
});
} else {
// Global mode: check if the current player is eliminated
const isPlayerEliminated = myPlayer !== null && !myPlayer.isAlive();
// If the current player is eliminated, show all players
// If the current player is alive, show only visible alive players
if (isPlayerEliminated) {
// Eliminated players can see all players in global mode
filteredPlayers = sorted;
} else {
// Alive players can only see visible alive players in global mode
// But always include my player and allies
filteredPlayers = sorted.filter((player) => {
// Always show my player
if (myPlayer && player === myPlayer) {
return true;
}
// Always show allies
if (myPlayer && myPlayer.isAlliedWith(player)) {
return true;
}
// For others, check if they are alive and visible
return player.isAlive() && this.isPlayerVisible(player);
});
}
}
}
const alivePlayers = filteredPlayers.filter((player) => player.isAlive());
const playersToShow = this.showTopFive
? alivePlayers.slice(0, 5)
: alivePlayers;
this.players = playersToShow.map((player, index) => {
const maxTroops = this.game!.config().maxTroops(player);
// In Fog of War local mode, hide player information if not visible
let playerName = player.displayName();
let playerScore = formatPercentage(player.numTilesOwned() / numTilesWithoutFallout);
let playerGold = renderNumber(player.gold());
const currentPlayerMaxTroops = this.game!.config().maxTroops(player);
let playerMaxTroops = renderTroops(currentPlayerMaxTroops || 0);
if (this.game && this.game.config().gameConfig().gameMode === GameMode.FogOfWar &&
this._leaderboardMode === "local" &&
!this.isPlayerVisible(player) &&
!(myPlayer && player === myPlayer) &&
!(myPlayer && myPlayer.isAlliedWith(player))) {
// Hide information for non-visible players (except my player and allies)
playerName = "?????";
playerScore = "?????";
playerGold = "?????";
playerMaxTroops = "?????";
}
return {
name: player.displayName(),
name: playerName,
position: index + 1,
score: formatPercentage(
player.numTilesOwned() / numTilesWithoutFallout,
),
gold: renderNumber(player.gold()),
maxTroops: renderTroops(maxTroops),
score: playerScore,
gold: playerGold,
maxTroops: playerMaxTroops,
isMyPlayer: player === myPlayer,
isOnSameTeam:
myPlayer !== null &&
@@ -125,6 +309,7 @@ export class Leaderboard extends LitElement implements Layer {
};
});
// If it's my player and not in the list, add it
if (
myPlayer !== null &&
this.players.find((p) => p.isMyPlayer) === undefined
@@ -137,7 +322,15 @@ export class Leaderboard extends LitElement implements Layer {
}
}
if (myPlayer.isAlive()) {
// In Fog of War mode, only add my player if they are visible or if we are in global mode
// and the player is eliminated
const isPlayerEliminated = myPlayer !== null && !myPlayer.isAlive();
const shouldAddMyPlayer =
this.game.config().gameConfig().gameMode !== GameMode.FogOfWar ||
(this._leaderboardMode === "global" && isPlayerEliminated) ||
this.isPlayerVisible(myPlayer);
if (myPlayer.isAlive() && shouldAddMyPlayer) {
const myPlayerMaxTroops = this.game!.config().maxTroops(myPlayer);
this.players.pop();
this.players.push({
@@ -160,6 +353,22 @@ export class Leaderboard extends LitElement implements Layer {
private handleRowClickPlayer(player: PlayerView) {
if (this.eventBus === null) return;
// In Fog of War mode, eliminated players can view other players' vision
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
// In local mode, only allow focus if the player's information is visible (not hidden)
if (this._leaderboardMode === "local" &&
!this.isPlayerVisible(player) &&
!(myPlayer && player === myPlayer) &&
!(myPlayer && myPlayer.isAlliedWith(player))) {
// Don't allow focus when information is hidden
return;
}
}
// Comportamento normal para jogadores vivos - apenas foca no jogador
this.eventBus.emit(new GoToPlayerEvent(player));
}
@@ -173,6 +382,11 @@ export class Leaderboard extends LitElement implements Layer {
if (!this.visible) {
return html``;
}
const isFogOfWarMode = this.game?.config().gameConfig().gameMode === GameMode.FogOfWar;
const myPlayer = this.game?.myPlayer();
const isPlayerEliminated = myPlayer !== undefined && myPlayer !== null && !myPlayer.isAlive();
return html`
<div
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-xs lg:text-sm md:max-h-[50vh] ${this
@@ -181,21 +395,28 @@ export class Leaderboard extends LitElement implements Layer {
: "hidden"}"
@contextmenu=${(e: Event) => e.preventDefault()}
>
${isFogOfWarMode ? html`
<div class="bg-gray-800/70 w-full text-center py-1 text-xs">
${this._leaderboardMode === "local"
? translateText("leaderboard.local_mode")
: translateText("leaderboard.global_mode")}
${isPlayerEliminated ? html` (${translateText("leaderboard.eliminated")})` : ""}
</div>
` : ""}
<div
class="grid bg-gray-800/70 w-full text-xs md:text-xs lg:text-sm"
style="grid-template-columns: 30px 100px 70px 55px 105px;"
style="grid-template-columns: 30px 100px 70px 55px 105px${isFogOfWarMode && isPlayerEliminated ? ' 20px' : ''};"
>
<div class="contents font-bold bg-gray-700/50">
<div class="py-1 md:py-2 text-center border-b border-slate-500">
#
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 truncate"
>
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${translateText("leaderboard.player")}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
@click=${() => this.setSort("tiles")}
>
${translateText("leaderboard.owned")}
@@ -206,7 +427,7 @@ export class Leaderboard extends LitElement implements Layer {
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
@click=${() => this.setSort("gold")}
>
${translateText("leaderboard.gold")}
@@ -217,7 +438,7 @@ export class Leaderboard extends LitElement implements Layer {
: ""}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate"
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap"
@click=${() => this.setSort("maxtroops")}
>
${translateText("leaderboard.maxtroops")}
@@ -227,6 +448,11 @@ export class Leaderboard extends LitElement implements Layer {
: "⬇️"
: ""}
</div>
${isFogOfWarMode && isPlayerEliminated ? html`
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${translateText("leaderboard.view")}
</div>
` : ""}
</div>
${repeat(
@@ -234,16 +460,25 @@ export class Leaderboard extends LitElement implements Layer {
(p) => p.player.id(),
(player) => html`
<div
class="contents hover:bg-slate-600/60 ${player.isOnSameTeam
class="contents hover:bg-slate-600/60 ${player.isMyPlayer
? "font-bold"
: ""} cursor-pointer"
@click=${() => this.handleRowClickPlayer(player.player)}
: ""} ${isFogOfWarMode && isPlayerEliminated ? 'cursor-pointer' : 'cursor-pointer'}"
@click=${(e: Event) => {
// Only handle row click for non-eliminated players or when not in Fog of War mode
if (!isFogOfWarMode || !isPlayerEliminated) {
this.handleRowClickPlayer(player.player);
}
}}
>
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${player.position}
</div>
<div
class="py-1 md:py-2 text-center border-b border-slate-500 truncate"
class="py-1 md:py-2 text-center border-b border-slate-500 truncate cursor-pointer"
@click=${(e: Event) => {
e.stopPropagation();
this.handleRowClickPlayer(player.player);
}}
>
${player.name}
</div>
@@ -256,21 +491,60 @@ export class Leaderboard extends LitElement implements Layer {
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${player.maxTroops}
</div>
${isFogOfWarMode && isPlayerEliminated ? html`
<div class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer"
@click=${(e: Event) => {
e.stopPropagation();
if (this.eventBus) {
// In Fog of War mode, eliminated players can view other players' vision
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
if (myPlayer && !myPlayer.isAlive()) {
// Set the player whose vision we want to view
this.setViewingPlayerVision(player.player);
// Also emit event for other systems to handle
this.eventBus.emit(new ViewPlayerVisionEvent(player.player));
// Focus on the player's position on the map
const nameLocation = player.player.nameLocation();
if (nameLocation) {
this.eventBus.emit(new GoToPositionEvent(nameLocation.x, nameLocation.y));
}
}
}
}
}}>
👁️
</div>
` : ""}
</div>
`,
)}
</div>
</div>
<button
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto block"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
}}
>
${this.showTopFive ? "+" : "-"}
</button>
<div class="flex justify-center gap-2 mt-1">
<button
class="px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
}}
>
${this.showTopFive ? "+" : "-"}
</button>
${isFogOfWarMode && isPlayerEliminated ? html`
<button
class="px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white"
@click=${() => this.toggleLeaderboardMode()}
>
${this._leaderboardMode === "local"
? translateText("leaderboard.switch_to_global")
: translateText("leaderboard.switch_to_local")}
</button>
` : ""}
</div>
`;
}
}
+23 -1
View File
@@ -1,7 +1,7 @@
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { PlayerActions } from "../../../core/game/Game";
import { GameMode, PlayerActions } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
@@ -32,6 +32,10 @@ export class MainRadialMenu extends LitElement implements Layer {
private chatIntegration: ChatIntegration;
private clickedTile: TileRef | null = null;
/**
* Referência à camada de Fog of War para controlar visibilidade no menu radial
*/
private fogOfWarLayer: any = null; // Reference to FogOfWarLayer
constructor(
private eventBus: EventBus,
@@ -71,6 +75,16 @@ export class MainRadialMenu extends LitElement implements Layer {
this.chatIntegration = new ChatIntegration(this.game, this.eventBus);
}
// Method to set the reference to FogOfWarLayer
public setFogOfWarLayer(fogLayer: any) {
this.fogOfWarLayer = fogLayer;
}
// Method to set the reference to NameLayer (not used in this implementation but added for consistency)
public setNameLayer(nameLayer: any) {
// Not used in this implementation
}
init() {
this.radialMenu.init();
@@ -85,6 +99,13 @@ export class MainRadialMenu extends LitElement implements Layer {
if (this.game.myPlayer() === null) {
return;
}
// In Fog of War mode, allow the radial menu in all areas
// Filtering which buttons to show will be done in rootMenuElement
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
// The logic for which buttons to show at each fog level
// is handled in rootMenuElement, so we allow the menu in all areas
}
this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
this.game
.myPlayer()!
@@ -131,6 +152,7 @@ export class MainRadialMenu extends LitElement implements Layer {
uiState: this.uiState,
closeMenu: () => this.closeMenu(),
eventBus: this.eventBus,
fogOfWarLayer: this.fogOfWarLayer,
};
const isFriendlyTarget =
+126 -6
View File
@@ -2,7 +2,7 @@ import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
import { Cell } from "../../../core/game/Game";
import { Cell, GameMode } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent } from "../../InputHandler";
@@ -45,12 +45,18 @@ export class NameLayer implements Layer {
private userSettings: UserSettings = new UserSettings();
private isVisible: boolean = true;
private firstPlace: PlayerView | null = null;
private fogOfWarLayer: any = null; // Reference to FogOfWarLayer
/**
* @param fogOfWarLayer Referência opcional à camada de Fog of War para controlar visibilidade dos nomes dos jogadores
*/
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private eventBus: EventBus,
fogOfWarLayer: any = null, // Optional reference to FogOfWarLayer
) {
this.fogOfWarLayer = fogOfWarLayer;
this.shieldIconImage = new Image();
this.shieldIconImage.src = shieldIcon;
this.shieldIconImage = new Image();
@@ -113,18 +119,111 @@ export class NameLayer implements Layer {
if (!render.player.nameLocation() || !render.player.isAlive()) {
return;
}
// Check if we are in Fog of War mode and if the player is in a visible area
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar && this.fogOfWarLayer) {
// Get the player's position
const nameLocation = render.player.nameLocation();
if (nameLocation) {
// Get the current player (myPlayer)
const myPlayer = this.game.myPlayer();
// Always show names of allies regardless of fog
if (myPlayer && myPlayer.isAlliedWith(render.player)) {
// Check other visibility conditions
const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size));
const size = this.transformHandler.scale * baseSize;
const isOnScreen = render.location
? this.transformHandler.isOnScreen(render.location)
: false;
const maxZoomScale = 17;
if (
!this.isVisible ||
size < 7 ||
(this.transformHandler.scale > maxZoomScale && size > 100) ||
!isOnScreen
) {
render.element.style.display = "none";
} else {
render.element.style.display = "flex";
}
return;
}
// Check if myPlayer exists and has a name location
if (myPlayer && myPlayer.nameLocation()) {
const myNameLocation = myPlayer.nameLocation();
// Calculate distance between the two players
const dx = nameLocation.x - myNameLocation.x;
const dy = nameLocation.y - myNameLocation.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// If within 15 tiles radius, the name is always visible
if (distance <= 15) {
// Check other visibility conditions
const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size));
const size = this.transformHandler.scale * baseSize;
const isOnScreen = render.location
? this.transformHandler.isOnScreen(render.location)
: false;
const maxZoomScale = 17;
if (
!this.isVisible ||
size < 7 ||
(this.transformHandler.scale > maxZoomScale && size > 100) ||
!isOnScreen
) {
render.element.style.display = "none";
} else {
render.element.style.display = "flex";
}
return;
} else {
// For non-allies, check fog value
const tileRef = this.game.ref(nameLocation.x, nameLocation.y);
const fogValue = this.fogOfWarLayer.getFogValueAt(tileRef);
// If fog is 0.8 or higher (completely fogged), don't show the name
if (fogValue >= 0.8) {
render.element.style.display = "none";
return;
}
}
} else {
// For non-allies, fallback to original fog check
const tileRef = this.game.ref(nameLocation.x, nameLocation.y);
const fogValue = this.fogOfWarLayer.getFogValueAt(tileRef);
// If fog is 0.8 or higher (completely fogged), don't show the name
if (fogValue >= 0.8) {
render.element.style.display = "none";
return;
}
}
// Check if the player is an ally and has been eliminated
if (myPlayer && myPlayer.isAlliedWith(render.player) && !render.player.isAlive()) {
// Hide the name of eliminated allies
render.element.style.display = "none";
return;
}
}
}
// Regular visibility checks for non-Fog of War modes
const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size));
const size = this.transformHandler.scale * baseSize;
const isOnScreen = render.location
? this.transformHandler.isOnScreen(render.location)
: false;
const maxZoomScale = 17;
if (
!this.isVisible ||
size < 7 ||
(this.transformHandler.scale > maxZoomScale && size > 100) ||
!this.isVisible ||
size < 7 ||
(this.transformHandler.scale > maxZoomScale && size > 100) ||
!isOnScreen
) {
render.element.style.display = "none";
@@ -137,6 +236,27 @@ export class NameLayer implements Layer {
if (this.game.ticks() % 10 !== 0) {
return;
}
// Add all players normally in Fog of War mode
// Visibility will be controlled in updateElementVisibility
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
for (const player of this.game.playerViews()) {
if (player.isAlive() && !this.seenPlayers.has(player)) {
this.seenPlayers.add(player);
this.renders.push(
new RenderInfo(
player,
0,
null,
0,
"",
this.createPlayerElement(player),
),
);
}
}
return;
}
// Precompute the first-place player for performance
this.firstPlace = getFirstPlacePlayer(this.game);
@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators.js";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import {
GameMode,
PlayerProfile,
PlayerType,
Relation,
@@ -24,6 +25,7 @@ import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
import { FogOfWarLayer } from "./FogOfWarLayer";
import allianceIcon from "/images/AllianceIcon.svg?url";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
@@ -81,6 +83,18 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private lastMouseUpdate = 0;
private showDetails = true;
// Track which players have been seen (visibility memory)
private seenPlayers: Set<string> = new Set();
/**
* Referência à camada de Fog of War para verificação de visibilidade
*/
@property({ type: Object })
public fogOfWarLayer: FogOfWarLayer | null = null; // Reference to FogOfWarLayer for visibility checking
@property({ type: Object })
public nameLayer: any = null; // Reference to NameLayer for visibility checking
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
@@ -121,11 +135,19 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
const owner = this.game.owner(tile);
if (owner && owner.isPlayer()) {
this.player = owner as PlayerView;
this.player.profile().then((p) => {
this.playerProfile = p;
});
this.setVisible(true);
const player = owner as PlayerView;
// Check if player info should be visible based on fog of war
if (this.shouldShowPlayerInfo(player)) {
this.player = player;
this.player.profile().then((p) => {
this.playerProfile = p;
});
this.setVisible(true);
// Mark player as seen
this.seenPlayers.add(player.id());
}
} else if (!this.game.isLand(tile)) {
const units = this.game
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
@@ -139,6 +161,25 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
}
// Check if player info should be shown based on fog of war visibility
private shouldShowPlayerInfo(player: PlayerView): boolean {
// If not in fog of war mode, show info
if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
return true;
}
// In fog of war mode, only show info if player is visible (fog 0 at name location)
const nameLocation = player.nameLocation();
if (nameLocation && this.fogOfWarLayer) {
const idx = nameLocation.y * this.game.width() + nameLocation.x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// Only show if fog is 0 (fully visible)
return fogValue === 0;
}
// If no fog layer available, default to showing info
return true;
}
tick() {
this.requestUpdate();
}
+229 -20
View File
@@ -1,5 +1,5 @@
import { Config } from "../../../core/configuration/Config";
import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
import { AllPlayers, GameMode, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
@@ -40,6 +40,10 @@ export interface MenuElementParams {
eventBus: EventBus;
uiState?: UIState;
closeMenu: () => void;
/**
* Referência opcional à camada de Fog of War para controle de visibilidade no menu radial
*/
fogOfWarLayer?: any;
}
export interface MenuElement {
@@ -117,6 +121,32 @@ function isFriendlyTarget(params: MenuElementParams): boolean {
return isFriendly.call(selectedPlayer, params.myPlayer);
}
// Helper function to check if a player is visible in Fog of War mode
function isPlayerVisibleInFog(params: MenuElementParams): boolean {
// This function should only be called in Fog of War mode
// If somehow called outside Fog of War mode, return true (visible)
if (params.game.config().gameConfig().gameMode !== GameMode.FogOfWar || !params.fogOfWarLayer) {
return true;
}
// If no selected player, consider as not visible
if (!params.selected) {
return false;
}
// Check if the selected player's name location is visible
const nameLocation = params.selected.nameLocation();
if (!nameLocation) {
return false;
}
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Player is visible if fog value is less than 0.8 (consistent with NameLayer logic)
return fogValue < 0.8;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const infoChatElement: MenuElement = {
id: "info_chat",
@@ -251,10 +281,21 @@ const allyDonateTroopsElement: MenuElement = {
const infoPlayerElement: MenuElement = {
id: "info_player",
name: "player",
disabled: () => false,
disabled: (params: MenuElementParams) => {
// Check if player is visible in Fog of War mode
if (!isPlayerVisibleInFog(params)) {
return true; // Disable if player is not visible
}
// Original condition - always enabled
return false;
},
color: COLORS.info,
icon: infoIcon,
action: (params: MenuElementParams) => {
// Check if player is visible in Fog of War mode
if (!isPlayerVisibleInFog(params)) {
return; // Don't show info panel if player is not visible
}
params.playerPanel.show(params.playerActions, params.tile);
},
};
@@ -263,7 +304,23 @@ const infoPlayerElement: MenuElement = {
const infoEmojiElement: MenuElement = {
id: "info_emoji",
name: "emoji",
disabled: () => false,
disabled: (params: MenuElementParams) => {
// Check if we're in Fog of War mode
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
// If we have a selected player, check if their name location is visible
if (params.selected) {
const nameLocation = params.selected.nameLocation();
if (nameLocation) {
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Disable if fog value is 1 (completely hidden)
return fogValue >= 1.0;
}
}
}
// Original condition - always enabled
return false;
},
color: COLORS.infoEmoji,
icon: emojiIcon,
subMenu: (params: MenuElementParams) => {
@@ -271,10 +328,42 @@ const infoEmojiElement: MenuElement = {
{
id: "emoji_more",
name: "more",
disabled: () => false,
disabled: (params: MenuElementParams) => {
// Check if we're in Fog of War mode
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
// If we have a selected player, check if their name location is visible
if (params.selected) {
const nameLocation = params.selected.nameLocation();
if (nameLocation) {
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Disable if fog value is 1 (completely hidden)
return fogValue >= 1.0;
}
}
}
// Original condition - always enabled
return false;
},
color: COLORS.infoEmoji,
icon: emojiIcon,
action: (params: MenuElementParams) => {
// Check if we're in Fog of War mode and verify visibility
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
// Check if the selected player's name location is visible
if (params.selected) {
const nameLocation = params.selected.nameLocation();
if (nameLocation) {
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Only proceed if fog value is less than 1 (visible)
if (fogValue >= 1.0) {
params.closeMenu();
return; // Don't show emoji table if player's name is not visible
}
}
}
}
params.emojiTable.showTable((emoji) => {
const targetPlayer =
params.selected === params.game.myPlayer()
@@ -296,9 +385,41 @@ const infoEmojiElement: MenuElement = {
id: `emoji_${i}`,
name: flattenedEmojiTable[i],
text: flattenedEmojiTable[i],
disabled: () => false,
disabled: (params: MenuElementParams) => {
// Check if we're in Fog of War mode
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
// If we have a selected player, check if their name location is visible
if (params.selected) {
const nameLocation = params.selected.nameLocation();
if (nameLocation) {
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Disable if fog value is 1 (completely hidden)
return fogValue >= 1.0;
}
}
}
// Original condition - always enabled
return false;
},
fontSize: "25px",
action: (params: MenuElementParams) => {
// Check if we're in Fog of War mode and verify visibility
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
// Check if the selected player's name location is visible
if (params.selected) {
const nameLocation = params.selected.nameLocation();
if (nameLocation) {
const idx = nameLocation.y * params.game.width() + nameLocation.x;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// Only proceed if fog value is less than 1 (visible)
if (fogValue >= 1.0) {
params.closeMenu();
return; // Don't send emoji if player's name is not visible
}
}
}
}
const targetPlayer =
params.selected === params.game.myPlayer()
? AllPlayers
@@ -316,11 +437,21 @@ const infoEmojiElement: MenuElement = {
export const infoMenuElement: MenuElement = {
id: Slot.Info,
name: "info",
disabled: (params: MenuElementParams) =>
!params.selected || params.game.inSpawnPhase(),
disabled: (params: MenuElementParams) => {
// Check if player is visible in Fog of War mode
if (!isPlayerVisibleInFog(params)) {
return true; // Disable if player is not visible
}
// Original condition
return !params.selected || params.game.inSpawnPhase();
},
icon: infoIcon,
color: COLORS.info,
action: (params: MenuElementParams) => {
// Check if player is visible in Fog of War mode
if (!isPlayerVisibleInFog(params)) {
return; // Don't show info panel if player is not visible
}
params.playerPanel.show(params.playerActions, params.tile);
},
};
@@ -548,6 +679,20 @@ export const boatMenuElement: MenuElement = {
color: COLORS.boat,
action: async (params: MenuElementParams) => {
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
const tileX = params.game.x(params.tile);
const tileY = params.game.y(params.tile);
const idx = tileY * params.game.width() + tileX;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// If it's exactly fog = 1, don't send the boat attack
if (fogValue >= 1.0) {
params.closeMenu();
return;
}
}
const spawn = await params.playerActionHandler.findBestTransportShipSpawn(
params.myPlayer,
params.tile,
@@ -568,9 +713,32 @@ export const centerButtonElement: CenterButtonElement = {
disabled: (params: MenuElementParams): boolean => {
const tileOwner = params.game.owner(params.tile);
const isLand = params.game.isLand(params.tile);
if (!isLand) {
// If in spawn phase (loading screen) and random spawn is enabled, disable the center button
if (params.game.inSpawnPhase() && params.game.config().isRandomSpawn()) {
return true;
}
// In Fog of War mode, allow the center button on sea tiles for the boat button
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
// No spawn phase (loading screen), disable the center button
if (params.game.inSpawnPhase()) {
return true;
}
// Allow on sea tiles if the player has transport units available
if (!isLand) {
return !params.playerActions.buildableUnits.some(
(unit) => unit.type === UnitType.TransportShip && unit.canBuild
);
}
} else {
// In other modes, disable on sea tiles as before
if (!isLand) {
return true;
}
}
if (params.game.inSpawnPhase()) {
if (tileOwner.isPlayer()) {
return true;
@@ -585,6 +753,20 @@ export const centerButtonElement: CenterButtonElement = {
return !params.playerActions.canAttack;
},
action: (params: MenuElementParams) => {
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar && params.fogOfWarLayer) {
const tileX = params.game.x(params.tile);
const tileY = params.game.y(params.tile);
const idx = tileY * params.game.width() + tileX;
const fogValue = params.fogOfWarLayer.getFogValueAt(idx);
// If it's exactly fog = 1, don't execute the center button action
if (fogValue >= 1.0) {
params.closeMenu();
return;
}
}
if (params.game.inSpawnPhase()) {
params.playerActionHandler.handleSpawn(params.tile);
} else {
@@ -616,6 +798,28 @@ export const rootMenuElement: MenuElement = {
icon: infoIcon,
color: COLORS.info,
subMenu: (params: MenuElementParams) => {
// Check the fog value to determine which menus are available
let fogValue = 0;
let isFogLevel1 = false; // fog = 1
if (params.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
const tileX = params.game.x(params.tile);
const tileY = params.game.y(params.tile);
const idx = tileY * params.game.width() + tileX;
// We need to access the FogOfWarLayer to get the fog value
// For now, we'll check if we can access it through the params
if ((params as any).fogOfWarLayer) {
const fogOfWarLayer = (params as any).fogOfWarLayer;
fogValue = fogOfWarLayer.getFogValueAt(idx);
// Check if it's exactly fog = 1
if (fogValue >= 1.0) {
isFogLevel1 = true;
}
}
}
let ally = allyRequestElement;
if (params.selected?.isAlliedWith(params.myPlayer)) {
ally = allyBreakElement;
@@ -626,18 +830,23 @@ export const rootMenuElement: MenuElement = {
tileOwner.isPlayer() &&
(tileOwner as PlayerView).id() === params.myPlayer.id();
const menuItems: (MenuElement | null)[] = [
infoMenuElement,
...(isOwnTerritory
? [deleteUnitElement, ally, buildMenuElement]
: [
boatMenuElement,
ally,
isFriendlyTarget(params)
? donateGoldRadialElement
: attackMenuElement,
]),
];
const menuItems: (MenuElement | null)[] = [];
// Specific logic based on fog value:
if (isFogLevel1) {
// Fog = 1: Only Attack Menu available
menuItems.push(attackMenuElement);
} else {
// All other fog values: All default menus available
menuItems.push(infoMenuElement);
if (isOwnTerritory) {
menuItems.push(deleteUnitElement, ally, buildMenuElement);
} else {
menuItems.push(boatMenuElement, ally);
menuItems.push(isFriendlyTarget(params) ? donateGoldRadialElement : attackMenuElement);
}
}
return menuItems.filter((item): item is MenuElement => item !== null);
},
+39 -1
View File
@@ -1,7 +1,7 @@
import { Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { UnitType, GameMode } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
@@ -9,6 +9,7 @@ import { UnitSelectionEvent } from "../../InputHandler";
import { ProgressBar } from "../ProgressBar";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { FogOfWarLayer } from "./FogOfWarLayer";
const COLOR_PROGRESSION = [
"rgb(232, 25, 25)",
@@ -48,10 +49,14 @@ export class UILayer implements Layer {
// Visual settings for selection
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
/**
* @param fogOfWarLayer Referência opcional à camada de Fog of War para controlar visibilidade de elementos de interface
*/
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
private fogOfWarLayer?: FogOfWarLayer,
) {
this.theme = game.config().theme();
}
@@ -270,6 +275,18 @@ export class UILayer implements Layer {
if (maxHealth === undefined || this.context === null) {
return;
}
// Check fog of war for Warship units
if (unit.type() === UnitType.Warship && this.fogOfWarLayer && this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogValue = this.fogOfWarLayer.getFogValueAt(unit.tile());
if (fogValue >= 0.8) {
// Don't draw health bar if unit is in fog 0.8 or higher
this.allHealthBars.get(unit.id())?.clear();
this.allHealthBars.delete(unit.id());
return;
}
}
if (
this.allHealthBars.has(unit.id()) &&
(unit.health() >= maxHealth || unit.health() <= 0 || !unit.isActive())
@@ -356,6 +373,27 @@ export class UILayer implements Layer {
if (!this.context) {
return;
}
// Check fog of war for fixed structures
const fixedStructures = [
UnitType.City,
UnitType.Factory,
UnitType.Port,
UnitType.DefensePost,
UnitType.MissileSilo,
UnitType.SAMLauncher
];
if (fixedStructures.includes(unit.type()) && this.fogOfWarLayer && this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogValue = this.fogOfWarLayer.getFogValueAt(unit.tile());
if (fogValue >= 0.8) {
// Don't draw loading bar if fixed structure is in fog 0.8 or higher
this.allProgressBars.get(unit.id())?.progressBar.clear();
this.allProgressBars.delete(unit.id());
return;
}
}
if (!this.allProgressBars.has(unit.id())) {
const progressBar = new ProgressBar(
COLOR_PROGRESSION,
+79 -6
View File
@@ -1,7 +1,7 @@
import { colord, Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { GameMode, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, UnitView } from "../../../core/game/GameView";
import { BezenhamLine } from "../../../core/utilities/Line";
@@ -14,6 +14,7 @@ import {
} from "../../InputHandler";
import { MoveWarshipIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { FogOfWarLayer } from "./FogOfWarLayer";
import { Layer } from "./Layer";
import { GameUpdateType } from "../../../core/game/GameUpdates";
@@ -51,10 +52,14 @@ export class UnitLayer implements Layer {
// Configuration for unit selection
private readonly WARSHIP_SELECTION_RADIUS = 10; // Radius in game cells for warship selection hit zone
/**
* @param fogOfWarLayer Referência opcional à camada de Fog of War para controlar visibilidade de unidades
*/
constructor(
private game: GameView,
private eventBus: EventBus,
transformHandler: TransformHandler,
private fogOfWarLayer?: FogOfWarLayer,
) {
this.theme = game.config().theme();
this.transformHandler = transformHandler;
@@ -338,7 +343,18 @@ export class UnitLayer implements Layer {
private handleWarShipEvent(unit: UnitView) {
if (unit.targetUnitId()) {
this.drawSprite(unit, colord("rgb(200,0,0)"));
// Check fog of war for Warship attack color
let attackColor = colord("rgb(200,0,0)"); // Default red color
if (this.fogOfWarLayer && this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
const fogValue = this.fogOfWarLayer.getFogValueAt(unit.tile());
if (fogValue >= 0.8) {
// Dark blue opaque color when in fog 0.8 or higher
attackColor = colord("rgb(0,0,139)").alpha(0.7); // Dark blue with 70% opacity
}
}
this.drawSprite(unit, attackColor);
} else {
this.drawSprite(unit);
}
@@ -384,12 +400,31 @@ export class UnitLayer implements Layer {
private drawTrail(trail: number[], color: Colord, rel: Relationship) {
// Paint new trail
for (const t of trail) {
// Check fog of war for trail visibility
let alpha = 150;
if (this.fogOfWarLayer && this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
const x = this.game.x(t);
const y = this.game.y(t);
const fullIdx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(fullIdx);
// If fog is 0.8 or higher, don't draw the trail
if (fogValue >= 0.8) {
continue; // Skip drawing this trail segment
}
// If fog is between 0 and 0.8, potentially adjust alpha
else if (fogValue > 0 && fogValue < 0.8) {
// Could apply partial opacity based on fog level
alpha = Math.floor(150 * (1 - fogValue));
}
}
this.paintCell(
this.game.x(t),
this.game.y(t),
rel,
color,
150,
alpha,
this.unitTrailContext,
);
}
@@ -582,9 +617,47 @@ export class UnitLayer implements Layer {
if (unit.isActive()) {
const targetable = unit.targetable();
if (!targetable) {
// Apply fog of war effects
let fogEffectAlpha = 1.0;
let unitVisible = true;
if (this.fogOfWarLayer && this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
// Check if this is a fixed unit (City, Port, Defense Post, Missile Silo, SAM Launcher, Factory)
const unitType = unit.type?.() || "";
const isFixed = ["City", "Port", "Defense Post", "Missile Silo", "SAM Launcher", "Factory"].includes(unitType);
if (isFixed) {
// For fixed units, check fog visibility directly
const fixedUnitVisibility = this.fogOfWarLayer.getFixedUnitFogVisibility(unit);
if (!fixedUnitVisibility.isVisible) {
unitVisible = false;
}
} else {
// For mobile units, use the existing logic
const fogEffect: any = this.fogOfWarLayer.getMobileUnitFogEffect(unit.id());
if (fogEffect.isInvisible) {
// Unit is in fog 0.8 or higher, set opacity to 0.1
fogEffectAlpha = 0.1;
} else if (fogEffect.isOpacued) {
// Unit should be opacued immediately
fogEffectAlpha = 0.1;
}
}
}
// If unit is not visible based on fog, set its opacity to 0
if (!unitVisible) {
fogEffectAlpha = 0;
}
if (!targetable || fogEffectAlpha < 1.0) {
this.context.save();
this.context.globalAlpha = 0.5;
if (!targetable && fogEffectAlpha === 1.0) {
this.context.globalAlpha = 0.5;
} else {
this.context.globalAlpha = Math.min(0.5, fogEffectAlpha);
}
}
this.context.drawImage(
sprite,
@@ -593,7 +666,7 @@ export class UnitLayer implements Layer {
sprite.width,
sprite.width,
);
if (!targetable) {
if (!targetable || fogEffectAlpha < 1.0) {
this.context.restore();
}
}
+193
View File
@@ -0,0 +1,193 @@
import { Game, Player, UnitType } from "./Game";
import { GameMap, TileRef } from "./GameMap";
/**
* Classe responsável por gerenciar o sistema de Fog of War (nevoeiro de guerra)
* no jogo. Controla quais tiles são visíveis para cada jogador.
*/
/**
* Gerencia o sistema de Fog of War (nevoeiro de guerra) no jogo.
* Controla quais tiles são visíveis para cada jogador no modo Fog of War.
*/
export class FogOfWarManager {
private exploredTiles: Map<string, Set<TileRef>> = new Map(); // playerId -> explored tiles
constructor(private game: Game) {}
/**
* Inicializa o sistema de Fog of War para todos os jogadores
*/
public initialize(): void {
// Para cada jogador, cria um conjunto vazio de tiles explorados
for (const player of this.game.players()) {
this.exploredTiles.set(player.id(), new Set<TileRef>());
}
}
/**
* Marca tiles como explorados para um jogador específico
* @param player O jogador que explorou os tiles
* @param tiles Os tiles que foram explorados
*/
public markAsExplored(player: Player, tiles: Set<TileRef> | TileRef[]): void {
const playerId = player.id();
const exploredSet = this.exploredTiles.get(playerId);
if (!exploredSet) {
console.warn(`No explored tiles set found for player ${playerId}`);
return;
}
// Adiciona todos os tiles ao conjunto de explorados
for (const tile of tiles) {
exploredSet.add(tile);
// Also marks the tile as explored on the map (for persistence)
const gameMap = this.game.map() as GameMap & {
setExplored?: (ref: TileRef, value: boolean) => void
};
if (gameMap.setExplored) {
gameMap.setExplored(tile, true);
}
}
}
/**
* Verifica se um tile é visível para um jogador
* @param player O jogador que está tentando ver o tile
* @param tile O tile a ser verificado
* @returns true se o tile é visível, false caso contrário
*/
public isVisible(player: Player, tile: TileRef): boolean {
const playerId = player.id();
const exploredSet = this.exploredTiles.get(playerId);
if (!exploredSet) {
return false;
}
// Verifica se o tile foi explorado
if (exploredSet.has(tile)) {
return true;
}
// Tiles adjacent to explored tiles are also visible
const neighbors = this.game.map().neighbors(tile);
for (const neighbor of neighbors) {
if (exploredSet.has(neighbor)) {
return true;
}
}
return false;
}
/**
* Obtém todos os tiles visíveis para um jogador
* @param player O jogador
* @returns Conjunto de tiles visíveis
*/
public getVisibleTiles(player: Player): Set<TileRef> {
const visibleTiles = new Set<TileRef>();
const playerId = player.id();
const exploredSet = this.exploredTiles.get(playerId);
if (!exploredSet) {
return visibleTiles;
}
// Adiciona todos os tiles explorados
for (const tile of exploredSet) {
visibleTiles.add(tile);
}
// Adiciona tiles adjacentes aos explorados
const tempSet = new Set<TileRef>();
for (const tile of exploredSet) {
const neighbors = this.game.map().neighbors(tile);
for (const neighbor of neighbors) {
if (!exploredSet.has(neighbor)) {
tempSet.add(neighbor);
}
}
}
for (const tile of tempSet) {
visibleTiles.add(tile);
}
return visibleTiles;
}
/**
* Atualiza a visibilidade baseada nas unidades do jogador
* Deve ser chamado a cada turno
* @param player O jogador
*/
public updateVisibility(player: Player): void {
const visibleTiles = new Set<TileRef>();
// Adds tiles visible by the player's units
const units = player.units();
for (const unit of units) {
const unitTile = unit.tile();
visibleTiles.add(unitTile);
// Adds adjacent tiles (basic vision range)
const neighbors = this.game.map().neighbors(unitTile);
for (const neighbor of neighbors) {
visibleTiles.add(neighbor);
}
// Para unidades especiais como navios, pode ter range maior
if (unit.type() === "Warship" || unit.type() === "Transport") {
const extendedView = this.game.map().circleSearch(unitTile, 3);
for (const tile of extendedView) {
visibleTiles.add(tile);
}
}
}
// Adiciona tiles das cidades do jogador
const cities = player.units(UnitType.City);
for (const city of cities) {
const cityTile = city.tile();
visibleTiles.add(cityTile);
// Cities have greater vision
const cityView = this.game.map().circleSearch(cityTile, 2);
for (const tile of cityView) {
visibleTiles.add(tile);
}
}
// Marca os tiles como explorados
this.markAsExplored(player, visibleTiles);
}
/**
* Verifica se dois jogadores podem ver tiles uns dos outros
* @param player1 Primeiro jogador
* @param player2 Segundo jogador
* @returns true se algum tile de um jogador é visível para o outro
*/
public canSeeEachOther(player1: Player, player2: Player): boolean {
// Checks if any tile of player2 is visible to player1
const player2Tiles = player2.tiles();
for (const tile of player2Tiles) {
if (this.isVisible(player1, tile)) {
return true;
}
}
// Checks if any tile of player1 is visible to player2
const player1Tiles = player1.tiles();
for (const tile of player1Tiles) {
if (this.isVisible(player2, tile)) {
return true;
}
}
return false;
}
}
+44 -1
View File
@@ -4,6 +4,7 @@ import { AllPlayersStats, ClientID, Winner } from "../Schemas";
import { simpleHash } from "../Util";
import { AllianceImpl } from "./AllianceImpl";
import { AllianceRequestImpl } from "./AllianceRequestImpl";
import { FogOfWarManager } from "./FogOfWar";
import {
Alliance,
AllianceRequest,
@@ -81,6 +82,7 @@ export class GameImpl implements Game {
private playerTeams: Team[];
private botTeam: Team = ColoredTeams.Bot;
private _railNetwork: RailNetwork = createRailNetwork(this);
private fogOfWarManager?: FogOfWarManager;
// Used to assign unique IDs to each new alliance
private nextAllianceID: number = 0;
@@ -100,6 +102,12 @@ export class GameImpl implements Game {
this._height = _map.height();
this.unitGrid = new UnitGrid(this._map);
// Inicializa o sistema de Fog of War se estiver no modo correto
if (_config.gameConfig().gameMode === GameMode.FogOfWar) {
this.fogOfWarManager = new FogOfWarManager(this);
this.fogOfWarManager.initialize();
}
if (_config.gameConfig().gameMode === GameMode.Team) {
this.populateTeams();
}
@@ -149,7 +157,7 @@ export class GameImpl implements Game {
}
private addPlayers() {
if (this.config().gameConfig().gameMode === GameMode.FFA) {
if (this.config().gameConfig().gameMode === GameMode.FFA || this.config().gameConfig().gameMode === GameMode.FogOfWar) {
this._humans.forEach((p) => this.addPlayer(p));
this._nations.forEach((n) => this.addPlayer(n.playerInfo));
return;
@@ -384,6 +392,9 @@ export class GameImpl implements Game {
for (const player of this._players.values()) {
// Players change each to so always add them
this.addUpdate(player.toUpdate());
// Atualiza o Fog of War para este jogador
this.updateFogOfWar(player);
}
if (this.ticks() % 10 === 0) {
this.addUpdate({
@@ -985,6 +996,38 @@ export class GameImpl implements Game {
// Record stats
this.stats().goldWar(conqueror, conquered, gold);
}
// Fog of War methods
/**
* Verifies if a tile is visible to a player in Fog of War mode.
* In other game modes, all tiles are considered visible.
* @param player The player whose visibility is being checked
* @param tile The tile reference to check visibility for
* @returns true if the tile is visible to the player, false otherwise
*/
isTileVisible(player: Player, tile: TileRef): boolean {
if (!this.fogOfWarManager) {
// If not in Fog of War mode, everything is visible
return true;
}
return this.fogOfWarManager.isVisible(player, tile);
}
getVisibleTiles(player: Player): Set<TileRef> {
if (!this.fogOfWarManager) {
// If not in Fog of War mode, returns all tiles
const allTiles = new Set<TileRef>();
this.map().forEachTile(tile => allTiles.add(tile));
return allTiles;
}
return this.fogOfWarManager.getVisibleTiles(player);
}
updateFogOfWar(player: Player): void {
if (this.fogOfWarManager) {
this.fogOfWarManager.updateVisibility(player);
}
}
}
// Or a more dynamic approach that will catch new enum values: