@model IEnumerable<long> <style> .progress { border-radius: 0 !important; } .player-stat-icon { height: 1.5rem; width: 1.5rem; background-size: 1.5rem 1.5rem; } </style> <div class="row p-0 ml-auto mr-auto mb-4"> <div class="col-12 col-xl-10 p-0 ml-auto mr-auto p-0 pl-lg-3 pr-lg-3 "> <ul class="nav nav-tabs border-top border-bottom nav-fill" role="tablist"> @foreach (SharedLibraryCore.Dtos.ServerInfo server in ViewBag.Servers) { <li class="nav-item"> <a asp-controller="Radar" asp-action="Index" asp-route-serverId="@server.ID" class="nav-link @(server.ID == ViewBag.ActiveServerId ? "active": "")" aria-selected="@(server.ID == ViewBag.ActiveServerId ? "true": "false")"><color-code value="@server.Name" allow="@ViewBag.EnableColorCodes"></color-code></a> </li> } </ul> </div> </div> <div class="row p-0 ml-auto mr-auto col-12 col-xl-10"> <div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-right text-center player-data-left" style="opacity: 0;"> </div> <div class="pl-0 pr-0 pl-lg-3 pr-lg-3 col-lg-6 col-12 pb-4"> <div id="map_name" class="h4 text-center pb-2 pt-2 mb-0 bg-primary">—</div> <div id="map_list" style="background-size:cover; padding-bottom: 100% !important;"> <canvas id="map_canvas" style="position:absolute;"></canvas> </div> </div> <div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-left text-center player-data-right" style="opacity: 0;"> </div> </div> <!-- images used by canvas --> <img class="hide" id="hud_death" src="~/images/radar/death.png" /> @section scripts { <script defer="defer"> 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"] = "hud_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 = index % 2 == 0 ? $('.player-data-left') : $('.player-data-right'); column.append(`<div class="progress" style="height: 1.5rem; background-color: transparent;"> <div style="position: absolute; font-size: 1rem; left: 1.5rem;">${player.name}</div> <div class="progress-bar bg-success" role="progressbar" style="min-width: 0px; width: ${player.health}%" aria-valuenow="${player.health}" aria-valuemin="0" aria-valuemax="100"></div> <div class="progress-bar bg-danger" role="progressbar" style="min-width: 0px; border-right: 0px; width: ${100 - player.health}%" aria-valuenow="${100 - player.health}" aria-valuemin="0" aria-valuemax="100"></div> </div> <div class="d-flex flex-row flex-wrap p-2 mb-4 bg-dark border-bottom"> <div style="width: 3rem; height: 1.5rem; background-image:url(${weaponImageForWeapon(player.weapon)}); background-size: 3rem 1.5rem;" class="mr-auto text-left"> </div> <div class="player-stat-icon" style="background-image:url('/images/radar/kills.png')"></div> <div class="pr-2">${player.kills}</div> <div class="player-stat-icon" style="background-image:url('/images/radar/death.png')"></div> <div class="pr-3">${player.deaths}</div> <span class="align-self-center oi oi-target pr-1"></span> <div class="pr-3 ">${player.deaths == 0 ? player.kills.toFixed(2) : (player.kills / player.deaths).toFixed(2)}</div> <span class="align-self-center oi oi-graph pr-1"></span> <div>${ player.playTime == 0 ? '—' : Math.round(player.score / (player.playTime / 60))}</div> </div>`); }); $('.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('@Url.Action("Data", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_radarItem) { newRadarData = _radarItem; }); $.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', 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 () { $.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_map) { stateInfo.mapInfo = _map; updateRadarData(); setInterval(updateRadarData, stateInfo.updateFrequency); window.requestAnimationFrame(updateMap); }); }) </script> }