1 Commits
Author SHA1 Message Date
legonzaur 9f596b9f50 'publish' 2023-06-01 10:36:49 +02:00
126 changed files with 51 additions and 15684 deletions
-4
View File
@@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11
-5
View File
@@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
-25
View File
@@ -1,25 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
dist
-34
View File
@@ -1,34 +0,0 @@
# toneapi_webclient
Tone is an API that allows Northstar players to track their kill statistics : https://toneapi.github.io/ToneAPI_webclient/
If you want your server to appear on this website, you can download the server side mod from [here](https://github.com/ToneAPI/ToneAPI_servermod)
The server side mod will send kills data to Tone API, which will then process them.
You will have to set a personal token for the mod to work, please ask me for one on Discord
Alternatively, you can selfhost your own backend [here](https://github.com/ToneAPI/ToneAPI_backend)
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+1
View File
@@ -0,0 +1 @@
.vlist[data-v-d1645486]{overflow-y:auto}.vlist[data-v-d1645486],.vscroll[data-v-d1645486]{height:calc(100% - 3em)}.inactive[data-v-12531736],.offline[data-v-12531736]{color:var(--comment)}.serverView[data-v-12531736]{margin:0 1rem 1rem 1rem;overflow:auto}.serverList[data-v-12531736]{overflow-y:auto;height:100%}.serverHeaders[data-v-12531736],.serverRow[data-v-12531736]{box-sizing:border-box;display:grid;grid-template-columns:4ch 178px 20ch 8ch 8ch 8ch 1fr;background:var(--bg-color);text-align:left;cursor:pointer;margin:0;transition:border .25s;height:100px}.serverHeaders[data-v-12531736]{height:3em;background:var(--current-line)}.serverHeaders>span[data-v-12531736]{padding:.5em .8em 0 .25em}.serverRow.odd[data-v-12531736]{background:var(--accent)}.serverRow.uneven[data-v-12531736]{background:var(--bg-color)}.serverRow[data-v-12531736]:hover{background:var(--current-line)}.serverRow>div[data-v-12531736]{padding:.5em .8em 0 .25em}.serverRow>div[data-v-12531736]:not(:last-child){border-right:solid var(--bg-color) 2px}img[data-v-12531736]{-o-object-fit:contain;object-fit:contain;height:100%}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
:root{--bg-color:#282a36;--accent:#353744;--current-line:#44475a;--comment:#6272a4;--foreground:#f8f8f2;--orange:#ffb86c;--cyan:#8be9fd;--green:#50fa7b;--pink:#ff79c6;--purple:#bd93f9;--red:#f55;--yellow:#f1fa8c}body{background:var(--bg-color);margin:0}#app{height:100vh;width:100vw;font-family:Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:var(--foreground);display:grid;grid-template-areas:"header header" "filters body " "stats body " "footer body ";grid-template-columns:1fr 5fr;grid-template-rows:auto 1fr auto auto}@media only screen and (max-width:922px){#app{grid-template-areas:"header header" "filters filters" "body body ";grid-template-rows:auto auto 1fr}}@media only screen and (max-width:600px){.github-link{display:none}}header{grid-area:header;height:64px;background:var(--bg-color);color:var(--foreground)}nav{display:flex;flex-direction:row;text-align:center;vertical-align:middle}nav a{font-weight:700;color:var(--foreground)}nav a.router-link-exact-active{color:var(--orange)}.navbar-logo img{height:48px;padding:8px 24px}.navbar-element{text-align:center;line-height:16px;padding:24px 32px}.navbar-element:hover,.navbar-logo:hover{background:var(--current-line)}.spacer{width:100%}
+1
View File
@@ -0,0 +1 @@
:root{--bg-color:#282a36;--accent:#353744;--current-line:#44475a;--comment:#6272a4;--foreground:#f8f8f2;--orange:#ffb86c;--cyan:#8be9fd;--green:#50fa7b;--pink:#ff79c6;--purple:#bd93f9;--red:#f55;--yellow:#f1fa8c}body{background:var(--bg-color);margin:0}#app{height:100vh;width:100vw;font-family:Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:var(--foreground);display:grid;grid-template-areas:"header header" "filters body " "stats body " "footer body ";grid-template-columns:1fr 5fr;grid-template-rows:auto 1fr auto auto}@media only screen and (max-width:922px){#app{grid-template-areas:"header header" "filters filters" "body body ";grid-template-rows:auto auto 1fr}}@media only screen and (max-width:600px){.github-link{display:none}}header{grid-area:header;height:64px;background:var(--bg-color);color:var(--foreground)}nav{display:flex;flex-direction:row;text-align:center;vertical-align:middle}nav a{font-weight:700;color:var(--foreground)}nav a.router-link-exact-active{color:var(--orange)}.navbar-logo img{height:48px;padding:8px 24px}.navbar-element{text-align:center;line-height:16px;padding:24px 32px}.navbar-element:hover,.navbar-logo:hover{background:var(--current-line)}.spacer{width:100%}
View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><meta content="Players : Tone API" property="og:title"/><meta content="Northstar players kills statistics" property="og:description"/><meta content="https://toneapi.github.io/ToneAPI_webclient/" property="og:url"/><meta content="https://toneapi.github.io/ToneAPI_webclient/og_image.png" property="og:image"/><meta name="twitter:card" content="summary_large_image"><link rel="icon" href="/ToneAPI_webclient/favicon.ico"><title>Players : Tone API</title><script defer="defer" src="/ToneAPI_webclient/js/chunk-vendors.78b0bc71.js"></script><script defer="defer" src="/ToneAPI_webclient/js/index.ecca6261.js"></script><link href="/ToneAPI_webclient/css/index.85c03c07.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but Players : Tone API doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
+1
View File
@@ -0,0 +1 @@
"use strict";(self["webpackChunktoneapi_webclient"]=self["webpackChunktoneapi_webclient"]||[]).push([[855],{2677:function(e,t,s){s.d(t,{Z:function(){return v}});var i=s(3396),r=s(7139);const n={ref:"vscroll",class:"vscroll"};function a(e,t,s,a,l,o){return(0,i.wg)(),(0,i.iD)("div",{ref:"vlist",class:"vlist",onScroll:t[0]||(t[0]=(...t)=>e.scroll&&e.scroll(...t))},[(0,i._)("div",n,[(0,i._)("div",{style:(0,r.j5)(`padding: ${e.interval[0]*e.$props.rowHeight}px 0px ${Math.max(0,e.list.length*e.$props.rowHeight-e.interval[1]*e.$props.rowHeight)}px;`)},[((0,i.wg)(!0),(0,i.iD)(i.HY,null,(0,i.Ko)(e.visibleList,((t,s)=>(0,i.WI)(e.$slots,"default",{data:t,index:s+e.interval[0]},void 0,!0))),256))],4)],512)],544)}var l=(0,i.aZ)({data(){return{toLoad:10,vlistHeight:10,vIndex:0}},props:{list:{type:Array,default:()=>[]},rowHeight:{type:Number,default:()=>32},highlighted:String},mounted(){this.$props.list&&(this.vlistHeight=this.$refs.vlist?.getBoundingClientRect().height||0,this.toLoad=3*this.visibleCount,this.$props.highlighted&&setTimeout((()=>this.$refs.vlist?.scrollBy({top:this.list.findIndex((e=>e.id===this.$props.highlighted))*this.rowHeight-this.$refs.vlist?.scrollTop-(this.vlistHeight/2-this.$props.rowHeight),behavior:"smooth"})),1))},computed:{interval(){return[Math.max(Math.min(this.list.length-this.toLoad,this.vIndex),0),Math.min(this.vIndex+this.toLoad,this.list.length)]},visibleList(){return this.list.slice(this.interval[0],this.interval[1])},visibleCount(){return Math.ceil(this.vlistHeight/this.$props.rowHeight)}},watch:{list(){this.vlistHeight=this.$refs.vlist?.getBoundingClientRect().height||0},highlighted(e){e&&setTimeout((()=>this.$refs.vlist?.scrollBy({top:this.list.findIndex((t=>t.id===e))*this.rowHeight-this.$refs.vlist?.scrollTop-(this.vlistHeight/2-this.$props.rowHeight),behavior:"smooth"})),1)}},methods:{scroll(e){const t=e.target?.scrollTop,s=Math.floor(t/this.$props.rowHeight),i=s<this.vIndex,r=s>this.vIndex+this.visibleCount;if(i?this.vIndex=Math.max(s-this.visibleCount,0):r&&(this.vIndex=Math.min(s,this.list.length-this.toLoad)),r||i){}}}}),o=s(89);const h=(0,o.Z)(l,[["render",a],["__scopeId","data-v-d1645486"]]);var v=h},9855:function(e,t,s){s.r(t),s.d(t,{default:function(){return w}});var i=s(3396),r=s(7139);const n=e=>((0,i.dD)("data-v-12531736"),e=e(),(0,i.Cn)(),e),a=n((()=>(0,i._)("div",{id:"filters"},null,-1))),l={class:"serverView"},o={class:"serverList"},h=n((()=>(0,i._)("div",{class:"serverHeaders"},[(0,i._)("span"),(0,i._)("span",null,"Map"),(0,i._)("span",null,"Server name"),(0,i._)("span",null,"Players"),(0,i._)("span",null,"Kills"),(0,i._)("span",null,"Max distance"),(0,i._)("span",null,"Average distance")],-1))),v=["src"],d={key:1};function u(e,t,s,n,u,p){const c=(0,i.up)("VirtualList");return(0,i.wg)(),(0,i.iD)(i.HY,null,[a,(0,i._)("div",l,[(0,i._)("div",o,[h,(0,i.Wm)(c,{list:e.serverList,"row-height":100},{default:(0,i.w5)((t=>[(0,i._)("div",{class:(0,r.C_)("serverRow "+(e.nsServersByName[t.data.name]?"":"offline ")+(e.nsServersByName[t.data.name]?.playerCount?"":"inactive ")+((t.index+1)%2?"odd ":"uneven "))},[(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(t.index+1),1)]),e.nsServersByName[t.data.name]?((0,i.wg)(),(0,i.iD)("img",{key:0,src:`${e.publicPath}maps/${e.nsServersByName[t.data.name].map}_lobby.png`},null,8,v)):(0,i.kq)("",!0),e.nsServersByName[t.data.name]?(0,i.kq)("",!0):((0,i.wg)(),(0,i.iD)("div",d)),(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(t.data.name+(e.nsServersByName[t.data.name]?"":" (offline)")),1)]),(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(e.nsServersByName[t.data.name]?e.nsServersByName[t.data.name]?.playerCount+"/"+e.nsServersByName[t.data.name]?.maxPlayers:""),1)]),(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(t.data._value.deaths),1)]),(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(t.data._value.max_distance),1)]),(0,i._)("div",null,[(0,i._)("span",null,(0,r.zw)(Math.round(100*(t.data._value.total_distance/t.data._value.kills||0))/100),1)])],2)])),_:1},8,["list"])])])],64)}var p=s(7672),c=s(2677),m=s(4870),g=(0,i.aZ)({name:"ServerView",components:{VirtualList:c.Z},data(){return{store:(0,p.D)(),publicPath:"/ToneAPI_webclient/"}},computed:{servers(){const e=this.store.getServerList({})?.value.data;return e||(0,m.SU)(this.store.fetchServers({})).data},serverList(){const e=Object.entries(this.servers).map((e=>({...e[1],name:e[0]})));return e.sort(((e,t)=>{const s=this.nsServersByName[e.name]?this.nsServersByName[e.name].playerCount?1:0:-1,i=this.nsServersByName[t.name]?this.nsServersByName[t.name].playerCount?1:0:-1;return i-s})),e},nsServers(){const e=this.store.getNSServers;return e||this.store.fetchNSServers()},nsServersByName(){return Object.fromEntries(this.nsServers.map((e=>[e.name,e])))}}}),_=s(89);const f=(0,_.Z)(g,[["render",u],["__scopeId","data-v-12531736"]]);var w=f}}]);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

-13463
View File
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
{
"name": "toneapi_webclient",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"assert": "^2.0.0",
"chart.js": "^4.2.1",
"chartjs-plugin-annotation": "^2.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.8.3",
"pinia": "^2.0.35",
"vue": "^3.2.13",
"vue-chartjs": "^5.2.0",
"vue-class-component": "^8.0.0-0",
"vue-multiselect": "^3.0.0-beta.1",
"vue-router": "^4.0.3"
},
"devDependencies": {
"@types/assert": "^1.5.6",
"@types/node": "^18.16.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0",
"@vue/eslint-config-typescript": "^9.1.0",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^8.0.3",
"typescript": "~4.5.5"
}
}
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta content="Players : Tone API" property="og:title" />
<meta content="Northstar players kills statistics" property="og:description" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/" property="og:url" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/og_image.png" property="og:image" />
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta content="Servers : Tone API" property="og:title" />
<meta content="Northstar servers kills statistics" property="og:description" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/servers" property="og:url" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/server_og_image.png?" property="og:image" />
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
-4
View File
@@ -1,4 +0,0 @@
call npm run build
call git add dist -f
call git commit -m 'publish'
call git subtree push --prefix dist origin gh-pages

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 512 KiB

+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><meta content="Servers : Tone API" property="og:title"/><meta content="Northstar servers kills statistics" property="og:description"/><meta content="https://toneapi.github.io/ToneAPI_webclient/servers" property="og:url"/><meta content="https://toneapi.github.io/ToneAPI_webclient/server_og_image.png?" property="og:image"/><meta name="twitter:card" content="summary_large_image"><link rel="icon" href="/ToneAPI_webclient/favicon.ico"><title>Servers : Tone API</title><script defer="defer" src="/ToneAPI_webclient/js/chunk-vendors.78b0bc71.js"></script><script defer="defer" src="/ToneAPI_webclient/js/server.0510ec8b.js"></script><link href="/ToneAPI_webclient/css/server.85c03c07.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but Servers : Tone API doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
-7
View File
@@ -1,7 +0,0 @@
import { Ref } from '@vue/reactivity'
declare module '@vue/reactivity' {
export interface Ref<T> extends Ref<T> {
_rawValue: T
}
}
-125
View File
@@ -1,125 +0,0 @@
<template>
<header>
<nav>
<!-- <div class="navbar-element">hamburger</div> -->
<div class="navbar-logo"><a href="https://github.com/ToneAPI" target="_blank"><img :src="`${publicPath}tone_icon.png`"/></a></div>
<div class="spacer"></div>
<!-- <router-link to="/" class="navbar-element">Home</router-link>
<router-link to="/about" class="navbar-element">About</router-link> -->
<router-link to="/servers" class="navbar-element">Servers&nbsp;(Beta)</router-link>
<router-link to="/" class="navbar-element">Players</router-link>
</nav>
</header>
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
publicPath: process.env.BASE_URL
}
}
})
</script>
<style>
:root {
--bg-color: #282a36;
--accent: #353744;
--current-line: #44475a;
--comment: #6272a4;
--foreground: #f8f8f2;
--orange: #ffb86c;
--cyan: #8be9fd;
--green: #50fa7b;
--pink: #ff79c6;
--purple: #bd93f9;
--red: #ff5555;
--yellow: #f1fa8c;
}
body {
background: var(--bg-color);
margin: 0;
}
#app {
height: 100vh;
width: 100vw;
font-family: 'Helvetica', 'Arial', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: var(--foreground);
display: grid;
grid-template-areas:
'header header'
'filters body '
'stats body '
'footer body ';
grid-template-columns: 1fr 5fr;
grid-template-rows: auto 1fr auto auto;
}
@media only screen and (max-width: 922px) {
#app {
grid-template-areas:
'header header'
'filters filters'
'body body ';
grid-template-rows: auto auto 1fr;
}
}
@media only screen and (max-width: 600px) {
.github-link{
display:none;
}
}
header {
grid-area: header;
height: 64px;
background: var(--bg-color);
color: var(--foreground);
}
nav {
display: flex;
flex-direction: row;
text-align: center;
vertical-align: middle;
}
nav a {
font-weight: bold;
color: var(--foreground);
}
nav a.router-link-exact-active {
color: var(--orange);
}
.navbar-logo img{
height:48px;
padding:8px 24px;
}
.navbar-element {
text-align: center;
line-height: 16px;
padding: 24px 32px;
}
.navbar-element:hover, .navbar-logo:hover {
background: var(--current-line)
}
.spacer {
width: 100%;
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

-65
View File
@@ -1,65 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
-23
View File
@@ -1,23 +0,0 @@
<template>
<div >
test
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref, toRaw, unref } from 'vue'
export default defineComponent({
components: { },
props: {
},
emits: ['highlightPlayer'],
data () {
return { test: 1 }
}
})
</script>
<style scoped>
</style>
-88
View File
@@ -1,88 +0,0 @@
<template>
<div ref="vlist" class="vlist" @scroll="scroll">
<div ref="vscroll" class="vscroll">
<div :style="`padding: ${(interval[0]*$props.rowHeight!)}px 0px ${Math.max(0,(((list.length)*$props.rowHeight!)-(interval[1])*$props.rowHeight!))}px;`">
<slot v-for="(data, index) in visibleList" :data="data" :index="index+interval[0]"></slot>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
data () {
return {
toLoad: 10,
vlistHeight: 10,
vIndex: 0
}
},
props: {
list: { type: Array as PropType<any[]>, default: () => [] },
rowHeight: { type: Number, default: () => 32 },
highlighted: String
},
mounted () {
if (this.$props.list) {
this.vlistHeight = (this.$refs.vlist as HTMLElement)?.getBoundingClientRect().height || 0
this.toLoad = this.visibleCount * 3
if (this.$props.highlighted) {
setTimeout(() => (this.$refs.vlist as HTMLElement)?.scrollBy({ top: ((this.list.findIndex(e => e.id === this.$props.highlighted)) * this.rowHeight) - ((this.$refs.vlist as HTMLElement))?.scrollTop - (this.vlistHeight / 2 - this.$props.rowHeight), behavior: 'smooth' }), 1)
}
}
},
computed: {
interval () {
// console.log(this.list.length, this.vIndex + this.toLoad)
return [Math.max(Math.min(this.list.length - this.toLoad, this.vIndex), 0), Math.min(this.vIndex + this.toLoad, this.list.length)]
},
visibleList () {
return this.list.slice(this.interval[0], this.interval[1])
},
visibleCount () {
return Math.ceil(this.vlistHeight / this.$props.rowHeight)
}
},
watch: {
list () {
this.vlistHeight = (this.$refs.vlist as HTMLElement)?.getBoundingClientRect().height || 0
},
highlighted (newVal) {
if (newVal) setTimeout(() => (this.$refs.vlist as HTMLElement)?.scrollBy({ top: ((this.list.findIndex(e => e.id === newVal)) * this.rowHeight) - ((this.$refs.vlist as HTMLElement))?.scrollTop - (this.vlistHeight / 2 - this.$props.rowHeight), behavior: 'smooth' }), 1)
}
},
methods: {
scroll (e:Event) {
const scrollTop = (e.target as HTMLElement)?.scrollTop
const scrollIndex = Math.floor(scrollTop / this.$props.rowHeight)
const mustScrollUp = scrollIndex < this.vIndex
const mustScrollDown = scrollIndex > this.vIndex + this.visibleCount
// console.log(this.vIndex + this.visibleCount, scrollIndex + this.visibleCount)
if (mustScrollUp) {
this.vIndex = Math.max(scrollIndex - this.visibleCount, 0)
} else if (mustScrollDown) {
this.vIndex = Math.min(scrollIndex, this.list.length - this.toLoad)
}
if (mustScrollDown || mustScrollUp) {
const msg = (mustScrollDown ? 'down ' : '') + (mustScrollUp ? 'up ' : '')
// console.log(this.toLoad)
// console.log((this.list.length * this.$props.rowHeight) - (this.vIndex * this.$props.rowHeight) - (this.visibleCount * this.$props.rowHeight))
}
}
}
})
</script>
<style scoped>
.vlist{
overflow-y:auto;
}
.vlist, .vscroll {
height: calc(100% - 3em);
}
</style>
-25
View File
@@ -1,25 +0,0 @@
<template>
<div class="loadingComponent">
<progress class="loadingBar" max="1" :value="value"></progress>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
value: Number
}
})
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.loadingComponent {
display:flex;
align-items: center;
justify-content: center;
height: calc(100% - 3em);
}
</style>
-247
View File
@@ -1,247 +0,0 @@
<template>
<div class="playerChart" ref="container">
<LoadingBar v-if="progress!==1" :value="progress"></LoadingBar>
<Scatter :data="chart" :options="chartOptions" v-if="progress===1" />
</div>
</template>
<script lang="ts">
import {
Chart as ChartJS,
LinearScale,
PointElement,
LineElement,
Tooltip,
Legend,
CoreChartOptions,
PluginChartOptions,
ScaleChartOptions
} from 'chart.js'
import annotationPlugin, { AnnotationPluginOptions } from 'chartjs-plugin-annotation'
import dataLabel from 'chartjs-plugin-datalabels'
import { useKillStore, Player, Filters } from '@/stores/kill'
import { Scatter } from 'vue-chartjs'
import { defineComponent, PropType, Ref, toRaw, triggerRef, unref } from 'vue'
import { ChartEvent } from 'chart.js/dist/core/core.plugins'
import { _DeepPartialObject } from 'chart.js/dist/types/utils'
import LoadingBar from './LoadingBar.vue'
ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annotationPlugin, dataLabel)
export default defineComponent({
name: 'PlayerChart',
props: {
filters: Object as PropType<Filters>,
playerHighlighted: String
},
data: () => {
return {
store: useKillStore(),
refreshColors: 0,
playerData: []
}
},
emits: ['highlightPlayer'],
components: {
Scatter, LoadingBar
},
mounted () {
this.refreshColors++
},
computed: {
progress () {
if (this.filters) {
const { player: _, ...withoutPlayer } = this.filters
return this.store.getPlayerList(withoutPlayer)?.value.progress
}
return 0
},
playerList (): (Player & {id:string})[] {
const data = this.store.getPlayerList(this.filters || {})?.value.data
if (!data) return Object.entries(this.store.fetchPlayers(this.filters || {}).value.data).map(e => ({ id: e[0], ...toRaw(e[1].value) }))
const cut = Object.entries(data).map(e => ({ id: e[0], ...toRaw(e[1].value) }))
cut.sort((a, b) => {
return b.kills - a.kills
})
return cut.slice(0, 200)
},
colors () {
// eslint-disable-next-line no-unused-expressions
this.refreshColors
const colors = {} as { [key: string]: string }
if (this.$refs?.container) {
const style = getComputedStyle(this.$refs.container as Element)
colors.fg = style.getPropertyValue('--foreground')
colors.bg = style.getPropertyValue('--bg-color')
colors.orange = style.getPropertyValue('--orange')
colors.cyan = style.getPropertyValue('--cyan')
colors.currentLine = style.getPropertyValue('--current-line')
} else {
colors.fg = '#ffffff'
}
return colors
},
chart () {
const playerIndex = this.playerList.findIndex(e => e.id === this.$props.playerHighlighted)
const defaultColor = this.colors.cyan
const borderColor = new Array(this.playerList.length).fill(defaultColor)
borderColor[playerIndex] = this.colors.orange
const backgroundColor = borderColor
const pointRadius = new Array(this.playerList.length).fill(1)
pointRadius[playerIndex] = 20
const hoverRadius = new Array(this.playerList.length).fill(4)
hoverRadius[playerIndex] = 30
const pointStyle = new Array(this.playerList.length).fill('circle')
pointStyle[playerIndex] = 'crossRot'
return {
datasets: [
{
label: 'Players',
labels: this.playerList.map(e => e.id) || [] as string[],
borderColor,
backgroundColor,
pointRadius,
pointStyle,
hoverRadius,
data: this.playerList.map((player) => ({ y: player.kills, x: player.deaths }))
}
]
}
},
chartStaticOptions () {
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
layout: { autoPadding: false },
scales: {
y: {
title: {
text: 'Kills',
display: true,
color: this.colors.fg
},
ticks: {
color: this.colors.fg
}
},
x: {
title: {
text: 'Deaths',
display: true,
color: this.colors.fg
},
ticks: {
color: this.colors.fg
}
}
},
onClick: (e: ChartEvent, element: any) => {
this.$emit('highlightPlayer', element.length > 0 ? this.playerList[element[0].index].id : undefined)
if (this.store.getPlayerList(this.filters || {}) !== undefined) triggerRef(this.store.getPlayerList(this.filters || {}) as unknown as Ref)
},
onHover: (e: any, element: any) => {
if (!e.native.target) return
(e.native.target as HTMLElement).style.cursor = element[0] ? 'pointer' : 'default'
}
}
},
chartStaticPlugins () {
let endX = 0
let endY = 0
if (this.playerList) {
const maxX = Math.max(...this.playerList.map(e => e.deaths))
const maxY = Math.max(...this.playerList.map(e => e.kills))
if (maxY > maxX) {
endX = maxX
endY = (maxX / maxY) * maxY
} else {
endY = maxY
endX = (maxY / maxX) * maxX
}
}
return {
tooltip: {
callbacks: {
label: (ctx: any) => {
let label: string
if (!this.playerList) {
label = ctx.dataset.labels[ctx.dataIndex]
} else {
label = this.playerList[ctx.dataIndex].username
}
label += ' (' + ctx.parsed.y + ' kills ' + ctx.parsed.x + ' deaths)'
return label
}
}
},
annotation: {
annotations: {
line: {
type: 'line',
yMin: 0,
xMin: 0,
yMax: endY,
xMax: endX,
borderWidth: 2,
borderColor: this.colors.orange,
borderDash: [1, 5]
}
}
} as _DeepPartialObject<AnnotationPluginOptions>,
legend: { display: false }
}
},
chartOptions (): _DeepPartialObject<CoreChartOptions<'scatter'> & PluginChartOptions<'scatter'> & ScaleChartOptions<'scatter'>> {
// eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted
return {
...this.chartStaticOptions,
plugins: {
...this.chartStaticPlugins,
datalabels: {
formatter: (value: any, context: any) => {
return this.playerList[context.dataIndex].username
},
backgroundColor: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerList[context.dataIndex].id) ? this.colors.orange : null,
borderColor: this.colors.fg,
borderWidth: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerList[context.dataIndex].id) ? 1 : 0,
borderRadius: 5,
display: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerList[context.dataIndex].id) ? 'true' : 'auto',
align: -45,
anchor: 'end',
clamp: true,
color: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerList[context.dataIndex].id) ? this.colors.bg : this.colors.fg
}
}
}
}
}
/* methods: {
refreshChart () {
if (this.store.getPlayerList(this.filters || {}) !== undefined) triggerRef(this.store.getPlayerList(this.filters || {}) as unknown as Ref)
}
} */
})
</script>
<style scoped>
canvas {
background: var(--accent);
}
.playerChart{
position:relative;
}
#refreshChart{
position:absolute;
top: 0;
left:0;
}
@media only screen and (max-width: 922px) {
canvas {
display: none !important
}
}
</style>
-251
View File
@@ -1,251 +0,0 @@
<template>
<div class="playerTable" v-on:keydown="selectNextPlayer" tabindex="0">
<div class="playerHeaders">
<span></span>
<span v-on:click="updateSort('username')"
:class="sortingData.argument == 'username' ? 'selected' : ''">Username</span>
<span v-on:click="updateSort('kills')" :class="sortingData.argument == 'kills' ? 'selected' : ''">K</span>
<span v-on:click="updateSort('deaths')" :class="sortingData.argument == 'deaths' ? 'selected' : ''">D</span>
<span v-on:click="updateSort('k/d')" :class="sortingData.argument == 'k/d' ? 'selected' : ''">K/D</span>
<span v-on:click="updateSort('max_distance')" :class="sortingData.argument == 'max_distance' ? 'selected' : ''">Max
distance</span>
<span v-on:click="updateSort('avg_distance')"
:class="sortingData.argument == 'avg_distance' ? 'selected' : ''">Average distance</span>
</div>
<LoadingBar v-if="progress!==1" :value="progress"></LoadingBar>
<VirtualList :list="playerList" :row-height="32" v-slot="slotProps" :highlighted="playerHighlighted" v-if="progress===1">
<div :class="'playerRow ' + (slotProps.data.id === playerHighlighted ? 'selected ' : '') + ((slotProps.index + 1)%2 ? 'odd ':'uneven ')" v-on:click="$emit('highlightPlayer', slotProps.data.id)">
<div><span>{{ slotProps.index+1 }}</span></div>
<div><span>{{ slotProps.data.value.username }}</span></div>
<div><span>{{ slotProps.data.value.kills }}</span></div>
<div><span>{{ slotProps.data.value.deaths }}</span></div>
<div><span>{{ Math.round(slotProps.data.value.kills / Math.max(1, slotProps.data.value.deaths) * 100) / 100 }}</span></div>
<div><span>{{ slotProps.data.value.max_distance }}</span></div>
<div><span>{{ Math.round(((slotProps.data.value.total_distance / slotProps.data.value.kills) || 0) * 100) / 100 }}</span></div>
</div>
</VirtualList>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref } from 'vue'
import { useKillStore, Player, Filters } from '@/stores/kill'
import VirtualList from './List/VirtualList.vue'
import LoadingBar from './LoadingBar.vue'
export default defineComponent({
components: { VirtualList, LoadingBar },
props: {
filters: { type: Object as PropType<Filters>, default: () => ({}) },
playerHighlighted: String
},
emits: ['highlightPlayer'],
data () {
return {
sortingData: { direction: -1, argument: 'kills' as keyof Player | 'k/d' | 'avg_distance' },
store: useKillStore()
}
},
computed: {
progress () {
const { player: _, ...withoutPlayer } = this.filters
return this.store.getPlayerList(withoutPlayer)?.value.progress
},
players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayer } = this.filters
const data = this.store.getPlayerList(withoutPlayer)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayer).value.data
return data
},
playerList (): (Ref<Player> & { id: string })[] {
if (!this.players) return []
const players = Object.entries(this.players).map(e => {
const target = {} as ProxyHandler<Ref<Player>>
const player = new Proxy(e[1], target) as Ref<Player> & { id: string }
player.id = e[0]
return player
}) as (Ref<Player> & { id: string })[]
if (this.sortingData.argument === 'username') {
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' })
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return collator.compare(aVal.username, bVal.username)
})
} else if (this.sortingData.argument === 'k/d') {
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return aVal.kills / Math.max(1, aVal.deaths) - bVal.kills / Math.max(1, bVal.deaths)
})
} else if (this.sortingData.argument === 'avg_distance') {
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return ((aVal.total_distance / aVal.kills) || 0) - ((bVal.total_distance / bVal.kills) || 0)
})
} else {
const argument = this.sortingData.argument
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return aVal[argument] - bVal[argument]
})
}
if (this.sortingData.direction < 0) {
players.reverse()
}
return players
}
},
watch: {
playerHighlighted (newval) {
this.scrollToPlayer(newval)
},
playerList () {
if (this.playerHighlighted) this.scrollToPlayer(this.playerHighlighted)
}
},
methods: {
selectNextPlayer (e: KeyboardEvent) {
const values = {
Home: -Infinity,
End: Infinity,
PageDown: 100,
PageUp: -100,
ArrowUp: -1,
ArrowDown: 1,
ArrowRight: 10,
ArrowLeft: -10
} as { [key: string]: number }
if (!this.$props.playerHighlighted) return
if (!Object.keys(values).includes(e.key)) return
e.preventDefault()
const index = this.playerList.findIndex(e => e.id === this.$props.playerHighlighted)
let nextIndex = (index + values[e.key])
if (nextIndex >= this.playerList.length) nextIndex = this.playerList.length - 1
if (nextIndex < 0) nextIndex = 0
this.$emit('highlightPlayer', this.playerList[nextIndex].id)
},
updateSort (argument: string) {
if (this.sortingData.argument === argument) {
this.sortingData.direction *= -1
} else {
this.sortingData.direction = -1
}
this.sortingData.argument = argument as typeof this.sortingData.argument
},
scrollToPlayer (playerid: string) {
const newElement = this.$refs['player:' + playerid] as HTMLElement[]
// Some hack because of some inconsistency
setTimeout(function () {
if (newElement && newElement[0]) { newElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) }
}, 10)
}
}
})
</script>
<style>
.playerTable * {
box-sizing: border-box;
}
.playerTable {
grid-area: list;
margin-right: 1rem;
height: 100%;
user-select: none;
}
.playerRow,
.playerHeaders {
box-sizing: border-box;
display: grid;
grid-template-columns: 6ch 20ch 6ch 6ch 6ch 10ch 1fr;
background: var(--bg-color);
text-align: left;
cursor: pointer;
margin: 0;
/* border: 2px solid transparent; */
transition: border .25s;
height: 2em;
}
.playerRow.odd {
background: var(--accent);
}
.playerRow.uneven {
background: var(--bg-color);
}
.playerRow:hover {
background: var(--current-line);
}
.playerRow>div:not(:last-child){
border-right: solid var(--bg-color) 2px;
}
.playerRow>div {
padding: .5em .8em 0 .25em;
overflow: hidden;
}
.playerHeaders {
position: sticky;
top: 0;
height: 3em;
}
.playerHeaders>span {
padding: .5em .8em 0 .25em;
}
.playerHeaders{
background: var(--current-line);
}
.playerHeaders div:hover {
background: var(--bg-color);
}
.playerHeaders>div:not(:last-child) {
border-right: solid var(--bg-color) 2px;
}
.playerHeaders>span:last-child {
width: 100%;
}
@media only screen and (max-width: 600px) {
.playerRow>div:nth-child(6),
.playerHeaders>span:nth-child(6),
.playerRow>div:nth-child(7),
.playerHeaders>span:nth-child(7) {
display: none;
}
.playerRow,
.playerHeaders {
grid-template-columns: 6ch 20ch 6ch 6ch 1fr;
}
}
.player span {
pointer-events: none;
}
.selected {
background: var(--comment) !important;
}
</style>
-185
View File
@@ -1,185 +0,0 @@
<template>
<div class="weaponChart" ref="container">
<LoadingBar v-if="progress !== 1" :value="progress"></LoadingBar>
<Doughnut :data="chart" :options="chartOptions" v-if="progress === 1"/>
</div>
</template>
<script lang="ts">
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
import dataLabel from 'chartjs-plugin-datalabels'
import { Weapon, Filters, useKillStore } from '@/stores/kill'
import { Doughnut } from 'vue-chartjs'
import { defineComponent, PropType, Ref } from 'vue'
import weapons from '../stores/weapons.json'
import LoadingBar from './LoadingBar.vue'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
export default defineComponent({
name: 'WeaponChart',
props: {
filters: Object as PropType<Filters>,
playerHighlighted: String
},
emits: ['highlightPlayer'],
components: {
Doughnut, LoadingBar
},
mounted () {
this.refreshColors++
},
data: () => {
return {
store: useKillStore(),
refreshColors: 0
}
},
computed: {
progress () {
const { weapon: _, ...withoutWeapon } = this.filters || {}
const data = this.store.getWeaponList(withoutWeapon)?.value
if (!data) return this.store.fetchWeapons(withoutWeapon).value.progress
return data.progress
},
colors () {
// eslint-disable-next-line no-unused-expressions
this.refreshColors
const colors = {} as { [key: string]: string }
if (this.$refs.container) {
const style = getComputedStyle(this.$refs.container as Element)
colors.fg = style.getPropertyValue('--foreground')
colors.bg = style.getPropertyValue('--bg-color')
colors.orange = style.getPropertyValue('--orange')
colors.cyan = style.getPropertyValue('--cyan')
colors.yellow = style.getPropertyValue('--yellow')
colors.green = style.getPropertyValue('--green')
colors.purple = style.getPropertyValue('--purple')
colors.red = style.getPropertyValue('--red')
colors.pink = style.getPropertyValue('--pink')
colors.currentLine = style.getPropertyValue('--current-line')
} else {
colors.fg = '#ffffff'
}
return colors
},
chart () {
const colors = [
'cyan',
'green',
'orange',
'pink',
'purple',
'red',
'yellow'
]
const chartData = {
datasets: [
{
label: 'Weapons',
labels: this.sortedWeaponList || ([] as string[]),
borderColor: () => this.colors.fg,
backgroundColor: (context: any) => {
return this.colors[
colors[(context.dataIndex * 3) % colors.length]
]
},
data: [] as number[]
}
]
}
if (!this.sortedWeaponList) {
return chartData
}
chartData.datasets[0].data = this.sortedWeaponList.map((e) => {
if (!this.weapons) {
return 0
}
return this.weapons[e].value.kills
})
return chartData
},
chartOptions () {
// eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted
const options = {
responsive: true,
maintainAspectRatio: true,
animation: { duration: 500 },
layout: { autoPadding: false },
plugins: {
datalabels: {
formatter: (value: any, context: any) => {
const weaponId = this.sortedWeaponList[context.dataIndex]
// if (!(weapons as {[key:string]:string})[weaponId]) console.log(weaponId)
return (weapons as {[key:string]:string})[weaponId] || weaponId
},
backgroundColor: (context: any) => {
return context.dataset.backgroundColor(context)
},
borderColor: this.colors.fg,
borderRadius: 25,
borderWidth: 2,
color: this.colors.bg,
display: (context: any) => {
return context.dataIndex === context.dataset.labels.length - 1
? true
: 'auto'
},
padding: 6
},
tooltip: {
callbacks: {
label: (ctx: any) => {
let label: string
if (!this.sortedWeaponList) {
label = ctx.dataset.labels[ctx.dataIndex]
} else {
label = this.sortedWeaponList[ctx.dataIndex]
}
label += ' (' + this.weapons[label].value.kills + ' kills)'
return label
}
}
}
}
}
return options
},
weapons (): { [key: string]: Ref<Weapon> } {
const { weapon: _, ...withoutWeapon } = this.filters || {}
const data = this.store.getWeaponList(withoutWeapon)?.value.data
if (!data) return this.store.fetchWeapons(withoutWeapon).value.data
return data
},
sortedWeaponList (): string[] {
if (!this.weapons) return []
const weapons = Object.keys(this.weapons).filter(e => this.weapons[e].value.kills > 0)
weapons.sort((a, b) => {
if (Number(this.weapons[a].value.kills) < Number(this.weapons[b].value.kills)) {
return -1
}
if (Number(this.weapons[a].value.kills) > Number(this.weapons[b].value.kills)) {
return 1
}
return 0
})
return weapons
}
}
})
</script>
<style scoped>
.weaponChart {
width: calc(min(50vw, 50vh) - 3em);
height: calc(min(50vw, 50vh) - 3em);
}
@media only screen and (max-width: 922px) {
canvas {
display: none !important;
}
}
</style>
-164
View File
@@ -1,164 +0,0 @@
/* eslint-disable camelcase */
import { createApp, ref, unref } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import { useKillStore } from './stores/kill'
const pinia = createPinia()
createApp(App).use(pinia).use(router).mount('#app')
const dummyData = {
attacker_id: '1008704914497',
attacker_name: 'DarkwinV1',
cause_of_death: 'mp_weapon_car',
victim_id: '1008553404453',
victim_name: 'GURENTITANIUM',
attacker_current_weapon: 'mp_weapon_car',
victim_current_weapon: 'mp_weapon_sniper',
distance: 300,
game_mode: 'ps',
servername: "fvnkhead's 3v3",
map: 'lf_meadow',
host: 1
}
// setInterval(() => registerWebSocketKill(dummyData), 100)
type websocketData = {
attacker_id: string,
attacker_name: string,
cause_of_death: string,
victim_id: string,
victim_name: string,
attacker_current_weapon: string,
victim_current_weapon: string,
distance: number,
game_mode: string,
servername: string,
map: string,
host: number
}
// dev stuff
const store = useKillStore()
const socket = new WebSocket('wss://tone.sleepycat.date/v2/client/websocket')
socket.onmessage = function (e) {
if (e.data === 'ping') return socket.send('pong')
const data = JSON.parse(e.data)
registerWebSocketKill(data)
}
function registerWebSocketKill (data : websocketData) {
store.$state.players.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername) &&
(!e.value.filter.weapon ||
e.value.filter.weapon === data.cause_of_death)
// Handle other types of filters once those are implemented
)
}).forEach(e => {
if (!e.value.data[data.attacker_id]) {
e.value.data[data.attacker_id] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.attacker_name
})
}
if (!e.value.data[data.victim_id]) {
e.value.data[data.victim_id] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name
})
}
// handle deaths with equipped, eventually
/*
if(e.value.filter.weapon){
!e.value.data[data.victim_id] = e.value.data[data.victim_id] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name,
}
} */
unref(e.value.data[data.victim_id]).deaths++
unref(e.value.data[data.victim_id]).username = data.victim_name
unref(e.value.data[data.attacker_id]).username = data.attacker_name
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.attacker_id]).kills++
unref(e.value.data[data.attacker_id]).total_distance += data.distance
unref(e.value.data[data.attacker_id]).max_distance = Math.max(data.distance, unref(e.value.data[data.attacker_id]).max_distance)
}
})
store.$state.weapons.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername) &&
(!e.value.filter.player || e.value.filter.player === data.attacker_id)
// Handle other types of filters once those are implemented
)
}).forEach(e => {
if (!e.value.data[data.cause_of_death]) {
e.value.data[data.cause_of_death] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0
})
}
// handle deaths with equipped, eventually
/*
if(e.value.filter.weapon){
!e.value.data[data.victim_id] = e.value.data[data.victim_id] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name,
}
} */
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.cause_of_death]).kills++
unref(e.value.data[data.cause_of_death]).total_distance += data.distance
unref(e.value.data[data.cause_of_death]).max_distance = Math.max(data.distance, unref(e.value.data[data.cause_of_death]).max_distance)
}
unref(e.value.data[data.cause_of_death]).deaths++
})
store.$state.servers.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername)
// Handle other types of filters once those are implemented
)
}).forEach((e) => {
if (!e.value.data[data.servername]) {
e.value.data[data.servername] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0,
host: data.host
})
}
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.servername]).kills++
unref(e.value.data[data.servername]).total_distance += data.distance
unref(e.value.data[data.servername]).max_distance = Math.max(data.distance, unref(e.value.data[data.servername]).max_distance)
}
unref(e.value.data[data.servername]).deaths++
})
}
-36
View File
@@ -1,36 +0,0 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const ServerView = () => import('../views/ServerView.vue')
const PlayerView = () => import('../views/PlayerView.vue')
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: PlayerView
},
{
path: '/servers',
name: 'servers',
component: ServerView
}
// {
// path: '/players',
// name: 'players',
// component: PlayerView
// },
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
// }
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
-6
View File
@@ -1,6 +0,0 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
-240
View File
@@ -1,240 +0,0 @@
import { defineStore } from 'pinia'
import * as assert from 'assert'
import { Ref, reactive, ref, shallowRef, triggerRef, unref } from 'vue'
export interface Filters {
server?: string;
player?: string;
weapon?: string;
host?: string;
map?: string;
gamemode?: string;
}
function removeNullEntries (a: Filters) {
return Object.fromEntries(
Object.entries(a).filter((e) => e[1] !== undefined && e[1] !== null)
)
}
function objectEqual (a: Filters, b: Filters) {
try {
a = removeNullEntries(a)
b = removeNullEntries(b)
assert.deepStrictEqual(a, b)
return true
} catch {
return false
}
}
function fetchWithLoading (url: string, progress: (percentage: number) => void) {
return fetch(url).then(response => {
if (!response.ok) {
throw Error(response.status + ' ' + response.statusText)
}
if (!response.body) {
throw Error('ReadableStream not yet supported in this browser.')
}
// to access headers, server must send CORS header "Access-Control-Expose-Headers: content-encoding, content-length x-file-size"
// server must send custom x-file-size header if gzip or other content-encoding is used
const contentEncoding = response.headers.get('Content-Encoding')
const contentLength = response.headers.get(contentEncoding ? 'X-File-Size' : 'content-length')
if (contentLength === null) {
throw Error('Response size header unavailable')
}
const total = parseInt(contentLength, 10)
let loaded = 0
return new Response(
new ReadableStream({
start (controller) {
const reader = response.body!.getReader()
read()
function read () {
reader.read().then(({ done, value }) => {
if (done) {
controller.close()
progress(1)
return
}
if (value) {
loaded += value.byteLength
progress(loaded / total)
controller.enqueue(value)
}
read()
}).catch(error => {
console.error(error)
controller.error(error)
})
}
}
})
)
})
.catch(error => {
console.error(error)
})
}
export interface Kill {
deaths: number;
kills: number;
// eslint-disable-next-line camelcase
max_distance: number;
// eslint-disable-next-line camelcase
total_distance: number;
}
export interface KillData<T extends Kill> {
filter: Filters;
data: { [key: string]: Ref<T> };
progress?: number;
}
export interface Player extends Kill {
username: string;
// eslint-disable-next-line camelcase
// deaths_while_equipped?: number;
}
export interface Weapon extends Kill {
// eslint-disable-next-line camelcase
deaths_while_equipped: number;
}
export interface Server extends Kill {
host: number;
}
export interface NSServer {
name: string;
region: string;
description: string;
playerCount: number;
maxPlayers: number;
map: string;
playlist: string;
hasPassword: boolean;
modInfo: { Mods: [] }
}
// define your typings for the store state
export interface State {
servers: Ref<KillData<Server>>[];
players: Ref<KillData<Player>>[];
weapons: Ref<KillData<Weapon>>[];
maps: Ref<KillData<Kill>>[];
gamemodes: Ref<KillData<Kill>>[];
hosts: { [key: number]: string };
nsServers: NSServer[] | undefined
}
let serverInterval: number
export const useKillStore = defineStore('kill', {
state: (): State => ({
servers: [],
players: [],
weapons: [],
maps: [],
gamemodes: [],
hosts: {},
nsServers: undefined
}),
getters: {
getPlayerList: (state) => (filters: Filters) => {
return state.players.find((e) => objectEqual(unref(e).filter, filters))
},
getWeaponList: (state) => (filters: Filters) =>
state.weapons.find((e) => objectEqual(unref(e).filter, filters)),
getServerList: (state) => (filters: Filters) =>
state.servers.find((e) => objectEqual(unref(e).filter, filters)),
getNSServers: (state) => state.nsServers
},
actions: {
fetchPlayers (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.players.find((e) => objectEqual(unref(e).filter, filter))
if (entry === undefined) {
entry = shallowRef({ filter, data: {} })
this.players.push(entry)
}
fetchWithLoading(
'https://tone.sleepycat.date/v2/client/players?' +
new URLSearchParams(filter as Record<keyof Filters, string>),
(progress) => {
if (entry) {
unref(entry).progress = progress
if (progress !== 1) triggerRef(entry)
}
}
).then(async response => {
if (entry) {
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Player)]))
triggerRef(entry)
}
})
return entry
},
fetchWeapons (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.weapons.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.weapons.push(entry)
}
fetchWithLoading(
'https://tone.sleepycat.date/v2/client/weapons?' +
new URLSearchParams(filter as Record<keyof Filters, string>),
(progress) => {
if (entry) {
unref(entry).progress = progress
if (progress !== 1) triggerRef(entry)
}
}
).then(async response => {
if (entry) {
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Weapon)]))
triggerRef(entry)
}
})
return entry
},
fetchServers (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.servers.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.servers.push(entry)
}
fetchWithLoading(
'https://tone.sleepycat.date/v2/client/servers?' +
new URLSearchParams(filter as Record<keyof Filters, string>),
(progress) => {
if (entry) {
unref(entry).progress = progress
if (progress !== 1) triggerRef(entry)
}
}
).then(async response => {
if (entry) {
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Server)]))
triggerRef(entry)
}
})
return entry
},
fetchNSServers () {
fetch(
'https://northstar.tf/client/servers').then(async response => {
this.nsServers = await response.json()
})
clearInterval(serverInterval)
serverInterval = setInterval(() => this.fetchNSServers(), 60000)
return this.nsServers || []
}
}
})
-76
View File
@@ -1,76 +0,0 @@
{
"rodeo_battery_removal": "Rodeo Battery Removal",
"human_execution": "Human Execution",
"melee_pilot_kunai": "Kunai Melee",
"execution": "Execution",
"mp_weapon_car": "CAR",
"mp_weapon_defender": "Charge Rifle",
"mp_weapon_doubletake": "Double Take",
"mp_weapon_epg": "EPG-1",
"mp_weapon_hemlok_smg": "Volt",
"mp_weapon_lstar": "L-STAR",
"mp_weapon_r97": "R-97",
"mp_weapon_rspn101": "R-201",
"mp_weapon_smart_pistol": "Smart Pistol",
"mp_weapon_sniper": "Kraber",
"mp_weapon_vinson": "Flatline",
"mp_weapon_autopistol": "RE-45 auto",
"mp_weapon_alternator_smg": "Alternator",
"mp_weapon_frag_grenade": "Frag Grenade",
"mp_weapon_g2": "G2",
"mp_weapon_lmg": "Spitfire",
"mp_weapon_smr": "Sidewinder SMR",
"mp_weapon_softball": "Softball",
"mp_weapon_thermite_grenade": "Firestar",
"pilot_emptyhanded": "Melee",
"mp_weapon_mastiff": "Mastiff",
"mp_weapon_mgl": "MGL",
"mp_weapon_rspn101_og": "R-101",
"mp_weapon_wingman": "Wingman",
"invalid": "invalid",
"mp_weapon_arc_launcher": "Thunderbolt",
"mp_weapon_dmr": "DMR",
"mp_weapon_grenade_electric_smoke": "Electric Smoke",
"mp_weapon_pulse_lmg": "Cold War",
"mp_weapon_satchel": "Satchel",
"mp_weapon_shotgun": "EVA-8 Auto",
"mp_weapon_shotgun_pistol": "Mozambique",
"mp_weapon_wingman_n": "Wingman Elite",
"mp_weapon_grenade_gravity": "Gravity Star",
"mp_weapon_grenade_sonar": "Pulse Blade",
"mp_weapon_rocket_launcher": "Archer",
"outOfBounds": "OutOfBounds",
"mind_crime": "Mind Crime",
"mp_weapon_esaw": "Devotion",
"fall": "Fall",
"mp_weapon_semipistol": "P2016",
"mp_weapon_grenade_emp": "Arc Grenade",
"mp_weapon_hemlok": "Hemlok",
"melee_pilot_emptyhanded": "Melee",
"phase_shift": "Phase Shift",
"damagedef_suicide": "Suicide",
"damagedef_titan_hotdrop": "Crushed by Titanfall",
"mp_titanweapon_arc_cannon": "Arc Cannon",
"mp_weapon_peacekraber": "Peacekraber",
"damagedef_crush": "Crushed",
"mp_titanweapon_brute4_quad_rocket": "Quad Rocket Launcher",
"mp_weapon_turretplasma": "Antititan Turret",
"mp_titanweapon_xo16_vanguard": "XO-16",
"mp_titanweapon_sticky_40mm": "40mm Cannon",
"mp_titanweapon_predator_cannon": "Predator Cannon",
"mp_titanweapon_leadwall": "Leadwall",
"mp_titanweapon_sniper": "Plasma Railgun",
"titan_execution": "Titan Execution",
"titan_explosion": "Titan Explosion",
"auto_titan_melee": "Autotitan Melee",
"mp_titanweapon_vortex_shield_ion":"Vortex Shield",
"mp_titanweapon_heat_shield":"Heat shield",
"bubble_shield":"Bubble Shield",
"mp_titancore_salvo_core":"Salvo Core",
"mp_titancore_flame_wave":"Flame Wave",
"mp_titanweapon_flightcore_rockets":"Flight Core",
"mp_titancore_laser_cannon":"Laser Cannon",
"mp_titancore_flame_wave_secondary":"Scorched Earth",
"mp_titanweapon_flame_wall":"Flame Wall",
"mp_titanweapon_arc_wave":"Arc Wave"
}
-5
View File
@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
-292
View File
@@ -1,292 +0,0 @@
<template>
<div id="filters">
<span class="multiselect-wrapper">
<VueMultiselect
selectLabel=""
deselectLabel="remove"
placeholder="Select server"
v-model="model.server"
:options="sortedServerList"
:allow-empty="true"
>
</VueMultiselect>
<button @click="model.server = undefined" :disabled="!model.server">X</button>
</span>
<span class="multiselect-wrapper">
<VueMultiselect
selectLabel=""
deselectLabel="remove"
placeholder="Select weapon"
v-model="model.weapon"
:custom-label="((e: any) => weaponLocale[e] || e)"
:options="sortedWeaponList"
:allow-empty="true"
>
</VueMultiselect>
<button @click="model.weapon = undefined" :disabled="!model.weapon">X</button>
</span>
<span class="multiselect-wrapper">
<VueMultiselect
:options-limit="20"
selectLabel=""
deselectLabel="remove"
placeholder="Search player"
v-model="playerHighlighted"
:options="sortedPlayerList"
:allow-empty="true"
:custom-label="((e: Player) => e?.username)"
></VueMultiselect>
<button @click="playerHighlighted = undefined" :disabled="!playerHighlighted">X</button>
</span>
</div>
<div id="playerView">
<PlayerList
:filters="filters"
v-on:highlightPlayer="highlight_player"
:playerHighlighted="playerHighlighted?.id"
>
</PlayerList>
<PlayerChart
:filters="filters"
v-on:highlightPlayer="highlight_player"
:playerHighlighted="playerHighlighted?.id"
>
</PlayerChart>
<WeaponChart
:filters="{ player: playerHighlighted?.id, ...filters }"
:playerHighlighted="playerHighlighted?.id"
>
</WeaponChart>
</div>
</template>
<script lang="ts">
import { Player, Weapon, Server, useKillStore, Filters } from '@/stores/kill'
import PlayerList from '@/components/PlayerList.vue'
import PlayerChart from '@/components/PlayerChart.vue'
import WeaponChart from '@/components/WeaponChart.vue'
import { Ref, defineComponent, unref } from 'vue'
import VueMultiselect from 'vue-multiselect'
import 'vue-multiselect/dist/vue-multiselect.css'
import weapons from '../stores/weapons.json'
import router from '@/router'
export default defineComponent({
name: 'PlayerView',
components: {
VueMultiselect,
PlayerList,
PlayerChart,
WeaponChart
},
data () {
return {
model: { server: undefined, weapon: undefined } as unknown as {
weapon: string | undefined;
server: string | undefined;
},
filters: {} as Filters,
store: useKillStore(),
playerHighlighted: undefined as (Player & { id: string }) | undefined,
weaponLocale: weapons as { [key: string]: string }
}
},
beforeCreate () {
let server
if (this.$route.query.server) server = this.$route.query.server?.toString()
this.model = {
server,
weapon: this.$route.query.weapon?.toString()
}
},
computed: {
servers (): { [key: string]: Ref<Server> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { server: _, player: _1, ...withoutServer } = this.filters
const data = this.store.getServerList(withoutServer)?.value.data
if (!data) return unref(this.store.fetchServers(withoutServer)).data
return data
},
groupedServers () {
const hosts = [] as {
id: number;
servers: (Server & { name?: string })[];
}[]
Object.entries(this.servers).forEach((e) => {
const server = unref(e[1])
if (!hosts.find((host) => host.id === server.host)) { hosts.push({ id: server.host, servers: [] as Server[] }) }
hosts
.find((host) => host.id === server.host)
?.servers.push({ name: e[0], ...server })
})
return hosts
},
weapons (): { [key: string]: Ref<Weapon> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { weapon: _, player: _1, ...withoutWeapons } = this.filters
const data = this.store.getWeaponList(withoutWeapons)?.value.data
if (!data) return this.store.fetchWeapons(withoutWeapons).value.data
return data
},
players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayers } = this.filters
const data = this.store.getPlayerList(withoutPlayers)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayers).value.data
return data
},
sortedWeaponList (): string[] {
if (!this.weapons) return []
const weapons = Object.keys(this.weapons)
return weapons.sort()
},
sortedServerList (): string[] {
if (!this.servers) return []
const servers = Object.keys(this.servers)
return servers.sort()
},
sortedPlayerList (): Player[] {
if (!this.players) return []
const players = Object.entries(this.players).map((e) => ({
id: e[0],
...e[1]._rawValue
}))
return players.sort((a, b) => {
if (a.username < b.username) {
return -1
}
if (a.username > b.username) {
return 1
}
return 0
})
}
},
watch: {
players () {
const player = this.$route.query.player?.toString()
if (player && this.playerHighlighted?.id !== player) {
this.highlight_player(player)
}
},
model: {
handler (newModel) {
this.filters.server = newModel.server
this.filters.weapon = newModel.weapon
},
deep: true
},
filters: {
handler (newValue) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { server: _, weapon: _2, ...withoutFilters } = this.$route.query
router.push({ query: { ...newValue, ...withoutFilters } })// .then(e => { console.log(e) })
},
deep: true
},
playerHighlighted (newValue, oldValue) {
if (newValue?.id === oldValue?.id) return
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayer } = this.$route.query
router.push({ query: { player: newValue?.id, ...withoutPlayer } })// .then(e => { console.log(e) })
},
$route: {
handler: function () {
this.applyRouteFilters()
},
immediate: true
}
},
methods: {
highlight_player (playerid: string) {
const player = this.players[playerid]
if (player) {
this.playerHighlighted = { id: playerid, ...unref(player) }
return
}
this.playerHighlighted = undefined
},
applyRouteFilters () {
if (this.playerHighlighted?.id !== this.$route.query.player) {
const playerId = this.$route.query.player?.toString()
if (playerId) {
this.highlight_player(playerId)
}
}
if (this.filters.weapon !== this.$route.query.weapon) {
this.model.weapon = this.$route.query.weapon?.toString()
}
if (this.filters.server !== this.$route.query.server) {
if (this.$route.query.server) this.model.server = this.$route.query.server?.toString()
else this.model.server = undefined
}
}
}
})
</script>
<style scoped>
.multiselect {
margin: 0 0.5em 1em 0.5em;
}
.multiselect-wrapper {
display: flex;
}
.multiselect-wrapper button {
margin: 0 0 1em 0;
font-size: 16px;
}
#playerView {
margin: 0 1rem 1rem 1rem;
/* height:100%; */
display: grid;
overflow: auto;
grid-template-areas:
"list chart"
"list info";
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
}
@media only screen and (max-width: 922px) {
#playerView {
margin: 0 1rem 1rem 1rem;
/* height:100%; */
display: grid;
overflow: auto;
grid-template-areas: "list list";
grid-template-columns: 100%;
grid-template-rows: 100%;
width: calc(100vw - 1em);
margin: 0 0rem 1rem 1rem;
}
.multiselect-wrapper button {
margin: 0 0.5em 1em 0;
}
.multiselect {
width: calc(100% - 1em);
}
.playerChart {
display: none;
}
.weaponChart {
display: none;
}
}
#filters {
grid-area: filters;
}
</style>
-144
View File
@@ -1,144 +0,0 @@
<template>
<div id="filters">
</div>
<div class="serverView">
<div class="serverList">
<div class="serverHeaders">
<span></span>
<span>Map</span>
<span>Server name</span>
<span>Players</span>
<span>Kills</span>
<span>Max distance</span>
<span>Average distance</span>
</div>
<VirtualList :list="serverList" :row-height="100" v-slot="slotProps">
<div
:class="'serverRow ' + (!nsServersByName[slotProps.data.name] ? 'offline ' : '') + (!nsServersByName[slotProps.data.name]?.playerCount ? 'inactive ' : '') + ((slotProps.index + 1) % 2 ? 'odd ' : 'uneven ')">
<div><span>{{ slotProps.index + 1 }}</span></div>
<img v-if="nsServersByName[slotProps.data.name]"
:src="`${publicPath}maps/${nsServersByName[slotProps.data.name].map}_lobby.png`" />
<div v-if="!nsServersByName[slotProps.data.name]"></div>
<div><span>{{ slotProps.data.name + (!nsServersByName[slotProps.data.name] ? ' (offline)' : '') }}</span></div>
<div><span>{{ nsServersByName[slotProps.data.name] ? nsServersByName[slotProps.data.name]?.playerCount
+ '/' + nsServersByName[slotProps.data.name]?.maxPlayers : '' }}</span></div>
<div><span>{{ slotProps.data._value.deaths }}</span></div>
<div><span>{{ slotProps.data._value.max_distance }}</span></div>
<div><span>{{ Math.round(((slotProps.data._value.total_distance / slotProps.data._value.kills) || 0) * 100) /
100 }}</span></div>
</div>
</VirtualList>
</div>
</div>
</template>
<script lang="ts">
import { NSServer, Server, useKillStore } from '@/stores/kill'
import VirtualList from '@/components/List/VirtualList.vue'
import { Ref, defineComponent, unref } from 'vue'
export default defineComponent({
name: 'ServerView',
components: {
VirtualList
},
data () {
return {
store: useKillStore(),
publicPath: process.env.BASE_URL
}
},
computed: {
servers (): { [key: string]: Ref<Server> } {
const data = this.store.getServerList({})?.value.data
if (!data) return unref(this.store.fetchServers({})).data
return data
},
serverList (): (Ref<Server> & { name: string })[] {
const data = Object.entries(this.servers).map(e => ({ ...e[1], name: e[0] }))
data.sort((server1, server2) => {
const a = !this.nsServersByName[server1.name] ? -1 : this.nsServersByName[server1.name].playerCount ? 1 : 0
const b = !this.nsServersByName[server2.name] ? -1 : this.nsServersByName[server2.name].playerCount ? 1 : 0
return b - a
})
return data
},
nsServers (): NSServer[] {
const data = this.store.getNSServers
if (!data) {
return this.store.fetchNSServers()
}
return data
},
nsServersByName (): { [key: string]: NSServer } {
return Object.fromEntries(this.nsServers.map(e => [e.name, e]))
}
}
})
</script>
<style scoped>
.inactive {
color: var(--comment);
}
.offline {
color: var(--comment);
}
.serverView{
margin: 0 1rem 1rem 1rem;
overflow:auto;
}
.serverList {
overflow-y: auto;
height: 100%;
}
.serverRow,
.serverHeaders {
box-sizing: border-box;
display: grid;
grid-template-columns: 4ch 178px 20ch 8ch 8ch 8ch 1fr;
background: var(--bg-color);
text-align: left;
cursor: pointer;
margin: 0;
/* border: 2px solid transparent; */
transition: border .25s;
height: 100px;
}
.serverHeaders {
height: 3em;
background: var(--current-line);
}
.serverHeaders > span {
padding: .5em .8em 0 .25em;
}
.serverRow.odd {
background: var(--accent);
}
.serverRow.uneven {
background: var(--bg-color);
}
.serverRow:hover {
background: var(--current-line);
}
.serverRow>div{
padding: .5em .8em 0 .25em;
}
.serverRow>div:not(:last-child){
border-right: solid var(--bg-color) 2px;
}
img {
object-fit: contain;
height: 100%;
}</style>

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

-40
View File
@@ -1,40 +0,0 @@
{
"compilerOptions": {
"resolveJsonModule":true,
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"typeRoots" : ["node_modules/@types", "src/@types"],
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
]
}
-20
View File
@@ -1,20 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
productionSourceMap: process.env.NODE_ENV !== 'production',
transpileDependencies: true,
publicPath: process.env.NODE_ENV === 'production' ? '/ToneAPI_webclient/' : '/',
pages: {
index: {
entry: 'src/main.ts',
template: 'public/index.html',
filename: 'index.html',
title: 'Players : Tone API'
},
server: {
entry: 'src/main.ts',
template: 'public/servers.html',
filename: 'servers.html',
title: 'Servers : Tone API'
}
}
})

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

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