mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 16:56:36 +00:00
Fow test 2 on branch reset to 14 Oct
This commit is contained in:
@@ -232,7 +232,8 @@
|
||||
},
|
||||
"game_mode": {
|
||||
"ffa": "Free for All",
|
||||
"teams": "Teams"
|
||||
"teams": "Teams",
|
||||
"fog_of_war": "Kriegsnebel"
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "Sprache auswählen"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -260,7 +260,8 @@
|
||||
},
|
||||
"game_mode": {
|
||||
"ffa": "バトルロワイヤル",
|
||||
"teams": "チーム"
|
||||
"teams": "チーム",
|
||||
"fog_of_war": "戦争の霧"
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "言語を選択"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -260,7 +260,8 @@
|
||||
},
|
||||
"game_mode": {
|
||||
"ffa": "Каждый против каждого (FFA)",
|
||||
"teams": "Команды"
|
||||
"teams": "Команды",
|
||||
"fog_of_war": "Туман войны"
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "Выберите язык"
|
||||
|
||||
@@ -257,7 +257,8 @@
|
||||
},
|
||||
"game_mode": {
|
||||
"ffa": "混战",
|
||||
"teams": "团队"
|
||||
"teams": "团队",
|
||||
"fog_of_war": "战争迷雾"
|
||||
},
|
||||
"select_lang": {
|
||||
"title": "选择语言"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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``;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) + "%";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user