Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f96e06a038 | |||
| 4ec8eea6f0 | |||
| a15e01cc49 | |||
| 02b7bb1a43 | |||
| 1604067244 | |||
| 263e9633f8 | |||
| ed39bdb41e | |||
| 4dec09c043 | |||
| 6708bcf48c | |||
| fe7b683fbe | |||
| 8ab5999780 | |||
| 8ad1a9410b | |||
| 4a2a871458 | |||
| dff9a6d4fb | |||
| 29ab58a0d9 | |||
| d9ecd18273 | |||
| abe80584fa | |||
| c31606706e | |||
| 68ec8960ab | |||
| 656fce7e4f | |||
| 02bf217740 | |||
| f88baa5fc9 |
@@ -1 +0,0 @@
|
|||||||
PUBLIC_URL=/dev/
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/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*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
src/HUD/Radar2
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,133 +1,148 @@
|
|||||||
|
### **CS2 React HUD for [LHM.gg](https://LHM.gg)**
|
||||||
|
|
||||||
<p align="center">
|
CS2 React HUD for [LHM.gg](https://LHM.gg), created by Lexogrine, is an open source Counter-Strike 2 HUD that you can use and modify to your needs. It’s the core element of building customized CS2 HUDs and spectator overlays for the [LHM.gg](https://LHM.gg) platform.
|
||||||
<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
|
It comes with a set of default options and features that you can use for creating your unique esport experience.
|
||||||
|
|
||||||
|
#### **Keybinds**
|
||||||
|
|
||||||
|
**Left Alt + B**
|
||||||
|
Makes radar smaller by 20px;
|
||||||
|
|
||||||
Fullfledged example of the React HUD made for HUD Manager. It has:
|
**Left Alt + V**
|
||||||
|
Makes radar bigger by 20px;
|
||||||
|
|
||||||
- Custom actions
|
**Left Alt + C**
|
||||||
- Keybinds
|
Toggles camera feed
|
||||||
- Player cam feed
|
|
||||||
- Custom Radar
|
|
||||||
|
|
||||||
## Keybinds:
|
#### **Panel**
|
||||||
### **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**
|
LHM HUDs can be configured in HUD Settings when opened in LHM. The schema for this configuration panel is available in `/public/panel.json`.
|
||||||
## Trivia settings
|
|
||||||
|
#### Trivia settings
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|--|--|
|
| -------------- | ----------- |
|
||||||
| Trivia title | `Text` |
|
| Trivia title | `Text` |
|
||||||
| Trivia content | `Text` |
|
| Trivia content | `Text` |
|
||||||
|
|
||||||
|
#### Display settings
|
||||||
## Display settings
|
|
||||||
|
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|--|--|
|
| --------------------------- | ------------ |
|
||||||
| Left/right box's title| `Text` |
|
|
||||||
| Left/right box's title | `Text` |
|
| Left/right box's title | `Text` |
|
||||||
|
| Left/right box's subtitle | `Text` |
|
||||||
| Left/right box's image logo | `Image file` |
|
| Left/right box's image logo | `Image file` |
|
||||||
|
|
||||||
## Example settings
|
#### **Preview**
|
||||||
|
|
||||||

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

|

|
||||||
|
|
||||||
# Download
|
#### **Download**
|
||||||
|
|
||||||
To download it just click here: [DOWNLOAD HUD](https://github.com/lexogrine/cs2-react-hud/releases/latest)
|
To download it, simply click here: [**DOWNLOAD CS React HUD for LHM.gg**](https://lhm.gg/download?target=cs2)
|
||||||
|
|
||||||
# Instruction
|
#### **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 CS2 data to the HUD.
|
|
||||||
|
|
||||||
## Identifying HUD
|
##### **Setting up**
|
||||||
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
|
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 LHM.gg open so it will pass CS2 data to the HUD.
|
||||||
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
|
##### **Identifying HUD**
|
||||||
|
|
||||||
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.
|
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**
|
||||||
|
|
||||||
## File structure
|
To build a 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 LHM.gg upload area.
|
||||||
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.
|
|
||||||
|
|
||||||
|
##### **Signing**
|
||||||
|
|
||||||
## `panel.json` API
|
To create Signed CS2 HUD for [LHM.gg](https://LHM.gg) to prevent at least from modifying compiled JavaScript files, run `npm run sign`. It's the same as `npm run pack` command but with an additional step of signing `.js` and `.css` files and `hud.json`.
|
||||||
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;
|
##### **File structure**
|
||||||
|
|
||||||
if(!display) return;
|
The HUD is separated into two parts - the API part, which connects to the LHM.gg API and communicates with it: `src/App.tsx` file and `src/api` directory. Usually, you don't want to play with it, so the whole thing 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 separated into its own folder. Styles are kept in the `src/HUD/styles`. Names are quite self-explanatory, and to modify the style of the element, you should just find the styling by the file and class name.
|
||||||
|
|
||||||
if(display[`${this.props.side}_title`]){
|
##### **panel.json API**
|
||||||
this.setState({title:display[`${this.props.side}_title`]})
|
|
||||||
}
|
To get the incoming data from the LHM.gg, let's take a look at the `src/HUD/SideBoxes/SideBox.tsx` component:
|
||||||
if(display[`${this.props.side}_subtitle`]){
|
|
||||||
this.setState({subtitle:display[`${this.props.side}_subtitle`]})
|
```typescript
|
||||||
}
|
const Sidebox = ({ side, hide }: { side: "left" | "right"; hide: boolean }) => {
|
||||||
if(display[`${this.props.side}_image`]){
|
const [image, setImage] = useState<string | null>(null);
|
||||||
this.setState({image:display[`${this.props.side}_image`]})
|
const data = useConfig("display_settings");
|
||||||
|
|
||||||
|
useOnConfigChange(
|
||||||
|
"display_settings",
|
||||||
|
(data) => {
|
||||||
|
if (data && `${side}_image` in data) {
|
||||||
|
const imageUrl = `${apiUrl}api/huds/${
|
||||||
|
hudIdentity.name || "dev"
|
||||||
|
}/display_settings/${side}_image?isDev=${
|
||||||
|
hudIdentity.isDev
|
||||||
|
}&cache=${new Date().getTime()}`;
|
||||||
|
setImage(imageUrl);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data || !data[`${side}_title`]) return null;
|
||||||
|
return (
|
||||||
|
<div className={`sidebox ${side} ${hide ? "hide" : ""}`}>
|
||||||
|
<div className="title_container">
|
||||||
|
<div className="title">{data[`${side}_title`]}</div>
|
||||||
|
<div className="subtitle">{data[`${side}_subtitle`]}</div>
|
||||||
|
</div>
|
||||||
|
<div className="image_container">
|
||||||
|
{image ? <img src={image} id={`image_left`} alt={"Left"} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
You can just read data from the HUDs settings by using `useConfig` hook. Everything is now strictly typed. If you make a change to the panel or keybinds JSON files, Vite server will automatically generate types for you, so `useConfig` should always be up to date.
|
||||||
|
|
||||||
|
If you want to listen for a change in settings, you can use `useOnConfigChange`. In this case, we are using this to force refresh the `src` attribute of the `img` element.
|
||||||
|
|
||||||
|
If you want to listen for action input, you can just use `useAction` hook, like here in `Trivia.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useAction("triviaState", (state) => {
|
||||||
|
setShow(state === "show");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
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
|
For the action input, we need to import the `actions` object and create a listener with the parameter on it.
|
||||||
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`:
|
|
||||||
|
##### **keybinds.json API**
|
||||||
|
|
||||||
|
Keybinds API works in a very similar way to `panel.json` action API. This time, the example will be from `RadarMaps.tsx`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useAction(
|
||||||
|
"radarBigger",
|
||||||
|
() => {
|
||||||
|
setRadarSize((p) => p + 10);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useAction(
|
||||||
|
"radarSmaller",
|
||||||
|
() => {
|
||||||
|
setRadarSize((p) => p - 10);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **Killfeed**
|
||||||
|
|
||||||
|
Listening for kills is very easy - we can see that in `src/HUD/Killfeed/Killfeed.tsx`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
GSI.on("kill", kill => {
|
GSI.on("kill", kill => {
|
||||||
@@ -135,8 +150,17 @@ componentDidMount() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
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
|
The Killfeed component basically just keeps kills in the state during the round, and after the round, it cleans the state. Kills have a CSS animation, which makes them gently show, and after a few seconds, disappear. The experience is very smooth. You can fiddle with the styling in the `killfeed.scss`. This killfeed detects who killed whom, if there was an assist (flash assist as well), used weapon, headshot, and wallbang.
|
||||||
Radar is custom React-based component, made by Hubert Walczak, and is easily editable from css.
|
|
||||||
|
##### **Radar**
|
||||||
|
|
||||||
|
Radar is a custom React-based component made by Hubert Walczak and is easily editable from CSS.
|
||||||
|
|
||||||
|
#### **About Lexogrine**
|
||||||
|
|
||||||
|
[Lexogrine](https://lexogrine.com) is an AI software development company, offering top-tier AI, web, and mobile design and development services for international companies. Alongside that, Lexogrine offers a set of web and mobile applications - including [LHM.gg](https://LHM.gg) - that revolutionize the way experts and specialists from different industries work together on a daily basis.
|
||||||
|
|
||||||
|
[Lexogrine](https://lexogrine.com) specializes in AI development, alongside web, mobile, and cloud development with technologies like TypeScript, Python, LLM, React, React Native, Node.js, Prisma, Medusa, Pytorch, AWS, and Google Cloud Platform.
|
||||||
|
|
||||||
|
With over 5 years of experience, Lexogrine delivered hundreds of projects, supporting companies and enterprises from all over the world.
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>LHM CS2 Default HUD</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,60 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "lexogrine_cs2_hud",
|
"name": "cs2-react-hud",
|
||||||
"version": "1.0.1",
|
|
||||||
"homepage": "./",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"version": "1.0.6",
|
||||||
"@craco/craco": "^5.7.0",
|
"type": "module",
|
||||||
"@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.2",
|
|
||||||
"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": {
|
"scripts": {
|
||||||
"zip": "npm-build-zip",
|
"zip": "npm-build-zip",
|
||||||
"start": "craco start",
|
"dev": "vite --host",
|
||||||
"build": "react-scripts build",
|
"start": "vite --host",
|
||||||
"test": "react-scripts test",
|
"build": "tsc && vite build",
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"pack": "npm run build && npm run zip",
|
"pack": "npm run build && npm run zip",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview",
|
||||||
"sign": "npm run build && node sign.js && npm-build-zip --name=signed"
|
"sign": "npm run build && node sign.js && npm-build-zip --name=signed"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"dependencies": {
|
||||||
"extends": "react-app"
|
"csgogsi": "^3.0.5",
|
||||||
},
|
"jsonwebtoken": "^9.0.2",
|
||||||
"browserslist": {
|
"react": "^18.2.0",
|
||||||
"production": [
|
"react-dom": "^18.2.0",
|
||||||
">0.2%",
|
"simple-peer": "^9.11.1",
|
||||||
"not dead",
|
"simple-websockets": "^1.2.0",
|
||||||
"not op_mini all"
|
"socket.io-client": "^4.7.2",
|
||||||
],
|
"uuid": "^9.0.1"
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/history": "^4.7.5",
|
"@types/react": "^18.2.15",
|
||||||
"@types/socket.io-client": "^1.4.32",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/uuid": "^8.3.1",
|
"@types/simple-peer": "^9.11.5",
|
||||||
"internal-ip": "^6.2.0",
|
"@types/uuid": "^9.0.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"npm-build-zip": "^1.0.2",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"open": "^8.0.2",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
"sass": "^1.32.5",
|
"eslint": "^8.45.0",
|
||||||
"socket.io-client": "^2.4.0"
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"npm-build-zip": "^1.0.4",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"sass": "^1.68.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-compatible-readable-stream": "^3.6.1",
|
||||||
|
"vite-plugin-svgr": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name":"Lexogrine CS2 HUD",
|
"name":"LHM CS2 Default HUD",
|
||||||
"version":"1.0.0",
|
"version":"1.0.6",
|
||||||
"author":"Lexogrine",
|
"author":"Lexogrine",
|
||||||
"legacy": false,
|
"legacy": false,
|
||||||
"radar": true,
|
"radar": true,
|
||||||
"killfeed": false,
|
"killfeed": false,
|
||||||
"game":"cs2",
|
"game":"cs2",
|
||||||
"boltobserv":{
|
"boltobserv":{
|
||||||
"css":true,
|
"css":false,
|
||||||
"maps":true
|
"maps":false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -2,5 +2,13 @@
|
|||||||
{
|
{
|
||||||
"bind":"Alt+C",
|
"bind":"Alt+C",
|
||||||
"action":"toggleCams"
|
"action":"toggleCams"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bind":"Alt+V",
|
||||||
|
"action":"radarBigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bind":"Alt+B",
|
||||||
|
"action":"radarSmaller"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const crypto = require('crypto');
|
import crypto from 'crypto';
|
||||||
const jwt = require('jsonwebtoken');
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __esm_dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const sign = () => {
|
const sign = () => {
|
||||||
const getAllFilesToSign = (hudDir) => {
|
const getAllFilesToSign = (hudDir) => {
|
||||||
@@ -17,7 +20,7 @@ const sign = () => {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dir = path.join(__dirname, 'build');
|
const dir = path.join(__esm_dirname, 'build');
|
||||||
|
|
||||||
const keyFile = path.join(dir, 'key');
|
const keyFile = path.join(dir, 'key');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Instance, SignalData } from 'simple-peer';
|
import { Instance, SignalData } from 'simple-peer';
|
||||||
import api from '../../api/api';
|
import api from '..';
|
||||||
import { socket as Socket } from "../../App";
|
import { socket } from '../socket';
|
||||||
const Peer = require('simple-peer');
|
import Peer from 'simple-peer';
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
|
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ const closeConnection = (steamid: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initiateConnection = async () => {
|
const initiateConnection = async () => {
|
||||||
const socket = Socket as SocketIOClient.Socket;
|
|
||||||
const camera = await api.camera.get();
|
const camera = await api.camera.get();
|
||||||
await wait(1000);
|
await wait(1000);
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ const initiateConnection = async () => {
|
|||||||
|
|
||||||
if (camera.uuid !== roomId) return;
|
if (camera.uuid !== roomId) return;
|
||||||
|
|
||||||
const peerConnection: PeerInstance = new Peer({ initiator: false, trickle: false });
|
const peerConnection = new Peer({ initiator: false, trickle: false }) as PeerInstance;
|
||||||
|
|
||||||
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
|
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
|
||||||
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { CSGOGSI, Player, PlayerExtension } from 'csgogsi';
|
||||||
|
import api, { isDev } from '..';
|
||||||
|
|
||||||
|
export const hudIdentity = {
|
||||||
|
name: '',
|
||||||
|
isDev
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GSI = new CSGOGSI();
|
||||||
|
GSI.regulationMR = 12;
|
||||||
|
|
||||||
|
GSI.on("data", data => {
|
||||||
|
loadPlayers(data.players);
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestedSteamIDs: string[] = [];
|
||||||
|
|
||||||
|
const loadPlayers = async (players: Player[]) => {
|
||||||
|
const leftOvers = players.filter(player => !requestedSteamIDs.includes(player.steamid));
|
||||||
|
const leftOverSteamids = leftOvers.map(player => player.steamid);
|
||||||
|
if(!leftOvers.length) return;
|
||||||
|
|
||||||
|
requestedSteamIDs.push(...leftOverSteamids);
|
||||||
|
|
||||||
|
const extensions = await api.players.get(leftOverSteamids);
|
||||||
|
|
||||||
|
const playersExtensions: PlayerExtension[] = extensions.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,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
GSI.players.push(...playersExtensions);
|
||||||
|
|
||||||
|
|
||||||
|
leftOvers.forEach(player => {
|
||||||
|
loadAvatarURL(player);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface AvatarLoader {
|
||||||
|
loader: Promise<string>,
|
||||||
|
url: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatars: { [key: string]: AvatarLoader } = {};
|
||||||
|
|
||||||
|
const loadAvatarURL = (player: Player) => {
|
||||||
|
if(!player.steamid) return;
|
||||||
|
if(avatars[player.steamid]) return avatars[player.steamid].url;
|
||||||
|
avatars[player.steamid] = {
|
||||||
|
url: player.avatar || '',
|
||||||
|
loader: new Promise((resolve) => {
|
||||||
|
api.players.getAvatarURLs(player.steamid).then(result => {
|
||||||
|
const avatarUrl = result.custom || result.steam;
|
||||||
|
const existing = GSI.players.find(playerEx => playerEx.steamid === player.steamid);
|
||||||
|
const target = existing || {
|
||||||
|
id: player.steamid,
|
||||||
|
name: player.name,
|
||||||
|
realName: player.realName,
|
||||||
|
steamid: player.steamid,
|
||||||
|
country: player.country,
|
||||||
|
avatar: player.avatar,
|
||||||
|
extra: player.extra
|
||||||
|
}
|
||||||
|
if(target) target.avatar = avatarUrl;
|
||||||
|
|
||||||
|
if(!existing){
|
||||||
|
GSI.players.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
avatars[player.steamid].url = result.custom || result.steam;
|
||||||
|
resolve(result.custom || result.custom);
|
||||||
|
}).catch(() => {
|
||||||
|
delete avatars[player.steamid];
|
||||||
|
resolve('');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useEffect, useState, createContext, ReactNode, useContext } from "react";
|
||||||
|
import ActionManager, { ActionHandler, ConfigManager } from "./managers";
|
||||||
|
import { Events } from "csgogsi";
|
||||||
|
import { GSI } from "../HUD";
|
||||||
|
import type { AllActions, AllInputs, GetInputsFromSection, Sections } from "./settings";
|
||||||
|
|
||||||
|
export const actions = new ActionManager();
|
||||||
|
export const configs = new ConfigManager();
|
||||||
|
|
||||||
|
type EmptyListener = () => void;
|
||||||
|
|
||||||
|
type BaseEvents = keyof Events;
|
||||||
|
type Callback<K> = K extends BaseEvents ? Events[K] | EmptyListener : EmptyListener;
|
||||||
|
|
||||||
|
export function useAction<T extends keyof AllActions>(action: T, callback: ActionHandler<T extends keyof AllActions ? AllActions[T] : never>, deps?: React.DependencyList) {
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
actions.on(action, callback);
|
||||||
|
return () => {
|
||||||
|
actions.off(action, callback);
|
||||||
|
};
|
||||||
|
}, deps ? [action, ...deps] : [action, callback]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function useOnConfigChange<K extends keyof Sections, T = any>(section: K, callback: ActionHandler<{ [L in keyof (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)]?: (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)[L] } | null>, deps?: React.DependencyList){
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onDataChanged = (data: any) => {
|
||||||
|
callback(data?.[section] || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
configs.onChange(onDataChanged);
|
||||||
|
onDataChanged(configs.data);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
configs.off(onDataChanged);
|
||||||
|
}
|
||||||
|
}, deps ? [section, ...deps] : [section, callback])
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onGSI<T extends BaseEvents>(event: T, callback: Callback<T>, deps?: React.DependencyList){
|
||||||
|
useEffect(() => {
|
||||||
|
GSI.on(event, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
GSI.off(event, callback);
|
||||||
|
}
|
||||||
|
}, deps ? [event, ...deps] : [event, callback])
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsContext = createContext<AllInputs | null>({} as AllInputs);
|
||||||
|
export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [ data, setData ] = useState<AllInputs | null>(configs.data as AllInputs || null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onDataChanged = (data: any) => {
|
||||||
|
setData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
configs.onChange(onDataChanged);
|
||||||
|
onDataChanged(configs.data);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
configs.off(onDataChanged);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider
|
||||||
|
value={data}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useConfig<K extends keyof Sections>(section: K){
|
||||||
|
const context = useContext(SettingsContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('generic Hook must be used within a Generic Provider');
|
||||||
|
}
|
||||||
|
return context?.[section];
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const keybindDefinition = [
|
||||||
|
{
|
||||||
|
"bind": "Alt+C",
|
||||||
|
"action": "toggleCams"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bind": "Alt+V",
|
||||||
|
"action": "radarBigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bind": "Alt+B",
|
||||||
|
"action": "radarSmaller"
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
export type ActionHandler<T = any> = (data: T) => void;
|
||||||
|
|
||||||
|
export default class ActionManager {
|
||||||
|
handlers: { [K in string]?: ActionHandler[] };
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.handlers = {}
|
||||||
|
|
||||||
|
/*this.on('data', _data => {
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
execute = <T = any>(eventName: string, argument?: T) => {
|
||||||
|
const handlers = this.handlers[eventName] || [];
|
||||||
|
for(const handler of handlers){
|
||||||
|
handler(argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on = <T = any>(eventName: string, handler: ActionHandler<T>) => {
|
||||||
|
if(!this.handlers[eventName]) this.handlers[eventName] = [];
|
||||||
|
this.handlers[eventName]!.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
off = (eventName: string, handler: ActionHandler) => {
|
||||||
|
if(!this.handlers[eventName]) this.handlers[eventName] = [];
|
||||||
|
this.handlers[eventName] = this.handlers[eventName]!.filter(h => h !== handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ConfigManager {
|
||||||
|
listeners: ActionHandler[];
|
||||||
|
data: { [K in string]?: any };
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
this.listeners = [];
|
||||||
|
this.data = {};
|
||||||
|
}
|
||||||
|
save(data: { [K in string]?: any }){
|
||||||
|
this.data = data;
|
||||||
|
this.execute();
|
||||||
|
|
||||||
|
/*const listeners = this.listeners.get(eventName);
|
||||||
|
if(!listeners) return false;
|
||||||
|
listeners.forEach(callback => {
|
||||||
|
if(argument) callback(argument);
|
||||||
|
else callback();
|
||||||
|
});
|
||||||
|
return true;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(){
|
||||||
|
const listeners = this.listeners;
|
||||||
|
if(!listeners || !listeners.length) return false;
|
||||||
|
listeners.forEach(listener => {
|
||||||
|
listener(this.data);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (listener: ActionHandler) => {
|
||||||
|
const listOfListeners = this.listeners || [];
|
||||||
|
listOfListeners.push(listener);
|
||||||
|
this.listeners = listOfListeners;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
off = (listener: ActionHandler) => {
|
||||||
|
this.listeners = this.listeners.filter(l => l !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
export const panelDefinition = [
|
||||||
|
{
|
||||||
|
"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": "select",
|
||||||
|
"name": "replace_avatars",
|
||||||
|
"label": "Use team logos as player avatars",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"label": "Only if player has no avatar",
|
||||||
|
"name": "if_missing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Always",
|
||||||
|
"name": "always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Match, Player, Team } from "../types"
|
||||||
|
import { keybindDefinition } from "./keybinds";
|
||||||
|
import { panelDefinition } from "./panel";
|
||||||
|
|
||||||
|
type Prettify<T> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
} & {};
|
||||||
|
|
||||||
|
type Settings = typeof panelDefinition;
|
||||||
|
|
||||||
|
type Keybinds = typeof keybindDefinition;
|
||||||
|
|
||||||
|
|
||||||
|
type InputMapper = {
|
||||||
|
text: string;
|
||||||
|
player: {
|
||||||
|
type: "player",
|
||||||
|
id: string,
|
||||||
|
player: Player | null
|
||||||
|
};
|
||||||
|
match: {
|
||||||
|
type: "match",
|
||||||
|
id: string,
|
||||||
|
match: Match | null
|
||||||
|
};
|
||||||
|
team: {
|
||||||
|
type: "team",
|
||||||
|
id: string,
|
||||||
|
team: Team | null
|
||||||
|
};
|
||||||
|
checkbox: boolean;
|
||||||
|
action: never;
|
||||||
|
image: string;
|
||||||
|
images: string[];
|
||||||
|
select: string;
|
||||||
|
}
|
||||||
|
type NonNeverKeys<T extends { [K: string]: any }> = { [K in keyof T]: T[K] extends never ? never : K }[keyof T];
|
||||||
|
|
||||||
|
|
||||||
|
type ValueMapper<T extends Settings[number]["inputs"]> = { [K in T[number] as K["name"]]: K extends { type: "action" } ? never : (K extends { type: "select" } ? K["values"][number]["name"] | "" : InputMapper[K["type"]]) }
|
||||||
|
|
||||||
|
export type GetInputsFromSection<T extends Settings[number]["inputs"]> = { [K in NonNeverKeys<ValueMapper<T>>]: ValueMapper<T>[K]};
|
||||||
|
export type Sections = { [K in Settings[number] as K["name"]]: K["inputs"]}
|
||||||
|
|
||||||
|
export type AllInputs = { [K in keyof Sections]: GetInputsFromSection<Sections[K]> };
|
||||||
|
|
||||||
|
type ActionValueMapper<T extends Settings[number]["inputs"]> = { [K in T[number] as K["name"]]: K extends { type: "action" } ? K["values"][number]["name"] : never }
|
||||||
|
|
||||||
|
type GetActionsFromSection<T extends Settings[number]["inputs"]> = { [K in NonNeverKeys<ActionValueMapper<T>>]: ActionValueMapper<T>[K]};
|
||||||
|
|
||||||
|
export type AllActions = Prettify<GetActionsFromSection<Settings[number]["inputs"]> & { [K in Keybinds[number]["action"]]: never }>;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import * as I from './types';
|
||||||
|
|
||||||
|
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
export const port = Number(query.get('port') || 1349);
|
||||||
|
export const variant = query.get("variant") || "default";
|
||||||
|
|
||||||
|
export const isDev = !query.get("isProd");
|
||||||
|
|
||||||
|
export const config = { apiAddress: isDev ? `http://localhost:${port}/` : '/' }
|
||||||
|
export const apiUrl = config.apiAddress;
|
||||||
|
|
||||||
|
export async function apiV2(url: string, method = 'GET', body?: any) {
|
||||||
|
const options: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
|
options.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
let data: any = null;
|
||||||
|
return fetch(`${apiUrl}api/${url}`, options)
|
||||||
|
.then(res => {
|
||||||
|
data = res;
|
||||||
|
return res.json().catch(_e => data && data.status < 300)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
match: {
|
||||||
|
get: async (): Promise<I.Match[]> => apiV2(`match`),
|
||||||
|
getCurrent: async (): Promise<I.Match> => apiV2(`match/current`)
|
||||||
|
},
|
||||||
|
camera: {
|
||||||
|
get: (): Promise<{ availablePlayers: ({ steamid: string, label: string })[], uuid: string }> => apiV2('camera'),
|
||||||
|
toggleVmix: (status?: boolean) => new Promise<boolean>(r => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
// let finished = false;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
r(false);
|
||||||
|
}, 1000)
|
||||||
|
fetch(`http://localhost:2715/visibility${status !== undefined ? `?status=${status}` : ''}`, { method: "POST", signal }).then(() => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
r(true);
|
||||||
|
//finished = true;
|
||||||
|
}).catch(() => { r(false); });
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
|
||||||
|
get: (): Promise<I.Team[]> => apiV2(`teams`),
|
||||||
|
},
|
||||||
|
players: {
|
||||||
|
get: async (steamids?: string[]): Promise<I.Player[]> => apiV2(steamids ? `players?steamids=${steamids.join(';')}` : `players`),
|
||||||
|
getAvatarURLs: async (steamid: string): Promise<{ custom: string, steam: string }> => apiV2(`players/avatar/steamid/${steamid}`)
|
||||||
|
},
|
||||||
|
tournaments: {
|
||||||
|
get: () => apiV2('tournament')
|
||||||
|
},
|
||||||
|
maps: {
|
||||||
|
get: (): Promise<I.GameMap[]> => apiV2('game-maps/cs2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { isDev, port } from ".";
|
||||||
|
import { GSI, hudIdentity } from "./HUD";
|
||||||
|
import { CSGORaw } from "csgogsi";
|
||||||
|
import { actions, configs } from "./contexts/actions";
|
||||||
|
import { initiateConnection } from "./HUD/camera";
|
||||||
|
|
||||||
|
export const socket = io(isDev ? `localhost:${port}` : '/');
|
||||||
|
|
||||||
|
type RoundPlayerDamage = {
|
||||||
|
steamid: string;
|
||||||
|
damage: number;
|
||||||
|
};
|
||||||
|
type RoundDamage = {
|
||||||
|
round: number;
|
||||||
|
players: RoundPlayerDamage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("update", (data: any, damage: any) => {
|
||||||
|
if (damage) {
|
||||||
|
GSI.damage = damage;
|
||||||
|
}
|
||||||
|
GSI.digest(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isInWindow = !!window.parent.ipcApi;
|
||||||
|
|
||||||
|
if(isInWindow){
|
||||||
|
window.parent.ipcApi.receive('raw', (data: CSGORaw, damage?: RoundDamage[]) => {
|
||||||
|
if(damage){
|
||||||
|
GSI.damage = damage;
|
||||||
|
}
|
||||||
|
GSI.digest(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = window.location.href;
|
||||||
|
|
||||||
|
socket.emit("started");
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
hudIdentity.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);
|
||||||
|
hudIdentity.name = segment.substr(0, segment.lastIndexOf('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("readyToRegister", () => {
|
||||||
|
socket.emit("register", hudIdentity.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);
|
||||||
|
})
|
||||||
@@ -46,11 +46,21 @@ export interface DepthTournamentMatchup extends TournamentMatchup {
|
|||||||
parents: DepthTournamentMatchup[];
|
parents: DepthTournamentMatchup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TournamentTypes = 'swiss' | 'single' | 'double';
|
||||||
|
|
||||||
|
export type TournamentStage = {
|
||||||
|
type: TournamentTypes;
|
||||||
|
matchups: TournamentMatchup[];
|
||||||
|
teams: number;
|
||||||
|
phases: number;
|
||||||
|
participants: string[];
|
||||||
|
};
|
||||||
export interface Tournament {
|
export interface Tournament {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
matchups: TournamentMatchup[];
|
groups: TournamentStage[];
|
||||||
|
playoffs: TournamentStage;
|
||||||
autoCreate: boolean;
|
autoCreate: boolean;
|
||||||
}
|
}
|
||||||
export interface RoundData {
|
export interface RoundData {
|
||||||
@@ -149,3 +159,28 @@ export type Knife =
|
|||||||
| "knife_ursus"//
|
| "knife_ursus"//
|
||||||
| "knife_widowmaker"//
|
| "knife_widowmaker"//
|
||||||
| "knife_canis";//
|
| "knife_canis";//
|
||||||
|
|
||||||
|
export interface GameMapRadar {
|
||||||
|
id: number;
|
||||||
|
lhmId: string;
|
||||||
|
originHeight?: number;
|
||||||
|
originX?: number;
|
||||||
|
originY?: number;
|
||||||
|
pxPerUX?: number;
|
||||||
|
pxPerUY?: number;
|
||||||
|
radar: string;
|
||||||
|
visibleOverHeight: number | null;
|
||||||
|
visibleUnderHeight: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameMap {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
lhmId: string;
|
||||||
|
game: string;
|
||||||
|
image?: string;
|
||||||
|
radars: GameMapRadar[];
|
||||||
|
verticalImage?: string;
|
||||||
|
inVetoPool: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@import "fonts/montserrat.css";
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Rajdhani", 'Montserrat', 'Roboto', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
/*background-image: url('./assets/bg.png');/**/
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Louis George Cafe';
|
||||||
|
src: local('Louis George Cafe'), url('./fonts/Louis George Cafe.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rounded_Elegance';
|
||||||
|
src: local('Rounded_Elegance'), url('./fonts/Rounded_Elegance.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
perspective: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--main-panel-color: rgba(12, 15, 18, 0.85);
|
||||||
|
--sub-panel-color: rgba(0, 0, 0, 0.83);
|
||||||
|
--white-dull: rgba(10, 5, 5, 0.25);
|
||||||
|
--white-full: rgba(250, 250, 250, 1);
|
||||||
|
--white-half: rgba(250, 250, 250, 0.5);
|
||||||
|
--color-gray: rgba(191, 191, 191, 1.0);
|
||||||
|
--color-moneys: #a7d32e;
|
||||||
|
--dev-purple: rgba(200, 0, 255, 1);
|
||||||
|
--color-t: #c19511;
|
||||||
|
--color-ct: #5788a8;
|
||||||
|
--color-new-t: #f0c941;
|
||||||
|
--color-new-ct: #5ab8f4;
|
||||||
|
--color-bomb: #f22222;
|
||||||
|
--color-defuse: #2222f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,172 +1,28 @@
|
|||||||
import React from 'react';
|
import { useEffect, useState } from 'react'
|
||||||
|
import './App.css'
|
||||||
|
import { CSGO } from 'csgogsi'
|
||||||
|
import { SettingsProvider, onGSI } from './API/contexts/actions'
|
||||||
import Layout from './HUD/Layout/Layout';
|
import Layout from './HUD/Layout/Layout';
|
||||||
import api, { port, isDev } from './api/api';
|
import './API/socket';
|
||||||
import { loadAvatarURL } from './api/avatars';
|
import { Match } from './API/types';
|
||||||
import ActionManager, { ConfigManager } from './api/actionManager';
|
import api from './API';
|
||||||
|
import { GSI } from './API/HUD';
|
||||||
|
import { socket } from './API/socket';
|
||||||
|
|
||||||
import { CSGO, PlayerExtension, GSISocket, CSGORaw } from "csgogsi-socket";
|
function App() {
|
||||||
import { Match } from './api/interfaces';
|
const [ game, setGame ] = useState<CSGO | null>(null);
|
||||||
import { initiateConnection } from './HUD/Camera/mediaStream';
|
const [ match, setMatch ] = useState<Match | null>(null);
|
||||||
|
|
||||||
let isInWindow = !!window.parent.ipcApi;
|
useEffect(() => {
|
||||||
|
const onMatchPing = () => {
|
||||||
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 => {
|
api.match.getCurrent().then(match => {
|
||||||
if (!match) {
|
if (!match) {
|
||||||
GSI.teams.left = null;
|
GSI.teams.left = null;
|
||||||
GSI.teams.right = null;
|
GSI.teams.right = null;
|
||||||
|
setMatch(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ match });
|
setMatch(match);
|
||||||
|
|
||||||
let isReversed = false;
|
let isReversed = false;
|
||||||
if (GSI.last) {
|
if (GSI.last) {
|
||||||
@@ -175,7 +31,6 @@ class App extends React.Component<any, { match: Match | null, game: CSGO | null,
|
|||||||
if (current && current.reverseSide) {
|
if (current && current.reverseSide) {
|
||||||
isReversed = true;
|
isReversed = true;
|
||||||
}
|
}
|
||||||
this.setState({ checked: true });
|
|
||||||
}
|
}
|
||||||
if (match.left.id) {
|
if (match.left.id) {
|
||||||
api.teams.getOne(match.left.id).then(left => {
|
api.teams.getOne(match.left.id).then(left => {
|
||||||
@@ -199,17 +54,30 @@ class App extends React.Component<any, { match: Match | null, game: CSGO | null,
|
|||||||
|
|
||||||
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
//dataLoader.match = null;
|
GSI.teams.left = null;
|
||||||
});
|
GSI.teams.right = null;
|
||||||
|
setMatch(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
socket.on("match", onMatchPing);
|
||||||
|
onMatchPing();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("match", onMatchPing);
|
||||||
}
|
}
|
||||||
render() {
|
}, [])
|
||||||
if (!this.state.game) return null;
|
|
||||||
|
onGSI('data', game => {
|
||||||
|
|
||||||
|
setGame(game);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!game) return null;
|
||||||
return (
|
return (
|
||||||
<Layout game={this.state.game} match={this.state.match} />
|
<SettingsProvider>
|
||||||
|
<Layout game={game} match={match} />
|
||||||
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export default App
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { mediaStreams } from "./mediaStream";
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { mediaStreams } from "../../API/HUD/camera";
|
||||||
type Props = {
|
type Props = {
|
||||||
steamid: string,
|
steamid: string,
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import PlayerCamera from "./Camera";
|
import PlayerCamera from "./Camera";
|
||||||
import api from "../../api/api";
|
import api from "../../API";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Player } from 'csgogsi-socket';
|
import { ArmorFull, ArmorHelmet } from "./../../assets/Icons";
|
||||||
import {ArmorHelmet, ArmorFull} from './../../assets/Icons';
|
|
||||||
export default class Armor extends React.Component<{ player: Player }> {
|
const Armor = ({ health, armor, helmet }: { health: number, armor: number, helmet: boolean }) => {
|
||||||
render() {
|
if (!health || !armor) return null;
|
||||||
const { player } = this.props;
|
|
||||||
if(!player.state.health || !player.state.armor) return '';
|
|
||||||
return (
|
return (
|
||||||
<div className={`armor_indicator`}>
|
<div className={`armor_indicator`}>
|
||||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull/>}
|
{helmet ? <ArmorHelmet /> : <ArmorFull />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
export default React.memo(Armor);
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import { Player } from "csgogsi";
|
||||||
import { Player } from 'csgogsi-socket';
|
import { Bomb as BombIcon } from "./../../assets/Icons";
|
||||||
import {Bomb as BombIcon} from './../../assets/Icons';
|
const Bomb = ({ player }: { player: Player }) => {
|
||||||
export default class Bomb extends React.Component<{ player: Player }> {
|
if (Object.values(player.weapons).every((weapon) => weapon.type !== "C4")) {
|
||||||
render() {
|
return null;
|
||||||
const { player } = this.props;
|
}
|
||||||
if(Object.values(player.weapons).every(weapon => weapon.type !== "C4")) return '';
|
|
||||||
return (
|
return (
|
||||||
<div className={`armor_indicator`}>
|
<div className={`armor_indicator`}>
|
||||||
<BombIcon />
|
<BombIcon />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default Bomb;
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React from 'react';
|
import { Player } from 'csgogsi';
|
||||||
import { Player } from 'csgogsi-socket';
|
|
||||||
import {Defuse as DefuseIcon} from './../../assets/Icons';
|
import {Defuse as DefuseIcon} from './../../assets/Icons';
|
||||||
export default class Defuse extends React.Component<{ player: Player }> {
|
const Defuse = ({ player }: { player: Player }) => {
|
||||||
render() {
|
|
||||||
const { player } = this.props;
|
|
||||||
if(!player.state.health || !player.state.defusekit) return '';
|
if(!player.state.health || !player.state.defusekit) return '';
|
||||||
return (
|
return (
|
||||||
<div className={`defuse_indicator`}>
|
<div className={`defuse_indicator`}>
|
||||||
@@ -11,5 +8,4 @@ export default class Defuse extends React.Component<{ player: Player }> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default Defuse;
|
||||||
}
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Weapon from './../Weapon/Weapon';
|
import Weapon from './../Weapon/Weapon';
|
||||||
import flash_assist from './../../assets/flash_assist.png';
|
import flash_assist from './../../assets/flash_assist.png';
|
||||||
|
|
||||||
|
|
||||||
import { C4, Defuse, FlashedKill, Headshot, NoScope, SmokeKill, Suicide, Wallbang } from "./../../assets/Icons"
|
import { C4, Defuse, FlashedKill, Headshot, NoScope, SmokeKill, Suicide, Wallbang } from "./../../assets/Icons"
|
||||||
import { ExtendedKillEvent, BombEvent } from "./Killfeed"
|
import { ExtendedKillEvent, BombEvent } from "./Killfeed"
|
||||||
export default class Kill extends React.Component<{ event: ExtendedKillEvent | BombEvent }> {
|
|
||||||
render() {
|
|
||||||
const { event } = this.props;
|
const Kill = ({event}: { event: ExtendedKillEvent | BombEvent }) => {
|
||||||
if (event.type !== "kill") {
|
if (event.type !== "kill") {
|
||||||
return (
|
return (
|
||||||
<div className={`single_kill`}>
|
<div className={`single_kill`}>
|
||||||
@@ -50,6 +48,6 @@ export default class Kill extends React.Component<{ event: ExtendedKillEvent | B
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
export default Kill;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { GSI } from './../../App';
|
|
||||||
import { KillEvent, Player } from 'csgogsi-socket';
|
import { KillEvent, Player } from 'csgogsi';
|
||||||
import Kill from './Kill';
|
import Kill from './Kill';
|
||||||
import './killfeed.scss';
|
import './killfeed.scss';
|
||||||
|
import { onGSI } from '../../API/contexts/actions';
|
||||||
|
|
||||||
|
|
||||||
export interface ExtendedKillEvent extends KillEvent {
|
export interface ExtendedKillEvent extends KillEvent {
|
||||||
@@ -14,63 +15,24 @@ export interface BombEvent {
|
|||||||
type: 'plant' | 'defuse'
|
type: 'plant' | 'defuse'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Killfeed extends React.Component<any, { events: (BombEvent | ExtendedKillEvent)[] }> {
|
const Killfeed = () => {
|
||||||
|
const [ events, setEvents ] = useState<(BombEvent | ExtendedKillEvent)[]>([]);
|
||||||
constructor(props: any){
|
onGSI("kill", kill => {
|
||||||
super(props);
|
setEvents(ev => [...ev, {...kill, type: 'kill'}]);
|
||||||
this.state = {
|
}, []);
|
||||||
events: []
|
onGSI("data", data => {
|
||||||
}
|
|
||||||
}
|
|
||||||
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(data.round && data.round.phase === "freezetime"){
|
||||||
if(Number(data.phase_countdowns.phase_ends_in) < 10 && this.state.events.length > 0){
|
if(Number(data.phase_countdowns.phase_ends_in) < 10 && events.length > 0){
|
||||||
this.setState({events:[]})
|
setEvents([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
/*
|
|
||||||
GSI.on("bombPlant", player => {
|
|
||||||
this.addBombEvent(player, 'plant');
|
|
||||||
})
|
|
||||||
GSI.on("bombDefuse", player => {
|
|
||||||
this.addBombEvent(player, 'defuse');
|
|
||||||
})
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<div className="killfeed">
|
<div className="killfeed">
|
||||||
{this.state.events.map(event => <Kill event={event}/>)}
|
{events.map(event => <Kill event={event}/>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(Killfeed);
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from "react";
|
import { useState } from "react";
|
||||||
import TeamBox from "./../Players/TeamBox";
|
import TeamBox from "./../Players/TeamBox";
|
||||||
import MatchBar from "../MatchBar/MatchBar";
|
import MatchBar from "../MatchBar/MatchBar";
|
||||||
import SeriesBox from "../MatchBar/SeriesBox";
|
import SeriesBox from "../MatchBar/SeriesBox";
|
||||||
import Observed from "./../Players/Observed";
|
import Observed from "./../Players/Observed";
|
||||||
import { CSGO, Team } from "csgogsi-socket";
|
|
||||||
import { Match } from "../../api/interfaces";
|
|
||||||
import RadarMaps from "./../Radar/RadarMaps";
|
import RadarMaps from "./../Radar/RadarMaps";
|
||||||
import Trivia from "../Trivia/Trivia";
|
import Trivia from "../Trivia/Trivia";
|
||||||
import SideBox from '../SideBoxes/SideBox';
|
import SideBox from '../SideBoxes/SideBox';
|
||||||
import { GSI, actions } from "./../../App";
|
|
||||||
import MoneyBox from '../SideBoxes/Money';
|
import MoneyBox from '../SideBoxes/Money';
|
||||||
import UtilityLevel from '../SideBoxes/UtilityLevel';
|
import UtilityLevel from '../SideBoxes/UtilityLevel';
|
||||||
import Killfeed from "../Killfeed/Killfeed";
|
import Killfeed from "../Killfeed/Killfeed";
|
||||||
@@ -17,66 +14,40 @@ import Overview from "../Overview/Overview";
|
|||||||
import Tournament from "../Tournament/Tournament";
|
import Tournament from "../Tournament/Tournament";
|
||||||
import Pause from "../PauseTimeout/Pause";
|
import Pause from "../PauseTimeout/Pause";
|
||||||
import Timeout from "../PauseTimeout/Timeout";
|
import Timeout from "../PauseTimeout/Timeout";
|
||||||
import PlayerCamera from "../Camera/Camera";
|
import { CSGO } from "csgogsi";
|
||||||
|
import { Match } from "../../API/types";
|
||||||
|
import { useAction } from "../../API/contexts/actions";
|
||||||
|
import { Scout } from "../Scout";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: CSGO,
|
game: CSGO,
|
||||||
match: Match | null
|
match: Match | null
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
interface State {
|
interface State {
|
||||||
winner: Team | null,
|
winner: Team | null,
|
||||||
showWin: boolean,
|
showWin: boolean,
|
||||||
forceHide: boolean
|
forceHide: boolean
|
||||||
}
|
}*/
|
||||||
|
|
||||||
export default class Layout extends React.Component<Props, State> {
|
const Layout = ({game,match}: Props) => {
|
||||||
constructor(props: Props) {
|
const [ forceHide, setForceHide ] = useState(false);
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
winner: null,
|
|
||||||
showWin: false,
|
|
||||||
forceHide: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useAction('boxesState', (state) => {
|
||||||
GSI.on('roundEnd', score => {
|
console.log("UPDATE STATE UMC", state);
|
||||||
this.setState({ winner: score.winner, showWin: true }, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.setState({ showWin: false })
|
|
||||||
}, 4000)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
actions.on("boxesState", (state: string) => {
|
|
||||||
if (state === "show") {
|
if (state === "show") {
|
||||||
this.setState({ forceHide: false });
|
setForceHide(false);
|
||||||
} else if (state === "hide") {
|
} else if (state === "hide") {
|
||||||
this.setState({ forceHide: true });
|
setForceHide(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 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 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 leftPlayers = game.players.filter(player => player.team.side === left.side);
|
||||||
const rightPlayers = game.players.filter(player => player.team.side === right.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 isFreezetime = (game.round && game.round.phase === "freezetime") || game.phase_countdowns.phase === "freezetime";
|
||||||
const { forceHide } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layout">
|
<div className="layout">
|
||||||
<div className={`players_alive`}>
|
<div className={`players_alive`}>
|
||||||
@@ -93,17 +64,17 @@ export default class Layout extends React.Component<Props, State> {
|
|||||||
<MatchBar map={game.map} phase={game.phase_countdowns} bomb={game.bomb} match={match} />
|
<MatchBar map={game.map} phase={game.phase_countdowns} bomb={game.bomb} match={match} />
|
||||||
<Pause phase={game.phase_countdowns}/>
|
<Pause phase={game.phase_countdowns}/>
|
||||||
<Timeout map={game.map} phase={game.phase_countdowns} />
|
<Timeout map={game.map} phase={game.phase_countdowns} />
|
||||||
<SeriesBox map={game.map} phase={game.phase_countdowns} match={match} />
|
<SeriesBox map={game.map} match={match} />
|
||||||
|
|
||||||
<Tournament />
|
<Tournament />
|
||||||
|
|
||||||
<Observed player={game.player} veto={this.getVeto()} round={game.map.round+1}/>
|
<Observed player={game.player} />
|
||||||
|
|
||||||
<TeamBox team={left} players={leftPlayers} side="left" current={game.player} />
|
<TeamBox team={left} players={leftPlayers} side="left" current={game.player} />
|
||||||
<TeamBox team={right} players={rightPlayers} side="right" current={game.player} />
|
<TeamBox team={right} players={rightPlayers} side="right" current={game.player} />
|
||||||
|
|
||||||
<Trivia />
|
<Trivia />
|
||||||
|
<Scout left={left.side} right={right.side} />
|
||||||
<MapSeries teams={[left, right]} match={match} isFreezetime={isFreezetime} map={game.map} />
|
<MapSeries teams={[left, right]} match={match} isFreezetime={isFreezetime} map={game.map} />
|
||||||
<div className={"boxes left"}>
|
<div className={"boxes left"}>
|
||||||
<UtilityLevel side={left.side} players={game.players} show={isFreezetime && !forceHide} />
|
<UtilityLevel side={left.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||||
@@ -132,4 +103,4 @@ export default class Layout extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
export default Layout;
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from "react";
|
import * as I from "csgogsi";
|
||||||
import * as I from "csgogsi-socket";
|
|
||||||
import { Match, Veto } from '../../api/interfaces';
|
|
||||||
import TeamLogo from "../MatchBar/TeamLogo";
|
import TeamLogo from "../MatchBar/TeamLogo";
|
||||||
import "./mapseries.scss";
|
import "./mapseries.scss";
|
||||||
|
import { Match, Veto } from "../../API/types";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
match: Match | null;
|
match: Match | null;
|
||||||
teams: I.Team[];
|
teams: I.Team[];
|
||||||
isFreezetime: boolean;
|
isFreezetime: boolean;
|
||||||
map: I.Map
|
map: I.Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IVetoProps {
|
interface IVetoProps {
|
||||||
@@ -17,46 +16,51 @@ interface IVetoProps {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VetoEntry extends React.Component<IVetoProps> {
|
const VetoEntry = ({ veto, teams, active }: IVetoProps) => {
|
||||||
render(){
|
return (
|
||||||
const { veto, teams, active } = this.props;
|
<div className={`veto_container ${active ? "active" : ""}`}>
|
||||||
return <div className={`veto_container ${active ? 'active' : ''}`}>
|
|
||||||
<div className="veto_map_name">
|
<div className="veto_map_name">
|
||||||
{veto.mapName}
|
{veto.mapName}
|
||||||
</div>
|
</div>
|
||||||
<div className="veto_picker">
|
<div className="veto_picker">
|
||||||
<TeamLogo team={teams.filter(team => team.id === veto.teamId)[0]} />
|
<TeamLogo team={teams.filter((team) => team.id === veto.teamId)[0]} />
|
||||||
</div>
|
</div>
|
||||||
<div className="veto_winner">
|
<div className="veto_winner">
|
||||||
<TeamLogo team={teams.filter(team => team.id === veto.winner)[0]} />
|
<TeamLogo team={teams.filter((team) => team.id === veto.winner)[0]} />
|
||||||
</div>
|
</div>
|
||||||
<div className="veto_score">
|
<div className="veto_score">
|
||||||
{Object.values((veto.score || ['-','-'])).sort().join(":")}
|
{Object.values(veto.score || ["-", "-"]).sort().join(":")}
|
||||||
</div>
|
</div>
|
||||||
<div className='active_container'>
|
<div className="active_container">
|
||||||
<div className='active'>Currently playing</div>
|
<div className="active">Currently playing</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default class MapSeries extends React.Component<IProps> {
|
const MapSeries = ({ match, teams, isFreezetime, map }: IProps) => {
|
||||||
|
|
||||||
render() {
|
|
||||||
const { match, teams, isFreezetime, map } = this.props;
|
|
||||||
if (!match || !match.vetos.length) return null;
|
if (!match || !match.vetos.length) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`map_series_container ${isFreezetime ? 'show': 'hide'}`}>
|
<div className={`map_series_container ${isFreezetime ? "show" : "hide"}`}>
|
||||||
<div className="title_bar">
|
<div className="title_bar">
|
||||||
<div className="picked">Picked</div>
|
<div className="picked">Picked</div>
|
||||||
<div className="winner">Winner</div>
|
<div className="winner">Winner</div>
|
||||||
<div className="score">Score</div>
|
<div className="score">Score</div>
|
||||||
</div>
|
</div>
|
||||||
{match.vetos.filter(veto => veto.type !== "ban").map(veto => {
|
{match.vetos.filter((veto) =>
|
||||||
|
veto.type !== "ban"
|
||||||
|
).map((veto) => {
|
||||||
if (!veto.mapName) return null;
|
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)}/>
|
return (
|
||||||
|
<VetoEntry
|
||||||
|
key={`${match.id}${veto.mapName}${veto.teamId}${veto.side}`}
|
||||||
|
veto={veto}
|
||||||
|
teams={teams}
|
||||||
|
active={map.name.includes(veto.mapName)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
export default MapSeries;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React from "react";
|
import * as I from "csgogsi";
|
||||||
import * as I from "csgogsi-socket";
|
|
||||||
import "./matchbar.scss";
|
import "./matchbar.scss";
|
||||||
import TeamScore from "./TeamScore";
|
import TeamScore from "./TeamScore";
|
||||||
import Bomb from "./../Timers/BombTimer";
|
import Bomb from "./../Timers/BombTimer";
|
||||||
import Countdown from "./../Timers/Countdown";
|
import { useBombTimer } from "./../Timers/Countdown";
|
||||||
import { GSI } from "../../App";
|
import { Match } from './../../API/types';
|
||||||
import { Match } from "../../api/interfaces";
|
|
||||||
|
|
||||||
function stringToClock(time: string | number, pad = true) {
|
function stringToClock(time: string | number, pad = true) {
|
||||||
if (typeof time === "string") {
|
if (typeof time === "string") {
|
||||||
@@ -23,142 +22,19 @@ function stringToClock(time: string | number, pad = true) {
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
match: Match | null;
|
match: Match | null;
|
||||||
map: I.Map;
|
map: I.Map;
|
||||||
phase: I.PhaseRaw,
|
phase: I.CSGO["phase_countdowns"],
|
||||||
bomb: I.Bomb | null,
|
bomb: I.Bomb | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Timer {
|
export interface Timer {
|
||||||
width: number;
|
time: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
countdown: number;
|
|
||||||
side: "left"|"right";
|
side: "left"|"right";
|
||||||
type: "defusing" | "planting";
|
type: "defusing" | "planting";
|
||||||
player: I.Player | null;
|
player: I.Player | null;
|
||||||
}
|
}
|
||||||
|
const getRoundLabel = (mapRound: number) => {
|
||||||
interface IState {
|
const round = mapRound + 1;
|
||||||
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 <= 24) {
|
if (round <= 24) {
|
||||||
return `Round ${round}/24`;
|
return `Round ${round}/24`;
|
||||||
}
|
}
|
||||||
@@ -166,38 +42,34 @@ export default class TeamBox extends React.Component<IProps, IState> {
|
|||||||
const OT = Math.ceil(additionalRounds/6);
|
const OT = Math.ceil(additionalRounds/6);
|
||||||
return `OT ${OT} (${additionalRounds - (OT - 1)*6}/6)`;
|
return `OT ${OT} (${additionalRounds - (OT - 1)*6}/6)`;
|
||||||
}
|
}
|
||||||
render() {
|
|
||||||
const { defusing, planting, winState } = this.state;
|
const Matchbar = (props: IProps) => {
|
||||||
const { bomb, match, map, phase } = this.props;
|
const { bomb, match, map, phase } = props;
|
||||||
const time = stringToClock(phase.phase_ends_in);
|
const time = stringToClock(phase.phase_ends_in);
|
||||||
const left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
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 right = map.team_ct.orientation === "left" ? map.team_t : map.team_ct;
|
||||||
const isPlanted = bomb && (bomb.state === "defusing" || bomb.state === "planted");
|
const isPlanted = bomb && (bomb.state === "defusing" || bomb.state === "planted");
|
||||||
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
||||||
let leftTimer: Timer | null = null, rightTimer: Timer | null = null;
|
|
||||||
if(defusing.active || planting.active){
|
const bombData = useBombTimer();
|
||||||
if(defusing.active){
|
const plantTimer: Timer | null = bombData.state === "planting" ? { time:bombData.plantTime, active: true, side: bombData.player?.team.orientation || "right", player: bombData.player, type: "planting"} : null;
|
||||||
if(defusing.side === "left") leftTimer = defusing;
|
const defuseTimer: Timer | null = bombData.state === "defusing" ? { time:bombData.defuseTime, active: true, side: bombData.player?.team.orientation || "left", player: bombData.player, type: "defusing"} : null;
|
||||||
else rightTimer = defusing;
|
|
||||||
} else {
|
|
||||||
if(planting.side === "left") leftTimer = planting;
|
|
||||||
else rightTimer = planting;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id={`matchbar`}>
|
<div id={`matchbar`}>
|
||||||
<TeamScore team={left} orientation={"left"} timer={leftTimer} showWin={winState.show && winState.side === "left"} />
|
<TeamScore team={left} orientation={"left"} timer={left.side === "CT" ? defuseTimer : plantTimer}/>
|
||||||
<div className={`score left ${left.side}`}>{left.score}</div>
|
<div className={`score left ${left.side}`}>{left.score}</div>
|
||||||
<div id="timer" className={bo === 0 ? 'no-bo' : ''}>
|
<div id="timer" className={bo === 0 ? 'no-bo' : ''}>
|
||||||
<div id={`round_timer_text`} className={isPlanted ? "hide":""}>{time}</div>
|
<div id={`round_timer_text`} className={isPlanted ? "hide":""}>{time}</div>
|
||||||
<div id="round_now" className={isPlanted ? "hide":""}>{this.getRoundLabel()}</div>
|
<div id="round_now" className={isPlanted ? "hide":""}>{getRoundLabel(map.round)}</div>
|
||||||
<Bomb />
|
<Bomb />
|
||||||
</div>
|
</div>
|
||||||
<div className={`score right ${right.side}`}>{right.score}</div>
|
<div className={`score right ${right.side}`}>{right.score}</div>
|
||||||
<TeamScore team={right} orientation={"right"} timer={rightTimer} showWin={winState.show && winState.side === "right"} />
|
<TeamScore team={right} orientation={"right"} timer={right.side === "CT" ? defuseTimer : plantTimer} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default Matchbar;
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import React from "react";
|
import * as I from "csgogsi";
|
||||||
import * as I from "csgogsi-socket";
|
import { Match } from "../../API/types";
|
||||||
import { Match } from "../../api/interfaces";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
map: I.Map;
|
map: I.Map;
|
||||||
phase: I.PhaseRaw;
|
|
||||||
match: Match | null;
|
match: Match | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SeriesBox extends React.Component<Props> {
|
const SeriesBox = ({ map, match }: Props) => {
|
||||||
render() {
|
|
||||||
const { match, map } = this.props;
|
|
||||||
const amountOfMaps = (match && Math.floor(Number(match.matchType.substr(-1)) / 2) + 1) || 0;
|
const amountOfMaps = (match && Math.floor(Number(match.matchType.substr(-1)) / 2) + 1) || 0;
|
||||||
const bo = (match && Number(match.matchType.substr(-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 left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
||||||
@@ -41,4 +37,5 @@ export default class SeriesBox extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default SeriesBox;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import { Team } from 'csgogsi';
|
||||||
import { Team } from 'csgogsi-socket';
|
import * as I from '../../API/types';
|
||||||
import * as I from '../../api/interfaces';
|
import { apiUrl } from './../../API';
|
||||||
import { apiUrl } from './../../api/api';
|
import { LogoCT, LogoT } from './../../assets/Icons';
|
||||||
|
|
||||||
export default class TeamLogo extends React.Component<{ team?: Team | I.Team | null, height?: number, width?: number}> {
|
const TeamLogo = ({team, height, width }: { team?: Team | I.Team | null, height?: number, width?: number}) => {
|
||||||
render(){
|
|
||||||
const { team } = this.props;
|
|
||||||
if(!team) return null;
|
if(!team) return null;
|
||||||
let id = '';
|
let id = '';
|
||||||
const { logo } = team;
|
const { logo } = team;
|
||||||
@@ -16,9 +14,10 @@ export default class TeamLogo extends React.Component<{ team?: Team | I.Team | n
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={`logo`}>
|
<div className={`logo`}>
|
||||||
{ logo && id ? <img src={`${apiUrl}api/teams/logo/${id}`} width={this.props.width} height={this.props.height} alt={'Team logo'} /> : ''}
|
<img src={logo && id ? `${apiUrl}api/teams/logo/${id}` : ('side' in team && team.side === "CT" ? LogoCT : LogoT)} width={width} height={height} alt={'Team logo'} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default TeamLogo;
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
import React from "react";
|
import * as I from "csgogsi";
|
||||||
import * as I from "csgogsi-socket";
|
|
||||||
import WinIndicator from "./WinIndicator";
|
|
||||||
import { Timer } from "./MatchBar";
|
import { Timer } from "./MatchBar";
|
||||||
import TeamLogo from './TeamLogo';
|
import TeamLogo from './TeamLogo';
|
||||||
import PlantDefuse from "../Timers/PlantDefuse"
|
import PlantDefuse from "../Timers/PlantDefuse"
|
||||||
|
import { onGSI } from "../../API/contexts/actions";
|
||||||
|
import WinAnnouncement from "./WinIndicator";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
team: I.Team;
|
|
||||||
orientation: "left" | "right";
|
orientation: "left" | "right";
|
||||||
timer: Timer | null;
|
timer: Timer | null;
|
||||||
showWin: boolean;
|
team: I.Team;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TeamScore extends React.Component<IProps> {
|
const TeamScore = ({orientation, timer, team }: IProps) => {
|
||||||
render() {
|
const [ show, setShow ] = useState(false);
|
||||||
const { orientation, timer, team, showWin } = this.props;
|
|
||||||
|
onGSI("roundEnd", result => {
|
||||||
|
if(result.winner.orientation !== orientation) return;
|
||||||
|
setShow(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setShow(false);
|
||||||
|
}, 5000);
|
||||||
|
}, [orientation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`team ${orientation} ${team.side}`}>
|
<div className={`team ${orientation} ${team.side || ''}`}>
|
||||||
<div className="team-name">{team.name}</div>
|
<div className="team-name">{team?.name || null}</div>
|
||||||
<TeamLogo team={team} />
|
<TeamLogo team={team} />
|
||||||
<div className="round-thingy"><div className="inner"></div></div>
|
<div className="round-thingy"><div className="inner"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<PlantDefuse timer={timer} side={orientation} />
|
<PlantDefuse timer={timer} side={orientation} />
|
||||||
<WinIndicator team={team} show={showWin}/>
|
<WinAnnouncement team={team} show={show} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default TeamScore;
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react';
|
import { Team } from 'csgogsi';
|
||||||
import { Team } from 'csgogsi-socket';
|
|
||||||
|
|
||||||
export default class WinAnnouncement extends React.Component<{ team: Team | null, show: boolean }> {
|
const WinAnnouncement = ({team, show }: { team: Team | null, show: boolean }) => {
|
||||||
render() {
|
|
||||||
const { team, show } = this.props;
|
|
||||||
if(!team) return null;
|
if(!team) return null;
|
||||||
return <div className={`win_text ${show ? 'show' : ''} ${team.orientation} ${team.side}`}>
|
return <div className={`win_text ${show ? 'show' : ''} ${team.orientation} ${team.side}`}>
|
||||||
WINS THE ROUND!
|
WINS THE ROUND!
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
export default WinAnnouncement;
|
||||||
@@ -1,22 +1,20 @@
|
|||||||
import React from 'react';
|
import * as I from "../../API/types";
|
||||||
import * as I from '../../api/interfaces';
|
import "./index.scss";
|
||||||
import TeamLogo from '../MatchBar/TeamLogo';
|
import TeamLogo from "../MatchBar/TeamLogo";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
match: I.Match,
|
match: I.Match;
|
||||||
show: boolean,
|
show: boolean;
|
||||||
teams: I.Team[],
|
teams: I.Team[];
|
||||||
veto: I.Veto | null
|
veto: I.Veto | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MatchOverview extends React.Component<IProps> {
|
const MatchOverview = ({ match, teams, show }: IProps) => {
|
||||||
render() {
|
const left = teams.find((team) => team._id === match.left.id);
|
||||||
const { match, teams, show } = this.props;
|
const right = teams.find((team) => team._id === match.right.id);
|
||||||
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;
|
if (!match || !left || !right) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`match-overview ${show ? 'show':''}`}>
|
<div className={`match-overview ${show ? "show" : ""}`}>
|
||||||
<div className="match-overview-title">
|
<div className="match-overview-title">
|
||||||
Upcoming match
|
Upcoming match
|
||||||
</div>
|
</div>
|
||||||
@@ -25,17 +23,17 @@ export default class MatchOverview extends React.Component<IProps> {
|
|||||||
<div className="match-overview-team-logo">
|
<div className="match-overview-team-logo">
|
||||||
<TeamLogo team={left} height={40} />
|
<TeamLogo team={left} height={40} />
|
||||||
</div>
|
</div>
|
||||||
<div className="match-overview-team-name">{left.name}</div>
|
<div className="match-overview-team-name">{(left.shortName || left.name).substring(0, 4)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="match-overview-vs">vs</div>
|
<div className="match-overview-vs">vs</div>
|
||||||
<div className="match-overview-team">
|
<div className="match-overview-team">
|
||||||
<div className="match-overview-team-logo">
|
<div className="match-overview-team-logo">
|
||||||
<TeamLogo team={right} height={40} />
|
<TeamLogo team={right} height={40} />
|
||||||
</div>
|
</div>
|
||||||
<div className="match-overview-team-name">{right.name}</div>
|
<div className="match-overview-team-name">{(right.shortName || right.name).substring(0, 4)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
export default MatchOverview;
|
||||||
|
|||||||
@@ -1,27 +1,10 @@
|
|||||||
import React from 'react';
|
import { useState } from 'react';
|
||||||
import { actions, configs } from '../../App';
|
|
||||||
import * as I from '../../api/interfaces';
|
|
||||||
import PlayerOverview from '../PlayerOverview/PlayerOverview';
|
import PlayerOverview from '../PlayerOverview/PlayerOverview';
|
||||||
import MatchOverview from '../MatchOverview/MatchOverview';
|
import MatchOverview from '../MatchOverview/MatchOverview';
|
||||||
import TeamOverview from '../TeamOverview/TeamOverview';
|
import { Map, Player } from 'csgogsi';
|
||||||
import { Map, Player } from 'csgogsi-socket';
|
import * as I from './../../API/types';
|
||||||
import api from '../../api/api';
|
import api from './../../API';
|
||||||
|
import { useConfig, useOnConfigChange } from '../../API/contexts/actions';
|
||||||
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 {
|
interface IProps {
|
||||||
match: I.Match | null,
|
match: I.Match | null,
|
||||||
@@ -29,97 +12,30 @@ interface IProps {
|
|||||||
players: Player[]
|
players: Player[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Overview extends React.Component<IProps, IState> {
|
const Overview = ({ match, map, players }: IProps) => {
|
||||||
constructor(props: IProps){
|
const [ teams, setTeams ] = useState<I.Team[]>([]);
|
||||||
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 mapName = map.name.substring(map.name.lastIndexOf('/')+1);
|
||||||
const veto = match.vetos.find(veto => veto.mapName === mapName);
|
|
||||||
if(!veto) return null;
|
const previewData = useConfig("preview_settings");
|
||||||
return veto;
|
|
||||||
}
|
useOnConfigChange('preview_settings', async data => {
|
||||||
renderPlayer = () => {
|
console.log(data);
|
||||||
const { player } = this.state;
|
if(!data?.match_preview?.match?.left.id || !data?.match_preview?.match?.right.id) return;
|
||||||
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()} />
|
const teams = await Promise.all([api.teams.getOne(data?.match_preview?.match?.left.id), api.teams.getOne(data?.match_preview?.match?.right.id)]);
|
||||||
}
|
if(!teams[0] || !teams[1]) return;
|
||||||
renderMatch = () => {
|
|
||||||
const { match } = this.state;
|
setTeams(teams);
|
||||||
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}/>
|
|
||||||
}
|
const playerData = previewData?.player_preview?.player;
|
||||||
renderTeam = () => {
|
const matchData = previewData?.match_preview?.match;
|
||||||
const { team } = this.state;
|
|
||||||
if(!team.data) return null;
|
const veto = match?.vetos.find(veto => veto.mapName === mapName) || null;
|
||||||
return <TeamOverview team={team.data} show={team.show} veto={this.getVeto()} />
|
return (<>
|
||||||
}
|
{ playerData ? <PlayerOverview round={map.round + 1} player={playerData} players={players} show={!!previewData.player_preview_toggle} veto={veto} /> : null}
|
||||||
render() {
|
{ matchData && teams[0] && teams[1] ? <MatchOverview match={matchData} veto={veto} teams={teams} show={!!previewData.match_preview_toggle} /> : null }
|
||||||
return (
|
</>)
|
||||||
<>
|
|
||||||
{this.renderPlayer()}
|
|
||||||
{this.renderMatch()}
|
|
||||||
{this.renderTeam()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Overview;
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import React from "react";
|
import { CSGO } from "csgogsi";
|
||||||
import { PhaseRaw } from "csgogsi-socket";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
phase: PhaseRaw | null
|
phase: CSGO["phase_countdowns"] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Pause extends React.Component<IProps> {
|
const Pause = ({ phase }: IProps) => {
|
||||||
render() {
|
|
||||||
const { phase } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<div id={`pause`} className={phase && phase.phase === "paused" ? 'show' : ''}>
|
<div
|
||||||
|
id={`pause`}
|
||||||
|
className={phase && phase.phase === "paused" ? "show" : ""}
|
||||||
|
>
|
||||||
PAUSE
|
PAUSE
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
export default Pause;
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import React from "react";
|
import { Map, CSGO } from "csgogsi";
|
||||||
import { Map, PhaseRaw } from "csgogsi-socket";
|
|
||||||
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
phase: PhaseRaw | null,
|
phase: CSGO["phase_countdowns"] | null;
|
||||||
map: Map
|
map: Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Timeout extends React.Component<IProps> {
|
const Timeout = ({ phase, map }: IProps) => {
|
||||||
render() {
|
const time = phase && Math.abs(Math.ceil(phase.phase_ends_in));
|
||||||
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;
|
const team = phase && phase.phase === "timeout_t" ? map.team_t : map.team_ct;
|
||||||
|
|
||||||
return (
|
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): ''}`}>
|
<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.substring(8)
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{team.name} TIMEOUT
|
{team.name} TIMEOUT
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
export default Timeout;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import React from 'react';
|
import * as I from '../../API/types';
|
||||||
import * as I from '../../api/interfaces';
|
|
||||||
import { avatars } from './../../api/avatars';
|
|
||||||
import { apiUrl } from '../../api/api';
|
|
||||||
import { getCountry } from '../countries';
|
import { getCountry } from '../countries';
|
||||||
import { Player } from 'csgogsi-socket';
|
import { Player } from 'csgogsi';
|
||||||
import "./playeroverview.scss";
|
import "./playeroverview.scss";
|
||||||
|
import { apiUrl } from '../../API';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
player: I.Player,
|
player: I.Player,
|
||||||
@@ -14,32 +12,9 @@ interface IProps {
|
|||||||
round: number
|
round: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PlayerOverview extends React.Component<IProps> {
|
const sum = (data: number[]) => data.reduce((a, b) => a + b, 0);
|
||||||
sum = (data: number[]) => data.reduce((a, b) => a + b, 0);
|
|
||||||
|
|
||||||
getData = () => {
|
const calcWidth = (val: number | string, max?: number) => {
|
||||||
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);
|
const value = Number(val);
|
||||||
if(value === 0) return 0;
|
if(value === 0) return 0;
|
||||||
let maximum = max;
|
let maximum = max;
|
||||||
@@ -51,22 +26,41 @@ export default class PlayerOverview extends React.Component<IProps> {
|
|||||||
}
|
}
|
||||||
return 100*value/maximum;
|
return 100*value/maximum;
|
||||||
}
|
}
|
||||||
render() {
|
|
||||||
const { player, veto, players } = this.props;
|
const PlayerOverview = ({ player, show, veto, players, round }: IProps) => {
|
||||||
const data = this.getData();
|
if(!player || !veto || !veto.rounds) return null;
|
||||||
if(!player || !veto || !veto.rounds || !data) return null;
|
const getData = () => {
|
||||||
let url = null;
|
if(!veto.rounds) return null;
|
||||||
// const avatarData = avatars.find(avatar => avatar.steamid === player.steamid);
|
const stats = veto.rounds.map(round => round ? round.players[player.steamid] : {
|
||||||
const avatarData = avatars[player.steamid];
|
kills: 0,
|
||||||
if(avatarData && avatarData.url){
|
killshs: 0,
|
||||||
url = avatarData.url;
|
damage: 0
|
||||||
|
}).filter(data => !!data);
|
||||||
|
const overall = {
|
||||||
|
damage: sum(stats.map(round => round.damage)),
|
||||||
|
kills: sum(stats.map(round => round.kills)),
|
||||||
|
killshs: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getData();
|
||||||
|
if(!data) return null;
|
||||||
|
const url = player.avatar;
|
||||||
|
|
||||||
const countryName = player.country ? getCountry(player.country) : null;
|
const countryName = player.country ? getCountry(player.country) : null;
|
||||||
let side = '';
|
let side = '';
|
||||||
const inGamePlayer = players.find(inGamePlayer => inGamePlayer.steamid === player.steamid);
|
const inGamePlayer = players.find(inGamePlayer => inGamePlayer.steamid === player.steamid);
|
||||||
if(inGamePlayer) side = inGamePlayer.team.side;
|
if(inGamePlayer) side = inGamePlayer.team.side;
|
||||||
return (
|
return (
|
||||||
<div className={`player-overview ${this.props.show ? 'show':''} ${side}`}>
|
<div className={`player-overview ${show ? 'show':''} ${side}`}>
|
||||||
<div className="player-overview-picture">
|
<div className="player-overview-picture">
|
||||||
{url ? <img src={url} alt={`${player.username}'s avatar`}/> : null}
|
{url ? <img src={url} alt={`${player.username}'s avatar`}/> : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -76,29 +70,31 @@ export default class PlayerOverview extends React.Component<IProps> {
|
|||||||
<div className="player-overview-stat">
|
<div className="player-overview-stat">
|
||||||
<div className="label">KILLS: {data.kills}</div>
|
<div className="label">KILLS: {data.kills}</div>
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="filling" style={{width:`${this.calcWidth(data.kills, data.kills <= 30 ? 30 : 40)}%`}}></div>
|
<div className="filling" style={{width:`${calcWidth(data.kills, data.kills <= 30 ? 30 : 40)}%`}}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="player-overview-stat">
|
<div className="player-overview-stat">
|
||||||
<div className="label">HS: {data.hsp}%</div>
|
<div className="label">HS: {data.hsp}%</div>
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="filling" style={{width:`${this.calcWidth(data.hsp, 100)}%`}}></div>
|
<div className="filling" style={{width:`${calcWidth(data.hsp, 100)}%`}}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="player-overview-stat">
|
<div className="player-overview-stat">
|
||||||
<div className="label">ADR: {data.adr}</div>
|
<div className="label">ADR: {data.adr}</div>
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="filling" style={{width:`${this.calcWidth(data.adr)}%`}}></div>
|
<div className="filling" style={{width:`${calcWidth(data.adr)}%`}}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="player-overview-stat">
|
<div className="player-overview-stat">
|
||||||
<div className="label">KPR: {data.kpr}</div>
|
<div className="label">KPR: {data.kpr}</div>
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="filling" style={{width:`${this.calcWidth(Number(data.kpr)*100)}%`}}></div>
|
<div className="filling" style={{width:`${calcWidth(Number(data.kpr)*100)}%`}}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default PlayerOverview;
|
||||||
@@ -1,70 +1,50 @@
|
|||||||
import React from 'react';
|
import CameraContainer from "../Camera/Container";
|
||||||
import CameraContainer from '../Camera/Container';
|
|
||||||
import PlayerCamera from "./../Camera/Camera";
|
import PlayerCamera from "./../Camera/Camera";
|
||||||
|
|
||||||
import { avatars } from './../../api/avatars';
|
import { Skull } from "./../../assets/Icons";
|
||||||
|
import { useConfig } from "../../API/contexts/actions";
|
||||||
import { Skull } from './../../assets/Icons';
|
import { apiUrl } from "../../API";
|
||||||
import { configs } from '../../App';
|
|
||||||
import { apiUrl } from '../../api/api';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
steamid: string,
|
steamid: string;
|
||||||
teamId?: string | null,
|
url: string | null;
|
||||||
slot?: number,
|
slot?: number;
|
||||||
height?: number,
|
height?: number;
|
||||||
width?: number,
|
width?: number;
|
||||||
showSkull?: boolean,
|
showSkull?: boolean;
|
||||||
showCam?: boolean,
|
showCam?: boolean;
|
||||||
sidePlayer?: boolean
|
sidePlayer?: boolean;
|
||||||
}
|
teamId?: string | null
|
||||||
export default class Avatar extends React.Component<IProps, { replaceAvatar: 'never' | 'if_missing' | 'always' }> {
|
|
||||||
constructor(props: IProps){
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
replaceAvatar: 'never'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
const onDataChange = (data:any) => {
|
|
||||||
if(!data) return;
|
|
||||||
const display = data.display_settings;
|
|
||||||
if(!display) return;
|
|
||||||
this.setState({
|
|
||||||
replaceAvatar: display.replace_avatars || 'never'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
configs.onChange(onDataChange);
|
|
||||||
onDataChange(configs.data);
|
|
||||||
}
|
|
||||||
getAvatarUrl = () => {
|
|
||||||
const avatarData = avatars[this.props.steamid] && avatars[this.props.steamid].url ? avatars[this.props.steamid].url : null;
|
|
||||||
|
|
||||||
if(this.state.replaceAvatar === 'always' || (this.state.replaceAvatar === 'if_missing' && !avatarData)){
|
|
||||||
return this.props.teamId ? `${apiUrl}api/teams/logo/${this.props.teamId}` : avatarData || null;
|
|
||||||
}
|
|
||||||
return avatarData || null;
|
|
||||||
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const { showCam, steamid, width, height, showSkull, sidePlayer } = this.props;
|
|
||||||
//const url = avatars.filter(avatar => avatar.steamid === this.props.steamid)[0];
|
|
||||||
const avatarUrl = this.getAvatarUrl();
|
|
||||||
if (!avatarUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
const Avatar = (
|
||||||
|
{ steamid, url, height, width, showCam, showSkull, sidePlayer, teamId }: IProps,
|
||||||
|
) => {
|
||||||
|
const data = useConfig("display_settings");
|
||||||
|
|
||||||
|
const avatarUrl = teamId && (data?.replace_avatars === "always" || (data?.replace_avatars === "if_missing" && !url)) ? `${apiUrl}api/teams/logo/${teamId}` : url;
|
||||||
|
if(!avatarUrl && !showCam) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`avatar`}>
|
<div className={`avatar`}>
|
||||||
{
|
{showCam
|
||||||
showCam ? ( sidePlayer ? <div className="videofeed"><PlayerCamera steamid={steamid} visible={true} /></div> : <CameraContainer observedSteamid={steamid} />) : null
|
? (sidePlayer
|
||||||
}
|
? (
|
||||||
{
|
<div className="videofeed">
|
||||||
showSkull ? <Skull height={height} width={width} /> : <img src={avatarUrl} height={height} width={width} alt={'Avatar'} />
|
<PlayerCamera steamid={steamid} visible={true} />
|
||||||
}
|
</div>
|
||||||
|
)
|
||||||
|
: <CameraContainer observedSteamid={steamid} />)
|
||||||
|
: null}
|
||||||
|
{showSkull
|
||||||
|
? <Skull height={height} width={width} />
|
||||||
|
: (
|
||||||
|
avatarUrl ? <img
|
||||||
|
src={avatarUrl}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
alt={"Avatar"}
|
||||||
|
/> : null
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default Avatar;
|
||||||
}
|
|
||||||
@@ -1,64 +1,43 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Player } from "csgogsi-socket";
|
import { Player } from "csgogsi";
|
||||||
import Weapon from "./../Weapon/Weapon";
|
import Weapon from "./../Weapon/Weapon";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
import TeamLogo from "./../MatchBar/TeamLogo";
|
import TeamLogo from "./../MatchBar/TeamLogo";
|
||||||
import "./observed.scss";
|
import "./observed.scss";
|
||||||
import { apiUrl } from './../../api/api';
|
|
||||||
import { getCountry } from "./../countries";
|
import { getCountry } from "./../countries";
|
||||||
import { ArmorHelmet, ArmorFull, HealthFull, Bullets } from './../../assets/Icons';
|
import { ArmorHelmet, ArmorFull, HealthFull, Bullets } from './../../assets/Icons';
|
||||||
import { Veto } from "../../api/interfaces";
|
import { apiUrl } from './../../API';
|
||||||
import { actions } from "../../App";
|
import { useAction } from "../../API/contexts/actions";
|
||||||
|
|
||||||
class Statistic extends React.PureComponent<{ label: string; value: string | number, }> {
|
|
||||||
render() {
|
const Statistic = React.memo(({ label, value }: { label: string; value: string | number, }) => {
|
||||||
return (
|
return (
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="label">{this.props.label}</div>
|
<div className="label">{label}</div>
|
||||||
<div className="value">{this.props.value}</div>
|
<div className="value">{value}</div>
|
||||||
</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);
|
const Observed = ({ player }: { player: Player | null }) => {
|
||||||
}
|
const [ showCam, setShowCam ] = useState(true);
|
||||||
render() {
|
|
||||||
if (!this.props.player) return '';
|
useAction('toggleCams', () => {
|
||||||
const { player } = this.props;
|
setShowCam(p => !p);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!player) return null;
|
||||||
|
|
||||||
const country = player.country || player.team.country;
|
const country = player.country || player.team.country;
|
||||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
const currentWeapon = player.weapons.filter(weapon => weapon.state === "active")[0];
|
||||||
const currentWeapon = weapons.filter(weapon => weapon.state === "active")[0];
|
const grenades = player.weapons.filter(weapon => weapon.type === "Grenade");
|
||||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
|
||||||
const { stats } = player;
|
const { stats } = player;
|
||||||
const ratio = stats.deaths === 0 ? stats.kills : stats.kills / stats.deaths;
|
const ratio = stats.deaths === 0 ? stats.kills : stats.kills / stats.deaths;
|
||||||
const countryName = country ? getCountry(country) : null;
|
const countryName = country ? getCountry(country) : null;
|
||||||
return (
|
return (
|
||||||
<div className={`observed ${player.team.side}`}>
|
<div className={`observed ${player.team.side}`}>
|
||||||
<div className="main_row">
|
<div className="main_row">
|
||||||
{<Avatar teamId={player.team.id} steamid={player.steamid} height={140} width={140} showCam={this.state.showCam} slot={player.observer_slot} />}
|
<Avatar teamId={player.team.id} url={player.avatar} steamid={player.steamid} height={140} width={140} showCam={showCam} slot={player.observer_slot} />
|
||||||
<TeamLogo team={player.team} height={35} width={35} />
|
<TeamLogo team={player.team} height={35} width={35} />
|
||||||
<div className="username_container">
|
<div className="username_container">
|
||||||
<div className="username">{player.name}</div>
|
<div className="username">{player.name}</div>
|
||||||
@@ -103,4 +82,5 @@ export default class Observed extends React.Component<{ player: Player | null, v
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default Observed;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import * as I from "csgogsi";
|
||||||
import * as I from "csgogsi-socket";
|
|
||||||
import Weapon from "./../Weapon/Weapon";
|
import Weapon from "./../Weapon/Weapon";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
import Armor from "./../Indicators/Armor";
|
import Armor from "./../Indicators/Armor";
|
||||||
import Bomb from "./../Indicators/Bomb";
|
import Bomb from "./../Indicators/Bomb";
|
||||||
import Defuse from "./../Indicators/Defuse";
|
import Defuse from "./../Indicators/Defuse";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
player: I.Player,
|
player: I.Player,
|
||||||
@@ -24,9 +24,9 @@ const compareWeapon = (weaponOne: I.WeaponRaw, weaponTwo: I.WeaponRaw) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compareWeapons = (weaponsObjectOne: { [key: string]: I.WeaponRaw }, weaponsObjectTwo: { [key: string]: I.WeaponRaw }) => {
|
const compareWeapons = (weaponsObjectOne: I.Weapon[], weaponsObjectTwo: I.Weapon[]) => {
|
||||||
const weaponsOne = Object.values(weaponsObjectOne).sort((a, b) => (a.name as any) - (b.name as any))
|
const weaponsOne = [...weaponsObjectOne].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
const weaponsTwo = Object.values(weaponsObjectTwo).sort((a, b) => (a.name as any) - (b.name as any))
|
const weaponsTwo = [...weaponsObjectTwo].sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
if (weaponsOne.length !== weaponsTwo.length) return false;
|
if (weaponsOne.length !== weaponsTwo.length) return false;
|
||||||
|
|
||||||
@@ -58,6 +58,8 @@ const arePlayersEqual = (playerOne: I.Player, playerTwo: I.Player) => {
|
|||||||
playerOne.state.equip_value === playerTwo.state.equip_value &&
|
playerOne.state.equip_value === playerTwo.state.equip_value &&
|
||||||
playerOne.state.adr === playerTwo.state.adr &&
|
playerOne.state.adr === playerTwo.state.adr &&
|
||||||
playerOne.avatar === playerTwo.avatar &&
|
playerOne.avatar === playerTwo.avatar &&
|
||||||
|
!!playerOne.team.id === !!playerTwo.team.id &&
|
||||||
|
playerOne.team.side === playerTwo.team.side &&
|
||||||
playerOne.country === playerTwo.country &&
|
playerOne.country === playerTwo.country &&
|
||||||
playerOne.realName === playerTwo.realName &&
|
playerOne.realName === playerTwo.realName &&
|
||||||
compareWeapons(playerOne.weapons, playerTwo.weapons)
|
compareWeapons(playerOne.weapons, playerTwo.weapons)
|
||||||
@@ -65,18 +67,20 @@ const arePlayersEqual = (playerOne: I.Player, playerTwo: I.Player) => {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Player = ({ player, isObserved }: IProps) => {
|
const Player = ({ player, isObserved }: IProps) => {
|
||||||
|
|
||||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
const weapons = 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 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 secondary = weapons.filter(weapon => weapon.type === "Pistol")[0] || null;
|
||||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
||||||
const isLeft = player.team.orientation === "left";
|
const isLeft = player.team.orientation === "left";
|
||||||
|
|
||||||
|
const zeus = weapons.find(weapon => weapon.name === "taser");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`player ${player.state.health === 0 ? "dead" : ""} ${isObserved ? 'active' : ''}`}>
|
<div className={`player ${player.state.health === 0 ? "dead" : ""} ${isObserved ? 'active' : ''}`}>
|
||||||
<div className="player_data">
|
<div className="player_data">
|
||||||
<Avatar teamId={player.team.id} steamid={player.steamid} height={57} width={57} showSkull={false} showCam={false} sidePlayer={true} />
|
<Avatar teamId={player.team.id} steamid={player.steamid} url={player.avatar} height={57} width={57} showSkull={false} showCam={false} sidePlayer={true} />
|
||||||
<div className="dead-stats">
|
<div className="dead-stats">
|
||||||
<div className="labels">
|
<div className="labels">
|
||||||
<div className="stat-label">K</div>
|
<div className="stat-label">K</div>
|
||||||
@@ -104,10 +108,11 @@ const Player = ({ player, isObserved }: IProps) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="armor_and_utility">
|
<div className="armor_and_utility">
|
||||||
<Bomb player={player} />
|
<Bomb player={player} />
|
||||||
<Armor player={player} />
|
<Armor health={player.state.health} armor={player.state.armor} helmet={player.state.helmet} />
|
||||||
<Defuse player={player} />
|
<Defuse player={player} />
|
||||||
</div>
|
</div>
|
||||||
<div className="money">${player.state.money}</div>
|
<div className="money">${player.state.money}</div>
|
||||||
|
{zeus ? <Weapon className={`zeus ${player.team.orientation}`} weapon="taser" active={zeus.state === "active"} /> : null}
|
||||||
<div className="grenades">
|
<div className="grenades">
|
||||||
{grenades.map(grenade => (
|
{grenades.map(grenade => (
|
||||||
[
|
[
|
||||||
@@ -131,5 +136,5 @@ const arePropsEqual = (prevProps: Readonly<IProps>, nextProps: Readonly<IProps>)
|
|||||||
return arePlayersEqual(prevProps.player, nextProps.player);
|
return arePlayersEqual(prevProps.player, nextProps.player);
|
||||||
}
|
}
|
||||||
|
|
||||||
//export default React.memo(Player, arePropsEqual);
|
export default React.memo(Player, arePropsEqual);
|
||||||
export default Player;
|
//export default Player;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
import Player from './Player'
|
import Player from './Player'
|
||||||
import * as I from 'csgogsi-socket';
|
import * as I from 'csgogsi';
|
||||||
import './players.scss';
|
import './players.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,17 +8,15 @@ interface Props {
|
|||||||
side: 'right' | 'left',
|
side: 'right' | 'left',
|
||||||
current: I.Player | null,
|
current: I.Player | null,
|
||||||
}
|
}
|
||||||
|
const TeamBox = ({players, team, side, current}: Props) => {
|
||||||
export default class TeamBox extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<div className={`teambox ${this.props.team.side} ${this.props.side}`}>
|
<div className={`teambox ${team.side} ${side}`}>
|
||||||
{this.props.players.map(player => <Player
|
{players.map(player => <Player
|
||||||
key={player.steamid}
|
key={player.steamid}
|
||||||
player={player}
|
player={player}
|
||||||
isObserved={!!(this.props.current && this.props.current.steamid === player.steamid)}
|
isObserved={!!(current && current.steamid === player.steamid)}
|
||||||
/>)}
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
export default TeamBox;
|
||||||
@@ -46,6 +46,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.weapon.zeus {
|
||||||
|
height: 30px;
|
||||||
|
position: absolute;
|
||||||
|
&.left {
|
||||||
|
left: 170px;
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
right: 170px;
|
||||||
|
}
|
||||||
|
}
|
||||||
&.right {
|
&.right {
|
||||||
right: 10px;
|
right: 10px;
|
||||||
.player {
|
.player {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import type { Bomb, Side } from 'csgogsi';
|
||||||
import { Bomb } from 'csgogsi-socket';
|
import maps, { MapConfig, ZoomAreas } from './maps';
|
||||||
import maps, { ScaleConfig, MapConfig, ZoomAreas } from './maps';
|
import './index.scss';
|
||||||
import './index.css';
|
|
||||||
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { parsePosition } from './utils';
|
||||||
interface IProps {
|
interface IProps {
|
||||||
players: RadarPlayerObject[];
|
players: RadarPlayerObject[];
|
||||||
grenades: RadarGrenadeObject[];
|
grenades: RadarGrenadeObject[];
|
||||||
@@ -12,57 +12,85 @@ interface IProps {
|
|||||||
zoom?: ZoomAreas;
|
zoom?: ZoomAreas;
|
||||||
mapConfig: MapConfig,
|
mapConfig: MapConfig,
|
||||||
reverseZoom: string,
|
reverseZoom: string,
|
||||||
parsePosition: (position: number[], size: number, config: ScaleConfig) => number[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isShooting = (lastShoot: number) => (new Date()).getTime() - lastShoot <= 250;
|
const isShooting = (lastShoot: number) => (new Date()).getTime() - lastShoot <= 250;
|
||||||
class App extends React.Component<IProps> {
|
|
||||||
constructor(props: IProps) {
|
type Explosion = {
|
||||||
super(props);
|
position: number[],
|
||||||
this.state = {
|
grenadeId: string
|
||||||
players: [],
|
|
||||||
grenades: [],
|
|
||||||
bomb: null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGrenade = (grenade: RadarGrenadeObject) => {
|
/*
|
||||||
if ("flames" in grenade) {
|
const SMOKE_PARTICLE_BASE_AMOUNT = 10;
|
||||||
|
//const PARTICLE_MAP_SOURCE = Array(SMOKE_PARTICLE_BASE_AMOUNT**2).fill(0);
|
||||||
|
|
||||||
|
const SMOKE_PARTICLE_SIZE_ABSOLUT = config.smokeSize/SMOKE_PARTICLE_BASE_AMOUNT;
|
||||||
|
|
||||||
|
const PARTICLE_SIDE = `${SMOKE_PARTICLE_SIZE_ABSOLUT}px`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const FRAG_RADIUS = 40;
|
||||||
|
//const MAX_DISTANCE_BETWEEN_FRAG_AND_SMOKE = config.smokeSize + FRAG_RADIUS;
|
||||||
|
|
||||||
|
const getDistance = (X: number[], Y: number[]) => {
|
||||||
|
const a = X[0] - Y[0];
|
||||||
|
const b = X[1] - Y[1];
|
||||||
|
|
||||||
|
return Math.sqrt( a*a + b*b );
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPosOfIndex = (index: number, origin: number[]) => {
|
||||||
|
const leftI = index%10;
|
||||||
|
const topI = Math.floor(index/10);
|
||||||
|
return ([origin[0] - config.smokeSize/2 + (leftI + 0.5) * SMOKE_PARTICLE_SIZE_ABSOLUT, origin[1] - config.smokeSize/2 + (topI + 0.5)*SMOKE_PARTICLE_SIZE_ABSOLUT]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Particle = ({ index, explosions, origin }: { index: number, origin: number[], explosions: Explosion[] }) => {
|
||||||
|
const PARTICLE_POSITION = getPosOfIndex(index, origin);
|
||||||
|
const isHidden = getDistance(PARTICLE_POSITION, origin) > config.smokeSize/2 || explosions.some(expl => getDistance(expl.position, PARTICLE_POSITION) <= FRAG_RADIUS);
|
||||||
|
return (<div className={`particle ${isHidden ? 'hide':''}`} style={{ width: PARTICLE_SIDE, height: PARTICLE_SIDE}}/>);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Grenade = ({ reverseZoom, type, state, visible, position, flames, side }: { explosions: Explosion[], reverseZoom: string, side: Side | null, flames: boolean, type: RadarGrenadeObject["type"], state: RadarGrenadeObject["state"], visible: boolean, position: number[] }) => {
|
||||||
|
if (flames) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { reverseZoom } = this.props;
|
|
||||||
|
if(type === "smoke" && (state === "landed" || state === "exploded")){
|
||||||
return (
|
return (
|
||||||
<div key={grenade.id} className={`grenade ${grenade.type} ${grenade.side || ''} ${grenade.state} ${grenade.visible ? 'visible':'hidden'}`}
|
<div className={`grenade ${type} ${state} ${side || ''} ${visible ? 'visible':'hidden'}`}
|
||||||
style={{
|
style={{
|
||||||
transform: `translateX(${grenade.position[0].toFixed(2)}px) translateY(${grenade.position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`,
|
transform: `translateX(${position[0]}px) translateY(${position[1]}px) translateZ(10px) scale(${reverseZoom})`,
|
||||||
}}>
|
}}>
|
||||||
|
<div className="content" style={{ width: config.smokeSize, height: config.smokeSize }}>
|
||||||
|
<div className="explode-point"></div>
|
||||||
|
<div className="background">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`grenade ${type} ${state} ${side || ''} ${visible ? 'visible':'hidden'}`}
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${position[0]}px) translateY(${position[1]}px) translateZ(10px) scale(${reverseZoom})`,
|
||||||
|
}}>
|
||||||
|
<div className="content">
|
||||||
<div className="explode-point"></div>
|
<div className="explode-point"></div>
|
||||||
<div className="background"></div>
|
<div className="background"></div>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
renderBomb = () => {
|
|
||||||
const { bomb, mapConfig, reverseZoom } = this.props;
|
const Bomb = ({ bomb, mapConfig, reverseZoom }: { reverseZoom: string, bomb?: Bomb | null, mapConfig: MapConfig }) => {
|
||||||
if(!bomb) return null;
|
if(!bomb) return null;
|
||||||
if(bomb.state === "carried" || bomb.state === "planting") return null;
|
if(bomb.state === "carried" || bomb.state === "planting") return null;
|
||||||
if("config" in mapConfig){
|
if("config" in mapConfig){
|
||||||
const position = this.props.parsePosition(bomb.position, 30, mapConfig.config);
|
const position = parsePosition(bomb.position, mapConfig.config);
|
||||||
if(!position) return null;
|
if(!position) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,41 +98,68 @@ class App extends React.Component<IProps> {
|
|||||||
style={{
|
style={{
|
||||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||||
}}>
|
}}>
|
||||||
|
<div className="content">
|
||||||
<div className="explode-point"></div>
|
<div className="explode-point"></div>
|
||||||
<div className="background"></div>
|
<div className="background"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return mapConfig.configs.map(config => {
|
return mapConfig.configs.map(config => {
|
||||||
const position = this.props.parsePosition(bomb.position, 30, config.config);
|
const position = parsePosition(bomb.position, config.config);
|
||||||
if(!position) return null;
|
if(!position) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`bomb ${bomb.state} ${config.isVisible(bomb.position[2]) ? 'visible':'hidden'}`}
|
<div key={`bomb_${config.id}`} className={`bomb ${bomb.state} ${config.isVisible(bomb.position[2]) ? 'visible':'hidden'}`}
|
||||||
style={{
|
style={{
|
||||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px)`
|
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||||
}}>
|
}}>
|
||||||
|
<div className="content">
|
||||||
<div className="explode-point"></div>
|
<div className="explode-point"></div>
|
||||||
<div className="background"></div>
|
<div className="background"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
render() {
|
|
||||||
const { players, grenades, zoom } = this.props;
|
|
||||||
|
|
||||||
const style: React.CSSProperties = { backgroundImage: `url(${maps[this.props.mapName].file})` }
|
const PlayerDot = ({ player, reverseZoom }: { reverseZoom: string, player: RadarPlayerObject }) => {
|
||||||
|
const isShootingNow = isShooting(player.lastShoot);
|
||||||
|
//console.log('x',isShooting(player.lastShoot), player.steamid);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
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})`,
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<div className="content" style={{
|
||||||
|
width: config.playerSize * player.scale,
|
||||||
|
height: config.playerSize * player.scale
|
||||||
|
}}>
|
||||||
|
<div className="background-fire" style={{ transform: `rotate(${-90 + player.position[2]}deg)`, opacity: isShootingNow ? 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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Radar = ({ players, grenades, mapConfig, bomb, mapName, zoom, reverseZoom }: IProps) => {
|
||||||
|
//if(players.length === 0) return null;
|
||||||
|
|
||||||
|
const style: React.CSSProperties = { backgroundImage: `url(${maps[mapName].file})` }
|
||||||
|
|
||||||
if(zoom){
|
if(zoom){
|
||||||
style.transform = `scale(${zoom.zoom})`;
|
style.transform = `scale(${zoom.zoom})`;
|
||||||
style.transformOrigin = `${zoom.origin[0]}px ${zoom.origin[1]}px`;
|
style.transformOrigin = `${zoom.origin[0]}px ${zoom.origin[1]}px`;
|
||||||
}
|
}
|
||||||
//if(players.length === 0) return null;
|
const explosions = grenades.filter(grenade => grenade.type === "frag" && grenade.state === "exploded");
|
||||||
return <div className="map" style={style}>
|
return <div className="map" style={style}>
|
||||||
{players.map(this.renderDot)}
|
{players.map(player => <PlayerDot reverseZoom={reverseZoom} key={player.id} player={player} />)}
|
||||||
{grenades.map(this.renderGrenade)}
|
{grenades.map(grenade => <Grenade explosions={explosions.map(g => ({ position: g.position, grenadeId: g.id }))} reverseZoom={reverseZoom} side={grenade.side} key={grenade.id} type={grenade.type} state={grenade.state} visible={grenade.visible} position={grenade.position} flames={"flames" in grenade} />)}
|
||||||
{this.renderBomb()}
|
<Bomb reverseZoom={reverseZoom} bomb={bomb} mapConfig={mapConfig} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default Radar;
|
||||||
|
|||||||
@@ -1,322 +1,79 @@
|
|||||||
import React from 'react';
|
import { Player, Bomb, Grenade, FragOrFireBombOrFlashbandGrenade } from 'csgogsi';
|
||||||
import { Player, Bomb } from 'csgogsi-socket';
|
import maps from './maps';
|
||||||
import maps, { ScaleConfig } from './maps';
|
|
||||||
import LexoRadar from './LexoRadar';
|
import LexoRadar from './LexoRadar';
|
||||||
import { ExtendedGrenade, Grenade, RadarPlayerObject, RadarGrenadeObject } from './interface';
|
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||||
import config from './config';
|
import { EXPLODE_TIME_FRAG, explosionPlaces, extendGrenade, extendPlayer, grenadesStates, playersStates } from './utils';
|
||||||
|
import { GSI } from '../../../API/HUD';
|
||||||
|
|
||||||
const DESCALE_ON_ZOOM = true;
|
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 {
|
interface IProps {
|
||||||
players: Player[],
|
players: Player[],
|
||||||
bomb?: Bomb | null,
|
bomb?: Bomb | null,
|
||||||
player: Player | null,
|
player: Player | null,
|
||||||
grenades?: any
|
grenades: Grenade[]
|
||||||
size?: number,
|
size?: number,
|
||||||
mapName: string
|
mapName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends React.Component<IProps> {
|
GSI.prependListener("data", () => {
|
||||||
round = (n: number) => {
|
|
||||||
const r = 0.02;
|
|
||||||
return Math.round(n / r) * r;
|
|
||||||
}
|
|
||||||
|
|
||||||
parsePosition = (position: number[], size: number, config: ScaleConfig) => {
|
const currentGrenades = GSI.current?.grenades || []
|
||||||
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);
|
grenadesStates.unshift(currentGrenades);
|
||||||
|
grenadesStates.splice(5);
|
||||||
|
|
||||||
|
playersStates.unshift(GSI.current?.players || []);
|
||||||
|
playersStates.splice(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
GSI.prependListener("data", data => {
|
||||||
|
const { last } = GSI;
|
||||||
|
if(!last) return;
|
||||||
|
|
||||||
|
for(const grenade of data.grenades.filter((grenade): grenade is FragOrFireBombOrFlashbandGrenade => grenade.type === "frag")){
|
||||||
|
const old = last.grenades.find((oldGrenade): oldGrenade is FragOrFireBombOrFlashbandGrenade => oldGrenade.id === grenade.id);
|
||||||
|
if(!old) continue;
|
||||||
|
|
||||||
|
if(grenade.lifetime >= EXPLODE_TIME_FRAG && old.lifetime < EXPLODE_TIME_FRAG){
|
||||||
|
explosionPlaces[grenade.id] = grenade.position;
|
||||||
}
|
}
|
||||||
if (grenadesStates.length > 5) {
|
|
||||||
grenadesStates = grenadesStates.slice(0, 5);
|
|
||||||
}
|
}
|
||||||
const size = this.props.size || 300;
|
for(const grenadeId of Object.keys(explosionPlaces)){
|
||||||
|
const doesExist = data.grenades.some(grenade => grenade.id === grenadeId);
|
||||||
|
if(!doesExist){
|
||||||
|
delete explosionPlaces[grenadeId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LexoRadarContainer = ({ size = 300, mapName, bomb, player, players, grenades }: IProps) => {
|
||||||
const offset = (size - (size * size / 1024)) / 2;
|
const offset = (size - (size * size / 1024)) / 2;
|
||||||
|
|
||||||
const config = maps[this.props.mapName];
|
if (!(mapName in maps)) {
|
||||||
|
|
||||||
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 }}>
|
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||||
Unsupported map
|
Unsupported map
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
const playersExtended: RadarPlayerObject[] = players.map(pl => extendPlayer({ player: pl, steamId: player?.steamid || null, mapName })).filter((player): player is RadarPlayerObject => player !== null).flat();
|
||||||
|
const grenadesExtended = grenades.map(grenade => extendGrenade({ grenade, side: playersExtended.find(player => player.steamid === grenade.owner)?.side || 'CT', mapName })).filter(entry => entry !== null).flat() as RadarGrenadeObject[];
|
||||||
|
const config = maps[mapName];
|
||||||
|
|
||||||
|
const zooms = config && config.zooms || [];
|
||||||
|
|
||||||
|
const activeZoom = zooms.find(zoom => zoom.threshold(playersExtended.map(pl => pl.player)));
|
||||||
|
|
||||||
|
const reverseZoom = 1/(activeZoom && activeZoom.zoom || 1);
|
||||||
|
// s*(1024-s)/2048
|
||||||
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||||
<LexoRadar
|
<LexoRadar
|
||||||
players={players}
|
players={playersExtended}
|
||||||
grenades={grenades}
|
grenades={grenadesExtended}
|
||||||
parsePosition={this.parsePosition}
|
bomb={bomb}
|
||||||
bomb={this.props.bomb}
|
mapName={mapName}
|
||||||
mapName={this.props.mapName}
|
mapConfig={config}
|
||||||
mapConfig={maps[this.props.mapName]}
|
|
||||||
zoom={activeZoom}
|
zoom={activeZoom}
|
||||||
reverseZoom={DESCALE_ON_ZOOM ? reverseZoom.toFixed(2) : '1'}
|
reverseZoom={DESCALE_ON_ZOOM ? reverseZoom.toFixed(2) : '1'}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default LexoRadarContainer;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const config = {
|
const config = {
|
||||||
playerSize: 60,
|
playerSize: 70,
|
||||||
|
smokeSize: 50
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
@@ -51,12 +51,20 @@ html, body, .map-container {
|
|||||||
}
|
}
|
||||||
.map .player, .map .grenade, .map .bomb {
|
.map .player, .map .grenade, .map .bomb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height:30px;
|
height:0px;
|
||||||
width:30px;
|
width:0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
|
.content {
|
||||||
|
position: absolute;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
/*transition: all 0.1s ease;/**/
|
/*transition: all 0.1s ease;/**/
|
||||||
}
|
}
|
||||||
.map .player .background {
|
.map .player .background {
|
||||||
@@ -66,7 +74,7 @@ html, body, .map-container {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
transition:transform 0.2s ease;
|
transition:transform 0.2s linear;
|
||||||
}
|
}
|
||||||
.map .player .background-fire {
|
.map .player .background-fire {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -140,10 +148,12 @@ html, body, .map-container {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
.map .player.active {
|
.map .player.active {
|
||||||
width:120%;
|
|
||||||
height:120%;
|
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
.map .player.active .content {
|
||||||
|
width:48px;
|
||||||
|
height:48px;
|
||||||
|
}
|
||||||
.map .grenade .background {
|
.map .grenade .background {
|
||||||
border-radius:50%;
|
border-radius:50%;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
@@ -157,9 +167,6 @@ html, body, .map-container {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 1s;
|
transition: opacity 1s;
|
||||||
}
|
}
|
||||||
.map .grenade.smoke {
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
.map .grenade.smoke.inair .background {
|
.map .grenade.smoke.inair .background {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -167,7 +174,6 @@ html, body, .map-container {
|
|||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
.map .grenade.smoke.exploded .background {
|
.map .grenade.smoke.exploded .background {
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
.map .grenade.flashbang, .map .grenade.frag {
|
.map .grenade.flashbang, .map .grenade.frag {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
@@ -183,6 +189,8 @@ html, body, .map-container {
|
|||||||
.map .grenade .explode-point, .map .bomb .explode-point {
|
.map .grenade .explode-point, .map .bomb .explode-point {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
top: calc(50% - 1px);
|
||||||
|
left: calc(50% - 1px);
|
||||||
height: 2px;
|
height: 2px;
|
||||||
border-radius: 0.08px;
|
border-radius: 0.08px;
|
||||||
}
|
}
|
||||||
@@ -194,7 +202,6 @@ html, body, .map-container {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.map .grenade.smoke .background {
|
.map .grenade.smoke .background {
|
||||||
border: 5px solid grey;
|
|
||||||
background-color: rgba(255,255,255,0.5);
|
background-color: rgba(255,255,255,0.5);
|
||||||
}
|
}
|
||||||
.map .grenade.smoke.CT .background {
|
.map .grenade.smoke.CT .background {
|
||||||
@@ -214,9 +221,8 @@ html, body, .map-container {
|
|||||||
width:12px;
|
width:12px;
|
||||||
height:12px;
|
height:12px;
|
||||||
}
|
}
|
||||||
.map .grenade.smoke {
|
.map .grenade .content {
|
||||||
width:60px;
|
position: absolute;
|
||||||
height:60px;
|
|
||||||
}
|
}
|
||||||
.map .grenade.inferno .background {
|
.map .grenade.inferno .background {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
@@ -270,16 +276,29 @@ html, body, .map-container {
|
|||||||
background: red;
|
background: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes Hidden {
|
.map .player.dead .background {
|
||||||
from {
|
display: none;
|
||||||
}
|
}
|
||||||
to {
|
.map .player.dead .label {
|
||||||
display: none !important;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
.map .player.dead.CT .label, .map .player.CT .label div {
|
||||||
|
color: var(--color-new-ct);
|
||||||
|
}
|
||||||
|
.map .player.dead.T .label, .map .player.T .label div {
|
||||||
|
color: var(--color-new-t);
|
||||||
}
|
}
|
||||||
.map .hidden {
|
.map .hidden {
|
||||||
opacity: 0;
|
opacity: 0 !important;
|
||||||
animation: Hidden 1s ease 1s 1;
|
}
|
||||||
animation-fill-mode: forwards;/**/
|
|
||||||
|
.map .grenade {
|
||||||
|
&.smoke {
|
||||||
|
&.exploded, &.landed {
|
||||||
|
.background {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Player, Side } from "csgogsi";
|
import { Player, Side, Grenade } from "csgogsi";
|
||||||
|
|
||||||
export interface RadarPlayerObject {
|
export interface RadarPlayerObject {
|
||||||
id: string,
|
id: string,
|
||||||
@@ -26,30 +26,5 @@ export interface RadarGrenadeObject {
|
|||||||
visible: boolean,
|
visible: boolean,
|
||||||
id: string,
|
id: string,
|
||||||
}
|
}
|
||||||
export interface GrenadeBase {
|
|
||||||
owner: string,
|
|
||||||
type: 'decoy' | 'smoke' | 'frag' | 'firebomb' | 'flashbang' | 'inferno'
|
|
||||||
lifetime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DecoySmokeGrenade extends GrenadeBase {
|
export type ExtendedGrenade = Grenade & { side: Side | null, };
|
||||||
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, };
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 60 KiB |
@@ -1,16 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 47 KiB |
@@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 127 KiB |
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB |
@@ -1,67 +0,0 @@
|
|||||||
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
|
|
||||||
}*/
|
|
||||||
|
Before Width: | Height: | Size: 77 KiB |
@@ -1,71 +1,125 @@
|
|||||||
import de_mirage from './de_mirage';
|
import { Player } from "csgogsi";
|
||||||
import de_cache from './de_cache';
|
import api, { apiUrl } from "../../../../API";
|
||||||
import de_dust2 from './de_dust2';
|
import { GameMapRadar } from "../../../../API/types";
|
||||||
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 = {
|
export type ZoomAreas = {
|
||||||
threshold: (players: Player[]) => boolean;
|
threshold: (players: Player[]) => boolean;
|
||||||
origin: number[],
|
origin: number[];
|
||||||
zoom: number
|
zoom: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ScaleConfig {
|
export interface ScaleConfig {
|
||||||
origin: {
|
origin: {
|
||||||
x:number,
|
x: number;
|
||||||
y:number
|
y: number;
|
||||||
},
|
};
|
||||||
pxPerUX: number,
|
pxPerUX: number;
|
||||||
pxPerUY: number,
|
pxPerUY: number;
|
||||||
originHeight?: number
|
originHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RadarFile = string;
|
||||||
interface SingleLayer {
|
interface SingleLayer {
|
||||||
config: ScaleConfig,
|
config: ScaleConfig;
|
||||||
file: string,
|
file: RadarFile;
|
||||||
zooms?: ZoomAreas[]
|
zooms?: ZoomAreas[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DoubleLayer {
|
interface DoubleLayer {
|
||||||
configs: {
|
configs: {
|
||||||
id: string,
|
id: string;
|
||||||
config: ScaleConfig,
|
config: ScaleConfig;
|
||||||
isVisible: (height: number) => boolean
|
isVisible: (height: number) => boolean;
|
||||||
}[],
|
}[];
|
||||||
file: string,
|
file: RadarFile;
|
||||||
zooms?: ZoomAreas[]
|
zooms?: ZoomAreas[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapConfig = SingleLayer | DoubleLayer;
|
export type MapConfig = SingleLayer | DoubleLayer;
|
||||||
|
|
||||||
const maps: { [key: string] : MapConfig} = {
|
const maps: { [key: string]: MapConfig } = {};
|
||||||
de_mirage,
|
|
||||||
de_cache,
|
api.maps
|
||||||
de_inferno,
|
.get()
|
||||||
de_dust2,
|
.then((newMaps) => {
|
||||||
de_train,
|
newMaps.forEach((map) => {
|
||||||
de_overpass,
|
const mainRadar: GameMapRadar | null =
|
||||||
de_nuke,
|
map.radars.find((radar) => radar.lhmId === "default") ||
|
||||||
de_vertigo,
|
map.radars[0] ||
|
||||||
de_ancient,
|
null;
|
||||||
de_anubis
|
const hasMultipleRadars = map.radars.length > 1;
|
||||||
|
if (!mainRadar) return;
|
||||||
|
|
||||||
|
if (!hasMultipleRadars) {
|
||||||
|
maps[map.lhmId] = {
|
||||||
|
config: {
|
||||||
|
origin: {
|
||||||
|
x: mainRadar.originX || 0,
|
||||||
|
y: mainRadar.originY || 0,
|
||||||
|
},
|
||||||
|
pxPerUX: mainRadar.pxPerUX || 0,
|
||||||
|
pxPerUY: mainRadar.pxPerUY || 0,
|
||||||
|
},
|
||||||
|
file: `${apiUrl}api/game-maps/cs2/image/${map.lhmId}/radar`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
maps[map.lhmId] = {
|
||||||
|
configs: map.radars.map((m: any) => ({
|
||||||
|
id: m.lhmId,
|
||||||
|
config: {
|
||||||
|
origin: {
|
||||||
|
x: m.originX || 0,
|
||||||
|
y: m.originY || 0,
|
||||||
|
},
|
||||||
|
pxPerUX: m.pxPerUX || 0,
|
||||||
|
pxPerUY: m.pxPerUY || 0,
|
||||||
|
},
|
||||||
|
isVisible: (height: number) =>
|
||||||
|
(m.visibleUnderHeight !== null
|
||||||
|
? m.visibleUnderHeight < height
|
||||||
|
: true) &&
|
||||||
|
(m.visibleOverHeight !== null
|
||||||
|
? m.visibleOverHeight > height
|
||||||
|
: true),
|
||||||
|
})),
|
||||||
|
file: `${apiUrl}api/game-maps/cs2/image/${map.lhmId}/radar`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
api.maps.get().then(fallbackMaps => {
|
if (map.lhmId === "de_vertigo") {
|
||||||
const mapNames = Object.keys(fallbackMaps);
|
maps[map.lhmId].zooms = [
|
||||||
for(const mapName of mapNames){
|
{
|
||||||
if(mapName in maps){
|
threshold: (players: Player[]) => {
|
||||||
continue;
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
maps[mapName] = fallbackMaps[mapName];
|
});
|
||||||
}
|
})
|
||||||
}).catch(() => {});
|
.catch(() => { });
|
||||||
|
|
||||||
export default maps;
|
export default maps;
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { Player, Side, Grenade } from "csgogsi";
|
||||||
|
import maps, { ScaleConfig } from "./maps";
|
||||||
|
import { ExtendedGrenade, RadarGrenadeObject, RadarPlayerObject } from "./interface";
|
||||||
|
import { InfernoGrenade } from "csgogsi";
|
||||||
|
|
||||||
|
export const playersStates: Player[][] = [];
|
||||||
|
export const grenadesStates: Grenade[][] = [];
|
||||||
|
const directions: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
export const explosionPlaces: 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const round = (n: number) => {
|
||||||
|
const r = 0.02;
|
||||||
|
return Math.round(n / r) * r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parsePosition = (position: number[], config: ScaleConfig) => {
|
||||||
|
const left = config.origin.x + (position[0] * config.pxPerUX);
|
||||||
|
const top = config.origin.y + (position[1] * config.pxPerUY);
|
||||||
|
|
||||||
|
return [round(left), round(top)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parsePlayerPosition = (player: Player, mapConfig: ScaleConfig) => {
|
||||||
|
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 => parsePosition(playerEntry.position, 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const parseGrenadePosition = (grenade: ExtendedGrenade, config: ScaleConfig) => {
|
||||||
|
if(grenade.id in explosionPlaces) return parsePosition(explosionPlaces[grenade.id], config);
|
||||||
|
const grenadeData = grenadesStates.slice(0, 5).map(grenades => grenades.filter(gr => gr.id === grenade.id)[0]).filter(pl => !!pl);
|
||||||
|
if (grenadeData.length === 0) return "position" in grenade ? parsePosition(grenade.position, config) : null;
|
||||||
|
const positions = grenadeData.map(grenadeEntry => ("position" in grenadeEntry ? parsePosition(grenadeEntry.position, config) : null)).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];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EXPLODE_TIME_FRAG = 1.6;
|
||||||
|
export const EXPLODE_TIME_FLASH = 1.45;
|
||||||
|
|
||||||
|
export const extendGrenade = ({grenade, mapName, side }: { side: Side, grenade: Grenade, mapName: string}) => {
|
||||||
|
// const owner = this.props.players.find(player => player.steamid === grenade.owner);
|
||||||
|
const extGrenade: ExtendedGrenade = {
|
||||||
|
...grenade,
|
||||||
|
side:/* owner?.team.side ||*/ side
|
||||||
|
}
|
||||||
|
const map = maps[mapName];
|
||||||
|
if (extGrenade.type === "inferno") {
|
||||||
|
const mapFlame = (flame: InfernoGrenade["flames"][number]) => {
|
||||||
|
if ("config" in map) {
|
||||||
|
return ({
|
||||||
|
position: parsePosition(flame.position, map.config),
|
||||||
|
id: `${flame.id}_${extGrenade.id}`,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map.configs.map(config => ({
|
||||||
|
id: `${flame.id}_${extGrenade.id}_${config.id}`,
|
||||||
|
visible: config.isVisible(flame.position[2]),
|
||||||
|
position: parsePosition(flame.position, config.config)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const flames = 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 = parseGrenadePosition(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) {
|
||||||
|
grenadeObject.state = "landed";
|
||||||
|
if (extGrenade.effecttime >= 16.5) {
|
||||||
|
grenadeObject.state = 'exploded';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((extGrenade.type === 'flashbang' && extGrenade.lifetime >= EXPLODE_TIME_FLASH) || (extGrenade.type === 'frag' && extGrenade.lifetime >= EXPLODE_TIME_FRAG)) {
|
||||||
|
grenadeObject.state = 'exploded';
|
||||||
|
}
|
||||||
|
return grenadeObject;
|
||||||
|
}
|
||||||
|
return map.configs.map(config => {
|
||||||
|
const position = parseGrenadePosition(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[2])
|
||||||
|
}
|
||||||
|
if (extGrenade.type === "smoke") {
|
||||||
|
if (extGrenade.effecttime !== 0) {
|
||||||
|
grenadeObject.state = "landed";
|
||||||
|
if (extGrenade.effecttime >= 16.5) {
|
||||||
|
grenadeObject.state = 'exploded';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((extGrenade.type === 'flashbang' && extGrenade.lifetime >= EXPLODE_TIME_FLASH) || (extGrenade.type === 'frag' && extGrenade.lifetime >= EXPLODE_TIME_FRAG)) {
|
||||||
|
grenadeObject.state = 'exploded';
|
||||||
|
}
|
||||||
|
return grenadeObject;
|
||||||
|
}).filter((grenade): grenade is RadarGrenadeObject => grenade !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extendPlayer = ({ player, steamId, mapName }: { mapName: string, player: Player, steamId: string | null}): RadarPlayerObject | RadarPlayerObject[] | 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[mapName];
|
||||||
|
const playerObject: RadarPlayerObject = {
|
||||||
|
id: player.steamid,
|
||||||
|
label: player.observer_slot !== undefined ? player.observer_slot : "",
|
||||||
|
side: player.team.side,
|
||||||
|
position: [],
|
||||||
|
visible: true,
|
||||||
|
isActive: steamId === player.steamid,
|
||||||
|
forward: 0,
|
||||||
|
scale: 1,
|
||||||
|
steamid: player.steamid,
|
||||||
|
flashed: player.state.flashed > 35,
|
||||||
|
shooting: isShooting,
|
||||||
|
lastShoot: shooting.lastShoot,
|
||||||
|
isAlive: player.state.health > 0,
|
||||||
|
hasBomb: !!Object.values(player.weapons).find(weapon => weapon.type === "C4"),
|
||||||
|
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 = parsePlayerPosition(player, map.config);
|
||||||
|
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: parsePlayerPosition(player, config.config),
|
||||||
|
id: `${player.steamid}_${config.id}`,
|
||||||
|
visible: config.isVisible(player.position[2])
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,50 +1,20 @@
|
|||||||
import React from "react";
|
import { CSGO } from "csgogsi";
|
||||||
import { isDev } from './../../api/api';
|
|
||||||
import { CSGO } from "csgogsi-socket";
|
|
||||||
import LexoRadarContainer from './LexoRadar/LexoRadarContainer';
|
import LexoRadarContainer from './LexoRadar/LexoRadarContainer';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Props { radarSize: number, game: CSGO }
|
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> {
|
const Radar = ({ radarSize, game }: Props) => {
|
||||||
state = {
|
const { players, player, bomb, grenades, map } = game;
|
||||||
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
|
return <LexoRadarContainer
|
||||||
players={players}
|
players={players}
|
||||||
player={player}
|
player={player}
|
||||||
bomb={bomb}
|
bomb={bomb}
|
||||||
grenades={grenades}
|
grenades={grenades}
|
||||||
size={this.props.radarSize}
|
size={radarSize}
|
||||||
mapName={map.name.substring(map.name.lastIndexOf('/')+1)}
|
mapName={map.name.substring(map.name.lastIndexOf('/')+1)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default Radar;
|
||||||
@@ -1,70 +1,58 @@
|
|||||||
import React from "react";
|
import { useState } from "react";
|
||||||
import "./radar.scss";
|
import "./radar.scss";
|
||||||
import { Match, Veto } from "../../api/interfaces";
|
import { Match, Veto } from "../../API/types";
|
||||||
import { Map, CSGO, Team } from 'csgogsi-socket';
|
import { Map, CSGO, Team } from 'csgogsi';
|
||||||
import { actions } from './../../App';
|
|
||||||
import Radar from './Radar'
|
import Radar from './Radar'
|
||||||
import TeamLogo from "../MatchBar/TeamLogo";
|
|
||||||
|
import { useAction } from "../../API/contexts/actions";
|
||||||
|
|
||||||
interface Props { match: Match | null, map: Map, game: CSGO }
|
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> {
|
const RadarMaps = ({ match, map, game }: Props) => {
|
||||||
state = {
|
const [ radarSize, setRadarSize ] = useState(366);
|
||||||
showRadar: true,
|
const [ showBig, setShowBig ] = useState(false);
|
||||||
radarSize: 350,
|
|
||||||
showBig: false
|
useAction('radarBigger', () => {
|
||||||
}
|
setRadarSize(p => p+10);
|
||||||
componentDidMount() {
|
}, []);
|
||||||
actions.on('radarBigger', () => this.radarChangeSize(20));
|
|
||||||
actions.on('radarSmaller', () => this.radarChangeSize(-20));
|
useAction('radarSmaller', () => {
|
||||||
actions.on('toggleRadar', () => { this.setState(state => ({ showRadar: !state.showRadar })) });
|
setRadarSize(p => p-10);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useAction('toggleRadarView', () => {
|
||||||
|
setShowBig(p => !p);
|
||||||
|
}, []);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div id={`radar_maps_container`} className={`${!showRadar ? 'hide' : ''} ${showBig ? 'preview':''}`}>
|
<div id={`radar_maps_container`} className={` ${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={match} map={map} game={game} /> : null}
|
||||||
{match ? <MapsBar match={this.props.match} map={this.props.map} game={this.props.game} /> : null}
|
<Radar radarSize={showBig ? 600: radarSize} game={game} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class MapsBar extends React.PureComponent<Props> {
|
export default RadarMaps;
|
||||||
render() {
|
|
||||||
const { match, map } = this.props;
|
const MapsBar = ({ match, map }: Props) => {
|
||||||
if (!match || !match.vetos.length) return '';
|
if (!match || !match.vetos.length) return '';
|
||||||
const picks = match.vetos.filter(veto => veto.type !== "ban" && veto.mapName);
|
const picks = match.vetos.filter(veto => veto.type !== "ban" && veto.mapName);
|
||||||
if (picks.length > 3) {
|
if (picks.length > 3) {
|
||||||
const current = picks.find(veto => map.name.includes(veto.mapName));
|
const current = picks.find(veto => map.name.includes(veto.mapName));
|
||||||
if (!current) return null;
|
if (!current) return null;
|
||||||
return <div id="maps_container">
|
return <div id="maps_container">
|
||||||
|
<div className="bestof">Best of {match.matchType.replace("bo", "")}</div>
|
||||||
{<MapEntry veto={current} map={map} team={current.type === "decider" ? null : map.team_ct.id === current.teamId ? map.team_ct : map.team_t} />}
|
{<MapEntry veto={current} map={map} team={current.type === "decider" ? null : map.team_ct.id === current.teamId ? map.team_ct : map.team_t} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
return <div id="maps_container">
|
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 className="bestof">Best of {match.matchType.replace("bo", "")}</div>
|
||||||
|
{match.vetos.filter(veto => veto.type !== "ban").filter(veto => veto.teamId || veto.type === "decider").map(veto => <MapEntry key={veto.mapName} veto={veto} map={map} team={veto.type === "decider" ? null : map.team_ct.id === veto.teamId ? map.team_ct : map.team_t} />)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class MapEntry extends React.PureComponent<{ veto: Veto, map: Map, team: Team | null }> {
|
const MapEntry = ({ veto, map }: { veto: Veto, map: Map, team: Team | null }) => {
|
||||||
render() {
|
|
||||||
const { veto, map, team } = this.props;
|
|
||||||
return <div className="veto_entry">
|
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.replace("de_", "")}</div>
|
||||||
<div className={`map_name ${map.name.includes(veto.mapName) ? 'active' : ''}`}>{veto.mapName}</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
transition: all 1s;
|
transition: all 1s;
|
||||||
.map-container {
|
.map-container {
|
||||||
transition: all 1s;
|
transition: all 1s;
|
||||||
@@ -24,18 +23,24 @@
|
|||||||
perspective: 500px;
|
perspective: 500px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.radar-component-container {
|
|
||||||
width: 350px;
|
|
||||||
height:350px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#maps_container {
|
#maps_container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
background-color: rgba(0,0,0,0.5);
|
color: white;
|
||||||
|
background-color: var(--sub-panel-color);
|
||||||
|
|
||||||
|
.bestof {
|
||||||
|
width: 121px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.veto_entry {
|
.veto_entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -59,8 +64,15 @@
|
|||||||
width: 23px;
|
width: 23px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_name.active {
|
.map_name {
|
||||||
text-shadow: 0 0 15px white;
|
font-size:12px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#scout {
|
||||||
|
width: 512.5px;
|
||||||
|
background-color: var(--sub-panel-color);
|
||||||
|
padding: 10px;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: white;
|
||||||
|
top: 126px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.bar-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 29px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
height: 100%;
|
||||||
|
.team-prediction {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.team-CT {
|
||||||
|
background-color: var(--color-new-ct);
|
||||||
|
}
|
||||||
|
.team-T {
|
||||||
|
background-color: var(--color-new-t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: 2;
|
||||||
|
align-items: center;
|
||||||
|
.team-prediction-value {
|
||||||
|
margin: 0 10px;
|
||||||
|
text-shadow: 1px 1px 0px black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Side } from "csgogsi";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
export const Scout = ({ left, right }: { left: Side, right: Side }) => {
|
||||||
|
return <div id="scout">
|
||||||
|
<div className="bar-container">
|
||||||
|
<div className="overlay">
|
||||||
|
<div className={`team-prediction-value left ${left}`}>50%</div>
|
||||||
|
<div className={`team-prediction-value right ${right}`}>50%</div>
|
||||||
|
</div>
|
||||||
|
<div className="bar">
|
||||||
|
<div className={`team-prediction team-${left} left`} />
|
||||||
|
<div className={`team-prediction team-${right} right`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,45 +1,47 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
class LossBox extends React.PureComponent<{ active: boolean, side: 'CT' | 'T' }>{
|
const LossBox = React.memo(({ active, side }: { active: boolean; side: "CT" | "T" }) => {
|
||||||
render(){
|
return (
|
||||||
return <div className={`loss-box ${this.props.side} ${this.props.active ? 'active':''}`}></div>
|
<div
|
||||||
}
|
className={`loss-box ${side} ${
|
||||||
}
|
active ? "active" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
side: 'left' | 'right',
|
side: "left" | "right";
|
||||||
team: 'CT' | 'T',
|
team: "CT" | "T";
|
||||||
loss: number,
|
loss: number;
|
||||||
equipment: number,
|
equipment: number;
|
||||||
money: number,
|
money: number;
|
||||||
show: boolean,
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Money extends React.PureComponent<Props> {
|
const Money = ({ side, team, loss, equipment, money, show }: Props) => {
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<div className={`moneybox ${this.props.side} ${this.props.team} ${this.props.show ? "show" : "hide"}`}>
|
<div className={`moneybox ${side} ${team} ${show ? "show" : "hide"}`}>
|
||||||
<div className="loss_container">
|
<div className="loss_container">
|
||||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 4} />
|
<LossBox side={team} active={(loss - 1400) / 500 >= 4} />
|
||||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 3} />
|
<LossBox side={team} active={(loss - 1400) / 500 >= 3} />
|
||||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 2} />
|
<LossBox side={team} active={(loss - 1400) / 500 >= 2} />
|
||||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 1} />
|
<LossBox side={team} active={(loss - 1400) / 500 >= 1} />
|
||||||
</div>
|
</div>
|
||||||
<div className="money_container">
|
<div className="money_container">
|
||||||
<div className="title">Loss Bonus</div>
|
<div className="title">Loss Bonus</div>
|
||||||
<div className="value">${this.props.loss}</div>
|
<div className="value">${loss}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="money_container">
|
<div className="money_container">
|
||||||
<div className="title">Team Money</div>
|
<div className="title">Team Money</div>
|
||||||
<div className="value">${this.props.money}</div>
|
<div className="value">${money}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="money_container">
|
<div className="money_container">
|
||||||
<div className="title">Equipment Value</div>
|
<div className="title">Equipment Value</div>
|
||||||
<div className="value">${this.props.equipment}</div>
|
<div className="value">${equipment}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default Money;
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,49 +1,32 @@
|
|||||||
import React from 'react';
|
import { useState } from 'react';
|
||||||
import './sideboxes.scss'
|
import './sideboxes.scss'
|
||||||
import {configs, hudIdentity} from './../../App';
|
import { apiUrl } from './../../API';
|
||||||
import { apiUrl } from '../../api/api';
|
import { useConfig, useOnConfigChange } from '../../API/contexts/actions';
|
||||||
|
import { hudIdentity } from '../../API/HUD';
|
||||||
|
|
||||||
export default class SideBox extends React.Component<{ side: 'left' | 'right', hide: boolean}, { title: string, subtitle: string, image?: string }> {
|
const Sidebox = ({side, hide} : { side: 'left' | 'right', hide: boolean}) => {
|
||||||
constructor(props: any) {
|
const [ image, setImage ] = useState<string | null>(null);
|
||||||
super(props);
|
const data = useConfig('display_settings');
|
||||||
this.state = {
|
|
||||||
title:'Title',
|
|
||||||
subtitle:'Content',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useOnConfigChange('display_settings', data => {
|
||||||
configs.onChange((data:any) => {
|
if(data && `${side}_image` in data){
|
||||||
if(!data) return;
|
const imageUrl = `${apiUrl}api/huds/${hudIdentity.name || 'dev'}/display_settings/${side}_image?isDev=${hudIdentity.isDev}&cache=${(new Date()).getTime()}`;
|
||||||
const display = data.display_settings;
|
setImage(imageUrl);
|
||||||
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() {
|
if(!data || !data[`${side}_title`]) return null;
|
||||||
const { image, title, subtitle} = this.state;
|
|
||||||
if(!title) return '';
|
|
||||||
return (
|
return (
|
||||||
<div className={`sidebox ${this.props.side} ${this.props.hide ? 'hide':''}`}>
|
<div className={`sidebox ${side} ${hide ? 'hide':''}`}>
|
||||||
<div className="title_container">
|
<div className="title_container">
|
||||||
<div className="title">{title}</div>
|
<div className="title">{data[`${side}_title`]}</div>
|
||||||
<div className="subtitle">{subtitle}</div>
|
<div className="subtitle">{data[`${side}_subtitle`]}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="image_container">
|
<div className="image_container">
|
||||||
{image ? <img src={image} id={`image_left`} alt={'Left'}/>:''}
|
{image ? <img src={image} id={`image_left`} alt={'Left'}/>:null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export default Sidebox;
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from "react";
|
|
||||||
import Weapon from "./../Weapon/Weapon";
|
import Weapon from "./../Weapon/Weapon";
|
||||||
import { Player, WeaponRaw, Side } from "csgogsi-socket";
|
import { Player, Side, WeaponRaw } from "csgogsi";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sides?: 'reversed',
|
sides?: "reversed";
|
||||||
show: boolean;
|
show: boolean;
|
||||||
side: 'CT' | 'T',
|
side: "CT" | "T";
|
||||||
players: Player[]
|
players: Player[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function utilityState(amount: number) {
|
function utilityState(amount: number) {
|
||||||
@@ -49,17 +48,30 @@ function utilityColor(amount: number) {
|
|||||||
|
|
||||||
function sum(grenades: WeaponRaw[], name: string) {
|
function sum(grenades: WeaponRaw[], name: string) {
|
||||||
return (
|
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 })
|
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
|
.ammo_reserve || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseGrenades(players: Player[], side: Side) {
|
function parseGrenades(players: Player[], side: Side) {
|
||||||
const grenades = players
|
const grenades = players
|
||||||
.filter(player => player.team.side === side)
|
.filter((player) => player.team.side === side)
|
||||||
.map(player => Object.values(player.weapons).filter(weapon => weapon.type === "Grenade"))
|
.map((player) =>
|
||||||
|
Object.values(player.weapons).filter((weapon) =>
|
||||||
|
weapon.type === "Grenade"
|
||||||
|
)
|
||||||
|
)
|
||||||
.flat()
|
.flat()
|
||||||
.map(grenade => ({ ...grenade, name: grenade.name.replace("weapon_", "") }));
|
.map((grenade) => ({
|
||||||
|
...grenade,
|
||||||
|
name: grenade.name.replace("weapon_", ""),
|
||||||
|
}));
|
||||||
return grenades;
|
return grenades;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,40 +81,44 @@ export function summarise(players: Player[], side: Side) {
|
|||||||
hg: sum(grenades, "hegrenade"),
|
hg: sum(grenades, "hegrenade"),
|
||||||
flashes: sum(grenades, "flashbang"),
|
flashes: sum(grenades, "flashbang"),
|
||||||
smokes: sum(grenades, "smokegrenade"),
|
smokes: sum(grenades, "smokegrenade"),
|
||||||
inc: sum(grenades, "incgrenade") + sum(grenades, "molotov")
|
inc: sum(grenades, "incgrenade") + sum(grenades, "molotov"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class GrenadeContainer extends React.PureComponent<{ grenade: string; amount: number }> {
|
const GrenadeContainer = (
|
||||||
render() {
|
{ grenade, amount }: { grenade: string; amount: number },
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className="grenade_container">
|
<div className="grenade_container">
|
||||||
<div className="grenade_image">
|
<div className="grenade_image">
|
||||||
<Weapon weapon={this.props.grenade} active={false} isGrenade />
|
<Weapon weapon={grenade} active={false} isGrenade />
|
||||||
</div>
|
</div>
|
||||||
<div className="grenade_amount">x{this.props.amount}</div>
|
<div className="grenade_amount">x{amount}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default class SideBox extends React.Component<Props> {
|
const SideBox = ({ players, side, show }: Props) => {
|
||||||
render() {
|
const grenades = summarise(players, side);
|
||||||
const grenades = summarise(this.props.players, this.props.side);
|
|
||||||
const total = Object.values(grenades).reduce((a, b) => a + b, 0);
|
const total = Object.values(grenades).reduce((a, b) => a + b, 0);
|
||||||
return (
|
return (
|
||||||
<div className={`utilitybox ${this.props.side || ''} ${this.props.show ? "show" : "hide"}`}>
|
<div className={`utilitybox ${side || ""} ${show ? "show" : "hide"}`}>
|
||||||
<div className="title_container">
|
<div className="title_container">
|
||||||
<div className="title">Utility Level - </div>
|
<div className="title">Utility Level - </div>
|
||||||
<div className="subtitle" style={{color: utilityColor(total)}}>{utilityState(total)}</div>
|
<div className="subtitle" style={{ color: utilityColor(total) }}>
|
||||||
|
{utilityState(total)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grenades_container">
|
<div className="grenades_container">
|
||||||
<GrenadeContainer grenade="smokegrenade" amount={grenades.smokes} />
|
<GrenadeContainer grenade="smokegrenade" amount={grenades.smokes} />
|
||||||
<GrenadeContainer grenade={this.props.side === 'CT' ? 'incgrenade' : 'molotov'} amount={grenades.inc} />
|
<GrenadeContainer
|
||||||
|
grenade={side === "CT" ? "incgrenade" : "molotov"}
|
||||||
|
amount={grenades.inc}
|
||||||
|
/>
|
||||||
<GrenadeContainer grenade="flashbang" amount={grenades.flashes} />
|
<GrenadeContainer grenade="flashbang" amount={grenades.flashes} />
|
||||||
<GrenadeContainer grenade="hegrenade" amount={grenades.hg} />
|
<GrenadeContainer grenade="hegrenade" amount={grenades.hg} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
export default SideBox;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,18 @@
|
|||||||
import React from "react";
|
import { MAX_TIMER, useBombTimer } from "./Countdown";
|
||||||
|
|
||||||
import { GSI } from "./../../App";
|
|
||||||
import BombTimer from "./Countdown";
|
|
||||||
import { C4 } from "./../../assets/Icons";
|
import { C4 } from "./../../assets/Icons";
|
||||||
|
|
||||||
export default class Bomb extends React.Component<any, { height: number; show: boolean }> {
|
const Bomb = () => {
|
||||||
constructor(props: any) {
|
const bombData = useBombTimer();
|
||||||
super(props);
|
const show = bombData.state === "planted" || bombData.state === "defusing";
|
||||||
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 (
|
return (
|
||||||
<div id={`bomb_container`}>
|
<div id={`bomb_container`}>
|
||||||
<div className={`bomb_timer ${this.state.show ? "show" : "hide"}`} style={{ height: `${this.state.height}%` }}></div>
|
<div className={`bomb_timer ${show ? "show" : "hide"}`} style={{ height: `${bombData.bombTime*100/MAX_TIMER.bomb}%` }}></div>
|
||||||
<div className={`bomb_icon ${this.state.show ? "show" : "hide"}`}>
|
<div className={`bomb_icon ${show ? "show" : "hide"}`}>
|
||||||
<C4 fill="white" />
|
<C4 fill="white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default Bomb;
|
||||||
@@ -1,46 +1,83 @@
|
|||||||
export default class Countdown {
|
import { Bomb, Events, Player } from "csgogsi";
|
||||||
last: number;
|
import { useEffect, useRef, useState } from "react";
|
||||||
time: number;
|
import { GSI } from "../../API/HUD";
|
||||||
step: (time: number) => void;
|
|
||||||
on: boolean;
|
|
||||||
resetFunc?: Function;
|
|
||||||
|
|
||||||
constructor(step: (time: number) => void){
|
export const MAX_TIMER = {
|
||||||
this.last = 0;
|
planting: 3,
|
||||||
this.time = 0;
|
defuse_kit: 5,
|
||||||
this.on = false;
|
defuse_nokit: 10,
|
||||||
this.step = step;
|
bomb: 40
|
||||||
}
|
}
|
||||||
onReset(func: Function) {
|
const findNewTime = (current: number, newTime: number) => Math.abs(current - newTime) > 2 ? newTime : current;
|
||||||
this.resetFunc = func;
|
|
||||||
}
|
export const useBombTimer = () => {
|
||||||
stepWrapper = (time: number) =>{
|
const [ player, setPlayerSteamId ] = useState<Player | null>(null);
|
||||||
if(this.time < 0) return this.reset();
|
const [ bombState, setBombState ] = useState<Bomb["state"] | null>(null);
|
||||||
if(!this.on) return this.reset();
|
const [ site, setBombSite ] = useState<string | null>(null);
|
||||||
if(!this.last) this.last = time;
|
|
||||||
if(this.time !== Number((this.time - (time - this.last)/1000))){
|
const [ plantTime, setPlantTime ] = useState(0);
|
||||||
this.time = Number((this.time - (time - this.last)/1000));
|
const [ bombTime, setBombTime ] = useState(0);
|
||||||
this.step(this.time);
|
const [ defuseTime, setDefuseTime ] = useState(0);
|
||||||
}
|
|
||||||
this.last =time;
|
// Inner loop logic
|
||||||
|
const previousTimeRef = useRef(0);
|
||||||
|
const rAFRef = useRef<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onData: Events["data"] = data => {
|
||||||
|
const { bomb } = data;
|
||||||
|
|
||||||
|
const bombPlayer = bomb?.player || null;
|
||||||
|
const state = bomb?.state || null;
|
||||||
|
const site = bomb?.site || null;
|
||||||
|
|
||||||
|
const countdown = bomb?.countdown || 0;
|
||||||
|
|
||||||
|
setPlayerSteamId(bombPlayer);
|
||||||
|
setBombState(state);
|
||||||
|
setBombSite(site);
|
||||||
|
|
||||||
|
const plantNewTime = state === "planting" ? countdown : 0;
|
||||||
|
const defuseNewTime = state === "defusing" ? countdown : 0;
|
||||||
|
|
||||||
|
|
||||||
if(this.last) requestAnimationFrame(this.stepWrapper)
|
setPlantTime(curr => findNewTime(curr, plantNewTime));
|
||||||
|
setDefuseTime(curr => findNewTime(curr, defuseNewTime));
|
||||||
|
setBombTime(p => state === "planted" ? findNewTime(p, countdown) : p);
|
||||||
}
|
}
|
||||||
|
|
||||||
go(duration: string | number){
|
GSI.on("data", onData);
|
||||||
//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);
|
|
||||||
|
|
||||||
|
const animationFrame = (time: number) => {
|
||||||
|
if(previousTimeRef.current){
|
||||||
|
const deltaTime = time - previousTimeRef.current;
|
||||||
|
const dTs = deltaTime/1000;
|
||||||
|
|
||||||
|
setPlantTime(p => p <= 0 ? 0 : p - dTs);
|
||||||
|
setDefuseTime(p => p <= 0 ? 0 : p - dTs);
|
||||||
|
setBombTime(p => p <= 0 ? 0 : p - dTs);
|
||||||
|
}
|
||||||
|
previousTimeRef.current = time;
|
||||||
|
rAFRef.current = requestAnimationFrame(animationFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(){
|
rAFRef.current = requestAnimationFrame(animationFrame);
|
||||||
this.last = 0;
|
|
||||||
this.time = 0;
|
return () => {
|
||||||
this.on = false;
|
GSI.off("data", onData);
|
||||||
if(this.resetFunc) this.resetFunc();
|
if(rAFRef.current) cancelAnimationFrame(rAFRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return ({
|
||||||
|
state: bombState,
|
||||||
|
player,
|
||||||
|
site,
|
||||||
|
defuseTime,
|
||||||
|
bombTime,
|
||||||
|
plantTime
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Timer } from "../MatchBar/MatchBar";
|
import { Timer } from "../MatchBar/MatchBar";
|
||||||
import { Player } from "csgogsi";
|
import { Player } from "csgogsi";
|
||||||
import * as I from "./../../assets/Icons";
|
import * as I from "./../../assets/Icons";
|
||||||
|
import { MAX_TIMER } from "./Countdown";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
timer: Timer | null;
|
timer: Timer | null;
|
||||||
side: "right" | "left"
|
side: "right" | "left"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Bomb extends React.Component<IProps> {
|
const getCaption = (type: "defusing" | "planting", player: Player | null) => {
|
||||||
getCaption = (type: "defusing" | "planting", player: Player | null) => {
|
|
||||||
if(!player) return null;
|
if(!player) return null;
|
||||||
if(type === "defusing"){
|
if(type === "defusing"){
|
||||||
return <>
|
return <>
|
||||||
@@ -23,19 +21,19 @@ export default class Bomb extends React.Component<IProps> {
|
|||||||
<div className={'T'}>{player.name} is planting the bomb</div>
|
<div className={'T'}>{player.name} is planting the bomb</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
render() {
|
const Bomb = ({ timer, side }: IProps) =>{
|
||||||
const { side, timer } = this.props;
|
if(!timer) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`defuse_plant_container ${side} ${timer && timer.active ? 'show' :'hide'}`}>
|
<div className={`defuse_plant_container ${side} ${timer && timer.active ? 'show' :'hide'}`}>
|
||||||
{
|
{
|
||||||
timer ?
|
timer ?
|
||||||
<div className={`defuse_plant_caption`}>
|
<div className={`defuse_plant_caption`}>
|
||||||
{this.getCaption(timer.type, timer.player)}
|
{getCaption(timer.type, timer.player)}
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className="defuse_plant_bar" style={{ width: `${(timer && timer.width) || 0}%` }}></div>
|
<div className="defuse_plant_bar" style={{ width: `${(timer.time * 100 / (timer.type === "planting" ? MAX_TIMER.planting : timer.player?.state.defusekit ? MAX_TIMER.defuse_kit : MAX_TIMER.defuse_nokit ))}%` }}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
export default Bomb;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import * as I from './../../API/types';
|
||||||
import * as I from './../../api/interfaces';
|
import { apiUrl } from '../../API';
|
||||||
|
|
||||||
interface MatchData {
|
interface MatchData {
|
||||||
left: { name: string; score: string | number; logo: string };
|
left: { name: string; score: string | number; logo: string };
|
||||||
@@ -11,43 +11,39 @@ interface Props {
|
|||||||
teams: I.Team[]
|
teams: I.Team[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Ladder extends React.Component<Props> {
|
const joinParents = (matchup: I.TournamentMatchup, matchups: I.TournamentMatchup[]) => {
|
||||||
joinParents = (matchup: I.TournamentMatchup, matchups: I.TournamentMatchup[]) => {
|
if (!matchup) return matchup;
|
||||||
const { tournament } = this.props;
|
|
||||||
if (!tournament || !matchup) return matchup;
|
|
||||||
|
|
||||||
if (matchup.parents.length) return matchup;
|
if (matchup.parents.length) return matchup;
|
||||||
|
|
||||||
const parents = matchups.filter(m => m.winner_to === matchup._id || m.loser_to === matchup._id);
|
const parents = matchups.filter(m => m.winner_to === matchup._id || m.loser_to === matchup._id);
|
||||||
if (!parents.length) return matchup;
|
if (!parents.length) return matchup;
|
||||||
matchup.parents.push(...parents.map(parent => this.joinParents(parent, matchups)));
|
matchup.parents.push(...parents.map(parent => joinParents(parent, matchups)));
|
||||||
|
|
||||||
return matchup;
|
return matchup;
|
||||||
};
|
};
|
||||||
|
|
||||||
copyMatchups = (): I.DepthTournamentMatchup[] => {
|
const copyMatchups = (currentMatchups: I.TournamentMatchup[]): I.DepthTournamentMatchup[] => {
|
||||||
if (!this.props.tournament) return [];
|
const matchups = JSON.parse(JSON.stringify(currentMatchups)) as I.DepthTournamentMatchup[];
|
||||||
const matchups = JSON.parse(JSON.stringify(this.props.tournament.matchups)) as I.DepthTournamentMatchup[];
|
|
||||||
return matchups;
|
return matchups;
|
||||||
};
|
};
|
||||||
|
|
||||||
setDepth = (matchups: I.DepthTournamentMatchup[], matchup: I.DepthTournamentMatchup, depth: number, force = false) => {
|
const setDepth = (matchups: I.DepthTournamentMatchup[], matchup: I.DepthTournamentMatchup, depth: number, force = false) => {
|
||||||
const getParents = (matchup: I.DepthTournamentMatchup) => {
|
const getParents = (matchup: I.DepthTournamentMatchup) => {
|
||||||
return matchups.filter(parent => parent.loser_to === matchup._id || parent.winner_to === matchup._id);
|
return matchups.filter(parent => parent.loser_to === matchup._id || parent.winner_to === matchup._id);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!matchup.depth || force) {
|
if (!matchup.depth || force) {
|
||||||
matchup.depth = depth;
|
matchup.depth = depth;
|
||||||
getParents(matchup).forEach(matchup => this.setDepth(matchups, matchup, depth + 1));
|
getParents(matchup).forEach(matchup => setDepth(matchups, matchup, depth + 1));
|
||||||
}
|
}
|
||||||
if (matchup.depth <= depth - 1) {
|
if (matchup.depth <= depth - 1) {
|
||||||
this.setDepth(matchups, matchup, depth - 1, true);
|
setDepth(matchups, matchup, depth - 1, true);
|
||||||
}
|
}
|
||||||
return matchup;
|
return matchup;
|
||||||
};
|
};
|
||||||
|
|
||||||
getMatch = (matchup: I.TournamentMatchup) => {
|
const getMatch = ({ matchup, matches, teams: allTeams}: { matches: I.Match[], teams: I.Team[], matchup: I.TournamentMatchup}) => {
|
||||||
const { matches } = this.props;
|
|
||||||
const matchData: MatchData = {
|
const matchData: MatchData = {
|
||||||
left: { name: 'TBD', score: '-', logo: '' },
|
left: { name: 'TBD', score: '-', logo: '' },
|
||||||
right: { name: 'TBD', score: '-', logo: '' }
|
right: { name: 'TBD', score: '-', logo: '' }
|
||||||
@@ -55,8 +51,8 @@ export default class Ladder extends React.Component<Props> {
|
|||||||
const match = matches.find(match => match.id === matchup.matchId);
|
const match = matches.find(match => match.id === matchup.matchId);
|
||||||
if (!match) return matchData;
|
if (!match) return matchData;
|
||||||
const teams = [
|
const teams = [
|
||||||
this.props.teams.find(team => team._id === match.left.id),
|
allTeams.find(team => team._id === match.left.id),
|
||||||
this.props.teams.find(team => team._id === match.right.id)
|
allTeams.find(team => team._id === match.right.id)
|
||||||
];
|
];
|
||||||
if (teams[0]) {
|
if (teams[0]) {
|
||||||
matchData.left.name = teams[0].name;
|
matchData.left.name = teams[0].name;
|
||||||
@@ -71,23 +67,23 @@ export default class Ladder extends React.Component<Props> {
|
|||||||
return matchData;
|
return matchData;
|
||||||
};
|
};
|
||||||
|
|
||||||
renderBracket = (
|
const Ladder = ({ tournament, matches, teams }: Props) => {
|
||||||
|
const renderBracket = (
|
||||||
matchup: I.DepthTournamentMatchup | null | undefined,
|
matchup: I.DepthTournamentMatchup | null | undefined,
|
||||||
depth: number,
|
depth: number,
|
||||||
fromChildId: string | undefined,
|
fromChildId: string | undefined,
|
||||||
childVisibleParents: number,
|
childVisibleParents: number,
|
||||||
isLast = false
|
isLast = false
|
||||||
) => {
|
) => {
|
||||||
const { tournament, matches } = this.props;
|
|
||||||
if (!matchup || !tournament) return null;
|
if (!matchup || !tournament) return null;
|
||||||
const match = this.getMatch(matchup);
|
const match = getMatch({ teams: teams, matches: matches, matchup});
|
||||||
|
|
||||||
if (fromChildId === matchup.loser_to) return null;
|
if (fromChildId === matchup.loser_to) return null;
|
||||||
const parentsToRender = matchup.parents.filter(matchupParent => matchupParent.loser_to !== matchup._id);
|
const parentsToRender = matchup.parents.filter(matchupParent => matchupParent.loser_to !== matchup._id);
|
||||||
if (matchup.depth > depth) {
|
if (matchup.depth > depth) {
|
||||||
return (
|
return (
|
||||||
<div className="empty-bracket">
|
<div className="empty-bracket">
|
||||||
{this.renderBracket(matchup, depth + 1, fromChildId, parentsToRender.length)}
|
{renderBracket(matchup, depth + 1, fromChildId, parentsToRender.length)}
|
||||||
<div className="connector"></div>
|
<div className="connector"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -97,8 +93,8 @@ export default class Ladder extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div className={`bracket depth-${depth}`}>
|
<div className={`bracket depth-${depth}`}>
|
||||||
<div className="parent-brackets">
|
<div className="parent-brackets">
|
||||||
{this.renderBracket(matchup.parents[0], depth + 1, matchup._id, parentsToRender.length)}
|
{renderBracket(matchup.parents[0], depth + 1, matchup._id, parentsToRender.length)}
|
||||||
{this.renderBracket(matchup.parents[1], depth + 1, matchup._id, parentsToRender.length)}
|
{renderBracket(matchup.parents[1], depth + 1, matchup._id, parentsToRender.length)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bracket-details">
|
<div className="bracket-details">
|
||||||
<div
|
<div
|
||||||
@@ -110,14 +106,14 @@ export default class Ladder extends React.Component<Props> {
|
|||||||
<div className={`match-details ${isCurrent ? 'current':''}`}>
|
<div className={`match-details ${isCurrent ? 'current':''}`}>
|
||||||
<div className="team-data">
|
<div className="team-data">
|
||||||
<div className="team-logo">
|
<div className="team-logo">
|
||||||
{match.left.logo ? <img src={match.left.logo} alt="Logo" /> : null}
|
{match.left.logo ? <img src={`${apiUrl}api/teams/logo/direct/${match.left.logo}`} alt="Logo" /> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="team-name">{match.left.name}</div>
|
<div className="team-name">{match.left.name}</div>
|
||||||
<div className="team-score">{match.left.score}</div>
|
<div className="team-score">{match.left.score}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="team-data">
|
<div className="team-data">
|
||||||
<div className="team-logo">
|
<div className="team-logo">
|
||||||
{match.right.logo ? <img src={match.right.logo} alt="Logo" /> : null}
|
{match.right.logo ? <img src={`${apiUrl}api/teams/logo/direct/${match.right.logo}`} alt="Logo" /> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="team-name">{match.right.name}</div>
|
<div className="team-name">{match.right.name}</div>
|
||||||
<div className="team-score">{match.right.score}</div>
|
<div className="team-score">{match.right.score}</div>
|
||||||
@@ -132,14 +128,13 @@ export default class Ladder extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const { tournament } = this.props;
|
|
||||||
if (!tournament) return null;
|
if (!tournament) return null;
|
||||||
const matchups = this.copyMatchups();
|
const matchups = copyMatchups(tournament.playoffs.matchups);
|
||||||
const gf = matchups.find(matchup => matchup.winner_to === null);
|
const gf = matchups.find(matchup => matchup.winner_to === null);
|
||||||
if (!gf) return null;
|
if (!gf) return null;
|
||||||
const joinedParents = this.joinParents(gf, matchups);
|
const joinedParents = joinParents(gf, matchups);
|
||||||
const matchupWithDepth = this.setDepth(matchups, joinedParents as I.DepthTournamentMatchup, 0);
|
const matchupWithDepth = setDepth(matchups, joinedParents as I.DepthTournamentMatchup, 0);
|
||||||
return this.renderBracket(matchupWithDepth, 0, undefined, 2, true);
|
return renderBracket(matchupWithDepth, 0, undefined, 2, true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Ladder;
|
||||||
@@ -1,45 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './tournament.scss';
|
import './tournament.scss';
|
||||||
import { actions } from '../../App';
|
import * as I from './../../API/types';
|
||||||
import * as I from './../../api/interfaces';
|
import api from './../../API';
|
||||||
import api from '../../api/api';
|
|
||||||
import Ladder from './Ladder';
|
import Ladder from './Ladder';
|
||||||
interface State {
|
import { useAction } from '../../API/contexts/actions';
|
||||||
tournament: I.Tournament | null,
|
import { useEffect, useState } from 'react';
|
||||||
teams: I.Team[],
|
|
||||||
matches: I.Match[],
|
const Tournament = () => {
|
||||||
show: boolean,
|
const [ show, setShow ] = useState(false);
|
||||||
}
|
const [ teams, setTeams ] = useState<I.Team[]>([]);
|
||||||
export default class Tournament extends React.Component<{}, State> {
|
const [ matches, setMatches ] = useState<I.Match[]>([]);
|
||||||
constructor(props: {}) {
|
const [ tournament, setTournament ] = useState<I.Tournament | null>(null);
|
||||||
super(props);
|
|
||||||
this.state = {
|
useAction("showTournament", (data) => {
|
||||||
tournament: null,
|
setShow(data === "show");
|
||||||
matches: [],
|
});
|
||||||
teams: [],
|
|
||||||
show: false
|
useEffect(() => {
|
||||||
}
|
api.tournaments.get().then(({ tournament }) => {
|
||||||
}
|
|
||||||
async componentDidMount() {
|
|
||||||
const { tournament } = await api.tournaments.get();
|
|
||||||
if(tournament){
|
if(tournament){
|
||||||
actions.on("showTournament", async (show: string) => {
|
setTournament(tournament);
|
||||||
if(show !== "show"){
|
|
||||||
return this.setState({show: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({tournament}, () => {
|
Promise.allSettled([api.match.get(), api.teams.get()]).then(([matches, teams]) =>{
|
||||||
this.setState({show:true})
|
setTeams(teams.status === "fulfilled" ? teams.value : []);
|
||||||
});
|
setMatches(matches.status === "fulfilled" ? matches.value : []);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
Promise.all([api.match.get(), api.teams.get()]).then(([matches, teams]) =>{
|
|
||||||
this.setState({matches, teams});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
const { tournament, matches, teams, show } = this.state;
|
|
||||||
if(!tournament) return null;
|
if(!tournament) return null;
|
||||||
return (
|
return (
|
||||||
<div className={`ladder-container ${show ? 'show':''}`}>
|
<div className={`ladder-container ${show ? 'show':''}`}>
|
||||||
@@ -59,4 +48,4 @@ export default class Tournament extends React.Component<{}, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export default React.memo(Tournament);
|
||||||
@@ -1,43 +1,26 @@
|
|||||||
import React from 'react';
|
|
||||||
import './trivia.scss';
|
import './trivia.scss';
|
||||||
|
import { useAction, useConfig } from '../../API/contexts/actions';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {configs, actions} from './../../App';
|
const Trivia = () => {
|
||||||
|
const [ show, setShow ] = useState(false);
|
||||||
|
|
||||||
export default class Trivia extends React.Component<any, { title: string, content: string, show: boolean }> {
|
const data = useConfig('trivia');
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
title:'Title',
|
|
||||||
content:'Content',
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useAction('triviaState', (state) => {
|
||||||
configs.onChange((data:any) => {
|
setShow(state === "show");
|
||||||
if(!data) return;
|
});
|
||||||
const trivia = data.trivia;
|
|
||||||
if(!trivia) return;
|
useAction('toggleCams', () => {
|
||||||
|
setShow(p => !p);
|
||||||
if(trivia.title && trivia.content){
|
|
||||||
this.setState({title:trivia.title, content:trivia.content})
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
actions.on("triviaState", (state: any) => {
|
|
||||||
this.setState({show: state === "show"})
|
|
||||||
});
|
|
||||||
actions.on("toggleTrivia", () => {
|
|
||||||
this.setState({show: !this.state.show})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<div className={`trivia_container ${this.state.show ? 'show': 'hide'}`}>
|
<div className={`trivia_container ${show ? 'show': 'hide'}`}>
|
||||||
<div className="title">{this.state.title}</div>
|
<div className="title">{data?.title}</div>
|
||||||
<div className="content">{this.state.content}</div>
|
<div className="content">{data?.content}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export default React.memo(Trivia);
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import * as Weapons from './../../assets/Weapons';
|
import * as Weapons from "./../../assets/Weapons";
|
||||||
|
|
||||||
interface IProps extends React.SVGProps<SVGSVGElement> {
|
interface IProps extends React.SVGProps<SVGSVGElement> {
|
||||||
weapon: string,
|
weapon: string;
|
||||||
active: boolean,
|
active: boolean;
|
||||||
isGrenade?: boolean
|
isGrenade?: boolean;
|
||||||
}
|
}
|
||||||
export default class WeaponImage extends React.Component<IProps> {
|
const WeaponImage = ({ weapon, active, isGrenade, ...rest }: IProps) => {
|
||||||
render() {
|
const weaponId = weapon.replace("weapon_", "");
|
||||||
const { weapon, active, isGrenade, ...rest } = this.props;
|
const Weapon = (Weapons as any)[weaponId];
|
||||||
const Weapon = (Weapons as any)[weapon];
|
|
||||||
const { className, ...svgProps } = rest;
|
const { className, ...svgProps } = rest;
|
||||||
if (!Weapon) return null;
|
if (!Weapon) return null;
|
||||||
return (
|
return (
|
||||||
<Weapon fill="white" className={`${active ? 'active':''} weapon ${isGrenade ? 'grenade' : ''} ${className || ''}`} {...svgProps} />
|
<Weapon
|
||||||
|
fill="white"
|
||||||
|
className={`${active ? "active" : ""} weapon ${
|
||||||
|
isGrenade ? "grenade" : ""
|
||||||
|
} ${className || ""}`}
|
||||||
|
{...svgProps}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
export default React.memo(WeaponImage);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
export default class ActionManager {
|
|
||||||
listeners: Map<string, Function[]>;
|
|
||||||
|
|
||||||
constructor(){
|
|
||||||
this.listeners = new Map();
|
|
||||||
|
|
||||||
/*this.on('data', _data => {
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
execute(eventName: string, argument?: any){
|
|
||||||
const listeners = this.listeners.get(eventName);
|
|
||||||
if(!listeners) return false;
|
|
||||||
listeners.forEach(callback => {
|
|
||||||
if(argument) callback(argument);
|
|
||||||
else callback();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
on(eventName: string, listener: Function){
|
|
||||||
const listOfListeners = this.listeners.get(eventName) || [];
|
|
||||||
listOfListeners.push(listener);
|
|
||||||
this.listeners.set(eventName, listOfListeners);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class ConfigManager {
|
|
||||||
listeners: Function[];
|
|
||||||
data: any;
|
|
||||||
|
|
||||||
constructor(){
|
|
||||||
this.listeners = [];
|
|
||||||
this.data = {};
|
|
||||||
}
|
|
||||||
save(data: any){
|
|
||||||
this.data = data;
|
|
||||||
this.execute();
|
|
||||||
|
|
||||||
/*const listeners = this.listeners.get(eventName);
|
|
||||||
if(!listeners) return false;
|
|
||||||
listeners.forEach(callback => {
|
|
||||||
if(argument) callback(argument);
|
|
||||||
else callback();
|
|
||||||
});
|
|
||||||
return true;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(){
|
|
||||||
const listeners = this.listeners;
|
|
||||||
if(!listeners || !listeners.length) return false;
|
|
||||||
listeners.forEach(listener => {
|
|
||||||
listener(this.data);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(listener: Function){
|
|
||||||
const listOfListeners = this.listeners || [];
|
|
||||||
listOfListeners.push(listener);
|
|
||||||
this.listeners = listOfListeners;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import * as I from './interfaces';
|
|
||||||
import queryString from 'query-string';
|
|
||||||
import { MapConfig } from '../HUD/Radar/LexoRadar/maps';
|
|
||||||
|
|
||||||
|
|
||||||
const query = queryString.parseUrl(window.location.href).query;
|
|
||||||
export const variant = query?.variant || "default";
|
|
||||||
|
|
||||||
export const port = (query && Number(query.port)) || 1349;
|
|
||||||
|
|
||||||
export const isDev = !query.isProd;
|
|
||||||
|
|
||||||
export const config = {apiAddress:isDev ? `http://localhost:${port}/` : '/'}
|
|
||||||
export const apiUrl = config.apiAddress;
|
|
||||||
|
|
||||||
export async function apiV2(url: string, method = 'GET', body?: any) {
|
|
||||||
const options: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
||||||
}
|
|
||||||
if (body) {
|
|
||||||
options.body = JSON.stringify(body)
|
|
||||||
}
|
|
||||||
let data: any = null;
|
|
||||||
return fetch(`${apiUrl}api/${url}`, options)
|
|
||||||
.then(res => {
|
|
||||||
data = res;
|
|
||||||
return res.json().catch(_e => data && data.status < 300)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
match: {
|
|
||||||
get: async (): Promise<I.Match[]> => apiV2(`match`),
|
|
||||||
getCurrent: async (): Promise<I.Match> => apiV2(`match/current`)
|
|
||||||
},
|
|
||||||
camera: {
|
|
||||||
get: (): Promise<{ availablePlayers: ({steamid:string, label: string})[], uuid: string }> => apiV2('camera')
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
|
|
||||||
get: (): Promise<I.Team[]> => apiV2(`teams`),
|
|
||||||
},
|
|
||||||
players: {
|
|
||||||
get: async (steamids?: string[]): Promise<I.Player[]> => apiV2(steamids ? `players?steamids=${steamids.join(';')}` :`players`),
|
|
||||||
getAvatarURLs: async (steamid: string): Promise<{custom: string, steam: string}> => apiV2(`players/avatar/steamid/${steamid}`)
|
|
||||||
},
|
|
||||||
tournaments: {
|
|
||||||
get: () => apiV2('tournament')
|
|
||||||
},
|
|
||||||
maps: {
|
|
||||||
get: (): Promise<{ [key: string] : MapConfig}> => apiV2('radar/maps')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default api;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import api from './api';
|
|
||||||
interface AvatarLoader {
|
|
||||||
loader: Promise<string>,
|
|
||||||
url: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const avatars: { [key: string]: AvatarLoader } = {};
|
|
||||||
|
|
||||||
export const loadAvatarURL = (steamid: string) => {
|
|
||||||
if(!steamid) return;
|
|
||||||
if(avatars[steamid]) return avatars[steamid].url;
|
|
||||||
avatars[steamid] = {
|
|
||||||
url: '',
|
|
||||||
loader: new Promise((resolve) => {
|
|
||||||
api.players.getAvatarURLs(steamid).then(result => {
|
|
||||||
avatars[steamid].url = result.custom || result.steam;
|
|
||||||
resolve(result.custom || result.custom);
|
|
||||||
}).catch(() => {
|
|
||||||
delete avatars[steamid];
|
|
||||||
resolve('');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
import { ReactComponent as ArmorFull } from './../assets/images/icon_armor_full_default.svg';
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
import { ReactComponent as ArmorHalf } from './../assets/images/icon_armor_half_default.svg';
|
import ArmorFull from './../assets/images/icon_armor_full_default.svg?react';
|
||||||
import { ReactComponent as ArmorHalfHelmet } from './../assets/images/icon_armor_half_helmet_default.svg';
|
import ArmorHalf from './../assets/images/icon_armor_half_default.svg?react';
|
||||||
import { ReactComponent as ArmorHelmet } from './../assets/images/icon_armor_helmet_default.svg';
|
import ArmorHalfHelmet from './../assets/images/icon_armor_half_helmet_default.svg?react';
|
||||||
import { ReactComponent as ArmorNone } from './../assets/images/icon_armor_none_default.svg';
|
import ArmorHelmet from './../assets/images/icon_armor_helmet_default.svg?react';
|
||||||
import { ReactComponent as Blind } from './../assets/images/icon_blind.svg';
|
import ArmorNone from './../assets/images/icon_armor_none_default.svg?react';
|
||||||
import { ReactComponent as Bomb } from './../assets/images/icon_bomb_default.svg';
|
import Blind from './../assets/images/icon_blind.svg?react';
|
||||||
import { ReactComponent as BombExplosion } from './../assets/images/icon_bomb_explosion_default.svg';
|
import Bomb from './../assets/images/icon_bomb_default.svg?react';
|
||||||
import { ReactComponent as Bullets } from './../assets/images/icon_bullets_default.svg';
|
import BombExplosion from './../assets/images/icon_bomb_explosion_default.svg?react';
|
||||||
import { ReactComponent as Burning } from './../assets/images/icon_burning.svg';
|
import Bullets from './../assets/images/icon_bullets_default.svg?react';
|
||||||
import { ReactComponent as C4 } from './../assets/images/icon_c4_default.svg';
|
import Burning from './../assets/images/icon_burning.svg?react';
|
||||||
import { ReactComponent as Defuse } from './../assets/images/icon_defuse_default.svg';
|
import C4 from './../assets/images/icon_c4_default.svg?react';
|
||||||
import { ReactComponent as Health } from './../assets/images/icon_health_default.svg';
|
import Defuse from './../assets/images/icon_defuse_default.svg?react';
|
||||||
import { ReactComponent as HealthFull } from './../assets/images/icon_health_full_default.svg';
|
import Health from './../assets/images/icon_health_default.svg?react';
|
||||||
import { ReactComponent as Hourglass } from './../assets/images/icon_hourglass_default.svg';
|
import HealthFull from './../assets/images/icon_health_full_default.svg?react';
|
||||||
import { ReactComponent as Microphone } from './../assets/images/icon_microphone.svg';
|
import Hourglass from './../assets/images/icon_hourglass_default.svg?react';
|
||||||
import { ReactComponent as Pause } from './../assets/images/icon_pause_default.svg';
|
import Microphone from './../assets/images/icon_microphone.svg?react';
|
||||||
import { ReactComponent as Skull } from './../assets/images/icon_skull_default.svg';
|
import Pause from './../assets/images/icon_pause_default.svg?react';
|
||||||
import { ReactComponent as Timer } from './../assets/images/icon_timer_default.svg';
|
import Skull from './../assets/images/icon_skull_default.svg?react';
|
||||||
import { ReactComponent as FlashedKill } from './../assets/images/flashed_kill.svg';
|
import Timer from './../assets/images/icon_timer_default.svg?react';
|
||||||
import { ReactComponent as Headshot } from './../assets/images/headshot.svg';
|
import FlashedKill from './../assets/images/flashed_kill.svg?react';
|
||||||
import { ReactComponent as NoScope } from './../assets/images/noscope.svg';
|
import Headshot from './../assets/images/headshot.svg?react';
|
||||||
import { ReactComponent as SmokeKill } from './../assets/images/smoke_kill.svg';
|
import NoScope from './../assets/images/noscope.svg?react';
|
||||||
import { ReactComponent as Suicide } from './../assets/images/suicide.svg';
|
import SmokeKill from './../assets/images/smoke_kill.svg?react';
|
||||||
import { ReactComponent as Wallbang } from './../assets/images/wallbang.svg';
|
import Suicide from './../assets/images/suicide.svg?react';
|
||||||
|
import Wallbang from './../assets/images/wallbang.svg?react';
|
||||||
import LogoCT from './../assets/images/logo_CT_default.png';
|
import LogoCT from './../assets/images/logo_CT_default.png';
|
||||||
import LogoT from './../assets/images/logo_T_default.png';
|
import LogoT from './../assets/images/logo_T_default.png';
|
||||||
import { ReactComponent as SmallBomb } from "./../assets/images/bomb.svg";
|
import SmallBomb from "./../assets/images/bomb.svg?react";
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SmallBomb,
|
SmallBomb,
|
||||||
|
|||||||