- aircraft attack orders fly a sortie to the target, bomb once, return to a friendly airport and re-arm (two-day cooldown, charged per aircraft); rebasing is bounded by fuel rather than the 10-tile combat radius - artillery and warships open a sustained hourly bombardment on a standing target, drawn as a red dashed line and ring while either endpoint is selected - a game launched from localhost can enable a testing mode whose train and build rows offer free, instant orders; the server only honours them for a loopback admin in a testing game - an open city panel now unlocks aircraft as soon as an airport finishes
288 lines
8.6 KiB
JavaScript
288 lines
8.6 KiB
JavaScript
// 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 { 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);
|
|
}
|
|
|
|
// -------------------------------------------------------------- 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 $("<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));
|
|
}
|
|
|
|
$("#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 = $("<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;
|
|
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");
|
|
gameScreen = new GameScreen(network, config);
|
|
showScreen("screen-game");
|
|
gameScreen.enter();
|
|
if (isAdmin) gameScreen.sendSetup();
|
|
}
|
|
|
|
// Reconnect notice clears once the player navigates back to the menu.
|
|
$("#menu-create, #menu-join").on("click", () => $("#disconnect-banner").addClass("hidden"));
|