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);
/** 页面列表 */