Hentai Heroes SFW

Removing explicit images in Hentai Heroes game and changing game background to a SFW one.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Hentai Heroes SFW
// @namespace    https://sleazyfork.org/fr/scripts/539097-hentai-heroes-sfw
// @description  Removing explicit images in Hentai Heroes game and changing game background to a SFW one.
// @version      4.2.0
// @match        https://*.hentaiheroes.com/*
// @run-at       document-start
// @grant        none
// @author       Geto_hh
// @license      MIT
// ==/UserScript==

// ==CHANGELOG==
// 4.2.0: refactor of imagesSrcToReplace and cleanup
// 4.1.8: fix champions css due to display none to opacity 0 change
// 4.1.7: revert background-image css processing back to background-image instead of opacity
// 4.1.6: refactor setElementsDisplay to modifyElementsStyle
// 4.1.5: fix removed background-image css (opacity instead of display)
// 4.1.4: fix removed image css (opacity instead of display)
// 4.1.3: fix button position
// 4.1.2: fix button position
// 4.1.1: fix panel position
// 4.1.0: show NSFW icon when every settings are false
// 4.0.1: various fixes
// 4.0.0: Add settings icon and panel
// 3.12.1: Fix affection scenes
// 3.12.0: Update README.md
// 3.11.0: Update description
// 3.10.0: Hide login background
// 3.9.0: Hide champion's club girl avatars
// 3.8.0: Hide login video
// 3.7.0: Hide level up pop-up girl
// 3.6.1: Fix club champion css
// 3.6.0: Hide champions
// 3.5.0: Hide lse girl
// 3.4.0: Hide images in harem
// 3.3.2: Update description
// 3.3.1: Update description
// 3.3.0: Optimize code
// 3.2.0: Optimize and mutualize code, fix bugs (empty selector crash, dead ternary, QUEST TypeError, unused constant)
// 3.1.0: Refactor code to mutualize page lists
// 3.0.0: Remove girl img src modifications and observer due to girl media url changes that no longer allow the process
// 2.2.0: Add waifu page support
// 2.1.6: Fix event selectors
// 2.1.5: Fix event selectors
// 2.1.4: Fix harem selectors
// 2.1.3: Fix harem selectors
// 2.1.2: Fix harem selectors
// 2.1.1: Fix mobile login image removal
// 2.1.0: Remove mobile images
// 2.0.6: Trying Github webhook again
// 2.0.5: Trying Github webhook
// 2.0.4: Updating description
// 2.0.3: Fixed login screen img removal
// 2.0.2: Fixed champions css positions and mythic day lively scene
// 2.0.1: Fixed Harem avatar modification
// 2.0.0: Improved observer handling and delaying
// 1.12.2: Fix modify script
// 1.12.1: Fix various pages
// 1.12.0: Add penta-drill support
// 1.11.2: Add new pages and fix champions page display
// 1.11.1: Add leagues page support
// 1.11.0: Split avatars, icons & girl images
// 1.10.1: Fix css selectors
// 1.10.0: Add option to replace background
// 1.9.0: Add option to hide avatars
// 1.8.0: Add option to hide girls
// 1.7.0: Put observer back for girls and home background
// 1.6.0: Use style to hide images and background images
// 1.5.0: Split hide process and modify process & remove observer
// 1.4.0: Hide girls on the event and refill pop-up
// 1.3.0: Replace home page background image to alway have the same one and avoid NSFW ones
// 1.2.0: Allow user to show affection scene when clicking on eye icon
// 1.1.0: Page per page query selectors and optimized girl icons processing on edit team pages
// 1.0.1: Small fixes
// 1.0.0: Optimize script for release
// 0.6.0: Use only document query selectors and run continously
// 0.5.0: Optimized script for faster processing
// 0.4.0: Stop processing mutations when a 'diamond' or 'speech_bubble_info_icn' class element is clicked
// 0.3.0: Run script only once per page load
// 0.2.0: Added namespace
// 0.1.0: First available version on SleazyFork
// ==/CHANGELOG==

/**
 * CONFIGURATION
 */
const DEBUG_LIMIT_ACTIVATED = false;
const HIDE_ADDS = false;
const HIDE_BACKGROUND = false;

/**
 * VARIABLES
 */
// let is required — DEBUG_ACTIVATED is reassigned to false inside checkDebugLimit()
let DEBUG_ACTIVATED = true; // eslint-disable-line no-var
let debugLimitCount = 0;
let isCssInjected = false;
let isDOMReady = false;

/**
 * HHSFW SETTINGS — persisted in localStorage under the key 'HHSFW.settings'.
 * Defaults match the original hardcoded values.
 */
const HHSFW_DEFAULTS = {
  HIDE_EVENT_GIRLS_AVATARS : true,
  HIDE_HAREM_SELECTED_GIRL_AVATAR : true,
  HIDE_OTHER_GIRLS_AVATARS : true,
  HIDE_OTHER_PLAYERS_AVATARS : true,
  HIDE_VIDEO_PREVIEWS : true,
  REPLACE_BACKGROUND : true,
};

(function loadHhsfwSettings() {
  try {
    const stored = localStorage.getItem('HHSFW.settings');
    if (stored) {
      const parsed = JSON.parse(stored);
      Object.keys(HHSFW_DEFAULTS).forEach(function (key) {
        if (typeof parsed[key] === 'boolean') {
          HHSFW_DEFAULTS[key] = parsed[key];
        }
      });
    }
  } catch (e) {
    console.error('[HHSFW] Failed to load settings from localStorage:', e);
  }
})();

// Destructure once so PAGE_LIST can reference these as plain identifiers.
const {
  HIDE_EVENT_GIRLS_AVATARS,
  HIDE_HAREM_SELECTED_GIRL_AVATAR,
  HIDE_OTHER_GIRLS_AVATARS,
  HIDE_OTHER_PLAYERS_AVATARS,
  HIDE_VIDEO_PREVIEWS,
  REPLACE_BACKGROUND,
} = HHSFW_DEFAULTS;

/**
 * CONSTANTS
 */
const NEW_BACKGROUND_URL =
  'https://hh2.hh-content.com/pictures/gallery/6/2200x/401-a8339a2168753900db437d91f2ed39ff.jpg';

// Pre-evaluated once — avoids repeating the same conditional spread across every page entry
const PLAYER_AVATAR_SELECTORS = ['.player-profile-picture > img'];
const BACKGROUND_SELECTORS = ['.fixed_scaled > img'];
const VIDEO_PREVIEW_SELECTORS = [
  '.lively_scene > img',
  '.lively_scene-wrapper > .unlocked > img',
  '.lively_scenes_preview > div > img',
  '.lse_puzzle_wrapper > .lively_scene_image',
];

const PAGE_LIST = [
  {
    name : 'ALL',
    slug : '',
    selectors : {
      backgroundImagesSrcToHidePermanently : [
        '.bundle > #special-offer',
        '.bundle > #starter-offer',
        '#crosspromo_show_ad > .crosspromo_banner',
        '#crosspromo_show_localreward > .crosspromo_banner',
        '.mc-card-container > .rewards-container',
        '.product-offer-container > .product-offer-background-container',
      ],
      imagesSrcToHidePermanently : [
        '.background_image-style > img',
        '.background_image-style > source',
        '.video-background > .variant-video',
        '.intro > .quest-container > #scene > .canvas > .picture',
        '.background_image-style > img',
        '#no_energy_popup > .avatar',
        '.info-top-block > .bunny-rotate-device',
        '.container > .avatar',
        '.prestige > .avatar',
        '#special-offer > .background-video',
        '.pwa-info-container > .install_app_girl',
        ...(HIDE_ADDS ? [
          '.exo-native-widget',
          '.ad-revive-container',
        ] : []),
        ...(HIDE_BACKGROUND ? BACKGROUND_SELECTORS : []),
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.rewards > .girl-avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? PLAYER_AVATAR_SELECTORS : []),
        ...(HIDE_VIDEO_PREVIEWS ? VIDEO_PREVIEW_SELECTORS : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'ACTIVITIES',
    slug : '/activities.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : ['.contest > .contest_header'],
      imagesSrcToHidePermanently : [
        '.mission_image > img',
        '.pop_thumb > img',
        '.pop-record > .pop-record-bg',
        '.timer-girl-container > img',
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.pop-details-left > img', '.pop_girl_avatar > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'ADVENTURES',
    slug : '/adventures.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : ['.adventure-card-container'],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CHAMPIONS',
    slug : '/champions/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        '.champions-over__champion-wrapper > .champions-over__champion-image',
        '.champions-over__champion-wrapper > .champions-over__champion-dialog-box',
        '.defender-preview > img',
        '.attacker-preview > .character',
        '.rounds-info__figures > .figure',
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.champions-over__champion-wrapper > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CHARACTERS',
    slug : '/characters/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CLUB CHAMPION',
    slug : '/club-champion.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        '.attacker-preview > .character',
        '.defender-preview > img',
        '.figure',
        '.champions-over__champion-wrapper > .champions-over__champion-image',
        '.champions-over__champion-wrapper > .champions-over__champion-dialog-box',
        '.girl-fav-position > .favorite-position',
        '.girl-card > .fav-position',
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.champions-over__champion-wrapper > .avatar',
        ] : []),
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.attacker-girl > .avatar',
          '.defender-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CLUBS',
    slug : '/clubs.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT LABYRINTH TEAM',
    slug : '/edit-labyrinth-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT TEAM',
    slug : '/edit-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT WORLD BOSS TEAM',
    slug : '/edit-world-boss-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EVENT',
    slug : '/event.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.column-girl > img',
          '.girls-container > .avatar',
          '.lse_girl_container > .avatar',
          '.right-container > .avatar',
          '.slide > .avatar',
          '.sm-static-girl > img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'GIRL',
    slug : '/girl/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.awakening-container > .avatar',
          '.team-slot-container > img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'GOD PATH',
    slug : '/god-path.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.feature-girl > .avatar'] : []),
        '.container-category > .feature-bgr',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'HAREM',
    slug : '/harem.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'HOME',
    slug : '/home.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [
        '#special-offer',
        '.news_page_content > .news_page_pic',
        '.news_thumb > .news_thumb_pic',
      ],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.waifu-container > .avatar'] : []),
        '.info-top-block > .bunny-rotate-device',
        '.pwa-info-container > .install_app_girl',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH',
    slug : '/labyrinth.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.labyrinth-girl > .avatar',
          '.shop-labyrinth-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH BATTLE',
    slug : '/labyrinth-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.pvp-girls > .avatar',
          '.labyrinth-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH ENTRANCE',
    slug : '/labyrinth-entrance.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.labyrinth-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH POOL SELECT',
    slug : '/labyrinth-pool-select.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH PRE-BATTLE',
    slug : '/labyrinth-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.labyrinth-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUE BATTLE',
    slug : '/league-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUES',
    slug : '/leagues.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl-block > .avatar'] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img', '.player-profile-picture > img'] : []),
        '.tier_icons > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUES PRE-BATTLE',
    slug : '/leagues-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LOVE RAIDS',
    slug : '/love-raids.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left-girl-container > .avatar',
          '.left-girl-container > .girl-img',
          '.right-girl-container > .avatar',
          '.right-girl-container > .girl-img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'MEMBER PROGRESSION',
    slug : '/member-progression.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : ['.page-girl > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PACHINKO',
    slug : '/pachinko.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        '.pachinko_img > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON',
    slug : '/pantheon.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-container > .avatar'] : []),
        '.pantheon_bgr > .stage-bgr',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON BATTLE',
    slug : '/pantheon-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON PRE-BATTLE',
    slug : '/pantheon-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        '.fixed_scaled > img',
        '.player-profile-picture > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PATH OF GLORY',
    slug : '/path-of-glory.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left_side > .avatar',
          '.right_side > .avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PATH OF VALOR',
    slug : '/path-of-valor.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left_side > .avatar',
          '.right_side > .avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL',
    slug : '/penta-drill.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL ARENA',
    slug : '/penta-drill-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL BATTLE',
    slug : '/penta-drill-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.pvp-girls > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PVP ARENA',
    slug : '/pvp-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.feature-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'QUEST',
    slug : '/quest/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : ['.canvas > .picture'],
    },
  },
  {
    name : 'SEASON',
    slug : '/season.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASON ARENA',
    slug : '/season-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASON BATTLE',
    slug : '/season-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASONAL',
    slug : '/seasonal.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girls-reward-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SIDE QUESTS',
    slug : '/side-quests.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : ['.side-quest-image > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TEAMS',
    slug : '/teams.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : ['.girl-image-container > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TROLL BATTLE',
    slug : '/troll-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TROLL PRE-BATTLE',
    slug : '/troll-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WAIFU',
    slug : '/waifu.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD',
    slug : '/world/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_world > .avatar'] : []),
        '.troll_world > .troll-tier-img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS BATTLE',
    slug : '/world-boss-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.pvp-girls > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS EVENT',
    slug : '/world-boss-event',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.left-container > .avatar',
          '.right-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS PRE-BATTLE',
    slug : '/world-boss-pre-battle',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
];

// Pre-filter once at startup — only keep entries that match the current URL.
// The 'ALL' entry (empty slug) always matches. All others are tested against href.
const ACTIVE_PAGES = PAGE_LIST
  .filter(({slug}) => !slug || window.location.href.includes(slug));

/**
 * Injects a CSS rule hiding all matched selectors via the given CSS property.
 * Unified helper for both display:none and background-image:none cases.
 */
function injectCssHideRule(selectorsArray, cssProperty) {
  if (selectorsArray.length === 0) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log(`> INJECTING CSS HIDE RULE: ${cssProperty}`);
  }
  const style = document.createElement('style');
  const cssValue = cssProperty === 'opacity' ? '0' : 'none';
  style.textContent = `${selectorsArray.join(', ')} { ${cssProperty}: ${cssValue} !important; }\n`;
  document.head.prepend(style);
}

/**
 * Replaces the src of all matched elements with newSrc.
 */
function processImagesSrcToReplace(selectorsArray, newSrc) {
  if (selectorsArray.length === 0 || !newSrc) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log('> PROCESSING IMAGES SRC TO REPLACE');
  }
  const elements = document.querySelectorAll(selectorsArray.join(', '));
  if (DEBUG_ACTIVATED) {
    console.log('> nb of elements:', elements.length);
  }
  elements.forEach((element) => {
    if (element.src) {
      element.src = newSrc;
    }
  });
}

/**
 * Modify style directly on matched elements.
 * Used for temporarily hiding (opacity: 0) or showing again (opacity: 100) elements.
 */
function modifyElementsStyle(selectorsArray, property, value) {
  if (selectorsArray.length === 0) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log(`> SETTING ELEMENTS style ${property} to ${value}`);
  }
  const elements = document.querySelectorAll(selectorsArray.join(', '));
  if (DEBUG_ACTIVATED) {
    console.log('> nb of elements:', elements.length);
  }
  elements.forEach((element) => {
    element.style[property] = value;
  });
}

/**
 * Increments the debug limit counter and disables debug logging once threshold is reached.
 */
function checkDebugLimit() {
  if (!DEBUG_LIMIT_ACTIVATED) {
    return;
  }
  debugLimitCount++;
  if (debugLimitCount > 3) {
    DEBUG_ACTIVATED = false;
  }
}

/**
 * Returns true if at least one user-controlled setting is enabled.
 * Used to short-circuit runAllHidingProcesses when everything is off.
 */
function isAnySettingEnabled() {
  return Object.values(HHSFW_DEFAULTS).some(Boolean);
}

/**
 * Unified page processing loop — runs all selectors for the current page.
 * ACTIVE_PAGES is pre-filtered at startup, so no slug-matching happens here.
 */
function runAllHidingProcesses() {
  ACTIVE_PAGES.forEach(({name, selectors, slug}) => {
    if (DEBUG_ACTIVATED) {
      console.log('> ')
      console.log(`> HIDING MEDIAS IN ${name} PAGE with SLUG: ${slug}`)
    }
    const {
      backgroundImagesSrcToHidePermanently,
      imagesSrcToHidePermanently,
      imagesToHideTemporarily,
    } = selectors;

    // CSS injection — runs once only on the early call, before DOMContentLoaded
    if (!isCssInjected) {
      injectCssHideRule(backgroundImagesSrcToHidePermanently, 'background-image');
      injectCssHideRule(imagesSrcToHidePermanently, 'opacity');
    }

    if (isDOMReady) { // DOM manipulation — skipped on the early call, only runs after DOMContentLoaded
      modifyElementsStyle(imagesToHideTemporarily, 'display', 'none');
    }
  });

  // ALL PAGES - REPLACE BACKGROUND
  if (isDOMReady && REPLACE_BACKGROUND && !HIDE_BACKGROUND) { // DOM manipulation — skipped on the early call, only runs after DOMContentLoaded
    processImagesSrcToReplace(BACKGROUND_SELECTORS, NEW_BACKGROUND_URL);
  }
}

/**
 * Unified page processing loop — runs all selectors for the current page.
 * ACTIVE_PAGES is pre-filtered at startup, so no slug-matching happens here.
 */
function runAllRevealingProcesses() {
  if (!isDOMReady) {
    return;
  }
  ACTIVE_PAGES.forEach(({name, selectors, slug}) => {
    if (DEBUG_ACTIVATED) {
      console.log('> ');
      console.log(`> REVEALING MEDIAS IN ${name} PAGE with SLUG: ${slug}`);
    }
    modifyElementsStyle(selectors.imagesToHideTemporarily,'display', 'block');
  });
}

// Add event listener for clicks
document.addEventListener('click', function (event) {
  if (
    event.target.classList.contains('diamond') ||
    event.target.classList.contains('speech_bubble_info_icn') ||
    (event.target.parentElement &&
      event.target.parentElement.classList.contains('eye') &&
      window.location.href.includes('/quest/'))
  ) {
    if (DEBUG_ACTIVATED) {
      console.log('> ');
      console.log('> SPECIAL BUTTON CLICKED (IMG PROCESSING STOPPED)');
    }
    runAllRevealingProcesses();
  }
});

/**
 * Saves the current HHSFW_DEFAULTS state to localStorage.
 */
function saveHhsfwSettings() {
  try {
    localStorage.setItem('HHSFW.settings', JSON.stringify(HHSFW_DEFAULTS));
  } catch (e) {
    /* localStorage unavailable — skip silently */
  }
}

/**
 * Builds and injects the HHSFW settings panel and its toggle button into
 * #contains_all. Only runs on /home.html.
 *
 * The panel mirrors the style of #hhsOptions from the HHS script:
 * a tooltip div shown/hidden by clicking the logo image.
 */
function injectHhsButtonSibling() {
  // Only inject on the home page
  if (!window.location.href.includes('/home.html')) {
    return;
  }

  const container = document.getElementById('contains_all');
  if (!container) {
    if (DEBUG_ACTIVATED) {
      console.log('> #contains_all not found — not injecting HHSFW logo + settings panel');
    }
    return;
  }

  // Ensure the container is a positioning context
  if (window.getComputedStyle(container).position === 'static') {
    container.style.position = 'relative';
  }

  // ── Styles ────────────────────────────────────────────────────────────────
  // Media query breakpoints
  const mediaDesktop = '@media only screen and (min-width: 1026px)';
  const mediaMobile = '@media only screen and (max-width: 1025px)';

  // Detect sibling scripts that affect layout.
  // Store the element directly to avoid a second querySelector later.
  const hhPlusPlusButton = document.querySelector('.hh-plus-plus-config-button');
  if (DEBUG_ACTIVATED) {
    console.log('> hhPlusPlusButton:', hhPlusPlusButton);
  }

  // Detect OCD.js "customizedHomeScreen" setting from its localStorage key
  let customizedHomeScreen = false;
  try {
    customizedHomeScreen = localStorage.getItem('HHS.customizedHomeScreen') === 'true';
  } catch (e) {
    /* localStorage unavailable — keep false */
  }
  if (DEBUG_ACTIVATED) {
    console.log('> customizedHomeScreen:', customizedHomeScreen);
  }

  const dynamicStyle = document.createElement('style');
  document.head.appendChild(dynamicStyle);
  const sheet = dynamicStyle.sheet;

  sheet.insertRule(`#hhsfwToggle {
    height: 35px;
    position: absolute;
    z-index: 5;
    filter: drop-shadow(0px 0px 5px white);
    cursor: pointer;
  }`);

  if (customizedHomeScreen) {
    if (hhPlusPlusButton) {
      sheet.insertRule(`${mediaDesktop} { 
        #hhsfwToggle {
          right: 50px;
          top: 150px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 62px;
          top: 165px;
        }
      }`);
    } else {
      sheet.insertRule(`${mediaDesktop} { 
        #hhsfwToggle {
          right: 41px;
          top: 150px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 45px;
          top: 165px;
        }
      }`);
    }
  } else {
    if (hhPlusPlusButton) {
      sheet.insertRule(`${mediaDesktop} {
        #hhsfwToggle {
          right: 50px;
          top: 110px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 50px;
          top: 160px;
          width: 64px !important;
          height: 64px !important;
        }
      }`);
    } else {
      sheet.insertRule(`${mediaDesktop} {
        #hhsfwToggle {
          right: 40px;
          top: 85px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 125px;
          top: 85px;
          width: 38px !important;
          height: 38px !important;
        }
      }`);
    }
  }

  // ── Static panel + UI styles (unchanged) ─────────────────────────────────
  const style = document.createElement('style');
  style.textContent = `
    #hhsfwPanel {
      display: none;
      position: absolute;
      z-index: 99;
      background: #1a1a2e;
      border: 1px solid #e91e8c;
      border-radius: 8px;
      padding: 14px 18px 10px;
      min-width: 280px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.7);
      font-family: Arial, sans-serif;
      font-size: 13px;
      color: #f0f0f0;
    }
    #hhsfwPanel .hhsfw-title {
      font-weight: bold;
      font-size: 14px;
      color: #e91e8c;
      margin-bottom: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    #hhsfwPanel .hhsfw-close {
      cursor: pointer;
      font-size: 16px;
      color: #aaa;
      line-height: 1;
    }
    #hhsfwPanel .hhsfw-close:hover { color: #fff; }
    #hhsfwPanel .script_setting {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 10px;
      margin-bottom: 8px;
    }
    #hhsfwPanel .script_setting span {
      display: flex;
      align-items: center;
      line-height: 1;
    }
    #hhsfwPanel .switch {
      position: relative;
      display: inline-block;
      width: 36px;
      height: 20px;
      min-width: 36px;
      min-height: 20px;
      flex-shrink: 0;
      top: 0px;
    }
    #hhsfwPanel .switch input { opacity: 0; width: 0; height: 0; }
    #hhsfwPanel .slider {
      position: absolute;
      cursor: pointer;
      inset: 0;
      background: #444;
      border-radius: 20px;
      transition: background 0.2s;
      margin-right: 0px;
    }
    #hhsfwPanel .slider:before {
      content: '';
      position: absolute;
      width: 14px;
      height: 14px;
      left: 3px;
      bottom: 3px;
      background: #fff;
      border-radius: 50%;
      transition: transform 0.2s;
    }
    #hhsfwPanel .switch input:checked + .slider { background: #e91e8c; }
    #hhsfwPanel .switch input:checked + .slider:before { transform: translateX(16px); }
    #hhsfwPanel .hhsfw-note {
      font-size: 11px;
      color: #888;
      margin-top: 8px;
      border-top: 1px solid #333;
      padding-top: 6px;
    }
  `;
  document.head.appendChild(style);

  // ── Settings panel ────────────────────────────────────────────────────────
  const panel = document.createElement('div');
  panel.id = 'hhsfwPanel';
  panel.innerHTML = `
    <div class="hhsfw-title">
      Hentai Heroes SFW
      <span class="hhsfw-close" id="hhsfwClose">✕</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_EVENT_GIRLS_AVATARS" ${HHSFW_DEFAULTS.HIDE_EVENT_GIRLS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide event girls' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_HAREM_SELECTED_GIRL_AVATAR" ${HHSFW_DEFAULTS.HIDE_HAREM_SELECTED_GIRL_AVATAR ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide harem selected girl avatar</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_OTHER_GIRLS_AVATARS" ${HHSFW_DEFAULTS.HIDE_OTHER_GIRLS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide other girls' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_OTHER_PLAYERS_AVATARS" ${HHSFW_DEFAULTS.HIDE_OTHER_PLAYERS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide other players' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_VIDEO_PREVIEWS" ${HHSFW_DEFAULTS.HIDE_VIDEO_PREVIEWS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide video previews</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-REPLACE_BACKGROUND" ${HHSFW_DEFAULTS.REPLACE_BACKGROUND ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Change background to SFW one</span>
    </div>
    <div class="hhsfw-note">Changes take effect on next page load.</div>
  `;
  container.appendChild(panel);
  // Set the initial display via inline style so the toggle handler's
  // panel.style.display check always compares against a known inline value.
  // Without this, style.display starts as '' (empty) even though the CSS rule
  // says display:none — causing the first click to set it to 'none' instead of 'block'.
  panel.style.display = 'none';

  // ── Toggle button (logo image) ────────────────────────────────────────────
  const img = document.createElement('img');
  img.id = 'hhsfwToggle';
  img.src = isAnySettingEnabled() ? 'https://i.postimg.cc/d3QRs43j/SFW_logo.png' : 'https://i.postimg.cc/7hP1HmhM/NSFW_logo.png';
  img.title = 'HHSFW Settings';
  container.appendChild(img);

  // ── Event listeners ───────────────────────────────────────────────────────
  img.addEventListener('click', function () {
    if (panel.style.display === 'none') {
      // Measure panel width before showing it
      panel.style.visibility = 'hidden';
      panel.style.display = 'block';

      // img.offsetTop/offsetLeft are already relative to #contains_all
      // (the nearest positioned ancestor), so they map directly to
      // `top`/`left` values for `position: absolute` inside that container.
      // Bottom of toggle = offsetTop + offsetHeight
      // Left of panel so its right edge aligns with toggle's left edge:
      //   panelLeft = img.offsetLeft - panel.offsetWidth
      const panelTop = img.offsetTop + img.offsetHeight;
      const panelLeft = img.offsetLeft - panel.offsetWidth;

      panel.style.top = panelTop + 'px';
      panel.style.left = panelLeft + 'px';
      panel.style.right = '';

      panel.style.visibility = '';
    } else {
      panel.style.display = 'none';
    }
  });

  document.getElementById('hhsfwClose').addEventListener('click', function () {
    panel.style.display = 'none';
  });

  [
    'HIDE_EVENT_GIRLS_AVATARS',
    'HIDE_HAREM_SELECTED_GIRL_AVATAR',
    'HIDE_OTHER_GIRLS_AVATARS',
    'HIDE_OTHER_PLAYERS_AVATARS',
    'HIDE_VIDEO_PREVIEWS',
    'REPLACE_BACKGROUND'
  ].forEach(function (key) {
    document.getElementById('hhsfw-' + key).addEventListener('change', function (e) {
      HHSFW_DEFAULTS[key] = e.target.checked;
      saveHhsfwSettings();
    });
  });
}

// DOM is ready, resources may still be loading.
// Set isDOMReady so that processImagesSrcToReplace and modifyElementsStyle are now allowed to run.
// CSS injection is skipped on this second call (isCssInjected is already true).
document.addEventListener('DOMContentLoaded', function () {
  if (DEBUG_ACTIVATED) {
    console.log('> ');
    console.log('> DOMContentLoaded');
  }

  if (isAnySettingEnabled()) {
    isDOMReady = true;
    runAllHidingProcesses();
  }
});

// All resources (including images) have fully loaded — safe to query elements
// that are injected late by the game. This is the last event in the page
// loading lifecycle, fired after DOMContentLoaded and all sub-resources.
window.addEventListener('load', function () {
  if (DEBUG_ACTIVATED) {
    console.log('> ');
    console.log('> window load');
  }

  // Delay so sibling scripts (OCD.js, HH++) have time to inject their
  // own buttons into the DOM before we read their positions.
  setTimeout(function () {
    injectHhsButtonSibling();
  }, 500);
});

// Run immediately at script start — CSS injection only (isDOMReady is false, DOM manipulation is skipped)
if (isAnySettingEnabled()) {
  checkDebugLimit();
  runAllHidingProcesses();
  isCssInjected = true;
}