diff --git a/libs/control.js b/libs/control.js index a166c2a3..164fb752 100644 --- a/libs/control.js +++ b/libs/control.js @@ -1,3885 +1,3885 @@ -/// - -/* -control.js:游戏主要逻辑控制 -主要负责status相关内容,以及各种变量获取/存储 -寻路算法和人物行走也在此文件内 - */ - -"use strict"; - -function control() { - this._init(); -} - -control.prototype._init = function () { - this.controldata = functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a.control; - this.renderFrameFuncs = []; - this.replayActions = []; - this.weathers = {}; - this.resizes = []; - this.noAutoEvents = true; - /** @type {{[canvasName:string]:Function}} */ - this.dymCanvasResizeEvents = {}; - // --- 注册系统的animationFrame - this.registerAnimationFrame("totalTime", false, this._animationFrame_totalTime); - this.registerAnimationFrame("autoSave", true, this._animationFrame_autoSave); - this.registerAnimationFrame("globalAnimate", true, this._animationFrame_globalAnimate); - this.registerAnimationFrame("animate", true, this._animationFrame_animate); - this.registerAnimationFrame("heroMoving", true, this._animationFrame_heroMoving); - this.registerAnimationFrame("weather", true, this._animationFrame_weather); - this.registerAnimationFrame("tip", true, this._animateFrame_tip); - this.registerAnimationFrame("parallelDo", false, this._animationFrame_parallelDo); - // --- 注册系统的天气 - this.registerWeather("rain", this._weather_rain, this._animationFrame_weather_rain); - this.registerWeather("snow", this._weather_snow, this._animationFrame_weather_snow); - this.registerWeather("fog", this._weather_fog, this.__animateFrame_weather_image); - this.registerWeather("cloud", this._weather_cloud, this.__animateFrame_weather_image); - this.registerWeather("sun", this._weather_sun, this._animationFrame_weather_sun); - // --- 注册系统的replay - this.registerReplayAction("move", this._replayAction_move); - this.registerReplayAction("item", this._replayAction_item); - this.registerReplayAction("equip", this._replayAction_equip); - this.registerReplayAction("unEquip", this._replayAction_unEquip); - this.registerReplayAction("saveEquip", this._replayAction_saveEquip); - this.registerReplayAction("loadEquip", this._replayAction_loadEquip); - this.registerReplayAction("fly", this._replayAction_fly); - this.registerReplayAction("shop", this._replayAction_shop); - this.registerReplayAction("turn", this._replayAction_turn); - this.registerReplayAction("getNext", this._replayAction_getNext); - this.registerReplayAction("moveDirectly", this._replayAction_moveDirectly); - this.registerReplayAction("key", this._replayAction_key); - this.registerReplayAction("click", this._replayAction_click); - this.registerReplayAction("ignoreInput", this._replayAction_ignoreInput); - this.registerReplayAction("no", this._replayAction_no); - // --- 注册系统的resize - this.registerResize("gameGroup", this._resize_gameGroup); - this.registerResize("canvas", this._resize_canvas); - this.registerResize("statusBar", this._resize_statusBar); - this.registerResize("status", this._resize_status); - this.registerResize("toolBar", this._resize_toolBar); - this.registerResize("tools", this._resize_tools); -} - -// ------ requestAnimationFrame 相关 ------ // - -////// 注册一个 animationFrame ////// -// name:名称,可用来作为注销使用;needPlaying:是否只在游戏运行时才执行(在标题界面不执行) -// func:要执行的函数,或插件中的函数名;可接受timestamp(从页面加载完毕到当前所经过的时间)作为参数 -control.prototype.registerAnimationFrame = function (name, needPlaying, func) { - this.unregisterAnimationFrame(name); - this.renderFrameFuncs.push({ name: name, needPlaying: needPlaying, func: func }); -} - -////// 注销一个 animationFrame ////// -control.prototype.unregisterAnimationFrame = function (name) { - this.renderFrameFuncs = this.renderFrameFuncs.filter(function (x) { return x.name != name; }); -} - -////// 设置requestAnimationFrame ////// -control.prototype._setRequestAnimationFrame = function () { - this._checkRequestAnimationFrame(); - core.animateFrame.totalTime = Math.max(core.animateFrame.totalTime, core.getLocalStorage('totalTime', 0)); - var loop = function (timestamp) { - core.control.renderFrameFuncs.forEach(function (b) { - if (b.func) { - try { - if (core.isPlaying() || !b.needPlaying) - core.doFunc(b.func, core.control, timestamp); - } - catch (e) { - console.error(e); - console.error("ERROR in requestAnimationFrame[" + b.name + "]:已自动注销该项。"); - core.unregisterAnimationFrame(b.name); - } - } - }) - window.requestAnimationFrame(loop); - } - window.requestAnimationFrame(loop); -} - -control.prototype._checkRequestAnimationFrame = function () { - (function () { - var lastTime = 0; - var vendors = ['webkit', 'moz']; - for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; - window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了 - window[vendors[x] + 'CancelRequestAnimationFrame']; - } - - if (!window.requestAnimationFrame) { - window.requestAnimationFrame = function (callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); - var id = window.setTimeout(function () { - callback(currTime + timeToCall); - }, timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - } - if (!window.cancelAnimationFrame) { - window.cancelAnimationFrame = function (id) { - clearTimeout(id); - }; - } - }()); -} - -control.prototype._animationFrame_totalTime = function (timestamp) { - core.animateFrame.totalTime += timestamp - core.animateFrame.totalTimeStart; - core.animateFrame.totalTimeStart = timestamp; - if (core.isPlaying()) { - core.status.hero.statistics.totalTime = core.animateFrame.totalTime; - core.status.hero.statistics.currTime += timestamp - (core.status.hero.statistics.start || timestamp); - core.status.hero.statistics.start = timestamp; - } -} - -control.prototype._animationFrame_autoSave = function (timestamp) { - if (timestamp - core.saves.autosave.time <= 5000) return; - core.control.checkAutosave(); - core.saves.autosave.time = timestamp; -} - -control.prototype._animationFrame_globalAnimate = function (timestamp) { - if (timestamp - core.animateFrame.globalTime <= core.values.animateSpeed) return; - core.status.globalAnimateStatus++; - if (core.status.floorId) { - // Global Animate - core.status.globalAnimateObjs.forEach(function (block) { - core.drawBlock(block, core.status.globalAnimateStatus); - }); - - // Global floor images - core.maps._drawFloorImages(core.status.floorId, core.canvas.bg, 'bg', core.status.floorAnimateObjs || [], core.status.globalAnimateStatus); - core.maps._drawFloorImages(core.status.floorId, core.canvas.fg, 'fg', core.status.floorAnimateObjs || [], core.status.globalAnimateStatus); - - // Global Autotile Animate - core.status.autotileAnimateObjs.forEach(function (block) { - core.maps._drawAutotileAnimate(block, core.status.globalAnimateStatus); - }); - - // Global hero animate - if ((core.status.hero || {}).animate && core.status.heroMoving == 0 && main.mode == 'play' && !core.status.preview.enabled) { - core.drawHero('stop', null, core.status.globalAnimateStatus); - } - } - // Box animate - core.drawBoxAnimate(); - core.animateFrame.globalTime = timestamp; -} - -control.prototype._animationFrame_animate = function (timestamp) { - if (timestamp - core.animateFrame.animateTime < 50 || !core.status.animateObjs || core.status.animateObjs.length == 0) return; - core.clearMap('animate'); - // 更新帧 - for (var i = 0; i < core.status.animateObjs.length; i++) { - var obj = core.status.animateObjs[i]; - if (obj.index == obj.animate.frames.length) { - (function (callback) { - setTimeout(function () { - if (callback) callback(); - }); - })(obj.callback); - } - } - core.status.animateObjs = core.status.animateObjs.filter(function (obj) { - return obj.index < obj.animate.frames.length; - }); - core.status.animateObjs.forEach(function (obj) { - if (obj.hero) { - core.maps._drawAnimateFrame('animate', obj.animate, core.status.heroCenter.px, core.status.heroCenter.py, obj.index++); - } else { - core.maps._drawAnimateFrame('animate', obj.animate, obj.centerX, obj.centerY, obj.index++); - } - }); - core.animateFrame.animateTime = timestamp; -} - -control.prototype._animationFrame_heroMoving = function (timestamp) { - if (core.status.heroMoving <= 0) return; - // 换腿 - if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) { - core.animateFrame.leftLeg = !core.animateFrame.leftLeg; - core.animateFrame.moveTime = timestamp; - } - core.drawHero(core.animateFrame.leftLeg ? 'leftFoot' : 'rightFoot', 4 * core.status.heroMoving); -} - -control.prototype._animationFrame_weather = function (timestamp) { - var weather = core.animateFrame.weather, type = weather.type; - if (!core.dymCanvas.weather || !core.control.weathers[type] || !core.control.weathers[type].frameFunc) return; - try { - core.doFunc(core.control.weathers[type].frameFunc, core.control, timestamp, core.animateFrame.weather.level); - } catch (e) { - console.error(e); - console.error("ERROR in weather[" + type + "]:已自动注销该项。"); - core.unregisterWeather(type); - } -} - -control.prototype._animationFrame_weather_rain = function (timestamp, level) { - if (timestamp - core.animateFrame.weather.time < 30) return; - var ctx = core.dymCanvas.weather, ox = core.bigmap.offsetX, oy = core.bigmap.offsetY; - core.clearMap('weather'); - ctx.strokeStyle = 'rgba(174,194,224,0.8)'; - ctx.lineWidth = 1; - ctx.lineCap = 'round'; - - core.animateFrame.weather.nodes.forEach(function (p) { - ctx.beginPath(); - ctx.moveTo(p.x - ox, p.y - oy); - ctx.lineTo(p.x + p.l * p.xs - ox, p.y + p.l * p.ys - oy); - ctx.stroke(); - - p.x += p.xs; - p.y += p.ys; - if (p.x > core.bigmap.width * 32 || p.y > core.bigmap.height * 32) { - p.x = Math.random() * core.bigmap.width * 32; - p.y = -10; - } - - }); - - ctx.fill(); - core.animateFrame.weather.time = timestamp; -} - -control.prototype._animationFrame_weather_snow = function (timestamp, level) { - if (timestamp - core.animateFrame.weather.time < 30) return; - var ctx = core.dymCanvas.weather, ox = core.bigmap.offsetX, oy = core.bigmap.offsetY; - core.clearMap('weather'); - ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; - ctx.beginPath(); - core.animateFrame.weather.data = core.animateFrame.weather.data || 0; - core.animateFrame.weather.data += 0.01; - - var angle = core.animateFrame.weather.data; - core.animateFrame.weather.nodes.forEach(function (p) { - ctx.moveTo(p.x - ox, p.y - oy); - ctx.arc(p.x - ox, p.y - oy, p.r, 0, Math.PI * 2, true); - // update - p.x += Math.sin(angle) * core.animateFrame.weather.level; - p.y += Math.cos(angle + p.d) + 1 + p.r / 2; - if (p.x > core.bigmap.width * 32 + 5 || p.x < -5 || p.y > core.bigmap.height * 32) { - if (Math.random() > 1 / 3) { - p.x = Math.random() * core.bigmap.width * 32; - p.y = -10; - } - else { - if (Math.sin(angle) > 0) - p.x = -5; - else - p.x = core.bigmap.width * 32 + 5; - p.y = Math.random() * core.bigmap.height * 32; - } - } - }); - ctx.fill(); - core.animateFrame.weather.time = timestamp; -} - -control.prototype.__animateFrame_weather_image = function (timestamp, level) { - if (timestamp - core.animateFrame.weather.time < 30) return; - var node = core.animateFrame.weather.nodes[0]; - var image = node.image; - if (!image) return; - core.clearMap('weather'); - core.setAlpha('weather', node.level / 500); - var wind = 1.5; - var width = image.width, height = image.height; - node.x += node.dx * wind; - node.y += (2 * node.dy - 1) * wind; - if (node.x + 3 * width <= core.__PIXELS__) { - node.x += 4 * width; - while (node.x > 0) node.x -= width; - } - node.dy += node.delta; - if (node.dy >= 1) { - node.delta = -0.001; - } else if (node.dy <= 0) { - node.delta = 0.001; - } - if (node.y + 3 * height <= core.__PIXELS__) { - node.y += 4 * height; - while (node.y > 0) node.y -= height; - } - else if (node.y >= 0) { - node.y -= height; - } - for (var i = 0; i < 3; ++i) { - for (var j = 0; j < 3; ++j) { - if (node.x + (i + 1) * width <= 0 || node.x + i * width >= core.__PIXELS__ - || node.y + (j + 1) * height <= 0 || node.y + j * height >= core.__PIXELS__) - continue; - core.drawImage('weather', image, node.x + i * width, node.y + j * height); - } - } - core.setAlpha('weather', 1); - core.animateFrame.weather.time = timestamp; -} - -control.prototype._animationFrame_weather_sun = function (timestamp, level) { - if (timestamp - core.animateFrame.weather.time < 30) return; - var node = core.animateFrame.weather.nodes[0]; - var opacity = node.opacity + node.delta; - if (opacity > level / 10 + 0.3 || opacity < level / 10 - 0.3) - node.delta = -node.delta; - node.opacity = opacity; - core.setOpacity('weather', core.clamp(opacity, 0, 1)); - core.animateFrame.weather.time = timestamp; -} - -control.prototype._animateFrame_tip = function (timestamp) { - if (core.animateFrame.tip == null) return; - var tip = core.animateFrame.tip; - if (timestamp - tip.time <= 30) return; - var delta = timestamp - tip.time; - tip.time = timestamp; - - core.setFont('data', "16px Arial"); - core.setTextAlign('data', 'left'); - core.clearMap('data', 0, 0, core.__PIXELS__, 50); - core.ui._drawTip_drawOne(tip); - if (tip.stage == 1) { - tip.opacity += 0.05; - if (tip.opacity >= 0.6) { - tip.stage = 2; - tip.displayTime = 0; - } - } else if (tip.stage == 2) { - tip.displayTime += delta; - if (tip.displayTime >= 1000) tip.stage = 3; - } else tip.opacity -= 0.05; - - if (tip.opacity <= 0) { - core.animateFrame.tip = null; - } -} - -control.prototype._animationFrame_parallelDo = function (timestamp) { - core.control.controldata.parallelDo(timestamp); -} - -// ------ 标题界面的处理 ------ // - -////// 显示游戏开始界面 ////// -control.prototype.showStartAnimate = function (noAnimate, callback) { - this._showStartAnimate_resetDom(); - if (core.flags.startUsingCanvas || noAnimate) - return this._showStartAnimate_finished(core.flags.startUsingCanvas, callback); - core.hideWithAnimate(core.dom.startTop, 20, function () { - core.control._showStartAnimate_finished(false, callback); - }); -} - -control.prototype._showStartAnimate_resetDom = function () { - core.dom.startPanel.style.opacity = 1; - core.dom.startPanel.style.display = "block"; - core.dom.startTop.style.opacity = 1; - core.dom.startTop.style.display = "block"; - core.dom.startButtonGroup.style.display = 'none'; - core.dom.startButtons.style.display = 'block'; - core.dom.levelChooseButtons.style.display = 'none'; - core.status.played = false; - core.clearStatus(); - core.clearMap('all'); - core.dom.musicBtn.style.display = 'block'; - core.setMusicBtn(); - // 重置音量 - core.events.setVolume(1, 0); - core.updateStatusBar(); -} - -control.prototype._showStartAnimate_finished = function (start, callback) { - core.dom.startTop.style.display = 'none'; - core.dom.startButtonGroup.style.display = 'block'; - main.selectedButton = null; - main.selectButton(0); - if (start) core.startGame(); - if (callback) callback(); -} - -////// 隐藏游戏开始界面 ////// -control.prototype.hideStartAnimate = function (callback) { - core.hideWithAnimate(core.dom.startPanel, 20, callback); -} - -////// 游戏是否已经开始 ////// -control.prototype.isPlaying = function () { - return core.status.played; -} - -////// 清除游戏状态和数据 ////// -control.prototype.clearStatus = function () { - // 停止各个Timeout和Interval - for (var i in core.timeout) { - clearTimeout(core.timeout[i]); - core.timeout[i] = null; - } - for (var i in core.interval) { - clearInterval(core.interval[i]); - core.interval[i] = null; - } - core.status = {}; - core.clearStatusBar(); - core.deleteAllCanvas(); - core.status.played = false; -} - -control.prototype._initStatistics = function (totalTime) { - if (!core.isset(core.status.hero.statistics)) - core.status.hero.statistics = { - 'totalTime': totalTime, - 'currTime': 0, - 'hp': 0, - "battle": 0, - 'money': 0, - 'exp': 0, - 'battleDamage': 0, - 'poisonDamage': 0, - 'extraDamage': 0, - 'vampireExtraLoss': 0, - 'damagePerFloor': {}, - 'moveDirectly': 0, - 'ignoreSteps': 0, - 'oneDefEffect': 0, - 'oneMdefEffect': 0, - } -} - -/** - * @param {'battleDamage'|'poisonDamage'|'extraDamage'|'vampireExtraLoss'} key - * @param {number} value - * @param {string} [floorId] - */ -control.prototype.addFloorStatistics = function (key, value, floorId) { - if (!floorId) floorId = core.status.floorId; - const statistics = core.status.hero.statistics; - if (key !== 'battleDamage' && key !== 'poisonDamage' && key !== 'extraDamage' && - key !== 'vampireExtraLoss' - ) return; - if (!statistics.damagePerFloor) { - statistics.damagePerFloor = {}; - } - if (!statistics.damagePerFloor[floorId]) { - statistics.damagePerFloor[floorId] = {}; - } - if (!statistics.damagePerFloor[floorId][key]) { - statistics.damagePerFloor[floorId][key] = 0; - } - statistics.damagePerFloor[floorId][key] += value; -} - -// ------ 自动寻路,人物行走 ------ // - -////// 清除自动寻路路线 ////// -control.prototype.clearAutomaticRouteNode = function (x, y) { - core.clearMap('route', x * 32 + 5 - core.status.automaticRoute.offsetX, y * 32 + 5 - core.status.automaticRoute.offsetY, 27, 27); -} - -////// 停止自动寻路操作 ////// -control.prototype.stopAutomaticRoute = function () { - if (!core.status.played) return; - core.status.automaticRoute.autoHeroMove = false; - core.status.automaticRoute.autoStep = 0; - core.status.automaticRoute.destStep = 0; - core.status.automaticRoute.movedStep = 0; - core.status.automaticRoute.autoStepRoutes = []; - core.status.automaticRoute.destX = null; - core.status.automaticRoute.destY = null; - core.status.automaticRoute.lastDirection = null; - core.status.heroStop = true; - if (core.status.automaticRoute.moveStepBeforeStop.length == 0) - core.deleteCanvas('route'); -} - -////// 保存剩下的寻路,并停止 ////// -control.prototype.saveAndStopAutomaticRoute = function () { - var automaticRoute = core.status.automaticRoute; - if (automaticRoute.moveStepBeforeStop.length == 0) { - automaticRoute.moveStepBeforeStop = automaticRoute.autoStepRoutes.slice(automaticRoute.autoStep - 1); - if (automaticRoute.moveStepBeforeStop.length >= 1) - automaticRoute.moveStepBeforeStop[0].step -= automaticRoute.movedStep; - } - this.stopAutomaticRoute(); -} - -////// 继续剩下的自动寻路操作 ////// -control.prototype.continueAutomaticRoute = function () { - // 此函数只应由events.afterOpenDoor和events.afterBattle调用 - var moveStep = core.status.automaticRoute.moveStepBeforeStop; - //core.status.automaticRoute.moveStepBeforeStop = []; - if (moveStep.length === 0 || (moveStep.length === 1 && moveStep[0].step === 1)) { - core.status.automaticRoute.moveStepBeforeStop = []; - } - else { - core.setAutoHeroMove(moveStep); - } -} - -////// 清空剩下的自动寻路列表 ////// -control.prototype.clearContinueAutomaticRoute = function (callback) { - core.deleteCanvas('route'); - core.status.automaticRoute.moveStepBeforeStop = []; - if (callback) callback(); -} - -////// 设置自动寻路路线 ////// -control.prototype.setAutomaticRoute = function (destX, destY, stepPostfix) { - if (!core.status.played || core.status.lockControl) return; - if (this._setAutomaticRoute_isMoving(destX, destY)) return; - if (this._setAutomaticRoute_isTurning(destX, destY, stepPostfix)) return; - if (this._setAutomaticRoute_clickMoveDirectly(destX, destY, stepPostfix)) return; - // 找寻自动寻路路线 - var moveStep = core.automaticRoute(destX, destY); - if (moveStep.length == 0 && (destX != core.status.hero.loc.x || destY != core.status.hero.loc.y || stepPostfix.length == 0)) - return; - moveStep = moveStep.concat(stepPostfix); - core.status.automaticRoute.destX = destX; - core.status.automaticRoute.destY = destY; - this._setAutomaticRoute_drawRoute(moveStep); - this._setAutomaticRoute_setAutoSteps(moveStep); - // 立刻移动 - core.setAutoHeroMove(); -} - -control.prototype._setAutomaticRoute_isMoving = function (destX, destY) { - if (core.status.automaticRoute.autoHeroMove) { - var lastX = core.status.automaticRoute.destX, lastY = core.status.automaticRoute.destY; - core.stopAutomaticRoute(); - // 双击瞬移 - if (lastX == destX && lastY == destY) { - core.status.automaticRoute.moveDirectly = true; - setTimeout(function () { - if (core.status.automaticRoute.moveDirectly && core.status.heroMoving == 0) { - core.control.tryMoveDirectly(destX, destY); - } - core.status.automaticRoute.moveDirectly = false; - }, core.values.moveSpeed); - } - return true; - } - return false; -} - -control.prototype._setAutomaticRoute_isTurning = function (destX, destY, stepPostfix) { - if (destX == core.status.hero.loc.x && destY == core.status.hero.loc.y && stepPostfix.length == 0) { - if (core.timeout.turnHeroTimeout == null) { - var routeLength = core.status.route.length; - core.timeout.turnHeroTimeout = setTimeout(function () { - if (core.status.route.length == routeLength) core.turnHero(); - clearTimeout(core.timeout.turnHeroTimeout); - core.timeout.turnHeroTimeout = null; - }, 250); - } - else { - clearTimeout(core.timeout.turnHeroTimeout); - core.timeout.turnHeroTimeout = null; - core.getNextItem(); - } - return true; - } - if (core.timeout.turnHeroTimeout != null) return true; - return false; -} - -control.prototype._setAutomaticRoute_clickMoveDirectly = function (destX, destY, stepPostfix) { - // 单击瞬间移动 - if (core.status.heroStop && core.status.heroMoving == 0) { - if (stepPostfix.length <= 1 && !core.hasFlag('__noClickMove__') && core.control.tryMoveDirectly(destX, destY)) - return true; - } - return false; -} - -control.prototype._setAutomaticRoute_drawRoute = function (moveStep) { - // 计算绘制区域的宽高,并尽可能小的创建route层 - var sx = core.bigmap.width * 32, sy = core.bigmap.height * 32, dx = 0, dy = 0; - moveStep.forEach(function (t) { - sx = Math.min(sx, t.x * 32); dx = Math.max(dx, t.x * 32); - sy = Math.min(sy, t.y * 32); dy = Math.max(dy, t.y * 32); - }); - core.status.automaticRoute.offsetX = sx; - core.status.automaticRoute.offsetY = sy; - var ctx = core.createCanvas('route', sx - core.bigmap.offsetX, sy - core.bigmap.offsetY, dx - sx + 32, dy - sy + 32, 95); - ctx.fillStyle = '#bfbfbf'; - ctx.strokeStyle = '#bfbfbf'; - ctx.lineWidth = 8; - for (var m = 0; m < moveStep.length; m++) { - if (m == moveStep.length - 1) { - ctx.fillRect(moveStep[m].x * 32 + 10 - sx, moveStep[m].y * 32 + 10 - sy, 12, 12); - } - else { - ctx.beginPath(); - var cx = moveStep[m].x * 32 + 16 - sx, cy = moveStep[m].y * 32 + 16 - sy; - var currDir = moveStep[m].direction, nextDir = moveStep[m + 1].direction; - ctx.moveTo(cx - core.utils.scan[currDir].x * 11, cy - core.utils.scan[currDir].y * 11); - ctx.lineTo(cx, cy); - ctx.lineTo(cx + core.utils.scan[nextDir].x * 11, cy + core.utils.scan[nextDir].y * 11); - ctx.stroke(); - } - } -} - -control.prototype._setAutomaticRoute_setAutoSteps = function (moveStep) { - // 路线转autoStepRoutes - var step = 0, currStep = null; - moveStep.forEach(function (t) { - var dir = t.direction; - if (currStep == null || currStep == dir) - step++; - else { - core.status.automaticRoute.autoStepRoutes.push({ 'direction': currStep, 'step': step }); - step = 1; - } - currStep = dir; - }); - core.status.automaticRoute.autoStepRoutes.push({ 'direction': currStep, 'step': step }); -} - -////// 设置勇士的自动行走路线 ////// -control.prototype.setAutoHeroMove = function (steps) { - steps = steps || core.status.automaticRoute.autoStepRoutes; - if (steps.length == 0) return; - core.status.automaticRoute.autoStepRoutes = steps; - core.status.automaticRoute.autoHeroMove = true; - core.status.automaticRoute.autoStep = 1; - core.status.automaticRoute.destStep = steps[0].step; - core.moveHero(steps[0].direction); -} - -////// 设置行走的效果动画 ////// -control.prototype.setHeroMoveInterval = function (callback) { - if (core.status.heroMoving > 0) return; - if (core.status.replay.speed == 24) { - if (callback) callback(); - return; - } - - core.status.heroMoving = 1; - - var toAdd = 1; - if (core.status.replay.speed > 3) toAdd = 2; - if (core.status.replay.speed > 6) toAdd = 4; - if (core.status.replay.speed > 12) toAdd = 8; - - core.interval.heroMoveInterval = window.setInterval(function () { - core.status.heroMoving += toAdd; - if (core.status.heroMoving >= 8) { - clearInterval(core.interval.heroMoveInterval); - core.status.heroMoving = 0; - if (callback) callback(); - } - }, core.values.moveSpeed / 8 * toAdd / core.status.replay.speed); -} - -////// 每移动一格后执行的事件 ////// -control.prototype.moveOneStep = function (callback) { - return this.controldata.moveOneStep(callback); -} - -////// 实际每一步的行走过程 ////// -control.prototype.moveAction = function (callback) { - if (core.status.heroMoving > 0) return; - const nextX = core.nextX(), nextY = core.nextY(); - const noPass = core.noPass(nextX, nextY); - const canMove = core.canMoveHero(); - // 下一个点如果不可通行 - if (noPass || !canMove) return this._moveAction_noPass(canMove, callback); - // 下一个点如果可通行 - // 满足条件时触发一次自动存档 - if (!core.isReplaying()) { - const dir = core.getHeroLoc('direction'); - let needAutoSave = false; - if (core.getLocalStorage("autoSaveBeforeUseItem") && - core.onSki(core.maps.getBgNumber(nextX, nextY))) { - needAutoSave = true; - } - else if (core.getLocalStorage('autoSaveBeforePickItem') && - core.getBlockCls(nextX, nextY) === 'items') { - needAutoSave = true; - } - if (needAutoSave) { - core.status.route.push(`turn:${dir}`); // 保存触发转向后的方向 - core.control.autosave(); - } - } - this._moveAction_moving(callback); -} - -control.prototype._moveAction_noPass = function (canMove, callback) { - core.status.route.push(core.getHeroLoc('direction')); - core.status.automaticRoute.moveStepBeforeStop = []; - core.status.automaticRoute.lastDirection = core.getHeroLoc('direction'); - if (canMove) core.trigger(core.nextX(), core.nextY()); - core.drawHero(); - - if (core.status.automaticRoute.moveStepBeforeStop.length == 0) { - core.clearContinueAutomaticRoute(); - core.stopAutomaticRoute(); - } - if (callback) callback(); -} - -control.prototype._moveAction_moving = function (callback) { - core.setHeroMoveInterval(function () { - core.setHeroLoc('x', core.nextX(), true); - core.setHeroLoc('y', core.nextY(), true); - - var direction = core.getHeroLoc('direction'); - core.control._moveAction_popAutomaticRoute(); - core.status.route.push(direction); - - core.moveOneStep(); - core.checkRouteFolding(); - if (callback) callback(); - }); -} - -control.prototype._moveAction_popAutomaticRoute = function () { - var automaticRoute = core.status.automaticRoute; - // 检查自动寻路是否被弹出 - if (automaticRoute.autoHeroMove) { - automaticRoute.movedStep++; - automaticRoute.lastDirection = core.getHeroLoc('direction'); - if (automaticRoute.destStep == automaticRoute.movedStep) { - if (automaticRoute.autoStep == automaticRoute.autoStepRoutes.length) { - core.clearContinueAutomaticRoute(); - core.stopAutomaticRoute(); - } - else { - automaticRoute.movedStep = 0; - automaticRoute.destStep = automaticRoute.autoStepRoutes[automaticRoute.autoStep].step; - core.setHeroLoc('direction', automaticRoute.autoStepRoutes[automaticRoute.autoStep].direction); - core.status.automaticRoute.autoStep++; - } - } - } -} - -////// 让勇士开始移动 ////// -control.prototype.moveHero = function (direction, callback) { - // 如果正在移动,直接return - if (core.status.heroMoving != 0) return; - if (core.isset(direction)) { - if (core.status.hero.loc.direction != direction) { - core.status.dirBeforeMove = core.status.hero.loc.direction; // 记录转向前的方向,如果下一步自动保存要还原到这个方向 - } - core.setHeroLoc('direction', direction); - } - - if (callback) return this.moveAction(callback); - this._moveHero_moving(); -} - -control.prototype._moveHero_moving = function () { - // ------ 我已经看不懂这个函数了,反正好用就行23333333 - core.status.heroStop = false; - core.status.automaticRoute.moveDirectly = false; - var move = function () { - if (!core.status.heroStop) { - if (core.hasFlag('debug') && core.status.ctrlDown) { - if (core.status.heroMoving != 0) return; - // 检测是否穿出去 - var nx = core.nextX(), ny = core.nextY(); - if (nx < 0 || nx >= core.bigmap.width || ny < 0 || ny >= core.bigmap.height) return; - core.eventMoveHero([core.getHeroLoc('direction')], core.values.moveSpeed, move); - } - else if (core.hasFlag('debug_wallHacking')) { - if (core.status.heroMoving != 0) return; - // 检测是否穿出去 - var nx = core.nextX(), ny = core.nextY(); - if (nx < 0 || nx >= core.bigmap.width || ny < 0 || ny >= core.bigmap.height) return; - core.eventMoveHero([core.getHeroLoc('direction')], core.values.moveSpeed); // 取消递归,方便调试时单击走一步的情形 - } - else { - core.moveAction(); - setTimeout(move, 50); - } - } - } - move(); -} - -////// 当前是否正在移动 ////// -control.prototype.isMoving = function () { - return !core.status.heroStop || core.status.heroMoving > 0; -} - -////// 停止勇士的一切行动,等待勇士行动结束后,再执行callback ////// -control.prototype.waitHeroToStop = function (callback) { - var lastDirection = core.status.automaticRoute.lastDirection; - core.stopAutomaticRoute(); - core.clearContinueAutomaticRoute(); - if (callback) { - core.status.replay.animate = true; - core.lockControl(); - core.status.automaticRoute.moveDirectly = false; - setTimeout(function () { - core.status.replay.animate = false; - if (core.isset(lastDirection)) - core.setHeroLoc('direction', lastDirection); - core.drawHero(); - callback(); - }, core.status.replay.speed == 24 ? 1 : 30); - } -} - -////// 转向 ////// -control.prototype.turnHero = function (direction) { - if (direction) { - core.setHeroLoc('direction', direction); - core.drawHero(); - core.status.route.push("turn:" + direction); - return; - } - core.setHeroLoc('direction', core.turnDirection(':right')); - core.drawHero(); - core.status.route.push("turn"); - core.checkRouteFolding(); -} - -////// 瞬间移动 ////// -control.prototype.moveDirectly = function (destX, destY, ignoreSteps) { - return this.controldata.moveDirectly(destX, destY, ignoreSteps); -} - -////// 尝试瞬间移动 ////// -control.prototype.tryMoveDirectly = function (destX, destY) { - if (this.nearHero(destX, destY)) return false; - var canMoveArray = core.maps.generateMovableArray(); - var dirs = [[destX, destY], [destX - 1, destY, "right"], [destX, destY - 1, "down"], [destX, destY + 1, "up"], [destX + 1, destY, "left"]]; - var canMoveDirectlyArray = core.canMoveDirectlyArray(dirs, canMoveArray); - - for (var i = 0; i < dirs.length; ++i) { - var d = dirs[i], dx = d[0], dy = d[1], dir = d[2]; - if (dx < 0 || dx >= core.bigmap.width || dy < 0 || dy >= core.bigmap.height) continue; - if (dir && !core.inArray(canMoveArray[dx][dy], dir)) continue; - if (canMoveDirectlyArray[i] < 0) continue; - if (core.control.moveDirectly(dx, dy, canMoveDirectlyArray[i])) { - if (dir) core.moveHero(dir, function () { }); - return true; - } - } - return false; -} - -////// 绘制勇士 ////// -control.prototype.drawHero = function (status, offset, frame) { - if (!core.isPlaying() || !core.status.floorId || core.status.gameOver) return; - var x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), direction = core.getHeroLoc('direction'); - status = status || 'stop'; - if (!offset) offset = 0; - - var way = core.utils.scan2[direction]; - var dx = way.x, dy = way.y; - var offsetX = typeof offset == 'number' ? dx * offset : (offset.x || 0); - var offsetY = typeof offset == 'number' ? dy * offset : (offset.y || 0); - offset = { x: offsetX, y: offsetY, offset: offset }; - - core.clearAutomaticRouteNode(x + dx, y + dy); - core.clearMap('hero'); - core.status.heroCenter.px = 32 * x + offsetX + 16; - core.status.heroCenter.py = 32 * y + offsetY + 32 - core.material.icons.hero.height / 2; - - // 重置hero层画布 - core.setGameCanvasTranslate('hero', 0, 0); - delete core.canvas.hero._px; - delete core.canvas.hero._py; - core.status.preview.enabled = false; - if (!core.hasFlag('__lockViewport__')) { - this._drawHero_updateViewport(x, y, offset); - } - - this._drawHero_draw(direction, x, y, status, offset, frame); -} - -control.prototype._drawHero_updateViewport = function (x, y, offset) { - core.bigmap.offsetX = core.clamp((x - core.__HALF_SIZE__) * 32 + offset.x, 0, 32 * core.bigmap.width - core.__PIXELS__); - core.bigmap.offsetY = core.clamp((y - core.__HALF_SIZE__) * 32 + offset.y, 0, 32 * core.bigmap.height - core.__PIXELS__); - core.control.updateViewport(); -} - -control.prototype._drawHero_draw = function (direction, x, y, status, offset, frame) { - offset = offset || { x: 0, y: 0, offset: 0, px: 0, py: 0 }; - var opacity = core.setAlpha('hero', core.getFlag('__heroOpacity__', 1)) - this._drawHero_getDrawObjs(direction, x, y, status, offset).forEach(function (block) { - core.drawImage('hero', block.img, (block.heroIcon[block.status] + (frame || 0)) % 4 * block.width, - block.heroIcon.loc * block.height, block.width, block.height, - block.posx + (32 - block.width) / 2, block.posy + 32 - block.height, block.width, block.height); - }); - core.setAlpha('hero', opacity); -} - -control.prototype._drawHero_getDrawObjs = function (direction, x, y, status, offset) { - var heroIconArr = core.material.icons.hero, drawObjs = [], index = 0; - drawObjs.push({ - "img": core.material.images.hero, - "width": core.material.icons.hero.width || 32, - "height": core.material.icons.hero.height, - "heroIcon": heroIconArr[direction], - "posx": x * 32 - core.bigmap.offsetX + offset.x, - "posy": y * 32 - core.bigmap.offsetY + offset.y, - "status": status, - "index": index++, - }); - if (typeof offset.offset == 'number') { - core.status.hero.followers.forEach(function (t) { - drawObjs.push({ - "img": core.material.images.images[t.name], - "width": core.material.images.images[t.name].width / 4, - "height": core.material.images.images[t.name].height / 4, - "heroIcon": heroIconArr[t.direction], - "posx": 32 * t.x - core.bigmap.offsetX + (t.stop ? 0 : core.utils.scan2[t.direction].x * Math.abs(offset.offset)), - "posy": 32 * t.y - core.bigmap.offsetY + (t.stop ? 0 : core.utils.scan2[t.direction].y * Math.abs(offset.offset)), - "status": t.stop ? "stop" : status, - "index": index++ - }); - }); - } - return drawObjs.sort(function (a, b) { - return a.posy == b.posy ? b.index - a.index : a.posy - b.posy; - }); -} - -control.prototype.setHeroOpacity = function (opacity, moveMode, time, callback) { - time = time || 0; - if (time == 0) { - core.setFlag('__heroOpacity__', opacity); - core.drawHero(); - if (callback) callback(); - return; - } - time /= Math.max(core.status.replay.speed, 1) - - var fromOpacity = core.getFlag('__heroOpacity__', 1); - var step = 0, steps = parseInt(time / 10); - if (steps <= 0) steps = 1; - var moveFunc = core.applyEasing(moveMode); - - var animate = setInterval(function () { - step++; - core.setFlag('__heroOpacity__', fromOpacity + (opacity - fromOpacity) * moveFunc(step / steps)); - core.drawHero(); - if (step == steps) { - delete core.animateFrame.asyncId[animate]; - clearInterval(animate); - if (callback) callback(); - } - }, 10); - - core.animateFrame.lastAsyncId = animate; - core.animateFrame.asyncId[animate] = callback; -} - -// ------ 画布、位置、阻激夹域,显伤 ------ // - -////// 设置画布偏移 -control.prototype.setGameCanvasTranslate = function (canvas, x, y) { - var c = core.dom.gameCanvas[canvas]; - x = x * core.domStyle.scale; - y = y * core.domStyle.scale; - c.style.transform = 'translate(' + x + 'px,' + y + 'px)'; - c.style.webkitTransform = 'translate(' + x + 'px,' + y + 'px)'; - c.style.OTransform = 'translate(' + x + 'px,' + y + 'px)'; - c.style.MozTransform = 'translate(' + x + 'px,' + y + 'px)'; - if (main.mode === 'editor' && editor.isMobile) { - c.style.transform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; - c.style.webkitTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; - c.style.OTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; - c.style.MozTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; - } -}; - -////// 加减画布偏移 -control.prototype.addGameCanvasTranslate = function (x, y) { - for (var ii = 0, canvas; canvas = core.dom.gameCanvas[ii]; ii++) { - var id = canvas.getAttribute('id'); - if (id == 'ui' || id == 'data') continue; // UI层和data层不移动 - var offsetX = x, offsetY = y; - if (core.bigmap.canvas.indexOf(id) >= 0) { - if (core.bigmap.v2) { - offsetX -= (core.bigmap.offsetX - 32 * core.bigmap.posX) + 32; - offsetY -= (core.bigmap.offsetY - 32 * core.bigmap.posY) + 32; - } else { - offsetX -= core.bigmap.offsetX; - offsetY -= core.bigmap.offsetY; - } - } - core.control.setGameCanvasTranslate(id, offsetX, offsetY); - } -} - -////// 更新视野范围 ////// -control.prototype.updateViewport = function () { - // 当前是否应该重绘? - if (core.bigmap.v2) { - if (core.bigmap.offsetX >= core.bigmap.posX * 32 + 32 - || core.bigmap.offsetX <= core.bigmap.posX * 32 - 32 - || core.bigmap.offsetY >= core.bigmap.posY * 32 + 32 - || core.bigmap.offsetY <= core.bigmap.posY * 32 - 32) { - core.bigmap.posX = parseInt(core.bigmap.offsetX / 32); - core.bigmap.posY = parseInt(core.bigmap.offsetY / 32); - core.redrawMap(); - } - } else { - core.bigmap.posX = core.bigmap.posY = 0; - } - var offsetX = core.bigmap.v2 ? -(core.bigmap.offsetX - 32 * core.bigmap.posX) - 32 : -core.bigmap.offsetX; - var offsetY = core.bigmap.v2 ? -(core.bigmap.offsetY - 32 * core.bigmap.posY) - 32 : -core.bigmap.offsetY; - - core.bigmap.canvas.forEach(function (cn) { - core.control.setGameCanvasTranslate(cn, offsetX, offsetY); - }); - // ------ 路线 - core.relocateCanvas('route', core.status.automaticRoute.offsetX - core.bigmap.offsetX, core.status.automaticRoute.offsetY - core.bigmap.offsetY); - // ------ 所有的大怪物也都需要重定位 - for (var one in core.dymCanvas) { - if (one.startsWith('_bigImage_')) { - var ox = core.dymCanvas[one].canvas.getAttribute('_ox'); - var oy = core.dymCanvas[one].canvas.getAttribute('_oy'); - if (ox != null && oy != null) { - core.relocateCanvas(one, parseInt(ox) - core.bigmap.offsetX, parseInt(oy) - core.bigmap.offsetY); - } - } - } - -} - -////// 设置视野范围 ////// -control.prototype.setViewport = function (px, py) { - var originOffsetX = core.bigmap.offsetX, originOffsetY = core.bigmap.offsetY; - core.bigmap.offsetX = core.clamp(px, 0, 32 * core.bigmap.width - core.__PIXELS__); - core.bigmap.offsetY = core.clamp(py, 0, 32 * core.bigmap.height - core.__PIXELS__); - this.updateViewport(); - // ------ hero层也需要! - var px = parseFloat(core.canvas.hero._px) || 0; - var py = parseFloat(core.canvas.hero._py) || 0; - px += originOffsetX - core.bigmap.offsetX; - py += originOffsetY - core.bigmap.offsetY; - core.control.setGameCanvasTranslate('hero', px, py); - core.canvas.hero._px = px; - core.canvas.hero._py = py; -} - -////// 移动视野范围 ////// -control.prototype.moveViewport = function (x, y, moveMode, time, callback) { - time = time || 0; - time /= Math.max(core.status.replay.speed, 1) - var per_time = 10, step = 0, steps = parseInt(time / per_time); - if (steps <= 0) { - this.setViewport(32 * x, 32 * y); - if (callback) callback(); - return; - } - var px = core.clamp(32 * x, 0, 32 * core.bigmap.width - core.__PIXELS__); - var py = core.clamp(32 * y, 0, 32 * core.bigmap.height - core.__PIXELS__); - var cx = core.bigmap.offsetX; - var cy = core.bigmap.offsetY; - var moveFunc = core.applyEasing(moveMode); - - var animate = window.setInterval(function () { - step++; - core.setViewport(cx + moveFunc(step / steps) * (px - cx), cy + moveFunc(step / steps) * (py - cy)); - if (step == steps) { - delete core.animateFrame.asyncId[animate]; - clearInterval(animate); - core.setViewport(px, py); - if (callback) callback(); - } - }, per_time); - - core.animateFrame.lastAsyncId = animate; - core.animateFrame.asyncId[animate] = callback; -} - -////// 获得勇士面对位置的x坐标 ////// -control.prototype.nextX = function (n) { - if (n == null) n = 1; - return core.getHeroLoc('x') + core.utils.scan[core.getHeroLoc('direction')].x * n; -} - -////// 获得勇士面对位置的y坐标 ////// -control.prototype.nextY = function (n) { - if (n == null) n = 1; - return core.getHeroLoc('y') + core.utils.scan[core.getHeroLoc('direction')].y * n; -} - -////// 某个点是否在勇士旁边 ////// -control.prototype.nearHero = function (x, y, n) { - if (n == null) n = 1; - return Math.abs(x - core.getHeroLoc('x')) + Math.abs(y - core.getHeroLoc('y')) <= n; -} - -////// 聚集跟随者 ////// -control.prototype.gatherFollowers = function () { - var x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), dir = core.getHeroLoc('direction'); - core.status.hero.followers.forEach(function (t) { - t.x = x; - t.y = y; - t.stop = true; - t.direction = dir; - }); -} - -////// 更新跟随者坐标 ////// -control.prototype.updateFollowers = function () { - core.status.hero.followers.forEach(function (t) { - if (!t.stop) { - t.x += core.utils.scan2[t.direction].x; - t.y += core.utils.scan2[t.direction].y; - } - }) - - var nowx = core.getHeroLoc('x'), nowy = core.getHeroLoc('y'); - core.status.hero.followers.forEach(function (t) { - t.stop = true; - var dx = nowx - t.x, dy = nowy - t.y; - for (var dir in core.utils.scan2) { - if (core.utils.scan2[dir].x == dx && core.utils.scan2[dir].y == dy) { - t.stop = false; - t.direction = dir; - } - } - nowx = t.x; nowy = t.y; - }) -} - -////// 瞬移更新跟随者坐标 ////// -control.prototype._moveDirectyFollowers = function (x, y) { - var route = core.automaticRoute(x, y); - if (route.length == 0) route = [{ x: x, y: y, direction: core.getHeroLoc('direction') }]; - - var nowx = x, nowy = y; - for (var i = 0; i < core.status.hero.followers.length; ++i) { - var t = core.status.hero.followers[i]; - var index = route.length - i - 2; - if (index < 0) index = 0; - t.stop = true; - t.x = route[index].x; - t.y = route[index].y; - t.direction = route[index].direction; - var dx = nowx - t.x, dy = nowy - t.y; - for (var dir in core.utils.scan2) { - if (core.utils.scan2[dir].x == dx && core.utils.scan2[dir].y == dy) { - t.stop = false; - t.direction = dir; - } - } - nowx = t.x; nowy = t.y; - } -} - -////// 更新领域、夹击、阻击的伤害地图 ////// -control.prototype.updateCheckBlock = function (floorId) { - return this.controldata.updateCheckBlock(floorId); -} - -////// 检查并执行领域、夹击、阻击事件 ////// - -control.prototype.checkBlock = function () { - const floorId = core.status.floorId; - const x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), loc = x + "," + y; - const damage = core.status.checkBlock.damage[loc]; - if (damage) { - core.status.hero.hp -= damage; - const text = (Object.keys(core.status.checkBlock.type[loc] || {}).join(",")) || "伤害"; - core.drawTip("受到" + text + damage + "点"); - core.drawHeroAnimate("zone"); - this._checkBlock_disableQuickShop(); - core.status.hero.statistics.extraDamage += damage; - if (!core.status.hero.statistics.damagePerFloor) { - core.status.hero.statistics.damagePerFloor = {}; - } - if (core.control.addFloorStatistics) { - core.control.addFloorStatistics("extraDamage", damage, floorId); - } - if (core.status.hero.hp <= 0) { - core.status.hero.hp = 0; - core.updateStatusBar(false, true); - core.events.lose(); - return; - } else { - core.updateStatusBar(false, true); - } - } - let actions = []; - - const ambushAction = this._checkBlock_ambush(core.status.checkBlock.ambush[loc]); - if (ambushAction.length > 0) core.push(actions, ambushAction); - - const repulseAction = this._checkBlock_repulse(core.status.checkBlock.repulse[loc]); - if (repulseAction.length > 0) core.push(actions, repulseAction); - - // 追猎需要等待阻击完成,避免发生碰撞导致怪物消失 先清理四周追猎,追猎移动,再清理一轮四周的追猎 - const currChase = core.status.checkBlock.chase[loc]; - if (currChase && currChase.length > 0) { - if (actions.length === 0) { - const adjacentChase = core.checkBlock_adjacentChase(); - if (adjacentChase && adjacentChase.length > 0) core.push(actions, adjacentChase); - } - else core.push(actions, { "type": "function", "async": true, "function": "function(){core.checkBlock_adjacentChase(true);}" }); - } - const chaseAction = this._checkBlock_chase(currChase); - if (chaseAction.length > 0) core.push(actions, chaseAction); - if (currChase && currChase.length > 0) { - core.push(actions, { "type": "function", "async": true, "function": "function(){core.checkBlock_adjacentChase(true);}" }); - } - if (ambushAction.length > 0 && core.getLocalStorage("autoSaveBeforeUseItem")) { - core.push(actions, { "type": "autoSave" }); // 捕捉触发后自动存档 - } - - const autoClear = core.plugin.autoClear; - if (autoClear) { // 检查autoClear的存在性,防止接档出现bug - if (actions.length > 0) { - actions.push({ - "type": "function", "async": true, - "function": "function(){core.plugin.autoClear();core.doAction();}" - }); // 注意要放在事件队列最后。经测试放在insertAction回调当中仍有小概率先于阻击结算触发,原因不明 - core.insertAction(actions, x, y); - } - else autoClear(); // 阻击结算后执行自动清怪 - } - else { - if (actions.length > 0) core.insertAction(actions, x, y); - } -} - -control.prototype.checkBlock_adjacentChase = function (inAction) { - const { x: hx, y: hy } = core.status.hero.loc; - const scan = core.utils.scan; - const actions = []; - - for (const dir in scan) { - const [nx, ny] = [hx + scan[dir].x, hy + scan[dir].y]; - const blockId = core.getBlockId(nx, ny); - const special = core.getEnemyValue(blockId, 'special', nx, ny); - if (core.hasSpecial(special, 28)) { - actions.push({ - "type": "function", "function": "function() { " + - "core.battle('" + blockId + "', " + nx + "," + ny + ", true, core.doAction); " + - "}", "async": true - }); - } - } - if (!inAction) return actions; - if (actions.length > 0) core.insertAction(actions); - core.doAction(); -} - -control.prototype._checkBlock_disableQuickShop = function () { - // 禁用快捷商店 - if (core.flags.disableShopOnDamage) { - Object.keys(core.status.shops).forEach(function (shopId) { - core.setShopVisited(shopId, false); - }); - } -} - -////// 阻击 ////// -control.prototype._checkBlock_repulse = function (repulse) { - if (!repulse || repulse.length == 0) return []; - var actions = []; - repulse.forEach(function (t) { - actions.push({ "type": "move", "loc": [t[0], t[1]], "steps": [t[3]], "time": 80, "keep": true, "async": true }); - }); - if (actions.length > 0) actions.push({ "type": "waitAsync" }); - return actions; -} - -////// 捕捉 ////// -control.prototype._checkBlock_ambush = function (ambush) { - if (!ambush || ambush.length == 0) return []; - // 捕捉效果 - let actions = []; - ambush.forEach(function (t) { - actions.push({ "type": "move", "loc": [t[0], t[1]], "steps": [t[3]], "time": 100, "keep": false, "async": true }); - }); - if (actions.length > 0) actions.push({ "type": "waitAsync" }); - // 强制战斗 - ambush.forEach(function (t) { - actions.push({ - "type": "function", "function": "function() { " + - "core.battle('" + t[2] + "', " + t[0] + "," + t[1] + ", true, core.doAction); " + - "}", "async": true - }); - }); - return actions; -} - -////// 获取追猎的视野和行动可穿越的图块类型 ////// -control.prototype.getChaseType = function () { - let clsList = ['items']; - if (core.flags.chaseThroughEnemy) core.push(clsList, ['enemys', 'enemy48']); - return clsList; -} - -////// 追猎 ////// -control.prototype._checkBlock_chase = function (chase) { - if (!chase || chase.length === 0) return []; - var actions = []; - const { x: hx, y: hy } = core.status.hero.loc; - const reverseDir = { 'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left' }; - chase = chase.sort((a, b) => { - const { x: ax, y: ay } = a; - const { x: bx, y: by } = b; - return Math.abs(ax - hx) + Math.abs(ay - hy) - Math.abs(bx - hx) - Math.abs(by - hy); - }) - chase.forEach((currChaseInfo) => { - const { x, y, dir } = currChaseInfo; - const [aimx, aimy] = [x + core.utils.scan[dir].x, y + core.utils.scan[dir].y]; - if (!(aimx === hx && aimy === hy)) { - actions.push({ - "type": "if", "condition": "!core.getBlock(" + aimx + "," + aimy + ")", - "true": [ - { "type": "move", "loc": [x, y], "time": 80, "keep": true, "async": true, "steps": [dir + ":1"] }, - ], - "false": [ - { "type": "setValue", "name": "flag:chaseAimCls", "value": `core.getBlockCls(${aimx},${aimy})` }, - { - "type": "if", "condition": "core.getChaseType().includes(core.getFlag('chaseAimCls'))", - "true": [ - { - "type": "if", "condition": "[\"enemys\",\"enemy48\"].includes(core.getFlag('chaseAimCls'))", - "true": [ - { - "type": "function", "function": `function () { - core.exchangeBlock(${x}, ${y}, ${aimx}, ${aimy}, '${dir}', 100, null); }` - }, - ], - "false": [ - { "type": "move", "loc": [x, y], "time": 80, "keep": true, "async": true, "steps": [dir + ":1"] }, - { "type": "move", "loc": [aimx, aimy], "time": 80, "keep": true, "async": true, "steps": [reverseDir[dir] + ":1"] }, - ] - }, - { "type": "waitAsync", "excludeAnimates": true }, - ] - }, - { "type": "setValue", "name": "flag:chaseCls", "value": null }, - ] - }); - } - }) - - if (actions.length > 0) actions.push({ "type": "waitAsync" }); - return actions; -} - -////// 更新全地图显伤 ////// -control.prototype.updateDamage = function (floorId, ctx) { - floorId = floorId || core.status.floorId; - if (!floorId || core.status.gameOver || main.mode != 'play') return; - var onMap = ctx == null; - - // 没有怪物手册 - if (!core.hasItem('book')) return; - core.status.damage.posX = core.bigmap.posX; - core.status.damage.posY = core.bigmap.posY; - if (!onMap) { - var width = core.floors[floorId].width, height = core.floors[floorId].height; - // 地图过大的缩略图不绘制显伤 - if (width * height > core.bigmap.threshold) return; - } - this._updateDamage_damage(floorId, onMap); - this._updateDamage_extraDamage(floorId, onMap); - this.drawDamage(ctx); -} - -control.prototype.getEnemyValueString = function(name, blockId, x, y, floorId) { - if (name == null) return { text: null, color: null }; - const colorMap = { - hp: '#55ff55', atk: '#ff5555', - def: '#5555ff', money: '#ffff55', exp: '#ffaa33', - criticalDamage: '#B22222', defDamage: '#1E90FF', - vampireDiff:'#f20c00', - } - switch (name) { - case "damage": - if (core.flags.displayEnemyDamage) { - const damageString = core.enemys.getDamageString(blockId, x, y, floorId); - return { text: damageString.damage, color: damageString.color }; - } - break; - case "critical": - if (core.flags.displayCritical) { - const critical = core.enemys.nextCriticals(blockId, 1, x, y, floorId); - const criticalString = core.formatBigNumber((critical[0] || [])[0], true); - return { text: criticalString == '???' ? '?' : criticalString, color: '#FFFFFF' }; - } - break; - case "hp": - case "atk": - case "def": - case "money": - case "exp": - const value = core.enemys.getEnemyValue(blockId, name, x, y, floorId); - const valueString = core.utils.formatBigNumber(value, 5); - return { text: valueString, color: colorMap[name] }; - case "notBomb": - const notBomb = core.enemys.getEnemyValue(blockId, name, x, y, floorId); - const notBombStr = notBomb ? "b" : ""; - return { text: notBombStr, color: colorMap[name] || "white" }; - case "criticalDamage": - let criticalDamage = 0; - const criticals = core.enemys.nextCriticals(blockId, 1, x, y, floorId); - if (criticals && criticals.length > 0) { - criticalDamage = criticals[0][1]; - criticalDamage = core.formatBigNumber(criticalDamage, 5); - } - return { text: criticalDamage, color: colorMap.criticalDamage }; - case "defDamage": - const defDamage = core.enemys.getDefDamage(blockId, 1, x, y, floorId); - return { text: defDamage, color: colorMap.defDamage }; - case "vampireDiff": - const info = { text: "", color: colorMap.vampireDiff }; - if (!core.enemys.getVampireExtraLoss) return info; - const loss = core.enemys.getVampireExtraLoss(blockId, x, y, floorId); - if (loss != null) info.text = `+${loss}`; - return info; - case "special": - const specialData = core.getLocalStorage('specialIconData', {}); - const special = core.enemys.getEnemyValue(blockId, 'special', x, y, floorId); - const specialArr = core.utils.parseSpecial(special); - let text = ''; - for (const s of specialArr) { - if (specialData[s]) { - text += specialData[s]; - } - } - return { text, color: '#FFFFFF' }; - } -} - -control.prototype.pushDamageData = function(name, px, py, blockId, x, y, floorId) { - if (name == null) return; - const { text, color } = this.getEnemyValueString(name, blockId, x, y, floorId); - core.status.damage.data.push({ text, px, py, color }); -} - -control.prototype._updateDamage_damage = function (floorId, onMap) { - core.status.damage.data = []; - if (!core.flags.displayEnemyDamage && !core.flags.displayExtraDamage) return; - - core.extractBlocks(floorId); - - core.status.maps[floorId].blocks.forEach(function (block) { - const x = block.x, - y = block.y, - blockId = block.event.id; - - // v2优化,只绘制范围内的部分 - if (onMap && core.bigmap.v2) { - if (x < core.bigmap.posX - core.bigmap.extend || x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend - || y < core.bigmap.posY - core.bigmap.extend || y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend) { - return; - } - } - - if (!block.disable && block.event.cls.indexOf('enemy') == 0 && block.event.displayDamage !== false) { - const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} }; - const data = core.getLocalStorage('displayData', defaultData); - - core.control.pushDamageData(data.leftdown[1], 32 * x + 1, 32 * (y + 1) - 1, blockId, x, y, floorId); - core.control.pushDamageData(data.leftdown[2], 32 * x + 1, 32 * (y + 1) - 11, blockId, x, y, floorId); - core.control.pushDamageData(data.leftdown[3], 32 * x + 1, 32 * (y + 1) - 21, blockId, x, y, floorId); - core.control.pushDamageData(data.rightup[1], 32 * x + 22, 32 * (y + 1) - 21, blockId, x, y, floorId); - core.control.pushDamageData(data.rightup[2], 32 * x + 22, 32 * (y + 1) - 11, blockId, x, y, floorId); - core.control.pushDamageData(data.rightup[3], 32 * x + 22, 32 * (y + 1) - 1, blockId, x, y, floorId); - } - }); -} - -control.prototype._updateDamage_extraDamage = function (floorId, onMap) { - core.status.damage.extraData = []; - if (!core.flags.displayExtraDamage) return; - - var width = core.floors[floorId].width, height = core.floors[floorId].height; - var startX = onMap && core.bigmap.v2 ? Math.max(0, core.bigmap.posX - core.bigmap.extend) : 0; - var endX = onMap && core.bigmap.v2 ? Math.min(width, core.bigmap.posX + core.__SIZE__ + core.bigmap.extend + 1) : width; - var startY = onMap && core.bigmap.v2 ? Math.max(0, core.bigmap.posY - core.bigmap.extend) : 0; - var endY = onMap && core.bigmap.v2 ? Math.min(height, core.bigmap.posY + core.__SIZE__ + core.bigmap.extend + 1) : height; - - for (var x = startX; x < endX; x++) { - for (var y = startY; y < endY; y++) { - var alpha = 1; - if (core.noPass(x, y, floorId)) { - if (core.flags.extraDamageType == 2) alpha = 0; - else if (core.flags.extraDamageType == 1) alpha = 0.6; - } - var damage = core.status.checkBlock.damage[x + "," + y] || 0; - if (damage > 0) { // 该点伤害 - damage = core.formatBigNumber(damage, true); - core.status.damage.extraData.push({ text: damage, px: 32 * x + 16, py: 32 * (y + 1) - 14, color: '#ffaa33', alpha: alpha }); - } - else { // 检查捕捉 - if (core.status.checkBlock.ambush[x + "," + y]) { - core.status.damage.extraData.push({ text: '!', px: 32 * x + 16, py: 32 * (y + 1) - 14, color: '#ffaa33', alpha: alpha }); - } - } - } - } -} - -////// 重绘地图显伤 ////// -control.prototype.drawDamage = function (ctx) { - if (core.status.gameOver || !core.status.damage || main.mode != 'play') return; - var onMap = false; - if (ctx == null) { - ctx = core.canvas.damage; - core.clearMap('damage'); - onMap = true; - } - - if (onMap && core.bigmap.v2) { - // 检查是否需要重算... - if (Math.abs(core.bigmap.posX - core.status.damage.posX) >= core.bigmap.extend - 1 - || Math.abs(core.bigmap.posY - core.status.damage.posY) >= core.bigmap.extend - 1) { - return this.updateDamage(); - } - } - return this._drawDamage_draw(ctx, onMap); -} - -control.prototype._drawDamage_draw = function (ctx, onMap) { - if (!core.hasItem('book')) return; - - core.setFont(ctx, "bold 11px Arial"); - core.setTextAlign(ctx, 'left'); - core.status.damage.data.forEach(function (one) { - var px = one.px, py = one.py; - if (onMap && core.bigmap.v2) { - px -= core.bigmap.posX * 32; - py -= core.bigmap.posY * 32; - if (px < -32 * 2 || px > core.__PIXELS__ + 32 || py < -32 || py > core.__PIXELS__ + 32) - return; - } - core.fillBoldText(ctx, one.text, px, py, one.color); - }); - - core.setTextAlign(ctx, 'center'); - core.status.damage.extraData.forEach(function (one) { - var px = one.px, py = one.py; - if (onMap && core.bigmap.v2) { - px -= core.bigmap.posX * 32; - py -= core.bigmap.posY * 32; - if (px < -32 || px > core.__PIXELS__ + 32 || py < -32 || py > core.__PIXELS__ + 32) - return; - } - var alpha = core.setAlpha(ctx, one.alpha); - core.fillBoldText(ctx, one.text, px, py, one.color); - core.setAlpha(ctx, alpha); - }); -} - -// ------ 录像相关 ------ // - -////// 选择录像文件 ////// -control.prototype.chooseReplayFile = function () { - core.readFile(function (obj) { - if (obj.name != core.firstData.name) return alert("存档和游戏不一致!"); - if (!obj.route) return alert("无效的录像!"); - var _replay = function () { - core.startGame(core.flags.startUsingCanvas ? '' : obj.hard || '', obj.seed, core.decodeRoute(obj.route)); - } - if (obj.version && obj.version != core.firstData.version) { - core.myconfirm("游戏版本不一致!\n你仍然想播放录像吗?", _replay); - return; - } - _replay(); - }, null, ".h5route"); -} - -////// 开始播放 ////// -control.prototype.startReplay = function (list) { - if (!core.isPlaying()) return; - core.status.replay.replaying = true; - core.status.replay.pausing = true; - core.status.replay.failed = false; - core.status.replay.speed = 1.0; - core.status.replay.toReplay = core.cloneArray(list); - core.status.replay.totalList = core.status.route.concat(list); - core.status.replay.steps = 0; - core.status.replay.save = []; - core.createCanvas('replay', 0, core.__PIXELS__ - 40, core.__PIXELS__, 40, 199); - core.setOpacity('replay', 0.6); - this._replay_drawProgress(); - core.updateStatusBar(false, true); - core.drawTip("开始播放"); - this.replay(); - core.setToolbarButton("replay"); -} - -////// 更改播放状态 ////// -control.prototype.triggerReplay = function () { - if (core.status.replay.pausing) this.resumeReplay(); - else this.pauseReplay(); -} - -////// 暂停播放 ////// -control.prototype.pauseReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - core.status.replay.pausing = true; - core.updateStatusBar(false, true); - core.drawTip("暂停播放"); -} - -////// 恢复播放 ////// -control.prototype.resumeReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - core.status.replay.pausing = false; - core.updateStatusBar(false, true); - core.drawTip("恢复播放"); - core.replay(); -} - -////// 单步播放 ////// -control.prototype.stepReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - core.replay(true); -} - -////// 加速播放 ////// -control.prototype.speedUpReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - var speeds = [0.2, 0.5, 1, 2, 3, 6, 12, 24]; - for (var i = speeds.length - 2; i >= 0; i--) { - if (speeds[i] <= core.status.replay.speed) { - core.status.replay.speed = speeds[i + 1]; - break; - } - } - core.drawTip("x" + core.status.replay.speed + "倍"); -} - -////// 减速播放 ////// -control.prototype.speedDownReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - var speeds = [0.2, 0.5, 1, 2, 3, 6, 12, 24]; - for (var i = 1; i <= speeds.length; i++) { - if (speeds[i] >= core.status.replay.speed) { - core.status.replay.speed = speeds[i - 1]; - break; - } - } - core.drawTip("x" + core.status.replay.speed + "倍"); -} - -////// 设置播放速度 ////// -control.prototype.setReplaySpeed = function (speed) { - if (!core.isPlaying() || !core.isReplaying()) return; - core.status.replay.speed = speed; - core.drawTip("x" + core.status.replay.speed + "倍"); -} - -////// 停止播放 ////// -control.prototype.stopReplay = function (force) { - if (!core.isPlaying()) return; - if (!core.isReplaying() && !force) return; - core.status.replay.toReplay = []; - core.status.replay.totalList = []; - core.status.replay.replaying = false; - core.status.replay.pausing = false; - core.status.replay.failed = false; - core.status.replay.speed = 1.0; - core.status.replay.steps = 0; - core.status.replay.save = []; - core.deleteCanvas('replay'); - core.updateStatusBar(false, true); - core.drawTip("停止播放并恢复游戏"); - core.setToolbarButton('normal'); -} - -////// 回退 ////// -control.prototype.rewindReplay = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - if (core.status.replay.save.length == 0) { - core.playSound('操作失败'); - return core.drawTip("无法再回到上一个节点"); - } - var save = core.status.replay.save, data = save.pop(); - core.loadData(data.data, function () { - core.removeFlag('__fromLoad__'); - core.status.replay = { - "replaying": true, - "pausing": true, - "animate": false, - "toReplay": data.replay.toReplay, - "totalList": data.replay.totalList, - "speed": core.status.replay.speed, - "steps": data.replay.steps, - "save": save - } - core.createCanvas('replay', 0, core.__PIXELS__ - 40, core.__PIXELS__, 40, 199); - core.setOpacity('replay', 0.6); - core.control._replay_drawProgress(); - core.updateStatusBar(false, true); - core.drawTip("成功回退到上一个节点"); - }); -} - -////// 回放时存档 ////// -control.prototype._replay_SL = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - if (core.hasFlag('__forbidSave__')) { - core.playSound('操作失败'); - return core.drawTip('当前禁止存档'); - } - this._replay_hideProgress(); - - core.lockControl(); - core.status.event.id = 'save'; - var saveIndex = core.saves.saveIndex; - var page = parseInt((saveIndex - 1) / 5), offset = saveIndex - 5 * page; - - core.ui._drawSLPanel(10 * page + offset); -} - -////// 回放时查看怪物手册 ////// -control.prototype._replay_book = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || (core.status.event.id && core.status.event.id != 'viewMaps')) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - if (!core.hasItem('book')) { - core.playSound('操作失败'); - return core.drawTip('你没有' + core.material.items['book'].name, 'book'); - } - this._replay_hideProgress(); - - // 从“浏览地图”页面打开 - if (core.status.event.id == 'viewMaps') - core.status.event.ui = core.status.event.data; - - core.lockControl(); - core.status.event.id = 'book'; - core.useItem('book', true); -} - -////// 回放录像时浏览地图 ////// -control.prototype._replay_viewMap = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - this._replay_hideProgress(); - - core.lockControl(); - core.status.event.id = 'viewMaps'; - core.ui._drawViewMaps(); -} - -control.prototype._replay_toolbox = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - this._replay_hideProgress(); - - core.lockControl(); - core.status.event.id = 'toolbox'; - core.ui._drawToolbox(); -} - -control.prototype._replay_equipbox = function () { - if (!core.isPlaying() || !core.isReplaying()) return; - if (!core.status.replay.pausing) { - core.playSound('操作失败'); - return core.drawTip("请先暂停录像"); - } - if (core.isMoving() || core.status.replay.animate || core.status.event.id) { - core.playSound('操作失败'); - return core.drawTip("请等待当前事件的处理结束"); - } - this._replay_hideProgress(); - - core.lockControl(); - core.status.event.id = 'equipbox'; - core.ui._drawEquipbox(); -} - -////// 是否正在播放录像 ////// -control.prototype.isReplaying = function () { - return (core.status.replay || {}).replaying; -} - -////// 回放 ////// -control.prototype.replay = function (force) { - if (!core.isPlaying() || !core.isReplaying() - || core.status.replay.animate || core.status.event.id || core.status.replay.failed) return; - if (core.status.replay.pausing && !force) return; - - this._replay_drawProgress(); - if (core.status.replay.toReplay.length == 0) - return this._replay_finished(); - this._replay_save(); - var action = core.status.replay.toReplay.shift(); - if (this._doReplayAction(action)) return; - this._replay_error(action); -} - -////// 注册一个录像行为 ////// -// name:自定义名称,可用于注销使用 -// func:具体执行录像的函数,可为一个函数或插件中的函数名; -// 需要接受一个action参数,代表录像回放时的下一个操作 -// func返回true代表成功处理了此录像行为,false代表没有处理此录像行为。 -control.prototype.registerReplayAction = function (name, func) { - this.unregisterReplayAction(name); - this.replayActions.push({ name: name, func: func }); -} - -////// 注销一个录像行为 ////// -control.prototype.unregisterReplayAction = function (name) { - this.replayActions = this.replayActions.filter(function (b) { return b.name != name; }); -} - -////// 执行录像行为,会在注册的函数中依次执行直到得到true为止 ////// -control.prototype._doReplayAction = function (action) { - for (var i in this.replayActions) { - try { - if (core.doFunc(this.replayActions[i].func, this, action)) return true; - } catch (e) { - console.error(e); - console.error("ERROR in replayActions[" + this.replayActions[i].name + "]:已自动注销该项。"); - core.unregisterReplayAction(this.replayActions[i].name); - } - } - return false; -} - -control.prototype._replay_finished = function () { - core.status.replay.replaying = false; - core.status.replay.failed = false; - core.status.event.selection = 0; - var str = "录像播放完毕,你想退出播放吗?"; - if (core.status.route.length != core.status.replay.totalList.length - || core.subarray(core.status.route, core.status.replay.totalList) == null) { - str = "录像播放完毕,但记录不一致。\n请检查录像播放时的二次记录问题。\n你想退出播放吗?"; - } - core.ui.drawConfirmBox(str, function () { - core.ui.closePanel(); - core.stopReplay(true); - }, function () { - core.status.replay.replaying = true; - core.ui.closePanel(); - core.pauseReplay(); - }); -} - -control.prototype._replay_save = function () { - core.status.replay.steps++; - if (core.status.replay.steps % 40 == 1) { - if (core.status.replay.save.length == 30) - core.status.replay.save.shift(); - core.status.replay.save.push({ - "data": core.saveData(), "replay": { - "totalList": core.cloneArray(core.status.replay.totalList), - "toReplay": core.cloneArray(core.status.replay.toReplay), - "steps": core.status.replay.steps - } - }); - } -} - -control.prototype._replay_error = function (action, callback) { - core.ui.closePanel(); - core.status.replay.replaying = false; - core.status.replay.failed = true; - var len = core.status.replay.toReplay.length; - var prevList = core.status.replay.totalList.slice(-len - 11, -len - 1); - var nextList = core.status.replay.toReplay.slice(0, 10); - console.log("录像文件出错,当前操作:" + action); - console.log("之前的10个操作是:\n" + prevList.toString()); - console.log("接下来10个操作是:\n" + nextList.toString()); - core.ui.drawConfirmBox("录像文件出错,你想回到上个节点吗?", function () { - core.status.replay.failed = false; - core.ui.closePanel(); - if (core.status.replay.save.length > 0) { - core.status.replay.replaying = true; - core.status.replay.pausing = true; - core.rewindReplay(); - } - else { - core.playSound('操作失败'); - core.stopReplay(true); - core.drawTip("无法回到上一个节点"); - if (callback) callback(); - } - }, function () { - core.status.replay.failed = false; - core.ui.closePanel(); - core.stopReplay(true); - if (callback) callback(); - }); -} - -control.prototype._replay_hideProgress = function () { - if (core.dymCanvas.replay) core.dymCanvas.replay.canvas.style.display = 'none'; -} - -control.prototype._replay_drawProgress = function () { - if (!core.dymCanvas.replay) return; - if (core.dymCanvas.replay.canvas.style.display == 'none') core.dymCanvas.replay.canvas.style.display = 'block'; - var total = core.status.replay.totalList.length, left = total - core.status.replay.toReplay.length; - var content = '播放进度:' + left + ' / ' + total + '(' + (left / total * 100).toFixed(2) + '%)'; - var width = 26 + core.calWidth('replay', content, "16px Arial"); - core.clearMap('replay'); - core.fillRect('replay', 0, 0, width, 40, '#000000'); - core.fillText('replay', content, 16, 27, '#FFFFFF'); -} - -control.prototype.__replay_getTimeout = function () { - if (core.status.replay.speed == 24) return 0; - return 100 / Math.max(1, core.status.replay.speed); -} - -control.prototype._replayAction_move = function (action) { - if (["up", "down", "left", "right"].indexOf(action) < 0) return false; - core.moveHero(action, core.replay); - return true; -} - -control.prototype._replayAction_item = function (action) { - if (action.indexOf("item:") != 0) return false; - var itemId = action.substring(5); - if (!core.canUseItem(itemId)) return false; - if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) { - core.useItem(itemId, false, core.replay); - return true; - } - var tools = core.getToolboxItems('tools'), - constants = core.getToolboxItems('constants'); - var index, per = core.__SIZE__ - 1; - if ((index = tools.indexOf(itemId)) >= 0) { - core.status.event.data = { "toolsPage": Math.floor(index / per) + 1, "constantsPage": 1 }; - index = index % per; - } - else if ((index = constants.indexOf(itemId)) >= 0) { - core.status.event.data = { "toolsPage": 1, "constantsPage": Math.floor(index / per) + 1 }; - index = index % per + per; - } - if (index < 0) return false; - core.ui._drawToolbox(index); - setTimeout(function () { - core.ui.closePanel(); - core.useItem(itemId, false, core.replay); - }, core.control.__replay_getTimeout()); - return true; -} - -control.prototype._replayAction_equip = function (action) { - if (action.indexOf("equip:") != 0) return false; - var equipId = action.substring(6); - var ownEquipment = core.getToolboxItems('equips'); - var index = ownEquipment.indexOf(equipId), per = core.__SIZE__ - 1; - if (index < 0) { - core.removeFlag('__doNotCheckAutoEvents__'); - return false; - } - - var cb = function () { - var next = core.status.replay.toReplay[0] || ""; - if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { - core.removeFlag('__doNotCheckAutoEvents__'); - core.checkAutoEvents(); - } - core.replay(); - } - core.setFlag('__doNotCheckAutoEvents__', true); - - core.status.route.push(action); - if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) { - core.loadEquip(equipId, cb); - return true; - } - core.status.event.data = { "page": Math.floor(index / per) + 1, "selectId": null }; - index = index % per + per; - core.ui._drawEquipbox(index); - setTimeout(function () { - core.ui.closePanel(); - core.loadEquip(equipId, cb); - }, core.control.__replay_getTimeout()); - return true; -} - -control.prototype._replayAction_unEquip = function (action) { - if (action.indexOf("unEquip:") != 0) return false; - var equipType = parseInt(action.substring(8)); - if (!core.isset(equipType)) { - core.removeFlag('__doNotCheckAutoEvents__'); - return false; - } - - var cb = function () { - var next = core.status.replay.toReplay[0] || ""; - if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { - core.removeFlag('__doNotCheckAutoEvents__'); - core.checkAutoEvents(); - } - core.replay(); - } - core.setFlag('__doNotCheckAutoEvents__', true); - - core.ui._drawEquipbox(equipType); - core.status.route.push(action); - if (core.status.replay.speed == 24) { - core.unloadEquip(equipType, cb); - return true; - } - setTimeout(function () { - core.ui.closePanel(); - core.unloadEquip(equipType, cb); - }, core.control.__replay_getTimeout()); - return true; -} - -control.prototype._replayAction_saveEquip = function (action) { - if (action.indexOf('saveEquip:') != 0) return false; - core.quickSaveEquip(parseInt(action.substring(10))); - core.replay(); - return true; -} - -control.prototype._replayAction_loadEquip = function (action) { - if (action.indexOf('loadEquip:') != 0) return false; - core.quickLoadEquip(parseInt(action.substring(10))); - core.replay(); - return true; -} - -control.prototype._replayAction_fly = function (action) { - if (action.indexOf("fly:") != 0) return false; - var floorId = action.substring(4); - var toIndex = core.floorIds.indexOf(floorId); - if (!core.canUseItem('fly') || (!core.maps.canUseFlyHere())) return false; - core.ui.drawFly(toIndex); - if (core.status.replay.speed == 24) { - if (!core.flyTo(floorId, core.replay)) - core.control._replay_error(action); - return true; - } - setTimeout(function () { - if (!core.flyTo(floorId, core.replay)) - core.control._replay_error(action); - }, core.control.__replay_getTimeout()); - return true; -} - -control.prototype._replayAction_shop = function (action) { - if (action.indexOf("shop:") != 0) return false; - var shopId = action.substring(5); - if (core.canUseQuickShop(shopId) != null || !core.canOpenShop(shopId)) { - this._replay_error(shopId); - return true; - } - core.openShop(shopId, false); - core.replay(); - return true; -} - -control.prototype._replayAction_turn = function (action) { - if (action != 'turn' && action.indexOf('turn:') != 0) return false; - if (action == 'turn') core.turnHero(); - else core.turnHero(action.substring(5)); - core.replay(); - return true; -} - -control.prototype._replayAction_getNext = function (action) { - if (action != "getNext") return false; - core.getNextItem(); - core.replay(); - return true; -} - -control.prototype._replayAction_moveDirectly = function (action) { - if (action.indexOf("move:") != 0) return false; - - var pos = action.substring(5).split(":"); - var x = parseInt(pos[0]), y = parseInt(pos[1]); - var nowx = core.getHeroLoc('x'), nowy = core.getHeroLoc('y'); - var ignoreSteps = core.canMoveDirectly(x, y); - if (!core.moveDirectly(x, y, ignoreSteps)) return false; - if (core.status.replay.speed == 24) { - core.replay(); - return true; - } - - core.ui.drawArrow('ui', 32 * nowx + 16 - core.bigmap.offsetX, 32 * nowy + 16 - core.bigmap.offsetY, - 32 * x + 16 - core.bigmap.offsetX, 32 * y + 16 - core.bigmap.offsetY, '#FF0000', 3); - var timeout = this.__replay_getTimeout(); - if (ignoreSteps < 10) timeout = timeout * ignoreSteps / 10; - setTimeout(function () { - core.clearMap('ui'); - core.replay(); - }, timeout); - return true; -} - -control.prototype._replayAction_key = function (action) { - if (action.indexOf("key:") != 0) return false; - core.actions.keyUp(parseInt(action.substring(4)), false, true); - core.replay(); - return true; -} - -control.prototype._replayAction_click = function (action) { - if (action.indexOf("click:") != 0) return false; - var p = action.split(":"); - if (p.length != 4) return false; - core.actions.doRegisteredAction("onStatusBarClick", parseInt(p[2]), parseInt(p[3]), parseInt(p[1])); - core.replay(); - return true; -} - -control.prototype._replayAction_ignoreInput = function (action) { - if (action.indexOf('input:') == 0 || action.indexOf('input2:') == 0 || action.indexOf('choices:') == 0 || action.indexOf('random:') == 0) { - console.warn('警告!录像播放中出现了未知的 ' + action + '!'); - core.replay(); - return true; - } - return false; -} - -control.prototype._replayAction_no = function (action) { - if (action != 'no') return false; - core.status.route.push(action); - core.replay(); - return true; -} - -// ------ 存读档相关 ------ // - -////// 自动存档 ////// -control.prototype.autosave = function (removeLast) { - if (core.hasFlag('__forbidSave__')) return; - core.saves.actionCount++; - var x = null; - if (removeLast) { - x = core.status.route.pop(); - core.status.route.push("turn:" + core.getHeroLoc('direction')); - } - if (core.status.event.id == 'action') // 事件中的自动存档 - core.setFlag("__events__", core.clone(core.status.event.data)); - if (core.saves.autosave.data == null) { - core.saves.autosave.data = []; - } - core.saves.autosave.data.splice(core.saves.autosave.now, 0, core.saveData()); - core.saves.autosave.now += 1; - if (core.saves.autosave.data.length > core.saves.autosave.max) { - if (core.saves.autosave.now < core.saves.autosave.max / 2) - core.saves.autosave.data.pop(); - else { - core.saves.autosave.data.shift(); - core.saves.autosave.now = core.saves.autosave.now - 1; - } - } - core.saves.autosave.updated = true; - core.saves.ids[0] = true; - core.removeFlag("__events__"); - if (removeLast) { - core.status.route.pop(); - if (x) core.status.route.push(x); - } -} - -/////// 实际进行自动存档 ////// -control.prototype.checkAutosave = function () { - if (!core.animateFrame || !core.saves || !core.saves.autosave) return; - core.setLocalStorage('totalTime', core.animateFrame.totalTime); - var autosave = core.saves.autosave; - if (autosave.data == null || !autosave.updated || !autosave.storage) return; - autosave.updated = false; - if (autosave.data.length >= 1) { - core.setLocalForage("autoSave", autosave.data[autosave.now - 1]); - } -} - -////// 实际进行存读档事件 ////// -control.prototype.doSL = function (id, type) { - switch (type) { - case 'save': this._doSL_save(id); break; - case 'load': this._doSL_load(id, this._doSL_load_afterGet); break; - case 'reload': this._doSL_reload(id, this._doSL_load_afterGet); break; - case 'replayLoad': this._doSL_load(id, this._doSL_replayLoad_afterGet); break; - case 'replayRemain': this._doSL_load(id, this._doSL_replayRemain_afterGet); break; - case 'replaySince': this._doSL_load(id, this._doSL_replaySince_afterGet); break; - } -} - -control.prototype._doSL_save = function (id) { - if (id == 'autoSave') { - core.playSound('操作失败'); - return core.drawTip('不能覆盖自动存档!'); - } - // 在事件中的存档 - if (core.status.event.interval != null) - core.setFlag("__events__", core.status.event.interval); - var data = core.saveData(); - if (core.isReplaying() && core.status.replay.toReplay.length > 0) { - data.__toReplay__ = core.encodeRoute(core.status.replay.toReplay); - } - core.setLocalForage("save" + id, data, function () { - core.saves.saveIndex = id; - core.saves.actionCount = 0; - core.setLocalStorage('saveIndex', core.saves.saveIndex); - // 恢复事件 - if (!core.events.recoverEvents(core.status.event.interval)) - core.ui.closePanel(); - core.playSound('存档'); - core.drawTip('存档成功!'); - }, function (err) { - console.error(err); - alert("存档失败,错误信息:\n" + err); - }); - core.removeFlag("__events__"); - return; -} - -control.prototype._doSL_load = function (id, callback) { - const autosave = core.saves.autosave; - if (id == 'autoSave' && autosave.data != null) { - let tip = '读档成功'; - autosave.now -= 1; - var data = autosave.data.splice(autosave.now, 1)[0]; - if (core.isPlaying() && !core.status.gameOver) { - core.control.autosave(0); - autosave.now -= 1; - } - if (autosave.now == 0) { - autosave.data.unshift(core.clone(data)); - autosave.now += 1; - } - callback(id, data); - } - else { - core.getLocalForage(id == 'autoSave' ? id : "save" + id, null, function (data) { - if (id == 'autoSave' && data != null) { - autosave.data = data; - if (!(autosave.data instanceof Array)) { - autosave.data = [autosave.data]; - } - autosave.now = autosave.data.length; - return core.control._doSL_load(id, callback); - } - callback(id, data); - }, function (err) { - console.error(err); - alert("无效的存档"); - }) - } - return; -} - -control.prototype._doSL_reload = function (id, callback) { - if (core.saves.autosave.data != null && core.saves.autosave.now < core.saves.autosave.data.length) { - var data = core.saves.autosave.data.splice(core.saves.autosave.now, 1)[0]; - core.control.autosave(false); - callback(id, data); - } - else core.drawFailTip('当前不能再继续向后回溯自动存档'); - return; -} - -control.prototype._doSL_load_afterGet = function (id, data) { - if (!data) return alert("无效的存档"); - var _replay = function () { - core.startGame(data.hard, data.hero.flags.__seed__, core.decodeRoute(data.route)); - }; - if (data.version != core.firstData.version) { - core.myconfirm("存档版本不匹配!\n你想回放此存档的录像吗?\n可以随时停止录像播放以继续游戏。", _replay); - return; - } - if (data.hero.flags.__events__ && data.guid != core.getGuid()) { - core.myconfirm("此存档可能存在风险,你想要播放录像么?", _replay); - return; - } - core.ui.closePanel(); - core.loadData(data, function () { - core.removeFlag('__fromLoad__'); - core.drawTip("读档成功"); - if (id != "autoSave") { - core.saves.saveIndex = id; - core.saves.actionCount = 0; - core.setLocalStorage('saveIndex', core.saves.saveIndex); - } - else core.saves.actionCount = 1; - }); -} - -control.prototype._doSL_replayLoad_afterGet = function (id, data) { - if (!data) { - core.playSound('操作失败'); - return core.drawTip("无效的存档"); - } - if (data.version != core.firstData.version) { - core.playSound('操作失败'); - return core.drawTip("存档版本不匹配"); - } - if (data.hero.flags.__events__ && data.guid != core.getGuid()) { - core.playSound('操作失败'); - return core.drawTip("此存档可能存在风险,无法读档"); - } - var route = core.subarray(core.status.route, core.decodeRoute(data.route)); - if (route == null) { - core.playSound('操作失败'); - return core.drawTip("无法从此存档回放录像"); - } - core.loadData(data, function () { - core.removeFlag('__fromLoad__'); - core.startReplay(route); - core.drawTip("回退到存档节点"); - }); -} - -control.prototype._doSL_replayRemain_afterGet = function (id, data) { - if (!data) { - core.playSound('操作失败'); - return core.drawTip("无效的存档"); - } - var route = core.decodeRoute(data.route); - if (core.status.tempRoute) { - var remainRoute = core.subarray(route, core.status.tempRoute); - if (remainRoute == null) - return alert("无法接续播放录像!\n该存档必须是前一个选择的存档的后续内容。"); - delete core.status.tempRoute; - core.ui.closePanel(); - core.startReplay(remainRoute); - core.drawTip("接续播放录像"); - return; - } - else if (data.floorId != core.status.floorId || data.hero.loc.x != core.getHeroLoc('x') || data.hero.loc.y != core.getHeroLoc('y')) - return alert("楼层或坐标不一致!"); - - core.status.tempRoute = route; - core.ui.closePanel(); - core.drawText("\t[步骤2]请选择第二个存档。\n\r[yellow]该存档必须是前一个存档的后续。\r\n将尝试播放到此存档。", function () { - core.status.event.id = 'replayRemain'; - core.lockControl(); - var saveIndex = core.saves.saveIndex; - var page = parseInt((saveIndex - 1) / 5), offset = saveIndex - 5 * page; - core.ui._drawSLPanel(10 * page + offset); - }); -} - -control.prototype._doSL_replaySince_afterGet = function (id, data) { - if (data.floorId != core.status.floorId || data.hero.loc.x != core.getHeroLoc('x') || data.hero.loc.y != core.getHeroLoc('y')) - return alert("楼层或坐标不一致!"); - if (!data.__toReplay__) return alert('该存档没有剩余录像!'); - core.ui.closePanel(); - core.startReplay(core.decodeRoute(data.__toReplay__)); - core.drawTip("播放存档剩余录像"); - return; -} - -////// 同步存档到服务器 ////// -control.prototype.syncSave = function (type) { - core.ui.drawWaiting("正在同步,请稍候..."); - if (type == 'all') { - const callback = function (saves) { - core.control._syncSave_http(type, saves); - }; - core.ui.drawConfirmBox("您正在试图同步您的\r[Salmon]所有\r存档,\n这可能导致接收方已有的存档被\r[Salmon]覆盖\r。\n确定要这么做?", function () { - core.getAllSaves(callback); - }, () => { core.ui.closePanel(); }); - } - else { // 这里的检测逻辑:每存读档一次actionCount置0, 若读的自动档, actionCount置为1, 每autosave一次actionCount+1。若actionCount不为0就会触发检测。 - const index = core.saves.saveIndex; - if (!index) return core.drawText("没有要同步的存档"); - const callback = function (save) { - const autosave = core.saves.autosave; - if (autosave.data instanceof Array && core.saves.actionCount > 0) { - const ctx = core.createCanvas('syncSave', core.__PIXELS__ / 2 - 52, 18, 104, 104, 141); - core.strokeRect(ctx, 0, 0, 104, 104, 'yellow', 2); - const map = core.maps.loadMap(save.maps, save.floorId); - core.drawThumbnail(save.floorId, map.blocks, { - heroLoc: save.hero.loc, heroIcon: save.hero.image, flags: save.hero.flags, - ctx: ctx, x: 2, y: 2, size: 100, noHD: true, - }); - core.ui.drawConfirmBox("您正试图同步" + index + "号存档,确定是这个存档吗?\n如果不是,请返回并\r[yellow]保存当前\r状态再同步.", function () { - core.control._syncSave_http(type, save); - core.deleteCanvas("syncSave"); - }, () => { - core.ui.closePanel(); - core.deleteCanvas("syncSave"); - }); - } - else core.control._syncSave_http(type, save); - } - core.getSave(index, callback); - } -} - -control.prototype._syncSave_http = function (type, saves) { - if (!saves) return core.drawText("没有要同步的存档"); - var formData = new FormData(); - formData.append('type', 'save'); - formData.append('name', core.firstData.name); - formData.append('data', LZString.compressToBase64(JSON.stringify(saves))); - formData.append('shorten', '1'); - - core.http("POST", "/games/sync.php", formData, function (data) { - var response = JSON.parse(data); - if (response.code < 0) { - core.drawText("出错啦!\n无法同步存档到服务器。\n错误原因:" + response.msg); - } - else { - core.drawText((type == 'all' ? "所有存档" : "存档" + core.saves.saveIndex) + "同步成功!\n\n您的存档编号+密码: \r[yellow]" - + response.code + response.msg - + "\r\n\n请牢记以上信息(如截图等),在从服务器\n同步存档时使用。") - } - }, function (e) { - core.drawText("出错啦!\n无法同步存档到服务器。\n错误原因:" + e); - }) -} - -////// 从服务器加载存档 ////// -control.prototype.syncLoad = function () { - core.myprompt("请输入存档编号+密码", null, function (idpassword) { - if (!idpassword) return core.ui._drawSyncSave(); - if (!/^\d{6}\w{4}$/.test(idpassword) && !/^\d{4}\w{3}$/.test(idpassword)) { - core.drawText("不合法的存档编号+密码!"); - return; - } - core.ui.drawWaiting("正在同步,请稍候..."); - if (idpassword.length == 7) { - core.control._syncLoad_http(idpassword.substring(0, 4), idpassword.substring(4)); - } else { - core.control._syncLoad_http(idpassword.substring(0, 6), idpassword.substring(6)); - } - }, () => { }); -} - -control.prototype._syncLoad_http = function (id, password) { - var formData = new FormData(); - formData.append('type', 'load'); - formData.append('name', core.firstData.name); - formData.append('id', id); - formData.append('password', password); - - core.http("POST", "/games/sync.php", formData, function (data) { - var response = JSON.parse(data); - if (response.code == 0) { - var msg = null; - try { - msg = JSON.parse(LZString.decompressFromBase64(response.msg)); - } catch (e) { } - if (!msg) { - try { - msg = JSON.parse(response.msg); - } catch (e) { } - } - if (msg) { - core.control._syncLoad_write(msg); - } else { - core.drawText("出错啦!\n存档解析失败!"); - } - } - else { - core.drawText("出错啦!\n无法从服务器同步存档。\n错误原因:" + response.msg); - } - }, function (e) { - core.drawText("出错啦!\n无法从服务器同步存档。\n错误原因:" + e); - }); -} - -control.prototype._syncLoad_write = function (data) { - if (data instanceof Array) { - core.status.event.selection = 1; - core.ui.drawConfirmBox("所有本地存档都将被覆盖,确认?", function () { - for (var i = 1; i <= 5 * (main.savePages || 30); i++) { - if (i <= data.length) - core.setLocalForage("save" + i, data[i - 1]); - else if (core.saves.ids[i]) - core.removeLocalForage("save" + i); - } - core.ui.closePanel(); - core.drawText("同步成功!\n你的本地所有存档均已被覆盖。"); - }, function () { - core.status.event.selection = 0; - core.ui._drawSyncSave(); - }); - } - else { - // 只覆盖单存档 - core.setLocalForage("save" + core.saves.saveIndex, data, function () { - core.drawText("同步成功!\n单存档已覆盖至存档" + core.saves.saveIndex); - }); - } -} - -////// 存档到本地 ////// -control.prototype.saveData = function () { - return this.controldata.saveData(); -} - -////// 从本地读档 ////// -control.prototype.loadData = function (data, callback) { - return this.controldata.loadData(data, callback); -} - -control.prototype.getSave = function (index, callback) { - if (index == 0) { - // --- 自动存档先从缓存中获取 - if (core.saves.autosave.data != null) - callback(core.saves.autosave.data); - else { - core.getLocalForage("autoSave", null, function (data) { - if (data != null) { - core.saves.autosave.data = data; - if (!(core.saves.autosave.data instanceof Array)) { - core.saves.autosave.data = [core.saves.autosave.data]; - } - core.saves.autosave.now = core.saves.autosave.data.length; - } - callback(core.saves.autosave.data); - }, function (err) { - console.error(err); - callback(null); - }); - } - return; - } - core.getLocalForage("save" + index, null, function (data) { - if (callback) callback(data); - }, function (err) { - console.error(err); - if (callback) callback(null); - }); -} - -control.prototype.getSaves = function (ids, callback) { - if (!(ids instanceof Array)) return this.getSave(ids, callback); - var count = ids.length, data = {}; - for (var i = 0; i < ids.length; ++i) { - (function (i) { - core.getSave(ids[i], function (result) { - data[i] = result; - if (Object.keys(data).length == count) - callback(data); - }) - })(i); - } -} - -control.prototype.getAllSaves = function (callback) { - var ids = Object.keys(core.saves.ids).filter(function (x) { return x != 0; }) - .sort(function (a, b) { return a - b; }), saves = []; - this.getSaves(ids, function (data) { - for (var i = 0; i < ids.length; ++i) { - if (data[i] != null) - saves.push(data[i]); - } - callback(saves); - }); -} - -////// 获得所有存在存档的存档位 ////// -control.prototype.getSaveIndexes = function (callback) { - var indexes = {}; - core.keysLocalForage(function (err, keys) { - if (err) { - console.error(err); - return callback(indexes); - } - keys.forEach(function (key) { - core.control._getSaveIndexes_getIndex(indexes, key); - }); - callback(indexes); - }); -} - -control.prototype._getSaveIndexes_getIndex = function (indexes, name) { - var e = new RegExp('^' + core.firstData.name + "_(save\\d+|autoSave)$").exec(name); - if (e) { - if (e[1] == 'autoSave') indexes[0] = true; - else indexes[parseInt(e[1].substring(4))] = true; - } -} - -////// 判断某个存档位是否存在存档 ////// -control.prototype.hasSave = function (index) { - return core.saves.ids[index] || false; -} - -////// 删除某个存档 -control.prototype.removeSave = function (index, callback) { - if (index == 0 || index == "autoSave") { - index = "autoSave"; - core.removeLocalForage(index, function () { - core.saves.autosave.data = null; - core.saves.autosave.updated = false; - if (callback) callback(); - }); - return; - } - core.removeLocalForage("save" + index, function () { - core.saves.favorite = core.saves.favorite.filter(function (i) { return core.hasSave(i); }); - delete core.saves.favoriteName[index]; - core.control._updateFavoriteSaves(); - if (callback) callback(); - }, function () { - core.playSound('操作失败'); - core.drawTip("无法删除存档!"); - if (callback) callback(); - }); -} - -////// 读取收藏信息 -control.prototype._loadFavoriteSaves = function () { - core.saves.favorite = core.getLocalStorage("favorite", []); - // --- 移除不存在的收藏 - core.saves.favorite = core.saves.favorite.filter(function (i) { return core.hasSave(i); }); - core.saves.favoriteName = core.getLocalStorage("favoriteName", {}); -} - -control.prototype._updateFavoriteSaves = function () { - core.setLocalStorage("favorite", core.saves.favorite); - core.setLocalStorage("favoriteName", core.saves.favoriteName); -} - -// ------ 属性,状态,位置,buff,变量,锁定控制等 ------ // - -////// 设置勇士属性 ////// -control.prototype.setStatus = function (name, value) { - if (!core.status.hero) return; - if (name == 'x' || name == 'y' || name == 'direction') - this.setHeroLoc(name, value); - else - core.status.hero[name] = value; -} - -////// 增减勇士属性 ////// -control.prototype.addStatus = function (name, value) { - this.setStatus(name, this.getStatus(name) + value); -} - -////// 获得勇士属性 ////// -control.prototype.getStatus = function (name) { - if (!core.status.hero) return null; - if (name == 'x' || name == 'y' || name == 'direction') - return this.getHeroLoc(name); - if (main.mode == 'editor' && !core.hasFlag('__statistics__')) { - return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero[name]; - } - return core.status.hero[name]; -} - -////// 从status中获得属性,如果不存在则从勇士属性中获取 ////// -control.prototype.getStatusOrDefault = function (status, name) { - if (status && name in status) - return Math.floor(status[name]); - return Math.floor(this.getStatus(name)); -} - -////// 获得勇士实际属性(增幅后的) ////// -control.prototype.getRealStatus = function (name) { - return this.getRealStatusOrDefault(null, name); -} - -////// 从status中获得实际属性(增幅后的),如果不存在则从勇士属性中获取 ////// -control.prototype.getRealStatusOrDefault = function (status, name) { - return Math.floor(this.getStatusOrDefault(status, name) * this.getBuff(name)); -} - -////// 获得勇士原始属性(无装备和衰弱影响) ////// -control.prototype.getNakedStatus = function (name) { - var value = this.getStatus(name); - if (value == null) return value; - // 装备增幅 - core.status.hero.equipment.forEach(function (v) { - if (!v || !(core.material.items[v] || {}).equip) return; - value -= core.material.items[v].equip.value[name] || 0; - }); - // 衰弱扣除 - if (core.hasFlag('weak') && core.values.weakValue >= 1 && (name == 'atk' || name == 'def')) { - value += core.values.weakValue; - } - return value; -} - -////// 获得某个属性的名字 ////// -control.prototype.getStatusLabel = function (name) { - if (this.controldata.getStatusLabel) { - return this.controldata.getStatusLabel(name) || name; - } - return { - name: "名称", lv: "等级", hpmax: "生命上限", hp: "生命", manamax: "魔力上限", mana: "魔力", - atk: "攻击", def: "防御", mdef: "护盾", money: "金币", exp: "经验", point: "加点", steps: "步数", - special: "特殊属性", - }[name] || name; -} - -////// 设置某个属性的增幅值 ////// -control.prototype.setBuff = function (name, value) { - // 仅保留三位有效buff值 - value = parseFloat(value.toFixed(3)); - this.setFlag('__' + name + '_buff__', value); -} - -////// 加减某个属性的增幅值 ////// -control.prototype.addBuff = function (name, value) { - var buff = this.getBuff(name) + value; - // 仅保留三位有效buff值 - buff = parseFloat(buff.toFixed(3)); - this.setFlag('__' + name + '_buff__', buff); -} - -////// 获得某个属性的增幅值 ////// -control.prototype.getBuff = function (name) { - return core.getFlag('__' + name + '_buff__', 1); -} - -////// 获得或移除毒衰咒效果 ////// -control.prototype.triggerDebuff = function (action, type) { - return this.controldata.triggerDebuff(action, type); -} - -////// 设置勇士的位置 ////// -control.prototype.setHeroLoc = function (name, value, noGather) { - if (!core.status.hero) return; - core.status.hero.loc[name] = value; - if ((name == 'x' || name == 'y') && !noGather) { - this.gatherFollowers(); - } -} - -////// 获得勇士的位置 ////// -control.prototype.getHeroLoc = function (name) { - if (!core.status.hero) return; - if (main.mode == 'editor') { - if (name == null) return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero.loc; - return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero.loc[name]; - } - if (name == null) return core.status.hero.loc; - return core.status.hero.loc[name]; -} - -////// 获得某个等级的名称 ////// -control.prototype.getLvName = function (lv) { - if (!core.status.hero) return null; - if (lv == null) lv = core.status.hero.lv; - return ((core.firstData.levelUp || [])[lv - 1] || {}).title || lv; -} - -////// 获得下个等级所需经验;如果不存在下个等级,返回null。 ////// -control.prototype.getNextLvUpNeed = function () { - if (!core.status.hero) return null; - if (core.status.hero.lv >= core.firstData.levelUp.length) return null; - var need = core.calValue(core.firstData.levelUp[core.status.hero.lv].need); - if (core.flags.statusBarItems.indexOf('levelUpLeftMode') >= 0) - return Math.max(need - core.getStatus('exp'), 0); - else return need; -} - -////// 设置某个自定义变量或flag ////// -control.prototype.setFlag = function (name, value) { - if (value == null) return this.removeFlag(name); - if (!core.status.hero) return; - core.status.hero.flags[name] = value; -} - -////// 增加某个flag数值 ////// -control.prototype.addFlag = function (name, value) { - if (!core.status.hero) return; - core.setFlag(name, core.getFlag(name, 0) + value); -} - -////// 获得某个自定义变量或flag ////// -control.prototype.getFlag = function (name, defaultValue) { - if (!core.status.hero) return defaultValue; - var value = core.status.hero.flags[name]; - return value != null ? value : defaultValue; -} - -////// 是否存在某个自定义变量或flag,且值为true ////// -control.prototype.hasFlag = function (name) { - return !!core.getFlag(name); -} - -////// 删除某个自定义变量或flag ////// -control.prototype.removeFlag = function (name) { - if (!core.status.hero) return; - delete core.status.hero.flags[name]; -} - -////// 获得某个点的独立开关 ////// -control.prototype.getSwitch = function (x, y, floorId, name, defaultValue) { - var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); - return this.getFlag(prefix + "@" + name, defaultValue); -} - -////// 设置某个点的独立开关 ////// -control.prototype.setSwitch = function (x, y, floorId, name, value) { - var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); - return this.setFlag(prefix + "@" + name, value); -} - -////// 增加某个点的独立开关 ////// -control.prototype.addSwitch = function (x, y, floorId, name, value) { - var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); - return this.addFlag(prefix + "@" + name, value); -} - -////// 判定某个点的独立开关 ////// -control.prototype.hasSwitch = function (x, y, floorId, name) { - var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); - return this.hasFlag(prefix + "@" + name); -} - -////// 删除某个点的独立开关 ////// -control.prototype.removeSwitch = function (x, y, floorId, name) { - var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); - return this.removeFlag(prefix + "@" + name); -} - -////// 锁定状态栏,常常用于事件处理 ////// -control.prototype.lockControl = function () { - core.status.lockControl = true; -} - -////// 解锁状态栏 ////// -control.prototype.unlockControl = function () { - core.status.lockControl = false; -} - -////// 开启debug模式 ////// -control.prototype.debug = function () { - core.setFlag('debug', true); - core.drawText("\t[调试模式开启]此模式下按住Ctrl键(或Ctrl+Shift键)可以穿墙并忽略一切事件。\n此模式下将无法上传成绩。"); -} - -control.prototype._bindRoutePush = function () { - core.status.route.push = function (element) { - // 忽视移动、转向、瞬移 - if (["up", "down", "left", "right", "turn"].indexOf(element) < 0 && !element.startsWith("move:")) { - core.clearRouteFolding(); - } - Array.prototype.push.call(core.status.route, element); - } -} - -////// 清除录像折叠信息 ////// -control.prototype.clearRouteFolding = function () { - core.status.routeFolding = {}; -} - -////// 检查录像折叠 ////// -control.prototype.checkRouteFolding = function () { - // 未开启、未开始游戏、录像播放中、正在事件中:不执行 - if (!core.flags.enableRouteFolding || !core.isPlaying() || core.isReplaying() || core.status.event.id) { - return this.clearRouteFolding(); - } - var hero = core.clone(core.status.hero, function (name, value) { - return name != 'steps' && typeof value == 'number'; - }); - var index = [core.getHeroLoc('x'), core.getHeroLoc('y'), core.getHeroLoc('direction').charAt(0)].join(','); - core.status.routeFolding = core.status.routeFolding || {}; - if (core.status.routeFolding[index]) { - var one = core.status.routeFolding[index]; - if (core.same(one.hero, hero) && one.length < core.status.route.length) { - Object.keys(core.status.routeFolding).forEach(function (v) { - if (core.status.routeFolding[v].length >= one.length) delete core.status.routeFolding[v]; - }); - core.status.route = core.status.route.slice(0, one.length); - this._bindRoutePush(); - } - } - core.status.routeFolding[index] = { hero: hero, length: core.status.route.length }; -} - -// ------ 天气,色调,BGM ------ // - -control.prototype.getMappedName = function (name) { - return core.getFlag('__nameMap__', {})[name] || (main.nameMap || {})[name] || name; -} - -////// 更改天气效果 ////// -control.prototype.setWeather = function (type, level) { - // 非雨雪 - if (type == null || !this.weathers[type]) { - core.deleteCanvas('weather') - core.animateFrame.weather.type = null; - core.animateFrame.weather.nodes = []; - return; - } - if (level == null) level = core.animateFrame.weather.level; - level = core.clamp(parseInt(level) || 5, 1, 10); - // 当前天气:则忽略 - if (type == core.animateFrame.weather.type && level == core.animateFrame.weather.level) return; - - // 计算当前的宽高 - core.createCanvas('weather', 0, 0, core.__PIXELS__, core.__PIXELS__, 80); - core.setOpacity('weather', 1.0); - core.animateFrame.weather.type = type; - core.animateFrame.weather.level = level; - core.animateFrame.weather.nodes = []; - try { - core.doFunc(this.weathers[type].initFunc, this, level); - } catch (e) { - console.error(e); - console.error("ERROR in weather[" + type + "]:已自动注销该项。"); - core.unregisterWeather(type); - } -} - -////// 注册一个天气 ////// -// name为天气类型,如 sun, rain, snow 等 -// initFunc 为设置为此天气时的初始化,接受level参数 -// frameFunc 为该天气下每帧的效果,接受和timestamp参数(从页面加载完毕到当前经过的时间) -control.prototype.registerWeather = function (name, initFunc, frameFunc) { - this.unregisterWeather(name); - this.weathers[name] = { initFunc: initFunc, frameFunc: frameFunc }; -} - -////// 取消注册一个天气 ////// -control.prototype.unregisterWeather = function (name) { - delete this.weathers[name]; - if (core.animateFrame.weather.type == name) { - this.setWeather(null); - } -} - -control.prototype._weather_rain = function (level) { - var number = level * parseInt(20 * core.bigmap.width * core.bigmap.height / (core.__SIZE__ * core.__SIZE__)); - for (var a = 0; a < number; a++) { - core.animateFrame.weather.nodes.push({ - 'x': Math.random() * core.bigmap.width * 32, - 'y': Math.random() * core.bigmap.height * 32, - 'l': Math.random() * 2.5, - 'xs': -4 + Math.random() * 4 + 2, - 'ys': Math.random() * 10 + 10 - }) - } -} - -control.prototype._weather_snow = function (level) { - var number = level * parseInt(20 * core.bigmap.width * core.bigmap.height / (core.__SIZE__ * core.__SIZE__)); - for (var a = 0; a < number; a++) { - core.animateFrame.weather.nodes.push({ - 'x': Math.random() * core.bigmap.width * 32, - 'y': Math.random() * core.bigmap.height * 32, - 'r': Math.random() * 5 + 1, - 'd': Math.random() * Math.min(level, 200), - }) - } -} - -control.prototype._weather_fog = function (level) { - if (!core.animateFrame.weather.fog) return; - core.animateFrame.weather.nodes = [{ - 'image': core.animateFrame.weather.fog, - 'level': 40 * level, - 'x': 0, - 'y': -core.__PIXELS__ / 2, - 'dx': -Math.random() * 1.5, - 'dy': Math.random(), - 'delta': 0.001, - }]; -} - -control.prototype._weather_cloud = function (level) { - if (!core.animateFrame.weather.cloud) return; - core.animateFrame.weather.nodes = [{ - 'image': core.animateFrame.weather.cloud, - 'level': 40 * level, - 'x': 0, - 'y': -core.__PIXELS__ / 2, - 'dx': -Math.random() * 1.5, - 'dy': Math.random(), - 'delta': 0.001, - }]; -} - -control.prototype._weather_sun = function (level) { - if (!core.animateFrame.weather.sun) return; - // 直接绘制 - core.clearMap('weather'); - core.drawImage('weather', core.animateFrame.weather.sun, 0, 0, core.animateFrame.weather.sun.width, core.animateFrame.weather.sun.height, 0, 0, core.__PIXELS__, core.__PIXELS__); - core.setOpacity('weather', level / 10); - core.animateFrame.weather.nodes = [{ opacity: level / 10, delta: 0.01 }]; -} - -////// 更改画面色调 ////// -control.prototype.setCurtain = function (color, time, moveMode, callback) { - if (time == null) time = 750; - if (time <= 0) time = 0; - if (!core.status.curtainColor) - core.status.curtainColor = [0, 0, 0, 0]; - if (!color) color = [0, 0, 0, 0]; - if (color[3] == null) color[3] = 1; - color[3] = core.clamp(color[3], 0, 1); - - if (time == 0) { - // 直接变色 - core.clearMap('curtain'); - core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(color)); - core.status.curtainColor = color; - if (callback) callback(); - return; - } - - this._setCurtain_animate(core.status.curtainColor, color, time, moveMode, callback); -} - -control.prototype._setCurtain_animate = function (nowColor, color, time, moveMode, callback) { - time /= Math.max(core.status.replay.speed, 1) - var per_time = 10, step = 0, steps = parseInt(time / per_time); - if (steps <= 0) steps = 1; - var curr = nowColor; - var moveFunc = core.applyEasing(moveMode); - - var cb = function () { - core.status.curtainColor = curr; - if (callback) callback(); - } - var animate = setInterval(function () { - step++; - curr = [ - nowColor[0] + (color[0] - nowColor[0]) * moveFunc(step / steps), - nowColor[1] + (color[1] - nowColor[1]) * moveFunc(step / steps), - nowColor[2] + (color[2] - nowColor[2]) * moveFunc(step / steps), - nowColor[3] + (color[3] - nowColor[3]) * moveFunc(step / steps), - ] - core.clearMap('curtain'); - core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(curr)); - if (step == steps) { - delete core.animateFrame.asyncId[animate]; - clearInterval(animate); - cb(); - } - }, per_time); - - core.animateFrame.lastAsyncId = animate; - core.animateFrame.asyncId[animate] = cb; -} - -////// 画面闪烁 ////// -control.prototype.screenFlash = function (color, time, times, moveMode, callback) { - times = times || 1; - time = time / 3; - var nowColor = core.clone(core.status.curtainColor); - core.setCurtain(color, time, moveMode, function () { - core.setCurtain(nowColor, time * 2, moveMode, function () { - if (times > 1) - core.screenFlash(color, time * 3, times - 1, moveMode, callback); - else { - if (callback) callback(); - } - }); - }); -} - -////// 播放背景音乐 ////// -control.prototype.playBgm = function (bgm, startTime) { - bgm = core.getMappedName(bgm); - if (main.mode != 'play' || !core.material.bgms[bgm]) return; - // 如果不允许播放 - if (!core.musicStatus.bgmStatus) { - try { - core.musicStatus.playingBgm = bgm; - core.musicStatus.lastBgm = bgm; - core.material.bgms[bgm].pause(); - } - catch (e) { - console.error(e); - } - return; - } - this.setMusicBtn(); - - try { - this._playBgm_play(bgm, startTime); - } - catch (e) { - console.log("无法播放BGM " + bgm); - console.error(e); - core.musicStatus.playingBgm = null; - } -} - -control.prototype._playBgm_play = function (bgm, startTime) { - // 如果当前正在播放,且和本BGM相同,直接忽略 - if (core.musicStatus.playingBgm == bgm && !core.material.bgms[core.musicStatus.playingBgm].paused) { - return; - } - // 如果正在播放中,暂停 - if (core.musicStatus.playingBgm) { - core.material.bgms[core.musicStatus.playingBgm].pause(); - } - // 缓存BGM - core.loader.loadBgm(bgm); - // 播放当前BGM - core.material.bgms[bgm].volume = core.musicStatus.userVolume * core.musicStatus.designVolume; - core.material.bgms[bgm].currentTime = startTime || 0; - core.material.bgms[bgm].play(); - core.musicStatus.playingBgm = bgm; - core.musicStatus.lastBgm = bgm; - core.setBgmSpeed(100); -} - -///// 设置当前背景音乐的播放速度 ////// -control.prototype.setBgmSpeed = function (speed, usePitch) { - var bgm = core.musicStatus.playingBgm; - if (main.mode != 'play' || !core.material.bgms[bgm]) return; - bgm = core.material.bgms[bgm]; - if (speed < 30 || speed > 300) return; - bgm.playbackRate = speed / 100; - core.musicStatus.bgmSpeed = speed; - - if (bgm.preservesPitch != null) { - if (bgm.__preservesPitch == null) bgm.__preservesPitch = bgm.preservesPitch; - if (usePitch == null) bgm.preservesPitch = bgm.__preservesPitch; - else if (usePitch) bgm.preservesPitch = false; - else bgm.preservesPitch = true; - core.musicStatus.bgmUsePitch = usePitch; - } -} - -////// 暂停背景音乐的播放 ////// -control.prototype.pauseBgm = function () { - if (main.mode != 'play') return; - try { - if (core.musicStatus.playingBgm) { - core.musicStatus.pauseTime = core.material.bgms[core.musicStatus.playingBgm].currentTime; - core.material.bgms[core.musicStatus.playingBgm].pause(); - core.musicStatus.playingBgm = null; - } - } - catch (e) { - console.log("无法暂停BGM"); - console.error(e); - } - this.setMusicBtn(); -} - -////// 恢复背景音乐的播放 ////// -control.prototype.resumeBgm = function (resumeTime) { - if (main.mode != 'play') return; - try { - var speed = core.musicStatus.bgmSpeed; - var usePitch = core.musicStatus.bgmUsePitch; - core.playBgm(core.musicStatus.playingBgm || core.musicStatus.lastBgm || main.startBgm, - resumeTime ? core.musicStatus.pauseTime : 0); - if (resumeTime) { - core.setBgmSpeed(speed, usePitch); - } - } - catch (e) { - console.log("无法恢复BGM"); - console.error(e); - } - this.setMusicBtn(); -} - -control.prototype.setMusicBtn = function () { - if (core.musicStatus.bgmStatus) - core.dom.musicBtn.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAABWVBMVEX///9iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmL///8AAAC5ubn+/v6xsbEtLS0MDAxmZmZoaGhvb2/c3Nzd3d38/Pz9/f0oKCgpKSl0dHR1dXW6urrb29v7+/v09PTv7+/39/cgICACAgImJibh4eGFhYWGhoaHh4eOjo5paWm7u7vDw8PMzMwyMjI7OztAQEDe3t5FRUVMTEzj4+Pl5eXm5ubp6enr6+tcXFzi4uL19fVeXl74+PgjIyNkZGQGBgaSkpKYmJiampqenp4DAwMwMDBnZ2cICAivr68eHh63t7cLCwsSEhLw8PBhYWEUFBQVFRXNzc3Pz8/Z2dna2toaGhqkpKSlpaWpqamrq6tFOUNAAAAAc3RSTlMAAwQFBhUWGxwkJSYyO0dISVBRUmpvj5CSk5SVoaOlpqiysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyA0IuUgAAAVdJREFUeF5NkVVbw0AQRTcQrLR4IIEGcidJoaUuQHF3d3d3+P/CkuxCzss8nG++mbnDBJXhNt2CpbeFK1kQpSEKidlc8S9qdATRa6UIdQMoxEpDA0Ov3wUAPfW+qLWACydNv9zMrzkJwPK6FB3oHyOfXfuNxvoBQ+GmBYinhHB77TmiVBxoYUw1AYcEq332AS8OYKosAuTT0nza9uU2USYPRJgGxEiSOFywJ3mNARozgBJJzkfLvfu8JgGDWcC9FEsjWzR+y80gYDEAA8QZ3N6kmP1Fs3fEASB7pob7Hh+Wz5L0ci17Or05J7bH6B6dZv05XWK3rG+myV05Ert592Qo55sPuoIr7hEZHHtieIPWy0RU9DLwc3Mnck/vi8/E8XNrDWQtEVnL/ySKMrv0jPwPp870fprcyYifmiEmqGpHkI5q9ofSFIUk2qiwIGpEMyxYhhZRRcMPz89RJ2s9W8wAAAAASUVORK5CYII="; - else - core.dom.musicBtn.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAABYlBMVEX///9iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmL////8/PwAAABmZmZoaGihoaGioqKxsbG5ubnb29vc3Nzd3d3h4eHi4uL9/f3+/v4tLS1nZ2d0dHSUlJSenp66uroMDAz7+/spKSkoKCgUFBRpaWkVFRVvb291dXU7OzuVlZWYmJhkZGQgICAjIyOkpKQCAgK3t7cGBgbv7++pqamrq6seHh4mJiZhYWGamprp6enr6+saGhpeXl7j4+Pl5eXm5uZKSkrw8PD09PT19fW7u7vDw8PMzMwICAgwMDAyMjILCwtAQECGhoaHh4eBgYGFhYUSEhJXV1dZWVlcXFyOjo6SkpLNzc339/fPz8/Z2dna2tqTk5OlpaWxOPeTAAAAdnRSTlMAAwQFBhUWGxwkJSYyO0dISVBRUmpvj5CSk5SVoaOlpqiysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyNuo+uwAAAWJJREFUeF5NkmV34zAQReUm7WbTuJBNunY3bvXGDjNTkZkZlpn5/9eR5FPfbzr3jGb0RkwRiMQMDm7EIgHmRxtLwMOaHHoQjwz4MUKeCM8AWMrmd7u7f/aXAMyOShHiQD1n04DtN5e5FMBFlSauIsm585dKi4CpuSYKJIv1tBDVmvOSqJgEoowFLSBHaQh10XHWiCgHWEGmAw2blPrvOK/KRJUGoLM4kCVSKrWz7HwgoiwQZyaQJ0+9PvxV23BNATAZB25IqX9b3+jTW9fcApwB6NLgUD5NY3mPXnwmFwBezff1ztzRFzTp94FXMy36HDuCa2RafdnnmZqtL818Gl9/qNnEeyrUk2aTPiKj3qMyWBVi/YSuWq5qiwxkbtX3vYWzdz/l8M0k8ERlvViiB1Ygslb7SbVtJezncj+Cx5bYaeGuonZqhZlieAp+no74/s5EAh6JcY35Cepxk4ObcT3IJPe/1lKsDpFCFQAAAABJRU5ErkJggg=="; -} - -////// 更改背景音乐的播放 ////// -control.prototype.triggerBgm = function () { - if (main.mode != 'play') return; - - core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus; - if (core.musicStatus.bgmStatus) - this.resumeBgm(); - else - this.pauseBgm(); - core.setLocalStorage('bgmStatus', core.musicStatus.bgmStatus); -} - -let playingSoundList = []; - -////// 播放音频 ////// -control.prototype.playSound = function (sound, pitch, inputCallback) { - sound = core.getMappedName(sound); - if (main.mode != 'play' || !core.musicStatus.soundStatus || !core.material.sounds[sound] || playingSoundList.includes(sound)) return; - playingSoundList.push(sound); - const callback = () => { - playingSoundList = playingSoundList.filter((soundName) => soundName !== sound); - if (inputCallback) inputCallback(); - } - try { - if (core.musicStatus.audioContext != null) { - var source = core.musicStatus.audioContext.createBufferSource(); - source.__name = sound; - source.buffer = core.material.sounds[sound]; - source.connect(core.musicStatus.gainNode); - var id = setTimeout(null); - if (pitch && pitch >= 30 && pitch <= 300) { - source.playbackRate.setValueAtTime(pitch / 100, 0); - } - source.onended = function () { - delete core.musicStatus.playingSounds[id]; - if (callback) callback(); - } - core.musicStatus.playingSounds[id] = source; - if (source.start) source.start(0); - else if (source.noteOn) source.noteOn(0); - return id; - } - else { - core.material.sounds[sound].volume = core.musicStatus.userVolume; - core.material.sounds[sound].play(); - if (callback) callback(); - } - } - catch (e) { - console.log("无法播放SE " + sound); - console.error(e); - } -} - -////// 停止所有音频 ////// -control.prototype.stopSound = function (id) { - if (id == null) { - Object.keys(core.musicStatus.playingSounds).forEach(function (id) { - core.control.stopSound(id); - }); - return; - } - var source = core.musicStatus.playingSounds[id]; - if (!source) return; - try { - if (source.stop) source.stop(); - else if (source.noteOff) source.noteOff(); - } - catch (e) { - console.error(e); - } - delete core.musicStatus.playingSounds[id]; -} - -////// 获得当前正在播放的所有(指定)音效的id列表 ////// -control.prototype.getPlayingSounds = function (name) { - name = core.getMappedName(name); - return Object.keys(core.musicStatus.playingSounds).filter(function (one) { - return name == null || core.musicStatus.playingSounds[one].__name == name - }); -} - -////// 检查bgm状态 ////// -control.prototype.checkBgm = function () { - core.playBgm(core.musicStatus.playingBgm || main.startBgm); -} - -///// 设置屏幕放缩 ////// -control.prototype.setDisplayScale = function (delta) { - var index = core.domStyle.availableScale.indexOf(core.domStyle.scale); - if (index < 0) return; - index = (index + delta + core.domStyle.availableScale.length) % core.domStyle.availableScale.length; - core.domStyle.scale = core.domStyle.availableScale[index]; - core.setLocalStorage('scale', core.domStyle.scale); - core.resize(); -} - -// ------ 状态栏,工具栏等相关 ------ // - -////// 清空状态栏 ////// -control.prototype.clearStatusBar = function () { - Object.keys(core.statusBar).forEach(function (e) { - if (core.statusBar[e].innerHTML != null) { - core.statusBar[e].innerHTML = " "; - core.statusBar[e].removeAttribute('_style'); - core.statusBar[e].removeAttribute('_value'); - } - }) - core.statusBar.image.book.style.opacity = 0.3; - if (!core.flags.equipboxButton) - core.statusBar.image.fly.style.opacity = 0.3; -} - -////// 更新状态栏 ////// -control.prototype.updateStatusBar = function (doNotCheckAutoEvents, immediate) { - if (immediate) { - return this.updateStatusBar_update(); - } - if (!doNotCheckAutoEvents) this.noAutoEvents = false; - if (core.isReplaying()) return this.updateStatusBar_update(); - requestAnimationFrame(this.updateStatusBar_update) -} - -control.prototype.updateStatusBar_update = function () { - if (!core.isPlaying() || core.hasFlag('__statistics__')) return; - core.control.controldata.updateStatusBar(); - if (!core.control.noAutoEvents) core.checkAutoEvents(); - core.control._updateStatusBar_setToolboxIcon(); - core.clearRouteFolding(); - core.control.noAutoEvents = true; -} - -/** 切换replay/pause 并刷新工具栏图标的完成度 */ -control.prototype._updateStatusBar_setToolboxIcon = function () { - if (core.isReplaying()) { - main.statusBar.image.play.src = core.status.replay.pausing ? core.statusBar.icons.play.src : core.statusBar.icons.pause.src; - } - core.statusBar.image.book.style.opacity = core.hasItem('book') ? 1 : 0.3; - core.statusBar.image.fly.style.opacity = core.hasItem('fly') ? 1 : 0.3; - core.statusBar.image.undoRollback.style.opacity = - (core.saves.autosave.data != null && core.saves.autosave.now < core.saves.autosave.data.length) ? 1 : 0.3; - core.statusBar.image.save.style.opacity = core.hasFlag('__forbidSave__') ? 0.3 : 1; - -} - -control.prototype.showStatusBar = function () { - if (main.mode == 'editor') return; - if (core.domStyle.showStatusBar) return; - var statusItems = core.dom.status; - core.domStyle.showStatusBar = true; - core.removeFlag('hideStatusBar'); - // 显示 - for (var i = 0; i < statusItems.length; ++i) - statusItems[i].style.opacity = 1; - core.setToolbarButton(core.domStyle.toolbarBtn); - // this.setToolbarButton(false); - core.dom.tools.hard.style.display = 'block'; - core.dom.toolBar.style.display = 'block'; -} - -control.prototype.hideStatusBar = function (showToolbox) { - if (main.mode == 'editor') return; - - // 如果原本就是隐藏的,则先显示 - if (!core.domStyle.showStatusBar) - this.showStatusBar(); - if (core.isReplaying()) showToolbox = true; - - var statusItems = core.dom.status, toolItems = core.dom.tools; - core.domStyle.showStatusBar = false; - core.setFlag('hideStatusBar', true); - core.setFlag('showToolbox', showToolbox || null); - // 隐藏 - for (var i = 0; i < statusItems.length; ++i) - statusItems[i].style.opacity = 0; - if (!showToolbox) core.setToolbarButton('hide'); - // if ((!core.domStyle.isVertical && !core.flags.extendToolbar) || !showToolbox) { - // for (var i = 0; i < toolItems.length; ++i) - // toolItems[i].style.display = 'none'; - // } - // if (!core.domStyle.isVertical && !core.flags.extendToolbar) { - // core.dom.toolBar.style.display = 'none'; - // } -} - -////// 更新状态栏的勇士图标 ////// -control.prototype.updateHeroIcon = function (name) { - name = name || "hero.png"; - if (core.statusBar.icons.name == name) return; - core.statusBar.icons.name = name; - - var image = core.material.images.hero; - // 全身图 - var w = core.material.icons.hero.width || 32; - var h = core.material.icons.hero.height || 48; - var ratio = Math.min(w / h, 1), width = 32 * ratio, left = 16 - width / 2; - - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext("2d"); - canvas.width = 32; - canvas.height = 32; - core.drawImage(ctx, image, 0, 0, w, h, left, 0, width, 32); - - core.statusBar.image.name.src = canvas.toDataURL("image/png"); -} - -////// 改变工具栏为按钮1-8 ////// -// control.prototype.setToolbarButton = function (useButton) { -// if (!core.domStyle.showStatusBar) { -// // 隐藏状态栏时检查竖屏 -// if (!core.domStyle.isVertical && !core.flags.extendToolbar) { -// for (var i = 0; i < core.dom.tools.length; ++i) -// core.dom.tools[i].style.display = 'none'; -// return; -// } -// if (!core.hasFlag('showToolbox')) return; -// else core.dom.tools.hard.style.display = 'block'; -// } - -// if (useButton == null) useButton = core.domStyle.toolbarBtn; -// if ((!core.domStyle.isVertical && !core.flags.extendToolbar) || core.isReplaying()) useButton = false; -// core.domStyle.toolbarBtn = useButton; - -// if (useButton) { -// ["book", "fly", "toolbox", "keyboard", "shop", "save", "load", "settings"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'none'; -// }); -// ["rollback", "undoRollback"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'none'; -// }); -// ["btn1", "btn2", "btn3", "btn4", "btn5", "btn6", "btn7", "btnAlt"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'block'; -// }) -// if (!core.flags.showHard) { -// ["btn8", "btn9"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'block'; -// }); -// } -// main.statusBar.image.btnAlt.style.filter = core.getLocalStorage('altKey') ? 'sepia(1) contrast(1.5)' : ''; -// } -// else { -// ["btn1", "btn2", "btn3", "btn4", "btn5", "btn6", "btn7", "btnAlt"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'none'; -// }); -// ["btn8", "btn9"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'none'; -// }); -// ["book", "fly", "toolbox", "save", "load", "settings"].forEach(function (t) { -// core.statusBar.image[t].style.display = 'block'; -// }); -// if (core.domStyle.isVertical || core.flags.extendToolbar || !core.flags.showHard) { -// core.statusBar.image.shop.style.display = "block"; -// } -// else core.statusBar.image.shop.style.display = "none"; - -// ["rollback", "undoRollback"].forEach(function (t) { -// core.statusBar.image[t].style.display = core.flags.showHard ? "none" : "block"; -// }); - -// core.statusBar.image.keyboard.style.display -// = core.domStyle.isVertical || core.flags.extendToolbar ? "block" : "none"; -// } -// } - -////// ------ resize处理 ------ // - -control.prototype._shouldDisplayStatus = function (id) { - if (id == null) { - var toDraw = [], status = core.dom.status; - for (var i = 0; i < status.length; ++i) { - var dom = core.dom.status[i], idCol = dom.id; - if (idCol.indexOf("Col") != idCol.length - 3) continue; - var id = idCol.substring(0, idCol.length - 3); - if (!this._shouldDisplayStatus(id)) continue; - toDraw.push(id); - } - return toDraw; - } - var obj = {}; - core.flags.statusBarItems.forEach(function (v) { obj[v] = true; }) - switch (id) { - case 'floor': return obj.enableFloor; - case 'name': return obj.enableName; - case 'lv': return obj.enableLv; - case 'hp': return obj.enableHP; - case 'hpmax': return obj.enableHPMax; - case 'mana': return obj.enableMana; - case 'atk': return obj.enableAtk; - case 'def': return obj.enableDef; - case 'mdef': return obj.enableMDef; - case 'money': return obj.enableMoney; - case 'exp': return obj.enableExp && !obj.levelUpLeftMode; - case 'up': return obj.enableLevelUp; - case 'skill': return obj.enableSkill; - case 'key': return obj.enableKeys; - case 'pzf': return obj.enablePZF; - case 'debuff': return obj.enableDebuff; - default: return true; - } -} - -////// 注册一个resize函数 ////// -// name为名称,可供注销使用 -// func可以是一个函数,或者是插件中的函数名;可以接受obj参数,详见resize函数。 -control.prototype.registerResize = function (name, func) { - this.unregisterResize(name); - this.resizes.push({ name: name, func: func }); -} - -////// 注销一个resize函数 ////// -control.prototype.unregisterResize = function (name) { - this.resizes = this.resizes.filter(function (b) { return b.name != name; }); -} - -/** 执行所有resize函数 */ -control.prototype._doResize = function (obj) { - for (var i in this.resizes) { - try { - if (core.doFunc(this.resizes[i].func, this, obj)) return true; - } catch (e) { - console.error(e); - console.error("ERROR in resizes[" + this.resizes[i].name + "]:已自动注销该项。"); - this.unregisterResize(this.resizes[i].name); - } - } - return false; -} - -/** 执行单个特定名称的resize函数 */ -control.prototype._doOneResize = function (obj, type) { - try { - const currEvent = this.resizes.find((resizeEvent) => resizeEvent.name === type); - if (!currEvent) return; - if (core.doFunc(currEvent.func, this, obj)) return true; - } catch (e) { - console.error(e); - console.error("ERROR in resizes[" + this.resizes[type].name + "]:已自动注销该项。"); - this.unregisterResize(this.resizes[type].name); - } - return false; -} - -////// 屏幕分辨率改变后重新自适应 ////// -control.prototype.resize = function (type) { - if (main.mode == 'editor') return; - var clientWidth = main.dom.body.clientWidth, clientHeight = main.dom.body.clientHeight; - var CANVAS_WIDTH = core.__PIXELS__, BAR_WIDTH = Math.round(core.__PIXELS__ * 0.31); - var BORDER = 3; - var extendToolbar = core.flags.extendToolbar; - - var horizontalMaxRatio = (clientHeight - 2 * BORDER - (extendToolbar ? BORDER : 0)) / (CANVAS_WIDTH + (extendToolbar ? 38 : 0)); - - if (clientWidth - 3 * BORDER >= CANVAS_WIDTH + BAR_WIDTH || (clientWidth > clientHeight && horizontalMaxRatio < 1)) { - // 横屏 - core.domStyle.isVertical = false; - - core.domStyle.availableScale = []; - [1, 1.25, 1.5, 1.75, 2, 2.25, 2.5].forEach(function (v) { - if (clientWidth - 3 * BORDER >= v * (CANVAS_WIDTH + BAR_WIDTH) && horizontalMaxRatio >= v) { - core.domStyle.availableScale.push(v); - } - }); - if (core.domStyle.availableScale.indexOf(core.domStyle.scale) < 0) { - core.domStyle.scale = Math.min(1, horizontalMaxRatio); - } - } - else { - // 竖屏 - core.domStyle.isVertical = true; - core.domStyle.scale = Math.min((clientWidth - 2 * BORDER) / CANVAS_WIDTH); - core.domStyle.availableScale = []; - extendToolbar = false; - } - - var statusDisplayArr = this._shouldDisplayStatus(), count = statusDisplayArr.length; - var statusCanvas = core.flags.statusCanvas, statusCanvasRows = core.values.statusCanvasRowsOnMobile || 3; - var col = statusCanvas ? statusCanvasRows : Math.ceil(count / 3); - if (col > 5) { - if (statusCanvas) alert("自绘状态栏的在竖屏下的行数应不超过5!"); - else alert("当前状态栏数目(" + count + ")大于15,请调整到不超过15以避免手机端出现显示问题。"); - } - var globalAttribute = core.status.globalAttribute || core.initStatus.globalAttribute; - - var obj = { - clientWidth: clientWidth, - clientHeight: clientHeight, - CANVAS_WIDTH: CANVAS_WIDTH, - BORDER: BORDER, - BAR_WIDTH: BAR_WIDTH, - TOOLBAR_HEIGHT: 38, - outerSize: CANVAS_WIDTH * core.domStyle.scale + 2 * BORDER, - globalAttribute: globalAttribute, - border: '3px ' + core.arrayToRGBA(globalAttribute.borderColor) + ' solid', - statusDisplayArr: statusDisplayArr, - count: count, - col: col, - statusBarHeightInVertical: core.domStyle.isVertical ? (32 * col + 6) * core.domStyle.scale + 2 * BORDER : 0, - toolbarHeightInVertical: core.domStyle.isVertical ? 38 * core.domStyle.scale + 2 * BORDER : 0, - extendToolbar: extendToolbar, - is15x15: core.__SIZE__ == 15 - }; - - if (type != null) { - if (type instanceof Array) type.forEach((oneType) => this._doOneResize(obj, oneType)); - else this._doOneResize(obj, type); - } - else this._doResize(obj); - // this.setToolbarButton(); - core.updateStatusBar(); -} - -control.prototype._resize_gameGroup = function (obj) { - var startBackground = core.domStyle.isVertical ? (main.styles.startVerticalBackground || main.styles.startBackground) : main.styles.startBackground; - if (main.dom.startBackground.getAttribute('__src__') != startBackground) { - main.dom.startBackground.setAttribute('__src__', startBackground); - main.dom.startBackground.src = startBackground; - } - - var gameGroup = core.dom.gameGroup; - var totalWidth, totalHeight; - if (core.domStyle.isVertical) { - totalWidth = obj.outerSize; - totalHeight = obj.outerSize + obj.statusBarHeightInVertical + obj.toolbarHeightInVertical - } - else { - totalWidth = obj.outerSize + obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER; - totalHeight = obj.outerSize + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0); - } - gameGroup.style.width = totalWidth + "px"; - gameGroup.style.height = totalHeight + "px"; - gameGroup.style.left = (obj.clientWidth - totalWidth) / 2 + "px"; - gameGroup.style.top = (obj.clientHeight - totalHeight) / 2 + "px"; - // floorMsgGroup - var floorMsgGroup = core.dom.floorMsgGroup; - floorMsgGroup.style = obj.globalAttribute.floorChangingStyle; - floorMsgGroup.style.width = obj.outerSize - 2 * obj.BORDER + "px"; - floorMsgGroup.style.height = totalHeight - 2 * obj.BORDER + "px"; - floorMsgGroup.style.fontSize = 16 * core.domStyle.scale + "px"; - // startPanel - core.dom.startPanel.style.fontSize = 16 * core.domStyle.scale + "px"; - // musicBtn - if (core.domStyle.isVertical || core.domStyle.scale < 1) { - core.dom.musicBtn.style.right = core.dom.musicBtn.style.bottom = "3px"; - } - else { - core.dom.musicBtn.style.right = (obj.clientWidth - totalWidth) / 2 + "px"; - core.dom.musicBtn.style.bottom = (obj.clientHeight - totalHeight) / 2 - 27 + "px"; - } -} - -control.prototype.registerDymCanvasResizeEvent = function (name, event){ - this.dymCanvasResizeEvents[name] = event; -} - -control.prototype.unregisterDymCanvasResizeEvent = function (name){ - delete this.dymCanvasResizeEvents[name]; -} - -control.prototype._resize_canvas = function (obj) { - var innerSize = (obj.CANVAS_WIDTH * core.domStyle.scale) + "px"; - if (!core.isPlaying()) { - for (var i = 0; i < core.dom.gameCanvas.length; ++i) { - var ctx = core.dom.gameCanvas[i].getContext('2d'); - core.resizeCanvas(ctx, core.__PIXELS__, core.__PIXELS__); - core.dom.gameCanvas[i].style.width = core.dom.gameCanvas[i].style.height = innerSize; - } - } else { - requestAnimationFrame(function () { - for (var i = 0; i < core.dom.gameCanvas.length; ++i) - core.dom.gameCanvas[i].style.width = core.dom.gameCanvas[i].style.height = innerSize; - }); - } - core.dom.gif.style.width = core.dom.gif.style.height = innerSize; - core.dom.gif2.style.width = core.dom.gif2.style.height = innerSize; - core.dom.gameDraw.style.width = core.dom.gameDraw.style.height = innerSize; - core.dom.gameDraw.style.top = obj.statusBarHeightInVertical + "px"; - core.dom.gameDraw.style.right = 0; - core.dom.gameDraw.style.border = obj.border; - // resize bigmap - core.bigmap.canvas.forEach(function (cn) { - var ratio = core.canvas[cn].canvas.hasAttribute('isHD') ? core.domStyle.ratio : 1; - core.canvas[cn].canvas.style.width = core.canvas[cn].canvas.width / ratio * core.domStyle.scale + "px"; - core.canvas[cn].canvas.style.height = core.canvas[cn].canvas.height / ratio * core.domStyle.scale + "px"; - }); - // resize dynamic canvas - if (!core.isPlaying()) { - for (var name in core.dymCanvas) { - var ctx = core.dymCanvas[name], canvas = ctx.canvas; - // core.maps._setHDCanvasSize(ctx, parseFloat(canvas.getAttribute('_width')), parseFloat(canvas.getAttribute('_height'))); - canvas.style.left = parseFloat(canvas.getAttribute("_left")) * core.domStyle.scale + "px"; - canvas.style.top = parseFloat(canvas.getAttribute("_top")) * core.domStyle.scale + "px"; - var scale = canvas.getAttribute('_scale') || 1; - core.resizeCanvas(canvas, canvas.width * scale / core.domStyle.scale, canvas.height * scale / core.domStyle.scale); - } - } else { - for (var name in core.dymCanvas) { - var ctx = core.dymCanvas[name], canvas = ctx.canvas; - core.resizeCanvas(ctx, parseFloat(canvas.getAttribute("_width")), parseFloat(canvas.getAttribute("_height"))); - canvas.style.left = parseFloat(canvas.getAttribute("_left")) * core.domStyle.scale + "px"; - canvas.style.top = parseFloat(canvas.getAttribute("_top")) * core.domStyle.scale + "px"; - } - if (this.dymCanvasResizeEvents[name]) { - const event = this.dymCanvasResizeEvents[name]; - event(); - } - } - // resize next - main.dom.next.style.width = main.dom.next.style.height = 5 * core.domStyle.scale + "px"; - main.dom.next.style.borderBottomWidth = main.dom.next.style.borderRightWidth = 4 * core.domStyle.scale + "px"; -} - -control.prototype._resize_statusBar = function (obj) { - // statusBar - var statusBar = core.dom.statusBar; - if (core.domStyle.isVertical) { - statusBar.style.width = obj.outerSize + "px"; - statusBar.style.height = obj.statusBarHeightInVertical + "px"; - statusBar.style.background = obj.globalAttribute.statusTopBackground; - statusBar.style.fontSize = 16 * core.domStyle.scale + "px"; - } - else { - statusBar.style.width = (obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER) + "px"; - statusBar.style.height = obj.outerSize + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0) + "px"; - statusBar.style.background = obj.globalAttribute.statusLeftBackground; - // --- 计算文字大小 - if (obj.extendToolbar) { - statusBar.style.fontSize = 16 * core.domStyle.scale + "px"; - } else { - statusBar.style.fontSize = 16 * Math.min(1, (core.__HALF_SIZE__ + 3) / obj.count) * core.domStyle.scale + "px"; - } - } - statusBar.style.display = 'block'; - statusBar.style.borderTop = statusBar.style.borderLeft = obj.border; - statusBar.style.borderRight = core.domStyle.isVertical ? obj.border : ''; - statusBar.style.borderBottom = core.domStyle.isVertical ? '' : obj.border; - // 自绘状态栏 - if (core.domStyle.isVertical) { - core.dom.statusCanvas.style.width = obj.CANVAS_WIDTH * core.domStyle.scale + "px"; - core.dom.statusCanvas.style.height = obj.statusBarHeightInVertical - 3 + "px"; - core.maps._setHDCanvasSize(core.dom.statusCanvasCtx, obj.CANVAS_WIDTH, obj.col * 32 + 9); - } - else { - core.dom.statusCanvas.style.width = obj.BAR_WIDTH * core.domStyle.scale + "px"; - core.dom.statusCanvas.style.height = obj.outerSize - 2 * obj.BORDER + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0) + "px"; - core.maps._setHDCanvasSize(core.dom.statusCanvasCtx, obj.BAR_WIDTH, obj.CANVAS_WIDTH + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT + obj.BORDER : 0)); - } - core.dom.statusCanvas.style.display = core.flags.statusCanvas ? "block" : "none"; -} - -control.prototype._resize_status = function (obj) { - var statusHeight; - if (core.domStyle.isVertical) { - statusHeight = 32 * core.domStyle.scale * 0.8; - } else { - statusHeight = (obj.extendToolbar ? core.__SIZE__ : core.__HALF_SIZE__ + 3) / obj.count * 32 * core.domStyle.scale * 0.8; - } - // status - for (var i = 0; i < core.dom.status.length; ++i) { - var id = core.dom.status[i].id, style = core.dom.status[i].style; - if (id.endsWith("Col")) id = id.substring(0, id.length - 3); - style.display = core.flags.statusCanvas || obj.statusDisplayArr.indexOf(id) < 0 ? 'none' : 'block'; - style.margin = 3 * core.domStyle.scale + "px"; - style.height = statusHeight + "px"; - style.maxWidth = obj.BAR_WIDTH * core.domStyle.scale * (core.domStyle.isVertical ? 0.95 : 1) + obj.BORDER + "px"; - if (obj.is15x15 && !core.domStyle.isVertical) - style.marginLeft = 11 * core.domStyle.scale + "px"; - } - // statusLabels, statusTexts - for (var i = 0; i < core.dom.statusLabels.length; ++i) { - core.dom.statusLabels[i].style.lineHeight = statusHeight + "px"; - core.dom.statusLabels[i].style.marginLeft = 6 * core.domStyle.scale + "px"; - } - for (var i = 0; i < core.dom.statusTexts.length; ++i) { - core.dom.statusTexts[i].style.color = core.arrayToRGBA(obj.globalAttribute.statusBarColor); - } - // keys - if (core.flags.statusBarItems.indexOf('enableGreenKey') >= 0) { - core.dom.keyCol.style.fontSize = '0.75em'; - core.statusBar.greenKey.style.display = ''; - } else { - core.dom.keyCol.style.fontSize = ''; - core.statusBar.greenKey.style.display = 'none'; - } -} - -/** toolBar的样式 */ -control.prototype._resize_toolBar = function (obj) { - var toolBar = core.dom.toolBar; - if (core.domStyle.isVertical) { - toolBar.style.left = 0; - toolBar.style.right = ""; - toolBar.style.width = obj.outerSize + "px"; - toolBar.style.top = obj.statusBarHeightInVertical + obj.outerSize + "px"; - toolBar.style.height = obj.toolbarHeightInVertical + "px"; - toolBar.style.background = obj.globalAttribute.toolsBackground; - } - else { - if (obj.extendToolbar) { - toolBar.style.left = ""; - toolBar.style.right = 0; - toolBar.style.width = obj.outerSize + "px"; - toolBar.style.top = obj.outerSize + "px"; - toolBar.style.height = obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER + "px"; - toolBar.style.background = obj.globalAttribute.toolsBackground; - } else { - toolBar.style.left = 0; - toolBar.style.right = ""; - toolBar.style.width = obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER + "px"; - toolBar.style.top = 0.718 * obj.outerSize + "px"; - toolBar.style.height = 0.281 * obj.outerSize + "px"; - toolBar.style.background = 'transparent'; - } - } - toolBar.style.borderLeft = obj.border; - toolBar.style.borderRight = toolBar.style.borderBottom = core.domStyle.isVertical || obj.extendToolbar ? obj.border : ''; - toolBar.style.fontSize = 16 * core.domStyle.scale + "px"; - - // 本项控制toolBar本身的显示与隐藏 - if (!core.domStyle.showStatusBar && !core.domStyle.isVertical && !obj.extendToolbar) { - toolBar.style.display = 'none'; - } else { - toolBar.style.display = 'block'; - } -} - -/** toolBar中各个tools的样式 */ -control.prototype._resize_tools = function (obj) { - var toolsHeight = 32 * core.domStyle.scale * ((core.domStyle.isVertical || obj.extendToolbar) && !obj.is15x15 ? 0.95 : 1); - var toolsMarginLeft; - if (core.domStyle.isVertical || obj.extendToolbar) - toolsMarginLeft = (core.__HALF_SIZE__ - 3) * 3 * core.domStyle.scale - 1; - else - toolsMarginLeft = (obj.BAR_WIDTH * core.domStyle.scale - 9 - toolsHeight * 3) / 4; - for (var i = 0; i < core.dom.tools.length; ++i) { - var style = core.dom.tools[i].style; - style.height = toolsHeight + "px"; - style.marginLeft = toolsMarginLeft + "px"; - style.marginTop = 3 * core.domStyle.scale + "px" - } - core.dom.hard.style.lineHeight = toolsHeight + "px"; - if (core.domStyle.isVertical || obj.extendToolbar) { - const toolsCount = core.domStyle.toolsCount; - if (toolsCount <= 8) { - core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - (toolsCount + 0.5) * toolsHeight - 22 + "px"; - } - else if (toolsCount === 9) { - core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - toolsCount * toolsHeight - 22 + "px"; - } - else { // toolsCount is 10 - core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - toolsCount * toolsHeight - 13 + "px"; - core.dom.hard.style.marginLeft = "1px"; - } - } - else { - core.dom.hard.style.width = obj.BAR_WIDTH * core.domStyle.scale - 9 - 2 * toolsMarginLeft + "px"; - if (!obj.is15x15) core.dom.hard.style.marginTop = 0; - } -} +/// + +/* +control.js:游戏主要逻辑控制 +主要负责status相关内容,以及各种变量获取/存储 +寻路算法和人物行走也在此文件内 + */ + +"use strict"; + +function control() { + this._init(); +} + +control.prototype._init = function () { + this.controldata = functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a.control; + this.renderFrameFuncs = []; + this.replayActions = []; + this.weathers = {}; + this.resizes = []; + this.noAutoEvents = true; + /** @type {{[canvasName:string]:Function}} */ + this.dymCanvasResizeEvents = {}; + // --- 注册系统的animationFrame + this.registerAnimationFrame("totalTime", false, this._animationFrame_totalTime); + this.registerAnimationFrame("autoSave", true, this._animationFrame_autoSave); + this.registerAnimationFrame("globalAnimate", true, this._animationFrame_globalAnimate); + this.registerAnimationFrame("animate", true, this._animationFrame_animate); + this.registerAnimationFrame("heroMoving", true, this._animationFrame_heroMoving); + this.registerAnimationFrame("weather", true, this._animationFrame_weather); + this.registerAnimationFrame("tip", true, this._animateFrame_tip); + this.registerAnimationFrame("parallelDo", false, this._animationFrame_parallelDo); + // --- 注册系统的天气 + this.registerWeather("rain", this._weather_rain, this._animationFrame_weather_rain); + this.registerWeather("snow", this._weather_snow, this._animationFrame_weather_snow); + this.registerWeather("fog", this._weather_fog, this.__animateFrame_weather_image); + this.registerWeather("cloud", this._weather_cloud, this.__animateFrame_weather_image); + this.registerWeather("sun", this._weather_sun, this._animationFrame_weather_sun); + // --- 注册系统的replay + this.registerReplayAction("move", this._replayAction_move); + this.registerReplayAction("item", this._replayAction_item); + this.registerReplayAction("equip", this._replayAction_equip); + this.registerReplayAction("unEquip", this._replayAction_unEquip); + this.registerReplayAction("saveEquip", this._replayAction_saveEquip); + this.registerReplayAction("loadEquip", this._replayAction_loadEquip); + this.registerReplayAction("fly", this._replayAction_fly); + this.registerReplayAction("shop", this._replayAction_shop); + this.registerReplayAction("turn", this._replayAction_turn); + this.registerReplayAction("getNext", this._replayAction_getNext); + this.registerReplayAction("moveDirectly", this._replayAction_moveDirectly); + this.registerReplayAction("key", this._replayAction_key); + this.registerReplayAction("click", this._replayAction_click); + this.registerReplayAction("ignoreInput", this._replayAction_ignoreInput); + this.registerReplayAction("no", this._replayAction_no); + // --- 注册系统的resize + this.registerResize("gameGroup", this._resize_gameGroup); + this.registerResize("canvas", this._resize_canvas); + this.registerResize("statusBar", this._resize_statusBar); + this.registerResize("status", this._resize_status); + this.registerResize("toolBar", this._resize_toolBar); + this.registerResize("tools", this._resize_tools); +} + +// ------ requestAnimationFrame 相关 ------ // + +////// 注册一个 animationFrame ////// +// name:名称,可用来作为注销使用;needPlaying:是否只在游戏运行时才执行(在标题界面不执行) +// func:要执行的函数,或插件中的函数名;可接受timestamp(从页面加载完毕到当前所经过的时间)作为参数 +control.prototype.registerAnimationFrame = function (name, needPlaying, func) { + this.unregisterAnimationFrame(name); + this.renderFrameFuncs.push({ name: name, needPlaying: needPlaying, func: func }); +} + +////// 注销一个 animationFrame ////// +control.prototype.unregisterAnimationFrame = function (name) { + this.renderFrameFuncs = this.renderFrameFuncs.filter(function (x) { return x.name != name; }); +} + +////// 设置requestAnimationFrame ////// +control.prototype._setRequestAnimationFrame = function () { + this._checkRequestAnimationFrame(); + core.animateFrame.totalTime = Math.max(core.animateFrame.totalTime, core.getLocalStorage('totalTime', 0)); + var loop = function (timestamp) { + core.control.renderFrameFuncs.forEach(function (b) { + if (b.func) { + try { + if (core.isPlaying() || !b.needPlaying) + core.doFunc(b.func, core.control, timestamp); + } + catch (e) { + console.error(e); + console.error("ERROR in requestAnimationFrame[" + b.name + "]:已自动注销该项。"); + core.unregisterAnimationFrame(b.name); + } + } + }) + window.requestAnimationFrame(loop); + } + window.requestAnimationFrame(loop); +} + +control.prototype._checkRequestAnimationFrame = function () { + (function () { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了 + window[vendors[x] + 'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + } + }()); +} + +control.prototype._animationFrame_totalTime = function (timestamp) { + core.animateFrame.totalTime += timestamp - core.animateFrame.totalTimeStart; + core.animateFrame.totalTimeStart = timestamp; + if (core.isPlaying()) { + core.status.hero.statistics.totalTime = core.animateFrame.totalTime; + core.status.hero.statistics.currTime += timestamp - (core.status.hero.statistics.start || timestamp); + core.status.hero.statistics.start = timestamp; + } +} + +control.prototype._animationFrame_autoSave = function (timestamp) { + if (timestamp - core.saves.autosave.time <= 5000) return; + core.control.checkAutosave(); + core.saves.autosave.time = timestamp; +} + +control.prototype._animationFrame_globalAnimate = function (timestamp) { + if (timestamp - core.animateFrame.globalTime <= core.values.animateSpeed) return; + core.status.globalAnimateStatus++; + if (core.status.floorId) { + // Global Animate + core.status.globalAnimateObjs.forEach(function (block) { + core.drawBlock(block, core.status.globalAnimateStatus); + }); + + // Global floor images + core.maps._drawFloorImages(core.status.floorId, core.canvas.bg, 'bg', core.status.floorAnimateObjs || [], core.status.globalAnimateStatus); + core.maps._drawFloorImages(core.status.floorId, core.canvas.fg, 'fg', core.status.floorAnimateObjs || [], core.status.globalAnimateStatus); + + // Global Autotile Animate + core.status.autotileAnimateObjs.forEach(function (block) { + core.maps._drawAutotileAnimate(block, core.status.globalAnimateStatus); + }); + + // Global hero animate + if ((core.status.hero || {}).animate && core.status.heroMoving == 0 && main.mode == 'play' && !core.status.preview.enabled) { + core.drawHero('stop', null, core.status.globalAnimateStatus); + } + } + // Box animate + core.drawBoxAnimate(); + core.animateFrame.globalTime = timestamp; +} + +control.prototype._animationFrame_animate = function (timestamp) { + if (timestamp - core.animateFrame.animateTime < 50 || !core.status.animateObjs || core.status.animateObjs.length == 0) return; + core.clearMap('animate'); + // 更新帧 + for (var i = 0; i < core.status.animateObjs.length; i++) { + var obj = core.status.animateObjs[i]; + if (obj.index == obj.animate.frames.length) { + (function (callback) { + setTimeout(function () { + if (callback) callback(); + }); + })(obj.callback); + } + } + core.status.animateObjs = core.status.animateObjs.filter(function (obj) { + return obj.index < obj.animate.frames.length; + }); + core.status.animateObjs.forEach(function (obj) { + if (obj.hero) { + core.maps._drawAnimateFrame('animate', obj.animate, core.status.heroCenter.px, core.status.heroCenter.py, obj.index++); + } else { + core.maps._drawAnimateFrame('animate', obj.animate, obj.centerX, obj.centerY, obj.index++); + } + }); + core.animateFrame.animateTime = timestamp; +} + +control.prototype._animationFrame_heroMoving = function (timestamp) { + if (core.status.heroMoving <= 0) return; + // 换腿 + if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) { + core.animateFrame.leftLeg = !core.animateFrame.leftLeg; + core.animateFrame.moveTime = timestamp; + } + core.drawHero(core.animateFrame.leftLeg ? 'leftFoot' : 'rightFoot', 4 * core.status.heroMoving); +} + +control.prototype._animationFrame_weather = function (timestamp) { + var weather = core.animateFrame.weather, type = weather.type; + if (!core.dymCanvas.weather || !core.control.weathers[type] || !core.control.weathers[type].frameFunc) return; + try { + core.doFunc(core.control.weathers[type].frameFunc, core.control, timestamp, core.animateFrame.weather.level); + } catch (e) { + console.error(e); + console.error("ERROR in weather[" + type + "]:已自动注销该项。"); + core.unregisterWeather(type); + } +} + +control.prototype._animationFrame_weather_rain = function (timestamp, level) { + if (timestamp - core.animateFrame.weather.time < 30) return; + var ctx = core.dymCanvas.weather, ox = core.bigmap.offsetX, oy = core.bigmap.offsetY; + core.clearMap('weather'); + ctx.strokeStyle = 'rgba(174,194,224,0.8)'; + ctx.lineWidth = 1; + ctx.lineCap = 'round'; + + core.animateFrame.weather.nodes.forEach(function (p) { + ctx.beginPath(); + ctx.moveTo(p.x - ox, p.y - oy); + ctx.lineTo(p.x + p.l * p.xs - ox, p.y + p.l * p.ys - oy); + ctx.stroke(); + + p.x += p.xs; + p.y += p.ys; + if (p.x > core.bigmap.width * 32 || p.y > core.bigmap.height * 32) { + p.x = Math.random() * core.bigmap.width * 32; + p.y = -10; + } + + }); + + ctx.fill(); + core.animateFrame.weather.time = timestamp; +} + +control.prototype._animationFrame_weather_snow = function (timestamp, level) { + if (timestamp - core.animateFrame.weather.time < 30) return; + var ctx = core.dymCanvas.weather, ox = core.bigmap.offsetX, oy = core.bigmap.offsetY; + core.clearMap('weather'); + ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; + ctx.beginPath(); + core.animateFrame.weather.data = core.animateFrame.weather.data || 0; + core.animateFrame.weather.data += 0.01; + + var angle = core.animateFrame.weather.data; + core.animateFrame.weather.nodes.forEach(function (p) { + ctx.moveTo(p.x - ox, p.y - oy); + ctx.arc(p.x - ox, p.y - oy, p.r, 0, Math.PI * 2, true); + // update + p.x += Math.sin(angle) * core.animateFrame.weather.level; + p.y += Math.cos(angle + p.d) + 1 + p.r / 2; + if (p.x > core.bigmap.width * 32 + 5 || p.x < -5 || p.y > core.bigmap.height * 32) { + if (Math.random() > 1 / 3) { + p.x = Math.random() * core.bigmap.width * 32; + p.y = -10; + } + else { + if (Math.sin(angle) > 0) + p.x = -5; + else + p.x = core.bigmap.width * 32 + 5; + p.y = Math.random() * core.bigmap.height * 32; + } + } + }); + ctx.fill(); + core.animateFrame.weather.time = timestamp; +} + +control.prototype.__animateFrame_weather_image = function (timestamp, level) { + if (timestamp - core.animateFrame.weather.time < 30) return; + var node = core.animateFrame.weather.nodes[0]; + var image = node.image; + if (!image) return; + core.clearMap('weather'); + core.setAlpha('weather', node.level / 500); + var wind = 1.5; + var width = image.width, height = image.height; + node.x += node.dx * wind; + node.y += (2 * node.dy - 1) * wind; + if (node.x + 3 * width <= core.__PIXELS__) { + node.x += 4 * width; + while (node.x > 0) node.x -= width; + } + node.dy += node.delta; + if (node.dy >= 1) { + node.delta = -0.001; + } else if (node.dy <= 0) { + node.delta = 0.001; + } + if (node.y + 3 * height <= core.__PIXELS__) { + node.y += 4 * height; + while (node.y > 0) node.y -= height; + } + else if (node.y >= 0) { + node.y -= height; + } + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 3; ++j) { + if (node.x + (i + 1) * width <= 0 || node.x + i * width >= core.__PIXELS__ + || node.y + (j + 1) * height <= 0 || node.y + j * height >= core.__PIXELS__) + continue; + core.drawImage('weather', image, node.x + i * width, node.y + j * height); + } + } + core.setAlpha('weather', 1); + core.animateFrame.weather.time = timestamp; +} + +control.prototype._animationFrame_weather_sun = function (timestamp, level) { + if (timestamp - core.animateFrame.weather.time < 30) return; + var node = core.animateFrame.weather.nodes[0]; + var opacity = node.opacity + node.delta; + if (opacity > level / 10 + 0.3 || opacity < level / 10 - 0.3) + node.delta = -node.delta; + node.opacity = opacity; + core.setOpacity('weather', core.clamp(opacity, 0, 1)); + core.animateFrame.weather.time = timestamp; +} + +control.prototype._animateFrame_tip = function (timestamp) { + if (core.animateFrame.tip == null) return; + var tip = core.animateFrame.tip; + if (timestamp - tip.time <= 30) return; + var delta = timestamp - tip.time; + tip.time = timestamp; + + core.setFont('data', "16px Arial"); + core.setTextAlign('data', 'left'); + core.clearMap('data', 0, 0, core.__PIXELS__, 50); + core.ui._drawTip_drawOne(tip); + if (tip.stage == 1) { + tip.opacity += 0.05; + if (tip.opacity >= 0.6) { + tip.stage = 2; + tip.displayTime = 0; + } + } else if (tip.stage == 2) { + tip.displayTime += delta; + if (tip.displayTime >= 1000) tip.stage = 3; + } else tip.opacity -= 0.05; + + if (tip.opacity <= 0) { + core.animateFrame.tip = null; + } +} + +control.prototype._animationFrame_parallelDo = function (timestamp) { + core.control.controldata.parallelDo(timestamp); +} + +// ------ 标题界面的处理 ------ // + +////// 显示游戏开始界面 ////// +control.prototype.showStartAnimate = function (noAnimate, callback) { + this._showStartAnimate_resetDom(); + if (core.flags.startUsingCanvas || noAnimate) + return this._showStartAnimate_finished(core.flags.startUsingCanvas, callback); + core.hideWithAnimate(core.dom.startTop, 20, function () { + core.control._showStartAnimate_finished(false, callback); + }); +} + +control.prototype._showStartAnimate_resetDom = function () { + core.dom.startPanel.style.opacity = 1; + core.dom.startPanel.style.display = "block"; + core.dom.startTop.style.opacity = 1; + core.dom.startTop.style.display = "block"; + core.dom.startButtonGroup.style.display = 'none'; + core.dom.startButtons.style.display = 'block'; + core.dom.levelChooseButtons.style.display = 'none'; + core.status.played = false; + core.clearStatus(); + core.clearMap('all'); + core.dom.musicBtn.style.display = 'block'; + core.setMusicBtn(); + // 重置音量 + core.events.setVolume(1, 0); + core.updateStatusBar(); +} + +control.prototype._showStartAnimate_finished = function (start, callback) { + core.dom.startTop.style.display = 'none'; + core.dom.startButtonGroup.style.display = 'block'; + main.selectedButton = null; + main.selectButton(0); + if (start) core.startGame(); + if (callback) callback(); +} + +////// 隐藏游戏开始界面 ////// +control.prototype.hideStartAnimate = function (callback) { + core.hideWithAnimate(core.dom.startPanel, 20, callback); +} + +////// 游戏是否已经开始 ////// +control.prototype.isPlaying = function () { + return core.status.played; +} + +////// 清除游戏状态和数据 ////// +control.prototype.clearStatus = function () { + // 停止各个Timeout和Interval + for (var i in core.timeout) { + clearTimeout(core.timeout[i]); + core.timeout[i] = null; + } + for (var i in core.interval) { + clearInterval(core.interval[i]); + core.interval[i] = null; + } + core.status = {}; + core.clearStatusBar(); + core.deleteAllCanvas(); + core.status.played = false; +} + +control.prototype._initStatistics = function (totalTime) { + if (!core.isset(core.status.hero.statistics)) + core.status.hero.statistics = { + 'totalTime': totalTime, + 'currTime': 0, + 'hp': 0, + "battle": 0, + 'money': 0, + 'exp': 0, + 'battleDamage': 0, + 'poisonDamage': 0, + 'extraDamage': 0, + 'vampireExtraLoss': 0, + 'damagePerFloor': {}, + 'moveDirectly': 0, + 'ignoreSteps': 0, + 'oneDefEffect': 0, + 'oneMdefEffect': 0, + } +} + +/** + * @param {'battleDamage'|'poisonDamage'|'extraDamage'|'vampireExtraLoss'} key + * @param {number} value + * @param {string} [floorId] + */ +control.prototype.addFloorStatistics = function (key, value, floorId) { + if (!floorId) floorId = core.status.floorId; + const statistics = core.status.hero.statistics; + if (key !== 'battleDamage' && key !== 'poisonDamage' && key !== 'extraDamage' && + key !== 'vampireExtraLoss' + ) return; + if (!statistics.damagePerFloor) { + statistics.damagePerFloor = {}; + } + if (!statistics.damagePerFloor[floorId]) { + statistics.damagePerFloor[floorId] = {}; + } + if (!statistics.damagePerFloor[floorId][key]) { + statistics.damagePerFloor[floorId][key] = 0; + } + statistics.damagePerFloor[floorId][key] += value; +} + +// ------ 自动寻路,人物行走 ------ // + +////// 清除自动寻路路线 ////// +control.prototype.clearAutomaticRouteNode = function (x, y) { + core.clearMap('route', x * 32 + 5 - core.status.automaticRoute.offsetX, y * 32 + 5 - core.status.automaticRoute.offsetY, 27, 27); +} + +////// 停止自动寻路操作 ////// +control.prototype.stopAutomaticRoute = function () { + if (!core.status.played) return; + core.status.automaticRoute.autoHeroMove = false; + core.status.automaticRoute.autoStep = 0; + core.status.automaticRoute.destStep = 0; + core.status.automaticRoute.movedStep = 0; + core.status.automaticRoute.autoStepRoutes = []; + core.status.automaticRoute.destX = null; + core.status.automaticRoute.destY = null; + core.status.automaticRoute.lastDirection = null; + core.status.heroStop = true; + if (core.status.automaticRoute.moveStepBeforeStop.length == 0) + core.deleteCanvas('route'); +} + +////// 保存剩下的寻路,并停止 ////// +control.prototype.saveAndStopAutomaticRoute = function () { + var automaticRoute = core.status.automaticRoute; + if (automaticRoute.moveStepBeforeStop.length == 0) { + automaticRoute.moveStepBeforeStop = automaticRoute.autoStepRoutes.slice(automaticRoute.autoStep - 1); + if (automaticRoute.moveStepBeforeStop.length >= 1) + automaticRoute.moveStepBeforeStop[0].step -= automaticRoute.movedStep; + } + this.stopAutomaticRoute(); +} + +////// 继续剩下的自动寻路操作 ////// +control.prototype.continueAutomaticRoute = function () { + // 此函数只应由events.afterOpenDoor和events.afterBattle调用 + var moveStep = core.status.automaticRoute.moveStepBeforeStop; + //core.status.automaticRoute.moveStepBeforeStop = []; + if (moveStep.length === 0 || (moveStep.length === 1 && moveStep[0].step === 1)) { + core.status.automaticRoute.moveStepBeforeStop = []; + } + else { + core.setAutoHeroMove(moveStep); + } +} + +////// 清空剩下的自动寻路列表 ////// +control.prototype.clearContinueAutomaticRoute = function (callback) { + core.deleteCanvas('route'); + core.status.automaticRoute.moveStepBeforeStop = []; + if (callback) callback(); +} + +////// 设置自动寻路路线 ////// +control.prototype.setAutomaticRoute = function (destX, destY, stepPostfix) { + if (!core.status.played || core.status.lockControl) return; + if (this._setAutomaticRoute_isMoving(destX, destY)) return; + if (this._setAutomaticRoute_isTurning(destX, destY, stepPostfix)) return; + if (this._setAutomaticRoute_clickMoveDirectly(destX, destY, stepPostfix)) return; + // 找寻自动寻路路线 + var moveStep = core.automaticRoute(destX, destY); + if (moveStep.length == 0 && (destX != core.status.hero.loc.x || destY != core.status.hero.loc.y || stepPostfix.length == 0)) + return; + moveStep = moveStep.concat(stepPostfix); + core.status.automaticRoute.destX = destX; + core.status.automaticRoute.destY = destY; + this._setAutomaticRoute_drawRoute(moveStep); + this._setAutomaticRoute_setAutoSteps(moveStep); + // 立刻移动 + core.setAutoHeroMove(); +} + +control.prototype._setAutomaticRoute_isMoving = function (destX, destY) { + if (core.status.automaticRoute.autoHeroMove) { + var lastX = core.status.automaticRoute.destX, lastY = core.status.automaticRoute.destY; + core.stopAutomaticRoute(); + // 双击瞬移 + if (lastX == destX && lastY == destY) { + core.status.automaticRoute.moveDirectly = true; + setTimeout(function () { + if (core.status.automaticRoute.moveDirectly && core.status.heroMoving == 0) { + core.control.tryMoveDirectly(destX, destY); + } + core.status.automaticRoute.moveDirectly = false; + }, core.values.moveSpeed); + } + return true; + } + return false; +} + +control.prototype._setAutomaticRoute_isTurning = function (destX, destY, stepPostfix) { + if (destX == core.status.hero.loc.x && destY == core.status.hero.loc.y && stepPostfix.length == 0) { + if (core.timeout.turnHeroTimeout == null) { + var routeLength = core.status.route.length; + core.timeout.turnHeroTimeout = setTimeout(function () { + if (core.status.route.length == routeLength) core.turnHero(); + clearTimeout(core.timeout.turnHeroTimeout); + core.timeout.turnHeroTimeout = null; + }, 250); + } + else { + clearTimeout(core.timeout.turnHeroTimeout); + core.timeout.turnHeroTimeout = null; + core.getNextItem(); + } + return true; + } + if (core.timeout.turnHeroTimeout != null) return true; + return false; +} + +control.prototype._setAutomaticRoute_clickMoveDirectly = function (destX, destY, stepPostfix) { + // 单击瞬间移动 + if (core.status.heroStop && core.status.heroMoving == 0) { + if (stepPostfix.length <= 1 && !core.hasFlag('__noClickMove__') && core.control.tryMoveDirectly(destX, destY)) + return true; + } + return false; +} + +control.prototype._setAutomaticRoute_drawRoute = function (moveStep) { + // 计算绘制区域的宽高,并尽可能小的创建route层 + var sx = core.bigmap.width * 32, sy = core.bigmap.height * 32, dx = 0, dy = 0; + moveStep.forEach(function (t) { + sx = Math.min(sx, t.x * 32); dx = Math.max(dx, t.x * 32); + sy = Math.min(sy, t.y * 32); dy = Math.max(dy, t.y * 32); + }); + core.status.automaticRoute.offsetX = sx; + core.status.automaticRoute.offsetY = sy; + var ctx = core.createCanvas('route', sx - core.bigmap.offsetX, sy - core.bigmap.offsetY, dx - sx + 32, dy - sy + 32, 95); + ctx.fillStyle = '#bfbfbf'; + ctx.strokeStyle = '#bfbfbf'; + ctx.lineWidth = 8; + for (var m = 0; m < moveStep.length; m++) { + if (m == moveStep.length - 1) { + ctx.fillRect(moveStep[m].x * 32 + 10 - sx, moveStep[m].y * 32 + 10 - sy, 12, 12); + } + else { + ctx.beginPath(); + var cx = moveStep[m].x * 32 + 16 - sx, cy = moveStep[m].y * 32 + 16 - sy; + var currDir = moveStep[m].direction, nextDir = moveStep[m + 1].direction; + ctx.moveTo(cx - core.utils.scan[currDir].x * 11, cy - core.utils.scan[currDir].y * 11); + ctx.lineTo(cx, cy); + ctx.lineTo(cx + core.utils.scan[nextDir].x * 11, cy + core.utils.scan[nextDir].y * 11); + ctx.stroke(); + } + } +} + +control.prototype._setAutomaticRoute_setAutoSteps = function (moveStep) { + // 路线转autoStepRoutes + var step = 0, currStep = null; + moveStep.forEach(function (t) { + var dir = t.direction; + if (currStep == null || currStep == dir) + step++; + else { + core.status.automaticRoute.autoStepRoutes.push({ 'direction': currStep, 'step': step }); + step = 1; + } + currStep = dir; + }); + core.status.automaticRoute.autoStepRoutes.push({ 'direction': currStep, 'step': step }); +} + +////// 设置勇士的自动行走路线 ////// +control.prototype.setAutoHeroMove = function (steps) { + steps = steps || core.status.automaticRoute.autoStepRoutes; + if (steps.length == 0) return; + core.status.automaticRoute.autoStepRoutes = steps; + core.status.automaticRoute.autoHeroMove = true; + core.status.automaticRoute.autoStep = 1; + core.status.automaticRoute.destStep = steps[0].step; + core.moveHero(steps[0].direction); +} + +////// 设置行走的效果动画 ////// +control.prototype.setHeroMoveInterval = function (callback) { + if (core.status.heroMoving > 0) return; + if (core.status.replay.speed == 24) { + if (callback) callback(); + return; + } + + core.status.heroMoving = 1; + + var toAdd = 1; + if (core.status.replay.speed > 3) toAdd = 2; + if (core.status.replay.speed > 6) toAdd = 4; + if (core.status.replay.speed > 12) toAdd = 8; + + core.interval.heroMoveInterval = window.setInterval(function () { + core.status.heroMoving += toAdd; + if (core.status.heroMoving >= 8) { + clearInterval(core.interval.heroMoveInterval); + core.status.heroMoving = 0; + if (callback) callback(); + } + }, core.values.moveSpeed / 8 * toAdd / core.status.replay.speed); +} + +////// 每移动一格后执行的事件 ////// +control.prototype.moveOneStep = function (callback) { + return this.controldata.moveOneStep(callback); +} + +////// 实际每一步的行走过程 ////// +control.prototype.moveAction = function (callback) { + if (core.status.heroMoving > 0) return; + const nextX = core.nextX(), nextY = core.nextY(); + const noPass = core.noPass(nextX, nextY); + const canMove = core.canMoveHero(); + // 下一个点如果不可通行 + if (noPass || !canMove) return this._moveAction_noPass(canMove, callback); + // 下一个点如果可通行 + // 满足条件时触发一次自动存档 + if (!core.isReplaying()) { + const dir = core.getHeroLoc('direction'); + let needAutoSave = false; + if (core.getLocalStorage("autoSaveBeforeUseItem") && + core.onSki(core.maps.getBgNumber(nextX, nextY))) { + needAutoSave = true; + } + else if (core.getLocalStorage('autoSaveBeforePickItem') && + core.getBlockCls(nextX, nextY) === 'items') { + needAutoSave = true; + } + if (needAutoSave) { + core.status.route.push(`turn:${dir}`); // 保存触发转向后的方向 + core.control.autosave(); + } + } + this._moveAction_moving(callback); +} + +control.prototype._moveAction_noPass = function (canMove, callback) { + core.status.route.push(core.getHeroLoc('direction')); + core.status.automaticRoute.moveStepBeforeStop = []; + core.status.automaticRoute.lastDirection = core.getHeroLoc('direction'); + if (canMove) core.trigger(core.nextX(), core.nextY()); + core.drawHero(); + + if (core.status.automaticRoute.moveStepBeforeStop.length == 0) { + core.clearContinueAutomaticRoute(); + core.stopAutomaticRoute(); + } + if (callback) callback(); +} + +control.prototype._moveAction_moving = function (callback) { + core.setHeroMoveInterval(function () { + core.setHeroLoc('x', core.nextX(), true); + core.setHeroLoc('y', core.nextY(), true); + + var direction = core.getHeroLoc('direction'); + core.control._moveAction_popAutomaticRoute(); + core.status.route.push(direction); + + core.moveOneStep(); + core.checkRouteFolding(); + if (callback) callback(); + }); +} + +control.prototype._moveAction_popAutomaticRoute = function () { + var automaticRoute = core.status.automaticRoute; + // 检查自动寻路是否被弹出 + if (automaticRoute.autoHeroMove) { + automaticRoute.movedStep++; + automaticRoute.lastDirection = core.getHeroLoc('direction'); + if (automaticRoute.destStep == automaticRoute.movedStep) { + if (automaticRoute.autoStep == automaticRoute.autoStepRoutes.length) { + core.clearContinueAutomaticRoute(); + core.stopAutomaticRoute(); + } + else { + automaticRoute.movedStep = 0; + automaticRoute.destStep = automaticRoute.autoStepRoutes[automaticRoute.autoStep].step; + core.setHeroLoc('direction', automaticRoute.autoStepRoutes[automaticRoute.autoStep].direction); + core.status.automaticRoute.autoStep++; + } + } + } +} + +////// 让勇士开始移动 ////// +control.prototype.moveHero = function (direction, callback) { + // 如果正在移动,直接return + if (core.status.heroMoving != 0) return; + if (core.isset(direction)) { + if (core.status.hero.loc.direction != direction) { + core.status.dirBeforeMove = core.status.hero.loc.direction; // 记录转向前的方向,如果下一步自动保存要还原到这个方向 + } + core.setHeroLoc('direction', direction); + } + + if (callback) return this.moveAction(callback); + this._moveHero_moving(); +} + +control.prototype._moveHero_moving = function () { + // ------ 我已经看不懂这个函数了,反正好用就行23333333 + core.status.heroStop = false; + core.status.automaticRoute.moveDirectly = false; + var move = function () { + if (!core.status.heroStop) { + if (core.hasFlag('debug') && core.status.ctrlDown) { + if (core.status.heroMoving != 0) return; + // 检测是否穿出去 + var nx = core.nextX(), ny = core.nextY(); + if (nx < 0 || nx >= core.bigmap.width || ny < 0 || ny >= core.bigmap.height) return; + core.eventMoveHero([core.getHeroLoc('direction')], core.values.moveSpeed, move); + } + else if (core.hasFlag('debug_wallHacking')) { + if (core.status.heroMoving != 0) return; + // 检测是否穿出去 + var nx = core.nextX(), ny = core.nextY(); + if (nx < 0 || nx >= core.bigmap.width || ny < 0 || ny >= core.bigmap.height) return; + core.eventMoveHero([core.getHeroLoc('direction')], core.values.moveSpeed); // 取消递归,方便调试时单击走一步的情形 + } + else { + core.moveAction(); + setTimeout(move, 50); + } + } + } + move(); +} + +////// 当前是否正在移动 ////// +control.prototype.isMoving = function () { + return !core.status.heroStop || core.status.heroMoving > 0; +} + +////// 停止勇士的一切行动,等待勇士行动结束后,再执行callback ////// +control.prototype.waitHeroToStop = function (callback) { + var lastDirection = core.status.automaticRoute.lastDirection; + core.stopAutomaticRoute(); + core.clearContinueAutomaticRoute(); + if (callback) { + core.status.replay.animate = true; + core.lockControl(); + core.status.automaticRoute.moveDirectly = false; + setTimeout(function () { + core.status.replay.animate = false; + if (core.isset(lastDirection)) + core.setHeroLoc('direction', lastDirection); + core.drawHero(); + callback(); + }, core.status.replay.speed == 24 ? 1 : 30); + } +} + +////// 转向 ////// +control.prototype.turnHero = function (direction) { + if (direction) { + core.setHeroLoc('direction', direction); + core.drawHero(); + core.status.route.push("turn:" + direction); + return; + } + core.setHeroLoc('direction', core.turnDirection(':right')); + core.drawHero(); + core.status.route.push("turn"); + core.checkRouteFolding(); +} + +////// 瞬间移动 ////// +control.prototype.moveDirectly = function (destX, destY, ignoreSteps) { + return this.controldata.moveDirectly(destX, destY, ignoreSteps); +} + +////// 尝试瞬间移动 ////// +control.prototype.tryMoveDirectly = function (destX, destY) { + if (this.nearHero(destX, destY)) return false; + var canMoveArray = core.maps.generateMovableArray(); + var dirs = [[destX, destY], [destX - 1, destY, "right"], [destX, destY - 1, "down"], [destX, destY + 1, "up"], [destX + 1, destY, "left"]]; + var canMoveDirectlyArray = core.canMoveDirectlyArray(dirs, canMoveArray); + + for (var i = 0; i < dirs.length; ++i) { + var d = dirs[i], dx = d[0], dy = d[1], dir = d[2]; + if (dx < 0 || dx >= core.bigmap.width || dy < 0 || dy >= core.bigmap.height) continue; + if (dir && !core.inArray(canMoveArray[dx][dy], dir)) continue; + if (canMoveDirectlyArray[i] < 0) continue; + if (core.control.moveDirectly(dx, dy, canMoveDirectlyArray[i])) { + if (dir) core.moveHero(dir, function () { }); + return true; + } + } + return false; +} + +////// 绘制勇士 ////// +control.prototype.drawHero = function (status, offset, frame) { + if (!core.isPlaying() || !core.status.floorId || core.status.gameOver) return; + var x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), direction = core.getHeroLoc('direction'); + status = status || 'stop'; + if (!offset) offset = 0; + + var way = core.utils.scan2[direction]; + var dx = way.x, dy = way.y; + var offsetX = typeof offset == 'number' ? dx * offset : (offset.x || 0); + var offsetY = typeof offset == 'number' ? dy * offset : (offset.y || 0); + offset = { x: offsetX, y: offsetY, offset: offset }; + + core.clearAutomaticRouteNode(x + dx, y + dy); + core.clearMap('hero'); + core.status.heroCenter.px = 32 * x + offsetX + 16; + core.status.heroCenter.py = 32 * y + offsetY + 32 - core.material.icons.hero.height / 2; + + // 重置hero层画布 + core.setGameCanvasTranslate('hero', 0, 0); + delete core.canvas.hero._px; + delete core.canvas.hero._py; + core.status.preview.enabled = false; + if (!core.hasFlag('__lockViewport__')) { + this._drawHero_updateViewport(x, y, offset); + } + + this._drawHero_draw(direction, x, y, status, offset, frame); +} + +control.prototype._drawHero_updateViewport = function (x, y, offset) { + core.bigmap.offsetX = core.clamp((x - core.__HALF_SIZE__) * 32 + offset.x, 0, 32 * core.bigmap.width - core.__PIXELS__); + core.bigmap.offsetY = core.clamp((y - core.__HALF_SIZE__) * 32 + offset.y, 0, 32 * core.bigmap.height - core.__PIXELS__); + core.control.updateViewport(); +} + +control.prototype._drawHero_draw = function (direction, x, y, status, offset, frame) { + offset = offset || { x: 0, y: 0, offset: 0, px: 0, py: 0 }; + var opacity = core.setAlpha('hero', core.getFlag('__heroOpacity__', 1)) + this._drawHero_getDrawObjs(direction, x, y, status, offset).forEach(function (block) { + core.drawImage('hero', block.img, (block.heroIcon[block.status] + (frame || 0)) % 4 * block.width, + block.heroIcon.loc * block.height, block.width, block.height, + block.posx + (32 - block.width) / 2, block.posy + 32 - block.height, block.width, block.height); + }); + core.setAlpha('hero', opacity); +} + +control.prototype._drawHero_getDrawObjs = function (direction, x, y, status, offset) { + var heroIconArr = core.material.icons.hero, drawObjs = [], index = 0; + drawObjs.push({ + "img": core.material.images.hero, + "width": core.material.icons.hero.width || 32, + "height": core.material.icons.hero.height, + "heroIcon": heroIconArr[direction], + "posx": x * 32 - core.bigmap.offsetX + offset.x, + "posy": y * 32 - core.bigmap.offsetY + offset.y, + "status": status, + "index": index++, + }); + if (typeof offset.offset == 'number') { + core.status.hero.followers.forEach(function (t) { + drawObjs.push({ + "img": core.material.images.images[t.name], + "width": core.material.images.images[t.name].width / 4, + "height": core.material.images.images[t.name].height / 4, + "heroIcon": heroIconArr[t.direction], + "posx": 32 * t.x - core.bigmap.offsetX + (t.stop ? 0 : core.utils.scan2[t.direction].x * Math.abs(offset.offset)), + "posy": 32 * t.y - core.bigmap.offsetY + (t.stop ? 0 : core.utils.scan2[t.direction].y * Math.abs(offset.offset)), + "status": t.stop ? "stop" : status, + "index": index++ + }); + }); + } + return drawObjs.sort(function (a, b) { + return a.posy == b.posy ? b.index - a.index : a.posy - b.posy; + }); +} + +control.prototype.setHeroOpacity = function (opacity, moveMode, time, callback) { + time = time || 0; + if (time == 0) { + core.setFlag('__heroOpacity__', opacity); + core.drawHero(); + if (callback) callback(); + return; + } + time /= Math.max(core.status.replay.speed, 1) + + var fromOpacity = core.getFlag('__heroOpacity__', 1); + var step = 0, steps = parseInt(time / 10); + if (steps <= 0) steps = 1; + var moveFunc = core.applyEasing(moveMode); + + var animate = setInterval(function () { + step++; + core.setFlag('__heroOpacity__', fromOpacity + (opacity - fromOpacity) * moveFunc(step / steps)); + core.drawHero(); + if (step == steps) { + delete core.animateFrame.asyncId[animate]; + clearInterval(animate); + if (callback) callback(); + } + }, 10); + + core.animateFrame.lastAsyncId = animate; + core.animateFrame.asyncId[animate] = callback; +} + +// ------ 画布、位置、阻激夹域,显伤 ------ // + +////// 设置画布偏移 +control.prototype.setGameCanvasTranslate = function (canvas, x, y) { + var c = core.dom.gameCanvas[canvas]; + x = x * core.domStyle.scale; + y = y * core.domStyle.scale; + c.style.transform = 'translate(' + x + 'px,' + y + 'px)'; + c.style.webkitTransform = 'translate(' + x + 'px,' + y + 'px)'; + c.style.OTransform = 'translate(' + x + 'px,' + y + 'px)'; + c.style.MozTransform = 'translate(' + x + 'px,' + y + 'px)'; + if (main.mode === 'editor' && editor.isMobile) { + c.style.transform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; + c.style.webkitTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; + c.style.OTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; + c.style.MozTransform = 'translate(' + (x / core.__PIXELS__ * 96) + 'vw,' + (y / core.__PIXELS__ * 96) + 'vw)'; + } +}; + +////// 加减画布偏移 +control.prototype.addGameCanvasTranslate = function (x, y) { + for (var ii = 0, canvas; canvas = core.dom.gameCanvas[ii]; ii++) { + var id = canvas.getAttribute('id'); + if (id == 'ui' || id == 'data') continue; // UI层和data层不移动 + var offsetX = x, offsetY = y; + if (core.bigmap.canvas.indexOf(id) >= 0) { + if (core.bigmap.v2) { + offsetX -= (core.bigmap.offsetX - 32 * core.bigmap.posX) + 32; + offsetY -= (core.bigmap.offsetY - 32 * core.bigmap.posY) + 32; + } else { + offsetX -= core.bigmap.offsetX; + offsetY -= core.bigmap.offsetY; + } + } + core.control.setGameCanvasTranslate(id, offsetX, offsetY); + } +} + +////// 更新视野范围 ////// +control.prototype.updateViewport = function () { + // 当前是否应该重绘? + if (core.bigmap.v2) { + if (core.bigmap.offsetX >= core.bigmap.posX * 32 + 32 + || core.bigmap.offsetX <= core.bigmap.posX * 32 - 32 + || core.bigmap.offsetY >= core.bigmap.posY * 32 + 32 + || core.bigmap.offsetY <= core.bigmap.posY * 32 - 32) { + core.bigmap.posX = parseInt(core.bigmap.offsetX / 32); + core.bigmap.posY = parseInt(core.bigmap.offsetY / 32); + core.redrawMap(); + } + } else { + core.bigmap.posX = core.bigmap.posY = 0; + } + var offsetX = core.bigmap.v2 ? -(core.bigmap.offsetX - 32 * core.bigmap.posX) - 32 : -core.bigmap.offsetX; + var offsetY = core.bigmap.v2 ? -(core.bigmap.offsetY - 32 * core.bigmap.posY) - 32 : -core.bigmap.offsetY; + + core.bigmap.canvas.forEach(function (cn) { + core.control.setGameCanvasTranslate(cn, offsetX, offsetY); + }); + // ------ 路线 + core.relocateCanvas('route', core.status.automaticRoute.offsetX - core.bigmap.offsetX, core.status.automaticRoute.offsetY - core.bigmap.offsetY); + // ------ 所有的大怪物也都需要重定位 + for (var one in core.dymCanvas) { + if (one.startsWith('_bigImage_')) { + var ox = core.dymCanvas[one].canvas.getAttribute('_ox'); + var oy = core.dymCanvas[one].canvas.getAttribute('_oy'); + if (ox != null && oy != null) { + core.relocateCanvas(one, parseInt(ox) - core.bigmap.offsetX, parseInt(oy) - core.bigmap.offsetY); + } + } + } + +} + +////// 设置视野范围 ////// +control.prototype.setViewport = function (px, py) { + var originOffsetX = core.bigmap.offsetX, originOffsetY = core.bigmap.offsetY; + core.bigmap.offsetX = core.clamp(px, 0, 32 * core.bigmap.width - core.__PIXELS__); + core.bigmap.offsetY = core.clamp(py, 0, 32 * core.bigmap.height - core.__PIXELS__); + this.updateViewport(); + // ------ hero层也需要! + var px = parseFloat(core.canvas.hero._px) || 0; + var py = parseFloat(core.canvas.hero._py) || 0; + px += originOffsetX - core.bigmap.offsetX; + py += originOffsetY - core.bigmap.offsetY; + core.control.setGameCanvasTranslate('hero', px, py); + core.canvas.hero._px = px; + core.canvas.hero._py = py; +} + +////// 移动视野范围 ////// +control.prototype.moveViewport = function (x, y, moveMode, time, callback) { + time = time || 0; + time /= Math.max(core.status.replay.speed, 1) + var per_time = 10, step = 0, steps = parseInt(time / per_time); + if (steps <= 0) { + this.setViewport(32 * x, 32 * y); + if (callback) callback(); + return; + } + var px = core.clamp(32 * x, 0, 32 * core.bigmap.width - core.__PIXELS__); + var py = core.clamp(32 * y, 0, 32 * core.bigmap.height - core.__PIXELS__); + var cx = core.bigmap.offsetX; + var cy = core.bigmap.offsetY; + var moveFunc = core.applyEasing(moveMode); + + var animate = window.setInterval(function () { + step++; + core.setViewport(cx + moveFunc(step / steps) * (px - cx), cy + moveFunc(step / steps) * (py - cy)); + if (step == steps) { + delete core.animateFrame.asyncId[animate]; + clearInterval(animate); + core.setViewport(px, py); + if (callback) callback(); + } + }, per_time); + + core.animateFrame.lastAsyncId = animate; + core.animateFrame.asyncId[animate] = callback; +} + +////// 获得勇士面对位置的x坐标 ////// +control.prototype.nextX = function (n) { + if (n == null) n = 1; + return core.getHeroLoc('x') + core.utils.scan[core.getHeroLoc('direction')].x * n; +} + +////// 获得勇士面对位置的y坐标 ////// +control.prototype.nextY = function (n) { + if (n == null) n = 1; + return core.getHeroLoc('y') + core.utils.scan[core.getHeroLoc('direction')].y * n; +} + +////// 某个点是否在勇士旁边 ////// +control.prototype.nearHero = function (x, y, n) { + if (n == null) n = 1; + return Math.abs(x - core.getHeroLoc('x')) + Math.abs(y - core.getHeroLoc('y')) <= n; +} + +////// 聚集跟随者 ////// +control.prototype.gatherFollowers = function () { + var x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), dir = core.getHeroLoc('direction'); + core.status.hero.followers.forEach(function (t) { + t.x = x; + t.y = y; + t.stop = true; + t.direction = dir; + }); +} + +////// 更新跟随者坐标 ////// +control.prototype.updateFollowers = function () { + core.status.hero.followers.forEach(function (t) { + if (!t.stop) { + t.x += core.utils.scan2[t.direction].x; + t.y += core.utils.scan2[t.direction].y; + } + }) + + var nowx = core.getHeroLoc('x'), nowy = core.getHeroLoc('y'); + core.status.hero.followers.forEach(function (t) { + t.stop = true; + var dx = nowx - t.x, dy = nowy - t.y; + for (var dir in core.utils.scan2) { + if (core.utils.scan2[dir].x == dx && core.utils.scan2[dir].y == dy) { + t.stop = false; + t.direction = dir; + } + } + nowx = t.x; nowy = t.y; + }) +} + +////// 瞬移更新跟随者坐标 ////// +control.prototype._moveDirectyFollowers = function (x, y) { + var route = core.automaticRoute(x, y); + if (route.length == 0) route = [{ x: x, y: y, direction: core.getHeroLoc('direction') }]; + + var nowx = x, nowy = y; + for (var i = 0; i < core.status.hero.followers.length; ++i) { + var t = core.status.hero.followers[i]; + var index = route.length - i - 2; + if (index < 0) index = 0; + t.stop = true; + t.x = route[index].x; + t.y = route[index].y; + t.direction = route[index].direction; + var dx = nowx - t.x, dy = nowy - t.y; + for (var dir in core.utils.scan2) { + if (core.utils.scan2[dir].x == dx && core.utils.scan2[dir].y == dy) { + t.stop = false; + t.direction = dir; + } + } + nowx = t.x; nowy = t.y; + } +} + +////// 更新领域、夹击、阻击的伤害地图 ////// +control.prototype.updateCheckBlock = function (floorId) { + return this.controldata.updateCheckBlock(floorId); +} + +////// 检查并执行领域、夹击、阻击事件 ////// + +control.prototype.checkBlock = function () { + const floorId = core.status.floorId; + const x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), loc = x + "," + y; + const damage = core.status.checkBlock.damage[loc]; + if (damage) { + core.status.hero.hp -= damage; + const text = (Object.keys(core.status.checkBlock.type[loc] || {}).join(",")) || "伤害"; + core.drawTip("受到" + text + damage + "点"); + core.drawHeroAnimate("zone"); + this._checkBlock_disableQuickShop(); + core.status.hero.statistics.extraDamage += damage; + if (!core.status.hero.statistics.damagePerFloor) { + core.status.hero.statistics.damagePerFloor = {}; + } + if (core.control.addFloorStatistics) { + core.control.addFloorStatistics("extraDamage", damage, floorId); + } + if (core.status.hero.hp <= 0) { + core.status.hero.hp = 0; + core.updateStatusBar(false, true); + core.events.lose(); + return; + } else { + core.updateStatusBar(false, true); + } + } + let actions = []; + + const ambushAction = this._checkBlock_ambush(core.status.checkBlock.ambush[loc]); + if (ambushAction.length > 0) core.push(actions, ambushAction); + + const repulseAction = this._checkBlock_repulse(core.status.checkBlock.repulse[loc]); + if (repulseAction.length > 0) core.push(actions, repulseAction); + + // 追猎需要等待阻击完成,避免发生碰撞导致怪物消失 先清理四周追猎,追猎移动,再清理一轮四周的追猎 + const currChase = core.status.checkBlock.chase[loc]; + if (currChase && currChase.length > 0) { + if (actions.length === 0) { + const adjacentChase = core.checkBlock_adjacentChase(); + if (adjacentChase && adjacentChase.length > 0) core.push(actions, adjacentChase); + } + else core.push(actions, { "type": "function", "async": true, "function": "function(){core.checkBlock_adjacentChase(true);}" }); + } + const chaseAction = this._checkBlock_chase(currChase); + if (chaseAction.length > 0) core.push(actions, chaseAction); + if (currChase && currChase.length > 0) { + core.push(actions, { "type": "function", "async": true, "function": "function(){core.checkBlock_adjacentChase(true);}" }); + } + if (ambushAction.length > 0 && core.getLocalStorage("autoSaveBeforeUseItem")) { + core.push(actions, { "type": "autoSave" }); // 捕捉触发后自动存档 + } + + const autoClear = core.plugin.autoClear; + if (autoClear) { // 检查autoClear的存在性,防止接档出现bug + if (actions.length > 0) { + actions.push({ + "type": "function", "async": true, + "function": "function(){core.plugin.autoClear();core.doAction();}" + }); // 注意要放在事件队列最后。经测试放在insertAction回调当中仍有小概率先于阻击结算触发,原因不明 + core.insertAction(actions, x, y); + } + else autoClear(); // 阻击结算后执行自动清怪 + } + else { + if (actions.length > 0) core.insertAction(actions, x, y); + } +} + +control.prototype.checkBlock_adjacentChase = function (inAction) { + const { x: hx, y: hy } = core.status.hero.loc; + const scan = core.utils.scan; + const actions = []; + + for (const dir in scan) { + const [nx, ny] = [hx + scan[dir].x, hy + scan[dir].y]; + const blockId = core.getBlockId(nx, ny); + const special = core.getEnemyValue(blockId, 'special', nx, ny); + if (core.hasSpecial(special, 28)) { + actions.push({ + "type": "function", "function": "function() { " + + "core.battle('" + blockId + "', " + nx + "," + ny + ", true, core.doAction); " + + "}", "async": true + }); + } + } + if (!inAction) return actions; + if (actions.length > 0) core.insertAction(actions); + core.doAction(); +} + +control.prototype._checkBlock_disableQuickShop = function () { + // 禁用快捷商店 + if (core.flags.disableShopOnDamage) { + Object.keys(core.status.shops).forEach(function (shopId) { + core.setShopVisited(shopId, false); + }); + } +} + +////// 阻击 ////// +control.prototype._checkBlock_repulse = function (repulse) { + if (!repulse || repulse.length == 0) return []; + var actions = []; + repulse.forEach(function (t) { + actions.push({ "type": "move", "loc": [t[0], t[1]], "steps": [t[3]], "time": 80, "keep": true, "async": true }); + }); + if (actions.length > 0) actions.push({ "type": "waitAsync" }); + return actions; +} + +////// 捕捉 ////// +control.prototype._checkBlock_ambush = function (ambush) { + if (!ambush || ambush.length == 0) return []; + // 捕捉效果 + let actions = []; + ambush.forEach(function (t) { + actions.push({ "type": "move", "loc": [t[0], t[1]], "steps": [t[3]], "time": 100, "keep": false, "async": true }); + }); + if (actions.length > 0) actions.push({ "type": "waitAsync" }); + // 强制战斗 + ambush.forEach(function (t) { + actions.push({ + "type": "function", "function": "function() { " + + "core.battle('" + t[2] + "', " + t[0] + "," + t[1] + ", true, core.doAction); " + + "}", "async": true + }); + }); + return actions; +} + +////// 获取追猎的视野和行动可穿越的图块类型 ////// +control.prototype.getChaseType = function () { + let clsList = ['items']; + if (core.flags.chaseThroughEnemy) core.push(clsList, ['enemys', 'enemy48']); + return clsList; +} + +////// 追猎 ////// +control.prototype._checkBlock_chase = function (chase) { + if (!chase || chase.length === 0) return []; + var actions = []; + const { x: hx, y: hy } = core.status.hero.loc; + const reverseDir = { 'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left' }; + chase = chase.sort((a, b) => { + const { x: ax, y: ay } = a; + const { x: bx, y: by } = b; + return Math.abs(ax - hx) + Math.abs(ay - hy) - Math.abs(bx - hx) - Math.abs(by - hy); + }) + chase.forEach((currChaseInfo) => { + const { x, y, dir } = currChaseInfo; + const [aimx, aimy] = [x + core.utils.scan[dir].x, y + core.utils.scan[dir].y]; + if (!(aimx === hx && aimy === hy)) { + actions.push({ + "type": "if", "condition": "!core.getBlock(" + aimx + "," + aimy + ")", + "true": [ + { "type": "move", "loc": [x, y], "time": 80, "keep": true, "async": true, "steps": [dir + ":1"] }, + ], + "false": [ + { "type": "setValue", "name": "flag:chaseAimCls", "value": `core.getBlockCls(${aimx},${aimy})` }, + { + "type": "if", "condition": "core.getChaseType().includes(core.getFlag('chaseAimCls'))", + "true": [ + { + "type": "if", "condition": "[\"enemys\",\"enemy48\"].includes(core.getFlag('chaseAimCls'))", + "true": [ + { + "type": "function", "function": `function () { + core.exchangeBlock(${x}, ${y}, ${aimx}, ${aimy}, '${dir}', 100, null); }` + }, + ], + "false": [ + { "type": "move", "loc": [x, y], "time": 80, "keep": true, "async": true, "steps": [dir + ":1"] }, + { "type": "move", "loc": [aimx, aimy], "time": 80, "keep": true, "async": true, "steps": [reverseDir[dir] + ":1"] }, + ] + }, + { "type": "waitAsync", "excludeAnimates": true }, + ] + }, + { "type": "setValue", "name": "flag:chaseCls", "value": null }, + ] + }); + } + }) + + if (actions.length > 0) actions.push({ "type": "waitAsync" }); + return actions; +} + +////// 更新全地图显伤 ////// +control.prototype.updateDamage = function (floorId, ctx) { + floorId = floorId || core.status.floorId; + if (!floorId || core.status.gameOver || main.mode != 'play') return; + var onMap = ctx == null; + + // 没有怪物手册 + if (!core.hasItem('book')) return; + core.status.damage.posX = core.bigmap.posX; + core.status.damage.posY = core.bigmap.posY; + if (!onMap) { + var width = core.floors[floorId].width, height = core.floors[floorId].height; + // 地图过大的缩略图不绘制显伤 + if (width * height > core.bigmap.threshold) return; + } + this._updateDamage_damage(floorId, onMap); + this._updateDamage_extraDamage(floorId, onMap); + this.drawDamage(ctx); +} + +control.prototype.getEnemyValueString = function(name, blockId, x, y, floorId) { + if (name == null) return { text: null, color: null }; + const colorMap = { + hp: '#55ff55', atk: '#ff5555', + def: '#5555ff', money: '#ffff55', exp: '#ffaa33', + criticalDamage: '#B22222', defDamage: '#1E90FF', + vampireDiff:'#f20c00', + } + switch (name) { + case "damage": + if (core.flags.displayEnemyDamage) { + const damageString = core.enemys.getDamageString(blockId, x, y, floorId); + return { text: damageString.damage, color: damageString.color }; + } + break; + case "critical": + if (core.flags.displayCritical) { + const critical = core.enemys.nextCriticals(blockId, 1, x, y, floorId); + const criticalString = core.formatBigNumber((critical[0] || [])[0], true); + return { text: criticalString == '???' ? '?' : criticalString, color: '#FFFFFF' }; + } + break; + case "hp": + case "atk": + case "def": + case "money": + case "exp": + const value = core.enemys.getEnemyValue(blockId, name, x, y, floorId); + const valueString = core.utils.formatBigNumber(value, 5); + return { text: valueString, color: colorMap[name] }; + case "notBomb": + const notBomb = core.enemys.getEnemyValue(blockId, name, x, y, floorId); + const notBombStr = notBomb ? "b" : ""; + return { text: notBombStr, color: colorMap[name] || "white" }; + case "criticalDamage": + let criticalDamage = 0; + const criticals = core.enemys.nextCriticals(blockId, 1, x, y, floorId); + if (criticals && criticals.length > 0) { + criticalDamage = criticals[0][1]; + criticalDamage = core.formatBigNumber(criticalDamage, 5); + } + return { text: criticalDamage, color: colorMap.criticalDamage }; + case "defDamage": + const defDamage = core.enemys.getDefDamage(blockId, 1, x, y, floorId); + return { text: defDamage, color: colorMap.defDamage }; + case "vampireDiff": + const info = { text: "", color: colorMap.vampireDiff }; + if (!core.enemys.getVampireExtraLoss) return info; + const loss = core.enemys.getVampireExtraLoss(blockId, x, y, floorId); + if (loss != null) info.text = `+${loss}`; + return info; + case "special": + const specialData = core.getLocalStorage('specialIconData', {}); + const special = core.enemys.getEnemyValue(blockId, 'special', x, y, floorId); + const specialArr = core.utils.parseSpecial(special); + let text = ''; + for (const s of specialArr) { + if (specialData[s]) { + text += specialData[s]; + } + } + return { text, color: '#FFFFFF' }; + } +} + +control.prototype.pushDamageData = function(name, px, py, blockId, x, y, floorId) { + if (name == null) return; + const { text, color } = this.getEnemyValueString(name, blockId, x, y, floorId); + core.status.damage.data.push({ text, px, py, color }); +} + +control.prototype._updateDamage_damage = function (floorId, onMap) { + core.status.damage.data = []; + if (!core.flags.displayEnemyDamage && !core.flags.displayExtraDamage) return; + + core.extractBlocks(floorId); + + core.status.maps[floorId].blocks.forEach(function (block) { + const x = block.x, + y = block.y, + blockId = block.event.id; + + // v2优化,只绘制范围内的部分 + if (onMap && core.bigmap.v2) { + if (x < core.bigmap.posX - core.bigmap.extend || x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend + || y < core.bigmap.posY - core.bigmap.extend || y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend) { + return; + } + } + + if (!block.disable && block.event.cls.indexOf('enemy') == 0 && block.event.displayDamage !== false) { + const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} }; + const data = core.getLocalStorage('displayData', defaultData); + + core.control.pushDamageData(data.leftdown[1], 32 * x + 1, 32 * (y + 1) - 1, blockId, x, y, floorId); + core.control.pushDamageData(data.leftdown[2], 32 * x + 1, 32 * (y + 1) - 11, blockId, x, y, floorId); + core.control.pushDamageData(data.leftdown[3], 32 * x + 1, 32 * (y + 1) - 21, blockId, x, y, floorId); + core.control.pushDamageData(data.rightup[1], 32 * x + 22, 32 * (y + 1) - 21, blockId, x, y, floorId); + core.control.pushDamageData(data.rightup[2], 32 * x + 22, 32 * (y + 1) - 11, blockId, x, y, floorId); + core.control.pushDamageData(data.rightup[3], 32 * x + 22, 32 * (y + 1) - 1, blockId, x, y, floorId); + } + }); +} + +control.prototype._updateDamage_extraDamage = function (floorId, onMap) { + core.status.damage.extraData = []; + if (!core.flags.displayExtraDamage) return; + + var width = core.floors[floorId].width, height = core.floors[floorId].height; + var startX = onMap && core.bigmap.v2 ? Math.max(0, core.bigmap.posX - core.bigmap.extend) : 0; + var endX = onMap && core.bigmap.v2 ? Math.min(width, core.bigmap.posX + core.__SIZE__ + core.bigmap.extend + 1) : width; + var startY = onMap && core.bigmap.v2 ? Math.max(0, core.bigmap.posY - core.bigmap.extend) : 0; + var endY = onMap && core.bigmap.v2 ? Math.min(height, core.bigmap.posY + core.__SIZE__ + core.bigmap.extend + 1) : height; + + for (var x = startX; x < endX; x++) { + for (var y = startY; y < endY; y++) { + var alpha = 1; + if (core.noPass(x, y, floorId)) { + if (core.flags.extraDamageType == 2) alpha = 0; + else if (core.flags.extraDamageType == 1) alpha = 0.6; + } + var damage = core.status.checkBlock.damage[x + "," + y] || 0; + if (damage > 0) { // 该点伤害 + damage = core.formatBigNumber(damage, true); + core.status.damage.extraData.push({ text: damage, px: 32 * x + 16, py: 32 * (y + 1) - 14, color: '#ffaa33', alpha: alpha }); + } + else { // 检查捕捉 + if (core.status.checkBlock.ambush[x + "," + y]) { + core.status.damage.extraData.push({ text: '!', px: 32 * x + 16, py: 32 * (y + 1) - 14, color: '#ffaa33', alpha: alpha }); + } + } + } + } +} + +////// 重绘地图显伤 ////// +control.prototype.drawDamage = function (ctx) { + if (core.status.gameOver || !core.status.damage || main.mode != 'play') return; + var onMap = false; + if (ctx == null) { + ctx = core.canvas.damage; + core.clearMap('damage'); + onMap = true; + } + + if (onMap && core.bigmap.v2) { + // 检查是否需要重算... + if (Math.abs(core.bigmap.posX - core.status.damage.posX) >= core.bigmap.extend - 1 + || Math.abs(core.bigmap.posY - core.status.damage.posY) >= core.bigmap.extend - 1) { + return this.updateDamage(); + } + } + return this._drawDamage_draw(ctx, onMap); +} + +control.prototype._drawDamage_draw = function (ctx, onMap) { + if (!core.hasItem('book')) return; + + core.setFont(ctx, "bold 11px Arial"); + core.setTextAlign(ctx, 'left'); + core.status.damage.data.forEach(function (one) { + var px = one.px, py = one.py; + if (onMap && core.bigmap.v2) { + px -= core.bigmap.posX * 32; + py -= core.bigmap.posY * 32; + if (px < -32 * 2 || px > core.__PIXELS__ + 32 || py < -32 || py > core.__PIXELS__ + 32) + return; + } + core.fillBoldText(ctx, one.text, px, py, one.color); + }); + + core.setTextAlign(ctx, 'center'); + core.status.damage.extraData.forEach(function (one) { + var px = one.px, py = one.py; + if (onMap && core.bigmap.v2) { + px -= core.bigmap.posX * 32; + py -= core.bigmap.posY * 32; + if (px < -32 || px > core.__PIXELS__ + 32 || py < -32 || py > core.__PIXELS__ + 32) + return; + } + var alpha = core.setAlpha(ctx, one.alpha); + core.fillBoldText(ctx, one.text, px, py, one.color); + core.setAlpha(ctx, alpha); + }); +} + +// ------ 录像相关 ------ // + +////// 选择录像文件 ////// +control.prototype.chooseReplayFile = function () { + core.readFile(function (obj) { + if (obj.name != core.firstData.name) return alert("存档和游戏不一致!"); + if (!obj.route) return alert("无效的录像!"); + var _replay = function () { + core.startGame(core.flags.startUsingCanvas ? '' : obj.hard || '', obj.seed, core.decodeRoute(obj.route)); + } + if (obj.version && obj.version != core.firstData.version) { + core.myconfirm("游戏版本不一致!\n你仍然想播放录像吗?", _replay); + return; + } + _replay(); + }, null, ".h5route"); +} + +////// 开始播放 ////// +control.prototype.startReplay = function (list) { + if (!core.isPlaying()) return; + core.status.replay.replaying = true; + core.status.replay.pausing = true; + core.status.replay.failed = false; + core.status.replay.speed = 1.0; + core.status.replay.toReplay = core.cloneArray(list); + core.status.replay.totalList = core.status.route.concat(list); + core.status.replay.steps = 0; + core.status.replay.save = []; + core.createCanvas('replay', 0, core.__PIXELS__ - 40, core.__PIXELS__, 40, 199); + core.setOpacity('replay', 0.6); + this._replay_drawProgress(); + core.updateStatusBar(false, true); + core.drawTip("开始播放"); + this.replay(); + core.setToolbarButton("replay"); +} + +////// 更改播放状态 ////// +control.prototype.triggerReplay = function () { + if (core.status.replay.pausing) this.resumeReplay(); + else this.pauseReplay(); +} + +////// 暂停播放 ////// +control.prototype.pauseReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + core.status.replay.pausing = true; + core.updateStatusBar(false, true); + core.drawTip("暂停播放"); +} + +////// 恢复播放 ////// +control.prototype.resumeReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + core.status.replay.pausing = false; + core.updateStatusBar(false, true); + core.drawTip("恢复播放"); + core.replay(); +} + +////// 单步播放 ////// +control.prototype.stepReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + core.replay(true); +} + +////// 加速播放 ////// +control.prototype.speedUpReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + var speeds = [0.2, 0.5, 1, 2, 3, 6, 12, 24]; + for (var i = speeds.length - 2; i >= 0; i--) { + if (speeds[i] <= core.status.replay.speed) { + core.status.replay.speed = speeds[i + 1]; + break; + } + } + core.drawTip("x" + core.status.replay.speed + "倍"); +} + +////// 减速播放 ////// +control.prototype.speedDownReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + var speeds = [0.2, 0.5, 1, 2, 3, 6, 12, 24]; + for (var i = 1; i <= speeds.length; i++) { + if (speeds[i] >= core.status.replay.speed) { + core.status.replay.speed = speeds[i - 1]; + break; + } + } + core.drawTip("x" + core.status.replay.speed + "倍"); +} + +////// 设置播放速度 ////// +control.prototype.setReplaySpeed = function (speed) { + if (!core.isPlaying() || !core.isReplaying()) return; + core.status.replay.speed = speed; + core.drawTip("x" + core.status.replay.speed + "倍"); +} + +////// 停止播放 ////// +control.prototype.stopReplay = function (force) { + if (!core.isPlaying()) return; + if (!core.isReplaying() && !force) return; + core.status.replay.toReplay = []; + core.status.replay.totalList = []; + core.status.replay.replaying = false; + core.status.replay.pausing = false; + core.status.replay.failed = false; + core.status.replay.speed = 1.0; + core.status.replay.steps = 0; + core.status.replay.save = []; + core.deleteCanvas('replay'); + core.updateStatusBar(false, true); + core.drawTip("停止播放并恢复游戏"); + core.setToolbarButton('normal'); +} + +////// 回退 ////// +control.prototype.rewindReplay = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + if (core.status.replay.save.length == 0) { + core.playSound('操作失败'); + return core.drawTip("无法再回到上一个节点"); + } + var save = core.status.replay.save, data = save.pop(); + core.loadData(data.data, function () { + core.removeFlag('__fromLoad__'); + core.status.replay = { + "replaying": true, + "pausing": true, + "animate": false, + "toReplay": data.replay.toReplay, + "totalList": data.replay.totalList, + "speed": core.status.replay.speed, + "steps": data.replay.steps, + "save": save + } + core.createCanvas('replay', 0, core.__PIXELS__ - 40, core.__PIXELS__, 40, 199); + core.setOpacity('replay', 0.6); + core.control._replay_drawProgress(); + core.updateStatusBar(false, true); + core.drawTip("成功回退到上一个节点"); + }); +} + +////// 回放时存档 ////// +control.prototype._replay_SL = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + if (core.hasFlag('__forbidSave__')) { + core.playSound('操作失败'); + return core.drawTip('当前禁止存档'); + } + this._replay_hideProgress(); + + core.lockControl(); + core.status.event.id = 'save'; + var saveIndex = core.saves.saveIndex; + var page = parseInt((saveIndex - 1) / 5), offset = saveIndex - 5 * page; + + core.ui._drawSLPanel(10 * page + offset); +} + +////// 回放时查看怪物手册 ////// +control.prototype._replay_book = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || (core.status.event.id && core.status.event.id != 'viewMaps')) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + if (!core.hasItem('book')) { + core.playSound('操作失败'); + return core.drawTip('你没有' + core.material.items['book'].name, 'book'); + } + this._replay_hideProgress(); + + // 从“浏览地图”页面打开 + if (core.status.event.id == 'viewMaps') + core.status.event.ui = core.status.event.data; + + core.lockControl(); + core.status.event.id = 'book'; + core.useItem('book', true); +} + +////// 回放录像时浏览地图 ////// +control.prototype._replay_viewMap = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + this._replay_hideProgress(); + + core.lockControl(); + core.status.event.id = 'viewMaps'; + core.ui._drawViewMaps(); +} + +control.prototype._replay_toolbox = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + this._replay_hideProgress(); + + core.lockControl(); + core.status.event.id = 'toolbox'; + core.ui._drawToolbox(); +} + +control.prototype._replay_equipbox = function () { + if (!core.isPlaying() || !core.isReplaying()) return; + if (!core.status.replay.pausing) { + core.playSound('操作失败'); + return core.drawTip("请先暂停录像"); + } + if (core.isMoving() || core.status.replay.animate || core.status.event.id) { + core.playSound('操作失败'); + return core.drawTip("请等待当前事件的处理结束"); + } + this._replay_hideProgress(); + + core.lockControl(); + core.status.event.id = 'equipbox'; + core.ui._drawEquipbox(); +} + +////// 是否正在播放录像 ////// +control.prototype.isReplaying = function () { + return (core.status.replay || {}).replaying; +} + +////// 回放 ////// +control.prototype.replay = function (force) { + if (!core.isPlaying() || !core.isReplaying() + || core.status.replay.animate || core.status.event.id || core.status.replay.failed) return; + if (core.status.replay.pausing && !force) return; + + this._replay_drawProgress(); + if (core.status.replay.toReplay.length == 0) + return this._replay_finished(); + this._replay_save(); + var action = core.status.replay.toReplay.shift(); + if (this._doReplayAction(action)) return; + this._replay_error(action); +} + +////// 注册一个录像行为 ////// +// name:自定义名称,可用于注销使用 +// func:具体执行录像的函数,可为一个函数或插件中的函数名; +// 需要接受一个action参数,代表录像回放时的下一个操作 +// func返回true代表成功处理了此录像行为,false代表没有处理此录像行为。 +control.prototype.registerReplayAction = function (name, func) { + this.unregisterReplayAction(name); + this.replayActions.push({ name: name, func: func }); +} + +////// 注销一个录像行为 ////// +control.prototype.unregisterReplayAction = function (name) { + this.replayActions = this.replayActions.filter(function (b) { return b.name != name; }); +} + +////// 执行录像行为,会在注册的函数中依次执行直到得到true为止 ////// +control.prototype._doReplayAction = function (action) { + for (var i in this.replayActions) { + try { + if (core.doFunc(this.replayActions[i].func, this, action)) return true; + } catch (e) { + console.error(e); + console.error("ERROR in replayActions[" + this.replayActions[i].name + "]:已自动注销该项。"); + core.unregisterReplayAction(this.replayActions[i].name); + } + } + return false; +} + +control.prototype._replay_finished = function () { + core.status.replay.replaying = false; + core.status.replay.failed = false; + core.status.event.selection = 0; + var str = "录像播放完毕,你想退出播放吗?"; + if (core.status.route.length != core.status.replay.totalList.length + || core.subarray(core.status.route, core.status.replay.totalList) == null) { + str = "录像播放完毕,但记录不一致。\n请检查录像播放时的二次记录问题。\n你想退出播放吗?"; + } + core.ui.drawConfirmBox(str, function () { + core.ui.closePanel(); + core.stopReplay(true); + }, function () { + core.status.replay.replaying = true; + core.ui.closePanel(); + core.pauseReplay(); + }); +} + +control.prototype._replay_save = function () { + core.status.replay.steps++; + if (core.status.replay.steps % 40 == 1) { + if (core.status.replay.save.length == 30) + core.status.replay.save.shift(); + core.status.replay.save.push({ + "data": core.saveData(), "replay": { + "totalList": core.cloneArray(core.status.replay.totalList), + "toReplay": core.cloneArray(core.status.replay.toReplay), + "steps": core.status.replay.steps + } + }); + } +} + +control.prototype._replay_error = function (action, callback) { + core.ui.closePanel(); + core.status.replay.replaying = false; + core.status.replay.failed = true; + var len = core.status.replay.toReplay.length; + var prevList = core.status.replay.totalList.slice(-len - 11, -len - 1); + var nextList = core.status.replay.toReplay.slice(0, 10); + console.log("录像文件出错,当前操作:" + action); + console.log("之前的10个操作是:\n" + prevList.toString()); + console.log("接下来10个操作是:\n" + nextList.toString()); + core.ui.drawConfirmBox("录像文件出错,你想回到上个节点吗?", function () { + core.status.replay.failed = false; + core.ui.closePanel(); + if (core.status.replay.save.length > 0) { + core.status.replay.replaying = true; + core.status.replay.pausing = true; + core.rewindReplay(); + } + else { + core.playSound('操作失败'); + core.stopReplay(true); + core.drawTip("无法回到上一个节点"); + if (callback) callback(); + } + }, function () { + core.status.replay.failed = false; + core.ui.closePanel(); + core.stopReplay(true); + if (callback) callback(); + }); +} + +control.prototype._replay_hideProgress = function () { + if (core.dymCanvas.replay) core.dymCanvas.replay.canvas.style.display = 'none'; +} + +control.prototype._replay_drawProgress = function () { + if (!core.dymCanvas.replay) return; + if (core.dymCanvas.replay.canvas.style.display == 'none') core.dymCanvas.replay.canvas.style.display = 'block'; + var total = core.status.replay.totalList.length, left = total - core.status.replay.toReplay.length; + var content = '播放进度:' + left + ' / ' + total + '(' + (left / total * 100).toFixed(2) + '%)'; + var width = 26 + core.calWidth('replay', content, "16px Arial"); + core.clearMap('replay'); + core.fillRect('replay', 0, 0, width, 40, '#000000'); + core.fillText('replay', content, 16, 27, '#FFFFFF'); +} + +control.prototype.__replay_getTimeout = function () { + if (core.status.replay.speed == 24) return 0; + return 100 / Math.max(1, core.status.replay.speed); +} + +control.prototype._replayAction_move = function (action) { + if (["up", "down", "left", "right"].indexOf(action) < 0) return false; + core.moveHero(action, core.replay); + return true; +} + +control.prototype._replayAction_item = function (action) { + if (action.indexOf("item:") != 0) return false; + var itemId = action.substring(5); + if (!core.canUseItem(itemId)) return false; + if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) { + core.useItem(itemId, false, core.replay); + return true; + } + var tools = core.getToolboxItems('tools'), + constants = core.getToolboxItems('constants'); + var index, per = core.__SIZE__ - 1; + if ((index = tools.indexOf(itemId)) >= 0) { + core.status.event.data = { "toolsPage": Math.floor(index / per) + 1, "constantsPage": 1 }; + index = index % per; + } + else if ((index = constants.indexOf(itemId)) >= 0) { + core.status.event.data = { "toolsPage": 1, "constantsPage": Math.floor(index / per) + 1 }; + index = index % per + per; + } + if (index < 0) return false; + core.ui._drawToolbox(index); + setTimeout(function () { + core.ui.closePanel(); + core.useItem(itemId, false, core.replay); + }, core.control.__replay_getTimeout()); + return true; +} + +control.prototype._replayAction_equip = function (action) { + if (action.indexOf("equip:") != 0) return false; + var equipId = action.substring(6); + var ownEquipment = core.getToolboxItems('equips'); + var index = ownEquipment.indexOf(equipId), per = core.__SIZE__ - 1; + if (index < 0) { + core.removeFlag('__doNotCheckAutoEvents__'); + return false; + } + + var cb = function () { + var next = core.status.replay.toReplay[0] || ""; + if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { + core.removeFlag('__doNotCheckAutoEvents__'); + core.checkAutoEvents(); + } + core.replay(); + } + core.setFlag('__doNotCheckAutoEvents__', true); + + core.status.route.push(action); + if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) { + core.loadEquip(equipId, cb); + return true; + } + core.status.event.data = { "page": Math.floor(index / per) + 1, "selectId": null }; + index = index % per + per; + core.ui._drawEquipbox(index); + setTimeout(function () { + core.ui.closePanel(); + core.loadEquip(equipId, cb); + }, core.control.__replay_getTimeout()); + return true; +} + +control.prototype._replayAction_unEquip = function (action) { + if (action.indexOf("unEquip:") != 0) return false; + var equipType = parseInt(action.substring(8)); + if (!core.isset(equipType)) { + core.removeFlag('__doNotCheckAutoEvents__'); + return false; + } + + var cb = function () { + var next = core.status.replay.toReplay[0] || ""; + if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { + core.removeFlag('__doNotCheckAutoEvents__'); + core.checkAutoEvents(); + } + core.replay(); + } + core.setFlag('__doNotCheckAutoEvents__', true); + + core.ui._drawEquipbox(equipType); + core.status.route.push(action); + if (core.status.replay.speed == 24) { + core.unloadEquip(equipType, cb); + return true; + } + setTimeout(function () { + core.ui.closePanel(); + core.unloadEquip(equipType, cb); + }, core.control.__replay_getTimeout()); + return true; +} + +control.prototype._replayAction_saveEquip = function (action) { + if (action.indexOf('saveEquip:') != 0) return false; + core.quickSaveEquip(parseInt(action.substring(10))); + core.replay(); + return true; +} + +control.prototype._replayAction_loadEquip = function (action) { + if (action.indexOf('loadEquip:') != 0) return false; + core.quickLoadEquip(parseInt(action.substring(10))); + core.replay(); + return true; +} + +control.prototype._replayAction_fly = function (action) { + if (action.indexOf("fly:") != 0) return false; + var floorId = action.substring(4); + var toIndex = core.floorIds.indexOf(floorId); + if (!core.canUseItem('fly') || (!core.maps.canUseFlyHere())) return false; + core.ui.drawFly(toIndex); + if (core.status.replay.speed == 24) { + if (!core.flyTo(floorId, core.replay)) + core.control._replay_error(action); + return true; + } + setTimeout(function () { + if (!core.flyTo(floorId, core.replay)) + core.control._replay_error(action); + }, core.control.__replay_getTimeout()); + return true; +} + +control.prototype._replayAction_shop = function (action) { + if (action.indexOf("shop:") != 0) return false; + var shopId = action.substring(5); + if (core.canUseQuickShop(shopId) != null || !core.canOpenShop(shopId)) { + this._replay_error(shopId); + return true; + } + core.openShop(shopId, false); + core.replay(); + return true; +} + +control.prototype._replayAction_turn = function (action) { + if (action != 'turn' && action.indexOf('turn:') != 0) return false; + if (action == 'turn') core.turnHero(); + else core.turnHero(action.substring(5)); + core.replay(); + return true; +} + +control.prototype._replayAction_getNext = function (action) { + if (action != "getNext") return false; + core.getNextItem(); + core.replay(); + return true; +} + +control.prototype._replayAction_moveDirectly = function (action) { + if (action.indexOf("move:") != 0) return false; + + var pos = action.substring(5).split(":"); + var x = parseInt(pos[0]), y = parseInt(pos[1]); + var nowx = core.getHeroLoc('x'), nowy = core.getHeroLoc('y'); + var ignoreSteps = core.canMoveDirectly(x, y); + if (!core.moveDirectly(x, y, ignoreSteps)) return false; + if (core.status.replay.speed == 24) { + core.replay(); + return true; + } + + core.ui.drawArrow('ui', 32 * nowx + 16 - core.bigmap.offsetX, 32 * nowy + 16 - core.bigmap.offsetY, + 32 * x + 16 - core.bigmap.offsetX, 32 * y + 16 - core.bigmap.offsetY, '#FF0000', 3); + var timeout = this.__replay_getTimeout(); + if (ignoreSteps < 10) timeout = timeout * ignoreSteps / 10; + setTimeout(function () { + core.clearMap('ui'); + core.replay(); + }, timeout); + return true; +} + +control.prototype._replayAction_key = function (action) { + if (action.indexOf("key:") != 0) return false; + core.actions.keyUp(parseInt(action.substring(4)), false, true); + core.replay(); + return true; +} + +control.prototype._replayAction_click = function (action) { + if (action.indexOf("click:") != 0) return false; + var p = action.split(":"); + if (p.length != 4) return false; + core.actions.doRegisteredAction("onStatusBarClick", parseInt(p[2]), parseInt(p[3]), parseInt(p[1])); + core.replay(); + return true; +} + +control.prototype._replayAction_ignoreInput = function (action) { + if (action.indexOf('input:') == 0 || action.indexOf('input2:') == 0 || action.indexOf('choices:') == 0 || action.indexOf('random:') == 0) { + console.warn('警告!录像播放中出现了未知的 ' + action + '!'); + core.replay(); + return true; + } + return false; +} + +control.prototype._replayAction_no = function (action) { + if (action != 'no') return false; + core.status.route.push(action); + core.replay(); + return true; +} + +// ------ 存读档相关 ------ // + +////// 自动存档 ////// +control.prototype.autosave = function (removeLast) { + if (core.hasFlag('__forbidSave__')) return; + core.saves.actionCount++; + var x = null; + if (removeLast) { + x = core.status.route.pop(); + core.status.route.push("turn:" + core.getHeroLoc('direction')); + } + if (core.status.event.id == 'action') // 事件中的自动存档 + core.setFlag("__events__", core.clone(core.status.event.data)); + if (core.saves.autosave.data == null) { + core.saves.autosave.data = []; + } + core.saves.autosave.data.splice(core.saves.autosave.now, 0, core.saveData()); + core.saves.autosave.now += 1; + if (core.saves.autosave.data.length > core.saves.autosave.max) { + if (core.saves.autosave.now < core.saves.autosave.max / 2) + core.saves.autosave.data.pop(); + else { + core.saves.autosave.data.shift(); + core.saves.autosave.now = core.saves.autosave.now - 1; + } + } + core.saves.autosave.updated = true; + core.saves.ids[0] = true; + core.removeFlag("__events__"); + if (removeLast) { + core.status.route.pop(); + if (x) core.status.route.push(x); + } +} + +/////// 实际进行自动存档 ////// +control.prototype.checkAutosave = function () { + if (!core.animateFrame || !core.saves || !core.saves.autosave) return; + core.setLocalStorage('totalTime', core.animateFrame.totalTime); + var autosave = core.saves.autosave; + if (autosave.data == null || !autosave.updated || !autosave.storage) return; + autosave.updated = false; + if (autosave.data.length >= 1) { + core.setLocalForage("autoSave", autosave.data[autosave.now - 1]); + } +} + +////// 实际进行存读档事件 ////// +control.prototype.doSL = function (id, type) { + switch (type) { + case 'save': this._doSL_save(id); break; + case 'load': this._doSL_load(id, this._doSL_load_afterGet); break; + case 'reload': this._doSL_reload(id, this._doSL_load_afterGet); break; + case 'replayLoad': this._doSL_load(id, this._doSL_replayLoad_afterGet); break; + case 'replayRemain': this._doSL_load(id, this._doSL_replayRemain_afterGet); break; + case 'replaySince': this._doSL_load(id, this._doSL_replaySince_afterGet); break; + } +} + +control.prototype._doSL_save = function (id) { + if (id == 'autoSave') { + core.playSound('操作失败'); + return core.drawTip('不能覆盖自动存档!'); + } + // 在事件中的存档 + if (core.status.event.interval != null) + core.setFlag("__events__", core.status.event.interval); + var data = core.saveData(); + if (core.isReplaying() && core.status.replay.toReplay.length > 0) { + data.__toReplay__ = core.encodeRoute(core.status.replay.toReplay); + } + core.setLocalForage("save" + id, data, function () { + core.saves.saveIndex = id; + core.saves.actionCount = 0; + core.setLocalStorage('saveIndex', core.saves.saveIndex); + // 恢复事件 + if (!core.events.recoverEvents(core.status.event.interval)) + core.ui.closePanel(); + core.playSound('存档'); + core.drawTip('存档成功!'); + }, function (err) { + console.error(err); + alert("存档失败,错误信息:\n" + err); + }); + core.removeFlag("__events__"); + return; +} + +control.prototype._doSL_load = function (id, callback) { + const autosave = core.saves.autosave; + if (id == 'autoSave' && autosave.data != null) { + let tip = '读档成功'; + autosave.now -= 1; + var data = autosave.data.splice(autosave.now, 1)[0]; + if (core.isPlaying() && !core.status.gameOver) { + core.control.autosave(0); + autosave.now -= 1; + } + if (autosave.now == 0) { + autosave.data.unshift(core.clone(data)); + autosave.now += 1; + } + callback(id, data); + } + else { + core.getLocalForage(id == 'autoSave' ? id : "save" + id, null, function (data) { + if (id == 'autoSave' && data != null) { + autosave.data = data; + if (!(autosave.data instanceof Array)) { + autosave.data = [autosave.data]; + } + autosave.now = autosave.data.length; + return core.control._doSL_load(id, callback); + } + callback(id, data); + }, function (err) { + console.error(err); + alert("无效的存档"); + }) + } + return; +} + +control.prototype._doSL_reload = function (id, callback) { + if (core.saves.autosave.data != null && core.saves.autosave.now < core.saves.autosave.data.length) { + var data = core.saves.autosave.data.splice(core.saves.autosave.now, 1)[0]; + core.control.autosave(false); + callback(id, data); + } + else core.drawFailTip('当前不能再继续向后回溯自动存档'); + return; +} + +control.prototype._doSL_load_afterGet = function (id, data) { + if (!data) return alert("无效的存档"); + var _replay = function () { + core.startGame(data.hard, data.hero.flags.__seed__, core.decodeRoute(data.route)); + }; + if (data.version != core.firstData.version) { + core.myconfirm("存档版本不匹配!\n你想回放此存档的录像吗?\n可以随时停止录像播放以继续游戏。", _replay); + return; + } + if (data.hero.flags.__events__ && data.guid != core.getGuid()) { + core.myconfirm("此存档可能存在风险,你想要播放录像么?", _replay); + return; + } + core.ui.closePanel(); + core.loadData(data, function () { + core.removeFlag('__fromLoad__'); + core.drawTip("读档成功"); + if (id != "autoSave") { + core.saves.saveIndex = id; + core.saves.actionCount = 0; + core.setLocalStorage('saveIndex', core.saves.saveIndex); + } + else core.saves.actionCount = 1; + }); +} + +control.prototype._doSL_replayLoad_afterGet = function (id, data) { + if (!data) { + core.playSound('操作失败'); + return core.drawTip("无效的存档"); + } + if (data.version != core.firstData.version) { + core.playSound('操作失败'); + return core.drawTip("存档版本不匹配"); + } + if (data.hero.flags.__events__ && data.guid != core.getGuid()) { + core.playSound('操作失败'); + return core.drawTip("此存档可能存在风险,无法读档"); + } + var route = core.subarray(core.status.route, core.decodeRoute(data.route)); + if (route == null) { + core.playSound('操作失败'); + return core.drawTip("无法从此存档回放录像"); + } + core.loadData(data, function () { + core.removeFlag('__fromLoad__'); + core.startReplay(route); + core.drawTip("回退到存档节点"); + }); +} + +control.prototype._doSL_replayRemain_afterGet = function (id, data) { + if (!data) { + core.playSound('操作失败'); + return core.drawTip("无效的存档"); + } + var route = core.decodeRoute(data.route); + if (core.status.tempRoute) { + var remainRoute = core.subarray(route, core.status.tempRoute); + if (remainRoute == null) + return alert("无法接续播放录像!\n该存档必须是前一个选择的存档的后续内容。"); + delete core.status.tempRoute; + core.ui.closePanel(); + core.startReplay(remainRoute); + core.drawTip("接续播放录像"); + return; + } + else if (data.floorId != core.status.floorId || data.hero.loc.x != core.getHeroLoc('x') || data.hero.loc.y != core.getHeroLoc('y')) + return alert("楼层或坐标不一致!"); + + core.status.tempRoute = route; + core.ui.closePanel(); + core.drawText("\t[步骤2]请选择第二个存档。\n\r[yellow]该存档必须是前一个存档的后续。\r\n将尝试播放到此存档。", function () { + core.status.event.id = 'replayRemain'; + core.lockControl(); + var saveIndex = core.saves.saveIndex; + var page = parseInt((saveIndex - 1) / 5), offset = saveIndex - 5 * page; + core.ui._drawSLPanel(10 * page + offset); + }); +} + +control.prototype._doSL_replaySince_afterGet = function (id, data) { + if (data.floorId != core.status.floorId || data.hero.loc.x != core.getHeroLoc('x') || data.hero.loc.y != core.getHeroLoc('y')) + return alert("楼层或坐标不一致!"); + if (!data.__toReplay__) return alert('该存档没有剩余录像!'); + core.ui.closePanel(); + core.startReplay(core.decodeRoute(data.__toReplay__)); + core.drawTip("播放存档剩余录像"); + return; +} + +////// 同步存档到服务器 ////// +control.prototype.syncSave = function (type) { + core.ui.drawWaiting("正在同步,请稍候..."); + if (type == 'all') { + const callback = function (saves) { + core.control._syncSave_http(type, saves); + }; + core.ui.drawConfirmBox("您正在试图同步您的\r[Salmon]所有\r存档,\n这可能导致接收方已有的存档被\r[Salmon]覆盖\r。\n确定要这么做?", function () { + core.getAllSaves(callback); + }, () => { core.ui.closePanel(); }); + } + else { // 这里的检测逻辑:每存读档一次actionCount置0, 若读的自动档, actionCount置为1, 每autosave一次actionCount+1。若actionCount不为0就会触发检测。 + const index = core.saves.saveIndex; + if (!index) return core.drawText("没有要同步的存档"); + const callback = function (save) { + const autosave = core.saves.autosave; + if (autosave.data instanceof Array && core.saves.actionCount > 0) { + const ctx = core.createCanvas('syncSave', core.__PIXELS__ / 2 - 52, 18, 104, 104, 141); + core.strokeRect(ctx, 0, 0, 104, 104, 'yellow', 2); + const map = core.maps.loadMap(save.maps, save.floorId); + core.drawThumbnail(save.floorId, map.blocks, { + heroLoc: save.hero.loc, heroIcon: save.hero.image, flags: save.hero.flags, + ctx: ctx, x: 2, y: 2, size: 100, noHD: true, + }); + core.ui.drawConfirmBox("您正试图同步" + index + "号存档,确定是这个存档吗?\n如果不是,请返回并\r[yellow]保存当前\r状态再同步.", function () { + core.control._syncSave_http(type, save); + core.deleteCanvas("syncSave"); + }, () => { + core.ui.closePanel(); + core.deleteCanvas("syncSave"); + }); + } + else core.control._syncSave_http(type, save); + } + core.getSave(index, callback); + } +} + +control.prototype._syncSave_http = function (type, saves) { + if (!saves) return core.drawText("没有要同步的存档"); + var formData = new FormData(); + formData.append('type', 'save'); + formData.append('name', core.firstData.name); + formData.append('data', LZString.compressToBase64(JSON.stringify(saves))); + formData.append('shorten', '1'); + + core.http("POST", "/games/sync.php", formData, function (data) { + var response = JSON.parse(data); + if (response.code < 0) { + core.drawText("出错啦!\n无法同步存档到服务器。\n错误原因:" + response.msg); + } + else { + core.drawText((type == 'all' ? "所有存档" : "存档" + core.saves.saveIndex) + "同步成功!\n\n您的存档编号+密码: \r[yellow]" + + response.code + response.msg + + "\r\n\n请牢记以上信息(如截图等),在从服务器\n同步存档时使用。") + } + }, function (e) { + core.drawText("出错啦!\n无法同步存档到服务器。\n错误原因:" + e); + }) +} + +////// 从服务器加载存档 ////// +control.prototype.syncLoad = function () { + core.myprompt("请输入存档编号+密码", null, function (idpassword) { + if (!idpassword) return core.ui._drawSyncSave(); + if (!/^\d{6}\w{4}$/.test(idpassword) && !/^\d{4}\w{3}$/.test(idpassword)) { + core.drawText("不合法的存档编号+密码!"); + return; + } + core.ui.drawWaiting("正在同步,请稍候..."); + if (idpassword.length == 7) { + core.control._syncLoad_http(idpassword.substring(0, 4), idpassword.substring(4)); + } else { + core.control._syncLoad_http(idpassword.substring(0, 6), idpassword.substring(6)); + } + }, () => { }); +} + +control.prototype._syncLoad_http = function (id, password) { + var formData = new FormData(); + formData.append('type', 'load'); + formData.append('name', core.firstData.name); + formData.append('id', id); + formData.append('password', password); + + core.http("POST", "/games/sync.php", formData, function (data) { + var response = JSON.parse(data); + if (response.code == 0) { + var msg = null; + try { + msg = JSON.parse(LZString.decompressFromBase64(response.msg)); + } catch (e) { } + if (!msg) { + try { + msg = JSON.parse(response.msg); + } catch (e) { } + } + if (msg) { + core.control._syncLoad_write(msg); + } else { + core.drawText("出错啦!\n存档解析失败!"); + } + } + else { + core.drawText("出错啦!\n无法从服务器同步存档。\n错误原因:" + response.msg); + } + }, function (e) { + core.drawText("出错啦!\n无法从服务器同步存档。\n错误原因:" + e); + }); +} + +control.prototype._syncLoad_write = function (data) { + if (data instanceof Array) { + core.status.event.selection = 1; + core.ui.drawConfirmBox("所有本地存档都将被覆盖,确认?", function () { + for (var i = 1; i <= 5 * (main.savePages || 30); i++) { + if (i <= data.length) + core.setLocalForage("save" + i, data[i - 1]); + else if (core.saves.ids[i]) + core.removeLocalForage("save" + i); + } + core.ui.closePanel(); + core.drawText("同步成功!\n你的本地所有存档均已被覆盖。"); + }, function () { + core.status.event.selection = 0; + core.ui._drawSyncSave(); + }); + } + else { + // 只覆盖单存档 + core.setLocalForage("save" + core.saves.saveIndex, data, function () { + core.drawText("同步成功!\n单存档已覆盖至存档" + core.saves.saveIndex); + }); + } +} + +////// 存档到本地 ////// +control.prototype.saveData = function () { + return this.controldata.saveData(); +} + +////// 从本地读档 ////// +control.prototype.loadData = function (data, callback) { + return this.controldata.loadData(data, callback); +} + +control.prototype.getSave = function (index, callback) { + if (index == 0) { + // --- 自动存档先从缓存中获取 + if (core.saves.autosave.data != null) + callback(core.saves.autosave.data); + else { + core.getLocalForage("autoSave", null, function (data) { + if (data != null) { + core.saves.autosave.data = data; + if (!(core.saves.autosave.data instanceof Array)) { + core.saves.autosave.data = [core.saves.autosave.data]; + } + core.saves.autosave.now = core.saves.autosave.data.length; + } + callback(core.saves.autosave.data); + }, function (err) { + console.error(err); + callback(null); + }); + } + return; + } + core.getLocalForage("save" + index, null, function (data) { + if (callback) callback(data); + }, function (err) { + console.error(err); + if (callback) callback(null); + }); +} + +control.prototype.getSaves = function (ids, callback) { + if (!(ids instanceof Array)) return this.getSave(ids, callback); + var count = ids.length, data = {}; + for (var i = 0; i < ids.length; ++i) { + (function (i) { + core.getSave(ids[i], function (result) { + data[i] = result; + if (Object.keys(data).length == count) + callback(data); + }) + })(i); + } +} + +control.prototype.getAllSaves = function (callback) { + var ids = Object.keys(core.saves.ids).filter(function (x) { return x != 0; }) + .sort(function (a, b) { return a - b; }), saves = []; + this.getSaves(ids, function (data) { + for (var i = 0; i < ids.length; ++i) { + if (data[i] != null) + saves.push(data[i]); + } + callback(saves); + }); +} + +////// 获得所有存在存档的存档位 ////// +control.prototype.getSaveIndexes = function (callback) { + var indexes = {}; + core.keysLocalForage(function (err, keys) { + if (err) { + console.error(err); + return callback(indexes); + } + keys.forEach(function (key) { + core.control._getSaveIndexes_getIndex(indexes, key); + }); + callback(indexes); + }); +} + +control.prototype._getSaveIndexes_getIndex = function (indexes, name) { + var e = new RegExp('^' + core.firstData.name + "_(save\\d+|autoSave)$").exec(name); + if (e) { + if (e[1] == 'autoSave') indexes[0] = true; + else indexes[parseInt(e[1].substring(4))] = true; + } +} + +////// 判断某个存档位是否存在存档 ////// +control.prototype.hasSave = function (index) { + return core.saves.ids[index] || false; +} + +////// 删除某个存档 +control.prototype.removeSave = function (index, callback) { + if (index == 0 || index == "autoSave") { + index = "autoSave"; + core.removeLocalForage(index, function () { + core.saves.autosave.data = null; + core.saves.autosave.updated = false; + if (callback) callback(); + }); + return; + } + core.removeLocalForage("save" + index, function () { + core.saves.favorite = core.saves.favorite.filter(function (i) { return core.hasSave(i); }); + delete core.saves.favoriteName[index]; + core.control._updateFavoriteSaves(); + if (callback) callback(); + }, function () { + core.playSound('操作失败'); + core.drawTip("无法删除存档!"); + if (callback) callback(); + }); +} + +////// 读取收藏信息 +control.prototype._loadFavoriteSaves = function () { + core.saves.favorite = core.getLocalStorage("favorite", []); + // --- 移除不存在的收藏 + core.saves.favorite = core.saves.favorite.filter(function (i) { return core.hasSave(i); }); + core.saves.favoriteName = core.getLocalStorage("favoriteName", {}); +} + +control.prototype._updateFavoriteSaves = function () { + core.setLocalStorage("favorite", core.saves.favorite); + core.setLocalStorage("favoriteName", core.saves.favoriteName); +} + +// ------ 属性,状态,位置,buff,变量,锁定控制等 ------ // + +////// 设置勇士属性 ////// +control.prototype.setStatus = function (name, value) { + if (!core.status.hero) return; + if (name == 'x' || name == 'y' || name == 'direction') + this.setHeroLoc(name, value); + else + core.status.hero[name] = value; +} + +////// 增减勇士属性 ////// +control.prototype.addStatus = function (name, value) { + this.setStatus(name, this.getStatus(name) + value); +} + +////// 获得勇士属性 ////// +control.prototype.getStatus = function (name) { + if (!core.status.hero) return null; + if (name == 'x' || name == 'y' || name == 'direction') + return this.getHeroLoc(name); + if (main.mode == 'editor' && !core.hasFlag('__statistics__')) { + return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero[name]; + } + return core.status.hero[name]; +} + +////// 从status中获得属性,如果不存在则从勇士属性中获取 ////// +control.prototype.getStatusOrDefault = function (status, name) { + if (status && name in status) + return Math.floor(status[name]); + return Math.floor(this.getStatus(name)); +} + +////// 获得勇士实际属性(增幅后的) ////// +control.prototype.getRealStatus = function (name) { + return this.getRealStatusOrDefault(null, name); +} + +////// 从status中获得实际属性(增幅后的),如果不存在则从勇士属性中获取 ////// +control.prototype.getRealStatusOrDefault = function (status, name) { + return Math.floor(this.getStatusOrDefault(status, name) * this.getBuff(name)); +} + +////// 获得勇士原始属性(无装备和衰弱影响) ////// +control.prototype.getNakedStatus = function (name) { + var value = this.getStatus(name); + if (value == null) return value; + // 装备增幅 + core.status.hero.equipment.forEach(function (v) { + if (!v || !(core.material.items[v] || {}).equip) return; + value -= core.material.items[v].equip.value[name] || 0; + }); + // 衰弱扣除 + if (core.hasFlag('weak') && core.values.weakValue >= 1 && (name == 'atk' || name == 'def')) { + value += core.values.weakValue; + } + return value; +} + +////// 获得某个属性的名字 ////// +control.prototype.getStatusLabel = function (name) { + if (this.controldata.getStatusLabel) { + return this.controldata.getStatusLabel(name) || name; + } + return { + name: "名称", lv: "等级", hpmax: "生命上限", hp: "生命", manamax: "魔力上限", mana: "魔力", + atk: "攻击", def: "防御", mdef: "护盾", money: "金币", exp: "经验", point: "加点", steps: "步数", + special: "特殊属性", + }[name] || name; +} + +////// 设置某个属性的增幅值 ////// +control.prototype.setBuff = function (name, value) { + // 仅保留三位有效buff值 + value = parseFloat(value.toFixed(3)); + this.setFlag('__' + name + '_buff__', value); +} + +////// 加减某个属性的增幅值 ////// +control.prototype.addBuff = function (name, value) { + var buff = this.getBuff(name) + value; + // 仅保留三位有效buff值 + buff = parseFloat(buff.toFixed(3)); + this.setFlag('__' + name + '_buff__', buff); +} + +////// 获得某个属性的增幅值 ////// +control.prototype.getBuff = function (name) { + return core.getFlag('__' + name + '_buff__', 1); +} + +////// 获得或移除毒衰咒效果 ////// +control.prototype.triggerDebuff = function (action, type) { + return this.controldata.triggerDebuff(action, type); +} + +////// 设置勇士的位置 ////// +control.prototype.setHeroLoc = function (name, value, noGather) { + if (!core.status.hero) return; + core.status.hero.loc[name] = value; + if ((name == 'x' || name == 'y') && !noGather) { + this.gatherFollowers(); + } +} + +////// 获得勇士的位置 ////// +control.prototype.getHeroLoc = function (name) { + if (!core.status.hero) return; + if (main.mode == 'editor') { + if (name == null) return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero.loc; + return data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.hero.loc[name]; + } + if (name == null) return core.status.hero.loc; + return core.status.hero.loc[name]; +} + +////// 获得某个等级的名称 ////// +control.prototype.getLvName = function (lv) { + if (!core.status.hero) return null; + if (lv == null) lv = core.status.hero.lv; + return ((core.firstData.levelUp || [])[lv - 1] || {}).title || lv; +} + +////// 获得下个等级所需经验;如果不存在下个等级,返回null。 ////// +control.prototype.getNextLvUpNeed = function () { + if (!core.status.hero) return null; + if (core.status.hero.lv >= core.firstData.levelUp.length) return null; + var need = core.calValue(core.firstData.levelUp[core.status.hero.lv].need); + if (core.flags.statusBarItems.indexOf('levelUpLeftMode') >= 0) + return Math.max(need - core.getStatus('exp'), 0); + else return need; +} + +////// 设置某个自定义变量或flag ////// +control.prototype.setFlag = function (name, value) { + if (value == null) return this.removeFlag(name); + if (!core.status.hero) return; + core.status.hero.flags[name] = value; +} + +////// 增加某个flag数值 ////// +control.prototype.addFlag = function (name, value) { + if (!core.status.hero) return; + core.setFlag(name, core.getFlag(name, 0) + value); +} + +////// 获得某个自定义变量或flag ////// +control.prototype.getFlag = function (name, defaultValue) { + if (!core.status.hero) return defaultValue; + var value = core.status.hero.flags[name]; + return value != null ? value : defaultValue; +} + +////// 是否存在某个自定义变量或flag,且值为true ////// +control.prototype.hasFlag = function (name) { + return !!core.getFlag(name); +} + +////// 删除某个自定义变量或flag ////// +control.prototype.removeFlag = function (name) { + if (!core.status.hero) return; + delete core.status.hero.flags[name]; +} + +////// 获得某个点的独立开关 ////// +control.prototype.getSwitch = function (x, y, floorId, name, defaultValue) { + var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); + return this.getFlag(prefix + "@" + name, defaultValue); +} + +////// 设置某个点的独立开关 ////// +control.prototype.setSwitch = function (x, y, floorId, name, value) { + var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); + return this.setFlag(prefix + "@" + name, value); +} + +////// 增加某个点的独立开关 ////// +control.prototype.addSwitch = function (x, y, floorId, name, value) { + var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); + return this.addFlag(prefix + "@" + name, value); +} + +////// 判定某个点的独立开关 ////// +control.prototype.hasSwitch = function (x, y, floorId, name) { + var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); + return this.hasFlag(prefix + "@" + name); +} + +////// 删除某个点的独立开关 ////// +control.prototype.removeSwitch = function (x, y, floorId, name) { + var prefix = [floorId || core.status.floorId || ":f", x != null ? x : "x", y != null ? y : "y"].join("@"); + return this.removeFlag(prefix + "@" + name); +} + +////// 锁定状态栏,常常用于事件处理 ////// +control.prototype.lockControl = function () { + core.status.lockControl = true; +} + +////// 解锁状态栏 ////// +control.prototype.unlockControl = function () { + core.status.lockControl = false; +} + +////// 开启debug模式 ////// +control.prototype.debug = function () { + core.setFlag('debug', true); + core.drawText("\t[调试模式开启]此模式下按住Ctrl键(或Ctrl+Shift键)可以穿墙并忽略一切事件。\n此模式下将无法上传成绩。"); +} + +control.prototype._bindRoutePush = function () { + core.status.route.push = function (element) { + // 忽视移动、转向、瞬移 + if (["up", "down", "left", "right", "turn"].indexOf(element) < 0 && !element.startsWith("move:")) { + core.clearRouteFolding(); + } + Array.prototype.push.call(core.status.route, element); + } +} + +////// 清除录像折叠信息 ////// +control.prototype.clearRouteFolding = function () { + core.status.routeFolding = {}; +} + +////// 检查录像折叠 ////// +control.prototype.checkRouteFolding = function () { + // 未开启、未开始游戏、录像播放中、正在事件中:不执行 + if (!core.flags.enableRouteFolding || !core.isPlaying() || core.isReplaying() || core.status.event.id) { + return this.clearRouteFolding(); + } + var hero = core.clone(core.status.hero, function (name, value) { + return name != 'steps' && typeof value == 'number'; + }); + var index = [core.getHeroLoc('x'), core.getHeroLoc('y'), core.getHeroLoc('direction').charAt(0)].join(','); + core.status.routeFolding = core.status.routeFolding || {}; + if (core.status.routeFolding[index]) { + var one = core.status.routeFolding[index]; + if (core.same(one.hero, hero) && one.length < core.status.route.length) { + Object.keys(core.status.routeFolding).forEach(function (v) { + if (core.status.routeFolding[v].length >= one.length) delete core.status.routeFolding[v]; + }); + core.status.route = core.status.route.slice(0, one.length); + this._bindRoutePush(); + } + } + core.status.routeFolding[index] = { hero: hero, length: core.status.route.length }; +} + +// ------ 天气,色调,BGM ------ // + +control.prototype.getMappedName = function (name) { + return core.getFlag('__nameMap__', {})[name] || (main.nameMap || {})[name] || name; +} + +////// 更改天气效果 ////// +control.prototype.setWeather = function (type, level) { + // 非雨雪 + if (type == null || !this.weathers[type]) { + core.deleteCanvas('weather') + core.animateFrame.weather.type = null; + core.animateFrame.weather.nodes = []; + return; + } + if (level == null) level = core.animateFrame.weather.level; + level = core.clamp(parseInt(level) || 5, 1, 10); + // 当前天气:则忽略 + if (type == core.animateFrame.weather.type && level == core.animateFrame.weather.level) return; + + // 计算当前的宽高 + core.createCanvas('weather', 0, 0, core.__PIXELS__, core.__PIXELS__, 80); + core.setOpacity('weather', 1.0); + core.animateFrame.weather.type = type; + core.animateFrame.weather.level = level; + core.animateFrame.weather.nodes = []; + try { + core.doFunc(this.weathers[type].initFunc, this, level); + } catch (e) { + console.error(e); + console.error("ERROR in weather[" + type + "]:已自动注销该项。"); + core.unregisterWeather(type); + } +} + +////// 注册一个天气 ////// +// name为天气类型,如 sun, rain, snow 等 +// initFunc 为设置为此天气时的初始化,接受level参数 +// frameFunc 为该天气下每帧的效果,接受和timestamp参数(从页面加载完毕到当前经过的时间) +control.prototype.registerWeather = function (name, initFunc, frameFunc) { + this.unregisterWeather(name); + this.weathers[name] = { initFunc: initFunc, frameFunc: frameFunc }; +} + +////// 取消注册一个天气 ////// +control.prototype.unregisterWeather = function (name) { + delete this.weathers[name]; + if (core.animateFrame.weather.type == name) { + this.setWeather(null); + } +} + +control.prototype._weather_rain = function (level) { + var number = level * parseInt(20 * core.bigmap.width * core.bigmap.height / (core.__SIZE__ * core.__SIZE__)); + for (var a = 0; a < number; a++) { + core.animateFrame.weather.nodes.push({ + 'x': Math.random() * core.bigmap.width * 32, + 'y': Math.random() * core.bigmap.height * 32, + 'l': Math.random() * 2.5, + 'xs': -4 + Math.random() * 4 + 2, + 'ys': Math.random() * 10 + 10 + }) + } +} + +control.prototype._weather_snow = function (level) { + var number = level * parseInt(20 * core.bigmap.width * core.bigmap.height / (core.__SIZE__ * core.__SIZE__)); + for (var a = 0; a < number; a++) { + core.animateFrame.weather.nodes.push({ + 'x': Math.random() * core.bigmap.width * 32, + 'y': Math.random() * core.bigmap.height * 32, + 'r': Math.random() * 5 + 1, + 'd': Math.random() * Math.min(level, 200), + }) + } +} + +control.prototype._weather_fog = function (level) { + if (!core.animateFrame.weather.fog) return; + core.animateFrame.weather.nodes = [{ + 'image': core.animateFrame.weather.fog, + 'level': 40 * level, + 'x': 0, + 'y': -core.__PIXELS__ / 2, + 'dx': -Math.random() * 1.5, + 'dy': Math.random(), + 'delta': 0.001, + }]; +} + +control.prototype._weather_cloud = function (level) { + if (!core.animateFrame.weather.cloud) return; + core.animateFrame.weather.nodes = [{ + 'image': core.animateFrame.weather.cloud, + 'level': 40 * level, + 'x': 0, + 'y': -core.__PIXELS__ / 2, + 'dx': -Math.random() * 1.5, + 'dy': Math.random(), + 'delta': 0.001, + }]; +} + +control.prototype._weather_sun = function (level) { + if (!core.animateFrame.weather.sun) return; + // 直接绘制 + core.clearMap('weather'); + core.drawImage('weather', core.animateFrame.weather.sun, 0, 0, core.animateFrame.weather.sun.width, core.animateFrame.weather.sun.height, 0, 0, core.__PIXELS__, core.__PIXELS__); + core.setOpacity('weather', level / 10); + core.animateFrame.weather.nodes = [{ opacity: level / 10, delta: 0.01 }]; +} + +////// 更改画面色调 ////// +control.prototype.setCurtain = function (color, time, moveMode, callback) { + if (time == null) time = 750; + if (time <= 0) time = 0; + if (!core.status.curtainColor) + core.status.curtainColor = [0, 0, 0, 0]; + if (!color) color = [0, 0, 0, 0]; + if (color[3] == null) color[3] = 1; + color[3] = core.clamp(color[3], 0, 1); + + if (time == 0) { + // 直接变色 + core.clearMap('curtain'); + core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(color)); + core.status.curtainColor = color; + if (callback) callback(); + return; + } + + this._setCurtain_animate(core.status.curtainColor, color, time, moveMode, callback); +} + +control.prototype._setCurtain_animate = function (nowColor, color, time, moveMode, callback) { + time /= Math.max(core.status.replay.speed, 1) + var per_time = 10, step = 0, steps = parseInt(time / per_time); + if (steps <= 0) steps = 1; + var curr = nowColor; + var moveFunc = core.applyEasing(moveMode); + + var cb = function () { + core.status.curtainColor = curr; + if (callback) callback(); + } + var animate = setInterval(function () { + step++; + curr = [ + nowColor[0] + (color[0] - nowColor[0]) * moveFunc(step / steps), + nowColor[1] + (color[1] - nowColor[1]) * moveFunc(step / steps), + nowColor[2] + (color[2] - nowColor[2]) * moveFunc(step / steps), + nowColor[3] + (color[3] - nowColor[3]) * moveFunc(step / steps), + ] + core.clearMap('curtain'); + core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(curr)); + if (step == steps) { + delete core.animateFrame.asyncId[animate]; + clearInterval(animate); + cb(); + } + }, per_time); + + core.animateFrame.lastAsyncId = animate; + core.animateFrame.asyncId[animate] = cb; +} + +////// 画面闪烁 ////// +control.prototype.screenFlash = function (color, time, times, moveMode, callback) { + times = times || 1; + time = time / 3; + var nowColor = core.clone(core.status.curtainColor); + core.setCurtain(color, time, moveMode, function () { + core.setCurtain(nowColor, time * 2, moveMode, function () { + if (times > 1) + core.screenFlash(color, time * 3, times - 1, moveMode, callback); + else { + if (callback) callback(); + } + }); + }); +} + +////// 播放背景音乐 ////// +control.prototype.playBgm = function (bgm, startTime) { + bgm = core.getMappedName(bgm); + if (main.mode != 'play' || !core.material.bgms[bgm]) return; + // 如果不允许播放 + if (!core.musicStatus.bgmStatus) { + try { + core.musicStatus.playingBgm = bgm; + core.musicStatus.lastBgm = bgm; + core.material.bgms[bgm].pause(); + } + catch (e) { + console.error(e); + } + return; + } + this.setMusicBtn(); + + try { + this._playBgm_play(bgm, startTime); + } + catch (e) { + console.log("无法播放BGM " + bgm); + console.error(e); + core.musicStatus.playingBgm = null; + } +} + +control.prototype._playBgm_play = function (bgm, startTime) { + // 如果当前正在播放,且和本BGM相同,直接忽略 + if (core.musicStatus.playingBgm == bgm && !core.material.bgms[core.musicStatus.playingBgm].paused) { + return; + } + // 如果正在播放中,暂停 + if (core.musicStatus.playingBgm) { + core.material.bgms[core.musicStatus.playingBgm].pause(); + } + // 缓存BGM + core.loader.loadBgm(bgm); + // 播放当前BGM + core.material.bgms[bgm].volume = core.musicStatus.userVolume * core.musicStatus.designVolume; + core.material.bgms[bgm].currentTime = startTime || 0; + core.material.bgms[bgm].play(); + core.musicStatus.playingBgm = bgm; + core.musicStatus.lastBgm = bgm; + core.setBgmSpeed(100); +} + +///// 设置当前背景音乐的播放速度 ////// +control.prototype.setBgmSpeed = function (speed, usePitch) { + var bgm = core.musicStatus.playingBgm; + if (main.mode != 'play' || !core.material.bgms[bgm]) return; + bgm = core.material.bgms[bgm]; + if (speed < 30 || speed > 300) return; + bgm.playbackRate = speed / 100; + core.musicStatus.bgmSpeed = speed; + + if (bgm.preservesPitch != null) { + if (bgm.__preservesPitch == null) bgm.__preservesPitch = bgm.preservesPitch; + if (usePitch == null) bgm.preservesPitch = bgm.__preservesPitch; + else if (usePitch) bgm.preservesPitch = false; + else bgm.preservesPitch = true; + core.musicStatus.bgmUsePitch = usePitch; + } +} + +////// 暂停背景音乐的播放 ////// +control.prototype.pauseBgm = function () { + if (main.mode != 'play') return; + try { + if (core.musicStatus.playingBgm) { + core.musicStatus.pauseTime = core.material.bgms[core.musicStatus.playingBgm].currentTime; + core.material.bgms[core.musicStatus.playingBgm].pause(); + core.musicStatus.playingBgm = null; + } + } + catch (e) { + console.log("无法暂停BGM"); + console.error(e); + } + this.setMusicBtn(); +} + +////// 恢复背景音乐的播放 ////// +control.prototype.resumeBgm = function (resumeTime) { + if (main.mode != 'play') return; + try { + var speed = core.musicStatus.bgmSpeed; + var usePitch = core.musicStatus.bgmUsePitch; + core.playBgm(core.musicStatus.playingBgm || core.musicStatus.lastBgm || main.startBgm, + resumeTime ? core.musicStatus.pauseTime : 0); + if (resumeTime) { + core.setBgmSpeed(speed, usePitch); + } + } + catch (e) { + console.log("无法恢复BGM"); + console.error(e); + } + this.setMusicBtn(); +} + +control.prototype.setMusicBtn = function () { + if (core.musicStatus.bgmStatus) + core.dom.musicBtn.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAABWVBMVEX///9iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmL///8AAAC5ubn+/v6xsbEtLS0MDAxmZmZoaGhvb2/c3Nzd3d38/Pz9/f0oKCgpKSl0dHR1dXW6urrb29v7+/v09PTv7+/39/cgICACAgImJibh4eGFhYWGhoaHh4eOjo5paWm7u7vDw8PMzMwyMjI7OztAQEDe3t5FRUVMTEzj4+Pl5eXm5ubp6enr6+tcXFzi4uL19fVeXl74+PgjIyNkZGQGBgaSkpKYmJiampqenp4DAwMwMDBnZ2cICAivr68eHh63t7cLCwsSEhLw8PBhYWEUFBQVFRXNzc3Pz8/Z2dna2toaGhqkpKSlpaWpqamrq6tFOUNAAAAAc3RSTlMAAwQFBhUWGxwkJSYyO0dISVBRUmpvj5CSk5SVoaOlpqiysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyA0IuUgAAAVdJREFUeF5NkVVbw0AQRTcQrLR4IIEGcidJoaUuQHF3d3d3+P/CkuxCzss8nG++mbnDBJXhNt2CpbeFK1kQpSEKidlc8S9qdATRa6UIdQMoxEpDA0Ov3wUAPfW+qLWACydNv9zMrzkJwPK6FB3oHyOfXfuNxvoBQ+GmBYinhHB77TmiVBxoYUw1AYcEq332AS8OYKosAuTT0nza9uU2USYPRJgGxEiSOFywJ3mNARozgBJJzkfLvfu8JgGDWcC9FEsjWzR+y80gYDEAA8QZ3N6kmP1Fs3fEASB7pob7Hh+Wz5L0ci17Or05J7bH6B6dZv05XWK3rG+myV05Ert592Qo55sPuoIr7hEZHHtieIPWy0RU9DLwc3Mnck/vi8/E8XNrDWQtEVnL/ySKMrv0jPwPp870fprcyYifmiEmqGpHkI5q9ofSFIUk2qiwIGpEMyxYhhZRRcMPz89RJ2s9W8wAAAAASUVORK5CYII="; + else + core.dom.musicBtn.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAABYlBMVEX///9iYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmL////8/PwAAABmZmZoaGihoaGioqKxsbG5ubnb29vc3Nzd3d3h4eHi4uL9/f3+/v4tLS1nZ2d0dHSUlJSenp66uroMDAz7+/spKSkoKCgUFBRpaWkVFRVvb291dXU7OzuVlZWYmJhkZGQgICAjIyOkpKQCAgK3t7cGBgbv7++pqamrq6seHh4mJiZhYWGamprp6enr6+saGhpeXl7j4+Pl5eXm5uZKSkrw8PD09PT19fW7u7vDw8PMzMwICAgwMDAyMjILCwtAQECGhoaHh4eBgYGFhYUSEhJXV1dZWVlcXFyOjo6SkpLNzc339/fPz8/Z2dna2tqTk5OlpaWxOPeTAAAAdnRSTlMAAwQFBhUWGxwkJSYyO0dISVBRUmpvj5CSk5SVoaOlpqiysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyNuo+uwAAAWJJREFUeF5NkmV34zAQReUm7WbTuJBNunY3bvXGDjNTkZkZlpn5/9eR5FPfbzr3jGb0RkwRiMQMDm7EIgHmRxtLwMOaHHoQjwz4MUKeCM8AWMrmd7u7f/aXAMyOShHiQD1n04DtN5e5FMBFlSauIsm585dKi4CpuSYKJIv1tBDVmvOSqJgEoowFLSBHaQh10XHWiCgHWEGmAw2blPrvOK/KRJUGoLM4kCVSKrWz7HwgoiwQZyaQJ0+9PvxV23BNATAZB25IqX9b3+jTW9fcApwB6NLgUD5NY3mPXnwmFwBezff1ztzRFzTp94FXMy36HDuCa2RafdnnmZqtL818Gl9/qNnEeyrUk2aTPiKj3qMyWBVi/YSuWq5qiwxkbtX3vYWzdz/l8M0k8ERlvViiB1Ygslb7SbVtJezncj+Cx5bYaeGuonZqhZlieAp+no74/s5EAh6JcY35Cepxk4ObcT3IJPe/1lKsDpFCFQAAAABJRU5ErkJggg=="; +} + +////// 更改背景音乐的播放 ////// +control.prototype.triggerBgm = function () { + if (main.mode != 'play') return; + + core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus; + if (core.musicStatus.bgmStatus) + this.resumeBgm(); + else + this.pauseBgm(); + core.setLocalStorage('bgmStatus', core.musicStatus.bgmStatus); +} + +let playingSoundList = []; + +////// 播放音频 ////// +control.prototype.playSound = function (sound, pitch, inputCallback) { + sound = core.getMappedName(sound); + if (main.mode != 'play' || !core.musicStatus.soundStatus || !core.material.sounds[sound] || playingSoundList.includes(sound)) return; + playingSoundList.push(sound); + const callback = () => { + playingSoundList = playingSoundList.filter((soundName) => soundName !== sound); + if (inputCallback) inputCallback(); + } + try { + if (core.musicStatus.audioContext != null) { + var source = core.musicStatus.audioContext.createBufferSource(); + source.__name = sound; + source.buffer = core.material.sounds[sound]; + source.connect(core.musicStatus.gainNode); + var id = setTimeout(null); + if (pitch && pitch >= 30 && pitch <= 300) { + source.playbackRate.setValueAtTime(pitch / 100, 0); + } + source.onended = function () { + delete core.musicStatus.playingSounds[id]; + if (callback) callback(); + } + core.musicStatus.playingSounds[id] = source; + if (source.start) source.start(0); + else if (source.noteOn) source.noteOn(0); + return id; + } + else { + core.material.sounds[sound].volume = core.musicStatus.userVolume; + core.material.sounds[sound].play(); + if (callback) callback(); + } + } + catch (e) { + console.log("无法播放SE " + sound); + console.error(e); + } +} + +////// 停止所有音频 ////// +control.prototype.stopSound = function (id) { + if (id == null) { + Object.keys(core.musicStatus.playingSounds).forEach(function (id) { + core.control.stopSound(id); + }); + return; + } + var source = core.musicStatus.playingSounds[id]; + if (!source) return; + try { + if (source.stop) source.stop(); + else if (source.noteOff) source.noteOff(); + } + catch (e) { + console.error(e); + } + delete core.musicStatus.playingSounds[id]; +} + +////// 获得当前正在播放的所有(指定)音效的id列表 ////// +control.prototype.getPlayingSounds = function (name) { + name = core.getMappedName(name); + return Object.keys(core.musicStatus.playingSounds).filter(function (one) { + return name == null || core.musicStatus.playingSounds[one].__name == name + }); +} + +////// 检查bgm状态 ////// +control.prototype.checkBgm = function () { + core.playBgm(core.musicStatus.playingBgm || main.startBgm); +} + +///// 设置屏幕放缩 ////// +control.prototype.setDisplayScale = function (delta) { + var index = core.domStyle.availableScale.indexOf(core.domStyle.scale); + if (index < 0) return; + index = (index + delta + core.domStyle.availableScale.length) % core.domStyle.availableScale.length; + core.domStyle.scale = core.domStyle.availableScale[index]; + core.setLocalStorage('scale', core.domStyle.scale); + core.resize(); +} + +// ------ 状态栏,工具栏等相关 ------ // + +////// 清空状态栏 ////// +control.prototype.clearStatusBar = function () { + Object.keys(core.statusBar).forEach(function (e) { + if (core.statusBar[e].innerHTML != null) { + core.statusBar[e].innerHTML = " "; + core.statusBar[e].removeAttribute('_style'); + core.statusBar[e].removeAttribute('_value'); + } + }) + core.statusBar.image.book.style.opacity = 0.3; + if (!core.flags.equipboxButton) + core.statusBar.image.fly.style.opacity = 0.3; +} + +////// 更新状态栏 ////// +control.prototype.updateStatusBar = function (doNotCheckAutoEvents, immediate) { + if (immediate) { + return this.updateStatusBar_update(); + } + if (!doNotCheckAutoEvents) this.noAutoEvents = false; + if (core.isReplaying()) return this.updateStatusBar_update(); + requestAnimationFrame(this.updateStatusBar_update) +} + +control.prototype.updateStatusBar_update = function () { + if (!core.isPlaying() || core.hasFlag('__statistics__')) return; + core.control.controldata.updateStatusBar(); + if (!core.control.noAutoEvents) core.checkAutoEvents(); + core.control._updateStatusBar_setToolboxIcon(); + core.clearRouteFolding(); + core.control.noAutoEvents = true; +} + +/** 切换replay/pause 并刷新工具栏图标的完成度 */ +control.prototype._updateStatusBar_setToolboxIcon = function () { + if (core.isReplaying()) { + main.statusBar.image.play.src = core.status.replay.pausing ? core.statusBar.icons.play.src : core.statusBar.icons.pause.src; + } + core.statusBar.image.book.style.opacity = core.hasItem('book') ? 1 : 0.3; + core.statusBar.image.fly.style.opacity = core.hasItem('fly') ? 1 : 0.3; + core.statusBar.image.undoRollback.style.opacity = + (core.saves.autosave.data != null && core.saves.autosave.now < core.saves.autosave.data.length) ? 1 : 0.3; + core.statusBar.image.save.style.opacity = core.hasFlag('__forbidSave__') ? 0.3 : 1; + +} + +control.prototype.showStatusBar = function () { + if (main.mode == 'editor') return; + if (core.domStyle.showStatusBar) return; + var statusItems = core.dom.status; + core.domStyle.showStatusBar = true; + core.removeFlag('hideStatusBar'); + // 显示 + for (var i = 0; i < statusItems.length; ++i) + statusItems[i].style.opacity = 1; + core.setToolbarButton(core.domStyle.toolbarBtn); + // this.setToolbarButton(false); + core.dom.tools.hard.style.display = 'block'; + core.dom.toolBar.style.display = 'block'; +} + +control.prototype.hideStatusBar = function (showToolbox) { + if (main.mode == 'editor') return; + + // 如果原本就是隐藏的,则先显示 + if (!core.domStyle.showStatusBar) + this.showStatusBar(); + if (core.isReplaying()) showToolbox = true; + + var statusItems = core.dom.status, toolItems = core.dom.tools; + core.domStyle.showStatusBar = false; + core.setFlag('hideStatusBar', true); + core.setFlag('showToolbox', showToolbox || null); + // 隐藏 + for (var i = 0; i < statusItems.length; ++i) + statusItems[i].style.opacity = 0; + if (!showToolbox) core.setToolbarButton('hide'); + // if ((!core.domStyle.isVertical && !core.flags.extendToolbar) || !showToolbox) { + // for (var i = 0; i < toolItems.length; ++i) + // toolItems[i].style.display = 'none'; + // } + // if (!core.domStyle.isVertical && !core.flags.extendToolbar) { + // core.dom.toolBar.style.display = 'none'; + // } +} + +////// 更新状态栏的勇士图标 ////// +control.prototype.updateHeroIcon = function (name) { + name = name || "hero.png"; + if (core.statusBar.icons.name == name) return; + core.statusBar.icons.name = name; + + var image = core.material.images.hero; + // 全身图 + var w = core.material.icons.hero.width || 32; + var h = core.material.icons.hero.height || 48; + var ratio = Math.min(w / h, 1), width = 32 * ratio, left = 16 - width / 2; + + var canvas = document.createElement("canvas"); + var ctx = canvas.getContext("2d"); + canvas.width = 32; + canvas.height = 32; + core.drawImage(ctx, image, 0, 0, w, h, left, 0, width, 32); + + core.statusBar.image.name.src = canvas.toDataURL("image/png"); +} + +////// 改变工具栏为按钮1-8 ////// +// control.prototype.setToolbarButton = function (useButton) { +// if (!core.domStyle.showStatusBar) { +// // 隐藏状态栏时检查竖屏 +// if (!core.domStyle.isVertical && !core.flags.extendToolbar) { +// for (var i = 0; i < core.dom.tools.length; ++i) +// core.dom.tools[i].style.display = 'none'; +// return; +// } +// if (!core.hasFlag('showToolbox')) return; +// else core.dom.tools.hard.style.display = 'block'; +// } + +// if (useButton == null) useButton = core.domStyle.toolbarBtn; +// if ((!core.domStyle.isVertical && !core.flags.extendToolbar) || core.isReplaying()) useButton = false; +// core.domStyle.toolbarBtn = useButton; + +// if (useButton) { +// ["book", "fly", "toolbox", "keyboard", "shop", "save", "load", "settings"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'none'; +// }); +// ["rollback", "undoRollback"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'none'; +// }); +// ["btn1", "btn2", "btn3", "btn4", "btn5", "btn6", "btn7", "btnAlt"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'block'; +// }) +// if (!core.flags.showHard) { +// ["btn8", "btn9"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'block'; +// }); +// } +// main.statusBar.image.btnAlt.style.filter = core.getLocalStorage('altKey') ? 'sepia(1) contrast(1.5)' : ''; +// } +// else { +// ["btn1", "btn2", "btn3", "btn4", "btn5", "btn6", "btn7", "btnAlt"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'none'; +// }); +// ["btn8", "btn9"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'none'; +// }); +// ["book", "fly", "toolbox", "save", "load", "settings"].forEach(function (t) { +// core.statusBar.image[t].style.display = 'block'; +// }); +// if (core.domStyle.isVertical || core.flags.extendToolbar || !core.flags.showHard) { +// core.statusBar.image.shop.style.display = "block"; +// } +// else core.statusBar.image.shop.style.display = "none"; + +// ["rollback", "undoRollback"].forEach(function (t) { +// core.statusBar.image[t].style.display = core.flags.showHard ? "none" : "block"; +// }); + +// core.statusBar.image.keyboard.style.display +// = core.domStyle.isVertical || core.flags.extendToolbar ? "block" : "none"; +// } +// } + +////// ------ resize处理 ------ // + +control.prototype._shouldDisplayStatus = function (id) { + if (id == null) { + var toDraw = [], status = core.dom.status; + for (var i = 0; i < status.length; ++i) { + var dom = core.dom.status[i], idCol = dom.id; + if (idCol.indexOf("Col") != idCol.length - 3) continue; + var id = idCol.substring(0, idCol.length - 3); + if (!this._shouldDisplayStatus(id)) continue; + toDraw.push(id); + } + return toDraw; + } + var obj = {}; + core.flags.statusBarItems.forEach(function (v) { obj[v] = true; }) + switch (id) { + case 'floor': return obj.enableFloor; + case 'name': return obj.enableName; + case 'lv': return obj.enableLv; + case 'hp': return obj.enableHP; + case 'hpmax': return obj.enableHPMax; + case 'mana': return obj.enableMana; + case 'atk': return obj.enableAtk; + case 'def': return obj.enableDef; + case 'mdef': return obj.enableMDef; + case 'money': return obj.enableMoney; + case 'exp': return obj.enableExp && !obj.levelUpLeftMode; + case 'up': return obj.enableLevelUp; + case 'skill': return obj.enableSkill; + case 'key': return obj.enableKeys; + case 'pzf': return obj.enablePZF; + case 'debuff': return obj.enableDebuff; + default: return true; + } +} + +////// 注册一个resize函数 ////// +// name为名称,可供注销使用 +// func可以是一个函数,或者是插件中的函数名;可以接受obj参数,详见resize函数。 +control.prototype.registerResize = function (name, func) { + this.unregisterResize(name); + this.resizes.push({ name: name, func: func }); +} + +////// 注销一个resize函数 ////// +control.prototype.unregisterResize = function (name) { + this.resizes = this.resizes.filter(function (b) { return b.name != name; }); +} + +/** 执行所有resize函数 */ +control.prototype._doResize = function (obj) { + for (var i in this.resizes) { + try { + if (core.doFunc(this.resizes[i].func, this, obj)) return true; + } catch (e) { + console.error(e); + console.error("ERROR in resizes[" + this.resizes[i].name + "]:已自动注销该项。"); + this.unregisterResize(this.resizes[i].name); + } + } + return false; +} + +/** 执行单个特定名称的resize函数 */ +control.prototype._doOneResize = function (obj, type) { + try { + const currEvent = this.resizes.find((resizeEvent) => resizeEvent.name === type); + if (!currEvent) return; + if (core.doFunc(currEvent.func, this, obj)) return true; + } catch (e) { + console.error(e); + console.error("ERROR in resizes[" + this.resizes[type].name + "]:已自动注销该项。"); + this.unregisterResize(this.resizes[type].name); + } + return false; +} + +////// 屏幕分辨率改变后重新自适应 ////// +control.prototype.resize = function (type) { + if (main.mode == 'editor') return; + var clientWidth = main.dom.body.clientWidth, clientHeight = main.dom.body.clientHeight; + var CANVAS_WIDTH = core.__PIXELS__, BAR_WIDTH = Math.round(core.__PIXELS__ * 0.31); + var BORDER = 3; + var extendToolbar = core.flags.extendToolbar; + + var horizontalMaxRatio = (clientHeight - 2 * BORDER - (extendToolbar ? BORDER : 0)) / (CANVAS_WIDTH + (extendToolbar ? 38 : 0)); + + if (clientWidth - 3 * BORDER >= CANVAS_WIDTH + BAR_WIDTH || (clientWidth > clientHeight && horizontalMaxRatio < 1)) { + // 横屏 + core.domStyle.isVertical = false; + + core.domStyle.availableScale = []; + [1, 1.25, 1.5, 1.75, 2, 2.25, 2.5].forEach(function (v) { + if (clientWidth - 3 * BORDER >= v * (CANVAS_WIDTH + BAR_WIDTH) && horizontalMaxRatio >= v) { + core.domStyle.availableScale.push(v); + } + }); + if (core.domStyle.availableScale.indexOf(core.domStyle.scale) < 0) { + core.domStyle.scale = Math.min(1, horizontalMaxRatio); + } + } + else { + // 竖屏 + core.domStyle.isVertical = true; + core.domStyle.scale = Math.min((clientWidth - 2 * BORDER) / CANVAS_WIDTH); + core.domStyle.availableScale = []; + extendToolbar = false; + } + + var statusDisplayArr = this._shouldDisplayStatus(), count = statusDisplayArr.length; + var statusCanvas = core.flags.statusCanvas, statusCanvasRows = core.values.statusCanvasRowsOnMobile || 3; + var col = statusCanvas ? statusCanvasRows : Math.ceil(count / 3); + if (col > 5) { + if (statusCanvas) alert("自绘状态栏的在竖屏下的行数应不超过5!"); + else alert("当前状态栏数目(" + count + ")大于15,请调整到不超过15以避免手机端出现显示问题。"); + } + var globalAttribute = core.status.globalAttribute || core.initStatus.globalAttribute; + + var obj = { + clientWidth: clientWidth, + clientHeight: clientHeight, + CANVAS_WIDTH: CANVAS_WIDTH, + BORDER: BORDER, + BAR_WIDTH: BAR_WIDTH, + TOOLBAR_HEIGHT: 38, + outerSize: CANVAS_WIDTH * core.domStyle.scale + 2 * BORDER, + globalAttribute: globalAttribute, + border: '3px ' + core.arrayToRGBA(globalAttribute.borderColor) + ' solid', + statusDisplayArr: statusDisplayArr, + count: count, + col: col, + statusBarHeightInVertical: core.domStyle.isVertical ? (32 * col + 6) * core.domStyle.scale + 2 * BORDER : 0, + toolbarHeightInVertical: core.domStyle.isVertical ? 38 * core.domStyle.scale + 2 * BORDER : 0, + extendToolbar: extendToolbar, + is15x15: core.__SIZE__ == 15 + }; + + if (type != null) { + if (type instanceof Array) type.forEach((oneType) => this._doOneResize(obj, oneType)); + else this._doOneResize(obj, type); + } + else this._doResize(obj); + // this.setToolbarButton(); + core.updateStatusBar(); +} + +control.prototype._resize_gameGroup = function (obj) { + var startBackground = core.domStyle.isVertical ? (main.styles.startVerticalBackground || main.styles.startBackground) : main.styles.startBackground; + if (main.dom.startBackground.getAttribute('__src__') != startBackground) { + main.dom.startBackground.setAttribute('__src__', startBackground); + main.dom.startBackground.src = startBackground; + } + + var gameGroup = core.dom.gameGroup; + var totalWidth, totalHeight; + if (core.domStyle.isVertical) { + totalWidth = obj.outerSize; + totalHeight = obj.outerSize + obj.statusBarHeightInVertical + obj.toolbarHeightInVertical + } + else { + totalWidth = obj.outerSize + obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER; + totalHeight = obj.outerSize + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0); + } + gameGroup.style.width = totalWidth + "px"; + gameGroup.style.height = totalHeight + "px"; + gameGroup.style.left = (obj.clientWidth - totalWidth) / 2 + "px"; + gameGroup.style.top = (obj.clientHeight - totalHeight) / 2 + "px"; + // floorMsgGroup + var floorMsgGroup = core.dom.floorMsgGroup; + floorMsgGroup.style = obj.globalAttribute.floorChangingStyle; + floorMsgGroup.style.width = obj.outerSize - 2 * obj.BORDER + "px"; + floorMsgGroup.style.height = totalHeight - 2 * obj.BORDER + "px"; + floorMsgGroup.style.fontSize = 16 * core.domStyle.scale + "px"; + // startPanel + core.dom.startPanel.style.fontSize = 16 * core.domStyle.scale + "px"; + // musicBtn + if (core.domStyle.isVertical || core.domStyle.scale < 1) { + core.dom.musicBtn.style.right = core.dom.musicBtn.style.bottom = "3px"; + } + else { + core.dom.musicBtn.style.right = (obj.clientWidth - totalWidth) / 2 + "px"; + core.dom.musicBtn.style.bottom = (obj.clientHeight - totalHeight) / 2 - 27 + "px"; + } +} + +control.prototype.registerDymCanvasResizeEvent = function (name, event) { + this.dymCanvasResizeEvents[name] = event; +} + +control.prototype.unregisterDymCanvasResizeEvent = function (name) { + delete this.dymCanvasResizeEvents[name]; +} + +control.prototype._resize_canvas = function (obj) { + var innerSize = (obj.CANVAS_WIDTH * core.domStyle.scale) + "px"; + if (!core.isPlaying()) { + for (var i = 0; i < core.dom.gameCanvas.length; ++i) { + var ctx = core.dom.gameCanvas[i].getContext('2d'); + core.resizeCanvas(ctx, core.__PIXELS__, core.__PIXELS__); + core.dom.gameCanvas[i].style.width = core.dom.gameCanvas[i].style.height = innerSize; + } + } else { + requestAnimationFrame(function () { + for (var i = 0; i < core.dom.gameCanvas.length; ++i) + core.dom.gameCanvas[i].style.width = core.dom.gameCanvas[i].style.height = innerSize; + }); + } + core.dom.gif.style.width = core.dom.gif.style.height = innerSize; + core.dom.gif2.style.width = core.dom.gif2.style.height = innerSize; + core.dom.gameDraw.style.width = core.dom.gameDraw.style.height = innerSize; + core.dom.gameDraw.style.top = obj.statusBarHeightInVertical + "px"; + core.dom.gameDraw.style.right = 0; + core.dom.gameDraw.style.border = obj.border; + // resize bigmap + core.bigmap.canvas.forEach(function (cn) { + var ratio = core.canvas[cn].canvas.hasAttribute('isHD') ? core.domStyle.ratio : 1; + core.canvas[cn].canvas.style.width = core.canvas[cn].canvas.width / ratio * core.domStyle.scale + "px"; + core.canvas[cn].canvas.style.height = core.canvas[cn].canvas.height / ratio * core.domStyle.scale + "px"; + }); + // resize dynamic canvas + if (!core.isPlaying()) { + for (var name in core.dymCanvas) { + var ctx = core.dymCanvas[name], canvas = ctx.canvas; + // core.maps._setHDCanvasSize(ctx, parseFloat(canvas.getAttribute('_width')), parseFloat(canvas.getAttribute('_height'))); + canvas.style.left = parseFloat(canvas.getAttribute("_left")) * core.domStyle.scale + "px"; + canvas.style.top = parseFloat(canvas.getAttribute("_top")) * core.domStyle.scale + "px"; + var scale = canvas.getAttribute('_scale') || 1; + core.resizeCanvas(canvas, canvas.width * scale / core.domStyle.scale, canvas.height * scale / core.domStyle.scale); + } + } else { + for (var name in core.dymCanvas) { + var ctx = core.dymCanvas[name], canvas = ctx.canvas; + core.resizeCanvas(ctx, parseFloat(canvas.getAttribute("_width")), parseFloat(canvas.getAttribute("_height"))); + canvas.style.left = parseFloat(canvas.getAttribute("_left")) * core.domStyle.scale + "px"; + canvas.style.top = parseFloat(canvas.getAttribute("_top")) * core.domStyle.scale + "px"; + if (this.dymCanvasResizeEvents[name]) { + const event = this.dymCanvasResizeEvents[name]; + event(); + } + } + } + // resize next + main.dom.next.style.width = main.dom.next.style.height = 5 * core.domStyle.scale + "px"; + main.dom.next.style.borderBottomWidth = main.dom.next.style.borderRightWidth = 4 * core.domStyle.scale + "px"; +} + +control.prototype._resize_statusBar = function (obj) { + // statusBar + var statusBar = core.dom.statusBar; + if (core.domStyle.isVertical) { + statusBar.style.width = obj.outerSize + "px"; + statusBar.style.height = obj.statusBarHeightInVertical + "px"; + statusBar.style.background = obj.globalAttribute.statusTopBackground; + statusBar.style.fontSize = 16 * core.domStyle.scale + "px"; + } + else { + statusBar.style.width = (obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER) + "px"; + statusBar.style.height = obj.outerSize + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0) + "px"; + statusBar.style.background = obj.globalAttribute.statusLeftBackground; + // --- 计算文字大小 + if (obj.extendToolbar) { + statusBar.style.fontSize = 16 * core.domStyle.scale + "px"; + } else { + statusBar.style.fontSize = 16 * Math.min(1, (core.__HALF_SIZE__ + 3) / obj.count) * core.domStyle.scale + "px"; + } + } + statusBar.style.display = 'block'; + statusBar.style.borderTop = statusBar.style.borderLeft = obj.border; + statusBar.style.borderRight = core.domStyle.isVertical ? obj.border : ''; + statusBar.style.borderBottom = core.domStyle.isVertical ? '' : obj.border; + // 自绘状态栏 + if (core.domStyle.isVertical) { + core.dom.statusCanvas.style.width = obj.CANVAS_WIDTH * core.domStyle.scale + "px"; + core.dom.statusCanvas.style.height = obj.statusBarHeightInVertical - 3 + "px"; + core.maps._setHDCanvasSize(core.dom.statusCanvasCtx, obj.CANVAS_WIDTH, obj.col * 32 + 9); + } + else { + core.dom.statusCanvas.style.width = obj.BAR_WIDTH * core.domStyle.scale + "px"; + core.dom.statusCanvas.style.height = obj.outerSize - 2 * obj.BORDER + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER : 0) + "px"; + core.maps._setHDCanvasSize(core.dom.statusCanvasCtx, obj.BAR_WIDTH, obj.CANVAS_WIDTH + (obj.extendToolbar ? obj.TOOLBAR_HEIGHT + obj.BORDER : 0)); + } + core.dom.statusCanvas.style.display = core.flags.statusCanvas ? "block" : "none"; +} + +control.prototype._resize_status = function (obj) { + var statusHeight; + if (core.domStyle.isVertical) { + statusHeight = 32 * core.domStyle.scale * 0.8; + } else { + statusHeight = (obj.extendToolbar ? core.__SIZE__ : core.__HALF_SIZE__ + 3) / obj.count * 32 * core.domStyle.scale * 0.8; + } + // status + for (var i = 0; i < core.dom.status.length; ++i) { + var id = core.dom.status[i].id, style = core.dom.status[i].style; + if (id.endsWith("Col")) id = id.substring(0, id.length - 3); + style.display = core.flags.statusCanvas || obj.statusDisplayArr.indexOf(id) < 0 ? 'none' : 'block'; + style.margin = 3 * core.domStyle.scale + "px"; + style.height = statusHeight + "px"; + style.maxWidth = obj.BAR_WIDTH * core.domStyle.scale * (core.domStyle.isVertical ? 0.95 : 1) + obj.BORDER + "px"; + if (obj.is15x15 && !core.domStyle.isVertical) + style.marginLeft = 11 * core.domStyle.scale + "px"; + } + // statusLabels, statusTexts + for (var i = 0; i < core.dom.statusLabels.length; ++i) { + core.dom.statusLabels[i].style.lineHeight = statusHeight + "px"; + core.dom.statusLabels[i].style.marginLeft = 6 * core.domStyle.scale + "px"; + } + for (var i = 0; i < core.dom.statusTexts.length; ++i) { + core.dom.statusTexts[i].style.color = core.arrayToRGBA(obj.globalAttribute.statusBarColor); + } + // keys + if (core.flags.statusBarItems.indexOf('enableGreenKey') >= 0) { + core.dom.keyCol.style.fontSize = '0.75em'; + core.statusBar.greenKey.style.display = ''; + } else { + core.dom.keyCol.style.fontSize = ''; + core.statusBar.greenKey.style.display = 'none'; + } +} + +/** toolBar的样式 */ +control.prototype._resize_toolBar = function (obj) { + var toolBar = core.dom.toolBar; + if (core.domStyle.isVertical) { + toolBar.style.left = 0; + toolBar.style.right = ""; + toolBar.style.width = obj.outerSize + "px"; + toolBar.style.top = obj.statusBarHeightInVertical + obj.outerSize + "px"; + toolBar.style.height = obj.toolbarHeightInVertical + "px"; + toolBar.style.background = obj.globalAttribute.toolsBackground; + } + else { + if (obj.extendToolbar) { + toolBar.style.left = ""; + toolBar.style.right = 0; + toolBar.style.width = obj.outerSize + "px"; + toolBar.style.top = obj.outerSize + "px"; + toolBar.style.height = obj.TOOLBAR_HEIGHT * core.domStyle.scale + obj.BORDER + "px"; + toolBar.style.background = obj.globalAttribute.toolsBackground; + } else { + toolBar.style.left = 0; + toolBar.style.right = ""; + toolBar.style.width = obj.BAR_WIDTH * core.domStyle.scale + obj.BORDER + "px"; + toolBar.style.top = 0.718 * obj.outerSize + "px"; + toolBar.style.height = 0.281 * obj.outerSize + "px"; + toolBar.style.background = 'transparent'; + } + } + toolBar.style.borderLeft = obj.border; + toolBar.style.borderRight = toolBar.style.borderBottom = core.domStyle.isVertical || obj.extendToolbar ? obj.border : ''; + toolBar.style.fontSize = 16 * core.domStyle.scale + "px"; + + // 本项控制toolBar本身的显示与隐藏 + if (!core.domStyle.showStatusBar && !core.domStyle.isVertical && !obj.extendToolbar) { + toolBar.style.display = 'none'; + } else { + toolBar.style.display = 'block'; + } +} + +/** toolBar中各个tools的样式 */ +control.prototype._resize_tools = function (obj) { + var toolsHeight = 32 * core.domStyle.scale * ((core.domStyle.isVertical || obj.extendToolbar) && !obj.is15x15 ? 0.95 : 1); + var toolsMarginLeft; + if (core.domStyle.isVertical || obj.extendToolbar) + toolsMarginLeft = (core.__HALF_SIZE__ - 3) * 3 * core.domStyle.scale - 1; + else + toolsMarginLeft = (obj.BAR_WIDTH * core.domStyle.scale - 9 - toolsHeight * 3) / 4; + for (var i = 0; i < core.dom.tools.length; ++i) { + var style = core.dom.tools[i].style; + style.height = toolsHeight + "px"; + style.marginLeft = toolsMarginLeft + "px"; + style.marginTop = 3 * core.domStyle.scale + "px" + } + core.dom.hard.style.lineHeight = toolsHeight + "px"; + if (core.domStyle.isVertical || obj.extendToolbar) { + const toolsCount = core.domStyle.toolsCount; + if (toolsCount <= 8) { + core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - (toolsCount + 0.5) * toolsHeight - 22 + "px"; + } + else if (toolsCount === 9) { + core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - toolsCount * toolsHeight - 22 + "px"; + } + else { // toolsCount is 10 + core.dom.hard.style.width = obj.outerSize - (toolsCount + 1) * toolsMarginLeft - toolsCount * toolsHeight - 13 + "px"; + core.dom.hard.style.marginLeft = "1px"; + } + } + else { + core.dom.hard.style.width = obj.BAR_WIDTH * core.domStyle.scale - 9 - 2 * toolsMarginLeft + "px"; + if (!obj.is15x15) core.dom.hard.style.marginTop = 0; + } +} diff --git a/project/plugins.js b/project/plugins.js index 173e51b0..81f71a13 100644 --- a/project/plugins.js +++ b/project/plugins.js @@ -1,10366 +1,10384 @@ -// @ts-check -/// -var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = -{ - "init": function () { - - console.log("插件编写测试"); - - // 可以写一些直接执行的代码 - // 在这里写的代码将会在【资源加载前】被执行,此时图片等资源尚未被加载。 - // 请勿在这里对包括bgm,图片等资源进行操作。 - - core.plugin._afterLoadResources = function () { - // 本函数将在所有资源加载完毕后,游戏开启前被执行 - // 可以在这个函数里面对资源进行一些操作。 - // 若需要进行切分图片,可以使用 core.splitImage() 函数,或直接在全塔属性-图片切分中操作 - } - - // 可以在任何地方(如afterXXX或自定义脚本事件)调用函数,方法为 core.plugin.xxx(); - // 从V2.6开始,插件中用this.XXX方式定义的函数也会被转发到core中,详见文档-脚本-函数的转发。 - }, - "shop": function () { - // 【全局商店】相关的功能 - /** - * 打开一个全局商店 - * @param {string} shopId - * @param {boolean} noRoute - */ - core.plugin.openShop = function (shopId, noRoute) { - const shop = core.status.shops[shopId]; - // Step 1: 检查能否打开此商店 - if (!this.canOpenShop(shopId)) { - core.drawTip("该商店尚未开启"); - return false; - } - - // Step 2: (如有必要)记录打开商店的脚本事件 - if (!noRoute) { - core.status.route.push("shop:" + shopId); - } - - // Step 3: 检查道具商店 or 公共事件 - if (shop.item) { - if (core.openItemShop) { - core.openItemShop(shopId); - } else { - core.playSound('操作失败'); - core.insertAction("道具商店插件不存在!请检查是否存在该插件!"); - } - return; - } - if (shop.commonEvent) { - core.insertCommonEvent(shop.commonEvent, shop.args); - return; - } - - _shouldProcessKeyUp = true; - - // Step 4: 执行标准公共商店 - core.insertAction(this._convertShop(shop)); - return true; - } - - /** - * @param {shop} shop - * 将一个全局商店转变成可预览的公共事件 - */ - core.plugin._convertShop = function (shop) { - return [ - { "type": "function", "function": "function() {core.addFlag('@temp@shop', 1);}" }, - { - "type": "while", - "condition": "true", - "data": [ - // 检测能否访问该商店 - { - "type": "if", - "condition": "core.isShopVisited('" + shop.id + "')", - "true": [ - // 可以访问,直接插入执行效果 - { "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', false) }" }, - ], - "false": [ - // 不能访问的情况下:检测能否预览 - { - "type": "if", - "condition": shop.disablePreview, - "true": [ - // 不可预览,提示并退出 - { "type": "playSound", "name": "操作失败" }, - "当前无法访问该商店!", - { "type": "break" }, - ], - "false": [ - // 可以预览:将商店全部内容进行替换 - { "type": "tip", "text": "当前处于预览模式,不可购买" }, - { "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', true) }" }, - ] - } - ] - } - ] - }, - { "type": "function", "function": "function() {core.addFlag('@temp@shop', -1);}" } - ]; - } - - /** - * @param {string} shopId - * @param {boolean} previewMode - */ - core.plugin._convertShop_replaceChoices = function (shopId, previewMode) { - var shop = core.status.shops[shopId]; - var choices = (shop.choices || []).filter(function (choice) { - if (choice.condition == null || choice.condition == '') return true; - try { return core.calValue(choice.condition); } catch (e) { return true; } - }).map(function (choice) { - var ableToBuy = core.calValue(choice.need); - return { - "text": choice.text, - "icon": choice.icon, - "color": ableToBuy && !previewMode ? choice.color : [153, 153, 153, 1], - "action": ableToBuy && !previewMode ? [{ "type": "playSound", "name": "商店" }].concat(choice.action) : [ - { "type": "playSound", "name": "操作失败" }, - { "type": "tip", "text": previewMode ? "预览模式下不可购买" : "购买条件不足" } - ] - }; - }).concat({ "text": "离开", "action": [{ "type": "playSound", "name": "取消" }, { "type": "break" }] }); - core.insertAction({ "type": "choices", "text": shop.text, "choices": choices }); - } - - /** - * 是否访问过某个快捷商店 - */ - core.plugin.isShopVisited = function (id) { - if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {}); - const shops = core.getFlag("__shops__"); - if (!shops[id]) shops[id] = {}; - return shops[id].visited; - } - - /// 当前应当显示的快捷商店列表 - core.plugin.listShopIds = function () { - return Object.keys(core.status.shops).filter(function (id) { - return core.isShopVisited(id) || !core.status.shops[id].mustEnable; - }); - } - - /** 是否能够打开某个商店 **/ - core.plugin.canOpenShop = function (id) { - if (this.isShopVisited(id)) return true; - const shop = core.status.shops[id]; - if (shop.item || shop.commonEvent || shop.mustEnable) return false; - return true; - } - - /** 启用或禁用某个快捷商店 */ - core.plugin.setShopVisited = function (id, visited) { - if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {}); - var shops = core.getFlag("__shops__"); - if (!shops[id]) shops[id] = {}; - if (visited) shops[id].visited = true; - else delete shops[id].visited; - } - - /** - * 能否使用快捷商店 - * @param {string} id - * @returns {'当前楼层不能使用快捷商店。'|null} - */ - core.plugin.canUseQuickShop = function (id) { - // 如果返回一个字符串,表示不能,字符串为不能使用的提示 - // 返回null代表可以使用 - - // 检查当前楼层的canUseQuickShop选项是否为false - if (core.status.thisMap.canUseQuickShop === false) - return '当前楼层不能使用快捷商店。'; - return null; - } - - let _shouldProcessKeyUp = true; - - /// 允许商店X键退出 - core.registerAction('keyUp', 'shops', function (keycode) { - if (!core.status.lockControl || core.status.event.id != 'action') return false; - if ((keycode == 13 || keycode == 32) && !_shouldProcessKeyUp) { - _shouldProcessKeyUp = true; - return true; - } - - if (!core.hasFlag("@temp@shop") || core.status.event.data.type != 'choices') return false; - var data = core.status.event.data.current; - var choices = data.choices; - var topIndex = core.actions._getChoicesTopIndex(choices.length); - if (keycode == 88 || keycode == 27) { // X, ESC - core.actions._clickAction(core.actions.HSIZE, topIndex + choices.length - 1); - return true; - } - return false; - }, 60); - - /// 允许长按空格或回车连续执行操作 - core.registerAction('keyDown', 'shops', function (keycode) { - if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false; - if (core.status.event.data.type != 'choices') return false; - core.status.onShopLongDown = true; - var data = core.status.event.data.current; - var choices = data.choices; - var topIndex = core.actions._getChoicesTopIndex(choices.length); - if (keycode == 13 || keycode == 32) { // Space, Enter - core.actions._clickAction(core.actions.HSIZE, topIndex + core.status.event.selection); - _shouldProcessKeyUp = false; - return true; - } - return false; - }, 60); - - // 允许长按屏幕连续执行操作 - core.registerAction('longClick', 'shops', function (x, y, px, py) { - if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false; - if (core.status.event.data.type != 'choices') return false; - var data = core.status.event.data.current; - var choices = data.choices; - var topIndex = core.actions._getChoicesTopIndex(choices.length); - if (x >= core.actions.CHOICES_LEFT && x <= core.actions.CHOICES_RIGHT && y >= topIndex && y < topIndex + choices.length) { - core.actions._clickAction(x, y); - return true; - } - return false; - }, 60); - }, - "removeMap": function () { - // 高层塔砍层插件,删除后不会存入存档,不可浏览地图也不可飞到。 - // 推荐用法: - // 对于超高层或分区域塔,当在1区时将2区以后的地图删除;1区结束时恢复2区,进二区时删除1区地图,以此类推 - // 这样可以大幅减少存档空间,以及加快存读档速度 - - /** - * 删除楼层 - * @example // 删除MT1~MT300之间的全部层 - * core.removeMaps("MT1", "MT300") - * @example // 只删除MT10层 - * core.removeMaps("MT10") - * @param {string} fromId - * @param {string} toId - */ - function removeMaps(fromId, toId) { - toId = toId || fromId; - var fromIndex = core.floorIds.indexOf(fromId), - toIndex = core.floorIds.indexOf(toId); - if (toIndex < 0) toIndex = core.floorIds.length - 1; - flags.__visited__ = flags.__visited__ || {}; - flags.__removed__ = flags.__removed__ || []; - flags.__disabled__ = flags.__disabled__ || {}; - flags.__leaveLoc__ = flags.__leaveLoc__ || {}; - for (var i = fromIndex; i <= toIndex; ++i) { - var floorId = core.floorIds[i]; - if (core.status.maps[floorId].deleted) continue; - delete flags.__visited__[floorId]; - flags.__removed__.push(floorId); - delete flags.__disabled__[floorId]; - delete flags.__leaveLoc__[floorId]; - (core.status.autoEvents || []).forEach(function (event) { - if (event.floorId == floorId && event.currentFloor) { - core.autoEventExecuting(event.symbol, false); - core.autoEventExecuted(event.symbol, false); - } - }); - core.status.maps[floorId].deleted = true; - core.status.maps[floorId].canFlyTo = false; - core.status.maps[floorId].canFlyFrom = false; - core.status.maps[floorId].cannotViewMap = true; - } - } - core.plugin.removeMaps = removeMaps; - - /** - * 恢复楼层 - * @example // 恢复MT1~MT300之间的全部层 - * core.resumeMaps("MT1", "MT300") - * @example // 只恢复MT10层 - * core.resumeMaps("MT10") - * @param {string} fromId - * @param {string} toId - */ - function resumeMaps(fromId, toId) { - toId = toId || fromId; - var fromIndex = core.floorIds.indexOf(fromId), - toIndex = core.floorIds.indexOf(toId); - if (toIndex < 0) toIndex = core.floorIds.length - 1; - flags.__removed__ = flags.__removed__ || []; - for (var i = fromIndex; i <= toIndex; ++i) { - var floorId = core.floorIds[i]; - if (!core.status.maps[floorId].deleted) continue; - flags.__removed__ = flags.__removed__.filter(function (f) { return f != floorId; }); - core.status.maps[floorId] = core.loadFloor(floorId); - } - } - core.plugin.resumeMaps = resumeMaps; - - /** 分区砍层相关 - * @param {string} floorId - */ - const inAnyPartition = function (floorId) { - let inPartition = false; - (core.floorPartitions || []).forEach(function (floor) { - const fromIndex = core.floorIds.indexOf(floor[0]); - let toIndex = core.floorIds.indexOf(floor[1]); - const index = core.floorIds.indexOf(floorId); - if (fromIndex < 0 || index < 0) return; - if (toIndex < 0) toIndex = core.floorIds.length - 1; - if (index >= fromIndex && index <= toIndex) inPartition = true; - }); - return inPartition; - } - - /** - * 分区砍层 - * @param {string} floorId - **/ - function autoRemoveMaps(floorId) { - if (main.mode != 'play' || !inAnyPartition(floorId)) return; - // 根据分区信息自动砍层与恢复 - (core.floorPartitions || []).forEach(function (floor) { - var fromIndex = core.floorIds.indexOf(floor[0]); - var toIndex = core.floorIds.indexOf(floor[1]); - var index = core.floorIds.indexOf(floorId); - if (fromIndex < 0 || index < 0) return; - if (toIndex < 0) toIndex = core.floorIds.length - 1; - if (index >= fromIndex && index <= toIndex) { - core.resumeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]); - } else { - core.removeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]); - } - }); - } - core.plugin.autoRemoveMaps = autoRemoveMaps; - }, - "fiveLayers": function () { - // 是否启用五图层(增加背景2层和前景2层) 将__enable置为true即会启用;启用后请保存后刷新编辑器 - // 背景层2将会覆盖背景层 被事件层覆盖 前景层2将会覆盖前景层 - // 另外 请注意加入两个新图层 会让大地图的性能降低一些 - // 插件作者:ad - var __enable = false; - if (!__enable) return; - - // 创建新图层 - function createCanvas(name, zIndex) { - if (!name) return; - const canvas = document.createElement('canvas'); - canvas.id = name; - canvas.className = 'gameCanvas'; - // 编辑器模式下设置zIndex会导致加入的图层覆盖优先级过高 - if (main.mode != "editor") canvas.style.zIndex = zIndex || 0; - // 将图层插入进游戏内容 - document.getElementById('gameDraw')?.appendChild(canvas); - const ctx = canvas.getContext('2d'); - if (ctx) core.canvas[name] = ctx; - canvas.width = core.__PIXELS__; - canvas.height = core.__PIXELS__; - return canvas; - } - - const bg2Canvas = createCanvas('bg2', 20); - const fg2Canvas = createCanvas('fg2', 63); - // 大地图适配 - core.bigmap.canvas = ["bg2", "fg2", "bg", "event", "event2", "fg", "damage"]; - core.initStatus.bg2maps = {}; - core.initStatus.fg2maps = {}; - - if (main.mode == 'editor') { - /*插入编辑器的图层 不做此步新增图层无法在编辑器显示*/ - // 编辑器图层覆盖优先级 eui > efg > fg(前景层) > event2(48*32图块的事件层) > event(事件层) > bg(背景层) - // 背景层2(bg2) 插入事件层(event)之前(即bg与event之间) - if (bg2Canvas) document.getElementById('mapEdit')?.insertBefore(bg2Canvas, document.getElementById('event')); - // 前景层2(fg2) 插入编辑器前景(efg)之前(即fg之后) - if (fg2Canvas) document.getElementById('mapEdit')?.insertBefore(fg2Canvas, document.getElementById('ebm')); - // 原本有三个图层 从4开始添加 - var num = 4; - // 新增图层存入editor.dom中 - editor.dom.bg2c = core.canvas.bg2.canvas; - editor.dom.bg2Ctx = core.canvas.bg2; - editor.dom.fg2c = core.canvas.fg2.canvas; - editor.dom.fg2Ctx = core.canvas.fg2; - editor.dom.maps.push('bg2map', 'fg2map'); - editor.dom.canvas.push('bg2', 'fg2'); - - // 创建编辑器上的按钮 - const createCanvasBtn = function (name) { - // 电脑端创建按钮 - const input = document.createElement('input'); - // layerMod4/layerMod5 - const id = 'layerMod' + num++; - // bg2map/fg2map - const value = name + 'map'; - input.type = 'radio'; - input.name = 'layerMod'; - input.id = id; - input.value = value; - editor.dom[id] = input; - input.onchange = function () { - editor.uifunctions.setLayerMod(value); - } - return input; - }; - - const createCanvasBtn_mobile = function (name) { - // 手机端往选择列表中添加子选项 - const input = document.createElement('option'); - const id = 'layerMod' + num++; - const value = name + 'map'; - input.name = 'layerMod'; - input.value = value; - editor.dom[id] = input; - return input; - }; - if (!editor.isMobile) { - const input = createCanvasBtn('bg2'); - const input2 = createCanvasBtn('fg2'); - // 获取事件层及其父节点 - const child = document.getElementById('layerMod'), - parent = child?.parentNode; - if (parent) { - // 背景层2插入事件层前 - parent.insertBefore(input, child); - // 不能直接更改背景层2的innerText 所以创建文本节点 - var txt = document.createTextNode('bg2'); - // 插入事件层前(即新插入的背景层2前) - parent.insertBefore(txt, child); - // 向最后插入前景层2(即插入前景层后) - parent.appendChild(input2); - var txt2 = document.createTextNode('fg2'); - parent.appendChild(txt2); - parent.childNodes[2].replaceWith("bg"); - parent.childNodes[6].replaceWith("事件"); - parent.childNodes[8].replaceWith("fg"); - } - } else { - const input = createCanvasBtn_mobile('bg2'); - const input2 = createCanvasBtn_mobile('fg2'); - // 手机端因为是选项 所以可以直接改innerText - input.innerText = '背景层2'; - input2.innerText = '前景层2'; - const parent = document.getElementById('layerMod'); - if (parent) { - parent.insertBefore(input, parent.children[1]); - parent.appendChild(input2); - } - } - } - - var _loadFloor_doNotCopy = core.maps._loadFloor_doNotCopy; - core.maps._loadFloor_doNotCopy = function () { - return ["bg2map", "fg2map"].concat(_loadFloor_doNotCopy()); - } - ////// 绘制背景和前景层 ////// - core.maps._drawBg_draw = function (floorId, toDrawCtx, cacheCtx, config) { - config.ctx = cacheCtx; - core.maps._drawBg_drawBackground(floorId, config); - // ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制背景图块;后绘制的覆盖先绘制的。 - core.maps._drawFloorImages(floorId, config.ctx, 'bg', null, null, config.onMap); - core.maps._drawBgFgMap(floorId, 'bg', config); - if (config.onMap) { - core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); - core.clearMap('bg2'); - core.clearMap(cacheCtx); - } - core.maps._drawBgFgMap(floorId, 'bg2', config); - if (config.onMap) core.drawImage('bg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); - config.ctx = toDrawCtx; - } - core.maps._drawFg_draw = function (floorId, toDrawCtx, cacheCtx, config) { - config.ctx = cacheCtx; - // ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制前景图块;后绘制的覆盖先绘制的。 - core.maps._drawFloorImages(floorId, config.ctx, 'fg', null, null, config.onMap); - core.maps._drawBgFgMap(floorId, 'fg', config); - if (config.onMap) { - core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); - core.clearMap('fg2'); - core.clearMap(cacheCtx); - } - core.maps._drawBgFgMap(floorId, 'fg2', config); - if (config.onMap) core.drawImage('fg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); - config.ctx = toDrawCtx; - } - ////// 移动判定 ////// - core.maps._generateMovableArray_arrays = function (floorId) { - return { - bgArray: this.getBgMapArray(floorId), - fgArray: this.getFgMapArray(floorId), - eventArray: this.getMapArray(floorId), - bg2Array: this._getBgFgMapArray('bg2', floorId), - fg2Array: this._getBgFgMapArray('fg2', floorId) - }; - } - - // @todo 测试五图层插件在此处是否表现正常 - // @todo 五图层配合autotile是否有bug 待测试 - // 楼层贴图绘制 - core.maps._drawFloorImage = function (ctx, name, one, image, currStatus, onMap) { - var height = image.height; - var imageName = one.name + (one.reverse || ''); - var width = parseInt((one.w == null ? image.width : one.w) / (one.frame || 1)); - var height = one.h == null ? image.height : one.h; - var sx = (one.sx || 0) + (currStatus || 0) % (one.frame || 1) * width; - var sy = one.sy || 0; - var x = one.x || 0, y = one.y || 0; - if (onMap && core.bigmap.v2) { - if (x > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || x + width < 32 * core.bigmap.posX - 32 || - y > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || y + height < 32 * core.bigmap.posY - 32) { - return; - } - x -= 32 * core.bigmap.posX; - y -= 32 * core.bigmap.posY; - } - - if (one.canvas != 'auto' && one.canvas != name) return; - if (one.canvas != 'auto') { - if (currStatus != null) core.clearMap(ctx, x, y, width, height); - core.drawImage(ctx, imageName, sx, sy, width, height, x, y, width, height); - } else { - if (name === 'bg' || name === 'bg2') { - if (currStatus != null) core.clearMap(ctx, x, y + height - 32, width, 32); - core.drawImage(ctx, imageName, sx, sy + height - 32, width, 32, x, y + height - 32, width, 32); - } else if (name == 'fg' || name === 'fg2') { - if (currStatus != null) core.clearMap(ctx, x, y, width, height - 32); - core.drawImage(ctx, imageName, sx, sy, width, height - 32, x, y, width, height - 32); - } - } - } - }, - "itemShop": function () { - // 道具商店相关的插件 - // 可在全塔属性-全局商店中使用「道具商店」事件块进行编辑(如果找不到可以在入口方块中找) - - var shopId = null; // 当前商店ID - var type = 0; // 当前正在选中的类型,0买入1卖出 - /** 当前正在选中的道具 - * @type {number | null} - */ - var selectItem = 0; - var selectCount = 0; // 当前已经选中的数量 - var page = 0; - var totalPage = 0; - var totalMoney = 0; - var list = []; - var shopInfo = null; // 商店信息 - var choices = []; // 商店选项 - var use = 'money'; - var useText = '金币'; - - var bigFont = core.ui._buildFont(20, false), - middleFont = core.ui._buildFont(18, false); - - this._drawItemShop = function () { - // 绘制道具商店 - - // Step 1: 背景和固定的几个文字 - core.ui._createUIEvent(); - core.clearMap('uievent'); - core.ui.clearUIEventSelector(); - core.setTextAlign('uievent', 'left'); - core.setTextBaseline('uievent', 'top'); - core.fillRect('uievent', 0, 0, 416, 416, 'black'); - core.drawWindowSkin('winskin.png', 'uievent', 0, 0, 416, 56); - core.drawWindowSkin('winskin.png', 'uievent', 0, 56, 312, 56); - core.drawWindowSkin('winskin.png', 'uievent', 0, 112, 312, 304); - core.drawWindowSkin('winskin.png', 'uievent', 312, 56, 104, 56); - core.drawWindowSkin('winskin.png', 'uievent', 312, 112, 104, 304); - core.setFillStyle('uievent', 'white'); - core.setStrokeStyle('uievent', 'white'); - core.fillText("uievent", "购买", 32, 74, 'white', bigFont); - core.fillText("uievent", "卖出", 132, 74); - core.fillText("uievent", "离开", 232, 74); - core.fillText("uievent", "当前" + useText, 324, 66, null, middleFont); - core.setTextAlign("uievent", "right"); - core.fillText("uievent", core.formatBigNumber(core.status.hero[use]), 405, 89); - core.setTextAlign("uievent", "left"); - core.ui.drawUIEventSelector(1, "winskin.png", 22 + 100 * type, 66, 60, 33); - if (selectItem != null) { - core.setTextAlign('uievent', 'center'); - core.fillText("uievent", type == 0 ? "买入个数" : "卖出个数", 364, 320, null, bigFont); - core.fillText("uievent", "< " + selectCount + " >", 364, 350); - core.fillText("uievent", "确定", 364, 380); - } - - // Step 2:获得列表并展示 - list = choices.filter(function (one) { - if (one.condition != null && one.condition != '') { - try { if (!core.calValue(one.condition)) return false; } catch (e) { } - } - return (type == 0 && one.money != null) || (type == 1 && one.sell != null); - }); - var per_page = 6; - totalPage = Math.ceil(list.length / per_page); - page = Math.floor((selectItem || 0) / per_page) + 1; - - // 绘制分页 - if (totalPage > 1) { - var half = 156; - core.setTextAlign('uievent', 'center'); - core.fillText('uievent', page + " / " + totalPage, half, 388, null, middleFont); - if (page > 1) core.fillText('uievent', '上一页', half - 80, 388); - if (page < totalPage) core.fillText('uievent', '下一页', half + 80, 388); - } - core.setTextAlign('uievent', 'left'); - - // 绘制每一项 - var start = (page - 1) * per_page; - for (var i = 0; i < per_page; ++i) { - var curr = start + i; - if (curr >= list.length) break; - var item = list[curr]; - core.drawIcon('uievent', item.id, 10, 125 + i * 40); - core.setTextAlign('uievent', 'left'); - core.fillText('uievent', core.material.items[item.id].name, 50, 132 + i * 40, null, bigFont); - core.setTextAlign('uievent', 'right'); - core.fillText('uievent', (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)) + useText + "/个", 300, 133 + i * 40, null, middleFont); - core.setTextAlign("uievent", "left"); - if (curr == selectItem) { - // 绘制描述,文字自动放缩 - var text = core.material.items[item.id].text || "该道具暂无描述"; - try { text = core.replaceText(text); } catch (e) { } - for (var fontSize = 20; fontSize >= 8; fontSize -= 2) { - var config = { left: 10, fontSize: fontSize, maxWidth: 403 }; - var height = core.getTextContentHeight(text, config); - if (height <= 50) { - config.top = (56 - height) / 2; - core.drawTextContent("uievent", text, config); - break; - } - } - core.ui.drawUIEventSelector(2, "winskin.png", 8, 120 + i * 40, 295, 40); - if (type == 0 && item.number != null) { - core.fillText("uievent", "存货", 324, 132, null, bigFont); - core.setTextAlign("uievent", "right"); - core.fillText("uievent", item.number, 406, 132, null, null, 40); - } else if (type == 1) { - core.fillText("uievent", "数量", 324, 132, null, bigFont); - core.setTextAlign("uievent", "right"); - core.fillText("uievent", core.itemCount(item.id), 406, 132, null, null, 40); - } - core.setTextAlign("uievent", "left"); - core.fillText("uievent", "预计" + useText, 324, 250); - core.setTextAlign("uievent", "right"); - totalMoney = selectCount * (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)); - core.fillText("uievent", core.formatBigNumber(totalMoney), 405, 280); - - core.setTextAlign("uievent", "left"); - core.fillText("uievent", type == 0 ? "已购次数" : "已卖次数", 324, 170); - core.setTextAlign("uievent", "right"); - core.fillText("uievent", (type == 0 ? item.money_count : item.sell_count) || 0, 405, 200); - } - } - - core.setTextAlign('uievent', 'left'); - core.setTextBaseline('uievent', 'alphabetic'); - } - - var _add = function (item, delta) { - if (item == null) return; - selectCount = core.clamp( - selectCount + delta, 0, - Math.min(type == 0 ? Math.floor(core.status.hero[use] / core.calValue(item.money)) : core.itemCount(item.id), - type == 0 && item.number != null ? item.number : Number.MAX_SAFE_INTEGER) - ); - } - - var _confirm = function (item) { - if (item == null || selectCount == 0) return; - if (type == 0) { - core.status.hero[use] -= totalMoney; - core.getItem(item.id, selectCount); - core.stopSound(); - core.playSound('确定'); - if (item.number != null) item.number -= selectCount; - item.money_count = (item.money_count || 0) + selectCount; - } else { - core.status.hero[use] += totalMoney; - core.removeItem(item.id, selectCount); - core.playSound('确定'); - core.drawTip("成功卖出" + selectCount + "个" + core.material.items[item.id].name, item.id); - if (item.number != null) item.number += selectCount; - item.sell_count = (item.sell_count || 0) + selectCount; - } - selectCount = 0; - } - - this._performItemShopKeyBoard = function (keycode) { - var item = list[selectItem] || null; - // 键盘操作 - switch (keycode) { - case 38: // up - if (selectItem == null) break; - if (selectItem == 0) selectItem = null; - else selectItem--; - selectCount = 0; - break; - case 37: // left - if (selectItem == null) { - if (type > 0) type--; - break; - } - _add(item, -1); - break; - case 39: // right - if (selectItem == null) { - if (type < 2) type++; - break; - } - _add(item, 1); - break; - case 40: // down - if (selectItem == null) { - if (list.length > 0) selectItem = 0; - break; - } - if (list.length == 0) break; - selectItem = Math.min(selectItem + 1, list.length - 1); - selectCount = 0; - break; - case 13: - case 32: // Enter/Space - if (selectItem == null) { - if (type == 2) - core.insertAction({ "type": "break" }); - else if (list.length > 0) - selectItem = 0; - break; - } - _confirm(item); - break; - case 27: // ESC - if (selectItem == null) { - core.insertAction({ "type": "break" }); - break; - } - selectItem = null; - break; - } - } - - this._performItemShopClick = function (px, py) { - var item = list[selectItem] || null; - // 鼠标操作 - if (px >= 22 && px <= 82 && py >= 71 && py <= 102) { - // 买 - if (type != 0) { - type = 0; - selectItem = null; - selectCount = 0; - } - return; - } - if (px >= 122 && px <= 182 && py >= 71 && py <= 102) { - // 卖 - if (type != 1) { - type = 1; - selectItem = null; - selectCount = 0; - } - return; - } - if (px >= 222 && px <= 282 && py >= 71 && py <= 102) // 离开 - return core.insertAction({ "type": "break" }); - // < > - if (px >= 318 && px <= 341 && py >= 348 && py <= 376) - return _add(item, -1); - if (px >= 388 && px <= 416 && py >= 348 && py <= 376) - return _add(item, 1); - // 确定 - if (px >= 341 && px <= 387 && py >= 380 && py <= 407) - return _confirm(item); - - // 上一页/下一页 - if (px >= 45 && px <= 105 && py >= 388) { - if (page > 1) { - selectItem -= 6; - selectCount = 0; - } - return; - } - if (px >= 208 && px <= 268 && py >= 388) { - if (page < totalPage) { - selectItem = Math.min(selectItem + 6, list.length - 1); - selectCount = 0; - } - return; - } - - // 实际区域 - if (px >= 9 && px <= 300 && py >= 120 && py < 360) { - if (list.length == 0) return; - var index = parseInt((py - 120) / 40); - var newItem = 6 * (page - 1) + index; - if (newItem >= list.length) newItem = list.length - 1; - if (newItem != selectItem) { - selectItem = newItem; - selectCount = 0; - } - return; - } - } - - this._performItemShopAction = function () { - if (flags.type == 0) return this._performItemShopKeyBoard(flags.keycode); - else return this._performItemShopClick(flags.px, flags.py); - } - - this.openItemShop = function (itemShopId) { - shopId = itemShopId; - type = 0; - page = 0; - selectItem = null; - selectCount = 0; - core.isShopVisited(itemShopId); - shopInfo = flags.__shops__[shopId]; - if (shopInfo.choices == null) shopInfo.choices = core.clone(core.status.shops[shopId].choices); - choices = shopInfo.choices; - use = core.status.shops[shopId].use; - if (use != 'exp') use = 'money'; - useText = use == 'money' ? '金币' : '经验'; - - core.insertAction([{ - "type": "while", - "condition": "true", - "data": [ - { "type": "function", "function": "function () { core.plugin._drawItemShop(); }" }, - { "type": "wait" }, - { "type": "function", "function": "function() { core.plugin._performItemShopAction(); }" } - ] - }, - { - "type": "function", - "function": "function () { core.deleteCanvas('uievent'); core.ui.clearUIEventSelector(); }" - } - ]); - } - - }, - "enemyLevel": function () { - // 此插件将提供怪物手册中的怪物境界显示 - // 使用此插件需要先给每个怪物定义境界,方法如下: - // 点击怪物的【配置表格】,找到“【怪物】相关的表格配置”,然后在【名称】仿照增加境界定义: - /* - "level": { - "_leaf": true, - "_type": "textarea", - "_string": true, - "_data": "境界" - }, - */ - // 然后保存刷新,可以看到怪物的属性定义中出现了【境界】。再开启本插件即可。 - - // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 - var __enable = false; - if (!__enable) return; - - // 这里定义每个境界的显示颜色;可以写'red', '#RRGGBB' 或者[r,g,b,a]四元数组 - var levelToColors = { - "萌新一阶": "red", - "萌新二阶": "#FF0000", - "萌新三阶": [255, 0, 0, 1], - }; - - // 复写 _drawBook_drawName - var originDrawBook = core.ui._drawBook_drawName; - core.ui._drawBook_drawName = function (index, enemy, top, left, width) { - // 如果没有境界,则直接调用原始代码绘制 - if (!enemy.level) return originDrawBook.call(core.ui, index, enemy, top, left, width); - // 存在境界,则额外进行绘制 - core.setTextAlign('ui', 'center'); - if (enemy.specialText.length == 0) { - core.fillText('ui', enemy.name, left + width / 2, - top + 27, '#DDDDDD', this._buildFont(17, true)); - core.fillText('ui', enemy.level, left + width / 2, - top + 51, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true)); - } else { - core.fillText('ui', enemy.name, left + width / 2, - top + 20, '#DDDDDD', this._buildFont(17, true), width); - switch (enemy.specialText.length) { - case 1: - core.fillText('ui', enemy.specialText[0], left + width / 2, - top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'), - this._buildFont(14, true), width); - break; - case 2: - // Step 1: 计算字体 - var text = enemy.specialText[0] + " " + enemy.specialText[1]; - core.setFontForMaxWidth('ui', text, width, this._buildFont(14, true)); - // Step 2: 计算总宽度 - var totalWidth = core.calWidth('ui', text); - var leftWidth = core.calWidth('ui', enemy.specialText[0]); - var rightWidth = core.calWidth('ui', enemy.specialText[1]); - // Step 3: 绘制 - core.fillText('ui', enemy.specialText[0], left + (width + leftWidth - totalWidth) / 2, - top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A')); - core.fillText('ui', enemy.specialText[1], left + (width + totalWidth - rightWidth) / 2, - top + 38, core.arrayToRGBA((enemy.specialColor || [])[1] || '#FF6A6A')); - break; - default: - core.fillText('ui', '多属性...', left + width / 2, - top + 38, '#FF6A6A', this._buildFont(14, true), width); - } - core.fillText('ui', enemy.level, left + width / 2, - top + 56, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true)); - } - } - - // 也可以复写其他的属性颜色如怪物攻防等,具体参见下面的例子的注释部分 - core.ui._drawBook_drawRow1 = function (index, enemy, top, left, width, position) { - // 绘制第一行 - core.setTextAlign('ui', 'left'); - var b13 = this._buildFont(13, true), - f13 = this._buildFont(13, false); - var col1 = left, - col2 = left + width * 9 / 25, - col3 = left + width * 17 / 25; - core.fillText('ui', '生命', col1, position, '#DDDDDD', f13); - core.fillText('ui', core.formatBigNumber(enemy.hp || 0), col1 + 30, position, /*'red' */ null, b13); - core.fillText('ui', '攻击', col2, position, null, f13); - core.fillText('ui', core.formatBigNumber(enemy.atk || 0), col2 + 30, position, /* '#FF0000' */ null, b13); - core.fillText('ui', '防御', col3, position, null, f13); - core.fillText('ui', core.formatBigNumber(enemy.def || 0), col3 + 30, position, /* [255, 0, 0, 1] */ null, b13); - } - - - }, - "multiHeros": function () { - // 多角色插件 - // Step 1: 启用本插件 - // Step 2: 定义每个新的角色各项初始数据(参见下方注释) - // Step 3: 在游戏中的任何地方都可以调用 `core.changeHero()` 进行切换;也可以 `core.changeHero(1)` 来切换到某个具体的角色上 - - // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 - var __enable = false; - if (!__enable) return; - - // 在这里定义全部的新角色属性 - // 请注意,在这里定义的内容不会多角色共用,在切换时会进行恢复。 - // 你也可以自行新增或删除,比如不共用金币则可以加上"money"的初始化,不共用道具则可以加上"items"的初始化, - // 多角色共用hp的话则删除hp,等等。总之,不共用的属性都在这里进行定义就好。 - var hero1 = { - "floorId": "MT0", // 该角色初始楼层ID;如果共用楼层可以注释此项 - "image": "brave.png", // 角色的行走图名称;此项必填不然会报错 - "name": "1号角色", - "lv": 1, - "hp": 10000, // 如果HP共用可注释此项 - "atk": 1000, - "def": 1000, - "mdef": 0, - // "money": 0, // 如果要不共用金币则取消此项注释 - // "exp": 0, // 如果要不共用经验则取消此项注释 - "loc": { "x": 0, "y": 0, "direction": "up" }, // 该角色初始位置;如果共用位置可注释此项 - "items": { - "tools": {}, // 如果共用消耗道具(含钥匙)则可注释此项 - // "constants": {}, // 如果不共用永久道具(如手册)可取消注释此项 - "equips": {}, // 如果共用在背包的装备可注释此项 - }, - "equipment": [], // 如果共用装备可注释此项;此项和上面的「共用在背包的装备」需要拥有相同状态,不然可能出现问题 - }; - // 也可以类似新增其他角色 - // 新增的角色,各项属性共用与不共用的选择必须和上面完全相同,否则可能出现问题。 - // var hero2 = { ... - - var heroCount = 2; // 包含默认角色在内总共多少个角色,该值需手动修改。 - - this.initHeros = function () { - core.setFlag("hero1", core.clone(hero1)); // 将属性值存到变量中 - // core.setFlag("hero2", core.clone(hero2)); // 更多的角色也存入变量中;每个定义的角色都需要新增一行 - - // 检测是否存在装备 - if (hero1.equipment) { - if (!hero1.items || !hero1.items.equips) { - alert('多角色插件的equipment和道具中的equips必须拥有相同状态!'); - } - // 存99号套装为全空 - var saveEquips = core.getFlag("saveEquips", []); - saveEquips[99] = []; - core.setFlag("saveEquips", saveEquips); - } else { - if (hero1.items && hero1.items.equips) { - alert('多角色插件的equipment和道具中的equips必须拥有相同状态!'); - } - } - } - - // 在游戏开始注入initHeros - var _startGame_setHard = core.events._startGame_setHard; - core.events._startGame_setHard = function () { - _startGame_setHard.call(core.events); - core.initHeros(); - } - - // 切换角色 - // 可以使用 core.changeHero() 来切换到下一个角色 - // 也可以 core.changeHero(1) 来切换到某个角色(默认角色为0) - this.changeHero = function (toHeroId) { - var currHeroId = core.getFlag("heroId", 0); // 获得当前角色ID - if (toHeroId == null) { - toHeroId = (currHeroId + 1) % heroCount; - } - if (currHeroId == toHeroId) return; - - var saveList = Object.keys(hero1); - - // 保存当前内容 - var toSave = {}; - // 暂时干掉 drawTip 和 音效,避免切装时的提示 - var _drawTip = core.ui.drawTip; - core.ui.drawTip = function () { }; - var _playSound = core.control.playSound; - core.control.playSound = function () { return undefined; }; - // 记录当前录像,因为可能存在换装问题 - core.clearRouteFolding(); - var routeLength = core.status.route.length; - // 优先判定装备 - if (hero1.equipment) { - core.items.quickSaveEquip(100 + currHeroId); - core.items.quickLoadEquip(99); - } - - saveList.forEach(function (name) { - if (name == 'floorId') toSave[name] = core.status.floorId; // 楼层单独设置 - else if (name == 'items') { - toSave.items = core.clone(core.status.hero.items); - Object.keys(toSave.items).forEach(function (one) { - if (!hero1.items[one]) delete toSave.items[one]; - }); - } else toSave[name] = core.clone(core.status.hero[name]); // 使用core.clone()来创建新对象 - }); - - core.setFlag("hero" + currHeroId, toSave); // 将当前角色信息进行保存 - var data = core.getFlag("hero" + toHeroId); // 获得要切换的角色保存内容 - - // 设置角色的属性值 - saveList.forEach(function (name) { - if (name == "items") { - Object.keys(core.status.hero.items).forEach(function (one) { - if (data.items[one]) core.status.hero.items[one] = core.clone(data.items[one]); - }); - } else { - core.status.hero[name] = core.clone(data[name]); - } - }); - // 最后装上装备 - if (hero1.equipment) { - core.items.quickLoadEquip(100 + toHeroId); - } - - core.ui.drawTip = _drawTip; - core.control.playSound = _playSound; - core.status.route = core.status.route.slice(0, routeLength); - core.control._bindRoutePush(); - - // 插入事件:改变角色行走图并进行楼层切换 - var toFloorId = data.floorId || core.status.floorId; - var toLoc = data.loc || core.status.hero.loc; - core.insertAction([ - { "type": "setHeroIcon", "name": data.image || "hero.png" }, // 改变行走图 - // 同层则用changePos,不同层则用changeFloor;这是为了避免共用楼层造成触发eachArrive - toFloorId != core.status.floorId ? { - "type": "changeFloor", - "floorId": toFloorId, - "loc": [toLoc.x, toLoc.y], - "direction": toLoc.direction, - "time": 0 // 可以在这里设置切换时间 - } : { "type": "changePos", "loc": [toLoc.x, toLoc.y], "direction": toLoc.direction } - // 你还可以在这里执行其他事件,比如增加或取消跟随效果 - ]); - core.setFlag("heroId", toHeroId); // 保存切换到的角色ID - } - }, - "heroFourFrames": function () { - // 样板的勇士/跟随者移动时只使用2、4两帧,观感较差。本插件可以将四帧全用上。 - - // 是否启用本插件 - var __enable = false; - if (!__enable) return; - - ["up", "down", "left", "right"].forEach(function (one) { - // 指定中间帧动画 - core.material.icons.hero[one].midFoot = 2; - }); - - var heroMoving = function (timestamp) { - if (core.status.heroMoving <= 0) return; - if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) { - core.animateFrame.leftLeg++; - core.animateFrame.moveTime = timestamp; - } - core.drawHero(['stop', 'leftFoot', 'midFoot', 'rightFoot'][core.animateFrame.leftLeg % 4], 4 * core.status.heroMoving); - } - core.registerAnimationFrame('heroMoving', true, heroMoving); - - core.events._eventMoveHero_moving = function (step, moveSteps) { - var curr = moveSteps[0]; - var direction = curr[0], x = core.getHeroLoc('x'), y = core.getHeroLoc('y'); - // ------ 前进/后退 - var o = direction == 'backward' ? -1 : 1; - if (direction == 'forward' || direction == 'backward') direction = core.getHeroLoc('direction'); - var faceDirection = direction; - if (direction == 'leftup' || direction == 'leftdown') faceDirection = 'left'; - if (direction == 'rightup' || direction == 'rightdown') faceDirection = 'right'; - core.setHeroLoc('direction', direction); - if (curr[1] <= 0) { - core.setHeroLoc('direction', faceDirection); - moveSteps.shift(); - return true; - } - if (step <= 4) core.drawHero('stop', 4 * o * step); - else if (step <= 8) core.drawHero('leftFoot', 4 * o * step); - else if (step <= 12) core.drawHero('midFoot', 4 * o * (step - 8)); - else if (step <= 16) core.drawHero('rightFoot', 4 * o * (step - 8)); // if (step == 8) { - if (step == 8 || step == 16) { - core.setHeroLoc('x', x + o * core.utils.scan2[direction].x, true); - core.setHeroLoc('y', y + o * core.utils.scan2[direction].y, true); - core.updateFollowers(); - curr[1]--; - if (curr[1] <= 0) moveSteps.shift(); - core.setHeroLoc('direction', faceDirection); - return step == 16; - } - return false; - } - }, - "startCanvas": function () { - // 使用本插件可以将自绘的标题界面居中。仅在【标题开启事件化】后才有效。 - // 由于一些技术性的原因,标题界面事件化无法应用到覆盖状态栏的整个界面。 - // 这是一个较为妥协的插件,会在自绘标题界面时隐藏状态栏、工具栏和边框,并将画布进行居中。 - // 本插件仅在全塔属性的 "startCanvas" 生效;进入 "startText" 时将会离开居中状态,回归正常界面。 - - // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 - var __enable = false; - if (!__enable) return; - - // 检查【标题开启事件化】是否开启 - if (!core.flags.startUsingCanvas || main.mode != 'play') return; - - var _isTitleCanvasEnabled = false; - var _getClickLoc = core.actions._getClickLoc; - this._setTitleCanvas = function () { - if (_isTitleCanvasEnabled) return; - _isTitleCanvasEnabled = true; - - // 禁用窗口resize - window.onresize = function () { }; - core.resize = function () { } - - // 隐藏状态栏 - core.dom.statusBar.style.display = 'none'; - core.dom.statusCanvas.style.display = 'none'; - core.dom.toolBar.style.display = 'none'; - // 居中画布 - if (core.domStyle.isVertical) { - core.dom.gameDraw.style.top = - (parseInt(core.dom.gameGroup.style.height) - parseInt(core.dom.gameDraw.style.height)) / 2 + "px"; - } else { - core.dom.gameDraw.style.right = - (parseInt(core.dom.gameGroup.style.width) - parseInt(core.dom.gameDraw.style.width)) / 2 + "px"; - } - core.dom.gameDraw.style.border = '3px transparent solid'; - core.actions._getClickLoc = function (x, y) { - var left = core.dom.gameGroup.offsetLeft + core.dom.gameDraw.offsetLeft + 3; - var top = core.dom.gameGroup.offsetTop + core.dom.gameDraw.offsetTop + 3; - var loc = { 'x': Math.max(x - left, 0), 'y': Math.max(y - top, 0), 'size': 32 * core.domStyle.scale }; - return loc; - } - } - - this._resetTitleCanvas = function () { - if (!_isTitleCanvasEnabled) return; - _isTitleCanvasEnabled = false; - window.onresize = function () { try { main.core.resize(); } catch (ee) { console.error(ee) } } - core.resize = function () { return core.control.resize(); } - core.resize(); - core.actions._getClickLoc = _getClickLoc; - } - - // 复写“开始游戏” - core.events._startGame_start = function (hard, seed, route, callback) { - console.log('开始游戏'); - core.resetGame(core.firstData.hero, hard, null, core.cloneArray(core.initStatus.maps)); - core.setHeroLoc('x', -1); - core.setHeroLoc('y', -1); - - if (seed != null) { - core.setFlag('__seed__', seed); - core.setFlag('__rand__', seed); - } else core.utils.__init_seed(); - - core.clearStatusBar(); - core.plugin._setTitleCanvas(); - - var todo = []; - core.hideStatusBar(); - core.push(todo, core.firstData.startCanvas); - core.push(todo, { "type": "function", "function": "function() { core.plugin._resetTitleCanvas(); core.events._startGame_setHard(); }" }) - core.push(todo, core.firstData.startText); - this.insertAction(todo, null, null, function () { - core.events._startGame_afterStart(callback); - }); - - if (route != null) core.startReplay(route); - } - - var _loadData = core.control.loadData; - core.control.loadData = function (data, callback) { - core.plugin._resetTitleCanvas(); - _loadData.call(core.control, data, callback); - } - }, - "advancedAnimation": function () { - // -------------------- 插件说明 -------------------- // - // github仓库:https://github.com/unanmed/animate - // npm包名:mutate-animate - // npm地址:https://www.npmjs.com/package/mutate-animate - - // 是否开启本插件,默认启用;将此改成 false 将禁用本插件。 - var __enable = true; - - if (main.replayChecking) __enable = false; - if (!__enable) { - core.plugin.animate = {}; - this.tickerSet = new Set(); - this.deleteAllAnis = () => { }; - return; - } - - /** 该集合中的所有Ticker在跨层时需要被摧毁 */ - this.tickerSet = new Set(); - - /** 对Map中所有Ticker执行摧毁事件 */ - this.deleteAllAnis = function () { - core.plugin.tickerSet.forEach((ticker) => ticker.destroy()); - } - - let w = []; - const k = (n) => { - for (const i of w) - if (i.status === "running") - try { - for (const t of i.funcs) - t(n - i.startTime); - } catch (t) { - i.destroy(), console.error(t); - } - requestAnimationFrame(k); - }; - requestAnimationFrame(k); - - /** Ticker类 */ - class I { - constructor() { - this.funcs = /* @__PURE__ */ new Set(); - this.status = "stop"; - this.startTime = 0; - this.status = "running", w.push(this), requestAnimationFrame((i) => this.startTime = i); - } - add(i) { - return this.funcs.add(i), this; - } - remove(i) { - return this.funcs.delete(i), this; - } - clear() { - this.funcs.clear(); - } - destroy() { - core.plugin.tickerSet.delete(this); - this.clear(), this.stop(); - } - stop() { - this.status = "stop", w = w.filter((i) => i !== this); - } - } - /** AnimationBase类 */ - class F { - constructor() { - this.timing = Date.now; - this.relation = "absolute"; - this.easeTime = 0; - this.applying = {}; - this.getTime = Date.now; - const ticker = new I(); - this.ticker = ticker; - this.value = {}; - this.listener = {}; - this.timing = (i) => i; - } - async all() { - if (Object.values(this.applying).every((i) => i === !0)) - throw new ReferenceError("There is no animates to be waited."); - await new Promise((i) => { - const t = () => { - Object.values(this.applying).every((e) => e === !1) && (this.unlisten("end", t), i("all animated.")); - }; - this.listen("end", t); - }); - } - async n(i) { - const t = Object.values(this.applying).filter((s) => s === !0).length; - if (t < i) - throw new ReferenceError( - `You are trying to wait ${i} animate, but there are only ${t} animate animating.` - ); - let e = 0; - await new Promise((s) => { - const r = () => { - e++, e === i && (this.unlisten("end", r), s(`${i} animated.`)); - }; - this.listen("end", r); - }); - } - async w(i) { - if (this.applying[i] === !1) - throw new ReferenceError(`The ${i} animate is not animating.`); - await new Promise((t) => { - const e = () => { - this.applying[i] === !1 && (this.unlisten("end", e), t(`${i} animated.`)); - }; - this.listen("end", e); - }); - } - listen(i, t) { - var e, s; - (s = (e = this.listener)[i]) != null || (e[i] = []), this.listener[i].push(t); - } - unlisten(i, t) { - const e = this.listener[i].findIndex((s) => s === t); - if (e === -1) - throw new ReferenceError( - "You are trying to remove a nonexistent listener." - ); - this.listener[i].splice(e, 1); - } - hook(...i) { - const t = Object.entries(this.listener).filter( - (e) => i.includes(e[0]) - ); - for (const [e, s] of t) - for (const r of s) - r(this, e); - } - } - - function y(n) { - return n != null; - } - async function R(n) { - return new Promise((i) => setTimeout(i, n)); - } - /** Animation类 */ - class j extends F { - constructor() { - super(); - this.shakeTiming; - this.path; - this.multiTiming; - this.value = {}; - this.size = 1; - this.angle = 0; - this.targetValue = { - system: { - move: [0, 0], - moveAs: [0, 0], - resize: 0, - rotate: 0, - shake: 0, - /** @type {number[]} */"@@bind": [] - }, - custom: {} - }; - this.animateFn = { - system: { - move: [() => { }, () => { }], - moveAs: () => { }, - resize: () => { }, - rotate: () => { }, - shake: () => { }, - "@@bind": () => { } - }, - custom: {} - }; - this.ox = 0; - this.oy = 0; - this.sx = 0; - this.sy = 0; - this.bindInfo = []; - this.timing = (t) => t, this.shakeTiming = (t) => t, this.multiTiming = (t) => [t, t], this.path = (t) => [t, t], this.applying = { - move: !1, - scale: !1, - rotate: !1, - shake: !1 - }, this.ticker.add(() => { - const { running: t } = this.listener; - if (y(t)) - for (const e of t) - e(this, "running"); - }); - } - get x() { - return this.ox + this.sx; - } - get y() { - return this.oy + this.sy; - } - mode(t, e = !1) { - return typeof t(0) == "number" ? e ? this.shakeTiming = t : this.timing = t : this.multiTiming = t, this; - } - time(t) { - return this.easeTime = t, this; - } - relative() { - return this.relation = "relative", this; - } - absolute() { - return this.relation = "absolute", this; - } - bind(...t) { - return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = t, this; - } - unbind() { - return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = [], this; - } - move(t, e) { - return this.applying.move && this.end(!0, "move"), this.applySys("ox", t, "move"), this.applySys("oy", e, "move"), this; - } - rotate(t) { - return this.applySys("angle", t, "rotate"), this; - } - scale(t) { - return this.applySys("size", t, "resize"), this; - } - shake(t, e) { - this.applying.shake === !0 && this.end(!0, "shake"), this.applying.shake = !0; - const { easeTime: s, shakeTiming: r } = this, l = this.getTime(); - if (this.hook("start", "shakestart"), s <= 0) - return this.end(!1, "shake"), this; - const a = () => { - const c = this.getTime() - l; - if (c > s) { - this.ticker.remove(a), this.applying.shake = !1, this.sx = 0, this.sy = 0, this.hook("end", "shakeend"); - return; - } - const h = c / s, - m = r(h); - this.sx = m * t, this.sy = m * e; - }; - return this.ticker.add(a), this.animateFn.system.shake = a, this; - } - moveAs(t) { - this.applying.moveAs && this.end(!0, "moveAs"), this.applying.moveAs = !0, this.path = t; - const { easeTime: e, relation: s, timing: r } = this, l = this.getTime(), [a, u] = [this.x, this.y], [c, h] = (() => { - if (s === "absolute") - return t(1); { - const [d, f] = t(1); - return [a + d, u + f]; - } - })(); - if (this.hook("start", "movestart"), e <= 0) - return this.end(!1, "moveAs"), this; - const m = () => { - const f = this.getTime() - l; - if (f > e) { - this.end(!0, "moveAs"); - return; - } - const g = f / e, - [v, x] = t(r(g)); - s === "absolute" ? (this.ox = v, this.oy = x) : (this.ox = a + v, this.oy = u + x); - }; - return this.ticker.add(m), this.animateFn.system.moveAs = m, this.targetValue.system.moveAs = [c, h], this; - } - register(t, e) { - if (typeof this.value[t] == "number") - return this.error( - `Property ${t} has been regietered twice.`, - "reregister" - ); - this.value[t] = e, this.applying[t] = !1; - } - apply(t, e) { - this.applying[t] === !0 && this.end(!1, t), t in this.value || this.error( - `You are trying to execute nonexistent property ${t}.` - ), this.applying[t] = !0; - const s = this.value[t], - r = this.getTime(), - { timing: l, relation: a, easeTime: u } = this, - c = a === "absolute" ? e - s : e; - if (this.hook("start"), u <= 0) - return this.end(!1, t), this; - const h = () => { - const d = this.getTime() - r; - if (d > u) { - this.end(!1, t); - return; - } - const f = d / u, - g = l(f); - this.value[t] = s + g * c; - }; - return this.ticker.add(h), this.animateFn.custom[t] = h, this.targetValue.custom[t] = c + s, this; - } - applyMulti() { - this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.applying["@@bind"] = !0; - const t = this.bindInfo, - e = t.map((h) => this.value[h]), - s = this.getTime(), - { multiTiming: r, relation: l, easeTime: a } = this, - u = r(1); - if (u.length !== e.length) - throw new TypeError( - `The number of binded animate attributes and timing function returns's length does not match. binded: ${t.length}, timing: ${u.length}` - ); - if (this.hook("start"), a <= 0) - return this.end(!1, "@@bind"), this; - const c = () => { - const m = this.getTime() - s; - if (m > a) { - this.end(!1, "@@bind"); - return; - } - const d = m / a, - f = r(d); - t.forEach((g, v) => { - l === "absolute" ? this.value[g] = f[v] : this.value[g] = e[v] + f[v]; - }); - }; - return this.ticker.add(c), this.animateFn.custom["@@bind"] = c, this.targetValue.system["@@bind"] = u, this; - } - applySys(t, e, s) { - s !== "move" && this.applying[s] === !0 && this.end(!0, s), this.applying[s] = !0; - const r = this[t], - l = this.getTime(), - a = this.timing, - u = this.relation, - c = this.easeTime, - h = u === "absolute" ? e - r : e; - if (this.hook("start", `${s}start`), c <= 0) - return this.end(!0, s); - const m = () => { - const f = this.getTime() - l; - if (f > c) { - this.end(!0, s); - return; - } - const g = f / c, - v = a(g); - this[t] = r + h * v, t !== "oy" && this.hook(s); - }; - this.ticker.add(m), t === "ox" ? this.animateFn.system.move[0] = m : t === "oy" ? this.animateFn.system.move[1] = m : this.animateFn.system[s] = m, s === "move" ? (t === "ox" && (this.targetValue.system.move[0] = h + r), t === "oy" && (this.targetValue.system.move[1] = h + r)) : s !== "shake" && (this.targetValue.system[s] = h + r); - } - error(t, e) { - throw e === "repeat" ? new Error( - `Cannot execute the same animation twice. Info: ${t}` - ) : e === "reregister" ? new Error( - `Cannot register an animated property twice. Info: ${t}` - ) : new Error(t); - } - end(t, e) { - if (t === !0) - if (this.applying[e] = !1, e === "move" ? (this.ticker.remove(this.animateFn.system.move[0]), this.ticker.remove(this.animateFn.system.move[1])) : e === "moveAs" ? this.ticker.remove(this.animateFn.system.moveAs) : e === "@@bind" ? this.ticker.remove(this.animateFn.system["@@bind"]) : this.ticker.remove( - this.animateFn.system[e] - ), e === "move") { - const [s, r] = this.targetValue.system.move; - this.ox = s, this.oy = r, this.hook("moveend", "end"); - } else if (e === "moveAs") { - const [s, r] = this.targetValue.system.moveAs; - this.ox = s, this.oy = r, this.hook("moveend", "end"); - } else - e === "rotate" ? (this.angle = this.targetValue.system.rotate, this.hook("rotateend", "end")) : e === "resize" ? (this.size = this.targetValue.system.resize, this.hook("resizeend", "end")) : e === "@@bind" ? this.bindInfo.forEach((r, l) => { - this.value[r] = this.targetValue.system["@@bind"][l]; - }) : (this.sx = 0, this.sy = 0, this.hook("shakeend", "end")); - else - this.applying[e] = !1, this.ticker.remove(this.animateFn.custom[e]), this.value[e] = this.targetValue.custom[e], this.hook("end"); - } - } - class O extends F { - constructor() { - super(); - this.now = {}; - this.target = {}; - this.transitionFn = {}; - this.value = undefined; - this.handleSet = (t, e, s) => (this.transition(e, s), !0); - this.handleGet = (t, e) => this.now[e]; - this.timing = (t) => t, this.value = new Proxy(this.target, { - set: this.handleSet, - get: this.handleGet - }); - } - mode(t) { - return this.timing = t, this; - } - time(t) { - return this.easeTime = t, this; - } - relative() { - return this.relation = "relative", this; - } - absolute() { - return this.relation = "absolute", this; - } - transition(t, e) { - if (e === this.target[t]) - return this; - if (!y(this.now[t])) - return this.now[t] = e, this; - this.applying[t] && this.end(t, !0), this.applying[t] = !0, this.hook("start"); - const s = this.getTime(), - r = this.easeTime, - l = this.timing, - a = this.now[t], - u = e + (this.relation === "absolute" ? 0 : a), - c = u - a; - this.target[t] = u; - const h = () => { - const d = this.getTime() - s; - if (d >= r) { - this.end(t); - return; - } - const f = d / r; - this.now[t] = l(f) * c + a, this.hook("running"); - }; - return this.transitionFn[t] = h, this.ticker.add(h), r <= 0 ? (this.end(t), this) : this; - } - end(t, e = !1) { - const s = this.transitionFn[t]; - if (!y(s)) - throw new ReferenceError( - `You are trying to end an ended transition: ${t}` - ); - this.ticker.remove(this.transitionFn[t]), delete this.transitionFn[t], this.applying[t] = !1, this.hook("end"), e || (this.now[t] = this.target[t]); - } - } - const T = (...n) => n.reduce((i, t) => i + t, 0), - b = (n) => { - if (n === 0) - return 1; - let i = n; - for (; n > 1;) - n--, i *= n; - return i; - }, - A = (n, i) => Math.round(b(i) / (b(n) * b(i - n))), - p = (n, i, t = (e) => 1 - i(1 - e)) => n === "in" ? i : n === "out" ? t : n === "in-out" ? (e) => e < 0.5 ? i(e * 2) / 2 : 0.5 + t((e - 0.5) * 2) / 2 : (e) => e < 0.5 ? t(e * 2) / 2 : 0.5 + i((e - 0.5) * 2) / 2, - $ = Math.cosh(2), - z = Math.acosh(2), - V = Math.tanh(3), - P = Math.atan(5); - - function Y() { - return (n) => n; - } - - function q(...n) { - const i = [0].concat(n); - i.push(1); - const t = i.length, - e = Array(t).fill(0).map((s, r) => A(r, t - 1)); - return (s) => { - const r = e.map((l, a) => l * i[a] * (1 - s) ** (t - a - 1) * s ** a); - return T(...r); - }; - } - - function U(n, i) { - if (n === "sin") { - const t = (s) => Math.sin(s * Math.PI / 2); - return p(i, (s) => 1 - t(1 - s), t); - } - if (n === "sec") { - const t = (s) => 1 / Math.cos(s); - return p(i, (s) => t(s * Math.PI / 3) - 1); - } - throw new TypeError( - "Unexpected parameters are delivered in trigo timing function." - ); - } - - function C(n, i) { - if (!Number.isInteger(n)) - throw new TypeError( - "The first parameter of power timing function only allow integer." - ); - return p(i, (e) => e ** n); - } - - function G(n, i) { - if (n === "sin") - return p(i, (e) => (Math.cosh(e * 2) - 1) / ($ - 1)); - if (n === "tan") { - const t = (s) => Math.tanh(s * 3) * 1 / V; - return p(i, (s) => 1 - t(1 - s), t); - } - if (n === "sec") { - const t = (s) => 1 / Math.cosh(s); - return p(i, (s) => 1 - (t(s * z) - 0.5) * 2); - } - throw new TypeError( - "Unexpected parameters are delivered in hyper timing function." - ); - } - - function N(n, i) { - if (n === "sin") { - const t = (s) => Math.asin(s) / Math.PI * 2; - return p(i, (s) => 1 - t(1 - s), t); - } - if (n === "tan") { - const t = (s) => Math.atan(s * 5) / P; - return p(i, (s) => 1 - t(1 - s), t); - } - throw new TypeError( - "Unexpected parameters are delivered in inverse trigo timing function." - ); - } - /** @param {(input:number) => number} [i=() => 1] */ - function B(n, i = () => 1) { - let t = -1; - return (e) => (t *= -1, e < 0.5 ? n * i(e * 2) * t : n * i((1 - e) * 2) * t); - } - - function D(n, i = 1, t = [0, 0], e = 0, s = (l) => 1, r = !1) { - return (l) => { - const a = i * l * Math.PI * 2 + e * Math.PI / 180, - u = Math.cos(a), - c = Math.sin(a), - h = n * s(s(r ? 1 - l : l)); - return [h * u + t[0], h * c + t[1]]; - }; - } - - function H(n, i, ...t) { - const e = [n].concat(t); - e.push(i); - const s = e.length, - r = Array(s).fill(0).map((l, a) => A(a, s - 1)); - return (l) => { - const a = r.map((c, h) => c * e[h][0] * (1 - l) ** (s - h - 1) * l ** h), - u = r.map((c, h) => c * e[h][1] * (1 - l) ** (s - h - 1) * l ** h); - return [T(...a), T(...u)]; - }; - } - - core.plugin.animate = { - Animation: j, - AnimationBase: F, - Ticker: I, - Transition: O, - bezier: q, - bezierPath: H, - circle: D, - hyper: G, - inverseTrigo: N, - linear: Y, - power: C, - shake: B, - sleep: R, - trigo: U, - } - - }, - "drawItemDetail": function () { - /* 宝石血瓶左下角显示数值 - * 需要将 变量:itemDetail改为true才可正常运行 - * 请尽量减少勇士的属性数量,否则可能会出现严重卡顿(划掉,现在你放一万个属性也不会卡) - * 注意:这里的属性必须是core.status.hero里面的,flag无法显示 - * 如果不想显示,可以core.setFlag("itemDetail", false); - * 然后再core.getItemDetail(); - * 如有bug在大群或造塔群@古祠 - */ - - // 忽略的道具 - const ignore = ['superPotion']; - - // 取消注释下面这句可以减少超大地图的判定。 - // 如果地图宝石过多,可能会略有卡顿,可以尝试取消注释下面这句话来解决。 - // core.bigmap.threshold = 256; - const origin = core.control.updateStatusBar; - core.updateStatusBar = core.control.updateStatusBar = function () { - if (core.hasFlag('__statistics__')) return; - else return origin.apply(core.control, arguments); - } - - core.control.updateDamage = function (floorId, ctx) { - floorId = floorId || core.status.floorId; - if (!floorId || core.status.gameOver || main.mode != 'play') return; - const onMap = ctx == null; - - // 没有怪物手册 - if (!core.hasItem('book')) return; - core.status.damage.posX = core.bigmap.posX; - core.status.damage.posY = core.bigmap.posY; - if (!onMap) { - const width = core.floors[floorId].width, - height = core.floors[floorId].height; - // 地图过大的缩略图不绘制显伤 - if (width * height > core.bigmap.threshold) return; - } - this._updateDamage_damage(floorId, onMap); - this._updateDamage_extraDamage(floorId, onMap); - core.getItemDetail(floorId); // 宝石血瓶详细信息 - this.drawDamage(ctx); - }; - - function getRatio() { - let ratio = (core.status.thisMap?.ratio) ?? 1; - const currEvent = core.status.event; - if (!currEvent) return ratio; - switch (currEvent.id) { - case 'viewMaps': //调整浏览地图时的倍率 - if (currEvent.data) { - const viewMapFloorId = (currEvent.data.floorId); - ratio = core.status.maps[viewMapFloorId].ratio; - } - break; - case 'fly': //调整在楼传界面浏览地图时的倍率 - ratio = core.status.maps[core.floorIds[currEvent.data]].ratio; - break; - } - return ratio; - } - - // 因为core.status.hero是进存档的,必定可序列化 - const clone = (typeof structuredClone === 'function') ? structuredClone : (ele) => JSON.parse(JSON.stringify(ele)); - - // 数值类型的属性(包含装备的百分比提升项) - const statusName = new Set(['atk', 'atkper', 'def', 'defper', 'mdef', 'mdefper', 'hp', - 'hpmax', 'hpmaxper', 'mana', 'manamax', 'money', 'exp']); - // 获取宝石信息 并绘制 - this.getItemDetail = function (floorId) { - if (!core.getFlag('itemDetail')) return; - if (!core.status.thisMap) return; - floorId = floorId ?? core.status.thisMap.floorId; - const beforeRatio = core.status.thisMap.ratio; - core.status.thisMap.ratio = core.status.maps[floorId].ratio; - let diff = {}; - const before = core.status.hero; - const hero = clone(core.status.hero); - const handler = { - set(target, key, v) { - if (!statusName.has(key)) return true; // 非数值类型的属性就不用记了,显示也没有意义 - diff[key] = v - (target[key] || 0); - if (!diff[key]) diff[key] = void 0; - return true; - } - }; - core.status.hero = new Proxy(hero, handler); - core.status.maps[floorId].blocks.forEach(function (block) { - if ( - block.event.cls !== 'items' || - ignore.includes(block.event.id) || - block.disable - ) - return; - const x = block.x, - y = block.y; - // v2优化,只绘制范围内的部分 - if (core.bigmap.v2) { - if ( - x < core.bigmap.posX - core.bigmap.extend || - x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend || - y < core.bigmap.posY - core.bigmap.extend || - y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend - ) { - return; - } - } - diff = {}; - const id = block.event.id; - const item = core.material.items[id]; - switch (item.cls) { - case 'equips': { - // 装备也显示 - diff = item.equip.value ?? {}; - const per = item.equip.percentage ?? {}; - for (const name in per) { - diff[name + 'per'] = per[name].toString() + '%'; - } - break; - } - case 'items': { - // 跟数据统计原理一样 执行效果 前后比较 - core.setFlag('__statistics__', true); - if (item.itemEffect != null) { - try { - eval(item.itemEffect); - } catch (error) { } - } - const ratio = getRatio(); - const effectObj = core.getItemEffectValue(id, ratio); - for (let statusName in effectObj) { - if (!effectObj.hasOwnProperty(statusName)) continue; - if (!diff.hasOwnProperty(statusName)) diff[statusName] = 0; - diff[statusName] += effectObj[statusName]; - } - break; - } - } - drawItemDetail(diff, x, y); - }); - core.status.thisMap.ratio = beforeRatio; - core.status.hero = before; - window.hero = before; - window.flags = before.flags; - }; - - // 绘制 - function drawItemDetail(diff, x, y) { - const px = 32 * x + 2, - py = 32 * y + 30; - let content = ''; - // 获得数据和颜色 - let i = 0; - for (const name in diff) { - if (!diff[name]) continue; - let color = '#fff'; - - if (typeof diff[name] === 'number') - content = core.formatBigNumber(diff[name], true); - else content = diff[name]; - switch (name) { - case 'atk': - case 'atkper': - color = '#FF7A7A'; - break; - case 'def': - case 'defper': - color = '#00E6F1'; - break; - case 'mdef': - case 'mdefper': - color = '#6EFF83'; - break; - case 'hp': - color = '#A4FF00'; - break; - case 'hpmax': - case 'hpmaxper': - color = '#F9FF00'; - break; - case 'mana': - case 'manamax': - color = '#CC6666'; - break; - case 'money': - color = '#FFD700'; - break; - case 'exp': - color = '#BA55D3'; - break; - } - // 绘制 - core.status.damage.data.push({ - text: content, - px: px, - py: py - 10 * i, - color: color - }); - i++; - } - } - }, - "autoClear": function () { - // 在此增加新插件 - /** - * --------------- 使用说明 --------------- - * 变量autoGet控制自动拾取开关 - * 变量autoBattle控制自动清怪开关 - */ - const ctxName = 'autoClear'; - - // 每走一步后自动拾取的判定要放在阻击结算之后,见libs,并不写在本插件当中 - - core.plugin.autoClear = auto; - - function willLvUp(exp) { - const nextExp = core.getNextLvUpNeed(); - if (typeof exp === 'number' && typeof nextExp === 'number' && exp >= nextExp) return true; - return false; - } - - /** - * 是否清这个怪,可以修改这里来实现对不同怪的不同操作 - * @param {string} enemy - * @param {number} x - * @param {number} y - */ - function canBattle(enemy, x, y) { - const loc = `${x},${y}`; - const floor = core.floors[core.status.floorId]; - const e = core.getEnemyValue(enemy, null, x, y); - const hasEvent = - has(floor.afterBattle[loc]) || has(floor.beforeBattle[loc]) || - has(e.beforeBattle) || has(e.afterBattle) || - has(floor.events[loc]) || willLvUp(e.exp); // 防止有升级后事件 - - // 有事件,不清 - if (hasEvent) return false; - - const cache = core.status.checkBlock.cache; - const hasGuards = has(cache) && has(cache[loc]) && - has(cache[loc]["guards"]) && cache[loc]["guards"].length > 0; // 该敌人会被支援 - if (hasGuards) return false; - - // 有特定特殊属性的怪不清 - if ( - core.hasSpecial(e.special, 12) || // 中毒 - core.hasSpecial(e.special, 13) || // 衰弱 - core.hasSpecial(e.special, 14) || // 诅咒 - core.hasSpecial(e.special, 19) || // 自爆 - core.hasSpecial(e.special, 21) || // 退化 - core.hasSpecial(e.special, 26) || // 支援 - core.hasSpecial(e.special, 27) || // 捕捉:逻辑上应该让怪物来找角色 - core.hasSpecial(e.special, 28) || // 追猎:逻辑上应该让怪物来找角色 - core.hasSpecial(e.special, 29) // 败移:特殊战后事件 - ) { - return false; - } - const damage = core.getDamageInfo(enemy, void 0, x, y)?.damage; - // 0伤或负伤,清 - if (has(damage) && damage <= 0) return true; - return false; - } - - /** - * 判断一个点是否能遍历 - */ - function judge(block, nx, ny, tx, ty, dir, floorId, autoBattle, autoGet) { - if (!has(block)) { // 说明什么都没有,没事件也没图块 - return { type: "none", canGoThrough: true }; - } - const cls = block.event.cls; - const loc = `${tx},${ty}`; - const floor = core.floors[floorId]; - const changeFloor = floor.changeFloor[loc]; - const isEnemy = autoBattle && cls.startsWith('enemy'), - isItem = autoGet && cls === 'items'; - // 因为没有判定往来图块的通行性,这里宁可严格一点,非空地(block.id === 0)一律不给穿 - if (has(changeFloor)) { - if ((changeFloor.ignoreChangeFloor ?? core.flags.ignoreChangeFloor) && block.id === 0) { - return { type: "unknown", canGoThrough: true }; - } - return { type: "unknown", canGoThrough: false }; - } - - if (has(core.floors[floorId].events[loc])) return { type: "unknown", canGoThrough: false }; - - if (isEnemy) return { type: "enemy", canGoThrough: true }; - if (isItem) return { type: "item", canGoThrough: true }; - - return { type: "unknown", canGoThrough: false }; - } - - /** - * 是否捡拾这个物品 - */ - function canGetItem(item, loc, floorId = core.status.floorId) { - // 可以用于检测道具是否应该被捡起,例如如果捡起后血量超过80%则不捡起可以这么写: - // if (item.cls === 'items') { - // let diff = {}; - // const before = core.status.hero; - // const hero = core.clone(core.status.hero); - // const handler = { - // set(target, key, v) { - // diff[key] = v - (target[key] || 0); - // if (!diff[key]) diff[key] = void 0; - // return true; - // } - // }; - // core.status.hero = new Proxy(hero, handler); - - // eval(item.itemEffect); - - // core.status.hero = before; - // window.hero = before; - // window.flags = before.flags; - // if ( - // diff.hp && - // diff.hp + core.status.hero.hp > core.status.hero.hpmax * 0.8 - // ) - // return false; - // } - const floor = core.floors[floorId]; - if (has(floor.afterGetItem[loc])) return false; - if (item.cls === 'items') { - const itemEffectType = core.getItemEffectType(item.id); - if (core.hasFlag('noRouting_HP') && itemEffectType.includes('hp')) return false; - if (core.hasFlag('noRouting_MDEF') && itemEffectType.includes('mdef')) return false; - if (core.hasFlag('noRouting_ATK') && itemEffectType.includes('atk')) return false; - if (core.hasFlag('noRouting_DEF') && itemEffectType.includes('def')) return false; - } - return true; - } - - /** - * @template T - * @param {T} v - * @returns {v is NonNullable} - */ - function has(v) { - return v !== null && v !== undefined; - } - - function hasBlockDamage(loc) { - const checkblockInfo = core.status.checkBlock; - const damage = checkblockInfo.damage[loc]; - const ambush = checkblockInfo.ambush[loc]; - const repulse = checkblockInfo.repulse[loc]; - const chase = checkblockInfo.chase[loc]; - - return (has(damage) && damage > 0) || has(ambush) || has(repulse) || has(chase); - } - - class AttractAnimate { - constructor() { - this.name = 'attractAnimate'; - this.isPlaying = false; - this.nodes = []; - this.lastTime = -1; - this.thr = 5; // 缓动比例倒数,越大移动越慢 - } - - add(id, x, y, callback) { - if (core.isReplaying()) return; - this.nodes.push({ id, x, y, callback }); - } - - start() { - if (this.isPlaying) return; - if (core.isReplaying()) return; - if (core.getLocalStorage('skipPerform')) return; - this.isPlaying = true; - core.registerAnimationFrame(this.name, true, this.update.bind(this)); - this.ctx = core.createCanvas(this.name, 0, 0, core.__PIXELS__, core.__PIXELS__, 120); - } - - remove() { - core.unregisterAnimationFrame(this.name); - core.deleteCanvas(this.name); - this.isPlaying = false; - } - - clear() { - this.nodes = []; - this.remove(); - } - - update(timeStamp) { - const { name, thr, nodes } = this; - - if (this.lastTime < 0) this.lastTime = timeStamp; - if (timeStamp - this.lastTime < 20) return; - this.lastTime = timeStamp; - - core.clearMap(name); - - const heroCenterX = core.status.heroCenter.px - 16; - const heroCenterY = core.status.heroCenter.py - 16; - - for (const n of nodes) { - const dx = heroCenterX - n.x; - const dy = heroCenterY - n.y; - - if (Math.abs(dx) <= thr && Math.abs(dy) <= thr) { - n.dead = true; - } else { - n.x += ~~(dx / thr); - n.y += ~~(dy / thr); - } - - core.drawIcon(name, n.id, n.x, n.y, 32, 32); - } - - // 过滤掉 dead 的节点并执行回调 - const remainingNodes = []; - for (const n of nodes) { - if (n.dead && n.callback) { - n.callback(); - } - if (!n.dead) { - remainingNodes.push(n); - } - } - this.nodes = remainingNodes; - - if (this.nodes.length === 0) { - this.remove(); - } - } - } - - const animateHwnd = new AttractAnimate(); - - /** 拾取单个物品的动画 */ - this.pickOneItemAnimate = function (id, x, y, callback) { - if (core.isReplaying()) return; - animateHwnd.add(id, x, y, callback); - animateHwnd.start(); - }; - /** 在每次切换楼层后调用 */ - this.clearAttractAnimate = function () { - animateHwnd.clear(); - } - - /** - * 广搜,搜索可以到达的需要清的怪 - * @param {string} floorId - */ - function bfs(floorId, deep = Infinity) { - core.extractBlocks(floorId); - const objs = core.getMapBlocksObj(floorId); - const bgMap = core.getBgMapArray(floorId); - const { x, y } = core.status.hero.loc; - const dir = /** @type {[direction, number, number][]} */ Object.entries(core.utils.scan).map(v => [v[0], v[1].x, v[1].y]); - const floor = core.status.maps[floorId]; - - /** @type {[number, number][]} */ - const queue = [ - [x, y] - ]; - const mapped = { - [`${x},${y}`]: true - }; - - const autoBattle = core.getFlag('autoBattle', false), - autoGet = core.getFlag('autoGet', false); - if (!autoGet && !autoBattle) return; - - while (queue.length > 0 && deep > 0) { - const [nx, ny] = queue.shift(); - dir.forEach(v => { - const [tx, ty] = [nx + v[1], ny + v[2]]; - if (tx < 0 || ty < 0 || tx >= floor.width || ty >= floor.height) { - return; - } - const loc = `${tx},${ty}`; - if (mapped[loc]) return; - const block = objs[loc]; - mapped[loc] = true; - if (core.onSki(bgMap[ty][tx])) return; // bfs不允许穿过滑冰 - const { type, canGoThrough } = judge(block, nx, ny, tx, ty, v[0], floorId, autoBattle, autoGet); - if (!canGoThrough) return; - - if (type === 'enemy') { - if (canBattle(block.event.id, tx, ty) && !block.disable) { - core.battle(block.event.id, tx, ty); - core.updateCheckBlock(); - } else { - return; - } - } else if (type === 'item') { - const item = core.material.items[block.event.id]; - if (canGetItem(item, loc, floorId)) { - if (!core.isReplaying()) animateHwnd.add(item.id, 32 * tx, 32 * ty); - core.getItem(item.id, 1, tx, ty); - } else { - return; - } - } - if (hasBlockDamage(loc)) return; - queue.push([tx, ty]); - }); - deep--; - } - } - - function auto() { - if (!core.status.floorId || !core.status.checkBlock.damage) return; // 这两个条件不知道什么情形下会出现 - if (core.status.event.id == 'action' || core.events.onSki() || core.status.lockControl) return; // 在冰上不允许触发自动清怪 - const before = flags.__forbidSave__; - const { x, y } = core.status.hero.loc; - const floor = core.floors[core.status.floorId]; - const loc = `${x},${y}`; - const hasEvent = has(floor.events[loc]); - if (hasEvent) return; // 如果有事件,直接不清了 - const block = core.getBlock(x, y); - if (block != null && block.event.cls !== 'items') return; // 角色站的位置为空地和物品以外的图块,不清(例如箭头,可能无法返回) - - let deep = Infinity; - if (hasBlockDamage(loc)) { - deep = core.flags.enableGentleClick ? 1 : 0; // 角色站的位置有地图伤害时,仍然允许轻点附近1格 - } - flags.__forbidSave__ = true; - flags.__statistics__ = true; - const ctx = core.getContextByName(ctxName); - if (!ctx) { - core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 75); - core.setAlpha(ctxName, 0.6); - } - bfs(core.status.floorId, deep); - if (!core.isReplaying()) animateHwnd.start(); - flags.__statistics__ = false; - flags.__forbidSave__ = before; - core.updateStatusBar(); - } - }, - "scrollingText": function () { - // 本插件用于绘制在线留言 - // 说明:https://h5mota.com/bbs/thread/?tid=1017 - // 目前使用core.http代替帖子中提到的axios - - /** 塔的英文名 */ - const towerName = core.firstData.name; - - let [W, H] = [core.__SIZE__, core.__SIZE__]; - let [WIDTH, HEIGHT] = [core.__PIXELS__, core.__PIXELS__]; - - //#region 弹幕的收发 - this.getComment = function () { - if (core.isReplaying()) return; - let form = new FormData(); - form.append('type', '1'); - form.append('towername', towerName); - core.utils.http( - 'POST', - 'https://h5mota.com/backend/tower/barrage.php', - form, - function (res) { - try { - res = JSON.parse(res); - console.log(res); - core.drawTip('接收成功!', 'postman'); - core.playSound('item.mp3'); - let commentCollection = {}; - const commentList = res?.list; - const isEmpty = /^\s*$/; - for (let i = 0, l = commentList.length; i <= l - 1; i++) { - if (isEmpty.test(commentList[i]?.comment)) continue; - const commentTagsList = commentList[i].tags.split(','); - const [cFloorId, cX, cY] = commentTagsList; - if (0 <= cX && cX <= W - 1 && 0 <= cY && cY <= H - 1 && core.floorIds.includes(cFloorId)) { - if (!commentCollection.hasOwnProperty(cFloorId)) { commentCollection[cFloorId] = {}; } - const str = cX + ',' + cY; - if (!commentCollection[cFloorId].hasOwnProperty(str)) { commentCollection[cFloorId][str] = []; } - commentCollection[cFloorId][str].push(commentList[i]?.comment); - } - } - core.setFlag('commentCollection', commentCollection); - } catch (err) { - core.drawFailTip('在线留言接收失败! ' + err.message, 'postman'); - } - }, - function (err) { - console.log(err); - if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' }; - else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') }; - else err = JSON.parse(err); - core.drawFailTip('在线留言接收失败! ' + err?.message, 'postman'); - }, - null, null, null, 1000 - ); - } - - this.postComment = function (comment, tags) { - if (core.isReplaying()) return; - const isEmpty = /^\s*$/; - if (isEmpty.test(comment)) { - core.drawFailTip('您输入的消息为空,请重发!', 'postman'); - return; - } - let form = new FormData(); - form.append('type', '2'); - form.append('towername', towerName); - form.append('comment', comment); - form.append('tags', tags); - core.utils.http( - 'POST', - 'https://h5mota.com/backend/tower/barrage.php', - form, - function (res) { - try { - res = JSON.parse(res); - console.log(res); - if (res?.code === 0) { - core.drawTip('提交成功! ', 'postman') - } else { - core.drawTip('提交失败! ' + res?.message, 'postman'); - } - } catch (err) { - core.drawFailTip('提交失败! ' + err.message, 'postman'); - } - }, - function (err) { - console.log(err); - if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' }; - else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') }; - else err = JSON.parse(err); - core.drawFailTip('提交失败! ' + err?.message, 'postman'); - }, - null, null, null, 1000 - ); - } - //#endregion - - /** 若变量comment为真,在每层切换时在地上有弹幕的地方显示相应图标。 */ - this.drawCommentSign = function () { - if (!core.hasFlag('comment') || core.isReplaying()) return; - let commentCollection = core.getFlag('commentCollection', {}), - floorId = core.status.floorId; - core.createCanvas('sign', 0, 0, WIDTH, HEIGHT, 61); - core.setOpacity('sign', 0.6); - if (commentCollection.hasOwnProperty(floorId)) { - for (let pos in commentCollection[floorId]) { - const l = commentCollection[floorId][pos].length; - for (let i = 0; i <= l - 1; i++) { - const [x, y] = pos.split(',').map(x => Number(x)); - core.drawIcon('sign', 'postman', 32 * x, 32 * y); - break; - } - } - } - } - - /** 立即清除楼层的弹幕图标。关闭弹幕相关设置时调用。 */ - this.clearCommentSign = function () { - core.deleteCanvas('sign'); - } - - /** 默认一次显示的弹幕数 */ - const showNum = 5; - - // 每走一步或瞬移,调用该函数,若目标点有弹幕,显示之 - this.showComment = function (x, y) { - if (!core.getFlag('comment') || core.isReplaying()) return; - const commentCollection = core.getFlag('commentCollection', {}); - const floorId = core.status.floorId, - str = x + ',' + y; - if (commentCollection.hasOwnProperty(floorId) && - commentCollection[floorId].hasOwnProperty(str)) { - let commentArr = commentCollection[floorId][str].concat(); - const commentArrPicked = pickComment(commentArr, showNum); - drawComment(commentArrPicked); - } - } - - /** 返回从commentArr中挑选showNum个comment组成的数组*/ - function pickComment(commentArr, showNum) { - let showList = []; - if (commentArr.length <= showNum) { - showList = commentArr; - } else { - for (let i = 0; i <= showNum - 1; i++) { - const l = commentArr.length, - n = core.plugin.dice(l - 1); - showList.push(commentArr[n]); - commentArr.splice(n, 1); - } - } - return showList; - } - - function drawComment(commentArr) { - const l = commentArr.length; - let yList = generateCommentYList(20, HEIGHT - 20, showNum); - if (l < showNum) yList = getRandomElements(yList, l); - for (let i = 0; i <= l - 1; i++) { - drawCommentStr(commentArr[i], WIDTH + 20 * Math.random(), - yList[i], Math.random() * 0.1 + 0.1); - } - } - - /** 生成count个随机数,范围从min到max,作为弹幕的y坐标*/ - function generateCommentYList(min, max, count) { - let yList = Array(count).fill(0); - const distance = (max - min) / (count + 1); - for (let i = 0; i < count; i++) { - yList[i] = min + distance * (i + 1) + (Math.random() - 0.5) * (distance / 2); - } - return yList; - } - - function getRandomElements(arr, count) { - let result = [...arr]; - let len = result.length; - count = Math.min(len, count); - - for (let i = len - 1; i > len - 1 - count; i--) { - let j = Math.floor(Math.random() * (i + 1)); - [result[i], result[j]] = [result[j], result[i]]; - } - - return result.slice(len - count); - } - - //#region 弹幕绘制部分 - const { Animation, linear, Ticker } = core.plugin.animate ?? {}; - const ctxName = 'scrollingText'; - - if (Ticker) { - const ticker = new Ticker(); - ticker.add(() => { - if (core.isReplaying()) return; - core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 136); //每帧重绘该画布 - }); - } - - /** - * 绘制弹幕 - * @example - * drawCommentStr('OK', 450, 200, 0.1); - * @param {string} content 弹幕的内容 - * @param {number} x 弹幕的初始x坐标 - * @param {number} y 弹幕的初始y坐标 - * @param {number} vx 弹幕的横向滚动速度 - */ - function drawCommentStr(content, x, y, vx) { - if (core.isReplaying() || !Animation) return; - const ani = new Animation(); - core.plugin.tickerSet.add(ani.ticker); - ani.ticker.add(() => { - core.fillText(ctxName, content, x + ani.x, y, 'white', '16px Verdana'); - }) - // 弹幕的最大长度5600,再长属于异常数据 - const aim = 100 + x + Math.min(core.calWidth(ctxName, content, '16px Verdana'), 5000); - ani.mode(linear()) - .time(aim / vx) - .absolute() - .move(-aim, 0) - ani.all().then(() => { - ani.ticker.destroy(); - }); - } - //#endregion - - }, - "uiBaseClass": function () { - // 本插件定义了一些用于绘制的基类 - /** - * @typedef {(x:number,y:number,px:number,py:number)=>void} posFunc - */ - /** 按钮基类 */ - class ButtonBase { - constructor(x, y, w, h) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - this.disable = false; - this.status = 'none'; - - // 下面三项在initbtnMap时添加 - /** - * @type {MenuBase} 所在的Menu,用于触发重绘等事件 - */ - // @ts-ignore 将在菜单初始化时传入 - this.menu; - /** @type {string} 所在的Menu的画布名称 */ - this.ctx = ''; - /** @type {string|number} 自身在所在的Menu的btnMap中的索引 */ - this.key = ''; - - /** @type {posFunc} */ - this.ondown = () => { }; - /** @type {posFunc|undefined} */ - this.onmove = undefined; - /** @type {posFunc|undefined} */ - this.onup = undefined; - } - - /** 绘制该按钮的外观 - * @interface - */ - draw() { } - - /** 默认为矩形判定区 */ - inRange(px, py) { - return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h; - } - } - const KeyCodeEnum = { - BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, SpaceBar: 32, - PageUp: 33, PageDown: 34, Left: 37, Up: 38, Right: 39, - Down: 40, C: 67, Q: 81, T: 84, - }; - - /** @typedef {'ondown'|'onmove'|'onup'|'keyDown'|'keyUp'|'onmousewheel'} eventType */ - class MenuBase { - /** - * @param {string} name 菜单名称,作为绘制画布时的名称 - * @param {eventType[]} [toListen] - * @param {number} [x] - * @param {number} [y] - * @param {number} [w] - * @param {number} [h] - * @param {number} [zIndex] - */ - constructor(name, toListen, x, y, w, h, zIndex) { - this.name = name; - /** @type {Map} 本菜单上的按钮列表,每次绘制将触发按钮的draw事件 */ - this.btnMap = new Map(); - /** 当前画布是否正被绘制 */ - this.onDraw = false; - /** @type {Set} 当前画布需要监听的事件类型 */ - this.toListen = new Set(toListen); - - this.x = x ?? 0; - this.y = y ?? 0; - this.w = w ?? core.__PIXELS__; - this.h = h ?? core.__PIXELS__; - this.zIndex = zIndex ?? 136; // 136比uievent大1 - } - - // #region 监听事件 - /** 返回换算后的画布上的相对坐标 */ - convertCoordinate(px, py) { - return [px - this.x, py - this.y]; - } - - /** 默认为矩形判定区 */ - inRange(px, py) { - return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h; - } - - ondown(x, y, rawpx, rawpy) { - if (!this.inRange(rawpx, rawpy)) return; - const [px, py] = this.convertCoordinate(rawpx, rawpy); - this.ondownEvent(x, y, px, py); - this.ondownBtnEvent(x, y, px, py); - } - - /** 点击画布自身触发的事件 - * @interface - */ - ondownEvent(x, y, px, py) { } - - // btnMap 一个key指向一个对象 包含btn本身,对应事件,是否disable btn本身不包含event 由菜单赋予 - - /** 点击画布的按钮触发的事件 - * @interface - */ - ondownBtnEvent(x, y, px, py) { - this.btnMap.forEach((btn) => { - if (btn.disable) return; - if (btn.inRange(px, py)) { - btn.ondown(x, y, px, py); - } - }); - } - - /** 屏幕被鼠标滑动或手指拖动时触发的事件 - * @interface (x:number,y:number,px:number,py:number):void - */ - onmove(x, y, rawpx, rawpy) { - if (!this.inRange(rawpx, rawpy)) return; - const [px, py] = this.convertCoordinate(rawpx, rawpy); - this.onmoveEvent(x, y, px, py); - this.onmoveBtnEvent(x, y, px, py); - } - - onmoveEvent(x, y, px, py) { } - - onmoveBtnEvent(x, y, px, py) { - this.btnMap.forEach((btn) => { - if (btn.disable) return; - if (btn.inRange(px, py) && btn.onmove) { - btn.onmove(x, y, px, py); - } - }); - } - - /** 当屏幕被鼠标或手指放开时触发的事件 - * @interface (x:number,y:number,px:number,py:number):void - */ - onup(x, y, rawpx, rawpy) { - if (!this.inRange(rawpx, rawpy)) return; - const [px, py] = this.convertCoordinate(rawpx, rawpy); - this.onupEvent(x, y, px, py); - this.onupBtnEvent(x, y, px, py); - } - - onupEvent(x, y, px, py) { } - - onupBtnEvent(x, y, px, py) { - this.btnMap.forEach((btn) => { - if (btn.disable) return; - if (btn.inRange(px, py) && btn.onup) { - btn.onup(x, y, px, py); - } - }); - } - - /** 按键被按下时触发的事件 - * @interface (keycode:number)=>void - */ - keyDownEvent(keycode) { } - - /** 按键被放开时触发的事件 - * @interface (keycode:number,altkey?:boolean,fromReplay?:boolean)=>void) - */ - keyUpEvent(keycode, altkey, fromReplay) { } - - /** 鼠标滚轮滚动时触发的事件 - * @interface (direct:1|-1):void - */ - onmousewheelEvent(direct) { } - // #endregion - - /** - * @param {string | number} key - * @param {ButtonBase} btn - * @param {posFunc | {ondown:posFunc,onmove?:posFunc,onup?:posFunc}} [event] - */ - registerBtn(key, btn, event) { - btn.menu = this; - btn.ctx = this.name; - btn.key = key; - if (event == null) { } - else if (typeof event === 'function') { - btn.ondown = event; - } else { - const { ondown, onmove, onup } = event; - btn.ondown = ondown; - btn.onmove = onmove; - btn.onup = onup; - } - this.btnMap.set(key, btn); - } - - registerBtns(arr) { - arr.forEach(ele => { - const [key, btn, event] = ele; - this.registerBtn(key, btn, event); - }); - } - - // 创建并返回本菜单的画布 - createCanvas() { - return core.createCanvas(this.name, this.x, this.y, this.w, this.h, this.zIndex); - } - - drawButtonContent() { - this.btnMap.forEach((button) => { - if (!button.disable) button.draw(); - }) - } - - drawContent() { - this.drawButtonContent(); - this.onDraw = true; - } - - beginListen() { - if (this.toListen.has('ondown')) core.registerAction('ondown', this.name, this.ondown.bind(this), 100); - if (this.toListen.has('keyDown')) core.registerAction('keyDown', this.name, this.keyDownEvent.bind(this), 100); - if (this.toListen.has('keyUp')) core.registerAction('keyUp', this.name, this.keyUpEvent.bind(this), 100); - if (this.toListen.has('onmove')) core.registerAction('onmove', this.name, this.onmove.bind(this), 100) - if (this.toListen.has('onup')) core.registerAction('onup', this.name, this.onup.bind(this), 100); - if (this.toListen.has('onmousewheel')) core.registerAction('onmousewheel', this.name, this.onmousewheelEvent.bind(this), 100); - } - - endListen() { - core.unregisterAction('ondown', this.name); - core.unregisterAction('keyDown', this.name); - core.unregisterAction('keyUp', this.name); - core.unregisterAction('onmove', this.name); - core.unregisterAction('onup', this.name); - core.unregisterAction('onmousewheel', this.name); - } - - remove() { - core.ui.deleteCanvas(this.name); - this.onDraw = false; - } - - clear() { - this.endListen(); - this.remove(); - } - - init() { - this.beginListen(); - this.drawContent(); - } - } - - class Pagination extends MenuBase { - constructor(pageList, currPage, name, toListen, x, y, w, h, zIndex) { - super(name, toListen, x, y, w, h, zIndex); - /** - * 当前页面列表 - * @type {Array} - */ - this.pageList = pageList; - /** - * 当前页的序号 - * @type {number} - */ - this.currPage = currPage || 0; - } - - initOnePage(index) { - this.currPage = index; - this.pageList[index].init(); - } - - changePage(num) { - if (num !== this.currPage) { - const beforeMenu = this.pageList[this.currPage]; - beforeMenu.clear(); - } - this.initOnePage(num); - } - - pageDown() { - if (this.currPage > 0) this.changePage(this.currPage - 1); - } - - pageUp() { - if (this.currPage < this.pageList.length - 1) this.changePage(this.currPage + 1); - } - - clear() { - this.pageList.forEach((page) => page.clear()); - super.clear(); - } - } - - // 圆角带文字的按钮 - class RoundBtn extends ButtonBase { - constructor(x, y, w, h, text, config) { - super(x, y, w, h); - this.text = text; - this.config = config || {}; - } - - draw() { - const ctx = this.ctx; - const { x, y, w, h } = this; - const { - fillStyle = 'rgb(204, 204, 204)', strokeStyle = 'black', fontStyle = 'black', - selectedFillStyle = 'rgb(255, 51, 153)', selectedstrokeStyle = 'black', selectedFontStyle = 'white', - radius = 3, lineWidth = 1, angle = null, font = '16px Verdana' - } = this.config || {}; - core.setTextAlign(ctx, 'center'); - core.setTextBaseline(ctx, 'alphabetic'); - if (this.status === 'selected') { - core.fillRoundRect(ctx, x, y, w, h, radius, selectedFillStyle, angle); - core.strokeRoundRect(ctx, x, y, w, h, radius, selectedstrokeStyle, lineWidth, angle); - core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, selectedFontStyle, font); - } else { - core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle); - core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle); - core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, fontStyle, font); - } - } - } - - class IconBtn extends ButtonBase { - constructor(x, y, w, h, icon, config) { - super(x, y, w, h); - this.icon = icon; - this.config = config || {}; - } - - draw() { - const ctx = this.ctx; - const { x, y, w, h } = this; - const { - strokeStyle = 'black', fillStyle = 'white', - radius = 3, lineWidth = 1, angle = null, frame = 0, - iconX = x, iconY = y, iconW = w, iconH = h, - crossline1 = false, crossline2 = false, crossLineOffset = 2, - crossLineStyle = 'red', crossLineWidth = 2, - } = this.config || {}; - if (fillStyle !== 'none') core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle); - if (strokeStyle !== 'none') core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle); - core.drawIcon(ctx, this.icon, iconX, iconY, iconW, iconH, frame); - if (crossline1) { - core.drawLine(ctx, x + crossLineOffset, y + crossLineOffset, - x + w - crossLineOffset, y + h - crossLineOffset, - crossLineStyle, crossLineWidth); - } - if (crossline2) { - core.drawLine(ctx, x + crossLineOffset, y + h - crossLineOffset, - x + w - crossLineOffset, y + crossLineOffset, - crossLineStyle, crossLineWidth); - } - } - } - - class ExitBtn extends ButtonBase { - constructor(x, y, w, h, config) { - super(x, y, w, h); - this.config = config || {}; - } - - draw() { - const ctx = this.ctx; - const { - strokeStyle = ' #D32F2F', fillStyle = ' #EF5350', lineStyle = 'white', - radius = 3, lineOffsetX = 5, lineWidthX = 3, - } = this.config || {}; - const [x, y, w, h] = [this.x, this.y, this.w, this.h]; - core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle); - core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle); - core.drawLine(ctx, x + lineOffsetX, y + lineOffsetX, x + w - lineOffsetX, y + h - lineOffsetX, lineStyle, lineWidthX); - core.drawLine(ctx, x + lineOffsetX, y + h - lineOffsetX, x + w - lineOffsetX, y + lineOffsetX, lineStyle, lineWidthX); - } - } - - class ArrowBtn extends ButtonBase { - constructor(x, y, w, h, dir, config) { - super(x, y, w, h); - this.config = config || {}; - /** @type {'left'|'right'} */ - this.dir = dir; - } - - draw() { - const { - marginLeft = 6, marginTop = 5, marginRight = 4, - backStyle = 'gray', arrowStyle = 'black' - } = this.config || {}; - const { x, y, w, h, ctx } = this; - core.fillRoundRect(ctx, x, y, w, h, 3, backStyle); - if (this.dir === 'left') - core.fillPolygon(ctx, [ - [x + w - marginLeft, y + marginTop], - [x + w - marginLeft, y + h - marginTop], - [x + marginRight, y + h / 2] - ], arrowStyle); - else if (this.dir === 'right') - core.fillPolygon(ctx, [ - [x + marginLeft, y + marginTop], - [x + marginLeft, y + h - marginTop], - [x + w - marginRight, y + h / 2] - ], arrowStyle); - } - } - core.plugin.uiBase = { - ButtonBase, RoundBtn, IconBtn, ExitBtn, - ArrowBtn, MenuBase, Pagination, KeyCodeEnum - }; - }, - "newBackpackLook": function () { - // 本插件定义了一些用于绘制的基类 - - let __enable = true; - if (!__enable) return; - - /** @todo 尝试干掉redraw */ - // #region 复写 - - core.ui._drawToolbox = function () { drawItemBox('all'); }.bind(core.ui); - core.ui._drawEquipbox = function () { drawItemBox('equips'); }.bind(core.ui); - core.actions._keyDownToolbox = core.actions._keyDownEquipbox = function (keyCode) { return true; }.bind(core.actions); - core.actions._clickToolbox = core.actions._clickEquipbox = function (x, y, px, py) { return true; }.bind(core.actions); - core.actions._keyUpToolbox = core.actions._keyUpEquipbox = function (keyCode) { return true; }.bind(core.actions); - // 暂不考虑修改core.status.event.id,该变量牵涉太多,作用不完全清楚 - // 录像模式下下列函数会进行检测,不处于特定模式时,阻止自定义监听事件 - core.actions._checkReplaying = function () { - if (core.isReplaying() && !UI._back?.onDraw && - ['save', 'book', 'book-detail', 'viewMaps', 'toolbox', 'equipbox', 'text'].indexOf(core.status.event.id) < 0) { - return true; - } - return false; - }.bind(core.actions); - - const oriClosePanel = core.ui.closePanel; - core.ui.closePanel = function () { - oriClosePanel.apply(core.ui, [arguments]); - UI.clearAll(); - } - - core.control._replayAction_item = function (action) { - if (action.indexOf("item:") != 0) return false; - const itemId = action.substring(5); - if (!core.canUseItem(itemId)) return false; - if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) { - core.useItem(itemId, false, core.replay); - return true; - } - core.ui._drawToolbox(0); - const itemInv = UI.itemInv; - const totalIndex = itemInv.allItemList.indexOf(itemId); - const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0); - const currIndex = totalIndex - page * itemInv.pageCap; - itemInv.page = page; - itemInv.setIndex(currIndex); - itemInv.drawContent(); - setTimeout(function () { - core.ui.closePanel(); - core.useItem(itemId, false, core.replay); - }, core.control.__replay_getTimeout()); - return true; - } - - core.control._replayAction_equip = function (action) { - if (action.indexOf("equip:") != 0) return false; - const equipId = action.substring(6); - if (!core.hasItem(equipId)) { - core.removeFlag('__doNotCheckAutoEvents__'); - return false; - } - - const callbackFunc = function () { - const next = core.status.replay.toReplay[0] || ""; - if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { - core.removeFlag('__doNotCheckAutoEvents__'); - core.checkAutoEvents(); - } - core.replay(); - } - core.setFlag('__doNotCheckAutoEvents__', true); - - core.status.route.push(action); - if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) { - core.loadEquip(equipId, callbackFunc); - return true; - } - core.ui._drawEquipbox(0); - const { itemId, itemInv, equipSlots } = UI; - const totalIndex = itemInv.allItemList.indexOf(itemId); - const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0); - const currIndex = totalIndex - page * itemInv.pageCap; - itemInv.page = page; - itemInv.setIndex(currIndex); - itemInv.drawContent(); - equipSlots.drawContent(); - setTimeout(function () { - core.ui.closePanel(); - core.loadEquip(equipId, callbackFunc); - }, core.control.__replay_getTimeout()); - return true; - } - - core.control._replayAction_unEquip = function (action) { - if (action.indexOf("unEquip:") != 0) return false; - const equipType = parseInt(action.substring(8)); - if (!core.isset(equipType)) { - core.removeFlag('__doNotCheckAutoEvents__'); - return false; - } - - const callback = function () { - [UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => { - if (menu && menu.onDraw) menu.drawContent(); - }); - core.ui.closePanel(); - const next = core.status.replay.toReplay[0] || ""; - if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { - core.removeFlag('__doNotCheckAutoEvents__'); - core.checkAutoEvents(); - } - core.replay(); - } - core.setFlag('__doNotCheckAutoEvents__', true); - - core.status.route.push(action); - if (core.status.replay.speed == 24) { - core.unloadEquip(equipType, callback); - return true; - } - const { itemInv, equipSlots } = UI; - const page = Math.max(Math.ceil(equipType / itemInv.pageCap) - 1, 0); - const currIndex = equipType - page * itemInv.pageCap; - itemInv.page = page; - itemInv.setIndex(currIndex); - itemInv.drawContent(); - equipSlots.drawContent(); - core.ui._drawEquipbox(0); - setTimeout(function () { - core.unloadEquip(equipType, callback); - }, core.control.__replay_getTimeout()); - return true; - } - core.registerReplayAction("item", core.control._replayAction_item); - core.registerReplayAction("equip", core.control._replayAction_equip); - core.registerReplayAction("unEquip", core.control._replayAction_unEquip); - - // 复写control.startReplay - const oriStartReplay = core.control.startReplay; // 进入播放录像模式时清空道具栏选中目标的缓存 - core.control.startReplay = function (list) { - clearItemBoxCache(); - oriStartReplay.apply(core.control, [list, ...arguments]); - } - - // 复写items._afterUseItem - // flag:itemsUsedCount {[itemId:string]:boolean} 成功使用了的道具的计数 - const origin__afterUseItem = items.prototype._afterUseItem; - items.prototype._afterUseItem = function (itemId) { - const itemsUsedCount = core.getFlag('itemsUsedCount', {}); - if (!itemsUsedCount.hasOwnProperty(itemId)) itemsUsedCount[itemId] = 0; - itemsUsedCount[itemId]++; - core.setFlag('itemsUsedCount', itemsUsedCount); - origin__afterUseItem(itemId); - } - - // 复写ui.getToolboxItems - // flag:markedItems string[] 在道具栏中置顶的道具的列表 - // flag:hideInfo {[itemId:string]:boolean} 手动选择了显示/隐藏的道具的列表 - core.ui.getToolboxItems = function (cls, showHide, sortFunc) { - const markedItems = core.getFlag('markedItems', []); - - // 暂时不采用按使用次数排序这个函数 因为导致物品排序频繁变动,反而体验不好 - // const itemsUsedCount = core.getFlag('itemsUsedCount', {}); - // if (!sortFunc) sortFunc = (itemId1, itemId2) => { - // const item1Count = itemsUsedCount[itemId1] || 0, - // item2Count = itemsUsedCount[itemId2] || 0; - // return item2Count - item1Count; - // } - - let list = []; - if (cls === 'all') { - for (let name in core.status.hero.items) { - if (name === "equips") continue; - list = list.concat(Object.keys(core.status.hero.items[name])); // 获取'constants'和'tools'整体的列表 - } - } else if (core.status.hero.items[cls]) { - list = Object.keys(core.status.hero.items[cls] || {}); - } - const markedList = list.filter((itemId) => markedItems.includes(itemId)).sort(sortFunc), - unmarkedList = list.filter((itemId) => !markedItems.includes(itemId)).sort(sortFunc); - list = [...markedList, ...unmarkedList]; - const hideInfo = core.getFlag('hideInfo', {}); - if (!showHide) list = list.filter(function (id) { - if (hideInfo[id]) return false; - return !core.material.items[id].hideInToolbox; - }) - return list; - } - - // 复写resize,保证屏幕变化时此画布表现正常 - const originResize = core.control.resize; - core.control.resize = function () { - originResize.apply(core.control, arguments); - const { _back, itemInv, _equipSlots: _equipChangeBoard, _itemInfo: _itemInfoBoard } = UI; - [_back, itemInv, _equipChangeBoard, _itemInfoBoard].forEach((menu) => { if (menu && menu.onDraw) menu.drawContent(); }); - } - - // #endregion - const { ButtonBase, RoundBtn, IconBtn, ExitBtn, MenuBase, KeyCodeEnum } = core.plugin.uiBase; - // #region 绘制用到的按钮类 - - /** 隐藏物品的按钮 */ - class HideBtn extends RoundBtn { - constructor(x, y, w, h, config) { - super(x, y, w, h, '隐藏', config); - } - - draw() { - const itemId = UI.itemId; - if (core.material.items[itemId]) { - const hideInfo = core.getFlag('hideInfo', {}); - if (hideInfo.hasOwnProperty(itemId)) this.text = hideInfo[itemId] ? "显示" : "隐藏"; - else this.text = core.material.items[itemId].hideInToolbox ? "显示" : "隐藏"; - } - super.draw(); - } - } - - /** 置顶物品的按钮 */ - class MarkBtn extends RoundBtn { - constructor(x, y, w, h, config) { - super(x, y, w, h, '置顶', config); - } - - draw() { - const itemId = UI.itemId; - const markedItems = core.getFlag('markedItems', []); - this.text = markedItems.includes(itemId) ? "取消" : "置顶"; - super.draw(); - } - } - - /** 切换到显示指定分类的物品(如:永久,消耗)的模式的按钮 */ - class ClassifyBtn extends RoundBtn { - constructor(x, y, w, h, text, subType, config) { - super(x, y, w, h, text, config); - this.subType = subType; - } - - draw() { - const { type, toolInv } = UI; - if (toolInv.subType === this.subType) this.status = 'selected'; - else this.status = "none"; - super.draw(); - } - } - - /** 控制是否显示隐藏物品的按钮 */ - class ShowHideBtn extends ButtonBase { - draw() { - const ctx = this.ctx; - const squareSize = this.h; - core.strokeRect(ctx, this.x, this.y, squareSize, squareSize, 'black'); - core.fillRect(ctx, this.x + 1, this.y + 1, squareSize - 2, squareSize - 2, 'white'); - const font = core.ui._buildFont(this.h - 4); - core.setTextAlign(ctx, 'left'); - core.setTextBaseline(ctx, 'middle'); - if (core.hasFlag('showHideItem')) core.fillText(ctx, '√', this.x + 3, this.y + 10, 'red', font); - core.fillText(ctx, '查看隐藏', this.x + squareSize + 6, this.y + 10, 'white', font); - } - } - - /** 切换道具栏和装备栏的按钮 */ - class SwitchBtn extends IconBtn { - constructor(x, y, w, h, config) { - super(x, y, w, h, 'toolbox', config); - } - - draw() { - this.icon = (UI.type === 'all') ? 'toolbox' : 'equipbox'; - super.draw(); - } - } - - class NoHoverBtn extends IconBtn { - draw() { - super.draw(); - const ctx = this.ctx; - const { x, y, w, h } = this; - core.drawImage(ctx, "mousewheel.png", x, y, w, h); - } - } - - class ArrowBtn extends ButtonBase { - constructor(x, y, w, h, dir, config) { - super(x, y, w, h); - this.config = config || {}; - /** @type {'left'|'right'} */ - this.dir = dir; - } - - draw() { - const { - marginLeft = 6, marginTop = 5, marginRight = 4, - backStyle = 'gray', arrowStyle = 'black' - } = this.config || {}; - const ctx = this.ctx; - core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, backStyle); - if (this.dir === 'left') - core.fillPolygon(ctx, [ - [this.x + this.w - marginLeft, this.y + marginTop], - [this.x + this.w - marginLeft, this.y + this.h - marginTop], - [this.x + marginRight, this.y + this.h / 2] - ], arrowStyle); - else if (this.dir === 'right') - core.fillPolygon(ctx, [ - [this.x + marginLeft, this.y + marginTop], - [this.x + marginLeft, this.y + this.h - marginTop], - [this.x + this.w - marginRight, this.y + this.h / 2] - ], arrowStyle); - core.setAlpha(ctx, 1); - } - } - - /** 切换装备面板的单个装备选框 */ - class EquipBox extends ButtonBase { - constructor(x, y, w, h) { - super(x, y, w, h); - /** @type {EquipSlots} */ // @ts-ignore - this.menu; - this.key = -1; - } - - draw() { - const ctx = this.ctx; - const [x, y, w, h] = [this.x, this.y, this.w, this.h]; - const space = 2, - lineWidth = 2, - squareSize = w; - const equipId = core.getEquip(this.menu.getTotalIndex(this.key)); - if (equipId) core.drawIcon(ctx, equipId, x + 4, y + 4, squareSize - 8, squareSize - 8); - const color = (UI.selectType === 'equipBox' && this.menu.index === this.key) ? 'gold' : 'white'; - core.strokeRect(ctx, x, y, squareSize, squareSize, color, lineWidth); - core.setTextAlign(ctx, "center"); - core.setTextBaseline(ctx, "top"); - const tx = x + w / 2, - ty = y + squareSize + space; - core.fillText(ctx, this.menu.currItemList[this.key], tx, ty, color, '14px Verdana'); - } - } - - // #endregion - // #region 绘制用到的菜单类 - - // 道具栏/装备栏的背景 - class ItemBoxBack extends MenuBase { - constructor() { - // 装备栏和道具栏共用同一个光标,故所有按键事件全部写在这里处理 - super('itemBoxBack', ['ondown', 'keyDown', 'keyUp'], null, null, null, null, 136); - } - - keyDownEvent(keyCode) { - const { type, selectType, itemInv, equipSlots } = UI; - if (keyCode === KeyCodeEnum.Left) { // left - if (selectType === 'toolBox') itemInv.pageDown(); - else if (selectType === 'equipBox') { - if (equipSlots.index === 0) { - equipSlots.pageDown(); - } else { - equipSlots.setIndex(equipSlots.index - 1); - } - } - } else if (keyCode === KeyCodeEnum.Right) { // right - if (selectType === 'toolBox') itemInv.pageUp(); - else if (selectType === 'equipBox') { - if (equipSlots.index === equipSlots.currItemList.length - 1) { - equipSlots.pageUp(); - } else { - equipSlots.setIndex(equipSlots.index + 1); - } - } - } else if (keyCode === KeyCodeEnum.Up) { // up - if (selectType === 'toolBox') { - if (itemInv.index === 0) { - if (type === 'equips') { // 在仅物品栏模式下点上键到顶,切换到上一页,否则会切换到装备栏 - equipSlots.setIndex(equipSlots.currItemList.length - 1); - } else { - itemInv.pageDown(); // 向上到顶将翻到上一页 - } - } else { - itemInv.setIndex(itemInv.index - 1); - } - } else if (selectType === 'equipBox') { - if (equipSlots.index >= equipSlots.rowMax) { - equipSlots.index -= equipSlots.rowMax; - } - } - } else if (keyCode === KeyCodeEnum.Down) { // down - if (selectType === 'toolBox') { - if (itemInv.index < itemInv.currItemList.length - 1) { - itemInv.setIndex(itemInv.index + 1); - } else { - itemInv.pageUp(); // 向下到底将翻到下一页 - } - } else if (selectType === 'equipBox') { - let newIndex = equipSlots.index + equipSlots.rowMax; - if (newIndex < equipSlots.currItemList.length - 1) { - equipSlots.setIndex(newIndex); - } else { - equipSlots.setIndex(0); - } - } - } - } - - keyUpEvent(keyCode, altKey) { - const { itemId, selectType, itemInv, equipSlots: equipChangeBoard } = UI; - if (keyCode === KeyCodeEnum.Q) { // Q - if (UI.type === "equips") UI.exit(); - else UI.switchType(); - } else if (keyCode === KeyCodeEnum.T) { // T - if (UI.type === "all") UI.exit(); - else UI.switchType(); - } else if (keyCode === KeyCodeEnum.BackSpace || keyCode === KeyCodeEnum.Esc) { // BackSpace/Esc - UI.exit(); - } else if (keyCode === KeyCodeEnum.Enter || keyCode === KeyCodeEnum.SpaceBar || keyCode === KeyCodeEnum.C) { // Enter/SpaceBar/C - if (selectType === "toolBox") { - if (core.material.items[itemId]) itemInv.triggerItem(); - } else if (selectType === "equipBox") { - equipChangeBoard.triggerItem(); - [UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => { - if (menu && menu.onDraw) menu.drawContent(); - }); - } else { - itemInv.setIndex(0); - } - } else if (altKey && keyCode >= 48 && keyCode <= 57) { // 都有自动切装了还有神人想要这个Alt换装 服了 - core.items.quickSaveEquip(keyCode - 48); - return; - } - } - - drawContent() { - const ctx = this.createCanvas(); - this.drawBackGround(ctx); - super.drawContent(); - } - - drawBackGround(ctx) { - core.strokeRoundRect(ctx, 2, 2, 412, 412, 5, 'white', 2); - core.fillRoundRect(ctx, 3, 3, 410, 410, 5, 'rgb(108, 187, 219)'); - core.drawLine(ctx, 248, 3, 248, 413, 'white', 2); // 左栏和右栏的分界线 - if (UI.type === 'equips') core.drawLine(ctx, 3, 140, 248, 140, 'white', 2); // 装备栏和道具栏的分界线 - } - } - - // 物品列表和换装界面的共用基类 - class ItemListBase extends MenuBase { - constructor(name, x, y, w, h, zIndex) { - super(name, ['ondown', 'onmove'], x, y, w, h, zIndex); - /** 当前页 */ - this.page = 0; - /** 一页最多可容纳的道具数量 */ - this.pageCap = 0; - /** 当前最大页数,page应小于此值*/ - this.pageMax = 1; - /** @type {string[]} 此界面总的物品列表 */ - this.allItemList = []; - /** @type {string[]} 此界面当前页展示的物品列表 */ - this.currItemList = []; - /** 当前选中了第几个道具 */ - this.index = 0; - } - - /** - * @abstract 获取最新的物品列表 - * @returns {string[]} - */ - getItemList() { return []; } - - /** - * @virtual - * @description 选中指定位置 - * @param {number} index - **/ - setIndex(index) { } - - /** - * @abstract 尝试使用当前选中的物品 - */ - triggerItem() { } - - /** 更新物品列表 */ - updateItemList() { - this.allItemList = this.getItemList(); - this.pageMax = Math.ceil(this.allItemList.length / this.pageCap); - if (this.pageMax < 1) this.pageMax = 1; - if (this.page >= this.pageMax) this.page = this.pageMax - 1; - this.currItemList = this.allItemList.slice(this.page * this.pageCap, (this.page + 1) * this.pageCap); - if (this.index >= this.currItemList.length && this.currItemList.length > 0) this.setIndex(this.currItemList.length - 1); - } - - canPageUp() { - return this.page < this.pageMax - 1; - } - - canPageDown() { - return this.page > 0; - } - - /** 翻页,更新物品列表,并重绘该菜单自身 */ - pageUp() { - if (!this.canPageUp()) return; - this.page++; - this.updateItemList(); - this.setIndex(Math.min(this.index, this.currItemList.length - 1)); - this.drawContent(); - } - - /** 翻页,更新物品列表,并重绘该菜单自身 */ - pageDown() { - if (!this.canPageDown()) return; - this.page--; - this.updateItemList(); - this.setIndex(this.index); - this.drawContent(); - } - } - - /** 展示角色当前已穿戴的装备的面板 */ - class EquipSlots extends ItemListBase { - constructor(x, y, w, h, zIndex) { - super('equipChangeBoard', x, y, w, h, zIndex); - this.columnMax = 4; - this.rowMax = 2; - this.pageCap = this.columnMax * this.rowMax; - this.updateItemList(); - const currNameList = this.currItemList; - - const columnCount = Math.min(currNameList.length, this.columnMax), // 判断装备孔数量是否小于最大列数 - rowCount = Math.min(Math.ceil(currNameList.length / this.columnMax), this.rowMax); - const [boxWidth, boxHeight] = [36, 52]; - const spaceX = (this.w - columnCount * boxWidth) / (1 + columnCount), - spaceY = (this.h - rowCount * boxHeight) / (1 + rowCount); - let [xi, yi] = [spaceX, spaceY]; - - // 装备孔的按钮在这里注册 - for (let i = 0; i < this.pageCap; i++) { - if (!this.btnMap.has(i)) { - const btn = new EquipBox(xi, yi, boxWidth, boxHeight); - this.registerBtn(i, btn, { - ondown: function () { - if (this.index !== i) { - this.setIndex(i); - } else this.triggerItem(i); - }.bind(this), - onmove: function () { - if (UI.isNoHover) return; - if (this.index !== i) { - this.setIndex(i); - } - }.bind(this), - }); - if ((i >= this.currItemList.length)) btn.disable = true; - } else { - const btn = this.btnMap.get(i); - if (btn) btn.disable = (i >= this.currItemList.length); - } - if ((i + 1) % this.columnMax === 0) { - xi = spaceX; - yi += spaceY + boxHeight; - } else { xi += spaceX + boxWidth; } - } - } - - drawContent() { - const ctx = this.createCanvas(); - if (this.pageMax > 1) { - core.setTextAlign(ctx, "center"); - core.setTextBaseline(ctx, "alphabetic"); - core.fillText(ctx, this.page + 1 + '/' + this.pageMax, this.w / 2, this.h - 2, 'white', '12px Verdana'); - } - // 切装面板只有1页时不激活翻页按钮 - if (this.allItemList.length < this.pageCap) { - const pgDown = this.btnMap.get('pgDownBtn'); - const pgUp = this.btnMap.get('pgUpBtn'); - if (pgDown) pgDown.disable = true; - if (pgUp) pgUp.disable = true; - } - - super.drawContent(); - } - - /** 注意,对于装备切换面板来说,它的装备列表不是装备本身,而是角色的装备孔 */ - getItemList() { - return core.status.globalAttribute.equipName; - } - - /** @param {number} index 根据当前页选中的序号换算对应装备在角色装备中的总序号 */ - getTotalIndex(index) { - if (index == null) index = this.index; - return this.page * this.pageCap + index; - } - - /** @param {number} index */ - changePageByTotalIndex(index) { - const newPage = Math.floor(index / this.pageCap); - this.page = newPage; - this.updateItemList(); - } - - /** 脱下指定位置的装备 */ - triggerItem(index) { - const totalIndex = this.getTotalIndex(index); - if (core.status.hero.equipment[totalIndex]) { - core.unloadEquip(totalIndex); - core.status.route.push("unEquip:" + totalIndex); - this.updateItemList(); - this.drawContent(); - UI.itemInv.updateItemList(); //穿脱装备是双向的过程,装备栏和道具栏的物品列表组成都会变 - UI.itemInv.drawContent(); - UI.itemId = ''; - } - } - - setIndex(index) { - this.index = index; - UI.equipInv.index = -1; - core.ui.clearUIEventSelector(1); - // 被选中的装备框变色 - this.btnMap.forEach((ele, key) => { - if (ele instanceof EquipBox) { - if (key === index) ele.status = 'selected'; - else ele.status = 'none'; - } - }) - UI.selectType = 'equipBox'; - const totalIndex = this.getTotalIndex(index); - UI.itemId = core.status.hero.equipment[totalIndex]; - this.drawButtonContent(); - } - - pageUp() { - if (!this.canPageUp()) return; - this.page++; - this.updateItemList(); - this.btnMap.forEach((btn, key) => { - if (btn instanceof EquipBox) { - if (Number(key) >= this.currItemList.length) btn.disable = true; - } - }); - this.setIndex(Math.min(this.index, this.currItemList.length - 1)); - this.drawContent(); - } - - pageDown() { - if (!this.canPageDown()) return; - this.page--; - this.updateItemList(); - this.btnMap.forEach((btn, key) => { - if (btn instanceof EquipBox) { - if (Number(key) <= this.currItemList.length - 1) btn.disable = false; - } - }); - this.setIndex(this.index); - this.drawContent(); - } - } - - /** 展示角色当前背包物品的面板,有道具/装备两种模式 */ - class InventoryBase extends ItemListBase { - /** - * @param {*} name - * @param {string} x - * @param {number} y - * @param {number} w - * @param {number} h - * @param {number} zIndex - * @param {number} pageCap 单个页面显示的物品数 - */ - constructor(name, x, y, w, h, zIndex, pageCap) { - super(name, x, y, w, h, zIndex); - - /** @type {number} 单个物品占据的列宽 */ - this.oneItemHeight = 30; - /** @type {number} 单个页面显示的物品数, -1是因为最后一行要留给换行按钮*/ - this.pageCap = pageCap; - this.marginLeft = 15; // 物品栏左边距 - this.marginTop = 40; // 物品栏上边距 - this.x0 = this.x + this.marginLeft; // 物品栏左边距 - this.y0 = this.y + this.marginTop; - this.w0 = this.w - this.marginLeft - 5; // 物品栏真实宽度 - this.h0 = this.oneItemHeight * (this.pageCap + 1); // +1是因为最后一行要留给翻页按钮 - } - - drawContent() { - const ctx = this.createCanvas(); - const { x0, y0, w0, h0, marginTop, marginLeft } = this; - const { w, h } = this; - core.fillRect(ctx, marginLeft, marginTop, w0, h0, 'rgb(0, 105, 148)'); - - core.setTextBaseline(ctx, "middle"); - for (let i = 0; i < this.currItemList.length; i++) { - this.drawOneItem(i); - } - core.setTextAlign(ctx, "center"); - core.setTextBaseline(ctx, "alphabetic"); - core.fillText(ctx, (this.page + 1) + '/' + this.pageMax, w / 2, y0 + h0 - 4, 'white', '12px Verdana'); - super.drawContent(); - } - - /** @param {number} currIndex */ - drawOneItem(currIndex) { - const itemId = this.currItemList[currIndex]; - const ctx = core.dymCanvas[this.name]; - const { x0, y0, marginTop } = this; - const dy = this.oneItemHeight * currIndex + marginTop; - /** @type {Item|{}} */ - const item = core.material.items[itemId] || {}; - const num = core.formatBigNumber(core.itemCount(itemId), 5) || 0; // 道具数量过大时需要format - - // 被隐藏的道具在显示时需要半透明 - const hideInfo = core.getFlag('hideInfo', {}); - if (item && (hideInfo.hasOwnProperty(itemId) ? hideInfo[itemId] : item.hideInToolbox)) core.setAlpha(ctx, 0.5); - - // 绘制物品图标 - if (core.material.items[itemId]) core.drawIcon(ctx, itemId, x0 + 4, dy + 6, 18, 18); - - core.setTextAlign(ctx, "right"); - core.setTextBaseline(ctx, "middle"); - // 绘制物品数量 ×几 - const numText = "×" + num; - core.fillText(ctx, numText, x0 + 218, dy + this.oneItemHeight / 2, 'white', '18px Verdana'); - - // 绘制物品名称 - const markedItems = core.getFlag('markedItems', []); - const name = item.name || "???"; - core.setTextAlign(ctx, "left"); - core.fillText(ctx, name, x0 + 24, dy + this.oneItemHeight / 2, markedItems.includes(itemId) ? 'gold' : 'white', '18px Verdana', 180); - core.setAlpha(ctx, 1); - } - - clear() { - core.clearUIEventSelector(1); - super.clear(); - } - - /** 绘制选中物品的光标,在selectType或index改变时自动执行绘制/擦除 - * @param {number} index 选中物品在当前页的序号 - */ - drawSelector(index) { - const [x0, y0, w0] = [this.x0, this.y0, this.w0]; // 光标绘制是绝对坐标 - core.drawUIEventSelector(1, 'winskin.png', x0, y0 + index * this.oneItemHeight, w0, this.oneItemHeight, 140); - } - - /** 选中指定序号的位置,改变选中道具的ID,重绘光标 - * @param {number} index 选中物品在当前页的序号 - */ - setIndex(index) { - this.index = index; - if (UI.type === 'equips') { - UI.equipSlots.index = -1; - UI.equipSlots.drawButtonContent(); // 清除装备栏的选中状态 - UI.selectType = 'toolBox'; - } - this.drawSelector(index); - UI.itemId = this.currItemList[index]; - } - - ondownEvent(_x, _y, px, py) { - py -= this.marginTop; // 转换为相对于物品栏内部的坐标 - const index = Math.floor(py / this.oneItemHeight); - if (index < 0 || index >= this.currItemList.length) return; - if (UI.selectType !== 'toolBox' || this.index !== index) { - this.setIndex(index); - } else { - this.triggerItem(); - } - } - - onmoveEvent(_x, _y, px, py) { - if (UI.isNoHover) return; - py -= this.marginTop; // 转换为相对于物品栏内部的坐标 - const index = Math.floor(py / this.oneItemHeight); - if (index < 0 || index >= this.currItemList.length) return; - if (UI.selectType !== 'toolBox' || this.index !== index) { - this.setIndex(index); - } - } - } - - class ToolInventory extends InventoryBase { - constructor(x, y, w, h, zIndex, pageCap) { - super('toolInventory', x, y, w, h, zIndex, pageCap); - /** @type {'all'|'tools'|'constants'} 当前显示哪个子菜单 */ - this.subType = 'all'; - - /** 各个子页面当前选中的位置,用于在切换后显示原位置 */ - this.cache = { - all: { page: 0, index: 0 }, - tools: { page: 0, index: 0 }, - constants: { page: 0, index: 0 } - } - } - - getItemList() { - return core.getToolboxItems(this.subType, core.hasFlag('showHideItem')); - } - - triggerItem() { - const itemId = UI.itemId; - if (!core.canUseItem(itemId) && itemId !== 'centerFly') { - core.drawFailTip("当前无法使用" + core.material.items[itemId].name, itemId); - return; - } - UI.clearAll(); - setTimeout(() => { - core.unlockControl(); - core.tryUseItem(itemId); - }, 0); - } - - /** 物品栏仅显示指定类型物品 */ - classify(subType) { - if (UI.type !== 'all') { - // 非物品栏类型的菜单不支持分类 - return; - } - if (this.subType !== subType) { - const oldConfig = this.cache[this.subType], - newConfig = this.cache[subType]; - oldConfig.page = this.page; - oldConfig.index = this.index; - this.page = newConfig.page; - this.index = newConfig.index; - this.subType = subType; - this.updateItemList(); - this.setIndex(this.index); - this.drawContent(); - UI.back.drawContent(); // 切换物品栏类型时需要重绘背景 - } - } - } - - class EquipInventory extends InventoryBase { - getItemList() { - return core.getToolboxItems('equips', core.hasFlag('showHideItem')); - } - - triggerItem() { - const equip = UI.itemId; - if (!core.canEquip(equip, true)) return; - const equipPos = core.getEquipTypeById(equip); - UI.equipSlots.changePageByTotalIndex(equipPos); - core.loadEquip(equip); - core.status.route.push("equip:" + equip); - this.updateItemList(); // 穿上装备会导致道具数量变化,需要重新生成装备列表 - this.setIndex(this.index); - this.drawContent(); - UI.equipSlots.drawContent(); - } - } - - class ItemInfoBoard extends MenuBase { - constructor(x, y, w, h) { - super('itemInfoBox', ['ondown'], x, y, w, h, 137); - } - - drawContent() { - const ctx = this.createCanvas(); - core.strokeRoundRect(ctx, 23, 27, 32, 32, 2, 'white', 2); - const itemId = UI.itemId; - if (itemId) core.drawIcon(ctx, itemId, 24, 28, 30, 30); - - // 修改这里可以编辑未选中道具时的默认值 - const defaultItem = { cls: "constants", name: "无道具", id: "-", text: "没有道具最永久" }; - const defaultEquip = { cls: "equips", name: "无装备", id: "-", text: "一无所有,又何尝不是一种装备", equip: { type: "装备" } }; - let item = core.material.items[itemId]; - if (!item) item = (UI.type === 'all' ? defaultItem : defaultEquip); - core.setTextAlign(ctx, "left"); - core.setTextBaseline(ctx, "middle"); - core.fillText(ctx, item.name, 66, 46, 'black', 'bold 18px Verdana', 98); // 物品名字 e.g.护符 - core.fillText(ctx, "类型", 20, 75, 'crimson', '14px Verdana'); - core.fillText(ctx, "【" + getItemClsName(item) + "】", 50, 75, 'rgb(47, 49, 54)', '14px Verdana'); // 物品类型 e.g.【永久道具】 - - if (UI.type === 'all') { // 显示物品累计使用的次数 - core.fillText(ctx, "累计使用", 20, 95, 'crimson', '14px Verdana'); - const itemsUsedCount = core.getFlag('itemsUsedCount', {}); - core.fillText(ctx, itemsUsedCount[itemId] || 0, 80, 95, 'rgb(47, 49, 54)', '14px Verdana'); - } - const rawItemText = core.replaceText(item.text) ?? ""; - const itemText = rawItemText + ((UI.type === "equips") ? this.getEquipCompareInfo(item) : ""); // 物品描述信息 - core.drawTextContent(ctx, itemText, { - left: 20, - top: 110, - bold: false, - color: "black", - align: "left", - fontSize: 15, - maxWidth: 150 - }); - const currItemHotKey = HotkeySelect.getHotkeyNum(itemId); - // 获取快捷键设置按钮当前的图标 - - const setHotkeyBtn = /** @type {IconBtnClass} */ (this.btnMap.get('setHotkeyBtn')); - if (setHotkeyBtn) { - setHotkeyBtn.disable = (UI.type === 'equips'); - setHotkeyBtn.icon = (currItemHotKey == null) ? 'keyboard' : ('btn' + currItemHotKey); - } - super.drawContent(); - } - - /*** @param {Item} item */ - getEquipCompareInfo(item) { - let str = ''; - if (UI.type !== "equips") return str; - - let equipType = item.equip?.type; - if (!equipType) return str; - if (typeof equipType == "string") equipType = core.getEquipTypeByName(equipType); - let compare; - /** @todo 准备卸下装备时显示卸下的比较信息 */ - if (UI.selectType == "equipBox") compare = core.compareEquipment(null, item.id); - else compare = core.compareEquipment(item.id, core.getEquip(equipType)); - // --- 变化值... - for (const name in core.status.hero) { - if (typeof core.status.hero[name] != 'number') continue; - let nowValue = core.getRealStatus(name); - let newValue = Math.floor((core.getStatus(name) + (compare.value[name] || 0)) * - (core.getBuff(name) * 100 + (compare.percentage[name] || 0)) / 100); - if (nowValue === newValue) continue; - const color = newValue > nowValue ? '#00FF00' : '#FF0000'; - const [nowValueStr, newValueStr] = [nowValue, newValue].map(value => core.formatBigNumber(value)); - str += "\n" + core.getStatusLabel(name) + " " + nowValueStr + "->\r[" + color + "]" + newValueStr + "\r"; - } - return str; - } - } - - /** 取消设置快捷键的按钮 */ - class SetNullBtn extends ButtonBase { - draw() { - const [x, y, w, h] = [this.x, this.y, this.w, this.h]; - core.strokeRect(this.ctx, x, y, w, h, 'red', 2); - core.drawLine(this.ctx, x, y, x + w, y + h, 'red', 2); - } - } - - // 为当前道具设定一个快捷键 - class HotkeySelect extends MenuBase { - constructor(itemId, x, y, w, h, zIndex) { - super('hotkeySelect', ['ondown', 'keyDown'], x, y, w, h, zIndex); - this.itemId = itemId; - /** @type {number | null} null代表当前道具没有快捷键 */ - this.hotkeyNum = HotkeySelect.getHotkeyNum(this.itemId); - } - - drawContent() { - const ctx = this.createCanvas(); - const [x, y, w, h] = [this.x, this.y, this.w, this.h]; - core.fillRect(ctx, 3, 3, w - 6, h - 6, ' #A8CABA'); - core.strokeRect(ctx, 0, 0, w, h, ' #004B23', 3); - core.setTextAlign(ctx, 'center'); - core.setTextBaseline(ctx, 'alphabetic'); - core.fillText(ctx, '为当前道具选择一个快捷键', this.w / 2, 40, 'black', '20px Verdana'); - core.fillText(ctx, '无自定义设置时样板的默认快捷键', this.w / 2, 60, 'gray', '16px Verdana'); - core.fillText(ctx, '(如123分别对应破炸飞)不可在此设置', this.w / 2, 80, 'gray', '16px Verdana'); - // 无自定义设置时样板的默认快捷键(如123分别对应破炸飞),不可在此设置 - // 绘制指向当前快捷键的监听 - if (this.hotkeyNum != null) { - const btn = this.btnMap.get('btn' + this.hotkeyNum); - if (btn) { - core.fillPolygon(ctx, [ - [btn.x + 12, btn.y + btn.h + 10], - [btn.x + btn.w - 12, btn.y + btn.h + 10], - [btn.x + btn.w / 2, btn.y + btn.h + 2] - ], 'black'); - } - } - super.drawContent(); - } - /* @__PURE__ */ - static getHotkeyNum(itemId) { - for (let i = 1; i <= 9; i++) { - const currHotkey = core.getLocalStorage('hotkey' + i, null); - if (currHotkey === itemId) { - return i; - } - } - return null; - } - - deleteHotkey() { - if (this.hotkeyNum != null) core.setLocalStorage('hotkey' + this.hotkeyNum, null); - } - - setHotkey(num) { - this.deleteHotkey(); - core.setLocalStorage('hotkey' + num, this.itemId); - this.hotkeyNum = Number(num); - } - - clear() { - super.clear(); - const { back, itemInv, equipSlots, itemInfo } = UI; - [back, itemInv, equipSlots, itemInfo].forEach(menu => { - if (menu.onDraw) menu.beginListen(); // 注意本来就没在绘制的则不监听 - }); - UI.itemInfo.drawContent(); // 快捷键图标会发生变化 - } - } - - /** @param {string} itemId */ - function hotkeySelectFactory(itemId) { - const hotkeySelect = new HotkeySelect(itemId, 60, 100, 296, 206, 141); // 应当比物品背包的选择光标大,遮盖住前者 - const setHotkeyNum = function (i) { - const num = this.key.replace('btn', ''); - hotkeySelect.setHotkey(num); - hotkeySelect.clear(); - UI.itemInfo.drawContent(); - } - - const [btnSize, btnInterval] = [32, 20]; - const leftMargin = hotkeySelect.w / 2 - 2.5 * btnSize - 2 * btnInterval; - const style = { strokeStyle: 'none', fillStyle: 'none' }; - - for (let i = 0; i < 9; i++) { - const num = i + 1; - const row = (i <= 4) ? 1 : 2; - let btn; - if (row === 1) btn = new IconBtn(leftMargin + i * (btnSize + btnInterval), 100, btnSize, btnSize, 'btn' + num, style); - else btn = new IconBtn(leftMargin + (i - 5) * (btnSize + btnInterval), 150, btnSize, btnSize, 'btn' + num, style); - hotkeySelect.registerBtn('btn' + num, btn, () => setHotkeyNum.call(btn, i)); - } - const setNullBtn = new SetNullBtn(leftMargin + 4 * (btnSize + btnInterval), 150, btnSize, btnSize); - hotkeySelect.registerBtn('setNullBtn', setNullBtn, () => { - hotkeySelect.deleteHotkey(); - hotkeySelect.clear(); - UI.itemInfo.drawContent(); - }); - const exitBtn = new ExitBtn(274, 5, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 }); - hotkeySelect.registerBtn('exitBtn', exitBtn, () => hotkeySelect.clear()); - return hotkeySelect; - } - - // #endregion - // #region 核心功能函数和全局变量 - function getItemClsName(item) { - const itemClsName = { - "constants": "永久道具", - "tools": "消耗道具", - } - if (item == null) return "未知"; - if (item.cls == "equips") { - if (typeof item.equip.type == "string") return item.equip.type; - const type = core.getEquipTypeById(item.id); - return core.status.globalAttribute.equipName[type]; - } else return itemClsName[item.cls] || item.cls; - } - - function clearItemBoxCache() { - UI._itemId = ''; - UI.selectType = 'toolBox'; - [UI._toolInv, UI._equipInv, UI._equipSlots].forEach(menu => { - if (menu) { - menu.index = 0; - menu.page = 0; - } - }); - } // 每次存读档,及进行录像回放时调用,清空之前选中的道具信息 - this.clearItemBoxCache = clearItemBoxCache; - - // 以下是本插件范围内的全局变量 - - const UI = { - /** 当前打开的是道具页还是装备页 - * @type {'all'|'equips'} - **/ - type: 'all', - /** @type {'toolBox'|'equipBox'} 当前选中了哪一个子界面(切装面板/物品栏) */ - selectType: 'toolBox', - _itemId: '', - /** @type {string} 当前选中的物品ID,变化时自动触发右侧栏的信息变化 */ - get itemId() { - return this._itemId; - }, - set itemId(value) { - if (this._itemId !== value) { - this._itemId = value; - this.itemInfo.drawContent(); - } - }, - /** @type {undefined|ItemBoxBack} 物品页面的背景 */ - _back: undefined, - /** @type {undefined|ToolInventory} 道具背包 */ - _toolInv: undefined, - /** @type {undefined|EquipInventory} 装备背包 */ - _equipInv: undefined, - /** @type {undefined|ItemInfoBoard} 右侧显示选中物品详细信息的页面 */ - _itemInfo: undefined, - /** @type {undefined|EquipSlots} 显示已穿戴装备的面板 */ - _equipSlots: undefined, - /** 是否监听onMove事件 */ - isNoHover: core.getLocalStorage('itemBoxNoHover'), - /** 物品页面的背景 */ - get back() { - if (!this._back) { - this._back = new ItemBoxBack(); - const switchModeBtn = new SwitchBtn(385, 5, 24, 24, { strokeStyle: ' #8B4513', fillStyle: ' #D2691E' }); - const exitBtn = new ExitBtn(385, 385, 24, 24); - this._back.registerBtns([['exitBtn', exitBtn, UI.exit.bind(UI)], - ['switchModeBtn', switchModeBtn, UI.switchType.bind(UI)]]); - } - return this._back; - }, - /** 道具背包 */ - get toolInv() { - if (!this._toolInv) { - this._toolInv = new ToolInventory(0, 0, 240, 416, 137, 11); - const pgDown = new ArrowBtn(20, 375, 20, 20, 'left'); - const pgUp = new ArrowBtn(215, 375, 20, 20, 'right'); - this.toolInv.registerBtns([ - ['pgDownBtn', pgDown, () => UI.toolInv.pageDown()], - ['pgUpBtn', pgUp, () => UI.toolInv.pageUp()] - ]); - const allBtn = new ClassifyBtn(20, 10, 44, 24, "全部", "all"), - toolsBtn = new ClassifyBtn(80, 10, 44, 24, "消耗", "tools"), - constantsBtn = new ClassifyBtn(140, 10, 44, 24, "永久", "constants"); - const noHoverBtn = new NoHoverBtn(210, 10, 24, 24, null, { crossline1: this.isNoHover }); - const noHover = () => { - UI.isNoHover = !UI.isNoHover; - core.setLocalStorage('itemBoxNoHover', UI.isNoHover); - noHoverBtn.config.crossline1 = UI.isNoHover; - UI._toolInv?.drawButtonContent(); - } - this._toolInv.registerBtns([ - ['allBtn', allBtn, () => this.toolInv.classify('all')], - ['toolsBtn', toolsBtn, () => this.toolInv.classify('tools')], - ['constantsBtn', constantsBtn, () => this.toolInv.classify('constants')], - ['noHoverBtn', noHoverBtn, noHover] - ]); - if (!core.platform.isPC) noHoverBtn.disable = true; // 非PC端本来就触发不了onmove事件,故隐藏此按钮 - } - return this._toolInv; - }, - /** 装备背包 */ - get equipInv() { - if (!this._equipInv) { - this._equipInv = new EquipInventory('equipInventory', 0, 120, 240, 416, 137, 7); - const pgDown = new ArrowBtn(20, 255, 20, 20, 'left'); - const pgUp = new ArrowBtn(215, 255, 20, 20, 'right');; - this._equipInv.registerBtn('pgDownBtn', pgDown, () => UI.equipInv.pageDown()); - this._equipInv.registerBtn('pgUpBtn', pgUp, () => UI.equipInv.pageUp()); - } - return this._equipInv; - }, - /** 物品背包 */ - get itemInv() { - return (this.type === 'all') ? this.toolInv : this.equipInv; - }, - /** 右侧显示选中物品详细信息的页面 */ - get itemInfo() { - if (!this._itemInfo) { - this._itemInfo = new ItemInfoBoard(240, 0, core.__PIXELS__ - 240, core.__PIXELS__); - const hideBtn = new HideBtn(20, 380, 46, 24); - this._itemInfo.registerBtn('hideBtn', hideBtn, () => { - this.hideItem(UI.itemId); - this.itemInv.updateItemList(); - this.itemInv.setIndex(this.itemInv.index); - this.itemInv.drawContent(); - }); - const markBtn = new MarkBtn(80, 380, 46, 24); - this._itemInfo.registerBtn('markBtn', markBtn, () => { - this.markItem(UI.itemId); - this.itemInv.updateItemList(); - this.itemInv.setIndex(this.itemInv.index); - this.itemInv.drawContent(); - }); - const showHideBtn = new ShowHideBtn(20, 350, 95, 18); - this._itemInfo.registerBtn('showHideBtn', showHideBtn, () => { - this.switchShowHide(); - this.itemInv.updateItemList(); - this.itemInv.setIndex(this.itemInv.index); - this.itemInv.drawContent(); - this.itemInfo.drawContent(); // 这里不论选中物品是否变化,itemInfo必定要重绘,因为按钮图案会变 - }); - const setHotkeyBtn = new IconBtn(145, 60, 24, 24, 'keyboard'); - this.itemInfo.registerBtn('setHotkeyBtn', setHotkeyBtn, () => { - if (!UI.itemId) return; - [UI._back, UI.itemInv, UI._equipSlots, UI._itemInfo].forEach((menu) => { - if (menu) menu.endListen(); - }); - const hotkeySelect = hotkeySelectFactory(UI.itemId); - hotkeySelect.init(); - }); - } - return this._itemInfo; - }, - get equipSlots() { - if (!this._equipSlots) { - this._equipSlots = new EquipSlots(7, 10, 240, 125, 137); - const config = { marginLeft: 4, marginTop: 3, marginRight: 2 }; - const pgDown = new ArrowBtn(0, 56, 14, 14, 'left', config); - const pgUp = new ArrowBtn(224, 56, 14, 14, 'right', config); - this._equipSlots.registerBtn('pgDownBtn', pgDown, () => UI.equipSlots.pageDown()); - this._equipSlots.registerBtn('pgUpBtn', pgUp, () => UI.equipSlots.pageUp()); - } - return this._equipSlots; - }, - /** 清空各个菜单的绘制和监听(不包括解除锁定) */ - clearAll() { - [this._back, this._toolInv, this._equipInv, this._itemInfo, this._equipSlots].forEach((menu) => { - if (menu) menu.clear(); - }); - core.status.event.id = null; - }, - initAll() { - [UI.back, UI.itemInv, UI.itemInfo].forEach((menu) => menu.init()); - if (UI.type === 'equips') UI.equipSlots.init(); - }, - /** 解除锁定,退出道具栏/装备栏 */ - exit() { - this.clearAll(); - setTimeout(core.unlockControl, 0); - }, - /** 在道具栏/装备栏模式中切换 */ - switchType() { - this.type = (this.type === 'all') ? 'equips' : 'all'; - if (this.type === 'all') { - this.equipSlots.clear(); - this.equipInv.clear(); - } else if (this.type === 'equips') { - this.toolInv.clear(); - this.equipSlots.init(); - } - this.itemInv.updateItemList(); - this.back.drawContent(); - this.itemInv.init(); - if (this.type === 'all') { - this.itemInv.setIndex(this.itemInv.index); - } - else if (this.type === 'equips') { - if (UI.selectType === 'toolBox') { - this.itemInv.setIndex(this.itemInv.index); - } - else { - this.equipSlots.setIndex(this.equipSlots.index); - } - } - }, - /** 隐藏 | 取消隐藏当前选中的物品 */ - hideItem(itemId) { - if (!itemId) return; - let hideInfo = core.getFlag('hideInfo', {}); - if (hideInfo.hasOwnProperty(itemId)) { - hideInfo[itemId] = !hideInfo[itemId]; - } else { - hideInfo[itemId] = !core.material.items[itemId].hideInToolbox; - } - core.setFlag('hideInfo', hideInfo); - }, - /** 置顶 | 取消置顶当前选中的物品 */ - markItem(itemId) { - let markedItems = core.getFlag('markedItems', []); - if (markedItems.includes(itemId)) markedItems = markedItems.filter((currId) => currId !== itemId); - else markedItems.push(itemId); - core.setFlag('markedItems', markedItems); - }, - /** 切入/切出显示已隐藏物品的模式 */ - switchShowHide() { - core.setFlag('showHideItem', !core.getFlag('showHideItem', false)); - } - } - - // 程序入口,在_drawToolbox 与 _drawEquipbox处调用 - /** @param {'all'|'equips'} currType */ - function drawItemBox(currType) { - UI.clearAll(); - if (UI._toolInv && UI._toolInv?.onDraw && currType === UI.type) return; - core.lockControl(); - UI.type = currType; - UI.itemInv.updateItemList(); - UI.itemInv.setIndex(UI.itemInv.index); - UI.initAll(); - } - // #endregion - }, - "autoChangeEquip": function () { - // 调用方法:在合适的位置调用函数figureEquip即可,例如在脚本编辑-按键处理加入case 89: core.plugin.figureEquip(); break; - // 即按Y键进入切装模式 - - let compareMode = false; - let equipStatus = []; - let equipIncluded; - - ////// 请在[]中填好不参与换装的装备孔的序号。 - // 例如,0号,4号装备孔不参与换装,则 ignoreList 应设为[0,4] - // 所有装备孔都参与换装,则 ignoreList 应设为[] - let ignoreList = []; - - ////// 请在{}中根据装备的穿脱事件手动填写装备穿脱时要执行的函数,没有则不填。只填写有效的数值变化即可。 - // 例如:{'sword3':{'equip':function(){core.setFlag('mms3',1);},'unequip':function(){core.setFlag('mms3',0);}}} - let equipEvents = {}; - - function compareEquip() { - - return new Promise(function (res) { - - const canvas = 'compareEquip', - width = core._PX_ || core.__PIXELS__, - height = core._PY_ || core.__PIXELS__; - - core.lockControl(); - - function finish() { - compareMode = false; - core.unregisterAction('onclick', 'bestEquip'); - core.deleteCanvas(canvas); - res(void 0); - } - - core.createCanvas(canvas, 0, 0, width, height, 160); - core.setTextAlign(canvas, 'center'); - core.fillText(canvas, '点击选择一个怪物,点击非怪物图块自动退出', width / 2, 20, 'red', '18px Arial'); - - core.registerAction('onclick', 'bestEquip', function (x, y, px, py) { - const cls = core.getBlockCls(x, y), - id = core.getBlockId(x, y); - if (!(cls === 'enemys' || cls === "enemy48")) { - finish(); - return false; - } - figureBestEquip(id, x, y); - core.updateDamage(); - finish(); - }, 100); - }) - } - - function figureBestEquip(id, x, y) { - compareMode = true; - const equipNum = core.status.globalAttribute.equipName.length; // 装备总数量 - - // 角色初始各项数值,用于推算出最优切装后复原初始状态 - const oriEffect = { - 'value': { 'atk': core.status.hero.atk, 'def': core.status.hero.def, 'mdef': core.status.hero.mdef, }, - 'percentage': { 'atk': core.getBuff('atk'), 'def': core.getBuff('def'), 'mdef': core.getBuff('mdef'), }, - 'equipment': core.clone(core.status.hero.equipment), - } - - if (!equipIncluded) equipIncluded = getEquipIncluded(equipNum); - - const equipIncludedNum = equipIncluded.length; - const equipNameList = core.status.globalAttribute.equipName.filter((ele, i) => { return !ignoreList.includes(i); }); - - equipStatus = equipIncluded.map((ele) => core.getEquip(ele)); //当前参与计算的各个装备孔的装备 - const equipOwned = getEquipOwned(equipNum); - let equipList = getEquipList(equipIncludedNum, equipOwned, equipNameList); - - const equipCombination = traverseSetCombinations(equipList); - - const bestCombination = findBestEquipComb(equipCombination, equipOwned, id, x, y); - - ['atk', 'def', 'mdef'].forEach((ele) => { - core.setStatus(ele, oriEffect.value[ele]); - core.setBuff(ele, oriEffect.percentage[ele]); - }); - core.status.hero.equipment = core.clone(oriEffect.equipment); - - equipBestComb(bestCombination, equipIncluded, equipNameList); - } - - // 返回一个包含所有参与切装计算的装备孔的序号的数组。 - // 例如,0,2,4号装备孔参与切装计算,则本函数返回[0,2,4] - function getEquipIncluded(equipNum) { - let equipIncluded = []; - for (let i = 0; i < equipNum; i++) { - if (!ignoreList.includes(i)) equipIncluded.push(i); - } - return equipIncluded; - } - - function getEquipOwned(equipNum) { - // equipOwned:当前拥有的所有装备的数量 - // 形如{sword1: 2, sword2: 1} - let equipOwned = core.clone(core.status.hero.items.equips); - for (let i = 0; i < equipNum; i++) { - if (ignoreList.includes(i)) continue; - const currEquip = core.getEquip(i); - if (currEquip !== null) - if (equipOwned.hasOwnProperty(currEquip)) equipOwned[currEquip]++; - else equipOwned[currEquip] = 1; - } - return equipOwned; - } - - // 生成切装列表,为一个二维数组 - function getEquipList(equipNum, equipOwned, equipNameList) { - // equipNameList:计入切装计算的装备格子的名称列表,可重复 - // 形如['武器', '武器', '盾牌'] - let equipList = Array(equipNameList.length).fill(void 0).map(() => new Set([null])); - - //对每个装备孔展开 - for (let i = 0, l = equipNameList.length; i < l; i++) { - for (let j in equipOwned) { - let equipType = core.material.items[j].equip.type; - switch (typeof equipType) { - case 'number': - for (let k = 0, l = equipOwned[j]; k < l; k++) { - equipList[equipIncluded.indexOf(equipType)].add(j); - } - break; - case 'string': - if (equipType === equipNameList[i]) - for (let k = 0, l = equipOwned[j]; k < l; k++) { equipList[i].add(j); } - break; - } - } - } - return equipList; - } - - function traverseSetCombinations(arr) { - const result = []; - const currentCombination = []; - - function backtrack(index) { - if (index === arr.length) { - result.push([...currentCombination]); - return; - } - const currentSet = Array.from(arr[index]); - for (let value of currentSet) { - currentCombination[index] = value; - backtrack(index + 1); - } - } - backtrack(0); - return result; - } - - function getEleCount(ele, arr) { - let count = 0; - for (let i = 0, l = arr.length; i < l; i++) { - if (arr[i] === ele) count++; - } - return count; - } - - function hasEnoughEquip(currComb, equipOwned) { - for (let i in equipOwned) { - const equipNeed = getEleCount(i, currComb); - if (equipOwned[i] < equipNeed) return false; - } - return true; - } - - // 按照给定的列表aimStatus,形如['sword1','sword2',null,'sword1'],修改equipStatus进行模拟切装 - function simulateEquip(equipStatus, aimStatus) { - equipIncluded.forEach((ele, i) => { core.status.hero.equipment[ele] = aimStatus[i]; }) - for (let i = 0, l = equipStatus.length; i < l; i++) { - if (equipStatus[i] !== aimStatus[i]) { - if (aimStatus[i] === null) { - const unequipId = equipStatus[i]; - if (equipEvents.hasOwnProperty(unequipId) && - equipEvents[unequipId].hasOwnProperty('unequip')) - equipEvents[unequipId].unequip(); - core.items._loadEquipEffect(null, unequipId); - } else { - const equipId = aimStatus[i]; - if (equipEvents.hasOwnProperty(equipId) && - equipEvents[equipId].hasOwnProperty('equip')) - equipEvents[equipId].equip(); - core.items._loadEquipEffect(equipId, equipStatus[i]); - } - equipStatus[i] = aimStatus[i]; - } - } - } - - function findBestEquipComb(equipCombination, equipOwned, id, x, y) { - let minDamage = core.getDamage(id, x, y), - bestCombination = core.clone(equipStatus); - for (let i = 0, l = equipCombination.length; i < l; i++) { - const currComb = equipCombination[i]; - if (!hasEnoughEquip(currComb, equipOwned)) continue; - simulateEquip(equipStatus, currComb); - let damage = core.getDamage(id, x, y); - if (damage !== null && (minDamage === null || damage < minDamage)) { - minDamage = damage; - bestCombination = core.clone(equipStatus); - } - } - return bestCombination; - } - - function equipBestComb(bestCombination, equipIncluded, equipNameList) { - /** @type {Set} */ - const duplicatedName = new Set([]), - name = core.status.globalAttribute.equipName; - - // 脱下重复装备 - equipNameList.forEach((ele) => { - if (getEleCount(ele, equipNameList) > 1) duplicatedName.add(ele); - }) - equipIncluded.forEach((ele) => { - if (duplicatedName.has(name[ele]) && core.getEquip(ele) !== null) { - core.unloadEquip(ele); - core.status.route.push("unEquip:" + ele.toString()); - } - }) - - for (let i = 0, l = bestCombination.length; i < l; i++) { - const currEquip = bestCombination[i], - pos = equipIncluded[i]; - if (core.getEquip(pos) === currEquip) continue; - else if (currEquip === null) { - core.unloadEquip(pos); - core.status.route.push("unEquip:" + pos.toString()); - } else { - core.loadEquip(currEquip); - core.status.route.push("equip:" + currEquip.toString()); - } - } - } - - this.figureEquip = function () { - compareEquip().then(function (confirm) { - core.unlockControl(); - }) - } - }, - "customizableToolBar": function () { - // 自定义工具栏显示项 - // 本插件需要配合main.js, control.js等的修改 - // 新的逻辑如下: - // 函数_updateStatusBar_setToolboxIcon在updateStatusBar中调用,仅用于切换录像replay/pause,和计算几个图标的透明度 - // setToolbarButton根据输入的类型重新向状态栏填入元素 - // resize添加一个只刷新工具栏的模式,此外,resize不调用setToolbarButton,相反,setToolbarButton后调用resize - // 以下地方调用setToolbarButton: hard的点击事件,回放录像的进入/退出事件 - // 要注意一点, floor是不能作为toolBar元素的,statuBar具有同id元素,样式会相互冲突 复制了一个外形一样的图标叫view,作为浏览地图按钮 - - /** - * PC 默认6个键且不需要切入数字键的模式 最多9个键 手机 默认9个键 最多9个键(手机理论上可以塞10个键,但是懒得判定了) - * normal:普通模式 num:按下数字键切换到的模式 replay:录像模式 opacity:透明度 - */ - const defaultConfig = { - normal: { - vertical: ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback'], - horizontal: ['book', 'fly', 'toolbox', 'save', 'load', 'settings'] - }, - num: { - vertical: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt'], - horizontal: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt'] - }, - replay: { - vertical: ['play', 'stop', 'rewind', 'book', 'view', 'speedDown', 'speedUp', 'save'], - horizontal: ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'save'] - }, - hide: { - vertical: [], horizontal: [], - } - }; - - function isVertical() { - return core.domStyle.isVertical || core.flags.extendToolbar; - } - - function getToolBarConfig(type) { - const currObj = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); - return isVertical() ? currObj.vertical : currObj.horizontal; - } - this.getToolBarConfig = getToolBarConfig; - - /** - * - * @param {string} type - * @param {number} index - * @param {string} value - * @example core.setToolBarConfig('normal', 3, null) - */ - function setToolBarConfig(type, index, value) { - const allToolType = ['normal', 'num', 'replay']; - const allTools = ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback', 'undoRollback', - 'btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btn9', 'btnAlt', - 'equipbox', 'floor', 'play', 'rewind', 'speedDown', 'speedUp', 'stop', 'single', 'view']; - if (!allToolType.includes(type) || index < 0 || index > 8) { - core.drawFailTip('请选中工具栏的一个合法位置作为目标点!'); - return; - } - if (value !== 'delete' && !allTools.includes(value)) { - core.drawFailTip('请选中一个图标作为替换目标!'); - return; - } - const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); - const key = isVertical() ? 'vertical' : 'horizontal'; - if (type === 'replay' && - (['fly', 'shop', 'load', 'settings', 'btnAlt', 'rollback', 'undoRollback', 'btnAlt'].includes(value))) { - core.drawFailTip('该按钮不允许放在录像模式下!'); - return; - } // 录像模式下的按键处理有一套专门的逻辑,在_sys_onkeyUp_replay,实际上并不能读取自动档 - if (type !== 'replay' && ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'single'].includes(value)) { - core.drawFailTip('该按钮不允许放在非录像模式下!'); - return; - } - - if (value === 'delete') toorBarConfig[key].splice(index, 1); - else { - if (index > toorBarConfig[key].length) { - core.drawFailTip('按钮中间不能有空白!'); - return; - } - const oldIndex = toorBarConfig[key].indexOf(value); - if (oldIndex !== -1) { // 如果目标位置有图标,两者交换 - if (toorBarConfig[key][index] === value) return; - const aimTool = toorBarConfig[key][index]; - toorBarConfig[key][index] = value; - toorBarConfig[key][oldIndex] = aimTool; - } - else toorBarConfig[key][index] = value; - } - core.setLocalStorage('toorBarConfig' + type, toorBarConfig); - setToolbarButton(core.domStyle.toolbarBtn); - } - this.setToolBarConfig = setToolBarConfig; - - function resetToolBarConfig() { - for (let type in defaultConfig) { - if (!defaultConfig.hasOwnProperty(type)) return; - const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); - core.setLocalStorage('toorBarConfig' + type, toorBarConfig); - } - } - this.resetToolBarConfig = resetToolBarConfig; - - /** - * - * @param {'normal'|'num'|'replay'|'hide'} type - */ - function setToolbarButton(type) { - if (main.replayChecking) return; // 录像验证必须干掉此函数 因为它操作了DOM - const currList = getToolBarConfig(type); - if (!currList) return; - const fragment = document.createDocumentFragment(); - for (let i = 0, l = currList.length; i < l; i++) { - const iconId = currList[i]; - const currEle = core.statusBar.image[iconId]; - if (!currEle) continue; - currEle.style.display = 'block'; - fragment.appendChild(currEle); - } - if (type !== "hide") { - core.domStyle.toolbarBtn = type; - } - core.domStyle.toolsCount = currList.length; - fragment.appendChild(core.dom.hard); // 难度一定会显示 因为难度所在位置要用于切换常规模式和数字模式 难度的尺寸是动态决定的 - core.dom.toolBar.innerHTML = ''; - core.dom.toolBar.appendChild(fragment); - core.control.resize('tools'); // 在这里计算难度的尺寸 - } - this.setToolbarButton = setToolbarButton; - }, - "setting": function () { - // 自绘设置界面 - // 请保持本插件在所有插件的最下方 - - // #region 复写 - // 复写resize,保证屏幕变化时此画布表现正常 - const originResize = core.control.resize; - core.control.resize = function () { - originResize.apply(core.control, arguments); - /** @type {SettingBack} */ - const settingMenu = core.plugin.settingMenu; - if (settingMenu && settingMenu.onDraw) { - settingMenu.drawContent(); - settingMenu.pageList[settingMenu.currPage].drawContent(); - } - } - // #endregion - - /** - * @typedef {{ - * getName:()=>string, - * effect:()=>void, - * text:string, - * replay?:boolean, - * draw?:((ctx:string)=>void) - * }} Setting - */ - function invertFlag(name) { - core.setFlag(name, !core.hasFlag(name)); - } - - function invertLocalStorage(name) { - const value = core.getLocalStorage(name, false); - core.setLocalStorage(name, !value); - } - - // #endregion - // #region 设置的具体内容,与相应的录像注册 - // #endregion - // #region 按钮类 - - const { ButtonBase, RoundBtn, IconBtn, ExitBtn, - ArrowBtn, MenuBase, Pagination, KeyCodeEnum } = core.plugin.uiBase; - - class TextButton extends ButtonBase { - constructor(x, y, w, h, text) { - super(x, y, w, h); - this.text = text; - this.draw = () => { - if (this.disable) return; - core.ui.fillText(this.ctx, this.text, - this.x + this.w / 2, this.y + this.h / 2 + 5, 'white', '16px Verdana'); - } - } - } - // #endregion - // #region 菜单类 - - // #region 单个设置菜单的基类 - /** 单个设置按钮的基类 */ - class SettingButton extends ButtonBase { - constructor(x, y, w, h, ...drawArgs) { - super(x, y, w, h); - /** @type {Setting} 将在registerBtn时赋予此项 */ // @ts-ignore - this.setting; - this.drawArgs = drawArgs; - } - - draw() { - const ctx = this.ctx; - const [x, y, w, h] = [this.x, this.y, this.w, this.h]; - const setting = this.setting; - // 取消注释下面这一句将显示所有按钮的判定框 - // core.strokeRect(ctx, this.x, this.y, this.w, this.h, 'yellow'); - core.setTextAlign(ctx, 'start'); - core.ui.fillText(ctx, setting.getName(), x, y + h / 2 + 5, 'white', '16px Verdana'); - const drawFunc = setting.draw; - if (this.status === 'selected') { - core.drawUIEventSelector(0, "winskin.png", this.x, this.y, this.w, this.h, 137); - } - if (drawFunc) drawFunc.apply(this, [ctx, ...this.drawArgs]); - } - } - - // @todo 悬浮按钮不改变时不触发事件 这个优化有没有做 - - /** 单个设置菜单的基类 */ - class SettingOnePage extends MenuBase { - constructor(name, settingData) { - super(name, ['ondown', 'onmove', 'keyDown']); - this.text = ''; - /** @type {string | undefined}*/ - this.selectedPos; - /** @type {SettingButton | undefined}*/ - this.selectedBtn; - /** @type {{[x:string]:Setting}}*/ - this.settingData = settingData; - } - - keyDownEvent(keyCode) { - let x, y; - const changePos = (newPos) => { - if (this.btnMap.has(newPos)) { - const button = this.btnMap.get(newPos); - this.focus(button, newPos); - } - } - if ([KeyCodeEnum.Left, KeyCodeEnum.Right, KeyCodeEnum.Up, KeyCodeEnum.Down].includes(keyCode)) { - if (!this.selectedBtn || !this.selectedPos) { - const button = this.btnMap.get('1,1'); - if (button) this.focus(button, '1,1'); - return; - } else { - [x, y] = this.selectedPos.split(',').map((x) => parseInt(x)); - if (keyCode === KeyCodeEnum.Left) x--; - if (keyCode === KeyCodeEnum.Up) y--; - if (keyCode === KeyCodeEnum.Right) x++; - if (keyCode === KeyCodeEnum.Down) y++; - let newPos = x + ',' + y; - - // 逻辑:左右,查找不到对应坐标就不动。 - // 上下,查找不到对应坐标,只要该列存在第一个元素,就会移到该列。 - - if (keyCode === KeyCodeEnum.Left || keyCode === KeyCodeEnum.Right) { - changePos(newPos); - } - if (keyCode === KeyCodeEnum.Up || keyCode === KeyCodeEnum.Down) { - if (this.btnMap.has(newPos)) { - const button = this.btnMap.get(newPos); - this.focus(button, newPos); - } else { - newPos = '1,' + y; - changePos(newPos); - } - } - } - } else { - switch (keyCode) { - case KeyCodeEnum.Enter: // Enter/Space - case KeyCodeEnum.SpaceBar: - if (this.selectedBtn) this.selectedBtn.ondown(-1, -1, -1, -1); - break; - } - } - } - - /** - * @param {SettingButton} btn - * @param {string} key 对应事件的索引 - **/ - execEffect(btn, key, ...eventArgs) { - const setting = btn.setting; - setting.effect.apply(this, eventArgs); - if (setting.replay) { - let actionString = 'cSet:' + key; - if (eventArgs && eventArgs.length > 0) { - actionString += ':' + eventArgs.map(arg => encodeURIComponent(arg)).join(':'); - } - core.status.route.push(actionString); - } - this.drawContent(); - } - - /** 聚焦到指定的按钮并重绘菜单 */ - focus(button, pos) { - if (this.selectedBtn === button) return; // 如果当前按钮已被选中,则不做任何操作 - this.selectedPos = pos; - this.selectedBtn = button; - this.btnMap.forEach((currBtn) => { - currBtn.status = (currBtn === button) ? 'selected' : 'none'; - }); - if (button instanceof SettingButton) { - this.text = button.setting.text || ''; - } - this.drawContent(); - } - - drawContent(ctx) { - if (!ctx) ctx = this.name; - super.drawContent(); - if (this.text && this.text.length > 0) { - core.ui.drawTextContent(ctx, this.text, { - left: 30, - top: 78, - bold: false, - color: "white", - align: "left", - fontSize: 14, - maxWidth: 350 - }); - } - } - - clear() { - core.clearUIEventSelector(0); // 光标的绘制在按钮中进行 - super.clear(); - } - - /** - * @override - * @param {string} pos - * @param {string} key - * @param {SettingButton} btn - * @param {...(string|number)} eventArgs - */ - registerSettingBtn(pos, key, btn, ...eventArgs) { - super.registerBtn(pos, btn, { - 'ondown': () => { - if (this.selectedBtn === btn) this.execEffect(btn, key, ...eventArgs); - else this.focus(btn, pos); - }, - 'onmove': () => { - this.focus(btn, pos); - } - }); - btn.setting = this.settingData[key]; - } - - registerSettingBtns(arr) { - arr.forEach(ele => { - const [key, btn, event, ...eventArgs] = ele; - this.registerSettingBtn(key, btn, event, ...eventArgs); - }); - } - } - // #endregion - - // #region 功能菜单 - function checkSkipFuncs() { - const skipText = core.getLocalStorage('skipText'); - // 此函数用于检测是否处在录像模式下,是则将跳过所有非必要对话 - core.events.__action_checkReplaying = skipText ? function () { - core.doAction(); - return true; - }.bind(core.events) : events.prototype.__action_checkReplaying; - - const skipPeform = core.getLocalStorage('skipPeform'); - - const instantMove = function (fromX, fromY, aimX, aimY, keep, callback) { - const [_block, blockInfo] = core.maps._getAndRemoveBlock(fromX, fromY); - if (keep) { - core.setBlock(blockInfo.number, aimX, aimY); - core.showBlock(aimX, aimY); - } - if (callback) callback(); - } - - core.maps.jumpBlock = skipPeform ? function (sx, sy, ex, ey, time, keep, callback) { - return instantMove(sx, sy, ex, ey, keep, callback); - }.bind(core.maps) : maps.prototype.jumpBlock; - - core.maps.moveBlock = skipPeform ? function (x, y, steps, time, keep, callback) { - maps.prototype.moveBlock(x, y, steps, 1, keep, callback); - }.bind(core.maps) : maps.prototype.moveBlock; - - core.maps.drawAnimate = skipPeform ? function (name, x, y, alignWindow, callback) { - if (callback) callback(); - return -1; - }.bind(core.maps) : maps.prototype.drawAnimate; - - core.maps.drawHeroAnimate = skipPeform ? function (name, callback) { - if (callback) callback(); - return -1; - }.bind(core.maps) : maps.prototype.drawHeroAnimate; - - core.events.jumpHero = skipPeform ? function (ex, ey, time, callback) { - const { x: sx, y: sy } = core.status.hero.loc; - if (ex == null) ex = sx; - if (ey == null) ey = sy; - core.setHeroLoc('x', ex); - core.setHeroLoc('y', ey); - core.clearMap('hero'); - core.drawHero(); - if (callback) callback(); - }.bind(core.events) : events.prototype.jumpHero; - - core.events.vibrate = skipPeform ? function (direction, time, speed, power, callback) { - if (callback) callback(); - return; - }.bind(core.events) : events.prototype.vibrate; - - core.events._action_sleep = skipPeform ? function (data, x, y, prefix) { - core.doAction(); - }.bind(core.events) : events.prototype._action_sleep; - } - this.checkSkipFuncs = checkSkipFuncs; - /** @type {{[x:string]:Setting}} */ - const gamePlaySetting = { - autoGet: { - getName: () => '自动拾取:' + (core.getFlag('autoGet', false) ? '开' : '关'), - effect: () => invertFlag('autoGet'), - text: '每走一步,自动拾取当前层可获得的道具。', - replay: true, - }, - autoBattle: { - getName: () => '自动清怪:' + (core.getFlag('autoBattle', false) ? '开' : '关'), - effect: () => invertFlag('autoBattle'), - text: '每走一步,自动和当前层可到达位置伤害为0的敌人战斗。对部分特殊敌人无效。', - replay: true, - }, - noRouting_HP: { - getName: () => '', - effect: () => invertFlag('noRouting_HP'), - text: '自动寻路时绕过加血物品。同时自动拾取也将忽略这类物品。', - replay: true, - /** @this {SettingButton} */ - draw: function (ctx) { - core.setAlpha(ctx, core.hasFlag('noRouting_HP') ? 1 : 0.3); - core.drawIcon(ctx, 'redPotion', this.x, this.y, this.w, this.h); - core.setAlpha(ctx, 1); - } - }, - noRouting_MDEF: { - getName: () => '', - effect: () => invertFlag('noRouting_MDEF'), - text: '自动寻路时绕过加护盾物品。同时自动拾取也将忽略这类物品。', - replay: true, - /** @this {SettingButton} */ - draw: function (ctx) { - core.setAlpha(ctx, core.hasFlag('noRouting_MDEF') ? 1 : 0.3); - core.drawIcon(ctx, 'greenGem', this.x, this.y, this.w, this.h); - core.setAlpha(ctx, 1); - } - }, - noRouting_ATK: { - getName: () => '', - effect: () => invertFlag('noRouting_ATK'), - text: '自动寻路时绕过加攻物品。同时自动拾取也将忽略这类物品。', - replay: true, - /** @this {SettingButton} */ - draw: function (ctx) { - core.setAlpha(ctx, core.hasFlag('noRouting_ATK') ? 1 : 0.3); - core.drawIcon(ctx, 'redGem', this.x, this.y, this.w, this.h); - core.setAlpha(ctx, 1); - } - }, - noRouting_DEF: { - getName: () => '', - effect: () => invertFlag('noRouting_DEF'), - text: '自动寻路时绕过加防物品。同时自动拾取也将忽略这类物品。', - replay: true, - /** @this {SettingButton} */ - draw: function (ctx) { - core.setAlpha(ctx, core.hasFlag('noRouting_DEF') ? 1 : 0.3); - core.drawIcon(ctx, 'blueGem', this.x, this.y, this.w, this.h); - core.setAlpha(ctx, 1); - } - }, - clickMove: { - getName: () => '单击瞬移:' + (core.hasFlag('__noClickMove__') ? '关' : '开'), - effect: () => invertFlag('__noClickMove__'), - text: '系统设置。单击即可触发瞬移。', - replay: true, - }, - moveSpeedDown: { - getName: () => ' < 步时:' + core.values.moveSpeed, - effect: () => core.actions._clickSwitchs_action_moveSpeed(-10), - text: '缩短步时。', - replay: false, // 录像中不可录入任何DOM操作 - }, - moveSpeedUp: { - getName: () => ' > ', - effect: () => core.actions._clickSwitchs_action_moveSpeed(10), - text: '增大步时。', - replay: false, - }, - floorChangeTimeDown: { - getName: () => ' < 转场:' + core.values.floorChangeTime, - effect: () => core.actions._clickSwitchs_action_floorChangeTime(-100), - text: '缩短转场时间。', - replay: false, // 录像中不可录入任何DOM操作 - }, - floorChangeTimeUp: { - getName: () => ' > ', - effect: () => core.actions._clickSwitchs_action_floorChangeTime(100), - text: '增大转场时间。', - replay: false, - }, - skipText: { - getName: () => '跳过剧情:' + (core.getLocalStorage('skipText', false) ? '开' : '关'), - effect: () => { - invertLocalStorage('skipText'); - checkSkipFuncs(); - }, - text: '跳过全部文字对话。初见请勿开启此选项。', - replay: false, - }, - skipPeform: { - getName: () => '跳过演出:' + (core.getLocalStorage('skipPerform', false) ? '开' : '关'), - effect: () => { - invertLocalStorage('skipPerform'); - checkSkipFuncs(); - }, - text: '加速等待、播放动画等常见演出效果。', - replay: false, - }, - comment: { - getName: () => '在线留言:' + (core.hasFlag('comment') ? '开' : '关'), - effect: () => { - if (core.hasFlag('comment')) { - core.setFlag('comment', false); - core.plugin.clearCommentSign(); - } else { - core.setFlag('comment', true); - core.plugin.drawCommentSign(); - } - }, - text: '在地图上显示玩家的在线留言。', - replay: true, - }, - autoHideFloor: { - getName: () => '自动隐藏楼层:' + (core.hasFlag('autoHideFloor') ? '开' : '关'), - effect: () => { - invertFlag('autoHideFloor'); - }, - text: '一个楼层已无物品、敌人、NPC(不含已忽略图块),且无未到达楼层传送口时可被自动隐藏,仅在首次进入此状态时在楼传界面触发。', - replay: true, - }, - autoSaveBeforeUseItem: { - getName: () => '使用物品自动保存:' + (core.getLocalStorage('autoSaveBeforeUseItem') ? '开' : '关'), - effect: () => { - invertLocalStorage('autoSaveBeforeUseItem'); - }, - text: '使用消耗类物品(含破炸飞跳)前,以及即将走入滑冰、触发捕捉时自动存档。', - replay: false, - }, - autoSaveBeforePickItem: { - getName: () => '拾取物品自动保存:' + (core.getLocalStorage('autoSaveBeforePickItem') ? '开' : '关'), - effect: () => { - invertLocalStorage('autoSaveBeforePickItem'); - }, - text: '拾取地上物品前自动存档。', - replay: false, - }, - } - - class GamePlay extends SettingOnePage { - constructor() { - super('gamePlay', gamePlaySetting); - } - - drawContent() { - const ctx = this.createCanvas(); - core.fillText(ctx, '-- 自动 --', 40, 175, ' #FFE4B5', '18px Verdana'); - core.fillText(ctx, '-- 瞬移 --', 40, 225, ' #FFE4B5', '18px Verdana'); - core.fillText(ctx, '绕开', 220, 250, 'white', '16px Verdana'); - core.fillText(ctx, '-- 杂项 --', 40, 275, ' #FFE4B5', '18px Verdana'); - super.drawContent(ctx); - } - } - - function gamePlayFactory() { - const gamePlayMenu = new GamePlay(); - gamePlayMenu.registerSettingBtns([ - ['1,1', 'autoGet', new SettingButton(40, 180, 150, 30)], - ['2,1', 'autoBattle', new SettingButton(220, 180, 150, 30)], - ['1,2', 'clickMove', new SettingButton(40, 230, 150, 30)], - ['2,2', 'noRouting_HP', new SettingButton(260, 234, 24, 24)], - ['3,2', 'noRouting_MDEF', new SettingButton(290, 234, 24, 24)], - ['4,2', 'noRouting_ATK', new SettingButton(320, 234, 24, 24)], - ['5,2', 'noRouting_DEF', new SettingButton(350, 234, 24, 24)], - ['1,3', 'moveSpeedDown', new SettingButton(40, 280, 25, 25)], - ['2,3', 'moveSpeedUp', new SettingButton(140, 280, 25, 25)], - ['3,3', 'floorChangeTimeDown', new SettingButton(220, 280, 25, 25)], - ['4,3', 'floorChangeTimeUp', new SettingButton(340, 280, 25, 25)], - ['1,4', 'skipText', new SettingButton(40, 305, 150, 25)], - ['2,4', 'skipPeform', new SettingButton(220, 305, 150, 25)], - ['1,5', 'comment', new SettingButton(40, 330, 150, 25)], - ['2,5', 'autoHideFloor', new SettingButton(220, 330, 150, 25)], - ['1,6', 'autoSaveBeforeUseItem', new SettingButton(40, 355, 150, 25)], - ['2,6', 'autoSaveBeforePickItem', new SettingButton(220, 355, 150, 25)], - ]); - return gamePlayMenu; - } - // #endregion - // #region 视图菜单 - /** @type {{[x:string]:Setting}} */ - const gameViewSetting = { - itemDetail: { - getName: () => '物品显示数据:' + (core.hasFlag('itemDetail') ? '开' : '关'), - effect: () => { - invertFlag('itemDetail'); - core.control.updateStatusBar(); // 更新地图上物品的属性显示 - }, - text: '在地图上显示即捡即用道具和装备增加的属性值。', - replay: true, - }, - zoomIn: { - getName: () => ' < 放缩:' + Math.max(core.domStyle.scale, 1) + 'x', - effect: () => core.actions._clickSwitchs_display_setSize(-1), - text: '放缩', - replay: false, - }, - zoomOut: { - getName: () => ' > ', - effect: () => core.actions._clickSwitchs_display_setSize(1), - text: '放缩。', - replay: false, - }, - HDCanvas: { - getName: () => '高清画面:' + (core.flags.enableHDCanvas ? '开' : '关'), - effect: core.actions._clickSwitchs_display_enableHDCanvas, - text: '高清画面。本功能开关后刷新游戏才能看到效果。', - replay: false, - }, - enableEnemyPoint: { - getName: () => '定点怪显:' + (core.flags.enableEnemyPoint ? '开' : '关'), - effect: core.actions._clickSwitchs_display_enableEnemyPoint, - text: '怪物属性定点显示功能,即属性不同的怪物会在怪物手册单列。', - replay: false, - }, - displayEnemyDamage: { - getName: () => '怪物显伤:' + (core.flags.displayEnemyDamage ? '开' : '关'), - effect: core.actions._clickSwitchs_display_enemyDamage, - text: '地图上显示怪物的伤害。', - replay: false, - }, - displayCritical: { - getName: () => '临界显伤:' + (core.flags.displayCritical ? '开' : '关'), - effect: core.actions._clickSwitchs_display_critical, - text: '地图上显示怪物的临界值', - replay: false, - }, - displayExtraDamage: { - getName: () => '领域显伤:' + (core.flags.displayExtraDamage ? '开' : '关'), - effect: core.actions._clickSwitchs_display_extraDamage, - text: '领域显伤', - replay: false, - }, - extraDamageType: { - getName: () => '领域模式:' + (core.flags.extraDamageType == 2 ? '[最简]' : core.flags.extraDamageType == 1 ? '[半透明]' : '[完整]'), - effect: core.actions._clickSwitchs_display_extraDamageType, - text: '是否显示不可通行地块的领域伤害。', - replay: false, - }, - autoScale: { - getName: () => '自动放缩:' + (core.getLocalStorage('autoScale') ? '开' : '关'), - effect: () => { - core.setLocalStorage('autoScale', !core.getLocalStorage('autoScale')); - }, - text: '自动放缩。', - replay: false, - }, - bgm: { - getName: () => '音乐:' + (core.musicStatus.bgmStatus ? '开' : '关'), - effect: core.actions._clickSwitchs_sounds_bgm, - text: '播放背景音乐。', - replay: false, - }, - se: { - getName: () => '音效:' + (core.musicStatus.soundStatus ? '开' : '关'), - effect: core.actions._clickSwitchs_sounds_se, - text: '播放音效。', - replay: false, - }, - decreaseVolume: { - getName: () => " < 音量:" + Math.round(Math.sqrt(100 * core.musicStatus.userVolume)), - effect: () => core.actions._clickSwitchs_sounds_userVolume(-1), - text: '减小音量。', - replay: false, - }, - increaseVolume: { - getName: () => ' > ', - effect: () => core.actions._clickSwitchs_sounds_userVolume(1), - text: '增大音量。', - replay: false, - }, - }; - - class GameView extends SettingOnePage { - constructor() { - super('gameView', gameViewSetting); - } - - drawContent() { - const ctx = this.createCanvas(); - core.fillText(ctx, '-- 显示 --', 40, 175, ' #FFE4B5', '18px Verdana'); - core.fillText(ctx, '-- 音效 --', 40, 320, ' #FFE4B5', '18px Verdana'); - super.drawContent(ctx); - } - } - - function gameViewFactory() { - const gameViewMenu = new GameView(); - const advanceDisplayBtn = new RoundBtn(300, 280, 32, 18, "高级", { - font: '12px Verdana', fillStyle: "SlateGray", radius: 1, fontStyle: "yellow", - strokeStyle: "black" - }); - gameViewMenu.registerBtn('openAdvanceDisplay', advanceDisplayBtn, () => { - const settingMenu = core.plugin.settingMenu; - if (settingMenu) { - settingMenu.endListen(); - // 隐藏大菜单的按钮是为了避免视觉上的干扰 - settingMenu.btnMap.forEach(btn => { btn.disable = true }); - settingMenu.pageList[settingMenu.currPage].endListen(); - settingMenu.drawContent(); - } - core.ui.clearUIEventSelector(0); - const advanceDisplayMenu = advanceDisplayFactory(); - advanceDisplayMenu.init(); - }); - gameViewMenu.registerSettingBtns([ - ['1,1', 'itemDetail', new SettingButton(40, 180, 150, 25)], - ['1,2', 'displayEnemyDamage', new SettingButton(40, 205, 150, 25)], - ['1,3', 'displayExtraDamage', new SettingButton(40, 230, 150, 25)], - ['1,4', 'autoScale', new SettingButton(40, 255, 150, 25)], - ['1,5', 'HDCanvas', new SettingButton(40, 280, 150, 25)], - ['1,6', 'bgm', new SettingButton(40, 325, 150, 25)], - ['1,7', 'decreaseVolume', new SettingButton(40, 350, 25, 25)], - ['2,7', 'increaseVolume', new SettingButton(140, 350, 25, 25)], - ['2,1', 'displayEnemyDamage', new SettingButton(220, 180, 150, 25)], - ['2,2', 'displayCritical', new SettingButton(220, 205, 150, 25)], - ['2,3', 'extraDamageType', new SettingButton(220, 230, 150, 25)], - ['2,4', 'zoomIn', new SettingButton(220, 255, 25, 25)], - ['3,4', 'zoomOut', new SettingButton(330, 255, 25, 25)], - ['2,6', 'se', new SettingButton(220, 325, 150, 25)], - ]); - return gameViewMenu; - } - // #endregion - // #region 快捷键菜单 - /** @type {{[x:string]:Setting}} */ - const keySetting = { - leftHand: { - getName: () => '左手模式:' + (core.flags.leftHandPrefer ? '开' : '关'), - effect: () => { - core.flags.leftHandPrefer = !core.flags.leftHandPrefer; - core.setLocalStorage('leftHandPrefer', core.flags.leftHandPrefer); - }, - text: '系统设置。左手模式下WASD将用于移动角色,IJKL对应于原始的WASD进行存读档等操作。', - replay: true, - }, - setHotKey: { - getName: () => '', - effect: /** @this {GameView} */ function (num) { - core.utils.myprompt('输入物品名。名称(例如:破墙镐)或英文ID(例如:pickaxe)均可。', '', (value) => { - const itemInfo = core.material.items; - const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value); - if (aimItem) { - if (['constants', 'tools'].includes(aimItem.cls)) { - for (let i = 1; i <= 9; i++) { - if (i !== num && core.getLocalStorage('hotkey' + i) === aimItem.id) { - core.setLocalStorage('hotkey' + i, null); - } - } - core.setLocalStorage('hotkey' + num, aimItem.id); - this.drawContent(); - } else { - core.drawFailTip('错误:该类型的物品不支持快捷使用!'); - } - } else { - core.drawFailTip('错误:找不到该名称的物品!'); - } - }, () => { }); - }, - text: '给选定的数字键绑定一个可快捷使用的物品。', - replay: false, - draw: /** @this {SettingButton} */ function (ctx, num) { - const item = core.getLocalStorage('hotkey' + num, null); - let icon, itemName; - if (item && core.material.items.hasOwnProperty(item)) { - icon = item; - itemName = core.material.items[item].name; - } else { - const itemMap = { - '1': { icon: 'pickaxe', itemName: '破墙镐' }, - '2': { icon: 'bomb', itemName: '炸弹' }, - '3': { icon: 'centerFly', itemName: '中心飞' }, - '4': { itemName: '杂物' }, - '5': { itemName: '回退一步' }, - '6': { itemName: '撤销回退' }, - '7': { itemName: '轻按' } - }; - if (num >= 1 && num <= 9) { - ({ icon, itemName = '无' } = itemMap[num] || {}); - } - } - const keyIcon = 'btn' + num; - core.drawIcon(ctx, keyIcon, this.x, this.y, 16, 16); - const hasItem = core.material.items.hasOwnProperty(icon); - if (hasItem) core.drawIcon(ctx, icon, this.x + 20, this.y, 16, 16); - core.setTextAlign(ctx, 'left'); - core.setTextBaseline(ctx, 'alphabetic'); - core.fillText(ctx, itemName || '无', this.x + (hasItem ? 40 : 20), this.y + 14, 'white', '16px Verdana'); - } - }, - clearHotKeys: { - getName: () => '', - effect: /** @this {GameView} */ function () { - for (let i = 1; i <= 9; i++) { - core.setLocalStorage('hotkey' + i, null); - } - this.drawContent(); - core.drawSuccessTip('快捷键已重置到默认状态。'); - }, - text: '重置本页面所有快捷键到默认状态。', - replay: false, - draw: /** @this {SettingButton} */ function (ctx) { - core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#D3D3D3'); - core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#888888'); - core.fillText(ctx, '重置', this.x + 5, this.y + this.h / 2 + 5, '#333333', '16px Verdana'); - } - } - }; - class KeyMenu extends SettingOnePage { - constructor() { - super('key', keySetting); - } - - drawContent() { - const ctx = this.createCanvas(); - core.fillText(ctx, '-- 快捷键 --', 40, 205, ' #FFE4B5', '18px Verdana'); - core.fillText(ctx, "注意⚠️更推荐在背包", 160, 190, "rgb(255,255,51)", '12px Verdana'); - core.fillText(ctx, "中点击右上角", 287, 190, "rgb(255,255,51)", '12px Verdana'); - core.fillText(ctx, "图标设置单物品的快捷键", 160, 210, "rgb(255,255,51)", '12px Verdana'); - core.drawIcon(ctx, "toolbox", 272, 178, 16, 16); - core.drawIcon(ctx, "keyboard", 359, 178, 16, 16); - super.drawContent(ctx); - } - } - - function keyMenuFactory() { - const keyMenu = new KeyMenu(); - keyMenu.registerSettingBtns([ - ['1,1', 'leftHand', new SettingButton(40, 160, 150, 25)], - ['1,2', 'setHotKey', new SettingButton(40, 220, 150, 25, '1'), 1], - ['2,2', 'setHotKey', new SettingButton(220, 220, 150, 25, '2'), 2], - ['1,3', 'setHotKey', new SettingButton(40, 250, 150, 25, '3'), 3], - ['2,3', 'setHotKey', new SettingButton(220, 250, 150, 25, '4'), 4], - ['1,4', 'setHotKey', new SettingButton(40, 280, 150, 25, '5'), 5], - ['2,4', 'setHotKey', new SettingButton(220, 280, 150, 25, '6'), 6], - ['1,5', 'setHotKey', new SettingButton(40, 310, 150, 25, '7'), 7], - ['1,6', 'clearHotKeys', new SettingButton(300, 350, 42, 25)], - ]); - return keyMenu; - } - // #endregion - // #region 自定义工具栏界面 - /** 自定义工具栏界面 */ - class ToolBtn extends ButtonBase { - constructor(x, y, w, h, icon, text, config) { - super(x, y, w, h); - /** @type {ToolBarConfigPage} */ // @ts-ignore - this.menu; - this.icon = icon; // 特殊icon:delete 用于删除图标 - /** @todo 这里需要重构 */ - this.text = text; - this.config = config || {}; - this.ondown = () => { - this.menu.selectedTool = this.icon; - this.menu.text = this.text; - this.menu.drawContent(); - }; - } - - draw() { - const ctx = this.ctx; - const { strokeStyle = 'white', fillStyle = 'white', selectedStyle = 'gold' } = this.config; - core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, (this.menu.selectedTool === this.icon) ? selectedStyle : strokeStyle); - if (this.icon === 'delete') { - core.drawLine(ctx, this.x + 2, this.y + 2, this.x + this.w - 2, this.y + this.h - 2, 'red', 2); - core.drawLine(ctx, this.x + 2, this.y + this.h - 2, this.x + this.w - 2, this.y + 2, 'red', 2); - } else core.drawIcon(ctx, this.icon, this.x, this.y, this.w, this.h); - } - } - - class ToolBarBtn extends ButtonBase { - constructor(x, y, size, type, length, text) { - super(x, y, length * size, size); - /** @type {ToolBarConfigPage} */ // @ts-ignore - this.menu; - this.type = type; - this.length = length; - /** @todo 这里需要重构 */ - this.text = text; - this.ondown = (x, y, px, py) => { - const squareSize = this.h; - const index = Math.floor((px - this.x) / squareSize); - this.menu.type = type; - this.menu.index = index; - this.menu.drawContent(); - } - } - - draw() { - const ctx = this.ctx; - const squareSize = this.h; - const type = this.type; - const toolBarConfig = core.plugin.getToolBarConfig(type); - for (let i = 0; i < this.length; i++) { - const style = (this.menu.type === type && this.menu.index === i) ? 'gold' : 'white'; - core.strokeRoundRect(ctx, this.x + squareSize * i + i, this.y, squareSize, squareSize, 3, style); - core.drawIcon(ctx, toolBarConfig[i], this.x + squareSize * i + i, this.y, squareSize, squareSize); - } - } - } - - class ToolBarConfigPage extends MenuBase { - constructor() { - super('toolBarConfig', ['ondown']); - /** 当前选中的图标 */ - this.selectedTool = 'none'; - /** 当前选中了哪个类型的工具栏 */ - this.type = 'none'; - /** 当前选中工具栏哪个位置(1-9) */ - this.index = -1; - this.text = ''; - } - - drawContent() { - const ctx = this.createCanvas(); - core.setTextAlign(ctx, 'left'); - core.fillText(ctx, '常规', 40, 175, ' #FFE4B5', '16px Verdana'); - core.fillText(ctx, '数字', 40, 205, ' #FFE4B5', '16px Verdana'); - core.fillText(ctx, '录像', 40, 235, ' #FFE4B5', '16px Verdana'); - core.fillText(ctx, '可选按钮', 40, 265, ' #FFE4B5', '16px Verdana'); - if (this.text && this.text.length > 0) { - core.ui.drawTextContent(ctx, this.text, { - left: 30, - top: 78, - bold: false, - color: "white", - align: "left", - fontSize: 14, - maxWidth: 350 - }); - } - super.drawContent(); - } - - setToolBarConfig() { - core.setToolBarConfig(this.type, this.index, this.selectedTool); - this.drawContent(); - } - } - - function toolBarConfigFactory() { - // 名字不能叫toolBar 画布toolBar被系统占了 - const toolBarMenu = new ToolBarConfigPage(); - const changeToolBarBtn = new RoundBtn(320, 158, 42, 24, '执行', -1); - changeToolBarBtn.ondown = function () { - core.setToolBarConfig(this.menu.type, this.menu.index, this.menu.selectedTool); - this.menu.drawContent(); - }.bind(changeToolBarBtn); - toolBarMenu.registerBtns([ - ['1,1', new ToolBarBtn(80, 158, 24, 'normal', 9, '常规模式下显示在工具栏中的图标。')], - ['1,2', changeToolBarBtn, () => toolBarMenu.setToolBarConfig()], - ['2,1', new ToolBarBtn(80, 188, 24, 'num', 9, '数字模式下显示在工具栏中的图标。')], - ['3,1', new ToolBarBtn(80, 218, 24, 'replay', 9, '录像模式下显示在工具栏中的图标。')], - ['5,1', new ToolBtn(40, 275, 24, 24, 'book', '打开怪物手册')], - ['5,2', new ToolBtn(70, 275, 24, 24, 'fly', '进行楼层传送')], - ['5,3', new ToolBtn(100, 275, 24, 24, 'toolbox', '打开物品背包')], - ['5,4', new ToolBtn(130, 275, 24, 24, 'equipbox', '打开装备背包')], - ['5,5', new ToolBtn(160, 275, 24, 24, 'keyboard', '打开虚拟键盘')], - ['5,6', new ToolBtn(190, 275, 24, 24, 'shop', '打开快捷商店')], - ['5,7', new ToolBtn(220, 275, 24, 24, 'save', '存档')], - ['5,8', new ToolBtn(250, 275, 24, 24, 'load', '读档')], - ['5,9', new ToolBtn(280, 275, 24, 24, 'settings', '打开系统设置')], - ['5,10', new ToolBtn(310, 275, 24, 24, 'rollback', '读取自动存档')], - ['5,11', new ToolBtn(340, 275, 24, 24, 'undoRollback', '取消读取自动存档')], - ['6,1', new ToolBtn(40, 305, 24, 24, 'btn1', '数字键1')], - ['6,2', new ToolBtn(70, 305, 24, 24, 'btn2', '数字键2')], - ['6,3', new ToolBtn(100, 305, 24, 24, 'btn3', '数字键3')], - ['6,4', new ToolBtn(130, 305, 24, 24, 'btn4', '数字键4')], - ['6,5', new ToolBtn(160, 305, 24, 24, 'btn5', '数字键5')], - ['6,6', new ToolBtn(190, 305, 24, 24, 'btn6', '数字键6')], - ['6,7', new ToolBtn(220, 305, 24, 24, 'btn7', '数字键7')], - ['6,8', new ToolBtn(250, 305, 24, 24, 'btn8', '数字键8')], - ['6,9', new ToolBtn(280, 305, 24, 24, 'btn9', '数字键9')], - ['6,10', new ToolBtn(310, 305, 24, 24, 'btnAlt', '开关Alt模式(需要配合数字键使用)')], - ['7,1', new ToolBtn(40, 335, 24, 24, 'play', '播放/暂停录像')], - ['7,2', new ToolBtn(70, 335, 24, 24, 'stop', '停止播放录像')], - ['7,3', new ToolBtn(100, 335, 24, 24, 'rewind', '录像模式下回退')], - ['7,4', new ToolBtn(130, 335, 24, 24, 'speedDown', '减速录像(最低0.2倍)')], - ['7,5', new ToolBtn(160, 335, 24, 24, 'speedUp', '加速录像(最高24倍)')], - ['7,6', new ToolBtn(190, 335, 24, 24, 'single', '单步播放录像')], - ['7,7', new ToolBtn(220, 335, 24, 24, 'view', '浏览地图')], - ['7,8', new ToolBtn(250, 335, 24, 24, 'delete', '删除已有图标')], - ]); - return toolBarMenu; - } - // #endregion - // #region 控制台界面 - /** @type {{[x:string]:Setting}} */ - const consoleSetting = { - debug_wallHacking: { - getName: () => ' 穿墙:' + (core.hasFlag('debug_wallHacking') ? '开' : '关'), - effect: () => { - core.setFlag('debug', true); - invertFlag('debug_wallHacking'); - }, - text: '开启时将始终穿墙并无视各种事件,无论是否按下Ctrl。', - replay: false, - }, - debug_statusName: { - getName: () => core.getFlag('debug_statusName', '??'), - /** @this {ConsoleMenu} */ - effect: function () { - const dictionary = { - '体力': 'hp', - '血量': 'hp', - '生命': 'hp', - '血': 'hp', - '体力上限': 'hpmax', - '血量上限': 'hpmax', - '生命上限': 'hpmax', - '血限': 'hpmax', - '攻击': 'atk', - '攻': 'atk', - '防御': 'def', - '防': 'def', - '魔防': 'mdef', - '护盾': 'mdef', - 'mf': 'mdef', - '金币': 'money', - '金钱': 'money', - '钱': 'money', - '经验': 'exp', - '魔力': 'mana', - '魔': 'mana', - '蓝': 'mana', - }; - core.utils.myprompt('输入要修改的属性名称', '', (value) => { - const heroStatus = core.status.hero; - if (dictionary.hasOwnProperty(value)) { - value = dictionary[value]; - } - if (heroStatus && heroStatus.hasOwnProperty(value) && ['hp', 'hpmax', 'atk', 'def', 'mdef', 'money', 'exp', 'mana', 'manamax'].includes(value)) { - core.setFlag('debug_statusName', value); - this.menu.drawContent(); - } else { - core.drawFailTip('错误:不合法的名称!'); - } - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_statusValue: { - getName: () => { - let value = core.getFlag('debug_statusValue', '??'); - if (typeof value === 'number') return core.formatBigNumber(value, 5); - else return value; - }, - /** @this {ConsoleMenu} */ - effect: function () { - core.utils.myprompt('输入要修改到的值', null, (input) => { - const value = parseInt(input); - if (!Number.isNaN(value)) { - core.setFlag('debug_statusValue', value); - this.menu.drawContent(); - } else { - core.drawFailTip('错误:不合法的值!'); - } - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_setStatus: { - getName: () => '', - /** @this {ConsoleMenu} */ - effect: function () { - const name = core.getFlag('debug_statusName'), - value = core.getFlag('debug_statusValue'); - if (!(name && core.status.hero && core.status.hero.hasOwnProperty(name))) { - core.drawFailTip('错误:不合法的名称!'); - return; - } - if (!Number.isInteger(value)) { - core.drawFailTip('错误:不合法的值!'); - return; - } - core.setFlag('debug', true); - core.setStatus(name, value); - core.updateStatusBar(); - core.drawSuccessTip('设置成功!'); - }, - text: '将角色状态设为相应值。', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); - core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); - core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); - }, - }, - debug_itemName: { - getName: () => core.getFlag('debug_itemName', '??'), - /** @this {ConsoleMenu} */ - effect: function () { - core.utils.myprompt('输入要修改的物品名称', null, (value) => { - const itemInfo = core.material.items; - if (itemInfo) { - const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value); - if (aimItem) { - core.setFlag('debug_itemName', aimItem.id); - this.menu.drawContent(); - return; - } - } - core.drawFailTip('错误:不合法的名称!'); - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_itemValue: { - getName: () => core.getFlag('debug_itemValue', '??'), - /** @this {ConsoleMenu} */ - effect: function () { - core.setFlag('debug', true); - core.utils.myprompt('输入要修改到的值', null, (input) => { - const value = parseInt(input); - if (!Number.isNaN(value)) { - core.setFlag('debug_itemValue', value); - this.menu.drawContent(); - } else { - core.drawFailTip('错误:不合法的值!'); - } - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_setItem: { - getName: () => '', - /** @this {ConsoleMenu} */ - effect: function () { - const name = core.getFlag('debug_itemName'), - value = core.getFlag('debug_itemValue'); - const itemInfo = core.material.items; - - if (name && itemInfo) { - let itemExist = Object.values(itemInfo).some((item) => item.id === name); - if (!itemExist) { - core.drawFailTip('错误:不合法的名称!'); - return; - } - } else { - core.drawFailTip('错误:不合法的名称!'); - return; - } - - if (!Number.isInteger(value)) { - core.drawFailTip('错误:不合法的值!'); - return; - } - - core.setFlag('debug', true); - core.setItem(name, value); - core.updateStatusBar(); - core.drawSuccessTip('设置成功!'); - }, - text: '将道具数设为相应值。', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); - core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); - core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); - }, - }, - debug_flagName: { - getName: () => core.getFlag('debug_flagName', '??'), - /** @this {ConsoleMenu} */ - effect: function () { - core.setFlag('debug', true); - core.utils.myprompt('输入要修改的变量名。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => { - if (!value.startsWith('debug')) { - core.setFlag('debug_flagName', value); - this.menu.drawContent(); - } else { - core.drawFailTip('错误:不合法的名称!'); - } - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_flagValue: { - getName: () => core.getFlag('debug_flagValue', '??'), - /** @this {ConsoleMenu} */ - effect: function () { - core.setFlag('debug', true); - core.utils.myprompt('输入要修改到的值。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => { - let newValue; - try { - newValue = JSON.parse(value.trim()); - } catch { - core.drawFailTip('错误:不合法的值,无法解析!'); - return; - } - core.setFlag('debug_flagValue', newValue); - this.menu.drawContent(); - }, () => { }); - }, - text: '', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); - }, - }, - debug_setFlag: { - getName: () => '', - /** @this {ConsoleMenu} */ - effect: function () { - const name = core.getFlag('debug_flagName'), - value = core.getFlag('debug_flagValue'); - if (!name) { - core.drawFailTip('错误:不合法的变量名称!'); - return; - } - core.setFlag('debug', true); - core.setFlag(name, value); - core.updateStatusBar(); - core.drawSuccessTip('设置成功!'); - }, - text: '将变量设为相应值。', - replay: false, - /** @this {SettingButton} */ - draw: function (ctx) { - core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); - core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); - core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); - }, - }, - }; - - class ConsoleMenu extends SettingOnePage { - constructor() { - super('console', consoleSetting); - this.menu = this; - } - - drawContent() { - const ctx = this.createCanvas(); - const consoleWarnText = - "本页面的功能仅供调试用。使用后相应存档将变红,录像不能通过,且无法提交。请读档到普通存档后正常游玩方可提交。"; - core.setTextAlign(ctx, 'left'); - core.setTextBaseline(ctx, 'alphabetic'); - core.fillText(ctx, "本页面的功能仅供调试用。使用后相应存档将变红,录像", 30, 170, " #FFC0CB", '14px Verdana'); - core.fillText(ctx, "不能通过,且无法提交。请读档到普通存档后正常游玩方", 30, 190, " #FFC0CB", '14px Verdana'); - core.fillText(ctx, "可提交。", 30, 210, " #FFC0CB", '14px Verdana'); - core.fillText(ctx, "属性", 45, 264, 'white', '16px Verdana'); - core.fillText(ctx, "设为", 170, 264, 'white', '16px Verdana'); - core.fillText(ctx, "物品", 45, 290, 'white', '16px Verdana'); - core.fillText(ctx, "数量设为", 170, 290, 'white', '16px Verdana'); - core.fillText(ctx, "变量", 45, 316, 'white', '16px Verdana'); - core.fillText(ctx, "设为", 170, 316, 'white', '16px Verdana'); - super.drawContent(ctx); - } - } - - function consoleMenuFactory() { - const consoleMenu = new ConsoleMenu(); - consoleMenu.registerSettingBtns([ - ['1,1', 'debug_wallHacking', new SettingButton(40, 220, 150, 25)], - ['1,2', 'debug_statusName', new SettingButton(80, 250, 80, 20)], - ['2,2', 'debug_statusValue', new SettingButton(210, 250, 80, 20)], - ['3,2', 'debug_setStatus', new SettingButton(340, 250, 40, 20)], - ['1,3', 'debug_itemName', new SettingButton(80, 276, 80, 20)], - ['2,3', 'debug_itemValue', new SettingButton(240, 276, 80, 20)], - ['3,3', 'debug_setItem', new SettingButton(340, 276, 40, 20)], - ['1,4', 'debug_flagName', new SettingButton(80, 302, 80, 20)], - ['2,4', 'debug_flagValue', new SettingButton(210, 302, 80, 20)], - ['3,4', 'debug_setFlag', new SettingButton(340, 302, 40, 20)], - ]); - return consoleMenu; - } - // #endregion - // #region 敌人信息显示调节界面 - const infoNameMap = { - 'damage': '伤害', 'critical': '临界', 'hp': core.control.getStatusLabel('hp'), - 'atk': core.control.getStatusLabel('atk'), 'def': core.control.getStatusLabel('def'), - 'money': core.control.getStatusLabel('money'), 'exp': core.control.getStatusLabel('exp'), - 'criticalDamage': '临界减伤', 'defDamage': '1防减伤', 'special': '特殊属性', - 'notBomb': '不可炸', 'vampireDiff': '吸血额外受伤' - } - - class DisplayInfoBtn extends RoundBtn { - constructor(x, y, w, h, pos, index) { - super(x, y, w, h); - this.pos = pos; - this.index = index; - } - - draw() { - super.draw(); - const isEmpty = !this.infoName; - const ctx = this.ctx; - const { x, y, w, h } = this; - const [x0, y0, r, offset] = [x + w + 15, y + h / 2, h / 2 - 2, 2]; - core.fillCircle(ctx, x0, y0, r, isEmpty ? 'lime' : 'red'); - core.drawLine(ctx, x0 - r + offset, y0, x0 + r - offset, y0, 'white', 2); - if (isEmpty) core.drawLine(ctx, x0, y0 - r + offset, x0, y0 + r - offset, 'white', 2); - } - - inRange(px, py) { // 实际的点击判定区是右边的圆形 - const { x, y, w, h } = this; - return px >= x + w + 15 - h / 2 && px <= x + w + 15 + h / 2 && py >= y && py <= y + h; - } - } - - class SpecialIconBtn extends RoundBtn { } - - class AdvanceDisplayMenu extends MenuBase { - constructor() { - super('advanceDisplay', ['ondown']); - this.selectedBtn = null; - this.getData(); - this.getAllSpecials(); - this.getSpecialIconData(); - this.specialIconPage = 0; - } - - drawContent() { - const ctx = this.createCanvas(); - core.setTextAlign(ctx, 'center'); - core.setTextBaseline(ctx, 'alphabetic'); - core.fillRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 320, 5, "rgba(50,50,50,1)"); - core.fillText(ctx, "设定敌人左下角显示的数据", 110, 90, 'white', '14px Verdana'); - core.fillText(ctx, "设定敌人右上角显示的数据", 110, 190, 'white', '14px Verdana'); - core.fillText(ctx, "设定特殊属性的代表字符", 110, 290, 'white', '14px Verdana'); - this.getData(); - this.setInfoName(); - this.getSpecialIconData(); - super.drawContent(ctx); - } - - clear() { - super.clear(); - const settingMenu = core.plugin.settingMenu; - if (settingMenu) { - settingMenu.beginListen(); - settingMenu.pageList[settingMenu.currPage].beginListen(); - } - } - - getData() { - const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} }; - this.data = core.getLocalStorage('displayData', defaultData); - } - - setData(pos, index, infoName) { - this.data[pos][index] = infoName; - core.setLocalStorage('displayData', this.data); - this.setInfoName(); - } - - setInfoName() { - this.btnMap.forEach(btn => { - if (!(btn instanceof DisplayInfoBtn)) return; - const pos = btn.pos; - const index = btn.index; - const infoName = this.data[pos][index]; - if (infoName) { - btn.infoName = infoName; - const name = infoNameMap[infoName]; - btn.text = name; - if (name.length >= 5) { - if (!btn.config) btn.config = {}; - btn.config.font = "12px Verdana"; - } - } - else { - btn.infoName = null; - btn.text = ''; - } - }); - } - - getAllSpecials() { // allSpecials是所有用到了的特殊属性的数组 - let allSpecialsSet = new Set(); - Object.values(core.material.enemys).forEach(enemy => { - const special = core.utils.parseSpecial(enemy.special); - allSpecialsSet = new Set([...allSpecialsSet, ...special]); - }) - this.allSpecials = [...allSpecialsSet].sort((a, b) => a - b); - } - - getSpecialIconData() { - const specialData = core.getLocalStorage('specialIconData', {}); - this.specialIconData = specialData; - } - - getCurrSpecialIconList() { - return this.allSpecials.slice(this.specialIconPage * 6, - this.specialIconPage * 6 + 6); - } - - /** 更新每个按钮对应的文本,在翻页和修改内容时需要手动调用 */ - setSpecialIconBtnText() { - const specialIconList = this.getCurrSpecialIconList(); - for (let i = 0; i <= 5; i++) { - const key = 's' + (i + 1); - const btn = this.btnMap.get(key); - const index = this.specialIconPage * 6 + i; - if (index >= this.allSpecials.length) { - btn.text = ''; - } - else { - const specialNum = this.allSpecials[index]; - const specialIndexMap = core.enemys.getSpecialIndexMap(); - const [key, name] = specialIndexMap[specialNum]; - const icon = this.specialIconData[specialNum] || "无"; - btn.text = `${key}:${name} ${icon}`; - } - } - } - - setSpecialIconData(index) { - const specialIconList = this.getCurrSpecialIconList(); - const specialNum = specialIconList[index]; - if (!specialNum) return; - core.utils.myprompt('输入该特殊属性的代表字符', null, (value) => { - if (value.length > 1) value = value[0]; // 最多保留一位字符 - this.specialIconData[specialNum] = value; - core.setLocalStorage('specialIconData', this.specialIconData); - this.setSpecialIconBtnText(); - this.drawContent(); - }); - } - - pageUp() { - if (this.specialIconPage * 6 + 6 < this.allSpecials.length) { - this.specialIconPage++; - this.setSpecialIconBtnText(); - this.drawContent(); - } - } - - pageDown() { - if (this.specialIconPage > 0) { - this.specialIconPage--; - this.setSpecialIconBtnText(); - this.drawContent(); - } - } - } - - function advanceDisplayFactory() { - const advanceDisplayMenu = new AdvanceDisplayMenu(); - const exitBtn = new ExitBtn(370, 80, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 }) - advanceDisplayMenu.registerBtn('exitBtn', exitBtn, () => { - advanceDisplayMenu.clear(); - const settingMenu = core.plugin.settingMenu; - settingMenu.btnMap.forEach(btn => { btn.disable = false }); - settingMenu.drawContent(); - }); - const btn1 = new DisplayInfoBtn(50, 100, 75, 20, 'leftdown', 1), - btn2 = new DisplayInfoBtn(50, 125, 75, 20, 'leftdown', 2), - btn3 = new DisplayInfoBtn(50, 150, 75, 20, 'leftdown', 3), - btn4 = new DisplayInfoBtn(50, 200, 75, 20, 'rightup', 1), - btn5 = new DisplayInfoBtn(50, 225, 75, 20, 'rightup', 2), - btn6 = new DisplayInfoBtn(50, 250, 75, 20, 'rightup', 3); - /** 一行显示几个选项 */ - const ROW_INFONAME = 6; - const infoNameList = Object.keys(infoNameMap); - const l = infoNameList.length; - - const setNewInfo = function (infoName) { - return function () { - const btn = advanceDisplayMenu.selectedBtn; - if (!btn) return; - advanceDisplayMenu.setData(btn.pos, btn.index, infoName); - advanceDisplayMenu.btnMap.forEach((btn, key) => { - if (btn.key.startsWith("temp")) btn.disable = true; - }); - advanceDisplayMenu.selectedBtn = null; - advanceDisplayMenu.drawContent(); - core.control.updateStatusBar(); // 手动刷新一下地图显伤 - } - } - for (let i = 0; i < l; i++) { - const name = infoNameMap[infoNameList[i]]; - const config = { - fillStyle: 'Azure', - font: name.length >= 5 ? "12px Verdana" : "14px Verdana", - } - const x = (i >= ROW_INFONAME) ? 300 : 200; - const y = 100 + 25 * ((i >= ROW_INFONAME) ? (i - ROW_INFONAME) : i); - const tempBtn = new RoundBtn(x, y, 80, 20, name, config); - tempBtn.disable = true; - advanceDisplayMenu.registerBtn('temp' + i, tempBtn, setNewInfo(infoNameList[i])); - } - - const change = function (btn) { - return function () { - if (btn.infoName) { - advanceDisplayMenu.setData(btn.pos, btn.index, null); - } - else { - if (advanceDisplayMenu.selectedBtn && advanceDisplayMenu.selectedBtn === btn) { - // 点击左边刚点过的按钮会收起展开菜单 - advanceDisplayMenu.btnMap.forEach((btn, key) => { - if (btn.key.startsWith("temp")) btn.disable = true; - }); - advanceDisplayMenu.selectedBtn = null; - advanceDisplayMenu.drawContent(); - } else { - advanceDisplayMenu.selectedBtn = btn; - advanceDisplayMenu.btnMap.forEach((btn, key) => { - if (btn.key.startsWith("temp")) btn.disable = false; - }); - } - } - advanceDisplayMenu.drawContent(); - } - } - advanceDisplayMenu.registerBtns([ - ['1', btn1, change(btn1)], ['2', btn2, change(btn2)], ['3', btn3, change(btn3)], - ['4', btn4, change(btn4)], ['5', btn5, change(btn5)], ['6', btn6, change(btn6)], - ]); - const config = { "font": "12px Verdana" }; - advanceDisplayMenu.registerBtn('s1', new SpecialIconBtn(50, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 0)); - advanceDisplayMenu.registerBtn('s2', new SpecialIconBtn(150, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 1)); - advanceDisplayMenu.registerBtn('s3', new SpecialIconBtn(250, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 2)); - advanceDisplayMenu.registerBtn('s4', new SpecialIconBtn(50, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 3)); - advanceDisplayMenu.registerBtn('s5', new SpecialIconBtn(150, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 4)); - advanceDisplayMenu.registerBtn('s6', new SpecialIconBtn(250, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 5)); - advanceDisplayMenu.registerBtn('pageDown', new ArrowBtn(50, 360, 16, 16, 'left'), - advanceDisplayMenu.pageDown.bind(advanceDisplayMenu)); - advanceDisplayMenu.registerBtn('pageUp', new ArrowBtn(320, 360, 16, 16, 'right'), - advanceDisplayMenu.pageUp.bind(advanceDisplayMenu)); - advanceDisplayMenu.setSpecialIconBtnText(); - return advanceDisplayMenu; - } - // #endregion - - class PageChangeBtn extends RoundBtn { - constructor(x, y, w, h, text) { - super(x, y, w, h, text); - /** @type {SettingBack} */ // @ts-ignore - this.menu; - this.ondown = () => this.menu.changePage(this.key); - } - } - - class SettingBack extends Pagination { - constructor(pageList, currPage, name) { - super(pageList, currPage, name, ['ondown', 'keyDown']); - } - - changePage(index) { - this.btnMap.forEach((btn, key) => { - btn.status = (key === index) ? 'selected' : 'none'; - }); - super.changePage(index); - this.drawContent(); - } - - /** @override */ - keyDownEvent(keyCode) { - if (keyCode === KeyCodeEnum.PageDown) this.pageDown(); - else if (keyCode === KeyCodeEnum.PageUp) this.pageUp(); - else if (keyCode === KeyCodeEnum.Esc) this.quit(); - } - - quit() { - this.clear(); - setTimeout(core.unlockControl, 0); // 消抖,防止点击关闭按钮的一瞬间触发瞬移。 - } - - drawSettingBackGround(ctx) { - core.strokeRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "white", 2); - core.fillRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "gray"); - - // 绘制设置说明的文本框 - core.strokeRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 70, 3, "white"); - core.fillRoundRect(ctx, 21, 71, core.__PIXELS__ - 42, 68, 3, " #555555"); - - // 绘制设置的框体 - core.strokeRoundRect(ctx, 20, 150, core.__PIXELS__ - 40, 256, 3, "white"); - core.fillRoundRect(ctx, 21, 151, core.__PIXELS__ - 42, 254, 3, " #999999"); - - core.setTextAlign(ctx, 'center'); - core.ui.fillText(ctx, "设置", core.__PIXELS__ / 2, 25, 'white', '20px Verdana'); - } - - drawContent() { - const ctx = core.createCanvas(this.name, this.x, this.y, this.w, this.h, 136); - this.drawSettingBackGround(ctx); - super.drawContent(); - // this.initOnePage(this.currPage); - } - } - - const allSettings = { - ...gamePlaySetting, - ...gameViewSetting, - ...keySetting, - ...consoleSetting, - }; - // 注册点击SettingButton的行为cSet, 只有replay为真时计入录像 - core.registerReplayAction('cSet', (action) => { - const strArr = action.split(':'); - if (strArr[0] !== 'cSet') return false; - const currSetting = allSettings[strArr[1]]; //@todo bugfix - if (!currSetting || !currSetting.replay || strArr[1].startsWith('debug')) return false; - - let params = strArr.slice(2); - - if (params.length > 0) { - currSetting.effect.apply(currSetting, params); - } else { - currSetting.effect.call(currSetting); - } - - core.status.route.push(action); - core.replay(); - return true; - }); - - this.openSetting = function () { - if (core.isReplaying()) return; - core.lockControl(); - const ctx = 'setting'; - - const gamePlayMenu = gamePlayFactory(); - const gameViewMenu = gameViewFactory(); - const keyMenu = keyMenuFactory(); - const toolBarMenu = toolBarConfigFactory(); - const consoleMenu = consoleMenuFactory(); - - // 在此处添加新的菜单页面 - const settingMenu = new SettingBack([gamePlayMenu, gameViewMenu, keyMenu, toolBarMenu, consoleMenu], 0, ctx); - - // 主页面的按钮列表 - const gamePlayBtn = new PageChangeBtn(32, 40, 46, 24, '功能'), - gameViewBtn = new PageChangeBtn(92, 40, 46, 24, '音画'), - keyBtn = new PageChangeBtn(152, 40, 46, 24, '按键'), - toolBarBtn = new PageChangeBtn(212, 40, 66, 24, '工具栏'), - consoleBtn = new PageChangeBtn(292, 40, 66, 24, '控制台'); - const quit = new TextButton(360, 10, 45, 25, '[退出]'); - - settingMenu.registerBtns([ - [0, gamePlayBtn], - [1, gameViewBtn], - [2, keyBtn], - [3, toolBarBtn], - [4, consoleBtn], - ['quit', quit, () => settingMenu.quit()] - ]); - - // 放缩时重绘整个大menu - core.plugin.settingMenu = settingMenu; - // 设置初始时选中的按键为第一个按键 - gamePlayBtn.status = 'selected'; - - settingMenu.init(); - settingMenu.changePage(0); - } - // @todo 新版存档界面 - }, - "opusAdaptation": function () { - // 将__enable置为false将关闭插件 - let __enable = true; - if (!__enable || main.mode === "editor") return; - const { OggOpusDecoderWebWorker } = window["ogg-opus-decoder"]; - const { OggVorbisDecoderWebWorker } = window["ogg-vorbis-decoder"]; - const { CodecParser } = window.CodecParser; - const { Transition, linear } = core.plugin.animate; - - const audio = new Audio(); - const AudioStatus = { - Playing: 0, - Pausing: 1, - Paused: 2, - Stoping: 3, - Stoped: 4, - }; - const supportMap = new Map(); - const AudioType = { - Mp3: "audio/mpeg", - Wav: 'audio/wav; codecs="1"', - Flac: "audio/flac", - Opus: 'audio/ogg; codecs="opus"', - Ogg: 'audio/ogg; codecs="vorbis"', - Aac: "audio/aac", - }; - /** - * 检查一种音频类型是否能被播放 - * @param type 音频类型 AudioType - */ - function isAudioSupport(type) { - if (supportMap.has(type)) return supportMap.get(type); - else { - const support = audio.canPlayType(type); - const canPlay = support === "maybe" || support === "probably"; - supportMap.set(type, canPlay); - return canPlay; - } - } - - const typeMap = new Map([ - ["ogg", AudioType.Ogg], - ["mp3", AudioType.Mp3], - ["wav", AudioType.Wav], - ["flac", AudioType.Flac], - ["opus", AudioType.Opus], - ["aac", AudioType.Aac], - ]); - - /** - * 根据文件名拓展猜测其类型 - * @param file 文件名 string - */ - function guessTypeByExt(file) { - const ext = /\.[a-zA-Z\d]+$/.exec(file); - if (!ext?.[0]) return ""; - const type = ext[0].slice(1); - return typeMap.get(type.toLocaleLowerCase()) ?? ""; - } - - isAudioSupport(AudioType.Ogg); - isAudioSupport(AudioType.Mp3); - isAudioSupport(AudioType.Wav); - isAudioSupport(AudioType.Flac); - isAudioSupport(AudioType.Opus); - isAudioSupport(AudioType.Aac); - - function isNil(value) { - return value === void 0 || value === null; - } - - function sleep(time) { - return new Promise((res) => setTimeout(res, time)); - } - class AudioEffect { - constructor(ac) { } - /** - * 连接至其他效果器 - * @param target 目标输入 IAudioInput - * @param output 当前效果器输出通道 Number - * @param input 目标效果器的输入通道 Number - */ - connect(target, output, input) { - this.output.connect(target.input, output, input); - } - - /** - * 与其他效果器取消连接 - * @param target 目标输入 IAudioInput - * @param output 当前效果器输出通道 Number - * @param input 目标效果器的输入通道 Number - */ - disconnect(target, output, input) { - if (!target) { - if (!isNil(output)) { - this.output.disconnect(output); - } else { - this.output.disconnect(); - } - } else { - if (!isNil(output)) { - if (!isNil(input)) { - this.output.disconnect(target.input, output, input); - } else { - this.output.disconnect(target.input, output); - } - } else { - this.output.disconnect(target.input); - } - } - } - } - - class StereoEffect extends AudioEffect { - constructor(ac) { - super(ac); - const panner = ac.createPanner(); - this.input = panner; - this.output = panner; - } - - /** - * 设置音频朝向,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 - * @param x 朝向x坐标 Number - * @param y 朝向y坐标 Number - * @param z 朝向z坐标 Number - */ - setOrientation(x, y, z) { - this.output.orientationX.value = x; - this.output.orientationY.value = y; - this.output.orientationZ.value = z; - } - /** - * 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 - * @param x 位置x坐标 Number - * @param y 位置y坐标 Number - * @param z 位置z坐标 Number - */ - setPosition(x, y, z) { - this.output.positionX.value = x; - this.output.positionY.value = y; - this.output.positionZ.value = z; - } - end() { } - - start() { } - } - class VolumeEffect extends AudioEffect { - constructor(ac) { - super(ac); - const gain = ac.createGain(); - this.input = gain; - this.output = gain; - } - - /** - * 设置音量大小 - * @param volume 音量大小 Number - */ - setVolume(volume) { - this.output.gain.value = volume; - } - - /** - * 获取音量大小 Number - */ - getVolume() { - return this.output.gain.value; - } - - end() { } - - start() { } - } - class ChannelVolumeEffect extends AudioEffect { - /** 所有的音量控制节点 */ - - constructor(ac) { - super(ac); - /** 所有的音量控制节点 */ - this.gain = []; - const splitter = ac.createChannelSplitter(); - const merger = ac.createChannelMerger(); - this.output = merger; - this.input = splitter; - for (let i = 0; i < 6; i++) { - const gain = ac.createGain(); - splitter.connect(gain, i); - gain.connect(merger, 0, i); - this.gain.push(gain); - } - } - - /** - * 设置某个声道的音量大小 - * @param channel 要设置的声道,可填0-5 Number - * @param volume 这个声道的音量大小 Number - */ - setVolume(channel, volume) { - if (!this.gain[channel]) return; - this.gain[channel].gain.value = volume; - } - - /** - * 获取某个声道的音量大小,可填0-5 - * @param channel 要获取的声道 Number - */ - getVolume(channel) { - if (!this.gain[channel]) return 0; - return this.gain[channel].gain.value; - } - - end() { } - - start() { } - } - class DelayEffect extends AudioEffect { - constructor(ac) { - super(ac); - - const delay = ac.createDelay(); - this.input = delay; - this.output = delay; - } - - /** - * 设置延迟时长 - * @param delay 延迟时长,单位秒 Number - */ - setDelay(delay) { - this.output.delayTime.value = delay; - } - - /** - * 获取延迟时长 - */ - getDelay() { - return this.output.delayTime.value; - } - - end() { } - - start() { } - } - class EchoEffect extends AudioEffect { - constructor(ac) { - super(ac); - /** 当前增益 */ - this.gain = 0.5; - /** 是否正在播放 */ - this.playing = false; - const delay = ac.createDelay(); - const gain = ac.createGain(); - gain.gain.value = 0.5; - delay.delayTime.value = 0.05; - delay.connect(gain); - gain.connect(delay); - /** 延迟节点 */ - this.delay = delay; - /** 反馈增益节点 */ - this.gainNode = gain; - - this.input = gain; - this.output = gain; - } - - /** - * 设置回声反馈增益大小 - * @param gain 增益大小,范围 0-1,大于等于1的视为0.5,小于0的视为0 Number - */ - setFeedbackGain(gain) { - const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain; - this.gain = resolved; - if (this.playing) this.gainNode.gain.value = resolved; - } - - /** - * 设置回声间隔时长 - * @param delay 回声时长,范围 0.01-Infinity,小于0.01的视为0.01 Number - */ - setEchoDelay(delay) { - const resolved = delay < 0.01 ? 0.01 : delay; - this.delay.delayTime.value = resolved; - } - - /** - * 获取反馈节点增益 - */ - getFeedbackGain() { - return this.gain; - } - - /** - * 获取回声间隔时长 - */ - getEchoDelay() { - return this.delay.delayTime.value; - } - - end() { - this.playing = false; - const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10; - sleep(this.delay.delayTime.value * echoTime).then(() => { - if (!this.playing) this.gainNode.gain.value = 0; - }); - } - - start() { - this.playing = true; - this.gainNode.gain.value = this.gain; - } - } - - class StreamLoader { - constructor(url) { - /** 传输目标 Set */ - this.target = new Set(); - this.loading = false; - } - - /** - * 将加载流传递给字节流读取对象 - * @param reader 字节流读取对象 IStreamReader - */ - pipe(reader) { - if (this.loading) { - console.warn( - "Cannot pipe new StreamReader object when stream is loading." - ); - return; - } - this.target.add(reader); - reader.piped(this); - return this; - } - - async start() { - if (this.loading) return; - this.loading = true; - const response = await window.fetch(this.url); - const stream = response.body; - if (!stream) { - console.error("Cannot get reader when fetching '" + this.url + "'."); - return; - } - // 获取读取器 - this.stream = stream; - const reader = response.body?.getReader(); - const targets = [...this.target]; - - await Promise.all(targets.map((v) => v.start(stream, this, response))); - if (reader && reader.read) { - // 开始流传输 - while (true) { - const { value, done } = await reader.read(); - await Promise.all( - targets.map((v) => v.pump(value, done, response)) - ); - if (done) break; - } - } else { - // 如果不支持流传输 - const buffer = await response.arrayBuffer(); - const data = new Uint8Array(buffer); - await Promise.all(targets.map((v) => v.pump(data, true, response))); - } - - this.loading = false; - targets.forEach((v) => v.end(true)); - } - - cancel(reason) { - if (!this.stream) return; - this.stream.cancel(reason); - this.loading = false; - this.target.forEach((v) => v.end(false, reason)); - } - } - - /** @type {[string, number[]][]} */ - const fileSignatures = [ - [AudioType.Mp3, [0x49, 0x44, 0x33]], - [AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]], - [AudioType.Wav, [0x52, 0x49, 0x46, 0x46]], - [AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]], - [AudioType.Aac, [0xff, 0xf1]], - [AudioType.Aac, [0xff, 0xf9]], - ]; - /** @type {[string, number[]][]} */ - const oggHeaders = [ - [AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]], - ]; - - function checkAudioType(data) { - let audioType = ""; - // 检查头文件获取音频类型,仅检查前256个字节 - const toCheck = data.slice(0, 256); - for (const [type, value] of fileSignatures) { - if (value.every((v, i) => toCheck[i] === v)) { - audioType = type; - break; - } - } - if (audioType === AudioType.Ogg) { - // 如果是ogg的话,进一步判断是不是opus - for (const [key, value] of oggHeaders) { - const has = toCheck.some((_, i) => { - return value.every((v, ii) => toCheck[i + ii] === v); - }); - if (has) { - audioType = key; - break; - } - } - } - - return audioType; - } - class AudioDecoder { - /** - * 注册一个解码器 - * @param type 要注册的解码器允许解码的类型 - * @param decoder 解码器对象 - */ - static registerDecoder(type, decoder) { - if (!this.decoderMap) this.decoderMap = new Map(); - if (this.decoderMap.has(type)) { - console.warn( - "Audio stream decoder for audio type '" + - type + - "' has already existed." - ); - return; - } - - this.decoderMap.set(type, decoder); - } - - /** - * 解码音频数据 - * @param data 音频文件数据 - * @param player AudioPlayer实例 - */ - static async decodeAudioData(data, player) { - // 检查头文件获取音频类型,仅检查前256个字节 - const toCheck = data.slice(0, 256); - const type = checkAudioType(data); - if (type === "") { - console.error( - "Unknown audio type. Header: '" + - [...toCheck] - .map((v) => v.toString().padStart(2, "0")) - .join(" ") - .toUpperCase() + - "'" - ); - return null; - } - if (isAudioSupport(type)) { - if (data.buffer instanceof ArrayBuffer) { - return player.ac.decodeAudioData(data.buffer); - } else { - return null; - } - } else { - const Decoder = this.decoderMap.get(type); - if (!Decoder) { - return null; - } else { - const decoder = new Decoder(); - await decoder.create(); - const decodedData = await decoder.decode(data); - if (!decodedData) return null; - const buffer = player.ac.createBuffer( - decodedData.channelData.length, - decodedData.channelData[0].length, - decodedData.sampleRate - ); - decodedData.channelData.forEach((v, i) => { - buffer.copyToChannel(v, i); - }); - decoder.destroy(); - return buffer; - } - } - } - } - - class VorbisDecoder { - /** - * 创建音频解码器 - */ - async create() { - this.decoder = new OggVorbisDecoderWebWorker(); - await this.decoder.ready; - } - /** - * 摧毁这个解码器 - */ - destroy() { - this.decoder?.free(); - } - /** - * 解码流数据 - * @param data 流数据 - */ - - async decode(data) { - return this.decoder?.decode(data); - } - /** - * 解码整个文件 - * @param data 文件数据 - */ - async decodeAll(data) { - return this.decoder?.decodeFile(data); - } - /** - * 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用 - */ - async flush() { - return this.decoder?.flush(); - } - } - - class OpusDecoder { - /** - * 创建音频解码器 - */ - async create() { - this.decoder = new OggOpusDecoderWebWorker(); - await this.decoder.ready; - } - /** - * 摧毁这个解码器 - */ - destroy() { - this.decoder?.free(); - } - /** - * 解码流数据 - * @param data 流数据 - */ - async decode(data) { - return this.decoder?.decode(data); - } - /** - * 解码整个文件 - * @param data 文件数据 - */ - async decodeAll(data) { - return this.decoder?.decodeFile(data); - } - /** - * 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用 - */ - async flush() { - return await this.decoder?.flush(); - } - } - const mimeTypeMap = { - [AudioType.Aac]: "audio/aac", - [AudioType.Flac]: "audio/flac", - [AudioType.Mp3]: "audio/mpeg", - [AudioType.Ogg]: "application/ogg", - [AudioType.Opus]: "application/ogg", - [AudioType.Wav]: "application/ogg", - }; - - function isOggPage(data) { - return !isNil(data.isFirstPage); - } - class AudioStreamSource { - constructor(context) { - this.output = context.createBufferSource(); - /** 是否已经完全加载完毕 */ - this.loaded = false; - /** 是否正在播放 */ - this.playing = false; - /** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */ - this.buffered = 0; - /** 已经缓冲的采样点数量 */ - this.bufferedSamples = 0; - /** 歌曲时长,加载完毕之前保持为 0 */ - this.duration = 0; - /** 在流传输阶段,至少缓冲多长时间的音频之后才开始播放,单位秒 */ - this.bufferPlayDuration = 1; - /** 音频的采样率,未成功解析出之前保持为 0 */ - this.sampleRate = 0; - //是否循环播放 - this.loop = false; - /** 上一次播放是从何时开始的 */ - this.lastStartWhen = 0; - /** 开始播放时刻 */ - this.lastStartTime = 0; - /** 上一次播放的缓存长度 */ - this.lastBufferSamples = 0; - - /** 是否已经获取到头文件 */ - this.headerRecieved = false; - /** 音频类型 */ - this.audioType = ""; - /** 每多长时间组成一个缓存 Float32Array */ - this.bufferChunkSize = 10; - /** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */ - this.audioData = []; - - this.errored = false; - this.ac = context; - } - /** 当前已经播放了多长时间 */ - get currentTime() { - return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; - } - /** - * 设置每个缓存数据的大小,默认为10秒钟一个缓存数据 - * @param size 每个缓存数据的时长,单位秒 - */ - setChunkSize(size) { - if (this.controller?.loading || this.loaded) return; - this.bufferChunkSize = size; - } - - piped(controller) { - this.controller = controller; - } - - async pump(data, done) { - if (!data || this.errored) return; - if (!this.headerRecieved) { - // 检查头文件获取音频类型,仅检查前256个字节 - const toCheck = data.slice(0, 256); - this.audioType = checkAudioType(data); - if (!this.audioType) { - console.error( - "Unknown audio type. Header: '" + - [...toCheck] - .map((v) => v.toString(16).padStart(2, "0")) - .join(" ") - .toUpperCase() + - "'" - ); - return; - } - // 创建解码器 - const Decoder = AudioDecoder.decoderMap.get(this.audioType); - if (!Decoder) { - this.errored = true; - console.error( - "Cannot decode stream source type of '" + - this.audioType + - "', since there is no registered decoder for that type." - ); - return Promise.reject( - `Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.` - ); - } - this.decoder = new Decoder(); - // 创建数据解析器 - const mime = mimeTypeMap[this.audioType]; - const parser = new CodecParser(mime); - this.parser = parser; - await this.decoder.create(); - this.headerRecieved = true; - } - - const decoder = this.decoder; - const parser = this.parser; - if (!decoder || !parser) { - this.errored = true; - return Promise.reject( - "No parser or decoder attached in this AudioStreamSource" - ); - } - - await this.decodeData(data, decoder, parser); - if (done) await this.decodeFlushData(decoder, parser); - this.checkBufferedPlay(); - } - - /** - * 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告 - */ - checkSampleRate(info) { - for (const one of info) { - const frame = isOggPage(one) ? one.codecFrames[0] : one; - if (frame) { - const rate = frame.header.sampleRate; - if (this.sampleRate === 0) { - this.sampleRate = rate; - break; - } else { - if (rate !== this.sampleRate) { - console.warn("Sample rate in stream audio must be constant."); - } - } - } - } - } - - /** - * 解析音频数据 - */ - async decodeData(data, decoder, parser) { - // 解析音频数据 - const audioData = await decoder.decode(data); - if (!audioData) return; - // @ts-expect-error 库类型声明错误 - const audioInfo = [...parser.parseChunk(data)]; - - // 检查采样率 - this.checkSampleRate(audioInfo); - // 追加音频数据 - this.appendDecodedData(audioData, audioInfo); - } - - /** - * 解码剩余数据 - */ - async decodeFlushData(decoder, parser) { - const audioData = await decoder.flush(); - if (!audioData) return; - // @ts-expect-error 库类型声明错误 - const audioInfo = [...parser.flush()]; - - this.checkSampleRate(audioInfo); - this.appendDecodedData(audioData, audioInfo); - } - - /** - * 追加音频数据 - */ - appendDecodedData(data, info) { - const channels = data.channelData.length; - if (channels === 0) return; - if (this.audioData.length !== channels) { - this.audioData = []; - for (let i = 0; i < channels; i++) { - this.audioData.push([]); - } - } - // 计算出应该放在哪 - const chunk = this.sampleRate * this.bufferChunkSize; - const sampled = this.bufferedSamples; - const pushIndex = Math.floor(sampled / chunk); - const bufferIndex = sampled % chunk; - const dataLength = data.channelData[0].length; - let buffered = 0; - let nowIndex = pushIndex; - let toBuffer = bufferIndex; - while (buffered < dataLength) { - const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk; - - for (let i = 0; i < channels; i++) { - const audioData = this.audioData[i]; - if (!audioData[nowIndex]) { - audioData.push(new Float32Array(chunk)); - } - const toPush = data.channelData[i].slice(buffered, buffered + rest); - - audioData[nowIndex].set(toPush, toBuffer); - } - buffered += rest; - nowIndex++; - toBuffer = 0; - } - - this.buffered += - info.reduce((prev, curr) => prev + curr.duration, 0) / 1000; - this.bufferedSamples += info.reduce( - (prev, curr) => prev + curr.samples, - 0 - ); - } - - /** - * 检查已缓冲内容,并在未开始播放时播放 - */ - checkBufferedPlay() { - if (this.playing || this.sampleRate === 0) return; - const played = this.lastBufferSamples / this.sampleRate; - const dt = this.buffered - played; - if (this.loaded) { - this.playAudio(played); - return; - } - if (dt < this.bufferPlayDuration) return; - - this.lastBufferSamples = this.bufferedSamples; - // 需要播放 - this.mergeBuffers(); - if (!this.buffer) return; - if (this.playing) this.output.stop(); - this.createSourceNode(this.buffer); - this.output.loop = false; - this.output.start(0, played); - this.lastStartTime = this.ac.currentTime; - this.playing = true; - this.output.addEventListener("ended", () => { - this.playing = false; - this.checkBufferedPlay(); - }); - } - - mergeBuffers() { - const buffer = this.ac.createBuffer( - this.audioData.length, - this.bufferedSamples, - this.sampleRate - ); - const chunk = this.sampleRate * this.bufferChunkSize; - const bufferedChunks = Math.floor(this.bufferedSamples / chunk); - const restLength = this.bufferedSamples % chunk; - for (let i = 0; i < this.audioData.length; i++) { - const audio = this.audioData[i]; - const data = new Float32Array(this.bufferedSamples); - for (let j = 0; j < bufferedChunks; j++) { - data.set(audio[j], chunk * j); - } - if (restLength !== 0) { - data.set( - audio[bufferedChunks].slice(0, restLength), - chunk * bufferedChunks - ); - } - - buffer.copyToChannel(data, i, 0); - } - this.buffer = buffer; - } - - async start() { - delete this.buffer; - this.headerRecieved = false; - this.audioType = ""; - this.errored = false; - this.buffered = 0; - this.sampleRate = 0; - this.bufferedSamples = 0; - this.duration = 0; - this.loaded = false; - if (this.playing) this.output.stop(); - this.playing = false; - this.lastStartTime = this.ac.currentTime; - } - - end(done, reason) { - if (done && this.buffer) { - this.loaded = true; - delete this.controller; - this.mergeBuffers(); - - this.duration = this.buffered; - this.audioData = []; - this.decoder?.destroy(); - delete this.decoder; - delete this.parser; - } else { - console.warn( - "Unexpected end when loading stream audio, reason: '" + - (reason ?? "") + - "'" - ); - } - } - - playAudio(when) { - if (!this.buffer) return; - this.lastStartTime = this.ac.currentTime; - if (this.playing) this.output.stop(); - if (this.route.status !== AudioStatus.Playing) { - this.route.status = AudioStatus.Playing; - } - this.createSourceNode(this.buffer); - this.output.start(0, when); - this.playing = true; - - this.output.addEventListener("ended", () => { - this.playing = false; - if (this.route.status === AudioStatus.Playing) { - this.route.status = AudioStatus.Stoped; - } - if (this.loop && !this.output.loop) this.play(0); - }); - } - /** - * 开始播放这个音频源 - */ - play(when) { - if (this.playing || this.errored) return; - if (this.loaded && this.buffer) { - this.playing = true; - this.playAudio(when); - } else { - this.controller?.start(); - } - } - - createSourceNode(buffer) { - if (!this.target) return; - const node = this.ac.createBufferSource(); - node.buffer = buffer; - if (this.playing) this.output.stop(); - this.playing = false; - this.output = node; - node.connect(this.target.input); - node.loop = this.loop; - } - /** - * 停止播放这个音频源 - * @returns 音频暂停的时刻 number - */ - stop() { - if (this.playing) this.output.stop(); - this.playing = false; - return this.ac.currentTime - this.lastStartTime; - } - /** - * 连接到音频路由图上,每次调用播放的时候都会执行一次 - * @param target 连接至的目标 IAudioInput - */ - connect(target) { - this.target = target; - } - /** - * 设置是否循环播放 - * @param loop 是否循环 boolean) - */ - setLoop(loop) { - this.loop = loop; - } - } - class AudioElementSource { - constructor(context) { - const audio = new Audio(); - audio.preload = "none"; - this.output = context.createMediaElementSource(audio); - this.audio = audio; - this.ac = context; - audio.addEventListener("play", () => { - this.playing = true; - if (this.route.status !== AudioStatus.Playing) { - this.route.status = AudioStatus.Playing; - } - }); - audio.addEventListener("ended", () => { - this.playing = false; - if (this.route.status === AudioStatus.Playing) { - this.route.status = AudioStatus.Stoped; - } - }); - } - get duration() { - return this.audio.duration; - } - get currentTime() { - return this.audio.currentTime; - } - /** - * 设置音频源的路径 - * @param url 音频路径 - */ - setSource(url) { - this.audio.src = url; - } - - play(when = 0) { - if (this.playing) return; - this.audio.currentTime = when; - this.audio.play(); - } - - stop() { - this.audio.pause(); - this.playing = false; - if (this.route.status === AudioStatus.Playing) { - this.route.status = AudioStatus.Stoped; - } - return this.audio.currentTime; - } - - connect(target) { - this.output.connect(target.input); - } - - setLoop(loop) { - this.audio.loop = loop; - } - } - class AudioBufferSource { - constructor(context) { - this.output = context.createBufferSource(); - /** 是否循环 */ - this.loop = false; - /** 上一次播放是从何时开始的 */ - this.lastStartWhen = 0; - /** 播放开始时刻 */ - this.lastStartTime = 0; - this.duration = 0; - this.ac = context; - } - get currentTime() { - return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; - } - - /** - * 设置音频源数据 - * @param buffer 音频源,可以是未解析的 ArrayBuffer,也可以是已解析的 AudioBuffer - */ - async setBuffer(buffer) { - if (buffer instanceof ArrayBuffer) { - this.buffer = await this.ac.decodeAudioData(buffer); - } else { - this.buffer = buffer; - } - this.duration = this.buffer.duration; - } - - play(when) { - if (this.playing || !this.buffer) return; - this.playing = true; - this.lastStartTime = this.ac.currentTime; - if (this.route.status !== AudioStatus.Playing) { - this.route.status = AudioStatus.Playing; - } - this.createSourceNode(this.buffer); - this.output.start(0, when); - this.output.addEventListener("ended", () => { - this.playing = false; - if (this.route.status === AudioStatus.Playing) { - this.route.status = AudioStatus.Stoped; - } - if (this.loop && !this.output.loop) this.play(0); - }); - } - - createSourceNode(buffer) { - if (!this.target) return; - const node = this.ac.createBufferSource(); - node.buffer = buffer; - this.output = node; - node.connect(this.target.input); - node.loop = this.loop; - } - - stop() { - this.output.stop(); - return this.ac.currentTime - this.lastStartTime; - } - - connect(target) { - this.target = target; - } - - setLoop(loop) { - this.loop = loop; - } - } - class AudioPlayer { - constructor() { - /** 音频播放上下文 */ - this.ac = new AudioContext(); - /** 音量节点 */ - this.gain = this.ac.createGain(); - this.gain.connect(this.ac.destination); - this.audioRoutes = new Map(); - } - /** - * 解码音频数据 - * @param data 音频数据 - */ - decodeAudioData(data) { - return AudioDecoder.decodeAudioData(data, this); - } - /** - * 设置音量 - * @param volume 音量 - */ - setVolume(volume) { - this.gain.gain.value = volume; - } - - /** - * 获取音量 - */ - getVolume() { - return this.gain.gain.value; - } - - /** - * 创建一个音频源 - * @param Source 音频源类 - */ - createSource(Source) { - return new Source(this.ac); - } - - /** - * 创建一个兼容流式音频源,可以与流式加载相结合,主要用于处理 opus ogg 不兼容的情况 - */ - createStreamSource() { - return new AudioStreamSource(this.ac); - } - - /** - * 创建一个通过 audio 元素播放的音频源 - */ - createElementSource() { - return new AudioElementSource(this.ac); - } - - /** - * 创建一个通过 AudioBuffer 播放的音频源 - */ - createBufferSource() { - return new AudioBufferSource(this.ac); - } - - /** - * 获取音频目的地 - */ - getDestination() { - return this.gain; - } - - /** - * 创建一个音频效果器 - * @param Effect 效果器类 - */ - createEffect(Effect) { - return new Effect(this.ac); - } - - /** - * 创建一个修改音量的效果器 - * ```txt - * |----------| - * Input ----> | GainNode | ----> Output - * |----------| - * ``` - */ - createVolumeEffect() { - return new VolumeEffect(this.ac); - } - - /** - * 创建一个立体声效果器 - * ```txt - * |------------| - * Input ----> | PannerNode | ----> Output - * |------------| - * ``` - */ - createStereoEffect() { - return new StereoEffect(this.ac); - } - - /** - * 创建一个修改单个声道音量的效果器 - * ```txt - * |----------| - * -> | GainNode | \ - * |--------------| / |----------| -> |------------| - * Input ----> | SplitterNode | ...... | MergerNode | ----> Output - * |--------------| \ |----------| -> |------------| - * -> | GainNode | / - * |----------| - * ``` - */ - createChannelVolumeEffect() { - return new ChannelVolumeEffect(this.ac); - } - - /** - * 创建一个延迟效果器 - * |-----------| - * Input ----> | DelayNode | ----> Output - * |-----------| - */ - createDelay() { - return new DelayEffect(this.ac); - } - - /** - * 创建一个回声效果器 - * ```txt - * |----------| - * Input ----> | GainNode | ----> Output - * ^ |----------| | - * | | - * | |------------| ↓ - * |-- | Delay Node | <-- - * |------------| - * ``` - */ - createEchoEffect() { - return new EchoEffect(this.ac); - } - - /** - * 创建一个音频播放路由 - * @param source 音频源 - */ - createRoute(source) { - return new AudioRoute(source, this); - } - - /** - * 添加一个音频播放路由,可以直接被播放 - * @param id 这个音频播放路由的名称 - * @param route 音频播放路由对象 - */ - addRoute(id, route) { - if (!this.audioRoutes) this.audioRoutes = new Map(); - if (this.audioRoutes.has(id)) { - console.warn( - "Audio route with id of '" + - id + - "' has already existed. New route will override old route." - ); - } - this.audioRoutes.set(id, route); - } - - /** - * 根据名称获取音频播放路由对象 - * @param id 音频播放路由的名称 - */ - getRoute(id) { - return this.audioRoutes.get(id); - } - /** - * 移除一个音频播放路由 - * @param id 要移除的播放路由的名称 - */ - removeRoute(id) { - this.audioRoutes.delete(id); - } - /** - * 播放音频 - * @param id 音频名称 - * @param when 从音频的哪个位置开始播放,单位秒 - */ - play(id, when) { - const route = this.getRoute(id); - if (!route) { - console.warn( - "Cannot play audio route '" + - id + - "', since there is not added route named it." - ); - return; - } - - route.play(when); - } - - /** - * 暂停音频播放 - * @param id 音频名称 - * @returns 当音乐真正停止时兑现 - */ - pause(id) { - const route = this.getRoute(id); - if (!route) { - console.warn( - "Cannot pause audio route '" + - id + - "', since there is not added route named it." - ); - return; - } - return route.pause(); - } - - /** - * 停止音频播放 - * @param id 音频名称 - * @returns 当音乐真正停止时兑现 - */ - stop(id) { - const route = this.getRoute(id); - if (!route) { - console.warn( - "Cannot stop audio route '" + - id + - "', since there is not added route named it." - ); - return; - } - return route.stop(); - } - - /** - * 继续音频播放 - * @param id 音频名称 - */ - resume(id) { - const route = this.getRoute(id); - if (!route) { - console.warn( - "Cannot pause audio route '" + - id + - "', since there is not added route named it." - ); - return; - } - route.resume(); - } - - /** - * 设置听者位置,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 - * @param x 位置x坐标 - * @param y 位置y坐标 - * @param z 位置z坐标 - */ - setListenerPosition(x, y, z) { - const listener = this.ac.listener; - listener.positionX.value = x; - listener.positionY.value = y; - listener.positionZ.value = z; - } - - /** - * 设置听者朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 - * @param x 朝向x坐标 - * @param y 朝向y坐标 - * @param z 朝向z坐标 - */ - setListenerOrientation(x, y, z) { - const listener = this.ac.listener; - listener.forwardX.value = x; - listener.forwardY.value = y; - listener.forwardZ.value = z; - } - - /** - * 设置听者头顶朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 - * @param x 头顶朝向x坐标 - * @param y 头顶朝向y坐标 - * @param z 头顶朝向z坐标 - */ - setListenerUp(x, y, z) { - const listener = this.ac.listener; - listener.upX.value = x; - listener.upY.value = y; - listener.upZ.value = z; - } - } - class AudioRoute { - constructor(source, player) { - source.route = this; - this.output = source.output; - - /** 效果器路由图 */ - this.effectRoute = []; - - /** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */ - this.endTime = 0; - /** 暂停时播放了多长时间 */ - this.pauseCurrentTime = 0; - /** 当前播放状态 */ - this.player = player; - this.status = AudioStatus.Stoped; - - this.shouldStop = false; - /** - * 每次暂停或停止时自增,用于判断当前正在处理的情况。 - * 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作 - */ - this.stopIdentifier = 0; - /** 暂停时刻 */ - this.pauseTime = 0; - this.source = source; - this.source.player = player; - } - /** 音频时长,单位秒 */ - get duration() { - return this.source.duration; - } - /** 当前播放了多长时间,单位秒 */ - get currentTime() { - if (this.status === AudioStatus.Paused) { - return this.pauseCurrentTime; - } else { - return this.source.currentTime; - } - } - set currentTime(time) { - this.source.stop(); - this.source.play(time); - } - /** - * 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。 - * @param time 暂停或停止时,经过多长时间之后才会结束音频的播放 - */ - setEndTime(time) { - this.endTime = time; - } - - /** - * 当音频播放时执行的函数,可以用于音频淡入效果 - * @param fn 音频开始播放时执行的函数 - */ - onStart(fn) { - this.audioStartHook = fn; - } - - /** - * 当音频暂停或停止时执行的函数,可以用于音频淡出效果 - * @param fn 音频在暂停或停止时执行的函数,不填时表示取消这个钩子。 - * 包含两个参数,第一个参数是结束时长,第二个参数是当前音频播放路由对象 - */ - onEnd(fn) { - this.audioEndHook = fn; - } - - /** - * 开始播放这个音频 - * @param when 从音频的什么时候开始播放,单位秒 - */ - async play(when = 0) { - if (this.status === AudioStatus.Playing) return; - this.link(); - await this.player.ac.resume(); - if (this.effectRoute.length > 0) { - const first = this.effectRoute[0]; - this.source.connect(first); - const last = this.effectRoute.at(-1); - last.connect({ input: this.player.getDestination() }); - } else { - this.source.connect({ input: this.player.getDestination() }); - } - this.source.play(when); - this.status = AudioStatus.Playing; - this.pauseTime = 0; - this.audioStartHook?.(this); - this.startAllEffect(); - if (this.status !== AudioStatus.Playing) { - this.status = AudioStatus.Playing; - } - } - - /** - * 暂停音频播放 - */ - async pause() { - if (this.status !== AudioStatus.Playing) return; - this.status = AudioStatus.Pausing; - this.stopIdentifier++; - const identifier = this.stopIdentifier; - if (this.audioEndHook) { - this.audioEndHook(this.endTime, this); - await sleep(this.endTime); - } - if ( - this.status !== AudioStatus.Pausing || - this.stopIdentifier !== identifier - ) { - return; - } - this.pauseCurrentTime = this.source.currentTime; - const time = this.source.stop(); - this.pauseTime = time; - if (this.shouldStop) { - this.status = AudioStatus.Stoped; - this.endAllEffect(); - - this.shouldStop = false; - } else { - this.status = AudioStatus.Paused; - this.endAllEffect(); - } - this.endAllEffect(); - } - - /** - * 继续音频播放 - */ - resume() { - if (this.status === AudioStatus.Playing) return; - if ( - this.status === AudioStatus.Pausing || - this.status === AudioStatus.Stoping - ) { - this.audioStartHook?.(this); - - return; - } - if (this.status === AudioStatus.Paused) { - this.play(this.pauseTime); - } else { - this.play(0); - } - this.status = AudioStatus.Playing; - this.pauseTime = 0; - this.audioStartHook?.(this); - this.startAllEffect(); - } - - /** - * 停止音频播放 - */ - async stop() { - if (this.status !== AudioStatus.Playing) { - if (this.status === AudioStatus.Pausing) { - this.shouldStop = true; - } - return; - } - this.status = AudioStatus.Stoping; - this.stopIdentifier++; - const identifier = this.stopIdentifier; - if (this.audioEndHook) { - this.audioEndHook(this.endTime, this); - await sleep(this.endTime); - } - if ( - this.status !== AudioStatus.Stoping || - this.stopIdentifier !== identifier - ) { - return; - } - this.source.stop(); - this.status = AudioStatus.Stoped; - this.pauseTime = 0; - this.endAllEffect(); - } - - /** - * 添加效果器 - * @param effect 要添加的效果,可以是数组,表示一次添加多个 - * @param index 从哪个位置开始添加,如果大于数组长度,那么加到末尾,如果小于0,那么将会从后面往前数。默认添加到末尾 - */ - addEffect(effect, index) { - if (isNil(index)) { - if (effect instanceof Array) { - this.effectRoute.push(...effect); - } else { - this.effectRoute.push(effect); - } - } else { - if (effect instanceof Array) { - this.effectRoute.splice(index, 0, ...effect); - } else { - this.effectRoute.splice(index, 0, effect); - } - } - this.setOutput(); - if (this.source.playing) this.link(); - } - - /** - * 移除一个效果器 - * @param effect 要移除的效果 - */ - removeEffect(effect) { - const index = this.effectRoute.indexOf(effect); - if (index === -1) return; - this.effectRoute.splice(index, 1); - effect.disconnect(); - this.setOutput(); - if (this.source.playing) this.link(); - } - - setOutput() { - const effect = this.effectRoute.at(-1); - if (!effect) this.output = this.source.output; - else this.output = effect.output; - } - - /** - * 连接音频路由图 - */ - link() { - this.effectRoute.forEach((v) => v.disconnect()); - this.effectRoute.forEach((v, i) => { - const next = this.effectRoute[i + 1]; - if (next) { - v.connect(next); - } - }); - } - - startAllEffect() { - this.effectRoute.forEach((v) => v.start()); - } - - endAllEffect() { - this.effectRoute.forEach((v) => v.end()); - } - } - - const audioPlayer = new AudioPlayer(); - - class BgmController { - constructor(player) { - this.mainGain = player.createVolumeEffect(); - this.player = player; - /** bgm音频名称的前缀 */ - this.prefix = "bgms."; - /** 每个 bgm 的音量控制器 */ - this.gain = new Map(); - - /** 正在播放的 bgm */ - this.playingBgm = ""; - /** 是否正在播放 */ - this.playing = false; - - /** 是否已经启用 */ - this.enabled = true; - /** 是否屏蔽所有的音乐切换 */ - this.blocking = false; - /** 渐变时长 */ - this.transitionTime = 2000; - } - - /** - * 设置音频渐变时长 - * @param time 渐变时长 - */ - setTransitionTime(time) { - this.transitionTime = time; - for (const [, value] of this.gain) { - value.transition.time(time); - } - } - - /** - * 屏蔽音乐切换 - */ - blockChange() { - this.blocking = true; - } - - /** - * 取消屏蔽音乐切换 - */ - unblockChange() { - this.blocking = false; - } - - /** - * 设置总音量大小 - * @param volume 音量大小 - */ - setVolume(volume) { - this.mainGain.setVolume(volume); - this._volume = volume; - } - /** - * 获取总音量大小 - */ - getVolume() { - return this.mainGain.getVolume(); - } - /** - * 设置是否启用 - * @param enabled 是否启用 - */ - setEnabled(enabled) { - if (enabled) this.resume(); - else this.stop(); - this.enabled = enabled; - } - - /** - * 设置 bgm 音频名称的前缀 - */ - setPrefix(prefix) { - this.prefix = prefix; - } - - getId(name) { - return `${this.prefix}${name}`; - } - - /** - * 根据 bgm 名称获取其 AudioRoute 实例 - * @param id 音频名称 - */ - get(id) { - return this.player.getRoute(this.getId(id)); - } - - /** - * 添加一个 bgm - * @param id 要添加的 bgm 的名称 - * @param url 指定 bgm 的加载地址 - */ - addBgm(id, url = `project/bgms/${id}`) { - const type = guessTypeByExt(id); - if (!type) { - console.warn( - "Unknown audio extension name: '" + - id.split(".").slice(0, -1).join(".") + - "'" - ); - return; - } - const gain = this.player.createVolumeEffect(); - if (isAudioSupport(type)) { - const source = audioPlayer.createElementSource(); - source.setSource(url); - source.setLoop(true); - const route = new AudioRoute(source, audioPlayer); - route.addEffect([gain, this.mainGain]); - audioPlayer.addRoute(this.getId(id), route); - this.setTransition(id, route, gain); - } else { - const source = audioPlayer.createStreamSource(); - const stream = new StreamLoader(url); - stream.pipe(source); - source.setLoop(true); - const route = new AudioRoute(source, audioPlayer); - route.addEffect([gain, this.mainGain]); - audioPlayer.addRoute(this.getId(id), route); - this.setTransition(id, route, gain); - } - } - - /** - * 移除一个 bgm - * @param id 要移除的 bgm 的名称 - */ - removeBgm(id) { - this.player.removeRoute(this.getId(id)); - const gain = this.gain.get(id); - gain?.transition.ticker.destroy(); - this.gain.delete(id); - } - - setTransition(id, route, gain) { - const transition = new Transition(); - transition - .time(this.transitionTime) - .mode(linear()) - .transition("volume", 0); - - const tick = () => { - gain.setVolume(transition.value.volume); - }; - - /** - * @param expect 在结束时应该是正在播放还是停止 - */ - const setTick = async (expect) => { - transition.ticker.remove(tick); - transition.ticker.add(tick); - const identifier = route.stopIdentifier; - await sleep(this.transitionTime + 500); - if (route.status === expect && identifier === route.stopIdentifier) { - transition.ticker.remove(tick); - if (route.status === AudioStatus.Playing) { - gain.setVolume(1); - } else { - gain.setVolume(0); - } - } - }; - - route.onStart(async () => { - transition.transition("volume", 1); - setTick(AudioStatus.Playing); - }); - route.onEnd(() => { - transition.transition("volume", 0); - setTick(AudioStatus.Paused); - }); - route.setEndTime(this.transitionTime); - - this.gain.set(id, { effect: gain, transition }); - } - - /** - * 播放一个 bgm - * @param id 要播放的 bgm 名称 - */ - play(id, when) { - if (this.blocking) return; - if (id !== this.playingBgm && this.playingBgm) { - this.player.pause(this.getId(this.playingBgm)); - } - this.playingBgm = id; - if (!this.enabled) return; - this.player.play(this.getId(id), when); - this.playing = true; - } - - /** - * 继续当前的 bgm - */ - resume() { - if (this.blocking || !this.enabled || this.playing) return; - if (this.playingBgm) { - this.player.resume(this.getId(this.playingBgm)); - } - this.playing = true; - } - - /** - * 暂停当前的 bgm - */ - pause() { - if (this.blocking || !this.enabled) return; - if (this.playingBgm) { - this.player.pause(this.getId(this.playingBgm)); - } - this.playing = false; - } - - /** - * 停止当前的 bgm - */ - stop() { - if (this.blocking || !this.enabled) return; - if (this.playingBgm) { - this.player.stop(this.getId(this.playingBgm)); - } - this.playing = false; - } - } - const bgmController = new BgmController(audioPlayer); - - class SoundPlayer { - constructor(player) { - /** 每个音效的唯一标识符 */ - this.num = 0; - this.enabled = true; - this.gain = player.createVolumeEffect(); - /** 每个音效的数据 */ - this.buffer = new Map(); - /** 所有正在播放的音乐 */ - this.playing = new Set(); - this.player = player; - } - /** - * 设置是否启用音效 - * @param enabled 是否启用音效 - */ - setEnabled(enabled) { - if (!enabled) this.stopAllSounds(); - this.enabled = enabled; - } - - /** - * 设置音量大小 - * @param volume 音量大小 - */ - setVolume(volume) { - this.gain.setVolume(volume); - } - /** - * 获取音量大小 - */ - getVolume() { - return this.gain.getVolume(); - } - /** - * 添加一个音效 - * @param id 音效名称 - * @param data 音效的Uint8Array数据 - */ - async add(id, data) { - const buffer = await this.player.decodeAudioData(data); - if (!buffer) { - console.warn( - "Cannot decode sound '" + - id + - "', since audio file may not supported by 2.b." - ); - return; - } - this.buffer.set(id, buffer); - } - - /** - * 播放一个音效 - * @param id 音效名称 - * @param position 音频位置,[0, 0, 0]表示正中心,x轴指向水平向右,y轴指向水平向上,z轴指向竖直向上 - * @param orientation 音频朝向,[0, 1, 0]表示朝向前方 - */ - play(id, position = [0, 0, 0], orientation = [1, 0, 0]) { - if (!this.enabled || !id) return -1; - const buffer = this.buffer.get(id); - if (!buffer) { - console.warn( - "Cannot play sound '" + - id + - "', since there is no added data named it." - ); - return -1; - } - const soundNum = this.num++; - - const source = this.player.createBufferSource(); - source.setBuffer(buffer); - const route = this.player.createRoute(source); - const stereo = this.player.createStereoEffect(); - stereo.setPosition(position[0], position[1], position[2]); - stereo.setOrientation(orientation[0], orientation[1], orientation[2]); - route.addEffect([stereo, this.gain]); - this.player.addRoute(`sounds.${soundNum}`, route); - route.play(); - source.output.addEventListener("ended", () => { - this.playing.delete(soundNum); - }); - this.playing.add(soundNum); - return soundNum; - } - - /** - * 停止一个音效 - * @param num 音效的唯一 id - */ - stop(num) { - const id = `sounds.${num}`; - const route = this.player.getRoute(id); - if (route) { - route.stop(); - this.player.removeRoute(id); - this.playing.delete(num); - } - } - - /** - * 停止播放所有音效 - */ - stopAllSounds() { - this.playing.forEach((v) => { - const id = `sounds.${v}`; - const route = this.player.getRoute(id); - if (route) { - route.stop(); - this.player.removeRoute(id); - } - }); - this.playing.clear(); - } - } - const soundPlayer = new SoundPlayer(audioPlayer); - - function loadAllBgm() { - const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; - for (const bgm of data.main.bgms) { - bgmController.addBgm(bgm); - } - } - loadAllBgm(); - AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder); - AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder); - - core.plugin.audioSystem = { - AudioType, - AudioDecoder, - AudioStatus, - checkAudioType, - isAudioSupport, - audioPlayer, - soundPlayer, - bgmController, - guessTypeByExt, - BgmController, - SoundPlayer, - EchoEffect, - DelayEffect, - ChannelVolumeEffect, - VolumeEffect, - StereoEffect, - AudioEffect, - AudioPlayer, - AudioRoute, - AudioStreamSource, - AudioElementSource, - AudioBufferSource, - loadAllBgm, - StreamLoader, - }; - //bgm相关复写 - control.prototype.playBgm = (bgm, when) => { - bgm = core.getMappedName(bgm); - if (main.mode != "play" || !core.material.bgms[bgm]) return; - // 如果不允许播放 - if (!core.musicStatus.bgmStatus) { - try { - core.musicStatus.playingBgm = bgm; - core.musicStatus.lastBgm = bgm; - core.material.bgms[bgm].pause(); - } catch (e) { - console.error(e); - } - return; - } - core.setMusicBtn(); - - try { - bgmController.play(bgm, when); - } catch (e) { - console.log("无法播放BGM " + bgm); - console.error(e); - core.musicStatus.playingBgm = null; - } - - }; - control.prototype.pauseBgm = () => { - bgmController.pause(); - core.setMusicBtn(); - }; - - control.prototype.resumeBgm = function () { - bgmController.resume(); - core.setMusicBtn(); - }; - control.prototype.checkBgm = function () { - core.playBgm(bgmController.playingBgm || main.startBgm); - }; - control.prototype.triggerBgm = function () { - core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus; - if (bgmController.playing) bgmController.pause(); - else bgmController.resume(); - core.setMusicBtn(); - core.setLocalStorage("bgmStatus", core.musicStatus.bgmStatus); - }; - //sound相关复写 - control.prototype.playSound = function ( - sound, - _pitch, - callback, - position, - orientation - ) { - if (main.mode != "play" || !core.musicStatus.soundStatus) return callback?.(); - const name = core.getMappedName(sound); - const num = soundPlayer.play(name, position, orientation); - const route = audioPlayer.getRoute(`sounds.${num}`); - if (!route) { - callback?.(); - return -1; - } else { - sleep(route.duration * 1000).then(() => callback?.()); - return num; - } - }; - control.prototype.stopSound = function (id) { - if (isNil(id)) { - soundPlayer.stopAllSounds(); - } else { - soundPlayer.stop(id); - } - }; - control.prototype.getPlayingSounds = function () { - return [...soundPlayer.playing]; - }; - //sound加载复写 - loader.prototype._loadOneSound_decodeData = function (name, data) { - if (data instanceof Blob) { - var blobReader = new zip.BlobReader(data); - blobReader.init(function () { - blobReader.readUint8Array(0, blobReader.size, function (uint8) { - //core.loader._loadOneSound_decodeData(name, uint8.buffer); - soundPlayer.add(name, uint8); - }); - }); - return; - } - if (data instanceof ArrayBuffer) { - const uint8 = new Uint8Array(data); - soundPlayer.add(name, uint8); - } - }; - //音量控制复写 - soundPlayer.setVolume( - core.musicStatus.userVolume * core.musicStatus.designVolume - ); - bgmController.setVolume( - core.musicStatus.userVolume * core.musicStatus.designVolume - ); - actions.prototype._clickSwitchs_sounds_userVolume = function (delta) { - var value = Math.round(Math.sqrt(100 * core.musicStatus.userVolume)); - if (value == 0 && delta < 0) return; - core.musicStatus.userVolume = core.clamp( - Math.pow(value + delta, 2) / 100, - 0, - 1 - ); - //audioContext 音效 不受designVolume 影响 - if (core.musicStatus.gainNode != null) - core.musicStatus.gainNode.gain.value = core.musicStatus.userVolume; - soundPlayer.setVolume( - core.musicStatus.userVolume * core.musicStatus.designVolume - ); - bgmController.setVolume( - core.musicStatus.userVolume * core.musicStatus.designVolume - ); - core.setLocalStorage("userVolume", core.musicStatus.userVolume); - core.playSound("确定"); - core.ui._drawSwitchs_sounds(); - }; - }, - "platFly": function () { - // 本插件可以给平面塔启用一个带小地图的楼传,默认关闭 - // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 - var __enable = false; - if (!__enable) return; - - /* 第一步:复制到插件 - * 第二步:将flag:usePlatFly改为true可将带地图的楼传启用 - * 第三步:将flag:__useMinimap__设为true可开启小地图,该操作需切换楼层 - * 修改小地图的缩放可以修改flag:userScale,默认为1 - * 注意:楼层转换必须使用楼层坐标,而不是前一楼、后一楼 - * 关于操作:上楼和下楼操作为PgUp和PgDn,后退10层和前进10层操作为,(<)和.(>) - * 注意:请尽量保证地图传送的地方物理位置正好对准(即把地图拼接上箭头位置恰好对齐)否则连线的位置可能不准 - * 分区说明:用普通事件的楼层转换(红点)代替楼层转换(绿点)即可 - * 说明:紫色表示目前可以到达却没有到达过的地图(所以这是一个具有探索性的地图插件) - */ - // 录像验证直接干掉这个插件 - if (main.replayChecking) return; - // *** --- 以下数据为用户可修改数据 修改后不影响该插件的基本功能 --- *** // - // 检测楼层转换的图块id - var leftPortal = "leftPortal", // 左 - rightPortal = "rightPortal", // 右 - upPortal = "upPortal", // 上 - downPortal = "downPortal", // 下 - upFloor = "upFloor", // 上楼 - downFloor = "downFloor"; // 下楼 - // 一些常用默认值 - var defaultScale = 1, // 默认缩放比率 - defaultLoop = 5, // 绘制地图时的循环检测地图路线次数,loop为5说明最远的地图可以用6步到达 - defaultOpacity = 0.6, // 默认不透明度 - defaultMinorAlpha = defaultOpacity / 2; // 3D绘图时,不与当前层处于同一高度层的默认初始不透明度,不透明度会随着层数的增加或减少而减少 - // *** --- 用户修改区 END --- *** // - // 其余可自定义内容均用 //***--- 包裹着 - - /* -----------------------------------下面是一些调用案例 - * - * 如果想在状态栏显示可以在状态栏自绘里这么写: - core.drawFlyMap(ctx, 你想要的左上角横坐标, 你想要的左上角纵坐标, 你想要的宽度, 你想要的高度, core.status.floorId, {fromUser: true, opacity: 1, loop: 3, use3D:false}); - * - * -----------------------------------以下为高深区域,如果不必要可以不看 - * - * 主要函数说明(这几个函数异常强大,善用可以有弄一些有意思的东西,既然作者都说强大了,那就是特别强大) - * core.drawFlyMap(ctx : string, x?: number, y?: number, width?:number, height?:number, floorId?:string, config?:any): void - * 参数说明: - * ctx:画布,如果不存在则会新建x,y,width,height参数的画布 - * x,y,width,height:画布不存在时创建该数据的画布,如果存在,那么在画布的x,y位置绘制宽度为width,高度为height的地图 - * floorId:以该楼层为中心绘制 - * config:配制参数,包括以下内容(该参数为对象) - * fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层) - * oriFloor:这个不用管就好,具体原理比较复杂,不填或者和floorId一样就行 - * scale:缩放比率,这个是绝对比率,与画布大小无关,默认为1 - * interval:地图间的间距,默认为width / 24 - * deltaH:3D地图的纵向高度差,默认为绘制高度(height)/ 4 - * noErase:是否不清空画布再绘制,默认为false - * fromMini:是否是在小地图上绘制时调用的(无自动缩放和自动定位) - * loop:绘制层数,fromUser为true时才有用,默认为5 - * opacity:不透明度,默认为0.6 - * minorAlpha:3D地图的不与当前层在同一高度的楼层的初始不透明度,默认为opacity/2 - * layer:平面模式下绘制的楼层高度,当前所在层为0,上楼则+1,下楼则-1,默认为0,只有当use3D为false且可以绘制3D地图时有效 - * use3D:是否启用3D绘图,前提是所有地图不都在同一高度,默认为false - * reLeft:3D重新定位画布的左位置,默认为-240 - * reTop:3D重新定位画布的上位置,默认为-240 - * clearCache:是否清除缓存重算,默认为false - * map:这个是最强大的功能了,可以将自定义地图插入,以绘制自定义地图 - * 自定义地图格式:{"left_0_7":"MT1", "right_2_7":"MT2"}以此类推也可以"left_0_7,right_3_7,up_1_0"这种套娃,使用的前提是指定位置得有相应方向的箭头,不然会出错,注意楼层id是到往的楼层的id,如果想绘制3D地图,可以这么写:{"top_1_3,left_0_7": "1_MT1"},前面的1_必须写,上楼为top,下楼为bottom - * 默认取自floorId地图 - * 如有疑问可在造塔群@古祠 - * - * core.getFlyMap(floorId?:string, fromUser?:boolean, oriFloor?:string, loop?:number):Object - * 参数说明:floorId:以该楼层为中心绘制 - * fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层) - * oriFloor:这个不用管就好,具体原理比较复杂,填null或者和floorId一样就行 - * loop:绘制层数,fromUser为true时才有用,默认为5 - */ - - ////// 绘制楼层传送器 ////// - var originDrawFly = core.ui.drawFly; - ui.prototype.drawFly = function (page) { - if (!flags.usePlatFly || core.isReplaying()) return originDrawFly.call(core.ui, page); - core.status.event.data = page; - var floorId = core.floorIds[page]; - core.clearMap('ui'); - core.setAlpha('ui', 0.85); - core.fillRect('ui', 0, 0, this.PIXEL, this.PIXEL, '#000000'); - core.setAlpha('ui', 1); - var size = this.PIXEL; - //***--- 楼传绘制 可根据自己的需求更改 更改之后注意更改点击操作的函数 - // 背景 - core.drawThumbnail(floorId, null, { ctx: 'ui', x: 0, y: 0, size: size, damage: true, fromFly: true }); - core.fillRect("ui", 0, 65, 32, size - 130, [0, 0, 0, 0.7]); - core.fillRect("ui", size, 65, -32, size - 130, [0, 0, 0, 0.7]); - core.fillRect("ui", 65, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]); - core.fillRect("ui", 65, size, size / 2 - 114, -32, [0, 0, 0, 0.7]); - core.fillRect("ui", size / 2 + 49, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]); - core.fillRect("ui", size / 2 + 49, size, size / 2 - 114, -32, [0, 0, 0, 0.7]); - core.fillRect("ui", size / 2 - 47, 0, 94, 32, [0, 0, 0, 0.7]); - core.fillRect("ui", size / 2 - 47, size, 94, -32, [0, 0, 0, 0.7]); - core.fillRect("ui", 0, 0, 63, 32, [0, 0, 0, 0.7]); - core.fillRect("ui", size, 0, -63, 32, [0, 0, 0, 0.7]); - core.fillRect("ui", 0, 32, 32, 31, [0, 0, 0, 0.7]); - core.fillRect("ui", 0, size - 32, 32, -31, [0, 0, 0, 0.7]); - core.fillRect("ui", 0, size, 63, -32, [0, 0, 0, 0.7]); - core.fillRect("ui", size, size, -63, -32, [0, 0, 0, 0.7]); - core.fillRect("ui", size, size - 32, -32, -31, [0, 0, 0, 0.7]); - core.fillRect("ui", size, 32, -32, 31, [0, 0, 0, 0.7]); - core.setTextAlign("ui", "center"); - // 文字 - if (core.getFloorByDirection("left", floorId)) - core.fillText("ui", "←", 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("left", - floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); - if (core.getFloorByDirection("right", floorId)) - core.fillText("ui", "→", size - 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("right", - floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); - if (core.getFloorByDirection("up", floorId)) - core.fillText("ui", "↑", size / 2, 28, core.hasVisitedFloor(core.getFloorByDirection("up", - floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); - if (core.getFloorByDirection("down", floorId)) - core.fillText("ui", "↓", size / 2, size - 4, core.hasVisitedFloor(core.getFloorByDirection("down", - floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); - if (core.getFloorByDirection("top", floorId)) - core.fillText("ui", "上楼", size / 4 + 8, 24, core.hasVisitedFloor(core.getFloorByDirection("top", - floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font); - if (core.getFloorByDirection("bottom", floorId)) - core.fillText("ui", "下楼", size / 4 * 3 - 8, 24, core.hasVisitedFloor(core.getFloorByDirection("bottom", - floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font); - core.fillText("ui", "退10层", size / 4 + 8, size - 8, core.actions._getNextFlyFloor(-1) == page ? "#ff22ff" : "#ffffff", - "22px " + core.status.globalAttribute.font); - core.fillText("ui", "进10层", size / 4 * 3 - 8, size - 8, core.actions._getNextFlyFloor(1) == page ? "#ff22ff" : "#ffffff", - "22px " + core.status.globalAttribute.font); - core.fillText("ui", "退出", 32, 24, "#ffffff", "22px " + core.status.globalAttribute.font); - core.fillText("ui", "楼层名", 32, size - 8, "#ffffff", "22px " + core.status.globalAttribute.font); - core.fillText("ui", "(B)", 16, size - 40, "#ffffff", "22px " + core.status.globalAttribute.font); - core.createCanvas("mapOnUi", -240, -240, size + 480, size + 480, 150); - core.drawFlyMap("mapOnUi", 240, 240, size, size, floorId, { fromUser: true, oriFloor: floorId, use3D: flags.use3D }); - if (core.can3D(floorId) && !flags.in3D) - core.fillText("ui", "3D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font); - if (flags.in3D) - core.fillText("ui", "2D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font); - if (core.can3D(floorId)) core.fillText("ui", "(Z)", size - 16, 56, "#ffffff", "20px " + core.status.globalAttribute.font); - if (flags.flyTitle) { - var style = document.getElementById("ui").getContext("2d"); - style.shadowColor = "rgba(0, 0, 0, 1)"; - style.shadowBlur = 5; - core.fillRect("ui", size / 4, size / 4, size / 2, size / 8, [180, 180, 180, 0.7]); - core.strokeRect("ui", size / 4, size / 4, size / 2, size / 8, [255, 255, 255, 0.7], 3); - style.shadowOffsetX = 4; - style.shadowOffsetY = 2; - core.fillText("ui", (core.status.maps[floorId] || {}).title, size / 2, size / 16 * 5 + 11, "#ffffff", - "32px " + core.status.globalAttribute.font); - style.shadowColor = "none"; - style.shadowBlur = 0; - style.shadowOffsetX = 0; - style.shadowOffsetY = 0; - } - //***--- 楼传绘制 - }; - ////// 楼层传送器界面时的点击操作 ////// - var originClickFly = core.actions._clickFly; - actions.prototype._clickFly = function (x, y) { - if (!flags.usePlatFly || core.isReplaying()) return originClickFly.call(core.actions, x, y); - var page = core.status.event.data; - var floorId = core.floorIds[page]; - //***--- 点击操作 可以修改的地方只有x,y坐标 其余不可修改 - if (x <= 2 && y <= 1) { - core.playSound('取消'); - core.deleteCanvas("mapOnUi") - core.ui.closePanel(); - return; - } - // 3D模式 - if (x >= core.__SIZE__ - 2 && y <= 1) { - if (core.can3D(floorId) && !flags.in3D) - flags.use3D = true; - if (flags.in3D) flags.use3D = false; - core.playSound('光标移动'); - core.ui.drawFly(page); - return; - } - // 显示名称 - if (x <= 1 && y >= core.__SIZE__ - 2) { - if (flags.flyTitle) flags.flyTitle = false; - else flags.flyTitle = true; - core.playSound('光标移动'); - core.ui.drawFly(page); - return; - } - // 飞过去 - if (x > 1 && x < core.__SIZE__ - 1 && y > 1 && y < core.__SIZE__ - 1) { - if (core.status.maps[core.status.floorId].canFlyFrom && - core.status.maps[core.floorIds[core.status.event.data]].canFlyTo && - core.hasVisitedFloor(core.floorIds[core.status.event.data])) { - core.deleteCanvas("mapOnUi"); - } - core.flyTo(core.floorIds[core.status.event.data]); - } - // 前进10层 后退10层 - if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(-1) != page && - x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2) { - core.ui.drawFly(this._getNextFlyFloor(-10)); - core.playSound("光标移动"); - } - if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(1) != page && - x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3) { - core.ui.drawFly(this._getNextFlyFloor(10)); - core.playSound("光标移动"); - } - // 获取索引 - function getId(direction) { - var id = core.getFloorByDirection(direction, floorId); - for (var i in core.floorIds) { - if (core.floorIds[i] == id) return parseInt(i); - } - } - // 上下左右和上下楼 - if (x < 1 && core.getFloorByDirection("left", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("left", floorId)) && y >= 2 && y < core.__SIZE__ - 2) { - core.playSound("光标移动"); - core.drawFly(getId("left")); - } - if (x > core.__SIZE__ - 2 && core.getFloorByDirection("right", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("right", floorId)) && y >= 2 && y < core.__SIZE__ - 2) { - core.playSound("光标移动"); - core.drawFly(getId("right")); - } - if (y < 1 && core.getFloorByDirection("up", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("up", floorId)) && - x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) { - core.playSound("光标移动"); - core.drawFly(getId("up")); - } - if (y > core.__SIZE__ - 2 && core.getFloorByDirection("down", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("down", floorId)) && - x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) { - core.playSound("光标移动"); - core.drawFly(getId("down")); - } - if (y < 1 && x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2 && - core.getFloorByDirection("top", floorId) && core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) { - core.playSound("光标移动"); - core.drawFly(getId("top")); - } - if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3 && - core.getFloorByDirection("bottom", floorId) && core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) { - core.playSound("光标移动"); - core.drawFly(getId("bottom")); - } - return; - //***--- 点击操作 - }; - ////// 楼层传送器界面时,按下某个键的操作 ////// - var originKeyDownFly = core.actions._keyDownFly; - actions.prototype._keyDownFly = function (keycode) { - if (!flags.usePlatFly || core.isReplaying()) return originKeyDownFly.call(core.actions, keycode); - var page = core.status.event.data; - var floorId = core.floorIds[page]; - // 获取索引 - function getId(direction) { - var id = core.getFloorByDirection(direction, floorId); - for (var i in core.floorIds) { - if (core.floorIds[i] == id) return parseInt(i); - } - } - //***--- 按键操作 只可以修改按键的keycode - if (keycode == 37 && core.getFloorByDirection("left", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("left", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("left")); - } else if (keycode == 38 && core.getFloorByDirection("up", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("up", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("up")); - } else if (keycode == 39 && core.getFloorByDirection("right", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("right", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("right")); - } else if (keycode == 40 && core.getFloorByDirection("down", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("down", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("down")); - } else if (keycode == 33 && core.getFloorByDirection("top", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("top")); - } else if (keycode == 34 && core.getFloorByDirection("bottom", floorId) && - core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) { - core.playSound('光标移动'); - core.ui.drawFly(getId("bottom")); - } else if (keycode == 90) { - if (core.can3D(floorId) && !flags.in3D) - flags.use3D = true; - if (flags.in3D) flags.use3D = false; - core.playSound('光标移动'); - core.ui.drawFly(page); - } else if (keycode == 66) { // 地图名 - if (flags.flyTitle) flags.flyTitle = false; - else flags.flyTitle = true; - core.playSound('光标移动'); - core.ui.drawFly(page); - } else if (keycode == 188 && core.actions._getNextFlyFloor(-1) != page) { // 退10层 - core.ui.drawFly(this._getNextFlyFloor(-10)); - core.playSound("光标移动"); - } else if (keycode == 190 && core.actions._getNextFlyFloor(1) != page) { // 进10层 - core.ui.drawFly(this._getNextFlyFloor(10)); - core.playSound("光标移动"); - } - return; - //***--- 按键操作 - }; - ////// 楼层传送器界面时,放开某个键的操作 ////// - var originKeyUpFly = core.actions._keyUpFly; - actions.prototype._keyUpFly = function (keycode) { - if (!flags.usePlatFly || core.isReplaying()) return originKeyUpFly.call(core.actions, keycode); - if (keycode == 71 || keycode == 27 || keycode == 88) { - core.playSound('取消'); - core.deleteCanvas("mapOnUi"); - core.ui.closePanel(); - } - if (keycode == 13 || keycode == 32 || keycode == 67) - this._clickFly(this.HSIZE - 1, this.HSIZE - 1); - return; - }; - ////// 点击楼层传送器时的打开操作 ////// - events.prototype.useFly = function (fromUserAction) { - if (core.isReplaying()) return; - - // 从“浏览地图”页面:尝试直接传送到该层 - if (core.status.event.id == 'viewMaps') { - if (!core.hasItem('fly')) { - core.playSound('操作失败'); - core.drawTip('你没有' + core.material.items['fly'].name, 'fly'); - } else if (!core.canUseItem('fly')) { - core.playSound('操作失败'); - core.drawTip('无法传送到当前层', 'fly'); - } else { - core.flyTo(core.status.event.data.floorId); - } - return; - } - if (!this._checkStatus('fly', fromUserAction, true)) { - core.deleteCanvas('mapOnUi'); - return; - } - if (core.flags.flyNearStair && !core.nearStair()) { - core.playSound('操作失败'); - core.drawTip("只有在楼梯边才能使用" + core.material.items['fly'].name, 'fly'); - core.unlockControl(); - core.status.event.data = null; - core.status.event.id = null; - return; - } - if (!core.canUseItem('fly')) { - core.playSound('操作失败'); - core.drawTip(core.material.items['fly'].name + "好像失效了", 'fly'); - core.unlockControl(); - core.status.event.data = null; - core.status.event.id = null; - return; - } - core.playSound('打开界面'); - core.useItem('fly', true); - return; - }; - // 获取区域平面地图 - this.getFlyMap = function (floorId, fromUser, oriFloor, loop, clearCache) { - floorId = floorId || core.status.floorId; - if (floorId == oriFloor && !fromUser) return; - oriFloor = oriFloor || core.status.floorId; - if (!floorId) return; - // 判断是否需要缓存 - function needCache(fromUser) { - if (fromUser && !core.status.flyMap[floorId]) return true; - if (!fromUser && !core.status.flyMap.cache[floorId]) return true; - return false; - } - // 缓存,加快运行速率 - if (!core.status.flyMap) core.status.flyMap = {}; - if (!core.status.flyMap.cache) core.status.flyMap.cache = {}; - if (!core.status.layer) core.status.layer = {}; - if (!core.status.layer[floorId]) core.status.layer[floorId] = {}; - if (core.status.flyMap.cache[floorId] && fromUser) delete core.status.flyMap.cache[floorId] - if (core.status.flyMap[floorId] && !fromUser) delete core.status.flyMap[floorId] - if (needCache(fromUser) || clearCache) { - // 初始化 - core.status.flyMap[floorId] = {}; - core.status.flyMap[floorId].thisMap = {}; - core.extractBlocks(floorId); - core.status.maps[floorId].blocks.forEach(function (block) { - var id = block.event.id; - var x = block.x, - y = block.y; - var trigger = block.event.trigger; - if (trigger != "changeFloor" && trigger != "upFloor" && trigger != "downFloor") return; - // 是箭头且可以切换地图 - var toFloor = block.event.data.floorId; - // 加入相应位置 - // 箭头 - if (id == leftPortal) { - core.status.flyMap[floorId].thisMap["left_" + x + "_" + y] = toFloor; - } - if (id == upPortal) { - core.status.flyMap[floorId].thisMap["up_" + x + "_" + y] = toFloor; - } - if (id == rightPortal) { - core.status.flyMap[floorId].thisMap["right_" + x + "_" + y] = toFloor; - } - if (id == downPortal) { - core.status.flyMap[floorId].thisMap["down_" + x + "_" + y] = toFloor; - } - // 上下楼 - if (id == upFloor) { - core.status.flyMap[floorId].thisMap["top_" + x + "_" + y] = toFloor; - core.status.layer[floorId].top = true; - } - if (id == downFloor) { - core.status.flyMap[floorId].thisMap["bottom_" + x + "_" + y] = toFloor; - core.status.layer[floorId].bottom = true; - } - }); - // 把下几层接着检测出来 - if (fromUser) { - var usedId = {}; - for (var c = 1; c <= loop; c++) { - for (var i in core.status.flyMap[floorId].thisMap) { - var link = core.status.flyMap[floorId].thisMap; - if (!core.hasVisitedFloor(link[i]) || link[i] instanceof Object || usedId[link[i]]) continue; - usedId[link[i]] = true; - var next = core.getFlyMap(link[i], false, oriFloor); - for (var to in next) { - if (!core.status.layer[next[to]]) core.status.layer[next[to]] = {}; - core.status.flyMap[floorId].thisMap[i + ',' + to] = next[to]; - } - } - } - } - // 把先上再下之类的去掉 - if (fromUser) { - for (var i in core.status.flyMap[floorId].thisMap) { - var route = i.split(","); - for (var one = 0; one <= route.length - 2; one++) { - var step = route[one], - next = route[one + 1]; - if ((step.startsWith("up") && next.startsWith("down")) || - (step.startsWith("down") && next.startsWith("up")) || - (step.startsWith("left") && next.startsWith("right")) || - (step.startsWith("right") && next.startsWith("left")) || - (step.startsWith("top") && next.startsWith("bottom")) || - (step.startsWith("bottom") && next.startsWith("top"))) { - delete core.status.flyMap[floorId].thisMap[i]; - } - } - } - } - // 非当前层不能存此类缓存 - if (!fromUser) { - core.status.flyMap.cache = {}; - core.status.flyMap.cache[floorId] = {}; - core.status.flyMap.cache[floorId].thisMap = core.status.flyMap[floorId].thisMap; - delete core.status.flyMap[floorId]; - } - return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap; - } else { // 直接使用缓存 - return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap; - } - }; - this.can3D = function (floorId) { - var map = core.getFlyMap(floorId, true, floorId); - for (var route in map) { - if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) - return true; - } - return false; - }; - // 绘制地图 - this.drawFlyMap = function (ctx, x, y, width, height, floorId, config) { - // 初始化配置项 - //***--- 初始化 可以修改 || 后的默认值 参数说明在开头的高深区域中 - var fromUser = config.fromUser || false, - oriFloor = config.oriFloor || core.status.floorId, - scale = config.scale || null, - interval = config.interval || (width / 24), - noErase = config.noErase || false, - fromMini = config.fromMini || false, - loop = config.loop || defaultLoop, - opacity = config.opacity || defaultOpacity, - layer = config.layer || 0, - use3D = config.use3D || false, - clearCache = config.clearCache || false, - map = config.map || null; - //***--- 初始化 - map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache); - floorId = floorId || core.status.floorId; - if (!floorId) return; - // 检测是否需要3D绘图 - if (!fromMini && use3D) { - for (var route in map) { - if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) { - config.map = map; - return core.draw3DFlyMap(ctx, x, y, width, height, floorId, config); - } - } - } - if (layer != 0 && !use3D) { - var canLayer = false - for (var route in map) { - if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) { - canLayer = true; - break; - } - } - if (!canLayer) layer = 0; - } - flags.in3D = false; - // 初始化 - var userScale = true; - var newCreate = false; - if (!scale) { - userScale = false; - scale = scale || defaultScale; - } - if (!core.dymCanvas[ctx]) { - core.createCanvas(ctx, x, y, width, height, 140); - newCreate = true; - } - x = x || 0; - y = y || 0; - // 获得canvas属性 - width = width || document.getElementById(ctx).width; - height = height || document.getElementById(ctx).height; - var oLeft = document.getElementById(ctx).offsetLeft / core.domStyle.scale, - oTop = document.getElementById(ctx).offsetTop / core.domStyle.scale; - // 重置大地图和楼传地图的canvas位置 - if (ctx == "mapOnUi") core.relocateCanvas("mapOnUi", -240, -240); - if (!noErase) - core.clearMap(ctx); - var horCenter = Math.floor(width / 2), - uprCenter = Math.floor(height / 2); - if (!newCreate) { - horCenter += x; - uprCenter += y; - } - var centerX = horCenter, - centerY = uprCenter; - var left = centerX, - right = centerX, - up = centerY, - down = centerY; - var used = {}; - var haveLayer = {}; - var nx = horCenter, - ny = uprCenter; - // 先把所在楼层绘制了 - if (layer == 0) { - var nw = core.status.maps[floorId].width * 2 * scale, - nh = core.status.maps[floorId].height * 2 * scale; - core.setAlpha(ctx, 1); - core.fillRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#000000"); - core.strokeRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#ffff22", 3 * scale); - // 当前层上下楼显示 - if (!haveLayer[floorId]) { - core.setAlpha(ctx, 1); - var needLayer = core.status.layer[floorId]; - if (needLayer.top && needLayer.bottom) { - core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale, - centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); - core.drawIcon(ctx, "downFloor", centerX - nw / 2 + core.__SIZE__ * scale, - centerY - nh / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); - } - if (needLayer.top && !needLayer.bottom) { - core.drawIcon(ctx, "upFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2, - Math.min(nw, nh), Math.min(nw, nh)); - } - if (!needLayer.top && needLayer.bottom) { - core.drawIcon(ctx, "downFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2, - Math.min(nw, nh), Math.min(nw, nh)); - } - haveLayer[floorId] = true; - } - // 四侧最远位置 - if (left > centerX - nw / 2) left = centerX - nw / 2; - if (right < centerX + nw / 2) right = centerX + nw / 2; - if (down < centerY + nh / 2) down = centerY + nh / 2; - if (up > centerY - nh / 2) up = centerY - nh / 2; - } - core.setAlpha(ctx, opacity); - for (var route in map) { // 绘制楼层和线条 - var rouArr = route.split(","); - // 检索路线及画线 - // 初始化 - centerX = nx; - centerY = ny; - var nowFloor = floorId || core.status.floorId; - var nowLayer = 0; - for (var one in rouArr) { // 一个一个检测 - var step = rouArr[one].split("_"); - var cx = step[1], - cy = step[2]; - // 获得当前图块 - core.getMapBlocksObj(nowFloor, true); - var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy]; - if (!nowBlock) continue; - var toLoc = nowBlock.event.data.loc, - toFloor = nowBlock.event.data.floorId; - var needLayer = core.status.layer[toFloor]; - // 当前层宽度和高度 - var nw = core.status.maps[nowFloor].width * 2 * scale, - nh = core.status.maps[nowFloor].height * 2 * scale; - // 目标层宽度和高度 - var tw = core.status.maps[toFloor].width * 2 * scale, - th = core.status.maps[toFloor].height * 2 * scale; - // 将当前层变为toFloor - nowFloor = toFloor; - // 超范围不画 - if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height || - centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue; - // 绘制toFloor层 - core.setAlpha(ctx, opacity); - // 确定center 根据箭头自适配 同时绘制线条 我已经看不懂了 - if (!use3D && (step[0] == "top" || step[0] == "bottom") && layer == 0) break; - if (step[0] == "top") nowLayer++; - if (step[0] == "bottom") nowLayer--; - if (step[0] == 'left') { - var shouldTo = th / 2, - realTo = toLoc[1] * 2 * scale; - var shouldFrom = nh / 2, - realFrom = step[2] * 2 * scale; - if (nowLayer == layer) { - core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom, - centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom, - centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale); - } - centerX -= nw / 2 + tw / 2 + interval; - centerY += shouldTo - realTo + realFrom - shouldFrom; - } - if (step[0] == 'right') { - var shouldTo = th / 2, - realTo = toLoc[1] * 2 * scale; - var shouldFrom = nh / 2, - realFrom = step[2] * 2 * scale; - if (nowLayer == layer) { - core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom, - centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom, - centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale); - } - centerX += nw / 2 + tw / 2 + interval; - centerY += shouldTo - realTo + realFrom - shouldFrom; - } - if (step[0] == 'up') { - var shouldTo = tw / 2, - realTo = toLoc[0] * 2 * scale; - var shouldFrom = nw / 2, - realFrom = step[1] * 2 * scale; - if (nowLayer == layer) { - core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2, - centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2, - centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#000000", 2 * scale); - } - centerY -= nh / 2 + th / 2 + interval; - centerX += shouldTo - realTo + realFrom - shouldFrom; - } - if (step[0] == 'down') { - var shouldTo = tw / 2, - realTo = toLoc[0] * 2 * scale; - var shouldFrom = nw / 2, - realFrom = step[1] * 2 * scale; - if (nowLayer == layer) { - core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2, - centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2, - centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#000000", 2 * scale); - } - centerY += nh / 2 + th / 2 + interval; - centerX += shouldTo - realTo + realFrom - shouldFrom; - } - // 只有和目标层高度相同时才绘制 - if (nowLayer != layer) continue; - // 超范围的不画 - if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height || - centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue; - // 四侧最远位置 - if (left > centerX - tw / 2) left = centerX - tw / 2; - if (right < centerX + tw / 2) right = centerX + tw / 2; - if (down < centerY + th / 2) down = centerY + th / 2; - if (up > centerY - th / 2) up = centerY - th / 2; - // 画过了不画 - if (used[toFloor]) continue; - used[toFloor] = true; - // 画地图格 - if (core.hasVisitedFloor(toFloor)) { - core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#000000"); - core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale); - } else { - core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ff22ff"); - core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale); - break; - } - // 上下楼显示 - if (haveLayer[toFloor]) continue; - core.setAlpha(ctx, opacity); - if (needLayer.top && needLayer.bottom) { - core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale, - centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); - core.drawIcon(ctx, "downFloor", centerX - tw / 2 + core.__SIZE__ * scale, - centerY - th / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); - } - if (needLayer.top && !needLayer.bottom) { - core.drawIcon(ctx, "upFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2, - Math.min(tw, th), Math.min(tw, th)); - } - if (!needLayer.top && needLayer.bottom) { - core.drawIcon(ctx, "downFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2, - Math.min(tw, th), Math.min(tw, th)); - } - haveLayer[toFloor] = true; - } - } - // 自动缩放 - if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) { - scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64)); - var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, layer: layer, opacity: opacity, loop: loop }; - return core.drawFlyMap(ctx, x, y, width, height, floorId, con); - } - // 大地图和楼层地图自适配定位 - if (ctx == "mapOnUi" && !fromMini && (left - nx < -128 || right - nx > 128 || - up - ny < -128 || down - ny > 128)) { - core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2); - } - }; - // 3D绘图 - this.draw3DFlyMap = function (ctx, x, y, width, height, floorId, config) { - // 初始化配置项 - //***--- 初始化 同上一个初始化 - var fromUser = config.fromUser || false, - oriFloor = config.oriFloor || core.status.floorId, - scale = config.scale || null, - interval = config.interval || (width / 24), - deltaH = config.deltaH || (height / 8), - noErase = config.noErase || false, - fromMini = config.fromMini || false, - loop = config.loop || defaultLoop, - opacity = config.opacity || defaultOpacity, - minorAlpha = config.minorAlpha || defaultMinorAlpha, - reLeft = config.reLeft || -240, - reTop = config.reTop || -240, - clearCache = config.clearCache || false, - map = config.map || null; - //***--- 初始化 - map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache); - // 当前层是否一个人在一层 - var alone = true; - for (var route in map) { - if (route.startsWith("left") || route.startsWith("right") || - route.startsWith("up") || route.startsWith("down")) { - alone = false; - break; - } - } - // 是则增加一个先上再下的路径 - if (alone) { - for (var route in map) { - if (route.startsWith("top")) { - var first = route.split(",")[0].split("_"); - break; - } - } - var success = false; - core.getMapBlocksObj(nowFloor, true); - var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]]; - var toFloor = nowBlock.event.data.floorId; - core.extractBlocks(toFloor); - core.status.maps[toFloor].blocks.forEach(function (block) { - var id = block.event.id; - var x = block.x, - y = block.y; - var trigger = block.event.trigger; - if (trigger != "changeFloor") return; - if (id == "downFloor") { - map["top_" + first[1] + "_" + first[2] + "," + "bottom_" + x + "_" + y] = floorId; - success = true; - return; - } - }); - // 添加先上再下失败 尝试先下再上 - if (!success) { - for (var route in map) { - if (route.startsWith("bottom")) { - var first = route.split(",")[0].split("_"); - break; - } - } - var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]]; - var toFloor = nowBlock.event.data.floorId; - core.extractBlocks(toFloor); - core.status.maps[toFloor].blocks.forEach(function (block) { - var id = block.event.id; - var x = block.x, - y = block.y; - var trigger = block.event.trigger; - if (trigger != "changeFloor") return; - if (id == "upFloor") { - map["bottom_" + first[1] + "_" + first[2] + "," + "top_" + x + "_" + y] = floorId; - success = true; - return; - } - }); - } - } - floorId = floorId || core.status.floorId; - if (!floorId) return; - flags.in3D = true; - // 初始化 - // 获得排序过的楼层路径 - map = core.sortFloor(map); - map = map.map; - var userScale = true; - var newCreate = false; - if (!scale) { - userScale = false; - scale = scale || defaultScale; - } - if (!core.dymCanvas[ctx]) { - core.createCanvas(ctx, x, y, width, height, 140); - newCreate = true; - } - x = x || 0; - y = y || 0; - // 获得canvas属性 - width = width || document.getElementById(ctx).width; - height = height || document.getElementById(ctx).height; - // 重置canvas位置 - core.relocateCanvas(ctx, reLeft, reTop); - if (!noErase) - core.clearMap(ctx); - var horCenter = Math.floor(width / 2), - uprCenter = Math.floor(height / 2); - if (!newCreate) { - horCenter += x; - uprCenter += y; - } - // 单元格的中心点 即水平线中点处 - var centerX = horCenter, - centerY = uprCenter; - var left = centerX, - right = centerX, - up = centerY, - down = centerY; - var used = {}; - var nx = horCenter, - ny = uprCenter; - // 开始绘制 - for (var i = 0; i < map.length; i++) { - var route = map[i][0]; - var nowLayer = map[i][1]; - var everyLayer = 0; - route = route.split(","); - // 每条路线初始化 - centerX = horCenter; - centerY = uprCenter; - centerX += core.status.maps[floorId].height * scale * Math.SQRT2 / 4; - if (flags.viewingLayer) { - centerY += deltaH * flags.viewingLayer; - } - var nowFloor = floorId || core.status.floorId; - for (var one = 0; one < route.length; one++) { - var step = route[one].split("_"); - var cx = step[1], - cy = step[2]; - // 检测高度,是否与nowLayer一致 不一致在处理完center以后不绘制 - if (step[0] == "top") everyLayer++; - if (step[0] == "bottom") everyLayer--; - // 获得当前图块 - core.getMapBlocksObj(nowFloor, true); - var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy]; - if (!nowBlock) continue; - var toLoc = nowBlock.event.data.loc, - toFloor = nowBlock.event.data.floorId; - // 当前层宽度和高度 - // 斜二测画法 - var nw = core.status.maps[nowFloor].width * 2 * scale, - nh = core.status.maps[nowFloor].height * scale * Math.SQRT2 / 2; - // 目标层宽度和高度 - var tw = core.status.maps[toFloor].width * 2 * scale, - th = core.status.maps[toFloor].height * scale * Math.SQRT2 / 2; - if (!(toLoc instanceof Array)) { - toLoc = [Math.floor(tw / 4 / scale), Math.floor(th / 4 / scale)]; - } - // 绘制当前层 - if (nowLayer == 0 && !used[floorId]) { - core.setAlpha(ctx, 1); - used[floorId] = true; - var nowW = core.status.maps[floorId].width * 2 * scale; - var nowH = core.status.maps[floorId].height * scale * Math.SQRT2 / 2; - var nodes = [ - [centerX - nowW / 2 - nowH / 2, centerY + nowH / 2], - [centerX + nowW / 2 - nowH / 2, centerY + nowH / 2], - [centerX + nowW / 2 + nowH / 2, centerY - nowH / 2], - [centerX - nowW / 2 + nowH / 2, centerY - nowH / 2] - ]; - core.fillPolygon(ctx, nodes, "#000000"); - core.strokePolygon(ctx, nodes, "#ffff22", 1.5 * scale); - // 四侧最远位置 - if (left > centerX - nw / 2 - nh / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - nw / 2 - nh / 2; - if (right < centerX + nw / 2 + nh / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + nw / 2 + nh / 2; - } - // 将当前层变为toFloor - var fromFloor = nowFloor; - nowFloor = toFloor; - // 计算center 画同层间的线 我已经看不懂了 - // 设置不透明度 - if (nowLayer == (flags.viewingLayer || 0)) { - core.setAlpha(ctx, opacity); - } else { - core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); - } - if (step[0] == "left") { - var shouldFrom = nh / 2, - realFrom = cy * scale * Math.SQRT2 / 2; - var shouldTo = th / 2, - realTo = toLoc[1] * scale * Math.SQRT2 / 2; - if (everyLayer == nowLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) - core.drawLine(ctx, centerX - nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom, - centerX - nw / 2 - interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale); - centerX -= nw / 2 + tw / 2 + interval + shouldTo - realTo + realFrom - shouldFrom; - centerY += shouldTo - realTo + realFrom - shouldFrom; - } - if (step[0] == "right") { - var shouldFrom = nh / 2, - realFrom = cy * scale * Math.SQRT2 / 2; - var shouldTo = th / 2, - realTo = toLoc[1] * scale * Math.SQRT2 / 2; - if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) - core.drawLine(ctx, centerX + nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom, - centerX + nw / 2 + interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale); - centerX += nw / 2 + tw / 2 + interval - (shouldTo - realTo + realFrom - shouldFrom); - centerY += shouldTo - realTo + realFrom - shouldFrom; - } - if (step[0] == "up") { - var shouldTo = tw / 2, - realTo = toLoc[0] * scale * 2; - var shouldFrom = nw / 2, - realFrom = cx * scale * 2; - if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) - core.drawLine(ctx, centerX + realFrom - shouldFrom + nh / 2, centerY - nh / 2, centerX + realFrom - - shouldFrom + interval * Math.SQRT2 / 4 + nh / 2, centerY - nh / 2 - interval * Math.SQRT2 / 4, "#ffffff", 2 * scale); - centerY -= nh / 2 + th / 2 + interval * Math.SQRT2 / 4; - centerX += shouldTo - realTo + realFrom - shouldFrom + (nh / 2 + th / 2 + interval * Math.SQRT2 / 4); - } - if (step[0] == "down") { - var shouldTo = tw / 2, - realTo = toLoc[0] * scale * 2; - var shouldFrom = nw / 2, - realFrom = cx * scale * 2; - if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) - core.drawLine(ctx, centerX + realFrom - shouldFrom - nh / 2, centerY + nh / 2, centerX + realFrom - - shouldFrom - interval * Math.SQRT2 / 4 - nh / 2, centerY + nh / 2 + interval * Math.SQRT2 / 4, "#ffffff", 2 * scale); - centerY += nh / 2 + th / 2 + interval * Math.SQRT2 / 4; - centerX += shouldTo - realTo + realFrom - shouldFrom - (nh / 2 + th / 2 + interval * Math.SQRT2 / 4); - } - if (step[0] == "top") { - centerY -= deltaH; - } - if (step[0] == "bottom") { - centerY += deltaH; - } - if (everyLayer == nowLayer && step[0] != "top" && step[0] != "bottom") { - used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; - used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; - } - if (everyLayer != nowLayer) continue; - // 四侧最远位置 - if (!flags.viewingLayer) { - if (left > centerX - tw / 2 - th / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - tw / 2 - th / 2; - if (right < centerX + tw / 2 + th / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + tw / 2 + th / 2; - if (down < centerY + th / 2 && nowLayer == (flags.viewingLayer || 0)) down = centerY + th / 2; - if (up > centerY - th / 2 && nowLayer == (flags.viewingLayer || 0)) up = centerY - th / 2; - } - // 不同高度层之间的连线 - if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) { - core.setAlpha(ctx, opacity * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); - if (step[0] == "top") { - core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#000000", 2 * scale); - used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; - used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; - } - } - if (!used[toFloor]) { - used[toFloor] = true; - // 设置不透明度 - if (nowLayer == (flags.viewingLayer || 0)) { - core.setAlpha(ctx, opacity); - } else { - core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); - } - // 画地图 - var nodes = [ - [centerX - tw / 2 - th / 2, centerY + th / 2], // 左下 - [centerX + tw / 2 - th / 2, centerY + th / 2], // 右下 - [centerX + tw / 2 + th / 2, centerY - th / 2], // 右上 - [centerX - tw / 2 + th / 2, centerY - th / 2] // 左上 - ]; - if (core.hasVisitedFloor(toFloor)) { - core.fillPolygon(ctx, nodes, "#000000"); - } else { - core.fillPolygon(ctx, nodes, "#ff22ff"); - } - core.strokePolygon(ctx, nodes, "#ffffff", 1.5 * scale); - } - // 不同高度层之间的连线 - if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) { - if (step[0] == "bottom") { - core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#ffffff", 5 * scale); - core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#000000", 2 * scale); - used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; - used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; - } - } - } - } - // 自动缩放 - if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) { - scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64)); - var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, opacity: opacity, loop: loop, use3D: true }; - return core.draw3DFlyMap(ctx, x, y, width, height, floorId, con); - } - // 大地图和楼层地图自适配定位 - if (ctx == "mapOnUi") - core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2); - }; - // 不同高度楼层排序 - this.sortFloor = function (map) { - map = map || core.getFlyMap(null, true); - var totalLayer = 1, - topLayer = 0, - bottomLayer = 0, - nowLayer = 0; - // 拆分map - for (var i in map) { - var route = i.split(","); - nowLayer = 0; - for (var one in route) { - var step = route[one].split("_"); - // 层数处理 并记录每一层的层数 - if (step[0] == "top") { - nowLayer++; - map[i] = nowLayer + "_" + map[i]; - if (nowLayer > topLayer) topLayer = nowLayer; - } - if (step[0] == "bottom") { - nowLayer--; - map[i] = nowLayer + "_" + map[i]; - if (nowLayer < bottomLayer) bottomLayer = nowLayer; - } - } - } - // 总层数 - totalLayer = topLayer - bottomLayer + 1; - // 按楼层高度由低到高排序 - // 先变成数组 - var mapArr = []; - for (var one in map) { - mapArr.push([one, parseInt(map[one]) || 0]); - } - // 再sort - mapArr.sort(function (a, b) { return a[1] - b[1]; }); - return { map: mapArr, totalLayer: totalLayer, top: topLayer, bottom: bottomLayer }; - }; - // 由方向获得楼层坐标 - this.getFloorByDirection = function (direction, floorId) { - floorId = floorId || core.status.floorId; - var route = core.getFlyMap(floorId); - for (var step in route) { - if (step.indexOf(direction) >= 0) { - return route[step]; - } - } - return null; - }; - ////// 转换楼层结束的事件 检查小地图 ////// - var originAfterChangeFloor = core.events.afterChangeFloor; - events.prototype.afterChangeFloor = function (floorId) { - if (!flags.__useMinimap__ || core.isReplaying()) { - core.deleteCanvas("minimap"); - core.deleteCanvas("mapArrow"); - core.unregisterAction("ondown", "closeMinimap"); - core.unregisterAction("ondown", "openMinimap"); - return originAfterChangeFloor.call(core.events, floorId); - } - if (main.mode != 'play') return; - this.eventdata.afterChangeFloor(floorId); - // 防止小地图出问题 - core.unregisterAction("ondown", "closeMinimap"); - core.unregisterAction("ondown", "openMinimap"); - // 切换小地图 - core.checkMinimap(true, true); - return; - }; - ////// 瞬间移动 检查小地图 ////// - var originMoveDirectly = core.control.moveDirectly; - control.prototype.moveDirectly = function (destX, destY, ignoreSteps) { - if (!flags.__useMinimap__ || core.isReplaying()) - return originMoveDirectly.call(core.control, destX, destY, ignoreSteps); - var canMoveDirectly = this.controldata.moveDirectly(destX, destY, ignoreSteps); - if (canMoveDirectly) core.checkMinimap(); - return canMoveDirectly; - }; - ////// 每移动一格后执行的事件 检查小地图 ////// - var originMoveOneStep = core.control.moveOneStep; - control.prototype.moveOneStep = function (callback) { - if (!flags.__useMinimap__ || core.isReplaying()) - return originMoveOneStep.call(core.control, callback); - this.controldata.moveOneStep(callback); - core.checkMinimap(); - }; - // 检查小地图开闭情况 改变小地图位置 - this.checkMinimap = function (fromUser, reDraw) { - if (!flags.__useMinimap__ || core.isReplaying()) { - core.unregisterAction("ondown", "closeMinimap"); - core.unregisterAction("ondown", "openMinimap"); - core.deleteCanvas("mapArrow"); - core.deleteCanvas("minimap"); - return; - } - reDraw = reDraw || false; - // 是否重绘 - if (reDraw) { - if (flags.minimap) core.drawMinimap(flags.__onLeft__); - else core.drawClosedMap(flags.__onLeft__); - } - var hx = core.status.hero.loc.x; - var opened = flags.minimap; - var onLeft = hx >= Math.ceil(core.__SIZE__ / 3 * 2); - fromUser = fromUser || false; // 开关小地图相关 - if (!flags.__onLeft__) flags.__onLeft__ = false; - // 如果地图上没有小地图 画到右边 - if (!core.dymCanvas.mapArrow && !core.dymCanvas.minimap && !reDraw) { - if (flags.minimap) core.drawMinimap(); - else core.drawClosedMap(); - flags.__onLeft__ = false; - } - // 人物在中间 不执行 - if (hx >= Math.ceil(core.__SIZE__ / 3 + core.bigmap.offsetX / 32) && - hx <= Math.floor(core.__SIZE__ / 3 * 2 + core.bigmap.offsetX / 32)) return; - // 重定位画布 和 翻转 - // 挪到右边 - if (!onLeft && (flags.__onLeft__ || fromUser)) { - flags.__onLeft__ = false; - if (opened) { - core.relocateCanvas("minimap", core.__PIXELS__ - 120, 0); - core.relocateCanvas("mapArrow", core.__PIXELS__ - 140, 0); - document.getElementById('mapArrow').style.transform = 'none'; - } else { - core.relocateCanvas("minimap", core.__PIXELS__, 0); - core.relocateCanvas("mapArrow", core.__PIXELS__ - 20, 0); - document.getElementById('mapArrow').style.transform = 'none'; - } - } - // 挪到左边 - if (onLeft && (!flags.__onLeft__ || fromUser)) { - flags.__onLeft__ = true; - if (opened) { - core.relocateCanvas("minimap", 0, 0); - core.relocateCanvas("mapArrow", 120, 0); - document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; - } else { - core.relocateCanvas("minimap", -120, 0); - core.relocateCanvas("mapArrow", 0, 0); - document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; - } - } - }; - // 点击小地图的action - this.registerMinimapAction = function (open) { - if (!open) { - core.registerAction("ondown", "closeMinimap", function (x, y, px, py) { - if (!flags.__onLeft__) { - if (px >= core.__PIXELS__ - 140 && px <= core.__PIXELS__ - 120 && - py >= 0 && py <= 120) { - core.closeMinimap(); - core.unregisterAction("ondown", "closeMinimap"); - return true; - } - if (px >= core.__PIXELS__ - 120 && py <= 120) { - core.playSound("打开界面"); - core.drawTotalMap(); - return true; - } - } else { - if (px >= 120 && px <= 140 && py >= 0 && py <= 120) { - core.closeMinimap(); - core.unregisterAction("ondown", "closeMinimap"); - return true; - } - if (px <= 120 && py <= 120) { - core.playSound("打开界面"); - core.drawTotalMap(); - return true; - } - } - }, 10); - } else { - core.registerAction("ondown", "openMinimap", function (x, y, px, py) { - if (!flags.__onLeft__) { - if (px >= core.__PIXELS__ - 20 && py <= 120) { - core.openMinimap(); - core.unregisterAction("ondown", "openMinimap"); - return true; - } - } else { - if (px <= 20 && py <= 120) { - core.openMinimap(); - core.unregisterAction("ondown", "openMinimap"); - return true; - } - } - }, 10); - } - }; - // 地图上的小地图 - this.drawMinimap = function (toLeft) { - if (!flags.__useMinimap__) { - core.deleteCanvas("mapArrow"); - core.deleteCanvas("minimap"); - return; - } - var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1); - if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale) - scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1); - // 绘制 - core.createCanvas("minimap", core.__PIXELS__ - 120, 0, 120, 120, 100); - core.createCanvas("mapArrow", core.__PIXELS__ - 140, 0, 20, 120, 100); - if (toLeft) { - core.relocateCanvas("minimap", 0, 0); - core.relocateCanvas("mapArrow", 120, 0); - document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; - } - core.clearMap("minimap"); - core.clearMap("mapArrow"); - // 黑色底 - core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]); - var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true }; - core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config); - // 向右箭头 - core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]); - core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2); - core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2); - core.setTextAlign("mapArrow", "center"); - core.fillText("mapArrow", ">", 10, 67, [100, 100, 100, 0.9], "20px Verdana"); - core.registerMinimapAction(false); - }; - // 关闭小地图 - this.closeMinimap = function () { - if (!flags.__useMinimap__) { - core.deleteCanvas("mapArrow"); - core.deleteCanvas("minimap"); - return; - } - var onLeft = flags.__onLeft__; - var frame = 0; - var x = core.__PIXELS__, - a = 0.096, - speed = 4.8; - if (onLeft) { - x = 260; - a = -a; - speed = -speed; - } - var interval = setInterval(function () { - core.relocateCanvas("mapArrow", x - 140, 0); - if (!onLeft) - core.relocateCanvas("minimap", x - 120, 0); - else core.relocateCanvas("minimap", x - 260, 0); - speed -= a; - x += speed; - if (frame == 50) { - flags.minimap = false; - clearInterval(interval); - core.drawClosedMap(onLeft); - core.checkMinimap(true); - } - frame++; - }, 20); - }; - // 合上的小地图 - this.drawClosedMap = function (toLeft) { - if (!flags.__useMinimap__) { - core.deleteCanvas("mapArrow"); - core.deleteCanvas("minimap"); - return; - } - var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1); - if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale) - scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1); - // 绘制 - core.createCanvas("minimap", core.__PIXELS__, 0, 120, 120, 100); - core.createCanvas("mapArrow", core.__PIXELS__ - 20, 0, 20, 120, 100); - core.clearMap("minimap"); - core.clearMap("mapArrow"); - if (toLeft) { - core.relocateCanvas("minimap", -120, 0); - core.relocateCanvas("mapArrow", 0, 0); - document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; - } - // 黑色底 - core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]); - var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true }; - core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config); - // 向左箭头 - core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]); - core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2); - core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2); - core.setTextAlign("mapArrow", "center"); - core.fillText("mapArrow", "<", 10, 67, [100, 100, 100, 0.9], "20px Verdana"); - core.registerMinimapAction(true); - }; - // 打开小地图 - this.openMinimap = function () { - if (!flags.__useMinimap__) { - core.deleteCanvas("mapArrow"); - core.deleteCanvas("minimap"); - return; - } - var onLeft = flags.__onLeft__; - var frame = 0; - var x = 120 + core.__PIXELS__, - a = 0.096, - speed = 4.8; - if (onLeft) { - x = 140; - a = -a; - speed = -speed; - } - var interval = setInterval(function () { - core.relocateCanvas("mapArrow", x - 140, 0); - if (!flags.__onLeft__) - core.relocateCanvas("minimap", x - 120, 0); - else core.relocateCanvas("minimap", x - 260, 0); - speed -= a; - x -= speed; - if (frame == 50) { - flags.minimap = true; - clearInterval(interval); - core.drawMinimap(onLeft); - core.checkMinimap(true); - } - frame++; - }, 20); - }; - // 大地图 - this.drawTotalMap = function (floorId) { - floorId = floorId || core.status.floorId; - core.status.event.id = "totalMap"; - core.lockControl(); - if (!flags.viewingLayer) flags.viewingLayer = 0; - var loop = 5; - if (flags.worldMap) loop = core.floorIds.length; - // 大地图时点击和键盘操作 - core.registerAction("ondown", "onDownTmap", function (x, y) { - if (core.status.event.id == "totalMap") { - if (y < 1 && x <= Math.floor(core.__SIZE__ / 2) - 2) { // 上移一层 - if (flags.viewingLayer < core.sortFloor().top) { - flags.viewingLayer++; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1) { // 下移一层 - if (flags.viewingLayer > core.sortFloor().bottom) { - flags.viewingLayer--; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - if (y < 1 && x < Math.ceil(core.__SIZE__ / 2) + 1 && x > Math.floor(core.__SIZE__ / 2) - 2) { - // 区域地图 - if (flags.worldMap) { - flags.worldMap = false; - flags.viewingLayer = 0; - } else flags.worldMap = true; - core.playSound('光标移动'); - core.drawTotalMap(); - return true; - } - if (y >= core.__SIZE__ - 1 && x <= Math.floor(core.__SIZE__ / 2)) { // 3D - if (core.can3D(floorId) && !flags.in3D) flags.use3D = true; - if (flags.in3D) flags.use3D = false; - flags.mapHint = false; - core.playSound('光标移动'); - core.drawTotalMap(); - return true; - } - if (y >= core.__SIZE__ - 1 && x >= Math.ceil(core.__SIZE__ / 2)) { // hint - if (flags.in3D) { - if (!flags.mapHint) flags.mapHint = true; - else flags.mapHint = false; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - flags.viewingLayer = 0; - core.playSound("取消") - core.deleteCanvas("mapOnUi"); - core.deleteCanvas("back"); - core.deleteCanvas("tips"); - core.closePanel(); - core.unregisterAction("ondown", "onDownTmap"); - return true; - } - }, 110); - core.registerAction("keyUp", "keyUpTmap", function (keycode) { - if (core.status.event.id == "totalMap") { - if (keycode == 33) { // PgUp - if (flags.viewingLayer < core.sortFloor().top) { - flags.viewingLayer++; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - if (keycode == 34) { // PgDn - if (flags.viewingLayer > core.sortFloor().bottom) { - flags.viewingLayer--; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - if (keycode == 90) { // Z - if (core.can3D(floorId) && !flags.in3D) flags.use3D = true; - if (flags.in3D) flags.use3D = false; - flags.mapHint = false; - core.playSound('光标移动'); - core.drawTotalMap(); - return true; - } - if (keycode == 84) { // T - if (flags.in3D) { - if (!flags.mapHint) flags.mapHint = true; - else flags.mapHint = false; - core.playSound('光标移动'); - core.drawTotalMap(); - } - return true; - } - if (keycode == 87) { // W - if (flags.worldMap) { - flags.worldMap = false; - flags.viewingLayer = 0; - } else flags.worldMap = true; - core.playSound('光标移动'); - core.drawTotalMap(); - return true; - } - flags.viewingLayer = 0; - core.playSound("取消") - core.deleteCanvas("mapOnUi"); - core.deleteCanvas("back"); - core.deleteCanvas("tips"); - core.closePanel(); - core.unregisterAction("keyUp", "keyUpTmap"); - return true; - } - }, 110); - // 开始画 - core.createCanvas("mapOnUi", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 150); - core.createCanvas("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 140); - core.createCanvas("tips", 0, 0, core.__PIXELS__, core.__PIXELS__, 160); - var ctx = document.getElementById("tips").getContext("2d"); - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 5; - ctx.shadowColor = "rgba(100, 100, 255, 1)"; - core.fillRect("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, [0, 0, 0, 0.9]); - core.fillRect("tips", 0, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]); - core.fillRect("tips", core.__PIXELS__ / 2 + 50, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]); - core.fillRect("tips", core.__PIXELS__ / 2 - 46, 0, 92, 32, [200, 200, 200, 0.8]); - core.drawLine("tips", 0, 32, core.__PIXELS__ / 2 - 50, 32, [50, 50, 50, 0.8], 3); - core.drawLine("tips", core.__PIXELS__ / 2 + 50, 32, core.__PIXELS__, 32, [50, 50, 50, 0.8], 3); - core.drawLine("tips", core.__PIXELS__ / 2 - 46, 32, core.__PIXELS__ / 2 + 46, 32, [50, 50, 50, 0.8], 3); - core.fillRect("tips", 0, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]); - core.fillRect("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]); - core.drawLine("tips", 0, core.__PIXELS__ - 32, core.__PIXELS__ / 2 - 5, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3); - core.drawLine("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__ - 32, core.__PIXELS__, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3); - core.setTextAlign("tips", "center"); - core.fillText("tips", "上移一层", core.__PIXELS__ / 4 - 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - core.fillText("tips", "下移一层", core.__PIXELS__ / 4 * 3 + 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - core.fillText("tips", flags.worldMap ? "小地图" : "区域地图", core.__PIXELS__ / 2, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - core.drawFlyMap("mapOnUi", 240, 240, core.__PIXELS__, core.__PIXELS__, - floorId, { fromUser: true, opacity: 1, oriFloor: floorId, noErase: true, use3D: flags.use3D, layer: flags.viewingLayer, loop: loop, clearCache: true }); - if (flags.in3D) - core.fillText("tips", "参考线(T)", core.__PIXELS__ / 4 * 3, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - if (core.can3D(floorId) && !flags.in3D) - core.fillText("tips", "3D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - if (flags.in3D) - core.fillText("tips", "2D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); - if (flags.mapHint) { - core.drawLine("back", 240, 240 + core.__PIXELS__, 240 + core.__PIXELS__, 240, [100, 100, 240, 0.4], 2); - core.drawLine("back", 240 + core.__PIXELS__ / 2, 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, [100, 100, 240, 0.4], 2); - core.drawLine("back", 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, 240 + core.__PIXELS__ / 2, [100, 100, 240, 0.4], 2); - } - }; - }, - "statistics": function () { - // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 - let __enable = true; - if (!__enable) return; - - const PX = core.__PIXELS__, - SIZE = core.__SIZE__; - const { - ButtonBase, RoundBtn, IconBtn, ExitBtn, - ArrowBtn, MenuBase, Pagination, KeyCodeEnum - } = core.plugin.uiBase; - - // 每次退出数据统计时清空,并且清空statistics变量 - /** - * @typedef {{[itemName:string]:{count:number, [floorId:string]:number}}} objCount - * @typedef {{count:number,money:number,exp:number,point:number,}} enemys - * @typedef {{count:number,hp:number,atk:number,def:number,mdef:number}} items - * @typedef {{toolsCount:objCount,gemsCount:objCount, - * equipsCount:objCount,enemysCount:objCount, - * enemys:enemys,items:items,}} floorStatistic - * @typedef {{[floorId:string]:floorStatistic}} statistics - */ - /** @type {statistics} */ - let statistics = {}; - - // 因为core.status.hero是进存档的,必定可序列化 - const clone = (typeof structuredClone === 'function') ? structuredClone : (ele) => JSON.parse(JSON.stringify(ele)); - - function getFloorStat(floorId) { - const stat = { - toolsCount: { total: 0, }, - gemsCount: { total: 0, }, - equipsCount: { total: 0, }, - enemysCount: { total: 0, }, - enemys: { count: 0, money: 0, exp: 0, point: 0, }, - items: { count: 0, hp: 0, atk: 0, def: 0, mdef: 0, }, - }; - core.maps.extractBlocks(floorId); - const blocks = core.status.maps[floorId].blocks; - const temp = clone(core.status.hero); - const ratio = core.status.thisMap.ratio; - core.setFlag("__statistics__", true); - // 在样板中对ratio使用了clone,这很奇怪,因为ratio似乎应该是一个number值,先改过来,看看会不会出问题 - core.status.thisMap.ratio = core.status.maps[floorId].ratio; - - for (let i = 0, l = blocks.length; i < l; i++) { - const block = blocks[i]; - const cls = block.event.cls, - id = block.event.id; - if (cls === "enemys" || cls === "enemy48") { - const enemy = core.material.enemys[block.event.id]; - stat.enemys.count++; - stat.enemys.money += enemy.money || 0; - stat.enemys.exp += enemy.exp || 0; - stat.enemys.point += enemy.point || 0; - - stat.enemysCount.total++; - if (!stat.enemysCount[id]) stat.enemysCount[id] = 0; - stat.enemysCount[id]++; - } else if (cls === "items") { - // 圣水享受特殊待遇,不参与统计,凡是效果依赖角色当前属性的物品都不应该统计,因为不知道实际吃到的时候角色是什么状态 - if (id === "superPotion") continue; - if (core.material.items[id].cls === "items") { - const effectObj = core.items.getItemEffectValue(id); - for (let statusName in effectObj) { - if (effectObj.hasOwnProperty(statusName)) core.addStatus(statusName, effectObj[statusName]); - } - if (core.material.items[id].itemEffect != null) { - try { eval(core.material.items[id].itemEffect); } catch (e) { } - } - stat.gemsCount.total++; - if (!stat.gemsCount[id]) stat.gemsCount[id] = 0; - stat.gemsCount[id]++; - } else if (core.material.items[id].cls === "tools") { - stat.toolsCount.total++; - if (!stat.toolsCount[id]) stat.toolsCount[id] = 0; - stat.toolsCount[id]++; - } else if (core.material.items[id].cls === "equips") { - stat.equipsCount.total++; - if (!stat.equipsCount[id]) stat.equipsCount[id] = 0; - stat.equipsCount[id]++; - } - } else if (block.event.trigger === "openDoor") { - stat.toolsCount.total++; - if (!stat.toolsCount[id]) stat.toolsCount[id] = 0; - stat.toolsCount[id]++; - } - } - ["hp", "atk", "def", "mdef"].forEach(status => { - stat.items[status] = core.status.hero[status] - temp[status]; - }); - core.setFlag("__statistics__", false); - core.status.thisMap.ratio = ratio; - core.status.hero = temp; - window.hero = core.status.hero; - window.flags = core.status.hero.flags; - return stat; - } - - function addToFormer(obj1, obj2) { - for (let i in obj2) { - if (!obj1[i]) obj1[i] = 0; - obj1[i] += obj2[i]; - } - } - - /** - * @param {{total:number; [itemId:string]:{[floorId:string]:number}}} obj1 - * @param {{total:number, [itemId:string]:number}} obj2 - */ - function addCountToFormer(obj1, obj2, floorId) { - obj1.total += obj2.total; - for (let itemId in obj2) { - if (!obj1[itemId]) obj1[itemId] = { total: 0 }; - if (!obj2[itemId]) continue; - obj1[itemId].total += obj2[itemId]; - obj1[itemId][floorId] = obj2[itemId]; - } - } - - /** - * @typedef {{total:number;[floorId:string]:number}} ObjSTAT - * @typedef {{toolsCount:ObjSTAT,gemsCount:ObjSTAT, - * equipsCount:ObjSTAT,enemysCount:ObjSTAT, - * enemys:enemys,items:items, - * battleDamage:ObjSTAT, extraDamage:ObjSTAT, - * poisonDamage:ObjSTAT, vampireExtraLoss:ObjSTAT,}} STAT - */ - /** - * @param {string[]} floorList 要统计的楼层的列表 - */ - function getFloorListStat(floorList) { - const statistics = core.status.hero.statistics; - const damagePerFloor = statistics.damagePerFloor; - /** @type {STAT} */ - const stat = { - toolsCount: { total: 0, }, - gemsCount: { total: 0, }, - equipsCount: { total: 0, }, - enemysCount: { total: 0, }, - enemys: { count: 0, money: 0, exp: 0, point: 0, }, - items: { count: 0, hp: 0, atk: 0, def: 0, mdef: 0, }, - battleDamage: { total: statistics.battleDamage }, - extraDamage: { total: statistics.extraDamage }, - poisonDamage: { total: statistics.poisonDamage }, - vampireExtraLoss: { total: statistics.vampireExtraLoss }, - }; - floorList.forEach(floorId => { - if (!statistics[floorId]) statistics[floorId] = getFloorStat(floorId); - addToFormer(stat.enemys, statistics[floorId].enemys); - addToFormer(stat.items, statistics[floorId].items); - addCountToFormer(stat.toolsCount, statistics[floorId].toolsCount, floorId); - addCountToFormer(stat.gemsCount, statistics[floorId].gemsCount, floorId); - addCountToFormer(stat.equipsCount, statistics[floorId].equipsCount, floorId); - addCountToFormer(stat.enemysCount, statistics[floorId].enemysCount, floorId); - if (damagePerFloor[floorId]) { - ["battleDamage","extraDamage","poisonDamage","vampireExtraLoss"].forEach(key=>{ - stat[key][floorId] = damagePerFloor[floorId]?.[key] || 0; - }); - } - }); - return stat; - } - - class FloorBtn extends ButtonBase { - constructor(x, y, w, h) { - super(x, y, w, h); - this.floorId = ""; - /** @type {number} */this.key; - /** @type {FloorStatisticsMenu} */ this.menu; - } - - draw() { - const { x, y, w, h, floorId, ctx } = this; - let strokeStyle = "black"; - if (core.markedFloorIds[floorId]) strokeStyle = "yellow"; - if (this.menu.areaBorder === floorId) strokeStyle = "orange"; - core.ui.strokeRect(ctx, x - 1, y - 1, w + 2, h + 2, strokeStyle, 2); - core.maps.drawThumbnail(floorId, null, { - x, - y, - ctx, - damage: false, - noHD: true, - size: w - }); - core.setTextAlign(ctx, "center"); - if (floorId === core.status.floorId) { - core.ui.fillBoldText(ctx, "当前层", x + w / 2, y + h / 2, "red", "white", "12px Verdana"); - } - core.ui.fillText(ctx, floorId, x + w / 2, y + h + 12, "black", "12px Verdana"); - core.ui.fillText(ctx, core.status.maps[floorId].name, x + w / 2, y + h + 24, "black", "12px Verdana"); - } - } - - class FloorStatisticsMenu extends MenuBase { - constructor(name, toListen, x, y, w, h, zIndex, capacity) { - super(name, toListen, x, y, w, h, zIndex); - /** 一页的地图容量 */ - this.capacity = capacity; - this.totalFloorList = this.getTotalFloorList(); - this.page = Math.floor(this.totalFloorList.indexOf(core.status.floorId) / capacity); - this.floorList = this.getFloorList(); - /** 区域选择时的起始楼层*/this.areaBorder = ""; - /** 是否处于区域选择模式下 */this.areaSelectOn = false; - } - - updateBtnInfo() { - this.btnMap.forEach((btn, key) => { - if (btn instanceof FloorBtn) { - btn.disable = (key >= this.floorList.length); - btn.floorId = this.floorList[key]; - } - }); - } - - drawContent() { - const { x, y, w, h, zIndex, name } = this; - const ctx = core.createCanvas(name, x, y, w, h, zIndex); - this.updateBtnInfo(); - super.drawContent(); - } - - getTotalFloorList() { - return core.floorIds.filter(floorId => { - const floorData = core.status.maps[floorId]; - if (floorData.deleted) return false; // 被砍层插件砍掉的地图不计入 - if (floorData.cannotViewMap && floorId !== core.status.floorId) return false; - return true; - }) - } - - getFloorList() { - return this.totalFloorList.slice(this.page * this.capacity, - Math.min((this.page + 1) * this.capacity, this.totalFloorList.length)); - } - - pageDown() { - if (this.page > 0) { - this.page--; - this.floorList = this.getFloorList(); - this.drawContent(); - } - } - - pageUp() { - if ((this.page + 1) * this.capacity < this.totalFloorList.length) { - this.page++; - this.floorList = this.getFloorList(); - this.drawContent(); - } - } - } - - class FloorStatisticsTextMenu extends MenuBase { - constructor(name, toListen, x, y, w, h, zIndex) { - super(name, toListen, x, y, w, h, zIndex); - this.statItemList = core.ui.uidata.drawStatistics(); - /** @type {STAT|null} */ - this.stat = null; - this.itemPage = 0; // 道具列表到了第几页 - this.floorPage = 0; // 楼层列表到了第几页 - this.ROW = 7; // 表格行数 - this.COLUMN = 5; // 表格列数 - this.itemList = []; // 要统计的元件的id列表 - this.floorList = []; //要统计的楼层的id列表 - /** @type {"tools"|"items"|"equips"|"enemys"|"damage"} */ - this.mode = "tools"; - } - - getFloorList() { - const floorList = []; - for (let floorId in core.markedFloorIds) { - if (core.markedFloorIds[floorId]) floorList.push(floorId); - } - return floorList; - } - - getItemList(count) { - let itemList = null; - const sortFunc = (a, b) => count[b].total - count[a].total; - if (this.mode === "tools") { - const doorNameList = []; - const itemNameList = []; - for (let i in count) { - if (i === "total") continue; - if (core.material.items[i]) itemNameList.push(i); - else doorNameList.push(i); - } - itemList = [...doorNameList.sort(sortFunc), ...itemNameList.sort(sortFunc)]; - } - else if (this.mode === "damage") { - itemList = ["battleDamage", "extraDamage"]; - if (core.status.hero.statistics.poisonDamage > 0) itemList.push("poisonDamage"); - if (core.status.hero.statistics?.vampireExtraLoss > 0) itemList.push("vampireExtraLoss"); - } - else itemList = Object.keys(count).filter(item => item !== "total").sort(sortFunc); - return itemList; - } - - getStat() { - return getFloorListStat(this.floorList); - } - - itemPageDown() { - if (this.itemPage > 0) this.itemPage--; - this.drawContent(); - } - - itemPageUp() { - if ((this.itemPage + 1) * this.ROW < this.itemList.length) this.itemPage++; - this.drawContent(); - } - - floorPageDown() { - if (this.floorPage > 0) this.floorPage--; - this.drawContent(); - } - - floorPageUp() { - if ((this.floorPage + 1) * this.COLUMN < this.floorList.length) this.floorPage++; - this.drawContent(); - } - - /** @param {ObjSTAT} count */ - drawTable(ctx, count) { - const { itemPage, floorPage, ROW, COLUMN, itemList, floorList } = this; - - const curritemList = itemList.slice(itemPage * ROW, - Math.min((itemPage + 1) * ROW, itemList.length)); - const currFloorList = floorList.slice(floorPage * COLUMN, - Math.min((floorPage + 1) * COLUMN, floorList.length)); - - core.ui.setTextBaseline(ctx, "top"); - core.ui.setTextAlign(ctx, "right"); - for (let i = -1, l = curritemList.length; i < l; i++) { - let id, name, total; - if (i !== -1) { - id = curritemList[i]; - if (this.mode !== "damage") { - name = core.material.items[id]?.name || core.material.enemys[id]?.name || - core.getBlockById(id).event.name || ""; - } - else { - if (id === "battleDamage") name = "战斗伤害"; - else if (id === "extraDamage") name = "地图伤害"; - else if (id === "poisonDamage") name = "中毒伤害"; - else if (id === "vampireExtraLoss") name = "吸血额外伤害"; - } - console.log(count[id]); - if (count[id]==null) debugger; - total = count[id].total || 0; - } - core.ui.strokeRect(ctx, 20, 155 + 25 * i, 80, 25, "black"); - for (let j = -1, l1 = currFloorList.length; j < l1; j++) { - core.ui.strokeRect(ctx, 150 + 50 * j, 155 + 25 * i, 50, 25, "black"); - const floorId = currFloorList[j]; - const floorCountStr = count[id]?.[currFloorList[j]] || "-"; - const text = (i === -1) ? ((j === -1) ? "总数" : floorId) : - ((j === -1) ? total : floorCountStr); - core.ui.fillText(ctx, text, 194 + 50 * j, 163 + 25 * i, - (text === "-") ? "gray" : "black", "12px Verdana"); - } - if (i === -1) continue; // i=-1是表头 - core.ui.drawIcon(ctx, id, 25, 160 + 25 * i, 16, 16); - core.ui.fillText(ctx, name, 94, 163 + 25 * i, "black", name.length < 5 ? "12px Verdana" : "8px Verdana"); - - } - } - - drawContent() { - if (!this.stat) return; - const { x, y, w, h, zIndex, name, stat } = this; - const ctx = core.createCanvas(name, x, y, w, h, zIndex); - const { enemys, items, toolsCount, gemsCount, equipsCount, enemysCount, - battleDamage, extraDamage, poisonDamage, vampireExtraLoss } = stat; - const enemyText = `给定地图中共有怪物${enemys.count}个` + - (enemys.money > 0 ? `,总金币${enemys.money}.` : '') + - (enemys.exp > 0 ? `,总经验${enemys.exp}` : '') + - (enemys.point > 0 ? `,总加点${enemys.point}` : '') + "。"; - let gemText = ''; - const itemEffectText = `所有即捡即用物品共计加生命值${items.hp}点` + - `,\n攻击${items.atk}点` + - `,防御${items.def}点` + - `,护盾${items.mdef}点` + "。"; - const text = enemyText + '\n' + itemEffectText; - core.ui.drawTextContent(ctx, text, { - maxWidth: 360, color: "black", left: 20, top: 10, - fontSize: 14 - }); - let count = toolsCount; - if (this.mode === "items") count = gemsCount; - else if (this.mode === "equips") count = equipsCount; - else if (this.mode === "enemys") count = enemysCount; - else if (this.mode === "damage") count = { battleDamage, extraDamage, poisonDamage, vampireExtraLoss }; - this.itemList = this.getItemList(count); - this.drawTable(ctx, count); - super.drawContent(); - } - } - - class PlayerStatisticsMenu extends MenuBase { - drawContent() { - const { name, x, y, w, h, zIndex } = this; - const ctx = this.createCanvas(name, x, y, w, h, zIndex); - const statistics = core.status.hero.statistics; - const str = `当前总步数:${core.status.hero.steps},当前游戏时长:${core.formatTime(statistics.currTime)},总游戏时长${core.formatTime(statistics.totalTime)}。瞬间移动次数:${statistics.moveDirectly},共计少走${statistics.ignoreSteps}步。 - -总计通过血瓶恢复生命值为${core.formatBigNumber(statistics.hp)}点。 - -总计打死了${statistics.battle}个怪物,得到了${core.formatBigNumber(statistics.money)}金币,${core.formatBigNumber(statistics.exp)}点经验。 - -受到的总伤害为${core.formatBigNumber(statistics.battleDamage + statistics.poisonDamage + statistics.extraDamage)},其中战斗伤害${core.formatBigNumber(statistics.battleDamage)}点${core.flags.statusBarItems.indexOf('enableDebuff') >= 0 ? `,中毒伤害${core.formatBigNumber(statistics.poisonDamage)}点` : ""},领域/夹击/阻击/血网伤害${core.formatBigNumber(statistics.extraDamage)}点。 -${statistics["vampireExtraLoss"] > 0 ? `吸血怪物累计对你造成超出最低伤害的额外伤害${core.formatBigNumber(statistics["vampireExtraLoss"])}点。` : ``} -${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.formatBigNumber(statistics.oneDefEffect || 0)}` : ""}${statistics.hasOwnProperty("oneMdefEffect") ? `,1护盾累计减伤为${core.formatBigNumber(statistics.oneMdefEffect || 0)}` : ""}。 -`; - core.drawTextContent(ctx, str, { - left: 30, top: 20, color: "black", maxWidth: 360, fontSize: 14 - }); - } - } - - class StatisticsMenu extends Pagination { - drawContent() { - const { x, y, w, h, zIndex, name } = this; - const ctx = core.createCanvas(name, x, y, w, h, zIndex); - core.ui.fillRect(ctx, x, y, w, h, "#E2E2E2"); - super.drawContent(); - } - } - - function StatisticsMenuFactory() { - const ROW = 3, COLUMN = 4; - const floorStatisticsMenu = new FloorStatisticsMenu("floorStatistics", ["ondown"], - 0, 50, PX, PX - 50, 132, ROW * COLUMN); - const floorStatisticsTextMenu = new FloorStatisticsTextMenu("floorStatisticsText", ["ondown"], - 0, 50, PX, PX - 50, 132); - const playerStatisticsMenu = new PlayerStatisticsMenu("playerStatistics", ["ondown"], - 0, 50, PX, PX - 50, 132); - const staticsMenu = new StatisticsMenu( - [floorStatisticsMenu, floorStatisticsTextMenu, playerStatisticsMenu], - 0, "statistics", ["ondown"], 0, 0, PX, PX, 131); - - const floorStatisticsList = []; - /** @this {FloorBtn} */ - const mark = function () { - /** @type {FloorStatisticsMenu} */ - const menu = this.menu; - if (menu.areaSelectOn) { - if (!menu.areaBorder) menu.areaBorder = this.floorId; - else { - let index1 = menu.totalFloorList.indexOf(menu.areaBorder), - index2 = menu.totalFloorList.indexOf(this.floorId); - if (index2 < index1) [index1, index2] = [index2, index1]; - menu.totalFloorList.slice(index1, index2 + 1).forEach(floorId => { - core.markedFloorIds[floorId] = true; - }); - menu.areaSelectOn = false; - menu.areaBorder = ""; - } - } - else { - core.markedFloorIds[this.floorId] = !core.markedFloorIds[this.floorId]; - } - menu.drawContent(); - }; - for (let i = 0; i < ROW; i++) { - for (let j = 0; j < COLUMN; j++) { - const floorBtn = new FloorBtn(43 + j * 90, 20 + i * 100, 60, 60); - floorStatisticsList.push([i * COLUMN + j, floorBtn, mark.bind(floorBtn)]); - } - } - const pgDownBtn = new ArrowBtn(10, 145, 16, 16, "left"), - pgUpBtn = new ArrowBtn(390, 145, 16, 16, "right"); - floorStatisticsList.push(["pgDown", pgDownBtn, floorStatisticsMenu.pageDown.bind(floorStatisticsMenu)], - ["pgUp", pgUpBtn, floorStatisticsMenu.pageUp.bind(floorStatisticsMenu)]); - const checkAllBtn = new RoundBtn(30, 320, 50, 20, "全选"); - const inverseBtn = new RoundBtn(90, 320, 50, 20, "反选"); - const clearSelectBtn = new RoundBtn(150, 320, 60, 20, "全不选"); - const areaSelectBtn = new RoundBtn(220, 320, 76, 20, "区域选择"); - const getOutcomeBtn = new RoundBtn(306, 320, 76, 20, "统计所选"); - floorStatisticsList.push( - ["checkAll", checkAllBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { - core.markedFloorIds[floorId] = true; - }); - floorStatisticsMenu.drawContent(); - }], - ["inverse", inverseBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { - core.markedFloorIds[floorId] = !core.markedFloorIds[floorId]; - }); - floorStatisticsMenu.drawContent(); - }], - ["clearSelect", clearSelectBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { - core.markedFloorIds[floorId] = false; - }); - floorStatisticsMenu.drawContent(); - }], - ["areaSelect", areaSelectBtn, () => { - if (!floorStatisticsMenu.areaSelectOn) { - floorStatisticsMenu.areaSelectOn = true; - core.drawSuccessTip("请依次选择目标区域的起始楼层和终点楼层"); - } - else { - floorStatisticsMenu.areaSelectOn = true; - floorStatisticsMenu.areaBorder = ""; - core.drawFailTip("选择已取消"); - } - }], - ["getOutcome", getOutcomeBtn, () => { - const floorList = []; - for (let floorId in core.markedFloorIds) { - if (core.markedFloorIds[floorId]) floorList.push(floorId); - } - floorStatisticsTextMenu.floorList = floorStatisticsTextMenu.getFloorList(); - floorStatisticsTextMenu.stat = floorStatisticsTextMenu.getStat(); - staticsMenu.changePage(1); - }]); - floorStatisticsMenu.registerBtns(floorStatisticsList); - - const config = { font: "14px Verdana", selectedFillStyle: "#5B8DEF" }; - const toolStatBtn = new RoundBtn(20, 90, 90, 20, "消耗品和门", config); - const itemStatBtn = new RoundBtn(125, 90, 70, 20, "血瓶宝石", config); - const equipStatBtn = new RoundBtn(215, 90, 40, 20, "装备", config); - const enemyStatBtn = new RoundBtn(275, 90, 40, 20, "敌人", config); - const damageStatBtn = new RoundBtn(335, 90, 40, 20, "伤害", config); - const itemDownBtn = new ArrowBtn(20, 335, 16, 16, "left"); - const itemUpBtn = new ArrowBtn(40, 335, 16, 16, "right"); - const floorDownBtn = new ArrowBtn(360, 335, 16, 16, "left"); - const floorUpBtn = new ArrowBtn(380, 335, 16, 16, "right"); - - const changeMode = (btn, mode) => () => { - floorStatisticsTextMenu.btnMap.forEach(ele => { - ele.status = (btn === ele) ? "selected" : "none"; - }); - floorStatisticsTextMenu.mode = mode; - floorStatisticsTextMenu.itemPage = 0; - floorStatisticsTextMenu.drawContent(); - }; - - floorStatisticsTextMenu.registerBtns([ - ["toolStat", toolStatBtn, changeMode(toolStatBtn, "tools")], - ["itemStat", itemStatBtn, changeMode(itemStatBtn, "items")], - ["equipStat", equipStatBtn, changeMode(equipStatBtn, "equips")], - ["enemyStat", enemyStatBtn, changeMode(enemyStatBtn, "enemys")], - ["damageStat", damageStatBtn, changeMode(damageStatBtn, "damage")], - ["itemDown", itemDownBtn, () => floorStatisticsTextMenu.itemPageDown()], - ["itemUp", itemUpBtn, () => floorStatisticsTextMenu.itemPageUp()], - ["floorDown", floorDownBtn, () => floorStatisticsTextMenu.floorPageDown()], - ["floorUp", floorUpBtn, () => floorStatisticsTextMenu.floorPageUp()], - ]); - toolStatBtn.status = "selected"; - - const exitBtn = new ExitBtn(380, 10, 24, 24); - const mainConfig = { fillStyle: "LightSteelBlue", selectedFillStyle: "blue" } // 120+80+8=208 - const floorBtn = new RoundBtn(120, 20, 80, 20, "楼层数据", mainConfig); - const playerBtn = new RoundBtn(216, 20, 80, 20, "玩家数据", mainConfig); - floorBtn.status = "selected"; - const exit = () => { - core.setFlag("__statistics__", null); - setTimeout(() => { - staticsMenu.clear(); - core.unlockControl(); - }, 1); - } - staticsMenu.registerBtns([ - ["exit", exitBtn, exit], - ["floor", floorBtn, () => { - floorBtn.status = "selected"; - playerBtn.status = "none"; - staticsMenu.changePage(0); - staticsMenu.drawButtonContent(); - }], - ["player", playerBtn, () => { - floorBtn.status = "none"; - playerBtn.status = "selected"; - staticsMenu.changePage(2); - staticsMenu.drawButtonContent(); - }], - ]); - return staticsMenu; - } - - function _drawStatistics() { - core.ui.closePanel(); - core.control.lockControl(); - const staticsMenu = StatisticsMenuFactory(); - staticsMenu.init(); - staticsMenu.initOnePage(0); - } - core.ui._drawStatistics = _drawStatistics; - } +// @ts-check +/// +var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = +{ + "init": function () { + + console.log("插件编写测试"); + + // 可以写一些直接执行的代码 + // 在这里写的代码将会在【资源加载前】被执行,此时图片等资源尚未被加载。 + // 请勿在这里对包括bgm,图片等资源进行操作。 + + core.plugin._afterLoadResources = function () { + // 本函数将在所有资源加载完毕后,游戏开启前被执行 + // 可以在这个函数里面对资源进行一些操作。 + // 若需要进行切分图片,可以使用 core.splitImage() 函数,或直接在全塔属性-图片切分中操作 + } + + // 可以在任何地方(如afterXXX或自定义脚本事件)调用函数,方法为 core.plugin.xxx(); + // 从V2.6开始,插件中用this.XXX方式定义的函数也会被转发到core中,详见文档-脚本-函数的转发。 + }, + "shop": function () { + // 【全局商店】相关的功能 + /** + * 打开一个全局商店 + * @param {string} shopId + * @param {boolean} noRoute + */ + core.plugin.openShop = function (shopId, noRoute) { + const shop = core.status.shops[shopId]; + // Step 1: 检查能否打开此商店 + if (!this.canOpenShop(shopId)) { + core.drawTip("该商店尚未开启"); + return false; + } + + // Step 2: (如有必要)记录打开商店的脚本事件 + if (!noRoute) { + core.status.route.push("shop:" + shopId); + } + + // Step 3: 检查道具商店 or 公共事件 + if (shop.item) { + if (core.openItemShop) { + core.openItemShop(shopId); + } else { + core.playSound('操作失败'); + core.insertAction("道具商店插件不存在!请检查是否存在该插件!"); + } + return; + } + if (shop.commonEvent) { + core.insertCommonEvent(shop.commonEvent, shop.args); + return; + } + + _shouldProcessKeyUp = true; + + // Step 4: 执行标准公共商店 + core.insertAction(this._convertShop(shop)); + return true; + } + + /** + * @param {shop} shop + * 将一个全局商店转变成可预览的公共事件 + */ + core.plugin._convertShop = function (shop) { + return [ + { "type": "function", "function": "function() {core.addFlag('@temp@shop', 1);}" }, + { + "type": "while", + "condition": "true", + "data": [ + // 检测能否访问该商店 + { + "type": "if", + "condition": "core.isShopVisited('" + shop.id + "')", + "true": [ + // 可以访问,直接插入执行效果 + { "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', false) }" }, + ], + "false": [ + // 不能访问的情况下:检测能否预览 + { + "type": "if", + "condition": shop.disablePreview, + "true": [ + // 不可预览,提示并退出 + { "type": "playSound", "name": "操作失败" }, + "当前无法访问该商店!", + { "type": "break" }, + ], + "false": [ + // 可以预览:将商店全部内容进行替换 + { "type": "tip", "text": "当前处于预览模式,不可购买" }, + { "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', true) }" }, + ] + } + ] + } + ] + }, + { "type": "function", "function": "function() {core.addFlag('@temp@shop', -1);}" } + ]; + } + + /** + * @param {string} shopId + * @param {boolean} previewMode + */ + core.plugin._convertShop_replaceChoices = function (shopId, previewMode) { + var shop = core.status.shops[shopId]; + var choices = (shop.choices || []).filter(function (choice) { + if (choice.condition == null || choice.condition == '') return true; + try { return core.calValue(choice.condition); } catch (e) { return true; } + }).map(function (choice) { + var ableToBuy = core.calValue(choice.need); + return { + "text": choice.text, + "icon": choice.icon, + "color": ableToBuy && !previewMode ? choice.color : [153, 153, 153, 1], + "action": ableToBuy && !previewMode ? [{ "type": "playSound", "name": "商店" }].concat(choice.action) : [ + { "type": "playSound", "name": "操作失败" }, + { "type": "tip", "text": previewMode ? "预览模式下不可购买" : "购买条件不足" } + ] + }; + }).concat({ "text": "离开", "action": [{ "type": "playSound", "name": "取消" }, { "type": "break" }] }); + core.insertAction({ "type": "choices", "text": shop.text, "choices": choices }); + } + + /** + * 是否访问过某个快捷商店 + */ + core.plugin.isShopVisited = function (id) { + if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {}); + const shops = core.getFlag("__shops__"); + if (!shops[id]) shops[id] = {}; + return shops[id].visited; + } + + /// 当前应当显示的快捷商店列表 + core.plugin.listShopIds = function () { + return Object.keys(core.status.shops).filter(function (id) { + return core.isShopVisited(id) || !core.status.shops[id].mustEnable; + }); + } + + /** 是否能够打开某个商店 **/ + core.plugin.canOpenShop = function (id) { + if (this.isShopVisited(id)) return true; + const shop = core.status.shops[id]; + if (shop.item || shop.commonEvent || shop.mustEnable) return false; + return true; + } + + /** 启用或禁用某个快捷商店 */ + core.plugin.setShopVisited = function (id, visited) { + if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {}); + var shops = core.getFlag("__shops__"); + if (!shops[id]) shops[id] = {}; + if (visited) shops[id].visited = true; + else delete shops[id].visited; + } + + /** + * 能否使用快捷商店 + * @param {string} id + * @returns {'当前楼层不能使用快捷商店。'|null} + */ + core.plugin.canUseQuickShop = function (id) { + // 如果返回一个字符串,表示不能,字符串为不能使用的提示 + // 返回null代表可以使用 + + // 检查当前楼层的canUseQuickShop选项是否为false + if (core.status.thisMap.canUseQuickShop === false) + return '当前楼层不能使用快捷商店。'; + return null; + } + + let _shouldProcessKeyUp = true; + + /// 允许商店X键退出 + core.registerAction('keyUp', 'shops', function (keycode) { + if (!core.status.lockControl || core.status.event.id != 'action') return false; + if ((keycode == 13 || keycode == 32) && !_shouldProcessKeyUp) { + _shouldProcessKeyUp = true; + return true; + } + + if (!core.hasFlag("@temp@shop") || core.status.event.data.type != 'choices') return false; + var data = core.status.event.data.current; + var choices = data.choices; + var topIndex = core.actions._getChoicesTopIndex(choices.length); + if (keycode == 88 || keycode == 27) { // X, ESC + core.actions._clickAction(core.actions.HSIZE, topIndex + choices.length - 1); + return true; + } + return false; + }, 60); + + /// 允许长按空格或回车连续执行操作 + core.registerAction('keyDown', 'shops', function (keycode) { + if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false; + if (core.status.event.data.type != 'choices') return false; + core.status.onShopLongDown = true; + var data = core.status.event.data.current; + var choices = data.choices; + var topIndex = core.actions._getChoicesTopIndex(choices.length); + if (keycode == 13 || keycode == 32) { // Space, Enter + core.actions._clickAction(core.actions.HSIZE, topIndex + core.status.event.selection); + _shouldProcessKeyUp = false; + return true; + } + return false; + }, 60); + + // 允许长按屏幕连续执行操作 + core.registerAction('longClick', 'shops', function (x, y, px, py) { + if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false; + if (core.status.event.data.type != 'choices') return false; + var data = core.status.event.data.current; + var choices = data.choices; + var topIndex = core.actions._getChoicesTopIndex(choices.length); + if (x >= core.actions.CHOICES_LEFT && x <= core.actions.CHOICES_RIGHT && y >= topIndex && y < topIndex + choices.length) { + core.actions._clickAction(x, y); + return true; + } + return false; + }, 60); + }, + "removeMap": function () { + // 高层塔砍层插件,删除后不会存入存档,不可浏览地图也不可飞到。 + // 推荐用法: + // 对于超高层或分区域塔,当在1区时将2区以后的地图删除;1区结束时恢复2区,进二区时删除1区地图,以此类推 + // 这样可以大幅减少存档空间,以及加快存读档速度 + + /** + * 删除楼层 + * @example // 删除MT1~MT300之间的全部层 + * core.removeMaps("MT1", "MT300") + * @example // 只删除MT10层 + * core.removeMaps("MT10") + * @param {string} fromId + * @param {string} toId + */ + function removeMaps(fromId, toId) { + toId = toId || fromId; + var fromIndex = core.floorIds.indexOf(fromId), + toIndex = core.floorIds.indexOf(toId); + if (toIndex < 0) toIndex = core.floorIds.length - 1; + flags.__visited__ = flags.__visited__ || {}; + flags.__removed__ = flags.__removed__ || []; + flags.__disabled__ = flags.__disabled__ || {}; + flags.__leaveLoc__ = flags.__leaveLoc__ || {}; + for (var i = fromIndex; i <= toIndex; ++i) { + var floorId = core.floorIds[i]; + if (core.status.maps[floorId].deleted) continue; + delete flags.__visited__[floorId]; + flags.__removed__.push(floorId); + delete flags.__disabled__[floorId]; + delete flags.__leaveLoc__[floorId]; + (core.status.autoEvents || []).forEach(function (event) { + if (event.floorId == floorId && event.currentFloor) { + core.autoEventExecuting(event.symbol, false); + core.autoEventExecuted(event.symbol, false); + } + }); + core.status.maps[floorId].deleted = true; + core.status.maps[floorId].canFlyTo = false; + core.status.maps[floorId].canFlyFrom = false; + core.status.maps[floorId].cannotViewMap = true; + } + } + core.plugin.removeMaps = removeMaps; + + /** + * 恢复楼层 + * @example // 恢复MT1~MT300之间的全部层 + * core.resumeMaps("MT1", "MT300") + * @example // 只恢复MT10层 + * core.resumeMaps("MT10") + * @param {string} fromId + * @param {string} toId + */ + function resumeMaps(fromId, toId) { + toId = toId || fromId; + var fromIndex = core.floorIds.indexOf(fromId), + toIndex = core.floorIds.indexOf(toId); + if (toIndex < 0) toIndex = core.floorIds.length - 1; + flags.__removed__ = flags.__removed__ || []; + for (var i = fromIndex; i <= toIndex; ++i) { + var floorId = core.floorIds[i]; + if (!core.status.maps[floorId].deleted) continue; + flags.__removed__ = flags.__removed__.filter(function (f) { return f != floorId; }); + core.status.maps[floorId] = core.loadFloor(floorId); + } + } + core.plugin.resumeMaps = resumeMaps; + + /** 分区砍层相关 + * @param {string} floorId + */ + const inAnyPartition = function (floorId) { + let inPartition = false; + (core.floorPartitions || []).forEach(function (floor) { + const fromIndex = core.floorIds.indexOf(floor[0]); + let toIndex = core.floorIds.indexOf(floor[1]); + const index = core.floorIds.indexOf(floorId); + if (fromIndex < 0 || index < 0) return; + if (toIndex < 0) toIndex = core.floorIds.length - 1; + if (index >= fromIndex && index <= toIndex) inPartition = true; + }); + return inPartition; + } + + /** + * 分区砍层 + * @param {string} floorId + **/ + function autoRemoveMaps(floorId) { + if (main.mode != 'play' || !inAnyPartition(floorId)) return; + // 根据分区信息自动砍层与恢复 + (core.floorPartitions || []).forEach(function (floor) { + var fromIndex = core.floorIds.indexOf(floor[0]); + var toIndex = core.floorIds.indexOf(floor[1]); + var index = core.floorIds.indexOf(floorId); + if (fromIndex < 0 || index < 0) return; + if (toIndex < 0) toIndex = core.floorIds.length - 1; + if (index >= fromIndex && index <= toIndex) { + core.resumeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]); + } else { + core.removeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]); + } + }); + } + core.plugin.autoRemoveMaps = autoRemoveMaps; + }, + "fiveLayers": function () { + // 是否启用五图层(增加背景2层和前景2层) 将__enable置为true即会启用;启用后请保存后刷新编辑器 + // 背景层2将会覆盖背景层 被事件层覆盖 前景层2将会覆盖前景层 + // 另外 请注意加入两个新图层 会让大地图的性能降低一些 + // 插件作者:ad + var __enable = false; + if (!__enable) return; + + // 创建新图层 + function createCanvas(name, zIndex) { + if (!name) return; + const canvas = document.createElement('canvas'); + canvas.id = name; + canvas.className = 'gameCanvas'; + // 编辑器模式下设置zIndex会导致加入的图层覆盖优先级过高 + if (main.mode != "editor") canvas.style.zIndex = zIndex || 0; + // 将图层插入进游戏内容 + document.getElementById('gameDraw')?.appendChild(canvas); + const ctx = canvas.getContext('2d'); + if (ctx) core.canvas[name] = ctx; + canvas.width = core.__PIXELS__; + canvas.height = core.__PIXELS__; + return canvas; + } + + const bg2Canvas = createCanvas('bg2', 20); + const fg2Canvas = createCanvas('fg2', 63); + // 大地图适配 + core.bigmap.canvas = ["bg2", "fg2", "bg", "event", "event2", "fg", "damage"]; + core.initStatus.bg2maps = {}; + core.initStatus.fg2maps = {}; + + if (main.mode == 'editor') { + /*插入编辑器的图层 不做此步新增图层无法在编辑器显示*/ + // 编辑器图层覆盖优先级 eui > efg > fg(前景层) > event2(48*32图块的事件层) > event(事件层) > bg(背景层) + // 背景层2(bg2) 插入事件层(event)之前(即bg与event之间) + if (bg2Canvas) document.getElementById('mapEdit')?.insertBefore(bg2Canvas, document.getElementById('event')); + // 前景层2(fg2) 插入编辑器前景(efg)之前(即fg之后) + if (fg2Canvas) document.getElementById('mapEdit')?.insertBefore(fg2Canvas, document.getElementById('ebm')); + // 原本有三个图层 从4开始添加 + var num = 4; + // 新增图层存入editor.dom中 + editor.dom.bg2c = core.canvas.bg2.canvas; + editor.dom.bg2Ctx = core.canvas.bg2; + editor.dom.fg2c = core.canvas.fg2.canvas; + editor.dom.fg2Ctx = core.canvas.fg2; + editor.dom.maps.push('bg2map', 'fg2map'); + editor.dom.canvas.push('bg2', 'fg2'); + + // 创建编辑器上的按钮 + const createCanvasBtn = function (name) { + // 电脑端创建按钮 + const input = document.createElement('input'); + // layerMod4/layerMod5 + const id = 'layerMod' + num++; + // bg2map/fg2map + const value = name + 'map'; + input.type = 'radio'; + input.name = 'layerMod'; + input.id = id; + input.value = value; + editor.dom[id] = input; + input.onchange = function () { + editor.uifunctions.setLayerMod(value); + } + return input; + }; + + const createCanvasBtn_mobile = function (name) { + // 手机端往选择列表中添加子选项 + const input = document.createElement('option'); + const id = 'layerMod' + num++; + const value = name + 'map'; + input.name = 'layerMod'; + input.value = value; + editor.dom[id] = input; + return input; + }; + if (!editor.isMobile) { + const input = createCanvasBtn('bg2'); + const input2 = createCanvasBtn('fg2'); + // 获取事件层及其父节点 + const child = document.getElementById('layerMod'), + parent = child?.parentNode; + if (parent) { + // 背景层2插入事件层前 + parent.insertBefore(input, child); + // 不能直接更改背景层2的innerText 所以创建文本节点 + var txt = document.createTextNode('bg2'); + // 插入事件层前(即新插入的背景层2前) + parent.insertBefore(txt, child); + // 向最后插入前景层2(即插入前景层后) + parent.appendChild(input2); + var txt2 = document.createTextNode('fg2'); + parent.appendChild(txt2); + parent.childNodes[2].replaceWith("bg"); + parent.childNodes[6].replaceWith("事件"); + parent.childNodes[8].replaceWith("fg"); + } + } else { + const input = createCanvasBtn_mobile('bg2'); + const input2 = createCanvasBtn_mobile('fg2'); + // 手机端因为是选项 所以可以直接改innerText + input.innerText = '背景层2'; + input2.innerText = '前景层2'; + const parent = document.getElementById('layerMod'); + if (parent) { + parent.insertBefore(input, parent.children[1]); + parent.appendChild(input2); + } + } + } + + var _loadFloor_doNotCopy = core.maps._loadFloor_doNotCopy; + core.maps._loadFloor_doNotCopy = function () { + return ["bg2map", "fg2map"].concat(_loadFloor_doNotCopy()); + } + ////// 绘制背景和前景层 ////// + core.maps._drawBg_draw = function (floorId, toDrawCtx, cacheCtx, config) { + config.ctx = cacheCtx; + core.maps._drawBg_drawBackground(floorId, config); + // ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制背景图块;后绘制的覆盖先绘制的。 + core.maps._drawFloorImages(floorId, config.ctx, 'bg', null, null, config.onMap); + core.maps._drawBgFgMap(floorId, 'bg', config); + if (config.onMap) { + core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); + core.clearMap('bg2'); + core.clearMap(cacheCtx); + } + core.maps._drawBgFgMap(floorId, 'bg2', config); + if (config.onMap) core.drawImage('bg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); + config.ctx = toDrawCtx; + } + core.maps._drawFg_draw = function (floorId, toDrawCtx, cacheCtx, config) { + config.ctx = cacheCtx; + // ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制前景图块;后绘制的覆盖先绘制的。 + core.maps._drawFloorImages(floorId, config.ctx, 'fg', null, null, config.onMap); + core.maps._drawBgFgMap(floorId, 'fg', config); + if (config.onMap) { + core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); + core.clearMap('fg2'); + core.clearMap(cacheCtx); + } + core.maps._drawBgFgMap(floorId, 'fg2', config); + if (config.onMap) core.drawImage('fg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0); + config.ctx = toDrawCtx; + } + ////// 移动判定 ////// + core.maps._generateMovableArray_arrays = function (floorId) { + return { + bgArray: this.getBgMapArray(floorId), + fgArray: this.getFgMapArray(floorId), + eventArray: this.getMapArray(floorId), + bg2Array: this._getBgFgMapArray('bg2', floorId), + fg2Array: this._getBgFgMapArray('fg2', floorId) + }; + } + + // @todo 测试五图层插件在此处是否表现正常 + // @todo 五图层配合autotile是否有bug 待测试 + // 楼层贴图绘制 + core.maps._drawFloorImage = function (ctx, name, one, image, currStatus, onMap) { + var height = image.height; + var imageName = one.name + (one.reverse || ''); + var width = parseInt((one.w == null ? image.width : one.w) / (one.frame || 1)); + var height = one.h == null ? image.height : one.h; + var sx = (one.sx || 0) + (currStatus || 0) % (one.frame || 1) * width; + var sy = one.sy || 0; + var x = one.x || 0, y = one.y || 0; + if (onMap && core.bigmap.v2) { + if (x > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || x + width < 32 * core.bigmap.posX - 32 || + y > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || y + height < 32 * core.bigmap.posY - 32) { + return; + } + x -= 32 * core.bigmap.posX; + y -= 32 * core.bigmap.posY; + } + + if (one.canvas != 'auto' && one.canvas != name) return; + if (one.canvas != 'auto') { + if (currStatus != null) core.clearMap(ctx, x, y, width, height); + core.drawImage(ctx, imageName, sx, sy, width, height, x, y, width, height); + } else { + if (name === 'bg' || name === 'bg2') { + if (currStatus != null) core.clearMap(ctx, x, y + height - 32, width, 32); + core.drawImage(ctx, imageName, sx, sy + height - 32, width, 32, x, y + height - 32, width, 32); + } else if (name == 'fg' || name === 'fg2') { + if (currStatus != null) core.clearMap(ctx, x, y, width, height - 32); + core.drawImage(ctx, imageName, sx, sy, width, height - 32, x, y, width, height - 32); + } + } + } + }, + "itemShop": function () { + // 道具商店相关的插件 + // 可在全塔属性-全局商店中使用「道具商店」事件块进行编辑(如果找不到可以在入口方块中找) + + var shopId = null; // 当前商店ID + var type = 0; // 当前正在选中的类型,0买入1卖出 + /** 当前正在选中的道具 + * @type {number | null} + */ + var selectItem = 0; + var selectCount = 0; // 当前已经选中的数量 + var page = 0; + var totalPage = 0; + var totalMoney = 0; + var list = []; + var shopInfo = null; // 商店信息 + var choices = []; // 商店选项 + var use = 'money'; + var useText = '金币'; + + var bigFont = core.ui._buildFont(20, false), + middleFont = core.ui._buildFont(18, false); + + this._drawItemShop = function () { + // 绘制道具商店 + + // Step 1: 背景和固定的几个文字 + core.ui._createUIEvent(); + core.clearMap('uievent'); + core.ui.clearUIEventSelector(); + core.setTextAlign('uievent', 'left'); + core.setTextBaseline('uievent', 'top'); + core.fillRect('uievent', 0, 0, 416, 416, 'black'); + core.drawWindowSkin('winskin.png', 'uievent', 0, 0, 416, 56); + core.drawWindowSkin('winskin.png', 'uievent', 0, 56, 312, 56); + core.drawWindowSkin('winskin.png', 'uievent', 0, 112, 312, 304); + core.drawWindowSkin('winskin.png', 'uievent', 312, 56, 104, 56); + core.drawWindowSkin('winskin.png', 'uievent', 312, 112, 104, 304); + core.setFillStyle('uievent', 'white'); + core.setStrokeStyle('uievent', 'white'); + core.fillText("uievent", "购买", 32, 74, 'white', bigFont); + core.fillText("uievent", "卖出", 132, 74); + core.fillText("uievent", "离开", 232, 74); + core.fillText("uievent", "当前" + useText, 324, 66, null, middleFont); + core.setTextAlign("uievent", "right"); + core.fillText("uievent", core.formatBigNumber(core.status.hero[use]), 405, 89); + core.setTextAlign("uievent", "left"); + core.ui.drawUIEventSelector(1, "winskin.png", 22 + 100 * type, 66, 60, 33); + if (selectItem != null) { + core.setTextAlign('uievent', 'center'); + core.fillText("uievent", type == 0 ? "买入个数" : "卖出个数", 364, 320, null, bigFont); + core.fillText("uievent", "< " + selectCount + " >", 364, 350); + core.fillText("uievent", "确定", 364, 380); + } + + // Step 2:获得列表并展示 + list = choices.filter(function (one) { + if (one.condition != null && one.condition != '') { + try { if (!core.calValue(one.condition)) return false; } catch (e) { } + } + return (type == 0 && one.money != null) || (type == 1 && one.sell != null); + }); + var per_page = 6; + totalPage = Math.ceil(list.length / per_page); + page = Math.floor((selectItem || 0) / per_page) + 1; + + // 绘制分页 + if (totalPage > 1) { + var half = 156; + core.setTextAlign('uievent', 'center'); + core.fillText('uievent', page + " / " + totalPage, half, 388, null, middleFont); + if (page > 1) core.fillText('uievent', '上一页', half - 80, 388); + if (page < totalPage) core.fillText('uievent', '下一页', half + 80, 388); + } + core.setTextAlign('uievent', 'left'); + + // 绘制每一项 + var start = (page - 1) * per_page; + for (var i = 0; i < per_page; ++i) { + var curr = start + i; + if (curr >= list.length) break; + var item = list[curr]; + core.drawIcon('uievent', item.id, 10, 125 + i * 40); + core.setTextAlign('uievent', 'left'); + core.fillText('uievent', core.material.items[item.id].name, 50, 132 + i * 40, null, bigFont); + core.setTextAlign('uievent', 'right'); + core.fillText('uievent', (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)) + useText + "/个", 300, 133 + i * 40, null, middleFont); + core.setTextAlign("uievent", "left"); + if (curr == selectItem) { + // 绘制描述,文字自动放缩 + var text = core.material.items[item.id].text || "该道具暂无描述"; + try { text = core.replaceText(text); } catch (e) { } + for (var fontSize = 20; fontSize >= 8; fontSize -= 2) { + var config = { left: 10, fontSize: fontSize, maxWidth: 403 }; + var height = core.getTextContentHeight(text, config); + if (height <= 50) { + config.top = (56 - height) / 2; + core.drawTextContent("uievent", text, config); + break; + } + } + core.ui.drawUIEventSelector(2, "winskin.png", 8, 120 + i * 40, 295, 40); + if (type == 0 && item.number != null) { + core.fillText("uievent", "存货", 324, 132, null, bigFont); + core.setTextAlign("uievent", "right"); + core.fillText("uievent", item.number, 406, 132, null, null, 40); + } else if (type == 1) { + core.fillText("uievent", "数量", 324, 132, null, bigFont); + core.setTextAlign("uievent", "right"); + core.fillText("uievent", core.itemCount(item.id), 406, 132, null, null, 40); + } + core.setTextAlign("uievent", "left"); + core.fillText("uievent", "预计" + useText, 324, 250); + core.setTextAlign("uievent", "right"); + totalMoney = selectCount * (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)); + core.fillText("uievent", core.formatBigNumber(totalMoney), 405, 280); + + core.setTextAlign("uievent", "left"); + core.fillText("uievent", type == 0 ? "已购次数" : "已卖次数", 324, 170); + core.setTextAlign("uievent", "right"); + core.fillText("uievent", (type == 0 ? item.money_count : item.sell_count) || 0, 405, 200); + } + } + + core.setTextAlign('uievent', 'left'); + core.setTextBaseline('uievent', 'alphabetic'); + } + + var _add = function (item, delta) { + if (item == null) return; + selectCount = core.clamp( + selectCount + delta, 0, + Math.min(type == 0 ? Math.floor(core.status.hero[use] / core.calValue(item.money)) : core.itemCount(item.id), + type == 0 && item.number != null ? item.number : Number.MAX_SAFE_INTEGER) + ); + } + + var _confirm = function (item) { + if (item == null || selectCount == 0) return; + if (type == 0) { + core.status.hero[use] -= totalMoney; + core.getItem(item.id, selectCount); + core.stopSound(); + core.playSound('确定'); + if (item.number != null) item.number -= selectCount; + item.money_count = (item.money_count || 0) + selectCount; + } else { + core.status.hero[use] += totalMoney; + core.removeItem(item.id, selectCount); + core.playSound('确定'); + core.drawTip("成功卖出" + selectCount + "个" + core.material.items[item.id].name, item.id); + if (item.number != null) item.number += selectCount; + item.sell_count = (item.sell_count || 0) + selectCount; + } + selectCount = 0; + } + + this._performItemShopKeyBoard = function (keycode) { + var item = list[selectItem] || null; + // 键盘操作 + switch (keycode) { + case 38: // up + if (selectItem == null) break; + if (selectItem == 0) selectItem = null; + else selectItem--; + selectCount = 0; + break; + case 37: // left + if (selectItem == null) { + if (type > 0) type--; + break; + } + _add(item, -1); + break; + case 39: // right + if (selectItem == null) { + if (type < 2) type++; + break; + } + _add(item, 1); + break; + case 40: // down + if (selectItem == null) { + if (list.length > 0) selectItem = 0; + break; + } + if (list.length == 0) break; + selectItem = Math.min(selectItem + 1, list.length - 1); + selectCount = 0; + break; + case 13: + case 32: // Enter/Space + if (selectItem == null) { + if (type == 2) + core.insertAction({ "type": "break" }); + else if (list.length > 0) + selectItem = 0; + break; + } + _confirm(item); + break; + case 27: // ESC + if (selectItem == null) { + core.insertAction({ "type": "break" }); + break; + } + selectItem = null; + break; + } + } + + this._performItemShopClick = function (px, py) { + var item = list[selectItem] || null; + // 鼠标操作 + if (px >= 22 && px <= 82 && py >= 71 && py <= 102) { + // 买 + if (type != 0) { + type = 0; + selectItem = null; + selectCount = 0; + } + return; + } + if (px >= 122 && px <= 182 && py >= 71 && py <= 102) { + // 卖 + if (type != 1) { + type = 1; + selectItem = null; + selectCount = 0; + } + return; + } + if (px >= 222 && px <= 282 && py >= 71 && py <= 102) // 离开 + return core.insertAction({ "type": "break" }); + // < > + if (px >= 318 && px <= 341 && py >= 348 && py <= 376) + return _add(item, -1); + if (px >= 388 && px <= 416 && py >= 348 && py <= 376) + return _add(item, 1); + // 确定 + if (px >= 341 && px <= 387 && py >= 380 && py <= 407) + return _confirm(item); + + // 上一页/下一页 + if (px >= 45 && px <= 105 && py >= 388) { + if (page > 1) { + selectItem -= 6; + selectCount = 0; + } + return; + } + if (px >= 208 && px <= 268 && py >= 388) { + if (page < totalPage) { + selectItem = Math.min(selectItem + 6, list.length - 1); + selectCount = 0; + } + return; + } + + // 实际区域 + if (px >= 9 && px <= 300 && py >= 120 && py < 360) { + if (list.length == 0) return; + var index = parseInt((py - 120) / 40); + var newItem = 6 * (page - 1) + index; + if (newItem >= list.length) newItem = list.length - 1; + if (newItem != selectItem) { + selectItem = newItem; + selectCount = 0; + } + return; + } + } + + this._performItemShopAction = function () { + if (flags.type == 0) return this._performItemShopKeyBoard(flags.keycode); + else return this._performItemShopClick(flags.px, flags.py); + } + + this.openItemShop = function (itemShopId) { + shopId = itemShopId; + type = 0; + page = 0; + selectItem = null; + selectCount = 0; + core.isShopVisited(itemShopId); + shopInfo = flags.__shops__[shopId]; + if (shopInfo.choices == null) shopInfo.choices = core.clone(core.status.shops[shopId].choices); + choices = shopInfo.choices; + use = core.status.shops[shopId].use; + if (use != 'exp') use = 'money'; + useText = use == 'money' ? '金币' : '经验'; + + core.insertAction([{ + "type": "while", + "condition": "true", + "data": [ + { "type": "function", "function": "function () { core.plugin._drawItemShop(); }" }, + { "type": "wait" }, + { "type": "function", "function": "function() { core.plugin._performItemShopAction(); }" } + ] + }, + { + "type": "function", + "function": "function () { core.deleteCanvas('uievent'); core.ui.clearUIEventSelector(); }" + } + ]); + } + + }, + "enemyLevel": function () { + // 此插件将提供怪物手册中的怪物境界显示 + // 使用此插件需要先给每个怪物定义境界,方法如下: + // 点击怪物的【配置表格】,找到“【怪物】相关的表格配置”,然后在【名称】仿照增加境界定义: + /* + "level": { + "_leaf": true, + "_type": "textarea", + "_string": true, + "_data": "境界" + }, + */ + // 然后保存刷新,可以看到怪物的属性定义中出现了【境界】。再开启本插件即可。 + + // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 + var __enable = false; + if (!__enable) return; + + // 这里定义每个境界的显示颜色;可以写'red', '#RRGGBB' 或者[r,g,b,a]四元数组 + var levelToColors = { + "萌新一阶": "red", + "萌新二阶": "#FF0000", + "萌新三阶": [255, 0, 0, 1], + }; + + // 复写 _drawBook_drawName + var originDrawBook = core.ui._drawBook_drawName; + core.ui._drawBook_drawName = function (index, enemy, top, left, width) { + // 如果没有境界,则直接调用原始代码绘制 + if (!enemy.level) return originDrawBook.call(core.ui, index, enemy, top, left, width); + // 存在境界,则额外进行绘制 + core.setTextAlign('ui', 'center'); + if (enemy.specialText.length == 0) { + core.fillText('ui', enemy.name, left + width / 2, + top + 27, '#DDDDDD', this._buildFont(17, true)); + core.fillText('ui', enemy.level, left + width / 2, + top + 51, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true)); + } else { + core.fillText('ui', enemy.name, left + width / 2, + top + 20, '#DDDDDD', this._buildFont(17, true), width); + switch (enemy.specialText.length) { + case 1: + core.fillText('ui', enemy.specialText[0], left + width / 2, + top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'), + this._buildFont(14, true), width); + break; + case 2: + // Step 1: 计算字体 + var text = enemy.specialText[0] + " " + enemy.specialText[1]; + core.setFontForMaxWidth('ui', text, width, this._buildFont(14, true)); + // Step 2: 计算总宽度 + var totalWidth = core.calWidth('ui', text); + var leftWidth = core.calWidth('ui', enemy.specialText[0]); + var rightWidth = core.calWidth('ui', enemy.specialText[1]); + // Step 3: 绘制 + core.fillText('ui', enemy.specialText[0], left + (width + leftWidth - totalWidth) / 2, + top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A')); + core.fillText('ui', enemy.specialText[1], left + (width + totalWidth - rightWidth) / 2, + top + 38, core.arrayToRGBA((enemy.specialColor || [])[1] || '#FF6A6A')); + break; + default: + core.fillText('ui', '多属性...', left + width / 2, + top + 38, '#FF6A6A', this._buildFont(14, true), width); + } + core.fillText('ui', enemy.level, left + width / 2, + top + 56, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true)); + } + } + + // 也可以复写其他的属性颜色如怪物攻防等,具体参见下面的例子的注释部分 + core.ui._drawBook_drawRow1 = function (index, enemy, top, left, width, position) { + // 绘制第一行 + core.setTextAlign('ui', 'left'); + var b13 = this._buildFont(13, true), + f13 = this._buildFont(13, false); + var col1 = left, + col2 = left + width * 9 / 25, + col3 = left + width * 17 / 25; + core.fillText('ui', '生命', col1, position, '#DDDDDD', f13); + core.fillText('ui', core.formatBigNumber(enemy.hp || 0), col1 + 30, position, /*'red' */ null, b13); + core.fillText('ui', '攻击', col2, position, null, f13); + core.fillText('ui', core.formatBigNumber(enemy.atk || 0), col2 + 30, position, /* '#FF0000' */ null, b13); + core.fillText('ui', '防御', col3, position, null, f13); + core.fillText('ui', core.formatBigNumber(enemy.def || 0), col3 + 30, position, /* [255, 0, 0, 1] */ null, b13); + } + + + }, + "multiHeros": function () { + // 多角色插件 + // Step 1: 启用本插件 + // Step 2: 定义每个新的角色各项初始数据(参见下方注释) + // Step 3: 在游戏中的任何地方都可以调用 `core.changeHero()` 进行切换;也可以 `core.changeHero(1)` 来切换到某个具体的角色上 + + // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 + var __enable = false; + if (!__enable) return; + + // 在这里定义全部的新角色属性 + // 请注意,在这里定义的内容不会多角色共用,在切换时会进行恢复。 + // 你也可以自行新增或删除,比如不共用金币则可以加上"money"的初始化,不共用道具则可以加上"items"的初始化, + // 多角色共用hp的话则删除hp,等等。总之,不共用的属性都在这里进行定义就好。 + var hero1 = { + "floorId": "MT0", // 该角色初始楼层ID;如果共用楼层可以注释此项 + "image": "brave.png", // 角色的行走图名称;此项必填不然会报错 + "name": "1号角色", + "lv": 1, + "hp": 10000, // 如果HP共用可注释此项 + "atk": 1000, + "def": 1000, + "mdef": 0, + // "money": 0, // 如果要不共用金币则取消此项注释 + // "exp": 0, // 如果要不共用经验则取消此项注释 + "loc": { "x": 0, "y": 0, "direction": "up" }, // 该角色初始位置;如果共用位置可注释此项 + "items": { + "tools": {}, // 如果共用消耗道具(含钥匙)则可注释此项 + // "constants": {}, // 如果不共用永久道具(如手册)可取消注释此项 + "equips": {}, // 如果共用在背包的装备可注释此项 + }, + "equipment": [], // 如果共用装备可注释此项;此项和上面的「共用在背包的装备」需要拥有相同状态,不然可能出现问题 + }; + // 也可以类似新增其他角色 + // 新增的角色,各项属性共用与不共用的选择必须和上面完全相同,否则可能出现问题。 + // var hero2 = { ... + + var heroCount = 2; // 包含默认角色在内总共多少个角色,该值需手动修改。 + + this.initHeros = function () { + core.setFlag("hero1", core.clone(hero1)); // 将属性值存到变量中 + // core.setFlag("hero2", core.clone(hero2)); // 更多的角色也存入变量中;每个定义的角色都需要新增一行 + + // 检测是否存在装备 + if (hero1.equipment) { + if (!hero1.items || !hero1.items.equips) { + alert('多角色插件的equipment和道具中的equips必须拥有相同状态!'); + } + // 存99号套装为全空 + var saveEquips = core.getFlag("saveEquips", []); + saveEquips[99] = []; + core.setFlag("saveEquips", saveEquips); + } else { + if (hero1.items && hero1.items.equips) { + alert('多角色插件的equipment和道具中的equips必须拥有相同状态!'); + } + } + } + + // 在游戏开始注入initHeros + var _startGame_setHard = core.events._startGame_setHard; + core.events._startGame_setHard = function () { + _startGame_setHard.call(core.events); + core.initHeros(); + } + + // 切换角色 + // 可以使用 core.changeHero() 来切换到下一个角色 + // 也可以 core.changeHero(1) 来切换到某个角色(默认角色为0) + this.changeHero = function (toHeroId) { + var currHeroId = core.getFlag("heroId", 0); // 获得当前角色ID + if (toHeroId == null) { + toHeroId = (currHeroId + 1) % heroCount; + } + if (currHeroId == toHeroId) return; + + var saveList = Object.keys(hero1); + + // 保存当前内容 + var toSave = {}; + // 暂时干掉 drawTip 和 音效,避免切装时的提示 + var _drawTip = core.ui.drawTip; + core.ui.drawTip = function () { }; + var _playSound = core.control.playSound; + core.control.playSound = function () { return undefined; }; + // 记录当前录像,因为可能存在换装问题 + core.clearRouteFolding(); + var routeLength = core.status.route.length; + // 优先判定装备 + if (hero1.equipment) { + core.items.quickSaveEquip(100 + currHeroId); + core.items.quickLoadEquip(99); + } + + saveList.forEach(function (name) { + if (name == 'floorId') toSave[name] = core.status.floorId; // 楼层单独设置 + else if (name == 'items') { + toSave.items = core.clone(core.status.hero.items); + Object.keys(toSave.items).forEach(function (one) { + if (!hero1.items[one]) delete toSave.items[one]; + }); + } else toSave[name] = core.clone(core.status.hero[name]); // 使用core.clone()来创建新对象 + }); + + core.setFlag("hero" + currHeroId, toSave); // 将当前角色信息进行保存 + var data = core.getFlag("hero" + toHeroId); // 获得要切换的角色保存内容 + + // 设置角色的属性值 + saveList.forEach(function (name) { + if (name == "items") { + Object.keys(core.status.hero.items).forEach(function (one) { + if (data.items[one]) core.status.hero.items[one] = core.clone(data.items[one]); + }); + } else { + core.status.hero[name] = core.clone(data[name]); + } + }); + // 最后装上装备 + if (hero1.equipment) { + core.items.quickLoadEquip(100 + toHeroId); + } + + core.ui.drawTip = _drawTip; + core.control.playSound = _playSound; + core.status.route = core.status.route.slice(0, routeLength); + core.control._bindRoutePush(); + + // 插入事件:改变角色行走图并进行楼层切换 + var toFloorId = data.floorId || core.status.floorId; + var toLoc = data.loc || core.status.hero.loc; + core.insertAction([ + { "type": "setHeroIcon", "name": data.image || "hero.png" }, // 改变行走图 + // 同层则用changePos,不同层则用changeFloor;这是为了避免共用楼层造成触发eachArrive + toFloorId != core.status.floorId ? { + "type": "changeFloor", + "floorId": toFloorId, + "loc": [toLoc.x, toLoc.y], + "direction": toLoc.direction, + "time": 0 // 可以在这里设置切换时间 + } : { "type": "changePos", "loc": [toLoc.x, toLoc.y], "direction": toLoc.direction } + // 你还可以在这里执行其他事件,比如增加或取消跟随效果 + ]); + core.setFlag("heroId", toHeroId); // 保存切换到的角色ID + } + }, + "heroFourFrames": function () { + // 样板的勇士/跟随者移动时只使用2、4两帧,观感较差。本插件可以将四帧全用上。 + + // 是否启用本插件 + var __enable = false; + if (!__enable) return; + + ["up", "down", "left", "right"].forEach(function (one) { + // 指定中间帧动画 + core.material.icons.hero[one].midFoot = 2; + }); + + var heroMoving = function (timestamp) { + if (core.status.heroMoving <= 0) return; + if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) { + core.animateFrame.leftLeg++; + core.animateFrame.moveTime = timestamp; + } + core.drawHero(['stop', 'leftFoot', 'midFoot', 'rightFoot'][core.animateFrame.leftLeg % 4], 4 * core.status.heroMoving); + } + core.registerAnimationFrame('heroMoving', true, heroMoving); + + core.events._eventMoveHero_moving = function (step, moveSteps) { + var curr = moveSteps[0]; + var direction = curr[0], x = core.getHeroLoc('x'), y = core.getHeroLoc('y'); + // ------ 前进/后退 + var o = direction == 'backward' ? -1 : 1; + if (direction == 'forward' || direction == 'backward') direction = core.getHeroLoc('direction'); + var faceDirection = direction; + if (direction == 'leftup' || direction == 'leftdown') faceDirection = 'left'; + if (direction == 'rightup' || direction == 'rightdown') faceDirection = 'right'; + core.setHeroLoc('direction', direction); + if (curr[1] <= 0) { + core.setHeroLoc('direction', faceDirection); + moveSteps.shift(); + return true; + } + if (step <= 4) core.drawHero('stop', 4 * o * step); + else if (step <= 8) core.drawHero('leftFoot', 4 * o * step); + else if (step <= 12) core.drawHero('midFoot', 4 * o * (step - 8)); + else if (step <= 16) core.drawHero('rightFoot', 4 * o * (step - 8)); // if (step == 8) { + if (step == 8 || step == 16) { + core.setHeroLoc('x', x + o * core.utils.scan2[direction].x, true); + core.setHeroLoc('y', y + o * core.utils.scan2[direction].y, true); + core.updateFollowers(); + curr[1]--; + if (curr[1] <= 0) moveSteps.shift(); + core.setHeroLoc('direction', faceDirection); + return step == 16; + } + return false; + } + }, + "startCanvas": function () { + // 使用本插件可以将自绘的标题界面居中。仅在【标题开启事件化】后才有效。 + // 由于一些技术性的原因,标题界面事件化无法应用到覆盖状态栏的整个界面。 + // 这是一个较为妥协的插件,会在自绘标题界面时隐藏状态栏、工具栏和边框,并将画布进行居中。 + // 本插件仅在全塔属性的 "startCanvas" 生效;进入 "startText" 时将会离开居中状态,回归正常界面。 + + // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 + var __enable = false; + if (!__enable) return; + + // 检查【标题开启事件化】是否开启 + if (!core.flags.startUsingCanvas || main.mode != 'play') return; + + var _isTitleCanvasEnabled = false; + var _getClickLoc = core.actions._getClickLoc; + this._setTitleCanvas = function () { + if (_isTitleCanvasEnabled) return; + _isTitleCanvasEnabled = true; + + // 禁用窗口resize + window.onresize = function () { }; + core.resize = function () { } + + // 隐藏状态栏 + core.dom.statusBar.style.display = 'none'; + core.dom.statusCanvas.style.display = 'none'; + core.dom.toolBar.style.display = 'none'; + // 居中画布 + if (core.domStyle.isVertical) { + core.dom.gameDraw.style.top = + (parseInt(core.dom.gameGroup.style.height) - parseInt(core.dom.gameDraw.style.height)) / 2 + "px"; + } else { + core.dom.gameDraw.style.right = + (parseInt(core.dom.gameGroup.style.width) - parseInt(core.dom.gameDraw.style.width)) / 2 + "px"; + } + core.dom.gameDraw.style.border = '3px transparent solid'; + core.actions._getClickLoc = function (x, y) { + var left = core.dom.gameGroup.offsetLeft + core.dom.gameDraw.offsetLeft + 3; + var top = core.dom.gameGroup.offsetTop + core.dom.gameDraw.offsetTop + 3; + var loc = { 'x': Math.max(x - left, 0), 'y': Math.max(y - top, 0), 'size': 32 * core.domStyle.scale }; + return loc; + } + } + + this._resetTitleCanvas = function () { + if (!_isTitleCanvasEnabled) return; + _isTitleCanvasEnabled = false; + window.onresize = function () { try { main.core.resize(); } catch (ee) { console.error(ee) } } + core.resize = function () { return core.control.resize(); } + core.resize(); + core.actions._getClickLoc = _getClickLoc; + } + + // 复写“开始游戏” + core.events._startGame_start = function (hard, seed, route, callback) { + console.log('开始游戏'); + core.resetGame(core.firstData.hero, hard, null, core.cloneArray(core.initStatus.maps)); + core.setHeroLoc('x', -1); + core.setHeroLoc('y', -1); + + if (seed != null) { + core.setFlag('__seed__', seed); + core.setFlag('__rand__', seed); + } else core.utils.__init_seed(); + + core.clearStatusBar(); + core.plugin._setTitleCanvas(); + + var todo = []; + core.hideStatusBar(); + core.push(todo, core.firstData.startCanvas); + core.push(todo, { "type": "function", "function": "function() { core.plugin._resetTitleCanvas(); core.events._startGame_setHard(); }" }) + core.push(todo, core.firstData.startText); + this.insertAction(todo, null, null, function () { + core.events._startGame_afterStart(callback); + }); + + if (route != null) core.startReplay(route); + } + + var _loadData = core.control.loadData; + core.control.loadData = function (data, callback) { + core.plugin._resetTitleCanvas(); + _loadData.call(core.control, data, callback); + } + }, + "platFly": function () { + // 本插件可以给平面塔启用一个带小地图的楼传,默认关闭 + // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 + var __enable = false; + if (!__enable) return; + + /* 第一步:复制到插件 + * 第二步:将flag:usePlatFly改为true可将带地图的楼传启用 + * 第三步:将flag:__useMinimap__设为true可开启小地图,该操作需切换楼层 + * 修改小地图的缩放可以修改flag:userScale,默认为1 + * 注意:楼层转换必须使用楼层坐标,而不是前一楼、后一楼 + * 关于操作:上楼和下楼操作为PgUp和PgDn,后退10层和前进10层操作为,(<)和.(>) + * 注意:请尽量保证地图传送的地方物理位置正好对准(即把地图拼接上箭头位置恰好对齐)否则连线的位置可能不准 + * 分区说明:用普通事件的楼层转换(红点)代替楼层转换(绿点)即可 + * 说明:紫色表示目前可以到达却没有到达过的地图(所以这是一个具有探索性的地图插件) + */ + // 录像验证直接干掉这个插件 + if (main.replayChecking) return; + // *** --- 以下数据为用户可修改数据 修改后不影响该插件的基本功能 --- *** // + // 检测楼层转换的图块id + var leftPortal = "leftPortal", // 左 + rightPortal = "rightPortal", // 右 + upPortal = "upPortal", // 上 + downPortal = "downPortal", // 下 + upFloor = "upFloor", // 上楼 + downFloor = "downFloor"; // 下楼 + // 一些常用默认值 + var defaultScale = 1, // 默认缩放比率 + defaultLoop = 5, // 绘制地图时的循环检测地图路线次数,loop为5说明最远的地图可以用6步到达 + defaultOpacity = 0.6, // 默认不透明度 + defaultMinorAlpha = defaultOpacity / 2; // 3D绘图时,不与当前层处于同一高度层的默认初始不透明度,不透明度会随着层数的增加或减少而减少 + // *** --- 用户修改区 END --- *** // + // 其余可自定义内容均用 //***--- 包裹着 + + /* -----------------------------------下面是一些调用案例 + * + * 如果想在状态栏显示可以在状态栏自绘里这么写: + core.drawFlyMap(ctx, 你想要的左上角横坐标, 你想要的左上角纵坐标, 你想要的宽度, 你想要的高度, core.status.floorId, {fromUser: true, opacity: 1, loop: 3, use3D:false}); + * + * -----------------------------------以下为高深区域,如果不必要可以不看 + * + * 主要函数说明(这几个函数异常强大,善用可以有弄一些有意思的东西,既然作者都说强大了,那就是特别强大) + * core.drawFlyMap(ctx : string, x?: number, y?: number, width?:number, height?:number, floorId?:string, config?:any): void + * 参数说明: + * ctx:画布,如果不存在则会新建x,y,width,height参数的画布 + * x,y,width,height:画布不存在时创建该数据的画布,如果存在,那么在画布的x,y位置绘制宽度为width,高度为height的地图 + * floorId:以该楼层为中心绘制 + * config:配制参数,包括以下内容(该参数为对象) + * fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层) + * oriFloor:这个不用管就好,具体原理比较复杂,不填或者和floorId一样就行 + * scale:缩放比率,这个是绝对比率,与画布大小无关,默认为1 + * interval:地图间的间距,默认为width / 24 + * deltaH:3D地图的纵向高度差,默认为绘制高度(height)/ 4 + * noErase:是否不清空画布再绘制,默认为false + * fromMini:是否是在小地图上绘制时调用的(无自动缩放和自动定位) + * loop:绘制层数,fromUser为true时才有用,默认为5 + * opacity:不透明度,默认为0.6 + * minorAlpha:3D地图的不与当前层在同一高度的楼层的初始不透明度,默认为opacity/2 + * layer:平面模式下绘制的楼层高度,当前所在层为0,上楼则+1,下楼则-1,默认为0,只有当use3D为false且可以绘制3D地图时有效 + * use3D:是否启用3D绘图,前提是所有地图不都在同一高度,默认为false + * reLeft:3D重新定位画布的左位置,默认为-240 + * reTop:3D重新定位画布的上位置,默认为-240 + * clearCache:是否清除缓存重算,默认为false + * map:这个是最强大的功能了,可以将自定义地图插入,以绘制自定义地图 + * 自定义地图格式:{"left_0_7":"MT1", "right_2_7":"MT2"}以此类推也可以"left_0_7,right_3_7,up_1_0"这种套娃,使用的前提是指定位置得有相应方向的箭头,不然会出错,注意楼层id是到往的楼层的id,如果想绘制3D地图,可以这么写:{"top_1_3,left_0_7": "1_MT1"},前面的1_必须写,上楼为top,下楼为bottom + * 默认取自floorId地图 + * 如有疑问可在造塔群@古祠 + * + * core.getFlyMap(floorId?:string, fromUser?:boolean, oriFloor?:string, loop?:number):Object + * 参数说明:floorId:以该楼层为中心绘制 + * fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层) + * oriFloor:这个不用管就好,具体原理比较复杂,填null或者和floorId一样就行 + * loop:绘制层数,fromUser为true时才有用,默认为5 + */ + + ////// 绘制楼层传送器 ////// + var originDrawFly = core.ui.drawFly; + ui.prototype.drawFly = function (page) { + if (!flags.usePlatFly || core.isReplaying()) return originDrawFly.call(core.ui, page); + core.status.event.data = page; + var floorId = core.floorIds[page]; + core.clearMap('ui'); + core.setAlpha('ui', 0.85); + core.fillRect('ui', 0, 0, this.PIXEL, this.PIXEL, '#000000'); + core.setAlpha('ui', 1); + var size = this.PIXEL; + //***--- 楼传绘制 可根据自己的需求更改 更改之后注意更改点击操作的函数 + // 背景 + core.drawThumbnail(floorId, null, { ctx: 'ui', x: 0, y: 0, size: size, damage: true, fromFly: true }); + core.fillRect("ui", 0, 65, 32, size - 130, [0, 0, 0, 0.7]); + core.fillRect("ui", size, 65, -32, size - 130, [0, 0, 0, 0.7]); + core.fillRect("ui", 65, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]); + core.fillRect("ui", 65, size, size / 2 - 114, -32, [0, 0, 0, 0.7]); + core.fillRect("ui", size / 2 + 49, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]); + core.fillRect("ui", size / 2 + 49, size, size / 2 - 114, -32, [0, 0, 0, 0.7]); + core.fillRect("ui", size / 2 - 47, 0, 94, 32, [0, 0, 0, 0.7]); + core.fillRect("ui", size / 2 - 47, size, 94, -32, [0, 0, 0, 0.7]); + core.fillRect("ui", 0, 0, 63, 32, [0, 0, 0, 0.7]); + core.fillRect("ui", size, 0, -63, 32, [0, 0, 0, 0.7]); + core.fillRect("ui", 0, 32, 32, 31, [0, 0, 0, 0.7]); + core.fillRect("ui", 0, size - 32, 32, -31, [0, 0, 0, 0.7]); + core.fillRect("ui", 0, size, 63, -32, [0, 0, 0, 0.7]); + core.fillRect("ui", size, size, -63, -32, [0, 0, 0, 0.7]); + core.fillRect("ui", size, size - 32, -32, -31, [0, 0, 0, 0.7]); + core.fillRect("ui", size, 32, -32, 31, [0, 0, 0, 0.7]); + core.setTextAlign("ui", "center"); + // 文字 + if (core.getFloorByDirection("left", floorId)) + core.fillText("ui", "←", 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("left", + floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); + if (core.getFloorByDirection("right", floorId)) + core.fillText("ui", "→", size - 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("right", + floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); + if (core.getFloorByDirection("up", floorId)) + core.fillText("ui", "↑", size / 2, 28, core.hasVisitedFloor(core.getFloorByDirection("up", + floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); + if (core.getFloorByDirection("down", floorId)) + core.fillText("ui", "↓", size / 2, size - 4, core.hasVisitedFloor(core.getFloorByDirection("down", + floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana"); + if (core.getFloorByDirection("top", floorId)) + core.fillText("ui", "上楼", size / 4 + 8, 24, core.hasVisitedFloor(core.getFloorByDirection("top", + floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font); + if (core.getFloorByDirection("bottom", floorId)) + core.fillText("ui", "下楼", size / 4 * 3 - 8, 24, core.hasVisitedFloor(core.getFloorByDirection("bottom", + floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font); + core.fillText("ui", "退10层", size / 4 + 8, size - 8, core.actions._getNextFlyFloor(-1) == page ? "#ff22ff" : "#ffffff", + "22px " + core.status.globalAttribute.font); + core.fillText("ui", "进10层", size / 4 * 3 - 8, size - 8, core.actions._getNextFlyFloor(1) == page ? "#ff22ff" : "#ffffff", + "22px " + core.status.globalAttribute.font); + core.fillText("ui", "退出", 32, 24, "#ffffff", "22px " + core.status.globalAttribute.font); + core.fillText("ui", "楼层名", 32, size - 8, "#ffffff", "22px " + core.status.globalAttribute.font); + core.fillText("ui", "(B)", 16, size - 40, "#ffffff", "22px " + core.status.globalAttribute.font); + core.createCanvas("mapOnUi", -240, -240, size + 480, size + 480, 150); + core.drawFlyMap("mapOnUi", 240, 240, size, size, floorId, { fromUser: true, oriFloor: floorId, use3D: flags.use3D }); + if (core.can3D(floorId) && !flags.in3D) + core.fillText("ui", "3D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font); + if (flags.in3D) + core.fillText("ui", "2D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font); + if (core.can3D(floorId)) core.fillText("ui", "(Z)", size - 16, 56, "#ffffff", "20px " + core.status.globalAttribute.font); + if (flags.flyTitle) { + var style = document.getElementById("ui").getContext("2d"); + style.shadowColor = "rgba(0, 0, 0, 1)"; + style.shadowBlur = 5; + core.fillRect("ui", size / 4, size / 4, size / 2, size / 8, [180, 180, 180, 0.7]); + core.strokeRect("ui", size / 4, size / 4, size / 2, size / 8, [255, 255, 255, 0.7], 3); + style.shadowOffsetX = 4; + style.shadowOffsetY = 2; + core.fillText("ui", (core.status.maps[floorId] || {}).title, size / 2, size / 16 * 5 + 11, "#ffffff", + "32px " + core.status.globalAttribute.font); + style.shadowColor = "none"; + style.shadowBlur = 0; + style.shadowOffsetX = 0; + style.shadowOffsetY = 0; + } + //***--- 楼传绘制 + }; + ////// 楼层传送器界面时的点击操作 ////// + var originClickFly = core.actions._clickFly; + actions.prototype._clickFly = function (x, y) { + if (!flags.usePlatFly || core.isReplaying()) return originClickFly.call(core.actions, x, y); + var page = core.status.event.data; + var floorId = core.floorIds[page]; + //***--- 点击操作 可以修改的地方只有x,y坐标 其余不可修改 + if (x <= 2 && y <= 1) { + core.playSound('取消'); + core.deleteCanvas("mapOnUi") + core.ui.closePanel(); + return; + } + // 3D模式 + if (x >= core.__SIZE__ - 2 && y <= 1) { + if (core.can3D(floorId) && !flags.in3D) + flags.use3D = true; + if (flags.in3D) flags.use3D = false; + core.playSound('光标移动'); + core.ui.drawFly(page); + return; + } + // 显示名称 + if (x <= 1 && y >= core.__SIZE__ - 2) { + if (flags.flyTitle) flags.flyTitle = false; + else flags.flyTitle = true; + core.playSound('光标移动'); + core.ui.drawFly(page); + return; + } + // 飞过去 + if (x > 1 && x < core.__SIZE__ - 1 && y > 1 && y < core.__SIZE__ - 1) { + if (core.status.maps[core.status.floorId].canFlyFrom && + core.status.maps[core.floorIds[core.status.event.data]].canFlyTo && + core.hasVisitedFloor(core.floorIds[core.status.event.data])) { + core.deleteCanvas("mapOnUi"); + } + core.flyTo(core.floorIds[core.status.event.data]); + } + // 前进10层 后退10层 + if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(-1) != page && + x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2) { + core.ui.drawFly(this._getNextFlyFloor(-10)); + core.playSound("光标移动"); + } + if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(1) != page && + x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3) { + core.ui.drawFly(this._getNextFlyFloor(10)); + core.playSound("光标移动"); + } + // 获取索引 + function getId(direction) { + var id = core.getFloorByDirection(direction, floorId); + for (var i in core.floorIds) { + if (core.floorIds[i] == id) return parseInt(i); + } + } + // 上下左右和上下楼 + if (x < 1 && core.getFloorByDirection("left", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("left", floorId)) && y >= 2 && y < core.__SIZE__ - 2) { + core.playSound("光标移动"); + core.drawFly(getId("left")); + } + if (x > core.__SIZE__ - 2 && core.getFloorByDirection("right", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("right", floorId)) && y >= 2 && y < core.__SIZE__ - 2) { + core.playSound("光标移动"); + core.drawFly(getId("right")); + } + if (y < 1 && core.getFloorByDirection("up", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("up", floorId)) && + x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) { + core.playSound("光标移动"); + core.drawFly(getId("up")); + } + if (y > core.__SIZE__ - 2 && core.getFloorByDirection("down", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("down", floorId)) && + x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) { + core.playSound("光标移动"); + core.drawFly(getId("down")); + } + if (y < 1 && x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2 && + core.getFloorByDirection("top", floorId) && core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) { + core.playSound("光标移动"); + core.drawFly(getId("top")); + } + if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3 && + core.getFloorByDirection("bottom", floorId) && core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) { + core.playSound("光标移动"); + core.drawFly(getId("bottom")); + } + return; + //***--- 点击操作 + }; + ////// 楼层传送器界面时,按下某个键的操作 ////// + var originKeyDownFly = core.actions._keyDownFly; + actions.prototype._keyDownFly = function (keycode) { + if (!flags.usePlatFly || core.isReplaying()) return originKeyDownFly.call(core.actions, keycode); + var page = core.status.event.data; + var floorId = core.floorIds[page]; + // 获取索引 + function getId(direction) { + var id = core.getFloorByDirection(direction, floorId); + for (var i in core.floorIds) { + if (core.floorIds[i] == id) return parseInt(i); + } + } + //***--- 按键操作 只可以修改按键的keycode + if (keycode == 37 && core.getFloorByDirection("left", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("left", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("left")); + } else if (keycode == 38 && core.getFloorByDirection("up", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("up", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("up")); + } else if (keycode == 39 && core.getFloorByDirection("right", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("right", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("right")); + } else if (keycode == 40 && core.getFloorByDirection("down", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("down", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("down")); + } else if (keycode == 33 && core.getFloorByDirection("top", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("top")); + } else if (keycode == 34 && core.getFloorByDirection("bottom", floorId) && + core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) { + core.playSound('光标移动'); + core.ui.drawFly(getId("bottom")); + } else if (keycode == 90) { + if (core.can3D(floorId) && !flags.in3D) + flags.use3D = true; + if (flags.in3D) flags.use3D = false; + core.playSound('光标移动'); + core.ui.drawFly(page); + } else if (keycode == 66) { // 地图名 + if (flags.flyTitle) flags.flyTitle = false; + else flags.flyTitle = true; + core.playSound('光标移动'); + core.ui.drawFly(page); + } else if (keycode == 188 && core.actions._getNextFlyFloor(-1) != page) { // 退10层 + core.ui.drawFly(this._getNextFlyFloor(-10)); + core.playSound("光标移动"); + } else if (keycode == 190 && core.actions._getNextFlyFloor(1) != page) { // 进10层 + core.ui.drawFly(this._getNextFlyFloor(10)); + core.playSound("光标移动"); + } + return; + //***--- 按键操作 + }; + ////// 楼层传送器界面时,放开某个键的操作 ////// + var originKeyUpFly = core.actions._keyUpFly; + actions.prototype._keyUpFly = function (keycode) { + if (!flags.usePlatFly || core.isReplaying()) return originKeyUpFly.call(core.actions, keycode); + if (keycode == 71 || keycode == 27 || keycode == 88) { + core.playSound('取消'); + core.deleteCanvas("mapOnUi"); + core.ui.closePanel(); + } + if (keycode == 13 || keycode == 32 || keycode == 67) + this._clickFly(this.HSIZE - 1, this.HSIZE - 1); + return; + }; + ////// 点击楼层传送器时的打开操作 ////// + events.prototype.useFly = function (fromUserAction) { + if (core.isReplaying()) return; + + // 从“浏览地图”页面:尝试直接传送到该层 + if (core.status.event.id == 'viewMaps') { + if (!core.hasItem('fly')) { + core.playSound('操作失败'); + core.drawTip('你没有' + core.material.items['fly'].name, 'fly'); + } else if (!core.canUseItem('fly')) { + core.playSound('操作失败'); + core.drawTip('无法传送到当前层', 'fly'); + } else { + core.flyTo(core.status.event.data.floorId); + } + return; + } + if (!this._checkStatus('fly', fromUserAction, true)) { + core.deleteCanvas('mapOnUi'); + return; + } + if (core.flags.flyNearStair && !core.nearStair()) { + core.playSound('操作失败'); + core.drawTip("只有在楼梯边才能使用" + core.material.items['fly'].name, 'fly'); + core.unlockControl(); + core.status.event.data = null; + core.status.event.id = null; + return; + } + if (!core.canUseItem('fly')) { + core.playSound('操作失败'); + core.drawTip(core.material.items['fly'].name + "好像失效了", 'fly'); + core.unlockControl(); + core.status.event.data = null; + core.status.event.id = null; + return; + } + core.playSound('打开界面'); + core.useItem('fly', true); + return; + }; + // 获取区域平面地图 + this.getFlyMap = function (floorId, fromUser, oriFloor, loop, clearCache) { + floorId = floorId || core.status.floorId; + if (floorId == oriFloor && !fromUser) return; + oriFloor = oriFloor || core.status.floorId; + if (!floorId) return; + // 判断是否需要缓存 + function needCache(fromUser) { + if (fromUser && !core.status.flyMap[floorId]) return true; + if (!fromUser && !core.status.flyMap.cache[floorId]) return true; + return false; + } + // 缓存,加快运行速率 + if (!core.status.flyMap) core.status.flyMap = {}; + if (!core.status.flyMap.cache) core.status.flyMap.cache = {}; + if (!core.status.layer) core.status.layer = {}; + if (!core.status.layer[floorId]) core.status.layer[floorId] = {}; + if (core.status.flyMap.cache[floorId] && fromUser) delete core.status.flyMap.cache[floorId] + if (core.status.flyMap[floorId] && !fromUser) delete core.status.flyMap[floorId] + if (needCache(fromUser) || clearCache) { + // 初始化 + core.status.flyMap[floorId] = {}; + core.status.flyMap[floorId].thisMap = {}; + core.extractBlocks(floorId); + core.status.maps[floorId].blocks.forEach(function (block) { + var id = block.event.id; + var x = block.x, + y = block.y; + var trigger = block.event.trigger; + if (trigger != "changeFloor" && trigger != "upFloor" && trigger != "downFloor") return; + // 是箭头且可以切换地图 + var toFloor = block.event.data.floorId; + // 加入相应位置 + // 箭头 + if (id == leftPortal) { + core.status.flyMap[floorId].thisMap["left_" + x + "_" + y] = toFloor; + } + if (id == upPortal) { + core.status.flyMap[floorId].thisMap["up_" + x + "_" + y] = toFloor; + } + if (id == rightPortal) { + core.status.flyMap[floorId].thisMap["right_" + x + "_" + y] = toFloor; + } + if (id == downPortal) { + core.status.flyMap[floorId].thisMap["down_" + x + "_" + y] = toFloor; + } + // 上下楼 + if (id == upFloor) { + core.status.flyMap[floorId].thisMap["top_" + x + "_" + y] = toFloor; + core.status.layer[floorId].top = true; + } + if (id == downFloor) { + core.status.flyMap[floorId].thisMap["bottom_" + x + "_" + y] = toFloor; + core.status.layer[floorId].bottom = true; + } + }); + // 把下几层接着检测出来 + if (fromUser) { + var usedId = {}; + for (var c = 1; c <= loop; c++) { + for (var i in core.status.flyMap[floorId].thisMap) { + var link = core.status.flyMap[floorId].thisMap; + if (!core.hasVisitedFloor(link[i]) || link[i] instanceof Object || usedId[link[i]]) continue; + usedId[link[i]] = true; + var next = core.getFlyMap(link[i], false, oriFloor); + for (var to in next) { + if (!core.status.layer[next[to]]) core.status.layer[next[to]] = {}; + core.status.flyMap[floorId].thisMap[i + ',' + to] = next[to]; + } + } + } + } + // 把先上再下之类的去掉 + if (fromUser) { + for (var i in core.status.flyMap[floorId].thisMap) { + var route = i.split(","); + for (var one = 0; one <= route.length - 2; one++) { + var step = route[one], + next = route[one + 1]; + if ((step.startsWith("up") && next.startsWith("down")) || + (step.startsWith("down") && next.startsWith("up")) || + (step.startsWith("left") && next.startsWith("right")) || + (step.startsWith("right") && next.startsWith("left")) || + (step.startsWith("top") && next.startsWith("bottom")) || + (step.startsWith("bottom") && next.startsWith("top"))) { + delete core.status.flyMap[floorId].thisMap[i]; + } + } + } + } + // 非当前层不能存此类缓存 + if (!fromUser) { + core.status.flyMap.cache = {}; + core.status.flyMap.cache[floorId] = {}; + core.status.flyMap.cache[floorId].thisMap = core.status.flyMap[floorId].thisMap; + delete core.status.flyMap[floorId]; + } + return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap; + } else { // 直接使用缓存 + return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap; + } + }; + this.can3D = function (floorId) { + var map = core.getFlyMap(floorId, true, floorId); + for (var route in map) { + if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) + return true; + } + return false; + }; + // 绘制地图 + this.drawFlyMap = function (ctx, x, y, width, height, floorId, config) { + // 初始化配置项 + //***--- 初始化 可以修改 || 后的默认值 参数说明在开头的高深区域中 + var fromUser = config.fromUser || false, + oriFloor = config.oriFloor || core.status.floorId, + scale = config.scale || null, + interval = config.interval || (width / 24), + noErase = config.noErase || false, + fromMini = config.fromMini || false, + loop = config.loop || defaultLoop, + opacity = config.opacity || defaultOpacity, + layer = config.layer || 0, + use3D = config.use3D || false, + clearCache = config.clearCache || false, + map = config.map || null; + //***--- 初始化 + map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache); + floorId = floorId || core.status.floorId; + if (!floorId) return; + // 检测是否需要3D绘图 + if (!fromMini && use3D) { + for (var route in map) { + if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) { + config.map = map; + return core.draw3DFlyMap(ctx, x, y, width, height, floorId, config); + } + } + } + if (layer != 0 && !use3D) { + var canLayer = false + for (var route in map) { + if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) { + canLayer = true; + break; + } + } + if (!canLayer) layer = 0; + } + flags.in3D = false; + // 初始化 + var userScale = true; + var newCreate = false; + if (!scale) { + userScale = false; + scale = scale || defaultScale; + } + if (!core.dymCanvas[ctx]) { + core.createCanvas(ctx, x, y, width, height, 140); + newCreate = true; + } + x = x || 0; + y = y || 0; + // 获得canvas属性 + width = width || document.getElementById(ctx).width; + height = height || document.getElementById(ctx).height; + var oLeft = document.getElementById(ctx).offsetLeft / core.domStyle.scale, + oTop = document.getElementById(ctx).offsetTop / core.domStyle.scale; + // 重置大地图和楼传地图的canvas位置 + if (ctx == "mapOnUi") core.relocateCanvas("mapOnUi", -240, -240); + if (!noErase) + core.clearMap(ctx); + var horCenter = Math.floor(width / 2), + uprCenter = Math.floor(height / 2); + if (!newCreate) { + horCenter += x; + uprCenter += y; + } + var centerX = horCenter, + centerY = uprCenter; + var left = centerX, + right = centerX, + up = centerY, + down = centerY; + var used = {}; + var haveLayer = {}; + var nx = horCenter, + ny = uprCenter; + // 先把所在楼层绘制了 + if (layer == 0) { + var nw = core.status.maps[floorId].width * 2 * scale, + nh = core.status.maps[floorId].height * 2 * scale; + core.setAlpha(ctx, 1); + core.fillRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#000000"); + core.strokeRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#ffff22", 3 * scale); + // 当前层上下楼显示 + if (!haveLayer[floorId]) { + core.setAlpha(ctx, 1); + var needLayer = core.status.layer[floorId]; + if (needLayer.top && needLayer.bottom) { + core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale, + centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); + core.drawIcon(ctx, "downFloor", centerX - nw / 2 + core.__SIZE__ * scale, + centerY - nh / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); + } + if (needLayer.top && !needLayer.bottom) { + core.drawIcon(ctx, "upFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2, + Math.min(nw, nh), Math.min(nw, nh)); + } + if (!needLayer.top && needLayer.bottom) { + core.drawIcon(ctx, "downFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2, + Math.min(nw, nh), Math.min(nw, nh)); + } + haveLayer[floorId] = true; + } + // 四侧最远位置 + if (left > centerX - nw / 2) left = centerX - nw / 2; + if (right < centerX + nw / 2) right = centerX + nw / 2; + if (down < centerY + nh / 2) down = centerY + nh / 2; + if (up > centerY - nh / 2) up = centerY - nh / 2; + } + core.setAlpha(ctx, opacity); + for (var route in map) { // 绘制楼层和线条 + var rouArr = route.split(","); + // 检索路线及画线 + // 初始化 + centerX = nx; + centerY = ny; + var nowFloor = floorId || core.status.floorId; + var nowLayer = 0; + for (var one in rouArr) { // 一个一个检测 + var step = rouArr[one].split("_"); + var cx = step[1], + cy = step[2]; + // 获得当前图块 + core.getMapBlocksObj(nowFloor, true); + var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy]; + if (!nowBlock) continue; + var toLoc = nowBlock.event.data.loc, + toFloor = nowBlock.event.data.floorId; + var needLayer = core.status.layer[toFloor]; + // 当前层宽度和高度 + var nw = core.status.maps[nowFloor].width * 2 * scale, + nh = core.status.maps[nowFloor].height * 2 * scale; + // 目标层宽度和高度 + var tw = core.status.maps[toFloor].width * 2 * scale, + th = core.status.maps[toFloor].height * 2 * scale; + // 将当前层变为toFloor + nowFloor = toFloor; + // 超范围不画 + if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height || + centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue; + // 绘制toFloor层 + core.setAlpha(ctx, opacity); + // 确定center 根据箭头自适配 同时绘制线条 我已经看不懂了 + if (!use3D && (step[0] == "top" || step[0] == "bottom") && layer == 0) break; + if (step[0] == "top") nowLayer++; + if (step[0] == "bottom") nowLayer--; + if (step[0] == 'left') { + var shouldTo = th / 2, + realTo = toLoc[1] * 2 * scale; + var shouldFrom = nh / 2, + realFrom = step[2] * 2 * scale; + if (nowLayer == layer) { + core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom, + centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom, + centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale); + } + centerX -= nw / 2 + tw / 2 + interval; + centerY += shouldTo - realTo + realFrom - shouldFrom; + } + if (step[0] == 'right') { + var shouldTo = th / 2, + realTo = toLoc[1] * 2 * scale; + var shouldFrom = nh / 2, + realFrom = step[2] * 2 * scale; + if (nowLayer == layer) { + core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom, + centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom, + centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale); + } + centerX += nw / 2 + tw / 2 + interval; + centerY += shouldTo - realTo + realFrom - shouldFrom; + } + if (step[0] == 'up') { + var shouldTo = tw / 2, + realTo = toLoc[0] * 2 * scale; + var shouldFrom = nw / 2, + realFrom = step[1] * 2 * scale; + if (nowLayer == layer) { + core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2, + centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2, + centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#000000", 2 * scale); + } + centerY -= nh / 2 + th / 2 + interval; + centerX += shouldTo - realTo + realFrom - shouldFrom; + } + if (step[0] == 'down') { + var shouldTo = tw / 2, + realTo = toLoc[0] * 2 * scale; + var shouldFrom = nw / 2, + realFrom = step[1] * 2 * scale; + if (nowLayer == layer) { + core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2, + centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2, + centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#000000", 2 * scale); + } + centerY += nh / 2 + th / 2 + interval; + centerX += shouldTo - realTo + realFrom - shouldFrom; + } + // 只有和目标层高度相同时才绘制 + if (nowLayer != layer) continue; + // 超范围的不画 + if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height || + centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue; + // 四侧最远位置 + if (left > centerX - tw / 2) left = centerX - tw / 2; + if (right < centerX + tw / 2) right = centerX + tw / 2; + if (down < centerY + th / 2) down = centerY + th / 2; + if (up > centerY - th / 2) up = centerY - th / 2; + // 画过了不画 + if (used[toFloor]) continue; + used[toFloor] = true; + // 画地图格 + if (core.hasVisitedFloor(toFloor)) { + core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#000000"); + core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale); + } else { + core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ff22ff"); + core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale); + break; + } + // 上下楼显示 + if (haveLayer[toFloor]) continue; + core.setAlpha(ctx, opacity); + if (needLayer.top && needLayer.bottom) { + core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale, + centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); + core.drawIcon(ctx, "downFloor", centerX - tw / 2 + core.__SIZE__ * scale, + centerY - th / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale); + } + if (needLayer.top && !needLayer.bottom) { + core.drawIcon(ctx, "upFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2, + Math.min(tw, th), Math.min(tw, th)); + } + if (!needLayer.top && needLayer.bottom) { + core.drawIcon(ctx, "downFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2, + Math.min(tw, th), Math.min(tw, th)); + } + haveLayer[toFloor] = true; + } + } + // 自动缩放 + if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) { + scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64)); + var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, layer: layer, opacity: opacity, loop: loop }; + return core.drawFlyMap(ctx, x, y, width, height, floorId, con); + } + // 大地图和楼层地图自适配定位 + if (ctx == "mapOnUi" && !fromMini && (left - nx < -128 || right - nx > 128 || + up - ny < -128 || down - ny > 128)) { + core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2); + } + }; + // 3D绘图 + this.draw3DFlyMap = function (ctx, x, y, width, height, floorId, config) { + // 初始化配置项 + //***--- 初始化 同上一个初始化 + var fromUser = config.fromUser || false, + oriFloor = config.oriFloor || core.status.floorId, + scale = config.scale || null, + interval = config.interval || (width / 24), + deltaH = config.deltaH || (height / 8), + noErase = config.noErase || false, + fromMini = config.fromMini || false, + loop = config.loop || defaultLoop, + opacity = config.opacity || defaultOpacity, + minorAlpha = config.minorAlpha || defaultMinorAlpha, + reLeft = config.reLeft || -240, + reTop = config.reTop || -240, + clearCache = config.clearCache || false, + map = config.map || null; + //***--- 初始化 + map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache); + // 当前层是否一个人在一层 + var alone = true; + for (var route in map) { + if (route.startsWith("left") || route.startsWith("right") || + route.startsWith("up") || route.startsWith("down")) { + alone = false; + break; + } + } + // 是则增加一个先上再下的路径 + if (alone) { + for (var route in map) { + if (route.startsWith("top")) { + var first = route.split(",")[0].split("_"); + break; + } + } + var success = false; + core.getMapBlocksObj(nowFloor, true); + var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]]; + var toFloor = nowBlock.event.data.floorId; + core.extractBlocks(toFloor); + core.status.maps[toFloor].blocks.forEach(function (block) { + var id = block.event.id; + var x = block.x, + y = block.y; + var trigger = block.event.trigger; + if (trigger != "changeFloor") return; + if (id == "downFloor") { + map["top_" + first[1] + "_" + first[2] + "," + "bottom_" + x + "_" + y] = floorId; + success = true; + return; + } + }); + // 添加先上再下失败 尝试先下再上 + if (!success) { + for (var route in map) { + if (route.startsWith("bottom")) { + var first = route.split(",")[0].split("_"); + break; + } + } + var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]]; + var toFloor = nowBlock.event.data.floorId; + core.extractBlocks(toFloor); + core.status.maps[toFloor].blocks.forEach(function (block) { + var id = block.event.id; + var x = block.x, + y = block.y; + var trigger = block.event.trigger; + if (trigger != "changeFloor") return; + if (id == "upFloor") { + map["bottom_" + first[1] + "_" + first[2] + "," + "top_" + x + "_" + y] = floorId; + success = true; + return; + } + }); + } + } + floorId = floorId || core.status.floorId; + if (!floorId) return; + flags.in3D = true; + // 初始化 + // 获得排序过的楼层路径 + map = core.sortFloor(map); + map = map.map; + var userScale = true; + var newCreate = false; + if (!scale) { + userScale = false; + scale = scale || defaultScale; + } + if (!core.dymCanvas[ctx]) { + core.createCanvas(ctx, x, y, width, height, 140); + newCreate = true; + } + x = x || 0; + y = y || 0; + // 获得canvas属性 + width = width || document.getElementById(ctx).width; + height = height || document.getElementById(ctx).height; + // 重置canvas位置 + core.relocateCanvas(ctx, reLeft, reTop); + if (!noErase) + core.clearMap(ctx); + var horCenter = Math.floor(width / 2), + uprCenter = Math.floor(height / 2); + if (!newCreate) { + horCenter += x; + uprCenter += y; + } + // 单元格的中心点 即水平线中点处 + var centerX = horCenter, + centerY = uprCenter; + var left = centerX, + right = centerX, + up = centerY, + down = centerY; + var used = {}; + var nx = horCenter, + ny = uprCenter; + // 开始绘制 + for (var i = 0; i < map.length; i++) { + var route = map[i][0]; + var nowLayer = map[i][1]; + var everyLayer = 0; + route = route.split(","); + // 每条路线初始化 + centerX = horCenter; + centerY = uprCenter; + centerX += core.status.maps[floorId].height * scale * Math.SQRT2 / 4; + if (flags.viewingLayer) { + centerY += deltaH * flags.viewingLayer; + } + var nowFloor = floorId || core.status.floorId; + for (var one = 0; one < route.length; one++) { + var step = route[one].split("_"); + var cx = step[1], + cy = step[2]; + // 检测高度,是否与nowLayer一致 不一致在处理完center以后不绘制 + if (step[0] == "top") everyLayer++; + if (step[0] == "bottom") everyLayer--; + // 获得当前图块 + core.getMapBlocksObj(nowFloor, true); + var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy]; + if (!nowBlock) continue; + var toLoc = nowBlock.event.data.loc, + toFloor = nowBlock.event.data.floorId; + // 当前层宽度和高度 + // 斜二测画法 + var nw = core.status.maps[nowFloor].width * 2 * scale, + nh = core.status.maps[nowFloor].height * scale * Math.SQRT2 / 2; + // 目标层宽度和高度 + var tw = core.status.maps[toFloor].width * 2 * scale, + th = core.status.maps[toFloor].height * scale * Math.SQRT2 / 2; + if (!(toLoc instanceof Array)) { + toLoc = [Math.floor(tw / 4 / scale), Math.floor(th / 4 / scale)]; + } + // 绘制当前层 + if (nowLayer == 0 && !used[floorId]) { + core.setAlpha(ctx, 1); + used[floorId] = true; + var nowW = core.status.maps[floorId].width * 2 * scale; + var nowH = core.status.maps[floorId].height * scale * Math.SQRT2 / 2; + var nodes = [ + [centerX - nowW / 2 - nowH / 2, centerY + nowH / 2], + [centerX + nowW / 2 - nowH / 2, centerY + nowH / 2], + [centerX + nowW / 2 + nowH / 2, centerY - nowH / 2], + [centerX - nowW / 2 + nowH / 2, centerY - nowH / 2] + ]; + core.fillPolygon(ctx, nodes, "#000000"); + core.strokePolygon(ctx, nodes, "#ffff22", 1.5 * scale); + // 四侧最远位置 + if (left > centerX - nw / 2 - nh / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - nw / 2 - nh / 2; + if (right < centerX + nw / 2 + nh / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + nw / 2 + nh / 2; + } + // 将当前层变为toFloor + var fromFloor = nowFloor; + nowFloor = toFloor; + // 计算center 画同层间的线 我已经看不懂了 + // 设置不透明度 + if (nowLayer == (flags.viewingLayer || 0)) { + core.setAlpha(ctx, opacity); + } else { + core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); + } + if (step[0] == "left") { + var shouldFrom = nh / 2, + realFrom = cy * scale * Math.SQRT2 / 2; + var shouldTo = th / 2, + realTo = toLoc[1] * scale * Math.SQRT2 / 2; + if (everyLayer == nowLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) + core.drawLine(ctx, centerX - nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom, + centerX - nw / 2 - interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale); + centerX -= nw / 2 + tw / 2 + interval + shouldTo - realTo + realFrom - shouldFrom; + centerY += shouldTo - realTo + realFrom - shouldFrom; + } + if (step[0] == "right") { + var shouldFrom = nh / 2, + realFrom = cy * scale * Math.SQRT2 / 2; + var shouldTo = th / 2, + realTo = toLoc[1] * scale * Math.SQRT2 / 2; + if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) + core.drawLine(ctx, centerX + nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom, + centerX + nw / 2 + interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale); + centerX += nw / 2 + tw / 2 + interval - (shouldTo - realTo + realFrom - shouldFrom); + centerY += shouldTo - realTo + realFrom - shouldFrom; + } + if (step[0] == "up") { + var shouldTo = tw / 2, + realTo = toLoc[0] * scale * 2; + var shouldFrom = nw / 2, + realFrom = cx * scale * 2; + if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) + core.drawLine(ctx, centerX + realFrom - shouldFrom + nh / 2, centerY - nh / 2, centerX + realFrom - + shouldFrom + interval * Math.SQRT2 / 4 + nh / 2, centerY - nh / 2 - interval * Math.SQRT2 / 4, "#ffffff", 2 * scale); + centerY -= nh / 2 + th / 2 + interval * Math.SQRT2 / 4; + centerX += shouldTo - realTo + realFrom - shouldFrom + (nh / 2 + th / 2 + interval * Math.SQRT2 / 4); + } + if (step[0] == "down") { + var shouldTo = tw / 2, + realTo = toLoc[0] * scale * 2; + var shouldFrom = nw / 2, + realFrom = cx * scale * 2; + if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) + core.drawLine(ctx, centerX + realFrom - shouldFrom - nh / 2, centerY + nh / 2, centerX + realFrom - + shouldFrom - interval * Math.SQRT2 / 4 - nh / 2, centerY + nh / 2 + interval * Math.SQRT2 / 4, "#ffffff", 2 * scale); + centerY += nh / 2 + th / 2 + interval * Math.SQRT2 / 4; + centerX += shouldTo - realTo + realFrom - shouldFrom - (nh / 2 + th / 2 + interval * Math.SQRT2 / 4); + } + if (step[0] == "top") { + centerY -= deltaH; + } + if (step[0] == "bottom") { + centerY += deltaH; + } + if (everyLayer == nowLayer && step[0] != "top" && step[0] != "bottom") { + used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; + used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; + } + if (everyLayer != nowLayer) continue; + // 四侧最远位置 + if (!flags.viewingLayer) { + if (left > centerX - tw / 2 - th / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - tw / 2 - th / 2; + if (right < centerX + tw / 2 + th / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + tw / 2 + th / 2; + if (down < centerY + th / 2 && nowLayer == (flags.viewingLayer || 0)) down = centerY + th / 2; + if (up > centerY - th / 2 && nowLayer == (flags.viewingLayer || 0)) up = centerY - th / 2; + } + // 不同高度层之间的连线 + if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) { + core.setAlpha(ctx, opacity * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); + if (step[0] == "top") { + core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#000000", 2 * scale); + used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; + used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; + } + } + if (!used[toFloor]) { + used[toFloor] = true; + // 设置不透明度 + if (nowLayer == (flags.viewingLayer || 0)) { + core.setAlpha(ctx, opacity); + } else { + core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0)))); + } + // 画地图 + var nodes = [ + [centerX - tw / 2 - th / 2, centerY + th / 2], // 左下 + [centerX + tw / 2 - th / 2, centerY + th / 2], // 右下 + [centerX + tw / 2 + th / 2, centerY - th / 2], // 右上 + [centerX - tw / 2 + th / 2, centerY - th / 2] // 左上 + ]; + if (core.hasVisitedFloor(toFloor)) { + core.fillPolygon(ctx, nodes, "#000000"); + } else { + core.fillPolygon(ctx, nodes, "#ff22ff"); + } + core.strokePolygon(ctx, nodes, "#ffffff", 1.5 * scale); + } + // 不同高度层之间的连线 + if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) { + if (step[0] == "bottom") { + core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#ffffff", 5 * scale); + core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#000000", 2 * scale); + used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true; + used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true; + } + } + } + } + // 自动缩放 + if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) { + scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64)); + var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, opacity: opacity, loop: loop, use3D: true }; + return core.draw3DFlyMap(ctx, x, y, width, height, floorId, con); + } + // 大地图和楼层地图自适配定位 + if (ctx == "mapOnUi") + core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2); + }; + // 不同高度楼层排序 + this.sortFloor = function (map) { + map = map || core.getFlyMap(null, true); + var totalLayer = 1, + topLayer = 0, + bottomLayer = 0, + nowLayer = 0; + // 拆分map + for (var i in map) { + var route = i.split(","); + nowLayer = 0; + for (var one in route) { + var step = route[one].split("_"); + // 层数处理 并记录每一层的层数 + if (step[0] == "top") { + nowLayer++; + map[i] = nowLayer + "_" + map[i]; + if (nowLayer > topLayer) topLayer = nowLayer; + } + if (step[0] == "bottom") { + nowLayer--; + map[i] = nowLayer + "_" + map[i]; + if (nowLayer < bottomLayer) bottomLayer = nowLayer; + } + } + } + // 总层数 + totalLayer = topLayer - bottomLayer + 1; + // 按楼层高度由低到高排序 + // 先变成数组 + var mapArr = []; + for (var one in map) { + mapArr.push([one, parseInt(map[one]) || 0]); + } + // 再sort + mapArr.sort(function (a, b) { return a[1] - b[1]; }); + return { map: mapArr, totalLayer: totalLayer, top: topLayer, bottom: bottomLayer }; + }; + // 由方向获得楼层坐标 + this.getFloorByDirection = function (direction, floorId) { + floorId = floorId || core.status.floorId; + var route = core.getFlyMap(floorId); + for (var step in route) { + if (step.indexOf(direction) >= 0) { + return route[step]; + } + } + return null; + }; + ////// 转换楼层结束的事件 检查小地图 ////// + var originAfterChangeFloor = core.events.afterChangeFloor; + events.prototype.afterChangeFloor = function (floorId) { + if (!flags.__useMinimap__ || core.isReplaying()) { + core.deleteCanvas("minimap"); + core.deleteCanvas("mapArrow"); + core.unregisterAction("ondown", "closeMinimap"); + core.unregisterAction("ondown", "openMinimap"); + return originAfterChangeFloor.call(core.events, floorId); + } + if (main.mode != 'play') return; + this.eventdata.afterChangeFloor(floorId); + // 防止小地图出问题 + core.unregisterAction("ondown", "closeMinimap"); + core.unregisterAction("ondown", "openMinimap"); + // 切换小地图 + core.checkMinimap(true, true); + return; + }; + ////// 瞬间移动 检查小地图 ////// + var originMoveDirectly = core.control.moveDirectly; + control.prototype.moveDirectly = function (destX, destY, ignoreSteps) { + if (!flags.__useMinimap__ || core.isReplaying()) + return originMoveDirectly.call(core.control, destX, destY, ignoreSteps); + var canMoveDirectly = this.controldata.moveDirectly(destX, destY, ignoreSteps); + if (canMoveDirectly) core.checkMinimap(); + return canMoveDirectly; + }; + ////// 每移动一格后执行的事件 检查小地图 ////// + var originMoveOneStep = core.control.moveOneStep; + control.prototype.moveOneStep = function (callback) { + if (!flags.__useMinimap__ || core.isReplaying()) + return originMoveOneStep.call(core.control, callback); + this.controldata.moveOneStep(callback); + core.checkMinimap(); + }; + // 检查小地图开闭情况 改变小地图位置 + this.checkMinimap = function (fromUser, reDraw) { + if (!flags.__useMinimap__ || core.isReplaying()) { + core.unregisterAction("ondown", "closeMinimap"); + core.unregisterAction("ondown", "openMinimap"); + core.deleteCanvas("mapArrow"); + core.deleteCanvas("minimap"); + return; + } + reDraw = reDraw || false; + // 是否重绘 + if (reDraw) { + if (flags.minimap) core.drawMinimap(flags.__onLeft__); + else core.drawClosedMap(flags.__onLeft__); + } + var hx = core.status.hero.loc.x; + var opened = flags.minimap; + var onLeft = hx >= Math.ceil(core.__SIZE__ / 3 * 2); + fromUser = fromUser || false; // 开关小地图相关 + if (!flags.__onLeft__) flags.__onLeft__ = false; + // 如果地图上没有小地图 画到右边 + if (!core.dymCanvas.mapArrow && !core.dymCanvas.minimap && !reDraw) { + if (flags.minimap) core.drawMinimap(); + else core.drawClosedMap(); + flags.__onLeft__ = false; + } + // 人物在中间 不执行 + if (hx >= Math.ceil(core.__SIZE__ / 3 + core.bigmap.offsetX / 32) && + hx <= Math.floor(core.__SIZE__ / 3 * 2 + core.bigmap.offsetX / 32)) return; + // 重定位画布 和 翻转 + // 挪到右边 + if (!onLeft && (flags.__onLeft__ || fromUser)) { + flags.__onLeft__ = false; + if (opened) { + core.relocateCanvas("minimap", core.__PIXELS__ - 120, 0); + core.relocateCanvas("mapArrow", core.__PIXELS__ - 140, 0); + document.getElementById('mapArrow').style.transform = 'none'; + } else { + core.relocateCanvas("minimap", core.__PIXELS__, 0); + core.relocateCanvas("mapArrow", core.__PIXELS__ - 20, 0); + document.getElementById('mapArrow').style.transform = 'none'; + } + } + // 挪到左边 + if (onLeft && (!flags.__onLeft__ || fromUser)) { + flags.__onLeft__ = true; + if (opened) { + core.relocateCanvas("minimap", 0, 0); + core.relocateCanvas("mapArrow", 120, 0); + document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; + } else { + core.relocateCanvas("minimap", -120, 0); + core.relocateCanvas("mapArrow", 0, 0); + document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; + } + } + }; + // 点击小地图的action + this.registerMinimapAction = function (open) { + if (!open) { + core.registerAction("ondown", "closeMinimap", function (x, y, px, py) { + if (!flags.__onLeft__) { + if (px >= core.__PIXELS__ - 140 && px <= core.__PIXELS__ - 120 && + py >= 0 && py <= 120) { + core.closeMinimap(); + core.unregisterAction("ondown", "closeMinimap"); + return true; + } + if (px >= core.__PIXELS__ - 120 && py <= 120) { + core.playSound("打开界面"); + core.drawTotalMap(); + return true; + } + } else { + if (px >= 120 && px <= 140 && py >= 0 && py <= 120) { + core.closeMinimap(); + core.unregisterAction("ondown", "closeMinimap"); + return true; + } + if (px <= 120 && py <= 120) { + core.playSound("打开界面"); + core.drawTotalMap(); + return true; + } + } + }, 10); + } else { + core.registerAction("ondown", "openMinimap", function (x, y, px, py) { + if (!flags.__onLeft__) { + if (px >= core.__PIXELS__ - 20 && py <= 120) { + core.openMinimap(); + core.unregisterAction("ondown", "openMinimap"); + return true; + } + } else { + if (px <= 20 && py <= 120) { + core.openMinimap(); + core.unregisterAction("ondown", "openMinimap"); + return true; + } + } + }, 10); + } + }; + // 地图上的小地图 + this.drawMinimap = function (toLeft) { + if (!flags.__useMinimap__) { + core.deleteCanvas("mapArrow"); + core.deleteCanvas("minimap"); + return; + } + var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1); + if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale) + scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1); + // 绘制 + core.createCanvas("minimap", core.__PIXELS__ - 120, 0, 120, 120, 100); + core.createCanvas("mapArrow", core.__PIXELS__ - 140, 0, 20, 120, 100); + if (toLeft) { + core.relocateCanvas("minimap", 0, 0); + core.relocateCanvas("mapArrow", 120, 0); + document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; + } + core.clearMap("minimap"); + core.clearMap("mapArrow"); + // 黑色底 + core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]); + var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true }; + core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config); + // 向右箭头 + core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]); + core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2); + core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2); + core.setTextAlign("mapArrow", "center"); + core.fillText("mapArrow", ">", 10, 67, [100, 100, 100, 0.9], "20px Verdana"); + core.registerMinimapAction(false); + }; + // 关闭小地图 + this.closeMinimap = function () { + if (!flags.__useMinimap__) { + core.deleteCanvas("mapArrow"); + core.deleteCanvas("minimap"); + return; + } + var onLeft = flags.__onLeft__; + var frame = 0; + var x = core.__PIXELS__, + a = 0.096, + speed = 4.8; + if (onLeft) { + x = 260; + a = -a; + speed = -speed; + } + var interval = setInterval(function () { + core.relocateCanvas("mapArrow", x - 140, 0); + if (!onLeft) + core.relocateCanvas("minimap", x - 120, 0); + else core.relocateCanvas("minimap", x - 260, 0); + speed -= a; + x += speed; + if (frame == 50) { + flags.minimap = false; + clearInterval(interval); + core.drawClosedMap(onLeft); + core.checkMinimap(true); + } + frame++; + }, 20); + }; + // 合上的小地图 + this.drawClosedMap = function (toLeft) { + if (!flags.__useMinimap__) { + core.deleteCanvas("mapArrow"); + core.deleteCanvas("minimap"); + return; + } + var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1); + if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale) + scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1); + // 绘制 + core.createCanvas("minimap", core.__PIXELS__, 0, 120, 120, 100); + core.createCanvas("mapArrow", core.__PIXELS__ - 20, 0, 20, 120, 100); + core.clearMap("minimap"); + core.clearMap("mapArrow"); + if (toLeft) { + core.relocateCanvas("minimap", -120, 0); + core.relocateCanvas("mapArrow", 0, 0); + document.getElementById('mapArrow').style.transform = 'rotateY(180deg)'; + } + // 黑色底 + core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]); + var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true }; + core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config); + // 向左箭头 + core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]); + core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2); + core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2); + core.setTextAlign("mapArrow", "center"); + core.fillText("mapArrow", "<", 10, 67, [100, 100, 100, 0.9], "20px Verdana"); + core.registerMinimapAction(true); + }; + // 打开小地图 + this.openMinimap = function () { + if (!flags.__useMinimap__) { + core.deleteCanvas("mapArrow"); + core.deleteCanvas("minimap"); + return; + } + var onLeft = flags.__onLeft__; + var frame = 0; + var x = 120 + core.__PIXELS__, + a = 0.096, + speed = 4.8; + if (onLeft) { + x = 140; + a = -a; + speed = -speed; + } + var interval = setInterval(function () { + core.relocateCanvas("mapArrow", x - 140, 0); + if (!flags.__onLeft__) + core.relocateCanvas("minimap", x - 120, 0); + else core.relocateCanvas("minimap", x - 260, 0); + speed -= a; + x -= speed; + if (frame == 50) { + flags.minimap = true; + clearInterval(interval); + core.drawMinimap(onLeft); + core.checkMinimap(true); + } + frame++; + }, 20); + }; + // 大地图 + this.drawTotalMap = function (floorId) { + floorId = floorId || core.status.floorId; + core.status.event.id = "totalMap"; + core.lockControl(); + if (!flags.viewingLayer) flags.viewingLayer = 0; + var loop = 5; + if (flags.worldMap) loop = core.floorIds.length; + // 大地图时点击和键盘操作 + core.registerAction("ondown", "onDownTmap", function (x, y) { + if (core.status.event.id == "totalMap") { + if (y < 1 && x <= Math.floor(core.__SIZE__ / 2) - 2) { // 上移一层 + if (flags.viewingLayer < core.sortFloor().top) { + flags.viewingLayer++; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1) { // 下移一层 + if (flags.viewingLayer > core.sortFloor().bottom) { + flags.viewingLayer--; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + if (y < 1 && x < Math.ceil(core.__SIZE__ / 2) + 1 && x > Math.floor(core.__SIZE__ / 2) - 2) { + // 区域地图 + if (flags.worldMap) { + flags.worldMap = false; + flags.viewingLayer = 0; + } else flags.worldMap = true; + core.playSound('光标移动'); + core.drawTotalMap(); + return true; + } + if (y >= core.__SIZE__ - 1 && x <= Math.floor(core.__SIZE__ / 2)) { // 3D + if (core.can3D(floorId) && !flags.in3D) flags.use3D = true; + if (flags.in3D) flags.use3D = false; + flags.mapHint = false; + core.playSound('光标移动'); + core.drawTotalMap(); + return true; + } + if (y >= core.__SIZE__ - 1 && x >= Math.ceil(core.__SIZE__ / 2)) { // hint + if (flags.in3D) { + if (!flags.mapHint) flags.mapHint = true; + else flags.mapHint = false; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + flags.viewingLayer = 0; + core.playSound("取消") + core.deleteCanvas("mapOnUi"); + core.deleteCanvas("back"); + core.deleteCanvas("tips"); + core.closePanel(); + core.unregisterAction("ondown", "onDownTmap"); + return true; + } + }, 110); + core.registerAction("keyUp", "keyUpTmap", function (keycode) { + if (core.status.event.id == "totalMap") { + if (keycode == 33) { // PgUp + if (flags.viewingLayer < core.sortFloor().top) { + flags.viewingLayer++; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + if (keycode == 34) { // PgDn + if (flags.viewingLayer > core.sortFloor().bottom) { + flags.viewingLayer--; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + if (keycode == 90) { // Z + if (core.can3D(floorId) && !flags.in3D) flags.use3D = true; + if (flags.in3D) flags.use3D = false; + flags.mapHint = false; + core.playSound('光标移动'); + core.drawTotalMap(); + return true; + } + if (keycode == 84) { // T + if (flags.in3D) { + if (!flags.mapHint) flags.mapHint = true; + else flags.mapHint = false; + core.playSound('光标移动'); + core.drawTotalMap(); + } + return true; + } + if (keycode == 87) { // W + if (flags.worldMap) { + flags.worldMap = false; + flags.viewingLayer = 0; + } else flags.worldMap = true; + core.playSound('光标移动'); + core.drawTotalMap(); + return true; + } + flags.viewingLayer = 0; + core.playSound("取消") + core.deleteCanvas("mapOnUi"); + core.deleteCanvas("back"); + core.deleteCanvas("tips"); + core.closePanel(); + core.unregisterAction("keyUp", "keyUpTmap"); + return true; + } + }, 110); + // 开始画 + core.createCanvas("mapOnUi", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 150); + core.createCanvas("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 140); + core.createCanvas("tips", 0, 0, core.__PIXELS__, core.__PIXELS__, 160); + var ctx = document.getElementById("tips").getContext("2d"); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.shadowBlur = 5; + ctx.shadowColor = "rgba(100, 100, 255, 1)"; + core.fillRect("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, [0, 0, 0, 0.9]); + core.fillRect("tips", 0, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]); + core.fillRect("tips", core.__PIXELS__ / 2 + 50, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]); + core.fillRect("tips", core.__PIXELS__ / 2 - 46, 0, 92, 32, [200, 200, 200, 0.8]); + core.drawLine("tips", 0, 32, core.__PIXELS__ / 2 - 50, 32, [50, 50, 50, 0.8], 3); + core.drawLine("tips", core.__PIXELS__ / 2 + 50, 32, core.__PIXELS__, 32, [50, 50, 50, 0.8], 3); + core.drawLine("tips", core.__PIXELS__ / 2 - 46, 32, core.__PIXELS__ / 2 + 46, 32, [50, 50, 50, 0.8], 3); + core.fillRect("tips", 0, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]); + core.fillRect("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]); + core.drawLine("tips", 0, core.__PIXELS__ - 32, core.__PIXELS__ / 2 - 5, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3); + core.drawLine("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__ - 32, core.__PIXELS__, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3); + core.setTextAlign("tips", "center"); + core.fillText("tips", "上移一层", core.__PIXELS__ / 4 - 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + core.fillText("tips", "下移一层", core.__PIXELS__ / 4 * 3 + 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + core.fillText("tips", flags.worldMap ? "小地图" : "区域地图", core.__PIXELS__ / 2, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + core.drawFlyMap("mapOnUi", 240, 240, core.__PIXELS__, core.__PIXELS__, + floorId, { fromUser: true, opacity: 1, oriFloor: floorId, noErase: true, use3D: flags.use3D, layer: flags.viewingLayer, loop: loop, clearCache: true }); + if (flags.in3D) + core.fillText("tips", "参考线(T)", core.__PIXELS__ / 4 * 3, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + if (core.can3D(floorId) && !flags.in3D) + core.fillText("tips", "3D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + if (flags.in3D) + core.fillText("tips", "2D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font); + if (flags.mapHint) { + core.drawLine("back", 240, 240 + core.__PIXELS__, 240 + core.__PIXELS__, 240, [100, 100, 240, 0.4], 2); + core.drawLine("back", 240 + core.__PIXELS__ / 2, 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, [100, 100, 240, 0.4], 2); + core.drawLine("back", 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, 240 + core.__PIXELS__ / 2, [100, 100, 240, 0.4], 2); + } + }; + }, + "advancedAnimation": function () { + // -------------------- 插件说明 -------------------- // + // github仓库:https://github.com/unanmed/animate + // npm包名:mutate-animate + // npm地址:https://www.npmjs.com/package/mutate-animate + + // 是否开启本插件,默认启用;将此改成 false 将禁用本插件。 + var __enable = true; + + if (main.replayChecking) __enable = false; + if (!__enable) { + core.plugin.animate = {}; + this.tickerSet = new Set(); + this.deleteAllAnis = () => { }; + return; + } + + /** 该集合中的所有Ticker在跨层时需要被摧毁 */ + this.tickerSet = new Set(); + + /** 对Map中所有Ticker执行摧毁事件 */ + this.deleteAllAnis = function () { + core.plugin.tickerSet.forEach((ticker) => ticker.destroy()); + } + + let w = []; + const k = (n) => { + for (const i of w) + if (i.status === "running") + try { + for (const t of i.funcs) + t(n - i.startTime); + } catch (t) { + i.destroy(), console.error(t); + } + requestAnimationFrame(k); + }; + requestAnimationFrame(k); + + /** Ticker类 */ + class I { + constructor() { + this.funcs = /* @__PURE__ */ new Set(); + this.status = "stop"; + this.startTime = 0; + this.status = "running", w.push(this), requestAnimationFrame((i) => this.startTime = i); + } + add(i) { + return this.funcs.add(i), this; + } + remove(i) { + return this.funcs.delete(i), this; + } + clear() { + this.funcs.clear(); + } + destroy() { + core.plugin.tickerSet.delete(this); + this.clear(), this.stop(); + } + stop() { + this.status = "stop", w = w.filter((i) => i !== this); + } + } + /** AnimationBase类 */ + class F { + constructor() { + this.timing = Date.now; + this.relation = "absolute"; + this.easeTime = 0; + this.applying = {}; + this.getTime = Date.now; + const ticker = new I(); + this.ticker = ticker; + this.value = {}; + this.listener = {}; + this.timing = (i) => i; + } + async all() { + if (Object.values(this.applying).every((i) => i === !0)) + throw new ReferenceError("There is no animates to be waited."); + await new Promise((i) => { + const t = () => { + Object.values(this.applying).every((e) => e === !1) && (this.unlisten("end", t), i("all animated.")); + }; + this.listen("end", t); + }); + } + async n(i) { + const t = Object.values(this.applying).filter((s) => s === !0).length; + if (t < i) + throw new ReferenceError( + `You are trying to wait ${i} animate, but there are only ${t} animate animating.` + ); + let e = 0; + await new Promise((s) => { + const r = () => { + e++, e === i && (this.unlisten("end", r), s(`${i} animated.`)); + }; + this.listen("end", r); + }); + } + async w(i) { + if (this.applying[i] === !1) + throw new ReferenceError(`The ${i} animate is not animating.`); + await new Promise((t) => { + const e = () => { + this.applying[i] === !1 && (this.unlisten("end", e), t(`${i} animated.`)); + }; + this.listen("end", e); + }); + } + listen(i, t) { + var e, s; + (s = (e = this.listener)[i]) != null || (e[i] = []), this.listener[i].push(t); + } + unlisten(i, t) { + const e = this.listener[i].findIndex((s) => s === t); + if (e === -1) + throw new ReferenceError( + "You are trying to remove a nonexistent listener." + ); + this.listener[i].splice(e, 1); + } + hook(...i) { + const t = Object.entries(this.listener).filter( + (e) => i.includes(e[0]) + ); + for (const [e, s] of t) + for (const r of s) + r(this, e); + } + } + + function y(n) { + return n != null; + } + async function R(n) { + return new Promise((i) => setTimeout(i, n)); + } + /** Animation类 */ + class j extends F { + constructor() { + super(); + this.shakeTiming; + this.path; + this.multiTiming; + this.value = {}; + this.size = 1; + this.angle = 0; + this.targetValue = { + system: { + move: [0, 0], + moveAs: [0, 0], + resize: 0, + rotate: 0, + shake: 0, + /** @type {number[]} */"@@bind": [] + }, + custom: {} + }; + this.animateFn = { + system: { + move: [() => { }, () => { }], + moveAs: () => { }, + resize: () => { }, + rotate: () => { }, + shake: () => { }, + "@@bind": () => { } + }, + custom: {} + }; + this.ox = 0; + this.oy = 0; + this.sx = 0; + this.sy = 0; + this.bindInfo = []; + this.timing = (t) => t, this.shakeTiming = (t) => t, this.multiTiming = (t) => [t, t], this.path = (t) => [t, t], this.applying = { + move: !1, + scale: !1, + rotate: !1, + shake: !1 + }, this.ticker.add(() => { + const { running: t } = this.listener; + if (y(t)) + for (const e of t) + e(this, "running"); + }); + } + get x() { + return this.ox + this.sx; + } + get y() { + return this.oy + this.sy; + } + mode(t, e = !1) { + return typeof t(0) == "number" ? e ? this.shakeTiming = t : this.timing = t : this.multiTiming = t, this; + } + time(t) { + return this.easeTime = t, this; + } + relative() { + return this.relation = "relative", this; + } + absolute() { + return this.relation = "absolute", this; + } + bind(...t) { + return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = t, this; + } + unbind() { + return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = [], this; + } + move(t, e) { + return this.applying.move && this.end(!0, "move"), this.applySys("ox", t, "move"), this.applySys("oy", e, "move"), this; + } + rotate(t) { + return this.applySys("angle", t, "rotate"), this; + } + scale(t) { + return this.applySys("size", t, "resize"), this; + } + shake(t, e) { + this.applying.shake === !0 && this.end(!0, "shake"), this.applying.shake = !0; + const { easeTime: s, shakeTiming: r } = this, l = this.getTime(); + if (this.hook("start", "shakestart"), s <= 0) + return this.end(!1, "shake"), this; + const a = () => { + const c = this.getTime() - l; + if (c > s) { + this.ticker.remove(a), this.applying.shake = !1, this.sx = 0, this.sy = 0, this.hook("end", "shakeend"); + return; + } + const h = c / s, + m = r(h); + this.sx = m * t, this.sy = m * e; + }; + return this.ticker.add(a), this.animateFn.system.shake = a, this; + } + moveAs(t) { + this.applying.moveAs && this.end(!0, "moveAs"), this.applying.moveAs = !0, this.path = t; + const { easeTime: e, relation: s, timing: r } = this, l = this.getTime(), [a, u] = [this.x, this.y], [c, h] = (() => { + if (s === "absolute") + return t(1); { + const [d, f] = t(1); + return [a + d, u + f]; + } + })(); + if (this.hook("start", "movestart"), e <= 0) + return this.end(!1, "moveAs"), this; + const m = () => { + const f = this.getTime() - l; + if (f > e) { + this.end(!0, "moveAs"); + return; + } + const g = f / e, + [v, x] = t(r(g)); + s === "absolute" ? (this.ox = v, this.oy = x) : (this.ox = a + v, this.oy = u + x); + }; + return this.ticker.add(m), this.animateFn.system.moveAs = m, this.targetValue.system.moveAs = [c, h], this; + } + register(t, e) { + if (typeof this.value[t] == "number") + return this.error( + `Property ${t} has been regietered twice.`, + "reregister" + ); + this.value[t] = e, this.applying[t] = !1; + } + apply(t, e) { + this.applying[t] === !0 && this.end(!1, t), t in this.value || this.error( + `You are trying to execute nonexistent property ${t}.` + ), this.applying[t] = !0; + const s = this.value[t], + r = this.getTime(), + { timing: l, relation: a, easeTime: u } = this, + c = a === "absolute" ? e - s : e; + if (this.hook("start"), u <= 0) + return this.end(!1, t), this; + const h = () => { + const d = this.getTime() - r; + if (d > u) { + this.end(!1, t); + return; + } + const f = d / u, + g = l(f); + this.value[t] = s + g * c; + }; + return this.ticker.add(h), this.animateFn.custom[t] = h, this.targetValue.custom[t] = c + s, this; + } + applyMulti() { + this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.applying["@@bind"] = !0; + const t = this.bindInfo, + e = t.map((h) => this.value[h]), + s = this.getTime(), + { multiTiming: r, relation: l, easeTime: a } = this, + u = r(1); + if (u.length !== e.length) + throw new TypeError( + `The number of binded animate attributes and timing function returns's length does not match. binded: ${t.length}, timing: ${u.length}` + ); + if (this.hook("start"), a <= 0) + return this.end(!1, "@@bind"), this; + const c = () => { + const m = this.getTime() - s; + if (m > a) { + this.end(!1, "@@bind"); + return; + } + const d = m / a, + f = r(d); + t.forEach((g, v) => { + l === "absolute" ? this.value[g] = f[v] : this.value[g] = e[v] + f[v]; + }); + }; + return this.ticker.add(c), this.animateFn.custom["@@bind"] = c, this.targetValue.system["@@bind"] = u, this; + } + applySys(t, e, s) { + s !== "move" && this.applying[s] === !0 && this.end(!0, s), this.applying[s] = !0; + const r = this[t], + l = this.getTime(), + a = this.timing, + u = this.relation, + c = this.easeTime, + h = u === "absolute" ? e - r : e; + if (this.hook("start", `${s}start`), c <= 0) + return this.end(!0, s); + const m = () => { + const f = this.getTime() - l; + if (f > c) { + this.end(!0, s); + return; + } + const g = f / c, + v = a(g); + this[t] = r + h * v, t !== "oy" && this.hook(s); + }; + this.ticker.add(m), t === "ox" ? this.animateFn.system.move[0] = m : t === "oy" ? this.animateFn.system.move[1] = m : this.animateFn.system[s] = m, s === "move" ? (t === "ox" && (this.targetValue.system.move[0] = h + r), t === "oy" && (this.targetValue.system.move[1] = h + r)) : s !== "shake" && (this.targetValue.system[s] = h + r); + } + error(t, e) { + throw e === "repeat" ? new Error( + `Cannot execute the same animation twice. Info: ${t}` + ) : e === "reregister" ? new Error( + `Cannot register an animated property twice. Info: ${t}` + ) : new Error(t); + } + end(t, e) { + if (t === !0) + if (this.applying[e] = !1, e === "move" ? (this.ticker.remove(this.animateFn.system.move[0]), this.ticker.remove(this.animateFn.system.move[1])) : e === "moveAs" ? this.ticker.remove(this.animateFn.system.moveAs) : e === "@@bind" ? this.ticker.remove(this.animateFn.system["@@bind"]) : this.ticker.remove( + this.animateFn.system[e] + ), e === "move") { + const [s, r] = this.targetValue.system.move; + this.ox = s, this.oy = r, this.hook("moveend", "end"); + } else if (e === "moveAs") { + const [s, r] = this.targetValue.system.moveAs; + this.ox = s, this.oy = r, this.hook("moveend", "end"); + } else + e === "rotate" ? (this.angle = this.targetValue.system.rotate, this.hook("rotateend", "end")) : e === "resize" ? (this.size = this.targetValue.system.resize, this.hook("resizeend", "end")) : e === "@@bind" ? this.bindInfo.forEach((r, l) => { + this.value[r] = this.targetValue.system["@@bind"][l]; + }) : (this.sx = 0, this.sy = 0, this.hook("shakeend", "end")); + else + this.applying[e] = !1, this.ticker.remove(this.animateFn.custom[e]), this.value[e] = this.targetValue.custom[e], this.hook("end"); + } + } + class O extends F { + constructor() { + super(); + this.now = {}; + this.target = {}; + this.transitionFn = {}; + this.value = undefined; + this.handleSet = (t, e, s) => (this.transition(e, s), !0); + this.handleGet = (t, e) => this.now[e]; + this.timing = (t) => t, this.value = new Proxy(this.target, { + set: this.handleSet, + get: this.handleGet + }); + } + mode(t) { + return this.timing = t, this; + } + time(t) { + return this.easeTime = t, this; + } + relative() { + return this.relation = "relative", this; + } + absolute() { + return this.relation = "absolute", this; + } + transition(t, e) { + if (e === this.target[t]) + return this; + if (!y(this.now[t])) + return this.now[t] = e, this; + this.applying[t] && this.end(t, !0), this.applying[t] = !0, this.hook("start"); + const s = this.getTime(), + r = this.easeTime, + l = this.timing, + a = this.now[t], + u = e + (this.relation === "absolute" ? 0 : a), + c = u - a; + this.target[t] = u; + const h = () => { + const d = this.getTime() - s; + if (d >= r) { + this.end(t); + return; + } + const f = d / r; + this.now[t] = l(f) * c + a, this.hook("running"); + }; + return this.transitionFn[t] = h, this.ticker.add(h), r <= 0 ? (this.end(t), this) : this; + } + end(t, e = !1) { + const s = this.transitionFn[t]; + if (!y(s)) + throw new ReferenceError( + `You are trying to end an ended transition: ${t}` + ); + this.ticker.remove(this.transitionFn[t]), delete this.transitionFn[t], this.applying[t] = !1, this.hook("end"), e || (this.now[t] = this.target[t]); + } + } + const T = (...n) => n.reduce((i, t) => i + t, 0), + b = (n) => { + if (n === 0) + return 1; + let i = n; + for (; n > 1;) + n--, i *= n; + return i; + }, + A = (n, i) => Math.round(b(i) / (b(n) * b(i - n))), + p = (n, i, t = (e) => 1 - i(1 - e)) => n === "in" ? i : n === "out" ? t : n === "in-out" ? (e) => e < 0.5 ? i(e * 2) / 2 : 0.5 + t((e - 0.5) * 2) / 2 : (e) => e < 0.5 ? t(e * 2) / 2 : 0.5 + i((e - 0.5) * 2) / 2, + $ = Math.cosh(2), + z = Math.acosh(2), + V = Math.tanh(3), + P = Math.atan(5); + + function Y() { + return (n) => n; + } + + function q(...n) { + const i = [0].concat(n); + i.push(1); + const t = i.length, + e = Array(t).fill(0).map((s, r) => A(r, t - 1)); + return (s) => { + const r = e.map((l, a) => l * i[a] * (1 - s) ** (t - a - 1) * s ** a); + return T(...r); + }; + } + + function U(n, i) { + if (n === "sin") { + const t = (s) => Math.sin(s * Math.PI / 2); + return p(i, (s) => 1 - t(1 - s), t); + } + if (n === "sec") { + const t = (s) => 1 / Math.cos(s); + return p(i, (s) => t(s * Math.PI / 3) - 1); + } + throw new TypeError( + "Unexpected parameters are delivered in trigo timing function." + ); + } + + function C(n, i) { + if (!Number.isInteger(n)) + throw new TypeError( + "The first parameter of power timing function only allow integer." + ); + return p(i, (e) => e ** n); + } + + function G(n, i) { + if (n === "sin") + return p(i, (e) => (Math.cosh(e * 2) - 1) / ($ - 1)); + if (n === "tan") { + const t = (s) => Math.tanh(s * 3) * 1 / V; + return p(i, (s) => 1 - t(1 - s), t); + } + if (n === "sec") { + const t = (s) => 1 / Math.cosh(s); + return p(i, (s) => 1 - (t(s * z) - 0.5) * 2); + } + throw new TypeError( + "Unexpected parameters are delivered in hyper timing function." + ); + } + + function N(n, i) { + if (n === "sin") { + const t = (s) => Math.asin(s) / Math.PI * 2; + return p(i, (s) => 1 - t(1 - s), t); + } + if (n === "tan") { + const t = (s) => Math.atan(s * 5) / P; + return p(i, (s) => 1 - t(1 - s), t); + } + throw new TypeError( + "Unexpected parameters are delivered in inverse trigo timing function." + ); + } + /** @param {(input:number) => number} [i=() => 1] */ + function B(n, i = () => 1) { + let t = -1; + return (e) => (t *= -1, e < 0.5 ? n * i(e * 2) * t : n * i((1 - e) * 2) * t); + } + + function D(n, i = 1, t = [0, 0], e = 0, s = (l) => 1, r = !1) { + return (l) => { + const a = i * l * Math.PI * 2 + e * Math.PI / 180, + u = Math.cos(a), + c = Math.sin(a), + h = n * s(s(r ? 1 - l : l)); + return [h * u + t[0], h * c + t[1]]; + }; + } + + function H(n, i, ...t) { + const e = [n].concat(t); + e.push(i); + const s = e.length, + r = Array(s).fill(0).map((l, a) => A(a, s - 1)); + return (l) => { + const a = r.map((c, h) => c * e[h][0] * (1 - l) ** (s - h - 1) * l ** h), + u = r.map((c, h) => c * e[h][1] * (1 - l) ** (s - h - 1) * l ** h); + return [T(...a), T(...u)]; + }; + } + + core.plugin.animate = { + Animation: j, + AnimationBase: F, + Ticker: I, + Transition: O, + bezier: q, + bezierPath: H, + circle: D, + hyper: G, + inverseTrigo: N, + linear: Y, + power: C, + shake: B, + sleep: R, + trigo: U, + } + + }, + "drawItemDetail": function () { + /* 宝石血瓶左下角显示数值 + * 需要将 变量:itemDetail改为true才可正常运行 + * 请尽量减少勇士的属性数量,否则可能会出现严重卡顿(划掉,现在你放一万个属性也不会卡) + * 注意:这里的属性必须是core.status.hero里面的,flag无法显示 + * 如果不想显示,可以core.setFlag("itemDetail", false); + * 然后再core.getItemDetail(); + * 如有bug在大群或造塔群@古祠 + */ + + // 忽略的道具 + const ignore = ['superPotion']; + + // 取消注释下面这句可以减少超大地图的判定。 + // 如果地图宝石过多,可能会略有卡顿,可以尝试取消注释下面这句话来解决。 + // core.bigmap.threshold = 256; + const origin = core.control.updateStatusBar; + core.updateStatusBar = core.control.updateStatusBar = function () { + if (core.hasFlag('__statistics__')) return; + else return origin.apply(core.control, arguments); + } + + core.control.updateDamage = function (floorId, ctx) { + floorId = floorId || core.status.floorId; + if (!floorId || core.status.gameOver || main.mode != 'play') return; + const onMap = ctx == null; + + // 没有怪物手册 + if (!core.hasItem('book')) return; + core.status.damage.posX = core.bigmap.posX; + core.status.damage.posY = core.bigmap.posY; + if (!onMap) { + const width = core.floors[floorId].width, + height = core.floors[floorId].height; + // 地图过大的缩略图不绘制显伤 + if (width * height > core.bigmap.threshold) return; + } + this._updateDamage_damage(floorId, onMap); + this._updateDamage_extraDamage(floorId, onMap); + core.getItemDetail(floorId); // 宝石血瓶详细信息 + this.drawDamage(ctx); + }; + + function getRatio() { + let ratio = (core.status.thisMap?.ratio) ?? 1; + const currEvent = core.status.event; + if (!currEvent) return ratio; + switch (currEvent.id) { + case 'viewMaps': //调整浏览地图时的倍率 + if (currEvent.data) { + const viewMapFloorId = (currEvent.data.floorId); + ratio = core.status.maps[viewMapFloorId].ratio; + } + break; + case 'fly': //调整在楼传界面浏览地图时的倍率 + ratio = core.status.maps[core.floorIds[currEvent.data]].ratio; + break; + } + return ratio; + } + + // 因为core.status.hero是进存档的,必定可序列化 + const clone = (typeof structuredClone === 'function') ? structuredClone : (ele) => JSON.parse(JSON.stringify(ele)); + + // 数值类型的属性(包含装备的百分比提升项) + const statusName = new Set(['atk', 'atkper', 'def', 'defper', 'mdef', 'mdefper', 'hp', + 'hpmax', 'hpmaxper', 'mana', 'manamax', 'money', 'exp']); + // 获取宝石信息 并绘制 + this.getItemDetail = function (floorId) { + if (!core.getFlag('itemDetail')) return; + if (!core.status.thisMap) return; + floorId = floorId ?? core.status.thisMap.floorId; + const beforeRatio = core.status.thisMap.ratio; + core.status.thisMap.ratio = core.status.maps[floorId].ratio; + let diff = {}; + const before = core.status.hero; + const hero = clone(core.status.hero); + const handler = { + set(target, key, v) { + if (!statusName.has(key)) return true; // 非数值类型的属性就不用记了,显示也没有意义 + diff[key] = v - (target[key] || 0); + if (!diff[key]) diff[key] = void 0; + return true; + } + }; + core.status.hero = new Proxy(hero, handler); + core.status.maps[floorId].blocks.forEach(function (block) { + if ( + block.event.cls !== 'items' || + ignore.includes(block.event.id) || + block.disable + ) + return; + const x = block.x, + y = block.y; + // v2优化,只绘制范围内的部分 + if (core.bigmap.v2) { + if ( + x < core.bigmap.posX - core.bigmap.extend || + x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend || + y < core.bigmap.posY - core.bigmap.extend || + y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend + ) { + return; + } + } + diff = {}; + const id = block.event.id; + const item = core.material.items[id]; + switch (item.cls) { + case 'equips': { + // 装备也显示 + diff = item.equip.value ?? {}; + const per = item.equip.percentage ?? {}; + for (const name in per) { + diff[name + 'per'] = per[name].toString() + '%'; + } + break; + } + case 'items': { + // 跟数据统计原理一样 执行效果 前后比较 + core.setFlag('__statistics__', true); + if (item.itemEffect != null) { + try { + eval(item.itemEffect); + } catch (error) { } + } + const ratio = getRatio(); + const effectObj = core.getItemEffectValue(id, ratio); + for (let statusName in effectObj) { + if (!effectObj.hasOwnProperty(statusName)) continue; + if (!diff.hasOwnProperty(statusName)) diff[statusName] = 0; + diff[statusName] += effectObj[statusName]; + } + break; + } + } + drawItemDetail(diff, x, y); + }); + core.status.thisMap.ratio = beforeRatio; + core.status.hero = before; + window.hero = before; + window.flags = before.flags; + }; + + // 绘制 + function drawItemDetail(diff, x, y) { + const px = 32 * x + 2, + py = 32 * y + 30; + let content = ''; + // 获得数据和颜色 + let i = 0; + for (const name in diff) { + if (!diff[name]) continue; + let color = '#fff'; + + if (typeof diff[name] === 'number') + content = core.formatBigNumber(diff[name], true); + else content = diff[name]; + switch (name) { + case 'atk': + case 'atkper': + color = '#FF7A7A'; + break; + case 'def': + case 'defper': + color = '#00E6F1'; + break; + case 'mdef': + case 'mdefper': + color = '#6EFF83'; + break; + case 'hp': + color = '#A4FF00'; + break; + case 'hpmax': + case 'hpmaxper': + color = '#F9FF00'; + break; + case 'mana': + case 'manamax': + color = '#CC6666'; + break; + case 'money': + color = '#FFD700'; + break; + case 'exp': + color = '#BA55D3'; + break; + } + // 绘制 + core.status.damage.data.push({ + text: content, + px: px, + py: py - 10 * i, + color: color + }); + i++; + } + } + }, + "autoClear": function () { + // 在此增加新插件 + /** + * --------------- 使用说明 --------------- + * 变量autoGet控制自动拾取开关 + * 变量autoBattle控制自动清怪开关 + */ + const ctxName = 'autoClear'; + + // 每走一步后自动拾取的判定要放在阻击结算之后,见libs,并不写在本插件当中 + + core.plugin.autoClear = auto; + + function willLvUp(exp) { + const nextExp = core.getNextLvUpNeed(); + if (typeof exp === 'number' && typeof nextExp === 'number' && exp >= nextExp) return true; + return false; + } + + /** + * 是否清这个怪,可以修改这里来实现对不同怪的不同操作 + * @param {string} enemy + * @param {number} x + * @param {number} y + */ + function canBattle(enemy, x, y) { + const loc = `${x},${y}`; + const floor = core.floors[core.status.floorId]; + const e = core.getEnemyValue(enemy, null, x, y); + const hasEvent = + has(floor.afterBattle[loc]) || has(floor.beforeBattle[loc]) || + has(e.beforeBattle) || has(e.afterBattle) || + has(floor.events[loc]) || willLvUp(e.exp); // 防止有升级后事件 + + // 有事件,不清 + if (hasEvent) return false; + + const cache = core.status.checkBlock.cache; + const hasGuards = has(cache) && has(cache[loc]) && + has(cache[loc]["guards"]) && cache[loc]["guards"].length > 0; // 该敌人会被支援 + if (hasGuards) return false; + + // 有特定特殊属性的怪不清 + if ( + core.hasSpecial(e.special, 12) || // 中毒 + core.hasSpecial(e.special, 13) || // 衰弱 + core.hasSpecial(e.special, 14) || // 诅咒 + core.hasSpecial(e.special, 19) || // 自爆 + core.hasSpecial(e.special, 21) || // 退化 + core.hasSpecial(e.special, 26) || // 支援 + core.hasSpecial(e.special, 27) || // 捕捉:逻辑上应该让怪物来找角色 + core.hasSpecial(e.special, 28) || // 追猎:逻辑上应该让怪物来找角色 + core.hasSpecial(e.special, 29) // 败移:特殊战后事件 + ) { + return false; + } + const damage = core.getDamageInfo(enemy, void 0, x, y)?.damage; + // 0伤或负伤,清 + if (has(damage) && damage <= 0) return true; + return false; + } + + /** + * 判断一个点是否能遍历 + */ + function judge(block, nx, ny, tx, ty, dir, floorId, autoBattle, autoGet) { + if (!has(block)) { // 说明什么都没有,没事件也没图块 + return { type: "none", canGoThrough: true }; + } + const cls = block.event.cls; + const loc = `${tx},${ty}`; + const floor = core.floors[floorId]; + const changeFloor = floor.changeFloor[loc]; + const isEnemy = autoBattle && cls.startsWith('enemy'), + isItem = autoGet && cls === 'items'; + // 因为没有判定往来图块的通行性,这里宁可严格一点,非空地(block.id === 0)一律不给穿 + if (has(changeFloor)) { + if ((changeFloor.ignoreChangeFloor ?? core.flags.ignoreChangeFloor) && block.id === 0) { + return { type: "unknown", canGoThrough: true }; + } + return { type: "unknown", canGoThrough: false }; + } + + if (has(core.floors[floorId].events[loc])) return { type: "unknown", canGoThrough: false }; + + if (isEnemy) return { type: "enemy", canGoThrough: true }; + if (isItem) return { type: "item", canGoThrough: true }; + + return { type: "unknown", canGoThrough: false }; + } + + /** + * 是否捡拾这个物品 + */ + function canGetItem(item, loc, floorId = core.status.floorId) { + // 可以用于检测道具是否应该被捡起,例如如果捡起后血量超过80%则不捡起可以这么写: + // if (item.cls === 'items') { + // let diff = {}; + // const before = core.status.hero; + // const hero = core.clone(core.status.hero); + // const handler = { + // set(target, key, v) { + // diff[key] = v - (target[key] || 0); + // if (!diff[key]) diff[key] = void 0; + // return true; + // } + // }; + // core.status.hero = new Proxy(hero, handler); + + // eval(item.itemEffect); + + // core.status.hero = before; + // window.hero = before; + // window.flags = before.flags; + // if ( + // diff.hp && + // diff.hp + core.status.hero.hp > core.status.hero.hpmax * 0.8 + // ) + // return false; + // } + const floor = core.floors[floorId]; + if (has(floor.afterGetItem[loc])) return false; + if (item.cls === 'items') { + const itemEffectType = core.getItemEffectType(item.id); + if (core.hasFlag('noRouting_HP') && itemEffectType.includes('hp')) return false; + if (core.hasFlag('noRouting_MDEF') && itemEffectType.includes('mdef')) return false; + if (core.hasFlag('noRouting_ATK') && itemEffectType.includes('atk')) return false; + if (core.hasFlag('noRouting_DEF') && itemEffectType.includes('def')) return false; + } + return true; + } + + /** + * @template T + * @param {T} v + * @returns {v is NonNullable} + */ + function has(v) { + return v !== null && v !== undefined; + } + + function hasBlockDamage(loc) { + const checkblockInfo = core.status.checkBlock; + const damage = checkblockInfo.damage[loc]; + const ambush = checkblockInfo.ambush[loc]; + const repulse = checkblockInfo.repulse[loc]; + const chase = checkblockInfo.chase[loc]; + + return (has(damage) && damage > 0) || has(ambush) || has(repulse) || has(chase); + } + + class AttractAnimate { + constructor() { + this.name = 'attractAnimate'; + this.isPlaying = false; + this.nodes = []; + this.lastTime = -1; + this.thr = 5; // 缓动比例倒数,越大移动越慢 + } + + add(id, x, y, callback) { + if (core.isReplaying()) return; + this.nodes.push({ id, x, y, callback }); + } + + start() { + if (this.isPlaying) return; + if (core.isReplaying()) return; + if (core.getLocalStorage('skipPerform')) return; + this.isPlaying = true; + core.registerAnimationFrame(this.name, true, this.update.bind(this)); + this.ctx = core.createCanvas(this.name, 0, 0, core.__PIXELS__, core.__PIXELS__, 120); + } + + remove() { + core.unregisterAnimationFrame(this.name); + core.deleteCanvas(this.name); + this.isPlaying = false; + } + + clear() { + this.nodes = []; + this.remove(); + } + + update(timeStamp) { + const { name, thr, nodes } = this; + + if (this.lastTime < 0) this.lastTime = timeStamp; + if (timeStamp - this.lastTime < 20) return; + this.lastTime = timeStamp; + + core.clearMap(name); + + const heroCenterX = core.status.heroCenter.px - 16; + const heroCenterY = core.status.heroCenter.py - 16; + + for (const n of nodes) { + const dx = heroCenterX - n.x; + const dy = heroCenterY - n.y; + + if (Math.abs(dx) <= thr && Math.abs(dy) <= thr) { + n.dead = true; + } else { + n.x += ~~(dx / thr); + n.y += ~~(dy / thr); + } + + core.drawIcon(name, n.id, n.x, n.y, 32, 32); + } + + // 过滤掉 dead 的节点并执行回调 + const remainingNodes = []; + for (const n of nodes) { + if (n.dead && n.callback) { + n.callback(); + } + if (!n.dead) { + remainingNodes.push(n); + } + } + this.nodes = remainingNodes; + + if (this.nodes.length === 0) { + this.remove(); + } + } + } + + const animateHwnd = new AttractAnimate(); + + /** 拾取单个物品的动画 */ + this.pickOneItemAnimate = function (id, x, y, callback) { + if (core.isReplaying()) return; + animateHwnd.add(id, x, y, callback); + animateHwnd.start(); + }; + /** 在每次切换楼层后调用 */ + this.clearAttractAnimate = function () { + animateHwnd.clear(); + } + + /** + * 广搜,搜索可以到达的需要清的怪 + * @param {string} floorId + */ + function bfs(floorId, deep = Infinity) { + core.extractBlocks(floorId); + const objs = core.getMapBlocksObj(floorId); + const bgMap = core.getBgMapArray(floorId); + const { x, y } = core.status.hero.loc; + const dir = /** @type {[direction, number, number][]} */ Object.entries(core.utils.scan).map(v => [v[0], v[1].x, v[1].y]); + const floor = core.status.maps[floorId]; + + /** @type {[number, number][]} */ + const queue = [ + [x, y] + ]; + const mapped = { + [`${x},${y}`]: true + }; + + const autoBattle = core.getFlag('autoBattle', false), + autoGet = core.getFlag('autoGet', false); + if (!autoGet && !autoBattle) return; + + while (queue.length > 0 && deep > 0) { + const [nx, ny] = queue.shift(); + dir.forEach(v => { + const [tx, ty] = [nx + v[1], ny + v[2]]; + if (tx < 0 || ty < 0 || tx >= floor.width || ty >= floor.height) { + return; + } + const loc = `${tx},${ty}`; + if (mapped[loc]) return; + const block = objs[loc]; + mapped[loc] = true; + if (core.onSki(bgMap[ty][tx])) return; // bfs不允许穿过滑冰 + const { type, canGoThrough } = judge(block, nx, ny, tx, ty, v[0], floorId, autoBattle, autoGet); + if (!canGoThrough) return; + + if (type === 'enemy') { + if (canBattle(block.event.id, tx, ty) && !block.disable) { + core.battle(block.event.id, tx, ty); + core.updateCheckBlock(); + } else { + return; + } + } else if (type === 'item') { + const item = core.material.items[block.event.id]; + if (canGetItem(item, loc, floorId)) { + if (!core.isReplaying()) animateHwnd.add(item.id, 32 * tx, 32 * ty); + core.getItem(item.id, 1, tx, ty); + } else { + return; + } + } + if (hasBlockDamage(loc)) return; + queue.push([tx, ty]); + }); + deep--; + } + } + + function auto() { + if (!core.status.floorId || !core.status.checkBlock.damage) return; // 这两个条件不知道什么情形下会出现 + if (core.status.event.id == 'action' || core.events.onSki() || core.status.lockControl) return; // 在冰上不允许触发自动清怪 + const before = flags.__forbidSave__; + const { x, y } = core.status.hero.loc; + const floor = core.floors[core.status.floorId]; + const loc = `${x},${y}`; + const hasEvent = has(floor.events[loc]); + if (hasEvent) return; // 如果有事件,直接不清了 + const block = core.getBlock(x, y); + if (block != null && block.event.cls !== 'items') return; // 角色站的位置为空地和物品以外的图块,不清(例如箭头,可能无法返回) + + let deep = Infinity; + if (hasBlockDamage(loc)) { + deep = core.flags.enableGentleClick ? 1 : 0; // 角色站的位置有地图伤害时,仍然允许轻点附近1格 + } + flags.__forbidSave__ = true; + flags.__statistics__ = true; + const ctx = core.getContextByName(ctxName); + if (!ctx) { + core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 75); + core.setAlpha(ctxName, 0.6); + } + bfs(core.status.floorId, deep); + if (!core.isReplaying()) animateHwnd.start(); + flags.__statistics__ = false; + flags.__forbidSave__ = before; + core.updateStatusBar(); + } + }, + "scrollingText": function () { + // 本插件用于绘制在线留言 + // 说明:https://h5mota.com/bbs/thread/?tid=1017 + // 目前使用core.http代替帖子中提到的axios + + /** 塔的英文名 */ + const towerName = core.firstData.name; + + let [W, H] = [core.__SIZE__, core.__SIZE__]; + let [WIDTH, HEIGHT] = [core.__PIXELS__, core.__PIXELS__]; + + //#region 弹幕的收发 + this.getComment = function () { + if (core.isReplaying()) return; + let form = new FormData(); + form.append('type', '1'); + form.append('towername', towerName); + core.utils.http( + 'POST', + 'https://h5mota.com/backend/tower/barrage.php', + form, + function (res) { + try { + res = JSON.parse(res); + console.log(res); + core.drawTip('接收成功!', 'postman'); + core.playSound('item.mp3'); + let commentCollection = {}; + const commentList = res?.list; + const isEmpty = /^\s*$/; + for (let i = 0, l = commentList.length; i <= l - 1; i++) { + if (isEmpty.test(commentList[i]?.comment)) continue; + const commentTagsList = commentList[i].tags.split(','); + const [cFloorId, cX, cY] = commentTagsList; + if (0 <= cX && cX <= W - 1 && 0 <= cY && cY <= H - 1 && core.floorIds.includes(cFloorId)) { + if (!commentCollection.hasOwnProperty(cFloorId)) { commentCollection[cFloorId] = {}; } + const str = cX + ',' + cY; + if (!commentCollection[cFloorId].hasOwnProperty(str)) { commentCollection[cFloorId][str] = []; } + commentCollection[cFloorId][str].push(commentList[i]?.comment); + } + } + core.setFlag('commentCollection', commentCollection); + } catch (err) { + core.drawFailTip('在线留言接收失败! ' + err.message, 'postman'); + } + }, + function (err) { + console.log(err); + if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' }; + else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') }; + else err = JSON.parse(err); + core.drawFailTip('在线留言接收失败! ' + err?.message, 'postman'); + }, + null, null, null, 1000 + ); + } + + this.postComment = function (comment, tags) { + if (core.isReplaying()) return; + const isEmpty = /^\s*$/; + if (isEmpty.test(comment)) { + core.drawFailTip('您输入的消息为空,请重发!', 'postman'); + return; + } + let form = new FormData(); + form.append('type', '2'); + form.append('towername', towerName); + form.append('comment', comment); + form.append('tags', tags); + core.utils.http( + 'POST', + 'https://h5mota.com/backend/tower/barrage.php', + form, + function (res) { + try { + res = JSON.parse(res); + console.log(res); + if (res?.code === 0) { + core.drawTip('提交成功! ', 'postman') + } else { + core.drawTip('提交失败! ' + res?.message, 'postman'); + } + } catch (err) { + core.drawFailTip('提交失败! ' + err.message, 'postman'); + } + }, + function (err) { + console.log(err); + if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' }; + else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') }; + else err = JSON.parse(err); + core.drawFailTip('提交失败! ' + err?.message, 'postman'); + }, + null, null, null, 1000 + ); + } + //#endregion + + /** 若变量comment为真,在每层切换时在地上有弹幕的地方显示相应图标。 */ + this.drawCommentSign = function () { + if (!core.hasFlag('comment') || core.isReplaying()) return; + let commentCollection = core.getFlag('commentCollection', {}), + floorId = core.status.floorId; + core.createCanvas('sign', 0, 0, WIDTH, HEIGHT, 61); + core.setOpacity('sign', 0.6); + if (commentCollection.hasOwnProperty(floorId)) { + for (let pos in commentCollection[floorId]) { + const l = commentCollection[floorId][pos].length; + for (let i = 0; i <= l - 1; i++) { + const [x, y] = pos.split(',').map(x => Number(x)); + core.drawIcon('sign', 'postman', 32 * x, 32 * y); + break; + } + } + } + } + + /** 立即清除楼层的弹幕图标。关闭弹幕相关设置时调用。 */ + this.clearCommentSign = function () { + core.deleteCanvas('sign'); + } + + /** 默认一次显示的弹幕数 */ + const showNum = 5; + + // 每走一步或瞬移,调用该函数,若目标点有弹幕,显示之 + this.showComment = function (x, y) { + if (!core.getFlag('comment') || core.isReplaying()) return; + const commentCollection = core.getFlag('commentCollection', {}); + const floorId = core.status.floorId, + str = x + ',' + y; + if (commentCollection.hasOwnProperty(floorId) && + commentCollection[floorId].hasOwnProperty(str)) { + let commentArr = commentCollection[floorId][str].concat(); + const commentArrPicked = pickComment(commentArr, showNum); + drawComment(commentArrPicked); + } + } + + /** 返回从commentArr中挑选showNum个comment组成的数组*/ + function pickComment(commentArr, showNum) { + let showList = []; + if (commentArr.length <= showNum) { + showList = commentArr; + } else { + for (let i = 0; i <= showNum - 1; i++) { + const l = commentArr.length, + n = core.plugin.dice(l - 1); + showList.push(commentArr[n]); + commentArr.splice(n, 1); + } + } + return showList; + } + + function drawComment(commentArr) { + const l = commentArr.length; + let yList = generateCommentYList(20, HEIGHT - 20, showNum); + if (l < showNum) yList = getRandomElements(yList, l); + for (let i = 0; i <= l - 1; i++) { + drawCommentStr(commentArr[i], WIDTH + 20 * Math.random(), + yList[i], Math.random() * 0.1 + 0.1); + } + } + + /** 生成count个随机数,范围从min到max,作为弹幕的y坐标*/ + function generateCommentYList(min, max, count) { + let yList = Array(count).fill(0); + const distance = (max - min) / (count + 1); + for (let i = 0; i < count; i++) { + yList[i] = min + distance * (i + 1) + (Math.random() - 0.5) * (distance / 2); + } + return yList; + } + + function getRandomElements(arr, count) { + let result = [...arr]; + let len = result.length; + count = Math.min(len, count); + + for (let i = len - 1; i > len - 1 - count; i--) { + let j = Math.floor(Math.random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + + return result.slice(len - count); + } + + //#region 弹幕绘制部分 + const { Animation, linear, Ticker } = core.plugin.animate ?? {}; + const ctxName = 'scrollingText'; + + if (Ticker) { + const ticker = new Ticker(); + ticker.add(() => { + if (core.isReplaying()) return; + core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 136); //每帧重绘该画布 + }); + } + + /** + * 绘制弹幕 + * @example + * drawCommentStr('OK', 450, 200, 0.1); + * @param {string} content 弹幕的内容 + * @param {number} x 弹幕的初始x坐标 + * @param {number} y 弹幕的初始y坐标 + * @param {number} vx 弹幕的横向滚动速度 + */ + function drawCommentStr(content, x, y, vx) { + if (core.isReplaying() || !Animation) return; + const ani = new Animation(); + core.plugin.tickerSet.add(ani.ticker); + ani.ticker.add(() => { + core.fillText(ctxName, content, x + ani.x, y, 'white', '16px Verdana'); + }) + // 弹幕的最大长度5600,再长属于异常数据 + const aim = 100 + x + Math.min(core.calWidth(ctxName, content, '16px Verdana'), 5000); + ani.mode(linear()) + .time(aim / vx) + .absolute() + .move(-aim, 0) + ani.all().then(() => { + ani.ticker.destroy(); + }); + } + //#endregion + + }, + "uiBaseClass": function () { + // 本插件定义了一些用于绘制的基类 + /** + * @typedef {(x:number,y:number,px:number,py:number)=>void} posFunc + */ + /** 按钮基类 */ + class ButtonBase { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.disable = false; + this.status = 'none'; + + // 下面三项在initbtnMap时添加 + /** + * @type {MenuBase} 所在的Menu,用于触发重绘等事件 + */ + // @ts-ignore 将在菜单初始化时传入 + this.menu; + /** @type {string} 所在的Menu的画布名称 */ + this.ctx = ''; + /** @type {string|number} 自身在所在的Menu的btnMap中的索引 */ + this.key = ''; + + /** @type {posFunc} */ + this.ondown = () => { }; + /** @type {posFunc|undefined} */ + this.onmove = undefined; + /** @type {posFunc|undefined} */ + this.onup = undefined; + } + + /** 绘制该按钮的外观 + * @interface + */ + draw() { } + + /** 默认为矩形判定区 */ + inRange(px, py) { + return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h; + } + } + const KeyCodeEnum = { + BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, SpaceBar: 32, + PageUp: 33, PageDown: 34, Left: 37, Up: 38, Right: 39, + Down: 40, C: 67, Q: 81, T: 84, + }; + + /** @typedef {'ondown'|'onmove'|'onup'|'keyDown'|'keyUp'|'onmousewheel'} eventType */ + class MenuBase { + /** + * @param {string} name 菜单名称,作为绘制画布时的名称 + * @param {eventType[]} [toListen] + * @param {number} [x] + * @param {number} [y] + * @param {number} [w] + * @param {number} [h] + * @param {number} [zIndex] + */ + constructor(name, toListen, x, y, w, h, zIndex) { + this.name = name; + /** @type {Map} 本菜单上的按钮列表,每次绘制将触发按钮的draw事件 */ + this.btnMap = new Map(); + /** 当前画布是否正被绘制 */ + this.onDraw = false; + /** @type {Set} 当前画布需要监听的事件类型 */ + this.toListen = new Set(toListen); + + this.x = x ?? 0; + this.y = y ?? 0; + this.w = w ?? core.__PIXELS__; + this.h = h ?? core.__PIXELS__; + this.zIndex = zIndex ?? 136; // 136比uievent大1 + } + + // #region 监听事件 + /** 返回换算后的画布上的相对坐标 */ + convertCoordinate(px, py) { + return [px - this.x, py - this.y]; + } + + /** 默认为矩形判定区 */ + inRange(px, py) { + return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h; + } + + ondown(x, y, rawpx, rawpy) { + if (!this.inRange(rawpx, rawpy)) return; + const [px, py] = this.convertCoordinate(rawpx, rawpy); + this.ondownEvent(x, y, px, py); + this.ondownBtnEvent(x, y, px, py); + } + + /** 点击画布自身触发的事件 + * @interface + */ + ondownEvent(x, y, px, py) { } + + // btnMap 一个key指向一个对象 包含btn本身,对应事件,是否disable btn本身不包含event 由菜单赋予 + + /** 点击画布的按钮触发的事件 + * @interface + */ + ondownBtnEvent(x, y, px, py) { + this.btnMap.forEach((btn) => { + if (btn.disable) return; + if (btn.inRange(px, py)) { + btn.ondown(x, y, px, py); + } + }); + } + + /** 屏幕被鼠标滑动或手指拖动时触发的事件 + * @interface (x:number,y:number,px:number,py:number):void + */ + onmove(x, y, rawpx, rawpy) { + if (!this.inRange(rawpx, rawpy)) return; + const [px, py] = this.convertCoordinate(rawpx, rawpy); + this.onmoveEvent(x, y, px, py); + this.onmoveBtnEvent(x, y, px, py); + } + + onmoveEvent(x, y, px, py) { } + + onmoveBtnEvent(x, y, px, py) { + this.btnMap.forEach((btn) => { + if (btn.disable) return; + if (btn.inRange(px, py) && btn.onmove) { + btn.onmove(x, y, px, py); + } + }); + } + + /** 当屏幕被鼠标或手指放开时触发的事件 + * @interface (x:number,y:number,px:number,py:number):void + */ + onup(x, y, rawpx, rawpy) { + if (!this.inRange(rawpx, rawpy)) return; + const [px, py] = this.convertCoordinate(rawpx, rawpy); + this.onupEvent(x, y, px, py); + this.onupBtnEvent(x, y, px, py); + } + + onupEvent(x, y, px, py) { } + + onupBtnEvent(x, y, px, py) { + this.btnMap.forEach((btn) => { + if (btn.disable) return; + if (btn.inRange(px, py) && btn.onup) { + btn.onup(x, y, px, py); + } + }); + } + + /** 按键被按下时触发的事件 + * @interface (keycode:number)=>void + */ + keyDownEvent(keycode) { } + + /** 按键被放开时触发的事件 + * @interface (keycode:number,altkey?:boolean,fromReplay?:boolean)=>void) + */ + keyUpEvent(keycode, altkey, fromReplay) { } + + /** 鼠标滚轮滚动时触发的事件 + * @interface (direct:1|-1):void + */ + onmousewheelEvent(direct) { } + // #endregion + + /** + * @param {string | number} key + * @param {ButtonBase} btn + * @param {posFunc | {ondown:posFunc,onmove?:posFunc,onup?:posFunc}} [event] + */ + registerBtn(key, btn, event) { + btn.menu = this; + btn.ctx = this.name; + btn.key = key; + if (event == null) { } + else if (typeof event === 'function') { + btn.ondown = event; + } else { + const { ondown, onmove, onup } = event; + btn.ondown = ondown; + btn.onmove = onmove; + btn.onup = onup; + } + this.btnMap.set(key, btn); + } + + registerBtns(arr) { + arr.forEach(ele => { + const [key, btn, event] = ele; + this.registerBtn(key, btn, event); + }); + } + + // 创建并返回本菜单的画布 + createCanvas() { + return core.createCanvas(this.name, this.x, this.y, this.w, this.h, this.zIndex); + } + + drawButtonContent() { + this.btnMap.forEach((button) => { + if (!button.disable) button.draw(); + }) + } + + drawContent() { + this.drawButtonContent(); + this.onDraw = true; + } + + beginListen() { + if (this.toListen.has('ondown')) core.registerAction('ondown', this.name, this.ondown.bind(this), 100); + if (this.toListen.has('keyDown')) core.registerAction('keyDown', this.name, this.keyDownEvent.bind(this), 100); + if (this.toListen.has('keyUp')) core.registerAction('keyUp', this.name, this.keyUpEvent.bind(this), 100); + if (this.toListen.has('onmove')) core.registerAction('onmove', this.name, this.onmove.bind(this), 100) + if (this.toListen.has('onup')) core.registerAction('onup', this.name, this.onup.bind(this), 100); + if (this.toListen.has('onmousewheel')) core.registerAction('onmousewheel', this.name, this.onmousewheelEvent.bind(this), 100); + } + + endListen() { + core.unregisterAction('ondown', this.name); + core.unregisterAction('keyDown', this.name); + core.unregisterAction('keyUp', this.name); + core.unregisterAction('onmove', this.name); + core.unregisterAction('onup', this.name); + core.unregisterAction('onmousewheel', this.name); + } + + remove() { + core.ui.deleteCanvas(this.name); + this.onDraw = false; + } + + clear() { + this.endListen(); + this.remove(); + } + + init() { + this.beginListen(); + this.drawContent(); + } + } + + class Pagination extends MenuBase { + constructor(pageList, currPage, name, toListen, x, y, w, h, zIndex) { + super(name, toListen, x, y, w, h, zIndex); + /** + * 当前页面列表 + * @type {Array} + */ + this.pageList = pageList; + /** + * 当前页的序号 + * @type {number} + */ + this.currPage = currPage || 0; + } + + initOnePage(index) { + this.currPage = index; + this.pageList[index].init(); + } + + changePage(num) { + if (num !== this.currPage) { + const beforeMenu = this.pageList[this.currPage]; + beforeMenu.clear(); + } + this.initOnePage(num); + } + + pageDown() { + if (this.currPage > 0) this.changePage(this.currPage - 1); + } + + pageUp() { + if (this.currPage < this.pageList.length - 1) this.changePage(this.currPage + 1); + } + + clear() { + this.pageList.forEach((page) => page.clear()); + super.clear(); + } + } + + // 圆角带文字的按钮 + class RoundBtn extends ButtonBase { + constructor(x, y, w, h, text, config) { + super(x, y, w, h); + this.text = text; + this.config = config || {}; + } + + draw() { + const ctx = this.ctx; + const { x, y, w, h } = this; + const { + fillStyle = 'rgb(204, 204, 204)', strokeStyle = 'black', fontStyle = 'black', + selectedFillStyle = 'rgb(255, 51, 153)', selectedstrokeStyle = 'black', selectedFontStyle = 'white', + radius = 3, lineWidth = 1, angle = null, font = '16px Verdana' + } = this.config || {}; + core.setTextAlign(ctx, 'center'); + core.setTextBaseline(ctx, 'alphabetic'); + if (this.status === 'selected') { + core.fillRoundRect(ctx, x, y, w, h, radius, selectedFillStyle, angle); + core.strokeRoundRect(ctx, x, y, w, h, radius, selectedstrokeStyle, lineWidth, angle); + core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, selectedFontStyle, font); + } else { + core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle); + core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle); + core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, fontStyle, font); + } + } + } + + class IconBtn extends ButtonBase { + constructor(x, y, w, h, icon, config) { + super(x, y, w, h); + this.icon = icon; + this.config = config || {}; + } + + draw() { + const ctx = this.ctx; + const { x, y, w, h } = this; + const { + strokeStyle = 'black', fillStyle = 'white', + radius = 3, lineWidth = 1, angle = null, frame = 0, + iconX = x, iconY = y, iconW = w, iconH = h, + crossline1 = false, crossline2 = false, crossLineOffset = 2, + crossLineStyle = 'red', crossLineWidth = 2, + } = this.config || {}; + if (fillStyle !== 'none') core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle); + if (strokeStyle !== 'none') core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle); + core.drawIcon(ctx, this.icon, iconX, iconY, iconW, iconH, frame); + if (crossline1) { + core.drawLine(ctx, x + crossLineOffset, y + crossLineOffset, + x + w - crossLineOffset, y + h - crossLineOffset, + crossLineStyle, crossLineWidth); + } + if (crossline2) { + core.drawLine(ctx, x + crossLineOffset, y + h - crossLineOffset, + x + w - crossLineOffset, y + crossLineOffset, + crossLineStyle, crossLineWidth); + } + } + } + + class ExitBtn extends ButtonBase { + constructor(x, y, w, h, config) { + super(x, y, w, h); + this.config = config || {}; + } + + draw() { + const ctx = this.ctx; + const { + strokeStyle = ' #D32F2F', fillStyle = ' #EF5350', lineStyle = 'white', + radius = 3, lineOffsetX = 5, lineWidthX = 3, + } = this.config || {}; + const [x, y, w, h] = [this.x, this.y, this.w, this.h]; + core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle); + core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle); + core.drawLine(ctx, x + lineOffsetX, y + lineOffsetX, x + w - lineOffsetX, y + h - lineOffsetX, lineStyle, lineWidthX); + core.drawLine(ctx, x + lineOffsetX, y + h - lineOffsetX, x + w - lineOffsetX, y + lineOffsetX, lineStyle, lineWidthX); + } + } + + class ArrowBtn extends ButtonBase { + constructor(x, y, w, h, dir, config) { + super(x, y, w, h); + this.config = config || {}; + /** @type {'left'|'right'|'up'|'down'} */ + this.dir = dir; + } + + draw() { + const isVertical = (this.dir === "up") || (this.dir === "down"); + const { + /** 箭头尖端到所指的边的距离 **/ + marginTip = isVertical ? 5 : 4, + /** 箭头两侧到各自最接近的边的距离 **/ + marginSide = isVertical ? 5 : 5, + /** 箭头尾部到最接近的边的距离 **/ + marginTail = isVertical ? 5 : 6, + backStyle = 'gray', + arrowStyle = 'black' + } = this.config || {}; + + const { x, y, w, h, ctx } = this; + + core.fillRoundRect(ctx, x, y, w, h, 3, backStyle); + + let points = []; + + if (this.dir === 'left') { + points = [ + [x + marginTip, y + h / 2], + [x + w - marginTail, y + marginSide], + [x + w - marginTail, y + h - marginSide] + ]; + } + else if (this.dir === 'right') { + points = [ + [x + w - marginTip, y + h / 2], + [x + marginTail, y + marginSide], + [x + marginTail, y + h - marginSide] + ]; + } + else if (this.dir === 'up') { + points = [ + [x + w / 2, y + marginTip], + [x + marginSide, y + h - marginTail], + [x + w - marginSide, y + h - marginTail] + ]; + } + else if (this.dir === 'down') { + points = [ + [x + w / 2, y + h - marginTip], + [x + marginSide, y + marginTail], + [x + w - marginSide, y + marginTail] + ]; + } + + if (points.length > 0) { + core.fillPolygon(ctx, points, arrowStyle); + } + } + } + core.plugin.uiBase = { + ButtonBase, RoundBtn, IconBtn, ExitBtn, + ArrowBtn, MenuBase, Pagination, KeyCodeEnum + }; + }, + "newBackpackLook": function () { + // 本插件定义了一些用于绘制的基类 + + let __enable = true; + if (!__enable) return; + + /** @todo 尝试干掉redraw */ + // #region 复写 + + core.ui._drawToolbox = function () { drawItemBox('all'); }.bind(core.ui); + core.ui._drawEquipbox = function () { drawItemBox('equips'); }.bind(core.ui); + core.actions._keyDownToolbox = core.actions._keyDownEquipbox = function (keyCode) { return true; }.bind(core.actions); + core.actions._clickToolbox = core.actions._clickEquipbox = function (x, y, px, py) { return true; }.bind(core.actions); + core.actions._keyUpToolbox = core.actions._keyUpEquipbox = function (keyCode) { return true; }.bind(core.actions); + // 暂不考虑修改core.status.event.id,该变量牵涉太多,作用不完全清楚 + // 录像模式下下列函数会进行检测,不处于特定模式时,阻止自定义监听事件 + core.actions._checkReplaying = function () { + if (core.isReplaying() && !UI._back?.onDraw && + ['save', 'book', 'book-detail', 'viewMaps', 'toolbox', 'equipbox', 'text'].indexOf(core.status.event.id) < 0) { + return true; + } + return false; + }.bind(core.actions); + + const oriClosePanel = core.ui.closePanel; + core.ui.closePanel = function () { + oriClosePanel.apply(core.ui, [arguments]); + UI.clearAll(); + } + + core.control._replayAction_item = function (action) { + if (action.indexOf("item:") != 0) return false; + const itemId = action.substring(5); + if (!core.canUseItem(itemId)) return false; + if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) { + core.useItem(itemId, false, core.replay); + return true; + } + core.ui._drawToolbox(0); + const itemInv = UI.itemInv; + const totalIndex = itemInv.allItemList.indexOf(itemId); + const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0); + const currIndex = totalIndex - page * itemInv.pageCap; + itemInv.page = page; + itemInv.setIndex(currIndex); + itemInv.drawContent(); + setTimeout(function () { + core.ui.closePanel(); + core.useItem(itemId, false, core.replay); + }, core.control.__replay_getTimeout()); + return true; + } + + core.control._replayAction_equip = function (action) { + if (action.indexOf("equip:") != 0) return false; + const equipId = action.substring(6); + if (!core.hasItem(equipId)) { + core.removeFlag('__doNotCheckAutoEvents__'); + return false; + } + + const callbackFunc = function () { + const next = core.status.replay.toReplay[0] || ""; + if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { + core.removeFlag('__doNotCheckAutoEvents__'); + core.checkAutoEvents(); + } + core.replay(); + } + core.setFlag('__doNotCheckAutoEvents__', true); + + core.status.route.push(action); + if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) { + core.loadEquip(equipId, callbackFunc); + return true; + } + core.ui._drawEquipbox(0); + const { itemId, itemInv, equipSlots } = UI; + const totalIndex = itemInv.allItemList.indexOf(itemId); + const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0); + const currIndex = totalIndex - page * itemInv.pageCap; + itemInv.page = page; + itemInv.setIndex(currIndex); + itemInv.drawContent(); + equipSlots.drawContent(); + setTimeout(function () { + core.ui.closePanel(); + core.loadEquip(equipId, callbackFunc); + }, core.control.__replay_getTimeout()); + return true; + } + + core.control._replayAction_unEquip = function (action) { + if (action.indexOf("unEquip:") != 0) return false; + const equipType = parseInt(action.substring(8)); + if (!core.isset(equipType)) { + core.removeFlag('__doNotCheckAutoEvents__'); + return false; + } + + const callback = function () { + [UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => { + if (menu && menu.onDraw) menu.drawContent(); + }); + core.ui.closePanel(); + const next = core.status.replay.toReplay[0] || ""; + if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) { + core.removeFlag('__doNotCheckAutoEvents__'); + core.checkAutoEvents(); + } + core.replay(); + } + core.setFlag('__doNotCheckAutoEvents__', true); + + core.status.route.push(action); + if (core.status.replay.speed == 24) { + core.unloadEquip(equipType, callback); + return true; + } + const { itemInv, equipSlots } = UI; + const page = Math.max(Math.ceil(equipType / itemInv.pageCap) - 1, 0); + const currIndex = equipType - page * itemInv.pageCap; + itemInv.page = page; + itemInv.setIndex(currIndex); + itemInv.drawContent(); + equipSlots.drawContent(); + core.ui._drawEquipbox(0); + setTimeout(function () { + core.unloadEquip(equipType, callback); + }, core.control.__replay_getTimeout()); + return true; + } + core.registerReplayAction("item", core.control._replayAction_item); + core.registerReplayAction("equip", core.control._replayAction_equip); + core.registerReplayAction("unEquip", core.control._replayAction_unEquip); + + // 复写control.startReplay + const oriStartReplay = core.control.startReplay; // 进入播放录像模式时清空道具栏选中目标的缓存 + core.control.startReplay = function (list) { + clearItemBoxCache(); + oriStartReplay.apply(core.control, [list, ...arguments]); + } + + // 复写items._afterUseItem + // flag:itemsUsedCount {[itemId:string]:boolean} 成功使用了的道具的计数 + const origin__afterUseItem = items.prototype._afterUseItem; + items.prototype._afterUseItem = function (itemId) { + const itemsUsedCount = core.getFlag('itemsUsedCount', {}); + if (!itemsUsedCount.hasOwnProperty(itemId)) itemsUsedCount[itemId] = 0; + itemsUsedCount[itemId]++; + core.setFlag('itemsUsedCount', itemsUsedCount); + origin__afterUseItem(itemId); + } + + // 复写ui.getToolboxItems + // flag:markedItems string[] 在道具栏中置顶的道具的列表 + // flag:hideInfo {[itemId:string]:boolean} 手动选择了显示/隐藏的道具的列表 + core.ui.getToolboxItems = function (cls, showHide, sortFunc) { + const markedItems = core.getFlag('markedItems', []); + + // 暂时不采用按使用次数排序这个函数 因为导致物品排序频繁变动,反而体验不好 + // const itemsUsedCount = core.getFlag('itemsUsedCount', {}); + // if (!sortFunc) sortFunc = (itemId1, itemId2) => { + // const item1Count = itemsUsedCount[itemId1] || 0, + // item2Count = itemsUsedCount[itemId2] || 0; + // return item2Count - item1Count; + // } + + let list = []; + if (cls === 'all') { + for (let name in core.status.hero.items) { + if (name === "equips") continue; + list = list.concat(Object.keys(core.status.hero.items[name])); // 获取'constants'和'tools'整体的列表 + } + } else if (core.status.hero.items[cls]) { + list = Object.keys(core.status.hero.items[cls] || {}); + } + const markedList = list.filter((itemId) => markedItems.includes(itemId)).sort(sortFunc), + unmarkedList = list.filter((itemId) => !markedItems.includes(itemId)).sort(sortFunc); + list = [...markedList, ...unmarkedList]; + const hideInfo = core.getFlag('hideInfo', {}); + if (!showHide) list = list.filter(function (id) { + if (hideInfo[id]) return false; + return !core.material.items[id].hideInToolbox; + }) + return list; + } + + // 复写resize,保证屏幕变化时此画布表现正常 + const originResize = core.control.resize; + core.control.resize = function () { + originResize.apply(core.control, arguments); + const { _back, itemInv, _equipSlots: _equipChangeBoard, _itemInfo: _itemInfoBoard } = UI; + [_back, itemInv, _equipChangeBoard, _itemInfoBoard].forEach((menu) => { if (menu && menu.onDraw) menu.drawContent(); }); + } + + // #endregion + const { ButtonBase, RoundBtn, IconBtn, ExitBtn, MenuBase, ArrowBtn, KeyCodeEnum } = core.plugin.uiBase; + // #region 绘制用到的按钮类 + + /** 隐藏物品的按钮 */ + class HideBtn extends RoundBtn { + constructor(x, y, w, h, config) { + super(x, y, w, h, '隐藏', config); + } + + draw() { + const itemId = UI.itemId; + if (core.material.items[itemId]) { + const hideInfo = core.getFlag('hideInfo', {}); + if (hideInfo.hasOwnProperty(itemId)) this.text = hideInfo[itemId] ? "显示" : "隐藏"; + else this.text = core.material.items[itemId].hideInToolbox ? "显示" : "隐藏"; + } + super.draw(); + } + } + + /** 置顶物品的按钮 */ + class MarkBtn extends RoundBtn { + constructor(x, y, w, h, config) { + super(x, y, w, h, '置顶', config); + } + + draw() { + const itemId = UI.itemId; + const markedItems = core.getFlag('markedItems', []); + this.text = markedItems.includes(itemId) ? "取消" : "置顶"; + super.draw(); + } + } + + /** 切换到显示指定分类的物品(如:永久,消耗)的模式的按钮 */ + class ClassifyBtn extends RoundBtn { + constructor(x, y, w, h, text, subType, config) { + super(x, y, w, h, text, config); + this.subType = subType; + } + + draw() { + const { type, toolInv } = UI; + if (toolInv.subType === this.subType) this.status = 'selected'; + else this.status = "none"; + super.draw(); + } + } + + /** 控制是否显示隐藏物品的按钮 */ + class ShowHideBtn extends ButtonBase { + draw() { + const ctx = this.ctx; + const squareSize = this.h; + core.strokeRect(ctx, this.x, this.y, squareSize, squareSize, 'black'); + core.fillRect(ctx, this.x + 1, this.y + 1, squareSize - 2, squareSize - 2, 'white'); + const font = core.ui._buildFont(this.h - 4); + core.setTextAlign(ctx, 'left'); + core.setTextBaseline(ctx, 'middle'); + if (core.hasFlag('showHideItem')) core.fillText(ctx, '√', this.x + 3, this.y + 10, 'red', font); + core.fillText(ctx, '查看隐藏', this.x + squareSize + 6, this.y + 10, 'white', font); + } + } + + /** 切换道具栏和装备栏的按钮 */ + class SwitchBtn extends IconBtn { + constructor(x, y, w, h, config) { + super(x, y, w, h, 'toolbox', config); + } + + draw() { + this.icon = (UI.type === 'all') ? 'toolbox' : 'equipbox'; + super.draw(); + } + } + + class NoHoverBtn extends IconBtn { + draw() { + super.draw(); + const ctx = this.ctx; + const { x, y, w, h } = this; + core.drawImage(ctx, "mousewheel.png", x, y, w, h); + } + } + + /** 切换装备面板的单个装备选框 */ + class EquipBox extends ButtonBase { + constructor(x, y, w, h) { + super(x, y, w, h); + /** @type {EquipSlots} */ // @ts-ignore + this.menu; + this.key = -1; + } + + draw() { + const ctx = this.ctx; + const [x, y, w, h] = [this.x, this.y, this.w, this.h]; + const space = 2, + lineWidth = 2, + squareSize = w; + const equipId = core.getEquip(this.menu.getTotalIndex(this.key)); + if (equipId) core.drawIcon(ctx, equipId, x + 4, y + 4, squareSize - 8, squareSize - 8); + const color = (UI.selectType === 'equipBox' && this.menu.index === this.key) ? 'gold' : 'white'; + core.strokeRect(ctx, x, y, squareSize, squareSize, color, lineWidth); + core.setTextAlign(ctx, "center"); + core.setTextBaseline(ctx, "top"); + const tx = x + w / 2, + ty = y + squareSize + space; + core.fillText(ctx, this.menu.currItemList[this.key], tx, ty, color, '14px Verdana'); + } + } + + // #endregion + // #region 绘制用到的菜单类 + + // 道具栏/装备栏的背景 + class ItemBoxBack extends MenuBase { + constructor() { + // 装备栏和道具栏共用同一个光标,故所有按键事件全部写在这里处理 + super('itemBoxBack', ['ondown', 'keyDown', 'keyUp'], null, null, null, null, 136); + } + + keyDownEvent(keyCode) { + const { type, selectType, itemInv, equipSlots } = UI; + if (keyCode === KeyCodeEnum.Left) { // left + if (selectType === 'toolBox') itemInv.pageDown(); + else if (selectType === 'equipBox') { + if (equipSlots.index === 0) { + equipSlots.pageDown(); + } else { + equipSlots.setIndex(equipSlots.index - 1); + } + } + } else if (keyCode === KeyCodeEnum.Right) { // right + if (selectType === 'toolBox') itemInv.pageUp(); + else if (selectType === 'equipBox') { + if (equipSlots.index === equipSlots.currItemList.length - 1) { + equipSlots.pageUp(); + } else { + equipSlots.setIndex(equipSlots.index + 1); + } + } + } else if (keyCode === KeyCodeEnum.Up) { // up + if (selectType === 'toolBox') { + if (itemInv.index === 0) { + if (type === 'equips') { // 在仅物品栏模式下点上键到顶,切换到上一页,否则会切换到装备栏 + equipSlots.setIndex(equipSlots.currItemList.length - 1); + } else { + itemInv.pageDown(); // 向上到顶将翻到上一页 + } + } else { + itemInv.setIndex(itemInv.index - 1); + } + } else if (selectType === 'equipBox') { + if (equipSlots.index >= equipSlots.rowMax) { + equipSlots.index -= equipSlots.rowMax; + } + } + } else if (keyCode === KeyCodeEnum.Down) { // down + if (selectType === 'toolBox') { + if (itemInv.index < itemInv.currItemList.length - 1) { + itemInv.setIndex(itemInv.index + 1); + } else { + itemInv.pageUp(); // 向下到底将翻到下一页 + } + } else if (selectType === 'equipBox') { + let newIndex = equipSlots.index + equipSlots.rowMax; + if (newIndex < equipSlots.currItemList.length - 1) { + equipSlots.setIndex(newIndex); + } else { + equipSlots.setIndex(0); + } + } + } + } + + keyUpEvent(keyCode, altKey) { + const { itemId, selectType, itemInv, equipSlots: equipChangeBoard } = UI; + if (keyCode === KeyCodeEnum.Q) { // Q + if (UI.type === "equips") UI.exit(); + else UI.switchType(); + } else if (keyCode === KeyCodeEnum.T) { // T + if (UI.type === "all") UI.exit(); + else UI.switchType(); + } else if (keyCode === KeyCodeEnum.BackSpace || keyCode === KeyCodeEnum.Esc) { // BackSpace/Esc + UI.exit(); + } else if (keyCode === KeyCodeEnum.Enter || keyCode === KeyCodeEnum.SpaceBar || keyCode === KeyCodeEnum.C) { // Enter/SpaceBar/C + if (selectType === "toolBox") { + if (core.material.items[itemId]) itemInv.triggerItem(); + } else if (selectType === "equipBox") { + equipChangeBoard.triggerItem(); + [UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => { + if (menu && menu.onDraw) menu.drawContent(); + }); + } else { + itemInv.setIndex(0); + } + } else if (altKey && keyCode >= 48 && keyCode <= 57) { // 都有自动切装了还有神人想要这个Alt换装 服了 + core.items.quickSaveEquip(keyCode - 48); + return; + } + } + + drawContent() { + const ctx = this.createCanvas(); + this.drawBackGround(ctx); + super.drawContent(); + } + + drawBackGround(ctx) { + core.strokeRoundRect(ctx, 2, 2, 412, 412, 5, 'white', 2); + core.fillRoundRect(ctx, 3, 3, 410, 410, 5, 'rgb(108, 187, 219)'); + core.drawLine(ctx, 248, 3, 248, 413, 'white', 2); // 左栏和右栏的分界线 + if (UI.type === 'equips') core.drawLine(ctx, 3, 140, 248, 140, 'white', 2); // 装备栏和道具栏的分界线 + } + } + + // 物品列表和换装界面的共用基类 + class ItemListBase extends MenuBase { + constructor(name, x, y, w, h, zIndex) { + super(name, ['ondown', 'onmove'], x, y, w, h, zIndex); + /** 当前页 */ + this.page = 0; + /** 一页最多可容纳的道具数量 */ + this.pageCap = 0; + /** 当前最大页数,page应小于此值*/ + this.pageMax = 1; + /** @type {string[]} 此界面总的物品列表 */ + this.allItemList = []; + /** @type {string[]} 此界面当前页展示的物品列表 */ + this.currItemList = []; + /** 当前选中了第几个道具 */ + this.index = 0; + } + + /** + * @abstract 获取最新的物品列表 + * @returns {string[]} + */ + getItemList() { return []; } + + /** + * @virtual + * @description 选中指定位置 + * @param {number} index + **/ + setIndex(index) { } + + /** + * @abstract 尝试使用当前选中的物品 + */ + triggerItem() { } + + /** 更新物品列表 */ + updateItemList() { + this.allItemList = this.getItemList(); + this.pageMax = Math.ceil(this.allItemList.length / this.pageCap); + if (this.pageMax < 1) this.pageMax = 1; + if (this.page >= this.pageMax) this.page = this.pageMax - 1; + this.currItemList = this.allItemList.slice(this.page * this.pageCap, (this.page + 1) * this.pageCap); + if (this.index >= this.currItemList.length && this.currItemList.length > 0) this.setIndex(this.currItemList.length - 1); + } + + canPageUp() { + return this.page < this.pageMax - 1; + } + + canPageDown() { + return this.page > 0; + } + + /** 翻页,更新物品列表,并重绘该菜单自身 */ + pageUp() { + if (!this.canPageUp()) return; + this.page++; + this.updateItemList(); + this.setIndex(Math.min(this.index, this.currItemList.length - 1)); + this.drawContent(); + } + + /** 翻页,更新物品列表,并重绘该菜单自身 */ + pageDown() { + if (!this.canPageDown()) return; + this.page--; + this.updateItemList(); + this.setIndex(this.index); + this.drawContent(); + } + } + + /** 展示角色当前已穿戴的装备的面板 */ + class EquipSlots extends ItemListBase { + constructor(x, y, w, h, zIndex) { + super('equipChangeBoard', x, y, w, h, zIndex); + this.columnMax = 4; + this.rowMax = 2; + this.pageCap = this.columnMax * this.rowMax; + this.updateItemList(); + const currNameList = this.currItemList; + + const columnCount = Math.min(currNameList.length, this.columnMax), // 判断装备孔数量是否小于最大列数 + rowCount = Math.min(Math.ceil(currNameList.length / this.columnMax), this.rowMax); + const [boxWidth, boxHeight] = [36, 52]; + const spaceX = (this.w - columnCount * boxWidth) / (1 + columnCount), + spaceY = (this.h - rowCount * boxHeight) / (1 + rowCount); + let [xi, yi] = [spaceX, spaceY]; + + // 装备孔的按钮在这里注册 + for (let i = 0; i < this.pageCap; i++) { + if (!this.btnMap.has(i)) { + const btn = new EquipBox(xi, yi, boxWidth, boxHeight); + this.registerBtn(i, btn, { + ondown: function () { + if (this.index !== i) { + this.setIndex(i); + } else this.triggerItem(i); + }.bind(this), + onmove: function () { + if (UI.isNoHover) return; + if (this.index !== i) { + this.setIndex(i); + } + }.bind(this), + }); + if ((i >= this.currItemList.length)) btn.disable = true; + } else { + const btn = this.btnMap.get(i); + if (btn) btn.disable = (i >= this.currItemList.length); + } + if ((i + 1) % this.columnMax === 0) { + xi = spaceX; + yi += spaceY + boxHeight; + } else { xi += spaceX + boxWidth; } + } + } + + drawContent() { + const ctx = this.createCanvas(); + if (this.pageMax > 1) { + core.setTextAlign(ctx, "center"); + core.setTextBaseline(ctx, "alphabetic"); + core.fillText(ctx, this.page + 1 + '/' + this.pageMax, this.w / 2, this.h - 2, 'white', '12px Verdana'); + } + // 切装面板只有1页时不激活翻页按钮 + if (this.allItemList.length < this.pageCap) { + const pgDown = this.btnMap.get('pgDownBtn'); + const pgUp = this.btnMap.get('pgUpBtn'); + if (pgDown) pgDown.disable = true; + if (pgUp) pgUp.disable = true; + } + + super.drawContent(); + } + + /** 注意,对于装备切换面板来说,它的装备列表不是装备本身,而是角色的装备孔 */ + getItemList() { + return core.status.globalAttribute.equipName; + } + + /** @param {number} index 根据当前页选中的序号换算对应装备在角色装备中的总序号 */ + getTotalIndex(index) { + if (index == null) index = this.index; + return this.page * this.pageCap + index; + } + + /** @param {number} index */ + changePageByTotalIndex(index) { + const newPage = Math.floor(index / this.pageCap); + this.page = newPage; + this.updateItemList(); + } + + /** 脱下指定位置的装备 */ + triggerItem(index) { + const totalIndex = this.getTotalIndex(index); + if (core.status.hero.equipment[totalIndex]) { + core.unloadEquip(totalIndex); + core.status.route.push("unEquip:" + totalIndex); + this.updateItemList(); + this.drawContent(); + UI.itemInv.updateItemList(); //穿脱装备是双向的过程,装备栏和道具栏的物品列表组成都会变 + UI.itemInv.drawContent(); + UI.itemId = ''; + } + } + + setIndex(index) { + this.index = index; + UI.equipInv.index = -1; + core.ui.clearUIEventSelector(1); + // 被选中的装备框变色 + this.btnMap.forEach((ele, key) => { + if (ele instanceof EquipBox) { + if (key === index) ele.status = 'selected'; + else ele.status = 'none'; + } + }) + UI.selectType = 'equipBox'; + const totalIndex = this.getTotalIndex(index); + UI.itemId = core.status.hero.equipment[totalIndex]; + this.drawButtonContent(); + } + + pageUp() { + if (!this.canPageUp()) return; + this.page++; + this.updateItemList(); + this.btnMap.forEach((btn, key) => { + if (btn instanceof EquipBox) { + if (Number(key) >= this.currItemList.length) btn.disable = true; + } + }); + this.setIndex(Math.min(this.index, this.currItemList.length - 1)); + this.drawContent(); + } + + pageDown() { + if (!this.canPageDown()) return; + this.page--; + this.updateItemList(); + this.btnMap.forEach((btn, key) => { + if (btn instanceof EquipBox) { + if (Number(key) <= this.currItemList.length - 1) btn.disable = false; + } + }); + this.setIndex(this.index); + this.drawContent(); + } + } + + /** 展示角色当前背包物品的面板,有道具/装备两种模式 */ + class InventoryBase extends ItemListBase { + /** + * @param {*} name + * @param {string} x + * @param {number} y + * @param {number} w + * @param {number} h + * @param {number} zIndex + * @param {number} pageCap 单个页面显示的物品数 + */ + constructor(name, x, y, w, h, zIndex, pageCap) { + super(name, x, y, w, h, zIndex); + + /** @type {number} 单个物品占据的列宽 */ + this.oneItemHeight = 30; + /** @type {number} 单个页面显示的物品数, -1是因为最后一行要留给换行按钮*/ + this.pageCap = pageCap; + this.marginLeft = 15; // 物品栏左边距 + this.marginTop = 40; // 物品栏上边距 + this.x0 = this.x + this.marginLeft; // 物品栏左边距 + this.y0 = this.y + this.marginTop; + this.w0 = this.w - this.marginLeft - 5; // 物品栏真实宽度 + this.h0 = this.oneItemHeight * (this.pageCap + 1); // +1是因为最后一行要留给翻页按钮 + } + + drawContent() { + const ctx = this.createCanvas(); + const { x0, y0, w0, h0, marginTop, marginLeft } = this; + const { w, h } = this; + core.fillRect(ctx, marginLeft, marginTop, w0, h0, 'rgb(0, 105, 148)'); + + core.setTextBaseline(ctx, "middle"); + for (let i = 0; i < this.currItemList.length; i++) { + this.drawOneItem(i); + } + core.setTextAlign(ctx, "center"); + core.setTextBaseline(ctx, "alphabetic"); + core.fillText(ctx, (this.page + 1) + '/' + this.pageMax, w / 2, y0 + h0 - 4, 'white', '12px Verdana'); + super.drawContent(); + } + + /** @param {number} currIndex */ + drawOneItem(currIndex) { + const itemId = this.currItemList[currIndex]; + const ctx = core.dymCanvas[this.name]; + const { x0, y0, marginTop } = this; + const dy = this.oneItemHeight * currIndex + marginTop; + /** @type {Item|{}} */ + const item = core.material.items[itemId] || {}; + const num = core.formatBigNumber(core.itemCount(itemId), 5) || 0; // 道具数量过大时需要format + + // 被隐藏的道具在显示时需要半透明 + const hideInfo = core.getFlag('hideInfo', {}); + if (item && (hideInfo.hasOwnProperty(itemId) ? hideInfo[itemId] : item.hideInToolbox)) core.setAlpha(ctx, 0.5); + + // 绘制物品图标 + if (core.material.items[itemId]) core.drawIcon(ctx, itemId, x0 + 4, dy + 6, 18, 18); + + core.setTextAlign(ctx, "right"); + core.setTextBaseline(ctx, "middle"); + // 绘制物品数量 ×几 + const numText = "×" + num; + core.fillText(ctx, numText, x0 + 218, dy + this.oneItemHeight / 2, 'white', '18px Verdana'); + + // 绘制物品名称 + const markedItems = core.getFlag('markedItems', []); + const name = item.name || "???"; + core.setTextAlign(ctx, "left"); + core.fillText(ctx, name, x0 + 24, dy + this.oneItemHeight / 2, markedItems.includes(itemId) ? 'gold' : 'white', '18px Verdana', 180); + core.setAlpha(ctx, 1); + } + + clear() { + core.clearUIEventSelector(1); + super.clear(); + } + + /** 绘制选中物品的光标,在selectType或index改变时自动执行绘制/擦除 + * @param {number} index 选中物品在当前页的序号 + */ + drawSelector(index) { + const [x0, y0, w0] = [this.x0, this.y0, this.w0]; // 光标绘制是绝对坐标 + core.drawUIEventSelector(1, 'winskin.png', x0, y0 + index * this.oneItemHeight, w0, this.oneItemHeight, 140); + } + + /** 选中指定序号的位置,改变选中道具的ID,重绘光标 + * @param {number} index 选中物品在当前页的序号 + */ + setIndex(index) { + this.index = index; + if (UI.type === 'equips') { + UI.equipSlots.index = -1; + UI.equipSlots.drawButtonContent(); // 清除装备栏的选中状态 + UI.selectType = 'toolBox'; + } + this.drawSelector(index); + UI.itemId = this.currItemList[index]; + } + + ondownEvent(_x, _y, px, py) { + py -= this.marginTop; // 转换为相对于物品栏内部的坐标 + const index = Math.floor(py / this.oneItemHeight); + if (index < 0 || index >= this.currItemList.length) return; + if (UI.selectType !== 'toolBox' || this.index !== index) { + this.setIndex(index); + } else { + this.triggerItem(); + } + } + + onmoveEvent(_x, _y, px, py) { + if (UI.isNoHover) return; + py -= this.marginTop; // 转换为相对于物品栏内部的坐标 + const index = Math.floor(py / this.oneItemHeight); + if (index < 0 || index >= this.currItemList.length) return; + if (UI.selectType !== 'toolBox' || this.index !== index) { + this.setIndex(index); + } + } + } + + class ToolInventory extends InventoryBase { + constructor(x, y, w, h, zIndex, pageCap) { + super('toolInventory', x, y, w, h, zIndex, pageCap); + /** @type {'all'|'tools'|'constants'} 当前显示哪个子菜单 */ + this.subType = 'all'; + + /** 各个子页面当前选中的位置,用于在切换后显示原位置 */ + this.cache = { + all: { page: 0, index: 0 }, + tools: { page: 0, index: 0 }, + constants: { page: 0, index: 0 } + } + } + + getItemList() { + return core.getToolboxItems(this.subType, core.hasFlag('showHideItem')); + } + + triggerItem() { + const itemId = UI.itemId; + if (!core.canUseItem(itemId) && itemId !== 'centerFly') { + core.drawFailTip("当前无法使用" + core.material.items[itemId].name, itemId); + return; + } + UI.clearAll(); + setTimeout(() => { + core.unlockControl(); + core.tryUseItem(itemId); + }, 0); + } + + /** 物品栏仅显示指定类型物品 */ + classify(subType) { + if (UI.type !== 'all') { + // 非物品栏类型的菜单不支持分类 + return; + } + if (this.subType !== subType) { + const oldConfig = this.cache[this.subType], + newConfig = this.cache[subType]; + oldConfig.page = this.page; + oldConfig.index = this.index; + this.page = newConfig.page; + this.index = newConfig.index; + this.subType = subType; + this.updateItemList(); + this.setIndex(this.index); + this.drawContent(); + UI.back.drawContent(); // 切换物品栏类型时需要重绘背景 + } + } + } + + class EquipInventory extends InventoryBase { + getItemList() { + return core.getToolboxItems('equips', core.hasFlag('showHideItem')); + } + + triggerItem() { + const equip = UI.itemId; + if (!core.canEquip(equip, true)) return; + const equipPos = core.getEquipTypeById(equip); + UI.equipSlots.changePageByTotalIndex(equipPos); + core.loadEquip(equip); + core.status.route.push("equip:" + equip); + this.updateItemList(); // 穿上装备会导致道具数量变化,需要重新生成装备列表 + this.setIndex(this.index); + this.drawContent(); + UI.equipSlots.drawContent(); + } + } + + class ItemInfoBoard extends MenuBase { + constructor(x, y, w, h) { + super('itemInfoBox', ['ondown'], x, y, w, h, 137); + } + + drawContent() { + const ctx = this.createCanvas(); + core.strokeRoundRect(ctx, 23, 27, 32, 32, 2, 'white', 2); + const itemId = UI.itemId; + if (itemId) core.drawIcon(ctx, itemId, 24, 28, 30, 30); + + // 修改这里可以编辑未选中道具时的默认值 + const defaultItem = { cls: "constants", name: "无道具", id: "-", text: "没有道具最永久" }; + const defaultEquip = { cls: "equips", name: "无装备", id: "-", text: "一无所有,又何尝不是一种装备", equip: { type: "装备" } }; + let item = core.material.items[itemId]; + if (!item) item = (UI.type === 'all' ? defaultItem : defaultEquip); + core.setTextAlign(ctx, "left"); + core.setTextBaseline(ctx, "middle"); + core.fillText(ctx, item.name, 66, 46, 'black', 'bold 18px Verdana', 98); // 物品名字 e.g.护符 + core.fillText(ctx, "类型", 20, 75, 'crimson', '14px Verdana'); + core.fillText(ctx, "【" + getItemClsName(item) + "】", 50, 75, 'rgb(47, 49, 54)', '14px Verdana'); // 物品类型 e.g.【永久道具】 + + if (UI.type === 'all') { // 显示物品累计使用的次数 + core.fillText(ctx, "累计使用", 20, 95, 'crimson', '14px Verdana'); + const itemsUsedCount = core.getFlag('itemsUsedCount', {}); + core.fillText(ctx, itemsUsedCount[itemId] || 0, 80, 95, 'rgb(47, 49, 54)', '14px Verdana'); + } + const rawItemText = core.replaceText(item.text) ?? ""; + const itemText = rawItemText + ((UI.type === "equips") ? this.getEquipCompareInfo(item) : ""); // 物品描述信息 + core.drawTextContent(ctx, itemText, { + left: 20, + top: 110, + bold: false, + color: "black", + align: "left", + fontSize: 15, + maxWidth: 150 + }); + const currItemHotKey = HotkeySelect.getHotkeyNum(itemId); + // 获取快捷键设置按钮当前的图标 + + const setHotkeyBtn = /** @type {IconBtnClass} */ (this.btnMap.get('setHotkeyBtn')); + if (setHotkeyBtn) { + setHotkeyBtn.disable = (UI.type === 'equips'); + setHotkeyBtn.icon = (currItemHotKey == null) ? 'keyboard' : ('btn' + currItemHotKey); + } + super.drawContent(); + } + + /*** @param {Item} item */ + getEquipCompareInfo(item) { + let str = ''; + if (UI.type !== "equips") return str; + + let equipType = item.equip?.type; + if (!equipType) return str; + if (typeof equipType == "string") equipType = core.getEquipTypeByName(equipType); + let compare; + /** @todo 准备卸下装备时显示卸下的比较信息 */ + if (UI.selectType == "equipBox") compare = core.compareEquipment(null, item.id); + else compare = core.compareEquipment(item.id, core.getEquip(equipType)); + // --- 变化值... + for (const name in core.status.hero) { + if (typeof core.status.hero[name] != 'number') continue; + let nowValue = core.getRealStatus(name); + let newValue = Math.floor((core.getStatus(name) + (compare.value[name] || 0)) * + (core.getBuff(name) * 100 + (compare.percentage[name] || 0)) / 100); + if (nowValue === newValue) continue; + const color = newValue > nowValue ? '#00FF00' : '#FF0000'; + const [nowValueStr, newValueStr] = [nowValue, newValue].map(value => core.formatBigNumber(value)); + str += "\n" + core.getStatusLabel(name) + " " + nowValueStr + "->\r[" + color + "]" + newValueStr + "\r"; + } + return str; + } + } + + /** 取消设置快捷键的按钮 */ + class SetNullBtn extends ButtonBase { + draw() { + const [x, y, w, h] = [this.x, this.y, this.w, this.h]; + core.strokeRect(this.ctx, x, y, w, h, 'red', 2); + core.drawLine(this.ctx, x, y, x + w, y + h, 'red', 2); + } + } + + // 为当前道具设定一个快捷键 + class HotkeySelect extends MenuBase { + constructor(itemId, x, y, w, h, zIndex) { + super('hotkeySelect', ['ondown', 'keyDown'], x, y, w, h, zIndex); + this.itemId = itemId; + /** @type {number | null} null代表当前道具没有快捷键 */ + this.hotkeyNum = HotkeySelect.getHotkeyNum(this.itemId); + } + + drawContent() { + const ctx = this.createCanvas(); + const [x, y, w, h] = [this.x, this.y, this.w, this.h]; + core.fillRect(ctx, 3, 3, w - 6, h - 6, ' #A8CABA'); + core.strokeRect(ctx, 0, 0, w, h, ' #004B23', 3); + core.setTextAlign(ctx, 'center'); + core.setTextBaseline(ctx, 'alphabetic'); + core.fillText(ctx, '为当前道具选择一个快捷键', this.w / 2, 40, 'black', '20px Verdana'); + core.fillText(ctx, '无自定义设置时样板的默认快捷键', this.w / 2, 60, 'gray', '16px Verdana'); + core.fillText(ctx, '(如123分别对应破炸飞)不可在此设置', this.w / 2, 80, 'gray', '16px Verdana'); + // 无自定义设置时样板的默认快捷键(如123分别对应破炸飞),不可在此设置 + // 绘制指向当前快捷键的监听 + if (this.hotkeyNum != null) { + const btn = this.btnMap.get('btn' + this.hotkeyNum); + if (btn) { + core.fillPolygon(ctx, [ + [btn.x + 12, btn.y + btn.h + 10], + [btn.x + btn.w - 12, btn.y + btn.h + 10], + [btn.x + btn.w / 2, btn.y + btn.h + 2] + ], 'black'); + } + } + super.drawContent(); + } + /* @__PURE__ */ + static getHotkeyNum(itemId) { + for (let i = 1; i <= 9; i++) { + const currHotkey = core.getLocalStorage('hotkey' + i, null); + if (currHotkey === itemId) { + return i; + } + } + return null; + } + + deleteHotkey() { + if (this.hotkeyNum != null) core.setLocalStorage('hotkey' + this.hotkeyNum, null); + } + + setHotkey(num) { + this.deleteHotkey(); + core.setLocalStorage('hotkey' + num, this.itemId); + this.hotkeyNum = Number(num); + } + + clear() { + super.clear(); + const { back, itemInv, equipSlots, itemInfo } = UI; + [back, itemInv, equipSlots, itemInfo].forEach(menu => { + if (menu.onDraw) menu.beginListen(); // 注意本来就没在绘制的则不监听 + }); + UI.itemInfo.drawContent(); // 快捷键图标会发生变化 + } + } + + /** @param {string} itemId */ + function hotkeySelectFactory(itemId) { + const hotkeySelect = new HotkeySelect(itemId, 60, 100, 296, 206, 141); // 应当比物品背包的选择光标大,遮盖住前者 + const setHotkeyNum = function (i) { + const num = this.key.replace('btn', ''); + hotkeySelect.setHotkey(num); + hotkeySelect.clear(); + UI.itemInfo.drawContent(); + } + + const [btnSize, btnInterval] = [32, 20]; + const leftMargin = hotkeySelect.w / 2 - 2.5 * btnSize - 2 * btnInterval; + const style = { strokeStyle: 'none', fillStyle: 'none' }; + + for (let i = 0; i < 9; i++) { + const num = i + 1; + const row = (i <= 4) ? 1 : 2; + let btn; + if (row === 1) btn = new IconBtn(leftMargin + i * (btnSize + btnInterval), 100, btnSize, btnSize, 'btn' + num, style); + else btn = new IconBtn(leftMargin + (i - 5) * (btnSize + btnInterval), 150, btnSize, btnSize, 'btn' + num, style); + hotkeySelect.registerBtn('btn' + num, btn, () => setHotkeyNum.call(btn, i)); + } + const setNullBtn = new SetNullBtn(leftMargin + 4 * (btnSize + btnInterval), 150, btnSize, btnSize); + hotkeySelect.registerBtn('setNullBtn', setNullBtn, () => { + hotkeySelect.deleteHotkey(); + hotkeySelect.clear(); + UI.itemInfo.drawContent(); + }); + const exitBtn = new ExitBtn(274, 5, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 }); + hotkeySelect.registerBtn('exitBtn', exitBtn, () => hotkeySelect.clear()); + return hotkeySelect; + } + + // #endregion + // #region 核心功能函数和全局变量 + function getItemClsName(item) { + const itemClsName = { + "constants": "永久道具", + "tools": "消耗道具", + } + if (item == null) return "未知"; + if (item.cls == "equips") { + if (typeof item.equip.type == "string") return item.equip.type; + const type = core.getEquipTypeById(item.id); + return core.status.globalAttribute.equipName[type]; + } else return itemClsName[item.cls] || item.cls; + } + + function clearItemBoxCache() { + UI._itemId = ''; + UI.selectType = 'toolBox'; + [UI._toolInv, UI._equipInv, UI._equipSlots].forEach(menu => { + if (menu) { + menu.index = 0; + menu.page = 0; + } + }); + } // 每次存读档,及进行录像回放时调用,清空之前选中的道具信息 + this.clearItemBoxCache = clearItemBoxCache; + + // 以下是本插件范围内的全局变量 + + const UI = { + /** 当前打开的是道具页还是装备页 + * @type {'all'|'equips'} + **/ + type: 'all', + /** @type {'toolBox'|'equipBox'} 当前选中了哪一个子界面(切装面板/物品栏) */ + selectType: 'toolBox', + _itemId: '', + /** @type {string} 当前选中的物品ID,变化时自动触发右侧栏的信息变化 */ + get itemId() { + return this._itemId; + }, + set itemId(value) { + if (this._itemId !== value) { + this._itemId = value; + this.itemInfo.drawContent(); + } + }, + /** @type {undefined|ItemBoxBack} 物品页面的背景 */ + _back: undefined, + /** @type {undefined|ToolInventory} 道具背包 */ + _toolInv: undefined, + /** @type {undefined|EquipInventory} 装备背包 */ + _equipInv: undefined, + /** @type {undefined|ItemInfoBoard} 右侧显示选中物品详细信息的页面 */ + _itemInfo: undefined, + /** @type {undefined|EquipSlots} 显示已穿戴装备的面板 */ + _equipSlots: undefined, + /** 是否监听onMove事件 */ + isNoHover: core.getLocalStorage('itemBoxNoHover'), + /** 物品页面的背景 */ + get back() { + if (!this._back) { + this._back = new ItemBoxBack(); + const switchModeBtn = new SwitchBtn(385, 5, 24, 24, { strokeStyle: ' #8B4513', fillStyle: ' #D2691E' }); + const exitBtn = new ExitBtn(385, 385, 24, 24); + this._back.registerBtns([['exitBtn', exitBtn, UI.exit.bind(UI)], + ['switchModeBtn', switchModeBtn, UI.switchType.bind(UI)]]); + } + return this._back; + }, + /** 道具背包 */ + get toolInv() { + if (!this._toolInv) { + this._toolInv = new ToolInventory(0, 0, 240, 416, 137, 11); + const pgDown = new ArrowBtn(20, 375, 20, 20, 'left'); + const pgUp = new ArrowBtn(215, 375, 20, 20, 'right'); + this.toolInv.registerBtns([ + ['pgDownBtn', pgDown, () => UI.toolInv.pageDown()], + ['pgUpBtn', pgUp, () => UI.toolInv.pageUp()] + ]); + const allBtn = new ClassifyBtn(20, 10, 44, 24, "全部", "all"), + toolsBtn = new ClassifyBtn(80, 10, 44, 24, "消耗", "tools"), + constantsBtn = new ClassifyBtn(140, 10, 44, 24, "永久", "constants"); + const noHoverBtn = new NoHoverBtn(210, 10, 24, 24, null, { crossline1: this.isNoHover }); + const noHover = () => { + UI.isNoHover = !UI.isNoHover; + core.setLocalStorage('itemBoxNoHover', UI.isNoHover); + noHoverBtn.config.crossline1 = UI.isNoHover; + UI._toolInv?.drawButtonContent(); + } + this._toolInv.registerBtns([ + ['allBtn', allBtn, () => this.toolInv.classify('all')], + ['toolsBtn', toolsBtn, () => this.toolInv.classify('tools')], + ['constantsBtn', constantsBtn, () => this.toolInv.classify('constants')], + ['noHoverBtn', noHoverBtn, noHover] + ]); + if (!core.platform.isPC) noHoverBtn.disable = true; // 非PC端本来就触发不了onmove事件,故隐藏此按钮 + } + return this._toolInv; + }, + /** 装备背包 */ + get equipInv() { + if (!this._equipInv) { + this._equipInv = new EquipInventory('equipInventory', 0, 120, 240, 416, 137, 7); + const pgDown = new ArrowBtn(20, 255, 20, 20, 'left'); + const pgUp = new ArrowBtn(215, 255, 20, 20, 'right');; + this._equipInv.registerBtn('pgDownBtn', pgDown, () => UI.equipInv.pageDown()); + this._equipInv.registerBtn('pgUpBtn', pgUp, () => UI.equipInv.pageUp()); + } + return this._equipInv; + }, + /** 物品背包 */ + get itemInv() { + return (this.type === 'all') ? this.toolInv : this.equipInv; + }, + /** 右侧显示选中物品详细信息的页面 */ + get itemInfo() { + if (!this._itemInfo) { + this._itemInfo = new ItemInfoBoard(240, 0, core.__PIXELS__ - 240, core.__PIXELS__); + const hideBtn = new HideBtn(20, 380, 46, 24); + this._itemInfo.registerBtn('hideBtn', hideBtn, () => { + this.hideItem(UI.itemId); + this.itemInv.updateItemList(); + this.itemInv.setIndex(this.itemInv.index); + this.itemInv.drawContent(); + }); + const markBtn = new MarkBtn(80, 380, 46, 24); + this._itemInfo.registerBtn('markBtn', markBtn, () => { + this.markItem(UI.itemId); + this.itemInv.updateItemList(); + this.itemInv.setIndex(this.itemInv.index); + this.itemInv.drawContent(); + }); + const showHideBtn = new ShowHideBtn(20, 350, 95, 18); + this._itemInfo.registerBtn('showHideBtn', showHideBtn, () => { + this.switchShowHide(); + this.itemInv.updateItemList(); + this.itemInv.setIndex(this.itemInv.index); + this.itemInv.drawContent(); + this.itemInfo.drawContent(); // 这里不论选中物品是否变化,itemInfo必定要重绘,因为按钮图案会变 + }); + const setHotkeyBtn = new IconBtn(145, 60, 24, 24, 'keyboard'); + this.itemInfo.registerBtn('setHotkeyBtn', setHotkeyBtn, () => { + if (!UI.itemId) return; + [UI._back, UI.itemInv, UI._equipSlots, UI._itemInfo].forEach((menu) => { + if (menu) menu.endListen(); + }); + const hotkeySelect = hotkeySelectFactory(UI.itemId); + hotkeySelect.init(); + }); + } + return this._itemInfo; + }, + get equipSlots() { + if (!this._equipSlots) { + this._equipSlots = new EquipSlots(7, 10, 240, 125, 137); + const config = { marginTail: 4, marginSide: 3, marginTip: 2 }; + const pgDown = new ArrowBtn(0, 56, 14, 14, 'left', config); + const pgUp = new ArrowBtn(224, 56, 14, 14, 'right', config); + this._equipSlots.registerBtn('pgDownBtn', pgDown, () => UI.equipSlots.pageDown()); + this._equipSlots.registerBtn('pgUpBtn', pgUp, () => UI.equipSlots.pageUp()); + } + return this._equipSlots; + }, + /** 清空各个菜单的绘制和监听(不包括解除锁定) */ + clearAll() { + [this._back, this._toolInv, this._equipInv, this._itemInfo, this._equipSlots].forEach((menu) => { + if (menu) menu.clear(); + }); + core.status.event.id = null; + }, + initAll() { + [UI.back, UI.itemInv, UI.itemInfo].forEach((menu) => menu.init()); + if (UI.type === 'equips') UI.equipSlots.init(); + }, + /** 解除锁定,退出道具栏/装备栏 */ + exit() { + this.clearAll(); + setTimeout(core.unlockControl, 0); + }, + /** 在道具栏/装备栏模式中切换 */ + switchType() { + this.type = (this.type === 'all') ? 'equips' : 'all'; + if (this.type === 'all') { + this.equipSlots.clear(); + this.equipInv.clear(); + } else if (this.type === 'equips') { + this.toolInv.clear(); + this.equipSlots.init(); + } + this.itemInv.updateItemList(); + this.back.drawContent(); + this.itemInv.init(); + if (this.type === 'all') { + this.itemInv.setIndex(this.itemInv.index); + } + else if (this.type === 'equips') { + if (UI.selectType === 'toolBox') { + this.itemInv.setIndex(this.itemInv.index); + } + else { + this.equipSlots.setIndex(this.equipSlots.index); + } + } + }, + /** 隐藏 | 取消隐藏当前选中的物品 */ + hideItem(itemId) { + if (!itemId) return; + let hideInfo = core.getFlag('hideInfo', {}); + if (hideInfo.hasOwnProperty(itemId)) { + hideInfo[itemId] = !hideInfo[itemId]; + } else { + hideInfo[itemId] = !core.material.items[itemId].hideInToolbox; + } + core.setFlag('hideInfo', hideInfo); + }, + /** 置顶 | 取消置顶当前选中的物品 */ + markItem(itemId) { + let markedItems = core.getFlag('markedItems', []); + if (markedItems.includes(itemId)) markedItems = markedItems.filter((currId) => currId !== itemId); + else markedItems.push(itemId); + core.setFlag('markedItems', markedItems); + }, + /** 切入/切出显示已隐藏物品的模式 */ + switchShowHide() { + core.setFlag('showHideItem', !core.getFlag('showHideItem', false)); + } + } + + // 程序入口,在_drawToolbox 与 _drawEquipbox处调用 + /** @param {'all'|'equips'} currType */ + function drawItemBox(currType) { + UI.clearAll(); + if (UI._toolInv && UI._toolInv?.onDraw && currType === UI.type) return; + core.lockControl(); + UI.type = currType; + UI.itemInv.updateItemList(); + UI.itemInv.setIndex(UI.itemInv.index); + UI.initAll(); + } + // #endregion + }, + "autoChangeEquip": function () { + // 调用方法:在合适的位置调用函数figureEquip即可,例如在脚本编辑-按键处理加入case 89: core.plugin.figureEquip(); break; + // 即按Y键进入切装模式 + + let compareMode = false; + let equipStatus = []; + let equipIncluded; + + ////// 请在[]中填好不参与换装的装备孔的序号。 + // 例如,0号,4号装备孔不参与换装,则 ignoreList 应设为[0,4] + // 所有装备孔都参与换装,则 ignoreList 应设为[] + let ignoreList = []; + + ////// 请在{}中根据装备的穿脱事件手动填写装备穿脱时要执行的函数,没有则不填。只填写有效的数值变化即可。 + // 例如:{'sword3':{'equip':function(){core.setFlag('mms3',1);},'unequip':function(){core.setFlag('mms3',0);}}} + let equipEvents = {}; + + function compareEquip() { + + return new Promise(function (res) { + + const canvas = 'compareEquip', + width = core._PX_ || core.__PIXELS__, + height = core._PY_ || core.__PIXELS__; + + core.lockControl(); + + function finish() { + compareMode = false; + core.unregisterAction('onclick', 'bestEquip'); + core.deleteCanvas(canvas); + res(void 0); + } + + core.createCanvas(canvas, 0, 0, width, height, 160); + core.setTextAlign(canvas, 'center'); + core.fillText(canvas, '点击选择一个怪物,点击非怪物图块自动退出', width / 2, 20, 'red', '18px Arial'); + + core.registerAction('onclick', 'bestEquip', function (x, y, px, py) { + const cls = core.getBlockCls(x, y), + id = core.getBlockId(x, y); + if (!(cls === 'enemys' || cls === "enemy48")) { + finish(); + return false; + } + figureBestEquip(id, x, y); + core.updateDamage(); + finish(); + }, 100); + }) + } + + function figureBestEquip(id, x, y) { + compareMode = true; + const equipNum = core.status.globalAttribute.equipName.length; // 装备总数量 + + // 角色初始各项数值,用于推算出最优切装后复原初始状态 + const oriEffect = { + 'value': { 'atk': core.status.hero.atk, 'def': core.status.hero.def, 'mdef': core.status.hero.mdef, }, + 'percentage': { 'atk': core.getBuff('atk'), 'def': core.getBuff('def'), 'mdef': core.getBuff('mdef'), }, + 'equipment': core.clone(core.status.hero.equipment), + } + + if (!equipIncluded) equipIncluded = getEquipIncluded(equipNum); + + const equipIncludedNum = equipIncluded.length; + const equipNameList = core.status.globalAttribute.equipName.filter((ele, i) => { return !ignoreList.includes(i); }); + + equipStatus = equipIncluded.map((ele) => core.getEquip(ele)); //当前参与计算的各个装备孔的装备 + const equipOwned = getEquipOwned(equipNum); + let equipList = getEquipList(equipIncludedNum, equipOwned, equipNameList); + + const equipCombination = traverseSetCombinations(equipList); + + const bestCombination = findBestEquipComb(equipCombination, equipOwned, id, x, y); + + ['atk', 'def', 'mdef'].forEach((ele) => { + core.setStatus(ele, oriEffect.value[ele]); + core.setBuff(ele, oriEffect.percentage[ele]); + }); + core.status.hero.equipment = core.clone(oriEffect.equipment); + + equipBestComb(bestCombination, equipIncluded, equipNameList); + } + + // 返回一个包含所有参与切装计算的装备孔的序号的数组。 + // 例如,0,2,4号装备孔参与切装计算,则本函数返回[0,2,4] + function getEquipIncluded(equipNum) { + let equipIncluded = []; + for (let i = 0; i < equipNum; i++) { + if (!ignoreList.includes(i)) equipIncluded.push(i); + } + return equipIncluded; + } + + function getEquipOwned(equipNum) { + // equipOwned:当前拥有的所有装备的数量 + // 形如{sword1: 2, sword2: 1} + let equipOwned = core.clone(core.status.hero.items.equips); + for (let i = 0; i < equipNum; i++) { + if (ignoreList.includes(i)) continue; + const currEquip = core.getEquip(i); + if (currEquip !== null) + if (equipOwned.hasOwnProperty(currEquip)) equipOwned[currEquip]++; + else equipOwned[currEquip] = 1; + } + return equipOwned; + } + + // 生成切装列表,为一个二维数组 + function getEquipList(equipNum, equipOwned, equipNameList) { + // equipNameList:计入切装计算的装备格子的名称列表,可重复 + // 形如['武器', '武器', '盾牌'] + let equipList = Array(equipNameList.length).fill(void 0).map(() => new Set([null])); + + //对每个装备孔展开 + for (let i = 0, l = equipNameList.length; i < l; i++) { + for (let j in equipOwned) { + let equipType = core.material.items[j].equip.type; + switch (typeof equipType) { + case 'number': + for (let k = 0, l = equipOwned[j]; k < l; k++) { + equipList[equipIncluded.indexOf(equipType)].add(j); + } + break; + case 'string': + if (equipType === equipNameList[i]) + for (let k = 0, l = equipOwned[j]; k < l; k++) { equipList[i].add(j); } + break; + } + } + } + return equipList; + } + + function traverseSetCombinations(arr) { + const result = []; + const currentCombination = []; + + function backtrack(index) { + if (index === arr.length) { + result.push([...currentCombination]); + return; + } + const currentSet = Array.from(arr[index]); + for (let value of currentSet) { + currentCombination[index] = value; + backtrack(index + 1); + } + } + backtrack(0); + return result; + } + + function getEleCount(ele, arr) { + let count = 0; + for (let i = 0, l = arr.length; i < l; i++) { + if (arr[i] === ele) count++; + } + return count; + } + + function hasEnoughEquip(currComb, equipOwned) { + for (let i in equipOwned) { + const equipNeed = getEleCount(i, currComb); + if (equipOwned[i] < equipNeed) return false; + } + return true; + } + + // 按照给定的列表aimStatus,形如['sword1','sword2',null,'sword1'],修改equipStatus进行模拟切装 + function simulateEquip(equipStatus, aimStatus) { + equipIncluded.forEach((ele, i) => { core.status.hero.equipment[ele] = aimStatus[i]; }) + for (let i = 0, l = equipStatus.length; i < l; i++) { + if (equipStatus[i] !== aimStatus[i]) { + if (aimStatus[i] === null) { + const unequipId = equipStatus[i]; + if (equipEvents.hasOwnProperty(unequipId) && + equipEvents[unequipId].hasOwnProperty('unequip')) + equipEvents[unequipId].unequip(); + core.items._loadEquipEffect(null, unequipId); + } else { + const equipId = aimStatus[i]; + if (equipEvents.hasOwnProperty(equipId) && + equipEvents[equipId].hasOwnProperty('equip')) + equipEvents[equipId].equip(); + core.items._loadEquipEffect(equipId, equipStatus[i]); + } + equipStatus[i] = aimStatus[i]; + } + } + } + + function findBestEquipComb(equipCombination, equipOwned, id, x, y) { + let minDamage = core.getDamage(id, x, y), + bestCombination = core.clone(equipStatus); + for (let i = 0, l = equipCombination.length; i < l; i++) { + const currComb = equipCombination[i]; + if (!hasEnoughEquip(currComb, equipOwned)) continue; + simulateEquip(equipStatus, currComb); + let damage = core.getDamage(id, x, y); + if (damage !== null && (minDamage === null || damage < minDamage)) { + minDamage = damage; + bestCombination = core.clone(equipStatus); + } + } + return bestCombination; + } + + function equipBestComb(bestCombination, equipIncluded, equipNameList) { + /** @type {Set} */ + const duplicatedName = new Set([]), + name = core.status.globalAttribute.equipName; + + // 脱下重复装备 + equipNameList.forEach((ele) => { + if (getEleCount(ele, equipNameList) > 1) duplicatedName.add(ele); + }) + equipIncluded.forEach((ele) => { + if (duplicatedName.has(name[ele]) && core.getEquip(ele) !== null) { + core.unloadEquip(ele); + core.status.route.push("unEquip:" + ele.toString()); + } + }) + + for (let i = 0, l = bestCombination.length; i < l; i++) { + const currEquip = bestCombination[i], + pos = equipIncluded[i]; + if (core.getEquip(pos) === currEquip) continue; + else if (currEquip === null) { + core.unloadEquip(pos); + core.status.route.push("unEquip:" + pos.toString()); + } else { + core.loadEquip(currEquip); + core.status.route.push("equip:" + currEquip.toString()); + } + } + } + + this.figureEquip = function () { + compareEquip().then(function (confirm) { + core.unlockControl(); + }) + } + }, + "customizableToolBar": function () { + // 自定义工具栏显示项 + // 本插件需要配合main.js, control.js等的修改 + // 新的逻辑如下: + // 函数_updateStatusBar_setToolboxIcon在updateStatusBar中调用,仅用于切换录像replay/pause,和计算几个图标的透明度 + // setToolbarButton根据输入的类型重新向状态栏填入元素 + // resize添加一个只刷新工具栏的模式,此外,resize不调用setToolbarButton,相反,setToolbarButton后调用resize + // 以下地方调用setToolbarButton: hard的点击事件,回放录像的进入/退出事件 + // 要注意一点, floor是不能作为toolBar元素的,statuBar具有同id元素,样式会相互冲突 复制了一个外形一样的图标叫view,作为浏览地图按钮 + + /** + * PC 默认6个键且不需要切入数字键的模式 最多9个键 手机 默认9个键 最多9个键(手机理论上可以塞10个键,但是懒得判定了) + * normal:普通模式 num:按下数字键切换到的模式 replay:录像模式 opacity:透明度 + */ + const defaultConfig = { + normal: { + vertical: ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback'], + horizontal: ['book', 'fly', 'toolbox', 'save', 'load', 'settings'] + }, + num: { + vertical: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt'], + horizontal: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt'] + }, + replay: { + vertical: ['play', 'stop', 'rewind', 'book', 'view', 'speedDown', 'speedUp', 'save'], + horizontal: ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'save'] + }, + hide: { + vertical: [], horizontal: [], + } + }; + + function isVertical() { + return core.domStyle.isVertical || core.flags.extendToolbar; + } + + function getToolBarConfig(type) { + const currObj = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); + return isVertical() ? currObj.vertical : currObj.horizontal; + } + this.getToolBarConfig = getToolBarConfig; + + /** + * + * @param {string} type + * @param {number} index + * @param {string} value + * @example core.setToolBarConfig('normal', 3, null) + */ + function setToolBarConfig(type, index, value) { + const allToolType = ['normal', 'num', 'replay']; + const allTools = ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback', 'undoRollback', + 'btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btn9', 'btnAlt', + 'equipbox', 'floor', 'play', 'rewind', 'speedDown', 'speedUp', 'stop', 'single', 'view']; + if (!allToolType.includes(type) || index < 0 || index > 8) { + core.drawFailTip('请选中工具栏的一个合法位置作为目标点!'); + return; + } + if (value !== 'delete' && !allTools.includes(value)) { + core.drawFailTip('请选中一个图标作为替换目标!'); + return; + } + const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); + const key = isVertical() ? 'vertical' : 'horizontal'; + if (type === 'replay' && + (['fly', 'shop', 'load', 'settings', 'btnAlt', 'rollback', 'undoRollback', 'btnAlt'].includes(value))) { + core.drawFailTip('该按钮不允许放在录像模式下!'); + return; + } // 录像模式下的按键处理有一套专门的逻辑,在_sys_onkeyUp_replay,实际上并不能读取自动档 + if (type !== 'replay' && ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'single'].includes(value)) { + core.drawFailTip('该按钮不允许放在非录像模式下!'); + return; + } + + if (value === 'delete') toorBarConfig[key].splice(index, 1); + else { + if (index > toorBarConfig[key].length) { + core.drawFailTip('按钮中间不能有空白!'); + return; + } + const oldIndex = toorBarConfig[key].indexOf(value); + if (oldIndex !== -1) { // 如果目标位置有图标,两者交换 + if (toorBarConfig[key][index] === value) return; + const aimTool = toorBarConfig[key][index]; + toorBarConfig[key][index] = value; + toorBarConfig[key][oldIndex] = aimTool; + } + else toorBarConfig[key][index] = value; + } + core.setLocalStorage('toorBarConfig' + type, toorBarConfig); + setToolbarButton(core.domStyle.toolbarBtn); + } + this.setToolBarConfig = setToolBarConfig; + + function resetToolBarConfig() { + for (let type in defaultConfig) { + if (!defaultConfig.hasOwnProperty(type)) return; + const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]); + core.setLocalStorage('toorBarConfig' + type, toorBarConfig); + } + } + this.resetToolBarConfig = resetToolBarConfig; + + /** + * + * @param {'normal'|'num'|'replay'|'hide'} type + */ + function setToolbarButton(type) { + if (main.replayChecking) return; // 录像验证必须干掉此函数 因为它操作了DOM + const currList = getToolBarConfig(type); + if (!currList) return; + const fragment = document.createDocumentFragment(); + for (let i = 0, l = currList.length; i < l; i++) { + const iconId = currList[i]; + const currEle = core.statusBar.image[iconId]; + if (!currEle) continue; + currEle.style.display = 'block'; + fragment.appendChild(currEle); + } + if (type !== "hide") { + core.domStyle.toolbarBtn = type; + } + core.domStyle.toolsCount = currList.length; + fragment.appendChild(core.dom.hard); // 难度一定会显示 因为难度所在位置要用于切换常规模式和数字模式 难度的尺寸是动态决定的 + core.dom.toolBar.innerHTML = ''; + core.dom.toolBar.appendChild(fragment); + core.control.resize('tools'); // 在这里计算难度的尺寸 + } + this.setToolbarButton = setToolbarButton; + }, + "setting": function () { + // 自绘设置界面 + // 请保持本插件在所有插件的最下方 + + // #region 复写 + // 复写resize,保证屏幕变化时此画布表现正常 + const originResize = core.control.resize; + core.control.resize = function () { + originResize.apply(core.control, arguments); + /** @type {SettingBack} */ + const settingMenu = core.plugin.settingMenu; + if (settingMenu && settingMenu.onDraw) { + settingMenu.drawContent(); + settingMenu.pageList[settingMenu.currPage].drawContent(); + } + } + // #endregion + + /** + * @typedef {{ + * getName:()=>string, + * effect:()=>void, + * text:string, + * replay?:boolean, + * draw?:((ctx:string)=>void) + * }} Setting + */ + function invertFlag(name) { + core.setFlag(name, !core.hasFlag(name)); + } + + function invertLocalStorage(name) { + const value = core.getLocalStorage(name, false); + core.setLocalStorage(name, !value); + } + + // #endregion + // #region 设置的具体内容,与相应的录像注册 + // #endregion + // #region 按钮类 + + const { ButtonBase, RoundBtn, IconBtn, ExitBtn, + ArrowBtn, MenuBase, Pagination, KeyCodeEnum } = core.plugin.uiBase; + + class TextButton extends ButtonBase { + constructor(x, y, w, h, text) { + super(x, y, w, h); + this.text = text; + this.draw = () => { + if (this.disable) return; + core.ui.fillText(this.ctx, this.text, + this.x + this.w / 2, this.y + this.h / 2 + 5, 'white', '16px Verdana'); + } + } + } + // #endregion + // #region 菜单类 + + // #region 单个设置菜单的基类 + /** 单个设置按钮的基类 */ + class SettingButton extends ButtonBase { + constructor(x, y, w, h, ...drawArgs) { + super(x, y, w, h); + /** @type {Setting} 将在registerBtn时赋予此项 */ // @ts-ignore + this.setting; + this.drawArgs = drawArgs; + } + + draw() { + const ctx = this.ctx; + const [x, y, w, h] = [this.x, this.y, this.w, this.h]; + const setting = this.setting; + // 取消注释下面这一句将显示所有按钮的判定框 + // core.strokeRect(ctx, this.x, this.y, this.w, this.h, 'yellow'); + core.setTextAlign(ctx, 'start'); + core.ui.fillText(ctx, setting.getName(), x, y + h / 2 + 5, 'white', '16px Verdana'); + const drawFunc = setting.draw; + if (this.status === 'selected') { + core.drawUIEventSelector(0, "winskin.png", this.x, this.y, this.w, this.h, 137); + } + if (drawFunc) drawFunc.apply(this, [ctx, ...this.drawArgs]); + } + } + + // @todo 悬浮按钮不改变时不触发事件 这个优化有没有做 + + /** 单个设置菜单的基类 */ + class SettingOnePage extends MenuBase { + constructor(name, settingData) { + super(name, ['ondown', 'onmove', 'keyDown']); + this.text = ''; + /** @type {string | undefined}*/ + this.selectedPos; + /** @type {SettingButton | undefined}*/ + this.selectedBtn; + /** @type {{[x:string]:Setting}}*/ + this.settingData = settingData; + } + + keyDownEvent(keyCode) { + let x, y; + const changePos = (newPos) => { + if (this.btnMap.has(newPos)) { + const button = this.btnMap.get(newPos); + this.focus(button, newPos); + } + } + if ([KeyCodeEnum.Left, KeyCodeEnum.Right, KeyCodeEnum.Up, KeyCodeEnum.Down].includes(keyCode)) { + if (!this.selectedBtn || !this.selectedPos) { + const button = this.btnMap.get('1,1'); + if (button) this.focus(button, '1,1'); + return; + } else { + [x, y] = this.selectedPos.split(',').map((x) => parseInt(x)); + if (keyCode === KeyCodeEnum.Left) x--; + if (keyCode === KeyCodeEnum.Up) y--; + if (keyCode === KeyCodeEnum.Right) x++; + if (keyCode === KeyCodeEnum.Down) y++; + let newPos = x + ',' + y; + + // 逻辑:左右,查找不到对应坐标就不动。 + // 上下,查找不到对应坐标,只要该列存在第一个元素,就会移到该列。 + + if (keyCode === KeyCodeEnum.Left || keyCode === KeyCodeEnum.Right) { + changePos(newPos); + } + if (keyCode === KeyCodeEnum.Up || keyCode === KeyCodeEnum.Down) { + if (this.btnMap.has(newPos)) { + const button = this.btnMap.get(newPos); + this.focus(button, newPos); + } else { + newPos = '1,' + y; + changePos(newPos); + } + } + } + } else { + switch (keyCode) { + case KeyCodeEnum.Enter: // Enter/Space + case KeyCodeEnum.SpaceBar: + if (this.selectedBtn) this.selectedBtn.ondown(-1, -1, -1, -1); + break; + } + } + } + + /** + * @param {SettingButton} btn + * @param {string} key 对应事件的索引 + **/ + execEffect(btn, key, ...eventArgs) { + const setting = btn.setting; + setting.effect.apply(this, eventArgs); + if (setting.replay) { + let actionString = 'cSet:' + key; + if (eventArgs && eventArgs.length > 0) { + actionString += ':' + eventArgs.map(arg => encodeURIComponent(arg)).join(':'); + } + core.status.route.push(actionString); + } + this.drawContent(); + } + + /** 聚焦到指定的按钮并重绘菜单 */ + focus(button, pos) { + if (this.selectedBtn === button) return; // 如果当前按钮已被选中,则不做任何操作 + this.selectedPos = pos; + this.selectedBtn = button; + this.btnMap.forEach((currBtn) => { + currBtn.status = (currBtn === button) ? 'selected' : 'none'; + }); + if (button instanceof SettingButton) { + this.text = button.setting.text || ''; + } + this.drawContent(); + } + + drawContent(ctx) { + if (!ctx) ctx = this.name; + super.drawContent(); + if (this.text && this.text.length > 0) { + core.ui.drawTextContent(ctx, this.text, { + left: 30, + top: 78, + bold: false, + color: "white", + align: "left", + fontSize: 14, + maxWidth: 350 + }); + } + } + + clear() { + core.clearUIEventSelector(0); // 光标的绘制在按钮中进行 + super.clear(); + } + + /** + * @override + * @param {string} pos + * @param {string} key + * @param {SettingButton} btn + * @param {...(string|number)} eventArgs + */ + registerSettingBtn(pos, key, btn, ...eventArgs) { + super.registerBtn(pos, btn, { + 'ondown': () => { + if (this.selectedBtn === btn) this.execEffect(btn, key, ...eventArgs); + else this.focus(btn, pos); + }, + 'onmove': () => { + this.focus(btn, pos); + } + }); + btn.setting = this.settingData[key]; + } + + registerSettingBtns(arr) { + arr.forEach(ele => { + const [key, btn, event, ...eventArgs] = ele; + this.registerSettingBtn(key, btn, event, ...eventArgs); + }); + } + } + // #endregion + + // #region 功能菜单 + function checkSkipFuncs() { + const skipText = core.getLocalStorage('skipText'); + // 此函数用于检测是否处在录像模式下,是则将跳过所有非必要对话 + core.events.__action_checkReplaying = skipText ? function () { + core.doAction(); + return true; + }.bind(core.events) : events.prototype.__action_checkReplaying; + + const skipPeform = core.getLocalStorage('skipPeform'); + + const instantMove = function (fromX, fromY, aimX, aimY, keep, callback) { + const [_block, blockInfo] = core.maps._getAndRemoveBlock(fromX, fromY); + if (keep) { + core.setBlock(blockInfo.number, aimX, aimY); + core.showBlock(aimX, aimY); + } + if (callback) callback(); + } + + core.maps.jumpBlock = skipPeform ? function (sx, sy, ex, ey, time, keep, callback) { + return instantMove(sx, sy, ex, ey, keep, callback); + }.bind(core.maps) : maps.prototype.jumpBlock; + + core.maps.moveBlock = skipPeform ? function (x, y, steps, time, keep, callback) { + maps.prototype.moveBlock(x, y, steps, 1, keep, callback); + }.bind(core.maps) : maps.prototype.moveBlock; + + core.maps.drawAnimate = skipPeform ? function (name, x, y, alignWindow, callback) { + if (callback) callback(); + return -1; + }.bind(core.maps) : maps.prototype.drawAnimate; + + core.maps.drawHeroAnimate = skipPeform ? function (name, callback) { + if (callback) callback(); + return -1; + }.bind(core.maps) : maps.prototype.drawHeroAnimate; + + core.events.jumpHero = skipPeform ? function (ex, ey, time, callback) { + const { x: sx, y: sy } = core.status.hero.loc; + if (ex == null) ex = sx; + if (ey == null) ey = sy; + core.setHeroLoc('x', ex); + core.setHeroLoc('y', ey); + core.clearMap('hero'); + core.drawHero(); + if (callback) callback(); + }.bind(core.events) : events.prototype.jumpHero; + + core.events.vibrate = skipPeform ? function (direction, time, speed, power, callback) { + if (callback) callback(); + return; + }.bind(core.events) : events.prototype.vibrate; + + core.events._action_sleep = skipPeform ? function (data, x, y, prefix) { + core.doAction(); + }.bind(core.events) : events.prototype._action_sleep; + } + this.checkSkipFuncs = checkSkipFuncs; + /** @type {{[x:string]:Setting}} */ + const gamePlaySetting = { + autoGet: { + getName: () => '自动拾取:' + (core.getFlag('autoGet', false) ? '开' : '关'), + effect: () => invertFlag('autoGet'), + text: '每走一步,自动拾取当前层可获得的道具。', + replay: true, + }, + autoBattle: { + getName: () => '自动清怪:' + (core.getFlag('autoBattle', false) ? '开' : '关'), + effect: () => invertFlag('autoBattle'), + text: '每走一步,自动和当前层可到达位置伤害为0的敌人战斗。对部分特殊敌人无效。', + replay: true, + }, + noRouting_HP: { + getName: () => '', + effect: () => invertFlag('noRouting_HP'), + text: '自动寻路时绕过加血物品。同时自动拾取也将忽略这类物品。', + replay: true, + /** @this {SettingButton} */ + draw: function (ctx) { + core.setAlpha(ctx, core.hasFlag('noRouting_HP') ? 1 : 0.3); + core.drawIcon(ctx, 'redPotion', this.x, this.y, this.w, this.h); + core.setAlpha(ctx, 1); + } + }, + noRouting_MDEF: { + getName: () => '', + effect: () => invertFlag('noRouting_MDEF'), + text: '自动寻路时绕过加护盾物品。同时自动拾取也将忽略这类物品。', + replay: true, + /** @this {SettingButton} */ + draw: function (ctx) { + core.setAlpha(ctx, core.hasFlag('noRouting_MDEF') ? 1 : 0.3); + core.drawIcon(ctx, 'greenGem', this.x, this.y, this.w, this.h); + core.setAlpha(ctx, 1); + } + }, + noRouting_ATK: { + getName: () => '', + effect: () => invertFlag('noRouting_ATK'), + text: '自动寻路时绕过加攻物品。同时自动拾取也将忽略这类物品。', + replay: true, + /** @this {SettingButton} */ + draw: function (ctx) { + core.setAlpha(ctx, core.hasFlag('noRouting_ATK') ? 1 : 0.3); + core.drawIcon(ctx, 'redGem', this.x, this.y, this.w, this.h); + core.setAlpha(ctx, 1); + } + }, + noRouting_DEF: { + getName: () => '', + effect: () => invertFlag('noRouting_DEF'), + text: '自动寻路时绕过加防物品。同时自动拾取也将忽略这类物品。', + replay: true, + /** @this {SettingButton} */ + draw: function (ctx) { + core.setAlpha(ctx, core.hasFlag('noRouting_DEF') ? 1 : 0.3); + core.drawIcon(ctx, 'blueGem', this.x, this.y, this.w, this.h); + core.setAlpha(ctx, 1); + } + }, + clickMove: { + getName: () => '单击瞬移:' + (core.hasFlag('__noClickMove__') ? '关' : '开'), + effect: () => invertFlag('__noClickMove__'), + text: '系统设置。单击即可触发瞬移。', + replay: true, + }, + moveSpeedDown: { + getName: () => ' < 步时:' + core.values.moveSpeed, + effect: () => core.actions._clickSwitchs_action_moveSpeed(-10), + text: '缩短步时。', + replay: false, // 录像中不可录入任何DOM操作 + }, + moveSpeedUp: { + getName: () => ' > ', + effect: () => core.actions._clickSwitchs_action_moveSpeed(10), + text: '增大步时。', + replay: false, + }, + floorChangeTimeDown: { + getName: () => ' < 转场:' + core.values.floorChangeTime, + effect: () => core.actions._clickSwitchs_action_floorChangeTime(-100), + text: '缩短转场时间。', + replay: false, // 录像中不可录入任何DOM操作 + }, + floorChangeTimeUp: { + getName: () => ' > ', + effect: () => core.actions._clickSwitchs_action_floorChangeTime(100), + text: '增大转场时间。', + replay: false, + }, + skipText: { + getName: () => '跳过剧情:' + (core.getLocalStorage('skipText', false) ? '开' : '关'), + effect: () => { + invertLocalStorage('skipText'); + checkSkipFuncs(); + }, + text: '跳过全部文字对话。初见请勿开启此选项。', + replay: false, + }, + skipPeform: { + getName: () => '跳过演出:' + (core.getLocalStorage('skipPerform', false) ? '开' : '关'), + effect: () => { + invertLocalStorage('skipPerform'); + checkSkipFuncs(); + }, + text: '加速等待、播放动画等常见演出效果。', + replay: false, + }, + comment: { + getName: () => '在线留言:' + (core.hasFlag('comment') ? '开' : '关'), + effect: () => { + if (core.hasFlag('comment')) { + core.setFlag('comment', false); + core.plugin.clearCommentSign(); + } else { + core.setFlag('comment', true); + core.plugin.drawCommentSign(); + } + }, + text: '在地图上显示玩家的在线留言。', + replay: true, + }, + autoHideFloor: { + getName: () => '自动隐藏楼层:' + (core.hasFlag('autoHideFloor') ? '开' : '关'), + effect: () => { + invertFlag('autoHideFloor'); + }, + text: '一个楼层已无物品、敌人、NPC(不含已忽略图块),且无未到达楼层传送口时可被自动隐藏,仅在首次进入此状态时在楼传界面触发。', + replay: true, + }, + autoSaveBeforeUseItem: { + getName: () => '使用物品自动保存:' + (core.getLocalStorage('autoSaveBeforeUseItem') ? '开' : '关'), + effect: () => { + invertLocalStorage('autoSaveBeforeUseItem'); + }, + text: '使用消耗类物品(含破炸飞跳)前,以及即将走入滑冰、触发捕捉时自动存档。', + replay: false, + }, + autoSaveBeforePickItem: { + getName: () => '拾取物品自动保存:' + (core.getLocalStorage('autoSaveBeforePickItem') ? '开' : '关'), + effect: () => { + invertLocalStorage('autoSaveBeforePickItem'); + }, + text: '拾取地上物品前自动存档。', + replay: false, + }, + } + + class GamePlay extends SettingOnePage { + constructor() { + super('gamePlay', gamePlaySetting); + } + + drawContent() { + const ctx = this.createCanvas(); + core.fillText(ctx, '-- 自动 --', 40, 175, ' #FFE4B5', '18px Verdana'); + core.fillText(ctx, '-- 瞬移 --', 40, 225, ' #FFE4B5', '18px Verdana'); + core.fillText(ctx, '绕开', 220, 250, 'white', '16px Verdana'); + core.fillText(ctx, '-- 杂项 --', 40, 275, ' #FFE4B5', '18px Verdana'); + super.drawContent(ctx); + } + } + + function gamePlayFactory() { + const gamePlayMenu = new GamePlay(); + gamePlayMenu.registerSettingBtns([ + ['1,1', 'autoGet', new SettingButton(40, 180, 150, 30)], + ['2,1', 'autoBattle', new SettingButton(220, 180, 150, 30)], + ['1,2', 'clickMove', new SettingButton(40, 230, 150, 30)], + ['2,2', 'noRouting_HP', new SettingButton(260, 234, 24, 24)], + ['3,2', 'noRouting_MDEF', new SettingButton(290, 234, 24, 24)], + ['4,2', 'noRouting_ATK', new SettingButton(320, 234, 24, 24)], + ['5,2', 'noRouting_DEF', new SettingButton(350, 234, 24, 24)], + ['1,3', 'moveSpeedDown', new SettingButton(40, 280, 25, 25)], + ['2,3', 'moveSpeedUp', new SettingButton(140, 280, 25, 25)], + ['3,3', 'floorChangeTimeDown', new SettingButton(220, 280, 25, 25)], + ['4,3', 'floorChangeTimeUp', new SettingButton(340, 280, 25, 25)], + ['1,4', 'skipText', new SettingButton(40, 305, 150, 25)], + ['2,4', 'skipPeform', new SettingButton(220, 305, 150, 25)], + ['1,5', 'comment', new SettingButton(40, 330, 150, 25)], + ['2,5', 'autoHideFloor', new SettingButton(220, 330, 150, 25)], + ['1,6', 'autoSaveBeforeUseItem', new SettingButton(40, 355, 150, 25)], + ['2,6', 'autoSaveBeforePickItem', new SettingButton(220, 355, 150, 25)], + ]); + return gamePlayMenu; + } + // #endregion + // #region 视图菜单 + /** @type {{[x:string]:Setting}} */ + const gameViewSetting = { + itemDetail: { + getName: () => '物品显示数据:' + (core.hasFlag('itemDetail') ? '开' : '关'), + effect: () => { + invertFlag('itemDetail'); + core.control.updateStatusBar(); // 更新地图上物品的属性显示 + }, + text: '在地图上显示即捡即用道具和装备增加的属性值。', + replay: true, + }, + zoomIn: { + getName: () => ' < 放缩:' + Math.max(core.domStyle.scale, 1) + 'x', + effect: () => core.actions._clickSwitchs_display_setSize(-1), + text: '放缩', + replay: false, + }, + zoomOut: { + getName: () => ' > ', + effect: () => core.actions._clickSwitchs_display_setSize(1), + text: '放缩。', + replay: false, + }, + HDCanvas: { + getName: () => '高清画面:' + (core.flags.enableHDCanvas ? '开' : '关'), + effect: core.actions._clickSwitchs_display_enableHDCanvas, + text: '高清画面。本功能开关后刷新游戏才能看到效果。', + replay: false, + }, + enableEnemyPoint: { + getName: () => '定点怪显:' + (core.flags.enableEnemyPoint ? '开' : '关'), + effect: core.actions._clickSwitchs_display_enableEnemyPoint, + text: '怪物属性定点显示功能,即属性不同的怪物会在怪物手册单列。', + replay: false, + }, + displayEnemyDamage: { + getName: () => '怪物显伤:' + (core.flags.displayEnemyDamage ? '开' : '关'), + effect: core.actions._clickSwitchs_display_enemyDamage, + text: '地图上显示怪物的伤害。', + replay: false, + }, + displayCritical: { + getName: () => '临界显伤:' + (core.flags.displayCritical ? '开' : '关'), + effect: core.actions._clickSwitchs_display_critical, + text: '地图上显示怪物的临界值', + replay: false, + }, + displayExtraDamage: { + getName: () => '领域显伤:' + (core.flags.displayExtraDamage ? '开' : '关'), + effect: core.actions._clickSwitchs_display_extraDamage, + text: '领域显伤', + replay: false, + }, + extraDamageType: { + getName: () => '领域模式:' + (core.flags.extraDamageType == 2 ? '[最简]' : core.flags.extraDamageType == 1 ? '[半透明]' : '[完整]'), + effect: core.actions._clickSwitchs_display_extraDamageType, + text: '是否显示不可通行地块的领域伤害。', + replay: false, + }, + autoScale: { + getName: () => '自动放缩:' + (core.getLocalStorage('autoScale') ? '开' : '关'), + effect: () => { + core.setLocalStorage('autoScale', !core.getLocalStorage('autoScale')); + }, + text: '自动放缩。', + replay: false, + }, + bgm: { + getName: () => '音乐:' + (core.musicStatus.bgmStatus ? '开' : '关'), + effect: core.actions._clickSwitchs_sounds_bgm, + text: '播放背景音乐。', + replay: false, + }, + se: { + getName: () => '音效:' + (core.musicStatus.soundStatus ? '开' : '关'), + effect: core.actions._clickSwitchs_sounds_se, + text: '播放音效。', + replay: false, + }, + decreaseVolume: { + getName: () => " < 音量:" + Math.round(Math.sqrt(100 * core.musicStatus.userVolume)), + effect: () => core.actions._clickSwitchs_sounds_userVolume(-1), + text: '减小音量。', + replay: false, + }, + increaseVolume: { + getName: () => ' > ', + effect: () => core.actions._clickSwitchs_sounds_userVolume(1), + text: '增大音量。', + replay: false, + }, + }; + + class GameView extends SettingOnePage { + constructor() { + super('gameView', gameViewSetting); + } + + drawContent() { + const ctx = this.createCanvas(); + core.fillText(ctx, '-- 显示 --', 40, 175, ' #FFE4B5', '18px Verdana'); + core.fillText(ctx, '-- 音效 --', 40, 320, ' #FFE4B5', '18px Verdana'); + super.drawContent(ctx); + } + } + + function gameViewFactory() { + const gameViewMenu = new GameView(); + const advanceDisplayBtn = new RoundBtn(300, 280, 32, 18, "高级", { + font: '12px Verdana', fillStyle: "SlateGray", radius: 1, fontStyle: "yellow", + strokeStyle: "black" + }); + gameViewMenu.registerBtn('openAdvanceDisplay', advanceDisplayBtn, () => { + const settingMenu = core.plugin.settingMenu; + if (settingMenu) { + settingMenu.endListen(); + // 隐藏大菜单的按钮是为了避免视觉上的干扰 + settingMenu.btnMap.forEach(btn => { btn.disable = true }); + settingMenu.pageList[settingMenu.currPage].endListen(); + settingMenu.drawContent(); + } + core.ui.clearUIEventSelector(0); + const advanceDisplayMenu = advanceDisplayFactory(); + advanceDisplayMenu.init(); + }); + gameViewMenu.registerSettingBtns([ + ['1,1', 'itemDetail', new SettingButton(40, 180, 150, 25)], + ['1,2', 'displayEnemyDamage', new SettingButton(40, 205, 150, 25)], + ['1,3', 'displayExtraDamage', new SettingButton(40, 230, 150, 25)], + ['1,4', 'autoScale', new SettingButton(40, 255, 150, 25)], + ['1,5', 'HDCanvas', new SettingButton(40, 280, 150, 25)], + ['1,6', 'bgm', new SettingButton(40, 325, 150, 25)], + ['1,7', 'decreaseVolume', new SettingButton(40, 350, 25, 25)], + ['2,7', 'increaseVolume', new SettingButton(140, 350, 25, 25)], + ['2,1', 'displayEnemyDamage', new SettingButton(220, 180, 150, 25)], + ['2,2', 'displayCritical', new SettingButton(220, 205, 150, 25)], + ['2,3', 'extraDamageType', new SettingButton(220, 230, 150, 25)], + ['2,4', 'zoomIn', new SettingButton(220, 255, 25, 25)], + ['3,4', 'zoomOut', new SettingButton(330, 255, 25, 25)], + ['2,6', 'se', new SettingButton(220, 325, 150, 25)], + ]); + return gameViewMenu; + } + // #endregion + // #region 快捷键菜单 + /** @type {{[x:string]:Setting}} */ + const keySetting = { + leftHand: { + getName: () => '左手模式:' + (core.flags.leftHandPrefer ? '开' : '关'), + effect: () => { + core.flags.leftHandPrefer = !core.flags.leftHandPrefer; + core.setLocalStorage('leftHandPrefer', core.flags.leftHandPrefer); + }, + text: '系统设置。左手模式下WASD将用于移动角色,IJKL对应于原始的WASD进行存读档等操作。', + replay: true, + }, + setHotKey: { + getName: () => '', + effect: /** @this {GameView} */ function (num) { + core.utils.myprompt('输入物品名。名称(例如:破墙镐)或英文ID(例如:pickaxe)均可。', '', (value) => { + const itemInfo = core.material.items; + const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value); + if (aimItem) { + if (['constants', 'tools'].includes(aimItem.cls)) { + for (let i = 1; i <= 9; i++) { + if (i !== num && core.getLocalStorage('hotkey' + i) === aimItem.id) { + core.setLocalStorage('hotkey' + i, null); + } + } + core.setLocalStorage('hotkey' + num, aimItem.id); + this.drawContent(); + } else { + core.drawFailTip('错误:该类型的物品不支持快捷使用!'); + } + } else { + core.drawFailTip('错误:找不到该名称的物品!'); + } + }, () => { }); + }, + text: '给选定的数字键绑定一个可快捷使用的物品。', + replay: false, + draw: /** @this {SettingButton} */ function (ctx, num) { + const item = core.getLocalStorage('hotkey' + num, null); + let icon, itemName; + if (item && core.material.items.hasOwnProperty(item)) { + icon = item; + itemName = core.material.items[item].name; + } else { + const itemMap = { + '1': { icon: 'pickaxe', itemName: '破墙镐' }, + '2': { icon: 'bomb', itemName: '炸弹' }, + '3': { icon: 'centerFly', itemName: '中心飞' }, + '4': { itemName: '杂物' }, + '5': { itemName: '回退一步' }, + '6': { itemName: '撤销回退' }, + '7': { itemName: '轻按' } + }; + if (num >= 1 && num <= 9) { + ({ icon, itemName = '无' } = itemMap[num] || {}); + } + } + const keyIcon = 'btn' + num; + core.drawIcon(ctx, keyIcon, this.x, this.y, 16, 16); + const hasItem = core.material.items.hasOwnProperty(icon); + if (hasItem) core.drawIcon(ctx, icon, this.x + 20, this.y, 16, 16); + core.setTextAlign(ctx, 'left'); + core.setTextBaseline(ctx, 'alphabetic'); + core.fillText(ctx, itemName || '无', this.x + (hasItem ? 40 : 20), this.y + 14, 'white', '16px Verdana'); + } + }, + clearHotKeys: { + getName: () => '', + effect: /** @this {GameView} */ function () { + for (let i = 1; i <= 9; i++) { + core.setLocalStorage('hotkey' + i, null); + } + this.drawContent(); + core.drawSuccessTip('快捷键已重置到默认状态。'); + }, + text: '重置本页面所有快捷键到默认状态。', + replay: false, + draw: /** @this {SettingButton} */ function (ctx) { + core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#D3D3D3'); + core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#888888'); + core.fillText(ctx, '重置', this.x + 5, this.y + this.h / 2 + 5, '#333333', '16px Verdana'); + } + } + }; + class KeyMenu extends SettingOnePage { + constructor() { + super('key', keySetting); + } + + drawContent() { + const ctx = this.createCanvas(); + core.fillText(ctx, '-- 快捷键 --', 40, 205, ' #FFE4B5', '18px Verdana'); + core.fillText(ctx, "注意⚠️更推荐在背包", 160, 190, "rgb(255,255,51)", '12px Verdana'); + core.fillText(ctx, "中点击右上角", 287, 190, "rgb(255,255,51)", '12px Verdana'); + core.fillText(ctx, "图标设置单物品的快捷键", 160, 210, "rgb(255,255,51)", '12px Verdana'); + core.drawIcon(ctx, "toolbox", 272, 178, 16, 16); + core.drawIcon(ctx, "keyboard", 359, 178, 16, 16); + super.drawContent(ctx); + } + } + + function keyMenuFactory() { + const keyMenu = new KeyMenu(); + keyMenu.registerSettingBtns([ + ['1,1', 'leftHand', new SettingButton(40, 160, 150, 25)], + ['1,2', 'setHotKey', new SettingButton(40, 220, 150, 25, '1'), 1], + ['2,2', 'setHotKey', new SettingButton(220, 220, 150, 25, '2'), 2], + ['1,3', 'setHotKey', new SettingButton(40, 250, 150, 25, '3'), 3], + ['2,3', 'setHotKey', new SettingButton(220, 250, 150, 25, '4'), 4], + ['1,4', 'setHotKey', new SettingButton(40, 280, 150, 25, '5'), 5], + ['2,4', 'setHotKey', new SettingButton(220, 280, 150, 25, '6'), 6], + ['1,5', 'setHotKey', new SettingButton(40, 310, 150, 25, '7'), 7], + ['1,6', 'clearHotKeys', new SettingButton(300, 350, 42, 25)], + ]); + return keyMenu; + } + // #endregion + // #region 自定义工具栏界面 + /** 自定义工具栏界面 */ + class ToolBtn extends ButtonBase { + constructor(x, y, w, h, icon, text, config) { + super(x, y, w, h); + /** @type {ToolBarConfigPage} */ // @ts-ignore + this.menu; + this.icon = icon; // 特殊icon:delete 用于删除图标 + /** @todo 这里需要重构 */ + this.text = text; + this.config = config || {}; + this.ondown = () => { + this.menu.selectedTool = this.icon; + this.menu.text = this.text; + this.menu.drawContent(); + }; + } + + draw() { + const ctx = this.ctx; + const { strokeStyle = 'white', fillStyle = 'white', selectedStyle = 'gold' } = this.config; + core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, (this.menu.selectedTool === this.icon) ? selectedStyle : strokeStyle); + if (this.icon === 'delete') { + core.drawLine(ctx, this.x + 2, this.y + 2, this.x + this.w - 2, this.y + this.h - 2, 'red', 2); + core.drawLine(ctx, this.x + 2, this.y + this.h - 2, this.x + this.w - 2, this.y + 2, 'red', 2); + } else core.drawIcon(ctx, this.icon, this.x, this.y, this.w, this.h); + } + } + + class ToolBarBtn extends ButtonBase { + constructor(x, y, size, type, length, text) { + super(x, y, length * size, size); + /** @type {ToolBarConfigPage} */ // @ts-ignore + this.menu; + this.type = type; + this.length = length; + /** @todo 这里需要重构 */ + this.text = text; + this.ondown = (x, y, px, py) => { + const squareSize = this.h; + const index = Math.floor((px - this.x) / squareSize); + this.menu.type = type; + this.menu.index = index; + this.menu.drawContent(); + } + } + + draw() { + const ctx = this.ctx; + const squareSize = this.h; + const type = this.type; + const toolBarConfig = core.plugin.getToolBarConfig(type); + for (let i = 0; i < this.length; i++) { + const style = (this.menu.type === type && this.menu.index === i) ? 'gold' : 'white'; + core.strokeRoundRect(ctx, this.x + squareSize * i + i, this.y, squareSize, squareSize, 3, style); + core.drawIcon(ctx, toolBarConfig[i], this.x + squareSize * i + i, this.y, squareSize, squareSize); + } + } + } + + class ToolBarConfigPage extends MenuBase { + constructor() { + super('toolBarConfig', ['ondown']); + /** 当前选中的图标 */ + this.selectedTool = 'none'; + /** 当前选中了哪个类型的工具栏 */ + this.type = 'none'; + /** 当前选中工具栏哪个位置(1-9) */ + this.index = -1; + this.text = ''; + } + + drawContent() { + const ctx = this.createCanvas(); + core.setTextAlign(ctx, 'left'); + core.fillText(ctx, '常规', 40, 175, ' #FFE4B5', '16px Verdana'); + core.fillText(ctx, '数字', 40, 205, ' #FFE4B5', '16px Verdana'); + core.fillText(ctx, '录像', 40, 235, ' #FFE4B5', '16px Verdana'); + core.fillText(ctx, '可选按钮', 40, 265, ' #FFE4B5', '16px Verdana'); + if (this.text && this.text.length > 0) { + core.ui.drawTextContent(ctx, this.text, { + left: 30, + top: 78, + bold: false, + color: "white", + align: "left", + fontSize: 14, + maxWidth: 350 + }); + } + super.drawContent(); + } + + setToolBarConfig() { + core.setToolBarConfig(this.type, this.index, this.selectedTool); + this.drawContent(); + } + } + + function toolBarConfigFactory() { + // 名字不能叫toolBar 画布toolBar被系统占了 + const toolBarMenu = new ToolBarConfigPage(); + const changeToolBarBtn = new RoundBtn(320, 158, 42, 24, '执行', -1); + changeToolBarBtn.ondown = function () { + core.setToolBarConfig(this.menu.type, this.menu.index, this.menu.selectedTool); + this.menu.drawContent(); + }.bind(changeToolBarBtn); + toolBarMenu.registerBtns([ + ['1,1', new ToolBarBtn(80, 158, 24, 'normal', 9, '常规模式下显示在工具栏中的图标。')], + ['1,2', changeToolBarBtn, () => toolBarMenu.setToolBarConfig()], + ['2,1', new ToolBarBtn(80, 188, 24, 'num', 9, '数字模式下显示在工具栏中的图标。')], + ['3,1', new ToolBarBtn(80, 218, 24, 'replay', 9, '录像模式下显示在工具栏中的图标。')], + ['5,1', new ToolBtn(40, 275, 24, 24, 'book', '打开怪物手册')], + ['5,2', new ToolBtn(70, 275, 24, 24, 'fly', '进行楼层传送')], + ['5,3', new ToolBtn(100, 275, 24, 24, 'toolbox', '打开物品背包')], + ['5,4', new ToolBtn(130, 275, 24, 24, 'equipbox', '打开装备背包')], + ['5,5', new ToolBtn(160, 275, 24, 24, 'keyboard', '打开虚拟键盘')], + ['5,6', new ToolBtn(190, 275, 24, 24, 'shop', '打开快捷商店')], + ['5,7', new ToolBtn(220, 275, 24, 24, 'save', '存档')], + ['5,8', new ToolBtn(250, 275, 24, 24, 'load', '读档')], + ['5,9', new ToolBtn(280, 275, 24, 24, 'settings', '打开系统设置')], + ['5,10', new ToolBtn(310, 275, 24, 24, 'rollback', '读取自动存档')], + ['5,11', new ToolBtn(340, 275, 24, 24, 'undoRollback', '取消读取自动存档')], + ['6,1', new ToolBtn(40, 305, 24, 24, 'btn1', '数字键1')], + ['6,2', new ToolBtn(70, 305, 24, 24, 'btn2', '数字键2')], + ['6,3', new ToolBtn(100, 305, 24, 24, 'btn3', '数字键3')], + ['6,4', new ToolBtn(130, 305, 24, 24, 'btn4', '数字键4')], + ['6,5', new ToolBtn(160, 305, 24, 24, 'btn5', '数字键5')], + ['6,6', new ToolBtn(190, 305, 24, 24, 'btn6', '数字键6')], + ['6,7', new ToolBtn(220, 305, 24, 24, 'btn7', '数字键7')], + ['6,8', new ToolBtn(250, 305, 24, 24, 'btn8', '数字键8')], + ['6,9', new ToolBtn(280, 305, 24, 24, 'btn9', '数字键9')], + ['6,10', new ToolBtn(310, 305, 24, 24, 'btnAlt', '开关Alt模式(需要配合数字键使用)')], + ['7,1', new ToolBtn(40, 335, 24, 24, 'play', '播放/暂停录像')], + ['7,2', new ToolBtn(70, 335, 24, 24, 'stop', '停止播放录像')], + ['7,3', new ToolBtn(100, 335, 24, 24, 'rewind', '录像模式下回退')], + ['7,4', new ToolBtn(130, 335, 24, 24, 'speedDown', '减速录像(最低0.2倍)')], + ['7,5', new ToolBtn(160, 335, 24, 24, 'speedUp', '加速录像(最高24倍)')], + ['7,6', new ToolBtn(190, 335, 24, 24, 'single', '单步播放录像')], + ['7,7', new ToolBtn(220, 335, 24, 24, 'view', '浏览地图')], + ['7,8', new ToolBtn(250, 335, 24, 24, 'delete', '删除已有图标')], + ]); + return toolBarMenu; + } + // #endregion + // #region 控制台界面 + /** @type {{[x:string]:Setting}} */ + const consoleSetting = { + debug_wallHacking: { + getName: () => ' 穿墙:' + (core.hasFlag('debug_wallHacking') ? '开' : '关'), + effect: () => { + core.setFlag('debug', true); + invertFlag('debug_wallHacking'); + }, + text: '开启时将始终穿墙并无视各种事件,无论是否按下Ctrl。', + replay: false, + }, + debug_statusName: { + getName: () => core.getFlag('debug_statusName', '??'), + /** @this {ConsoleMenu} */ + effect: function () { + const dictionary = { + '体力': 'hp', + '血量': 'hp', + '生命': 'hp', + '血': 'hp', + '体力上限': 'hpmax', + '血量上限': 'hpmax', + '生命上限': 'hpmax', + '血限': 'hpmax', + '攻击': 'atk', + '攻': 'atk', + '防御': 'def', + '防': 'def', + '魔防': 'mdef', + '护盾': 'mdef', + 'mf': 'mdef', + '金币': 'money', + '金钱': 'money', + '钱': 'money', + '经验': 'exp', + '魔力': 'mana', + '魔': 'mana', + '蓝': 'mana', + }; + core.utils.myprompt('输入要修改的属性名称', '', (value) => { + const heroStatus = core.status.hero; + if (dictionary.hasOwnProperty(value)) { + value = dictionary[value]; + } + if (heroStatus && heroStatus.hasOwnProperty(value) && ['hp', 'hpmax', 'atk', 'def', 'mdef', 'money', 'exp', 'mana', 'manamax'].includes(value)) { + core.setFlag('debug_statusName', value); + this.menu.drawContent(); + } else { + core.drawFailTip('错误:不合法的名称!'); + } + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_statusValue: { + getName: () => { + let value = core.getFlag('debug_statusValue', '??'); + if (typeof value === 'number') return core.formatBigNumber(value, 5); + else return value; + }, + /** @this {ConsoleMenu} */ + effect: function () { + core.utils.myprompt('输入要修改到的值', null, (input) => { + const value = parseInt(input); + if (!Number.isNaN(value)) { + core.setFlag('debug_statusValue', value); + this.menu.drawContent(); + } else { + core.drawFailTip('错误:不合法的值!'); + } + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_setStatus: { + getName: () => '', + /** @this {ConsoleMenu} */ + effect: function () { + const name = core.getFlag('debug_statusName'), + value = core.getFlag('debug_statusValue'); + if (!(name && core.status.hero && core.status.hero.hasOwnProperty(name))) { + core.drawFailTip('错误:不合法的名称!'); + return; + } + if (!Number.isInteger(value)) { + core.drawFailTip('错误:不合法的值!'); + return; + } + core.setFlag('debug', true); + core.setStatus(name, value); + core.updateStatusBar(); + core.drawSuccessTip('设置成功!'); + }, + text: '将角色状态设为相应值。', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); + core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); + core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); + }, + }, + debug_itemName: { + getName: () => core.getFlag('debug_itemName', '??'), + /** @this {ConsoleMenu} */ + effect: function () { + core.utils.myprompt('输入要修改的物品名称', null, (value) => { + const itemInfo = core.material.items; + if (itemInfo) { + const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value); + if (aimItem) { + core.setFlag('debug_itemName', aimItem.id); + this.menu.drawContent(); + return; + } + } + core.drawFailTip('错误:不合法的名称!'); + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_itemValue: { + getName: () => core.getFlag('debug_itemValue', '??'), + /** @this {ConsoleMenu} */ + effect: function () { + core.setFlag('debug', true); + core.utils.myprompt('输入要修改到的值', null, (input) => { + const value = parseInt(input); + if (!Number.isNaN(value)) { + core.setFlag('debug_itemValue', value); + this.menu.drawContent(); + } else { + core.drawFailTip('错误:不合法的值!'); + } + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_setItem: { + getName: () => '', + /** @this {ConsoleMenu} */ + effect: function () { + const name = core.getFlag('debug_itemName'), + value = core.getFlag('debug_itemValue'); + const itemInfo = core.material.items; + + if (name && itemInfo) { + let itemExist = Object.values(itemInfo).some((item) => item.id === name); + if (!itemExist) { + core.drawFailTip('错误:不合法的名称!'); + return; + } + } else { + core.drawFailTip('错误:不合法的名称!'); + return; + } + + if (!Number.isInteger(value)) { + core.drawFailTip('错误:不合法的值!'); + return; + } + + core.setFlag('debug', true); + core.setItem(name, value); + core.updateStatusBar(); + core.drawSuccessTip('设置成功!'); + }, + text: '将道具数设为相应值。', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); + core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); + core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); + }, + }, + debug_flagName: { + getName: () => core.getFlag('debug_flagName', '??'), + /** @this {ConsoleMenu} */ + effect: function () { + core.setFlag('debug', true); + core.utils.myprompt('输入要修改的变量名。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => { + if (!value.startsWith('debug')) { + core.setFlag('debug_flagName', value); + this.menu.drawContent(); + } else { + core.drawFailTip('错误:不合法的名称!'); + } + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_flagValue: { + getName: () => core.getFlag('debug_flagValue', '??'), + /** @this {ConsoleMenu} */ + effect: function () { + core.setFlag('debug', true); + core.utils.myprompt('输入要修改到的值。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => { + let newValue; + try { + newValue = JSON.parse(value.trim()); + } catch { + core.drawFailTip('错误:不合法的值,无法解析!'); + return; + } + core.setFlag('debug_flagValue', newValue); + this.menu.drawContent(); + }, () => { }); + }, + text: '', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090'); + }, + }, + debug_setFlag: { + getName: () => '', + /** @this {ConsoleMenu} */ + effect: function () { + const name = core.getFlag('debug_flagName'), + value = core.getFlag('debug_flagValue'); + if (!name) { + core.drawFailTip('错误:不合法的变量名称!'); + return; + } + core.setFlag('debug', true); + core.setFlag(name, value); + core.updateStatusBar(); + core.drawSuccessTip('设置成功!'); + }, + text: '将变量设为相应值。', + replay: false, + /** @this {SettingButton} */ + draw: function (ctx) { + core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3'); + core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888'); + core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana'); + }, + }, + }; + + class ConsoleMenu extends SettingOnePage { + constructor() { + super('console', consoleSetting); + this.menu = this; + } + + drawContent() { + const ctx = this.createCanvas(); + const consoleWarnText = + "本页面的功能仅供调试用。使用后相应存档将变红,录像不能通过,且无法提交。请读档到普通存档后正常游玩方可提交。"; + core.setTextAlign(ctx, 'left'); + core.setTextBaseline(ctx, 'alphabetic'); + core.fillText(ctx, "本页面的功能仅供调试用。使用后相应存档将变红,录像", 30, 170, " #FFC0CB", '14px Verdana'); + core.fillText(ctx, "不能通过,且无法提交。请读档到普通存档后正常游玩方", 30, 190, " #FFC0CB", '14px Verdana'); + core.fillText(ctx, "可提交。", 30, 210, " #FFC0CB", '14px Verdana'); + core.fillText(ctx, "属性", 45, 264, 'white', '16px Verdana'); + core.fillText(ctx, "设为", 170, 264, 'white', '16px Verdana'); + core.fillText(ctx, "物品", 45, 290, 'white', '16px Verdana'); + core.fillText(ctx, "数量设为", 170, 290, 'white', '16px Verdana'); + core.fillText(ctx, "变量", 45, 316, 'white', '16px Verdana'); + core.fillText(ctx, "设为", 170, 316, 'white', '16px Verdana'); + super.drawContent(ctx); + } + } + + function consoleMenuFactory() { + const consoleMenu = new ConsoleMenu(); + consoleMenu.registerSettingBtns([ + ['1,1', 'debug_wallHacking', new SettingButton(40, 220, 150, 25)], + ['1,2', 'debug_statusName', new SettingButton(80, 250, 80, 20)], + ['2,2', 'debug_statusValue', new SettingButton(210, 250, 80, 20)], + ['3,2', 'debug_setStatus', new SettingButton(340, 250, 40, 20)], + ['1,3', 'debug_itemName', new SettingButton(80, 276, 80, 20)], + ['2,3', 'debug_itemValue', new SettingButton(240, 276, 80, 20)], + ['3,3', 'debug_setItem', new SettingButton(340, 276, 40, 20)], + ['1,4', 'debug_flagName', new SettingButton(80, 302, 80, 20)], + ['2,4', 'debug_flagValue', new SettingButton(210, 302, 80, 20)], + ['3,4', 'debug_setFlag', new SettingButton(340, 302, 40, 20)], + ]); + return consoleMenu; + } + // #endregion + // #region 敌人信息显示调节界面 + const infoNameMap = { + 'damage': '伤害', 'critical': '临界', 'hp': core.control.getStatusLabel('hp'), + 'atk': core.control.getStatusLabel('atk'), 'def': core.control.getStatusLabel('def'), + 'money': core.control.getStatusLabel('money'), 'exp': core.control.getStatusLabel('exp'), + 'criticalDamage': '临界减伤', 'defDamage': '1防减伤', 'special': '特殊属性', + 'notBomb': '不可炸', 'vampireDiff': '吸血额外受伤' + } + + class DisplayInfoBtn extends RoundBtn { + constructor(x, y, w, h, pos, index) { + super(x, y, w, h); + this.pos = pos; + this.index = index; + } + + draw() { + super.draw(); + const isEmpty = !this.infoName; + const ctx = this.ctx; + const { x, y, w, h } = this; + const [x0, y0, r, offset] = [x + w + 15, y + h / 2, h / 2 - 2, 2]; + core.fillCircle(ctx, x0, y0, r, isEmpty ? 'lime' : 'red'); + core.drawLine(ctx, x0 - r + offset, y0, x0 + r - offset, y0, 'white', 2); + if (isEmpty) core.drawLine(ctx, x0, y0 - r + offset, x0, y0 + r - offset, 'white', 2); + } + + inRange(px, py) { // 实际的点击判定区是右边的圆形 + const { x, y, w, h } = this; + return px >= x + w + 15 - h / 2 && px <= x + w + 15 + h / 2 && py >= y && py <= y + h; + } + } + + class SpecialIconBtn extends RoundBtn { } + + class AdvanceDisplayMenu extends MenuBase { + constructor() { + super('advanceDisplay', ['ondown']); + this.selectedBtn = null; + this.getData(); + this.getAllSpecials(); + this.getSpecialIconData(); + this.specialIconPage = 0; + } + + drawContent() { + const ctx = this.createCanvas(); + core.setTextAlign(ctx, 'center'); + core.setTextBaseline(ctx, 'alphabetic'); + core.fillRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 320, 5, "rgba(50,50,50,1)"); + core.fillText(ctx, "设定敌人左下角显示的数据", 110, 90, 'white', '14px Verdana'); + core.fillText(ctx, "设定敌人右上角显示的数据", 110, 190, 'white', '14px Verdana'); + core.fillText(ctx, "设定特殊属性的代表字符", 110, 290, 'white', '14px Verdana'); + this.getData(); + this.setInfoName(); + this.getSpecialIconData(); + super.drawContent(ctx); + } + + clear() { + super.clear(); + const settingMenu = core.plugin.settingMenu; + if (settingMenu) { + settingMenu.beginListen(); + settingMenu.pageList[settingMenu.currPage].beginListen(); + } + } + + getData() { + const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} }; + this.data = core.getLocalStorage('displayData', defaultData); + } + + setData(pos, index, infoName) { + this.data[pos][index] = infoName; + core.setLocalStorage('displayData', this.data); + this.setInfoName(); + } + + setInfoName() { + this.btnMap.forEach(btn => { + if (!(btn instanceof DisplayInfoBtn)) return; + const pos = btn.pos; + const index = btn.index; + const infoName = this.data[pos][index]; + if (infoName) { + btn.infoName = infoName; + const name = infoNameMap[infoName]; + btn.text = name; + if (name.length >= 5) { + if (!btn.config) btn.config = {}; + btn.config.font = "12px Verdana"; + } + } + else { + btn.infoName = null; + btn.text = ''; + } + }); + } + + getAllSpecials() { // allSpecials是所有用到了的特殊属性的数组 + let allSpecialsSet = new Set(); + Object.values(core.material.enemys).forEach(enemy => { + const special = core.utils.parseSpecial(enemy.special); + allSpecialsSet = new Set([...allSpecialsSet, ...special]); + }) + this.allSpecials = [...allSpecialsSet].sort((a, b) => a - b); + } + + getSpecialIconData() { + const specialData = core.getLocalStorage('specialIconData', {}); + this.specialIconData = specialData; + } + + getCurrSpecialIconList() { + return this.allSpecials.slice(this.specialIconPage * 6, + this.specialIconPage * 6 + 6); + } + + /** 更新每个按钮对应的文本,在翻页和修改内容时需要手动调用 */ + setSpecialIconBtnText() { + const specialIconList = this.getCurrSpecialIconList(); + for (let i = 0; i <= 5; i++) { + const key = 's' + (i + 1); + const btn = this.btnMap.get(key); + const index = this.specialIconPage * 6 + i; + if (index >= this.allSpecials.length) { + btn.text = ''; + } + else { + const specialNum = this.allSpecials[index]; + const specialIndexMap = core.enemys.getSpecialIndexMap(); + const [key, name] = specialIndexMap[specialNum]; + const icon = this.specialIconData[specialNum] || "无"; + btn.text = `${key}:${name} ${icon}`; + } + } + } + + setSpecialIconData(index) { + const specialIconList = this.getCurrSpecialIconList(); + const specialNum = specialIconList[index]; + if (!specialNum) return; + core.utils.myprompt('输入该特殊属性的代表字符', null, (value) => { + if (value.length > 1) value = value[0]; // 最多保留一位字符 + this.specialIconData[specialNum] = value; + core.setLocalStorage('specialIconData', this.specialIconData); + this.setSpecialIconBtnText(); + this.drawContent(); + }); + } + + pageUp() { + if (this.specialIconPage * 6 + 6 < this.allSpecials.length) { + this.specialIconPage++; + this.setSpecialIconBtnText(); + this.drawContent(); + } + } + + pageDown() { + if (this.specialIconPage > 0) { + this.specialIconPage--; + this.setSpecialIconBtnText(); + this.drawContent(); + } + } + } + + function advanceDisplayFactory() { + const advanceDisplayMenu = new AdvanceDisplayMenu(); + const exitBtn = new ExitBtn(370, 80, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 }) + advanceDisplayMenu.registerBtn('exitBtn', exitBtn, () => { + advanceDisplayMenu.clear(); + const settingMenu = core.plugin.settingMenu; + settingMenu.btnMap.forEach(btn => { btn.disable = false }); + settingMenu.drawContent(); + }); + const btn1 = new DisplayInfoBtn(50, 100, 75, 20, 'leftdown', 1), + btn2 = new DisplayInfoBtn(50, 125, 75, 20, 'leftdown', 2), + btn3 = new DisplayInfoBtn(50, 150, 75, 20, 'leftdown', 3), + btn4 = new DisplayInfoBtn(50, 200, 75, 20, 'rightup', 1), + btn5 = new DisplayInfoBtn(50, 225, 75, 20, 'rightup', 2), + btn6 = new DisplayInfoBtn(50, 250, 75, 20, 'rightup', 3); + /** 一行显示几个选项 */ + const ROW_INFONAME = 6; + const infoNameList = Object.keys(infoNameMap); + const l = infoNameList.length; + + const setNewInfo = function (infoName) { + return function () { + const btn = advanceDisplayMenu.selectedBtn; + if (!btn) return; + advanceDisplayMenu.setData(btn.pos, btn.index, infoName); + advanceDisplayMenu.btnMap.forEach((btn, key) => { + if (btn.key.startsWith("temp")) btn.disable = true; + }); + advanceDisplayMenu.selectedBtn = null; + advanceDisplayMenu.drawContent(); + core.control.updateStatusBar(); // 手动刷新一下地图显伤 + } + } + for (let i = 0; i < l; i++) { + const name = infoNameMap[infoNameList[i]]; + const config = { + fillStyle: 'Azure', + font: name.length >= 5 ? "12px Verdana" : "14px Verdana", + } + const x = (i >= ROW_INFONAME) ? 300 : 200; + const y = 100 + 25 * ((i >= ROW_INFONAME) ? (i - ROW_INFONAME) : i); + const tempBtn = new RoundBtn(x, y, 80, 20, name, config); + tempBtn.disable = true; + advanceDisplayMenu.registerBtn('temp' + i, tempBtn, setNewInfo(infoNameList[i])); + } + + const change = function (btn) { + return function () { + if (btn.infoName) { + advanceDisplayMenu.setData(btn.pos, btn.index, null); + } + else { + if (advanceDisplayMenu.selectedBtn && advanceDisplayMenu.selectedBtn === btn) { + // 点击左边刚点过的按钮会收起展开菜单 + advanceDisplayMenu.btnMap.forEach((btn, key) => { + if (btn.key.startsWith("temp")) btn.disable = true; + }); + advanceDisplayMenu.selectedBtn = null; + advanceDisplayMenu.drawContent(); + } else { + advanceDisplayMenu.selectedBtn = btn; + advanceDisplayMenu.btnMap.forEach((btn, key) => { + if (btn.key.startsWith("temp")) btn.disable = false; + }); + } + } + advanceDisplayMenu.drawContent(); + } + } + advanceDisplayMenu.registerBtns([ + ['1', btn1, change(btn1)], ['2', btn2, change(btn2)], ['3', btn3, change(btn3)], + ['4', btn4, change(btn4)], ['5', btn5, change(btn5)], ['6', btn6, change(btn6)], + ]); + const config = { "font": "12px Verdana" }; + advanceDisplayMenu.registerBtn('s1', new SpecialIconBtn(50, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 0)); + advanceDisplayMenu.registerBtn('s2', new SpecialIconBtn(150, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 1)); + advanceDisplayMenu.registerBtn('s3', new SpecialIconBtn(250, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 2)); + advanceDisplayMenu.registerBtn('s4', new SpecialIconBtn(50, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 3)); + advanceDisplayMenu.registerBtn('s5', new SpecialIconBtn(150, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 4)); + advanceDisplayMenu.registerBtn('s6', new SpecialIconBtn(250, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 5)); + advanceDisplayMenu.registerBtn('pageDown', new ArrowBtn(50, 360, 16, 16, 'left'), + advanceDisplayMenu.pageDown.bind(advanceDisplayMenu)); + advanceDisplayMenu.registerBtn('pageUp', new ArrowBtn(320, 360, 16, 16, 'right'), + advanceDisplayMenu.pageUp.bind(advanceDisplayMenu)); + advanceDisplayMenu.setSpecialIconBtnText(); + return advanceDisplayMenu; + } + // #endregion + + class PageChangeBtn extends RoundBtn { + constructor(x, y, w, h, text) { + super(x, y, w, h, text); + /** @type {SettingBack} */ // @ts-ignore + this.menu; + this.ondown = () => this.menu.changePage(this.key); + } + } + + class SettingBack extends Pagination { + constructor(pageList, currPage, name) { + super(pageList, currPage, name, ['ondown', 'keyDown']); + } + + changePage(index) { + this.btnMap.forEach((btn, key) => { + btn.status = (key === index) ? 'selected' : 'none'; + }); + super.changePage(index); + this.drawContent(); + } + + /** @override */ + keyDownEvent(keyCode) { + if (keyCode === KeyCodeEnum.PageDown) this.pageDown(); + else if (keyCode === KeyCodeEnum.PageUp) this.pageUp(); + else if (keyCode === KeyCodeEnum.Esc) this.quit(); + } + + quit() { + this.clear(); + setTimeout(core.unlockControl, 0); // 消抖,防止点击关闭按钮的一瞬间触发瞬移。 + } + + drawSettingBackGround(ctx) { + core.strokeRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "white", 2); + core.fillRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "gray"); + + // 绘制设置说明的文本框 + core.strokeRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 70, 3, "white"); + core.fillRoundRect(ctx, 21, 71, core.__PIXELS__ - 42, 68, 3, " #555555"); + + // 绘制设置的框体 + core.strokeRoundRect(ctx, 20, 150, core.__PIXELS__ - 40, 256, 3, "white"); + core.fillRoundRect(ctx, 21, 151, core.__PIXELS__ - 42, 254, 3, " #999999"); + + core.setTextAlign(ctx, 'center'); + core.ui.fillText(ctx, "设置", core.__PIXELS__ / 2, 25, 'white', '20px Verdana'); + } + + drawContent() { + const ctx = core.createCanvas(this.name, this.x, this.y, this.w, this.h, 136); + this.drawSettingBackGround(ctx); + super.drawContent(); + // this.initOnePage(this.currPage); + } + } + + const allSettings = { + ...gamePlaySetting, + ...gameViewSetting, + ...keySetting, + ...consoleSetting, + }; + // 注册点击SettingButton的行为cSet, 只有replay为真时计入录像 + core.registerReplayAction('cSet', (action) => { + const strArr = action.split(':'); + if (strArr[0] !== 'cSet') return false; + const currSetting = allSettings[strArr[1]]; //@todo bugfix + if (!currSetting || !currSetting.replay || strArr[1].startsWith('debug')) return false; + + let params = strArr.slice(2); + + if (params.length > 0) { + currSetting.effect.apply(currSetting, params); + } else { + currSetting.effect.call(currSetting); + } + + core.status.route.push(action); + core.replay(); + return true; + }); + + this.openSetting = function () { + if (core.isReplaying()) return; + core.lockControl(); + const ctx = 'setting'; + + const gamePlayMenu = gamePlayFactory(); + const gameViewMenu = gameViewFactory(); + const keyMenu = keyMenuFactory(); + const toolBarMenu = toolBarConfigFactory(); + const consoleMenu = consoleMenuFactory(); + + // 在此处添加新的菜单页面 + const settingMenu = new SettingBack([gamePlayMenu, gameViewMenu, keyMenu, toolBarMenu, consoleMenu], 0, ctx); + + // 主页面的按钮列表 + const gamePlayBtn = new PageChangeBtn(32, 40, 46, 24, '功能'), + gameViewBtn = new PageChangeBtn(92, 40, 46, 24, '音画'), + keyBtn = new PageChangeBtn(152, 40, 46, 24, '按键'), + toolBarBtn = new PageChangeBtn(212, 40, 66, 24, '工具栏'), + consoleBtn = new PageChangeBtn(292, 40, 66, 24, '控制台'); + const quit = new TextButton(360, 10, 45, 25, '[退出]'); + + settingMenu.registerBtns([ + [0, gamePlayBtn], + [1, gameViewBtn], + [2, keyBtn], + [3, toolBarBtn], + [4, consoleBtn], + ['quit', quit, () => settingMenu.quit()] + ]); + + // 放缩时重绘整个大menu + core.plugin.settingMenu = settingMenu; + // 设置初始时选中的按键为第一个按键 + gamePlayBtn.status = 'selected'; + + settingMenu.init(); + settingMenu.changePage(0); + } + // @todo 新版存档界面 + }, + "opusAdaptation": function () { + // 将__enable置为false将关闭插件 + let __enable = true; + if (!__enable || main.mode === "editor") return; + const { OggOpusDecoderWebWorker } = window["ogg-opus-decoder"]; + const { OggVorbisDecoderWebWorker } = window["ogg-vorbis-decoder"]; + const { CodecParser } = window.CodecParser; + const { Transition, linear } = core.plugin.animate; + + const audio = new Audio(); + const AudioStatus = { + Playing: 0, + Pausing: 1, + Paused: 2, + Stoping: 3, + Stoped: 4, + }; + const supportMap = new Map(); + const AudioType = { + Mp3: "audio/mpeg", + Wav: 'audio/wav; codecs="1"', + Flac: "audio/flac", + Opus: 'audio/ogg; codecs="opus"', + Ogg: 'audio/ogg; codecs="vorbis"', + Aac: "audio/aac", + }; + /** + * 检查一种音频类型是否能被播放 + * @param type 音频类型 AudioType + */ + function isAudioSupport(type) { + if (supportMap.has(type)) return supportMap.get(type); + else { + const support = audio.canPlayType(type); + const canPlay = support === "maybe" || support === "probably"; + supportMap.set(type, canPlay); + return canPlay; + } + } + + const typeMap = new Map([ + ["ogg", AudioType.Ogg], + ["mp3", AudioType.Mp3], + ["wav", AudioType.Wav], + ["flac", AudioType.Flac], + ["opus", AudioType.Opus], + ["aac", AudioType.Aac], + ]); + + /** + * 根据文件名拓展猜测其类型 + * @param file 文件名 string + */ + function guessTypeByExt(file) { + const ext = /\.[a-zA-Z\d]+$/.exec(file); + if (!ext?.[0]) return ""; + const type = ext[0].slice(1); + return typeMap.get(type.toLocaleLowerCase()) ?? ""; + } + + isAudioSupport(AudioType.Ogg); + isAudioSupport(AudioType.Mp3); + isAudioSupport(AudioType.Wav); + isAudioSupport(AudioType.Flac); + isAudioSupport(AudioType.Opus); + isAudioSupport(AudioType.Aac); + + function isNil(value) { + return value === void 0 || value === null; + } + + function sleep(time) { + return new Promise((res) => setTimeout(res, time)); + } + class AudioEffect { + constructor(ac) { } + /** + * 连接至其他效果器 + * @param target 目标输入 IAudioInput + * @param output 当前效果器输出通道 Number + * @param input 目标效果器的输入通道 Number + */ + connect(target, output, input) { + this.output.connect(target.input, output, input); + } + + /** + * 与其他效果器取消连接 + * @param target 目标输入 IAudioInput + * @param output 当前效果器输出通道 Number + * @param input 目标效果器的输入通道 Number + */ + disconnect(target, output, input) { + if (!target) { + if (!isNil(output)) { + this.output.disconnect(output); + } else { + this.output.disconnect(); + } + } else { + if (!isNil(output)) { + if (!isNil(input)) { + this.output.disconnect(target.input, output, input); + } else { + this.output.disconnect(target.input, output); + } + } else { + this.output.disconnect(target.input); + } + } + } + } + + class StereoEffect extends AudioEffect { + constructor(ac) { + super(ac); + const panner = ac.createPanner(); + this.input = panner; + this.output = panner; + } + + /** + * 设置音频朝向,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 朝向x坐标 Number + * @param y 朝向y坐标 Number + * @param z 朝向z坐标 Number + */ + setOrientation(x, y, z) { + this.output.orientationX.value = x; + this.output.orientationY.value = y; + this.output.orientationZ.value = z; + } + /** + * 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 位置x坐标 Number + * @param y 位置y坐标 Number + * @param z 位置z坐标 Number + */ + setPosition(x, y, z) { + this.output.positionX.value = x; + this.output.positionY.value = y; + this.output.positionZ.value = z; + } + end() { } + + start() { } + } + class VolumeEffect extends AudioEffect { + constructor(ac) { + super(ac); + const gain = ac.createGain(); + this.input = gain; + this.output = gain; + } + + /** + * 设置音量大小 + * @param volume 音量大小 Number + */ + setVolume(volume) { + this.output.gain.value = volume; + } + + /** + * 获取音量大小 Number + */ + getVolume() { + return this.output.gain.value; + } + + end() { } + + start() { } + } + class ChannelVolumeEffect extends AudioEffect { + /** 所有的音量控制节点 */ + + constructor(ac) { + super(ac); + /** 所有的音量控制节点 */ + this.gain = []; + const splitter = ac.createChannelSplitter(); + const merger = ac.createChannelMerger(); + this.output = merger; + this.input = splitter; + for (let i = 0; i < 6; i++) { + const gain = ac.createGain(); + splitter.connect(gain, i); + gain.connect(merger, 0, i); + this.gain.push(gain); + } + } + + /** + * 设置某个声道的音量大小 + * @param channel 要设置的声道,可填0-5 Number + * @param volume 这个声道的音量大小 Number + */ + setVolume(channel, volume) { + if (!this.gain[channel]) return; + this.gain[channel].gain.value = volume; + } + + /** + * 获取某个声道的音量大小,可填0-5 + * @param channel 要获取的声道 Number + */ + getVolume(channel) { + if (!this.gain[channel]) return 0; + return this.gain[channel].gain.value; + } + + end() { } + + start() { } + } + class DelayEffect extends AudioEffect { + constructor(ac) { + super(ac); + + const delay = ac.createDelay(); + this.input = delay; + this.output = delay; + } + + /** + * 设置延迟时长 + * @param delay 延迟时长,单位秒 Number + */ + setDelay(delay) { + this.output.delayTime.value = delay; + } + + /** + * 获取延迟时长 + */ + getDelay() { + return this.output.delayTime.value; + } + + end() { } + + start() { } + } + class EchoEffect extends AudioEffect { + constructor(ac) { + super(ac); + /** 当前增益 */ + this.gain = 0.5; + /** 是否正在播放 */ + this.playing = false; + const delay = ac.createDelay(); + const gain = ac.createGain(); + gain.gain.value = 0.5; + delay.delayTime.value = 0.05; + delay.connect(gain); + gain.connect(delay); + /** 延迟节点 */ + this.delay = delay; + /** 反馈增益节点 */ + this.gainNode = gain; + + this.input = gain; + this.output = gain; + } + + /** + * 设置回声反馈增益大小 + * @param gain 增益大小,范围 0-1,大于等于1的视为0.5,小于0的视为0 Number + */ + setFeedbackGain(gain) { + const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain; + this.gain = resolved; + if (this.playing) this.gainNode.gain.value = resolved; + } + + /** + * 设置回声间隔时长 + * @param delay 回声时长,范围 0.01-Infinity,小于0.01的视为0.01 Number + */ + setEchoDelay(delay) { + const resolved = delay < 0.01 ? 0.01 : delay; + this.delay.delayTime.value = resolved; + } + + /** + * 获取反馈节点增益 + */ + getFeedbackGain() { + return this.gain; + } + + /** + * 获取回声间隔时长 + */ + getEchoDelay() { + return this.delay.delayTime.value; + } + + end() { + this.playing = false; + const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10; + sleep(this.delay.delayTime.value * echoTime).then(() => { + if (!this.playing) this.gainNode.gain.value = 0; + }); + } + + start() { + this.playing = true; + this.gainNode.gain.value = this.gain; + } + } + + class StreamLoader { + constructor(url) { + /** 传输目标 Set */ + this.target = new Set(); + this.loading = false; + } + + /** + * 将加载流传递给字节流读取对象 + * @param reader 字节流读取对象 IStreamReader + */ + pipe(reader) { + if (this.loading) { + console.warn( + "Cannot pipe new StreamReader object when stream is loading." + ); + return; + } + this.target.add(reader); + reader.piped(this); + return this; + } + + async start() { + if (this.loading) return; + this.loading = true; + const response = await window.fetch(this.url); + const stream = response.body; + if (!stream) { + console.error("Cannot get reader when fetching '" + this.url + "'."); + return; + } + // 获取读取器 + this.stream = stream; + const reader = response.body?.getReader(); + const targets = [...this.target]; + + await Promise.all(targets.map((v) => v.start(stream, this, response))); + if (reader && reader.read) { + // 开始流传输 + while (true) { + const { value, done } = await reader.read(); + await Promise.all( + targets.map((v) => v.pump(value, done, response)) + ); + if (done) break; + } + } else { + // 如果不支持流传输 + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + await Promise.all(targets.map((v) => v.pump(data, true, response))); + } + + this.loading = false; + targets.forEach((v) => v.end(true)); + } + + cancel(reason) { + if (!this.stream) return; + this.stream.cancel(reason); + this.loading = false; + this.target.forEach((v) => v.end(false, reason)); + } + } + + /** @type {[string, number[]][]} */ + const fileSignatures = [ + [AudioType.Mp3, [0x49, 0x44, 0x33]], + [AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]], + [AudioType.Wav, [0x52, 0x49, 0x46, 0x46]], + [AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]], + [AudioType.Aac, [0xff, 0xf1]], + [AudioType.Aac, [0xff, 0xf9]], + ]; + /** @type {[string, number[]][]} */ + const oggHeaders = [ + [AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]], + ]; + + function checkAudioType(data) { + let audioType = ""; + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + for (const [type, value] of fileSignatures) { + if (value.every((v, i) => toCheck[i] === v)) { + audioType = type; + break; + } + } + if (audioType === AudioType.Ogg) { + // 如果是ogg的话,进一步判断是不是opus + for (const [key, value] of oggHeaders) { + const has = toCheck.some((_, i) => { + return value.every((v, ii) => toCheck[i + ii] === v); + }); + if (has) { + audioType = key; + break; + } + } + } + + return audioType; + } + class AudioDecoder { + /** + * 注册一个解码器 + * @param type 要注册的解码器允许解码的类型 + * @param decoder 解码器对象 + */ + static registerDecoder(type, decoder) { + if (!this.decoderMap) this.decoderMap = new Map(); + if (this.decoderMap.has(type)) { + console.warn( + "Audio stream decoder for audio type '" + + type + + "' has already existed." + ); + return; + } + + this.decoderMap.set(type, decoder); + } + + /** + * 解码音频数据 + * @param data 音频文件数据 + * @param player AudioPlayer实例 + */ + static async decodeAudioData(data, player) { + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + const type = checkAudioType(data); + if (type === "") { + console.error( + "Unknown audio type. Header: '" + + [...toCheck] + .map((v) => v.toString().padStart(2, "0")) + .join(" ") + .toUpperCase() + + "'" + ); + return null; + } + if (isAudioSupport(type)) { + if (data.buffer instanceof ArrayBuffer) { + return player.ac.decodeAudioData(data.buffer); + } else { + return null; + } + } else { + const Decoder = this.decoderMap.get(type); + if (!Decoder) { + return null; + } else { + const decoder = new Decoder(); + await decoder.create(); + const decodedData = await decoder.decode(data); + if (!decodedData) return null; + const buffer = player.ac.createBuffer( + decodedData.channelData.length, + decodedData.channelData[0].length, + decodedData.sampleRate + ); + decodedData.channelData.forEach((v, i) => { + buffer.copyToChannel(v, i); + }); + decoder.destroy(); + return buffer; + } + } + } + } + + class VorbisDecoder { + /** + * 创建音频解码器 + */ + async create() { + this.decoder = new OggVorbisDecoderWebWorker(); + await this.decoder.ready; + } + /** + * 摧毁这个解码器 + */ + destroy() { + this.decoder?.free(); + } + /** + * 解码流数据 + * @param data 流数据 + */ + + async decode(data) { + return this.decoder?.decode(data); + } + /** + * 解码整个文件 + * @param data 文件数据 + */ + async decodeAll(data) { + return this.decoder?.decodeFile(data); + } + /** + * 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用 + */ + async flush() { + return this.decoder?.flush(); + } + } + + class OpusDecoder { + /** + * 创建音频解码器 + */ + async create() { + this.decoder = new OggOpusDecoderWebWorker(); + await this.decoder.ready; + } + /** + * 摧毁这个解码器 + */ + destroy() { + this.decoder?.free(); + } + /** + * 解码流数据 + * @param data 流数据 + */ + async decode(data) { + return this.decoder?.decode(data); + } + /** + * 解码整个文件 + * @param data 文件数据 + */ + async decodeAll(data) { + return this.decoder?.decodeFile(data); + } + /** + * 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用 + */ + async flush() { + return await this.decoder?.flush(); + } + } + const mimeTypeMap = { + [AudioType.Aac]: "audio/aac", + [AudioType.Flac]: "audio/flac", + [AudioType.Mp3]: "audio/mpeg", + [AudioType.Ogg]: "application/ogg", + [AudioType.Opus]: "application/ogg", + [AudioType.Wav]: "application/ogg", + }; + + function isOggPage(data) { + return !isNil(data.isFirstPage); + } + class AudioStreamSource { + constructor(context) { + this.output = context.createBufferSource(); + /** 是否已经完全加载完毕 */ + this.loaded = false; + /** 是否正在播放 */ + this.playing = false; + /** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */ + this.buffered = 0; + /** 已经缓冲的采样点数量 */ + this.bufferedSamples = 0; + /** 歌曲时长,加载完毕之前保持为 0 */ + this.duration = 0; + /** 在流传输阶段,至少缓冲多长时间的音频之后才开始播放,单位秒 */ + this.bufferPlayDuration = 1; + /** 音频的采样率,未成功解析出之前保持为 0 */ + this.sampleRate = 0; + //是否循环播放 + this.loop = false; + /** 上一次播放是从何时开始的 */ + this.lastStartWhen = 0; + /** 开始播放时刻 */ + this.lastStartTime = 0; + /** 上一次播放的缓存长度 */ + this.lastBufferSamples = 0; + + /** 是否已经获取到头文件 */ + this.headerRecieved = false; + /** 音频类型 */ + this.audioType = ""; + /** 每多长时间组成一个缓存 Float32Array */ + this.bufferChunkSize = 10; + /** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */ + this.audioData = []; + + this.errored = false; + this.ac = context; + } + /** 当前已经播放了多长时间 */ + get currentTime() { + return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; + } + /** + * 设置每个缓存数据的大小,默认为10秒钟一个缓存数据 + * @param size 每个缓存数据的时长,单位秒 + */ + setChunkSize(size) { + if (this.controller?.loading || this.loaded) return; + this.bufferChunkSize = size; + } + + piped(controller) { + this.controller = controller; + } + + async pump(data, done) { + if (!data || this.errored) return; + if (!this.headerRecieved) { + // 检查头文件获取音频类型,仅检查前256个字节 + const toCheck = data.slice(0, 256); + this.audioType = checkAudioType(data); + if (!this.audioType) { + console.error( + "Unknown audio type. Header: '" + + [...toCheck] + .map((v) => v.toString(16).padStart(2, "0")) + .join(" ") + .toUpperCase() + + "'" + ); + return; + } + // 创建解码器 + const Decoder = AudioDecoder.decoderMap.get(this.audioType); + if (!Decoder) { + this.errored = true; + console.error( + "Cannot decode stream source type of '" + + this.audioType + + "', since there is no registered decoder for that type." + ); + return Promise.reject( + `Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.` + ); + } + this.decoder = new Decoder(); + // 创建数据解析器 + const mime = mimeTypeMap[this.audioType]; + const parser = new CodecParser(mime); + this.parser = parser; + await this.decoder.create(); + this.headerRecieved = true; + } + + const decoder = this.decoder; + const parser = this.parser; + if (!decoder || !parser) { + this.errored = true; + return Promise.reject( + "No parser or decoder attached in this AudioStreamSource" + ); + } + + await this.decodeData(data, decoder, parser); + if (done) await this.decodeFlushData(decoder, parser); + this.checkBufferedPlay(); + } + + /** + * 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告 + */ + checkSampleRate(info) { + for (const one of info) { + const frame = isOggPage(one) ? one.codecFrames[0] : one; + if (frame) { + const rate = frame.header.sampleRate; + if (this.sampleRate === 0) { + this.sampleRate = rate; + break; + } else { + if (rate !== this.sampleRate) { + console.warn("Sample rate in stream audio must be constant."); + } + } + } + } + } + + /** + * 解析音频数据 + */ + async decodeData(data, decoder, parser) { + // 解析音频数据 + const audioData = await decoder.decode(data); + if (!audioData) return; + // @ts-expect-error 库类型声明错误 + const audioInfo = [...parser.parseChunk(data)]; + + // 检查采样率 + this.checkSampleRate(audioInfo); + // 追加音频数据 + this.appendDecodedData(audioData, audioInfo); + } + + /** + * 解码剩余数据 + */ + async decodeFlushData(decoder, parser) { + const audioData = await decoder.flush(); + if (!audioData) return; + // @ts-expect-error 库类型声明错误 + const audioInfo = [...parser.flush()]; + + this.checkSampleRate(audioInfo); + this.appendDecodedData(audioData, audioInfo); + } + + /** + * 追加音频数据 + */ + appendDecodedData(data, info) { + const channels = data.channelData.length; + if (channels === 0) return; + if (this.audioData.length !== channels) { + this.audioData = []; + for (let i = 0; i < channels; i++) { + this.audioData.push([]); + } + } + // 计算出应该放在哪 + const chunk = this.sampleRate * this.bufferChunkSize; + const sampled = this.bufferedSamples; + const pushIndex = Math.floor(sampled / chunk); + const bufferIndex = sampled % chunk; + const dataLength = data.channelData[0].length; + let buffered = 0; + let nowIndex = pushIndex; + let toBuffer = bufferIndex; + while (buffered < dataLength) { + const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk; + + for (let i = 0; i < channels; i++) { + const audioData = this.audioData[i]; + if (!audioData[nowIndex]) { + audioData.push(new Float32Array(chunk)); + } + const toPush = data.channelData[i].slice(buffered, buffered + rest); + + audioData[nowIndex].set(toPush, toBuffer); + } + buffered += rest; + nowIndex++; + toBuffer = 0; + } + + this.buffered += + info.reduce((prev, curr) => prev + curr.duration, 0) / 1000; + this.bufferedSamples += info.reduce( + (prev, curr) => prev + curr.samples, + 0 + ); + } + + /** + * 检查已缓冲内容,并在未开始播放时播放 + */ + checkBufferedPlay() { + if (this.playing || this.sampleRate === 0) return; + const played = this.lastBufferSamples / this.sampleRate; + const dt = this.buffered - played; + if (this.loaded) { + this.playAudio(played); + return; + } + if (dt < this.bufferPlayDuration) return; + + this.lastBufferSamples = this.bufferedSamples; + // 需要播放 + this.mergeBuffers(); + if (!this.buffer) return; + if (this.playing) this.output.stop(); + this.createSourceNode(this.buffer); + this.output.loop = false; + this.output.start(0, played); + this.lastStartTime = this.ac.currentTime; + this.playing = true; + this.output.addEventListener("ended", () => { + this.playing = false; + this.checkBufferedPlay(); + }); + } + + mergeBuffers() { + const buffer = this.ac.createBuffer( + this.audioData.length, + this.bufferedSamples, + this.sampleRate + ); + const chunk = this.sampleRate * this.bufferChunkSize; + const bufferedChunks = Math.floor(this.bufferedSamples / chunk); + const restLength = this.bufferedSamples % chunk; + for (let i = 0; i < this.audioData.length; i++) { + const audio = this.audioData[i]; + const data = new Float32Array(this.bufferedSamples); + for (let j = 0; j < bufferedChunks; j++) { + data.set(audio[j], chunk * j); + } + if (restLength !== 0) { + data.set( + audio[bufferedChunks].slice(0, restLength), + chunk * bufferedChunks + ); + } + + buffer.copyToChannel(data, i, 0); + } + this.buffer = buffer; + } + + async start() { + delete this.buffer; + this.headerRecieved = false; + this.audioType = ""; + this.errored = false; + this.buffered = 0; + this.sampleRate = 0; + this.bufferedSamples = 0; + this.duration = 0; + this.loaded = false; + if (this.playing) this.output.stop(); + this.playing = false; + this.lastStartTime = this.ac.currentTime; + } + + end(done, reason) { + if (done && this.buffer) { + this.loaded = true; + delete this.controller; + this.mergeBuffers(); + + this.duration = this.buffered; + this.audioData = []; + this.decoder?.destroy(); + delete this.decoder; + delete this.parser; + } else { + console.warn( + "Unexpected end when loading stream audio, reason: '" + + (reason ?? "") + + "'" + ); + } + } + + playAudio(when) { + if (!this.buffer) return; + this.lastStartTime = this.ac.currentTime; + if (this.playing) this.output.stop(); + if (this.route.status !== AudioStatus.Playing) { + this.route.status = AudioStatus.Playing; + } + this.createSourceNode(this.buffer); + this.output.start(0, when); + this.playing = true; + + this.output.addEventListener("ended", () => { + this.playing = false; + if (this.route.status === AudioStatus.Playing) { + this.route.status = AudioStatus.Stoped; + } + if (this.loop && !this.output.loop) this.play(0); + }); + } + /** + * 开始播放这个音频源 + */ + play(when) { + if (this.playing || this.errored) return; + if (this.loaded && this.buffer) { + this.playing = true; + this.playAudio(when); + } else { + this.controller?.start(); + } + } + + createSourceNode(buffer) { + if (!this.target) return; + const node = this.ac.createBufferSource(); + node.buffer = buffer; + if (this.playing) this.output.stop(); + this.playing = false; + this.output = node; + node.connect(this.target.input); + node.loop = this.loop; + } + /** + * 停止播放这个音频源 + * @returns 音频暂停的时刻 number + */ + stop() { + if (this.playing) this.output.stop(); + this.playing = false; + return this.ac.currentTime - this.lastStartTime; + } + /** + * 连接到音频路由图上,每次调用播放的时候都会执行一次 + * @param target 连接至的目标 IAudioInput + */ + connect(target) { + this.target = target; + } + /** + * 设置是否循环播放 + * @param loop 是否循环 boolean) + */ + setLoop(loop) { + this.loop = loop; + } + } + class AudioElementSource { + constructor(context) { + const audio = new Audio(); + audio.preload = "none"; + this.output = context.createMediaElementSource(audio); + this.audio = audio; + this.ac = context; + audio.addEventListener("play", () => { + this.playing = true; + if (this.route.status !== AudioStatus.Playing) { + this.route.status = AudioStatus.Playing; + } + }); + audio.addEventListener("ended", () => { + this.playing = false; + if (this.route.status === AudioStatus.Playing) { + this.route.status = AudioStatus.Stoped; + } + }); + } + get duration() { + return this.audio.duration; + } + get currentTime() { + return this.audio.currentTime; + } + /** + * 设置音频源的路径 + * @param url 音频路径 + */ + setSource(url) { + this.audio.src = url; + } + + play(when = 0) { + if (this.playing) return; + this.audio.currentTime = when; + this.audio.play(); + } + + stop() { + this.audio.pause(); + this.playing = false; + if (this.route.status === AudioStatus.Playing) { + this.route.status = AudioStatus.Stoped; + } + return this.audio.currentTime; + } + + connect(target) { + this.output.connect(target.input); + } + + setLoop(loop) { + this.audio.loop = loop; + } + } + class AudioBufferSource { + constructor(context) { + this.output = context.createBufferSource(); + /** 是否循环 */ + this.loop = false; + /** 上一次播放是从何时开始的 */ + this.lastStartWhen = 0; + /** 播放开始时刻 */ + this.lastStartTime = 0; + this.duration = 0; + this.ac = context; + } + get currentTime() { + return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; + } + + /** + * 设置音频源数据 + * @param buffer 音频源,可以是未解析的 ArrayBuffer,也可以是已解析的 AudioBuffer + */ + async setBuffer(buffer) { + if (buffer instanceof ArrayBuffer) { + this.buffer = await this.ac.decodeAudioData(buffer); + } else { + this.buffer = buffer; + } + this.duration = this.buffer.duration; + } + + play(when) { + if (this.playing || !this.buffer) return; + this.playing = true; + this.lastStartTime = this.ac.currentTime; + if (this.route.status !== AudioStatus.Playing) { + this.route.status = AudioStatus.Playing; + } + this.createSourceNode(this.buffer); + this.output.start(0, when); + this.output.addEventListener("ended", () => { + this.playing = false; + if (this.route.status === AudioStatus.Playing) { + this.route.status = AudioStatus.Stoped; + } + if (this.loop && !this.output.loop) this.play(0); + }); + } + + createSourceNode(buffer) { + if (!this.target) return; + const node = this.ac.createBufferSource(); + node.buffer = buffer; + this.output = node; + node.connect(this.target.input); + node.loop = this.loop; + } + + stop() { + this.output.stop(); + return this.ac.currentTime - this.lastStartTime; + } + + connect(target) { + this.target = target; + } + + setLoop(loop) { + this.loop = loop; + } + } + class AudioPlayer { + constructor() { + /** 音频播放上下文 */ + this.ac = new AudioContext(); + /** 音量节点 */ + this.gain = this.ac.createGain(); + this.gain.connect(this.ac.destination); + this.audioRoutes = new Map(); + } + /** + * 解码音频数据 + * @param data 音频数据 + */ + decodeAudioData(data) { + return AudioDecoder.decodeAudioData(data, this); + } + /** + * 设置音量 + * @param volume 音量 + */ + setVolume(volume) { + this.gain.gain.value = volume; + } + + /** + * 获取音量 + */ + getVolume() { + return this.gain.gain.value; + } + + /** + * 创建一个音频源 + * @param Source 音频源类 + */ + createSource(Source) { + return new Source(this.ac); + } + + /** + * 创建一个兼容流式音频源,可以与流式加载相结合,主要用于处理 opus ogg 不兼容的情况 + */ + createStreamSource() { + return new AudioStreamSource(this.ac); + } + + /** + * 创建一个通过 audio 元素播放的音频源 + */ + createElementSource() { + return new AudioElementSource(this.ac); + } + + /** + * 创建一个通过 AudioBuffer 播放的音频源 + */ + createBufferSource() { + return new AudioBufferSource(this.ac); + } + + /** + * 获取音频目的地 + */ + getDestination() { + return this.gain; + } + + /** + * 创建一个音频效果器 + * @param Effect 效果器类 + */ + createEffect(Effect) { + return new Effect(this.ac); + } + + /** + * 创建一个修改音量的效果器 + * ```txt + * |----------| + * Input ----> | GainNode | ----> Output + * |----------| + * ``` + */ + createVolumeEffect() { + return new VolumeEffect(this.ac); + } + + /** + * 创建一个立体声效果器 + * ```txt + * |------------| + * Input ----> | PannerNode | ----> Output + * |------------| + * ``` + */ + createStereoEffect() { + return new StereoEffect(this.ac); + } + + /** + * 创建一个修改单个声道音量的效果器 + * ```txt + * |----------| + * -> | GainNode | \ + * |--------------| / |----------| -> |------------| + * Input ----> | SplitterNode | ...... | MergerNode | ----> Output + * |--------------| \ |----------| -> |------------| + * -> | GainNode | / + * |----------| + * ``` + */ + createChannelVolumeEffect() { + return new ChannelVolumeEffect(this.ac); + } + + /** + * 创建一个延迟效果器 + * |-----------| + * Input ----> | DelayNode | ----> Output + * |-----------| + */ + createDelay() { + return new DelayEffect(this.ac); + } + + /** + * 创建一个回声效果器 + * ```txt + * |----------| + * Input ----> | GainNode | ----> Output + * ^ |----------| | + * | | + * | |------------| ↓ + * |-- | Delay Node | <-- + * |------------| + * ``` + */ + createEchoEffect() { + return new EchoEffect(this.ac); + } + + /** + * 创建一个音频播放路由 + * @param source 音频源 + */ + createRoute(source) { + return new AudioRoute(source, this); + } + + /** + * 添加一个音频播放路由,可以直接被播放 + * @param id 这个音频播放路由的名称 + * @param route 音频播放路由对象 + */ + addRoute(id, route) { + if (!this.audioRoutes) this.audioRoutes = new Map(); + if (this.audioRoutes.has(id)) { + console.warn( + "Audio route with id of '" + + id + + "' has already existed. New route will override old route." + ); + } + this.audioRoutes.set(id, route); + } + + /** + * 根据名称获取音频播放路由对象 + * @param id 音频播放路由的名称 + */ + getRoute(id) { + return this.audioRoutes.get(id); + } + /** + * 移除一个音频播放路由 + * @param id 要移除的播放路由的名称 + */ + removeRoute(id) { + this.audioRoutes.delete(id); + } + /** + * 播放音频 + * @param id 音频名称 + * @param when 从音频的哪个位置开始播放,单位秒 + */ + play(id, when) { + const route = this.getRoute(id); + if (!route) { + console.warn( + "Cannot play audio route '" + + id + + "', since there is not added route named it." + ); + return; + } + + route.play(when); + } + + /** + * 暂停音频播放 + * @param id 音频名称 + * @returns 当音乐真正停止时兑现 + */ + pause(id) { + const route = this.getRoute(id); + if (!route) { + console.warn( + "Cannot pause audio route '" + + id + + "', since there is not added route named it." + ); + return; + } + return route.pause(); + } + + /** + * 停止音频播放 + * @param id 音频名称 + * @returns 当音乐真正停止时兑现 + */ + stop(id) { + const route = this.getRoute(id); + if (!route) { + console.warn( + "Cannot stop audio route '" + + id + + "', since there is not added route named it." + ); + return; + } + return route.stop(); + } + + /** + * 继续音频播放 + * @param id 音频名称 + */ + resume(id) { + const route = this.getRoute(id); + if (!route) { + console.warn( + "Cannot pause audio route '" + + id + + "', since there is not added route named it." + ); + return; + } + route.resume(); + } + + /** + * 设置听者位置,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 位置x坐标 + * @param y 位置y坐标 + * @param z 位置z坐标 + */ + setListenerPosition(x, y, z) { + const listener = this.ac.listener; + listener.positionX.value = x; + listener.positionY.value = y; + listener.positionZ.value = z; + } + + /** + * 设置听者朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 朝向x坐标 + * @param y 朝向y坐标 + * @param z 朝向z坐标 + */ + setListenerOrientation(x, y, z) { + const listener = this.ac.listener; + listener.forwardX.value = x; + listener.forwardY.value = y; + listener.forwardZ.value = z; + } + + /** + * 设置听者头顶朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户 + * @param x 头顶朝向x坐标 + * @param y 头顶朝向y坐标 + * @param z 头顶朝向z坐标 + */ + setListenerUp(x, y, z) { + const listener = this.ac.listener; + listener.upX.value = x; + listener.upY.value = y; + listener.upZ.value = z; + } + } + class AudioRoute { + constructor(source, player) { + source.route = this; + this.output = source.output; + + /** 效果器路由图 */ + this.effectRoute = []; + + /** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */ + this.endTime = 0; + /** 暂停时播放了多长时间 */ + this.pauseCurrentTime = 0; + /** 当前播放状态 */ + this.player = player; + this.status = AudioStatus.Stoped; + + this.shouldStop = false; + /** + * 每次暂停或停止时自增,用于判断当前正在处理的情况。 + * 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作 + */ + this.stopIdentifier = 0; + /** 暂停时刻 */ + this.pauseTime = 0; + this.source = source; + this.source.player = player; + } + /** 音频时长,单位秒 */ + get duration() { + return this.source.duration; + } + /** 当前播放了多长时间,单位秒 */ + get currentTime() { + if (this.status === AudioStatus.Paused) { + return this.pauseCurrentTime; + } else { + return this.source.currentTime; + } + } + set currentTime(time) { + this.source.stop(); + this.source.play(time); + } + /** + * 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。 + * @param time 暂停或停止时,经过多长时间之后才会结束音频的播放 + */ + setEndTime(time) { + this.endTime = time; + } + + /** + * 当音频播放时执行的函数,可以用于音频淡入效果 + * @param fn 音频开始播放时执行的函数 + */ + onStart(fn) { + this.audioStartHook = fn; + } + + /** + * 当音频暂停或停止时执行的函数,可以用于音频淡出效果 + * @param fn 音频在暂停或停止时执行的函数,不填时表示取消这个钩子。 + * 包含两个参数,第一个参数是结束时长,第二个参数是当前音频播放路由对象 + */ + onEnd(fn) { + this.audioEndHook = fn; + } + + /** + * 开始播放这个音频 + * @param when 从音频的什么时候开始播放,单位秒 + */ + async play(when = 0) { + if (this.status === AudioStatus.Playing) return; + this.link(); + await this.player.ac.resume(); + if (this.effectRoute.length > 0) { + const first = this.effectRoute[0]; + this.source.connect(first); + const last = this.effectRoute.at(-1); + last.connect({ input: this.player.getDestination() }); + } else { + this.source.connect({ input: this.player.getDestination() }); + } + this.source.play(when); + this.status = AudioStatus.Playing; + this.pauseTime = 0; + this.audioStartHook?.(this); + this.startAllEffect(); + if (this.status !== AudioStatus.Playing) { + this.status = AudioStatus.Playing; + } + } + + /** + * 暂停音频播放 + */ + async pause() { + if (this.status !== AudioStatus.Playing) return; + this.status = AudioStatus.Pausing; + this.stopIdentifier++; + const identifier = this.stopIdentifier; + if (this.audioEndHook) { + this.audioEndHook(this.endTime, this); + await sleep(this.endTime); + } + if ( + this.status !== AudioStatus.Pausing || + this.stopIdentifier !== identifier + ) { + return; + } + this.pauseCurrentTime = this.source.currentTime; + const time = this.source.stop(); + this.pauseTime = time; + if (this.shouldStop) { + this.status = AudioStatus.Stoped; + this.endAllEffect(); + + this.shouldStop = false; + } else { + this.status = AudioStatus.Paused; + this.endAllEffect(); + } + this.endAllEffect(); + } + + /** + * 继续音频播放 + */ + resume() { + if (this.status === AudioStatus.Playing) return; + if ( + this.status === AudioStatus.Pausing || + this.status === AudioStatus.Stoping + ) { + this.audioStartHook?.(this); + + return; + } + if (this.status === AudioStatus.Paused) { + this.play(this.pauseTime); + } else { + this.play(0); + } + this.status = AudioStatus.Playing; + this.pauseTime = 0; + this.audioStartHook?.(this); + this.startAllEffect(); + } + + /** + * 停止音频播放 + */ + async stop() { + if (this.status !== AudioStatus.Playing) { + if (this.status === AudioStatus.Pausing) { + this.shouldStop = true; + } + return; + } + this.status = AudioStatus.Stoping; + this.stopIdentifier++; + const identifier = this.stopIdentifier; + if (this.audioEndHook) { + this.audioEndHook(this.endTime, this); + await sleep(this.endTime); + } + if ( + this.status !== AudioStatus.Stoping || + this.stopIdentifier !== identifier + ) { + return; + } + this.source.stop(); + this.status = AudioStatus.Stoped; + this.pauseTime = 0; + this.endAllEffect(); + } + + /** + * 添加效果器 + * @param effect 要添加的效果,可以是数组,表示一次添加多个 + * @param index 从哪个位置开始添加,如果大于数组长度,那么加到末尾,如果小于0,那么将会从后面往前数。默认添加到末尾 + */ + addEffect(effect, index) { + if (isNil(index)) { + if (effect instanceof Array) { + this.effectRoute.push(...effect); + } else { + this.effectRoute.push(effect); + } + } else { + if (effect instanceof Array) { + this.effectRoute.splice(index, 0, ...effect); + } else { + this.effectRoute.splice(index, 0, effect); + } + } + this.setOutput(); + if (this.source.playing) this.link(); + } + + /** + * 移除一个效果器 + * @param effect 要移除的效果 + */ + removeEffect(effect) { + const index = this.effectRoute.indexOf(effect); + if (index === -1) return; + this.effectRoute.splice(index, 1); + effect.disconnect(); + this.setOutput(); + if (this.source.playing) this.link(); + } + + setOutput() { + const effect = this.effectRoute.at(-1); + if (!effect) this.output = this.source.output; + else this.output = effect.output; + } + + /** + * 连接音频路由图 + */ + link() { + this.effectRoute.forEach((v) => v.disconnect()); + this.effectRoute.forEach((v, i) => { + const next = this.effectRoute[i + 1]; + if (next) { + v.connect(next); + } + }); + } + + startAllEffect() { + this.effectRoute.forEach((v) => v.start()); + } + + endAllEffect() { + this.effectRoute.forEach((v) => v.end()); + } + } + + const audioPlayer = new AudioPlayer(); + + class BgmController { + constructor(player) { + this.mainGain = player.createVolumeEffect(); + this.player = player; + /** bgm音频名称的前缀 */ + this.prefix = "bgms."; + /** 每个 bgm 的音量控制器 */ + this.gain = new Map(); + + /** 正在播放的 bgm */ + this.playingBgm = ""; + /** 是否正在播放 */ + this.playing = false; + + /** 是否已经启用 */ + this.enabled = true; + /** 是否屏蔽所有的音乐切换 */ + this.blocking = false; + /** 渐变时长 */ + this.transitionTime = 2000; + } + + /** + * 设置音频渐变时长 + * @param time 渐变时长 + */ + setTransitionTime(time) { + this.transitionTime = time; + for (const [, value] of this.gain) { + value.transition.time(time); + } + } + + /** + * 屏蔽音乐切换 + */ + blockChange() { + this.blocking = true; + } + + /** + * 取消屏蔽音乐切换 + */ + unblockChange() { + this.blocking = false; + } + + /** + * 设置总音量大小 + * @param volume 音量大小 + */ + setVolume(volume) { + this.mainGain.setVolume(volume); + this._volume = volume; + } + /** + * 获取总音量大小 + */ + getVolume() { + return this.mainGain.getVolume(); + } + /** + * 设置是否启用 + * @param enabled 是否启用 + */ + setEnabled(enabled) { + if (enabled) this.resume(); + else this.stop(); + this.enabled = enabled; + } + + /** + * 设置 bgm 音频名称的前缀 + */ + setPrefix(prefix) { + this.prefix = prefix; + } + + getId(name) { + return `${this.prefix}${name}`; + } + + /** + * 根据 bgm 名称获取其 AudioRoute 实例 + * @param id 音频名称 + */ + get(id) { + return this.player.getRoute(this.getId(id)); + } + + /** + * 添加一个 bgm + * @param id 要添加的 bgm 的名称 + * @param url 指定 bgm 的加载地址 + */ + addBgm(id, url = `project/bgms/${id}`) { + const type = guessTypeByExt(id); + if (!type) { + console.warn( + "Unknown audio extension name: '" + + id.split(".").slice(0, -1).join(".") + + "'" + ); + return; + } + const gain = this.player.createVolumeEffect(); + if (isAudioSupport(type)) { + const source = audioPlayer.createElementSource(); + source.setSource(url); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } else { + const source = audioPlayer.createStreamSource(); + const stream = new StreamLoader(url); + stream.pipe(source); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } + } + + /** + * 移除一个 bgm + * @param id 要移除的 bgm 的名称 + */ + removeBgm(id) { + this.player.removeRoute(this.getId(id)); + const gain = this.gain.get(id); + gain?.transition.ticker.destroy(); + this.gain.delete(id); + } + + setTransition(id, route, gain) { + const transition = new Transition(); + transition + .time(this.transitionTime) + .mode(linear()) + .transition("volume", 0); + + const tick = () => { + gain.setVolume(transition.value.volume); + }; + + /** + * @param expect 在结束时应该是正在播放还是停止 + */ + const setTick = async (expect) => { + transition.ticker.remove(tick); + transition.ticker.add(tick); + const identifier = route.stopIdentifier; + await sleep(this.transitionTime + 500); + if (route.status === expect && identifier === route.stopIdentifier) { + transition.ticker.remove(tick); + if (route.status === AudioStatus.Playing) { + gain.setVolume(1); + } else { + gain.setVolume(0); + } + } + }; + + route.onStart(async () => { + transition.transition("volume", 1); + setTick(AudioStatus.Playing); + }); + route.onEnd(() => { + transition.transition("volume", 0); + setTick(AudioStatus.Paused); + }); + route.setEndTime(this.transitionTime); + + this.gain.set(id, { effect: gain, transition }); + } + + /** + * 播放一个 bgm + * @param id 要播放的 bgm 名称 + */ + play(id, when) { + if (this.blocking) return; + if (id !== this.playingBgm && this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playingBgm = id; + if (!this.enabled) return; + this.player.play(this.getId(id), when); + this.playing = true; + } + + /** + * 继续当前的 bgm + */ + resume() { + if (this.blocking || !this.enabled || this.playing) return; + if (this.playingBgm) { + this.player.resume(this.getId(this.playingBgm)); + } + this.playing = true; + } + + /** + * 暂停当前的 bgm + */ + pause() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playing = false; + } + + /** + * 停止当前的 bgm + */ + stop() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.stop(this.getId(this.playingBgm)); + } + this.playing = false; + } + } + const bgmController = new BgmController(audioPlayer); + + class SoundPlayer { + constructor(player) { + /** 每个音效的唯一标识符 */ + this.num = 0; + this.enabled = true; + this.gain = player.createVolumeEffect(); + /** 每个音效的数据 */ + this.buffer = new Map(); + /** 所有正在播放的音乐 */ + this.playing = new Set(); + this.player = player; + } + /** + * 设置是否启用音效 + * @param enabled 是否启用音效 + */ + setEnabled(enabled) { + if (!enabled) this.stopAllSounds(); + this.enabled = enabled; + } + + /** + * 设置音量大小 + * @param volume 音量大小 + */ + setVolume(volume) { + this.gain.setVolume(volume); + } + /** + * 获取音量大小 + */ + getVolume() { + return this.gain.getVolume(); + } + /** + * 添加一个音效 + * @param id 音效名称 + * @param data 音效的Uint8Array数据 + */ + async add(id, data) { + const buffer = await this.player.decodeAudioData(data); + if (!buffer) { + console.warn( + "Cannot decode sound '" + + id + + "', since audio file may not supported by 2.b." + ); + return; + } + this.buffer.set(id, buffer); + } + + /** + * 播放一个音效 + * @param id 音效名称 + * @param position 音频位置,[0, 0, 0]表示正中心,x轴指向水平向右,y轴指向水平向上,z轴指向竖直向上 + * @param orientation 音频朝向,[0, 1, 0]表示朝向前方 + */ + play(id, position = [0, 0, 0], orientation = [1, 0, 0]) { + if (!this.enabled || !id) return -1; + const buffer = this.buffer.get(id); + if (!buffer) { + console.warn( + "Cannot play sound '" + + id + + "', since there is no added data named it." + ); + return -1; + } + const soundNum = this.num++; + + const source = this.player.createBufferSource(); + source.setBuffer(buffer); + const route = this.player.createRoute(source); + const stereo = this.player.createStereoEffect(); + stereo.setPosition(position[0], position[1], position[2]); + stereo.setOrientation(orientation[0], orientation[1], orientation[2]); + route.addEffect([stereo, this.gain]); + this.player.addRoute(`sounds.${soundNum}`, route); + route.play(); + source.output.addEventListener("ended", () => { + this.playing.delete(soundNum); + }); + this.playing.add(soundNum); + return soundNum; + } + + /** + * 停止一个音效 + * @param num 音效的唯一 id + */ + stop(num) { + const id = `sounds.${num}`; + const route = this.player.getRoute(id); + if (route) { + route.stop(); + this.player.removeRoute(id); + this.playing.delete(num); + } + } + + /** + * 停止播放所有音效 + */ + stopAllSounds() { + this.playing.forEach((v) => { + const id = `sounds.${v}`; + const route = this.player.getRoute(id); + if (route) { + route.stop(); + this.player.removeRoute(id); + } + }); + this.playing.clear(); + } + } + const soundPlayer = new SoundPlayer(audioPlayer); + + function loadAllBgm() { + const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; + for (const bgm of data.main.bgms) { + bgmController.addBgm(bgm); + } + } + loadAllBgm(); + AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder); + AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder); + + core.plugin.audioSystem = { + AudioType, + AudioDecoder, + AudioStatus, + checkAudioType, + isAudioSupport, + audioPlayer, + soundPlayer, + bgmController, + guessTypeByExt, + BgmController, + SoundPlayer, + EchoEffect, + DelayEffect, + ChannelVolumeEffect, + VolumeEffect, + StereoEffect, + AudioEffect, + AudioPlayer, + AudioRoute, + AudioStreamSource, + AudioElementSource, + AudioBufferSource, + loadAllBgm, + StreamLoader, + }; + //bgm相关复写 + control.prototype.playBgm = (bgm, when) => { + bgm = core.getMappedName(bgm); + if (main.mode != "play" || !core.material.bgms[bgm]) return; + // 如果不允许播放 + if (!core.musicStatus.bgmStatus) { + try { + core.musicStatus.playingBgm = bgm; + core.musicStatus.lastBgm = bgm; + core.material.bgms[bgm].pause(); + } catch (e) { + console.error(e); + } + return; + } + core.setMusicBtn(); + + try { + bgmController.play(bgm, when); + } catch (e) { + console.log("无法播放BGM " + bgm); + console.error(e); + core.musicStatus.playingBgm = null; + } + + }; + control.prototype.pauseBgm = () => { + bgmController.pause(); + core.setMusicBtn(); + }; + + control.prototype.resumeBgm = function () { + bgmController.resume(); + core.setMusicBtn(); + }; + control.prototype.checkBgm = function () { + core.playBgm(bgmController.playingBgm || main.startBgm); + }; + control.prototype.triggerBgm = function () { + core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus; + if (bgmController.playing) bgmController.pause(); + else bgmController.resume(); + core.setMusicBtn(); + core.setLocalStorage("bgmStatus", core.musicStatus.bgmStatus); + }; + //sound相关复写 + control.prototype.playSound = function ( + sound, + _pitch, + callback, + position, + orientation + ) { + if (main.mode != "play" || !core.musicStatus.soundStatus) return callback?.(); + const name = core.getMappedName(sound); + const num = soundPlayer.play(name, position, orientation); + const route = audioPlayer.getRoute(`sounds.${num}`); + if (!route) { + callback?.(); + return -1; + } else { + sleep(route.duration * 1000).then(() => callback?.()); + return num; + } + }; + control.prototype.stopSound = function (id) { + if (isNil(id)) { + soundPlayer.stopAllSounds(); + } else { + soundPlayer.stop(id); + } + }; + control.prototype.getPlayingSounds = function () { + return [...soundPlayer.playing]; + }; + //sound加载复写 + loader.prototype._loadOneSound_decodeData = function (name, data) { + if (data instanceof Blob) { + var blobReader = new zip.BlobReader(data); + blobReader.init(function () { + blobReader.readUint8Array(0, blobReader.size, function (uint8) { + //core.loader._loadOneSound_decodeData(name, uint8.buffer); + soundPlayer.add(name, uint8); + }); + }); + return; + } + if (data instanceof ArrayBuffer) { + const uint8 = new Uint8Array(data); + soundPlayer.add(name, uint8); + } + }; + //音量控制复写 + soundPlayer.setVolume( + core.musicStatus.userVolume * core.musicStatus.designVolume + ); + bgmController.setVolume( + core.musicStatus.userVolume * core.musicStatus.designVolume + ); + actions.prototype._clickSwitchs_sounds_userVolume = function (delta) { + var value = Math.round(Math.sqrt(100 * core.musicStatus.userVolume)); + if (value == 0 && delta < 0) return; + core.musicStatus.userVolume = core.clamp( + Math.pow(value + delta, 2) / 100, + 0, + 1 + ); + //audioContext 音效 不受designVolume 影响 + if (core.musicStatus.gainNode != null) + core.musicStatus.gainNode.gain.value = core.musicStatus.userVolume; + soundPlayer.setVolume( + core.musicStatus.userVolume * core.musicStatus.designVolume + ); + bgmController.setVolume( + core.musicStatus.userVolume * core.musicStatus.designVolume + ); + core.setLocalStorage("userVolume", core.musicStatus.userVolume); + core.playSound("确定"); + core.ui._drawSwitchs_sounds(); + }; + }, + "statistics": function () { + // 是否开启本插件,默认禁用;将此改成 true 将启用本插件。 + let __enable = true; + if (!__enable) return; + + const PX = core.__PIXELS__, + SIZE = core.__SIZE__; + const { + ButtonBase, RoundBtn, IconBtn, ExitBtn, + ArrowBtn, MenuBase, Pagination, KeyCodeEnum + } = core.plugin.uiBase; + + // 每次退出数据统计时清空,并且清空statistics变量 + /** + * @typedef {{[itemName:string]:{count:number, [floorId:string]:number}}} objCount + * @typedef {{count:number,money:number,exp:number,point:number,}} enemys + * @typedef {{count:number,hp:number,atk:number,def:number,mdef:number}} items + * @typedef {{toolsCount:objCount,gemsCount:objCount, + * equipsCount:objCount,enemysCount:objCount, + * enemys:enemys,items:items,}} floorStatistic + * @typedef {{[floorId:string]:floorStatistic}} statistics + */ + /** @type {statistics} */ + let statistics = {}; + + // 因为core.status.hero是进存档的,必定可序列化 + const clone = (typeof structuredClone === 'function') ? structuredClone : (ele) => JSON.parse(JSON.stringify(ele)); + + function getFloorStat(floorId) { + const stat = { + toolsCount: { total: 0, }, + gemsCount: { total: 0, }, + equipsCount: { total: 0, }, + enemysCount: { total: 0, }, + enemys: { count: 0, money: 0, exp: 0, point: 0, }, + items: { count: 0, hp: 0, atk: 0, def: 0, mdef: 0, }, + }; + core.maps.extractBlocks(floorId); + const blocks = core.status.maps[floorId].blocks; + const temp = clone(core.status.hero); + const ratio = core.status.thisMap.ratio; + core.setFlag("__statistics__", true); + // 在样板中对ratio使用了clone,这很奇怪,因为ratio似乎应该是一个number值,先改过来,看看会不会出问题 + core.status.thisMap.ratio = core.status.maps[floorId].ratio; + + for (let i = 0, l = blocks.length; i < l; i++) { + const block = blocks[i]; + const cls = block.event.cls, + id = block.event.id; + if (cls === "enemys" || cls === "enemy48") { + const enemy = core.material.enemys[block.event.id]; + stat.enemys.count++; + stat.enemys.money += enemy.money || 0; + stat.enemys.exp += enemy.exp || 0; + stat.enemys.point += enemy.point || 0; + + stat.enemysCount.total++; + if (!stat.enemysCount[id]) stat.enemysCount[id] = 0; + stat.enemysCount[id]++; + } else if (cls === "items") { + // 圣水享受特殊待遇,不参与统计,凡是效果依赖角色当前属性的物品都不应该统计,因为不知道实际吃到的时候角色是什么状态 + if (id === "superPotion") continue; + if (core.material.items[id].cls === "items") { + const effectObj = core.items.getItemEffectValue(id); + for (let statusName in effectObj) { + if (effectObj.hasOwnProperty(statusName)) core.addStatus(statusName, effectObj[statusName]); + } + if (core.material.items[id].itemEffect != null) { + try { eval(core.material.items[id].itemEffect); } catch (e) { } + } + stat.gemsCount.total++; + if (!stat.gemsCount[id]) stat.gemsCount[id] = 0; + stat.gemsCount[id]++; + } else if (core.material.items[id].cls === "tools") { + stat.toolsCount.total++; + if (!stat.toolsCount[id]) stat.toolsCount[id] = 0; + stat.toolsCount[id]++; + } else if (core.material.items[id].cls === "equips") { + stat.equipsCount.total++; + if (!stat.equipsCount[id]) stat.equipsCount[id] = 0; + stat.equipsCount[id]++; + } + } else if (block.event.trigger === "openDoor") { + stat.toolsCount.total++; + if (!stat.toolsCount[id]) stat.toolsCount[id] = 0; + stat.toolsCount[id]++; + } + } + ["hp", "atk", "def", "mdef"].forEach(status => { + stat.items[status] = core.status.hero[status] - temp[status]; + }); + core.setFlag("__statistics__", false); + core.status.thisMap.ratio = ratio; + core.status.hero = temp; + window.hero = core.status.hero; + window.flags = core.status.hero.flags; + return stat; + } + + function addToFormer(obj1, obj2) { + for (let i in obj2) { + if (!obj1[i]) obj1[i] = 0; + obj1[i] += obj2[i]; + } + } + + /** + * @param {{total:number; [itemId:string]:{[floorId:string]:number}}} obj1 + * @param {{total:number, [itemId:string]:number}} obj2 + */ + function addCountToFormer(obj1, obj2, floorId) { + obj1.total += obj2.total; + for (let itemId in obj2) { + if (!obj1[itemId]) obj1[itemId] = { total: 0 }; + if (!obj2[itemId]) continue; + obj1[itemId].total += obj2[itemId]; + obj1[itemId][floorId] = obj2[itemId]; + } + } + + /** + * @typedef {{total:number;[floorId:string]:number}} ObjSTAT + * @typedef {{toolsCount:ObjSTAT,gemsCount:ObjSTAT, + * equipsCount:ObjSTAT,enemysCount:ObjSTAT, + * enemys:enemys,items:items, + * battleDamage:ObjSTAT, extraDamage:ObjSTAT, + * poisonDamage:ObjSTAT, vampireExtraLoss:ObjSTAT,}} STAT + */ + /** + * @param {string[]} floorList 要统计的楼层的列表 + */ + function getFloorListStat(floorList) { + const statistics = core.status.hero.statistics; + const damagePerFloor = statistics.damagePerFloor; + /** @type {STAT} */ + const stat = { + toolsCount: { total: 0, }, + gemsCount: { total: 0, }, + equipsCount: { total: 0, }, + enemysCount: { total: 0, }, + enemys: { count: 0, money: 0, exp: 0, point: 0, }, + items: { count: 0, hp: 0, atk: 0, def: 0, mdef: 0, }, + battleDamage: { total: statistics.battleDamage }, + extraDamage: { total: statistics.extraDamage }, + poisonDamage: { total: statistics.poisonDamage }, + vampireExtraLoss: { total: statistics.vampireExtraLoss }, + }; + floorList.forEach(floorId => { + if (!statistics[floorId]) statistics[floorId] = getFloorStat(floorId); + addToFormer(stat.enemys, statistics[floorId].enemys); + addToFormer(stat.items, statistics[floorId].items); + addCountToFormer(stat.toolsCount, statistics[floorId].toolsCount, floorId); + addCountToFormer(stat.gemsCount, statistics[floorId].gemsCount, floorId); + addCountToFormer(stat.equipsCount, statistics[floorId].equipsCount, floorId); + addCountToFormer(stat.enemysCount, statistics[floorId].enemysCount, floorId); + if (damagePerFloor[floorId]) { + ["battleDamage", "extraDamage", "poisonDamage", "vampireExtraLoss"].forEach(key => { + stat[key][floorId] = damagePerFloor[floorId]?.[key] || 0; + }); + } + }); + return stat; + } + + class FloorBtn extends ButtonBase { + constructor(x, y, w, h) { + super(x, y, w, h); + this.floorId = ""; + /** @type {number} */this.key; + /** @type {FloorStatisticsMenu} */ this.menu; + } + + draw() { + const { x, y, w, h, floorId, ctx } = this; + let strokeStyle = "black"; + if (core.markedFloorIds[floorId]) strokeStyle = "yellow"; + if (this.menu.areaBorder === floorId) strokeStyle = "orange"; + core.ui.strokeRect(ctx, x - 1, y - 1, w + 2, h + 2, strokeStyle, 2); + core.maps.drawThumbnail(floorId, null, { + x, + y, + ctx, + damage: false, + noHD: true, + size: w + }); + core.setTextAlign(ctx, "center"); + if (floorId === core.status.floorId) { + core.ui.fillBoldText(ctx, "当前层", x + w / 2, y + h / 2, "red", "white", "12px Verdana"); + } + core.ui.fillText(ctx, floorId, x + w / 2, y + h + 12, "black", "12px Verdana"); + core.ui.fillText(ctx, core.status.maps[floorId].name, x + w / 2, y + h + 24, "black", "12px Verdana"); + } + } + + class FloorStatisticsMenu extends MenuBase { + constructor(name, toListen, x, y, w, h, zIndex, capacity) { + super(name, toListen, x, y, w, h, zIndex); + /** 一页的地图容量 */ + this.capacity = capacity; + this.totalFloorList = this.getTotalFloorList(); + this.page = Math.floor(this.totalFloorList.indexOf(core.status.floorId) / capacity); + this.floorList = this.getFloorList(); + /** 区域选择时的起始楼层*/this.areaBorder = ""; + /** 是否处于区域选择模式下 */this.areaSelectOn = false; + } + + updateBtnInfo() { + this.btnMap.forEach((btn, key) => { + if (btn instanceof FloorBtn) { + btn.disable = (key >= this.floorList.length); + btn.floorId = this.floorList[key]; + } + }); + } + + drawContent() { + const { x, y, w, h, zIndex, name } = this; + const ctx = core.createCanvas(name, x, y, w, h, zIndex); + this.updateBtnInfo(); + super.drawContent(); + } + + getTotalFloorList() { + return core.floorIds.filter(floorId => { + const floorData = core.status.maps[floorId]; + if (floorData.deleted) return false; // 被砍层插件砍掉的地图不计入 + if (floorData.cannotViewMap && floorId !== core.status.floorId) return false; + return true; + }) + } + + getFloorList() { + return this.totalFloorList.slice(this.page * this.capacity, + Math.min((this.page + 1) * this.capacity, this.totalFloorList.length)); + } + + pageDown() { + if (this.page > 0) { + this.page--; + this.floorList = this.getFloorList(); + this.drawContent(); + } + } + + pageUp() { + if ((this.page + 1) * this.capacity < this.totalFloorList.length) { + this.page++; + this.floorList = this.getFloorList(); + this.drawContent(); + } + } + } + + class FloorStatisticsTextMenu extends MenuBase { + constructor(name, toListen, x, y, w, h, zIndex) { + super(name, toListen, x, y, w, h, zIndex); + /** @type {STAT|null} */ + this.stat = null; + this.itemPage = 0; // 道具列表到了第几页 + this.floorPage = 0; // 楼层列表到了第几页 + this.ROW = 7; // 表格行数 + this.COLUMN = 5; // 表格列数 + this.itemList = []; // 要统计的元件的id列表 + this.floorList = []; //要统计的楼层的id列表 + /** @type {"tools"|"items"|"equips"|"enemys"|"damage"} */ + this.mode = "tools"; + } + + getFloorList() { + const floorList = []; + for (let floorId in core.markedFloorIds) { + if (core.markedFloorIds[floorId]) floorList.push(floorId); + } + return floorList; + } + + getItemList(count) { + let itemList = null; + const sortFunc = (a, b) => count[b].total - count[a].total; + if (this.mode === "tools") { + const doorNameList = []; + const itemNameList = []; + for (let i in count) { + if (i === "total") continue; + if (core.material.items[i]) itemNameList.push(i); + else doorNameList.push(i); + } + itemList = [...doorNameList.sort(sortFunc), ...itemNameList.sort(sortFunc)]; + } + else if (this.mode === "damage") { + itemList = ["battleDamage", "extraDamage"]; + if (core.status.hero.statistics.poisonDamage > 0) itemList.push("poisonDamage"); + if (core.status.hero.statistics?.vampireExtraLoss > 0) itemList.push("vampireExtraLoss"); + } + else itemList = Object.keys(count).filter(item => item !== "total").sort(sortFunc); + return itemList; + } + + getStat() { + return getFloorListStat(this.floorList); + } + + itemPageDown() { + if (this.itemPage > 0) this.itemPage--; + this.drawContent(); + } + + itemPageUp() { + if ((this.itemPage + 1) * this.ROW < this.itemList.length) this.itemPage++; + this.drawContent(); + } + + floorPageDown() { + if (this.floorPage > 0) this.floorPage--; + this.drawContent(); + } + + floorPageUp() { + if ((this.floorPage + 1) * this.COLUMN < this.floorList.length) this.floorPage++; + this.drawContent(); + } + + /** @param {ObjSTAT} count */ + drawTable(ctx, count) { + const { itemPage, floorPage, ROW, COLUMN, itemList, floorList } = this; + + const curritemList = itemList.slice(itemPage * ROW, + Math.min((itemPage + 1) * ROW, itemList.length)); + const currFloorList = floorList.slice(floorPage * COLUMN, + Math.min((floorPage + 1) * COLUMN, floorList.length)); + + core.ui.setTextBaseline(ctx, "top"); + core.ui.setTextAlign(ctx, "right"); + for (let i = -1, l = curritemList.length; i < l; i++) { + let id, name, total; + if (i !== -1) { + id = curritemList[i]; + if (this.mode !== "damage") { + name = core.material.items[id]?.name || core.material.enemys[id]?.name || + core.getBlockById(id).event.name || ""; + } + else { + if (id === "battleDamage") name = "战斗伤害"; + else if (id === "extraDamage") name = "地图伤害"; + else if (id === "poisonDamage") name = "中毒伤害"; + else if (id === "vampireExtraLoss") name = "吸血额外伤害"; + } + total = count[id].total || 0; + } + core.ui.strokeRect(ctx, 20, 155 + 25 * i, 80, 25, "black"); + for (let j = -1, l1 = currFloorList.length; j < l1; j++) { + core.ui.strokeRect(ctx, 150 + 50 * j, 155 + 25 * i, 50, 25, "black"); + const floorId = currFloorList[j]; + const floorCountStr = count[id]?.[currFloorList[j]] || "-"; + const text = (i === -1) ? ((j === -1) ? "总数" : floorId) : + ((j === -1) ? total : floorCountStr); + core.ui.fillText(ctx, text, 194 + 50 * j, 163 + 25 * i, + (text === "-") ? "gray" : "black", "12px Verdana"); + } + if (i === -1) continue; // i=-1是表头 + core.ui.drawIcon(ctx, id, 25, 160 + 25 * i, 16, 16); + core.ui.fillText(ctx, name, 94, 163 + 25 * i, "black", name.length < 5 ? "12px Verdana" : "8px Verdana"); + + } + } + + drawContent() { + if (!this.stat) return; + const { x, y, w, h, zIndex, name, stat } = this; + const ctx = core.createCanvas(name, x, y, w, h, zIndex); + const { enemys, items, toolsCount, gemsCount, equipsCount, enemysCount, + battleDamage, extraDamage, poisonDamage, vampireExtraLoss } = stat; + const enemyText = `给定地图中共有怪物${enemys.count}个` + + (enemys.money > 0 ? `,总金币${enemys.money}.` : '') + + (enemys.exp > 0 ? `,总经验${enemys.exp}` : '') + + (enemys.point > 0 ? `,总加点${enemys.point}` : '') + "。"; + let gemText = ''; + const itemEffectText = `所有即捡即用物品共计加生命值${items.hp}点` + + `,\n攻击${items.atk}点` + + `,防御${items.def}点` + + `,护盾${items.mdef}点` + "。"; + const text = enemyText + '\n' + itemEffectText; + core.ui.drawTextContent(ctx, text, { + maxWidth: 360, color: "black", left: 20, top: 10, + fontSize: 14 + }); + let count = toolsCount; + if (this.mode === "items") count = gemsCount; + else if (this.mode === "equips") count = equipsCount; + else if (this.mode === "enemys") count = enemysCount; + else if (this.mode === "damage") count = { battleDamage, extraDamage, poisonDamage, vampireExtraLoss }; + this.itemList = this.getItemList(count); + this.drawTable(ctx, count); + super.drawContent(); + } + } + + class PlayerStatisticsMenu extends MenuBase { + drawContent() { + const { name, x, y, w, h, zIndex } = this; + const ctx = this.createCanvas(name, x, y, w, h, zIndex); + const statistics = core.status.hero.statistics; + const str = `当前总步数:${core.status.hero.steps},当前游戏时长:${core.formatTime(statistics.currTime)},总游戏时长${core.formatTime(statistics.totalTime)}。瞬间移动次数:${statistics.moveDirectly},共计少走${statistics.ignoreSteps}步。 + +总计通过血瓶恢复生命值为${core.formatBigNumber(statistics.hp)}点。 + +总计打死了${statistics.battle}个怪物,得到了${core.formatBigNumber(statistics.money)}金币,${core.formatBigNumber(statistics.exp)}点经验。 + +受到的总伤害为${core.formatBigNumber(statistics.battleDamage + statistics.poisonDamage + statistics.extraDamage)},其中战斗伤害${core.formatBigNumber(statistics.battleDamage)}点${core.flags.statusBarItems.indexOf('enableDebuff') >= 0 ? `,中毒伤害${core.formatBigNumber(statistics.poisonDamage)}点` : ""},领域/夹击/阻击/血网伤害${core.formatBigNumber(statistics.extraDamage)}点。 +${statistics["vampireExtraLoss"] > 0 ? `吸血怪物累计对你造成超出最低伤害的额外伤害${core.formatBigNumber(statistics["vampireExtraLoss"])}点。` : ``} +${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.formatBigNumber(statistics.oneDefEffect || 0)}` : ""}${statistics.hasOwnProperty("oneMdefEffect") ? `,1护盾累计减伤为${core.formatBigNumber(statistics.oneMdefEffect || 0)}` : ""}。 +`; + core.drawTextContent(ctx, str, { + left: 30, top: 20, color: "black", maxWidth: 360, fontSize: 14 + }); + } + } + + class StatisticsMenu extends Pagination { + drawContent() { + const { x, y, w, h, zIndex, name } = this; + const ctx = core.createCanvas(name, x, y, w, h, zIndex); + core.ui.fillRect(ctx, x, y, w, h, "#E2E2E2"); + super.drawContent(); + } + } + + function StatisticsMenuFactory() { + const ROW = 3, COLUMN = 4; + const floorStatisticsMenu = new FloorStatisticsMenu("floorStatistics", ["ondown"], + 0, 50, PX, PX - 50, 132, ROW * COLUMN); + const floorStatisticsTextMenu = new FloorStatisticsTextMenu("floorStatisticsText", ["ondown"], + 0, 50, PX, PX - 50, 132); + const playerStatisticsMenu = new PlayerStatisticsMenu("playerStatistics", ["ondown"], + 0, 50, PX, PX - 50, 132); + const statisticsMenu = new StatisticsMenu( + [floorStatisticsMenu, floorStatisticsTextMenu, playerStatisticsMenu], + 0, "statistics", ["ondown"], 0, 0, PX, PX, 131); + + const floorStatisticsList = []; + /** @this {FloorBtn} */ + const mark = function () { + /** @type {FloorStatisticsMenu} */ + const menu = this.menu; + if (menu.areaSelectOn) { + if (!menu.areaBorder) menu.areaBorder = this.floorId; + else { + let index1 = menu.totalFloorList.indexOf(menu.areaBorder), + index2 = menu.totalFloorList.indexOf(this.floorId); + if (index2 < index1) [index1, index2] = [index2, index1]; + menu.totalFloorList.slice(index1, index2 + 1).forEach(floorId => { + core.markedFloorIds[floorId] = true; + }); + menu.areaSelectOn = false; + menu.areaBorder = ""; + } + } + else { + core.markedFloorIds[this.floorId] = !core.markedFloorIds[this.floorId]; + } + menu.drawContent(); + }; + for (let i = 0; i < ROW; i++) { + for (let j = 0; j < COLUMN; j++) { + const floorBtn = new FloorBtn(43 + j * 90, 20 + i * 100, 60, 60); + floorStatisticsList.push([i * COLUMN + j, floorBtn, mark.bind(floorBtn)]); + } + } + const pgDownBtn = new ArrowBtn(10, 145, 16, 16, "left"), + pgUpBtn = new ArrowBtn(390, 145, 16, 16, "right"); + floorStatisticsList.push(["pgDown", pgDownBtn, floorStatisticsMenu.pageDown.bind(floorStatisticsMenu)], + ["pgUp", pgUpBtn, floorStatisticsMenu.pageUp.bind(floorStatisticsMenu)]); + const checkAllBtn = new RoundBtn(30, 320, 50, 20, "全选"); + const inverseBtn = new RoundBtn(90, 320, 50, 20, "反选"); + const clearSelectBtn = new RoundBtn(150, 320, 60, 20, "全不选"); + const areaSelectBtn = new RoundBtn(220, 320, 76, 20, "区域选择"); + const getOutcomeBtn = new RoundBtn(306, 320, 76, 20, "统计所选"); + floorStatisticsList.push( + ["checkAll", checkAllBtn, () => { + floorStatisticsMenu.totalFloorList.forEach(floorId => { + core.markedFloorIds[floorId] = true; + }); + floorStatisticsMenu.drawContent(); + }], + ["inverse", inverseBtn, () => { + floorStatisticsMenu.totalFloorList.forEach(floorId => { + core.markedFloorIds[floorId] = !core.markedFloorIds[floorId]; + }); + floorStatisticsMenu.drawContent(); + }], + ["clearSelect", clearSelectBtn, () => { + floorStatisticsMenu.totalFloorList.forEach(floorId => { + core.markedFloorIds[floorId] = false; + }); + floorStatisticsMenu.drawContent(); + }], + ["areaSelect", areaSelectBtn, () => { + if (!floorStatisticsMenu.areaSelectOn) { + floorStatisticsMenu.areaSelectOn = true; + core.drawSuccessTip("请依次选择目标区域的起始楼层和终点楼层"); + } + else { + floorStatisticsMenu.areaSelectOn = true; + floorStatisticsMenu.areaBorder = ""; + core.drawFailTip("选择已取消"); + } + }], + ["getOutcome", getOutcomeBtn, () => { + const floorList = []; + for (let floorId in core.markedFloorIds) { + if (core.markedFloorIds[floorId]) floorList.push(floorId); + } + floorStatisticsTextMenu.floorList = floorStatisticsTextMenu.getFloorList(); + floorStatisticsTextMenu.stat = floorStatisticsTextMenu.getStat(); + statisticsMenu.changePage(1); + }]); + floorStatisticsMenu.registerBtns(floorStatisticsList); + + const config = { font: "14px Verdana", selectedFillStyle: "#5B8DEF" }; + const toolStatBtn = new RoundBtn(20, 90, 90, 20, "消耗品和门", config); + const itemStatBtn = new RoundBtn(125, 90, 70, 20, "血瓶宝石", config); + const equipStatBtn = new RoundBtn(215, 90, 40, 20, "装备", config); + const enemyStatBtn = new RoundBtn(275, 90, 40, 20, "敌人", config); + const damageStatBtn = new RoundBtn(335, 90, 40, 20, "伤害", config); + const itemUpBtn = new ArrowBtn(20, 335, 16, 16, "down"); + const itemDownBtn = new ArrowBtn(40, 335, 16, 16, "up"); + const floorDownBtn = new ArrowBtn(360, 335, 16, 16, "left"); + const floorUpBtn = new ArrowBtn(380, 335, 16, 16, "right"); + + const changeMode = (btn, mode) => () => { + floorStatisticsTextMenu.btnMap.forEach(ele => { + ele.status = (btn === ele) ? "selected" : "none"; + }); + floorStatisticsTextMenu.mode = mode; + floorStatisticsTextMenu.itemPage = 0; + floorStatisticsTextMenu.drawContent(); + }; + + floorStatisticsTextMenu.registerBtns([ + ["toolStat", toolStatBtn, changeMode(toolStatBtn, "tools")], + ["itemStat", itemStatBtn, changeMode(itemStatBtn, "items")], + ["equipStat", equipStatBtn, changeMode(equipStatBtn, "equips")], + ["enemyStat", enemyStatBtn, changeMode(enemyStatBtn, "enemys")], + ["damageStat", damageStatBtn, changeMode(damageStatBtn, "damage")], + ["itemDown", itemDownBtn, () => floorStatisticsTextMenu.itemPageDown()], + ["itemUp", itemUpBtn, () => floorStatisticsTextMenu.itemPageUp()], + ["floorDown", floorDownBtn, () => floorStatisticsTextMenu.floorPageDown()], + ["floorUp", floorUpBtn, () => floorStatisticsTextMenu.floorPageUp()], + ]); + toolStatBtn.status = "selected"; + + const exitBtn = new ExitBtn(380, 10, 24, 24); + const mainConfig = { fillStyle: "LightSteelBlue", selectedFillStyle: "blue" } // 120+80+8=208 + const floorBtn = new RoundBtn(120, 20, 80, 20, "楼层数据", mainConfig); + const playerBtn = new RoundBtn(216, 20, 80, 20, "玩家数据", mainConfig); + floorBtn.status = "selected"; + const exit = () => { + core.setFlag("__statistics__", null); + if (core.control.unregisterDymCanvasResizeEvent) { + const unregisterRedraw = (menu) => { + core.control.unregisterDymCanvasResizeEvent(menu.name); + } + unregisterRedraw(statisticsMenu); + unregisterRedraw(floorStatisticsMenu); + unregisterRedraw(floorStatisticsTextMenu); + unregisterRedraw(playerStatisticsMenu); + } + setTimeout(() => { + statisticsMenu.clear(); + core.unlockControl(); + }, 1); + } + statisticsMenu.registerBtns([ + ["exit", exitBtn, exit], + ["floor", floorBtn, () => { + floorBtn.status = "selected"; + playerBtn.status = "none"; + statisticsMenu.changePage(0); + statisticsMenu.drawButtonContent(); + }], + ["player", playerBtn, () => { + floorBtn.status = "none"; + playerBtn.status = "selected"; + statisticsMenu.changePage(2); + statisticsMenu.drawButtonContent(); + }], + ]); + if (core.control.registerDymCanvasResizeEvent) { + const registerRedraw = (menu) => { + core.control.registerDymCanvasResizeEvent(menu.name, function () { + if (menu.onDraw) menu.drawContent(); + }) + } + registerRedraw(statisticsMenu); + registerRedraw(floorStatisticsMenu); + registerRedraw(floorStatisticsTextMenu); + registerRedraw(playerStatisticsMenu); + } + return statisticsMenu; + } + + function _drawStatistics() { + core.ui.closePanel(); + core.control.lockControl(); + const staticsMenu = StatisticsMenuFactory(); + staticsMenu.init(); + staticsMenu.initOnePage(0); + } + core.ui._drawStatistics = _drawStatistics; + } } \ No newline at end of file diff --git a/runtime.d.ts b/runtime.d.ts index 29105fbe..0dbcee38 100644 --- a/runtime.d.ts +++ b/runtime.d.ts @@ -1,3651 +1,3664 @@ -/** - * @file runtime.d.ts 运行时的类型标注 - * @author 秋橙 & tocque - */ - -type direction = 'up' | 'down' | 'left' | 'right' -type move = 'forward' | direction -type loc = { direction: direction, x: number, y: number } -type rgbarray = [number, number, number, number] - -type Events = MotaAction[] | string - -type Block = { - disable: boolean, - x: number, - y: number, - id: number, - event: { - cls: string, - id: string, - [key: string]: any - } -} - -type frameObj = { - angle: number - index: number - mirror: number - opacity: number - x: number - y: number - zoom: number -} - -type CtxRefer = string | CanvasRenderingContext2D | HTMLCanvasElement | HTMLImageElement - -type Animate = { - frame: number - frames: frameObj[][] - images: HTMLImageElement[] - ratio: number - se: string -} - -type Floor = { - title: string, - ratio: number -} - -type FloorImage = { - canvas: 'bg' | 'auto' | 'fg' - name: string - x: number - y: number - reverse?: ':x' | ':y' | ':o' - disable?: boolean - sx?: number - sy?: number - w?: number - h?: number - frame?: number -} - -type ResolvedMap = { - floorId: string - afterBattle: { [x: string]: Events } - afterOpenDoor: { [x: string]: Events } - afterGetItem: { [x: string]: Events } - autoEvent: { [x: string]: autoEvent } - beforeBattle: { [x: string]: Events } - canFlyFrom: boolean - canFlyTo: boolean - canUseQuickShop: boolean - cannotMove: Object - cannotMoveIn: Object - cannotViewMap: boolean - changeFloor: { - [x: string]: { - floorId: ':before' | ':after' | ':now' | string - loc?: [number, number] - stair?: 'upFloor' | 'downFloor' | ':symmetry' | ':symmetry_x' | ':symmetry_y' | 'flyPoint' - direction?: 'left' | 'right' | 'up' | 'down' | ':left' | ':right' | ':back' | ':hero' | ':backhero' - time?: number - ignoreChangeFloor?: boolean - } - } - defaultGround: string - bgm: string | Array - bgmap: number[][] - /** 事件层 */ - map: number[][] - fgmap: number[][] - width: number - height: number - images: FloorImage[] - name: string - ratio: number - title: string - weather: [string, number] - blocks: Array - events: { [pos: string]: MotaAction[] }, - /** 砍层用? */ - deleted: boolean -} - -type Enemy = { - id: string - name: string - displayIdInBook: string - special: number | number[] - hp: number - atk: number - def: number - money: number - exp: number - point: number - [key: string]: any -} - -type Item = { - cls: string - id: string - name?: string - [key: string]: any -} - -type Save = { - -} - -type MotaAction = { - type: string, - [key: string]: any -} | string - -type SystemFlags = { - enableXxx: boolean - flyNearStair: boolean - flyAccessStair: boolean - steelDoorWithoutKey: boolean - betweenAttackMax: boolean - ignoreChangeFloor: boolean - disableShopOnDamage: boolean - blurFg: boolean -} - -type event = { type: string, [key: string]: any } - -type autoEvent = { - condition: string; - currentFloor: boolean; - data: MotaAction[]; - delayExecute: boolean; - floorId: string; - index: string; - multiExecute: boolean; - priority: number; - symbol: string; - x: number; - y: number; -} - -type step = 'up' | 'down' | 'left' | 'right' | 'forward' | 'backward' - -type HeroStatus = { - equipment: string[] - lv: number - name: string - hp: number - hpmax: number - mana: number - manamax: number - atk: number - def: number - mdef: number - money: number - exp: number - loc: { - direction: direction - x: number - y: number - } - items: { - keys: { [key: string]: number } - constants: { [key: string]: number } - tools: { [key: string]: number } - equips: { [key: string]: number } - } - flags: { [key: string]: any } - steps: number - statistics: { - battle: number - battleDamage: number - /** 每层累计受到的伤害 */ - damagePerFloor: { - [floorId: string]: { - battleDamage?: number; - extraDamage?: number - poisonDamage?: number; - vampireExtraLoss?: number; - } - } - currTime: number - exp: number - extraDamage: number - hp: number - ignoreSteps: number - money: number - moveDirectly: number - poisonDamage: number - vampireExtraLoss: number - start?: number - totalTime: number, - oneDefEffect: number, - oneMdefEffect: number, - } - [key: string]: any -} - -type shop = { - id: string, - text: string, - item: boolean, - mustEnable: boolean, - /** 能否预览 */disablePreview: boolean, - testInList: string, - choices: any[], - commonEvent: string, - [key: string]: any -} - -type gameStatus = { - played: boolean - gameOver: boolean - - /** 当前勇士状态信息。例如core.status.hero.atk就是当前勇士的攻击力数值 */ - hero: HeroStatus - - /** 当前层的floorId */ - floorId: string - /** 获得所有楼层的地图信息 */ - maps: { [key: string]: ResolvedMap } - /** 获得当前楼层信息,等价于core.status.maps[core.status.floorId] */ - thisMap: ResolvedMap - bgmaps: { [key: string]: number[][] } - fgmaps: { [key: string]: number[][] } - - /** 五图层插件增加的背景层2 */ - bg2maps: { [key: string]: number[][] } - /** 五图层插件增加的前景层2 */ - fg2maps: { [key: string]: number[][] } - - mapBlockObjs: { [key: string]: any } - /** 显伤伤害 */ - checkBlock: { - ambush: { [x: string]: [number, number, string, direction] } - repulse: { [x: string]: [number, number, string, direction] } - chase: { [x: string]: [number, number, string, direction] } - damage: { [x: string]: number } - needCache: boolean - type: { [x: string]: { [x: string]: boolean } } - cache: { - [s: string]: { - hp_buff: number - atk_buff: number - def_buff: number - guards: Array<[number, number, string]> - } - } - } - damage: { - posX: number - posY: number - data: Array<{ - text: string - px: number - py: number - color: string | Array - } - > - extraData: Array<{ - [x: string]: { - text: string - px: number - py: number - color: string | Array - alpha: number - } - }> - } - - lockControl: boolean - - /** 勇士移动状态 */ - heroMoving: number - heroStop: boolean - heroCenter: { - px: number - py: number - } - - // 自动寻路相关 - automaticRoute: { - autoHeroMove: boolean - autoStep: number - movedStep: number - destStep: number - destX: any - destY: any - offsetX: any - offsetY: any - autoStepRoutes: [] - moveStepBeforeStop: [] - lastDirection: any - cursorX: any - cursorY: any - moveDirectly: boolean - }, - - // 按下键的时间:为了判定双击 - downTime: number - ctrlDown: boolean - onShopLongDown: boolean - - // 路线&回放 - route: string[], - replay: { - replaying: boolean - pausing: boolean - /** 正在某段动画中 */animate: boolean - toReplay: string[] - totalList: string[] - speed: number - steps: number - save: [] - } - - // event事件 - shops: { - [shopName:string]:shop - } - event: { - id?: string | null - data: any - selection: any - ui: any - interval?: number | null - } - autoEvents: autoEvent[] - textAttribute: { - position: string - offset: number - title: rgbarray - background: rgbarray - text: rgbarray - titlefont: number - textfont: number - bold: boolean - time: number - letterSpacing: number - animateTime: number - }, - globalAttribute: { - equipName: string[] - statusLeftBackground: string - statusTopBackground: string - toolsBackground: string - borderColor: string - statusBarColor: string - floorChangingStyle: string - font: string - } - curtainColor: null - - // 动画 - globalAnimateObjs: [] - floorAnimateObjs: [] - boxAnimateObjs: [] - autotileAnimateObjs: [] - globalAnimateStatus: number - animateObjs: [] - - /** "新版道具栏"插件的状态变量 */ - thisUIEventInfo: { - allItems: string[], - maxPage: number, - pageItems: string[], - maxItem: number, - page: number, select: { - id?: string, - type?: string, - action?: string - } - } - /** "新版道具栏"插件的状态变量 */ - thisEventClickArea: {} -} - -type editor = { - MAX_NUM: number; - bgmap: any[]; - brushMod: string; - config: Object; - core: Object; - currentFloorData: Object; - currentFloorId: string; - dom: { - canvas: string[] - maps: string[] - [x: string]: string[] | CanvasRenderingContext2D | HTMLCanvasElement | HTMLInputElement | HTMLOptionElement - } - fgmap: any[]; - file: Object; - fs: Object; - ids: any[]; - indexs: any[]; - info: Object; - isMobile: boolean; - layerMod: string; - loc: Object; - main: Object; - map: any[]; - mode: Object; - pos: Object; - printe: Function; - uifunctions: { [x: string]: Function } - uivalues: Object; - useCompress: boolean; - used_flags: Object; - version: string; - viewportLoc: Object; - widthsX: Object; -} - -/** @file control.js 主要用来进行游戏控制,比如行走控制、自动寻路、存读档等等游戏核心内容。 */ -interface control { - - _updateDamage_damage(floorId: string, onMap: boolean): void - _updateDamage_extraDamage(floorId: string, onMap: boolean): void - _bindRoutePush(): void - __replay_getTimeout(): number - /** 物品数据显示 */ - getItemDetail(floorId: string): void - - _replayAction_item(action: string): boolean - _replayAction_equip(action: string): boolean - _replayAction_unEquip(action: string): boolean - - /** - * 开启调试模式, 此模式下可以按Ctrl键进行穿墙, 并忽略一切事件。 - * 此模式下不可回放录像和上传成绩。 - */ - debug(): void - - /** - * 刷新状态栏和地图显伤 - * 2.9.1优化:非必须立刻执行的刷新(一般是伤害相关的除外)的延迟到下一动画帧执行 - * @param doNotCheckAutoEvents 是否不检查自动事件 - * @param immediate 是否立刻刷新,而非延迟到下一动画帧刷新 - */ - updateStatusBar(doNotCheckAutoEvents?: boolean, immediate?: boolean): void - - /** 删除某个flag/变量 */ - removeFlag(name: string): void - - /** 设置某个独立开关 */ - setSwitch(x: number, y: number, floorId: string, name: string, value: any): void - - /** 获得某个独立开关 */ - getSwitch(x: number, y: number, floorId: string, name: string, defaultValue: any): any - - /** 增加某个独立开关 */ - addSwitch(x: number, y: number, floorId: string, name: string, value: any): void - - /** 判定某个独立开关 */ - hasSwitch(x: number, y: number, floorId: string, name: string): boolean - - /** 删除独立开关 */ - removeSwitch(x: number, y: number, floorId: string, name: string): boolean - - /** 设置大地图的偏移量 */ - setGameCanvasTranslate(canvasId: string, x: number, y: number): void - - /** 更新大地图的可见区域 */ - updateViewport(): void - - /** 立刻聚集所有的跟随者 */ - gatherFollowers(): void - - /** 回放下一个操作 */ - replay(): void - - /** - * 进入标题画面 - * @example core.showStartAnimate(); // 重启游戏但不重置bgm - * @param noAnimate 可选,true表示不由黑屏淡入而是立即亮屏 - * @param callback 可选,完全亮屏后的回调函数 - */ - showStartAnimate(noAnimate?: boolean, callback?: () => void): void - - /** - * 淡出标题画面 - * @example core.hideStartAnimate(core.startGame); // 淡出标题画面并开始新游戏,跳过难度选择 - * @param callback 标题画面完全淡出后的回调函数 - */ - hideStartAnimate(callback?: () => void): void - - /** - * 半自动寻路,用于鼠标或手指拖动 - * @example core.setAutomaticRoute(0, 0, [{direction: "right", x: 4, y: 9}, {direction: "right", x: 5, y: 9}, {direction: "right", x: 6, y: 9}, {direction: "up", x: 6, y: 8}]); - * @param destX 鼠标或手指的起拖点横坐标 - * @param destY 鼠标或手指的起拖点纵坐标 - * @param stepPostfix 拖动轨迹的数组表示,每项为一步的方向和目标点。 - */ - setAutomaticRoute(destX: number, destY: number, stepPostfix: Array<{ direction: direction, x: number, y: number }>): void - - /** - * 连续行走 - * @example core.setAutoHeroMove([{direction: "up", step: 1}, {direction: "left", step: 3}, {direction: "right", step: 3}, {direction: "up", step: 9}]); // 上左左左右右右上9 - * @param steps 压缩的步伐数组,每项表示朝某方向走多少步 - */ - setAutoHeroMove(steps: Array<{ direction: direction, step: number }>): void - - /** - * 尝试前进一步,如果面前不可被踏入就会直接触发该点事件 - * @example core.moveAction(core.doAction); // 尝试前进一步,然后继续事件处理。常用于在事件流中让主角像自由行动时一样前进一步,可以照常触发moveOneStep(跑毒和计步)和面前的事件(包括但不限于阻激夹域捕) - * @param callback 走一步后的回调函数,可选 - */ - moveAction(callback?: () => void): void - - /** - * 连续前进,不撞南墙不回头 - * @example core.moveHero(); // 连续前进 - * @param direction 可选,如果设置了就会先转身到该方向 - * @param callback 可选,如果设置了就只走一步 - */ - moveHero(direction?: direction, callback?: () => void): void - - /** - * 等待主角停下 - * @example core.waitHeroToStop(core.vibrate); // 等待主角停下,然后视野左右抖动1秒 - * @param callback 主角停止后的回调函数 - */ - waitHeroToStop(callback?: () => void): void - - /** - * 主角转向并计入录像,不会导致跟随者聚集,会导致视野重置到以主角为中心 - * @example core.turnHero(); // 主角顺时针旋转90°,即单击主角或按下Z键的效果 - * @param direction 主角的新朝向,可为 up, down, left, right, :left, :right, :back 七种之一 - */ - turnHero(direction?: direction): void - - /** - * 尝试瞬移,如果该点有图块/事件/阻激夹域捕则会瞬移到它旁边再走一步(不可踏入的话当然还是触发该点事件),这一步的方向优先和瞬移前主角的朝向一致 - * @example core.tryMoveDirectly(6, 0); // 尝试瞬移到地图顶部的正中央,以样板0层为例,实际效果是瞬移到了上楼梯下面一格然后向上走一步并触发上楼事件 - * @param destX 目标点的横坐标 - * @param destY 目标点的纵坐标 - */ - tryMoveDirectly(destX: number, destY: number): void - - /** - * 绘制主角和跟随者并重置视野到以主角为中心 - * @example core.drawHero(); // 原地绘制主角的静止帧 - * @param status 绘制状态,一般用stop - * @param offset 相对主角逻辑位置的偏移量,不填视为无偏移 - * @param frame 绘制第几帧 - */ - drawHero(status?: 'stop' | 'leftFoot' | 'rightFoot' | 'midFoot', offset?: number, frame?: number): void - - /** - * 获取主角面前第n格的横坐标 - * @example core.closeDoor(core.nextX(), core.nextY(), 'yellowDoor', core.turnHero); // 在主角面前关上一扇黄门,然后主角顺时针旋转90° - * @param n 目标格与主角的距离,面前为正数,背后为负数,脚下为0,不填视为1 - */ - nextX(n?: number): number - - /** - * 获取主角面前第n格的纵坐标 - * @example core.jumpHero(core.nextX(2), core.nextY(2)); // 主角向前跃过一格,即跳跃靴道具的使用效果 - * @param n 目标格与主角的距离,面前为正数,背后为负数,脚下为0,不填视为1 - */ - nextY(n?: number): number - - /** - * 判定主角是否身处某个点的锯齿领域(取曼哈顿距离) - * @example core.nearHero(6, 6, 6); // 判定主角是否身处点(6,6)的半径为6的锯齿领域 - * @param x 领域的中心横坐标 - * @param y 领域的中心纵坐标 - * @param n 领域的半径,不填视为1 - */ - nearHero(x: number, y: number, n?: number): boolean - - /** - * 请不要直接使用该函数,请使用core.updateStatusBar()代替!重算并绘制地图显伤 - * @example core.updateDamage(); // 更新当前地图的显伤,绘制在显伤层(废话) - * @param floorId 地图id,不填视为当前地图。预览地图时填写 - * @param ctx 绘制到的画布,如果填写了就会画在该画布而不是显伤层 - */ - updateDamage(floorId?: string, ctx?: CanvasRenderingContext2D): void - - /** 仅重绘地图显伤 */ - drawDamage(ctx?: CanvasRenderingContext2D): void - - /** - * 设置主角的某个属性 - * @example core.setStatus('loc', {x : 0, y : 0, direction : 'up'}); // 设置主角位置为地图左上角,脸朝上 - * @param name 属性的英文名,其中'x'、'y'和'direction'会被特殊处理为 core.setHeroLoc(name, value),其他的('exp'被视为'exp')会直接对 core.status.hero[name] 赋值 - * @param value 属性的新值 - */ - setStatus(name: K, value: HeroStatus[K]): void - - /** - * 增减主角的某个属性,等价于core.setStatus(name, core.getStatus(name) + value) - * @example core.addStatus('name', '酱'); // 在主角的名字后加一个“酱”字 - * @param name 属性的英文名 - * @param value 属性的增量,请注意旧量和增量中只要有一个是字符串就会把两者连起来成为一个更长的字符串 - */ - addStatus(name: K, value: HeroStatus[K]): void - - /** - * 读取主角的某个属性,不包括百分比修正 - * @example core.getStatus('loc'); // 读取主角的坐标和朝向 - * @param name 属性的英文名,其中'x'、'y'和'direction'会被特殊处理为 core.getHeroLoc(name),其他的('exp'被视为'exp')会直接读取 core.status.hero[name] - * @returns 属性值 - */ - getStatus(name: K): HeroStatus[K] - - /** - * 计算主角的某个属性,包括百分比修正 - * @example core.getRealStatus('atk'); // 计算主角的攻击力,包括百分比修正。战斗使用的就是这个值 - * @param name 属性的英文名,请注意只能用于数值类属性哦,否则乘法会得到NaN - */ - getRealStatus(name: K): HeroStatus[K] - - /** 获得某个状态的名字 */ - getStatusLabel(name: K): string - - /** - * 设置主角某个属性的百分比修正倍率,初始值为1, - * 倍率存放在flag: '__'+name+'_buff__' 中 - * @example core.setBuff('atk', 0.5); // 主角能发挥出的攻击力减半 - * @param name 属性的英文名,请注意只能用于数值类属性哦,否则随后的乘法会得到NaN - * @param value 新的百分比修正倍率,不填(效果上)视为1 - */ - setBuff(name: K, value?: HeroStatus[K]): void - - /** - * 增减主角某个属性的百分比修正倍率,加减法叠加和抵消。等价于 core.setBuff(name, core.getBuff(name) + value) - * @example core.addBuff('atk', -0.1); // 主角获得一层“攻击力减一成”的负面效果 - * @param name 属性的英文名,请注意只能用于数值类属性哦,否则随后的乘法会得到NaN - * @param value 倍率的增量 - */ - addBuff(name: K, value: HeroStatus[K]): void - - /** - * 读取主角某个属性的百分比修正倍率,初始值为1 - * @example core.getBuff('atk'); // 主角当前能发挥出多大比例的攻击力 - * @param name 属性的英文名 - */ - getBuff(name: HeroStatus[K]): number - - /** - * 获得或移除毒衰咒效果 - * @param action 获得还是移除,'get'为获得,'remove'为移除 - * @param type 要获得或移除的毒衰咒效果 - */ - triggerDebuff(action: string, type: string | string[]): void - - /** - * 设置勇士位置 - * 值得注意的是,这句话虽然会使勇士改变位置,但并不会使界面重新绘制; - * 如需立刻重新绘制地图还需调用:core.clearMap('hero'); core.drawHero(); 来对界面进行更新。 - * @example core.setHeroLoc('x', 5) // 将勇士当前位置的横坐标设置为5。 - * @param name 要设置的坐标属性 - * @param value 新值 - * @param noGather 是否聚集跟随者 - */ - setHeroLoc(name: 'x' | 'y', value: number, noGather?: boolean): void - setHeroLoc(name: 'direction', value: direction, noGather?: boolean): void - - /** - * 读取主角的位置和/或朝向 - * @example core.getHeroLoc(); // 读取主角的位置和朝向 - * @param name 要读取横坐标还是纵坐标还是朝向还是都读取 - * @returns name ? core.status.hero.loc[name] : core.status.hero.loc - */ - getHeroLoc(): { x: number, y: number, direction: direction } - getHeroLoc(name: 'x' | 'y'): number - getHeroLoc(name: 'direction'): direction - - /** - * 根据级别的数字获取对应的名称,后者定义在全塔属性 - * @example core.getLvName(); // 获取主角当前级别的名称,如“下级佣兵” - * @param lv 级别的数字,不填则视为主角当前的级别 - * @returns 级别的名称,如果不存在就还是返回数字 - */ - getLvName(lv?: number): string | number - - /** - * 获得下次升级需要的经验值。 - * 升级扣除模式下会返回经验差值;非扣除模式下会返回总共需要的经验值。 - * 如果无法进行下次升级,返回null。 - */ - getNextLvUpNeed(): number - - /** - * 设置一个flag变量 - * @example core.setFlag('poison', true); // 令主角中毒 - * @param name 变量名,支持中文 - * @param value 变量的新值,不填或填null视为删除 - */ - setFlag(name: string, value?: any): void - - /** - * 增减一个flag变量,等价于 core.setFlag(name, core.getFlag(name, 0) + value) - * @example core.addFlag('hatred', 1); // 增加1点仇恨值 - * @param name 变量名,支持中文 - * @param value 变量的增量 - */ - addFlag(name: string, value: number | string): void - - /** - * 读取一个flag变量 - * @param name 变量名,支持中文 - * @param defaultValue 当变量不存在时的返回值,可选(事件流中默认填0)。 - * @returns flags[name] ?? defaultValue - */ - getFlag(name: string, defaultValue?: any): any - - /** - * 判定一个flag变量是否存在且不为false、0、''、null、undefined和NaN - * @example core.hasFlag('poison'); // 判断主角当前是否中毒 - * @param name 变量名,支持中文 - * @returns !!core.getFlag(name) - */ - hasFlag(name: string): boolean - - /** - * 设置天气,不计入存档。如需长期生效请使用core.events._action_setWeather()函数 - * @example core.setWeather('fog', 10); // 设置十级大雾天 - * @param type 新天气的类型,不填视为无天气 - * @param level 新天气(晴天除外)的级别,必须为不大于10的正整数,不填视为5 - */ - setWeather(type?: 'rain' | 'snow' | 'sun' | 'fog' | 'cloud' | string, level?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10): void - - /** 注册一个天气 */ - registerWeather(name: string, initFunc: (level: number) => void, frameFunc?: (timestamp: number, level: number) => void): void - - /** 注销一个天气 */ - unregisterWeather(name: string): void; - - /** - * 更改画面色调,不计入存档。如需长期生效请使用core.events._action_setCurtain()函数 - * @example core.setCurtain(); // 恢复画面色调,用时四分之三秒 - * @param color 一行三列(第四列视为1)或一行四列(第四列若大于1则会被视为1,第四列若为负数则会被视为0)的颜色数组,不填视为[0, 0, 0, 0] - * @param time 渐变时间,单位为毫秒。不填视为750ms,负数视为0(无渐变,立即更改) - * @param callback 更改完毕后的回调函数,可选。事件流中常取core.doAction - */ - setCurtain(color?: [number, number, number, number?], time?: number, moveMode?: string, callback?: () => void): void - - /** - * 画面闪烁 - * @example core.screenFlash([255, 0, 0, 1], 3); // 红屏一闪而过 - * @param color 一行三列(第四列视为1)或一行四列(第四列若大于1则会被视为1,第四列若填负数则会被视为0)的颜色数组,必填 - * @param time 单次闪烁时长,实际闪烁效果为先花其三分之一的时间渐变到目标色调,再花剩余三分之二的时间渐变回去 - * @param times 闪烁的总次数,不填或填0都视为1 - * @param callback 闪烁全部完毕后的回调函数,可选 - */ - screenFlash(color: [number, number, number, number?], time: number, times?: number, moveMode?: string, callback?: () => void): void - - /** - * 播放背景音乐,中途开播但不计入存档且只会持续到下次场景切换。如需长期生效请将背景音乐的文件名赋值给flags.__bgm__ - * @example core.playBgm('bgm.mp3', 30); // 播放bgm.mp3,并跳过前半分钟 - * @param bgm 背景音乐的文件名,支持全塔属性中映射前的中文名 - * @param startTime 跳过前多少秒,不填则不跳过 - */ - playBgm(bgm: string, startTime?: number): void - - /** - * 注册一个 animationFrame - * @param name 名称,可用来作为注销使用 - * @param needPlaying 是否只在游戏运行时才执行(在标题界面不执行) - * @param func 要执行的函数,或插件中的函数名;可接受timestamp(从页面加载完毕到当前所经过的时间)作为参数 - */ - registerAnimationFrame(name: string, needPlaying: boolean, func?: (timestamp: number) => void): void - - /** 注销一个animationFrame */ - unregisterAnimationFrame(name: string): void - - /** 游戏是否已经开始 */ - isPlaying(): boolean - - /** 清除游戏状态和数据 */ - clearStatus(): void - - /** 清除自动寻路路线 */ - clearAutomaticRouteNode(x?: any, y?: any): void - - /** 停止自动寻路操作 */ - stopAutomaticRoute(): void - - /** 保存剩下的寻路,并停止 */ - saveAndStopAutomaticRoute(): void - - /** 继续剩下的自动寻路操作 */ - continueAutomaticRoute(): void - - /** 清空剩下的自动寻路列表 */ - clearContinueAutomaticRoute(callback?: () => any): void - - /** 设置行走的效果动画 */ - setHeroMoveInterval(callback?: () => any): void - - /** 每移动一格后执行的事件 */ - moveOneStep(callback?: () => any): void - - /** 当前是否正在移动 */ - isMoving(): boolean - - /** 瞬间移动 */ - moveDirectly(destX?: any, destY?: any, ignoreSteps?: any): void - - /** 改变勇士的不透明度 */ - setHeroOpacity(opacity?: number, moveMode?: string, time?: any, callback?: () => any): void - - /** 加减画布偏移 */ - addGameCanvasTranslate(x?: number, y?: number): void - - /** - * 设置视野范围 - * px,py: 左上角相对大地图的像素坐标,不需要为32倍数 - */ - setViewport(px?: number, py?: number): void - - /** 移动视野范围 */ - moveViewport(x: number, y: number, moveMode?: string, time?: number, callback?: () => any): void - - /** 更新跟随者坐标 */ - updateFollowers(): void - - /** 更新领域、夹击、阻击的伤害地图 */ - updateCheckBlock(floorId?: string): boolean - - /** 检查并执行领域、夹击、阻击事件 */ - checkBlock(): void - - /** 选择录像文件 */ - chooseReplayFile(): void - - /** 开始播放 */ - startReplay(list?: any): void - - /** 更改播放状态 */ - triggerReplay(): void - - /** 暂停播放 */ - pauseReplay(): void - - /** 恢复播放 */ - resumeReplay(): void - - /** 单步播放 */ - stepReplay(): void - - /** 加速播放 */ - speedUpReplay(): void - - /** 减速播放 */ - speedDownReplay(): void - - /** 设置播放速度 */ - setReplaySpeed(speed?: number): void - - /** 停止播放 */ - stopReplay(force?: boolean): void - - /** 回退 */ - rewindReplay(): void - - /** 是否正在播放录像 */ - isReplaying(): boolean - - /** - * 注册一个录像行为 - * @param name 自定义名称,可用于注销使用 - * @param func 具体执行录像的函数,可为一个函数或插件中的函数名; - * 需要接受一个action参数,代表录像回放时的下一个操作 - * func返回true代表成功处理了此录像行为,false代表没有处理此录像行为。 - */ - registerReplayAction(name: string, func: ((action: string) => boolean) | (() => boolean)): void - - /** 注销一个录像行为 */ - unregisterReplayAction(name: string): void - - /** 自动存档 */ - autosave(removeLast?: any): void - - /** 实际进行自动存档 */ - checkAutosave(): void - - /** 实际进行存读档事件 */ - doSL(id?: string, type?: any): void - - /** 同步存档到服务器 */ - syncSave(type?: any): void - - /** 从服务器加载存档 */ - syncLoad(): void - - /** 存档到本地 */ - saveData(): any - - /** 从本地读档 */ - loadData(data?: any, callback?: () => any): any - - /** 获得某个存档内容 */ - getSave(index?: any, callback?: () => any): any - - /** 获得某些存档内容 */ - getSaves(ids?: any, callback?: () => any): any - - /** 获得所有存档内容 */ - getAllSaves(callback?: () => any): any - - /** 获得所有存在存档的存档位 */ - getSaveIndexes(callback?: () => any): any - - /** 判断某个存档位是否存在存档 */ - hasSave(index?: number): boolean - - /** 删除某个存档 */ - removeSave(index?: number, callback?: () => any): void - - /** 从status中获得属性,如果不存在则从勇士属性中获取 */ - getStatusOrDefault(status?: any, name?: string): any - - /** 从status中获得实际属性(增幅后的),如果不存在则从勇士属性中获取 */ - getRealStatusOrDefault(status?: any, name?: string): any - - /** 获得勇士原始属性(无装备和衰弱影响) */ - getNakedStatus(name?: string): any - - /** 锁定用户控制,常常用于事件处理 */ - lockControl(): void - - /** 解锁用户控制 */ - unlockControl(): void - - /** 清空录像折叠信息 */ - clearRouteFolding(): void - - /** 检查录像折叠信息 */ - checkRouteFolding(): void - - /** 获得映射文件名 */ - getMappedName(name?: string): string - - /** 暂停背景音乐的播放 */ - pauseBgm(): void - - /** 恢复背景音乐的播放 */ - resumeBgm(resumeTime?: number): void - - /** 设置背景音乐的播放速度和音调 */ - setBgmSpeed(speed: number, usePitch?: boolean): void - - /** 设置音乐图标的显隐状态 */ - setMusicBtn(): void - - /** 开启或关闭背景音乐的播放 */ - triggerBgm(): void - - /** 播放一个音效 */ - playSound(sound: string, pitch?: number, callback?: () => any): number | undefined - - /** 停止(所有)音频 */ - stopSound(id?: number): void - - /** 获得正在播放的所有(指定)音效的id列表 */ - getPlayingSounds(name?: string): Array - - /** 检查bgm状态 */ - checkBgm(): void - - /** 设置屏幕放缩 */ - setDisplayScale(delta: number): void - - /** 清空状态栏 */ - clearStatusBar(): void - - /** 显示状态栏 */ - showStatusBar(): void - - /** 隐藏状态栏 */ - hideStatusBar(showToolbox?: boolean): void - - /** 更新状态栏的勇士图标 */ - updateHeroIcon(name: string): void - - /** 改变工具栏为按钮1-8 */ - setToolbarButton(useButton?: boolean): void - - /** - * 注册一个resize函数 - * @param name 名称,可供注销使用 - * @param func 可以是一个函数,或者是插件中的函数名;可以接受obj参数,详见resize函数。 - */ - registerResize(name: string, func: (obj: any) => void): void - - /** 注销一个resize函数 */ - unregisterResize(name: string): void - - /** 屏幕分辨率改变后重新自适应 - * @param type 重新设置指定类型的已注册resize事件 - */ - resize(type?:string): void - - /** - * 291改新增函数,在楼层数据统计中添加指定项目的值 - */ - addFloorStatistics(key: 'battleDamage' | 'poisonDamage' | 'extraDamage' | 'vampireExtraLoss', - value: number, floorId?: string): void -} - -/**@file events.js将处理所有和事件相关的操作。 */ -interface events { - /** 根据难度设置flag:hard */ - _startGame_setHard(): void - _eventMoveHero_moving(step: number, moveSteps: [direction | 'forward' | 'backward' | - 'leftup' | 'leftdown' | 'rightup' | 'rightdown', number][]): boolean - __action_checkReplaying(): boolean - _action_sleep(data: { time: number }, x?: undefined, y?: undefined, prefix?: undefined): void - _startGame_start(hard: string, seed: number, route: string, callback: Function): void - _startGame_afterStart(callback: Function): void - - /** - * 开始新游戏 - * @example core.startGame('咸鱼乱撞', 0, ''); // 开始一局咸鱼乱撞难度的新游戏,随机种子为0 - * @param hard 难度名,会显示在左下角(横屏)或右下角(竖屏) - * @param seed 随机种子,相同的种子保证了录像的可重复性 - * @param route 经由base64压缩后的录像,用于从头开始的录像回放 - * @param callback 回调函数,可选 - */ - startGame(hard: string, seed: number, route: string, callback?: () => void): void - - /** - * 游戏结束 - * @example core.gameOver(); // 游戏失败 - * @param ending 结局名,省略表示失败 - * @param fromReplay true表示在播放录像,可选 - * @param norank true表示不计入榜单,可选 - */ - gameOver(ending?: string, fromReplay?: boolean, norank?: boolean): void - - /** - * 战斗,如果填写了坐标就会删除该点的敌人并触发战后事件 - * @example core.battle('greenSlime'); // 和从天而降的绿头怪战斗(如果打得过) - * @param id 敌人id,必填 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param force true表示强制战斗,可选 - * @param callback 回调函数,可选 - */ - battle(id: string, x?: number, y?: number, force?: boolean, callback?: () => void): void - - /** - * 开门(包括三种基础墙) - * @example core.openDoor(0, 0, true, core.jumpHero); // 打开左上角的门,需要钥匙,然后主角原地跳跃半秒 - * @param x 门的横坐标 - * @param y 门的纵坐标 - * @param needKey true表示需要钥匙,会导致机关门打不开 - * @param callback 门完全打开后或打不开时的回调函数,可选 - */ - openDoor(x: number, y: number, needKey?: boolean, callback?: () => void): void - - /** - * 获得道具并提示,如果填写了坐标就会删除该点的该道具 - * @example core.getItem('book'); // 获得敌人手册并提示 - * @param id 道具id,必填 - * @param num 获得的数量,不填视为1,填了就别填坐标了 - * @param x 道具的横坐标,可选 - * @param y 道具的纵坐标,可选 - * @param callback 回调函数,可选 - */ - getItem(id: string, num?: number, x?: number, y?: number, callback?: () => void): void - - /** - * 场景切换 - * @example core.changeFloor('MT0'); // 传送到主塔0层,主角坐标和朝向不变,黑屏时间取用户设置值 - * @param floorId 传送的目标地图id,可以填':before'和':after'分别表示楼下或楼上 - * @param stair 传送的位置,可以填':now'(保持不变,可省略),':symmetry'(中心对称),':symmetry_x'(左右对称),':symmetry_y'(上下对称)或图块id(该图块最好在目标层唯一,一般为'downFloor'或'upFloor') - * @param heroLoc 传送的坐标(如果填写了,就会覆盖上述的粗略目标位置)和传送后主角的朝向(不填表示保持不变) - * @param time 传送的黑屏时间,单位为毫秒。不填为用户设置值 - * @param callback 黑屏结束后的回调函数,可选 - */ - changeFloor(floorId: string, stair?: string, heroLoc?: { x?: number, y?: number, direction?: direction }, time?: number, callback?: () => void): void - - /** - * 执行下一个事件指令,常作为回调 - * @example core.setCurtain([0,0,0,1], undefined, null, core.doAction); // 事件中的原生脚本,配合勾选“不自动执行下一个事件”来达到此改变色调只持续到下次场景切换的效果 - * @param keepUI true表示不清除UI画布和选择光标 - */ - doAction(keepUI?: true): void - - /** - * 插入一段事件;此项不可插入公共事件,请用 core.insertCommonEvent - * @example core.insertAction('一段文字'); // 插入一个显示文章 - * @param action 单个事件指令,或事件指令数组 - * @param x 新的当前点横坐标,可选 - * @param y 新的当前点纵坐标,可选 - * @param callback 新的回调函数,可选 - * @param addToLast 插入的位置,true表示插入到末尾,否则插入到开头 - */ - insertAction(action: Events | MotaAction, x?: number | null, y?: number | null, callback?: () => void, addToLast?: boolean | null): void - - /** - * 设置一项敌人属性并计入存档 - * @example core.setEnemy('greenSlime', 'def', 0); // 把绿头怪的防御设为0 - * @param id 敌人id - * @param name 属性的英文缩写 - * @param value 属性的新值,可选 - * @param operator 操作符,可选 - * @param prefix 独立开关前缀,一般不需要,下同 - */ - setEnemy(id: string, name: K, value?: Enemy[K], operator?: string, prefix?: string): void - - /** 设置某个点的敌人属性 */ - setEnemyOnPoint(x: number, y: number, floorId: string, name: K, value?: Enemy[K], operator?: string, prefix?: string): void - - /** 重置某个点的敌人属性 */ - resetEnemyOnPoint(x: number, y: number, floorId?: string): void - - /** 将某个点已经设置的敌人属性移动到其他点 */ - moveEnemyOnPoint(fromX: number, fromY: number, toX: number, toY: number, floorId?: string): void - - /** - * 设置一项楼层属性并刷新状态栏 - * @example core.setFloorInfo('ratio', 2, 'MT0'); // 把主塔0层的血瓶和宝石变为双倍效果 - * @param name 要求改的属性名 - * @param values 属性的新值 - * @param floorId 楼层id,不填视为当前层 - * @param prefix 独立开关前缀,一般不需要,下同 - */ - setFloorInfo(name: K, values?: Floor[K] | boolean | number | string | [number, number] | [string, number?] | Array, floorId?: string, prefix?: string): void - - /** - * 设置一个系统开关 - * @example core.setGlobalFlag('steelDoorWithoutKey', true); // 使全塔的所有铁门都不再需要钥匙就能打开 - * @param name 系统开关的英文名 - * @param value 开关的新值,您可以用!core.flags[name]简单地表示将此开关反转 - */ - setGlobalFlag(name: keyof SystemFlags, value: boolean): void - - /** - * 关门,目标点必须为空地 - * @example core.closeDoor(0, 0, 'yellowWall', core.jumpHero); // 在左上角关掉一堵黄墙,然后主角原地跳跃半秒 - * @param x 横坐标 - * @param y 纵坐标 - * @param id 门的id,也可以用三种基础墙 - * @param callback 门完全关上后的回调函数,可选 - */ - closeDoor(x: number, y: number, id: string, callback?: () => void): void - - /** - * 显示一张图片 - * @example core.showImage(1, core.material.images.images['winskin.png'], [0,0,128,128], [0,0,416,416], 0.5, 1000); // 裁剪winskin.png的最左边128×128px,放大到铺满整个视野,1秒内淡入到50%透明,编号为1 - * @param code 图片编号,为不大于50的正整数,加上100后就是对应画布层的z值,较大的会遮罩较小的,注意色调层的z值为125,UI层为140 - * @param image 图片文件名(可以是全塔属性中映射前的中文名)或图片对象(见上面的例子) - * @param sloc 一行且至多四列的数组,表示从原图裁剪的左上角坐标和宽高,可选 - * @param loc 一行且至多四列的数组,表示图片在视野中的左上角坐标和宽高,可选 - * @param opacityVal 不透明度,为小于1的正数。不填视为1 - * @param time 淡入时间,单位为毫秒。不填视为0 - * @param callback 图片完全显示出来后的回调函数,可选 - */ - showImage(code: number, image: string | HTMLImageElement, sloc?: Array, loc?: Array, opacityVal?: number, time?: number, callback?: () => void): void - - /** - * 隐藏一张图片 - * @example core.hideImage(1, 1000, core.jumpHero); // 1秒内淡出1号图片,然后主角原地跳跃半秒 - * @param code 图片编号 - * @param time 淡出时间,单位为毫秒 - * @param callback 图片完全消失后的回调函数,可选 - */ - hideImage(code: number, time?: number, callback?: () => void): void - - /** - * 移动一张图片并/或改变其透明度 - * @example core.moveImage(1, null, 0.5); // 1秒内把1号图片变为50%透明 - * @param code 图片编号 - * @param to 新的左上角坐标,省略表示原地改变透明度 - * @param opacityVal 新的透明度,省略表示不变 - * @param moveMode 移动模式 - * @param time 移动用时,单位为毫秒。不填视为1秒 - * @param callback 图片移动完毕后的回调函数,可选 - */ - moveImage(code: number, to?: [number?, number?], opacityVal?: number, moveMode?: string, time?: number, callback?: () => void): void - - /** - * 旋转一张图片 - * @param code 图片编号 - * @param center 旋转中心像素(以屏幕为基准);不填视为图片本身中心 - * @param angle 旋转角度;正数为顺时针,负数为逆时针 - * @param moveMode 旋转模式 - * @param time 移动用时,单位为毫秒。不填视为1秒 - * @param callback 图片移动完毕后的回调函数,可选 - */ - rotateImage(code: number, center?: [number?, number?], angle?: number, moveMode?: string, time?: number, callback?: () => void): void - - /** 放缩一张图片 */ - scaleImage(code: number, center?: [Number?, number?], scale?: number, moveMode?: string, time?: number, callback?: () => void): void - - /** - * 绘制一张动图或擦除所有动图 - * @example core.showGif(); // 擦除所有动图 - * @param name 动图文件名,可以是全塔属性中映射前的中文名 - * @param x 动图在视野中的左上角横坐标 - * @param y 动图在视野中的左上角纵坐标 - */ - showGif(name?: string, x?: number, y?: number): void - - /** - * 调节bgm的音量 - * @example core.setVolume(0, 100, core.jumpHero); // 0.1秒内淡出bgm,然后主角原地跳跃半秒 - * @param value 新的音量,为0或不大于1的正数。注意系统设置中是这个值的平方根的十倍 - * @param time 渐变用时,单位为毫秒。不填或小于100毫秒都视为0 - * @param callback 渐变完成后的回调函数,可选 - */ - setVolume(value: number, time?: number, callback?: () => void): void - - /** - * 视野抖动 - * @example core.vibrate(); // 视野左右抖动1秒 - * @param direction 抖动方向 - * @param time 抖动时长,单位为毫秒 - * @param speed 抖动速度 - * @param power 抖动幅度 - * @param callback 抖动平息后的回调函数,可选 - */ - vibrate(direction?: string, time?: number, speed?: number, power?: number, callback?: () => void): void - - /** - * 强制移动主角(包括后退),这个函数的作者已经看不懂这个函数了 - * @example core.eventMoveHero(['forward'], 125, core.jumpHero); // 主角强制前进一步,用时1/8秒,然后主角原地跳跃半秒 - * @param steps 步伐数组,注意后退时跟随者的行为会很难看 - * @param time 每步的用时,单位为毫秒。0或不填则取主角的移速,如果后者也不存在就取0.1秒 - * @param callback 移动完毕后的回调函数,可选 - */ - eventMoveHero(steps: step[], time?: number, callback?: () => void): void - - /** - * 主角跳跃,跳跃勇士。ex和ey为目标点的坐标,可以为null表示原地跳跃。time为总跳跃时间。 - * @example core.jumpHero(); // 主角原地跳跃半秒 - * @param ex 跳跃后的横坐标 - * @param ey 跳跃后的纵坐标 - * @param time 跳跃时长,单位为毫秒。不填视为半秒 - * @param callback 跳跃完毕后的回调函数,可选 - */ - jumpHero(ex?: number, ey?: number, time?: number, callback?: () => void): void - - /** - * 更改主角行走图 - * @example core.setHeroIcon('npc48.png', true); // 把主角从阳光变成样板0层左下角的小姐姐,但不立即刷新 - * @param name 新的行走图文件名,可以是全塔属性中映射前的中文名。映射后会被存入core.status.hero.image - * @param noDraw true表示不立即刷新(刷新会导致大地图下视野重置到以主角为中心) - */ - setHeroIcon(name: string, noDraw?: boolean): void - - /** - * 尝试使用一个道具 - * @example core.tryUseItem('pickaxe'); // 尝试使用破墙镐 - * @param itemId 道具id,其中敌人手册、传送器和飞行器会被特殊处理 - */ - tryUseItem(itemId: string): void - - /** 初始化游戏 */ - resetGame(hero?: HeroStatus, hard?: any, floorId?: string | null, maps?: any | null, values?: any | null): void - - /** 游戏获胜事件 */ - win(reason?: string, norank?: boolean, noexit?: boolean): void - - /** 游戏失败事件 */ - lose(reason?: string): void - - /** 重新开始游戏;此函数将回到标题页面 */ - restart(): void - - /** 询问是否需要重新开始 */ - confirmRestart(): void - - /** - * 注册一个系统事件 - * @param type 事件名 - * @param func 为事件的处理函数,可接受(data,callback)参数 - */ - registerSystemEvent(type: string, func: (data?: any, callback?: () => void) => void): void - - /** 注销一个系统事件 */ - unregisterSystemEvent(type: string): void - - /** 执行一个系统事件 */ - doSystemEvent(type: string, data?: any, callback?: () => any): void - - /** - * 触发(x,y)点的系统事件;会执行该点图块的script属性,同时支持战斗(会触发战后)、道具(会触发道具后)、楼层切换等等 - */ - trigger(x?: number, y?: number, callback?: () => any): void - - /** 战斗前触发的事件 */ - beforeBattle(enemyId?: string, x?: number, y?: number): void - - /** 战斗结束后触发的事件 */ - afterBattle(enemyId?: string, x?: number, y?: number): void - - /** 开一个门后触发的事件 */ - afterOpenDoor(doorId?: string, x?: number, y?: number): void - - /** 获得一个道具后的shij */ - afterGetItem(id?: string, x?: number, y?: number, isGentleClick?: boolean): void - - /** - * 轻按获得面前的物品或周围唯一物品 - * @param noRoute 若为true则不计入录像 - */ - getNextItem(noRoute?: boolean): void - - /** 楼层转换中 */ - changingFloor(floorId?: string, heroLoc?: any): void - - /** 转换楼层结束的事件 */ - afterChangeFloor(floorId?: string): void - - /** 是否到达过某个楼层 */ - hasVisitedFloor(floorId?: string): boolean - - /** 到达某楼层 */ - visitFloor(floorId?: string): void - - /** 推箱子 */ - pushBox(data?: any): void - - /** 推箱子后的事件 */ - afterPushBox(): void - - /** 当前是否在冰上 */ - onSki(number?: number): boolean - - /** - * 注册一个自定义事件 - * @param type 事件类型 - * @param func 事件的处理函数,可接受(data, x, y, prefix)参数 - * data为事件内容,x和y为当前点坐标(可为null),prefix为当前点前缀 - */ - registerEvent(type: string, func: (data: any, x?: number, y?: number, prefix?: string) => void): void - - /** 注销一个自定义事件 */ - unregisterEvent(type: string): void - - /** 执行一个自定义事件 */ - doEvent(data?: any, x?: number, y?: number, prefix?: any): void - - /** 直接设置事件列表 */ - setEvents(list?: any, x?: number, y?: number, callback?: () => any): void - - /** 开始执行一系列自定义事件 */ - startEvents(list?: any, x?: number, y?: number, callback?: () => any): void - - /** - * 插入一个公共事件 - * @example core.insertCommonEvent('加点事件', [3]); - * @param name 公共事件名;如果公共事件不存在则直接忽略 - * @param args 参数列表,为一个数组,将依次赋值给 flag:arg1, flag:arg2, ... - * @param x 新的当前点横坐标,可选 - * @param y 新的当前点纵坐标,可选 - * @param callback 新的回调函数,可选 - * @param addToLast 插入的位置,true表示插入到末尾,否则插入到开头 - */ - insertCommonEvent(name?: string, args?: any, x?: number, y?: number, callback?: () => any, addToLast?: boolean): void - - /** 获得一个公共事件 */ - getCommonEvent(name: string): any - - /** 恢复一个事件 */ - recoverEvents(data?: any): void - - /** 检测自动事件 */ - checkAutoEvents(): void - - /** 当前是否在执行某个自动事件 */ - autoEventExecuting(symbol?: string, value?: any): boolean - - /** 当前是否执行过某个自动事件 */ - autoEventExecuted(symbol?: string, value?: any): boolean - - /** 将当前点坐标入栈 */ - pushEventLoc(x?: number, y?: number, floorId?: string): boolean - - /** 将当前点坐标入栈 */ - popEventLoc(): any - - /** 预编辑事件 */ - precompile(data?: any): any - - /** 点击怪物手册时的打开操作 */ - openBook(fromUserAction?: boolean): void - - /** 点击楼层传送器时的打开操作 */ - useFly(fromUserAction?: boolean): void - - /** 飞往某一层 */ - flyTo(toId?: string, callback?: () => boolean): void - - /** 点击存档按钮时的打开操作 */ - save(fromUserAction?: boolean): void - - /** 点击读档按钮时的打开操作 */ - load(fromUserAction?: boolean): void - - /** 点击装备栏时的打开操作 */ - openEquipbox(fromUserAction?: boolean): void - - /** 点击工具栏时的打开操作 */ - openToolbox(fromUserAction?: boolean): void - - /** 点击快捷商店按钮时的打开操作 */ - openQuickShop(fromUserAction?: boolean): void - - /** 点击虚拟键盘时的打开操作 */ - openKeyBoard(fromUserAction?: boolean): void - - /** 点击设置按钮时的操作 */ - openSettings(fromUserAction?: boolean): void - - /** 当前是否有未处理完毕的异步事件(不包含动画和音效) */ - hasAsync(): boolean - - /** 立刻停止所有异步事件 */ - stopAsync(): void - - /** - * 跟随 - * @param name 要跟随的一个合法的4x4的行走图名称,需要在全塔属性注册 - */ - follow(name: string): void - - /** - * 取消跟随 - * @param name 取消跟随的行走图,不填则取消全部跟随者 - */ - unfollow(name?: string): void - - /** 数值操作 */ - setValue(name: string, operator: string, value: any, prefix?: string): void - - /** 数值增减 */ - addValue(name: string, value: any, prefix?: string): void - - /** 设置全塔属性 */ - setGlobalAttribute(name: string, value: any): void - - /** 设置剧情文本的属性 */ - setTextAttribute(data: any): void - - /** 清除对话框 */ - clearTextBox(code: number): void - - /** 移动对话框 */ - moveTextBox(code: number, loc: [number], relative: boolean, moveMode?: string, time?: number, callback?: () => any): void - - /** 设置文件别名 */ - setNameMap(name: string, value?: string): void - - /** 检查升级事件 */ - checkLvUp(): void -} - -/** @file actions.js 定义了玩家的操作控制 */ -interface actions { - SIZE: number - HSIZE: number - LAST: number - CHOICES_LEFT: number - CHOICES_RIGHT: number - _getChoicesTopIndex(length: number): number - _clickAction(x?: number, y?: number, px?: number, py?: number): number - /** 获得点击事件相对左上角的坐标 */ - _getClickLoc(x: number, y: number): { x: number, y: number, size: number } - _clickSwitchs_action_moveSpeed(delta: number): void - _clickSwitchs_action_floorChangeTime(delta: number): void - _clickSwitchs_display_setSize(delta: number): void - /** 高清画面开关 */_clickSwitchs_display_enableHDCanvas(): void - /** 定点怪显开关 */_clickSwitchs_display_enableEnemyPoint(): void - /** 怪物显伤开关 */_clickSwitchs_display_enemyDamage(): void - /** 临界显示开关 */_clickSwitchs_display_critical(): void - /** 领域显示开关 */_clickSwitchs_display_extraDamage(): void - /** 领域显示模式开关 */_clickSwitchs_display_extraDamageType(): void - /** 音乐开关 */_clickSwitchs_sounds_bgm(): void - /** 音效开关 */_clickSwitchs_sounds_se(): void - /** 音量调节开关 */_clickSwitchs_sounds_userVolume(delta: number): void - - _keyDownToolbox(keyCode: number): void - _keyUpToolbox(keyCode: number): void - _clickToolbox(x: number, y: number, px: number, py: number): void - _keyDownEquipbox(keyCode: number): void - _keyUpEquipbox(keyCode: number): void - _clickEquipbox(x: number, y: number, px: number, py: number): void - - /** - * 此函数将注册一个用户交互行为。 - * @param action 要注册的交互类型,如 ondown, onclick, keyDown 等等。 - * @param name 你的自定义名称,可被注销使用;同名重复注册将后者覆盖前者。 - * @param func 执行函数。 - * 如果func返回true,则不会再继续执行其他的交互函数;否则会继续执行其他的交互函数。 - * @param priority 优先级;优先级高的将会被执行。此项可不填,默认为0 - */ - registerAction(action: string, name: string, func: string | ((...params: any) => void), priority?: number): void - - /** 注销一个用户交互行为 */ - unregisterAction(action: string, name: string): void - - /** 执行一个用户交互行为 */ - doRegisteredAction(action: string, ...params: any): void - - /** 按下某个键时 */ - onkeyDown(e: KeyboardEvent): void - - /** 放开某个键时 */ - onkeyUp(e: KeyboardEvent): void - - /** 按住某个键时 */ - pressKey(keyCode: number): void - - /** 根据按下键的code来执行一系列操作 */ - keyDown(keyCode: number): void - - /** 根据放开键的code来执行一系列操作 */ - keyUp(keyCode: number, altKey?: boolean, fromReplay?: boolean): void - - /** 点击(触摸)事件按下时 */ - ondown(loc: number[]): void - - /** 当在触摸屏上滑动时 */ - onmove(loc: number[]): void - - /** 当点击(触摸)事件放开时 */ - onup(loc: number[]): void - - /** 具体点击屏幕上(x,y)点时,执行的操作 */ - onclick(x: number, y: number, px: number, py: number, stepPostfix?: any): void - - /** 滑动鼠标滚轮时的操作 */ - onmousewheel(direct: 1 | -1): void - - /** 长按Ctrl键时 */ - keyDownCtrl(): void - - /** 长按 */ - longClick(x: number, y: number, px: number, py: number, fromEvent?: boolean): void - - /** 点击自绘状态栏时 */ - onStatusBarClick(e?: MouseEvent): void -} - -/** @file enemys.js 定义了一系列和敌人相关的API函数。 */ -interface enemys { - - /** - * 判定某种特殊属性的有无 - * @example core.hasSpecial('greenSlime', 1) // 判定绿头怪有无先攻属性 - * @param special 敌人id或敌人对象或正整数数组或自然数 - * @param test 待检查的属性编号 - * @returns 若special为数组或数且含有test或相等、或special为敌人id或对象且具有此属性,则返回true - */ - hasSpecial(special: number | number[] | string | Enemy, test: number): boolean - - /** - * 获得某种敌人的全部特殊属性名称 - * @example core.getSpecialText('greenSlime') // ['先攻', '3连击', '破甲', '反击'] - * @param enemy 敌人id或敌人对象,如core.material.enemys.greenSlime - * @returns 字符串数组 - */ - getSpecialText(enemy: string | Enemy): string[] - - /** - * 获得某种敌人的某种特殊属性的介绍 - * @example core.getSpecialHint('bat', 1) // '先攻:怪物首先攻击' - * @param enemy 敌人id或敌人对象,用于确定属性的具体数值,否则可选 - * @param special 属性编号,可以是该敌人没有的属性 - * @returns 属性的介绍,以属性名加中文冒号开头 - */ - getSpecialHint(enemy: string | Enemy, special: number): string - - /** - * 获得某个敌人的某项属性值 - * @param enemy 敌人id或敌人对象 - * @param name 要获取的属性名,不填时获取完整的该敌人数据 - * @param x 敌人的横坐标,可选,填写时该点属性覆盖core.material.enemys中相应属性 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选。 - */ - getEnemyValue(enemy: string | Enemy, name?: string | null, x?: number, y?: number, floorId?: string): any - - /** - * 判定主角当前能否打败某只敌人 - * @example core.canBattle('greenSlime',0,0,'MT0') // 能否打败主塔0层左上角的绿头怪(假设有) - * @param enemy 敌人id或敌人对象 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns true表示可以打败,false表示无法打败 - */ - canBattle(enemy: string | Enemy, x?: number, y?: number, floorId?: string): boolean - - /** - * 获得某只敌人对主角的总伤害 - * @example core.getDamage('greenSlime',0,0,'MT0') // 绿头怪的总伤害 - * @param enemy 敌人id或敌人对象 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns 总伤害,如果因为没有破防或无敌怪等其他原因无法战斗,则返回null - */ - getDamage(enemy: string | Enemy, x?: number, y?: number, floorId?: string): number - - /** - * 获得某只敌人的地图显伤,包括颜色 - * @example core.getDamageString('greenSlime', 0, 0, 'MT0') // 绿头怪的地图显伤 - * @param enemy 敌人id或敌人对象 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns damage: 表示伤害值或为'???',color: 形如'#RrGgBb' - */ - getDamageString(enemy: string | Enemy, x?: number, y?: number, floorId?: string): { - damage: string, - color: string - } - - /** - * 获得某只敌人接下来的若干个临界及其减伤,算法基于useLoop开关选择回合法或二分法 - * @example core.nextCriticals('greenSlime', 9, 0, 0, 'MT0') // 绿头怪接下来的9个临界 - * @param enemy 敌人id或敌人对象 - * @param number 要计算的临界数量,可选,默认为1 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns 两列的二维数组,每行表示一个临界及其减伤 - */ - nextCriticals(enemy: string | Enemy, number?: number, x?: number, y?: number, floorId?: string): number[][] - - /** - * 计算再加若干点防御能使某只敌人对主角的总伤害降低多少 - * @example core.nextCriticals('greenSlime', 10, 0, 0, 'MT0') // 再加10点防御能使绿头怪的伤害降低多少 - * @param enemy 敌人id或敌人对象 - * @param k 假设主角增加的防御力,可选,默认为1 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns 总伤害的减少量 - */ - getDefDamage(enemy: string | Enemy, k?: number, x?: number, y?: number, floorId?: string): number - - /** - * 计算再加若干点护盾能使某只敌人对主角的总伤害降低多少 - * @example core.nextCriticals('greenSlime', 10, 0, 0, 'MT0') // 再加10点防御能使绿头怪的伤害降低多少 - * @param enemy 敌人id或敌人对象 - * @param k 假设主角增加的护盾,可选,默认为1 - * @param x 敌人的横坐标,可选 - * @param y 敌人的纵坐标,可选 - * @param floorId 敌人所在的地图,可选 - * @returns 总伤害的减少量 - */ - getMdefDamage(enemy: string | Enemy, k?: number, x?: number, y?: number, floorId?: string): number - - /** - * 计算和吸血怪物当前的战斗伤害比最低战斗伤害高多少。返回null说明打不过或怪物不吸血 - * @param {string|Enemy} enemy - * @param {number} [x] - * @param {number} [y] - * @param {string} [floorId] - * @param {{hp:number,atk:number,def:number,mdef:number}} [heroStatus] 未填时默认使用角色当前属性计算 - * @returns {null|number} - */ - getVampireExtraLoss(enemy: string | Enemy, x?: number, y?: number, - floorId?: string, heroStatus?: { hp: number, atk: number, def: number, mdef: number }) - - /** - * 获得某张地图的敌人集合,用于手册绘制 - * @example core.getCurrentEnemys('MT0') // 主塔0层的敌人集合 - * @param floorId 地图id,可选 - * @returns 敌人集合,按伤害升序排列,支持多朝向怪合并 - */ - getCurrentEnemys(floorId?: string): Enemy[] - - /** - * 检查某些楼层是否还有漏打的(某种)敌人 - * @example core.hasEnemyLeft('greenSlime', ['sample0', 'sample1']) // 样板0层和1层是否有漏打的绿头怪 - * @param enemyId 敌人id,可选,默认为任意敌人 - * @param floorId 地图id或其数组,可选,默认为当前地图 - * @returns true表示有敌人被漏打,false表示敌人已死光 - */ - hasEnemyLeft(enemyId?: string, floorId?: string | string[]): boolean - - /** 获得所有怪物原始数据的一个副本 */ - getEnemys(): any - - /** 获得所有特殊属性定义 */ - getSpecials(): [number, string | ((enemy: Enemy) => string), string | ((enemy: Enemy) => string), - string | [number, number, number, number?], number?][] - - /** 获得所有特殊属性的颜色 */ - getSpecialColor(enemy: string | Enemy): Array - - /** 获得所有特殊属性的额外标记 */ - getSpecialFlag(enemy: string | Enemy): Array - - /** 获得怪物真实属性 */ - getEnemyInfo(enemy: string | Enemy, hero?: any, x?: number, y?: number, floorId?: string): { - hp: number - def: number - atk: number - money: number - exp: number - point: number - special: number | number[] - guards: Array<[number, number, string]> - [x: string]: any - } - - /** 获得战斗伤害信息(实际伤害计算函数) */ - getDamageInfo(enemy: string | Enemy, hero?: any, x?: number, y?: number, floorId?: string): { - mon_hp: number - mon_atk: number - mon_def: number - init_damage: number - per_damage: number - hero_per_damage: number - turn: number - damage: number - [x: string]: any - } -} - -type mapsConfig = { - onMap: boolean, ctx?: CanvasRenderingContext2D, - redraw?: boolean, postDraw?: (() => void)[] -} - -/** @file maps.js负责一切和地图相关的处理内容 */ -interface maps { - _loadFloor_doNotCopy(): string[] - _getAndRemoveBlock(x: number, y: number): [Block, any] - _drawBg_draw(floorId: string, toDrawCtx: CanvasRenderingContext2D, cacheCtx: CanvasRenderingContext2D, - config: mapsConfig): void - _drawBgFgMap(floorId: string | null | undefined, name: string, config: mapsConfig): void - _drawBg_drawBackground(floorId: string, config: mapsConfig): void - /** 绘制楼层贴图 */ - _drawFloorImages(floorId: string | null | undefined, ctx: string | CanvasRenderingContext2D, name: string, - images?: FloorImage[] | null, currStatus?: number | null, onMap?: boolean | null): void - _drawFg_draw(floorId: string | null | undefined, toDrawCtx: CanvasRenderingContext2D, - cacheCtx: CanvasRenderingContext2D, config: mapsConfig): void - _getBgFgMapArray(name: string, floorId?: string | null, noCache?: boolean | null): any[] - _generateMovableArray_arrays(floorId: string): { bgArray: number[][], fgArray: number[][], eventArray: number[][] } - _drawFloorImage(ctx: string | CanvasRenderingContext2D, name: string, one: FloorImage, - image: any, currStatus: any, onMap: boolean) - - /** - * 获取初始core.maps.blockInfo的一个拷贝 - */ - getBlocksInfo(): { - [x: string]: { - cls: string - id: string - name: string - cannotIn: string[] - cannotOut: string[] - canPass: boolean - [x: string]: any - } - } - - /** - * 根据图块id得到数字(地图矩阵中的值) - * @example core.getNumberById('yellowWall'); // 1 - * @param id 图块id - * @returns 图块的数字,定义在project\maps.js(请注意和project\icons.js中的“图块索引”相区分!) - */ - getNumberById(id: string): number - - /** - * 生成事件层矩阵 - * @example core.getMapArray('MT0'); // 生成主塔0层的事件层矩阵,隐藏的图块视为0 - * @param floorId 地图id,不填视为当前地图 - * @param showDisable 可选,true表示隐藏的图块也会被表示出来 - * @returns 事件层矩阵,注意对其阵元的访问是[y][x] - */ - getMapArray(floorId?: string, noCache?: boolean): number[][] - - /** 判定图块的事件层数字;不存在为0 */ - getMapNumber(floorId?: string, noCache?: boolean): number - - /** - * 生成背景层矩阵 - * @example core.getBgMapArray('MT0'); // 生成主塔0层的背景层矩阵,使用缓存 - * @param floorId 地图id,不填视为当前地图 - * @param noCache 可选,true表示不使用缓存 - * @returns 背景层矩阵,注意对其阵元的访问是[y][x] - */ - getBgMapArray(floorId?: string, noCache?: boolean): number[][] - - /** - * 生成前景层矩阵 - * @example core.getFgMapArray('MT0'); // 生成主塔0层的前景层矩阵,使用缓存 - * @param floorId 地图id,不填视为当前地图 - * @param noCache 可选,true表示不使用缓存 - * @returns 前景层矩阵,注意对其阵元的访问是[y][x] - */ - getFgMapArray(floorId?: string, noCache?: boolean): number[][] - - /** - * 判定背景层的一个位置是什么 - * @example core.getBgNumber(); // 判断主角脚下的背景层图块的数字 - * @param x 横坐标,不填为当前勇士坐标 - * @param y 纵坐标,不填为当前勇士坐标 - * @param floorId 地图id,不填视为当前地图 - * @param 可选,true表示不使用缓存而强制重算 - */ - getBgNumber(x?: number, y?: number, floorId?: string, noCache?: boolean): number - - /** - * 判定前景层的一个位置是什么 - * @example core.getFgNumber(); // 判断主角脚下的前景层图块的数字 - * @param x 横坐标,不填为当前勇士坐标 - * @param y 纵坐标,不填为当前勇士坐标 - * @param floorId 地图id,不填视为当前地图 - * @param 可选,true表示不使用缓存而强制重算 - */ - getFgNumber(x?: number, y?: number, floorId?: string, noCache?: boolean): number - - /** - * 可通行性判定 - * @example core.generateMovableArray(); // 判断当前地图主角从各点能向何方向移动 - * @param floorId 地图id,不填视为当前地图 - * @returns 从各点可移动方向的三维数组 - */ - generateMovableArray(floorId?: string): Array>> - - /** - * 单点单朝向的可通行性判定 - * @exmaple core.canMoveHero(); // 判断主角是否可以前进一步 - * @param x 起点横坐标,不填视为主角当前的 - * @param y 起点纵坐标,不填视为主角当前的 - * @param direction 移动的方向,不填视为主角面对的方向 - * @param floorId 地图id,不填视为当前地图 - * @returns true表示可移动,false表示不可移动 - */ - canMoveHero(x?: number, y?: number, direction?: direction, floorId?: string): boolean - - /** - * 能否瞬移到某点,并求出节约的步数。 - * @example core.canMoveDirectly(0, 0); // 能否瞬移到地图左上角 - * @param destX 目标点的横坐标 - * @param destY 目标点的纵坐标 - * @returns 正数表示节约的步数,-1表示不可瞬移 - */ - canMoveDirectly(destX: number, destY: number): number - - /** - * 自动寻路 - * @example core.automaticRoute(0, 0); // 自动寻路到地图左上角 - * @param destX 目标点的横坐标 - * @param destY 目标点的纵坐标 - * @returns 每步走完后主角的loc属性组成的一维数组 - */ - automaticRoute(destX: number, destY: number): Array<{ direction: direction, x: number, y: number }> - - /** - * 地图绘制 - * @example core.drawMap(); // 绘制当前地图 - * @param floorId 地图id,省略表示当前楼层 - * @param callback 重绘完毕后的回调函数,可选 - */ - drawMap(floorId?: string, callback?: () => void): void - - /** - * 重绘地图 - */ - redrawMap(): void - - /** - * 绘制背景层(含贴图,其与背景层矩阵的绘制顺序可通过复写此函数来改变) - * @example core.drawBg(); // 绘制当前地图的背景层 - * @param floorId 地图id,不填视为当前地图 - * @param ctx 某画布的ctx,用于绘制缩略图,一般不需要 - */ - drawBg(floorId?: string, ctx?: CanvasRenderingContext2D): void - - /** - * 绘制事件层 - * @example core.drawEvents(); // 绘制当前地图的事件层 - * @param floorId 地图id,不填视为当前地图 - * @param blocks 一般不需要 - * @param ctx 某画布的ctx,用于绘制缩略图,一般不需要 - */ - drawEvents(floorId?: string, blocks?: Block[], ctx?: CanvasRenderingContext2D): void - - /** - * 绘制前景层(含贴图,其与前景层矩阵的绘制顺序可通过复写此函数来改变) - * @example core.drawFg(); // 绘制当前地图的前景层 - * @param floorId 地图id,不填视为当前地图 - * @param config - */ - drawFg(floorId?: string, config?: mapsConfig): void - - /** - * 绘制缩略图 - * @example core.drawThumbnail(); // 绘制当前地图的缩略图 - * @param floorId 地图id,不填视为当前地图 - * @param blocks 一般不需要 - * @param options 额外的绘制项,可选。可以增绘主角位置和朝向、采用不同于游戏中的主角行走图、增绘显伤、提供flags用于存读档 - */ - drawThumbnail(floorId?: string, blocks?: Block[], options?: { - heroLoc?: [number, number] - heroIcon?: string - /** 是否绘制显伤 */ - damage?: boolean - /** 存读档时使用,可以无视 */ - flags?: { [x: string]: any } - ctx?: CtxRefer - x?: number - y?: number - /** 绘制大小 */ - size?: number - /** 绘制全图 */ - all?: boolean - /** 绘制的视野中心 */ - centerX?: number - /** 绘制的视野中心 */ - centerY?: number - /** 存读档时使用,可以无视 */ - noHD: boolean - }): void - - /** - * 判定某个点是否不可被踏入(不基于主角生命值和图块cannotIn属性) - * @example core.noPass(0, 0); // 判断地图左上角能否被踏入 - * @param x 目标点的横坐标 - * @param y 目标点的纵坐标 - * @param floorId 目标点所在的地图id,不填视为当前地图 - * @returns true表示可踏入 - */ - noPass(x: number, y: number, floorId?: string): boolean - - /** - * 判定某个点的图块id - * @example if(core.getBlockId(x1, y1) != 'greenSlime' && core.getBlockId(x2, y2) != 'redSlime') core.openDoor(x3, y3); // 一个简单的机关门事件,打败或炸掉这一对绿头怪和红头怪就开门 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - * @param showDisable 隐藏点是否不返回null,true表示不返回null - * @returns 图块id,该点无图块则返回null - */ - getBlockId(x: number, y: number, floorId?: string, showDisable?: boolean): string | null - - /** 判定某个点的图块数字;空图块为0 */ - getBlockNumber(x: number, y: number, floorId?: string, showDisable?: boolean): number - - /** - * 判定某个点的图块类型 - * @example if(core.getBlockCls(x1, y1) != 'enemys' && core.getBlockCls(x2, y2) != 'enemy48') core.openDoor(x3, y3); // 另一个简单的机关门事件,打败或炸掉这一对不同身高的敌人就开门 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - * @param showDisable 隐藏点是否不返回null,true表示不返回null - * @returns 图块类型,即“地形、四帧动画、矮敌人、高敌人、道具、矮npc、高npc、自动元件、额外地形”之一 - */ - getBlockCls(x: number, y: number, floorId?: string, showDisable?: boolean): 'terrains' | 'animates' | 'enemys' | 'enemy48' | 'items' | 'npcs' | 'npc48' | 'autotile' | 'tileset' | null - - /** - * 搜索图块, 支持通配符 - * @example core.searchBlock('*Door'); // 搜索当前地图的所有门 - * @param id 图块id,支持星号表示任意多个(0个起)字符 - * @param floorId 地图id,不填视为当前地图 - * @param showDisable 隐藏点是否计入,true表示计入 - * @returns 一个详尽的数组,一般只用到其长度 - */ - searchBlock(id: string, floorId?: string | Array, showDisable?: boolean): Array<{ floorId: string, index: number, x: number, y: number, block: Block }> - - /** - * 根据给定的筛选函数搜索全部满足条件的图块 - * @example core.searchBlockWithFilter(function (block) { return block.event.id.endsWith('Door'); }); // 搜索当前地图的所有门 - * @param blockFilter 筛选函数,可接受block输入,应当返回一个boolean值 - * @param floorId 地图id,不填视为当前地图 - * @param showDisable 隐藏点是否计入,true表示计入 - * @returns 一个详尽的数组 - */ - searchBlockWithFilter(blockFilter: (block: Block) => boolean, floorId?: string | Array, showDisable?: boolean): Array<{ floorId: string, index: number, x: number, y: number, block: Block }> - - /** - * 显示(隐藏或显示的)图块,此函数将被“显示事件”指令和勾选了“不消失”的“移动/跳跃事件”指令(如阻击怪)的终点调用 - * @example core.showBlock(0, 0); // 显示地图左上角的图块 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - showBlock(x: number, y: number, floorId?: string): void - - /** - * 隐藏一个图块,对应于「隐藏事件」且不删除 - * @example core.hideBlock(0, 0); // 隐藏地图左上角的图块 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - hideBlock(x: number, y: number, floorId?: string): void - - /** - * 删除一个图块,对应于「隐藏事件」并同时删除 - * @example core.removeBlock(0, 0); // 尝试删除地图左上角的图块 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - removeBlock(x: number, y: number, floorId?: string): void - - /** - * 转变图块 - * @example core.setBlock(1, 0, 0); // 把地图左上角变成黄墙 - * @param number 新图块的数字(也支持纯数字字符串如'1')或id - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - setBlock(number: number | string, x: number, y: number, floorId?: string): void - - /** - * 设置某个点的不透明度 - * @param opacity 不透明度,范围0~1 - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - setBlockOpacity(opacity: number, x: number, y: number, floorId: string): void - - /** - * 批量替换图块 - * @example core.replaceBlock(21, 22, core.floorIds); // 把游戏中地上当前所有的黄钥匙都变成蓝钥匙 - * @param fromNumber 旧图块的数字 - * @param toNumber 新图块的数字 - * @param floorId 地图id或其数组,不填视为当前地图 - */ - replaceBlock(fromNumber: number, toNumber: number, floorId?: string | Array): void - - /** - * 转变图层块 - * @example core.setBgFgBlock('bg', 167, 6, 6); // 把当前地图背景层的中心块改为滑冰 - * @param name 背景还是前景 - * @param number 新图层块的数字(也支持纯数字字符串如'1')或id - * @param x 横坐标 - * @param y 纵坐标 - * @param floorId 地图id,不填视为当前地图 - */ - setBgFgBlock(name: 'bg' | 'fg', number: number | string, x: number, y: number, floorId?: string): void - - /** - * 改变事件层图块的连通性 - * @example core.setBlockConnectivity(165, 'noPass', true); - * @example core.setBlockConnectivity(162, 'cannotIn', ['left', 'right']); - * @param num 图层的数字或id - * @param statusName 要修改的图块属性的名称 - * @param statusValue 要修改到的值 - */ - setBlockConnectivity(num: number | string, statusName: 'noPass' | 'cannotIn' | 'cannotOut', statusValue: unknown): void - - /** - * 移动图块 - * @example core.moveBlock(0, 0, ['down']); // 令地图左上角的图块下移一格,用时半秒,再花半秒淡出 - * @param x 起点的横坐标 - * @param y 起点的纵坐标 - * @param steps 步伐数组 - * @param time 单步和淡出用时,单位为毫秒。不填视为半秒 - * @param keep 是否不淡出,true表示不淡出 - * @param callback 移动或淡出后的回调函数,可选 - */ - moveBlock(x: number, y: number, steps: step[], time?: number, keep?: boolean, callback?: () => void): void - - /** - * 跳跃图块;从V2.7开始不再有音效 - * @example core.jumpBlock(0, 0, 0, 0); // 令地图左上角的图块原地跳跃半秒,再花半秒淡出 - * @param sx 起点的横坐标 - * @param sy 起点的纵坐标 - * @param ex 终点的横坐标 - * @param ey 终点的纵坐标 - * @param time 单步和淡出用时,单位为毫秒。不填视为半秒 - * @param keep 是否不淡出,true表示不淡出 - * @param callback 落地或淡出后的回调函数,可选 - */ - jumpBlock(sx: number, sy: number, ex: number, ey: number, time?: number, keep?: boolean, callback?: () => void): void - - /** - * 播放动画,注意即使指定了主角的坐标也不会跟随主角移动,如有需要请使用core.drawHeroAnimate(name, callback)函数 - * @example core.drawAnimate('attack', core.nextX(), core.nextY(), false, core.vibrate); // 在主角面前一格播放普攻动画,动画停止后视野左右抖动1秒 - * @param name 动画文件名,不含后缀 - * @param x 绝对横坐标 - * @param y 绝对纵坐标 - * @param alignWindow 是否是相对窗口的坐标 - * @param callback 动画停止后的回调函数,可选 - * @returns 一个数字,可作为core.stopAnimate()的参数来立即停止播放(届时还可选择是否执行此次播放的回调函数) - */ - drawAnimate(name: string, x: number, y: number, alignWindow?: boolean, callback?: () => void): number - - /** - * 播放跟随勇士的动画 - * @param name 动画名 - * @param callback 动画停止后的回调函数,可选 - * @returns 一个数字,可作为core.stopAnimate()的参数来立即停止播放(届时还可选择是否执行此次播放的回调函数) - */ - drawHeroAnimate(name: string, callback?: () => void): number - - /** - * 立刻停止一个动画播放 - * @param id 播放动画的编号,即drawAnimate或drawHeroAnimate返回值 - * @param doCallback 是否执行该动画的回调函数 - */ - stopAnimate(id?: number, doCallback?: boolean): void - - /** 获得当前正在播放的所有(指定)动画的id列表 */ - getPlayingAnimates(name?: string): Array - - /** 加载某个楼层(从剧本或存档中) */ - loadFloor(floorId: string, map?: any): ResolvedMap - - /** 根据需求解析出blocks */ - extractBlocks(map?: any): void - - /** 根据需求为UI解析出blocks */ - extractBlocks(map?: any, flags?: any): void - - /** 根据数字获得图块 */ - getBlockByNumber(number: number): Block - - /** 根据ID获得图块 */ - getBlockById(id: string): Block - - /** 获得当前事件点的ID */ - getIdOfThis(id?: string): string - - /** 初始化一个图块 */ - initBlock(x?: number, y?: number, id?: string | number, addInfo?: boolean, eventFloor?: any): Block - - /** 压缩地图 */ - compressMap(mapArr?: any, floorId?: string): object - - /** 解压缩地图 */ - decompressMap(mapArr?: any, floorId?: string): object - - /** 将当前地图重新变成数字,以便于存档 */ - saveMap(floorId?: string): any - - /** 将存档中的地图信息重新读取出来 */ - loadMap(data?: any, floorId?: string, flags?: any): object - - /** 更改地图画布的尺寸 */ - resizeMap(floorId?: string): void - - /** 以x,y的形式返回每个点的事件 */ - getMapBlocksObj(floorId?: string, noCache?: boolean): object - - /** 获得某些点可否通行的信息 */ - canMoveDirectlyArray(locs?: any): object - - /** 绘制一个图块 */ - drawBlock(block?: any, animate?: any): void - - /** 生成groundPattern */ - generateGroundPattern(floorId?: string): void - - /** 某个点是否存在NPC */ - npcExists(x?: number, y?: number, floorId?: string): boolean - - /** 某个点是否存在(指定的)地形 */ - terrainExists(x?: number, y?: number, id?: string, floorId?: string): boolean - - /** 某个点是否存在楼梯 */ - stairExists(x?: number, y?: number, floorId?: string): boolean - - /** 当前位置是否在楼梯边;在楼传平面塔模式下对箭头也有效 */ - nearStair(): boolean - - /** 当前位置是否可以直接移动到某个楼梯附近的空地 */ - accessStair(): boolean - - /** 当前位置是否满足使用楼传的条件 */ - canUseFlyHere(): boolean - - /** 某个点是否存在(指定的)怪物 */ - enemyExists(x?: number, y?: number, id?: string, floorId?: string): boolean - - /** 获得某个点的block */ - getBlock(x?: number, y?: number, floorId?: string, showDisable?: boolean): Block - - /** 获得某个图块或素材的信息,包括ID,cls,图片,坐标,faceIds等等 */ - getBlockInfo(block?: any): any - - /** 获得某个图块对应行走图朝向向下的那一项的id;如果不存在行走图绑定则返回自身id */ - getFaceDownId(block?: any): string - - /** 根据图块的索引来隐藏图块 */ - hideBlockByIndex(index?: any, floorId?: string): void - - /** 一次性隐藏多个block */ - hideBlockByIndexes(indexes?: any, floorId?: string): void - - /** 根据block的索引(尽可能)删除该块 */ - removeBlockByIndex(index?: any, floorId?: string): void - - /** 一次性删除多个block */ - removeBlockByIndexes(indexes?: any, floorId?: string): void - - /** 显示前景/背景地图 */ - showBgFgMap(name?: string, loc?: any, floorId?: string, callback?: () => any): void - - /** 隐藏前景/背景地图 */ - hideBgFgMap(name?: string, loc?: any, floorId?: string, callback?: () => any): void - - /** 显示一个楼层贴图 */ - showFloorImage(loc?: any, floorId?: string, callback?: () => any): void - - /** 隐藏一个楼层贴图 */ - hideFloorImage(loc?: any, floorId?: string, callback?: () => any): void - - /** 动画形式转变某点图块 */ - animateSetBlock(number?: number | string, x?: number, y?: number, floorId?: string, time?: number, callback?: () => any): void - - /** 动画形式同时转变若干点图块 */ - animateSetBlocks(number?: number | string, locs?: any, floorId?: string, time?: number, callback?: () => any): void - - /** 事件转向 */ - turnBlock(direction?: string, x?: number, y?: number, floorId?: string): void - - /** 重置地图 */ - resetMap(floorId?: string | string[]): void - - /** 显示/隐藏某个块时的动画效果 */ - animateBlock(loc?: any, type?: any, time?: any, callback?: () => any): void - - /** 添加一个全局动画 */ - addGlobalAnimate(block?: any): void - - /** 删除一个或所有全局动画 */ - removeGlobalAnimate(x?: number, y?: number, name?: string): void - - /** 绘制UI层的box动画 */ - drawBoxAnimate(): void -} - -/** @file loader.js 主要负责资源的加载 */ -interface loader { - /** 加载一系列图片 */ - loadImages(dir: any, names: any, toSave: any, callback?: () => any): any - - /** 加载某一张图片 */ - loadImage(dir: any, imgName?: any, callback?: () => any): any - - /** 从zip中加载一系列图片 */ - loadImagesFromZip(url?: any, names?: any, toSave?: any, onprogress?: any, onfinished?: any): any - - /** 加载一个音乐 */ - loadOneMusic(name?: string): any - - /** 加载一个音效 */ - loadOneSound(name?: string): any - - /** 加载一个bgm */ - loadBgm(name?: string): any - - /** 释放一个bgm的缓存 */ - freeBgm(name?: string): any -} - -/** @file items.js 主要负责一切和道具相关的内容。 */ -interface items { - _loadEquipEffect(equipId?: string | null, unloadEquipId?: string | null); - - /** - * 获得给定ratio下(不填默认当前地图倍率)该即捡即用类的道具获得时勇士属性的增加效果 - * @example core.getItemEffectValue('redGem',1) // 获得倍率为1时红宝石增加的属性值 - * @param itemId 道具id - * @param ratio 倍率,不填默认当前地图倍率 - */ - getItemEffectValue(itemId: string, ratio?: number): { [key: string]: number } - - /** - * 即捡即用类的道具获得时的效果 - * @example core.getItemEffect('redPotion', 10) // 执行获得10瓶红血的效果 - * @param itemId 道具id - * @param itemNum 道具数量,可选,默认为1 - */ - getItemEffect(itemId: string, itemNum?: number): void - - /** - * 获取“即捡即用类”道具会增加哪些属性 - * @example core.getItemEffect('redPotion') // 返回红血增加的属性列表 - * @param itemId 道具id - */ - getItemEffectType(itemId: string): string[] - - /** - * 即捡即用类的道具获得时的额外提示 - * @example core.getItemEffectTip(redPotion) // (获得 红血瓶)',生命+100' - * @param itemId 道具id - * @returns 图块属性itemEffectTip的内容 - */ - getItemEffectTip(itemId: string): string - - /** - * 使用一个道具 - * @example core.useItem('pickaxe', true) // 使用破墙镐,不计入录像,无回调 - * @param itemId 道具id - * @param noRoute 是否不计入录像,快捷键使用的请填true,否则可省略 - * @param callback 道具使用完毕或使用失败后的回调函数 - */ - useItem(itemId: string, noRoute?: boolean, callback?: () => void): void - - /** - * 检查能否使用某种道具 - * @example core.canUseItem('pickaxe') // 能否使用破墙镐 - * @param itemId 道具id - * @returns true表示可以使用 - */ - canUseItem(itemId: string): boolean - - /** - * 统计某种道具的持有量 - * @example core.itemCount('yellowKey') // 持有多少把黄钥匙 - * @param itemId 道具id - * @returns 该种道具的持有量,不包括已穿戴的装备 - */ - itemCount(itemId: string): number - - /** - * 检查主角是否持有某种道具(不包括已穿戴的装备) - * @example core.hasItem('yellowKey') // 主角是否持有黄钥匙 - * @param itemId 道具id - * @returns true表示持有 - */ - hasItem(itemId: string): boolean - - /** - * 检查主角是否穿戴着某件装备 - * @example core.hasEquip('sword5') // 主角是否装备了神圣剑 - * @param itemId 装备id - * @returns true表示已装备 - */ - hasEquip(itemId: string): boolean - - /** - * 检查主角某种类型的装备目前是什么 - * @example core.getEquip(1) // 主角目前装备了什么盾牌 - * @param equipType 装备类型,自然数 - * @returns 装备id,null表示未穿戴 - */ - getEquip(equipType: number): string - - /** - * 设置某种道具的持有量 - * @example core.setItem('yellowKey', 3) // 设置黄钥匙为3把 - * @param itemId 道具id - * @param itemNum 新的持有量,可选,自然数,默认为0 - */ - setItem(itemId: string, itemNum?: number): void - - /** - * 静默增减某种道具的持有量 不会更新游戏画面或是显示提示 - * @example core.addItem('yellowKey', -2) // 没收两把黄钥匙 - * @param itemId 道具id - * @param itemNum 增加量,负数表示没收 - */ - addItem(itemId: string, itemNum?: number): void - - /** - * 判定某件装备的类型 - * @example core.getEquipTypeById('shield5') // 1(盾牌) - * @param equipId 装备id - * @returns 类型编号,自然数 - */ - getEquipTypeById(equipId: string): number - - /** - * 检查能否穿上某件装备 - * @example core.canEquip('sword5', true) // 主角可以装备神圣剑吗,如果不能会有提示 - * @param equipId 装备id - * @param hint 无法穿上时是否提示(比如是因为未持有还是别的什么原因) - * @returns true表示可以穿上,false表示无法穿上 - */ - canEquip(equipId: string, hint: boolean): boolean - - /** - * 尝试穿上某件背包里的装备并提示 - * @example core.loadEquip('sword5') // 尝试装备上背包里的神圣剑,无回调 - * @param equipId 装备id - * @param callback 穿戴成功或失败后的回调函数 - */ - loadEquip(equipId: string, callback?: () => void): void - - /** - * 脱下某个类型的装备 - * @example core.unloadEquip(1) // 卸下盾牌,无回调 - * @param equipType 装备类型编号,自然数 - * @param callback 卸下装备后的回调函数 - */ - unloadEquip(equipType: number, callback?: () => void): void - - /** - * 比较两件(类型可不同)装备的优劣 - * @example core.compareEquipment('sword5', 'shield5') // 比较神圣剑和神圣盾的优劣 - * @param compareEquipId 装备甲的id - * @param beComparedEquipId 装备乙的id - * @returns 两装备的各属性差,甲减乙,0省略 - */ - compareEquipment(compareEquipId?: string | null, beComparedEquipId?: string | null): { [key: string]: number } - - /** - * 保存当前套装 - * @example core.quickSaveEquip(1) // 将当前套装保存为1号套装 - * @param index 套装编号,自然数 - */ - quickSaveEquip(index: number): void - - /** - * 快速换装 - * @example core.quickLoadEquip(1) // 快速换上1号套装 - * @param index 套装编号,自然数 - */ - quickLoadEquip(index: number): void - - /** 获得所有道具 */ - getItems(): void - - /** 删除某个物品 */ - removeItem(itemId?: string, itemNum?: number): void - - /** 根据类型获得一个可用的装备孔 */ - getEquipTypeByName(name?: string): number - - /** - * 设置某个装备的属性并计入存档 - * @example core.setEquip('sword1', 'value', 'atk', 300, '+='); // 设置铁剑的攻击力数值再加300 - * @param equipId 装备id - * @param valueType 增幅类型,只能是value(数值)或percentage(百分比) - * @param name 要修改的属性名称,如atk - * @param value 要修改到的属性数值 - * @param operator 操作符,可选,如+=表示在原始值上增加 - * @param prefix 独立开关前缀,一般不需要 - */ - setEquip(equipId: string, valueType: string, name: string, value: any, operator?: string, prefix?: string): void -} - -/** @file ui.js 主要用来进行UI窗口的绘制,如对话框、怪物手册、楼传器、存读档界面等等。*/ -interface ui { - _buildFont(fontSize?: number | string, bold?: boolean, italic?: boolean, font?: string): string - _createUIEvent(): void - _drawBook_drawName(index: number, enemy: Enemy, top: number, left: number, width: number): void - _drawBook_drawRow1(index: number, enemy: Enemy, top: number, left: number, width: number, position: number): void - _drawToolbox(index: number): void - _drawEquipbox(index: number): void - - /** - * 根据画布名找到一个画布的context;支持系统画布和自定义画布。如果不存在画布返回null。 - * 也可以传画布的context自身,则返回自己。 - */ - getContextByName(canvas: CtxRefer): CanvasRenderingContext2D - - /** - * 清空某个画布图层 - * name为画布名,可以是系统画布之一,也可以是任意自定义动态创建的画布名;还可以直接传画布的context本身。(下同) - * 如果name也可以是'all',若为all则为清空所有系统画布。 - */ - clearMap(name: CtxRefer): void - /** - * 清空某个画布图层的指定区域 - */ - clearMap(name: CtxRefer, x: number, y: number, w: number, h: number): void - - /** - * 在某个画布上绘制一段文字 - * @param text 要绘制的文本 - * @param style 绘制的样式 - * @param font 绘制的字体 - */ - fillText(name: CtxRefer, text: string | number, x: number, y: number, style?: string | null, font?: string | null, maxWidth?: number | null): void - - /** - * 在某个画布上绘制一个描黑边的文字 - * @param text 要绘制的文本 - * @param style 绘制的样式 - * @param strokeStyle 绘制的描边颜色 - * @param font 绘制的字体 - */ - fillBoldText(name: CtxRefer, text: string, x: number, y: number, style?: string, strokeStyle?: string, font?: string, maxWidth?: number): void - - /** - * 绘制一个矩形。style可选为绘制样式 - * @param style 绘制的样式 - * @param angle 旋转角度,弧度制 - */ - fillRect(name: CtxRefer, x: number, y: number, width: number, height: number, style?: string, angle?: number): void - - /** - * 绘制一个矩形的边框 - * @param style 绘制的样式 - */ - strokeRect(name: CtxRefer, x: number, y: number, width: number, height: number, style: string, lineWidth?: number, angle?: number): void - - /** - * 动态创建一个画布。name为要创建的画布名,如果已存在则会直接取用当前存在的。 - * x,y为创建的画布相对窗口左上角的像素坐标,width,height为创建的长宽。 - * zIndex为创建的纵向高度(关系到画布之间的覆盖),z值高的将覆盖z值低的;系统画布的z值可在个性化中查看。 - * 返回创建的画布的context,也可以通过core.dymCanvas[name]调用 - */ - createCanvas(name: string, x: number, y: number, width: number, height: number, zIndex: number): CanvasRenderingContext2D - - /** 重新定位一个自定义画布 */ - relocateCanvas(name: string, x: number, y: number, useDelta: boolean): void - - /** - * 重新设置一个自定义画布的大小 - * @param styleOnly 是否只修改style,而不修改元素上的长宽,如果是true,会出现模糊现象 - * @param isTempCanvas 是否是临时画布,如果填true,会将临时画布修改为高清画布 - */ - resizeCanvas(name: string, x?: number, y?: number, styleOnly?: boolean, isTempCanvas?: boolean): void - - /** 设置一个自定义画布的旋转角度 */ - rotateCanvas(name: string, angle: number, centerX?: number, centerY?: number): void - - /** 删除一个自定义画布 */ - deleteCanvas(name: string | ((name: string) => boolean)): void - - /** 清空所有的自定义画布 */ - deleteAllCanvas(): void - - /** - * 在一个画布上绘制图片 - * 后面的8个坐标参数与canvas的drawImage的八个参数完全相同。 - * 请查看 http://www.w3school.com.cn/html5/canvas_drawimage.asp 了解更多。 - * @param name 可以是系统画布之一,也可以是任意自定义动态创建的画布名 画布名称或者画布的context - * @param image 要绘制的图片,可以是一个全塔属性中定义的图片名(会从images中去获取),图片本身,或者一个画布。 - */ - drawImage(name: CtxRefer, - image: CanvasImageSource | string, dx: number, dy: number): void - drawImage(name: CtxRefer, - image: CanvasImageSource | string, dx: number, dy: number, dw: number, dh: number): void - drawImage(name: CtxRefer, - image: CanvasImageSource | string, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void - - /** 根据最大宽度自动缩小字体 */ - setFontForMaxWidth(name: string | CanvasRenderingContext2D, text: string, maxWidth: number, font?: any): string - - /** 在某个canvas上绘制一个圆角矩形 */ - fillRoundRect(name: string | CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, style?: string, angle?: number): void - - /** 在某个canvas上绘制一个圆角矩形的边框 */ - strokeRoundRect(name: string | CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, style?: string, lineWidth?: number, angle?: number): void - - /** 在某个canvas上绘制一个多边形 */ - fillPolygon(name: string | CanvasRenderingContext2D, nodes?: any, style?: string): void - - /** 在某个canvas上绘制一个多边形的边框 */ - strokePolygon(name: string | CanvasRenderingContext2D, nodes?: any, style?: string, lineWidth?: number): void - - /** 在某个canvas上绘制一个椭圆 */ - fillEllipse(name: string | CanvasRenderingContext2D, x: number, y: number, a: number, b: number, angle?: number, style?: any): void - - /** 在某个canvas上绘制一个圆 */ - fillCircle(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, style?: string): void - - /** 在某个canvas上绘制一个椭圆的边框 */ - strokeEllipse(name: string | CanvasRenderingContext2D, x: number, y: number, a: number, b: number, angle?: number, style?: string, lineWidth?: number): void - - /** 在某个canvas上绘制一个圆的边框 */ - strokeCircle(name: string | CanvasRenderingContext2D, x: number, y: number, r: any, style?: string, lineWidth?: number): void - - /** 在某个canvas上绘制一个扇形 */ - fillArc(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, start: number, end: number, style?: string): void - - /** 在某个canvas上绘制一段弧 */ - strokeArc(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, start: number, end: number, style?: string, lineWidth?: number): void - - /** 保存某个canvas状态 */ - saveCanvas(name: string | CanvasRenderingContext2D): void - - /** 加载某个canvas状态 */ - loadCanvas(name: string | CanvasRenderingContext2D): void - - /** 设置某个canvas的baseline */ - setTextBaseline(name: string | CanvasRenderingContext2D, baseline: 'alphabetic' | 'top' | 'hanging' | 'middle' | 'ideographic' | 'bottom'): void - - /** 字符串自动换行的分割 */ - splitLines(name: string | CanvasRenderingContext2D, text: string, maxWidth?: number, font?: string): void - - /** 在某个canvas上绘制一个图标 */ - drawIcon(name: string | CanvasRenderingContext2D, id: string, x: number, y: number, w?: number, h?: number, frame?: number): void - - /** 结束一切事件和绘制,关闭UI窗口,返回游戏进程 */ - closePanel(): void - - /** 清空UI层内容 */ - clearUI(): void - - /** - * 左上角绘制一段提示 - * @param text 要提示的文字内容,支持 ${} 语法 - * @param id 要绘制的图标ID - * @param frame 要绘制图标的第几帧 - */ - drawTip(text: string, id?: string, frame?: number): void - - /** - * 左上角绘制一段提示同时播放错误音效 - * @param text 要提示的文字内容,支持 ${} 语法 - * @param id 要绘制的图标ID - * @param frame 要绘制图标的第几帧 - */ - drawFailTip(text: string, id?: string, frame?: number): void - - /** - * 左上角绘制一段提示同时播放成功音效 - * @param text 要提示的文字内容,支持 ${} 语法 - * @param id 要绘制的图标ID - * @param frame 要绘制图标的第几帧 - */ - drawSuccessTip(text: string, id?: string, frame?: number): void - - /** 地图中间绘制一段文字 */ - drawText(contents: string, callback?: () => any): void - - /** 自绘选择光标 */ - drawUIEventSelector(code: number, background: string, x: number, y: number, w: number, h: number, z?: number): void - - /** 清除一个或多个选择光标 */ - clearUIEventSelector(code?: number | number[] | null): void - - /** 绘制一个确认框 */ - drawConfirmBox(text: string, yesCallback?: () => void, noCallback?: () => void): void - - /** 绘制WindowSkin */ - drawWindowSkin(background: any, ctx: string | CanvasRenderingContext2D, x: number, y: number, w: number, h: number, direction?: any, px?: any, py?: any): void - - /** 绘制一个背景图,可绘制winskin或纯色背景;支持小箭头绘制 */ - drawBackground(left: string, top: string, right: string, bottom: string, posInfo?: any): void - - /** - * 绘制一段文字到某个画布上面 - * @param ctx 要绘制到的画布 - * @param content 要绘制的内容;转义字符只允许保留 \n, \r[...], \i[...], \c[...], \d, \e - * @param config 绘制配置项,目前暂时包含如下内容(均为可选) - * left, top:起始点位置;maxWidth:单行最大宽度;color:默认颜色;align:左中右 - * fontSize:字体大小;lineHeight:行高;time:打字机间隔;font:默认字体名 - * @returns 绘制信息 - */ - drawTextContent(ctx: string | CanvasRenderingContext2D, content: string, config: { - left?: number - top?: number - maxWidth?: number - color?: number | string - align?: 'left' | 'center' | 'right' - fontSize: number - lineHeight?: number - time?: number - font?: string - letterSpacing?: number - bold?: boolean - italic?: boolean - }): any - - /** 获得某段文字的预计绘制高度;参见 drawTextContent */ - getTextContentHeight(content: string, config?: any): number - - /** 绘制一个对话框 */ - drawTextBox(content: string, config?: any): void - - /** 绘制滚动字幕 */ - drawScrollText(content: string, time: number, lineHeight?: number, callback?: () => any): void - - /** 文本图片化 */ - textImage(content: string, lineHeight?: number): any - - /** 绘制一个选项界面 */ - drawChoices(content: string, choices: any): void - - /** 绘制等待界面 */ - drawWaiting(text: string): void - - /** 绘制分页 */ - drawPagination(page?: any, totalPage?: any, y?: number): void - - /** 绘制怪物手册 */ - drawBook(index?: any): void - - /** 绘制楼层传送器 */ - drawFly(page?: any): void - - /** 获得所有应该在道具栏显示的某个类型道具 - * @param cls 道具类型 - * @param cls 是否显示隐藏的道具 - */ - getToolboxItems(cls: 'equips' | 'tools' | 'constants' | 'all', showHide?: boolean): string[] - - /** 绘制状态栏 */ - drawStatusBar(): void - - /** 绘制灯光效果 */ - drawLight(name: string | CanvasRenderingContext2D, color?: any, lights?: any, lightDec?: number): void - - /** 在某个canvas上绘制一条线 */ - drawLine(name: string | CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number, style?: string, lineWidth?: number): void - - /** 在某个canvas上绘制一个箭头 */ - drawArrow(name: string | CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number, style?: string, lineWidth?: number): void - - /** 设置某个canvas的文字字体 */ - setFont(name: string | CanvasRenderingContext2D, font: string): void - - /** 设置某个canvas的线宽度 */ - setLineWidth(name: string | CanvasRenderingContext2D, lineWidth: number): void - - /** 设置某个canvas的alpha值;返回设置之前画布的不透明度。 */ - setAlpha(name: string | CanvasRenderingContext2D, alpha: number): number - - /** 设置某个canvas的filter属性 */ - setFilter(name: string | CanvasRenderingContext2D, filter: any): void - - /** 设置某个canvas的透明度;尽量不要使用本函数,而是全部换成setAlpha实现 */ - setOpacity(name: string | CanvasRenderingContext2D, opacity: number): void - - /** 设置某个canvas的绘制属性(如颜色等) */ - setFillStyle(name: string | CanvasRenderingContext2D, style: string): void - - /** 设置某个canvas边框属性 */ - setStrokeStyle(name: string | CanvasRenderingContext2D, style: string): void - - /** 设置某个canvas的对齐 */ - setTextAlign(name: string | CanvasRenderingContext2D, align: string): void - - /** 计算某段文字的宽度 */ - calWidth(name: string | CanvasRenderingContext2D, text: string, font?: string): number -} - -/** 工具类 主要用来进行一些辅助函数的计算 */ -interface utils { - readonly scan: { - 'up': { 'x': 0, 'y': -1 }, - 'left': { 'x': -1, 'y': 0 }, - 'down': { 'x': 0, 'y': 1 }, - 'right': { 'x': 1, 'y': 0 } - }; - readonly scan2: { - 'up': { 'x': 0, 'y': -1 }, - 'left': { 'x': -1, 'y': 0 }, - 'down': { 'x': 0, 'y': 1 }, - 'right': { 'x': 1, 'y': 0 }, - 'leftup': { 'x': -1, 'y': -1 }, - 'leftdown': { 'x': -1, 'y': 1 }, - 'rightup': { 'x': 1, 'y': -1 }, - 'rightdown': { 'x': 1, 'y': 1 } - }; - /** 初始化种子 */ - __init_seed(): void - - /** - * 将一段文字中的${}(表达式)进行替换。 - * @example core.replaceText('衬衫的价格是${status:hp}镑${item:yellowKey}便士。'); // 把主角的生命值和持有的黄钥匙数量代入这句话 - * @param text 模板字符串,可以使用${}计算js表达式,支持“状态、物品、变量、独立开关、全局存储、图块id、图块类型、敌人数据、装备id”等量参与运算 - * @returns 替换完毕后的字符串 - */ - replaceText(text: string, prefix?: string): string - - /** - * 对一个表达式中的特殊规则进行替换,如status:xxx等。 - * @example core.replaceValue('衬衫的价格是${status:hp}镑${item:yellowKey}便士。'); // 把这两个冒号表达式替换为core.getStatus('hp')和core.itemCount('yellowKey')这样的函数调用 - * @param value 模板字符串,注意独立开关不会被替换 - * @returns 替换完毕后的字符串 - */ - replaceValue(value: string): string - - /** - * 计算一个表达式的值,支持status:xxx等的计算。 - * @example core.calValue('status:hp + status:def'); // 计算主角的生命值加防御力 - * @param value 待求值的表达式 - * @param prefix 独立开关前缀,一般可省略 - * @returns 求出的值 - */ - calValue(value: string, prefix?: string): any - - /** - * 将b(可以是另一个数组)插入数组a的开头,此函数用于弥补a.unshift(b)中b只能是单项的不足。 - * @example core.unshift(todo, {type: 'unfollow'}); // 在事件指令数组todo的开头插入“取消所有跟随者”指令 - * @param a 原数组 - * @param b 待插入的新首项或前缀数组 - * @returns 插入完毕后的新数组,它是改变原数组a本身得到的 - */ - unshift(a: any[], b: any): any[] - - /** - * 将b(可以是另一个数组)插入数组a的末尾,此函数用于弥补a.push(b)中b只能是单项的不足。 - * @example core.push(todo, {type: 'unfollow'}); // 在事件指令数组todo的末尾插入“取消所有跟随者”指令 - * @param a 原数组 - * @param b 待插入的新末项或后缀数组 - * @returns 插入完毕后的新数组,它是改变原数组a本身得到的 - */ - push(a: any[], b: any): any[] - - /** - * 设置一个全局存储,适用于global:xxx,录像播放时将忽略此函数。 - * @example core.setBlobal('一周目已通关', true); // 设置全局存储“一周目已通关”为true,方便二周目游戏中的新要素。 - * @param key 全局变量名称,支持中文 - * @param value 全局变量的新值,不填或null表示清除此全局存储 - */ - setGlobal(key: string, value?: any): void - - /** - * 读取一个全局存储,适用于global:xxx,支持录像。 - * @example if (core.getGlobal('一周目已通关', false) === true) core.getItem('dagger'); // 二周目游戏进行到此处时会获得一把屠龙匕首 - * @param key 全局变量名称,支持中文 - * @param defaultValue 可选,当此全局变量不存在或值为null、undefined时,用此值代替 - * @returns 全局变量的值 - */ - getGlobal(key: string, defaultValue?: any): any - - /** - * 深拷贝一个对象(函数将原样返回) - * @example core.clone(core.status.hero, (name, value) => (name == 'items' || typeof value == 'number'), false); // 深拷贝主角的属性和道具 - * @param data 待拷贝对象 - * @param filter 过滤器,可选,表示data为数组或对象时拷贝哪些项或属性,true表示拷贝 - * @param recursion 过滤器是否递归,可选。true表示过滤器也被递归 - * @returns 拷贝的结果,注意函数将原样返回 - */ - clone(data?: T, filter?: (name: string, value: any) => boolean, recursion?: boolean): T - - /** 深拷贝一个1D或2D的数组 */ - cloneArray(data?: Array | Array>): Array | Array> - - /** 将怪物特殊属性转为升序数组并去零 */ - parseSpecial(special: null | number | number[]): number[] - - /** 比较两个变量是否值相等 */ - deepEqual(x: unknown, y: unknown): boolean - - /** - * 等比例切分一张图片 - * @example core.splitImage(core.material.images.images['npc48.png'], 32, 48); // 把npc48.png切分成若干32×48px的小人 - * @param image 图片名(支持映射前的中文名)或图片对象(参见上面的例子),获取不到时返回[] - * @param width 子图的宽度,单位为像素。原图总宽度必须是其倍数,不填视为32 - * @param height 子图的高度,单位为像素。原图总高度必须是其倍数,不填视为正方形 - * @returns 子图组成的数组,在原图中呈先行后列,从左到右、从上到下排列。 - */ - splitImage(image?: string | HTMLImageElement, width?: number, height?: number): HTMLImageElement[] - - /** - * 大数字格式化,单位为10000的倍数(w,e,z,j,g),末尾四舍五入 - * @example core.formatBigNumber(123456789, false); // "12346w" - * @param x 原数字 - * @param onMap 可选,true表示用于地图显伤,结果总字符数最多为5,否则最多为6 - * @returns 格式化结果 - */ - formatBigNumber(x: number, onMap?: boolean | number): string - - /** 变速移动 */ - applyEasing(mode?: string): (number) => number; - - /** - * 颜色数组转十六进制 - * @example core.arrayToRGB([102, 204, 255]); // "#66ccff",加载画面的宣传色 - * @param color 一行三列的数组,各元素必须为不大于255的自然数 - * @returns 该颜色的十六进制表示,使用小写字母 - */ - arrayToRGB(color: [number, number, number]): string - - /** - * 颜色数组转字符串 - * @example core.arrayToRGBA([102, 204, 255]); // "rgba(102,204,255,1)" - * @param color 一行三列或一行四列的数组,前三个元素必须为不大于255的自然数。第四个元素(如果有)必须为0或不大于1的数字,第四个元素不填视为1 - * @returns 该颜色的字符串表示 - */ - arrayToRGBA(color: [number, number, number, number]): string - - /** - * 录像一压,其结果会被再次base64压缩 - * @example core.encodeRoute(core.status.route); // 一压当前录像 - * @param route 原始录像,自定义内容(不予压缩,原样写入)必须由0-9A-Za-z和下划线、冒号组成,所以中文和数组需要用JSON.stringify预处理再base64压缩才能交由一压 - * @returns 一压的结果 - */ - encodeRoute(route: string[]): string - - /** - * 录像解压的最后一步,即一压的逆过程 - * @example core.decodeRoute(core.encodeRoute(core.status.route)); // 一压当前录像再解压-_-| - * @param route 录像解压倒数第二步的结果,即一压的结果 - * @returns 原始录像 - */ - decodeRoute(route: string): string[] - - /** - * 判断一个值是否不为null,undefined和NaN - * @example core.isset(0/0); // false,因为0/0等于NaN - * @param v 待测值,可选 - * @returns false表示待测值为null、undefined、NaN或未填写,true表示为其他值。即!(v == null || v != v) - */ - isset(v?: any): boolean - - /** - * 判定一个数组是否为另一个数组的前缀,用于录像接续播放。请注意函数名没有大写字母 - * @example core.subarray(['ad', '米库', '小精灵', '小破草', '小艾'], ['ad', '米库', '小精灵']); // ['小破草', '小艾'] - * @param a 可能的母数组,不填或比b短将返回null - * @param b 可能的前缀,不填或比a长将返回null - * @returns 如果b不是a的前缀将返回null,否则将返回a去掉此前缀后的剩余数组 - */ - subarray(a?: any[], b?: any[]): any[] | null - - /** - * 判定array是不是一个数组,以及element是否在该数组中。 - * @param array 可能的数组,不为数组或不填将导致返回值为false - * @param element 待查找的元素 - * @returns 如果array为数组且具有element这项,就返回true,否则返回false - */ - inArray(array?: any, element?: any): boolean - - /** - * 将x限定在[a,b]区间内,注意a和b可交换 - * @example core.clamp(1200, 1, 1000); // 1000 - * @param x 原始值,!x为true时x一律视为0 - * @param a 下限值,大于b将导致与b交换 - * @param b 上限值,小于a将导致与a交换 - */ - clamp(x: number, a: number, b: number): number - - /** - * 填写非自绘状态栏 - * @example core.setStatusBarInnerHTML('hp', core.status.hero.hp, 'color: #66CCFF'); // 更新状态栏中的主角生命,使用加载画面的宣传色 - * @param name 状态栏项的名称,如'hp', 'atk', 'def'等。必须是core.statusBar中的一个合法项 - * @param value 要填写的内容,大数字会被格式化为至多6个字符,无中文的内容会被自动设为斜体 - * @param css 额外的css样式,可选。如更改颜色等 - */ - setStatusBarInnerHTML(name: string, value: any, css?: string): void - - /** - * 求字符串的国标码字节数,也可用于等宽字体下文本的宽度测算。请注意样板的默认字体Verdana不是等宽字体 - * @example core.strlen('无敌ad'); // 6 - * @param str 待测字符串 - * @returns 字符串的国标码字节数,每个汉字为2,每个ASCII字符为1 - */ - strlen(str: string): number - - /** - * 通配符匹配,用于搜索图块等批量处理。 - * @example core.playSound(core.matchWildcard('*Key', itemId) ? 'item.mp3' : 'door.mp3'); // 判断捡到的是钥匙还是别的道具,从而播放不同的音效 - * @param pattern 模式串,每个星号表示任意多个(0个起)字符 - * @param string 待测串 - * @returns true表示匹配成功,false表示匹配失败 - */ - matchWildcard(pattern: string, string: string): boolean - - /** - * base64加密 - * @example core.encodeBase64('If you found this note in a small wooden box with a heart on it'); // "SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0" - * @param str 明文 - * @returns 密文 - */ - encodeBase64(str: string): string - - /** - * base64解密 - * @example core.decodeBase64('SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0'); // "If you found this note in a small wooden box with a heart on it" - * @param str 密文 - * @returns 明文 - */ - decodeBase64(str: string): string - - /** - * 不支持SL的随机数 - * @exmaple 1 + core.rand(6); // 随机生成一个小于7的正整数,模拟骰子的效果 - * @param num 填正数表示生成小于num的随机自然数,否则生成小于1的随机正数 - * @returns 随机数,即使读档也不会改变结果 - */ - rand(num?: number): number - - /** - * 支持SL的随机数,并计入录像 - * @exmaple 1 + core.rand2(6); // 随机生成一个小于7的正整数,模拟骰子的效果 - * @param num 正整数,0或不填会被视为2147483648 - * @returns 属于 [0, num) 的随机数 - */ - rand2(num?: number): number - - /** - * 弹窗请求下载一个文本文件 - * @example core.download('route.txt', core.status.route); // 弹窗请求下载录像 - * @param filename 文件名 - * @param content 文件内容 - */ - download(filename: string, content: string | String[]): void - - /** - * 显示确认框,类似core.drawConfirmBox() - * @example core.myconfirm('重启游戏?', core.restart); // 弹窗询问玩家是否重启游戏 - * @param hint 弹窗的内容 - * @param yesCallback 确定后的回调函数 - * @param noCallback 取消后的回调函数,可选 - */ - myconfirm(hint: string, yesCallback: () => void, noCallback?: () => void): void - - /** - * 判定深层相等, 会逐层比较每个元素 - * @example core.same(['1', 2], ['1', 2]); // true - */ - same(a?: any, b?: any): boolean - - /** - * 尝试请求读取一个本地文件内容 [异步] - * @param success 成功后的回调 - * @param error 失败后的回调 - * @param readType 不设置则以文本读取,否则以DataUrl形式读取 - */ - readFile(success, error, readType): void - - /** - * 文件读取完毕后的内容处理 [异步] - * @param content - */ - readFileContent(content): void - - /** - * 尝试复制一段文本到剪切板。 - */ - copy(data: string): void - - /** - * 发送一个HTTP请求 [异步] - * @param type 请求类型 - * @param url 目标地址 - * @param formData 如果是POST请求则为表单数据 - * @param success 成功后的回调 - * @param error 失败后的回调 - * @param timeout 超时时间 - */ - http(type: 'GET' | 'POST', url: string, formData: FormData, success: () => void, error: () => void, - mimeType?: string | null, responseType?: XMLHttpRequestResponseType | null, - onprogress?: boolean | null, timeout?: number | null): void - - /** 获得浏览器唯一的guid */ - getGuid(): string - - /** 解压缩一个数据 */ - decompress(value: any): any - - /** 设置本地存储 */ - setLocalStorage(key: string, value?: any): void - - /** 获得本地存储 */ - getLocalStorage(key: string, defaultValue?: any): any - - /** 移除本地存储 */ - removeLocalStorage(key: string): void - - /** 往数据库写入一段数据 */ - setLocalForage(key: string, value?: any, successCallback?: () => void, errorCallback?: () => void): void - - /** 从数据库读出一段数据 */ - getLocalForage(key: string, defaultValue?: any, successCallback?: (data: any) => void, errorCallback?: () => void): void - - /** 移除数据库的数据 */ - removeLocalForage(key: string, successCallback?: () => void, errorCallback?: () => void): void - - /** 格式化日期为字符串 */ - formatDate(date: Date): string - - /** 格式化日期为最简字符串 */ - formatDate2(date: Date): string - - /** 格式化时间 */ - formatTime(time: number): string - - /** 两位数显示 */ - setTwoDigits(x: number): string - - /** 格式化文件大小 */ - formatSize(size: number): string - - /** 访问浏览器cookie */ - getCookie(name: string): string - - /** - * 计算应当转向某个方向 - * @param turn 转向的方向 - * @param direction 当前方向 - */ - turnDirection(turn: 'up' | 'down' | 'left' | 'right' | ':left' | ':right' | ':back', direction?: string): string - - /** 是否满足正则表达式 */ - matchRegex(pattern: string, string: string): string - - /** 让用户输入一段文字 - * @param hint 输入框的提示语 - * @param value 输入值为null时的默认值 - * @param callback 确认时的回调 - * @param failCallback 取消时的回调,不填则默认与确认时的回调相同 - */ - myprompt(hint: string, value?: string | null, callback?: (data: string) => any, failCallback?: (data: string) => any): void - - /** 动画显示某对象 */ - showWithAnimate(obj?: any, speed?: number, callback?: () => any): void - - /** 动画使某对象消失 */ - hideWithAnimate(obj?: any, speed?: number, callback?: () => any): void - - /** 解压一段内容 */ - unzip(blobOrUrl?: any, success?: (data: any) => void, error?: (error: string) => void, convertToText?: boolean, onprogress?: (loaded: number, total: number) => void): void -} - -/** 和图标相关的函数 */ -interface icons { - - /** 获得所有图标类型 */ - getIcons(): void - - /** 根据ID获得其类型 */ - getClsFromId(id?: string): string - - /** 获得所有图标的ID */ - getAllIconIds(): void - - /** 根据图块数字或ID获得所在的tileset和坐标信息 */ - getTilesetOffset(id?: string): void -} - -interface plugin { - // aniMap: Map - - /** 高级动画相关的类 */ - animate: { - Animation?: any, AnimationBase?: any, Ticker?: any, Transition?: any, bezier?: any, bezierPath?: any, circle?: any, - hyper?: any, inverseTrigo?: any, linear?: any, power?: any, shake?: any, sleep?: any, trigo?: any, - } - /** 该集合中的所有Ticker在跨层时需要被摧毁 */ - tickerSet: Set - - /** 打开一个道具商店 */ - openItemShop: (itemShopId: string) => void - /** 某个全局商店是否被访问过 */ - isShopVisited(id: string): boolean - - /** 砍层插件删除fromId到toId间的楼层 - * @example core.removeMaps("MT1", "MT300") 删除MT1~MT300之间的全部层 - * @example core.removeMaps("MT10") 只删除MT10层 - */ - removeMaps(fromId: string, toId?: string): void - - /** 砍层插件恢复fromId到toId间的楼层 - * @example core.resumeMaps("MT1", "MT300") 恢复MT1~MT300之间的全部层 - * @example core.resumeMaps("MT10") 只恢复MT10层 - */ - resumeMaps(fromId: string, toId?: string): void - - /** 多角色插件,初始化各角色属性 */ - initHeros(): void - /** 多角色插件,切换到另一角色 */ - changeHero(toHeroId?: number): void - - uiBase: { - KeyCodeEnum: { - BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, SpaceBar: 32, - PageUp: 33, PageDown: 34, Left: 37, Up: 38, Right: 39, - Down: 40, C: 67, Q: 81, T: 84, - } - ButtonBase: ButtonBase - RoundBtn: RoundBtn - IconBtn: IconBtn - ExitBtn: ExitBtn - MenuBase: MenuBase - Pagination: Pagination - } - - /** 设置菜单 */ - settingMenu: MenuBaseClass | undefined - - // core.plugin.xxx 可能是任意变量,任意签名和返回值的函数 - [x: string]: any -} - -/*** 用于绘制的基类 */ -/*** 按钮基类 */ -interface ButtonBase { - new(x: number, y: number, w: number, h: number): ButtonBaseClass; -} - -type posFunc = (x: number, y: number, px: number, py: number) => void; - -declare class ButtonBaseClass { - constructor(x: number, y: number, w: number, h: number); - /** 按钮判定区域左上角的X坐标 */ - x: number; - /** 按钮判定区域左上角的Y坐标 */ - y: number; - /** 按钮判定区域的宽度 */ - w: number; - /** 按钮判定区域的高度 */ - h: number; - /** 按钮是否失效。失效时将跳过绘制和监听。 */ - disable: boolean; - /** 按钮是否处于被选中的状态 */ - status: 'selected' | 'none'; - /** 按钮所在的菜单 */ - menu: MenuBaseClass - /** 按钮所在的画布 */ - ctx: string - /** 按钮在菜单中的索引 */ - key: string | number - /** - * 按钮的绘制方法 - * @virtual - **/ - draw(): void - /** 按钮被按下时触发的事件 */ - ondown: posFunc; - /** 鼠标悬停时触发的事件 */ - onmove: posFunc | undefined; - /** 按钮抬起时触发的事件 */ - onup: posFunc | undefined; - /** 当前点击坐标是否在该按钮的判定区范围内 */ - inRange(px, py): boolean; -} - -interface RoundBtn { - new(x: number, y: number, w: number, h: number, text: string, config?: any): RoundBtnClass; -} - -declare class RoundBtnClass extends ButtonBaseClass { - constructor(x: number, y: number, w: number, h: number, text: string, config?: any); - /** 按钮绘制的文字 */ - text: string - /** 按钮的绘制属性配置 */ - config: { - fillStyle?: string, strokeStyle?: string, fontStyle?: string, - selectedFillStyle?: string, selectedstrokeStyle?: string, selectedFontStyle?: string, - radius?: number, lineWidth?: number, angle?: number, font?: string - } -} - -interface IconBtn { - new(x: number, y: number, w: number, h: number, icon: string, config?: any): IconBtnClass; -} - -declare class IconBtnClass extends ButtonBaseClass { - constructor(x: number, y: number, w: number, h: number, icon: string, config?: any); - /** 按钮绘制的图标名称 */ - icon: string - /** 按钮的绘制属性配置 */ - config: { - fillStyle?: string, strokeStyle?: string, - radius?: number, lineWidth?: number, angle?: number, font?: number - } -} - -interface ExitBtn { - new(x: number, y: number, w: number, h: number, config?: any): ExitBtnClass; -} - -declare class ExitBtnClass extends ButtonBaseClass { - constructor(x: number, y: number, w: number, h: number, config?: any); - /** 按钮的绘制属性配置 */ - config: { - fillStyle?: string, strokeStyle?: string, lineStyle?: string - radius?: number, lineOffsetX?: number, lineWidthX?: number, - } -} - -interface MenuBase { - new(name: string, toListen: eventType[], - x?: number | null, y?: number | null, w?: number | null, h?: number | null, - zIndex?: number | null): MenuBaseClass; -} - -type eventType = 'ondown' | 'onmove' | 'onup' | 'keyDown' | 'keyUp' | 'onmousewheel'; - -/*** 菜单基类 */ -declare class MenuBaseClass { - /** - * @param name 该菜单使用的画布的名称 - */ - constructor(name: string, toListen: eventType[], - x?: number | null, y?: number | null, w?: number | null, h?: number | null, - zIndex?: number | null); - /** 该菜单画布左上角的x坐标 */ - x: number - /** 该菜单画布左上角的y坐标 */ - y: number - /** 该菜单画布的宽度 */ - w: number - /** 该菜单画布的高度 */ - h: number - /** 该菜单画布的z值 */ - zIndex: number - /** 该菜单使用的画布的名称 */ - name: string - /** 该画布是否正在被绘制 */ - onDraw: boolean - /** 该菜单的按钮列表 */ - btnMap: Map - - ondown(x: number, y: number, rawpx: number, rawpy: number): void - /** - * @virtual - * 点击菜单自身触发的事件 */ - ondownEvent(x: number, y: number, px: number, py: number): void - /** - * @virtual - * 点击菜单上的按钮触发的事件 */ - ondownBtnEvent(x: number, y: number, px: number, py: number): void - onmove(x: number, y: number, rawpx: number, rawpy: number): void - /** - * @virtual - * 鼠标在菜单自身上移动触发的事件 */ - onmoveEvent(x: number, y: number, px: number, py: number): void - /** - * @virtual - * 鼠标移动到按钮上触发的事件 */ - onmoveBtnEvent(x: number, y: number, px: number, py: number): void - onup(x: number, y: number, rawpx: number, rawpy: number): void - /** - * @virtual - * 当菜单自身被鼠标或手指放开时触发的事件 */ - onupEvent(x: number, y: number, px: number, py: number): void - /** - * @virtual - * 当菜单上的按钮被鼠标或手指放开时触发的事件 */ - onupBtnEvent(x: number, y: number, px: number, py: number): void - - /** 按键被按下时触发的事件 */ - keyDownEvent(keycode: number): void - /** 按键被放开时触发的事件 */ - keyUpEvent(keycode: number, altkey?: boolean, fromReplay?: boolean): void; - /** 鼠标滚轮滚动时触发的事件 */ - onMouseWheelEvent(direct: 1 | -1): void; - - /** 返回换算后的画布上的相对坐标 */ - convertCoordinate(px: number, py: number): [number, number] - /** 检查坐标是否在画布范围内 */ - isPosValid(px: number, py: number): boolean - - - /** 注册一个按钮的事件 */ - registerBtn(key: string | number, button: ButtonBaseClass, - event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }): void - /** 注册若干个按钮的事件 */ - registerBtns(btns: [key: string | number, button: ButtonBaseClass, - event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }][]): void - - /** 创建并返回本菜单的画布 */ - createCanvas(): CanvasRenderingContext2D - /** 绘制该菜单上的按钮 */ - drawButtonContent(): void - /** 绘制该菜单上的按钮(需要派生类自行绘制画布和填充内容) */ - drawContent(): void - /** 开始监听该菜单的各个事件 */ - beginListen(): void - /** 取消监听该菜单的各个事件 */ - endListen(): void - /** 擦除该菜单的画布 */ - remove(): void - /** 取消监听该菜单的各个事件, 并清除该菜单的画布 */ - clear(): void - /** 开始监听和绘制 */ - init(): void -} - -interface Pagination { - new(pageList: MenuBase[], currPage: number, name: string, toListen: eventType[], - x?: number, y?: number, w?: number, h?: number, zIndex?: number): PaginationClass -} - -declare class PaginationClass extends MenuBaseClass { - constructor(pageList: MenuBase[], currPage: number, name: string, toListen: eventType[], - x?: number, y?: number, w?: number, h?: number, zIndex?: number); - /** 页面列表 */ - pageList: MenuBase[] - /** 当前页码 */ - currPage: number - - initOnePage(index: number): void - /** 切换到指定的页码 */ - changePage(index: number): void - /** 向下翻页 */ - pageDown(): void - /** 向上翻页 */ - pageUp(): void - /** 清除当前页和分页本身 */ - clear(): void -} - -type CoreMixin = { - firstData: { [x: string]: any } - /** 全塔属性开关 */flags: { - statusBarItems: string[] - [flagName: string]: boolean | number - } - /** 全局数值 */values: - { - /** 全局帧动画时间 */animateSpeed: number; - floorChangeTime: number; - /** 勇士移速 */moveSpeed: number; - statusCanvasRowsOnMobile: number; - - redGem: number; - blueGem: number; - greenGem: number; - redPotion: number; - bluePotion: number; - yellowPotion: number; - greenPotion: number; - - breakArmor: number; - counterAttack: number; - hatred: number; - lavaDamage: number; - poisonDamage: number; - purify: number; - weakValue: number; - } - - /** 地图可视部分大小 */ - readonly __SIZE__: number; - /** 地图像素 */ - readonly __PIXELS__: number; - /** 地图像素的一半 */ - readonly __HALF_SIZE__: number; - - /** 地图长度,仅2.10有效 */ - readonly _PX_: number; - /** 地图宽度,仅2.10有效 */ - readonly _PY_: number; - - /** 游戏素材 */ - readonly material: { - readonly animates: { [key: string]: Animate }, - readonly images: {}, - readonly bgms: { [key: string]: HTMLAudioElement }, - readonly sounds: { [key: string]: HTMLAudioElement }, - readonly ground: CanvasRenderingContext2D - /** - * 怪物信息 - * @example core.material.enemys.greenSlime // 获得绿色史莱姆的属性数据 - */ - readonly enemys: { [key: string]: Enemy }, - /** 道具信息 */ - readonly items: { [key: string]: Item } - readonly icons: { - animates: { [blockName: string]: number }; - autotile: { [blockName: string]: number }; - enemy48: { [blockName: string]: number }; - enemys: { [blockName: string]: number }; - hero: { - [dir: string]: { - loc: number; - stop: number; - leftFoot: number; - rightFoot: number; - /** 插件"勇士4帧动画"定义的中间帧动画 */ - midFoot: number; - } - } & { height: number }, - items: { [blockName: string]: number }; - npc48: { [blockName: string]: number }; - npcs: { [blockName: string]: number }; - terrains: { [blockName: string]: number }; - } - } - readonly timeout: { - turnHeroTimeout: any, - onDownTimeout: any, - sleepTimeout: any, - } - readonly interval: { - heroMoveInterval: any, - onDownInterval: any, - } - readonly animateFrame: { - totalTime: number - totalTimeStart: number - globalAnimate: boolean, - globalTime: number - selectorTime: number - selectorUp: boolean, - animateTime: number - moveTime: number - lastLegTime: number - /** 插件"勇士4帧动画"开启时为number */ - leftLeg: number, - readonly weather: { - time: number - type: any - nodes: [], - data: any - fog: any, - cloud: any, - }, - readonly tips: { - time: number - offset: number - list: [], - lastSize: number - }, - readonly asyncId: {} - } - readonly musicStatus: { - audioContext: AudioContext, - /** 是否播放BGM */bgmStatus: boolean - /** 是否播放SE */soundStatus: boolean - /** 正在播放的BGM */playingBgm: string - /** 上次播放的bgm */lastBgm: string - gainNode: GainNode, - /** 正在播放的SE */playingSounds: {} - /** 音量 */volume: number - /** 缓存BGM内容 */cachedBgms: string[] - /** 缓存的bgm数量 */cachedBgmCount: number - /** 用户调节的音量 */userVolume: number - } - readonly platform: { - /** 是否http */isOnline: boolean - /** 是否是PC */isPC: boolean - /** 是否是Android */isAndroid: boolean - /** 是否是iOS */isIOS: boolean - string: string - /** 是否是微信 */isWeChat: boolean - /** 是否是QQ */isQQ: boolean - /** 是否是Chrome */isChrome: boolean - /** 是否支持复制到剪切板 */supportCopy: boolean - - fileInput: null - /** 是否支持FileReader */fileReader: null - /** 读取成功 */successCallback: null - /** 读取失败 */errorCallback: null - } - readonly dom: { [key: string]: HTMLElement } - /** dom样式 */ - readonly domStyle: { - ratio: number, - scale: number, - isVertical: boolean, - showStatusBar: boolean, - toolbarBtn: 'normal' | 'num' | 'replay' | 'hide', - toolsCount: number, - } - readonly bigmap: { - canvas: string[], - offsetX: number // in pixel - offsetY: number - posX: number - posY: number - width: number // map width and height - height: number - v2: boolean - threshold: number - extend: number - scale: number - tempCanvas: CanvasRenderingContext2D // A temp canvas for drawing - cacheCanvas: CanvasRenderingContext2D - } - readonly saves: { - saveIndex: number - /** 每次存读档后进行了几步操作,用于同步存档前的检查 */ - actionCount: number - readonly ids: { [key: number]: boolean } - autosave: { - data: Save[] - max: number - storage: true - time: number - now: number - updated: boolean - } - favorite: [] - readonly favoriteName: {} - } - readonly initStatus: gameStatus - readonly canvas: { [key: string]: CanvasRenderingContext2D } - readonly dymCanvas: { [key: string]: CanvasRenderingContext2D } - /** 游戏状态 */ - readonly status: gameStatus - - /** - * 获得所有楼层的信息 - * @example core.floors[core.status.floorId].events // 获得本楼层的所有自定义事件 - */ - readonly floors: { [key: string]: ResolvedMap } - readonly floorIds: string[] - /** 砍层插件使用的分区信息 */ - readonly floorPartitions: [string, string][] - - readonly control: control - readonly loader: loader - readonly events: events - readonly enemys: enemys - readonly items: items - readonly maps: maps - readonly ui: ui - readonly utils: utils - readonly icons: icons - readonly actions: actions - readonly plugin: plugin - readonly statusBar: Main['statusBar'] -} & control & events & loader & enemys & items & maps & ui & utils & icons & actions & plugin - -interface Main { - /** 是否在进行在线录像验证 */ - replayChecking: boolean - readonly core: CoreMixin - readonly dom: { [key: string]: HTMLElement } - /** 游戏版本,发布后会被随机,请勿使用该属性 */ - readonly version: string - readonly useCompress: boolean - readonly savePages: number - readonly mode: 'play' | 'editor' - readonly statusBar: { - images: { [x: string]: HTMLElement } - icons: { [x: string]: string | HTMLImageElement } - [x: string]: HTMLElement | object - } - readonly __VERSION__: string - readonly __VERSION_CODE__: number - - /** 输出内容(极不好用,建议换成console,我甚至不知道样板为什么会有这个东西)*/ - log(e: string | Error, error: boolean): void -} -declare let main: Main -declare let core: CoreMixin - -declare let flags: { [x: string]: any } -declare let hero: CoreMixin['status']['hero'] -declare let editor: editor - -declare let control: () => control -declare let events: () => events -declare let loader: () => loader -declare let enemys: () => enemys -declare let items: () => items -declare let maps: () => maps -declare let ui: () => ui -declare let utils: () => utils -declare let icons: () => icons -declare let actions: () => actions - +/** + * @file runtime.d.ts 运行时的类型标注 + * @author 秋橙 & tocque + */ + +type direction = 'up' | 'down' | 'left' | 'right' +type move = 'forward' | direction +type loc = { direction: direction, x: number, y: number } +type rgbarray = [number, number, number, number] + +type Events = MotaAction[] | string + +type Block = { + disable: boolean, + x: number, + y: number, + id: number, + event: { + cls: string, + id: string, + [key: string]: any + } +} + +type frameObj = { + angle: number + index: number + mirror: number + opacity: number + x: number + y: number + zoom: number +} + +type CtxRefer = string | CanvasRenderingContext2D | HTMLCanvasElement | HTMLImageElement + +type Animate = { + frame: number + frames: frameObj[][] + images: HTMLImageElement[] + ratio: number + se: string +} + +type Floor = { + title: string, + ratio: number +} + +type FloorImage = { + canvas: 'bg' | 'auto' | 'fg' + name: string + x: number + y: number + reverse?: ':x' | ':y' | ':o' + disable?: boolean + sx?: number + sy?: number + w?: number + h?: number + frame?: number +} + +type ResolvedMap = { + floorId: string + afterBattle: { [x: string]: Events } + afterOpenDoor: { [x: string]: Events } + afterGetItem: { [x: string]: Events } + autoEvent: { [x: string]: autoEvent } + beforeBattle: { [x: string]: Events } + canFlyFrom: boolean + canFlyTo: boolean + canUseQuickShop: boolean + cannotMove: Object + cannotMoveIn: Object + cannotViewMap: boolean + changeFloor: { + [x: string]: { + floorId: ':before' | ':after' | ':now' | string + loc?: [number, number] + stair?: 'upFloor' | 'downFloor' | ':symmetry' | ':symmetry_x' | ':symmetry_y' | 'flyPoint' + direction?: 'left' | 'right' | 'up' | 'down' | ':left' | ':right' | ':back' | ':hero' | ':backhero' + time?: number + ignoreChangeFloor?: boolean + } + } + defaultGround: string + bgm: string | Array + bgmap: number[][] + /** 事件层 */ + map: number[][] + fgmap: number[][] + width: number + height: number + images: FloorImage[] + name: string + ratio: number + title: string + weather: [string, number] + blocks: Array + events: { [pos: string]: MotaAction[] }, + /** 砍层用? */ + deleted: boolean +} + +type Enemy = { + id: string + name: string + displayIdInBook: string + special: number | number[] + hp: number + atk: number + def: number + money: number + exp: number + point: number + [key: string]: any +} + +type Item = { + cls: string + id: string + name?: string + [key: string]: any +} + +type Save = { + +} + +type MotaAction = { + type: string, + [key: string]: any +} | string + +type SystemFlags = { + enableXxx: boolean + flyNearStair: boolean + flyAccessStair: boolean + steelDoorWithoutKey: boolean + betweenAttackMax: boolean + ignoreChangeFloor: boolean + disableShopOnDamage: boolean + blurFg: boolean +} + +type event = { type: string, [key: string]: any } + +type autoEvent = { + condition: string; + currentFloor: boolean; + data: MotaAction[]; + delayExecute: boolean; + floorId: string; + index: string; + multiExecute: boolean; + priority: number; + symbol: string; + x: number; + y: number; +} + +type step = 'up' | 'down' | 'left' | 'right' | 'forward' | 'backward' + +type HeroStatus = { + equipment: string[] + lv: number + name: string + hp: number + hpmax: number + mana: number + manamax: number + atk: number + def: number + mdef: number + money: number + exp: number + loc: { + direction: direction + x: number + y: number + } + items: { + keys: { [key: string]: number } + constants: { [key: string]: number } + tools: { [key: string]: number } + equips: { [key: string]: number } + } + flags: { [key: string]: any } + steps: number + statistics: { + battle: number + battleDamage: number + /** 每层累计受到的伤害 */ + damagePerFloor: { + [floorId: string]: { + battleDamage?: number; + extraDamage?: number + poisonDamage?: number; + vampireExtraLoss?: number; + } + } + currTime: number + exp: number + extraDamage: number + hp: number + ignoreSteps: number + money: number + moveDirectly: number + poisonDamage: number + vampireExtraLoss: number + start?: number + totalTime: number, + oneDefEffect: number, + oneMdefEffect: number, + } + [key: string]: any +} + +type shop = { + id: string, + text: string, + item: boolean, + mustEnable: boolean, + /** 能否预览 */disablePreview: boolean, + testInList: string, + choices: any[], + commonEvent: string, + [key: string]: any +} + +type gameStatus = { + played: boolean + gameOver: boolean + + /** 当前勇士状态信息。例如core.status.hero.atk就是当前勇士的攻击力数值 */ + hero: HeroStatus + + /** 当前层的floorId */ + floorId: string + /** 获得所有楼层的地图信息 */ + maps: { [key: string]: ResolvedMap } + /** 获得当前楼层信息,等价于core.status.maps[core.status.floorId] */ + thisMap: ResolvedMap + bgmaps: { [key: string]: number[][] } + fgmaps: { [key: string]: number[][] } + + /** 五图层插件增加的背景层2 */ + bg2maps: { [key: string]: number[][] } + /** 五图层插件增加的前景层2 */ + fg2maps: { [key: string]: number[][] } + + mapBlockObjs: { [key: string]: any } + /** 显伤伤害 */ + checkBlock: { + ambush: { [x: string]: [number, number, string, direction] } + repulse: { [x: string]: [number, number, string, direction] } + chase: { [x: string]: [number, number, string, direction] } + damage: { [x: string]: number } + needCache: boolean + type: { [x: string]: { [x: string]: boolean } } + cache: { + [s: string]: { + hp_buff: number + atk_buff: number + def_buff: number + guards: Array<[number, number, string]> + } + } + } + damage: { + posX: number + posY: number + data: Array<{ + text: string + px: number + py: number + color: string | Array + } + > + extraData: Array<{ + [x: string]: { + text: string + px: number + py: number + color: string | Array + alpha: number + } + }> + } + + lockControl: boolean + + /** 勇士移动状态 */ + heroMoving: number + heroStop: boolean + heroCenter: { + px: number + py: number + } + + // 自动寻路相关 + automaticRoute: { + autoHeroMove: boolean + autoStep: number + movedStep: number + destStep: number + destX: any + destY: any + offsetX: any + offsetY: any + autoStepRoutes: [] + moveStepBeforeStop: [] + lastDirection: any + cursorX: any + cursorY: any + moveDirectly: boolean + }, + + // 按下键的时间:为了判定双击 + downTime: number + ctrlDown: boolean + onShopLongDown: boolean + + // 路线&回放 + route: string[], + replay: { + replaying: boolean + pausing: boolean + /** 正在某段动画中 */animate: boolean + toReplay: string[] + totalList: string[] + speed: number + steps: number + save: [] + } + + // event事件 + shops: { + [shopName:string]:shop + } + event: { + id?: string | null + data: any + selection: any + ui: any + interval?: number | null + } + autoEvents: autoEvent[] + textAttribute: { + position: string + offset: number + title: rgbarray + background: rgbarray + text: rgbarray + titlefont: number + textfont: number + bold: boolean + time: number + letterSpacing: number + animateTime: number + }, + globalAttribute: { + equipName: string[] + statusLeftBackground: string + statusTopBackground: string + toolsBackground: string + borderColor: string + statusBarColor: string + floorChangingStyle: string + font: string + } + curtainColor: null + + // 动画 + globalAnimateObjs: [] + floorAnimateObjs: [] + boxAnimateObjs: [] + autotileAnimateObjs: [] + globalAnimateStatus: number + animateObjs: [] + + /** "新版道具栏"插件的状态变量 */ + thisUIEventInfo: { + allItems: string[], + maxPage: number, + pageItems: string[], + maxItem: number, + page: number, select: { + id?: string, + type?: string, + action?: string + } + } + /** "新版道具栏"插件的状态变量 */ + thisEventClickArea: {} +} + +type editor = { + MAX_NUM: number; + bgmap: any[]; + brushMod: string; + config: Object; + core: Object; + currentFloorData: Object; + currentFloorId: string; + dom: { + canvas: string[] + maps: string[] + [x: string]: string[] | CanvasRenderingContext2D | HTMLCanvasElement | HTMLInputElement | HTMLOptionElement + } + fgmap: any[]; + file: Object; + fs: Object; + ids: any[]; + indexs: any[]; + info: Object; + isMobile: boolean; + layerMod: string; + loc: Object; + main: Object; + map: any[]; + mode: Object; + pos: Object; + printe: Function; + uifunctions: { [x: string]: Function } + uivalues: Object; + useCompress: boolean; + used_flags: Object; + version: string; + viewportLoc: Object; + widthsX: Object; +} + +/** @file control.js 主要用来进行游戏控制,比如行走控制、自动寻路、存读档等等游戏核心内容。 */ +interface control { + + _updateDamage_damage(floorId: string, onMap: boolean): void + _updateDamage_extraDamage(floorId: string, onMap: boolean): void + _bindRoutePush(): void + __replay_getTimeout(): number + /** 物品数据显示 */ + getItemDetail(floorId: string): void + + _replayAction_item(action: string): boolean + _replayAction_equip(action: string): boolean + _replayAction_unEquip(action: string): boolean + + /** + * 开启调试模式, 此模式下可以按Ctrl键进行穿墙, 并忽略一切事件。 + * 此模式下不可回放录像和上传成绩。 + */ + debug(): void + + /** + * 刷新状态栏和地图显伤 + * 2.9.1优化:非必须立刻执行的刷新(一般是伤害相关的除外)的延迟到下一动画帧执行 + * @param doNotCheckAutoEvents 是否不检查自动事件 + * @param immediate 是否立刻刷新,而非延迟到下一动画帧刷新 + */ + updateStatusBar(doNotCheckAutoEvents?: boolean, immediate?: boolean): void + + /** 删除某个flag/变量 */ + removeFlag(name: string): void + + /** 设置某个独立开关 */ + setSwitch(x: number, y: number, floorId: string, name: string, value: any): void + + /** 获得某个独立开关 */ + getSwitch(x: number, y: number, floorId: string, name: string, defaultValue: any): any + + /** 增加某个独立开关 */ + addSwitch(x: number, y: number, floorId: string, name: string, value: any): void + + /** 判定某个独立开关 */ + hasSwitch(x: number, y: number, floorId: string, name: string): boolean + + /** 删除独立开关 */ + removeSwitch(x: number, y: number, floorId: string, name: string): boolean + + /** 设置大地图的偏移量 */ + setGameCanvasTranslate(canvasId: string, x: number, y: number): void + + /** 更新大地图的可见区域 */ + updateViewport(): void + + /** 立刻聚集所有的跟随者 */ + gatherFollowers(): void + + /** 回放下一个操作 */ + replay(): void + + /** + * 进入标题画面 + * @example core.showStartAnimate(); // 重启游戏但不重置bgm + * @param noAnimate 可选,true表示不由黑屏淡入而是立即亮屏 + * @param callback 可选,完全亮屏后的回调函数 + */ + showStartAnimate(noAnimate?: boolean, callback?: () => void): void + + /** + * 淡出标题画面 + * @example core.hideStartAnimate(core.startGame); // 淡出标题画面并开始新游戏,跳过难度选择 + * @param callback 标题画面完全淡出后的回调函数 + */ + hideStartAnimate(callback?: () => void): void + + /** + * 半自动寻路,用于鼠标或手指拖动 + * @example core.setAutomaticRoute(0, 0, [{direction: "right", x: 4, y: 9}, {direction: "right", x: 5, y: 9}, {direction: "right", x: 6, y: 9}, {direction: "up", x: 6, y: 8}]); + * @param destX 鼠标或手指的起拖点横坐标 + * @param destY 鼠标或手指的起拖点纵坐标 + * @param stepPostfix 拖动轨迹的数组表示,每项为一步的方向和目标点。 + */ + setAutomaticRoute(destX: number, destY: number, stepPostfix: Array<{ direction: direction, x: number, y: number }>): void + + /** + * 连续行走 + * @example core.setAutoHeroMove([{direction: "up", step: 1}, {direction: "left", step: 3}, {direction: "right", step: 3}, {direction: "up", step: 9}]); // 上左左左右右右上9 + * @param steps 压缩的步伐数组,每项表示朝某方向走多少步 + */ + setAutoHeroMove(steps: Array<{ direction: direction, step: number }>): void + + /** + * 尝试前进一步,如果面前不可被踏入就会直接触发该点事件 + * @example core.moveAction(core.doAction); // 尝试前进一步,然后继续事件处理。常用于在事件流中让主角像自由行动时一样前进一步,可以照常触发moveOneStep(跑毒和计步)和面前的事件(包括但不限于阻激夹域捕) + * @param callback 走一步后的回调函数,可选 + */ + moveAction(callback?: () => void): void + + /** + * 连续前进,不撞南墙不回头 + * @example core.moveHero(); // 连续前进 + * @param direction 可选,如果设置了就会先转身到该方向 + * @param callback 可选,如果设置了就只走一步 + */ + moveHero(direction?: direction, callback?: () => void): void + + /** + * 等待主角停下 + * @example core.waitHeroToStop(core.vibrate); // 等待主角停下,然后视野左右抖动1秒 + * @param callback 主角停止后的回调函数 + */ + waitHeroToStop(callback?: () => void): void + + /** + * 主角转向并计入录像,不会导致跟随者聚集,会导致视野重置到以主角为中心 + * @example core.turnHero(); // 主角顺时针旋转90°,即单击主角或按下Z键的效果 + * @param direction 主角的新朝向,可为 up, down, left, right, :left, :right, :back 七种之一 + */ + turnHero(direction?: direction): void + + /** + * 尝试瞬移,如果该点有图块/事件/阻激夹域捕则会瞬移到它旁边再走一步(不可踏入的话当然还是触发该点事件),这一步的方向优先和瞬移前主角的朝向一致 + * @example core.tryMoveDirectly(6, 0); // 尝试瞬移到地图顶部的正中央,以样板0层为例,实际效果是瞬移到了上楼梯下面一格然后向上走一步并触发上楼事件 + * @param destX 目标点的横坐标 + * @param destY 目标点的纵坐标 + */ + tryMoveDirectly(destX: number, destY: number): void + + /** + * 绘制主角和跟随者并重置视野到以主角为中心 + * @example core.drawHero(); // 原地绘制主角的静止帧 + * @param status 绘制状态,一般用stop + * @param offset 相对主角逻辑位置的偏移量,不填视为无偏移 + * @param frame 绘制第几帧 + */ + drawHero(status?: 'stop' | 'leftFoot' | 'rightFoot' | 'midFoot', offset?: number, frame?: number): void + + /** + * 获取主角面前第n格的横坐标 + * @example core.closeDoor(core.nextX(), core.nextY(), 'yellowDoor', core.turnHero); // 在主角面前关上一扇黄门,然后主角顺时针旋转90° + * @param n 目标格与主角的距离,面前为正数,背后为负数,脚下为0,不填视为1 + */ + nextX(n?: number): number + + /** + * 获取主角面前第n格的纵坐标 + * @example core.jumpHero(core.nextX(2), core.nextY(2)); // 主角向前跃过一格,即跳跃靴道具的使用效果 + * @param n 目标格与主角的距离,面前为正数,背后为负数,脚下为0,不填视为1 + */ + nextY(n?: number): number + + /** + * 判定主角是否身处某个点的锯齿领域(取曼哈顿距离) + * @example core.nearHero(6, 6, 6); // 判定主角是否身处点(6,6)的半径为6的锯齿领域 + * @param x 领域的中心横坐标 + * @param y 领域的中心纵坐标 + * @param n 领域的半径,不填视为1 + */ + nearHero(x: number, y: number, n?: number): boolean + + /** + * 请不要直接使用该函数,请使用core.updateStatusBar()代替!重算并绘制地图显伤 + * @example core.updateDamage(); // 更新当前地图的显伤,绘制在显伤层(废话) + * @param floorId 地图id,不填视为当前地图。预览地图时填写 + * @param ctx 绘制到的画布,如果填写了就会画在该画布而不是显伤层 + */ + updateDamage(floorId?: string, ctx?: CanvasRenderingContext2D): void + + /** 仅重绘地图显伤 */ + drawDamage(ctx?: CanvasRenderingContext2D): void + + /** + * 设置主角的某个属性 + * @example core.setStatus('loc', {x : 0, y : 0, direction : 'up'}); // 设置主角位置为地图左上角,脸朝上 + * @param name 属性的英文名,其中'x'、'y'和'direction'会被特殊处理为 core.setHeroLoc(name, value),其他的('exp'被视为'exp')会直接对 core.status.hero[name] 赋值 + * @param value 属性的新值 + */ + setStatus(name: K, value: HeroStatus[K]): void + + /** + * 增减主角的某个属性,等价于core.setStatus(name, core.getStatus(name) + value) + * @example core.addStatus('name', '酱'); // 在主角的名字后加一个“酱”字 + * @param name 属性的英文名 + * @param value 属性的增量,请注意旧量和增量中只要有一个是字符串就会把两者连起来成为一个更长的字符串 + */ + addStatus(name: K, value: HeroStatus[K]): void + + /** + * 读取主角的某个属性,不包括百分比修正 + * @example core.getStatus('loc'); // 读取主角的坐标和朝向 + * @param name 属性的英文名,其中'x'、'y'和'direction'会被特殊处理为 core.getHeroLoc(name),其他的('exp'被视为'exp')会直接读取 core.status.hero[name] + * @returns 属性值 + */ + getStatus(name: K): HeroStatus[K] + + /** + * 计算主角的某个属性,包括百分比修正 + * @example core.getRealStatus('atk'); // 计算主角的攻击力,包括百分比修正。战斗使用的就是这个值 + * @param name 属性的英文名,请注意只能用于数值类属性哦,否则乘法会得到NaN + */ + getRealStatus(name: K): HeroStatus[K] + + /** 获得某个状态的名字 */ + getStatusLabel(name: K): string + + /** + * 设置主角某个属性的百分比修正倍率,初始值为1, + * 倍率存放在flag: '__'+name+'_buff__' 中 + * @example core.setBuff('atk', 0.5); // 主角能发挥出的攻击力减半 + * @param name 属性的英文名,请注意只能用于数值类属性哦,否则随后的乘法会得到NaN + * @param value 新的百分比修正倍率,不填(效果上)视为1 + */ + setBuff(name: K, value?: HeroStatus[K]): void + + /** + * 增减主角某个属性的百分比修正倍率,加减法叠加和抵消。等价于 core.setBuff(name, core.getBuff(name) + value) + * @example core.addBuff('atk', -0.1); // 主角获得一层“攻击力减一成”的负面效果 + * @param name 属性的英文名,请注意只能用于数值类属性哦,否则随后的乘法会得到NaN + * @param value 倍率的增量 + */ + addBuff(name: K, value: HeroStatus[K]): void + + /** + * 读取主角某个属性的百分比修正倍率,初始值为1 + * @example core.getBuff('atk'); // 主角当前能发挥出多大比例的攻击力 + * @param name 属性的英文名 + */ + getBuff(name: HeroStatus[K]): number + + /** + * 获得或移除毒衰咒效果 + * @param action 获得还是移除,'get'为获得,'remove'为移除 + * @param type 要获得或移除的毒衰咒效果 + */ + triggerDebuff(action: string, type: string | string[]): void + + /** + * 设置勇士位置 + * 值得注意的是,这句话虽然会使勇士改变位置,但并不会使界面重新绘制; + * 如需立刻重新绘制地图还需调用:core.clearMap('hero'); core.drawHero(); 来对界面进行更新。 + * @example core.setHeroLoc('x', 5) // 将勇士当前位置的横坐标设置为5。 + * @param name 要设置的坐标属性 + * @param value 新值 + * @param noGather 是否聚集跟随者 + */ + setHeroLoc(name: 'x' | 'y', value: number, noGather?: boolean): void + setHeroLoc(name: 'direction', value: direction, noGather?: boolean): void + + /** + * 读取主角的位置和/或朝向 + * @example core.getHeroLoc(); // 读取主角的位置和朝向 + * @param name 要读取横坐标还是纵坐标还是朝向还是都读取 + * @returns name ? core.status.hero.loc[name] : core.status.hero.loc + */ + getHeroLoc(): { x: number, y: number, direction: direction } + getHeroLoc(name: 'x' | 'y'): number + getHeroLoc(name: 'direction'): direction + + /** + * 根据级别的数字获取对应的名称,后者定义在全塔属性 + * @example core.getLvName(); // 获取主角当前级别的名称,如“下级佣兵” + * @param lv 级别的数字,不填则视为主角当前的级别 + * @returns 级别的名称,如果不存在就还是返回数字 + */ + getLvName(lv?: number): string | number + + /** + * 获得下次升级需要的经验值。 + * 升级扣除模式下会返回经验差值;非扣除模式下会返回总共需要的经验值。 + * 如果无法进行下次升级,返回null。 + */ + getNextLvUpNeed(): number + + /** + * 设置一个flag变量 + * @example core.setFlag('poison', true); // 令主角中毒 + * @param name 变量名,支持中文 + * @param value 变量的新值,不填或填null视为删除 + */ + setFlag(name: string, value?: any): void + + /** + * 增减一个flag变量,等价于 core.setFlag(name, core.getFlag(name, 0) + value) + * @example core.addFlag('hatred', 1); // 增加1点仇恨值 + * @param name 变量名,支持中文 + * @param value 变量的增量 + */ + addFlag(name: string, value: number | string): void + + /** + * 读取一个flag变量 + * @param name 变量名,支持中文 + * @param defaultValue 当变量不存在时的返回值,可选(事件流中默认填0)。 + * @returns flags[name] ?? defaultValue + */ + getFlag(name: string, defaultValue?: any): any + + /** + * 判定一个flag变量是否存在且不为false、0、''、null、undefined和NaN + * @example core.hasFlag('poison'); // 判断主角当前是否中毒 + * @param name 变量名,支持中文 + * @returns !!core.getFlag(name) + */ + hasFlag(name: string): boolean + + /** + * 设置天气,不计入存档。如需长期生效请使用core.events._action_setWeather()函数 + * @example core.setWeather('fog', 10); // 设置十级大雾天 + * @param type 新天气的类型,不填视为无天气 + * @param level 新天气(晴天除外)的级别,必须为不大于10的正整数,不填视为5 + */ + setWeather(type?: 'rain' | 'snow' | 'sun' | 'fog' | 'cloud' | string, level?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10): void + + /** 注册一个天气 */ + registerWeather(name: string, initFunc: (level: number) => void, frameFunc?: (timestamp: number, level: number) => void): void + + /** 注销一个天气 */ + unregisterWeather(name: string): void; + + /** + * 更改画面色调,不计入存档。如需长期生效请使用core.events._action_setCurtain()函数 + * @example core.setCurtain(); // 恢复画面色调,用时四分之三秒 + * @param color 一行三列(第四列视为1)或一行四列(第四列若大于1则会被视为1,第四列若为负数则会被视为0)的颜色数组,不填视为[0, 0, 0, 0] + * @param time 渐变时间,单位为毫秒。不填视为750ms,负数视为0(无渐变,立即更改) + * @param callback 更改完毕后的回调函数,可选。事件流中常取core.doAction + */ + setCurtain(color?: [number, number, number, number?], time?: number, moveMode?: string, callback?: () => void): void + + /** + * 画面闪烁 + * @example core.screenFlash([255, 0, 0, 1], 3); // 红屏一闪而过 + * @param color 一行三列(第四列视为1)或一行四列(第四列若大于1则会被视为1,第四列若填负数则会被视为0)的颜色数组,必填 + * @param time 单次闪烁时长,实际闪烁效果为先花其三分之一的时间渐变到目标色调,再花剩余三分之二的时间渐变回去 + * @param times 闪烁的总次数,不填或填0都视为1 + * @param callback 闪烁全部完毕后的回调函数,可选 + */ + screenFlash(color: [number, number, number, number?], time: number, times?: number, moveMode?: string, callback?: () => void): void + + /** + * 播放背景音乐,中途开播但不计入存档且只会持续到下次场景切换。如需长期生效请将背景音乐的文件名赋值给flags.__bgm__ + * @example core.playBgm('bgm.mp3', 30); // 播放bgm.mp3,并跳过前半分钟 + * @param bgm 背景音乐的文件名,支持全塔属性中映射前的中文名 + * @param startTime 跳过前多少秒,不填则不跳过 + */ + playBgm(bgm: string, startTime?: number): void + + /** + * 注册一个 animationFrame + * @param name 名称,可用来作为注销使用 + * @param needPlaying 是否只在游戏运行时才执行(在标题界面不执行) + * @param func 要执行的函数,或插件中的函数名;可接受timestamp(从页面加载完毕到当前所经过的时间)作为参数 + */ + registerAnimationFrame(name: string, needPlaying: boolean, func?: (timestamp: number) => void): void + + /** 注销一个animationFrame */ + unregisterAnimationFrame(name: string): void + + /** 游戏是否已经开始 */ + isPlaying(): boolean + + /** 清除游戏状态和数据 */ + clearStatus(): void + + /** 清除自动寻路路线 */ + clearAutomaticRouteNode(x?: any, y?: any): void + + /** 停止自动寻路操作 */ + stopAutomaticRoute(): void + + /** 保存剩下的寻路,并停止 */ + saveAndStopAutomaticRoute(): void + + /** 继续剩下的自动寻路操作 */ + continueAutomaticRoute(): void + + /** 清空剩下的自动寻路列表 */ + clearContinueAutomaticRoute(callback?: () => any): void + + /** 设置行走的效果动画 */ + setHeroMoveInterval(callback?: () => any): void + + /** 每移动一格后执行的事件 */ + moveOneStep(callback?: () => any): void + + /** 当前是否正在移动 */ + isMoving(): boolean + + /** 瞬间移动 */ + moveDirectly(destX?: any, destY?: any, ignoreSteps?: any): void + + /** 改变勇士的不透明度 */ + setHeroOpacity(opacity?: number, moveMode?: string, time?: any, callback?: () => any): void + + /** 加减画布偏移 */ + addGameCanvasTranslate(x?: number, y?: number): void + + /** + * 设置视野范围 + * px,py: 左上角相对大地图的像素坐标,不需要为32倍数 + */ + setViewport(px?: number, py?: number): void + + /** 移动视野范围 */ + moveViewport(x: number, y: number, moveMode?: string, time?: number, callback?: () => any): void + + /** 更新跟随者坐标 */ + updateFollowers(): void + + /** 更新领域、夹击、阻击的伤害地图 */ + updateCheckBlock(floorId?: string): boolean + + /** 检查并执行领域、夹击、阻击事件 */ + checkBlock(): void + + /** 选择录像文件 */ + chooseReplayFile(): void + + /** 开始播放 */ + startReplay(list?: any): void + + /** 更改播放状态 */ + triggerReplay(): void + + /** 暂停播放 */ + pauseReplay(): void + + /** 恢复播放 */ + resumeReplay(): void + + /** 单步播放 */ + stepReplay(): void + + /** 加速播放 */ + speedUpReplay(): void + + /** 减速播放 */ + speedDownReplay(): void + + /** 设置播放速度 */ + setReplaySpeed(speed?: number): void + + /** 停止播放 */ + stopReplay(force?: boolean): void + + /** 回退 */ + rewindReplay(): void + + /** 是否正在播放录像 */ + isReplaying(): boolean + + /** + * 注册一个录像行为 + * @param name 自定义名称,可用于注销使用 + * @param func 具体执行录像的函数,可为一个函数或插件中的函数名; + * 需要接受一个action参数,代表录像回放时的下一个操作 + * func返回true代表成功处理了此录像行为,false代表没有处理此录像行为。 + */ + registerReplayAction(name: string, func: ((action: string) => boolean) | (() => boolean)): void + + /** 注销一个录像行为 */ + unregisterReplayAction(name: string): void + + /** 自动存档 */ + autosave(removeLast?: any): void + + /** 实际进行自动存档 */ + checkAutosave(): void + + /** 实际进行存读档事件 */ + doSL(id?: string, type?: any): void + + /** 同步存档到服务器 */ + syncSave(type?: any): void + + /** 从服务器加载存档 */ + syncLoad(): void + + /** 存档到本地 */ + saveData(): any + + /** 从本地读档 */ + loadData(data?: any, callback?: () => any): any + + /** 获得某个存档内容 */ + getSave(index?: any, callback?: () => any): any + + /** 获得某些存档内容 */ + getSaves(ids?: any, callback?: () => any): any + + /** 获得所有存档内容 */ + getAllSaves(callback?: () => any): any + + /** 获得所有存在存档的存档位 */ + getSaveIndexes(callback?: () => any): any + + /** 判断某个存档位是否存在存档 */ + hasSave(index?: number): boolean + + /** 删除某个存档 */ + removeSave(index?: number, callback?: () => any): void + + /** 从status中获得属性,如果不存在则从勇士属性中获取 */ + getStatusOrDefault(status?: any, name?: string): any + + /** 从status中获得实际属性(增幅后的),如果不存在则从勇士属性中获取 */ + getRealStatusOrDefault(status?: any, name?: string): any + + /** 获得勇士原始属性(无装备和衰弱影响) */ + getNakedStatus(name?: string): any + + /** 锁定用户控制,常常用于事件处理 */ + lockControl(): void + + /** 解锁用户控制 */ + unlockControl(): void + + /** 清空录像折叠信息 */ + clearRouteFolding(): void + + /** 检查录像折叠信息 */ + checkRouteFolding(): void + + /** 获得映射文件名 */ + getMappedName(name?: string): string + + /** 暂停背景音乐的播放 */ + pauseBgm(): void + + /** 恢复背景音乐的播放 */ + resumeBgm(resumeTime?: number): void + + /** 设置背景音乐的播放速度和音调 */ + setBgmSpeed(speed: number, usePitch?: boolean): void + + /** 设置音乐图标的显隐状态 */ + setMusicBtn(): void + + /** 开启或关闭背景音乐的播放 */ + triggerBgm(): void + + /** 播放一个音效 */ + playSound(sound: string, pitch?: number, callback?: () => any): number | undefined + + /** 停止(所有)音频 */ + stopSound(id?: number): void + + /** 获得正在播放的所有(指定)音效的id列表 */ + getPlayingSounds(name?: string): Array + + /** 检查bgm状态 */ + checkBgm(): void + + /** 设置屏幕放缩 */ + setDisplayScale(delta: number): void + + /** 清空状态栏 */ + clearStatusBar(): void + + /** 显示状态栏 */ + showStatusBar(): void + + /** 隐藏状态栏 */ + hideStatusBar(showToolbox?: boolean): void + + /** 更新状态栏的勇士图标 */ + updateHeroIcon(name: string): void + + /** 改变工具栏为按钮1-8 */ + setToolbarButton(useButton?: boolean): void + + /** + * 注册一个resize函数 + * @param name 名称,可供注销使用 + * @param func 可以是一个函数,或者是插件中的函数名;可以接受obj参数,详见resize函数。 + */ + registerResize(name: string, func: (obj: any) => void): void + + /** 注销一个resize函数 */ + unregisterResize(name: string): void + + /** 屏幕分辨率改变后重新自适应 + * @param type 重新设置指定类型的已注册resize事件 + */ + resize(type?:string): void + + /** + * 291改新增函数,在楼层数据统计中添加指定项目的值 + */ + addFloorStatistics(key: 'battleDamage' | 'poisonDamage' | 'extraDamage' | 'vampireExtraLoss', + value: number, floorId?: string): void + + /** + * 291改新增函数,注册指定名称的自定义画布在画面resize时的重绘事件 + * @param name 自定义画布的名称 + * @param event resize时的重绘事件 + */ + registerDymCanvasResizeEvent(name: string, event: Function): void + + /** + * 291改新增函数,取消注册指定名称的自定义画布在画面resize时的重绘事件 + * @param name 自定义画布的名称 + */ + unregisterDymCanvasResizeEvent(name: string): void +} + +/**@file events.js将处理所有和事件相关的操作。 */ +interface events { + /** 根据难度设置flag:hard */ + _startGame_setHard(): void + _eventMoveHero_moving(step: number, moveSteps: [direction | 'forward' | 'backward' | + 'leftup' | 'leftdown' | 'rightup' | 'rightdown', number][]): boolean + __action_checkReplaying(): boolean + _action_sleep(data: { time: number }, x?: undefined, y?: undefined, prefix?: undefined): void + _startGame_start(hard: string, seed: number, route: string, callback: Function): void + _startGame_afterStart(callback: Function): void + + /** + * 开始新游戏 + * @example core.startGame('咸鱼乱撞', 0, ''); // 开始一局咸鱼乱撞难度的新游戏,随机种子为0 + * @param hard 难度名,会显示在左下角(横屏)或右下角(竖屏) + * @param seed 随机种子,相同的种子保证了录像的可重复性 + * @param route 经由base64压缩后的录像,用于从头开始的录像回放 + * @param callback 回调函数,可选 + */ + startGame(hard: string, seed: number, route: string, callback?: () => void): void + + /** + * 游戏结束 + * @example core.gameOver(); // 游戏失败 + * @param ending 结局名,省略表示失败 + * @param fromReplay true表示在播放录像,可选 + * @param norank true表示不计入榜单,可选 + */ + gameOver(ending?: string, fromReplay?: boolean, norank?: boolean): void + + /** + * 战斗,如果填写了坐标就会删除该点的敌人并触发战后事件 + * @example core.battle('greenSlime'); // 和从天而降的绿头怪战斗(如果打得过) + * @param id 敌人id,必填 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param force true表示强制战斗,可选 + * @param callback 回调函数,可选 + */ + battle(id: string, x?: number, y?: number, force?: boolean, callback?: () => void): void + + /** + * 开门(包括三种基础墙) + * @example core.openDoor(0, 0, true, core.jumpHero); // 打开左上角的门,需要钥匙,然后主角原地跳跃半秒 + * @param x 门的横坐标 + * @param y 门的纵坐标 + * @param needKey true表示需要钥匙,会导致机关门打不开 + * @param callback 门完全打开后或打不开时的回调函数,可选 + */ + openDoor(x: number, y: number, needKey?: boolean, callback?: () => void): void + + /** + * 获得道具并提示,如果填写了坐标就会删除该点的该道具 + * @example core.getItem('book'); // 获得敌人手册并提示 + * @param id 道具id,必填 + * @param num 获得的数量,不填视为1,填了就别填坐标了 + * @param x 道具的横坐标,可选 + * @param y 道具的纵坐标,可选 + * @param callback 回调函数,可选 + */ + getItem(id: string, num?: number, x?: number, y?: number, callback?: () => void): void + + /** + * 场景切换 + * @example core.changeFloor('MT0'); // 传送到主塔0层,主角坐标和朝向不变,黑屏时间取用户设置值 + * @param floorId 传送的目标地图id,可以填':before'和':after'分别表示楼下或楼上 + * @param stair 传送的位置,可以填':now'(保持不变,可省略),':symmetry'(中心对称),':symmetry_x'(左右对称),':symmetry_y'(上下对称)或图块id(该图块最好在目标层唯一,一般为'downFloor'或'upFloor') + * @param heroLoc 传送的坐标(如果填写了,就会覆盖上述的粗略目标位置)和传送后主角的朝向(不填表示保持不变) + * @param time 传送的黑屏时间,单位为毫秒。不填为用户设置值 + * @param callback 黑屏结束后的回调函数,可选 + */ + changeFloor(floorId: string, stair?: string, heroLoc?: { x?: number, y?: number, direction?: direction }, time?: number, callback?: () => void): void + + /** + * 执行下一个事件指令,常作为回调 + * @example core.setCurtain([0,0,0,1], undefined, null, core.doAction); // 事件中的原生脚本,配合勾选“不自动执行下一个事件”来达到此改变色调只持续到下次场景切换的效果 + * @param keepUI true表示不清除UI画布和选择光标 + */ + doAction(keepUI?: true): void + + /** + * 插入一段事件;此项不可插入公共事件,请用 core.insertCommonEvent + * @example core.insertAction('一段文字'); // 插入一个显示文章 + * @param action 单个事件指令,或事件指令数组 + * @param x 新的当前点横坐标,可选 + * @param y 新的当前点纵坐标,可选 + * @param callback 新的回调函数,可选 + * @param addToLast 插入的位置,true表示插入到末尾,否则插入到开头 + */ + insertAction(action: Events | MotaAction, x?: number | null, y?: number | null, callback?: () => void, addToLast?: boolean | null): void + + /** + * 设置一项敌人属性并计入存档 + * @example core.setEnemy('greenSlime', 'def', 0); // 把绿头怪的防御设为0 + * @param id 敌人id + * @param name 属性的英文缩写 + * @param value 属性的新值,可选 + * @param operator 操作符,可选 + * @param prefix 独立开关前缀,一般不需要,下同 + */ + setEnemy(id: string, name: K, value?: Enemy[K], operator?: string, prefix?: string): void + + /** 设置某个点的敌人属性 */ + setEnemyOnPoint(x: number, y: number, floorId: string, name: K, value?: Enemy[K], operator?: string, prefix?: string): void + + /** 重置某个点的敌人属性 */ + resetEnemyOnPoint(x: number, y: number, floorId?: string): void + + /** 将某个点已经设置的敌人属性移动到其他点 */ + moveEnemyOnPoint(fromX: number, fromY: number, toX: number, toY: number, floorId?: string): void + + /** + * 设置一项楼层属性并刷新状态栏 + * @example core.setFloorInfo('ratio', 2, 'MT0'); // 把主塔0层的血瓶和宝石变为双倍效果 + * @param name 要求改的属性名 + * @param values 属性的新值 + * @param floorId 楼层id,不填视为当前层 + * @param prefix 独立开关前缀,一般不需要,下同 + */ + setFloorInfo(name: K, values?: Floor[K] | boolean | number | string | [number, number] | [string, number?] | Array, floorId?: string, prefix?: string): void + + /** + * 设置一个系统开关 + * @example core.setGlobalFlag('steelDoorWithoutKey', true); // 使全塔的所有铁门都不再需要钥匙就能打开 + * @param name 系统开关的英文名 + * @param value 开关的新值,您可以用!core.flags[name]简单地表示将此开关反转 + */ + setGlobalFlag(name: keyof SystemFlags, value: boolean): void + + /** + * 关门,目标点必须为空地 + * @example core.closeDoor(0, 0, 'yellowWall', core.jumpHero); // 在左上角关掉一堵黄墙,然后主角原地跳跃半秒 + * @param x 横坐标 + * @param y 纵坐标 + * @param id 门的id,也可以用三种基础墙 + * @param callback 门完全关上后的回调函数,可选 + */ + closeDoor(x: number, y: number, id: string, callback?: () => void): void + + /** + * 显示一张图片 + * @example core.showImage(1, core.material.images.images['winskin.png'], [0,0,128,128], [0,0,416,416], 0.5, 1000); // 裁剪winskin.png的最左边128×128px,放大到铺满整个视野,1秒内淡入到50%透明,编号为1 + * @param code 图片编号,为不大于50的正整数,加上100后就是对应画布层的z值,较大的会遮罩较小的,注意色调层的z值为125,UI层为140 + * @param image 图片文件名(可以是全塔属性中映射前的中文名)或图片对象(见上面的例子) + * @param sloc 一行且至多四列的数组,表示从原图裁剪的左上角坐标和宽高,可选 + * @param loc 一行且至多四列的数组,表示图片在视野中的左上角坐标和宽高,可选 + * @param opacityVal 不透明度,为小于1的正数。不填视为1 + * @param time 淡入时间,单位为毫秒。不填视为0 + * @param callback 图片完全显示出来后的回调函数,可选 + */ + showImage(code: number, image: string | HTMLImageElement, sloc?: Array, loc?: Array, opacityVal?: number, time?: number, callback?: () => void): void + + /** + * 隐藏一张图片 + * @example core.hideImage(1, 1000, core.jumpHero); // 1秒内淡出1号图片,然后主角原地跳跃半秒 + * @param code 图片编号 + * @param time 淡出时间,单位为毫秒 + * @param callback 图片完全消失后的回调函数,可选 + */ + hideImage(code: number, time?: number, callback?: () => void): void + + /** + * 移动一张图片并/或改变其透明度 + * @example core.moveImage(1, null, 0.5); // 1秒内把1号图片变为50%透明 + * @param code 图片编号 + * @param to 新的左上角坐标,省略表示原地改变透明度 + * @param opacityVal 新的透明度,省略表示不变 + * @param moveMode 移动模式 + * @param time 移动用时,单位为毫秒。不填视为1秒 + * @param callback 图片移动完毕后的回调函数,可选 + */ + moveImage(code: number, to?: [number?, number?], opacityVal?: number, moveMode?: string, time?: number, callback?: () => void): void + + /** + * 旋转一张图片 + * @param code 图片编号 + * @param center 旋转中心像素(以屏幕为基准);不填视为图片本身中心 + * @param angle 旋转角度;正数为顺时针,负数为逆时针 + * @param moveMode 旋转模式 + * @param time 移动用时,单位为毫秒。不填视为1秒 + * @param callback 图片移动完毕后的回调函数,可选 + */ + rotateImage(code: number, center?: [number?, number?], angle?: number, moveMode?: string, time?: number, callback?: () => void): void + + /** 放缩一张图片 */ + scaleImage(code: number, center?: [Number?, number?], scale?: number, moveMode?: string, time?: number, callback?: () => void): void + + /** + * 绘制一张动图或擦除所有动图 + * @example core.showGif(); // 擦除所有动图 + * @param name 动图文件名,可以是全塔属性中映射前的中文名 + * @param x 动图在视野中的左上角横坐标 + * @param y 动图在视野中的左上角纵坐标 + */ + showGif(name?: string, x?: number, y?: number): void + + /** + * 调节bgm的音量 + * @example core.setVolume(0, 100, core.jumpHero); // 0.1秒内淡出bgm,然后主角原地跳跃半秒 + * @param value 新的音量,为0或不大于1的正数。注意系统设置中是这个值的平方根的十倍 + * @param time 渐变用时,单位为毫秒。不填或小于100毫秒都视为0 + * @param callback 渐变完成后的回调函数,可选 + */ + setVolume(value: number, time?: number, callback?: () => void): void + + /** + * 视野抖动 + * @example core.vibrate(); // 视野左右抖动1秒 + * @param direction 抖动方向 + * @param time 抖动时长,单位为毫秒 + * @param speed 抖动速度 + * @param power 抖动幅度 + * @param callback 抖动平息后的回调函数,可选 + */ + vibrate(direction?: string, time?: number, speed?: number, power?: number, callback?: () => void): void + + /** + * 强制移动主角(包括后退),这个函数的作者已经看不懂这个函数了 + * @example core.eventMoveHero(['forward'], 125, core.jumpHero); // 主角强制前进一步,用时1/8秒,然后主角原地跳跃半秒 + * @param steps 步伐数组,注意后退时跟随者的行为会很难看 + * @param time 每步的用时,单位为毫秒。0或不填则取主角的移速,如果后者也不存在就取0.1秒 + * @param callback 移动完毕后的回调函数,可选 + */ + eventMoveHero(steps: step[], time?: number, callback?: () => void): void + + /** + * 主角跳跃,跳跃勇士。ex和ey为目标点的坐标,可以为null表示原地跳跃。time为总跳跃时间。 + * @example core.jumpHero(); // 主角原地跳跃半秒 + * @param ex 跳跃后的横坐标 + * @param ey 跳跃后的纵坐标 + * @param time 跳跃时长,单位为毫秒。不填视为半秒 + * @param callback 跳跃完毕后的回调函数,可选 + */ + jumpHero(ex?: number, ey?: number, time?: number, callback?: () => void): void + + /** + * 更改主角行走图 + * @example core.setHeroIcon('npc48.png', true); // 把主角从阳光变成样板0层左下角的小姐姐,但不立即刷新 + * @param name 新的行走图文件名,可以是全塔属性中映射前的中文名。映射后会被存入core.status.hero.image + * @param noDraw true表示不立即刷新(刷新会导致大地图下视野重置到以主角为中心) + */ + setHeroIcon(name: string, noDraw?: boolean): void + + /** + * 尝试使用一个道具 + * @example core.tryUseItem('pickaxe'); // 尝试使用破墙镐 + * @param itemId 道具id,其中敌人手册、传送器和飞行器会被特殊处理 + */ + tryUseItem(itemId: string): void + + /** 初始化游戏 */ + resetGame(hero?: HeroStatus, hard?: any, floorId?: string | null, maps?: any | null, values?: any | null): void + + /** 游戏获胜事件 */ + win(reason?: string, norank?: boolean, noexit?: boolean): void + + /** 游戏失败事件 */ + lose(reason?: string): void + + /** 重新开始游戏;此函数将回到标题页面 */ + restart(): void + + /** 询问是否需要重新开始 */ + confirmRestart(): void + + /** + * 注册一个系统事件 + * @param type 事件名 + * @param func 为事件的处理函数,可接受(data,callback)参数 + */ + registerSystemEvent(type: string, func: (data?: any, callback?: () => void) => void): void + + /** 注销一个系统事件 */ + unregisterSystemEvent(type: string): void + + /** 执行一个系统事件 */ + doSystemEvent(type: string, data?: any, callback?: () => any): void + + /** + * 触发(x,y)点的系统事件;会执行该点图块的script属性,同时支持战斗(会触发战后)、道具(会触发道具后)、楼层切换等等 + */ + trigger(x?: number, y?: number, callback?: () => any): void + + /** 战斗前触发的事件 */ + beforeBattle(enemyId?: string, x?: number, y?: number): void + + /** 战斗结束后触发的事件 */ + afterBattle(enemyId?: string, x?: number, y?: number): void + + /** 开一个门后触发的事件 */ + afterOpenDoor(doorId?: string, x?: number, y?: number): void + + /** 获得一个道具后的shij */ + afterGetItem(id?: string, x?: number, y?: number, isGentleClick?: boolean): void + + /** + * 轻按获得面前的物品或周围唯一物品 + * @param noRoute 若为true则不计入录像 + */ + getNextItem(noRoute?: boolean): void + + /** 楼层转换中 */ + changingFloor(floorId?: string, heroLoc?: any): void + + /** 转换楼层结束的事件 */ + afterChangeFloor(floorId?: string): void + + /** 是否到达过某个楼层 */ + hasVisitedFloor(floorId?: string): boolean + + /** 到达某楼层 */ + visitFloor(floorId?: string): void + + /** 推箱子 */ + pushBox(data?: any): void + + /** 推箱子后的事件 */ + afterPushBox(): void + + /** 当前是否在冰上 */ + onSki(number?: number): boolean + + /** + * 注册一个自定义事件 + * @param type 事件类型 + * @param func 事件的处理函数,可接受(data, x, y, prefix)参数 + * data为事件内容,x和y为当前点坐标(可为null),prefix为当前点前缀 + */ + registerEvent(type: string, func: (data: any, x?: number, y?: number, prefix?: string) => void): void + + /** 注销一个自定义事件 */ + unregisterEvent(type: string): void + + /** 执行一个自定义事件 */ + doEvent(data?: any, x?: number, y?: number, prefix?: any): void + + /** 直接设置事件列表 */ + setEvents(list?: any, x?: number, y?: number, callback?: () => any): void + + /** 开始执行一系列自定义事件 */ + startEvents(list?: any, x?: number, y?: number, callback?: () => any): void + + /** + * 插入一个公共事件 + * @example core.insertCommonEvent('加点事件', [3]); + * @param name 公共事件名;如果公共事件不存在则直接忽略 + * @param args 参数列表,为一个数组,将依次赋值给 flag:arg1, flag:arg2, ... + * @param x 新的当前点横坐标,可选 + * @param y 新的当前点纵坐标,可选 + * @param callback 新的回调函数,可选 + * @param addToLast 插入的位置,true表示插入到末尾,否则插入到开头 + */ + insertCommonEvent(name?: string, args?: any, x?: number, y?: number, callback?: () => any, addToLast?: boolean): void + + /** 获得一个公共事件 */ + getCommonEvent(name: string): any + + /** 恢复一个事件 */ + recoverEvents(data?: any): void + + /** 检测自动事件 */ + checkAutoEvents(): void + + /** 当前是否在执行某个自动事件 */ + autoEventExecuting(symbol?: string, value?: any): boolean + + /** 当前是否执行过某个自动事件 */ + autoEventExecuted(symbol?: string, value?: any): boolean + + /** 将当前点坐标入栈 */ + pushEventLoc(x?: number, y?: number, floorId?: string): boolean + + /** 将当前点坐标入栈 */ + popEventLoc(): any + + /** 预编辑事件 */ + precompile(data?: any): any + + /** 点击怪物手册时的打开操作 */ + openBook(fromUserAction?: boolean): void + + /** 点击楼层传送器时的打开操作 */ + useFly(fromUserAction?: boolean): void + + /** 飞往某一层 */ + flyTo(toId?: string, callback?: () => boolean): void + + /** 点击存档按钮时的打开操作 */ + save(fromUserAction?: boolean): void + + /** 点击读档按钮时的打开操作 */ + load(fromUserAction?: boolean): void + + /** 点击装备栏时的打开操作 */ + openEquipbox(fromUserAction?: boolean): void + + /** 点击工具栏时的打开操作 */ + openToolbox(fromUserAction?: boolean): void + + /** 点击快捷商店按钮时的打开操作 */ + openQuickShop(fromUserAction?: boolean): void + + /** 点击虚拟键盘时的打开操作 */ + openKeyBoard(fromUserAction?: boolean): void + + /** 点击设置按钮时的操作 */ + openSettings(fromUserAction?: boolean): void + + /** 当前是否有未处理完毕的异步事件(不包含动画和音效) */ + hasAsync(): boolean + + /** 立刻停止所有异步事件 */ + stopAsync(): void + + /** + * 跟随 + * @param name 要跟随的一个合法的4x4的行走图名称,需要在全塔属性注册 + */ + follow(name: string): void + + /** + * 取消跟随 + * @param name 取消跟随的行走图,不填则取消全部跟随者 + */ + unfollow(name?: string): void + + /** 数值操作 */ + setValue(name: string, operator: string, value: any, prefix?: string): void + + /** 数值增减 */ + addValue(name: string, value: any, prefix?: string): void + + /** 设置全塔属性 */ + setGlobalAttribute(name: string, value: any): void + + /** 设置剧情文本的属性 */ + setTextAttribute(data: any): void + + /** 清除对话框 */ + clearTextBox(code: number): void + + /** 移动对话框 */ + moveTextBox(code: number, loc: [number], relative: boolean, moveMode?: string, time?: number, callback?: () => any): void + + /** 设置文件别名 */ + setNameMap(name: string, value?: string): void + + /** 检查升级事件 */ + checkLvUp(): void +} + +/** @file actions.js 定义了玩家的操作控制 */ +interface actions { + SIZE: number + HSIZE: number + LAST: number + CHOICES_LEFT: number + CHOICES_RIGHT: number + _getChoicesTopIndex(length: number): number + _clickAction(x?: number, y?: number, px?: number, py?: number): number + /** 获得点击事件相对左上角的坐标 */ + _getClickLoc(x: number, y: number): { x: number, y: number, size: number } + _clickSwitchs_action_moveSpeed(delta: number): void + _clickSwitchs_action_floorChangeTime(delta: number): void + _clickSwitchs_display_setSize(delta: number): void + /** 高清画面开关 */_clickSwitchs_display_enableHDCanvas(): void + /** 定点怪显开关 */_clickSwitchs_display_enableEnemyPoint(): void + /** 怪物显伤开关 */_clickSwitchs_display_enemyDamage(): void + /** 临界显示开关 */_clickSwitchs_display_critical(): void + /** 领域显示开关 */_clickSwitchs_display_extraDamage(): void + /** 领域显示模式开关 */_clickSwitchs_display_extraDamageType(): void + /** 音乐开关 */_clickSwitchs_sounds_bgm(): void + /** 音效开关 */_clickSwitchs_sounds_se(): void + /** 音量调节开关 */_clickSwitchs_sounds_userVolume(delta: number): void + + _keyDownToolbox(keyCode: number): void + _keyUpToolbox(keyCode: number): void + _clickToolbox(x: number, y: number, px: number, py: number): void + _keyDownEquipbox(keyCode: number): void + _keyUpEquipbox(keyCode: number): void + _clickEquipbox(x: number, y: number, px: number, py: number): void + + /** + * 此函数将注册一个用户交互行为。 + * @param action 要注册的交互类型,如 ondown, onclick, keyDown 等等。 + * @param name 你的自定义名称,可被注销使用;同名重复注册将后者覆盖前者。 + * @param func 执行函数。 + * 如果func返回true,则不会再继续执行其他的交互函数;否则会继续执行其他的交互函数。 + * @param priority 优先级;优先级高的将会被执行。此项可不填,默认为0 + */ + registerAction(action: string, name: string, func: string | ((...params: any) => void), priority?: number): void + + /** 注销一个用户交互行为 */ + unregisterAction(action: string, name: string): void + + /** 执行一个用户交互行为 */ + doRegisteredAction(action: string, ...params: any): void + + /** 按下某个键时 */ + onkeyDown(e: KeyboardEvent): void + + /** 放开某个键时 */ + onkeyUp(e: KeyboardEvent): void + + /** 按住某个键时 */ + pressKey(keyCode: number): void + + /** 根据按下键的code来执行一系列操作 */ + keyDown(keyCode: number): void + + /** 根据放开键的code来执行一系列操作 */ + keyUp(keyCode: number, altKey?: boolean, fromReplay?: boolean): void + + /** 点击(触摸)事件按下时 */ + ondown(loc: number[]): void + + /** 当在触摸屏上滑动时 */ + onmove(loc: number[]): void + + /** 当点击(触摸)事件放开时 */ + onup(loc: number[]): void + + /** 具体点击屏幕上(x,y)点时,执行的操作 */ + onclick(x: number, y: number, px: number, py: number, stepPostfix?: any): void + + /** 滑动鼠标滚轮时的操作 */ + onmousewheel(direct: 1 | -1): void + + /** 长按Ctrl键时 */ + keyDownCtrl(): void + + /** 长按 */ + longClick(x: number, y: number, px: number, py: number, fromEvent?: boolean): void + + /** 点击自绘状态栏时 */ + onStatusBarClick(e?: MouseEvent): void +} + +/** @file enemys.js 定义了一系列和敌人相关的API函数。 */ +interface enemys { + + /** + * 判定某种特殊属性的有无 + * @example core.hasSpecial('greenSlime', 1) // 判定绿头怪有无先攻属性 + * @param special 敌人id或敌人对象或正整数数组或自然数 + * @param test 待检查的属性编号 + * @returns 若special为数组或数且含有test或相等、或special为敌人id或对象且具有此属性,则返回true + */ + hasSpecial(special: number | number[] | string | Enemy, test: number): boolean + + /** + * 获得某种敌人的全部特殊属性名称 + * @example core.getSpecialText('greenSlime') // ['先攻', '3连击', '破甲', '反击'] + * @param enemy 敌人id或敌人对象,如core.material.enemys.greenSlime + * @returns 字符串数组 + */ + getSpecialText(enemy: string | Enemy): string[] + + /** + * 获得某种敌人的某种特殊属性的介绍 + * @example core.getSpecialHint('bat', 1) // '先攻:怪物首先攻击' + * @param enemy 敌人id或敌人对象,用于确定属性的具体数值,否则可选 + * @param special 属性编号,可以是该敌人没有的属性 + * @returns 属性的介绍,以属性名加中文冒号开头 + */ + getSpecialHint(enemy: string | Enemy, special: number): string + + /** + * 获得某个敌人的某项属性值 + * @param enemy 敌人id或敌人对象 + * @param name 要获取的属性名,不填时获取完整的该敌人数据 + * @param x 敌人的横坐标,可选,填写时该点属性覆盖core.material.enemys中相应属性 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选。 + */ + getEnemyValue(enemy: string | Enemy, name?: string | null, x?: number, y?: number, floorId?: string): any + + /** + * 判定主角当前能否打败某只敌人 + * @example core.canBattle('greenSlime',0,0,'MT0') // 能否打败主塔0层左上角的绿头怪(假设有) + * @param enemy 敌人id或敌人对象 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns true表示可以打败,false表示无法打败 + */ + canBattle(enemy: string | Enemy, x?: number, y?: number, floorId?: string): boolean + + /** + * 获得某只敌人对主角的总伤害 + * @example core.getDamage('greenSlime',0,0,'MT0') // 绿头怪的总伤害 + * @param enemy 敌人id或敌人对象 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns 总伤害,如果因为没有破防或无敌怪等其他原因无法战斗,则返回null + */ + getDamage(enemy: string | Enemy, x?: number, y?: number, floorId?: string): number + + /** + * 获得某只敌人的地图显伤,包括颜色 + * @example core.getDamageString('greenSlime', 0, 0, 'MT0') // 绿头怪的地图显伤 + * @param enemy 敌人id或敌人对象 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns damage: 表示伤害值或为'???',color: 形如'#RrGgBb' + */ + getDamageString(enemy: string | Enemy, x?: number, y?: number, floorId?: string): { + damage: string, + color: string + } + + /** + * 获得某只敌人接下来的若干个临界及其减伤,算法基于useLoop开关选择回合法或二分法 + * @example core.nextCriticals('greenSlime', 9, 0, 0, 'MT0') // 绿头怪接下来的9个临界 + * @param enemy 敌人id或敌人对象 + * @param number 要计算的临界数量,可选,默认为1 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns 两列的二维数组,每行表示一个临界及其减伤 + */ + nextCriticals(enemy: string | Enemy, number?: number, x?: number, y?: number, floorId?: string): number[][] + + /** + * 计算再加若干点防御能使某只敌人对主角的总伤害降低多少 + * @example core.nextCriticals('greenSlime', 10, 0, 0, 'MT0') // 再加10点防御能使绿头怪的伤害降低多少 + * @param enemy 敌人id或敌人对象 + * @param k 假设主角增加的防御力,可选,默认为1 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns 总伤害的减少量 + */ + getDefDamage(enemy: string | Enemy, k?: number, x?: number, y?: number, floorId?: string): number + + /** + * 计算再加若干点护盾能使某只敌人对主角的总伤害降低多少 + * @example core.nextCriticals('greenSlime', 10, 0, 0, 'MT0') // 再加10点防御能使绿头怪的伤害降低多少 + * @param enemy 敌人id或敌人对象 + * @param k 假设主角增加的护盾,可选,默认为1 + * @param x 敌人的横坐标,可选 + * @param y 敌人的纵坐标,可选 + * @param floorId 敌人所在的地图,可选 + * @returns 总伤害的减少量 + */ + getMdefDamage(enemy: string | Enemy, k?: number, x?: number, y?: number, floorId?: string): number + + /** + * 计算和吸血怪物当前的战斗伤害比最低战斗伤害高多少。返回null说明打不过或怪物不吸血 + * @param {string|Enemy} enemy + * @param {number} [x] + * @param {number} [y] + * @param {string} [floorId] + * @param {{hp:number,atk:number,def:number,mdef:number}} [heroStatus] 未填时默认使用角色当前属性计算 + * @returns {null|number} + */ + getVampireExtraLoss(enemy: string | Enemy, x?: number, y?: number, + floorId?: string, heroStatus?: { hp: number, atk: number, def: number, mdef: number }) + + /** + * 获得某张地图的敌人集合,用于手册绘制 + * @example core.getCurrentEnemys('MT0') // 主塔0层的敌人集合 + * @param floorId 地图id,可选 + * @returns 敌人集合,按伤害升序排列,支持多朝向怪合并 + */ + getCurrentEnemys(floorId?: string): Enemy[] + + /** + * 检查某些楼层是否还有漏打的(某种)敌人 + * @example core.hasEnemyLeft('greenSlime', ['sample0', 'sample1']) // 样板0层和1层是否有漏打的绿头怪 + * @param enemyId 敌人id,可选,默认为任意敌人 + * @param floorId 地图id或其数组,可选,默认为当前地图 + * @returns true表示有敌人被漏打,false表示敌人已死光 + */ + hasEnemyLeft(enemyId?: string, floorId?: string | string[]): boolean + + /** 获得所有怪物原始数据的一个副本 */ + getEnemys(): any + + /** 获得所有特殊属性定义 */ + getSpecials(): [number, string | ((enemy: Enemy) => string), string | ((enemy: Enemy) => string), + string | [number, number, number, number?], number?][] + + /** 获得所有特殊属性的颜色 */ + getSpecialColor(enemy: string | Enemy): Array + + /** 获得所有特殊属性的额外标记 */ + getSpecialFlag(enemy: string | Enemy): Array + + /** 获得怪物真实属性 */ + getEnemyInfo(enemy: string | Enemy, hero?: any, x?: number, y?: number, floorId?: string): { + hp: number + def: number + atk: number + money: number + exp: number + point: number + special: number | number[] + guards: Array<[number, number, string]> + [x: string]: any + } + + /** 获得战斗伤害信息(实际伤害计算函数) */ + getDamageInfo(enemy: string | Enemy, hero?: any, x?: number, y?: number, floorId?: string): { + mon_hp: number + mon_atk: number + mon_def: number + init_damage: number + per_damage: number + hero_per_damage: number + turn: number + damage: number + [x: string]: any + } +} + +type mapsConfig = { + onMap: boolean, ctx?: CanvasRenderingContext2D, + redraw?: boolean, postDraw?: (() => void)[] +} + +/** @file maps.js负责一切和地图相关的处理内容 */ +interface maps { + _loadFloor_doNotCopy(): string[] + _getAndRemoveBlock(x: number, y: number): [Block, any] + _drawBg_draw(floorId: string, toDrawCtx: CanvasRenderingContext2D, cacheCtx: CanvasRenderingContext2D, + config: mapsConfig): void + _drawBgFgMap(floorId: string | null | undefined, name: string, config: mapsConfig): void + _drawBg_drawBackground(floorId: string, config: mapsConfig): void + /** 绘制楼层贴图 */ + _drawFloorImages(floorId: string | null | undefined, ctx: string | CanvasRenderingContext2D, name: string, + images?: FloorImage[] | null, currStatus?: number | null, onMap?: boolean | null): void + _drawFg_draw(floorId: string | null | undefined, toDrawCtx: CanvasRenderingContext2D, + cacheCtx: CanvasRenderingContext2D, config: mapsConfig): void + _getBgFgMapArray(name: string, floorId?: string | null, noCache?: boolean | null): any[] + _generateMovableArray_arrays(floorId: string): { bgArray: number[][], fgArray: number[][], eventArray: number[][] } + _drawFloorImage(ctx: string | CanvasRenderingContext2D, name: string, one: FloorImage, + image: any, currStatus: any, onMap: boolean) + + /** + * 获取初始core.maps.blockInfo的一个拷贝 + */ + getBlocksInfo(): { + [x: string]: { + cls: string + id: string + name: string + cannotIn: string[] + cannotOut: string[] + canPass: boolean + [x: string]: any + } + } + + /** + * 根据图块id得到数字(地图矩阵中的值) + * @example core.getNumberById('yellowWall'); // 1 + * @param id 图块id + * @returns 图块的数字,定义在project\maps.js(请注意和project\icons.js中的“图块索引”相区分!) + */ + getNumberById(id: string): number + + /** + * 生成事件层矩阵 + * @example core.getMapArray('MT0'); // 生成主塔0层的事件层矩阵,隐藏的图块视为0 + * @param floorId 地图id,不填视为当前地图 + * @param showDisable 可选,true表示隐藏的图块也会被表示出来 + * @returns 事件层矩阵,注意对其阵元的访问是[y][x] + */ + getMapArray(floorId?: string, noCache?: boolean): number[][] + + /** 判定图块的事件层数字;不存在为0 */ + getMapNumber(floorId?: string, noCache?: boolean): number + + /** + * 生成背景层矩阵 + * @example core.getBgMapArray('MT0'); // 生成主塔0层的背景层矩阵,使用缓存 + * @param floorId 地图id,不填视为当前地图 + * @param noCache 可选,true表示不使用缓存 + * @returns 背景层矩阵,注意对其阵元的访问是[y][x] + */ + getBgMapArray(floorId?: string, noCache?: boolean): number[][] + + /** + * 生成前景层矩阵 + * @example core.getFgMapArray('MT0'); // 生成主塔0层的前景层矩阵,使用缓存 + * @param floorId 地图id,不填视为当前地图 + * @param noCache 可选,true表示不使用缓存 + * @returns 前景层矩阵,注意对其阵元的访问是[y][x] + */ + getFgMapArray(floorId?: string, noCache?: boolean): number[][] + + /** + * 判定背景层的一个位置是什么 + * @example core.getBgNumber(); // 判断主角脚下的背景层图块的数字 + * @param x 横坐标,不填为当前勇士坐标 + * @param y 纵坐标,不填为当前勇士坐标 + * @param floorId 地图id,不填视为当前地图 + * @param 可选,true表示不使用缓存而强制重算 + */ + getBgNumber(x?: number, y?: number, floorId?: string, noCache?: boolean): number + + /** + * 判定前景层的一个位置是什么 + * @example core.getFgNumber(); // 判断主角脚下的前景层图块的数字 + * @param x 横坐标,不填为当前勇士坐标 + * @param y 纵坐标,不填为当前勇士坐标 + * @param floorId 地图id,不填视为当前地图 + * @param 可选,true表示不使用缓存而强制重算 + */ + getFgNumber(x?: number, y?: number, floorId?: string, noCache?: boolean): number + + /** + * 可通行性判定 + * @example core.generateMovableArray(); // 判断当前地图主角从各点能向何方向移动 + * @param floorId 地图id,不填视为当前地图 + * @returns 从各点可移动方向的三维数组 + */ + generateMovableArray(floorId?: string): Array>> + + /** + * 单点单朝向的可通行性判定 + * @exmaple core.canMoveHero(); // 判断主角是否可以前进一步 + * @param x 起点横坐标,不填视为主角当前的 + * @param y 起点纵坐标,不填视为主角当前的 + * @param direction 移动的方向,不填视为主角面对的方向 + * @param floorId 地图id,不填视为当前地图 + * @returns true表示可移动,false表示不可移动 + */ + canMoveHero(x?: number, y?: number, direction?: direction, floorId?: string): boolean + + /** + * 能否瞬移到某点,并求出节约的步数。 + * @example core.canMoveDirectly(0, 0); // 能否瞬移到地图左上角 + * @param destX 目标点的横坐标 + * @param destY 目标点的纵坐标 + * @returns 正数表示节约的步数,-1表示不可瞬移 + */ + canMoveDirectly(destX: number, destY: number): number + + /** + * 自动寻路 + * @example core.automaticRoute(0, 0); // 自动寻路到地图左上角 + * @param destX 目标点的横坐标 + * @param destY 目标点的纵坐标 + * @returns 每步走完后主角的loc属性组成的一维数组 + */ + automaticRoute(destX: number, destY: number): Array<{ direction: direction, x: number, y: number }> + + /** + * 地图绘制 + * @example core.drawMap(); // 绘制当前地图 + * @param floorId 地图id,省略表示当前楼层 + * @param callback 重绘完毕后的回调函数,可选 + */ + drawMap(floorId?: string, callback?: () => void): void + + /** + * 重绘地图 + */ + redrawMap(): void + + /** + * 绘制背景层(含贴图,其与背景层矩阵的绘制顺序可通过复写此函数来改变) + * @example core.drawBg(); // 绘制当前地图的背景层 + * @param floorId 地图id,不填视为当前地图 + * @param ctx 某画布的ctx,用于绘制缩略图,一般不需要 + */ + drawBg(floorId?: string, ctx?: CanvasRenderingContext2D): void + + /** + * 绘制事件层 + * @example core.drawEvents(); // 绘制当前地图的事件层 + * @param floorId 地图id,不填视为当前地图 + * @param blocks 一般不需要 + * @param ctx 某画布的ctx,用于绘制缩略图,一般不需要 + */ + drawEvents(floorId?: string, blocks?: Block[], ctx?: CanvasRenderingContext2D): void + + /** + * 绘制前景层(含贴图,其与前景层矩阵的绘制顺序可通过复写此函数来改变) + * @example core.drawFg(); // 绘制当前地图的前景层 + * @param floorId 地图id,不填视为当前地图 + * @param config + */ + drawFg(floorId?: string, config?: mapsConfig): void + + /** + * 绘制缩略图 + * @example core.drawThumbnail(); // 绘制当前地图的缩略图 + * @param floorId 地图id,不填视为当前地图 + * @param blocks 一般不需要 + * @param options 额外的绘制项,可选。可以增绘主角位置和朝向、采用不同于游戏中的主角行走图、增绘显伤、提供flags用于存读档 + */ + drawThumbnail(floorId?: string, blocks?: Block[], options?: { + heroLoc?: [number, number] + heroIcon?: string + /** 是否绘制显伤 */ + damage?: boolean + /** 存读档时使用,可以无视 */ + flags?: { [x: string]: any } + ctx?: CtxRefer + x?: number + y?: number + /** 绘制大小 */ + size?: number + /** 绘制全图 */ + all?: boolean + /** 绘制的视野中心 */ + centerX?: number + /** 绘制的视野中心 */ + centerY?: number + /** 存读档时使用,可以无视 */ + noHD: boolean + }): void + + /** + * 判定某个点是否不可被踏入(不基于主角生命值和图块cannotIn属性) + * @example core.noPass(0, 0); // 判断地图左上角能否被踏入 + * @param x 目标点的横坐标 + * @param y 目标点的纵坐标 + * @param floorId 目标点所在的地图id,不填视为当前地图 + * @returns true表示可踏入 + */ + noPass(x: number, y: number, floorId?: string): boolean + + /** + * 判定某个点的图块id + * @example if(core.getBlockId(x1, y1) != 'greenSlime' && core.getBlockId(x2, y2) != 'redSlime') core.openDoor(x3, y3); // 一个简单的机关门事件,打败或炸掉这一对绿头怪和红头怪就开门 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + * @param showDisable 隐藏点是否不返回null,true表示不返回null + * @returns 图块id,该点无图块则返回null + */ + getBlockId(x: number, y: number, floorId?: string, showDisable?: boolean): string | null + + /** 判定某个点的图块数字;空图块为0 */ + getBlockNumber(x: number, y: number, floorId?: string, showDisable?: boolean): number + + /** + * 判定某个点的图块类型 + * @example if(core.getBlockCls(x1, y1) != 'enemys' && core.getBlockCls(x2, y2) != 'enemy48') core.openDoor(x3, y3); // 另一个简单的机关门事件,打败或炸掉这一对不同身高的敌人就开门 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + * @param showDisable 隐藏点是否不返回null,true表示不返回null + * @returns 图块类型,即“地形、四帧动画、矮敌人、高敌人、道具、矮npc、高npc、自动元件、额外地形”之一 + */ + getBlockCls(x: number, y: number, floorId?: string, showDisable?: boolean): 'terrains' | 'animates' | 'enemys' | 'enemy48' | 'items' | 'npcs' | 'npc48' | 'autotile' | 'tileset' | null + + /** + * 搜索图块, 支持通配符 + * @example core.searchBlock('*Door'); // 搜索当前地图的所有门 + * @param id 图块id,支持星号表示任意多个(0个起)字符 + * @param floorId 地图id,不填视为当前地图 + * @param showDisable 隐藏点是否计入,true表示计入 + * @returns 一个详尽的数组,一般只用到其长度 + */ + searchBlock(id: string, floorId?: string | Array, showDisable?: boolean): Array<{ floorId: string, index: number, x: number, y: number, block: Block }> + + /** + * 根据给定的筛选函数搜索全部满足条件的图块 + * @example core.searchBlockWithFilter(function (block) { return block.event.id.endsWith('Door'); }); // 搜索当前地图的所有门 + * @param blockFilter 筛选函数,可接受block输入,应当返回一个boolean值 + * @param floorId 地图id,不填视为当前地图 + * @param showDisable 隐藏点是否计入,true表示计入 + * @returns 一个详尽的数组 + */ + searchBlockWithFilter(blockFilter: (block: Block) => boolean, floorId?: string | Array, showDisable?: boolean): Array<{ floorId: string, index: number, x: number, y: number, block: Block }> + + /** + * 显示(隐藏或显示的)图块,此函数将被“显示事件”指令和勾选了“不消失”的“移动/跳跃事件”指令(如阻击怪)的终点调用 + * @example core.showBlock(0, 0); // 显示地图左上角的图块 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + showBlock(x: number, y: number, floorId?: string): void + + /** + * 隐藏一个图块,对应于「隐藏事件」且不删除 + * @example core.hideBlock(0, 0); // 隐藏地图左上角的图块 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + hideBlock(x: number, y: number, floorId?: string): void + + /** + * 删除一个图块,对应于「隐藏事件」并同时删除 + * @example core.removeBlock(0, 0); // 尝试删除地图左上角的图块 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + removeBlock(x: number, y: number, floorId?: string): void + + /** + * 转变图块 + * @example core.setBlock(1, 0, 0); // 把地图左上角变成黄墙 + * @param number 新图块的数字(也支持纯数字字符串如'1')或id + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + setBlock(number: number | string, x: number, y: number, floorId?: string): void + + /** + * 设置某个点的不透明度 + * @param opacity 不透明度,范围0~1 + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + setBlockOpacity(opacity: number, x: number, y: number, floorId: string): void + + /** + * 批量替换图块 + * @example core.replaceBlock(21, 22, core.floorIds); // 把游戏中地上当前所有的黄钥匙都变成蓝钥匙 + * @param fromNumber 旧图块的数字 + * @param toNumber 新图块的数字 + * @param floorId 地图id或其数组,不填视为当前地图 + */ + replaceBlock(fromNumber: number, toNumber: number, floorId?: string | Array): void + + /** + * 转变图层块 + * @example core.setBgFgBlock('bg', 167, 6, 6); // 把当前地图背景层的中心块改为滑冰 + * @param name 背景还是前景 + * @param number 新图层块的数字(也支持纯数字字符串如'1')或id + * @param x 横坐标 + * @param y 纵坐标 + * @param floorId 地图id,不填视为当前地图 + */ + setBgFgBlock(name: 'bg' | 'fg', number: number | string, x: number, y: number, floorId?: string): void + + /** + * 改变事件层图块的连通性 + * @example core.setBlockConnectivity(165, 'noPass', true); + * @example core.setBlockConnectivity(162, 'cannotIn', ['left', 'right']); + * @param num 图层的数字或id + * @param statusName 要修改的图块属性的名称 + * @param statusValue 要修改到的值 + */ + setBlockConnectivity(num: number | string, statusName: 'noPass' | 'cannotIn' | 'cannotOut', statusValue: unknown): void + + /** + * 移动图块 + * @example core.moveBlock(0, 0, ['down']); // 令地图左上角的图块下移一格,用时半秒,再花半秒淡出 + * @param x 起点的横坐标 + * @param y 起点的纵坐标 + * @param steps 步伐数组 + * @param time 单步和淡出用时,单位为毫秒。不填视为半秒 + * @param keep 是否不淡出,true表示不淡出 + * @param callback 移动或淡出后的回调函数,可选 + */ + moveBlock(x: number, y: number, steps: step[], time?: number, keep?: boolean, callback?: () => void): void + + /** + * 跳跃图块;从V2.7开始不再有音效 + * @example core.jumpBlock(0, 0, 0, 0); // 令地图左上角的图块原地跳跃半秒,再花半秒淡出 + * @param sx 起点的横坐标 + * @param sy 起点的纵坐标 + * @param ex 终点的横坐标 + * @param ey 终点的纵坐标 + * @param time 单步和淡出用时,单位为毫秒。不填视为半秒 + * @param keep 是否不淡出,true表示不淡出 + * @param callback 落地或淡出后的回调函数,可选 + */ + jumpBlock(sx: number, sy: number, ex: number, ey: number, time?: number, keep?: boolean, callback?: () => void): void + + /** + * 播放动画,注意即使指定了主角的坐标也不会跟随主角移动,如有需要请使用core.drawHeroAnimate(name, callback)函数 + * @example core.drawAnimate('attack', core.nextX(), core.nextY(), false, core.vibrate); // 在主角面前一格播放普攻动画,动画停止后视野左右抖动1秒 + * @param name 动画文件名,不含后缀 + * @param x 绝对横坐标 + * @param y 绝对纵坐标 + * @param alignWindow 是否是相对窗口的坐标 + * @param callback 动画停止后的回调函数,可选 + * @returns 一个数字,可作为core.stopAnimate()的参数来立即停止播放(届时还可选择是否执行此次播放的回调函数) + */ + drawAnimate(name: string, x: number, y: number, alignWindow?: boolean, callback?: () => void): number + + /** + * 播放跟随勇士的动画 + * @param name 动画名 + * @param callback 动画停止后的回调函数,可选 + * @returns 一个数字,可作为core.stopAnimate()的参数来立即停止播放(届时还可选择是否执行此次播放的回调函数) + */ + drawHeroAnimate(name: string, callback?: () => void): number + + /** + * 立刻停止一个动画播放 + * @param id 播放动画的编号,即drawAnimate或drawHeroAnimate返回值 + * @param doCallback 是否执行该动画的回调函数 + */ + stopAnimate(id?: number, doCallback?: boolean): void + + /** 获得当前正在播放的所有(指定)动画的id列表 */ + getPlayingAnimates(name?: string): Array + + /** 加载某个楼层(从剧本或存档中) */ + loadFloor(floorId: string, map?: any): ResolvedMap + + /** 根据需求解析出blocks */ + extractBlocks(map?: any): void + + /** 根据需求为UI解析出blocks */ + extractBlocks(map?: any, flags?: any): void + + /** 根据数字获得图块 */ + getBlockByNumber(number: number): Block + + /** 根据ID获得图块 */ + getBlockById(id: string): Block + + /** 获得当前事件点的ID */ + getIdOfThis(id?: string): string + + /** 初始化一个图块 */ + initBlock(x?: number, y?: number, id?: string | number, addInfo?: boolean, eventFloor?: any): Block + + /** 压缩地图 */ + compressMap(mapArr?: any, floorId?: string): object + + /** 解压缩地图 */ + decompressMap(mapArr?: any, floorId?: string): object + + /** 将当前地图重新变成数字,以便于存档 */ + saveMap(floorId?: string): any + + /** 将存档中的地图信息重新读取出来 */ + loadMap(data?: any, floorId?: string, flags?: any): object + + /** 更改地图画布的尺寸 */ + resizeMap(floorId?: string): void + + /** 以x,y的形式返回每个点的事件 */ + getMapBlocksObj(floorId?: string, noCache?: boolean): object + + /** 获得某些点可否通行的信息 */ + canMoveDirectlyArray(locs?: any): object + + /** 绘制一个图块 */ + drawBlock(block?: any, animate?: any): void + + /** 生成groundPattern */ + generateGroundPattern(floorId?: string): void + + /** 某个点是否存在NPC */ + npcExists(x?: number, y?: number, floorId?: string): boolean + + /** 某个点是否存在(指定的)地形 */ + terrainExists(x?: number, y?: number, id?: string, floorId?: string): boolean + + /** 某个点是否存在楼梯 */ + stairExists(x?: number, y?: number, floorId?: string): boolean + + /** 当前位置是否在楼梯边;在楼传平面塔模式下对箭头也有效 */ + nearStair(): boolean + + /** 当前位置是否可以直接移动到某个楼梯附近的空地 */ + accessStair(): boolean + + /** 当前位置是否满足使用楼传的条件 */ + canUseFlyHere(): boolean + + /** 某个点是否存在(指定的)怪物 */ + enemyExists(x?: number, y?: number, id?: string, floorId?: string): boolean + + /** 获得某个点的block */ + getBlock(x?: number, y?: number, floorId?: string, showDisable?: boolean): Block + + /** 获得某个图块或素材的信息,包括ID,cls,图片,坐标,faceIds等等 */ + getBlockInfo(block?: any): any + + /** 获得某个图块对应行走图朝向向下的那一项的id;如果不存在行走图绑定则返回自身id */ + getFaceDownId(block?: any): string + + /** 根据图块的索引来隐藏图块 */ + hideBlockByIndex(index?: any, floorId?: string): void + + /** 一次性隐藏多个block */ + hideBlockByIndexes(indexes?: any, floorId?: string): void + + /** 根据block的索引(尽可能)删除该块 */ + removeBlockByIndex(index?: any, floorId?: string): void + + /** 一次性删除多个block */ + removeBlockByIndexes(indexes?: any, floorId?: string): void + + /** 显示前景/背景地图 */ + showBgFgMap(name?: string, loc?: any, floorId?: string, callback?: () => any): void + + /** 隐藏前景/背景地图 */ + hideBgFgMap(name?: string, loc?: any, floorId?: string, callback?: () => any): void + + /** 显示一个楼层贴图 */ + showFloorImage(loc?: any, floorId?: string, callback?: () => any): void + + /** 隐藏一个楼层贴图 */ + hideFloorImage(loc?: any, floorId?: string, callback?: () => any): void + + /** 动画形式转变某点图块 */ + animateSetBlock(number?: number | string, x?: number, y?: number, floorId?: string, time?: number, callback?: () => any): void + + /** 动画形式同时转变若干点图块 */ + animateSetBlocks(number?: number | string, locs?: any, floorId?: string, time?: number, callback?: () => any): void + + /** 事件转向 */ + turnBlock(direction?: string, x?: number, y?: number, floorId?: string): void + + /** 重置地图 */ + resetMap(floorId?: string | string[]): void + + /** 显示/隐藏某个块时的动画效果 */ + animateBlock(loc?: any, type?: any, time?: any, callback?: () => any): void + + /** 添加一个全局动画 */ + addGlobalAnimate(block?: any): void + + /** 删除一个或所有全局动画 */ + removeGlobalAnimate(x?: number, y?: number, name?: string): void + + /** 绘制UI层的box动画 */ + drawBoxAnimate(): void +} + +/** @file loader.js 主要负责资源的加载 */ +interface loader { + /** 加载一系列图片 */ + loadImages(dir: any, names: any, toSave: any, callback?: () => any): any + + /** 加载某一张图片 */ + loadImage(dir: any, imgName?: any, callback?: () => any): any + + /** 从zip中加载一系列图片 */ + loadImagesFromZip(url?: any, names?: any, toSave?: any, onprogress?: any, onfinished?: any): any + + /** 加载一个音乐 */ + loadOneMusic(name?: string): any + + /** 加载一个音效 */ + loadOneSound(name?: string): any + + /** 加载一个bgm */ + loadBgm(name?: string): any + + /** 释放一个bgm的缓存 */ + freeBgm(name?: string): any +} + +/** @file items.js 主要负责一切和道具相关的内容。 */ +interface items { + _loadEquipEffect(equipId?: string | null, unloadEquipId?: string | null); + + /** + * 获得给定ratio下(不填默认当前地图倍率)该即捡即用类的道具获得时勇士属性的增加效果 + * @example core.getItemEffectValue('redGem',1) // 获得倍率为1时红宝石增加的属性值 + * @param itemId 道具id + * @param ratio 倍率,不填默认当前地图倍率 + */ + getItemEffectValue(itemId: string, ratio?: number): { [key: string]: number } + + /** + * 即捡即用类的道具获得时的效果 + * @example core.getItemEffect('redPotion', 10) // 执行获得10瓶红血的效果 + * @param itemId 道具id + * @param itemNum 道具数量,可选,默认为1 + */ + getItemEffect(itemId: string, itemNum?: number): void + + /** + * 获取“即捡即用类”道具会增加哪些属性 + * @example core.getItemEffect('redPotion') // 返回红血增加的属性列表 + * @param itemId 道具id + */ + getItemEffectType(itemId: string): string[] + + /** + * 即捡即用类的道具获得时的额外提示 + * @example core.getItemEffectTip(redPotion) // (获得 红血瓶)',生命+100' + * @param itemId 道具id + * @returns 图块属性itemEffectTip的内容 + */ + getItemEffectTip(itemId: string): string + + /** + * 使用一个道具 + * @example core.useItem('pickaxe', true) // 使用破墙镐,不计入录像,无回调 + * @param itemId 道具id + * @param noRoute 是否不计入录像,快捷键使用的请填true,否则可省略 + * @param callback 道具使用完毕或使用失败后的回调函数 + */ + useItem(itemId: string, noRoute?: boolean, callback?: () => void): void + + /** + * 检查能否使用某种道具 + * @example core.canUseItem('pickaxe') // 能否使用破墙镐 + * @param itemId 道具id + * @returns true表示可以使用 + */ + canUseItem(itemId: string): boolean + + /** + * 统计某种道具的持有量 + * @example core.itemCount('yellowKey') // 持有多少把黄钥匙 + * @param itemId 道具id + * @returns 该种道具的持有量,不包括已穿戴的装备 + */ + itemCount(itemId: string): number + + /** + * 检查主角是否持有某种道具(不包括已穿戴的装备) + * @example core.hasItem('yellowKey') // 主角是否持有黄钥匙 + * @param itemId 道具id + * @returns true表示持有 + */ + hasItem(itemId: string): boolean + + /** + * 检查主角是否穿戴着某件装备 + * @example core.hasEquip('sword5') // 主角是否装备了神圣剑 + * @param itemId 装备id + * @returns true表示已装备 + */ + hasEquip(itemId: string): boolean + + /** + * 检查主角某种类型的装备目前是什么 + * @example core.getEquip(1) // 主角目前装备了什么盾牌 + * @param equipType 装备类型,自然数 + * @returns 装备id,null表示未穿戴 + */ + getEquip(equipType: number): string + + /** + * 设置某种道具的持有量 + * @example core.setItem('yellowKey', 3) // 设置黄钥匙为3把 + * @param itemId 道具id + * @param itemNum 新的持有量,可选,自然数,默认为0 + */ + setItem(itemId: string, itemNum?: number): void + + /** + * 静默增减某种道具的持有量 不会更新游戏画面或是显示提示 + * @example core.addItem('yellowKey', -2) // 没收两把黄钥匙 + * @param itemId 道具id + * @param itemNum 增加量,负数表示没收 + */ + addItem(itemId: string, itemNum?: number): void + + /** + * 判定某件装备的类型 + * @example core.getEquipTypeById('shield5') // 1(盾牌) + * @param equipId 装备id + * @returns 类型编号,自然数 + */ + getEquipTypeById(equipId: string): number + + /** + * 检查能否穿上某件装备 + * @example core.canEquip('sword5', true) // 主角可以装备神圣剑吗,如果不能会有提示 + * @param equipId 装备id + * @param hint 无法穿上时是否提示(比如是因为未持有还是别的什么原因) + * @returns true表示可以穿上,false表示无法穿上 + */ + canEquip(equipId: string, hint: boolean): boolean + + /** + * 尝试穿上某件背包里的装备并提示 + * @example core.loadEquip('sword5') // 尝试装备上背包里的神圣剑,无回调 + * @param equipId 装备id + * @param callback 穿戴成功或失败后的回调函数 + */ + loadEquip(equipId: string, callback?: () => void): void + + /** + * 脱下某个类型的装备 + * @example core.unloadEquip(1) // 卸下盾牌,无回调 + * @param equipType 装备类型编号,自然数 + * @param callback 卸下装备后的回调函数 + */ + unloadEquip(equipType: number, callback?: () => void): void + + /** + * 比较两件(类型可不同)装备的优劣 + * @example core.compareEquipment('sword5', 'shield5') // 比较神圣剑和神圣盾的优劣 + * @param compareEquipId 装备甲的id + * @param beComparedEquipId 装备乙的id + * @returns 两装备的各属性差,甲减乙,0省略 + */ + compareEquipment(compareEquipId?: string | null, beComparedEquipId?: string | null): { [key: string]: number } + + /** + * 保存当前套装 + * @example core.quickSaveEquip(1) // 将当前套装保存为1号套装 + * @param index 套装编号,自然数 + */ + quickSaveEquip(index: number): void + + /** + * 快速换装 + * @example core.quickLoadEquip(1) // 快速换上1号套装 + * @param index 套装编号,自然数 + */ + quickLoadEquip(index: number): void + + /** 获得所有道具 */ + getItems(): void + + /** 删除某个物品 */ + removeItem(itemId?: string, itemNum?: number): void + + /** 根据类型获得一个可用的装备孔 */ + getEquipTypeByName(name?: string): number + + /** + * 设置某个装备的属性并计入存档 + * @example core.setEquip('sword1', 'value', 'atk', 300, '+='); // 设置铁剑的攻击力数值再加300 + * @param equipId 装备id + * @param valueType 增幅类型,只能是value(数值)或percentage(百分比) + * @param name 要修改的属性名称,如atk + * @param value 要修改到的属性数值 + * @param operator 操作符,可选,如+=表示在原始值上增加 + * @param prefix 独立开关前缀,一般不需要 + */ + setEquip(equipId: string, valueType: string, name: string, value: any, operator?: string, prefix?: string): void +} + +/** @file ui.js 主要用来进行UI窗口的绘制,如对话框、怪物手册、楼传器、存读档界面等等。*/ +interface ui { + _buildFont(fontSize?: number | string, bold?: boolean, italic?: boolean, font?: string): string + _createUIEvent(): void + _drawBook_drawName(index: number, enemy: Enemy, top: number, left: number, width: number): void + _drawBook_drawRow1(index: number, enemy: Enemy, top: number, left: number, width: number, position: number): void + _drawToolbox(index: number): void + _drawEquipbox(index: number): void + + /** + * 根据画布名找到一个画布的context;支持系统画布和自定义画布。如果不存在画布返回null。 + * 也可以传画布的context自身,则返回自己。 + */ + getContextByName(canvas: CtxRefer): CanvasRenderingContext2D + + /** + * 清空某个画布图层 + * name为画布名,可以是系统画布之一,也可以是任意自定义动态创建的画布名;还可以直接传画布的context本身。(下同) + * 如果name也可以是'all',若为all则为清空所有系统画布。 + */ + clearMap(name: CtxRefer): void + /** + * 清空某个画布图层的指定区域 + */ + clearMap(name: CtxRefer, x: number, y: number, w: number, h: number): void + + /** + * 在某个画布上绘制一段文字 + * @param text 要绘制的文本 + * @param style 绘制的样式 + * @param font 绘制的字体 + */ + fillText(name: CtxRefer, text: string | number, x: number, y: number, style?: string | null, font?: string | null, maxWidth?: number | null): void + + /** + * 在某个画布上绘制一个描黑边的文字 + * @param text 要绘制的文本 + * @param style 绘制的样式 + * @param strokeStyle 绘制的描边颜色 + * @param font 绘制的字体 + */ + fillBoldText(name: CtxRefer, text: string, x: number, y: number, style?: string, strokeStyle?: string, font?: string, maxWidth?: number): void + + /** + * 绘制一个矩形。style可选为绘制样式 + * @param style 绘制的样式 + * @param angle 旋转角度,弧度制 + */ + fillRect(name: CtxRefer, x: number, y: number, width: number, height: number, style?: string, angle?: number): void + + /** + * 绘制一个矩形的边框 + * @param style 绘制的样式 + */ + strokeRect(name: CtxRefer, x: number, y: number, width: number, height: number, style: string, lineWidth?: number, angle?: number): void + + /** + * 动态创建一个画布。name为要创建的画布名,如果已存在则会直接取用当前存在的。 + * x,y为创建的画布相对窗口左上角的像素坐标,width,height为创建的长宽。 + * zIndex为创建的纵向高度(关系到画布之间的覆盖),z值高的将覆盖z值低的;系统画布的z值可在个性化中查看。 + * 返回创建的画布的context,也可以通过core.dymCanvas[name]调用 + */ + createCanvas(name: string, x: number, y: number, width: number, height: number, zIndex: number): CanvasRenderingContext2D + + /** 重新定位一个自定义画布 */ + relocateCanvas(name: string, x: number, y: number, useDelta: boolean): void + + /** + * 重新设置一个自定义画布的大小 + * @param styleOnly 是否只修改style,而不修改元素上的长宽,如果是true,会出现模糊现象 + * @param isTempCanvas 是否是临时画布,如果填true,会将临时画布修改为高清画布 + */ + resizeCanvas(name: string, x?: number, y?: number, styleOnly?: boolean, isTempCanvas?: boolean): void + + /** 设置一个自定义画布的旋转角度 */ + rotateCanvas(name: string, angle: number, centerX?: number, centerY?: number): void + + /** 删除一个自定义画布 */ + deleteCanvas(name: string | ((name: string) => boolean)): void + + /** 清空所有的自定义画布 */ + deleteAllCanvas(): void + + /** + * 在一个画布上绘制图片 + * 后面的8个坐标参数与canvas的drawImage的八个参数完全相同。 + * 请查看 http://www.w3school.com.cn/html5/canvas_drawimage.asp 了解更多。 + * @param name 可以是系统画布之一,也可以是任意自定义动态创建的画布名 画布名称或者画布的context + * @param image 要绘制的图片,可以是一个全塔属性中定义的图片名(会从images中去获取),图片本身,或者一个画布。 + */ + drawImage(name: CtxRefer, + image: CanvasImageSource | string, dx: number, dy: number): void + drawImage(name: CtxRefer, + image: CanvasImageSource | string, dx: number, dy: number, dw: number, dh: number): void + drawImage(name: CtxRefer, + image: CanvasImageSource | string, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void + + /** 根据最大宽度自动缩小字体 */ + setFontForMaxWidth(name: string | CanvasRenderingContext2D, text: string, maxWidth: number, font?: any): string + + /** 在某个canvas上绘制一个圆角矩形 */ + fillRoundRect(name: string | CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, style?: string, angle?: number): void + + /** 在某个canvas上绘制一个圆角矩形的边框 */ + strokeRoundRect(name: string | CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, style?: string, lineWidth?: number, angle?: number): void + + /** 在某个canvas上绘制一个多边形 */ + fillPolygon(name: string | CanvasRenderingContext2D, nodes?: any, style?: string): void + + /** 在某个canvas上绘制一个多边形的边框 */ + strokePolygon(name: string | CanvasRenderingContext2D, nodes?: any, style?: string, lineWidth?: number): void + + /** 在某个canvas上绘制一个椭圆 */ + fillEllipse(name: string | CanvasRenderingContext2D, x: number, y: number, a: number, b: number, angle?: number, style?: any): void + + /** 在某个canvas上绘制一个圆 */ + fillCircle(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, style?: string): void + + /** 在某个canvas上绘制一个椭圆的边框 */ + strokeEllipse(name: string | CanvasRenderingContext2D, x: number, y: number, a: number, b: number, angle?: number, style?: string, lineWidth?: number): void + + /** 在某个canvas上绘制一个圆的边框 */ + strokeCircle(name: string | CanvasRenderingContext2D, x: number, y: number, r: any, style?: string, lineWidth?: number): void + + /** 在某个canvas上绘制一个扇形 */ + fillArc(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, start: number, end: number, style?: string): void + + /** 在某个canvas上绘制一段弧 */ + strokeArc(name: string | CanvasRenderingContext2D, x: number, y: number, r: number, start: number, end: number, style?: string, lineWidth?: number): void + + /** 保存某个canvas状态 */ + saveCanvas(name: string | CanvasRenderingContext2D): void + + /** 加载某个canvas状态 */ + loadCanvas(name: string | CanvasRenderingContext2D): void + + /** 设置某个canvas的baseline */ + setTextBaseline(name: string | CanvasRenderingContext2D, baseline: 'alphabetic' | 'top' | 'hanging' | 'middle' | 'ideographic' | 'bottom'): void + + /** 字符串自动换行的分割 */ + splitLines(name: string | CanvasRenderingContext2D, text: string, maxWidth?: number, font?: string): void + + /** 在某个canvas上绘制一个图标 */ + drawIcon(name: string | CanvasRenderingContext2D, id: string, x: number, y: number, w?: number, h?: number, frame?: number): void + + /** 结束一切事件和绘制,关闭UI窗口,返回游戏进程 */ + closePanel(): void + + /** 清空UI层内容 */ + clearUI(): void + + /** + * 左上角绘制一段提示 + * @param text 要提示的文字内容,支持 ${} 语法 + * @param id 要绘制的图标ID + * @param frame 要绘制图标的第几帧 + */ + drawTip(text: string, id?: string, frame?: number): void + + /** + * 左上角绘制一段提示同时播放错误音效 + * @param text 要提示的文字内容,支持 ${} 语法 + * @param id 要绘制的图标ID + * @param frame 要绘制图标的第几帧 + */ + drawFailTip(text: string, id?: string, frame?: number): void + + /** + * 左上角绘制一段提示同时播放成功音效 + * @param text 要提示的文字内容,支持 ${} 语法 + * @param id 要绘制的图标ID + * @param frame 要绘制图标的第几帧 + */ + drawSuccessTip(text: string, id?: string, frame?: number): void + + /** 地图中间绘制一段文字 */ + drawText(contents: string, callback?: () => any): void + + /** 自绘选择光标 */ + drawUIEventSelector(code: number, background: string, x: number, y: number, w: number, h: number, z?: number): void + + /** 清除一个或多个选择光标 */ + clearUIEventSelector(code?: number | number[] | null): void + + /** 绘制一个确认框 */ + drawConfirmBox(text: string, yesCallback?: () => void, noCallback?: () => void): void + + /** 绘制WindowSkin */ + drawWindowSkin(background: any, ctx: string | CanvasRenderingContext2D, x: number, y: number, w: number, h: number, direction?: any, px?: any, py?: any): void + + /** 绘制一个背景图,可绘制winskin或纯色背景;支持小箭头绘制 */ + drawBackground(left: string, top: string, right: string, bottom: string, posInfo?: any): void + + /** + * 绘制一段文字到某个画布上面 + * @param ctx 要绘制到的画布 + * @param content 要绘制的内容;转义字符只允许保留 \n, \r[...], \i[...], \c[...], \d, \e + * @param config 绘制配置项,目前暂时包含如下内容(均为可选) + * left, top:起始点位置;maxWidth:单行最大宽度;color:默认颜色;align:左中右 + * fontSize:字体大小;lineHeight:行高;time:打字机间隔;font:默认字体名 + * @returns 绘制信息 + */ + drawTextContent(ctx: string | CanvasRenderingContext2D, content: string, config: { + left?: number + top?: number + maxWidth?: number + color?: number | string + align?: 'left' | 'center' | 'right' + fontSize: number + lineHeight?: number + time?: number + font?: string + letterSpacing?: number + bold?: boolean + italic?: boolean + }): any + + /** 获得某段文字的预计绘制高度;参见 drawTextContent */ + getTextContentHeight(content: string, config?: any): number + + /** 绘制一个对话框 */ + drawTextBox(content: string, config?: any): void + + /** 绘制滚动字幕 */ + drawScrollText(content: string, time: number, lineHeight?: number, callback?: () => any): void + + /** 文本图片化 */ + textImage(content: string, lineHeight?: number): any + + /** 绘制一个选项界面 */ + drawChoices(content: string, choices: any): void + + /** 绘制等待界面 */ + drawWaiting(text: string): void + + /** 绘制分页 */ + drawPagination(page?: any, totalPage?: any, y?: number): void + + /** 绘制怪物手册 */ + drawBook(index?: any): void + + /** 绘制楼层传送器 */ + drawFly(page?: any): void + + /** 获得所有应该在道具栏显示的某个类型道具 + * @param cls 道具类型 + * @param cls 是否显示隐藏的道具 + */ + getToolboxItems(cls: 'equips' | 'tools' | 'constants' | 'all', showHide?: boolean): string[] + + /** 绘制状态栏 */ + drawStatusBar(): void + + /** 绘制灯光效果 */ + drawLight(name: string | CanvasRenderingContext2D, color?: any, lights?: any, lightDec?: number): void + + /** 在某个canvas上绘制一条线 */ + drawLine(name: string | CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number, style?: string, lineWidth?: number): void + + /** 在某个canvas上绘制一个箭头 */ + drawArrow(name: string | CanvasRenderingContext2D, x1: number, y1: number, x2: number, y2: number, style?: string, lineWidth?: number): void + + /** 设置某个canvas的文字字体 */ + setFont(name: string | CanvasRenderingContext2D, font: string): void + + /** 设置某个canvas的线宽度 */ + setLineWidth(name: string | CanvasRenderingContext2D, lineWidth: number): void + + /** 设置某个canvas的alpha值;返回设置之前画布的不透明度。 */ + setAlpha(name: string | CanvasRenderingContext2D, alpha: number): number + + /** 设置某个canvas的filter属性 */ + setFilter(name: string | CanvasRenderingContext2D, filter: any): void + + /** 设置某个canvas的透明度;尽量不要使用本函数,而是全部换成setAlpha实现 */ + setOpacity(name: string | CanvasRenderingContext2D, opacity: number): void + + /** 设置某个canvas的绘制属性(如颜色等) */ + setFillStyle(name: string | CanvasRenderingContext2D, style: string): void + + /** 设置某个canvas边框属性 */ + setStrokeStyle(name: string | CanvasRenderingContext2D, style: string): void + + /** 设置某个canvas的对齐 */ + setTextAlign(name: string | CanvasRenderingContext2D, align: string): void + + /** 计算某段文字的宽度 */ + calWidth(name: string | CanvasRenderingContext2D, text: string, font?: string): number +} + +/** 工具类 主要用来进行一些辅助函数的计算 */ +interface utils { + readonly scan: { + 'up': { 'x': 0, 'y': -1 }, + 'left': { 'x': -1, 'y': 0 }, + 'down': { 'x': 0, 'y': 1 }, + 'right': { 'x': 1, 'y': 0 } + }; + readonly scan2: { + 'up': { 'x': 0, 'y': -1 }, + 'left': { 'x': -1, 'y': 0 }, + 'down': { 'x': 0, 'y': 1 }, + 'right': { 'x': 1, 'y': 0 }, + 'leftup': { 'x': -1, 'y': -1 }, + 'leftdown': { 'x': -1, 'y': 1 }, + 'rightup': { 'x': 1, 'y': -1 }, + 'rightdown': { 'x': 1, 'y': 1 } + }; + /** 初始化种子 */ + __init_seed(): void + + /** + * 将一段文字中的${}(表达式)进行替换。 + * @example core.replaceText('衬衫的价格是${status:hp}镑${item:yellowKey}便士。'); // 把主角的生命值和持有的黄钥匙数量代入这句话 + * @param text 模板字符串,可以使用${}计算js表达式,支持“状态、物品、变量、独立开关、全局存储、图块id、图块类型、敌人数据、装备id”等量参与运算 + * @returns 替换完毕后的字符串 + */ + replaceText(text: string, prefix?: string): string + + /** + * 对一个表达式中的特殊规则进行替换,如status:xxx等。 + * @example core.replaceValue('衬衫的价格是${status:hp}镑${item:yellowKey}便士。'); // 把这两个冒号表达式替换为core.getStatus('hp')和core.itemCount('yellowKey')这样的函数调用 + * @param value 模板字符串,注意独立开关不会被替换 + * @returns 替换完毕后的字符串 + */ + replaceValue(value: string): string + + /** + * 计算一个表达式的值,支持status:xxx等的计算。 + * @example core.calValue('status:hp + status:def'); // 计算主角的生命值加防御力 + * @param value 待求值的表达式 + * @param prefix 独立开关前缀,一般可省略 + * @returns 求出的值 + */ + calValue(value: string, prefix?: string): any + + /** + * 将b(可以是另一个数组)插入数组a的开头,此函数用于弥补a.unshift(b)中b只能是单项的不足。 + * @example core.unshift(todo, {type: 'unfollow'}); // 在事件指令数组todo的开头插入“取消所有跟随者”指令 + * @param a 原数组 + * @param b 待插入的新首项或前缀数组 + * @returns 插入完毕后的新数组,它是改变原数组a本身得到的 + */ + unshift(a: any[], b: any): any[] + + /** + * 将b(可以是另一个数组)插入数组a的末尾,此函数用于弥补a.push(b)中b只能是单项的不足。 + * @example core.push(todo, {type: 'unfollow'}); // 在事件指令数组todo的末尾插入“取消所有跟随者”指令 + * @param a 原数组 + * @param b 待插入的新末项或后缀数组 + * @returns 插入完毕后的新数组,它是改变原数组a本身得到的 + */ + push(a: any[], b: any): any[] + + /** + * 设置一个全局存储,适用于global:xxx,录像播放时将忽略此函数。 + * @example core.setBlobal('一周目已通关', true); // 设置全局存储“一周目已通关”为true,方便二周目游戏中的新要素。 + * @param key 全局变量名称,支持中文 + * @param value 全局变量的新值,不填或null表示清除此全局存储 + */ + setGlobal(key: string, value?: any): void + + /** + * 读取一个全局存储,适用于global:xxx,支持录像。 + * @example if (core.getGlobal('一周目已通关', false) === true) core.getItem('dagger'); // 二周目游戏进行到此处时会获得一把屠龙匕首 + * @param key 全局变量名称,支持中文 + * @param defaultValue 可选,当此全局变量不存在或值为null、undefined时,用此值代替 + * @returns 全局变量的值 + */ + getGlobal(key: string, defaultValue?: any): any + + /** + * 深拷贝一个对象(函数将原样返回) + * @example core.clone(core.status.hero, (name, value) => (name == 'items' || typeof value == 'number'), false); // 深拷贝主角的属性和道具 + * @param data 待拷贝对象 + * @param filter 过滤器,可选,表示data为数组或对象时拷贝哪些项或属性,true表示拷贝 + * @param recursion 过滤器是否递归,可选。true表示过滤器也被递归 + * @returns 拷贝的结果,注意函数将原样返回 + */ + clone(data?: T, filter?: (name: string, value: any) => boolean, recursion?: boolean): T + + /** 深拷贝一个1D或2D的数组 */ + cloneArray(data?: Array | Array>): Array | Array> + + /** 将怪物特殊属性转为升序数组并去零 */ + parseSpecial(special: null | number | number[]): number[] + + /** 比较两个变量是否值相等 */ + deepEqual(x: unknown, y: unknown): boolean + + /** + * 等比例切分一张图片 + * @example core.splitImage(core.material.images.images['npc48.png'], 32, 48); // 把npc48.png切分成若干32×48px的小人 + * @param image 图片名(支持映射前的中文名)或图片对象(参见上面的例子),获取不到时返回[] + * @param width 子图的宽度,单位为像素。原图总宽度必须是其倍数,不填视为32 + * @param height 子图的高度,单位为像素。原图总高度必须是其倍数,不填视为正方形 + * @returns 子图组成的数组,在原图中呈先行后列,从左到右、从上到下排列。 + */ + splitImage(image?: string | HTMLImageElement, width?: number, height?: number): HTMLImageElement[] + + /** + * 大数字格式化,单位为10000的倍数(w,e,z,j,g),末尾四舍五入 + * @example core.formatBigNumber(123456789, false); // "12346w" + * @param x 原数字 + * @param onMap 可选,true表示用于地图显伤,结果总字符数最多为5,否则最多为6 + * @returns 格式化结果 + */ + formatBigNumber(x: number, onMap?: boolean | number): string + + /** 变速移动 */ + applyEasing(mode?: string): (number) => number; + + /** + * 颜色数组转十六进制 + * @example core.arrayToRGB([102, 204, 255]); // "#66ccff",加载画面的宣传色 + * @param color 一行三列的数组,各元素必须为不大于255的自然数 + * @returns 该颜色的十六进制表示,使用小写字母 + */ + arrayToRGB(color: [number, number, number]): string + + /** + * 颜色数组转字符串 + * @example core.arrayToRGBA([102, 204, 255]); // "rgba(102,204,255,1)" + * @param color 一行三列或一行四列的数组,前三个元素必须为不大于255的自然数。第四个元素(如果有)必须为0或不大于1的数字,第四个元素不填视为1 + * @returns 该颜色的字符串表示 + */ + arrayToRGBA(color: [number, number, number, number]): string + + /** + * 录像一压,其结果会被再次base64压缩 + * @example core.encodeRoute(core.status.route); // 一压当前录像 + * @param route 原始录像,自定义内容(不予压缩,原样写入)必须由0-9A-Za-z和下划线、冒号组成,所以中文和数组需要用JSON.stringify预处理再base64压缩才能交由一压 + * @returns 一压的结果 + */ + encodeRoute(route: string[]): string + + /** + * 录像解压的最后一步,即一压的逆过程 + * @example core.decodeRoute(core.encodeRoute(core.status.route)); // 一压当前录像再解压-_-| + * @param route 录像解压倒数第二步的结果,即一压的结果 + * @returns 原始录像 + */ + decodeRoute(route: string): string[] + + /** + * 判断一个值是否不为null,undefined和NaN + * @example core.isset(0/0); // false,因为0/0等于NaN + * @param v 待测值,可选 + * @returns false表示待测值为null、undefined、NaN或未填写,true表示为其他值。即!(v == null || v != v) + */ + isset(v?: any): boolean + + /** + * 判定一个数组是否为另一个数组的前缀,用于录像接续播放。请注意函数名没有大写字母 + * @example core.subarray(['ad', '米库', '小精灵', '小破草', '小艾'], ['ad', '米库', '小精灵']); // ['小破草', '小艾'] + * @param a 可能的母数组,不填或比b短将返回null + * @param b 可能的前缀,不填或比a长将返回null + * @returns 如果b不是a的前缀将返回null,否则将返回a去掉此前缀后的剩余数组 + */ + subarray(a?: any[], b?: any[]): any[] | null + + /** + * 判定array是不是一个数组,以及element是否在该数组中。 + * @param array 可能的数组,不为数组或不填将导致返回值为false + * @param element 待查找的元素 + * @returns 如果array为数组且具有element这项,就返回true,否则返回false + */ + inArray(array?: any, element?: any): boolean + + /** + * 将x限定在[a,b]区间内,注意a和b可交换 + * @example core.clamp(1200, 1, 1000); // 1000 + * @param x 原始值,!x为true时x一律视为0 + * @param a 下限值,大于b将导致与b交换 + * @param b 上限值,小于a将导致与a交换 + */ + clamp(x: number, a: number, b: number): number + + /** + * 填写非自绘状态栏 + * @example core.setStatusBarInnerHTML('hp', core.status.hero.hp, 'color: #66CCFF'); // 更新状态栏中的主角生命,使用加载画面的宣传色 + * @param name 状态栏项的名称,如'hp', 'atk', 'def'等。必须是core.statusBar中的一个合法项 + * @param value 要填写的内容,大数字会被格式化为至多6个字符,无中文的内容会被自动设为斜体 + * @param css 额外的css样式,可选。如更改颜色等 + */ + setStatusBarInnerHTML(name: string, value: any, css?: string): void + + /** + * 求字符串的国标码字节数,也可用于等宽字体下文本的宽度测算。请注意样板的默认字体Verdana不是等宽字体 + * @example core.strlen('无敌ad'); // 6 + * @param str 待测字符串 + * @returns 字符串的国标码字节数,每个汉字为2,每个ASCII字符为1 + */ + strlen(str: string): number + + /** + * 通配符匹配,用于搜索图块等批量处理。 + * @example core.playSound(core.matchWildcard('*Key', itemId) ? 'item.mp3' : 'door.mp3'); // 判断捡到的是钥匙还是别的道具,从而播放不同的音效 + * @param pattern 模式串,每个星号表示任意多个(0个起)字符 + * @param string 待测串 + * @returns true表示匹配成功,false表示匹配失败 + */ + matchWildcard(pattern: string, string: string): boolean + + /** + * base64加密 + * @example core.encodeBase64('If you found this note in a small wooden box with a heart on it'); // "SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0" + * @param str 明文 + * @returns 密文 + */ + encodeBase64(str: string): string + + /** + * base64解密 + * @example core.decodeBase64('SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0'); // "If you found this note in a small wooden box with a heart on it" + * @param str 密文 + * @returns 明文 + */ + decodeBase64(str: string): string + + /** + * 不支持SL的随机数 + * @exmaple 1 + core.rand(6); // 随机生成一个小于7的正整数,模拟骰子的效果 + * @param num 填正数表示生成小于num的随机自然数,否则生成小于1的随机正数 + * @returns 随机数,即使读档也不会改变结果 + */ + rand(num?: number): number + + /** + * 支持SL的随机数,并计入录像 + * @exmaple 1 + core.rand2(6); // 随机生成一个小于7的正整数,模拟骰子的效果 + * @param num 正整数,0或不填会被视为2147483648 + * @returns 属于 [0, num) 的随机数 + */ + rand2(num?: number): number + + /** + * 弹窗请求下载一个文本文件 + * @example core.download('route.txt', core.status.route); // 弹窗请求下载录像 + * @param filename 文件名 + * @param content 文件内容 + */ + download(filename: string, content: string | String[]): void + + /** + * 显示确认框,类似core.drawConfirmBox() + * @example core.myconfirm('重启游戏?', core.restart); // 弹窗询问玩家是否重启游戏 + * @param hint 弹窗的内容 + * @param yesCallback 确定后的回调函数 + * @param noCallback 取消后的回调函数,可选 + */ + myconfirm(hint: string, yesCallback: () => void, noCallback?: () => void): void + + /** + * 判定深层相等, 会逐层比较每个元素 + * @example core.same(['1', 2], ['1', 2]); // true + */ + same(a?: any, b?: any): boolean + + /** + * 尝试请求读取一个本地文件内容 [异步] + * @param success 成功后的回调 + * @param error 失败后的回调 + * @param readType 不设置则以文本读取,否则以DataUrl形式读取 + */ + readFile(success, error, readType): void + + /** + * 文件读取完毕后的内容处理 [异步] + * @param content + */ + readFileContent(content): void + + /** + * 尝试复制一段文本到剪切板。 + */ + copy(data: string): void + + /** + * 发送一个HTTP请求 [异步] + * @param type 请求类型 + * @param url 目标地址 + * @param formData 如果是POST请求则为表单数据 + * @param success 成功后的回调 + * @param error 失败后的回调 + * @param timeout 超时时间 + */ + http(type: 'GET' | 'POST', url: string, formData: FormData, success: () => void, error: () => void, + mimeType?: string | null, responseType?: XMLHttpRequestResponseType | null, + onprogress?: boolean | null, timeout?: number | null): void + + /** 获得浏览器唯一的guid */ + getGuid(): string + + /** 解压缩一个数据 */ + decompress(value: any): any + + /** 设置本地存储 */ + setLocalStorage(key: string, value?: any): void + + /** 获得本地存储 */ + getLocalStorage(key: string, defaultValue?: any): any + + /** 移除本地存储 */ + removeLocalStorage(key: string): void + + /** 往数据库写入一段数据 */ + setLocalForage(key: string, value?: any, successCallback?: () => void, errorCallback?: () => void): void + + /** 从数据库读出一段数据 */ + getLocalForage(key: string, defaultValue?: any, successCallback?: (data: any) => void, errorCallback?: () => void): void + + /** 移除数据库的数据 */ + removeLocalForage(key: string, successCallback?: () => void, errorCallback?: () => void): void + + /** 格式化日期为字符串 */ + formatDate(date: Date): string + + /** 格式化日期为最简字符串 */ + formatDate2(date: Date): string + + /** 格式化时间 */ + formatTime(time: number): string + + /** 两位数显示 */ + setTwoDigits(x: number): string + + /** 格式化文件大小 */ + formatSize(size: number): string + + /** 访问浏览器cookie */ + getCookie(name: string): string + + /** + * 计算应当转向某个方向 + * @param turn 转向的方向 + * @param direction 当前方向 + */ + turnDirection(turn: 'up' | 'down' | 'left' | 'right' | ':left' | ':right' | ':back', direction?: string): string + + /** 是否满足正则表达式 */ + matchRegex(pattern: string, string: string): string + + /** 让用户输入一段文字 + * @param hint 输入框的提示语 + * @param value 输入值为null时的默认值 + * @param callback 确认时的回调 + * @param failCallback 取消时的回调,不填则默认与确认时的回调相同 + */ + myprompt(hint: string, value?: string | null, callback?: (data: string) => any, failCallback?: (data: string) => any): void + + /** 动画显示某对象 */ + showWithAnimate(obj?: any, speed?: number, callback?: () => any): void + + /** 动画使某对象消失 */ + hideWithAnimate(obj?: any, speed?: number, callback?: () => any): void + + /** 解压一段内容 */ + unzip(blobOrUrl?: any, success?: (data: any) => void, error?: (error: string) => void, convertToText?: boolean, onprogress?: (loaded: number, total: number) => void): void +} + +/** 和图标相关的函数 */ +interface icons { + + /** 获得所有图标类型 */ + getIcons(): void + + /** 根据ID获得其类型 */ + getClsFromId(id?: string): string + + /** 获得所有图标的ID */ + getAllIconIds(): void + + /** 根据图块数字或ID获得所在的tileset和坐标信息 */ + getTilesetOffset(id?: string): void +} + +interface plugin { + // aniMap: Map + + /** 高级动画相关的类 */ + animate: { + Animation?: any, AnimationBase?: any, Ticker?: any, Transition?: any, bezier?: any, bezierPath?: any, circle?: any, + hyper?: any, inverseTrigo?: any, linear?: any, power?: any, shake?: any, sleep?: any, trigo?: any, + } + /** 该集合中的所有Ticker在跨层时需要被摧毁 */ + tickerSet: Set + + /** 打开一个道具商店 */ + openItemShop: (itemShopId: string) => void + /** 某个全局商店是否被访问过 */ + isShopVisited(id: string): boolean + + /** 砍层插件删除fromId到toId间的楼层 + * @example core.removeMaps("MT1", "MT300") 删除MT1~MT300之间的全部层 + * @example core.removeMaps("MT10") 只删除MT10层 + */ + removeMaps(fromId: string, toId?: string): void + + /** 砍层插件恢复fromId到toId间的楼层 + * @example core.resumeMaps("MT1", "MT300") 恢复MT1~MT300之间的全部层 + * @example core.resumeMaps("MT10") 只恢复MT10层 + */ + resumeMaps(fromId: string, toId?: string): void + + /** 多角色插件,初始化各角色属性 */ + initHeros(): void + /** 多角色插件,切换到另一角色 */ + changeHero(toHeroId?: number): void + + uiBase: { + KeyCodeEnum: { + BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, SpaceBar: 32, + PageUp: 33, PageDown: 34, Left: 37, Up: 38, Right: 39, + Down: 40, C: 67, Q: 81, T: 84, + } + ButtonBase: ButtonBase + RoundBtn: RoundBtn + IconBtn: IconBtn + ExitBtn: ExitBtn + MenuBase: MenuBase + Pagination: Pagination + } + + /** 设置菜单 */ + settingMenu: MenuBaseClass | undefined + + // core.plugin.xxx 可能是任意变量,任意签名和返回值的函数 + [x: string]: any +} + +/*** 用于绘制的基类 */ +/*** 按钮基类 */ +interface ButtonBase { + new(x: number, y: number, w: number, h: number): ButtonBaseClass; +} + +type posFunc = (x: number, y: number, px: number, py: number) => void; + +declare class ButtonBaseClass { + constructor(x: number, y: number, w: number, h: number); + /** 按钮判定区域左上角的X坐标 */ + x: number; + /** 按钮判定区域左上角的Y坐标 */ + y: number; + /** 按钮判定区域的宽度 */ + w: number; + /** 按钮判定区域的高度 */ + h: number; + /** 按钮是否失效。失效时将跳过绘制和监听。 */ + disable: boolean; + /** 按钮是否处于被选中的状态 */ + status: 'selected' | 'none'; + /** 按钮所在的菜单 */ + menu: MenuBaseClass + /** 按钮所在的画布 */ + ctx: string + /** 按钮在菜单中的索引 */ + key: string | number + /** + * 按钮的绘制方法 + * @virtual + **/ + draw(): void + /** 按钮被按下时触发的事件 */ + ondown: posFunc; + /** 鼠标悬停时触发的事件 */ + onmove: posFunc | undefined; + /** 按钮抬起时触发的事件 */ + onup: posFunc | undefined; + /** 当前点击坐标是否在该按钮的判定区范围内 */ + inRange(px, py): boolean; +} + +interface RoundBtn { + new(x: number, y: number, w: number, h: number, text: string, config?: any): RoundBtnClass; +} + +declare class RoundBtnClass extends ButtonBaseClass { + constructor(x: number, y: number, w: number, h: number, text: string, config?: any); + /** 按钮绘制的文字 */ + text: string + /** 按钮的绘制属性配置 */ + config: { + fillStyle?: string, strokeStyle?: string, fontStyle?: string, + selectedFillStyle?: string, selectedstrokeStyle?: string, selectedFontStyle?: string, + radius?: number, lineWidth?: number, angle?: number, font?: string + } +} + +interface IconBtn { + new(x: number, y: number, w: number, h: number, icon: string, config?: any): IconBtnClass; +} + +declare class IconBtnClass extends ButtonBaseClass { + constructor(x: number, y: number, w: number, h: number, icon: string, config?: any); + /** 按钮绘制的图标名称 */ + icon: string + /** 按钮的绘制属性配置 */ + config: { + fillStyle?: string, strokeStyle?: string, + radius?: number, lineWidth?: number, angle?: number, font?: number + } +} + +interface ExitBtn { + new(x: number, y: number, w: number, h: number, config?: any): ExitBtnClass; +} + +declare class ExitBtnClass extends ButtonBaseClass { + constructor(x: number, y: number, w: number, h: number, config?: any); + /** 按钮的绘制属性配置 */ + config: { + fillStyle?: string, strokeStyle?: string, lineStyle?: string + radius?: number, lineOffsetX?: number, lineWidthX?: number, + } +} + +interface MenuBase { + new(name: string, toListen: eventType[], + x?: number | null, y?: number | null, w?: number | null, h?: number | null, + zIndex?: number | null): MenuBaseClass; +} + +type eventType = 'ondown' | 'onmove' | 'onup' | 'keyDown' | 'keyUp' | 'onmousewheel'; + +/*** 菜单基类 */ +declare class MenuBaseClass { + /** + * @param name 该菜单使用的画布的名称 + */ + constructor(name: string, toListen: eventType[], + x?: number | null, y?: number | null, w?: number | null, h?: number | null, + zIndex?: number | null); + /** 该菜单画布左上角的x坐标 */ + x: number + /** 该菜单画布左上角的y坐标 */ + y: number + /** 该菜单画布的宽度 */ + w: number + /** 该菜单画布的高度 */ + h: number + /** 该菜单画布的z值 */ + zIndex: number + /** 该菜单使用的画布的名称 */ + name: string + /** 该画布是否正在被绘制 */ + onDraw: boolean + /** 该菜单的按钮列表 */ + btnMap: Map + + ondown(x: number, y: number, rawpx: number, rawpy: number): void + /** + * @virtual + * 点击菜单自身触发的事件 */ + ondownEvent(x: number, y: number, px: number, py: number): void + /** + * @virtual + * 点击菜单上的按钮触发的事件 */ + ondownBtnEvent(x: number, y: number, px: number, py: number): void + onmove(x: number, y: number, rawpx: number, rawpy: number): void + /** + * @virtual + * 鼠标在菜单自身上移动触发的事件 */ + onmoveEvent(x: number, y: number, px: number, py: number): void + /** + * @virtual + * 鼠标移动到按钮上触发的事件 */ + onmoveBtnEvent(x: number, y: number, px: number, py: number): void + onup(x: number, y: number, rawpx: number, rawpy: number): void + /** + * @virtual + * 当菜单自身被鼠标或手指放开时触发的事件 */ + onupEvent(x: number, y: number, px: number, py: number): void + /** + * @virtual + * 当菜单上的按钮被鼠标或手指放开时触发的事件 */ + onupBtnEvent(x: number, y: number, px: number, py: number): void + + /** 按键被按下时触发的事件 */ + keyDownEvent(keycode: number): void + /** 按键被放开时触发的事件 */ + keyUpEvent(keycode: number, altkey?: boolean, fromReplay?: boolean): void; + /** 鼠标滚轮滚动时触发的事件 */ + onMouseWheelEvent(direct: 1 | -1): void; + + /** 返回换算后的画布上的相对坐标 */ + convertCoordinate(px: number, py: number): [number, number] + /** 检查坐标是否在画布范围内 */ + isPosValid(px: number, py: number): boolean + + + /** 注册一个按钮的事件 */ + registerBtn(key: string | number, button: ButtonBaseClass, + event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }): void + /** 注册若干个按钮的事件 */ + registerBtns(btns: [key: string | number, button: ButtonBaseClass, + event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }][]): void + + /** 创建并返回本菜单的画布 */ + createCanvas(): CanvasRenderingContext2D + /** 绘制该菜单上的按钮 */ + drawButtonContent(): void + /** 绘制该菜单上的按钮(需要派生类自行绘制画布和填充内容) */ + drawContent(): void + /** 开始监听该菜单的各个事件 */ + beginListen(): void + /** 取消监听该菜单的各个事件 */ + endListen(): void + /** 擦除该菜单的画布 */ + remove(): void + /** 取消监听该菜单的各个事件, 并清除该菜单的画布 */ + clear(): void + /** 开始监听和绘制 */ + init(): void +} + +interface Pagination { + new(pageList: MenuBase[], currPage: number, name: string, toListen: eventType[], + x?: number, y?: number, w?: number, h?: number, zIndex?: number): PaginationClass +} + +declare class PaginationClass extends MenuBaseClass { + constructor(pageList: MenuBase[], currPage: number, name: string, toListen: eventType[], + x?: number, y?: number, w?: number, h?: number, zIndex?: number); + /** 页面列表 */ + pageList: MenuBase[] + /** 当前页码 */ + currPage: number + + initOnePage(index: number): void + /** 切换到指定的页码 */ + changePage(index: number): void + /** 向下翻页 */ + pageDown(): void + /** 向上翻页 */ + pageUp(): void + /** 清除当前页和分页本身 */ + clear(): void +} + +type CoreMixin = { + firstData: { [x: string]: any } + /** 全塔属性开关 */flags: { + statusBarItems: string[] + [flagName: string]: boolean | number + } + /** 全局数值 */values: + { + /** 全局帧动画时间 */animateSpeed: number; + floorChangeTime: number; + /** 勇士移速 */moveSpeed: number; + statusCanvasRowsOnMobile: number; + + redGem: number; + blueGem: number; + greenGem: number; + redPotion: number; + bluePotion: number; + yellowPotion: number; + greenPotion: number; + + breakArmor: number; + counterAttack: number; + hatred: number; + lavaDamage: number; + poisonDamage: number; + purify: number; + weakValue: number; + } + + /** 地图可视部分大小 */ + readonly __SIZE__: number; + /** 地图像素 */ + readonly __PIXELS__: number; + /** 地图像素的一半 */ + readonly __HALF_SIZE__: number; + + /** 地图长度,仅2.10有效 */ + readonly _PX_: number; + /** 地图宽度,仅2.10有效 */ + readonly _PY_: number; + + /** 游戏素材 */ + readonly material: { + readonly animates: { [key: string]: Animate }, + readonly images: {}, + readonly bgms: { [key: string]: HTMLAudioElement }, + readonly sounds: { [key: string]: HTMLAudioElement }, + readonly ground: CanvasRenderingContext2D + /** + * 怪物信息 + * @example core.material.enemys.greenSlime // 获得绿色史莱姆的属性数据 + */ + readonly enemys: { [key: string]: Enemy }, + /** 道具信息 */ + readonly items: { [key: string]: Item } + readonly icons: { + animates: { [blockName: string]: number }; + autotile: { [blockName: string]: number }; + enemy48: { [blockName: string]: number }; + enemys: { [blockName: string]: number }; + hero: { + [dir: string]: { + loc: number; + stop: number; + leftFoot: number; + rightFoot: number; + /** 插件"勇士4帧动画"定义的中间帧动画 */ + midFoot: number; + } + } & { height: number }, + items: { [blockName: string]: number }; + npc48: { [blockName: string]: number }; + npcs: { [blockName: string]: number }; + terrains: { [blockName: string]: number }; + } + } + readonly timeout: { + turnHeroTimeout: any, + onDownTimeout: any, + sleepTimeout: any, + } + readonly interval: { + heroMoveInterval: any, + onDownInterval: any, + } + readonly animateFrame: { + totalTime: number + totalTimeStart: number + globalAnimate: boolean, + globalTime: number + selectorTime: number + selectorUp: boolean, + animateTime: number + moveTime: number + lastLegTime: number + /** 插件"勇士4帧动画"开启时为number */ + leftLeg: number, + readonly weather: { + time: number + type: any + nodes: [], + data: any + fog: any, + cloud: any, + }, + readonly tips: { + time: number + offset: number + list: [], + lastSize: number + }, + readonly asyncId: {} + } + readonly musicStatus: { + audioContext: AudioContext, + /** 是否播放BGM */bgmStatus: boolean + /** 是否播放SE */soundStatus: boolean + /** 正在播放的BGM */playingBgm: string + /** 上次播放的bgm */lastBgm: string + gainNode: GainNode, + /** 正在播放的SE */playingSounds: {} + /** 音量 */volume: number + /** 缓存BGM内容 */cachedBgms: string[] + /** 缓存的bgm数量 */cachedBgmCount: number + /** 用户调节的音量 */userVolume: number + } + readonly platform: { + /** 是否http */isOnline: boolean + /** 是否是PC */isPC: boolean + /** 是否是Android */isAndroid: boolean + /** 是否是iOS */isIOS: boolean + string: string + /** 是否是微信 */isWeChat: boolean + /** 是否是QQ */isQQ: boolean + /** 是否是Chrome */isChrome: boolean + /** 是否支持复制到剪切板 */supportCopy: boolean + + fileInput: null + /** 是否支持FileReader */fileReader: null + /** 读取成功 */successCallback: null + /** 读取失败 */errorCallback: null + } + readonly dom: { [key: string]: HTMLElement } + /** dom样式 */ + readonly domStyle: { + ratio: number, + scale: number, + isVertical: boolean, + showStatusBar: boolean, + toolbarBtn: 'normal' | 'num' | 'replay' | 'hide', + toolsCount: number, + } + readonly bigmap: { + canvas: string[], + offsetX: number // in pixel + offsetY: number + posX: number + posY: number + width: number // map width and height + height: number + v2: boolean + threshold: number + extend: number + scale: number + tempCanvas: CanvasRenderingContext2D // A temp canvas for drawing + cacheCanvas: CanvasRenderingContext2D + } + readonly saves: { + saveIndex: number + /** 每次存读档后进行了几步操作,用于同步存档前的检查 */ + actionCount: number + readonly ids: { [key: number]: boolean } + autosave: { + data: Save[] + max: number + storage: true + time: number + now: number + updated: boolean + } + favorite: [] + readonly favoriteName: {} + } + readonly initStatus: gameStatus + readonly canvas: { [key: string]: CanvasRenderingContext2D } + readonly dymCanvas: { [key: string]: CanvasRenderingContext2D } + /** 游戏状态 */ + readonly status: gameStatus + + /** + * 获得所有楼层的信息 + * @example core.floors[core.status.floorId].events // 获得本楼层的所有自定义事件 + */ + readonly floors: { [key: string]: ResolvedMap } + readonly floorIds: string[] + /** 砍层插件使用的分区信息 */ + readonly floorPartitions: [string, string][] + + readonly control: control + readonly loader: loader + readonly events: events + readonly enemys: enemys + readonly items: items + readonly maps: maps + readonly ui: ui + readonly utils: utils + readonly icons: icons + readonly actions: actions + readonly plugin: plugin + readonly statusBar: Main['statusBar'] +} & control & events & loader & enemys & items & maps & ui & utils & icons & actions & plugin + +interface Main { + /** 是否在进行在线录像验证 */ + replayChecking: boolean + readonly core: CoreMixin + readonly dom: { [key: string]: HTMLElement } + /** 游戏版本,发布后会被随机,请勿使用该属性 */ + readonly version: string + readonly useCompress: boolean + readonly savePages: number + readonly mode: 'play' | 'editor' + readonly statusBar: { + images: { [x: string]: HTMLElement } + icons: { [x: string]: string | HTMLImageElement } + [x: string]: HTMLElement | object + } + readonly __VERSION__: string + readonly __VERSION_CODE__: number + + /** 输出内容(极不好用,建议换成console,我甚至不知道样板为什么会有这个东西)*/ + log(e: string | Error, error: boolean): void +} +declare let main: Main +declare let core: CoreMixin + +declare let flags: { [x: string]: any } +declare let hero: CoreMixin['status']['hero'] +declare let editor: editor + +declare let control: () => control +declare let events: () => events +declare let loader: () => loader +declare let enemys: () => enemys +declare let items: () => items +declare let maps: () => maps +declare let ui: () => ui +declare let utils: () => utils +declare let icons: () => icons +declare let actions: () => actions +