- Political map country names were invisible because the label SVG had a zero-sized viewport, which some browsers never paint; size it to the world and guard it with a test. - Added a Devlog button showing the ten latest commits, generated into client/js/devlog_data.js by scripts/generate-devlog.js and refreshed by the pre-commit hook. - Worked through FIXME.md: the city button and a unit double-click now open the city, and the stat tooltip escapes the HUD so the unit panel cannot cover it. - The server creates its game at startup and clients only join it; the Create game screen and the configure_game path are gone, and testing is a --testing server flag. - A page on a loopback host signs in as adrien, claims France and gets the free, instant testing buttons, while the server grants free orders only to a loopback socket whose Origin is also loopback.
304 lines
9.7 KiB
JavaScript
304 lines
9.7 KiB
JavaScript
// Application bootstrap: the main menu and the join flow. A page served from
|
|
// the local machine signs in as the developer and claims France without showing
|
|
// the join form, and the server grants it the testing shortcuts.
|
|
|
|
import { Network } from "./net.js";
|
|
import { GameScreen } from "./game_screen.js";
|
|
import { bootLog } from "./loading.js";
|
|
import { audio } from "./audio.js";
|
|
import { renderCredits } from "./credits.js";
|
|
import { DEVLOG } from "./devlog_data.js";
|
|
import { renderDevlog } from "./devlog.js";
|
|
import { chooseMenuBackground } from "./menu_background.js";
|
|
import { CIVILISATIONS } from "../../shared/data.js";
|
|
|
|
const network = new Network();
|
|
// A page on the local machine signs in automatically. The server independently
|
|
// checks the connection's origin before honouring any free order.
|
|
function isLoopbackHost(hostname) {
|
|
const host = String(hostname || "").replace(/^\[|\]$/g, "");
|
|
return host === "localhost" || host === "::1" || host.startsWith("127.");
|
|
}
|
|
const isLocalhost = isLoopbackHost(window.location.hostname);
|
|
const LOCAL_NAME = "adrien";
|
|
const LOCAL_PASSWORD = "admin";
|
|
const LOCAL_CIV = "france";
|
|
const config = {
|
|
playerName: "",
|
|
password: "",
|
|
// Shows the free, instant testing buttons in the city and tile panels.
|
|
testing: isLocalhost,
|
|
};
|
|
|
|
let gameScreen = null;
|
|
let joining = false;
|
|
let games = [];
|
|
let selectedGame = -1;
|
|
|
|
function showScreen(id) {
|
|
$(".screen").removeClass("active");
|
|
$(`#${id}`).addClass("active");
|
|
}
|
|
|
|
function status(id, message) {
|
|
$(`#${id}`).text(message);
|
|
}
|
|
|
|
// ---------------------------------------------------------- appearance ----
|
|
|
|
// Two pieces of key art ship; pick one for this launch. It backs the main menu
|
|
// and stays behind the boot log while a world loads.
|
|
chooseMenuBackground();
|
|
|
|
// -------------------------------------------------------------- audio -----
|
|
|
|
// The sound toggle appears both on the main menu and in the in-game dock, so
|
|
// both buttons are relabelled whenever the setting changes.
|
|
function refreshAudioButtons() {
|
|
$("#menu-audio").text(audio.enabled ? "Sound: On" : "Sound: Off");
|
|
$("#audio-button").text(audio.enabled ? "Sound on" : "Sound off");
|
|
}
|
|
audio.onChange = refreshAudioButtons;
|
|
refreshAudioButtons();
|
|
|
|
// Every button in the game clicks; the manager ignores the call when muted.
|
|
$(document).on("click", "button", () => audio.play("click"));
|
|
$("#menu-audio, #audio-button").on("click", () => audio.toggle());
|
|
|
|
// ------------------------------------------------------------ credits -----
|
|
|
|
renderCredits($("#credits-list"));
|
|
const $creditsModal = $("#modal-credits");
|
|
$("#menu-credits").on("click", () => $creditsModal.removeClass("hidden"));
|
|
$creditsModal.find(".modal-close").on("click", () => $creditsModal.addClass("hidden"));
|
|
|
|
// ------------------------------------------------------------- devlog -----
|
|
|
|
renderDevlog($("#devlog-list"), DEVLOG);
|
|
const $devlogModal = $("#modal-devlog");
|
|
$("#menu-devlog").on("click", () => $devlogModal.removeClass("hidden"));
|
|
$devlogModal.find(".modal-close").on("click", () => $devlogModal.addClass("hidden"));
|
|
|
|
$(document).on("keydown", (event) => {
|
|
if (event.key !== "Escape") return;
|
|
$creditsModal.addClass("hidden");
|
|
$devlogModal.addClass("hidden");
|
|
});
|
|
|
|
// -------------------------------------------------------------- menu ------
|
|
|
|
// The server creates its game at startup, so the menu only has Join. A page on
|
|
// the local machine skips the form and signs in directly.
|
|
$("#menu-join").on("click", () => {
|
|
if (isLocalhost) {
|
|
autoJoinLocal();
|
|
return;
|
|
}
|
|
showScreen("screen-join");
|
|
refreshGames();
|
|
});
|
|
|
|
// -------------------------------------------------------------- join ------
|
|
|
|
$("#join-server").val(network.defaultServerUrl());
|
|
|
|
$("#join-refresh").on("click", () => refreshGames());
|
|
$("#join-back").on("click", () => {
|
|
network.close();
|
|
showScreen("screen-menu");
|
|
});
|
|
$("#join-go").on("click", joinSelected);
|
|
$("#choose-back").on("click", () => {
|
|
network.close();
|
|
showScreen("screen-join");
|
|
setJoinEnabled(true);
|
|
});
|
|
|
|
function joinServerUrl() {
|
|
return network.normalizeUrl($("#join-server").val());
|
|
}
|
|
|
|
function refreshGames(message = "") {
|
|
joining = false;
|
|
games = [];
|
|
selectedGame = -1;
|
|
$("#join-games").empty();
|
|
setJoinEnabled(false);
|
|
status("join-status", message || "Looking for games...");
|
|
network.requestGameList(joinServerUrl()).catch(() => {
|
|
status("join-status", "Could not reach the game server.");
|
|
setJoinEnabled(true);
|
|
});
|
|
}
|
|
|
|
function setJoinEnabled(enabled) {
|
|
$("#join-server, #join-name, #join-password").prop("disabled", !enabled);
|
|
$("#join-refresh, #join-go").prop("disabled", !enabled);
|
|
}
|
|
|
|
function renderGames(list) {
|
|
games = list;
|
|
selectedGame = -1;
|
|
const $list = $("#join-games").empty();
|
|
for (let i = 0; i < list.length; i++) {
|
|
const game = list[i];
|
|
const parts = [game.name || "Game", game.date || "?", `${game.players || 0} player(s)`];
|
|
if (game.civs && game.civs.length) parts.push(game.civs.join(", "));
|
|
const $item = $("<li></li>").text(parts.join(" — "));
|
|
$item.on("click", () => {
|
|
selectedGame = i;
|
|
$("#join-games li").removeClass("selected");
|
|
$item.addClass("selected");
|
|
});
|
|
$list.append($item);
|
|
}
|
|
if (list.length === 0) {
|
|
status("join-status", "No game in progress on this server.");
|
|
} else {
|
|
selectedGame = 0;
|
|
$list.children().first().addClass("selected");
|
|
status("join-status", "Select a game and enter a name and a password.");
|
|
}
|
|
setJoinEnabled(true);
|
|
}
|
|
|
|
function joinSelected() {
|
|
const name = $("#join-name").val().trim();
|
|
const password = $("#join-password").val();
|
|
if (!name || !password) {
|
|
status("join-status", "Enter a name and a password.");
|
|
return;
|
|
}
|
|
if (selectedGame < 0) {
|
|
status("join-status", "Select a game to join.");
|
|
return;
|
|
}
|
|
config.playerName = name;
|
|
config.password = password;
|
|
joining = true;
|
|
setJoinEnabled(false);
|
|
status("join-status", "Connecting...");
|
|
network.joinGame(joinServerUrl(), name, password).catch(() => {
|
|
status("join-status", "Could not reach the game server.");
|
|
setJoinEnabled(true);
|
|
});
|
|
}
|
|
|
|
// The local fast path: no server list, no login form and no nation picker. The
|
|
// server still decides what a local peer is allowed to do.
|
|
function autoJoinLocal() {
|
|
config.playerName = LOCAL_NAME;
|
|
config.password = LOCAL_PASSWORD;
|
|
joining = true;
|
|
status("join-status", "Connecting...");
|
|
network.joinGame(network.defaultServerUrl(), LOCAL_NAME, LOCAL_PASSWORD).catch(() => {
|
|
fallBackToJoin("Could not reach the game server.");
|
|
});
|
|
}
|
|
|
|
// The local fast path failed (a name claimed with a different password, a busy
|
|
// server): show the ordinary join screen so it can be fixed by hand.
|
|
function fallBackToJoin(message) {
|
|
config.testing = false;
|
|
joining = false;
|
|
showScreen("screen-join");
|
|
refreshGames(message);
|
|
}
|
|
|
|
// ------------------------------------------------------------ sessions ----
|
|
|
|
network.on("session_started", () => {
|
|
// The server follows up with the unclaimed civilisations to pick from.
|
|
status("join-status", "");
|
|
});
|
|
|
|
// Offer the unclaimed civilisations; picking one claims it for this player. A
|
|
// local page claims France (or the first free nation) without showing a picker.
|
|
network.on("choose_civ_received", (civs) => {
|
|
if (config.testing) {
|
|
const pick = (civs || []).find((c) => c.id === LOCAL_CIV) || (civs || [])[0];
|
|
if (pick) {
|
|
network.claimCiv(pick.index);
|
|
return;
|
|
}
|
|
status("choose-status", "No unclaimed nation left — joining as a spectator.");
|
|
startGameScreen();
|
|
return;
|
|
}
|
|
if (!civs || civs.length === 0) {
|
|
status("choose-status", "No unclaimed nation left — joining as a spectator.");
|
|
startGameScreen();
|
|
return;
|
|
}
|
|
renderCivChoices(civs);
|
|
showScreen("screen-choose");
|
|
});
|
|
|
|
network.on("civ_assigned", () => {
|
|
if (gameScreen === null) {
|
|
status("choose-status", "");
|
|
startGameScreen();
|
|
}
|
|
});
|
|
|
|
function civOption(civ) {
|
|
return $("<li class='civ-option'></li>")
|
|
.attr("data-civ-id", civ.id)
|
|
.append($("<img alt=''>").attr("src", `assets/flags/${civ.flag}`))
|
|
.append($("<span></span>").text(civ.name));
|
|
}
|
|
|
|
function renderCivChoices(civs) {
|
|
const $list = $("#choose-civs").empty();
|
|
for (const entry of civs) {
|
|
const civ = CIVILISATIONS.find((c) => c.id === entry.id) || entry;
|
|
const $item = civOption(civ);
|
|
$item.on("click", () => {
|
|
$list.children().removeClass("selected");
|
|
$item.addClass("selected");
|
|
config.playerCivId = entry.id;
|
|
status("choose-status", "Claiming...");
|
|
network.claimCiv(entry.index);
|
|
});
|
|
$list.append($item);
|
|
}
|
|
}
|
|
|
|
network.on("session_failed", (message) => {
|
|
if (isLocalhost && config.testing) {
|
|
fallBackToJoin(message);
|
|
return;
|
|
}
|
|
setJoinEnabled(true);
|
|
status("join-status", message);
|
|
if (joining) refreshGames(message);
|
|
});
|
|
|
|
network.on("game_list_received", (list) => renderGames(list));
|
|
network.on("game_state_received", (state) => {
|
|
if (gameScreen) gameScreen.onState(state);
|
|
});
|
|
network.on("server_disconnected", () => {
|
|
$("#disconnect-banner").text("Disconnected from the server.").removeClass("hidden");
|
|
});
|
|
|
|
function startGameScreen() {
|
|
if (gameScreen) {
|
|
gameScreen.leave();
|
|
$(window).off("keydown.game");
|
|
}
|
|
$("#disconnect-banner").addClass("hidden");
|
|
// Start the boot log before the screen is built, so the WebGL context, shader
|
|
// compile and texture load are timed and shown alongside the world generation.
|
|
bootLog.bind($("#loading-overlay")).begin();
|
|
gameScreen = new GameScreen(network, config);
|
|
gameScreen.beginLoading();
|
|
showScreen("screen-game");
|
|
gameScreen.enter();
|
|
bootLog.step("Opened the game screen");
|
|
bootLog.step("Waiting for the world from the server");
|
|
}
|
|
|
|
// Reconnect notice clears once the player navigates back to the menu.
|
|
$("#menu-join").on("click", () => $("#disconnect-banner").addClass("hidden"));
|