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);
+ }
+ }}
>
{
+ 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));
+ }
+ }
+ }
+ }
+ }}>
+ 👁️
+
`;
}
}
diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts
index 3151c6a48..5f16845cf 100644
--- a/src/client/graphics/layers/MainRadialMenu.ts
+++ b/src/client/graphics/layers/MainRadialMenu.ts
@@ -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 =
diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts
index 1c0b94a22..16591a62e 100644
--- a/src/client/graphics/layers/NameLayer.ts
+++ b/src/client/graphics/layers/NameLayer.ts
@@ -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);
diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts
index 56cfb3988..4b37b0b0c 100644
--- a/src/client/graphics/layers/PlayerInfoOverlay.ts
+++ b/src/client/graphics/layers/PlayerInfoOverlay.ts
@@ -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 = 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();
}
diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts
index 6f72f7149..a7c7218fb 100644
--- a/src/client/graphics/layers/RadialMenuElements.ts
+++ b/src/client/graphics/layers/RadialMenuElements.ts
@@ -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);
},
diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts
index d8edb3f02..cbdb3ac55 100644
--- a/src/client/graphics/layers/UILayer.ts
+++ b/src/client/graphics/layers/UILayer.ts
@@ -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,
diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts
index ca1de78e7..b73212892 100644
--- a/src/client/graphics/layers/UnitLayer.ts
+++ b/src/client/graphics/layers/UnitLayer.ts
@@ -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();
}
}
diff --git a/src/core/game/FogOfWar.ts b/src/core/game/FogOfWar.ts
new file mode 100644
index 000000000..b98417cfb
--- /dev/null
+++ b/src/core/game/FogOfWar.ts
@@ -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> = 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());
+ }
+ }
+
+ /**
+ * 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[]): 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 {
+ const visibleTiles = new Set();
+ 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();
+ 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();
+
+ // 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;
+ }
+}
\ No newline at end of file
diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts
index ee0a7783a..4fd047266 100644
--- a/src/core/game/GameImpl.ts
+++ b/src/core/game/GameImpl.ts
@@ -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 {
+ if (!this.fogOfWarManager) {
+ // If not in Fog of War mode, returns all tiles
+ const allTiles = new Set();
+ 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: