// Application bootstrap: screen routing plus the session state that the new // game and join screens fill in (the menu/new-game/join flow). 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 { chooseMenuBackground } from "./menu_background.js"; import { CIVILISATIONS } from "../../shared/data.js"; const network = new Network(); const config = { playerName: "", password: "", playerCivId: CIVILISATIONS[0].id, civilisations: [], seed: 0, // Set when the creator ticks testing mode on a locally served page. testing: false, }; // Testing tools are only offered to a page served from the local machine; the // server likewise refuses to enable them for a remote connection. const isLocalhost = ["localhost", "127.0.0.1", "::1", "[::1]"].includes( window.location.hostname ); $("#new-testing-row").toggleClass("hidden", !isLocalhost); let gameScreen = null; let joining = false; let mode = "join"; let games = []; let selectedGame = -1; let selectedCivId = CIVILISATIONS[0].id; 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")); $(document).on("keydown", (event) => { if (event.key === "Escape") $creditsModal.addClass("hidden"); }); // -------------------------------------------------------------- menu ------ $("#menu-create").on("click", () => showScreen("screen-new")); $("#menu-join").on("click", () => { showScreen("screen-join"); refreshGames(); }); // ---------------------------------------------------------- new game ------ const $civSelect = $("#new-civs"); // The picker reads alphabetically; the catalogue order is left alone so it can // still drive the default roster. const menuCivs = CIVILISATIONS.slice().sort((a, b) => a.name.localeCompare(b.name)); for (const civ of menuCivs) { const $item = civOption(civ); $item.on("click", () => { selectedCivId = civ.id; $civSelect.children().removeClass("selected"); $item.addClass("selected"); }); $civSelect.append($item); } $civSelect.children().filter(`[data-civ-id="${selectedCivId}"]`).addClass("selected"); $("#new-count").attr("max", CIVILISATIONS.length); // A fresh game starts with every civilisation in the world. $("#new-count").val(CIVILISATIONS.length); function civOption(civ) { return $("
  • ") .attr("data-civ-id", civ.id) .append($("").attr("src", `assets/flags/${civ.flag}`)) .append($("").text(civ.name)); } $("#new-back").on("click", () => { network.close(); showScreen("screen-menu"); }); $("#new-start").on("click", () => { const civId = selectedCivId; const count = Math.max(1, Math.min(CIVILISATIONS.length, Number($("#new-count").val()) || 1)); const name = $("#new-name").val().trim(); const password = $("#new-password").val(); if (!name || !password) { status("new-status", "Enter a name and a password."); return; } const player = CIVILISATIONS.find((c) => c.id === civId) || CIVILISATIONS[0]; const list = [player]; for (const civ of CIVILISATIONS) { if (list.length >= count) break; if (civ.id !== player.id) list.push(civ); } config.playerName = name; config.password = password; config.playerCivId = player.id; config.civilisations = list; config.seed = 0; config.testing = isLocalhost && $("#new-testing").is(":checked"); mode = "create"; setNewEnabled(false); status("new-status", "Starting the game..."); network.createGame(name, password).catch(() => { setNewEnabled(true); status("new-status", "Could not connect to the game server."); }); }); function setNewEnabled(enabled) { $("#new-start, #new-back").prop("disabled", !enabled); $("#new-count, #new-name, #new-password, #new-testing").prop("disabled", !enabled); $("#new-civs").css("pointer-events", enabled ? "" : "none"); } // -------------------------------------------------------------- join ------ $("#join-server").val(network.defaultServerUrl()); $("#join-refresh").on("click", () => refreshGames()); $("#join-back").on("click", () => { network.close(); showScreen("screen-menu"); }); $("#join-create").on("click", () => showScreen("screen-new")); $("#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, #join-create").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 = $("
  • ").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; config.playerCivId = null; config.civilisations = []; mode = "join"; 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); }); } // ------------------------------------------------------------ sessions ---- network.on("session_started", () => { if (mode === "create") { status("new-status", ""); startGameScreen(true); } else { // 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. network.on("choose_civ_received", (civs) => { if (mode !== "join") return; if (!civs || civs.length === 0) { status("choose-status", "No unclaimed nation left — joining as a spectator."); startGameScreen(false); return; } renderCivChoices(civs); showScreen("screen-choose"); }); network.on("civ_assigned", () => { if (mode === "join" && gameScreen === null) { status("choose-status", ""); startGameScreen(false); } }); 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 (mode === "create") { setNewEnabled(true); status("new-status", message); } else { 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(isAdmin) { 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"); if (isAdmin) { gameScreen.sendSetup(); bootLog.step("Requested the world from the server"); } else { bootLog.step("Waiting for the world from the server"); } } // Reconnect notice clears once the player navigates back to the menu. $("#menu-create, #menu-join").on("click", () => $("#disconnect-banner").addClass("hidden"));