Files
Battle-for-Tismo/client/js/app.js
T
adrien 1a9d48cdc9 Added aircraft sorties, artillery bombardment and local testing mode
- 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
2026-09-18 21:55:30 +02:00

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"));