597 lines
20 KiB
JavaScript
597 lines
20 KiB
JavaScript
// World-map screen controller. It wires the map view, the HUD and the modals
|
|
// to the snapshots the server broadcasts, and turns player input into orders.
|
|
// It owns no simulation: movement between snapshots is dead reckoning with the
|
|
// same cost model as the server.
|
|
|
|
import { MapView } from "./map_view.js";
|
|
import { TrainModal, BuildingsModal, NationModal, ConfirmModal, NewsModal, describeNews } from "./modals.js";
|
|
import { GameFeed } from "./feed.js";
|
|
import { compact, groupDigits, improvements as fmtImprovements } from "../../shared/text_format.js";
|
|
import {
|
|
CIVILISATIONS,
|
|
PROTO_UNITS,
|
|
TRAINABLE_UNIT_IDS,
|
|
BUILDINGS,
|
|
GOVERNMENTS,
|
|
TECHNOLOGIES,
|
|
NEWS_WAR,
|
|
} from "../../shared/data.js";
|
|
import { defenseFactors, unitDefense } from "../../shared/rules.js";
|
|
import { dateString } from "../../shared/game_clock.js";
|
|
|
|
export class GameScreen {
|
|
constructor(network, config) {
|
|
this.network = network;
|
|
this.config = config;
|
|
this.$screen = $("#screen-game");
|
|
this.snapshot = null;
|
|
this.localCiv = 0;
|
|
this.civilisations = [];
|
|
this.protoUnits = PROTO_UNITS;
|
|
this.selectedUnitId = 0;
|
|
this.selectedCityId = 0;
|
|
this._trainingCityId = 0;
|
|
this._cityBuildings = new Map();
|
|
this._population = new Map();
|
|
this._territoryOwner = new Map();
|
|
this._researched = new Set();
|
|
this._wars = new Set();
|
|
this._news = [];
|
|
this._seenNews = null;
|
|
this._entered = false;
|
|
this._lastFrame = 0;
|
|
this._fpsFrames = 0;
|
|
this._fpsElapsed = 0;
|
|
|
|
this.map = new MapView(
|
|
$("#map-viewport"),
|
|
$("#map-world"),
|
|
{
|
|
overview: $("#layer-overview"),
|
|
terrain: $("#layer-terrain"),
|
|
borders: $("#layer-borders"),
|
|
fog: $("#layer-fog"),
|
|
paths: $("#layer-paths"),
|
|
entities: $("#layer-entities"),
|
|
labels: $("#layer-labels"),
|
|
}
|
|
);
|
|
this.trainModal = new TrainModal();
|
|
this.buildingsModal = new BuildingsModal();
|
|
this.nationModal = new NationModal();
|
|
this.confirmModal = new ConfirmModal();
|
|
this.newsModal = new NewsModal();
|
|
this.feed = new GameFeed($("#game-feed"));
|
|
this.$statTooltip = $("#stat-tooltip");
|
|
this._hoveredStat = null;
|
|
|
|
this._bindMap();
|
|
this._bindHud();
|
|
}
|
|
|
|
enter() {
|
|
this.$screen.addClass("active");
|
|
this._entered = true;
|
|
this.map._applyCamera();
|
|
this._lastFrame = performance.now();
|
|
requestAnimationFrame((t) => this._frame(t));
|
|
}
|
|
|
|
leave() {
|
|
this._entered = false;
|
|
this.$screen.removeClass("active");
|
|
this.feed.clear();
|
|
this._seenNews = null;
|
|
}
|
|
|
|
// ------------------------------------------------------------- orders ---
|
|
|
|
sendOrder(order) {
|
|
this.network.sendOrder(order);
|
|
}
|
|
|
|
sendSetup() {
|
|
const ids = this.config.civilisations.map((c) => c.id);
|
|
const playerIndex = Math.max(0, ids.indexOf(this.config.playerCivId));
|
|
this.network.sendGameSetup({
|
|
seed: this.config.seed || 0,
|
|
player_civ: playerIndex,
|
|
civilisations: ids,
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------- snapshots ---
|
|
|
|
onState(state) {
|
|
this.snapshot = state;
|
|
this.localCiv = state.viewer;
|
|
this.civilisations = state.civs.map(
|
|
(c) => CIVILISATIONS.find((full) => full.id === c.id) || { id: c.id, name: c.name, primaryColour: "#888" }
|
|
);
|
|
this.protoUnits = state.protos.map(
|
|
(p) => PROTO_UNITS.find((full) => full.id === p.id) || { id: p.id, name: p.name }
|
|
);
|
|
this._population = new Map(state.population.map((e) => [`${e[0]},${e[1]}`, e[2]]));
|
|
this._territoryOwner = new Map(state.territory.map((e) => [`${e[0]},${e[1]}`, e[2]]));
|
|
this._cityBuildings = new Map();
|
|
for (const city of state.cities) {
|
|
this._cityBuildings.set(city.id, { ...city.buildings });
|
|
}
|
|
this._researched = new Set(
|
|
(state.researched[this.localCiv] || []).map((index) => Number(index))
|
|
);
|
|
this._wars = new Set();
|
|
for (const entry of state.diplomacy || []) {
|
|
if (entry[2] !== "war") continue;
|
|
const a = entry[0];
|
|
const b = entry[1];
|
|
this._wars.add(a < b ? `${a}:${b}` : `${b}:${a}`);
|
|
}
|
|
this._news = state.news || [];
|
|
this._updateFeed(this._news);
|
|
|
|
this.map.setCatalogue(this.civilisations, this.protoUnits);
|
|
this.map.applySnapshot(state);
|
|
this.map.setSelectedUnit(this.selectedUnitId);
|
|
this._updateTopPanel(state);
|
|
this._updateBottomPanel();
|
|
this._updateTrainModal();
|
|
this._updateBuildingsModal();
|
|
this._updateNationModal();
|
|
this._updateNewsModal();
|
|
}
|
|
|
|
// ---------------------------------------------------------------- HUD ----
|
|
|
|
_bindHud() {
|
|
$("#city-train").on("click", () => this._openTrainModal());
|
|
$("#city-build").on("click", () => this._openBuildingsModal());
|
|
$("#civ-flag").on("click", () => this._openNationModal());
|
|
$("#news-button").on("click", () => this._openNewsModal());
|
|
|
|
this.trainModal.onTrain = (cityId, protoIndex) =>
|
|
this.sendOrder({ type: "train", city: cityId, proto: protoIndex });
|
|
this.buildingsModal.onBuild = (cityId, index) =>
|
|
this.sendOrder({ type: "build", city: cityId, building: index });
|
|
this.buildingsModal.onDemolish = (cityId, index) =>
|
|
this.sendOrder({ type: "demolish", city: cityId, building: index });
|
|
this.nationModal.onGovernment = (index) =>
|
|
this.sendOrder({ type: "government", government: index });
|
|
this.nationModal.onTechnology = (index) =>
|
|
this.sendOrder({ type: "technology", technology: index });
|
|
this.nationModal.onDeclareWar = (target) =>
|
|
this.sendOrder({ type: "declare_war", target });
|
|
|
|
$("#unit-attack")
|
|
.on("mouseenter", (event) => this._showStatTooltip($(event.currentTarget)))
|
|
.on("mouseleave", () => this._hideStatTooltip());
|
|
$("#unit-defense")
|
|
.on("mouseenter", (event) => this._showStatTooltip($(event.currentTarget)))
|
|
.on("mouseleave", () => this._hideStatTooltip());
|
|
|
|
$(window).on("keydown.game", (event) => this._onKey(event));
|
|
}
|
|
|
|
// A small hover tooltip anchored above a stat, fed by its data-tooltip text.
|
|
_showStatTooltip($stat) {
|
|
const text = $stat.attr("data-tooltip");
|
|
if (!text) return;
|
|
this._hoveredStat = $stat;
|
|
this.$statTooltip.text(text).removeClass("hidden");
|
|
const offset = $stat.offset() || { left: 0, top: 0 };
|
|
const width = this.$statTooltip.outerWidth() || 0;
|
|
const height = this.$statTooltip.outerHeight() || 0;
|
|
this.$statTooltip.css({
|
|
left: `${Math.max(8, offset.left + $stat.outerWidth() / 2 - width / 2)}px`,
|
|
top: `${Math.max(8, offset.top - height - 8)}px`,
|
|
});
|
|
}
|
|
|
|
_hideStatTooltip() {
|
|
this._hoveredStat = null;
|
|
this.$statTooltip.addClass("hidden");
|
|
}
|
|
|
|
_bindMap() {
|
|
this.map.onUnitSelected = (id) => {
|
|
if (!id) {
|
|
this.selectedUnitId = 0;
|
|
this._showUnit(null);
|
|
return;
|
|
}
|
|
const unit = (this.snapshot.units || []).find((u) => u.id === id);
|
|
if (!unit || unit.civ !== this.localCiv) {
|
|
this.map.setSelectedUnit(0);
|
|
this.selectedUnitId = 0;
|
|
return;
|
|
}
|
|
this.selectedUnitId = id;
|
|
this.selectedCityId = 0;
|
|
this.map.setSelectedUnit(id);
|
|
this._showUnit(unit);
|
|
};
|
|
this.map.onCitySelected = (id) => {
|
|
const city = (this.snapshot.cities || []).find((c) => c.id === id);
|
|
if (!city || city.civ !== this.localCiv) return;
|
|
this.selectedCityId = id;
|
|
this.selectedUnitId = 0;
|
|
this.map.setSelectedUnit(0);
|
|
this._showCity(city);
|
|
};
|
|
this.map.onTileRequested = (coords) => this._showTile(coords);
|
|
this.map.onMoveOrdered = (unitId, coords) => {
|
|
const unit = (this.snapshot.units || []).find((u) => u.id === unitId);
|
|
if (!unit || unit.civ !== this.localCiv) return;
|
|
const targets = this._unalliedTerritoryOnRoute(unit, coords);
|
|
if (targets.length > 0) {
|
|
const names = targets.map((index) => this._civName(index)).join(", ");
|
|
const demonyms = targets.map((index) => this._civDemonym(index)).join(", ");
|
|
this.confirmModal.show(
|
|
"Declare war?",
|
|
`Marching into ${demonyms} territory will declare war on ${names}. Continue?`,
|
|
() => this._sendMove(unit, coords)
|
|
);
|
|
return;
|
|
}
|
|
this._sendMove(unit, coords);
|
|
};
|
|
}
|
|
|
|
_sendMove(unit, coords) {
|
|
this.sendOrder({ type: "move", unit: unit.id, coords: [coords.x, coords.y] });
|
|
this.map.predictMove(unit, coords);
|
|
}
|
|
|
|
// The civilisations, other than our own, whose territory the predicted route
|
|
// would enter while we are at peace with them. Only military units can start
|
|
// a war this way; civilians wander in without one.
|
|
_unalliedTerritoryOnRoute(unit, coords) {
|
|
if (!this._isMilitaryUnit(unit)) return [];
|
|
const route = this.map.planMove(unit, coords);
|
|
if (!route || route.length < 2) return [];
|
|
const targets = [];
|
|
for (const point of route) {
|
|
const owner = this.map.territory.get(`${point.x},${point.y}`);
|
|
if (owner === undefined || owner < 0 || owner === unit.civ) continue;
|
|
if (this._isAtWar(unit.civ, owner)) continue;
|
|
if (!targets.includes(owner)) targets.push(owner);
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
_isMilitaryUnit(unit) {
|
|
const proto = this.protoUnits[unit.proto];
|
|
return !!(proto && proto.military);
|
|
}
|
|
|
|
_isAtWar(a, b) {
|
|
if (a < 0 || b < 0 || a === b) return false;
|
|
return this._wars.has(a < b ? `${a}:${b}` : `${b}:${a}`);
|
|
}
|
|
|
|
_civName(index) {
|
|
const civ = this.civilisations[index];
|
|
return civ ? civ.name : `Nation ${index + 1}`;
|
|
}
|
|
|
|
_civDemonym(index) {
|
|
const civ = this.civilisations[index];
|
|
return civ && civ.demonym ? civ.demonym : this._civName(index);
|
|
}
|
|
|
|
_updateTopPanel(state) {
|
|
const civ = this.civilisations[this.localCiv];
|
|
if (civ) {
|
|
$("#civ-name").text(civ.name);
|
|
const flag = civ.flag || (CIVILISATIONS.find((c) => c.id === civ.id) || {}).flag;
|
|
if (flag) $("#civ-flag-img").attr("src", `assets/flags/${flag}`);
|
|
}
|
|
$("#player-name").text(this.config.playerName || (this.network.players[this.network.localPlayerId] || {}).name || "");
|
|
$("#admin-label").toggle(this.network.isAdmin);
|
|
$("#stat-date").text(dateString(state.totalHours));
|
|
const stats = state.viewerStats || {};
|
|
$("#stat-pop").text(compact(stats.population || 0));
|
|
$("#stat-gdp").text(`€${compact(stats.gdp || 0)}`);
|
|
$("#stat-gdppc").text(`€${compact(stats.gdpPerCapita || 0)}`);
|
|
$("#stat-budget").text(`€${compact(stats.budget || 0)}`);
|
|
}
|
|
|
|
_updateBottomPanel() {
|
|
if (this.selectedUnitId) {
|
|
const unit = this._unit(this.selectedUnitId);
|
|
if (unit) this._showUnit(unit);
|
|
} else if (this.selectedCityId) {
|
|
const city = this._city(this.selectedCityId);
|
|
if (city) this._showCity(city);
|
|
}
|
|
}
|
|
|
|
_showUnit(unit) {
|
|
if (!unit) {
|
|
$("#unit-info").addClass("hidden");
|
|
this._hideStatTooltip();
|
|
this._refreshBottomVisibility();
|
|
return;
|
|
}
|
|
const proto = this.protoUnits[unit.proto];
|
|
$("#unit-name").text(proto ? proto.name : "Unit");
|
|
this._showUnitStats(unit, proto);
|
|
const ratio = unit.maxHp > 0 ? (unit.hp / unit.maxHp) * 100 : 0;
|
|
$("#unit-health-fill").css("width", `${Math.max(0, Math.min(100, ratio))}%`);
|
|
$("#tile-info, #city-info").addClass("hidden");
|
|
$("#unit-info").removeClass("hidden");
|
|
this._refreshBottomVisibility();
|
|
}
|
|
|
|
// Attack and defense of the unit where it stands, with the hovering tooltips
|
|
// listing each factor that shaped the number.
|
|
_showUnitStats(unit, proto) {
|
|
const coords = { x: unit.coords[0], y: unit.coords[1] };
|
|
const tile = this.map.getTile(coords);
|
|
const hasCity = ((this.snapshot && this.snapshot.cities) || []).some(
|
|
(city) => city.coords[0] === coords.x && city.coords[1] === coords.y
|
|
);
|
|
const baseAttack = proto && typeof proto.attack === "number" ? proto.attack : 0;
|
|
const baseDefense = proto && typeof proto.defense === "number" ? proto.defense : 0;
|
|
const baseRange = proto && typeof proto.range === "number" ? proto.range : 1;
|
|
|
|
const attackParts = [`Base ${Math.round(baseAttack)}`];
|
|
if (baseRange > 1) attackParts.push(`Range ${baseRange}`);
|
|
$("#unit-attack")
|
|
.text(`Attack ${Math.round(baseAttack)}`)
|
|
.attr("data-tooltip", attackParts.join(", "));
|
|
|
|
const factors = defenseFactors(tile, hasCity);
|
|
const defense = unitDefense(baseDefense, tile, hasCity);
|
|
const parts = [`Base ${Math.round(baseDefense)}`];
|
|
for (const factor of factors) {
|
|
parts.push(`${factor.label} +${Math.round(baseDefense * factor.bonus)}`);
|
|
}
|
|
$("#unit-defense")
|
|
.text(`Defense ${Math.round(defense)}`)
|
|
.attr("data-tooltip", parts.join(", "));
|
|
// Keep an open tooltip in step with the unit it describes across snapshots.
|
|
if (this._hoveredStat && !this.$statTooltip.hasClass("hidden")) {
|
|
const text = this._hoveredStat.attr("data-tooltip");
|
|
if (text) this.$statTooltip.text(text);
|
|
} else {
|
|
this._hideStatTooltip();
|
|
}
|
|
}
|
|
|
|
_showCity(city) {
|
|
if (!city) {
|
|
$("#city-info").addClass("hidden");
|
|
this._refreshBottomVisibility();
|
|
return;
|
|
}
|
|
$("#city-name").text(city.name);
|
|
$("#city-population").text(`Population: ${groupDigits(city.population)}`);
|
|
$("#city-improvements").text(`Improvements: ${fmtImprovements(city.improvements)}`);
|
|
$("#tile-info, #unit-info").addClass("hidden");
|
|
$("#city-info").removeClass("hidden");
|
|
this._refreshBottomVisibility();
|
|
}
|
|
|
|
_showTile(coords) {
|
|
const k = `${coords.x},${coords.y}`;
|
|
const owner = this._territoryOwner.has(k) ? this._territoryOwner.get(k) : -1;
|
|
const civ = this.civilisations[owner];
|
|
const tile = this.map.getTile(coords);
|
|
const city = (this.snapshot.cities || []).find(
|
|
(c) => c.coords[0] === coords.x && c.coords[1] === coords.y
|
|
);
|
|
$("#tile-coords").text(`Tile (${coords.x}, ${coords.y})`);
|
|
$("#tile-owner").text(`Owner: ${civ ? civ.name : "Unclaimed"}`);
|
|
$("#tile-terrain").text(`Terrain: ${tile ? tile.terrainType : "Unknown"}`);
|
|
$("#tile-population").text(`Population: ${groupDigits(this._population.get(k) || 0)}`);
|
|
$("#tile-improvements").text(`Improvements: ${fmtImprovements(city ? city.improvements : [])}`);
|
|
$("#city-info, #unit-info").addClass("hidden");
|
|
$("#tile-info").removeClass("hidden");
|
|
this._refreshBottomVisibility();
|
|
}
|
|
|
|
_refreshBottomVisibility() {
|
|
const visible =
|
|
!$("#tile-info").hasClass("hidden") ||
|
|
!$("#city-info").hasClass("hidden") ||
|
|
!$("#unit-info").hasClass("hidden");
|
|
$("#bottom-panel").toggleClass("hidden", !visible);
|
|
}
|
|
|
|
_unit(id) {
|
|
return (this.snapshot.units || []).find((u) => u.id === id) || null;
|
|
}
|
|
|
|
_city(id) {
|
|
return (this.snapshot.cities || []).find((c) => c.id === id) || null;
|
|
}
|
|
|
|
// -------------------------------------------------------------- modals ---
|
|
|
|
_openTrainModal() {
|
|
const city = this._city(this.selectedCityId);
|
|
if (!city) return;
|
|
const trainable = TRAINABLE_UNIT_IDS.map((id) => {
|
|
const index = this.protoUnits.findIndex((p) => p.id === id);
|
|
return index >= 0 ? { ...this.protoUnits[index], serverIndex: index } : null;
|
|
}).filter(Boolean);
|
|
this.buildingsModal.close();
|
|
this.trainModal.show(
|
|
{ id: city.id, name: city.name },
|
|
(this.snapshot.viewerStats || {}).gdp || 0,
|
|
trainable
|
|
);
|
|
this._updateTrainModal();
|
|
}
|
|
|
|
_updateTrainModal() {
|
|
if (!this.snapshot || !this.selectedCityId) return;
|
|
if (!this.trainModal.isShowing(this.selectedCityId)) {
|
|
this._trainingCityId = 0;
|
|
return;
|
|
}
|
|
const entry = (this.snapshot.training || {})[this.selectedCityId];
|
|
if (!entry) {
|
|
if (this._trainingCityId === this.selectedCityId) {
|
|
this.trainModal.endTraining();
|
|
this._trainingCityId = 0;
|
|
}
|
|
return;
|
|
}
|
|
const proto = this.protoUnits[entry.protoIndex];
|
|
const name = proto ? proto.name : "unit";
|
|
if (this._trainingCityId !== this.selectedCityId) {
|
|
this._trainingCityId = this.selectedCityId;
|
|
this.trainModal.beginTraining(name, entry.totalHours, entry.elapsedHours);
|
|
} else {
|
|
this.trainModal.setTrainingProgress(name, entry.elapsedHours, entry.totalHours);
|
|
}
|
|
}
|
|
|
|
_openBuildingsModal() {
|
|
const city = this._city(this.selectedCityId);
|
|
if (!city) return;
|
|
this.trainModal.close();
|
|
this.buildingsModal.show(
|
|
{ id: city.id, name: city.name },
|
|
(this.snapshot.viewerStats || {}).gdp || 0,
|
|
(this.snapshot.viewerStats || {}).budget || 0,
|
|
BUILDINGS,
|
|
this._cityBuildings.get(city.id) || {}
|
|
);
|
|
}
|
|
|
|
_updateBuildingsModal() {
|
|
if (!this.snapshot || !this.buildingsModal.isShowing(this.selectedCityId)) return;
|
|
const stats = this.snapshot.viewerStats || {};
|
|
this.buildingsModal.refresh(
|
|
stats.gdp || 0,
|
|
stats.budget || 0,
|
|
this._cityBuildings.get(this.selectedCityId) || {}
|
|
);
|
|
}
|
|
|
|
_openNationModal() {
|
|
const stats = this.snapshot.viewerStats || {};
|
|
this.nationModal.show(
|
|
GOVERNMENTS,
|
|
stats.government || 0,
|
|
TECHNOLOGIES,
|
|
this._researched,
|
|
stats.gdp || 0,
|
|
stats.budget || 0,
|
|
stats.research || 0,
|
|
this._finances(stats),
|
|
this._diplomacyInfo()
|
|
);
|
|
}
|
|
|
|
_updateNationModal() {
|
|
if (!this.snapshot || !this.nationModal.isShowing()) return;
|
|
const stats = this.snapshot.viewerStats || {};
|
|
this.nationModal.refresh(
|
|
stats.government || 0,
|
|
this._researched,
|
|
stats.gdp || 0,
|
|
stats.budget || 0,
|
|
stats.research || 0,
|
|
this._finances(stats),
|
|
this._diplomacyInfo()
|
|
);
|
|
}
|
|
|
|
_diplomacyInfo() {
|
|
return {
|
|
civs: this.civilisations,
|
|
localCiv: this.localCiv,
|
|
relations: (this.snapshot && this.snapshot.diplomacy) || [],
|
|
};
|
|
}
|
|
|
|
_openNewsModal() {
|
|
this.newsModal.show(this._news, this.civilisations);
|
|
}
|
|
|
|
_updateNewsModal() {
|
|
if (!this.snapshot || !this.newsModal.isShowing()) return;
|
|
this.newsModal.refresh(this._news, this.civilisations);
|
|
}
|
|
|
|
// Pops newly arrived news into the corner feed. The first snapshot only
|
|
// records what is already known, so history never floods the screen.
|
|
_updateFeed(news) {
|
|
if (this._seenNews === null) {
|
|
this._seenNews = new Set(news.map((entry) => entry.id));
|
|
return;
|
|
}
|
|
for (const entry of news) {
|
|
if (this._seenNews.has(entry.id)) continue;
|
|
this._seenNews.add(entry.id);
|
|
this._pushNews(entry);
|
|
}
|
|
}
|
|
|
|
_pushNews(entry) {
|
|
if (entry.type === NEWS_WAR && entry.target === this.localCiv) {
|
|
this.feed.push(`${this._civName(entry.actor)} has declared war on you!`, { alert: true });
|
|
return;
|
|
}
|
|
this.feed.push(describeNews(entry, this.civilisations));
|
|
}
|
|
|
|
_finances(stats) {
|
|
return stats.breakdown || null;
|
|
}
|
|
|
|
// --------------------------------------------------------------- input ---
|
|
|
|
_onKey(event) {
|
|
if (!this._entered) return;
|
|
const step = event.shiftKey ? 120 : 40;
|
|
switch (event.key) {
|
|
case "w": case "ArrowUp": this.map.panBy(0, step); break;
|
|
case "s": case "ArrowDown": this.map.panBy(0, -step); break;
|
|
case "a": case "ArrowLeft": this.map.panBy(step, 0); break;
|
|
case "d": case "ArrowRight": this.map.panBy(-step, 0); break;
|
|
case "+": case "=":
|
|
this.map.camera.zoom = Math.min(2, this.map.camera.zoom * 1.1);
|
|
this.map._applyCamera();
|
|
break;
|
|
case "-":
|
|
this.map.camera.zoom = Math.max(0.2, this.map.camera.zoom / 1.1);
|
|
this.map._applyCamera();
|
|
break;
|
|
case "Escape":
|
|
this.trainModal.close();
|
|
this.buildingsModal.close();
|
|
this.nationModal.close();
|
|
this.confirmModal.close();
|
|
this.newsModal.close();
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
|
|
// --------------------------------------------------------------- frame ---
|
|
|
|
_frame(now) {
|
|
if (!this._entered) return;
|
|
const dt = Math.min(0.1, (now - this._lastFrame) / 1000);
|
|
this._lastFrame = now;
|
|
this.map.animate(dt);
|
|
this._fpsFrames += 1;
|
|
this._fpsElapsed += dt;
|
|
if (this._fpsElapsed >= 0.25) {
|
|
const fps = Math.round(this._fpsFrames / this._fpsElapsed);
|
|
this._fpsElapsed = 0;
|
|
this._fpsFrames = 0;
|
|
const $label = $("#fps-label").text(`${fps} FPS`);
|
|
$label.css("color", fps < 30 ? "#f24" : fps < 50 ? "#fa4" : "#8fdc8f");
|
|
}
|
|
requestAnimationFrame((t) => this._frame(t));
|
|
}
|
|
}
|