mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:10:42 +00:00
Delayed lobby start (#4184)
Resolves #4169 ## Description: Adds a delayed lobby start option. Utilizes the same system as for public lobbies. The default for the option is for lobbies to take 3 seconds to start, however this can easily be changed. The current setting is controlled through an enable-disable slider, however there are multiple other options for how to control this. For example we could do a slider, an input field, a dropdown etc. And i dont necessarily know if the currently implemented option is the best. Furhtermore im not sure if i have used the language file completely correctly. There is now a duplicate field for both private and public lobby. However there is not category shared between the two. So i decided to reuse the field from public for private games, as this simplified the code a bit. **Host video** https://github.com/user-attachments/assets/6f3db6e4-7323-4fad-8544-efb8cef4d969 **Non-host video** https://github.com/user-attachments/assets/ee02a072-1f42-4dde-a5d9-120fda862eb7 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: FrederikJA
This commit is contained in:
@@ -704,8 +704,11 @@
|
||||
"random_spawn": "Random spawn",
|
||||
"remove_player": "Remove {username}",
|
||||
"start": "Start Game",
|
||||
"start_delay": "Start delay (Seconds)",
|
||||
"start_delay_placeholder": "3",
|
||||
"starting_gold": "Starting Gold (Millions)",
|
||||
"starting_gold_placeholder": "5",
|
||||
"starting_in": "Starting in {time}. Click to cancel",
|
||||
"team_count": "Number of Teams",
|
||||
"teams_Duos": "Duos (teams of 2)",
|
||||
"teams_Humans Vs Nations": "Humans vs Nations",
|
||||
|
||||
@@ -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,
|
||||
@@ -25,6 +30,7 @@ import "./components/baseComponents/Modal";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import { CopyButton } from "./components/CopyButton";
|
||||
import "./components/GameConfigSettings";
|
||||
import "./components/InputCard";
|
||||
import "./components/LobbyPlayerView";
|
||||
import "./components/ToggleInputCard";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
@@ -65,6 +71,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
@state() private donateTroops: boolean = false;
|
||||
@state() private maxTimer: boolean = false;
|
||||
@state() private maxTimerValue: number | undefined = undefined;
|
||||
@state() private startDelayValue: number | undefined = 3;
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private randomSpawn: boolean = false;
|
||||
@state() private compactMap: boolean = false;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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("host_modal.starting_in", {
|
||||
time: renderDuration(secondsRemaining),
|
||||
});
|
||||
|
||||
const inputCards = [
|
||||
html`<toggle-input-card
|
||||
.labelKey=${"host_modal.max_timer"}
|
||||
@@ -195,6 +224,19 @@ export class HostLobbyModal extends BaseModal {
|
||||
.onInput=${this.handleMaxTimerValueChanges}
|
||||
.onKeyDown=${this.handleMaxTimerValueKeyDown}
|
||||
></toggle-input-card>`,
|
||||
html`<input-card
|
||||
.labelKey=${"host_modal.start_delay"}
|
||||
.inputId=${"start-delay-value"}
|
||||
.inputMin=${0}
|
||||
.inputMax=${600}
|
||||
.inputStep=${"1"}
|
||||
.inputValue=${this.startDelayValue}
|
||||
.inputAriaLabel=${translateText("host_modal.start_delay")}
|
||||
.inputPlaceholder=${translateText("host_modal.start_delay_placeholder")}
|
||||
.defaultInputValue=${3}
|
||||
.onChange=${this.handleStartDelayValueChanges}
|
||||
.onKeyDown=${this.handleStartDelayValueKeyDown}
|
||||
></input-card>`,
|
||||
html`<toggle-input-card
|
||||
.labelKey=${"host_modal.player_immunity_duration"}
|
||||
.checked=${this.spawnImmunity}
|
||||
@@ -419,11 +461,9 @@ 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}
|
||||
></o-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -529,6 +569,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
this.donateTroops = false;
|
||||
this.maxTimer = false;
|
||||
this.maxTimerValue = undefined;
|
||||
this.startDelayValue = 3;
|
||||
this.instantBuild = false;
|
||||
this.randomSpawn = false;
|
||||
this.compactMap = false;
|
||||
@@ -900,6 +941,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;
|
||||
@@ -967,6 +1028,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
this.defaultNationCount,
|
||||
),
|
||||
maxTimerValue: this.maxTimer === true ? this.maxTimerValue : null,
|
||||
startDelay: this.startDelayValue,
|
||||
goldMultiplier:
|
||||
this.goldMultiplier === true ? this.goldMultiplierValue : null,
|
||||
startingGold:
|
||||
@@ -998,7 +1060,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)" : ""}`,
|
||||
@@ -1008,7 +1070,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,
|
||||
}),
|
||||
|
||||
@@ -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),
|
||||
@@ -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
|
||||
${maxPlayers > 0
|
||||
? html`
|
||||
<div
|
||||
class="flex items-center gap-2 text-white/80 text-xs font-bold uppercase tracking-widest"
|
||||
>
|
||||
<span class="text-sm font-bold text-white"
|
||||
>${statusLabel}</span
|
||||
>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
+8
-5
@@ -54,7 +54,7 @@ import { TerritoryPatternsModal } from "./TerritoryPatternsModal";
|
||||
import { TokenLoginModal } from "./TokenLoginModal";
|
||||
import {
|
||||
SendKickPlayerIntentEvent,
|
||||
SendStartGameEvent,
|
||||
SendToggleGameStartTimer,
|
||||
SendUpdateGameConfigIntentEvent,
|
||||
} from "./Transport";
|
||||
import { UserSettingModal } from "./UserSettingModal";
|
||||
@@ -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>;
|
||||
@@ -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),
|
||||
@@ -1008,9 +1011,9 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
private handleStartGame() {
|
||||
private handleToggleGameStartTimer() {
|
||||
if (this.eventBus) {
|
||||
this.eventBus.emit(new SendStartGameEvent());
|
||||
this.eventBus.emit(new SendToggleGameStartTimer());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../Utils";
|
||||
import { CARD_LABEL_CLASS, cardClass, INPUT_CLASS } from "./InputCardStyles";
|
||||
|
||||
@customElement("input-card")
|
||||
export class InputCard extends LitElement {
|
||||
@property({ attribute: false }) labelKey = "";
|
||||
@property({ attribute: false }) inputId?: string;
|
||||
@property({ attribute: false }) inputType = "number";
|
||||
@property({ attribute: false }) inputMin?: number | string;
|
||||
@property({ attribute: false }) inputMax?: number | string;
|
||||
@property({ attribute: false }) inputStep?: number | string;
|
||||
@property({ attribute: false }) inputValue?: number | string;
|
||||
@property({ attribute: false }) inputAriaLabel?: string;
|
||||
@property({ attribute: false }) inputPlaceholder?: string;
|
||||
@property({ attribute: false }) onInput?: (e: Event) => void;
|
||||
@property({ attribute: false }) onChange?: (e: Event) => void;
|
||||
@property({ attribute: false }) onKeyDown?: (e: KeyboardEvent) => void;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="${cardClass(true)}">
|
||||
<div
|
||||
class="w-full h-full p-3 flex flex-col items-center justify-between gap-2"
|
||||
>
|
||||
<div class="h-[30px] my-1"></div>
|
||||
|
||||
<span class="${CARD_LABEL_CLASS} text-center text-white">
|
||||
${translateText(this.labelKey)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute left-3 right-3 top-1/2 -translate-y-1/2 z-10">
|
||||
<input
|
||||
type=${this.inputType}
|
||||
id=${this.inputId ?? nothing}
|
||||
min=${this.inputMin ?? nothing}
|
||||
max=${this.inputMax ?? nothing}
|
||||
step=${this.inputStep ?? nothing}
|
||||
.value=${String(this.inputValue ?? "")}
|
||||
class=${INPUT_CLASS}
|
||||
aria-label=${this.inputAriaLabel ?? nothing}
|
||||
placeholder=${this.inputPlaceholder ?? nothing}
|
||||
@input=${this.onInput}
|
||||
@change=${this.onChange}
|
||||
@keydown=${this.onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const ACTIVE_CARD =
|
||||
"bg-malibu-blue/20 border-malibu-blue/50 shadow-[var(--shadow-malibu-blue)]";
|
||||
export const INACTIVE_CARD =
|
||||
"bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20";
|
||||
export const INPUT_CLASS =
|
||||
"w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-malibu-blue p-1 my-1";
|
||||
export const CARD_LABEL_CLASS =
|
||||
"text-xs uppercase font-bold tracking-wider leading-tight break-words hyphens-auto";
|
||||
|
||||
export function cardClass(active: boolean, extra = ""): string {
|
||||
return `w-full h-full rounded-xl border cursor-pointer transition-all duration-200 active:scale-95 relative overflow-hidden ${extra} ${active ? ACTIVE_CARD : INACTIVE_CARD}`;
|
||||
}
|
||||
@@ -1,19 +1,7 @@
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
const ACTIVE_CARD =
|
||||
"bg-malibu-blue/20 border-malibu-blue/50 shadow-[var(--shadow-malibu-blue)]";
|
||||
const INACTIVE_CARD =
|
||||
"bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20";
|
||||
const INPUT_CLASS =
|
||||
"w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-malibu-blue p-1 my-1";
|
||||
const CARD_LABEL_CLASS =
|
||||
"text-xs uppercase font-bold tracking-wider leading-tight break-words hyphens-auto";
|
||||
|
||||
function cardClass(active: boolean, extra = ""): string {
|
||||
return `w-full h-full rounded-xl border cursor-pointer transition-all duration-200 active:scale-95 ${extra} ${active ? ACTIVE_CARD : INACTIVE_CARD}`;
|
||||
}
|
||||
import { CARD_LABEL_CLASS, INPUT_CLASS, cardClass } from "./InputCardStyles";
|
||||
|
||||
@customElement("toggle-input-card")
|
||||
export class ToggleInputCard extends LitElement {
|
||||
@@ -103,7 +91,7 @@ export class ToggleInputCard extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="${cardClass(this.checked, "relative overflow-hidden")}">
|
||||
<div class="${cardClass(this.checked)}">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed=${this.checked}
|
||||
|
||||
+8
-5
@@ -51,7 +51,7 @@ export type Intent =
|
||||
| KickPlayerIntent
|
||||
| TogglePauseIntent
|
||||
| UpdateGameConfigIntent
|
||||
| StartGameIntent;
|
||||
| ToggleGameStartTimer;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
|
||||
@@ -85,7 +85,9 @@ export type TogglePauseIntent = z.infer<typeof TogglePauseIntentSchema>;
|
||||
export type UpdateGameConfigIntent = z.infer<
|
||||
typeof UpdateGameConfigIntentSchema
|
||||
>;
|
||||
export type StartGameIntent = z.infer<typeof StartGameIntentSchema>;
|
||||
export type ToggleGameStartTimer = z.infer<
|
||||
typeof ToggleGameStartTimerIntentSchema
|
||||
>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
@@ -281,6 +283,7 @@ export const GameConfigSchema = z.object({
|
||||
randomSpawn: z.boolean(),
|
||||
maxPlayers: z.number().optional(),
|
||||
maxTimerValue: z.number().int().min(1).max(120).nullable().optional(), // In minutes
|
||||
startDelay: z.number().int().min(0).max(600).nullable().optional(), // In seconds
|
||||
spawnImmunityDuration: z.number().int().min(0).nullable().optional(), // In ticks
|
||||
disabledUnits: z.enum(UnitType).array().optional(),
|
||||
playerTeams: TeamCountConfigSchema.optional(),
|
||||
@@ -481,8 +484,8 @@ export const UpdateGameConfigIntentSchema = z.object({
|
||||
config: GameConfigSchema.partial(),
|
||||
});
|
||||
|
||||
export const StartGameIntentSchema = z.object({
|
||||
type: z.literal("start_game"),
|
||||
export const ToggleGameStartTimerIntentSchema = z.object({
|
||||
type: z.literal("toggle_game_start_timer"),
|
||||
});
|
||||
|
||||
const IntentSchema = z.discriminatedUnion("type", [
|
||||
@@ -510,7 +513,7 @@ const IntentSchema = z.discriminatedUnion("type", [
|
||||
KickPlayerIntentSchema,
|
||||
TogglePauseIntentSchema,
|
||||
UpdateGameConfigIntentSchema,
|
||||
StartGameIntentSchema,
|
||||
ToggleGameStartTimerIntentSchema,
|
||||
]);
|
||||
|
||||
// StampedIntent = Intent with server-stamped clientID (used in turns and execution)
|
||||
|
||||
+11
-21
@@ -149,6 +149,9 @@ export class GameServer {
|
||||
if (gameConfig.maxTimerValue !== undefined) {
|
||||
this.gameConfig.maxTimerValue = gameConfig.maxTimerValue ?? undefined;
|
||||
}
|
||||
if (gameConfig.startDelay !== undefined) {
|
||||
this.gameConfig.startDelay = gameConfig.startDelay ?? undefined;
|
||||
}
|
||||
if (gameConfig.instantBuild !== undefined) {
|
||||
this.gameConfig.instantBuild = gameConfig.instantBuild;
|
||||
}
|
||||
@@ -488,7 +491,7 @@ export class GameServer {
|
||||
this.updateGameConfig(stampedIntent.config);
|
||||
return;
|
||||
}
|
||||
case "start_game": {
|
||||
case "toggle_game_start_timer": {
|
||||
if (client.clientID !== this.lobbyCreatorID) {
|
||||
this.log.warn(`Only lobby creator can start game`, {
|
||||
clientID: client.clientID,
|
||||
@@ -514,7 +517,13 @@ export class GameServer {
|
||||
creatorID: client.clientID,
|
||||
gameID: this.id,
|
||||
});
|
||||
this.start();
|
||||
if (this.startsAt) {
|
||||
this.startsAt = undefined;
|
||||
} else {
|
||||
this.setStartsAt(
|
||||
Date.now() + (this.gameConfig.startDelay ?? 0) * 1000,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "toggle_pause": {
|
||||
@@ -931,25 +940,6 @@ export class GameServer {
|
||||
const noRecentPings = now > this.lastPingUpdate + 20 * 1000;
|
||||
const noActive = this.activeClients.length === 0;
|
||||
|
||||
if (this.gameConfig.gameType !== GameType.Public) {
|
||||
if (this._hasStarted) {
|
||||
if (noActive && noRecentPings) {
|
||||
this.log.info("private game complete", {
|
||||
gameID: this.id,
|
||||
});
|
||||
return GamePhase.Finished;
|
||||
} else {
|
||||
return GamePhase.Active;
|
||||
}
|
||||
} else if (this._hasEnded) {
|
||||
return GamePhase.Finished;
|
||||
} else {
|
||||
return GamePhase.Lobby;
|
||||
}
|
||||
}
|
||||
|
||||
// Public Games
|
||||
|
||||
const lessThanLifetime = this.startsAt ? Date.now() < this.startsAt : true;
|
||||
if (
|
||||
lessThanLifetime &&
|
||||
|
||||
Reference in New Issue
Block a user