diff --git a/api/config/api-url.js b/api/config/api-url.js
index 4e68b70..87a179e 100644
--- a/api/config/api-url.js
+++ b/api/config/api-url.js
@@ -9,6 +9,7 @@ module.exports = {
command: rootRoute + '/cmd',
decorations: rootRoute + '/decorations',
overview: rootRoute + '/overview',
+ players: rootRoute + '/players',
ranks: rootRoute + '/ranks',
request: rootRoute + '/request',
signatures: '/signatures',
diff --git a/api/package.json b/api/package.json
index 9cfdf82..b1c4c3f 100644
--- a/api/package.json
+++ b/api/package.json
@@ -17,7 +17,7 @@
"cors": "^2.8.3",
"cron": "^1.2.1",
"debug": "~2.2.0",
- "express": "~4.13.1",
+ "express": "^4.16.1",
"imagemin": "^5.2.2",
"imagemin-pngquant": "^5.0.0",
"jimp": "^0.2.27",
diff --git a/api/routes/players.js b/api/routes/players.js
new file mode 100644
index 0000000..c296f8d
--- /dev/null
+++ b/api/routes/players.js
@@ -0,0 +1,56 @@
+"use strict";
+
+// modules
+const express = require('express');
+const logger = require('debug')('cc:wars');
+
+// HTTP status codes by name
+const codes = require('./http-codes');
+
+const routerHandling = require('../middleware/router-handling');
+
+// Mongoose Model using mongoDB
+const CampaignModel = require('../models/campaign');
+const PlayerModel = require('../models/player');
+const WarModel = require('../models/war');
+
+const campaignPlayer = express.Router();
+
+// routes **********************
+campaignPlayer.route('/:campaignId/:playerName')
+ .get((req, res, next) => {
+ CampaignModel.findById(req.params.campaignId, (err, campaign) => {
+ if (err) return next(err);
+ WarModel.find({campaign: req.params.campaignId}, '_id', (err, wars) => {
+ if (err) return next(err);
+ const warIds = wars.map((obj) => {
+ return obj._id;
+ });
+ PlayerModel.find({name: req.params.playerName, warId: {"$in": warIds}})
+ .populate('warId')
+ .exec((err, items) => {
+ if (err) return next(err);
+ if (!items || items.length === 0) {
+ const err = new Error('unknown player name');
+ err.status = codes.notfound;
+ return next(err)
+ }
+ res.locals.items = {
+ name: req.params.playerName,
+ campaign: campaign,
+ players: items
+ };
+ next();
+ })
+ })
+ })
+ })
+
+ .all(
+ routerHandling.httpMethodNotAllowed
+ );
+
+
+campaignPlayer.use(routerHandling.emptyResponse);
+
+module.exports = campaignPlayer;
diff --git a/api/server.js b/api/server.js
index 9c203a9..b961de2 100644
--- a/api/server.js
+++ b/api/server.js
@@ -30,6 +30,7 @@ const rankRouter = require('./routes/ranks');
const decorationRouter = require('./routes/decorations');
const awardingRouter = require('./routes/awardings');
const requestRouter = require('./routes/request');
+const playerRouter = require('./routes/players');
const signatureRouter = require('./routes/signatures');
const commandRouter = require('./routes/command');
const campaignRouter = require('./routes/campaigns');
@@ -80,6 +81,7 @@ app.use(urls.decorations, decorationRouter);
app.use(urls.request, requestRouter);
app.use(urls.awards, awardingRouter);
app.use(urls.wars, warRouter);
+app.use(urls.players, playerRouter);
app.use(urls.campaigns,campaignRouter);
app.use(urls.command, apiAuthenticationMiddleware, checkAdmin, commandRouter);
app.use(urls.account, apiAuthenticationMiddleware, checkAdmin, accountRouter);
diff --git a/package.json b/package.json
index 15f21aa..fe735aa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "opt-cc",
- "version": "1.4.4",
+ "version": "1.5.0",
"license": "MIT",
"private": true,
"scripts": {
diff --git a/static/src/app/app.config.ts b/static/src/app/app.config.ts
index 2eb172f..ad97dbe 100644
--- a/static/src/app/app.config.ts
+++ b/static/src/app/app.config.ts
@@ -11,6 +11,7 @@ export class AppConfig {
public readonly apiSquadPath = this.apiUrl + '/squads/';
public readonly apiUserPath = this.apiUrl + '/users/';
public readonly apiOverviewPath = this.apiUrl + '/overview';
+ public readonly apiPlayersPath = this.apiUrl + '/players';
public readonly apiRequestAwardPath = this.apiUrl + '/request/award';
public readonly apiPromotionPath = this.apiUrl + '/request/promotion';
public readonly apiWarPath = this.apiUrl + '/wars';
diff --git a/static/src/app/models/model-interfaces.ts b/static/src/app/models/model-interfaces.ts
index a4ccc0a..d5b536f 100644
--- a/static/src/app/models/model-interfaces.ts
+++ b/static/src/app/models/model-interfaces.ts
@@ -29,6 +29,12 @@ export interface Player {
flagTouch?: number;
}
+export interface CampaignPlayer {
+ name?: string;
+ campaign?: Campaign;
+ players?: Player[];
+}
+
export interface Campaign {
_id?: string;
title?: string;
diff --git a/static/src/app/services/player-service/player.service.ts b/static/src/app/services/player-service/player.service.ts
new file mode 100644
index 0000000..ae231cc
--- /dev/null
+++ b/static/src/app/services/player-service/player.service.ts
@@ -0,0 +1,18 @@
+import {Injectable} from "@angular/core";
+import {AppConfig} from "../../app.config";
+import {HttpClient} from "../http-client";
+
+@Injectable()
+export class PlayerService {
+
+ constructor(private http: HttpClient,
+ private config: AppConfig) {
+ }
+
+ getCampaignPlayer(campaignId: string, playerName: string) {
+ return this.http.get(this.config.apiPlayersPath + '/' + campaignId + '/' + playerName)
+ .map(res => res.json())
+ }
+
+}
+
diff --git a/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.css b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.css
new file mode 100644
index 0000000..0332e44
--- /dev/null
+++ b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.css
@@ -0,0 +1,58 @@
+.overview {
+ position: fixed;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ border-left: thin solid lightgrey;
+ bottom: 20px;
+ width: 80%;
+ padding-left: 20px;
+ padding-right: 5%;
+ padding-top: 70px;
+ height: 100vh;
+}
+
+h2 {
+ padding: 10px;
+}
+
+.btn-back {
+ clear: both;
+ float: left;
+ width: 120px;
+ margin-left: 10px;
+}
+
+.sum-container {
+ width: 100%;
+ margin: auto;
+ clear: left;
+ padding: 2%;
+}
+
+.gauge-container {
+ padding: 35px;
+}
+
+.sum-bar-container {
+ width: 70%;
+ height: 400px;
+ margin: auto;
+ padding-left: 4%;
+}
+
+.charts-parent {
+ clear: left;
+ padding-top: 50px;
+ width: 100%;
+ margin: auto;
+}
+
+.chart-container {
+ width: 42%;
+ min-width: 500px;
+ height: 300px;
+ padding: 15px;
+ margin: 2%;
+ float: left;
+}
+
diff --git a/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.html b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.html
new file mode 100644
index 0000000..9542f3f
--- /dev/null
+++ b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.html
@@ -0,0 +1,167 @@
+
+
+
Spielerstatistik - {{campaignPlayer.name}}
+
{{campaignPlayer.campaign.title}} Kampagne
+
+
< Zurück
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.ts b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.ts
new file mode 100644
index 0000000..7ccd760
--- /dev/null
+++ b/static/src/app/statistic/campaign-player-detail/campaign-player-detail.component.ts
@@ -0,0 +1,178 @@
+import {Component} from "@angular/core";
+import {ActivatedRoute} from "@angular/router";
+import {CampaignPlayer} from "../../models/model-interfaces";
+import {PlayerService} from "../../services/player-service/player.service";
+import {ChartUtils} from "../../utils/chart-utils";
+import {Location} from '@angular/common';
+
+
+@Component({
+ selector: 'campaign-player-detail',
+ templateUrl: './campaign-player-detail.component.html',
+ styleUrls: ['./campaign-player-detail.component.css', '../../style/list-entry.css']
+})
+export class CampaignPlayerDetailComponent {
+
+ campaignPlayer: CampaignPlayer = {campaign: {}, players: []};
+
+ sumData: any[] = [];
+ killData: any[] = [];
+ friendlyFireData: any[] = [];
+ deathData: any[] = [];
+ respawnData: any[] = [];
+ reviveData: any[] = [];
+ captureData: any[] = [];
+
+ yAxisKill = 'Kills';
+ yAxisFriendlyFire = 'FriendlyFire';
+ yAxisDeath = 'Tode';
+ yAxisRespawn = 'Respawn';
+ yAxisRevive = 'Revive';
+ yAxisCapture = 'Eroberungen';
+ avgLabel = 'Durchschnitt';
+
+ colorScheme = {
+ domain: ['#00ce12']
+ };
+ colorSchemeBar = {
+ domain: [
+ '#2d5a00', '#455600', '#00561f', '#3f3b00', '#003c19', '#083c00'
+ ]
+ };
+ showRefLines = true;
+ showRefLabels = true;
+ killRefLines = [];
+ deathRefLines = [];
+ captureRefLines = [];
+ friendlyFireRefLines = [];
+ reviveRefLines = [];
+ respawnRefLines = [];
+
+ gradient = false;
+ xAxis = true;
+ yAxis = true;
+ legend = false;
+ showXAxisLabel = true;
+ showYAxisLabel = true;
+ autoscale = false;
+ timeline = false;
+ roundDomains = true;
+
+ totalKills;
+ totalFriendlyFire;
+ totalDeath;
+ totalRespawn;
+ totalRevive;
+ totalCapture;
+
+ kdRatio = 0;
+ maxKd = 1.7;
+
+ respawnDeathRatio = 0;
+ maxRespawnDeathRatio = 1;
+
+
+ constructor(private route: ActivatedRoute,
+ private location: Location,
+ private playerService: PlayerService) {
+ }
+
+ ngOnInit() {
+ this.route.params
+ .map(params => [params['id'], params['playerName']])
+ .flatMap(id => this.playerService.getCampaignPlayer(id[0], encodeURIComponent(id[1])))
+ .subscribe(campaignPlayer => {
+ this.campaignPlayer = campaignPlayer;
+ this.killData = this.assignData(this.yAxisKill, "kill");
+ this.friendlyFireData = this.assignData(this.yAxisFriendlyFire, "friendlyFire");
+ this.deathData = this.assignData(this.yAxisDeath, "death");
+ this.respawnData = this.assignData(this.yAxisRespawn, "respawn");
+ this.reviveData = this.assignData(this.yAxisRevive, "revive");
+ this.captureData = this.assignData(this.yAxisCapture, "flagTouch");
+
+ this.kdRatio = parseFloat((this.totalKills / this.totalDeath).toFixed(2));
+ if (this.kdRatio > 1) this.maxKd = this.kdRatio * 1.7;
+
+ this.respawnDeathRatio = parseFloat((this.totalRespawn / this.totalDeath).toFixed(2));
+
+ this.sumData = [
+ {
+ name: this.yAxisKill,
+ value: this.totalKills
+ },
+ {
+ name: this.yAxisFriendlyFire,
+ value: this.totalFriendlyFire
+ },
+ {
+ name: this.yAxisDeath,
+ value: this.totalDeath
+ },
+ {
+ name: this.yAxisRespawn,
+ value: this.totalRespawn
+ },
+ {
+ name: this.yAxisRevive,
+ value: this.totalRevive
+ },
+ {
+ name: this.yAxisCapture,
+ value: this.totalCapture
+ }
+ ];
+
+ Object.assign(this, [this.sumData, this.killData, this.friendlyFireData, this.deathData, this.respawnData, this.reviveData, this.captureData]);
+ });
+ }
+
+ private assignData(label, field) {
+ let killObj = {
+ name: label,
+ series: []
+ };
+ const playerLength = this.campaignPlayer.players.length;
+ let total = 0;
+ for (let i = 0; i < playerLength; i++) {
+ const warDateString = ChartUtils.getShortDateString(this.campaignPlayer.players[i].warId.date);
+ const warKills = this.campaignPlayer.players[i][field];
+ killObj.series.push({
+ name: warDateString,
+ value: warKills
+ });
+ total += warKills;
+ }
+ switch (field) {
+ case 'kill':
+ this.killRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalKills = total;
+ break;
+ case 'friendlyFire':
+ this.friendlyFireRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalFriendlyFire = total;
+ break;
+ case 'death':
+ this.deathRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalDeath = total;
+ break;
+ case 'respawn':
+ this.respawnRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalRespawn = total;
+ break;
+ case 'revive':
+ this.reviveRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalRevive = total;
+ break;
+ case 'flagTouch':
+ this.captureRefLines.push({value: total / playerLength, name: this.avgLabel});
+ this.totalCapture = total;
+ break;
+ }
+ return [killObj];
+ }
+
+ navigateBack() {
+ this.location.back();
+ }
+
+}
diff --git a/static/src/app/statistic/overview/stats-overview.component.html b/static/src/app/statistic/overview/stats-overview.component.html
index cff395c..7ab2b59 100644
--- a/static/src/app/statistic/overview/stats-overview.component.html
+++ b/static/src/app/statistic/overview/stats-overview.component.html
@@ -10,9 +10,7 @@
(click)="goToSlide(1)">Spielerzahlen
-
-
-
+
Gesamtpunktzahl
diff --git a/static/src/app/statistic/overview/stats-overview.component.ts b/static/src/app/statistic/overview/stats-overview.component.ts
index 2b624aa..0a01419 100644
--- a/static/src/app/statistic/overview/stats-overview.component.ts
+++ b/static/src/app/statistic/overview/stats-overview.component.ts
@@ -2,12 +2,13 @@ import {Component} from "@angular/core";
import {ActivatedRoute} from "@angular/router";
import {CarouselConfig} from "ngx-bootstrap";
import {CampaignService} from "../../services/campaign-service/campaign.service";
+import {ChartUtils} from "../../utils/chart-utils";
@Component({
selector: 'stats-overview',
templateUrl: './stats-overview.component.html',
- styleUrls: ['./stats-overview.component.css'],
+ styleUrls: ['./stats-overview.component.css', '../../style/list-entry.css'],
inputs: ['campaigns'],
providers: [{provide: CarouselConfig, useValue: {interval: false}}]
})
@@ -94,11 +95,7 @@ export class StatisticOverviewComponent {
for (let i = wars.length - 1; i >= 0; i--) {
let j = wars.length - i - 1;
- // const warDateString = new Date(wars[i].date); TODO: use ngx-chart timeline
- const isoDate = wars[i].date.slice(0, 10);
- const dayDate = parseInt(isoDate.slice(8, 10)) + 1;
- const warDateString = (dayDate < 10 ? "0" + dayDate : dayDate) + '.'
- + isoDate.slice(5, 7) + '.' + isoDate.slice(2, 4);
+ const warDateString = ChartUtils.getShortDateString(wars[i].date);
pointsObj[0].series.push({
name: warDateString,
diff --git a/static/src/app/statistic/stats.module.ts b/static/src/app/statistic/stats.module.ts
index 15b9e9d..e2edbc4 100644
--- a/static/src/app/statistic/stats.module.ts
+++ b/static/src/app/statistic/stats.module.ts
@@ -3,16 +3,17 @@ import {CommonModule} from "@angular/common";
import {SharedModule} from "../shared.module";
import {statsRouterModule, statsRoutingComponents} from "./stats.routing";
import {WarService} from "../services/war-service/war.service";
-import {LineChartModule, PieChartModule} from "@swimlane/ngx-charts";
+import {NgxChartsModule} from "@swimlane/ngx-charts";
import {AccordionModule, CarouselModule} from "ngx-bootstrap";
import {CampaignService} from "../services/campaign-service/campaign.service";
import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {PlayerService} from "../services/player-service/player.service";
@NgModule({
declarations: statsRoutingComponents,
- imports: [CommonModule, SharedModule, statsRouterModule, LineChartModule, PieChartModule,
+ imports: [CommonModule, SharedModule, statsRouterModule, NgxChartsModule,
AccordionModule.forRoot(), CarouselModule.forRoot(), NgxDatatableModule],
- providers: [WarService, CampaignService]
+ providers: [WarService, CampaignService, PlayerService]
})
export class StatsModule {
static routes = statsRouterModule;
diff --git a/static/src/app/statistic/stats.routing.ts b/static/src/app/statistic/stats.routing.ts
index acff442..dbc6951 100644
--- a/static/src/app/statistic/stats.routing.ts
+++ b/static/src/app/statistic/stats.routing.ts
@@ -7,6 +7,7 @@ import {StatisticOverviewComponent} from "./overview/stats-overview.component";
import {WarItemComponent} from "./war-list/war-item.component";
import {ModuleWithProviders} from "@angular/core";
import {CampaignSubmitComponent} from "./campaign-submit/campaign-submit.component";
+import {CampaignPlayerDetailComponent} from "./campaign-player-detail/campaign-player-detail.component";
export const statsRoutes: Routes = [{
@@ -37,10 +38,15 @@ export const statsRoutes: Routes = [{
path: 'war/:id',
component: WarDetailComponent,
outlet: 'right'
- }];
+ },
+ {
+ path: 'campaign-player/:id/:playerName',
+ component: CampaignPlayerDetailComponent,
+ outlet: 'right'
+ },];
export const statsRouterModule: ModuleWithProviders = RouterModule.forChild(statsRoutes);
export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, CampaignSubmitComponent,
- WarListComponent, WarSubmitComponent, WarDetailComponent, WarItemComponent];
+ WarListComponent, WarSubmitComponent, WarDetailComponent, CampaignPlayerDetailComponent, WarItemComponent];
diff --git a/static/src/app/statistic/war-detail/war-detail.component.css b/static/src/app/statistic/war-detail/war-detail.component.css
index ac08fbe..454c430 100644
--- a/static/src/app/statistic/war-detail/war-detail.component.css
+++ b/static/src/app/statistic/war-detail/war-detail.component.css
@@ -51,6 +51,7 @@
:host /deep/ .datatable-body-row {
color: #222222;
border-bottom: 1px solid grey;
+ cursor: pointer;
}
:host /deep/ .datatable-body-row:hover {
diff --git a/static/src/app/statistic/war-detail/war-detail.component.html b/static/src/app/statistic/war-detail/war-detail.component.html
index 6ff3d83..fa69568 100644
--- a/static/src/app/statistic/war-detail/war-detail.component.html
+++ b/static/src/app/statistic/war-detail/war-detail.component.html
@@ -1,4 +1,4 @@
-
+
{{war.title}} - vom {{war.date | date: 'dd.MM.yyyy'}}
@@ -57,10 +57,13 @@
[messages]="{emptyMessage: 'Loading...'}"
[headerHeight]="cellHeight"
[rowHeight]="cellHeight"
- [cssClasses]='customClasses'>
+ [cssClasses]='customClasses'
+ [selectionType]="'single'"
+ (select)="selectPlayerDetail($event)">
-
+
{{value}}
diff --git a/static/src/app/statistic/war-detail/war-detail.component.ts b/static/src/app/statistic/war-detail/war-detail.component.ts
index 4d84ef6..3695170 100644
--- a/static/src/app/statistic/war-detail/war-detail.component.ts
+++ b/static/src/app/statistic/war-detail/war-detail.component.ts
@@ -1,5 +1,5 @@
import {Component} from "@angular/core";
-import {ActivatedRoute} from "@angular/router";
+import {ActivatedRoute, Router} from "@angular/router";
import {WarService} from "../../services/war-service/war.service";
import {War} from "../../models/model-interfaces";
@@ -7,7 +7,7 @@ import {War} from "../../models/model-interfaces";
@Component({
selector: 'war-detail',
templateUrl: './war-detail.component.html',
- styleUrls: ['./war-detail.component.css']
+ styleUrls: ['./war-detail.component.css', '../../style/list-entry.css']
})
export class WarDetailComponent {
@@ -29,6 +29,7 @@ export class WarDetailComponent {
};
constructor(private route: ActivatedRoute,
+ private router: Router,
private warService: WarService) {
Object.assign(this, this.playerChart)
}
@@ -64,4 +65,11 @@ export class WarDetailComponent {
}
}
+ selectPlayerDetail(player) {
+ if (player && player.selected && player.selected.length > 0) {
+ this.router.navigate(['../../campaign-player/' + this.war.campaign + '/' + player.selected[0].name],
+ {relativeTo: this.route});
+ }
+ }
+
}
diff --git a/static/src/app/utils/chart-utils.ts b/static/src/app/utils/chart-utils.ts
new file mode 100644
index 0000000..0761dca
--- /dev/null
+++ b/static/src/app/utils/chart-utils.ts
@@ -0,0 +1,10 @@
+export class ChartUtils {
+
+ public static getShortDateString(date) : string {
+ const isoDate = date.slice(0, 10);
+ const dayDate = parseInt(isoDate.slice(8, 10)) + 1;
+ return (dayDate < 10 ? "0" + dayDate : dayDate) + '.'
+ + isoDate.slice(5, 7) + '.' + isoDate.slice(2, 4);
+ }
+
+}