Initial commit
This includes changes that were made in the forked version. Commit history from that time is not accessible.
No files matched your search
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.idea/
|
||||
backend-build/
|
||||
cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Riot Community Volunteers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,42 @@
|
||||
# ZSŁ LOL CUP - Pick and ban UI
|
||||
|
||||
This is a fork of a popular overlay package that displays graphics during pick phase in League of Legends. It has graphics and color palette that matches the 6. edition of ZSŁ LOL CUP tournament. It also includes updated dependencies so it's easier to install and requires no additional configuration.
|
||||
|
||||
## Installation
|
||||
|
||||
- Requirements
|
||||
- Node.js >=19.0.0
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/zsl-gdansk/lol-cup-pick-ban-ui.git
|
||||
$ cd "lol-cup-pick-ban-ui"
|
||||
```
|
||||
|
||||
2. Install dependencies using `yarn`
|
||||
|
||||
```shell
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
3. Start both projects using
|
||||
|
||||
```shell
|
||||
$ yarn start
|
||||
|
||||
# or separately
|
||||
$ cd apps/backend
|
||||
$ yarn start
|
||||
$ cd ../layout
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
## Apps
|
||||
|
||||
- **backend** - running WebSocket server on port 8999
|
||||
- **layout** - [http://localhost:3000](http://localhost:3000) (frontend)
|
||||
|
||||
## Read more
|
||||
|
||||
[Documentation of the original project](https://github.com/RCVolus/lol-pick-ban-ui/blob/master/README.md)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["es2015"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: 'module', // Allows for the use of imports
|
||||
},
|
||||
rules: {
|
||||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
||||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
||||
quotes: ["error", "single"],
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import readline from 'readline';
|
||||
import logger from './logging';
|
||||
const log = logger('Console');
|
||||
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
|
||||
declare class KeyEvent {
|
||||
sequence: string;
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
shift: boolean;
|
||||
}
|
||||
|
||||
const handlers: Array<(event: KeyEvent) => void> = [];
|
||||
|
||||
if (process.stdin.setRawMode) {
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.on('keypress', (str, key: KeyEvent) => {
|
||||
if (key.ctrl && key.name === 'c') {
|
||||
process.exit();
|
||||
} else {
|
||||
handlers.forEach(handler => {
|
||||
handler(key);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.info('Cannot listen for console events: no stdin found.');
|
||||
}
|
||||
|
||||
export const registerHandler = (handler: (event: KeyEvent) => void): void => {
|
||||
handlers.push(handler);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
class GlobalContext {
|
||||
commandLine: {
|
||||
data: string;
|
||||
record: string;
|
||||
leaguePath: string;
|
||||
experimentalConnector: boolean;
|
||||
debug: boolean;
|
||||
} = {
|
||||
data: '',
|
||||
record: '',
|
||||
leaguePath: '',
|
||||
experimentalConnector: false,
|
||||
debug: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default new GlobalContext();
|
||||
@@ -0,0 +1,30 @@
|
||||
import logger from './logging/logger';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import Controller from './state/Controller';
|
||||
|
||||
const log = logger('tick');
|
||||
|
||||
class TickManager {
|
||||
controller: Controller;
|
||||
timeout?: Timeout;
|
||||
tickRate = 1;
|
||||
|
||||
constructor(kwargs: { controller: Controller }) {
|
||||
this.controller = kwargs.controller;
|
||||
}
|
||||
|
||||
startLoop(): void {
|
||||
log.info(`Starting main loop with ${1 / this.tickRate} ticks/s!`);
|
||||
this.timeout = setInterval(() => this.runLoop(), 1000 / this.tickRate);
|
||||
}
|
||||
|
||||
async runLoop(): Promise<void> {
|
||||
const newState = await this.controller.dataProvider.getCurrentData();
|
||||
|
||||
if (newState !== null) {
|
||||
this.controller.applyNewState(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TickManager;
|
||||
@@ -0,0 +1,68 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
|
||||
import WebSocketServer from './websocket';
|
||||
import logger, { setLogLevel } from './logging';
|
||||
import TickManager from './TickManager';
|
||||
import { AddressInfo } from 'net';
|
||||
import State from './state';
|
||||
import { getDataProvider } from './data/DataProviderService';
|
||||
import minimist from 'minimist';
|
||||
import DataDragon from './data/league/DataDragon';
|
||||
import Controller from './state/Controller';
|
||||
import GlobalContext from './GlobalContext';
|
||||
import './Console';
|
||||
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
// Needs to be done before logging is initialized, in order to set log level correctly
|
||||
GlobalContext.commandLine = {
|
||||
data: argv['data'],
|
||||
record: argv['record'],
|
||||
leaguePath: argv['leaguePath'] || '',
|
||||
experimentalConnector: argv['experimentalConnector'],
|
||||
debug: argv['debug'],
|
||||
};
|
||||
if (GlobalContext.commandLine.debug) {
|
||||
setLogLevel('debug');
|
||||
}
|
||||
|
||||
const log = logger('main');
|
||||
const app = express();
|
||||
|
||||
log.info(' _ _ ____ ___ ____ _ _ ___ ');
|
||||
log.info(' | | ___ | | | _ \\( _ ) | __ ) | | | |_ _|');
|
||||
log.info(' | | / _ \\| | | |_) / _ \\/\\ _ \\ | | | || | ');
|
||||
log.info(' | |__| (_) | |___ | __/ (_> < |_) | | |_| || | ');
|
||||
log.info(' |_____\\___/|_____| |_| \\___/\\/____/ \\___/|___|');
|
||||
log.info(' ');
|
||||
|
||||
log.debug('Logging in debug mode!');
|
||||
log.info('Configuration: ' + JSON.stringify(GlobalContext.commandLine));
|
||||
|
||||
const state = new State();
|
||||
const ddragon = new DataDragon(state);
|
||||
const dataProvider = getDataProvider();
|
||||
const controller = new Controller({ dataProvider, state, ddragon });
|
||||
const tickManager = new TickManager({ controller });
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
await ddragon.init();
|
||||
|
||||
const server = http.createServer(app);
|
||||
app.use('/cache', express.static(__dirname + '/../cache'));
|
||||
const wsServer = new WebSocketServer(server, state);
|
||||
wsServer.startHeartbeat();
|
||||
|
||||
tickManager.startLoop();
|
||||
|
||||
server.listen(process.env.PORT || 8999, () => {
|
||||
if (server.address() === null) {
|
||||
return log.error('Failed to start server.');
|
||||
}
|
||||
const serverAddress = server.address() as AddressInfo;
|
||||
return log.info(`Server started on ${JSON.stringify(serverAddress)}`);
|
||||
});
|
||||
};
|
||||
|
||||
main().then();
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"frontend": {
|
||||
"scoreEnabled": false,
|
||||
"spellsEnabled": false,
|
||||
"blueTeam": {
|
||||
"name": "TEAM1",
|
||||
"score": 0,
|
||||
"color": "#0e3225"
|
||||
},
|
||||
"redTeam": {
|
||||
"name": "TEAM2",
|
||||
"score": 0,
|
||||
"color": "#084c34"
|
||||
},
|
||||
"patch": "9.19"
|
||||
},
|
||||
"contentPatch": "latest",
|
||||
"contentCdn": "https://ddragon.leagueoflegends.com/cdn"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Session } from '../types/lcu';
|
||||
|
||||
export class CurrentState {
|
||||
isChampSelectActive: boolean;
|
||||
session: Session;
|
||||
|
||||
constructor(isChampSelectActive: boolean, session: Session) {
|
||||
this.isChampSelectActive = isChampSelectActive;
|
||||
this.session = session;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CurrentState } from './CurrentState';
|
||||
import { Session, Summoner } from '../types/lcu';
|
||||
import { EventEmitter } from 'events';
|
||||
import logger from '../logging/logger';
|
||||
import ReplayDataProviderService from './ReplayDataProviderService';
|
||||
import LeagueDataProviderService from './league/LeagueDataProviderService';
|
||||
import GlobalContext from '../GlobalContext';
|
||||
const log = logger('DataProviderService');
|
||||
|
||||
export default interface DataProviderService extends EventEmitter {
|
||||
getCurrentData(): Promise<CurrentState | null>;
|
||||
cacheSummoners(session: Session): Promise<void>;
|
||||
getSummonerById(id: number): Summoner;
|
||||
}
|
||||
|
||||
export const getDataProvider = (): DataProviderService => {
|
||||
if (GlobalContext.commandLine.data && GlobalContext.commandLine.data.startsWith('../recordings/')) {
|
||||
const recordingFile = GlobalContext.commandLine.data;
|
||||
log.info(`Using recording as data provider service: ${recordingFile}`);
|
||||
log.warn('THIS IS PROBABLY MEANT FOR TESTING USAGE AND SHOULD ONLY BE USED IN PRODUCTION, IF YOU KNOW WHAT YOU ARE DOING!');
|
||||
|
||||
return new ReplayDataProviderService(recordingFile);
|
||||
}
|
||||
log.info('Using League Client as data provider service.');
|
||||
return new LeagueDataProviderService();
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import DataProviderService from './DataProviderService';
|
||||
import { EventEmitter } from 'events';
|
||||
import fs from 'fs';
|
||||
import { Session, Summoner } from '../types/lcu';
|
||||
import { CurrentState } from './CurrentState';
|
||||
import logger from '../logging/logger';
|
||||
import RecordingDatapoint from '../recording/RecordingDatapoint';
|
||||
import { registerHandler } from '../Console';
|
||||
const log = logger('ReplayDataProviderService');
|
||||
|
||||
export default class ReplayDataProviderService extends EventEmitter implements DataProviderService {
|
||||
replayFilePath: string;
|
||||
recording: {
|
||||
summoners: Array<Summoner>;
|
||||
dataPoints: Array<RecordingDatapoint>;
|
||||
};
|
||||
numberOfPoints: number;
|
||||
isPaused = false;
|
||||
|
||||
constructor(replayFile: string) {
|
||||
super();
|
||||
|
||||
this.replayFilePath = replayFile + '.json';
|
||||
|
||||
const recordingString = fs.readFileSync(this.replayFilePath, 'utf8');
|
||||
|
||||
this.recording = JSON.parse(recordingString);
|
||||
this.numberOfPoints = this.recording.dataPoints.length;
|
||||
|
||||
// Pause / UnPause
|
||||
registerHandler(event => {
|
||||
if (event.name === 'p') {
|
||||
this.togglePause();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async cacheSummoners(): Promise<void> {
|
||||
// Nothing to do here - summoners are loaded already :)
|
||||
}
|
||||
|
||||
async getCurrentData(): Promise<CurrentState> {
|
||||
log.info(`${this.numberOfPoints - this.recording.dataPoints.length} / ${this.numberOfPoints}`);
|
||||
|
||||
let nextPoint;
|
||||
|
||||
if (!this.isPaused) {
|
||||
nextPoint = this.recording.dataPoints.shift();
|
||||
} else {
|
||||
nextPoint = this.recording.dataPoints[0];
|
||||
}
|
||||
|
||||
if (!nextPoint) {
|
||||
return new CurrentState(false, new Session());
|
||||
}
|
||||
|
||||
return nextPoint;
|
||||
}
|
||||
|
||||
getSummonerById(id: number): Summoner {
|
||||
return this.recording.summoners.filter((summoner) => summoner.summonerId === id)[0];
|
||||
}
|
||||
|
||||
togglePause(): void {
|
||||
this.isPaused = !this.isPaused;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import needle from 'needle';
|
||||
import * as fs from 'fs';
|
||||
import cliProgress from 'cli-progress';
|
||||
|
||||
import logger from '../../logging';
|
||||
import { Champion, Spell } from '../../types/dto';
|
||||
import State from '../../state';
|
||||
|
||||
const log = logger('datadragon');
|
||||
const realm = 'euw';
|
||||
|
||||
class DataDragon {
|
||||
versions = {
|
||||
n: {
|
||||
champion: '',
|
||||
item: ''
|
||||
},
|
||||
cdn: ''
|
||||
};
|
||||
champions: Array<Champion> = [];
|
||||
summonerSpells: Array<Spell> = [];
|
||||
state: State;
|
||||
|
||||
constructor(state: State) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const config = this.state.getConfig();
|
||||
|
||||
if (config.contentPatch === 'latest') {
|
||||
log.info('Getting latest versions from ddragon.');
|
||||
this.versions = (await needle('get', `https://ddragon.leagueoflegends.com/realms/${realm}.json`, { json: true })).body;
|
||||
} else {
|
||||
log.info(`Using version from configuration: ${config.contentPatch}`);
|
||||
this.versions = {
|
||||
cdn: config.contentCdn,
|
||||
n: {
|
||||
champion: config.contentPatch,
|
||||
item: config.contentPatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.state.data.meta.version = {
|
||||
champion: this.versions.n.champion,
|
||||
item: this.versions.n.item,
|
||||
};
|
||||
this.state.data.meta.cdn = this.versions.cdn;
|
||||
this.state.triggerUpdate();
|
||||
|
||||
log.info(`Champion: ${this.state.data.meta.version.champion}, Item: ${this.state.data.meta.version.item}, CDN: ${this.state.data.meta.cdn}`);
|
||||
|
||||
this.champions = Object.values((await needle('get', `${this.state.data.meta.cdn}/${this.state.data.meta.version.champion}/data/en_US/champion.json`, { json: true })).body.data);
|
||||
log.info(`Loaded ${this.champions.length} champions`);
|
||||
|
||||
this.summonerSpells = Object.values((await needle('get', `${this.state.data.meta.cdn}/${this.state.data.meta.version.item}/data/en_US/summoner.json`, { json: true })).body.data);
|
||||
log.info(`Loaded ${this.summonerSpells.length} summoner spells`);
|
||||
|
||||
// Download all champion images and spell images
|
||||
await this.checkLocalCache();
|
||||
}
|
||||
|
||||
getChampionById(id: number): Champion | null {
|
||||
return this.champions.find((champion: Champion) => {
|
||||
if (parseInt(champion.key || '0', 10) === id) {
|
||||
return this.extendChampionLocal(champion);
|
||||
}
|
||||
}) || null;
|
||||
}
|
||||
|
||||
extendChampion(champion: Champion): Champion {
|
||||
champion.splashImg = `${this.state.getCDN()}/img/champion/splash/${champion.id}_0.jpg`;
|
||||
// champion.splashCenteredImg = `https://cdn.communitydragon.org/${this.state.getVersion()}/champion/${champion.id}/splash-art/centered`;
|
||||
// Data Dragon CDN broken workaround
|
||||
champion.splashCenteredImg = `https://raw.communitydragon.org/${this.state.getMajorMinorVersion()}/plugins/rcp-be-lol-game-data/global/default/v1/champion-splashes/${champion.key}/${champion.key}000.jpg`;
|
||||
champion.squareImg = `${this.state.getVersionCDN()}/img/champion/${champion.id}.png`;
|
||||
champion.loadingImg = `${this.state.getCDN()}/img/champion/loading/${champion.id}_0.jpg`;
|
||||
return champion;
|
||||
}
|
||||
extendChampionLocal(champion: Champion): Champion {
|
||||
champion.splashImg = `/cache/${this.versions.n.champion}/champion/${champion.id}_splash.jpg`;
|
||||
champion.splashCenteredImg = `/cache/${this.versions.n.champion}/champion/${champion.id}_centered_splash.jpg`;
|
||||
champion.squareImg = `/cache/${this.versions.n.champion}/champion/${champion.id}_square.png`;
|
||||
champion.loadingImg = `/cache/${this.versions.n.champion}/champion/${champion.id}_loading.jpg`;
|
||||
return champion;
|
||||
}
|
||||
|
||||
getSummonerSpellById(id: number): Spell | null {
|
||||
return this.summonerSpells.find((spell: Spell) => {
|
||||
if (parseInt(spell.key as string, 10) === id) {
|
||||
return this.extendSummonerSpellLocal(spell);
|
||||
}
|
||||
}) || null;
|
||||
}
|
||||
|
||||
extendSummonerSpell(spell: Spell): Spell {
|
||||
spell.icon = `${this.state.getVersionCDN()}/img/spell/${spell.id}.png`;
|
||||
return spell;
|
||||
}
|
||||
extendSummonerSpellLocal(spell: Spell): Spell {
|
||||
spell.icon = `/cache/${this.versions.n.item}/spell/${spell.id}.png`;
|
||||
return spell;
|
||||
}
|
||||
|
||||
async checkLocalCache(): Promise<void> {
|
||||
const patch = this.state.data.meta.version.champion;
|
||||
|
||||
const patchFolder = `./cache/${patch}`;
|
||||
const patchFolderChampion = patchFolder + '/champion';
|
||||
const patchFolderSpell = patchFolder + '/spell';
|
||||
|
||||
if (fs.existsSync(patchFolder)) {
|
||||
log.info(`Directory ${patchFolder} exists already. Please remove it if you want to re-download it.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.mkdirSync('./cache');
|
||||
} catch (e) {
|
||||
log.debug('Directory ./cache exists already or cannot be created.');
|
||||
}
|
||||
fs.mkdirSync(patchFolder);
|
||||
fs.mkdirSync(patchFolderChampion);
|
||||
fs.mkdirSync(patchFolderSpell);
|
||||
|
||||
log.info('Download process started. This could take a while. Downloading to: ' + patchFolder);
|
||||
|
||||
const downloadFile = (targetUrl: string, targetPath: string) => (): Promise<void> => new Promise<void>((resolve, reject): void => {
|
||||
needle('get', targetUrl, {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
open_timeout: 0
|
||||
})
|
||||
.then(function(resp) {
|
||||
const out = fs.createWriteStream(targetPath);
|
||||
out.write(resp.raw);
|
||||
out.close();
|
||||
resolve();
|
||||
})
|
||||
.catch(function(err) {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const tasks: Array<() => Promise<void>> = [];
|
||||
|
||||
this.champions.forEach(champion => {
|
||||
champion = this.extendChampion(champion);
|
||||
tasks.push(downloadFile(champion.loadingImg, `${patchFolderChampion}/${champion.id}_loading.jpg`));
|
||||
tasks.push(downloadFile(champion.splashImg, `${patchFolderChampion}/${champion.id}_splash.jpg`));
|
||||
tasks.push(downloadFile(champion.splashCenteredImg, `${patchFolderChampion}/${champion.id}_centered_splash.jpg`));
|
||||
tasks.push(downloadFile(champion.squareImg, `${patchFolderChampion}/${champion.id}_square.png`));
|
||||
});
|
||||
|
||||
this.summonerSpells.forEach(spell => {
|
||||
spell = this.extendSummonerSpell(spell);
|
||||
tasks.push(downloadFile(spell.icon, `${patchFolderSpell}/${spell.id}.png`));
|
||||
});
|
||||
|
||||
log.info(`Downloading ${tasks.length} assets from datadragon!`);
|
||||
const batchSize = 10;
|
||||
|
||||
const bar = new cliProgress.Bar({
|
||||
format: 'Downloading assets [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}'
|
||||
});
|
||||
|
||||
bar.start(tasks.length, 0);
|
||||
for (let i = 0; i < tasks.length; i = i + batchSize) {
|
||||
const currentTasks = tasks.slice(i, i + batchSize);
|
||||
|
||||
await Promise.all(currentTasks.map(task => task()));
|
||||
bar.update(i + 1);
|
||||
}
|
||||
bar.stop();
|
||||
|
||||
log.info(`Downloading ${tasks.length} assets finished.`);
|
||||
}
|
||||
}
|
||||
|
||||
export default DataDragon;
|
||||
@@ -0,0 +1,139 @@
|
||||
import logger from '../../logging';
|
||||
import { Session, Cell, Summoner } from '../../types/lcu';
|
||||
|
||||
import { CurrentState } from '../CurrentState';
|
||||
import { EventEmitter } from 'events';
|
||||
import DataProviderService from '../DataProviderService';
|
||||
import Recorder from '../../recording/Recorder';
|
||||
import GlobalContext from '../../GlobalContext';
|
||||
import Connector, {
|
||||
ConnectionInfo,
|
||||
LibraryConnector,
|
||||
ExperimentalConnector,
|
||||
} from './connector';
|
||||
import { Response } from 'league-connect';
|
||||
const log = logger('LCUDataProviderService');
|
||||
|
||||
class LeagueDataProviderService extends EventEmitter
|
||||
implements DataProviderService {
|
||||
connector: Connector;
|
||||
connectionInfo!: ConnectionInfo;
|
||||
recorder!: Recorder;
|
||||
|
||||
requestConfig = {
|
||||
json: true,
|
||||
rejectUnauthorized: false,
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
summoners: Array<Summoner> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (GlobalContext.commandLine.experimentalConnector) {
|
||||
this.connector = new ExperimentalConnector({
|
||||
leaguePath: GlobalContext.commandLine.leaguePath,
|
||||
});
|
||||
} else {
|
||||
this.connector = new LibraryConnector({
|
||||
leaguePath: GlobalContext.commandLine.leaguePath,
|
||||
});
|
||||
}
|
||||
|
||||
this.onLeagueConnected = this.onLeagueConnected.bind(this);
|
||||
this.onLeagueDisconnected = this.onLeagueDisconnected.bind(this);
|
||||
this.getCurrentData = this.getCurrentData.bind(this);
|
||||
|
||||
if (GlobalContext.commandLine.record) {
|
||||
this.recorder = new Recorder(GlobalContext.commandLine.record);
|
||||
log.info('Recording to ' + GlobalContext.commandLine.record);
|
||||
}
|
||||
|
||||
this.connector.on('connect', this.onLeagueConnected);
|
||||
this.connector.on('disconnect', this.onLeagueDisconnected);
|
||||
|
||||
this.connector.start();
|
||||
log.info('Waiting for LeagueClient to connect');
|
||||
}
|
||||
|
||||
async getCurrentData(): Promise<CurrentState | null> {
|
||||
if (!this.connectionInfo || !this.connectionInfo.port) {
|
||||
log.debug('Not connected to LCU, but tried to get data.');
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await this.connector.request({
|
||||
url: '/lol-champ-select/v1/session',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
const currentState = new CurrentState(
|
||||
response?.ok ?? false,
|
||||
await response?.json()
|
||||
);
|
||||
|
||||
if (this.recorder) {
|
||||
this.recorder.addDataPoint(currentState);
|
||||
this.recorder.save();
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
async cacheSummoners(session: Session): Promise<void> {
|
||||
const blueTeam = session.myTeam;
|
||||
const redTeam = session.theirTeam;
|
||||
|
||||
const fetchPlayersFromTeam = (
|
||||
team: Array<Cell>
|
||||
): Array<Promise<Response<any> | undefined>> =>
|
||||
team.map((cell) =>
|
||||
this.connector.request({
|
||||
url: `/lol-summoner/v1/summoners/${cell.summonerId}`,
|
||||
method: 'GET'
|
||||
})
|
||||
);
|
||||
|
||||
const jobs = [
|
||||
...fetchPlayersFromTeam(blueTeam),
|
||||
...fetchPlayersFromTeam(redTeam),
|
||||
];
|
||||
|
||||
const completedJobs = await Promise.all(jobs);
|
||||
|
||||
completedJobs.forEach(async (job) => {
|
||||
this.summoners.push(await job?.json());
|
||||
});
|
||||
|
||||
if (this.recorder) {
|
||||
this.recorder.setSummoners(this.summoners);
|
||||
}
|
||||
}
|
||||
|
||||
getSummonerById(id: number): Summoner {
|
||||
return this.summoners.filter((summoner) => summoner.summonerId === id)[0];
|
||||
}
|
||||
|
||||
onLeagueConnected(e: ConnectionInfo): void {
|
||||
log.info(`LeagueClient connected: ${JSON.stringify(e)}`);
|
||||
this.connectionInfo = e;
|
||||
this.requestConfig.username = this.connectionInfo.username;
|
||||
this.requestConfig.password = this.connectionInfo.password;
|
||||
|
||||
this.emit('connected');
|
||||
}
|
||||
|
||||
onLeagueDisconnected(): void {
|
||||
log.info('LeagueClient disconnected');
|
||||
|
||||
if (this.recorder) {
|
||||
this.recorder.save();
|
||||
}
|
||||
|
||||
this.emit('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
export default LeagueDataProviderService;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { RequestOptions, Response } from 'league-connect';
|
||||
|
||||
export class ConnectionInfo {
|
||||
port!: number;
|
||||
username!: string;
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class ConnectorOptions {
|
||||
leaguePath?: string;
|
||||
}
|
||||
|
||||
declare interface Connector extends EventEmitter {
|
||||
options: ConnectorOptions;
|
||||
|
||||
start(): void;
|
||||
|
||||
on(event: 'disconnect', listener: () => void): this;
|
||||
on(event: 'connect', listener: (info: ConnectionInfo) => void): this;
|
||||
|
||||
request(args: RequestOptions): Promise<Response<any> | undefined>;
|
||||
}
|
||||
export default Connector;
|
||||
@@ -0,0 +1,120 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import cp from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import logger from '../../../logging';
|
||||
import Connector, { ConnectorOptions, ConnectionInfo } from './Connector';
|
||||
import { RequestOptions, Response } from 'league-connect';
|
||||
import needle from 'needle';
|
||||
|
||||
const log = logger('ExperimentalConnector');
|
||||
|
||||
class ExperimentalConnectorOptions extends ConnectorOptions {}
|
||||
|
||||
export default class ExperimentalConnector extends EventEmitter
|
||||
implements Connector {
|
||||
options: ExperimentalConnectorOptions;
|
||||
connectionInfo?: ConnectionInfo
|
||||
|
||||
isConnected = false;
|
||||
|
||||
requestConfig = {
|
||||
json: true,
|
||||
rejectUnauthorized: false,
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
constructor(options: ExperimentalConnectorOptions) {
|
||||
super();
|
||||
|
||||
this.options = options;
|
||||
|
||||
log.debug('ExperimentalConnector initialized');
|
||||
log.warn(
|
||||
'The experimental connector is another system used to find out how to talk to your local league client, in order to fetch real time information. It currently only' +
|
||||
' supports windows, and will probably perform worse than the LibraryConnector. However, if you have no success with the LibraryConnector, you may also try this.'
|
||||
);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
log.info('ExperimentalConnector started');
|
||||
this.update();
|
||||
|
||||
setInterval(() => this.update(), 5000)
|
||||
}
|
||||
|
||||
async request(args: RequestOptions<any>): Promise<Response<any> | undefined> {
|
||||
if (!this.connectionInfo) return
|
||||
return await needle(
|
||||
'get',
|
||||
`https://127.0.0.1:${this.connectionInfo.port}${args.url}`,
|
||||
this.requestConfig
|
||||
) as unknown as Response<any>
|
||||
}
|
||||
|
||||
update(): void {
|
||||
if (this.isConnected) {
|
||||
log.debug('Already connected, not checking.')
|
||||
return;
|
||||
}
|
||||
|
||||
const readLockfile = async (leaguePath: string): Promise<void> => {
|
||||
// Read lockfile
|
||||
const lockFilePath = path.join(leaguePath, 'lockfile')
|
||||
if (!fs.existsSync(lockFilePath)) {
|
||||
log.info(`Failed to find lock file at ${lockFilePath}. Is the League Client running and the location correct?`)
|
||||
return
|
||||
}
|
||||
const lockfile = await fs.readFile(
|
||||
lockFilePath,
|
||||
'utf8'
|
||||
);
|
||||
log.debug('Found lockfile at: ' + lockFilePath);
|
||||
const splitted = lockfile.split(':');
|
||||
|
||||
if (!this.isConnected) {
|
||||
this.isConnected = true;
|
||||
|
||||
const connectionInfo: ConnectionInfo = {
|
||||
username: 'riot',
|
||||
port: parseInt(splitted[2], 10),
|
||||
password: splitted[3],
|
||||
};
|
||||
|
||||
this.connectionInfo = connectionInfo
|
||||
|
||||
this.requestConfig.password = splitted[3]
|
||||
this.requestConfig.username = 'riot'
|
||||
|
||||
this.emit('connect', connectionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.leaguePath) {
|
||||
const leaguePath = path.normalize(this.options.leaguePath)
|
||||
log.debug(`Using configured league path: ${leaguePath}`)
|
||||
readLockfile(leaguePath)
|
||||
} else {
|
||||
log.debug('Trying to detect league path automatically.')
|
||||
|
||||
const INSTALL_REGEX_WIN = /"--install-directory=(.*?)"/;
|
||||
const command = 'WMIC PROCESS WHERE name=\'LeagueClientUx.exe\' GET commandline';
|
||||
|
||||
cp.exec(command, async (err, stdout, stderr) => {
|
||||
if (err || !stdout || stderr) {
|
||||
log.debug(
|
||||
'Failed to find the league process. Is the LeagueClient running?', err
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = stdout.match(INSTALL_REGEX_WIN) || [];
|
||||
const leaguePath = parts[1];
|
||||
log.debug('Found LeagueClient installation path: ' + leaguePath);
|
||||
|
||||
readLockfile(leaguePath)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { authenticate, connect, LeagueClient, LeagueWebSocket, Credentials, request, RequestOptions, Response } from 'league-connect'
|
||||
import logger from '../../../logging';
|
||||
import Connector, { ConnectorOptions } from './Connector';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const log = logger('LibraryConnector');
|
||||
|
||||
class LibraryConnectorOptions extends ConnectorOptions {}
|
||||
|
||||
export default class LibraryConnector extends EventEmitter
|
||||
implements Connector {
|
||||
options: LibraryConnectorOptions;
|
||||
|
||||
leagueClient?: LeagueClient;
|
||||
credentials?: Credentials;
|
||||
ws?: LeagueWebSocket;
|
||||
|
||||
constructor(options: LibraryConnectorOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
|
||||
log.debug('LibraryConnector initialized');
|
||||
}
|
||||
|
||||
async start() {
|
||||
|
||||
try {
|
||||
const credentials = await authenticate()
|
||||
this.credentials = credentials
|
||||
this.credentials.password = credentials.password.replace('--app-port', '')
|
||||
|
||||
this.ws = await connect(this.credentials);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.emit('connect', this.credentials)
|
||||
log.info('LibraryConnector started');
|
||||
}
|
||||
|
||||
this.ws.onerror = e => {
|
||||
log.error(`LibraryConnector failed ${e}`);
|
||||
this.emit('disconnect')
|
||||
}
|
||||
|
||||
this.leagueClient = new LeagueClient(this.credentials);
|
||||
this.leagueClient.start()
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
|
||||
this.leagueClient?.on('connect', (newCredentials) => {
|
||||
this.credentials = newCredentials
|
||||
this.credentials.password = newCredentials.password.replace('--app-port', '')
|
||||
this.emit('connect', this.credentials)
|
||||
})
|
||||
|
||||
this.leagueClient?.on('disconnect', () => {
|
||||
this.credentials = undefined
|
||||
this.emit('disconnect')
|
||||
})
|
||||
}
|
||||
|
||||
public async request (arg: RequestOptions): Promise<Response<any> | undefined> {
|
||||
if (!this.credentials) return
|
||||
|
||||
try {
|
||||
const res = await request(arg, this.credentials)
|
||||
return res
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Connector, { ConnectionInfo } from './Connector';
|
||||
import LibraryConnector from './LibraryConnector';
|
||||
import ExperimentalConnector from './ExperimentalConnector';
|
||||
|
||||
export { ConnectionInfo, LibraryConnector, ExperimentalConnector };
|
||||
|
||||
export default Connector;
|
||||
@@ -0,0 +1,4 @@
|
||||
import logger, { setLogLevel } from './logger';
|
||||
|
||||
export default logger;
|
||||
export { setLogLevel };
|
||||
@@ -0,0 +1,45 @@
|
||||
import winston, { Logger } from 'winston';
|
||||
import GlobalContext from '../GlobalContext';
|
||||
|
||||
const loggers: Logger[] = [];
|
||||
|
||||
const customFormat = winston.format.printf(
|
||||
({ level, message, label, timestamp }) =>
|
||||
`${timestamp} [${level.padEnd(15)}] ${`\u001b[95m${label}\u001b[39m`.padEnd(
|
||||
22
|
||||
)}: ${message}`
|
||||
);
|
||||
|
||||
const createLogger = (label: string): Logger => {
|
||||
const logger = winston.createLogger({
|
||||
level: GlobalContext.commandLine.debug
|
||||
? 'debug'
|
||||
: process.env.LOGLEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
customFormat
|
||||
),
|
||||
defaultMeta: { label },
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
// new winston.transports.File({ filename: 'combined.log' })
|
||||
new winston.transports.Console(),
|
||||
],
|
||||
});
|
||||
loggers.push(logger);
|
||||
return logger;
|
||||
};
|
||||
|
||||
const setLogLevel = (level: string) => {
|
||||
loggers.forEach((logger) => {
|
||||
logger.level = level;
|
||||
});
|
||||
};
|
||||
|
||||
export default (label: string): Logger => createLogger(label);
|
||||
export { setLogLevel };
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "backend-build/app.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "npm run build && node backend-build/app.js",
|
||||
"tsc": "tsc",
|
||||
"lint": "eslint backend/**"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "9.0.0",
|
||||
"cli-progress": "^3.3.1",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"fs-extra": "^9.0.0",
|
||||
"league-connect": "^5.4.0",
|
||||
"minimist": "^1.2.3",
|
||||
"morgan": "~1.9.1",
|
||||
"needle": "^2.4.0",
|
||||
"typescript": "^4.9.4",
|
||||
"winston": "^3.2.1",
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-progress": "^1.8.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/needle": "^2.0.4",
|
||||
"@types/node": "12.12.42",
|
||||
"@types/ws": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^2.3.2",
|
||||
"@typescript-eslint/parser": "^2.3.2",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-plugin-import": "^2.18.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CurrentState } from '../data/CurrentState';
|
||||
import fs from 'fs';
|
||||
import { Summoner } from '../types/lcu';
|
||||
import logger from '../logging/logger';
|
||||
import RecordingDatapoint from './RecordingDatapoint';
|
||||
const log = logger('Recorder');
|
||||
|
||||
export default class Recorder {
|
||||
name: string;
|
||||
dataPoints: Array<RecordingDatapoint> = [];
|
||||
summoners: Array<Summoner> = [];
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
addDataPoint(state: CurrentState): void {
|
||||
const dataPoint = new RecordingDatapoint(state.isChampSelectActive, state.session, new Date());
|
||||
this.dataPoints.push(dataPoint);
|
||||
}
|
||||
|
||||
setSummoners(summoners: Array<Summoner>): void {
|
||||
this.summoners = summoners;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const recordingsPath = '../recordings';
|
||||
|
||||
if (!fs.existsSync(recordingsPath)) {
|
||||
fs.mkdirSync(recordingsPath);
|
||||
}
|
||||
|
||||
// Remove duplicates from datapoints. This ensures that we have the pace of 1 state change / second!
|
||||
/* const distinctDataPoints = this.dataPoints.filter((value, index, self) => {
|
||||
if (index === 0) {
|
||||
return true;
|
||||
}
|
||||
if (JSON.stringify(self[index - 1].session) === JSON.stringify(value.session)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}); */
|
||||
|
||||
const jsonString = JSON.stringify({
|
||||
dataPoints: this.dataPoints,
|
||||
summoners: this.summoners
|
||||
});
|
||||
|
||||
fs.writeFileSync(recordingsPath + '/' + this.name + '.json', jsonString);
|
||||
log.debug(`Recording ${this.name} saved!`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CurrentState } from '../data/CurrentState';
|
||||
import { Session } from '../types/lcu';
|
||||
|
||||
export default class RecordingDatapoint extends CurrentState {
|
||||
time: Date;
|
||||
|
||||
constructor(isChampSelectActive: boolean, session: Session, time: Date) {
|
||||
super(isChampSelectActive, session);
|
||||
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { EventEmitter } from "events";
|
||||
import convertState from "./converter";
|
||||
import { CurrentState } from "../data/CurrentState";
|
||||
import DataProviderService from "../data/DataProviderService";
|
||||
import State from "./State";
|
||||
import DataDragon from "../data/league/DataDragon";
|
||||
import logger from "../logging/logger";
|
||||
|
||||
const log = logger("Controller");
|
||||
|
||||
export default class Controller extends EventEmitter {
|
||||
dataProvider: DataProviderService;
|
||||
state: State;
|
||||
ddragon: DataDragon;
|
||||
|
||||
constructor(kwargs: {
|
||||
dataProvider: DataProviderService;
|
||||
state: State;
|
||||
ddragon: DataDragon;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.dataProvider = kwargs.dataProvider;
|
||||
this.state = kwargs.state;
|
||||
this.ddragon = kwargs.ddragon;
|
||||
|
||||
this.dataProvider.on("connected", () => {
|
||||
log.debug("DataProvider connected!");
|
||||
this.state.leagueConnected();
|
||||
});
|
||||
|
||||
this.dataProvider.on("disconnected", () => {
|
||||
log.debug("DataProvider disconnected!");
|
||||
this.state.leagueDisconnected();
|
||||
});
|
||||
}
|
||||
|
||||
applyNewState(newState: CurrentState): void {
|
||||
if (!this.state.data.champSelectActive && newState.isChampSelectActive) {
|
||||
log.info("ChampSelect started!");
|
||||
this.state.champselectStarted();
|
||||
|
||||
// Also cache information about summoners
|
||||
this.dataProvider.cacheSummoners(newState.session).then();
|
||||
}
|
||||
if (this.state.data.champSelectActive && !newState.isChampSelectActive) {
|
||||
log.info("ChampSelect ended!");
|
||||
this.state.champselectEnded();
|
||||
}
|
||||
|
||||
// We can't do anything if champselect is not active!
|
||||
if (!newState.isChampSelectActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedData = convertState(newState, this.dataProvider, this.ddragon);
|
||||
|
||||
const currentActionBefore = this.state.data.getCurrentAction();
|
||||
|
||||
this.state.newState(cleanedData);
|
||||
|
||||
// Get the current action
|
||||
const currentActionAfter = this.state.data.getCurrentAction();
|
||||
|
||||
const isActionEqual = (firstAction: any, secondAction: any): boolean => {
|
||||
if (firstAction.state !== secondAction.state) {
|
||||
return false;
|
||||
}
|
||||
if (firstAction.state === "none" && secondAction.state === "none") {
|
||||
return true;
|
||||
}
|
||||
if (firstAction.team !== secondAction.team) {
|
||||
return false;
|
||||
}
|
||||
if (firstAction.num === secondAction.num) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!isActionEqual(currentActionBefore, currentActionAfter)) {
|
||||
const action = this.state.data.refreshAction(currentActionBefore);
|
||||
|
||||
this.state.newAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import EventEmitter from 'events';
|
||||
import * as fs from 'fs';
|
||||
import { Config, StateData, Team } from '../types/dto';
|
||||
|
||||
class State extends EventEmitter {
|
||||
data: StateData;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.data = new StateData();
|
||||
this.data.config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
}
|
||||
|
||||
champselectStarted(): void {
|
||||
this.data.config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
|
||||
this.emit('champSelectStarted');
|
||||
|
||||
this.data.champSelectActive = true;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
champselectEnded(): void {
|
||||
this.data.blueTeam = new Team();
|
||||
this.data.redTeam = new Team();
|
||||
this.data.timer = 0;
|
||||
this.data.champSelectActive = false;
|
||||
|
||||
this.emit('champSelectEnded');
|
||||
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
newState(state: {
|
||||
redTeam: Team;
|
||||
blueTeam: Team;
|
||||
timer: number;
|
||||
state: string;
|
||||
}): void {
|
||||
let shouldUpdate = false;
|
||||
|
||||
if (JSON.stringify(this.data.blueTeam) !== JSON.stringify(state.blueTeam)) {
|
||||
this.data.blueTeam = state.blueTeam;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (JSON.stringify(this.data.redTeam) !== JSON.stringify(state.redTeam)) {
|
||||
this.data.redTeam = state.redTeam;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (this.data.timer !== state.timer) {
|
||||
this.data.timer = state.timer;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (this.data.state !== state.state) {
|
||||
this.data.state = state.state;
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
this.triggerUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
newAction(action: any): void {
|
||||
this.emit('newAction', action);
|
||||
}
|
||||
|
||||
leagueConnected(): void {
|
||||
this.data.leagueConnected = true;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
leagueDisconnected(): void {
|
||||
this.data.leagueConnected = false;
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
triggerUpdate(): void {
|
||||
this.emit('stateUpdate', this.data);
|
||||
}
|
||||
|
||||
getVersionCDN(): string {
|
||||
return `${this.data.meta.cdn}/${this.data.meta.version.champion}`;
|
||||
}
|
||||
|
||||
getVersion(): string {
|
||||
return this.data.meta.version.champion;
|
||||
}
|
||||
|
||||
getCDN(): string {
|
||||
return this.data.meta.cdn;
|
||||
}
|
||||
|
||||
getMajorMinorVersion(): string {
|
||||
return this.data.meta.version.champion.split('.').slice(0, 2).join('.');
|
||||
}
|
||||
|
||||
getConfig(): Config {
|
||||
return this.data.config;
|
||||
}
|
||||
}
|
||||
|
||||
export default State;
|
||||
@@ -0,0 +1,142 @@
|
||||
import {Action, ActionType, Cell, Timer} from '../types/lcu';
|
||||
import {Ban, Champion, Pick, Team} from '../types/dto';
|
||||
import DataProviderService from '../data/DataProviderService';
|
||||
import DataDragon from '../data/league/DataDragon';
|
||||
import {CurrentState} from '../data/CurrentState';
|
||||
import RecordingDatapoint from '../recording/RecordingDatapoint';
|
||||
|
||||
const convertTeam = (kwargs: { team: Array<Cell>; actions: Array<Action>; dataProvider: DataProviderService; ddragon: DataDragon }): Team => {
|
||||
const newTeam = new Team();
|
||||
newTeam.picks = kwargs.team.map((cell: Cell) => {
|
||||
const currentAction = kwargs.actions.find((action) => !action.completed);
|
||||
|
||||
const pick = new Pick(cell.cellId);
|
||||
|
||||
const spell1 = kwargs.ddragon.getSummonerSpellById(cell.spell1Id);
|
||||
pick.spell1 = {
|
||||
id: cell.spell1Id,
|
||||
icon: spell1 ? spell1.icon : '',
|
||||
};
|
||||
|
||||
const spell2 = kwargs.ddragon.getSummonerSpellById(cell.spell2Id);
|
||||
pick.spell2 = {
|
||||
id: cell.spell2Id,
|
||||
icon: spell2 ? spell2.icon : '',
|
||||
};
|
||||
|
||||
const champion = kwargs.ddragon.getChampionById(cell.championId);
|
||||
pick.champion = {
|
||||
id: cell.championId,
|
||||
name: champion ? champion.name : '',
|
||||
idName: champion ? champion.id.toString() : '',
|
||||
loadingImg: champion ? champion.loadingImg : '',
|
||||
splashImg: champion ? champion.splashImg : '',
|
||||
splashCenteredImg: champion ? champion.splashCenteredImg : '',
|
||||
squareImg: champion ? champion.squareImg : '',
|
||||
};
|
||||
|
||||
const summoner = kwargs.dataProvider.getSummonerById(cell.summonerId);
|
||||
if (summoner) {
|
||||
pick.displayName = summoner.displayName;
|
||||
}
|
||||
|
||||
if (currentAction && currentAction.type === ActionType.PICK && currentAction.actorCellId === cell.cellId && !currentAction.completed) {
|
||||
pick.isActive = true;
|
||||
newTeam.isActive = true;
|
||||
}
|
||||
|
||||
return pick;
|
||||
});
|
||||
|
||||
const isInThisTeam = (cellId: number): boolean => kwargs.team.filter((cell) => cell.cellId === cellId).length !== 0;
|
||||
|
||||
let isBanDetermined = false;
|
||||
newTeam.bans = kwargs.actions.filter((action) => action.type === ActionType.BAN && isInThisTeam(action.actorCellId)).map((action) => {
|
||||
const ban = new Ban();
|
||||
|
||||
if (!action.completed && !isBanDetermined) {
|
||||
isBanDetermined = true;
|
||||
ban.isActive = true;
|
||||
newTeam.isActive = true;
|
||||
ban.champion = new Champion();
|
||||
return ban;
|
||||
}
|
||||
|
||||
const champion = kwargs.ddragon.getChampionById(action.championId);
|
||||
ban.champion = {
|
||||
id: action.championId,
|
||||
name: champion ? champion.name : '',
|
||||
idName: champion ? champion.id.toString() : '',
|
||||
loadingImg: champion ? champion.loadingImg : '',
|
||||
splashImg: champion ? champion.splashImg : '',
|
||||
splashCenteredImg: champion ? champion.splashCenteredImg : '',
|
||||
squareImg: champion ? champion.squareImg : '',
|
||||
};
|
||||
|
||||
return ban;
|
||||
});
|
||||
|
||||
return newTeam;
|
||||
};
|
||||
|
||||
const convertTimer = (timer: Timer, currentDate: Date): number => {
|
||||
const startOfPhase = timer.internalNowInEpochMs;
|
||||
const expectedEndOfPhase = startOfPhase + timer.adjustedTimeLeftInPhase;
|
||||
|
||||
const countdown = expectedEndOfPhase - currentDate.getTime();
|
||||
const countdownSec = Math.floor(countdown / 1000);
|
||||
|
||||
if (countdownSec < 0) {
|
||||
return 0;
|
||||
}
|
||||
return countdownSec;
|
||||
};
|
||||
|
||||
const convertStateName = (actions: Array<Action>) => {
|
||||
const currentActionIndex = actions.findIndex((action) => !action.completed);
|
||||
|
||||
if (currentActionIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const currentAction = actions[currentActionIndex];
|
||||
if (currentAction.type == ActionType.BAN) {
|
||||
if (currentActionIndex <= 6) {
|
||||
return 'BAN PHASE 1';
|
||||
} else {
|
||||
return 'BAN PHASE 2';
|
||||
}
|
||||
} else {
|
||||
if (currentActionIndex <= 12) {
|
||||
return 'PICK PHASE 1';
|
||||
} else {
|
||||
return 'PICK PHASE 2';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const convertState = (state: CurrentState, dataProvider: DataProviderService, ddragon: DataDragon): { blueTeam: Team; redTeam: Team; timer: number; state: string } => {
|
||||
const lcuSession = state.session;
|
||||
|
||||
const currentDate = (state as RecordingDatapoint).time ? new Date((state as RecordingDatapoint).time) : new Date();
|
||||
|
||||
const flattenedActions: Array<Action> = [];
|
||||
lcuSession.actions.forEach(actionGroup => {
|
||||
flattenedActions.push(...actionGroup);
|
||||
});
|
||||
|
||||
const blueTeam = convertTeam({ team: lcuSession.myTeam, actions: flattenedActions, dataProvider, ddragon });
|
||||
const redTeam = convertTeam({ team: lcuSession.theirTeam, actions: flattenedActions, dataProvider, ddragon });
|
||||
|
||||
const timer = convertTimer(lcuSession.timer, currentDate);
|
||||
const stateName = convertStateName(flattenedActions);
|
||||
|
||||
return {
|
||||
blueTeam,
|
||||
redTeam,
|
||||
timer,
|
||||
state: stateName
|
||||
};
|
||||
};
|
||||
|
||||
export default convertState;
|
||||
@@ -0,0 +1,3 @@
|
||||
import State from './State';
|
||||
|
||||
export default State;
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"incremental": true, /* Enable incremental compilation */
|
||||
"target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./backend-build", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
"tsBuildInfoFile": "./backend-build/.tsbuildinfo", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": ["./types"], /* List of folders to include type definitions from. */
|
||||
// "types": ["./types.ts"], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface Actionable {
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Actionable, Champion } from '.';
|
||||
|
||||
export class Ban implements Actionable {
|
||||
champion = new Champion();
|
||||
isActive = false;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export class Champion {
|
||||
id = 0;
|
||||
name = '';
|
||||
key? = '';
|
||||
splashImg = '';
|
||||
splashCenteredImg = '';
|
||||
loadingImg = '';
|
||||
squareImg = '';
|
||||
idName = '';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class Config {
|
||||
contentPatch = 'latest';
|
||||
contentCdn = 'https://ddragon.leagueoflegends.com/cdn';
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Version } from '.';
|
||||
|
||||
export class Meta {
|
||||
cdn = '';
|
||||
version = new Version();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Actionable, Champion, Spell } from '.';
|
||||
|
||||
export class Pick implements Actionable {
|
||||
id: number;
|
||||
spell1!: Spell;
|
||||
spell2!: Spell;
|
||||
champion!: Champion;
|
||||
isActive = false;
|
||||
displayName = '';
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class Spell {
|
||||
id = 0;
|
||||
key?: string;
|
||||
icon = '';
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Config, Meta, Team } from '.';
|
||||
|
||||
export class StateData {
|
||||
leagueConnected = false;
|
||||
champSelectActive = false;
|
||||
blueTeam = new Team();
|
||||
redTeam = new Team();
|
||||
meta = new Meta();
|
||||
timer = 0;
|
||||
state = 'PICK 1';
|
||||
config = new Config();
|
||||
|
||||
getCurrentAction() {
|
||||
// If either team is active, return none
|
||||
if (!this.blueTeam.isActive && !this.redTeam.isActive) {
|
||||
return {
|
||||
state: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
// If both teams are active, return none
|
||||
if (this.blueTeam.isActive && this.redTeam.isActive) {
|
||||
return {
|
||||
state: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
// get the active team
|
||||
const activeTeam = this.blueTeam.isActive ? this.blueTeam : this.redTeam;
|
||||
const activeTeamName = this.blueTeam.isActive ? 'blueTeam' : 'redTeam';
|
||||
|
||||
const activeBans = activeTeam.bans.filter((ban) => ban.isActive);
|
||||
const activePicks = activeTeam.picks.filter((pick) => pick.isActive);
|
||||
|
||||
if (activeBans.length > 0) {
|
||||
return {
|
||||
state: 'ban',
|
||||
data: activeBans[0],
|
||||
team: activeTeamName,
|
||||
num: activeTeam.bans.indexOf(activeBans[0]),
|
||||
};
|
||||
}
|
||||
|
||||
if (activePicks.length > 0) {
|
||||
return {
|
||||
state: 'pick',
|
||||
data: activePicks[0],
|
||||
team: activeTeamName,
|
||||
num: activeTeam.picks.indexOf(activePicks[0]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
refreshAction(action: any) {
|
||||
if (action.state === 'none') {
|
||||
return action;
|
||||
}
|
||||
|
||||
const team = action.team === 'blueTeam' ? this.blueTeam : this.redTeam;
|
||||
|
||||
const array = action.state === 'ban' ? team.bans : team.picks;
|
||||
|
||||
return {
|
||||
...action,
|
||||
data: array[action.num],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Ban, Pick, Actionable } from '.';
|
||||
|
||||
export class Team implements Actionable {
|
||||
bans: Array<Ban> = [];
|
||||
picks: Array<Pick> = [];
|
||||
isActive = false;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class Version {
|
||||
champion = '';
|
||||
item = '';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from './Actionable';
|
||||
export * from './Ban';
|
||||
export * from './Champion';
|
||||
export * from './Meta';
|
||||
export * from './Pick';
|
||||
export * from './Spell';
|
||||
export * from './StateData';
|
||||
export * from './Team';
|
||||
export * from './Version';
|
||||
export * from './Config';
|
||||
@@ -0,0 +1,5 @@
|
||||
import PBEvent from './PBEvent';
|
||||
|
||||
export default class ChampSelectEndedEvent implements PBEvent {
|
||||
eventType = 'champSelectEnded';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import PBEvent from './PBEvent';
|
||||
|
||||
export default class ChampSelectStartedEvent implements PBEvent {
|
||||
eventType = 'champSelectStarted';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import PBEvent from './PBEvent';
|
||||
import { Config } from '../dto';
|
||||
|
||||
export default class HeartbeatEvent implements PBEvent {
|
||||
constructor(config: Config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
eventType = 'heartbeat';
|
||||
config: Config
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import PBEvent from "./PBEvent";
|
||||
|
||||
export default class NewActionEvent implements PBEvent {
|
||||
constructor(action: any) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
eventType = "newAction";
|
||||
action: any;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import PBEvent from './PBEvent';
|
||||
import { StateData } from '../dto';
|
||||
|
||||
export default class NewStateEvent implements PBEvent {
|
||||
constructor(state: StateData) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
eventType = 'newState';
|
||||
state: StateData
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default interface PBEvent {
|
||||
eventType: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ActionType } from '.';
|
||||
|
||||
export class Action {
|
||||
completed!: boolean;
|
||||
championId!: number;
|
||||
type!: ActionType;
|
||||
actorCellId!: number;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum ActionType {
|
||||
PICK = 'pick',
|
||||
BAN = 'ban'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class Cell {
|
||||
cellId!: number;
|
||||
championId!: number;
|
||||
summonerId!: number;
|
||||
spell1Id!: number;
|
||||
spell2Id!: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Action, Cell, Timer } from '.';
|
||||
|
||||
export class Session {
|
||||
myTeam: Array<Cell> = [];
|
||||
theirTeam: Array<Cell> = [];
|
||||
actions: Array<Array<Action>> = [];
|
||||
timer: Timer = new Timer();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class Summoner {
|
||||
displayName = '';
|
||||
summonerId = 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class Timer {
|
||||
adjustedTimeLeftInPhase = 0;
|
||||
adjustedTimeLeftInPhaseInSec = 0;
|
||||
internalNowInEpochMs = 0;
|
||||
phase = '';
|
||||
timeLeftInPhase = 0;
|
||||
timeLeftInPhaseInSec = 0;
|
||||
totalTimeInPhase = 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './Action';
|
||||
export * from './ActionType';
|
||||
export * from './Cell';
|
||||
export * from './Session';
|
||||
export * from './Summoner';
|
||||
export * from './Timer';
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as ws from "ws";
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
|
||||
import logger from "../logging";
|
||||
import { StateData } from "../types/dto";
|
||||
import State from "../state";
|
||||
import PBEvent from "../types/events/PBEvent";
|
||||
import NewStateEvent from "../types/events/NewStateEvent";
|
||||
import HeartbeatEvent from "../types/events/HeartbeatEvent";
|
||||
import ChampSelectStartedEvent from "../types/events/ChampSelectStartedEvent";
|
||||
import ChampSelectEndedEvent from "../types/events/ChampSelectEndedEvent";
|
||||
import NewActionEvent from "../types/events/NewActionEvent";
|
||||
|
||||
const log = logger("websocket");
|
||||
|
||||
class WebSocketServer {
|
||||
server: ws.Server;
|
||||
state: State;
|
||||
clients: Array<WebSocket> = [];
|
||||
exampleClients: Array<WebSocket> = [];
|
||||
heartbeatInterval?: NodeJS.Timeout;
|
||||
config: any;
|
||||
|
||||
constructor(server: http.Server, state: State) {
|
||||
this.server = new ws.Server({ server });
|
||||
this.state = state;
|
||||
|
||||
this.sendHeartbeat = this.sendHeartbeat.bind(this);
|
||||
|
||||
// Event listeners
|
||||
this.server.on("connection", (socket: WebSocket, request) =>
|
||||
this.handleConnection(socket, request)
|
||||
);
|
||||
|
||||
state.on("stateUpdate", (newState: StateData) => {
|
||||
newState.config = this.config;
|
||||
this.sendEvent(new NewStateEvent(newState));
|
||||
});
|
||||
state.on("champSelectStarted", () =>
|
||||
this.sendEvent(new ChampSelectStartedEvent())
|
||||
);
|
||||
state.on("champSelectEnded", () =>
|
||||
this.sendEvent(new ChampSelectEndedEvent())
|
||||
);
|
||||
state.on("newAction", (action) => {
|
||||
this.sendEvent(new NewActionEvent(action));
|
||||
});
|
||||
}
|
||||
|
||||
startHeartbeat(): void {
|
||||
this.heartbeatInterval = setInterval(this.sendHeartbeat, 1000);
|
||||
}
|
||||
|
||||
handleConnection(socket: WebSocket, request: http.IncomingMessage): void {
|
||||
this.clients.push(socket);
|
||||
socket.send(JSON.stringify(new NewStateEvent(this.state.data)));
|
||||
}
|
||||
|
||||
sendEvent(event: PBEvent): void {
|
||||
const serializedEvent = JSON.stringify(event);
|
||||
log.debug(`New Event: ${serializedEvent}`);
|
||||
|
||||
this.clients.forEach((client: WebSocket) => {
|
||||
client.send(serializedEvent);
|
||||
});
|
||||
}
|
||||
|
||||
sendHeartbeat(): void {
|
||||
this.config = JSON.parse(fs.readFileSync("./config.json", "utf8"));
|
||||
const heartbeatEvent = new HeartbeatEvent(this.config);
|
||||
const heartbeatSerialized = JSON.stringify(heartbeatEvent);
|
||||
|
||||
this.clients.forEach((client: WebSocket) => {
|
||||
client.send(heartbeatSerialized);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default WebSocketServer;
|
||||
@@ -0,0 +1,3 @@
|
||||
import websocket from './WebSocketServer';
|
||||
|
||||
export default websocket;
|
||||
@@ -0,0 +1 @@
|
||||
BROWSER=none
|
||||
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# 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*
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "layout",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.1",
|
||||
"sass": "^1.57.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable */
|
||||
/* Event Emitter */
|
||||
const eventify = (self) => {
|
||||
self.events = {};
|
||||
|
||||
self.on = function (event, listener) {
|
||||
if (typeof self.events[event] !== 'object') {
|
||||
self.events[event] = []
|
||||
}
|
||||
|
||||
self.events[event].push(listener)
|
||||
};
|
||||
|
||||
self.removeListener = function (event, listener) {
|
||||
let idx;
|
||||
|
||||
if (typeof self.events[event] === 'object') {
|
||||
idx = self.events[event].indexOf(listener);
|
||||
|
||||
if (idx > -1) {
|
||||
self.events[event].splice(idx, 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.emit = function (event) {
|
||||
let i, listeners, length, args = [].slice.call(arguments, 1);
|
||||
|
||||
if (typeof self.events[event] === 'object') {
|
||||
listeners = self.events[event].slice();
|
||||
length = listeners.length;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
listeners[i].apply(self, args)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.once = function (event, listener) {
|
||||
self.on(event, function g () {
|
||||
self.removeListener(event, g);
|
||||
listener.apply(self, arguments)
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
const getQueryVariable = variable => {
|
||||
const query = window.location.search.substring(1);
|
||||
const vars = query.split('&');
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split('=');
|
||||
if (decodeURIComponent(pair[0]) === variable) {
|
||||
return decodeURIComponent(pair[1]);
|
||||
}
|
||||
}
|
||||
console.log('Query variable %s not found', variable);
|
||||
};
|
||||
|
||||
const PB = {};
|
||||
eventify(PB);
|
||||
|
||||
PB.getQueryVariable = getQueryVariable;
|
||||
|
||||
|
||||
PB.toAbsoluteUrl = (convertUrl, baseUrl) => {
|
||||
baseUrl = baseUrl || getQueryVariable('backend');
|
||||
|
||||
if (!convertUrl || !convertUrl.startsWith('/cache')) {
|
||||
return convertUrl;
|
||||
}
|
||||
|
||||
const httpBackendUrl = baseUrl.replace('ws://', 'http://').replace('wss://', 'https://');
|
||||
const components = httpBackendUrl.split('/');
|
||||
|
||||
return components[0] + '//' + components[2] + convertUrl;
|
||||
};
|
||||
|
||||
|
||||
PB.start = function(url) {
|
||||
this.backend = getQueryVariable('backend') || url;
|
||||
|
||||
console.log('[PB] Connecting to ws backend on ' + this.backend);
|
||||
|
||||
const connect = () => {
|
||||
this.socket = new WebSocket(this.backend);
|
||||
this.emit('statusChange', 'CONNECTING');
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.emit('statusChange', 'CONNECTED');
|
||||
console.log('[PB] Connection established!')
|
||||
};
|
||||
this.socket.onclose = () => {
|
||||
this.emit('statusChange', 'CLOSED');
|
||||
setTimeout(connect, 500);
|
||||
console.log('[PB] Attempt reconnect in 500ms');
|
||||
};
|
||||
this.socket.onerror = () => {
|
||||
this.emit('statusChange', 'ERROR');
|
||||
};
|
||||
this.socket.onmessage = msg => {
|
||||
const data = JSON.parse(msg.data);
|
||||
// Maybe check if heartbeat arrives regularly to assure that connection is alive?
|
||||
|
||||
if (data.eventType) {
|
||||
this.emit(data.eventType, data);
|
||||
} else {
|
||||
console.log('[PB] Unexpected packet format: ' + JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
};
|
||||
|
||||
Window.PB = PB;
|
||||
@@ -0,0 +1,14 @@
|
||||
<!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>ZSŁ LOL CUP - Pick/Ban UI</title>
|
||||
<script src="frontend-lib.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import Overlay from "./components/Overlay";
|
||||
import convertState from './convertState';
|
||||
import Error from './Error';
|
||||
|
||||
function App() {
|
||||
const [globalState, setGlobalState] = useState({});
|
||||
const [config, setConfig] = useState({
|
||||
frontend: {
|
||||
scoreEnabled: false,
|
||||
spellsEnabled: true,
|
||||
blueTeam: {
|
||||
name: "Team Blue",
|
||||
score: 0,
|
||||
color: "rgb(0,151,196)"
|
||||
},
|
||||
redTeam: {
|
||||
name: "Team Red",
|
||||
score: 0,
|
||||
color: "rgb(222,40,70)"
|
||||
},
|
||||
patch: ""
|
||||
}
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
useEffect(() => {
|
||||
Window.PB.on('newState', state => {
|
||||
setGlobalState(state.state);
|
||||
setConfig(state.state.config);
|
||||
});
|
||||
|
||||
Window.PB.on('heartbeat', hb => {
|
||||
console.info(`Got new config: ${JSON.stringify(hb.config)}`);
|
||||
setConfig(hb.config);
|
||||
});
|
||||
|
||||
try {
|
||||
Window.PB.start();
|
||||
} catch {
|
||||
setError('error: failed to read backend url query param. make sure you set ?backend=ws://[ip]:[port] as query parameter.')
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (Window.PB.getQueryVariable('status') === '1') {
|
||||
const status = {
|
||||
backend: Window.PB.getQueryVariable('backend'),
|
||||
error: error,
|
||||
config: config,
|
||||
state: { ...globalState, config: undefined, blueTeam: JSON.stringify(globalState.blueTeam), redTeam: JSON.stringify(globalState.redTeam) }
|
||||
}
|
||||
return <Error message={`status: ${JSON.stringify(status, undefined, 4)}`} isStatus />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error message={error} />
|
||||
}
|
||||
|
||||
if (config) {
|
||||
return (
|
||||
<div className="App">
|
||||
<Overlay state={convertState(globalState, Window.PB.backend)} config={config}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Error message='Unable to load configuration' />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,15 @@
|
||||
.Error--Container {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.Error--Background {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
max-width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import './Error.css';
|
||||
|
||||
const Error = ({ message, isStatus }) => {
|
||||
return (
|
||||
<div className="Error--Container">
|
||||
<div className="Error--Background" style={isStatus ? { fontSize: 11 } : {}}>
|
||||
<pre style={{ maxWidth: '100vw' }}>
|
||||
{message}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Error;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><defs><style>.cls-1{fill:#666;}</style></defs><title>ban_placeholder</title><g id="BG"><rect width="60" height="60"/></g><g id="Top"><path class="cls-1" d="M30,13A17,17,0,1,0,47,30,17,17,0,0,0,30,13ZM17.64,30A12.37,12.37,0,0,1,37,19.77L19.77,37A12.36,12.36,0,0,1,17.64,30ZM30,42.36a12.36,12.36,0,0,1-6.95-2.13L40.23,23.05A12.37,12.37,0,0,1,30,42.36Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308 560"><defs><style>.cls-1{fill:#666;}.cls-2{fill:#fff;}</style></defs><title>bot_splash_placeholder</title><g id="Ebene_2" data-name="Ebene 2"><rect class="cls-1" x="144" y="186" width="24" height="24" transform="translate(312 396) rotate(180)"/><polygon class="cls-1" points="127 221.19 127 169 179.7 169 191.82 157 115 157 115 233.07 127 221.19"/></g><g id="Top"><polygon class="cls-2" points="183 178 183 225 136 225 121 240 198 240 198 163 183 178"/></g></svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308 560"><defs><style>.cls-1{fill:#fff;}</style></defs><title>jung_splash_placeholder</title><g id="Top"><path class="cls-1" d="M127,217l25.5,24,6-7.5s6-37.5-27-76.5c7.5,21,12,42,12,51-6-13.5-21-25.5-27-27C127,193,127,217,127,217Z"/><path class="cls-1" d="M155.5,188.5s7.5-21,21-31.5C172,169,165,187.5,162,210A90.81,90.81,0,0,0,155.5,188.5Z"/><path class="cls-1" d="M163,230l13-14s4-23.5,14.5-34C178.5,188,163,206,163,230Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 488 B |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308 560"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#666;}</style></defs><title>mid_splash_placeholder</title><g id="Top"><polygon class="cls-1" points="180 157 115 222 115 240 133 240 198 175 198 157 180 157"/><polygon class="cls-2" points="127 200.89 127 169 158.89 169 170.89 157 115 157 115 212.89 127 200.89"/><polygon class="cls-2" points="186 196.11 186 228 154.11 228 142.11 240 198 240 198 184.11 186 196.11"/></g></svg>
|
||||
|
After Width: | Height: | Size: 482 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308 560"><defs><style>.cls-1{fill:#fff;}</style></defs><title>sup_splash_placeholder</title><g id="Top"><path class="cls-1" d="M166.28,182.11,172.91,202l18.21-3.31-11.59-13.25s14.91-1.65,31.47-13.25H174.56Z"/><path class="cls-1" d="M149.72,182.11,143.09,202l-18.22-3.31,11.6-13.25s-14.91-1.65-31.47-13.25h36.44Z"/><polygon class="cls-1" points="161.31 185.42 158 187.08 154.69 185.42 143.09 233.45 158 243.39 172.91 233.45 161.31 185.42"/><polygon class="cls-1" points="141.44 160.58 158 182.11 174.56 160.58 169.59 155.61 146.41 155.61 141.44 160.58"/></g></svg>
|
||||
|
After Width: | Height: | Size: 616 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 308 560"><defs><style>.cls-1{fill:#666;}.cls-2{fill:#fff;}</style></defs><title>top_splash_placeholder</title><g id="Top"><rect class="cls-1" x="145" y="187" width="24" height="24"/><polygon class="cls-1" points="186 175.81 186 228 133.3 228 121.18 240 198 240 198 163.93 186 175.81"/><polygon class="cls-2" points="130 219 130 172 177 172 192 157 115 157 115 234 130 219"/></g></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import css from './style/index.module.scss';
|
||||
|
||||
const Bans = props => (
|
||||
<div className={cx(css.Ban)}>
|
||||
<div className={cx(css.BanImage, {
|
||||
[css.Active]: props.isActive
|
||||
})}>
|
||||
<img src={props.champion.squareImg} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Bans;
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import Pick from "./Pick";
|
||||
|
||||
import css from './style/index.module.scss';
|
||||
import Ban from "./Ban";
|
||||
|
||||
import logo from '../assets/logo.png';
|
||||
|
||||
export default class Overlay extends React.Component {
|
||||
state = {
|
||||
currentAnimationState: css.TheAbsoluteVoid,
|
||||
openingAnimationPlayed: false
|
||||
};
|
||||
|
||||
playOpeningAnimation() {
|
||||
this.setState({openingAnimationPlayed: true});
|
||||
setTimeout(() => {
|
||||
this.setState({currentAnimationState: css.AnimationHidden});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({currentAnimationState: css.AnimationTimer + ' ' + css.AnimationBansPick});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({currentAnimationState: css.AnimationBansPick + ' ' + css.AnimationBansPickOnly});
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({currentAnimationState: css.AnimationPigs});
|
||||
}, 1000);
|
||||
}, 1450);
|
||||
}, 700);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state, config } = this.props;
|
||||
|
||||
if (state.champSelectActive && !this.state.openingAnimationPlayed) {
|
||||
this.playOpeningAnimation();
|
||||
}
|
||||
|
||||
if (!state.champSelectActive && this.state.openingAnimationPlayed) {
|
||||
this.setState({openingAnimationPlayed: false});
|
||||
this.setState({currentAnimationState: css.TheAbsoluteVoid});
|
||||
}
|
||||
|
||||
console.log(state);
|
||||
|
||||
const renderBans = (teamState) =>{
|
||||
const list = teamState.bans.map((ban, idx) => <Ban key={`ban-${idx}`} {...ban} />);
|
||||
list.splice(3, 0, <div key="ban-spacer" className={css.Spacing} />);
|
||||
return <div className={cx(css.BansBox)}>{list}</div>;
|
||||
};
|
||||
|
||||
const renderTeam = (teamName, teamConfig, teamState) => (
|
||||
<div className={cx(css.Team, teamName)}>
|
||||
<div className={cx(css.Picks)}>
|
||||
{teamState.picks.map((pick, idx) => <Pick key={`pick-${idx}`} config={this.props.config} {...pick} />)}
|
||||
</div>
|
||||
<div className={css.BansWrapper}>
|
||||
<div className={cx(css.Bans, {[css.WithScore]: config.frontend.scoreEnabled})}>
|
||||
{teamName === css.TeamBlue && config.frontend.scoreEnabled && <div className={css.TeamScore}>
|
||||
{teamConfig.score}
|
||||
</div>}
|
||||
{teamName === css.TeamRed && renderBans(teamState)}
|
||||
<div className={css.TeamName}>
|
||||
{teamConfig.name}
|
||||
</div>
|
||||
{teamName === css.TeamBlue && renderBans(teamState)}
|
||||
{teamName === css.TeamRed && config.frontend.scoreEnabled && <div className={css.TeamScore}>
|
||||
{teamConfig.score}
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx(css.Overlay, css.Layout, this.state.currentAnimationState)} style={{"--color-red": config.frontend.redTeam.color, "--color-blue": config.frontend.blueTeam.color}}>
|
||||
{Object.keys(state).length === 0 && <div className={cx(css.infoBox)}>Not connected to backend service!</div>}
|
||||
{Object.keys(state).length !== 0 &&
|
||||
<div className={cx(css.ChampSelect)}>
|
||||
{!state.leagueConnected && <div className={cx(css.infoBox)}>Not connected to client!</div> }
|
||||
<div className={cx(css.MiddleBox)}>
|
||||
<div className={cx(css.Logo)}>
|
||||
<img src={logo} alt="" />
|
||||
</div>
|
||||
<div className={cx(css.Patch)}>
|
||||
{state.state}
|
||||
</div>
|
||||
<div className={cx(css.Timer, {
|
||||
[`${css.Red} ${css.Blue}`]: !state.blueTeam.isActive && !state.redTeam.isActive,
|
||||
[css.Blue]: state.blueTeam.isActive,
|
||||
[css.Red]: state.redTeam.isActive
|
||||
})}>
|
||||
<div className={cx(css.Background, css.Blue)} />
|
||||
<div className={cx(css.Background, css.Red)} />
|
||||
{state.timer < 100 && <div className={cx(css.TimerChars)}>
|
||||
{state.timer.toString().split('').map((char, idx) => <div key={`div-${idx}`}
|
||||
className={cx(css.TimerChar)}>{char}</div>)}
|
||||
</div>}
|
||||
{state.timer >= 100 && <div className={cx(css.TimerChars)}>
|
||||
{state.timer}
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
{renderTeam(css.TeamBlue, config.frontend.blueTeam, state.blueTeam)}
|
||||
{renderTeam(css.TeamRed, config.frontend.redTeam, state.redTeam)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import css from './style/index.module.scss';
|
||||
|
||||
const Pick = props => (
|
||||
<div className={cx(css.Pick, { [css.Active]: props.isActive })}>
|
||||
{props.spell1 && props.spell2 && props.config.frontend.spellsEnabled && props.champion.name && !props.isActive && <div className={cx(css.SummonerSpells)}>
|
||||
<img src={props.spell1.icon} alt="" />
|
||||
<img src={props.spell2.icon} alt="" />
|
||||
</div>}
|
||||
<div className={cx(css.PickImage, {
|
||||
[css.Active]: props.isActive
|
||||
})}>
|
||||
<img src={props.champion.loadingImg} alt="" />
|
||||
</div>
|
||||
<div className={cx(css.PlayerName)}>
|
||||
<span>{props.displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Pick;
|
||||
@@ -0,0 +1,146 @@
|
||||
.TheAbsoluteVoid {
|
||||
* {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.AnimationHidden {
|
||||
.Timer {
|
||||
position: relative;
|
||||
/* bottom: -130px; */
|
||||
bottom: -300px;
|
||||
animation: timer 0.75s forwards;
|
||||
|
||||
.Blue, .Red {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.Team {
|
||||
opacity: 0;
|
||||
}
|
||||
.Logo, .Patch {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timer {
|
||||
100% {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.AnimationTimer {
|
||||
.Timer {
|
||||
.Blue {
|
||||
left: 100% !important;
|
||||
transition: none !important;
|
||||
animation: timer-colors 0.5s forwards;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
.Red {
|
||||
left: -100% !important;
|
||||
transition: none !important;
|
||||
animation: timer-colors 0.5s forwards;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
}
|
||||
.Team {
|
||||
&.TeamBlue {
|
||||
.Bans {
|
||||
transform: translateX(100%);
|
||||
animation: bans-fadein 1s forwards;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
&.TeamRed {
|
||||
.Bans {
|
||||
transform: translateX(-100%);
|
||||
animation: bans-fadein 1s forwards;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timer-colors {
|
||||
100% {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@keyframes bans-fadein {
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.AnimationBansPick {
|
||||
--animation-initial-delay: 0.4s;
|
||||
--animation-progress: 0.1745s;
|
||||
|
||||
.Pick {
|
||||
animation: pick-move-up 0.7s forwards;
|
||||
top: 100%;
|
||||
}
|
||||
.TeamBlue .Pick {
|
||||
&:nth-child(5) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 1));
|
||||
}
|
||||
&:nth-child(4) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 2));
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 3));
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 4));
|
||||
}
|
||||
&:nth-child(1) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 5));
|
||||
}
|
||||
}
|
||||
.TeamRed .Pick {
|
||||
&:nth-child(1) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 1));
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 2));
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 3));
|
||||
}
|
||||
&:nth-child(4) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 4));
|
||||
}
|
||||
&:nth-child(5) {
|
||||
animation-delay: calc(var(--animation-initial-delay) + calc(var(--animation-progress) * 5));
|
||||
}
|
||||
}
|
||||
.Logo, .Patch {
|
||||
position: relative;
|
||||
top: 100%;
|
||||
z-index: -20;
|
||||
animation: pick-move-up 0.7s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.AnimationBansPickOnly {
|
||||
.Timer {
|
||||
.Blue {
|
||||
left: 0 !important;
|
||||
}
|
||||
.Red {
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pick-move-up {
|
||||
100% {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.AnimationPigs {
|
||||
// EPIC gamer moment
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.Bans {
|
||||
width: 100%;
|
||||
height: var(--box-ban-height);
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
.Ban {
|
||||
display: block;
|
||||
width: calc(var(--box-ban-height) - calc(2 * var(--border-width)));
|
||||
height: calc(var(--box-ban-height) - calc(2 * var(--border-width)));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: calc(100% + calc(2 * var(--ban-image-crop)));
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: calc(-1 * var(--ban-image-crop)) calc(-1 * var(--ban-image-crop) + 1px);
|
||||
}
|
||||
|
||||
.BanImage.Active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
animation: pulsate-ban 1.5s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.Spacing {
|
||||
width: var(--box-ban-spacer);
|
||||
}
|
||||
}
|
||||
|
||||
&.TeamBlue .BansBox {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.BansWrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.BansBox {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes pulsate-ban {
|
||||
0% {
|
||||
opacity: 1; // 0.7
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1; // 0.7
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap");
|
||||
|
||||
body {
|
||||
font-family: "Lato", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@import "./fonts";
|
||||
|
||||
body {
|
||||
// background: black;
|
||||
// font-family: 'Nova Flat', sans-serif;
|
||||
font-family: Lato, Arial, Helvetica, sans-serif;
|
||||
|
||||
// background: url("https://dotesports-media.nyc3.cdn.digitaloceanspaces.com/wp-content/uploads/2019/06/14131635/LEC-Summer-2019.jpg");
|
||||
background-size: cover;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@import "./variables";
|
||||
@import "./animation";
|
||||
|
||||
.Layout {
|
||||
.MiddleBox {
|
||||
position: absolute;
|
||||
color: white;
|
||||
width: var(--box-width);
|
||||
height: calc(var(--box-pick-height) + var(--box-ban-height));
|
||||
|
||||
bottom: var(--champselect-margin-bot);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Patch {
|
||||
height: var(--box-patch-height);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
/* border-left: darkgrey 1px solid;
|
||||
border-right: darkgrey 1px solid; */
|
||||
/* background-color: black; */
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.Logo {
|
||||
width: 100%;
|
||||
height: var(--box-logo-height);
|
||||
/* border-left: darkgrey 1px solid;
|
||||
border-right: darkgrey 1px solid; */
|
||||
/* background-color: black; */
|
||||
|
||||
z-index: 10;
|
||||
|
||||
img {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
margin: 0 auto;
|
||||
padding-top: 10%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./timer";
|
||||
@import "./team";
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
.Picks {
|
||||
width: 100%;
|
||||
height: var(--box-pick-height);
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.Pick {
|
||||
position: relative;
|
||||
width: 20%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #0a0a0a;
|
||||
border-left: solid var(--border-between-color) 1px;
|
||||
|
||||
&.Active {
|
||||
.PlayerName {
|
||||
height: calc(2 * var(--pick-name-height)) !important;
|
||||
margin-top: calc(-1 * var(--pick-name-height));
|
||||
line-height: calc(2 * var(--pick-name-height));
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.PlayerName {
|
||||
transition: height 1s, font-size 1s, margin-top 1s, background-color 0.3s, color 0.3s;
|
||||
height: var(--pick-name-height);
|
||||
color: white;
|
||||
background-color: black;
|
||||
text-align: center;
|
||||
font-size: calc(0.5 * var(--pick-name-height));
|
||||
line-height: var(--pick-name-height);
|
||||
}
|
||||
|
||||
.SummonerSpells {
|
||||
position: absolute;
|
||||
top: calc(var(--box-pick-height) - 30px - var(--summoner-spell-size));
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
img {
|
||||
height: var(--summoner-spell-size);
|
||||
width: var(--summoner-spell-size);
|
||||
}
|
||||
}
|
||||
|
||||
.PickImage {
|
||||
position: relative;
|
||||
height: calc(100% - var(--pick-name-height));
|
||||
|
||||
img {
|
||||
width: calc(100% + calc(2 * var(--pick-image-crop)));
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: calc(-1 * var(--pick-image-crop)) calc(-1 * var(--pick-image-crop));
|
||||
}
|
||||
|
||||
&.Active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
// height: calc(100% - var(--pick-name-height));
|
||||
height: calc(100% + var(--pick-name-height));
|
||||
opacity: 0.5;
|
||||
animation: pulsate-pick 1.5s infinite;
|
||||
transition: height 1s;
|
||||
|
||||
// z-index: 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.TeamBlue .Picks .Pick {
|
||||
&:last-child {
|
||||
border-right: solid var(--border-between-color) 1px;
|
||||
}
|
||||
&:first-child {
|
||||
border: none;
|
||||
}
|
||||
&.Active {
|
||||
.PlayerName {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
.PlayerName::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.1;
|
||||
background-color: var(--color-blue);
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.TeamRed .Picks .Pick {
|
||||
&:first-child {
|
||||
border-left: solid var(--border-between-color) 1px;
|
||||
}
|
||||
&.Active {
|
||||
.PlayerName {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
.PlayerName::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.1;
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulsate-pick {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
.Team {
|
||||
position: absolute;
|
||||
width: var(--box-team-width);
|
||||
height: var(--box-height);
|
||||
|
||||
bottom: var(--champselect-margin-bot);
|
||||
|
||||
.TeamName {
|
||||
color: white;
|
||||
font-family: "Lato", Arial, Helvetica, sans-serif;
|
||||
font-size: 40px;
|
||||
margin-top: 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.TeamScore {
|
||||
font-size: 60px;
|
||||
margin-top: -20px;
|
||||
color: white;
|
||||
font-family: "Lato", Arial, Helvetica, sans-serif;
|
||||
width: var(--box-ban-height);
|
||||
text-align: center;
|
||||
height: calc(100% + 23px);
|
||||
}
|
||||
|
||||
@import "./picks";
|
||||
@import "./bans";
|
||||
|
||||
&.TeamBlue {
|
||||
right: calc(50% + calc(var(--box-width) / 2));
|
||||
|
||||
.TeamScore {
|
||||
border-right: 3px solid black;
|
||||
}
|
||||
|
||||
.Bans {
|
||||
justify-content: flex-end;
|
||||
background-color: var(--color-blue);
|
||||
border: solid var(--color-blue) var(--border-width);
|
||||
float: right;
|
||||
|
||||
.Ban {
|
||||
border-left: solid var(--color-blue) var(--border-width);
|
||||
|
||||
.BanImage.Active::before {
|
||||
background-image: linear-gradient(to left, var(--color-blue) 0%, rgba(0, 0, 0, 0) 50%, var(--color-blue) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Picks {
|
||||
.Pick {
|
||||
.PickImage.Active::before {
|
||||
background-image: linear-gradient(to left, var(--color-blue) 0%, rgba(0, 0, 0, 0) 50%, var(--color-blue) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TeamName {
|
||||
margin-right: auto;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&.TeamRed {
|
||||
left: calc(50% + calc(var(--box-width) / 2));
|
||||
|
||||
.TeamScore {
|
||||
border-left: 3px solid black;
|
||||
}
|
||||
|
||||
.Bans {
|
||||
background-color: var(--color-red);
|
||||
border: solid var(--color-red) var(--border-width);
|
||||
float: left;
|
||||
|
||||
.Ban {
|
||||
border-right: solid var(--color-red) var(--border-width);
|
||||
|
||||
.BanImage.Active::before {
|
||||
background-image: linear-gradient(to left, var(--color-red) 0%, rgba(0, 0, 0, 0) 50%, var(--color-red) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Picks {
|
||||
.Pick {
|
||||
.PickImage.Active::before {
|
||||
background-image: linear-gradient(to left, var(--color-red) 0%, rgba(0, 0, 0, 0) 50%, var(--color-red) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.TeamName {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.Timer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
// height: var(--box-timer-height);
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
height: 338px;
|
||||
margin-top: -260px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
.Background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
transition: left 1s;
|
||||
|
||||
&.Blue {
|
||||
background: linear-gradient(to left, rgba(0, 0, 0, 0) 0%, var(--color-blue) 100%);
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
&.Red {
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, var(--color-red) 100%);
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.Blue {
|
||||
.Background.Blue {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.Red {
|
||||
.Background.Red {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.Both {
|
||||
background: linear-gradient(to right, var(--color-blue) 0%, var(--color-red) 100%);
|
||||
}
|
||||
|
||||
.TimerChars {
|
||||
font-size: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: 260px;
|
||||
|
||||
.TimerChar {
|
||||
width: 30px;
|
||||
margin: 0 2px;
|
||||
text-align: right;
|
||||
|
||||
&:nth-child(2) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
--box-width: 160px;
|
||||
--box-height: calc(var(--box-pick-height) + var(--box-ban-height));
|
||||
--box-logo-height: calc(100% - calc(var(--box-ban-height) + var(--box-patch-height)));
|
||||
--box-timer-height: var(--box-ban-height);
|
||||
--box-patch-height: 40px;
|
||||
|
||||
--box-team-width: calc(5 * var(--box-width));
|
||||
--box-pick-height: 260px;
|
||||
--box-ban-height: 59px;
|
||||
--box-ban-spacer: calc(var(--box-ban-height) / 2);
|
||||
|
||||
--pick-name-height: 30px;
|
||||
--pick-image-crop: 8px;
|
||||
|
||||
--ban-image-crop: 6px;
|
||||
--summoner-spell-size: 40px;
|
||||
|
||||
--champselect-margin-bot: 50px;
|
||||
|
||||
--color-red: rgb(222,40,70);
|
||||
--color-blue: rgb(0,151,196);
|
||||
|
||||
--border-between-color: darkgray;
|
||||
--border-width: 3px;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import banImg from "./assets/ban_placeholder.svg";
|
||||
import topSplash from "./assets/top_splash_placeholder.svg";
|
||||
import jungSplash from "./assets/jung_splash_placeholder.svg";
|
||||
import midSplash from "./assets/mid_splash_placeholder.svg";
|
||||
import botSplash from "./assets/bot_splash_placeholder.svg";
|
||||
import supSplash from "./assets/sup_splash_placeholder.svg";
|
||||
|
||||
const pickSplashes = [topSplash, jungSplash, midSplash, botSplash, supSplash];
|
||||
|
||||
const makeUrlAbsolute = (url, backendUrl) => {
|
||||
if (!url || !url.startsWith('/cache')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const httpBackendUrl = backendUrl.replace('ws://', 'http://').replace('wss://', 'https://');
|
||||
const components = httpBackendUrl.split('/')
|
||||
|
||||
return components[0] + '//' + components[2] + url;
|
||||
};
|
||||
|
||||
const putPlaceholders = (team, backendUrl) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// Picks
|
||||
// Check if exists
|
||||
if (i >= team.picks.length) {
|
||||
// Does not exists, push
|
||||
team.picks.push({
|
||||
champion: {
|
||||
loadingImg: pickSplashes[i]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Exists, check!
|
||||
const pick = team.picks[i];
|
||||
if (!pick.champion || !pick.champion.loadingImg) {
|
||||
pick.champion = {
|
||||
loadingImg: pickSplashes[i]
|
||||
};
|
||||
// pick.spell1 = null;
|
||||
// pick.spell2 = null;
|
||||
}
|
||||
|
||||
if (pick.spell1) {
|
||||
pick.spell1.icon = makeUrlAbsolute(pick.spell1.icon, backendUrl);
|
||||
}
|
||||
if (pick.spell2) {
|
||||
pick.spell2.icon = makeUrlAbsolute(pick.spell2.icon, backendUrl);
|
||||
}
|
||||
pick.champion.loadingImg = makeUrlAbsolute(pick.champion.loadingImg, backendUrl);
|
||||
pick.champion.splashImg = makeUrlAbsolute(pick.champion.splashImg, backendUrl);
|
||||
pick.champion.squareImg = makeUrlAbsolute(pick.champion.squareImg, backendUrl);
|
||||
}
|
||||
|
||||
// Bans
|
||||
if (i >= team.bans.length) {
|
||||
// Does not exist
|
||||
team.bans.push({
|
||||
champion: {
|
||||
squareImg: banImg
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const ban = team.bans[i];
|
||||
if (!ban.champion || !ban.champion.squareImg) {
|
||||
ban.champion = {
|
||||
squareImg: banImg
|
||||
}
|
||||
}
|
||||
|
||||
ban.champion.squareImg = makeUrlAbsolute(ban.champion.squareImg, backendUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const convertState = (state, backendUrl) => {
|
||||
if (Object.keys(state).length !== 0) {
|
||||
putPlaceholders(state.blueTeam, backendUrl);
|
||||
putPlaceholders(state.redTeam, backendUrl);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export default convertState;
|
||||
@@ -0,0 +1,16 @@
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.infoBox {
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: red;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
const rootDomElement = document.querySelector("#root");
|
||||
const reactRoot = ReactDOM.createRoot(rootDomElement);
|
||||
reactRoot.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "lol-cup-pick-ban-ui",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:zsl-gdansk/lol-cup-pick-ban-ui.git",
|
||||
"author": "GRZANA <grzanatv@gmail.com>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"turbo": "^1.7.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"apps/**"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "turbo run start --no-cache --parallel --continue"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"dataPoints":[{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:36.595Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:37.593Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:38.596Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:39.596Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:40.596Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:41.595Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:42.596Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:43.596Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:44.597Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:45.598Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:46.598Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:47.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:48.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:49.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:50.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:51.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:52.601Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:53.600Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:54.602Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:55.602Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:56.602Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:57.603Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:58.603Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:11:59.604Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:12:00.603Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:12:01.605Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:12:02.605Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2022-02-04T15:12:03.605Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"imLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"dataPoints":[{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-10-27T08:15:18.012Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"c1~e3abfb6296bfa0b243ee78e9d78989de297f48a7@sec.pvp.net","chatRoomPassword":"7L3FpmOA8bQyprYr"},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":false,"localPlayerCellId":0,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":4,"spell2Id":21,"summonerId":76838801,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[],"timer":{"adjustedTimeLeftInPhase":27811,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1572164118872,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30811,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30841},"trades":[]},"time":"2019-10-27T08:15:19.016Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"c1~e3abfb6296bfa0b243ee78e9d78989de297f48a7@sec.pvp.net","chatRoomPassword":"7L3FpmOA8bQyprYr"},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":false,"localPlayerCellId":0,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":4,"spell2Id":21,"summonerId":76838801,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[],"timer":{"adjustedTimeLeftInPhase":27811,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1572164118872,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30811,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30841},"trades":[]},"time":"2019-10-27T08:15:20.012Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"c1~e3abfb6296bfa0b243ee78e9d78989de297f48a7@sec.pvp.net","chatRoomPassword":"7L3FpmOA8bQyprYr"},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":false,"localPlayerCellId":0,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":4,"spell2Id":21,"summonerId":76838801,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[],"timer":{"adjustedTimeLeftInPhase":27811,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1572164118872,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30811,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30841},"trades":[]},"time":"2019-10-27T08:15:21.013Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"c1~e3abfb6296bfa0b243ee78e9d78989de297f48a7@sec.pvp.net","chatRoomPassword":"7L3FpmOA8bQyprYr"},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":false,"localPlayerCellId":0,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":4,"spell2Id":21,"summonerId":76838801,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[],"timer":{"adjustedTimeLeftInPhase":27811,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1572164118872,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30811,"timeLeftInPhaseInSecLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"dataPoints":[{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-10-30T19:20:14.602Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":24234331,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":33990379,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":2,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":29228531,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":3,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":21677707,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":4,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":22894242,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":5,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":20223425,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":6,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":20461267,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":7,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":21056888,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":8,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":19745911,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":9,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":77621355,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27720,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1572463215071,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30720,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30720},"trades":[{"cellId":0,"id":100,"state":"INVALID"},{"cellId":1,"id":101,"state":"INVALID"},{"cellId":2,"id":102,"state":"INVALID"},{"cellId":3,"id":103,"state":"INVALID"},{"cellId":4,"id":104,"state":"INVALID"}]},"time":"2019-10-30T19:20:15.604Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":24234331,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":33990379,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":2,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":29228531,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":3,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":21677707,"team":1,"wardSkinId":-1},{"assignedPosition":"","cellId":4,"championId":0,"championPickIntent":0,"entitledFeatureType":"","plaLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"dataPoints":[{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:38.495Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:39.496Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:40.496Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:41.497Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:42.497Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:43.497Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:44.495Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:45.500Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:46.498Z"},{"isChampSelectActive":false,"session":{"errorCode":"RPC_ERROR","httpStatus":404,"implementationDetails":{},"message":"No active delegate"},"time":"2019-12-04T20:02:47.496Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":41688955,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":24234331,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":2,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":47947874,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27857,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1575489767930,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30857,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30857},"trades":[{"cellId":0,"id":100,"state":"INVALID"}]},"time":"2019-12-04T20:02:48.501Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":41688955,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":24234331,"team":2,"wardSkinId":-1},{"assignedPosition":"","cellId":2,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":47947874,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27857,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1575489767930,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30857,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30857},"trades":[{"cellId":0,"id":100,"state":"INVALID"}]},"time":"2019-12-04T20:02:49.499Z"},{"isChLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"dataPoints":[{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160506,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160501,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27265,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1573067976082,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30265,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30265},"trades":[{"cellId":0,"id":100,"state":"INVALID"}]},"time":"2019-11-06T19:19:36.085Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160506,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160501,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27265,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1573067976082,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30265,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30265},"trades":[{"cellId":0,"id":100,"state":"INVALID"}]},"time":"2019-11-06T19:19:36.928Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160506,"team":1,"wardSkinId":-1}],"rerollsRemaining":0,"skipChampionSelect":false,"theirTeam":[{"assignedPosition":"","cellId":1,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":1160501,"team":2,"wardSkinId":-1}],"timer":{"adjustedTimeLeftInPhase":27265,"adjustedTimeLeftInPhaseInSec":27,"internalNowInEpochMs":1573067976082,"isInfinite":false,"phase":"BAN_PICK","timeLeftInPhase":30265,"timeLeftInPhaseInSec":30,"totalTimeInPhase":30265},"trades":[{"cellId":0,"id":100,"state":"INVALID"}]},"time":"2019-11-06T19:19:37.928Z"},{"isChampSelectActive":true,"session":{"actions":[[{"actorCellId":0,"championId":0,"completed":false,"id":1,"pickTurn":1,"type":"ban"}]],"allowBattleBoost":false,"allowDuplicatePicks":false,"allowLockedEvents":false,"allowRerolling":false,"allowSkinSelection":true,"bans":{"myTeamBans":[],"numBans":10,"theirTeamBans":[]},"benchChampionIds":[],"benchEnabled":false,"boostableSkinCount":1,"chatDetails":{"chatRoomName":"","chatRoomPassword":null},"counter":-1,"entitledFeatureState":{"additionalRerolls":0,"unlockedSkinIds":[]},"isSpectating":true,"localPlayerCellId":-1,"lockedEventIndex":-1,"myTeam":[{"assignedPosition":"","cellId":0,"championId":0,"championPickIntent":0,"entitledFeatureType":"","playerType":"PLAYER","selectedSkinId":0,"spell1Id":1,"spell2Id":3,"summonerId":11605Line truncated
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://turborepo.org/schema.json",
|
||||
"pipeline": {
|
||||
"start": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||