MugenHelper

use at your own risk

// ==UserScript==
// @name         MugenHelper
// @namespace    Joeyvan@Joybrick
// @version      2.7.2
// @description  use at your own risk
// @author       Joeyvan
// @match        *://badgameshow.com/top.cgi
// @match        *://www.badgameshow.com/top.cgi
// @match        *://laborrtious.ddns.net/top.cgi
// @match        *://behind.laborrtious.tk/top.cgi
// @match        *://172.104.88.92/hero/top.cgi
// @match        *://catding.tw/hero/top.cgi
// @match        *://changame.ml/top.cgi
// @match        *://changame2.ml/top.cgi
// @require      http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js
// @require      http://code.jquery.com/ui/1.9.2/jquery-ui.js
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @grant        unsafeWindow
// ==/UserScript==

//===== config =====
const StorageKey = "MugenHelper" + document.domain;
const DefaultDelay = 1 * 1000;
const DefaultRetryTimes = 3;
//
const DefaultUIPositionTop = 10;
const DefaultUIPositionLeft = 10;
//
const ControllerInterval = 1.5 * 1000;
//
const IsHeartBeatActivate = true;
const HeartbeatInterval = 10 * 1000;
const HeartbeatMaxCount = 2;
const IsForceRefresh = false;
const ForceRefreshInterval = 15 * 1000;
//
const QuestInterval = 5 * 1000;
//
const BankInterval = 5 * 1000;
//
const InnInterval = 5 * 1000;
const InnHPThreshold = 50;
const InnMPThreshold = 50;
//
const MajoInterval = 5 * 1000;
//
const CleanConsoleInterval = 30 * 60 * 1000;
//
const validMap = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,20,21,22,23,24,30,31,32,40,41,42,43,44,50,51,55,56];
const savedMap = [1,2,3,4,30,31,40,41,42,43,51,55,56];
const IsCatDingVersion = document.domain.indexOf("catding") >= 0 || document.domain.indexOf("172.104.88.92") >= 0;
//==================

//===== Debug config =====
const ShowLog = false;
const ShowInfo = false;
const ShowWarnLog = false;
const ShowErrorLog = true;
const DebugMode = false;
//=========================

class XHR
{
    MakeRequest(method, url, param)
    {
        return new Promise(function (resolve, reject)
        {
            let xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
            xhr.onload = () =>
            {
                if (xhr.status >= 200 && xhr.status < 300)
                {
                    resolve(xhr.response);
                }
                else
                {
                    reject({
                        status: xhr.status,
                        statusText: xhr.statusText
                    });
                }
            };

            xhr.onerror = () =>
            {
                reject({
                    status: xhr.status,
                    statusText: xhr.statusText
                });
            };

            if(param != undefined)
                xhr.send(param);
            else
                xhr.send();

            _Logger.log(`${method} ${param} to ${url}`);
        });
    }
}

class Selector
{
    has(object, key)
    {
        return object ? hasOwnProperty.call(object, key) : false;
    }

    async $(selector, rootnode = unsafeWindow, retry = true)
    {
        var result = rootnode.document.querySelector(selector);
        if(result)
        {
            _Logger.logInfo("found \"" + selector + "\" at \"" + rootnode + "\" : \"" + result + "\"");
            return result;
        }
        else
        {
            if(retry)
            {
                _Logger.logWarning("can't find element : " + selector + "\n at " + rootnode);
                await _DelayHelper.sleep();
                return await this.$(selector, rootnode);
            }
        }

        return undefined;
    }

    async getIframeWindow(selector, rootnode = unsafeWindow, targetProperty = undefined)
    {
        var iframe_object = await this.$(selector, rootnode);
        var doc;
        var result = undefined;

        if (iframe_object.contentWindow)
        {
            result = iframe_object.contentWindow;
        }

        if (iframe_object.unsafeWindow)
        {
            result = iframe_object.unsafeWindow;
        }

        if (!doc && iframe_object.contentDocument)
        {
            doc = iframe_object.contentDocument;
        }

        if (!doc && iframe_object.document)
        {
            doc = iframe_object.document;
        }

        if (doc && doc.defaultView)
        {
            result = doc.defaultView;
        }

        if (doc && doc.parentWindow)
        {
            result = doc.parentWindow;
        }

        if(result == undefined || targetProperty != undefined && this.has(result, targetProperty) == false)
        {
            _Logger.logWarning("can't get iframe unsafeWindow.");
            await _DelayHelper.sleep();
            return await this.getIframeWindow(selector, rootnode, targetProperty);
        }
        return result;
    }
}

class Logger
{
    constructor()
    {
        unsafeWindow.actionframe.alert = (message) => this.log(message);
        unsafeWindow.alert = (message) => this.log(message);
    }

    log(message)
    {
        if(ShowLog) console.log(message);
    }

    logInfo(message)
    {
        if(ShowInfo) console.info(message);
    }

    logError(message)
    {
        if(ShowErrorLog) console.error(message);
    }

    logWarning(message)
    {
        if(ShowWarnLog) console.warn(message)
    }

    clear()
    {
        console.clear();
    }
}

class GameCoreWrapper
{
    constructor()
    {
        this.main = unsafeWindow;
        this.frame = unsafeWindow.actionframe;
    }

    async retry(callback, times = DefaultRetryTimes)
    {
        if(typeof(callback) != "function")
        {
            throw "callback is not a function.";
        }

        if(times == 0)
        {
            throw "something wrong happened, please contact script author.";
        }

        try
        {
            return await callback();
        }
        catch(ex)
        {
            _Logger.logError(ex);
            
            if(ex.status == 503)
            {
                let statusButton = await _Selector.$("#statusbutton");
                statusButton.click();
                await _DelayHelper.sleep(100000);
                await _GameWrapper.BackTown();
            }

            await _DelayHelper.sleep();
            return await this.retry(callback, times - 1);
        }
    }

    async BackTown()
    {
        await this.retry(() => {
            this.main.backtown();
        });
        _Logger.logInfo("Back to town.");
    }

    async Teleport(place, facility)
    {
        await this.retry(() => {
            this.main.fastkeyform(place, facility);
        });
        _Logger.logInfo("Teleport to " + place + "'s " + facility + ".");
    }

    async Refresh()
    {
        await this.retry(() => {
            this.main.get_all_data();
        });

        _Logger.logInfo("Refresh.");
    }

    async GetObj(id)
    {
        return await this.retry(() => {
            return this.main.getObj(id);
        });
    }

    async GetServerSyncTime()
    {
        return parseInt(this.main.moya);
    }

    async SetNextBattleTime(value)
    {
        this.main.BTIME = value;
    }

    async GetNextBattleTime()
    {
        return parseInt(this.main.BTIME);
    }

    async WaitUntilNextBattleEnd()
    {
        let second = await this.retry(() => {
            return this.GetNextBattleTime();
        });
        let millisecond = ((second + 1) * 1000) + DefaultDelay;

        await _DelayHelper.sleep(millisecond);
    }

    async GetAutoAttack()
    {
        return await this.GetObj('autoattack');
    }

    async DoBattle()
    {
        let id = (await _Selector.$("#battlef > input[type=hidden]:nth-child(1)")).value;
        let pass = (await _Selector.$("#battlef > input[type=hidden]:nth-child(2)")).value;
        let rnd = moya;
        let mode = (await _Selector.$("select[name=mode]")).value;

        let url = "battle.cgi";
        let param = `id=${id}&pass=${pass}&rmode=&rnd=${rnd}&mode=${mode}&rnd2=${rnd}`;

        await this.retry(async () => {
            let result = await _XHR.MakeRequest('POST', url, param);
            if(DebugMode)
            {
                let regex = result.match(/離下次可戰鬥時間剩([\d]+)[\W]?秒。/);
                if(regex != undefined && regex[1] <= 1)
                    throw regex[0];
            }
        });

        _Logger.logInfo(`${new Date()} 進行了一次戰鬥`);

        await this.Refresh();
    }
}

class DelayHelper
{
    async sleep(ms = DefaultDelay)
    {
        return new Promise(r => setTimeout(r, ms));
    }
}

class Task
{
    constructor(callback, interval, immediate = true)
    {
        if(typeof(callback) != "function")
        {
            throw "Task type error"
        }

        this.callback = callback;
        this.interval = interval;

        if(immediate)
        {
            this.nextTime = Date.now();
        }
        else
        {
            this.SetNextTime();
        }
    }

    SetUniqureId(id)
    {
        this.id = id;
    }

    SetNextTime()
    {
        let now = Date.now();
        this.nextTime = now + this.interval;
    }

    IsOverdue()
    {
        let now = Date.now();
        return now >= this.nextTime;
    }

    async Excute()
    {
        await this.callback();
        this.SetNextTime();
    }
}

class TaskController
{
    constructor()
    {
        this.Tasks = [];
        this.handle = undefined;
        this.IsBusy = false;
    }

    AddTask(callback, interval)
    {
        let task = new Task(callback, interval);
        task.SetUniqureId(this.taskCounter++);

        this.Tasks.push(task);

        return task.id;
    }

    ClearTask()
    {
        this.Tasks.length = 0;
    }

    RemoveTask(id)
    {
        this.Tasks = this.Tasks.filter(x => x.id != id);
    }

    async DoTasks()
    {
        if(this.IsBusy)
            return;

        try
        {
            this.IsBusy = true;

            for(let task of this.Tasks)
            {
                if(task.IsOverdue())
                {
                    await task.Excute();
                    await _DelayHelper.sleep();
                }
            }
        }
        catch(ex)
        {
            _Logger.logError(ex);
        }
        finally
        {
            this.IsBusy = false;
        }
    }

    StartInterval(timeout = ControllerInterval)
    {
        if(this.handle != undefined)
        {
            throw "TaskController was ready";
        }

        this.handle = setInterval(this.DoTasks.bind(this), timeout);
    }

    StopInterval()
    {
        if(this.handle == undefined)
        {
            return;
        }

        clearInterval(this.handle);
        this.handle = undefined;
    }
}

class StorageManager
{
    Initialize()
    {
        this._data = {};
        let data = GM_getValue(StorageKey, "{}");
        this._data = JSON.parse(data);
    }

    OnLeave()
    {
        let data = JSON.stringify(this._data);
        GM_setValue(StorageKey, data);
    }

    Get(key, defaultValue)
    {
        if(this._data[key] == undefined)
            this._data[key] = defaultValue;

        return this._data[key];
    }

    Set(key, value)
    {
        this._data[key] = value;
    }
}

class UIManager
{
    Initialize()
    {
        this.IsActivate = false;
        this._ZIndex = 0;
        this._top = _StorageManager.Get("top", DefaultUIPositionTop);
        this._left = _StorageManager.Get("left", DefaultUIPositionLeft);
        this.UpdateZIndex();
        this.CreateMainWindow();

        _HeartBeat.SetupUI();
        _Quest.SetupUI();
        _Bank.SetupUI();
        _Inn.SetupUI();
        _WareHouse.SetupUI();
        _Majo.SetupUI();

        this.CreateControlButton();
    }

    OnLeave()
    {
        _StorageManager.Set("top", this._top);
        _StorageManager.Set("left", this._left);
    }

    UpdateZIndex()
    {
        $("div").each((index, obj) => {
            let _current = parseInt($(obj).css("zIndex"), 10);

            if(isNaN(_current) == false && _current > this._ZIndex) {
                this._ZIndex = _current + 1;
            }
        });
    }

    CreateMainWindow()
    {
        $('body').append('<div id = "MainWindow" class = "ui-widget-header">MugenHelper v2.7.2</div>');

        $("#MainWindow").css("position", "absolute");
        $("#MainWindow").css("top", this._top + "px");
        $("#MainWindow").css("left", this._left + "px");
        $("#MainWindow").css("z-index", this._ZIndex);
        $("#MainWindow").css("background", "#ecebeb");
        $("#MainWindow").css("border", 1 + "px solid #333");
        $("#MainWindow").css("border-radius", 5 + "px");
        $("#MainWindow").css("height", "auto");
        $("#MainWindow").css("width", 280);
        $("#MainWindow").css("margin", "0px auto");
        $("#MainWindow").draggable();
        $("#MainWindow").mouseup((event) => {
            this._top = parseInt($("#MainWindow").css("top")) - $(unsafeWindow).scrollTop();
            this._left = parseInt($("#MainWindow").css("left"));
        });

        $(unsafeWindow).scroll((event) => {
            let topValue = $(unsafeWindow).scrollTop();
            $("#MainWindow").css("top", (topValue + this._top) + "px");
        });
    }

    CreateControlButton()
    {
        $("#MainWindow").append("<div><input type=\"button\" id=\"StartButton\" value=\"開始\"></input><input type=\"button\" id=\"StopButton\" value=\"暫停\"></input><span id=\"status\">未執行</span><div>");
        $("#StartButton").click(() => {
            if(this.IsActivate)
                return;
            this.IsActivate = true;
            $("#status").html("執行中");
            $("#status").css("color", "green");
            $("#StartButton").attr("disabled", "disabled");
            $("#StopButton").removeAttr("disabled");
            this.UpdateUI();

            if($("#IsMajoActivate").attr('checked'))
                _TaskController.AddTask(_Majo.DoScript.bind(_Majo), MajoInterval);
            if($("#IsHeartBeatActivate").attr('checked'))
            {
                _TaskController.AddTask(_HeartBeat.DoScript.bind(_HeartBeat), HeartbeatInterval);
            }
            if($("#IsBankActivate").attr('checked'))
                _TaskController.AddTask(_Bank.DoScript.bind(_Bank), BankInterval);
            if($("#IsInnActivate").attr('checked'))
                _TaskController.AddTask(_Inn.DoScript.bind(_Inn), InnInterval);
            if($("#IsQuestActivate").attr('checked'))
                _TaskController.AddTask(_Quest.DoScript.bind(_Quest), QuestInterval);
            if($("#IsWareHouseActivate").attr('checked'))
                _TaskController.AddTask(_WareHouse.DoScript.bind(_WareHouse), Math.min(Math.max(parseInt($("#WareHouseInterval").val()), 30), 8640) * 1000);

            _TaskController.AddTask(_Logger.clear.bind(_Logger), CleanConsoleInterval);
            _TaskController.StartInterval();
        });

        $("#StopButton").click(() => {
            if(this.IsActivate == false)
                return;
            this.IsActivate = false;
            $("#status").html("暫停中");
            $("#status").css("color", "red");
            $("#StopButton").attr("disabled", "disabled");
            $("#StartButton").removeAttr("disabled");
            this.UpdateUI();

            _HeartBeat.counter = 0;
            _Quest.CompleteCount = -1;
            _TaskController.StopInterval();
            _TaskController.ClearTask();
        });
    }

    UpdateUI()
    {
        _HeartBeat.UpdateUI(this.IsActivate);
        _Quest.UpdateUI(this.IsActivate);
        _Bank.UpdateUI(this.IsActivate);
        _Inn.UpdateUI(this.IsActivate);
        _Majo.UpdateUI(this.IsActivate);
        _WareHouse.UpdateUI(this.IsActivate);
    }
}

class HeartBeat
{
    constructor()
    {
        this.counter = 0;
        this.temp = undefined;
    }

    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsHeartBeatActivate\"></input>維持連線</div>");
        $("#ForceRefreshInterval").css("width", 25);
        $("#IsHeartBeatActivate").change(() => {
            if($("#IsHeartBeatActivate").attr('checked'))
                $("#IsForceRefresh").removeAttr("disabled");
            else
                $("#IsForceRefresh").attr("disabled", "disabled");
        });
        $("#IsForceRefresh").change(() => {
            if($("#IsForceRefresh").attr('checked'))
                $("#ForceRefreshInterval").removeAttr("disabled");
            else
                $("#ForceRefreshInterval").attr("disabled", "disabled");
        });
        this.UpdateUI();
        $("#ForceRefreshInterval").val(_StorageManager.Get("ForceRefreshInterval", ForceRefreshInterval / 1000));
        if(_StorageManager.Get("IsHeartBeatActivate", IsHeartBeatActivate))
        {
            $("#IsHeartBeatActivate").attr('checked','checked')
            if(_StorageManager.Get("IsForceRefresh", IsForceRefresh))
            {
                $("#IsForceRefresh").attr('checked','checked')
            }
            else
            {
                $("#IsForceRefresh").removeAttr("checked");
            }
        }
        else
        {
            $("#ForceRefreshInterval").attr("disabled", "disabled");
            $("#IsForceRefresh").attr("disabled", "disabled");
            $("#IsHeartBeatActivate").removeAttr("checked");
        }


    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsHeartBeatActivate").attr("disabled", "disabled");
            $("#IsForceRefresh").attr("disabled", "disabled");
            $("#ForceRefreshInterval").attr("disabled", "disabled");
        }
        else
        {
            $("#IsHeartBeatActivate").removeAttr("disabled");
            $("#IsForceRefresh").removeAttr("disabled");
            $("#ForceRefreshInterval").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        let systime = await _GameWrapper.GetServerSyncTime();

        if(this.temp == systime)
        {
            this.counter++;

            if(this.counter > HeartbeatMaxCount)
            {
                await this.Refresh();
                this.counter = 0;
            }
        }
        else
        {
            this.counter = 0;
            this.temp = systime;
        }
    }

    async Refresh()
    {
        await _GameWrapper.Refresh();
        _Logger.logInfo("Revive! - " + new Date());

        let autoattck = await _GameWrapper.GetAutoAttack();
        autoattck.checked = true;
    }

    OnLeave()
    {
        _StorageManager.Set("IsHeartBeatActivate", $("#IsHeartBeatActivate").attr('checked') == "checked");
        _StorageManager.Set("IsForceRefresh", $("#IsForceRefresh").attr('checked') == "checked");
        _StorageManager.Set("ForceRefreshInterval", $("#ForceRefreshInterval").val());
    }
}

class Quest
{
    constructor()
    {
        this.CompleteCount = -1;
    }

    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsQuestActivate\">自動任務<div>");
        this.UpdateUI();
        if(_StorageManager.Get("IsQuestActivate", true))
        {
            $("#IsQuestActivate").attr('checked','checked')
        }
        else
        {
            $("#IsQuestActivate").removeAttr("checked");
        }
    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsQuestActivate").attr("disabled", "disabled");
        }
        else
        {
            $("#IsQuestActivate").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        let now = IsCatDingVersion ? parseInt((await _Selector.$("#mtotal")).innerText) : parseInt((await _Selector.$("#mname")).innerText.match(/([\d]+)/)[1]);

        if(this.CompleteCount > 0 && now < this.CompleteCount)
            return;

        await _GameWrapper.WaitUntilNextBattleEnd();

        let autoattck = await _GameWrapper.GetAutoAttack();
        let temp = autoattck.checked;

        await _GameWrapper.Teleport('town','quest');
        await _DelayHelper.sleep();

        let status = await this.GetStatus();

        if(status.IsDone)
        {
            let button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(3) > input.FC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','quest');
            await _DelayHelper.sleep();

            button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(3) > input.FC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','quest');
            await _DelayHelper.sleep();

            status = await this.GetStatus();
            this.CompleteCount = now + status.count;
        }
        else if(status.fuckingBug)
        {
            this.CompleteCount = now + 1;
        }
        else if(status.IsNotGet)
        {
            let button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(3) > input.FC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','quest');
            await _DelayHelper.sleep();

            status = await this.GetStatus();
            this.CompleteCount = now + status.count;
        }
        else if(status.IsIng)
        {
            this.CompleteCount = now + status.count;
        }

        _Logger.logInfo("QuestStatus: " + status);
        _Logger.logInfo("Quest Complete when " + this.CompleteCount + "battles achieved.")

        await _GameWrapper.BackTown();
        await _DelayHelper.sleep();

        autoattck.checked = temp;
    }

    async GetStatus()
    {
        let target = await _Selector.$("body > table > tbody > tr:nth-child(3) > td", unsafeWindow.actionframe);
        let content = target.innerText.trim().split('\n', 1)[0];
        let IsNotGet = content.indexOf("最近各城鎮地怪物變多,請幫忙村民消滅牠們(請在任意城鎮何意地圖打怪)") > 0;
        let IsIng = content.indexOf("目前正在進行") > 0;
        let IsDone = content.indexOf("感謝你的幫忙") > 0;
        let fuckingBug = content.indexOf("購買指定的裝備給指定的任務屋") > 0;
        let count = IsIng ? parseInt(content.match(/怪([\d]+)隻/)[1]) : undefined;

        let result = {IsNotGet: IsNotGet, IsIng: IsIng, IsDone: IsDone, fuckingBug: fuckingBug, count: count};

        return result;
    }

    OnLeave()
    {
        _StorageManager.Set("IsQuestActivate", $("#IsQuestActivate").attr('checked') == "checked");
    }
}

class Bank
{
    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsBankActivate\">自動存錢:<input type=\"text\" id=\"BankThrehold\"></input>萬</div>");
        $("#BankThrehold").css("width", 50);
        $("#IsBankActivate").change(() => {
            if($("#IsBankActivate").attr('checked'))
                $("#BankThrehold").removeAttr("disabled");
            else
                $("#BankThrehold").attr("disabled", "disabled");
        });
        this.UpdateUI();
        $("#BankThrehold").val(_StorageManager.Get("BankThrehold", 100));
        if(_StorageManager.Get("IsBankActivate", true))
        {
            $("#IsBankActivate").attr('checked','checked')
        }
        else
        {
            $("#IsBankActivate").removeAttr("checked");
            $("#BankThrehold").attr("disabled", "disabled");
        }
    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsBankActivate").attr("disabled", "disabled");
            $("#BankThrehold").attr("disabled", "disabled");
        }
        else
        {
            $("#IsBankActivate").removeAttr("disabled");
            if($("#IsBankActivate").attr('checked'))
                $("#BankThrehold").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        let threhold = Math.min(Math.max($("#BankThrehold").val(), 10), 100000000);
        let gold = IsCatDingVersion ? (await _Selector.$("#mgold")).innerText : (await _Selector.$("#mgold > font:nth-child(1)")).innerText; 
        let regex = gold.match(/([\d]*[億.萬.])/g);
        if(regex == undefined)
            return;

        let result = 0;
        for(let ele of regex)
        {
            if(ele.indexOf("億") > 0)
                result += parseInt(ele) * 10000;
            else if(ele.indexOf("萬") > 0)
            result += parseInt(ele);
        }

        if(result >= threhold)
        {
            let autoattck = await _GameWrapper.GetAutoAttack();
            let temp = autoattck.checked;

            await _GameWrapper.Teleport('town','bank');
            await _DelayHelper.sleep();

            let button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(4) > input.MFC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            _GameWrapper.BackTown()
            await _DelayHelper.sleep();

            autoattck.checked = temp;
        }
    }

    OnLeave()
    {
        _StorageManager.Set("BankThrehold", $("#BankThrehold").val());
        _StorageManager.Set("IsBankActivate", $("#IsBankActivate").attr('checked') == "checked");
    }
}

class Inn
{
    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsInnActivate\">自動住宿:  HP<input type=\"text\" id=\"InnHPThreshold\"></input>%以下  MP<input type=\"text\" id=\"InnMPThreshold\"></input>%以下</div></div>");
        $("#InnHPThreshold").css("width", 25);
        $("#InnMPThreshold").css("width", 25);
        $("#IsInnActivate").change(() => {
            if($("#IsInnActivate").attr('checked'))
            {
                $("#InnHPThreshold").removeAttr("disabled");
                $("#InnMPThreshold").removeAttr("disabled");
            }
            else
            {
                $("#InnHPThreshold").attr("disabled", "disabled");
                $("#InnMPThreshold").attr("disabled", "disabled");
            }
        });
        this.UpdateUI();

        $("#InnHPThreshold").val(_StorageManager.Get("InnHPThreshold", InnHPThreshold));
        $("#InnMPThreshold").val(_StorageManager.Get("InnMPThreshold", InnMPThreshold));
        if(_StorageManager.Get("IsInnActivate", true))
        {
            $("#IsInnActivate").attr('checked','checked')
        }
        else
        {
            $("#IsInnActivate").removeAttr("checked");
            $("#InnHPThreshold").attr("disabled", "disabled");
            $("#InnMPThreshold").attr("disabled", "disabled");
        }
    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsInnActivate").attr("disabled", "disabled");
            $("#InnHPThreshold").attr("disabled", "disabled");
            $("#InnMPThreshold").attr("disabled", "disabled");
        }
        else
        {
            if($("#IsInnActivate").attr('checked'))
            {
                $("#InnHPThreshold").removeAttr("disabled");
                $("#InnMPThreshold").removeAttr("disabled");
            }

            $("#IsInnActivate").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        let hpRegex = (await _Selector.$("#mhp", this.outer)).textContent.match(/([\d]+)\/([\d]+)/);
        let currenthp = hpRegex[1];
        let maxhp = hpRegex[2];

        let mpRegex = (await _Selector.$("#mmp", this.outer)).textContent.match(/([\d]+)\/([\d]+)/);
        let currentmp = mpRegex[1];
        let maxmp = mpRegex[2];

        let hpThreshold = Math.min(Math.max($("#InnHPThreshold").val(), 0), 100) / 100;
        let mpThreshold = Math.min(Math.max($("#InnMPThreshold").val(), 0), 100) / 100;

        if(((currenthp / maxhp) < hpThreshold) || ((currentmp / maxmp) < mpThreshold))
        {
            let autoattck = await _GameWrapper.GetAutoAttack();
            let temp = autoattck.checked;

            await _GameWrapper.Teleport('town','bank');
            await _DelayHelper.sleep();

            let button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(4) > input.MFC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','inn');
            await _DelayHelper.sleep();

            _GameWrapper.BackTown()
            await _DelayHelper.sleep();

            autoattck.checked = (currenthp == 0) ? true : temp;
        }
    }

    OnLeave()
    {
        _StorageManager.Set("InnHPThreshold", $("#InnHPThreshold").val());
        _StorageManager.Set("IsInnActivate", $("#IsInnActivate").attr('checked') == "checked");
    }
}

class Majo
{
    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsMajoActivate\">魔女商店:<select id='MajoOption'></select><div>");
        $("#MajoOption").css("width", 180);

        if(IsCatDingVersion)
        {
            $("#MajoOption").append("<option value='0'>力量之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='1'>生命之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='2'>智慧之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='3'>精神之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='4'>幸運之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='5'>速度之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='6'>火之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='7'>水之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='8'>風之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='9'>星之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='10'>雷之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='11'>光之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='12'>暗之石(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='13'>太陽之鑰(12000000 Gold)</option>");
            $("#MajoOption").append("<option value='14'>贊助點數10點(30000000 Gold)</option>");
        }
        else
        {
            $("#MajoOption").append("<option value='0'>神秘的果實(500000 Gold)</option>");
            $("#MajoOption").append("<option value='1'>熟練之玉(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='2'>伊莉亞之角(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='3'>巴羅之花(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='4'>獸之肉(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='5'>長壽之素(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='6'>魔女之粉(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='7'>仙人之御握(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='8'>精神安定劑(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='9'>香純之飴(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='10'>小矮人種子(2000000 Gold)</option>");
            $("#MajoOption").append("<option value='11'>力量之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='12'>生命之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='13'>智慧之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='14'>精神之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='15'>幸運之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='16'>速度之果(4000000 Gold)</option>");
            $("#MajoOption").append("<option value='17'>熟練之書(10000000 Gold)</option>");
            $("#MajoOption").append("<option value='18'>太陽之鑰(12000000 Gold)</option>");
            $("#MajoOption").append("<option value='19'>希望之果(50000000 Gold)</option>");
        }

        $("#IsMajoActivate").change(() => {
            if($("#IsMajoActivate").attr('checked'))
                $("#MajoOption").removeAttr("disabled");
            else
                $("#MajoOption").attr("disabled", "disabled");
        });
        this.UpdateUI();
        $("#MajoOption").val(_StorageManager.Get("MajoOption", 0));
        if(_StorageManager.Get("IsMajoActivate", true))
        {
            $("#IsMajoActivate").attr('checked','checked')
        }
        else
        {
            $("#IsMajoActivate").removeAttr("checked");
            $("#MajoOption").attr("disabled", "disabled");
        }
    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsMajoActivate").attr("disabled", "disabled");
            $("#MajoOption").attr("disabled", "disabled");
        }
        else
        {
            if($("#IsMajoActivate").attr('checked'))
                $("#MajoOption").removeAttr("disabled");
            $("#IsMajoActivate").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        let facility = (await _Selector.$("#townf > select:nth-child(4)")).value.trim();

        if(facility == "rshop")
        {
            let autoattck = await _GameWrapper.GetAutoAttack();
            let temp = autoattck.checked;

            await _GameWrapper.Teleport('town','bank');
            await _DelayHelper.sleep();

            let button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(6) > input.MFC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','rshop');
            await _DelayHelper.sleep();

            let target = $("#MajoOption").val();
            let radio = await _Selector.$("html > body > table.TC > tbody > tr > td > table.TC > tbody > tr > td > input", unsafeWindow.actionframe);
            radio.value = target;
            radio.checked = true;
            button = await _Selector.$("html > body > table.TC > tbody > tr > td > table.TC > tbody > tr > td > input.FC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            await _GameWrapper.Teleport('town','bank');
            await _DelayHelper.sleep();

            button = await _Selector.$("body > table > tbody > tr:nth-child(3) > td > form:nth-child(4) > input.MFC", unsafeWindow.actionframe);
            button.click();
            await _DelayHelper.sleep();

            _GameWrapper.BackTown()
            await _DelayHelper.sleep();

            autoattck.checked = temp;
        }
    }

    OnLeave()
    {
        _StorageManager.Set("MajoOption", $("#MajoOption").val());
        _StorageManager.Set("IsMajoActivate", $("#IsMajoActivate").attr('checked') == "checked");
    }
}

class WareHouse
{
    SetupUI()
    {
        $("#MainWindow").append("<div><input type=\"checkbox\" id=\"IsWareHouseActivate\">自動存倉:<input type=\"text\" id=\"WareHouseInterval\"></input>秒<div>");
        $("#WareHouseInterval").css("width", 50);
        $("#IsWareHouseActivate").change(() => {
            if($("#IsWareHouseActivate").attr('checked'))
                $("#WareHouseInterval").removeAttr("disabled");
            else
                $("#WareHouseInterval").attr("disabled", "disabled");
        });
        this.UpdateUI();
        $("#WareHouseInterval").val(_StorageManager.Get("WareHouseInterval", 1800));
        if(_StorageManager.Get("IsWareHouseActivate", true))
        {
            $("#IsWareHouseActivate").attr('checked','checked')
        }
        else
        {
            $("#IsWareHouseActivate").removeAttr("checked");
            $("#WareHouseInterval").attr("disabled", "disabled");
        }
    }

    UpdateUI(isActivate = false)
    {
        if(isActivate)
        {
            $("#IsWareHouseActivate").attr("disabled", "disabled");
            $("#WareHouseInterval").attr("disabled", "disabled");
        }
        else
        {
            if($("#IsWareHouseActivate").attr('checked'))
                $("#WareHouseInterval").removeAttr("disabled");
            $("#IsWareHouseActivate").removeAttr("disabled");
        }
    }

    async DoScript()
    {
        await _GameWrapper.WaitUntilNextBattleEnd();

        let autoattck = await _GameWrapper.GetAutoAttack();
        let temp = autoattck.checked;
        let id = (await _Selector.$("#battlef > input[type=hidden]:nth-child(1)")).value;
        let pass = (await _Selector.$("#battlef > input[type=hidden]:nth-child(2)")).value;

        await _GameWrapper.Teleport('town','storage');
        await _DelayHelper.sleep();

        let header = (await _Selector.$("body > table > tbody > tr:nth-child(3) > td:nth-child(2) > table > tbody > tr:nth-child(1) > td > font", unsafeWindow.actionframe)).innerText;
        let result = parseInt(header.match(/手持物品一覽\(([\d]+)\/[\d]+\)/)[1]);

        if(result > 0)
        {
            let no = Array.from(Array(result).keys()).join(",");
            let url = "town.cgi";
            let param = `id=${id}&pass=${pass}&rmode=&itype=0&mode=storage_in&no=${no}`;

            await _XHR.MakeRequest('POST', url, param);
            await _DelayHelper.sleep();
        }

        await _GameWrapper.BackTown();
        await _DelayHelper.sleep();

        autoattck.checked = temp;
    }

    OnLeave()
    {
        _StorageManager.Set("WareHouseInterval", $("#WareHouseInterval").val());
        _StorageManager.Set("IsWareHouseActivate", $("#IsWareHouseActivate").attr('checked') == "checked");
    }
}

function Override()
{
    let SelfMake_to = async () =>
    {
        let interval = document.hidden ? 1000 : 100;
        try
        {
            if(BTIME > 0)
            {
                BTIME -= (interval / 1000);
                BTIME = Math.round(BTIME * 10) / 10;
                let output = (BTIME < 0) ? "<font color=black><b>行動OK</b></font>" : `<font color=black>距下次行動剩餘${BTIME}秒</font>`;
                getObj("tok").innerHTML = output;
            }
            else
            {
                let form = getObj('battlef');
                let nmp = form.mode.options[form.mode.selectedIndex].value;

                if(nmp == 42)
                {
                    let Proficiency = parseInt(getObj("mabp").innerText);
                    if(Proficiency < 3000)
                    {
                        _Logger.log("熟練度不足以繼續刷藍天之下");
                        getObj("autoattack").checked = false;
                    }
                }

                if (getObj("autoattack").checked)
                {
                    getObj("tok").innerHTML = "<font color=blue>剩餘秒數讀取中...</font>";

                    if (validMap.indexOf(parseInt(nmp)) >= 0)
                    {
                        if(savedMap.indexOf(parseInt(nmp)) >= 0)
                            battlemap = nmp;

                        spshow = true;

                        await _GameWrapper.DoBattle();
                    }
                }
                else
                {
                    getObj("tok").innerHTML = "<font color=black><b>行動OK</b></font>";
                }
            }

        }
        catch(ex)
        {
            _Logger.logError(ex);
        }

        setTimeout(to, interval);
    }

    let SelfMake_loading = (msgs, disb) =>
    {
        getObj("tok").innerHTML =("<font color=blue>剩餘秒數讀取中...</font>");
        getObj('rbutton').value=msgs;
        getObj('rbutton').disabled=disb;
        getObj('rbutton2').value=msgs;
        getObj('rbutton2').disabled=disb;
        getObj('rebutton').disabled=disb;
        getObj('rebutton').value=msgs;
        getObj('battlebutton').disabled=disb;
        getObj('townbutton').disabled=disb;
        getObj('statusbutton').disabled=disb;
        getObj('countrybutton').disabled=disb;
    }

    if(DebugMode)
    {
        unsafeWindow.to = SelfMake_to;
        unsafeWindow.loading = SelfMake_loading;

        unsafeWindow.addEventListener('visibilitychange', async () => {
            await _GameWrapper.Refresh();
        }, false);
    }
}

unsafeWindow.Init = async function()
{
    unsafeWindow._XHR = new XHR();
    unsafeWindow._StorageManager = new StorageManager();
    unsafeWindow._UIManager = new UIManager();
    unsafeWindow._HeartBeat = new HeartBeat();
    unsafeWindow._Quest = new Quest();
    unsafeWindow._Bank = new Bank();
    unsafeWindow._Inn = new Inn();
    unsafeWindow._Majo = new Majo();
    unsafeWindow._WareHouse = new WareHouse();
    unsafeWindow._Selector = new Selector();
    unsafeWindow._Logger = new Logger();
    unsafeWindow._DelayHelper = new DelayHelper();
    unsafeWindow._TaskController = new TaskController();
    unsafeWindow._GameWrapper = new GameCoreWrapper();

    unsafeWindow._StorageManager.Initialize();
    unsafeWindow._UIManager.Initialize();

    if(IsCatDingVersion)
        savedMap.push(50);
    Override();
}

unsafeWindow.Init().catch(console.error);

unsafeWindow.onunload = (async function() {
    unsafeWindow._WareHouse.OnLeave();
    unsafeWindow._Majo.OnLeave();
    unsafeWindow._Inn.OnLeave();
    unsafeWindow._Bank.OnLeave();
    unsafeWindow._Quest.OnLeave();
    unsafeWindow._HeartBeat.OnLeave();
    unsafeWindow._UIManager.OnLeave();
    unsafeWindow._StorageManager.OnLeave();
});