[Pokeclicker] Shared EVs

Share Effort Points (EVs) across all Pokemon with Pokerus (Resistant).

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name          [Pokeclicker] Shared EVs
// @namespace     Pokeclicker Scripts
// @author        wizanyx
// @description   Share Effort Points (EVs) across all Pokemon with Pokerus (Resistant).
// @copyright     https://github.com/wizanyx
// @license       GPL-3.0 License
// @version       0.0.1

// @homepageURL   https://github.com/wizanyx/Pokeclicker-Scripts/
// @supportURL    https://github.com/wizanyx/Pokeclicker-Scripts/issues

// @match         https://www.pokeclicker.com/
// @icon          https://www.google.com/s2/favicons?domain=pokeclicker.com
// @grant         unsafeWindow
// @run-at        document-idle
// ==/UserScript==

const SETTINGS = {
    STORAGE_KEY: "pokeclicker_sharedEVs_settings",
    UI_CONTAINER_ID: "customScriptsContainer",
};

/**
 * Core Manager for the Shared EVs functionality.
 * Handles state management, persistence, and value tracking.
 */
const SharedEVsManager = {
    // Observable state
    totalEffortPoints: ko.observable(0),
    enabled: ko.observable(true),
    
    // Internal tracking
    _tempEffortPoints: 0,
    initialized: false,

    /**
     * Initializes the manager, loads settings, and calculates initial pool.
     */
    initialize() {
        this.load();
        
        // Calculate initial pool from all caught Pokemon
        const currentTotal = App.game.party.caughtPokemon.reduce(
            (acc, p) => acc + p.effortPoints,
            0
        );
        
        this.totalEffortPoints(currentTotal);
        this._tempEffortPoints = currentTotal;
        this.initialized = true;

        // Auto-save loop
        setInterval(() => this.save(), 60000);
        
        // Subscribe to changes
        this.enabled.subscribe(() => this.save());
    },

    /**
     * Loads settings from LocalStorage.
     */
    load() {
        try {
            const json = localStorage.getItem(SETTINGS.STORAGE_KEY);
            if (json) {
                const data = JSON.parse(json);
                if (data.enabled !== undefined) this.enabled(data.enabled);
            }
        } catch (e) {
            console.error("[SharedEVs] Failed to load settings:", e);
        }
    },

    /**
     * Saves current settings to LocalStorage.
     */
    save() {
        if (!this.initialized) return;
        
        // Update observable from temp tracker to ensure UI is fresh
        this.totalEffortPoints(this._tempEffortPoints);

        const data = {
            enabled: this.enabled()
        };
        localStorage.setItem(SETTINGS.STORAGE_KEY, JSON.stringify(data));
    },

    /**
     * Updates the internal tracker when EP is gained.
     * @param {number} delta - The amount of EP gained.
     */
    addEffortPoints(delta) {
        this._tempEffortPoints += delta;
    }
};

/**
 * Handles Game Logic patches (Monkey Patching).
 */
const GameMechanics = {
    applyPatches() {
        // Patch PartyPokemon.effortPoints setter to track gains
        const originalEffortPointsDescriptor = Object.getOwnPropertyDescriptor(
            PartyPokemon.prototype,
            "effortPoints"
        );

        Object.defineProperty(PartyPokemon.prototype, "effortPoints", {
            set: function (value) {
                SharedEVsManager.addEffortPoints(value - this._effortPoints());
                originalEffortPointsDescriptor.set.call(this, value);
            },
        });

        // Patch calculateEVs to use the shared pool
        PartyPokemon.prototype.calculateEVs = function () {
            const power = App.game.challenges.list.slowEVs.active.peek()
                ? GameConstants.EP_CHALLENGE_MODIFIER
                : 1;

            // Standard Behavior if disabled
            if (!SharedEVsManager.enabled()) {
                return this._effortPoints() / GameConstants.EP_EV_RATIO / power;
            }

            // Shared Behavior
            const epToUse = (this.pokerus >= GameConstants.Pokerus.Resistant)
                ? SharedEVsManager.totalEffortPoints()
                : this._effortPoints();

            return epToUse / GameConstants.EP_EV_RATIO / power;
        };
    }
};

/**
 * Handles User Interface Injection and Customizations.
 */
const UserInterface = {
    /**
     * Injects the shared scripts container into the Left Column.
     */
    createContainer() {
        if (document.getElementById(SETTINGS.UI_CONTAINER_ID)) return;

        const leftColumn = document.getElementById("left-column");
        if (leftColumn) {
            const div = document.createElement("div");
            div.id = SETTINGS.UI_CONTAINER_ID;
            div.className = "card sortable border-secondary mb-3";
            div.innerHTML = `
                <div class="card-header p-0" data-toggle="collapse" href="#customScriptsBody">
                    <span>Scripts</span>
                </div>
                <div id="customScriptsBody" class="card-body p-0 show"></div>
            `;
            leftColumn.appendChild(div);
        }
    },

    /**
     * Injects the Shared EVs card into the container.
     */
    injectScriptCard() {
        // Check if card already exists
        if (document.getElementById("sharedEVsDisplay")) return;
        
        const displayDiv = document.createElement("div");
        displayDiv.id = "sharedEVsDisplay";

        const html = `
            <div class="card-header p-0 border-top" data-toggle="collapse" href="#sharedEVsInner">
                <span>Shared EVs</span>
            </div>
            <div id="sharedEVsInner" class="collapse show">
                <div class="card-body p-0">
                     <button class="btn btn-block"
                        style="border-radius: 0;"
                        data-bind="
                            click: function() { SharedEVsManager.enabled(!SharedEVsManager.enabled()); },
                            class: SharedEVsManager.enabled() ? 'btn-success' : 'btn-danger',
                            text: 'Enabled [' + (SharedEVsManager.enabled() ? 'ON' : 'OFF') + ']'
                        ">
                    </button>
                </div>
            </div>
        `;
        
        displayDiv.innerHTML = html;
        const scriptBody = document.getElementById("customScriptsBody");
        scriptBody.appendChild(displayDiv);
        ko.applyBindings({ SharedEVsManager }, displayDiv);
    }
};

/**
 * Main Initialization Function
 */
function initializeSharedEVs() {
    SharedEVsManager.initialize();
    UserInterface.injectScriptCard();
}

/**
 * Run patches before game start
 */
function runPriorityPatches() {
    GameMechanics.applyPatches();
    UserInterface.createContainer();
}

// Loader
function loadScript(scriptName, initFunction, priorityFunction) {
    function reportScriptError(scriptName, error) {
        const details =
            error?.stack ||
            error?.message ||
            error?.toString?.() ||
            String(error);
        console.error(
            `Error while initializing '${scriptName}' userscript:\n${error}`,
        );
        console.error(details);
        Notifier.notify({
            type: NotificationConstants.NotificationOption.warning,
            title: scriptName,
            message: `The '${scriptName}' userscript crashed while loading. Check for updates or disable the script, then restart the game.\n\nReport script issues to the script developer, not to the Pokéclicker team.\n\n${details}`,
            timeout: GameConstants.DAY,
        });
    }
    const windowObject = !App.isUsingClient ? unsafeWindow : window;
    // Inject handlers if they don't exist yet
    if (windowObject.ScriptInitializers === undefined) {
        windowObject.ScriptInitializers = {};
        const oldInit = Preload.hideSplashScreen;
        var hasInitialized = false;

        // Initializes scripts once enough of the game has loaded
        Preload.hideSplashScreen = function (...args) {
            var result = oldInit.apply(this, args);
            if (App.game && !hasInitialized) {
                // Initialize all attached userscripts
                Object.entries(windowObject.ScriptInitializers).forEach(
                    ([scriptName, initFunction]) => {
                        try {
                            initFunction();
                            console.log(`'${scriptName}' userscript loaded.`);
                        } catch (e) {
                            reportScriptError(scriptName, e);
                        }
                    },
                );
                hasInitialized = true;
            }
            return result;
        };
    }

    // Prevent issues with duplicate script names
    if (windowObject.ScriptInitializers[scriptName] !== undefined) {
        console.warn(`Duplicate '${scriptName}' userscripts found!`);
        Notifier.notify({
            type: NotificationConstants.NotificationOption.warning,
            title: scriptName,
            message: `Duplicate '${scriptName}' userscripts detected. This could cause unpredictable behavior and is not recommended.`,
            timeout: GameConstants.DAY,
        });
        let number = 2;
        while (
            windowObject.ScriptInitializers[`${scriptName} ${number}`] !==
            undefined
        ) {
            number++;
        }
        scriptName = `${scriptName} ${number}`;
    }
    // Add initializer for this particular script
    windowObject.ScriptInitializers[scriptName] = initFunction;
    // Run any functions that need to execute before the game starts
    if (priorityFunction) {
        $(document).ready(() => {
            try {
                priorityFunction();
            } catch (e) {
                reportScriptError(scriptName, e);
                // Remove main initialization function
                windowObject.ScriptInitializers[scriptName] = () => null;
            }
        });
    }
}

if (!App.isUsingClient) {
    unsafeWindow.SharedEVsManager = SharedEVsManager;
} else {
    window.SharedEVsManager = SharedEVsManager;
}

loadScript("Shared EVs", initializeSharedEVs, runPriorityPatches);