diff --git a/resources/lang/en.json b/resources/lang/en.json
index 7f9a2734a..d77b60b7d 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -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"
}
}
+
diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts
index 27c2f01cb..5df7ac228 100644
--- a/src/client/ClientGameRunner.ts
+++ b/src/client/ClientGameRunner.ts
@@ -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) {
diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts
index 76917c21d..e2e9fcb2a 100644
--- a/src/client/SinglePlayerModal.ts
+++ b/src/client/SinglePlayerModal.ts
@@ -190,10 +190,20 @@ export class SinglePlayerModal extends LitElement {
${translateText("game_mode.teams")}
+
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: