1
0
mirror of https://github.com/lexogrine/cs2-react-hud.git synced 2025-12-10 02:42:49 +01:00

initial commit

This commit is contained in:
Hubert Walczak 2023-09-11 12:37:32 +02:00
commit 989ede8638
No known key found for this signature in database
GPG Key ID: 17BB1C9355357860
247 changed files with 7656 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
GENERATE_SOURCEMAP=false
BROWSER=none

1
.env.development Normal file
View File

@ -0,0 +1 @@
PUBLIC_URL=/dev/

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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 of HUDs settings](settings.png)
## Preview
![Preview of HUDs panel in action](preview.png)
# 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.

8
aco Normal file

File diff suppressed because one or more lines are too long

45
craco.config.js Normal file
View 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;
}
}
};

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

60
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

13
public/hud.json Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
[
{
"bind":"Alt+C",
"action":"toggleCams"
}
]

148
public/panel.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

75
sign.js Normal file
View 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
View 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
View 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;

View 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
View 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;
}
}

View 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 };

View 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>
);
}
}

View 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>
);
}
}

View 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
View 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>
);
}
}

View 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>
);
}
}

View 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
View 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>
);
}
}

View 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>
);
}
}

View 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);
}

View 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>
</>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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}/>
</>
);
}
}

View 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>
}
}

View 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);
}

View 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>
);
}
}

View 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()}
</>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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;
}

View 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>
);
}
}

View 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
View 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;

View 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>
);
}
}

View 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;
}
}

View 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;
}
}

View 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;

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

View File

@ -0,0 +1,5 @@
const config = {
playerSize: 60,
}
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
import Firebomb from './firebomb.png'
import Flash from './flash.png'
import Smoke from './smoke.png'
export { Firebomb, Flash, Smoke };

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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;/**/
}

View 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, };

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View 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
}*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View 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
View 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)}
/>
}
}

View 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
View 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;
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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 -&nbsp;</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>
);
}
}

View 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);
}
}
}

View 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
);
}
}

View 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;
}

View 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>
);
}
}

View 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();
}
}

View 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>
);
}
}

Some files were not shown because too many files have changed in this diff Show More