initial commit
1
.env.development
Normal file
@ -0,0 +1 @@
|
||||
PUBLIC_URL=/dev/
|
||||
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Lexogrine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
49
OpenBrowserPlugin.js
Normal file
@ -0,0 +1,49 @@
|
||||
const open = require('open');
|
||||
|
||||
module.exports = class OpenBrowser {
|
||||
constructor(options) {
|
||||
if (typeof options === 'string') {
|
||||
this.options = Object.assign(
|
||||
{
|
||||
hasOpen: false
|
||||
},
|
||||
{
|
||||
url: options
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.options = Object.assign(
|
||||
{
|
||||
port: 8080,
|
||||
host: 'localhost',
|
||||
protocol: 'http:',
|
||||
hasOpen: false
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
const options = this.options;
|
||||
let url;
|
||||
let hasOpen = options.hasOpen;
|
||||
if (options.protocol && !options.protocol.endsWith(':')) options.protocol += ':';
|
||||
if (options.url) url = options.url;
|
||||
else url = `${options.protocol}//${options.host}:${options.port}`;
|
||||
if (compiler.hooks) {
|
||||
compiler.hooks.afterEmit.tap('openBrowser', () => {
|
||||
if (!hasOpen) open(url);
|
||||
hasOpen = true;
|
||||
this.options.hasOpen = true;
|
||||
});
|
||||
} else {
|
||||
compiler.plugin('after-emit', (c, cb) => {
|
||||
if (!hasOpen) open(url);
|
||||
hasOpen = true;
|
||||
this.options.hasOpen = true;
|
||||
return cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
143
README.md
Normal file
@ -0,0 +1,143 @@
|
||||
|
||||
<p align="center">
|
||||
<p align="center" style="font-weight:600; letter-spacing:1pt; font-size:20pt;">LEXOGRINE HUD</p>
|
||||
<p align="center"><img src="icon.png" alt="Logo" width="80" height="80"></p>
|
||||
<p align="center" style="font-weight:400;">Powered by <a href='https://github.com/lexogrine/hud-manager'><strong>« Lexogrine HUD Manager »</strong></a></p>
|
||||
</p>
|
||||
|
||||
# Lexogrine HUD
|
||||
|
||||
|
||||
|
||||
Fullfledged example of the React HUD made for HUD Manager. It has:
|
||||
|
||||
- Custom actions
|
||||
- Keybinds
|
||||
- Killfeed
|
||||
- Player cam feed
|
||||
- Custom Radar
|
||||
|
||||
## Keybinds:
|
||||
### **Left Alt + S**
|
||||
>Makes radar smaller by 20px;
|
||||
### **Left Alt + B**
|
||||
>Makes radar bigger by 20px;
|
||||
### **Left Alt + T**
|
||||
>Shows trivia box
|
||||
### **Left Alt + M**
|
||||
>Toggles upcoming match box
|
||||
### **Left Alt + P**
|
||||
>Toggles player preview
|
||||
### **Left Alt + C**
|
||||
>Toggles camera feed
|
||||
### **Left Ctrl + B**
|
||||
>Make radar invisible
|
||||
|
||||
## **Panel**
|
||||
## Trivia settings
|
||||
|
||||
| Field|Description |
|
||||
|--|--|
|
||||
| Trivia title| `Text` |
|
||||
| Trivia content| `Text` |
|
||||
|
||||
|
||||
## Display settings
|
||||
|
||||
|
||||
| Field|Description |
|
||||
|--|--|
|
||||
| Left/right box's title| `Text` |
|
||||
| Left/right box's title| `Text` |
|
||||
| Left/right box's image logo| `Image file` |
|
||||
|
||||
## Example settings
|
||||
|
||||

|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
# Download
|
||||
|
||||
To download it just click here: [DOWNLOAD HUD](https://github.com/lexogrine/csgo-react-hud/releases/latest)
|
||||
|
||||
# Instruction
|
||||
## Setting up
|
||||
Fork this repo, clone it, and then run `npm install` and `npm start`. HUD should start on the 3500 port. For this to work have HUD Manager opened so it will pass CS:GO data to the HUD.
|
||||
|
||||
## Identifying HUD
|
||||
In `/public` directory edit hud.json so it fits you - fill HUD's name, author, version, specify the radar and killfeed functionalities. At the end replace the thumb.png with your icon :)
|
||||
|
||||
## Building & distributing
|
||||
To build version to distribute and move around, in the root directory run `npm run pack`. It will create the zip file for distribution. Now you can just drag and drop this file into the HUD Managers upload area.
|
||||
|
||||
## Signing
|
||||
|
||||
To create Signed HUD to prevent at least from modyfing compiled Javascript files run `npm run sign`. It's the same as `npm run pack` command but with additional step of signing .js and .css files and hud.json.
|
||||
|
||||
|
||||
## File structure
|
||||
The HUD is seperated into two parts - the API part, that connects to the HUD Manager API and communicate with it: `src/App.tsx` file and `src/api` directory. Usually, you don't want to play with it, so the whole runs without a problem.
|
||||
The second part is the render part - `src/HUD`, `src/fonts` and `src/assets` are the directories you want to modify. In the `src/HUD` each element of the HUD is seperated into its own folder. Styles are kept in the `src/HUD/styles`. Names are quite self-explanatory, and to modify style of the element you should just find the styling by the file and class name.
|
||||
|
||||
|
||||
## `panel.json` API
|
||||
To get the incoming data from the HUD Manager, let's take a look at the `src/HUD/SideBoxes/SideBox.tsx` `componentDidMount()` method:
|
||||
```javascript
|
||||
import {configs} from './../../App';
|
||||
...
|
||||
configs.onChange((data:any) => {
|
||||
if(!data) return;
|
||||
|
||||
const display = data.display_settings;
|
||||
|
||||
if(!display) return;
|
||||
|
||||
if(display[`${this.props.side}_title`]){
|
||||
this.setState({title:display[`${this.props.side}_title`]})
|
||||
}
|
||||
if(display[`${this.props.side}_subtitle`]){
|
||||
this.setState({subtitle:display[`${this.props.side}_subtitle`]})
|
||||
}
|
||||
if(display[`${this.props.side}_image`]){
|
||||
this.setState({image:display[`${this.props.side}_image`]})
|
||||
}
|
||||
});
|
||||
```
|
||||
To retrieve incoming data, you should just import `configs` object and then listen for the changes with `onChange` method. Usually you want to check for the specific data, as in the callback it will always serve the full form from the Manager.
|
||||
However it looks different in the case of action input. In this case, let's look at the `src/HUD/Trivia/Trivia.tsx`:
|
||||
```javascript
|
||||
import {configs, actions} from './../../App';
|
||||
...
|
||||
actions.on("triviaState", (state: any) => {
|
||||
this.setState({show: state === "show"})
|
||||
});
|
||||
```
|
||||
For the action input we need to import the `actions` object and create listener with the parameter on it.
|
||||
## `keybinds.json` API
|
||||
Keybinds API works in very similiar to `panel.json` action API. One more time the example will be from `src/HUD/Trivia/Trivia.tsx`:
|
||||
```javascript
|
||||
import {configs, actions} from './../../App';
|
||||
...
|
||||
actions.on("toggleTrivia", () => {
|
||||
this.setState({show: !this.state.show})
|
||||
});
|
||||
```
|
||||
Keybinds listener works on the same object as action input, in this case however there are no parameter to retrieve.
|
||||
|
||||
## Killfeed
|
||||
Because our `csgogsi` has the ability to process input from HLAE's MIRV, listening for kills is very easy. We can see than in `src/HUD/Killfeed/Killfeed.tsx`:
|
||||
```javascript
|
||||
componentDidMount() {
|
||||
GSI.on("kill", kill => {
|
||||
this.addKill(kill);
|
||||
});
|
||||
}
|
||||
```
|
||||
The Killfeed component basically just keeps kills in the state during the round, and after the round it cleans the state. Kills have CSS animation, that makes them gently show, and after a few seconds disappear, the experience is very smooth. You can fiddle with the styling in the `killfeed.css`
|
||||
This killfeed detects who killed whom, if there was an assist (flash assist as well), used weapon, headshot and wallbang.
|
||||
|
||||
## Radar
|
||||
Radar is custom React-based component, made by Hubert Walczak, and is easily editable from css.
|
||||
45
craco.config.js
Normal file
@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const homedir = require('os').homedir();
|
||||
const internalIp = require('internal-ip');
|
||||
const OpenBrowserPlugin = require('./OpenBrowserPlugin');
|
||||
|
||||
const pathToConfig = path.join(process.env.APPDATA || path.join(homedir, '.config'), 'hud-manager', 'databases', 'config');
|
||||
let port = 1349;
|
||||
|
||||
const getPort = () => {
|
||||
if(!fs.existsSync(pathToConfig)){
|
||||
console.warn('LHM Config file unavailable');
|
||||
return port;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(pathToConfig, 'utf-8'));
|
||||
|
||||
if(!config.port){
|
||||
console.warn('LHM Port unavailable');
|
||||
}
|
||||
|
||||
console.warn('LHM Port detected as', config.port);
|
||||
return config.port;
|
||||
|
||||
} catch {
|
||||
console.warn('LHM Config file invalid');
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
port = getPort();
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: 3500,
|
||||
open: false
|
||||
},
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
webpackConfig.plugins.push(new OpenBrowserPlugin({ url: `http://${internalIp.v4.sync()}:${port}/development/`}))
|
||||
return webpackConfig;
|
||||
}
|
||||
}
|
||||
};
|
||||
60
package.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "lexogrine_hud",
|
||||
"version": "1.0.0",
|
||||
"homepage": "./",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^5.7.0",
|
||||
"@types/jest": "24.0.19",
|
||||
"@types/node": "16.11.20",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/simple-peer": "^9.11.3",
|
||||
"buffer": "^6.0.3",
|
||||
"csgogsi-socket": "^2.7.1",
|
||||
"query-string": "^6.12.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"simple-peer": "^9.11.0",
|
||||
"simple-websockets": "^1.1.0",
|
||||
"typescript": "^4.5.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"zip": "npm-build-zip",
|
||||
"start": "craco start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"pack": "npm run build && npm run zip",
|
||||
"sign": "npm run build && node sign.js && npm-build-zip --name=signed"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/history": "^4.7.5",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"npm-build-zip": "^1.0.2",
|
||||
"open": "^8.0.2",
|
||||
"sass": "^1.32.5",
|
||||
"socket.io-client": "^2.4.0"
|
||||
}
|
||||
}
|
||||
BIN
preview.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
13
public/hud.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name":"Lexogrine CS2 HUD",
|
||||
"version":"1.0.0",
|
||||
"author":"Lexogrine",
|
||||
"legacy": false,
|
||||
"radar": true,
|
||||
"killfeed": false,
|
||||
"game":"cs2",
|
||||
"boltobserv":{
|
||||
"css":true,
|
||||
"maps":true
|
||||
}
|
||||
}
|
||||
23
public/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
6
public/keybinds.json
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"bind":"Alt+C",
|
||||
"action":"toggleCams"
|
||||
}
|
||||
]
|
||||
148
public/panel.json
Normal file
@ -0,0 +1,148 @@
|
||||
[
|
||||
{
|
||||
"label": "Trivia",
|
||||
"name":"trivia",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "title",
|
||||
"label": "Trivia title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "content",
|
||||
"label": "Trivia content"
|
||||
},
|
||||
{
|
||||
"type": "action",
|
||||
"name": "triviaState",
|
||||
"values": [
|
||||
{
|
||||
"name": "show",
|
||||
"label": "Show trivia"
|
||||
},
|
||||
{
|
||||
"name": "hide",
|
||||
"label": "Hide trivia"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Display settings",
|
||||
"name":"display_settings",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "left_title",
|
||||
"label": "Left box's title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "right_title",
|
||||
"label": "Right box's title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "left_subtitle",
|
||||
"label": "Left box's subtitle"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "right_subtitle",
|
||||
"label": "Right box's subtitle"
|
||||
},
|
||||
{
|
||||
"type":"image",
|
||||
"name":"left_image",
|
||||
"label":"Left box's image logo"
|
||||
},
|
||||
{
|
||||
"type":"image",
|
||||
"name":"right_image",
|
||||
"label":"Right box's image logo"
|
||||
},
|
||||
{
|
||||
"type": "action",
|
||||
"name": "boxesState",
|
||||
"values": [
|
||||
{
|
||||
"name": "show",
|
||||
"label": "Show boxes"
|
||||
},
|
||||
{
|
||||
"name": "hide",
|
||||
"label": "Hide boxes"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "action",
|
||||
"name": "toggleRadarView",
|
||||
"values": [
|
||||
{
|
||||
"name": "toggler",
|
||||
"label": "Toggle radar view"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Player & Match overview",
|
||||
"name":"preview_settings",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "match",
|
||||
"name": "match_preview",
|
||||
"label": "Pick an upcoming match"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "select_preview",
|
||||
"label": "Mood indicator",
|
||||
"values": [
|
||||
{
|
||||
"name": "show",
|
||||
"label": ":)"
|
||||
},
|
||||
{
|
||||
"name": "hide",
|
||||
"label": ":("
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "player",
|
||||
"name": "player_preview",
|
||||
"label": "Pick a player to preview"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "player_preview_toggle",
|
||||
"label": "Show player preview"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "match_preview_toggle",
|
||||
"label": "Show upcoming match"
|
||||
},
|
||||
{
|
||||
"type": "action",
|
||||
"name": "showTournament",
|
||||
"values": [
|
||||
{
|
||||
"name": "show",
|
||||
"label": "Show tournament"
|
||||
},
|
||||
{
|
||||
"name": "hide",
|
||||
"label": "Hide tournament"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
48
public/radar.css
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Use this file to apply custom styles to the radar image
|
||||
*/
|
||||
|
||||
/* Any player dot */
|
||||
div.dot {}
|
||||
|
||||
/* Players that are CT or T */
|
||||
div.dot.CT {}
|
||||
div.dot.T {}
|
||||
|
||||
/* The player with the bomb */
|
||||
div.dot.bomb{}
|
||||
|
||||
/* The player currently being observed */
|
||||
div.dot.active {}
|
||||
|
||||
/* A dead player */
|
||||
div.dot.dead {}
|
||||
|
||||
/* The number on a player dot */
|
||||
div.label {}
|
||||
|
||||
/* The number on the dot being spectated */
|
||||
div.label.active {}
|
||||
|
||||
/* The dropped or planted bomb on the map */
|
||||
#bomb {
|
||||
height: 4vmin;
|
||||
width: 4vmin;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
|
||||
/* Smoke circles on the map */
|
||||
#smokes {
|
||||
display: none;
|
||||
}
|
||||
#smokes > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Inferno circles on the map */
|
||||
.inferno > div {}
|
||||
|
||||
/* The advisory on screen */
|
||||
#advisory {
|
||||
display: none !important;
|
||||
}
|
||||
BIN
public/thumb.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
settings.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
75
sign.js
Normal file
@ -0,0 +1,75 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const sign = () => {
|
||||
const getAllFilesToSign = (hudDir) => {
|
||||
const files = [];
|
||||
const getFiles = (dir) => {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
const fileDirectory = path.join(dir, file);
|
||||
if (fs.statSync(fileDirectory).isDirectory()) return getFiles(fileDirectory);
|
||||
else if (fileDirectory.endsWith('.js') || fileDirectory.endsWith('.css')) return files.push(fileDirectory);
|
||||
})
|
||||
}
|
||||
getFiles(hudDir)
|
||||
return files;
|
||||
}
|
||||
|
||||
const dir = path.join(__dirname, 'build');
|
||||
|
||||
const keyFile = path.join(dir, 'key');
|
||||
|
||||
if (fs.existsSync(keyFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filesToSign = getAllFilesToSign(dir);
|
||||
const passphrase = crypto.randomBytes(48).toString('hex');
|
||||
|
||||
filesToSign.push(path.join(dir, 'hud.json'));
|
||||
|
||||
const keys = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem',
|
||||
cipher: 'aes-256-cbc',
|
||||
passphrase
|
||||
}
|
||||
});
|
||||
|
||||
let success = true;
|
||||
|
||||
const fileToContent = {};
|
||||
|
||||
filesToSign.forEach(file => {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
try {
|
||||
const signed = jwt.sign(content, { key: keys.privateKey.toString(), passphrase }, { algorithm: 'RS256' });
|
||||
fileToContent[file] = signed;
|
||||
} catch {
|
||||
success = false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (!success) return false;
|
||||
|
||||
filesToSign.forEach(file => {
|
||||
fs.writeFileSync(file, fileToContent[file]);
|
||||
});
|
||||
|
||||
fs.writeFileSync(keyFile, keys.publicKey.toString());
|
||||
|
||||
return success;
|
||||
}
|
||||
sign();
|
||||
215
src/App.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import Layout from './HUD/Layout/Layout';
|
||||
import api, { port, isDev } from './api/api';
|
||||
import { loadAvatarURL } from './api/avatars';
|
||||
import ActionManager, { ConfigManager } from './api/actionManager';
|
||||
|
||||
import { CSGO, PlayerExtension, GSISocket, CSGORaw } from "csgogsi-socket";
|
||||
import { Match } from './api/interfaces';
|
||||
import { initiateConnection } from './HUD/Camera/mediaStream';
|
||||
|
||||
let isInWindow = !!window.parent.ipcApi;
|
||||
|
||||
export const { GSI, socket } = GSISocket(isDev ? `localhost:${port}` : '/', "update");
|
||||
|
||||
GSI.regulationMR = 12;
|
||||
|
||||
if(isInWindow){
|
||||
window.parent.ipcApi.receive('raw', (data: CSGORaw, damage?: RoundDamage[]) => {
|
||||
if(damage){
|
||||
GSI.damage = damage;
|
||||
}
|
||||
GSI.digest(data);
|
||||
});
|
||||
}
|
||||
|
||||
type RoundPlayerDamage = {
|
||||
steamid: string;
|
||||
damage: number;
|
||||
};
|
||||
type RoundDamage = {
|
||||
round: number;
|
||||
players: RoundPlayerDamage[];
|
||||
};
|
||||
|
||||
socket.on('update', (_csgo: any, damage?: RoundDamage[]) => {
|
||||
if(damage) GSI.damage = damage;
|
||||
});
|
||||
|
||||
export const actions = new ActionManager();
|
||||
export const configs = new ConfigManager();
|
||||
|
||||
export const hudIdentity = {
|
||||
name: '',
|
||||
isDev: false
|
||||
};
|
||||
|
||||
interface DataLoader {
|
||||
match: Promise<void> | null
|
||||
}
|
||||
|
||||
const dataLoader: DataLoader = {
|
||||
match: null
|
||||
}
|
||||
|
||||
class App extends React.Component<any, { match: Match | null, game: CSGO | null, steamids: string[], checked: boolean }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
game: null,
|
||||
steamids: [],
|
||||
match: null,
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
|
||||
verifyPlayers = async (game: CSGO) => {
|
||||
const steamids = game.players.map(player => player.steamid);
|
||||
steamids.forEach(steamid => {
|
||||
loadAvatarURL(steamid);
|
||||
})
|
||||
|
||||
if (steamids.every(steamid => this.state.steamids.includes(steamid))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loaded = GSI.players.map(player => player.steamid);
|
||||
|
||||
const notCheckedPlayers = steamids.filter(steamid => !loaded.includes(steamid));
|
||||
|
||||
const extensioned = await api.players.get(notCheckedPlayers);
|
||||
|
||||
const lacking = notCheckedPlayers.filter(steamid => extensioned.map(player => player.steamid).includes(steamid));
|
||||
|
||||
const players: PlayerExtension[] = extensioned
|
||||
.filter(player => lacking.includes(player.steamid))
|
||||
.map(player => (
|
||||
{
|
||||
id: player._id,
|
||||
name: player.username,
|
||||
realName: `${player.firstName} ${player.lastName}`,
|
||||
steamid: player.steamid,
|
||||
country: player.country,
|
||||
avatar: player.avatar,
|
||||
extra: player.extra,
|
||||
})
|
||||
);
|
||||
|
||||
const gsiLoaded = GSI.players;
|
||||
|
||||
gsiLoaded.push(...players);
|
||||
|
||||
GSI.players = gsiLoaded;
|
||||
this.setState({ steamids });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMatch();
|
||||
const href = window.location.href;
|
||||
socket.emit("started");
|
||||
let isDev = false;
|
||||
let name = '';
|
||||
if (href.indexOf('/huds/') === -1) {
|
||||
isDev = true;
|
||||
name = (Math.random() * 1000 + 1).toString(36).replace(/[^a-z]+/g, '').substr(0, 15);
|
||||
hudIdentity.isDev = true;
|
||||
} else {
|
||||
const segment = href.substr(href.indexOf('/huds/') + 6);
|
||||
name = segment.substr(0, segment.lastIndexOf('/'));
|
||||
hudIdentity.name = name;
|
||||
}
|
||||
|
||||
socket.on("readyToRegister", () => {
|
||||
socket.emit("register", name, isDev, "cs2", isInWindow ? "IPC" : "DEFAULT");
|
||||
initiateConnection();
|
||||
});
|
||||
socket.on(`hud_config`, (data: any) => {
|
||||
configs.save(data);
|
||||
});
|
||||
socket.on(`hud_action`, (data: any) => {
|
||||
actions.execute(data.action, data.data);
|
||||
});
|
||||
socket.on('keybindAction', (action: string) => {
|
||||
actions.execute(action);
|
||||
});
|
||||
|
||||
socket.on("refreshHUD", () => {
|
||||
window.top?.location.reload();
|
||||
});
|
||||
|
||||
socket.on("update_mirv", (data: any) => {
|
||||
GSI.digestMIRV(data);
|
||||
})
|
||||
GSI.on('data', game => {
|
||||
if (!this.state.game || this.state.steamids.length) this.verifyPlayers(game);
|
||||
|
||||
const wasLoaded = !!this.state.game;
|
||||
|
||||
this.setState({ game }, () => {
|
||||
if(!wasLoaded) this.loadMatch(true);
|
||||
});
|
||||
});
|
||||
socket.on('match', () => {
|
||||
|
||||
this.loadMatch(true);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
loadMatch = async (force = false) => {
|
||||
if (!dataLoader.match || force) {
|
||||
dataLoader.match = new Promise((resolve) => {
|
||||
api.match.getCurrent().then(match => {
|
||||
if (!match) {
|
||||
GSI.teams.left = null;
|
||||
GSI.teams.right = null;
|
||||
return;
|
||||
}
|
||||
this.setState({ match });
|
||||
|
||||
let isReversed = false;
|
||||
if (GSI.last) {
|
||||
const mapName = GSI.last.map.name.substring(GSI.last.map.name.lastIndexOf('/') + 1);
|
||||
const current = match.vetos.filter(veto => veto.mapName === mapName)[0];
|
||||
if (current && current.reverseSide) {
|
||||
isReversed = true;
|
||||
}
|
||||
this.setState({ checked: true });
|
||||
}
|
||||
if (match.left.id) {
|
||||
api.teams.getOne(match.left.id).then(left => {
|
||||
const gsiTeamData = { id: left._id, name: left.name, country: left.country, logo: left.logo, map_score: match.left.wins, extra: left.extra };
|
||||
|
||||
if (!isReversed) {
|
||||
GSI.teams.left = gsiTeamData;
|
||||
}
|
||||
else GSI.teams.right = gsiTeamData;
|
||||
});
|
||||
}
|
||||
if (match.right.id) {
|
||||
api.teams.getOne(match.right.id).then(right => {
|
||||
const gsiTeamData = { id: right._id, name: right.name, country: right.country, logo: right.logo, map_score: match.right.wins, extra: right.extra };
|
||||
|
||||
if (!isReversed) GSI.teams.right = gsiTeamData;
|
||||
else GSI.teams.left = gsiTeamData;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}).catch(() => {
|
||||
//dataLoader.match = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.state.game) return null;
|
||||
return (
|
||||
<Layout game={this.state.game} match={this.state.match} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default App;
|
||||
78
src/HUD/Camera/Camera.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { mediaStreams } from "./mediaStream";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
type Props = {
|
||||
steamid: string,
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const CameraView = ({ steamid, visible }: Props) => {
|
||||
const [uuid] = useState(uuidv4());
|
||||
const [ forceHide, setForceHide ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const mountStream = (stream: MediaStream) => {
|
||||
console.log("mounting video");
|
||||
const remoteVideo = document.getElementById(`remote-video-${steamid}-${uuid}`) as HTMLVideoElement;
|
||||
if(!remoteVideo || !stream){
|
||||
console.log("no video element")
|
||||
}
|
||||
if (!remoteVideo || !stream) return;
|
||||
|
||||
remoteVideo.srcObject = stream;
|
||||
|
||||
remoteVideo.play();
|
||||
}
|
||||
|
||||
const mountExistingStream = () => {
|
||||
const currentStream = mediaStreams.players.find(player => player.steamid === steamid);
|
||||
if(!currentStream || !currentStream.peerConnection || !currentStream.peerConnection._remoteStreams) return;
|
||||
|
||||
const stream = currentStream.peerConnection._remoteStreams[0];
|
||||
|
||||
if(!stream) return;
|
||||
|
||||
mountStream(stream);
|
||||
}
|
||||
|
||||
const onStreamCreate = (stream: MediaStream) => {
|
||||
mountStream(stream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const onStreamDestroy = () => {
|
||||
const remoteVideo = document.getElementById(`remote-video-${steamid}-${uuid}`) as HTMLVideoElement;
|
||||
|
||||
if (!remoteVideo) return;
|
||||
|
||||
remoteVideo.srcObject = null;
|
||||
}
|
||||
|
||||
const onBlockedUpdate = (steamids: string[]) => {
|
||||
setForceHide(steamids.includes(steamid));
|
||||
}
|
||||
|
||||
mediaStreams.onStreamCreate(onStreamCreate, steamid);
|
||||
mediaStreams.onStreamDestroy(onStreamDestroy, steamid);
|
||||
mediaStreams.onBlockedUpdate(onBlockedUpdate);
|
||||
|
||||
mountExistingStream();
|
||||
|
||||
return () => {
|
||||
mediaStreams.removeListener(onStreamCreate);
|
||||
mediaStreams.removeListener(onStreamDestroy);
|
||||
mediaStreams.removeListener(onBlockedUpdate);
|
||||
}
|
||||
}, [steamid]);
|
||||
|
||||
return <React.Fragment>
|
||||
<video className="video-call-preview" autoPlay muted id={`remote-video-${steamid}-${uuid}`} style={{ opacity: visible && !forceHide ? 1 : 0.001 }}></video>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
export default CameraView;
|
||||
24
src/HUD/Camera/Container.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PlayerCamera from "./Camera";
|
||||
import api from "../../api/api";
|
||||
import "./index.scss";
|
||||
|
||||
|
||||
|
||||
const CameraContainer = ({ observedSteamid }: { observedSteamid: string | null }) => {
|
||||
const [ players, setPlayers ] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api.camera.get().then(response => {
|
||||
setPlayers(response.availablePlayers.map(player => player.steamid));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <div id="cameras-container">
|
||||
{
|
||||
players.map(steamid => (<PlayerCamera key={steamid} steamid={steamid} visible={observedSteamid === steamid} />))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default CameraContainer;
|
||||
13
src/HUD/Camera/index.scss
Normal file
@ -0,0 +1,13 @@
|
||||
#cameras-container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
video {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
153
src/HUD/Camera/mediaStream.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { Instance, SignalData } from 'simple-peer';
|
||||
import api from '../../api/api';
|
||||
import { socket as Socket } from "../../App";
|
||||
const Peer = require('simple-peer');
|
||||
|
||||
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
type OfferData = {
|
||||
offer: SignalData,
|
||||
}
|
||||
|
||||
type PeerInstance = Instance & { _remoteStreams: MediaStream[] }
|
||||
|
||||
type MediaStreamPlayer = {
|
||||
peerConnection: PeerInstance | null;
|
||||
steamid: string;
|
||||
}
|
||||
|
||||
type ListenerType = ({ listener: (stream: MediaStream) => void, event: 'create', steamid: string } | ({ listener: () => void, event: 'destroy', steamid: string }));
|
||||
|
||||
type MediaStreamManager = {
|
||||
blocked: string[];
|
||||
blockedListeners: ((blocked: string[]) => void)[],
|
||||
players: MediaStreamPlayer[];
|
||||
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => void;
|
||||
onStreamDestroy: (listener: () => void, steamid: string) => void;
|
||||
onBlockedUpdate: (listener: (steamids: string[]) => void) => void;
|
||||
removeListener: (listener: any) => void;
|
||||
listeners: ListenerType[];
|
||||
}
|
||||
|
||||
const mediaStreams: MediaStreamManager = {
|
||||
blocked: [],
|
||||
blockedListeners: [],
|
||||
players: [],
|
||||
listeners: [],
|
||||
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => {
|
||||
mediaStreams.listeners.push({ listener, event: "create", steamid });
|
||||
},
|
||||
onBlockedUpdate: (listener: (blocked: string[]) => void) => {
|
||||
mediaStreams.blockedListeners.push(listener);
|
||||
},
|
||||
onStreamDestroy: (listener: () => void, steamid: string) => {
|
||||
mediaStreams.listeners.push({ listener, event: "destroy", steamid });
|
||||
},
|
||||
removeListener: (listenerToRemove: any) => {
|
||||
mediaStreams.listeners = mediaStreams.listeners.filter(listener => listener !== listenerToRemove);
|
||||
mediaStreams.blockedListeners = mediaStreams.blockedListeners.filter(listener => listener !== listenerToRemove);
|
||||
}
|
||||
};
|
||||
|
||||
const getConnectionInfo = (steamid: string) => mediaStreams.players.find(player => player.steamid === steamid) || null;
|
||||
|
||||
const closeConnection = (steamid: string) => {
|
||||
const connectionInfo = getConnectionInfo(steamid);
|
||||
try {
|
||||
if (connectionInfo) {
|
||||
if (connectionInfo.peerConnection) {
|
||||
connectionInfo.peerConnection.removeAllListeners();
|
||||
connectionInfo.peerConnection.destroy();
|
||||
}
|
||||
connectionInfo.peerConnection = null;
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
for (const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)) {
|
||||
if (listener.event === "destroy") listener.listener();
|
||||
}
|
||||
mediaStreams.players = mediaStreams.players.filter(player => player.steamid !== steamid);
|
||||
console.log(mediaStreams.players)
|
||||
}
|
||||
|
||||
const initiateConnection = async () => {
|
||||
const socket = Socket as SocketIOClient.Socket;
|
||||
const camera = await api.camera.get();
|
||||
await wait(1000);
|
||||
|
||||
socket.emit("registerAsHUD", camera.uuid);
|
||||
|
||||
socket.on('playersCameraStatus', (players: { steamid: string, label: string, allow: boolean, active: boolean }[]) => {
|
||||
const blockedSteamids = players.filter(player => !player.allow).map(player => player.steamid);
|
||||
mediaStreams.blocked = blockedSteamids;
|
||||
|
||||
for (const listener of mediaStreams.blockedListeners) {
|
||||
listener(blockedSteamids);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('offerFromPlayer', async (roomId: string, offerData: OfferData, steamid: string) => {
|
||||
// It's not from available player, ignore incoming request
|
||||
/*if(!camera.availablePlayers.find(player => player.steamid === steamid)){
|
||||
console.log("Wrong player");
|
||||
return;
|
||||
}*/
|
||||
const currentConnection = getConnectionInfo(steamid);
|
||||
|
||||
// Connection already made, ignore incoming request
|
||||
if (currentConnection) {
|
||||
console.log("Connection has been made already");
|
||||
return;
|
||||
}
|
||||
|
||||
if (camera.uuid !== roomId) return;
|
||||
|
||||
const peerConnection: PeerInstance = new Peer({ initiator: false, trickle: false });
|
||||
|
||||
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
|
||||
|
||||
mediaStreams.players.push(mediaStreamPlayer);
|
||||
|
||||
peerConnection.on('signal', answer => {
|
||||
console.log("SIGNAL COMING IN");
|
||||
const offer = JSON.parse(JSON.stringify(answer)) as RTCSessionDescriptionInit;
|
||||
socket.emit("offerFromHUD", roomId, { offer }, steamid);
|
||||
});
|
||||
|
||||
peerConnection.on('error', (err) => {
|
||||
console.log(err)
|
||||
closeConnection(steamid);
|
||||
});
|
||||
|
||||
peerConnection.on('stream', () => {
|
||||
console.log("STREAM COMING IN");
|
||||
const currentConnection = getConnectionInfo(steamid);
|
||||
if (!currentConnection) {
|
||||
console.log("Connection not established");
|
||||
closeConnection(steamid);
|
||||
return;
|
||||
}
|
||||
if (peerConnection._remoteStreams.length === 0) {
|
||||
console.log('no stream?');
|
||||
return;
|
||||
}
|
||||
for (const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)) {
|
||||
if (listener.event === "create") listener.listener(peerConnection._remoteStreams[0]);
|
||||
}
|
||||
});
|
||||
|
||||
peerConnection.on('close', () => {
|
||||
console.log("CLOSE COMING IN");
|
||||
const currentConnection = getConnectionInfo(steamid);
|
||||
if (!currentConnection) return;
|
||||
|
||||
closeConnection(steamid);
|
||||
});
|
||||
console.log("Sending offer");
|
||||
peerConnection.signal(offerData.offer);
|
||||
});
|
||||
}
|
||||
|
||||
export { mediaStreams, initiateConnection };
|
||||
15
src/HUD/Indicators/Armor.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {ArmorHelmet, ArmorFull} from './../../assets/Icons';
|
||||
export default class Armor extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(!player.state.health || !player.state.armor) return '';
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
15
src/HUD/Indicators/Bomb.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {Bomb as BombIcon} from './../../assets/Icons';
|
||||
export default class Bomb extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(Object.values(player.weapons).every(weapon => weapon.type !== "C4")) return '';
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
<BombIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
15
src/HUD/Indicators/Defuse.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {Defuse as DefuseIcon} from './../../assets/Icons';
|
||||
export default class Defuse extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(!player.state.health || !player.state.defusekit) return '';
|
||||
return (
|
||||
<div className={`defuse_indicator`}>
|
||||
<DefuseIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
55
src/HUD/Killfeed/Kill.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import Weapon from './../Weapon/Weapon';
|
||||
import flash_assist from './../../assets/flash_assist.png';
|
||||
|
||||
|
||||
import { C4, Defuse, FlashedKill, Headshot, NoScope, SmokeKill, Suicide, Wallbang } from "./../../assets/Icons"
|
||||
import { ExtendedKillEvent, BombEvent } from "./Killfeed"
|
||||
export default class Kill extends React.Component<{ event: ExtendedKillEvent | BombEvent }> {
|
||||
render() {
|
||||
const { event } = this.props;
|
||||
if (event.type !== "kill") {
|
||||
return (
|
||||
<div className={`single_kill`}>
|
||||
<div className={`killer_name ${event.player.team.side}`}>{event.player.name}</div>
|
||||
<div className="way">
|
||||
{event.type === "plant" ? <C4 height="18px" /> : <Defuse height="18px" />}
|
||||
</div>
|
||||
<div className={`victim_name`}>{event.type === "plant" ? "planted the bomb" : "defused the bomb"}</div>
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
let weapon = <Weapon weapon={event.weapon} active={false} />;
|
||||
if(event.killer === event.victim) {
|
||||
weapon = <Suicide />;
|
||||
} else if(event.weapon === 'planted_c4'){
|
||||
weapon = <Weapon weapon={'c4'} active={false} />
|
||||
}
|
||||
return (
|
||||
<div className='single_kill_container'>
|
||||
<div className={`single_kill`}>
|
||||
{event.attackerblind ? <FlashedKill /> : null}
|
||||
{event.killer ? <div className={`killer_name ${event.killer.team.side}`}>{event.killer.name}</div> : null}
|
||||
{event.assister ?
|
||||
<React.Fragment>
|
||||
<div className="plus">+</div>
|
||||
{event.flashed ? <img src={flash_assist} className="flash_assist" alt={'[FLASH]'} /> : null}
|
||||
<div className={`assister_name ${event.assister.team.side}`}>{event.assister.name}</div>
|
||||
</React.Fragment>
|
||||
: ''}
|
||||
<div className="way">
|
||||
{weapon}
|
||||
{event.thrusmoke ? <SmokeKill /> : null}
|
||||
{event.noscope ? <NoScope /> : null}
|
||||
{event.wallbang ? <Wallbang /> : null}
|
||||
{event.headshot ? <Headshot /> : null}
|
||||
</div>
|
||||
<div className={`victim_name ${event.victim.team.side}`}>{event.victim.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
76
src/HUD/Killfeed/Killfeed.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { GSI } from './../../App';
|
||||
import { KillEvent, Player } from 'csgogsi-socket';
|
||||
import Kill from './Kill';
|
||||
import './killfeed.scss';
|
||||
|
||||
|
||||
export interface ExtendedKillEvent extends KillEvent {
|
||||
type: 'kill'
|
||||
}
|
||||
|
||||
export interface BombEvent {
|
||||
player: Player,
|
||||
type: 'plant' | 'defuse'
|
||||
}
|
||||
|
||||
export default class Killfeed extends React.Component<any, { events: (BombEvent | ExtendedKillEvent)[] }> {
|
||||
|
||||
constructor(props: any){
|
||||
super(props);
|
||||
this.state = {
|
||||
events: []
|
||||
}
|
||||
}
|
||||
addKill = (kill: KillEvent) => {
|
||||
this.setState(state => {
|
||||
state.events.push({...kill, type: 'kill'});
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
addBombEvent = (player: Player, type: 'plant' | 'defuse') => {
|
||||
if(!player) return;
|
||||
const event: BombEvent = {
|
||||
player: player,
|
||||
type: type
|
||||
}
|
||||
this.setState(state => {
|
||||
state.events.push(event);
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
GSI.on("kill", kill => {
|
||||
this.addKill(kill);
|
||||
});
|
||||
GSI.on("data", data => {
|
||||
|
||||
if(data.round && data.round.phase === "freezetime"){
|
||||
if(Number(data.phase_countdowns.phase_ends_in) < 10 && this.state.events.length > 0){
|
||||
this.setState({events:[]})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
GSI.on("bombPlant", player => {
|
||||
this.addBombEvent(player, 'plant');
|
||||
})
|
||||
GSI.on("bombDefuse", player => {
|
||||
this.addBombEvent(player, 'defuse');
|
||||
})
|
||||
|
||||
*/
|
||||
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="killfeed">
|
||||
{this.state.events.map(event => <Kill event={event}/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
112
src/HUD/Killfeed/killfeed.scss
Normal file
@ -0,0 +1,112 @@
|
||||
@keyframes KillLifeCycle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
height: 43px;
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
height: 43px;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.killfeed {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 93px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.single_kill_container {
|
||||
animation: KillLifeCycle 5s linear 1;
|
||||
animation-fill-mode: forwards;/**/
|
||||
overflow: hidden;
|
||||
}
|
||||
.single_kill {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: rgba(0,0,0,0.64);
|
||||
margin-bottom:5px;
|
||||
color:white;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
font-family: Montserrat;
|
||||
font-weight:500;
|
||||
padding-left:15px;
|
||||
padding-right:15px;
|
||||
|
||||
> svg {
|
||||
max-height: 24px;
|
||||
max-width: 28px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin:10px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
svg.weapon {
|
||||
height: 28px;
|
||||
filter: invert(0%);
|
||||
}
|
||||
|
||||
.CT {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
|
||||
.T {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
|
||||
.way .headshot, .way .wallbang, .flash_assist, .flash_kill, .smoke_kill, .noscope_kill {
|
||||
max-height:20px;
|
||||
margin-left:5px;
|
||||
}
|
||||
|
||||
.flash_assist {
|
||||
margin-left: 0;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
.plus {
|
||||
margin-left: 0;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.assister_name {
|
||||
margin-left:0px;
|
||||
}
|
||||
|
||||
.way {
|
||||
text-align: center;
|
||||
|
||||
.wallbang {
|
||||
max-height: 24px;
|
||||
}
|
||||
svg:not(.weapon) {
|
||||
max-height: 24px;
|
||||
max-width: 28px;
|
||||
}
|
||||
}
|
||||
&.hide {
|
||||
transition:height 1s, padding 1s, opacity 1s, margin 1s;
|
||||
opacity:0;
|
||||
height:0;
|
||||
margin:0;
|
||||
}
|
||||
&.show {
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/HUD/Layout/Layout.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import TeamBox from "./../Players/TeamBox";
|
||||
import MatchBar from "../MatchBar/MatchBar";
|
||||
import SeriesBox from "../MatchBar/SeriesBox";
|
||||
import Observed from "./../Players/Observed";
|
||||
import { CSGO, Team } from "csgogsi-socket";
|
||||
import { Match } from "../../api/interfaces";
|
||||
import RadarMaps from "./../Radar/RadarMaps";
|
||||
import Trivia from "../Trivia/Trivia";
|
||||
import SideBox from '../SideBoxes/SideBox';
|
||||
import { GSI, actions } from "./../../App";
|
||||
import MoneyBox from '../SideBoxes/Money';
|
||||
import UtilityLevel from '../SideBoxes/UtilityLevel';
|
||||
import Killfeed from "../Killfeed/Killfeed";
|
||||
import MapSeries from "../MapSeries/MapSeries";
|
||||
import Overview from "../Overview/Overview";
|
||||
import Tournament from "../Tournament/Tournament";
|
||||
import Pause from "../PauseTimeout/Pause";
|
||||
import Timeout from "../PauseTimeout/Timeout";
|
||||
import PlayerCamera from "../Camera/Camera";
|
||||
|
||||
interface Props {
|
||||
game: CSGO,
|
||||
match: Match | null
|
||||
}
|
||||
|
||||
interface State {
|
||||
winner: Team | null,
|
||||
showWin: boolean,
|
||||
forceHide: boolean
|
||||
}
|
||||
|
||||
export default class Layout extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
winner: null,
|
||||
showWin: false,
|
||||
forceHide: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
GSI.on('roundEnd', score => {
|
||||
this.setState({ winner: score.winner, showWin: true }, () => {
|
||||
setTimeout(() => {
|
||||
this.setState({ showWin: false })
|
||||
}, 4000)
|
||||
});
|
||||
});
|
||||
actions.on("boxesState", (state: string) => {
|
||||
if (state === "show") {
|
||||
this.setState({ forceHide: false });
|
||||
} else if (state === "hide") {
|
||||
this.setState({ forceHide: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getVeto = () => {
|
||||
const { game, match } = this.props;
|
||||
const { map } = game;
|
||||
if (!match) return null;
|
||||
const mapName = map.name.substring(map.name.lastIndexOf('/') + 1);
|
||||
const veto = match.vetos.find(veto => veto.mapName === mapName);
|
||||
if (!veto) return null;
|
||||
return veto;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { game, match } = this.props;
|
||||
const left = game.map.team_ct.orientation === "left" ? game.map.team_ct : game.map.team_t;
|
||||
const right = game.map.team_ct.orientation === "left" ? game.map.team_t : game.map.team_ct;
|
||||
|
||||
const leftPlayers = game.players.filter(player => player.team.side === left.side);
|
||||
const rightPlayers = game.players.filter(player => player.team.side === right.side);
|
||||
const isFreezetime = (game.round && game.round.phase === "freezetime") || game.phase_countdowns.phase === "freezetime";
|
||||
const { forceHide } = this.state;
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<div className={`players_alive`}>
|
||||
<div className="title_container">Players alive</div>
|
||||
<div className="counter_container">
|
||||
<div className={`team_counter ${left.side}`}>{leftPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
<div className={`vs_counter`}>VS</div>
|
||||
<div className={`team_counter ${right.side}`}>{rightPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Killfeed />
|
||||
<Overview match={match} map={game.map} players={game.players || []} />
|
||||
<RadarMaps match={match} map={game.map} game={game} />
|
||||
<MatchBar map={game.map} phase={game.phase_countdowns} bomb={game.bomb} match={match} />
|
||||
<Pause phase={game.phase_countdowns}/>
|
||||
<Timeout map={game.map} phase={game.phase_countdowns} />
|
||||
<SeriesBox map={game.map} phase={game.phase_countdowns} match={match} />
|
||||
|
||||
<Tournament />
|
||||
|
||||
<Observed player={game.player} veto={this.getVeto()} round={game.map.round+1}/>
|
||||
|
||||
<TeamBox team={left} players={leftPlayers} side="left" current={game.player} />
|
||||
<TeamBox team={right} players={rightPlayers} side="right" current={game.player} />
|
||||
|
||||
<Trivia />
|
||||
|
||||
<MapSeries teams={[left, right]} match={match} isFreezetime={isFreezetime} map={game.map} />
|
||||
<div className={"boxes left"}>
|
||||
<UtilityLevel side={left.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="left" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={left.side}
|
||||
side="left"
|
||||
loss={Math.min(left.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={leftPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={leftPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
<div className={"boxes right"}>
|
||||
<UtilityLevel side={right.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="right" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={right.side}
|
||||
side="right"
|
||||
loss={Math.min(right.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={rightPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={rightPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
62
src/HUD/MapSeries/MapSeries.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import { Match, Veto } from '../../api/interfaces';
|
||||
import TeamLogo from "../MatchBar/TeamLogo";
|
||||
import "./mapseries.scss";
|
||||
|
||||
interface IProps {
|
||||
match: Match | null;
|
||||
teams: I.Team[];
|
||||
isFreezetime: boolean;
|
||||
map: I.Map
|
||||
}
|
||||
|
||||
interface IVetoProps {
|
||||
veto: Veto;
|
||||
teams: I.Team[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
class VetoEntry extends React.Component<IVetoProps> {
|
||||
render(){
|
||||
const { veto, teams, active } = this.props;
|
||||
return <div className={`veto_container ${active ? 'active' : ''}`}>
|
||||
<div className="veto_map_name">
|
||||
{veto.mapName}
|
||||
</div>
|
||||
<div className="veto_picker">
|
||||
<TeamLogo team={teams.filter(team => team.id === veto.teamId)[0]} />
|
||||
</div>
|
||||
<div className="veto_winner">
|
||||
<TeamLogo team={teams.filter(team => team.id === veto.winner)[0]} />
|
||||
</div>
|
||||
<div className="veto_score">
|
||||
{Object.values((veto.score || ['-','-'])).sort().join(":")}
|
||||
</div>
|
||||
<div className='active_container'>
|
||||
<div className='active'>Currently playing</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default class MapSeries extends React.Component<IProps> {
|
||||
|
||||
render() {
|
||||
const { match, teams, isFreezetime, map } = this.props;
|
||||
if (!match || !match.vetos.length) return null;
|
||||
return (
|
||||
<div className={`map_series_container ${isFreezetime ? 'show': 'hide'}`}>
|
||||
<div className="title_bar">
|
||||
<div className="picked">Picked</div>
|
||||
<div className="winner">Winner</div>
|
||||
<div className="score">Score</div>
|
||||
</div>
|
||||
{match.vetos.filter(veto => veto.type !== "ban").map(veto => {
|
||||
if(!veto.mapName) return null;
|
||||
return <VetoEntry key={`${match.id}${veto.mapName}${veto.teamId}${veto.side}`} veto={veto} teams={teams} active={map.name.includes(veto.mapName)}/>
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
153
src/HUD/MapSeries/mapseries.scss
Normal file
@ -0,0 +1,153 @@
|
||||
.map_series_container {
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 20px;
|
||||
width: 347px;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
.veto_container {
|
||||
display: flex;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
&.active {
|
||||
.veto_winner {
|
||||
display: none;
|
||||
}
|
||||
.veto_score {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.veto_map_name {
|
||||
width: 116px;
|
||||
text-transform: uppercase;
|
||||
background-color: rgba(255,255,255,1);
|
||||
color: black;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
font-family: Montserrat;
|
||||
font-weight: 700;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
div {
|
||||
img {
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
}
|
||||
}
|
||||
.veto_picker {
|
||||
width: 77px;
|
||||
text-align: center;
|
||||
}
|
||||
.veto_winner {
|
||||
width: 77px;
|
||||
text-align: center;
|
||||
}
|
||||
.active_container {
|
||||
width: 154px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.active {
|
||||
background: white;
|
||||
color: black;
|
||||
width: 117px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
font-family: Montserrat;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
&:not(.active) {
|
||||
.active_container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.veto_score {
|
||||
width: 77px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.title_bar {
|
||||
display: flex;
|
||||
background-color: rgba(0,0,0,0.64);
|
||||
height: 25px;
|
||||
color: white;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-family: Montserrat;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
div {
|
||||
width: 77px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.picked {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#timeout {
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 150px;
|
||||
background-color: var(--sub-panel-color);
|
||||
width: 600px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
font-size: 35px;
|
||||
transition: opacity 1.5s;
|
||||
opacity: 0;
|
||||
}
|
||||
#pause {
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 150px;
|
||||
background-color: var(--sub-panel-color);
|
||||
width: 600px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
font-size: 35px;
|
||||
transition: opacity 1.5s;
|
||||
opacity: 0;
|
||||
color: white;
|
||||
}
|
||||
#timeout.show, #pause.show {
|
||||
opacity: 1;
|
||||
}
|
||||
#timeout.t {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
#timeout.ct {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
203
src/HUD/MatchBar/MatchBar.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import "./matchbar.scss";
|
||||
import TeamScore from "./TeamScore";
|
||||
import Bomb from "./../Timers/BombTimer";
|
||||
import Countdown from "./../Timers/Countdown";
|
||||
import { GSI } from "../../App";
|
||||
import { Match } from "../../api/interfaces";
|
||||
|
||||
function stringToClock(time: string | number, pad = true) {
|
||||
if (typeof time === "string") {
|
||||
time = parseFloat(time);
|
||||
}
|
||||
const countdown = Math.abs(Math.ceil(time));
|
||||
const minutes = Math.floor(countdown / 60);
|
||||
const seconds = countdown - minutes * 60;
|
||||
if (pad && seconds < 10) {
|
||||
return `${minutes}:0${seconds}`;
|
||||
}
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
match: Match | null;
|
||||
map: I.Map;
|
||||
phase: I.PhaseRaw,
|
||||
bomb: I.Bomb | null,
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
width: number;
|
||||
active: boolean;
|
||||
countdown: number;
|
||||
side: "left"|"right";
|
||||
type: "defusing" | "planting";
|
||||
player: I.Player | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
defusing: Timer,
|
||||
planting: Timer,
|
||||
winState: {
|
||||
side: "left"|"right",
|
||||
show: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default class TeamBox extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps){
|
||||
super(props);
|
||||
this.state = {
|
||||
defusing: {
|
||||
width: 0,
|
||||
active: false,
|
||||
countdown: 10,
|
||||
side: "left",
|
||||
type: "defusing",
|
||||
player: null
|
||||
},
|
||||
planting: {
|
||||
width: 0,
|
||||
active: false,
|
||||
countdown: 10, // Fake
|
||||
side: "right",
|
||||
type: "planting",
|
||||
player: null
|
||||
},
|
||||
winState: {
|
||||
side: 'left',
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
plantStop = () => this.setState(state => {
|
||||
state.planting.active = false;
|
||||
return state;
|
||||
});
|
||||
|
||||
setWidth = (type: 'defusing' | 'planting', width: number) => {
|
||||
this.setState(state => {
|
||||
state[type].width = width;
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
initPlantTimer = () => {
|
||||
const bomb = new Countdown(time => {
|
||||
let width = time * 100;
|
||||
this.setWidth("planting", width/3);
|
||||
});
|
||||
GSI.on("bombPlantStart", player => {
|
||||
if(!player || !player.team) return;
|
||||
this.setState(state => {
|
||||
state.planting.active = true;
|
||||
state.planting.side = player.team.orientation;
|
||||
state.planting.player = player;
|
||||
})
|
||||
})
|
||||
GSI.on("data", data => {
|
||||
if(!data.bomb || !data.bomb.countdown || data.bomb.state !== "planting") return this.plantStop();
|
||||
this.setState(state => {
|
||||
state.planting.active = true;
|
||||
})
|
||||
return bomb.go(data.bomb.countdown);
|
||||
});
|
||||
}
|
||||
|
||||
defuseStop = () => this.setState(state => {
|
||||
state.defusing.active = false;
|
||||
state.defusing.countdown = 10;
|
||||
return state;
|
||||
});
|
||||
|
||||
initDefuseTimer = () => {
|
||||
const bomb = new Countdown(time => {
|
||||
let width = time > this.state.defusing.countdown ? this.state.defusing.countdown*100 : time * 100;
|
||||
this.setWidth("defusing", width/this.state.defusing.countdown);
|
||||
});
|
||||
GSI.on("defuseStart", player => {
|
||||
if(!player || !player.team) return;
|
||||
this.setState(state => {
|
||||
state.defusing.active = true;
|
||||
state.defusing.countdown = !Boolean(player.state.defusekit) ? 10 : 5;
|
||||
state.defusing.side = player.team.orientation;
|
||||
state.defusing.player = player;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
GSI.on("data", data => {
|
||||
if(!data.bomb || !data.bomb.countdown || data.bomb.state !== "defusing") return this.defuseStop();
|
||||
this.setState(state => {
|
||||
state.defusing.active = true;
|
||||
return state;
|
||||
})
|
||||
return bomb.go(data.bomb.countdown);
|
||||
});
|
||||
}
|
||||
|
||||
resetWin = () => {
|
||||
setTimeout(() => {
|
||||
this.setState(state => {
|
||||
state.winState.show = false;
|
||||
return state;
|
||||
})
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.initDefuseTimer();
|
||||
this.initPlantTimer();
|
||||
GSI.on("roundEnd", score => {
|
||||
this.setState(state => {
|
||||
state.winState.show = true;
|
||||
state.winState.side = score.winner.orientation;
|
||||
return state;
|
||||
}, this.resetWin);
|
||||
});
|
||||
}
|
||||
getRoundLabel = () => {
|
||||
const { map } = this.props;
|
||||
const round = map.round + 1;
|
||||
if (round <= 30) {
|
||||
return `Round ${round}/30`;
|
||||
}
|
||||
const additionalRounds = round - 30;
|
||||
const OT = Math.ceil(additionalRounds/6);
|
||||
return `OT ${OT} (${additionalRounds - (OT - 1)*6}/6)`;
|
||||
}
|
||||
render() {
|
||||
const { defusing, planting, winState } = this.state;
|
||||
const { bomb, match, map, phase } = this.props;
|
||||
const time = stringToClock(phase.phase_ends_in);
|
||||
const left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
||||
const right = map.team_ct.orientation === "left" ? map.team_t : map.team_ct;
|
||||
const isPlanted = bomb && (bomb.state === "defusing" || bomb.state === "planted");
|
||||
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
||||
let leftTimer: Timer | null = null, rightTimer: Timer | null = null;
|
||||
if(defusing.active || planting.active){
|
||||
if(defusing.active){
|
||||
if(defusing.side === "left") leftTimer = defusing;
|
||||
else rightTimer = defusing;
|
||||
} else {
|
||||
if(planting.side === "left") leftTimer = planting;
|
||||
else rightTimer = planting;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div id={`matchbar`}>
|
||||
<TeamScore team={left} orientation={"left"} timer={leftTimer} showWin={winState.show && winState.side === "left"} />
|
||||
<div className={`score left ${left.side}`}>{left.score}</div>
|
||||
<div id="timer" className={bo === 0 ? 'no-bo' : ''}>
|
||||
<div id={`round_timer_text`} className={isPlanted ? "hide":""}>{time}</div>
|
||||
<div id="round_now" className={isPlanted ? "hide":""}>{this.getRoundLabel()}</div>
|
||||
<Bomb />
|
||||
</div>
|
||||
<div className={`score right ${right.side}`}>{right.score}</div>
|
||||
<TeamScore team={right} orientation={"right"} timer={rightTimer} showWin={winState.show && winState.side === "right"} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/HUD/MatchBar/SeriesBox.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import { Match } from "../../api/interfaces";
|
||||
|
||||
interface Props {
|
||||
map: I.Map;
|
||||
phase: I.PhaseRaw;
|
||||
match: Match | null;
|
||||
}
|
||||
|
||||
export default class SeriesBox extends React.Component<Props> {
|
||||
render() {
|
||||
const { match, map } = this.props;
|
||||
const amountOfMaps = (match && Math.floor(Number(match.matchType.substr(-1)) / 2) + 1) || 0;
|
||||
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
||||
const left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
||||
const right = map.team_ct.orientation === "left" ? map.team_t : map.team_ct;
|
||||
return (
|
||||
<div id="encapsulator">
|
||||
<div className="container left">
|
||||
<div className={`series_wins left `}>
|
||||
<div className={`wins_box_container`}>
|
||||
{new Array(amountOfMaps).fill(0).map((_, i) => (
|
||||
<div key={i} className={`wins_box ${left.matches_won_this_series > i ? "win" : ""} ${left.side}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="series_container">
|
||||
<div id="series_text">{ bo ? `BEST OF ${bo}` : '' }</div>
|
||||
</div>
|
||||
<div className="container right">
|
||||
<div className={`series_wins right `}>
|
||||
<div className={`wins_box_container`}>
|
||||
{new Array(amountOfMaps).fill(0).map((_, i) => (
|
||||
<div key={i} className={`wins_box ${right.matches_won_this_series > i ? "win" : ""} ${right.side}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/HUD/MatchBar/TeamLogo.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Team } from 'csgogsi-socket';
|
||||
import * as I from '../../api/interfaces';
|
||||
import { apiUrl } from './../../api/api';
|
||||
|
||||
export default class TeamLogo extends React.Component<{ team?: Team | I.Team | null, height?: number, width?: number}> {
|
||||
render(){
|
||||
const { team } = this.props;
|
||||
if(!team) return null;
|
||||
let id = '';
|
||||
const { logo } = team;
|
||||
if('_id' in team){
|
||||
id = team._id;
|
||||
} else if('id' in team && team.id){
|
||||
id = team.id;
|
||||
}
|
||||
return (
|
||||
<div className={`logo`}>
|
||||
{ logo && id ? <img src={`${apiUrl}api/teams/logo/${id}`} width={this.props.width} height={this.props.height} alt={'Team logo'} /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
30
src/HUD/MatchBar/TeamScore.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import WinIndicator from "./WinIndicator";
|
||||
import { Timer } from "./MatchBar";
|
||||
import TeamLogo from './TeamLogo';
|
||||
import PlantDefuse from "../Timers/PlantDefuse"
|
||||
|
||||
interface IProps {
|
||||
team: I.Team;
|
||||
orientation: "left" | "right";
|
||||
timer: Timer | null;
|
||||
showWin: boolean;
|
||||
}
|
||||
|
||||
export default class TeamScore extends React.Component<IProps> {
|
||||
render() {
|
||||
const { orientation, timer, team, showWin } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className={`team ${orientation} ${team.side}`}>
|
||||
<div className="team-name">{team.name}</div>
|
||||
<TeamLogo team={team} />
|
||||
<div className="round-thingy"><div className="inner"></div></div>
|
||||
</div>
|
||||
<PlantDefuse timer={timer} side={orientation} />
|
||||
<WinIndicator team={team} show={showWin}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/HUD/MatchBar/WinIndicator.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Team } from 'csgogsi-socket';
|
||||
|
||||
export default class WinAnnouncement extends React.Component<{ team: Team | null, show: boolean }> {
|
||||
render() {
|
||||
const { team, show } = this.props;
|
||||
if(!team) return null;
|
||||
return <div className={`win_text ${show ? 'show' : ''} ${team.orientation} ${team.side}`}>
|
||||
WINS THE ROUND!
|
||||
</div>
|
||||
}
|
||||
}
|
||||
391
src/HUD/MatchBar/matchbar.scss
Normal file
@ -0,0 +1,391 @@
|
||||
@keyframes ShowWinCycle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
5% {
|
||||
opacity: 1;
|
||||
height: 50px;
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
height: 50px;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
#matchbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: fixed;
|
||||
justify-content: center;
|
||||
width: 1148px;
|
||||
height: 70px;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
.CT {
|
||||
color: var(--color-new-ct);
|
||||
.round-thingy {
|
||||
.inner {
|
||||
background-color: #28abff;
|
||||
}
|
||||
background-color: #28abff80;
|
||||
}
|
||||
}
|
||||
.T {
|
||||
color: var(--color-new-t);
|
||||
.round-thingy {
|
||||
.inner {
|
||||
background-color: #ffc600;
|
||||
}
|
||||
background-color: #ffc60080;
|
||||
}
|
||||
}
|
||||
#timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 126px;
|
||||
height: 115px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
background-color: var(--sub-panel-color);
|
||||
top: -10px;
|
||||
&.no-bo {
|
||||
height: 87px;
|
||||
}
|
||||
}
|
||||
#bomb_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
z-index: 0;
|
||||
.bomb_timer {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 0;
|
||||
background-color: var(--color-bomb);
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.bomb_icon {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 6px;
|
||||
max-height: 80%;
|
||||
max-width: 80%;
|
||||
}
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
#round_timer_text {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 55px;
|
||||
justify-content: center;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
z-index: 1;
|
||||
color: var(--white-full);
|
||||
align-items: flex-end;
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#round_now {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 27px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
z-index: 1;
|
||||
color: var(--white-full);
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.team {
|
||||
width: 426px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 105px;
|
||||
height: 70px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background-color: var(--sub-panel-color);
|
||||
img {
|
||||
max-width: 90%;
|
||||
max-height: 65%;
|
||||
}
|
||||
}
|
||||
&.left {
|
||||
justify-content: center;
|
||||
flex-direction: row-reverse;
|
||||
.round-thingy {
|
||||
left: -30px;
|
||||
}
|
||||
.logo {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
.round-thingy {
|
||||
right: -30px;
|
||||
}
|
||||
.logo {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
.team-name {
|
||||
display: flex;
|
||||
width: 360px;
|
||||
height: 70px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
background-color: var(--sub-panel-color);
|
||||
}
|
||||
.round-thingy {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
.inner {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.score {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 77px;
|
||||
height: 70px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
background-color: var(--sub-panel-color);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 10px;
|
||||
height: 70px;
|
||||
&.CT {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
&.T {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.win_text {
|
||||
position: fixed;
|
||||
display: none;
|
||||
opacity: 1;
|
||||
width: 503px;
|
||||
height: 50px;
|
||||
top: 70px;
|
||||
align-items: center;
|
||||
color: black;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
font-size: 20px;
|
||||
font-family: Montserrat;
|
||||
font-weight: 600;
|
||||
&.show {
|
||||
display: flex;
|
||||
animation: ShowWinCycle 5s linear 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&.right {
|
||||
left: calc(50% + 71px);
|
||||
}
|
||||
&.left {
|
||||
right: calc(50% + 71px);
|
||||
}
|
||||
}
|
||||
|
||||
.defuse_plant_container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
width: 503px;
|
||||
height: 49px;
|
||||
top: 70px;
|
||||
align-items: center;
|
||||
color: white;
|
||||
justify-content: center;
|
||||
background-color: rgba(0,0,0,0.65);
|
||||
.defuse_plant_bar {
|
||||
height: 49px;
|
||||
background-color: #3c3c3c;
|
||||
position: absolute;
|
||||
width: 0%;
|
||||
}
|
||||
.defuse_plant_caption {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
text-transform: uppercase;
|
||||
align-items: flex-end;
|
||||
svg {
|
||||
margin-right: 13px;
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
left: calc(50% + 71px);
|
||||
.defuse_plant_bar {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
&.left {
|
||||
right: calc(50% + 71px);
|
||||
.defuse_plant_bar {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
#encapsulator {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: fixed;
|
||||
justify-content: center;
|
||||
top: 80px;
|
||||
width: 1148px;
|
||||
height: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
.CT {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
.T {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
.wins_bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 10px;
|
||||
height: 30px;
|
||||
}
|
||||
.wins_bar.CT {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
.wins_bar.T {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
.alert_bar.CT {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
.alert_bar.T {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
#series_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 126px;
|
||||
height: 30px;
|
||||
}
|
||||
#series_text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: var(--white-full);
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 511px;
|
||||
height: 100%;
|
||||
}
|
||||
.container.left {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.container.right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.series_wins {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 400px;
|
||||
height: 30px;
|
||||
z-index: 1;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
top: -30px;
|
||||
transition: top 0.5s;
|
||||
}
|
||||
.series_wins.show {
|
||||
top: 0px;
|
||||
}
|
||||
.wins_box_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.series_wins.left {
|
||||
.wins_box_container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
.wins_box {
|
||||
width: 77px;
|
||||
height: 7px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wins_box.CT {
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
}
|
||||
.wins_box.CT.win {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
.wins_box.T {
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
}
|
||||
.wins_box.T.win {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
41
src/HUD/MatchOverview/MatchOverview.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import TeamLogo from '../MatchBar/TeamLogo';
|
||||
|
||||
interface IProps {
|
||||
match: I.Match,
|
||||
show: boolean,
|
||||
teams: I.Team[],
|
||||
veto: I.Veto | null
|
||||
}
|
||||
|
||||
export default class MatchOverview extends React.Component<IProps> {
|
||||
render() {
|
||||
const { match, teams, show } = this.props;
|
||||
const left = teams.find(team => team._id === match.left.id);
|
||||
const right = teams.find(team => team._id === match.right.id);
|
||||
if(!match || !left || !right) return null;
|
||||
return (
|
||||
<div className={`match-overview ${show ? 'show':''}`}>
|
||||
<div className="match-overview-title">
|
||||
Upcoming match
|
||||
</div>
|
||||
<div className="match-overview-teams">
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={left} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{left.name}</div>
|
||||
</div>
|
||||
<div className="match-overview-vs">vs</div>
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={right} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{right.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
125
src/HUD/Overview/Overview.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { actions, configs } from '../../App';
|
||||
import * as I from '../../api/interfaces';
|
||||
import PlayerOverview from '../PlayerOverview/PlayerOverview';
|
||||
import MatchOverview from '../MatchOverview/MatchOverview';
|
||||
import TeamOverview from '../TeamOverview/TeamOverview';
|
||||
import { Map, Player } from 'csgogsi-socket';
|
||||
import api from '../../api/api';
|
||||
|
||||
interface IState {
|
||||
player: {
|
||||
data: I.Player | null,
|
||||
show: boolean
|
||||
},
|
||||
match: {
|
||||
data: I.Match | null,
|
||||
show: boolean,
|
||||
teams: I.Team[],
|
||||
},
|
||||
team: {
|
||||
data: I.Team | null,
|
||||
show: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
match: I.Match | null,
|
||||
map: Map,
|
||||
players: Player[]
|
||||
}
|
||||
|
||||
export default class Overview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps){
|
||||
super(props);
|
||||
this.state = {
|
||||
player: {
|
||||
data: null,
|
||||
show: false
|
||||
},
|
||||
match: {
|
||||
data: null,
|
||||
show: false,
|
||||
teams: []
|
||||
},
|
||||
team: {
|
||||
data: null,
|
||||
show: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
loadTeams = async () => {
|
||||
const { match } = this.state;
|
||||
if(!match.data || !match.data.left.id || !match.data.right.id) return;
|
||||
const teams = await Promise.all([api.teams.getOne(match.data.left.id), api.teams.getOne(match.data.right.id)]);
|
||||
if(!teams[0] || !teams[1]) return;
|
||||
this.setState(state => {
|
||||
state.match.teams = teams;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
configs.onChange((data: any) => {
|
||||
if(!data || !data.preview_settings) return;
|
||||
this.setState({
|
||||
player: {
|
||||
data: (data.preview_settings.player_preview && data.preview_settings.player_preview.player) || null,
|
||||
show: Boolean(data.preview_settings.player_preview_toggle)
|
||||
},
|
||||
team: {
|
||||
data: (data.preview_settings.team_preview && data.preview_settings.team_preview.team) || null,
|
||||
show: Boolean(data.preview_settings.team_preview_toggle)
|
||||
},
|
||||
match: {
|
||||
data: (data.preview_settings.match_preview && data.preview_settings.match_preview.match) || null,
|
||||
show: Boolean(data.preview_settings.match_preview_toggle),
|
||||
teams: this.state.match.teams
|
||||
}
|
||||
}, this.loadTeams);
|
||||
});
|
||||
actions.on("toggleUpcomingMatch", () => {
|
||||
this.setState(state => {
|
||||
state.match.show = !state.match.show;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
actions.on("togglePlayerPreview", () => {
|
||||
this.setState(state => {
|
||||
state.player.show = !state.player.show;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
}
|
||||
getVeto = () => {
|
||||
const { map, match } = this.props;
|
||||
if(!match) return null;
|
||||
const mapName = map.name.substring(map.name.lastIndexOf('/')+1);
|
||||
const veto = match.vetos.find(veto => veto.mapName === mapName);
|
||||
if(!veto) return null;
|
||||
return veto;
|
||||
}
|
||||
renderPlayer = () => {
|
||||
const { player } = this.state;
|
||||
if(!player.data) return null;
|
||||
return <PlayerOverview round={this.props.map.round + 1} player={player.data} players={this.props.players} show={player.show} veto={this.getVeto()} />
|
||||
}
|
||||
renderMatch = () => {
|
||||
const { match } = this.state;
|
||||
if(!match.data || !match.teams[0] || !match.teams[1]) return null;
|
||||
return <MatchOverview match={match.data} show={match.show} veto={this.getVeto()} teams={match.teams}/>
|
||||
}
|
||||
renderTeam = () => {
|
||||
const { team } = this.state;
|
||||
if(!team.data) return null;
|
||||
return <TeamOverview team={team.data} show={team.show} veto={this.getVeto()} />
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderPlayer()}
|
||||
{this.renderMatch()}
|
||||
{this.renderTeam()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/HUD/PauseTimeout/Pause.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { PhaseRaw } from "csgogsi-socket";
|
||||
|
||||
interface IProps {
|
||||
phase: PhaseRaw | null
|
||||
}
|
||||
|
||||
export default class Pause extends React.Component<IProps> {
|
||||
render() {
|
||||
const { phase } = this.props;
|
||||
return (
|
||||
<div id={`pause`} className={phase && phase.phase === "paused" ? 'show' : ''}>
|
||||
PAUSE
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/HUD/PauseTimeout/Timeout.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Map, PhaseRaw } from "csgogsi-socket";
|
||||
|
||||
|
||||
interface IProps {
|
||||
phase: PhaseRaw | null,
|
||||
map: Map
|
||||
}
|
||||
|
||||
export default class Timeout extends React.Component<IProps> {
|
||||
render() {
|
||||
const { phase, map } = this.props;
|
||||
const time = phase && Math.abs(Math.ceil(parseFloat(phase.phase_ends_in)));
|
||||
const team = phase && phase.phase === "timeout_t" ? map.team_t : map.team_ct;
|
||||
|
||||
return (
|
||||
<div id={`timeout`} className={`${time && time > 2 && phase && (phase.phase === "timeout_t" || phase.phase === "timeout_ct") ? 'show' : ''} ${phase && (phase.phase === "timeout_t" || phase.phase === "timeout_ct") ? phase.phase.substr(8): ''}`}>
|
||||
{ team.name } TIMEOUT
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
104
src/HUD/PlayerOverview/PlayerOverview.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import { avatars } from './../../api/avatars';
|
||||
import { apiUrl } from '../../api/api';
|
||||
import { getCountry } from '../countries';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import "./playeroverview.scss";
|
||||
|
||||
interface IProps {
|
||||
player: I.Player,
|
||||
show: boolean,
|
||||
veto: I.Veto | null
|
||||
players: Player[],
|
||||
round: number
|
||||
}
|
||||
|
||||
export default class PlayerOverview extends React.Component<IProps> {
|
||||
sum = (data: number[]) => data.reduce((a, b) => a + b, 0);
|
||||
|
||||
getData = () => {
|
||||
const { veto, player, round } = this.props;
|
||||
if(!player || !veto || !veto.rounds) return null;
|
||||
const stats = veto.rounds.map(round => round ? round.players[player.steamid] : {
|
||||
kills: 0,
|
||||
killshs: 0,
|
||||
damage: 0
|
||||
}).filter(data => !!data);
|
||||
const overall = {
|
||||
damage: this.sum(stats.map(round => round.damage)),
|
||||
kills: this.sum(stats.map(round => round.kills)),
|
||||
killshs: this.sum(stats.map(round => round.killshs)),
|
||||
};
|
||||
const data = {
|
||||
adr: stats.length !== 0 ? (overall.damage/(round-1)).toFixed(0) : '0',
|
||||
kills: overall.kills,
|
||||
killshs: overall.kills,
|
||||
kpr: stats.length !== 0 ? (overall.kills/stats.length).toFixed(2) : 0,
|
||||
hsp: overall.kills !== 0 ? (100*overall.killshs/overall.kills).toFixed(0) : '0'
|
||||
}
|
||||
return data;
|
||||
}
|
||||
calcWidth = (val: number | string, max?: number) => {
|
||||
const value = Number(val);
|
||||
if(value === 0) return 0;
|
||||
let maximum = max;
|
||||
if(!maximum) {
|
||||
maximum = Math.ceil(value/100)*100;
|
||||
}
|
||||
if(value > maximum){
|
||||
return 100;
|
||||
}
|
||||
return 100*value/maximum;
|
||||
}
|
||||
render() {
|
||||
const { player, veto, players } = this.props;
|
||||
const data = this.getData();
|
||||
if(!player || !veto || !veto.rounds || !data) return null;
|
||||
let url = null;
|
||||
// const avatarData = avatars.find(avatar => avatar.steamid === player.steamid);
|
||||
const avatarData = avatars[player.steamid];
|
||||
if(avatarData && avatarData.url){
|
||||
url = avatarData.url;
|
||||
}
|
||||
const countryName = player.country ? getCountry(player.country) : null;
|
||||
let side = '';
|
||||
const inGamePlayer = players.find(inGamePlayer => inGamePlayer.steamid === player.steamid);
|
||||
if(inGamePlayer) side = inGamePlayer.team.side;
|
||||
return (
|
||||
<div className={`player-overview ${this.props.show ? 'show':''} ${side}`}>
|
||||
<div className="player-overview-picture">
|
||||
{url ? <img src={url} alt={`${player.username}'s avatar`}/> : null}
|
||||
</div>
|
||||
<div className="player-overview-username">{url && countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} className="flag" alt={countryName}/> : null }{player.username.toUpperCase()}</div>
|
||||
|
||||
<div className="player-overview-stats">
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KILLS: {data.kills}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.kills, data.kills <= 30 ? 30 : 40)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">HS: {data.hsp}%</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.hsp, 100)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">ADR: {data.adr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.adr)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KPR: {data.kpr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(Number(data.kpr)*100)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
74
src/HUD/PlayerOverview/playeroverview.scss
Normal file
@ -0,0 +1,74 @@
|
||||
.player-overview {
|
||||
position: fixed;
|
||||
top: 130px;
|
||||
background-color: #000000a8;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 300px;
|
||||
padding: 30px;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.75s;
|
||||
}
|
||||
.player-overview.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.player-overview-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 8px 0;
|
||||
.panel {
|
||||
height: 10px;
|
||||
margin: 3px 0;
|
||||
.filling {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.player-overview.CT {
|
||||
.player-overview-stat {
|
||||
.panel {
|
||||
.filling {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
background-color: #082435;
|
||||
}
|
||||
}
|
||||
}
|
||||
.player-overview.T {
|
||||
.player-overview-stat {
|
||||
.panel {
|
||||
.filling {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
background-color: #331b00;
|
||||
}
|
||||
}
|
||||
}
|
||||
.player-overview-picture {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
margin-bottom: 20px;
|
||||
max-width: 170px;
|
||||
max-height: 170px;
|
||||
}
|
||||
}
|
||||
.player-overview-username {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
img.flag {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
41
src/HUD/Players/Avatar.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import CameraContainer from '../Camera/Container';
|
||||
import PlayerCamera from "./../Camera/Camera";
|
||||
|
||||
import { avatars } from './../../api/avatars';
|
||||
|
||||
import { Skull } from './../../assets/Icons';
|
||||
|
||||
interface IProps {
|
||||
steamid: string,
|
||||
slot?: number,
|
||||
height?: number,
|
||||
width?: number,
|
||||
showSkull?: boolean,
|
||||
showCam?: boolean,
|
||||
sidePlayer?: boolean
|
||||
}
|
||||
export default class Avatar extends React.Component<IProps> {
|
||||
|
||||
render() {
|
||||
const { showCam, steamid, width, height, showSkull, sidePlayer } = this.props;
|
||||
//const url = avatars.filter(avatar => avatar.steamid === this.props.steamid)[0];
|
||||
const avatarData = avatars[this.props.steamid];
|
||||
if (!avatarData || !avatarData.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`avatar`}>
|
||||
{
|
||||
showCam ? ( sidePlayer ? <div className="videofeed"><PlayerCamera steamid={steamid} visible={true} /></div> : <CameraContainer observedSteamid={steamid} />) : null
|
||||
}
|
||||
{
|
||||
showSkull ? <Skull height={height} width={width} /> : <img src={avatarData.url} height={height} width={width} alt={'Avatar'} />
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
106
src/HUD/Players/Observed.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React from "react";
|
||||
import { Player } from "csgogsi-socket";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import Avatar from "./Avatar";
|
||||
import TeamLogo from "./../MatchBar/TeamLogo";
|
||||
import "./observed.scss";
|
||||
import { apiUrl } from './../../api/api';
|
||||
import { getCountry } from "./../countries";
|
||||
import { ArmorHelmet, ArmorFull, HealthFull, Bullets } from './../../assets/Icons';
|
||||
import { Veto } from "../../api/interfaces";
|
||||
import { actions } from "../../App";
|
||||
|
||||
class Statistic extends React.PureComponent<{ label: string; value: string | number, }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="stat">
|
||||
<div className="label">{this.props.label}</div>
|
||||
<div className="value">{this.props.value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Observed extends React.Component<{ player: Player | null, veto: Veto | null, round: number }, { showCam: boolean }> {
|
||||
constructor(props: any){
|
||||
super(props);
|
||||
this.state = {
|
||||
showCam: true
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
actions.on('toggleCams', () => {
|
||||
console.log(this.state.showCam)
|
||||
this.setState({ showCam: !this.state.showCam });
|
||||
});
|
||||
}
|
||||
getAdr = () => {
|
||||
const { veto, player } = this.props;
|
||||
if (!player || !veto || !veto.rounds) return null;
|
||||
const damageInRounds = veto.rounds.map(round => round ? round.players[player.steamid] : {
|
||||
kills: 0,
|
||||
killshs: 0,
|
||||
damage: 0
|
||||
}).filter(data => !!data).map(roundData => roundData.damage);
|
||||
|
||||
return damageInRounds.reduce((a, b) => a + b, 0) / (this.props.round - 1);
|
||||
}
|
||||
render() {
|
||||
if (!this.props.player) return '';
|
||||
const { player } = this.props;
|
||||
const country = player.country || player.team.country;
|
||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
||||
const currentWeapon = weapons.filter(weapon => weapon.state === "active")[0];
|
||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
||||
const { stats } = player;
|
||||
const ratio = stats.deaths === 0 ? stats.kills : stats.kills / stats.deaths;
|
||||
const countryName = country ? getCountry(country) : null;
|
||||
return (
|
||||
<div className={`observed ${player.team.side}`}>
|
||||
<div className="main_row">
|
||||
{<Avatar steamid={player.steamid} height={140} width={140} showCam={this.state.showCam} slot={player.observer_slot} />}
|
||||
<TeamLogo team={player.team} height={35} width={35} />
|
||||
<div className="username_container">
|
||||
<div className="username">{player.name}</div>
|
||||
<div className="real_name">{player.realName}</div>
|
||||
</div>
|
||||
<div className="flag">{countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} alt={countryName} /> : ''}</div>
|
||||
<div className="grenade_container">
|
||||
{grenades.map(grenade => <React.Fragment key={`${player.steamid}_${grenade.name}_${grenade.ammo_reserve || 1}`}>
|
||||
<Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade />
|
||||
{
|
||||
grenade.ammo_reserve === 2 ? <Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade /> : null}
|
||||
</React.Fragment>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats_row">
|
||||
<div className="health_armor_container">
|
||||
<div className="health-icon icon">
|
||||
<HealthFull />
|
||||
</div>
|
||||
<div className="health text">{player.state.health}</div>
|
||||
<div className="armor-icon icon">
|
||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull />}
|
||||
</div>
|
||||
<div className="health text">{player.state.armor}</div>
|
||||
</div>
|
||||
<div className="statistics">
|
||||
<Statistic label={"K"} value={stats.kills} />
|
||||
<Statistic label={"A"} value={stats.assists} />
|
||||
<Statistic label={"D"} value={stats.deaths} />
|
||||
<Statistic label={"K/D"} value={ratio.toFixed(2)} />
|
||||
</div>
|
||||
<div className="ammo">
|
||||
<div className="ammo_icon_container">
|
||||
<Bullets />
|
||||
</div>
|
||||
<div className="ammo_counter">
|
||||
<div className="ammo_clip">{(currentWeapon && currentWeapon.ammo_clip) || "-"}</div>
|
||||
<div className="ammo_reserve">/{(currentWeapon && currentWeapon.ammo_reserve) || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
135
src/HUD/Players/Player.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import Avatar from "./Avatar";
|
||||
import Armor from "./../Indicators/Armor";
|
||||
import Bomb from "./../Indicators/Bomb";
|
||||
import Defuse from "./../Indicators/Defuse";
|
||||
|
||||
interface IProps {
|
||||
player: I.Player,
|
||||
isObserved: boolean,
|
||||
}
|
||||
|
||||
const compareWeapon = (weaponOne: I.WeaponRaw, weaponTwo: I.WeaponRaw) => {
|
||||
if (weaponOne.name === weaponTwo.name &&
|
||||
weaponOne.paintkit === weaponTwo.paintkit &&
|
||||
weaponOne.type === weaponTwo.type &&
|
||||
weaponOne.ammo_clip === weaponTwo.ammo_clip &&
|
||||
weaponOne.ammo_clip_max === weaponTwo.ammo_clip_max &&
|
||||
weaponOne.ammo_reserve === weaponTwo.ammo_reserve &&
|
||||
weaponOne.state === weaponTwo.state
|
||||
) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const compareWeapons = (weaponsObjectOne: { [key: string]: I.WeaponRaw }, weaponsObjectTwo: { [key: string]: I.WeaponRaw }) => {
|
||||
const weaponsOne = Object.values(weaponsObjectOne).sort((a, b) => (a.name as any) - (b.name as any))
|
||||
const weaponsTwo = Object.values(weaponsObjectTwo).sort((a, b) => (a.name as any) - (b.name as any))
|
||||
|
||||
if (weaponsOne.length !== weaponsTwo.length) return false;
|
||||
|
||||
return weaponsOne.every((weapon, i) => compareWeapon(weapon, weaponsTwo[i]));
|
||||
}
|
||||
|
||||
const arePlayersEqual = (playerOne: I.Player, playerTwo: I.Player) => {
|
||||
if (playerOne.name === playerTwo.name &&
|
||||
playerOne.steamid === playerTwo.steamid &&
|
||||
playerOne.observer_slot === playerTwo.observer_slot &&
|
||||
playerOne.defaultName === playerTwo.defaultName &&
|
||||
playerOne.clan === playerTwo.clan &&
|
||||
playerOne.stats.kills === playerTwo.stats.kills &&
|
||||
playerOne.stats.assists === playerTwo.stats.assists &&
|
||||
playerOne.stats.deaths === playerTwo.stats.deaths &&
|
||||
playerOne.stats.mvps === playerTwo.stats.mvps &&
|
||||
playerOne.stats.score === playerTwo.stats.score &&
|
||||
playerOne.state.health === playerTwo.state.health &&
|
||||
playerOne.state.armor === playerTwo.state.armor &&
|
||||
playerOne.state.helmet === playerTwo.state.helmet &&
|
||||
playerOne.state.defusekit === playerTwo.state.defusekit &&
|
||||
playerOne.state.flashed === playerTwo.state.flashed &&
|
||||
playerOne.state.smoked === playerTwo.state.smoked &&
|
||||
playerOne.state.burning === playerTwo.state.burning &&
|
||||
playerOne.state.money === playerTwo.state.money &&
|
||||
playerOne.state.round_killhs === playerTwo.state.round_killhs &&
|
||||
playerOne.state.round_kills === playerTwo.state.round_kills &&
|
||||
playerOne.state.round_totaldmg === playerTwo.state.round_totaldmg &&
|
||||
playerOne.state.equip_value === playerTwo.state.equip_value &&
|
||||
playerOne.state.adr === playerTwo.state.adr &&
|
||||
playerOne.avatar === playerTwo.avatar &&
|
||||
playerOne.country === playerTwo.country &&
|
||||
playerOne.realName === playerTwo.realName &&
|
||||
compareWeapons(playerOne.weapons, playerTwo.weapons)
|
||||
) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const Player = ({ player, isObserved }: IProps) => {
|
||||
|
||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
||||
const primary = weapons.filter(weapon => !['C4', 'Pistol', 'Knife', 'Grenade', undefined].includes(weapon.type))[0] || null;
|
||||
const secondary = weapons.filter(weapon => weapon.type === "Pistol")[0] || null;
|
||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
||||
const isLeft = player.team.orientation === "left";
|
||||
return (
|
||||
<div className={`player ${player.state.health === 0 ? "dead" : ""} ${isObserved ? 'active' : ''}`}>
|
||||
<div className="player_data">
|
||||
<Avatar steamid={player.steamid} height={57} width={57} showSkull={false} showCam={false} sidePlayer={true} />
|
||||
<div className="dead-stats">
|
||||
<div className="labels">
|
||||
<div className="stat-label">K</div>
|
||||
<div className="stat-label">A</div>
|
||||
<div className="stat-label">D</div>
|
||||
</div>
|
||||
<div className="values">
|
||||
<div className="stat-value">{player.stats.kills}</div>
|
||||
<div className="stat-value">{player.stats.assists}</div>
|
||||
<div className="stat-value">{player.stats.deaths}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player_stats">
|
||||
<div className="row">
|
||||
<div className="health">
|
||||
{player.state.health}
|
||||
</div>
|
||||
<div className="username">
|
||||
<div>{isLeft ? <span>{player.observer_slot}</span> : null} {player.name} {!isLeft ? <span>{player.observer_slot}</span> : null}</div>
|
||||
{primary || secondary ? <Weapon weapon={primary ? primary.name : secondary.name} active={primary ? primary.state === "active" : secondary.state === "active"} /> : ""}
|
||||
{player.state.round_kills ? <div className="roundkills-container">{player.state.round_kills}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`hp_bar ${player.state.health <= 20 ? 'low' : ''}`} style={{ width: `${player.state.health}%` }}></div>
|
||||
<div className="row">
|
||||
<div className="armor_and_utility">
|
||||
<Bomb player={player} />
|
||||
<Armor player={player} />
|
||||
<Defuse player={player} />
|
||||
</div>
|
||||
<div className="money">${player.state.money}</div>
|
||||
<div className="grenades">
|
||||
{grenades.map(grenade => (
|
||||
[
|
||||
<Weapon key={`${grenade.name}-${grenade.state}`} weapon={grenade.name} active={grenade.state === "active"} isGrenade />,
|
||||
grenade.ammo_reserve === 2 ? <Weapon key={`${grenade.name}-${grenade.state}-double`} weapon={grenade.name} active={false} isGrenade /> : null,
|
||||
]
|
||||
))}
|
||||
</div>
|
||||
<div className="secondary_weapon">{primary && secondary ? <Weapon weapon={secondary.name} active={secondary.state === "active"} /> : ""}</div>
|
||||
</div>
|
||||
<div className="active_border"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const arePropsEqual = (prevProps: Readonly<IProps>, nextProps: Readonly<IProps>) => {
|
||||
if (prevProps.isObserved !== nextProps.isObserved) return false;
|
||||
|
||||
return arePlayersEqual(prevProps.player, nextProps.player);
|
||||
}
|
||||
|
||||
//export default React.memo(Player, arePropsEqual);
|
||||
export default Player;
|
||||
25
src/HUD/Players/TeamBox.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import Player from './Player'
|
||||
import * as I from 'csgogsi-socket';
|
||||
import './players.scss';
|
||||
|
||||
interface Props {
|
||||
players: I.Player[],
|
||||
team: I.Team,
|
||||
side: 'right' | 'left',
|
||||
current: I.Player | null,
|
||||
}
|
||||
|
||||
export default class TeamBox extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className={`teambox ${this.props.team.side} ${this.props.side}`}>
|
||||
{this.props.players.map(player => <Player
|
||||
key={player.steamid}
|
||||
player={player}
|
||||
isObserved={!!(this.props.current && this.props.current.steamid === player.steamid)}
|
||||
/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
247
src/HUD/Players/observed.scss
Normal file
@ -0,0 +1,247 @@
|
||||
.observed {
|
||||
position: fixed;
|
||||
width: 620px;
|
||||
bottom: 120px;
|
||||
margin-left: -310px;
|
||||
left: 50%;
|
||||
height: 86px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
|
||||
.main_row, .stats_row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.stats_row {
|
||||
height: 28px;
|
||||
|
||||
.stats_cell {
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
|
||||
|
||||
.label {
|
||||
height: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #b2b2b2;
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.ammo {
|
||||
height: 100%;
|
||||
width: 140px;
|
||||
display: flex;
|
||||
.ammo_counter {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
> div {
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ammo_clip {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ammo_reserve {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.ammo_icon_container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
max-width: 30px;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.health_armor_container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 140px;
|
||||
justify-content: center;
|
||||
.text {
|
||||
width: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 21px;
|
||||
height: 100%;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
max-height:30px;
|
||||
max-width:22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.statistics {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.stat {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main_row {
|
||||
font-weight: 600;
|
||||
.logo {
|
||||
height: 43px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.grenade_container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 140px;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
.username_container {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
.real_name {
|
||||
font-size: 7pt;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
.weapon.grenade {
|
||||
height: 28px;
|
||||
/*filter: invert(1);*/
|
||||
|
||||
&:last-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.sidebar {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
&:first-child {
|
||||
left: -10px;
|
||||
}
|
||||
&:last-child {
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
.flag {
|
||||
height: 43px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
height: 43px;
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
img {
|
||||
border-radius:50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.T {
|
||||
border-bottom: 6px solid var(--color-new-t);
|
||||
.main_row .username_container .username, .stats_row .ammo .ammo_counter .ammo_reserve {
|
||||
color: var(--color-new-t)
|
||||
}
|
||||
.sidebar {
|
||||
background-color: var(--color-new-t)
|
||||
}
|
||||
.stats_row .statistics .stat .label {
|
||||
color: var(--color-new-t);
|
||||
margin-right: 2px;
|
||||
}
|
||||
.stats_row .health_armor_container .icon, .stats_row .ammo .ammo_icon_container {
|
||||
svg {
|
||||
fill: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.CT {
|
||||
border-bottom: 6px solid var(--color-new-ct);
|
||||
.main_row .username_container .username, .stats_row .ammo .ammo_counter .ammo_reserve {
|
||||
color: var(--color-new-ct)
|
||||
}
|
||||
.sidebar {
|
||||
background-color: var(--color-new-ct)
|
||||
}
|
||||
.stats_row .statistics .stat .label {
|
||||
color: var(--color-new-ct);
|
||||
margin-right: 2px;
|
||||
}
|
||||
.stats_row .health_armor_container .icon, .stats_row .ammo .ammo_icon_container {
|
||||
svg {
|
||||
fill: var(--color-new-ct);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#cameraFeed {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transform: scale(0.94);
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
transform-origin: bottom left;
|
||||
iframe {
|
||||
position: relative;
|
||||
border: none;
|
||||
width: 750px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
371
src/HUD/Players/players.scss
Normal file
@ -0,0 +1,371 @@
|
||||
.teambox {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 1;
|
||||
transition: opacity 0.75s;
|
||||
&.CT {
|
||||
.player .hp_bar {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
}
|
||||
&.T {
|
||||
.player .hp_bar {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
&.left {
|
||||
left: 10px;
|
||||
.player {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
.grenades {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.armor_and_utility {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.money {
|
||||
margin-right: auto;
|
||||
}
|
||||
.username .roundkills-container {
|
||||
right: 115px;
|
||||
}
|
||||
.secondary_weapon {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
.dead-stats {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
right: 10px;
|
||||
.player {
|
||||
flex-direction: row-reverse;
|
||||
.dead-stats {
|
||||
left: 8px;
|
||||
}
|
||||
.player_data {
|
||||
flex-direction: row-reverse;
|
||||
.hp_bar {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.row {
|
||||
flex-direction: row-reverse;
|
||||
.grenades {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.username {
|
||||
flex-direction: row-reverse;
|
||||
.roundkills-container {
|
||||
left: 115px;
|
||||
}
|
||||
}
|
||||
.secondary_weapon {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.armor_and_utility {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.money {
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.weapon {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.player {
|
||||
width: 645px;
|
||||
height: 70px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&.active {
|
||||
.player_data {
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
&.dead {
|
||||
opacity: 0.7;
|
||||
.player_side_bar {
|
||||
background-color: var(--main-panel-color) !important;
|
||||
}
|
||||
.player_data {
|
||||
.avatar {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
.dead-stats {
|
||||
display: flex;
|
||||
}
|
||||
.row {
|
||||
.hp_background_2 {
|
||||
opacity: 0;
|
||||
}
|
||||
.health {
|
||||
color: #b2b2b2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.username {
|
||||
color: #b2b2b2;
|
||||
}
|
||||
.armor_and_utility {
|
||||
width: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.money {
|
||||
color: #466722;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:last-child .player_data {
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
.player_side_bar {
|
||||
width: 10px;
|
||||
height: 70px;
|
||||
&.CT {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
&.T {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
.dead-stats {
|
||||
position: absolute;
|
||||
height: 85%;
|
||||
width: 60px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
opacity: 0.75;
|
||||
top: 10%;
|
||||
.labels, .values {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
.stat-label, .stat-value {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.player_data {
|
||||
background-color: var(--sub-panel-color);
|
||||
width: 415px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
.player_stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.hp_bar {
|
||||
height: 2px;
|
||||
&.low {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
.row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
svg.weapon {
|
||||
filter: invert(45%);
|
||||
&.active {
|
||||
filter: invert(0);
|
||||
}
|
||||
}
|
||||
.hp_background, .hp_background_2 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
.hp_background_2 {
|
||||
background-color: var(--color-bomb);
|
||||
transition: width 0.75s 1.5s;
|
||||
}
|
||||
.armor_and_utility {
|
||||
width: 39px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
.armor_indicator, .bomb_indicator, .defuse_indicator {
|
||||
svg {
|
||||
max-height: 20px;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.username {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
max-width: calc(100% - 49px);
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
font-size: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.roundkills-container {
|
||||
position: absolute;
|
||||
background-image: url('./../../assets/images/icon_skull_default.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 10px;
|
||||
background-position: left 2px;
|
||||
padding-left: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
div span {
|
||||
opacity: 0.6;
|
||||
font-size:15px;
|
||||
}
|
||||
svg.weapon {
|
||||
max-height: 30px;
|
||||
width: auto;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
max-width: 100px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
.money {
|
||||
width: 60px;
|
||||
color: var(--color-moneys);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.grenades {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
svg.grenade {
|
||||
max-height: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.health {
|
||||
width: 49px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size:18px;
|
||||
}
|
||||
.secondary_weapon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
max-height: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.videofeed {
|
||||
width:70px;
|
||||
height:70px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
video {
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
img {
|
||||
border-radius:50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.players_alive {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 180px;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
opacity: 1;
|
||||
transition: opacity 1s;
|
||||
.counter_container {
|
||||
display: flex;
|
||||
height: 45px;
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size:30px;
|
||||
color: white;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.team_counter {
|
||||
background-color: rgba(0,0,0,0.75);
|
||||
}
|
||||
.CT {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
.T {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
.title_container {
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
height:20px;
|
||||
font-size:14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0,0,0,0.75);
|
||||
}
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
110
src/HUD/Radar/LexoRadar/LexoRadar.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { Bomb } from 'csgogsi-socket';
|
||||
import maps, { ScaleConfig, MapConfig, ZoomAreas } from './maps';
|
||||
import './index.css';
|
||||
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||
import config from './config';
|
||||
interface IProps {
|
||||
players: RadarPlayerObject[];
|
||||
grenades: RadarGrenadeObject[];
|
||||
bomb?: Bomb | null;
|
||||
mapName: string;
|
||||
zoom?: ZoomAreas;
|
||||
mapConfig: MapConfig,
|
||||
reverseZoom: string,
|
||||
parsePosition: (position: number[], size: number, config: ScaleConfig) => number[]
|
||||
}
|
||||
|
||||
const isShooting = (lastShoot: number) => (new Date()).getTime() - lastShoot <= 250;
|
||||
class App extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
players: [],
|
||||
grenades: [],
|
||||
bomb: null
|
||||
}
|
||||
}
|
||||
|
||||
renderGrenade = (grenade: RadarGrenadeObject) => {
|
||||
if ("flames" in grenade) {
|
||||
return null;
|
||||
}
|
||||
const { reverseZoom } = this.props;
|
||||
return (
|
||||
<div key={grenade.id} className={`grenade ${grenade.type} ${grenade.side || ''} ${grenade.state} ${grenade.visible ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${grenade.position[0].toFixed(2)}px) translateY(${grenade.position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`,
|
||||
}}>
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderDot = (player: RadarPlayerObject) => {
|
||||
const { reverseZoom } = this.props;
|
||||
return (
|
||||
<div key={player.id}
|
||||
className={`player ${player.shooting? 'shooting':''} ${player.flashed ? 'flashed':''} ${player.side} ${player.hasBomb ? 'hasBomb':''} ${player.isActive ? 'active' : ''} ${!player.isAlive ? 'dead' : ''} ${player.visible ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${player.position[0].toFixed(2)}px) translateY(${player.position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`,
|
||||
width: config.playerSize * player.scale,
|
||||
height: config.playerSize * player.scale,
|
||||
}}>
|
||||
<div className="background-fire" style={{ transform: `rotate(${-90 + player.position[2]}deg)`, opacity: isShooting(player.lastShoot) ? 1 : 0 }} ><div className="bg"/></div>
|
||||
<div className="background" style={{ transform: `rotate(${45 + player.position[2]}deg)` }}></div>
|
||||
<div className="label">{player.label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderBomb = () => {
|
||||
const { bomb, mapConfig, reverseZoom } = this.props;
|
||||
if(!bomb) return null;
|
||||
if(bomb.state === "carried" || bomb.state === "planting") return null;
|
||||
if("config" in mapConfig){
|
||||
const position = this.props.parsePosition(bomb.position, 30, mapConfig.config);
|
||||
if(!position) return null;
|
||||
|
||||
return (
|
||||
<div className={`bomb ${bomb.state} visible`}
|
||||
style={{
|
||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||
}}>
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return mapConfig.configs.map(config => {
|
||||
const position = this.props.parsePosition(bomb.position, 30, config.config);
|
||||
if(!position) return null;
|
||||
return (
|
||||
<div className={`bomb ${bomb.state} ${config.isVisible(bomb.position[2]) ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px)`
|
||||
}}>
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const { players, grenades, zoom } = this.props;
|
||||
|
||||
const style: React.CSSProperties = { backgroundImage: `url(${maps[this.props.mapName].file})` }
|
||||
|
||||
if(zoom){
|
||||
style.transform = `scale(${zoom.zoom})`;
|
||||
style.transformOrigin = `${zoom.origin[0]}px ${zoom.origin[1]}px`;
|
||||
}
|
||||
//if(players.length === 0) return null;
|
||||
return <div className="map" style={style}>
|
||||
{players.map(this.renderDot)}
|
||||
{grenades.map(this.renderGrenade)}
|
||||
{this.renderBomb()}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
322
src/HUD/Radar/LexoRadar/LexoRadarContainer.tsx
Normal file
@ -0,0 +1,322 @@
|
||||
import React from 'react';
|
||||
import { Player, Bomb } from 'csgogsi-socket';
|
||||
import maps, { ScaleConfig } from './maps';
|
||||
import LexoRadar from './LexoRadar';
|
||||
import { ExtendedGrenade, Grenade, RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||
import config from './config';
|
||||
|
||||
const DESCALE_ON_ZOOM = true;
|
||||
|
||||
|
||||
let playersStates: Player[][] = [];
|
||||
let grenadesStates: ExtendedGrenade[][] = [];
|
||||
const directions: Record<string, number> = {};
|
||||
type ShootingState = {
|
||||
ammo: number,
|
||||
weapon: string,
|
||||
lastShoot: number
|
||||
}
|
||||
let shootingState: Record<string, ShootingState> = {};
|
||||
|
||||
const calculateDirection = (player: Player) => {
|
||||
if (directions[player.steamid] && !player.state.health) return directions[player.steamid];
|
||||
|
||||
const [forwardV1, forwardV2] = player.forward;
|
||||
let direction = 0;
|
||||
|
||||
const [axisA, axisB] = [Math.asin(forwardV1), Math.acos(forwardV2)].map(axis => axis * 180 / Math.PI);
|
||||
|
||||
if (axisB < 45) {
|
||||
direction = Math.abs(axisA);
|
||||
} else if (axisB > 135) {
|
||||
direction = 180 - Math.abs(axisA);
|
||||
} else {
|
||||
direction = axisB;
|
||||
}
|
||||
|
||||
if (axisA < 0) {
|
||||
direction = -(direction -= 360);
|
||||
}
|
||||
|
||||
if (!directions[player.steamid]) {
|
||||
directions[player.steamid] = direction;
|
||||
}
|
||||
|
||||
const previous = directions[player.steamid];
|
||||
|
||||
let modifier = previous;
|
||||
modifier -= 360 * Math.floor(previous / 360);
|
||||
modifier = -(modifier -= direction);
|
||||
|
||||
if (Math.abs(modifier) > 180) {
|
||||
modifier -= 360 * Math.abs(modifier) / modifier;
|
||||
}
|
||||
directions[player.steamid] += modifier;
|
||||
|
||||
return directions[player.steamid];
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
players: Player[],
|
||||
bomb?: Bomb | null,
|
||||
player: Player | null,
|
||||
grenades?: any
|
||||
size?: number,
|
||||
mapName: string
|
||||
}
|
||||
|
||||
class App extends React.Component<IProps> {
|
||||
round = (n: number) => {
|
||||
const r = 0.02;
|
||||
return Math.round(n / r) * r;
|
||||
}
|
||||
|
||||
parsePosition = (position: number[], size: number, config: ScaleConfig) => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return [0, 0];
|
||||
}
|
||||
const left = config.origin.x + (position[0] * config.pxPerUX) - (size / 2);
|
||||
const top = config.origin.y + (position[1] * config.pxPerUY) - (size / 2);
|
||||
|
||||
return [this.round(left), this.round(top)];
|
||||
}
|
||||
|
||||
parseGrenadePosition = (grenade: ExtendedGrenade, config: ScaleConfig) => {
|
||||
if (!("position" in grenade)) {
|
||||
return null;
|
||||
}
|
||||
let size = 30;
|
||||
if (grenade.type === "smoke") {
|
||||
size = 60;
|
||||
}
|
||||
return this.parsePosition(grenade.position.split(", ").map(pos => Number(pos)), size, config);
|
||||
}
|
||||
getGrenadePosition = (grenade: ExtendedGrenade, config: ScaleConfig) => {
|
||||
const grenadeData = grenadesStates.slice(0, 5).map(grenades => grenades.filter(gr => gr.id === grenade.id)[0]).filter(pl => !!pl);
|
||||
if (grenadeData.length === 0) return null;
|
||||
const positions = grenadeData.map(grenadeEntry => this.parseGrenadePosition(grenadeEntry, config)).filter(posData => posData !== null) as number[][];
|
||||
if (positions.length === 0) return null;
|
||||
const entryAmount = positions.length;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (const position of positions) {
|
||||
x += position[0];
|
||||
y += position[1];
|
||||
}
|
||||
|
||||
return [x / entryAmount, y / entryAmount];
|
||||
}
|
||||
getPosition = (player: Player, mapConfig: ScaleConfig, scale: number) => {
|
||||
const playerData = playersStates.slice(0, 5).map(players => players.filter(pl => pl.steamid === player.steamid)[0]).filter(pl => !!pl);
|
||||
if (playerData.length === 0) return [0, 0];
|
||||
const positions = playerData.map(playerEntry => this.parsePosition(playerEntry.position, config.playerSize * scale, mapConfig));
|
||||
const entryAmount = positions.length;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (const position of positions) {
|
||||
x += position[0];
|
||||
y += position[1];
|
||||
}
|
||||
|
||||
const degree = calculateDirection(player);
|
||||
return [x / entryAmount, y / entryAmount, degree];
|
||||
}
|
||||
mapPlayer = (active: Player | null) => (player: Player): RadarPlayerObject | RadarPlayerObject[] | null => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const weapons = player.weapons ? Object.values(player.weapons) : [];
|
||||
const weapon = weapons.find(weapon => weapon.state === "active" && weapon.type !== "C4" && weapon.type !== "Knife" && weapon.type !== "Grenade");
|
||||
|
||||
const shooting: ShootingState = { ammo: weapon && weapon.ammo_clip || 0, weapon: weapon && weapon.name || '', lastShoot: 0 };
|
||||
|
||||
const lastShoot = shootingState[player.steamid] || shooting;
|
||||
|
||||
let isShooting = false;
|
||||
|
||||
if (shooting.weapon === lastShoot.weapon && shooting.ammo < lastShoot.ammo) {
|
||||
isShooting = true;
|
||||
}
|
||||
|
||||
shooting.lastShoot = isShooting ? (new Date()).getTime() : lastShoot.lastShoot;
|
||||
|
||||
shootingState[player.steamid] = shooting;
|
||||
|
||||
|
||||
const map = maps[this.props.mapName];
|
||||
const playerObject: RadarPlayerObject = {
|
||||
id: player.steamid,
|
||||
label: player.observer_slot !== undefined ? player.observer_slot : "",
|
||||
side: player.team.side,
|
||||
position: [],
|
||||
visible: true,
|
||||
isActive: !!active && active.steamid === player.steamid,
|
||||
forward: 0,
|
||||
steamid: player.steamid,
|
||||
isAlive: player.state.health > 0,
|
||||
hasBomb: !!Object.values(player.weapons).find(weapon => weapon.type === "C4"),
|
||||
flashed: player.state.flashed > 35,
|
||||
shooting: isShooting,
|
||||
lastShoot: shooting.lastShoot,
|
||||
scale: 1,
|
||||
player
|
||||
}
|
||||
if ("config" in map) {
|
||||
const scale = map.config.originHeight === undefined ? 1 : (1 + (player.position[2] - map.config.originHeight) / 1000);
|
||||
|
||||
playerObject.scale = scale;
|
||||
|
||||
const position = this.getPosition(player, map.config, scale);
|
||||
playerObject.position = position;
|
||||
|
||||
return playerObject;
|
||||
}
|
||||
return map.configs.map(config => {
|
||||
const scale = config.config.originHeight === undefined ? 1 : (1 + (player.position[2] - config.config.originHeight) / 750);
|
||||
|
||||
playerObject.scale = scale;
|
||||
|
||||
return ({
|
||||
...playerObject,
|
||||
position: this.getPosition(player, config.config, scale),
|
||||
id: `${player.steamid}_${config.id}`,
|
||||
visible: config.isVisible(player.position[2])
|
||||
})
|
||||
});
|
||||
}
|
||||
mapGrenade = (extGrenade: ExtendedGrenade) => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return null;
|
||||
}
|
||||
const map = maps[this.props.mapName];
|
||||
if (extGrenade.type === "inferno") {
|
||||
const mapFlame = (id: string) => {
|
||||
if ("config" in map) {
|
||||
return ({
|
||||
position: this.parsePosition(extGrenade.flames[id].split(", ").map(pos => Number(pos)), 12, map.config),
|
||||
id: `${id}_${extGrenade.id}`,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
return map.configs.map(config => ({
|
||||
id: `${id}_${extGrenade.id}_${config.id}`,
|
||||
visible: config.isVisible(extGrenade.flames[id].split(", ").map(Number)[2]),
|
||||
position: this.parsePosition(extGrenade.flames[id].split(", ").map(pos => Number(pos)), 12, config.config)
|
||||
}));
|
||||
}
|
||||
const flames = Object.keys(extGrenade.flames).map(mapFlame).flat();
|
||||
const flameObjects: RadarGrenadeObject[] = flames.map(flame => ({
|
||||
...flame,
|
||||
side: extGrenade.side,
|
||||
type: 'inferno',
|
||||
state: 'landed'
|
||||
}));
|
||||
return flameObjects;
|
||||
}
|
||||
|
||||
if ("config" in map) {
|
||||
const position = this.getGrenadePosition(extGrenade, map.config);
|
||||
if (!position) return null;
|
||||
const grenadeObject: RadarGrenadeObject = {
|
||||
type: extGrenade.type,
|
||||
state: 'inair',
|
||||
side: extGrenade.side,
|
||||
position,
|
||||
id: extGrenade.id,
|
||||
visible: true
|
||||
}
|
||||
if (extGrenade.type === "smoke") {
|
||||
if (extGrenade.effecttime !== "0.0") {
|
||||
grenadeObject.state = "landed";
|
||||
if (Number(extGrenade.effecttime) >= 16.5) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
} else if (extGrenade.type === 'flashbang' || extGrenade.type === 'frag') {
|
||||
if (Number(extGrenade.lifetime) >= 1.25) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
return grenadeObject;
|
||||
}
|
||||
return map.configs.map(config => {
|
||||
const position = this.getGrenadePosition(extGrenade, config.config);
|
||||
if (!position) return null;
|
||||
const grenadeObject: RadarGrenadeObject = {
|
||||
type: extGrenade.type,
|
||||
state: 'inair',
|
||||
side: extGrenade.side,
|
||||
position,
|
||||
id: `${extGrenade.id}_${config.id}`,
|
||||
visible: config.isVisible(extGrenade.position.split(", ").map(Number)[2])
|
||||
}
|
||||
if (extGrenade.type === "smoke") {
|
||||
if (extGrenade.effecttime !== "0.0") {
|
||||
grenadeObject.state = "landed";
|
||||
if (Number(extGrenade.effecttime) >= 16.5) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
} else if (extGrenade.type === 'flashbang' || extGrenade.type === 'frag') {
|
||||
if (Number(extGrenade.lifetime) >= 1.25) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
return grenadeObject;
|
||||
}).filter((grenade): grenade is RadarGrenadeObject => grenade !== null);
|
||||
|
||||
}
|
||||
getSideOfGrenade = (grenade: Grenade) => {
|
||||
const owner = this.props.players.find(player => player.steamid === grenade.owner);
|
||||
if (!owner) return null;
|
||||
return owner.team.side;
|
||||
}
|
||||
render() {
|
||||
const players: RadarPlayerObject[] = this.props.players.map(this.mapPlayer(this.props.player)).filter((player): player is RadarPlayerObject => player !== null).flat();
|
||||
playersStates.unshift(this.props.players);
|
||||
if (playersStates.length > 5) {
|
||||
playersStates = playersStates.slice(0, 5);
|
||||
}
|
||||
let grenades: RadarGrenadeObject[] = [];
|
||||
const currentGrenades = Object.keys(this.props.grenades as { [key: string]: Grenade }).map(grenadeId => ({ ...this.props.grenades[grenadeId], id: grenadeId, side: this.getSideOfGrenade(this.props.grenades[grenadeId]) })) as ExtendedGrenade[];
|
||||
if (currentGrenades) {
|
||||
grenades = currentGrenades.map(this.mapGrenade).filter(entry => entry !== null).flat() as RadarGrenadeObject[];
|
||||
grenadesStates.unshift(currentGrenades);
|
||||
}
|
||||
if (grenadesStates.length > 5) {
|
||||
grenadesStates = grenadesStates.slice(0, 5);
|
||||
}
|
||||
const size = this.props.size || 300;
|
||||
const offset = (size - (size * size / 1024)) / 2;
|
||||
|
||||
const config = maps[this.props.mapName];
|
||||
|
||||
const zooms = config && config.zooms || [];
|
||||
|
||||
const activeZoom = zooms.find(zoom => zoom.threshold(players.map(pl => pl.player)));
|
||||
|
||||
const reverseZoom = 1/(activeZoom && activeZoom.zoom || 1);
|
||||
|
||||
// s*(1024-s)/2048
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||
Unsupported map
|
||||
</div>;
|
||||
}
|
||||
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||
<LexoRadar
|
||||
players={players}
|
||||
grenades={grenades}
|
||||
parsePosition={this.parsePosition}
|
||||
bomb={this.props.bomb}
|
||||
mapName={this.props.mapName}
|
||||
mapConfig={maps[this.props.mapName]}
|
||||
zoom={activeZoom}
|
||||
reverseZoom={DESCALE_ON_ZOOM ? reverseZoom.toFixed(2) : '1'}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
BIN
src/HUD/Radar/LexoRadar/assets/playerBg.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/HUD/Radar/LexoRadar/assets/shootFire.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/HUD/Radar/LexoRadar/assets/shootFire.zip
Normal file
5
src/HUD/Radar/LexoRadar/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
playerSize: 60,
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/grenades/bomb.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/HUD/Radar/LexoRadar/grenades/firebomb.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/HUD/Radar/LexoRadar/grenades/flash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/HUD/Radar/LexoRadar/grenades/frag.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
5
src/HUD/Radar/LexoRadar/grenades/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Firebomb from './firebomb.png'
|
||||
import Flash from './flash.png'
|
||||
import Smoke from './smoke.png'
|
||||
|
||||
export { Firebomb, Flash, Smoke };
|
||||
BIN
src/HUD/Radar/LexoRadar/grenades/smoke.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/HUD/Radar/LexoRadar/grenades/weapon_decoy.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/HUD/Radar/LexoRadar/grenades/weapon_incgrenade.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
285
src/HUD/Radar/LexoRadar/index.css
Normal file
@ -0,0 +1,285 @@
|
||||
html, body, .map-container {
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes FlashOrFragDeployed {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 50px rgba(0, 0, 0, 0.8);
|
||||
opacity:0;
|
||||
}
|
||||
}
|
||||
@keyframes BombPlanted {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(185, 5, 5, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 50px rgba(185, 5, 5, 0);
|
||||
}
|
||||
}
|
||||
@keyframes BombExploded {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(185, 5, 5, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 150px rgba(185, 5, 5, 0);
|
||||
}
|
||||
}
|
||||
@keyframes BombDefused {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(5, 185, 5, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 150px rgba(5, 185, 5, 0);
|
||||
}
|
||||
}
|
||||
.map-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-container .map {
|
||||
width:1024px;
|
||||
height: 1024px;
|
||||
position: relative;
|
||||
transform: scale(1);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.map .player, .map .grenade, .map .bomb {
|
||||
position: absolute;
|
||||
height:30px;
|
||||
width:30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.5s ease;
|
||||
/*transition: all 0.1s ease;/**/
|
||||
}
|
||||
.map .player .background {
|
||||
/*background-color:white;*/
|
||||
/*clip-path: polygon(0 0, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0 0);*/
|
||||
background-image: url('./assets/playerBg.png');
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
transition:transform 0.2s ease;
|
||||
}
|
||||
.map .player .background-fire {
|
||||
position: absolute;
|
||||
width:200%;
|
||||
height:200%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:transform 0.2s ease;
|
||||
/*background-color:white;*/
|
||||
/*clip-path: polygon(0 0, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0 0);*/
|
||||
|
||||
}
|
||||
@keyframes Blink {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.map .player .background-fire .bg {
|
||||
width:100%;
|
||||
height:100%;
|
||||
background-image: url('./assets/shootFire.png');
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
animation: Blink;
|
||||
animation-duration: 0.25s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.map .player.dead {
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
}
|
||||
.map .player {
|
||||
transition:transform 0.1s ease, opacity 1s;
|
||||
image-rendering: -moz-crisp-edges; /* Firefox */
|
||||
image-rendering: -o-crisp-edges; /* Opera */
|
||||
image-rendering: -webkit-optimize-contrast; /* Webkit (non-standard naming) */
|
||||
image-rendering: crisp-edges;
|
||||
-ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */
|
||||
}
|
||||
.map .player:not(.dead) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.map .player.flashed.CT:not(.dead) .label {
|
||||
background: rgb(159 197 255);
|
||||
}
|
||||
.map .player.flashed.T:not(.dead) .label {
|
||||
background: rgb(255 219 165);
|
||||
}
|
||||
.map .player.active .background {
|
||||
width:120%;
|
||||
height:120%;
|
||||
}
|
||||
|
||||
/*
|
||||
.map .player.shooting .background {
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.map .player.active.shooting .background {
|
||||
width: 132%;
|
||||
height: 132%;
|
||||
}
|
||||
*/
|
||||
.map .player.active {
|
||||
width:120%;
|
||||
height:120%;
|
||||
z-index: 3;
|
||||
}
|
||||
.map .grenade .background {
|
||||
border-radius:50%;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity:1;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
.map .grenade.smoke .background {
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
opacity: 1;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
.map .grenade.smoke {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.map .grenade.smoke.inair .background {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
background-image: url('./grenades/smoke.png');
|
||||
filter: invert(1);
|
||||
}
|
||||
.map .grenade.smoke.exploded .background {
|
||||
opacity: 0;
|
||||
}
|
||||
.map .grenade.flashbang, .map .grenade.frag {
|
||||
filter: invert(1);
|
||||
}
|
||||
.map .grenade.flashbang .background {
|
||||
background-image: url('./grenades/flash.png');
|
||||
background-color: transparent;
|
||||
}
|
||||
.map .grenade.frag .background {
|
||||
background-image: url('./grenades/frag.png');
|
||||
background-color: transparent;
|
||||
}
|
||||
.map .grenade .explode-point, .map .bomb .explode-point {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 0.08px;
|
||||
}
|
||||
.map .grenade.flashbang.exploded .explode-point, .map .grenade.frag.exploded .explode-point {
|
||||
animation: FlashOrFragDeployed 0.25s 1 forwards;
|
||||
}
|
||||
|
||||
.map .grenade.flashbang.exploded .background, .map .grenade.frag.exploded .background {
|
||||
opacity: 0;
|
||||
}
|
||||
.map .grenade.smoke .background {
|
||||
border: 5px solid grey;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.map .grenade.smoke.CT .background {
|
||||
border: 5px solid var(--color-new-ct);
|
||||
background-color: rgba(255, 255, 255,0.5);
|
||||
}
|
||||
.map .grenade.smoke.T .background {
|
||||
border: 5px solid var(--color-new-t);
|
||||
background-color: rgba(255, 255, 255,0.5);
|
||||
}
|
||||
.map .grenade.firebomb .background {
|
||||
background-color:transparent;
|
||||
background-image: url('./grenades/firebomb.png');
|
||||
filter: invert(1);
|
||||
}
|
||||
.map .grenade.inferno {
|
||||
width:12px;
|
||||
height:12px;
|
||||
}
|
||||
.map .grenade.smoke {
|
||||
width:60px;
|
||||
height:60px;
|
||||
}
|
||||
.map .grenade.inferno .background {
|
||||
background-color: red;
|
||||
opacity: 0.5;
|
||||
border: 2px solid orange;
|
||||
}
|
||||
.map .player .background, .map .player .label,.map .grenade .background, .map .bomb .background {
|
||||
position: absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.map .player .label {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.5s;
|
||||
-webkit-font-smoothing: crisp-edges;
|
||||
font-size: 37px;
|
||||
text-shadow: 4px 4px 0 black;
|
||||
}
|
||||
.map .bomb {
|
||||
transition:transform 0.1s ease;
|
||||
}
|
||||
.map .bomb .background {
|
||||
background-image: url('./grenades/bomb.png');
|
||||
background-size: 66%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius:50%;
|
||||
}
|
||||
.map .bomb.planted .explode-point, .map .bomb.defusing .explode-point {
|
||||
animation: BombPlanted 2s infinite;
|
||||
}
|
||||
.map .bomb.exploded .explode-point {
|
||||
animation: BombExploded 2s 1 forwards;
|
||||
}
|
||||
.map .bomb.defused .explode-point {
|
||||
animation: BombDefused 2s 1 forwards;
|
||||
}
|
||||
.map .player.CT .label {
|
||||
background: rgb(16, 88, 197)
|
||||
}
|
||||
.map .player.T .label {
|
||||
background: rgb(255, 153, 0);
|
||||
}
|
||||
.map .player.T.hasBomb .label {
|
||||
background: red;
|
||||
}
|
||||
|
||||
@keyframes Hidden {
|
||||
from {
|
||||
}
|
||||
to {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.map .hidden {
|
||||
opacity: 0;
|
||||
animation: Hidden 1s ease 1s 1;
|
||||
animation-fill-mode: forwards;/**/
|
||||
|
||||
}
|
||||
55
src/HUD/Radar/LexoRadar/interface.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Player, Side } from "csgogsi";
|
||||
|
||||
export interface RadarPlayerObject {
|
||||
id: string,
|
||||
label: string | number,
|
||||
visible: boolean,
|
||||
side: Side,
|
||||
position: number[],
|
||||
forward: number,
|
||||
isActive: boolean,
|
||||
isAlive: boolean,
|
||||
steamid: string,
|
||||
hasBomb: boolean,
|
||||
flashed: boolean,
|
||||
shooting: boolean,
|
||||
lastShoot: number,
|
||||
scale: number,
|
||||
player: Player
|
||||
}
|
||||
|
||||
export interface RadarGrenadeObject {
|
||||
state: 'inair' | 'landed' | 'exploded'
|
||||
side: Side | null,
|
||||
type: 'decoy' | 'smoke' | 'frag' | 'firebomb' | 'flashbang' | 'inferno',
|
||||
position: number[],
|
||||
visible: boolean,
|
||||
id: string,
|
||||
}
|
||||
export interface GrenadeBase {
|
||||
owner: string,
|
||||
type: 'decoy' | 'smoke' | 'frag' | 'firebomb' | 'flashbang' | 'inferno'
|
||||
lifetime: string
|
||||
}
|
||||
|
||||
export interface DecoySmokeGrenade extends GrenadeBase {
|
||||
position: string,
|
||||
velocity: string,
|
||||
type: 'decoy' | 'smoke',
|
||||
effecttime: string,
|
||||
}
|
||||
|
||||
export interface DefaultGrenade extends GrenadeBase {
|
||||
position: string,
|
||||
type: 'frag' | 'firebomb' | 'flashbang',
|
||||
velocity: string,
|
||||
}
|
||||
|
||||
export interface InfernoGrenade extends GrenadeBase {
|
||||
type: 'inferno',
|
||||
flames: { [key: string]: string }
|
||||
}
|
||||
|
||||
export type Grenade = DecoySmokeGrenade | DefaultGrenade | InfernoGrenade;
|
||||
|
||||
export type ExtendedGrenade = Grenade & { id: string, side: Side | null, };
|
||||
15
src/HUD/Radar/LexoRadar/maps/de_ancient/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 583.2590342775677,
|
||||
"y": 428.92222042149115
|
||||
},
|
||||
"pxPerUX": 0.1983512056034216,
|
||||
"pxPerUY": -0.20108163914549304
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_ancient/radar.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_anubis/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 536.3392873296655,
|
||||
"y": 638.0789844851904
|
||||
},
|
||||
"pxPerUX": 0.1907910426894958,
|
||||
"pxPerUY": -0.18993888105312648
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_anubis/radar.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_cache/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 361.7243823603619,
|
||||
"y": 579.553558767951
|
||||
},
|
||||
"pxPerUX": 0.1830927328891829,
|
||||
"pxPerUY": -0.17650705879909936
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_cache/radar.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_dust2/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 563.1339320329055,
|
||||
"y": 736.9535330430065
|
||||
},
|
||||
"pxPerUX": 0.2278315639654376,
|
||||
"pxPerUY": -0.22776482548619972
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_dust2/radar.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_inferno/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 426.51386123945593,
|
||||
"y": 790.7266981544722
|
||||
},
|
||||
"pxPerUX": 0.2041685571162696,
|
||||
"pxPerUY": -0.20465735943851654
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_inferno/radar.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
16
src/HUD/Radar/LexoRadar/maps/de_mirage/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 645.7196725473384,
|
||||
"y": 340.2921393569175
|
||||
},
|
||||
"pxPerUX": 0.20118507589946494,
|
||||
"pxPerUY": -0.20138282875746794,
|
||||
"originHeight": -170,
|
||||
},
|
||||
"file": radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_mirage/radar.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
37
src/HUD/Radar/LexoRadar/maps/de_nuke/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const high = {
|
||||
"origin": {
|
||||
"x": 473.1284773048749,
|
||||
"y": 165.7329003801045
|
||||
},
|
||||
"pxPerUX": 0.14376095926926907,
|
||||
"pxPerUY": -0.14736670935219626
|
||||
};
|
||||
|
||||
const low = {
|
||||
"origin": {
|
||||
"x": 473.66746071612374,
|
||||
"y": 638.302101754172
|
||||
},
|
||||
"pxPerUX": 0.1436068746398272,
|
||||
"pxPerUY": -0.14533406508526941
|
||||
};
|
||||
|
||||
const config = {
|
||||
configs: [
|
||||
{
|
||||
id: 'high',
|
||||
config: high,
|
||||
isVisible: (height: number) => height >= -450,
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
config: low,
|
||||
isVisible: (height: number) => height < -450,
|
||||
},
|
||||
],
|
||||
file: radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_nuke/radar.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_overpass/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 927.3988878244819,
|
||||
"y": 343.8221009185496
|
||||
},
|
||||
"pxPerUX": 0.1923720959212443,
|
||||
"pxPerUY": -0.19427507725530338
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_overpass/radar.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
15
src/HUD/Radar/LexoRadar/maps/de_train/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 527.365542903922,
|
||||
"y": 511.81469648562296
|
||||
},
|
||||
"pxPerUX": 0.21532584158170223,
|
||||
"pxPerUY": -0.21299254526091588
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_train/radar.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
67
src/HUD/Radar/LexoRadar/maps/de_vertigo/index.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import radar from './radar.png'
|
||||
|
||||
const high = {
|
||||
"origin": {
|
||||
"x": 784.4793452283254,
|
||||
"y": 255.42597837029027
|
||||
},
|
||||
"pxPerUX": 0.19856123172015677,
|
||||
"pxPerUY": -0.19820052722907044
|
||||
};
|
||||
|
||||
const low = {
|
||||
"origin": {
|
||||
"x": 780.5145858437052,
|
||||
"y": 695.4259783702903
|
||||
},
|
||||
"pxPerUX": 0.1989615567841087,
|
||||
"pxPerUY": -0.19820052722907044
|
||||
};
|
||||
|
||||
const config = {
|
||||
configs: [
|
||||
{
|
||||
id: 'high',
|
||||
config: high,
|
||||
isVisible: (height: number) => height >= 11700,
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
config: low,
|
||||
isVisible: (height: number) => height < 11700,
|
||||
},
|
||||
],
|
||||
zooms: [{
|
||||
threshold: (players: Player[]) => {
|
||||
const alivePlayers = players.filter(player => player.state.health);
|
||||
return alivePlayers.length > 0 && alivePlayers.every(player => player.position[2] < 11700)
|
||||
},
|
||||
origin: [472, 1130],
|
||||
zoom: 2
|
||||
}, {
|
||||
threshold: (players: Player[]) => {
|
||||
const alivePlayers = players.filter(player => player.state.health);
|
||||
return alivePlayers.length > 0 && players.filter(player => player.state.health).every(player => player.position[2] >= 11700);
|
||||
},
|
||||
origin: [528, 15],
|
||||
zoom: 1.75
|
||||
}],
|
||||
file: radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
/*
|
||||
import radar from './radar.png'
|
||||
|
||||
export default {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 971.5536135341899,
|
||||
"y": 424.5618319055844
|
||||
},
|
||||
"pxPerUX": 0.34708183044632246,
|
||||
"pxPerUY": -0.3450882697407333
|
||||
},
|
||||
"file":radar
|
||||
}*/
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_vertigo/radar.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
71
src/HUD/Radar/LexoRadar/maps/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import de_mirage from './de_mirage';
|
||||
import de_cache from './de_cache';
|
||||
import de_dust2 from './de_dust2';
|
||||
import de_inferno from './de_inferno';
|
||||
import de_train from './de_train';
|
||||
import de_overpass from './de_overpass';
|
||||
import de_nuke from './de_nuke';
|
||||
import de_vertigo from './de_vertigo';
|
||||
import de_anubis from './de_anubis';
|
||||
import de_ancient from './de_ancient';
|
||||
import api from '../../../../api/api';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
|
||||
export type ZoomAreas = {
|
||||
threshold: (players: Player[]) => boolean;
|
||||
origin: number[],
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export interface ScaleConfig {
|
||||
origin: {
|
||||
x:number,
|
||||
y:number
|
||||
},
|
||||
pxPerUX: number,
|
||||
pxPerUY: number,
|
||||
originHeight?: number
|
||||
}
|
||||
|
||||
interface SingleLayer {
|
||||
config: ScaleConfig,
|
||||
file: string,
|
||||
zooms?: ZoomAreas[]
|
||||
}
|
||||
|
||||
interface DoubleLayer {
|
||||
configs: {
|
||||
id: string,
|
||||
config: ScaleConfig,
|
||||
isVisible: (height: number) => boolean
|
||||
}[],
|
||||
file: string,
|
||||
zooms?: ZoomAreas[]
|
||||
}
|
||||
|
||||
export type MapConfig = SingleLayer | DoubleLayer;
|
||||
|
||||
const maps: { [key: string] : MapConfig} = {
|
||||
de_mirage,
|
||||
de_cache,
|
||||
de_inferno,
|
||||
de_dust2,
|
||||
de_train,
|
||||
de_overpass,
|
||||
de_nuke,
|
||||
de_vertigo,
|
||||
de_ancient,
|
||||
de_anubis
|
||||
}
|
||||
|
||||
api.maps.get().then(fallbackMaps => {
|
||||
const mapNames = Object.keys(fallbackMaps);
|
||||
for(const mapName of mapNames){
|
||||
if(mapName in maps){
|
||||
continue;
|
||||
}
|
||||
maps[mapName] = fallbackMaps[mapName];
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
export default maps;
|
||||
50
src/HUD/Radar/Radar.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { isDev } from './../../api/api';
|
||||
import { CSGO } from "csgogsi-socket";
|
||||
import LexoRadarContainer from './LexoRadar/LexoRadarContainer';
|
||||
|
||||
|
||||
|
||||
interface Props { radarSize: number, game: CSGO }
|
||||
interface State {
|
||||
showRadar: boolean,
|
||||
loaded: boolean,
|
||||
boltobserv:{
|
||||
css: boolean,
|
||||
maps: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default class Radar extends React.Component<Props, State> {
|
||||
state = {
|
||||
showRadar: true,
|
||||
loaded: !isDev,
|
||||
boltobserv: {
|
||||
css: true,
|
||||
maps: true
|
||||
}
|
||||
}
|
||||
async componentDidMount(){
|
||||
/*if(isDev){
|
||||
const response = await fetch('hud.json');
|
||||
const hud = await response.json();
|
||||
const boltobserv = {
|
||||
css: Boolean(hud && hud.boltobserv && hud.boltobserv.css),
|
||||
maps: Boolean(hud && hud.boltobserv && hud.boltobserv.maps)
|
||||
}
|
||||
this.setState({boltobserv, loaded: true});
|
||||
}*/
|
||||
}
|
||||
|
||||
render() {
|
||||
const { players, player, bomb, grenades, map } = this.props.game;
|
||||
return <LexoRadarContainer
|
||||
players={players}
|
||||
player={player}
|
||||
bomb={bomb}
|
||||
grenades={grenades}
|
||||
size={this.props.radarSize}
|
||||
mapName={map.name.substring(map.name.lastIndexOf('/')+1)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
70
src/HUD/Radar/RadarMaps.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import "./radar.scss";
|
||||
import { Match, Veto } from "../../api/interfaces";
|
||||
import { Map, CSGO, Team } from 'csgogsi-socket';
|
||||
import { actions } from './../../App';
|
||||
import Radar from './Radar'
|
||||
import TeamLogo from "../MatchBar/TeamLogo";
|
||||
|
||||
interface Props { match: Match | null, map: Map, game: CSGO }
|
||||
interface State { showRadar: boolean, radarSize: number, showBig: boolean }
|
||||
|
||||
export default class RadarMaps extends React.Component<Props, State> {
|
||||
state = {
|
||||
showRadar: true,
|
||||
radarSize: 350,
|
||||
showBig: false
|
||||
}
|
||||
componentDidMount() {
|
||||
actions.on('radarBigger', () => this.radarChangeSize(20));
|
||||
actions.on('radarSmaller', () => this.radarChangeSize(-20));
|
||||
actions.on('toggleRadar', () => { this.setState(state => ({ showRadar: !state.showRadar })) });
|
||||
|
||||
actions.on("toggleRadarView", () => {
|
||||
this.setState({showBig:!this.state.showBig});
|
||||
});
|
||||
}
|
||||
radarChangeSize = (delta: number) => {
|
||||
const newSize = this.state.radarSize + delta;
|
||||
this.setState({ radarSize: newSize > 0 ? newSize : this.state.radarSize });
|
||||
}
|
||||
render() {
|
||||
const { match } = this.props;
|
||||
const { radarSize, showBig, showRadar } = this.state;
|
||||
const size = showBig ? 600 : radarSize;
|
||||
return (
|
||||
<div id={`radar_maps_container`} className={`${!showRadar ? 'hide' : ''} ${showBig ? 'preview':''}`}>
|
||||
<div className="radar-component-container" style={{width: `${size}px`, height: `${size}px`}}><Radar radarSize={size} game={this.props.game} /></div>
|
||||
{match ? <MapsBar match={this.props.match} map={this.props.map} game={this.props.game} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MapsBar extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { match, map } = this.props;
|
||||
if (!match || !match.vetos.length) return '';
|
||||
const picks = match.vetos.filter(veto => veto.type !== "ban" && veto.mapName);
|
||||
if (picks.length > 3) {
|
||||
const current = picks.find(veto => map.name.includes(veto.mapName));
|
||||
if (!current) return null;
|
||||
return <div id="maps_container">
|
||||
{<MapEntry veto={current} map={map} team={current.type === "decider" ? null : map.team_ct.id === current.teamId ? map.team_ct : map.team_t} />}
|
||||
</div>
|
||||
}
|
||||
return <div id="maps_container">
|
||||
{match.vetos.filter(veto => veto.type !== "ban").filter(veto => veto.teamId || veto.type === "decider").map(veto => <MapEntry key={veto.mapName} veto={veto} map={this.props.map} team={veto.type === "decider" ? null : map.team_ct.id === veto.teamId ? map.team_ct : map.team_t} />)}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class MapEntry extends React.PureComponent<{ veto: Veto, map: Map, team: Team | null }> {
|
||||
render() {
|
||||
const { veto, map, team } = this.props;
|
||||
return <div className="veto_entry">
|
||||
<div className="team_logo">{team ? <TeamLogo team={team} /> : null}</div>
|
||||
<div className={`map_name ${map.name.includes(veto.mapName) ? 'active' : ''}`}>{veto.mapName}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
66
src/HUD/Radar/radar.scss
Normal file
@ -0,0 +1,66 @@
|
||||
#radar_maps_container {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
border: none;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
transition: all 1s;
|
||||
.map-container {
|
||||
transition: all 1s;
|
||||
}
|
||||
}
|
||||
#iframe_radar {
|
||||
border: none;
|
||||
}
|
||||
#radar_maps_container.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
#radar_maps_container.preview {
|
||||
transform: rotate3d(1, 0, 0, 14deg) translateX(-50%);
|
||||
transform-style: preserve-3d;
|
||||
left: 50%;
|
||||
top: 100px;
|
||||
.map {
|
||||
perspective: 500px;
|
||||
}
|
||||
}
|
||||
.radar-component-container {
|
||||
width: 350px;
|
||||
height:350px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#maps_container {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.veto_entry {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
>div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.team_logo {
|
||||
img {
|
||||
max-height: 23px;
|
||||
padding-right: 3px;
|
||||
max-width: 23px;
|
||||
}
|
||||
.logo {
|
||||
height: 23px;
|
||||
width: 23px;
|
||||
}
|
||||
}
|
||||
.map_name.active {
|
||||
text-shadow: 0 0 15px white;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
45
src/HUD/SideBoxes/Money.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
class LossBox extends React.PureComponent<{ active: boolean, side: 'CT' | 'T' }>{
|
||||
render(){
|
||||
return <div className={`loss-box ${this.props.side} ${this.props.active ? 'active':''}`}></div>
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
side: 'left' | 'right',
|
||||
team: 'CT' | 'T',
|
||||
loss: number,
|
||||
equipment: number,
|
||||
money: number,
|
||||
show: boolean,
|
||||
}
|
||||
|
||||
export default class Money extends React.PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`moneybox ${this.props.side} ${this.props.team} ${this.props.show ? "show" : "hide"}`}>
|
||||
<div className="loss_container">
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 4} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 3} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 2} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 1} />
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Loss Bonus</div>
|
||||
<div className="value">${this.props.loss}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Team Money</div>
|
||||
<div className="value">${this.props.money}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Equipment Value</div>
|
||||
<div className="value">${this.props.equipment}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
49
src/HUD/SideBoxes/SideBox.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import './sideboxes.scss'
|
||||
import {configs, hudIdentity} from './../../App';
|
||||
import { apiUrl } from '../../api/api';
|
||||
|
||||
export default class SideBox extends React.Component<{ side: 'left' | 'right', hide: boolean}, { title: string, subtitle: string, image?: string }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title:'Title',
|
||||
subtitle:'Content',
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
configs.onChange((data:any) => {
|
||||
if(!data) return;
|
||||
const display = data.display_settings;
|
||||
if(!display) return;
|
||||
if(`${this.props.side}_title` in display){
|
||||
this.setState({title:display[`${this.props.side}_title`]})
|
||||
}
|
||||
if(`${this.props.side}_subtitle` in display){
|
||||
this.setState({subtitle:display[`${this.props.side}_subtitle`]})
|
||||
}
|
||||
if(`${this.props.side}_image` in display){
|
||||
const imageUrl = `${apiUrl}api/huds/${hudIdentity.name || 'dev'}/display_settings/${this.props.side}_image?isDev=${hudIdentity.isDev}&cache=${(new Date()).getTime()}`;
|
||||
this.setState({image:imageUrl})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { image, title, subtitle} = this.state;
|
||||
if(!title) return '';
|
||||
return (
|
||||
<div className={`sidebox ${this.props.side} ${this.props.hide ? 'hide':''}`}>
|
||||
<div className="title_container">
|
||||
<div className="title">{title}</div>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
</div>
|
||||
<div className="image_container">
|
||||
{image ? <img src={image} id={`image_left`} alt={'Left'}/>:''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
108
src/HUD/SideBoxes/UtilityLevel.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React from "react";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import { Player, WeaponRaw, Side } from "csgogsi-socket";
|
||||
|
||||
interface Props {
|
||||
sides?: 'reversed',
|
||||
show: boolean;
|
||||
side: 'CT' | 'T',
|
||||
players: Player[]
|
||||
}
|
||||
|
||||
function utilityState(amount: number) {
|
||||
if (amount === 20) {
|
||||
return "Full";
|
||||
}
|
||||
if (amount > 14) {
|
||||
return "Great";
|
||||
}
|
||||
if (amount > 9) {
|
||||
return "Good";
|
||||
}
|
||||
if (amount > 5) {
|
||||
return "Low";
|
||||
}
|
||||
if (amount > 0) {
|
||||
return "Poor";
|
||||
}
|
||||
return "None";
|
||||
}
|
||||
|
||||
function utilityColor(amount: number) {
|
||||
if (amount === 20) {
|
||||
return "#22f222";
|
||||
}
|
||||
if (amount > 14) {
|
||||
return "#32f218";
|
||||
}
|
||||
if (amount > 9) {
|
||||
return "#8ef218";
|
||||
}
|
||||
if (amount > 5) {
|
||||
return "#f29318";
|
||||
}
|
||||
if (amount > 0) {
|
||||
return "#f25618";
|
||||
}
|
||||
return "#f21822";
|
||||
}
|
||||
|
||||
function sum(grenades: WeaponRaw[], name: string) {
|
||||
return (
|
||||
grenades.filter(grenade => grenade.name === name).reduce((prev, next) => ({ ...next, ammo_reserve: (prev.ammo_reserve || 0) + (next.ammo_reserve || 0) }), { name: "", ammo_reserve: 0 })
|
||||
.ammo_reserve || 0
|
||||
);
|
||||
}
|
||||
|
||||
function parseGrenades(players: Player[], side: Side) {
|
||||
const grenades = players
|
||||
.filter(player => player.team.side === side)
|
||||
.map(player => Object.values(player.weapons).filter(weapon => weapon.type === "Grenade"))
|
||||
.flat()
|
||||
.map(grenade => ({ ...grenade, name: grenade.name.replace("weapon_", "") }));
|
||||
return grenades;
|
||||
}
|
||||
|
||||
export function summarise(players: Player[], side: Side) {
|
||||
const grenades = parseGrenades(players, side);
|
||||
return {
|
||||
hg: sum(grenades, "hegrenade"),
|
||||
flashes: sum(grenades, "flashbang"),
|
||||
smokes: sum(grenades, "smokegrenade"),
|
||||
inc: sum(grenades, "incgrenade") + sum(grenades, "molotov")
|
||||
};
|
||||
}
|
||||
|
||||
class GrenadeContainer extends React.PureComponent<{ grenade: string; amount: number }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="grenade_container">
|
||||
<div className="grenade_image">
|
||||
<Weapon weapon={this.props.grenade} active={false} isGrenade />
|
||||
</div>
|
||||
<div className="grenade_amount">x{this.props.amount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SideBox extends React.Component<Props> {
|
||||
render() {
|
||||
const grenades = summarise(this.props.players, this.props.side);
|
||||
const total = Object.values(grenades).reduce((a, b) => a+b, 0);
|
||||
return (
|
||||
<div className={`utilitybox ${this.props.side || ''} ${this.props.show ? "show" : "hide"}`}>
|
||||
<div className="title_container">
|
||||
<div className="title">Utility Level - </div>
|
||||
<div className="subtitle" style={{color: utilityColor(total)}}>{utilityState(total)}</div>
|
||||
</div>
|
||||
<div className="grenades_container">
|
||||
<GrenadeContainer grenade="smokegrenade" amount={grenades.smokes} />
|
||||
<GrenadeContainer grenade={this.props.side === 'CT' ? 'incgrenade' : 'molotov'} amount={grenades.inc} />
|
||||
<GrenadeContainer grenade="flashbang" amount={grenades.flashes} />
|
||||
<GrenadeContainer grenade="hegrenade" amount={grenades.hg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
221
src/HUD/SideBoxes/sideboxes.scss
Normal file
@ -0,0 +1,221 @@
|
||||
.boxes {
|
||||
position: fixed;
|
||||
bottom: 384px;
|
||||
}
|
||||
.boxes.left {
|
||||
left: 10px;
|
||||
}
|
||||
.boxes.right {
|
||||
right: 10px;
|
||||
}
|
||||
.boxes.hide {
|
||||
.utilitybox {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.moneybox {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.sidebox {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.utilitybox.hide {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.moneybox.hide {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.sidebox.hide {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.sidebox {
|
||||
width: 415px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
opacity: 1;
|
||||
transition: all 0.75s;
|
||||
background-color: var(--sub-panel-color);
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
align-items: center;
|
||||
.title_container {
|
||||
color: var(--white-full);
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
>div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.image_container {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
max-width: 70px;
|
||||
max-height: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.utilitybox {
|
||||
width: 415px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
opacity: 1;
|
||||
transition: all 0.75s;
|
||||
background-color: var(--sub-panel-color);
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
flex-direction: column;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
.title_container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
height: 24px;
|
||||
}
|
||||
.title {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.grenades_container {
|
||||
display: flex ;
|
||||
|
||||
flex: 1;
|
||||
height: 46px;
|
||||
.grenade_container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
color: white;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
height: 30px;
|
||||
width:auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.moneybox {
|
||||
width: 415px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
opacity: 1;
|
||||
transition: all 0.75s;
|
||||
background-color: var(--sub-panel-color);
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0px;
|
||||
.money_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
color: white;
|
||||
.title {
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
.value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.loss_container {
|
||||
width: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.loss-box {
|
||||
height: 13px;
|
||||
width: 100%;
|
||||
background-color: #515254;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.loss-box.active.CT {
|
||||
background-color: var(--color-new-ct);
|
||||
}
|
||||
.loss-box.active.T {
|
||||
background-color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebox.right {
|
||||
flex-direction: row-reverse;
|
||||
.title_container {
|
||||
>div {
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sidebox.left {
|
||||
.title_container {
|
||||
>div {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.utilitybox.CT {
|
||||
.title {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
}
|
||||
.utilitybox.T {
|
||||
.title {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
.moneybox.right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.moneybox.CT {
|
||||
.money_container {
|
||||
.title {
|
||||
color: var(--color-new-ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
.moneybox.T {
|
||||
.money_container {
|
||||
.title {
|
||||
color: var(--color-new-t);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/HUD/TeamOverview/TeamOverview.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import "./teamoverview.scss";
|
||||
|
||||
interface IProps {
|
||||
team: I.Team,
|
||||
show: boolean,
|
||||
veto: I.Veto | null
|
||||
}
|
||||
|
||||
export default class TeamOverview extends React.Component<IProps> {
|
||||
render() {
|
||||
if(!this.props.team) return null;
|
||||
return (
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/HUD/TeamOverview/teamoverview.scss
Normal file
@ -0,0 +1,42 @@
|
||||
.match-overview {
|
||||
opacity: 0;
|
||||
transition: opacity 0.75s;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
right: 10px;
|
||||
top: 375px;
|
||||
background-color: #000000a8;
|
||||
text-transform: uppercase;
|
||||
width: 250px;
|
||||
color: white;
|
||||
}
|
||||
.match-overview.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.match-overview-teams {
|
||||
display: flex;
|
||||
>div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 5px 0;
|
||||
&:not(.match-overview-vs) {
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
.match-overview-title {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
padding: 3px 0;
|
||||
}
|
||||
.match-overview-vs {
|
||||
width: 20px;
|
||||
flex: unset;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
font-size: 30px;
|
||||
}
|
||||
49
src/HUD/Timers/BombTimer.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
|
||||
import { GSI } from "./../../App";
|
||||
import BombTimer from "./Countdown";
|
||||
import { C4 } from "./../../assets/Icons";
|
||||
|
||||
export default class Bomb extends React.Component<any, { height: number; show: boolean }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: 0,
|
||||
show: false
|
||||
};
|
||||
}
|
||||
hide = () => {
|
||||
this.setState({ show: false, height: 100 });
|
||||
};
|
||||
componentDidMount() {
|
||||
const bomb = new BombTimer(time => {
|
||||
let height = time > 40 ? 4000 : time * 100;
|
||||
this.setState({ height: height / 40 });
|
||||
});
|
||||
bomb.onReset(this.hide);
|
||||
GSI.on("data", data => {
|
||||
if (data.bomb && data.bomb.countdown) {
|
||||
if (data.bomb.state === "planted") {
|
||||
this.setState({ show: true });
|
||||
return bomb.go(data.bomb.countdown);
|
||||
}
|
||||
if (data.bomb.state !== "defusing") {
|
||||
this.hide();
|
||||
}
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id={`bomb_container`}>
|
||||
<div className={`bomb_timer ${this.state.show ? "show" : "hide"}`} style={{ height: `${this.state.height}%` }}></div>
|
||||
<div className={`bomb_icon ${this.state.show ? "show" : "hide"}`}>
|
||||
<C4 fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/HUD/Timers/Countdown.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export default class Countdown {
|
||||
last: number;
|
||||
time: number;
|
||||
step: (time: number) => void;
|
||||
on: boolean;
|
||||
resetFunc?: Function;
|
||||
|
||||
constructor(step: (time: number) => void){
|
||||
this.last = 0;
|
||||
this.time = 0;
|
||||
this.on = false;
|
||||
this.step = step;
|
||||
}
|
||||
onReset(func: Function) {
|
||||
this.resetFunc = func;
|
||||
}
|
||||
stepWrapper = (time: number) =>{
|
||||
if(this.time < 0) return this.reset();
|
||||
if(!this.on) return this.reset();
|
||||
if(!this.last) this.last = time;
|
||||
if(this.time !== Number((this.time - (time - this.last)/1000))){
|
||||
this.time = Number((this.time - (time - this.last)/1000));
|
||||
this.step(this.time);
|
||||
}
|
||||
this.last =time;
|
||||
|
||||
|
||||
if(this.last) requestAnimationFrame(this.stepWrapper)
|
||||
}
|
||||
|
||||
go(duration: string | number){
|
||||
//console.log("STARTED WITH ", duration);
|
||||
if(typeof duration === "string") duration = Number(duration);
|
||||
if(Math.abs(duration - this.time) > 2) this.time = duration;
|
||||
this.on = true;
|
||||
if(!this.last ) requestAnimationFrame(this.stepWrapper);
|
||||
|
||||
}
|
||||
|
||||
reset(){
|
||||
this.last = 0;
|
||||
this.time = 0;
|
||||
this.on = false;
|
||||
if(this.resetFunc) this.resetFunc();
|
||||
}
|
||||
}
|
||||
41
src/HUD/Timers/PlantDefuse.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { Timer } from "../MatchBar/MatchBar";
|
||||
import { Player } from "csgogsi";
|
||||
import * as I from "./../../assets/Icons";
|
||||
|
||||
interface IProps {
|
||||
timer: Timer | null;
|
||||
side: "right" | "left"
|
||||
}
|
||||
|
||||
export default class Bomb extends React.Component<IProps> {
|
||||
getCaption = (type: "defusing" | "planting", player: Player | null) => {
|
||||
if(!player) return null;
|
||||
if(type === "defusing"){
|
||||
return <>
|
||||
<I.Defuse height={22} width={22} fill="var(--color-new-ct)" />
|
||||
<div className={'CT'}>{player.name} is defusing the bomb</div>
|
||||
</>;
|
||||
}
|
||||
return <>
|
||||
<I.SmallBomb height={22} fill="var(--color-new-t)"/>
|
||||
<div className={'T'}>{player.name} is planting the bomb</div>
|
||||
</>;
|
||||
}
|
||||
render() {
|
||||
const { side, timer } = this.props;
|
||||
return (
|
||||
<div className={`defuse_plant_container ${side} ${timer && timer.active ? 'show' :'hide'}`}>
|
||||
{
|
||||
timer ?
|
||||
<div className={`defuse_plant_caption`}>
|
||||
{this.getCaption(timer.type, timer.player)}
|
||||
</div> : null
|
||||
}
|
||||
|
||||
<div className="defuse_plant_bar" style={{ width: `${(timer && timer.width) || 0}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||