// ==UserScript== // @name Hentai Heroes++ League Booster Detector Add-on // @description Adding detection of boosters to league. // @version 0.2.5 // @match https://*.hentaiheroes.com/* // @match https://nutaku.haremheroes.com/* // @match https://www.gayharem.com/* // @match https://nutaku.gayharem.com/* // @match https://*.comixharem.com/* // @match https://*.hornyheroes.com/* // @match https://*.pornstarharem.com/* // @run-at document-end // @updateURL https://raw.githubusercontent.com/zoop0kemon/hh-booster-detector/main/hh-booster-detector.js // @downloadURL https://raw.githubusercontent.com/zoop0kemon/hh-booster-detector/main/hh-booster-detector.js // @grant none // @author 45026831(Numbers), zoopokemon // ==/UserScript== /* =========== CHANGELOG =========== */ // 0.2.5: Fixing collection of opponent player equip data // 0.2.4: Fixes for variable name and behavior changes after 22/03 update // 0.2.3: Adding pending indication while profile data is being loaded // 0.2.2: Adding tooltips on mobile // 0.2.1: Fixing ginseng check in the corner-case where profile stats don't match snapshot // 0.2.0: Changing stat collection to work with mythic items // 0.1.14: Improving harem endurance bonus calculation now that the game calculates it properly // 0.1.13: Taking ego dominance bonus into account now that it's incuded in the opponent's stats // 0.1.12: Fixing ego check after game update // 0.1.11: Adding PSH matcher for Weds official release // 0.1.10: re added team girl counts when missing // 0.1.9: Reversing sun and dominance attack bonuses to fix base stat estimates // 0.1.8: Pre-empting change to playerLeaguesData currently being tested on TS. Using now-exposed opponent synergies to improve accuracy. // 0.1.7: Applying the club bonus to harem level, fixing rainbow stat per level magic number // 0.1.6: Emergency fixes for camelCase vars renamed to snake_case // 0.1.5: Adding matcher for CxH // 0.1.4: Accounting for element synergy bonuses in stats // 0.1.3: Removing manual summing of girl stats, using totalPower value instead // 0.1.2: Adding support for mobile // 0.1.1: Adding back text stroke on boosted stats to be easier on the eyes. Adding matchers for GH // 0.1.0: Major refactor around how results are displayed, slight boosts are shown in orange instead of full red, logs disabled by default with details moved into tooltips // 0.0.17: Improving calculations in the estimate scenario using formulae from zoopokemon // 0.0.16: Improving accuracy of monostat calculation by using both attack and harmony // 0.0.15: Adding back chlorella checking in estimate scenario, cleaning up magic numbers into self-documenting code // 0.0.14: Fixing bug in secondary and tertiary stats for display // 0.0.13: Cleaning up the maths on estimations // 0.0.12: If base stats not available, attempt to work backwards from derived stats, display estimates in stat tooltips // 0.0.11: Making use of new tooltip data attribute // 0.0.10: Removing unrounded value from cordy debug output // 0.0.9: Fixing typo in Cordy check // 0.0.8: Changing to match new calculations for BDSM // 0.0.7: Setting an upper limit of 6 on the monostat count calculation // 0.0.6: Adjusting Jujubes formula to more accurately reflect actual equipment stats // 0.0.5: Adding booster icons in the table (after checks have run) // 0.0.4: Adding detection for Jujubes // 0.0.3: Adding detection for Ginseng // 0.0.2: Updated Chlorella check to use Alpha's main stat instead of Alpha's Player Class stat // 0.0.1: Initial version. Adding detection for Chlorella and Cordyceps // Define jQuery const { $, location } = window; const LOGS_ENABLED = false if (!$) { console.log('HH++ BOOSTER DETECTOR WARNING: No jQuery found. Probably an error page. Ending the script here'); return; } // Define CSS const sheet = (function () { const style = document.createElement('style') document.head.appendChild(style) return style.sheet })() const currentPage = location.pathname const profileDataCache = {} const HC = 1 const CH = 2 const KH = 3 const classRelationships = { [HC]: { s: KH, t: CH }, [CH]: { s: HC, t: KH }, [KH]: { s: CH, t: HC } } const lang = $('html')[0].lang.substring(0, 2) let locale = 'fr' if (lang === 'en') { locale = 'en' } // Magic numbers const RAINBOW_STAT_PER_LEVEL = 7 const RAINBOW_HARM_PER_LEVEL = 9.1 const RAINBOW_HARM_BASE = 90 const MONOSTAT_PER_LEVEL = 11.15 const PRIMARY_PER_LEVEL = 9 + 30 const SECONDARY_PER_LEVEL = 7 + 30 const TERTIARY_PER_LEVEL = 5 + 30 const ENDURANCE_PER_PRIMARY = 4 const DEFENCE_PER_NON_PRIM = 0.25 const HARMONY_PER_NON_PRIM = 0.5 const MIN_POTENTIAL_MONOSTAT = 0 const MAX_POTENTIAL_MONOSTAT = 6 const HAREM_BONUS_PER_LVL = 52 const RAINBOW_MONO_DIFF = MONOSTAT_PER_LEVEL - RAINBOW_STAT_PER_LEVEL const FULL_RAINBOW_PER_LEVEL = 6 * RAINBOW_STAT_PER_LEVEL const LEGENDARY_CHLORELLA = 10 const LEGENDARY_CORDYCEPS = 10 const LEGENDARY_GINSENG = 6 const LEGENDARY_JUJUBES = 20 const GIRLS_PER_LEVEL_CAP = [0, 0, 0, 0, 0, 20, 25, 30, 35, 40, 50, 60, 70, 85, 100] const boundMonostatCount = (count) => Math.max(MIN_POTENTIAL_MONOSTAT, Math.min(MAX_POTENTIAL_MONOSTAT, count)) const getClubBonus = (hasClub, multiplier = 1) => hasClub ? 1 + (0.1 * multiplier) : 1 const estimateUnboostedEnduranceForLevel = (level, monostatCount, hasClub, equipCaracs, opponentClass) => { const basePrimary = basePrimaryStatForLevel(level) const equipPrimary = equipCaracs ? equipCaracs[`carac${opponentClass}`] : equipPrimaryStatForLevel(level, monostatCount) const equipEndurance = equipCaracs ? equipCaracs.endurance : rainbowStatForLevel(level, 6 - monostatCount) const haremBonus = caculateHaremBonus(estimateHaremLevel(level)) const clubBonus = getClubBonus(hasClub) const comboClubBonus = getClubBonus(hasClub, 2) return Math.round(ENDURANCE_PER_PRIMARY * basePrimary * comboClubBonus) + Math.round(ENDURANCE_PER_PRIMARY * equipPrimary * clubBonus) + equipEndurance * clubBonus + haremBonus * clubBonus } const basePrimaryStatForLevel = (level) => level * PRIMARY_PER_LEVEL const equipPrimaryStatForLevel = (level, monostatCount) => rainbowStatForLevel(level, 6 - monostatCount) + monostatStatForLevel(level, monostatCount) const rainbowStatForLevel = (level, count) => level * (count * RAINBOW_STAT_PER_LEVEL) const monostatStatForLevel = (level, count) => level * (count * MONOSTAT_PER_LEVEL) const estimateUnboostedPrimaryStatForLevel = (level, monostatCount, hasClub, equipCaracs, opponentClass) => (basePrimaryStatForLevel(level) + (equipCaracs ? equipCaracs[`carac${opponentClass}`] : equipPrimaryStatForLevel(level, monostatCount))) * getClubBonus(hasClub) const estimateHaremBonusForLevel = (level) => level * HAREM_BONUS_PER_LVL const caculateHaremBonus = (haremLevel) => Math.round(Math.sqrt(haremLevel) * 50) const calculateExtraPercent = (expected, actual) => Math.round(((actual - expected) / expected) * 100) const buildResultTooltip = (existingContent, caracs, expected, actual, extraPercent) => `${existingContent ? existingContent : ''}
Expected ${caracs.map(carac => ``).join('+')}:${Math.round(expected).toLocaleString(locale)}
Actual ${caracs.map(carac => ``).join('+')}:${Math.round(actual).toLocaleString(locale)}
${extraPercent > 0 ? `Extra: ${extraPercent}%` : ''} ` function estimateHaremLevel(level) { const { opponent_fighter, loadedLeaguePlayers } = window let opponent_data if (Object.keys(loadedLeaguePlayers)?.length) { let opponentId = $('#leagues_right .avatar_border>img').attr('hero-page-id') opponent_data = loadedLeaguePlayers[opponentId].player } else { opponent_data = opponent_fighter.player } const teamLevels = opponent_data.team.girls.map(({ level }) => level) const teamLevel = teamLevels.reduce((a, b) => a + b, 0) const teamCount = teamLevels.length const girlsCount = opponent_data.team.synergies.map(({ harem_girls_count }) => harem_girls_count).reduce((a, b) => a + b, 0) if (girlsCount <= 7) { if (girlsCount == teamCount) { return teamLevel } else { return girlsCount * (teamLevel / teamCount) } } else { const levelCap = Math.ceil(Math.max(...teamLevels) / 50) * 50 const min_girls = GIRLS_PER_LEVEL_CAP[levelCap / 50 - 1] if (levelCap <= 250 || (levelCap == 350 && girlsCount < min_girls)) { return girlsCount * (teamLevel / teamCount) } else { let bias = teamLevels.reduce((a, b) => a + (b - (levelCap - 50)), 0) return Math.min((min_girls * (levelCap - 50)) + (0.5 * level * girlsCount) + bias, levelCap * girlsCount) } } } function findBonusFromSynergies(synergies, element, teamGirlSynergyBonusesMissing, counts) { const { bonus_multiplier, team_bonus_per_girl } = synergies.find(({ element: { type } }) => type === element) return bonus_multiplier + (teamGirlSynergyBonusesMissing ? counts[element] * team_bonus_per_girl : 0) } const ELEMENTS = { egoDamage: { fire: 'nature', nature: 'stone', stone: 'sun', sun: 'water', water: 'fire' } } function calculateDominationBonuses(playerElements, opponentElements) { const bonuses = { opponent: { ego: 0, attack: 0, } }; [ { a: opponentElements, b: playerElements, k: 'opponent' } ].forEach(({ a, b, k }) => { a.forEach(element => { if (ELEMENTS.egoDamage[element] && b.includes(ELEMENTS.egoDamage[element])) { bonuses[k].ego += 0.1 bonuses[k].attack += 0.1 } }) }) return bonuses } async function getEquipDataFromProfile(playerId) { if (!profileDataCache[playerId]) { const html = await new Promise((res) => { window.$.ajax({ url: `/hero/${playerId}/profile.html`, success: res }) }) const $page = $(html) const equip_code = $page.find('script').text() const equips = Object.values(JSON.parse(equip_code.match(/{.*}/)[0])) const stats = {} const CARAC_KEYS = ['1', '2', '3', 'endurance', 'chance'] CARAC_KEYS.forEach(carac => { const caracVal = $page.find(`.fight_stats [carac=${carac}]`).text() stats[carac] = +caracVal.replace(/[^0-9]/g, '') }) profileDataCache[playerId] = { equips, stats } } return profileDataCache[playerId] } if (currentPage.includes('tower-of-fame')) { boosterModule() } function boosterModule() { let opponent_data let opponentLvl let opponentEgo let opponentMainStat let opponentScndStat let opponentTertStat let opponentNonMainStatSum let opponentAtk let opponentDef let opponentEndurance let opponentHarmony let opponentClass let opponentMonostatCount let opponentHasClub let opponentGirlSum let opponentBonuses let isEstimate let $attack let $ego let $defense let $harmony let $attackMobile let $egoMobile let $defenseMobile let $harmonyMobile let ownDefenseReductionAdjustment = 0 let attackDominanceBonusAdjustment = 0 let egoDominanceBonusAdjustment = 0 let attackResonanceBonusAdjustment = 0 let egoResonanceBonusAdjustment = 0 let harmonyResonanceBonusAdjustment = 0 let equipCaracs async function getStats() { const { opponent_fighter, hero_fighter, loadedLeaguePlayers, GT } = window if (Object.keys(loadedLeaguePlayers)?.length) { let opponentId = $('#leagues_right .avatar_border>img').attr('hero-page-id') opponent_data = loadedLeaguePlayers[opponentId].player } else { opponent_data = opponent_fighter.player } opponentHasClub = !!(opponent_data.club && opponent_data.club.id_club) opponentLvl = parseInt(opponent_data.level, 10) opponentClass = opponent_data.class const { caracs } = opponent_data if (caracs) { opponentEgo = caracs.ego opponentMainStat = caracs[`carac${opponentClass}`] opponentScndStat = caracs[`carac${classRelationships[opponentClass].s}`] opponentTertStat = caracs[`carac${classRelationships[opponentClass].t}`] opponentEndurance = caracs.endurance opponentAtk = caracs.damage opponentHarmony = caracs.chance opponentDef = caracs.defense } else { opponentEgo = opponent_data.total_ego || opponent_data.remaining_ego opponentMainStat = opponent_data[`carac${opponentClass}`] opponentScndStat = opponent_data[`carac${classRelationships[opponentClass].s}`] opponentTertStat = opponent_data[`carac${classRelationships[opponentClass].t}`] opponentEndurance = opponent_data.endurance opponentAtk = opponent_data.damage opponentHarmony = opponent_data.chance opponentDef = opponent_data.defense } isEstimate = false const { equips, stats } = await getEquipDataFromProfile(opponent_data.id_fighter) const { team } = opponent_data const { synergies, total_power } = team opponentGirlSum = total_power const teamGirlSynergyBonusesMissing = synergies.every(({ team_girls_count }) => !team_girls_count) let counts if (teamGirlSynergyBonusesMissing) { console.log('Opponent Team Girl Synergy Bonuses Missing') const opponentTeamMemberElements = []; [0, 1, 2, 3, 4, 5, 6].forEach(key => { const teamMember = team.girls[key] if (teamMember && teamMember.element) { opponentTeamMemberElements.push(teamMember.element) } }) 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 }) } opponentBonuses = { attack: findBonusFromSynergies(synergies, 'darkness', teamGirlSynergyBonusesMissing, counts), defense: findBonusFromSynergies(synergies, 'light', teamGirlSynergyBonusesMissing, counts), harmony: findBonusFromSynergies(synergies, 'psychic', teamGirlSynergyBonusesMissing, counts), ego: findBonusFromSynergies(synergies, 'nature', teamGirlSynergyBonusesMissing, counts), } ownDefenseReductionAdjustment = findBonusFromSynergies(hero_fighter.team.synergies, 'sun') const [heroTheme, opponentTheme] = [hero_fighter, opponent_data].map(data => data.team.theme_elements.map(({ type }) => type)) const dominanceBonuses = calculateDominationBonuses(heroTheme, opponentTheme) const mythicEquipBonuses = { damage: 0, defense: 0, chance: 0, ego: 0, } equipCaracs = { carac1: 0, carac2: 0, carac3: 0, endurance: 0, chance: 0 } const CARAC_KEYS = ['carac1', 'carac2', 'carac3', 'endurance', 'chance'] equips.forEach(equip => { if (equip.resonance_bonuses) { const { class: classBonus, theme } = equip.resonance_bonuses const matchesClass = `${classBonus.identifier}` === `${opponentClass}` if (matchesClass) { mythicEquipBonuses[classBonus.resonance] += (+classBonus.bonus / 100) } const matchesTheme = (theme.identifier && opponentTheme.includes(theme.identifier)) || (!theme.identifier && !opponentTheme.length) if (matchesTheme) { mythicEquipBonuses[theme.resonance] += (+theme.bonus / 100) } } CARAC_KEYS.forEach(caracKey => { equipCaracs[caracKey] += +equip[`${caracKey}_equip`] }) }) console.log('calculated mythic equip bonuses', mythicEquipBonuses) const rawHarmony = Math.ceil(stats.chance * (1 + opponentBonuses.harmony) * (1 + mythicEquipBonuses.chance)) console.log('profile harm', rawHarmony, 'page harm', opponentHarmony, '(synergy:', opponentBonuses.harmony, '; equips:', mythicEquipBonuses.chance, '; total:', opponentHarmony, ')') const statsMatch = rawHarmony + 1 >= opponentHarmony && rawHarmony - 1 <= opponentHarmony console.log('stats match?', statsMatch) console.log('equip caracs', equipCaracs) if (statsMatch) { opponentMainStat = stats[opponentClass] opponentScndStat = stats[classRelationships[opponentClass].s] opponentTertStat = stats[classRelationships[opponentClass].t] opponentEndurance = stats.endurance } attackDominanceBonusAdjustment = dominanceBonuses.opponent.attack egoDominanceBonusAdjustment = dominanceBonuses.opponent.ego attackResonanceBonusAdjustment = mythicEquipBonuses.damage egoResonanceBonusAdjustment = mythicEquipBonuses.ego harmonyResonanceBonusAdjustment = mythicEquipBonuses.chance if (!opponentMainStat) { opponentMainStat = Math.ceil((((opponentAtk / (1 + attackDominanceBonusAdjustment)) / (1 + opponentBonuses.attack)) / (1 + mythicEquipBonuses.damage)) - (opponentGirlSum * 0.25)) isEstimate = true } if (!opponentScndStat) { opponentNonMainStatSum = ((((opponentDef / (1 - ownDefenseReductionAdjustment)) / (1 + opponentBonuses.defense)) / (1 + mythicEquipBonuses.defense)) - (opponentGirlSum * 0.12)) * 4 isEstimate = true } else { opponentNonMainStatSum = opponentScndStat + opponentTertStat } if (!opponentEndurance) { opponentEndurance = Math.ceil((((opponentEgo / 1 + egoDominanceBonusAdjustment) / (1 + opponentBonuses.ego)) / (1 + mythicEquipBonuses.ego)) - (opponentGirlSum * 2)) isEstimate = true } if (!isEstimate) { const statRatio = opponentScndStat / opponentMainStat opponentMonostatCount = boundMonostatCount(Math.round( ((SECONDARY_PER_LEVEL + FULL_RAINBOW_PER_LEVEL) - ((PRIMARY_PER_LEVEL + FULL_RAINBOW_PER_LEVEL) * statRatio)) / ((RAINBOW_MONO_DIFF * statRatio) + RAINBOW_STAT_PER_LEVEL) )) } else { const statRatio = opponentNonMainStatSum / opponentMainStat const monostatCountFromAttack = Math.round( ((TERTIARY_PER_LEVEL + SECONDARY_PER_LEVEL + (2 * FULL_RAINBOW_PER_LEVEL)) - ((PRIMARY_PER_LEVEL + FULL_RAINBOW_PER_LEVEL) * statRatio)) / ((RAINBOW_MONO_DIFF * statRatio) + 2 * RAINBOW_STAT_PER_LEVEL) ) const monostatCountFromHarmony = Math.round( MAX_POTENTIAL_MONOSTAT - ( ((opponentHarmony / (1 + opponentBonuses.harmony)) - opponentNonMainStatSum / 2) / ((RAINBOW_HARM_BASE + (opponentLvl * RAINBOW_HARM_PER_LEVEL)) * getClubBonus(opponentHasClub)) ) ) opponentMonostatCount = boundMonostatCount( Math.min(monostatCountFromAttack, monostatCountFromHarmony) ) } if (!opponentScndStat && isEstimate) { // Estimate sec and tert stats for display const secPerLevel = (SECONDARY_PER_LEVEL + (RAINBOW_STAT_PER_LEVEL * (6 - opponentMonostatCount))) const tertPerLevel = (TERTIARY_PER_LEVEL + (RAINBOW_STAT_PER_LEVEL * (6 - opponentMonostatCount))) const secShare = secPerLevel / (secPerLevel + tertPerLevel) const tertShare = tertPerLevel / (secPerLevel + tertPerLevel) opponentScndStat = Math.ceil(opponentNonMainStatSum * secShare) opponentTertStat = Math.ceil(opponentNonMainStatSum * tertShare) } $attack = $('#leagues_right .player_stats #player_attack_stat') $ego = $('#leagues_right .player_stats #player_ego_stat') $defense = $('#leagues_right .player_stats #player_defence_stat') $harmony = $('#leagues_right .player_stats #player_harmony_stat') $attackMobile = $('.selected-player-leagues .carac.attack') $egoMobile = $('.selected-player-leagues .carac.excitement') // [sic] $defenseMobile = $('.selected-player-leagues .carac.def0') $harmonyMobile = $('.selected-player-leagues .carac.harmony') const existingAttackTooltip = $attack.attr('tooltip') const existingDefenceTooltip = $defense.attr('tooltip') let existingEgoTooltip = $ego.attr('tooltip') let existingHarmonyTooltip = $harmony.attr('tooltip') if (existingEgoTooltip === '##carac_ego') { existingEgoTooltip = GT.ego } if (existingHarmonyTooltip === '!!HH_design:carac_chance!!') { existingHarmonyTooltip = GT.chance } const attackTooltip = `${existingAttackTooltip}
${isEstimate ? 'Estimate ' : ''} ${opponentMainStat.toLocaleString(locale)}
Monostat count: ${opponentMonostatCount}` $attack.attr('tooltip', attackTooltip) $attackMobile.attr('tooltip', attackTooltip) const defenseTooltip = `${existingDefenceTooltip}
${isEstimate ? 'Estimate ' : ''} ${opponentScndStat.toLocaleString(locale)}
${isEstimate ? 'Estimate ' : ''} ${opponentTertStat.toLocaleString(locale)}` $defense.attr('tooltip', defenseTooltip) $defenseMobile.attr('tooltip', defenseTooltip) const egoTooltip = `${existingEgoTooltip}
${isEstimate ? 'Estimate ' : ''} ${opponentEndurance.toLocaleString(locale)}` $ego.attr('tooltip', egoTooltip) $egoMobile.attr('tooltip', egoTooltip) $harmony.attr('tooltip', existingHarmonyTooltip) $harmonyMobile.attr('tooltip', existingHarmonyTooltip) } function checkChlorella() { let expectedEgo if (!isEstimate) { expectedEgo = Math.ceil((opponentEndurance + (2 * opponentGirlSum)) * (1 + opponentBonuses.ego) * (1 + egoDominanceBonusAdjustment) * (1 + egoResonanceBonusAdjustment)) } else { const expectedEndurance = estimateUnboostedEnduranceForLevel(opponentLvl, opponentMonostatCount, opponentHasClub, equipCaracs, opponentClass) expectedEgo = Math.ceil((expectedEndurance + (2 * opponentGirlSum)) * (1 + opponentBonuses.ego) * (1 + egoDominanceBonusAdjustment) * (1 + egoResonanceBonusAdjustment)) } const extraPercent = calculateExtraPercent(expectedEgo, opponentEgo) if (LOGS_ENABLED) console.log(`CHLORELLA CHECK: Expected: ${expectedEgo}, Actual: ${opponentEgo}, Extra: ${extraPercent}%`); const existingTooltip = $ego.attr('tooltip') const newTooltip = buildResultTooltip(existingTooltip, ['ego'], expectedEgo, opponentEgo, extraPercent) $ego.attr('tooltip', newTooltip) $egoMobile.attr('tooltip', newTooltip) if (extraPercent > 0) { let boosted = 'boosted' if (extraPercent < 0.25 * LEGENDARY_CHLORELLA) { boosted = 'boosted_light' } $ego.addClass(boosted) $egoMobile.addClass(boosted) } } function checkCordyceps() { let expectedAttack if (!isEstimate) { const expectedUnrounded = opponentMainStat + (0.25 * opponentGirlSum) expectedAttack = Math.ceil(expectedUnrounded * (1 + opponentBonuses.attack) * (1 + attackDominanceBonusAdjustment) * (1 + attackResonanceBonusAdjustment)) } else { const expectedMainStat = estimateUnboostedPrimaryStatForLevel(opponentLvl, opponentMonostatCount, opponentHasClub, equipCaracs, opponentClass) expectedAttack = Math.ceil((expectedMainStat + (0.25 * opponentGirlSum)) * (1 + opponentBonuses.attack) * (1 + attackDominanceBonusAdjustment) * (1 + attackResonanceBonusAdjustment)) } const extraPercent = calculateExtraPercent(expectedAttack, opponentAtk) if (LOGS_ENABLED) console.log(`CORDYCEPS CHECK: Expected: ${expectedAttack}, Actual: ${opponentAtk}, Extra: ${extraPercent}%`); const existingTooltip = $attack.attr('tooltip') const newTooltip = buildResultTooltip(existingTooltip, ['damage'], expectedAttack, opponentAtk, extraPercent) $attack.attr('tooltip', newTooltip) $attackMobile.attr('tooltip', newTooltip) if (extraPercent > 0) { let boosted = 'boosted' if (extraPercent < 0.25 * LEGENDARY_CORDYCEPS) { boosted = 'boosted_light' } $attack.addClass(boosted) $attackMobile.addClass(boosted) } } function checkGinseng() { let expectedMainStat let expectedNonMainStatSum let extraPercent if (!isEstimate) { expectedMainStat = estimateUnboostedPrimaryStatForLevel(opponentLvl, opponentMonostatCount, opponentHasClub, equipCaracs, opponentClass) extraPercent = calculateExtraPercent(expectedMainStat, opponentMainStat) } else { expectedNonMainStatSum = (opponentLvl * (SECONDARY_PER_LEVEL + TERTIARY_PER_LEVEL)) + equipCaracs[`carac${classRelationships[opponentClass].s}`] + equipCaracs[`carac${classRelationships[opponentClass].t}`] extraPercent = Math.round( ((opponentNonMainStatSum / expectedNonMainStatSum) - getClubBonus(opponentHasClub)) * 100 ) } if (LOGS_ENABLED) console.log(`GINSENG CHECK: Expected: ${isEstimate ? expectedNonMainStatSum : expectedMainStat}, Actual: ${isEstimate ? opponentNonMainStatSum : opponentMainStat}, Extra: ${extraPercent}%, Monostat count: ${opponentMonostatCount}, Has club: ${opponentHasClub}`); const existingTooltip = $defense.attr('tooltip') const newTooltip = buildResultTooltip( existingTooltip, isEstimate ? [`class${classRelationships[opponentClass].s}`, `class${classRelationships[opponentClass].t}`] : [`class${opponentClass}`], isEstimate ? expectedNonMainStatSum * getClubBonus(opponentHasClub) : expectedMainStat, isEstimate ? opponentNonMainStatSum : opponentMainStat, extraPercent) $defense.attr('tooltip', newTooltip) $defenseMobile.attr('tooltip', newTooltip) if (extraPercent > 0) { let boosted = 'boosted' if (extraPercent < 0.25 * LEGENDARY_GINSENG) { boosted = 'boosted_light' } $defense.addClass(boosted) $defenseMobile.addClass(boosted) } } function checkJujubes() { const clubBonus = getClubBonus(opponentHasClub) const expectedUnrounded = ((opponentNonMainStatSum * 0.5 * clubBonus) + equipCaracs.chance) * (1 + opponentBonuses.harmony) * (1 + harmonyResonanceBonusAdjustment) const expectedHarmony = Math.ceil(expectedUnrounded) const extraPercent = calculateExtraPercent(expectedHarmony, opponentHarmony) if (LOGS_ENABLED) console.log(`JUJUBES CHECK: Expected: ${expectedHarmony}, Actual: ${opponentHarmony}, Extra: ${extraPercent}%, Monostat count: ${opponentMonostatCount}, Has club: ${opponentHasClub}`); const existingTooltip = $harmony.attr('tooltip') const newTooltip = buildResultTooltip(existingTooltip, ['chance'], expectedHarmony, opponentHarmony, extraPercent) $harmony.attr('tooltip', newTooltip) $harmonyMobile.attr('tooltip', newTooltip) if (extraPercent > 0) { let boosted = 'boosted' if (extraPercent < 0.25 * LEGENDARY_JUJUBES) { boosted = 'boosted_light' } $harmony.addClass(boosted) $harmonyMobile.addClass(boosted) } } async function checkBoosters() { const $statsContainer = $('#leagues_right .player_stats') $statsContainer.addClass('booster-detector-pending') await getStats() checkCordyceps() checkChlorella() checkGinseng() checkJujubes() $statsContainer.removeClass('booster-detector-pending') } // Observer grabbed from HH++ let opntName; $('.leadTable').click(function () { opntName = '' }) function waitOpnt() { setTimeout(function () { if ($('#leagues_right .team-member > img').eq(3).data('new-girl-tooltip')) { checkBoosters() } else { waitOpnt() } }, 50); } const observeCallback = function () { const opntNameNew = $('#leagues_right .leagues_team_block .title')[0].innerHTML if (opntName !== opntNameNew) { opntName = opntNameNew waitOpnt() } } const observer = new MutationObserver(observeCallback); const test = document.getElementById('leagues_right'); observer.observe(test, { attributes: false, childList: true, subtree: false }); sheet.insertRule(` #leagues_right .boosted, .selected-player-leagues .boosted { color: #FF2F2F; text-shadow: rgb(0, 0, 0) 1px 1px 0px, rgb(0, 0, 0) -1px 1px 0px, rgb(0, 0, 0) -1px -1px 0px, rgb(0, 0, 0) 1px -1px 0px; } `); sheet.insertRule(` #leagues_right .boosted_light, .selected-player-leagues .boosted_light { color: #FFA500; text-shadow: rgb(0, 0, 0) 1px 1px 0px, rgb(0, 0, 0) -1px 1px 0px, rgb(0, 0, 0) -1px -1px 0px, rgb(0, 0, 0) 1px -1px 0px; } `); sheet.insertRule(` .booster-detector-pending { color: #aaa; } `); }