feat: arena ladder
This commit is contained in:
parent
ac6ca4c84e
commit
f7be06074e
12 changed files with 429 additions and 10 deletions
|
|
@ -275,6 +275,8 @@ This repository is used in production over at [ChromieCraft](https://www.chromie
|
|||
- [X] Multiple realms support
|
||||
- [ ] PvE ladder
|
||||
- [ ] PvP ladder
|
||||
- [X] Arena ladder
|
||||
- [ ] Achievements ladder
|
||||
|
||||
See the [open issues](https://github.com/r-o-b-o-t-o/azerothcore-armory/issues) for a list of suggested features and known issues.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { CharacterCustomization } from "./data/CharacterCustomization";
|
|||
import { IndexController } from "./controllers/IndexController";
|
||||
import { CharacterController } from "./controllers/CharacterController";
|
||||
import { GuildController } from "./controllers/GuildController";
|
||||
import { ArenaController } from "./controllers/ArenaController";
|
||||
|
||||
export class Armory {
|
||||
public characterCustomization: CharacterCustomization;
|
||||
|
|
@ -157,6 +158,11 @@ export class Armory {
|
|||
app.get("/guild/:realm/:name", this.wrapRoute(guildsController.guild.bind(guildsController)));
|
||||
app.get("/guild/:realm/:guild/members", this.wrapRoute(guildsController.members.bind(guildsController)));
|
||||
|
||||
const arenaController = new ArenaController(this);
|
||||
app.get("/arena", this.wrapRoute(arenaController.index.bind(arenaController)));
|
||||
app.get("/arena/ladder", this.wrapRoute(arenaController.ladder.bind(arenaController)));
|
||||
app.get("/arena/team/:realm/:name", this.wrapRoute(arenaController.team.bind(arenaController)));
|
||||
|
||||
app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Error handler
|
||||
|
||||
|
|
|
|||
156
src/armory/controllers/ArenaController.ts
Normal file
156
src/armory/controllers/ArenaController.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import * as express from "express";
|
||||
import { RowDataPacket } from "mysql2";
|
||||
|
||||
import { Armory } from "../Armory";
|
||||
import { IRealmConfig } from "../Config";
|
||||
import { IEmblem, Utils } from "../Utils";
|
||||
import { DataTablesSsp } from "../DataTablesSsp";
|
||||
|
||||
interface ITeamMemberData {
|
||||
name: string;
|
||||
weekGames: number;
|
||||
weekWins: number;
|
||||
seasonGames: number;
|
||||
seasonWins: number;
|
||||
personalRating: number;
|
||||
race: number;
|
||||
class: number;
|
||||
gender: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
interface ITeamData {
|
||||
name: string;
|
||||
captainGuid: number;
|
||||
type: number;
|
||||
rating: number;
|
||||
seasonGames: number;
|
||||
seasonWins: number;
|
||||
weekGames: number;
|
||||
weekWins: number;
|
||||
emblem: IEmblem;
|
||||
members: ITeamMemberData[];
|
||||
}
|
||||
|
||||
export class ArenaController {
|
||||
private armory: Armory;
|
||||
|
||||
public constructor(armory: Armory) {
|
||||
this.armory = armory;
|
||||
}
|
||||
|
||||
public async index(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
res.render("ladder-arena.hbs", {
|
||||
title: `Arena Ladder`,
|
||||
realms: this.armory.config.realms.map((r) => r.name),
|
||||
});
|
||||
}
|
||||
|
||||
public async team(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const teamName = req.params.name;
|
||||
|
||||
const realm = this.armory.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const teamData = await this.getTeamData(realm, teamName);
|
||||
if (teamData === null) {
|
||||
// Could not find guild
|
||||
return next(404);
|
||||
}
|
||||
|
||||
res.render("arena-team.hbs", {
|
||||
title: `Armory - ${teamData.name}`,
|
||||
realm: realm.name,
|
||||
...teamData,
|
||||
});
|
||||
}
|
||||
|
||||
public async ladder(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.query.realm as string;
|
||||
const realm = realmName === undefined ? this.armory.config.realms[0] : this.armory.config.realms.find((r) => r.name === realmName);
|
||||
if (realm === undefined) {
|
||||
return next(400);
|
||||
}
|
||||
|
||||
const teamSize = parseInt(req.query.teamsize as string);
|
||||
if (!(teamSize === 2 || teamSize === 3 || teamSize === 5)) {
|
||||
return next(400);
|
||||
}
|
||||
|
||||
const db = this.armory.getCharactersDb(realm.name);
|
||||
const charSet = await this.armory.getDatabaseCharset(realm.name);
|
||||
|
||||
const ssp = new DataTablesSsp(req.query, db, "arena_team", "arenaTeamId", [
|
||||
{ name: "name", collation: `${charSet}_general_ci` },
|
||||
{ name: "rating" },
|
||||
{ name: "seasonWins" },
|
||||
{ name: "seasonGames" },
|
||||
]);
|
||||
|
||||
const result = await ssp.where("`type` = " + teamSize).run(this.armory.config.dbQueryTimeout);
|
||||
|
||||
res.json({
|
||||
...result,
|
||||
realm: realm.name,
|
||||
teamSize,
|
||||
});
|
||||
}
|
||||
|
||||
private async getTeamData(realm: IRealmConfig, teamName: string): Promise<ITeamData> {
|
||||
const db = this.armory.getCharactersDb(realm.name);
|
||||
const [rows] = await db.query({
|
||||
sql: `
|
||||
SELECT arenaTeamId, name, captainGuid, type, rating, seasonGames, seasonWins, weekGames, weekWins, emblemStyle, emblemColor, borderStyle, borderColor, backgroundColor AS background
|
||||
FROM arena_team WHERE name = ?
|
||||
`,
|
||||
values: [teamName],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
if ((rows as RowDataPacket[]).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const team = rows[0];
|
||||
|
||||
const [memberRows] = await db.query({
|
||||
sql: `
|
||||
SELECT name, weekGames, weekWins, seasonGames, seasonWins, personalRating, race, class, gender, online
|
||||
FROM arena_team_member
|
||||
LEFT JOIN characters ON arena_team_member.guid = characters.guid
|
||||
WHERE arenaTeamId = ?
|
||||
`,
|
||||
values: [team.arenaTeamId],
|
||||
timeout: this.armory.config.dbQueryTimeout,
|
||||
});
|
||||
const members: ITeamMemberData[] = (memberRows as RowDataPacket[]).map((row) => {
|
||||
return {
|
||||
name: row.name,
|
||||
weekGames: row.weekGames,
|
||||
weekWins: row.weekWins,
|
||||
seasonGames: row.seasonGames,
|
||||
seasonWins: row.seasonWins,
|
||||
personalRating: row.personalRating,
|
||||
race: Utils.raceNames[row.race],
|
||||
class: Utils.classNames[row.class],
|
||||
gender: row.gender === 0 ? "male" : "female",
|
||||
online: row.online === 1,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: team.name,
|
||||
captainGuid: team.captainGuid,
|
||||
type: team.type,
|
||||
rating: team.rating,
|
||||
seasonGames: team.seasonGames,
|
||||
seasonWins: team.seasonWins,
|
||||
weekGames: team.weekGames,
|
||||
weekWins: team.weekWins,
|
||||
emblem: Utils.makeEmblemObject(team, false),
|
||||
members,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +319,7 @@ export class CharacterController {
|
|||
|
||||
res.render("character-pvp.hbs", {
|
||||
title: `Armory - ${charData.name} - PvP`,
|
||||
realm: realm.name,
|
||||
...this.makeSharedDataObject(realm, charData),
|
||||
faction: Utils.getFactionFromRaceId(charData.race),
|
||||
kills: await this.getPvpKills(realm.name, charData.guid),
|
||||
|
|
|
|||
115
static/arena-team.hbs
Normal file
115
static/arena-team.hbs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/arena-team.css">
|
||||
{{> datatables}}
|
||||
{{> emblems}}
|
||||
|
||||
<a href="{{websiteRoot}}/arena">Back to Arena Ladder</a>
|
||||
<br><br>
|
||||
|
||||
<div id="arena-header">
|
||||
<div class="emblem-container">
|
||||
<div class="arena-emblem">
|
||||
<canvas width="74" height="128"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="team-name is-size-3">{{name}}</div>
|
||||
<div class="is-size-5">
|
||||
{{type}}v{{type}} Team, {{realm}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="is-size-5">Rating: {{rating}}</div>
|
||||
<table id="stats">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Games</th>
|
||||
<th>Wins</th>
|
||||
<th>Losses</th>
|
||||
<th>Win Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Week</td>
|
||||
<td>{{weekGames}}</td>
|
||||
<td>{{weekWins}}</td>
|
||||
<td>{{subtract weekGames weekWins}}</td>
|
||||
<td>
|
||||
{{#if (isnt weekGames 0)}}
|
||||
{{toPrecision (multiply (divide weekWins weekGames) 100) 3}} %
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Season</td>
|
||||
<td>{{seasonGames}}</td>
|
||||
<td>{{seasonWins}}</td>
|
||||
<td>{{subtract seasonGames seasonWins}}</td>
|
||||
<td>
|
||||
{{#if (isnt seasonGames 0)}}
|
||||
{{toPrecision (multiply (divide seasonWins seasonGames) 100) 3}} %
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<div class="is-size-5">Members:</div>
|
||||
<table id="members" class="stripe hover row-border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Class</th>
|
||||
<th>Race</th>
|
||||
<th>Week Wins</th>
|
||||
<th>Week Losses</th>
|
||||
<th>Season Wins</th>
|
||||
<th>Season Losses</th>
|
||||
<th>Rating</th>
|
||||
<th>Online</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each members}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{../websiteRoot}}/character/{{../realm}}/{{name}}/pvp">
|
||||
{{name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<img src="{{../aowow}}/static/images/wow/icons/medium/class_{{class}}.jpg">
|
||||
</td>
|
||||
<td>
|
||||
<img src="{{../aowow}}/static/images/wow/icons/medium/race_{{race}}_{{gender}}.jpg">
|
||||
</td>
|
||||
<td>{{weekWins}}</td>
|
||||
<td>{{subtract weekGames weekWins}}</td>
|
||||
<td>{{seasonWins}}</td>
|
||||
<td>{{subtract seasonGames seasonWins}}</td>
|
||||
<td>{{personalRating}}</td>
|
||||
<td>{{#if online}}🟢{{else}}🔴{{/if}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script type="application/javascript">
|
||||
$(window).on("load", () => {
|
||||
createArenaEmblem({{this.type}}, {{{JSONstringify this.emblem}}}, $(".arena-emblem")[0]);
|
||||
|
||||
const dt = $("#members").DataTable({
|
||||
searching: false,
|
||||
paging: false,
|
||||
responsive: {
|
||||
details: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -20,7 +20,9 @@
|
|||
<div class="column is-full-touch is-one-quarter-desktop">
|
||||
<div class="box arena-team" data-team-id="{{this.id}}">
|
||||
<div class="info has-text-centered">
|
||||
<div class="is-size-4">{{this.name}}</div>
|
||||
<div class="is-size-4">
|
||||
<a href="{{../websiteRoot}}/arena/team/{{../realm}}/{{this.name}}">{{this.name}}</a>
|
||||
</div>
|
||||
<div class="is-size-5">{{this.type}}v{{this.type}} Team</div>
|
||||
<div>Rating: {{this.rating}}</div>
|
||||
<div>{{this.seasonWins}} W / {{subtract this.seasonGames this.seasonWins}} L</div>
|
||||
|
|
|
|||
29
static/css/arena-team.css
Normal file
29
static/css/arena-team.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#arena-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#arena-header .emblem-container {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#arena-header .info {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#arena-header .info .team-name {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
#stats td,
|
||||
#stats th {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dataTables_info {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
#select-realm {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#results_processing {
|
||||
height: 70px;
|
||||
}
|
||||
|
|
|
|||
8
static/css/ladder-arena.css
Normal file
8
static/css/ladder-arena.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#select-arena-type-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#select-arena-type-container .arena-type-label {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
<h1 class="title is-size-1">Armory</h1>
|
||||
|
||||
{{#if (not (equalsLength realms 1))}}
|
||||
<a href="{{websiteRoot}}/arena">Arena Ladder</a> 
|
||||
|
||||
<br><br>
|
||||
|
||||
{{#if (not (equalsLength realms 1))}}
|
||||
<div id="select-realm-container">
|
||||
<span class="realm-label">Realm:</span>
|
||||
<div class="select">
|
||||
|
|
|
|||
101
static/ladder-arena.hbs
Normal file
101
static/ladder-arena.hbs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{websiteRoot}}/css/ladder-arena.css">
|
||||
{{> datatables}}
|
||||
|
||||
<h1 class="title is-size-1">Arena Ladder</h1>
|
||||
|
||||
<a href="{{websiteRoot}}/">Armory</a> 
|
||||
|
||||
<br><br>
|
||||
|
||||
{{#if (not (equalsLength realms 1))}}
|
||||
<div id="select-realm-container">
|
||||
<span class="realm-label">Realm:</span>
|
||||
<div class="select">
|
||||
<select id="select-realm">
|
||||
{{#each realms}}
|
||||
<option>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
 
|
||||
<div class="select">
|
||||
<select id="select-arena-type">
|
||||
<option value="2">2v2</option>
|
||||
<option value="3">3v3</option>
|
||||
<option value="5">5v5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div id="select-arena-type-container">
|
||||
<span class="arena-type-label">Type:</span>
|
||||
<div class="select">
|
||||
<select id="select-arena-type">
|
||||
<option value="2">2v2</option>
|
||||
<option value="3">3v3</option>
|
||||
<option value="5">5v5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<br>
|
||||
|
||||
<table id="results" class="stripe hover row-border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Rating</th>
|
||||
<th>Wins</th>
|
||||
<th>Losses</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<script type="application/javascript">
|
||||
$(window).on("load", () => {
|
||||
let dt;
|
||||
|
||||
$("#select-realm, #select-arena-type").on("change", () => {
|
||||
dt.draw();
|
||||
});
|
||||
|
||||
dt = $("#results").DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searchDelay: 800,
|
||||
ajax: {
|
||||
url: `{{websiteRoot}}/arena/ladder`,
|
||||
data: d => {
|
||||
d.realm = $("#select-realm").val();
|
||||
d.teamsize = $("#select-arena-type").val();
|
||||
},
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 0,
|
||||
render: (name, type, row, meta) => `<a href="{{websiteRoot}}/arena/team/${meta.settings.json.realm}/${name}">${name}</a>`,
|
||||
},
|
||||
{
|
||||
targets: 1,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
targets: 2,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
searchable: false,
|
||||
render: (gamesPlayed, type, row, meta) => gamesPlayed - row[2],
|
||||
},
|
||||
],
|
||||
responsive: {
|
||||
details: true,
|
||||
},
|
||||
order: [[1, "desc"], [0, "asc"]],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<a href="{{websiteRoot}}/">Back to Armory</a>
|
||||
<br><br>
|
||||
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a> 
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a> 
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a>
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a>
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}">Character</a> 
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/talents">Talents</a> 
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/achievements">Achievements</a> 
|
||||
<a href="{{websiteRoot}}/character/{{realm}}/{{name}}/pvp">PvP</a> 
|
||||
<br>
|
||||
<br>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue