Jump to content

Community Scripts [Links in OP]


DvDivXXX
 Share

Recommended Posts

I was using my phone more often lately and discovered a few things, that might relate to the script.

When I use the regular Firefox, all the images are blured.
Screenshot_20230831-100358.thumb.png.7c0a2e3bac816fea57ca98a895cebb6f.png Screenshot_20230830-113241.thumb.png.b5c099d28d44a9ebd36c057fe1e66d0b.png Screenshot_20230830-113436.thumb.png.6f6f3e372eb8922af864354078274e47.png Screenshot_20230830-113323.thumb.png.48ed9f018ce02351d8197d15ddc45561.png

Turning the script of resolves this problem, but then I could use the app instead.

Interestingly when using Firefox Nightly, everything is ok.
But I found a few other things there.

Screenshot_20230901-101624.thumb.png.2b514bb8ebffe26d9b06c1bfe577bbb6.png

The Game links are partially hidden by the HH++ Logo again, that was fixed some time ago.
 

Screenshot_20230901-103327.thumb.png.db3e41abe1a7e2229922cafbeca58529.png

The Link to Harem Stats is right above the (very big) collect all Button, maybe that one could be made smaller to make some room for the Stats.

 

And will someone please tell me, where I can find F5 on my Phone? 🙄

Screenshot_20230825-092551.thumb.png.a5f8367151621af87c554ffb38506b1d.png

Not related to any script, but this is what you get using the new App, when there is very bad internet somewhere outside on the Countryside. 😁

Screenshot_20230825-000929.thumb.png.34c785d8b32b0d761f970faaf021a72b.png   Screenshot_20230825-092405.thumb.png.d12c1b4a138207505215f4faedcc6f0d.png

Link to comment
Share on other sites

Talking of the NSFW filter, there are a few images that get through it -

1. In the offers chest - the starter offer on the Starter Packs tab, and also the Koban Packs images. The starter offer one is particularly annoying because it loads itself without clicking on anything.

2. Love Bunny in the Hero Level Rewards screen.

@zoopokemon Thanks for all you do. When you get some time, could you have a look at including these, please?

 

Edit: Thanks!

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

@zoopokemon When I'm selecting a girl, and I'm in the screen where I can give her XP, items, etc. The name of the girl is nearly invisible.

It's (the girls name) in very light colour, and slightly too above the dark menu screen, where I can see the girl and my items. So the girl's name blends into the general theme baxkground (beacause it's shown a bit too high), instead of being contrasted by the dark background of the 'give items' menu.

Link to comment
Share on other sites

Still have them all on HH.

image.png.d1a2ef1b792deca6462edf8ede0937c9.png

Did the browser you are using saw the contest at any point before its end (did you play like on your phone for a whole day) ?

Or maybe localStorage cleared/lost since the end of the event.

Edited by mdnoria
Link to comment
Share on other sites

On 9/4/2023 at 1:55 PM, Master-17 said:

maybe it switched off

No it didn't, and the counter was shown yesterday. But they aren't shown on my phone either, neither in Firefox or FF-Nightly, so it can't be just local.

And it is only for HH, CxH is alright on PC and in both Browsers on Phone.
Other games on PC as well, didn't check them on Phone.

 

---------------

Counter is back, whatever happend it's alright again.

------

Now the Counter is gone again in HH and in CxH also.
PsH and TransPSH still have them, do I have to understand this?

Edited by Der DinX
Link to comment
Share on other sites

@zoopokemon

With today's update, tutorial prompts (Bunny explaining things) were added or reset for players. In the league, those get stuck until I disable HH++ (disabling L++ did not help). I hope someone is able to replicate. Strangely on test server the tutorial went through. On live server it stuck when Bunny should highlight the booster indicators. Instead the page just stood greyed out (unusable) but no Bunny/highlight/button to continue the tutorial was shown. I recognised it first on mobile with the PWA via Firefox mobile, then I switched to PC with Opera desktop and faced the same, so it is not client specific at least.

Link to comment
Share on other sites

2 hours ago, Der DinX said:

Had some of these too, as mentioned in Chit Chat, but no problems in just clicking them away.

Another thing, the new function of hovering the Frontpage girl makes it impossible to move her pose.
It has to be disabled first, to edit.

How do you disable this hovering? I don't like this, and I can't seem to find out how to turn it off

Link to comment
Share on other sites

  • Moderator
11 hours ago, Der DinX said:

Had some of these too, as mentioned in Chit Chat, but no problems in just clicking them away.

Another thing, the new function of hovering the Frontpage girl makes it impossible to move her pose.
It has to be disabled first, to edit.

Yeah, this bugs me, b/c I recently had to clear the game's cache in the past week, which removes the fave girls.  I know there's a way to save it, but didn't bother.  I went through reselecting them manually, but didn't adjust the pose for each, since that's time consuming.  Some hover off-screen, which is kinda funny.

On 9/3/2023 at 9:15 PM, Ravi-Sama said:

I think it'd be great if we could filter for girls w/ upgraded skills, to make it easier to move them around weekly, but that's probably something KK should do, w/ the harem's filter system.

On 9/4/2023 at 6:44 AM, HentaiGuru said:

Any chance to have Harem Skills filter in a near future? I don't think KK will add it faster than our community script providers 

I second this.  I had deja vu about it, when I read your comment, but I just realized that I mentioned it in the leagues++ thread instead.

  • Like 1
Link to comment
Share on other sites

@zoopokemon

Many thanks for your champion drop recorder. At first I was confused about drops vs bulbs:

image.thumb.png.0c534f3bf705fa1e088d70700db2e0eb.png

Comparing the absolute numbers with the percentages, it turns out that 2 legendary and 2 rare bulbs mean 1 x2 drop each. It looks a little strange with 1 mythic and 2 other rarities having the same percentages. Probably it could be made clearer to avoid confusion: What about always showing the number of drops instead of the number of bulbs, so that same numbers always share same percentages, and then instead adding a little "x2" to the non-mythic bulb icons at the left side.

  • Like 1
Link to comment
Share on other sites

I made a small change to the script, here is the updated version and what has changed:

  • [new] Show the number of invested light bulbs for every player in the league table. Highlight the number if the player has put in even a single point in the fifth skill for the first girl. The number is not really correct, as the game does not report the amount of points put into the first skill, but a good approximation. The column is not sortable. Can be disabled via settings.
  • [updated] Optionally show the girl power and the new "aggregated" power in the league table. Disabled per default. Can be enabled via settings. Not sortable.
  • 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.

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

 

Spoiler
// ==UserScript==
// @name            Hentai Heroes+++
// @description     Few QoL improvement and additions for competitive PvP players.
// @version         0.18.7
// @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: [
                {
                    key: 'girl_power',
                    label: 'Show girl power in the league table',
                    default: false
                },
                {
                    key: 'kinkoid_power',
                    label: 'Show the new power stat in the league table',
                    default: false
                },
                {
                    key: 'number_of_bulbs',
                    label: 'Show the number of invested bulbs in the league table',
                    default: true
                },
                {
                    key: 'load_player_data',
                    label: 'Load player data on league table row click',
                    default: true
                },
            ],
        }
        super({name: baseKey, configSchema})

        this.all_new_columns = ['kinkoid_power', 'girl_power', 'number_of_bulbs'];
        this.anchor_column = 'power';
    }

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

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

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

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

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

        this.hasRun = true;
    }

    extendLeagueDataModel() {
        const players_data = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {};

        // add power to the existing `opponents_list` data model
        for (var r = 0, n = opponents_list.length; r < n; r++) {
            const player = opponents_list[r];
            const id = parseInt(player.player.id_fighter);

            const player_data = players_data[id];
            const best_placement = player_data != undefined ? player_data.best_placement : -1;
            const placement_count = player_data != undefined ? player_data.placement_count : -1;

            player.best_placement = best_placement;
            player.placement_count = placement_count;

            player.kinkoid_power = number_reduce(player.player.team.power_display);
            player.girl_power = player.player.team.total_power.toFixed();
            player.number_of_bulbs = player.player.team.girls.flatMap(g => Object.values(g.skill_tiers_info)).reduce((a,g)=>a+g.skill_points_used, 0)
        }
    }

    showAdditionalTableHeaders(config) {
        // Additional CSS classes
        const row_styles = {kinkoid_power: '2rem', girl_power: '2.2rem', number_of_bulbs: '0.9rem'}
        for (const [clazz, min_width] of Object.entries(row_styles)) {
            // this.insertRule(`.league_table .data-row .head-column[column="${clazz}"] {display: flex; align-items: center; justify-content: center}`);
            this.insertRule(`.league_table .data-row .data-column[column="${clazz}"] {min-width: ${min_width}}`);
        }

        const columns = this.all_new_columns.filter(c => config[c]);

        const headers = {
            kinkoid_power: `<span>${GT.design.caracs_sum}</span>`,
            girl_power: `<span>${GT.design.total_power}</span>`, // <span class="upDownArrows_mix_icn">
            number_of_bulbs: '<span class="scrolls_legendary_icn"></span>',
        }

        $(`div.league_table div.head-row div.head-column[column=${this.anchor_column}]`).after(
            columns.map(c => `<div class="data-column head-column" column="${c}">${headers[c]}</div>`).join('')
        );

    }

    showAdditionalTableColumns(config) {
        const context = this;

        $('div.league_table')
            .find('div.body-row')
            .each(function(index) {
                const opponent = window.opponents_list[index];
                const columns = context.all_new_columns.filter(c => config[c]);

                $(this).find(`div.data-column[column=${context.anchor_column}]`).after(
                    columns.map(c => `<div class="data-column" column="${c}"><div class="${context.tableColumnClass(c, opponent)}">${opponent[c]}</div></div>`).join('')
                );
            })
    }

    tableColumnClass(column, opponent) {
        if (!column.includes('bulb')) {
            return '';
        }

        const clazz = 'matchRating matchRating-win-chance minus'; // or active_skills_icn

        const active_skills = opponent.player.team.girls[0].skill_tiers_info[5];
        return active_skills && active_skills.skill_points_used > 0 ? clazz : '';
    }

    showPlayersPlacementBadge(config) {
        if (!config.load_player_data) {
            return;
        }

        // 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(config) {
        if (!config.load_player_data) {
            return;
        }

        // Remove the go_pre_battle class to allow users to select the row (inspired by Leagues++)
        $('.league_table .data-column[column=can_fight] .go_pre_battle').removeClass('go_pre_battle');

        $('.league_table .body-row').on('click', function() {
            const element_nickname = $(this).find('.data-column[column=nickname] span.nickname')
            const player_id = element_nickname.attr('id-member');

            const opponent = window.opponents_list.find(x => parseInt(x.player.id_fighter) == player_id);
            if (opponent.best_placement != undefined) {
                return false;
            }

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

            return false;
        });
    }

    updatePlayerPlacementBadge(row, player_data) {
        const nicknameElement = row.find('div.data-column[column=nickname]');

        var badgeContainer = nicknameElement.find('.badge-container');
        if (!badgeContainer.length) {
            badgeContainer = $('<div class="badge-container" />').appendTo(nicknameElement);
        }

        // best placement indicator next to the nickname
        badgeContainer.html(this.createBestPlacementBadge(player_data));
    }

    createBestPlacementBadge(player) {
        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 5
Link to comment
Share on other sites

Club Chat++ v0.68 released

Added Nutaku support (HH, CxH, PsH, GH) 😄

Nutaku players had to do without it for a long time, but now you can also use Club Chat++ 😊

Direct Link: https://github.com/HH-GAME-MM/HH-Club-Chat-Plus-Plus/raw/main/HH-Club-Chat-Plus-Plus.user.js

 

I would be happy if a moderator can update the first post with: "Supported games: HH, CxH, PsH, GH and the Nutaku versions of the games"

Info for existing installations: You will most likely receive an update message from TamperMonkey

 

Edited by -MM-
  • Thanks 2
Link to comment
Share on other sites

  • 2 weeks later...
  • Moderator

This might be a big ask (or it might be a quick and dirty copy-paste, no idea, honestly) but since fighting regular champions is part of my routine again (and many other players' no doubt), I'm wondering if convenient left and right arrows to go from fighting one champ directly to fighting the next one could possibly be added to HH++? Basically the same thing as the script already has for PoPs.

  • Like 2
Link to comment
Share on other sites

Technically, it's relatively easy since it's "only" adding links to /champions/{current}-1, /champions/{current}+1. The harder part is probably in the UI part.https://www.hentaiheroes.com/champions/1https://www.hentaiheroes.com/champions/1https://www.hentaiheroes.com/champions/1https://www.hentaiheroes.com/champions/1https://www.hentaiheroes.com/champions/1https://www.hentaiheroes.com/champions/1

  • Thanks 1
Link to comment
Share on other sites

Il y a 2 heures, DvDivXXX a dit :

This might be a big ask (or it might be a quick and dirty copy-paste, no idea, honestly) but since fighting regular champions is part of my routine again (and many other players' no doubt), I'm wondering if convenient left and right arrows to go from fighting one champ directly to fighting the next one could possibly be added to HH++? Basically the same thing as the script already has for PoPs.

Something like that?

Hentai_Heroes_-_2023-09-24_00_56_54.png.7e490515e6ee8c39faedc7a763cf8c95.png

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

14 hours ago, DvDivXXX said:

left and right arrows to go from fighting one champ directly to fighting the next one

Still, using the Back button in the browser will be faster: because then, immediately after pressing the Perform button, you go to the list of champions, and do not return to the same champion. And if you don’t use return, you have to wait for the Skip button to appear, which takes quite a while.

  • Like 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...