Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
190 changes: 104 additions & 86 deletions assets/js/system-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,25 @@
}
}

window.openOperationModal = (opId) => {
const uc = window.globalIntel ? window.globalIntel.unitcommander : null;
if (!uc || !uc.campaigns) return;
const op = uc.campaigns.find((c) => c.id === opId);
if (!op) return;

const modal = document.getElementById('operation-modal');
if (!modal) return;

const img = document.getElementById('modal-op-image');
function updateModalContent(op) {
const title = document.getElementById('modal-op-title');
const date = document.getElementById('modal-op-date');
const brief = document.getElementById('modal-op-brief');
const map = document.getElementById('modal-op-map');
const img = document.getElementById('modal-op-image');

if (title) title.innerText = op.campaignName;
if (date)
date.innerText = `COMMENCED: ${new Date(op.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' }).toUpperCase()}`;
if (date) {
const formattedDate = new Date(op.created_at).toLocaleDateString(
'en-GB',
{
day: '2-digit',
month: 'long',
year: 'numeric',
},
);
date.innerText = `COMMENCED: ${formattedDate.toUpperCase()}`;
}
if (brief) brief.innerText = op.brief || 'NO_DATA_RECOVERED';
if (map) map.innerText = `THEATER: ${op.map || 'CLASSIFIED'}`;

Expand All @@ -117,6 +118,18 @@
img.classList.add('hidden');
}
}
}

window.openOperationModal = (opId) => {
const uc = window.globalIntel ? window.globalIntel.unitcommander : null;
if (!uc || !uc.campaigns) return;
const op = uc.campaigns.find((c) => c.id === opId);
if (!op) return;

const modal = document.getElementById('operation-modal');
if (!modal) return;

updateModalContent(op);

modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
Expand All @@ -132,33 +145,41 @@
if (e.key === 'Escape') window.closeOperationModal();
});

function updateBattlemetricsUI() {
function updateStatusIndicator(source, maxCapacity) {
const statusText = document.getElementById('status-text');
const statusIndicator = document.getElementById('status-indicator');
const playerCount = document.getElementById('player-count');

const source = window.globalIntel ? window.globalIntel.arma : null;
const maxCapacity = source ? source.maxPlayers || 40 : 40;
if (!statusText) return;

if (statusText) {
if (source && source.status === 'online') {
statusText.innerText = 'STATION_ACTIVE';
statusText.className =
'text-[8px] font-black text-mod-green tracking-widest uppercase font-mono';
if (statusIndicator)
statusIndicator.className = `w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.4)]`;
if (playerCount)
playerCount.innerText = `${source.players}/${maxCapacity} DEPLOYED`;
} else {
statusText.innerText = 'LINK_SEVERED';
statusText.className =
'text-[8px] font-black text-red-600 tracking-widest uppercase font-mono';
if (statusIndicator)
statusIndicator.className =
'w-1.5 h-1.5 bg-red-600 rounded-full opacity-40';
if (playerCount) playerCount.innerText = 'OFFLINE';
if (source && source.status === 'online') {
statusText.innerText = 'STATION_ACTIVE';
statusText.className =
'text-[8px] font-black text-mod-green tracking-widest uppercase font-mono';
if (statusIndicator) {
statusIndicator.className =
'w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.4)]';
}
if (playerCount) {
playerCount.innerText = `${source.players}/${maxCapacity} DEPLOYED`;
}
} else {
statusText.innerText = 'LINK_SEVERED';
statusText.className =
'text-[8px] font-black text-red-600 tracking-widest uppercase font-mono';
if (statusIndicator) {
statusIndicator.className =
'w-1.5 h-1.5 bg-red-600 rounded-full opacity-40';
}
if (playerCount) playerCount.innerText = 'OFFLINE';
}
}

function updateBattlemetricsUI() {
const source = window.globalIntel ? window.globalIntel.arma : null;
const maxCapacity = source ? source.maxPlayers || 40 : 40;

updateStatusIndicator(source, maxCapacity);

const containers = document.querySelectorAll('#battlemetrics-graph');
if (containers.length > 0) {
Expand Down Expand Up @@ -188,17 +209,36 @@
}
}

function getGraphData(data, startTime, safeMax, height, width, timeRange) {
const points = data
.map((d) => ({
v: d.attributes.value === 255 ? 0 : d.attributes.value,
t: d.attributes.timestamp,
}))
.filter((p) => p.v < 500 && new Date(p.t).getTime() >= startTime);

points.sort((a, b) => new Date(a.t) - new Date(b.t));

const getX = (t) => {
if (timeRange === 0) return 0;
const x = ((new Date(t).getTime() - startTime) / timeRange) * width;
return Math.max(0, Math.min(width, x));
};

const getY = (v) => {
const y = height - 4 - (v / safeMax) * (height - 8);
return Math.max(2, Math.min(height - 2, y + 2));
};

return { points, getX, getY };
}

function renderBattlemetricsGraph(data, container, maxVal = 40) {
if (!container) return;

const width = container.clientWidth;
const height = container.clientHeight || 100;
const safeMax = Math.max(maxVal, 10);

if (width === 0) {
// Avoid infinite recursion if container is hidden
return;
}
if (width === 0) return;

const range = window.currentBattlemetricsRange || 'today';
const nowTime = Date.now();
Expand All @@ -209,43 +249,24 @@
? 7 * 24 * 3600000
: 24 * 3600000;
const startTime = nowTime - lookback;
const endTime = nowTime;
const timeRange = endTime - startTime;

const getX = (t) => {
if (timeRange === 0) return 0;
const x = ((new Date(t).getTime() - startTime) / timeRange) * width;
return Math.max(0, Math.min(width, x));
};

const getY = (v) => {
const y = height - 4 - (v / safeMax) * (height - 8);
return Math.max(2, Math.min(height - 2, y + 2));
};
const timeRange = nowTime - startTime;
const safeMax = Math.max(maxVal, 10);

let points = [];
if (data && data.length > 0) {
points = data
.map((d) => ({
v: d.attributes.value === 255 ? 0 : d.attributes.value,
t: d.attributes.timestamp,
}))
.filter((p) => p.v < 500 && new Date(p.t).getTime() >= startTime);

points.sort((a, b) => new Date(a.t) - new Date(b.t));
}
const { points, getX, getY } = getGraphData(
data,
startTime,
safeMax,
height,
width,
timeRange,
);

if (points.length === 0) {
container.innerHTML = `<div class="h-full flex items-center justify-center font-mono text-[8px] text-neutral-800 uppercase tracking-[0.4em] ">No_Telemetry_Detected</div>`;
container.innerHTML =
'<div class="h-full flex items-center justify-center font-mono text-[8px] text-neutral-800 uppercase tracking-[0.4em] ">No_Telemetry_Detected</div>';
return;
}

const firstX = getX(points[0].t);
const firstY = getY(points[0].v);

let pathData = `M ${firstX} ${firstY}`;
let areaPath = `M ${firstX} ${height} L ${firstX} ${firstY}`;

const baselineY = getY(0);
const maxGap =
range === 'month'
Expand All @@ -254,32 +275,29 @@
? 3 * 3600000
: 65 * 60000;

let pathData = `M ${getX(points[0].t)} ${getY(points[0].v)}`;
let areaPath = `M ${getX(points[0].t)} ${height} L ${getX(points[0].t)} ${getY(points[0].v)}`;

points.forEach((p, index) => {
if (index === 0) return;
const curX = getX(p.t);
const curY = getY(p.v);
const prevP = points[index - 1];

if (index > 0) {
const prevP = points[index - 1];
const timeDiff = new Date(p.t).getTime() - new Date(prevP.t).getTime();

if (timeDiff > maxGap) {
pathData += ` L ${getX(prevP.t)} ${baselineY} M ${getX(p.t)} ${baselineY} L ${curX} ${curY}`;
areaPath += ` L ${getX(prevP.t)} ${baselineY} L ${getX(p.t)} ${baselineY} L ${curX} ${curY}`;
} else {
pathData += ` L ${curX} ${curY}`;
areaPath += ` L ${curX} ${curY}`;
}
if (new Date(p.t).getTime() - new Date(prevP.t).getTime() > maxGap) {
pathData += ` L ${getX(prevP.t)} ${baselineY} M ${getX(p.t)} ${baselineY} L ${curX} ${curY}`;
areaPath += ` L ${getX(prevP.t)} ${baselineY} L ${getX(p.t)} ${baselineY} L ${curX} ${curY}`;
} else {
pathData += ` L ${curX} ${curY}`;
areaPath += ` L ${curX} ${curY}`;
}
});

const lastP = points[points.length - 1];
const lastY = getY(lastP.v);

if (endTime - new Date(lastP.t).getTime() > 10 * 60000) {
pathData += ` L ${width} ${lastY}`;
areaPath += ` L ${width} ${lastY}`;
if (nowTime - new Date(lastP.t).getTime() > 10 * 60000) {
pathData += ` L ${width} ${getY(lastP.v)}`;
areaPath += ` L ${width} ${getY(lastP.v)}`;
}

areaPath += ` L ${width} ${height} Z`;

container.innerHTML = `
Expand Down
2 changes: 1 addition & 1 deletion scripts/fetch-external-data.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import https from 'node:https';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { GameDig as Gamedig } from 'gamedig';
import 'dotenv/config';
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-stats.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
Expand Down
Loading
Loading