Jump to content

Community Scripts [Links in OP]


DvDivXXX
 Share

Recommended Posts

31 minutes ago, DvDivXXX said:

One minor inconvenience I've been experiencing since I've noticed the indicators for top 4 or past wins (after visiting the opponent's profile) and that no one else seems to (judging by others' screenshots) is that I have the purple square to highlight top 4s, but not the red square to highlight wins:

This is 430i's script. And indeed, top 4 has (always had) the purple square while winners never had such a highlight, which made it a little difficult to see them easily between all the numbers and with the line breaks. With HH++ and L++, there is in any case more space required for this extra info to be shown nicely 🤔.

  • Thanks 1
Link to comment
Share on other sites

  • Moderator
On 8/15/2023 at 9:04 PM, Bobick said:

It should look a little different with the updated script, but the numbers reveal the best league results of your opponents (from their player profile):

image.png.282514ea9d72dbce9c40914b5d6f4159.png

@Horsting But at least some players seem to have a red square to highlight past winners. That's why I'm confused (good to know I'm not the only one with only the top 4 purple square and no highlight for wins, though).

---
While I'm nitpicking, since the latest updates of all three scripts I'm using (BDSM, Leagues++ and HH+++) this column became obsolete (at least for me) and I wouldn't mind the option to get rid of it (which might give a bit more room for other things currently a bit compressed such as past wins or top 4s as mentioned, but also opponents' names). I'm not clear on which script would need to make what change for that to happen, though:

image.png

Edited by DvDivXXX
updated
  • Like 1
Link to comment
Share on other sites

17 minutes ago, DvDivXXX said:

But at least some players seem to have a red square to highlight past winners.

Oh, interesting, I never recognised it, or rather, I only actively recognised that/when it was missing. Probably it is also related to insufficient space.

We'll see what 430i makes out of this. The additional E[X] column is indeed obsolete, and it is probably nice to have the TP or this new overall power column added optionally. Especially for those which aim for top 4, I guess, the info about which player has which league history is quite valuable, so I hope we get an update for the script to work nicely with HH++ and L++ with this feature 😊.

  • Like 1
Link to comment
Share on other sites

1 hour ago, DvDivXXX said:

Any idea what might cause it?

You are just too good and there is nobody else winning? 🤪

23 minutes ago, DvDivXXX said:

But at least some players seem to have a red square

I'm pretty sure I've seen those once ore twice, but that was when the script was new and I clicked those buttons a few  times.
Haven't done that in while, cause it is not important to me, and right now I only use BDSM, so can't tell if that still works.

  • Juicy peach 1
Link to comment
Share on other sites

@DvDivXXX The others are correct - the "best placement" boxes come from HH+++ and not from MM. I was a bit confused why it does not work for you, but seemingly noone else complained, but I think I have figured it out. I used some styling options from what I assumed was the base game, but it turned out they were provided by Harem++. Everyone who has it installed sees the proper red boxes, and I guess you dont? Anyway, this is fixed in the latest version so everything should be shown correctly independent of whether you have Harem++ or not. Let me know if that's not the case.

@-MM- There seems to be one major issue/incompatibility between your script and @zoopokemon's. Whenever your script is active, the theme colors from the snapshotted team are shown and not the currently selected team. To test it just select a different team, eg a balanced one and reload the page with and without your script. Without your script you will see a balanced team in the league table and with yours - the team colors from the last snapshot, presumably white. Looks a bit obscure, but I am hoping you have the time to take a look.

 

Anyway, here is the updated version from HH+++. I have removed a bunch of stuff, given that most is now part of HH++

  • [updated] Load the opponents' profile data when selecting them in the table - this will display their best D3 results in the league table next to the player's name. Configurable from the settings.

  • [updated] A check on the pre-battle page whether you have a suitable not-equipped mythic equipment in your inventory. It will show a small mythic equipment icon next to your opponents class. Works with and without League++, but it might display stale data when League++ is on due to the issue above.

  • Module for gathering league data. Whenever the league standings change a new snapshot will be stored. Afterwards you can export all stored snapshots as csv file.

  • Additional harem filter(s): filter for Legendary Days girls ('filter by' dropdown in the native harem)

Known issues:

  • [new] The table does not show any of the power columns anymore. I am hoping that @zoopokemon will add it.
Spoiler
// ==UserScript==
// @name            Hentai Heroes+++
// @description     Few QoL improvement and additions for competitive PvP players.
// @version         0.18.4
// @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";

// 3rd party localStorage keys
const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus'
const HHPLUSPLUS_OPPONENT_FILTER = LS_CONFIG_HHPLUSPLUS_NAME + "OpponentFilter"

// 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 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 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
            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}" 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 = 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`,
            subSettings: [],
        }
        super({name: baseKey, configSchema})
    }

    shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')}

    run() {
        if (this.hasRun || !this.shouldRun()) {return}

        HHPlusPlus.Helpers.defer(() => {
            HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_table', () => {
                this.extendLeagueDataModel();
                this.showPlayersPlacementBadge();
                this.addPlayerSelectHandler();
                // this.makeCompatibleWithLeaguePlusPlus();
            });

            $(document).on('player:update-profile-data', (event, data) => {
                this.extendLeagueDataModel();
                this.showPlayersPlacementBadge();
            });

            $(document).on('league:table-sorted', () => {
                this.showPlayersPlacementBadge();
            });
        });

        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;
        }
    }

    showPlayersPlacementBadge() {
        // Additional CSS classes
        this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:#ec0039}`);
        this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}');

        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() {
        // 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) {
        var clazz = ''
        if (player.best_placement == 1) {
            clazz = 'top1';
        } else if (player.best_placement >= 2 && player.best_placement <= 4) {
            clazz = 'top4';
        }

        return clazz ? `<span class="best-placement"><span class="scriptLeagueInfoIcon ${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('tower-of-fame')}

    run() {
        if (this.hasRun || !this.shouldRun()) {return}

        HHPlusPlus.Helpers.defer(() => {
            if (HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')) {
                $(document).ajaxComplete((evt, xhr, opt) => {
                    if (xhr.status == 200 && ~opt.url.search(/\/leagues-pre-battle.html\?id_opponent=\d+/)) {
                        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 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();

    // 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)

 

 

  • Thanks 3
  • Hearts 1
Link to comment
Share on other sites

  • Moderator
25 minutes ago, 430i said:

I used some styling options from what I assumed was the base game, but it turned out they were provided by Harem++. Everyone who has it installed sees the proper red boxes, and I guess you dont?

I had to open the monkey's dashboard to make sure. Harem++ is Liliat's script to help manage GG stuff, right? So, I actually have it installed, but I disable it by default, except when I want to look at my GG inventory as a whole. In general, while I find it very useful for that, I much prefer the normal harem view for most other things. Plus, I feel that the more scripts I run at once, the better the chance for conflicts between them (although in this particular case it seems that it was the other way around ^^).

For science, before updating your script, I'll enable Harem++ to confirm if that makes the red squares appear or not for me. Wait for iiiit... Yop. Your guess was spot on!

image.png

image.png

Never in a million years would have I guessed that a script that seemingly only affects the Harem page would have side effects on the Leagues page. ^^

Thanks a ton for this and for your new update, which I'll hurry up and install right now after disabling Harem++ again. ❤️

EDIT: It's much cleaner with your new version! (And I can confirm it now works with Liliat's script disabled):

image.png

 

27 minutes ago, 430i said:

Known issues:

  • [new] The table does not show any of the power columns anymore. I am hoping that @zoopokemon will add it.

I for one don't have any use for the "Power" column whatsoever. I find it a very vague, misleading and redundant indicator. So I'm in no hurry for this "issue" to get fixed (if others find it useful, that's cool, but honestly I'd love the option to hide it in that case).

Edited by DvDivXXX
updated
  • Like 1
Link to comment
Share on other sites

Nine scripts seems too much for me, since I don't know if they're working well in synch.

So if to use only 2-3 scripts permanently, which would you use? I'm using HH++ and Leagues++, and toggling the Pantheon script on rarely (since I past temple 1900).

Edition: Thanks @DvDivXXX for the idea to install and toggle on/off the Harem++ script, I should try it when I'll get back home from a short vacation. Maybe I should try doing that (installing and toggling on and off) for more scripts... 🤔 In this way, I can use 3-4 scripts on a given moment.

Edited by OmerB
  • Thanks 1
Link to comment
Share on other sites

6 hours ago, DvDivXXX said:

I for one don't have any use for the "Power" column whatsoever. I find it a very vague, misleading and redundant indicator.

430i had replaced the new power indicator by the team power of the opponents. I agree with him, it would be helpful to see these values at a glance in the table.

Edited by Bobick
  • Thanks 1
Link to comment
Share on other sites

8 hours ago, OmerB said:

So if to use only 2-3 scripts permanently, which would you use? I'm using HH++ and Leagues++, and toggling the Pantheon script on rarely (since I past temple 1900).

Most scripts touch different parts of the game, and even when touching similar parts, often they just do some internal info scraping and processing and then add an individual HTML element somewhere. It is rare that they really conflict, and if so, only visually, like HH++, League++ and 430i's league tweaks caused some quirks here and there when one one the game was upgrading, pretty expected after the large league upgrade, followed by the second smaller one.

League++ at least even goes a little further and only loads on the league subframe, hence is not loaded at all on any other HH page. Would be actually great if the headband simulator would do the same (if not the case already), just to follow best practice 🙂.

E.g. the pantheon (headband?) script does a pretty isolated thing on specific page. I see no reason to disable it. Rena's battle simulator adds its odds at a different place, compared to HH++, also not issue. The same calculation (respecting girls' tier 4 skill) is now also done by HH++, so I just keep it for the nice rounds table and to check whether really both scripts come to the same result in all cases.

So basically what I am saying: No need to worry about enabling a lot of scripts. Only if something does not look right somewhere, of course check whether it might be a conflict.

In case of Div, the equipment tooltip and booster detector are obsolete. Although the booster detector would still have some value, since the game shows them without any consistent correctness 😒.

  • Thanks 1
Link to comment
Share on other sites

  • Moderator
6 hours ago, Bobick said:

430i had replaced the new power indicator by the team power of the opponents. I agree with him, it would be helpful to see these values at a glance in the table.

Fair enough, I know many players seem to value "TP" (especially Ravi ^^) for some reason. I understand it's a lot more reliable and meaningful than the seemingly random and impossible to gauge "Power" column that Kinkoid introduced with the League UI destruction rework.

Personally, I never cared about "TP" even when I'm examining a specific opponent's setup or my own, as it doesn't tell me anything that I find valuable when I already have the final stats and the battle sim results, but to each their own.

I'd still love the option to hide it for myself if that's a possibility. 🙏🚽

Toilet Paper Running Sticker by Sam Brown Video for iOS ...

  • Like 2
Link to comment
Share on other sites

4 minutes ago, DvDivXXX said:

the seemingly random and impossible to gauge "Power" column

It is not random but follows a clear formula. As it is based on all final opponent stats, it is actually a better indicator for the opponent's difficulty than TP. Only benefit of TP is that, in combination with the final opponent stats, one is better able to derive the booster status. Otherwise I see no reason why one would want back TP.

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

  • Moderator
21 minutes ago, Horsting said:

Only benefit of TP is that, in combination with the final opponent stats, one is better able to derive the booster status. Otherwise I see no reason why one would want back TP.

Thanks for clarifying this for me. Well, since we can already see the opponent's boosters (even if we can't reliably know for sure just from that if they're really active or not at the time), even for this purpose TP doesn't sound that useful to me. It's pretty easy to tell those 4 cordys the script(s) show as expired are actually still active in the current snapshot by checking the opponent's Attack, and so on.

  • Like 1
Link to comment
Share on other sites

45 minutes ago, Horsting said:

Only benefit of TP is that, in combination with the final opponent stats, one is better able to derive the booster status. Otherwise I see no reason why one would want back TP.

That's exactly the reason why I want it back. I don't trust the booster status and opponent stats.

 

20 minutes ago, DvDivXXX said:

It's pretty easy to tell those 4 cordys the script(s) show as expired are actually still active in the current snapshot by checking the opponent's Attack, and so on.

4 cordys sure, but 1 cordy, 1 chlorella, 2 ginseng? 

I am of course fully aware that you have significantly more experience in D3, your league strategies and skills are certainly superior to mine. I feel a bit disoriented at the moment and can't rule out making some mistakes in choosing opponents. 

Edited by Bobick
  • Like 1
Link to comment
Share on other sites

I think 1 cordy/chlorella/ginseng is anyway not possible to reliably derive, due to MG, GG and tier 3 skill. Although it can be derived whether one does NOT have them, while (a little) too high stats can also come from gear/skills. When unsure, with L++, one can still select the opponent to see TP as well.

After all, best would be to have this configurable, of course, so everyone can see the column(s) one is most used to/can work best with.

  • Like 1
Link to comment
Share on other sites

Two suggestions:

1. @zoopokemon on the HH++ script, add an option to view the original 'power' column instead of the E[x] column. It seems that there are some players who wants it (I among them wants to see the power column, I can see the E[x] in each player window, so I don't need to have a column for it).

2. @-MM- When I'm doing one challenge at a time (which I prefer), the screen goes to the fight scene, and then comes back to the league screen, with the default order (regardless of how I ordered it before the fight). When I'm doing x3 fights at a time, the screen doesn't change, nor the order. Can you make it that the x1 fight will work the same (not changing the screen, and retaining the order that I chose)?

  • Like 1
  • Thinking 1
Link to comment
Share on other sites

I think there is a bug in zoo's script - when I click on the energy bar, I am redirected to the first side quest and not to the main story. Anyone else having this issue? I recently reached the last world (but I am far from the last scroll), maybe it's related to that 🤔?

Link to comment
Share on other sites

  • Moderator
1 hour ago, OmerB said:

I can see the E[x] in each player window, so I don't need to have a column for it).

I love the E[X] column.  It makes it so simple.  Wish it existed in the old format.  It's basically what I already did w/ a spreadsheet before, but no longer have to.

The power column on the other hand can be misleading, b/c it favors atk and def.

  • Like 2
Link to comment
Share on other sites

8 hours ago, 430i said:

I think there is a bug in zoo's script - when I click on the energy bar, I am redirected to the first side quest and not to the main story. Anyone else having this issue? I recently reached the last world (but I am far from the last scroll), maybe it's related to that 🤔?

AFAIK, It's not a script thing, but the plain game one. I know a player in my club, who plays without scripts (on CxH), who had similar problem.

Apparently, on some pages in the last world (on CxH too), the link to the latest place in the adventure is broken, so it reverted you to the side quests or to the champions to use your energy (as DvD has written). I experienced it on the last story of CxH.

Edited by OmerB
Link to comment
Share on other sites

1 hour ago, Ravi-Sama said:

The power column on the other hand can be misleading, b/c it favors atk and def.

Quote

Power = Ego + (Attack + Defense)*7.5 + Harmony*0.625

While it is probably slightly too much favoured (I would take factor 6 compared to ego), generally it makes much sense, of course. Harmony is too low rated, IMO, but I never really did math about how much its effect usually is. But I would probably give it factor 2 compared to ego.

I also like E[X] the most. But everyone is different, so best for the script would be probably the choice among E[X], (this) power and TP.

2 hours ago, OmerB said:

Can you make it that the x1 fight will work the same (not changing the screen, and retaining the order that I chose)?

I guess this is impossible (or not easily possible, at least), as the x1 and x2/x3 fights are not managed by the script but just calls to the server. And the game itself shortens x2/x3 fights to show the results immediately, while it forcefully redirects one to the battle screen to show the full fight for x1. But MM will know better. I guess this could be done, at least:

  • Automatically skip the fight when doing x1 performances. But I fear that this may fall below automation and hence might not be allowed.
  • Save sorting status and automatically recover it whenever the league table is loaded. This would be very nice in any case, as currently one always need to do 2 clicks to sort for weakest enemies at the top.
  • Like 1
  • Thinking 1
Link to comment
Share on other sites

  • Moderator
6 hours ago, 430i said:

I think there is a bug in zoo's script - when I click on the energy bar, I am redirected to the first side quest and not to the main story. Anyone else having this issue? I recently reached the last world (but I am far from the last scroll), maybe it's related to that 🤔?

6 hours ago, Bobick said:

Yes, me. At least sometimes. I couldn't figure out yet in which cases.

For me, it has always worked exactly this way (for like forever):

  1. As long as I'm working on the latest main adventure scroll, whenever I click on the energy bar, it takes me right there.
  2. Immediately after I complete the latest main adventure scroll, the next time I click on the energy bar, it takes me to Side Quests.
  3. Since I've long finished all of the Side Quests, after this one time, whenever I click on the energy bar again, it takes me right to the Champions' Reception Desk to buy tickets instead.
  4. As soon as the next new main adventure scroll appears, clicking on the energy bar takes me there instead. Rinse and repeat.

It's a very logical, predictable and useful cycle.

Are you guys taken to Side Quests even though you still have progress to make on the main story? Did you finish all the Side Quests or do you have any left? Does it happen consistently, every time? Is this something that started recently? I'm very surprised and confused about this.

7 hours ago, OmerB said:

I can see the E[x] in each player window, so I don't need to have a column for it)

To each their own. Having a column allows me to actually sort opponents by the sim results, which is immensely helpful. And as I've already explained, I have no use for any "Power" or "TP" column whatsoever. To each their own, though, and if we can get the option to pick and choose which column(s) we prefer to have, I'm all for it. I'm just not sure how much extra work it would be for Zoo (or Tom for those using the OCD version of HH++) and/or MM and 430i to adjust to HH++.

6 hours ago, Ravi-Sama said:

I love the E[X] column.  It makes it so simple.  Wish it existed in the old format.  It's basically what I already did w/ a spreadsheet before, but no longer have to.

Almost exactly this, except that to be very clear, as of this writing I no longer have an "E[X]" column and I assume what you're referring to, in which case I couldn't agree more with you, is instead the one currently called "Sim Results":

image.png

That's basically all I really need. Boosters, team color tags and other indicators are nice complements, but "Sim Results" is the one most important column for me.

That, and @430i's "best past performances" indicator, to be honest. I've always looked for this information manually in the past, for most opponents, and even with this I still go and check some opponents' profiles to see stuff like their MG etc. but the one most important thing I've always looked for was whether they've won or had lots of top 4s in the past. Having a visual reminder of it is invaluable  for me (after I've opened their profile once, to be very clear: it's not automation or anything the ToS would be against).

7 hours ago, OmerB said:

@-MM- When I'm doing one challenge at a time (which I prefer), the screen goes to the fight scene, and then comes back to the league screen, with the default order (regardless of how I ordered it before the fight). When I'm doing x3 fights at a time, the screen doesn't change, nor the order. Can you make it that the x1 fight will work the same (not changing the screen, and retaining the order that I chose)?

+1. This can be particularly annoying because the sorting gets lost every single time you move out of the main Leagues page, which gives a strong incentive to use the x3 option, but at the same time when doing that you only see your points total for the 3 fights, and you need to reload the Leagues page (and change the incredibly useful filter options Zoo added) if you really want to know how many points you've scored in each of the three fights.

Link to comment
Share on other sites

I'm not going to bring back the power column, because 1. the power value is a very arbitrary and misleading rating and 2. I'm not got going to try adding a new column into the table.

As for team power I have an idea

image.png.19e8ed2a53dfed916660844dc95c5df2.png

For the sorting option not being saved, I'm not going to do anything about that for now as there is still chance KK will fix it themselves. Also I'll look into that issue with the current main quest link. I didn't have time to look into it before, but I saw shal mention that it was broken in the game it self too.

image.png.75bbd909ae3ce625ef28efb4eb652656.png

  • Like 1
  • Thanks 5
Link to comment
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...