Moderator DvDivXXX Posted July 28, 2023 Author Moderator Share Posted July 28, 2023 (edited) 9 minutes ago, Master-17 said: For some reason, the Hentai Heroes++ League Booster Detector was not included in the set. There's actually a reason, but thank you for preparing the entry (MM style, with supported games and everything, nice!). If Zoo is fine with us linking to it directly, we can just copy-paste from your post and add it to the list on page 1. 6 hours ago, DvDivXXX said: What I'm wondering is if the implicit rule Numbers used to have in place for that script still applies these days. I'm referring to not giving the link directly so that it's a tiny "treasure hunt" of sorts, and you have to find it yourself to install it (hint: if you have two brain cells active and you know it's a script by Numbers and Zoo, it's not a big challenge ^^ but I liked the idea of limiting this tool to people with two or more brain cells active and the motivation to make one quick search to gain access to an amazing free tool). Is that still relevant for @zoopokemon as the current maintainer of the script? I don't want to post it if Zoo prefers to keep the "treasure hunt" vibe around it. EDIT: Come to think of it, for the average player, visiting the forum, then finding this thread might be as much of a "treasure hunt" than just finding Zoo's GitHub and browsing for it... ^^ So maybe I'm being too cautious with this. Edited July 28, 2023 by DvDivXXX updated 1 Link to comment Share on other sites More sharing options...
Tom208 Posted July 28, 2023 Share Posted July 28, 2023 About supported games, TPsH is supported by OCD HH++ script too. 2 1 Link to comment Share on other sites More sharing options...
Der DinX Posted July 28, 2023 Share Posted July 28, 2023 2 hours ago, Tom208 said: TPsH is supported by OCD Even if BDSM isn't supported there, it works as well with some minor but not relevant Issues. Just wanted to mention. Link to comment Share on other sites More sharing options...
Tom208 Posted July 28, 2023 Share Posted July 28, 2023 When I say, TPsH is supported, I just added the website to the list of the websites where the script is active and I also added the villains. For the rest, I consider it works in exactly the same way as HH. Link to comment Share on other sites More sharing options...
Der DinX Posted July 28, 2023 Share Posted July 28, 2023 (edited) On 7/28/2023 at 5:38 PM, Tom208 said: also added the villains That is mainly the minor Issue I was referring to. 😁 Tried to fix it myself, but as I don't know how it really works, I wasn't succesfull. Just adding Names isn't sufficiant, there has to be something else. On 7/28/2023 at 7:44 AM, 430i said: One more small script here: Found a minor Issue with that script. When an animated Scene of the Girl is selected, the little squares are still highlited when hovering above, and the face changes in the Thumbnail on the left. But the other poses will not be shown in large. The Slideshow cannot be started also, even when another scene is selected it doesn't work, although the is shown. I have to pick another scene, then choose another girl first and go back. Now it works. // @description Allows you to display any stage image of any harem girl, owned ones or not. Works also in event display and Places of Power. Includes zoom-in feature to display full-size girl images gallery (lightbox). Quote Works also in event display But only if the Animation is turned of in game settings. And another funny thing, you can make the Girls fly away. 😁 🕊️ Each time you hover over a square, the pic gets lifted up a bit. If you do that fast enough, she just flies away. 😉 😉🕊️😁 ❌ Edited July 30, 2023 by Der DinX 2 Link to comment Share on other sites More sharing options...
430i Posted August 4, 2023 Share Posted August 4, 2023 (edited) On 7/28/2023 at 6:37 PM, Der DinX said: Found a minor Issue with that script. I will take a look, but no promises as I dont use the animations. @zoopokemon There is an issue with the latest script version - the league stats at the bottom show up very rarely. I think the league table is created client-side, so not all UI elements that you expect are there, or rather it largely depends on the timing. I think wrapping it in a setTimeout() with 250ms delay should fix it (at least this worked for me). I personally am missing a lot more functionality in the league table, so I have updated my personal script (but you need zoo's script as well, as mine is just an extension of his). Here are few improvements in the league table: - hide/show opponents - highlight expired boosters (although who knows whether they really expired) - show girl power and themes (instead of the useless synergy popup) - button to load the user profile data (but not really show it) - this will show their best D3 results in the league table, but only after reload @zoopokemon If some of those are interesting for you, feel free to copy paste them. I explicitly havent fixed the sorting, as I am hoping that kinkoid will fix it in the next patch. And here is the script: Spoiler // ==UserScript== // @name Hentai Heroes+++ // @description Few QoL improvement and additions for competitive PvP players. // @version 0.14.0 // @match https://*.hentaiheroes.com/* // @match https://nutaku.haremheroes.com/* // @match https://*.gayharem.com/* // @match https://*.comixharem.com/* // @run-at document-body // @grant none // @author 430i // ==/UserScript== const {$, location, localStorage: storage} = window // localStorage keys const LS_CONFIG_NAME = 'HHPlusPlusPlus' const LEAGUE_BASE_KEY = LS_CONFIG_NAME + ".League"; const LEAGUE_SNAPSHOT_BASE_KEY = LEAGUE_BASE_KEY + ".Snapshot"; const CURRENT_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Current"; const PREVIOUS_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Previous"; const LEAGUE_PLAYERS_KEY = LEAGUE_BASE_KEY + ".Players"; const EQUIPMENT_COLLECTOR_KEY = LS_CONFIG_NAME + ".Equipment.Current"; // 3rd party localStorage keys const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus' const FOUGHT_OPPONENTS_HIDDEN = LS_CONFIG_HHPLUSPLUS_NAME + "FoughtOpponentsHidden" // icon paths const PATH_GROUPS = '<path d="M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14 c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43V18l4.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2 c0-1.1-0.9-2-2-2s-2,0.9-2,2C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14 c-0.39,0-0.76,0.04-1.13,0.1c0.4,0.68,0.63,1.46,0.63,2.29V18l4.5,0V16.43z M16.24,13.65c-1.17-0.52-2.61-0.9-4.24-0.9 c-1.63,0-3.07,0.39-4.24,0.9C6.68,14.13,6,15.21,6,16.39V18h12v-1.61C18,15.21,17.32,14.13,16.24,13.65z M8.07,16 c0.09-0.23,0.13-0.39,0.91-0.69c0.97-0.38,1.99-0.56,3.02-0.56s2.05,0.18,3.02,0.56c0.77,0.3,0.81,0.46,0.91,0.69H8.07z M12,8 c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.45,8,12,8 M12,6c-1.66,0-3,1.34-3,3c0,1.66,1.34,3,3,3s3-1.34,3-3 C15,7.34,13.66,6,12,6L12,6z"/>'; const PATH_GROUP = '<path d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z"/>'; const PATH_CLEAR = '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>'; class CurrentEquipmentCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('shop')) { return; } HHPlusPlus.Helpers.defer(() => { const eqElements = $("#equiped div.slot:not(:empty)[subtype!='0']"); if (eqElements.length != 6) { console.log("Did not find 6 equipment elements."); return; } const equipment = eqElements.map(function() { return $(this).data("d")}).get(); const equipmentStripped = equipment.map((e) => { return { id: e.identifier, // e.g "EF10", used to show the icon type: e.type, // always "armor" subtype: parseInt(e.subtype), // 1, 2, 3, 4, 5 or 6 mono_rainbow: parseInt(e.name_add), // 1 for hardcore only, 2 for charm only, 3 for know-how only, 16 for rainbow, etc.. carac1: e.carac1, carac2: e.carac2, carac3: e.carac3, harmony: e.chance, endurance: e.endurance, }; }); window.localStorage.setItem(EQUIPMENT_COLLECTOR_KEY, JSON.stringify(equipmentStripped)); }); } static get() { return JSON.parse(window.localStorage.getItem(EQUIPMENT_COLLECTOR_KEY)) || []; } } class LeaguePlayersCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')) { return; } HHPlusPlus.Helpers.defer(() => { HHPlusPlus.Helpers.onAjaxResponse(/action=fetch_hero&id=profile/, LeaguePlayersCollector.collectPlayerPlacementsFromAjaxResponse); LeaguePlayersCollector.collectPlayerData(); }); } static collectPlayerPlacementsFromAjaxResponse(response, opt) { const html = $("<div/>").html(response.html); const id = html.find("div.ranking_stats .id").text().match(/\d+/)[0]; const username = html.find(".hero_info h3 .hero-name").text(); const level = html.find('div[hero="level"]').text().trim(); const number_mythic_equipment = html.find('.hero_items .slot.mythic').length; const d3_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/9.png"]) span') // 9.png is D3 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (!id || !username || !level) { window.popup_message("Error when parsing player data."); return; } if (!d3_placement || d3_placement.length != 2) { // make sure our parser is working by checking the D2 data const d2_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/8.png"]) span') // 8.png is D2 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (d2_placement.length != 2) { window.popup_message("Error when parsing D2 player data."); } d3_placement.push(-1, 0); } const data = { id: parseInt(id), number_mythic_equipment, best_placement: d3_placement[0], placement_count: d3_placement[1], }; LeaguePlayersCollector.storePlayerData(data); } static collectPlayerData() { const me = LeaguePlayersCollector.me(); for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const girls = player.player.team.girls; const girl_levels = girls.map(g => g.level); const girl_levels_max = Math.max(...girl_levels); const girl_levels_total = girl_levels.reduce((a, b) => a + b, 0); const girl_levels_avg = Math.floor(girl_levels_total / girl_levels.length); const expected_points = LeaguePlayersCollector.calculateExpectedPoints(me, player); const data = { id: parseInt(player.player.id_fighter), username: player.player.nickname, level: parseInt(player.player.level), damage: player.player.damage, defense: player.player.defense, harmony: player.player.chance, ego: player.player.remaining_ego, power: player.player.team.total_power, club_id: player.player.club?.id_club, club_name: `"${player.player.club?.name || ''}"`, girl_levels_avg, girl_levels_max, expected_points, } LeaguePlayersCollector.storePlayerData(data); } } static me() { for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; if (player.player.nickname == memberInfo.name) { return player; } } window.popup_message("Could not find myself in the league table."); } static calculateExpectedPoints(me, other) { return undefined; } static storePlayerData(data) { const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; if (players[data.id] == undefined) { players[data.id] = {}; } Object.assign(players[data.id], data); storage.setItem(LEAGUE_PLAYERS_KEY, JSON.stringify(players)); } static export() { const columns = [ "id", "username", "level", "damage", "defense", "harmony", "ego", "power", "club_id", "club_name", "girl_levels_max", "girl_levels_avg", "expected_points", "number_mythic_equipment", "best_placement", "placement_count", ] const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; const data = Object.values(players).map(player => columns.map(column => player[column])); console.log([columns].concat(data).map(t => t.join(",")).join("\n")); } static clear() { storage.removeItem(LEAGUE_PLAYERS_KEY); } } class MyModule { constructor ({name, configSchema}) { this.group = '430i' this.name = name this.configSchema = configSchema this.hasRun = false this.insertedRuleIndexes = [] this.sheet = HHPlusPlus.Sheet.get() } insertRule (rule) { this.insertedRuleIndexes.push(this.sheet.insertRule(rule)) } tearDown () { this.insertedRuleIndexes.sort((a, b) => b-a).forEach(index => { this.sheet.deleteRule(index) }) this.insertedRuleIndexes = [] this.hasRun = false } } class LeagueScoutModule extends MyModule { constructor () { const baseKey = 'leagueScout' const configSchema = { baseKey, default: true, label: `Gather information about league opponents`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run () { if (this.hasRun || !this.shouldRun()) {return} $(document).on('league:rollover', () => { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; storage.setItem(PREVIOUS_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY); }) HHPlusPlus.Helpers.defer(() => { // read and store data const playerData = this.readPlayerData(); const snapshot = this.createSnapshot(playerData); this.storeSnapshot(snapshot); // create ui elements setTimeout(() => { const parent = $('div.league_buttons'); this.createDownloadButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY, PATH_GROUPS); this.createClearButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY); this.createDownloadButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY, PATH_GROUP); this.createClearButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY); }, 250); }); this.hasRun = true; } readPlayerData() { const data = []; for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const id = player.id_player; const rank = player.place; const name = player.player.nickname; const country = player.country; const level = parseInt(player.player.level); const points = parseInt(player.player_league_points); const elements = player.player.team.theme; data.push({id, name, rank, level, elements, points, country}); } // Sort the parsed data by rank. data.sort((a, b) => a.rank > b.rank); return data; } createSnapshot(playerData) { const numPlayers = playerData.length; const currentDate = new Date(window.server_now_ts * 1000); const leagueEndDate = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000); return { date: currentDate, league_end: leagueEndDate, num_players: numPlayers, player_data: playerData, } } storeSnapshot(snapshot) { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; if (data.length && JSON.stringify(data[data.length - 1].player_data) === JSON.stringify(snapshot.player_data)) { return; } data.push(snapshot); storage.setItem(CURRENT_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); } createButton(id, path) { return `<svg id="${id}" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" width="16px" height="16px" fill="#FFFFFF" style="cursor: pointer;"><g><rect fill="none" height="24" width="24"/></g><g>${path}</g></svg>` } createDownloadButton(parent, what, icon) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `download-${friendlyId}`; const downloadButton = this.createButton(buttonId, icon); parent.append(downloadButton); $(document.body).on('click', `#${buttonId}`, () => { const data = JSON.parse(storage.getItem(what)) || []; const separator = "," const columns = ["date", "player_id", "player_name", "player_rank", "player_points"]; const values = data.flatMap((e) => e.player_data.map((p) => [e.date, p.id, p.name, p.rank, p.points].join(separator))); let csvContent = `sep=${separator}\n` + columns.join(separator) + "\n" + values.join("\n"); var element = document.createElement('a'); element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent)); element.setAttribute('download', `${friendlyId}.csv`); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); } createClearButton(parent, what) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `clear-${friendlyId}`; const clearButton = this.createButton(buttonId, PATH_CLEAR); parent.append(clearButton); $(document.body).on('click', `#${buttonId}`, () => { storage.removeItem(what); }); } } class LeagueTableModule extends MyModule { constructor () { const baseKey = 'leagueTable' const configSchema = { baseKey, default: true, label: `Extend league table with additional opponents' information`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run() { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { this.extendLeagueDataModel(); setTimeout(() => { this.showHideUnhideButton(); this.extendLeagueTable(); }, 250); }); this.hasRun = true; } extendLeagueDataModel() { const players_data = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; // add power to the existing `opponents_list` data model for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const id = parseInt(player.player.id_fighter); const player_data = players_data[id]; const power = player_data != undefined ? player_data.power : -1; const expected_points = player_data != undefined ? player_data.expected_points : -1; const best_placement = player_data != undefined ? player_data.best_placement : -1; const placement_count = player_data != undefined ? player_data.placement_count : -1; window.opponents_list[r].power = power; window.opponents_list[r].expected_points = parseFloat(expected_points); window.opponents_list[r].best_placement = best_placement; window.opponents_list[r].placement_count = placement_count; } } extendLeagueTable() { // Additional CSS classes this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:var(--var-color-mythic)}`); this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}'); this.insertRule('.league_table .data-list .data-row .data-column[column="team"] span {background-size: 2em}'); this.insertRule('.league_table .data-list .data-row .data-column[column="reload"] button {padding: 2px 5px}'); // Highlight expired boosters $('div.league_table div.data-column[column=boosters] div[type=booster]') .each(function(index) { const d = $(this).data('d'); if (d.expiration == 0) { $(this).css({'border': '1px solid red'}); } }) // Create "power", "e[x]" ... $('div.league_table div.head-row > div.head-column[column=team]') .after( '<div class="data-column head-column" column="power">Power</div>' + '<div class="data-column head-column" column="expected_points">E[X]</div>' ); // ... and "reload" headers. $('div.league_table div.head-row > div.head-column:last-child') .after('<div class="data-column head-column" column="reload"></div>'); // Populate additional league table columns: // - add "power" and "e[x]" data // - add best placement indicators next to the opponents name // - replace synergy tooltip with theme icons $('div.league_table') .find('div.body-row') .each(function(index) { const opponent = window.opponents_list[index]; // power and e[x] columns const power = opponent.power; const e_x = opponent.expected_points; $(this).find('div.data-column[column=team]').after( `<div class="data-column" column="power">${(power).toFixed()}</div> <div class="data-column" column="expected_points">${(e_x).toFixed(2)}</div>` ); // best placement indicator next to the nickname if (opponent.best_placement == 1) { $(this).find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`) } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) { $(this).find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`) } // show theme icons instead of syngergy tooltip const themes = opponent.player.team.theme.split(',').map(t => t || "balanced"); const theme_icons = themes.map(t => `<span class="${t}_element_icn ${t}_theme_icn"></span>`); $(this).find('div.data-column[column=team]').html(theme_icons.join('')); // show best placement and mythic equipment reload button if (opponent.best_placement == undefined) { const load_button = $('<button class="blue_button_L">↻</button>'); load_button.on('click', () => { window.$.post({ url: '/ajax.php', data: { action: 'fetch_hero', id: 'profile', preview: false, player_id: parseInt(opponent.player.id_fighter), }, success: (data) => {} }) }) const div = $('<div class="data-column" column="reload"/>'); div.html(load_button) $(this).find('div.data-column:last-child').after(div); } }); // re-run the league table sorting with the additional "power" sort option // var sort_options = { level: 'number', nb_challenges_played: 'number', power: 'number', expected_points: 'number', place: 'number'}; // var sort_class = new TableSorting(opponents_list, 'id_player', 'place', sort_options, 'leagues', 'class'); // sort_class.init(); } showHideUnhideButton() { const hide = JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)) || false; const caption = hide ? "Show" : "Hide"; this.hideUnhide(hide); const btn = $(`<button id="beaten_opponents2" class="blue_button_L">${caption}</button>`); btn.on('click', () => { const next = !JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)); this.hideUnhide(next); storage.setItem(FOUGHT_OPPONENTS_HIDDEN, next); btn.text(next ? "Show" : "Hide"); }); $('div.league_buttons').append(btn); } hideUnhide(hide) { $('div.league_table') .find('div.body-row') .each(function(index) { const results = $(this).find('div.data-column[column=match_history]').find('div[class!="result "]').length; const fought_all = results == 3; if (fought_all && hide) { $(this).hide(); } else if (fought_all && !hide) { $(this).show(); } }); $('#leagues .league_content .league_table').getNiceScroll().resize(); } } class HaremFiltersModule extends MyModule { constructor () { const baseKey = 'haremFilters' const configSchema = { baseKey, default: true, label: `Show additional harem filters`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('harem') && !HHPlusPlus.Helpers.isCurrentPage('hero')} run () { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { // figure out the event ids for LD const ld_event_ids = $("select[name=event] option") .filter((idx, opt) => opt.text.includes("Legendary Days")) .map((idx, opt) => opt.value) .get() .map(id => parseInt(id)); // mark the girls as LD girls Object.values(window.girlsDataList) .filter(e => e.source_selectors.event) .filter(e => e.source_selectors.event.filter(id => ld_event_ids.includes(id)).length) .forEach(g => g.source_selectors.legendary_days = [0]); // TODO fill id, optional // add dropdown option $("select[name=lists]").append(new Option("Legendary Days", "legendary_days")); }); this.hasRun = true; } } setTimeout(() => { const {hhPlusPlusConfig, HHPlusPlus, location} = window; if (!$) { console.log('No jQuery found. Probably an error page. Ending the script here') return; } else if (!hhPlusPlusConfig || !HHPlusPlus) { console.log("HH++ is not available"); return; } else if (location.pathname === '/' && (location.hostname.includes('www') || location.hostname.includes('test'))) { console.log("iframe container, do nothing"); return; } // collectors CurrentEquipmentCollector.collect() LeaguePlayersCollector.collect() // modules const modules = [ new LeagueScoutModule(), new LeagueTableModule(), new HaremFiltersModule(), ] // register our own window hooks window.HHPlusPlusPlus = { exportLeagueData: LeaguePlayersCollector.export, clearLeagueData: LeaguePlayersCollector.clear, }; hhPlusPlusConfig.registerGroup({ key: '430i', name: '430i\'s Scripts' }) modules.forEach(module => hhPlusPlusConfig.registerModule(module)) hhPlusPlusConfig.loadConfig() hhPlusPlusConfig.runModules() HHPlusPlus.Helpers.runDeferred() }, 1) Edited August 7, 2023 by Ravi-Sama Ravi edit: added spoiler tag, so don't have to scroll. 6 1 Link to comment Share on other sites More sharing options...
Bobick Posted August 4, 2023 Share Posted August 4, 2023 (edited) 2 hours ago, 430i said: And here is the script: Thanks a ton! All the additions are very helpful. Do you expect any conflicts with zoo's scripts in future, or can the scripts stay installed in parralel? I see only NaN values for E(X), even after entering the battle screens. Is there something I can do to avoid this? Furthermore, is there a smart way to reload the table after triggering the opponent information button? Reloading the page does close the table, so I always had to enter and leave the (or another) battle screen. Good to know there are even more capable coders in the community, increasing my hopes that the scripts will remain functional. Edited August 4, 2023 by Bobick 1 Link to comment Share on other sites More sharing options...
430i Posted August 4, 2023 Share Posted August 4, 2023 34 minutes ago, Bobick said: Do you expect any conflicts with zoo's scripts in future, or can the scripts stay installed in parralel? I expect that there will be conflicts at some point, if/when he introduces similar features, but I will fix them and upload a new version. The more features are part of zoo's version the better is for the community (as his version is widely used), however I am not sure about his motivation levels and whether he has even anything planned for HH++ - his last message was very downbeat. Hence the reason for my version - the current league rework has actually a lot of potential, but kinkoid is in now way taking advantage of that. 41 minutes ago, Bobick said: I see only NaN values for E(X) Yeah that's expected, as it is still work in progress. I have a basic version running, but there are still a ton of issues with the calculation, so I havent shared it yet. 44 minutes ago, Bobick said: Furthermore, is there a smart way to reload the table after triggering the opponent information button? Yes there is, I did the bare minimum to get some usable league data and will work on improving the usability next. 1 Link to comment Share on other sites More sharing options...
zoopokemon Posted August 5, 2023 Share Posted August 5, 2023 7 hours ago, 430i said: @zoopokemon There is an issue with the latest script version - the league stats at the bottom show up very rarely. I think the league table is created client-side, so not all UI elements that you expect are there, or rather it largely depends on the timing. I think wrapping it in a setTimeout() with 250ms delay should fix it (at least this worked for me). I personally am missing a lot more functionality in the league table, so I have updated my personal script (but you need zoo's script as well, as mine is just an extension of his). Here are few improvements in the league table: - hide/show opponents - highlight expired boosters (although who knows whether they really expired) - show girl power and themes (instead of the useless synergy popup) - button to load the user profile data (but not really show it) - this will show their best D3 results in the league table, but only after reload @zoopokemon If some of those are interesting for you, feel free to copy paste them. I explicitly haven't fixed the sorting, as I am hoping that kinkoid will fix it in the next patch. Yeah, I forgot to add a Helpers.doWhenSelectorAvailable for that, will be fixed in the next update. Already working on adding a quick nav to the pre-battle page, and replacing the team ! with the team themes. Planning on replacing the hide/show opponents with a better filtering system that would include fightable opponent, team theme, and booster status. I might take the highlight expired boosters, but since the that's all bugged idk. I don't think I can add in the load the user profile data. Also note that for your script it breaks if you sort the table. Also the league sim simply can't be ran in the league page any more because your very own data there doesn't get updated when you switch teams or otherwise change your stats, not to mention that the stats aren't dynamically updated based on the opponent. If it could I would replace the "power" with the expected score. To better keep track of my plans for HH++ BDSM I suggest visiting the HH++ discord linked in the script's settings, I don't engage in the forum too often. 1 3 1 1 Link to comment Share on other sites More sharing options...
Horsting Posted August 5, 2023 Share Posted August 5, 2023 10 hours ago, zoopokemon said: Also the league sim simply can't be ran in the league page any more because your very own data there doesn't get updated when you switch teams or otherwise change your stats, not to mention that the stats aren't dynamically updated based on the opponent. Theoretically you could calculate at least the defence per opponent based on each playful penalty. But yeah, at least the own stats on the table are not accurate, e.g. they do not change on the table (immediately) when you change your team. Alternative would be to save the result from the battle page to show on the table, as a memory. It may not be accurate anymore, as the opponent may have changed (especially booster status), but it may give an orientation. ... but probably causes more confusion than help 🤔. Link to comment Share on other sites More sharing options...
Moderator Ravi-Sama Posted August 5, 2023 Moderator Share Posted August 5, 2023 18 hours ago, 430i said: I personally am missing a lot more functionality in the league table, so I have updated my personal script (but you need zoo's script as well, as mine is just an extension of his). Here are few improvements in the league table: - hide/show opponents - highlight expired boosters (although who knows whether they really expired) - show girl power and themes (instead of the useless synergy popup) - button to load the user profile data (but not really show it) - this will show their best D3 results in the league table, but only after reload I really like these upgrades, thanks! Highlighted expired boosters red is especially helpful. Also, I used to visit the player profile pages to check best rank, and sometimes mythic gear. Hopefully, they'll return too. Can always copy the game IDs, and manually create profile links, but that's too tedious. For example: Ravi-Sama's ID 659374 Profile Link: https://www.hentaiheroes.com/hero/659374/profile.html Can replace the ID #s w/ another player's, to view their profile. Was wondering if it's possible to sort players by booster expiration times, but maybe that could be more easily tested after sorting is fixed next week. If possible, then there'd be even less scrolling. 4 hours ago, Ravi-Sama said: Also, have some good news that league sorting should be fixed by next week, and the change team function will be re-added to the league page. We'll eventually be able to choose either the girl or stats view, as the default. I prefer stats. It's just more practical. Have to keep switching back in PSH atm. 1 Link to comment Share on other sites More sharing options...
430i Posted August 6, 2023 Share Posted August 6, 2023 (edited) Below is the updated version of my script (a mod can move the relevant posts to a new topic if you deem necessary). A list of the features: Hide/show fought opponents Highlight expired boosters (although who knows whether they really expired) Show girl power and themes (instead of the useless synergy popup) [updated] Button to load the user profile data (but not really show it) - this will display their best D3 results in the league table next to the player's name. No need to reload the page anymore and the column is hidden when all data is loaded. [new] Show your real team colors, team power and stats (the previous version only showed the snapshotted data, zoo's version as well shows the latest snapshot). Of course your stats might change depending on the opponent and your/their counters, but the values shown should be the closest "opponent-independent" values that are available in the game. There are still few known issues, see below. [new] Show the expected points for every opponent right in the leagues table. Right now the information is blurred, but it is trivial to unblur it (left as an exercise for the reader). [new] A check on the pre-battle page whether you have a suitable not-equipped mythic equipment in your inventory. Known issues: when you load the league page for the very first time the numbers (your own stats and all expected points) might be off until you open the team selection screen and (re-)select your team similarly when your boosters expire or you apply a new booster the numbers (your own stats and all expected points) will be incorrect until you visit the team selection page (no need to select a team, I think) Spoiler // ==UserScript== // @name Hentai Heroes+++ // @description Few QoL improvement and additions for competitive PvP players. // @version 0.16.8 // @match https://*.hentaiheroes.com/* // @match https://nutaku.haremheroes.com/* // @run-at document-body // @grant none // @author 430i // ==/UserScript== const {$, location, localStorage: storage} = window // localStorage keys const LS_CONFIG_NAME = 'HHPlusPlusPlus' const LEAGUE_BASE_KEY = LS_CONFIG_NAME + ".League"; const LEAGUE_SNAPSHOT_BASE_KEY = LEAGUE_BASE_KEY + ".Snapshot"; const CURRENT_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Current"; const PREVIOUS_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Previous"; const LEAGUE_PLAYERS_KEY = LEAGUE_BASE_KEY + ".Players"; const EQUIPMENT_KEY = LS_CONFIG_NAME + ".Equipment"; const EQUIPMENT_CURRENT_KEY = EQUIPMENT_KEY + ".Current"; const EQUIPMENT_BEST_MYTHIC_KEY = EQUIPMENT_KEY + ".Mythic"; const EQUIPMENT_FAVORITE_KEY = EQUIPMENT_KEY + ".Favorite"; const TEAMS_BASE_KEY = LS_CONFIG_NAME + ".Teams"; const TEAMS_ALL_KEY = TEAMS_BASE_KEY + ".All"; const TEAMS_CURRENT_ID_KEY = TEAMS_BASE_KEY + ".CurrentId"; // 3rd party localStorage keys const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus' const FOUGHT_OPPONENTS_HIDDEN = LS_CONFIG_HHPLUSPLUS_NAME + "FoughtOpponentsHidden" // icon paths const PATH_GROUPS = '<path d="M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14 c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43V18l4.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2 c0-1.1-0.9-2-2-2s-2,0.9-2,2C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14 c-0.39,0-0.76,0.04-1.13,0.1c0.4,0.68,0.63,1.46,0.63,2.29V18l4.5,0V16.43z M16.24,13.65c-1.17-0.52-2.61-0.9-4.24-0.9 c-1.63,0-3.07,0.39-4.24,0.9C6.68,14.13,6,15.21,6,16.39V18h12v-1.61C18,15.21,17.32,14.13,16.24,13.65z M8.07,16 c0.09-0.23,0.13-0.39,0.91-0.69c0.97-0.38,1.99-0.56,3.02-0.56s2.05,0.18,3.02,0.56c0.77,0.3,0.81,0.46,0.91,0.69H8.07z M12,8 c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.45,8,12,8 M12,6c-1.66,0-3,1.34-3,3c0,1.66,1.34,3,3,3s3-1.34,3-3 C15,7.34,13.66,6,12,6L12,6z"/>'; const PATH_GROUP = '<path d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z"/>'; const PATH_CLEAR = '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>'; class EquipmentCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('shop')) { return; } HHPlusPlus.Helpers.defer(() => { setTimeout(() => { EquipmentCollector.collectPlayerEquipment(); EquipmentCollector.collectBestMythicEquipment(); }, 250); }); } static collectPlayerEquipment() { const eqElements = $("div#equiped.armor-container div.slot:not(:empty)[subtype!='0']"); if (eqElements.length != 6) { console.log("Did not find 6 equipment elements."); return; } const equipment = eqElements.map(function() { return $(this).data("d")}).get(); const equipmentStripped = equipment.map((e) => { return { id: e.id_member_armor_equipped || e.id_member_armor, // unique item identifier? rarity: e.item.rarity, // legendary, mythic type: e.item.type, // always "armor" skin_id: e.skin.identifier, // EH13, ET21 etc subtype: parseInt(e.skin.subtype), // 1, 2, 3, 4, 5 or 6 carac1: parseInt(e.caracs.carac1), carac2: parseInt(e.caracs.carac2), carac3: parseInt(e.caracs.carac3), harmony: parseInt(e.caracs.chance), endurance: parseInt(e.caracs.endurance), }; }); window.localStorage.setItem(EQUIPMENT_CURRENT_KEY, JSON.stringify(equipmentStripped)); } static collectBestMythicEquipment() { const equipment = player_inventory.armor .filter(a => a.item.rarity == "mythic") .filter(a => parseInt(a.resonance_bonuses.class.identifier) == Hero.infos.class) .filter(a => a.resonance_bonuses.class.resonance == "damage") .filter(a => a.resonance_bonuses.theme.resonance == "defense"); window.localStorage.setItem(EQUIPMENT_BEST_MYTHIC_KEY, JSON.stringify(equipment)); } static getCurrent() { return JSON.parse(window.localStorage.getItem(EQUIPMENT_CURRENT_KEY)) || []; } static getBestMythic() { return JSON.parse(window.localStorage.getItem(EQUIPMENT_BEST_MYTHIC_KEY)) || []; } } class LeaguePlayersCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')) { return; } HHPlusPlus.Helpers.defer(() => { HHPlusPlus.Helpers.onAjaxResponse(/action=fetch_hero&id=profile/, LeaguePlayersCollector.collectPlayerPlacementsFromAjaxResponse); LeaguePlayersCollector.collectPlayerData(); }); } static collectPlayerPlacementsFromAjaxResponse(response, opt) { // If you are reading this, please look away, ugly code below // The mythic equipment data is actually not in the html, but in the form of a script that we have to eval const html = $("<div/>").html(response.html); $.globalEval(html.find('script').text()); // creates 'hero_items' const id = html.find("div.ranking_stats .id").text().match(/\d+/)[0]; const username = html.find(".hero_info h3 .hero-name").text(); const level = html.find('div[hero="level"]').text().trim(); const number_mythic_equipment = Object.values(hero_items).filter(i => i.item.rarity == "mythic").length; const d3_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/9.png"]) span') // 9.png is D3 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (!id || !username || !level) { window.popup_message("Error when parsing player data."); return; } if (!d3_placement || d3_placement.length != 2) { // make sure our parser is working by checking the D2 data const d2_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/8.png"]) span') // 8.png is D2 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (d2_placement.length != 2) { window.popup_message("Error when parsing D2 player data."); } d3_placement.push(-1, 0); } const data = { id: parseInt(id), number_mythic_equipment, best_placement: d3_placement[0], placement_count: d3_placement[1], }; LeaguePlayersCollector.storePlayerData(data); $(document).trigger('player:update-profile-data', {id: data.id}) } static collectPlayerData() { for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const girls = player.player.team.girls; const girl_levels = girls.map(g => g.level); const girl_levels_max = Math.max(...girl_levels); const girl_levels_total = girl_levels.reduce((a, b) => a + b, 0); const girl_levels_avg = Math.floor(girl_levels_total / girl_levels.length); const data = { id: parseInt(player.player.id_fighter), username: player.player.nickname, level: parseInt(player.player.level), damage: player.player.damage, defense: player.player.defense, harmony: player.player.chance, ego: player.player.remaining_ego, power: player.player.team.total_power, club_id: player.player.club?.id_club, club_name: `"${player.player.club?.name || ''}"`, girl_levels_avg, girl_levels_max, } LeaguePlayersCollector.storePlayerData(data); } } static me() { for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; if (player.player.nickname == Hero.infos.name) { return player; } } window.popup_message("Could not find myself in the league table."); } static storePlayerData(data) { const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; if (players[data.id] == undefined) { players[data.id] = {}; } Object.assign(players[data.id], data); storage.setItem(LEAGUE_PLAYERS_KEY, JSON.stringify(players)); } static export() { const columns = [ "id", "username", "level", "damage", "defense", "harmony", "ego", "power", "club_id", "club_name", "girl_levels_max", "girl_levels_avg", "expected_points", "number_mythic_equipment", "best_placement", "placement_count", ] const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; const data = Object.values(players).map(player => columns.map(column => player[column])); console.log([columns].concat(data).map(t => t.join(",")).join("\n")); } static clear() { storage.removeItem(LEAGUE_PLAYERS_KEY); } } class TeamsCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('teams')) { return; } HHPlusPlus.Helpers.defer(() => { // action=select_team&id_team=3680477&battle_type=leagues HHPlusPlus.Helpers.onAjaxResponse(/action=select_team&id_team=\d+&battle_type=leagues/, TeamsCollector.collectSelectedTeamFromAjaxResponse); TeamsCollector.collectTeams(); }); } static collectSelectedTeamFromAjaxResponse(response, opt) { // Figure out the team id from the request params const params = new URLSearchParams(opt.data); const team_id = params.get("id_team"); // Only store the team id to be able to easily refresh the team information when boosters expire or new ones are applied. window.localStorage.setItem(TEAMS_CURRENT_ID_KEY, JSON.stringify(parseInt(team_id))); } static collectTeams() { // Create a team map - team_id: team const teams = Object.fromEntries(Object.values(teams_data).filter(t => !t.locked).map(t => [parseInt(t.id_team), t])); storage.setItem(TEAMS_ALL_KEY, JSON.stringify(teams)); } static getCurrent() { const team_id = JSON.parse(window.localStorage.getItem(TEAMS_CURRENT_ID_KEY)); const all_teams = TeamsCollector.getAll(); if (team_id && all_teams) { return all_teams[team_id]; } window.popup_message( `Could not retrieve current team. Team ID: ${team_id}, all teams: ${Boolean(all_teams)}. Please open the team selection page and re-select your team.` ); } static getAll() { return JSON.parse(window.localStorage.getItem(TEAMS_ALL_KEY)) || {}; } } class LeagueSimulator { extract(me, other) { // This largely uses the upsteam 'League' class (thanks zoop0kemon), but tweaks it to the new input data and // takes the themes into account to caculate defence reduction and counter bonuses. const { team: playerTeam } = me.player; var { ego: playerEgo, damage: playerAtk, defense: playerDef, chance: playerCrit, } = playerTeam.caracs; const playerElements = playerTeam.theme_elements.map(({type}) => type); const playerWeaknessElements = playerTeam.theme_elements.map(({weakness}) => weakness); const playerSynergies = playerTeam.synergies; const playerBonuses = { critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'fire'), critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'stone'), healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'water'), reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'sun'), } const { nickname: opponentName, team: opponentTeam, } = other.player; var { ego: opponentEgo, chance: opponentCrit, damage: opponentAtk, defense: opponentDef, } = opponentTeam.caracs const opponentTeamMemberElements = []; [0,1,2,3,4,5,6].forEach(key => { const teamMember = opponentTeam.girls[key] if (teamMember && teamMember.element) { opponentTeamMemberElements.push(teamMember.element) } }) const opponentElements = opponentTeam.theme_elements.map(({type}) => type) const opponentWeaknessElements = opponentTeam.theme_elements.map(({weakness}) => weakness); const opponentSynergies = opponentTeam.synergies const teamGirlSynergyBonusesMissing = opponentSynergies.every(({team_girls_count}) => !team_girls_count) let counts if (teamGirlSynergyBonusesMissing) { // Open bug, sometimes opponent syergy data is missing team bonuses, so we need to rebuild it from the team counts = opponentTeamMemberElements.reduce((a,b)=>{a[b]++;return a}, { fire: 0, stone: 0, sun: 0, water: 0, nature: 0, darkness: 0, light: 0, psychic: 0 }) } const opponentBonuses = { critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'fire', teamGirlSynergyBonusesMissing, counts), critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'stone', teamGirlSynergyBonusesMissing, counts), healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'water', teamGirlSynergyBonusesMissing, counts), reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'sun', teamGirlSynergyBonusesMissing, counts), } const dominanceBonuses = HHPlusPlus.SimHelpers.calculateDominationBonuses(playerElements, opponentElements); const counterElementsAtkEgo = ['fire', 'nature', 'stone', 'sun', 'water']; const counterElementsHarmony = ['darkness', 'light', 'psychic']; const playerCountersOpponentAtkEgo = opponentWeaknessElements.some(e => counterElementsAtkEgo.includes(e) && playerElements.includes(e)); const opponentCountersPlayerAtkEgo = playerWeaknessElements.some(e => counterElementsAtkEgo.includes(e) && opponentElements.includes(e)); const playerCountersOpponentHarmony = opponentWeaknessElements.some(e => counterElementsHarmony.includes(e) && playerElements.includes(e)); const opponentCountersPlayerHarmony = playerWeaknessElements.some(e => counterElementsHarmony.includes(e) && opponentElements.includes(e)); // Attack & Ego if (playerCountersOpponentAtkEgo) { playerAtk *= 1.1; playerEgo *= 1.1; } if (opponentCountersPlayerAtkEgo) { opponentAtk *= 1.1; opponentEgo *= 1.1; } // Defence // Our opponent's defence has already been reduced when creating the league table // opponentDef *= (1 - playerBonuses.reduceDefence); playerDef *= (1 - opponentBonuses.reduceDefence); // Harmony if (playerCountersOpponentHarmony) { playerCrit *= 1.2; } if (opponentCountersPlayerHarmony) { opponentCrit *= 1.2; } const player = { hp: playerEgo, dmg: playerAtk - opponentDef, critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(playerCrit, opponentCrit) + dominanceBonuses.player.chance + playerBonuses.critChance, bonuses: {...playerBonuses, dominance: dominanceBonuses.player}, theme: playerElements, } const opponent = { hp: opponentEgo, dmg: opponentAtk - playerDef, critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(opponentCrit, playerCrit) + dominanceBonuses.opponent.chance + opponentBonuses.critChance, name: opponentName, bonuses: {...opponentBonuses, dominance: dominanceBonuses.opponent}, theme: opponentElements, } return {player, opponent} } } class MyModule { constructor ({name, configSchema}) { this.group = '430i' this.name = name this.configSchema = configSchema this.hasRun = false this.insertedRuleIndexes = [] this.sheet = HHPlusPlus.Sheet.get() } insertRule (rule) { this.insertedRuleIndexes.push(this.sheet.insertRule(rule)) } tearDown () { this.insertedRuleIndexes.sort((a, b) => b-a).forEach(index => { this.sheet.deleteRule(index) }) this.insertedRuleIndexes = [] this.hasRun = false } } class LeagueScoutModule extends MyModule { constructor () { const baseKey = 'leagueScout' const configSchema = { baseKey, default: true, label: `Gather information about league opponents`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run () { if (this.hasRun || !this.shouldRun()) {return} $(document).on('league:rollover', () => { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; storage.setItem(PREVIOUS_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY); }) HHPlusPlus.Helpers.defer(() => { // read and store data const playerData = this.readPlayerData(); const snapshot = this.createSnapshot(playerData); this.storeSnapshot(snapshot); // create ui elements HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => { const parent = $('div.league_buttons'); this.createDownloadButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY, PATH_GROUPS); this.createClearButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY); this.createDownloadButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY, PATH_GROUP); this.createClearButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY); }); }); this.hasRun = true; } readPlayerData() { const data = []; for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const id = player.id_player; const rank = player.place; const name = player.player.nickname; const country = player.country; const level = parseInt(player.player.level); const points = parseInt(player.player_league_points); const elements = player.player.team.theme; data.push({id, name, rank, level, elements, points, country}); } // Sort the parsed data by rank. data.sort((a, b) => a.rank > b.rank); return data; } createSnapshot(playerData) { const numPlayers = playerData.length; const currentDate = new Date(window.server_now_ts * 1000); const leagueEndDate = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000); return { date: currentDate, league_end: leagueEndDate, num_players: numPlayers, player_data: playerData, } } storeSnapshot(snapshot) { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; if (data.length && JSON.stringify(data[data.length - 1].player_data) === JSON.stringify(snapshot.player_data)) { return; } data.push(snapshot); storage.setItem(CURRENT_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); } createButton(id, path) { return `<svg id="${id}" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" width="16px" height="16px" fill="#FFFFFF" style="cursor: pointer;"><g><rect fill="none" height="24" width="24"/></g><g>${path}</g></svg>` } createDownloadButton(parent, what, icon) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `download-${friendlyId}`; const downloadButton = this.createButton(buttonId, icon); parent.append(downloadButton); $(document.body).on('click', `#${buttonId}`, () => { const data = JSON.parse(storage.getItem(what)) || []; const separator = "," const columns = ["date", "player_id", "player_name", "player_rank", "player_points"]; const values = data.flatMap((e) => e.player_data.map((p) => [e.date, p.id, p.name, p.rank, p.points].join(separator))); let csvContent = `sep=${separator}\n` + columns.join(separator) + "\n" + values.join("\n"); var element = document.createElement('a'); element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent)); element.setAttribute('download', `${friendlyId}.csv`); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); } createClearButton(parent, what) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `clear-${friendlyId}`; const clearButton = this.createButton(buttonId, PATH_CLEAR); parent.append(clearButton); $(document.body).on('click', `#${buttonId}`, () => { storage.removeItem(what); }); } } class LeagueTableModule extends MyModule { constructor () { const baseKey = 'leagueTable' const configSchema = { baseKey, default: true, label: `Extend league table with additional opponents' information`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run() { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { this.showPlayerLiveData(); this.extendLeagueDataModel(); HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => { this.showHideUnhideButton(); this.extendLeagueTable(); this.renderUpdatedStats(); }); $(document).on('player:update-profile-data', (event, data) => { this.extendLeagueDataModel(); this.onPlayerProfile(data); }); }); this.hasRun = true; } showPlayerLiveData() { // Show the live data for the player: theme and stats (atk, ego, def, harmony) const player = LeaguePlayersCollector.me(); const team = TeamsCollector.getCurrent(); if (!team) { return; } // Update the player's data player.player.team = team; // Update the opponents' data (only defence) for (var r = 0, n = window.opponents_list.length; r < n; r++) { const opponent = window.opponents_list[r]; if (opponent == player) { continue; } // Defence const reduceDefence = HHPlusPlus.SimHelpers.findBonusFromSynergies(player.player.team.synergies, 'sun'); opponent.player.team.caracs.defense *= (1 - reduceDefence); } } renderUpdatedStats() { // Update the UI to show the updated stats from the 'opponents_list' data model: // - the player stats with the stats of the currently selected team and not the one from the last snapshot // - the opponent's stats based on the currently selected player team $('div.league_table div.body-row') .each(function(row_index) { const team = opponents_list[row_index].player.team; $(this).find('div.data-column[column=boosters] div.player_stats span.carac_value') .each(function(carac_idx) { const carac_name = this.id.split("-")[1]; const carac_value = team[carac_name] || team.caracs[carac_name]; $(this).text(number_reduce(carac_value)); }); }); } calculateExpectedPoints(me, other) { const sim = new LeagueSimulator(); const {player, opponent} = sim.extract(me, other); const simulator = new HHPlusPlus.Simulator({player, opponent, highPrecisionMode: false, logging: false}) const result = simulator.run() return Object.entries(result.points).map(([pts, coef]) => parseInt(pts) * coef).reduce((acc, a) => acc + a, 0); } extendLeagueDataModel() { const me = LeaguePlayersCollector.me(); const players_data = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; // add power to the existing `opponents_list` data model for (var r = 0, n = opponents_list.length; r < n; r++) { const player = opponents_list[r]; const id = parseInt(player.player.id_fighter); const player_data = players_data[id]; const best_placement = player_data != undefined ? player_data.best_placement : -1; const placement_count = player_data != undefined ? player_data.placement_count : -1; const expected_points = this.calculateExpectedPoints(me, player); player.expected_points = expected_points; player.best_placement = best_placement; player.placement_count = placement_count; } } extendLeagueTable() { // Additional CSS classes this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:var(--var-color-mythic)}`); this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}'); this.insertRule('.league_table .data-list .body-row .data-column[column="team"] span {width: 20px; height: 20px; background-size: 20px}'); this.insertRule('.league_table .data-list .body-row .data-column[column="expected_points"] {filter: blur(5px); -webkit-filter: blur(5px);}'); this.insertRule('.league_table .data-list .body-row .data-column[column="reload"] button {padding: 2px 5px}'); // Highlight expired boosters $('div.league_table div.data-column[column=boosters] div[type=booster]') .each(function(index) { const d = $(this).data('d'); if (d.expiration == 0) { $(this).css({'border': '1px solid red'}); } }) // Create "power", "e[x]" ... $('div.league_table div.head-row div.head-column[column=team]') .after( '<div class="data-column head-column" column="power">Power</div>' + '<div class="data-column head-column" column="expected_points">E[X]</div>' ); // ... and "reload" headers. $('div.league_table div.head-row div.head-column:last-child') .after('<div class="data-column head-column" column="reload">↻</div>'); // Populate additional league table columns: // - add "power" and "e[x]" data // - add best placement indicators next to the opponents name // - replace synergy tooltip with theme icons $('div.league_table') .find('div.body-row') .each(function(index) { const opponent = window.opponents_list[index]; // power and e[x] columns const power = opponent.player.team.total_power; const e_x = opponent.expected_points; $(this).find('div.data-column[column=team]').after( `<div class="data-column" column="power">${(power).toFixed()}</div> <div class="data-column" column="expected_points">${(e_x).toFixed(2)}</div>` ); // best placement indicator next to the nickname if (opponent.best_placement == 1) { $(this).find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`) } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) { $(this).find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`) } // show theme icons instead of syngergy tooltip const themes = opponent.player.team.theme.split(',').map(t => t || "balanced"); const theme_icons = themes.map(t => `<span class="${t}_element_icn ${t}_theme_icn"></span>`); $(this).find('div.data-column[column=team]').html(theme_icons.join('')); // add empty reload column $(this).find('div.data-column:last-child').after('<div class="data-column" column="reload"/>'); // show best placement and mythic equipment reload button if needed if (opponent.best_placement == undefined) { const load_button = $('<button class="blue_button_L">↻</button>'); load_button.on('click', () => { window.$.post({ url: '/ajax.php', data: { action: 'fetch_hero', id: 'profile', preview: false, player_id: parseInt(opponent.player.id_fighter), }, success: (data) => {} }) }) $(this).find('div.data-column[column=reload]').html(load_button); } }); // If all player data has been loaded we can remove the whole reload column this.removeReloadColumnIfNeeded() // re-run the league table sorting with the additional "power" sort option // var sort_options = { level: 'number', nb_challenges_played: 'number', power: 'number', expected_points: 'number', place: 'number'}; // var sort_class = new TableSorting(opponents_list, 'id_player', 'place', sort_options, 'leagues', 'class'); // sort_class.init(); } onPlayerProfile(data) { const id = data.id; const idx = window.opponents_list.findIndex(x => parseInt(x.player.id_fighter) == id); const opponent = window.opponents_list[idx]; const row = $($('div.league_table div.body-row').get(idx)); // best placement indicator next to the nickname if (opponent.best_placement == 1) { row.find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`) } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) { row.find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`) } // remove reload button row.find('div.data-column[column=reload] button').remove(); // remove reload column (optional) this.removeReloadColumnIfNeeded() } removeReloadColumnIfNeeded() { const reload_buttons = $('div.league_table div.body-row div.data-column[column=reload] button').length if (reload_buttons == 0) { $('div.league_table div.head-row div.head-column[column=reload]').remove(); $('div.league_table div.body-row div.data-column[column=reload]').remove(); } } showHideUnhideButton() { const hide = JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)) || false; const caption = hide ? "Show" : "Hide"; this.hideUnhide(hide); const btn = $(`<button id="beaten_opponents2" class="blue_button_L">${caption}</button>`); btn.on('click', () => { const next = !JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)); this.hideUnhide(next); storage.setItem(FOUGHT_OPPONENTS_HIDDEN, next); btn.text(next ? "Show" : "Hide"); }); $('div.league_buttons').append(btn); } hideUnhide(hide) { $('div.league_table') .find('div.body-row') .each(function(index) { const results = $(this).find('div.data-column[column=match_history]').find('div[class!="result "]').length; const fought_all = results == 3; if (fought_all && hide) { $(this).hide(); } else if (fought_all && !hide) { $(this).show(); } }); $('#leagues .league_content .league_table').getNiceScroll().resize(); } } class PrebattleFlightCheckModule extends MyModule { constructor () { const baseKey = 'prebattleFlightCheck' const configSchema = { baseKey, default: true, label: `Run team and equipment checks before league battles`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('leagues-pre-battle')} run() { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { setTimeout(() => { this.checkMythicEquipment(); }, 150); }); this.hasRun = true; } checkMythicEquipment() { const me = EquipmentCollector.getBestMythic(); const equipment_themes = me.map(x => x.resonance_bonuses.theme.identifier); // use 'div.player-team' for the opponent's team const synergies = JSON.parse($('div.player-panel div.player-team div.icon-area').attr('synergy-data')); const themes = synergies.filter(x => x.team_girls_count >=3).map(x => x.element.type); const has_matching_me = themes.some(t => equipment_themes.includes(t)); if (has_matching_me) { window.popup_message("You have a perfect mythic equipment for this team in your inventory.") } } } class HaremFiltersModule extends MyModule { constructor () { const baseKey = 'haremFilters' const configSchema = { baseKey, default: true, label: `Show additional harem filters`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('harem') && !HHPlusPlus.Helpers.isCurrentPage('hero')} run () { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { // figure out the event ids for LD const ld_event_ids = $("select[name=event] option") .filter((idx, opt) => opt.text.includes("Legendary Days")) .map((idx, opt) => opt.value) .get() .map(id => parseInt(id)); // mark the girls as LD girls Object.values(window.girlsDataList) .filter(e => e.source_selectors.event) .filter(e => e.source_selectors.event.filter(id => ld_event_ids.includes(id)).length) .forEach(g => g.source_selectors.legendary_days = [0]); // TODO fill id, optional // add dropdown option $("select[name=lists]").append(new Option("Legendary Days", "legendary_days")); }); this.hasRun = true; } } setTimeout(() => { const {hhPlusPlusConfig, HHPlusPlus, location} = window; if (!$) { console.log('No jQuery found. Probably an error page. Ending the script here') return; } else if (!hhPlusPlusConfig || !HHPlusPlus) { console.log("HH++ is not available"); return; } else if (location.pathname === '/' && (location.hostname.includes('www') || location.hostname.includes('test'))) { console.log("iframe container, do nothing"); return; } // collectors EquipmentCollector.collect(); LeaguePlayersCollector.collect(); TeamsCollector.collect(); // modules const modules = [ new LeagueScoutModule(), new LeagueTableModule(), new HaremFiltersModule(), new PrebattleFlightCheckModule(), ] // register our own window hooks window.HHPlusPlusPlus = { exportLeagueData: LeaguePlayersCollector.export, clearLeagueData: LeaguePlayersCollector.clear, }; hhPlusPlusConfig.registerGroup({ key: '430i', name: '430i\'s Scripts' }) modules.forEach(module => hhPlusPlusConfig.registerModule(module)) hhPlusPlusConfig.loadConfig() hhPlusPlusConfig.runModules() HHPlusPlus.Helpers.runDeferred() }, 1) Edited August 7, 2023 by Ravi-Sama Ravi edit: added spoiler tag, so don't have to scroll. 6 Link to comment Share on other sites More sharing options...
Horsting Posted August 6, 2023 Share Posted August 6, 2023 @430i Many thanks for the update. Did you test compatibility with MM's script, is it? When I tested your first version, the table was not well aligned anymore, which could be solved by manually commenting out the e[x] column, which was anyway a WIP and not that valuable anymore with MM's side panel preview. 3 Link to comment Share on other sites More sharing options...
Der DinX Posted August 6, 2023 Share Posted August 6, 2023 (edited) // If you are reading this, please look away, ugly code below 😁 52 minutes ago, 430i said: Right now the information is blurred, but it is trivial to unblur it (left as an exercise for the reader). Seems only to be trivial for those who can really read and understand this code. I partly can tell which part of the text is for what part of the game, but that's about it. Couldn't find the needed hint jet. ------- Found it 😉 🤪 Edited August 6, 2023 by Der DinX Link to comment Share on other sites More sharing options...
Moderator Ravi-Sama Posted August 7, 2023 Moderator Share Posted August 7, 2023 17 hours ago, 430i said: [new] Show the expected points for every opponent right in the leagues table. Right now the information is blurred, but it is trivial to unblur it (left as an exercise for the reader). How I unblurred the E[X] column: Spoiler I searched "blur" and then set the two 5px values to 0px, to unblur the E[X]s. At first I deleted that part, but that removed the whole column. Setting them to 0 worked. Not sure if that's the best solution to that "exercise." Also added the "//" like @Horsting recommended. That section of code. Spoiler extendLeagueTable() { // Additional CSS classes this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:var(--var-color-mythic)}`); this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}'); this.insertRule('.league_table .data-list .body-row .data-column[column="team"] span {width: 20px; height: 20px; background-size: 20px}'); // this.insertRule('.league_table .data-list .body-row .data-column[column="expected_points"] {filter: blur(0px); -webkit-filter: blur(0px);}'); this.insertRule('.league_table .data-list .body-row .data-column[column="reload"] button {padding: 2px 5px}'); 1 1 Link to comment Share on other sites More sharing options...
OmerB Posted August 7, 2023 Share Posted August 7, 2023 (edited) Two remarks: 1. The code parts can be hidden with a [/spoiler] tag, like Ravi did above me, so we won't have to scroll through endless code. 2. The booster detector script is no longer relevant, and can be deleted from the OP (after @DvDivXXX contemplated so much whether to publish it or not 😁). It simply isn't working anymore. P.S. I was rather against publishing the booster detector script on the first page, since I was afraid that I would lose some advantage that I have. But I guess: a. most of the players doesn't visit the forum, only a tiny percentage does. b. It isn't relevant anymore 😆 Edited August 7, 2023 by OmerB 4 Link to comment Share on other sites More sharing options...
Horsting Posted August 7, 2023 Share Posted August 7, 2023 1 hour ago, Ravi-Sama said: I searched "blur" and then set the two 5px values to 0px Just comment out the whole line with `//`. The only point of the while CSS rule is the blue effect. 1 1 Link to comment Share on other sites More sharing options...
430i Posted August 7, 2023 Share Posted August 7, 2023 16 hours ago, Horsting said: @430i Many thanks for the update. Did you test compatibility with MM's script, is it? When I tested your first version, the table was not well aligned anymore, which could be solved by manually commenting out the e[x] column, which was anyway a WIP and not that valuable anymore with MM's side panel preview. It should be aligned now, although I dont care too much about that, however I havent tested it with MM's script, so there might be few layout issues. I will take a look later and see whether there are issue and how they can be fixed. 1 Link to comment Share on other sites More sharing options...
Horsting Posted August 7, 2023 Share Posted August 7, 2023 (edited) 4 hours ago, 430i said: It should be aligned now, although I dont care too much about that, however I havent tested it with MM's script, so there might be few layout issues. There are still layout issues. Major problem is that MM's script has the sidebar always shown, either with the girl or with the selected opponent info. And this clashes with the additional columns your's adds. And MM added expired booster highlights/fade some minutes ago, so the overlap of what both do raises. I still like the E[X] column your script adds. So no need to select each opponent, and perspectively also nice for sorting. But somehow the values are again wrong. Not sure what happened, at first they were correct after I loaded the teams edit page once, now they are wrong (including my own stats in the table) and I did not find a way to fix it. My stats are now somewhere in the middle between my snapshot and the actual ones shown on battle pages. Not sure if girls' skills are missing or so. Will probably have a deeper look into it tonight. But for my understanding, so I can better look for and understand things: Your script loads the actual team stats from the team edit page, right? If I see it right, the AME/LME AP bonus is not shown on the team edit page, so it is not taken into account by your script either. That might be already it and should be easy to fix. EDIT: Jep indeed it is the exact stats fetched from the team edit page. It is however missing the AM/LM bonus. Multiplying the team edit page AP value with 1.15 somehow does not bringt me to the exact AP value, when assuming mathematical correct rounding, but I guess the value shown on the team edit page is consequently rounded up, then it works. Even if it is not 100% correct, the E[X] values will be much closer to the real ones. A remaining error is the missing playful defence penalty. Also this data should be available to have the E[X] further corrected. But it won't make a big difference in most cases. Edited August 7, 2023 by Horsting 1 1 Link to comment Share on other sites More sharing options...
430i Posted August 7, 2023 Share Posted August 7, 2023 6 hours ago, Horsting said: Your script loads the actual team stats from the team edit page, right? Indeed. So far this has worked quite good for me, as long as you "refresh" the team whenever your boosters change. Besides the few known issues you should always see identical stats in the leagues table compared to the teams table (there is no additional calculation taking place at this point). However in order to calculate the correct E[X] the following additional calculations are taking place (which you have probably already gleaned from the code): Increase your or your opponent's attack and ego in case of counter elements. Here there is a bug, which currently only takes into account whether there is any counter element, and not how many counter elements - I assume a blue/yellow team should get a 20% (or is it 21% if the bonuses are multiplicative) vs a red/blue team. Right now you only get 10%, will fix it soon. Increase your attack by 15% in case of a mastery booster. This is currently missing, as I didn't have one equipped and forgot about it. Reduce your and your opponent's defence based on each other's yellow bonus. Increase your and your opponent's harmony in case of counter elements (the note from the first bullet point should apply here as well). Applying those calculation gives me the exact same E[X] in the league table as the one shown on the pre-battle page for every opponent I have checked so far. I am not sure why it gives you slightly different values, it might be due to some collisions with MM's script, I dont know. I will check it tomorrow, same as with the layout issues. I am hoping that most of my and MM's features make it in zoo's script, but this might take a bit, so for now you might need several scripts to make the most out of the new layout. [Insert now there's two of them meme here]. And all of this might change on Wednesday anyway... 6 Link to comment Share on other sites More sharing options...
430i Posted August 8, 2023 Share Posted August 8, 2023 Updated version, which should fix most of the aforementioned issues. Feedback welcome! A list of the features: Hide/show fought opponents Highlight expired boosters (although who knows whether they really expired) Show girl power and themes (instead of the useless synergy popup) Button to load the user profile data (but not really show it) - this will display their best D3 results in the league table next to the player's name. No need to reload the page anymore and the column is hidden when all data is loaded. Show your real team colors, team power and stats (the previous version only showed the snapshotted data, zoo's version as well shows the latest snapshot). Of course your stats might change depending on the opponent and your/their counters, but the values shown should be the closest "opponent-independent" values that are available in the game. There are still few known issues, see below. [updated] Show the expected points for every opponent right in the leagues table. Data is now unblurred by default. Fix expected score calculation when AM/LM boosters are active and there are more than 1 counter elements A check on the pre-battle page whether you have a suitable not-equipped mythic equipment in your inventory. [new] Compatibility with other scripts, most notably League++. Few layout compromises had to be made to make space for all the new data: narrower rank column, hide the player avatar, remove the native 'power' column, squeeze the league end timer Known issues: when you load the league page for the very first time the numbers (your own stats and all expected points) might be off until you open the team selection screen and (re-)select your team similarly when your boosters expire or you apply a new booster the numbers (your own stats and all expected points) will be incorrect until you visit the team selection page (no need to select a team, I think) Spoiler // ==UserScript== // @name Hentai Heroes+++ // @description Few QoL improvement and additions for competitive PvP players. // @version 0.16.13 // @match https://*.hentaiheroes.com/* // @match https://nutaku.haremheroes.com/* // @run-at document-body // @grant none // @author 430i // ==/UserScript== const {$, location, localStorage: storage} = window // localStorage keys const LS_CONFIG_NAME = 'HHPlusPlusPlus' const LEAGUE_BASE_KEY = LS_CONFIG_NAME + ".League"; const LEAGUE_SNAPSHOT_BASE_KEY = LEAGUE_BASE_KEY + ".Snapshot"; const CURRENT_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Current"; const PREVIOUS_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Previous"; const LEAGUE_PLAYERS_KEY = LEAGUE_BASE_KEY + ".Players"; const EQUIPMENT_KEY = LS_CONFIG_NAME + ".Equipment"; const EQUIPMENT_CURRENT_KEY = EQUIPMENT_KEY + ".Current"; const EQUIPMENT_BEST_MYTHIC_KEY = EQUIPMENT_KEY + ".Mythic"; const EQUIPMENT_FAVORITE_KEY = EQUIPMENT_KEY + ".Favorite"; const TEAMS_BASE_KEY = LS_CONFIG_NAME + ".Teams"; const TEAMS_ALL_KEY = TEAMS_BASE_KEY + ".All"; const TEAMS_CURRENT_ID_KEY = TEAMS_BASE_KEY + ".CurrentId"; // 3rd party localStorage keys const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus' const FOUGHT_OPPONENTS_HIDDEN = LS_CONFIG_HHPLUSPLUS_NAME + "FoughtOpponentsHidden" // icon paths const PATH_GROUPS = '<path d="M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14 c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43V18l4.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2 c0-1.1-0.9-2-2-2s-2,0.9-2,2C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14 c-0.39,0-0.76,0.04-1.13,0.1c0.4,0.68,0.63,1.46,0.63,2.29V18l4.5,0V16.43z M16.24,13.65c-1.17-0.52-2.61-0.9-4.24-0.9 c-1.63,0-3.07,0.39-4.24,0.9C6.68,14.13,6,15.21,6,16.39V18h12v-1.61C18,15.21,17.32,14.13,16.24,13.65z M8.07,16 c0.09-0.23,0.13-0.39,0.91-0.69c0.97-0.38,1.99-0.56,3.02-0.56s2.05,0.18,3.02,0.56c0.77,0.3,0.81,0.46,0.91,0.69H8.07z M12,8 c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.45,8,12,8 M12,6c-1.66,0-3,1.34-3,3c0,1.66,1.34,3,3,3s3-1.34,3-3 C15,7.34,13.66,6,12,6L12,6z"/>'; const PATH_GROUP = '<path d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z"/>'; const PATH_CLEAR = '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>'; class EquipmentCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('shop')) { return; } HHPlusPlus.Helpers.defer(() => { setTimeout(() => { EquipmentCollector.collectPlayerEquipment(); EquipmentCollector.collectBestMythicEquipment(); }, 250); }); } static collectPlayerEquipment() { const eqElements = $("div#equiped.armor-container div.slot:not(:empty)[subtype!='0']"); if (eqElements.length != 6) { console.log("Did not find 6 equipment elements."); return; } const equipment = eqElements.map(function() { return $(this).data("d")}).get(); const equipmentStripped = equipment.map((e) => { return { id: e.id_member_armor_equipped || e.id_member_armor, // unique item identifier? rarity: e.item.rarity, // legendary, mythic type: e.item.type, // always "armor" skin_id: e.skin.identifier, // EH13, ET21 etc subtype: parseInt(e.skin.subtype), // 1, 2, 3, 4, 5 or 6 carac1: parseInt(e.caracs.carac1), carac2: parseInt(e.caracs.carac2), carac3: parseInt(e.caracs.carac3), harmony: parseInt(e.caracs.chance), endurance: parseInt(e.caracs.endurance), }; }); window.localStorage.setItem(EQUIPMENT_CURRENT_KEY, JSON.stringify(equipmentStripped)); } static collectBestMythicEquipment() { const equipment = player_inventory.armor .filter(a => a.item.rarity == "mythic") .filter(a => parseInt(a.resonance_bonuses.class.identifier) == Hero.infos.class) .filter(a => a.resonance_bonuses.class.resonance == "damage") .filter(a => a.resonance_bonuses.theme.resonance == "defense"); window.localStorage.setItem(EQUIPMENT_BEST_MYTHIC_KEY, JSON.stringify(equipment)); } static getCurrent() { return JSON.parse(window.localStorage.getItem(EQUIPMENT_CURRENT_KEY)) || []; } static getBestMythic() { return JSON.parse(window.localStorage.getItem(EQUIPMENT_BEST_MYTHIC_KEY)) || []; } } class LeaguePlayersCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')) { return; } HHPlusPlus.Helpers.defer(() => { HHPlusPlus.Helpers.onAjaxResponse(/action=fetch_hero&id=profile/, LeaguePlayersCollector.collectPlayerPlacementsFromAjaxResponse); LeaguePlayersCollector.collectPlayerData(); }); } static collectPlayerPlacementsFromAjaxResponse(response, opt) { // If you are reading this, please look away, ugly code below // The mythic equipment data is actually not in the html, but in the form of a script that we have to eval const html = $("<div/>").html(response.html); $.globalEval(html.find('script').text()); // creates 'hero_items' const id = html.find("div.ranking_stats .id").text().match(/\d+/)[0]; const username = html.find(".hero_info h3 .hero-name").text(); const level = html.find('div[hero="level"]').text().trim(); const number_mythic_equipment = Object.values(hero_items).filter(i => i.item.rarity == "mythic").length; const d3_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/9.png"]) span') // 9.png is D3 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (!id || !username || !level) { window.popup_message("Error when parsing player data."); return; } if (!d3_placement || d3_placement.length != 2) { // make sure our parser is working by checking the D2 data const d2_placement = $("<div/>") .html(html) .find('div.history-independent-tier:has(img[src*="/8.png"]) span') // 8.png is D2 .map(function() {return parseInt($(this).text().trim().match(/\d+/));}) .get(); if (d2_placement.length != 2) { window.popup_message("Error when parsing D2 player data."); } d3_placement.push(-1, 0); } const data = { id: parseInt(id), number_mythic_equipment, best_placement: d3_placement[0], placement_count: d3_placement[1], }; LeaguePlayersCollector.storePlayerData(data); $(document).trigger('player:update-profile-data', {id: data.id}) } static collectPlayerData() { for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const girls = player.player.team.girls; const girl_levels = girls.map(g => g.level); const girl_levels_max = Math.max(...girl_levels); const girl_levels_total = girl_levels.reduce((a, b) => a + b, 0); const girl_levels_avg = Math.floor(girl_levels_total / girl_levels.length); const data = { id: parseInt(player.player.id_fighter), username: player.player.nickname, level: parseInt(player.player.level), damage: player.player.damage, defense: player.player.defense, harmony: player.player.chance, ego: player.player.remaining_ego, power: player.player.team.total_power, club_id: player.player.club?.id_club, club_name: `"${player.player.club?.name || ''}"`, girl_levels_avg, girl_levels_max, } LeaguePlayersCollector.storePlayerData(data); } } static me() { for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; if (player.player.nickname == Hero.infos.name) { return player; } } window.popup_message("Could not find myself in the league table."); } static storePlayerData(data) { const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; if (players[data.id] == undefined) { players[data.id] = {}; } Object.assign(players[data.id], data); storage.setItem(LEAGUE_PLAYERS_KEY, JSON.stringify(players)); } static export() { const columns = [ "id", "username", "level", "damage", "defense", "harmony", "ego", "power", "club_id", "club_name", "girl_levels_max", "girl_levels_avg", "expected_points", "number_mythic_equipment", "best_placement", "placement_count", ] const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; const data = Object.values(players).map(player => columns.map(column => player[column])); console.log([columns].concat(data).map(t => t.join(",")).join("\n")); } static clear() { storage.removeItem(LEAGUE_PLAYERS_KEY); } } class TeamsCollector { static collect() { if (!HHPlusPlus.Helpers.isCurrentPage('teams')) { return; } HHPlusPlus.Helpers.defer(() => { // action=select_team&id_team=3680477&battle_type=leagues HHPlusPlus.Helpers.onAjaxResponse(/action=select_team&id_team=\d+&battle_type=leagues/, TeamsCollector.collectSelectedTeamFromAjaxResponse); TeamsCollector.collectTeams(); }); } static collectSelectedTeamFromAjaxResponse(response, opt) { // Figure out the team id from the request params const params = new URLSearchParams(opt.data); const team_id = params.get("id_team"); // Only store the team id to be able to easily refresh the team information when boosters expire or new ones are applied. window.localStorage.setItem(TEAMS_CURRENT_ID_KEY, JSON.stringify(parseInt(team_id))); } static collectTeams() { // Create a team map - team_id: team const teams = Object.fromEntries(Object.values(teams_data).filter(t => !t.locked).map(t => [parseInt(t.id_team), t])); storage.setItem(TEAMS_ALL_KEY, JSON.stringify(teams)); } static getCurrent() { const team_id = JSON.parse(window.localStorage.getItem(TEAMS_CURRENT_ID_KEY)); const all_teams = TeamsCollector.getAll(); if (team_id && all_teams) { return all_teams[team_id]; } window.popup_message( `Could not retrieve current team. Team ID: ${team_id}, all teams: ${Boolean(all_teams)}. Please open the team selection page and re-select your team.` ); } static getAll() { return JSON.parse(window.localStorage.getItem(TEAMS_ALL_KEY)) || {}; } } class LeagueSimulator { extract(me, other) { // This largely uses the upsteam 'League' class (thanks zoop0kemon), but tweaks it to the new input data and // takes the themes into account to caculate defence reduction and counter bonuses. const { team: playerTeam } = me.player; var { ego: playerEgo, damage: playerAtk, defense: playerDef, chance: playerCrit, } = playerTeam.caracs; // MB2 - All Mastery, MB8 - Leagues Mastery const playerHasMasteryMythicBooster = me.boosters.some(b => ['MB2', 'MB8'].includes(b.item.identifier)) const playerElements = playerTeam.theme_elements.map(({type}) => type); const playerWeaknessElements = playerTeam.theme_elements.map(({weakness}) => weakness); const playerSynergies = playerTeam.synergies; const playerBonuses = { critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'fire'), critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'stone'), healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'water'), reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'sun'), } const { nickname: opponentName, team: opponentTeam, } = other.player; var { ego: opponentEgo, chance: opponentCrit, damage: opponentAtk, defense: opponentDef, } = opponentTeam.caracs // MB2 - All Mastery, MB8 - Leagues Mastery // The opponent boosters does not currently contain the mastery boosters, but include it in the calculation // just in case Kinkoid messes something up. const opponentHasMasteryMythicBooster = other.boosters.some(b => ['MB2', 'MB8'].includes(b.item.identifier)) const opponentTeamMemberElements = []; [0,1,2,3,4,5,6].forEach(key => { const teamMember = opponentTeam.girls[key] if (teamMember && teamMember.element) { opponentTeamMemberElements.push(teamMember.element) } }) const opponentElements = opponentTeam.theme_elements.map(({type}) => type) const opponentWeaknessElements = opponentTeam.theme_elements.map(({weakness}) => weakness); const opponentSynergies = opponentTeam.synergies const teamGirlSynergyBonusesMissing = opponentSynergies.every(({team_girls_count}) => !team_girls_count) let counts if (teamGirlSynergyBonusesMissing) { // Open bug, sometimes opponent syergy data is missing team bonuses, so we need to rebuild it from the team counts = opponentTeamMemberElements.reduce((a,b)=>{a[b]++;return a}, { fire: 0, stone: 0, sun: 0, water: 0, nature: 0, darkness: 0, light: 0, psychic: 0 }) } const opponentBonuses = { critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'fire', teamGirlSynergyBonusesMissing, counts), critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'stone', teamGirlSynergyBonusesMissing, counts), healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'water', teamGirlSynergyBonusesMissing, counts), reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'sun', teamGirlSynergyBonusesMissing, counts), } const dominanceBonuses = HHPlusPlus.SimHelpers.calculateDominationBonuses(playerElements, opponentElements); const counterElementsAtkEgo = ['fire', 'nature', 'stone', 'sun', 'water']; const counterElementsHarmony = ['darkness', 'light', 'psychic']; const playerCountersOpponentAtkEgo = opponentWeaknessElements.filter(e => counterElementsAtkEgo.includes(e) && playerElements.includes(e)).length; const opponentCountersPlayerAtkEgo = playerWeaknessElements.filter(e => counterElementsAtkEgo.includes(e) && opponentElements.includes(e)).length; const playerCountersOpponentHarmony = opponentWeaknessElements.filter(e => counterElementsHarmony.includes(e) && playerElements.includes(e)).length; const opponentCountersPlayerHarmony = playerWeaknessElements.filter(e => counterElementsHarmony.includes(e) && opponentElements.includes(e)).length; // Attack & Ego if (playerCountersOpponentAtkEgo) { // TODO is the coeficient additive or multiplicative? const coef = 1 + 0.1 * playerCountersOpponentAtkEgo; playerAtk *= coef; playerEgo *= coef; } if (opponentCountersPlayerAtkEgo) { // TODO is the coeficient additive or multiplicative? const coef = 1 + 0.1 * opponentCountersPlayerAtkEgo; opponentAtk *= coef; opponentEgo *= coef; } // Mastery boosters if (playerHasMasteryMythicBooster) { playerAtk *= 1.15; } if (opponentHasMasteryMythicBooster) { opponentAtk *= 1.15; } // Defence // Our opponent's defence has already been reduced when creating the league table // opponentDef *= (1 - playerBonuses.reduceDefence); playerDef *= (1 - opponentBonuses.reduceDefence); // Harmony if (playerCountersOpponentHarmony) { // TODO is the coeficient additive or multiplicative? const coef = 1 + 0.2 * playerCountersOpponentHarmony; playerCrit *= coef; } if (opponentCountersPlayerHarmony) { // TODO is the coeficient additive or multiplicative? const coef = 1 + 0.2 * opponentCountersPlayerHarmony; opponentCrit *= coef; } const player = { hp: playerEgo, dmg: playerAtk - opponentDef, critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(playerCrit, opponentCrit) + dominanceBonuses.player.chance + playerBonuses.critChance, bonuses: {...playerBonuses, dominance: dominanceBonuses.player}, theme: playerElements, } const opponent = { hp: opponentEgo, dmg: opponentAtk - playerDef, critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(opponentCrit, playerCrit) + dominanceBonuses.opponent.chance + opponentBonuses.critChance, name: opponentName, bonuses: {...opponentBonuses, dominance: dominanceBonuses.opponent}, theme: opponentElements, } return {player, opponent} } } class MyModule { constructor ({name, configSchema}) { this.group = '430i' this.name = name this.configSchema = configSchema this.hasRun = false this.insertedRuleIndexes = [] this.sheet = HHPlusPlus.Sheet.get() } insertRule (rule) { this.insertedRuleIndexes.push(this.sheet.insertRule(rule)) } tearDown () { this.insertedRuleIndexes.sort((a, b) => b-a).forEach(index => { this.sheet.deleteRule(index) }) this.insertedRuleIndexes = [] this.hasRun = false } } class LeagueScoutModule extends MyModule { constructor () { const baseKey = 'leagueScout' const configSchema = { baseKey, default: true, label: `Gather information about league opponents`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run () { if (this.hasRun || !this.shouldRun()) {return} $(document).on('league:rollover', () => { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; storage.setItem(PREVIOUS_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY); }) HHPlusPlus.Helpers.defer(() => { // read and store data const playerData = this.readPlayerData(); const snapshot = this.createSnapshot(playerData); this.storeSnapshot(snapshot); // create ui elements HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => { const parent = $('div.league_buttons'); this.createDownloadButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY, PATH_GROUPS); this.createClearButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY); this.createDownloadButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY, PATH_GROUP); this.createClearButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY); }); }); this.hasRun = true; } readPlayerData() { const data = []; for (var r = 0, n = window.opponents_list.length; r < n; r++) { const player = window.opponents_list[r]; const id = player.id_player; const rank = player.place; const name = player.player.nickname; const country = player.country; const level = parseInt(player.player.level); const points = parseInt(player.player_league_points); const elements = player.player.team.theme; data.push({id, name, rank, level, elements, points, country}); } // Sort the parsed data by rank. data.sort((a, b) => a.rank > b.rank); return data; } createSnapshot(playerData) { const numPlayers = playerData.length; const currentDate = new Date(window.server_now_ts * 1000); const leagueEndDate = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000); return { date: currentDate, league_end: leagueEndDate, num_players: numPlayers, player_data: playerData, } } storeSnapshot(snapshot) { const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || []; if (data.length && JSON.stringify(data[data.length - 1].player_data) === JSON.stringify(snapshot.player_data)) { return; } data.push(snapshot); storage.setItem(CURRENT_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data)); } createButton(id, path) { return `<svg id="${id}" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" width="16px" height="16px" fill="#FFFFFF" style="cursor: pointer;"><g><rect fill="none" height="24" width="24"/></g><g>${path}</g></svg>` } createDownloadButton(parent, what, icon) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `download-${friendlyId}`; const downloadButton = this.createButton(buttonId, icon); parent.append(downloadButton); $(document.body).on('click', `#${buttonId}`, () => { const data = JSON.parse(storage.getItem(what)) || []; const separator = "," const columns = ["date", "player_id", "player_name", "player_rank", "player_points"]; const values = data.flatMap((e) => e.player_data.map((p) => [e.date, p.id, p.name, p.rank, p.points].join(separator))); let csvContent = `sep=${separator}\n` + columns.join(separator) + "\n" + values.join("\n"); var element = document.createElement('a'); element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent)); element.setAttribute('download', `${friendlyId}.csv`); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); } createClearButton(parent, what) { if (!storage.getItem(what)) { return; } const friendlyId = what.toLowerCase().replaceAll(".", "-"); const buttonId = `clear-${friendlyId}`; const clearButton = this.createButton(buttonId, PATH_CLEAR); parent.append(clearButton); $(document.body).on('click', `#${buttonId}`, () => { storage.removeItem(what); }); } } class LeagueTableModule extends MyModule { constructor () { const baseKey = 'leagueTable' const configSchema = { baseKey, default: true, label: `Extend league table with additional opponents' information`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')} run() { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { this.showPlayerLiveData(); this.extendLeagueDataModel(); HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => { this.showHideUnhideButton(); this.extendLeagueTable(); this.renderUpdatedStats(); }); $(document).on('player:update-profile-data', (event, data) => { this.extendLeagueDataModel(); this.onPlayerProfile(data); }); }); this.hasRun = true; } showPlayerLiveData() { // Show the live data for the player: theme and stats (atk, ego, def, harmony) const player = LeaguePlayersCollector.me(); const team = TeamsCollector.getCurrent(); if (!team) { return; } // Update the player's data player.player.team = team; // Update the opponents' data (only defence) for (var r = 0, n = window.opponents_list.length; r < n; r++) { const opponent = window.opponents_list[r]; if (opponent == player) { continue; } // Defence const reduceDefence = HHPlusPlus.SimHelpers.findBonusFromSynergies(player.player.team.synergies, 'sun'); opponent.player.team.caracs.defense *= (1 - reduceDefence); } } renderUpdatedStats() { // Update the UI to show the updated stats from the 'opponents_list' data model: // - the player stats with the stats of the currently selected team and not the one from the last snapshot // - the opponent's stats based on the currently selected player team $('div.league_table div.body-row') .each(function(row_index) { const team = opponents_list[row_index].player.team; $(this).find('div.data-column[column=boosters] div.player_stats span.carac_value') .each(function(carac_idx) { const carac_name = this.id.split("-")[1]; const carac_value = team[carac_name] || team.caracs[carac_name]; $(this).text(number_reduce(carac_value)); }); }); } calculateExpectedPoints(me, other) { const sim = new LeagueSimulator(); const {player, opponent} = sim.extract(me, other); const simulator = new HHPlusPlus.Simulator({player, opponent, highPrecisionMode: false, logging: false}) const result = simulator.run() return Object.entries(result.points).map(([pts, coef]) => parseInt(pts) * coef).reduce((acc, a) => acc + a, 0); } extendLeagueDataModel() { const me = LeaguePlayersCollector.me(); const players_data = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {}; // add power to the existing `opponents_list` data model for (var r = 0, n = opponents_list.length; r < n; r++) { const player = opponents_list[r]; const id = parseInt(player.player.id_fighter); const player_data = players_data[id]; const best_placement = player_data != undefined ? player_data.best_placement : -1; const placement_count = player_data != undefined ? player_data.placement_count : -1; const expected_points = this.calculateExpectedPoints(me, player); player.expected_points = expected_points; player.best_placement = best_placement; player.placement_count = placement_count; } } extendLeagueTable() { // Additional CSS classes this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:var(--var-color-mythic)}`); this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}'); this.insertRule('.league_table .data-list .body-row .data-column[column="team"] span {width: 20px; height: 20px; background-size: 20px}'); // this.insertRule('.league_table .data-list .body-row .data-column[column="expected_points"] {filter: blur(5px); -webkit-filter: blur(5px);}'); this.insertRule('.league_table .data-list .body-row .data-column[column="reload"] button {padding: 2px 5px}'); // Highlight expired boosters $('div.league_table div.data-column[column=boosters] div[type=booster]') .each(function(index) { const d = $(this).data('d'); if (d.expiration == 0 && parseInt(d.usages_remaining) == 0) { $(this).css({'border': '1px solid red'}); } }) // Create "power", "e[x]" ... $('div.league_table div.head-row div.head-column[column=team]') .after( '<div class="data-column head-column" column="total_power">Power</div>' + '<div class="data-column head-column" column="expected_points">E[X]</div>' ); // ... and "reload" headers. $('div.league_table div.head-row div.head-column:last-child') .after('<div class="data-column head-column" column="reload">↻</div>'); // Populate additional league table columns: // - add "power" and "e[x]" data // - add best placement indicators next to the opponents name // - replace synergy tooltip with theme icons $('div.league_table') .find('div.body-row') .each(function(index) { const opponent = window.opponents_list[index]; // power and e[x] columns const power = opponent.player.team.total_power; const e_x = opponent.expected_points; $(this).find('div.data-column[column=team]').after( `<div class="data-column" column="total_power">${(power).toFixed()}</div> <div class="data-column" column="expected_points">${(e_x).toFixed(2)}</div>` ); // best placement indicator next to the nickname if (opponent.best_placement == 1) { $(this).find('div.data-column[column=nickname]') .append(`<span class="best-placement"><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`) } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) { $(this).find('div.data-column[column=nickname]') .append(`<span class="best-placement"><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`) } // show theme icons instead of syngergy tooltip const themes = opponent.player.team.theme.split(',').map(t => t || "balanced"); const theme_icons = themes.map(t => `<span class="${t}_element_icn ${t}_theme_icn"></span>`); $(this).find('div.data-column[column=team]').html(theme_icons.join('')); // add empty reload column $(this).find('div.data-column:last-child').after('<div class="data-column" column="reload"/>'); // show best placement and mythic equipment reload button if needed if (opponent.best_placement == undefined) { const load_button = $('<button class="blue_button_L">↻</button>'); load_button.on('click', () => { window.$.post({ url: '/ajax.php', data: { action: 'fetch_hero', id: 'profile', preview: false, player_id: parseInt(opponent.player.id_fighter), }, success: (data) => {} }) }) $(this).find('div.data-column[column=reload]').html(load_button); } }); // If all player data has been loaded we can remove the whole reload column this.removeReloadColumnIfNeeded(); // Compatibility with other scripts this.makeCompatibleWithLeaguePlusPlus(); // re-run the league table sorting with the additional "power" sort option // var sort_options = { level: 'number', nb_challenges_played: 'number', power: 'number', expected_points: 'number', place: 'number'}; // var sort_class = new TableSorting(opponents_list, 'id_player', 'place', sort_options, 'leagues', 'class'); // sort_class.init(); } onPlayerProfile(data) { const id = data.id; const idx = window.opponents_list.findIndex(x => parseInt(x.player.id_fighter) == id); const opponent = window.opponents_list[idx]; const row = $($('div.league_table div.body-row').get(idx)); // best placement indicator next to the nickname if (opponent.best_placement == 1) { row.find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`) } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) { row.find('div.data-column[column=nickname]') .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`) } // remove reload button row.find('div.data-column[column=reload] button').remove(); // remove reload column (optional) this.removeReloadColumnIfNeeded() } removeReloadColumnIfNeeded() { const reload_buttons = $('div.league_table div.body-row div.data-column[column=reload] button').length if (reload_buttons == 0) { $('div.league_table div.data-row div.data-column[column=reload]').remove(); } } makeCompatibleWithLeaguePlusPlus() { // If the League++ script is active, the layout is messed up, so we need to hide to default 'Power' column to fix it. HHPlusPlus.Helpers.doWhenSelectorAvailable('div#leagues div.league_buttons_block a.changeTeam', () => { // Make the 'place' colum narrower this.insertRule('#leagues.hidden_girl .league_table .data-list .data-row .data-column[column="place"] {min-width: 1rem}'); // Make the league ends label smaller to find real estate for the show/hide button this.insertRule('.league_end_in {min-width: 40px !important}'); // Remove the native 'power' column $('div.league_table div.data-row div.data-column[column=power]').remove(); // Remove the avatars $('div.league_table div.data-row div.data-column[column=nickname] div.square-avatar-wrapper').remove(); }); } showHideUnhideButton() { this.insertRule('#beaten_opponents2 {font-size: 12px; margin-left: 5px}'); const hide = JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)) || false; const caption = hide ? "Show" : "Hide"; this.hideUnhide(hide); const btn = $(`<a id="beaten_opponents2" class="blue_button_L">${caption}</a>`); btn.on('click', () => { const next = !JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)); this.hideUnhide(next); storage.setItem(FOUGHT_OPPONENTS_HIDDEN, next); btn.text(next ? "Show" : "Hide"); }); $('div.league_buttons').append(btn); } hideUnhide(hide) { $('div.league_table') .find('div.body-row') .each(function(index) { const results = $(this).find('div.data-column[column=match_history]').find('div[class!="result "]').length; const fought_all = results == 3; if (fought_all && hide) { $(this).hide(); } else if (fought_all && !hide) { $(this).show(); } }); $('#leagues .league_content .league_table').getNiceScroll().resize(); } } class PrebattleFlightCheckModule extends MyModule { constructor () { const baseKey = 'prebattleFlightCheck' const configSchema = { baseKey, default: true, label: `Run team and equipment checks before league battles`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('leagues-pre-battle')} run() { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { setTimeout(() => { this.checkMythicEquipment(); }, 150); }); this.hasRun = true; } checkMythicEquipment() { const me = EquipmentCollector.getBestMythic(); const equipment_themes = me.map(x => x.resonance_bonuses.theme.identifier); // use 'div.player-team' for the opponent's team const synergies = JSON.parse($('div.player-panel div.player-team div.icon-area').attr('synergy-data')); const themes = synergies.filter(x => x.team_girls_count >=3).map(x => x.element.type); const has_matching_me = themes.some(t => equipment_themes.includes(t)); if (has_matching_me) { window.popup_message("You have a perfect mythic equipment for this team in your inventory.") } } } class HaremFiltersModule extends MyModule { constructor () { const baseKey = 'haremFilters' const configSchema = { baseKey, default: true, label: `Show additional harem filters`, } super({name: baseKey, configSchema}) } shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('harem') && !HHPlusPlus.Helpers.isCurrentPage('hero')} run () { if (this.hasRun || !this.shouldRun()) {return} HHPlusPlus.Helpers.defer(() => { // figure out the event ids for LD const ld_event_ids = $("select[name=event] option") .filter((idx, opt) => opt.text.includes("Legendary Days")) .map((idx, opt) => opt.value) .get() .map(id => parseInt(id)); // mark the girls as LD girls Object.values(window.girlsDataList) .filter(e => e.source_selectors.event) .filter(e => e.source_selectors.event.filter(id => ld_event_ids.includes(id)).length) .forEach(g => g.source_selectors.legendary_days = [0]); // TODO fill id, optional // add dropdown option $("select[name=lists]").append(new Option("Legendary Days", "legendary_days")); }); this.hasRun = true; } } setTimeout(() => { const {hhPlusPlusConfig, HHPlusPlus, location} = window; if (!$) { console.log('No jQuery found. Probably an error page. Ending the script here') return; } else if (!hhPlusPlusConfig || !HHPlusPlus) { console.log("HH++ is not available"); return; } else if (location.pathname === '/' && (location.hostname.includes('www') || location.hostname.includes('test'))) { console.log("iframe container, do nothing"); return; } // collectors EquipmentCollector.collect(); LeaguePlayersCollector.collect(); TeamsCollector.collect(); // modules const modules = [ new LeagueScoutModule(), new LeagueTableModule(), new HaremFiltersModule(), new PrebattleFlightCheckModule(), ] // register our own window hooks window.HHPlusPlusPlus = { exportLeagueData: LeaguePlayersCollector.export, clearLeagueData: LeaguePlayersCollector.clear, }; hhPlusPlusConfig.registerGroup({ key: '430i', name: '430i\'s Scripts' }) modules.forEach(module => hhPlusPlusConfig.registerModule(module)) hhPlusPlusConfig.loadConfig() hhPlusPlusConfig.runModules() HHPlusPlus.Helpers.runDeferred() }, 1) 4 Link to comment Share on other sites More sharing options...
Horsting Posted August 8, 2023 Share Posted August 8, 2023 (edited) Works great. I see you removed the (original) power column, which I found a helpful orientation, also for sorting. But it was a necessity with the additional team power and E[X] columns. And now that E[X] is very close to exact, it supersedes the power value very good. Sorting by this would be nice. There are still glitches in combination with MM's script, when sorting, hiding finished opponents etc, but I think there is no point investing too much time in making and keeping those two fully compatible. In the end I hope Zoo (and Tom) finds time and mood to implement the features into HH++, step by step. Edited August 8, 2023 by Horsting Link to comment Share on other sites More sharing options...
Miccia Posted August 8, 2023 Share Posted August 8, 2023 Does anyone feel like taking over the Harem++ script after Liliat quits? It's a great script, it still works very well even if it should be implemented with the new skills. I love that script and the idea that with future updates it stops working terrifies me, probably now that I'm used to the update speed of the harem I would seriously consider quitting the game. 2 Link to comment Share on other sites More sharing options...
renalove Posted August 8, 2023 Share Posted August 8, 2023 I have good news and bad news. 1. A new battle simulator that supports girl skills has been completed. 😁 2. The league points simulator was terribly slow. I was forced to remove it. 😭 Here is the script: https://github.com/rena-jp/hh-battle-simulator 1 6 Link to comment Share on other sites More sharing options...
Horsting Posted August 8, 2023 Share Posted August 8, 2023 (edited) 19 minutes ago, renalove said: A new battle simulator that supports girl skills has been completed. 😁 Niice, the tier 4 skill is the part still missing in all other scripts. And the difference is not exactly small. Here an example with 3 maxed girls (2 common ones, but still): The skills are are a little OP IMO. Edited August 8, 2023 by Horsting 2 Link to comment Share on other sites More sharing options...
Recommended Posts
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now