diff --git a/.gitignore b/.gitignore index 62437df..a40b45d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules config.override.json5 +libraryfolders.vdf diff --git a/README.md b/README.md index 4d3c8b4..4244c36 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Advisories are automatically detected events that the observer might want to switch to. To make switching to this event easier, the observer slot number is displayed next to an icon noting the type of advisory. -The observer should still make his own judgement of the situation. +The observer should still make his own judgment of the situation. All possible advisories are (with increasing priority): @@ -54,10 +54,10 @@ Running without window borders enables it to dedicate as much space as possible 2. Copy the `gamestate_integration_boltobserv.cfg` file from the .zip to your CSGO config folder (the same folder you'd put an `autoexec.cfg`). For most installations this should be found at `C:\Program Files\Steam\steamapps\common\Counter-Strike Global Offensive\csgo\cfg`. 3. You're done! Start the `Boltobserv.exe` file in the unzipped folder. Boltobserv should now automatically connect to CSGO when it's launched. -Please report any bugs or feature requests here on Github. +Please report any bugs or feature requests here on Github. -## License +## License -This project is licensed under GPL-3. In short, this means that all changes you make to this project need to be made open source (among other things). Commercial use is encouraged, as is distribution. +This project is licensed under GPL-3. In short, this means that all changes you make to this project need to be made open source (among other things). Commercial use is encouraged, as is distribution. The paragraph above is not legally binding. See the LICENSE file for the full license. diff --git a/config.json5 b/config.json5 index bb977db..6ce853b 100644 --- a/config.json5 +++ b/config.json5 @@ -7,7 +7,7 @@ // Sends information about the grenades thrown this round to a server // This information in only used to train a model to predict grenade // landings, no personal information is send or logged - "nadeCollection": false, + "nadeCollection": true, // Settings related to the Boltobserv window "window": { @@ -17,6 +17,9 @@ // Make the background of the window transparent "transparent": false, + // Will disable GPU rendering, helps capture the window in programs like OBS + "disableGpu": false, + // The default shape and position of the window "defaultSize": { // The height and width in pixels @@ -45,14 +48,17 @@ // Settings related to the CSGO game "game": { // Seconds of inactivity before considering a connection to the game client as lost - // Set to -1 to never timout + // Set to -1 to never timeout "connectionTimout": 30, // The port GSI will try to connect to - "networkPort": 36363 + "networkPort": 36363, + + // Tries to detect the CSGO game on the machine and prompts to install the CFG file if it hasn't already + "installCfg": true }, - // Settings for automatically zomming in on alive players on the map + // Settings for automatically zooming in on alive players on the map "autozoom": { // Enable or disable autozoom "enable": false, @@ -64,7 +70,7 @@ "padding": 0.3 }, - // Settings that should not be used in normal oparation, but help to find issues + // Settings that should not be used in normal operation, but help to find issues "debug": { // Draw red squares over bombsite locations "drawBombsites": false, diff --git a/detectcfg.js b/detectcfg.js new file mode 100644 index 0000000..75f4672 --- /dev/null +++ b/detectcfg.js @@ -0,0 +1,71 @@ +const fs = require("fs") +const path = require("path") + +const steamPaths = [ + // Default Windows install path + path.join("C:", "Program Files (x86)", "Steam", "steamapps"), + // For development + path.join(__dirname) +] + +module.exports = { + found: [], + search: () => { + let exp = /"\d"\s*"(.*)"/g + let commonPaths = [] + + for (let steamPath of steamPaths) { + let vdfPath = path.join(steamPath, "libraryfolders.vdf") + + commonPaths.push(path.join(steamPath, "common")) + + if (fs.existsSync(vdfPath)) { + let vdfContent = fs.readFileSync(vdfPath, "utf8") + + let appPath = exp.exec(vdfContent) + + while (appPath != null) { + commonPaths.push(path.join(appPath[1], "steamapps", "common")) + appPath = exp.exec(vdfContent) + } + } + } + + for (let commonPath of commonPaths) { + let gamePath = path.join(commonPath, "Counter-Strike Global Offensive") + + if (fs.existsSync(gamePath)) { + console.info("Found installation in", gamePath) + + let configPath = path.join(gamePath, "csgo", "cfg", "gamestate_integration_boltobserv.cfg") + + if (fs.existsSync(configPath)) { + let foundHeader = fs.readFileSync(configPath, "utf8").split("\n")[0] + let ownHeader = fs.readFileSync(path.join(__dirname, "gamestate_integration_boltobserv.cfg"), "utf8").split("\n")[0] + + if (foundHeader != ownHeader) { + module.exports.found.push({ + type: "update", + path: gamePath + }) + } + } + else { + module.exports.found.push({ + type: "install", + path: gamePath + }) + } + } + } + }, + + install: (path) => { + // Need to reimport path because of electron shenanigans + let template = require("path").join(__dirname, "gamestate_integration_boltobserv.cfg") + let dest = require("path").join(path, "csgo", "cfg", "gamestate_integration_boltobserv.cfg") + fs.copyFileSync(template, dest) + + console.info("Installed config file as", dest) + } +} diff --git a/gamestate_integration_boltobserv.cfg b/gamestate_integration_boltobserv.cfg index 36b6cef..8225b4f 100644 --- a/gamestate_integration_boltobserv.cfg +++ b/gamestate_integration_boltobserv.cfg @@ -1,4 +1,4 @@ -"Boltobserv GSI" +"Boltobserv integration v2 | https://github.com/boltgolt/boltobserv" { "uri" "http://localhost:36363" "timeout" "0.1" diff --git a/html/map.html b/html/map.html index 0c41324..7c51d54 100644 --- a/html/map.html +++ b/html/map.html @@ -17,6 +17,8 @@
+
+
1
2
3
@@ -43,7 +45,7 @@
diff --git a/html/waiting.html b/html/waiting.html index 096ae59..8fa88a0 100644 --- a/html/waiting.html +++ b/html/waiting.html @@ -8,7 +8,12 @@ Boltobserv - Waiting for CSGO -
+ +
+ +
+
+
Waiting for CSGO to connect... @@ -17,7 +22,6 @@ version 0.0.1 -
diff --git a/http.js b/http.js index bdc5706..faf2336 100644 --- a/http.js +++ b/http.js @@ -31,7 +31,9 @@ let server = http.createServer(function(req, res) { } if (game.player) { - connObject.player = game.player.name + if (game.player.activity != "playing") { + connObject.player = game.player.name + } } process.send({ @@ -51,47 +53,49 @@ let server = http.createServer(function(req, res) { } } - // console.log(JSON.stringify(game)) if (game.allplayers) { let playerArr = [] - let context = { - defusing: false - } - if (game.phase_countdowns) { - if (game.phase_countdowns.phase == "defuse") { - context.defusing = true - } - } + for (let id in game.allplayers) { + if (!Number.isInteger(game.allplayers[id].observer_slot)) continue - for (let i in game.allplayers) { - if (!Number.isInteger(game.allplayers[i].observer_slot)) continue - - const nadeIDs = ["weapon_smokegrenade", "weapon_flashbang", "weapon_hegrenade", "weapon_incgrenade", "weapon_smokegrenade"] - let player = game.allplayers[i] + let player = game.allplayers[id] let pos = player.position.split(", ") + let angle = 0 let hasBomb = false let bombActive = false - let nadeActive = false + let isActive = false + let rawAngle = player.forward.split(", ") + + if (parseFloat(rawAngle[0]) > 0) { + angle = 90 + parseFloat(rawAngle[1]) * -1 * 90 + } + else { + angle = 270 + parseFloat(rawAngle[1]) * 90 + } + + if (game.player) { + if (game.player.observer_slot == player.observer_slot) { + isActive = true + } + } for (let t in player.weapons) { if (player.weapons[t].name == "weapon_c4") { hasBomb = true bombActive = player.weapons[t].state == "active" } - - if (player.weapons[t].state == "active" && nadeIDs.includes(player.weapons[t].name)) { - nadeActive = true - } } playerArr.push({ + id: id, num: player.observer_slot, team: player.team, alive: player.state.health > 0, + active: isActive, bomb: hasBomb, bombActive: bombActive, - nadeActive: nadeActive, + angle: angle, position: { x: parseFloat(pos[0]), y: parseFloat(pos[1]), @@ -103,7 +107,6 @@ let server = http.createServer(function(req, res) { process.send({ type: "players", data: { - context: context, players: playerArr } }) @@ -154,6 +157,23 @@ let server = http.createServer(function(req, res) { oldPhase = game.round.phase } } + + if (game.bomb) { + let pos = game.bomb.position.split(", ") + + process.send({ + type: "bomb", + data: { + state: game.bomb.state, + player: game.bomb.player, + position: { + x: parseFloat(pos[0]), + y: parseFloat(pos[1]), + z: parseFloat(pos[2]) + } + } + }) + } }) }) diff --git a/img/adv-bombA.png b/img/adv-bombA.png deleted file mode 100644 index 0fca639..0000000 Binary files a/img/adv-bombA.png and /dev/null differ diff --git a/img/adv-bombB.png b/img/adv-bombB.png deleted file mode 100644 index 541828e..0000000 Binary files a/img/adv-bombB.png and /dev/null differ diff --git a/img/adv-plant.png b/img/adv-plant.png new file mode 100644 index 0000000..4a89cbe Binary files /dev/null and b/img/adv-plant.png differ diff --git a/img/bomb-defused.png b/img/bomb-defused.png new file mode 100644 index 0000000..2429c83 Binary files /dev/null and b/img/bomb-defused.png differ diff --git a/img/bomb-dropped.png b/img/bomb-dropped.png new file mode 100644 index 0000000..31cf156 Binary files /dev/null and b/img/bomb-dropped.png differ diff --git a/img/bomb-planted.png b/img/bomb-planted.png new file mode 100644 index 0000000..0d64aa7 Binary files /dev/null and b/img/bomb-planted.png differ diff --git a/index.js b/index.js index 147fa73..8267441 100644 --- a/index.js +++ b/index.js @@ -4,11 +4,12 @@ const child_process = require("child_process") const app = electron.app const config = require("./loadconfig")(true) +const detectcfg = require("./detectcfg") let hasMap = false let connTimeout = false -function createWindow () { +function createWindow() { let winConfig = { width: config.window.defaultSize.width, height: config.window.defaultSize.height, @@ -24,7 +25,8 @@ function createWindow () { nodeIntegration: true, webaudio: false, webgl: false, - backgroundThrottling: false + backgroundThrottling: false, + allowEval: false } } @@ -53,6 +55,8 @@ function createWindow () { win.loadFile("html/waiting.html") + if (config.game.installCfg) detectcfg.search() + let http = child_process.fork(`${__dirname}/http.js`) http.on("message", (message) => { @@ -84,6 +88,21 @@ function createWindow () { }, config.game.connectionTimout * 1000) } }) + + electron.ipcMain.on("reqInstall", (event) => { + event.sender.send("cfgInstall", detectcfg.found) + }) + + electron.ipcMain.on("install", (event, path) => { + detectcfg.install(path) + }) +} + +if (config.window.disableGpu) { + console.info("GPU disabled by config option") + + app.disableHardwareAcceleration() + app.commandLine.appendSwitch("disable-gpu") } app.on("ready", createWindow) diff --git a/maprenderer.js b/maprenderer.js deleted file mode 100644 index 25b61be..0000000 --- a/maprenderer.js +++ /dev/null @@ -1,331 +0,0 @@ -const fs = require("fs") -const path = require("path") -const JSON5 = require("json5") -const renderer = require("electron").ipcRenderer - -const config = require("./loadconfig")() - -let mapData = {} -let currentMap = "none" - -for (let playerElem of document.getElementsByClassName("player")) { - playerElem.style.transform = `scale(${config.radar.playerDotScale}) translate(-50%, -50%)` -} - -/** - * Convert in-game position units to radar percentages - * @param {float} pos In-game position - * @param {float} offset Map offest for the right dimension - * @return {float} Relative radar percentage - */ -function positionToPerc(pos, offset) { - // The position of the player in game, with the bottom left corner as origin (0,0) - let gamePosition = pos + offset - // The position of the player relative to an 1024x1014 pixel grid - let pixelPosition = gamePosition / mapData.resolution - // The position of the player as an percentage for any size - return pixelPosition / 1024 * 100 -} - -renderer.on("map", (event, map) => { - if (currentMap == map) return - - let metaPath = path.join(__dirname, "maps", map, "meta.json5") - - if (!fs.existsSync(metaPath)) { - document.getElementById("unknownMap").style.display = "flex" - document.getElementById("unknownMap").children[0].innerHTML = "Unsupported map " + map - return - } - - document.getElementById("unknownMap").style.display = "none" - - currentMap = map - document.title = "Boltobserv - " + map - document.getElementById("radar").src = `../maps/${map}/radar.png` - - mapData = JSON5.parse(fs.readFileSync(metaPath, "utf8")) - - if (config.radar.hideAdvisories) { - document.getElementById("advisory").style.display = "none" - } - else { - document.getElementById("advisory").style.left = mapData.advisoryPosition.x + "%" - document.getElementById("advisory").style.bottom = mapData.advisoryPosition.y + "%" - document.getElementById("advisory").style.display = "block" - } - - function drawSite(element, cords) { - element.style.display = "block" - - element.style.left = positionToPerc(cords.x1, mapData.offset.x) + "%" - element.style.bottom = positionToPerc(cords.y1, mapData.offset.y) + "%" - - // Get the height and with by getting the distance between the points and converting it to an percentage - element.style.width = ((cords.x2 - cords.x1) / mapData.resolution / 1024 * 100) + "%" - element.style.height = ((cords.y2 - cords.y1) / mapData.resolution / 1024 * 100) + "%" - } - - if (config.debug.drawBombsites) { - drawSite(document.getElementById("siteA"), mapData.bombsites.a) - drawSite(document.getElementById("siteB"), mapData.bombsites.b) - } -}) - -renderer.on("players", (event, data) => { - if (currentMap == "none") return - - function playerOnSite(cords, rect) { - return rect.x1 <= cords.x - && cords.x <= rect.x2 - && rect.y1 <= cords.y - && cords.y <= rect.y2 - } - - let advisory = { - "type": "none", - "player": "?" - } - - let playersOnSites = [] - let ctsAlive = [] - let tsAlive = [] - - for (let player of data.players) { - let playerElement = document.getElementById("player" + player.num) - let classes = ["player", player.team] - - if (!player.alive) { - classes.push("dead") - playerPos[player.num].lock = true - } - else { - if (player.team == "CT") { - ctsAlive.push(player) - } - else { - tsAlive.push(player) - } - } - - let onA = playerOnSite(player.position, mapData.bombsites.a) - let onB = playerOnSite(player.position, mapData.bombsites.b) - - if (onA || onB) { - playersOnSites.push(player) - - if (player.bombActive) { - advisory.type = "holdingbomb" + (onA ? "A" : "B") - advisory.player = player.num - } - } - - if (player.bomb) { - classes.push("bomb") - } - if (player.nadeActive) { - classes.push("nade") - } - - playerElement.className = classes.join(" ") - playerElement.style.display = "block" - - playerPos[player.num].x = positionToPerc(player.position.x, mapData.offset.x) - playerPos[player.num].y = positionToPerc(player.position.y, mapData.offset.y) - } - - if (ctsAlive.length == 1 && advisory.type == "none") { - advisory.type = "solesurvivor" - advisory.player = ctsAlive[0].num - } - - if (tsAlive.length == 1 && advisory.type == "none") { - advisory.type = "solesurvivor" - advisory.player = tsAlive[0].num - } - - if (data.context.defusing) { - let ctsOnSites = playersOnSites.filter(player => player.team == "CT") - - advisory.type = "defuse" - advisory.player = "?" - - if (ctsOnSites.length == 1) { - advisory.player = ctsOnSites[0].num - } - } - - document.getElementById("advisory").className = advisory.type - document.getElementById("advisory").children[0].innerHTML = advisory.player -}) - -renderer.on("smokes", (event, smokes) => { - function fadeIn(smokeElement) { - setTimeout(() => { - smokeElement.className = "smokeEntity show" - }, 25) - } - - function remove(smokeElement) { - setTimeout(() => { - smokeElement.outerHTML = "" - }, 2000) - } - - let drawnSmokes = [] - - for (let smoke of smokes) { - let smokeElement = document.getElementById("smoke" + smoke.id) - - if (!smokeElement) { - smokeElement = document.createElement("div") - smokeElement.id = "smoke" + smoke.id - smokeElement.className = "smokeEntity hide" - - smokeElement.style.height = smokeElement.style.width = 290 / mapData.resolution / 1024 * 100 + "%" - - document.getElementById("smokes").appendChild(smokeElement) - - fadeIn(smokeElement) - } - - drawnSmokes.push() - - let percOffset = parseFloat(smokeElement.style.height) / 2 - - smokeElement.style.left = positionToPerc(smoke.position.x, mapData.offset.x) + "%" - smokeElement.style.bottom = positionToPerc(smoke.position.y, mapData.offset.y) - percOffset + "%" - - if (smoke.time > 15 && smokeElement.className != "smokeEntity fading") { - smokeElement.className = "smokeEntity fading" - } - - if (smoke.time > 16.4 && smokeElement.className != "smokeEntity fading hide") { - smokeElement.className = "smokeEntity fading hide" - remove(smokeElement) - } - } -}) - -let playerPos = [] -for (var i = 0; i < 10; i++) { - playerPos.push({ - x: null, - y: null, - lock: false - }) -} - -let playerBuffers = [[], [], [], [], [], [], [], [], [], []] - -setInterval(() => { - for (let num in playerBuffers) { - if (playerPos[num].x != null && !playerPos[num].lock) { - playerBuffers[num].unshift({ - x: playerPos[num].x, - y: playerPos[num].y - }) - playerBuffers[num] = playerBuffers[num].slice(0, config.radar.playerSmoothing) - } - - let bufferPercX = (playerBuffers[num].reduce((prev, curr) => prev + curr.x, 0) / (playerBuffers[num].length)) - let bufferPercY = (playerBuffers[num].reduce((prev, curr) => prev + curr.y, 0) / (playerBuffers[num].length)) - - let playerElement = document.getElementById("player" + num) - playerElement.style.left = bufferPercX + "%" - playerElement.style.bottom = bufferPercY + "%" - } -}, 10) - -let gamePhase = "freezetime" -renderer.on("round", (event, phase) => { - // Round has restared - if ((phase == "freezetime" && gamePhase == "over") || (phase == "live" && gamePhase == "over")) { - for (let num in playerBuffers) { - playerBuffers[num] = [] - playerPos[num] = { - x: null, - y: null, - lock: false - } - } - - - } - - - gamePhase = phase -}) - -let playersAlive = [] - -renderer.on("players", (event, data) => { - if (currentMap == "none") return - - let foundArray = [] - - for (let player of data.players) { - if (!player.alive) continue - - foundArray.push({ - x: positionToPerc(player.position.x, mapData.offset.x), - y: positionToPerc(player.position.y, mapData.offset.y) - }) - } - - playersAlive = foundArray -}) - -let radarStyle = document.getElementById("container").style -let radarQueues = { - scale: [1, 1, 1, 1, 1, 1], - x: [0, 0, 0, 0, 0, 0], - y: [0, 0, 0, 0, 0, 0] -} - -setInterval(() => { - let bounds = { - x: { - min: 100, - max: 0 - }, - y: { - min: 100, - max: 0 - } - } - - if (!config.autozoom.enable) return - - for (let player of playersAlive) { - if (bounds.x.min > player.x) bounds.x.min = player.x - if (bounds.x.max < player.x) bounds.x.max = player.x - if (bounds.y.min > player.y) bounds.y.min = player.y - if (bounds.y.max < player.y) bounds.y.max = player.y - } - - let radarScale = 1 + (1 - Math.max(bounds.x.max - bounds.x.min, bounds.y.max - bounds.y.min) / 100) - - // Do not zoom if the scale seems to have been calculated with 0 data - if (radarScale === 3) return - - // Limit the radar scale to base size, and keep a 20% buffer around the players - radarScale = Math.max(1, radarScale - config.autozoom.padding) - - let radarX = (((bounds.x.max + bounds.x.min) / 2) - 50) * -1 - let radarY = ((bounds.y.max + bounds.y.min) / 2) - 50 - - - radarQueues.scale.unshift(radarScale) - radarQueues.scale = radarQueues.scale.slice(0, config.autozoom.smoothing) - radarQueues.x.unshift(radarX) - radarQueues.x = radarQueues.x.slice(0, config.autozoom.smoothing) - radarQueues.y.unshift(radarY) - radarQueues.y = radarQueues.y.slice(0, config.autozoom.smoothing) - - let avgScale = radarQueues.scale.reduce((sum, el) => sum + el, 0) / radarQueues.scale.length - let avgX = radarQueues.x.reduce((sum, el) => sum + el, 0) / radarQueues.x.length - let avgY = radarQueues.y.reduce((sum, el) => sum + el, 0) / radarQueues.y.length - - radarStyle.transform = `scale(${avgScale}) translate(${avgX}%, ${avgY}%)` -}, 25) diff --git a/maps/de_cache/meta.json5 b/maps/de_cache/meta.json5 index ac0316f..efd8d24 100644 --- a/maps/de_cache/meta.json5 +++ b/maps/de_cache/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 1, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 5.48, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 2210 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": -490, - "y1": 1570, - // Top right point - "x2": 70, - "y2": 2250 - }, - "b": { - // Bottom left point - "x1": -390, - "y1": -1400, - // Top right point - "x2": 300, - "y2": -800 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 72, "y": 69 diff --git a/maps/de_dust2/meta.json5 b/maps/de_dust2/meta.json5 index 35b70c5..274ee44 100644 --- a/maps/de_dust2/meta.json5 +++ b/maps/de_dust2/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 1, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 4.40, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 1135 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": 890, - "y1": 2390, - // Top right point - "x2": 1330, - "y2": 2810 - }, - "b": { - // Bottom left point - "x1": -1790, - "y1": 2560, - // Top right point - "x2": -1290, - "y2": 3060 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 80, "y": 8.9 diff --git a/maps/de_inferno/meta.json5 b/maps/de_inferno/meta.json5 index a938e65..2b8cb25 100644 --- a/maps/de_inferno/meta.json5 +++ b/maps/de_inferno/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 1, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 4.91, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 1030 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 150 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": 1770, - "y1": 170, - // Top right point - "x2": 2240, - "y2": 890 - }, - "b": { - // Bottom left point - "x1": 80, - "y1": 2600, - // Top right point - "x2": 660, - "y2": 3210 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 16, "y": 56 diff --git a/maps/de_mirage/meta.json5 b/maps/de_mirage/meta.json5 index 5766dfb..b96184e 100644 --- a/maps/de_mirage/meta.json5 +++ b/maps/de_mirage/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 2, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 3, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 4.96, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 3250 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": -650, - "y1": -2250, - // Top right point - "x2": -200, - "y2": -1820 - }, - "b": { - // Bottom left point - "x1": -2240, - "y1": 120, - // Top right point - "x2": -1750, - "y2": 620 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 10, "y": 39.5 diff --git a/maps/de_nuke/meta.json5 b/maps/de_nuke/meta.json5 index bd0ab20..e4e0f44 100644 --- a/maps/de_nuke/meta.json5 +++ b/maps/de_nuke/meta.json5 @@ -1,45 +1,39 @@ { - // The version of this meta file - "version": 1, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 6.97, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar "offset": { - "x": 3440, - "y": 4100 + "x": 3300, + "y": 5880 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": 420, - "y1": -840, - // Top right point - "x2": 970, - "y2": -240 - }, - "b": { - // Bottom left point - "x1": 290, - "y1": -1370, - // Top right point - "x2": 1030, - "y2": -570 + // Contains any special map splits + "splits": [ + { + // The top and bottom y height for this split + "bounds": { + "top": -486, + "bottom": -2500 + }, + // The radar offset in percentages to apply when player is in the split + "offset": { + "x": 0, + "y": -46 + } } - }, + ], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 20, - "y": 59 + "y": 30 } } diff --git a/maps/de_nuke/radar.png b/maps/de_nuke/radar.png index 1fc3278..3e620f7 100644 Binary files a/maps/de_nuke/radar.png and b/maps/de_nuke/radar.png differ diff --git a/maps/de_overpass/meta.json5 b/maps/de_overpass/meta.json5 index 750590a..af21e74 100644 --- a/maps/de_overpass/meta.json5 +++ b/maps/de_overpass/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 2, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 3, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 5.17, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 3350 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": -2680, - "y1": 470, - // Top right point - "x2": -1680, - "y2": 1210 - }, - "b": { - // Bottom left point - "x1": -1330, - "y1": -20, - // Top right point - "x2": -900, - "y2": 480 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 72, "y": 84 diff --git a/maps/de_train/meta.json5 b/maps/de_train/meta.json5 index 943e205..3610f7c 100644 --- a/maps/de_train/meta.json5 +++ b/maps/de_train/meta.json5 @@ -1,7 +1,12 @@ { - // The version of this meta file - "version": 1, - // The amount of in-game units per pixel of the 1200px radar image + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image "resolution": 4.69, // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar @@ -10,34 +15,10 @@ "y": 2290 }, - // The minimum and maximum z (height) values the players can have on the map - "heightRange": { - "min": 0, - "max": 0 - }, - - // The bombsites, as defined by a bottom left and top right point - // Defined in in-game units - "bombsites": { - "a": { - // Bottom left point - "x1": 150, - "y1": -160, - // Top right point - "x2": 950, - "y2": 390 - }, - "b": { - // Bottom left point - "x1": -310, - "y1": -1370, - // Top right point - "x2": 430, - "y2": -950 - } - }, + // Contains any special map splits + "splits": [], - // Position in which advisoies should be placed, in percentages from the bottom left + // Position in which advisories should be placed, in percentages from the bottom left "advisoryPosition": { "x": 10, "y": 56 diff --git a/maps/de_vertigo/meta.json5 b/maps/de_vertigo/meta.json5 new file mode 100644 index 0000000..38e6b38 --- /dev/null +++ b/maps/de_vertigo/meta.json5 @@ -0,0 +1,39 @@ +{ + "version": { + // The version of this meta file + "radar": 2, + // The object format version this file is using + "format": 2 + }, + + // The amount of in-game units per pixel of the 1024px radar image + "resolution": 4.58, + + // How many in-game units is the origin (0,0) of the map from the bottom left point of the radar + "offset": { + "x": 2830, + "y": 1660 + }, + + // Contains any special map splits + "splits": [ + { + // The top and bottom y height for this split + "bounds": { + "top": 11680, + "bottom": 0 + }, + // The radar offset in percentages to apply when player is in the split + "offset": { + "x": 45.5, + "y": 44 + } + } + ], + + // Position in which advisories should be placed, in percentages from the bottom left + "advisoryPosition": { + "x": 19, + "y": 78 + } +} diff --git a/maps/de_vertigo/radar.png b/maps/de_vertigo/radar.png new file mode 100644 index 0000000..f876ee3 Binary files /dev/null and b/maps/de_vertigo/radar.png differ diff --git a/package.json b/package.json index 49ea32f..0e44ce4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boltobserv", - "version": "0.0.2", + "version": "0.1.0", "description": "External radar for CSGO observers", "main": "index.js", "homepage": "https://github.com/boltgolt/boltobserv/", diff --git a/renderers/_global.js b/renderers/_global.js new file mode 100644 index 0000000..8a3dfa8 --- /dev/null +++ b/renderers/_global.js @@ -0,0 +1,97 @@ +// Shared render object +// +// Provides shared variables and functions for all renderers. + +module.exports = { + renderer: require("electron").ipcRenderer, + config: require("../loadconfig")(), + + mapData: {}, + currentMap: "none", + // The last known game phase + gamePhase: "freezetime", + + playerPos: [], + playerBuffers: [], + playerSplits: [], + playerElements: [], + + /** + * Convert in-game position units to radar percentages + * @param {Array} positionObj In-game position object with X and Y, and an optional Z + * @param {String} axis The axis to calculate position for + * @param {Number} playerNum An optional player number to wipe location buffer on split switch + * @return {Number} Relative radar percentage + */ + positionToPerc: (positionObj, axis, playerNum) => { + // The position of the player in game, with the bottom left corner of the radar as origin (0,0) + let gamePosition = positionObj[axis] + module.exports.mapData.offset[axis] + // The position of the player relative to an 1024x1024 pixel grid + let pixelPosition = gamePosition / module.exports.mapData.resolution + // The position of the player as an percentage for any size + let precPosition = pixelPosition / 1024 * 100 + + // Set the split to the default map + let currentSplit = -1 + // Check if there are splits on the map and if we have a Z position + if (module.exports.mapData.splits.length > 0 && typeof positionObj.z == "number") { + // Go through each split + for (let i in module.exports.mapData.splits) { + let split = module.exports.mapData.splits[i] + + // If the position is within the split + if (positionObj.z > split.bounds.bottom && positionObj.z < split.bounds.top) { + // Apply the split offset and save this split + precPosition += split.offset[axis] + currentSplit = parseInt(i) + + // Stop checking other splits as there can only be one active split + break + } + } + } + + // If we're calculating a player position + if (typeof playerNum == "number") { + // Wipe the location buffer if we've changed split + // Prevents the player from flying across the radar on split switch + if (module.exports.playerSplits[playerNum] != currentSplit) { + module.exports.playerBuffers[playerNum] = [] + } + + // Save this split as the last split id seen + module.exports.playerSplits[playerNum] = currentSplit + } + + // Return the position relative to the radar image + return precPosition + } +} + +// Fill position and buffer arrays +for (var i = 0; i < 10; i++) { + module.exports.playerPos.push({ + x: null, + y: null, + alive: false + }) + + module.exports.playerSplits.push(-1) + module.exports.playerBuffers.push([]) + module.exports.playerElements.push(document.getElementById("player" + i)) +} + +// On a round indicator packet +module.exports.renderer.on("round", (event, phase) => { + // Abort if there's no change in phase + if (module.exports.gamePhase == phase) return + + // If the round has ended + if ((phase == "freezetime" && module.exports.gamePhase == "over") || (phase == "live" && module.exports.gamePhase == "over")) { + // Emit a custom event + module.exports.renderer.emit("roundend") + } + + // Set the new phase + module.exports.gamePhase = phase +}) diff --git a/renderers/_init.js b/renderers/_init.js new file mode 100644 index 0000000..03831a8 --- /dev/null +++ b/renderers/_init.js @@ -0,0 +1,25 @@ +// Render initializer +// +// Starts all other renderers. + +let global = require("./_global") +const fs = require("fs") +const path = require("path") + +// Get a list of available renderers +let renderers = fs.readdirSync(__dirname) + +for (let renderer of renderers) { + // Skip renderers that start with a "_", as they are only helpers + if (renderer.slice(0, 1) == "_") continue + // Load in the render module + require(path.join(__dirname, renderer)) +} + +// Loop through each player dot to apply the scaling config option +for (let playerElem of document.getElementsByClassName("player")) { + playerElem.style.transform = `scale(${global.config.radar.playerDotScale}) translate(-50%, -50%)` +} + +// Do the same for the bomb icon +document.getElementById("bomb").style.transform = `scale(${global.config.radar.playerDotScale}) translate(-50%, -50%)` diff --git a/renderers/advisory.js b/renderers/advisory.js new file mode 100644 index 0000000..21cb932 --- /dev/null +++ b/renderers/advisory.js @@ -0,0 +1,85 @@ +// Advisories +// +// Determines if an advisory can be shown and prioritizes the most important one. + +let global = require("./_global") + +let idToNum = {} +let advisories = { + planting: -1, + defuse: -1, + solesurvivor: -1 +} + +function trim(str) { + let string = str + "" + return string.substring(0, string.length - 2) +} + +function updateAdvisory() { + for (let name in advisories) { + if (advisories[name] != -1) { + document.getElementById("advisory").className = name + document.getElementById("advisory").children[0].innerHTML = idToNum[advisories[name]] + return + } + } + + document.getElementById("advisory").className = "" +} + +global.renderer.on("players", (event, data) => { + let ctsAlive = [] + let tsAlive = [] + + for (let player of data.players) { + if (player.alive) { + if (player.team == "CT") { + ctsAlive.push(player.id) + } + else { + tsAlive.push(player.id) + } + } + + if (idToNum[trim(player.id)] != player.num) { + idToNum[trim(player.id)] = player.num + } + } + + if (ctsAlive.length == 1) { + advisories.solesurvivor = trim(ctsAlive[0]) + } + else if (tsAlive.length == 1) { + advisories.solesurvivor = trim(tsAlive[0]) + } + else { + advisories.solesurvivor = -1 + } + + updateAdvisory() +}) + +global.renderer.on("bomb", (event, data) => { + if (idToNum[trim(data.player)]) { + if (data.state == "planting") { + advisories.planting = trim(data.player) + } + else { + advisories.planting = -1 + } + + if (data.state == "defusing") { + advisories.defuse = trim(data.player) + } + else { + advisories.defuse = -1 + } + } + else { + advisories.planting = -1 + advisories.defuse = -1 + } + + updateAdvisory() +}) diff --git a/renderers/bomb.js b/renderers/bomb.js new file mode 100644 index 0000000..3a8d111 --- /dev/null +++ b/renderers/bomb.js @@ -0,0 +1,29 @@ +// Bomb rendering +// +// Shows dropped and planted bomb. + +let global = require("./_global") + +let bombElement = document.getElementById("bomb") +let bombStyle = bombElement.style + +global.renderer.on("bomb", (event, bomb) => { + if (bomb.state == "carried" || bomb.state == "exploded") { + bombStyle.display = "none" + } + else { + bombStyle.display = "block" + bombStyle.left = global.positionToPerc(bomb.position, "x") + "%" + bombStyle.bottom = global.positionToPerc(bomb.position, "y") + "%" + } + + if (bomb.state == "planted" || bomb.state == "defusing") { + bombElement.className = "planted" + } + else if (bomb.state == "defused") { + bombElement.className = "defused" + } + else { + bombElement.className = "" + } +}) diff --git a/renderers/initMap.js b/renderers/initMap.js new file mode 100644 index 0000000..cfcabfd --- /dev/null +++ b/renderers/initMap.js @@ -0,0 +1,92 @@ +// Initial map rendering +// +// Responsible for changing radar background on map change, loading map +// metadata and applying some general config values. + +let global = require("./_global") +const path = require("path") +const fs = require("fs") +const JSON5 = require("json5") + +// Catch map data send by the game +global.renderer.on("map", (event, map) => { + /** + * Show a map error and quit + * @param {String} text What error message to show + */ + function throwMapError(text) { + document.getElementById("unknownMap").style.display = "flex" + document.getElementById("unknownMap").children[0].innerHTML = text + } + + // If map is unchanged we do not need to do anything + if (global.currentMap == map) return + + // Build the path to the metadata file + let metaPath = path.join(__dirname, "..", "maps", map, "meta.json5") + let radarPath = path.join(__dirname, "..", "maps", map, "radar.png") + + // If the meta file or radar backdrop does not exist, we don't support the map and need to quit + if (!fs.existsSync(metaPath) || !fs.existsSync(radarPath)) { + return throwMapError(`Unsupported map ${map}`) + } + + // Save the map metadata as a global attribute so other renderers can use it + try { + global.mapData = JSON5.parse(fs.readFileSync(metaPath, "utf8")) + } catch (e) { + // Catch and throw on JSON error + return throwMapError(`JSON error in ${map} map file :(`) + } + + // Check if the map uses the expected meta format + if (global.mapData.version.format != 2) { + return throwMapError(`Outdated map file for ${map}`) + } + + // Make sure that the "unknown map" message is turned off for valid maps + document.getElementById("unknownMap").style.display = "none" + + // Show the radar backdrop + document.getElementById("radar").src = `../maps/${map}/radar.png` + + // Set the map as the current map and in the window title + global.currentMap = map + document.title = "Boltobserv - " + map + + // Hide advisories if you've been disabled in the config + if (global.config.radar.hideAdvisories) { + document.getElementById("advisory").style.display = "none" + } + else { + // Otherwise, read the advisory position from config and apply it + document.getElementById("advisory").style.left = global.mapData.advisoryPosition.x + "%" + document.getElementById("advisory").style.bottom = global.mapData.advisoryPosition.y + "%" + document.getElementById("advisory").style.display = "block" + } + + /** + * Draw a red rectangle over a bombsite + * @param {DOMObject} element The element to move over a bombsite + * @param {Object} cords The x1 and y1 values for the bottom left point, and x2 and y2 values for the top right point + */ + function drawSite(element, cords) { + // Be sure the sites are visible + element.style.display = "block" + + // Set the bottom left corner as defined in the cords + element.style.left = global.positionToPerc({x: cords.x1}, "x") + "%" + element.style.bottom = global.positionToPerc({y: cords.y1}, "y") + "%" + + // Transform the x2 and y2 cords to height and width, as HTML requires + // Calculates the difference between the first and second points and translates that to a relative percentage + element.style.width = ((cords.x2 - cords.x1) / global.mapData.resolution / 1024 * 100) + "%" + element.style.height = ((cords.y2 - cords.y1) / global.mapData.resolution / 1024 * 100) + "%" + } + + // Draw the bombsites on the map if enabled + if (global.config.debug.drawBombsites) { + drawSite(document.getElementById("siteA"), global.mapData.bombsites.a) + drawSite(document.getElementById("siteB"), global.mapData.bombsites.b) + } +}) diff --git a/renderers/loopFast.js b/renderers/loopFast.js new file mode 100644 index 0000000..237eca8 --- /dev/null +++ b/renderers/loopFast.js @@ -0,0 +1,38 @@ +// Main loop +// +// Sets player position on screen at +/- 60fps + +let global = require("./_global") + +// Function to be executed before every frame paint +function step() { + // Go though every player location buffer + for (let num in global.playerBuffers) { + // if a new player position is available + if (global.playerPos[num].x != null) { + // Add it to the start of the buffer + global.playerBuffers[num].unshift({ + x: global.playerPos[num].x, + y: global.playerPos[num].y + }) + + // Limit the size of the buffer to the count specified in the config + global.playerBuffers[num] = global.playerBuffers[num].slice(0, global.config.radar.playerSmoothing) + } + + // Take the average of the X and Y buffers + let bufferPercX = (global.playerBuffers[num].reduce((prev, curr) => prev + curr.x, 0) / (global.playerBuffers[num].length)) + let bufferPercY = (global.playerBuffers[num].reduce((prev, curr) => prev + curr.y, 0) / (global.playerBuffers[num].length)) + + // Apply the calculated X and Y to the player dot + global.playerElements[num].style.left = bufferPercX + "%" + global.playerElements[num].style.bottom = bufferPercY + "%" + } + + // Wait for next repaint + window.requestAnimationFrame(step) +} + +// Request an update before the next repaint +// Maximizes FPS with the least CPU possible +window.requestAnimationFrame(step) diff --git a/renderers/loopSlow.js b/renderers/loopSlow.js new file mode 100644 index 0000000..d561101 --- /dev/null +++ b/renderers/loopSlow.js @@ -0,0 +1,74 @@ +// Secondary loop +// +// Handles radar autozoom every 25ms (40fps) + +let global = require("./_global") + +// Get the styling for the entire container +let radarStyle = document.getElementById("container").style +// Prepare position queues +let radarQueues = { + scale: [1, 1, 1, 1, 1, 1], + x: [0, 0, 0, 0, 0, 0], + y: [0, 0, 0, 0, 0, 0] +} + +// Run the loop every 25ms +setInterval(() => { + // Abort if autozoom isn't enabled + if (!global.config.autozoom.enable) return + + // Bounding rect around all living players + // Starts as impossibly small or large bounds so players will always overwrite it + let bounds = { + x: { + min: 100, + max: 0 + }, + y: { + min: 100, + max: 0 + } + } + + // Go through each player + for (let player of global.playerPos) { + // Ignore dead player markers for autozoom + if (!player.alive) continue + + // Overwrite the previous min/max if the value for this player is smaller/larger + if (bounds.x.min > player.x) bounds.x.min = player.x + if (bounds.x.max < player.x) bounds.x.max = player.x + if (bounds.y.min > player.y) bounds.y.min = player.y + if (bounds.y.max < player.y) bounds.y.max = player.y + } + + // Calculate the zoom-level where all players alive are visible + let radarScale = 1 + (1 - Math.max(bounds.x.max - bounds.x.min, bounds.y.max - bounds.y.min) / 100) + + // Do not zoom if the scale seems to have been calculated with just the default data + if (radarScale === 3) return + + // Limit the radar scale to base size, and keep a customizable padding around the players + radarScale = Math.max(1, radarScale - global.config.autozoom.padding) + + // Calculate the center of the bound + let radarX = (((bounds.x.max + bounds.x.min) / 2) - 50) * -1 + let radarY = ((bounds.y.max + bounds.y.min) / 2) - 50 + + // Add all calculated values to their queues, and limit the queue length + radarQueues.scale.unshift(radarScale) + radarQueues.scale = radarQueues.scale.slice(0, global.config.autozoom.smoothing) + radarQueues.x.unshift(radarX) + radarQueues.x = radarQueues.x.slice(0, global.config.autozoom.smoothing) + radarQueues.y.unshift(radarY) + radarQueues.y = radarQueues.y.slice(0, global.config.autozoom.smoothing) + + // Calculate the average for all 3 values + let avgScale = radarQueues.scale.reduce((sum, el) => sum + el, 0) / radarQueues.scale.length + let avgX = radarQueues.x.reduce((sum, el) => sum + el, 0) / radarQueues.x.length + let avgY = radarQueues.y.reduce((sum, el) => sum + el, 0) / radarQueues.y.length + + // Apply the style to the container + radarStyle.transform = `scale(${avgScale}) translate(${avgX}%, ${avgY}%)` +}, 25) diff --git a/renderers/playerPosition.js b/renderers/playerPosition.js new file mode 100644 index 0000000..25957ba --- /dev/null +++ b/renderers/playerPosition.js @@ -0,0 +1,54 @@ +// Player position calculations +// +// Sets player dot style and pushed to player location buffer, but does not set +// the location. + +let global = require("./_global") + +global.renderer.on("players", (event, data) => { + // Abort if no map has been selected yet + if (global.currentMap == "none") return + + // Loop though each player + for (let player of data.players) { + // Get their player element and start building the class + let playerElement = global.playerElements[player.num] + let classes = ["player", player.team] + + // Add the classes for dead players and bomb carriers + if (!player.alive) classes.push("dead") + if (player.bomb) classes.push("bomb") + if (player.active) classes.push("active") + + // Add all classes as a class string + let newClasses = classes.join(" ") + + // Check if the new classname is different than the one already applied + // This prevents unnecessary className updates and CSS recalculations + if (playerElement.className != newClasses) { + playerElement.className = newClasses + } + + // Save the position so the main loop can interpolate it + global.playerPos[player.num].x = global.positionToPerc(player.position, "x", player.num) + global.playerPos[player.num].y = global.positionToPerc(player.position, "y", player.num) + + // Set the player alive attribute (used in autozoom) + global.playerPos[player.num].alive = player.alive + } +}) + +// On round reset +global.renderer.on("roundend", (event, phase) => { + // Go through each player + for (let num in global.playerBuffers) { + // Empty the location buffer + global.playerBuffers[num] = [] + // Reset the player position + global.playerPos[num] = { + x: null, + y: null, + alive: false + } + } +}) diff --git a/renderers/smokes.js b/renderers/smokes.js new file mode 100644 index 0000000..23c6aa0 --- /dev/null +++ b/renderers/smokes.js @@ -0,0 +1,70 @@ +// Smoke rendering +// +// Shows smokes on map an calculates duration. + +let global = require("./_global") + +// The live position of all smokes +global.renderer.on("smokes", (event, smokes) => { + // Called to show the fade in animation with a delay + function fadeIn(smokeElement) { + setTimeout(() => { + smokeElement.className = "smokeEntity show" + }, 25) + } + + // Called to remove the smoke element after the fadeout + function remove(smokeElement) { + setTimeout(() => { + smokeElement.outerHTML = "" + }, 2000) + } + + // Go through each smoke + for (let smoke of smokes) { + // Get the smoke element + let smokeElement = document.getElementById("smoke" + smoke.id) + + // If the element does not exist yet, add it + if (!smokeElement) { + // Create a new element + smokeElement = document.createElement("div") + smokeElement.id = "smoke" + smoke.id + smokeElement.className = "smokeEntity hide" + + // Calculate the height and width based on the map resolution + smokeElement.style.height = smokeElement.style.width = 290 / global.mapData.resolution / 1024 * 100 + "%" + + // Add it to the DOM + document.getElementById("smokes").appendChild(smokeElement) + + // Play the fade in animation + fadeIn(smokeElement) + + // Calculate the offset needed to display the smoke correctly as seen in game + // Depends on the map resolution + let percOffset = parseFloat(smokeElement.style.height) / 2 + + // Set the location of the smoke + smokeElement.style.left = global.positionToPerc(smoke.position, "x") + "%" + smokeElement.style.bottom = global.positionToPerc(smoke.position, "y") - percOffset + "%" + } + + // If the smoke has been here for over 15 seconds, ready the smoke element for the fade away + // Setting the fading class will set the opacity transition to another value + if (smoke.time > 15 && smoke.time <= 16.4 && smokeElement.className != "smokeEntity fading") { + smokeElement.className = "smokeEntity fading" + } + + // Trigger the fade away + if (smoke.time > 16.4 && smokeElement.className != "smokeEntity fading hide") { + smokeElement.className = "smokeEntity fading hide" + remove(smokeElement) + } + } +}) + +// Clear all smokes on round reset +global.renderer.on("roundend", (event) => { + document.getElementById("smokes").innerHTML = "" +}) diff --git a/style/map.css b/style/map.css index 82362b5..13d4fd7 100644 --- a/style/map.css +++ b/style/map.css @@ -51,13 +51,51 @@ body { display: none; } +#bomb { + position: absolute; + background-image: url("../img/bomb-dropped.png"); + background-size: contain; + height: 12px; + width: 12px; + bottom: -30%; + left: -30%; +} + +#bomb.defused { + background-image: url("../img/bomb-defused.png"); + filter: drop-shadow(0 0 4px rgba(0, 255, 0, .3)); +} + +#bomb.planted { + background-image: url("../img/bomb-planted.png"); + animation: beep 1s infinite; +} + +@keyframes beep { + 0% { + filter: drop-shadow(0 0 0px rgba(255, 0, 0, 1)); + background-color: rgba(255, 0, 0, 0); + } + 10% { + filter: drop-shadow(0 0 6px rgba(255, 0, 0, 1)); + background-color: rgba(255, 0, 0, 0.1); + } + 20% { + filter: drop-shadow(0 0 14px rgba(255, 0, 0, 0)); + background-color: rgba(255, 0, 0, 0); + } +} + div.player { - display: none; position: absolute; height: 16px; width: 16px; + left: -100vw; + bottom: -100vh; + display: block; background: #f00; transform: translate(-8px, -8px); + transform-origin: center center; text-align: center; line-height: 16px; border-radius: 100%; @@ -68,18 +106,9 @@ div.player { font-size: 14px; opacity: 1; z-index: 15; - transition: bottom .01s linear, left: .01s linear; will-change: bottom, left; } -div.player.dead { - opacity: .4; - color: transparent; - text-shadow: none; - clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%); - z-index: 13; -} - div.player.CT { background: #5ab8f4; } @@ -100,6 +129,19 @@ div.player.bomb { background: #FF8200; } +div.player.active { + filter: drop-shadow(0 0 0 #FFF) drop-shadow(0 0 0 #FFF) drop-shadow(0 0 0 #FFF); +} + +div.player.dead { + opacity: .4; + color: transparent; + text-shadow: none; + clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%); + z-index: 13; + filter: none; +} + #advisory { position: absolute; width: 14%; @@ -141,15 +183,11 @@ div.player.bomb { font-weight: bold; } -#advisory.holdingbombA::before { - background-image: url("../img/adv-bombA.png"); -} - -#advisory.holdingbombB::before { - background-image: url("../img/adv-bombB.png"); +#advisory.planting::before { + background-image: url("../img/adv-plant.png"); } -#advisory.holdingbombA, #advisory.holdingbombB { +#advisory.planting { background: linear-gradient(to right, #AD9A3C, #ECCD37); box-shadow: 0 0 .4vmin .3vmin rgba(255,252,0,.2); } @@ -172,7 +210,7 @@ div.player.bomb { box-shadow: 0 0 .4vmin .3vmin rgba(13,255,0,.2); } -#advisory.holdingbombA span, #advisory.holdingbombB span, #advisory.defuse span, #advisory.solesurvivor span { +#advisory.planting span, #advisory.defuse span, #advisory.solesurvivor span { color: #fff; } diff --git a/style/waiting.css b/style/waiting.css index 6be2dad..dbd7f67 100644 --- a/style/waiting.css +++ b/style/waiting.css @@ -2,13 +2,79 @@ body { background: rgba(0, 0, 0, .24); } -body > div { +#game { display: flex; flex-direction: column; align-items: center; justify-content: center; } +#cfgs { + position: fixed; + top: 0; + left: 0; + width: 100vw; + z-index: 2000; + -webkit-app-region: no-drag; + cursor: default; +} + +#cfgs > div { + display: flex; + flex-direction: row; + background: #a647c7; + padding: 6px; + font-size: 13px; + margin-bottom: 3px; + line-height: 20px; + width: 100%; + box-sizing: border-box; +} + +#cfgs span { + font-weight: bold; + padding-left: 6px; +} + +#cfgs small { + font-size: 13px; + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; + padding: 0 .4em 0 .3em; +} + +#cfgs button { + background: #FFF; + border: 0; + color: #a647c7; + padding: 3px 0; + margin-right: 2px; + font-weight: bold; + font-size: 12px; + cursor: pointer; + position: relative; + text-align: center; + width: 60px; + text-transform: uppercase; + transform: scale(1); + transition: transform .15s; +} + +#cfgs button:hover { + transform: scale(1.05); +} + +#cfgs button:active { + transform: scale(0.98); +} + +#cfgs button:focus { + outline: none; +} + img { height: 20vmin; padding-bottom: 5vmin; @@ -30,3 +96,21 @@ img { font-size: 1.6vmin; text-transform: uppercase; } + +#version small { + font-size: inherit; + color: #7E7E7E; +} + +#version small:hover { + text-decoration: underline; + cursor: pointer; + position: relative; + z-index: 2000; + -webkit-app-region: no-drag; +} + +#dragarea { + max-width: initial; + max-height: initial; +}