Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f596b9f50 |
@@ -1,4 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
@@ -1,5 +0,0 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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/).
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -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%}
|
||||
@@ -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%}
|
||||
@@ -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%}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -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>
|
||||
@@ -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}}]);
|
||||
|
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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Ref } from '@vue/reactivity'
|
||||
|
||||
declare module '@vue/reactivity' {
|
||||
export interface Ref<T> extends Ref<T> {
|
||||
_rawValue: T
|
||||
}
|
||||
}
|
||||
@@ -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 (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>
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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++
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -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 || []
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 |