Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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",
"spawn_selected_automatically": "heads_up_message.spawn_selected_automatically"
}
}
5 changes: 4 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 @@ -658,7 +660,8 @@
"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",
"spawn_selected_automatically": "Spawn is selected automatically. Prepare for the battle"
},
"territory_patterns": {
"title": "Skins",
Expand Down
3 changes: 2 additions & 1 deletion src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,8 @@ export class ClientGameRunner {
if (
this.gameView.isLand(tile) &&
!this.gameView.hasOwner(tile) &&
this.gameView.inSpawnPhase()
this.gameView.inSpawnPhase() &&
!this.gameView.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.isRandomSpawn()
? translateText("heads_up_message.spawn_selected_automatically")
: 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().randomSpawn()) {
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;
randomSpawn(): 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 @@ -337,6 +337,9 @@ export class DefaultConfig implements Config {
instantBuild(): boolean {
return this._gameConfig.instantBuild;
}
randomSpawn(): 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 @@ -20,6 +20,7 @@ import { FakeHumanExecution } from "./FakeHumanExecution";
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NoOpExecution } from "./NoOpExecution";
import { PlayerSpawner } from "./PlayerSpawner";
import { QuickChatExecution } from "./QuickChatExecution";
import { RetreatExecution } from "./RetreatExecution";
import { SpawnExecution } from "./SpawnExecution";
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
80 changes: 80 additions & 0 deletions src/core/execution/PlayerSpawner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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[] = [];

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);
}

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

while (tries < 100) {
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) < 50) {
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 nornally not happen,
// but if it does maybe need to add some splash screen or add additional logic
console.warn(`cannot spawn ${player.id}`);
continue;
}

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

return this.players;
}
}
3 changes: 3 additions & 0 deletions src/core/game/GameView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,9 @@ export class GameView implements GameMap {
inSpawnPhase(): boolean {
return this.ticks() <= this._config.numSpawnPhaseTurns();
}
isRandomSpawn(): boolean {
return this._config.randomSpawn();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you remove this (and on Game), and leave it in the config. so GameView.config().isRandomSpawn()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed and refactored to use from config.

config(): Config {
return this._config;
}
Expand Down
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
3 changes: 3 additions & 0 deletions src/server/GameServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export class GameServer {
if (gameConfig.instantBuild !== undefined) {
this.gameConfig.instantBuild = gameConfig.instantBuild;
}
if (gameConfig.randomSpawn !== undefined) {
this.gameConfig.randomSpawn = gameConfig.randomSpawn;
}
if (gameConfig.gameMode !== undefined) {
this.gameConfig.gameMode = gameConfig.gameMode;
}
Expand Down
1 change: 1 addition & 0 deletions src/server/MapPlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class MapPlaylist {
infiniteTroops: false,
maxTimerValue: undefined,
instantBuild: false,
randomSpawn: false,
disableNPCs: mode === GameMode.Team && playerTeams !== HumansVsNations,
gameMode: mode,
playerTeams,
Expand Down
1 change: 1 addition & 0 deletions tests/util/Setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export async function setup(
infiniteGold: false,
infiniteTroops: false,
instantBuild: false,
randomSpawn: false,
..._gameConfig,
};
const config = new ConfigClass(
Expand Down
Loading