This commit is contained in:
evanpelle
2025-03-27 20:43:56 -07:00
committed by GitHub
parent 9ed1fe865c
commit d8fe41de7a
37 changed files with 767 additions and 387 deletions
+18 -2
View File
@@ -1,4 +1,10 @@
import { PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game";
import {
PlayerID,
GameMapType,
Difficulty,
GameType,
TeamName,
} from "../core/game/Game";
import { EventBus } from "../core/EventBus";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent } from "./InputHandler";
@@ -165,6 +171,15 @@ export class ClientGameRunner {
clientID: this.lobby.clientID,
},
];
let winner: ClientID | TeamName | null = null;
if (update.winnerType == "player") {
winner = this.gameView
.playerBySmallID(update.winner as number)
.clientID();
} else {
winner = update.winner as TeamName;
}
const record = createGameRecord(
this.lobby.gameID,
this.lobby.gameConfig,
@@ -173,7 +188,8 @@ export class ClientGameRunner {
[],
LocalPersistantStats.startTime(),
Date.now(),
this.gameView.playerBySmallID(update.winnerID).id(),
winner,
update.winnerType,
update.allPlayersStats,
);
LocalPersistantStats.endGame(record);
+83 -53
View File
@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, query, property, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game";
import { GameConfig, GameInfo } from "../core/Schemas";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
@@ -22,6 +22,7 @@ export class HostLobbyModal extends LitElement {
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs = false;
@state() private gameMode: GameMode = GameMode.FFA;
@state() private disableNukes: boolean = false;
@state() private bots: number = 400;
@state() private infiniteGold: boolean = false;
@@ -135,31 +136,54 @@ export class HostLobbyModal extends LitElement {
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
${translateText("host_modal.options_title")}
<!-- Game Mode Selection -->
<div class="options-section">
<div class="option-title">${translateText("host_modal.mode")}</div>
<div class="option-cards">
<div
class="option-card ${this.gameMode === GameMode.FFA ? "selected" : ""}"
@click=${() => this.handleGameModeSelection(GameMode.FFA)}
>
<div class="option-card-title">
${translateText("game_mode.ffa")}
</div>
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots == 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
<div
class="option-card ${this.gameMode === GameMode.Team ? "selected" : ""}"
@click=${() => this.handleGameModeSelection(GameMode.Team)}
>
<div class="option-card-title">
${translateText("game_mode.teams")}
</div>
</div>
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
${translateText("host_modal.options_title")}
</div>
<div class="option-cards">
<label for="bots-count" class="option-card">
<input
type="range"
id="bots-count"
min="0"
max="400"
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("host_modal.bots")}</span>${
this.bots == 0
? translateText("host_modal.bots_disabled")
: this.bots
}
</div>
</label>
<label
for="host-modal-disable-npcs"
@@ -244,37 +268,37 @@ export class HostLobbyModal extends LitElement {
</div>
</div>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${
this.players.length === 1
? translateText("host_modal.player")
: translateText("host_modal.players")
}
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
${this.players.length}
${
this.players.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")
? translateText("host_modal.player")
: translateText("host_modal.players")
}
</button>
</div>
<div class="players-list">
${this.players.map(
(player) => html`<span class="player-tag">${player}</span>`,
)}
</div>
</div>
</o-modal>
<button
@click=${this.startGame}
?disabled=${this.players.length < 2}
class="start-game-button"
>
${
this.players.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")
}
</button>
</div>
</o-modal>
`;
}
@@ -380,6 +404,11 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
private async handleGameModeSelection(value: GameMode) {
this.gameMode = value;
this.putGameConfig();
}
private async putGameConfig() {
const config = await getServerConfigFromClient();
const response = await fetch(
@@ -398,6 +427,7 @@ export class HostLobbyModal extends LitElement {
infiniteGold: this.infiniteGold,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
gameMode: this.gameMode,
} as GameConfig),
},
);
@@ -456,7 +486,7 @@ export class HostLobbyModal extends LitElement {
})
.then((response) => response.json())
.then((data: GameInfo) => {
console.log(`got response: ${data}`);
console.log(`got game info response: ${JSON.stringify(data)}`);
this.players = data.clients.map((p) => p.username);
});
}
+5 -3
View File
@@ -6,6 +6,7 @@ import {
ClientID,
ClientMessage,
ClientMessageSchema,
ClientSendWinnerMessage,
GameConfig,
GameID,
GameRecordSchema,
@@ -33,7 +34,7 @@ export class LocalServer {
private paused = false;
private winner: ClientID | null = null;
private winner: ClientSendWinnerMessage = null;
private allPlayersStats: AllPlayersStats = {};
constructor(
@@ -124,7 +125,7 @@ export class LocalServer {
}
}
if (clientMsg.type == "winner") {
this.winner = clientMsg.winner;
this.winner = clientMsg;
this.allPlayersStats = clientMsg.allPlayersStats;
}
}
@@ -164,7 +165,8 @@ export class LocalServer {
this.turns,
this.startedAt,
Date.now(),
this.winner,
this.winner?.winner,
this.winner?.winnerType,
this.allPlayersStats,
);
if (!saveFullGame) {
+35 -2
View File
@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game";
import { generateID as generateID } from "../core/Util";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
@@ -28,6 +28,7 @@ export class SinglePlayerModal extends LitElement {
@state() private infiniteTroops: boolean = false;
@state() private instantBuild: boolean = false;
@state() private useRandomMap: boolean = false;
@state() private gameMode: GameMode = GameMode.FFA;
render() {
return html`
@@ -107,6 +108,33 @@ export class SinglePlayerModal extends LitElement {
</div>
</div>
<!-- Game Mode Selection -->
<div class="options-section">
<div class="option-title">${translateText("host_modal.mode")}</div>
<div class="option-cards">
<div
class="option-card ${this.gameMode === GameMode.FFA
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.FFA)}
>
<div class="option-card-title">
${translateText("game_mode.ffa")}
</div>
</div>
<div
class="option-card ${this.gameMode === GameMode.Team
? "selected"
: ""}"
@click=${() => this.handleGameModeSelection(GameMode.Team)}
>
<div class="option-card-title">
${translateText("game_mode.teams")}
</div>
</div>
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">
@@ -122,7 +150,7 @@ export class SinglePlayerModal extends LitElement {
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
.value="${String(this.bots)}"
/>
<div class="option-card-title">
<span>${translateText("single_modal.bots")}</span>${this
@@ -277,6 +305,10 @@ export class SinglePlayerModal extends LitElement {
this.disableNukes = Boolean((e.target as HTMLInputElement).checked);
}
private handleGameModeSelection(value: GameMode) {
this.gameMode = value;
}
private getRandomMap(): GameMapType {
const maps = Object.values(GameMapType);
const randIdx = Math.floor(Math.random() * maps.length);
@@ -300,6 +332,7 @@ export class SinglePlayerModal extends LitElement {
gameConfig: {
gameMap: this.selectedMap,
gameType: GameType.Singleplayer,
gameMode: this.gameMode,
difficulty: this.selectedDifficulty,
disableNPCs: this.disableNPCs,
disableNukes: this.disableNukes,
+4 -1
View File
@@ -9,6 +9,7 @@ import {
Player,
PlayerID,
PlayerType,
TeamName,
Tick,
UnitType,
} from "../core/game/Game";
@@ -124,8 +125,9 @@ export class SendSetTargetTroopRatioEvent implements GameEvent {
export class SendWinnerEvent implements GameEvent {
constructor(
public readonly winner: ClientID,
public readonly winner: ClientID | TeamName,
public readonly allPlayersStats: AllPlayersStats,
public readonly winnerType: "player" | "team",
) {}
}
export class SendHashEvent implements GameEvent {
@@ -492,6 +494,7 @@ export class Transport {
gameID: this.lobbyConfig.gameID,
winner: event.winner,
allPlayersStats: event.allPlayersStats,
winnerType: event.winnerType,
});
this.sendMsg(JSON.stringify(msg));
} else {
+1 -1
View File
@@ -304,7 +304,7 @@ export class EventsDisplay extends LitElement implements Layer {
onTargetPlayerEvent(event: TargetPlayerUpdate) {
const other = this.game.playerBySmallID(event.playerID) as PlayerView;
const myPlayer = this.game.playerByClientID(this.clientID) as PlayerView;
if (!myPlayer || !myPlayer.isAlliedWith(other)) return;
if (!myPlayer || !myPlayer.isFriendly(other)) return;
const target = this.game.playerBySmallID(event.targetID) as PlayerView;
+3 -3
View File
@@ -195,7 +195,7 @@ export class NameLayer implements Layer {
nameDiv.appendChild(flagImg);
}
nameDiv.classList.add("player-name");
nameDiv.style.color = this.theme.textColor(player.info());
nameDiv.style.color = this.theme.textColor(player);
nameDiv.style.fontFamily = this.theme.font();
nameDiv.style.whiteSpace = "nowrap";
nameDiv.style.textOverflow = "ellipsis";
@@ -213,7 +213,7 @@ export class NameLayer implements Layer {
troopsDiv.classList.add("player-troops");
troopsDiv.setAttribute("translate", "no");
troopsDiv.textContent = renderTroops(player.troops());
troopsDiv.style.color = this.theme.textColor(player.info());
troopsDiv.style.color = this.theme.textColor(player);
troopsDiv.style.fontFamily = this.theme.font();
troopsDiv.style.zIndex = "3";
troopsDiv.style.marginTop = "-5%";
@@ -242,7 +242,7 @@ export class NameLayer implements Layer {
// Calculate base size and scale
const baseSize = Math.max(1, Math.floor(render.player.nameLocation().size));
render.fontSize = Math.max(4, Math.floor(baseSize * 0.4));
render.fontColor = this.theme.textColor(render.player.info());
render.fontColor = this.theme.textColor(render.player);
// Screen space calculations
const size = this.transformHandler.scale * baseSize;
@@ -163,7 +163,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const isAlly = myPlayer?.isAlliedWith(player);
const isFriendly = myPlayer?.isFriendly(player);
let relationHtml = null;
const attackingTroops = player
.outgoingAttacks()
@@ -198,7 +198,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div class="p-2">
<div
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex ${isAlly
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex ${isFriendly
? "text-green-500"
: "text-white"}"
>
@@ -244,7 +244,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderUnitInfo(unit: UnitView) {
const isAlly =
(unit.owner() == this.myPlayer() ||
this.myPlayer()?.isAlliedWith(unit.owner())) ??
this.myPlayer()?.isFriendly(unit.owner())) ??
false;
return html`
+3 -3
View File
@@ -188,7 +188,7 @@ export class StructureLayer implements Layer {
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() == UnitType.Construction
? underConstructionColor
: this.theme.territoryColor(unit.owner().info()),
: this.theme.territoryColor(unit.owner()),
130,
);
}
@@ -234,7 +234,7 @@ export class StructureLayer implements Layer {
if (!unit.isActive()) return;
let borderColor = this.theme.borderColor(unit.owner().info());
let borderColor = this.theme.borderColor(unit.owner());
if (unitType == UnitType.SAMLauncher && unit.isSamCooldown()) {
borderColor = reloadingColor;
} else if (unit.type() == UnitType.Construction) {
@@ -257,7 +257,7 @@ export class StructureLayer implements Layer {
height: number,
unit: UnitView,
) {
let color = this.theme.borderColor(unit.owner().info());
let color = this.theme.borderColor(unit.owner());
if (unit.type() == UnitType.Construction) {
color = underConstructionColor;
}
+3 -3
View File
@@ -251,14 +251,14 @@ export class TerritoryLayer implements Layer {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.defendedBorderColor(owner.info()),
this.theme.defendedBorderColor(owner),
255,
);
} else {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.borderColor(owner.info()),
this.theme.borderColor(owner),
255,
);
}
@@ -266,7 +266,7 @@ export class TerritoryLayer implements Layer {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.territoryColor(owner.info()),
this.theme.territoryColor(owner),
150,
);
}
+1 -1
View File
@@ -136,7 +136,7 @@ export class UILayer implements Layer {
baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
// Get the unit's owner color for the box
const ownerColor = this.theme.territoryColor(unit.owner().info());
const ownerColor = this.theme.territoryColor(unit.owner());
// Create a brighter version of the owner color for the selection
const selectionColor = ownerColor.lighten(0.2);
+14 -14
View File
@@ -194,7 +194,7 @@ export class UnitLayer implements Layer {
if (this.myPlayer == unit.owner()) {
return Relationship.Self;
}
if (this.myPlayer.isAlliedWith(unit.owner())) {
if (this.myPlayer.isFriendly(unit.owner())) {
return Relationship.Ally;
}
return Relationship.Enemy;
@@ -248,7 +248,7 @@ export class UnitLayer implements Layer {
return;
}
let outerColor = this.theme.territoryColor(unit.owner().info());
let outerColor = this.theme.territoryColor(unit.owner());
if (unit.warshipTargetId()) {
const targetOwner = this.game
.units()
@@ -276,7 +276,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -290,7 +290,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
this.theme.territoryColor(unit.owner()),
255,
);
}
@@ -316,14 +316,14 @@ export class UnitLayer implements Layer {
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
this.paintCell(
this.game.x(unit.lastTile()),
this.game.y(unit.lastTile()),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -358,7 +358,7 @@ export class UnitLayer implements Layer {
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -408,7 +408,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -426,7 +426,7 @@ export class UnitLayer implements Layer {
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -453,7 +453,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
this.theme.territoryColor(unit.owner()),
255,
);
}
@@ -467,7 +467,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -498,7 +498,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
this.theme.territoryColor(unit.owner()),
150,
);
}
@@ -512,7 +512,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.borderColor(unit.owner().info()),
this.theme.borderColor(unit.owner()),
255,
);
}
@@ -526,7 +526,7 @@ export class UnitLayer implements Layer {
this.game.x(t),
this.game.y(t),
rel,
this.theme.territoryColor(unit.owner().info()),
this.theme.territoryColor(unit.owner()),
255,
);
}
+31 -11
View File
@@ -1,6 +1,6 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Player } from "../../../core/game/Game";
import { Player, TeamName } from "../../../core/game/Game";
import { ClientID } from "../../../core/Schemas";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
@@ -218,18 +218,38 @@ export class WinModal extends LitElement implements Layer {
this.show();
}
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView;
this.eventBus.emit(
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats),
);
if (winner == this.game.myPlayer()) {
this._title = "You Won!";
this.won = true;
if (wu.winnerType === "team") {
this.eventBus.emit(
new SendWinnerEvent(
wu.winner as TeamName,
wu.allPlayersStats,
"team",
),
);
if (wu.winner == this.game.myPlayer()?.teamName()) {
this._title = "Your team won!";
this.won = true;
} else {
this._title = `${wu.winner} team has won!`;
this.won = false;
}
this.show();
} else {
this._title = `${winner.name()} has won!`;
this.won = false;
const winner = this.game.playerBySmallID(
wu.winner as number,
) as PlayerView;
this.eventBus.emit(
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
);
if (winner == this.game.myPlayer()) {
this._title = "You Won!";
this.won = true;
} else {
this._title = `${winner.name()} has won!`;
this.won = false;
}
this.show();
}
this.show();
});
}