Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion resources/lang/debug.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"disable_nations": "single_modal.disable_nations",
"instant_build": "single_modal.instant_build",
"infinite_gold": "single_modal.infinite_gold",
"random_spawn": "single_modal.random_spawn",
"infinite_troops": "single_modal.infinite_troops",
"disable_nukes": "single_modal.disable_nukes",
"start": "single_modal.start"
Expand Down Expand Up @@ -146,6 +147,7 @@
"bots_disabled": "host_modal.bots_disabled",
"disable_nations": "host_modal.disable_nations",
"instant_build": "host_modal.instant_build",
"random_spawn": "host_modal.random_spawn",
"infinite_gold": "host_modal.infinite_gold",
"infinite_troops": "host_modal.infinite_troops",
"disable_nukes": "host_modal.disable_nukes",
Expand All @@ -166,6 +168,7 @@
"Impossible": "difficulty.Impossible"
},
"heads_up_message": {
"choose_spawn": "heads_up_message.choose_spawn"
"choose_spawn": "heads_up_message.choose_spawn",
"random_spawn": "heads_up_message.random_spawn"
}
}
9 changes: 8 additions & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
},
"single_modal": {
"title": "Single Player",
"random_spawn": "Random spawn",
"allow_alliances": "Allow alliances",
"options_title": "Options",
"bots": "Bots: ",
Expand Down Expand Up @@ -262,6 +263,7 @@
"player": "Player",
"players": "Players",
"waiting": "Waiting for players...",
"random_spawn": "Random spawn",
"start": "Start Game",
"host_badge": "Host"
},
Expand Down Expand Up @@ -661,10 +663,15 @@
"copy_clipboard": "Copy to clipboard",
"copied": "Copied!",
"failed_copy": "Failed to copy",
"spawn_failed": {
"title": "Spawn failed",
"description": "Automatic spawn selection failed. You can't play this game."
},
"desync_notice": "You are desynced from other players. What you see might differ from other players."
},
"heads_up_message": {
"choose_spawn": "Choose a starting location"
"choose_spawn": "Choose a starting location",
"random_spawn": "Random spawn is enabled. Selecting starting location for you..."
},
"territory_patterns": {
"title": "Skins",
Expand Down
43 changes: 42 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from "./Transport";
import { createCanvas } from "./Utils";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { GoToPlayerEvent } from "./graphics/layers/Leaderboard";
import SoundManager from "./sound/SoundManager";

export interface LobbyConfig {
Expand Down Expand Up @@ -202,6 +203,8 @@ export class ClientGameRunner {

private lastMessageTime: number = 0;
private connectionCheckInterval: NodeJS.Timeout | null = null;
private goToPlayerTimeout: NodeJS.Timeout | null = null;

private lastTickReceiveTime: number = 0;
private currentTickDelay: number | undefined = undefined;

Expand Down Expand Up @@ -325,6 +328,39 @@ export class ClientGameRunner {
if (message.type === "start") {
this.hasJoined = true;
console.log("starting game!");

if (this.gameView.config().isRandomSpawn()) {
const goToPlayer = () => {
const myPlayer = this.gameView.myPlayer();

if (this.gameView.inSpawnPhase() && !myPlayer?.hasSpawned()) {
this.goToPlayerTimeout = setTimeout(goToPlayer, 1000);
return;
}

if (!myPlayer) {
return;
}

if (!this.gameView.inSpawnPhase() && !myPlayer.hasSpawned()) {
showErrorModal(
"spawn_failed",
translateText("error_modal.spawn_failed.description"),
this.lobby.gameID,
this.lobby.clientID,
true,
false,
translateText("error_modal.spawn_failed.title"),
);
return;
}

this.eventBus.emit(new GoToPlayerEvent(myPlayer));
};

goToPlayer();
}

for (const turn of message.turns) {
if (turn.turnNumber < this.turnsSeen) {
continue;
Expand Down Expand Up @@ -402,6 +438,10 @@ export class ClientGameRunner {
clearInterval(this.connectionCheckInterval);
this.connectionCheckInterval = null;
}
if (this.goToPlayerTimeout) {
clearTimeout(this.goToPlayerTimeout);
this.goToPlayerTimeout = null;
}
}

private inputEvent(event: MouseUpEvent) {
Expand All @@ -420,7 +460,8 @@ export class ClientGameRunner {
if (
this.gameView.isLand(tile) &&
!this.gameView.hasOwner(tile) &&
this.gameView.inSpawnPhase()
this.gameView.inSpawnPhase() &&
!this.gameView.config().isRandomSpawn()
) {
this.eventBus.emit(new SendSpawnIntentEvent(tile));
return;
Expand Down
23 changes: 23 additions & 0 deletions src/client/HostLobbyModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class HostLobbyModal extends LitElement {
@state() private maxTimer: boolean = false;
@state() private maxTimerValue: number | undefined = undefined;
@state() private instantBuild: boolean = false;
@state() private randomSpawn: boolean = false;
@state() private compactMap: boolean = false;
@state() private lobbyId = "";
@state() private copySuccess = false;
Expand Down Expand Up @@ -390,6 +391,22 @@ export class HostLobbyModal extends LitElement {
</div>
</label>

<label
for="random-spawn"
class="option-card ${this.randomSpawn ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="random-spawn"
@change=${this.handleRandomSpawnChange}
.checked=${this.randomSpawn}
/>
<div class="option-card-title">
${translateText("host_modal.random_spawn")}
</div>
</label>

<label
for="donate-gold"
class="option-card ${this.donateGold ? "selected" : ""}"
Expand Down Expand Up @@ -668,6 +685,11 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}

private handleRandomSpawnChange(e: Event) {
this.randomSpawn = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
}

private handleInfiniteGoldChange(e: Event) {
this.infiniteGold = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
Expand Down Expand Up @@ -749,6 +771,7 @@ export class HostLobbyModal extends LitElement {
infiniteTroops: this.infiniteTroops,
donateTroops: this.donateTroops,
instantBuild: this.instantBuild,
randomSpawn: this.randomSpawn,
gameMode: this.gameMode,
disabledUnits: this.disabledUnits,
playerTeams: this.teamCount,
Expand Down
22 changes: 22 additions & 0 deletions src/client/SinglePlayerModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class SinglePlayerModal extends LitElement {
@state() private maxTimer: boolean = false;
@state() private maxTimerValue: number | undefined = undefined;
@state() private instantBuild: boolean = false;
@state() private randomSpawn: boolean = false;
@state() private useRandomMap: boolean = false;
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: TeamCountConfig = 2;
Expand Down Expand Up @@ -293,6 +294,22 @@ export class SinglePlayerModal extends LitElement {
</div>
</label>

<label
for="singleplayer-modal-random-spawn"
class="option-card ${this.randomSpawn ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="singleplayer-modal-random-spawn"
@change=${this.handleRandomSpawnChange}
.checked=${this.randomSpawn}
/>
<div class="option-card-title">
${translateText("single_modal.random_spawn")}
</div>
</label>

<label
for="singleplayer-modal-infinite-gold"
class="option-card ${this.infiniteGold ? "selected" : ""}"
Expand Down Expand Up @@ -440,6 +457,10 @@ export class SinglePlayerModal extends LitElement {
this.instantBuild = Boolean((e.target as HTMLInputElement).checked);
}

private handleRandomSpawnChange(e: Event) {
this.randomSpawn = Boolean((e.target as HTMLInputElement).checked);
}

private handleInfiniteGoldChange(e: Event) {
this.infiniteGold = Boolean((e.target as HTMLInputElement).checked);
}
Expand Down Expand Up @@ -563,6 +584,7 @@ export class SinglePlayerModal extends LitElement {
donateTroops: true,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
randomSpawn: this.randomSpawn,
disabledUnits: this.disabledUnits
.map((u) => Object.values(UnitType).find((ut) => ut === u))
.filter((ut): ut is UnitType => ut !== undefined),
Expand Down
4 changes: 3 additions & 1 deletion src/client/graphics/layers/HeadsUpMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class HeadsUpMessage extends LitElement implements Layer {
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
${translateText("heads_up_message.choose_spawn")}
${this.game.config().isRandomSpawn()
? translateText("heads_up_message.random_spawn")
: translateText("heads_up_message.choose_spawn")}
</div>
`;
}
Expand Down
3 changes: 3 additions & 0 deletions src/core/GameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export class GameRunner {
) {}

init() {
if (this.game.config().isRandomSpawn()) {
this.game.addExecution(...this.execManager.spawnPlayers());
}
if (this.game.config().bots() > 0) {
this.game.addExecution(
...this.execManager.spawnBots(this.game.config().numBots()),
Expand Down
1 change: 1 addition & 0 deletions src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const GameConfigSchema = z.object({
infiniteGold: z.boolean(),
infiniteTroops: z.boolean(),
instantBuild: z.boolean(),
randomSpawn: z.boolean(),
maxPlayers: z.number().optional(),
maxTimerValue: z.number().int().min(1).max(120).optional(),
disabledUnits: z.enum(UnitType).array().optional(),
Expand Down
1 change: 1 addition & 0 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface Config {
infiniteTroops(): boolean;
donateTroops(): boolean;
instantBuild(): boolean;
isRandomSpawn(): boolean;
numSpawnPhaseTurns(): number;
userSettings(): UserSettings;
playerTeams(): TeamCountConfig;
Expand Down
3 changes: 3 additions & 0 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ export class DefaultConfig implements Config {
instantBuild(): boolean {
return this._gameConfig.instantBuild;
}
isRandomSpawn(): boolean {
return this._gameConfig.randomSpawn;
}
infiniteGold(): boolean {
return this._gameConfig.infiniteGold;
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/execution/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { SpawnExecution } from "./SpawnExecution";
import { TargetPlayerExecution } from "./TargetPlayerExecution";
import { TransportShipExecution } from "./TransportShipExecution";
import { UpgradeStructureExecution } from "./UpgradeStructureExecution";
import { PlayerSpawner } from "./utils/PlayerSpawner";

export class Executor {
// private random = new PseudoRandom(999)
Expand Down Expand Up @@ -131,6 +132,10 @@ export class Executor {
return new BotSpawner(this.mg, this.gameID).spawnBots(numBots);
}

spawnPlayers(): Execution[] {
return new PlayerSpawner(this.mg, this.gameID).spawnPlayers();
}

fakeHumanExecutions(): Execution[] {
const execs: Execution[] = [];
for (const nation of this.mg.nations()) {
Expand Down
83 changes: 83 additions & 0 deletions src/core/execution/utils/PlayerSpawner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Game, 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 PlayerSpawner {
private random: PseudoRandom;
private players: SpawnExecution[] = [];
private static readonly MAX_SPAWN_TRIES = 10_000;
private static readonly MIN_SPAWN_DISTANCE = 30;

constructor(
private gm: Game,
gameID: GameID,
) {
this.random = new PseudoRandom(simpleHash(gameID));
}

private randTile(): TileRef {
const x = this.random.nextInt(0, this.gm.width());
const y = this.random.nextInt(0, this.gm.height());

return this.gm.ref(x, y);
}

private randomSpawnLand(): TileRef | null {
let tries = 0;

while (tries < PlayerSpawner.MAX_SPAWN_TRIES) {
tries++;

const tile = this.randTile();

if (
!this.gm.isLand(tile) ||
this.gm.hasOwner(tile) ||
this.gm.isBorder(tile)
) {
continue;
}

let tooCloseToOtherPlayer = false;
for (const spawn of this.players) {
if (
this.gm.manhattanDist(spawn.tile, tile) <
PlayerSpawner.MIN_SPAWN_DISTANCE
) {
tooCloseToOtherPlayer = true;
break;
}
}

if (tooCloseToOtherPlayer) {
continue;
}

return tile;
}

return null;
}

spawnPlayers(): SpawnExecution[] {
for (const player of this.gm.allPlayers()) {
if (player.type() !== PlayerType.Human) {
continue;
}

const spawnLand = this.randomSpawnLand();

if (spawnLand === null) {
// TODO: this should normally not happen, additional logic may be needed, if this occurs
continue;
}

this.players.push(new SpawnExecution(player.info(), spawnLand));
}

return this.players;
}
}
1 change: 1 addition & 0 deletions src/server/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class GameManager {
infiniteTroops: false,
maxTimerValue: undefined,
instantBuild: false,
randomSpawn: false,
gameMode: GameMode.FFA,
bots: 400,
disabledUnits: [],
Expand Down
Loading
Loading