Fow test 2 on branch reset to 14 Oct

This commit is contained in:
variablevince
2025-11-08 21:52:28 +01:00
parent e7497bfb76
commit 377f847e49
28 changed files with 1494 additions and 137 deletions
+2 -1
View File
@@ -232,7 +232,8 @@
},
"game_mode": {
"ffa": "Free for All",
"teams": "Teams"
"teams": "Teams",
"fog_of_war": "Kriegsnebel"
},
"select_lang": {
"title": "Sprache auswählen"
+2 -1
View File
@@ -272,7 +272,8 @@
},
"game_mode": {
"ffa": "Free for All",
"teams": "Teams"
"teams": "Teams",
"fog_of_war": "Fog of War"
},
"select_lang": {
"title": "Select Language"
+2 -1
View File
@@ -202,7 +202,8 @@
},
"game_mode": {
"ffa": "Todos contra todos",
"teams": "Equipos"
"teams": "Equipos",
"fog_of_war": "Niebla de guerra"
},
"select_lang": {
"title": "Selecciona idioma"
+2 -1
View File
@@ -260,7 +260,8 @@
},
"game_mode": {
"ffa": "Chacun pour soi",
"teams": "Équipes"
"teams": "Équipes",
"fog_of_war": "Brouillard de guerre"
},
"select_lang": {
"title": "Sélectionner une langue"
+2 -1
View File
@@ -260,7 +260,8 @@
},
"game_mode": {
"ffa": "バトルロワイヤル",
"teams": "チーム"
"teams": "チーム",
"fog_of_war": "戦争の霧"
},
"select_lang": {
"title": "言語を選択"
+2 -1
View File
@@ -171,7 +171,8 @@
},
"game_mode": {
"ffa": "Free for All",
"teams": "Equipes"
"teams": "Equipes",
"fog_of_war": "Nevoeiro de Guerra"
},
"select_lang": {
"title": "Selecionar idioma"
+2 -1
View File
@@ -260,7 +260,8 @@
},
"game_mode": {
"ffa": "Каждый против каждого (FFA)",
"teams": "Команды"
"teams": "Команды",
"fog_of_war": "Туман войны"
},
"select_lang": {
"title": "Выберите язык"
+2 -1
View File
@@ -257,7 +257,8 @@
},
"game_mode": {
"ffa": "混战",
"teams": "团队"
"teams": "团队",
"fog_of_war": "战争迷雾"
},
"select_lang": {
"title": "选择语言"
+42 -14
View File
@@ -12,7 +12,7 @@ import {
import { createPartialGameRecord, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
import { PlayerActions, UnitType } from "../core/game/Game";
import { GameMode, PlayerActions, UnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
@@ -270,6 +270,15 @@ export class ClientGameRunner {
this.renderer.initialize();
this.input.initialize();
// Pass the game reference to the InputHandler
this.input.setGame(this.gameView);
// Pass the FogOfWarLayer reference to the InputHandler (if available)
if (this.renderer && this.renderer.fogOfWarLayer) {
this.input.setFogOfWarLayer(this.renderer.fogOfWarLayer);
}
this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => {
if (this.lobby.gameStartInfo === undefined) {
throw new Error("missing gameStartInfo");
@@ -386,9 +395,10 @@ export class ClientGameRunner {
}
private inputEvent(event: MouseUpEvent) {
if (!this.isActive || this.renderer.uiState.ghostStructure !== null) {
if (!this.isActive) {
return;
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
@@ -398,6 +408,24 @@ export class ClientGameRunner {
}
console.log(`clicked cell ${cell}`);
const tile = this.gameView.ref(cell.x, cell.y);
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
// Allow attacks even in areas with fog = 1
let isFoggedArea = false;
if (this.renderer && this.renderer.fogOfWarLayer &&
this.gameView.config().gameConfig().gameMode === GameMode.FogOfWar) {
const tileX = this.gameView.x(tile);
const tileY = this.gameView.y(tile);
const idx = tileY * this.gameView.width() + tileX;
const fogValue = this.renderer.fogOfWarLayer.getFogValueAt(idx);
// If fog is completely fogged (value 1), mark as fogged area
if (fogValue >= 1.0) {
isFoggedArea = true;
}
}
// Allow spawn in all modes, not just Fog of War mode
if (
this.gameView.isLand(tile) &&
!this.gameView.hasOwner(tile) &&
@@ -423,7 +451,7 @@ export class ClientGameRunner {
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
} else if (this.canAutoBoat(actions, tile)) {
} else if (this.canBoatAttack(actions, tile)) {
this.sendBoatAttackIntent(tile);
}
@@ -519,7 +547,7 @@ export class ClientGameRunner {
}
this.myPlayer.actions(tile).then((actions) => {
if (this.canBoatAttack(actions) !== false) {
if (!actions.canAttack && this.canBoatAttack(actions, tile)) {
this.sendBoatAttackIntent(tile);
}
});
@@ -567,7 +595,7 @@ export class ClientGameRunner {
return this.gameView.ref(cell.x, cell.y);
}
private canBoatAttack(actions: PlayerActions): false | TileRef {
private canBoatAttack(actions: PlayerActions, tile: TileRef): boolean {
const bu = actions.buildableUnits.find(
(bu) => bu.type === UnitType.TransportShip,
);
@@ -575,7 +603,11 @@ export class ClientGameRunner {
console.warn(`no transport ship buildable units`);
return false;
}
return bu.canBuild;
return (
bu.canBuild !== false &&
this.shouldBoat(tile, bu.canBuild) &&
this.gameView.isLand(tile)
);
}
private sendBoatAttackIntent(tile: TileRef) {
@@ -594,20 +626,16 @@ export class ClientGameRunner {
});
}
private canAutoBoat(actions: PlayerActions, tile: TileRef): boolean {
if (!this.gameView.isLand(tile)) return false;
const canBuild = this.canBoatAttack(actions);
if (canBuild === false) return false;
private shouldBoat(tile: TileRef, src: TileRef) {
// TODO: Global enable flag
// TODO: Global limit autoboat to nearby shore flag
// if (!enableAutoBoat) return false;
// if (!limitAutoBoatNear) return true;
const distanceSquared = this.gameView.euclideanDistSquared(tile, canBuild);
const distanceSquared = this.gameView.euclideanDistSquared(tile, src);
const limit = 100;
const limitSquared = limit * limit;
return distanceSquared < limitSquared;
if (distanceSquared > limitSquared) return false;
return true;
}
private onMouseMove(event: MouseMoveEvent) {
+10 -2
View File
@@ -155,7 +155,7 @@ export class HostLobbyModal extends LitElement {
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5 32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
></path>
</svg>
`
@@ -269,11 +269,19 @@ export class HostLobbyModal extends LitElement {
${translateText("game_mode.teams")}
</div>
</div>
<div
class="option-card ${this.gameMode === GameMode.FogOfWar ? "selected" : ""}"
@click=${() => this.handleGameModeSelection(GameMode.FogOfWar)}
>
<div class="option-card-title">
${translateText("game_mode.fog_of_war")}
</div>
</div>
</div>
</div>
${
this.gameMode === GameMode.FFA
this.gameMode === GameMode.FFA || this.gameMode === GameMode.FogOfWar
? ""
: html`
<!-- Team Count Selection -->
+122 -3
View File
@@ -1,5 +1,5 @@
import { EventBus, GameEvent } from "../core/EventBus";
import { UnitType } from "../core/game/Game";
import { GameMode, UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { UserSettings } from "../core/game/UserSettings";
import { UIState } from "./graphics/UIState";
@@ -138,6 +138,10 @@ export class InputHandler {
private readonly ZOOM_SPEED = 10;
private readonly userSettings: UserSettings = new UserSettings();
// Add reference to game and fogOfWarLayer to check the mode
private game: any = null;
private fogOfWarLayer: any = null;
constructor(
public uiState: UIState,
@@ -145,6 +149,16 @@ export class InputHandler {
private eventBus: EventBus,
) {}
// Method to set the reference to the game
public setGame(game: any) {
this.game = game;
}
// Method to set the reference to the FogOfWarLayer
public setFogOfWarLayer(fogOfWarLayer: any) {
this.fogOfWarLayer = fogOfWarLayer;
}
initialize() {
let saved: Record<string, string> = {};
try {
@@ -206,7 +220,9 @@ export class InputHandler {
this.canvas.addEventListener(
"wheel",
(e) => {
this.onScroll(e);
if (!this.onTrackpadPan(e)) {
this.onScroll(e);
}
this.onShiftScroll(e);
e.preventDefault();
},
@@ -219,6 +235,16 @@ export class InputHandler {
this.eventBus.emit(new MouseMoveEvent(e.clientX, e.clientY));
}
});
this.canvas.addEventListener("touchstart", (e) => this.onTouchStart(e), {
passive: false,
});
this.canvas.addEventListener("touchmove", (e) => this.onTouchMove(e), {
passive: false,
});
this.canvas.addEventListener("touchend", (e) => this.onTouchEnd(e), {
passive: false,
});
this.pointers.clear();
this.moveInterval = setInterval(() => {
@@ -442,7 +468,7 @@ export class InputHandler {
}
}
onPointerUp(event: PointerEvent) {
private onPointerUp(event: PointerEvent) {
if (event.button === 1) {
event.preventDefault();
return;
@@ -454,7 +480,38 @@ export class InputHandler {
this.pointerDown = false;
this.pointers.clear();
// Check if we are in Fog of War mode before showing the build menu
if (this.isModifierKeyPressed(event)) {
// If in Fog of War mode, check the fog value at the clicked position
if (this.game && this.game.config && this.fogOfWarLayer) {
// Check if the game mode is Fog of War (correct comparison)
const gameMode = this.game.config().gameConfig().gameMode;
if (gameMode === GameMode.FogOfWar) {
// Convert screen coordinates to world coordinates
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Convert to world coordinates
if (this.game.renderer && this.game.renderer.transformHandler) {
const worldCoords = this.game.renderer.transformHandler.screenToWorldCoordinates(x, y);
// Check if coordinates are valid
if (this.game.isValidCoord && this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
const tileRef = this.game.ref(worldCoords.x, worldCoords.y);
const tileX = this.game.x(tileRef);
const tileY = this.game.y(tileRef);
const idx = tileY * this.game.width() + tileX;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// If fog is completely fogged (value 1), don't show the menu
if (fogValue >= 1.0) {
return;
}
}
}
}
}
this.eventBus.emit(new ShowBuildMenuEvent(event.clientX, event.clientY));
return;
}
@@ -499,6 +556,27 @@ export class InputHandler {
}
}
private onTrackpadPan(event: WheelEvent): boolean {
if (event.shiftKey || event.ctrlKey || event.metaKey) {
return false;
}
const isTrackpadPan = event.deltaMode === 0 && event.deltaX !== 0;
if (!isTrackpadPan) {
return false;
}
const panSensitivity = 1.0;
const deltaX = -event.deltaX * panSensitivity;
const deltaY = -event.deltaY * panSensitivity;
if (Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5) {
this.eventBus.emit(new DragEvent(deltaX, deltaY));
}
return true;
}
private onPointerMove(event: PointerEvent) {
if (event.button === 1) {
event.preventDefault();
@@ -547,6 +625,47 @@ export class InputHandler {
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
}
private onTouchStart(event: TouchEvent) {
if (event.touches.length === 2) {
event.preventDefault();
// Solve screen jittering problem
const touch1 = event.touches[0];
const touch2 = event.touches[1];
this.lastPointerX = (touch1.clientX + touch2.clientX) / 2;
this.lastPointerY = (touch1.clientY + touch2.clientY) / 2;
}
}
private onTouchMove(event: TouchEvent) {
if (event.touches.length === 2) {
event.preventDefault();
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
if (this.lastPointerX !== 0 && this.lastPointerY !== 0) {
const deltaX = centerX - this.lastPointerX;
const deltaY = centerY - this.lastPointerY;
if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {
this.eventBus.emit(new DragEvent(deltaX, deltaY));
}
}
this.lastPointerX = centerX;
this.lastPointerY = centerY;
}
}
private onTouchEnd(event: TouchEvent) {
if (event.touches.length < 2) {
this.lastPointerX = 0;
this.lastPointerY = 0;
}
}
private getPinchDistance(): number {
const pointerEvents = Array.from(this.pointers.values());
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
+11 -1
View File
@@ -181,10 +181,20 @@ export class SinglePlayerModal extends LitElement {
${translateText("game_mode.teams")}
</div>
</div>
<div
class="option-card ${this.gameMode === GameMode.FogOfWar
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.FogOfWar)}
>
<div class="option-card-title">
${translateText("game_mode.fog_of_war")}
</div>
</div>
</div>
</div>
${this.gameMode === GameMode.FFA
${this.gameMode === GameMode.FFA || this.gameMode === GameMode.FogOfWar
? ""
: html`
<!-- Team Count Selection -->
+42 -13
View File
@@ -14,6 +14,7 @@ import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FPSDisplay } from "./layers/FPSDisplay";
import { FxLayer } from "./layers/FxLayer";
import { FogOfWarLayer } from "./layers/FogOfWarLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { GutterAdModal } from "./layers/GutterAdModal";
@@ -87,6 +88,7 @@ export function createRenderer(
console.error("GameLeftSidebar element not found in the DOM");
}
gameLeftSidebar.game = game;
gameLeftSidebar.eventBus = eventBus;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!teamStats || !(teamStats instanceof TeamStats)) {
@@ -230,6 +232,9 @@ export function createRenderer(
}
alertFrame.game = game;
const fogOfWarLayer = new FogOfWarLayer(game, transformHandler);
const nameLayer = new NameLayer(game, transformHandler, eventBus, fogOfWarLayer);
// When updating these layers please be mindful of the order.
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
@@ -242,26 +247,45 @@ export function createRenderer(
new FxLayer(game),
new UILayer(game, eventBus, transformHandler),
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new NameLayer(game, transformHandler, eventBus),
nameLayer,
eventsDisplay,
chatDisplay,
buildMenu,
new MainRadialMenu(
eventBus,
game,
transformHandler,
emojiTable as EmojiTable,
buildMenu,
uiState,
playerPanel,
),
// Pass the NameLayer reference to the MainRadialMenu
(() => {
const mainRadialMenu = new MainRadialMenu(
eventBus,
game,
transformHandler,
emojiTable as EmojiTable,
buildMenu,
uiState,
playerPanel,
);
mainRadialMenu.setFogOfWarLayer(fogOfWarLayer);
mainRadialMenu.setNameLayer(nameLayer);
return mainRadialMenu;
})(),
new SpawnTimer(game, transformHandler),
leaderboard,
// Pass the FogOfWarLayer reference to the Leaderboard
(() => {
leaderboard.fogOfWarLayer = fogOfWarLayer;
return leaderboard;
})(),
gameLeftSidebar,
unitDisplay,
gameRightSidebar,
// Pass the FogOfWarLayer reference to GameRightSidebar
(() => {
gameRightSidebar.fogOfWarLayer = fogOfWarLayer;
return gameRightSidebar;
})(),
controlPanel,
playerInfo,
// Pass the FogOfWarLayer and NameLayer references to PlayerInfoOverlay
(() => {
playerInfo.setFogOfWarLayer(fogOfWarLayer);
playerInfo.setNameLayer(nameLayer);
return playerInfo;
})(),
winModal,
replayPanel,
settingsModal,
@@ -273,6 +297,8 @@ export function createRenderer(
gutterAdModal,
alertFrame,
fpsDisplay,
// Fog of War layer moved to the end to render on top of all other elements
fogOfWarLayer,
];
return new GameRenderer(
@@ -302,6 +328,9 @@ export class GameRenderer {
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
// Add property for FogOfWarLayer
public fogOfWarLayer: any = null;
initialize() {
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
+4
View File
@@ -7,6 +7,7 @@ import {
GoToPositionEvent,
GoToUnitEvent,
} from "./layers/Leaderboard";
import { FogOfWarLayer } from "./layers/FogOfWarLayer";
export const GOTO_INTERVAL_MS = 16;
export const CAMERA_MAX_SPEED = 15;
@@ -22,6 +23,9 @@ export class TransformHandler {
private target: Cell | null;
private intervalID: NodeJS.Timeout | null = null;
private changed = false;
// Adding reference to FogOfWarLayer
public fogOfWarLayer: FogOfWarLayer | null = null;
constructor(
private game: GameView,
+18
View File
@@ -15,6 +15,7 @@ import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
BuildableUnit,
GameMode,
Gold,
PlayerActions,
UnitType,
@@ -129,6 +130,9 @@ export class BuildMenu extends LitElement implements Layer {
public playerActions: PlayerActions | null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
public transformHandler: TransformHandler;
// Add reference to fogOfWarLayer
public fogOfWarLayer: any = null;
init() {
this.eventBus.on(ShowBuildMenuEvent, (e) => {
@@ -151,6 +155,20 @@ export class BuildMenu extends LitElement implements Layer {
return;
}
const tile = this.game.ref(clickedCell.x, clickedCell.y);
// Check if we are in Fog of War mode and if the position is completely fogged (fog = 1)
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar && this.fogOfWarLayer) {
const x = this.game.x(tile);
const y = this.game.y(tile);
const idx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// If fog is completely fogged (value 1), don't show the menu
if (fogValue >= 1.0) {
return;
}
}
this.showMenu(tile);
});
this.eventBus.on(CloseViewEvent, () => this.hideMenu());
+516
View File
@@ -0,0 +1,516 @@
import { GameView } from "../../../core/game/GameView";
import { GameMode } from "../../../core/game/Game";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
export class FogOfWarLayer implements Layer {
private fogCanvas: HTMLCanvasElement;
private fogContext: CanvasRenderingContext2D;
private fogImageData: ImageData;
// Fog of War state (0.0 = fully visible, 0.8-1.0 = fog)
private fogMap: Float32Array;
// Territory map
// 0 = neutral
// 1 = controlled by player
private territoryMap: Uint8Array;
// Chunk system for optimization
private chunks: FogChunk[];
private chunkSize: number = 8;
private chunksX: number;
private chunksY: number;
// Territory tracking
private territory: Set<number>; // stores indices: y * mapWidth + x
// Update timing
private lastFogUpdate: number = 0;
private updateInterval: number = 100; // milliseconds - increased frequency for smoother transitions
constructor(
private game: GameView,
private transformHandler: TransformHandler,
) {
this.territory = new Set();
// Initialize fog map with Float32Array for smooth fading
this.fogMap = new Float32Array(this.game.width() * this.game.height()).fill(1.0); // 1.0 = never seen
// Initialize territory map
this.territoryMap = new Uint8Array(this.game.width() * this.game.height()).fill(0);
// Initialize chunks
this.chunksX = Math.ceil(this.game.width() / this.chunkSize);
this.chunksY = Math.ceil(this.game.height() / this.chunkSize);
this.chunks = [];
for (let y = 0; y < this.chunksY; y++) {
for (let x = 0; x < this.chunksX; x++) {
this.chunks.push({
x,
y,
isDirty: true,
startX: x * this.chunkSize,
startY: y * this.chunkSize,
endX: Math.min((x + 1) * this.chunkSize, this.game.width()),
endY: Math.min((y + 1) * this.chunkSize, this.game.height())
});
}
}
}
shouldTransform(): boolean {
return true;
}
init() {
// Create fog canvas
this.fogCanvas = document.createElement("canvas");
this.fogCanvas.width = this.game.width();
this.fogCanvas.height = this.game.height();
const context = this.fogCanvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.fogContext = context;
// Create image data for efficient rendering
this.fogImageData = this.fogContext.createImageData(
this.game.width(),
this.game.height()
);
// Mark all chunks as dirty for initial render
this.markAllChunksDirty();
}
redraw() {
// Mark all chunks as dirty when redrawing
this.markAllChunksDirty();
this.renderFog();
}
tick() {
// Only update fog in Fog of War mode
if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
return;
}
const now = Date.now();
if (now - this.lastFogUpdate < this.updateInterval) {
return;
}
this.lastFogUpdate = now;
let hasChanges = false;
// Gradually fade out vision that is no longer updated
// Only fade values that are less than 0.8 (visible areas)
// This creates a smooth transition from visible (0.0) to remembered (0.8) to unknown (1.0)
for (let i = 0; i < this.fogMap.length; i++) {
if (this.fogMap[i] < 0.8) { // Only fade values that are less than 0.8 (visible areas)
// Slowly return to 0.8 (remembered territory), but never to 1.0 (never seen)
const newValue = Math.min(this.fogMap[i] + 0.005, 0.8); // Increased fade rate for smoother transition
if (Math.abs(newValue - this.fogMap[i]) > 0.0001) {
this.fogMap[i] = newValue;
hasChanges = true;
// Mark the chunk as dirty
const x = i % this.game.width();
const y = Math.floor(i / this.game.width());
const chunkX = Math.floor(x / this.chunkSize);
const chunkY = Math.floor(y / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
}
// Update vision for player's units with dynamic fading
const myPlayer = this.game.myPlayer();
if (myPlayer) {
const units = myPlayer.units();
for (const unit of units) {
// Get vision range based on unit type
const visionRange = this.getUnitVisionRange(unit);
this.updateVisionWithFade(unit.tile(), visionRange);
}
// Update vision for player's territory borders with 20 tile radius
this.updateTerritoryBorderVision(myPlayer);
// Update vision from allied players in Fog of War mode
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
this.updateAlliedVision(myPlayer);
}
}
// Update vision for claimed territory
for (const idx of this.territory) {
if (this.fogMap[idx] > 0.0) {
this.fogMap[idx] = 0.0; // totally visible
hasChanges = true;
// Mark the chunk as dirty
const x = idx % this.game.width();
const y = Math.floor(idx / this.game.width());
const chunkX = Math.floor(x / this.chunkSize);
const chunkY = Math.floor(y / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
// Only mark all chunks as dirty if there are significant changes
// This optimization prevents unnecessary rendering when no visibility changes occur
if (hasChanges) {
// Additional check: if many chunks are dirty, it's more efficient to render all at once
const dirtyChunkCount = this.chunks.filter(chunk => chunk.isDirty).length;
if (dirtyChunkCount > this.chunks.length * 0.5) {
this.markAllChunksDirty();
}
}
}
renderLayer(context: CanvasRenderingContext2D) {
// Only render if the game mode is Fog of War
if (this.game.config().gameConfig().gameMode !== GameMode.FogOfWar) {
return;
}
// Only render if there are dirty chunks
if (this.hasDirtyChunks()) {
this.renderFog();
}
// Draw the fog canvas onto the main context
context.drawImage(
this.fogCanvas,
-this.game.width() / 2,
-this.game.height() / 2,
this.game.width(),
this.game.height()
);
}
private updateVisionWithFade(centerTile: number, radius: number) {
const centerX = this.game.x(centerTile);
const centerY = this.game.y(centerTile);
// Create a circular vision range with dynamic fading
const radiusSq = radius * radius;
for (let i = -radius; i <= radius; i++) {
for (let j = -radius; j <= radius; j++) {
// Check if the tile is within the circular radius
if (i * i + j * j <= radiusSq) {
const dx = centerX + i;
const dy = centerY + j;
if (dx >= 0 && dy >= 0 && dx < this.game.width() && dy < this.game.height()) {
const dist = Math.sqrt(i * i + j * j);
if (dist < radius) {
// Calculate fade based on distance: closer = more visible
// Using a smoother curve for more natural transition
const normalizedDist = Math.max(0.0, Math.min(1.0, dist / radius));
// Apply easing function for smoother transition: 0.8 * (1 - (1 - dist)^2)
const alpha = 0.8 * (1 - Math.pow(1 - normalizedDist, 2));
const idx = dy * this.game.width() + dx;
// Only update if the new value is lower (more visible)
if (alpha < this.fogMap[idx]) {
this.fogMap[idx] = alpha;
// Mark the chunk as dirty
const chunkX = Math.floor(dx / this.chunkSize);
const chunkY = Math.floor(dy / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
}
}
}
}
}
// New method to update vision from allied players
private updateAlliedVision(myPlayer: any) {
// Get all allied players
const alliedPlayers = this.game.playerViews().filter(player =>
player.id() !== myPlayer.id() && myPlayer.isAlliedWith(player)
);
// Update vision from each allied player's units
for (const alliedPlayer of alliedPlayers) {
const units = alliedPlayer.units();
for (const unit of units) {
// Get vision range based on unit type
const visionRange = this.getUnitVisionRange(unit);
this.updateVisionWithFade(unit.tile(), visionRange);
}
// Update vision from allied player's territory borders
if (typeof alliedPlayer.borderTiles === 'function') {
const borderTilesResult = alliedPlayer.borderTiles();
// Handle both synchronous and asynchronous borderTiles
if (borderTilesResult instanceof Promise) {
// For asynchronous case (PlayerView)
borderTilesResult.then((result: any) => {
const borderTiles = result.borderTiles || result;
this.applyAlliedBorderVision(borderTiles);
}).catch((error: any) => {
console.warn("Failed to get allied player border tiles:", error);
});
} else {
// For synchronous case
const borderTiles = borderTilesResult;
this.applyAlliedBorderVision(borderTiles);
}
}
}
}
// Apply vision to allied border tiles and surrounding area
private applyAlliedBorderVision(borderTiles: any) {
// Set border tiles to fully visible (0.0)
for (const tile of borderTiles) {
const x = this.game.x(tile);
const y = this.game.y(tile);
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
if (this.fogMap[idx] > 0.0) {
this.fogMap[idx] = 0.0; // totally visible
// Mark the chunk as dirty
const chunkX = Math.floor(x / this.chunkSize);
const chunkY = Math.floor(y / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
// Also apply vision radius around each border tile
this.updateVisionWithFade(tile, 20);
}
}
private renderFog() {
// Update only dirty chunks
for (const chunk of this.chunks) {
if (!chunk.isDirty) continue;
// Render this chunk
for (let y = chunk.startY; y < chunk.endY; y++) {
for (let x = chunk.startX; x < chunk.endX; x++) {
const idx = y * this.game.width() + x;
// Calculate pixel index in image data
const pixelIndex = (y * this.game.width() + x) * 4;
// First draw the territory (layer below fog)
if (this.territoryMap[idx] === 1) {
// Draw territory with green tint
this.fogImageData.data[pixelIndex + 0] = 0; // R
this.fogImageData.data[pixelIndex + 1] = 128; // G
this.fogImageData.data[pixelIndex + 2] = 0; // B
this.fogImageData.data[pixelIndex + 3] = 51; // A (20% opacity)
} else {
// Clear territory pixel
this.fogImageData.data[pixelIndex + 0] = 0; // R
this.fogImageData.data[pixelIndex + 1] = 0; // G
this.fogImageData.data[pixelIndex + 2] = 0; // B
this.fogImageData.data[pixelIndex + 3] = 0; // A
}
// Then draw the fog layer on top with dynamic alpha
const fogValue = this.fogMap[idx];
// Apply fog with dynamic alpha based on visibility
// 0.0 = totally visible (transparent)
// 0.8-1.0 = fog (black with varying opacity)
const alpha = Math.min(255, Math.max(0, Math.floor(fogValue * 255)));
// Add subtle texture variation for visual interest
const base = 20 + (Math.random() * 10); // Simple noise texture
this.fogImageData.data[pixelIndex + 0] = base; // R
this.fogImageData.data[pixelIndex + 1] = base; // G
this.fogImageData.data[pixelIndex + 2] = base; // B
this.fogImageData.data[pixelIndex + 3] = alpha; // A (dynamic opacity)
}
}
// Mark chunk as clean
chunk.isDirty = false;
}
// Put the updated image data to the canvas
this.fogContext.putImageData(this.fogImageData, 0, 0);
}
private markAllChunksDirty() {
for (const chunk of this.chunks) {
chunk.isDirty = true;
}
}
private hasDirtyChunks(): boolean {
return this.chunks.some(chunk => chunk.isDirty);
}
// Public method to claim territory (make it permanently visible)
public claimTerritory(x: number, y: number) {
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
this.territory.add(idx);
this.fogMap[idx] = 0.0; // totally visible
// Mark the chunk as dirty
const chunkX = Math.floor(x / this.chunkSize);
const chunkY = Math.floor(y / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
// Get fog value at specific index
public getFogValueAt(index: number): number {
if (index >= 0 && index < this.fogMap.length) {
return this.fogMap[index];
}
return 1.0; // Default to fully fogged if index is out of bounds
}
// Get vision range based on unit type
private getUnitVisionRange(unit: any): number {
// Get unit type
const unitType = unit.type();
// Get unit level (default to 1 if not available)
const level = typeof unit.level === 'function' ? unit.level() : 1;
// Calculate vision range boost: 20% per level
const visionBoost = 1 + (level - 1) * 0.2;
// Define base vision ranges for different unit types
let baseVisionRange = 15; // Default vision range for other units
switch(unitType) {
case "City":
baseVisionRange = 30; // Cities have long vision range
break;
case "Port":
baseVisionRange = 80; // Ports have good vision range
break;
case "Defense Post":
baseVisionRange = 70; // Defense posts have moderate vision range
break;
case "Warship":
baseVisionRange = 140; // Warships have good vision range
break;
case "Missile Silo":
baseVisionRange = 200; // Missile silos have moderate vision range
break;
case "SAM Launcher":
baseVisionRange = 400; // SAM launchers have moderate vision range
break;
case "Factory":
baseVisionRange = 35; // Factories have moderate vision range
break;
case "Atom Bomb":
baseVisionRange = 30; // Atom bombs have limited vision range
break;
case "Hydrogen Bomb":
baseVisionRange = 80; // Hydrogen bombs have moderate vision range
break;
case "MIRV":
baseVisionRange = 100; // MIRV bombs have good vision range
break;
}
// Apply vision boost for upgradable units
// Upgradable units: City, Port, Missile Silo, SAM Launcher, Factory
const upgradableUnits = ["City", "Port", "Missile Silo", "SAM Launcher", "Factory"];
if (upgradableUnits.includes(unitType)) {
return Math.round(baseVisionRange * visionBoost);
}
// Return base vision range for non-upgradable units
return baseVisionRange;
}
// Update vision for territory borders
private updateTerritoryBorderVision(player: any) {
// Get player's border tiles
if (typeof player.borderTiles === 'function') {
const borderTilesResult = player.borderTiles();
// Handle both synchronous and asynchronous borderTiles
if (borderTilesResult instanceof Promise) {
// For asynchronous case (PlayerView)
borderTilesResult.then((result: any) => {
const borderTiles = result.borderTiles || result;
this.applyBorderVision(borderTiles);
}).catch((error: any) => {
console.warn("Failed to get border tiles:", error);
});
} else {
// For synchronous case (PlayerImpl)
const borderTiles = borderTilesResult;
this.applyBorderVision(borderTiles);
}
}
}
// Apply vision to border tiles and surrounding area
private applyBorderVision(borderTiles: any) {
// Set border tiles to fully visible (0.0)
for (const tile of borderTiles) {
const x = this.game.x(tile);
const y = this.game.y(tile);
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
if (this.fogMap[idx] > 0.0) {
this.fogMap[idx] = 0.0; // totally visible
// Mark the chunk as dirty
const chunkX = Math.floor(x / this.chunkSize);
const chunkY = Math.floor(y / this.chunkSize);
const chunkIndex = chunkY * this.chunksX + chunkX;
if (chunkIndex < this.chunks.length) {
this.chunks[chunkIndex].isDirty = true;
}
}
}
// Also apply vision radius around each border tile
this.updateVisionWithFade(tile, 20);
}
}
}
// Chunk interface for optimization
interface FogChunk {
x: number;
y: number;
isDirty: boolean;
startX: number;
startY: number;
endX: number;
endY: number;
}
+26 -1
View File
@@ -7,6 +7,7 @@ import teamRegularIcon from "../../../../resources/images/TeamIconRegularWhite.s
import teamSolidIcon from "../../../../resources/images/TeamIconSolidWhite.svg";
import { GameMode } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { EventBus } from "../../../core/EventBus";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
@@ -22,6 +23,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
private playerColor: Colord = new Colord("#FFFFFF");
public game: GameView;
public eventBus: EventBus; // Adicionando a propriedade eventBus
private _shownOnInit = false;
createRenderRoot() {
@@ -66,11 +68,34 @@ export class GameLeftSidebar extends LitElement implements Layer {
}
private toggleLeaderboard(): void {
// In Fog of War mode, check if the player is eliminated before allowing to switch to global mode
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
const isPlayerEliminated = myPlayer !== null && !myPlayer.isAlive();
// If the player is alive, only allow local mode
if (myPlayer && myPlayer.isAlive()) {
// Ensure the leaderboard is in local mode
const leaderboard = this.querySelector('leader-board') as any;
if (leaderboard && leaderboard._leaderboardMode !== "local") {
leaderboard._leaderboardMode = "local";
}
}
}
this.isLeaderboardShow = !this.isLeaderboardShow;
// Emitting event when the leaderboard is toggled
if (this.eventBus) {
this.eventBus.emit({ type: "leaderboardToggled", show: this.isLeaderboardShow });
}
}
private toggleTeamLeaderboard(): void {
this.isTeamLeaderboardShow = !this.isTeamLeaderboardShow;
// Emitting event when the team leaderboard is toggled
if (this.eventBus) {
this.eventBus.emit({ type: "teamLeaderboardToggled", show: this.isTeamLeaderboardShow });
}
}
private get isTeamGame(): boolean {
@@ -148,4 +173,4 @@ export class GameLeftSidebar extends LitElement implements Layer {
</aside>
`;
}
}
}
+83 -1
View File
@@ -7,6 +7,7 @@ import replayRegularIcon from "../../../../resources/images/ReplayRegularIconWhi
import replaySolidIcon from "../../../../resources/images/ReplaySolidIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
@@ -15,11 +16,13 @@ import { translateText } from "../../Utils";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import { FogOfWarLayer } from "./FogOfWarLayer";
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public fogOfWarLayer: FogOfWarLayer | null = null; // Reference to FogOfWarLayer
@state()
private _isSinglePlayer: boolean = false;
@@ -104,9 +107,88 @@ export class GameRightSidebar extends LitElement implements Layer {
);
}
// Check if there are visible players or bots in Fog of War mode
private hasVisiblePlayersOrBots(): boolean {
// If not in Fog of War mode, show the sidebar
if (!this.game || this.game.config().gameConfig().gameMode !== GameMode.FogOfWar || !this.fogOfWarLayer) {
return true;
}
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
return false;
}
// Verificar todos os jogadores
const playerViews = this.game.playerViews();
for (const player of playerViews) {
// Ignore the own player
if (player.id() === myPlayer.id()) {
continue;
}
// Check if the player is alive
if (!player.isAlive()) {
continue;
}
// Check if the player has name location
const nameLocation = player.nameLocation();
if (!nameLocation) {
continue;
}
// Check if the player is visible (not covered by fog)
const x = nameLocation.x;
const y = nameLocation.y;
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// If fog is between 0.0 and 0.8 (visible or remembered area), the player is visible
if (fogValue < 0.8) {
return true;
}
}
}
// Verificar unidades (bots) de outros jogadores
const units = this.game.units();
for (const unit of units) {
// Check units that don't belong to the current player
if (unit.owner().id() !== myPlayer.id()) {
// Get the unit position
const tile = unit.tile();
const x = this.game.x(tile);
const y = this.game.y(tile);
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// If fog is between 0.0 and 0.8 (visible or remembered area), the unit is visible
if (fogValue < 0.8) {
return true;
}
}
}
}
// If we didn't find any visible player or bot, don't show the sidebar
return false;
}
render() {
if (this.game === undefined) return html``;
// In Fog of War mode, only show the sidebar if there are visible players or bots
const shouldShowSidebar = this.hasVisiblePlayersOrBots();
if (!shouldShowSidebar) {
return html``;
}
return html`
<aside
class=${`flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-gray-800/70 backdrop-blur-sm shadow-xs rounded-tl-lg rounded-bl-lg transition-transform duration-300 ease-out transform ${
@@ -175,4 +257,4 @@ export class GameRightSidebar extends LitElement implements Layer {
return html``;
}
}
}
}
+172 -14
View File
@@ -3,9 +3,11 @@ import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
import { FogOfWarLayer } from "./FogOfWarLayer";
interface Entry {
name: string;
@@ -17,10 +19,17 @@ interface Entry {
player: PlayerView;
}
// Event to view another player's vision (for eliminated players)
export class ViewPlayerVisionEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
// Event to go to a player
export class GoToPlayerEvent implements GameEvent {
constructor(public player: PlayerView) {}
}
// Event to go to a position
export class GoToPositionEvent implements GameEvent {
constructor(
public x: number,
@@ -28,6 +37,7 @@ export class GoToPositionEvent implements GameEvent {
) {}
}
// Event to go to a unit
export class GoToUnitEvent implements GameEvent {
constructor(public unit: UnitView) {}
}
@@ -36,6 +46,7 @@ export class GoToUnitEvent implements GameEvent {
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
public eventBus: EventBus | null = null;
public fogOfWarLayer: FogOfWarLayer | null = null; // Reference to FogOfWarLayer
players: Entry[] = [];
@@ -47,6 +58,10 @@ export class Leaderboard extends LitElement implements Layer {
@state()
private _sortOrder: "asc" | "desc" = "desc";
// Leaderboard mode: 'local' for visible only, 'global' for all players
@state()
private _leaderboardMode: "local" | "global" = "local";
createRenderRoot() {
return this; // use light DOM for Tailwind support
@@ -71,6 +86,67 @@ export class Leaderboard extends LitElement implements Layer {
}
this.updateLeaderboard();
}
// Check if the player can access the global leaderboard mode
private canAccessGlobalMode(): boolean {
// In Fog of War mode, only eliminated players can access global mode
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
return myPlayer !== null && !myPlayer.isAlive();
}
// In other modes, everyone can access global mode
return true;
}
// Toggle between leaderboard modes
private toggleLeaderboardMode() {
// Check if the player can access global mode
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
if (!this.canAccessGlobalMode() && this._leaderboardMode === "local") {
// If the player is alive and trying to switch to global mode, don't allow
return;
}
}
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
this._leaderboardMode = this._leaderboardMode === "local" ? "global" : "local";
this.updateLeaderboard();
}
}
// Check if a player is visible in Fog of War mode
private isPlayerVisible(player: PlayerView): boolean {
// If we're not in Fog of War mode, all players are visible
if (!this.game || this.game.config().gameConfig().gameMode !== GameMode.FogOfWar || !this.fogOfWarLayer) {
return true;
}
// If the player is eliminated, they are not visible in the normal leaderboard
if (!player.isAlive()) {
return false;
}
// Get the player's position
const nameLocation = player.nameLocation();
if (!nameLocation) {
return false;
}
const x = nameLocation.x;
const y = nameLocation.y;
// Check if coordinates are valid
if (x >= 0 && y >= 0 && x < this.game.width() && y < this.game.height()) {
const idx = y * this.game.width() + x;
const fogValue = this.fogOfWarLayer.getFogValueAt(idx);
// Consider visible if fog is between 0.0 and 0.8 (visible or remembered area)
return fogValue < 0.8;
}
return false;
}
private updateLeaderboard() {
if (this.game === null) throw new Error("Not initialized");
@@ -99,7 +175,31 @@ export class Leaderboard extends LitElement implements Layer {
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
const alivePlayers = sorted.filter((player) => player.isAlive());
// In Fog of War mode, filter players based on visibility and player state
let filteredPlayers = sorted;
if (this.game.config().gameConfig().gameMode === GameMode.FogOfWar) {
if (this._leaderboardMode === "local") {
// Local mode: show only visible players
filteredPlayers = sorted.filter((player) => this.isPlayerVisible(player));
} else {
// Global mode: check if the current player is eliminated
const isPlayerEliminated = myPlayer !== null && !myPlayer.isAlive();
// If the current player is eliminated, show all players
// If the current player is alive, show only visible alive players
if (isPlayerEliminated) {
// Eliminated players can see all players in global mode
filteredPlayers = sorted;
} else {
// Alive players can only see visible alive players in global mode
filteredPlayers = sorted.filter((player) =>
player.isAlive() && this.isPlayerVisible(player)
);
}
}
}
const alivePlayers = filteredPlayers.filter((player) => player.isAlive());
const playersToShow = this.showTopFive
? alivePlayers.slice(0, 5)
: alivePlayers;
@@ -119,6 +219,7 @@ export class Leaderboard extends LitElement implements Layer {
};
});
// If it's my player and not in the list, add it
if (
myPlayer !== null &&
this.players.find((p) => p.isMyPlayer) === undefined
@@ -131,7 +232,15 @@ export class Leaderboard extends LitElement implements Layer {
}
}
if (myPlayer.isAlive()) {
// In Fog of War mode, only add my player if they are visible or if we are in global mode
// and the player is eliminated
const isPlayerEliminated = myPlayer !== null && !myPlayer.isAlive();
const shouldAddMyPlayer =
this.game.config().gameConfig().gameMode !== GameMode.FogOfWar ||
(this._leaderboardMode === "global" && isPlayerEliminated) ||
this.isPlayerVisible(myPlayer);
if (myPlayer.isAlive() && shouldAddMyPlayer) {
const myPlayerTroops = myPlayer.troops() / 10;
this.players.pop();
this.players.push({
@@ -153,6 +262,18 @@ export class Leaderboard extends LitElement implements Layer {
private handleRowClickPlayer(player: PlayerView) {
if (this.eventBus === null) return;
// In Fog of War mode, eliminated players can view other players' vision
if (this.game?.config().gameConfig().gameMode === GameMode.FogOfWar) {
const myPlayer = this.game.myPlayer();
if (myPlayer && !myPlayer.isAlive()) {
// Emit event to view the selected player's vision
this.eventBus.emit(new ViewPlayerVisionEvent(player));
return;
}
}
// Comportamento normal para jogadores vivos
this.eventBus.emit(new GoToPlayerEvent(player));
}
@@ -166,6 +287,11 @@ export class Leaderboard extends LitElement implements Layer {
if (!this.visible) {
return html``;
}
const isFogOfWarMode = this.game?.config().gameConfig().gameMode === GameMode.FogOfWar;
const myPlayer = this.game?.myPlayer();
const isPlayerEliminated = myPlayer !== undefined && myPlayer !== null && !myPlayer.isAlive();
return html`
<div
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-xs lg:text-sm md:max-h-[50vh] ${this
@@ -174,9 +300,18 @@ export class Leaderboard extends LitElement implements Layer {
: "hidden"}"
@contextmenu=${(e: Event) => e.preventDefault()}
>
${isFogOfWarMode ? html`
<div class="bg-gray-800/70 w-full text-center py-1 text-xs">
${this._leaderboardMode === "local"
? translateText("leaderboard.local_mode")
: translateText("leaderboard.global_mode")}
${isPlayerEliminated ? html` (${translateText("leaderboard.eliminated")})` : ""}
</div>
` : ""}
<div
class="grid bg-gray-800/70 w-full text-xs md:text-xs lg:text-sm"
style="grid-template-columns: 30px 100px 70px 55px 75px;"
style="grid-template-columns: 30px 100px 70px 55px 75px${isFogOfWarMode && isPlayerEliminated ? ' 20px' : ''};"
>
<div class="contents font-bold bg-gray-700/50">
<div class="py-1 md:py-2 text-center border-b border-slate-500">
@@ -218,6 +353,11 @@ export class Leaderboard extends LitElement implements Layer {
: "⬇️"
: ""}
</div>
${isFogOfWarMode && isPlayerEliminated ? html`
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${translateText("leaderboard.view")}
</div>
` : ""}
</div>
${repeat(
@@ -227,7 +367,7 @@ export class Leaderboard extends LitElement implements Layer {
<div
class="contents hover:bg-slate-600/60 ${player.isMyPlayer
? "font-bold"
: ""} cursor-pointer"
: ""} ${isFogOfWarMode && isPlayerEliminated ? 'cursor-pointer' : 'cursor-pointer'}"
@click=${() => this.handleRowClickPlayer(player.player)}
>
<div class="py-1 md:py-2 text-center border-b border-slate-500">
@@ -247,21 +387,39 @@ export class Leaderboard extends LitElement implements Layer {
<div class="py-1 md:py-2 text-center border-b border-slate-500">
${player.troops}
</div>
${isFogOfWarMode && isPlayerEliminated ? html`
<div class="py-1 md:py-2 text-center border-b border-slate-500">
👁️
</div>
` : ""}
</div>
`,
)}
</div>
</div>
<button
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto block"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
}}
>
${this.showTopFive ? "+" : "-"}
</button>
<div class="flex justify-center gap-2 mt-1">
<button
class="px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
}}
>
${this.showTopFive ? "+" : "-"}
</button>
${isFogOfWarMode && isPlayerEliminated ? html`
<button
class="px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white"
@click=${() => this.toggleLeaderboardMode()}
>
${this._leaderboardMode === "local"
? translateText("leaderboard.switch_to_global")
: translateText("leaderboard.switch_to_local")}
</button>
` : ""}
</div>
`;
}
}
@@ -270,4 +428,4 @@ function formatPercentage(value: number): string {
const perc = value * 100;
if (Number.isNaN(perc)) return "0%";
return perc.toFixed(1) + "%";
}
}
+48 -4
View File
@@ -1,7 +1,7 @@
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { PlayerActions } from "../../../core/game/Game";
import { 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();
}
}
}
}
+60 -1
View File
@@ -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;
}
}
+66 -18
View File
@@ -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<string> = 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
}
}
}
@@ -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<UnitType> {
const Units: Set<UnitType> = new Set<UnitType>();
@@ -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);
+26 -6
View File
@@ -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);
}
}
}
-38
View File
@@ -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<TileRef>,
+15 -2
View File
@@ -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);
}
}
+148
View File
@@ -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}`;
}
}
+1
View File
@@ -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);