25 Commits
Author SHA1 Message Date
legonzaur 3e263211eb optimize data structure and refs 2023-05-04 11:48:40 +02:00
legonzaur 20b0e09cca 'publish' 2023-05-02 14:36:19 +02:00
legonzaur 78a4cc8a1b add social image 2023-05-02 14:35:54 +02:00
legonzaur 32efc16ae1 'publish' 2023-05-01 19:46:05 +02:00
legonzaur 33bd50081e disable clear button if filter can't be cleared 2023-05-01 19:45:50 +02:00
legonzaur 4824657f44 'publish' 2023-05-01 19:35:44 +02:00
legonzaur 50759f129c add query arguments for filters 2023-05-01 19:35:28 +02:00
legonzaur fd6ad61923 fix button margin for mobile UI 2023-05-01 17:54:30 +02:00
legonzaur 4e56f70fec 'publish' 2023-05-01 17:50:18 +02:00
legonzaur f815cb3d1a Add remove filter button 2023-05-01 17:50:01 +02:00
legonzaur ac57843c48 Fix responsive 2023-05-01 17:49:52 +02:00
legonzaur 93096daee3 'publish' 2023-05-01 17:28:15 +02:00
legonzaur c6ea1ce032 Split chart setup 2023-05-01 17:26:43 +02:00
legonzaur f65e4b20af Limit player dropdown to 20 entries 2023-05-01 17:26:17 +02:00
legonzaur d41b16e697 fix deselection of player 2023-05-01 15:51:40 +02:00
legonzaur 5f1611998a fix keyboard controls 2023-05-01 15:47:32 +02:00
legonzaur e155cb1013 'publish' 2023-05-01 15:23:51 +02:00
legonzaur b78072d385 speedup player List sorting 2023-05-01 15:23:25 +02:00
legonzaur 91977ded3d 'publish' 2023-04-28 17:24:30 +02:00
legonzaur b9fb1ce5a1 typo in CAR name 2023-04-28 17:24:07 +02:00
legonzaur 4b400348e6 'publish' 2023-04-28 17:21:50 +02:00
legonzaur bc1e23255d Add weapon translation 2023-04-28 17:21:26 +02:00
legonzaur dd075d2982 single API requests before recieving the data 2023-04-28 16:18:52 +02:00
legonzaur 0be6adb7d2 'publish' 2023-04-25 13:25:30 +02:00
legonzaur 0e0404ce0a fix production route 2023-04-25 13:25:08 +02:00
17 changed files with 642 additions and 276 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +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"><link rel="icon" href="/ToneAPI_webclient/favicon.ico"><title>toneapi_webclient</title><script defer="defer" src="/ToneAPI_webclient/js/chunk-vendors.3f4f208d.js"></script><script defer="defer" src="/ToneAPI_webclient/js/app.f8f821bc.js"></script><link href="/ToneAPI_webclient/css/app.bbc51a59.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but toneapi_webclient doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!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="Tone API" property="og:title"/><meta content="Northstar kill 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>toneapi_webclient</title><script defer="defer" src="/ToneAPI_webclient/js/chunk-vendors.3f4f208d.js"></script><script defer="defer" src="/ToneAPI_webclient/js/app.13330013.js"></script><link href="/ToneAPI_webclient/css/app.a9b40eb3.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but toneapi_webclient doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

+5
View File
@@ -4,6 +4,11 @@
<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="Tone API" property="og:title" />
<meta content="Northstar kill 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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

+102 -87
View File
@@ -17,12 +17,12 @@ import {
PluginChartOptions,
ScaleChartOptions
} from 'chart.js'
import annotationPlugin from 'chartjs-plugin-annotation'
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 } from 'vue'
import { defineComponent, PropType, toRaw, unref } from 'vue'
import { ChartEvent } from 'chart.js/dist/core/core.plugins'
import { _DeepPartialObject } from 'chart.js/dist/types/utils'
@@ -37,7 +37,8 @@ export default defineComponent({
data: () => {
return {
store: useKillStore(),
refreshColors: 0
refreshColors: 0,
playerData: []
}
},
emits: ['highlightPlayer'],
@@ -48,6 +49,15 @@ export default defineComponent({
this.refreshColors++
},
computed: {
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
@@ -64,39 +74,35 @@ export default defineComponent({
}
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.playerIdList || [] as string[],
borderColor: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.index]) ? this.colors.orange : this.colors.cyan,
backgroundColor: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.index]) ? this.colors.orange : this.colors.cyan,
pointRadius: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.index]) ? 20 : 1,
pointStyle: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.index]) ? 'crossRot' : 'circle',
hoverRadius: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.index]) ? 30 : 4,
data: this.playerIdList.map((id) => ({ y: this.players[id].kills, x: this.players[id].deaths }))
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 }))
}
]
}
},
chartOptions (): _DeepPartialObject<CoreChartOptions<'scatter'> & PluginChartOptions<'scatter'> & ScaleChartOptions<'scatter'>> {
// eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted
let endX = 0
let endY = 0
if (this.players) {
const maxX = Math.max(...Object.values(this.players).map(e => e.deaths))
const maxY = Math.max(...Object.values(this.players).map(e => e.kills))
if (maxY > maxX) {
endX = maxX
endY = (maxX / maxY) * maxY
} else {
endY = maxY
endX = (maxY / maxX) * maxX
}
}
chartStaticOptions () {
return {
responsive: true,
maintainAspectRatio: false,
@@ -124,53 +130,8 @@ export default defineComponent({
}
}
},
plugins: {
tooltip: {
callbacks: {
label: (ctx: any) => {
let label: string
if (!this.players) {
label = ctx.dataset.labels[ctx.dataIndex]
} else {
label = this.players[ctx.dataset.labels[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]
}
}
},
legend: { display: false },
datalabels: {
formatter: (value: any, context: any) => {
return this.players[this.playerIdList[context.dataIndex]].username
},
backgroundColor: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.dataIndex]) ? this.colors.orange : null,
borderColor: this.colors.fg,
borderWidth: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.dataIndex]) ? 1 : 0,
borderRadius: 5,
display: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.dataIndex]) ? 'true' : 'auto',
align: -45,
anchor: 'end',
clamp: true,
color: (context: any) => (this.$props.playerHighlighted && this.$props.playerHighlighted === this.playerIdList[context.dataIndex]) ? this.colors.bg : this.colors.fg
}
},
onClick: (e: ChartEvent, element: any) => {
this.$emit('highlightPlayer', element.length > 0 ? this.playerIdList[element[0].index] : undefined)
this.$emit('highlightPlayer', element.length > 0 ? this.playerList[element[0].index].id : undefined)
},
onHover: (e: any, element: any) => {
if (!e.native.target) return
@@ -178,22 +139,76 @@ export default defineComponent({
}
}
},
players (): { [key: string]: Player } {
const data = this.store.getPlayerList(this.filters || {})?.data
if (!data) return {}
const cut = Object.entries(data).sort((a, b) => {
if (a[1].kills < b[1].kills) {
return 1
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
}
if (a[1].kills > b[1].kills) {
return -1
}
return 0
}).slice(0, 200)
return Object.fromEntries(cut)
}
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 }
}
},
playerIdList (): string[] { return Object.keys(this.players) }
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
}
}
}
}
}
})
</script>
+80 -72
View File
@@ -2,32 +2,32 @@
<div class="playerTable" v-on:keydown="selectNextPlayer" tabindex="0">
<div class="playerHeaders">
<span></span>
<span v-on:click="sortPlayerList('username')"
<span v-on:click="updateSort('username')"
:class="sortingData.argument == 'username' ? 'selected' : ''">Username</span>
<span v-on:click="sortPlayerList('kills')" :class="sortingData.argument == 'kills' ? 'selected' : ''">K</span>
<span v-on:click="sortPlayerList('deaths')" :class="sortingData.argument == 'deaths' ? 'selected' : ''">D</span>
<span v-on:click="sortPlayerList('k/d')" :class="sortingData.argument == 'k/d' ? 'selected' : ''">K/D</span>
<span v-on:click="sortPlayerList('max_distance')"
<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="sortPlayerList('avg_distance')"
<span v-on:click="updateSort('avg_distance')"
:class="sortingData.argument == 'avg_distance' ? 'selected' : ''">average distance</span>
</div>
<div :class="'playerRow ' + (playerId === $props.playerHighlighted ? 'selected' : '')"
v-for="(playerId, index) in playerIdList" v-bind:key="playerId" v-on:click="$emit('highlightPlayer', playerId)"
:ref="`player:` + playerId">
<div :class="'playerRow ' + (player.id === $props.playerHighlighted ? 'selected' : '')"
v-for="(player, index) in playerList" v-bind:key="player.id" v-on:click="$emit('highlightPlayer', player.id)"
:ref="`player:` + player.id">
<div><span>{{ index+1 }}</span></div>
<div><span>{{ players[playerId].username }}</span></div>
<div><span>{{ players[playerId].kills }}</span></div>
<div><span>{{ players[playerId].deaths }}</span></div>
<div><span>{{ Math.round(players[playerId].kills / Math.max(1, players[playerId].deaths) * 100) / 100 }}</span></div>
<div><span>{{ players[playerId].max_distance }}</span></div>
<div><span>{{ Math.round(((players[playerId].total_distance / players[playerId].kills) || 0) * 100) / 100 }}</span></div>
<div><span>{{ player.value.username }}</span></div>
<div><span>{{ player.value.kills }}</span></div>
<div><span>{{ player.value.deaths }}</span></div>
<div><span>{{ Math.round(player.value.kills / Math.max(1, player.value.deaths) * 100) / 100 }}</span></div>
<div><span>{{ player.value.max_distance }}</span></div>
<div><span>{{ Math.round(((player.value.total_distance / player.value.kills) || 0) * 100) / 100 }}</span></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { defineComponent, PropType, Ref, toRaw, unref } from 'vue'
import { useKillStore, Player, Filters } from '@/stores/kill'
export default defineComponent({
@@ -39,74 +39,67 @@ export default defineComponent({
data () {
return {
sortingData: { direction: -1, argument: 'kills' as keyof Player | 'k/d' | 'avg_distance' },
playerIdList: [] as string[],
store: useKillStore()
}
},
computed: {
players (): { [key: string]: Player } {
players (): { [key: string]: Ref<Player> } {
const { player: _, ...withoutPlayer } = this.filters
return this.store.getPlayerList(withoutPlayer)?.data || {}
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 []
let 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: Ref<Player>, b: Ref<Player>) => {
return collator.compare(toRaw(a.value).username, toRaw(b.value).username)
})
} else if (this.sortingData.argument === 'k/d') {
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return toRaw(a.value).kills / Math.max(1, toRaw(a.value).deaths) - toRaw(b.value).kills / Math.max(1, toRaw(b.value).deaths)
})
} else if (this.sortingData.argument === 'avg_distance') {
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return ((toRaw(a.value).total_distance / toRaw(a.value).kills) || 0) - ((toRaw(b.value).total_distance / toRaw(b.value).kills) || 0)
})
} else {
const argument = this.sortingData.argument
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return toRaw(a.value)[argument] - toRaw(b.value)[argument]
})
}
if (this.sortingData.direction < 0) {
players.reverse()
}
players = players.slice(0, 500)
return players
}
},
watch: {
players: {
handler (newval: { [key: string]: Player }): void {
if (!newval) return
this.playerIdList = []
this.sortingData.direction *= -1
this.playerIdList = Object.keys(newval)
this.sortPlayerList(this.sortingData.argument)
this.playerIdList = this.playerIdList.slice(0, 500)
},
immediate: true
},
playerHighlighted (newval) {
const newElement = this.$refs['player:' + newval] as HTMLElement[]
if (newElement && newElement[0]) { newElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) }
this.scrollToPlayer(newval)
},
playerList () {
if (this.playerHighlighted) this.scrollToPlayer(this.playerHighlighted)
}
},
updated () {
const element = this.$refs['player:' + this.$props.playerHighlighted] as HTMLElement[]
/* const element = this.$refs['player:' + this.$props.playerHighlighted] as HTMLElement[]
if (!element || !element[0]) return
element[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
element[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) */
},
methods: {
sortPlayerList (arg: ((keyof Player) | 'k/d' | 'avg_distance')) {
if (arg === this.sortingData.argument) {
this.sortingData.direction *= -1
} else {
if (arg === 'username') {
this.sortingData.direction = 1
}
}
this.sortingData.argument = arg
this.playerIdList.sort((a: string, b: string) => {
if (!this.players) {
return 0
}
let varA, varB
if (arg === 'k/d') {
varA = this.players[a].kills / Math.max(1, this.players[a].deaths)
varB = this.players[b].kills / Math.max(1, this.players[b].deaths)
} else if (arg === 'avg_distance') {
varA = (this.players[a].total_distance / this.players[a].kills) || 0
varB = (this.players[b].total_distance / this.players[b].kills) || 0
} else {
varA = this.players[a][arg]
varB = this.players[b][arg]
}
if (varA === undefined || varB === undefined) return 0
if (varA < varB) {
return -1 * this.sortingData.direction
}
if (varA > varB) {
return 1 * this.sortingData.direction
}
return 0
})
},
selectNextPlayer (e: KeyboardEvent) {
const values = {
Home: -Infinity,
@@ -121,11 +114,26 @@ export default defineComponent({
if (!this.$props.playerHighlighted) return
if (!Object.keys(values).includes(e.key)) return
e.preventDefault()
const index = this.playerIdList.indexOf(this.$props.playerHighlighted)
const index = this.playerList.findIndex(e => e.id === this.$props.playerHighlighted)
let nextIndex = (index + values[e.key])
if (nextIndex >= this.playerIdList.length) nextIndex = this.playerIdList.length - 1
if (nextIndex >= this.playerList.length) nextIndex = this.playerList.length - 1
if (nextIndex < 0) nextIndex = 0
this.$emit('highlightPlayer', this.playerIdList[nextIndex])
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)
}
}
+12 -12
View File
@@ -9,7 +9,8 @@ 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 } from 'vue'
import { defineComponent, PropType, Ref, unref } from 'vue'
import weapons from '../stores/weapons.json'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
@@ -86,7 +87,7 @@ export default defineComponent({
if (!this.weapons) {
return 0
}
return this.weapons[e].kills
return this.weapons[e].value.kills
})
return chartData
},
@@ -101,7 +102,9 @@ export default defineComponent({
plugins: {
datalabels: {
formatter: (value: any, context: any) => {
return this.sortedWeaponList[context.dataIndex]
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)
@@ -126,7 +129,7 @@ export default defineComponent({
} else {
label = this.sortedWeaponList[ctx.dataIndex]
}
label += ' (' + this.weapons[label].kills + ' kills)'
label += ' (' + this.weapons[label].value.kills + ' kills)'
return label
}
}
@@ -135,23 +138,20 @@ export default defineComponent({
}
return options
},
weapons (): { [key: string]: Weapon } {
weapons (): { [key: string]: Ref<Weapon> } {
const { weapon: _, ...withoutWeapon } = this.filters || {}
const data = this.store.getWeaponList(withoutWeapon)?.data
if (!data) {
this.store.fetchWeapons(withoutWeapon)
return {}
}
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)
weapons.sort((a, b) => {
if (Number(this.weapons[a].kills) < Number(this.weapons[b].kills)) {
if (Number(this.weapons[a].value.kills) < Number(this.weapons[b].value.kills)) {
return -1
}
if (Number(this.weapons[a].kills) > Number(this.weapons[b].kills)) {
if (Number(this.weapons[a].value.kills) > Number(this.weapons[b].value.kills)) {
return 1
}
return 0
+102 -1
View File
@@ -1,8 +1,109 @@
import { createApp } from 'vue'
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 websocketData = {
attacker_id: '1010035351172',
attacker_name: 'Iverral',
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
}
const store = useKillStore()
setInterval(() => {
const allPlayerArrays = store.$state.players.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === websocketData.servername) &&
(!e.value.filter.weapon ||
e.value.filter.weapon === websocketData.cause_of_death)
// Handle other types of filters once those are implemented
)
})
allPlayerArrays.forEach(e => {
if (!e.value.data[websocketData.attacker_id]) {
e.value.data[websocketData.attacker_id] = ref({
deaths: 0,
kills: 500,
max_distance: 0,
total_distance: 0,
username: websocketData.attacker_name
})
}
if (!e.value.data[websocketData.victim_id]) {
e.value.data[websocketData.victim_id] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: websocketData.victim_name
})
}
// handle deaths with equipped, eventually
/*
if(e.value.filter.weapon){
!e.value.data[websocketData.victim_id] = e.value.data[websocketData.victim_id] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: websocketData.victim_name,
}
} */
unref(e.value.data[websocketData.victim_id]).deaths++
unref(e.value.data[websocketData.victim_id]).username = websocketData.victim_name
unref(e.value.data[websocketData.attacker_id]).username = websocketData.attacker_name
unref(e.value.data[websocketData.attacker_id]).kills++
unref(e.value.data[websocketData.attacker_id]).total_distance += websocketData.distance
unref(e.value.data[websocketData.attacker_id]).max_distance = Math.max(websocketData.distance, unref(e.value.data[websocketData.attacker_id]).max_distance)
})
const allWeaponsArrays = store.$state.weapons.filter((e) => {
return (
(!e.value.filter.player || e.value.filter.player === websocketData.attacker_id)
// Handle other types of filters once those are implemented
)
})
allWeaponsArrays.forEach(e => {
if (!e.value.data[websocketData.cause_of_death]) {
e.value.data[websocketData.cause_of_death] = ref({
deaths: 0,
kills: 500,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0
})
}
// handle deaths with equipped, eventually
/*
if(e.value.filter.weapon){
!e.value.data[websocketData.victim_id] = e.value.data[websocketData.victim_id] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: websocketData.victim_name,
}
} */
// unref(e.value.data[websocketData.cause_of_death]).deaths++
unref(e.value.data[websocketData.cause_of_death]).total_distance += websocketData.distance
unref(e.value.data[websocketData.cause_of_death]).kills++
unref(e.value.data[websocketData.cause_of_death]).max_distance = Math.max(websocketData.distance, unref(e.value.data[websocketData.cause_of_death]).max_distance)
})
}, 100)
+75 -45
View File
@@ -1,10 +1,22 @@
import { defineStore } from 'pinia'
import * as assert from 'assert'
import { Ref, reactive, ref, shallowRef, triggerRef, unref } from 'vue'
function removeNullEntries (a:any) {
return Object.fromEntries(Object.entries(a).filter(e => e[1] !== undefined && e[1] !== null))
export interface Filters {
server?: string;
player?: string;
weapon?: string;
host?: string;
map?: string;
gamemode?: string;
}
function objectEqual (a:any, b:any) {
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)
@@ -14,14 +26,6 @@ function objectEqual (a:any, b:any) {
return false
}
}
export interface Filters {
server?: string;
player?: string;
weapon?: string;
host?: string;
map?: string;
gamemode?: string;
}
export interface Kill {
deaths: number;
@@ -33,13 +37,13 @@ export interface Kill {
}
export interface KillData<T extends Kill> {
filter: Filters;
data: { [key: string]: T };
data: { [key: string]: Ref<T> };
}
export interface Player extends Kill {
username: string;
// eslint-disable-next-line camelcase
deaths_while_equipped?: number;
// deaths_while_equipped?: number;
}
export interface Weapon extends Kill {
@@ -53,11 +57,11 @@ export interface Server extends Kill {
// define your typings for the store state
export interface State {
servers: KillData<Server>[];
players: KillData<Player>[];
weapons: KillData<Weapon>[];
maps: KillData<Kill>[];
gamemodes: KillData<Kill>[];
servers: Ref<KillData<Server>>[];
players: Ref<KillData<Player>>[];
weapons: Ref<KillData<Weapon>>[];
maps: Ref<KillData<Kill>>[];
gamemodes: Ref<KillData<Kill>>[];
hosts: { [key: number]: string };
}
@@ -71,43 +75,69 @@ export const useKillStore = defineStore('kill', {
hosts: {}
}),
getters: {
getPlayerList: (state) => (filters: Filters) => state.players.find((e) => objectEqual(e.filter, filters)),
getWeaponList: (state) => (filters: Filters) => state.weapons.find((e) => objectEqual(e.filter, filters)),
getServerList: (state) => (filters: Filters) => state.servers.find((e) => objectEqual(e.filter, filters))
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))
},
actions: {
async fetchPlayers (filter: Filters) {
fetchPlayers (filter: Filters) {
filter = removeNullEntries(filter)
const response = await fetch(
'https://tone.sleepycat.date/v2_test/client/players?' +
let entry = this.players.find((e) => objectEqual(unref(e).filter, filter))
if (entry === undefined) {
entry = shallowRef({ filter, data: {} })
this.players.push(entry)
}
fetch(
'https://tone.sleepycat.date/v2/client/players?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
)
const data = await response.json()
const entry = this.players.find((e) => objectEqual(e.filter, filter))
if (!entry) return this.players.push({ filter, data })
entry.data = data
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Player)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
triggerRef(entry!)
})
return entry
},
async fetchWeapons (filter: Filters) {
fetchWeapons (filter: Filters) {
filter = removeNullEntries(filter)
const response = await fetch(
'https://tone.sleepycat.date/v2_test/client/weapons?' +
let entry = this.weapons.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.weapons.push(entry)
}
fetch(
'https://tone.sleepycat.date/v2/client/weapons?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
)
const data = await response.json()
const entry = this.weapons.find((e) => objectEqual(e.filter, filter))
if (!entry) return this.weapons.push({ filter, data })
entry.data = data
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Weapon)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
triggerRef(entry!)
})
return entry
},
async fetchServers (filter: Filters) {
fetchServers (filter: Filters) {
filter = removeNullEntries(filter)
const response = await fetch(
'https://tone.sleepycat.date/v2_test/client/servers?' +
let entry = this.servers.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.servers.push(entry)
}
fetch(
'https://tone.sleepycat.date/v2/client/servers?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
)
const data = await response.json()
const entry = this.servers.find((e) => objectEqual(e.filter, filter))
if (!entry) return this.servers.push({ filter, data })
entry.data = data
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Server)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
triggerRef(entry!)
})
return entry
}
}
})
+76
View File
@@ -0,0 +1,76 @@
{
"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"
}
+184 -54
View File
@@ -11,25 +11,73 @@
<option></option>
<option v-for="weaponId in sortedWeaponList" v-bind:key="weaponId" :value="weaponId">{{ weaponId }}</option>
</select> -->
<span class="multiselect-wrapper">
<VueMultiselect
selectLabel=""
deselectLabel="remove"
placeholder="Select server"
v-model="model.server"
:options="groupedServers"
group-values="servers"
group_label="id"
:group-select="false"
:allow-empty="true"
:multiple="false"
:custom-label="((e: any) => e.name)"
track-by="name"
label="name"
>
</VueMultiselect>
<button @click="model.server = undefined" :disabled="!model.server">X</button>
</span>
<VueMultiselect selectLabel="" deselectLabel="remove" placeholder="Select server" v-model="model.server"
:options="groupedServers" group-values="servers" group_label="id" :group-select="false" :allow-empty="true" :multiple="false" :custom-label="((e:any) => e.name)" track-by="name" label="name"
<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>
<VueMultiselect selectLabel="" deselectLabel="remove" placeholder="Select weapon" v-model="model.weapon"
:options="sortedWeaponList" :allow-empty="true"
></VueMultiselect>
<VueMultiselect selectLabel="" deselectLabel="remove" placeholder="Search player" v-model="playerHighlighted"
:options="sortedPlayerList" :allow-empty="true" :custom-label="((e:string) => players[e]?.username)"></VueMultiselect>
<button @click="playerHighlighted = undefined" :disabled="!playerHighlighted">X</button>
</span>
</div>
<div id="playerView">
<PlayerList :filters="filters" v-on:highlight-player="playerHighlighted = $event"
:playerHighlighted="playerHighlighted"></PlayerList>
<PlayerChart :filters="filters" v-on:highlight-player="playerHighlighted = $event"
:playerHighlighted="playerHighlighted"></PlayerChart>
<WeaponChart :filters="{player: playerHighlighted, ...filters}" :playerHighlighted="playerHighlighted"></WeaponChart>
<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>
@@ -38,58 +86,75 @@ 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 { defineComponent } from 'vue'
import { Ref, defineComponent, toRaw, 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
VueMultiselect,
PlayerList,
PlayerChart,
WeaponChart
},
data () {
return {
model: { server: undefined, weapon: undefined } as unknown as {weapon:Weapon | Weapon[], server: (Server& {name?:string}) | (Server& {name?:string})[]},
model: { server: undefined, weapon: undefined } as unknown as {
weapon: string | undefined;
server: { name?: string } | (Server & { name?: string })[] | undefined;
},
filters: {} as Filters,
store: useKillStore(),
playerHighlighted: undefined
playerHighlighted: undefined as (Player & { id: string }) | undefined,
weaponLocale: weapons as { [key: string]: string }
}
},
created () {
let server
if (this.$route.query.server) server = { name: this.$route.query.server?.toString() }
this.model = {
server,
weapon: this.$route.query.weapon?.toString()
}
},
computed: {
servers (): { [key: string]: Server } {
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)?.data
if (!data) {
this.store.fetchServers(withoutServer)
return {}
}
const data = this.store.getServerList(withoutServer)?.value.data
if (!data) return unref(this.store.fetchServers(withoutServer)).data
return data
},
groupedServers () {
const hosts = [] as {id:string, servers:(Server& {name?:string})[] }[]
const hosts = [] as {
id: string;
servers: (Server & { name?: string })[];
}[]
Object.entries(this.servers).forEach((e) => {
if (!hosts.find(host => host.id === e[1].host + 'sfdsdfsd')) hosts.push({ id: e[1].host + 'sfdsdfsd', servers: [] as Server[] })
hosts.find(host => host.id === e[1].host + 'sfdsdfsd')?.servers.push({ name: e[0], ...e[1] })
const server = unref(e[1])
if (!hosts.find((host) => host.id === server.host + 'sfdsdfsd')) { hosts.push({ id: server.host + 'sfdsdfsd', servers: [] as Server[] }) }
hosts
.find((host) => host.id === server.host + 'sfdsdfsd')
?.servers.push({ name: e[0], ...server })
})
return hosts
},
weapons (): { [key: string]: Weapon } {
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)?.data
if (!data) {
this.store.fetchWeapons(withoutWeapons)
return {}
}
const data = this.store.getWeaponList(withoutWeapons)?.value.data
if (!data) return this.store.fetchWeapons(withoutWeapons).value.data
return data
},
players (): { [key: string]: Player } {
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)?.data
if (!data) {
this.store.fetchPlayers(withoutPlayers)
return {}
}
const data = this.store.getPlayerList(withoutPlayers)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayers).value.data
return data
},
sortedWeaponList (): string[] {
@@ -97,22 +162,22 @@ export default defineComponent({
const weapons = Object.keys(this.weapons)
return weapons.sort()
},
sortedServerList ():string[] {
sortedServerList (): string[] {
if (!this.servers) return []
const servers = Object.keys(this.servers)
return servers.sort()
},
sortedPlayerList (): string[] {
sortedPlayerList (): Player[] {
if (!this.players) return []
const players = Object.keys(this.players)
return players.sort((a: string, b: string) => {
if (!this.players) {
return 0
}
if (this.players[a].username < this.players[b].username) {
const players = Object.entries(this.players).map((e) => ({
id: e[0],
...toRaw(e[1].value)
}))
return players.sort((a: Player, b: Player) => {
if (a.username < b.username) {
return -1
}
if (this.players[a].username > this.players[b].username) {
if (a.username > b.username) {
return 1
}
return 0
@@ -120,12 +185,57 @@ export default defineComponent({
}
},
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?.name
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' (to) {
if (this.playerHighlighted?.id !== to.query.player) {
const playerId = to.query.player?.toString()
if (playerId) {
this.highlight_player(playerId)
}
}
if (this.filters.weapon !== to.query.weapon) {
this.model.weapon = to.query.weapon
}
if (this.filters.server !== to.query.server) {
if (to.query.server) this.model.server = { name: to.query.server }
else this.model.server = undefined
}
}
},
methods: {
highlight_player (playerid: string) {
const player = this.players[playerid]
if (player) {
this.playerHighlighted = { id: playerid, ...unref(player) }
return
}
this.playerHighlighted = undefined
}
}
})
@@ -133,7 +243,16 @@ export default defineComponent({
<style scoped>
.multiselect {
margin: 0 .5em 1em .5em;
margin: 0 0.5em 1em 0.5em;
}
.multiselect-wrapper {
display: flex;
}
.multiselect-wrapper button {
margin: 0 0 1em 0;
font-size: 16px;
}
#playerView {
@@ -142,8 +261,8 @@ export default defineComponent({
display: grid;
overflow: auto;
grid-template-areas:
'list chart'
'list info';
"list chart"
"list info";
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
}
@@ -154,16 +273,27 @@ export default defineComponent({
/* height:100%; */
display: grid;
overflow: auto;
grid-template-areas:
'list list';
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)
width: calc(100% - 1em);
}
.playerChart {
display: none;
}
.weaponChart {
display: none;
}
}
+1
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"resolveJsonModule":true,
"target": "esnext",
"module": "esnext",
"strict": true,