Greasy Fork 支持简体中文。

YNOproject Yume2kki 变量44 推测

本工具通过从 HEAPU32 中检索入睡次数(变量#43)来推测 变量#44 的地址,实时显示变量的数值。可以在 GreasyFork 页面查看详细说明。

// ==UserScript==
// @name         YNOproject Yume2kki 变量44 推测
// @namespace    https://github.com/Exsper/
// @version      1.2.8
// @description  本工具通过从 HEAPU32 中检索入睡次数(变量#43)来推测 变量#44 的地址,实时显示变量的数值。可以在 GreasyFork 页面查看详细说明。
// @author       Exsper
// @homepage     https://github.com/Exsper/yno2kkivar44guess#readme
// @supportURL   https://github.com/Exsper/yno2kkivar44guess/issues
// @match        https://ynoproject.net/2kki/
// @require      https://cdn.staticfile.org/jquery/2.1.3/jquery.min.js
// @license      MIT License
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

let VAR_UPDATE_INTERVAL = 1000;

/**
 * @param {Array<number>} sourceArray 
 * @param {Array<number>|number} aimArray 
 */
function findDataIndex(sourceArray, aimArray) {
    if (typeof aimArray == "number") aimArray = [aimArray];
    let index = 0;
    let MAX_Index = sourceArray.length - 1;
    let cmpIndex = 0;
    let MAX_CmpIndex = aimArray.length - 1;
    let finds = [];
    while (index <= MAX_Index) {
        if (sourceArray[index + cmpIndex] == aimArray[cmpIndex]) {
            cmpIndex += 1;
            if (cmpIndex > MAX_CmpIndex) {
                finds.push(index);
                cmpIndex = 0;
                index += 1;
            }
        }
        else {
            cmpIndex = 0;
            index += 1;
        }
    }
    return finds;
}

function getSeasonString(sleepCount, seasonNumber) {
    if (seasonNumber !== getSeason(sleepCount)) return "错误";
    let dayIndex = (sleepCount + 1) % 3 + 1;
    switch (seasonNumber) {
        case 0:
            return '冬' + " 第" + dayIndex + "天";
        case 1:
            return '春' + " 第" + dayIndex + "天";
        case 2:
            return '夏' + " 第" + dayIndex + "天";
        case 3:
            return '秋' + " 第" + dayIndex + "天";
        default:
            return '错误';
    }
}

function getSeason(sleepTimes) {
    sleepTimes = sleepTimes % 554400;
    return Math.floor((sleepTimes + 4) / 3) % 4;
}

function filterMemory(sleepIndexList, sleepCount) {
    let qualifiedList = [];
    for (let i = 0; i < sleepIndexList.length; i++) {
        let num44Index = sleepIndexList[i] + 1;
        // #44范围为0-255
        if (easyrpgPlayer["HEAPU32"][num44Index] > 255) continue;
        // #94为季节,检查#94的值
        if (easyrpgPlayer["HEAPU32"][num44Index + 50] != getSeason(sleepCount)) continue;
        // 以下为可能值
        if (easyrpgPlayer["HEAPU32"][num44Index] != 0)
            // #44很可能不为0,将不为0的位置前置
            qualifiedList.unshift(num44Index);
        else
            qualifiedList.push(num44Index);
    }
    return qualifiedList;
}

class CustomVariable {
    constructor(index, name) {
        this.index = index;
        this.name = name;
    }

    toTableData() {
        return { title: this.name, index: this.index, callFuc: null };
    }
}

class Script {
    constructor() {
        this.sleepCount = -1;
        this.memoryIndexs = [];
        this.correctNum44Index = -1;
        this.customVars = [];
        this.showMapDefaultVars = -1;
    }

    loadStorage() {
        let customVarsData = GM_getValue("customVarsData", []);
        this.customVars.push(...(customVarsData.map((data) => new CustomVariable(data[0], data[1]))));
        this.sleepCount = GM_getValue("sleepCount", -1);
        this.showMapDefaultVars = GM_getValue("showMapDefaultVars", -1);
    }

    saveStorage() {
        let customVarsData = this.customVars.map((customVar) => [customVar.index, customVar.name]);
        GM_setValue("customVarsData", customVarsData);
        GM_setValue("sleepCount", this.sleepCount);
        GM_setValue("showMapDefaultVars", this.showMapDefaultVars);
    }

    init() {
        this.loadStorage();
        let $openButton = $('<button>', { text: "+", id: "rs-open", style: "float:left;top:30%;position:absolute;left:0%;", title: "显示窗口" }).appendTo($("body"));
        $openButton.click(() => {
            $("#rs-div").show();
            $("#rs-open").hide();
        });
        let $mainDiv = $("<div>", { id: "rs-div", class: "container", style: "top:40%;left:0%;transform: translate(0, -50%);width:180px;position:absolute;text-align:center;z-index:999;height:auto;max-height:70vh;min-height:160px;overflow-y:auto;border-top: 24px double #000000 !important;padding-top: 0px !important;" });
        $mainDiv.hide();
        let $statLabel = $("<span>", { id: "rs-stat", text: "请在 读取存档 后输入睡眠次数,即存档后显示的Day数。如果已经在梦境中需要+1", style: "display: block; padding: 6px;" }).appendTo($mainDiv);
        let $numBox = $("<input>", { type: "text", id: "rs-sleepcount", val: "100", style: "width:30px;align-self:center;" }).appendTo($mainDiv);
        if (this.sleepCount > 0) $numBox.val(this.sleepCount);
        let $checkButton = $('<button>', { type: "button", text: "确定", id: "rs-checkbtn", style: "width:fit-content;align-self:center;" }).appendTo($mainDiv);
        $checkButton.click(() => {
            $checkButton.attr("disabled", true);
            $checkButton.text("正在获取");
            let sleepCount = parseInt($numBox.val());
            if (sleepCount <= 0) {
                $checkButton.attr("disabled", false);
                $statLabel.text("请输入正整数");
                $checkButton.text("确定");
            }
            let result = filterMemory(findDataIndex(easyrpgPlayer["HEAPU32"], sleepCount), sleepCount);
            if (result.length <= 0) {
                $checkButton.attr("disabled", false);
                $statLabel.text("找不到符合条件的地址,请重试");
                $checkButton.text("确定");
            }
            else {
                this.sleepCount = sleepCount;
                this.saveStorage();
                this.memoryIndexs = result;
                this.isZero = this.memoryIndexs.map(mi => easyrpgPlayer["HEAPU32"][mi] == 0);
                if (this.memoryIndexs.length > 1) {
                    $statLabel.text("找到多个符合条件的地址,请选择正确的#44变量。建议到伪部屋睡几次床,每次都会更新的就是#44变量");
                    this.updateSelectTable();
                }
                else {
                    this.correctNum44Index = this.memoryIndexs[0];
                    this.updateDataTable();
                }
                $numBox.hide();
                $checkButton.hide();
            }
        });
        let $titleDiv = $("<div>", { id: "rs-title", style: "width: 100%; display: flex;" }).prependTo($mainDiv);
        let $rightDiv = $("<div>", { id: "rs-title-right", style: "display: flex; justify-content: right;" }).prependTo($titleDiv);
        let $leftDiv = $("<div>", { id: "rs-title-left", style: "width: 100%; display: flex; justify-content: left;" }).prependTo($titleDiv);
        let $backButton = $('<button>', { text: "←", id: "rs-back", title: "重新输入入睡次数" }).appendTo($leftDiv);
        $backButton.click(() => {
            this.reload();
        });
        let $initSessionWsButton = $('<button>', { text: "⟳", id: "rs-reconnect", title: "尝试重连" }).appendTo($leftDiv);
        $initSessionWsButton.click(() => {
            initSessionWs();
        });
        let $showMapDefaultVarsButton = $('<button>', { text: (this.showMapDefaultVars <= 0) ? "🌙" : "🔍", id: "rs-showmapvar", title: "显示部分地图状态,帮助跑图" }).appendTo($leftDiv);
        $showMapDefaultVarsButton.hide();
        $showMapDefaultVarsButton.click(() => {
            if (this.showMapDefaultVars <= 0) {
                this.showMapDefaultVars = 1;
                $showMapDefaultVarsButton.text("🔍");
            }
            else {
                this.showMapDefaultVars = 0;
                $showMapDefaultVarsButton.text("🌙");
            }
            this.saveStorage();
        });
        let $addVarButton = $('<button>', { text: "+", id: "rs-addvar", title: "添加自定义变量" }).appendTo($leftDiv);
        $addVarButton.hide();
        $addVarButton.click(() => {
            let index;
            let name;
            while (!index || typeof index !== "number" || index < 0) {
                index = prompt("请输入变量ID,例如:3513");
                if (index === null) return;
                index = parseInt(index);
            }
            while (!name || name.trim() === "") {
                name = prompt("请给变量起个名字,例如:踏破率");
                if (name === null) return;
            }
            this.customVars.push(new CustomVariable(index, name));
            alert("添加成功!如需删除请点击变量名。");
            this.saveStorage();
        });
        let $closeButton = $('<button>', { text: "-", id: "rs-close", title: "隐藏窗口" }).appendTo($rightDiv);
        $closeButton.click(() => {
            $("#rs-div").hide();
            $("#rs-open").show();
        });
        let $mainTable = $("<table>", { id: "rs-table", style: "table-layout:fixed; width:100%; word-wrap: break-word;" }).appendTo($mainDiv);
        $mainDiv.appendTo($("body"));
    }

    reload() {
        this.sleepCount = -1;
        this.memoryIndexs = [];
        this.correctNum44Index = -1;
        $("#rs-table").empty();
        $("#rs-stat").text("请在 读取存档 后输入睡眠次数,即存档后显示的Day数。如果已经在梦境中需要+1");
        $("#rs-sleepcount").show();
        $("#rs-checkbtn").show();
        $("#rs-checkbtn").attr("disabled", false);
        $("#rs-checkbtn").text("确定");
        $("#rs-addvar").hide();
        $("#rs-showmapvar").hide();
    }

    updateSelectTable() {
        if (this.memoryIndexs.length <= 0) return;
        this.memoryIndexs = this.memoryIndexs.filter((mi) => easyrpgPlayer["HEAPU32"][mi] <= 255);
        let $mainTable = $("#rs-table");
        $mainTable.empty();
        let $thead = $("<thead>").appendTo($mainTable);
        let $tr = $("<tr>").appendTo($thead);
        let $td = $("<td>", { style: "width:40%" }).appendTo($tr);
        $("<span>", { text: "地址" }).appendTo($td);
        $td = $("<td>", { style: "width:30%" }).appendTo($tr);
        $("<span>", { text: "数值" }).appendTo($td);
        $td = $("<td>", { style: "width:30%" }).appendTo($tr);
        $("<span>", { text: "" }).appendTo($td);
        let $tbody = $("<tbody>").appendTo($mainTable);
        this.memoryIndexs.map((mi, index) => {
            let $tr = $("<tr>", { style: "width:100%;" });
            let $td = $("<td>").appendTo($tr);
            $("<span>", { text: "0x" + mi.toString(16) }).appendTo($td);
            $td = $("<td>").appendTo($tr);
            $("<span>", { text: easyrpgPlayer["HEAPU32"][mi] }).appendTo($td);
            $td = $("<td>").appendTo($tr);
            let $selbtn = $("<a>", { text: "选择", "data-index": mi, style: "cursor: pointer;" }).appendTo($td);
            $selbtn.click(() => {
                this.correctNum44Index = parseInt($selbtn.attr("data-index"));
                this.updateDataTable();
            });
            $tr.appendTo($tbody);
        });
        setTimeout(() => {
            if (this.correctNum44Index <= 0) this.updateSelectTable();
        }, 1000);
    }

    getVariableIndex(variableNum) {
        if (this.correctNum44Index <= 0) return -1;
        let base = this.correctNum44Index - 44;
        return base + variableNum;
    }

    updateDataTable() {
        if (this.correctNum44Index <= 0) return;
        $("#rs-stat").text("当前状态");
        $("#rs-addvar").show();
        $("#rs-showmapvar").show();
        let $mainTable = $("#rs-table");
        $mainTable.empty();
        // 默认项
        let dataTableData =
            [
                { title: "入梦次数", index: 43, callFuc: null },
                { title: "变量#44", index: 44, callFuc: null },
                { title: "季节", index: [43, 94], callFuc: getSeasonString },
            ];
        // 自定义项
        this.customVars.map((customData) => {
            dataTableData.push(customData.toTableData());
        });
        // 地图项
        if (this.showMapDefaultVars > 0) {
            let mapID = easyrpgPlayer["HEAPU32"][this.getVariableIndex(26)];
            dataTableData.push(...(MapVariable.getMapVariableData(mapID)));
            // 特殊地图刷新间隔
            VAR_UPDATE_INTERVAL = MapVariable.getMapUpdateInterval(mapID);
        }
        dataTableData.map((varLineData, index) => {
            let $ltr = $("<tr>", { style: "width:100%;" });
            let $ltd = $("<td>", { style: "width:50%" }).appendTo($ltr);
            $("<span>", { text: varLineData.title }).appendTo($ltd);
            if (index > 2) { // 自定义项
                $ltd.click(() => {
                    this.customVars = this.customVars.filter((customVarData) => customVarData.index !== varLineData.index);
                    this.saveStorage();
                    $ltd.parent().remove();
                });
            }
            $ltd = $("<td>").appendTo($ltr);
            let data;
            if (varLineData.index instanceof Array === true) {
                data = [];
                for (let i = 0; i < varLineData.index.length; i++) data.push(easyrpgPlayer["HEAPU32"][this.getVariableIndex(varLineData.index[i])]);
                if (varLineData.callFuc) data = varLineData.callFuc(...data);
                if (typeof data !== "object") $("<span>", { text: data, title: "使用变量:" + varLineData.index.join(",") }).appendTo($ltd);
                else data.appendTo($ltd);
            }
            else {
                data = easyrpgPlayer["HEAPU32"][this.getVariableIndex(varLineData.index)];
                if (varLineData.callFuc) data = varLineData.callFuc(data);
                if (typeof data !== "object") $("<span>", { text: data, title: "使用变量:" + varLineData.index }).appendTo($ltd);
                else data.appendTo($ltd);
            }
            $ltr.appendTo($mainTable);
        });

        setTimeout(() => { this.updateDataTable(); }, VAR_UPDATE_INTERVAL);
    }
}

class MapVariable {
    static getMapUpdateInterval(mapID) {
        switch (mapID) {
            // Horror Maze
            case 1445: return 200;

            default: return 1000;
        }
    }

    static getMapVariableData(mapID) {
        switch (mapID) {
            // Black Building
            case 68: return [
                { title: "楼层数", index: 610, callFuc: this.callFuc_68_610 },
            ];
            // Infinite Library
            case 914: return [
                { title: "当前深度", index: 1, callFuc: this.callFuc_914_1 },
                { title: "预计深度", index: 2, callFuc: this.callFuc_914_2 },
            ];
            // Static Noise Hell
            // 变量8后续用于别处,此功能无效
            /*
            case 945: return [
                { title: "传送点位置", index: 8, callFuc: this.callFuc_945_8 },
            ];
            */
            // Smiley Face DECK
            case 1265: return [
                { title: "传送点位置", index: 2, callFuc: this.callFuc_1265_2 },
            ];
            // Bright Forest
            // 考虑到只用通过一次,可能会影响初次游戏体验,暂不启用该功能
            /*
            case 1348: return [
                { title: "传送点位置", index: [2,3,4,5], callFuc: this.callFuc_1348_2_3_4_5 },
            ];
            */
            // Horror Maze
            case 1445: return [
                { title: "玫瑰数", index: 4782 },
                { title: "位置", index: [2, 3, 4, 4782], callFuc: this.callFuc_1445_2_3_4_4782 },
            ];
            // 岛子Shimako相关
            case 1824: // 釣り堀
            case 1825: // 万華鏡の世界
            case 1826: // 通路(包括万华镜小区域怪物、六角形区域触手怪物)
            case 1871: // home
            case 1873: // 白い世界
            case 1874: // 白い世界
            case 1882: // 湖のほとり
            case 1890: // さんかく遺跡 
                return [
                    { title: "岛子事件红怪剩余", index: 4246, callFuc: this.callFuc_multi_4246 },
                ];
            // Rainy Apartments 楼梯
            case 1902: return [
                { title: "阶梯数", index: 80, callFuc: this.callFuc_1902_80 },
            ];

            default: return [];
        }
    }

    // 以下函数命名规则:callFuc_[地图ID]_[变量ID]

    static callFuc_68_610(val) {
        return val + "/45";
    }

    static callFuc_914_1(val) {
        return val + "/4";
    }

    static callFuc_914_2(val) {
        if (val > 4) val = 0;
        return "=>" + val;
    }

    /*
    static callFuc_945_8(val) {
        switch(val) {
            case 0: return "正上方偏左";
            case 1: return "左下角靠下";
            case 2: return "左下角靠左";
            case 3: return "最右上角";
            default: return "中间偏右上";
        }
    }
    */

    static callFuc_1265_2(val) {
        switch (val) {
            case 1: return "右边居中";
            case 2: return "右下";
            case 3: return "左下";
            case 4: return "右上";
            default: return "错误";
        }
    }

    /*
    static callFuc_1348_2_3_4_5(val2, val3, val4, val5) {
        if (val2 != 0) return "右上";
        else if (val3 != 0) return "右下";
        else if (val4 != 0) return "中间";
        else if (val5 != 0) return "左下";
    }
    */

    /**
     * @param {number} val2 monster pos
     * @param {number} val3 player pos
     * @param {1|2|3|4} val4 ↑1 →2 ↓3 ←4
     * @param {number} val4782 rose count
     */
    static callFuc_1445_2_3_4_4782(val2, val3, val4, val4782) {
        let imageDataUrl = HorrorMazeStuffs.getImage(val2, val3, val4, val4782);
        return $(`<img src="${imageDataUrl}">`);
    }

    static callFuc_multi_4246(val) {
        let tmp = val;
        let sum = [];
        // 病院后悬崖上眼球
        let hospitalDied = Math.floor(tmp / 640);
        tmp -= hospitalDied * 640;
        // 六角触手
        let hexagonDied = Math.floor(tmp / 320);
        tmp -= hexagonDied * 320;
        // 钓鱼人头
        let fishDied = Math.floor(tmp / 160);
        tmp -= fishDied * 160;
        // 万华镜眼球
        let scopeDied = Math.floor(tmp / 80);
        tmp -= scopeDied * 80;
        // 水坝怪物
        let damDied = Math.floor(tmp / 40);
        tmp -= damDied * 40;
        // 吸水怪物
        let drinkDied = Math.floor(tmp / 20);
        // tmp -= drinkDied * 20;
        if (drinkDied === 0) sum.push("树海A");
        if (scopeDied === 0) sum.push("万华镜");
        if (hexagonDied === 0) sum.push("六棱柱");
        if (fishDied === 0) sum.push("码头");
        if (damDied === 0) sum.push("树海B");
        if (hospitalDied === 0) sum.push("悬崖");
        return sum.join("/");
    }

    static callFuc_1902_80(val) {
        return val + "/144";
    }
}

class HorrorMazeStuffs {
    static background = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEIAAABaCAIAAACDsxdJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFXSURBVHhe7dj9DYIwEAVwdAT+xRV0Bd3AWd1AF3IGCZxFPVLbaykPeL+YiJaPXPpCD3aHpqmyqutatgray/fCraSMIVTXy7nfsLndH/0GQ2W33lC5eGgh+zBUdiyj00bohwyUxdlAklrGU5GBsjgbSIzLn8aeKoNth4o91SRYRqdro77IQFmcDSSpZUgj9UEGyuJsIOF7KiSrC5UW0kFpDJXdWkJ1Oh5l880tYTpUc7VMfzFUSIZQ6XbIdqeaBUOFZCRUic1VSS7tDBUSX6hsNyh9uOeEtmvpoxgqJEGhcv9EYaiibTJUIVMfdbgbsmGoIMX1VNOFKuTMjj6KoULiK6Odsv4jv4ExVEhYBhKWgYRlIJnw6S8Xvf6yp8JmfPrT/2TnuQRDhY1lIGEZSFgGktTlrwAuf0uT/426R9R5PDszVNhYBpIN3Knw8U4FaSRUS8RQ4aiqF/sK8E80KPOTAAAAAElFTkSuQmCC";
    static poss = {
        1: { x: 36, y: 84 },
        2: { x: 36, y: 80 },
        3: { x: 36, y: 75 },
        4: { x: 27, y: 75 },
        5: { x: 18, y: 75 },
        6: { x: 18, y: 69 },
        7: { x: 18, y: 63 },
        8: { x: 15, y: 63 },
        9: { x: 12, y: 63 },
        10: { x: 12, y: 69 },
        11: { x: 12, y: 74 },
        12: { x: 12, y: 79 },
        13: { x: 12, y: 84 },
        14: { x: 18, y: 84 },
        15: { x: 24, y: 84 },
        16: { x: 30, y: 84 },
        17: { x: 9, y: 84 },
        18: { x: 6, y: 84 },
        19: { x: 6, y: 79 },
        20: { x: 6, y: 74 },
        21: { x: 6, y: 69 },
        22: { x: 6, y: 63 },
        23: { x: 6, y: 57 },
        24: { x: 6, y: 51 },
        25: { x: 9, y: 51 },
        26: { x: 12, y: 51 },
        27: { x: 12, y: 57 },
        28: { x: 12, y: 45 },
        29: { x: 17, y: 45 },
        30: { x: 22, y: 45 },
        31: { x: 27, y: 45 },
        32: { x: 27, y: 51 },
        33: { x: 27, y: 57 },
        34: { x: 27, y: 63 },
        35: { x: 30, y: 63 },
        36: { x: 33, y: 63 },
        37: { x: 37, y: 63 },
        38: { x: 42, y: 63 },
        39: { x: 51, y: 63 },
        40: { x: 51, y: 69 },
        41: { x: 51, y: 75 },
        42: { x: 46, y: 75 },
        43: { x: 41, y: 75 },
        44: { x: 44, y: 84 },
        45: { x: 51, y: 84 },
        46: { x: 51, y: 80 },
        47: { x: 55, y: 84 },
        48: { x: 60, y: 84 },
        49: { x: 60, y: 78 },
        50: { x: 60, y: 72 },
        51: { x: 60, y: 66 },
        52: { x: 60, y: 60 },
        53: { x: 60, y: 54 },
        54: { x: 60, y: 48 },
        55: { x: 60, y: 43 },
        56: { x: 60, y: 39 },
        57: { x: 54, y: 39 },
        58: { x: 48, y: 39 },
        59: { x: 42, y: 39 },
        60: { x: 37, y: 39 },
        61: { x: 33, y: 39 },
        62: { x: 30, y: 39 },
        63: { x: 27, y: 39 },
        64: { x: 27, y: 42 },
        65: { x: 21, y: 39 },
        66: { x: 15, y: 39 },
        67: { x: 9, y: 39 },
        68: { x: 6, y: 39 },
        69: { x: 6, y: 42 },
        70: { x: 6, y: 46 },
        71: { x: 33, y: 45 },
        72: { x: 33, y: 51 },
        73: { x: 33, y: 57 },
        74: { x: 42, y: 55 },
        75: { x: 42, y: 48 },
        76: { x: 42, y: 43 },
        77: { x: 48, y: 48 },
        78: { x: 54, y: 48 },
        79: { x: 9, y: 33 },
        80: { x: 9, y: 27 },
        81: { x: 15, y: 27 },
        82: { x: 21, y: 27 },
        83: { x: 15, y: 21 },
        84: { x: 15, y: 15 },
        85: { x: 21, y: 15 },
        86: { x: 15, y: 9 },
        87: { x: 15, y: 3 },
        88: { x: 21, y: 3 },
        89: { x: 11, y: 3 },
        90: { x: 7, y: 3 },
        91: { x: 3, y: 3 },
        92: { x: 3, y: 9 },
        93: { x: 3, y: 15 },
        94: { x: 3, y: 21 },
        95: { x: 3, y: 27 },
    }
    static rosePos = [91, 18, 82, 48, 88, 5, 95, 39];
    static exitPos = 85;

    static player_color = "#00ff00";
    static monster_color = "#ff0000";
    static rose_color = "#f6a3ff";
    static exit_color = "#ff9400";

    static canvasId = "horrormazecanvas";

    static getCanvas() {
        let canvas = document.getElementById(this.canvasId);
        if (!canvas) {
            canvas = document.createElement('canvas');
            canvas.width = 66;
            canvas.height = 90;
            canvas.id = this.canvasId;
        }

        return canvas;
    }

    /**
     * @param {CanvasRenderingContext2D} ctx 
     * @param {number} posId 
     * @param {string} color
     */
    static drawRect(ctx, posId, color) {
        let pos = this.poss[posId];
        let x = 0;
        let y = 0;
        if (!!pos) {
            x = pos.x;
            y = pos.y;
        }
        ctx.fillStyle = color;
        ctx.fillRect(x, y, 3, 3);
        return ctx;
    }

    /**
     * @param {CanvasRenderingContext2D} ctx 
     * @param {number} posId 
     * @param {number} dir ↑1 →2 ↓3 ←4
     * @param {string} color 
     */
    static drawArrow(ctx, posId, dir, color) {
        let pos = this.poss[posId];
        let x = 0;
        let y = 0;
        if (!!pos) {
            x = pos.x;
            y = pos.y;
        }
        ctx.fillStyle = color;
        if (dir === 1) {
            ctx.fillRect(x, y - 2, 3, 1);
            ctx.fillRect(x + 1, y - 3, 1, 1);
        }
        else if (dir === 2) {
            ctx.fillRect(x + 4, y, 1, 3);
            ctx.fillRect(x + 5, y + 1, 1, 1);
        }
        else if (dir === 3) {
            ctx.fillRect(x, y + 4, 3, 1);
            ctx.fillRect(x + 1, y + 5, 1, 1);
        }
        else if (dir === 4) {
            ctx.fillRect(x - 2, y, 1, 3);
            ctx.fillRect(x - 3, y + 1, 1, 1);
        }
        return ctx;
    }

    /**
     * @param {CanvasRenderingContext2D} ctx 
     * @param {number} posId 
     * @param {number} dir ↑1 →2 ↓3 ←4
     */
    static drawPlayer(ctx, posId, dir) {
        return this.drawArrow(this.drawRect(ctx, posId, this.player_color), posId, dir, this.player_color);
    }

    /**
     * @param {CanvasRenderingContext2D} ctx 
     * @param {number} posId 
     */
    static drawMonster(ctx, posId) {
        return this.drawRect(ctx, posId, this.monster_color);
    }

    /**
     * @param {CanvasRenderingContext2D} ctx 
     * @param {number} roseCount 
     */
    static drawRoseOrExit(ctx, roseCount) {
        if (roseCount < 8) {
            let posId = this.rosePos[roseCount];
            return this.drawRect(ctx, posId, this.rose_color);
        }
        else {
            let posId = this.exitPos;
            return this.drawRect(ctx, posId, this.exit_color);
        }
    }

    static getImage(monsterPos, playerPos, playerDir, roseCount) {
        /**@type {HTMLCanvasElement} */
        let canvas = this.getCanvas();
        let ctx = canvas.getContext('2d');
        let img = new Image();
        img.src = this.background;
        ctx.drawImage(img, 0, 0, 66, 90);
        ctx = this.drawRoseOrExit(ctx, roseCount);
        ctx = this.drawPlayer(ctx, playerPos, playerDir);
        if (roseCount > 0) {
            ctx = this.drawMonster(ctx, monsterPos);
        }
        return canvas.toDataURL();
    }
}




// 确保网页加载完成
function check() {
    let $loaded = $("#loadingOverlay.loaded");
    if ($loaded.length > 0) {
        let script = new Script();
        script.init();
    }
    else setTimeout(function () { check(); }, 2000);
}

$(document).ready(() => {
    check();
});