Update to ingest/display new stats format; QoL updates soon...

This commit is contained in:
Anthony Mineo 2020-08-15 12:02:05 -04:00
parent bb179b82f8
commit 0b5aceca7a
9 changed files with 497 additions and 440 deletions

2
.gitignore vendored
View file

@ -4,6 +4,8 @@ node_modules
notes.md notes.md
app/t2-stat-parser/serverStats/stats app/t2-stat-parser/serverStats/stats
app/t2-stat-parser/serverStats/mlData
app/t2-stat-parser/serverStats/lData
docker-compose.deploy.yml docker-compose.deploy.yml
traefik.yml traefik.yml

View file

@ -14,9 +14,9 @@ import (
) )
func main() { func main() {
fmt.Println("Starting FTP stat file download") // fmt.Println("Starting FTP stat file download")
initFTP() initFTP()
fmt.Println("Stat files downloaded!") // fmt.Println("Stat files downloaded!")
fmt.Println("Starting stat parser") fmt.Println("Starting stat parser")
initParser() initParser()

View file

@ -59,12 +59,12 @@ type Game struct {
dbStatOverWrite int `db.players:"stat_overwrite"` dbStatOverWrite int `db.players:"stat_overwrite"`
statOverWrite int statOverWrite int
gameMap string `db.games:"map"` gameMap string `db.game_detail:"map"`
gameID int `db.games:"game_id"` gameID int `db.game_detail:"game_id"`
gameType string `db.games:"gametype"` gameType string `db.game_detail:"gametype"`
dateStamp string `db.games:"datestamp"` dateStamp string `db.game_detail:"datestamp"`
stats string `db.games:"stats"` stats string `db.game_detail:"stats"`
uuid string `db.games:"uuid"` uuid string `db.game_detail:"uuid"`
} }
func initParser() { func initParser() {
@ -265,7 +265,10 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
fmt.Println(g) fmt.Println(g)
} }
if checkGameEntry(g) == false && g.gameID != 0 { // Check if we need to create a new game record
checkGameRecord(g)
if checkGameEntryForPlayer(g) == false && g.gameID != 0 {
fmt.Println("does not exist, add") fmt.Println("does not exist, add")
// insert game stat // insert game stat
addPlayerGameStat(g, strings.ToLower(gt)) addPlayerGameStat(g, strings.ToLower(gt))
@ -283,6 +286,28 @@ func rowExists(query string, args ...interface{}) bool {
return exists return exists
} }
func checkGameRecord(g Game) {
check := rowExists("select game_id from games where game_id = $1 and map = $2", g.gameID, g.gameMap)
if !check {
createGame(g)
} else {
fmt.Println("Game Record ", g.gameID, g.gameMap, " already exists")
}
}
func createGame(g Game) {
fmt.Println("Creating new Game ", g.gameMap, g.gameID, g.dateStamp, g.gameType)
if g.gameID != 0 {
sqlInsert := `insert into games(map, game_id, datestamp, gametype) values($1,$2,$3,$4)`
_, err := db.Exec(sqlInsert, g.gameMap, g.gameID, g.dateStamp, g.gameType)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to create new game record (Possible Dupe ID): %v\n", err)
// Don't exit - just skip this insert
// os.Exit(1)
}
}
}
func checkPlayer(g Game) { func checkPlayer(g Game) {
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID) check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
if !check { if !check {
@ -292,8 +317,8 @@ func checkPlayer(g Game) {
} }
} }
func checkGameEntry(g Game) bool { func checkGameEntryForPlayer(g Game) bool {
check := rowExists("select player_guid from games where player_guid = $1 and game_id = $2 and map = $3", g.playerGUID, g.gameID, g.gameMap) check := rowExists("select player_guid from game_detail where player_guid = $1 and game_id = $2 and map = $3", g.playerGUID, g.gameID, g.gameMap)
if !check { if !check {
return false return false
} else { } else {
@ -341,7 +366,7 @@ func addPlayerGameStat(g Game, gt string) {
if g.dateStamp != "0" { if g.dateStamp != "0" {
// Insert new stat line // Insert new stat line
fmt.Println("New stat line!", g.playerName, g.dateStamp) fmt.Println("New stat line!", g.playerName, g.dateStamp)
sqlInsert := `insert into games(player_guid, player_name, stat_overwrite, map, game_id, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8,$9)` sqlInsert := `insert into game_detail(player_guid, player_name, stat_overwrite, map, game_id, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8,$9)`
_, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.stats, g.dateStamp, g.uuid, g.gameType) _, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.stats, g.dateStamp, g.uuid, g.gameType)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err) fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)

View file

@ -0,0 +1,7 @@
./lData
./mlData
./stats/CTFGame
./stats/DMGame
./stats/LakRabbitGame
./stats/SCTF/Game

View file

@ -1,71 +1,64 @@
'use strict' 'use strict';
const Database = use('Database') const Database = use('Database');
class GameController { class GameController {
// /games
async index({ inertia }) {
const pageTitle = `Latest Games`;
// /games // const gamesQry = await Database.raw(`
async index({ inertia }) { // SELECT distinct game_id,map,gametype, datestamp
// FROM games
// WHERE (stats->>'score' IS NOT NULL) AND (game_id <> 0)
// ORDER BY game_id desc
// LIMIT 2000;
// `);
const pageTitle = `Latest Games`; // const gamesQry = await Database.raw(`
// WITH gamelist AS (
// SELECT DISTINCT p.game_id, p.map, p.gametype, p.datestamp, count(*) OVER (PARTITION BY game_id) as count from games p
// )
// SELECT * from gamelist
// WHERE (count > 1) AND (game_id <> 0)
// ORDER BY game_id desc
// LIMIT 2000
// `);
// const gamesQry = await Database.raw(` // // filter out duplicate game_ids (https://dev.to/marinamosti/removing-duplicates-in-an-array-of-objects-in-js-with-sets-3fep)
// SELECT distinct game_id,map,gametype, datestamp // const games = gamesQry.rows.reduce((game, current) => {
// FROM games
// WHERE (stats->>'score' IS NOT NULL) AND (game_id <> 0)
// ORDER BY game_id desc
// LIMIT 2000;
// `);
// const x = game.find(item => item.game_id === current.game_id);
// if (!x) {
// return game.concat([current]);
// } else {
// return game;
// }
// }, []);
const gamesQry = await Database.raw(` const games = await Database.from('games').orderBy('game_id', 'desc').limit(200);
WITH gamelist AS ( // const CTFgames = await Database.from('games').where({gametype: 'CTFGame'}).orderBy('game_id', 'desc').limit(120);
SELECT DISTINCT p.game_id, p.map, p.gametype, p.datestamp, count(*) OVER (PARTITION BY game_id) as count from games p // const LAKgames = await Database.from('games').where({gametype: 'LakRabbitGame'}).orderBy('game_id', 'desc').limit(120);
)
SELECT * from gamelist
WHERE (count > 1) AND (game_id <> 0)
ORDER BY game_id desc
LIMIT 2000
`);
// filter out duplicate game_ids (https://dev.to/marinamosti/removing-duplicates-in-an-array-of-objects-in-js-with-sets-3fep) // move the 0 score display logic here
const games = gamesQry.rows.reduce((game, current) => {
const x = game.find(item => item.game_id === current.game_id); return inertia.render('Games/Main', { pageTitle, games }, { edgeVar: 'server-variable' });
if (!x) { }
return game.concat([current]);
} else {
return game;
}
}, []);
// move the 0 score display logic here
return inertia.render('Games/Main', { pageTitle, games }, { edgeVar: 'server-variable' })
}
// game/:game_id
async game({ inertia, request }) {
const gameInfo = await Database.from('games')
.distinct('game_id',
'map',
'player_name',
'player_guid',
'gametype',
'stats',
'datestamp')
.where({ game_id: request.params.game_id })
const pageTitle = {
name: gameInfo[0]['map'],
gametype: gameInfo[0]['gametype']
}
return inertia.render('Games/Game', { pageTitle, gameInfo }, { edgeVar: 'server-variable' })
}
// game/:game_id
async game({ inertia, request }) {
const gameInfo = await Database.from('game_detail')
.select('game_id', 'map', 'player_name', 'player_guid', 'gametype', 'stats', 'datestamp')
.where({ game_id: request.params.game_id })
.orderByRaw("stats->>'scoreTG' desc");
const pageTitle = {
name: gameInfo[0]['map'],
gametype: gameInfo[0]['gametype']
};
return inertia.render('Games/Game', { pageTitle, gameInfo }, { edgeVar: 'server-variable' });
}
} }
module.exports = GameController module.exports = GameController;

View file

@ -1,84 +1,82 @@
'use strict' 'use strict';
const Database = use('Database') const Database = use('Database');
class PlayerController { class PlayerController {
// Main Players List
async index({ inertia }) {
const pageTitle = 'All Players';
const players = await Database.table('players')
.distinct(
'player_guid',
'player_name',
'total_games_ctfgame',
'total_games_dmgame',
'total_games_lakrabbitgame',
'total_games_sctfgame',
'updated_at'
)
.groupBy('player_guid')
.orderBy('player_name', 'asc')
.limit(1000);
// Main Players List return inertia.render('Players/Main', { pageTitle, players }, { edgeVar: 'server-variable' });
async index({ inertia }) { }
const pageTitle = "All Players"
const players = await Database.table('players')
.distinct('player_guid',
'player_name',
'total_games_ctfgame',
'total_games_dmgame',
'total_games_lakrabbitgame',
'total_games_sctfgame',
'updated_at')
.groupBy('player_guid')
.orderBy('player_name', 'asc')
.limit(1000)
return inertia.render('Players/Main', { pageTitle, players }, { edgeVar: 'server-variable' }) // Player Detail
} async player({ inertia, request }) {
const playerInfo = await Database.from('players')
.distinct(
'player_guid',
'player_name',
'total_games_ctfgame',
'total_games_dmgame',
'total_games_lakrabbitgame',
'total_games_sctfgame'
)
.where({ player_guid: request.params.player_guid })
.limit(50);
// Player Detail const playerStatData = await Database.from('game_detail')
async player({ inertia, request }) { .select('game_id', 'gametype', 'stats')
const playerInfo = await Database.from('players') .where({ player_guid: request.params.player_guid })
.distinct('player_guid', .orderBy('game_id', 'desc')
'player_name', .limit(50);
'total_games_ctfgame',
'total_games_dmgame',
'total_games_lakrabbitgame',
'total_games_sctfgame')
.where({ player_guid: request.params.player_guid })
.limit(50)
// Dynamically generate and sum the stats object
let playerStatTotals = {},
statKeys = Object.keys(playerStatData[0].stats);
const playerStatData = await Database.from('games') for (let i = 0; i < statKeys.length; i++) {
.select('game_id', if (statKeys[i] === 'map' || statKeys[i] === 'dateStamp' || statKeys[i] === 'timeDayMonth') {
'gametype', continue;
'stats') }
.where({ player_guid: request.params.player_guid }) playerStatTotals[statKeys[i]] = 0;
.orderBy('game_id', 'desc') }
.limit(50)
// Loop through the playerStatsData query from the DB
playerStatData.map((statLine) => {
// look through each object in playerStatsData array
for (let [ key, value ] of Object.entries(statLine.stats)) {
// console.log(`${key}: ${value}`);
// If the stat item exists, add it -- if not create a new key in playerStatTotals
if (playerStatTotals.hasOwnProperty(key) === true) {
playerStatTotals[key] = playerStatTotals[key] + Number(value);
} else {
playerStatTotals[key] = Number(value);
}
}
});
// Dynamically generate and sum the stats object let playerData = {
let playerStatTotals = {}, player: playerInfo[0],
statKeys = Object.keys(playerStatData[0].stats) stats: playerStatData,
totals: playerStatTotals
};
for(let i = 0 ; i < statKeys.length; i++) { const pageTitle = playerData.player.player_name;
if(statKeys[i] === "map" || return inertia.render('Players/Player', { pageTitle, playerData }, { edgeVar: 'server-variable' });
statKeys[i] === "dateStamp" || }
statKeys[i] === "timeDayMonth" ){continue;}
playerStatTotals[statKeys[i]] = 0;
}
// Loop through the playerStatsData query from the DB
playerStatData.map(statLine => {
// look through each object in playerStatsData array
for (let [key, value] of Object.entries(statLine.stats)) {
// console.log(`${key}: ${value}`);
// If the stat item exists, add it -- if not create a new key in playerStatTotals
if(playerStatTotals.hasOwnProperty(key) === true){
playerStatTotals[key] = playerStatTotals[key] + Number(value);
}else{
playerStatTotals[key] = Number(value);
}
}
})
let playerData = {
player: playerInfo[0],
stats: playerStatData,
totals: playerStatTotals
}
const pageTitle = playerData.player.player_name
return inertia.render('Players/Player', { pageTitle, playerData }, { edgeVar: 'server-variable' })
}
} }
module.exports = PlayerController module.exports = PlayerController;

View file

@ -1,170 +1,171 @@
import React, {PureComponent} from 'react' import React, { PureComponent } from 'react';
import { InertiaLink } from '@inertiajs/inertia-react' import { InertiaLink } from '@inertiajs/inertia-react';
import Layout from '@/Shared/Layout' import Layout from '@/Shared/Layout';
import {PieChart, Pie, Sector} from 'recharts' import { PieChart, Pie, Sector } from 'recharts';
const renderActiveShape = (props) => { const renderActiveShape = (props) => {
const RADIAN = Math.PI / 180; const RADIAN = Math.PI / 180;
const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props;
fill, payload, percent, value } = props; const sin = Math.sin(-RADIAN * midAngle);
const sin = Math.sin(-RADIAN * midAngle); const cos = Math.cos(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle); const sx = cx + (outerRadius + 10) * cos;
const sx = cx + (outerRadius + 10) * cos; const sy = cy + (outerRadius + 10) * sin;
const sy = cy + (outerRadius + 10) * sin; const mx = cx + (outerRadius + 30) * cos;
const mx = cx + (outerRadius + 30) * cos; const my = cy + (outerRadius + 30) * sin;
const my = cy + (outerRadius + 30) * sin; const ex = mx + (cos >= 0 ? 1 : -1) * 22;
const ex = mx + (cos >= 0 ? 1 : -1) * 22; const ey = my;
const ey = my; const textAnchor = cos >= 0 ? 'start' : 'end';
const textAnchor = cos >= 0 ? 'start' : 'end';
return ( return (
<g> <g>
<text x={cx} y={cy} dy={8} textAnchor="middle" fill="#5850ec">{payload.name}</text> <text x={cx} y={cy} dy={8} textAnchor="middle" fill="#5850ec">
<Sector {payload.name}
cx={cx} </text>
cy={cy} <Sector
innerRadius={innerRadius} cx={cx}
outerRadius={outerRadius} cy={cy}
startAngle={startAngle} innerRadius={innerRadius}
endAngle={endAngle} outerRadius={outerRadius}
fill="#8884d8" startAngle={startAngle}
/> endAngle={endAngle}
<Sector fill="#8884d8"
cx={cx} />
cy={cy} <Sector
startAngle={startAngle} cx={cx}
endAngle={endAngle} cy={cy}
innerRadius={outerRadius + 6} startAngle={startAngle}
outerRadius={outerRadius + 10} endAngle={endAngle}
fill="#6761d6" innerRadius={outerRadius + 6}
/> outerRadius={outerRadius + 10}
<path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none"/> fill="#6761d6"
<circle cx={ex} cy={ey} r={2} fill={fill} stroke="none"/> />
<text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#333">{`${value}`}</text> <path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none" />
<text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#999"> <circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
{`(${(percent * 100).toFixed(2)}%)`} <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#333">{`${value}`}</text>
</text> <text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#999">
</g> {`(${(percent * 100).toFixed(2)}%)`}
); </text>
</g>
);
}; };
export class TwoLevelPieChart extends PureComponent { export class TwoLevelPieChart extends PureComponent {
state = { state = {
activeIndex: ((this.props.data.oScore >= this.props.data.dScore) ? 0 : 1 ), activeIndex: this.props.data.oScore >= this.props.data.dScore ? 0 : 1,
data: [{name: 'Offense', value: this.props.data.oScore}, data: [ { name: 'Offense', value: this.props.data.oScore }, { name: 'Defense', value: this.props.data.dScore } ]
{name: 'Defense', value: this.props.data.dScore}] };
};
onPieEnter = (data, index) => { onPieEnter = (data, index) => {
this.setState({ this.setState({
activeIndex: index, activeIndex: index
}); });
}; };
render() { render() {
return ( return (
<PieChart width={400} height={400}> <PieChart width={400} height={400}>
<Pie <Pie
activeIndex={this.state.activeIndex} activeIndex={this.state.activeIndex}
activeShape={renderActiveShape} activeShape={renderActiveShape}
data={this.state.data} data={this.state.data}
cx={200} cx={200}
cy={200} cy={200}
innerRadius={60} innerRadius={60}
outerRadius={80} outerRadius={80}
fill="#ccc" fill="#ccc"
dataKey="value" dataKey="value"
onMouseEnter={this.onPieEnter} onMouseEnter={this.onPieEnter}
className="text-xs" className="text-xs"
/> />
</PieChart> </PieChart>
); );
} }
} }
const PlayerRow = (player, index) => { const PlayerRow = (player, index) => {
// dont show scoreless players // dont show scoreless players
//if (Number(player.stats.score) <= 0){return} // if (Number(player.stats.scoreTG) <= 0) {
// return;
return <div className="flex flex-col rounded-lg shadow-lg overflow-hidden" key={index}> // }
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 className="text-lg leading-6 font-medium">
<InertiaLink href={`/player/${player.player_guid}`} className="text-indigo-600 hover:text-indigo-500 transition duration-150 ease-in-out">{player.player_name}</InertiaLink>
</h3>
</div>
<div>
<dl>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Total Score
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.score}
</dd>
</div>
<div className="bg-gray-50 flex items-center justify-center">
{
(player.gametype == "CTFGame" || player.gametype == "SCtFGame") ? <TwoLevelPieChart data={{
oScore: Number(player.stats.offenseScore[0]),
dScore: Number(player.stats.defenseScore[0])}
}/>
: ''
}
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Kills / Assists
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.kills} / {player.stats.assist}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
MAs
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.totalMA}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Flag Grabs / Caps
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagGrabs} / {player.stats.flagCaps}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Flag Defends / Carrier Kills / Returns
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagDefends} / {player.stats.carrierKills} / {player.stats.flagReturns}
</dd>
</div>
</dl>
</div>
</div>
</div>;
}
return (
<div className="flex flex-col rounded-lg shadow-lg overflow-hidden" key={index}>
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 className="text-lg leading-6 font-medium">
<InertiaLink
href={`/player/${player.player_guid}`}
className="text-indigo-600 hover:text-indigo-500 transition duration-150 ease-in-out"
>
{player.player_name}
</InertiaLink>
</h3>
</div>
<div>
<dl>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Total Score</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.scoreTG}
</dd>
</div>
<div className="bg-gray-50 flex items-center justify-center">
{player.gametype == 'CTFGame' || player.gametype == 'SCtFGame' ? (
<TwoLevelPieChart
data={{
oScore: Number(player.stats.offenseScoreTG[0]),
dScore: Number(player.stats.defenseScoreTG[0])
}}
/>
) : (
''
)}
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Kills / Assists</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.killsTG} / {player.stats.assistTG}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">MAs</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.totalMATG}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Flag Grabs / Caps</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagGrabsTG} / {player.stats.flagCapsTG}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Flag Defends / Capper Kills / Returns
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagDefendsTG} / {player.stats.carrierKillsTG} /{' '}
{player.stats.flagReturnsTG}
</dd>
</div>
</dl>
</div>
</div>
</div>
);
};
export default function Game(props) { export default function Game(props) {
return ( return (
<Layout title={props.pageTitle.name} gametype={props.pageTitle.gametype}> <Layout title={props.pageTitle.name} gametype={props.pageTitle.gametype}>
<div className="mt-2 grid gap-5 max-w-lg mx-auto lg:grid-cols-3 lg:max-w-none">
<div className="mt-2 grid gap-5 max-w-lg mx-auto lg:grid-cols-3 lg:max-w-none"> {props.gameInfo.map((game, index) => PlayerRow(game, index))}
{ props.gameInfo.map((game, index) => PlayerRow(game, index)) } </div>
</div> {/*
{/*
<div className="bg-white shadow overflow-hidden sm:rounded-md"> <div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="py-10 px-10"><code> {JSON.stringify(props.gameInfo)}</code></div> <div className="py-10 px-10"><code> {JSON.stringify(props.gameInfo)}</code></div>
</div> */} </div> */}
</Layout> </Layout>
) );
} }

View file

@ -1,166 +1,170 @@
import React from 'react' import React from 'react';
import { InertiaLink } from '@inertiajs/inertia-react' import { InertiaLink } from '@inertiajs/inertia-react';
import Layout from '@/Shared/Layout' import Layout from '@/Shared/Layout';
import GameTypesPlayedCols from '@/Components/GameTypesPlayedCols' import GameTypesPlayedCols from '@/Components/GameTypesPlayedCols';
import {Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis} from 'recharts'
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis } from 'recharts';
const returnWeaponTotals = (statTotals) => { const returnWeaponTotals = (statTotals) => {
let totals = [ let totals = [
{ weapon: 'Chaingun', val: statTotals["chainKills"]}, { weapon: 'Chaingun', val: statTotals['cgKillsTG'] },
{ weapon: 'Disc', val: statTotals["discKills"] }, { weapon: 'Disc', val: statTotals['discKillsTG'] },
{ weapon: 'Grenade Launcher', val: statTotals["grenadeKills"]}, { weapon: 'Grenade Launcher', val: statTotals['grenadeKillsTG'] },
{ weapon: 'Shocklance', val: statTotals["shockLanceKills"]}, { weapon: 'Shocklance', val: statTotals['shockKillsTG'] },
{ weapon: 'Laser Rifle', val: statTotals["laserKills"]}, { weapon: 'Laser Rifle', val: statTotals['laserKillsTG'] },
{ weapon: 'Blaster', val: statTotals["blasterKills"]}, { weapon: 'Blaster', val: statTotals['blasterKillsTG'] },
{ weapon: 'Plasma Rifle', val: statTotals["plasmaKills"]}, { weapon: 'Plasma Rifle', val: statTotals['plasmaKillsTG'] },
{ weapon: 'Mortar Launcher', val: statTotals["mortarKills"]}, { weapon: 'Mortar Launcher', val: statTotals['mortarKillsTG'] },
{ weapon: 'Missile Launcher', val: statTotals["missileKills"]}, { weapon: 'Missile Launcher', val: statTotals['missileKillsTG'] },
{ weapon: 'Hand Grenade', val: statTotals["hGrenadeKills"]}, { weapon: 'Hand Grenade', val: statTotals['hGrenadeKillsTG'] },
{ weapon: 'Mine', val: statTotals["mineKills"]}, { weapon: 'Mine', val: statTotals['mineKillsTG'] },
{ weapon: 'Satchel', val: statTotals["satchelChargeKills"]}, { weapon: 'Satchel', val: statTotals['satchelKillsTG'] }
]; ];
// dont return if the val is 0 // dont return if the val is 0
return totals.filter(function (el) { return totals.filter(function(el) {
return el.val > 0; return el.val > 0;
}); });
}; };
const GameCard = (player, index) => { const GameCard = (player, index) => {
// only display card if player has score // only display card if player has score
// if (Number(player.stats.score) <= 0){return} // if (Number(player.stats.score) <= 0){return}
return <div key={index} className="bg-white shadow overflow-hidden sm:rounded-lg mb-5">
<div className="px-4 py-5 border-b border-gray-200 sm:px-6">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-no-wrap">
<div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
<InertiaLink href={`/game/${player.game_id}`} className="text-indigo-600 hover:text-indigo-500 transition duration-150 ease-in-out">{player.stats.map}</InertiaLink>
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium leading-4 bg-gray-100 text-gray-800">{player.stats.dateStamp}</span>
</p>
</div>
<div className="ml-4 mt-4 flex-shrink-0">
<span className="inline-flex items-center px-3 py-0.5 rounded-full text-xs font-medium leading-5 bg-indigo-100 text-indigo-800">
{ player.gametype }
</span>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Total Score
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.score}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Kills / Assists
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.kills} / {player.stats.assist}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
MAs
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.totalMA}
</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Flag Grabs / Caps
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagGrabs} / {player.stats.flagCaps}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">
Flag Defends / Carrier Kills / Returns
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagDefends} / {player.stats.carrierKills} / {player.stats.flagReturns}
</dd>
</div>
</div>;
}
return (
<div key={index} className="bg-white shadow overflow-hidden sm:rounded-lg mb-5">
<div className="px-4 py-5 border-b border-gray-200 sm:px-6">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-no-wrap">
<div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
<InertiaLink
href={`/game/${player.game_id}`}
className="text-indigo-600 hover:text-indigo-500 transition duration-150 ease-in-out"
>
{player.stats.map}
</InertiaLink>
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium leading-4 bg-gray-100 text-gray-800">
{player.stats.dateStamp}
</span>
</p>
</div>
<div className="ml-4 mt-4 flex-shrink-0">
<span className="inline-flex items-center px-3 py-0.5 rounded-full text-xs font-medium leading-5 bg-indigo-100 text-indigo-800">
{player.gametype}
</span>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Total Score</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">{player.stats.scoreTG}</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Kills / Assists</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.killsTG} / {player.stats.assistTG}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">MAs</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">{player.stats.totalMATG}</dd>
</div>
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Flag Grabs / Caps</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagGrabsTG} / {player.stats.flagCapsTG}
</dd>
</div>
<div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm leading-5 font-medium text-gray-500">Flag Defends / Carrier Kills / Returns</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{player.stats.flagDefendsTG} / {player.stats.carrierKillsTG} / {player.stats.flagReturnsTG}
</dd>
</div>
</div>
);
};
export default function Player(props) { export default function Player(props) {
return ( return (
<Layout title={props.pageTitle}> <Layout title={props.pageTitle}>
<div className="md:grid md:grid-cols-4 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6 text-gray-900">Aggregate</h3>
<p className="mt-1 text-sm leading-5 text-gray-500">Stat Totals</p>
</div>
</div>
<div className="md:grid md:grid-cols-4 md:gap-6"> <div className="mt-5 md:mt-0 md:col-span-3">
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<div className="text-sm leading-5 font-medium text-gray-500">Games Played</div>
<div className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<GameTypesPlayedCols
ctf={props.playerData.player.total_games_ctfgame}
dm={props.playerData.player.total_games_dmgame}
lak={props.playerData.player.total_games_lakrabbitgame}
spawnctf={props.playerData.player.total_games_sctfgame}
/>
</div>
</div>
<div className="bg-gray-50 px-4 py-5">
<dt className="text-sm leading-5 font-medium text-gray-500">Weapon Usage</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 flex items-center justify-center">
<RadarChart
cx={300}
cy={250}
outerRadius={150}
width={600}
height={500}
data={
returnWeaponTotals(props.playerData.totals).length ? (
returnWeaponTotals(props.playerData.totals)
) : (
[ { weapon: 'No Data', val: 1 } ]
)
}
className="text-xs"
>
<PolarGrid />
<PolarAngleAxis dataKey="weapon" />
<PolarRadiusAxis />
<Radar
name={props.playerData.player.player_name}
dataKey="val"
stroke="#8884d8"
fill="#8884d8"
fillOpacity={0.6}
/>
</RadarChart>
</dd>
</div>
</div>
</div>
</div>
<div className="md:col-span-1"> <div className="md:grid md:grid-cols-4 md:gap-6 mt-10">
<div className="px-4 sm:px-0"> <div className="md:col-span-1">
<h3 className="text-lg font-medium leading-6 text-gray-900">Aggregate</h3> <div className="px-4 sm:px-0">
<p className="mt-1 text-sm leading-5 text-gray-500"> <h3 className="text-lg font-medium leading-6 text-gray-900">Game History</h3>
Stat Totals <p className="mt-1 text-sm leading-5 text-gray-500">
</p> Stats for past {props.playerData.stats.length} games
</div> </p>
</div> </div>
</div>
<div className="mt-5 md:mt-0 md:col-span-3"> <div className="mt-5 md:mt-0 md:col-span-3">
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> {props.playerData.stats.map((player, index) => GameCard(player, index))}
<div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> </div>
<div className="text-sm leading-5 font-medium text-gray-500"> </div>
Games Played {/*
</div>
<div className="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<GameTypesPlayedCols ctf={props.playerData.player.total_games_ctfgame}
dm={props.playerData.player.total_games_dmgame}
lak={props.playerData.player.total_games_lakrabbitgame}
spawnctf={props.playerData.player.total_games_sctfgame}/>
</div>
</div>
<div className="bg-gray-50 px-4 py-5">
<dt className="text-sm leading-5 font-medium text-gray-500">
Weapon Usage
</dt>
<dd className="mt-1 text-sm leading-5 text-gray-900 flex items-center justify-center">
<RadarChart cx={300} cy={250} outerRadius={150} width={600} height={500} data={ returnWeaponTotals(props.playerData.totals).length ? returnWeaponTotals(props.playerData.totals) : [{ weapon: 'No Data', val:1}]} className="text-xs">
<PolarGrid />
<PolarAngleAxis dataKey="weapon" />
<PolarRadiusAxis/>
<Radar name={props.playerData.player.player_name} dataKey="val" stroke="#8884d8" fill="#8884d8" fillOpacity={0.6}/>
</RadarChart>
</dd>
</div>
</div>
</div>
</div>
<div className="md:grid md:grid-cols-4 md:gap-6 mt-10">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-medium leading-6 text-gray-900">Game History</h3>
<p className="mt-1 text-sm leading-5 text-gray-500">
Stats for past {props.playerData.stats.length} games
</p>
</div>
</div>
<div className="mt-5 md:mt-0 md:col-span-3">
{ props.playerData.stats.map((player, index) => GameCard(player, index)) }
</div>
</div>
{/*
<div className="bg-white shadow overflow-hidden sm:rounded-md"> <div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="py-10 px-10"><code> {JSON.stringify(props.playerData.stats)}</code></div> <div className="py-10 px-10"><code> {JSON.stringify(props.playerData.stats)}</code></div>
</div> */} </div> */}
</Layout> </Layout>
) );
} }

View file

@ -31,27 +31,54 @@ CREATE TABLE "public"."players" (
); );
DROP TABLE IF EXISTS "public"."games"; DROP TABLE IF EXISTS "public"."games";
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
-- Table Definition -- Table Definition
CREATE TABLE "public"."games" ( CREATE TABLE "public"."games" (
"game_id" numeric NOT NULL UNIQUE,
"map" text NOT NULL,
"datestamp" timestamp NOT NULL,
"gametype" text NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT game_pk PRIMARY KEY (game_id)
);
DROP TABLE IF EXISTS "public"."game_detail";
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
-- Table Definition
CREATE TABLE "public"."game_detail" (
"id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass), "id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass),
"player_guid" numeric NOT NULL, "player_guid" numeric NOT NULL,
"player_name" text NOT NULL, "player_name" text NOT NULL,
"stat_overwrite" numeric NOT NULL, "stat_overwrite" numeric NOT NULL,
"map" text NOT NULL, "map" text NOT NULL,
"game_id" numeric NOT NULL DEFAULT 0, "game_id" numeric NOT NULL,
"stats" jsonb NOT NULL, "stats" jsonb NOT NULL,
"datestamp" timestamp NOT NULL, "datestamp" timestamp NOT NULL,
"uuid" text NOT NULL UNIQUE, "uuid" text NOT NULL UNIQUE,
"gametype" text NOT NULL, "gametype" text NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT games_pk PRIMARY KEY (id), CONSTRAINT games_pk PRIMARY KEY (id),
FOREIGN KEY (game_id) REFERENCES games (game_id),
FOREIGN KEY (player_guid) REFERENCES players (player_guid) FOREIGN KEY (player_guid) REFERENCES players (player_guid)
); );
CREATE OR REPLACE FUNCTION trigger_set_timestamp() CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN