Initial commit

This includes changes that were made in the forked version. Commit history from that time is not accessible.
This commit is contained in:
mmacierzanka committed 2023-01-25 19:12:39 +01:00
commit 3e62256988
94 files changed
+14389

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.idea/
backend-build/
cache/
+21
View File
@@ -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.
+42
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
+16
View File
@@ -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"]
},
};
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
+34
View File
@@ -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);
};
+17
View File
@@ -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();
+30
View File
@@ -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;
+68
View File
@@ -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();
+19
View File
@@ -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"
}
+11
View File
@@ -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;
}
}
+26
View File
@@ -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;
}
}
+179
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
import logger, { setLogLevel } from './logger';
export default logger;
export { setLogLevel };
+45
View File
@@ -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 };
+41
View File
@@ -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"
}
}
+52
View File
@@ -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;
}
}
+87
View File
@@ -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);
}
}
}
+107
View File
@@ -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;
+142
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
import State from './State';
export default State;
+64
View File
@@ -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. */
}
}
+3
View File
@@ -0,0 +1,3 @@
export interface Actionable {
isActive: boolean;
}
+6
View File
@@ -0,0 +1,6 @@
import { Actionable, Champion } from '.';
export class Ban implements Actionable {
champion = new Champion();
isActive = false;
}
+10
View File
@@ -0,0 +1,10 @@
export class Champion {
id = 0;
name = '';
key? = '';
splashImg = '';
splashCenteredImg = '';
loadingImg = '';
squareImg = '';
idName = '';
}
+4
View File
@@ -0,0 +1,4 @@
export class Config {
contentPatch = 'latest';
contentCdn = 'https://ddragon.leagueoflegends.com/cdn';
}
+6
View File
@@ -0,0 +1,6 @@
import { Version } from '.';
export class Meta {
cdn = '';
version = new Version();
}
+13
View File
@@ -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;
}
}
+5
View File
@@ -0,0 +1,5 @@
export class Spell {
id = 0;
key?: string;
icon = '';
}
+68
View File
@@ -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],
};
}
}
+7
View File
@@ -0,0 +1,7 @@
import { Ban, Pick, Actionable } from '.';
export class Team implements Actionable {
bans: Array<Ban> = [];
picks: Array<Pick> = [];
isActive = false;
}
+4
View File
@@ -0,0 +1,4 @@
export class Version {
champion = '';
item = '';
}
+10
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
export default interface PBEvent {
eventType: string;
}
+8
View File
@@ -0,0 +1,8 @@
import { ActionType } from '.';
export class Action {
completed!: boolean;
championId!: number;
type!: ActionType;
actorCellId!: number;
}
+4
View File
@@ -0,0 +1,4 @@
export enum ActionType {
PICK = 'pick',
BAN = 'ban'
}
+7
View File
@@ -0,0 +1,7 @@
export class Cell {
cellId!: number;
championId!: number;
summonerId!: number;
spell1Id!: number;
spell2Id!: number;
}
+8
View File
@@ -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();
}
+4
View File
@@ -0,0 +1,4 @@
export class Summoner {
displayName = '';
summonerId = 0;
}
+9
View File
@@ -0,0 +1,9 @@
export class Timer {
adjustedTimeLeftInPhase = 0;
adjustedTimeLeftInPhaseInSec = 0;
internalNowInEpochMs = 0;
phase = '';
timeLeftInPhase = 0;
timeLeftInPhaseInSec = 0;
totalTimeInPhase = 0;
}
+6
View File
@@ -0,0 +1,6 @@
export * from './Action';
export * from './ActionType';
export * from './Cell';
export * from './Session';
export * from './Summoner';
export * from './Timer';
+80
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
import websocket from './WebSocketServer';
export default websocket;
+1
View File
@@ -0,0 +1 @@
BROWSER=none
+23
View File
@@ -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*
+33
View File
@@ -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"
]
}
}
+115
View File
@@ -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;
+14
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+71
View File
@@ -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;
+15
View File
@@ -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;
}
+16
View File
@@ -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

Binary file not shown.

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

+16
View File
@@ -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;
+113
View File
@@ -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>
)
}
}
+23
View File
@@ -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";
}
+147
View File
@@ -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;
}
}
+100
View File
@@ -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;
}
+83
View File
@@ -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;
+16
View File
@@ -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;
}
+12
View File
@@ -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>
);
+18
View File
@@ -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"
}
}
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"start": {
"cache": false
}
}
}
+11395
View File
File diff suppressed because it is too large. Load diff