HeroWarsDungeonFull

Complex dungeon auto-battle for Hero Wars with titan-specific strategies. REQUIRES HeroWarsHelper installed

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name            HeroWarsDungeonFull
// @name:en         HeroWarsDungeonFull
// @name:ru         HeroWarsDungeonFull
// @namespace       HeroWarsDungeonFull
// @version         1.0
// @description     Complex dungeon auto-battle for Hero Wars with titan-specific strategies. REQUIRES HeroWarsHelper installed
// @description:en  Complex dungeon auto-battle for Hero Wars with titan-specific strategies. REQUIRES HeroWarsHelper installed
// @description:ru  Сложное автопрохождение подземелья для фуловых титанов. ТРЕБУЕТСЯ установленный HeroWarsHelper
// @author          Yipol (based on HeroWarsDungeon by ZingerY, ApuoH, Gudwin)
// @license         MIT
// @match           https://www.hero-wars.com/*
// @match           https://apps-1701433570146040.apps.fbsbx.com/*
// @run-at          document-start
// @grant           none
// ==/UserScript==

(function() {
    'use strict';

    // ========== 轮询等待 hwhelper.js 就绪 ==========
    function waitForHWH(callback) {
        if (typeof HWHData !== 'undefined' && typeof HWHClasses !== 'undefined') {
            callback();
        } else {
            setTimeout(() => waitForHWH(callback), 200);
        }
    }

    waitForHWH(function() {
        // ========== 1. 注入 I18N 标签 ==========
        const { i18nLangData } = HWHData;
        i18nLangData.en.DUNGEON2      = 'Dungeon full';
        i18nLangData.en.DUNGEON_FULL_TITLE = 'Dungeon for Full Titans';
        i18nLangData.en.STOP_DUNGEON  = 'Stop Dungeon';
        i18nLangData.en.STOP_DUNGEON_TITLE = 'Stop digging';
        i18nLangData.en.STOP_SCRIPT   = 'Stop the';
        i18nLangData.en.COMPLETE_DUNGEON_FULL = 'Complete the dungeon for Full Titans';

        i18nLangData.ru.DUNGEON2      = 'Подземелье фулл';
        i18nLangData.ru.DUNGEON_FULL_TITLE = 'Подземелье для фуловых титанов';
        i18nLangData.ru.STOP_DUNGEON  = 'Стоп подземка';
        i18nLangData.ru.STOP_DUNGEON_TITLE = 'Остановить копание';
        i18nLangData.ru.STOP_SCRIPT   = 'Остановить скрипт';
        i18nLangData.ru.COMPLETE_DUNGEON_FULL = 'Пройти подземелье фулл';

        // ========== 2. 全局 stopDung 开关 ==========
        window.stopDung = false;
        window.stopDungeon = function() { window.stopDung = true; };

        // ========== 3. DungeonFull 入口函数 ==========
        window.DungeonFull = function() {
            const { executeDungeon } = HWHClasses;
            return new Promise((resolve, reject) => {
                const dung = new executeDungeon(resolve, reject);
                const titanit = window.HWHFuncs.getInput('countTitanit');
                dung.start(titanit);
            });
        };

        // ========== 4. 注入 UI 按钮 ==========
        const I18N = window.HWHFuncs.I18N;
        const confShow = window.HWHFuncs.confShow;

        // 在 testDungeon 的 combineList 中插入停止按钮
        HWHData.buttons.testDungeon.combineList.push({
            get name() {
                return I18N('STOP_DUNGEON');
            },
            onClick: function() {
                confShow(I18N('STOP_SCRIPT') + ' ' + I18N('STOP_DUNGEON_TITLE') + '?', window.stopDungeon);
            },
            get title() {
                return I18N('STOP_DUNGEON_TITLE');
            },
            color: 'red',
        });

        // DungeonFull 按钮(独立 combine 按钮)
        HWHData.buttons.DungeonFull = {
            isCombine: true,
            combineList: [
                {
                    get name() {
                        return I18N('DUNGEON2');
                    },
                    onClick: function() {
                        confShow(I18N('RUN_SCRIPT') + ' ' + I18N('DUNGEON_FULL_TITLE') + '?', window.DungeonFull);
                    },
                    get title() {
                        return I18N('DUNGEON_FULL_TITLE');
                    },
                },
                {
                    name: '>>',
                    onClick: cheats.goClanIsland,
                    get title() {
                        return I18N('GUILD_ISLAND_TITLE');
                    },
                    color: 'green',
                },
            ],
        };

        // ========== 5. 覆盖 executeDungeon(照搬 executeDungeon2) ==========
        {
            const send = window.HWHFuncs.send;
            const BattleCalc = window.BattleCalc;
            const Calc = window.Calc;
            const Send = window.Send;
            const cheats = window.cheats;
            const getInput = window.HWHFuncs.getInput;
            const getTimer = window.HWHFuncs.getTimer;
            const countdownTimer = window.HWHFuncs.countdownTimer;
            const setProgress = window.testFuntions.setProgress;

            window.HWHClasses.executeDungeon = function(resolve, reject) {
                    let dungeonActivity = 0;
                    let startDungeonActivity = 0;
                    let maxDungeonActivity = 150;
                    let limitDungeonActivity = 30180;
                    let countShowStats = 1;
                    //let fastMode = isChecked('fastMode');
                    let end = false;

                    let countTeam = [];
                    let timeDungeon = {
                        all: new Date().getTime(),
                        findAttack: 0,
                        attackNeutral: 0,
                        attackEarthOrFire: 0
                    }

                    let titansStates = {};
                    let bestBattle = {};

                    let teams = {
                        neutral: [],
                        water: [],
                        earth: [],
                        fire: [],
                        hero: []
                    }

                    //тест
                    let talentMsg = '';
                    let talentMsgReward = ''

                    // 跨函数共享变量(严格模式下需要声明)
                    let battleData, battleType, battleResult;

                    let callsExecuteDungeon = {
                        calls: [{
                            name: "dungeonGetInfo",
                            args: {},
                            ident: "dungeonGetInfo"
                        }, {
                            name: "teamGetAll",
                            args: {},
                            ident: "teamGetAll"
                        }, {
                            name: "teamGetFavor",
                            args: {},
                            ident: "teamGetFavor"
                        }, {
                            name: "clanGetInfo",
                            args: {},
                            ident: "clanGetInfo"
                        }]
                    }

                    this.start = async function(titanit) {
                        //maxDungeonActivity = titanit > limitDungeonActivity ? limitDungeonActivity : titanit;
                        maxDungeonActivity = titanit || getInput('countTitanit');
                        send(callsExecuteDungeon, startDungeon);
                    }

                    /** Получаем данные по подземелью */
                    function startDungeon(e) {
                        window.stopDung = false; // стоп подземка
                        let res = e.results;
                        let dungeonGetInfo = res[0].result.response;
                        if (!dungeonGetInfo) {
                            endDungeon('noDungeon', res);
                            return;
                        }
                        console.log("Начинаем копать на фулл: ", new Date());
                        let teamGetAll = res[1].result.response;
                        let teamGetFavor = res[2].result.response;
                        dungeonActivity = res[3].result.response.stat.todayDungeonActivity;
                        startDungeonActivity = res[3].result.response.stat.todayDungeonActivity;
                        titansStates = dungeonGetInfo.states.titans;

                        teams.hero = {
                            favor: teamGetFavor.dungeon_hero,
                            heroes: teamGetAll.dungeon_hero.filter(id => id < 6000),
                            teamNum: 0,
                        }
                        let heroPet = teamGetAll.dungeon_hero.filter(id => id >= 6000).pop();
                        if (heroPet) {
                            teams.hero.pet = heroPet;
                        }
                        teams.neutral = getTitanTeam('neutral');
                        teams.water = {
                            favor: {},
                            heroes: getTitanTeam('water'),
                            teamNum: 0,
                        };
                        teams.earth = {
                            favor: {},
                            heroes: getTitanTeam('earth'),
                            teamNum: 0,
                        };
                        teams.fire = {
                            favor: {},
                            heroes: getTitanTeam('fire'),
                            teamNum: 0,
                        };

                        checkFloor(dungeonGetInfo);
                    }

                    function getTitanTeam(type) {
                        switch (type) {
                            case 'neutral':
                                return [4023, 4022, 4012, 4021, 4011, 4010, 4020];
                            case 'water':
                                return [4000, 4001, 4002, 4003]
                                    .filter(e => !titansStates[e]?.isDead);
                            case 'earth':
                                return [4020, 4022, 4021, 4023]
                                    .filter(e => !titansStates[e]?.isDead);
                            case 'fire':
                                return [4010, 4011, 4012, 4013]
                                    .filter(e => !titansStates[e]?.isDead);
                        }
                    }

                    /** Создать копию объекта */
                    function clone(a) {
                        return JSON.parse(JSON.stringify(a));
                    }

                    /** Находит стихию на этаже */
                    function findElement(floor, element) {
                        for (let i in floor) {
                            if (floor[i].attackerType === element) {
                                return i;
                            }
                        }
                        return undefined;
                    }

                    /** Проверяем этаж */
                    async function checkFloor(dungeonInfo) {
                        if (!('floor' in dungeonInfo) || dungeonInfo.floor?.state == 2) {
                            saveProgress();
                            return;
                        }
                        checkTalent(dungeonInfo);
                        // console.log(dungeonInfo, dungeonActivity);
                        setProgress(`${I18N('DUNGEON2')}: ${I18N('TITANIT')} ${dungeonActivity}/${maxDungeonActivity} ${talentMsg}`);
                        //setProgress('Dungeon: Титанит ' + dungeonActivity + '/' + maxDungeonActivity);
                        if (dungeonActivity >= maxDungeonActivity) {
                            endDungeon('Стоп подземка,', 'набрано титанита: ' + dungeonActivity + '/' + maxDungeonActivity);
                            return;
                        }
                        let activity = dungeonActivity - startDungeonActivity;
                        titansStates = dungeonInfo.states.titans;
                        if (window.stopDung){
                            endDungeon('Стоп подземка,', 'набрано титанита: ' + dungeonActivity + '/' + maxDungeonActivity);
                            return;
                        }
                        /*if (activity / 1000 > countShowStats) {
                            countShowStats++;
                            showStats();
                        }*/
                        bestBattle = {};
                        let floorChoices = dungeonInfo.floor.userData;
                        if (floorChoices.length > 1) {
                            for (let element in teams) {
                                let teamNum = findElement(floorChoices, element);
                                if (!!teamNum) {
                                    if (element == 'earth') {
                                        teamNum = await chooseEarthOrFire(floorChoices);
                                        if (teamNum < 0) {
                                            endDungeon('Невозможно победить без потери Титана!', dungeonInfo);
                                            return;
                                        }
                                    }
                                    chooseElement(floorChoices[teamNum].attackerType, teamNum);
                                    return;
                                }
                            }
                        } else {
                            chooseElement(floorChoices[0].attackerType, 0);
                        }
                    }
                    //тест черепахи
                    async function checkTalent(dungeonInfo) {
                        const talent = dungeonInfo.talent;
                        if (!talent) {
                            return;
                        }
                        const dungeonFloor = +dungeonInfo.floorNumber;
                        const talentFloor = +talent.floorRandValue;
                        let doorsAmount = 3 - talent.conditions.doorsAmount;

                        if (dungeonFloor === talentFloor && (!doorsAmount || !talent.conditions?.farmedDoors[dungeonFloor])) {
                            const reward = await Send({
                                calls: [
                                    { name: 'heroTalent_getReward', args: { talentType: 'tmntDungeonTalent', reroll: false }, ident: 'group_0_body' },
                                    { name: 'heroTalent_farmReward', args: { talentType: 'tmntDungeonTalent' }, ident: 'group_1_body' },
                                ],
                            }).then((e) => e.results[0].result.response);
                            const type = Object.keys(reward).pop();
                            const itemId = Object.keys(reward[type]).pop();
                            const count = reward[type][itemId];
                            const itemName = cheats.translate(`LIB_${type.toUpperCase()}_NAME_${itemId}`);
                            talentMsgReward += `<br> ${count} ${itemName}`;
                            doorsAmount++;
                        }
                        talentMsg = `<br>TMNT Talent: ${doorsAmount}/3 ${talentMsgReward}<br>`;
                    }

                    /** Выбираем огнем или землей атаковать */
                    async function chooseEarthOrFire(floorChoices) {
                        bestBattle.recovery = -11;
                        let selectedTeamNum = -1;
                        for (let attempt = 0; selectedTeamNum < 0 && attempt < 4; attempt++) {
                            for (let teamNum in floorChoices) {
                                let attackerType = floorChoices[teamNum].attackerType;
                                selectedTeamNum = await attemptAttackEarthOrFire(teamNum, attackerType, attempt);
                            }
                        }
                        console.log("Выбор команды огня или земли: ", selectedTeamNum < 0 ? "не сделан" : floorChoices[selectedTeamNum].attackerType);
                        return selectedTeamNum;
                    }

                    /** Попытка атаки землей и огнем */
                    async function attemptAttackEarthOrFire(teamNum, attackerType, attempt) {
                        let start = new Date();
                        let team = clone(teams[attackerType]);
                        let startIndex = team.heroes.length + attempt - 4;
                        if (startIndex >= 0) {
                            team.heroes = team.heroes.slice(startIndex);
                            let recovery = await getBestRecovery(teamNum, attackerType, team, 25);
                            if (recovery > bestBattle.recovery) {
                                bestBattle.recovery = recovery;
                                bestBattle.selectedTeamNum = teamNum;
                                bestBattle.team = team;
                            }
                        }
                        let workTime = new Date().getTime() - start.getTime();
                        timeDungeon.attackEarthOrFire += workTime;
                        if (bestBattle.recovery < -10) {
                            return -1;
                        }
                        return bestBattle.selectedTeamNum;
                    }

                    /** Выбираем стихию для атаки */
                    async function chooseElement(attackerType, teamNum) {
                        let result;
                        switch (attackerType) {
                            case 'hero':
                            case 'water':
                                result = await startBattle(teamNum, attackerType, teams[attackerType]);
                                break;
                            case 'earth':
                            case 'fire':
                                result = await attackEarthOrFire(teamNum, attackerType);
                                break;
                            case 'neutral':
                                result = await attackNeutral(teamNum, attackerType);
                        }
                        if (!!result && attackerType != 'hero') {
                            let recovery = (!!!bestBattle.recovery ? 10 * getRecovery(result) : bestBattle.recovery) * 100;
                            let titans = result.progress[0].attackers.heroes;
                            console.log("Проведен бой: " + attackerType +
                                        ", recovery = " + (recovery > 0 ? "+" : "") + Math.round(recovery) + "% \r\n", titans);
                        }
                        endBattle(result);
                    }

                    /** Атакуем Землей или Огнем */
                    async function attackEarthOrFire(teamNum, attackerType) {
                        if (!!!bestBattle.recovery) {
                            bestBattle.recovery = -11;
                            let selectedTeamNum = -1;
                            for (let attempt = 0; selectedTeamNum < 0 && attempt < 4; attempt++) {
                                selectedTeamNum = await attemptAttackEarthOrFire(teamNum, attackerType, attempt);
                            }
                            if (selectedTeamNum < 0) {
                                endDungeon('Невозможно победить без потери Титана!', attackerType);
                                return;
                            }
                        }
                        return findAttack(teamNum, attackerType, bestBattle.team);
                    }

                    /** Находим подходящий результат для атаки */
                    async function findAttack(teamNum, attackerType, team) {
                        let start = new Date();
                        let recovery = -1000;
                        let iterations = 0;
                        let result;
                        let correction = 0.01;
                        for (let needRecovery = bestBattle.recovery; recovery < needRecovery; needRecovery -= correction, iterations++) {
                            result = await startBattle(teamNum, attackerType, team);
                            recovery = getRecovery(result);
                        }
                        bestBattle.recovery = recovery;
                        let workTime = new Date().getTime() - start.getTime();
                        timeDungeon.findAttack += workTime;
                        return result;
                    }

                    /** Атакуем Нейтральной командой */
                    async function attackNeutral(teamNum, attackerType) {
                        let start = new Date();
                        let factors = calcFactor();
                        bestBattle.recovery = -0.2;
                        await findBestBattleNeutral(teamNum, attackerType, factors, true)
                        if (bestBattle.recovery < 0 || (bestBattle.recovery < 0.2 && factors[0].value < 0.5)) {
                            let recovery = 100 * bestBattle.recovery;
                            console.log("Не удалось найти удачный бой в быстром режиме: " + attackerType +
                                        ", recovery = " + (recovery > 0 ? "+" : "") + Math.round(recovery) + "% \r\n", bestBattle.attackers);
                            await findBestBattleNeutral(teamNum, attackerType, factors, false)
                        }
                        let workTime = new Date().getTime() - start.getTime();
                        timeDungeon.attackNeutral += workTime;
                        if (!!bestBattle.attackers) {
                            let team = getTeam(bestBattle.attackers);
                            return findAttack(teamNum, attackerType, team);
                        }
                        endDungeon('Не удалось найти удачный бой!', attackerType);
                        return undefined;
                    }

                    /** Находит лучшую нейтральную команду */
                    async function findBestBattleNeutral(teamNum, attackerType, factors, mode) {
                        let countFactors = factors.length < 4 ? factors.length : 4;
                        let aradgi = !titansStates['4013']?.isDead;
                        let edem = !titansStates['4023']?.isDead;
                        let dark = [4032, 4033].filter(e => !titansStates[e]?.isDead);
                        let light = [4042].filter(e => !titansStates[e]?.isDead);
                        let actions = [];
                        if (mode) {
                            for (let i = 0; i < countFactors; i++) {
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(factors[i].id)));
                            }
                            if (countFactors > 1) {
                                let firstId = factors[0].id;
                                let secondId = factors[1].id;
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4001, secondId)));
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4002, secondId)));
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4003, secondId)));
                            }
                            if (aradgi) {
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(4013)));
                                if (countFactors > 0) {
                                    let firstId = factors[0].id;
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4000, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4001, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4002, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4003, 4013)));
                                }
                                if (edem) {
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(4023, 4000, 4013)));
                                }
                            }
                        } else {
                            if (mode) {
                                for (let i = 0; i < factors.length; i++) {
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(factors[i].id)));
                                }
                            } else {
                                countFactors = factors.length < 2 ? factors.length : 2;
                            }
                            for (let i = 0; i < countFactors; i++) {
                                let mainId = factors[i].id;
                                if (aradgi && (mode || i > 0)) {
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4000, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4001, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4002, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4003, 4013)));
                                }
                                for (let j = 0; j < dark.length; j++) {
                                    let darkId = dark[j];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4001, darkId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4002, darkId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4003, darkId)));
                                }
                                for (let j = 0; j < light.length; j++) {
                                    let lightId = light[j];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4001, lightId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4002, lightId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4003, lightId)));
                                }
                                let isFull = mode || i > 0;
                                for (let j = isFull ? i + 1 : 2; j < factors.length; j++) {
                                    let extraId = factors[j].id;
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4000, extraId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4001, extraId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(mainId, 4002, extraId)));
                                }
                            }
                            if (aradgi) {
                                if (mode) {
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(4013)));
                                }
                                for (let j = 0; j < dark.length; j++) {
                                    let darkId = dark[j];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(darkId, 4001, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(darkId, 4002, 4013)));
                                }
                                for (let j = 0; j < light.length; j++) {
                                    let lightId = light[j];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(lightId, 4001, 4013)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(lightId, 4002, 4013)));
                                }
                            }
                            for (let j = 0; j < dark.length; j++) {
                                let firstId = dark[j];
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId)));
                                for (let k = j + 1; k < dark.length; k++) {
                                    let secondId = dark[k];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4001, secondId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4002, secondId)));
                                }
                            }
                            for (let j = 0; j < light.length; j++) {
                                let firstId = light[j];
                                actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId)));
                                for (let k = j + 1; k < light.length; k++) {
                                    let secondId = light[k];
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4001, secondId)));
                                    actions.push(startBattle(teamNum, attackerType, getNeutralTeam(firstId, 4002, secondId)));
                                }
                            }
                        }
                        for (let result of await Promise.all(actions)) {
                            let recovery = getRecovery(result);
                            if (recovery > bestBattle.recovery) {
                                bestBattle.recovery = recovery;
                                bestBattle.attackers = result.progress[0].attackers.heroes;
                            }
                        }
                    }

                    /** Получаем нейтральную команду */
                    function getNeutralTeam(id, swapId, addId) {
                        let neutralTeam = clone(teams.water);
                        let neutral = neutralTeam.heroes;
                        if (neutral.length == 4) {
                            if (!!swapId) {
                                for (let i in neutral) {
                                    if (neutral[i] == swapId) {
                                        neutral[i] = addId;
                                    }
                                }
                            }
                        } else if (!!addId) {
                            neutral.push(addId);
                        }
                        neutral.push(id);
                        return neutralTeam;
                    }

                    /** Получить команду титанов */
                    function getTeam(titans) {
                        return {
                            favor: {},
                            heroes: Object.keys(titans).map(id => parseInt(id)),
                            teamNum: 0,
                        };
                    }

                    /** Вычисляем фактор боеготовности титанов */
                    function calcFactor() {
                        let neutral = teams.neutral;
                        let factors = [];
                        for (let i in neutral) {
                            let titanId = neutral[i];
                            let titan = titansStates[titanId];
                            let factor = !!titan ? titan.hp / titan.maxHp + titan.energy / 10000.0 : 1;
                            if (factor > 0) {
                                factors.push({id: titanId, value: factor});
                            }
                        }
                        factors.sort(function(a, b) {
                            return a.value - b.value;
                        });
                        return factors;
                    }

                    /** Возвращает наилучший результат из нескольких боев */
                    async function getBestRecovery(teamNum, attackerType, team, countBattle) {
                        let bestRecovery = -1000;
                        let actions = [];
                        for (let i = 0; i < countBattle; i++) {
                            actions.push(startBattle(teamNum, attackerType, team));
                        }
                        for (let result of await Promise.all(actions)) {
                            let recovery = getRecovery(result);
                            if (recovery > bestRecovery) {
                                bestRecovery = recovery;
                            }
                        }
                        return bestRecovery;
                    }

                    /** Возвращает разницу в здоровье атакующей команды после и до битвы и проверяет здоровье титанов на необходимый минимум*/
                    function getRecovery(result) {
                        if (result.result.stars < 3) {
                            return -100;
                        }
                        let beforeSumFactor = 0;
                        let afterSumFactor = 0;
                        let beforeTitans = result.battleData.attackers;
                        let afterTitans = result.progress[0].attackers.heroes;
                        for (let i in afterTitans) {
                            let titan = afterTitans[i];
                            let percentHP = titan.hp / beforeTitans[i].hp;
                            let energy = titan.energy;
                            let factor = checkTitan(i, energy, percentHP) ? getFactor(i, energy, percentHP) : -100;
                            afterSumFactor += factor;
                        }
                        for (let i in beforeTitans) {
                            let titan = beforeTitans[i];
                            let state = titan.state;
                            beforeSumFactor += !!state ? getFactor(i, state.energy, state.hp / titan.hp) : 1;
                        }
                        return afterSumFactor - beforeSumFactor;
                    }

                    /** Возвращает состояние титана*/
                    function getFactor(id, energy, percentHP) {
                        let elemantId = id.toString().slice(2, 3);
                        let isEarthOrFire = elemantId == '1' || elemantId == '2';
                        let energyBonus = id == '4020' && energy == 1000 ? 0.1 : energy / 20000.0;
                        let factor = percentHP + energyBonus;
                        return isEarthOrFire ? factor : factor / 10;
                    }

                    /** Проверяет состояние титана*/
                    function checkTitan(id, energy, percentHP) {
                        switch (id.toString()) {
                            case '4020':
                                return percentHP > 0.25 || (energy == 1000 && percentHP > 0.05);
                            case '4010':
                                return percentHP + energy / 2000.0 > 0.63;
                            case '4000':
                                return percentHP > 0.62 || (energy < 1000 && (
                                    (percentHP > 0.45 && energy >= 400) ||
                                    (percentHP > 0.3 && energy >= 670)));
                        }
                        return true;
                    }


                    /** Начинаем бой */
                    function startBattle(teamNum, attackerType, args) {
                        return new Promise(function (resolve, reject) {
                            args.teamNum = teamNum;
                            let startBattleCall = {
                                calls: [{
                                    name: "dungeonStartBattle",
                                    args,
                                    ident: "body"
                                }]
                            }
                            send(startBattleCall, resultBattle, {
                                resolve,
                                teamNum,
                                attackerType
                            });
                        });
                    }

                    /** Возращает результат боя в промис */
                    function resultBattle(resultBattles, args) {
                    battleData = resultBattles.results[0].result.response;
                    battleType = "get_tower";
                    if (battleData.type == "dungeon_titan") {
                        battleType = "get_titan";
                    }
                    battleData.progress = [{ attackers: { input: ["auto", 0, 0, "auto", 0, 0] } }];
                    BattleCalc(battleData, battleType, function (result) {
                        result.teamNum = args.teamNum;
                        result.attackerType = args.attackerType;
                        args.resolve(result);
                    });
                    }

                    /** Заканчиваем бой */

                    ////
                    async function endBattle(battleInfo) {
                    if (!!battleInfo) {
                        const args = {
                            result: battleInfo.result,
                            progress: battleInfo.progress,
                        }
                        if (battleInfo.result.stars < 3) {
                                endDungeon('Герой или Титан мог погибнуть в бою!', battleInfo);
                                return;
                            }
                        if (HWHData.countPredictionCard > 0) {
                            args.isRaid = true;
                        } else {
                            const timer = getTimer(battleInfo.battleTime);
                            console.log(timer);
                            await countdownTimer(timer, `${I18N('DUNGEON2')}: ${I18N('TITANIT')} ${dungeonActivity}/${maxDungeonActivity} ${talentMsg}`);
                        }
                        const calls = [{
                            name: "dungeonEndBattle",
                            args,
                            ident: "body"
                        }];
                        send({ calls }, resultEndBattle);
                    } else {
                        endDungeon('dungeonEndBattle win: false\n', battleInfo);
                    }
                    }
                    /** Получаем и обрабатываем результаты боя */
                    function resultEndBattle(e) {
                        if (!!e && !!e.results) {
                            let battleResult = e.results[0].result.response;
                            if ('error' in battleResult) {
                                endDungeon('errorBattleResult', battleResult);
                                return;
                            }
                            let dungeonGetInfo = battleResult.dungeon ?? battleResult;
                            dungeonActivity += battleResult.reward.dungeonActivity ?? 0;
                            checkFloor(dungeonGetInfo);
                        } else {
                            endDungeon('Потеряна связь с сервером игры!', 'break');
                        }
                    }

                    /** Добавить команду титанов в общий список команд */
                    function addTeam(team) {
                        for (let i in countTeam) {
                            if (equalsTeam(countTeam[i].team, team)) {
                                countTeam[i].count++;
                                return;
                            }
                        }
                        countTeam.push({team: team, count: 1});
                    }

                    /** Сравнить команды на равенство */
                    function equalsTeam(team1, team2) {
                        if (team1.length == team2.length) {
                            for (let i in team1) {
                                if (team1[i] != team2[i]) {
                                    return false;
                                }
                            }
                            return true;
                        }
                        return false;
                    }

                    function saveProgress() {
                        let saveProgressCall = {
                            calls: [{
                                name: "dungeonSaveProgress",
                                args: {},
                                ident: "body"
                            }]
                        }
                        send(saveProgressCall, resultEndBattle);
                    }


                    /** Выводит статистику прохождения подземелья */
                    function showStats() {
                        let activity = dungeonActivity - startDungeonActivity;
                        let workTime = clone(timeDungeon);
                        workTime.all = new Date().getTime() - workTime.all;
                        for (let i in workTime) {
                            workTime[i] = Math.round(workTime[i] / 1000);
                        }
                        countTeam.sort(function(a, b) {
                            return b.count - a.count;
                        });
                        console.log(titansStates);
                        console.log("Собрано титанита: ", activity);
                        console.log("Скорость сбора: " + Math.round(3600 * activity / workTime.all) + " титанита/час");
                        console.log("Время раскопок: ");
                        for (let i in workTime) {
                            let timeNow = workTime[i];
                            let hours = Math.round(timeNow / 3600);
                            let mins = Math.round(timeNow % 3600 / 60);
                            console.log(i + ": ", hours + " ч. " + mins + " мин. " + timeNow % 60 + " сек.");
                        }
                        console.log("Частота использования команд: ");
                        for (let i in countTeam) {
                            let teams = countTeam[i];
                            console.log(teams.team + ": ", teams.count);
                        }
                    }

                    /** Заканчиваем копать подземелье */
                    function endDungeon(reason, info) {
                        if (!end) {
                            end = true;
                            console.log(reason, info);
                            showStats();
                            if (info == 'break') {
                                setProgress('Dungeon stoped: Титанит ' + dungeonActivity + '/' + maxDungeonActivity +
                                            "\r\nПотеряна связь с сервером игры!", true);
                            } else {
                                setProgress('Dungeon completed: Титанит ' + dungeonActivity + '/' + maxDungeonActivity, true);
                            }
                            resolve();
                        }
                    }
            };
        } // end executeDungeon override scope

        // ========== 6. 版本号显示 [dungeon] ==========
        (function patchVersion() {
            const scriptMenu = HWHClasses.ScriptMenu.getInst();
            if (!scriptMenu || !scriptMenu.mainMenu) {
                setTimeout(patchVersion, 500);
                return;
            }
            const headers = scriptMenu.mainMenu.querySelectorAll('.scriptMenu_header');
            if (headers.length >= 2) {
                const ver = headers[1];
                if (!ver.innerText.includes('[dungeon]')) {
                    ver.innerText += '[dungeon]';
                    ver.style.color = '#ff9900';
                }
            }
        })();

        console.log('%c[HeroWarsDungeonFull] 已注入复杂地下城策略', 'color: orange');
    });
})();