Jump to content

Community Scripts [Links in OP]


DvDivXXX
 Share

Recommended Posts

Yeah sorry about that guys. It really depends on how often you reload the page - some people might never experience the problem, while others might fill up their local storage in just 2-3 days. Ironically the new point estimation makes it worse, as it encourages you to probably refresh more often than previously.

I really dont have a solution, I will need to research the limitation about the local storage and whether there are other places to store some data. I looked into using unsent support tickets for that, but even those are visible to kinkoid and replied to, so that idea got shut pretty quickly. The only other solution that I can offer for now is just an advice to delete the previous league data as soon as the league ends and (optionally) you have downloaded the data. And of course, maybe dont refresh the page that often :D From experience the high-level players really lose more than 100 points in a week, let alone more than 25 points within few hours, so the points tracking should be fairly precise for the top10 with only few reloads per day.

I will need to look into reducing the memory usage, but I have one or two ideas for new features, which would actually need more data, not less.

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

Why not reorganize the data before saving it ?

In each snapshot you store the player id+name+country+level+...

Why not save all player data once and for all and just referring to them with their id in each snapshot ?

Same for the league_end date which is stored in each snapshot when it does not change throughout the week.

I suppose it's because you are saving the data as returned by the kinkoid api but you could gain a lot by doing these things for the price of some cpu time.

After that, the overkill solution would be to add a zip library to zip everything.

Edited by mdnoria
Link to comment
Share on other sites

11 minutes ago, mdnoria said:

Why not reorganize the data before saving it ?

In each snapshot you store the player id+name+country+level+...

Why not save all player data once and for all and just referring to them with their id in each snapshot ?

Same for the league_end date which is stored in each snapshot when it does not change throughout the week.

Yeah that's what I am planning to do. It started as a private script, so I didnt gave much thought about memory usage and just did it the easiest way possible, but now optimization is unavoidable. I will try to whip out a new version today or tomorrow. In the meantime, delete the previous league data, either via the UI, or if the local storage is already full - delete the "HHPlusPlusPlus.League.Snapshot.Previous" local storage key.

It seems there is a full fledged database support in the browser, which has very little limitations, so if everything else fails, this might be another way to solve it. But that requires a lot more work, so lets hope that the upcoming optimization will alleviate the problem.

  • Thanks 1
  • uwu 1
Link to comment
Share on other sites

Latest League++ will make things better, as x1 battles do not forcefully imply a page reload. I never reloaded the page just for a data snapshot, but mostly implied when searching for and after battling and opponent, or when checking back for opponents after their booster expired, which usually required another 2-5 revisits before the "really" expired (just had a case which took 1.5h before its stats finally dropped 😞).

32 minutes ago, 430i said:

It seems there is a full fledged database support in the browser, which has very little limitations, so if everything else fails, this might be another way to solve it. But that requires a lot more work, so lets hope that the upcoming optimization will alleviate the problem.

Indeed, this seems to be the right thing for larger data sets 😃https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB

  • Like 2
Link to comment
Share on other sites

As you may have noticed, several features have been added to the script. Most of them are slow and are disabled by default. You can enable them in the config if you need to.

image.thumb.png.7417bfdf2a91f1e316b07f402d668d1f.png

Booster and skill simulators simulate your booster and skill combinations. Not your opponent's. The skill simulator only changes tier 5 skill, tier 3 and 4 skills are not changed. The simulation is started by clicking on the icon next to your team hexagon.

image.thumb.jpeg.c1261ca1db9d69bbd07e722e18bf4272.jpeg

Clicking on the AME icon in your row on the league table will simulate booster combinations for all players.

image.jpeg.2a1a181d814d4b3b5c99fff368eb338f.jpeg

Added feature to show the probability of each league result. This is usually fast but sometimes slow.

image.png.27836bb0a01f28c11ae659020c99a1e0.png

Script
https://github.com/rena-jp/hh-battle-simulator-v4/raw/main/dist/hh-battle-simulator-v4.user.js

GitHub
https://github.com/rena-jp/hh-battle-simulator-v4

  • Thanks 4
  • Hearts 2
  • uwu 1
Link to comment
Share on other sites

23 minutes ago, renalove said:

Clicking on the AME icon in your row on the league table will simulate booster combinations for all players.

Woaaah, in an anime, I would be bleeding from my nose, you added the last two features I was hoping for: The probability for each result (I recognised it the last days), and now the booster simulator for the whole league. Now we have really all we need to know to do the best founded booster decision. In my case, with shield skill, my results are pretty similar to yours, with 3x Cordyceps + 1x Chlorella being the best combination, with and without AME, and generally confirming Cordyceps > Chlorella > Ginseng > Jujubes. Also 1x Cordyceps + 3x Chlorella being the best default setup as F2P (possible to keep up without buying Cordyceps):

image.thumb.png.1aaf1830aa8ae3a118e4b6d784153d44.png

  • Like 1
Link to comment
Share on other sites

 

Did I find a bug or does anyone have an explanation for this?

image.thumb.png.40abe087093b9861e141e787a5d9f5cb.png

Above my main team, below I was just playing around against one opponent, as I was hoping to find a team to beat him with E[P]>=24.95 without A/LM.

  • Above, higher AP, ego and defence, higher damage and healing, slightly less damage and healing from opponent, identical harmony and crit changes.
  • Below, one dark L5 switched for yellow M6. Yellow bonus reduces opponents defence from 44,453 to 43,523, but due to much lower AP, my damage is still lower (of course, alone dark>yellow is trivially well known), and also the higher GS4 bonus does lead to higher damage in any possible amount of rounds.
  • The opponent has no GS5, me max shield.
  • The higher stats in above case are because of RE incl. resonance bonuses, matching GS3 (all girls but Bunna have blue eyes) and the dark AP bonus.

So how the hack can I have identical E[P] in both cases?? The individual results table has identical chance for 25 points (94.53%, AFAIR), but the other chances are differently distributed, and in bottom case 1 point less is possible (with very low chance, of course). Also, in the league table, bottom team gives much less points/avg in the sim, and against a few individual other opponents I checked, so the data used did definitely change. I also navigated a lot back and forth to assure the script has gathered all needed current data.

EDIT: Ah, I should stop thinking too much too late at night. Simple: In all relevant possible outcomes, which do not result in 25 points already, the additional AP are not enough to end the fight one round earlier. And since the opponent does nearly the same amount of damage, and the ego is nearly identical as well in both cases, I end in the same ego %/points bracket. But it is rare to see zero impact of offensive stats on E[P], due to the many different possible courses of the battle, where usually at least some end earlier with higher damage, hence more remaining ego => points.

Edited by Horsting
  • uwu 1
Link to comment
Share on other sites

@Tom208

I'm having a compatibility problem between ocd and other scripts.
Given that I normally use these scripts at the same time on Chrome:
- OCD
- BDSM
- Heroes++
- Leagues+++
- Battle Simulator v4
For a while now in the league the Change Team button covers my stats and the season expiration timer covers the refill button.
This happens if I have OCD enabled, even disabling all keys in the script settings.
The same thing happens using my android smartphone with firefox.
Especially on the smartphone this is very annoying because I can't press the refill button.

All scripts enabled except ocd:

ScreenshotLegasenzaOCD.thumb.png.0836ef27ba9fab152b6d8e38cae5bd5e.png

With ocd enabled even with all features disabled:

ScreenshotLegaconOCD.thumb.png.5f561433b1b23fb1c230178101c8ee25.png

 

 

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

Jep, for the same reason I stopped using it: It works well when using HH++ OCD as main script and just some individual features from BDSM, but the other way round it causes some glitches. Some (conflicting) style tweaks are applied regardless how you set the toggles, respectively not all things do have a toggle. For best compatibility, it would be great it really everything the script does was behind a setting.

  • Like 1
Link to comment
Share on other sites

Il y a 4 heures, Miccia a dit :

@Tom208

I'm having a compatibility problem between ocd and other scripts.
Given that I normally use these scripts at the same time on Chrome:
- OCD
- BDSM
- Heroes++
- Leagues+++
- Battle Simulator v4
For a while now in the league the Change Team button covers my stats and the season expiration timer covers the refill button.
This happens if I have OCD enabled, even disabling all keys in the script settings.
The same thing happens using my android smartphone with firefox.
Especially on the smartphone this is very annoying because I can't press the refill button.

All scripts enabled except ocd:

ScreenshotLegasenzaOCD.thumb.png.0836ef27ba9fab152b6d8e38cae5bd5e.png

With ocd enabled even with all features disabled:

ScreenshotLegaconOCD.thumb.png.5f561433b1b23fb1c230178101c8ee25.png

 

 

Indeed, the CSS style for league was at the wrong place in the script, so even with the league feature disabled, it applies.
I've just updated the script, so the problem should have disappeared.

 

il y a 53 minutes, Horsting a dit :

For best compatibility, it would be great it really everything the script does was behind a setting.

Normally, there is no need for more options (and I wish to keep the menu more compact). But if you see some issues, please let me know. 😉

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

[NOTE FOR COMPETITIVE PLAYERS]

Due to a necessary optimization of the internal data used by the script, a migration step was necessary. The script will perform it automatically, so no manual step is needed. However I could only test it once with my own data, and while it worked, I cannot guarantee that it will work for everyone. As such you might have to clear the previously stored league data, which will also clear the lost points estimation. I would recommend writing the lost points before installing the latest version just in case.

 

Here is the updated script version and what has changed:

  • [new] Detect whether boosters have really expired.
  • [updated] Guesstimate the minimum number of lost points per user. For this to work the league data gather module needs to be enabled (it is per default). The number of lost points is shown when hovering over the points for a given opponent. The estimated value is the minimum number of lost points, but not necessarily the real value. For example if a player has gained 25 points since the last league table refresh, the script will assume that the player has not lost any points, while this might not be the case (e.g. by achieving 10, 10, 5 pts). Updated to include data from the first snapshot, thanks to @Horsting.
  • 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 column is not sortable. Can be disabled via settings.
  • 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. Data might be stale, as it is loaded once and is stored across leagues. 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.

  • [updated] 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. Optimized the data structure to reduce memory usage, however it might still fill up your browser's local storage.

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

Fixed upstream issues:

  • The number of lightbulbs should now be accurate, as the game now takes the bulbs invested in tier 1 skills into account.

Known issues:

  • Storing a lot of snapshots of the league data might fill up your browser's local storage, which is shared between all scripts. Thus it's recommended to clear the data from the previous league when it ends and do not reload the league page needlessly.
  • The opponents best placement data (aka placement badge) might be stale, as the data is only loaded once per player and is saved across leagues.
Spoiler
// ==UserScript==
// @name            Hentai Heroes+++
// @description     Few QoL improvement and additions for competitive PvP players.
// @version         0.18.18
// @match           https://*.hentaiheroes.com/*
// @match           https://nutaku.haremheroes.com/*
// @match           https://*.transpornstarharem.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 = LeagueScoutModule.getCurrent();

            LeagueScoutModule.setPrevious(data);
            LeagueScoutModule.deleteCurrent();
        })

        HHPlusPlus.Helpers.defer(() => {
            // read and store data
            this.storeSnapshot(this.readSnapshot());

            // create ui elements
            HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => {
                const parent = $('div.league_buttons');
                this.createDownloadButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY, PATH_GROUPS);
                this.createClearButton(parent, PREVIOUS_LEAGUE_SNAPSHOT_KEY);
                this.createDownloadButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY, PATH_GROUP);
                this.createClearButton(parent, CURRENT_LEAGUE_SNAPSHOT_KEY);
            });
        });

        this.hasRun = true;
    }

    readPlayerData() {
        const data = {};

        for (var r = 0, n = window.opponents_list.length; r < n; r++) {
            const player = window.opponents_list[r];

            const id = parseInt(player.player.id_fighter);
            const name = player.player.nickname;
            const country = player.country;
            const level = parseInt(player.player.level);

            const entry = {id, name, country, level};
            if (Object.values(entry).some(x => x == undefined || (typeof x !== 'string' && !Array.isArray(x) && isNaN(x)))) {
                console.log('Some player data is missing, maybe the opponents_list data structure changed?');
                console.log(entry);
            }

            data[id] = {name, country, level};
        }

        return data;
    }

    readSnapshot() {
        const data = [];

        for (var r = 0, n = window.opponents_list.length; r < n; r++) {
            const player = window.opponents_list[r];

            const id = parseInt(player.player.id_fighter);
            const rank = player.place;
            const points = parseInt(player.player_league_points);
            const elements = player.player.team.theme;

            const damage = player.player.damage;
            const defense = player.player.defense;
            const ego = player.player.remaining_ego;
            const chance = player.player.chance;

            // Take only the first two chars of the booster names, as those should be unique. Assume all are legendary.
            const boosters = player.boosters.filter(b => b.expiration > 0).map(b => b.item.name.slice(0, 2))

            // Create player snapshot and validate it.
            const entry = {id, rank, elements, points, damage, defense, ego, chance, boosters};
            if (Object.values(entry).some(x => x == undefined || (typeof x !== 'string' && !Array.isArray(x) && isNaN(x)))) {
                console.log('Some player data is missing, maybe the opponents_list data structure changed?');
                console.log(entry);
            }

            data.push(entry);
        }

        // Sort the parsed data by rank.
        data.sort((a, b) => a.rank > b.rank);

        return data;
    }

    storeSnapshot(snapshot_data) {
        var data = LeagueScoutModule.getCurrent();

        const current_date = new Date(window.server_now_ts * 1000);
        const league_end_date = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000);

        // Create the initial container data structure
        if (Object.keys(data).length == 0) {
            data = {
                league_end: league_end_date,
                num_players: data.length,
                player_data: this.readPlayerData(),
                snapshots: [],
            }
        }

        const snapshot = {
            date: current_date,
            snapshot: snapshot_data,
        }

        if (data.snapshots.length && JSON.stringify(data.snapshots[data.snapshots.length - 1].snapshot) === JSON.stringify(snapshot.snapshot)) {
            return;
        }

        data.snapshots.push(snapshot);
        LeagueScoutModule.setCurrent(data);
    }

    createButton(id, path) {
        return `<svg id="${id}" class="blue_button_L" width="32" height="32" style="padding: 5px" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="#FFFFFF"><g><rect fill="none" height="24" width="24"/></g><g>${path}</g></svg>`
    }

    createDownloadButton(parent, what, icon) {
        if (!storage.getItem(what)) {
            return;
        }

        const friendlyId = what.toLowerCase().replaceAll(".", "-");
        const buttonId = `download-${friendlyId}`;

        const downloadButton = this.createButton(buttonId, icon);
        parent.append(downloadButton);

        $(document.body).on('click', `#${buttonId}`, () => {
            const data = LeagueScoutModule.get(what);

            const separator = ","
            const columns = ["date", "player_id", "player_name", "player_rank", "player_points"];
            const values = data.snapshots.flatMap((e) => e.snapshot.map((p) => [e.date, p.id, data.player_data[p.id].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);
        });
    }

    static getCurrent() {
        return LeagueScoutModule.get(CURRENT_LEAGUE_SNAPSHOT_KEY)
    }

    static get(key) {
        var data = JSON.parse(storage.getItem(key)) || {};

        // Migrate from the old data structure
        if (Array.isArray(data) && data.length > 0) {
            const last_snapshot = data[data.length - 1];

            const reduceP = ({id, name, country, level}) => ({id, name, country, level});
            const reduceS = ({id, rank, elements, points, damage, defense, ego, chance, boosters}) => ({id, rank, elements, points, damage, defense, ego, chance, boosters});

            const player_data = last_snapshot.player_data.map(reduceP).reduce((map, obj) => {
                map[obj.id] = obj;
                return map;
            }, {});

            const snapshots = data.map(d => ({date: d.date, snapshot: d.player_data.map(reduceS)}));

            data = {
                league_end: last_snapshot.league_end,
                num_players: last_snapshot.num_players,
                player_data,
                snapshots,
            }
        }

        return data;
    }

    static deleteCurrent() {
        storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY);
    }

    static setCurrent(data) {
        storage.setItem(CURRENT_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data));
    }

    static setPrevious(data) {
        storage.setItem(PREVIOUS_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data));
    }
}

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.showMaxPointsTooltip();
                this.detectReallyExpiredBoosers();
                this.makeCompatibleWithLeaguePlusPlus();
            });

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

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

        this.hasRun = true;
    }

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

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

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

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

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

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

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

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

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

    }

    showAdditionalTableColumns(config) {
        this.insertRule(`.active_skill {color: red; text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000;}`);

        const context = this;

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

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

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

        const clazz = 'active_skill'; // or active_skills_icn

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

    showMaxPointsTooltip() {
        const data = LeagueScoutModule.getCurrent();

        if (!data || !data.snapshots.length) {
            return;
        }

        $('.league_table .body-row').each(function (idx) {
            const opponent = opponents_list[idx];
            const opponent_id = parseInt(opponent.player.id_fighter);

            const points = data.snapshots.map(d => d.snapshot.find(p => p.id == opponent_id).points);

            const remainder = points[0] % 25;
            var lost_points = (remainder == 0 ? 0 : 25 - remainder);

            for (let i = 0; i < points.length - 1; i+=1) {
                const diff = points[i+1] - points[i];
                const remainder = diff % 25;
                lost_points += (remainder == 0 ? 0 : 25 - remainder);
            }

            const element = $(this).find('.data-column[column=player_league_points]');
            element.attr('tooltip', `Lost points: ${lost_points}`);
        });
    }

    detectReallyExpiredBoosers() {
        const data = LeagueScoutModule.getCurrent();

        if (!data || !data.snapshots.length) {
            return;
        }

        $('.league_table .body-row').each(function (idx) {
            const opponent = opponents_list[idx];
            const opponent_id = parseInt(opponent.player.id_fighter);

            const is_currently_boosted = opponent.boosters.some(b => b.expiration > 0);
            if (is_currently_boosted) {
                return;
            }

            const boosted_data = data.snapshots.reverse().map(d => d.snapshot.find(p => p.id == opponent_id)).find(p => p.boosters && p.boosters.length >= 3);
            if (!boosted_data) {
                return;
            }

            console.log(`Player might be still boosted: ${opponent.nickname} (${idx})`);
            console.log(opponent.player);
            console.log(boosted_data);

            if (
                opponent.player.damage >= boosted_data.damage &&
                opponent.player.defense >= boosted_data.defense &&
                opponent.player.ego >= boosted_data.ego
            ) {
                const element = $(this).find('.data-column[column=boosters]');
                element.addClass('active_skill');
                console.log("YEAH")
            } else {
                console.log("NAH")
            }
        });
    }

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

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

        for (let i = 2; i <= 4; i++) {
            // this.insertRule(`.badge.top${i} {background-color:#8e36a9}`);
            this.insertRule(`.badge.top${i} {background:var(--legendary-bg);background-size:cover}`);
            this.insertRule(`.badge.top${i}::after {content:"${i}"}`);
        }

        const context = this;

        $('.league_table .body-row').each(function (idx) {
            const opponent = opponents_list[idx];
            if (opponent.best_placement != undefined) {
                context.updatePlayerPlacementBadge($(this), opponent);
            }
        });
    }

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

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

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

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

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

            return false;
        });
    }

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

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

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

    createBestPlacementBadge(player) {
        if (player.best_placement < 1 || player.best_placement > 4) {
            return ''
        }

        const clazz = `top${player.best_placement}`;
        return `<span class="best-placement"><span class="scriptLeagueInfoIcon badge ${clazz}"></span>${player.placement_count}</span>`;
    }

    makeCompatibleWithLeaguePlusPlus() {
        HHPlusPlus.Helpers.doWhenSelectorAvailable('div#leagues div.league_buttons a#change_team', () => {
            // Remove the avatars
            $('div.league_table div.data-row div.data-column[column=nickname] div.square-avatar-wrapper').remove();

            // Hide row when opponent has been fought
            const context = this;
            $('body').on('DOMSubtreeModified', '.league_table .body-row .data-column[column=match_history_sorting]', function() {
                context.hideUnhideRow($(this).parent('div.body-row'), context.isHideOpponents());
            });
        });
    }

    hideUnhideRow(row, hide) {
        const results = row.find('div.data-column[column=match_history_sorting]').find('div[class!="result "]').length;
        const fought_all = results == 3;
        if (fought_all && hide) {
            row.hide();
        } else if (fought_all && !hide) {
            row.show();
        }
    }

    isHideOpponents() {
        const filter = JSON.parse(storage.getItem(HHPLUSPLUS_OPPONENT_FILTER)) || {fought_opponent: false};
        return filter.fought_opponent;
    }
}

class PrebattleFlightCheckModule extends MyModule {
    constructor () {
        const baseKey = 'prebattleFlightCheck'
        const configSchema = {
            baseKey,
            default: true,
            label: `Run team and equipment checks before league battles`,
        }
        super({name: baseKey, configSchema})
    }

    shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('leagues-pre-battle') || HHPlusPlus.Helpers.isCurrentPage('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

Can anyone make something like this:
If, when choosing an opponent in Leagues, Season or Pantheon, you have a team that gives better average results, then a button would appear that switches the active team to this one.

Spoiler

For example we have such opponent

1.thumb.jpg.7f49e50f5afd999d776868efc770efd7.jpg

Active team gives us an average of 14.93 points (rena v.4 sim)

2.thumb.jpg.ea348bd8ef8b7131fd270c3175782917.jpg

But there is a better team (5), that gives us an average of 16.67 points

3.thumb.jpg.47f36851bf3e5ee45a3322d8e3118160.jpg

Then a button appears (as a design, I took a picture of the best team indicating its number, but you can use something else), when clicked, the active team (1) changes to the best one (5).
If such a command is already selected, then the additional button does not appear.

4.thumb.jpg.3af22435b42d8c030e5fc9f2a0c3966b.jpg

In theory, this should not be considered cheats in any way, because in essence, only information is read, as in the case of all other presented scripts, and changing the command simply saves us 2 transitions with loads.

P.S. For the Season, the team is selected for the currently selected opponent (1 of 3)

  • Thinking 1
Link to comment
Share on other sites

13 hours ago, Master-17 said:

If, when choosing an opponent in Leagues, Season or Pantheon, you have a team that gives better average results, then a button would appear that switches the active team to this one.

AFAIK, the data of the (other) teams is not available on the battle page. So it would need to be stored (e.g. in local storage) when accessing the team edit page. What I am not sure about is whether there is a way (API call or what) to switch the team outside of the team selection page. But at least, the information that there is a better team could be shown, similar to 430i's script showing if you have better ME for your current team in inventory.

Another topic @renalove:
Currently the all opponents booster simulator does not respect the "skip fought opponents" flag. Could you add this? IMO it makes sense, not only for consistency with the sum shown when hovering the column, but also as one might accumulate certain types of opponents, like stronger ones, or ones with certain GS5 (reflect) over the week, so to best boosters against those might change. That way one could better know how to reboost for the actual remaining opponents.

@430i
Snapshot data conversion seems to have worked well here, many thanks! Out of interest, what did you change?
And do I understand it correctly, that you implemented a booster detector like we had it before the league rework? I see the info in browser console, but no indicator in the table yet, probably because the opponents are fought already (hidden). Did not have a closer look yet. It is sad that such is still necessary (that the game indicators are not reliable), but many thanks that you addressed this!

Edited by Horsting
Link to comment
Share on other sites

Il y a 2 heures, Horsting a dit :

It is sad that such is still necessary (that the game indicators are not reliable)

Actually, it is reliable since it shows when the boosters started and when they will expire. But it doesn't show if the stats are boosted or not.

Maybe scripts aren't supposed to do everything for players.

Personaly, I think there are limits to what scripts can do. Even if it's not automation.

  • Like 5
Link to comment
Share on other sites

12 minutes ago, Tom208 said:

Actually, it is reliable since it shows when the boosters started and when they will expire. But it doesn't show if the stats are boosted or not.

But an indicator, which does not indicate the only relevant thing (stats) has zero purpose and even causes wrong decisions and is hence in the end worse than no indicator at all. I mean yeah, when you know how it works resp. does not work, then you can make it still helpful, but for everyone who is not reading deep into the forum, does tests etc, hence relying on what is shown is what you get, it is horrible.

15 minutes ago, Tom208 said:

Maybe scripts aren't supposed to do everything for players.

Personaly, I think there are limits to what scripts can do. Even if it's not automation.

Putting philosophy aside, a script can calculate whether an opponent is boosted or not, up to the precision where RE comes into play (when not storing that data and taking it into account as well), which is accurate enough to derive which boosters are effectively active, same as currently players can do themselves. So if someone invests the time/effort to code this feature into a script, I see zero reason not to do it, and am very thankful and happy to use it.

However, for completeness, currently it seems like the booster indicators of the game become mostly correct within 15 minutes. For unknown reasons, in probably 20% of cases, it however takes up to 1.5 hours after expired boosters for the opponent's stats to adjust. So it has changed a little to the better, but is still unreliable and inconsistent, and hence still in a completely unacceptable and embarrassing state, IMO.

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

2 hours ago, Horsting said:

And do I understand it correctly, that you implemented a booster detector like we had it before the league rework?

Not really - it just checks whether the stats of the player after the boosters expired are equal to the last known boosted stats. No fancy calculation, but something that you can do with pen and paper (or multiple open tabs).

  • Like 4
Link to comment
Share on other sites

49 minutes ago, 430i said:

it just checks whether the stats of the player after the boosters expired are equal to the last known boosted stats.

Ah okay, so basically what I am doing often manually for a single next target opponent: I select the opponent which has its boosters expired next, so that with Leagues++ its stats are shown at the right side, and just remember the 1 stat which indicates its boosters best. So when the boosters expired, I open the league page and only need to wait until the opponents stats show up to know whether the stats dropped already or not, no sorting or scrolling needed.

EDIT: @renalove That was fast (likely worked on before) regarding whole league booster simulator for only unfought opponents. Even unboosted is possible now. Many thank! 4x Chlorella ist currently best against my unfought and unboosted opponents, as there is one with reflect skill among them 😄.

image.png.e3639671ad2358e8091d19e32f61d935.png

Edited by Horsting
  • Surprised :O 1
Link to comment
Share on other sites

il y a une heure, Horsting a dit :

Putting philosophy aside, a script can calculate whether an opponent is boosted or not, up to the precision where RE comes into play (when not storing that data and taking it into account as well), which is accurate enough to derive which boosters are effectively active, same as currently players can do themselves. So if someone invests the time/effort to code this feature into a script, I see zero reason not to do it, and am very thankful and happy to use it.

Actually, my remark was more about the "which team should I use" feature than the "are these stats still boosted or not" feature.

Even if with the power team display you can estimate if stats are boosted or not by comparison to your own stats and team power.

  • Like 3
Link to comment
Share on other sites

16 hours ago, Master-17 said:

If, when choosing an opponent in Leagues, Season or Pantheon, you have a team that gives better average results, then a button would appear that switches the active team to this one.

16 hours ago, Master-17 said:

In theory, this should not be considered cheats in any way, because in essence, only information is read, as in the case of all other presented scripts, and changing the command simply saves us 2 transitions with loads.

It is not easy to store all teams because raw team data is surprisingly large. And I think the function to automatically switch to the best team with a single click may violate Terms of Use. I do not plan to implement such a feature. However, it is good to discuss such ideas.

16 hours ago, Master-17 said:

P.S. For the Season, the team is selected for the currently selected opponent (1 of 3)

Added a simulator for the last selected opponent in the season to the team page.

3 hours ago, Horsting said:

Currently the all opponents booster simulator does not respect the "skip fought opponents" flag. Could you add this?

Added a filter to the booster simulator in the league table.

  • Like 2
Link to comment
Share on other sites

1 hour ago, renalove said:

And I think the function to automatically switch to the best team with a single click may violate Terms of Use

It's not automatic. You press a button and another team is selected. It is not formed, but selected from those already formed. Essentially, in one click, you go to select a command, select a team, and go back.
*thinking* Although yes, perhaps this can be regarded as a violation of the rules, otherwise you can collect money this way: the Harem window opens, clicks on all the girls and then returns to the main screen. It seems that everything is the same as what the player does, but faster and then the “Collect everything” button (paid) is not needed, and the developers do not want this.
Well, okay, can we make a button indicating the presence of a “best team”? And so that when you click on it, you will immediately go to the desired team?

  • Thinking 1
Link to comment
Share on other sites

  • Moderator
On 11/6/2023 at 6:57 PM, Master-17 said:

*thinking* Although yes, perhaps this can be regarded as a violation of the rules

It would very likely be, and on top of that I honestly don't understand why you would want this so much. By default we have 6 team slots, of which a majority of us only really use 3 or 4 at most these days, usually for very clear and very different purposes. Eg Attack/Main Team, Defense/Reflect Team, Weak/Season Team, maybe a Pantheon Team, and then maybe an alternative or two for special cases (say, "as close as possible to my Attack/Main Team, but with or without such or such color tag").

In what circumstances would you A/ have a meaningful choice between two or more similarly relevant teams and B/ need a tool to make that choice for you? o.O

I'm honestly scratching my head, here (note that Teams 1, 3 and 4 are "empty"):

image.png

  • Like 1
Link to comment
Share on other sites

6 ore fa, DvDivXXX ha scritto:

In what circumstances would you A/ have a meaningful choice between two or more similarly relevant teams and B/ need a tool to make that choice for you? o.O

It would make sense in the future when you have many bulbs to skill specific teams (all blue eyes etc.). In some cases they might be better than the blessed team you have prepared.

Edited by Miccia
  • Thinking 1
Link to comment
Share on other sites

  • Moderator

@renalove & @430i  I recently updated to your new scripts. 😊 Thanks!

I like that the girls' attributes are visible now w/ little icons at the bottom, while hovering over them.  Useful to check if a player is actually making proper use of girl skills.  Also, to double check the synergy of your own teams.  Wish it was like this throughout the whole game.

image.png

The booster sim on the league table is impressive.  I see it shows that 4 chlorella + LM is best for un-boosted players, and 1 chlorella, 3 cords, + LM are best vs. the boosted ones.  It helps me prioritize which players to attack first, w/ which boosters, before they're about to expire.

2023-11-08_13-25-32.png2023-11-08_13-25-41.png

The skill sim makes me think that level 5 stun is the 2nd best, behind level 5 shield.

image.png

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

I also like the new column w/ bulbs, which is highlighted red if they're using a tier 5 skill.  It'll help me focus on those players first.  Mine shows 141, b/c I'm using 22 bulbs on 5 L5s (110), and 31 on my shielded M6.

image.png

Good to know that the browser's local storage can fill up, and it should be deleted after each league week.  I don't actually make meaningful use of those .csv files.  Before these scripts, I used to manually take snapshots, and graph out the players' scores over time for the LAA thread, but it was tedious, so I stopped doing it. 

I do like the idea of seeing potential lost points.  I've been keeping track of that just for my own score, but being able to see it for the players that might be threat, is useful.  It isn't working properly for me atm, b/c I updated it in the middle of the league week, after clearing previous snapshots out of caution.  Would definitely relieve some stress, to know a player you were competing w/ was way behind, w/ too many lost points to be a real threat.

  • Like 3
Link to comment
Share on other sites

10 hours ago, DvDivXXX said:

In what circumstances would you A/ have a meaningful choice between two or more similarly relevant teams and B/ need a tool to make that choice for you?

Mainly for the season/leagues, firstly, to immediately know that you have a team that is stronger against a given opponent than the one already selected, and secondly, to reduce the number of actions and downloads in order to select the right one. I don’t know why you have only 3-4 active teams left. After presenting the skills, the only thing I changed in my preparation strategy was to level up the shield of one of my best girls and now I put her in the center of all formed teams. Otherwise, as before, I form a rainbow or dominant team of the strongest girls as the main (defensive) and 5 teams against certain elements, so that they have a 10% increase in damage and ego. Which on average gives an increase of 1 point in the Leagues and 5-10% (or even 20-30) to the chance of winning in the Season in relation to the main team. And this is taking into account the Rena simulator.

  • Surprised :O 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...