Jump to content

[ August 2nd, 2023 ] Leagues Redesign


jelom
 Share

Recommended Posts

12 minutes ago, Ravi-Sama said:

The table stats are just from whichever was the most recent snapshot.

When does it update? When I look at the table for opponents, is the table showing me what they have or what they used to have? How the devil does KK expect intelligent play when the displayed table is wrong? Don't get me wrong, I fully hope to finish in the top 4, but this is 100% wrong.

At a minimum, can we get a timer showing when the global snapshot will update so we know what we are doing instead of guessing at it?

Edited by Pelinor
added content
  • Like 2
Link to comment
Share on other sites

  • Moderator
4 minutes ago, Miccia said:

the teams updated at around 16:00, at least 2 hours later than usual

I noticed a 2 hr delay yesterday too, when I was observing a player's boosted vs un-boosted stats.

Coincidence?

On 8/6/2023 at 10:05 AM, Ravi-Sama said:

Their stats finally deflated, except it happened ~2 hrs after they applied the new boosters.

  • Thinking 1
Link to comment
Share on other sites

Jep, I think shortly after 15:50 MET was when I saw it already, so probably exactly 2h later. This creates a nasty time window where it is difficult to select opponents:

  • There are the ones which had their boosters expired before blessings reset, so have no boosters but their team fully blessed. Battle them of hope that they won't have their team adjusted for new blessings after next snapshot?
  • The ones which have their boosters expire after reset, but their teams already adjusted. With the boosters expiry, their snapshot is immediately updated, hence their team is fully blessed as well, but boosters expired. One will likely not get a better chance to battle them, but if they are still strong, for a (relatively) low level player like me still a question whether to better battle weaker enemies first.
  • And then the ones which had their boosters expired and re-applied shortly before reset, hence after last snapshot. They should be effectively unboosted, but sometimes this is difficult to know from their stats. The booster detector was able to, but does not work anymore. And it is ridiculous to update an app when the game itself should actually show the exact and correct booster status.
  • Best to watch out for opponents which have their boosters expire after reset but before next snapshot, and then hope that they did not have their team adjusted yet for new blessings.
  • It was definitely easier to find easy opponents with snapshot shortly after blessings reset, like it was before, i.e. less time to adjust teams (if not done before reset already). But it is probably fairer now as everyone has more time between reset and snapshot to adjust the team (as long as boosters do not expire earlier).
Edited by Horsting
Link to comment
Share on other sites

image.png.5205d84331bd1388eb5fb92afcbf24d1.png

After the booster should vanish, I always get a phantom buff, unless I hover over it with the mouse, I never know, so that's really annoying

I really want to be able to sort the table again to find the ones I still need to challenge. Really unpleasent at the moment

  • Like 2
Link to comment
Share on other sites

If it were up to me, I would have put the blessing deadline on Thursday at the end of the championship.

So it's just a way to spend koban on refills, but it often profoundly distorts everything one has done in a week of the League.

  • Like 1
Link to comment
Share on other sites

  • Moderator
44 minutes ago, Pelinor said:

When I look at the table for opponents, is the table showing me what they have or what they used to have?

Since the table matches the opponent's current stats, you can trust those stats, but regarding when it was updated, it's anyone's guess.  Sometimes players have individual snapshots, and that was always the case, especially when they changed their boosters.

I atk when I see lower stats, near or below 100k Atk, and a nice E[X], preferably above 23.  That hints to me that they're probably un-boosted, even if they're shown to be boosted atm.

19 minutes ago, SamRei said:

I really want to be able to sort the table again to find the ones I still need to challenge. Really unpleasent at the moment

Can use @430i & @-MM-'s scripts to hide/show opponents, and sort, until sorting is officially fixed next week.

MM's Leagues ++

On 8/5/2023 at 7:27 AM, -MM- said:

430i's script highlighting expired boosters, w/ the blur removed already from the E[X] column:

16 hours ago, 430i said:
  Hide contents
// ==UserScript==
// @name            Hentai Heroes+++
// @description     Few QoL improvement and additions for competitive PvP players.
// @version         0.16.8
// @match           https://*.hentaiheroes.com/*
// @match           https://nutaku.haremheroes.com/*
// @run-at          document-body
// @grant           none
// @author          430i
// ==/UserScript==


const {$, location, localStorage: storage} = window

// localStorage keys
const LS_CONFIG_NAME = 'HHPlusPlusPlus'
const LEAGUE_BASE_KEY = LS_CONFIG_NAME + ".League";
const LEAGUE_SNAPSHOT_BASE_KEY = LEAGUE_BASE_KEY + ".Snapshot";
const CURRENT_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Current";
const PREVIOUS_LEAGUE_SNAPSHOT_KEY = LEAGUE_SNAPSHOT_BASE_KEY + ".Previous";
const LEAGUE_PLAYERS_KEY = LEAGUE_BASE_KEY + ".Players";
const EQUIPMENT_KEY = LS_CONFIG_NAME + ".Equipment";
const EQUIPMENT_CURRENT_KEY = EQUIPMENT_KEY + ".Current";
const EQUIPMENT_BEST_MYTHIC_KEY = EQUIPMENT_KEY + ".Mythic";
const EQUIPMENT_FAVORITE_KEY = EQUIPMENT_KEY + ".Favorite";
const TEAMS_BASE_KEY = LS_CONFIG_NAME + ".Teams";
const TEAMS_ALL_KEY = TEAMS_BASE_KEY + ".All";
const TEAMS_CURRENT_ID_KEY = TEAMS_BASE_KEY + ".CurrentId";

// 3rd party localStorage keys
const LS_CONFIG_HHPLUSPLUS_NAME = 'HHPlusPlus'
const FOUGHT_OPPONENTS_HIDDEN = LS_CONFIG_HHPLUSPLUS_NAME + "FoughtOpponentsHidden"

// icon paths
const PATH_GROUPS = '<path d="M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14 c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43V18l4.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2 c0-1.1-0.9-2-2-2s-2,0.9-2,2C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14 c-0.39,0-0.76,0.04-1.13,0.1c0.4,0.68,0.63,1.46,0.63,2.29V18l4.5,0V16.43z M16.24,13.65c-1.17-0.52-2.61-0.9-4.24-0.9 c-1.63,0-3.07,0.39-4.24,0.9C6.68,14.13,6,15.21,6,16.39V18h12v-1.61C18,15.21,17.32,14.13,16.24,13.65z M8.07,16 c0.09-0.23,0.13-0.39,0.91-0.69c0.97-0.38,1.99-0.56,3.02-0.56s2.05,0.18,3.02,0.56c0.77,0.3,0.81,0.46,0.91,0.69H8.07z M12,8 c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.45,8,12,8 M12,6c-1.66,0-3,1.34-3,3c0,1.66,1.34,3,3,3s3-1.34,3-3 C15,7.34,13.66,6,12,6L12,6z"/>';
const PATH_GROUP = '<path d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z"/>';
const PATH_CLEAR = '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>';

class EquipmentCollector {
    static collect() {
        if (!HHPlusPlus.Helpers.isCurrentPage('shop')) {
            return;
        }

        HHPlusPlus.Helpers.defer(() => {
            setTimeout(() => {
                EquipmentCollector.collectPlayerEquipment();
                EquipmentCollector.collectBestMythicEquipment();
            }, 250);
        });
    }

    static collectPlayerEquipment() {
        const eqElements = $("div#equiped.armor-container div.slot:not(:empty)[subtype!='0']");
        if (eqElements.length != 6) {
            console.log("Did not find 6 equipment elements.");
            return;
        }

        const equipment = eqElements.map(function() { return $(this).data("d")}).get();
        const equipmentStripped = equipment.map((e) => {
            return {
                id: e.id_member_armor_equipped || e.id_member_armor, // unique item identifier?
                rarity: e.item.rarity, // legendary, mythic
                type: e.item.type, // always "armor"
                skin_id: e.skin.identifier, // EH13, ET21 etc
                subtype: parseInt(e.skin.subtype), // 1, 2, 3, 4, 5 or 6
                carac1: parseInt(e.caracs.carac1),
                carac2: parseInt(e.caracs.carac2),
                carac3: parseInt(e.caracs.carac3),
                harmony: parseInt(e.caracs.chance),
                endurance: parseInt(e.caracs.endurance),
            };
        });

        window.localStorage.setItem(EQUIPMENT_CURRENT_KEY, JSON.stringify(equipmentStripped));
    }

    static collectBestMythicEquipment() {
        const equipment = player_inventory.armor
            .filter(a => a.item.rarity == "mythic")
            .filter(a => parseInt(a.resonance_bonuses.class.identifier) == Hero.infos.class)
            .filter(a => a.resonance_bonuses.class.resonance == "damage")
            .filter(a => a.resonance_bonuses.theme.resonance == "defense");

        window.localStorage.setItem(EQUIPMENT_BEST_MYTHIC_KEY, JSON.stringify(equipment));
    }

    static getCurrent() {
        return JSON.parse(window.localStorage.getItem(EQUIPMENT_CURRENT_KEY)) || [];
    }

    static getBestMythic() {
        return JSON.parse(window.localStorage.getItem(EQUIPMENT_BEST_MYTHIC_KEY)) || [];
    }
}

class LeaguePlayersCollector {
    static collect() {
        if (!HHPlusPlus.Helpers.isCurrentPage('tower-of-fame')) {
            return;
        }

        HHPlusPlus.Helpers.defer(() => {
            HHPlusPlus.Helpers.onAjaxResponse(/action=fetch_hero&id=profile/, LeaguePlayersCollector.collectPlayerPlacementsFromAjaxResponse);
            LeaguePlayersCollector.collectPlayerData();
        });
    }

    static collectPlayerPlacementsFromAjaxResponse(response, opt) {
        // If you are reading this, please look away, ugly code below
        // The mythic equipment data is actually not in the html, but in the form of a script that we have to eval
        const html = $("<div/>").html(response.html);
        $.globalEval(html.find('script').text()); // creates 'hero_items'

        const id = html.find("div.ranking_stats .id").text().match(/\d+/)[0];
        const username = html.find(".hero_info h3 .hero-name").text();
        const level = html.find('div[hero="level"]').text().trim();
        const number_mythic_equipment = Object.values(hero_items).filter(i => i.item.rarity == "mythic").length;
        const d3_placement = $("<div/>")
                            .html(html)
                            .find('div.history-independent-tier:has(img[src*="/9.png"]) span') // 9.png is D3
                            .map(function() {return parseInt($(this).text().trim().match(/\d+/));})
                            .get();

        if (!id || !username || !level) {
            window.popup_message("Error when parsing player data.");
            return;
        }

        if (!d3_placement || d3_placement.length != 2) {
            // make sure our parser is working by checking the D2 data
            const d2_placement = $("<div/>")
                                    .html(html)
                                    .find('div.history-independent-tier:has(img[src*="/8.png"]) span') // 8.png is D2
                                    .map(function() {return parseInt($(this).text().trim().match(/\d+/));})
                                    .get();

            if (d2_placement.length != 2) {
                window.popup_message("Error when parsing D2 player data.");
            }

            d3_placement.push(-1, 0);
        }

        const data = {
            id: parseInt(id),
            number_mythic_equipment,
            best_placement: d3_placement[0],
            placement_count: d3_placement[1],
        };

        LeaguePlayersCollector.storePlayerData(data);
        $(document).trigger('player:update-profile-data', {id: data.id})
    }

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

            const girls = player.player.team.girls;
            const girl_levels = girls.map(g => g.level);
            const girl_levels_max = Math.max(...girl_levels);
            const girl_levels_total = girl_levels.reduce((a, b) => a + b, 0);
            const girl_levels_avg = Math.floor(girl_levels_total / girl_levels.length);

            const data = {
                id: parseInt(player.player.id_fighter),
                username: player.player.nickname,
                level: parseInt(player.player.level),
                damage: player.player.damage,
                defense: player.player.defense,
                harmony: player.player.chance,
                ego: player.player.remaining_ego,
                power: player.player.team.total_power,
                club_id: player.player.club?.id_club,
                club_name: `"${player.player.club?.name || ''}"`,
                girl_levels_avg,
                girl_levels_max,
            }

            LeaguePlayersCollector.storePlayerData(data);
        }
    }

    static me() {
        for (var r = 0, n = window.opponents_list.length; r < n; r++) {
            const player = window.opponents_list[r];
            if (player.player.nickname == Hero.infos.name) {
                return player;
            }
        }

        window.popup_message("Could not find myself in the league table.");
    }

    static storePlayerData(data) {
        const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {};
        if (players[data.id] == undefined) {
            players[data.id] = {};
        }

        Object.assign(players[data.id], data);

        storage.setItem(LEAGUE_PLAYERS_KEY, JSON.stringify(players));
    }

    static export() {
        const columns = [
            "id",
            "username",
            "level",
            "damage",
            "defense",
            "harmony",
            "ego",
            "power",
            "club_id",
            "club_name",
            "girl_levels_max",
            "girl_levels_avg",
            "expected_points",
            "number_mythic_equipment",
            "best_placement",
            "placement_count",
        ]

        const players = JSON.parse(storage.getItem(LEAGUE_PLAYERS_KEY)) || {};
        const data = Object.values(players).map(player => columns.map(column => player[column]));

        console.log([columns].concat(data).map(t => t.join(",")).join("\n"));
    }

    static clear() {
        storage.removeItem(LEAGUE_PLAYERS_KEY);
    }
}

class TeamsCollector {
    static collect() {
        if (!HHPlusPlus.Helpers.isCurrentPage('teams')) {
            return;
        }

        HHPlusPlus.Helpers.defer(() => {
            // action=select_team&id_team=3680477&battle_type=leagues
            HHPlusPlus.Helpers.onAjaxResponse(/action=select_team&id_team=\d+&battle_type=leagues/, TeamsCollector.collectSelectedTeamFromAjaxResponse);
            TeamsCollector.collectTeams();
        });
    }

    static collectSelectedTeamFromAjaxResponse(response, opt) {
        // Figure out the team id from the request params
        const params = new URLSearchParams(opt.data);
        const team_id = params.get("id_team");

        // Only store the team id to be able to easily refresh the team information when boosters expire or new ones are applied.
        window.localStorage.setItem(TEAMS_CURRENT_ID_KEY, JSON.stringify(parseInt(team_id)));
    }

    static collectTeams() {
        // Create a team map - team_id: team
        const teams = Object.fromEntries(Object.values(teams_data).filter(t => !t.locked).map(t => [parseInt(t.id_team), t]));
        storage.setItem(TEAMS_ALL_KEY, JSON.stringify(teams));
    }

    static getCurrent() {
        const team_id = JSON.parse(window.localStorage.getItem(TEAMS_CURRENT_ID_KEY));
        const all_teams = TeamsCollector.getAll();

        if (team_id && all_teams) {
            return all_teams[team_id];
        }

        window.popup_message(
            `Could not retrieve current team. Team ID: ${team_id}, all teams: ${Boolean(all_teams)}.
            Please open the team selection page and re-select your team.`
        );
    }

    static getAll() {
        return JSON.parse(window.localStorage.getItem(TEAMS_ALL_KEY)) || {};
    }
}

class LeagueSimulator {
    extract(me, other) {
        // This largely uses the upsteam 'League' class (thanks zoop0kemon), but tweaks it to the new input data and
        // takes the themes into account to caculate defence reduction and counter bonuses.
        const {
            team: playerTeam
        } = me.player;

        var {
            ego: playerEgo,
            damage: playerAtk,
            defense: playerDef,
            chance: playerCrit,
        } = playerTeam.caracs;

        const playerElements = playerTeam.theme_elements.map(({type}) => type);
        const playerWeaknessElements = playerTeam.theme_elements.map(({weakness}) => weakness);
        const playerSynergies = playerTeam.synergies;
        const playerBonuses = {
            critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'fire'),
            critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'stone'),
            healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'water'),
            reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(playerSynergies, 'sun'),
        }

        const {
            nickname: opponentName,
            team: opponentTeam,
        } = other.player;

        var {
            ego: opponentEgo,
            chance: opponentCrit,
            damage: opponentAtk,
            defense: opponentDef,
        } = opponentTeam.caracs

        const opponentTeamMemberElements = [];
        [0,1,2,3,4,5,6].forEach(key => {
            const teamMember = opponentTeam.girls[key]
            if (teamMember && teamMember.element) {
                opponentTeamMemberElements.push(teamMember.element)
            }
        })
        const opponentElements = opponentTeam.theme_elements.map(({type}) => type)
        const opponentWeaknessElements = opponentTeam.theme_elements.map(({weakness}) => weakness);

        const opponentSynergies = opponentTeam.synergies
        const teamGirlSynergyBonusesMissing = opponentSynergies.every(({team_girls_count}) => !team_girls_count)
        let counts
        if (teamGirlSynergyBonusesMissing) {
            // Open bug, sometimes opponent syergy data is missing team bonuses, so we need to rebuild it from the team
            counts = opponentTeamMemberElements.reduce((a,b)=>{a[b]++;return a}, {
                fire: 0,
                stone: 0,
                sun: 0,
                water: 0,
                nature: 0,
                darkness: 0,
                light: 0,
                psychic: 0
            })
        }

        const opponentBonuses = {
            critDamage: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'fire', teamGirlSynergyBonusesMissing, counts),
            critChance: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'stone', teamGirlSynergyBonusesMissing, counts),
            healOnHit: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'water', teamGirlSynergyBonusesMissing, counts),
            reduceDefence: HHPlusPlus.SimHelpers.findBonusFromSynergies(opponentSynergies, 'sun', teamGirlSynergyBonusesMissing, counts),
        }

        const dominanceBonuses = HHPlusPlus.SimHelpers.calculateDominationBonuses(playerElements, opponentElements);

        const counterElementsAtkEgo = ['fire', 'nature', 'stone', 'sun', 'water'];
        const counterElementsHarmony = ['darkness', 'light', 'psychic'];

        const playerCountersOpponentAtkEgo = opponentWeaknessElements.some(e => counterElementsAtkEgo.includes(e) && playerElements.includes(e));
        const opponentCountersPlayerAtkEgo = playerWeaknessElements.some(e => counterElementsAtkEgo.includes(e) && opponentElements.includes(e));

        const playerCountersOpponentHarmony = opponentWeaknessElements.some(e => counterElementsHarmony.includes(e) && playerElements.includes(e));
        const opponentCountersPlayerHarmony = playerWeaknessElements.some(e => counterElementsHarmony.includes(e) && opponentElements.includes(e));

        // Attack & Ego
        if (playerCountersOpponentAtkEgo) {
            playerAtk *= 1.1;
            playerEgo *= 1.1;
        }
        if (opponentCountersPlayerAtkEgo) {
            opponentAtk *= 1.1;
            opponentEgo *= 1.1;
        }

        // Defence
        // Our opponent's defence has already been reduced when creating the league table
        // opponentDef *= (1 - playerBonuses.reduceDefence);
        playerDef *= (1 - opponentBonuses.reduceDefence);

        // Harmony
        if (playerCountersOpponentHarmony) {
            playerCrit *= 1.2;
        }
        if (opponentCountersPlayerHarmony) {
            opponentCrit *= 1.2;
        }

        const player = {
            hp: playerEgo,
            dmg: playerAtk - opponentDef,
            critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(playerCrit, opponentCrit) + dominanceBonuses.player.chance + playerBonuses.critChance,
            bonuses: {...playerBonuses, dominance: dominanceBonuses.player},
            theme: playerElements,
        }
        const opponent = {
            hp: opponentEgo,
            dmg: opponentAtk - playerDef,
            critchance: HHPlusPlus.SimHelpers.calculateCritChanceShare(opponentCrit, playerCrit) + dominanceBonuses.opponent.chance + opponentBonuses.critChance,
            name: opponentName,
            bonuses: {...opponentBonuses, dominance: dominanceBonuses.opponent},
            theme: opponentElements,
        }

        return {player, opponent}
    }
}

class MyModule {
    constructor ({name, configSchema}) {
        this.group = '430i'
        this.name = name
        this.configSchema = configSchema
        this.hasRun = false

        this.insertedRuleIndexes = []
        this.sheet = HHPlusPlus.Sheet.get()
    }

    insertRule (rule) {
        this.insertedRuleIndexes.push(this.sheet.insertRule(rule))
    }

    tearDown () {
        this.insertedRuleIndexes.sort((a, b) => b-a).forEach(index => {
            this.sheet.deleteRule(index)
        })

        this.insertedRuleIndexes = []
        this.hasRun = false
    }
}

class LeagueScoutModule extends MyModule {
    constructor () {
        const baseKey = 'leagueScout'
        const configSchema = {
            baseKey,
            default: true,
            label: `Gather information about league opponents`,
        }
        super({name: baseKey, configSchema})
    }

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

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

        $(document).on('league:rollover', () => {
            const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || [];

            storage.setItem(PREVIOUS_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data));
            storage.removeItem(CURRENT_LEAGUE_SNAPSHOT_KEY);
        })

        HHPlusPlus.Helpers.defer(() => {
            // read and store data
            const playerData = this.readPlayerData();
            const snapshot = this.createSnapshot(playerData);
            this.storeSnapshot(snapshot);

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

        this.hasRun = true;
    }

    readPlayerData() {
        const data = [];

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

            const id = player.id_player;
            const rank = player.place;
            const name = player.player.nickname;
            const country = player.country;
            const level = parseInt(player.player.level);
            const points = parseInt(player.player_league_points);
            const elements = player.player.team.theme;

            data.push({id, name, rank, level, elements, points, country});
        }

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

        return data;
    }

    createSnapshot(playerData) {
        const numPlayers = playerData.length;
        const currentDate = new Date(window.server_now_ts * 1000);
        const leagueEndDate = new Date(window.server_now_ts * 1000 + window.season_end_at * 1000);

        return {
            date: currentDate,
            league_end: leagueEndDate,
            num_players: numPlayers,
            player_data: playerData,
        }
    }

    storeSnapshot(snapshot) {
        const data = JSON.parse(storage.getItem(CURRENT_LEAGUE_SNAPSHOT_KEY)) || [];

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

        data.push(snapshot);
        storage.setItem(CURRENT_LEAGUE_SNAPSHOT_KEY, JSON.stringify(data));
    }

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

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

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

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

        $(document.body).on('click', `#${buttonId}`, () => {
            const data = JSON.parse(storage.getItem(what)) || [];

            const separator = ","
            const columns = ["date", "player_id", "player_name", "player_rank", "player_points"];
            const values = data.flatMap((e) => e.player_data.map((p) => [e.date, p.id, p.name, p.rank, p.points].join(separator)));

            let csvContent = `sep=${separator}\n` + columns.join(separator) + "\n" + values.join("\n");

            var element = document.createElement('a');
            element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent));
            element.setAttribute('download', `${friendlyId}.csv`);

            element.style.display = 'none';
            document.body.appendChild(element);

            element.click();

            document.body.removeChild(element);
        });
    }

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

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

        const clearButton = this.createButton(buttonId, PATH_CLEAR);
        parent.append(clearButton);

        $(document.body).on('click', `#${buttonId}`, () => {
            storage.removeItem(what);
        });
    }
}

class LeagueTableModule extends MyModule {
    constructor () {
        const baseKey = 'leagueTable'
        const configSchema = {
            baseKey,
            default: true,
            label: `Extend league table with additional opponents' information`,
        }
        super({name: baseKey, configSchema})
    }

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

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

        HHPlusPlus.Helpers.defer(() => {
            this.showPlayerLiveData();
            this.extendLeagueDataModel();

            HHPlusPlus.Helpers.doWhenSelectorAvailable('.league_buttons_block', () => {
                this.showHideUnhideButton();
                this.extendLeagueTable();
                this.renderUpdatedStats();
            });

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

        this.hasRun = true;
    }

    showPlayerLiveData() {
        // Show the live data for the player: theme and stats (atk, ego, def, harmony)
        const player = LeaguePlayersCollector.me();
        const team = TeamsCollector.getCurrent();

        if (!team) {
            return;
        }

        // Update the player's data
        player.player.team = team;

        // Update the opponents' data (only defence)
        for (var r = 0, n = window.opponents_list.length; r < n; r++) {
            const opponent = window.opponents_list[r];

            if (opponent == player) {
                continue;
            }

            // Defence
            const reduceDefence = HHPlusPlus.SimHelpers.findBonusFromSynergies(player.player.team.synergies, 'sun');
            opponent.player.team.caracs.defense *= (1 - reduceDefence);
        }
    }

    renderUpdatedStats() {
        // Update the UI to show the updated stats from the 'opponents_list' data model:
        //  - the player stats with the stats of the currently selected team and not the one from the last snapshot
        //  - the opponent's stats based on the currently selected player team
        $('div.league_table div.body-row')
        .each(function(row_index) {
            const team = opponents_list[row_index].player.team;

            $(this).find('div.data-column[column=boosters] div.player_stats span.carac_value')
            .each(function(carac_idx) {
                const carac_name = this.id.split("-")[1];
                const carac_value = team[carac_name] || team.caracs[carac_name];

                $(this).text(number_reduce(carac_value));
            });
        });
    }

    calculateExpectedPoints(me, other) {
        const sim = new LeagueSimulator();
        const {player, opponent} = sim.extract(me, other);

        const simulator = new HHPlusPlus.Simulator({player, opponent, highPrecisionMode: false, logging: false})
        const result = simulator.run()

        return Object.entries(result.points).map(([pts, coef]) => parseInt(pts) * coef).reduce((acc, a) => acc + a, 0);
    }

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

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

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

            const expected_points = this.calculateExpectedPoints(me, player);

            player.expected_points = expected_points;
            player.best_placement = best_placement;
            player.placement_count = placement_count;
        }
    }

    extendLeagueTable() {
        // Additional CSS classes
        this.insertRule(`.scriptLeagueInfoIcon.top1 {background-color:var(--var-color-mythic)}`);
        this.insertRule('.scriptLeagueInfoIcon.top1::after {content:"1"}');
        this.insertRule('.league_table .data-list .body-row .data-column[column="team"] span {width: 20px; height: 20px; background-size: 20px}');
        // this.insertRule('.league_table .data-list .body-row .data-column[column="expected_points"] {filter: blur(0px); -webkit-filter: blur(0px);}');
        this.insertRule('.league_table .data-list .body-row .data-column[column="reload"] button {padding: 2px 5px}');

        // Highlight expired boosters
        $('div.league_table div.data-column[column=boosters] div[type=booster]')
        .each(function(index) {
            const d = $(this).data('d');
            if (d.expiration == 0) {
                $(this).css({'border': '1px solid red'});
            }
        })

        // Create "power", "e[x]" ...
        $('div.league_table div.head-row div.head-column[column=team]')
        .after(
            '<div class="data-column head-column" column="power">Power</div>' +
            '<div class="data-column head-column" column="expected_points">E[X]</div>'
        );

        // ... and "reload" headers.
        $('div.league_table div.head-row div.head-column:last-child')
        .after('<div class="data-column head-column" column="reload">↻</div>');

        // Populate additional league table columns:
        //  - add "power" and "e[x]" data
        //  - add best placement indicators next to the opponents name
        //  - replace synergy tooltip with theme icons
        $('div.league_table')
            .find('div.body-row')
            .each(function(index) {
                const opponent = window.opponents_list[index];

                // power and e[x] columns
                const power = opponent.player.team.total_power;
                const e_x = opponent.expected_points;
                $(this).find('div.data-column[column=team]').after(
                    `<div class="data-column" column="power">${(power).toFixed()}</div>
                    <div class="data-column" column="expected_points">${(e_x).toFixed(2)}</div>`
                );

                // best placement indicator next to the nickname
                if (opponent.best_placement == 1) {
                    $(this).find('div.data-column[column=nickname]')
                    .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`)
                } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) {
                    $(this).find('div.data-column[column=nickname]')
                    .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`)
                }

                // show theme icons instead of syngergy tooltip
                const themes = opponent.player.team.theme.split(',').map(t => t || "balanced");
                const theme_icons = themes.map(t => `<span class="${t}_element_icn ${t}_theme_icn"></span>`);
                $(this).find('div.data-column[column=team]').html(theme_icons.join(''));

                // add empty reload column
                $(this).find('div.data-column:last-child').after('<div class="data-column" column="reload"/>');

                // show best placement and mythic equipment reload button if needed
                if (opponent.best_placement == undefined) {
                    const load_button = $('<button class="blue_button_L">↻</button>');
                    load_button.on('click', () => {
                        window.$.post({
                            url: '/ajax.php',
                            data: {
                                action: 'fetch_hero',
                                id: 'profile',
                                preview: false,
                                player_id: parseInt(opponent.player.id_fighter),
                            },
                            success: (data) => {}
                        })
                    })

                    $(this).find('div.data-column[column=reload]').html(load_button);
                }
            });

        // If all player data has been loaded we can remove the whole reload column
        this.removeReloadColumnIfNeeded()

        // re-run the league table sorting with the additional "power" sort option
        // var sort_options = { level: 'number', nb_challenges_played: 'number', power: 'number', expected_points: 'number', place: 'number'};
        // var sort_class = new TableSorting(opponents_list, 'id_player', 'place', sort_options, 'leagues', 'class');
        // sort_class.init();
    }

    onPlayerProfile(data) {
        const id = data.id;

        const idx = window.opponents_list.findIndex(x => parseInt(x.player.id_fighter) == id);
        const opponent = window.opponents_list[idx];

        const row = $($('div.league_table div.body-row').get(idx));

        // best placement indicator next to the nickname
        if (opponent.best_placement == 1) {
            row.find('div.data-column[column=nickname]')
            .append(`<span><span class="scriptLeagueInfoIcon top1"></span>${opponent.placement_count}</span>`)
        } else if (opponent.best_placement >= 2 && opponent.best_placement <= 4) {
            row.find('div.data-column[column=nickname]')
            .append(`<span><span class="scriptLeagueInfoIcon top4"></span>${opponent.placement_count}</span>`)
        }

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

        // remove reload column (optional)
        this.removeReloadColumnIfNeeded()
    }

    removeReloadColumnIfNeeded() {
        const reload_buttons = $('div.league_table div.body-row div.data-column[column=reload] button').length
        if (reload_buttons == 0) {
            $('div.league_table div.head-row div.head-column[column=reload]').remove();
            $('div.league_table div.body-row div.data-column[column=reload]').remove();
        }
    }

    showHideUnhideButton() {
        const hide = JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN)) || false;
        const caption = hide ? "Show" : "Hide";

        this.hideUnhide(hide);

        const btn = $(`<button id="beaten_opponents2" class="blue_button_L">${caption}</button>`);
        btn.on('click', () => {
            const next = !JSON.parse(storage.getItem(FOUGHT_OPPONENTS_HIDDEN));

            this.hideUnhide(next);
            storage.setItem(FOUGHT_OPPONENTS_HIDDEN, next);
            btn.text(next ? "Show" : "Hide");
        });

        $('div.league_buttons').append(btn);
    }

    hideUnhide(hide) {
        $('div.league_table')
            .find('div.body-row')
            .each(function(index) {
                const results = $(this).find('div.data-column[column=match_history]').find('div[class!="result "]').length;
                const fought_all = results == 3;
                if (fought_all && hide) {
                    $(this).hide();
                } else if (fought_all && !hide) {
                    $(this).show();
                }
            });

        $('#leagues .league_content .league_table').getNiceScroll().resize();
    }
}

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

    shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('leagues-pre-battle')}

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

        HHPlusPlus.Helpers.defer(() => {
            setTimeout(() => {
                this.checkMythicEquipment();
            }, 150);
        });

        this.hasRun = true;
    }

    checkMythicEquipment() {
        const me = EquipmentCollector.getBestMythic();
        const equipment_themes = me.map(x => x.resonance_bonuses.theme.identifier);

        // use 'div.player-team' for the opponent's team
        const synergies = JSON.parse($('div.player-panel div.player-team div.icon-area').attr('synergy-data'));
        const themes = synergies.filter(x => x.team_girls_count >=3).map(x => x.element.type);

        const has_matching_me = themes.some(t => equipment_themes.includes(t));
        if (has_matching_me) {
            window.popup_message("You have a perfect mythic equipment for this team in your inventory.")
        }
    }
}

class HaremFiltersModule extends MyModule {
    constructor () {
        const baseKey = 'haremFilters'
        const configSchema = {
            baseKey,
            default: true,
            label: `Show additional harem filters`,
        }
        super({name: baseKey, configSchema})
    }

    shouldRun() {return HHPlusPlus.Helpers.isCurrentPage('harem') && !HHPlusPlus.Helpers.isCurrentPage('hero')}

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

        HHPlusPlus.Helpers.defer(() => {
            // figure out the event ids for LD
            const ld_event_ids = $("select[name=event] option")
                                    .filter((idx, opt) => opt.text.includes("Legendary Days"))
                                    .map((idx, opt) => opt.value)
                                    .get()
                                    .map(id => parseInt(id));

            // mark the girls as LD girls
            Object.values(window.girlsDataList)
                  .filter(e => e.source_selectors.event)
                  .filter(e => e.source_selectors.event.filter(id => ld_event_ids.includes(id)).length)
                  .forEach(g => g.source_selectors.legendary_days = [0]); // TODO fill id, optional

            // add dropdown option
            $("select[name=lists]").append(new Option("Legendary Days", "legendary_days"));
        });

        this.hasRun = true;
    }
}

setTimeout(() => {
    const {hhPlusPlusConfig, HHPlusPlus, location} = window;

    if (!$) {
        console.log('No jQuery found. Probably an error page. Ending the script here')
        return;
    } else if (!hhPlusPlusConfig || !HHPlusPlus) {
        console.log("HH++ is not available");
        return;
    } else if (location.pathname === '/' && (location.hostname.includes('www') || location.hostname.includes('test'))) {
        console.log("iframe container, do nothing");
        return;
    }

    // collectors
    EquipmentCollector.collect();
    LeaguePlayersCollector.collect();
    TeamsCollector.collect();

    // modules
    const modules = [
        new LeagueScoutModule(),
        new LeagueTableModule(),
        new HaremFiltersModule(),
        new PrebattleFlightCheckModule(),
    ]

    // register our own window hooks
    window.HHPlusPlusPlus = {
        exportLeagueData: LeaguePlayersCollector.export,
        clearLeagueData: LeaguePlayersCollector.clear,
    };

    hhPlusPlusConfig.registerGroup({
        key: '430i',
        name: '430i\'s Scripts'
    })

    modules.forEach(module => hhPlusPlusConfig.registerModule(module))
    hhPlusPlusConfig.loadConfig()
    hhPlusPlusConfig.runModules()

    HHPlusPlus.Helpers.runDeferred()
}, 1)

 

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

I kinda miss the Olde Leagues GUI...

That said, the new GUI has some advantages, but they *MUST FIX* it!  There's a MASSIVE INFESTATION OF BUGS THAT NEEDS TO BE EXTERMINATED, POST HASTE!!

Furthermore, it strongly punctuated the GREATEST FLAW in the CXH Leagues;

image.thumb.png.c800458575257ea7684242259efddf80.png

Well, kinkoid, what do you have to say for yourself?

Kinkoid Games: "..."

image.thumb.png.4827232dbe806d8a4cf38455acd75ed7.png

Haah!  I thought so!  For SHAME, kinkoid!  For SHAME!!!

While we're at it, why did HH get 10 FISTS?  Why didn't CXH get 10 FISTS?  WTFFF is up with that?

  • Hug 1
Link to comment
Share on other sites

  • Moderator
14 hours ago, Karxan said:

CXH Leagues

Since this update applies to all HH clones (as far as I'm aware, I haven't checked them all, but all of the main ones at least), and it's a big topic as it is, I'm fine with players of CxH or other HH-based games participating in this HH thread rather than having multiple threads discussing the same core thing.

Having said that, the few bits of feedback that are specific to CxH and go beyond this specific patch we're discussing here shouldn't get in the way; better to discuss those few things in the CxH forum.

Cheers and hugs. Hope you get better soon.

  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...
On 8/2/2023 at 6:29 AM, Antimon said:

As I said in the patch note thread, I love the fact that yesterday they added the new league interface to test server, where it was clear sorting wasn't properly functioning at all, and they decided to add it straight away to the live server. Very kind of them. the booster bug is just the cherry on top

SSSSSSSSooo... Um, hey!  AANY word on this getting fixed?  The Level sorting being broken/not working in HH, CXH & PsH is a problem, slowing down gameplay.  I suspect it is broken in the 2-5 other kkgames.

  • Thinking 1
Link to comment
Share on other sites

9 minutes ago, Tom208 said:

yes it is not saved when you go back to the league leaderboard

But it did at some point, didn't it?

I'm sure I just sorted once from low to high level, and then worked my way up.
Was that from the Script, or could the game do it by itself?

Link to comment
Share on other sites

Seeing a name in know in league at the other end of the table the way I usually sort it, I can confirm the booster status of the game is complete rubbish. Until others have proven it wrong already, I thought that at least there are no boosters active if none are shown, but with 191k TP, 145 AP shouldn't be possible without at least 2 cordys, likely 3?

image.thumb.png.0338c31fb4dd0c0421d81c36f9aab01d.png

Also, I do now see cases where new boosters have been equipped before last snapshot (more than 6 hours ago), but stats are still unboosted:

image.thumb.png.334ca1f95c917909c5976be5ae4e397e.png

I already know before that new boosters are (usually) not effective until next snapshot, but did not record a clear case where even a snaphot did not make boosters effective. Then of course we know the expired boosters sometimes still being shown, sometimes not, immediately updating the particular opponent's snapshot.

So all a completely inconsistent mess. I asked this elsewhere already: Is KK informed, i.e. has there been a statement e.g. in Discord that the issue(s) are confirmed and a solution is worked on or so? It has been soon a month now ...

  • Hug 1
Link to comment
Share on other sites

  • Moderator
5 hours ago, Der DinX said:

I meant from before the Redesign

Sorting used to stick with the old UI, yes. You could sort once by "Challenged" for instance and it would show: yourself at the top of the list, then every opponent you haven't fought yet, and then all the ones you've already fought, sorted by their current ranks. And that would stick for days, no problem.

That was then, this is now, though. Right now the sorting doesn't stick.

4 hours ago, Horsting said:

So all a completely inconsistent mess. I asked this elsewhere already: Is KK informed, i.e. has there been a statement e.g. in Discord that the issue(s) are confirmed and a solution is worked on or so? It has been soon a month now ...

I'll attempt to answer this diplomatically. The bare game's league UI is still a big mess and barely usable, yes. Kinkoid knows about it, yes. Only a handful of people truly understand how it works and what's currently not working as competitive PvP players would like, though. And currently, even using three community scripts (HH++, MM's Leagues++ and 430i's HH+++) it's still not ideal, but still better than it used to be overall imho. The old design was worse overall, but we had it for so long that the one script (HH++) was sufficient to make consistent and reliable use of it. I don't expect the bare game's league design to ever improve to the point that it's sufficient on its own. So all in all, I'd rather have it stay as it is, so benevolent community script makers can fine tune their improvements over it more reliably.

  • Like 2
Link to comment
Share on other sites

  • Moderator
8 hours ago, Karxan said:

uh, NOPE! It will sort, but after the x1 or x3, it is unsorted & i'm at the top of the page.  THAT is still 100% BROKEN!

Or 'BROKKEN'...

I strongly agree with Tom, dude. You MUST CALM DOWN. I empathize with your RL and technical struggles, but even then, lashing out at everyone or anyone on the forum is neither healthy nor okay. Try using your own empathy a bit more. I'm not going to send you a formal warning as you're already on the short list of forum users who are kept around solely by mods and thanks to mods filtering your posts. But we can't filter dozens of posts at once when you're posting all over the place, and you've been so salty over the past few weeks the decision to approve or deny a post proposal from you is becoming harder and harder.

Take a deep breath and remember we're your forum pals, here. And we're all human beings with our own real life and, in many cases, our own struggles behind the scenes too. Don't yell at someone answering a question you've asked just because you're unhappy about something else. Thank them for helping you out.

  • Like 2
Link to comment
Share on other sites

19 hours ago, DvDivXXX said:

So all in all, I'd rather have it stay as it is, so benevolent community script makers can fine tune their improvements over it more reliably.

I find it unacceptable to just accept that the game is only usable with multiple community members donating their spare time to make it usable, and keep their scripts functional even that KK continuously changes HTML elements and stuff to break scripts over and over again. I mean if it is like that, it is like that, but at least we should try to make KK fixing things by themselves. I do not mean another re-design, I just mean to fix the obvious bugs: If boosters are shown already, then this should have a relevant meaning and represent what you are fighting against, is this really too much to expect from something your are/were spending money for (in my case at least)? I should pay Zoo, MM and 430i instead ...

If there is zero hope for this mess to be fixed by KK, then I think we should revive the booster detector, and make it replace the current booster indicators completely with own ones, instead of just doing some damage control with highlighting effects. This idea is actually the reason why I was asking, checking out whether working on this is worth it or not.

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

29 minutes ago, Master-17 said:

I no longer remember what you came up with above about "ended" boosters, but when checking today after the end of the timer, the stats actually decreased and the chances of winning and getting higher results increased.

This always happened in most of the cases: When the timer ends, the snapshot is updated. But when new boosters are equipped, it is usually not. However, there are cases where it happens the other way round, so pretty random.

But that is all the game, not the script: HH++ only adds the red/fading highlight effect to expired boosters. But you cannot fully trust that this always means that you fight against an unboosted enemy.

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

  • Moderator

Indeed. I've had opponents whose boosters were shown as brand-new in the list but when checking their actual stats (and fighting them) they were actually unboosted in the applicable league snapshot at the time (no later than half an hour ago, even: I fought someone who supposedly had 4 brand-new cordys equipped but their attack stat begged to differ).

I also had the reverse situation plenty of times (more often I feel): an opponent supposed to be unboosted according to the list but whose snapshot stats were actually 100% boosted.

While it's still a helpful hint (especially earlier in the league cycle when you have a lot of opponents to sift through), it's not a reliable indicator by any stretch of the imagination.

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