Skip to content
3 changes: 3 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@
"game_length": "Game length",
"pvp_immunity": "PVP immunity duration",
"starting_gold": "Starting Gold",
"starting_in": "Starting in {time}. Click to cancel",
"host_cheats": "Host Cheats"
},
"public_lobby": {
Expand Down Expand Up @@ -686,6 +687,8 @@
"nations_disabled": "Disabled",
"player_immunity_duration": "PVP immunity duration (minutes)",
"max_timer": "Game length (minutes)",
"start_delay": "Start delay (Seconds)",
"start_delay_placeholder": "3",
"mins_placeholder": "Mins",
"instant_build": "Instant build",
"infinite_gold": "Infinite gold",
Expand Down
97 changes: 87 additions & 10 deletions src/client/HostLobbyModal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ClientEnv } from "src/client/ClientEnv";
import { translateText } from "../client/Utils";
import {
calculateServerTimeOffset,
getSecondsUntilServerTimestamp,
renderDuration,
translateText,
} from "../client/Utils";
import { EventBus } from "../core/EventBus";
import {
Difficulty,
Expand Down Expand Up @@ -65,6 +70,8 @@ export class HostLobbyModal extends BaseModal {
@state() private donateTroops: boolean = false;
@state() private maxTimer: boolean = false;
@state() private maxTimerValue: number | undefined = undefined;
@state() private startDelay: boolean = true;
@state() private startDelayValue: number | undefined = 3;
@state() private instantBuild: boolean = false;
@state() private randomSpawn: boolean = false;
@state() private compactMap: boolean = false;
Expand All @@ -87,6 +94,8 @@ export class HostLobbyModal extends BaseModal {
@state() private hostCheatStartingGold: boolean = false;
@state() private hostCheatStartingGoldValue: number | undefined = undefined;
@state() private lobbyCreatorClientID: string = "";
@state() private lobbyStartAt: number | null = null;
@state() private serverTimeOffset: number = 0;

@property({ attribute: false }) eventBus: EventBus | null = null;
// Timers for debouncing slider changes
Expand All @@ -102,6 +111,10 @@ export class HostLobbyModal extends BaseModal {
if (!this.lobbyId || lobby.gameID !== this.lobbyId) {
return;
}
if ("serverTime" in lobby && typeof lobby.serverTime === "number") {
this.serverTimeOffset = calculateServerTimeOffset(lobby.serverTime);
}
this.lobbyStartAt = lobby.startsAt ?? null;
this.lobbyCreatorClientID = lobby.lobbyCreatorClientID ?? "";
if (lobby.clients) {
this.clients = lobby.clients;
Expand Down Expand Up @@ -180,6 +193,22 @@ export class HostLobbyModal extends BaseModal {
}

protected renderBody() {
const secondsRemaining =
this.lobbyStartAt !== null
? getSecondsUntilServerTimestamp(
this.lobbyStartAt,
this.serverTimeOffset,
)
: null;
const statusLabel =
secondsRemaining === null
? this.clients.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")
: translateText("private_lobby.starting_in", {
time: renderDuration(secondsRemaining),
});

const inputCards = [
html`<toggle-input-card
.labelKey=${"host_modal.max_timer"}
Expand All @@ -195,6 +224,22 @@ export class HostLobbyModal extends BaseModal {
.onInput=${this.handleMaxTimerValueChanges}
.onKeyDown=${this.handleMaxTimerValueKeyDown}
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"host_modal.start_delay"}
.checked=${this.startDelay}
.inputId=${"start-delay-value"}
.inputMin=${0}
.inputMax=${600}
.inputStep=${"any"}
.inputValue=${this.startDelayValue}
.inputAriaLabel=${translateText("host_modal.start_delay")}
.inputPlaceholder=${translateText("host_modal.start_delay_placeholder")}
.defaultInputValue=${3}
.minValidOnEnable=${1}
.onToggle=${this.handleStartDelayToggle}
.onChange=${this.handleStartDelayValueChanges}
.onKeyDown=${this.handleStartDelayValueKeyDown}
></toggle-input-card>`,
html`<toggle-input-card
.labelKey=${"host_modal.player_immunity_duration"}
.checked=${this.spawnImmunity}
Expand Down Expand Up @@ -396,8 +441,9 @@ export class HostLobbyModal extends BaseModal {
@bots-changed=${this.handleBotsChange}
@nations-changed=${this.handleNationsChange}
@option-toggle-changed=${this.handleConfigOptionToggleChanged}
@host-cheat-toggle-changed=${this
.handleConfigHostCheatToggleChanged}
@host-cheat-toggle-changed=${
this.handleConfigHostCheatToggleChanged
}
@unit-toggle-changed=${this.handleConfigUnitToggleChanged}
></game-config-settings>

Expand All @@ -419,14 +465,13 @@ export class HostLobbyModal extends BaseModal {
variant="primary"
width="block"
size="lg"
.title=${this.clients.length === 1
? translateText("host_modal.waiting")
: translateText("host_modal.start")}
?disable=${this.clients.length < 2}
@click=${this.startGame}
.title=${statusLabel}
?disable=${this.lobbyStartAt === null && this.clients.length < 2}
@click=${this.toggleGameStartTimer}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
></o-button>
</div>
</div>
</div>
`;
}

Expand Down Expand Up @@ -529,6 +574,8 @@ export class HostLobbyModal extends BaseModal {
this.donateTroops = false;
this.maxTimer = false;
this.maxTimerValue = undefined;
this.startDelay = true;
this.startDelayValue = 3;
this.instantBuild = false;
this.randomSpawn = false;
this.compactMap = false;
Expand Down Expand Up @@ -712,6 +759,15 @@ export class HostLobbyModal extends BaseModal {
this.putGameConfig();
};

private handleStartDelayToggle = (
checked: boolean,
value: number | string | undefined,
) => {
this.startDelay = checked;
this.startDelayValue = toOptionalNumber(value);
this.putGameConfig();
};

private handleSpawnImmunityToggle = (
checked: boolean,
value: number | string | undefined,
Expand Down Expand Up @@ -900,6 +956,26 @@ export class HostLobbyModal extends BaseModal {
this.putGameConfig();
};

private handleStartDelayValueKeyDown = (e: KeyboardEvent) => {
preventDisallowedKeys(e, ["-", "+", "e", "E"]);
};

private handleStartDelayValueChanges = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = parseBoundedIntegerFromInput(input, {
min: 0,
max: 600,
});

if (value === undefined) {
this.startDelayValue = undefined;
input.value = "";
} else {
this.startDelayValue = value;
}
this.putGameConfig();
};

private handleNationsChange = (e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
Expand Down Expand Up @@ -967,6 +1043,7 @@ export class HostLobbyModal extends BaseModal {
this.defaultNationCount,
),
maxTimerValue: this.maxTimer === true ? this.maxTimerValue : null,
startDelay: this.startDelay === true ? this.startDelayValue : null,
goldMultiplier:
this.goldMultiplier === true ? this.goldMultiplierValue : null,
startingGold:
Expand Down Expand Up @@ -998,7 +1075,7 @@ export class HostLobbyModal extends BaseModal {
);
}

private async startGame() {
private async toggleGameStartTimer() {
await this.putGameConfig();
console.log(
`Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
Expand All @@ -1008,7 +1085,7 @@ export class HostLobbyModal extends BaseModal {
this.leaveLobbyOnClose = false;

this.dispatchEvent(
new CustomEvent("start-game", {
new CustomEvent("toggle_game_start_timer", {
bubbles: true,
composed: true,
}),
Expand Down
81 changes: 33 additions & 48 deletions src/client/JoinLobbyModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export class JoinLobbyModal extends BaseModal {
: null;
const statusLabel =
secondsRemaining === null
? translateText("public_lobby.waiting_for_players")
? this.isPrivateLobby()
? translateText("private_lobby.joined_waiting")
: translateText("public_lobby.waiting_for_players")
: secondsRemaining > 0
? translateText("public_lobby.starting_in", {
time: renderDuration(secondsRemaining),
Expand Down Expand Up @@ -162,56 +164,39 @@ export class JoinLobbyModal extends BaseModal {
`}
</div>

${this.isPrivateLobby()
? html`
<div
class="p-6 lg:p-6 border-t border-white/10 bg-black/20 shrink-0"
>
<button
class="w-full py-4 text-sm font-bold text-white uppercase tracking-widest bg-malibu-blue hover:bg-aquarius disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all shadow-lg shadow-sky-900/20 hover:shadow-sky-900/40 hover:-translate-y-0.5 active:translate-y-0 disabled:transform-none"
disabled
${html`
<div class="p-6 lg:p-6 border-t border-white/10 bg-black/20 shrink-0">
<div
class="w-full px-4 py-3 rounded-xl border border-white/10 bg-white/5 flex items-center justify-between gap-3"
>
<div class="flex flex-col">
<span
class="text-[10px] font-bold uppercase tracking-widest text-white/40"
>${translateText("public_lobby.status")}</span
>
${translateText("private_lobby.joined_waiting")}
</button>
<span class="text-sm font-bold text-white">${statusLabel}</span>
</div>
`
: html`
<div
class="p-6 lg:p-6 border-t border-white/10 bg-black/20 shrink-0"
>
<div
class="w-full px-4 py-3 rounded-xl border border-white/10 bg-white/5 flex items-center justify-between gap-3"
>
<div class="flex flex-col">
<span
class="text-[10px] font-bold uppercase tracking-widest text-white/40"
>${translateText("public_lobby.status")}</span
>
<span class="text-sm font-bold text-white"
>${statusLabel}</span
${maxPlayers > 0
? html`
<div
class="flex items-center gap-2 text-white/80 text-xs font-bold uppercase tracking-widest"
>
</div>
${maxPlayers > 0
? html`
<div
class="flex items-center gap-2 text-white/80 text-xs font-bold uppercase tracking-widest"
>
<span>${playerCount}/${maxPlayers}</span>
<svg
class="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.972 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
></path>
</svg>
</div>
`
: html``}
</div>
</div>
`}
<span>${playerCount}/${maxPlayers}</span>
<svg
class="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.972 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
></path>
</svg>
</div>
`
: html``}
</div>
</div>
`}
</div>
`;
}
Expand Down
13 changes: 8 additions & 5 deletions src/client/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
import { TokenLoginModal } from "./TokenLoginModal";
import {
SendKickPlayerIntentEvent,
SendStartGameEvent,
SendToggleGameStartTimer,
SendUpdateGameConfigIntentEvent,
} from "./Transport";
import { UserSettingModal } from "./UserSettingModal";
Expand Down Expand Up @@ -219,7 +219,7 @@ declare global {
interface DocumentEventMap {
"join-lobby": CustomEvent<JoinLobbyEvent>;
"kick-player": CustomEvent;
"start-game": CustomEvent;
toggle_game_start_timer: CustomEvent;
"join-changed": CustomEvent;
"open-matchmaking": CustomEvent<undefined>;
userMeResponse: CustomEvent<UserMeResponse | false>;
Expand Down Expand Up @@ -376,7 +376,10 @@ class Client {
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener("kick-player", this.handleKickPlayer.bind(this));
document.addEventListener("start-game", this.handleStartGame.bind(this));
document.addEventListener(
"toggle_game_start_timer",
this.handleToggleGameStartTimer.bind(this),
);
document.addEventListener(
"update-game-config",
this.handleUpdateGameConfig.bind(this),
Expand Down Expand Up @@ -1008,9 +1011,9 @@ class Client {
}
}

private handleStartGame() {
private handleToggleGameStartTimer() {
if (this.eventBus) {
this.eventBus.emit(new SendStartGameEvent());
this.eventBus.emit(new SendToggleGameStartTimer());
}
}

Expand Down
12 changes: 8 additions & 4 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ export class SendUpdateGameConfigIntentEvent implements GameEvent {
constructor(public readonly config: Partial<GameConfig>) {}
}

export class SendStartGameEvent implements GameEvent {}
export class SendToggleGameStartTimer implements GameEvent {
constructor() {}
}

export class Transport {
private socket: WebSocket | null = null;
Expand Down Expand Up @@ -266,7 +268,9 @@ export class Transport {
this.onSendUpdateGameConfigIntent(e),
);

this.eventBus.on(SendStartGameEvent, () => this.onSendStartGame());
this.eventBus.on(SendToggleGameStartTimer, (e) =>
this.onSendToggleGameStartTimer(e),
);
}

private startPing() {
Expand Down Expand Up @@ -647,8 +651,8 @@ export class Transport {
});
}

private onSendStartGame() {
this.sendIntent({ type: "start_game" });
private onSendToggleGameStartTimer(event: SendToggleGameStartTimer) {
this.sendIntent({ type: "toggle_game_start_timer" });
}

private sendIntent(intent: Intent) {
Expand Down
Loading
Loading