HeroWarsDungeonFull

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 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');
    });
})();