+
+
+
+
${player.kills}
+
+
${player.deaths}
+
+
${player.deaths == 0 ? player.kills.toFixed(2) : (player.kills / player.deaths).toFixed(2)}
+
+
${ player.playTime == 0 ? '—' : Math.round(player.score / (player.playTime / 60))}
+
`);
+ });
+
+ $('.player-data-left').delay(1000).animate({opacity: 1}, 500);
+ $('.player-data-right').delay(1000).animate({opacity: 1}, 500);
+}
+
+const stateInfo = {
+ canvas: $('#map_canvas'),
+ ctx: $('#map_canvas')[0].getContext('2d'),
+ updateFrequency: 750,
+ updateFrameTimeDeviation: 0,
+ forwardDistance: undefined,
+ fovWidth: undefined,
+ mapInfo: undefined,
+ mapScaler: undefined,
+ deathIcons: {},
+ deathIconTime: 4000
+};
+
+function updateRadarData() {
+ $.getJSON(radarDataUrl, function (_radarItem) {
+ newRadarData = _radarItem;
+ });
+
+
+ $.getJSON(mapDataUrl, function (_map) {
+ stateInfo.mapInfo = _map
+ });
+
+ $.each(newRadarData, function (index, value) {
+ if (previousRadarData != undefined && index < previousRadarData.length) {
+
+ let previous = previousRadarData[index];
+
+ // this happens when the player has first joined and we haven't gotten two snapshots yet
+ if (value == null) {
+ return;
+ }
+
+ if (previous == null) {
+ previous = value;
+ }
+
+ // we don't want to treat a disconnected player snapshot as the previous
+ else if (previous.guid == value.guid) {
+ value.previous = previous;
+ }
+
+ // we haven't gotten a new item, it's just the old one again
+ if (previous.id === value.id) {
+ value.animationTime = previous.animationTime;
+ value.previous = value;
+ }
+
+ // they died between this snapshot and last so we wanna setup the death icon
+ if (!value.isAlive && previous.isAlive) {
+ stateInfo.deathIcons[value.guid] = {
+ animationTime: now,
+ location: value.location
+ };
+ }
+
+ // they respawned between this snapshot and last so we don't want to show wherever the were specating from
+ else if (value.isAlive && !previous.isAlive) {
+ value.previous = value;
+ }
+ }});
+
+ // we switch out the items to
+ previousRadarData = newRadarData;
+
+ $('#map_name').html(stateInfo.mapInfo.alias);
+ $('#map_list').css('background-image', `url(../images/radar/minimaps/compass_map_${stateInfo.mapInfo.name}@('@')2x.jpg)`);
+ checkCanvasSize(stateInfo.canvas, stateInfo.ctx, $('#map_list'), stateInfo.mapInfo);
+ updatePlayerData();
+}
+
+function updateMap() {
+ let ctx = stateInfo.ctx;
+
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ now = performance.now();
+
+ $.each(previousRadarData, function (index, value) {
+ if (value == null) {
+ return;
+ }
+
+ if (value.previous == null) {
+ value.previous = value;
+ }
+
+ // this indicates we got a new snapshot to work with so we set the time based off the previous
+ // frame deviation to have minimal interpolation skipping
+ if (value.animationTime === undefined) {
+ value.animationTime = now - stateInfo.updateFrameTimeDeviation;
+ }
+
+ if (!value.isAlive) {
+ return;
+ }
+
+ const elapsedFrameTime = now - value.animationTime;
+ const completionPercent = elapsedFrameTime / stateInfo.updateFrequency;
+
+ // certain maps like estate have an off center axis of origin, so we need to account for that
+ let rotatedPreviousLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.previous.location.x, value.previous.location.y, stateInfo.mapInfo.rotation);
+ let rotatedCurrentLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.location.x, value.location.y, stateInfo.mapInfo.rotation);
+
+ const startX = ((stateInfo.mapInfo.maxLeft - rotatedPreviousLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
+ const startY = ((stateInfo.mapInfo.maxTop - rotatedPreviousLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
+
+ const endX = ((stateInfo.mapInfo.maxLeft - rotatedCurrentLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
+ const endY = ((stateInfo.mapInfo.maxTop - rotatedCurrentLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
+
+ let teamColor = value.team == 'allies' ? 'rgb(0, 122, 204, 1)' : 'rgb(255, 69, 69)';
+ let fovColor = value.team == 'allies' ? 'rgba(0, 122, 204, 0.2)' : 'rgba(255, 69, 69, 0.2)';
+
+ // this takes care of moving past the roll-over point of yaw/pitch (ie 360->0)
+ const rollAngleFix = fixRollAngles(value.previous.radianAngles, value.radianAngles);
+
+ const radianLerpX = lerp(value.previous.radianAngles.x, rollAngleFix.x, completionPercent);
+ const radianLerpY = lerp(value.previous.radianAngles.y, rollAngleFix.y, completionPercent);
+
+ // this is some jankiness to get the fov to point the right direction
+ let firstVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX + toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
+ let secondVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX - toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
+
+ let currentX = lerp(startX, endX, completionPercent);
+ let currentY = lerp(startY, endY, completionPercent);
+
+ // we need to calculate the distance from the center of the map so we can scale if necessary
+ let centerX = ((stateInfo.mapInfo.maxLeft - stateInfo.mapInfo.centerY) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
+ let centerY = ((stateInfo.mapInfo.maxTop - stateInfo.mapInfo.centerX) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
+
+ // reuse lerp to scale the pixel to map ratio
+ currentX = lerp(centerX, currentX, stateInfo.mapInfo.scaler);
+ currentY = lerp(centerY, currentY, stateInfo.mapInfo.scaler);
+
+ drawCircle(ctx, currentX, currentY, teamColor);
+ drawTriangle(ctx,
+ { x: currentX, y: currentY },
+ { x: currentX + firstVertex.x, y: currentY + firstVertex.y },
+ { x: currentX + secondVertex.x, y: currentY + secondVertex.y },
+ fovColor);
+ drawText(ctx, currentX, currentY - (textOffset * stateInfo.imageScaler), value.name, 16, 'white', teamColor, 'center')
+ });
+
+ const completedIcons = [];
+
+ for (let key in stateInfo.deathIcons) {
+ const icon = stateInfo.deathIcons[key];
+
+ const x = ((stateInfo.mapInfo.maxLeft - icon.location.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
+ const y = ((stateInfo.mapInfo.maxTop - icon.location.x) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
+
+ const elapsedFrameTime = now - icon.animationTime;
+ const completionPercent = elapsedFrameTime / stateInfo.deathIconTime;
+ const opacity = easeLerp(1, 0, completionPercent);
+
+ drawImage(stateInfo.ctx, 'hud_death', x, y, opacity);
+
+ if (completionPercent >= 1) {
+ completedIcons.push(key);
+ }
+ }
+
+ for (let i = 0; i < completedIcons.length; i++) {
+ delete stateInfo.deathIcons[completedIcons[i]];
+ }
+
+ window.requestAnimationFrame(updateMap);
+}
+
+$(document).ready(function () {
+ if ($('#map_canvas').length === 0) {
+ return;
+ }
+ $.getJSON(radarDataUrl, function (_map) {
+ stateInfo.mapInfo = _map;
+ updateRadarData();
+ setInterval(updateRadarData, stateInfo.updateFrequency);
+ window.requestAnimationFrame(updateMap);
+ });
+})