HWH Auto-Buyer

Extension for HeroWarsHelper script - Buys items from shops based on user-defined rules.

// ==UserScript==
// @name         HWH Auto-Buyer
// @namespace    HWHAutoBuyer
// @version      1.2.2
// @description  Extension for HeroWarsHelper script - Buys items from shops based on user-defined rules.
// @author       Bartjan
// @match        https://www.hero-wars.com/*
// @match        https://apps-1701433570146040.apps.fbsbx.com/*
// @run-at       document-start
// ==/UserScript==

(function () {
    if (!this.HWHClasses || !this.HWHFuncs || !this.Caller) {
        console.log('%cHWH dependencies not found for Auto-Buyer extension', 'color: red');
        return;
    }
    console.log('%cStart Extension ' + GM_info.script.name + ', v' + GM_info.script.version + ' by ' + GM_info.script.author, 'color: green');
    const { HWHClasses, HWHFuncs, HWHData, cheats, Caller } = this;
    const { addExtentionName, getSaveVal, setSaveVal, setProgress } = HWHFuncs;
    addExtentionName(GM_info.script.name, GM_info.script.version, GM_info.script.author);

    const settings = {
        coin1: { input: null, default: true },
        coin2: { input: null, default: true },
        coin3: { input: null, default: true },
        coin4: { input: null, default: true },
        coin5: { input: null, default: true },
        coin6: { input: null, default: true },
        maxGear: { input: null, default: 3 },
        maxFragment: { input: null, default: 80 },
        minCoins: { input: null, default: 100000 },
        chaosPetShop: { input: null, default: false }, // << NEW SETTING
    };

    const COINS = [
        { id: '1', name: 'Arena Coin', setting: 'coin1' },
        { id: '2', name: 'Grand Arena Coin', setting: 'coin2' },
        { id: '3', name: 'Tower Coin', setting: 'coin3' },
        { id: '4', name: 'Outland Coin', setting: 'coin4' },
        { id: '5', name: 'Soul Coin', setting: 'coin5' },
        { id: '6', name: 'Friendship Coin', setting: 'coin6' },
        // Pet Soul Coin is not in default shops, handled manually for shopId 17
    ];

    const ALLOWED_COIN_IDS = COINS.map(coin => coin.id);
    const ALLOWED_REWARD_TYPES = ['gear', 'fragmentGear', 'fragmentScroll'];
    const SHOP_NAMES = {
        1: 'Town Shop', 4: 'Arena Shop', 5: 'Grand Arena Shop',
        6: 'Tower Shop', 8: 'Soul Shop', 9: 'Friendship Shop', 10: 'Outland Shop',
        17: 'Pet Soul Stone Shop',
    };

    function getItemName(rewardType, rewardData) {
        try {
            const itemId = Object.keys(rewardData)[0];
            let libType = rewardType.charAt(0).toUpperCase() + rewardType.slice(1);
            if (rewardType.startsWith('fragment')) {
                libType = rewardType.replace('fragment', '').charAt(0).toUpperCase() + rewardType.replace('fragment', '').slice(1);
            }
            const libName = `LIB_${libType.toUpperCase()}_NAME_${itemId}`;
            const translated = cheats.translate(libName);
            return translated.startsWith('LIB_') ? `${rewardType} ${itemId}` : translated;
        } catch (e) {
            return `${rewardType} ID`;
        }
    }

    async function autoBuyFromShops() {
        console.log('=== AUTO-BUYER START ===');
        setProgress('Starting Auto-Buyer...');
        const maxGear = parseInt(settings.maxGear.input.value) || settings.maxGear.default;
        const maxFragment = parseInt(settings.maxFragment.input.value) || settings.maxFragment.default;
        const minCoins = parseInt(settings.minCoins.input.value) || settings.minCoins.default;
        const enabledCoins = {};
        COINS.forEach(coin => {
            enabledCoins[coin.id] = settings[coin.setting].input.checked;
        });
        const buyChaosPetShop = settings.chaosPetShop.input.checked;
        const [shops, inventory, userInfo] = await Caller.send(['shopGetAll', 'inventoryGet', 'userGetInfo']);
        const currencyTracker = { ...inventory.coin, gold: userInfo.gold, ...inventory.petcoin }; // include petcoin if structure matches
        const callsToMake = [];
        const itemsToLog = [];

        for (const shopId in shops) {
            const idNum = parseInt(shopId);
            const currentShop = shops[shopId];
            if (!currentShop.slots) continue;


// Handle Pet Soul Stone Shop (17) for chaos particles via Pet Soul Coins, NO cost/balance checks
if (idNum === 17) {
    console.log("something in pet shop");
    if (!buyChaosPetShop) continue;
    // Only buy from slot 4, check reward is chaos particles
    const slot = currentShop.slots['4'];
    if (!slot || slot.bought || !slot.reward || !slot.cost) continue;
    // Confirm slot actually rewards chaos particles
    const rewardType = Object.keys(slot.reward)[0];
 //   if (!rewardType || !rewardType.toLowerCase().includes('chaos')) continue;
    // Remove all coin/cost checks: always buy if available
    callsToMake.push({
        name: 'shopBuy',
        args: {
            shopId: currentShop.id,
            slot: slot.id,
            cost: slot.cost,
            reward: slot.reward,
        },
        ident: `shopBuy_${currentShop.id}_${slot.id}`,
    });
    const rewardData = slot.reward[rewardType];
    const amount = Object.values(rewardData)[0];
    itemsToLog.push(`• ${getItemName(rewardType, rewardData)} (x${amount}) from Pet Soul Stone Shop (Chaos Particles)`);
    continue;
}


            // Normal logic for other shops (original logic, but skip shopId 17)
            if (idNum >= 11) continue;
            for (const slotId in currentShop.slots) {
                const slot = currentShop.slots[slotId];
                let shouldBuy = true;
                if (slot.bought || !slot.reward || !slot.cost) shouldBuy = false;
                if (shouldBuy) {
                    const costType = Object.keys(slot.cost)[0];
                    const costCurrencyId = Object.keys(slot.cost[costType])[0];
                    if (costType !== 'coin' || !ALLOWED_COIN_IDS.includes(costCurrencyId) || !enabledCoins[costCurrencyId]) {
                        shouldBuy = false;
                    }
                }
                if (shouldBuy) {
                    const costAmount = slot.cost[Object.keys(slot.cost)[0]][Object.keys(slot.cost[Object.keys(slot.cost)[0]])[0]];
                    const playerBalance = currencyTracker[Object.keys(slot.cost[Object.keys(slot.cost)[0]])[0]] || 0;
                    if (playerBalance < costAmount + minCoins) shouldBuy = false;
                }
                if (shouldBuy) {
                    for (const [rewardType, rewardData] of Object.entries(slot.reward)) {
                        if (!ALLOWED_REWARD_TYPES.includes(rewardType)) {
                            shouldBuy = false;
                            break;
                        }
                        const itemId = Object.keys(rewardData)[0];
                        let inventoryCount = 0;
                        if (inventory[rewardType] && inventory[rewardType][itemId]) {
                            inventoryCount = inventory[rewardType][itemId];
                        }
                        if ((rewardType === 'gear' || rewardType === 'scroll') && inventoryCount >= maxGear) {
                            shouldBuy = false;
                            break;
                        }
                        if ((rewardType === 'fragmentGear' || rewardType === 'fragmentScroll') && inventoryCount >= maxFragment) {
                            shouldBuy = false;
                            break;
                        }
                    }
                }
                if (shouldBuy) {
                    callsToMake.push({
                        name: 'shopBuy',
                        args: {
                            shopId: currentShop.id,
                            slot: slot.id,
                            cost: slot.cost,
                            reward: slot.reward,
                        },
                        ident: `shopBuy_${currentShop.id}_${slot.id}`,
                    });
                    const rewardType = Object.keys(slot.reward)[0];
                    const rewardData = slot.reward[rewardType];
                    const amount = Object.values(rewardData)[0];
                    itemsToLog.push(`• ${getItemName(rewardType, rewardData)} (x${amount}) from ${SHOP_NAMES[shopId] || `Shop ${shopId}`}`);
                    const costType = Object.keys(slot.cost)[0];
                    const costCurrencyId = Object.keys(slot.cost[costType])[0];
                    const costAmount = slot.cost[costType][costCurrencyId];
                    currencyTracker[costCurrencyId] -= costAmount;
                }
            }
        }

        if (callsToMake.length > 0) {
            console.log(`Attempting to buy ${callsToMake.length} items...`);
            setProgress(`Buying ${callsToMake.length} items...`);
            try {
                const buyResult = await Caller.send(callsToMake);
                if (buyResult) {
                    const boughtString = itemsToLog.join('\n');
                    console.log('%c--- Items Bought Successfully ---', 'color: lightgreen; font-weight: bold;');
                    console.log(boughtString);
                    setProgress(
                        `Bought ${itemsToLog.length} items! \n\n${boughtString}`,
                        true
                    );
                } else {
                    throw new Error("Buy command failed to return a result.");
                }
            } catch (error) {
                console.error('An error occurred during purchase:', error);
                setProgress('Error during purchase. Check console.', true);
            }
        } else {
            console.log('No items to buy based on current settings.');
            setProgress('No items to buy based on current settings.', true);
        }
        console.log('=== AUTO-BUYER END ===');
    }

    function initializeExtension() {
        function addShopCheckerControls() {
            const { ScriptMenu } = HWHClasses;
            const scriptMenu = ScriptMenu.getInst();
            const details = scriptMenu.addDetails('Auto-Buy Settings', 'autoBuySettings');
            COINS.forEach(coin => {
                settings[coin.setting].input = scriptMenu.addCheckbox(
                    `Buy with ${coin.name}`,
                    `Allows buying items using ${coin.name}`,
                    details
                );
            });
            settings.maxGear.input = scriptMenu.addInputText('Max Gear/Scroll Count:', 'e.g., 3', details);
            settings.maxFragment.input = scriptMenu.addInputText('Max Fragment Count:', 'e.g., 80', details);
            settings.minCoins.input = scriptMenu.addInputText('Min Coin Reserve:', 'e.g., 100000', details);
            // Pet Chaos checkbox
            settings.chaosPetShop.input = scriptMenu.addCheckbox(
                'Buy CP from Pet Shop',
                'Allows auto-buy of Chaos Particles (slot 4) from Pet shop with Pet Soul Coins',
                details
            );
            for (const key in settings) {
                const isCheckbox = settings[key].input.type === 'checkbox';
                const savedValue = getSaveVal(`shopChecker_${key}`, settings[key].default);
                if (isCheckbox) {
                    settings[key].input.checked = savedValue;
                    settings[key].input.addEventListener('change', (e) => {
                        setSaveVal(`shopChecker_${key}`, e.target.checked);
                    });
                } else {
                    settings[key].input.value = savedValue;
                    settings[key].input.addEventListener('input', (e) => {
                        setSaveVal(`shopChecker_${key}`, parseInt(e.target.value, 10) || settings[key].default);
                    });
                }
            }
        }

        addShopCheckerControls();
        const { buttons } = HWHData;
        buttons.autoBuyer = {
            get name() { return 'Auto-Buy Items'; },
            get title() { return 'Automatically buys allowed items from shops with ID < 11 based on your settings and Chaos from Pet Shop if enabled.'; },
            onClick: autoBuyFromShops,
            color: 'green',
        };
        console.log('%cAuto-Buyer extension loaded and attached to HWH menu.', 'color: green');
    }

    const { ScriptMenu } = HWHClasses;
    const scriptMenu = ScriptMenu.getInst();
    if (scriptMenu && scriptMenu.mainMenu) {
        initializeExtension();
    } else if (scriptMenu && scriptMenu.on) {
        scriptMenu.on('afterInit', initializeExtension);
    } else {
        initializeExtension();
    }
})();