// ==UserScript==
// @name Hentai Heroes+++
// @description Few QoL improvement and additions for competitive PvP players.
// @version 0.20.1
// @match https://*.hentaiheroes.com/*
// @match https://nutaku.haremheroes.com/*
// @match https://*.gayharem.com/*
// @match https://*.comixharem.com/*
// @match https://*.hornyheroes.com/*
// @match https://*.pornstarharem.com/*
// @match https://*.transpornstarharem.com/*
// @match https://*.gaypornstarharem.com/*
// @run-at document-body
// @namespace https://gitlab.com/hentaiheroes/hh-plus-plus-plus
// @grant none
// @license MIT
// @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";
// 3rd party localStorage keys
const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus'
const HHPLUSPLUS_OPPONENT_FILTER = LS_CONFIG_HHPLUSPLUS_NAME + "OpponentFilter"
// CONFIG
const MAX_NUM_SNAPSHOTS = 230;
const BOOSTER_EXPIRATION_MULTIPLIER = 1.01;
// 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.collectEquipmentData();
}, 250);
HHPlusPlus.Helpers.onAjaxResponse(/action=market_equip_armor*/, EquipmentCollector.onEquipmentChange);
});
}
static onEquipmentChange(data) {
// We need to delay the execution a bit to give chance to the native callback to run.
setTimeout(() => {
// The is a bug in HH (quelle surprise) when swapping equipments - the native callbacks adds the
// unequipped armor to the back of the inventory ('player_inventory'), but the equipped armor is not
// removed, which leaves an inconsistent state, so we need to remove it ourselves.
EquipmentCollector.removeEquippedArmorFromInventory(data.equipped_armor);
EquipmentCollector.collectEquipmentData();
}, 100);
}
static removeEquippedArmorFromInventory(equipped) {
const idx = player_inventory.armor.findIndex(e => {
return e.item.rarity == "mythic" &&
e.resonance_bonuses.class.bonus == equipped.resonance_bonuses.class.bonus &&
e.resonance_bonuses.class.identifier == equipped.resonance_bonuses.class.identifier &&
e.resonance_bonuses.class.resonance == equipped.resonance_bonuses.class.resonance &&
e.resonance_bonuses.theme.bonus == equipped.resonance_bonuses.theme.bonus &&
e.resonance_bonuses.theme.identifier == equipped.resonance_bonuses.theme.identifier &&
e.resonance_bonuses.theme.resonance == equipped.resonance_bonuses.theme.resonance &&
e.skin.id_item_skin === equipped.skin.id_item_skin &&
e.skin.id_skin_set === equipped.skin.id_skin_set &&
e.skin.identifier === equipped.skin.identifier &&
e.skin.name === equipped.skin.name &&
e.skin.subtype === equipped.skin.subtype;
});
// player_inventory.armor[idx] = data.unequipped_armor;
player_inventory.armor.splice(idx, 1);
}
static collectEquipmentData() {
EquipmentCollector.collectPlayerEquipment();
EquipmentCollector.collectBestMythicEquipment();
}
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),
bonuses: e.resonance_bonuses,
};
});
window.localStorage.setItem(EQUIPMENT_CURRENT_KEY, JSON.stringify(equipmentStripped));
}
static collectBestMythicEquipment() {
const hero = window.Hero ?? shared.Hero;
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('leagues')) {
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 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('leagues')}
run () {
if (this.hasRun || !this.shouldRun()) {return}
$(document).on('league:rollover', () => {
const data = LeagueScoutModule.getCurrent();
LeagueScoutModule.deleteCurrent();
LeagueScoutModule.setPrevious(data);
LeaguePlayersCollector.clear();
})
HHPlusPlus.Helpers.defer(() => {
// read and store data
this.storeSnapshot(this.readSnapshot());
// 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 = parseInt(player.player.id_fighter);
const name = player.player.nickname;
const country = player.country;
const level = parseInt(player.player.level);
const entry = {id, name, country, level};
if (Object.values(entry).some(x => x == undefined || (typeof x !== 'string' && !Array.isArray(x) && isNaN(x)))) {
console.log('Some player data is missing, maybe the opponents_list data structure changed?');
console.log(entry);
}
data[id] = {name, country, level};
}
return data;
}
readSnapshot() {
const data = [];
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 rank = player.place;
const points = parseInt(player.player_league_points);
const elements = player.player.team.theme;
const damage = player.player.damage;
const defense = player.player.defense;
const ego = player.player.remaining_ego;
const chance = player.player.chance;
const power = player.player.team.total_power;
// Take only the first two chars of the booster names, as those should be unique. Assume all are legendary.
const boosters = player.boosters.filter(b => b.expiration > 0).map(b => b.item.name.slice(0, 2))
// Create player snapshot and validate it.
const entry = {id, rank, elements, points, power, damage, defense, ego, chance, boosters};
if (Object.values(entry).some(x => x == undefined || (typeof x !== 'string' && !Array.isArray(x) && isNaN(x)))) {
console.log('Some player data is missing, maybe the opponents_list data structure changed?');
console.log(entry);
}
data.push(entry);
}
// Sort the parsed data by rank.
data.sort((a, b) => a.rank > b.rank);
return data;
}
storeSnapshot(snapshot_data) {
var data = LeagueScoutModule.getCurrent();
const current_date = new Date(window.server_now_ts * 1000);
const league_end_date = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000);
// Create the initial container data structure
if (Object.keys(data).length == 0) {
data = {
league_end: league_end_date,
num_players: data.length,
player_data: this.readPlayerData(),
snapshots: [],
}
}
const snapshot = {
date: current_date,
snapshot: snapshot_data,
}
if (data.snapshots.length && JSON.stringify(data.snapshots[data.snapshots.length - 1].snapshot) === JSON.stringify(snapshot.snapshot)) {
return;
}
if (data.snapshots.length >= MAX_NUM_SNAPSHOTS) {
var previous = LeagueScoutModule.getPrevious();
// delete an entry either from the previous league snapshots or from the current
if (previous && previous.snapshots && previous.snapshots.length > 0) {
previous.snapshots.shift()
LeagueScoutModule.setPrevious(previous);
} else {
data.snapshots.shift();
}
}
data.snapshots.push(snapshot);
LeagueScoutModule.setCurrent(data);
}
createButton(id, path) {
return `<svg id="${id}" class="blue_button_L" width="32" height="32" style="padding: 5px" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="#FFFFFF"><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 = LeagueScoutModule.get(what);
const separator = ","
const columns = ["date", "player_id", "player_name", "player_rank", "player_points", "player_power", "player_damage", "player_defense", "player_ego", "player_chance", "player_boosters"];
const values = data.snapshots.flatMap((e) => e.snapshot.map((p) => [e.date, p.id, data.player_data[p.id].name, p.rank, p.points, p.power, p.damage, p.defense, p.ego, p.chance, `"${p.boosters.join(",")}"`].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);
});
}
static getCurrent() {
return LeagueScoutModule.get(CURRENT_LEAGUE_SNAPSHOT_KEY);
}
static getPrevious() {
return LeagueScoutModule.get(PREVIOUS_LEAGUE_SNAPSHOT_KEY);
}
static get(key) {
var data = JSON.parse(storage.getItem(key)) || {};
// Migrate from the old data structure
if (Array.isArray(data) && data.length > 0) {
const last_snapshot = data[data.length - 1];
const reduceP = ({id, name, country, level}) => ({id, name, country, level});
const reduceS = ({id, rank, elements, points, damage, defense, ego, chance, boosters}) => ({id, rank, elements, points, damage, defense, ego, chance, boosters});
const player_data = last_snapshot.player_data.map(reduceP).reduce((map, obj) => {
map[obj.id] = obj;
return map;
}, {});
const snapshots = data.map(d => ({date: d.date, snapshot: d.player_data.map(reduceS)}));
data = {
league_end: last_snapshot.league_end,
num_players: last_snapshot.num_players,
player_data,
snapshots,
}
}
return data;
}
static deleteCurrent() {
storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY);
}
static setCurrent(data) {
this.set(CURRENT_LEAGUE_SNAPSHOT_KEY, data);
}
static setPrevious(data) {
this.set(PREVIOUS_LEAGUE_SNAPSHOT_KEY, data);
}
static set(key, data) {
if (data.snapshots.length > 0) {
storage.setItem(key, JSON.stringify(data));
} else {
storage.removeItem(key);
}
}
}
class LeagueTableModule extends MyModule {
constructor () {
const baseKey = 'leagueTable'
const configSchema = {
baseKey,
default: true,
label: `Extend league table with additional opponents' information`,
subSettings: [
{
key: 'girl_power',
label: 'Show girl power in the league table',
default: false
},
{
key: 'kinkoid_power',
label: 'Show the new power stat in the league table',
default: false
},
{
key: 'number_of_bulbs',
label: 'Show the number of invested bulbs in the league table',
default: true
},
{
key: 'load_player_data',
label: 'Load player data on league table row click',
default: true
},
],
}
super({name: baseKey, configSchema})
this.all_new_columns = ['kinkoid_power', 'girl_power', 'number_of_bulbs'];
this.anchor_column = 'power';
}
shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('leagues')}
run(config) {
if (this.hasRun || !this.shouldRun()) {return}
HHPlusPlus.Helpers.defer(() => {
HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_table', () => {
this.extendLeagueDataModel();
this.addPlayerSelectHandler(config);
this.showPlayersPlacementBadge(config);
this.showAdditionalTableHeaders(config);
this.showAdditionalTableColumns(config);
this.showMaxPointsTooltip();
this.detectReallyExpiredBoosers();
this.detectInflatedPower();
this.makeCompatibleWithLeaguePlusPlus();
});
$(document).on('player:update-profile-data', (event, data) => {
this.extendLeagueDataModel();
this.showPlayersPlacementBadge(config);
});
$(document).on('league:table-sorted', () => {
this.showPlayersPlacementBadge(config);
this.showAdditionalTableColumns(config);
this.showMaxPointsTooltip(config);
this.detectReallyExpiredBoosers();
this.detectInflatedPower();
this.makeCompatibleWithLeaguePlusPlus();
});
});
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 = 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;
player.best_placement = best_placement;
player.placement_count = placement_count;
player.kinkoid_power = number_reduce(player.player.team.power_display);
player.girl_power = player.player.team.total_power.toFixed();
player.number_of_bulbs = player.player.team.girls.flatMap(g => Object.values(g.skill_tiers_info)).reduce((a,g)=>a+g.skill_points_used, 0)
}
}
showAdditionalTableHeaders(config) {
// Additional CSS classes
const row_styles = {kinkoid_power: '2rem', girl_power: '2.2rem', number_of_bulbs: '0.9rem'}
for (const [clazz, min_width] of Object.entries(row_styles)) {
// this.insertRule(`.league_table .data-row .head-column[column="${clazz}"] {display: flex; align-items: center; justify-content: center}`);
this.insertRule(`.league_table .data-row .data-column[column="${clazz}"] {min-width: ${min_width}}`);
}
const columns = this.all_new_columns.filter(c => config[c]);
const headers = {
kinkoid_power: `<span>${GT.design.caracs_sum}</span>`,
girl_power: `<span>${GT.design.total_power}</span>`, // <span class="upDownArrows_mix_icn">
number_of_bulbs: '<span class="scrolls_legendary_icn"></span>',
}
$(`div.league_table div.head-row div.head-column[column=${this.anchor_column}]`).after(
columns.map(c => `<div class="data-column head-column" column="${c}">${headers[c]}</div>`).join('')
);
}
showAdditionalTableColumns(config) {
this.insertRule(`.active_skill {color: red; text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000;}`);
const context = this;
$('div.league_table')
.find('div.body-row')
.each(function(index) {
const opponent = window.opponents_list[index];
const columns = context.all_new_columns.filter(c => config[c]);
$(this).find(`div.data-column[column=${context.anchor_column}]`).after(
columns.map(c => `<div class="data-column" column="${c}"><div class="${context.tableColumnClass(c, opponent)}">${opponent[c]}</div></div>`).join('')
);
})
}
tableColumnClass(column, opponent) {
if (!column.includes('bulb')) {
return '';
}
const clazz = 'active_skill'; // or active_skills_icn
const active_skills = opponent.player.team.girls[0].skill_tiers_info[5];
return active_skills && active_skills.skill_points_used > 0 ? clazz : '';
}
showMaxPointsTooltip() {
const data = LeagueScoutModule.getCurrent();
if (!data || !data.snapshots.length) {
return;
}
$('.league_table .body-row').each(function (idx) {
const opponent = opponents_list[idx];
const opponent_id = parseInt(opponent.player.id_fighter);
const points = data.snapshots.map(d => d.snapshot.find(p => p.id == opponent_id).points);
const remainder = points[0] % 25;
var lost_points = (remainder == 0 ? 0 : 25 - remainder);
for (let i = 0; i < points.length - 1; i+=1) {
const diff = points[i+1] - points[i];
const remainder = diff % 25;
lost_points += (remainder == 0 ? 0 : 25 - remainder);
}
const element = $(this).find('.data-column[column=player_league_points]');
element.attr('tooltip', `Lost points: ${lost_points}`);
});
}
detectReallyExpiredBoosers() {
const data = LeagueScoutModule.getCurrent();
if (!data || !data.snapshots.length) {
return;
}
$('.league_table .body-row').each(function (idx) {
const opponent = opponents_list[idx];
const opponent_id = parseInt(opponent.player.id_fighter);
const is_currently_boosted = opponent.boosters.some(b => b.expiration > 0);
if (is_currently_boosted) {
return;
}
const snapshot_data = data.snapshots.map(d => d.snapshot.find(p => p.id == opponent_id)).reverse();
const boosted_data = snapshot_data.find(p => p.boosters && p.boosters.length >= 3);
if (!boosted_data) {
return;
}
if (
(opponent.player.damage * BOOSTER_EXPIRATION_MULTIPLIER) >= boosted_data.damage &&
(opponent.player.defense * BOOSTER_EXPIRATION_MULTIPLIER) >= boosted_data.defense &&
(opponent.player.remaining_ego * BOOSTER_EXPIRATION_MULTIPLIER) >= boosted_data.ego &&
(opponent.player.chance * BOOSTER_EXPIRATION_MULTIPLIER) >= boosted_data.chance
) {
const element = $(this).find('.data-column[column=boosters]');
element.addClass('active_skill');
}
});
}
detectInflatedPower(take_n = 30) {
const data = LeagueScoutModule.getCurrent();
if (!data || !data.snapshots.length) {
return;
}
$('.league_table .body-row').each(function (idx) {
const opponent = opponents_list[idx];
const opponent_id = parseInt(opponent.player.id_fighter);
const current_power = opponent.player.team.total_power;
const snapshot_data = data.snapshots.map(d => d.snapshot.find(p => p.id == opponent_id));
const latest_power = snapshot_data.map(x => x.power).filter(x => x).slice(-1 * take_n);
const lowest_power = Math.min(...latest_power);
if (current_power > lowest_power) {
const element = $(this).find('.data-column[column=team] span.team-power');
element.addClass('active_skill');
element.attr('tooltip', `Previously: ${lowest_power}`);
}
});
}
showPlayersPlacementBadge(config) {
if (!config.load_player_data) {
return;
}
// Additional CSS classes
this.insertRule('.badge.top1 {background-color:#ec0039}');
this.insertRule('.badge.top1::after {content:"1"}');
for (let i = 2; i <= 4; i++) {
// this.insertRule(`.badge.top${i} {background-color:#8e36a9}`);
this.insertRule(`.badge.top${i} {background:var(--legendary-bg);background-size:cover}`);
this.insertRule(`.badge.top${i}::after {content:"${i}"}`);
}
const context = this;
$('.league_table .body-row').each(function (idx) {
const opponent = opponents_list[idx];
if (opponent.best_placement != undefined) {
context.updatePlayerPlacementBadge($(this), opponent);
}
});
}
addPlayerSelectHandler(config) {
if (!config.load_player_data) {
return;
}
// Remove the go_pre_battle class to allow users to select the row (inspired by Leagues++)
$('.league_table .data-column[column=can_fight] .go_pre_battle').removeClass('go_pre_battle');
$('.league_table .body-row').on('click', function() {
const element_nickname = $(this).find('.data-column[column=nickname] span.nickname')
const player_id = element_nickname.attr('id-member');
const opponent = window.opponents_list.find(x => parseInt(x.player.id_fighter) == player_id);
if (opponent.best_placement != undefined) {
return false;
}
window.$.post({
url: '/ajax.php',
data: {
action: 'fetch_hero',
id: 'profile',
preview: false,
player_id: parseInt(player_id),
},
success: (data) => {}
});
return false;
});
}
updatePlayerPlacementBadge(row, player_data) {
const nicknameElement = row.find('div.data-column[column=nickname]');
var badgeContainer = nicknameElement.find('.badge-container');
if (!badgeContainer.length) {
badgeContainer = $('<div class="badge-container" />').appendTo(nicknameElement);
}
// best placement indicator next to the nickname
badgeContainer.html(this.createBestPlacementBadge(player_data));
}
createBestPlacementBadge(player) {
if (player.best_placement < 1 || player.best_placement > 4) {
return ''
}
const clazz = `top${player.best_placement}`;
return `<span class="best-placement"><span class="scriptLeagueInfoIcon badge ${clazz}"></span>${player.placement_count}</span>`;
}
makeCompatibleWithLeaguePlusPlus() {
HHPlusPlus.Helpers.doWhenSelectorAvailable('div#leagues div.league_buttons a#change_team', () => {
// Remove the avatars
$('div.league_table div.data-row div.data-column[column=nickname] div.square-avatar-wrapper').remove();
// Hide row when opponent has been fought
const context = this;
$('body').on('DOMSubtreeModified', '.league_table .body-row .data-column[column=match_history_sorting]', function() {
context.hideUnhideRow($(this).parent('div.body-row'), context.isHideOpponents());
});
});
}
hideUnhideRow(row, hide) {
const results = row.find('div.data-column[column=match_history_sorting]').find('div[class!="result "]').length;
const fought_all = results == 3;
if (fought_all && hide) {
row.hide();
} else if (fought_all && !hide) {
row.show();
}
}
isHideOpponents() {
const filter = JSON.parse(storage.getItem(HHPLUSPLUS_OPPONENT_FILTER)) || {fought_opponent: false};
return filter.fought_opponent;
}
}
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') || HHPlusPlus.Helpers.isCurrentPage('leagues')}
run() {
if (this.hasRun || !this.shouldRun()) {return}
HHPlusPlus.Helpers.defer(() => {
if (HHPlusPlus.Helpers.isCurrentPage('leagues')) {
$(document).ajaxComplete((evt, xhr, opt) => {
if (xhr.status == 200 && ~opt.url.search(/\/leagues-pre-battle.html\?id_opponent=\d+/)) {
const hero = window.Hero ?? shared.Hero;
const me = opponents_list.find(p => parseInt(p.player.id_fighter) == hero.infos.id);
const themes = me.player.team.theme_elements.map(x => x.type);
this.checkMythicEquipment(themes);
}
});
}
if (HHPlusPlus.Helpers.isCurrentPage('leagues-pre-battle')) {
HHPlusPlus.Helpers.doWhenSelectorAvailable('div.player-panel div.player-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);
this.checkMythicEquipment(themes);
});
}
});
this.hasRun = true;
}
checkMythicEquipment(themes_or_empty) {
// Additional CSS classes
this.insertRule(`.slot.size_xxs {width:1.5rem;height:1.5rem;-webkit-border-radius:.2rem;-moz-border-radius:.2rem;border-radius:.2rem}`);
const me = EquipmentCollector.getBestMythic();
const equipment_themes = me.map(x => x.resonance_bonuses.theme.identifier || 'balanced');
const themes = themes_or_empty.length ? themes_or_empty : ['balanced'];
const has_matching_me = themes.some(t => equipment_themes.includes(t));
if (has_matching_me) {
const tooltip = "You have a perfect mythic equipment for your team in your inventory.";
$('div.opponent div.player_details').append(
`<div class="slot size_xxs mythic random_equipment mythic" rarity="mythic" tooltip="${tooltip}">
<span class="mythic_equipment_icn"></span>
</div>`
);
}
}
}
class GirlPreviewModule extends MyModule {
constructor () {
const baseKey = 'girlPreviewFilters'
const configSchema = {
baseKey,
default: true,
label: `Girl preview`,
subSettings: [
{
key: 'preview_girl_pose',
label: 'Uncensor girl pose preview',
default: false
},
]
}
super({name: baseKey, configSchema})
}
shouldRun() {return true;}
run ({preview_girl_pose}) {
if (this.hasRun || !this.shouldRun()) {return}
HHPlusPlus.Helpers.defer(() => {
if (preview_girl_pose) {
this.previewGirlPose();
}
});
this.hasRun = true;
}
previewGirlPose() {
const observer = new MutationObserver(() => {
HHPlusPlus.Helpers.doWhenSelectorAvailable('#girl_preview_popup', () => {
$("div.pose-preview_wrapper").removeClass("locked");
$("span.preview-locked_icn").remove();
});
})
observer.observe($('#common-popups')[0], {childList: true});
}
}
class SeasonalEventModule extends MyModule {
constructor () {
const baseKey = 'seasonalEvent'
const configSchema = {
baseKey,
default: true,
label: `Seasonal event`,
}
super({name: baseKey, configSchema})
}
shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('seasonal');}
run () {
if (this.hasRun || !this.shouldRun()) {return}
HHPlusPlus.Helpers.defer(() => {
for (const e of [50, 100, 250, 500, 1000]) {
this.insertRule(`.badge.top${e} {background-color:#333; text-align:center}`);
this.insertRule(`.badge.top${e}::after {content:"${e}"}`);
this.insertRule('.scriptLeagueInfoIcon {display: inline-block;height: 16px;width: 32px;font-size: 10px;border-radius: 5px;margin-left: 6px;margin-right: 2px;text-shadow: 0 0 1px #000;-moz-transform: rotate(0.05deg);');
this.insertRule('.leaderboard-placement {font-size: 12px;text-shadow:1px 1px 0 #000}');
}
HHPlusPlus.Helpers.onAjaxResponse(/action=leaderboard&feature=seasonal_event_top/, this.rankingBadges);
});
this.hasRun = true;
}
rankingBadges(leaderboard) {
HHPlusPlus.Helpers.doWhenSelectorAvailable('.ranking-timer', () => {
const xd = [50, 100, 250, 500, 1000].map(e => {
const diff = leaderboard.leaderboard[e - 1].potions - leaderboard.hero_data.potions + 1;
return `<span class="leaderboard-placement"><span class="scriptLeagueInfoIcon badge top${e}"></span>${diff}</span>`;
}).join('');
$("div.ranking-timer").append(xd);
});
}
}
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();
// modules
const modules = [
new LeagueScoutModule(),
new LeagueTableModule(),
new PrebattleFlightCheckModule(),
new GirlPreviewModule(),
new SeasonalEventModule(),
]
// 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)