diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index 874defcd7..aee4a1873 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -23,7 +23,7 @@ - + diff --git a/Plugins/LiveRadar/Views/Radar/Index.cshtml b/Plugins/LiveRadar/Views/Radar/Index.cshtml index a4d697071..9874860ba 100644 --- a/Plugins/LiveRadar/Views/Radar/Index.cshtml +++ b/Plugins/LiveRadar/Views/Radar/Index.cshtml @@ -41,430 +41,13 @@ - @section scripts { - - + + + } diff --git a/WebfrontCore/bundleconfig.json b/WebfrontCore/bundleconfig.json index b38abb570..c60165ef0 100644 --- a/WebfrontCore/bundleconfig.json +++ b/WebfrontCore/bundleconfig.json @@ -29,7 +29,8 @@ "wwwroot/js/stats.js", "wwwroot/js/scoreboard.js", "wwwroot/js/configuration.js", - "wwwroot/js/advanced_stats.js" + "wwwroot/js/advanced_stats.js", + "wwwroot/js/liveradar.js" ], // Optionally specify minification options "minify": { diff --git a/WebfrontCore/wwwroot/js/liveradar.js b/WebfrontCore/wwwroot/js/liveradar.js new file mode 100644 index 000000000..82ba7ea6a --- /dev/null +++ b/WebfrontCore/wwwroot/js/liveradar.js @@ -0,0 +1,423 @@ +const textOffset = 15; +let previousRadarData = undefined; +let newRadarData = undefined; + +/************************ + * IW4 * + * **********************/ +const weapons = {}; +weapons["ak47"] = "ak47"; +weapons["ak47classic"] = "icon_ak47_classic"; +weapons["ak74u"] = "akd74u"; +weapons["m16"] = "m16a4"; +weapons["m4"] = "m4carbine"; +weapons["fn2000"] = "fn2000"; +weapons["masada"] = "masada"; +weapons["famas"] = "famas"; +weapons["fal"] = "fnfal"; +weapons["scar"] = "scar_h"; +weapons["tavor"] = "tavor"; + +weapons["mp5k"] = "mp5k"; +weapons["uzi"] = "mini_uzi"; +weapons["p90"] = "p90"; +weapons["kriss"] = "kriss"; +weapons["ump45"] = "ump45"; + +weapons["rpd"] = "rpd"; +weapons["sa80"] = "sa80_lmg"; +weapons["mg4"] = "mg4"; +weapons["m240"] = "m240"; +weapons["aug"] = "steyr"; + +weapons["barrett"] = "barrett50cal"; +weapons["wa2000"] = "wa2000"; +weapons["m21"] = "m14ebr"; +weapons["cheytac"] = "cheytac"; +weapons["dragunov"] = "dragunovsvd"; + +weapons["beretta"] = "m9beretta"; +weapons["usp"] = "usp_45"; +weapons["deserteagle"] = "desert_eagle"; +weapons["deserteaglegold"] = "desert_eagle_gold"; +weapons["desert"] +weapons["coltanaconda"] = "colt_anaconda"; + +weapons["tmp"] = "mp9"; +weapons["glock"] = "glock"; +weapons["beretta393"] = "beretta393"; +weapons["pp2000"] = "pp2000"; + +weapons["ranger"] = "sawed_off"; +weapons["model1887"] = "model1887"; +weapons["striker"] = "striker"; +weapons["aa12"] = "aa12"; +weapons["m1014"] = "benelli_m4"; +weapons["spas12"] = "spas12"; + +weapons["m79"] = "m79"; +weapons["rpg"] = "rpg"; +weapons["at4"] = "at4"; +weapons["stinger"] = "stinger"; +weapons["javelin"] = "javelin"; + +weapons["m40a3"] = "m40a3"; +weapons["none"] = "neutral"; +weapons["riotshield"] = "riot_shield"; +weapons["peacekeeper"] = "peacekeeper"; + +function drawCircle(context, x, y, color) { + context.beginPath(); + context.arc(x, y, 6 * stateInfo.imageScaler, 0, 2 * Math.PI, false); + context.fillStyle = color; + context.fill(); + context.lineWidth = 0.5; + context.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + context.closePath(); + context.stroke(); +} + +function drawLine(context, x1, y1, x2, y2, color) { + context.beginPath(); + context.lineWidth = '3'; + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.closePath(); + context.stroke(); +} + +function drawTriangle(context, v1, v2, v3, color) { + context.beginPath(); + context.moveTo(v1.x, v1.y); + context.lineTo(v2.x, v2.y); + context.lineTo(v3.x, v3.y); + context.closePath(); + context.fillStyle = color; + context.fill(); +} + +function drawText(context, x, y, text, size, fillColor, strokeColor, alignment = 'left') { + context.beginPath(); + context.save(); + context.font = `bold ${Math.max(12, size * stateInfo.imageScaler)}px courier new`; + context.fillStyle = fillColor; + context.shadowColor = strokeColor; + context.shadowBlur = 4; + context.textAlign = alignment; + context.fillText(text, x, y); + context.restore(); + context.closePath(); +} + +function drawImage(context, imgSelector, x, y, alpha = 1) { + context.save(); + context.globalAlpha = alpha; + context.drawImage(document.getElementById(imgSelector), x - (15 * stateInfo.imageScaler), y - (15 * stateInfo.imageScaler), 32 * stateInfo.imageScaler, 32 * stateInfo.imageScaler); + context.globalAlpha = 1; + context.restore(); +} + +function checkCanvasSize(canvas, context, minimap, map) { + + let width = Math.round(minimap.width()); + if (Math.round(context.canvas.width) != width) { + + canvas.width(width); + canvas.height(width); + + context.canvas.height = width; + context.canvas.width = context.canvas.height; + } + + stateInfo.imageScaler = (stateInfo.canvas.width() / 1024) + stateInfo.mapScalerX = (((stateInfo.mapInfo.right * stateInfo.imageScaler) - (stateInfo.mapInfo.left * stateInfo.imageScaler)) / stateInfo.mapInfo.width); + stateInfo.mapScalerY = (((stateInfo.mapInfo.bottom * stateInfo.imageScaler) - (stateInfo.mapInfo.top * stateInfo.imageScaler)) / stateInfo.mapInfo.height); + stateInfo.mapScaler = (stateInfo.mapScalerX + stateInfo.mapScalerY) / 2 + + stateInfo.forwardDistance = 500.0; + stateInfo.fovWidth = 40; +} + +function calculateViewPosition(x, y, distance) { + let nx = Math.cos(x) * Math.cos(y); + let ny = Math.sin(x) * Math.cos(y); + let nz = Math.sin(360.0 - y); + + return { x: (nx * distance) * stateInfo.mapScaler, y: (ny * distance) * stateInfo.mapScaler, z: (nz * distance) * stateInfo.mapScaler }; +} + +function lerp(start, end, complete) { + return (1 - complete) * start + complete * end; +} + +function easeLerp(start, end, t) { + let t2 = (1 - Math.cos(t * Math.PI)) / 2; + + return (start * (1-t2) + end * t2); +} + +function fixRollAngles(oldAngles, newAngles) { + let newX = newAngles.x; + let newY = newAngles.y; + + let angleDifferenceX = (oldAngles.x - newAngles.x); + + if (angleDifferenceX > Math.PI) { + newX = oldAngles.x + (Math.PI * 2) - angleDifferenceX; + } + + else if (Math.abs(newAngles.x - oldAngles.x) > Math.PI) { + newX = newAngles.x - (Math.PI * 2); + } + + let angleDifferenceY = (oldAngles.y - newAngles.y); + + if (angleDifferenceY > Math.PI) { + newY = oldAngles.y + (Math.PI * 2) - angleDifferenceY; + } + + else if (Math.abs(newAngles.y - oldAngles.y) > Math.PI) { + newY = newAngles.y - (Math.PI * 2); + } + + return { x: newX, y: newY }; +} + +function toRadians(deg) { + return deg * Math.PI / 180.0; +} + +function rotate(cx, cy, x, y, angle) { + var radians = (Math.PI / 180) * angle, + cos = Math.cos(radians), + sin = Math.sin(radians), + nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, + ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; + return { + x: nx, + y: ny + }; +} + +function weaponImageForWeapon(weapon) { + let name = weapon.split('_')[0]; + if (weapons[name] === undefined) { + console.log(name); + name = "none"; + } + + return `../images/radar/hud_weapons/hud_${weapons[name]}.png`; +} + +function updatePlayerData() { + $('.player-data-left').html(''); + $('.player-data-right').html(''); + + $.each(newRadarData, function (index, player) { + if (player == null) { + return; + } + + let column = player.team === 'allies' ? $('.player-data-left') : $('.player-data-right'); + column.append(`
+
${player.name}
+
+
+
+
+
+
+
+
${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); + }); +})