diff --git a/gui/app.css b/gui/app.css
index 0f651e8..e0185df 100644
--- a/gui/app.css
+++ b/gui/app.css
@@ -1,13 +1,20 @@
.navbar-main-menu {
- -webkit-user-select: none;
- -webkit-app-region: drag;
+ -webkit-user-select: none;
+ -webkit-app-region: drag;
}
+
.navbar-main-menu .navbar-nav {
- -webkit-app-region: no-drag;
+ -webkit-app-region: no-drag;
}
+
.navbar-main-menu .navbar-brand-icon {
- height: 32px;
+ height: 32px;
}
+
+.popover {
+ max-width: 600px;
+}
+
.content {
position: absolute;
overflow: auto;
@@ -16,6 +23,7 @@
top: 60px;
bottom: 0;
}
+
.talents {
position: absolute;
left: 0;
@@ -23,24 +31,31 @@
bottom: 0;
top: 0;
}
+
.replay-list tr i.fa.fa-plus-circle {
display: inline;
}
+
.replay-list tr i.fa.fa-minus-circle {
display: none;
}
+
.replay-list tr + tr.details {
display: none;
}
+
.replay-list tr.expanded i.fa.fa-plus-circle {
display: none;
}
+
.replay-list tr.expanded i.fa.fa-minus-circle {
display: inline;
}
+
.replay-list tr.expanded + tr.details {
display: table-row;
}
+
.main-flex {
position: absolute;
top: 0;
@@ -48,9 +63,11 @@
right: 0;
bottom: 0;
}
+
.main-flex .center-area {
overflow: auto;
}
+
.player-hero-none {
display: inline-block;
min-height: 58px;
@@ -58,6 +75,7 @@
text-align: center;
background: #000000;
}
+
.ban-hero-none {
display: inline-block;
min-height: 58px;
@@ -66,10 +84,12 @@
text-align: center;
background: #000000;
}
+
.team-blue .card,
.team-read .card {
min-width: 92px;
}
+
.team-blue-bans .card-img-top,
.team-red-bans .card-img-top {
height: 64px;
@@ -81,6 +101,7 @@
width: 80px;
position: relative;
}
+
.provider-heroescounters .heroescounters-hero .heroescounters-sort-value {
position: absolute;
bottom: 2px;
@@ -92,12 +113,15 @@
.provider-hotsdraft .bonus {
color: #28a745;
}
+
.provider-hotsdraft .malus {
color: #dc3545;
}
+
.provider-hotsdraft .hero {
font-weight: bold;
}
+
.provider-hotsdraft .notes {
font-size: 13px;
line-height: 14px;
@@ -107,36 +131,44 @@
height: 100%;
overflow: auto;
}
+
.provider-icyveins iframe {
- width:100%;
+ width: 100%;
height: 99%;
}
+
.provider-icyveins a {
color: inherit;
font-weight: bold;
}
+
.provider-icyveins .spell_icon {
max-height: 20px
}
+
.icyveins-hero-build-talents {
display: flex;
flex-direction: row;
justify-content: center;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier {
display: flex;
flex-direction: column;
margin: 8px;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_subtitle {
text-align: center;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_visual {
display: flex;
flex-direction: row;
padding: 8px;
justify-content: center;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_situational,
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_yes,
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_no {
@@ -146,30 +178,37 @@
border-radius: 2px;
align-self: flex-end;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_situational {
background-color: #ffb100;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_yes {
background-color: #00ff00;
width: 12px;
height: 12px;
align-self: flex-start;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier_visual .heroes_build_talent_tier_no {
background-color: rgba(0, 0, 0, 0.75);
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier > .heroes_build_talent_tier_recommended img {
width: 64px;
margin-bottom: 8px;
align-self: center;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier > .heroes_build_talent_tier_situational {
position: relative;
align-self: center;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier > .heroes_build_talent_tier_situational img {
width: 40px;
}
+
.icyveins-hero-build-talents .heroes_build_talent_tier > .heroes_build_talent_tier_situational .talent_marker.talent_marker_maybe {
position: absolute;
left: 50%;
diff --git a/gui/elements/draft.twig.html b/gui/elements/draft.twig.html
index 7dd9d10..716e1ed 100644
--- a/gui/elements/draft.twig.html
+++ b/gui/elements/draft.twig.html
@@ -32,10 +32,19 @@
{{ gui.draft.map }}
{% include "../elements/correction.twig.html" %}
diff --git a/gui/elements/player.twig.html b/gui/elements/player.twig.html
index 925f3d4..820f13a 100644
--- a/gui/elements/player.twig.html
+++ b/gui/elements/player.twig.html
@@ -17,10 +17,10 @@
{{ playerName }}
- {% if gui.hasPlayerKnownPicks(playerName) %}
-
+ {% if recentPicks != null %}
+
+
{% endif %}
diff --git a/gui/elements/playerPicks.twig.html b/gui/elements/playerPicks.twig.html
new file mode 100644
index 0000000..41207cf
--- /dev/null
+++ b/gui/elements/playerPicks.twig.html
@@ -0,0 +1,11 @@
+
+{% for battleTag, battleTagPicks in recentPicks %}
+
+ {{ battleTag }}
+ {% for playerPick in battleTagPicks|slice(0, 8) %}
+
+ {{ playerPick[0] }} ({{ playerPick[1] }})
+ {% endfor %}
+
+{% endfor %}
+
\ No newline at end of file
diff --git a/gui/pages/config.twig.html b/gui/pages/config.twig.html
index bfe184f..7c7b0c8 100644
--- a/gui/pages/config.twig.html
+++ b/gui/pages/config.twig.html
@@ -74,6 +74,22 @@ Configuration
+
+
Automatic replay upload
@@ -118,6 +134,7 @@ Configuration
jQuery(function() {
const fs = require("fs");
+ const dialog = require('electron').remote.dialog;
// Change language
jQuery("#language").on("change", function() {
@@ -140,7 +157,19 @@ Configuration
gui.setConfigOption("gameDisplay", value);
});
// Change game storage directory
- jQuery("#gameStorageDir").on("change keyup", function() {
+ jQuery("#gameStorageDir").on("click", function() {
+ let value = jQuery(this).val();
+ let dialogOptions = { properties: ['openDirectory'] };
+ if (value !== "") {
+ dialogOptions.defaultPath = value;
+ }
+ dialog.showOpenDialog(dialogOptions, (result) => {
+ if (typeof result !== "undefined") {
+ jQuery(this).val(result[0]).trigger("change");
+ }
+ });
+ });
+ jQuery("#gameStorageDir").on("change", function() {
let value = jQuery(this).val();
if (fs.existsSync(value)) {
gui.setConfigOption("gameStorageDir", (value !== "" ? value : null));
@@ -149,6 +178,33 @@ Configuration
jQuery(this).addClass("is-invalid").removeClass("is-valid");
}
});
+ // Change google BigQuery project id
+ jQuery("#googleBigQueryProject").on("change keyup", function() {
+ let value = jQuery(this).val();
+ gui.setConfigOption("googleBigQueryProject", (value !== "" ? value : null));
+ });
+ // Change google BigQuery authentification file
+ jQuery("#googleBigQueryAuth").on("click", function() {
+ let value = jQuery(this).val();
+ let dialogOptions = { properties: ['openFile'] };
+ if (value !== "") {
+ dialogOptions.defaultPath = value;
+ }
+ dialog.showOpenDialog(dialogOptions, (result) => {
+ if (typeof result !== "undefined") {
+ jQuery(this).val(result[0]).trigger("change");
+ }
+ });
+ });
+ jQuery("#googleBigQueryAuth").on("change", function() {
+ let value = jQuery(this).val();
+ if (fs.existsSync(value)) {
+ gui.setConfigOption("googleBigQueryAuth", (value !== "" ? value : null));
+ jQuery(this).addClass("is-valid").removeClass("is-invalid");
+ } else {
+ jQuery(this).addClass("is-invalid").removeClass("is-valid");
+ }
+ });
// Change own player name (for talent suggestions)
jQuery("#playerName").on("change keyup", function() {
let value = jQuery(this).val();
diff --git a/package.json b/package.json
index af5bd9f..1c5dc15 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hotsdrafter",
- "version": "1.2.4",
+ "version": "1.2.5",
"description": "Predict and support drafts for running hots matches",
"main": "main.js",
"scripts": {
@@ -30,16 +30,17 @@
"@electron-forge/maker-zip": "6.0.0-beta.33",
"@fortawesome/fontawesome-free": "^5.9.0",
"electron": "^5.0.6",
- "electron-builder": "^21.0.15",
- "electron-forge-maker-appimage": "^21.1.2",
+ "electron-builder": "^21.2.0",
+ "electron-forge-maker-appimage": "^21.2.0",
"electron-winstaller": "^3.0.4"
},
"dependencies": {
"@electron-forge/publisher-github": "^6.0.0-beta.43",
+ "@google-cloud/bigquery": "^4.1.6",
"bootstrap": "^4.3.1",
"cheerio": "^1.0.0-rc.3",
"electron-squirrel-startup": "^1.0.0",
- "hots-replay": "^1.0.0",
+ "hots-replay": "^1.0.3",
"jimp": "^0.6.4",
"jquery": "^3.4.1",
"popper.js": "^1.15.0",
diff --git a/src/config.js b/src/config.js
index 133ecca..387369b 100644
--- a/src/config.js
+++ b/src/config.js
@@ -22,6 +22,8 @@ class Config {
gameDisplay: null,
gameStorageDir: null,
gameImproveDetection: true,
+ googleBigQueryProject: null,
+ googleBigQueryAuth: null,
uploadProvider_hotsapi: false
};
this.visible = false;
diff --git a/src/external/heroescounters.js b/src/external/heroescounters.js
index ca88dd0..e48fe6f 100644
--- a/src/external/heroescounters.js
+++ b/src/external/heroescounters.js
@@ -164,9 +164,11 @@ class HeroesCountersProvider extends HotsDraftSuggestions {
this.updateActive = false;
if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
this.loadCoreData(body);
resolve(true);
@@ -265,11 +267,13 @@ class HeroesCountersProvider extends HotsDraftSuggestions {
'json': true
}, (error, response, body) => {
this.updateActive = false;
- if (error) {
+ if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
this.loadUpdateData(body);
resolve(true);
diff --git a/src/external/hotsdraft.js b/src/external/hotsdraft.js
index 31511a0..ff17c9e 100644
--- a/src/external/hotsdraft.js
+++ b/src/external/hotsdraft.js
@@ -135,9 +135,11 @@ class HeroesCountersProvider extends HotsDraftSuggestions {
this.updateActive = false;
if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
this.loadCoreData(body);
resolve(true);
@@ -261,11 +263,13 @@ class HeroesCountersProvider extends HotsDraftSuggestions {
'json': true
}, (error, response, body) => {
this.updateActive = false;
- if (error) {
+ if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
resolve(body);
});
diff --git a/src/external/icyveins.js b/src/external/icyveins.js
index c07a6e8..2d0bc93 100644
--- a/src/external/icyveins.js
+++ b/src/external/icyveins.js
@@ -71,9 +71,11 @@ class IcyVeinsProvider extends HotsTalentSuggestions {
this.updateActive = false;
if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
this.loadCoreData(body);
resolve(true);
@@ -131,9 +133,11 @@ class IcyVeinsProvider extends HotsTalentSuggestions {
}, (error, response, body) => {
if (error || (typeof response === "undefined")) {
reject(error);
+ return;
}
if (response.statusCode !== 200) {
reject('Invalid status code <' + response.statusCode + '>');
+ return;
}
this.parseBuildSectionData(body, section);
resolve(true);
diff --git a/src/hots-draft-app.js b/src/hots-draft-app.js
index fa6116c..d15c65d 100644
--- a/src/hots-draft-app.js
+++ b/src/hots-draft-app.js
@@ -246,6 +246,7 @@ class HotsDraftApp extends EventEmitter {
maps: this.gameData.maps,
replays: this.gameData.replays,
substitutions: this.gameData.substitutions,
+ playerBattleTags: this.gameData.playerBattleTags,
playerPicks: this.gameData.playerPicks
});
}
@@ -620,7 +621,8 @@ class HotsDraftApp extends EventEmitter {
heroName: player.getCharacter(),
heroNameImage: (player.isLocked() ? player.getImageHeroName().toString('base64') : null),
detectionFailed: player.isDetectionFailed(),
- locked: player.isLocked()
+ locked: player.isLocked(),
+ recentPicks: player.getRecentPicks()
};
}
collectReplayData(replayIndex) {
diff --git a/src/hots-draft-gui.js b/src/hots-draft-gui.js
index b6be4de..963333d 100644
--- a/src/hots-draft-gui.js
+++ b/src/hots-draft-gui.js
@@ -141,11 +141,8 @@ class HotsDraftGui extends EventEmitter {
name = name.toUpperCase();
return name;
}
- hasPlayerKnownPicks(playerName) {
- return this.gameData.playerPicks.hasOwnProperty(playerName);
- }
- getPlayerKnownPicks(playerName) {
- return this.gameData.playerPicks[playerName];
+ hasPlayerRecentPicks(playerName) {
+ return this.getPlayerBattleTags(playerName).length > 0;
}
getDisplays() {
return this.displays;
diff --git a/src/hots-draft-player.js b/src/hots-draft-player.js
index 3810171..10ad950 100644
--- a/src/hots-draft-player.js
+++ b/src/hots-draft-player.js
@@ -14,6 +14,7 @@ class HotsDraftPlayer extends EventEmitter {
this.locked = false;
this.imagePlayerName = null;
this.imageHeroName = null;
+ this.recentPicks = null;
}
getIndex() {
return this.index;
@@ -33,6 +34,9 @@ class HotsDraftPlayer extends EventEmitter {
getImageHeroName() {
return this.imageHeroName;
}
+ getRecentPicks() {
+ return this.recentPicks;
+ }
isDetectionFailed() {
return this.detectionError;
}
@@ -90,6 +94,10 @@ class HotsDraftPlayer extends EventEmitter {
setImageHeroName(image) {
this.imageHeroName = image;
}
+ setRecentPicks(picks) {
+ this.recentPicks = picks;
+ this.emit("change");
+ }
}
module.exports = HotsDraftPlayer;
diff --git a/src/hots-draft-screen.js b/src/hots-draft-screen.js
index 77b87d9..5884db5 100644
--- a/src/hots-draft-screen.js
+++ b/src/hots-draft-screen.js
@@ -585,12 +585,13 @@ class HotsDraftScreen extends EventEmitter {
detections.push(
playerImgName.getBufferAsync(jimp.MIME_PNG).then((buffer) => {
imagePlayerName = buffer;
- return ocrCluster.recognize(buffer, this.tessLangs+"+lat+rus+kor+chi_sim", this.tessParams);
+ return ocrCluster.recognize(buffer, this.tessLangs+"+lat+rus+kor", this.tessParams);
}).then((result) => {
let playerName = result.text.trim();
console.log(playerName+" / "+result.confidence);
player.setName(playerName, playerNameFinal);
player.setImagePlayerName(imagePlayerName);
+ this.app.gameData.updatePlayerRecentPicks(player);
return playerName;
})
);
diff --git a/src/hots-game-data.js b/src/hots-game-data.js
index b6edf9a..f4eae5f 100644
--- a/src/hots-game-data.js
+++ b/src/hots-game-data.js
@@ -6,6 +6,7 @@ const fs = require('fs');
const path = require('path');
const jimp = require('jimp');
const EventEmitter = require('events');
+const {BigQuery} = require('@google-cloud/bigquery');
// Local classes
const HotsReplay = require('hots-replay');
@@ -14,6 +15,11 @@ const HotsReplayUploaders = {
};
const HotsHelpers = require('./hots-helpers.js');
+// BigQuery Instance
+let bigQueryHotsApi = new BigQuery({
+ projectId: HotsHelpers.getConfig().getOption("googleBigQueryProject"),
+ keyFilename: HotsHelpers.getConfig().getOption("googleBigQueryAuth")
+});
class HotsGameData extends EventEmitter {
@@ -40,6 +46,7 @@ class HotsGameData extends EventEmitter {
lastUpdate: 0
};
this.playerPicks = {};
+ this.playerBattleTags = {};
this.saves = {
latestSave: { file: null, mtime: 0 },
lastUpdate: 0
@@ -114,25 +121,20 @@ class HotsGameData extends EventEmitter {
replayDetails: replay.getReplayDetails(),
replayUploads: {}
};
+ let battleTags = replay.getReplayBattleLobby().battleTags;
// Keep information about recent player picks
for (let i = 0; i < replayData.replayDetails.m_playerList.length; i++) {
let player = replayData.replayDetails.m_playerList[i];
- if (!this.playerPicks.hasOwnProperty(player.m_name)) {
- this.playerPicks[player.m_name] = [];
- }
- let playerHeroFound = false;
- for (let h = 0; h < this.playerPicks[player.m_name].length; h++) {
- if (this.playerPicks[player.m_name][h][0] === player.m_hero) {
- this.playerPicks[player.m_name][h][1]++;
- playerHeroFound = true;
+ if (battleTags.length > i) {
+ // Add battle tag
+ let playerBattleTag = battleTags[i].tag;
+ if (!this.playerBattleTags.hasOwnProperty(player.m_name)) {
+ this.playerBattleTags[player.m_name] = [];
+ }
+ if (this.playerBattleTags[player.m_name].indexOf(playerBattleTag) === -1) {
+ this.playerBattleTags[player.m_name].push(playerBattleTag);
}
}
- if (!playerHeroFound) {
- this.playerPicks[player.m_name].push([ player.m_hero, 1 ]);
- }
- this.playerPicks[player.m_name].sort((a,b) => {
- return b[1] - a[1];
- });
}
// Return replay data
resolve(replayData);
@@ -324,12 +326,13 @@ class HotsGameData extends EventEmitter {
let cacheContent = fs.readFileSync(storageFile);
try {
let cacheData = JSON.parse(cacheContent.toString());
- if (cacheData.formatVersion == 4) {
+ if (cacheData.formatVersion == 5) {
this.languageOptions = cacheData.languageOptions;
this.maps = cacheData.maps;
this.heroes = cacheData.heroes;
this.replays = cacheData.replays;
this.playerPicks = cacheData.playerPicks;
+ this.playerBattleTags = cacheData.playerBattleTags;
}
} catch (e) {
console.error("Failed to read gameData data!");
@@ -361,12 +364,13 @@ class HotsGameData extends EventEmitter {
// Write specific type into cache
let storageFile = this.getFile();
fs.writeFileSync( storageFile, JSON.stringify({
- formatVersion: 4,
+ formatVersion: 5,
languageOptions: this.languageOptions,
maps: this.maps,
heroes: this.heroes,
replays: this.replays,
- playerPicks: this.playerPicks
+ playerPicks: this.playerPicks,
+ playerBattleTags: this.playerBattleTags
}) );
}
update() {
@@ -421,7 +425,7 @@ class HotsGameData extends EventEmitter {
},
'jar': true
}, (error, response, body) => {
- if (error) {
+ if (error || (typeof response === "undefined")) {
reject(error);
return;
}
@@ -475,7 +479,7 @@ class HotsGameData extends EventEmitter {
},
'jar': true
}, (error, response, body) => {
- if (error) {
+ if (error || (typeof response === "undefined")) {
reject(error);
return;
}
@@ -673,6 +677,50 @@ class HotsGameData extends EventEmitter {
throw error;
});
}
+
+ /**
+ * @param {HotsDraftPlayer} player
+ */
+ updatePlayerRecentPicks(player) {
+ let playerName = player.getName();
+ if (!this.playerBattleTags.hasOwnProperty(playerName)) {
+ // No battletags known for player! Unable to fetch recent picks.
+ return;
+ }
+ let playerPicks = {};
+ for (let i = 0; i < this.playerBattleTags[playerName].length; i++) {
+ let playerBattleTag = this.playerBattleTags[playerName][i];
+ if (!this.playerPicks.hasOwnProperty(playerBattleTag)) {
+ // No recent picks known for player! Fetch from hotsapi.net
+ this.playerPicks[playerBattleTag] = [];
+ let playerBattleTagParts = playerBattleTag.match(/^(.+)#([0-9]+)$/);
+ if (playerBattleTagParts) {
+ let querySql = `
+ SELECT p.hero as heroName, COUNT(*) as pickCount
+ FROM \`cloud-project-179020.hotsapi.replays\` r, UNNEST(players) as p
+ WHERE (p.battletag_name = @tagName) AND (p.battletag_id = @tagId)
+ GROUP BY heroName
+ ORDER BY pickCount DESC`;
+ let queryOptions = {
+ query: querySql,
+ params: { tagName: playerBattleTagParts[1], tagId: parseInt(playerBattleTagParts[2]) }
+ };
+ bigQueryHotsApi.query(queryOptions).then((result) => {
+ result[0].forEach((row) => {
+ this.playerPicks[playerBattleTag].push([ row.heroName, row.pickCount ]);
+ });
+ playerPicks[playerBattleTag] = this.playerPicks[playerBattleTag];
+ player.setRecentPicks(playerPicks);
+ this.save();
+ });
+ }
+ return;
+ } else {
+ playerPicks[playerBattleTag] = this.playerPicks[playerBattleTag];
+ player.setRecentPicks(playerPicks);
+ }
+ }
+ }
progressReset() {
this.updateProgress.tasksPending = 1;
this.updateProgress.tasksDone = 0;