mirror of
https://github.com/lexogrine/cs2-react-hud.git
synced 2025-12-10 10:52:50 +01:00
Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
1
.env.development
Normal file
1
.env.development
Normal file
@ -0,0 +1 @@
|
||||
PUBLIC_URL=/dev/
|
||||
@ -1,18 +0,0 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
44
.gitignore
vendored
44
.gitignore
vendored
@ -1,26 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
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
|
||||
49
OpenBrowserPlugin.js
Normal file
49
OpenBrowserPlugin.js
Normal file
@ -0,0 +1,49 @@
|
||||
const open = require('open');
|
||||
|
||||
module.exports = class OpenBrowser {
|
||||
constructor(options) {
|
||||
if (typeof options === 'string') {
|
||||
this.options = Object.assign(
|
||||
{
|
||||
hasOpen: false
|
||||
},
|
||||
{
|
||||
url: options
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.options = Object.assign(
|
||||
{
|
||||
port: 8080,
|
||||
host: 'localhost',
|
||||
protocol: 'http:',
|
||||
hasOpen: false
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
const options = this.options;
|
||||
let url;
|
||||
let hasOpen = options.hasOpen;
|
||||
if (options.protocol && !options.protocol.endsWith(':')) options.protocol += ':';
|
||||
if (options.url) url = options.url;
|
||||
else url = `${options.protocol}//${options.host}:${options.port}`;
|
||||
if (compiler.hooks) {
|
||||
compiler.hooks.afterEmit.tap('openBrowser', () => {
|
||||
if (!hasOpen) open(url);
|
||||
hasOpen = true;
|
||||
this.options.hasOpen = true;
|
||||
});
|
||||
} else {
|
||||
compiler.plugin('after-emit', (c, cb) => {
|
||||
if (!hasOpen) open(url);
|
||||
hasOpen = true;
|
||||
this.options.hasOpen = true;
|
||||
return cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
235
README.md
235
README.md
@ -1,148 +1,134 @@
|
||||
### **CS2 React HUD for [LHM.gg](https://LHM.gg)**
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
It comes with a set of default options and features that you can use for creating your unique esport experience.
|
||||
# Lexogrine HUD
|
||||
|
||||
#### **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
|
||||
- Keybinds
|
||||
- Killfeed
|
||||
- Player cam feed
|
||||
- Custom Radar
|
||||
|
||||
**Left Alt + C**
|
||||
Toggles camera feed
|
||||
## Keybinds:
|
||||
### **Left Alt + S**
|
||||
>Makes radar smaller by 20px;
|
||||
### **Left Alt + B**
|
||||
>Makes radar bigger by 20px;
|
||||
### **Left Alt + T**
|
||||
>Shows trivia box
|
||||
### **Left Alt + M**
|
||||
>Toggles upcoming match box
|
||||
### **Left Alt + P**
|
||||
>Toggles player preview
|
||||
### **Left Alt + C**
|
||||
>Toggles camera feed
|
||||
### **Left Ctrl + B**
|
||||
>Make radar invisible
|
||||
|
||||
#### **Panel**
|
||||
## **Panel**
|
||||
## Trivia settings
|
||||
|
||||
LHM HUDs can be configured in HUD Settings when opened in LHM. The schema for this configuration panel is available in `/public/panel.json`.
|
||||
| Field|Description |
|
||||
|--|--|
|
||||
| Trivia title| `Text` |
|
||||
| Trivia content| `Text` |
|
||||
|
||||
#### Trivia settings
|
||||
|
||||
| Field | Description |
|
||||
| -------------- | ----------- |
|
||||
| Trivia title | `Text` |
|
||||
| Trivia content | `Text` |
|
||||
## Display settings
|
||||
|
||||
#### Display settings
|
||||
|
||||
| Field | Description |
|
||||
| --------------------------- | ------------ |
|
||||
| Left/right box's title | `Text` |
|
||||
| Left/right box's subtitle | `Text` |
|
||||
| Left/right box's image logo | `Image file` |
|
||||
| Field|Description |
|
||||
|--|--|
|
||||
| Left/right box's title| `Text` |
|
||||
| Left/right box's title| `Text` |
|
||||
| Left/right box's image logo| `Image file` |
|
||||
|
||||
#### **Preview**
|
||||
## Example settings
|
||||
|
||||

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

|
||||
|
||||
#### **Download**
|
||||
# Download
|
||||
|
||||
To download it, simply click here: [**DOWNLOAD CS React HUD for LHM.gg**](https://lhm.gg/download?target=cs2)
|
||||
To download it just click here: [DOWNLOAD HUD](https://github.com/lexogrine/csgo-react-hud/releases/latest)
|
||||
|
||||
#### **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 CS:GO data to the HUD.
|
||||
|
||||
##### **Setting up**
|
||||
## Identifying HUD
|
||||
In `/public` directory edit hud.json so it fits you - fill HUD's name, author, version, specify the radar and killfeed functionalities. At the end replace the thumb.png with your icon :)
|
||||
|
||||
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.
|
||||
## Building & distributing
|
||||
To build version to distribute and move around, in the root directory run `npm run pack`. It will create the zip file for distribution. Now you can just drag and drop this file into the HUD Managers upload area.
|
||||
|
||||
##### **Identifying HUD**
|
||||
## Signing
|
||||
|
||||
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 :)
|
||||
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.
|
||||
|
||||
##### **Building & distributing**
|
||||
|
||||
## File structure
|
||||
The HUD is seperated into two parts - the API part, that connects to the HUD Manager API and communicate with it: `src/App.tsx` file and `src/api` directory. Usually, you don't want to play with it, so the whole runs without a problem.
|
||||
The second part is the render part - `src/HUD`, `src/fonts` and `src/assets` are the directories you want to modify. In the `src/HUD` each element of the HUD is seperated into its own folder. Styles are kept in the `src/HUD/styles`. Names are quite self-explanatory, and to modify style of the element you should just find the styling by the file and class name.
|
||||
|
||||
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.
|
||||
|
||||
##### **Signing**
|
||||
|
||||
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`.
|
||||
|
||||
##### **File structure**
|
||||
|
||||
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.
|
||||
|
||||
##### **panel.json API**
|
||||
|
||||
To get the incoming data from the LHM.gg, let's take a look at the `src/HUD/SideBoxes/SideBox.tsx` component:
|
||||
|
||||
```typescript
|
||||
const Sidebox = ({ side, hide }: { side: "left" | "right"; hide: boolean }) => {
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
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");
|
||||
## `panel.json` API
|
||||
To get the incoming data from the HUD Manager, let's take a look at the `src/HUD/SideBoxes/SideBox.tsx` `componentDidMount()` method:
|
||||
```javascript
|
||||
import {configs} from './../../App';
|
||||
...
|
||||
configs.onChange((data:any) => {
|
||||
if(!data) return;
|
||||
|
||||
const display = data.display_settings;
|
||||
|
||||
if(!display) return;
|
||||
|
||||
if(display[`${this.props.side}_title`]){
|
||||
this.setState({title:display[`${this.props.side}_title`]})
|
||||
}
|
||||
if(display[`${this.props.side}_subtitle`]){
|
||||
this.setState({subtitle:display[`${this.props.side}_subtitle`]})
|
||||
}
|
||||
if(display[`${this.props.side}_image`]){
|
||||
this.setState({image:display[`${this.props.side}_image`]})
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
For the action input, we need to import the `actions` object and create a listener with the parameter on it.
|
||||
|
||||
##### **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);
|
||||
},
|
||||
[]
|
||||
);
|
||||
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**
|
||||
|
||||
Listening for kills is very easy - we can see that in `src/HUD/Killfeed/Killfeed.tsx`:
|
||||
|
||||
## Killfeed
|
||||
Because our `csgogsi` has the ability to process input from HLAE's MIRV, listening for kills is very easy. We can see than in `src/HUD/Killfeed/Killfeed.tsx`:
|
||||
```javascript
|
||||
componentDidMount() {
|
||||
GSI.on("kill", kill => {
|
||||
@ -150,17 +136,8 @@ 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.
|
||||
|
||||
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**
|
||||
|
||||
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.
|
||||
## Radar
|
||||
Radar is custom React-based component, made by Hubert Walczak, and is easily editable from css.
|
||||
45
craco.config.js
Normal file
45
craco.config.js
Normal file
@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const homedir = require('os').homedir();
|
||||
const internalIp = require('internal-ip');
|
||||
const OpenBrowserPlugin = require('./OpenBrowserPlugin');
|
||||
|
||||
const pathToConfig = path.join(process.env.APPDATA || path.join(homedir, '.config'), 'hud-manager', 'databases', 'config');
|
||||
let port = 1349;
|
||||
|
||||
const getPort = () => {
|
||||
if(!fs.existsSync(pathToConfig)){
|
||||
console.warn('LHM Config file unavailable');
|
||||
return port;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(pathToConfig, 'utf-8'));
|
||||
|
||||
if(!config.port){
|
||||
console.warn('LHM Port unavailable');
|
||||
}
|
||||
|
||||
console.warn('LHM Port detected as', config.port);
|
||||
return config.port;
|
||||
|
||||
} catch {
|
||||
console.warn('LHM Config file invalid');
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
port = getPort();
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: 3500,
|
||||
open: false
|
||||
},
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
webpackConfig.plugins.push(new OpenBrowserPlugin({ url: `http://${internalIp.v4.sync()}:${port}/development/`}))
|
||||
return webpackConfig;
|
||||
}
|
||||
}
|
||||
};
|
||||
16
index.html
16
index.html
@ -1,16 +0,0 @@
|
||||
<!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>
|
||||
4750
package-lock.json
generated
4750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@ -1,47 +1,60 @@
|
||||
{
|
||||
"name": "cs2-react-hud",
|
||||
"name": "lexogrine_cs2_hud",
|
||||
"version": "1.0.0",
|
||||
"homepage": "./",
|
||||
"private": true,
|
||||
"version": "1.0.6",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@craco/craco": "^5.7.0",
|
||||
"@types/jest": "24.0.19",
|
||||
"@types/node": "16.11.20",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/simple-peer": "^9.11.3",
|
||||
"buffer": "^6.0.3",
|
||||
"csgogsi-socket": "^2.7.1",
|
||||
"query-string": "^6.12.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"simple-peer": "^9.11.0",
|
||||
"simple-websockets": "^1.1.0",
|
||||
"typescript": "^4.5.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"zip": "npm-build-zip",
|
||||
"dev": "vite --host",
|
||||
"start": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"start": "craco start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"csgogsi": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"simple-peer": "^9.11.1",
|
||||
"simple-websockets": "^1.2.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"uuid": "^9.0.1"
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/simple-peer": "^9.11.5",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"eslint": "^8.45.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"
|
||||
"@types/history": "^4.7.5",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"internal-ip": "^6.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"npm-build-zip": "^1.0.2",
|
||||
"open": "^8.0.2",
|
||||
"sass": "^1.32.5",
|
||||
"socket.io-client": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name":"LHM CS2 Default HUD",
|
||||
"version":"1.0.6",
|
||||
"name":"Lexogrine CS2 HUD",
|
||||
"version":"1.0.0",
|
||||
"author":"Lexogrine",
|
||||
"legacy": false,
|
||||
"radar": true,
|
||||
"killfeed": false,
|
||||
"game":"cs2",
|
||||
"boltobserv":{
|
||||
"css":false,
|
||||
"maps":false
|
||||
"css":true,
|
||||
"maps":true
|
||||
}
|
||||
}
|
||||
23
public/index.html
Normal file
23
public/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@ -2,13 +2,5 @@
|
||||
{
|
||||
"bind":"Alt+C",
|
||||
"action":"toggleCams"
|
||||
},
|
||||
{
|
||||
"bind":"Alt+V",
|
||||
"action":"radarBigger"
|
||||
},
|
||||
{
|
||||
"bind":"Alt+B",
|
||||
"action":"radarSmaller"
|
||||
}
|
||||
]
|
||||
@ -63,21 +63,6 @@
|
||||
"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",
|
||||
|
||||
48
public/radar.css
Normal file
48
public/radar.css
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Use this file to apply custom styles to the radar image
|
||||
*/
|
||||
|
||||
/* Any player dot */
|
||||
div.dot {}
|
||||
|
||||
/* Players that are CT or T */
|
||||
div.dot.CT {}
|
||||
div.dot.T {}
|
||||
|
||||
/* The player with the bomb */
|
||||
div.dot.bomb{}
|
||||
|
||||
/* The player currently being observed */
|
||||
div.dot.active {}
|
||||
|
||||
/* A dead player */
|
||||
div.dot.dead {}
|
||||
|
||||
/* The number on a player dot */
|
||||
div.label {}
|
||||
|
||||
/* The number on the dot being spectated */
|
||||
div.label.active {}
|
||||
|
||||
/* The dropped or planted bomb on the map */
|
||||
#bomb {
|
||||
height: 4vmin;
|
||||
width: 4vmin;
|
||||
background-repeat:no-repeat;
|
||||
}
|
||||
|
||||
/* Smoke circles on the map */
|
||||
#smokes {
|
||||
display: none;
|
||||
}
|
||||
#smokes > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Inferno circles on the map */
|
||||
.inferno > div {}
|
||||
|
||||
/* The advisory on screen */
|
||||
#advisory {
|
||||
display: none !important;
|
||||
}
|
||||
13
sign.js
13
sign.js
@ -1,10 +1,7 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __esm_dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const sign = () => {
|
||||
const getAllFilesToSign = (hudDir) => {
|
||||
@ -20,7 +17,7 @@ const sign = () => {
|
||||
return files;
|
||||
}
|
||||
|
||||
const dir = path.join(__esm_dirname, 'build');
|
||||
const dir = path.join(__dirname, 'build');
|
||||
|
||||
const keyFile = path.join(dir, 'key');
|
||||
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
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('');
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
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];
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
export const keybindDefinition = [
|
||||
{
|
||||
"bind": "Alt+C",
|
||||
"action": "toggleCams"
|
||||
},
|
||||
{
|
||||
"bind": "Alt+V",
|
||||
"action": "radarBigger"
|
||||
},
|
||||
{
|
||||
"bind": "Alt+B",
|
||||
"action": "radarSmaller"
|
||||
}
|
||||
] as const;
|
||||
@ -1,71 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
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;
|
||||
@ -1,51 +0,0 @@
|
||||
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 }>;
|
||||
@ -1,69 +0,0 @@
|
||||
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);
|
||||
})
|
||||
56
src/App.css
56
src/App.css
@ -1,56 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
282
src/App.tsx
282
src/App.tsx
@ -1,83 +1,215 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
import { CSGO } from 'csgogsi'
|
||||
import { SettingsProvider, onGSI } from './API/contexts/actions'
|
||||
import React from 'react';
|
||||
import Layout from './HUD/Layout/Layout';
|
||||
import './API/socket';
|
||||
import { Match } from './API/types';
|
||||
import api from './API';
|
||||
import { GSI } from './API/HUD';
|
||||
import { socket } from './API/socket';
|
||||
import api, { port, isDev } from './api/api';
|
||||
import { loadAvatarURL } from './api/avatars';
|
||||
import ActionManager, { ConfigManager } from './api/actionManager';
|
||||
|
||||
function App() {
|
||||
const [ game, setGame ] = useState<CSGO | null>(null);
|
||||
const [ match, setMatch ] = useState<Match | null>(null);
|
||||
import { CSGO, PlayerExtension, GSISocket, CSGORaw } from "csgogsi-socket";
|
||||
import { Match } from './api/interfaces';
|
||||
import { initiateConnection } from './HUD/Camera/mediaStream';
|
||||
|
||||
useEffect(() => {
|
||||
const onMatchPing = () => {
|
||||
api.match.getCurrent().then(match => {
|
||||
if (!match) {
|
||||
GSI.teams.left = null;
|
||||
GSI.teams.right = null;
|
||||
setMatch(null);
|
||||
return;
|
||||
}
|
||||
setMatch(match);
|
||||
|
||||
let isReversed = false;
|
||||
if (GSI.last) {
|
||||
const mapName = GSI.last.map.name.substring(GSI.last.map.name.lastIndexOf('/') + 1);
|
||||
const current = match.vetos.filter(veto => veto.mapName === mapName)[0];
|
||||
if (current && current.reverseSide) {
|
||||
isReversed = true;
|
||||
}
|
||||
}
|
||||
if (match.left.id) {
|
||||
api.teams.getOne(match.left.id).then(left => {
|
||||
const gsiTeamData = { id: left._id, name: left.name, country: left.country, logo: left.logo, map_score: match.left.wins, extra: left.extra };
|
||||
|
||||
if (!isReversed) {
|
||||
GSI.teams.left = gsiTeamData;
|
||||
}
|
||||
else GSI.teams.right = gsiTeamData;
|
||||
});
|
||||
}
|
||||
if (match.right.id) {
|
||||
api.teams.getOne(match.right.id).then(right => {
|
||||
const gsiTeamData = { id: right._id, name: right.name, country: right.country, logo: right.logo, map_score: match.right.wins, extra: right.extra };
|
||||
|
||||
if (!isReversed) GSI.teams.right = gsiTeamData;
|
||||
else GSI.teams.left = gsiTeamData;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}).catch(() => {
|
||||
GSI.teams.left = null;
|
||||
GSI.teams.right = null;
|
||||
setMatch(null);
|
||||
});
|
||||
}
|
||||
socket.on("match", onMatchPing);
|
||||
onMatchPing();
|
||||
let isInWindow = !!window.parent.ipcApi;
|
||||
|
||||
return () => {
|
||||
socket.off("match", onMatchPing);
|
||||
}
|
||||
}, [])
|
||||
export const { GSI, socket } = GSISocket(isDev ? `localhost:${port}` : '/', "update");
|
||||
|
||||
onGSI('data', game => {
|
||||
GSI.regulationMR = 12;
|
||||
|
||||
setGame(game);
|
||||
}, []);
|
||||
|
||||
if (!game) return null;
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<Layout game={game} match={match} />
|
||||
</SettingsProvider>
|
||||
);
|
||||
if(isInWindow){
|
||||
window.parent.ipcApi.receive('raw', (data: CSGORaw, damage?: RoundDamage[]) => {
|
||||
if(damage){
|
||||
GSI.damage = damage;
|
||||
}
|
||||
GSI.digest(data);
|
||||
});
|
||||
}
|
||||
|
||||
export default App
|
||||
type RoundPlayerDamage = {
|
||||
steamid: string;
|
||||
damage: number;
|
||||
};
|
||||
type RoundDamage = {
|
||||
round: number;
|
||||
players: RoundPlayerDamage[];
|
||||
};
|
||||
|
||||
socket.on('update', (_csgo: any, damage?: RoundDamage[]) => {
|
||||
if(damage) GSI.damage = damage;
|
||||
});
|
||||
|
||||
export const actions = new ActionManager();
|
||||
export const configs = new ConfigManager();
|
||||
|
||||
export const hudIdentity = {
|
||||
name: '',
|
||||
isDev: false
|
||||
};
|
||||
|
||||
interface DataLoader {
|
||||
match: Promise<void> | null
|
||||
}
|
||||
|
||||
const dataLoader: DataLoader = {
|
||||
match: null
|
||||
}
|
||||
|
||||
class App extends React.Component<any, { match: Match | null, game: CSGO | null, steamids: string[], checked: boolean }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
game: null,
|
||||
steamids: [],
|
||||
match: null,
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
|
||||
verifyPlayers = async (game: CSGO) => {
|
||||
const steamids = game.players.map(player => player.steamid);
|
||||
steamids.forEach(steamid => {
|
||||
loadAvatarURL(steamid);
|
||||
})
|
||||
|
||||
if (steamids.every(steamid => this.state.steamids.includes(steamid))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loaded = GSI.players.map(player => player.steamid);
|
||||
|
||||
const notCheckedPlayers = steamids.filter(steamid => !loaded.includes(steamid));
|
||||
|
||||
const extensioned = await api.players.get(notCheckedPlayers);
|
||||
|
||||
const lacking = notCheckedPlayers.filter(steamid => extensioned.map(player => player.steamid).includes(steamid));
|
||||
|
||||
const players: PlayerExtension[] = extensioned
|
||||
.filter(player => lacking.includes(player.steamid))
|
||||
.map(player => (
|
||||
{
|
||||
id: player._id,
|
||||
name: player.username,
|
||||
realName: `${player.firstName} ${player.lastName}`,
|
||||
steamid: player.steamid,
|
||||
country: player.country,
|
||||
avatar: player.avatar,
|
||||
extra: player.extra,
|
||||
})
|
||||
);
|
||||
|
||||
const gsiLoaded = GSI.players;
|
||||
|
||||
gsiLoaded.push(...players);
|
||||
|
||||
GSI.players = gsiLoaded;
|
||||
this.setState({ steamids });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMatch();
|
||||
const href = window.location.href;
|
||||
socket.emit("started");
|
||||
let isDev = false;
|
||||
let name = '';
|
||||
if (href.indexOf('/huds/') === -1) {
|
||||
isDev = true;
|
||||
name = (Math.random() * 1000 + 1).toString(36).replace(/[^a-z]+/g, '').substr(0, 15);
|
||||
hudIdentity.isDev = true;
|
||||
} else {
|
||||
const segment = href.substr(href.indexOf('/huds/') + 6);
|
||||
name = segment.substr(0, segment.lastIndexOf('/'));
|
||||
hudIdentity.name = name;
|
||||
}
|
||||
|
||||
socket.on("readyToRegister", () => {
|
||||
socket.emit("register", name, isDev, "cs2", isInWindow ? "IPC" : "DEFAULT");
|
||||
initiateConnection();
|
||||
});
|
||||
socket.on(`hud_config`, (data: any) => {
|
||||
configs.save(data);
|
||||
});
|
||||
socket.on(`hud_action`, (data: any) => {
|
||||
actions.execute(data.action, data.data);
|
||||
});
|
||||
socket.on('keybindAction', (action: string) => {
|
||||
actions.execute(action);
|
||||
});
|
||||
|
||||
socket.on("refreshHUD", () => {
|
||||
window.top?.location.reload();
|
||||
});
|
||||
|
||||
socket.on("update_mirv", (data: any) => {
|
||||
GSI.digestMIRV(data);
|
||||
})
|
||||
GSI.on('data', game => {
|
||||
if (!this.state.game || this.state.steamids.length) this.verifyPlayers(game);
|
||||
|
||||
const wasLoaded = !!this.state.game;
|
||||
|
||||
this.setState({ game }, () => {
|
||||
if(!wasLoaded) this.loadMatch(true);
|
||||
});
|
||||
});
|
||||
socket.on('match', () => {
|
||||
|
||||
this.loadMatch(true);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
loadMatch = async (force = false) => {
|
||||
if (!dataLoader.match || force) {
|
||||
dataLoader.match = new Promise((resolve) => {
|
||||
api.match.getCurrent().then(match => {
|
||||
if (!match) {
|
||||
GSI.teams.left = null;
|
||||
GSI.teams.right = null;
|
||||
return;
|
||||
}
|
||||
this.setState({ match });
|
||||
|
||||
let isReversed = false;
|
||||
if (GSI.last) {
|
||||
const mapName = GSI.last.map.name.substring(GSI.last.map.name.lastIndexOf('/') + 1);
|
||||
const current = match.vetos.filter(veto => veto.mapName === mapName)[0];
|
||||
if (current && current.reverseSide) {
|
||||
isReversed = true;
|
||||
}
|
||||
this.setState({ checked: true });
|
||||
}
|
||||
if (match.left.id) {
|
||||
api.teams.getOne(match.left.id).then(left => {
|
||||
const gsiTeamData = { id: left._id, name: left.name, country: left.country, logo: left.logo, map_score: match.left.wins, extra: left.extra };
|
||||
|
||||
if (!isReversed) {
|
||||
GSI.teams.left = gsiTeamData;
|
||||
}
|
||||
else GSI.teams.right = gsiTeamData;
|
||||
});
|
||||
}
|
||||
if (match.right.id) {
|
||||
api.teams.getOne(match.right.id).then(right => {
|
||||
const gsiTeamData = { id: right._id, name: right.name, country: right.country, logo: right.logo, map_score: match.right.wins, extra: right.extra };
|
||||
|
||||
if (!isReversed) GSI.teams.right = gsiTeamData;
|
||||
else GSI.teams.left = gsiTeamData;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}).catch(() => {
|
||||
//dataLoader.match = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.state.game) return null;
|
||||
return (
|
||||
<Layout game={this.state.game} match={this.state.match} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default App;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { mediaStreams } from "./mediaStream";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { mediaStreams } from "../../API/HUD/camera";
|
||||
type Props = {
|
||||
steamid: string,
|
||||
visible: boolean;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PlayerCamera from "./Camera";
|
||||
import api from "../../API";
|
||||
import api from "../../api/api";
|
||||
import "./index.scss";
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Instance, SignalData } from 'simple-peer';
|
||||
import api from '..';
|
||||
import { socket } from '../socket';
|
||||
import Peer from 'simple-peer';
|
||||
import api from '../../api/api';
|
||||
import { socket as Socket } from "../../App";
|
||||
const Peer = require('simple-peer');
|
||||
|
||||
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
@ -73,6 +73,7 @@ const closeConnection = (steamid: string) => {
|
||||
}
|
||||
|
||||
const initiateConnection = async () => {
|
||||
const socket = Socket as SocketIOClient.Socket;
|
||||
const camera = await api.camera.get();
|
||||
await wait(1000);
|
||||
|
||||
@ -103,7 +104,7 @@ const initiateConnection = async () => {
|
||||
|
||||
if (camera.uuid !== roomId) return;
|
||||
|
||||
const peerConnection = new Peer({ initiator: false, trickle: false }) as PeerInstance;
|
||||
const peerConnection: PeerInstance = new Peer({ initiator: false, trickle: false });
|
||||
|
||||
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import { ArmorFull, ArmorHelmet } from "./../../assets/Icons";
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {ArmorHelmet, ArmorFull} from './../../assets/Icons';
|
||||
export default class Armor extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(!player.state.health || !player.state.armor) return '';
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Armor = ({ health, armor, helmet }: { health: number, armor: number, helmet: boolean }) => {
|
||||
if (!health || !armor) return null;
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
{helmet ? <ArmorHelmet /> : <ArmorFull />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Armor);
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { Player } from "csgogsi";
|
||||
import { Bomb as BombIcon } from "./../../assets/Icons";
|
||||
const Bomb = ({ player }: { player: Player }) => {
|
||||
if (Object.values(player.weapons).every((weapon) => weapon.type !== "C4")) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
<BombIcon />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Bomb;
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {Bomb as BombIcon} from './../../assets/Icons';
|
||||
export default class Bomb extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(Object.values(player.weapons).every(weapon => weapon.type !== "C4")) return '';
|
||||
return (
|
||||
<div className={`armor_indicator`}>
|
||||
<BombIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { Player } from 'csgogsi';
|
||||
import React from 'react';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import {Defuse as DefuseIcon} from './../../assets/Icons';
|
||||
const Defuse = ({ player }: { player: Player }) => {
|
||||
export default class Defuse extends React.Component<{ player: Player }> {
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
if(!player.state.health || !player.state.defusekit) return '';
|
||||
return (
|
||||
<div className={`defuse_indicator`}>
|
||||
<DefuseIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default Defuse;
|
||||
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import Weapon from './../Weapon/Weapon';
|
||||
import flash_assist from './../../assets/flash_assist.png';
|
||||
|
||||
|
||||
import { C4, Defuse, FlashedKill, Headshot, NoScope, SmokeKill, Suicide, Wallbang } from "./../../assets/Icons"
|
||||
import { ExtendedKillEvent, BombEvent } from "./Killfeed"
|
||||
|
||||
|
||||
const Kill = ({event}: { event: ExtendedKillEvent | BombEvent }) => {
|
||||
export default class Kill extends React.Component<{ event: ExtendedKillEvent | BombEvent }> {
|
||||
render() {
|
||||
const { event } = this.props;
|
||||
if (event.type !== "kill") {
|
||||
return (
|
||||
<div className={`single_kill`}>
|
||||
@ -48,6 +50,6 @@ import { ExtendedKillEvent, BombEvent } from "./Killfeed"
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default Kill;
|
||||
@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { KillEvent, Player } from 'csgogsi';
|
||||
import React from 'react';
|
||||
import { GSI } from './../../App';
|
||||
import { KillEvent, Player } from 'csgogsi-socket';
|
||||
import Kill from './Kill';
|
||||
import './killfeed.scss';
|
||||
import { onGSI } from '../../API/contexts/actions';
|
||||
|
||||
|
||||
export interface ExtendedKillEvent extends KillEvent {
|
||||
@ -15,24 +14,63 @@ export interface BombEvent {
|
||||
type: 'plant' | 'defuse'
|
||||
}
|
||||
|
||||
const Killfeed = () => {
|
||||
const [ events, setEvents ] = useState<(BombEvent | ExtendedKillEvent)[]>([]);
|
||||
onGSI("kill", kill => {
|
||||
setEvents(ev => [...ev, {...kill, type: 'kill'}]);
|
||||
}, []);
|
||||
onGSI("data", data => {
|
||||
if(data.round && data.round.phase === "freezetime"){
|
||||
if(Number(data.phase_countdowns.phase_ends_in) < 10 && events.length > 0){
|
||||
setEvents([]);
|
||||
}
|
||||
export default class Killfeed extends React.Component<any, { events: (BombEvent | ExtendedKillEvent)[] }> {
|
||||
|
||||
constructor(props: any){
|
||||
super(props);
|
||||
this.state = {
|
||||
events: []
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className="killfeed">
|
||||
{events.map(event => <Kill event={event}/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
addKill = (kill: KillEvent) => {
|
||||
this.setState(state => {
|
||||
state.events.push({...kill, type: 'kill'});
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
addBombEvent = (player: Player, type: 'plant' | 'defuse') => {
|
||||
if(!player) return;
|
||||
const event: BombEvent = {
|
||||
player: player,
|
||||
type: type
|
||||
}
|
||||
this.setState(state => {
|
||||
state.events.push(event);
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
GSI.on("kill", kill => {
|
||||
this.addKill(kill);
|
||||
});
|
||||
GSI.on("data", data => {
|
||||
|
||||
if(data.round && data.round.phase === "freezetime"){
|
||||
if(Number(data.phase_countdowns.phase_ends_in) < 10 && this.state.events.length > 0){
|
||||
this.setState({events:[]})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
GSI.on("bombPlant", player => {
|
||||
this.addBombEvent(player, 'plant');
|
||||
})
|
||||
GSI.on("bombDefuse", player => {
|
||||
this.addBombEvent(player, 'defuse');
|
||||
})
|
||||
|
||||
*/
|
||||
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="killfeed">
|
||||
{this.state.events.map(event => <Kill event={event}/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default React.memo(Killfeed);
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import React from "react";
|
||||
import TeamBox from "./../Players/TeamBox";
|
||||
import MatchBar from "../MatchBar/MatchBar";
|
||||
import SeriesBox from "../MatchBar/SeriesBox";
|
||||
import Observed from "./../Players/Observed";
|
||||
import { CSGO, Team } from "csgogsi-socket";
|
||||
import { Match } from "../../api/interfaces";
|
||||
import RadarMaps from "./../Radar/RadarMaps";
|
||||
import Trivia from "../Trivia/Trivia";
|
||||
import SideBox from '../SideBoxes/SideBox';
|
||||
import { GSI, actions } from "./../../App";
|
||||
import MoneyBox from '../SideBoxes/Money';
|
||||
import UtilityLevel from '../SideBoxes/UtilityLevel';
|
||||
import Killfeed from "../Killfeed/Killfeed";
|
||||
@ -14,93 +17,119 @@ import Overview from "../Overview/Overview";
|
||||
import Tournament from "../Tournament/Tournament";
|
||||
import Pause from "../PauseTimeout/Pause";
|
||||
import Timeout from "../PauseTimeout/Timeout";
|
||||
import { CSGO } from "csgogsi";
|
||||
import { Match } from "../../API/types";
|
||||
import { useAction } from "../../API/contexts/actions";
|
||||
import { Scout } from "../Scout";
|
||||
import PlayerCamera from "../Camera/Camera";
|
||||
|
||||
interface Props {
|
||||
game: CSGO,
|
||||
match: Match | null
|
||||
}
|
||||
/*
|
||||
|
||||
interface State {
|
||||
winner: Team | null,
|
||||
showWin: boolean,
|
||||
forceHide: boolean
|
||||
}*/
|
||||
}
|
||||
|
||||
const Layout = ({game,match}: Props) => {
|
||||
const [ forceHide, setForceHide ] = useState(false);
|
||||
|
||||
useAction('boxesState', (state) => {
|
||||
console.log("UPDATE STATE UMC", state);
|
||||
if (state === "show") {
|
||||
setForceHide(false);
|
||||
} else if (state === "hide") {
|
||||
setForceHide(true);
|
||||
export default class Layout extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
winner: null,
|
||||
showWin: false,
|
||||
forceHide: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
componentDidMount() {
|
||||
GSI.on('roundEnd', score => {
|
||||
this.setState({ winner: score.winner, showWin: true }, () => {
|
||||
setTimeout(() => {
|
||||
this.setState({ showWin: false })
|
||||
}, 4000)
|
||||
});
|
||||
});
|
||||
actions.on("boxesState", (state: string) => {
|
||||
if (state === "show") {
|
||||
this.setState({ forceHide: false });
|
||||
} else if (state === "hide") {
|
||||
this.setState({ forceHide: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const leftPlayers = game.players.filter(player => player.team.side === left.side);
|
||||
const rightPlayers = game.players.filter(player => player.team.side === right.side);
|
||||
const isFreezetime = (game.round && game.round.phase === "freezetime") || game.phase_countdowns.phase === "freezetime";
|
||||
return (
|
||||
<div className="layout">
|
||||
<div className={`players_alive`}>
|
||||
<div className="title_container">Players alive</div>
|
||||
<div className="counter_container">
|
||||
<div className={`team_counter ${left.side}`}>{leftPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
<div className={`vs_counter`}>VS</div>
|
||||
<div className={`team_counter ${right.side}`}>{rightPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
getVeto = () => {
|
||||
const { game, match } = this.props;
|
||||
const { map } = game;
|
||||
if (!match) return null;
|
||||
const mapName = map.name.substring(map.name.lastIndexOf('/') + 1);
|
||||
const veto = match.vetos.find(veto => veto.mapName === mapName);
|
||||
if (!veto) return null;
|
||||
return veto;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { game, match } = this.props;
|
||||
const left = game.map.team_ct.orientation === "left" ? game.map.team_ct : game.map.team_t;
|
||||
const right = game.map.team_ct.orientation === "left" ? game.map.team_t : game.map.team_ct;
|
||||
|
||||
const leftPlayers = game.players.filter(player => player.team.side === left.side);
|
||||
const rightPlayers = game.players.filter(player => player.team.side === right.side);
|
||||
const isFreezetime = (game.round && game.round.phase === "freezetime") || game.phase_countdowns.phase === "freezetime";
|
||||
const { forceHide } = this.state;
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<div className={`players_alive`}>
|
||||
<div className="title_container">Players alive</div>
|
||||
<div className="counter_container">
|
||||
<div className={`team_counter ${left.side}`}>{leftPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
<div className={`vs_counter`}>VS</div>
|
||||
<div className={`team_counter ${right.side}`}>{rightPlayers.filter(player => player.state.health > 0).length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Killfeed />
|
||||
<Overview match={match} map={game.map} players={game.players || []} />
|
||||
<RadarMaps match={match} map={game.map} game={game} />
|
||||
<MatchBar map={game.map} phase={game.phase_countdowns} bomb={game.bomb} match={match} />
|
||||
<Pause phase={game.phase_countdowns}/>
|
||||
<Timeout map={game.map} phase={game.phase_countdowns} />
|
||||
<SeriesBox map={game.map} phase={game.phase_countdowns} match={match} />
|
||||
|
||||
<Tournament />
|
||||
|
||||
<Observed player={game.player} veto={this.getVeto()} round={game.map.round+1}/>
|
||||
|
||||
<TeamBox team={left} players={leftPlayers} side="left" current={game.player} />
|
||||
<TeamBox team={right} players={rightPlayers} side="right" current={game.player} />
|
||||
|
||||
<Trivia />
|
||||
|
||||
<MapSeries teams={[left, right]} match={match} isFreezetime={isFreezetime} map={game.map} />
|
||||
<div className={"boxes left"}>
|
||||
<UtilityLevel side={left.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="left" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={left.side}
|
||||
side="left"
|
||||
loss={Math.min(left.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={leftPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={leftPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
<div className={"boxes right"}>
|
||||
<UtilityLevel side={right.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="right" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={right.side}
|
||||
side="right"
|
||||
loss={Math.min(right.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={rightPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={rightPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Killfeed />
|
||||
<Overview match={match} map={game.map} players={game.players || []} />
|
||||
<RadarMaps match={match} map={game.map} game={game} />
|
||||
<MatchBar map={game.map} phase={game.phase_countdowns} bomb={game.bomb} match={match} />
|
||||
<Pause phase={game.phase_countdowns}/>
|
||||
<Timeout map={game.map} phase={game.phase_countdowns} />
|
||||
<SeriesBox map={game.map} match={match} />
|
||||
|
||||
<Tournament />
|
||||
|
||||
<Observed player={game.player} />
|
||||
|
||||
<TeamBox team={left} players={leftPlayers} side="left" current={game.player} />
|
||||
<TeamBox team={right} players={rightPlayers} side="right" current={game.player} />
|
||||
|
||||
<Trivia />
|
||||
<Scout left={left.side} right={right.side} />
|
||||
<MapSeries teams={[left, right]} match={match} isFreezetime={isFreezetime} map={game.map} />
|
||||
<div className={"boxes left"}>
|
||||
<UtilityLevel side={left.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="left" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={left.side}
|
||||
side="left"
|
||||
loss={Math.min(left.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={leftPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={leftPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
<div className={"boxes right"}>
|
||||
<UtilityLevel side={right.side} players={game.players} show={isFreezetime && !forceHide} />
|
||||
<SideBox side="right" hide={forceHide} />
|
||||
<MoneyBox
|
||||
team={right.side}
|
||||
side="right"
|
||||
loss={Math.min(right.consecutive_round_losses * 500 + 1400, 3400)}
|
||||
equipment={rightPlayers.map(player => player.state.equip_value).reduce((pre, now) => pre + now, 0)}
|
||||
money={rightPlayers.map(player => player.state.money).reduce((pre, now) => pre + now, 0)}
|
||||
show={isFreezetime && !forceHide}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Layout;
|
||||
|
||||
@ -1,66 +1,62 @@
|
||||
import * as I from "csgogsi";
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import { Match, Veto } from '../../api/interfaces';
|
||||
import TeamLogo from "../MatchBar/TeamLogo";
|
||||
import "./mapseries.scss";
|
||||
import { Match, Veto } from "../../API/types";
|
||||
|
||||
interface IProps {
|
||||
match: Match | null;
|
||||
teams: I.Team[];
|
||||
isFreezetime: boolean;
|
||||
map: I.Map;
|
||||
match: Match | null;
|
||||
teams: I.Team[];
|
||||
isFreezetime: boolean;
|
||||
map: I.Map
|
||||
}
|
||||
|
||||
interface IVetoProps {
|
||||
veto: Veto;
|
||||
teams: I.Team[];
|
||||
active: boolean;
|
||||
veto: Veto;
|
||||
teams: I.Team[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const VetoEntry = ({ veto, teams, active }: IVetoProps) => {
|
||||
return (
|
||||
<div className={`veto_container ${active ? "active" : ""}`}>
|
||||
<div className="veto_map_name">
|
||||
{veto.mapName}
|
||||
</div>
|
||||
<div className="veto_picker">
|
||||
<TeamLogo team={teams.filter((team) => team.id === veto.teamId)[0]} />
|
||||
</div>
|
||||
<div className="veto_winner">
|
||||
<TeamLogo team={teams.filter((team) => team.id === veto.winner)[0]} />
|
||||
</div>
|
||||
<div className="veto_score">
|
||||
{Object.values(veto.score || ["-", "-"]).sort().join(":")}
|
||||
</div>
|
||||
<div className="active_container">
|
||||
<div className="active">Currently playing</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class VetoEntry extends React.Component<IVetoProps> {
|
||||
render(){
|
||||
const { veto, teams, active } = this.props;
|
||||
return <div className={`veto_container ${active ? 'active' : ''}`}>
|
||||
<div className="veto_map_name">
|
||||
{veto.mapName}
|
||||
</div>
|
||||
<div className="veto_picker">
|
||||
<TeamLogo team={teams.filter(team => team.id === veto.teamId)[0]} />
|
||||
</div>
|
||||
<div className="veto_winner">
|
||||
<TeamLogo team={teams.filter(team => team.id === veto.winner)[0]} />
|
||||
</div>
|
||||
<div className="veto_score">
|
||||
{Object.values((veto.score || ['-','-'])).sort().join(":")}
|
||||
</div>
|
||||
<div className='active_container'>
|
||||
<div className='active'>Currently playing</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const MapSeries = ({ match, teams, isFreezetime, map }: IProps) => {
|
||||
if (!match || !match.vetos.length) return null;
|
||||
return (
|
||||
<div className={`map_series_container ${isFreezetime ? "show" : "hide"}`}>
|
||||
<div className="title_bar">
|
||||
<div className="picked">Picked</div>
|
||||
<div className="winner">Winner</div>
|
||||
<div className="score">Score</div>
|
||||
</div>
|
||||
{match.vetos.filter((veto) =>
|
||||
veto.type !== "ban"
|
||||
).map((veto) => {
|
||||
if (!veto.mapName) return null;
|
||||
export default class MapSeries extends React.Component<IProps> {
|
||||
|
||||
render() {
|
||||
const { match, teams, isFreezetime, map } = this.props;
|
||||
if (!match || !match.vetos.length) return null;
|
||||
return (
|
||||
<VetoEntry
|
||||
key={`${match.id}${veto.mapName}${veto.teamId}${veto.side}`}
|
||||
veto={veto}
|
||||
teams={teams}
|
||||
active={map.name.includes(veto.mapName)}
|
||||
/>
|
||||
<div className={`map_series_container ${isFreezetime ? 'show': 'hide'}`}>
|
||||
<div className="title_bar">
|
||||
<div className="picked">Picked</div>
|
||||
<div className="winner">Winner</div>
|
||||
<div className="score">Score</div>
|
||||
</div>
|
||||
{match.vetos.filter(veto => veto.type !== "ban").map(veto => {
|
||||
if(!veto.mapName) return null;
|
||||
return <VetoEntry key={`${match.id}${veto.mapName}${veto.teamId}${veto.side}`} veto={veto} teams={teams} active={map.name.includes(veto.mapName)}/>
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MapSeries;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import * as I from "csgogsi";
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import "./matchbar.scss";
|
||||
import TeamScore from "./TeamScore";
|
||||
import Bomb from "./../Timers/BombTimer";
|
||||
import { useBombTimer } from "./../Timers/Countdown";
|
||||
import { Match } from './../../API/types';
|
||||
|
||||
import Countdown from "./../Timers/Countdown";
|
||||
import { GSI } from "../../App";
|
||||
import { Match } from "../../api/interfaces";
|
||||
|
||||
function stringToClock(time: string | number, pad = true) {
|
||||
if (typeof time === "string") {
|
||||
@ -22,54 +23,181 @@ function stringToClock(time: string | number, pad = true) {
|
||||
interface IProps {
|
||||
match: Match | null;
|
||||
map: I.Map;
|
||||
phase: I.CSGO["phase_countdowns"],
|
||||
phase: I.PhaseRaw,
|
||||
bomb: I.Bomb | null,
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
time: number;
|
||||
width: number;
|
||||
active: boolean;
|
||||
countdown: number;
|
||||
side: "left"|"right";
|
||||
type: "defusing" | "planting";
|
||||
player: I.Player | null;
|
||||
}
|
||||
const getRoundLabel = (mapRound: number) => {
|
||||
const round = mapRound + 1;
|
||||
if (round <= 24) {
|
||||
return `Round ${round}/24`;
|
||||
|
||||
interface IState {
|
||||
defusing: Timer,
|
||||
planting: Timer,
|
||||
winState: {
|
||||
side: "left"|"right",
|
||||
show: boolean
|
||||
}
|
||||
const additionalRounds = round - 24;
|
||||
const OT = Math.ceil(additionalRounds/6);
|
||||
return `OT ${OT} (${additionalRounds - (OT - 1)*6}/6)`;
|
||||
}
|
||||
|
||||
const Matchbar = (props: IProps) => {
|
||||
const { bomb, match, map, phase } = props;
|
||||
export default class TeamBox extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps){
|
||||
super(props);
|
||||
this.state = {
|
||||
defusing: {
|
||||
width: 0,
|
||||
active: false,
|
||||
countdown: 10,
|
||||
side: "left",
|
||||
type: "defusing",
|
||||
player: null
|
||||
},
|
||||
planting: {
|
||||
width: 0,
|
||||
active: false,
|
||||
countdown: 10, // Fake
|
||||
side: "right",
|
||||
type: "planting",
|
||||
player: null
|
||||
},
|
||||
winState: {
|
||||
side: 'left',
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
plantStop = () => this.setState(state => {
|
||||
state.planting.active = false;
|
||||
return state;
|
||||
});
|
||||
|
||||
setWidth = (type: 'defusing' | 'planting', width: number) => {
|
||||
this.setState(state => {
|
||||
state[type].width = width;
|
||||
return state;
|
||||
})
|
||||
}
|
||||
|
||||
initPlantTimer = () => {
|
||||
const bomb = new Countdown(time => {
|
||||
let width = time * 100;
|
||||
this.setWidth("planting", width/3);
|
||||
});
|
||||
GSI.on("bombPlantStart", player => {
|
||||
if(!player || !player.team) return;
|
||||
this.setState(state => {
|
||||
state.planting.active = true;
|
||||
state.planting.side = player.team.orientation;
|
||||
state.planting.player = player;
|
||||
})
|
||||
})
|
||||
GSI.on("data", data => {
|
||||
if(!data.bomb || !data.bomb.countdown || data.bomb.state !== "planting") return this.plantStop();
|
||||
this.setState(state => {
|
||||
state.planting.active = true;
|
||||
})
|
||||
return bomb.go(data.bomb.countdown);
|
||||
});
|
||||
}
|
||||
|
||||
defuseStop = () => this.setState(state => {
|
||||
state.defusing.active = false;
|
||||
state.defusing.countdown = 10;
|
||||
return state;
|
||||
});
|
||||
|
||||
initDefuseTimer = () => {
|
||||
const bomb = new Countdown(time => {
|
||||
let width = time > this.state.defusing.countdown ? this.state.defusing.countdown*100 : time * 100;
|
||||
this.setWidth("defusing", width/this.state.defusing.countdown);
|
||||
});
|
||||
GSI.on("defuseStart", player => {
|
||||
if(!player || !player.team) return;
|
||||
this.setState(state => {
|
||||
state.defusing.active = true;
|
||||
state.defusing.countdown = !Boolean(player.state.defusekit) ? 10 : 5;
|
||||
state.defusing.side = player.team.orientation;
|
||||
state.defusing.player = player;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
GSI.on("data", data => {
|
||||
if(!data.bomb || !data.bomb.countdown || data.bomb.state !== "defusing") return this.defuseStop();
|
||||
this.setState(state => {
|
||||
state.defusing.active = true;
|
||||
return state;
|
||||
})
|
||||
return bomb.go(data.bomb.countdown);
|
||||
});
|
||||
}
|
||||
|
||||
resetWin = () => {
|
||||
setTimeout(() => {
|
||||
this.setState(state => {
|
||||
state.winState.show = false;
|
||||
return state;
|
||||
})
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.initDefuseTimer();
|
||||
this.initPlantTimer();
|
||||
GSI.on("roundEnd", score => {
|
||||
this.setState(state => {
|
||||
state.winState.show = true;
|
||||
state.winState.side = score.winner.orientation;
|
||||
return state;
|
||||
}, this.resetWin);
|
||||
});
|
||||
}
|
||||
getRoundLabel = () => {
|
||||
const { map } = this.props;
|
||||
const round = map.round + 1;
|
||||
if (round <= 30) {
|
||||
return `Round ${round}/30`;
|
||||
}
|
||||
const additionalRounds = round - 30;
|
||||
const OT = Math.ceil(additionalRounds/6);
|
||||
return `OT ${OT} (${additionalRounds - (OT - 1)*6}/6)`;
|
||||
}
|
||||
render() {
|
||||
const { defusing, planting, winState } = this.state;
|
||||
const { bomb, match, map, phase } = this.props;
|
||||
const time = stringToClock(phase.phase_ends_in);
|
||||
const left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
||||
const right = map.team_ct.orientation === "left" ? map.team_t : map.team_ct;
|
||||
const isPlanted = bomb && (bomb.state === "defusing" || bomb.state === "planted");
|
||||
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
||||
|
||||
const bombData = useBombTimer();
|
||||
const plantTimer: Timer | null = bombData.state === "planting" ? { time:bombData.plantTime, active: true, side: bombData.player?.team.orientation || "right", player: bombData.player, type: "planting"} : null;
|
||||
const defuseTimer: Timer | null = bombData.state === "defusing" ? { time:bombData.defuseTime, active: true, side: bombData.player?.team.orientation || "left", player: bombData.player, type: "defusing"} : null;
|
||||
|
||||
let leftTimer: Timer | null = null, rightTimer: Timer | null = null;
|
||||
if(defusing.active || planting.active){
|
||||
if(defusing.active){
|
||||
if(defusing.side === "left") leftTimer = defusing;
|
||||
else rightTimer = defusing;
|
||||
} else {
|
||||
if(planting.side === "left") leftTimer = planting;
|
||||
else rightTimer = planting;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div id={`matchbar`}>
|
||||
<TeamScore team={left} orientation={"left"} timer={left.side === "CT" ? defuseTimer : plantTimer}/>
|
||||
<TeamScore team={left} orientation={"left"} timer={leftTimer} showWin={winState.show && winState.side === "left"} />
|
||||
<div className={`score left ${left.side}`}>{left.score}</div>
|
||||
<div id="timer" className={bo === 0 ? 'no-bo' : ''}>
|
||||
<div id={`round_timer_text`} className={isPlanted ? "hide":""}>{time}</div>
|
||||
<div id="round_now" className={isPlanted ? "hide":""}>{getRoundLabel(map.round)}</div>
|
||||
<div id="round_now" className={isPlanted ? "hide":""}>{this.getRoundLabel()}</div>
|
||||
<Bomb />
|
||||
</div>
|
||||
<div className={`score right ${right.side}`}>{right.score}</div>
|
||||
<TeamScore team={right} orientation={"right"} timer={right.side === "CT" ? defuseTimer : plantTimer} />
|
||||
<TeamScore team={right} orientation={"right"} timer={rightTimer} showWin={winState.show && winState.side === "right"} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Matchbar;
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import * as I from "csgogsi";
|
||||
import { Match } from "../../API/types";
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import { Match } from "../../api/interfaces";
|
||||
|
||||
interface Props {
|
||||
map: I.Map;
|
||||
phase: I.PhaseRaw;
|
||||
match: Match | null;
|
||||
}
|
||||
|
||||
const SeriesBox = ({ map, match }: Props) => {
|
||||
export default class SeriesBox extends React.Component<Props> {
|
||||
render() {
|
||||
const { match, map } = this.props;
|
||||
const amountOfMaps = (match && Math.floor(Number(match.matchType.substr(-1)) / 2) + 1) || 0;
|
||||
const bo = (match && Number(match.matchType.substr(-1))) || 0;
|
||||
const left = map.team_ct.orientation === "left" ? map.team_ct : map.team_t;
|
||||
@ -36,6 +40,5 @@ const SeriesBox = ({ map, match }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SeriesBox;
|
||||
@ -1,9 +1,11 @@
|
||||
import { Team } from 'csgogsi';
|
||||
import * as I from '../../API/types';
|
||||
import { apiUrl } from './../../API';
|
||||
import { LogoCT, LogoT } from './../../assets/Icons';
|
||||
import React from 'react';
|
||||
import { Team } from 'csgogsi-socket';
|
||||
import * as I from '../../api/interfaces';
|
||||
import { apiUrl } from './../../api/api';
|
||||
|
||||
const TeamLogo = ({team, height, width }: { team?: Team | I.Team | null, height?: number, width?: number}) => {
|
||||
export default class TeamLogo extends React.Component<{ team?: Team | I.Team | null, height?: number, width?: number}> {
|
||||
render(){
|
||||
const { team } = this.props;
|
||||
if(!team) return null;
|
||||
let id = '';
|
||||
const { logo } = team;
|
||||
@ -14,10 +16,9 @@ const TeamLogo = ({team, height, width }: { team?: Team | I.Team | null, height?
|
||||
}
|
||||
return (
|
||||
<div className={`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'} />
|
||||
{ logo && id ? <img src={`${apiUrl}api/teams/logo/${id}`} width={this.props.width} height={this.props.height} alt={'Team logo'} /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TeamLogo;
|
||||
|
||||
@ -1,40 +1,30 @@
|
||||
import * as I from "csgogsi";
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import WinIndicator from "./WinIndicator";
|
||||
import { Timer } from "./MatchBar";
|
||||
import TeamLogo from './TeamLogo';
|
||||
import PlantDefuse from "../Timers/PlantDefuse"
|
||||
import { onGSI } from "../../API/contexts/actions";
|
||||
import WinAnnouncement from "./WinIndicator";
|
||||
import { useState } from "react";
|
||||
|
||||
interface IProps {
|
||||
team: I.Team;
|
||||
orientation: "left" | "right";
|
||||
timer: Timer | null;
|
||||
team: I.Team;
|
||||
showWin: boolean;
|
||||
}
|
||||
|
||||
const TeamScore = ({orientation, timer, team }: IProps) => {
|
||||
const [ show, setShow ] = useState(false);
|
||||
|
||||
onGSI("roundEnd", result => {
|
||||
if(result.winner.orientation !== orientation) return;
|
||||
setShow(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
}, 5000);
|
||||
}, [orientation]);
|
||||
|
||||
export default class TeamScore extends React.Component<IProps> {
|
||||
render() {
|
||||
const { orientation, timer, team, showWin } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className={`team ${orientation} ${team.side || ''}`}>
|
||||
<div className="team-name">{team?.name || null}</div>
|
||||
<div className={`team ${orientation} ${team.side}`}>
|
||||
<div className="team-name">{team.name}</div>
|
||||
<TeamLogo team={team} />
|
||||
<div className="round-thingy"><div className="inner"></div></div>
|
||||
</div>
|
||||
<PlantDefuse timer={timer} side={orientation} />
|
||||
<WinAnnouncement team={team} show={show} />
|
||||
<WinIndicator team={team} show={showWin}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TeamScore;
|
||||
@ -1,11 +1,12 @@
|
||||
import { Team } from 'csgogsi';
|
||||
import React from 'react';
|
||||
import { Team } from 'csgogsi-socket';
|
||||
|
||||
const WinAnnouncement = ({team, show }: { team: Team | null, show: boolean }) => {
|
||||
export default class WinAnnouncement extends React.Component<{ team: Team | null, show: boolean }> {
|
||||
render() {
|
||||
const { team, show } = this.props;
|
||||
if(!team) return null;
|
||||
return <div className={`win_text ${show ? 'show' : ''} ${team.orientation} ${team.side}`}>
|
||||
WINS THE ROUND!
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default WinAnnouncement;
|
||||
@ -1,39 +1,41 @@
|
||||
import * as I from "../../API/types";
|
||||
import "./index.scss";
|
||||
import TeamLogo from "../MatchBar/TeamLogo";
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import TeamLogo from '../MatchBar/TeamLogo';
|
||||
|
||||
interface IProps {
|
||||
match: I.Match;
|
||||
show: boolean;
|
||||
teams: I.Team[];
|
||||
veto: I.Veto | null;
|
||||
match: I.Match,
|
||||
show: boolean,
|
||||
teams: I.Team[],
|
||||
veto: I.Veto | null
|
||||
}
|
||||
|
||||
const MatchOverview = ({ match, teams, show }: IProps) => {
|
||||
const left = teams.find((team) => team._id === match.left.id);
|
||||
const right = teams.find((team) => team._id === match.right.id);
|
||||
if (!match || !left || !right) return null;
|
||||
return (
|
||||
<div className={`match-overview ${show ? "show" : ""}`}>
|
||||
<div className="match-overview-title">
|
||||
Upcoming match
|
||||
</div>
|
||||
<div className="match-overview-teams">
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={left} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{(left.shortName || left.name).substring(0, 4)}</div>
|
||||
</div>
|
||||
<div className="match-overview-vs">vs</div>
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={right} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{(right.shortName || right.name).substring(0, 4)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MatchOverview;
|
||||
export default class MatchOverview extends React.Component<IProps> {
|
||||
render() {
|
||||
const { match, teams, show } = this.props;
|
||||
const left = teams.find(team => team._id === match.left.id);
|
||||
const right = teams.find(team => team._id === match.right.id);
|
||||
if(!match || !left || !right) return null;
|
||||
return (
|
||||
<div className={`match-overview ${show ? 'show':''}`}>
|
||||
<div className="match-overview-title">
|
||||
Upcoming match
|
||||
</div>
|
||||
<div className="match-overview-teams">
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={left} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{left.name}</div>
|
||||
</div>
|
||||
<div className="match-overview-vs">vs</div>
|
||||
<div className="match-overview-team">
|
||||
<div className="match-overview-team-logo">
|
||||
<TeamLogo team={right} height={40} />
|
||||
</div>
|
||||
<div className="match-overview-team-name">{right.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { actions, configs } from '../../App';
|
||||
import * as I from '../../api/interfaces';
|
||||
import PlayerOverview from '../PlayerOverview/PlayerOverview';
|
||||
import MatchOverview from '../MatchOverview/MatchOverview';
|
||||
import { Map, Player } from 'csgogsi';
|
||||
import * as I from './../../API/types';
|
||||
import api from './../../API';
|
||||
import { useConfig, useOnConfigChange } from '../../API/contexts/actions';
|
||||
import TeamOverview from '../TeamOverview/TeamOverview';
|
||||
import { Map, Player } from 'csgogsi-socket';
|
||||
import api from '../../api/api';
|
||||
|
||||
interface IState {
|
||||
player: {
|
||||
data: I.Player | null,
|
||||
show: boolean
|
||||
},
|
||||
match: {
|
||||
data: I.Match | null,
|
||||
show: boolean,
|
||||
teams: I.Team[],
|
||||
},
|
||||
team: {
|
||||
data: I.Team | null,
|
||||
show: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
match: I.Match | null,
|
||||
@ -12,30 +29,97 @@ interface IProps {
|
||||
players: Player[]
|
||||
}
|
||||
|
||||
const Overview = ({ match, map, players }: IProps) => {
|
||||
const [ teams, setTeams ] = useState<I.Team[]>([]);
|
||||
const mapName = map.name.substring(map.name.lastIndexOf('/')+1);
|
||||
|
||||
const previewData = useConfig("preview_settings");
|
||||
|
||||
useOnConfigChange('preview_settings', async data => {
|
||||
console.log(data);
|
||||
if(!data?.match_preview?.match?.left.id || !data?.match_preview?.match?.right.id) return;
|
||||
|
||||
const teams = await Promise.all([api.teams.getOne(data?.match_preview?.match?.left.id), api.teams.getOne(data?.match_preview?.match?.right.id)]);
|
||||
export default class Overview extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps){
|
||||
super(props);
|
||||
this.state = {
|
||||
player: {
|
||||
data: null,
|
||||
show: false
|
||||
},
|
||||
match: {
|
||||
data: null,
|
||||
show: false,
|
||||
teams: []
|
||||
},
|
||||
team: {
|
||||
data: null,
|
||||
show: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
loadTeams = async () => {
|
||||
const { match } = this.state;
|
||||
if(!match.data || !match.data.left.id || !match.data.right.id) return;
|
||||
const teams = await Promise.all([api.teams.getOne(match.data.left.id), api.teams.getOne(match.data.right.id)]);
|
||||
if(!teams[0] || !teams[1]) return;
|
||||
|
||||
setTeams(teams);
|
||||
}, []);
|
||||
|
||||
const playerData = previewData?.player_preview?.player;
|
||||
const matchData = previewData?.match_preview?.match;
|
||||
|
||||
const veto = match?.vetos.find(veto => veto.mapName === mapName) || null;
|
||||
return (<>
|
||||
{ playerData ? <PlayerOverview round={map.round + 1} player={playerData} players={players} show={!!previewData.player_preview_toggle} veto={veto} /> : null}
|
||||
{ matchData && teams[0] && teams[1] ? <MatchOverview match={matchData} veto={veto} teams={teams} show={!!previewData.match_preview_toggle} /> : null }
|
||||
</>)
|
||||
this.setState(state => {
|
||||
state.match.teams = teams;
|
||||
return state;
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
configs.onChange((data: any) => {
|
||||
if(!data || !data.preview_settings) return;
|
||||
this.setState({
|
||||
player: {
|
||||
data: (data.preview_settings.player_preview && data.preview_settings.player_preview.player) || null,
|
||||
show: Boolean(data.preview_settings.player_preview_toggle)
|
||||
},
|
||||
team: {
|
||||
data: (data.preview_settings.team_preview && data.preview_settings.team_preview.team) || null,
|
||||
show: Boolean(data.preview_settings.team_preview_toggle)
|
||||
},
|
||||
match: {
|
||||
data: (data.preview_settings.match_preview && data.preview_settings.match_preview.match) || null,
|
||||
show: Boolean(data.preview_settings.match_preview_toggle),
|
||||
teams: this.state.match.teams
|
||||
}
|
||||
}, this.loadTeams);
|
||||
});
|
||||
actions.on("toggleUpcomingMatch", () => {
|
||||
this.setState(state => {
|
||||
state.match.show = !state.match.show;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
actions.on("togglePlayerPreview", () => {
|
||||
this.setState(state => {
|
||||
state.player.show = !state.player.show;
|
||||
return state;
|
||||
})
|
||||
})
|
||||
}
|
||||
getVeto = () => {
|
||||
const { map, match } = this.props;
|
||||
if(!match) return null;
|
||||
const mapName = map.name.substring(map.name.lastIndexOf('/')+1);
|
||||
const veto = match.vetos.find(veto => veto.mapName === mapName);
|
||||
if(!veto) return null;
|
||||
return veto;
|
||||
}
|
||||
renderPlayer = () => {
|
||||
const { player } = this.state;
|
||||
if(!player.data) return null;
|
||||
return <PlayerOverview round={this.props.map.round + 1} player={player.data} players={this.props.players} show={player.show} veto={this.getVeto()} />
|
||||
}
|
||||
renderMatch = () => {
|
||||
const { match } = this.state;
|
||||
if(!match.data || !match.teams[0] || !match.teams[1]) return null;
|
||||
return <MatchOverview match={match.data} show={match.show} veto={this.getVeto()} teams={match.teams}/>
|
||||
}
|
||||
renderTeam = () => {
|
||||
const { team } = this.state;
|
||||
if(!team.data) return null;
|
||||
return <TeamOverview team={team.data} show={team.show} veto={this.getVeto()} />
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderPlayer()}
|
||||
{this.renderMatch()}
|
||||
{this.renderTeam()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Overview;
|
||||
@ -1,17 +1,17 @@
|
||||
import { CSGO } from "csgogsi";
|
||||
import React from "react";
|
||||
import { PhaseRaw } from "csgogsi-socket";
|
||||
|
||||
interface IProps {
|
||||
phase: CSGO["phase_countdowns"] | null;
|
||||
phase: PhaseRaw | null
|
||||
}
|
||||
|
||||
const Pause = ({ phase }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
id={`pause`}
|
||||
className={phase && phase.phase === "paused" ? "show" : ""}
|
||||
>
|
||||
PAUSE
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Pause;
|
||||
export default class Pause extends React.Component<IProps> {
|
||||
render() {
|
||||
const { phase } = this.props;
|
||||
return (
|
||||
<div id={`pause`} className={phase && phase.phase === "paused" ? 'show' : ''}>
|
||||
PAUSE
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +1,22 @@
|
||||
import { Map, CSGO } from "csgogsi";
|
||||
import React from "react";
|
||||
import { Map, PhaseRaw } from "csgogsi-socket";
|
||||
|
||||
|
||||
interface IProps {
|
||||
phase: CSGO["phase_countdowns"] | null;
|
||||
map: Map;
|
||||
phase: PhaseRaw | null,
|
||||
map: Map
|
||||
}
|
||||
|
||||
const Timeout = ({ phase, map }: IProps) => {
|
||||
const time = phase && Math.abs(Math.ceil(phase.phase_ends_in));
|
||||
const team = phase && phase.phase === "timeout_t" ? map.team_t : map.team_ct;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`timeout`}
|
||||
className={`${
|
||||
time && time > 2 && phase &&
|
||||
(phase.phase === "timeout_t" || phase.phase === "timeout_ct")
|
||||
? "show"
|
||||
: ""
|
||||
} ${
|
||||
phase && (phase.phase === "timeout_t" || phase.phase === "timeout_ct")
|
||||
? phase.phase.substring(8)
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{team.name} TIMEOUT
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Timeout;
|
||||
export default class Timeout extends React.Component<IProps> {
|
||||
render() {
|
||||
const { phase, map } = this.props;
|
||||
const time = phase && Math.abs(Math.ceil(parseFloat(phase.phase_ends_in)));
|
||||
const team = phase && phase.phase === "timeout_t" ? map.team_t : map.team_ct;
|
||||
|
||||
return (
|
||||
<div id={`timeout`} className={`${time && time > 2 && phase && (phase.phase === "timeout_t" || phase.phase === "timeout_ct") ? 'show' : ''} ${phase && (phase.phase === "timeout_t" || phase.phase === "timeout_ct") ? phase.phase.substr(8): ''}`}>
|
||||
{ team.name } TIMEOUT
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import * as I from '../../API/types';
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import { avatars } from './../../api/avatars';
|
||||
import { apiUrl } from '../../api/api';
|
||||
import { getCountry } from '../countries';
|
||||
import { Player } from 'csgogsi';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import "./playeroverview.scss";
|
||||
import { apiUrl } from '../../API';
|
||||
|
||||
interface IProps {
|
||||
player: I.Player,
|
||||
@ -12,34 +14,21 @@ interface IProps {
|
||||
round: number
|
||||
}
|
||||
|
||||
const sum = (data: number[]) => data.reduce((a, b) => a + b, 0);
|
||||
export default class PlayerOverview extends React.Component<IProps> {
|
||||
sum = (data: number[]) => data.reduce((a, b) => a + b, 0);
|
||||
|
||||
const calcWidth = (val: number | string, max?: number) => {
|
||||
const value = Number(val);
|
||||
if(value === 0) return 0;
|
||||
let maximum = max;
|
||||
if(!maximum) {
|
||||
maximum = Math.ceil(value/100)*100;
|
||||
}
|
||||
if(value > maximum){
|
||||
return 100;
|
||||
}
|
||||
return 100*value/maximum;
|
||||
}
|
||||
|
||||
const PlayerOverview = ({ player, show, veto, players, round }: IProps) => {
|
||||
if(!player || !veto || !veto.rounds) return null;
|
||||
const getData = () => {
|
||||
if(!veto.rounds) return null;
|
||||
getData = () => {
|
||||
const { veto, player, round } = this.props;
|
||||
if(!player || !veto || !veto.rounds) return null;
|
||||
const stats = veto.rounds.map(round => round ? round.players[player.steamid] : {
|
||||
kills: 0,
|
||||
killshs: 0,
|
||||
damage: 0
|
||||
}).filter(data => !!data);
|
||||
const overall = {
|
||||
damage: sum(stats.map(round => round.damage)),
|
||||
kills: sum(stats.map(round => round.kills)),
|
||||
killshs: sum(stats.map(round => round.killshs)),
|
||||
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',
|
||||
@ -50,51 +39,66 @@ const PlayerOverview = ({ player, show, veto, players, round }: IProps) => {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = getData();
|
||||
if(!data) return null;
|
||||
const url = player.avatar;
|
||||
|
||||
const countryName = player.country ? getCountry(player.country) : null;
|
||||
let side = '';
|
||||
const inGamePlayer = players.find(inGamePlayer => inGamePlayer.steamid === player.steamid);
|
||||
if(inGamePlayer) side = inGamePlayer.team.side;
|
||||
return (
|
||||
<div className={`player-overview ${show ? 'show':''} ${side}`}>
|
||||
<div className="player-overview-picture">
|
||||
{url ? <img src={url} alt={`${player.username}'s avatar`}/> : null}
|
||||
</div>
|
||||
<div className="player-overview-username">{url && countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} className="flag" alt={countryName}/> : null }{player.username.toUpperCase()}</div>
|
||||
|
||||
<div className="player-overview-stats">
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KILLS: {data.kills}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${calcWidth(data.kills, data.kills <= 30 ? 30 : 40)}%`}}></div>
|
||||
</div>
|
||||
calcWidth = (val: number | string, max?: number) => {
|
||||
const value = Number(val);
|
||||
if(value === 0) return 0;
|
||||
let maximum = max;
|
||||
if(!maximum) {
|
||||
maximum = Math.ceil(value/100)*100;
|
||||
}
|
||||
if(value > maximum){
|
||||
return 100;
|
||||
}
|
||||
return 100*value/maximum;
|
||||
}
|
||||
render() {
|
||||
const { player, veto, players } = this.props;
|
||||
const data = this.getData();
|
||||
if(!player || !veto || !veto.rounds || !data) return null;
|
||||
let url = null;
|
||||
// const avatarData = avatars.find(avatar => avatar.steamid === player.steamid);
|
||||
const avatarData = avatars[player.steamid];
|
||||
if(avatarData && avatarData.url){
|
||||
url = avatarData.url;
|
||||
}
|
||||
const countryName = player.country ? getCountry(player.country) : null;
|
||||
let side = '';
|
||||
const inGamePlayer = players.find(inGamePlayer => inGamePlayer.steamid === player.steamid);
|
||||
if(inGamePlayer) side = inGamePlayer.team.side;
|
||||
return (
|
||||
<div className={`player-overview ${this.props.show ? 'show':''} ${side}`}>
|
||||
<div className="player-overview-picture">
|
||||
{url ? <img src={url} alt={`${player.username}'s avatar`}/> : null}
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">HS: {data.hsp}%</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${calcWidth(data.hsp, 100)}%`}}></div>
|
||||
<div className="player-overview-username">{url && countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} className="flag" alt={countryName}/> : null }{player.username.toUpperCase()}</div>
|
||||
|
||||
<div className="player-overview-stats">
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KILLS: {data.kills}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.kills, data.kills <= 30 ? 30 : 40)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">ADR: {data.adr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${calcWidth(data.adr)}%`}}></div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">HS: {data.hsp}%</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.hsp, 100)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KPR: {data.kpr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${calcWidth(Number(data.kpr)*100)}%`}}></div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">ADR: {data.adr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(data.adr)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="player-overview-stat">
|
||||
<div className="label">KPR: {data.kpr}</div>
|
||||
<div className="panel">
|
||||
<div className="filling" style={{width:`${this.calcWidth(Number(data.kpr)*100)}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerOverview;
|
||||
@ -1,50 +1,41 @@
|
||||
import CameraContainer from "../Camera/Container";
|
||||
import React from 'react';
|
||||
import CameraContainer from '../Camera/Container';
|
||||
import PlayerCamera from "./../Camera/Camera";
|
||||
|
||||
import { Skull } from "./../../assets/Icons";
|
||||
import { useConfig } from "../../API/contexts/actions";
|
||||
import { apiUrl } from "../../API";
|
||||
import { avatars } from './../../api/avatars';
|
||||
|
||||
import { Skull } from './../../assets/Icons';
|
||||
|
||||
interface IProps {
|
||||
steamid: string;
|
||||
url: string | null;
|
||||
slot?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
showSkull?: boolean;
|
||||
showCam?: boolean;
|
||||
sidePlayer?: boolean;
|
||||
teamId?: string | null
|
||||
steamid: string,
|
||||
slot?: number,
|
||||
height?: number,
|
||||
width?: number,
|
||||
showSkull?: boolean,
|
||||
showCam?: boolean,
|
||||
sidePlayer?: boolean
|
||||
}
|
||||
const Avatar = (
|
||||
{ steamid, url, height, width, showCam, showSkull, sidePlayer, teamId }: IProps,
|
||||
) => {
|
||||
const data = useConfig("display_settings");
|
||||
export default class Avatar extends React.Component<IProps> {
|
||||
|
||||
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 (
|
||||
<div className={`avatar`}>
|
||||
{showCam
|
||||
? (sidePlayer
|
||||
? (
|
||||
<div className="videofeed">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
export default Avatar;
|
||||
render() {
|
||||
const { showCam, steamid, width, height, showSkull, sidePlayer } = this.props;
|
||||
//const url = avatars.filter(avatar => avatar.steamid === this.props.steamid)[0];
|
||||
const avatarData = avatars[this.props.steamid];
|
||||
if (!avatarData || !avatarData.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`avatar`}>
|
||||
{
|
||||
showCam ? ( sidePlayer ? <div className="videofeed"><PlayerCamera steamid={steamid} visible={true} /></div> : <CameraContainer observedSteamid={steamid} />) : null
|
||||
}
|
||||
{
|
||||
showSkull ? <Skull height={height} width={width} /> : <img src={avatarData.url} height={height} width={width} alt={'Avatar'} />
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,86 +1,106 @@
|
||||
import React, { useState } from "react";
|
||||
import { Player } from "csgogsi";
|
||||
import React from "react";
|
||||
import { Player } from "csgogsi-socket";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import Avatar from "./Avatar";
|
||||
import TeamLogo from "./../MatchBar/TeamLogo";
|
||||
import "./observed.scss";
|
||||
import { apiUrl } from './../../api/api';
|
||||
import { getCountry } from "./../countries";
|
||||
import { ArmorHelmet, ArmorFull, HealthFull, Bullets } from './../../assets/Icons';
|
||||
import { apiUrl } from './../../API';
|
||||
import { useAction } from "../../API/contexts/actions";
|
||||
import { Veto } from "../../api/interfaces";
|
||||
import { actions } from "../../App";
|
||||
|
||||
|
||||
const Statistic = React.memo(({ label, value }: { label: string; value: string | number, }) => {
|
||||
return (
|
||||
<div className="stat">
|
||||
<div className="label">{label}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Observed = ({ player }: { player: Player | null }) => {
|
||||
const [ showCam, setShowCam ] = useState(true);
|
||||
|
||||
useAction('toggleCams', () => {
|
||||
setShowCam(p => !p);
|
||||
});
|
||||
|
||||
if (!player) return null;
|
||||
|
||||
const country = player.country || player.team.country;
|
||||
const currentWeapon = player.weapons.filter(weapon => weapon.state === "active")[0];
|
||||
const grenades = player.weapons.filter(weapon => weapon.type === "Grenade");
|
||||
const { stats } = player;
|
||||
const ratio = stats.deaths === 0 ? stats.kills : stats.kills / stats.deaths;
|
||||
const countryName = country ? getCountry(country) : null;
|
||||
return (
|
||||
<div className={`observed ${player.team.side}`}>
|
||||
<div className="main_row">
|
||||
<Avatar 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} />
|
||||
<div className="username_container">
|
||||
<div className="username">{player.name}</div>
|
||||
<div className="real_name">{player.realName}</div>
|
||||
</div>
|
||||
<div className="flag">{countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} alt={countryName} /> : ''}</div>
|
||||
<div className="grenade_container">
|
||||
{grenades.map(grenade => <React.Fragment key={`${player.steamid}_${grenade.name}_${grenade.ammo_reserve || 1}`}>
|
||||
<Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade />
|
||||
{
|
||||
grenade.ammo_reserve === 2 ? <Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade /> : null}
|
||||
</React.Fragment>)}
|
||||
</div>
|
||||
class Statistic extends React.PureComponent<{ label: string; value: string | number, }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="stat">
|
||||
<div className="label">{this.props.label}</div>
|
||||
<div className="value">{this.props.value}</div>
|
||||
</div>
|
||||
<div className="stats_row">
|
||||
<div className="health_armor_container">
|
||||
<div className="health-icon icon">
|
||||
<HealthFull />
|
||||
</div>
|
||||
<div className="health text">{player.state.health}</div>
|
||||
<div className="armor-icon icon">
|
||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull />}
|
||||
</div>
|
||||
<div className="health text">{player.state.armor}</div>
|
||||
</div>
|
||||
<div className="statistics">
|
||||
<Statistic label={"K"} value={stats.kills} />
|
||||
<Statistic label={"A"} value={stats.assists} />
|
||||
<Statistic label={"D"} value={stats.deaths} />
|
||||
<Statistic label={"K/D"} value={ratio.toFixed(2)} />
|
||||
</div>
|
||||
<div className="ammo">
|
||||
<div className="ammo_icon_container">
|
||||
<Bullets />
|
||||
</div>
|
||||
<div className="ammo_counter">
|
||||
<div className="ammo_clip">{(currentWeapon && currentWeapon.ammo_clip) || "-"}</div>
|
||||
<div className="ammo_reserve">/{(currentWeapon && currentWeapon.ammo_reserve) || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Observed;
|
||||
export default class Observed extends React.Component<{ player: Player | null, veto: Veto | null, round: number }, { showCam: boolean }> {
|
||||
constructor(props: any){
|
||||
super(props);
|
||||
this.state = {
|
||||
showCam: true
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
actions.on('toggleCams', () => {
|
||||
console.log(this.state.showCam)
|
||||
this.setState({ showCam: !this.state.showCam });
|
||||
});
|
||||
}
|
||||
getAdr = () => {
|
||||
const { veto, player } = this.props;
|
||||
if (!player || !veto || !veto.rounds) return null;
|
||||
const damageInRounds = veto.rounds.map(round => round ? round.players[player.steamid] : {
|
||||
kills: 0,
|
||||
killshs: 0,
|
||||
damage: 0
|
||||
}).filter(data => !!data).map(roundData => roundData.damage);
|
||||
|
||||
return damageInRounds.reduce((a, b) => a + b, 0) / (this.props.round - 1);
|
||||
}
|
||||
render() {
|
||||
if (!this.props.player) return '';
|
||||
const { player } = this.props;
|
||||
const country = player.country || player.team.country;
|
||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
||||
const currentWeapon = weapons.filter(weapon => weapon.state === "active")[0];
|
||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
||||
const { stats } = player;
|
||||
const ratio = stats.deaths === 0 ? stats.kills : stats.kills / stats.deaths;
|
||||
const countryName = country ? getCountry(country) : null;
|
||||
return (
|
||||
<div className={`observed ${player.team.side}`}>
|
||||
<div className="main_row">
|
||||
{<Avatar steamid={player.steamid} height={140} width={140} showCam={this.state.showCam} slot={player.observer_slot} />}
|
||||
<TeamLogo team={player.team} height={35} width={35} />
|
||||
<div className="username_container">
|
||||
<div className="username">{player.name}</div>
|
||||
<div className="real_name">{player.realName}</div>
|
||||
</div>
|
||||
<div className="flag">{countryName ? <img src={`${apiUrl}files/img/flags/${countryName.replace(/ /g, "-")}.png`} alt={countryName} /> : ''}</div>
|
||||
<div className="grenade_container">
|
||||
{grenades.map(grenade => <React.Fragment key={`${player.steamid}_${grenade.name}_${grenade.ammo_reserve || 1}`}>
|
||||
<Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade />
|
||||
{
|
||||
grenade.ammo_reserve === 2 ? <Weapon weapon={grenade.name} active={grenade.state === "active"} isGrenade /> : null}
|
||||
</React.Fragment>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats_row">
|
||||
<div className="health_armor_container">
|
||||
<div className="health-icon icon">
|
||||
<HealthFull />
|
||||
</div>
|
||||
<div className="health text">{player.state.health}</div>
|
||||
<div className="armor-icon icon">
|
||||
{player.state.helmet ? <ArmorHelmet /> : <ArmorFull />}
|
||||
</div>
|
||||
<div className="health text">{player.state.armor}</div>
|
||||
</div>
|
||||
<div className="statistics">
|
||||
<Statistic label={"K"} value={stats.kills} />
|
||||
<Statistic label={"A"} value={stats.assists} />
|
||||
<Statistic label={"D"} value={stats.deaths} />
|
||||
<Statistic label={"K/D"} value={ratio.toFixed(2)} />
|
||||
</div>
|
||||
<div className="ammo">
|
||||
<div className="ammo_icon_container">
|
||||
<Bullets />
|
||||
</div>
|
||||
<div className="ammo_counter">
|
||||
<div className="ammo_clip">{(currentWeapon && currentWeapon.ammo_clip) || "-"}</div>
|
||||
<div className="ammo_reserve">/{(currentWeapon && currentWeapon.ammo_reserve) || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import * as I from "csgogsi";
|
||||
import React from "react";
|
||||
import * as I from "csgogsi-socket";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import Avatar from "./Avatar";
|
||||
import Armor from "./../Indicators/Armor";
|
||||
import Bomb from "./../Indicators/Bomb";
|
||||
import Defuse from "./../Indicators/Defuse";
|
||||
import React from "react";
|
||||
|
||||
interface IProps {
|
||||
player: I.Player,
|
||||
@ -24,9 +24,9 @@ const compareWeapon = (weaponOne: I.WeaponRaw, weaponTwo: I.WeaponRaw) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const compareWeapons = (weaponsObjectOne: I.Weapon[], weaponsObjectTwo: I.Weapon[]) => {
|
||||
const weaponsOne = [...weaponsObjectOne].sort((a, b) => a.name.localeCompare(b.name))
|
||||
const weaponsTwo = [...weaponsObjectTwo].sort((a, b) => a.name.localeCompare(b.name))
|
||||
const compareWeapons = (weaponsObjectOne: { [key: string]: I.WeaponRaw }, weaponsObjectTwo: { [key: string]: I.WeaponRaw }) => {
|
||||
const weaponsOne = Object.values(weaponsObjectOne).sort((a, b) => (a.name as any) - (b.name as any))
|
||||
const weaponsTwo = Object.values(weaponsObjectTwo).sort((a, b) => (a.name as any) - (b.name as any))
|
||||
|
||||
if (weaponsOne.length !== weaponsTwo.length) return false;
|
||||
|
||||
@ -58,8 +58,6 @@ const arePlayersEqual = (playerOne: I.Player, playerTwo: I.Player) => {
|
||||
playerOne.state.equip_value === playerTwo.state.equip_value &&
|
||||
playerOne.state.adr === playerTwo.state.adr &&
|
||||
playerOne.avatar === playerTwo.avatar &&
|
||||
!!playerOne.team.id === !!playerTwo.team.id &&
|
||||
playerOne.team.side === playerTwo.team.side &&
|
||||
playerOne.country === playerTwo.country &&
|
||||
playerOne.realName === playerTwo.realName &&
|
||||
compareWeapons(playerOne.weapons, playerTwo.weapons)
|
||||
@ -67,20 +65,18 @@ const arePlayersEqual = (playerOne: I.Player, playerTwo: I.Player) => {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const Player = ({ player, isObserved }: IProps) => {
|
||||
|
||||
const weapons = player.weapons.map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
||||
const weapons = Object.values(player.weapons).map(weapon => ({ ...weapon, name: weapon.name.replace("weapon_", "") }));
|
||||
const primary = weapons.filter(weapon => !['C4', 'Pistol', 'Knife', 'Grenade', undefined].includes(weapon.type))[0] || null;
|
||||
const secondary = weapons.filter(weapon => weapon.type === "Pistol")[0] || null;
|
||||
const grenades = weapons.filter(weapon => weapon.type === "Grenade");
|
||||
const isLeft = player.team.orientation === "left";
|
||||
|
||||
const zeus = weapons.find(weapon => weapon.name === "taser");
|
||||
|
||||
return (
|
||||
<div className={`player ${player.state.health === 0 ? "dead" : ""} ${isObserved ? 'active' : ''}`}>
|
||||
<div className="player_data">
|
||||
<Avatar teamId={player.team.id} steamid={player.steamid} url={player.avatar} height={57} width={57} showSkull={false} showCam={false} sidePlayer={true} />
|
||||
<Avatar steamid={player.steamid} height={57} width={57} showSkull={false} showCam={false} sidePlayer={true} />
|
||||
<div className="dead-stats">
|
||||
<div className="labels">
|
||||
<div className="stat-label">K</div>
|
||||
@ -108,11 +104,10 @@ const Player = ({ player, isObserved }: IProps) => {
|
||||
<div className="row">
|
||||
<div className="armor_and_utility">
|
||||
<Bomb player={player} />
|
||||
<Armor health={player.state.health} armor={player.state.armor} helmet={player.state.helmet} />
|
||||
<Armor player={player} />
|
||||
<Defuse player={player} />
|
||||
</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">
|
||||
{grenades.map(grenade => (
|
||||
[
|
||||
@ -136,5 +131,5 @@ const arePropsEqual = (prevProps: Readonly<IProps>, nextProps: Readonly<IProps>)
|
||||
return arePlayersEqual(prevProps.player, nextProps.player);
|
||||
}
|
||||
|
||||
export default React.memo(Player, arePropsEqual);
|
||||
//export default Player;
|
||||
//export default React.memo(Player, arePropsEqual);
|
||||
export default Player;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import Player from './Player'
|
||||
import * as I from 'csgogsi';
|
||||
import * as I from 'csgogsi-socket';
|
||||
import './players.scss';
|
||||
|
||||
interface Props {
|
||||
@ -8,15 +9,17 @@ interface Props {
|
||||
side: 'right' | 'left',
|
||||
current: I.Player | null,
|
||||
}
|
||||
const TeamBox = ({players, team, side, current}: Props) => {
|
||||
return (
|
||||
<div className={`teambox ${team.side} ${side}`}>
|
||||
{players.map(player => <Player
|
||||
key={player.steamid}
|
||||
player={player}
|
||||
isObserved={!!(current && current.steamid === player.steamid)}
|
||||
/>)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default class TeamBox extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className={`teambox ${this.props.team.side} ${this.props.side}`}>
|
||||
{this.props.players.map(player => <Player
|
||||
key={player.steamid}
|
||||
player={player}
|
||||
isObserved={!!(this.props.current && this.props.current.steamid === player.steamid)}
|
||||
/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default TeamBox;
|
||||
@ -46,16 +46,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.weapon.zeus {
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
&.left {
|
||||
left: 170px;
|
||||
}
|
||||
&.right {
|
||||
right: 170px;
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
right: 10px;
|
||||
.player {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Bomb, Side } from 'csgogsi';
|
||||
import maps, { MapConfig, ZoomAreas } from './maps';
|
||||
import './index.scss';
|
||||
import React from 'react';
|
||||
import { Bomb } from 'csgogsi-socket';
|
||||
import maps, { ScaleConfig, MapConfig, ZoomAreas } from './maps';
|
||||
import './index.css';
|
||||
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||
import config from './config';
|
||||
import { parsePosition } from './utils';
|
||||
interface IProps {
|
||||
players: RadarPlayerObject[];
|
||||
grenades: RadarGrenadeObject[];
|
||||
@ -12,154 +12,99 @@ interface IProps {
|
||||
zoom?: ZoomAreas;
|
||||
mapConfig: MapConfig,
|
||||
reverseZoom: string,
|
||||
parsePosition: (position: number[], size: number, config: ScaleConfig) => number[]
|
||||
}
|
||||
|
||||
const isShooting = (lastShoot: number) => (new Date()).getTime() - lastShoot <= 250;
|
||||
|
||||
type Explosion = {
|
||||
position: number[],
|
||||
grenadeId: string
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
class App extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
players: [],
|
||||
grenades: [],
|
||||
bomb: null
|
||||
}
|
||||
}
|
||||
|
||||
if(type === "smoke" && (state === "landed" || state === "exploded")){
|
||||
renderGrenade = (grenade: RadarGrenadeObject) => {
|
||||
if ("flames" in grenade) {
|
||||
return null;
|
||||
}
|
||||
const { reverseZoom } = this.props;
|
||||
return (
|
||||
<div className={`grenade ${type} ${state} ${side || ''} ${visible ? 'visible':'hidden'}`}
|
||||
<div key={grenade.id} className={`grenade ${grenade.type} ${grenade.side || ''} ${grenade.state} ${grenade.visible ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${position[0]}px) translateY(${position[1]}px) translateZ(10px) scale(${reverseZoom})`,
|
||||
transform: `translateX(${grenade.position[0].toFixed(2)}px) translateY(${grenade.position[1].toFixed(2)}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 className="background"></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="background"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Bomb = ({ bomb, mapConfig, reverseZoom }: { reverseZoom: string, bomb?: Bomb | null, mapConfig: MapConfig }) => {
|
||||
if(!bomb) return null;
|
||||
if(bomb.state === "carried" || bomb.state === "planting") return null;
|
||||
if("config" in mapConfig){
|
||||
const position = parsePosition(bomb.position, mapConfig.config);
|
||||
if(!position) return null;
|
||||
|
||||
renderDot = (player: RadarPlayerObject) => {
|
||||
const { reverseZoom } = this.props;
|
||||
return (
|
||||
<div className={`bomb ${bomb.state} visible`}
|
||||
<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(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||
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="content">
|
||||
<div className="background-fire" style={{ transform: `rotate(${-90 + player.position[2]}deg)`, opacity: isShooting(player.lastShoot) ? 1 : 0 }} ><div className="bg"/></div>
|
||||
<div className="background" style={{ transform: `rotate(${45 + player.position[2]}deg)` }}></div>
|
||||
<div className="label">{player.label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderBomb = () => {
|
||||
const { bomb, mapConfig, reverseZoom } = this.props;
|
||||
if(!bomb) return null;
|
||||
if(bomb.state === "carried" || bomb.state === "planting") return null;
|
||||
if("config" in mapConfig){
|
||||
const position = this.props.parsePosition(bomb.position, 30, mapConfig.config);
|
||||
if(!position) return null;
|
||||
|
||||
return (
|
||||
<div className={`bomb ${bomb.state} visible`}
|
||||
style={{
|
||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||
}}>
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return mapConfig.configs.map(config => {
|
||||
const position = parsePosition(bomb.position, config.config);
|
||||
if(!position) return null;
|
||||
return (
|
||||
<div key={`bomb_${config.id}`} className={`bomb ${bomb.state} ${config.isVisible(bomb.position[2]) ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px) scale(${reverseZoom})`
|
||||
}}>
|
||||
<div className="content">
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
return mapConfig.configs.map(config => {
|
||||
const position = this.props.parsePosition(bomb.position, 30, config.config);
|
||||
if(!position) return null;
|
||||
return (
|
||||
<div className={`bomb ${bomb.state} ${config.isVisible(bomb.position[2]) ? 'visible':'hidden'}`}
|
||||
style={{
|
||||
transform: `translateX(${position[0].toFixed(2)}px) translateY(${position[1].toFixed(2)}px) translateZ(10px)`
|
||||
}}>
|
||||
<div className="explode-point"></div>
|
||||
<div className="background"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const { players, grenades, zoom } = this.props;
|
||||
|
||||
|
||||
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})` }
|
||||
const style: React.CSSProperties = { backgroundImage: `url(${maps[this.props.mapName].file})` }
|
||||
|
||||
if(zoom){
|
||||
style.transform = `scale(${zoom.zoom})`;
|
||||
style.transformOrigin = `${zoom.origin[0]}px ${zoom.origin[1]}px`;
|
||||
}
|
||||
const explosions = grenades.filter(grenade => grenade.type === "frag" && grenade.state === "exploded");
|
||||
//if(players.length === 0) return null;
|
||||
return <div className="map" style={style}>
|
||||
{players.map(player => <PlayerDot reverseZoom={reverseZoom} key={player.id} player={player} />)}
|
||||
{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} />)}
|
||||
<Bomb reverseZoom={reverseZoom} bomb={bomb} mapConfig={mapConfig} />
|
||||
{players.map(this.renderDot)}
|
||||
{grenades.map(this.renderGrenade)}
|
||||
{this.renderBomb()}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Radar;
|
||||
export default App;
|
||||
|
||||
@ -1,79 +1,322 @@
|
||||
import { Player, Bomb, Grenade, FragOrFireBombOrFlashbandGrenade } from 'csgogsi';
|
||||
import maps from './maps';
|
||||
import React from 'react';
|
||||
import { Player, Bomb } from 'csgogsi-socket';
|
||||
import maps, { ScaleConfig } from './maps';
|
||||
import LexoRadar from './LexoRadar';
|
||||
import { RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||
import { EXPLODE_TIME_FRAG, explosionPlaces, extendGrenade, extendPlayer, grenadesStates, playersStates } from './utils';
|
||||
import { GSI } from '../../../API/HUD';
|
||||
import { ExtendedGrenade, Grenade, RadarPlayerObject, RadarGrenadeObject } from './interface';
|
||||
import config from './config';
|
||||
|
||||
const DESCALE_ON_ZOOM = true;
|
||||
|
||||
|
||||
let playersStates: Player[][] = [];
|
||||
let grenadesStates: ExtendedGrenade[][] = [];
|
||||
const directions: Record<string, number> = {};
|
||||
type ShootingState = {
|
||||
ammo: number,
|
||||
weapon: string,
|
||||
lastShoot: number
|
||||
}
|
||||
let shootingState: Record<string, ShootingState> = {};
|
||||
|
||||
const calculateDirection = (player: Player) => {
|
||||
if (directions[player.steamid] && !player.state.health) return directions[player.steamid];
|
||||
|
||||
const [forwardV1, forwardV2] = player.forward;
|
||||
let direction = 0;
|
||||
|
||||
const [axisA, axisB] = [Math.asin(forwardV1), Math.acos(forwardV2)].map(axis => axis * 180 / Math.PI);
|
||||
|
||||
if (axisB < 45) {
|
||||
direction = Math.abs(axisA);
|
||||
} else if (axisB > 135) {
|
||||
direction = 180 - Math.abs(axisA);
|
||||
} else {
|
||||
direction = axisB;
|
||||
}
|
||||
|
||||
if (axisA < 0) {
|
||||
direction = -(direction -= 360);
|
||||
}
|
||||
|
||||
if (!directions[player.steamid]) {
|
||||
directions[player.steamid] = direction;
|
||||
}
|
||||
|
||||
const previous = directions[player.steamid];
|
||||
|
||||
let modifier = previous;
|
||||
modifier -= 360 * Math.floor(previous / 360);
|
||||
modifier = -(modifier -= direction);
|
||||
|
||||
if (Math.abs(modifier) > 180) {
|
||||
modifier -= 360 * Math.abs(modifier) / modifier;
|
||||
}
|
||||
directions[player.steamid] += modifier;
|
||||
|
||||
return directions[player.steamid];
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
players: Player[],
|
||||
bomb?: Bomb | null,
|
||||
player: Player | null,
|
||||
grenades: Grenade[]
|
||||
grenades?: any
|
||||
size?: number,
|
||||
mapName: string
|
||||
}
|
||||
|
||||
GSI.prependListener("data", () => {
|
||||
|
||||
const currentGrenades = GSI.current?.grenades || []
|
||||
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;
|
||||
}
|
||||
class App extends React.Component<IProps> {
|
||||
round = (n: number) => {
|
||||
const r = 0.02;
|
||||
return Math.round(n / r) * r;
|
||||
}
|
||||
for(const grenadeId of Object.keys(explosionPlaces)){
|
||||
const doesExist = data.grenades.some(grenade => grenade.id === grenadeId);
|
||||
if(!doesExist){
|
||||
delete explosionPlaces[grenadeId];
|
||||
|
||||
parsePosition = (position: number[], size: number, config: ScaleConfig) => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return [0, 0];
|
||||
}
|
||||
const left = config.origin.x + (position[0] * config.pxPerUX) - (size / 2);
|
||||
const top = config.origin.y + (position[1] * config.pxPerUY) - (size / 2);
|
||||
|
||||
return [this.round(left), this.round(top)];
|
||||
}
|
||||
});
|
||||
|
||||
const LexoRadarContainer = ({ size = 300, mapName, bomb, player, players, grenades }: IProps) => {
|
||||
const offset = (size - (size * size / 1024)) / 2;
|
||||
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];
|
||||
}
|
||||
|
||||
if (!(mapName in maps)) {
|
||||
return [x / entryAmount, y / entryAmount];
|
||||
}
|
||||
getPosition = (player: Player, mapConfig: ScaleConfig, scale: number) => {
|
||||
const playerData = playersStates.slice(0, 5).map(players => players.filter(pl => pl.steamid === player.steamid)[0]).filter(pl => !!pl);
|
||||
if (playerData.length === 0) return [0, 0];
|
||||
const positions = playerData.map(playerEntry => this.parsePosition(playerEntry.position, config.playerSize * scale, mapConfig));
|
||||
const entryAmount = positions.length;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (const position of positions) {
|
||||
x += position[0];
|
||||
y += position[1];
|
||||
}
|
||||
|
||||
const degree = calculateDirection(player);
|
||||
return [x / entryAmount, y / entryAmount, degree];
|
||||
}
|
||||
mapPlayer = (active: Player | null) => (player: Player): RadarPlayerObject | RadarPlayerObject[] | null => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const weapons = player.weapons ? Object.values(player.weapons) : [];
|
||||
const weapon = weapons.find(weapon => weapon.state === "active" && weapon.type !== "C4" && weapon.type !== "Knife" && weapon.type !== "Grenade");
|
||||
|
||||
const shooting: ShootingState = { ammo: weapon && weapon.ammo_clip || 0, weapon: weapon && weapon.name || '', lastShoot: 0 };
|
||||
|
||||
const lastShoot = shootingState[player.steamid] || shooting;
|
||||
|
||||
let isShooting = false;
|
||||
|
||||
if (shooting.weapon === lastShoot.weapon && shooting.ammo < lastShoot.ammo) {
|
||||
isShooting = true;
|
||||
}
|
||||
|
||||
shooting.lastShoot = isShooting ? (new Date()).getTime() : lastShoot.lastShoot;
|
||||
|
||||
shootingState[player.steamid] = shooting;
|
||||
|
||||
|
||||
const map = maps[this.props.mapName];
|
||||
const playerObject: RadarPlayerObject = {
|
||||
id: player.steamid,
|
||||
label: player.observer_slot !== undefined ? player.observer_slot : "",
|
||||
side: player.team.side,
|
||||
position: [],
|
||||
visible: true,
|
||||
isActive: !!active && active.steamid === player.steamid,
|
||||
forward: 0,
|
||||
steamid: player.steamid,
|
||||
isAlive: player.state.health > 0,
|
||||
hasBomb: !!Object.values(player.weapons).find(weapon => weapon.type === "C4"),
|
||||
flashed: player.state.flashed > 35,
|
||||
shooting: isShooting,
|
||||
lastShoot: shooting.lastShoot,
|
||||
scale: 1,
|
||||
player
|
||||
}
|
||||
if ("config" in map) {
|
||||
const scale = map.config.originHeight === undefined ? 1 : (1 + (player.position[2] - map.config.originHeight) / 1000);
|
||||
|
||||
playerObject.scale = scale;
|
||||
|
||||
const position = this.getPosition(player, map.config, scale);
|
||||
playerObject.position = position;
|
||||
|
||||
return playerObject;
|
||||
}
|
||||
return map.configs.map(config => {
|
||||
const scale = config.config.originHeight === undefined ? 1 : (1 + (player.position[2] - config.config.originHeight) / 750);
|
||||
|
||||
playerObject.scale = scale;
|
||||
|
||||
return ({
|
||||
...playerObject,
|
||||
position: this.getPosition(player, config.config, scale),
|
||||
id: `${player.steamid}_${config.id}`,
|
||||
visible: config.isVisible(player.position[2])
|
||||
})
|
||||
});
|
||||
}
|
||||
mapGrenade = (extGrenade: ExtendedGrenade) => {
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return null;
|
||||
}
|
||||
const map = maps[this.props.mapName];
|
||||
if (extGrenade.type === "inferno") {
|
||||
const mapFlame = (id: string) => {
|
||||
if ("config" in map) {
|
||||
return ({
|
||||
position: this.parsePosition(extGrenade.flames[id].split(", ").map(pos => Number(pos)), 12, map.config),
|
||||
id: `${id}_${extGrenade.id}`,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
return map.configs.map(config => ({
|
||||
id: `${id}_${extGrenade.id}_${config.id}`,
|
||||
visible: config.isVisible(extGrenade.flames[id].split(", ").map(Number)[2]),
|
||||
position: this.parsePosition(extGrenade.flames[id].split(", ").map(pos => Number(pos)), 12, config.config)
|
||||
}));
|
||||
}
|
||||
const flames = Object.keys(extGrenade.flames).map(mapFlame).flat();
|
||||
const flameObjects: RadarGrenadeObject[] = flames.map(flame => ({
|
||||
...flame,
|
||||
side: extGrenade.side,
|
||||
type: 'inferno',
|
||||
state: 'landed'
|
||||
}));
|
||||
return flameObjects;
|
||||
}
|
||||
|
||||
if ("config" in map) {
|
||||
const position = this.getGrenadePosition(extGrenade, map.config);
|
||||
if (!position) return null;
|
||||
const grenadeObject: RadarGrenadeObject = {
|
||||
type: extGrenade.type,
|
||||
state: 'inair',
|
||||
side: extGrenade.side,
|
||||
position,
|
||||
id: extGrenade.id,
|
||||
visible: true
|
||||
}
|
||||
if (extGrenade.type === "smoke") {
|
||||
if (extGrenade.effecttime !== "0.0") {
|
||||
grenadeObject.state = "landed";
|
||||
if (Number(extGrenade.effecttime) >= 16.5) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
} else if (extGrenade.type === 'flashbang' || extGrenade.type === 'frag') {
|
||||
if (Number(extGrenade.lifetime) >= 1.25) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
return grenadeObject;
|
||||
}
|
||||
return map.configs.map(config => {
|
||||
const position = this.getGrenadePosition(extGrenade, config.config);
|
||||
if (!position) return null;
|
||||
const grenadeObject: RadarGrenadeObject = {
|
||||
type: extGrenade.type,
|
||||
state: 'inair',
|
||||
side: extGrenade.side,
|
||||
position,
|
||||
id: `${extGrenade.id}_${config.id}`,
|
||||
visible: config.isVisible(extGrenade.position.split(", ").map(Number)[2])
|
||||
}
|
||||
if (extGrenade.type === "smoke") {
|
||||
if (extGrenade.effecttime !== "0.0") {
|
||||
grenadeObject.state = "landed";
|
||||
if (Number(extGrenade.effecttime) >= 16.5) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
} else if (extGrenade.type === 'flashbang' || extGrenade.type === 'frag') {
|
||||
if (Number(extGrenade.lifetime) >= 1.25) {
|
||||
grenadeObject.state = 'exploded';
|
||||
}
|
||||
}
|
||||
return grenadeObject;
|
||||
}).filter((grenade): grenade is RadarGrenadeObject => grenade !== null);
|
||||
|
||||
}
|
||||
getSideOfGrenade = (grenade: Grenade) => {
|
||||
const owner = this.props.players.find(player => player.steamid === grenade.owner);
|
||||
if (!owner) return null;
|
||||
return owner.team.side;
|
||||
}
|
||||
render() {
|
||||
const players: RadarPlayerObject[] = this.props.players.map(this.mapPlayer(this.props.player)).filter((player): player is RadarPlayerObject => player !== null).flat();
|
||||
playersStates.unshift(this.props.players);
|
||||
if (playersStates.length > 5) {
|
||||
playersStates = playersStates.slice(0, 5);
|
||||
}
|
||||
let grenades: RadarGrenadeObject[] = [];
|
||||
const currentGrenades = Object.keys(this.props.grenades as { [key: string]: Grenade }).map(grenadeId => ({ ...this.props.grenades[grenadeId], id: grenadeId, side: this.getSideOfGrenade(this.props.grenades[grenadeId]) })) as ExtendedGrenade[];
|
||||
if (currentGrenades) {
|
||||
grenades = currentGrenades.map(this.mapGrenade).filter(entry => entry !== null).flat() as RadarGrenadeObject[];
|
||||
grenadesStates.unshift(currentGrenades);
|
||||
}
|
||||
if (grenadesStates.length > 5) {
|
||||
grenadesStates = grenadesStates.slice(0, 5);
|
||||
}
|
||||
const size = this.props.size || 300;
|
||||
const offset = (size - (size * size / 1024)) / 2;
|
||||
|
||||
const config = maps[this.props.mapName];
|
||||
|
||||
const zooms = config && config.zooms || [];
|
||||
|
||||
const activeZoom = zooms.find(zoom => zoom.threshold(players.map(pl => pl.player)));
|
||||
|
||||
const reverseZoom = 1/(activeZoom && activeZoom.zoom || 1);
|
||||
|
||||
// s*(1024-s)/2048
|
||||
if (!(this.props.mapName in maps)) {
|
||||
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||
Unsupported map
|
||||
</div>;
|
||||
}
|
||||
return <div className="map-container" style={{ width: size, height: size, transform: `scale(${size / 1024})`, top: -offset, left: -offset }}>
|
||||
Unsupported map
|
||||
<LexoRadar
|
||||
players={players}
|
||||
grenades={grenades}
|
||||
parsePosition={this.parsePosition}
|
||||
bomb={this.props.bomb}
|
||||
mapName={this.props.mapName}
|
||||
mapConfig={maps[this.props.mapName]}
|
||||
zoom={activeZoom}
|
||||
reverseZoom={DESCALE_ON_ZOOM ? reverseZoom.toFixed(2) : '1'}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
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 }}>
|
||||
<LexoRadar
|
||||
players={playersExtended}
|
||||
grenades={grenadesExtended}
|
||||
bomb={bomb}
|
||||
mapName={mapName}
|
||||
mapConfig={config}
|
||||
zoom={activeZoom}
|
||||
reverseZoom={DESCALE_ON_ZOOM ? reverseZoom.toFixed(2) : '1'}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default LexoRadarContainer;
|
||||
export default App;
|
||||
|
||||
BIN
src/HUD/Radar/LexoRadar/assets/shootFire.zip
Normal file
BIN
src/HUD/Radar/LexoRadar/assets/shootFire.zip
Normal file
Binary file not shown.
@ -1,6 +1,5 @@
|
||||
const config = {
|
||||
playerSize: 70,
|
||||
smokeSize: 50
|
||||
playerSize: 60,
|
||||
}
|
||||
|
||||
export default config;
|
||||
@ -51,20 +51,12 @@ html, body, .map-container {
|
||||
}
|
||||
.map .player, .map .grenade, .map .bomb {
|
||||
position: absolute;
|
||||
height:0px;
|
||||
width:0px;
|
||||
height:30px;
|
||||
width:30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;/**/
|
||||
}
|
||||
.map .player .background {
|
||||
@ -74,7 +66,7 @@ html, body, .map-container {
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
transition:transform 0.2s linear;
|
||||
transition:transform 0.2s ease;
|
||||
}
|
||||
.map .player .background-fire {
|
||||
position: absolute;
|
||||
@ -148,12 +140,10 @@ html, body, .map-container {
|
||||
}
|
||||
*/
|
||||
.map .player.active {
|
||||
width:120%;
|
||||
height:120%;
|
||||
z-index: 3;
|
||||
}
|
||||
.map .player.active .content {
|
||||
width:48px;
|
||||
height:48px;
|
||||
}
|
||||
.map .grenade .background {
|
||||
border-radius:50%;
|
||||
background-size: contain;
|
||||
@ -167,6 +157,9 @@ html, body, .map-container {
|
||||
opacity: 1;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
.map .grenade.smoke {
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.map .grenade.smoke.inair .background {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
@ -174,6 +167,7 @@ html, body, .map-container {
|
||||
filter: invert(1);
|
||||
}
|
||||
.map .grenade.smoke.exploded .background {
|
||||
opacity: 0;
|
||||
}
|
||||
.map .grenade.flashbang, .map .grenade.frag {
|
||||
filter: invert(1);
|
||||
@ -189,8 +183,6 @@ html, body, .map-container {
|
||||
.map .grenade .explode-point, .map .bomb .explode-point {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
top: calc(50% - 1px);
|
||||
left: calc(50% - 1px);
|
||||
height: 2px;
|
||||
border-radius: 0.08px;
|
||||
}
|
||||
@ -202,6 +194,7 @@ html, body, .map-container {
|
||||
opacity: 0;
|
||||
}
|
||||
.map .grenade.smoke .background {
|
||||
border: 5px solid grey;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
.map .grenade.smoke.CT .background {
|
||||
@ -221,8 +214,9 @@ html, body, .map-container {
|
||||
width:12px;
|
||||
height:12px;
|
||||
}
|
||||
.map .grenade .content {
|
||||
position: absolute;
|
||||
.map .grenade.smoke {
|
||||
width:60px;
|
||||
height:60px;
|
||||
}
|
||||
.map .grenade.inferno .background {
|
||||
background-color: red;
|
||||
@ -276,29 +270,16 @@ html, body, .map-container {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.map .player.dead .background {
|
||||
display: none;
|
||||
}
|
||||
.map .player.dead .label {
|
||||
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 {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.map .grenade {
|
||||
&.smoke {
|
||||
&.exploded, &.landed {
|
||||
.background {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@keyframes Hidden {
|
||||
from {
|
||||
}
|
||||
to {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.map .hidden {
|
||||
opacity: 0;
|
||||
animation: Hidden 1s ease 1s 1;
|
||||
animation-fill-mode: forwards;/**/
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Player, Side, Grenade } from "csgogsi";
|
||||
import { Player, Side } from "csgogsi";
|
||||
|
||||
export interface RadarPlayerObject {
|
||||
id: string,
|
||||
@ -26,5 +26,30 @@ export interface RadarGrenadeObject {
|
||||
visible: boolean,
|
||||
id: string,
|
||||
}
|
||||
export interface GrenadeBase {
|
||||
owner: string,
|
||||
type: 'decoy' | 'smoke' | 'frag' | 'firebomb' | 'flashbang' | 'inferno'
|
||||
lifetime: string
|
||||
}
|
||||
|
||||
export type ExtendedGrenade = Grenade & { side: Side | null, };
|
||||
export interface DecoySmokeGrenade extends GrenadeBase {
|
||||
position: string,
|
||||
velocity: string,
|
||||
type: 'decoy' | 'smoke',
|
||||
effecttime: string,
|
||||
}
|
||||
|
||||
export interface DefaultGrenade extends GrenadeBase {
|
||||
position: string,
|
||||
type: 'frag' | 'firebomb' | 'flashbang',
|
||||
velocity: string,
|
||||
}
|
||||
|
||||
export interface InfernoGrenade extends GrenadeBase {
|
||||
type: 'inferno',
|
||||
flames: { [key: string]: string }
|
||||
}
|
||||
|
||||
export type Grenade = DecoySmokeGrenade | DefaultGrenade | InfernoGrenade;
|
||||
|
||||
export type ExtendedGrenade = Grenade & { id: string, side: Side | null, };
|
||||
15
src/HUD/Radar/LexoRadar/maps/de_anubis/index.ts
Normal file
15
src/HUD/Radar/LexoRadar/maps/de_anubis/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import radar from './radar.png'
|
||||
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 536.3392873296655,
|
||||
"y": 638.0789844851904
|
||||
},
|
||||
"pxPerUX": 0.1907910426894958,
|
||||
"pxPerUY": -0.18993888105312648
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
export default config;
|
||||
BIN
src/HUD/Radar/LexoRadar/maps/de_anubis/radar.png
Normal file
BIN
src/HUD/Radar/LexoRadar/maps/de_anubis/radar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@ -7,7 +7,8 @@ const config = {
|
||||
"y": 340.2921393569175
|
||||
},
|
||||
"pxPerUX": 0.20118507589946494,
|
||||
"pxPerUY": -0.20138282875746794
|
||||
"pxPerUY": -0.20138282875746794,
|
||||
"originHeight": -170,
|
||||
},
|
||||
"file": radar
|
||||
}
|
||||
|
||||
@ -3,11 +3,11 @@ import radar from './radar.png'
|
||||
const config = {
|
||||
"config": {
|
||||
"origin": {
|
||||
"x": 557.7279495268139,
|
||||
"y": 507.83243734804853
|
||||
"x": 527.365542903922,
|
||||
"y": 511.81469648562296
|
||||
},
|
||||
"pxPerUX": 0.22712933753943218,
|
||||
"pxPerUY": -0.23013108811174968
|
||||
"pxPerUX": 0.21532584158170223,
|
||||
"pxPerUY": -0.21299254526091588
|
||||
},
|
||||
"file":radar
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 44 KiB |
@ -1,3 +1,4 @@
|
||||
import { Player } from 'csgogsi-socket';
|
||||
import radar from './radar.png'
|
||||
|
||||
const high = {
|
||||
@ -31,6 +32,21 @@ const config = {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 77 KiB |
@ -6,15 +6,17 @@ 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';
|
||||
import { Player } from 'csgogsi';
|
||||
import api from '../../../../api/api';
|
||||
import { Player } from 'csgogsi-socket';
|
||||
|
||||
export type ZoomAreas = {
|
||||
threshold: (players: Player[]) => boolean;
|
||||
origin: number[],
|
||||
zoom: number
|
||||
}
|
||||
|
||||
export interface ScaleConfig {
|
||||
origin: {
|
||||
x:number,
|
||||
@ -27,7 +29,7 @@ export interface ScaleConfig {
|
||||
|
||||
interface SingleLayer {
|
||||
config: ScaleConfig,
|
||||
file: string
|
||||
file: string,
|
||||
zooms?: ZoomAreas[]
|
||||
}
|
||||
|
||||
@ -37,7 +39,7 @@ interface DoubleLayer {
|
||||
config: ScaleConfig,
|
||||
isVisible: (height: number) => boolean
|
||||
}[],
|
||||
file: string
|
||||
file: string,
|
||||
zooms?: ZoomAreas[]
|
||||
}
|
||||
|
||||
@ -52,7 +54,8 @@ const maps: { [key: string] : MapConfig} = {
|
||||
de_overpass,
|
||||
de_nuke,
|
||||
de_vertigo,
|
||||
de_ancient
|
||||
de_ancient,
|
||||
de_anubis
|
||||
}
|
||||
|
||||
api.maps.get().then(fallbackMaps => {
|
||||
|
||||
@ -1,244 +0,0 @@
|
||||
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,20 +1,50 @@
|
||||
import { CSGO } from "csgogsi";
|
||||
import React from "react";
|
||||
import { isDev } from './../../api/api';
|
||||
import { CSGO } from "csgogsi-socket";
|
||||
import LexoRadarContainer from './LexoRadar/LexoRadarContainer';
|
||||
|
||||
|
||||
|
||||
interface Props { radarSize: number, game: CSGO }
|
||||
|
||||
const Radar = ({ radarSize, game }: Props) => {
|
||||
const { players, player, bomb, grenades, map } = game;
|
||||
return <LexoRadarContainer
|
||||
players={players}
|
||||
player={player}
|
||||
bomb={bomb}
|
||||
grenades={grenades}
|
||||
size={radarSize}
|
||||
mapName={map.name.substring(map.name.lastIndexOf('/')+1)}
|
||||
/>
|
||||
interface State {
|
||||
showRadar: boolean,
|
||||
loaded: boolean,
|
||||
boltobserv:{
|
||||
css: boolean,
|
||||
maps: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default Radar;
|
||||
export default class Radar extends React.Component<Props, State> {
|
||||
state = {
|
||||
showRadar: true,
|
||||
loaded: !isDev,
|
||||
boltobserv: {
|
||||
css: true,
|
||||
maps: true
|
||||
}
|
||||
}
|
||||
async componentDidMount(){
|
||||
/*if(isDev){
|
||||
const response = await fetch('hud.json');
|
||||
const hud = await response.json();
|
||||
const boltobserv = {
|
||||
css: Boolean(hud && hud.boltobserv && hud.boltobserv.css),
|
||||
maps: Boolean(hud && hud.boltobserv && hud.boltobserv.maps)
|
||||
}
|
||||
this.setState({boltobserv, loaded: true});
|
||||
}*/
|
||||
}
|
||||
|
||||
render() {
|
||||
const { players, player, bomb, grenades, map } = this.props.game;
|
||||
return <LexoRadarContainer
|
||||
players={players}
|
||||
player={player}
|
||||
bomb={bomb}
|
||||
grenades={grenades}
|
||||
size={this.props.radarSize}
|
||||
mapName={map.name.substring(map.name.lastIndexOf('/')+1)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,58 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import React from "react";
|
||||
import "./radar.scss";
|
||||
import { Match, Veto } from "../../API/types";
|
||||
import { Map, CSGO, Team } from 'csgogsi';
|
||||
import { Match, Veto } from "../../api/interfaces";
|
||||
import { Map, CSGO, Team } from 'csgogsi-socket';
|
||||
import { actions } from './../../App';
|
||||
import Radar from './Radar'
|
||||
|
||||
import { useAction } from "../../API/contexts/actions";
|
||||
import TeamLogo from "../MatchBar/TeamLogo";
|
||||
|
||||
interface Props { match: Match | null, map: Map, game: CSGO }
|
||||
interface State { showRadar: boolean, radarSize: number, showBig: boolean }
|
||||
|
||||
const RadarMaps = ({ match, map, game }: Props) => {
|
||||
const [ radarSize, setRadarSize ] = useState(366);
|
||||
const [ showBig, setShowBig ] = useState(false);
|
||||
export default class RadarMaps extends React.Component<Props, State> {
|
||||
state = {
|
||||
showRadar: true,
|
||||
radarSize: 350,
|
||||
showBig: false
|
||||
}
|
||||
componentDidMount() {
|
||||
actions.on('radarBigger', () => this.radarChangeSize(20));
|
||||
actions.on('radarSmaller', () => this.radarChangeSize(-20));
|
||||
actions.on('toggleRadar', () => { this.setState(state => ({ showRadar: !state.showRadar })) });
|
||||
|
||||
useAction('radarBigger', () => {
|
||||
setRadarSize(p => p+10);
|
||||
}, []);
|
||||
|
||||
useAction('radarSmaller', () => {
|
||||
setRadarSize(p => p-10);
|
||||
}, []);
|
||||
|
||||
useAction('toggleRadarView', () => {
|
||||
setShowBig(p => !p);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id={`radar_maps_container`} className={` ${showBig ? 'preview':''}`}>
|
||||
{match ? <MapsBar match={match} map={map} game={game} /> : null}
|
||||
<Radar radarSize={showBig ? 600: radarSize} game={game} />
|
||||
</div>
|
||||
);
|
||||
actions.on("toggleRadarView", () => {
|
||||
this.setState({showBig:!this.state.showBig});
|
||||
});
|
||||
}
|
||||
radarChangeSize = (delta: number) => {
|
||||
const newSize = this.state.radarSize + delta;
|
||||
this.setState({ radarSize: newSize > 0 ? newSize : this.state.radarSize });
|
||||
}
|
||||
render() {
|
||||
const { match } = this.props;
|
||||
const { radarSize, showBig, showRadar } = this.state;
|
||||
const size = showBig ? 600 : radarSize;
|
||||
return (
|
||||
<div id={`radar_maps_container`} className={`${!showRadar ? 'hide' : ''} ${showBig ? 'preview':''}`}>
|
||||
<div className="radar-component-container" style={{width: `${size}px`, height: `${size}px`}}><Radar radarSize={size} game={this.props.game} /></div>
|
||||
{match ? <MapsBar match={this.props.match} map={this.props.map} game={this.props.game} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RadarMaps;
|
||||
|
||||
const MapsBar = ({ match, map }: Props) => {
|
||||
if (!match || !match.vetos.length) return '';
|
||||
const picks = match.vetos.filter(veto => veto.type !== "ban" && veto.mapName);
|
||||
if (picks.length > 3) {
|
||||
const current = picks.find(veto => map.name.includes(veto.mapName));
|
||||
if (!current) return null;
|
||||
class MapsBar extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { match, map } = this.props;
|
||||
if (!match || !match.vetos.length) return '';
|
||||
const picks = match.vetos.filter(veto => veto.type !== "ban" && veto.mapName);
|
||||
if (picks.length > 3) {
|
||||
const current = picks.find(veto => map.name.includes(veto.mapName));
|
||||
if (!current) return null;
|
||||
return <div id="maps_container">
|
||||
{<MapEntry veto={current} map={map} team={current.type === "decider" ? null : map.team_ct.id === current.teamId ? map.team_ct : map.team_t} />}
|
||||
</div>
|
||||
}
|
||||
return <div id="maps_container">
|
||||
<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} />}
|
||||
{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>
|
||||
}
|
||||
return <div id="maps_container">
|
||||
<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>
|
||||
}
|
||||
|
||||
const MapEntry = ({ veto, map }: { veto: Veto, map: Map, team: Team | null }) => {
|
||||
return <div className="veto_entry">
|
||||
<div className={`map_name ${map.name.includes(veto.mapName) ? 'active' : ''}`}>{veto.mapName.replace("de_", "")}</div>
|
||||
</div>
|
||||
class MapEntry extends React.PureComponent<{ veto: Veto, map: Map, team: Team | null }> {
|
||||
render() {
|
||||
const { veto, map, team } = this.props;
|
||||
return <div className="veto_entry">
|
||||
<div className="team_logo">{team ? <TeamLogo team={team} /> : null}</div>
|
||||
<div className={`map_name ${map.name.includes(veto.mapName) ? 'active' : ''}`}>{veto.mapName}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
border: none;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
transition: all 1s;
|
||||
.map-container {
|
||||
transition: all 1s;
|
||||
@ -23,24 +24,18 @@
|
||||
perspective: 500px;
|
||||
}
|
||||
}
|
||||
.radar-component-container {
|
||||
width: 350px;
|
||||
height:350px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#maps_container {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
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;
|
||||
}
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.veto_entry {
|
||||
display: flex;
|
||||
@ -64,15 +59,8 @@
|
||||
width: 23px;
|
||||
}
|
||||
}
|
||||
.map_name {
|
||||
font-size:12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.5;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.map_name.active {
|
||||
text-shadow: 0 0 15px white;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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,47 +1,45 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
const LossBox = React.memo(({ active, side }: { active: boolean; side: "CT" | "T" }) => {
|
||||
return (
|
||||
<div
|
||||
className={`loss-box ${side} ${
|
||||
active ? "active" : ""
|
||||
}`}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface Props {
|
||||
side: "left" | "right";
|
||||
team: "CT" | "T";
|
||||
loss: number;
|
||||
equipment: number;
|
||||
money: number;
|
||||
show: boolean;
|
||||
class LossBox extends React.PureComponent<{ active: boolean, side: 'CT' | 'T' }>{
|
||||
render(){
|
||||
return <div className={`loss-box ${this.props.side} ${this.props.active ? 'active':''}`}></div>
|
||||
}
|
||||
}
|
||||
|
||||
const Money = ({ side, team, loss, equipment, money, show }: Props) => {
|
||||
return (
|
||||
<div className={`moneybox ${side} ${team} ${show ? "show" : "hide"}`}>
|
||||
<div className="loss_container">
|
||||
<LossBox side={team} active={(loss - 1400) / 500 >= 4} />
|
||||
<LossBox side={team} active={(loss - 1400) / 500 >= 3} />
|
||||
<LossBox side={team} active={(loss - 1400) / 500 >= 2} />
|
||||
<LossBox side={team} active={(loss - 1400) / 500 >= 1} />
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Loss Bonus</div>
|
||||
<div className="value">${loss}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Team Money</div>
|
||||
<div className="value">${money}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Equipment Value</div>
|
||||
<div className="value">${equipment}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Money;
|
||||
interface Props {
|
||||
side: 'left' | 'right',
|
||||
team: 'CT' | 'T',
|
||||
loss: number,
|
||||
equipment: number,
|
||||
money: number,
|
||||
show: boolean,
|
||||
}
|
||||
|
||||
export default class Money extends React.PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`moneybox ${this.props.side} ${this.props.team} ${this.props.show ? "show" : "hide"}`}>
|
||||
<div className="loss_container">
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 4} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 3} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 2} />
|
||||
<LossBox side={this.props.team} active={(this.props.loss-1400)/500 >= 1} />
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Loss Bonus</div>
|
||||
<div className="value">${this.props.loss}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Team Money</div>
|
||||
<div className="value">${this.props.money}</div>
|
||||
</div>
|
||||
<div className="money_container">
|
||||
<div className="title">Equipment Value</div>
|
||||
<div className="value">${this.props.equipment}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,32 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import './sideboxes.scss'
|
||||
import { apiUrl } from './../../API';
|
||||
import { useConfig, useOnConfigChange } from '../../API/contexts/actions';
|
||||
import { hudIdentity } from '../../API/HUD';
|
||||
import {configs, hudIdentity} from './../../App';
|
||||
import { apiUrl } from '../../api/api';
|
||||
|
||||
const Sidebox = ({side, hide} : { side: 'left' | 'right', hide: boolean}) => {
|
||||
const [ image, setImage ] = useState<string | null>(null);
|
||||
const data = useConfig('display_settings');
|
||||
export default class SideBox extends React.Component<{ side: 'left' | 'right', hide: boolean}, { title: string, subtitle: string, image?: string }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title:'Title',
|
||||
subtitle:'Content',
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
componentDidMount() {
|
||||
configs.onChange((data:any) => {
|
||||
if(!data) return;
|
||||
const display = data.display_settings;
|
||||
if(!display) return;
|
||||
if(`${this.props.side}_title` in display){
|
||||
this.setState({title:display[`${this.props.side}_title`]})
|
||||
}
|
||||
if(`${this.props.side}_subtitle` in display){
|
||||
this.setState({subtitle:display[`${this.props.side}_subtitle`]})
|
||||
}
|
||||
if(`${this.props.side}_image` in display){
|
||||
const imageUrl = `${apiUrl}api/huds/${hudIdentity.name || 'dev'}/display_settings/${this.props.side}_image?isDev=${hudIdentity.isDev}&cache=${(new Date()).getTime()}`;
|
||||
this.setState({image:imageUrl})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { image, title, subtitle} = this.state;
|
||||
if(!title) return '';
|
||||
return (
|
||||
<div className={`sidebox ${this.props.side} ${this.props.hide ? 'hide':''}`}>
|
||||
<div className="title_container">
|
||||
<div className="title">{title}</div>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
</div>
|
||||
<div className="image_container">
|
||||
{image ? <img src={image} id={`image_left`} alt={'Left'}/>:''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="image_container">
|
||||
{image ? <img src={image} id={`image_left`} alt={'Left'}/>:null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Sidebox;
|
||||
@ -1,11 +1,12 @@
|
||||
import React from "react";
|
||||
import Weapon from "./../Weapon/Weapon";
|
||||
import { Player, Side, WeaponRaw } from "csgogsi";
|
||||
import { Player, WeaponRaw, Side } from "csgogsi-socket";
|
||||
|
||||
interface Props {
|
||||
sides?: "reversed";
|
||||
show: boolean;
|
||||
side: "CT" | "T";
|
||||
players: Player[];
|
||||
sides?: 'reversed',
|
||||
show: boolean;
|
||||
side: 'CT' | 'T',
|
||||
players: Player[]
|
||||
}
|
||||
|
||||
function utilityState(amount: number) {
|
||||
@ -48,30 +49,17 @@ function utilityColor(amount: number) {
|
||||
|
||||
function sum(grenades: WeaponRaw[], name: string) {
|
||||
return (
|
||||
grenades.filter((grenade) => grenade.name === name).reduce(
|
||||
(prev, next) => ({
|
||||
...next,
|
||||
ammo_reserve: (prev.ammo_reserve || 0) + (next.ammo_reserve || 0),
|
||||
}),
|
||||
{ name: "", ammo_reserve: 0 },
|
||||
)
|
||||
grenades.filter(grenade => grenade.name === name).reduce((prev, next) => ({ ...next, ammo_reserve: (prev.ammo_reserve || 0) + (next.ammo_reserve || 0) }), { name: "", ammo_reserve: 0 })
|
||||
.ammo_reserve || 0
|
||||
);
|
||||
}
|
||||
|
||||
function parseGrenades(players: Player[], side: Side) {
|
||||
const grenades = players
|
||||
.filter((player) => player.team.side === side)
|
||||
.map((player) =>
|
||||
Object.values(player.weapons).filter((weapon) =>
|
||||
weapon.type === "Grenade"
|
||||
)
|
||||
)
|
||||
.filter(player => player.team.side === side)
|
||||
.map(player => Object.values(player.weapons).filter(weapon => weapon.type === "Grenade"))
|
||||
.flat()
|
||||
.map((grenade) => ({
|
||||
...grenade,
|
||||
name: grenade.name.replace("weapon_", ""),
|
||||
}));
|
||||
.map(grenade => ({ ...grenade, name: grenade.name.replace("weapon_", "") }));
|
||||
return grenades;
|
||||
}
|
||||
|
||||
@ -81,44 +69,40 @@ export function summarise(players: Player[], side: Side) {
|
||||
hg: sum(grenades, "hegrenade"),
|
||||
flashes: sum(grenades, "flashbang"),
|
||||
smokes: sum(grenades, "smokegrenade"),
|
||||
inc: sum(grenades, "incgrenade") + sum(grenades, "molotov"),
|
||||
inc: sum(grenades, "incgrenade") + sum(grenades, "molotov")
|
||||
};
|
||||
}
|
||||
|
||||
const GrenadeContainer = (
|
||||
{ grenade, amount }: { grenade: string; amount: number },
|
||||
) => {
|
||||
return (
|
||||
<div className="grenade_container">
|
||||
<div className="grenade_image">
|
||||
<Weapon weapon={grenade} active={false} isGrenade />
|
||||
</div>
|
||||
<div className="grenade_amount">x{amount}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SideBox = ({ players, side, show }: Props) => {
|
||||
const grenades = summarise(players, side);
|
||||
const total = Object.values(grenades).reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
<div className={`utilitybox ${side || ""} ${show ? "show" : "hide"}`}>
|
||||
<div className="title_container">
|
||||
<div className="title">Utility Level - </div>
|
||||
<div className="subtitle" style={{ color: utilityColor(total) }}>
|
||||
{utilityState(total)}
|
||||
class GrenadeContainer extends React.PureComponent<{ grenade: string; amount: number }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="grenade_container">
|
||||
<div className="grenade_image">
|
||||
<Weapon weapon={this.props.grenade} active={false} isGrenade />
|
||||
</div>
|
||||
<div className="grenade_amount">x{this.props.amount}</div>
|
||||
</div>
|
||||
<div className="grenades_container">
|
||||
<GrenadeContainer grenade="smokegrenade" amount={grenades.smokes} />
|
||||
<GrenadeContainer
|
||||
grenade={side === "CT" ? "incgrenade" : "molotov"}
|
||||
amount={grenades.inc}
|
||||
/>
|
||||
<GrenadeContainer grenade="flashbang" amount={grenades.flashes} />
|
||||
<GrenadeContainer grenade="hegrenade" amount={grenades.hg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SideBox;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SideBox extends React.Component<Props> {
|
||||
render() {
|
||||
const grenades = summarise(this.props.players, this.props.side);
|
||||
const total = Object.values(grenades).reduce((a, b) => a+b, 0);
|
||||
return (
|
||||
<div className={`utilitybox ${this.props.side || ''} ${this.props.show ? "show" : "hide"}`}>
|
||||
<div className="title_container">
|
||||
<div className="title">Utility Level - </div>
|
||||
<div className="subtitle" style={{color: utilityColor(total)}}>{utilityState(total)}</div>
|
||||
</div>
|
||||
<div className="grenades_container">
|
||||
<GrenadeContainer grenade="smokegrenade" amount={grenades.smokes} />
|
||||
<GrenadeContainer grenade={this.props.side === 'CT' ? 'incgrenade' : 'molotov'} amount={grenades.inc} />
|
||||
<GrenadeContainer grenade="flashbang" amount={grenades.flashes} />
|
||||
<GrenadeContainer grenade="hegrenade" amount={grenades.hg} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
18
src/HUD/TeamOverview/TeamOverview.tsx
Normal file
18
src/HUD/TeamOverview/TeamOverview.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import * as I from '../../api/interfaces';
|
||||
import "./teamoverview.scss";
|
||||
|
||||
interface IProps {
|
||||
team: I.Team,
|
||||
show: boolean,
|
||||
veto: I.Veto | null
|
||||
}
|
||||
|
||||
export default class TeamOverview extends React.Component<IProps> {
|
||||
render() {
|
||||
if(!this.props.team) return null;
|
||||
return (
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -39,4 +39,4 @@
|
||||
flex: unset;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,49 @@
|
||||
import { MAX_TIMER, useBombTimer } from "./Countdown";
|
||||
import React from "react";
|
||||
|
||||
import { GSI } from "./../../App";
|
||||
import BombTimer from "./Countdown";
|
||||
import { C4 } from "./../../assets/Icons";
|
||||
|
||||
const Bomb = () => {
|
||||
const bombData = useBombTimer();
|
||||
const show = bombData.state === "planted" || bombData.state === "defusing";
|
||||
|
||||
return (
|
||||
<div id={`bomb_container`}>
|
||||
<div className={`bomb_timer ${show ? "show" : "hide"}`} style={{ height: `${bombData.bombTime*100/MAX_TIMER.bomb}%` }}></div>
|
||||
<div className={`bomb_icon ${show ? "show" : "hide"}`}>
|
||||
<C4 fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default class Bomb extends React.Component<any, { height: number; show: boolean }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: 0,
|
||||
show: false
|
||||
};
|
||||
}
|
||||
hide = () => {
|
||||
this.setState({ show: false, height: 100 });
|
||||
};
|
||||
componentDidMount() {
|
||||
const bomb = new BombTimer(time => {
|
||||
let height = time > 40 ? 4000 : time * 100;
|
||||
this.setState({ height: height / 40 });
|
||||
});
|
||||
bomb.onReset(this.hide);
|
||||
GSI.on("data", data => {
|
||||
if (data.bomb && data.bomb.countdown) {
|
||||
if (data.bomb.state === "planted") {
|
||||
this.setState({ show: true });
|
||||
return bomb.go(data.bomb.countdown);
|
||||
}
|
||||
if (data.bomb.state !== "defusing") {
|
||||
this.hide();
|
||||
}
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default Bomb;
|
||||
render() {
|
||||
return (
|
||||
<div id={`bomb_container`}>
|
||||
<div className={`bomb_timer ${this.state.show ? "show" : "hide"}`} style={{ height: `${this.state.height}%` }}></div>
|
||||
<div className={`bomb_icon ${this.state.show ? "show" : "hide"}`}>
|
||||
<C4 fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,83 +1,46 @@
|
||||
import { Bomb, Events, Player } from "csgogsi";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { GSI } from "../../API/HUD";
|
||||
export default class Countdown {
|
||||
last: number;
|
||||
time: number;
|
||||
step: (time: number) => void;
|
||||
on: boolean;
|
||||
resetFunc?: Function;
|
||||
|
||||
export const MAX_TIMER = {
|
||||
planting: 3,
|
||||
defuse_kit: 5,
|
||||
defuse_nokit: 10,
|
||||
bomb: 40
|
||||
}
|
||||
const findNewTime = (current: number, newTime: number) => Math.abs(current - newTime) > 2 ? newTime : current;
|
||||
|
||||
export const useBombTimer = () => {
|
||||
const [ player, setPlayerSteamId ] = useState<Player | null>(null);
|
||||
const [ bombState, setBombState ] = useState<Bomb["state"] | null>(null);
|
||||
const [ site, setBombSite ] = useState<string | null>(null);
|
||||
|
||||
const [ plantTime, setPlantTime ] = useState(0);
|
||||
const [ bombTime, setBombTime ] = useState(0);
|
||||
const [ defuseTime, setDefuseTime ] = useState(0);
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
setPlantTime(curr => findNewTime(curr, plantNewTime));
|
||||
setDefuseTime(curr => findNewTime(curr, defuseNewTime));
|
||||
setBombTime(p => state === "planted" ? findNewTime(p, countdown) : p);
|
||||
}
|
||||
|
||||
GSI.on("data", onData);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
rAFRef.current = requestAnimationFrame(animationFrame);
|
||||
|
||||
return () => {
|
||||
GSI.off("data", onData);
|
||||
if(rAFRef.current) cancelAnimationFrame(rAFRef.current);
|
||||
constructor(step: (time: number) => void){
|
||||
this.last = 0;
|
||||
this.time = 0;
|
||||
this.on = false;
|
||||
this.step = step;
|
||||
}
|
||||
onReset(func: Function) {
|
||||
this.resetFunc = func;
|
||||
}
|
||||
stepWrapper = (time: number) =>{
|
||||
if(this.time < 0) return this.reset();
|
||||
if(!this.on) return this.reset();
|
||||
if(!this.last) this.last = time;
|
||||
if(this.time !== Number((this.time - (time - this.last)/1000))){
|
||||
this.time = Number((this.time - (time - this.last)/1000));
|
||||
this.step(this.time);
|
||||
}
|
||||
this.last =time;
|
||||
|
||||
|
||||
}, []);
|
||||
if(this.last) requestAnimationFrame(this.stepWrapper)
|
||||
}
|
||||
|
||||
go(duration: string | number){
|
||||
//console.log("STARTED WITH ", duration);
|
||||
if(typeof duration === "string") duration = Number(duration);
|
||||
if(Math.abs(duration - this.time) > 2) this.time = duration;
|
||||
this.on = true;
|
||||
if(!this.last ) requestAnimationFrame(this.stepWrapper);
|
||||
|
||||
return ({
|
||||
state: bombState,
|
||||
player,
|
||||
site,
|
||||
defuseTime,
|
||||
bombTime,
|
||||
plantTime
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reset(){
|
||||
this.last = 0;
|
||||
this.time = 0;
|
||||
this.on = false;
|
||||
if(this.resetFunc) this.resetFunc();
|
||||
}
|
||||
}
|
||||
@ -1,39 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { Timer } from "../MatchBar/MatchBar";
|
||||
import { Player } from "csgogsi";
|
||||
import * as I from "./../../assets/Icons";
|
||||
import { MAX_TIMER } from "./Countdown";
|
||||
|
||||
interface IProps {
|
||||
timer: Timer | null;
|
||||
side: "right" | "left"
|
||||
}
|
||||
|
||||
const getCaption = (type: "defusing" | "planting", player: Player | null) => {
|
||||
if(!player) return null;
|
||||
if(type === "defusing"){
|
||||
export default class Bomb extends React.Component<IProps> {
|
||||
getCaption = (type: "defusing" | "planting", player: Player | null) => {
|
||||
if(!player) return null;
|
||||
if(type === "defusing"){
|
||||
return <>
|
||||
<I.Defuse height={22} width={22} fill="var(--color-new-ct)" />
|
||||
<div className={'CT'}>{player.name} is defusing the bomb</div>
|
||||
</>;
|
||||
}
|
||||
return <>
|
||||
<I.Defuse height={22} width={22} fill="var(--color-new-ct)" />
|
||||
<div className={'CT'}>{player.name} is defusing the bomb</div>
|
||||
<I.SmallBomb height={22} fill="var(--color-new-t)"/>
|
||||
<div className={'T'}>{player.name} is planting the bomb</div>
|
||||
</>;
|
||||
}
|
||||
return <>
|
||||
<I.SmallBomb height={22} fill="var(--color-new-t)"/>
|
||||
<div className={'T'}>{player.name} is planting the bomb</div>
|
||||
</>;
|
||||
render() {
|
||||
const { side, timer } = this.props;
|
||||
return (
|
||||
<div className={`defuse_plant_container ${side} ${timer && timer.active ? 'show' :'hide'}`}>
|
||||
{
|
||||
timer ?
|
||||
<div className={`defuse_plant_caption`}>
|
||||
{this.getCaption(timer.type, timer.player)}
|
||||
</div> : null
|
||||
}
|
||||
|
||||
<div className="defuse_plant_bar" style={{ width: `${(timer && timer.width) || 0}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const Bomb = ({ timer, side }: IProps) =>{
|
||||
if(!timer) return null;
|
||||
return (
|
||||
<div className={`defuse_plant_container ${side} ${timer && timer.active ? 'show' :'hide'}`}>
|
||||
{
|
||||
timer ?
|
||||
<div className={`defuse_plant_caption`}>
|
||||
{getCaption(timer.type, timer.player)}
|
||||
</div> : null
|
||||
}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
export default Bomb;
|
||||
@ -1,5 +1,5 @@
|
||||
import * as I from './../../API/types';
|
||||
import { apiUrl } from '../../API';
|
||||
import React from 'react';
|
||||
import * as I from './../../api/interfaces';
|
||||
|
||||
interface MatchData {
|
||||
left: { name: string; score: string | number; logo: string };
|
||||
@ -11,79 +11,83 @@ interface Props {
|
||||
teams: I.Team[]
|
||||
}
|
||||
|
||||
const joinParents = (matchup: I.TournamentMatchup, matchups: I.TournamentMatchup[]) => {
|
||||
if (!matchup) return matchup;
|
||||
export default class Ladder extends React.Component<Props> {
|
||||
joinParents = (matchup: I.TournamentMatchup, matchups: I.TournamentMatchup[]) => {
|
||||
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);
|
||||
if (!parents.length) return matchup;
|
||||
matchup.parents.push(...parents.map(parent => joinParents(parent, matchups)));
|
||||
const parents = matchups.filter(m => m.winner_to === matchup._id || m.loser_to === matchup._id);
|
||||
if (!parents.length) return matchup;
|
||||
matchup.parents.push(...parents.map(parent => this.joinParents(parent, matchups)));
|
||||
|
||||
return matchup;
|
||||
};
|
||||
|
||||
const copyMatchups = (currentMatchups: I.TournamentMatchup[]): I.DepthTournamentMatchup[] => {
|
||||
const matchups = JSON.parse(JSON.stringify(currentMatchups)) as I.DepthTournamentMatchup[];
|
||||
return matchups;
|
||||
};
|
||||
|
||||
const setDepth = (matchups: I.DepthTournamentMatchup[], matchup: I.DepthTournamentMatchup, depth: number, force = false) => {
|
||||
const getParents = (matchup: I.DepthTournamentMatchup) => {
|
||||
return matchups.filter(parent => parent.loser_to === matchup._id || parent.winner_to === matchup._id);
|
||||
return matchup;
|
||||
};
|
||||
|
||||
if (!matchup.depth || force) {
|
||||
matchup.depth = depth;
|
||||
getParents(matchup).forEach(matchup => setDepth(matchups, matchup, depth + 1));
|
||||
}
|
||||
if (matchup.depth <= depth - 1) {
|
||||
setDepth(matchups, matchup, depth - 1, true);
|
||||
}
|
||||
return matchup;
|
||||
};
|
||||
|
||||
const getMatch = ({ matchup, matches, teams: allTeams}: { matches: I.Match[], teams: I.Team[], matchup: I.TournamentMatchup}) => {
|
||||
const matchData: MatchData = {
|
||||
left: { name: 'TBD', score: '-', logo: '' },
|
||||
right: { name: 'TBD', score: '-', logo: '' }
|
||||
copyMatchups = (): I.DepthTournamentMatchup[] => {
|
||||
if (!this.props.tournament) return [];
|
||||
const matchups = JSON.parse(JSON.stringify(this.props.tournament.matchups)) as I.DepthTournamentMatchup[];
|
||||
return matchups;
|
||||
};
|
||||
const match = matches.find(match => match.id === matchup.matchId);
|
||||
if (!match) return matchData;
|
||||
const teams = [
|
||||
allTeams.find(team => team._id === match.left.id),
|
||||
allTeams.find(team => team._id === match.right.id)
|
||||
];
|
||||
if (teams[0]) {
|
||||
matchData.left.name = teams[0].name;
|
||||
matchData.left.score = match.left.wins;
|
||||
matchData.left.logo = teams[0].logo;
|
||||
}
|
||||
if (teams[1]) {
|
||||
matchData.right.name = teams[1].name;
|
||||
matchData.right.score = match.right.wins;
|
||||
matchData.right.logo = teams[1].logo;
|
||||
}
|
||||
return matchData;
|
||||
};
|
||||
|
||||
const Ladder = ({ tournament, matches, teams }: Props) => {
|
||||
const renderBracket = (
|
||||
setDepth = (matchups: I.DepthTournamentMatchup[], matchup: I.DepthTournamentMatchup, depth: number, force = false) => {
|
||||
const getParents = (matchup: I.DepthTournamentMatchup) => {
|
||||
return matchups.filter(parent => parent.loser_to === matchup._id || parent.winner_to === matchup._id);
|
||||
};
|
||||
|
||||
if (!matchup.depth || force) {
|
||||
matchup.depth = depth;
|
||||
getParents(matchup).forEach(matchup => this.setDepth(matchups, matchup, depth + 1));
|
||||
}
|
||||
if (matchup.depth <= depth - 1) {
|
||||
this.setDepth(matchups, matchup, depth - 1, true);
|
||||
}
|
||||
return matchup;
|
||||
};
|
||||
|
||||
getMatch = (matchup: I.TournamentMatchup) => {
|
||||
const { matches } = this.props;
|
||||
const matchData: MatchData = {
|
||||
left: { name: 'TBD', score: '-', logo: '' },
|
||||
right: { name: 'TBD', score: '-', logo: '' }
|
||||
};
|
||||
const match = matches.find(match => match.id === matchup.matchId);
|
||||
if (!match) return matchData;
|
||||
const teams = [
|
||||
this.props.teams.find(team => team._id === match.left.id),
|
||||
this.props.teams.find(team => team._id === match.right.id)
|
||||
];
|
||||
if (teams[0]) {
|
||||
matchData.left.name = teams[0].name;
|
||||
matchData.left.score = match.left.wins;
|
||||
matchData.left.logo = teams[0].logo;
|
||||
}
|
||||
if (teams[1]) {
|
||||
matchData.right.name = teams[1].name;
|
||||
matchData.right.score = match.right.wins;
|
||||
matchData.right.logo = teams[1].logo;
|
||||
}
|
||||
return matchData;
|
||||
};
|
||||
|
||||
renderBracket = (
|
||||
matchup: I.DepthTournamentMatchup | null | undefined,
|
||||
depth: number,
|
||||
fromChildId: string | undefined,
|
||||
childVisibleParents: number,
|
||||
isLast = false
|
||||
) => {
|
||||
const { tournament, matches } = this.props;
|
||||
if (!matchup || !tournament) return null;
|
||||
const match = getMatch({ teams: teams, matches: matches, matchup});
|
||||
const match = this.getMatch(matchup);
|
||||
|
||||
if (fromChildId === matchup.loser_to) return null;
|
||||
const parentsToRender = matchup.parents.filter(matchupParent => matchupParent.loser_to !== matchup._id);
|
||||
if (matchup.depth > depth) {
|
||||
return (
|
||||
<div className="empty-bracket">
|
||||
{renderBracket(matchup, depth + 1, fromChildId, parentsToRender.length)}
|
||||
{this.renderBracket(matchup, depth + 1, fromChildId, parentsToRender.length)}
|
||||
<div className="connector"></div>
|
||||
</div>
|
||||
);
|
||||
@ -93,8 +97,8 @@ const Ladder = ({ tournament, matches, teams }: Props) => {
|
||||
return (
|
||||
<div className={`bracket depth-${depth}`}>
|
||||
<div className="parent-brackets">
|
||||
{renderBracket(matchup.parents[0], depth + 1, matchup._id, parentsToRender.length)}
|
||||
{renderBracket(matchup.parents[1], depth + 1, matchup._id, parentsToRender.length)}
|
||||
{this.renderBracket(matchup.parents[0], depth + 1, matchup._id, parentsToRender.length)}
|
||||
{this.renderBracket(matchup.parents[1], depth + 1, matchup._id, parentsToRender.length)}
|
||||
</div>
|
||||
<div className="bracket-details">
|
||||
<div
|
||||
@ -106,14 +110,14 @@ const Ladder = ({ tournament, matches, teams }: Props) => {
|
||||
<div className={`match-details ${isCurrent ? 'current':''}`}>
|
||||
<div className="team-data">
|
||||
<div className="team-logo">
|
||||
{match.left.logo ? <img src={`${apiUrl}api/teams/logo/direct/${match.left.logo}`} alt="Logo" /> : null}
|
||||
{match.left.logo ? <img src={match.left.logo} alt="Logo" /> : null}
|
||||
</div>
|
||||
<div className="team-name">{match.left.name}</div>
|
||||
<div className="team-score">{match.left.score}</div>
|
||||
</div>
|
||||
<div className="team-data">
|
||||
<div className="team-logo">
|
||||
{match.right.logo ? <img src={`${apiUrl}api/teams/logo/direct/${match.right.logo}`} alt="Logo" /> : null}
|
||||
{match.right.logo ? <img src={match.right.logo} alt="Logo" /> : null}
|
||||
</div>
|
||||
<div className="team-name">{match.right.name}</div>
|
||||
<div className="team-score">{match.right.score}</div>
|
||||
@ -128,13 +132,14 @@ const Ladder = ({ tournament, matches, teams }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tournament } = this.props;
|
||||
if (!tournament) return null;
|
||||
const matchups = copyMatchups(tournament.playoffs.matchups);
|
||||
const matchups = this.copyMatchups();
|
||||
const gf = matchups.find(matchup => matchup.winner_to === null);
|
||||
if (!gf) return null;
|
||||
const joinedParents = joinParents(gf, matchups);
|
||||
const matchupWithDepth = setDepth(matchups, joinedParents as I.DepthTournamentMatchup, 0);
|
||||
return renderBracket(matchupWithDepth, 0, undefined, 2, true);
|
||||
const joinedParents = this.joinParents(gf, matchups);
|
||||
const matchupWithDepth = this.setDepth(matchups, joinedParents as I.DepthTournamentMatchup, 0);
|
||||
return this.renderBracket(matchupWithDepth, 0, undefined, 2, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default Ladder;
|
||||
@ -1,51 +1,62 @@
|
||||
import React from 'react';
|
||||
import './tournament.scss';
|
||||
import * as I from './../../API/types';
|
||||
import api from './../../API';
|
||||
import { actions } from '../../App';
|
||||
import * as I from './../../api/interfaces';
|
||||
import api from '../../api/api';
|
||||
import Ladder from './Ladder';
|
||||
import { useAction } from '../../API/contexts/actions';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const Tournament = () => {
|
||||
const [ show, setShow ] = useState(false);
|
||||
const [ teams, setTeams ] = useState<I.Team[]>([]);
|
||||
const [ matches, setMatches ] = useState<I.Match[]>([]);
|
||||
const [ tournament, setTournament ] = useState<I.Tournament | null>(null);
|
||||
|
||||
useAction("showTournament", (data) => {
|
||||
setShow(data === "show");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
api.tournaments.get().then(({ tournament }) => {
|
||||
if(tournament){
|
||||
setTournament(tournament);
|
||||
|
||||
Promise.allSettled([api.match.get(), api.teams.get()]).then(([matches, teams]) =>{
|
||||
setTeams(teams.status === "fulfilled" ? teams.value : []);
|
||||
setMatches(matches.status === "fulfilled" ? matches.value : []);
|
||||
});
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
if(!tournament) return null;
|
||||
return (
|
||||
<div className={`ladder-container ${show ? 'show':''}`}>
|
||||
<div className="tournament-data">
|
||||
{ tournament.logo ? <img src={`data:image/jpeg;base64,${tournament.logo}`} alt={tournament.name} /> : null }
|
||||
<div className="tournament-name">
|
||||
{tournament.name}
|
||||
</div>
|
||||
</div>
|
||||
<Ladder
|
||||
tournament={tournament}
|
||||
matches={matches}
|
||||
teams={teams}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
interface State {
|
||||
tournament: I.Tournament | null,
|
||||
teams: I.Team[],
|
||||
matches: I.Match[],
|
||||
show: boolean,
|
||||
}
|
||||
export default class Tournament extends React.Component<{}, State> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tournament: null,
|
||||
matches: [],
|
||||
teams: [],
|
||||
show: false
|
||||
}
|
||||
}
|
||||
async componentDidMount() {
|
||||
const { tournament } = await api.tournaments.get();
|
||||
if(tournament){
|
||||
actions.on("showTournament", async (show: string) => {
|
||||
if(show !== "show"){
|
||||
return this.setState({show: false});
|
||||
}
|
||||
|
||||
this.setState({tournament}, () => {
|
||||
this.setState({show:true})
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
return (
|
||||
<div className={`ladder-container ${show ? 'show':''}`}>
|
||||
<div className="tournament-data">
|
||||
{ tournament.logo ? <img src={`data:image/jpeg;base64,${tournament.logo}`} alt={tournament.name} /> : null }
|
||||
<div className="tournament-name">
|
||||
{tournament.name}
|
||||
</div>
|
||||
</div>
|
||||
<Ladder
|
||||
tournament={tournament}
|
||||
matches={matches}
|
||||
teams={teams}
|
||||
/>
|
||||
|
||||
export default React.memo(Tournament);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,26 +1,43 @@
|
||||
import React from 'react';
|
||||
import './trivia.scss';
|
||||
import { useAction, useConfig } from '../../API/contexts/actions';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Trivia = () => {
|
||||
const [ show, setShow ] = useState(false);
|
||||
|
||||
const data = useConfig('trivia');
|
||||
import {configs, actions} from './../../App';
|
||||
|
||||
useAction('triviaState', (state) => {
|
||||
setShow(state === "show");
|
||||
});
|
||||
export default class Trivia extends React.Component<any, { title: string, content: string, show: boolean }> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title:'Title',
|
||||
content:'Content',
|
||||
show: false
|
||||
}
|
||||
}
|
||||
|
||||
useAction('toggleCams', () => {
|
||||
setShow(p => !p);
|
||||
});
|
||||
componentDidMount() {
|
||||
configs.onChange((data:any) => {
|
||||
if(!data) return;
|
||||
const trivia = data.trivia;
|
||||
if(!trivia) return;
|
||||
|
||||
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 (
|
||||
<div className={`trivia_container ${this.state.show ? 'show': 'hide'}`}>
|
||||
<div className="title">{this.state.title}</div>
|
||||
<div className="content">{this.state.content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`trivia_container ${show ? 'show': 'hide'}`}>
|
||||
<div className="title">{data?.title}</div>
|
||||
<div className="content">{data?.content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Trivia);
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
import React from "react";
|
||||
import * as Weapons from "./../../assets/Weapons";
|
||||
import React from 'react';
|
||||
import * as Weapons from './../../assets/Weapons';
|
||||
|
||||
interface IProps extends React.SVGProps<SVGSVGElement> {
|
||||
weapon: string;
|
||||
active: boolean;
|
||||
isGrenade?: boolean;
|
||||
weapon: string,
|
||||
active: boolean,
|
||||
isGrenade?: boolean
|
||||
}
|
||||
const WeaponImage = ({ weapon, active, isGrenade, ...rest }: IProps) => {
|
||||
const weaponId = weapon.replace("weapon_", "");
|
||||
const Weapon = (Weapons as any)[weaponId];
|
||||
const { className, ...svgProps } = rest;
|
||||
if (!Weapon) return null;
|
||||
return (
|
||||
<Weapon
|
||||
fill="white"
|
||||
className={`${active ? "active" : ""} weapon ${
|
||||
isGrenade ? "grenade" : ""
|
||||
} ${className || ""}`}
|
||||
{...svgProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WeaponImage);
|
||||
export default class WeaponImage extends React.Component<IProps> {
|
||||
render() {
|
||||
const { weapon, active, isGrenade, ...rest } = this.props;
|
||||
const Weapon = (Weapons as any)[weapon];
|
||||
const { className, ...svgProps } = rest;
|
||||
if(!Weapon) return null;
|
||||
return (
|
||||
<Weapon fill="white" className={`${active ? 'active':''} weapon ${isGrenade ? 'grenade' : ''} ${className || ''}`} {...svgProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
65
src/api/actionManager.ts
Normal file
65
src/api/actionManager.ts
Normal file
@ -0,0 +1,65 @@
|
||||
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,12 +1,14 @@
|
||||
import * as I from './types';
|
||||
import * as I from './interfaces';
|
||||
import queryString from 'query-string';
|
||||
import { MapConfig } from '../HUD/Radar/LexoRadar/maps';
|
||||
|
||||
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
export const port = Number(query.get('port') || 1349);
|
||||
export const variant = query.get("variant") || "default";
|
||||
const query = queryString.parseUrl(window.location.href).query;
|
||||
export const variant = query?.variant || "default";
|
||||
|
||||
export const isDev = !query.get("isProd");
|
||||
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;
|
||||
@ -33,22 +35,7 @@ const api = {
|
||||
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); });
|
||||
|
||||
})
|
||||
get: (): Promise<{ availablePlayers: ({steamid:string, label: string})[], uuid: string }> => apiV2('camera')
|
||||
},
|
||||
teams: {
|
||||
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
|
||||
24
src/api/avatars.ts
Normal file
24
src/api/avatars.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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('');
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -26,47 +26,37 @@ export interface HUD {
|
||||
legacy: boolean,
|
||||
dir: string
|
||||
}
|
||||
|
||||
|
||||
export interface Config {
|
||||
port: number,
|
||||
steamApiKey: string,
|
||||
token: string,
|
||||
}*/
|
||||
export interface TournamentMatchup {
|
||||
_id: string;
|
||||
loser_to: string | null; // IDs of Matchups, not Matches
|
||||
winner_to: string | null;
|
||||
label: string;
|
||||
matchId: string | null;
|
||||
parents: TournamentMatchup[];
|
||||
_id: string;
|
||||
loser_to: string | null; // IDs of Matchups, not Matches
|
||||
winner_to: string | null;
|
||||
label: string;
|
||||
matchId: string | null;
|
||||
parents: TournamentMatchup[];
|
||||
}
|
||||
|
||||
export interface DepthTournamentMatchup extends TournamentMatchup {
|
||||
depth: number;
|
||||
parents: DepthTournamentMatchup[];
|
||||
depth: number;
|
||||
parents: DepthTournamentMatchup[];
|
||||
}
|
||||
|
||||
export type TournamentTypes = 'swiss' | 'single' | 'double';
|
||||
|
||||
export type TournamentStage = {
|
||||
type: TournamentTypes;
|
||||
matchups: TournamentMatchup[];
|
||||
teams: number;
|
||||
phases: number;
|
||||
participants: string[];
|
||||
};
|
||||
export interface Tournament {
|
||||
_id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
groups: TournamentStage[];
|
||||
playoffs: TournamentStage;
|
||||
autoCreate: boolean;
|
||||
_id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
matchups: TournamentMatchup[];
|
||||
autoCreate: boolean;
|
||||
}
|
||||
export interface RoundData {
|
||||
round: number,
|
||||
players: {
|
||||
[steamid: string]: PlayerRoundData
|
||||
[steamid: string]: PlayerRoundData
|
||||
},
|
||||
winner: 'CT' | 'T' | null,
|
||||
win_type: 'bomb' | 'elimination' | 'defuse' | 'time',
|
||||
@ -138,24 +128,24 @@ export type Weapon =
|
||||
export type Pistol = "c75a" | "deagle" | "elite" | "fiveseven" | "glock" | "hkp2000" | "p250" | "revolver" | "taser" | "tec9" | "usp_silencer";
|
||||
|
||||
export type Knife =
|
||||
| "knife"//
|
||||
| "knife_css"//--
|
||||
| "knife_butterfly"//
|
||||
| "knife_falchion"//
|
||||
| "knife_flip"//
|
||||
| "knife_outdoor" // Nomad Knife
|
||||
| "knife_gut"//
|
||||
| "knife_gypsy_jackknife"//
|
||||
| "knife_karambit"//
|
||||
| "knife_bayonet" //
|
||||
| "knife_cord" //
|
||||
| "knife_m9_bayonet"//
|
||||
| "knife_push" // Shadow daggers
|
||||
| "knife_stiletto"//
|
||||
| "knife_survival_bowie"//
|
||||
| "knife_t"//
|
||||
| "knife_skeleton" //
|
||||
| "knife_tactical"//
|
||||
| "knife_ursus"//
|
||||
| "knife_widowmaker"//
|
||||
| "knife_canis";//
|
||||
| "knife"//
|
||||
| "knife_css"//--
|
||||
| "knife_butterfly"//
|
||||
| "knife_falchion"//
|
||||
| "knife_flip"//
|
||||
| "knife_outdoor" // Nomad Knife
|
||||
| "knife_gut"//
|
||||
| "knife_gypsy_jackknife"//
|
||||
| "knife_karambit"//
|
||||
| "knife_bayonet" //
|
||||
| "knife_cord" //
|
||||
| "knife_m9_bayonet"//
|
||||
| "knife_push" // Shadow daggers
|
||||
| "knife_stiletto"//
|
||||
| "knife_survival_bowie"//
|
||||
| "knife_t"//
|
||||
| "knife_skeleton" //
|
||||
| "knife_tactical"//
|
||||
| "knife_ursus"//
|
||||
| "knife_widowmaker"//
|
||||
| "knife_canis";//
|
||||
@ -1,32 +1,32 @@
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
import ArmorFull from './../assets/images/icon_armor_full_default.svg?react';
|
||||
import ArmorHalf from './../assets/images/icon_armor_half_default.svg?react';
|
||||
import ArmorHalfHelmet from './../assets/images/icon_armor_half_helmet_default.svg?react';
|
||||
import ArmorHelmet from './../assets/images/icon_armor_helmet_default.svg?react';
|
||||
import ArmorNone from './../assets/images/icon_armor_none_default.svg?react';
|
||||
import Blind from './../assets/images/icon_blind.svg?react';
|
||||
import Bomb from './../assets/images/icon_bomb_default.svg?react';
|
||||
import BombExplosion from './../assets/images/icon_bomb_explosion_default.svg?react';
|
||||
import Bullets from './../assets/images/icon_bullets_default.svg?react';
|
||||
import Burning from './../assets/images/icon_burning.svg?react';
|
||||
import C4 from './../assets/images/icon_c4_default.svg?react';
|
||||
import Defuse from './../assets/images/icon_defuse_default.svg?react';
|
||||
import Health from './../assets/images/icon_health_default.svg?react';
|
||||
import HealthFull from './../assets/images/icon_health_full_default.svg?react';
|
||||
import Hourglass from './../assets/images/icon_hourglass_default.svg?react';
|
||||
import Microphone from './../assets/images/icon_microphone.svg?react';
|
||||
import Pause from './../assets/images/icon_pause_default.svg?react';
|
||||
import Skull from './../assets/images/icon_skull_default.svg?react';
|
||||
import Timer from './../assets/images/icon_timer_default.svg?react';
|
||||
import FlashedKill from './../assets/images/flashed_kill.svg?react';
|
||||
import Headshot from './../assets/images/headshot.svg?react';
|
||||
import NoScope from './../assets/images/noscope.svg?react';
|
||||
import SmokeKill from './../assets/images/smoke_kill.svg?react';
|
||||
import Suicide from './../assets/images/suicide.svg?react';
|
||||
import Wallbang from './../assets/images/wallbang.svg?react';
|
||||
import { ReactComponent as ArmorFull } from './../assets/images/icon_armor_full_default.svg';
|
||||
import { ReactComponent as ArmorHalf } from './../assets/images/icon_armor_half_default.svg';
|
||||
import { ReactComponent as ArmorHalfHelmet } from './../assets/images/icon_armor_half_helmet_default.svg';
|
||||
import { ReactComponent as ArmorHelmet } from './../assets/images/icon_armor_helmet_default.svg';
|
||||
import { ReactComponent as ArmorNone } from './../assets/images/icon_armor_none_default.svg';
|
||||
import { ReactComponent as Blind } from './../assets/images/icon_blind.svg';
|
||||
import { ReactComponent as Bomb } from './../assets/images/icon_bomb_default.svg';
|
||||
import { ReactComponent as BombExplosion } from './../assets/images/icon_bomb_explosion_default.svg';
|
||||
import { ReactComponent as Bullets } from './../assets/images/icon_bullets_default.svg';
|
||||
import { ReactComponent as Burning } from './../assets/images/icon_burning.svg';
|
||||
import { ReactComponent as C4 } from './../assets/images/icon_c4_default.svg';
|
||||
import { ReactComponent as Defuse } from './../assets/images/icon_defuse_default.svg';
|
||||
import { ReactComponent as Health } from './../assets/images/icon_health_default.svg';
|
||||
import { ReactComponent as HealthFull } from './../assets/images/icon_health_full_default.svg';
|
||||
import { ReactComponent as Hourglass } from './../assets/images/icon_hourglass_default.svg';
|
||||
import { ReactComponent as Microphone } from './../assets/images/icon_microphone.svg';
|
||||
import { ReactComponent as Pause } from './../assets/images/icon_pause_default.svg';
|
||||
import { ReactComponent as Skull } from './../assets/images/icon_skull_default.svg';
|
||||
import { ReactComponent as Timer } from './../assets/images/icon_timer_default.svg';
|
||||
import { ReactComponent as FlashedKill } from './../assets/images/flashed_kill.svg';
|
||||
import { ReactComponent as Headshot } from './../assets/images/headshot.svg';
|
||||
import { ReactComponent as NoScope } from './../assets/images/noscope.svg';
|
||||
import { ReactComponent as SmokeKill } from './../assets/images/smoke_kill.svg';
|
||||
import { ReactComponent as Suicide } from './../assets/images/suicide.svg';
|
||||
import { ReactComponent as Wallbang } from './../assets/images/wallbang.svg';
|
||||
import LogoCT from './../assets/images/logo_CT_default.png';
|
||||
import LogoT from './../assets/images/logo_T_default.png';
|
||||
import SmallBomb from "./../assets/images/bomb.svg?react";
|
||||
import { ReactComponent as SmallBomb } from "./../assets/images/bomb.svg";
|
||||
|
||||
|
||||
export {
|
||||
SmallBomb,
|
||||
|
||||
@ -1,73 +1,73 @@
|
||||
import ak47 from './weapons/ak47.svg?react';
|
||||
import aug from './weapons/aug.svg?react';
|
||||
import awp from './weapons/awp.svg?react';
|
||||
import bayonet from './weapons/bayonet.svg?react';
|
||||
import bizon from './weapons/bizon.svg?react';
|
||||
import c4 from './weapons/c4.svg?react';
|
||||
import cz75a from './weapons/cz75a.svg?react';
|
||||
import deagle from './weapons/deagle.svg?react';
|
||||
import decoy from './weapons/decoy.svg?react';
|
||||
import elite from './weapons/elite.svg?react';
|
||||
import famas from './weapons/famas.svg?react';
|
||||
import fiveseven from './weapons/fiveseven.svg?react';
|
||||
import flashbang from './weapons/flashbang.svg?react';
|
||||
import g3sg1 from './weapons/g3sg1.svg?react';
|
||||
import galilar from './weapons/galilar.svg?react';
|
||||
import glock from './weapons/glock.svg?react';
|
||||
import hegrenade from './weapons/hegrenade.svg?react';
|
||||
import hkp2000 from './weapons/hkp2000.svg?react';
|
||||
import incgrenade from './weapons/incgrenade.svg?react';
|
||||
import inferno from './weapons/inferno.svg?react';
|
||||
import knife from './weapons/knife.svg?react';
|
||||
import knife_bayonet from './weapons/knife_bayonet.svg?react';
|
||||
import knife_butterfly from './weapons/knife_butterfly.svg?react';
|
||||
import knife_canis from './weapons/knife_canis.svg?react';
|
||||
import knife_cord from './weapons/knife_cord.svg?react';
|
||||
import knife_css from './weapons/knife_css.svg?react';
|
||||
import knife_falchion from './weapons/knife_falchion.svg?react';
|
||||
import knife_flip from './weapons/knife_flip.svg?react';
|
||||
import knife_gut from './weapons/knife_gut.svg?react';
|
||||
import knife_gypsy_jackknife from './weapons/knife_gypsy_jackknife.svg?react';
|
||||
import knife_karambit from './weapons/knife_karambit.svg?react';
|
||||
import knife_m9_bayonet from './weapons/knife_m9_bayonet.svg?react';
|
||||
import knife_outdoor from './weapons/knife_outdoor.svg?react';
|
||||
import knife_push from './weapons/knife_push.svg?react';
|
||||
import knife_skeleton from './weapons/knife_skeleton.svg?react';
|
||||
import knife_stiletto from './weapons/knife_stiletto.svg?react';
|
||||
import knife_survival_bowie from './weapons/knife_survival_bowie.svg?react';
|
||||
import knife_t from './weapons/knife_t.svg?react';
|
||||
import knife_tactical from './weapons/knife_tactical.svg?react';
|
||||
import knife_ursus from './weapons/knife_ursus.svg?react';
|
||||
import knife_widowmaker from './weapons/knife_widowmaker.svg?react';
|
||||
import m249 from './weapons/m249.svg?react';
|
||||
import m4a1 from './weapons/m4a1.svg?react';
|
||||
import m4a1_silencer from './weapons/m4a1_silencer.svg?react';
|
||||
import m4a1_silencer_off from './weapons/m4a1_silencer_off.svg?react';
|
||||
import mac10 from './weapons/mac10.svg?react';
|
||||
import mag7 from './weapons/mag7.svg?react';
|
||||
import molotov from './weapons/molotov.svg?react';
|
||||
import mp5sd from './weapons/mp5sd.svg?react';
|
||||
import mp7 from './weapons/mp7.svg?react';
|
||||
import mp9 from './weapons/mp9.svg?react';
|
||||
import negev from './weapons/negev.svg?react';
|
||||
import nova from './weapons/nova.svg?react';
|
||||
import out from './weapons/out.svg?react';
|
||||
import p250 from './weapons/p250.svg?react';
|
||||
import p90 from './weapons/p90.svg?react';
|
||||
import revolver from './weapons/revolver.svg?react';
|
||||
import sawedoff from './weapons/sawedoff.svg?react';
|
||||
import scar20 from './weapons/scar20.svg?react';
|
||||
import sg556 from './weapons/sg556.svg?react';
|
||||
import smokegrenade from './weapons/smokegrenade.svg?react';
|
||||
import ssg08 from './weapons/ssg08.svg?react';
|
||||
import taser from './weapons/taser.svg?react';
|
||||
import tec9 from './weapons/tec9.svg?react';
|
||||
import trigger_hurt from './weapons/trigger_hurt.svg?react';
|
||||
import ump45 from './weapons/ump45.svg?react';
|
||||
import usp_silencer from './weapons/usp_silencer.svg?react';
|
||||
import usp_silencer_off from './weapons/usp_silencer_off.svg?react';
|
||||
import world from './weapons/world.svg?react';
|
||||
import xm1014 from './weapons/xm1014.svg?react';
|
||||
import { ReactComponent as ak47 } from './weapons/ak47.svg';
|
||||
import { ReactComponent as aug } from './weapons/aug.svg';
|
||||
import { ReactComponent as awp } from './weapons/awp.svg';
|
||||
import { ReactComponent as bayonet } from './weapons/bayonet.svg';
|
||||
import { ReactComponent as bizon } from './weapons/bizon.svg';
|
||||
import { ReactComponent as c4 } from './weapons/c4.svg';
|
||||
import { ReactComponent as cz75a } from './weapons/cz75a.svg';
|
||||
import { ReactComponent as deagle } from './weapons/deagle.svg';
|
||||
import { ReactComponent as decoy } from './weapons/decoy.svg';
|
||||
import { ReactComponent as elite } from './weapons/elite.svg';
|
||||
import { ReactComponent as famas } from './weapons/famas.svg';
|
||||
import { ReactComponent as fiveseven } from './weapons/fiveseven.svg';
|
||||
import { ReactComponent as flashbang } from './weapons/flashbang.svg';
|
||||
import { ReactComponent as g3sg1 } from './weapons/g3sg1.svg';
|
||||
import { ReactComponent as galilar } from './weapons/galilar.svg';
|
||||
import { ReactComponent as glock } from './weapons/glock.svg';
|
||||
import { ReactComponent as hegrenade } from './weapons/hegrenade.svg';
|
||||
import { ReactComponent as hkp2000 } from './weapons/hkp2000.svg';
|
||||
import { ReactComponent as incgrenade } from './weapons/incgrenade.svg';
|
||||
import { ReactComponent as inferno } from './weapons/inferno.svg';
|
||||
import { ReactComponent as knife } from './weapons/knife.svg';
|
||||
import { ReactComponent as knife_bayonet } from './weapons/knife_bayonet.svg';
|
||||
import { ReactComponent as knife_butterfly } from './weapons/knife_butterfly.svg';
|
||||
import { ReactComponent as knife_canis } from './weapons/knife_canis.svg';
|
||||
import { ReactComponent as knife_cord } from './weapons/knife_cord.svg';
|
||||
import { ReactComponent as knife_css } from './weapons/knife_css.svg';
|
||||
import { ReactComponent as knife_falchion } from './weapons/knife_falchion.svg';
|
||||
import { ReactComponent as knife_flip } from './weapons/knife_flip.svg';
|
||||
import { ReactComponent as knife_gut } from './weapons/knife_gut.svg';
|
||||
import { ReactComponent as knife_gypsy_jackknife } from './weapons/knife_gypsy_jackknife.svg';
|
||||
import { ReactComponent as knife_karambit } from './weapons/knife_karambit.svg';
|
||||
import { ReactComponent as knife_m9_bayonet } from './weapons/knife_m9_bayonet.svg';
|
||||
import { ReactComponent as knife_outdoor } from './weapons/knife_outdoor.svg';
|
||||
import { ReactComponent as knife_push } from './weapons/knife_push.svg';
|
||||
import { ReactComponent as knife_skeleton } from './weapons/knife_skeleton.svg';
|
||||
import { ReactComponent as knife_stiletto } from './weapons/knife_stiletto.svg';
|
||||
import { ReactComponent as knife_survival_bowie } from './weapons/knife_survival_bowie.svg';
|
||||
import { ReactComponent as knife_t } from './weapons/knife_t.svg';
|
||||
import { ReactComponent as knife_tactical } from './weapons/knife_tactical.svg';
|
||||
import { ReactComponent as knife_ursus } from './weapons/knife_ursus.svg';
|
||||
import { ReactComponent as knife_widowmaker } from './weapons/knife_widowmaker.svg';
|
||||
import { ReactComponent as m249 } from './weapons/m249.svg';
|
||||
import { ReactComponent as m4a1 } from './weapons/m4a1.svg';
|
||||
import { ReactComponent as m4a1_silencer } from './weapons/m4a1_silencer.svg';
|
||||
import { ReactComponent as m4a1_silencer_off } from './weapons/m4a1_silencer_off.svg';
|
||||
import { ReactComponent as mac10 } from './weapons/mac10.svg';
|
||||
import { ReactComponent as mag7 } from './weapons/mag7.svg';
|
||||
import { ReactComponent as molotov } from './weapons/molotov.svg';
|
||||
import { ReactComponent as mp5sd } from './weapons/mp5sd.svg';
|
||||
import { ReactComponent as mp7 } from './weapons/mp7.svg';
|
||||
import { ReactComponent as mp9 } from './weapons/mp9.svg';
|
||||
import { ReactComponent as negev } from './weapons/negev.svg';
|
||||
import { ReactComponent as nova } from './weapons/nova.svg';
|
||||
import { ReactComponent as out } from './weapons/out.svg';
|
||||
import { ReactComponent as p250 } from './weapons/p250.svg';
|
||||
import { ReactComponent as p90 } from './weapons/p90.svg';
|
||||
import { ReactComponent as revolver } from './weapons/revolver.svg';
|
||||
import { ReactComponent as sawedoff } from './weapons/sawedoff.svg';
|
||||
import { ReactComponent as scar20 } from './weapons/scar20.svg';
|
||||
import { ReactComponent as sg556 } from './weapons/sg556.svg';
|
||||
import { ReactComponent as smokegrenade } from './weapons/smokegrenade.svg';
|
||||
import { ReactComponent as ssg08 } from './weapons/ssg08.svg';
|
||||
import { ReactComponent as taser } from './weapons/taser.svg';
|
||||
import { ReactComponent as tec9 } from './weapons/tec9.svg';
|
||||
import { ReactComponent as trigger_hurt } from './weapons/trigger_hurt.svg';
|
||||
import { ReactComponent as ump45 } from './weapons/ump45.svg';
|
||||
import { ReactComponent as usp_silencer } from './weapons/usp_silencer.svg';
|
||||
import { ReactComponent as usp_silencer_off } from './weapons/usp_silencer_off.svg';
|
||||
import { ReactComponent as world } from './weapons/world.svg';
|
||||
import { ReactComponent as xm1014 } from './weapons/xm1014.svg';
|
||||
export {
|
||||
ak47,
|
||||
aug,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
BIN
src/fonts/Stratum2/Stratum2 Bold Regular.ttf
Normal file
BIN
src/fonts/Stratum2/Stratum2 Bold Regular.ttf
Normal file
Binary file not shown.
BIN
src/fonts/Stratum2/Stratum2 Bold Regular.woff
Normal file
BIN
src/fonts/Stratum2/Stratum2 Bold Regular.woff
Normal file
Binary file not shown.
6
src/fonts/stratum2.css
Normal file
6
src/fonts/stratum2.css
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Stratum2';
|
||||
font-style: normal;
|
||||
src: url('./Stratum2/Stratum2 Bold Regular.ttf') format('truetype');
|
||||
}
|
||||
104
src/index.css
104
src/index.css
@ -1,69 +1,57 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
@import "fonts/montserrat.css";
|
||||
@import "fonts/stratum2.css";
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
font-family: 'Stratum2', 'Montserrat', 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/*background-image: url('./assets/bg.png');/**/
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
@font-face {
|
||||
font-family: 'Louis George Cafe';
|
||||
src: local('Louis George Cafe'), url('./fonts/Louis George Cafe.ttf') format('truetype');
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
@font-face {
|
||||
font-family: 'Rounded_Elegance';
|
||||
src: local('Rounded_Elegance'), url('./fonts/Rounded_Elegance.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
20
src/index.tsx
Normal file
20
src/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import "./fonts/Louis George Cafe.ttf";
|
||||
import "./fonts/Rounded_Elegance.ttf";
|
||||
import App from "./App";
|
||||
declare global {
|
||||
interface Window {
|
||||
ipcApi: {
|
||||
send: (channel: string, ...arg: any) => void;
|
||||
receive: (channel: string, func: (...arg: any) => void) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
19
src/main.tsx
19
src/main.tsx
@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import * as process from "process";
|
||||
globalThis.process = process;
|
||||
declare global {
|
||||
interface Window {
|
||||
ipcApi: {
|
||||
send: (channel: string, ...arg: any) => void;
|
||||
receive: (channel: string, func: (...arg: any) => void) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@ -1,25 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
import { PluginOption, defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const getDefinitionTemplate = (variable: string, content: string) => {
|
||||
return `export const ${variable} = ${content} as const;`;
|
||||
}
|
||||
|
||||
const updateDefinitonFile = async ({ filePath, content }: { filePath: string, content?: string }) => {
|
||||
const fileContent = content || await fs.promises.readFile(filePath, 'utf-8');
|
||||
const fileName = filePath.endsWith("panel.json") ? "panel.ts" : "keybinds.ts";
|
||||
|
||||
try {
|
||||
const json = JSON.parse(fileContent);
|
||||
const variableName = filePath.endsWith("panel.json") ? "panelDefinition" : "keybindDefinition";
|
||||
fs.writeFileSync(path.join(".", "src", "API", "contexts", fileName), getDefinitionTemplate(variableName, JSON.stringify(json, null, 2)));
|
||||
} catch (e) {
|
||||
console.error(`Updating ${fileName} failed:`);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
const settingsAndKeybindsPlugin = () => {
|
||||
return {
|
||||
name: 'settingsAndKeybinds',
|
||||
enforce: 'post',
|
||||
// HMR
|
||||
async handleHotUpdate({ file, read }) {
|
||||
if (file.endsWith('panel.json') || file.endsWith("keybinds.json")) {
|
||||
console.debug(`[vite][${path.basename(file)}] Rebuilding type definitions...`);
|
||||
const content = await read();
|
||||
|
||||
await updateDefinitonFile({ filePath: file, content });
|
||||
}
|
||||
},
|
||||
} satisfies PluginOption
|
||||
}
|
||||
// https://vitejs.dev/config/
|
||||
|
||||
export default defineConfig(async ({ command, mode }) => {
|
||||
|
||||
await updateDefinitonFile({ filePath: path.join(".", "public", "panel.json") });
|
||||
await updateDefinitonFile({ filePath: path.join(".", "public", "keybinds.json") });
|
||||
return (
|
||||
{
|
||||
plugins: [
|
||||
react(),
|
||||
svgr(),
|
||||
settingsAndKeybindsPlugin()
|
||||
],
|
||||
build: {
|
||||
outDir: 'build'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"readable-stream": "vite-compatible-readable-stream"
|
||||
}
|
||||
},
|
||||
base: mode === "development" ? '/dev/' : './',
|
||||
server: {
|
||||
open: "http://localhost:1349/development/",
|
||||
host: 'localhost',
|
||||
port: 3500,
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
// Node.js global to browser globalThis
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user