359 lines
15 KiB
JavaScript
359 lines
15 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var screenshots = [
|
|
{ file: "evac_1.jpg", title: "The road ahead" },
|
|
{ file: "evac_2.jpg", title: "Make every round count" },
|
|
{ file: "evac_3.jpg", title: "Hold the perimeter" },
|
|
{ file: "evac_4.jpg", title: "Extraction route" },
|
|
{ file: "abducted_1.jpg", title: "Into the unknown" },
|
|
{ file: "abducted_2.jpg", title: "No one fights alone" },
|
|
{ file: "abducted_3.jpg", title: "Containment breached" },
|
|
{ file: "horror_raid.jpg", title: "After the sirens" },
|
|
{ file: "scream_1.jpg", title: "Keep moving" },
|
|
{ file: "scream_2.jpg", title: "Lights out" },
|
|
{ file: "scream_3.jpg", title: "Something is watching" },
|
|
{ file: "southtown.jpg", title: "The last safe block" }
|
|
];
|
|
|
|
var tips = [
|
|
"Stay close. Co-op maps are at their best when the squad moves together.",
|
|
"Press F3 or type !shop to spend Credits on gear, abilities and companions.",
|
|
"Harder enemies award more Score and Credits. Lifetime Score determines your rank.",
|
|
"Type !nextmap to vote for the next mission, or !skip if the current map is stuck.",
|
|
"Dying can cost Credits, but it never reduces your lifetime Score or rank.",
|
|
"Hold TAB in game to compare lifetime scores with the rest of the squad."
|
|
];
|
|
|
|
var state = {
|
|
filesTotal: 0,
|
|
filesNeeded: 0,
|
|
maxNeeded: 0,
|
|
filesStarted: {},
|
|
startedCount: 0,
|
|
stage: 0,
|
|
progress: 4,
|
|
steamID: "",
|
|
profileRequested: "",
|
|
leaderboardRequested: false,
|
|
currentSlide: 0,
|
|
activeLayer: 0
|
|
};
|
|
|
|
function byId(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function queryValue(name) {
|
|
var match = new RegExp("(?:^|&)" + name + "=([^&]*)", "i").exec(window.location.search.substring(1));
|
|
return match ? decodeURIComponent(match[1].replace(/\+/g, " ")) : "";
|
|
}
|
|
|
|
function cleanInteger(value) {
|
|
value = parseInt(value, 10);
|
|
return isFinite(value) && value > 0 ? value : 0;
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
var text = String(Math.max(parseInt(value, 10) || 0, 0));
|
|
return text.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
function formatPlaytime(seconds) {
|
|
var minutes = Math.max(Math.floor((parseInt(seconds, 10) || 0) / 60), 0);
|
|
var days = Math.floor(minutes / 1440);
|
|
var hours = Math.floor((minutes % 1440) / 60);
|
|
var mins = minutes % 60;
|
|
if (days > 0) return days + "d " + hours + "h";
|
|
if (hours > 0) return hours + "h " + mins + "m";
|
|
return mins + "m";
|
|
}
|
|
|
|
function titleCaseMap(mapName) {
|
|
if (!mapName) return "Preparing mission";
|
|
var words = mapName.replace(/\.(bsp|ain)$/i, "").replace(/[_\-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
return words.replace(/\b([a-z])/g, function (_, letter) { return letter.toUpperCase(); });
|
|
}
|
|
|
|
function setStage(stage) {
|
|
var items = byId("stages").getElementsByTagName("li");
|
|
var index;
|
|
state.stage = Math.max(state.stage, Math.min(stage, items.length - 1));
|
|
for (index = 0; index < items.length; index += 1) {
|
|
items[index].className = index < state.stage ? "done" : (index === state.stage ? "active" : "");
|
|
}
|
|
}
|
|
|
|
function setProgress(value) {
|
|
value = Math.max(state.progress, Math.min(Math.round(value), 99));
|
|
state.progress = value;
|
|
byId("progress-bar").style.width = value + "%";
|
|
byId("percent").textContent = value;
|
|
}
|
|
|
|
function updateFileProgress() {
|
|
var needed = state.maxNeeded || state.filesNeeded;
|
|
var completedFromNeeded = needed && state.filesNeeded <= needed ? needed - state.filesNeeded : 0;
|
|
var completedFromStarts = Math.max(state.startedCount - 1, 0);
|
|
var completed = Math.max(completedFromNeeded, completedFromStarts);
|
|
var receiving = Math.min(Math.max(completed + (state.startedCount ? 1 : 0), 0), needed || state.filesTotal || 0);
|
|
var ratio;
|
|
|
|
if (needed > 0) {
|
|
completed = Math.min(completed, needed);
|
|
ratio = Math.min((completed + (state.startedCount ? 0.35 : 0)) / needed, 0.98);
|
|
setProgress(12 + ratio * 74);
|
|
byId("file-count").textContent = "FILE " + receiving + " OF " + needed + " REQUIRED";
|
|
} else if (state.filesTotal > 0) {
|
|
byId("file-count").textContent = state.filesTotal + " FILES ON MANIFEST";
|
|
}
|
|
}
|
|
|
|
function sourceForFile(fileName) {
|
|
if (/workshop|\.gma$|^\s*\d{7,}\s*$/i.test(fileName)) return "WORKSHOP CONTENT";
|
|
return "SERVER CONTENT";
|
|
}
|
|
|
|
function stageForStatus(status) {
|
|
if (/lua|client info|spawn|precach|ready|starting/i.test(status)) return 3;
|
|
if (/download|resource|materials|models|sound|fastdl/i.test(status)) return 2;
|
|
if (/workshop|addon|mount/i.test(status)) return 1;
|
|
return state.stage;
|
|
}
|
|
|
|
function progressForStatus(status, stage) {
|
|
if (stage === 1) return 10;
|
|
if (stage === 2) return 14;
|
|
if (/client info sent|ready|fully connected/i.test(status)) return 98;
|
|
if (/starting lua|lua started/i.test(status)) return 95;
|
|
if (/sending client info|precach|creating/i.test(status)) return 90;
|
|
if (stage === 3) return 88;
|
|
return 6;
|
|
}
|
|
|
|
function requestProfile(steamID) {
|
|
var request;
|
|
if (!/^7656119\d{10}$/.test(steamID) || state.profileRequested === steamID) return;
|
|
state.profileRequested = steamID;
|
|
request = new XMLHttpRequest();
|
|
request.open("GET", "/api/player?steamid=" + encodeURIComponent(steamID), true);
|
|
request.timeout = 3000;
|
|
request.onreadystatechange = function () {
|
|
var profile;
|
|
if (request.readyState !== 4) return;
|
|
if (request.status >= 200 && request.status < 300) {
|
|
try {
|
|
profile = JSON.parse(request.responseText);
|
|
showProfile(profile);
|
|
return;
|
|
} catch (ignore) {}
|
|
}
|
|
showProfileUnavailable();
|
|
};
|
|
request.ontimeout = showProfileUnavailable;
|
|
request.onerror = showProfileUnavailable;
|
|
request.send(null);
|
|
}
|
|
|
|
function monogram(name) {
|
|
var words = String(name || "?").replace(/[^a-z0-9 ]/gi, " ").trim().split(/\s+/);
|
|
var result = words.length > 1 ? words[0].charAt(0) + words[words.length - 1].charAt(0) : words[0].slice(0, 2);
|
|
return result.toUpperCase() || "?";
|
|
}
|
|
|
|
function showProfile(profile) {
|
|
var name = profile.playerName || "New operative";
|
|
byId("player-name").textContent = name;
|
|
byId("profile-monogram").textContent = monogram(name);
|
|
byId("profile-loading").className = "profile-loading hidden";
|
|
byId("profile-stats").className = "stats";
|
|
byId("player-score").textContent = formatNumber(profile.score);
|
|
byId("player-credits").textContent = formatNumber(profile.credits);
|
|
byId("player-time").textContent = formatPlaytime(profile.playtimeSeconds);
|
|
byId("player-rank").textContent = profile.rank || "Newbie";
|
|
byId("profile-kicker").textContent = profile.found ? "WELCOME BACK, OPERATIVE" : "NEW OPERATIVE RECORD";
|
|
}
|
|
|
|
function showProfileUnavailable() {
|
|
byId("profile-loading").innerHTML = "<span>Progression record available after deployment.</span>";
|
|
}
|
|
|
|
function showLeaderboard(players) {
|
|
var list = byId("leaderboard-list");
|
|
var index;
|
|
var player;
|
|
var row;
|
|
var position;
|
|
var identity;
|
|
var name;
|
|
var rank;
|
|
var score;
|
|
|
|
while (list.firstChild) list.removeChild(list.firstChild);
|
|
if (!players || !players.length) {
|
|
row = document.createElement("li");
|
|
row.className = "leaderboard-loading";
|
|
row.textContent = "No campaign records yet.";
|
|
list.appendChild(row);
|
|
return;
|
|
}
|
|
|
|
for (index = 0; index < players.length; index += 1) {
|
|
player = players[index];
|
|
row = document.createElement("li");
|
|
|
|
position = document.createElement("span");
|
|
position.className = "leaderboard-position";
|
|
position.textContent = String(player.position || index + 1).replace(/^(\d)$/, "0$1");
|
|
|
|
identity = document.createElement("div");
|
|
name = document.createElement("strong");
|
|
name.textContent = player.playerName || "Unknown operative";
|
|
rank = document.createElement("small");
|
|
rank.textContent = (player.rank || "Newbie") + " · " + formatPlaytime(player.playtimeSeconds);
|
|
identity.appendChild(name);
|
|
identity.appendChild(rank);
|
|
|
|
score = document.createElement("span");
|
|
score.className = "leaderboard-score";
|
|
score.textContent = formatNumber(player.score);
|
|
|
|
row.appendChild(position);
|
|
row.appendChild(identity);
|
|
row.appendChild(score);
|
|
list.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function showLeaderboardUnavailable() {
|
|
var list = byId("leaderboard-list");
|
|
list.innerHTML = '<li class="leaderboard-loading">Leaderboard temporarily unavailable.</li>';
|
|
}
|
|
|
|
function requestLeaderboard() {
|
|
var request;
|
|
if (state.leaderboardRequested) return;
|
|
state.leaderboardRequested = true;
|
|
request = new XMLHttpRequest();
|
|
request.open("GET", "/api/leaderboard", true);
|
|
request.timeout = 3000;
|
|
request.onreadystatechange = function () {
|
|
var payload;
|
|
if (request.readyState !== 4) return;
|
|
if (request.status >= 200 && request.status < 300) {
|
|
try {
|
|
payload = JSON.parse(request.responseText);
|
|
showLeaderboard(payload.players || []);
|
|
return;
|
|
} catch (ignore) {}
|
|
}
|
|
showLeaderboardUnavailable();
|
|
};
|
|
request.ontimeout = showLeaderboardUnavailable;
|
|
request.onerror = showLeaderboardUnavailable;
|
|
request.send(null);
|
|
}
|
|
|
|
function showSlide(index, immediate) {
|
|
var next = screenshots[index % screenshots.length];
|
|
var nextLayer = state.activeLayer ? byId("slide-a") : byId("slide-b");
|
|
var oldLayer = state.activeLayer ? byId("slide-b") : byId("slide-a");
|
|
var image = new Image();
|
|
var url = "/assets/screenshots/" + encodeURIComponent(next.file);
|
|
|
|
image.onload = function () {
|
|
nextLayer.style.backgroundImage = "url('" + url + "')";
|
|
if (immediate) {
|
|
nextLayer.style.transition = "none";
|
|
}
|
|
nextLayer.className = "slide slide-visible";
|
|
oldLayer.className = "slide";
|
|
if (immediate) {
|
|
window.setTimeout(function () { nextLayer.style.transition = ""; }, 50);
|
|
}
|
|
state.activeLayer = state.activeLayer ? 0 : 1;
|
|
state.currentSlide = index % screenshots.length;
|
|
byId("photo-index").textContent = (state.currentSlide + 1 < 10 ? "0" : "") + (state.currentSlide + 1) + " / " + screenshots.length;
|
|
byId("photo-title").textContent = next.title;
|
|
};
|
|
image.src = url;
|
|
}
|
|
|
|
window.GameDetails = function (serverName, serverURL, mapName, maxPlayers, steamID, gamemode) {
|
|
var players = cleanInteger(maxPlayers);
|
|
byId("server-name").textContent = serverName || "brunner.ninja";
|
|
byId("map-title").textContent = titleCaseMap(mapName);
|
|
byId("gamemode").textContent = titleCaseMap(gamemode || "feedc0de_coop").toUpperCase();
|
|
byId("slots").textContent = players ? players + " PLAYER SLOTS" : "CO-OPERATIVE CAMPAIGNS";
|
|
byId("server-state").textContent = "SERVER CONTACT ESTABLISHED";
|
|
state.steamID = String(steamID || state.steamID);
|
|
setProgress(6);
|
|
requestProfile(state.steamID);
|
|
};
|
|
|
|
window.SetFilesTotal = function (total) {
|
|
state.filesTotal = Math.max(state.filesTotal, cleanInteger(total));
|
|
updateFileProgress();
|
|
};
|
|
|
|
window.SetFilesNeeded = function (needed) {
|
|
state.filesNeeded = cleanInteger(needed);
|
|
state.maxNeeded = Math.max(state.maxNeeded, state.filesNeeded);
|
|
if (state.filesNeeded > 0) setStage(1);
|
|
updateFileProgress();
|
|
};
|
|
|
|
window.DownloadingFile = function (fileName) {
|
|
fileName = String(fileName || "Preparing content").replace(/\\/g, "/");
|
|
if (!state.filesStarted[fileName]) {
|
|
state.filesStarted[fileName] = true;
|
|
state.startedCount += 1;
|
|
}
|
|
byId("phase-label").textContent = sourceForFile(fileName);
|
|
byId("status-text").textContent = "Receiving required content";
|
|
byId("file-name").textContent = fileName;
|
|
setStage(sourceForFile(fileName) === "WORKSHOP CONTENT" ? 1 : 2);
|
|
updateFileProgress();
|
|
};
|
|
|
|
window.SetStatusChanged = function (status) {
|
|
var newStage;
|
|
status = String(status || "Working…").replace(/^\s+|\s+$/g, "");
|
|
newStage = stageForStatus(status);
|
|
setStage(newStage);
|
|
byId("status-text").textContent = status;
|
|
byId("phase-label").textContent = newStage === 3 ? "PREPARING YOUR SESSION" : (newStage === 2 ? "SERVER CONTENT" : (newStage === 1 ? "WORKSHOP CONTENT" : "JOINING SERVER"));
|
|
if (newStage === 3) byId("file-name").textContent = "Downloads complete — initializing game code";
|
|
setProgress(progressForStatus(status, newStage));
|
|
};
|
|
|
|
function initialize() {
|
|
var map = queryValue("map");
|
|
var steamID = queryValue("steamid");
|
|
var seed = 0;
|
|
var i;
|
|
|
|
for (i = 0; i < (map + steamID).length; i += 1) seed += (map + steamID).charCodeAt(i);
|
|
state.currentSlide = seed % screenshots.length;
|
|
byId("tip").textContent = tips[seed % tips.length];
|
|
if (map) byId("map-title").textContent = titleCaseMap(map);
|
|
if (steamID) {
|
|
state.steamID = steamID;
|
|
requestProfile(steamID);
|
|
}
|
|
requestLeaderboard();
|
|
showSlide(state.currentSlide, true);
|
|
window.setInterval(function () { showSlide(state.currentSlide + 1, false); }, 8000);
|
|
|
|
if (queryValue("demo") === "1") {
|
|
window.GameDetails("brunner.ninja", window.location.href, map || "tbr_coop_thousand_antlions_v3b", 16, steamID || "76561198171048231", "feedc0de_coop");
|
|
window.SetFilesTotal(148);
|
|
window.SetFilesNeeded(37);
|
|
window.SetStatusChanged("Downloading Workshop content");
|
|
window.setTimeout(function () { window.DownloadingFile("workshop/706104732/campaign_content.gma"); }, 500);
|
|
}
|
|
}
|
|
|
|
initialize();
|
|
}());
|