diff --git a/libs/control.js b/libs/control.js index 164fb752..bfa94c2f 100644 --- a/libs/control.js +++ b/libs/control.js @@ -459,9 +459,6 @@ control.prototype._initStatistics = function (totalTime) { 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 = {}; } diff --git a/project/functions.js b/project/functions.js index de7a2930..d7c8b5fe 100644 --- a/project/functions.js +++ b/project/functions.js @@ -1,1885 +1,1903 @@ -/// -var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = -{ - "events": { - "resetGame": function (hero, hard, floorId, maps, values) { - // 重置整个游戏;此函数将在游戏开始时,或者每次读档时最先被调用 - // hero:勇士信息;hard:难度;floorId:当前楼层ID;maps:地图信息;values:全局数值信息 - - // 清除游戏数据 - // 这一步会清空状态栏和全部画布内容,并删除所有动态创建的画布 - core.clearStatus(); - // 初始化status - core.status = core.clone(core.initStatus, function (name) { - return name != 'hero' && name != 'maps'; - }); - core.control._bindRoutePush(); - core.status.played = true; - // 初始化人物,图标,统计信息 - core.status.hero = core.clone(hero); - window.hero = core.status.hero; - window.flags = core.status.hero.flags; - core.events.setHeroIcon(core.status.hero.image, true); - core.control._initStatistics(core.animateFrame.totalTime); - core.status.hero.statistics.totalTime = core.animateFrame.totalTime = - Math.max(core.status.hero.statistics.totalTime, core.animateFrame.totalTime); - core.status.hero.statistics.start = null; - // 初始难度 - core.status.hard = hard || ""; - // 初始化地图 - core.status.floorId = floorId; - core.status.maps = maps; - core.maps._resetFloorImages(); - // 初始化怪物,道具,和地图图块信息 - core.material.enemys = core.enemys.getEnemys(); - core.material.items = core.items.getItems(); - core.maps.blocksInfo = core.maps.getBlocksInfo(); - // 初始化全局数值和全局开关 - core.values = core.clone(core.data.values); - for (var key in values || {}) - core.values[key] = values[key]; - core.flags = core.clone(core.data.flags); - var globalFlags = core.getFlag("globalFlags", {}); - for (var key in globalFlags) - core.flags[key] = globalFlags[key]; - core._init_sys_flags(); - // 初始化界面,状态栏等 - core.resize(); - // 状态栏是否显示 - if (core.hasFlag('hideStatusBar')) - core.hideStatusBar(core.hasFlag('showToolbox')); - else - core.showStatusBar(); - // 隐藏右下角的音乐按钮 - core.dom.musicBtn.style.display = 'none'; - - /** 以下为2.92样板新增内容的处理 */ - // 同步剧情跳过设置 - core.plugin.checkSkipFuncs(); - // 清空自绘道具栏的缓存 - core.plugin.clearItemBoxCache(); - // 设置一次工具栏,统计出元素数量 - // 注意:录像回退rewindReplay时会调用此函数,此时工具栏应该显示录像图标,所以这里要判定 - core.setToolbarButton(core.domStyle.toolbarBtn === 'replay' ? 'replay' : 'normal'); - }, - "win": function (reason, norank, noexit) { - // 游戏获胜事件 - // 请注意,成绩统计时是按照hp进行上传并排名 - // 因为bug,请不要试图在此处修改生命来修改分数,而是在提交前使用事件块修改生命 - - // 如果不退出,则临时存储数据 - if (noexit) { - core.status.extraEvent = core.clone(core.status.event); - } - - // 游戏获胜事件 - core.ui.closePanel(); - var replaying = core.isReplaying(); - if (replaying) core.stopReplay(); - core.waitHeroToStop(function () { - if (!noexit) { - core.clearMap('all'); // 清空全地图 - core.deleteAllCanvas(); // 删除所有创建的画布 - core.dom.gif2.innerHTML = ""; - } - reason = core.replaceText(reason); - core.drawText([ - "\t[" + (reason || "恭喜通关") + "]你的分数是${status:hp}。" - ], function () { - core.events.gameOver(reason || '', replaying, norank); - }) - }); - }, - "lose": function (reason) { - // 游戏失败事件 - core.ui.closePanel(); - var replaying = core.isReplaying(); - core.stopReplay(); - core.waitHeroToStop(function () { - core.drawText([ - "\t[" + (reason || "结局1") + "]你死了。\n如题。" - ], function () { - core.events.gameOver(null, replaying); - }); - }) - }, - "changingFloor": function (floorId, heroLoc) { - // 正在切换楼层过程中执行的操作;此函数的执行时间是“屏幕完全变黑“的那一刻 - // floorId为要切换到的楼层ID;heroLoc表示勇士切换到的位置 - - // ---------- 此时还没有进行切换,当前floorId还是原来的 ---------- // - var currentId = core.status.floorId || null; // 获得当前的floorId,可能为null - var fromLoad = core.hasFlag('__fromLoad__'); // 是否是读档造成的切换 - var isFlying = core.hasFlag('__isFlying__'); // 是否是楼传造成的切换 - if (!fromLoad && !(isFlying && currentId == floorId)) { - if (!core.hasFlag("__leaveLoc__")) core.setFlag("__leaveLoc__", {}); - if (currentId != null) core.getFlag("__leaveLoc__")[currentId] = core.clone(core.status.hero.loc); - } - - // 可以对currentId进行判定,比如删除某些自定义图层等 - // if (currentId == 'MT0') { - // core.deleteAllCanvas(); - // } - - // 根据分区信息自动砍层与恢复 - if (core.autoRemoveMaps) core.autoRemoveMaps(floorId); - - // 重置画布尺寸 - core.maps.resizeMap(floorId); - // 设置勇士的位置 - heroLoc.direction = core.turnDirection(heroLoc.direction); - core.status.hero.loc = heroLoc; - // 检查重生怪并重置 - if (!fromLoad) { - core.extractBlocks(floorId); - core.status.maps[floorId].blocks.forEach(function (block) { - if (block.disable && core.enemys.hasSpecial(block.event.id, 23)) { - block.disable = false; - core.setMapBlockDisabled(floorId, block.x, block.y, false); - core.maps._updateMapArray(floorId, block.x, block.y); - } - }); - core.control.gatherFollowers(); - } - - // ---------- 重绘新地图;这一步将会设置core.status.floorId ---------- // - core.drawMap(floorId); - - // 切换楼层BGM - if (core.status.maps[floorId].bgm) { - var bgm = core.status.maps[floorId].bgm; - if (bgm instanceof Array) bgm = bgm[Math.floor(Math.random() * bgm.length)]; // 多个bgm则随机播放一个 - if (!core.hasFlag("__bgm__")) core.playBgm(bgm); - } else if (fromLoad && !core.hasFlag("__bgm__")) { - core.pauseBgm(); - } - // 更改画面色调 - var color = core.getFlag('__color__', null); - if (!color && core.status.maps[floorId].color) - color = core.status.maps[floorId].color; - core.clearMap('curtain'); - core.status.curtainColor = color; - if (color) core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(color)); - // 更改天气 - var weather = core.getFlag('__weather__', null); - if (!weather && core.status.maps[floorId].weather) - weather = core.status.maps[floorId].weather; - if (weather) - core.setWeather(weather[0], weather[1]); - else core.setWeather(); - - core.plugin.deleteAllAnis(); // 摧毁所有现存的高级动画 - core.plugin.clearAttractAnimate(); // 摧毁拾取物品动画 - - // ...可以新增一些其他内容,比如创建个画布在右上角显示什么内容等等 - - }, - "afterChangeFloor": function (floorId) { - // 转换楼层结束的事件;此函数会在整个楼层切换完全结束后再执行 - // floorId是切换到的楼层 - - // 如果是读档,则进行检查(是否需要恢复事件) - if (core.hasFlag('__fromLoad__')) { - core.events.recoverEvents(core.getFlag("__events__")); - core.removeFlag("__events__"); - } else { - // 每次抵达楼层执行的事件 - core.insertAction(core.floors[floorId].eachArrive); - - // 首次抵达楼层时执行的事件(后插入,先执行) - if (!core.hasVisitedFloor(floorId)) { - core.insertAction(core.floors[floorId].firstArrive); - core.visitFloor(floorId); - } - } - if (!core.isReplaying()) core.plugin.drawCommentSign(); - }, - "flyTo": function (toId, callback) { - // 楼层传送器的使用,从当前楼层飞往toId - // 如果不能飞行请返回false - - var fromId = core.status.floorId; - - // 检查能否飞行 - if (!core.status.maps[fromId].canFlyFrom || !core.status.maps[toId].canFlyTo || !core.hasVisitedFloor(toId)) { - core.playSound('操作失败'); - core.drawTip("无法飞往" + core.status.maps[toId].title + "!", 'fly'); - return false; - } - - // 平面塔模式 - var stair = null, - loc = null; - if (core.flags.flyRecordPosition) { - loc = core.getFlag("__leaveLoc__", {})[toId] || null; - } - if (core.status.maps[toId].flyPoint != null && core.status.maps[toId].flyPoint.length == 2) { - stair = 'flyPoint'; - } - if (stair == null && loc == null) { - // 获得两个楼层的索引,以决定是上楼梯还是下楼梯 - var fromIndex = core.floorIds.indexOf(fromId), - toIndex = core.floorIds.indexOf(toId); - var stair = fromIndex <= toIndex ? "downFloor" : "upFloor"; - // 地下层:同层传送至上楼梯 - if (fromIndex == toIndex && core.status.maps[fromId].underGround) stair = "upFloor"; - } - - // 记录录像 - core.status.route.push("fly:" + toId); - // 传送 - core.ui.closePanel(); - core.setFlag('__isFlying__', true); - core.changeFloor(toId, stair, loc, null, function () { - core.removeFlag("__isFlying__"); - if (callback) callback(); - }); - - return true; - }, - "beforeBattle": function (enemyId, x, y) { - // 战斗前触发的事件,可以加上一些战前特效(详见下面支援的例子) - // 此函数在“检测能否战斗和自动存档”【之后】执行。如果需要更早的战前事件,请在插件中覆重写 core.events.doSystemEvent 函数。 - // 返回true则将继续战斗,返回false将不再战斗。 - - // ------ 支援技能 ------ // - if (x != null && y != null) { - var index = x + "," + y, - cache = core.status.checkBlock.cache[index] || {}, - guards = cache.guards || []; - // 如果存在支援怪 - if (guards.length > 0) { - // 记录flag,当前要参与支援的怪物 - core.setFlag("__guards__" + x + "_" + y, guards); - var actions = [{ "type": "playSound", "name": "跳跃" }]; - // 增加支援的特效动画(图块跳跃) - guards.forEach(function (g) { - core.push(actions, { "type": "jump", "from": [g[0], g[1]], "to": [x, y], "time": 300, "keep": false, "async": true }); - }); - core.push(actions, [ - { "type": "waitAsync" }, // 等待所有异步事件执行完毕 - { "type": "setBlock", "number": enemyId, "loc": [[x, y]] }, // 重新设置怪物自身 - { "type": "battle", "loc": [x, y] } // 重要!重新触发本次战斗 - ]); - core.insertAction(actions); - return false; - } - } - - return true; - }, - "afterBattle": function (enemyId, x, y) { - // 战斗结束后触发的事件 - - const floorId = core.status.floorId; - var enemy = core.getEnemyValue(enemyId, null, x, y); - var special = enemy.special; - - // 播放战斗音效和动画 - // 默认播放的动画;你也可以使用 - var animate = 'hand'; // 默认动画 - // 检查当前装备是否存在攻击动画 - var equipId = core.getEquip(0); - if (equipId && (core.material.items[equipId].equip || {}).animate) - animate = core.material.items[equipId].equip.animate; - // 你也可以在这里根据自己的需要,比如enemyId或special或flag来修改播放的动画效果 - // if (enemyId == '...') animate = '...'; - - // 检查该动画是否存在SE,如果不存在则使用默认音效 - if (!(core.material.animates[animate] || {}).se) - core.playSound('attack.mp3'); - - // 播放动画;如果不存在坐标(强制战斗)则播放到勇士自身 - if (x != null && y != null) - core.drawAnimate(animate, x, y); - else - core.drawHeroAnimate(animate); - - // 获得战斗伤害信息 - var damageInfo = core.getDamageInfo(enemyId, null, x, y) || {}; - // 战斗伤害 - var damage = damageInfo.damage; - // 当前战斗回合数,可用于战后所需的判定 - var turn = damageInfo.turn; - // 判定是否致死 - if (damage == null || damage >= core.status.hero.hp) { - core.status.hero.hp = 0; - core.updateStatusBar(false, true); - core.events.lose('战斗失败'); - return; - } - - // 记录统计数据 - const statistics = core.status.hero.statistics; - statistics.battleDamage += damage; - core.control.addFloorStatistics("battleDamage", damage); - statistics.battle++; - // 1防收益 - const defDamage = core.enemys.getDefDamage(enemy, -1, x, y, floorId); - if (typeof defDamage === "number") statistics.oneDefEffect -= defDamage; - - // 1护盾收益 - const mdefDamage = core.enemys.getMdefDamage(enemy, -1, x, y, floorId); - if (typeof mdefDamage === "number") statistics.oneMdefEffect -= mdefDamage; - - // 吸血额外伤害,必须放在实际扣减角色体力值之前,否则算出来是错的 - const vampireExtraLoss = core.enemys.getVampireExtraLoss(enemy, x, y, floorId); - if (vampireExtraLoss !== null) { - statistics.vampireExtraLoss += vampireExtraLoss; - core.control.addFloorStatistics("vampireExtraLoss", vampireExtraLoss); - } - - // 扣减体力值 - core.status.hero.hp -= damage; - - - // 计算当前怪物的支援怪物 - // guard:一个形如[[1, 1, 'greenSlime'], [[2, 2, 'redSlime']]]的数组 - var guards = []; - if (x != null && y != null) { - guards = core.getFlag("__guards__" + x + "_" + y, []); - core.removeFlag("__guards__" + x + "_" + y); - } - - const { x: hx, y: hy, direction } = core.status.hero.loc; - - /** 败移 */ - let failMove = false; - let failMoveInfo; - if (core.enemys.hasSpecial(special, 29)) { - for (let i = 2; i < core.__SIZE__ - 1; i++) { - const aimx = hx + core.utils.scan[direction].x * i, - aimy = hy + core.utils.scan[direction].y * i; - if (aimx < 0 || aimx > core.__SIZE__ - 1 || aimy < 0 || aimy > core.__SIZE__ - 1) break; - if (['enemys', 'enemy48'].includes(core.getBlockCls(aimx, aimy))) { - failMoveInfo = { aimx, aimy, aimId: core.getBlockId(aimx, aimy) }; - failMove = true; - break; - } - } - } - - // 获得金币 - var money = guards.reduce(function (curr, g) { - return curr + core.getEnemyValue(g[2], "money", g[0], g[1]); - }, core.getEnemyValue(enemy, "money", x, y)); - if (core.hasItem('coin')) money *= 2; // 幸运金币:双倍 - if (core.hasFlag('curse')) money = 0; // 诅咒效果 - if (failMove) money = 0; // 败移效果 - core.status.hero.money += money; - core.status.hero.statistics.money += money; - - // 获得经验 - var exp = guards.reduce(function (curr, g) { - return curr + core.getEnemyValue(g[2], "exp", g[0], g[1]); - }, core.getEnemyValue(enemy, "exp", x, y)); - if (core.hasFlag('curse')) exp = 0; - if (failMove) exp = 0; // 败移效果 - core.status.hero.exp += exp; - core.status.hero.statistics.exp += exp; - - var hint = "打败 " + core.getEnemyValue(enemy, "name", x, y); - if (core.flags.statusBarItems.indexOf('enableMoney') >= 0) - hint += ',' + core.getStatusLabel('money') + '+' + money; // hint += ",金币+" + money; - if (core.flags.statusBarItems.indexOf('enableExp') >= 0) - hint += ',' + core.getStatusLabel('exp') + '+' + exp; // hint += ",经验+" + exp; - if (failMove) hint = '敌人触发败移。'; - core.drawTip(hint, enemy.id); - - // 中毒 - if (core.enemys.hasSpecial(special, 12)) { - core.triggerDebuff('get', 'poison'); - } - // 衰弱 - if (core.enemys.hasSpecial(special, 13)) { - core.triggerDebuff('get', 'weak'); - } - // 诅咒 - if (core.enemys.hasSpecial(special, 14)) { - core.triggerDebuff('get', 'curse'); - } - // 仇恨怪物将仇恨值减半 - if (core.enemys.hasSpecial(special, 17)) { - core.setFlag('hatred', Math.floor(core.getFlag('hatred', 0) / 2)); - } - // 自爆 - if (core.enemys.hasSpecial(special, 19)) { - core.status.hero.statistics.battleDamage += core.status.hero.hp - 1; - core.status.hero.hp = 1; - } - // 退化 - if (core.enemys.hasSpecial(special, 21)) { - core.status.hero.atk -= (enemy.atkValue || 0); - core.status.hero.def -= (enemy.defValue || 0); - if (core.status.hero.atk < 0) core.status.hero.atk = 0; - if (core.status.hero.def < 0) core.status.hero.def = 0; - } - // 增加仇恨值 - core.setFlag('hatred', core.getFlag('hatred', 0) + core.values.hatred); - - // 战后的技能处理,比如扣除魔力值 - - // 事件的处理 - var todo = []; - - // 加点事件 - var point = guards.reduce(function (curr, g) { - return curr + core.getEnemyValue(g[2], "point", g[0], g[1]); - }, core.getEnemyValue(enemy, "point", x, y)) || 0; - if (core.flags.enableAddPoint && point > 0) { - core.push(todo, [{ "type": "insert", "name": "加点事件", "args": [point] }]); - } - - // 战后事件 - if (core.status.floorId != null) { - core.push(todo, core.floors[core.status.floorId].afterBattle[x + "," + y]); - } - core.push(todo, enemy.afterBattle); - - // 在这里增加其他的自定义事件需求 - /* - if (enemyId=='xxx') { - core.push(todo, [ - {"type": "...", ...}, - ]); - } - */ - - if (!failMove) { - // 如果事件不为空,将其插入 - if (todo.length > 0) core.insertAction(todo, x, y); - - // 删除该点设置的怪物信息 - delete ((flags.enemyOnPoint || {})[core.status.floorId] || {})[x + "," + y]; - - // 因为removeBlock和hideBlock都会刷新状态栏,因此将删除部分移动到这里并保证刷新只执行一次,以提升效率 - if (core.getBlock(x, y) != null) { - // 检查是否是重生怪物;如果是则仅隐藏不删除 - if (core.hasSpecial(enemy.special, 23)) { - core.hideBlock(x, y); - } else { - core.removeBlock(x, y); - } - } else { - core.updateStatusBar(); - } - } - else { - const { aimx, aimy, aimId } = failMoveInfo; - if (core.getBlockId(x, y) === enemyId && failMoveInfo && - core.getBlockId(aimx, aimy) === aimId) { - const doFailMove = [ - { "type": "setBlock", "number": enemyId, "loc": [[aimx, aimy]], "time": 50 }, - { "type": "setBlock", "number": aimId, "loc": [[x, y]], "time": 50 }, - { - "type": "function", "function": `function () { core.exchangeEnemyOnPoint(${x},${y},${aimx},${aimy}) }` - }, - ]; - core.insertAction(doFailMove); - } - } - - // 如果已有事件正在处理中 - if (core.status.event.id == null) - core.continueAutomaticRoute(); - else - core.clearContinueAutomaticRoute(); - - }, - "afterOpenDoor": function (doorId, x, y) { - // 开一个门后触发的事件 - - var todo = []; - // 检查该点的开门后事件 - if (core.status.floorId) { - core.push(todo, core.floors[core.status.floorId].afterOpenDoor[x + "," + y]); - } - // 检查批量开门事件 - var door = core.getBlockById(doorId); - if (door && door.event.doorInfo) { - core.push(todo, door.event.doorInfo.afterOpenDoor); - } - - if (todo.length > 0) core.insertAction(todo, x, y); - - if (core.status.event.id == null) - core.continueAutomaticRoute(); - else - core.clearContinueAutomaticRoute(); - }, - "afterGetItem": function (itemId, x, y, isGentleClick) { - // 获得一个道具后触发的事件 - // itemId:获得的道具ID;x和y是该道具所在的坐标 - // isGentleClick:是否是轻按触发的 - - let sound = '获得道具'; - const itemInfo = core.material.items[itemId]; - if (itemInfo.itemEffectEvent && itemInfo.itemEffectEvent.hasOwnProperty('sound')) { - sound = itemInfo.itemEffectEvent.sound; - } - core.playSound(sound); - - var todo = []; - // 检查该点的获得道具后事件。 - if (core.status.floorId == null) return; - var event = core.floors[core.status.floorId].afterGetItem[x + "," + y]; - if (event && (event instanceof Array || !isGentleClick || !event.disableOnGentleClick)) { - core.unshift(todo, event); - } - - if (todo.length > 0) core.insertAction(todo, x, y); - }, - "afterPushBox": function () { - // 推箱子后的事件 - if (core.searchBlock('box').length == 0) { - // 可以通过if语句来进行开门操作 - /* - if (core.status.floorId=='xxx') { // 在某个楼层 - core.insertAction([ // 插入一条事件 - {"type": "openDoor", "loc": [x,y]} // 开门 - ]) - } - */ - } - } - }, - "enemys": { - "getSpecials": function () { - // 获得怪物的特殊属性,每一行定义一个特殊属性。 - // 分为五项,第一项为该特殊属性的数字,第二项为特殊属性的名字,第三项为特殊属性的描述 - // 第四项为该特殊属性的颜色,可以写十六进制 #RRGGBB 或者 [r,g,b,a] 四元数组 - // 第五项为该特殊属性的标记;目前 1 代表是地图类技能(需要进行遍历全图) - // 名字和描述可以直接写字符串,也可以写个function将怪物传进去 - return [ - [1, "先攻", "怪物首先攻击", "#ffcc33"], - [2, "魔攻", "怪物无视角色的防御", "#bbb0ff"], - [3, "坚固", "怪物防御不小于角色攻击-1", "#c0b088"], - [4, "2连击", "怪物每回合攻击2次", "#ffee77"], - [5, "3连击", "怪物每回合攻击3次", "#ffee77"], - [6, function (enemy) { return (enemy.n || '') + "连击"; }, function (enemy) { return "怪物每回合攻击" + (enemy.n || 4) + "次"; }, "#ffee77"], - [7, "破甲", function (enemy) { return "战斗前,怪物附加角色防御的" + Math.floor(100 * (enemy.breakArmor || core.values.breakArmor || 0)) + "%作为伤害"; }, "#88c0ff"], - [8, "反击", function (enemy) { return "战斗时,怪物每回合附加角色攻击的" + Math.floor(100 * (enemy.counterAttack || core.values.counterAttack || 0)) + "%作为伤害,无视角色防御"; }, "#ffaa44"], - [9, "净化", function (enemy) { return "战斗前,怪物附加角色护盾的" + (enemy.purify || core.values.purify) + "倍作为伤害"; }, "#80eed6"], - [10, "模仿", "怪物的攻防和角色攻防相等", "#b0c0dd"], - [11, "吸血", function (enemy) { return "战斗前,怪物首先吸取角色的" + Math.floor(100 * enemy.vampire || 0) + "%生命(约" + Math.floor((enemy.vampire || 0) * core.getStatus('hp')) + "点)作为伤害" + (enemy.add ? ",并把伤害数值加到自身生命上" : ""); }, "#dd4448"], - [12, "中毒", "战斗后,角色陷入中毒状态,每一步损失生命" + core.values.poisonDamage + "点", "#99ee88"], - [13, "衰弱", "战斗后,角色陷入衰弱状态,攻防暂时下降" + (core.values.weakValue >= 1 ? core.values.weakValue + "点" : parseInt(core.values.weakValue * 100) + "%"), "#f0bbcc"], - [14, "诅咒", "战斗后,角色陷入诅咒状态,战斗无法获得金币和经验", "#bbeef0"], - [15, "领域", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "范围内" + (enemy.range || 1) + "格时自动减生命" + (enemy.zone || 0) + "点"; }, "#c677dd"], - [16, "夹击", "经过两只相同的怪物中间,角色生命值变成一半", "#bb99ee"], - [17, "仇恨", "战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得" + (core.values.hatred || 0) + "点仇恨值)", "#b0b666"], - [18, "阻击", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "时自动减生命" + (enemy.repulse || 0) + "点,同时怪物后退一格"; }, "#8888e6"], - [19, "自爆", "战斗后角色的生命值变成1", "#ff6666"], - [20, "无敌", "角色无法打败怪物,除非拥有十字架", "#aaaaaa"], - [21, "退化", function (enemy) { return "战斗后角色永久下降" + (enemy.atkValue || 0) + "点攻击和" + (enemy.defValue || 0) + "点防御"; }], - [22, "固伤", function (enemy) { return "战斗前,怪物对角色造成" + (enemy.fixedDamage || 0) + "点固定伤害,未开启负伤时无视角色护盾。"; }, "#ff9977"], - [23, "重生", "怪物被击败后,角色转换楼层则怪物将再次出现", "#a0e0ff"], - [24, "激光", function (enemy) { return "经过怪物同行或同列时自动减生命" + (enemy.laser || 0) + "点"; }, "#dda0dd"], - [25, "光环", function (enemy) { return (enemy.range != null ? ((enemy.haloSquare ? "该怪物九宫格" : "该怪物十字") + enemy.haloRange + "格范围内") : "同楼层所有") + "怪物生命提升" + (enemy.hpBuff || 0) + "%,攻击提升" + (enemy.atkBuff || 0) + "%,防御提升" + (enemy.defBuff || 0) + "%," + (enemy.haloAdd ? "可叠加" : "不可叠加"); }, "#e6e099", 1], - [26, "支援", "当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。", "#77c0b6", 1], - [27, "捕捉", function (enemy) { return "当走到怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "时会强制进行战斗。"; }, "#c0ddbb"], - [28, "追猎", "角色行走一步后若处在怪物视线内,怪物向角色移动一步。怪物走入角色十字1格以内时主动与角色开战。", "#DC143C"], - [29, "败移", "战后若角色面对的行/列有其它怪物,该怪物不会被击败,而是与其中最近的怪物交换位置。", "#c0ddbb"], - ]; - }, - "getEnemyInfo": function (enemy, hero, x, y, floorId) { - // 获得某个怪物变化后的数据;该函数将被伤害计算和怪物手册使用 - // 例如:坚固、模仿、仿攻等等 - // - // 参数说明: - // enemy:该怪物信息 - // hero_hp,hero_atk,hero_def,hero_mdef:勇士的生命攻防护盾数据 - // x,y:该怪物的坐标(查看手册和强制战斗时为undefined) - // floorId:该怪物所在的楼层 - // 后面三个参数主要是可以在光环等效果上可以适用(也可以按需制作部分范围光环效果) - floorId = floorId || core.status.floorId; - var hero_hp = core.getRealStatusOrDefault(hero, 'hp'), - hero_atk = core.getRealStatusOrDefault(hero, 'atk'), - hero_def = core.getRealStatusOrDefault(hero, 'def'), - hero_mdef = core.getRealStatusOrDefault(hero, 'mdef'); - - const enemyInfo = core.getEnemyValue(enemy, null, x, y, floorId); - var mon_hp = enemyInfo.hp, - mon_atk = enemyInfo.atk, - mon_def = enemyInfo.def, - mon_special = enemyInfo.special; - var mon_money = enemyInfo.money, - mon_exp = enemyInfo.exp, - mon_point = enemyInfo.point; - // 模仿 - if (core.hasSpecial(mon_special, 10)) { - mon_atk = hero_atk; - mon_def = hero_def; - } - // 坚固 - if (core.hasSpecial(mon_special, 3) && mon_def < hero_atk - 1) { - mon_def = hero_atk - 1; - } - - var guards = []; - - // 光环和支援检查 - if (!core.status.checkBlock) core.status.checkBlock = {}; - - if (core.status.checkBlock.needCache) { - // 从V2.5.4开始,对光环效果增加缓存,以解决多次重复计算的问题,从而大幅提升运行效率。 - var hp_buff = 0, - atk_buff = 0, - def_buff = 0; - // 已经计算过的光环怪ID列表,用于判定叠加 - var usedEnemyIds = {}; - // 检查光环和支援的缓存 - var index = x != null && y != null ? (x + "," + y) : floorId; - if (!core.status.checkBlock.cache) core.status.checkBlock.cache = {}; - var cache = core.status.checkBlock.cache[index]; - if (!cache) { - // 没有该点的缓存,则遍历每个图块 - core.extractBlocks(floorId); - core.status.maps[floorId].blocks.forEach(function (block) { - if (!block.disable) { - // 获得该图块的ID - var id = block.event.id, - enemy = core.getEnemyValue(id, null, block.x, block.y, floorId); - // 检查【光环】技能,数字25 - if (enemy && core.hasSpecial(enemy.special, 25)) { - // 检查是否是范围光环 - var inRange = enemy.haloRange == null; - if (enemy.haloRange != null && x != null && y != null) { - var dx = Math.abs(block.x - x), - dy = Math.abs(block.y - y); - // 检查十字和九宫格光环 - if (dx + dy <= enemy.haloRange) inRange = true; - if (enemy.haloSquare && dx <= enemy.haloRange && dy <= enemy.haloRange) inRange = true; - } - // 检查是否可叠加 - if (inRange && (enemy.haloAdd || !usedEnemyIds[enemy.id])) { - hp_buff += enemy.hpBuff || 0; - atk_buff += enemy.atkBuff || 0; - def_buff += enemy.defBuff || 0; - usedEnemyIds[enemy.id] = true; - } - } - // 检查【支援】技能,数字26 - if (enemy && core.hasSpecial(enemy.special, 26) && - // 检查支援条件,坐标存在,距离为1,且不能是自己 - // 其他类型的支援怪,比如十字之类的话.... 看着做是一样的 - x != null && y != null && Math.abs(block.x - x) <= 1 && Math.abs(block.y - y) <= 1 && !(x == block.x && y == block.y)) { - // 记录怪物的x,y,ID - guards.push([block.x, block.y, id]); - } - - // TODO:如果有其他类型光环怪物在这里仿照添加检查 - // 注:新增新的类光环属性(需要遍历全图的)需要在特殊属性定义那里的第五项写1,参见光环和支援的特殊属性定义。 - } - }); - - core.status.checkBlock.cache[index] = { "hp_buff": hp_buff, "atk_buff": atk_buff, "def_buff": def_buff, "guards": guards }; - } else { - // 直接使用缓存数据 - hp_buff = cache.hp_buff; - atk_buff = cache.atk_buff; - def_buff = cache.def_buff; - guards = cache.guards; - } - - // 增加比例;如果要增加数值可以直接在这里修改 - mon_hp *= (1 + hp_buff / 100); - mon_atk *= (1 + atk_buff / 100); - mon_def *= (1 + def_buff / 100); - } - - // TODO:可以在这里新增其他的怪物数据变化 - // 比如仿攻(怪物攻击不低于勇士攻击): - // if (core.hasSpecial(mon_special, 27) && mon_atk < hero_atk) { - // mon_atk = hero_atk; - // } - // 也可以按需增加各种自定义内容 - - return { - "hp": Math.floor(mon_hp), - "atk": Math.floor(mon_atk), - "def": Math.floor(mon_def), - "money": Math.floor(mon_money), - "exp": Math.floor(mon_exp), - "point": Math.floor(mon_point), - "special": mon_special, - "guards": guards, // 返回支援情况 - }; - }, - "getDamageInfo": function (enemy, hero, x, y, floorId) { - // 获得战斗伤害信息(实际伤害计算函数) - // - // 参数说明: - // enemy:该怪物信息 - // hero:勇士的当前数据;如果对应项不存在则会从core.status.hero中取。 - // x,y:该怪物的坐标(查看手册和强制战斗时为undefined) - // floorId:该怪物所在的楼层 - // 后面三个参数主要是可以在光环等效果上可以适用 - floorId = floorId || core.status.floorId; - - var hero_hp = core.getRealStatusOrDefault(hero, 'hp'), - hero_atk = core.getRealStatusOrDefault(hero, 'atk'), - hero_def = core.getRealStatusOrDefault(hero, 'def'), - hero_mdef = core.getRealStatusOrDefault(hero, 'mdef'), - origin_hero_hp = core.getStatusOrDefault(hero, 'hp'), - origin_hero_atk = core.getStatusOrDefault(hero, 'atk'), - origin_hero_def = core.getStatusOrDefault(hero, 'def'); - - // 勇士的负属性都按0计算 - hero_hp = Math.max(0, hero_hp); - hero_atk = Math.max(0, hero_atk); - hero_def = Math.max(0, hero_def); - hero_mdef = Math.max(0, hero_mdef); - - // 怪物的各项数据 - // 对坚固模仿等处理扔到了脚本编辑-getEnemyInfo之中 - var enemyInfo = core.enemys.getEnemyInfo(enemy, hero, x, y, floorId); - var mon_hp = enemyInfo.hp, - mon_atk = enemyInfo.atk, - mon_def = enemyInfo.def, - mon_special = enemyInfo.special; - - // 技能的处理 - if (core.getFlag('skill', 0) == 1) { // 开启了技能1:二倍斩 - hero_atk *= 2; // 计算时攻击力翻倍 - } - - // 如果是无敌属性,且勇士未持有十字架 - if (core.hasSpecial(mon_special, 20) && !core.hasItem("cross")) - return null; // 不可战斗 - - // 战前造成的额外伤害(可被护盾抵消) - var init_damage = 0; - - // 吸血 - if (core.hasSpecial(mon_special, 11)) { - var vampire_damage = hero_hp * enemy.vampire; - - // 如果有神圣盾免疫吸血等可以在这里写 - // 也可以用hasItem和hasEquip来判定装备 - // if (core.hasFlag('shield5')) vampire_damage = 0; - - vampire_damage = Math.floor(vampire_damage) || 0; - // 加到自身 - if (enemy.add) // 如果加到自身 - mon_hp += vampire_damage; - - init_damage += vampire_damage; - } - - // 每回合怪物对勇士造成的战斗伤害 - var per_damage = mon_atk - hero_def; - // 魔攻:战斗伤害就是怪物攻击力 - if (core.hasSpecial(mon_special, 2)) per_damage = mon_atk; - // 战斗伤害不能为负值 - if (per_damage < 0) per_damage = 0; - - // 2连击 & 3连击 & N连击 - if (core.hasSpecial(mon_special, 4)) per_damage *= 2; - if (core.hasSpecial(mon_special, 5)) per_damage *= 3; - if (core.hasSpecial(mon_special, 6)) per_damage *= (enemy.n || 4); - - // 每回合的反击伤害;反击是按照勇士的攻击次数来计算回合 - var counterDamage = 0; - if (core.hasSpecial(mon_special, 8)) - counterDamage += Math.floor((enemy.counterAttack || core.values.counterAttack) * hero_atk); - - // 先攻 - if (core.hasSpecial(mon_special, 1)) init_damage += per_damage; - - // 破甲 - if (core.hasSpecial(mon_special, 7)) - init_damage += Math.floor((enemy.breakArmor || core.values.breakArmor) * hero_def); - - // 净化 - if (core.hasSpecial(mon_special, 9)) - init_damage += Math.floor((enemy.purify || core.values.purify) * hero_mdef); - - // 勇士每回合对怪物造成的伤害 - var hero_per_damage = Math.max(hero_atk - mon_def, 0); - - // 如果没有破防,则不可战斗 - if (hero_per_damage <= 0) return null; - - // 勇士的攻击回合数;为怪物生命除以每回合伤害向上取整 - var turn = Math.ceil(mon_hp / hero_per_damage); - - // ------ 支援 ----- // - // 这个递归最好想明白为什么,flag:__extraTurn__是怎么用的 - var guards = core.getFlag("__guards__" + x + "_" + y, enemyInfo.guards); - var guard_before_current_enemy = false; // ------ 支援怪是先打(true)还是后打(false)? - turn += core.getFlag("__extraTurn__", 0); - if (guards.length > 0) { - if (!guard_before_current_enemy) { // --- 先打当前怪物,记录当前回合数 - core.setFlag("__extraTurn__", turn); - } - // 获得那些怪物组成小队战斗 - for (var i = 0; i < guards.length; i++) { - var gx = guards[i][0], - gy = guards[i][1], - gid = guards[i][2]; - // 递归计算支援怪伤害信息,这里不传x,y保证不会重复调用 - // 这里的mdef传0,因为逻辑上护盾只会在与第一个敌人战斗时被计算一次 - // 也就是说打支援敌人时护盾视为0。 - var info = core.enemys.getDamageInfo(core.getEnemyValue(gid, null, gx, gy, floorId), { hp: origin_hero_hp, atk: origin_hero_atk, def: origin_hero_def, mdef: 0 }); - if (info == null) { // 小队中任何一个怪物不可战斗,直接返回null - core.removeFlag("__extraTurn__"); - return null; - } - // 已经进行的回合数 - core.setFlag("__extraTurn__", info.turn); - init_damage += info.damage; - } - if (guard_before_current_enemy) { // --- 先打支援怪物,增加当前回合数 - turn += core.getFlag("__extraTurn__", 0); - } - } - core.removeFlag("__extraTurn__"); - // ------ 支援END ------ // - - // 最终伤害:初始伤害 + 怪物对勇士造成的伤害 + 反击伤害 - var damage = init_damage + (turn - 1) * per_damage + turn * counterDamage; - // 再扣去护盾 - damage -= hero_mdef; - - // 检查是否允许负伤 - if (!core.flags.enableNegativeDamage) - damage = Math.max(0, damage); - - // 最后处理仇恨和固伤(因为这两个不能被护盾减伤) - if (core.hasSpecial(mon_special, 17)) { // 仇恨 - damage += core.getFlag('hatred', 0); - } - if (core.hasSpecial(mon_special, 22)) { // 固伤 - damage += enemy.fixedDamage || 0; - } - - return { - "mon_hp": Math.floor(mon_hp), - "mon_atk": Math.floor(mon_atk), - "mon_def": Math.floor(mon_def), - "init_damage": Math.floor(init_damage), - "per_damage": Math.floor(per_damage), - "hero_per_damage": Math.floor(hero_per_damage), - "turn": Math.floor(turn), - "damage": Math.floor(damage) - }; - } - }, - "actions": { - "onKeyUp": function (keyCode, altKey) { - // 键盘按键处理,可以在这里自定义快捷键列表 - // keyCode:当前按键的keyCode(每个键的keyCode自行百度) - // altKey:Alt键是否被按下,为true代表同时按下了Alt键 - // 可以在这里任意增加或编辑每个按键的行为 - - // 如果处于正在行走状态,则不处理 - if (core.isMoving()) return; - - // 商店长按时忽略 - if (core.status.onShopLongDown) return core.status.onShopLongDown = false; - - // Alt+0~9,快捷换上套装 - if (altKey && keyCode >= 48 && keyCode <= 57) { - core.items.quickLoadEquip(keyCode - 48); - return; - } - - function useSingleItem(item) { - if (core.hasItem(item)) { - core.tryUseItem(item); - } else { - core.drawFailTip('当前未持有对应道具!'); - } - } - - function useItems(itemList) { - for (let i = 0, l = itemList.length; i < l; i++) { - const item = itemList[i]; - if (!core.canUseItem(item)) continue; - else { - core.tryUseItem(item); - return; - } - } - core.drawFailTip('当前未持有对应道具!'); - } - - // 根据keyCode值来执行对应操作 - switch (keyCode) { - case 27: // ESC:打开菜单栏 - core.openSettings(true); - break; - case 88: // X:使用怪物手册 - core.openBook(true); - break; - case 71: // G:使用楼传器 - core.useFly(true); - break; - case 65: // A:读取自动存档(回退) - core.doSL("autoSave", "load"); - break; - case 87: // W:撤销回退 - core.doSL("autoSave", "reload"); - break; - case 83: // S:存档 - core.save(true); - break; - case 68: // D:读档 - core.load(true); - break; - case 69: // E:打开光标 - core.ui._drawCursor(); - break; - case 84: // T:打开道具栏 - core.openToolbox(true); - break; - case 81: // Q:打开装备栏 - core.openEquipbox(true); - break; - case 90: // Z:转向 - core.turnHero(); - break; - case 86: // V:打开快捷商店列表 - core.openQuickShop(true); - break; - case 32: // SPACE:轻按 - core.getNextItem(); - break; - case 82: // R:回放录像 - core.ui._drawReplay(); - break; - case 33: - case 34: // PgUp/PgDn:浏览地图 - core.ui._drawViewMaps(); - break; - case 66: // B:打开数据统计 - core.ui._drawStatistics(); - break; - case 72: // H:打开帮助页面 - core.ui._drawHelp(); - break; - case 77: // M:打开存档笔记 - core.actions._clickNotes_show(); - break; - case 78: // N:重新开始 - core.confirmRestart(); - break; - case 79: // O:查看工程 - core.actions._clickGameInfo_openProject(); - break; - case 80: // P:游戏主页 - core.actions._clickGameInfo_openComments(); - break; - case 49: // 快捷键1: 破 - { - const item = core.getLocalStorage('hotkey1'); - if (item) useSingleItem(item); - else useSingleItem('pickaxe'); - } - break; - case 50: // 快捷键2: 炸 - { - const item = core.getLocalStorage('hotkey2'); - if (item) useSingleItem(item); - else useSingleItem('bomb'); - } - break; - case 51: // 快捷键3: 飞 - { - const item = core.getLocalStorage('hotkey3'); - if (item) useSingleItem(item); - else useSingleItem('centerFly'); - } - break; - case 52: // 快捷键4:破冰/冰冻/地震/上下楼器/... 其他道具依次判断 - { - const item = core.getLocalStorage('hotkey4'); - if (item) useSingleItem(item); - else { - const list = ["icePickaxe", "freezeBadge", "earthquake", "upFly", "downFly", "jumpShoes", - "lifeWand", "poisonWine", "weakWine", "curseWine", "superWine"]; - useItems(list); - } - } - break; - case 53: // 5:读取自动存档(回退),方便手机版操作 - { - const item = core.getLocalStorage('hotkey5'); - if (item) useSingleItem(item); - else core.doSL("autoSave", "load"); - } - break; - case 54: // 6:撤销回退,方便手机版操作 - { - const item = core.getLocalStorage('hotkey6'); - if (item) useSingleItem(item); - else core.doSL("autoSave", "reload"); - } - break; - case 55: // 快捷键7:绑定为轻按,方便手机版操作 - { - const item = core.getLocalStorage('hotkey7'); - if (item) useSingleItem(item); - else core.getNextItem(); - } - break; - case 56: // 快捷键8:空白 - { - const item = core.getLocalStorage('hotkey8'); - if (item) useSingleItem(item); - } - break; - case 57: // 快捷键9:空白 - { - const item = core.getLocalStorage('hotkey9'); - if (item) useSingleItem(item); - } - break; - case 118: // F7:开启debug模式 - case 119: // F8:由于F7与部分浏览器有冲突,故新增F8 - core.debug(); - break; - case 70: // F - break; - // 在这里可以任意新增或编辑已有的快捷键内容 - /* - case 0: // 使用该按键的keyCode - // 还可以再判定altKey是否被按下,即 if (altKey) { ... - - // ... 在这里写你要执行脚本 - // **强烈建议所有新增的自定义快捷键均能给个对应的道具可点击,以方便手机端的行为** - if (core.hasItem('...')) { - core.status.route.push("key:0"); - core.useItem('...', true); // 增加true代表该使用道具不计入录像 - } - - break; - */ - } - - }, - "onStatusBarClick": function (px, py, vertical) { - // 点击状态栏时触发的事件,仅在自绘状态栏开启时生效 - // px和py为点击的像素坐标 - // vertical为录像播放过程中的横竖屏信息 - // - // 横屏模式下状态栏的画布大小是 129*416 (开启拓展装备栏后是 129*457) - // 竖屏模式下状态栏的画布大小是 416*(32*rows+9) 其中rows为状态栏行数,即全塔属性中statusCanvasRowsOnMobile值 - // 可以使用 _isVertical() 来判定当前是否是竖屏模式 - - // 判定当前是否是竖屏模式。录像播放过程中可能会记录当时的横竖屏信息以覆盖。 - var _isVertical = function () { - if (core.isReplaying() && vertical != null) return vertical; - return core.domStyle.isVertical; - } - - // 如果正在执行事件,则忽略 - if (core.status.lockControl) return; - // 如果当前正在行走,则忽略;也可以使用 core.waitHeroToStop(callback) 来停止行走再回调执行脚本 - if (core.isMoving()) return; - - // 判定px和py来执行自己的脚本内容.... 注意横竖屏 - // console.log("onStatusBarClick: ", px, py, _isVertical()); - - // 样例一:点击某个区域后使用一个道具 - /* - if (core.hasItem("pickaxe")) { - if (_isVertical()) { - // 竖屏模式下 - if (px >= 200 && px <= 250 && py >= 50 && py <= 100) { - core.useItem("pickaxe"); - } - } else { - // 横屏模式下 - if (px >= 50 && px <= 100 && py >= 200 && py <= 250) { - core.useItem("pickaxe"); - } - } - } - */ - - // 样例二:点击某个区域后执行一段公共事件或脚本 - /* - if (core.hasFlag("xxx")) { - if (_isVertical()) { - // 竖屏模式下 - if (px >= 200 && px <= 250 && py >= 50 && py <= 100) { - // 记录点击坐标。这里的1代表此时是竖屏! - core.status.route.push("click:1:" + px + ":" + py); - // 可以插入公共事件 / 普通事件 / 执行一段脚本(如打开自绘UI或增减flag) - core.insertCommonEvent("道具商店"); - // core.insertAction(["一段事件"]); - // core.openItemShop("shop1"); - } - } else { - // 横屏模式下 - if (px >= 50 && px <= 100 && py >= 200 && py <= 250) { - // 记录点击坐标。这里的0代表此时是横屏! - core.status.route.push("click:0:" + px + ":" + py); - // 可以插入公共事件 / 普通事件 / 执行一段脚本(如打开自绘UI或增减flag) - core.insertCommonEvent("道具商店"); - // core.insertAction(["一段事件"]); - // core.openItemShop("shop1"); - } - } - } - */ - - } - }, - "control": { - "saveData": function () { - // 存档操作,此函数应该返回“具体要存档的内容” - - // 差异化存储values - var values = {}; - for (var key in core.values) { - if (!core.same(core.values[key], core.data.values[key])) - values[key] = core.clone(core.values[key]); - } - - // 要存档的内容 - var data = { - 'floorId': core.status.floorId, - 'hero': core.clone(core.status.hero), - 'hard': core.status.hard, - 'maps': core.clone(core.maps.saveMap()), - 'route': core.encodeRoute(core.status.route), - 'values': values, - 'version': core.firstData.version, - 'guid': core.getGuid(), - "time": new Date().getTime() - }; - - return data; - }, - "loadData": function (data, callback) { - // 读档操作;从存储中读取了内容后的行为 - - // 重置游戏和路线 - core.resetGame(data.hero, data.hard, data.floorId, core.maps.loadMap(data.maps, null, data.hero.flags), data.values); - core.status.route = core.decodeRoute(data.route); - core.control._bindRoutePush(); - // 文字属性,全局属性 - core.status.textAttribute = core.getFlag('textAttribute', core.status.textAttribute); - var toAttribute = core.getFlag('globalAttribute', core.status.globalAttribute); - if (!core.same(toAttribute, core.status.globalAttribute)) { - core.status.globalAttribute = toAttribute; - core.resize(); - } - // 重置音量 - core.events.setVolume(core.getFlag("__volume__", 1), 0); - // 加载勇士图标 - var icon = core.status.hero.image; - icon = core.getMappedName(icon); - if (core.material.images.images[icon]) { - core.material.images.hero = core.material.images.images[icon]; - core.material.icons.hero.width = core.material.images.images[icon].width / 4; - core.material.icons.hero.height = core.material.images.images[icon].height / 4; - } - core.setFlag('__fromLoad__', true); - - // TODO:增加自己的一些读档处理 - - // 切换到对应的楼层 - core.changeFloor(data.floorId, null, data.hero.loc, 0, function () { - // TODO:可以在这里设置读档后播放BGM - if (core.hasFlag("__bgm__")) { // 持续播放 - core.playBgm(core.getFlag("__bgm__")); - } - - core.removeFlag('__fromLoad__'); - if (callback) callback(); - }); - }, - "getStatusLabel": function (name) { - // 返回某个状态英文名的对应中文标签,如atk -> 攻击,def -> 防御等。 - // 请注意此项仅影响 libs/ 下的内容(如绘制怪物手册、数据统计等) - // 自行定义的(比如获得道具效果)中用到的“攻击+3”等需要自己去对应地方修改 - - return { - name: "名称", - lv: "等级", - hpmax: "生命上限", - hp: "生命", - manamax: "魔力上限", - mana: "魔力", - atk: "攻击", - def: "防御", - mdef: "护盾", - money: "金币", - exp: "经验", - point: "加点", - steps: "步数", - }[name] || name; - }, - "triggerDebuff": function (action, type) { - // 毒衰咒效果的获得与解除 - // action:获得还是解除;'get'表示获得,'remove'表示解除 - // type:一个数组表示获得了哪些毒衰咒效果;poison, weak,curse - if (!(type instanceof Array)) type = [type]; - - if (action == 'get') { - if (core.inArray(type, 'poison') && !core.hasFlag("poison")) { - // 获得毒效果 - core.setFlag('poison', true); - } - if (core.inArray(type, 'weak') && !core.hasFlag('weak')) { - // 获得衰效果 - core.setFlag('weak', true); - if (core.values.weakValue >= 1) { - // >=1,直接扣数值 - core.addStatus('atk', -core.values.weakValue); - core.addStatus('def', -core.values.weakValue); - } else { - // <1,扣比例 - core.addBuff('atk', -core.values.weakValue); - core.addBuff('def', -core.values.weakValue); - } - } - if (core.inArray(type, 'curse') && !core.hasFlag('curse')) { - // 获得咒效果 - core.setFlag('curse', true); - } - } else if (action == 'remove') { - var success = false; - if (core.inArray(type, "poison") && core.hasFlag("poison")) { - success = true; - // 移除毒效果 - core.setFlag("poison", false); - } - if (core.inArray(type, "weak") && core.hasFlag("weak")) { - success = true; - // 移除衰效果 - core.setFlag("weak", false); - if (core.values.weakValue >= 1) { - // >=1,直接扣数值 - core.addStatus('atk', core.values.weakValue); - core.addStatus('def', core.values.weakValue); - } else { - // <1,扣比例 - core.addBuff('atk', core.values.weakValue); - core.addBuff('def', core.values.weakValue); - } - } - if (core.inArray(type, "curse") && core.hasFlag("curse")) { - success = true; - // 移除咒效果 - core.setFlag("curse", false); - } - if (success) core.playSound('回血'); - } - }, - "updateStatusBar": function () { - // 更新状态栏 - - // 检查等级 - core.events.checkLvUp(); - - // 检查HP上限 - if (core.flags.statusBarItems.indexOf('enableHPMax') >= 0) { - core.setStatus('hp', Math.min(core.getRealStatus('hpmax'), core.getStatus('hp'))); - } - - // 设置楼层名 - if (core.status.floorId) { - core.setStatusBarInnerHTML('floor', core.status.maps[core.status.floorId].name); - } - - // 设置勇士名字和图标 - core.setStatusBarInnerHTML('name', core.getStatus('name')); - // 设置等级名称 - core.setStatusBarInnerHTML('lv', core.getLvName()); - - // 设置生命上限、生命值、攻防护盾金币和经验值 - var statusList = ['hpmax', 'hp', 'mana', 'atk', 'def', 'mdef', 'money', 'exp']; - statusList.forEach(function (item) { - // 向下取整 - core.status.hero[item] = Math.floor(core.status.hero[item]); - // 大数据格式化 - core.setStatusBarInnerHTML(item, core.getRealStatus(item)); - }); - - // 设置魔力值; status:manamax 只有在非负时才生效。 - if (core.status.hero.manamax != null && core.getRealStatus('manamax') >= 0) { - core.status.hero.mana = Math.min(core.status.hero.mana, core.getRealStatus('manamax')); - core.setStatusBarInnerHTML('mana', core.status.hero.mana + "/" + core.getRealStatus('manamax')); - } else { - core.setStatusBarInnerHTML("mana", core.status.hero.mana); - } - // 设置技能栏 - // 可以用flag:skill表示当前开启的技能类型,flag:skillName显示技能名;详见文档-个性化-技能塔的支持 - core.setStatusBarInnerHTML('skill', core.getFlag('skillName', '无')); - - // 可以在这里添加自己额外的状态栏信息,比如想攻击显示 +0.5 可以这么写: - // if (core.hasFlag('halfAtk')) core.setStatusBarInnerHTML('atk', core.statusBar.atk.innerText + "+0.5"); - - // 如果是自定义添加的状态栏,也需要在这里进行设置显示的数值 - - // 进阶 - if (core.flags.statusBarItems.indexOf('enableLevelUp') >= 0) { - core.setStatusBarInnerHTML('up', core.formatBigNumber(core.getNextLvUpNeed()) || ""); - } else core.setStatusBarInnerHTML('up', ""); - - // 钥匙 - var keys = ['yellowKey', 'blueKey', 'redKey', 'greenKey']; - keys.forEach(function (key) { - core.setStatusBarInnerHTML(key, core.setTwoDigits(core.itemCount(key))); - }); - // 毒衰咒 - core.setStatusBarInnerHTML('poison', core.hasFlag('poison') ? "毒" : ""); - core.setStatusBarInnerHTML('weak', core.hasFlag('weak') ? "衰" : ""); - core.setStatusBarInnerHTML('curse', core.hasFlag('curse') ? "咒" : ""); - // 破炸飞 - core.setStatusBarInnerHTML('pickaxe', "破" + core.itemCount('pickaxe')); - core.setStatusBarInnerHTML('bomb', "炸" + core.itemCount('bomb')); - core.setStatusBarInnerHTML('fly', "飞" + core.itemCount('centerFly')); - - // 难度 - if (core.statusBar.hard.innerText != core.status.hard) { - core.statusBar.hard.innerText = core.status.hard; - } - var hardColor = core.getFlag('__hardColor__', 'red'); - if (core.statusBar.hard.getAttribute('_style') != hardColor) { - core.statusBar.hard.style.color = hardColor; - core.statusBar.hard.setAttribute('_style', hardColor); - } - // 自定义状态栏绘制 - core.drawStatusBar(); - - // 更新阻激夹域的伤害值 - core.updateCheckBlock(); - // updateDamage只能在此处执行!!更新全地图显伤 - core.updateDamage(); - }, - "updateCheckBlock": function (floorId) { - // 领域、夹击、阻击等的伤害值计算 - floorId = floorId || core.status.floorId; - if (!floorId || !core.status.maps) return; - - var width = core.floors[floorId].width, - height = core.floors[floorId].height; - var blocks = core.getMapBlocksObj(floorId); - - var damage = {}, // 每个点的伤害值 - type = {}, // 每个点的伤害类型 - repulse = {}, // 每个点的阻击怪信息 - ambush = {}, // 每个点的捕捉信息 - chase = {}; // 每个点的追猎信息 - var betweenAttackLocs = {}; // 所有可能的夹击点 - var needCache = false; - var canGoDeadZone = core.flags.canGoDeadZone; - core.flags.canGoDeadZone = true; - - /** - * 追猎的视野能否通过该格子 - */ - function canSeeThrough(x, y) { - const block = core.getBlock(x, y); - // 空地默认一定可被穿过,有事件不允许穿过 - if (block === null || - (core.control.getChaseType().includes(block.event.cls) && !block.event.data)) return true; - return false; - } - - // 计算血网和领域、阻击、激光的伤害,计算捕捉信息 - for (var loc in blocks) { - const block = blocks[loc], - x = block.x, - y = block.y, - id = block.event.id, - enemy = core.getEnemyValue(id, null, x, y, floorId); - if (block.disable) continue; - - type[loc] = type[loc] || {}; - - // 血网 - // 如需调用当前楼层的ratio可使用 core.status.maps[floorId].ratio - if (id == 'lavaNet' && !core.hasItem('amulet')) { - damage[loc] = (damage[loc] || 0) + core.values.lavaDamage; - type[loc][(block.event.name || "血网") + "伤害"] = true; - } - - // 领域 - // 如果要防止领域伤害,可以直接简单的将 flag:no_zone 设为true - if (enemy && core.hasSpecial(enemy.special, 15) && !core.hasFlag('no_zone')) { - // 领域范围,默认为1 - var range = enemy.range || 1; - // 是否是九宫格领域 - var zoneSquare = false; - if (enemy.zoneSquare != null) zoneSquare = enemy.zoneSquare; - // 在范围内进行搜索,增加领域伤害值 - for (var dx = -range; dx <= range; dx++) { - for (var dy = -range; dy <= range; dy++) { - if (dx == 0 && dy == 0) continue; - var nx = x + dx, - ny = y + dy, - currloc = nx + "," + ny; - if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; - // 如果是十字领域,则还需要满足 |dx|+|dy|<=range - if (!zoneSquare && Math.abs(dx) + Math.abs(dy) > range) continue; - damage[currloc] = (damage[currloc] || 0) + (enemy.zone || 0); - type[currloc] = type[currloc] || {}; - type[currloc]["领域伤害"] = true; - } - } - } - - // 阻击 - // 如果要防止阻击伤害,可以直接简单的将 flag:no_repulse 设为true - if (enemy && core.hasSpecial(enemy.special, 18) && !core.hasFlag('no_repulse')) { - var scan = enemy.zoneSquare ? core.utils.scan2 : core.utils.scan; - for (var dir in scan) { - var nx = x + scan[dir].x, - ny = y + scan[dir].y, - currloc = nx + "," + ny; - if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; - damage[currloc] = (damage[currloc] || 0) + (enemy.repulse || 0); - type[currloc] = type[currloc] || {}; - type[currloc]["阻击伤害"] = true; - - var rdir = core.turnDirection(":back", dir); - // 检查下一个点是否存在事件(从而判定是否移动) - var rnx = x + scan[rdir].x, - rny = y + scan[rdir].y; - if (rnx < 0 || rnx >= width || rny < 0 || rny >= height) continue; - // 如需禁止阻击被推到已隐藏的事件处(如重生怪处),可将这一句的false改为true - if (core.getBlock(rnx, rny, floorId, false) != null) continue; - if (core.utils.scan[rdir] && !core.canMoveHero(x, y, rdir, floorId)) continue; - repulse[currloc] = (repulse[currloc] || []).concat([ - [x, y, id, rdir] - ]); - } - } - - // 激光 - // 如果要防止激光伤害,可以直接简单的将 flag:no_laser 设为true - if (enemy && core.hasSpecial(enemy.special, 24) && !core.hasFlag("no_laser")) { - for (var nx = 0; nx < width; nx++) { - var currloc = nx + "," + y; - if (nx != x) { - damage[currloc] = (damage[currloc] || 0) + (enemy.laser || 0); - type[currloc] = type[currloc] || {}; - type[currloc]["激光伤害"] = true; - } - } - for (var ny = 0; ny < height; ny++) { - var currloc = x + "," + ny; - if (ny != y) { - damage[currloc] = (damage[currloc] || 0) + (enemy.laser || 0); - type[currloc] = type[currloc] || {}; - type[currloc]["激光伤害"] = true; - } - } - } - - // 捕捉 - // 如果要防止捕捉效果,可以直接简单的将 flag:no_ambush 设为true - if (enemy && core.enemys.hasSpecial(enemy.special, 27) && !core.hasFlag("no_ambush")) { - var scan = enemy.zoneSquare ? core.utils.scan2 : core.utils.scan; - // 给周围格子加上【捕捉】记号 - for (var dir in scan) { - var nx = x + scan[dir].x, - ny = y + scan[dir].y, - currloc = nx + "," + ny; - if (nx < 0 || nx >= width || ny < 0 || ny >= height || (core.utils.scan[dir] && !core.canMoveHero(x, y, dir, floorId))) continue; - ambush[currloc] = (ambush[currloc] || []).concat([ - [x, y, id, dir] - ]); - } - } - - // 追猎 - if (enemy && core.hasSpecial(enemy.special, 28)) { - let scan = core.utils.scan; - for (const dir in scan) { - for (let i = 1; ; i++) { - const [nx, ny] = [x + i * scan[dir].x, y + i * scan[dir].y]; - const currloc = nx + "," + ny; - if (nx < 0 || nx > core.__SIZE__ - 1 || ny < 0 || ny > core.__SIZE__ - 1) break; - if (!canSeeThrough(nx, ny)) break; - if (!chase[currloc]) chase[currloc] = []; - chase[currloc].push({ x, y, dir }); - } - } - } - - // 夹击;在这里提前计算所有可能的夹击点,具体计算逻辑在下面 - // 如果要防止夹击伤害,可以简单的将 flag:no_betweenAttack 设为true - if (enemy && core.enemys.hasSpecial(enemy.special, 16) && !core.hasFlag('no_betweenAttack')) { - for (var dir in core.utils.scan) { - var nx = x + core.utils.scan[dir].x, - ny = y + core.utils.scan[dir].y, - currloc = nx + "," + ny; - if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; - betweenAttackLocs[currloc] = true; - } - } - - // 检查地图范围类技能 - var specialFlag = core.getSpecialFlag(enemy); - if (specialFlag & 1) needCache = true; - if (core.status.event.id == 'viewMaps') needCache = true; - if ((core.status.event.id == 'book' || core.status.event.id == 'bool-detail') && core.status.event.ui) needCache = true; - } - - // 对每个可能的夹击点计算夹击伤害 - for (var loc in betweenAttackLocs) { - let xy = loc.split(","), - x = parseInt(xy[0]), - y = parseInt(xy[1]); - // 夹击怪物的ID - var enemyId1 = null, - enemyId2 = null; - // 检查左右夹击 - var leftBlock = blocks[(x - 1) + "," + y], - rightBlock = blocks[(x + 1) + "," + y]; - var leftId = core.getFaceDownId(leftBlock), - rightId = core.getFaceDownId(rightBlock); - if (leftBlock && !leftBlock.disable && rightBlock && !rightBlock.disable && leftId == rightId) { - if (core.hasSpecial(leftId, 16)) - enemyId1 = leftId; - } - // 检查上下夹击 - var topBlock = blocks[x + "," + (y - 1)], - bottomBlock = blocks[x + "," + (y + 1)]; - var topId = core.getFaceDownId(topBlock), - bottomId = core.getFaceDownId(bottomBlock); - if (topBlock && !topBlock.disable && bottomBlock && !bottomBlock.disable && topId == bottomId) { - if (core.hasSpecial(topId, 16)) - enemyId2 = topId; - } - - if (enemyId1 != null || enemyId2 != null) { - var leftHp = core.status.hero.hp - (damage[loc] || 0); - - // 夹击伤害值 - var value = Math.floor(leftHp / 2); - // 是否不超过怪物伤害值 - if (core.flags.betweenAttackMax) { - var enemyDamage1 = core.getDamage(enemyId1, x, y, floorId); - if (enemyDamage1 != null && enemyDamage1 < value) - value = enemyDamage1; - var enemyDamage2 = core.getDamage(enemyId2, x, y, floorId); - if (enemyDamage2 != null && enemyDamage2 < value) - value = enemyDamage2; - } - if (value > 0) { - damage[loc] = (damage[loc] || 0) + value; - type[loc] = type[loc] || {}; - type[loc]["夹击伤害"] = true; - } - - } - } - - // 取消注释下面这一段可以让护盾抵御阻激夹域伤害 - /* - for (var loc in damage) { - damage[loc] = Math.max(0, damage[loc] - core.getRealStatus('mdef')); - } - */ - - core.flags.canGoDeadZone = canGoDeadZone; - core.status.checkBlock = { - damage: damage, - type: type, - repulse: repulse, - ambush: ambush, - chase: chase, - needCache: needCache, - cache: {} // clear cache - }; - }, - "moveOneStep": function (callback) { - // 勇士每走一步后执行的操作。callback为行走完毕后的回调 - // 这个函数执行在“刚走完”的时候,即还没有检查该点的事件和领域伤害等。 - // 请注意:瞬间移动不会执行该函数。如果要控制能否瞬间移动有三种方法: - // 1. 将全塔属性中的cannotMoveDirectly这个开关勾上,即可在全塔中全程禁止使用瞬移。 - // 2, 将楼层属性中的cannotMoveDirectly这个开关勾上,即禁止在该层楼使用瞬移。 - // 3. 将flag:cannotMoveDirectly置为true,即可使用flag控制在某段剧情范围内禁止瞬移。 - - // 增加步数 - core.status.hero.steps++; - // 更新跟随者状态,并绘制 - core.updateFollowers(); - core.drawHero(); - // 检查中毒状态的扣血和死亡 - if (core.hasFlag('poison')) { - core.status.hero.statistics.poisonDamage += core.values.poisonDamage; - if (core.control.addFloorStatistics) { - core.control.addFloorStatistics("poisonDamage", core.values.poisonDamage); - } - core.status.hero.hp -= core.values.poisonDamage; - if (core.status.hero.hp <= 0) { - core.status.hero.hp = 0; - core.updateStatusBar(false, true); - core.events.lose(); - return; - } else { - core.updateStatusBar(false, true); - } - } - - // 从v2.7开始,每一步行走不会再刷新状态栏。 - // 如果有特殊要求(如每走一步都加buff之类),可手动取消注释下面这一句: - // core.updateStatusBar(true, true); - - // 检查自动事件 - core.checkAutoEvents(); - - // ------ 检查目标点事件 ------ // - // 无事件的道具(如血瓶)需要优先于阻激夹域判定 - var nowx = core.getHeroLoc('x'), - nowy = core.getHeroLoc('y'); - var block = core.getBlock(nowx, nowy); - var hasTrigger = false; - if (block != null && block.event.trigger == 'getItem' && - !core.floors[core.status.floorId].afterGetItem[nowx + "," + nowy]) { - hasTrigger = true; - core.trigger(nowx, nowy, callback); - } - // 执行目标点的阻激夹域事件 - core.checkBlock(); - - // 执行目标点的script和事件 - if (!hasTrigger) - core.trigger(nowx, nowy, callback); - - // 绘制目标点的弹幕 - if (!core.isReplaying()) core.plugin.showComment(nowx, nowy); - - // 检查该点是否是滑冰 - if (core.onSki()) { - // 延迟到事件最后执行,因为这之前可能有阻激夹域动画 - core.insertAction({ "type": "moveAction" }, null, null, null, true); - } - - // ------ 检查目标点事件 END ------ // - - // 如需强行终止行走可以在这里条件判定: - // core.stopAutomaticRoute(); - }, - "moveDirectly": function (x, y, ignoreSteps) { - // 瞬间移动;x,y为要瞬间移动的点;ignoreSteps为减少的步数,可能之前已经被计算过 - // 返回true代表成功瞬移,false代表没有成功瞬移 - - // 判定能否瞬移到该点 - if (ignoreSteps == null) ignoreSteps = core.canMoveDirectly(x, y); - if (ignoreSteps >= 0) { - // 中毒也允许瞬移 - if (core.hasFlag('poison')) { - var damage = ignoreSteps * core.values.poisonDamage; - if (damage >= core.status.hero.hp) return false; - core.status.hero.statistics.poisonDamage += damage; - if (core.control.addFloorStatistics) { - core.control.addFloorStatistics("poisonDamage", damage); - } - core.status.hero.hp -= damage; - } - - core.clearMap('hero'); - // 获得勇士最后的朝向 - var lastDirection = core.status.route[core.status.route.length - 1]; - if (['left', 'right', 'up', 'down'].indexOf(lastDirection) >= 0) - core.setHeroLoc('direction', lastDirection); - // 设置坐标,并绘制 - core.control._moveDirectyFollowers(x, y); - core.status.hero.loc.x = x; - core.status.hero.loc.y = y; - core.drawHero(); - - // 绘制目标点的弹幕 - if (!core.isReplaying()) core.plugin.showComment(x, y); - // 记录录像 - core.status.route.push("move:" + x + ":" + y); - // 统计信息 - core.status.hero.statistics.moveDirectly++; - core.status.hero.statistics.ignoreSteps += ignoreSteps; - if (core.hasFlag('poison')) { - core.updateStatusBar(false, true); - } - core.checkRouteFolding(); - return true; - } - return false; - }, - "parallelDo": function (timestamp) { - // 并行事件处理,可以在这里写任何需要并行处理的脚本或事件 - // 该函数将被系统反复执行,每次执行间隔视浏览器或设备性能而定,一般约为16.6ms一次 - // 参数timestamp为“从游戏资源加载完毕到当前函数执行时”的时间差,以毫秒为单位 - - // 检查当前是否处于游戏开始状态 - if (!core.isPlaying()) return; - - // 执行当前楼层的并行事件处理 - if (core.status.floorId) { - try { - eval(core.floors[core.status.floorId].parallelDo); - } catch (e) { - console.error(e); - } - } - } - }, - "ui": { - "getToolboxItems": function (cls) { - // 获得道具栏中当前某类型道具的显示项和显示顺序 - // cls为道具类型,只可能是 tools, constants 和 equips - // 返回一个数组,代表当前某类型道具的显示内容和顺序 - // 默认按id升序排列,您可以取消下面的注释改为按名称排列 - - let list = Object.keys(core.status.hero.items[cls] || {}); - list = list.sort( /*function (id1, id2) { return core.material.items[id1].name <= core.material.items[id2].name ? -1 : 1 }*/); - return list; - }, - "drawStatusBar": function () { - // 自定义绘制状态栏,需要开启状态栏canvas化 - - // 如果是非状态栏canvas化,直接返回 - if (!core.flags.statusCanvas) return; - var ctx = core.dom.statusCanvasCtx; - // 清空状态栏 - core.clearMap(ctx); - // 如果是隐藏状态栏模式,直接返回 - if (!core.domStyle.showStatusBar) return; - - // 作为样板,只绘制楼层、生命、攻击、防御、护盾、金币、钥匙这七个内容 - // 需要其他的请自行进行修改;横竖屏都需要进行适配绘制。 - // (可以使用Chrome浏览器开控制台来模拟手机上的竖屏模式的显示效果,具体方式自行百度) - // 横屏模式下的画布大小是 129*416 - // 竖屏模式下的画布大小是 416*(32*rows+9) 其中rows为状态栏行数,即全塔属性中statusCanvasRowsOnMobile值 - // 可以使用 core.domStyle.isVertical 来判定当前是否是竖屏模式 - - core.setFillStyle(ctx, core.status.globalAttribute.statusBarColor || core.initStatus.globalAttribute.statusBarColor); - - // 绘制一段文字,带斜体判定 - var _fillBoldTextWithFontCheck = function (text, x, y, style) { - // 斜体判定:如果不是纯数字和字母,斜体会非常难看,需要取消 - if (!/^[-a-zA-Z0-9`~!@#$%^&*()_=+\[{\]}\\|;:'",<.>\/?]*$/.test(text)) - core.setFont(ctx, 'bold 18px Verdana'); - else core.setFont(ctx, 'italic bold 18px Verdana'); - core.fillBoldText(ctx, text, x, y, style); - } - - // 横竖屏需要分别绘制... - if (!core.domStyle.isVertical) { - // 横屏模式 - - // 绘制楼层 - core.drawImage(ctx, core.statusBar.icons.floor, 6, 9, 25, 25); - _fillBoldTextWithFontCheck((core.status.thisMap || {}).name || "", 42, 29); - - // 绘制生命 - core.drawImage(ctx, core.statusBar.icons.hp, 6, 43, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('hp')), 42, 63); - - // 绘制攻击 - core.drawImage(ctx, core.statusBar.icons.atk, 6, 77, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('atk')), 42, 97); - - // 绘制防御 - core.drawImage(ctx, core.statusBar.icons.def, 6, 111, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('def')), 42, 131); - - // 绘制护盾 - core.drawImage(ctx, core.statusBar.icons.mdef, 6, 145, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('mdef')), 42, 165); - - // 绘制金币 - core.drawImage(ctx, core.statusBar.icons.money, 6, 179, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.money), 42, 199); - - // 绘制经验 - core.drawImage(ctx, core.statusBar.icons.exp, 6, 213, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.exp), 42, 233); - - // 绘制三色钥匙 - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('yellowKey')), 11, 267, '#FFCCAA'); - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('blueKey')), 46, 267, '#AAAADD'); - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('redKey')), 81, 267, '#FF8888'); - - } else { - // 竖屏模式 - - // 绘制楼层 - core.drawImage(ctx, core.statusBar.icons.floor, 6, 6, 25, 25); - _fillBoldTextWithFontCheck((core.status.thisMap || {}).name || "", 42, 26); - - // 绘制生命 - core.drawImage(ctx, core.statusBar.icons.hp, 137, 6, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('hp')), 173, 26); - - // 绘制攻击 - core.drawImage(ctx, core.statusBar.icons.atk, 268, 6, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('atk')), 304, 26); - - // 绘制防御 - core.drawImage(ctx, core.statusBar.icons.def, 6, 38, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('def')), 42, 58); - - // 绘制护盾 - core.drawImage(ctx, core.statusBar.icons.mdef, 137, 38, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('mdef')), 173, 58); - - // 绘制金币 - core.drawImage(ctx, core.statusBar.icons.money, 268, 38, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.money), 304, 58); - - // 绘制经验 - core.drawImage(ctx, core.statusBar.icons.exp, 6, 70, 25, 25); - _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.exp), 42, 90); - - // 绘制三色钥匙 - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('yellowKey')), 142, 90, '#FFCCAA'); - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('blueKey')), 177, 90, '#AAAADD'); - _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('redKey')), 212, 90, '#FF8888'); - } - }, - "drawStatistics": function () { - // 浏览地图时参与的统计项目 - - return [ - 'yellowDoor', 'blueDoor', 'redDoor', 'greenDoor', 'steelDoor', - 'yellowKey', 'blueKey', 'redKey', 'greenKey', 'steelKey', - 'redGem', 'blueGem', 'greenGem', 'yellowGem', - 'redPotion', 'bluePotion', 'greenPotion', 'yellowPotion', 'superPotion', - 'pickaxe', 'bomb', 'centerFly', 'icePickaxe', 'freezeBadge', - 'earthquake', 'upFly', 'downFly', 'jumpShoes', 'lifeWand', - 'poisonWine', 'weakWine', 'curseWine', 'superWine', - 'sword1', 'sword2', 'sword3', 'sword4', 'sword5', - 'shield1', 'shield2', 'shield3', 'shield4', 'shield5', - // 在这里可以增加新的ID来进行统计个数,只能增加道具ID - ]; - }, - "drawAbout": function () { - // 绘制“关于”界面 - core.ui.closePanel(); - core.lockControl(); - core.status.event.id = 'about'; - - var left = 48, - top = 36, - right = core.__PIXELS__ - 2 * left, - bottom = core.__PIXELS__ - 2 * top; - - core.setAlpha('ui', 0.85); - core.fillRect('ui', left, top, right, bottom, '#000000'); - core.setAlpha('ui', 1); - core.strokeRect('ui', left - 1, top - 1, right + 1, bottom + 1, '#FFFFFF', 2); - - var text_start = left + 24; - - // 名称 - core.setTextAlign('ui', 'left'); - var globalAttribute = core.status.globalAttribute || core.initStatus.globalAttribute; - core.fillText('ui', "HTML5 魔塔样板", text_start, top + 35, globalAttribute.selectColor, "bold 22px " + globalAttribute.font); - core.fillText('ui', "版本: " + main.__VERSION__, text_start, top + 80, "#FFFFFF", "bold 17px " + globalAttribute.font); - core.fillText('ui', "作者: 艾之葵", text_start, top + 112); - core.fillText('ui', 'HTML5魔塔交流群:539113091', text_start, top + 112 + 32); - // TODO: 写自己的“关于”页面,每次增加32像素即可 - core.playSound('打开界面'); - } - } +/// +var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = +{ + "events": { + "resetGame": function (hero, hard, floorId, maps, values) { + // 重置整个游戏;此函数将在游戏开始时,或者每次读档时最先被调用 + // hero:勇士信息;hard:难度;floorId:当前楼层ID;maps:地图信息;values:全局数值信息 + + // 清除游戏数据 + // 这一步会清空状态栏和全部画布内容,并删除所有动态创建的画布 + core.clearStatus(); + // 初始化status + core.status = core.clone(core.initStatus, function (name) { + return name != 'hero' && name != 'maps'; + }); + core.control._bindRoutePush(); + core.status.played = true; + // 初始化人物,图标,统计信息 + core.status.hero = core.clone(hero); + window.hero = core.status.hero; + window.flags = core.status.hero.flags; + core.events.setHeroIcon(core.status.hero.image, true); + core.control._initStatistics(core.animateFrame.totalTime); + core.status.hero.statistics.totalTime = core.animateFrame.totalTime = + Math.max(core.status.hero.statistics.totalTime, core.animateFrame.totalTime); + core.status.hero.statistics.start = null; + // 初始难度 + core.status.hard = hard || ""; + // 初始化地图 + core.status.floorId = floorId; + core.status.maps = maps; + core.maps._resetFloorImages(); + // 初始化怪物,道具,和地图图块信息 + core.material.enemys = core.enemys.getEnemys(); + core.material.items = core.items.getItems(); + core.maps.blocksInfo = core.maps.getBlocksInfo(); + // 初始化全局数值和全局开关 + core.values = core.clone(core.data.values); + for (var key in values || {}) + core.values[key] = values[key]; + core.flags = core.clone(core.data.flags); + var globalFlags = core.getFlag("globalFlags", {}); + for (var key in globalFlags) + core.flags[key] = globalFlags[key]; + core._init_sys_flags(); + // 初始化界面,状态栏等 + core.resize(); + // 状态栏是否显示 + if (core.hasFlag('hideStatusBar')) + core.hideStatusBar(core.hasFlag('showToolbox')); + else + core.showStatusBar(); + // 隐藏右下角的音乐按钮 + core.dom.musicBtn.style.display = 'none'; + + /** 以下为2.92样板新增内容的处理 */ + // 同步剧情跳过设置 + core.plugin.checkSkipFuncs(); + // 清空自绘道具栏的缓存 + core.plugin.clearItemBoxCache(); + // 设置一次工具栏,统计出元素数量 + // 注意:录像回退rewindReplay时会调用此函数,此时工具栏应该显示录像图标,所以这里要判定 + core.setToolbarButton(core.domStyle.toolbarBtn === 'replay' ? 'replay' : 'normal'); + }, + "win": function (reason, norank, noexit) { + // 游戏获胜事件 + // 请注意,成绩统计时是按照hp进行上传并排名 + // 因为bug,请不要试图在此处修改生命来修改分数,而是在提交前使用事件块修改生命 + + // 如果不退出,则临时存储数据 + if (noexit) { + core.status.extraEvent = core.clone(core.status.event); + } + + // 游戏获胜事件 + core.ui.closePanel(); + var replaying = core.isReplaying(); + if (replaying) core.stopReplay(); + core.waitHeroToStop(function () { + if (!noexit) { + core.clearMap('all'); // 清空全地图 + core.deleteAllCanvas(); // 删除所有创建的画布 + core.dom.gif2.innerHTML = ""; + } + reason = core.replaceText(reason); + core.drawText([ + "\t[" + (reason || "恭喜通关") + "]你的分数是${status:hp}。" + ], function () { + core.events.gameOver(reason || '', replaying, norank); + }) + }); + }, + "lose": function (reason) { + // 游戏失败事件 + core.ui.closePanel(); + var replaying = core.isReplaying(); + core.stopReplay(); + core.waitHeroToStop(function () { + core.drawText([ + "\t[" + (reason || "结局1") + "]你死了。\n如题。" + ], function () { + core.events.gameOver(null, replaying); + }); + }) + }, + "changingFloor": function (floorId, heroLoc) { + // 正在切换楼层过程中执行的操作;此函数的执行时间是“屏幕完全变黑“的那一刻 + // floorId为要切换到的楼层ID;heroLoc表示勇士切换到的位置 + + // ---------- 此时还没有进行切换,当前floorId还是原来的 ---------- // + var currentId = core.status.floorId || null; // 获得当前的floorId,可能为null + var fromLoad = core.hasFlag('__fromLoad__'); // 是否是读档造成的切换 + var isFlying = core.hasFlag('__isFlying__'); // 是否是楼传造成的切换 + if (!fromLoad && !(isFlying && currentId == floorId)) { + if (!core.hasFlag("__leaveLoc__")) core.setFlag("__leaveLoc__", {}); + if (currentId != null) core.getFlag("__leaveLoc__")[currentId] = core.clone(core.status.hero.loc); + } + + // 可以对currentId进行判定,比如删除某些自定义图层等 + // if (currentId == 'MT0') { + // core.deleteAllCanvas(); + // } + + // 根据分区信息自动砍层与恢复 + if (core.autoRemoveMaps) core.autoRemoveMaps(floorId); + + // 重置画布尺寸 + core.maps.resizeMap(floorId); + // 设置勇士的位置 + heroLoc.direction = core.turnDirection(heroLoc.direction); + core.status.hero.loc = heroLoc; + // 检查重生怪并重置 + if (!fromLoad) { + core.extractBlocks(floorId); + core.status.maps[floorId].blocks.forEach(function (block) { + if (block.disable && core.enemys.hasSpecial(block.event.id, 23)) { + block.disable = false; + core.setMapBlockDisabled(floorId, block.x, block.y, false); + core.maps._updateMapArray(floorId, block.x, block.y); + } + }); + core.control.gatherFollowers(); + } + + // ---------- 重绘新地图;这一步将会设置core.status.floorId ---------- // + core.drawMap(floorId); + + // 切换楼层BGM + if (core.status.maps[floorId].bgm) { + var bgm = core.status.maps[floorId].bgm; + if (bgm instanceof Array) bgm = bgm[Math.floor(Math.random() * bgm.length)]; // 多个bgm则随机播放一个 + if (!core.hasFlag("__bgm__")) core.playBgm(bgm); + } else if (fromLoad && !core.hasFlag("__bgm__")) { + core.pauseBgm(); + } + // 更改画面色调 + var color = core.getFlag('__color__', null); + if (!color && core.status.maps[floorId].color) + color = core.status.maps[floorId].color; + core.clearMap('curtain'); + core.status.curtainColor = color; + if (color) core.fillRect('curtain', 0, 0, core.__PIXELS__, core.__PIXELS__, core.arrayToRGBA(color)); + // 更改天气 + var weather = core.getFlag('__weather__', null); + if (!weather && core.status.maps[floorId].weather) + weather = core.status.maps[floorId].weather; + if (weather) + core.setWeather(weather[0], weather[1]); + else core.setWeather(); + + core.plugin.deleteAllAnis(); // 摧毁所有现存的高级动画 + core.plugin.clearAttractAnimate(); // 摧毁拾取物品动画 + + // ...可以新增一些其他内容,比如创建个画布在右上角显示什么内容等等 + + }, + "afterChangeFloor": function (floorId) { + // 转换楼层结束的事件;此函数会在整个楼层切换完全结束后再执行 + // floorId是切换到的楼层 + + // 如果是读档,则进行检查(是否需要恢复事件) + if (core.hasFlag('__fromLoad__')) { + core.events.recoverEvents(core.getFlag("__events__")); + core.removeFlag("__events__"); + } else { + // 每次抵达楼层执行的事件 + core.insertAction(core.floors[floorId].eachArrive); + + // 首次抵达楼层时执行的事件(后插入,先执行) + if (!core.hasVisitedFloor(floorId)) { + core.insertAction(core.floors[floorId].firstArrive); + core.visitFloor(floorId); + } + } + if (!core.isReplaying()) core.plugin.drawCommentSign(); + }, + "flyTo": function (toId, callback) { + // 楼层传送器的使用,从当前楼层飞往toId + // 如果不能飞行请返回false + + var fromId = core.status.floorId; + + // 检查能否飞行 + if (!core.status.maps[fromId].canFlyFrom || !core.status.maps[toId].canFlyTo || !core.hasVisitedFloor(toId)) { + core.playSound('操作失败'); + core.drawTip("无法飞往" + core.status.maps[toId].title + "!", 'fly'); + return false; + } + + // 平面塔模式 + var stair = null, + loc = null; + if (core.flags.flyRecordPosition) { + loc = core.getFlag("__leaveLoc__", {})[toId] || null; + } + if (core.status.maps[toId].flyPoint != null && core.status.maps[toId].flyPoint.length == 2) { + stair = 'flyPoint'; + } + if (stair == null && loc == null) { + // 获得两个楼层的索引,以决定是上楼梯还是下楼梯 + var fromIndex = core.floorIds.indexOf(fromId), + toIndex = core.floorIds.indexOf(toId); + var stair = fromIndex <= toIndex ? "downFloor" : "upFloor"; + // 地下层:同层传送至上楼梯 + if (fromIndex == toIndex && core.status.maps[fromId].underGround) stair = "upFloor"; + } + + // 记录录像 + core.status.route.push("fly:" + toId); + // 传送 + core.ui.closePanel(); + core.setFlag('__isFlying__', true); + core.changeFloor(toId, stair, loc, null, function () { + core.removeFlag("__isFlying__"); + if (callback) callback(); + }); + + return true; + }, + "beforeBattle": function (enemyId, x, y) { + // 战斗前触发的事件,可以加上一些战前特效(详见下面支援的例子) + // 此函数在“检测能否战斗和自动存档”【之后】执行。如果需要更早的战前事件,请在插件中覆重写 core.events.doSystemEvent 函数。 + // 返回true则将继续战斗,返回false将不再战斗。 + + // ------ 支援技能 ------ // + if (x != null && y != null) { + var index = x + "," + y, + cache = core.status.checkBlock.cache[index] || {}, + guards = cache.guards || []; + // 如果存在支援怪 + if (guards.length > 0) { + // 记录flag,当前要参与支援的怪物 + core.setFlag("__guards__" + x + "_" + y, guards); + var actions = [{ "type": "playSound", "name": "跳跃" }]; + // 增加支援的特效动画(图块跳跃) + guards.forEach(function (g) { + core.push(actions, { "type": "jump", "from": [g[0], g[1]], "to": [x, y], "time": 300, "keep": false, "async": true }); + }); + core.push(actions, [ + { "type": "waitAsync" }, // 等待所有异步事件执行完毕 + { "type": "setBlock", "number": enemyId, "loc": [[x, y]] }, // 重新设置怪物自身 + { "type": "battle", "loc": [x, y] } // 重要!重新触发本次战斗 + ]); + core.insertAction(actions); + return false; + } + } + + return true; + }, + "afterBattle": function (enemyId, x, y) { + // 战斗结束后触发的事件 + + const floorId = core.status.floorId; + var enemy = core.getEnemyValue(enemyId, null, x, y); + var special = enemy.special; + + // 播放战斗音效和动画 + // 默认播放的动画;你也可以使用 + var animate = 'hand'; // 默认动画 + // 检查当前装备是否存在攻击动画 + var equipId = core.getEquip(0); + if (equipId && (core.material.items[equipId].equip || {}).animate) + animate = core.material.items[equipId].equip.animate; + // 你也可以在这里根据自己的需要,比如enemyId或special或flag来修改播放的动画效果 + // if (enemyId == '...') animate = '...'; + + // 检查该动画是否存在SE,如果不存在则使用默认音效 + if (!(core.material.animates[animate] || {}).se) + core.playSound('attack.mp3'); + + // 播放动画;如果不存在坐标(强制战斗)则播放到勇士自身 + if (x != null && y != null) + core.drawAnimate(animate, x, y); + else + core.drawHeroAnimate(animate); + + // 获得战斗伤害信息 + var damageInfo = core.getDamageInfo(enemyId, null, x, y) || {}; + // 战斗伤害 + var damage = damageInfo.damage; + // 当前战斗回合数,可用于战后所需的判定 + var turn = damageInfo.turn; + // 判定是否致死 + if (damage == null || damage >= core.status.hero.hp) { + core.status.hero.hp = 0; + core.updateStatusBar(false, true); + core.events.lose('战斗失败'); + return; + } + + // 记录统计数据 + const statistics = core.status.hero.statistics; + statistics.battleDamage += damage; + core.control.addFloorStatistics("battleDamage", damage); + statistics.battle++; + if (core.plugin.getUserConfig) { + const { damage: damageIntervals } = core.plugin.getUserConfig(); + const intervalRecord = damageIntervals.record; + intervalRecord.forEach(interval => { + if (interval.endsWith(",")) { + const min = Number(interval.replace(",", "")); + if (damage >= min) { + core.control.addFloorStatistics(interval, 1); + } + } + else { + const [min, max] = interval.split(",").map(x => Number(x)); + if (damage >= min && damage < max) { + core.control.addFloorStatistics(interval, 1); + } + } + }); + } + // 1防收益 + const defDamage = core.enemys.getDefDamage(enemy, -1, x, y, floorId); + if (typeof defDamage === "number") statistics.oneDefEffect -= defDamage; + + // 1护盾收益 + const mdefDamage = core.enemys.getMdefDamage(enemy, -1, x, y, floorId); + if (typeof mdefDamage === "number") statistics.oneMdefEffect -= mdefDamage; + + // 吸血额外伤害,必须放在实际扣减角色体力值之前,否则算出来是错的 + const vampireExtraLoss = core.enemys.getVampireExtraLoss(enemy, x, y, floorId); + if (vampireExtraLoss !== null) { + statistics.vampireExtraLoss += vampireExtraLoss; + core.control.addFloorStatistics("vampireExtraLoss", vampireExtraLoss); + } + + // 扣减体力值 + core.status.hero.hp -= damage; + + + // 计算当前怪物的支援怪物 + // guard:一个形如[[1, 1, 'greenSlime'], [[2, 2, 'redSlime']]]的数组 + var guards = []; + if (x != null && y != null) { + guards = core.getFlag("__guards__" + x + "_" + y, []); + core.removeFlag("__guards__" + x + "_" + y); + } + + const { x: hx, y: hy, direction } = core.status.hero.loc; + + /** 败移 */ + let failMove = false; + let failMoveInfo; + if (core.enemys.hasSpecial(special, 29)) { + for (let i = 2; i < core.__SIZE__ - 1; i++) { + const aimx = hx + core.utils.scan[direction].x * i, + aimy = hy + core.utils.scan[direction].y * i; + if (aimx < 0 || aimx > core.__SIZE__ - 1 || aimy < 0 || aimy > core.__SIZE__ - 1) break; + if (['enemys', 'enemy48'].includes(core.getBlockCls(aimx, aimy))) { + failMoveInfo = { aimx, aimy, aimId: core.getBlockId(aimx, aimy) }; + failMove = true; + break; + } + } + } + + // 获得金币 + var money = guards.reduce(function (curr, g) { + return curr + core.getEnemyValue(g[2], "money", g[0], g[1]); + }, core.getEnemyValue(enemy, "money", x, y)); + if (core.hasItem('coin')) money *= 2; // 幸运金币:双倍 + if (core.hasFlag('curse')) money = 0; // 诅咒效果 + if (failMove) money = 0; // 败移效果 + core.status.hero.money += money; + core.status.hero.statistics.money += money; + + // 获得经验 + var exp = guards.reduce(function (curr, g) { + return curr + core.getEnemyValue(g[2], "exp", g[0], g[1]); + }, core.getEnemyValue(enemy, "exp", x, y)); + if (core.hasFlag('curse')) exp = 0; + if (failMove) exp = 0; // 败移效果 + core.status.hero.exp += exp; + core.status.hero.statistics.exp += exp; + + var hint = "打败 " + core.getEnemyValue(enemy, "name", x, y); + if (core.flags.statusBarItems.indexOf('enableMoney') >= 0) + hint += ',' + core.getStatusLabel('money') + '+' + money; // hint += ",金币+" + money; + if (core.flags.statusBarItems.indexOf('enableExp') >= 0) + hint += ',' + core.getStatusLabel('exp') + '+' + exp; // hint += ",经验+" + exp; + if (failMove) hint = '敌人触发败移。'; + core.drawTip(hint, enemy.id); + + // 中毒 + if (core.enemys.hasSpecial(special, 12)) { + core.triggerDebuff('get', 'poison'); + } + // 衰弱 + if (core.enemys.hasSpecial(special, 13)) { + core.triggerDebuff('get', 'weak'); + } + // 诅咒 + if (core.enemys.hasSpecial(special, 14)) { + core.triggerDebuff('get', 'curse'); + } + // 仇恨怪物将仇恨值减半 + if (core.enemys.hasSpecial(special, 17)) { + core.setFlag('hatred', Math.floor(core.getFlag('hatred', 0) / 2)); + } + // 自爆 + if (core.enemys.hasSpecial(special, 19)) { + core.status.hero.statistics.battleDamage += core.status.hero.hp - 1; + core.status.hero.hp = 1; + } + // 退化 + if (core.enemys.hasSpecial(special, 21)) { + core.status.hero.atk -= (enemy.atkValue || 0); + core.status.hero.def -= (enemy.defValue || 0); + if (core.status.hero.atk < 0) core.status.hero.atk = 0; + if (core.status.hero.def < 0) core.status.hero.def = 0; + } + // 增加仇恨值 + core.setFlag('hatred', core.getFlag('hatred', 0) + core.values.hatred); + + // 战后的技能处理,比如扣除魔力值 + + // 事件的处理 + var todo = []; + + // 加点事件 + var point = guards.reduce(function (curr, g) { + return curr + core.getEnemyValue(g[2], "point", g[0], g[1]); + }, core.getEnemyValue(enemy, "point", x, y)) || 0; + if (core.flags.enableAddPoint && point > 0) { + core.push(todo, [{ "type": "insert", "name": "加点事件", "args": [point] }]); + } + + // 战后事件 + if (core.status.floorId != null) { + core.push(todo, core.floors[core.status.floorId].afterBattle[x + "," + y]); + } + core.push(todo, enemy.afterBattle); + + // 在这里增加其他的自定义事件需求 + /* + if (enemyId=='xxx') { + core.push(todo, [ + {"type": "...", ...}, + ]); + } + */ + + if (!failMove) { + // 如果事件不为空,将其插入 + if (todo.length > 0) core.insertAction(todo, x, y); + + // 删除该点设置的怪物信息 + delete ((flags.enemyOnPoint || {})[core.status.floorId] || {})[x + "," + y]; + + // 因为removeBlock和hideBlock都会刷新状态栏,因此将删除部分移动到这里并保证刷新只执行一次,以提升效率 + if (core.getBlock(x, y) != null) { + // 检查是否是重生怪物;如果是则仅隐藏不删除 + if (core.hasSpecial(enemy.special, 23)) { + core.hideBlock(x, y); + } else { + core.removeBlock(x, y); + } + } else { + core.updateStatusBar(); + } + } + else { + const { aimx, aimy, aimId } = failMoveInfo; + if (core.getBlockId(x, y) === enemyId && failMoveInfo && + core.getBlockId(aimx, aimy) === aimId) { + const doFailMove = [ + { "type": "setBlock", "number": enemyId, "loc": [[aimx, aimy]], "time": 50 }, + { "type": "setBlock", "number": aimId, "loc": [[x, y]], "time": 50 }, + { + "type": "function", "function": `function () { core.exchangeEnemyOnPoint(${x},${y},${aimx},${aimy}) }` + }, + ]; + core.insertAction(doFailMove); + } + } + + // 如果已有事件正在处理中 + if (core.status.event.id == null) + core.continueAutomaticRoute(); + else + core.clearContinueAutomaticRoute(); + + }, + "afterOpenDoor": function (doorId, x, y) { + // 开一个门后触发的事件 + + var todo = []; + // 检查该点的开门后事件 + if (core.status.floorId) { + core.push(todo, core.floors[core.status.floorId].afterOpenDoor[x + "," + y]); + } + // 检查批量开门事件 + var door = core.getBlockById(doorId); + if (door && door.event.doorInfo) { + core.push(todo, door.event.doorInfo.afterOpenDoor); + } + + if (todo.length > 0) core.insertAction(todo, x, y); + + if (core.status.event.id == null) + core.continueAutomaticRoute(); + else + core.clearContinueAutomaticRoute(); + }, + "afterGetItem": function (itemId, x, y, isGentleClick) { + // 获得一个道具后触发的事件 + // itemId:获得的道具ID;x和y是该道具所在的坐标 + // isGentleClick:是否是轻按触发的 + + let sound = '获得道具'; + const itemInfo = core.material.items[itemId]; + if (itemInfo.itemEffectEvent && itemInfo.itemEffectEvent.hasOwnProperty('sound')) { + sound = itemInfo.itemEffectEvent.sound; + } + core.playSound(sound); + + var todo = []; + // 检查该点的获得道具后事件。 + if (core.status.floorId == null) return; + var event = core.floors[core.status.floorId].afterGetItem[x + "," + y]; + if (event && (event instanceof Array || !isGentleClick || !event.disableOnGentleClick)) { + core.unshift(todo, event); + } + + if (todo.length > 0) core.insertAction(todo, x, y); + }, + "afterPushBox": function () { + // 推箱子后的事件 + if (core.searchBlock('box').length == 0) { + // 可以通过if语句来进行开门操作 + /* + if (core.status.floorId=='xxx') { // 在某个楼层 + core.insertAction([ // 插入一条事件 + {"type": "openDoor", "loc": [x,y]} // 开门 + ]) + } + */ + } + } + }, + "enemys": { + "getSpecials": function () { + // 获得怪物的特殊属性,每一行定义一个特殊属性。 + // 分为五项,第一项为该特殊属性的数字,第二项为特殊属性的名字,第三项为特殊属性的描述 + // 第四项为该特殊属性的颜色,可以写十六进制 #RRGGBB 或者 [r,g,b,a] 四元数组 + // 第五项为该特殊属性的标记;目前 1 代表是地图类技能(需要进行遍历全图) + // 名字和描述可以直接写字符串,也可以写个function将怪物传进去 + return [ + [1, "先攻", "怪物首先攻击", "#ffcc33"], + [2, "魔攻", "怪物无视角色的防御", "#bbb0ff"], + [3, "坚固", "怪物防御不小于角色攻击-1", "#c0b088"], + [4, "2连击", "怪物每回合攻击2次", "#ffee77"], + [5, "3连击", "怪物每回合攻击3次", "#ffee77"], + [6, function (enemy) { return (enemy.n || '') + "连击"; }, function (enemy) { return "怪物每回合攻击" + (enemy.n || 4) + "次"; }, "#ffee77"], + [7, "破甲", function (enemy) { return "战斗前,怪物附加角色防御的" + Math.floor(100 * (enemy.breakArmor || core.values.breakArmor || 0)) + "%作为伤害"; }, "#88c0ff"], + [8, "反击", function (enemy) { return "战斗时,怪物每回合附加角色攻击的" + Math.floor(100 * (enemy.counterAttack || core.values.counterAttack || 0)) + "%作为伤害,无视角色防御"; }, "#ffaa44"], + [9, "净化", function (enemy) { return "战斗前,怪物附加角色护盾的" + (enemy.purify || core.values.purify) + "倍作为伤害"; }, "#80eed6"], + [10, "模仿", "怪物的攻防和角色攻防相等", "#b0c0dd"], + [11, "吸血", function (enemy) { return "战斗前,怪物首先吸取角色的" + Math.floor(100 * enemy.vampire || 0) + "%生命(约" + Math.floor((enemy.vampire || 0) * core.getStatus('hp')) + "点)作为伤害" + (enemy.add ? ",并把伤害数值加到自身生命上" : ""); }, "#dd4448"], + [12, "中毒", "战斗后,角色陷入中毒状态,每一步损失生命" + core.values.poisonDamage + "点", "#99ee88"], + [13, "衰弱", "战斗后,角色陷入衰弱状态,攻防暂时下降" + (core.values.weakValue >= 1 ? core.values.weakValue + "点" : parseInt(core.values.weakValue * 100) + "%"), "#f0bbcc"], + [14, "诅咒", "战斗后,角色陷入诅咒状态,战斗无法获得金币和经验", "#bbeef0"], + [15, "领域", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "范围内" + (enemy.range || 1) + "格时自动减生命" + (enemy.zone || 0) + "点"; }, "#c677dd"], + [16, "夹击", "经过两只相同的怪物中间,角色生命值变成一半", "#bb99ee"], + [17, "仇恨", "战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得" + (core.values.hatred || 0) + "点仇恨值)", "#b0b666"], + [18, "阻击", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "时自动减生命" + (enemy.repulse || 0) + "点,同时怪物后退一格"; }, "#8888e6"], + [19, "自爆", "战斗后角色的生命值变成1", "#ff6666"], + [20, "无敌", "角色无法打败怪物,除非拥有十字架", "#aaaaaa"], + [21, "退化", function (enemy) { return "战斗后角色永久下降" + (enemy.atkValue || 0) + "点攻击和" + (enemy.defValue || 0) + "点防御"; }], + [22, "固伤", function (enemy) { return "战斗前,怪物对角色造成" + (enemy.fixedDamage || 0) + "点固定伤害,未开启负伤时无视角色护盾。"; }, "#ff9977"], + [23, "重生", "怪物被击败后,角色转换楼层则怪物将再次出现", "#a0e0ff"], + [24, "激光", function (enemy) { return "经过怪物同行或同列时自动减生命" + (enemy.laser || 0) + "点"; }, "#dda0dd"], + [25, "光环", function (enemy) { return (enemy.range != null ? ((enemy.haloSquare ? "该怪物九宫格" : "该怪物十字") + enemy.haloRange + "格范围内") : "同楼层所有") + "怪物生命提升" + (enemy.hpBuff || 0) + "%,攻击提升" + (enemy.atkBuff || 0) + "%,防御提升" + (enemy.defBuff || 0) + "%," + (enemy.haloAdd ? "可叠加" : "不可叠加"); }, "#e6e099", 1], + [26, "支援", "当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。", "#77c0b6", 1], + [27, "捕捉", function (enemy) { return "当走到怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "时会强制进行战斗。"; }, "#c0ddbb"], + [28, "追猎", "角色行走一步后若处在怪物视线内,怪物向角色移动一步。怪物走入角色十字1格以内时主动与角色开战。", "#DC143C"], + [29, "败移", "战后若角色面对的行/列有其它怪物,该怪物不会被击败,而是与其中最近的怪物交换位置。", "#c0ddbb"], + ]; + }, + "getEnemyInfo": function (enemy, hero, x, y, floorId) { + // 获得某个怪物变化后的数据;该函数将被伤害计算和怪物手册使用 + // 例如:坚固、模仿、仿攻等等 + // + // 参数说明: + // enemy:该怪物信息 + // hero_hp,hero_atk,hero_def,hero_mdef:勇士的生命攻防护盾数据 + // x,y:该怪物的坐标(查看手册和强制战斗时为undefined) + // floorId:该怪物所在的楼层 + // 后面三个参数主要是可以在光环等效果上可以适用(也可以按需制作部分范围光环效果) + floorId = floorId || core.status.floorId; + var hero_hp = core.getRealStatusOrDefault(hero, 'hp'), + hero_atk = core.getRealStatusOrDefault(hero, 'atk'), + hero_def = core.getRealStatusOrDefault(hero, 'def'), + hero_mdef = core.getRealStatusOrDefault(hero, 'mdef'); + + const enemyInfo = core.getEnemyValue(enemy, null, x, y, floorId); + var mon_hp = enemyInfo.hp, + mon_atk = enemyInfo.atk, + mon_def = enemyInfo.def, + mon_special = enemyInfo.special; + var mon_money = enemyInfo.money, + mon_exp = enemyInfo.exp, + mon_point = enemyInfo.point; + // 模仿 + if (core.hasSpecial(mon_special, 10)) { + mon_atk = hero_atk; + mon_def = hero_def; + } + // 坚固 + if (core.hasSpecial(mon_special, 3) && mon_def < hero_atk - 1) { + mon_def = hero_atk - 1; + } + + var guards = []; + + // 光环和支援检查 + if (!core.status.checkBlock) core.status.checkBlock = {}; + + if (core.status.checkBlock.needCache) { + // 从V2.5.4开始,对光环效果增加缓存,以解决多次重复计算的问题,从而大幅提升运行效率。 + var hp_buff = 0, + atk_buff = 0, + def_buff = 0; + // 已经计算过的光环怪ID列表,用于判定叠加 + var usedEnemyIds = {}; + // 检查光环和支援的缓存 + var index = x != null && y != null ? (x + "," + y) : floorId; + if (!core.status.checkBlock.cache) core.status.checkBlock.cache = {}; + var cache = core.status.checkBlock.cache[index]; + if (!cache) { + // 没有该点的缓存,则遍历每个图块 + core.extractBlocks(floorId); + core.status.maps[floorId].blocks.forEach(function (block) { + if (!block.disable) { + // 获得该图块的ID + var id = block.event.id, + enemy = core.getEnemyValue(id, null, block.x, block.y, floorId); + // 检查【光环】技能,数字25 + if (enemy && core.hasSpecial(enemy.special, 25)) { + // 检查是否是范围光环 + var inRange = enemy.haloRange == null; + if (enemy.haloRange != null && x != null && y != null) { + var dx = Math.abs(block.x - x), + dy = Math.abs(block.y - y); + // 检查十字和九宫格光环 + if (dx + dy <= enemy.haloRange) inRange = true; + if (enemy.haloSquare && dx <= enemy.haloRange && dy <= enemy.haloRange) inRange = true; + } + // 检查是否可叠加 + if (inRange && (enemy.haloAdd || !usedEnemyIds[enemy.id])) { + hp_buff += enemy.hpBuff || 0; + atk_buff += enemy.atkBuff || 0; + def_buff += enemy.defBuff || 0; + usedEnemyIds[enemy.id] = true; + } + } + // 检查【支援】技能,数字26 + if (enemy && core.hasSpecial(enemy.special, 26) && + // 检查支援条件,坐标存在,距离为1,且不能是自己 + // 其他类型的支援怪,比如十字之类的话.... 看着做是一样的 + x != null && y != null && Math.abs(block.x - x) <= 1 && Math.abs(block.y - y) <= 1 && !(x == block.x && y == block.y)) { + // 记录怪物的x,y,ID + guards.push([block.x, block.y, id]); + } + + // TODO:如果有其他类型光环怪物在这里仿照添加检查 + // 注:新增新的类光环属性(需要遍历全图的)需要在特殊属性定义那里的第五项写1,参见光环和支援的特殊属性定义。 + } + }); + + core.status.checkBlock.cache[index] = { "hp_buff": hp_buff, "atk_buff": atk_buff, "def_buff": def_buff, "guards": guards }; + } else { + // 直接使用缓存数据 + hp_buff = cache.hp_buff; + atk_buff = cache.atk_buff; + def_buff = cache.def_buff; + guards = cache.guards; + } + + // 增加比例;如果要增加数值可以直接在这里修改 + mon_hp *= (1 + hp_buff / 100); + mon_atk *= (1 + atk_buff / 100); + mon_def *= (1 + def_buff / 100); + } + + // TODO:可以在这里新增其他的怪物数据变化 + // 比如仿攻(怪物攻击不低于勇士攻击): + // if (core.hasSpecial(mon_special, 27) && mon_atk < hero_atk) { + // mon_atk = hero_atk; + // } + // 也可以按需增加各种自定义内容 + + return { + "hp": Math.floor(mon_hp), + "atk": Math.floor(mon_atk), + "def": Math.floor(mon_def), + "money": Math.floor(mon_money), + "exp": Math.floor(mon_exp), + "point": Math.floor(mon_point), + "special": mon_special, + "guards": guards, // 返回支援情况 + }; + }, + "getDamageInfo": function (enemy, hero, x, y, floorId) { + // 获得战斗伤害信息(实际伤害计算函数) + // + // 参数说明: + // enemy:该怪物信息 + // hero:勇士的当前数据;如果对应项不存在则会从core.status.hero中取。 + // x,y:该怪物的坐标(查看手册和强制战斗时为undefined) + // floorId:该怪物所在的楼层 + // 后面三个参数主要是可以在光环等效果上可以适用 + floorId = floorId || core.status.floorId; + + var hero_hp = core.getRealStatusOrDefault(hero, 'hp'), + hero_atk = core.getRealStatusOrDefault(hero, 'atk'), + hero_def = core.getRealStatusOrDefault(hero, 'def'), + hero_mdef = core.getRealStatusOrDefault(hero, 'mdef'), + origin_hero_hp = core.getStatusOrDefault(hero, 'hp'), + origin_hero_atk = core.getStatusOrDefault(hero, 'atk'), + origin_hero_def = core.getStatusOrDefault(hero, 'def'); + + // 勇士的负属性都按0计算 + hero_hp = Math.max(0, hero_hp); + hero_atk = Math.max(0, hero_atk); + hero_def = Math.max(0, hero_def); + hero_mdef = Math.max(0, hero_mdef); + + // 怪物的各项数据 + // 对坚固模仿等处理扔到了脚本编辑-getEnemyInfo之中 + var enemyInfo = core.enemys.getEnemyInfo(enemy, hero, x, y, floorId); + var mon_hp = enemyInfo.hp, + mon_atk = enemyInfo.atk, + mon_def = enemyInfo.def, + mon_special = enemyInfo.special; + + // 技能的处理 + if (core.getFlag('skill', 0) == 1) { // 开启了技能1:二倍斩 + hero_atk *= 2; // 计算时攻击力翻倍 + } + + // 如果是无敌属性,且勇士未持有十字架 + if (core.hasSpecial(mon_special, 20) && !core.hasItem("cross")) + return null; // 不可战斗 + + // 战前造成的额外伤害(可被护盾抵消) + var init_damage = 0; + + // 吸血 + if (core.hasSpecial(mon_special, 11)) { + var vampire_damage = hero_hp * enemy.vampire; + + // 如果有神圣盾免疫吸血等可以在这里写 + // 也可以用hasItem和hasEquip来判定装备 + // if (core.hasFlag('shield5')) vampire_damage = 0; + + vampire_damage = Math.floor(vampire_damage) || 0; + // 加到自身 + if (enemy.add) // 如果加到自身 + mon_hp += vampire_damage; + + init_damage += vampire_damage; + } + + // 每回合怪物对勇士造成的战斗伤害 + var per_damage = mon_atk - hero_def; + // 魔攻:战斗伤害就是怪物攻击力 + if (core.hasSpecial(mon_special, 2)) per_damage = mon_atk; + // 战斗伤害不能为负值 + if (per_damage < 0) per_damage = 0; + + // 2连击 & 3连击 & N连击 + if (core.hasSpecial(mon_special, 4)) per_damage *= 2; + if (core.hasSpecial(mon_special, 5)) per_damage *= 3; + if (core.hasSpecial(mon_special, 6)) per_damage *= (enemy.n || 4); + + // 每回合的反击伤害;反击是按照勇士的攻击次数来计算回合 + var counterDamage = 0; + if (core.hasSpecial(mon_special, 8)) + counterDamage += Math.floor((enemy.counterAttack || core.values.counterAttack) * hero_atk); + + // 先攻 + if (core.hasSpecial(mon_special, 1)) init_damage += per_damage; + + // 破甲 + if (core.hasSpecial(mon_special, 7)) + init_damage += Math.floor((enemy.breakArmor || core.values.breakArmor) * hero_def); + + // 净化 + if (core.hasSpecial(mon_special, 9)) + init_damage += Math.floor((enemy.purify || core.values.purify) * hero_mdef); + + // 勇士每回合对怪物造成的伤害 + var hero_per_damage = Math.max(hero_atk - mon_def, 0); + + // 如果没有破防,则不可战斗 + if (hero_per_damage <= 0) return null; + + // 勇士的攻击回合数;为怪物生命除以每回合伤害向上取整 + var turn = Math.ceil(mon_hp / hero_per_damage); + + // ------ 支援 ----- // + // 这个递归最好想明白为什么,flag:__extraTurn__是怎么用的 + var guards = core.getFlag("__guards__" + x + "_" + y, enemyInfo.guards); + var guard_before_current_enemy = false; // ------ 支援怪是先打(true)还是后打(false)? + turn += core.getFlag("__extraTurn__", 0); + if (guards.length > 0) { + if (!guard_before_current_enemy) { // --- 先打当前怪物,记录当前回合数 + core.setFlag("__extraTurn__", turn); + } + // 获得那些怪物组成小队战斗 + for (var i = 0; i < guards.length; i++) { + var gx = guards[i][0], + gy = guards[i][1], + gid = guards[i][2]; + // 递归计算支援怪伤害信息,这里不传x,y保证不会重复调用 + // 这里的mdef传0,因为逻辑上护盾只会在与第一个敌人战斗时被计算一次 + // 也就是说打支援敌人时护盾视为0。 + var info = core.enemys.getDamageInfo(core.getEnemyValue(gid, null, gx, gy, floorId), { hp: origin_hero_hp, atk: origin_hero_atk, def: origin_hero_def, mdef: 0 }); + if (info == null) { // 小队中任何一个怪物不可战斗,直接返回null + core.removeFlag("__extraTurn__"); + return null; + } + // 已经进行的回合数 + core.setFlag("__extraTurn__", info.turn); + init_damage += info.damage; + } + if (guard_before_current_enemy) { // --- 先打支援怪物,增加当前回合数 + turn += core.getFlag("__extraTurn__", 0); + } + } + core.removeFlag("__extraTurn__"); + // ------ 支援END ------ // + + // 最终伤害:初始伤害 + 怪物对勇士造成的伤害 + 反击伤害 + var damage = init_damage + (turn - 1) * per_damage + turn * counterDamage; + // 再扣去护盾 + damage -= hero_mdef; + + // 检查是否允许负伤 + if (!core.flags.enableNegativeDamage) + damage = Math.max(0, damage); + + // 最后处理仇恨和固伤(因为这两个不能被护盾减伤) + if (core.hasSpecial(mon_special, 17)) { // 仇恨 + damage += core.getFlag('hatred', 0); + } + if (core.hasSpecial(mon_special, 22)) { // 固伤 + damage += enemy.fixedDamage || 0; + } + + return { + "mon_hp": Math.floor(mon_hp), + "mon_atk": Math.floor(mon_atk), + "mon_def": Math.floor(mon_def), + "init_damage": Math.floor(init_damage), + "per_damage": Math.floor(per_damage), + "hero_per_damage": Math.floor(hero_per_damage), + "turn": Math.floor(turn), + "damage": Math.floor(damage) + }; + } + }, + "actions": { + "onKeyUp": function (keyCode, altKey) { + // 键盘按键处理,可以在这里自定义快捷键列表 + // keyCode:当前按键的keyCode(每个键的keyCode自行百度) + // altKey:Alt键是否被按下,为true代表同时按下了Alt键 + // 可以在这里任意增加或编辑每个按键的行为 + + // 如果处于正在行走状态,则不处理 + if (core.isMoving()) return; + + // 商店长按时忽略 + if (core.status.onShopLongDown) return core.status.onShopLongDown = false; + + // Alt+0~9,快捷换上套装 + if (altKey && keyCode >= 48 && keyCode <= 57) { + core.items.quickLoadEquip(keyCode - 48); + return; + } + + function useSingleItem(item) { + if (core.hasItem(item)) { + core.tryUseItem(item); + } else { + core.drawFailTip('当前未持有对应道具!'); + } + } + + function useItems(itemList) { + for (let i = 0, l = itemList.length; i < l; i++) { + const item = itemList[i]; + if (!core.canUseItem(item)) continue; + else { + core.tryUseItem(item); + return; + } + } + core.drawFailTip('当前未持有对应道具!'); + } + + // 根据keyCode值来执行对应操作 + switch (keyCode) { + case 27: // ESC:打开菜单栏 + core.openSettings(true); + break; + case 88: // X:使用怪物手册 + core.openBook(true); + break; + case 71: // G:使用楼传器 + core.useFly(true); + break; + case 65: // A:读取自动存档(回退) + core.doSL("autoSave", "load"); + break; + case 87: // W:撤销回退 + core.doSL("autoSave", "reload"); + break; + case 83: // S:存档 + core.save(true); + break; + case 68: // D:读档 + core.load(true); + break; + case 69: // E:打开光标 + core.ui._drawCursor(); + break; + case 84: // T:打开道具栏 + core.openToolbox(true); + break; + case 81: // Q:打开装备栏 + core.openEquipbox(true); + break; + case 90: // Z:转向 + core.turnHero(); + break; + case 86: // V:打开快捷商店列表 + core.openQuickShop(true); + break; + case 32: // SPACE:轻按 + core.getNextItem(); + break; + case 82: // R:回放录像 + core.ui._drawReplay(); + break; + case 33: + case 34: // PgUp/PgDn:浏览地图 + core.ui._drawViewMaps(); + break; + case 66: // B:打开数据统计 + core.ui._drawStatistics(); + break; + case 72: // H:打开帮助页面 + core.ui._drawHelp(); + break; + case 77: // M:打开存档笔记 + core.actions._clickNotes_show(); + break; + case 78: // N:重新开始 + core.confirmRestart(); + break; + case 79: // O:查看工程 + core.actions._clickGameInfo_openProject(); + break; + case 80: // P:游戏主页 + core.actions._clickGameInfo_openComments(); + break; + case 49: // 快捷键1: 破 + { + const item = core.getLocalStorage('hotkey1'); + if (item) useSingleItem(item); + else useSingleItem('pickaxe'); + } + break; + case 50: // 快捷键2: 炸 + { + const item = core.getLocalStorage('hotkey2'); + if (item) useSingleItem(item); + else useSingleItem('bomb'); + } + break; + case 51: // 快捷键3: 飞 + { + const item = core.getLocalStorage('hotkey3'); + if (item) useSingleItem(item); + else useSingleItem('centerFly'); + } + break; + case 52: // 快捷键4:破冰/冰冻/地震/上下楼器/... 其他道具依次判断 + { + const item = core.getLocalStorage('hotkey4'); + if (item) useSingleItem(item); + else { + const list = ["icePickaxe", "freezeBadge", "earthquake", "upFly", "downFly", "jumpShoes", + "lifeWand", "poisonWine", "weakWine", "curseWine", "superWine"]; + useItems(list); + } + } + break; + case 53: // 5:读取自动存档(回退),方便手机版操作 + { + const item = core.getLocalStorage('hotkey5'); + if (item) useSingleItem(item); + else core.doSL("autoSave", "load"); + } + break; + case 54: // 6:撤销回退,方便手机版操作 + { + const item = core.getLocalStorage('hotkey6'); + if (item) useSingleItem(item); + else core.doSL("autoSave", "reload"); + } + break; + case 55: // 快捷键7:绑定为轻按,方便手机版操作 + { + const item = core.getLocalStorage('hotkey7'); + if (item) useSingleItem(item); + else core.getNextItem(); + } + break; + case 56: // 快捷键8:空白 + { + const item = core.getLocalStorage('hotkey8'); + if (item) useSingleItem(item); + } + break; + case 57: // 快捷键9:空白 + { + const item = core.getLocalStorage('hotkey9'); + if (item) useSingleItem(item); + } + break; + case 118: // F7:开启debug模式 + case 119: // F8:由于F7与部分浏览器有冲突,故新增F8 + core.debug(); + break; + case 70: // F + break; + // 在这里可以任意新增或编辑已有的快捷键内容 + /* + case 0: // 使用该按键的keyCode + // 还可以再判定altKey是否被按下,即 if (altKey) { ... + + // ... 在这里写你要执行脚本 + // **强烈建议所有新增的自定义快捷键均能给个对应的道具可点击,以方便手机端的行为** + if (core.hasItem('...')) { + core.status.route.push("key:0"); + core.useItem('...', true); // 增加true代表该使用道具不计入录像 + } + + break; + */ + } + + }, + "onStatusBarClick": function (px, py, vertical) { + // 点击状态栏时触发的事件,仅在自绘状态栏开启时生效 + // px和py为点击的像素坐标 + // vertical为录像播放过程中的横竖屏信息 + // + // 横屏模式下状态栏的画布大小是 129*416 (开启拓展装备栏后是 129*457) + // 竖屏模式下状态栏的画布大小是 416*(32*rows+9) 其中rows为状态栏行数,即全塔属性中statusCanvasRowsOnMobile值 + // 可以使用 _isVertical() 来判定当前是否是竖屏模式 + + // 判定当前是否是竖屏模式。录像播放过程中可能会记录当时的横竖屏信息以覆盖。 + var _isVertical = function () { + if (core.isReplaying() && vertical != null) return vertical; + return core.domStyle.isVertical; + } + + // 如果正在执行事件,则忽略 + if (core.status.lockControl) return; + // 如果当前正在行走,则忽略;也可以使用 core.waitHeroToStop(callback) 来停止行走再回调执行脚本 + if (core.isMoving()) return; + + // 判定px和py来执行自己的脚本内容.... 注意横竖屏 + // console.log("onStatusBarClick: ", px, py, _isVertical()); + + // 样例一:点击某个区域后使用一个道具 + /* + if (core.hasItem("pickaxe")) { + if (_isVertical()) { + // 竖屏模式下 + if (px >= 200 && px <= 250 && py >= 50 && py <= 100) { + core.useItem("pickaxe"); + } + } else { + // 横屏模式下 + if (px >= 50 && px <= 100 && py >= 200 && py <= 250) { + core.useItem("pickaxe"); + } + } + } + */ + + // 样例二:点击某个区域后执行一段公共事件或脚本 + /* + if (core.hasFlag("xxx")) { + if (_isVertical()) { + // 竖屏模式下 + if (px >= 200 && px <= 250 && py >= 50 && py <= 100) { + // 记录点击坐标。这里的1代表此时是竖屏! + core.status.route.push("click:1:" + px + ":" + py); + // 可以插入公共事件 / 普通事件 / 执行一段脚本(如打开自绘UI或增减flag) + core.insertCommonEvent("道具商店"); + // core.insertAction(["一段事件"]); + // core.openItemShop("shop1"); + } + } else { + // 横屏模式下 + if (px >= 50 && px <= 100 && py >= 200 && py <= 250) { + // 记录点击坐标。这里的0代表此时是横屏! + core.status.route.push("click:0:" + px + ":" + py); + // 可以插入公共事件 / 普通事件 / 执行一段脚本(如打开自绘UI或增减flag) + core.insertCommonEvent("道具商店"); + // core.insertAction(["一段事件"]); + // core.openItemShop("shop1"); + } + } + } + */ + + } + }, + "control": { + "saveData": function () { + // 存档操作,此函数应该返回“具体要存档的内容” + + // 差异化存储values + var values = {}; + for (var key in core.values) { + if (!core.same(core.values[key], core.data.values[key])) + values[key] = core.clone(core.values[key]); + } + + // 要存档的内容 + var data = { + 'floorId': core.status.floorId, + 'hero': core.clone(core.status.hero), + 'hard': core.status.hard, + 'maps': core.clone(core.maps.saveMap()), + 'route': core.encodeRoute(core.status.route), + 'values': values, + 'version': core.firstData.version, + 'guid': core.getGuid(), + "time": new Date().getTime() + }; + + return data; + }, + "loadData": function (data, callback) { + // 读档操作;从存储中读取了内容后的行为 + + // 重置游戏和路线 + core.resetGame(data.hero, data.hard, data.floorId, core.maps.loadMap(data.maps, null, data.hero.flags), data.values); + core.status.route = core.decodeRoute(data.route); + core.control._bindRoutePush(); + // 文字属性,全局属性 + core.status.textAttribute = core.getFlag('textAttribute', core.status.textAttribute); + var toAttribute = core.getFlag('globalAttribute', core.status.globalAttribute); + if (!core.same(toAttribute, core.status.globalAttribute)) { + core.status.globalAttribute = toAttribute; + core.resize(); + } + // 重置音量 + core.events.setVolume(core.getFlag("__volume__", 1), 0); + // 加载勇士图标 + var icon = core.status.hero.image; + icon = core.getMappedName(icon); + if (core.material.images.images[icon]) { + core.material.images.hero = core.material.images.images[icon]; + core.material.icons.hero.width = core.material.images.images[icon].width / 4; + core.material.icons.hero.height = core.material.images.images[icon].height / 4; + } + core.setFlag('__fromLoad__', true); + + // TODO:增加自己的一些读档处理 + + // 切换到对应的楼层 + core.changeFloor(data.floorId, null, data.hero.loc, 0, function () { + // TODO:可以在这里设置读档后播放BGM + if (core.hasFlag("__bgm__")) { // 持续播放 + core.playBgm(core.getFlag("__bgm__")); + } + + core.removeFlag('__fromLoad__'); + if (callback) callback(); + }); + }, + "getStatusLabel": function (name) { + // 返回某个状态英文名的对应中文标签,如atk -> 攻击,def -> 防御等。 + // 请注意此项仅影响 libs/ 下的内容(如绘制怪物手册、数据统计等) + // 自行定义的(比如获得道具效果)中用到的“攻击+3”等需要自己去对应地方修改 + + return { + name: "名称", + lv: "等级", + hpmax: "生命上限", + hp: "生命", + manamax: "魔力上限", + mana: "魔力", + atk: "攻击", + def: "防御", + mdef: "护盾", + money: "金币", + exp: "经验", + point: "加点", + steps: "步数", + }[name] || name; + }, + "triggerDebuff": function (action, type) { + // 毒衰咒效果的获得与解除 + // action:获得还是解除;'get'表示获得,'remove'表示解除 + // type:一个数组表示获得了哪些毒衰咒效果;poison, weak,curse + if (!(type instanceof Array)) type = [type]; + + if (action == 'get') { + if (core.inArray(type, 'poison') && !core.hasFlag("poison")) { + // 获得毒效果 + core.setFlag('poison', true); + } + if (core.inArray(type, 'weak') && !core.hasFlag('weak')) { + // 获得衰效果 + core.setFlag('weak', true); + if (core.values.weakValue >= 1) { + // >=1,直接扣数值 + core.addStatus('atk', -core.values.weakValue); + core.addStatus('def', -core.values.weakValue); + } else { + // <1,扣比例 + core.addBuff('atk', -core.values.weakValue); + core.addBuff('def', -core.values.weakValue); + } + } + if (core.inArray(type, 'curse') && !core.hasFlag('curse')) { + // 获得咒效果 + core.setFlag('curse', true); + } + } else if (action == 'remove') { + var success = false; + if (core.inArray(type, "poison") && core.hasFlag("poison")) { + success = true; + // 移除毒效果 + core.setFlag("poison", false); + } + if (core.inArray(type, "weak") && core.hasFlag("weak")) { + success = true; + // 移除衰效果 + core.setFlag("weak", false); + if (core.values.weakValue >= 1) { + // >=1,直接扣数值 + core.addStatus('atk', core.values.weakValue); + core.addStatus('def', core.values.weakValue); + } else { + // <1,扣比例 + core.addBuff('atk', core.values.weakValue); + core.addBuff('def', core.values.weakValue); + } + } + if (core.inArray(type, "curse") && core.hasFlag("curse")) { + success = true; + // 移除咒效果 + core.setFlag("curse", false); + } + if (success) core.playSound('回血'); + } + }, + "updateStatusBar": function () { + // 更新状态栏 + + // 检查等级 + core.events.checkLvUp(); + + // 检查HP上限 + if (core.flags.statusBarItems.indexOf('enableHPMax') >= 0) { + core.setStatus('hp', Math.min(core.getRealStatus('hpmax'), core.getStatus('hp'))); + } + + // 设置楼层名 + if (core.status.floorId) { + core.setStatusBarInnerHTML('floor', core.status.maps[core.status.floorId].name); + } + + // 设置勇士名字和图标 + core.setStatusBarInnerHTML('name', core.getStatus('name')); + // 设置等级名称 + core.setStatusBarInnerHTML('lv', core.getLvName()); + + // 设置生命上限、生命值、攻防护盾金币和经验值 + var statusList = ['hpmax', 'hp', 'mana', 'atk', 'def', 'mdef', 'money', 'exp']; + statusList.forEach(function (item) { + // 向下取整 + core.status.hero[item] = Math.floor(core.status.hero[item]); + // 大数据格式化 + core.setStatusBarInnerHTML(item, core.getRealStatus(item)); + }); + + // 设置魔力值; status:manamax 只有在非负时才生效。 + if (core.status.hero.manamax != null && core.getRealStatus('manamax') >= 0) { + core.status.hero.mana = Math.min(core.status.hero.mana, core.getRealStatus('manamax')); + core.setStatusBarInnerHTML('mana', core.status.hero.mana + "/" + core.getRealStatus('manamax')); + } else { + core.setStatusBarInnerHTML("mana", core.status.hero.mana); + } + // 设置技能栏 + // 可以用flag:skill表示当前开启的技能类型,flag:skillName显示技能名;详见文档-个性化-技能塔的支持 + core.setStatusBarInnerHTML('skill', core.getFlag('skillName', '无')); + + // 可以在这里添加自己额外的状态栏信息,比如想攻击显示 +0.5 可以这么写: + // if (core.hasFlag('halfAtk')) core.setStatusBarInnerHTML('atk', core.statusBar.atk.innerText + "+0.5"); + + // 如果是自定义添加的状态栏,也需要在这里进行设置显示的数值 + + // 进阶 + if (core.flags.statusBarItems.indexOf('enableLevelUp') >= 0) { + core.setStatusBarInnerHTML('up', core.formatBigNumber(core.getNextLvUpNeed()) || ""); + } else core.setStatusBarInnerHTML('up', ""); + + // 钥匙 + var keys = ['yellowKey', 'blueKey', 'redKey', 'greenKey']; + keys.forEach(function (key) { + core.setStatusBarInnerHTML(key, core.setTwoDigits(core.itemCount(key))); + }); + // 毒衰咒 + core.setStatusBarInnerHTML('poison', core.hasFlag('poison') ? "毒" : ""); + core.setStatusBarInnerHTML('weak', core.hasFlag('weak') ? "衰" : ""); + core.setStatusBarInnerHTML('curse', core.hasFlag('curse') ? "咒" : ""); + // 破炸飞 + core.setStatusBarInnerHTML('pickaxe', "破" + core.itemCount('pickaxe')); + core.setStatusBarInnerHTML('bomb', "炸" + core.itemCount('bomb')); + core.setStatusBarInnerHTML('fly', "飞" + core.itemCount('centerFly')); + + // 难度 + if (core.statusBar.hard.innerText != core.status.hard) { + core.statusBar.hard.innerText = core.status.hard; + } + var hardColor = core.getFlag('__hardColor__', 'red'); + if (core.statusBar.hard.getAttribute('_style') != hardColor) { + core.statusBar.hard.style.color = hardColor; + core.statusBar.hard.setAttribute('_style', hardColor); + } + // 自定义状态栏绘制 + core.drawStatusBar(); + + // 更新阻激夹域的伤害值 + core.updateCheckBlock(); + // updateDamage只能在此处执行!!更新全地图显伤 + core.updateDamage(); + }, + "updateCheckBlock": function (floorId) { + // 领域、夹击、阻击等的伤害值计算 + floorId = floorId || core.status.floorId; + if (!floorId || !core.status.maps) return; + + var width = core.floors[floorId].width, + height = core.floors[floorId].height; + var blocks = core.getMapBlocksObj(floorId); + + var damage = {}, // 每个点的伤害值 + type = {}, // 每个点的伤害类型 + repulse = {}, // 每个点的阻击怪信息 + ambush = {}, // 每个点的捕捉信息 + chase = {}; // 每个点的追猎信息 + var betweenAttackLocs = {}; // 所有可能的夹击点 + var needCache = false; + var canGoDeadZone = core.flags.canGoDeadZone; + core.flags.canGoDeadZone = true; + + /** + * 追猎的视野能否通过该格子 + */ + function canSeeThrough(x, y) { + const block = core.getBlock(x, y); + // 空地默认一定可被穿过,有事件不允许穿过 + if (block === null || + (core.control.getChaseType().includes(block.event.cls) && !block.event.data)) return true; + return false; + } + + // 计算血网和领域、阻击、激光的伤害,计算捕捉信息 + for (var loc in blocks) { + const block = blocks[loc], + x = block.x, + y = block.y, + id = block.event.id, + enemy = core.getEnemyValue(id, null, x, y, floorId); + if (block.disable) continue; + + type[loc] = type[loc] || {}; + + // 血网 + // 如需调用当前楼层的ratio可使用 core.status.maps[floorId].ratio + if (id == 'lavaNet' && !core.hasItem('amulet')) { + damage[loc] = (damage[loc] || 0) + core.values.lavaDamage; + type[loc][(block.event.name || "血网") + "伤害"] = true; + } + + // 领域 + // 如果要防止领域伤害,可以直接简单的将 flag:no_zone 设为true + if (enemy && core.hasSpecial(enemy.special, 15) && !core.hasFlag('no_zone')) { + // 领域范围,默认为1 + var range = enemy.range || 1; + // 是否是九宫格领域 + var zoneSquare = false; + if (enemy.zoneSquare != null) zoneSquare = enemy.zoneSquare; + // 在范围内进行搜索,增加领域伤害值 + for (var dx = -range; dx <= range; dx++) { + for (var dy = -range; dy <= range; dy++) { + if (dx == 0 && dy == 0) continue; + var nx = x + dx, + ny = y + dy, + currloc = nx + "," + ny; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + // 如果是十字领域,则还需要满足 |dx|+|dy|<=range + if (!zoneSquare && Math.abs(dx) + Math.abs(dy) > range) continue; + damage[currloc] = (damage[currloc] || 0) + (enemy.zone || 0); + type[currloc] = type[currloc] || {}; + type[currloc]["领域伤害"] = true; + } + } + } + + // 阻击 + // 如果要防止阻击伤害,可以直接简单的将 flag:no_repulse 设为true + if (enemy && core.hasSpecial(enemy.special, 18) && !core.hasFlag('no_repulse')) { + var scan = enemy.zoneSquare ? core.utils.scan2 : core.utils.scan; + for (var dir in scan) { + var nx = x + scan[dir].x, + ny = y + scan[dir].y, + currloc = nx + "," + ny; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + damage[currloc] = (damage[currloc] || 0) + (enemy.repulse || 0); + type[currloc] = type[currloc] || {}; + type[currloc]["阻击伤害"] = true; + + var rdir = core.turnDirection(":back", dir); + // 检查下一个点是否存在事件(从而判定是否移动) + var rnx = x + scan[rdir].x, + rny = y + scan[rdir].y; + if (rnx < 0 || rnx >= width || rny < 0 || rny >= height) continue; + // 如需禁止阻击被推到已隐藏的事件处(如重生怪处),可将这一句的false改为true + if (core.getBlock(rnx, rny, floorId, false) != null) continue; + if (core.utils.scan[rdir] && !core.canMoveHero(x, y, rdir, floorId)) continue; + repulse[currloc] = (repulse[currloc] || []).concat([ + [x, y, id, rdir] + ]); + } + } + + // 激光 + // 如果要防止激光伤害,可以直接简单的将 flag:no_laser 设为true + if (enemy && core.hasSpecial(enemy.special, 24) && !core.hasFlag("no_laser")) { + for (var nx = 0; nx < width; nx++) { + var currloc = nx + "," + y; + if (nx != x) { + damage[currloc] = (damage[currloc] || 0) + (enemy.laser || 0); + type[currloc] = type[currloc] || {}; + type[currloc]["激光伤害"] = true; + } + } + for (var ny = 0; ny < height; ny++) { + var currloc = x + "," + ny; + if (ny != y) { + damage[currloc] = (damage[currloc] || 0) + (enemy.laser || 0); + type[currloc] = type[currloc] || {}; + type[currloc]["激光伤害"] = true; + } + } + } + + // 捕捉 + // 如果要防止捕捉效果,可以直接简单的将 flag:no_ambush 设为true + if (enemy && core.enemys.hasSpecial(enemy.special, 27) && !core.hasFlag("no_ambush")) { + var scan = enemy.zoneSquare ? core.utils.scan2 : core.utils.scan; + // 给周围格子加上【捕捉】记号 + for (var dir in scan) { + var nx = x + scan[dir].x, + ny = y + scan[dir].y, + currloc = nx + "," + ny; + if (nx < 0 || nx >= width || ny < 0 || ny >= height || (core.utils.scan[dir] && !core.canMoveHero(x, y, dir, floorId))) continue; + ambush[currloc] = (ambush[currloc] || []).concat([ + [x, y, id, dir] + ]); + } + } + + // 追猎 + if (enemy && core.hasSpecial(enemy.special, 28)) { + let scan = core.utils.scan; + for (const dir in scan) { + for (let i = 1; ; i++) { + const [nx, ny] = [x + i * scan[dir].x, y + i * scan[dir].y]; + const currloc = nx + "," + ny; + if (nx < 0 || nx > core.__SIZE__ - 1 || ny < 0 || ny > core.__SIZE__ - 1) break; + if (!canSeeThrough(nx, ny)) break; + if (!chase[currloc]) chase[currloc] = []; + chase[currloc].push({ x, y, dir }); + } + } + } + + // 夹击;在这里提前计算所有可能的夹击点,具体计算逻辑在下面 + // 如果要防止夹击伤害,可以简单的将 flag:no_betweenAttack 设为true + if (enemy && core.enemys.hasSpecial(enemy.special, 16) && !core.hasFlag('no_betweenAttack')) { + for (var dir in core.utils.scan) { + var nx = x + core.utils.scan[dir].x, + ny = y + core.utils.scan[dir].y, + currloc = nx + "," + ny; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + betweenAttackLocs[currloc] = true; + } + } + + // 检查地图范围类技能 + var specialFlag = core.getSpecialFlag(enemy); + if (specialFlag & 1) needCache = true; + if (core.status.event.id == 'viewMaps') needCache = true; + if ((core.status.event.id == 'book' || core.status.event.id == 'bool-detail') && core.status.event.ui) needCache = true; + } + + // 对每个可能的夹击点计算夹击伤害 + for (var loc in betweenAttackLocs) { + let xy = loc.split(","), + x = parseInt(xy[0]), + y = parseInt(xy[1]); + // 夹击怪物的ID + var enemyId1 = null, + enemyId2 = null; + // 检查左右夹击 + var leftBlock = blocks[(x - 1) + "," + y], + rightBlock = blocks[(x + 1) + "," + y]; + var leftId = core.getFaceDownId(leftBlock), + rightId = core.getFaceDownId(rightBlock); + if (leftBlock && !leftBlock.disable && rightBlock && !rightBlock.disable && leftId == rightId) { + if (core.hasSpecial(leftId, 16)) + enemyId1 = leftId; + } + // 检查上下夹击 + var topBlock = blocks[x + "," + (y - 1)], + bottomBlock = blocks[x + "," + (y + 1)]; + var topId = core.getFaceDownId(topBlock), + bottomId = core.getFaceDownId(bottomBlock); + if (topBlock && !topBlock.disable && bottomBlock && !bottomBlock.disable && topId == bottomId) { + if (core.hasSpecial(topId, 16)) + enemyId2 = topId; + } + + if (enemyId1 != null || enemyId2 != null) { + var leftHp = core.status.hero.hp - (damage[loc] || 0); + + // 夹击伤害值 + var value = Math.floor(leftHp / 2); + // 是否不超过怪物伤害值 + if (core.flags.betweenAttackMax) { + var enemyDamage1 = core.getDamage(enemyId1, x, y, floorId); + if (enemyDamage1 != null && enemyDamage1 < value) + value = enemyDamage1; + var enemyDamage2 = core.getDamage(enemyId2, x, y, floorId); + if (enemyDamage2 != null && enemyDamage2 < value) + value = enemyDamage2; + } + if (value > 0) { + damage[loc] = (damage[loc] || 0) + value; + type[loc] = type[loc] || {}; + type[loc]["夹击伤害"] = true; + } + + } + } + + // 取消注释下面这一段可以让护盾抵御阻激夹域伤害 + /* + for (var loc in damage) { + damage[loc] = Math.max(0, damage[loc] - core.getRealStatus('mdef')); + } + */ + + core.flags.canGoDeadZone = canGoDeadZone; + core.status.checkBlock = { + damage: damage, + type: type, + repulse: repulse, + ambush: ambush, + chase: chase, + needCache: needCache, + cache: {} // clear cache + }; + }, + "moveOneStep": function (callback) { + // 勇士每走一步后执行的操作。callback为行走完毕后的回调 + // 这个函数执行在“刚走完”的时候,即还没有检查该点的事件和领域伤害等。 + // 请注意:瞬间移动不会执行该函数。如果要控制能否瞬间移动有三种方法: + // 1. 将全塔属性中的cannotMoveDirectly这个开关勾上,即可在全塔中全程禁止使用瞬移。 + // 2, 将楼层属性中的cannotMoveDirectly这个开关勾上,即禁止在该层楼使用瞬移。 + // 3. 将flag:cannotMoveDirectly置为true,即可使用flag控制在某段剧情范围内禁止瞬移。 + + // 增加步数 + core.status.hero.steps++; + // 更新跟随者状态,并绘制 + core.updateFollowers(); + core.drawHero(); + // 检查中毒状态的扣血和死亡 + if (core.hasFlag('poison')) { + core.status.hero.statistics.poisonDamage += core.values.poisonDamage; + if (core.control.addFloorStatistics) { + core.control.addFloorStatistics("poisonDamage", core.values.poisonDamage); + } + core.status.hero.hp -= core.values.poisonDamage; + if (core.status.hero.hp <= 0) { + core.status.hero.hp = 0; + core.updateStatusBar(false, true); + core.events.lose(); + return; + } else { + core.updateStatusBar(false, true); + } + } + + // 从v2.7开始,每一步行走不会再刷新状态栏。 + // 如果有特殊要求(如每走一步都加buff之类),可手动取消注释下面这一句: + // core.updateStatusBar(true, true); + + // 检查自动事件 + core.checkAutoEvents(); + + // ------ 检查目标点事件 ------ // + // 无事件的道具(如血瓶)需要优先于阻激夹域判定 + var nowx = core.getHeroLoc('x'), + nowy = core.getHeroLoc('y'); + var block = core.getBlock(nowx, nowy); + var hasTrigger = false; + if (block != null && block.event.trigger == 'getItem' && + !core.floors[core.status.floorId].afterGetItem[nowx + "," + nowy]) { + hasTrigger = true; + core.trigger(nowx, nowy, callback); + } + // 执行目标点的阻激夹域事件 + core.checkBlock(); + + // 执行目标点的script和事件 + if (!hasTrigger) + core.trigger(nowx, nowy, callback); + + // 绘制目标点的弹幕 + if (!core.isReplaying()) core.plugin.showComment(nowx, nowy); + + // 检查该点是否是滑冰 + if (core.onSki()) { + // 延迟到事件最后执行,因为这之前可能有阻激夹域动画 + core.insertAction({ "type": "moveAction" }, null, null, null, true); + } + + // ------ 检查目标点事件 END ------ // + + // 如需强行终止行走可以在这里条件判定: + // core.stopAutomaticRoute(); + }, + "moveDirectly": function (x, y, ignoreSteps) { + // 瞬间移动;x,y为要瞬间移动的点;ignoreSteps为减少的步数,可能之前已经被计算过 + // 返回true代表成功瞬移,false代表没有成功瞬移 + + // 判定能否瞬移到该点 + if (ignoreSteps == null) ignoreSteps = core.canMoveDirectly(x, y); + if (ignoreSteps >= 0) { + // 中毒也允许瞬移 + if (core.hasFlag('poison')) { + var damage = ignoreSteps * core.values.poisonDamage; + if (damage >= core.status.hero.hp) return false; + core.status.hero.statistics.poisonDamage += damage; + if (core.control.addFloorStatistics) { + core.control.addFloorStatistics("poisonDamage", damage); + } + core.status.hero.hp -= damage; + } + + core.clearMap('hero'); + // 获得勇士最后的朝向 + var lastDirection = core.status.route[core.status.route.length - 1]; + if (['left', 'right', 'up', 'down'].indexOf(lastDirection) >= 0) + core.setHeroLoc('direction', lastDirection); + // 设置坐标,并绘制 + core.control._moveDirectyFollowers(x, y); + core.status.hero.loc.x = x; + core.status.hero.loc.y = y; + core.drawHero(); + + // 绘制目标点的弹幕 + if (!core.isReplaying()) core.plugin.showComment(x, y); + // 记录录像 + core.status.route.push("move:" + x + ":" + y); + // 统计信息 + core.status.hero.statistics.moveDirectly++; + core.status.hero.statistics.ignoreSteps += ignoreSteps; + if (core.hasFlag('poison')) { + core.updateStatusBar(false, true); + } + core.checkRouteFolding(); + return true; + } + return false; + }, + "parallelDo": function (timestamp) { + // 并行事件处理,可以在这里写任何需要并行处理的脚本或事件 + // 该函数将被系统反复执行,每次执行间隔视浏览器或设备性能而定,一般约为16.6ms一次 + // 参数timestamp为“从游戏资源加载完毕到当前函数执行时”的时间差,以毫秒为单位 + + // 检查当前是否处于游戏开始状态 + if (!core.isPlaying()) return; + + // 执行当前楼层的并行事件处理 + if (core.status.floorId) { + try { + eval(core.floors[core.status.floorId].parallelDo); + } catch (e) { + console.error(e); + } + } + } + }, + "ui": { + "getToolboxItems": function (cls) { + // 获得道具栏中当前某类型道具的显示项和显示顺序 + // cls为道具类型,只可能是 tools, constants 和 equips + // 返回一个数组,代表当前某类型道具的显示内容和顺序 + // 默认按id升序排列,您可以取消下面的注释改为按名称排列 + + let list = Object.keys(core.status.hero.items[cls] || {}); + list = list.sort( /*function (id1, id2) { return core.material.items[id1].name <= core.material.items[id2].name ? -1 : 1 }*/); + return list; + }, + "drawStatusBar": function () { + // 自定义绘制状态栏,需要开启状态栏canvas化 + + // 如果是非状态栏canvas化,直接返回 + if (!core.flags.statusCanvas) return; + var ctx = core.dom.statusCanvasCtx; + // 清空状态栏 + core.clearMap(ctx); + // 如果是隐藏状态栏模式,直接返回 + if (!core.domStyle.showStatusBar) return; + + // 作为样板,只绘制楼层、生命、攻击、防御、护盾、金币、钥匙这七个内容 + // 需要其他的请自行进行修改;横竖屏都需要进行适配绘制。 + // (可以使用Chrome浏览器开控制台来模拟手机上的竖屏模式的显示效果,具体方式自行百度) + // 横屏模式下的画布大小是 129*416 + // 竖屏模式下的画布大小是 416*(32*rows+9) 其中rows为状态栏行数,即全塔属性中statusCanvasRowsOnMobile值 + // 可以使用 core.domStyle.isVertical 来判定当前是否是竖屏模式 + + core.setFillStyle(ctx, core.status.globalAttribute.statusBarColor || core.initStatus.globalAttribute.statusBarColor); + + // 绘制一段文字,带斜体判定 + var _fillBoldTextWithFontCheck = function (text, x, y, style) { + // 斜体判定:如果不是纯数字和字母,斜体会非常难看,需要取消 + if (!/^[-a-zA-Z0-9`~!@#$%^&*()_=+\[{\]}\\|;:'",<.>\/?]*$/.test(text)) + core.setFont(ctx, 'bold 18px Verdana'); + else core.setFont(ctx, 'italic bold 18px Verdana'); + core.fillBoldText(ctx, text, x, y, style); + } + + // 横竖屏需要分别绘制... + if (!core.domStyle.isVertical) { + // 横屏模式 + + // 绘制楼层 + core.drawImage(ctx, core.statusBar.icons.floor, 6, 9, 25, 25); + _fillBoldTextWithFontCheck((core.status.thisMap || {}).name || "", 42, 29); + + // 绘制生命 + core.drawImage(ctx, core.statusBar.icons.hp, 6, 43, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('hp')), 42, 63); + + // 绘制攻击 + core.drawImage(ctx, core.statusBar.icons.atk, 6, 77, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('atk')), 42, 97); + + // 绘制防御 + core.drawImage(ctx, core.statusBar.icons.def, 6, 111, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('def')), 42, 131); + + // 绘制护盾 + core.drawImage(ctx, core.statusBar.icons.mdef, 6, 145, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('mdef')), 42, 165); + + // 绘制金币 + core.drawImage(ctx, core.statusBar.icons.money, 6, 179, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.money), 42, 199); + + // 绘制经验 + core.drawImage(ctx, core.statusBar.icons.exp, 6, 213, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.exp), 42, 233); + + // 绘制三色钥匙 + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('yellowKey')), 11, 267, '#FFCCAA'); + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('blueKey')), 46, 267, '#AAAADD'); + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('redKey')), 81, 267, '#FF8888'); + + } else { + // 竖屏模式 + + // 绘制楼层 + core.drawImage(ctx, core.statusBar.icons.floor, 6, 6, 25, 25); + _fillBoldTextWithFontCheck((core.status.thisMap || {}).name || "", 42, 26); + + // 绘制生命 + core.drawImage(ctx, core.statusBar.icons.hp, 137, 6, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('hp')), 173, 26); + + // 绘制攻击 + core.drawImage(ctx, core.statusBar.icons.atk, 268, 6, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('atk')), 304, 26); + + // 绘制防御 + core.drawImage(ctx, core.statusBar.icons.def, 6, 38, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('def')), 42, 58); + + // 绘制护盾 + core.drawImage(ctx, core.statusBar.icons.mdef, 137, 38, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.getRealStatus('mdef')), 173, 58); + + // 绘制金币 + core.drawImage(ctx, core.statusBar.icons.money, 268, 38, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.money), 304, 58); + + // 绘制经验 + core.drawImage(ctx, core.statusBar.icons.exp, 6, 70, 25, 25); + _fillBoldTextWithFontCheck(core.formatBigNumber(core.status.hero.exp), 42, 90); + + // 绘制三色钥匙 + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('yellowKey')), 142, 90, '#FFCCAA'); + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('blueKey')), 177, 90, '#AAAADD'); + _fillBoldTextWithFontCheck(core.setTwoDigits(core.itemCount('redKey')), 212, 90, '#FF8888'); + } + }, + "drawStatistics": function () { + // 浏览地图时参与的统计项目 + + return [ + 'yellowDoor', 'blueDoor', 'redDoor', 'greenDoor', 'steelDoor', + 'yellowKey', 'blueKey', 'redKey', 'greenKey', 'steelKey', + 'redGem', 'blueGem', 'greenGem', 'yellowGem', + 'redPotion', 'bluePotion', 'greenPotion', 'yellowPotion', 'superPotion', + 'pickaxe', 'bomb', 'centerFly', 'icePickaxe', 'freezeBadge', + 'earthquake', 'upFly', 'downFly', 'jumpShoes', 'lifeWand', + 'poisonWine', 'weakWine', 'curseWine', 'superWine', + 'sword1', 'sword2', 'sword3', 'sword4', 'sword5', + 'shield1', 'shield2', 'shield3', 'shield4', 'shield5', + // 在这里可以增加新的ID来进行统计个数,只能增加道具ID + ]; + }, + "drawAbout": function () { + // 绘制“关于”界面 + core.ui.closePanel(); + core.lockControl(); + core.status.event.id = 'about'; + + var left = 48, + top = 36, + right = core.__PIXELS__ - 2 * left, + bottom = core.__PIXELS__ - 2 * top; + + core.setAlpha('ui', 0.85); + core.fillRect('ui', left, top, right, bottom, '#000000'); + core.setAlpha('ui', 1); + core.strokeRect('ui', left - 1, top - 1, right + 1, bottom + 1, '#FFFFFF', 2); + + var text_start = left + 24; + + // 名称 + core.setTextAlign('ui', 'left'); + var globalAttribute = core.status.globalAttribute || core.initStatus.globalAttribute; + core.fillText('ui', "HTML5 魔塔样板", text_start, top + 35, globalAttribute.selectColor, "bold 22px " + globalAttribute.font); + core.fillText('ui', "版本: " + main.__VERSION__, text_start, top + 80, "#FFFFFF", "bold 17px " + globalAttribute.font); + core.fillText('ui', "作者: 艾之葵", text_start, top + 112); + core.fillText('ui', 'HTML5魔塔交流群:539113091', text_start, top + 112 + 32); + // TODO: 写自己的“关于”页面,每次增加32像素即可 + core.playSound('打开界面'); + } + } } \ No newline at end of file diff --git a/project/plugins.js b/project/plugins.js index 81f71a13..336f3ea7 100644 --- a/project/plugins.js +++ b/project/plugins.js @@ -3270,6 +3270,2105 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } }, + "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(); + }; + }, "drawItemDetail": function () { /* 宝石血瓶左下角显示数值 * 需要将 变量:itemDetail改为true才可正常运行 @@ -4079,9 +6178,20 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } } 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, + 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 */ @@ -4215,8 +6325,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = btn.menu = this; btn.ctx = this.name; btn.key = key; - if (event == null) { } - else if (typeof event === 'function') { + if (event == null) { } else if (typeof event === 'function') { btn.ondown = event; } else { const { ondown, onmove, onup } = event; @@ -4284,12 +6393,209 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } } + class PagedMenu extends MenuBase { + constructor(name, toListen, x, y, w, h, zIndex, capacity, data) { + super(name, toListen, x, y, w, h, zIndex); + this.page = 0; + /** @type {any[]} */ + this.totalItemList = data; + /** @type {any[]} */ + this.currItemList; + this.capacity = capacity; + } + + drawContent() { + const pageDownBtn = this.btnMap.get("pageDown"), + pageUpBtn = this.btnMap.get("pageUp"); + if (pageDownBtn) pageDownBtn.alpha = this.canPageDown() ? 1 : 0.3; + if (pageUpBtn) pageUpBtn.alpha = this.canPageUp() ? 1 : 0.3; + super.drawContent(); + } + + /** 此类初始化时需要调用一次 **/ + updateCurrItemList() { + this.currItemList = this.totalItemList.slice(this.page * this.capacity, (this.page + 1) * this.capacity); + } + + canPageDown() { + return this.page > 0; + } + + pageDown() { + if (!this.canPageDown()) return; + this.page--; + this.updateCurrItemList(); + this.drawContent(); + } + + canPageUp() { + return (this.page + 1) * this.capacity < this.totalItemList.length + } + + pageUp() { + if (!this.canPageUp()) return; + this.page++; + this.updateCurrItemList(); + this.drawContent(); + } + } + + class MultiTypePagedMenu extends MenuBase { + /** + * @param {string} name - 菜单名称 + * @param {any} toListen - 要监听的事件集合 + * @param {number} x - X坐标 + * @param {number} y - Y坐标 + * @param {number} w - 宽度 + * @param {number} h - 高度 + * @param {number} zIndex - 层级 + * @param {Object} pagesConfig + * 配置对象,格式示例: + * { + * 'items': { capacity: 5, totalItemList: [...], pageDownBtnKey: 'btnItemPrev', pageUpBtnKey: 'btnItemNext' }, + * 'logs': { capacity: 10, totalItemList: [...], pageDownBtnKey: 'btnLogPrev', pageUpBtnKey: 'btnLogNext' } + * } + */ + constructor(name, toListen, x, y, w, h, zIndex, pagesConfig) { + super(name, toListen, x, y, w, h, zIndex); + + // 内部状态存储 + this.page = {}; // { type: currentPageIndex } + this.totalItemList = {}; // { type: fullData[] } + this.currItemList = {}; // { type: currentSlice[] } + this.capacityConfig = {}; // { type: capacityNum } + + // 按钮键名映射存储: { type: { down: key, up: key } } + this.btnKeyMap = {}; + + for (const type in pagesConfig) { + const config = pagesConfig[type]; + this.capacityConfig[type] = config.capacity; + this.totalItemList[type] = config.totalItemList; + this.page[type] = 0; + this.btnKeyMap[type] = { + down: config.pageDownBtnKey, + up: config.pageUpBtnKey + }; + this.updateCurrItemList(type); + } + } + + drawContent() { + for (const type in this.btnKeyMap) { + const keys = this.btnKeyMap[type]; + + if (keys.down) { + const btn = this.btnMap.get(keys.down); + if (btn) { + btn.alpha = this.canPageDown(type) ? 1 : 0.3; + } + } + + if (keys.up) { + const btn = this.btnMap.get(keys.up); + if (btn) { + btn.alpha = this.canPageUp(type) ? 1 : 0.3; + } + } + } + super.drawContent(); + } + + /** + * @param {string} [pageType] + */ + updateCurrItemList(pageType) { + if (!pageType) { + for (const type in this.page) { + this.updateCurrItemList(type); + } + return; + } + const currentPage = this.page[pageType] || 0; + const capacity = this.capacityConfig[pageType]; + + const start = currentPage * capacity; + const end = start + capacity; + + this.currItemList[pageType] = this.totalItemList[pageType].slice(start, end); + } + + /** + * 检查指定类型是否可以向下翻页 (回到上一页) + * @param {string} pageType + * @returns {boolean} + */ + canPageDown(pageType) { + return (this.page[pageType] || 0) > 0; + } + + /** + * 执行向下翻页 (页码 -1) + * @param {string} pageType + */ + pageDown(pageType) { + if (!this.canPageDown(pageType)) return; + + this.page[pageType]--; + this.updateCurrItemList(pageType); + this.drawContent(); + } + + /** + * 检查指定类型是否可以向上翻页 (进入下一页) + * @param {string} pageType + * @returns {boolean} + */ + canPageUp(pageType) { + const currentPage = this.page[pageType] || 0; + const capacity = this.capacityConfig[pageType]; + const totalLen = this.totalItemList[pageType] ? this.totalItemList[pageType].length : 0; + + return (currentPage + 1) * capacity < totalLen; + } + + /** + * 执行向上翻页 (页码 +1) + * @param {string} pageType + */ + pageUp(pageType) { + if (!this.canPageUp(pageType)) return; + + this.page[pageType]++; + this.updateCurrItemList(pageType); + this.drawContent(); // 重绘以更新按钮状态和内容 + } + + // --- 辅助方法 --- + /** + * 获取指定类型的当前页码 (从0开始) + * @param {string} pageType + * @returns {number} + */ + getCurrentPage(pageType) { + return this.page[pageType] || 0; + } + + /** + * 获取指定类型的总页数 + * @param {string} pageType + * @returns {number} + */ + getTotalPages(pageType) { + const totalLen = this.totalItemList[pageType] ? this.totalItemList[pageType].length : 0; + const capacity = this.capacityConfig[pageType]; + if (!capacity || capacity <= 0) return 0; + return Math.ceil(totalLen / capacity); + } + } + class Pagination extends MenuBase { constructor(pageList, currPage, name, toListen, x, y, w, h, zIndex) { super(name, toListen, x, y, w, h, zIndex); /** * 当前页面列表 - * @type {Array} + * @type {Array} */ this.pageList = pageList; /** @@ -4415,6 +6721,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = this.config = config || {}; /** @type {'left'|'right'|'up'|'down'} */ this.dir = dir; + this.alpha = 1; } draw() { @@ -4430,7 +6737,9 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = arrowStyle = 'black' } = this.config || {}; - const { x, y, w, h, ctx } = this; + const { x, y, w, h, ctx, alpha } = this; + + const originAlpha = core.setAlpha(ctx, alpha); core.fillRoundRect(ctx, x, y, w, h, 3, backStyle); @@ -4438,41 +6747,55 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = if (this.dir === 'left') { points = [ - [x + marginTip, y + h / 2], - [x + w - marginTail, y + marginSide], - [x + w - marginTail, y + h - marginSide] + [x + marginTip, y + h / 2], + [x + w - marginTail, y + marginSide], + [x + w - marginTail, y + h - marginSide] ]; - } - else if (this.dir === 'right') { + } else if (this.dir === 'right') { points = [ - [x + w - marginTip, y + h / 2], - [x + marginTail, y + marginSide], + [x + w - marginTip, y + h / 2], + [x + marginTail, y + marginSide], [x + marginTail, y + h - marginSide] ]; - } - else if (this.dir === 'up') { + } else if (this.dir === 'up') { points = [ - [x + w / 2, y + marginTip], - [x + marginSide, y + h - marginTail], - [x + w - marginSide, y + h - marginTail] + [x + w / 2, y + marginTip], + [x + marginSide, y + h - marginTail], + [x + w - marginSide, y + h - marginTail] ]; - } - else if (this.dir === 'down') { + } else if (this.dir === 'down') { points = [ - [x + w / 2, y + h - marginTip], - [x + marginSide, y + marginTail], - [x + w - marginSide, y + marginTail] + [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.setAlpha(ctx, originAlpha); } } + const registerResize = (menu) => { + core.control.registerDymCanvasResizeEvent(menu.name, function () { + if (menu && menu.onDraw) menu.drawContent(); + }); + } + const unregisterResize = (menu) => core.control.unregisterDymCanvasResizeEvent(menu.name); core.plugin.uiBase = { - ButtonBase, RoundBtn, IconBtn, ExitBtn, - ArrowBtn, MenuBase, Pagination, KeyCodeEnum + ButtonBase, + RoundBtn, + IconBtn, + ExitBtn, + ArrowBtn, + MenuBase, + PagedMenu, + MultiTypePagedMenu, + Pagination, + KeyCodeEnum, + registerResize, + unregisterResize, }; }, "newBackpackLook": function () { @@ -5337,7 +7660,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = const currItemHotKey = HotkeySelect.getHotkeyNum(itemId); // 获取快捷键设置按钮当前的图标 - const setHotkeyBtn = /** @type {IconBtnClass} */ (this.btnMap.get('setHotkeyBtn')); + const setHotkeyBtn = /** @type {IconBtn} */ (this.btnMap.get('setHotkeyBtn')); if (setHotkeyBtn) { setHotkeyBtn.disable = (UI.type === 'equips'); setHotkeyBtn.icon = (currItemHotKey == null) ? 'keyboard' : ('btn' + currItemHotKey); @@ -7671,2105 +9994,6 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } // @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; @@ -9778,8 +10002,18 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = const PX = core.__PIXELS__, SIZE = core.__SIZE__; const { - ButtonBase, RoundBtn, IconBtn, ExitBtn, - ArrowBtn, MenuBase, Pagination, KeyCodeEnum + ButtonBase, + RoundBtn, + IconBtn, + ExitBtn, + ArrowBtn, + MenuBase, + PagedMenu, + MultiTypePagedMenu, + Pagination, + KeyCodeEnum, + registerResize, + unregisterResize, } = core.plugin.uiBase; // 每次退出数据统计时清空,并且清空statistics变量 @@ -9878,7 +10112,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = /** * @param {{total:number; [itemId:string]:{[floorId:string]:number}}} obj1 - * @param {{total:number, [itemId:string]:number}} obj2 + * @param {{total:number; [itemId:string]:number}} obj2 */ function addCountToFormer(obj1, obj2, floorId) { obj1.total += obj2.total; @@ -9896,7 +10130,8 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = * equipsCount:ObjSTAT,enemysCount:ObjSTAT, * enemys:enemys,items:items, * battleDamage:ObjSTAT, extraDamage:ObjSTAT, - * poisonDamage:ObjSTAT, vampireExtraLoss:ObjSTAT,}} STAT + * poisonDamage:ObjSTAT, vampireExtraLoss:ObjSTAT, + * }} STAT */ /** * @param {string[]} floorList 要统计的楼层的列表 @@ -9926,9 +10161,21 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = 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; - }); + for (let key in damagePerFloor[floorId]) { + if (["battleDamage", "extraDamage", "poisonDamage", "vampireExtraLoss"] + .includes(key) + ) { + stat[key][floorId] = damagePerFloor[floorId]?.[key] || 0; + } + else if (key.includes(",")) { + if (!stat[key]) { + stat[key] = { total: 0 }; + } + const value = damagePerFloor[floorId]?.[key] || 0; + stat[key][floorId] = value; + stat[key]["total"] += value; + } + } } }); return stat; @@ -9938,8 +10185,10 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = constructor(x, y, w, h) { super(x, y, w, h); this.floorId = ""; - /** @type {number} */this.key; - /** @type {FloorStatisticsMenu} */ this.menu; + /** @type {number} */ + this.key; + /** @type {FloorStatisticsMenu} */ + this.menu; } draw() { @@ -9965,23 +10214,32 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } } - class FloorStatisticsMenu extends MenuBase { + function 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; + }) + } + + class FloorStatisticsMenu extends PagedMenu { 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; + const data = getTotalFloorList(); + super(name, toListen, x, y, w, h, zIndex, capacity, data); + this.page = Math.floor(this.totalItemList.indexOf(core.status.floorId) / capacity); + /** 区域选择时的起始楼层*/ + this.areaBorder = ""; + /** 是否处于区域选择模式下 */ + this.areaSelectOn = false; + this.updateCurrItemList(); } updateBtnInfo() { this.btnMap.forEach((btn, key) => { if (btn instanceof FloorBtn) { - btn.disable = (key >= this.floorList.length); - btn.floorId = this.floorList[key]; + btn.disable = (key >= this.currItemList.length); + btn.floorId = this.currItemList[key]; } }); } @@ -9992,63 +10250,74 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = 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; - }) + function getNameById(id, mode) { + if (mode !== "damage") { + return core.material.items[id]?.name || core.material.enemys[id]?.name || + core.getBlockById(id).event.name || ""; } - - 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(); + else { + if (id === "battleDamage") return "战斗伤害"; + else if (id === "extraDamage") return "地图伤害"; + else if (id === "poisonDamage") return "中毒伤害"; + else if (id === "vampireExtraLoss") return "吸血额外伤害"; + else if (id === "customizeDamageRange") return "自定伤害区间"; + else { + return "伤害" + id.replace(",", "-"); // 说明是自定义的伤害区间 } } } - class FloorStatisticsTextMenu extends MenuBase { - constructor(name, toListen, x, y, w, h, zIndex) { - super(name, toListen, x, y, w, h, zIndex); + function getUserConfig() { + /** @type {StatisticsHeaderConfig} */ + const statisticsHeaderConfig = core.getLocalStorage("statisticsHeaderConfig", { + tools: { ignore: [] }, + items: { ignore: [] }, + equips: { ignore: [] }, + enemys: { ignore: [] }, + floors: { ignore: [] }, + damage: { ignore: [], record: [] }, + }); + return statisticsHeaderConfig; + } + this.getUserConfig = getUserConfig; + + class OpenPickHeaderBtn extends ButtonBase { + draw() { + super.draw(); + const ctx = this.ctx; + const { x, y, w } = this; + const [r, offset] = [w / 2, 2]; + core.fillCircle(ctx, x + r, y + r, r, 'lime'); + core.drawLine(ctx, x + offset, y + r, x + 2 * r - offset, y + r, 'white', 2); + core.drawLine(ctx, x + r, y + offset, x + r, y + 2 * r - offset, 'white', 2); + } + } + + class FloorStatisticsTextMenu extends MultiTypePagedMenu { + constructor(name, toListen, x, y, w, h, zIndex, pagesConfig) { + super(name, toListen, x, y, w, h, zIndex, pagesConfig); /** @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"; + /** 未被用户过滤的显示项总表 */ + this.allItemList = null; } - getFloorList() { + updateFloorList() { const floorList = []; for (let floorId in core.markedFloorIds) { if (core.markedFloorIds[floorId]) floorList.push(floorId); } - return floorList; + this.totalItemList["floors"] = floorList; + this.updateCurrItemList("floors"); } - getItemList(count) { + updateItemList(count) { let itemList = null; + const headerConfig = getUserConfig(); const sortFunc = (a, b) => count[b].total - count[a].total; if (this.mode === "tools") { const doorNameList = []; @@ -10059,66 +10328,38 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = else doorNameList.push(i); } itemList = [...doorNameList.sort(sortFunc), ...itemNameList.sort(sortFunc)]; - } - else if (this.mode === "damage") { + } 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; + itemList = [...itemList, ...headerConfig["damage"]["record"]]; + } else itemList = Object.keys(count).filter(item => item !== "total").sort(sortFunc); + this.allItemList = itemList; + const ignoreList = headerConfig[this.mode]["ignore"]; + // @todo 观察一下网站上这个是否会被polyfill + itemList = [...new Set(itemList).difference(new Set(ignoreList))]; + this.totalItemList["items"] = itemList; + this.updateCurrItemList("items"); } - 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(); + updateStat() { + this.stat = getFloorListStat(this.currItemList["floors"]); } /** @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)); + const currItemList = this.currItemList["items"]; + const currFloorList = this.currItemList["floors"]; core.ui.setTextBaseline(ctx, "top"); core.ui.setTextAlign(ctx, "right"); - for (let i = -1, l = curritemList.length; i < l; i++) { + 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; + id = currItemList[i]; + name = getNameById(id, this.mode); + // 当id为自定义的伤害区间时,count[id]可能为空 + 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++) { @@ -10132,8 +10373,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = } 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"); - + core.ui.fillText(ctx, name, 94, 163 + 25 * i, "black", (name.length < 5) ? "12px Verdana" : "8px Verdana"); } } @@ -10141,8 +10381,18 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = 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 { + 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}` : '') + @@ -10154,15 +10404,23 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = `,护盾${items.mdef}点` + "。"; const text = enemyText + '\n' + itemEffectText; core.ui.drawTextContent(ctx, text, { - maxWidth: 360, color: "black", left: 20, top: 10, + 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); + else if (this.mode === "damage") { + count = { battleDamage, extraDamage, poisonDamage, vampireExtraLoss }; + for (let key in stat) { + if (key.includes(",")) count[key] = stat[key]; + } + }; + this.updateItemList(count); // 更新物品总表 this.drawTable(ctx, count); super.drawContent(); } @@ -10184,7 +10442,11 @@ ${statistics["vampireExtraLoss"] > 0 ? `吸血怪物累计对你造成超出最 ${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 + left: 30, + top: 20, + color: "black", + maxWidth: 360, + fontSize: 14 }); } } @@ -10198,19 +10460,219 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form } } - function StatisticsMenuFactory() { - const ROW = 3, COLUMN = 4; + class HeaderBtn extends ButtonBase { + constructor(x, y, w, h) { + super(x, y, w, h); + /** @type {string} */ + this.text; + /** @type {boolean} */ + this.isIgnored; + } + + draw() { + const { x, y, w, h, ctx, text, isIgnored } = this; + const mode = this.menu.mode; + core.strokeRect(ctx, x, y, w, h, "black", 2); + core.setTextAlign(ctx, "left"); + core.setTextBaseline(ctx, "middle"); + const name = getNameById(text, mode); + const font = (name.length < 5) ? "12px Verdana" : "8px Verdana"; + if (mode !== "damage") { + core.ui.drawIcon(ctx, text, x + 2, y + 10, 12, 12); + core.fillText(ctx, name, x + 15, y + h / 2, "black", font); + } + else { + core.fillText(ctx, name, x + 5, y + h / 2, "black", font); + } + if (isIgnored) { + core.drawLine(ctx, x + 5, y + h / 2, x + w - 2, y + h / 2, "red", 2); + } + } + } + + /** + * 调整统计数据显示项目的菜单 + * @typedef {'tools'|'items'|'equips'|'enemys'|'damage'} StatisticsMode + * @typedef {{tools:{ignore:string[]},items:{ignore:string[]}, + * equips:{ignore:string[]},enemys:{ignore:string[]}, + * damage:{ignore:string[],record:string[]}, + * }} StatisticsHeaderConfig + **/ + class PickHeaderMenu extends PagedMenu { + /** @param {StatisticsMode} mode */ + constructor(name, toListen, x, y, w, h, zIndex, capacity, data, mode) { + super(name, toListen, x, y, w, h, zIndex, capacity, data); + /** @type {StatisticsMode} */ + this.mode = mode; + /** @type {StatisticsHeaderConfig} */ + this.userConfig = getUserConfig(); + this.totalItemList.push("customizeDamageRange"); + this.updateCurrItemList(); + } + + addIgnore(ele) { + this.userConfig[this.mode].ignore.push(ele); + } + + removeIgnore(ele) { + const mode = this.mode; + this.userConfig[mode].ignore = this.userConfig[mode].ignore + .filter(e => e !== ele); + } + + addRecord(ele) { + if (this.mode !== "damage") return; + core.utils.myprompt('输入要记录的伤害范围。格式:0,100 记录伤害0(含)-100(不含)的怪物。第二个数字不填代表无限大', '', (value) => { + if (value == null) { + core.drawFailTip('错误:不合法的输入!'); + return; + } + let min, max; + if (!value.includes(",")) { + min = Number(value); + if (Number.isNaN(min)) { + core.drawFailTip('错误:不合法的输入!'); + return; + } + value += ","; + } + else if (value.endsWith(",")) { + min = Number(value.replace(",", "")); + if (Number.isNaN(min)) { + core.drawFailTip('错误:不合法的输入!'); + return; + } + } + else { + const [minStr, maxStr] = value.split(","); + min = Number(minStr); + max = Number(maxStr); + if (Number.isNaN(min) || Number.isNaN(max) || max < min) { + core.drawFailTip('错误:不合法的输入!'); + return; + } + } + this.userConfig["damage"].record.push(value); + core.setLocalStorage("statisticsHeaderConfig", this.userConfig); + this.totalItemList.push(value); + this.updateCurrItemList(); + this.drawContent(); + }); + } + + removeRecord(ele) { + if (this.mode !== "damage") return; + this.userConfig["damage"].record = this.userConfig["damage"].record + .filter(e => e !== ele); + this.totalItemList = this.totalItemList.filter(e => e !== ele); + this.updateCurrItemList(); + } + + drawContent() { + const { x, y, w, h, zIndex, name } = this; + const ctx = core.createCanvas(name, x, y, w, h, zIndex); + core.ui.fillRect(ctx, 0, 0, w, h, '#A8CABA'); + core.ui.strokeRect(ctx, 0, 0, w, h, 'black', 2); + core.ui.fillText(ctx, '以下是本菜单的表头,你可以隐藏或重新显', 20, 20, "black", "16px Vedana"); + core.ui.fillText(ctx, '示某些项目', 20, 40, "black", "16px Vedana"); + + core.ui.fillText(ctx, "显示项", 20, 60, "black", "Bold 16px Vedana"); + + this.btnMap.forEach((btn, index) => { + if (!(btn instanceof HeaderBtn)) return; + index = Number(index); + if (index < this.capacity) { + btn.disable = index >= this.currItemList.length; + if (index < this.currItemList.length) { + const text = this.currItemList[index]; + btn.text = text; + btn.isIgnored = this.userConfig[this.mode]["ignore"].includes(text); + } + } + }); + super.drawContent(); + } + } + + function pickHeaderMenuFactory(data, mode, exitEvent) { + const ROW = 9, + COLUMN = 4; + const pickHeaderMenu = new PickHeaderMenu("pickHeader", ["ondown"], + 30, 30, PX - 60, PX - 60, 133, ROW * COLUMN, data, mode); + const headerPageDownBtn = new ArrowBtn(75, 48, 16, 16, "left"), + headerPageUpBtn = new ArrowBtn(95, 48, 16, 16, "right"); + const exitBtn = new ExitBtn(328, 5, 20, 20); + const exit = () => { + pickHeaderMenu.clear(); + unregisterResize(pickHeaderMenu); + if (exitEvent) exitEvent(); + } + const btnList = [ + ["pageDown", headerPageDownBtn, () => pickHeaderMenu.pageDown()], + ["pageUp", headerPageUpBtn, () => pickHeaderMenu.pageUp()], + ["exit", exitBtn, exit], + ]; + + function btnClickEvent(btn) { + return function () { + const eleName = this.text; + if (mode === "damage") { + if (eleName === "customizeDamageRange") { + this.menu.addRecord(eleName); + } + else if (eleName.includes(",")) { + this.menu.removeRecord(eleName); + } + else { + if (this.isIgnored) this.menu.removeIgnore(eleName); + else this.menu.addIgnore(eleName); + } + } + else { + if (this.isIgnored) this.menu.removeIgnore(eleName); + else this.menu.addIgnore(eleName); + } + core.setLocalStorage("statisticsHeaderConfig", this.menu.userConfig); + this.menu.drawContent(); + }.bind(btn); + } + + for (let i = 0; i < ROW; i++) { + for (let j = 0; j < COLUMN; j++) { + const btn = new HeaderBtn(20 + 80 * j, 70 + i * 30, 80, 30); + btnList.push([i * COLUMN + j, btn, btnClickEvent(btn)]); + } + } + pickHeaderMenu.registerBtns(btnList); + return pickHeaderMenu; + } + + 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); + 0, 50, PX, PX - 50, 132, { + 'items': { + capacity: 7, + totalItemList: [], + pageDownBtnKey: "itemsPageDown", + pageUpBtnKey: "itemsPageUp" + }, + 'floors': { + capacity: 5, + totalItemList: [], + pageDownBtnKey: "floorsPageDown", + pageUpBtnKey: "floorsPageUp", + } + }); 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} */ @@ -10218,31 +10680,34 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form if (menu.areaSelectOn) { if (!menu.areaBorder) menu.areaBorder = this.floorId; else { - let index1 = menu.totalFloorList.indexOf(menu.areaBorder), - index2 = menu.totalFloorList.indexOf(this.floorId); + let index1 = menu.totalItemList.indexOf(menu.areaBorder), + index2 = menu.totalItemList.indexOf(this.floorId); if (index2 < index1) [index1, index2] = [index2, index1]; - menu.totalFloorList.slice(index1, index2 + 1).forEach(floorId => { + menu.totalItemList.slice(index1, index2 + 1).forEach(floorId => { core.markedFloorIds[floorId] = true; }); menu.areaSelectOn = false; menu.areaBorder = ""; } - } - else { + } else { core.markedFloorIds[this.floorId] = !core.markedFloorIds[this.floorId]; } menu.drawContent(); }; + + const pageDownBtn = new ArrowBtn(10, 145, 16, 16, "left"), + pageUpBtn = new ArrowBtn(390, 145, 16, 16, "right"); + const floorStatisticsList = [ + ["pageDown", pageDownBtn, () => floorStatisticsMenu.pageDown()], + ["pageUp", pageUpBtn, () => floorStatisticsMenu.pageUp()], + ]; + 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, "全不选"); @@ -10250,19 +10715,19 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form const getOutcomeBtn = new RoundBtn(306, 320, 76, 20, "统计所选"); floorStatisticsList.push( ["checkAll", checkAllBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { + floorStatisticsMenu.totalItemList.forEach(floorId => { core.markedFloorIds[floorId] = true; }); floorStatisticsMenu.drawContent(); }], ["inverse", inverseBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { + floorStatisticsMenu.totalItemList.forEach(floorId => { core.markedFloorIds[floorId] = !core.markedFloorIds[floorId]; }); floorStatisticsMenu.drawContent(); }], ["clearSelect", clearSelectBtn, () => { - floorStatisticsMenu.totalFloorList.forEach(floorId => { + floorStatisticsMenu.totalItemList.forEach(floorId => { core.markedFloorIds[floorId] = false; }); floorStatisticsMenu.drawContent(); @@ -10271,25 +10736,21 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form if (!floorStatisticsMenu.areaSelectOn) { floorStatisticsMenu.areaSelectOn = true; core.drawSuccessTip("请依次选择目标区域的起始楼层和终点楼层"); - } - else { + } 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(); + floorStatisticsTextMenu.updateFloorList(); + floorStatisticsTextMenu.updateStat(); statisticsMenu.changePage(1); }]); floorStatisticsMenu.registerBtns(floorStatisticsList); const config = { font: "14px Verdana", selectedFillStyle: "#5B8DEF" }; + const pickHeaderBtn = new OpenPickHeaderBtn(25, 132, 20, 20); 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); @@ -10300,25 +10761,38 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form const floorDownBtn = new ArrowBtn(360, 335, 16, 16, "left"); const floorUpBtn = new ArrowBtn(380, 335, 16, 16, "right"); + /** @this {FloorStatisticsTextMenu} */ + const openPickHeader = function () { + statisticsMenu.endListen(); + this.endListen(); + const pickHeaderMenu = pickHeaderMenuFactory(this.allItemList, this.mode, function () { + statisticsMenu.beginListen(); + floorStatisticsTextMenu.beginListen(); + floorStatisticsTextMenu.drawContent(); + }); + registerResize(pickHeaderMenu); + pickHeaderMenu.init(); + }.bind(floorStatisticsTextMenu); const changeMode = (btn, mode) => () => { floorStatisticsTextMenu.btnMap.forEach(ele => { ele.status = (btn === ele) ? "selected" : "none"; }); floorStatisticsTextMenu.mode = mode; - floorStatisticsTextMenu.itemPage = 0; + floorStatisticsTextMenu.page["items"] = 0; floorStatisticsTextMenu.drawContent(); }; floorStatisticsTextMenu.registerBtns([ + ["pickHeader", pickHeaderBtn, openPickHeader], ["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()], + ["itemsPageDown", itemDownBtn, () => floorStatisticsTextMenu.pageDown("items")], + ["itemsPageUp", itemUpBtn, () => floorStatisticsTextMenu.pageUp("items")], + ["floorsPageDown", floorDownBtn, () => floorStatisticsTextMenu.pageDown("floors")], + ["floorsPageUp", floorUpBtn, () => floorStatisticsTextMenu.pageUp("floors")], ]); toolStatBtn.status = "selected"; @@ -10375,7 +10849,7 @@ ${statistics.hasOwnProperty("oneDefEffect") ? `1防御累计减伤为${core.form function _drawStatistics() { core.ui.closePanel(); core.control.lockControl(); - const staticsMenu = StatisticsMenuFactory(); + const staticsMenu = statisticsMenuFactory(); staticsMenu.init(); staticsMenu.initOnePage(0); } diff --git a/runtime.d.ts b/runtime.d.ts index 0dbcee38..7b1b790e 100644 --- a/runtime.d.ts +++ b/runtime.d.ts @@ -2753,7 +2753,7 @@ interface ui { setStrokeStyle(name: string | CanvasRenderingContext2D, style: string): void /** 设置某个canvas的对齐 */ - setTextAlign(name: string | CanvasRenderingContext2D, align: string): void + setTextAlign(name: string | CanvasRenderingContext2D, align: "left" | "right" | "center" | "start" | "end"): void /** 计算某段文字的宽度 */ calWidth(name: string | CanvasRenderingContext2D, text: string, font?: string): number @@ -3184,17 +3184,22 @@ interface plugin { 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 + }; + registerResize: (menu: MenuBase) => void; + unregisterResize: (menu: MenuBase) => void; + ButtonBase: typeof ButtonBase; + RoundBtn: typeof RoundBtn; + IconBtn: typeof IconBtn; + ExitBtn: typeof ExitBtn; + ArrowBtn: typeof ArrowBtn; + MenuBase: typeof MenuBase; + PagedMenu: typeof PagedMenu; + MultiTypePagedMenu: typeof MultiTypePagedMenu; + Pagination: typeof Pagination; } /** 设置菜单 */ - settingMenu: MenuBaseClass | undefined + settingMenu: MenuBase | undefined // core.plugin.xxx 可能是任意变量,任意签名和返回值的函数 [x: string]: any @@ -3202,13 +3207,9 @@ interface plugin { /*** 用于绘制的基类 */ /*** 按钮基类 */ -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 { +declare class ButtonBase { constructor(x: number, y: number, w: number, h: number); /** 按钮判定区域左上角的X坐标 */ x: number; @@ -3223,7 +3224,7 @@ declare class ButtonBaseClass { /** 按钮是否处于被选中的状态 */ status: 'selected' | 'none'; /** 按钮所在的菜单 */ - menu: MenuBaseClass + menu: MenuBase /** 按钮所在的画布 */ ctx: string /** 按钮在菜单中的索引 */ @@ -3243,11 +3244,7 @@ declare class ButtonBaseClass { 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 { +declare class RoundBtn extends ButtonBase { constructor(x: number, y: number, w: number, h: number, text: string, config?: any); /** 按钮绘制的文字 */ text: string @@ -3259,11 +3256,7 @@ declare class RoundBtnClass extends ButtonBaseClass { } } -interface IconBtn { - new(x: number, y: number, w: number, h: number, icon: string, config?: any): IconBtnClass; -} - -declare class IconBtnClass extends ButtonBaseClass { +declare class IconBtn extends ButtonBase { constructor(x: number, y: number, w: number, h: number, icon: string, config?: any); /** 按钮绘制的图标名称 */ icon: string @@ -3274,11 +3267,7 @@ declare class IconBtnClass extends ButtonBaseClass { } } -interface ExitBtn { - new(x: number, y: number, w: number, h: number, config?: any): ExitBtnClass; -} - -declare class ExitBtnClass extends ButtonBaseClass { +declare class ExitBtn extends ButtonBase { constructor(x: number, y: number, w: number, h: number, config?: any); /** 按钮的绘制属性配置 */ config: { @@ -3286,17 +3275,99 @@ declare class ExitBtnClass extends ButtonBaseClass { radius?: number, lineOffsetX?: number, lineWidthX?: number, } } +/** + * 箭头按钮的配置选项 + */ +interface ArrowBtnConfig { + /** + * 箭头尖端到所指的边的距离 + * 默认值: 垂直方向为 5, 水平方向为 4 + */ + marginTip?: number; -interface MenuBase { - new(name: string, toListen: eventType[], - x?: number | null, y?: number | null, w?: number | null, h?: number | null, - zIndex?: number | null): MenuBaseClass; + /** + * 箭头两侧到各自最接近的边的距离 + * 默认值: 5 + */ + marginSide?: number; + + /** + * 箭头尾部到最接近的边的距离 + * 默认值: 垂直方向为 5, 水平方向为 6 + */ + marginTail?: number; + + /** + * 背景圆角矩形的填充样式 (颜色字符串或渐变对象) + * 默认值: 'gray' + */ + backStyle?: string | CanvasGradient | CanvasPattern; + + /** + * 箭头三角形的填充样式 (颜色字符串或渐变对象) + * 默认值: 'black' + */ + arrowStyle?: string | CanvasGradient | CanvasPattern; +} + +/** + * 箭头方向类型 + */ +type ArrowDirection = 'left' | 'right' | 'up' | 'down'; + +/** + * 箭头按钮类 + * 继承自 ButtonBase,绘制带有方向指示箭头的按钮 + */ +declare class ArrowBtn extends ButtonBase { + /** + * 【Public】按钮配置对象 + * 包含边距、颜色等绘制参数 + */ + public config: ArrowBtnConfig; + + /** + * 【Public】箭头指向方向 + * 可选值: 'left', 'right', 'up', 'down' + */ + public dir: ArrowDirection; + + /** + * 【Public】透明度 (0.0 - 1.0) + * 用于控制绘制时的整体透明度 + */ + public alpha: number; + + /** + * 构造函数 + * @param x - X坐标 + * @param y - Y坐标 + * @param w - 宽度 + * @param h - 高度 + * @param dir - 箭头方向 ('left'|'right'|'up'|'down') + * @param config - 可选,配置对象。如果不传则使用默认值 + */ + constructor( + x: number, + y: number, + w: number, + h: number, + dir: ArrowDirection, + config?: ArrowBtnConfig + ); + + /** + * 绘制按钮 + * 绘制背景圆角矩形和指定方向的三角形箭头 + * 会使用 core.fillRoundRect 和 core.fillPolygon 进行绘制 + */ + draw(): void; } type eventType = 'ondown' | 'onmove' | 'onup' | 'keyDown' | 'keyUp' | 'onmousewheel'; /*** 菜单基类 */ -declare class MenuBaseClass { +declare class MenuBase { /** * @param name 该菜单使用的画布的名称 */ @@ -3318,7 +3389,7 @@ declare class MenuBaseClass { /** 该画布是否正在被绘制 */ onDraw: boolean /** 该菜单的按钮列表 */ - btnMap: Map + btnMap: Map ondown(x: number, y: number, rawpx: number, rawpy: number): void /** @@ -3362,10 +3433,10 @@ declare class MenuBaseClass { /** 注册一个按钮的事件 */ - registerBtn(key: string | number, button: ButtonBaseClass, + registerBtn(key: string | number, button: ButtonBase, event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }): void /** 注册若干个按钮的事件 */ - registerBtns(btns: [key: string | number, button: ButtonBaseClass, + registerBtns(btns: [key: string | number, button: ButtonBase, event?: posFunc | { ondown: posFunc, onmove?: posFunc, onup?: posFunc }][]): void /** 创建并返回本菜单的画布 */ @@ -3386,12 +3457,204 @@ declare class MenuBaseClass { 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 PagedMenu extends MenuBase { + /** + * 【Public】当前页码索引 (从 0 开始) + * 外部可直接读写,例如:menu.page = 2; + */ + public page: number; + + /** + * 【Public】完整的数据列表 + * 外部可直接访问或替换整个列表 + */ + public totalItemList: T[]; + + /** + * 【Public】当前页切片后的数据列表 + * 由 updateCurrItemList 方法计算得出 + */ + public currItemList: T[]; + + /** + * 【Public】每页容量 (每页显示的最大条目数) + */ + public capacity: number; + + /** + * 构造函数 + * @param name - 菜单名称 + * @param toListen - 要监听的事件集合 + * @param x - X坐标 + * @param y - Y坐标 + * @param w - 宽度 + * @param h - 高度 + * @param zIndex - 层级 + * @param capacity - 每页容量 + * @param data - 初始完整数据列表 + */ + constructor( + name: string, + toListen: any, + x: number, + y: number, + w: number, + h: number, + zIndex: number, + capacity: number, + data: T[] + ); + + /** + * 绘制内容 + * 重写此方法以更新 "pageDown" 和 "pageUp" 按钮的透明度状态 + */ + drawContent(): void; + + /** + * 更新当前页的数据切片 (currItemList) + * + * ⚠️ 注意:此类初始化后,必须手动调用一次此方法以填充 currItemList。 + * 当修改了 page 或 totalItemList 后,也需要重新调用此方法。 + */ + updateCurrItemList(): void; + + /** + * 检查是否可以向下翻页 (回到上一页) + * @returns 如果当前页码 > 0 则返回 true + */ + canPageDown(): boolean; + + /** + * 执行向下翻页 (页码 -1) + * 如果无法翻页则不执行任何操作。 + * 翻页成功后会自动调用 updateCurrItemList 和 drawContent。 + */ + pageDown(): void; + + /** + * 检查是否可以向上翻页 (进入下一页) + * @returns 如果还有下一页数据则返回 true + */ + canPageUp(): boolean; + + /** + * 执行向上翻页 (页码 +1) + * 如果无法翻页则不执行任何操作。 + * 翻页成功后会自动调用 updateCurrItemList 和 drawContent。 + */ + pageUp(): void; } -declare class PaginationClass extends MenuBaseClass { +declare class MultiTypePagedMenu extends MenuBase { + /** + * 内部状态:当前页码索引 + * 格式: { type: currentPageIndex } + */ + public page: Record; + + /** + * 内部状态:完整数据列表 + * 格式: { type: fullData[] } + */ + public totalItemList: Record; + + /** + * 内部状态:当前页切片数据 + * 格式: { type: currentSlice[] } + */ + public currItemList: Record; + + /** + * 内部状态:容量配置 + * 格式: { type: capacityNum } + */ + public capacityConfig: Record; + + /** + * 内部状态:按钮键名映射 + * 格式: { type: { down: key, up: key } } + */ + public btnKeyMap: Record; + + /** + * 构造函数 + * @param name - 菜单名称 + * @param toListen - 要监听的事件集合 + * @param x - X坐标 + * @param y - Y坐标 + * @param w - 宽度 + * @param h - 高度 + * @param zIndex - 层级 + * @param pagesConfig - 分页配置对象 + */ + constructor( + name: string, + toListen: any, + x: number, + y: number, + w: number, + h: number, + zIndex: number, + pagesConfig: PagesConfig + ); + + /** + * 绘制内容 + * 重写此方法以更新翻页按钮的透明度状态 + */ + drawContent(): void; + + /** + * 更新指定类型(或所有类型)的当前页数据切片 + * @param pageType - 可选,指定要更新的类型。如果不传则更新所有类型 + */ + updateCurrItemList(pageType?: string): void; + + /** + * 检查指定类型是否可以向下翻页 (回到上一页) + * @param pageType - 页面类型 + * @returns 如果当前页码大于0则返回 true + */ + canPageDown(pageType: string): boolean; + + /** + * 执行向下翻页 (页码 -1) + * 如果无法翻页则不执行任何操作 + * @param pageType - 页面类型 + */ + pageDown(pageType: string): void; + + /** + * 检查指定类型是否可以向上翻页 (进入下一页) + * @param pageType - 页面类型 + * @returns 如果还有下一页数据则返回 true + */ + canPageUp(pageType: string): boolean; + + /** + * 执行向上翻页 (页码 +1) + * 如果无法翻页则不执行任何操作 + * @param pageType - 页面类型 + */ + pageUp(pageType: string): void; + + /** + * 获取指定类型的当前页码 (从0开始) + * @param pageType - 页面类型 + * @returns 当前页码索引 + */ + getCurrentPage(pageType: string): number; + + /** + * 获取指定类型的总页数 + * @param pageType - 页面类型 + * @returns 总页数 + */ + getTotalPages(pageType: string): number; +} + +declare class Pagination extends MenuBase { constructor(pageList: MenuBase[], currPage: number, name: string, toListen: eventType[], x?: number, y?: number, w?: number, h?: number, zIndex?: number); /** 页面列表 */