Jump to content

Community Scripts [Links in OP]


DvDivXXX
 Share

Recommended Posts

  • Moderator
51 minutes ago, renalove said:

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

Thanks.  I added it to the original post.

The visible crit chance is impressive.  I always wanted that.

On 2/1/2022 at 7:00 AM, DvDivXXX said:

@renalove's Hentai Heroes Battle Simulator: It supports estimating the benefits of girl skills.
Overview: https://github.com/rena-jp/hh-battle-simulator
Direct Link: https://github.com/rena-jp/hh-battle-simulator/raw/main/hh-battle-simulator.user.js

 

  • Thanks 2
Link to comment
Share on other sites

@430i

The table is still not well aligned with your script active. The reason is that the newly added column have no fixed width, but it depends on the width of the contained text. The browser then justifies everything around it.

Something like this works:

.data-column[column="total_power"] {
    width: 20rem;
}
.data-column[column="expected_points"] {
    width: 14rem;
}

This covers the worst case of Power being "000000" and E[X] "00.00", as zeros are the largest characters. The width needs to be defined larger than the actual text is, as somehow a large area right to the actual text is implied. Not sure what the exact reason for this is (the purple hatched area spans the 20rem):

css.png.5a26058a7abdc951b0a53a351a545f7e.png

With MM's script, the width values need to be much larger, which must be related to the left side panel being shown as well when the table is expanded. But with the needed width to have the whole table aligned even with above worst case values, the Name column becomes too small and line breaks start, so that everything gets unaligned again. So without significantly reducing the width of other column, the text size, margins/padding, challenges/booster icons (e.g. boosters as small 2x2 grid, mobile unfriendly ...), and/or e.g. moving the "Challenge" column with the "Go" button into the side panel, it just does not fit.

This alignment stuff would be much easier with a real HTML table instead of each line being an individual div, but it probably has other downsides ...

Edited by Horsting
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

4 hours ago, Horsting said:

This covers the worst case of Power being "000000" and E[X] "00.00"

I don't know about the power (I doubt that it can be zero), but E[x] cannot be less than 3.00 - these are the minimum points that are given when losing. Plus, there is no point in adding zeros at the beginning of the value, and you can omit them at the end.

By the way, you can also reduce the size of the squares in the win/loss column, leaving the size of the text in them the same.

1.jpg.60d42ac6813d5e5f46ce1d8ce9c4320e.jpg

And make font size of stats bigger whithout much other edits

2.jpg.34ac8ae5db62859b6b837be292815803.jpg

Edited by Master-17
Link to comment
Share on other sites

13 hours ago, Master-17 said:

I don't know about the power (I doubt that it can be zero), but E[x] cannot be less than 3.00 - these are the minimum points that are given when losing. Plus, there is no point in adding zeros at the beginning of the value, and you can omit them at the end.

It is not about the lowest value, but about the biggest characters: "0" has the largest width with the ingame font, so it is the worst case in this regards. "20.00" or "200000" are truly possible values which come close to this width. Basically it is about given the columns a fixed width where all possible values fit inside, so that the grid arrangement does not move other fields of the line around and break the alignment by that.

Any way to reduce other column widths without negatively affecting their usability (also thinking about mobile phones) of course helps, when expended table and sidebar are both shown in parallel.

I would really like to understand the logic behind the fact that the cell width must include this large area to the right of it 🤔. Would be much easier if it was just the actual distance from left neighbour cell to right one minus margins (or how box-sizing is chosen).

Edited by Horsting
Link to comment
Share on other sites

HH++ BDSM has been updated to v1.37.1

image.thumb.png.7ec1e06973436e64c29109cc7e9d1dfe.png

I've replace the hide fought opponents with a more complex filter system to also include booster status and team theme. Also added is an option to pin the player row to keep it at the top.

Next would be to add the expired booster indicator like in 430i's script. Currently the booster filter only checks if a player is listed with no booster and not if it already expired, which I plan to improve. Also next would be to integrate most of MM's league script into HH++. After all of that I'll take a look at rena's sim, but if I can't get an E[X] out of it I'm not bothering adding it.

  • Like 4
  • Thanks 8
  • Hearts 4
  • GG 2
Link to comment
Share on other sites

That filter works great, and to me indeed looks like the best solution. Also I love to have my own table entry pinned to the top, many thanks!

4 hours ago, zoopokemon said:

Next would be to add the expired booster indicator like in 430i's script.

MM's script btw does this as well, giving them a red border like 430i's and fading them a little.

4 hours ago, zoopokemon said:

After all of that I'll take a look at rena's sim, but if I can't get an E[X] out of it I'm not bothering adding it.

It seems the code is a little too different from the HH++ battle simulator. It is probably easier to add the girls' tier 4 skill feature into the HH++ simulator instead of trying to merge in rena's whole simulator. As the "turns" is incremented and passed already, easiest is probably (after obtaining both overall attack bonuses) in playerAttack:

opponentHP -= attack.damageAmount * playerAtkBonus ** turns;
playerHP += attack.healAmount * playerAtkBonus ** turns;

And in opponentAttack:

playerHP -= attack.damageAmount * opponentAtkBonus ** turns;
opponentHP += attack.healAmount * opponentAtkBonus ** turns;

Probably rounding is additionally required. If the defence bonus shall be treated correctly as well (while no one should ever skill it), it becomes more complicated 🤔.

  • Like 2
Link to comment
Share on other sites

3 minutes ago, Horsting said:

It seems the code is a little too different from the HH++ battle simulator. It is probably easier to add the girls' tier 4 skill feature into the HH++ simulator instead of trying to merge in rena's whole simulator. As the "turns" is incremented and passed already, easiest is probably (after obtaining both overall attack bonuses) in playerAttack:

opponentHP -= attack.damageAmount * playerAtkBonus ** turns;
playerHP += attack.healAmount * playerAtkBonus ** turns;

And in opponentAttack:

playerHP -= attack.damageAmount * opponentAtkBonus ** turns;
opponentHP += attack.healAmount * opponentAtkBonus ** turns;

Probably rounding is additionally required. If the defence bonus shall be treated correctly as well (while no one should ever skill it), it becomes more complicated 🤔.

Well of course I'd be modifying the HH++ battle simulator based on rena's sim instead of just copying rena's sim. I did think for a bit that those modifications you mentioned would be all I would need to do, but damage isn't attack, it's attack - defense.

  • Like 2
Link to comment
Share on other sites

14 minutes ago, zoopokemon said:

but damage isn't attack, it's attack - defense.

Ah dammit you are right. So then the actual AP needs to be passed. Hmm, then the same can be done with the defence, and have the tier 4 defence penalty applied in the same turn, before calculating the damage(s) form it. But this is nasty, as the common and crit damages are currently calculated outside of the turn functions, which would then all need to be done within them. Uff, so pass AP, defence and a flag about whether it is a crit or not, then calculate damage and healing all within the attack function. Probably rena has a better approach to do it.

Link to comment
Share on other sites

@renalove's battle simulator just got league points prediction. Many thanks for this ❤️! And indeed it passes all relevant value through to the iterative attack functions, to calculate defence and damage values per round.

Ah lol, you can see it in @Ravi-Sama's screenshot above, nice timing 🤩.

Edited by Horsting
  • Like 3
Link to comment
Share on other sites

Rena's battle simulator takes the new girls' tier 4 skill into account and hence gives correct results, while HH++ currently does not. It is also interesting to compare the outcome of both scripts to see the effect of the skill on you and your opponent.

430i's script adds the E[X] values (the HH++ ones) and actual team power to the table, but it does not work well anymore with League++ after the last game update, and also the now functional sorting breaks it. I however like the idea to have E[X] values in the table, as replacement for the game's "Power" column, so I hope there is a way and motivation to pick this up and implement it (at least as option) into HH++ or League++, or having it as a compatible standalone feature of the app.

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

18 hours ago, Ravi-Sama said:

Don't know who's responsible.

You're welcome ;)

18 hours ago, DvDivXXX said:

To be clear, is there currently any benefit to install Rena's and/or 430i's script(s) on top of HH++ (BDSM) and MM's Leagues++?

Check the changelog below for the features added by my script.

--------

I had an hour or two, so I reworked the script to nicely work with all the other scripts and the base game

A list of the features:

 

  • Show your real team colors, team power and stats (zoo's version only 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.

  • Show girl power and themes (instead of the useless synergy popup).

  • Show the expected points for every opponent right in the leagues table.

  • Button to load the opponents' profile data (but not really show it) - this will display their best D3 results in the league table next to the player's name.

  • Highlight expired boosters (although who knows whether they really expired).

  • A check on the pre-battle page whether you have a suitable not-equipped mythic equipment in your inventory.

  • [updated] Compatibility with the base game and other scripts. The new numerical columns - girl power and expected points are now sortable. The layout is nicely aligned as well. The player avatar had to be hidden to make space for the new data.

Known issues:

  • [new] Not an issue, but rather a caveat. The script completely re-creates the league table. This means that any changes added to the game will be lost (for example new columns).
  • [new] The player row now shows 2 pins (from zoo's script), but I dont have the time to figure it out now.
  • 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
  • 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.17.0
// @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 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.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.reinitLeaguesTable();
                this.makeCompatibleWithLeaguePlusPlus();
            });

            $(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);
        }
    }

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

    // This is mostly copy-pasted from the native 'initLeaguesTable' method with a couple additional changes:
    //  - display best rankings next to the username in the 'name' column
    //  - display girl power in the 'power' column and not whatever crap Kinkoid came up with
    //  - display the theme instead of the synergy popup in the 'team' column
    reinitLeaguesTable() {
        // 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}');

        // Recreate the new settings struct with all additional goodies
        const settings = {
            sorting: true,
            sorting_callback: this.addSortCallback,
            column_display_names: {
                place: GT.design.Rank,
                nickname: GT.design.Name,
                team: GT.design.team,
                level: GT.design.Lvl,
                power: GT.design.caracs_sum,
                player_league_points: GT.design.leagues_points,
                match_history_sorting: GT.design.Challenges,
                boosters: GT.design.leagues_stats_and_boosters,
                expected_points: 'E[X]',
                can_fight: GT.design.challenge,
                reload: '↻'
            },
            visible_columns: [
                'place',
                'nickname',
                'level',
                'player_league_points',
                'power',
                'expected_points',
                'match_history_sorting',
                'team',
                'boosters',
                'can_fight',
            ],
            head_column_html: '<span>[column_value]<span class="upDownArrows_mix_icn"></span></span>',
            body_column_html: {
                nickname: this.createLeagueColumnNickname,
                level: (row) => row.player.level,
                power: (row) => row.player.team.total_power.toFixed(),
                team: this.createLeagueColumnTeam,
                player_league_points: (row) => number_reduce(row.player_league_points),
                match_history_sorting: this.createChallengesColumn,
                expected_points: (row) => row.expected_points.toFixed(2),
                boosters: this.createStatsAndBoostersColumn,
                can_fight: this.createChallengeButtonColumn,
                reload: this.createReloadButtonColumn,
                // 'body_column_html' is passed as 'this' to all html functions,
                // so we need to include a couple of utility functions in here
                canFight: this.canFight,
                createBestPlacementBadge: this.createBestPlacementBadge,
            }
        };

        const isReloadVisible = opponents_list.some(o => o.best_placement == undefined);
        if (isReloadVisible) {
            settings.visible_columns.push('reload');
        }

        initDataList(opponents_list, '#leagues .league_table', settings);

        $('#leagues .league_table').getNiceScroll().remove();
        $('#leagues .league_table').niceScroll({horizrailenabled: false});
        this.addSortCallback();

        $('.league_table .body-row .data-column[column="reload"] button').on('click', function() {
            const player_id = $(this).attr('id-member');

            window.$.post({
                url: '/ajax.php',
                data: {
                    action: 'fetch_hero',
                    id: 'profile',
                    preview: false,
                    player_id: parseInt(player_id),
                },
                success: (data) => {}
            })
        });

        // Re-trigger a refiltering of the leagues table in zoo's script
        $(document).trigger('league:table-sorted');
    }

    addSortCallback() {
        // Native callback implementation
        $(`.data-list [id-member="${Hero.infos.id}"]`).parent().parent().addClass('player-row')

        // Re-trigger a refiltering of the leagues table in zoo's script
        $(document).trigger('league:table-sorted');
    }

    // Native method, beatified
    canFight(oponent_data) {
        if (Hero.infos.id == oponent_data.player.id_fighter || !oponent_data.hasOwnProperty('match_history')) {
            return false
        }

        return oponent_data.match_history[oponent_data.player.id_fighter].some((battle) => !battle);
    }

    createBestPlacementBadge(row) {
        if (row.best_placement == 1) {
            return `<span class="best-placement"><span class="scriptLeagueInfoIcon top1"></span>${row.placement_count}</span>`;
        } else if (row.best_placement >= 2 && row.best_placement <= 4) {
            return `<span class="best-placement"><span class="scriptLeagueInfoIcon top4"></span>${row.placement_count}</span>`;
        }

        return '';
    }

    // Native method, bodified to include best placement badge
    createLeagueColumnNickname(row) {
        // best placement indicator next to the nickname
        const placement_badge = this.createBestPlacementBadge(row);

        return `<div class="square-avatar-wrapper" ><img src="${row.player.ico}"/></div><span class="country country-${row.country.toLowerCase()}" tooltip hh_title="${row.country_text}"></span><span class="nickname" id-member="${row.player.id_fighter}">${row.player.nickname}</span>${placement_badge}`;
    }

    createLeagueColumnTeam(row) {
        // show theme icons instead of syngergy tooltip
        const themes = (row.player.team.theme || "balanced").split(',');
        const theme_icons = themes.map(t => `<span class="${t}_element_icn ${t}_theme_icn"></span>`);
        return theme_icons.join('');
    }

    // Native method, beatified
    createChallengesColumn(row_data) {
        if (Hero.infos.id == row_data.player.id_fighter || !row_data.hasOwnProperty('match_history')) {
            return '-';
        }

        return row_data.match_history[row_data.player.id_fighter]
                .map(challenge => {
                    const challenge_class = challenge ? challenge.attacker_won : '';
                    const challenge_text = challenge ? challenge.match_points : '';
                    return '<div class="result ' + challenge_class + '">' + challenge_text + '</div>';
                })
                .join('');
    }

    // Native method, beatified
    createStatsAndBoostersColumn(row_data) {
        const is_player = Hero.infos.id == row_data.player.id_fighter;

        const stats_html = buildPlayerStats(
            row_data.player.team.caracs.damage,
            row_data.player.team.caracs.defense,
            row_data.player.team.caracs.ego,
            row_data.player.team.caracs.chance,
            is_player,
            true,
        );

        const boosters_html = row_data.boosters.map((booster) => newReward.slot(booster, 'xs')).join('');

        return stats_html + '<div class="boosters">' + boosters_html + '</div>'
    }

    // Native method, beatified
    createChallengeButtonColumn(row_data) {
        return this.canFight(row_data) ? '<a href="/leagues-pre-battle.html?id_opponent=' + row_data.player.id_fighter + '" class="go_pre_battle blue_button_L">' + GT.design.daily_goals_go + '</a>' : '';
    }

    createReloadButtonColumn(row) {
        return row.best_placement == undefined ? `<button class="blue_button_L" id-member="${row.player.id_fighter}"></button>` : '';
    }

    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
        row.find('div.data-column[column=nickname]').append(this.createBestPlacementBadge(opponent));

        // remove reload button
        row.find('div.data-column[column=reload] button').remove();

        // remove reload column (optional)
        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() {
        HHPlusPlus.Helpers.doWhenSelectorAvailable('div#leagues div.league_buttons_block a.changeTeam', () => {
            // 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')}

    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)

 

 

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

HH++ BDSM has been update to v1.37.2

image.png.3fd4f1ac98ca44299d1db295dbeb985a.png

Added booster status indicators to leagues closely matching those used for the resource bar booster tracker. Also adjusted the boosted filter to not hide opponent's who's booster should have expired, of course whether they actual are boosted or not is still an issue.

Also I add the ability to sort the team column. Wanted to see if the booster column could also be sorted by duration left, but there is no easy way for that.

  • Thanks 11
Link to comment
Share on other sites

34 minutes ago, SpectralCalm said:

Who knows if it's a patch adds or some script made it that way? If it's a patch - it's a killer feature, very cool! Seeing when others will run out of boosters is incredibly useful

image.thumb.png.be937fe8fc3fb3cb900b146d300d2a6d.png

Oh sorry, this is HH++ BDSM

Mod Edit: @SpectralCalm Please visit your own forum profile page for a few forum instructions. No more double posts. Thanks. Div*

Edited by DvDivXXX
Mod edit: warning
Link to comment
Share on other sites

On 8/11/2023 at 1:03 PM, 430i said:
        // Re-trigger a refiltering of the leagues table in zoo's script
        $(document).trigger('league:table-sorted');

 

17 minutes ago, Der DinX said:

@430i this looks funny 😉

image.png.8789187dcf1db28cff671f6ea8da872a.png

It appears when the Leagues Site opens, and vanishes when the table is sorted.

After returning from a fight, it looks like this again.

Actually I think I was able to fix it, but I have only tested it on my phone, so beware. Just remove the two lines from the script that I have quoted above.

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

@430i

Thanks for your update. I think you recognised that after Zoo's last update, there are some things broken:

  • The button to load opponents' profile data works (again, it did not before) but brings you to the battle page, just like if you click on the row elsewhere. The following solves it:
            $('.league_table .body-row .data-column[column="reload"] button').on('click', function() {
                const player_id = $(this).attr('id-member');
    
                window.$.post({
                    url: '/ajax.php',
                    data: {
                        action: 'fetch_hero',
                        id: 'profile',
                        preview: false,
                        player_id: parseInt(player_id),
                    },
                    success: (data) => {}
                })
                return false  <------------------------ This one, to abort any following click action!
            });

    That the button hides, and the whole column after all profiles have been loaded, works (still) very well 👍.

  • The booster expiry indicators are somehow doubled (EDIT: As now shown by the others above 😊). As far as I can see, you do not add own expiry indicators anymore, since Zoo did so now? Probably it is a timing between both script, when the table is re-created triggering the Zoo's expiry indicator to be added twice. It fixes itself when sorting the table by anything.
  • I see you added a league table download button and a clear button. To find out what the first is for is trivial, thanks for this :). But what does the second do? Probably some tooltip with a sentence about what they do would help, and maybe giving them some border, or the blue_button_L class. This looks nice (while if other buttons are added, like with MM's League++, it makes sense to align the sizes):
        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>`
        }

And two suggestions:

  • You remove the "new" overall power column and add the team power column and expected points. Actually, I find the overall power value more valuable than the team power, as it is derived from the final opponent stats, and not from the girls power only, being much closer to the actual opponent difficulty. But as you add E[X], you add an even better indicator about the opponents difficulty, the best to be true. So to relax the problem with too many columns taking space, I suggest to just remove the team power column and have E[X] just as successor of the overall power column. This is especially helpful when using League++ as well, since then the opponent side panel leaves less space for the table, so that opponent names are mostly hidden, especially when the button to load player info is still present.
  • What do you think about one button to load all player profiles at once? It will surely take a while, not sure if there is a nice and simple way to show a carrot until everything has been loaded, but currently there is quite much manual action to do to benefit from this feature, and until done the reduce width for other table elements.
Edited by Horsting
  • Like 3
  • Thanks 1
Link to comment
Share on other sites

@Horsting Thanks a lot for the feedback!

I will take a look in the evening at the load button redirection and will release a new version with the other fixes. Thanks for the suggestion how to fix it!

My booster highlight was indeed removed, but I forgot to mention it in the change log. The problem with the doubled pins and boosters was caused by erroneously calling zoo's script twice, see my comment to @Der DinX above for a quick fix, which will be included in the next version.

The league download button behaves a bit differently from zoo's. Zoo's download button creates a snapshot of the current league state that you can download, but I don't remember the format. My script checks the league table every time you open the page and if there has been any change (even by a single point) compared to last time you opened the page, it will create a snapshot and store it in the browser's local storage. When you click download, all snapshots will be converted to a single CSV file that you can easily open in excel or evaluate programmatically. However depending on how often the table changes and how often you open it, the snapshots might blow up your local storage, so there is the option to clear it. I will think of making them prettier or maybe moving them elsewhere, as there is very little real estate in the bottom right corner, but probably not in this version. There is a second download button with the snapshots from the last league, but it might be buggy at the moment, as I haven't tested it.

Your comment regarding the different power columns is reasonable and a good compromise. I might make it configurable at the end, so everyone can decide which power column should be shown, if any.

Your last suggestion about loading the player profiles, I think might be borderline automation. I agree that the current one is a bit clunky to use, but it should be relatively safe. However I think there is still room for improvement, especially when used with League++ or the upcoming version from zoo, where L++ is integrated. The way to do it would be to load the profile when selecting a player row in the table. That's the same way the booster detection script worked, so it should be fairly safe from automation point if view. It would still require manual effort - selecting all opponents one by one, but at least it will remove the need for the reload column.

Feedback on all points would be very welcome!

  • Like 3
  • Hearts 1
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...