e.preventDefault()}
>
+ ${isFogOfWarMode ? html`
+
+ ${this._leaderboardMode === "local"
+ ? translateText("leaderboard.local_mode")
+ : translateText("leaderboard.global_mode")}
+ ${isPlayerEliminated ? html` (${translateText("leaderboard.eliminated")})` : ""}
+
+ ` : ""}
+
@@ -218,6 +353,11 @@ export class Leaderboard extends LitElement implements Layer {
: "⬇️"
: ""}
+ ${isFogOfWarMode && isPlayerEliminated ? html`
+
+ ${translateText("leaderboard.view")}
+
+ ` : ""}
${repeat(
@@ -227,7 +367,7 @@ export class Leaderboard extends LitElement implements Layer {
this.handleRowClickPlayer(player.player)}
>
@@ -247,21 +387,39 @@ export class Leaderboard extends LitElement implements Layer {
${player.troops}
+ ${isFogOfWarMode && isPlayerEliminated ? html`
+
+ 👁️
+
+ ` : ""}
`,
)}
-
+
+
+
+ ${isFogOfWarMode && isPlayerEliminated ? html`
+
+ ` : ""}
+
`;
}
}
@@ -270,4 +428,4 @@ function formatPercentage(value: number): string {
const perc = value * 100;
if (Number.isNaN(perc)) return "0%";
return perc.toFixed(1) + "%";
-}
+}
\ No newline at end of file
diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts
index 002b488b0..2ff1c786e 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 { PlayerActions, GameMode } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
@@ -22,6 +22,14 @@ import {
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
import { ContextMenuEvent } from "../../InputHandler";
+import { FogOfWarLayer } from "./FogOfWarLayer";
+import { NameLayer } from "./NameLayer";
+
+// Extended interface to include fogOfWarLayer and nameLayer
+interface ExtendedMenuElementParams extends MenuElementParams {
+ fogOfWarLayer: FogOfWarLayer | null;
+ nameLayer: NameLayer | null;
+}
@customElement("main-radial-menu")
export class MainRadialMenu extends LitElement implements Layer {
@@ -31,6 +39,8 @@ export class MainRadialMenu extends LitElement implements Layer {
private chatIntegration: ChatIntegration;
private clickedTile: TileRef | null = null;
+ private fogOfWarLayer: FogOfWarLayer | null = null;
+ private nameLayer: NameLayer | null = null;
constructor(
private eventBus: EventBus,
@@ -71,6 +81,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: FogOfWarLayer) {
+ this.fogOfWarLayer = fogLayer;
+ }
+
+ // Method to set the reference to NameLayer
+ public setNameLayer(nameLayer: NameLayer) {
+ this.nameLayer = nameLayer;
+ }
+
init() {
this.radialMenu.init();
this.eventBus.on(ContextMenuEvent, (event) => {
@@ -84,6 +104,27 @@ export class MainRadialMenu extends LitElement implements Layer {
if (this.game.myPlayer() === null) {
return;
}
+
+ // Check if we are in Fog of War mode or other supported modes (FFA, Team)
+ const gameMode = this.game.config().gameConfig().gameMode;
+ if (gameMode === GameMode.FogOfWar && this.fogOfWarLayer) {
+ // In Fog of War mode, continue with existing logic
+ const tileRef = this.game.ref(worldCoords.x, worldCoords.y);
+ const x = this.game.x(tileRef);
+ const y = this.game.y(tileRef);
+ const idx = y * this.game.width() + x;
+ const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
+
+ // Show radial menu in all areas, but with different logic for fog = 1
+ // This check was removed because the radial menu should be displayed in all areas
+ } else if (gameMode === GameMode.FFA || gameMode === GameMode.Team) {
+ // In FFA and Team modes, allow radial menu
+ // No fog check in these modes
+ } else {
+ // In other modes, don't show radial menu
+ return;
+ }
+
this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
this.game
.myPlayer()!
@@ -116,7 +157,8 @@ export class MainRadialMenu extends LitElement implements Layer {
this.chatIntegration.setupChatModal(myPlayer, recipient);
}
- const params: MenuElementParams = {
+ // Extend parameters with fogOfWarLayer and nameLayer
+ const params: ExtendedMenuElementParams = {
myPlayer,
selected: recipient,
tile,
@@ -129,7 +171,9 @@ export class MainRadialMenu extends LitElement implements Layer {
chatIntegration: this.chatIntegration,
closeMenu: () => this.closeMenu(),
eventBus: this.eventBus,
- };
+ fogOfWarLayer: this.fogOfWarLayer,
+ nameLayer: this.nameLayer,
+ } as ExtendedMenuElementParams;
this.radialMenu.setParams(params);
if (screenX !== null && screenY !== null) {
@@ -180,4 +224,4 @@ export class MainRadialMenu extends LitElement implements Layer {
this.playerPanel.hide();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts
index 0548f6fd8..5b50efff7 100644
--- a/src/client/graphics/layers/NameLayer.ts
+++ b/src/client/graphics/layers/NameLayer.ts
@@ -14,7 +14,7 @@ import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { Theme } from "../../../core/configuration/Config";
-import { AllPlayers, Cell, nukeTypes } from "../../../core/game/Game";
+import { AllPlayers, Cell, GameMode, nukeTypes } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent } from "../../InputHandler";
@@ -60,12 +60,15 @@ export class NameLayer implements Layer {
private theme: Theme = this.game.config().theme();
private userSettings: UserSettings = new UserSettings();
private isVisible: boolean = true;
+ private fogOfWarLayer: any = null; // Reference to FogOfWarLayer
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private eventBus: EventBus,
+ fogOfWarLayer: any = null, // Optional reference to FogOfWarLayer
) {
+ this.fogOfWarLayer = fogOfWarLayer;
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
this.disconnectedIconImage = new Image();
@@ -134,6 +137,22 @@ export class NameLayer implements Layer {
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) {
+ const tileRef = this.game.ref(nameLocation.x, nameLocation.y);
+ const fogValue = this.fogOfWarLayer.getFogValueAt(tileRef);
+
+ // If fog is between 0.8 and 1.0 (completely fogged), don't show the name
+ if (fogValue > 0.8) {
+ render.element.style.display = "none";
+ return;
+ }
+ }
+ }
+
const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size));
const size = this.transformHandler.scale * baseSize;
const isOnScreen = render.location
@@ -634,4 +653,44 @@ export class NameLayer implements Layer {
}
return icon;
}
+
+ // Method to check if a specific player's name is visible
+ public isPlayerNameVisible(player: PlayerView): boolean {
+ // If we're not in Fog of War mode, the name is always visible
+ if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
+ return true;
+ }
+
+ // If we don't have access to the fog layer, assume it's not visible
+ if (!this.fogOfWarLayer) {
+ return false;
+ }
+
+ // Check if the player is alive
+ if (!player.isAlive()) {
+ return false;
+ }
+
+ // Get the player's name location
+ const nameLocation = player.nameLocation();
+ if (!nameLocation) {
+ return false;
+ }
+
+ // Check if coordinates are valid
+ if (nameLocation.x < 0 || nameLocation.y < 0 ||
+ nameLocation.x >= this.game.width() || nameLocation.y >= this.game.height()) {
+ return false;
+ }
+
+ // Convert x,y coordinates to index
+ const idx = nameLocation.y * this.game.width() + nameLocation.x;
+
+ // Get the fog value at the player's name position
+ const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
+
+ // If fog is between 0.8 and 1.0 (completely fogged), don't show the name
+ // If fog is less than 0.8, the name is visible
+ return fogValue < 0.8;
+ }
}
diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts
index 83505eb28..7888897bf 100644
--- a/src/client/graphics/layers/PlayerInfoOverlay.ts
+++ b/src/client/graphics/layers/PlayerInfoOverlay.ts
@@ -2,12 +2,12 @@ import { LitElement, TemplateResult, html } from "lit";
import { ref } from "lit-html/directives/ref.js";
import { customElement, property, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
+import portIcon from "../../../../resources/images/AnchorIcon.png";
import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg";
import cityIcon from "../../../../resources/images/CityIconWhite.svg";
import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg";
import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg";
-import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg";
-import portIcon from "../../../../resources/images/PortIcon.svg";
+import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png";
import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import { renderPlayerFlag } from "../../../core/CustomFlag";
import { EventBus } from "../../../core/EventBus";
@@ -17,6 +17,7 @@ import {
Relation,
Unit,
UnitType,
+ GameMode,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { AllianceView } from "../../../core/game/GameUpdates";
@@ -31,6 +32,10 @@ import {
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
+// Import FogOfWarLayer for visibility checking
+import { FogOfWarLayer } from "./FogOfWarLayer";
+// Import NameLayer for visibility checking
+import { NameLayer } from "./NameLayer";
function euclideanDistWorld(
coord: { x: number; y: number },
@@ -80,6 +85,25 @@ 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();
+
+ // Reference to FogOfWarLayer for visibility checking
+ private fogOfWarLayer: FogOfWarLayer | null = null;
+
+ // Reference to NameLayer for visibility checking
+ private nameLayer: NameLayer | null = null;
+
+ // Method to set FogOfWarLayer reference
+ public setFogOfWarLayer(fogLayer: FogOfWarLayer) {
+ this.fogOfWarLayer = fogLayer;
+ }
+
+ // Method to set NameLayer reference
+ public setNameLayer(nameLayer: NameLayer) {
+ this.nameLayer = nameLayer;
+ }
init() {
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
@@ -107,7 +131,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
this.player = null;
}
- public maybeShow(x: number, y: number) {
+ private maybeShow(x: number, y: number) {
this.hide();
const worldCoord = this.transform.screenToWorldCoordinates(x, y);
if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) {
@@ -120,11 +144,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)
@@ -138,6 +170,22 @@ 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 no fog layer or not in fog of war mode, show info
+ if (!this.fogOfWarLayer || this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
+ return true;
+ }
+
+ // If we don't have access to the nameLayer, assume it's not visible
+ if (!this.nameLayer) {
+ return false;
+ }
+
+ // Check if the player's name is visible through the NameLayer
+ return this.nameLayer.isPlayerNameVisible(player);
+ }
+
tick() {
this.requestUpdate();
}
@@ -268,13 +316,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
let playerType = "";
switch (player.type()) {
case PlayerType.Bot:
- playerType = translateText("player_type.bot");
+ playerType = translateText("player_info_overlay.bot");
break;
case PlayerType.FakeHuman:
- playerType = translateText("player_type.nation");
+ playerType = translateText("player_info_overlay.nation");
break;
case PlayerType.Human:
- playerType = translateText("player_type.player");
+ playerType = translateText("player_info_overlay.player");
break;
}
@@ -364,18 +412,18 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
cityIcon,
"player_info_overlay.cities",
)}
- ${this.displayUnitCount(
- player,
- UnitType.Factory,
- factoryIcon,
- "player_info_overlay.factories",
- )}
${this.displayUnitCount(
player,
UnitType.Port,
portIcon,
"player_info_overlay.ports",
)}
+ ${this.displayUnitCount(
+ player,
+ UnitType.Factory,
+ factoryIcon,
+ "player_info_overlay.factories",
+ )}
${this.displayUnitCount(
player,
UnitType.MissileSilo,
@@ -454,4 +502,4 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
-}
+}
\ No newline at end of file
diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts
index 9e1fe21ef..e3be9283a 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";
@@ -10,6 +10,7 @@ import { EmojiTable } from "./EmojiTable";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel";
import { TooltipItem } from "./RadialMenu";
+import { NameLayer } from "./NameLayer";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
@@ -25,6 +26,11 @@ import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import xIcon from "../../../../resources/images/XIcon.svg";
import { EventBus } from "../../../core/EventBus";
+// Extended interface to include nameLayer
+interface ExtendedMenuElementParams extends MenuElementParams {
+ nameLayer: NameLayer | null;
+}
+
export interface MenuElementParams {
myPlayer: PlayerView;
selected: PlayerView | null;
@@ -306,7 +312,7 @@ export const infoMenuElement: MenuElement = {
id: Slot.Info,
name: "info",
disabled: (params: MenuElementParams) =>
- !params.selected || params.game.inSpawnPhase(),
+ !params.selected || params.game.inSpawnPhase() || !isPlayerNameVisible(params as ExtendedMenuElementParams),
icon: infoIcon,
color: COLORS.info,
action: (params: MenuElementParams) => {
@@ -314,6 +320,28 @@ export const infoMenuElement: MenuElement = {
},
};
+// Function to check if the player's NameLayer is visible
+function isPlayerNameVisible(params: ExtendedMenuElementParams): boolean {
+ // If we're not in Fog of War mode, the name is always visible
+ if (params.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
+ // In FFA and Team modes, check if the selected player exists and is alive
+ return params.selected !== null && params.selected.isAlive();
+ }
+
+ // If we don't have access to the selected player, assume it's not visible
+ if (!params.selected) {
+ return false;
+ }
+
+ // If we don't have access to nameLayer, assume it's not visible
+ if (!params.nameLayer) {
+ return false;
+ }
+
+ // Check if the player's name is visible through NameLayer
+ return params.nameLayer.isPlayerNameVisible(params.selected);
+}
+
function getAllEnabledUnits(myPlayer: boolean, config: Config): Set {
const Units: Set = new Set();
@@ -567,6 +595,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
+
+ // Check if we have access to fogOfWarLayer through extended parameters
+ if ((params as any).fogOfWarLayer && (params as any).game) {
+ const game = (params as any).game;
+ const fogOfWarLayer = (params as any).fogOfWarLayer;
+
+ if (game.config().gameConfig().gameMode === GameMode.FogOfWar) {
+ const tileX = game.x(params.tile);
+ const tileY = game.y(params.tile);
+ const idx = tileY * game.width() + tileX;
+ 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;
@@ -577,17 +627,24 @@ export const rootMenuElement: MenuElement = {
tileOwner.isPlayer() &&
(tileOwner as PlayerView).id() === params.myPlayer.id();
- const menuItems: (MenuElement | null)[] = [
- infoMenuElement,
- boatMenuElement,
- ally,
- ];
+ const menuItems: (MenuElement | null)[] = [];
- if (isOwnTerritory) {
- menuItems.push(buildMenuElement);
- menuItems.push(deleteUnitElement);
- } else {
+ // 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);
+ menuItems.push(boatMenuElement);
+ menuItems.push(ally);
+
+ if (isOwnTerritory) {
+ menuItems.push(buildMenuElement);
+ menuItems.push(deleteUnitElement);
+ } else {
+ menuItems.push(attackMenuElement);
+ }
}
return menuItems.filter((item): item is MenuElement => item !== null);
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 3a309359f..b654ee4ee 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -7,6 +7,7 @@ import {
Attack,
Cell,
Game,
+ GameMode,
GameUpdates,
NameViewData,
Nation,
@@ -81,6 +82,7 @@ export async function createGameRunner(
game,
new Executor(game, gameStart.gameID, clientID),
callBack,
+ humans, // Passar os humanos para o GameRunner
);
gr.init();
return gr;
@@ -97,16 +99,34 @@ export class GameRunner {
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
+ private humans: PlayerInfo[], // Armazenar os humanos
) {}
init() {
- if (this.game.config().bots() > 0) {
+ // Check if the game mode is Fog of War to use specific spawn
+ if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
+ // In Fog of War mode, we use spawnFFARPlayers instead of default methods
this.game.addExecution(
- ...this.execManager.spawnBots(this.game.config().numBots()),
+ ...this.execManager.spawnFFARPlayers(
+ this.humans,
+ this.game.config().bots()
+ )
);
- }
- if (this.game.config().spawnNPCs()) {
- this.game.addExecution(...this.execManager.fakeHumanExecutions());
+
+ // Add executions for fake humans in Fog of War mode
+ if (this.game.config().spawnNPCs()) {
+ this.game.addExecution(...this.execManager.fakeHumanExecutions());
+ }
+ } else {
+ // Default game mode
+ if (this.game.config().bots() > 0) {
+ this.game.addExecution(
+ ...this.execManager.spawnBots(this.game.config().numBots()),
+ );
+ }
+ if (this.game.config().spawnNPCs()) {
+ this.game.addExecution(...this.execManager.fakeHumanExecutions());
+ }
}
this.game.addExecution(new WinCheckExecution());
}
@@ -259,4 +279,4 @@ export class GameRunner {
}
return player.bestTransportShipSpawn(targetTile);
}
-}
+}
\ No newline at end of file
diff --git a/src/core/Util.ts b/src/core/Util.ts
index 69317accf..ef0f70765 100644
--- a/src/core/Util.ts
+++ b/src/core/Util.ts
@@ -91,44 +91,6 @@ export function calculateBoundingBox(
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
}
-export function boundingBoxTiles(
- gm: GameMap,
- center: TileRef,
- radius: number,
-): TileRef[] {
- const tiles: TileRef[] = [];
-
- const centerX = gm.x(center);
- const centerY = gm.y(center);
-
- const minX = centerX - radius;
- const maxX = centerX + radius;
- const minY = centerY - radius;
- const maxY = centerY + radius;
-
- // Top and bottom edges (full width)
- for (let x = minX; x <= maxX; x++) {
- if (gm.isValidCoord(x, minY)) {
- tiles.push(gm.ref(x, minY));
- }
- if (gm.isValidCoord(x, maxY) && minY !== maxY) {
- tiles.push(gm.ref(x, maxY));
- }
- }
-
- // Left and right edges (exclude corners already added)
- for (let y = minY + 1; y < maxY; y++) {
- if (gm.isValidCoord(minX, y)) {
- tiles.push(gm.ref(minX, y));
- }
- if (gm.isValidCoord(maxX, y) && minX !== maxX) {
- tiles.push(gm.ref(maxX, y));
- }
- }
-
- return tiles;
-}
-
export function calculateBoundingBoxCenter(
gm: GameMap,
borderTiles: ReadonlySet,
diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts
index 67bd7c92d..d509f9082 100644
--- a/src/core/execution/ExecutionManager.ts
+++ b/src/core/execution/ExecutionManager.ts
@@ -1,4 +1,4 @@
-import { Execution, Game } from "../game/Game";
+import { Execution, Game, GameMode, PlayerInfo } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { ClientID, GameID, Intent, Turn } from "../Schemas";
import { simpleHash } from "../Util";
@@ -16,6 +16,7 @@ import { DonateTroopsExecution } from "./DonateTroopExecution";
import { EmbargoExecution } from "./EmbargoExecution";
import { EmojiExecution } from "./EmojiExecution";
import { FakeHumanExecution } from "./FakeHumanExecution";
+import { FFARSpawner } from "./FFARSpawner";
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NoOpExecution } from "./NoOpExecution";
@@ -50,6 +51,12 @@ export class Executor {
return new NoOpExecution();
}
+ // Interaction blocking during spawn phase in Fog of War mode
+ if (this.mg.config().gameConfig().gameMode === GameMode.FogOfWar && this.mg.inSpawnPhase()) {
+ // Convert all intents to NoOpExecution during spawn phase in FOW mode
+ return new NoOpExecution();
+ }
+
// create execution
switch (intent.type) {
case "attack": {
@@ -135,4 +142,10 @@ export class Executor {
}
return execs;
}
-}
+
+ spawnFFARPlayers(humans: PlayerInfo[], numBots: number): SpawnExecution[] {
+ // Automatic spawn in Fog of War mode
+ const nations = this.mg.nations().map(nation => nation.playerInfo);
+ return new FFARSpawner(this.mg, this.gameID).spawnFFARPlayers(humans, numBots, nations);
+ }
+}
\ No newline at end of file
diff --git a/src/core/execution/FFARSpawner.ts b/src/core/execution/FFARSpawner.ts
new file mode 100644
index 000000000..d1138549f
--- /dev/null
+++ b/src/core/execution/FFARSpawner.ts
@@ -0,0 +1,148 @@
+import { Game, PlayerInfo, PlayerType } from "../game/Game";
+import { TileRef } from "../game/GameMap";
+import { PseudoRandom } from "../PseudoRandom";
+import { GameID } from "../Schemas";
+import { simpleHash } from "../Util";
+import { SpawnExecution } from "./SpawnExecution";
+
+export class FFARSpawner {
+ private random: PseudoRandom;
+ private spawns: SpawnExecution[] = [];
+
+ constructor(
+ private gs: Game,
+ gameID: GameID,
+ ) {
+ this.random = new PseudoRandom(simpleHash(gameID));
+ }
+
+ spawnFFARPlayers(humans: PlayerInfo[], numBots: number, nations: PlayerInfo[]): SpawnExecution[] {
+ // Spawn human players with minimum distance of 20 tiles between them
+ for (const human of humans) {
+ const spawn = this.spawnHuman(human);
+ if (spawn !== null) {
+ this.spawns.push(spawn);
+ }
+ }
+
+ // Spawn fake humans (nations) with the same logic as human players
+ for (const nation of nations) {
+ const spawn = this.spawnHuman(nation);
+ if (spawn !== null) {
+ this.spawns.push(spawn);
+ }
+ }
+
+ // Spawn bots using the same logic as classic FFA mode
+ let tries = 0;
+ while (this.spawns.length < humans.length + nations.length + numBots) {
+ if (tries > 10000) {
+ console.log("too many retries while spawning bots, giving up");
+ return this.spawns;
+ }
+ const botName = this.randomBotName();
+ const spawn = this.spawnBot(botName);
+ if (spawn !== null) {
+ this.spawns.push(spawn);
+ } else {
+ tries++;
+ }
+ }
+ return this.spawns;
+ }
+
+ private spawnHuman(human: PlayerInfo): SpawnExecution | null {
+ let tries = 0;
+ const maxTries = 10000;
+ const minDistance = 20; // Minimum distance of 20 tiles between human players
+
+ while (tries < maxTries) {
+ const tile = this.randTile();
+ if (!this.gs.isLand(tile)) {
+ tries++;
+ continue;
+ }
+
+ // Check distance to existing spawns
+ let tooClose = false;
+ for (const spawn of this.spawns) {
+ if (this.gs.manhattanDist(spawn.tile, tile) < minDistance) {
+ tooClose = true;
+ break;
+ }
+ }
+
+ if (!tooClose) {
+ return new SpawnExecution(human, tile);
+ }
+
+ tries++;
+ }
+
+ console.warn(`Failed to spawn human player ${human.name} after ${maxTries} attempts`);
+ return null;
+ }
+
+ private spawnBot(botName: string): SpawnExecution | null {
+ const tile = this.randTile();
+ if (!this.gs.isLand(tile)) {
+ return null;
+ }
+ for (const spawn of this.spawns) {
+ if (this.gs.manhattanDist(spawn.tile, tile) < 30) {
+ return null;
+ }
+ }
+ return new SpawnExecution(
+ new PlayerInfo(botName, PlayerType.Bot, null, this.random.nextID()),
+ tile,
+ );
+ }
+
+ private randTile(): TileRef {
+ const x = this.random.nextInt(0, this.gs.width() - 1);
+ const y = this.random.nextInt(0, this.gs.height() - 1);
+ return this.gs.ref(x, y);
+ }
+
+ private randomBotName(): string {
+ const prefixes = [
+ "Bot",
+ "AI",
+ "Computer",
+ "Auto",
+ "NPC",
+ "Player",
+ "Digital",
+ "Virtual",
+ "Cyber",
+ "Tech",
+ ];
+ const suffixes = [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0",
+ "X",
+ "Z",
+ "Alpha",
+ "Beta",
+ "Gamma",
+ "Delta",
+ "Omega",
+ "Pro",
+ "Max",
+ "Ultra",
+ ];
+
+ const prefix = prefixes[this.random.nextInt(0, prefixes.length - 1)];
+ const suffix = suffixes[this.random.nextInt(0, suffixes.length - 1)];
+ return `${prefix}_${suffix}`;
+ }
+}
\ No newline at end of file
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index db48e0a2f..155306bc5 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -149,6 +149,7 @@ export const isGameType = (value: unknown): value is GameType =>
export enum GameMode {
FFA = "Free For All",
Team = "Team",
+ FogOfWar = "Fog of War",
}
export const isGameMode = (value: unknown): value is GameMode =>
isEnumValue(GameMode, value);