9711 lines
326 KiB
JavaScript
9711 lines
326 KiB
JavaScript
// @ts-check
|
||
/// <reference path="../runtime.d.ts" />
|
||
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
|
||
{
|
||
"init": function () {
|
||
|
||
console.log("插件编写测试");
|
||
|
||
// 可以写一些直接执行的代码
|
||
// 在这里写的代码将会在【资源加载前】被执行,此时图片等资源尚未被加载。
|
||
// 请勿在这里对包括bgm,图片等资源进行操作。
|
||
|
||
|
||
this._afterLoadResources = function () {
|
||
// 本函数将在所有资源加载完毕后,游戏开启前被执行
|
||
// 可以在这个函数里面对资源进行一些操作。
|
||
// 若需要进行切分图片,可以使用 core.splitImage() 函数,或直接在全塔属性-图片切分中操作
|
||
}
|
||
|
||
// 可以在任何地方(如afterXXX或自定义脚本事件)调用函数,方法为 core.plugin.xxx();
|
||
// 从V2.6开始,插件中用this.XXX方式定义的函数也会被转发到core中,详见文档-脚本-函数的转发。
|
||
},
|
||
"shop": function () {
|
||
// 【全局商店】相关的功能
|
||
//
|
||
// 打开一个全局商店
|
||
// shopId:要打开的商店id;noRoute:是否不计入录像
|
||
this.openShop = function (shopId, noRoute) {
|
||
var shop = core.status.shops[shopId];
|
||
// Step 1: 检查能否打开此商店
|
||
if (!this.canOpenShop(shopId)) {
|
||
core.drawTip("该商店尚未开启");
|
||
return false;
|
||
}
|
||
|
||
// Step 2: (如有必要)记录打开商店的脚本事件
|
||
if (!noRoute) {
|
||
core.status.route.push("shop:" + shopId);
|
||
}
|
||
|
||
// Step 3: 检查道具商店 or 公共事件
|
||
if (shop.item) {
|
||
if (core.openItemShop) {
|
||
core.openItemShop(shopId);
|
||
} else {
|
||
core.playSound('操作失败');
|
||
core.insertAction("道具商店插件不存在!请检查是否存在该插件!");
|
||
}
|
||
return;
|
||
}
|
||
if (shop.commonEvent) {
|
||
core.insertCommonEvent(shop.commonEvent, shop.args);
|
||
return;
|
||
}
|
||
|
||
_shouldProcessKeyUp = true;
|
||
|
||
// Step 4: 执行标准公共商店
|
||
core.insertAction(this._convertShop(shop));
|
||
return true;
|
||
}
|
||
|
||
////// 将一个全局商店转变成可预览的公共事件 //////
|
||
this._convertShop = function (shop) {
|
||
return [
|
||
{ "type": "function", "function": "function() {core.addFlag('@temp@shop', 1);}" },
|
||
{
|
||
"type": "while",
|
||
"condition": "true",
|
||
"data": [
|
||
// 检测能否访问该商店
|
||
{
|
||
"type": "if",
|
||
"condition": "core.isShopVisited('" + shop.id + "')",
|
||
"true": [
|
||
// 可以访问,直接插入执行效果
|
||
{ "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', false) }" },
|
||
],
|
||
"false": [
|
||
// 不能访问的情况下:检测能否预览
|
||
{
|
||
"type": "if",
|
||
"condition": shop.disablePreview,
|
||
"true": [
|
||
// 不可预览,提示并退出
|
||
{ "type": "playSound", "name": "操作失败" },
|
||
"当前无法访问该商店!",
|
||
{ "type": "break" },
|
||
],
|
||
"false": [
|
||
// 可以预览:将商店全部内容进行替换
|
||
{ "type": "tip", "text": "当前处于预览模式,不可购买" },
|
||
{ "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', true) }" },
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{ "type": "function", "function": "function() {core.addFlag('@temp@shop', -1);}" }
|
||
];
|
||
}
|
||
|
||
this._convertShop_replaceChoices = function (shopId, previewMode) {
|
||
var shop = core.status.shops[shopId];
|
||
var choices = (shop.choices || []).filter(function (choice) {
|
||
if (choice.condition == null || choice.condition == '') return true;
|
||
try { return core.calValue(choice.condition); } catch (e) { return true; }
|
||
}).map(function (choice) {
|
||
var ableToBuy = core.calValue(choice.need);
|
||
return {
|
||
"text": choice.text,
|
||
"icon": choice.icon,
|
||
"color": ableToBuy && !previewMode ? choice.color : [153, 153, 153, 1],
|
||
"action": ableToBuy && !previewMode ? [{ "type": "playSound", "name": "商店" }].concat(choice.action) : [
|
||
{ "type": "playSound", "name": "操作失败" },
|
||
{ "type": "tip", "text": previewMode ? "预览模式下不可购买" : "购买条件不足" }
|
||
]
|
||
};
|
||
}).concat({ "text": "离开", "action": [{ "type": "playSound", "name": "取消" }, { "type": "break" }] });
|
||
core.insertAction({ "type": "choices", "text": shop.text, "choices": choices });
|
||
}
|
||
|
||
/// 是否访问过某个快捷商店
|
||
this.isShopVisited = function (id) {
|
||
if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {});
|
||
var shops = core.getFlag("__shops__");
|
||
if (!shops[id]) shops[id] = {};
|
||
return shops[id].visited;
|
||
}
|
||
|
||
/// 当前应当显示的快捷商店列表
|
||
this.listShopIds = function () {
|
||
return Object.keys(core.status.shops).filter(function (id) {
|
||
return core.isShopVisited(id) || !core.status.shops[id].mustEnable;
|
||
});
|
||
}
|
||
|
||
/// 是否能够打开某个商店
|
||
this.canOpenShop = function (id) {
|
||
if (this.isShopVisited(id)) return true;
|
||
var shop = core.status.shops[id];
|
||
if (shop.item || shop.commonEvent || shop.mustEnable) return false;
|
||
return true;
|
||
}
|
||
|
||
/// 启用或禁用某个快捷商店
|
||
this.setShopVisited = function (id, visited) {
|
||
if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {});
|
||
var shops = core.getFlag("__shops__");
|
||
if (!shops[id]) shops[id] = {};
|
||
if (visited) shops[id].visited = true;
|
||
else delete shops[id].visited;
|
||
}
|
||
|
||
/// 能否使用快捷商店
|
||
this.canUseQuickShop = function (id) {
|
||
// 如果返回一个字符串,表示不能,字符串为不能使用的提示
|
||
// 返回null代表可以使用
|
||
|
||
// 检查当前楼层的canUseQuickShop选项是否为false
|
||
if (core.status.thisMap.canUseQuickShop === false)
|
||
return '当前楼层不能使用快捷商店。';
|
||
return null;
|
||
}
|
||
|
||
var _shouldProcessKeyUp = true;
|
||
|
||
/// 允许商店X键退出
|
||
core.registerAction('keyUp', 'shops', function (keycode) {
|
||
if (!core.status.lockControl || core.status.event.id != 'action') return false;
|
||
if ((keycode == 13 || keycode == 32) && !_shouldProcessKeyUp) {
|
||
_shouldProcessKeyUp = true;
|
||
return true;
|
||
}
|
||
|
||
if (!core.hasFlag("@temp@shop") || core.status.event.data.type != 'choices') return false;
|
||
var data = core.status.event.data.current;
|
||
var choices = data.choices;
|
||
var topIndex = core.actions._getChoicesTopIndex(choices.length);
|
||
if (keycode == 88 || keycode == 27) { // X, ESC
|
||
core.actions._clickAction(core.actions.HSIZE, topIndex + choices.length - 1);
|
||
return true;
|
||
}
|
||
return false;
|
||
}, 60);
|
||
|
||
/// 允许长按空格或回车连续执行操作
|
||
core.registerAction('keyDown', 'shops', function (keycode) {
|
||
if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false;
|
||
if (core.status.event.data.type != 'choices') return false;
|
||
core.status.onShopLongDown = true;
|
||
var data = core.status.event.data.current;
|
||
var choices = data.choices;
|
||
var topIndex = core.actions._getChoicesTopIndex(choices.length);
|
||
if (keycode == 13 || keycode == 32) { // Space, Enter
|
||
core.actions._clickAction(core.actions.HSIZE, topIndex + core.status.event.selection);
|
||
_shouldProcessKeyUp = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
}, 60);
|
||
|
||
// 允许长按屏幕连续执行操作
|
||
core.registerAction('longClick', 'shops', function (x, y, px, py) {
|
||
if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false;
|
||
if (core.status.event.data.type != 'choices') return false;
|
||
var data = core.status.event.data.current;
|
||
var choices = data.choices;
|
||
var topIndex = core.actions._getChoicesTopIndex(choices.length);
|
||
if (x >= core.actions.CHOICES_LEFT && x <= core.actions.CHOICES_RIGHT && y >= topIndex && y < topIndex + choices.length) {
|
||
core.actions._clickAction(x, y);
|
||
return true;
|
||
}
|
||
return false;
|
||
}, 60);
|
||
},
|
||
"removeMap": function () {
|
||
// 高层塔砍层插件,删除后不会存入存档,不可浏览地图也不可飞到。
|
||
// 推荐用法:
|
||
// 对于超高层或分区域塔,当在1区时将2区以后的地图删除;1区结束时恢复2区,进二区时删除1区地图,以此类推
|
||
// 这样可以大幅减少存档空间,以及加快存读档速度
|
||
|
||
// 删除楼层
|
||
// core.removeMaps("MT1", "MT300") 删除MT1~MT300之间的全部层
|
||
// core.removeMaps("MT10") 只删除MT10层
|
||
this.removeMaps = function (fromId, toId) {
|
||
toId = toId || fromId;
|
||
var fromIndex = core.floorIds.indexOf(fromId),
|
||
toIndex = core.floorIds.indexOf(toId);
|
||
if (toIndex < 0) toIndex = core.floorIds.length - 1;
|
||
flags.__visited__ = flags.__visited__ || {};
|
||
flags.__removed__ = flags.__removed__ || [];
|
||
flags.__disabled__ = flags.__disabled__ || {};
|
||
flags.__leaveLoc__ = flags.__leaveLoc__ || {};
|
||
for (var i = fromIndex; i <= toIndex; ++i) {
|
||
var floorId = core.floorIds[i];
|
||
if (core.status.maps[floorId].deleted) continue;
|
||
delete flags.__visited__[floorId];
|
||
flags.__removed__.push(floorId);
|
||
delete flags.__disabled__[floorId];
|
||
delete flags.__leaveLoc__[floorId];
|
||
(core.status.autoEvents || []).forEach(function (event) {
|
||
if (event.floorId == floorId && event.currentFloor) {
|
||
core.autoEventExecuting(event.symbol, false);
|
||
core.autoEventExecuted(event.symbol, false);
|
||
}
|
||
});
|
||
core.status.maps[floorId].deleted = true;
|
||
core.status.maps[floorId].canFlyTo = false;
|
||
core.status.maps[floorId].canFlyFrom = false;
|
||
core.status.maps[floorId].cannotViewMap = true;
|
||
}
|
||
}
|
||
|
||
// 恢复楼层
|
||
// core.resumeMaps("MT1", "MT300") 恢复MT1~MT300之间的全部层
|
||
// core.resumeMaps("MT10") 只恢复MT10层
|
||
this.resumeMaps = function (fromId, toId) {
|
||
toId = toId || fromId;
|
||
var fromIndex = core.floorIds.indexOf(fromId),
|
||
toIndex = core.floorIds.indexOf(toId);
|
||
if (toIndex < 0) toIndex = core.floorIds.length - 1;
|
||
flags.__removed__ = flags.__removed__ || [];
|
||
for (var i = fromIndex; i <= toIndex; ++i) {
|
||
var floorId = core.floorIds[i];
|
||
if (!core.status.maps[floorId].deleted) continue;
|
||
flags.__removed__ = flags.__removed__.filter(function (f) { return f != floorId; });
|
||
core.status.maps[floorId] = core.loadFloor(floorId);
|
||
}
|
||
}
|
||
|
||
// 分区砍层相关
|
||
var inAnyPartition = function (floorId) {
|
||
var inPartition = false;
|
||
(core.floorPartitions || []).forEach(function (floor) {
|
||
var fromIndex = core.floorIds.indexOf(floor[0]);
|
||
var toIndex = core.floorIds.indexOf(floor[1]);
|
||
var index = core.floorIds.indexOf(floorId);
|
||
if (fromIndex < 0 || index < 0) return;
|
||
if (toIndex < 0) toIndex = core.floorIds.length - 1;
|
||
if (index >= fromIndex && index <= toIndex) inPartition = true;
|
||
});
|
||
return inPartition;
|
||
}
|
||
|
||
// 分区砍层
|
||
this.autoRemoveMaps = function (floorId) {
|
||
if (main.mode != 'play' || !inAnyPartition(floorId)) return;
|
||
// 根据分区信息自动砍层与恢复
|
||
(core.floorPartitions || []).forEach(function (floor) {
|
||
var fromIndex = core.floorIds.indexOf(floor[0]);
|
||
var toIndex = core.floorIds.indexOf(floor[1]);
|
||
var index = core.floorIds.indexOf(floorId);
|
||
if (fromIndex < 0 || index < 0) return;
|
||
if (toIndex < 0) toIndex = core.floorIds.length - 1;
|
||
if (index >= fromIndex && index <= toIndex) {
|
||
core.resumeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]);
|
||
} else {
|
||
core.removeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]);
|
||
}
|
||
});
|
||
}
|
||
},
|
||
"fiveLayers": function () {
|
||
// 是否启用五图层(增加背景2层和前景2层) 将__enable置为true即会启用;启用后请保存后刷新编辑器
|
||
// 背景层2将会覆盖背景层 被事件层覆盖 前景层2将会覆盖前景层
|
||
// 另外 请注意加入两个新图层 会让大地图的性能降低一些
|
||
// 插件作者:ad
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
// 创建新图层
|
||
function createCanvas(name, zIndex) {
|
||
if (!name) return;
|
||
const canvas = document.createElement('canvas');
|
||
canvas.id = name;
|
||
canvas.className = 'gameCanvas';
|
||
// 编辑器模式下设置zIndex会导致加入的图层覆盖优先级过高
|
||
if (main.mode != "editor") canvas.style.zIndex = zIndex || 0;
|
||
// 将图层插入进游戏内容
|
||
document.getElementById('gameDraw')?.appendChild(canvas);
|
||
const ctx = canvas.getContext('2d');
|
||
if (ctx) core.canvas[name] = ctx;
|
||
canvas.width = core.__PIXELS__;
|
||
canvas.height = core.__PIXELS__;
|
||
return canvas;
|
||
}
|
||
|
||
const bg2Canvas = createCanvas('bg2', 20);
|
||
const fg2Canvas = createCanvas('fg2', 63);
|
||
// 大地图适配
|
||
core.bigmap.canvas = ["bg2", "fg2", "bg", "event", "event2", "fg", "damage"];
|
||
core.initStatus.bg2maps = {};
|
||
core.initStatus.fg2maps = {};
|
||
|
||
if (main.mode == 'editor') {
|
||
/*插入编辑器的图层 不做此步新增图层无法在编辑器显示*/
|
||
// 编辑器图层覆盖优先级 eui > efg > fg(前景层) > event2(48*32图块的事件层) > event(事件层) > bg(背景层)
|
||
// 背景层2(bg2) 插入事件层(event)之前(即bg与event之间)
|
||
if (bg2Canvas) document.getElementById('mapEdit')?.insertBefore(bg2Canvas, document.getElementById('event'));
|
||
// 前景层2(fg2) 插入编辑器前景(efg)之前(即fg之后)
|
||
if (fg2Canvas) document.getElementById('mapEdit')?.insertBefore(fg2Canvas, document.getElementById('ebm'));
|
||
// 原本有三个图层 从4开始添加
|
||
var num = 4;
|
||
// 新增图层存入editor.dom中
|
||
editor.dom.bg2c = core.canvas.bg2.canvas;
|
||
editor.dom.bg2Ctx = core.canvas.bg2;
|
||
editor.dom.fg2c = core.canvas.fg2.canvas;
|
||
editor.dom.fg2Ctx = core.canvas.fg2;
|
||
editor.dom.maps.push('bg2map', 'fg2map');
|
||
editor.dom.canvas.push('bg2', 'fg2');
|
||
|
||
// 创建编辑器上的按钮
|
||
const createCanvasBtn = function (name) {
|
||
// 电脑端创建按钮
|
||
const input = document.createElement('input');
|
||
// layerMod4/layerMod5
|
||
const id = 'layerMod' + num++;
|
||
// bg2map/fg2map
|
||
const value = name + 'map';
|
||
input.type = 'radio';
|
||
input.name = 'layerMod';
|
||
input.id = id;
|
||
input.value = value;
|
||
editor.dom[id] = input;
|
||
input.onchange = function () {
|
||
editor.uifunctions.setLayerMod(value);
|
||
}
|
||
return input;
|
||
};
|
||
|
||
const createCanvasBtn_mobile = function (name) {
|
||
// 手机端往选择列表中添加子选项
|
||
const input = document.createElement('option');
|
||
const id = 'layerMod' + num++;
|
||
const value = name + 'map';
|
||
input.name = 'layerMod';
|
||
input.value = value;
|
||
editor.dom[id] = input;
|
||
return input;
|
||
};
|
||
if (!editor.isMobile) {
|
||
const input = createCanvasBtn('bg2');
|
||
const input2 = createCanvasBtn('fg2');
|
||
// 获取事件层及其父节点
|
||
const child = document.getElementById('layerMod'),
|
||
parent = child?.parentNode;
|
||
if (parent) {
|
||
// 背景层2插入事件层前
|
||
parent.insertBefore(input, child);
|
||
// 不能直接更改背景层2的innerText 所以创建文本节点
|
||
var txt = document.createTextNode('bg2');
|
||
// 插入事件层前(即新插入的背景层2前)
|
||
parent.insertBefore(txt, child);
|
||
// 向最后插入前景层2(即插入前景层后)
|
||
parent.appendChild(input2);
|
||
var txt2 = document.createTextNode('fg2');
|
||
parent.appendChild(txt2);
|
||
parent.childNodes[2].replaceWith("bg");
|
||
parent.childNodes[6].replaceWith("事件");
|
||
parent.childNodes[8].replaceWith("fg");
|
||
}
|
||
} else {
|
||
const input = createCanvasBtn_mobile('bg2');
|
||
const input2 = createCanvasBtn_mobile('fg2');
|
||
// 手机端因为是选项 所以可以直接改innerText
|
||
input.innerText = '背景层2';
|
||
input2.innerText = '前景层2';
|
||
const parent = document.getElementById('layerMod');
|
||
if (parent) {
|
||
parent.insertBefore(input, parent.children[1]);
|
||
parent.appendChild(input2);
|
||
}
|
||
}
|
||
}
|
||
|
||
var _loadFloor_doNotCopy = core.maps._loadFloor_doNotCopy;
|
||
core.maps._loadFloor_doNotCopy = function () {
|
||
return ["bg2map", "fg2map"].concat(_loadFloor_doNotCopy());
|
||
}
|
||
////// 绘制背景和前景层 //////
|
||
core.maps._drawBg_draw = function (floorId, toDrawCtx, cacheCtx, config) {
|
||
config.ctx = cacheCtx;
|
||
core.maps._drawBg_drawBackground(floorId, config);
|
||
// ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制背景图块;后绘制的覆盖先绘制的。
|
||
core.maps._drawFloorImages(floorId, config.ctx, 'bg', null, null, config.onMap);
|
||
core.maps._drawBgFgMap(floorId, 'bg', config);
|
||
if (config.onMap) {
|
||
core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
|
||
core.clearMap('bg2');
|
||
core.clearMap(cacheCtx);
|
||
}
|
||
core.maps._drawBgFgMap(floorId, 'bg2', config);
|
||
if (config.onMap) core.drawImage('bg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
|
||
config.ctx = toDrawCtx;
|
||
}
|
||
core.maps._drawFg_draw = function (floorId, toDrawCtx, cacheCtx, config) {
|
||
config.ctx = cacheCtx;
|
||
// ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制前景图块;后绘制的覆盖先绘制的。
|
||
core.maps._drawFloorImages(floorId, config.ctx, 'fg', null, null, config.onMap);
|
||
core.maps._drawBgFgMap(floorId, 'fg', config);
|
||
if (config.onMap) {
|
||
core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
|
||
core.clearMap('fg2');
|
||
core.clearMap(cacheCtx);
|
||
}
|
||
core.maps._drawBgFgMap(floorId, 'fg2', config);
|
||
if (config.onMap) core.drawImage('fg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
|
||
config.ctx = toDrawCtx;
|
||
}
|
||
////// 移动判定 //////
|
||
core.maps._generateMovableArray_arrays = function (floorId) {
|
||
return {
|
||
bgArray: this.getBgMapArray(floorId),
|
||
fgArray: this.getFgMapArray(floorId),
|
||
eventArray: this.getMapArray(floorId),
|
||
bg2Array: this._getBgFgMapArray('bg2', floorId),
|
||
fg2Array: this._getBgFgMapArray('fg2', floorId)
|
||
};
|
||
}
|
||
|
||
// @todo 测试五图层插件在此处是否表现正常
|
||
// @todo 五图层配合autotile是否有bug 待测试
|
||
// 楼层贴图绘制
|
||
core.maps._drawFloorImage = function (ctx, name, one, image, currStatus, onMap) {
|
||
var height = image.height;
|
||
var imageName = one.name + (one.reverse || '');
|
||
var width = parseInt((one.w == null ? image.width : one.w) / (one.frame || 1));
|
||
var height = one.h == null ? image.height : one.h;
|
||
var sx = (one.sx || 0) + (currStatus || 0) % (one.frame || 1) * width;
|
||
var sy = one.sy || 0;
|
||
var x = one.x || 0, y = one.y || 0;
|
||
if (onMap && core.bigmap.v2) {
|
||
if (x > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || x + width < 32 * core.bigmap.posX - 32 ||
|
||
y > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || y + height < 32 * core.bigmap.posY - 32) {
|
||
return;
|
||
}
|
||
x -= 32 * core.bigmap.posX;
|
||
y -= 32 * core.bigmap.posY;
|
||
}
|
||
|
||
if (one.canvas != 'auto' && one.canvas != name) return;
|
||
if (one.canvas != 'auto') {
|
||
if (currStatus != null) core.clearMap(ctx, x, y, width, height);
|
||
core.drawImage(ctx, imageName, sx, sy, width, height, x, y, width, height);
|
||
} else {
|
||
if (name === 'bg' || name === 'bg2') {
|
||
if (currStatus != null) core.clearMap(ctx, x, y + height - 32, width, 32);
|
||
core.drawImage(ctx, imageName, sx, sy + height - 32, width, 32, x, y + height - 32, width, 32);
|
||
} else if (name == 'fg' || name === 'fg2') {
|
||
if (currStatus != null) core.clearMap(ctx, x, y, width, height - 32);
|
||
core.drawImage(ctx, imageName, sx, sy, width, height - 32, x, y, width, height - 32);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
"itemShop": function () {
|
||
// 道具商店相关的插件
|
||
// 可在全塔属性-全局商店中使用「道具商店」事件块进行编辑(如果找不到可以在入口方块中找)
|
||
|
||
var shopId = null; // 当前商店ID
|
||
var type = 0; // 当前正在选中的类型,0买入1卖出
|
||
/** 当前正在选中的道具
|
||
* @type {number | null}
|
||
*/
|
||
var selectItem = 0;
|
||
var selectCount = 0; // 当前已经选中的数量
|
||
var page = 0;
|
||
var totalPage = 0;
|
||
var totalMoney = 0;
|
||
var list = [];
|
||
var shopInfo = null; // 商店信息
|
||
var choices = []; // 商店选项
|
||
var use = 'money';
|
||
var useText = '金币';
|
||
|
||
var bigFont = core.ui._buildFont(20, false),
|
||
middleFont = core.ui._buildFont(18, false);
|
||
|
||
this._drawItemShop = function () {
|
||
// 绘制道具商店
|
||
|
||
// Step 1: 背景和固定的几个文字
|
||
core.ui._createUIEvent();
|
||
core.clearMap('uievent');
|
||
core.ui.clearUIEventSelector();
|
||
core.setTextAlign('uievent', 'left');
|
||
core.setTextBaseline('uievent', 'top');
|
||
core.fillRect('uievent', 0, 0, 416, 416, 'black');
|
||
core.drawWindowSkin('winskin.png', 'uievent', 0, 0, 416, 56);
|
||
core.drawWindowSkin('winskin.png', 'uievent', 0, 56, 312, 56);
|
||
core.drawWindowSkin('winskin.png', 'uievent', 0, 112, 312, 304);
|
||
core.drawWindowSkin('winskin.png', 'uievent', 312, 56, 104, 56);
|
||
core.drawWindowSkin('winskin.png', 'uievent', 312, 112, 104, 304);
|
||
core.setFillStyle('uievent', 'white');
|
||
core.setStrokeStyle('uievent', 'white');
|
||
core.fillText("uievent", "购买", 32, 74, 'white', bigFont);
|
||
core.fillText("uievent", "卖出", 132, 74);
|
||
core.fillText("uievent", "离开", 232, 74);
|
||
core.fillText("uievent", "当前" + useText, 324, 66, null, middleFont);
|
||
core.setTextAlign("uievent", "right");
|
||
core.fillText("uievent", core.formatBigNumber(core.status.hero[use]), 405, 89);
|
||
core.setTextAlign("uievent", "left");
|
||
core.ui.drawUIEventSelector(1, "winskin.png", 22 + 100 * type, 66, 60, 33);
|
||
if (selectItem != null) {
|
||
core.setTextAlign('uievent', 'center');
|
||
core.fillText("uievent", type == 0 ? "买入个数" : "卖出个数", 364, 320, null, bigFont);
|
||
core.fillText("uievent", "< " + selectCount + " >", 364, 350);
|
||
core.fillText("uievent", "确定", 364, 380);
|
||
}
|
||
|
||
// Step 2:获得列表并展示
|
||
list = choices.filter(function (one) {
|
||
if (one.condition != null && one.condition != '') {
|
||
try { if (!core.calValue(one.condition)) return false; } catch (e) { }
|
||
}
|
||
return (type == 0 && one.money != null) || (type == 1 && one.sell != null);
|
||
});
|
||
var per_page = 6;
|
||
totalPage = Math.ceil(list.length / per_page);
|
||
page = Math.floor((selectItem || 0) / per_page) + 1;
|
||
|
||
// 绘制分页
|
||
if (totalPage > 1) {
|
||
var half = 156;
|
||
core.setTextAlign('uievent', 'center');
|
||
core.fillText('uievent', page + " / " + totalPage, half, 388, null, middleFont);
|
||
if (page > 1) core.fillText('uievent', '上一页', half - 80, 388);
|
||
if (page < totalPage) core.fillText('uievent', '下一页', half + 80, 388);
|
||
}
|
||
core.setTextAlign('uievent', 'left');
|
||
|
||
// 绘制每一项
|
||
var start = (page - 1) * per_page;
|
||
for (var i = 0; i < per_page; ++i) {
|
||
var curr = start + i;
|
||
if (curr >= list.length) break;
|
||
var item = list[curr];
|
||
core.drawIcon('uievent', item.id, 10, 125 + i * 40);
|
||
core.setTextAlign('uievent', 'left');
|
||
core.fillText('uievent', core.material.items[item.id].name, 50, 132 + i * 40, null, bigFont);
|
||
core.setTextAlign('uievent', 'right');
|
||
core.fillText('uievent', (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)) + useText + "/个", 300, 133 + i * 40, null, middleFont);
|
||
core.setTextAlign("uievent", "left");
|
||
if (curr == selectItem) {
|
||
// 绘制描述,文字自动放缩
|
||
var text = core.material.items[item.id].text || "该道具暂无描述";
|
||
try { text = core.replaceText(text); } catch (e) { }
|
||
for (var fontSize = 20; fontSize >= 8; fontSize -= 2) {
|
||
var config = { left: 10, fontSize: fontSize, maxWidth: 403 };
|
||
var height = core.getTextContentHeight(text, config);
|
||
if (height <= 50) {
|
||
config.top = (56 - height) / 2;
|
||
core.drawTextContent("uievent", text, config);
|
||
break;
|
||
}
|
||
}
|
||
core.ui.drawUIEventSelector(2, "winskin.png", 8, 120 + i * 40, 295, 40);
|
||
if (type == 0 && item.number != null) {
|
||
core.fillText("uievent", "存货", 324, 132, null, bigFont);
|
||
core.setTextAlign("uievent", "right");
|
||
core.fillText("uievent", item.number, 406, 132, null, null, 40);
|
||
} else if (type == 1) {
|
||
core.fillText("uievent", "数量", 324, 132, null, bigFont);
|
||
core.setTextAlign("uievent", "right");
|
||
core.fillText("uievent", core.itemCount(item.id), 406, 132, null, null, 40);
|
||
}
|
||
core.setTextAlign("uievent", "left");
|
||
core.fillText("uievent", "预计" + useText, 324, 250);
|
||
core.setTextAlign("uievent", "right");
|
||
totalMoney = selectCount * (type == 0 ? core.calValue(item.money) : core.calValue(item.sell));
|
||
core.fillText("uievent", core.formatBigNumber(totalMoney), 405, 280);
|
||
|
||
core.setTextAlign("uievent", "left");
|
||
core.fillText("uievent", type == 0 ? "已购次数" : "已卖次数", 324, 170);
|
||
core.setTextAlign("uievent", "right");
|
||
core.fillText("uievent", (type == 0 ? item.money_count : item.sell_count) || 0, 405, 200);
|
||
}
|
||
}
|
||
|
||
core.setTextAlign('uievent', 'left');
|
||
core.setTextBaseline('uievent', 'alphabetic');
|
||
}
|
||
|
||
var _add = function (item, delta) {
|
||
if (item == null) return;
|
||
selectCount = core.clamp(
|
||
selectCount + delta, 0,
|
||
Math.min(type == 0 ? Math.floor(core.status.hero[use] / core.calValue(item.money)) : core.itemCount(item.id),
|
||
type == 0 && item.number != null ? item.number : Number.MAX_SAFE_INTEGER)
|
||
);
|
||
}
|
||
|
||
var _confirm = function (item) {
|
||
if (item == null || selectCount == 0) return;
|
||
if (type == 0) {
|
||
core.status.hero[use] -= totalMoney;
|
||
core.getItem(item.id, selectCount);
|
||
core.stopSound();
|
||
core.playSound('确定');
|
||
if (item.number != null) item.number -= selectCount;
|
||
item.money_count = (item.money_count || 0) + selectCount;
|
||
} else {
|
||
core.status.hero[use] += totalMoney;
|
||
core.removeItem(item.id, selectCount);
|
||
core.playSound('确定');
|
||
core.drawTip("成功卖出" + selectCount + "个" + core.material.items[item.id].name, item.id);
|
||
if (item.number != null) item.number += selectCount;
|
||
item.sell_count = (item.sell_count || 0) + selectCount;
|
||
}
|
||
selectCount = 0;
|
||
}
|
||
|
||
this._performItemShopKeyBoard = function (keycode) {
|
||
var item = list[selectItem] || null;
|
||
// 键盘操作
|
||
switch (keycode) {
|
||
case 38: // up
|
||
if (selectItem == null) break;
|
||
if (selectItem == 0) selectItem = null;
|
||
else selectItem--;
|
||
selectCount = 0;
|
||
break;
|
||
case 37: // left
|
||
if (selectItem == null) {
|
||
if (type > 0) type--;
|
||
break;
|
||
}
|
||
_add(item, -1);
|
||
break;
|
||
case 39: // right
|
||
if (selectItem == null) {
|
||
if (type < 2) type++;
|
||
break;
|
||
}
|
||
_add(item, 1);
|
||
break;
|
||
case 40: // down
|
||
if (selectItem == null) {
|
||
if (list.length > 0) selectItem = 0;
|
||
break;
|
||
}
|
||
if (list.length == 0) break;
|
||
selectItem = Math.min(selectItem + 1, list.length - 1);
|
||
selectCount = 0;
|
||
break;
|
||
case 13:
|
||
case 32: // Enter/Space
|
||
if (selectItem == null) {
|
||
if (type == 2)
|
||
core.insertAction({ "type": "break" });
|
||
else if (list.length > 0)
|
||
selectItem = 0;
|
||
break;
|
||
}
|
||
_confirm(item);
|
||
break;
|
||
case 27: // ESC
|
||
if (selectItem == null) {
|
||
core.insertAction({ "type": "break" });
|
||
break;
|
||
}
|
||
selectItem = null;
|
||
break;
|
||
}
|
||
}
|
||
|
||
this._performItemShopClick = function (px, py) {
|
||
var item = list[selectItem] || null;
|
||
// 鼠标操作
|
||
if (px >= 22 && px <= 82 && py >= 71 && py <= 102) {
|
||
// 买
|
||
if (type != 0) {
|
||
type = 0;
|
||
selectItem = null;
|
||
selectCount = 0;
|
||
}
|
||
return;
|
||
}
|
||
if (px >= 122 && px <= 182 && py >= 71 && py <= 102) {
|
||
// 卖
|
||
if (type != 1) {
|
||
type = 1;
|
||
selectItem = null;
|
||
selectCount = 0;
|
||
}
|
||
return;
|
||
}
|
||
if (px >= 222 && px <= 282 && py >= 71 && py <= 102) // 离开
|
||
return core.insertAction({ "type": "break" });
|
||
// < >
|
||
if (px >= 318 && px <= 341 && py >= 348 && py <= 376)
|
||
return _add(item, -1);
|
||
if (px >= 388 && px <= 416 && py >= 348 && py <= 376)
|
||
return _add(item, 1);
|
||
// 确定
|
||
if (px >= 341 && px <= 387 && py >= 380 && py <= 407)
|
||
return _confirm(item);
|
||
|
||
// 上一页/下一页
|
||
if (px >= 45 && px <= 105 && py >= 388) {
|
||
if (page > 1) {
|
||
selectItem -= 6;
|
||
selectCount = 0;
|
||
}
|
||
return;
|
||
}
|
||
if (px >= 208 && px <= 268 && py >= 388) {
|
||
if (page < totalPage) {
|
||
selectItem = Math.min(selectItem + 6, list.length - 1);
|
||
selectCount = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 实际区域
|
||
if (px >= 9 && px <= 300 && py >= 120 && py < 360) {
|
||
if (list.length == 0) return;
|
||
var index = parseInt((py - 120) / 40);
|
||
var newItem = 6 * (page - 1) + index;
|
||
if (newItem >= list.length) newItem = list.length - 1;
|
||
if (newItem != selectItem) {
|
||
selectItem = newItem;
|
||
selectCount = 0;
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
this._performItemShopAction = function () {
|
||
if (flags.type == 0) return this._performItemShopKeyBoard(flags.keycode);
|
||
else return this._performItemShopClick(flags.px, flags.py);
|
||
}
|
||
|
||
this.openItemShop = function (itemShopId) {
|
||
shopId = itemShopId;
|
||
type = 0;
|
||
page = 0;
|
||
selectItem = null;
|
||
selectCount = 0;
|
||
core.isShopVisited(itemShopId);
|
||
shopInfo = flags.__shops__[shopId];
|
||
if (shopInfo.choices == null) shopInfo.choices = core.clone(core.status.shops[shopId].choices);
|
||
choices = shopInfo.choices;
|
||
use = core.status.shops[shopId].use;
|
||
if (use != 'exp') use = 'money';
|
||
useText = use == 'money' ? '金币' : '经验';
|
||
|
||
core.insertAction([{
|
||
"type": "while",
|
||
"condition": "true",
|
||
"data": [
|
||
{ "type": "function", "function": "function () { core.plugin._drawItemShop(); }" },
|
||
{ "type": "wait" },
|
||
{ "type": "function", "function": "function() { core.plugin._performItemShopAction(); }" }
|
||
]
|
||
},
|
||
{
|
||
"type": "function",
|
||
"function": "function () { core.deleteCanvas('uievent'); core.ui.clearUIEventSelector(); }"
|
||
}
|
||
]);
|
||
}
|
||
|
||
},
|
||
"enemyLevel": function () {
|
||
// 此插件将提供怪物手册中的怪物境界显示
|
||
// 使用此插件需要先给每个怪物定义境界,方法如下:
|
||
// 点击怪物的【配置表格】,找到“【怪物】相关的表格配置”,然后在【名称】仿照增加境界定义:
|
||
/*
|
||
"level": {
|
||
"_leaf": true,
|
||
"_type": "textarea",
|
||
"_string": true,
|
||
"_data": "境界"
|
||
},
|
||
*/
|
||
// 然后保存刷新,可以看到怪物的属性定义中出现了【境界】。再开启本插件即可。
|
||
|
||
// 是否开启本插件,默认禁用;将此改成 true 将启用本插件。
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
// 这里定义每个境界的显示颜色;可以写'red', '#RRGGBB' 或者[r,g,b,a]四元数组
|
||
var levelToColors = {
|
||
"萌新一阶": "red",
|
||
"萌新二阶": "#FF0000",
|
||
"萌新三阶": [255, 0, 0, 1],
|
||
};
|
||
|
||
// 复写 _drawBook_drawName
|
||
var originDrawBook = core.ui._drawBook_drawName;
|
||
core.ui._drawBook_drawName = function (index, enemy, top, left, width) {
|
||
// 如果没有境界,则直接调用原始代码绘制
|
||
if (!enemy.level) return originDrawBook.call(core.ui, index, enemy, top, left, width);
|
||
// 存在境界,则额外进行绘制
|
||
core.setTextAlign('ui', 'center');
|
||
if (enemy.specialText.length == 0) {
|
||
core.fillText('ui', enemy.name, left + width / 2,
|
||
top + 27, '#DDDDDD', this._buildFont(17, true));
|
||
core.fillText('ui', enemy.level, left + width / 2,
|
||
top + 51, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true));
|
||
} else {
|
||
core.fillText('ui', enemy.name, left + width / 2,
|
||
top + 20, '#DDDDDD', this._buildFont(17, true), width);
|
||
switch (enemy.specialText.length) {
|
||
case 1:
|
||
core.fillText('ui', enemy.specialText[0], left + width / 2,
|
||
top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'),
|
||
this._buildFont(14, true), width);
|
||
break;
|
||
case 2:
|
||
// Step 1: 计算字体
|
||
var text = enemy.specialText[0] + " " + enemy.specialText[1];
|
||
core.setFontForMaxWidth('ui', text, width, this._buildFont(14, true));
|
||
// Step 2: 计算总宽度
|
||
var totalWidth = core.calWidth('ui', text);
|
||
var leftWidth = core.calWidth('ui', enemy.specialText[0]);
|
||
var rightWidth = core.calWidth('ui', enemy.specialText[1]);
|
||
// Step 3: 绘制
|
||
core.fillText('ui', enemy.specialText[0], left + (width + leftWidth - totalWidth) / 2,
|
||
top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'));
|
||
core.fillText('ui', enemy.specialText[1], left + (width + totalWidth - rightWidth) / 2,
|
||
top + 38, core.arrayToRGBA((enemy.specialColor || [])[1] || '#FF6A6A'));
|
||
break;
|
||
default:
|
||
core.fillText('ui', '多属性...', left + width / 2,
|
||
top + 38, '#FF6A6A', this._buildFont(14, true), width);
|
||
}
|
||
core.fillText('ui', enemy.level, left + width / 2,
|
||
top + 56, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true));
|
||
}
|
||
}
|
||
|
||
// 也可以复写其他的属性颜色如怪物攻防等,具体参见下面的例子的注释部分
|
||
core.ui._drawBook_drawRow1 = function (index, enemy, top, left, width, position) {
|
||
// 绘制第一行
|
||
core.setTextAlign('ui', 'left');
|
||
var b13 = this._buildFont(13, true),
|
||
f13 = this._buildFont(13, false);
|
||
var col1 = left,
|
||
col2 = left + width * 9 / 25,
|
||
col3 = left + width * 17 / 25;
|
||
core.fillText('ui', '生命', col1, position, '#DDDDDD', f13);
|
||
core.fillText('ui', core.formatBigNumber(enemy.hp || 0), col1 + 30, position, /*'red' */ null, b13);
|
||
core.fillText('ui', '攻击', col2, position, null, f13);
|
||
core.fillText('ui', core.formatBigNumber(enemy.atk || 0), col2 + 30, position, /* '#FF0000' */ null, b13);
|
||
core.fillText('ui', '防御', col3, position, null, f13);
|
||
core.fillText('ui', core.formatBigNumber(enemy.def || 0), col3 + 30, position, /* [255, 0, 0, 1] */ null, b13);
|
||
}
|
||
|
||
|
||
},
|
||
"multiHeros": function () {
|
||
// 多角色插件
|
||
// Step 1: 启用本插件
|
||
// Step 2: 定义每个新的角色各项初始数据(参见下方注释)
|
||
// Step 3: 在游戏中的任何地方都可以调用 `core.changeHero()` 进行切换;也可以 `core.changeHero(1)` 来切换到某个具体的角色上
|
||
|
||
// 是否开启本插件,默认禁用;将此改成 true 将启用本插件。
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
// 在这里定义全部的新角色属性
|
||
// 请注意,在这里定义的内容不会多角色共用,在切换时会进行恢复。
|
||
// 你也可以自行新增或删除,比如不共用金币则可以加上"money"的初始化,不共用道具则可以加上"items"的初始化,
|
||
// 多角色共用hp的话则删除hp,等等。总之,不共用的属性都在这里进行定义就好。
|
||
var hero1 = {
|
||
"floorId": "MT0", // 该角色初始楼层ID;如果共用楼层可以注释此项
|
||
"image": "brave.png", // 角色的行走图名称;此项必填不然会报错
|
||
"name": "1号角色",
|
||
"lv": 1,
|
||
"hp": 10000, // 如果HP共用可注释此项
|
||
"atk": 1000,
|
||
"def": 1000,
|
||
"mdef": 0,
|
||
// "money": 0, // 如果要不共用金币则取消此项注释
|
||
// "exp": 0, // 如果要不共用经验则取消此项注释
|
||
"loc": { "x": 0, "y": 0, "direction": "up" }, // 该角色初始位置;如果共用位置可注释此项
|
||
"items": {
|
||
"tools": {}, // 如果共用消耗道具(含钥匙)则可注释此项
|
||
// "constants": {}, // 如果不共用永久道具(如手册)可取消注释此项
|
||
"equips": {}, // 如果共用在背包的装备可注释此项
|
||
},
|
||
"equipment": [], // 如果共用装备可注释此项;此项和上面的「共用在背包的装备」需要拥有相同状态,不然可能出现问题
|
||
};
|
||
// 也可以类似新增其他角色
|
||
// 新增的角色,各项属性共用与不共用的选择必须和上面完全相同,否则可能出现问题。
|
||
// var hero2 = { ...
|
||
|
||
var heroCount = 2; // 包含默认角色在内总共多少个角色,该值需手动修改。
|
||
|
||
this.initHeros = function () {
|
||
core.setFlag("hero1", core.clone(hero1)); // 将属性值存到变量中
|
||
// core.setFlag("hero2", core.clone(hero2)); // 更多的角色也存入变量中;每个定义的角色都需要新增一行
|
||
|
||
// 检测是否存在装备
|
||
if (hero1.equipment) {
|
||
if (!hero1.items || !hero1.items.equips) {
|
||
alert('多角色插件的equipment和道具中的equips必须拥有相同状态!');
|
||
}
|
||
// 存99号套装为全空
|
||
var saveEquips = core.getFlag("saveEquips", []);
|
||
saveEquips[99] = [];
|
||
core.setFlag("saveEquips", saveEquips);
|
||
} else {
|
||
if (hero1.items && hero1.items.equips) {
|
||
alert('多角色插件的equipment和道具中的equips必须拥有相同状态!');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 在游戏开始注入initHeros
|
||
var _startGame_setHard = core.events._startGame_setHard;
|
||
core.events._startGame_setHard = function () {
|
||
_startGame_setHard.call(core.events);
|
||
core.initHeros();
|
||
}
|
||
|
||
// 切换角色
|
||
// 可以使用 core.changeHero() 来切换到下一个角色
|
||
// 也可以 core.changeHero(1) 来切换到某个角色(默认角色为0)
|
||
this.changeHero = function (toHeroId) {
|
||
var currHeroId = core.getFlag("heroId", 0); // 获得当前角色ID
|
||
if (toHeroId == null) {
|
||
toHeroId = (currHeroId + 1) % heroCount;
|
||
}
|
||
if (currHeroId == toHeroId) return;
|
||
|
||
var saveList = Object.keys(hero1);
|
||
|
||
// 保存当前内容
|
||
var toSave = {};
|
||
// 暂时干掉 drawTip 和 音效,避免切装时的提示
|
||
var _drawTip = core.ui.drawTip;
|
||
core.ui.drawTip = function () { };
|
||
var _playSound = core.control.playSound;
|
||
core.control.playSound = function () { return undefined; };
|
||
// 记录当前录像,因为可能存在换装问题
|
||
core.clearRouteFolding();
|
||
var routeLength = core.status.route.length;
|
||
// 优先判定装备
|
||
if (hero1.equipment) {
|
||
core.items.quickSaveEquip(100 + currHeroId);
|
||
core.items.quickLoadEquip(99);
|
||
}
|
||
|
||
saveList.forEach(function (name) {
|
||
if (name == 'floorId') toSave[name] = core.status.floorId; // 楼层单独设置
|
||
else if (name == 'items') {
|
||
toSave.items = core.clone(core.status.hero.items);
|
||
Object.keys(toSave.items).forEach(function (one) {
|
||
if (!hero1.items[one]) delete toSave.items[one];
|
||
});
|
||
} else toSave[name] = core.clone(core.status.hero[name]); // 使用core.clone()来创建新对象
|
||
});
|
||
|
||
core.setFlag("hero" + currHeroId, toSave); // 将当前角色信息进行保存
|
||
var data = core.getFlag("hero" + toHeroId); // 获得要切换的角色保存内容
|
||
|
||
// 设置角色的属性值
|
||
saveList.forEach(function (name) {
|
||
if (name == "items") {
|
||
Object.keys(core.status.hero.items).forEach(function (one) {
|
||
if (data.items[one]) core.status.hero.items[one] = core.clone(data.items[one]);
|
||
});
|
||
} else {
|
||
core.status.hero[name] = core.clone(data[name]);
|
||
}
|
||
});
|
||
// 最后装上装备
|
||
if (hero1.equipment) {
|
||
core.items.quickLoadEquip(100 + toHeroId);
|
||
}
|
||
|
||
core.ui.drawTip = _drawTip;
|
||
core.control.playSound = _playSound;
|
||
core.status.route = core.status.route.slice(0, routeLength);
|
||
core.control._bindRoutePush();
|
||
|
||
// 插入事件:改变角色行走图并进行楼层切换
|
||
var toFloorId = data.floorId || core.status.floorId;
|
||
var toLoc = data.loc || core.status.hero.loc;
|
||
core.insertAction([
|
||
{ "type": "setHeroIcon", "name": data.image || "hero.png" }, // 改变行走图
|
||
// 同层则用changePos,不同层则用changeFloor;这是为了避免共用楼层造成触发eachArrive
|
||
toFloorId != core.status.floorId ? {
|
||
"type": "changeFloor",
|
||
"floorId": toFloorId,
|
||
"loc": [toLoc.x, toLoc.y],
|
||
"direction": toLoc.direction,
|
||
"time": 0 // 可以在这里设置切换时间
|
||
} : { "type": "changePos", "loc": [toLoc.x, toLoc.y], "direction": toLoc.direction }
|
||
// 你还可以在这里执行其他事件,比如增加或取消跟随效果
|
||
]);
|
||
core.setFlag("heroId", toHeroId); // 保存切换到的角色ID
|
||
}
|
||
},
|
||
"heroFourFrames": function () {
|
||
// 样板的勇士/跟随者移动时只使用2、4两帧,观感较差。本插件可以将四帧全用上。
|
||
|
||
// 是否启用本插件
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
["up", "down", "left", "right"].forEach(function (one) {
|
||
// 指定中间帧动画
|
||
core.material.icons.hero[one].midFoot = 2;
|
||
});
|
||
|
||
var heroMoving = function (timestamp) {
|
||
if (core.status.heroMoving <= 0) return;
|
||
if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) {
|
||
core.animateFrame.leftLeg++;
|
||
core.animateFrame.moveTime = timestamp;
|
||
}
|
||
core.drawHero(['stop', 'leftFoot', 'midFoot', 'rightFoot'][core.animateFrame.leftLeg % 4], 4 * core.status.heroMoving);
|
||
}
|
||
core.registerAnimationFrame('heroMoving', true, heroMoving);
|
||
|
||
core.events._eventMoveHero_moving = function (step, moveSteps) {
|
||
var curr = moveSteps[0];
|
||
var direction = curr[0], x = core.getHeroLoc('x'), y = core.getHeroLoc('y');
|
||
// ------ 前进/后退
|
||
var o = direction == 'backward' ? -1 : 1;
|
||
if (direction == 'forward' || direction == 'backward') direction = core.getHeroLoc('direction');
|
||
var faceDirection = direction;
|
||
if (direction == 'leftup' || direction == 'leftdown') faceDirection = 'left';
|
||
if (direction == 'rightup' || direction == 'rightdown') faceDirection = 'right';
|
||
core.setHeroLoc('direction', direction);
|
||
if (curr[1] <= 0) {
|
||
core.setHeroLoc('direction', faceDirection);
|
||
moveSteps.shift();
|
||
return true;
|
||
}
|
||
if (step <= 4) core.drawHero('stop', 4 * o * step);
|
||
else if (step <= 8) core.drawHero('leftFoot', 4 * o * step);
|
||
else if (step <= 12) core.drawHero('midFoot', 4 * o * (step - 8));
|
||
else if (step <= 16) core.drawHero('rightFoot', 4 * o * (step - 8)); // if (step == 8) {
|
||
if (step == 8 || step == 16) {
|
||
core.setHeroLoc('x', x + o * core.utils.scan2[direction].x, true);
|
||
core.setHeroLoc('y', y + o * core.utils.scan2[direction].y, true);
|
||
core.updateFollowers();
|
||
curr[1]--;
|
||
if (curr[1] <= 0) moveSteps.shift();
|
||
core.setHeroLoc('direction', faceDirection);
|
||
return step == 16;
|
||
}
|
||
return false;
|
||
}
|
||
},
|
||
"startCanvas": function () {
|
||
// 使用本插件可以将自绘的标题界面居中。仅在【标题开启事件化】后才有效。
|
||
// 由于一些技术性的原因,标题界面事件化无法应用到覆盖状态栏的整个界面。
|
||
// 这是一个较为妥协的插件,会在自绘标题界面时隐藏状态栏、工具栏和边框,并将画布进行居中。
|
||
// 本插件仅在全塔属性的 "startCanvas" 生效;进入 "startText" 时将会离开居中状态,回归正常界面。
|
||
|
||
// 是否开启本插件,默认禁用;将此改成 true 将启用本插件。
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
// 检查【标题开启事件化】是否开启
|
||
if (!core.flags.startUsingCanvas || main.mode != 'play') return;
|
||
|
||
var _isTitleCanvasEnabled = false;
|
||
var _getClickLoc = core.actions._getClickLoc;
|
||
this._setTitleCanvas = function () {
|
||
if (_isTitleCanvasEnabled) return;
|
||
_isTitleCanvasEnabled = true;
|
||
|
||
// 禁用窗口resize
|
||
window.onresize = function () { };
|
||
core.resize = function () { }
|
||
|
||
// 隐藏状态栏
|
||
core.dom.statusBar.style.display = 'none';
|
||
core.dom.statusCanvas.style.display = 'none';
|
||
core.dom.toolBar.style.display = 'none';
|
||
// 居中画布
|
||
if (core.domStyle.isVertical) {
|
||
core.dom.gameDraw.style.top =
|
||
(parseInt(core.dom.gameGroup.style.height) - parseInt(core.dom.gameDraw.style.height)) / 2 + "px";
|
||
} else {
|
||
core.dom.gameDraw.style.right =
|
||
(parseInt(core.dom.gameGroup.style.width) - parseInt(core.dom.gameDraw.style.width)) / 2 + "px";
|
||
}
|
||
core.dom.gameDraw.style.border = '3px transparent solid';
|
||
core.actions._getClickLoc = function (x, y) {
|
||
var left = core.dom.gameGroup.offsetLeft + core.dom.gameDraw.offsetLeft + 3;
|
||
var top = core.dom.gameGroup.offsetTop + core.dom.gameDraw.offsetTop + 3;
|
||
var loc = { 'x': Math.max(x - left, 0), 'y': Math.max(y - top, 0), 'size': 32 * core.domStyle.scale };
|
||
return loc;
|
||
}
|
||
}
|
||
|
||
this._resetTitleCanvas = function () {
|
||
if (!_isTitleCanvasEnabled) return;
|
||
_isTitleCanvasEnabled = false;
|
||
window.onresize = function () { try { main.core.resize(); } catch (ee) { console.error(ee) } }
|
||
core.resize = function () { return core.control.resize(); }
|
||
core.resize();
|
||
core.actions._getClickLoc = _getClickLoc;
|
||
}
|
||
|
||
// 复写“开始游戏”
|
||
core.events._startGame_start = function (hard, seed, route, callback) {
|
||
console.log('开始游戏');
|
||
core.resetGame(core.firstData.hero, hard, null, core.cloneArray(core.initStatus.maps));
|
||
core.setHeroLoc('x', -1);
|
||
core.setHeroLoc('y', -1);
|
||
|
||
if (seed != null) {
|
||
core.setFlag('__seed__', seed);
|
||
core.setFlag('__rand__', seed);
|
||
} else core.utils.__init_seed();
|
||
|
||
core.clearStatusBar();
|
||
core.plugin._setTitleCanvas();
|
||
|
||
var todo = [];
|
||
core.hideStatusBar();
|
||
core.push(todo, core.firstData.startCanvas);
|
||
core.push(todo, { "type": "function", "function": "function() { core.plugin._resetTitleCanvas(); core.events._startGame_setHard(); }" })
|
||
core.push(todo, core.firstData.startText);
|
||
this.insertAction(todo, null, null, function () {
|
||
core.events._startGame_afterStart(callback);
|
||
});
|
||
|
||
if (route != null) core.startReplay(route);
|
||
}
|
||
|
||
var _loadData = core.control.loadData;
|
||
core.control.loadData = function (data, callback) {
|
||
core.plugin._resetTitleCanvas();
|
||
_loadData.call(core.control, data, callback);
|
||
}
|
||
},
|
||
"advancedAnimation": function () {
|
||
// -------------------- 插件说明 -------------------- //
|
||
// github仓库:https://github.com/unanmed/animate
|
||
// npm包名:mutate-animate
|
||
// npm地址:https://www.npmjs.com/package/mutate-animate
|
||
|
||
// 是否开启本插件,默认启用;将此改成 false 将禁用本插件。
|
||
var __enable = true;
|
||
|
||
if (main.replayChecking) __enable = false;
|
||
if (!__enable) {
|
||
core.plugin.animate = {};
|
||
this.tickerSet = new Set();
|
||
this.deleteAllAnis = () => { };
|
||
return;
|
||
}
|
||
|
||
/** 该集合中的所有Ticker在跨层时需要被摧毁 */
|
||
this.tickerSet = new Set();
|
||
|
||
/** 对Map中所有Ticker执行摧毁事件 */
|
||
this.deleteAllAnis = function () {
|
||
core.plugin.tickerSet.forEach((ticker) => ticker.destroy());
|
||
}
|
||
|
||
let w = [];
|
||
const k = (n) => {
|
||
for (const i of w)
|
||
if (i.status === "running")
|
||
try {
|
||
for (const t of i.funcs)
|
||
t(n - i.startTime);
|
||
} catch (t) {
|
||
i.destroy(), console.error(t);
|
||
}
|
||
requestAnimationFrame(k);
|
||
};
|
||
requestAnimationFrame(k);
|
||
|
||
/** Ticker类 */
|
||
class I {
|
||
constructor() {
|
||
this.funcs = /* @__PURE__ */ new Set();
|
||
this.status = "stop";
|
||
this.startTime = 0;
|
||
this.status = "running", w.push(this), requestAnimationFrame((i) => this.startTime = i);
|
||
}
|
||
add(i) {
|
||
return this.funcs.add(i), this;
|
||
}
|
||
remove(i) {
|
||
return this.funcs.delete(i), this;
|
||
}
|
||
clear() {
|
||
this.funcs.clear();
|
||
}
|
||
destroy() {
|
||
core.plugin.tickerSet.delete(this);
|
||
this.clear(), this.stop();
|
||
}
|
||
stop() {
|
||
this.status = "stop", w = w.filter((i) => i !== this);
|
||
}
|
||
}
|
||
/** AnimationBase类 */
|
||
class F {
|
||
constructor() {
|
||
this.timing = Date.now;
|
||
this.relation = "absolute";
|
||
this.easeTime = 0;
|
||
this.applying = {};
|
||
this.getTime = Date.now;
|
||
const ticker = new I();
|
||
this.ticker = ticker;
|
||
this.value = {};
|
||
this.listener = {};
|
||
this.timing = (i) => i;
|
||
}
|
||
async all() {
|
||
if (Object.values(this.applying).every((i) => i === !0))
|
||
throw new ReferenceError("There is no animates to be waited.");
|
||
await new Promise((i) => {
|
||
const t = () => {
|
||
Object.values(this.applying).every((e) => e === !1) && (this.unlisten("end", t), i("all animated."));
|
||
};
|
||
this.listen("end", t);
|
||
});
|
||
}
|
||
async n(i) {
|
||
const t = Object.values(this.applying).filter((s) => s === !0).length;
|
||
if (t < i)
|
||
throw new ReferenceError(
|
||
`You are trying to wait ${i} animate, but there are only ${t} animate animating.`
|
||
);
|
||
let e = 0;
|
||
await new Promise((s) => {
|
||
const r = () => {
|
||
e++, e === i && (this.unlisten("end", r), s(`${i} animated.`));
|
||
};
|
||
this.listen("end", r);
|
||
});
|
||
}
|
||
async w(i) {
|
||
if (this.applying[i] === !1)
|
||
throw new ReferenceError(`The ${i} animate is not animating.`);
|
||
await new Promise((t) => {
|
||
const e = () => {
|
||
this.applying[i] === !1 && (this.unlisten("end", e), t(`${i} animated.`));
|
||
};
|
||
this.listen("end", e);
|
||
});
|
||
}
|
||
listen(i, t) {
|
||
var e, s;
|
||
(s = (e = this.listener)[i]) != null || (e[i] = []), this.listener[i].push(t);
|
||
}
|
||
unlisten(i, t) {
|
||
const e = this.listener[i].findIndex((s) => s === t);
|
||
if (e === -1)
|
||
throw new ReferenceError(
|
||
"You are trying to remove a nonexistent listener."
|
||
);
|
||
this.listener[i].splice(e, 1);
|
||
}
|
||
hook(...i) {
|
||
const t = Object.entries(this.listener).filter(
|
||
(e) => i.includes(e[0])
|
||
);
|
||
for (const [e, s] of t)
|
||
for (const r of s)
|
||
r(this, e);
|
||
}
|
||
}
|
||
|
||
function y(n) {
|
||
return n != null;
|
||
}
|
||
async function R(n) {
|
||
return new Promise((i) => setTimeout(i, n));
|
||
}
|
||
/** Animation类 */
|
||
class j extends F {
|
||
constructor() {
|
||
super();
|
||
this.shakeTiming;
|
||
this.path;
|
||
this.multiTiming;
|
||
this.value = {};
|
||
this.size = 1;
|
||
this.angle = 0;
|
||
this.targetValue = {
|
||
system: {
|
||
move: [0, 0],
|
||
moveAs: [0, 0],
|
||
resize: 0,
|
||
rotate: 0,
|
||
shake: 0,
|
||
/** @type {number[]} */"@@bind": []
|
||
},
|
||
custom: {}
|
||
};
|
||
this.animateFn = {
|
||
system: {
|
||
move: [() => { }, () => { }],
|
||
moveAs: () => { },
|
||
resize: () => { },
|
||
rotate: () => { },
|
||
shake: () => { },
|
||
"@@bind": () => { }
|
||
},
|
||
custom: {}
|
||
};
|
||
this.ox = 0;
|
||
this.oy = 0;
|
||
this.sx = 0;
|
||
this.sy = 0;
|
||
this.bindInfo = [];
|
||
this.timing = (t) => t, this.shakeTiming = (t) => t, this.multiTiming = (t) => [t, t], this.path = (t) => [t, t], this.applying = {
|
||
move: !1,
|
||
scale: !1,
|
||
rotate: !1,
|
||
shake: !1
|
||
}, this.ticker.add(() => {
|
||
const { running: t } = this.listener;
|
||
if (y(t))
|
||
for (const e of t)
|
||
e(this, "running");
|
||
});
|
||
}
|
||
get x() {
|
||
return this.ox + this.sx;
|
||
}
|
||
get y() {
|
||
return this.oy + this.sy;
|
||
}
|
||
mode(t, e = !1) {
|
||
return typeof t(0) == "number" ? e ? this.shakeTiming = t : this.timing = t : this.multiTiming = t, this;
|
||
}
|
||
time(t) {
|
||
return this.easeTime = t, this;
|
||
}
|
||
relative() {
|
||
return this.relation = "relative", this;
|
||
}
|
||
absolute() {
|
||
return this.relation = "absolute", this;
|
||
}
|
||
bind(...t) {
|
||
return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = t, this;
|
||
}
|
||
unbind() {
|
||
return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = [], this;
|
||
}
|
||
move(t, e) {
|
||
return this.applying.move && this.end(!0, "move"), this.applySys("ox", t, "move"), this.applySys("oy", e, "move"), this;
|
||
}
|
||
rotate(t) {
|
||
return this.applySys("angle", t, "rotate"), this;
|
||
}
|
||
scale(t) {
|
||
return this.applySys("size", t, "resize"), this;
|
||
}
|
||
shake(t, e) {
|
||
this.applying.shake === !0 && this.end(!0, "shake"), this.applying.shake = !0;
|
||
const { easeTime: s, shakeTiming: r } = this, l = this.getTime();
|
||
if (this.hook("start", "shakestart"), s <= 0)
|
||
return this.end(!1, "shake"), this;
|
||
const a = () => {
|
||
const c = this.getTime() - l;
|
||
if (c > s) {
|
||
this.ticker.remove(a), this.applying.shake = !1, this.sx = 0, this.sy = 0, this.hook("end", "shakeend");
|
||
return;
|
||
}
|
||
const h = c / s,
|
||
m = r(h);
|
||
this.sx = m * t, this.sy = m * e;
|
||
};
|
||
return this.ticker.add(a), this.animateFn.system.shake = a, this;
|
||
}
|
||
moveAs(t) {
|
||
this.applying.moveAs && this.end(!0, "moveAs"), this.applying.moveAs = !0, this.path = t;
|
||
const { easeTime: e, relation: s, timing: r } = this, l = this.getTime(), [a, u] = [this.x, this.y], [c, h] = (() => {
|
||
if (s === "absolute")
|
||
return t(1); {
|
||
const [d, f] = t(1);
|
||
return [a + d, u + f];
|
||
}
|
||
})();
|
||
if (this.hook("start", "movestart"), e <= 0)
|
||
return this.end(!1, "moveAs"), this;
|
||
const m = () => {
|
||
const f = this.getTime() - l;
|
||
if (f > e) {
|
||
this.end(!0, "moveAs");
|
||
return;
|
||
}
|
||
const g = f / e,
|
||
[v, x] = t(r(g));
|
||
s === "absolute" ? (this.ox = v, this.oy = x) : (this.ox = a + v, this.oy = u + x);
|
||
};
|
||
return this.ticker.add(m), this.animateFn.system.moveAs = m, this.targetValue.system.moveAs = [c, h], this;
|
||
}
|
||
register(t, e) {
|
||
if (typeof this.value[t] == "number")
|
||
return this.error(
|
||
`Property ${t} has been regietered twice.`,
|
||
"reregister"
|
||
);
|
||
this.value[t] = e, this.applying[t] = !1;
|
||
}
|
||
apply(t, e) {
|
||
this.applying[t] === !0 && this.end(!1, t), t in this.value || this.error(
|
||
`You are trying to execute nonexistent property ${t}.`
|
||
), this.applying[t] = !0;
|
||
const s = this.value[t],
|
||
r = this.getTime(),
|
||
{ timing: l, relation: a, easeTime: u } = this,
|
||
c = a === "absolute" ? e - s : e;
|
||
if (this.hook("start"), u <= 0)
|
||
return this.end(!1, t), this;
|
||
const h = () => {
|
||
const d = this.getTime() - r;
|
||
if (d > u) {
|
||
this.end(!1, t);
|
||
return;
|
||
}
|
||
const f = d / u,
|
||
g = l(f);
|
||
this.value[t] = s + g * c;
|
||
};
|
||
return this.ticker.add(h), this.animateFn.custom[t] = h, this.targetValue.custom[t] = c + s, this;
|
||
}
|
||
applyMulti() {
|
||
this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.applying["@@bind"] = !0;
|
||
const t = this.bindInfo,
|
||
e = t.map((h) => this.value[h]),
|
||
s = this.getTime(),
|
||
{ multiTiming: r, relation: l, easeTime: a } = this,
|
||
u = r(1);
|
||
if (u.length !== e.length)
|
||
throw new TypeError(
|
||
`The number of binded animate attributes and timing function returns's length does not match. binded: ${t.length}, timing: ${u.length}`
|
||
);
|
||
if (this.hook("start"), a <= 0)
|
||
return this.end(!1, "@@bind"), this;
|
||
const c = () => {
|
||
const m = this.getTime() - s;
|
||
if (m > a) {
|
||
this.end(!1, "@@bind");
|
||
return;
|
||
}
|
||
const d = m / a,
|
||
f = r(d);
|
||
t.forEach((g, v) => {
|
||
l === "absolute" ? this.value[g] = f[v] : this.value[g] = e[v] + f[v];
|
||
});
|
||
};
|
||
return this.ticker.add(c), this.animateFn.custom["@@bind"] = c, this.targetValue.system["@@bind"] = u, this;
|
||
}
|
||
applySys(t, e, s) {
|
||
s !== "move" && this.applying[s] === !0 && this.end(!0, s), this.applying[s] = !0;
|
||
const r = this[t],
|
||
l = this.getTime(),
|
||
a = this.timing,
|
||
u = this.relation,
|
||
c = this.easeTime,
|
||
h = u === "absolute" ? e - r : e;
|
||
if (this.hook("start", `${s}start`), c <= 0)
|
||
return this.end(!0, s);
|
||
const m = () => {
|
||
const f = this.getTime() - l;
|
||
if (f > c) {
|
||
this.end(!0, s);
|
||
return;
|
||
}
|
||
const g = f / c,
|
||
v = a(g);
|
||
this[t] = r + h * v, t !== "oy" && this.hook(s);
|
||
};
|
||
this.ticker.add(m), t === "ox" ? this.animateFn.system.move[0] = m : t === "oy" ? this.animateFn.system.move[1] = m : this.animateFn.system[s] = m, s === "move" ? (t === "ox" && (this.targetValue.system.move[0] = h + r), t === "oy" && (this.targetValue.system.move[1] = h + r)) : s !== "shake" && (this.targetValue.system[s] = h + r);
|
||
}
|
||
error(t, e) {
|
||
throw e === "repeat" ? new Error(
|
||
`Cannot execute the same animation twice. Info: ${t}`
|
||
) : e === "reregister" ? new Error(
|
||
`Cannot register an animated property twice. Info: ${t}`
|
||
) : new Error(t);
|
||
}
|
||
end(t, e) {
|
||
if (t === !0)
|
||
if (this.applying[e] = !1, e === "move" ? (this.ticker.remove(this.animateFn.system.move[0]), this.ticker.remove(this.animateFn.system.move[1])) : e === "moveAs" ? this.ticker.remove(this.animateFn.system.moveAs) : e === "@@bind" ? this.ticker.remove(this.animateFn.system["@@bind"]) : this.ticker.remove(
|
||
this.animateFn.system[e]
|
||
), e === "move") {
|
||
const [s, r] = this.targetValue.system.move;
|
||
this.ox = s, this.oy = r, this.hook("moveend", "end");
|
||
} else if (e === "moveAs") {
|
||
const [s, r] = this.targetValue.system.moveAs;
|
||
this.ox = s, this.oy = r, this.hook("moveend", "end");
|
||
} else
|
||
e === "rotate" ? (this.angle = this.targetValue.system.rotate, this.hook("rotateend", "end")) : e === "resize" ? (this.size = this.targetValue.system.resize, this.hook("resizeend", "end")) : e === "@@bind" ? this.bindInfo.forEach((r, l) => {
|
||
this.value[r] = this.targetValue.system["@@bind"][l];
|
||
}) : (this.sx = 0, this.sy = 0, this.hook("shakeend", "end"));
|
||
else
|
||
this.applying[e] = !1, this.ticker.remove(this.animateFn.custom[e]), this.value[e] = this.targetValue.custom[e], this.hook("end");
|
||
}
|
||
}
|
||
class O extends F {
|
||
constructor() {
|
||
super();
|
||
this.now = {};
|
||
this.target = {};
|
||
this.transitionFn = {};
|
||
this.value = undefined;
|
||
this.handleSet = (t, e, s) => (this.transition(e, s), !0);
|
||
this.handleGet = (t, e) => this.now[e];
|
||
this.timing = (t) => t, this.value = new Proxy(this.target, {
|
||
set: this.handleSet,
|
||
get: this.handleGet
|
||
});
|
||
}
|
||
mode(t) {
|
||
return this.timing = t, this;
|
||
}
|
||
time(t) {
|
||
return this.easeTime = t, this;
|
||
}
|
||
relative() {
|
||
return this.relation = "relative", this;
|
||
}
|
||
absolute() {
|
||
return this.relation = "absolute", this;
|
||
}
|
||
transition(t, e) {
|
||
if (e === this.target[t])
|
||
return this;
|
||
if (!y(this.now[t]))
|
||
return this.now[t] = e, this;
|
||
this.applying[t] && this.end(t, !0), this.applying[t] = !0, this.hook("start");
|
||
const s = this.getTime(),
|
||
r = this.easeTime,
|
||
l = this.timing,
|
||
a = this.now[t],
|
||
u = e + (this.relation === "absolute" ? 0 : a),
|
||
c = u - a;
|
||
this.target[t] = u;
|
||
const h = () => {
|
||
const d = this.getTime() - s;
|
||
if (d >= r) {
|
||
this.end(t);
|
||
return;
|
||
}
|
||
const f = d / r;
|
||
this.now[t] = l(f) * c + a, this.hook("running");
|
||
};
|
||
return this.transitionFn[t] = h, this.ticker.add(h), r <= 0 ? (this.end(t), this) : this;
|
||
}
|
||
end(t, e = !1) {
|
||
const s = this.transitionFn[t];
|
||
if (!y(s))
|
||
throw new ReferenceError(
|
||
`You are trying to end an ended transition: ${t}`
|
||
);
|
||
this.ticker.remove(this.transitionFn[t]), delete this.transitionFn[t], this.applying[t] = !1, this.hook("end"), e || (this.now[t] = this.target[t]);
|
||
}
|
||
}
|
||
const T = (...n) => n.reduce((i, t) => i + t, 0),
|
||
b = (n) => {
|
||
if (n === 0)
|
||
return 1;
|
||
let i = n;
|
||
for (; n > 1;)
|
||
n--, i *= n;
|
||
return i;
|
||
},
|
||
A = (n, i) => Math.round(b(i) / (b(n) * b(i - n))),
|
||
p = (n, i, t = (e) => 1 - i(1 - e)) => n === "in" ? i : n === "out" ? t : n === "in-out" ? (e) => e < 0.5 ? i(e * 2) / 2 : 0.5 + t((e - 0.5) * 2) / 2 : (e) => e < 0.5 ? t(e * 2) / 2 : 0.5 + i((e - 0.5) * 2) / 2,
|
||
$ = Math.cosh(2),
|
||
z = Math.acosh(2),
|
||
V = Math.tanh(3),
|
||
P = Math.atan(5);
|
||
|
||
function Y() {
|
||
return (n) => n;
|
||
}
|
||
|
||
function q(...n) {
|
||
const i = [0].concat(n);
|
||
i.push(1);
|
||
const t = i.length,
|
||
e = Array(t).fill(0).map((s, r) => A(r, t - 1));
|
||
return (s) => {
|
||
const r = e.map((l, a) => l * i[a] * (1 - s) ** (t - a - 1) * s ** a);
|
||
return T(...r);
|
||
};
|
||
}
|
||
|
||
function U(n, i) {
|
||
if (n === "sin") {
|
||
const t = (s) => Math.sin(s * Math.PI / 2);
|
||
return p(i, (s) => 1 - t(1 - s), t);
|
||
}
|
||
if (n === "sec") {
|
||
const t = (s) => 1 / Math.cos(s);
|
||
return p(i, (s) => t(s * Math.PI / 3) - 1);
|
||
}
|
||
throw new TypeError(
|
||
"Unexpected parameters are delivered in trigo timing function."
|
||
);
|
||
}
|
||
|
||
function C(n, i) {
|
||
if (!Number.isInteger(n))
|
||
throw new TypeError(
|
||
"The first parameter of power timing function only allow integer."
|
||
);
|
||
return p(i, (e) => e ** n);
|
||
}
|
||
|
||
function G(n, i) {
|
||
if (n === "sin")
|
||
return p(i, (e) => (Math.cosh(e * 2) - 1) / ($ - 1));
|
||
if (n === "tan") {
|
||
const t = (s) => Math.tanh(s * 3) * 1 / V;
|
||
return p(i, (s) => 1 - t(1 - s), t);
|
||
}
|
||
if (n === "sec") {
|
||
const t = (s) => 1 / Math.cosh(s);
|
||
return p(i, (s) => 1 - (t(s * z) - 0.5) * 2);
|
||
}
|
||
throw new TypeError(
|
||
"Unexpected parameters are delivered in hyper timing function."
|
||
);
|
||
}
|
||
|
||
function N(n, i) {
|
||
if (n === "sin") {
|
||
const t = (s) => Math.asin(s) / Math.PI * 2;
|
||
return p(i, (s) => 1 - t(1 - s), t);
|
||
}
|
||
if (n === "tan") {
|
||
const t = (s) => Math.atan(s * 5) / P;
|
||
return p(i, (s) => 1 - t(1 - s), t);
|
||
}
|
||
throw new TypeError(
|
||
"Unexpected parameters are delivered in inverse trigo timing function."
|
||
);
|
||
}
|
||
/** @param {(input:number) => number} [i=() => 1] */
|
||
function B(n, i = () => 1) {
|
||
let t = -1;
|
||
return (e) => (t *= -1, e < 0.5 ? n * i(e * 2) * t : n * i((1 - e) * 2) * t);
|
||
}
|
||
|
||
function D(n, i = 1, t = [0, 0], e = 0, s = (l) => 1, r = !1) {
|
||
return (l) => {
|
||
const a = i * l * Math.PI * 2 + e * Math.PI / 180,
|
||
u = Math.cos(a),
|
||
c = Math.sin(a),
|
||
h = n * s(s(r ? 1 - l : l));
|
||
return [h * u + t[0], h * c + t[1]];
|
||
};
|
||
}
|
||
|
||
function H(n, i, ...t) {
|
||
const e = [n].concat(t);
|
||
e.push(i);
|
||
const s = e.length,
|
||
r = Array(s).fill(0).map((l, a) => A(a, s - 1));
|
||
return (l) => {
|
||
const a = r.map((c, h) => c * e[h][0] * (1 - l) ** (s - h - 1) * l ** h),
|
||
u = r.map((c, h) => c * e[h][1] * (1 - l) ** (s - h - 1) * l ** h);
|
||
return [T(...a), T(...u)];
|
||
};
|
||
}
|
||
|
||
core.plugin.animate = {
|
||
Animation: j,
|
||
AnimationBase: F,
|
||
Ticker: I,
|
||
Transition: O,
|
||
bezier: q,
|
||
bezierPath: H,
|
||
circle: D,
|
||
hyper: G,
|
||
inverseTrigo: N,
|
||
linear: Y,
|
||
power: C,
|
||
shake: B,
|
||
sleep: R,
|
||
trigo: U,
|
||
}
|
||
|
||
},
|
||
"drawItemDetail": function () {
|
||
/* 宝石血瓶左下角显示数值
|
||
* 需要将 变量:itemDetail改为true才可正常运行
|
||
* 请尽量减少勇士的属性数量,否则可能会出现严重卡顿(划掉,现在你放一万个属性也不会卡)
|
||
* 注意:这里的属性必须是core.status.hero里面的,flag无法显示
|
||
* 如果不想显示,可以core.setFlag("itemDetail", false);
|
||
* 然后再core.getItemDetail();
|
||
* 如有bug在大群或造塔群@古祠
|
||
*/
|
||
|
||
// 忽略的道具
|
||
const ignore = ['superPotion'];
|
||
|
||
// 取消注释下面这句可以减少超大地图的判定。
|
||
// 如果地图宝石过多,可能会略有卡顿,可以尝试取消注释下面这句话来解决。
|
||
// core.bigmap.threshold = 256;
|
||
const origin = core.control.updateStatusBar;
|
||
core.updateStatusBar = core.control.updateStatusBar = function () {
|
||
if (core.getFlag('__statistics__')) return;
|
||
else return origin.apply(core.control, arguments);
|
||
}
|
||
|
||
core.control.updateDamage = function (floorId, ctx) {
|
||
floorId = floorId || core.status.floorId;
|
||
if (!floorId || core.status.gameOver || main.mode != 'play') return;
|
||
const onMap = ctx == null;
|
||
|
||
// 没有怪物手册
|
||
if (!core.hasItem('book')) return;
|
||
core.status.damage.posX = core.bigmap.posX;
|
||
core.status.damage.posY = core.bigmap.posY;
|
||
if (!onMap) {
|
||
const width = core.floors[floorId].width,
|
||
height = core.floors[floorId].height;
|
||
// 地图过大的缩略图不绘制显伤
|
||
if (width * height > core.bigmap.threshold) return;
|
||
}
|
||
this._updateDamage_damage(floorId, onMap);
|
||
this._updateDamage_extraDamage(floorId, onMap);
|
||
core.getItemDetail(floorId); // 宝石血瓶详细信息
|
||
this.drawDamage(ctx);
|
||
};
|
||
|
||
function getRatio() {
|
||
let ratio = (core.status.thisMap?.ratio) ?? 1;
|
||
const currEvent = core.status.event;
|
||
if (!currEvent) return ratio;
|
||
switch (currEvent.id) {
|
||
case 'viewMaps': //调整浏览地图时的倍率
|
||
if (currEvent.data) {
|
||
const viewMapFloorId = (currEvent.data.floorId);
|
||
ratio = core.status.maps[viewMapFloorId].ratio;
|
||
}
|
||
break;
|
||
case 'fly': //调整在楼传界面浏览地图时的倍率
|
||
ratio = core.status.maps[core.floorIds[currEvent.data]].ratio;
|
||
break;
|
||
}
|
||
return ratio;
|
||
}
|
||
|
||
// 获取宝石信息 并绘制
|
||
this.getItemDetail = function (floorId) {
|
||
if (!core.getFlag('itemDetail')) return;
|
||
if (!core.status.thisMap) return;
|
||
floorId = floorId ?? core.status.thisMap.floorId;
|
||
const beforeRatio = core.status.thisMap.ratio;
|
||
core.status.thisMap.ratio = core.status.maps[floorId].ratio;
|
||
let diff = {};
|
||
const before = core.status.hero;
|
||
const hero = core.clone(core.status.hero);
|
||
const handler = {
|
||
set(target, key, v) {
|
||
diff[key] = v - (target[key] || 0);
|
||
if (!diff[key]) diff[key] = void 0;
|
||
return true;
|
||
}
|
||
};
|
||
core.status.hero = new Proxy(hero, handler);
|
||
core.status.maps[floorId].blocks.forEach(function (block) {
|
||
if (
|
||
block.event.cls !== 'items' ||
|
||
ignore.includes(block.event.id) ||
|
||
block.disable
|
||
)
|
||
return;
|
||
const x = block.x,
|
||
y = block.y;
|
||
// v2优化,只绘制范围内的部分
|
||
if (core.bigmap.v2) {
|
||
if (
|
||
x < core.bigmap.posX - core.bigmap.extend ||
|
||
x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend ||
|
||
y < core.bigmap.posY - core.bigmap.extend ||
|
||
y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
diff = {};
|
||
const id = block.event.id;
|
||
const item = core.material.items[id];
|
||
switch (item.cls) {
|
||
case 'equips': {
|
||
// 装备也显示
|
||
diff = item.equip.value ?? {};
|
||
const per = item.equip.percentage ?? {};
|
||
for (const name in per) {
|
||
diff[name + 'per'] = per[name].toString() + '%';
|
||
}
|
||
break;
|
||
}
|
||
case 'items': {
|
||
// 跟数据统计原理一样 执行效果 前后比较
|
||
core.setFlag('__statistics__', true);
|
||
try {
|
||
eval(item.itemEffect);
|
||
} catch (error) { }
|
||
const ratio = getRatio();
|
||
const effectObj = core.getItemEffectValue(id, ratio);
|
||
for (let statusName in effectObj) {
|
||
if (!effectObj.hasOwnProperty(statusName)) continue;
|
||
if (!diff.hasOwnProperty(statusName)) diff[statusName] = 0;
|
||
diff[statusName] += effectObj[statusName];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
drawItemDetail(diff, x, y);
|
||
});
|
||
core.status.thisMap.ratio = beforeRatio;
|
||
core.status.hero = before;
|
||
window.hero = before;
|
||
window.flags = before.flags;
|
||
};
|
||
|
||
// 绘制
|
||
function drawItemDetail(diff, x, y) {
|
||
const px = 32 * x + 2,
|
||
py = 32 * y + 30;
|
||
let content = '';
|
||
// 获得数据和颜色
|
||
let i = 0;
|
||
for (const name in diff) {
|
||
if (!diff[name]) continue;
|
||
let color = '#fff';
|
||
|
||
if (typeof diff[name] === 'number')
|
||
content = core.formatBigNumber(diff[name], true);
|
||
else content = diff[name];
|
||
switch (name) {
|
||
case 'atk':
|
||
case 'atkper':
|
||
color = ' #FF7A7A';
|
||
break;
|
||
case 'def':
|
||
case 'defper':
|
||
color = ' #00E6F1';
|
||
break;
|
||
case 'mdef':
|
||
case 'mdefper':
|
||
color = ' #6EFF83';
|
||
break;
|
||
case 'hp':
|
||
color = ' #A4FF00';
|
||
break;
|
||
case 'hpmax':
|
||
case 'hpmaxper':
|
||
color = ' #F9FF00';
|
||
break;
|
||
case 'mana':
|
||
case 'manamax':
|
||
color = ' #CC6666';
|
||
break;
|
||
}
|
||
// 绘制
|
||
core.status.damage.data.push({
|
||
text: content,
|
||
px: px,
|
||
py: py - 10 * i,
|
||
color: color
|
||
});
|
||
i++;
|
||
}
|
||
}
|
||
},
|
||
"autoClear": function () {
|
||
// 在此增加新插件
|
||
/**
|
||
* --------------- 使用说明 ---------------
|
||
* 变量autoGet控制自动拾取开关
|
||
* 变量autoBattle控制自动清怪开关
|
||
*/
|
||
const ctxName = 'autoClear';
|
||
|
||
// 每走一步后自动拾取的判定要放在阻击结算之后,见libs,并不写在本插件当中
|
||
|
||
this.autoClear = auto;
|
||
|
||
function willLvUp(exp) {
|
||
const nextExp = core.getNextLvUpNeed();
|
||
if (typeof exp === 'number' && typeof nextExp === 'number' && exp >= nextExp) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 是否清这个怪,可以修改这里来实现对不同怪的不同操作
|
||
* @param {string} enemy
|
||
* @param {number} x
|
||
* @param {number} y
|
||
*/
|
||
function canBattle(enemy, x, y) {
|
||
const loc = `${x},${y}`;
|
||
const floor = core.floors[core.status.floorId];
|
||
const e = core.getEnemyValue(enemy, null, x, y);
|
||
const hasEvent =
|
||
has(floor.afterBattle[loc]) || has(floor.beforeBattle[loc]) ||
|
||
has(e.beforeBattle) || has(e.afterBattle) ||
|
||
has(floor.events[loc]) || willLvUp(e.exp); // 防止有升级后事件
|
||
|
||
// 有事件,不清
|
||
if (hasEvent) return false;
|
||
|
||
const cache = core.status.checkBlock.cache;
|
||
const hasGuards = has(cache) && has(cache[loc]) &&
|
||
has(cache[loc]["guards"]) && cache[loc]["guards"].length > 0; // 该敌人会被支援
|
||
if (hasGuards) return false;
|
||
|
||
// 有特定特殊属性的怪不清
|
||
if (
|
||
core.hasSpecial(e.special, 12) || // 中毒
|
||
core.hasSpecial(e.special, 13) || // 衰弱
|
||
core.hasSpecial(e.special, 14) || // 诅咒
|
||
core.hasSpecial(e.special, 19) || // 自爆
|
||
core.hasSpecial(e.special, 21) || // 退化
|
||
core.hasSpecial(e.special, 26) || // 支援
|
||
core.hasSpecial(e.special, 27) || // 捕捉:逻辑上应该让怪物来找角色
|
||
core.hasSpecial(e.special, 28) || // 追猎:逻辑上应该让怪物来找角色
|
||
core.hasSpecial(e.special, 29) // 败移:特殊战后事件
|
||
) {
|
||
return false;
|
||
}
|
||
const damage = core.getDamageInfo(enemy, void 0, x, y)?.damage;
|
||
// 0伤或负伤,清
|
||
if (has(damage) && damage <= 0) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 判断一个点是否能遍历
|
||
*/
|
||
function judge(block, nx, ny, tx, ty, dir, floorId, autoBattle, autoGet) {
|
||
if (!has(block)) { // 说明什么都没有,没事件也没图块
|
||
return { type: "none", canGoThrough: true };
|
||
}
|
||
const cls = block.event.cls;
|
||
const loc = `${tx},${ty}`;
|
||
const floor = core.floors[floorId];
|
||
const changeFloor = floor.changeFloor[loc];
|
||
const isEnemy = autoBattle && cls.startsWith('enemy'),
|
||
isItem = autoGet && cls === 'items';
|
||
// 因为没有判定往来图块的通行性,这里宁可严格一点,非空地(block.id === 0)一律不给穿
|
||
if (has(changeFloor)) {
|
||
if ((changeFloor.ignoreChangeFloor ?? core.flags.ignoreChangeFloor) && block.id === 0) {
|
||
return { type: "unknown", canGoThrough: true };
|
||
}
|
||
return { type: "unknown", canGoThrough: false };
|
||
}
|
||
|
||
if (has(core.floors[floorId].events[loc])) return { type: "unknown", canGoThrough: false };
|
||
|
||
if (isEnemy) return { type: "enemy", canGoThrough: true };
|
||
if (isItem) return { type: "item", canGoThrough: true };
|
||
|
||
return { type: "unknown", canGoThrough: false };
|
||
}
|
||
|
||
/**
|
||
* 是否捡拾这个物品
|
||
*/
|
||
function canGetItem(item, loc, floorId = core.status.floorId) {
|
||
// 可以用于检测道具是否应该被捡起,例如如果捡起后血量超过80%则不捡起可以这么写:
|
||
// if (item.cls === 'items') {
|
||
// let diff = {};
|
||
// const before = core.status.hero;
|
||
// const hero = core.clone(core.status.hero);
|
||
// const handler = {
|
||
// set(target, key, v) {
|
||
// diff[key] = v - (target[key] || 0);
|
||
// if (!diff[key]) diff[key] = void 0;
|
||
// return true;
|
||
// }
|
||
// };
|
||
// core.status.hero = new Proxy(hero, handler);
|
||
|
||
// eval(item.itemEffect);
|
||
|
||
// core.status.hero = before;
|
||
// window.hero = before;
|
||
// window.flags = before.flags;
|
||
// if (
|
||
// diff.hp &&
|
||
// diff.hp + core.status.hero.hp > core.status.hero.hpmax * 0.8
|
||
// )
|
||
// return false;
|
||
// }
|
||
const floor = core.floors[floorId];
|
||
if (has(floor.afterGetItem[loc])) return false;
|
||
if (item.cls === 'items') {
|
||
const itemEffectType = core.getItemEffectType(item.id);
|
||
if (core.hasFlag('noRouting_HP') && itemEffectType.includes('hp')) return false;
|
||
if (core.hasFlag('noRouting_MDEF') && itemEffectType.includes('mdef')) return false;
|
||
if (core.hasFlag('noRouting_ATK') && itemEffectType.includes('atk')) return false;
|
||
if (core.hasFlag('noRouting_DEF') && itemEffectType.includes('def')) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @template T
|
||
* @param {T} v
|
||
* @returns {v is NonNullable<T>}
|
||
*/
|
||
function has(v) {
|
||
return v !== null && v !== undefined;
|
||
}
|
||
|
||
function hasBlockDamage(loc) {
|
||
const checkblockInfo = core.status.checkBlock;
|
||
const damage = checkblockInfo.damage[loc];
|
||
const ambush = checkblockInfo.ambush[loc];
|
||
const repulse = checkblockInfo.repulse[loc];
|
||
const chase = checkblockInfo.chase[loc];
|
||
|
||
return (has(damage) && damage > 0) || has(ambush) || has(repulse) || has(chase);
|
||
}
|
||
|
||
class AttractAnimate {
|
||
constructor() {
|
||
this.name = 'attractAnimate';
|
||
this.isPlaying = false;
|
||
this.nodes = [];
|
||
this.lastTime = -1;
|
||
this.thr = 5; // 缓动比例倒数,越大移动越慢
|
||
}
|
||
|
||
add(id, x, y, callback) {
|
||
if (core.isReplaying()) return;
|
||
this.nodes.push({ id, x, y, callback });
|
||
}
|
||
|
||
start() {
|
||
if (this.isPlaying) return;
|
||
if (core.isReplaying()) return;
|
||
if (core.getLocalStorage('skipPerform')) return;
|
||
this.isPlaying = true;
|
||
core.registerAnimationFrame(this.name, true, this.update.bind(this));
|
||
this.ctx = core.createCanvas(this.name, 0, 0, core.__PIXELS__, core.__PIXELS__, 120);
|
||
}
|
||
|
||
remove() {
|
||
core.unregisterAnimationFrame(this.name);
|
||
core.deleteCanvas(this.name);
|
||
this.isPlaying = false;
|
||
}
|
||
|
||
clear() {
|
||
this.nodes = [];
|
||
this.remove();
|
||
}
|
||
|
||
update(timeStamp) {
|
||
const { name, thr, nodes } = this;
|
||
|
||
if (this.lastTime < 0) this.lastTime = timeStamp;
|
||
if (timeStamp - this.lastTime < 20) return;
|
||
this.lastTime = timeStamp;
|
||
|
||
core.clearMap(name);
|
||
|
||
const heroCenterX = core.status.heroCenter.px - 16;
|
||
const heroCenterY = core.status.heroCenter.py - 16;
|
||
|
||
for (const n of nodes) {
|
||
const dx = heroCenterX - n.x;
|
||
const dy = heroCenterY - n.y;
|
||
|
||
if (Math.abs(dx) <= thr && Math.abs(dy) <= thr) {
|
||
n.dead = true;
|
||
} else {
|
||
n.x += ~~(dx / thr);
|
||
n.y += ~~(dy / thr);
|
||
}
|
||
|
||
core.drawIcon(name, n.id, n.x, n.y, 32, 32);
|
||
}
|
||
|
||
// 过滤掉 dead 的节点并执行回调
|
||
const remainingNodes = [];
|
||
for (const n of nodes) {
|
||
if (n.dead && n.callback) {
|
||
n.callback();
|
||
}
|
||
if (!n.dead) {
|
||
remainingNodes.push(n);
|
||
}
|
||
}
|
||
this.nodes = remainingNodes;
|
||
|
||
if (this.nodes.length === 0) {
|
||
this.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
const animateHwnd = new AttractAnimate();
|
||
|
||
/** 拾取单个物品的动画 */
|
||
this.pickOneItemAnimate = function (id, x, y, callback) {
|
||
if (core.isReplaying()) return;
|
||
animateHwnd.add(id, x, y, callback);
|
||
animateHwnd.start();
|
||
};
|
||
/** 在每次切换楼层后调用 */
|
||
this.clearAttractAnimate = function () {
|
||
animateHwnd.clear();
|
||
}
|
||
|
||
/**
|
||
* 广搜,搜索可以到达的需要清的怪
|
||
* @param {string} floorId
|
||
*/
|
||
function bfs(floorId, deep = Infinity) {
|
||
core.extractBlocks(floorId);
|
||
const objs = core.getMapBlocksObj(floorId);
|
||
const bgMap = core.getBgMapArray(floorId);
|
||
const { x, y } = core.status.hero.loc;
|
||
const dir = /** @type {[direction, number, number][]} */ Object.entries(core.utils.scan).map(v => [v[0], v[1].x, v[1].y]);
|
||
const floor = core.status.maps[floorId];
|
||
|
||
/** @type {[number, number][]} */
|
||
const queue = [
|
||
[x, y]
|
||
];
|
||
const mapped = {
|
||
[`${x},${y}`]: true
|
||
};
|
||
|
||
const autoBattle = core.getFlag('autoBattle', false),
|
||
autoGet = core.getFlag('autoGet', false);
|
||
if (!autoGet && !autoBattle) return;
|
||
|
||
while (queue.length > 0 && deep > 0) {
|
||
const [nx, ny] = queue.shift();
|
||
dir.forEach(v => {
|
||
const [tx, ty] = [nx + v[1], ny + v[2]];
|
||
if (tx < 0 || ty < 0 || tx >= floor.width || ty >= floor.height) {
|
||
return;
|
||
}
|
||
const loc = `${tx},${ty}`;
|
||
if (mapped[loc]) return;
|
||
const block = objs[loc];
|
||
mapped[loc] = true;
|
||
if (core.onSki(bgMap[ty][tx])) return; // bfs不允许穿过滑冰
|
||
const { type, canGoThrough } = judge(block, nx, ny, tx, ty, v[0], floorId, autoBattle, autoGet);
|
||
if (!canGoThrough) return;
|
||
|
||
if (type === 'enemy') {
|
||
if (canBattle(block.event.id, tx, ty) && !block.disable) {
|
||
core.battle(block.event.id, tx, ty);
|
||
core.updateCheckBlock();
|
||
} else {
|
||
return;
|
||
}
|
||
} else if (type === 'item') {
|
||
const item = core.material.items[block.event.id];
|
||
if (canGetItem(item, loc, floorId)) {
|
||
if (!core.isReplaying()) animateHwnd.add(item.id, 32 * tx, 32 * ty);
|
||
core.getItem(item.id, 1, tx, ty);
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
if (hasBlockDamage(loc)) return;
|
||
queue.push([tx, ty]);
|
||
});
|
||
deep--;
|
||
}
|
||
}
|
||
|
||
function auto() {
|
||
if (!core.status.floorId || !core.status.checkBlock.damage) return; // 这两个条件不知道什么情形下会出现
|
||
if (core.status.event.id == 'action' || core.events.onSki() || core.status.lockControl) return; // 在冰上不允许触发自动清怪
|
||
const before = flags.__forbidSave__;
|
||
const { x, y } = core.status.hero.loc;
|
||
const floor = core.floors[core.status.floorId];
|
||
const loc = `${x},${y}`;
|
||
const hasEvent = has(floor.events[loc]);
|
||
if (hasEvent) return; // 如果有事件,直接不清了
|
||
const block = core.getBlock(x, y);
|
||
if (block != null && block.event.cls !== 'items') return; // 角色站的位置为空地和物品以外的图块,不清(例如箭头,可能无法返回)
|
||
|
||
let deep = Infinity;
|
||
if (hasBlockDamage(loc)) {
|
||
deep = core.flags.enableGentleClick ? 1 : 0; // 角色站的位置有地图伤害时,仍然允许轻点附近1格
|
||
}
|
||
flags.__forbidSave__ = true;
|
||
flags.__statistics__ = true;
|
||
const ctx = core.getContextByName(ctxName);
|
||
if (!ctx) {
|
||
core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 75);
|
||
core.setAlpha(ctxName, 0.6);
|
||
}
|
||
bfs(core.status.floorId, deep);
|
||
if (!core.isReplaying()) animateHwnd.start();
|
||
flags.__statistics__ = false;
|
||
flags.__forbidSave__ = before;
|
||
core.updateStatusBar();
|
||
}
|
||
},
|
||
"scrollingText": function () {
|
||
// 本插件用于绘制在线留言
|
||
// 说明:https://h5mota.com/bbs/thread/?tid=1017
|
||
// 目前使用core.http代替帖子中提到的axios
|
||
|
||
/** 塔的英文名 */
|
||
const towerName = core.firstData.name;
|
||
|
||
let [W, H] = [core.__SIZE__, core.__SIZE__];
|
||
let [WIDTH, HEIGHT] = [core.__PIXELS__, core.__PIXELS__];
|
||
|
||
//#region 弹幕的收发
|
||
this.getComment = function () {
|
||
if (core.isReplaying()) return;
|
||
let form = new FormData();
|
||
form.append('type', '1');
|
||
form.append('towername', towerName);
|
||
core.utils.http(
|
||
'POST',
|
||
'https://h5mota.com/backend/tower/barrage.php',
|
||
form,
|
||
function (res) {
|
||
try {
|
||
res = JSON.parse(res);
|
||
console.log(res);
|
||
core.drawTip('接收成功!', 'postman');
|
||
core.playSound('item.mp3');
|
||
let commentCollection = {};
|
||
const commentList = res?.list;
|
||
const isEmpty = /^\s*$/;
|
||
for (let i = 0, l = commentList.length; i <= l - 1; i++) {
|
||
if (isEmpty.test(commentList[i]?.comment)) continue;
|
||
const commentTagsList = commentList[i].tags.split(',');
|
||
const [cFloorId, cX, cY] = commentTagsList;
|
||
if (0 <= cX && cX <= W - 1 && 0 <= cY && cY <= H - 1 && core.floorIds.includes(cFloorId)) {
|
||
if (!commentCollection.hasOwnProperty(cFloorId)) { commentCollection[cFloorId] = {}; }
|
||
const str = cX + ',' + cY;
|
||
if (!commentCollection[cFloorId].hasOwnProperty(str)) { commentCollection[cFloorId][str] = []; }
|
||
commentCollection[cFloorId][str].push(commentList[i]?.comment);
|
||
}
|
||
}
|
||
core.setFlag('commentCollection', commentCollection);
|
||
} catch (err) {
|
||
core.drawFailTip('在线留言接收失败! ' + err.message, 'postman');
|
||
}
|
||
},
|
||
function (err) {
|
||
console.log(err);
|
||
if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' };
|
||
else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') };
|
||
else err = JSON.parse(err);
|
||
core.drawFailTip('在线留言接收失败! ' + err?.message, 'postman');
|
||
},
|
||
null, null, null, 1000
|
||
);
|
||
}
|
||
|
||
this.postComment = function (comment, tags) {
|
||
if (core.isReplaying()) return;
|
||
const isEmpty = /^\s*$/;
|
||
if (isEmpty.test(comment)) {
|
||
core.drawFailTip('您输入的消息为空,请重发!', 'postman');
|
||
return;
|
||
}
|
||
let form = new FormData();
|
||
form.append('type', '2');
|
||
form.append('towername', towerName);
|
||
form.append('comment', comment);
|
||
form.append('tags', tags);
|
||
core.utils.http(
|
||
'POST',
|
||
'https://h5mota.com/backend/tower/barrage.php',
|
||
form,
|
||
function (res) {
|
||
try {
|
||
res = JSON.parse(res);
|
||
console.log(res);
|
||
if (res?.code === 0) {
|
||
core.drawTip('提交成功! ', 'postman')
|
||
} else {
|
||
core.drawTip('提交失败! ' + res?.message, 'postman');
|
||
}
|
||
} catch (err) {
|
||
core.drawFailTip('提交失败! ' + err.message, 'postman');
|
||
}
|
||
},
|
||
function (err) {
|
||
console.log(err);
|
||
if (['Abort', 'Timeout', 'Error on Connection'].includes(err)) err = { message: '连接异常' };
|
||
else if (err.startsWith('HTTP ')) err = { message: '连接异常, 状态码:' + err.replace('HTTP ', '') };
|
||
else err = JSON.parse(err);
|
||
core.drawFailTip('提交失败! ' + err?.message, 'postman');
|
||
},
|
||
null, null, null, 1000
|
||
);
|
||
}
|
||
//#endregion
|
||
|
||
/** 若变量comment为真,在每层切换时在地上有弹幕的地方显示相应图标。 */
|
||
this.drawCommentSign = function () {
|
||
if (!core.hasFlag('comment') || core.isReplaying()) return;
|
||
let commentCollection = core.getFlag('commentCollection', {}),
|
||
floorId = core.status.floorId;
|
||
core.createCanvas('sign', 0, 0, WIDTH, HEIGHT, 61);
|
||
core.setOpacity('sign', 0.6);
|
||
if (commentCollection.hasOwnProperty(floorId)) {
|
||
for (let pos in commentCollection[floorId]) {
|
||
const l = commentCollection[floorId][pos].length;
|
||
for (let i = 0; i <= l - 1; i++) {
|
||
const [x, y] = pos.split(',').map(x => Number(x));
|
||
core.drawIcon('sign', 'postman', 32 * x, 32 * y);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 立即清除楼层的弹幕图标。关闭弹幕相关设置时调用。 */
|
||
this.clearCommentSign = function () {
|
||
core.deleteCanvas('sign');
|
||
}
|
||
|
||
/** 默认一次显示的弹幕数 */
|
||
const showNum = 5;
|
||
|
||
// 每走一步或瞬移,调用该函数,若目标点有弹幕,显示之
|
||
this.showComment = function (x, y) {
|
||
if (!core.getFlag('comment') || core.isReplaying()) return;
|
||
const commentCollection = core.getFlag('commentCollection', {});
|
||
const floorId = core.status.floorId,
|
||
str = x + ',' + y;
|
||
if (commentCollection.hasOwnProperty(floorId) &&
|
||
commentCollection[floorId].hasOwnProperty(str)) {
|
||
let commentArr = commentCollection[floorId][str].concat();
|
||
const commentArrPicked = pickComment(commentArr, showNum);
|
||
drawComment(commentArrPicked);
|
||
}
|
||
}
|
||
|
||
/** 返回从commentArr中挑选showNum个comment组成的数组*/
|
||
function pickComment(commentArr, showNum) {
|
||
let showList = [];
|
||
if (commentArr.length <= showNum) {
|
||
showList = commentArr;
|
||
} else {
|
||
for (let i = 0; i <= showNum - 1; i++) {
|
||
const l = commentArr.length,
|
||
n = core.plugin.dice(l - 1);
|
||
showList.push(commentArr[n]);
|
||
commentArr.splice(n, 1);
|
||
}
|
||
}
|
||
return showList;
|
||
}
|
||
|
||
function drawComment(commentArr) {
|
||
const l = commentArr.length;
|
||
let yList = generateCommentYList(20, HEIGHT - 20, showNum);
|
||
if (l < showNum) yList = getRandomElements(yList, l);
|
||
for (let i = 0; i <= l - 1; i++) {
|
||
drawCommentStr(commentArr[i], WIDTH + 20 * Math.random(),
|
||
yList[i], Math.random() * 0.1 + 0.1);
|
||
}
|
||
}
|
||
|
||
/** 生成count个随机数,范围从min到max,作为弹幕的y坐标*/
|
||
function generateCommentYList(min, max, count) {
|
||
let yList = Array(count).fill(0);
|
||
const distance = (max - min) / (count + 1);
|
||
for (let i = 0; i < count; i++) {
|
||
yList[i] = min + distance * (i + 1) + (Math.random() - 0.5) * (distance / 2);
|
||
}
|
||
return yList;
|
||
}
|
||
|
||
function getRandomElements(arr, count) {
|
||
let result = [...arr];
|
||
let len = result.length;
|
||
count = Math.min(len, count);
|
||
|
||
for (let i = len - 1; i > len - 1 - count; i--) {
|
||
let j = Math.floor(Math.random() * (i + 1));
|
||
[result[i], result[j]] = [result[j], result[i]];
|
||
}
|
||
|
||
return result.slice(len - count);
|
||
}
|
||
|
||
//#region 弹幕绘制部分
|
||
const { Animation, linear, Ticker } = core.plugin.animate ?? {};
|
||
const ctxName = 'scrollingText';
|
||
|
||
if (Ticker) {
|
||
const ticker = new Ticker();
|
||
ticker.add(() => {
|
||
if (core.isReplaying()) return;
|
||
core.createCanvas(ctxName, 0, 0, core.__PIXELS__, core.__PIXELS__, 136); //每帧重绘该画布
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绘制弹幕
|
||
* @example
|
||
* drawCommentStr('OK', 450, 200, 0.1);
|
||
* @param {string} content 弹幕的内容
|
||
* @param {number} x 弹幕的初始x坐标
|
||
* @param {number} y 弹幕的初始y坐标
|
||
* @param {number} vx 弹幕的横向滚动速度
|
||
*/
|
||
function drawCommentStr(content, x, y, vx) {
|
||
if (core.isReplaying() || !Animation) return;
|
||
const ani = new Animation();
|
||
core.plugin.tickerSet.add(ani.ticker);
|
||
ani.ticker.add(() => {
|
||
core.fillText(ctxName, content, x + ani.x, y, 'white', '16px Verdana');
|
||
})
|
||
// 弹幕的最大长度5600,再长属于异常数据
|
||
const aim = 100 + x + Math.min(core.calWidth(ctxName, content, '16px Verdana'), 5000);
|
||
ani.mode(linear())
|
||
.time(aim / vx)
|
||
.absolute()
|
||
.move(-aim, 0)
|
||
ani.all().then(() => {
|
||
ani.ticker.destroy();
|
||
});
|
||
}
|
||
//#endregion
|
||
|
||
},
|
||
"uiBaseClass": function () {
|
||
// 本插件定义了一些用于绘制的基类
|
||
/**
|
||
* @typedef {(x:number,y:number,px:number,py:number)=>void} posFunc
|
||
*/
|
||
/** 按钮基类 */
|
||
class ButtonBase {
|
||
constructor(x, y, w, h) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.w = w;
|
||
this.h = h;
|
||
this.disable = false;
|
||
this.status = 'none';
|
||
|
||
// 下面三项在initbtnMap时添加
|
||
/**
|
||
* @type {MenuBase} 所在的Menu,用于触发重绘等事件
|
||
*/
|
||
// @ts-ignore 将在菜单初始化时传入
|
||
this.menu;
|
||
/** @type {string} 所在的Menu的画布名称 */
|
||
this.ctx = '';
|
||
/** @type {string|number} 自身在所在的Menu的btnMap中的索引 */
|
||
this.key = '';
|
||
|
||
/** @type {posFunc} */
|
||
this.ondown = () => { };
|
||
/** @type {posFunc|undefined} */
|
||
this.onmove = undefined;
|
||
/** @type {posFunc|undefined} */
|
||
this.onup = undefined;
|
||
}
|
||
|
||
/** 绘制该按钮的外观
|
||
* @interface
|
||
*/
|
||
draw() { }
|
||
|
||
/** 默认为矩形判定区 */
|
||
inRange(px, py) {
|
||
return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h;
|
||
}
|
||
}
|
||
const KeyCodeEnum = {
|
||
BackSpace: 8, Tab: 9, Enter: 13, Esc: 27, SpaceBar: 32,
|
||
PageUp: 33, PageDown: 34, Left: 37, Up: 38, Right: 39,
|
||
Down: 40, C: 67, Q: 81, T: 84,
|
||
};
|
||
|
||
/** @typedef {'ondown'|'onmove'|'onup'|'keyDown'|'keyUp'|'onmousewheel'} eventType */
|
||
class MenuBase {
|
||
/**
|
||
* @param {string} name 菜单名称,作为绘制画布时的名称
|
||
* @param {eventType[]} [toListen]
|
||
* @param {number} [x]
|
||
* @param {number} [y]
|
||
* @param {number} [w]
|
||
* @param {number} [h]
|
||
* @param {number} [zIndex]
|
||
*/
|
||
constructor(name, toListen, x, y, w, h, zIndex) {
|
||
this.name = name;
|
||
/** @type {Map<string|number, ButtonBase>} 本菜单上的按钮列表,每次绘制将触发按钮的draw事件 */
|
||
this.btnMap = new Map();
|
||
/** 当前画布是否正被绘制 */
|
||
this.onDraw = false;
|
||
/** @type {Set<eventType>} 当前画布需要监听的事件类型 */
|
||
this.toListen = new Set(toListen);
|
||
|
||
this.x = x ?? 0;
|
||
this.y = y ?? 0;
|
||
this.w = w ?? core.__PIXELS__;
|
||
this.h = h ?? core.__PIXELS__;
|
||
this.zIndex = zIndex ?? 136; // 136比uievent大1
|
||
}
|
||
|
||
// #region 监听事件
|
||
/** 返回换算后的画布上的相对坐标 */
|
||
convertCoordinate(px, py) {
|
||
return [px - this.x, py - this.y];
|
||
}
|
||
|
||
/** 默认为矩形判定区 */
|
||
inRange(px, py) {
|
||
return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h;
|
||
}
|
||
|
||
ondown(x, y, rawpx, rawpy) {
|
||
if (!this.inRange(rawpx, rawpy)) return;
|
||
const [px, py] = this.convertCoordinate(rawpx, rawpy);
|
||
this.ondownEvent(x, y, px, py);
|
||
this.ondownBtnEvent(x, y, px, py);
|
||
}
|
||
|
||
/** 点击画布自身触发的事件
|
||
* @interface
|
||
*/
|
||
ondownEvent(x, y, px, py) { }
|
||
|
||
// btnMap 一个key指向一个对象 包含btn本身,对应事件,是否disable btn本身不包含event 由菜单赋予
|
||
|
||
/** 点击画布的按钮触发的事件
|
||
* @interface
|
||
*/
|
||
ondownBtnEvent(x, y, px, py) {
|
||
this.btnMap.forEach((btn) => {
|
||
if (btn.disable) return;
|
||
if (btn.inRange(px, py)) {
|
||
btn.ondown(x, y, px, py);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 屏幕被鼠标滑动或手指拖动时触发的事件
|
||
* @interface (x:number,y:number,px:number,py:number):void
|
||
*/
|
||
onmove(x, y, rawpx, rawpy) {
|
||
if (!this.inRange(rawpx, rawpy)) return;
|
||
const [px, py] = this.convertCoordinate(rawpx, rawpy);
|
||
this.onmoveEvent(x, y, px, py);
|
||
this.onmoveBtnEvent(x, y, px, py);
|
||
}
|
||
|
||
onmoveEvent(x, y, px, py) { }
|
||
|
||
onmoveBtnEvent(x, y, px, py) {
|
||
this.btnMap.forEach((btn) => {
|
||
if (btn.disable) return;
|
||
if (btn.inRange(px, py) && btn.onmove) {
|
||
btn.onmove(x, y, px, py);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 当屏幕被鼠标或手指放开时触发的事件
|
||
* @interface (x:number,y:number,px:number,py:number):void
|
||
*/
|
||
onup(x, y, rawpx, rawpy) {
|
||
if (!this.inRange(rawpx, rawpy)) return;
|
||
const [px, py] = this.convertCoordinate(rawpx, rawpy);
|
||
this.onupEvent(x, y, px, py);
|
||
this.onupBtnEvent(x, y, px, py);
|
||
}
|
||
|
||
onupEvent(x, y, px, py) { }
|
||
|
||
onupBtnEvent(x, y, px, py) {
|
||
this.btnMap.forEach((btn) => {
|
||
if (btn.disable) return;
|
||
if (btn.inRange(px, py) && btn.onup) {
|
||
btn.onup(x, y, px, py);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 按键被按下时触发的事件
|
||
* @interface (keycode:number)=>void
|
||
*/
|
||
keyDownEvent(keycode) { }
|
||
|
||
/** 按键被放开时触发的事件
|
||
* @interface (keycode:number,altkey?:boolean,fromReplay?:boolean)=>void)
|
||
*/
|
||
keyUpEvent(keycode, altkey, fromReplay) { }
|
||
|
||
/** 鼠标滚轮滚动时触发的事件
|
||
* @interface (direct:1|-1):void
|
||
*/
|
||
onmousewheelEvent(direct) { }
|
||
// #endregion
|
||
|
||
/**
|
||
* @param {string | number} key
|
||
* @param {ButtonBase} btn
|
||
* @param {posFunc | {ondown:posFunc,onmove?:posFunc,onup?:posFunc}} [event]
|
||
*/
|
||
registerBtn(key, btn, event) {
|
||
btn.menu = this;
|
||
btn.ctx = this.name;
|
||
btn.key = key;
|
||
if (event == null) { }
|
||
else if (typeof event === 'function') {
|
||
btn.ondown = event;
|
||
} else {
|
||
const { ondown, onmove, onup } = event;
|
||
btn.ondown = ondown;
|
||
btn.onmove = onmove;
|
||
btn.onup = onup;
|
||
}
|
||
this.btnMap.set(key, btn);
|
||
}
|
||
|
||
registerBtns(arr) {
|
||
arr.forEach(ele => {
|
||
const [key, btn, event] = ele;
|
||
this.registerBtn(key, btn, event);
|
||
});
|
||
}
|
||
|
||
// 创建并返回本菜单的画布
|
||
createCanvas() {
|
||
return core.createCanvas(this.name, this.x, this.y, this.w, this.h, this.zIndex);
|
||
}
|
||
|
||
drawButtonContent() {
|
||
this.btnMap.forEach((button) => {
|
||
if (!button.disable) button.draw();
|
||
})
|
||
}
|
||
|
||
drawContent() {
|
||
this.drawButtonContent();
|
||
this.onDraw = true;
|
||
}
|
||
|
||
beginListen() {
|
||
if (this.toListen.has('ondown')) core.registerAction('ondown', this.name, this.ondown.bind(this), 100);
|
||
if (this.toListen.has('keyDown')) core.registerAction('keyDown', this.name, this.keyDownEvent.bind(this), 100);
|
||
if (this.toListen.has('keyUp')) core.registerAction('keyUp', this.name, this.keyUpEvent.bind(this), 100);
|
||
if (this.toListen.has('onmove')) core.registerAction('onmove', this.name, this.onmove.bind(this), 100)
|
||
if (this.toListen.has('onup')) core.registerAction('onup', this.name, this.onup.bind(this), 100);
|
||
if (this.toListen.has('onmousewheel')) core.registerAction('onmousewheel', this.name, this.onmousewheelEvent.bind(this), 100);
|
||
}
|
||
|
||
endListen() {
|
||
core.unregisterAction('ondown', this.name);
|
||
core.unregisterAction('keyDown', this.name);
|
||
core.unregisterAction('keyUp', this.name);
|
||
core.unregisterAction('onmove', this.name);
|
||
core.unregisterAction('onup', this.name);
|
||
core.unregisterAction('onmousewheel', this.name);
|
||
}
|
||
|
||
remove() {
|
||
core.ui.deleteCanvas(this.name);
|
||
this.onDraw = false;
|
||
}
|
||
|
||
clear() {
|
||
this.endListen();
|
||
this.remove();
|
||
}
|
||
|
||
init() {
|
||
this.beginListen();
|
||
this.drawContent();
|
||
}
|
||
}
|
||
|
||
class Pagination extends MenuBase {
|
||
constructor(pageList, currPage, name, toListen, x, y, w, h, zIndex) {
|
||
super(name, toListen, x, y, w, h, zIndex);
|
||
/**
|
||
* 当前页面列表
|
||
* @type {Array<MenuBaseClass>}
|
||
*/
|
||
this.pageList = pageList;
|
||
/**
|
||
* 当前页的序号
|
||
* @type {number}
|
||
*/
|
||
this.currPage = currPage || 0;
|
||
}
|
||
|
||
initOnePage(index) {
|
||
this.currPage = index;
|
||
this.pageList[index].init();
|
||
}
|
||
|
||
changePage(num) {
|
||
if (num !== this.currPage) {
|
||
const beforeMenu = this.pageList[this.currPage];
|
||
beforeMenu.clear();
|
||
}
|
||
this.initOnePage(num);
|
||
}
|
||
|
||
pageDown() {
|
||
if (this.currPage > 0) this.changePage(this.currPage - 1);
|
||
}
|
||
|
||
pageUp() {
|
||
if (this.currPage < this.pageList.length - 1) this.changePage(this.currPage + 1);
|
||
}
|
||
|
||
clear() {
|
||
this.pageList.forEach((page) => page.clear());
|
||
super.clear();
|
||
}
|
||
}
|
||
|
||
// 圆角带文字的按钮
|
||
class RoundBtn extends ButtonBase {
|
||
constructor(x, y, w, h, text, config) {
|
||
super(x, y, w, h);
|
||
this.text = text;
|
||
this.config = config || {};
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const { x, y, w, h } = this;
|
||
const {
|
||
fillStyle = 'rgb(204, 204, 204)', strokeStyle = 'black', fontStyle = 'black',
|
||
selectedFillStyle = 'rgb(255, 51, 153)', selectedstrokeStyle = 'black', selectedFontStyle = 'white',
|
||
radius = 3, lineWidth = 1, angle = null, font = '16px Verdana'
|
||
} = this.config || {};
|
||
core.setTextAlign(ctx, 'center');
|
||
core.setTextBaseline(ctx, 'alphabetic');
|
||
if (this.status === 'selected') {
|
||
core.fillRoundRect(ctx, x, y, w, h, radius, selectedFillStyle, angle);
|
||
core.strokeRoundRect(ctx, x, y, w, h, radius, selectedstrokeStyle, lineWidth, angle);
|
||
core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, selectedFontStyle, font);
|
||
} else {
|
||
core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle);
|
||
core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle);
|
||
core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, fontStyle, font);
|
||
}
|
||
}
|
||
}
|
||
|
||
class IconBtn extends ButtonBase {
|
||
constructor(x, y, w, h, icon, config) {
|
||
super(x, y, w, h);
|
||
this.icon = icon;
|
||
this.config = config || {};
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const { x, y, w, h } = this;
|
||
const {
|
||
strokeStyle = 'black', fillStyle = 'white',
|
||
radius = 3, lineWidth = 1, angle = null, frame = 0,
|
||
iconX = x, iconY = y, iconW = w, iconH = h,
|
||
crossline1 = false, crossline2 = false, crossLineOffset = 2,
|
||
crossLineStyle = 'red', crossLineWidth = 2,
|
||
} = this.config || {};
|
||
if (fillStyle !== 'none') core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle);
|
||
if (strokeStyle !== 'none') core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle);
|
||
core.drawIcon(ctx, this.icon, iconX, iconY, iconW, iconH, frame);
|
||
if (crossline1) {
|
||
core.drawLine(ctx, x + crossLineOffset, y + crossLineOffset,
|
||
x + w - crossLineOffset, y + h - crossLineOffset,
|
||
crossLineStyle, crossLineWidth);
|
||
}
|
||
if (crossline2) {
|
||
core.drawLine(ctx, x + crossLineOffset, y + h - crossLineOffset,
|
||
x + w - crossLineOffset, y + crossLineOffset,
|
||
crossLineStyle, crossLineWidth);
|
||
}
|
||
}
|
||
}
|
||
|
||
class ExitBtn extends ButtonBase {
|
||
constructor(x, y, w, h, config) {
|
||
super(x, y, w, h);
|
||
this.config = config || {};
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const {
|
||
strokeStyle = ' #D32F2F', fillStyle = ' #EF5350', lineStyle = 'white',
|
||
radius = 3, lineOffsetX = 5, lineWidthX = 3,
|
||
} = this.config || {};
|
||
const [x, y, w, h] = [this.x, this.y, this.w, this.h];
|
||
core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle);
|
||
core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle);
|
||
core.drawLine(ctx, x + lineOffsetX, y + lineOffsetX, x + w - lineOffsetX, y + h - lineOffsetX, lineStyle, lineWidthX);
|
||
core.drawLine(ctx, x + lineOffsetX, y + h - lineOffsetX, x + w - lineOffsetX, y + lineOffsetX, lineStyle, lineWidthX);
|
||
}
|
||
}
|
||
|
||
class ArrowBtn extends ButtonBase {
|
||
constructor(x, y, w, h, dir, config) {
|
||
super(x, y, w, h);
|
||
this.config = config || {};
|
||
/** @type {'left'|'right'} */
|
||
this.dir = dir;
|
||
}
|
||
|
||
draw() {
|
||
const {
|
||
marginLeft = 6, marginTop = 5, marginRight = 4,
|
||
backStyle = 'gray', arrowStyle = 'black'
|
||
} = this.config || {};
|
||
const { x, y, w, h, ctx } = this;
|
||
core.fillRoundRect(ctx, x, y, w, h, 3, backStyle);
|
||
if (this.dir === 'left')
|
||
core.fillPolygon(ctx, [
|
||
[x + w - marginLeft, y + marginTop],
|
||
[x + w - marginLeft, y + h - marginTop],
|
||
[x + marginRight, y + h / 2]
|
||
], arrowStyle);
|
||
else if (this.dir === 'right')
|
||
core.fillPolygon(ctx, [
|
||
[x + marginLeft, y + marginTop],
|
||
[x + marginLeft, y + h - marginTop],
|
||
[x + w - marginRight, y + h / 2]
|
||
], arrowStyle);
|
||
}
|
||
}
|
||
this.uiBase = {
|
||
ButtonBase, RoundBtn, IconBtn, ExitBtn,
|
||
ArrowBtn, MenuBase, Pagination, KeyCodeEnum
|
||
};
|
||
},
|
||
"newBackpackLook": function () {
|
||
// 本插件定义了一些用于绘制的基类
|
||
|
||
let __enable = true;
|
||
if (!__enable) return;
|
||
|
||
/** @todo 尝试干掉redraw */
|
||
// #region 复写
|
||
|
||
core.ui._drawToolbox = function () { drawItemBox('all'); }.bind(core.ui);
|
||
core.ui._drawEquipbox = function () { drawItemBox('equips'); }.bind(core.ui);
|
||
core.actions._keyDownToolbox = core.actions._keyDownEquipbox = function (keyCode) { return true; }.bind(core.actions);
|
||
core.actions._clickToolbox = core.actions._clickEquipbox = function (x, y, px, py) { return true; }.bind(core.actions);
|
||
core.actions._keyUpToolbox = core.actions._keyUpEquipbox = function (keyCode) { return true; }.bind(core.actions);
|
||
// 暂不考虑修改core.status.event.id,该变量牵涉太多,作用不完全清楚
|
||
// 录像模式下下列函数会进行检测,不处于特定模式时,阻止自定义监听事件
|
||
core.actions._checkReplaying = function () {
|
||
if (core.isReplaying() && !UI._back?.onDraw &&
|
||
['save', 'book', 'book-detail', 'viewMaps', 'toolbox', 'equipbox', 'text'].indexOf(core.status.event.id) < 0) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}.bind(core.actions);
|
||
|
||
const oriClosePanel = core.ui.closePanel;
|
||
core.ui.closePanel = function () {
|
||
oriClosePanel.apply(core.ui, [arguments]);
|
||
UI.clearAll();
|
||
}
|
||
|
||
core.control._replayAction_item = function (action) {
|
||
if (action.indexOf("item:") != 0) return false;
|
||
const itemId = action.substring(5);
|
||
if (!core.canUseItem(itemId)) return false;
|
||
if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) {
|
||
core.useItem(itemId, false, core.replay);
|
||
return true;
|
||
}
|
||
core.ui._drawToolbox(0);
|
||
const itemInv = UI.itemInv;
|
||
const totalIndex = itemInv.allItemList.indexOf(itemId);
|
||
const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0);
|
||
const currIndex = totalIndex - page * itemInv.pageCap;
|
||
itemInv.page = page;
|
||
itemInv.setIndex(currIndex);
|
||
itemInv.drawContent();
|
||
setTimeout(function () {
|
||
core.ui.closePanel();
|
||
core.useItem(itemId, false, core.replay);
|
||
}, core.control.__replay_getTimeout());
|
||
return true;
|
||
}
|
||
|
||
core.control._replayAction_equip = function (action) {
|
||
if (action.indexOf("equip:") != 0) return false;
|
||
const equipId = action.substring(6);
|
||
if (!core.hasItem(equipId)) {
|
||
core.removeFlag('__doNotCheckAutoEvents__');
|
||
return false;
|
||
}
|
||
|
||
const callbackFunc = function () {
|
||
const next = core.status.replay.toReplay[0] || "";
|
||
if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) {
|
||
core.removeFlag('__doNotCheckAutoEvents__');
|
||
core.checkAutoEvents();
|
||
}
|
||
core.replay();
|
||
}
|
||
core.setFlag('__doNotCheckAutoEvents__', true);
|
||
|
||
core.status.route.push(action);
|
||
if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) {
|
||
core.loadEquip(equipId, callbackFunc);
|
||
return true;
|
||
}
|
||
core.ui._drawEquipbox(0);
|
||
const { itemId, itemInv, equipSlots } = UI;
|
||
const totalIndex = itemInv.allItemList.indexOf(itemId);
|
||
const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0);
|
||
const currIndex = totalIndex - page * itemInv.pageCap;
|
||
itemInv.page = page;
|
||
itemInv.setIndex(currIndex);
|
||
itemInv.drawContent();
|
||
equipSlots.drawContent();
|
||
setTimeout(function () {
|
||
core.ui.closePanel();
|
||
core.loadEquip(equipId, callbackFunc);
|
||
}, core.control.__replay_getTimeout());
|
||
return true;
|
||
}
|
||
|
||
core.control._replayAction_unEquip = function (action) {
|
||
if (action.indexOf("unEquip:") != 0) return false;
|
||
const equipType = parseInt(action.substring(8));
|
||
if (!core.isset(equipType)) {
|
||
core.removeFlag('__doNotCheckAutoEvents__');
|
||
return false;
|
||
}
|
||
|
||
const callback = function () {
|
||
[UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => {
|
||
if (menu && menu.onDraw) menu.drawContent();
|
||
});
|
||
core.ui.closePanel();
|
||
const next = core.status.replay.toReplay[0] || "";
|
||
if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) {
|
||
core.removeFlag('__doNotCheckAutoEvents__');
|
||
core.checkAutoEvents();
|
||
}
|
||
core.replay();
|
||
}
|
||
core.setFlag('__doNotCheckAutoEvents__', true);
|
||
|
||
core.status.route.push(action);
|
||
if (core.status.replay.speed == 24) {
|
||
core.unloadEquip(equipType, callback);
|
||
return true;
|
||
}
|
||
const { itemInv, equipSlots } = UI;
|
||
const page = Math.max(Math.ceil(equipType / itemInv.pageCap) - 1, 0);
|
||
const currIndex = equipType - page * itemInv.pageCap;
|
||
itemInv.page = page;
|
||
itemInv.setIndex(currIndex);
|
||
itemInv.drawContent();
|
||
equipSlots.drawContent();
|
||
core.ui._drawEquipbox(0);
|
||
setTimeout(function () {
|
||
core.unloadEquip(equipType, callback);
|
||
}, core.control.__replay_getTimeout());
|
||
return true;
|
||
}
|
||
core.registerReplayAction("item", core.control._replayAction_item);
|
||
core.registerReplayAction("equip", core.control._replayAction_equip);
|
||
core.registerReplayAction("unEquip", core.control._replayAction_unEquip);
|
||
|
||
// 复写control.startReplay
|
||
const oriStartReplay = core.control.startReplay; // 进入播放录像模式时清空道具栏选中目标的缓存
|
||
core.control.startReplay = function (list) {
|
||
clearItemBoxCache();
|
||
oriStartReplay.apply(core.control, [list, ...arguments]);
|
||
}
|
||
|
||
// 复写items._afterUseItem
|
||
// flag:itemsUsedCount {[itemId:string]:boolean} 成功使用了的道具的计数
|
||
const origin__afterUseItem = items.prototype._afterUseItem;
|
||
items.prototype._afterUseItem = function (itemId) {
|
||
const itemsUsedCount = core.getFlag('itemsUsedCount', {});
|
||
if (!itemsUsedCount.hasOwnProperty(itemId)) itemsUsedCount[itemId] = 0;
|
||
itemsUsedCount[itemId]++;
|
||
core.setFlag('itemsUsedCount', itemsUsedCount);
|
||
origin__afterUseItem(itemId);
|
||
}
|
||
|
||
// 复写ui.getToolboxItems
|
||
// flag:markedItems string[] 在道具栏中置顶的道具的列表
|
||
// flag:hideInfo {[itemId:string]:boolean} 手动选择了显示/隐藏的道具的列表
|
||
core.ui.getToolboxItems = function (cls, showHide, sortFunc) {
|
||
const markedItems = core.getFlag('markedItems', []);
|
||
|
||
// 暂时不采用按使用次数排序这个函数 因为导致物品排序频繁变动,反而体验不好
|
||
// const itemsUsedCount = core.getFlag('itemsUsedCount', {});
|
||
// if (!sortFunc) sortFunc = (itemId1, itemId2) => {
|
||
// const item1Count = itemsUsedCount[itemId1] || 0,
|
||
// item2Count = itemsUsedCount[itemId2] || 0;
|
||
// return item2Count - item1Count;
|
||
// }
|
||
|
||
let list = [];
|
||
if (cls === 'all') {
|
||
for (let name in core.status.hero.items) {
|
||
if (name === "equips") continue;
|
||
list = list.concat(Object.keys(core.status.hero.items[name])); // 获取'constants'和'tools'整体的列表
|
||
}
|
||
} else if (core.status.hero.items[cls]) {
|
||
list = Object.keys(core.status.hero.items[cls] || {});
|
||
}
|
||
const markedList = list.filter((itemId) => markedItems.includes(itemId)).sort(sortFunc),
|
||
unmarkedList = list.filter((itemId) => !markedItems.includes(itemId)).sort(sortFunc);
|
||
list = [...markedList, ...unmarkedList];
|
||
const hideInfo = core.getFlag('hideInfo', {});
|
||
if (!showHide) list = list.filter(function (id) {
|
||
if (hideInfo[id]) return false;
|
||
return !core.material.items[id].hideInToolbox;
|
||
})
|
||
return list;
|
||
}
|
||
|
||
// 复写resize,保证屏幕变化时此画布表现正常
|
||
const originResize = core.control.resize;
|
||
core.control.resize = function () {
|
||
originResize.apply(core.control, arguments);
|
||
const { _back, _itemInv, _equipSlots: _equipChangeBoard, _itemInfo: _itemInfoBoard } = UI;
|
||
[_back, _itemInv, _equipChangeBoard, _itemInfoBoard].forEach((menu) => { if (menu && menu.onDraw) menu.drawContent(); });
|
||
}
|
||
|
||
// #endregion
|
||
const { ButtonBase, RoundBtn, IconBtn, ExitBtn, MenuBase, KeyCodeEnum } = core.plugin.uiBase;
|
||
// #region 绘制用到的按钮类
|
||
|
||
/** 隐藏物品的按钮 */
|
||
class HideBtn extends RoundBtn {
|
||
constructor(x, y, w, h, config) {
|
||
super(x, y, w, h, '隐藏', config);
|
||
}
|
||
|
||
draw() {
|
||
const itemId = UI.itemId;
|
||
if (core.material.items[itemId]) {
|
||
const hideInfo = core.getFlag('hideInfo', {});
|
||
if (hideInfo.hasOwnProperty(itemId)) this.text = hideInfo[itemId] ? "显示" : "隐藏";
|
||
else this.text = core.material.items[itemId].hideInToolbox ? "显示" : "隐藏";
|
||
}
|
||
super.draw();
|
||
}
|
||
}
|
||
|
||
/** 置顶物品的按钮 */
|
||
class MarkBtn extends RoundBtn {
|
||
constructor(x, y, w, h, config) {
|
||
super(x, y, w, h, '置顶', config);
|
||
}
|
||
|
||
draw() {
|
||
const itemId = UI.itemId;
|
||
const markedItems = core.getFlag('markedItems', []);
|
||
this.text = markedItems.includes(itemId) ? "取消" : "置顶";
|
||
super.draw();
|
||
}
|
||
}
|
||
|
||
/** 切换到显示指定分类的物品(如:永久,消耗)的模式的按钮 */
|
||
class ClassifyBtn extends RoundBtn {
|
||
constructor(x, y, w, h, text, subType, config) {
|
||
super(x, y, w, h, text, config);
|
||
this.subType = subType;
|
||
}
|
||
|
||
draw() {
|
||
const { type, toolInv } = UI;
|
||
if (toolInv.subType === this.subType) this.status = 'selected';
|
||
else this.status = "none";
|
||
super.draw();
|
||
}
|
||
}
|
||
|
||
/** 控制是否显示隐藏物品的按钮 */
|
||
class ShowHideBtn extends ButtonBase {
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const squareSize = this.h;
|
||
core.strokeRect(ctx, this.x, this.y, squareSize, squareSize, 'black');
|
||
core.fillRect(ctx, this.x + 1, this.y + 1, squareSize - 2, squareSize - 2, 'white');
|
||
const font = core.ui._buildFont(this.h - 4);
|
||
core.setTextAlign(ctx, 'left');
|
||
core.setTextBaseline(ctx, 'middle');
|
||
if (core.hasFlag('showHideItem')) core.fillText(ctx, '√', this.x + 3, this.y + 10, 'red', font);
|
||
core.fillText(ctx, '查看隐藏', this.x + squareSize + 6, this.y + 10, 'white', font);
|
||
}
|
||
}
|
||
|
||
/** 切换道具栏和装备栏的按钮 */
|
||
class SwitchBtn extends IconBtn {
|
||
constructor(x, y, w, h, config) {
|
||
super(x, y, w, h, 'toolbox', config);
|
||
}
|
||
|
||
draw() {
|
||
this.icon = (UI.type === 'all') ? 'toolbox' : 'equipbox';
|
||
super.draw();
|
||
}
|
||
}
|
||
|
||
class NoHoverBtn extends IconBtn {
|
||
draw() {
|
||
super.draw();
|
||
const ctx = this.ctx;
|
||
const { x, y, w, h } = this;
|
||
core.drawImage(ctx, "mousewheel.png", x, y, w, h);
|
||
}
|
||
}
|
||
|
||
class ArrowBtn extends ButtonBase {
|
||
constructor(x, y, w, h, dir, config) {
|
||
super(x, y, w, h);
|
||
this.config = config || {};
|
||
/** @type {'left'|'right'} */
|
||
this.dir = dir;
|
||
}
|
||
|
||
draw() {
|
||
const {
|
||
marginLeft = 6, marginTop = 5, marginRight = 4,
|
||
backStyle = 'gray', arrowStyle = 'black'
|
||
} = this.config || {};
|
||
const ctx = this.ctx;
|
||
core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, backStyle);
|
||
if (this.dir === 'left')
|
||
core.fillPolygon(ctx, [
|
||
[this.x + this.w - marginLeft, this.y + marginTop],
|
||
[this.x + this.w - marginLeft, this.y + this.h - marginTop],
|
||
[this.x + marginRight, this.y + this.h / 2]
|
||
], arrowStyle);
|
||
else if (this.dir === 'right')
|
||
core.fillPolygon(ctx, [
|
||
[this.x + marginLeft, this.y + marginTop],
|
||
[this.x + marginLeft, this.y + this.h - marginTop],
|
||
[this.x + this.w - marginRight, this.y + this.h / 2]
|
||
], arrowStyle);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
}
|
||
|
||
/** 切换装备面板的单个装备选框 */
|
||
class EquipBox extends ButtonBase {
|
||
constructor(x, y, w, h) {
|
||
super(x, y, w, h);
|
||
/** @type {EquipSlots} */ // @ts-ignore
|
||
this.menu;
|
||
this.key = -1;
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const [x, y, w, h] = [this.x, this.y, this.w, this.h];
|
||
const space = 2,
|
||
lineWidth = 2,
|
||
squareSize = w;
|
||
const equipId = core.getEquip(this.menu.getTotalIndex(this.key));
|
||
if (equipId) core.drawIcon(ctx, equipId, x + 4, y + 4, squareSize - 8, squareSize - 8);
|
||
const color = (UI.selectType === 'equipBox' && this.menu.index === this.key) ? 'gold' : 'white';
|
||
core.strokeRect(ctx, x, y, squareSize, squareSize, color, lineWidth);
|
||
core.setTextAlign(ctx, "center");
|
||
core.setTextBaseline(ctx, "top");
|
||
const tx = x + w / 2,
|
||
ty = y + squareSize + space;
|
||
core.fillText(ctx, this.menu.currItemList[this.key], tx, ty, color, '14px Verdana');
|
||
}
|
||
}
|
||
|
||
// #endregion
|
||
// #region 绘制用到的菜单类
|
||
|
||
// 道具栏/装备栏的背景
|
||
class ItemBoxBack extends MenuBase {
|
||
constructor() {
|
||
// 装备栏和道具栏共用同一个光标,故所有按键事件全部写在这里处理
|
||
super('itemBoxBack', ['ondown', 'keyDown', 'keyUp'], null, null, null, null, 136);
|
||
}
|
||
|
||
keyDownEvent(keyCode) {
|
||
const { type, selectType, itemInv, equipSlots } = UI;
|
||
if (keyCode === KeyCodeEnum.Left) { // left
|
||
if (selectType === 'toolBox') itemInv.pageDown();
|
||
else if (selectType === 'equipBox') {
|
||
if (equipSlots.index === 0) {
|
||
equipSlots.pageDown();
|
||
} else {
|
||
equipSlots.setIndex(equipSlots.index - 1);
|
||
}
|
||
}
|
||
} else if (keyCode === KeyCodeEnum.Right) { // right
|
||
if (selectType === 'toolBox') itemInv.pageUp();
|
||
else if (selectType === 'equipBox') {
|
||
if (equipSlots.index === equipSlots.currItemList.length - 1) {
|
||
equipSlots.pageUp();
|
||
} else {
|
||
equipSlots.setIndex(equipSlots.index + 1);
|
||
}
|
||
}
|
||
} else if (keyCode === KeyCodeEnum.Up) { // up
|
||
if (selectType === 'toolBox') {
|
||
if (itemInv.index === 0) {
|
||
if (type === 'equips') { // 在仅物品栏模式下点上键到顶,切换到上一页,否则会切换到装备栏
|
||
equipSlots.setIndex(equipSlots.currItemList.length - 1);
|
||
} else {
|
||
itemInv.pageDown(); // 向上到顶将翻到上一页
|
||
}
|
||
} else {
|
||
itemInv.setIndex(itemInv.index - 1);
|
||
}
|
||
} else if (selectType === 'equipBox') {
|
||
if (equipSlots.index >= equipSlots.rowMax) {
|
||
equipSlots.index -= equipSlots.rowMax;
|
||
}
|
||
}
|
||
} else if (keyCode === KeyCodeEnum.Down) { // down
|
||
if (selectType === 'toolBox') {
|
||
if (itemInv.index < itemInv.currItemList.length - 1) {
|
||
itemInv.setIndex(itemInv.index + 1);
|
||
} else {
|
||
itemInv.pageUp(); // 向下到底将翻到下一页
|
||
}
|
||
} else if (selectType === 'equipBox') {
|
||
let newIndex = equipSlots.index + equipSlots.rowMax;
|
||
if (newIndex < equipSlots.currItemList.length - 1) {
|
||
equipSlots.setIndex(newIndex);
|
||
} else {
|
||
equipSlots.setIndex(0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
keyUpEvent(keyCode, altKey) {
|
||
const { itemId, selectType, itemInv, equipSlots: equipChangeBoard } = UI;
|
||
if (keyCode === KeyCodeEnum.Q) { // Q
|
||
if (UI.type === "equips") UI.exit();
|
||
else UI.switchType();
|
||
} else if (keyCode === KeyCodeEnum.T) { // T
|
||
if (UI.type === "all") UI.exit();
|
||
else UI.switchType();
|
||
} else if (keyCode === KeyCodeEnum.BackSpace || keyCode === KeyCodeEnum.Esc) { // BackSpace/Esc
|
||
UI.exit();
|
||
} else if (keyCode === KeyCodeEnum.Enter || keyCode === KeyCodeEnum.SpaceBar || keyCode === KeyCodeEnum.C) { // Enter/SpaceBar/C
|
||
if (selectType === "toolBox") {
|
||
if (core.material.items[itemId]) itemInv.triggerItem();
|
||
} else if (selectType === "equipBox") {
|
||
equipChangeBoard.triggerItem();
|
||
[UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => {
|
||
if (menu && menu.onDraw) menu.drawContent();
|
||
});
|
||
} else {
|
||
itemInv.setIndex(0);
|
||
}
|
||
} else if (altKey && keyCode >= 48 && keyCode <= 57) { // 都有自动切装了还有神人想要这个Alt换装 服了
|
||
core.items.quickSaveEquip(keyCode - 48);
|
||
return;
|
||
}
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
this.drawBackGround(ctx);
|
||
super.drawContent();
|
||
}
|
||
|
||
drawBackGround(ctx) {
|
||
core.strokeRoundRect(ctx, 2, 2, 412, 412, 5, 'white', 2);
|
||
core.fillRoundRect(ctx, 3, 3, 410, 410, 5, 'rgb(108, 187, 219)');
|
||
core.drawLine(ctx, 248, 3, 248, 413, 'white', 2); // 左栏和右栏的分界线
|
||
if (UI.type === 'equips') core.drawLine(ctx, 3, 140, 248, 140, 'white', 2); // 装备栏和道具栏的分界线
|
||
}
|
||
}
|
||
|
||
// 物品列表和换装界面的共用基类
|
||
class ItemListBase extends MenuBase {
|
||
constructor(name, x, y, w, h, zIndex) {
|
||
super(name, ['ondown', 'onmove'], x, y, w, h, zIndex);
|
||
/** 当前页 */
|
||
this.page = 0;
|
||
/** 一页最多可容纳的道具数量 */
|
||
this.pageCap = 0;
|
||
/** 当前最大页数,page应小于此值*/
|
||
this.pageMax = 1;
|
||
/** @type {string[]} 此界面总的物品列表 */
|
||
this.allItemList = [];
|
||
/** @type {string[]} 此界面当前页展示的物品列表 */
|
||
this.currItemList = [];
|
||
/** 当前选中了第几个道具 */
|
||
this.index = 0;
|
||
}
|
||
|
||
/**
|
||
* @abstract 获取最新的物品列表
|
||
* @returns {string[]}
|
||
*/
|
||
getItemList() { return []; }
|
||
|
||
/**
|
||
* @virtual
|
||
* @description 选中指定位置
|
||
* @param {number} index
|
||
**/
|
||
setIndex(index) { }
|
||
|
||
/**
|
||
* @abstract 尝试使用当前选中的物品
|
||
*/
|
||
triggerItem() { }
|
||
|
||
/** 更新物品列表 */
|
||
updateItemList() {
|
||
this.allItemList = this.getItemList();
|
||
this.pageMax = Math.ceil(this.allItemList.length / this.pageCap);
|
||
if (this.pageMax < 1) this.pageMax = 1;
|
||
this.currItemList = this.allItemList.slice(this.page * this.pageCap, (this.page + 1) * this.pageCap);
|
||
if (this.index >= this.currItemList.length && this.currItemList.length > 0) this.setIndex(this.currItemList.length - 1);
|
||
}
|
||
|
||
canPageUp() {
|
||
return this.page < this.pageMax - 1;
|
||
}
|
||
|
||
canPageDown() {
|
||
return this.page > 0;
|
||
}
|
||
|
||
/** 翻页,更新物品列表,并重绘该菜单自身 */
|
||
pageUp() {
|
||
if (!this.canPageUp()) return;
|
||
this.page++;
|
||
this.updateItemList();
|
||
this.setIndex(Math.min(this.index, this.currItemList.length - 1));
|
||
this.drawContent();
|
||
}
|
||
|
||
/** 翻页,更新物品列表,并重绘该菜单自身 */
|
||
pageDown() {
|
||
if (!this.canPageDown()) return;
|
||
this.page--;
|
||
this.updateItemList();
|
||
this.setIndex(this.index);
|
||
this.drawContent();
|
||
}
|
||
}
|
||
|
||
/** 展示角色当前已穿戴的装备的面板 */
|
||
class EquipSlots extends ItemListBase {
|
||
constructor(x, y, w, h, zIndex) {
|
||
super('equipChangeBoard', x, y, w, h, zIndex);
|
||
this.columnMax = 4;
|
||
this.rowMax = 2;
|
||
this.pageCap = this.columnMax * this.rowMax;
|
||
this.updateItemList();
|
||
const currNameList = this.currItemList;
|
||
|
||
const columnCount = Math.min(currNameList.length, this.columnMax), // 判断装备孔数量是否小于最大列数
|
||
rowCount = Math.min(Math.ceil(currNameList.length / this.columnMax), this.rowMax);
|
||
const [boxWidth, boxHeight] = [36, 52];
|
||
const spaceX = (this.w - columnCount * boxWidth) / (1 + columnCount),
|
||
spaceY = (this.h - rowCount * boxHeight) / (1 + rowCount);
|
||
let [xi, yi] = [spaceX, spaceY];
|
||
|
||
// 装备孔的按钮在这里注册
|
||
for (let i = 0; i < this.pageCap; i++) {
|
||
if (!this.btnMap.has(i)) {
|
||
const btn = new EquipBox(xi, yi, boxWidth, boxHeight);
|
||
this.registerBtn(i, btn, {
|
||
ondown: function () {
|
||
if (this.index !== i) {
|
||
this.setIndex(i);
|
||
} else this.triggerItem(i);
|
||
}.bind(this),
|
||
onmove: function () {
|
||
if (UI.isNoHover) return;
|
||
if (this.index !== i) {
|
||
this.setIndex(i);
|
||
}
|
||
}.bind(this),
|
||
});
|
||
if ((i >= this.currItemList.length)) btn.disable = true;
|
||
} else {
|
||
const btn = this.btnMap.get(i);
|
||
if (btn) btn.disable = (i >= this.currItemList.length);
|
||
}
|
||
if ((i + 1) % this.columnMax === 0) {
|
||
xi = spaceX;
|
||
yi += spaceY + boxHeight;
|
||
} else { xi += spaceX + boxWidth; }
|
||
}
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
if (this.pageMax > 1) {
|
||
core.setTextAlign(ctx, "center");
|
||
core.setTextBaseline(ctx, "alphabetic");
|
||
core.fillText(ctx, this.page + 1 + '/' + this.pageMax, this.w / 2, this.h - 2, 'white', '12px Verdana');
|
||
}
|
||
// 切装面板只有1页时不激活翻页按钮
|
||
if (this.allItemList.length < this.pageCap) {
|
||
const pgDown = this.btnMap.get('pgDownBtn');
|
||
const pgUp = this.btnMap.get('pgUpBtn');
|
||
if (pgDown) pgDown.disable = true;
|
||
if (pgUp) pgUp.disable = true;
|
||
}
|
||
|
||
super.drawContent();
|
||
}
|
||
|
||
/** 注意,对于装备切换面板来说,它的装备列表不是装备本身,而是角色的装备孔 */
|
||
getItemList() {
|
||
return core.status.globalAttribute.equipName;
|
||
}
|
||
|
||
/** @param {number} index 根据当前页选中的序号换算对应装备在角色装备中的总序号 */
|
||
getTotalIndex(index) {
|
||
if (index == null) index = this.index;
|
||
return this.page * this.pageCap + index;
|
||
}
|
||
|
||
/** @param {number} index */
|
||
changePageByTotalIndex(index) {
|
||
const newPage = Math.floor(index / this.pageCap);
|
||
this.page = newPage;
|
||
this.updateItemList();
|
||
}
|
||
|
||
/** 脱下指定位置的装备 */
|
||
triggerItem(index) {
|
||
const totalIndex = this.getTotalIndex(index);
|
||
if (core.status.hero.equipment[totalIndex]) {
|
||
core.unloadEquip(totalIndex);
|
||
core.status.route.push("unEquip:" + totalIndex);
|
||
this.updateItemList();
|
||
this.drawContent();
|
||
UI.itemInv.updateItemList(); //穿脱装备是双向的过程,装备栏和道具栏的物品列表组成都会变
|
||
UI.itemInv.drawContent();
|
||
UI.itemId = '';
|
||
}
|
||
}
|
||
|
||
setIndex(index) {
|
||
this.index = index;
|
||
UI.equipInv.index = -1;
|
||
core.ui.clearUIEventSelector(1);
|
||
// 被选中的装备框变色
|
||
this.btnMap.forEach((ele, key) => {
|
||
if (ele instanceof EquipBox) {
|
||
if (key === index) ele.status = 'selected';
|
||
else ele.status = 'none';
|
||
}
|
||
})
|
||
UI.selectType = 'equipBox';
|
||
const totalIndex = this.getTotalIndex(index);
|
||
UI.itemId = core.status.hero.equipment[totalIndex];
|
||
this.drawButtonContent();
|
||
}
|
||
|
||
pageUp() {
|
||
if (!this.canPageUp()) return;
|
||
this.page++;
|
||
this.updateItemList();
|
||
this.btnMap.forEach((btn, key) => {
|
||
if (btn instanceof EquipBox) {
|
||
if (Number(key) >= this.currItemList.length) btn.disable = true;
|
||
}
|
||
});
|
||
this.setIndex(Math.min(this.index, this.currItemList.length - 1));
|
||
this.drawContent();
|
||
}
|
||
|
||
pageDown() {
|
||
if (!this.canPageDown()) return;
|
||
this.page--;
|
||
this.updateItemList();
|
||
this.btnMap.forEach((btn, key) => {
|
||
if (btn instanceof EquipBox) {
|
||
if (Number(key) <= this.currItemList.length - 1) btn.disable = false;
|
||
}
|
||
});
|
||
this.setIndex(this.index);
|
||
this.drawContent();
|
||
}
|
||
}
|
||
|
||
/** 展示角色当前背包物品的面板,有道具/装备两种模式 */
|
||
class InventoryBase extends ItemListBase {
|
||
/**
|
||
* @param {*} name
|
||
* @param {string} x
|
||
* @param {number} y
|
||
* @param {number} w
|
||
* @param {number} h
|
||
* @param {number} zIndex
|
||
* @param {number} pageCap 单个页面显示的物品数
|
||
*/
|
||
constructor(name, x, y, w, h, zIndex, pageCap) {
|
||
super(name, x, y, w, h, zIndex);
|
||
|
||
/** @type {number} 单个物品占据的列宽 */
|
||
this.oneItemHeight = 30;
|
||
/** @type {number} 单个页面显示的物品数, -1是因为最后一行要留给换行按钮*/
|
||
this.pageCap = pageCap;
|
||
this.marginLeft = 15; // 物品栏左边距
|
||
this.marginTop = 40; // 物品栏上边距
|
||
this.x0 = this.x + this.marginLeft; // 物品栏左边距
|
||
this.y0 = this.y + this.marginTop;
|
||
this.w0 = this.w - this.marginLeft - 5; // 物品栏真实宽度
|
||
this.h0 = this.oneItemHeight * (this.pageCap + 1); // +1是因为最后一行要留给翻页按钮
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
const { x0, y0, w0, h0, marginTop, marginLeft} = this;
|
||
const { w, h } = this;
|
||
core.fillRect(ctx, marginLeft, marginTop, w0, h0, 'rgb(0, 105, 148)');
|
||
|
||
core.setTextBaseline(ctx, "middle");
|
||
for (let i = 0; i < this.currItemList.length; i++) {
|
||
this.drawOneItem(i);
|
||
}
|
||
core.setTextAlign(ctx, "center");
|
||
core.setTextBaseline(ctx, "alphabetic");
|
||
core.fillText(ctx, (this.page + 1) + '/' + this.pageMax, w / 2, y0 + h0 - 4, 'white', '12px Verdana');
|
||
super.drawContent();
|
||
}
|
||
|
||
/** @param {number} currIndex */
|
||
drawOneItem(currIndex) {
|
||
const itemId = this.currItemList[currIndex];
|
||
const ctx = core.dymCanvas[this.name];
|
||
const { x0, y0, marginTop } = this;
|
||
const dy = this.oneItemHeight * currIndex + marginTop;
|
||
|
||
const item = core.material.items[itemId] || {};
|
||
const num = core.formatBigNumber(core.itemCount(itemId), 5) || 0; // 道具数量过大时需要format
|
||
|
||
// 被隐藏的道具在显示时需要半透明
|
||
const hideInfo = core.getFlag('hideInfo', {});
|
||
if (item && (hideInfo.hasOwnProperty(itemId) ? hideInfo[itemId] : item.hideInToolbox)) core.setAlpha(ctx, 0.5);
|
||
|
||
// 绘制物品图标
|
||
if (core.material.items[itemId]) core.drawIcon(ctx, itemId, x0 + 4, dy + 6, 18, 18);
|
||
|
||
core.setTextAlign(ctx, "right");
|
||
core.setTextBaseline(ctx, "middle");
|
||
// 绘制物品数量 ×几
|
||
const numText = "×" + num;
|
||
core.fillText(ctx, numText, x0 + 220, dy + this.oneItemHeight / 2, 'white', '18px Verdana');
|
||
|
||
// 绘制物品名称
|
||
const markedItems = core.getFlag('markedItems', []);
|
||
const name = item.name || "???";
|
||
core.setTextAlign(ctx, "left");
|
||
core.fillText(ctx, name, x0 + 24, dy + this.oneItemHeight / 2, markedItems.includes(itemId) ? 'gold' : 'white', '18px Verdana', 180);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
|
||
clear() {
|
||
core.clearUIEventSelector(1);
|
||
super.clear();
|
||
}
|
||
|
||
/** 绘制选中物品的光标,在selectType或index改变时自动执行绘制/擦除
|
||
* @param {number} index 选中物品在当前页的序号
|
||
*/
|
||
drawSelector(index) {
|
||
const [x0, y0, w0] = [this.x0, this.y0, this.w0]; // 光标绘制是绝对坐标
|
||
core.drawUIEventSelector(1, 'winskin.png', x0, y0 + index * this.oneItemHeight, w0, this.oneItemHeight, 140);
|
||
}
|
||
|
||
/** 选中指定序号的位置,改变选中道具的ID,重绘光标
|
||
* @param {number} index 选中物品在当前页的序号
|
||
*/
|
||
setIndex(index) {
|
||
this.index = index;
|
||
if (UI.type === 'equips') {
|
||
UI.equipSlots.index = -1;
|
||
UI.equipSlots.drawButtonContent(); // 清除装备栏的选中状态
|
||
UI.selectType = 'toolBox';
|
||
}
|
||
this.drawSelector(index);
|
||
UI.itemId = this.currItemList[index];
|
||
}
|
||
|
||
ondownEvent(_x, _y, px, py) {
|
||
py -= this.marginTop; // 转换为相对于物品栏内部的坐标
|
||
const index = Math.floor(py / this.oneItemHeight);
|
||
if (index < 0 || index >= this.currItemList.length) return;
|
||
if (UI.selectType !== 'toolBox' || this.index !== index) {
|
||
this.setIndex(index);
|
||
} else {
|
||
this.triggerItem();
|
||
}
|
||
}
|
||
|
||
onmoveEvent(_x, _y, px, py) {
|
||
if (UI.isNoHover) return;
|
||
py -= this.marginTop; // 转换为相对于物品栏内部的坐标
|
||
const index = Math.floor(py / this.oneItemHeight);
|
||
if (index < 0 || index >= this.currItemList.length) return;
|
||
if (UI.selectType !== 'toolBox' || this.index !== index) {
|
||
this.setIndex(index);
|
||
}
|
||
}
|
||
}
|
||
|
||
class ToolInventory extends InventoryBase {
|
||
constructor(x, y, w, h, zIndex, pageCap) {
|
||
super('toolInventory', x, y, w, h, zIndex, pageCap);
|
||
/** @type {'all'|'tools'|'constants'} 当前显示哪个子菜单 */
|
||
this.subType = 'all';
|
||
|
||
/** 各个子页面当前选中的位置,用于在切换后显示原位置 */
|
||
this.cache = {
|
||
all: { page: 0, index: 0 },
|
||
tools: { page: 0, index: 0 },
|
||
constants: { page: 0, index: 0 }
|
||
}
|
||
}
|
||
|
||
getItemList() {
|
||
return core.getToolboxItems(this.subType, core.hasFlag('showHideItem'));
|
||
}
|
||
|
||
triggerItem() {
|
||
const itemId = UI.itemId;
|
||
if (!core.canUseItem(itemId) && itemId !== 'centerFly') {
|
||
core.drawFailTip("当前无法使用" + core.material.items[itemId].name, itemId);
|
||
return;
|
||
}
|
||
UI.clearAll();
|
||
setTimeout(() => {
|
||
core.unlockControl();
|
||
core.tryUseItem(itemId);
|
||
}, 0);
|
||
}
|
||
|
||
/** 物品栏仅显示指定类型物品 */
|
||
classify(subType) {
|
||
if (UI.type !== 'all') {
|
||
// 非物品栏类型的菜单不支持分类
|
||
return;
|
||
}
|
||
if (this.subType !== subType) {
|
||
const oldConfig = this.cache[this.subType],
|
||
newConfig = this.cache[subType];
|
||
oldConfig.page = this.page;
|
||
oldConfig.index = this.index;
|
||
this.page = newConfig.page;
|
||
this.index = newConfig.index;
|
||
this.subType = subType;
|
||
this.updateItemList();
|
||
this.setIndex(this.index);
|
||
this.drawContent();
|
||
UI.back.drawContent(); // 切换物品栏类型时需要重绘背景
|
||
}
|
||
}
|
||
}
|
||
|
||
class EquipInventory extends InventoryBase {
|
||
getItemList() {
|
||
return core.getToolboxItems('equips', core.hasFlag('showHideItem'));
|
||
}
|
||
|
||
triggerItem() {
|
||
const equip = UI.itemId;
|
||
if (!core.canEquip(equip, true)) return;
|
||
const equipPos = core.getEquipTypeById(equip);
|
||
UI.equipSlots.changePageByTotalIndex(equipPos);
|
||
core.loadEquip(equip);
|
||
core.status.route.push("equip:" + equip);
|
||
this.updateItemList(); // 穿上装备会导致道具数量变化,需要重新生成装备列表
|
||
this.setIndex(this.index);
|
||
this.drawContent();
|
||
UI.equipSlots.drawContent();
|
||
}
|
||
}
|
||
|
||
class ItemInfoBoard extends MenuBase {
|
||
constructor(x, y, w, h) {
|
||
super('itemInfoBox', ['ondown'], x, y, w, h, 137);
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.strokeRoundRect(ctx, 23, 27, 32, 32, 2, 'white', 2);
|
||
const itemId = UI.itemId;
|
||
if (itemId) core.drawIcon(ctx, itemId, 24, 28, 30, 30);
|
||
|
||
// 修改这里可以编辑未选中道具时的默认值
|
||
const defaultItem = { cls: "constants", name: "无道具", id: "-", text: "没有道具最永久" };
|
||
const defaultEquip = { cls: "equips", name: "无装备", id: "-", text: "一无所有,又何尝不是一种装备", equip: { type: "装备" } };
|
||
let item = core.material.items[itemId];
|
||
if (!item) item = (UI.type === 'all' ? defaultItem : defaultEquip);
|
||
core.setTextAlign(ctx, "left");
|
||
core.setTextBaseline(ctx, "middle");
|
||
core.fillText(ctx, item.name, 66, 46, 'black', 'bold 18px Verdana', 98); // 物品名字 e.g.护符
|
||
core.fillText(ctx, "类型", 20, 75, 'crimson', '14px Verdana');
|
||
core.fillText(ctx, "【" + getItemClsName(item) + "】", 50, 75, 'rgb(47, 49, 54)', '14px Verdana'); // 物品类型 e.g.【永久道具】
|
||
|
||
core.fillText(ctx, "ID", 20, 95, 'crimson', '14px Verdana');
|
||
core.fillText(ctx, item.id, 50, 95, 'rgb(47, 49, 54)', '14px Verdana');
|
||
|
||
if (UI.type === 'all') { // 显示物品累计使用的次数,将作为排序依据
|
||
core.fillText(ctx, "累计使用", 20, 113, 'crimson', '14px Verdana');
|
||
const itemsUsedCount = core.getFlag('itemsUsedCount', {});
|
||
core.fillText(ctx, itemsUsedCount[itemId] || 0, 80, 113, 'rgb(47, 49, 54)', '14px Verdana');
|
||
}
|
||
const rawItemText = core.replaceText(item.text) ?? "";
|
||
const itemText = rawItemText + ((UI.type === "equips") ? this.getEquipCompareInfo(item) : ""); // 物品描述信息
|
||
core.drawTextContent(ctx, itemText, {
|
||
left: 20,
|
||
top: 125,
|
||
bold: false,
|
||
color: "black",
|
||
align: "left",
|
||
fontSize: 15,
|
||
maxWidth: 150
|
||
});
|
||
const currItemHotKey = HotkeySelect.getHotkeyNum(itemId);
|
||
// 获取快捷键设置按钮当前的图标
|
||
|
||
const setHotkeyBtn = /** @type {IconBtnClass} */ (this.btnMap.get('setHotkeyBtn'));
|
||
if (setHotkeyBtn) {
|
||
setHotkeyBtn.disable = (UI.type === 'equips');
|
||
setHotkeyBtn.icon = (currItemHotKey == null) ? 'keyboard' : ('btn' + currItemHotKey);
|
||
}
|
||
super.drawContent();
|
||
}
|
||
|
||
/*** @param {Item} item */
|
||
getEquipCompareInfo(item) {
|
||
let str = '';
|
||
if (UI.type !== "equips") return str;
|
||
|
||
let equipType = item.equip?.type;
|
||
if (!equipType) return str;
|
||
if (typeof equipType == "string") equipType = core.getEquipTypeByName(equipType);
|
||
let compare;
|
||
/** @todo 准备卸下装备时显示卸下的比较信息 */
|
||
if (UI.selectType == "equipBox") compare = core.compareEquipment(null, item.id);
|
||
else compare = core.compareEquipment(item.id, core.getEquip(equipType));
|
||
// --- 变化值...
|
||
for (const name in core.status.hero) {
|
||
if (typeof core.status.hero[name] != 'number') continue;
|
||
let nowValue = core.getRealStatus(name);
|
||
let newValue = Math.floor((core.getStatus(name) + (compare.value[name] || 0)) *
|
||
(core.getBuff(name) * 100 + (compare.percentage[name] || 0)) / 100);
|
||
if (nowValue === newValue) continue;
|
||
const color = newValue > nowValue ? '#00FF00' : '#FF0000';
|
||
const [nowValueStr, newValueStr] = [nowValue, newValue].map(value => core.formatBigNumber(value));
|
||
str += "\n" + core.getStatusLabel(name) + " " + nowValueStr + "->\r[" + color + "]" + newValueStr + "\r";
|
||
}
|
||
return str;
|
||
}
|
||
}
|
||
|
||
/** 取消设置快捷键的按钮 */
|
||
class SetNullBtn extends ButtonBase {
|
||
draw() {
|
||
const [x, y, w, h] = [this.x, this.y, this.w, this.h];
|
||
core.strokeRect(this.ctx, x, y, w, h, 'red', 2);
|
||
core.drawLine(this.ctx, x, y, x + w, y + h, 'red', 2);
|
||
}
|
||
}
|
||
|
||
// 为当前道具设定一个快捷键
|
||
class HotkeySelect extends MenuBase {
|
||
constructor(itemId, x, y, w, h, zIndex) {
|
||
super('hotkeySelect', ['ondown', 'keyDown'], x, y, w, h, zIndex);
|
||
this.itemId = itemId;
|
||
/** @type {number | null} null代表当前道具没有快捷键 */
|
||
this.hotkeyNum = HotkeySelect.getHotkeyNum(this.itemId);
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
const [x, y, w, h] = [this.x, this.y, this.w, this.h];
|
||
core.fillRect(ctx, 3, 3, w - 6, h - 6, ' #A8CABA');
|
||
core.strokeRect(ctx, 0, 0, w, h, ' #004B23', 3);
|
||
core.setTextAlign(ctx, 'center');
|
||
core.setTextBaseline(ctx, 'alphabetic');
|
||
core.fillText(ctx, '为当前道具选择一个快捷键', this.w / 2, 40, 'black', '20px Verdana');
|
||
core.fillText(ctx, '无自定义设置时样板的默认快捷键', this.w / 2, 60, 'gray', '16px Verdana');
|
||
core.fillText(ctx, '(如123分别对应破炸飞)不可在此设置', this.w / 2, 80, 'gray', '16px Verdana');
|
||
// 无自定义设置时样板的默认快捷键(如123分别对应破炸飞),不可在此设置
|
||
// 绘制指向当前快捷键的监听
|
||
if (this.hotkeyNum != null) {
|
||
const btn = this.btnMap.get('btn' + this.hotkeyNum);
|
||
if (btn) {
|
||
core.fillPolygon(ctx, [
|
||
[btn.x + 12, btn.y + btn.h + 10],
|
||
[btn.x + btn.w - 12, btn.y + btn.h + 10],
|
||
[btn.x + btn.w / 2, btn.y + btn.h + 2]
|
||
], 'black');
|
||
}
|
||
}
|
||
super.drawContent();
|
||
}
|
||
/* @__PURE__ */
|
||
static getHotkeyNum(itemId) {
|
||
for (let i = 1; i <= 9; i++) {
|
||
const currHotkey = core.getLocalStorage('hotkey' + i, null);
|
||
if (currHotkey === itemId) {
|
||
return i;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
deleteHotkey() {
|
||
if (this.hotkeyNum != null) core.setLocalStorage('hotkey' + this.hotkeyNum, null);
|
||
}
|
||
|
||
setHotkey(num) {
|
||
this.deleteHotkey();
|
||
core.setLocalStorage('hotkey' + num, this.itemId);
|
||
this.hotkeyNum = Number(num);
|
||
}
|
||
|
||
clear() {
|
||
super.clear();
|
||
const { back, itemInv, equipSlots, itemInfo } = UI;
|
||
[back, itemInv, equipSlots, itemInfo].forEach(menu => {
|
||
if (menu.onDraw) menu.beginListen(); // 注意本来就没在绘制的则不监听
|
||
});
|
||
UI.itemInfo.drawContent(); // 快捷键图标会发生变化
|
||
}
|
||
}
|
||
|
||
/** @param {string} itemId */
|
||
function hotkeySelectFactory(itemId) {
|
||
const hotkeySelect = new HotkeySelect(itemId, 60, 100, 296, 206, 141); // 应当比物品背包的选择光标大,遮盖住前者
|
||
const setHotkeyNum = function (i) {
|
||
const num = this.key.replace('btn', '');
|
||
hotkeySelect.setHotkey(num);
|
||
hotkeySelect.clear();
|
||
UI.itemInfo.drawContent();
|
||
}
|
||
|
||
const [btnSize, btnInterval] = [32, 20];
|
||
const leftMargin = hotkeySelect.w / 2 - 2.5 * btnSize - 2 * btnInterval;
|
||
const style = { strokeStyle: 'none', fillStyle: 'none' };
|
||
|
||
for (let i = 0; i < 9; i++) {
|
||
const num = i + 1;
|
||
const row = (i <= 4) ? 1 : 2;
|
||
let btn;
|
||
if (row === 1) btn = new IconBtn(leftMargin + i * (btnSize + btnInterval), 100, btnSize, btnSize, 'btn' + num, style);
|
||
else btn = new IconBtn(leftMargin + (i - 5) * (btnSize + btnInterval), 150, btnSize, btnSize, 'btn' + num, style);
|
||
hotkeySelect.registerBtn('btn' + num, btn, () => setHotkeyNum.call(btn, i));
|
||
}
|
||
const setNullBtn = new SetNullBtn(leftMargin + 4 * (btnSize + btnInterval), 150, btnSize, btnSize);
|
||
hotkeySelect.registerBtn('setNullBtn', setNullBtn, () => {
|
||
hotkeySelect.deleteHotkey();
|
||
hotkeySelect.clear();
|
||
UI.itemInfo.drawContent();
|
||
});
|
||
const exitBtn = new ExitBtn(274, 5, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 });
|
||
hotkeySelect.registerBtn('exitBtn', exitBtn, () => hotkeySelect.clear());
|
||
return hotkeySelect;
|
||
}
|
||
|
||
// #endregion
|
||
// #region 核心功能函数和全局变量
|
||
function getItemClsName(item) {
|
||
const itemClsName = {
|
||
"constants": "永久道具",
|
||
"tools": "消耗道具",
|
||
}
|
||
if (item == null) return "未知";
|
||
if (item.cls == "equips") {
|
||
if (typeof item.equip.type == "string") return item.equip.type;
|
||
const type = core.getEquipTypeById(item.id);
|
||
return core.status.globalAttribute.equipName[type];
|
||
} else return itemClsName[item.cls] || item.cls;
|
||
}
|
||
|
||
function clearItemBoxCache() {
|
||
UI._itemId = '';
|
||
UI.selectType = 'toolBox';
|
||
[UI._toolInv, UI._equipInv, UI._equipSlots].forEach((menu) => {
|
||
if (menu) menu.index = 0;
|
||
});
|
||
} // 每次存读档,及进行录像回放时调用,清空之前选中的道具信息
|
||
this.clearItemBoxCache = clearItemBoxCache;
|
||
|
||
// 以下是本插件范围内的全局变量
|
||
|
||
const UI = {
|
||
/** 当前打开的是道具页还是装备页
|
||
* @type {'all'|'equips'}
|
||
**/
|
||
type: 'all',
|
||
/** @type {'toolBox'|'equipBox'} 当前选中了哪一个子界面(切装面板/物品栏) */
|
||
selectType: 'toolBox',
|
||
_itemId: '',
|
||
/** @type {string} 当前选中的物品ID,变化时自动触发右侧栏的信息变化 */
|
||
get itemId() {
|
||
return this._itemId;
|
||
},
|
||
set itemId(value) {
|
||
if (this._itemId !== value) {
|
||
this._itemId = value;
|
||
this.itemInfo.drawContent();
|
||
}
|
||
},
|
||
/** @type {undefined|ItemBoxBack} 物品页面的背景 */
|
||
_back: undefined,
|
||
/** @type {undefined|ToolInventory} 道具背包 */
|
||
_toolInv: undefined,
|
||
/** @type {undefined|EquipInventory} 装备背包 */
|
||
_equipInv: undefined,
|
||
/** @type {undefined|ItemInfoBoard} 右侧显示选中物品详细信息的页面 */
|
||
_itemInfo: undefined,
|
||
/** @type {undefined|EquipSlots} 显示已穿戴装备的面板 */
|
||
_equipSlots: undefined,
|
||
/** 是否监听onMove事件 */
|
||
isNoHover: core.getLocalStorage('itemBoxNoHover'),
|
||
/** 物品页面的背景 */
|
||
get back() {
|
||
if (!this._back) {
|
||
this._back = new ItemBoxBack();
|
||
const switchModeBtn = new SwitchBtn(385, 5, 24, 24, { strokeStyle: ' #8B4513', fillStyle: ' #D2691E' });
|
||
const exitBtn = new ExitBtn(385, 385, 24, 24);
|
||
this._back.registerBtns([['exitBtn', exitBtn, UI.exit.bind(UI)],
|
||
['switchModeBtn', switchModeBtn, UI.switchType.bind(UI)]]);
|
||
}
|
||
return this._back;
|
||
},
|
||
/** 道具背包 */
|
||
get toolInv() {
|
||
if (!this._toolInv) {
|
||
// @todo 扩容,toolInv范围包括背景,而不只是道具栏本身
|
||
// this._toolInv = new ToolInventory(15, 40, 225, 360, 137);
|
||
this._toolInv = new ToolInventory(0, 0, 240, 416, 137, 11);
|
||
const pgDown = new ArrowBtn(20, 375, 20, 20, 'left');
|
||
const pgUp = new ArrowBtn(215, 375, 20, 20, 'right');
|
||
this.toolInv.registerBtns([
|
||
['pgDownBtn', pgDown, () => UI.toolInv.pageDown()],
|
||
['pgUpBtn', pgUp, () => UI.toolInv.pageUp()]
|
||
]);
|
||
const allBtn = new ClassifyBtn(20, 10, 44, 24, "全部", "all"),
|
||
toolsBtn = new ClassifyBtn(80, 10, 44, 24, "消耗", "tools"),
|
||
constantsBtn = new ClassifyBtn(140, 10, 44, 24, "永久", "constants");
|
||
const noHoverBtn = new NoHoverBtn(210, 10, 24, 24, null, { crossline1: this.isNoHover });
|
||
const noHover = () => {
|
||
UI.isNoHover = !UI.isNoHover;
|
||
core.setLocalStorage('itemBoxNoHover', UI.isNoHover);
|
||
noHoverBtn.config.crossline1 = UI.isNoHover;
|
||
UI._toolInv?.drawButtonContent();
|
||
}
|
||
this._toolInv.registerBtns([
|
||
['allBtn', allBtn, () => this.toolInv.classify('all')],
|
||
['toolsBtn', toolsBtn, () => this.toolInv.classify('tools')],
|
||
['constantsBtn', constantsBtn, () => this.toolInv.classify('constants')],
|
||
['noHoverBtn', noHoverBtn, noHover]
|
||
]);
|
||
if (!core.platform.isPC) noHoverBtn.disable = true; // 非PC端本来就触发不了onmove事件,故隐藏此按钮
|
||
}
|
||
return this._toolInv;
|
||
},
|
||
/** 装备背包 */
|
||
get equipInv() {
|
||
if (!this._equipInv) {
|
||
// this._equipInv = new EquipInventory(15, 160, 225, 240, 137);
|
||
this._equipInv = new EquipInventory('equipInventory', 0, 120, 240, 416, 137, 7);
|
||
const pgDown = new ArrowBtn(20, 255, 20, 20, 'left');
|
||
const pgUp = new ArrowBtn(215, 255, 20, 20, 'right');;
|
||
this._equipInv.registerBtn('pgDownBtn', pgDown, () => UI.equipInv.pageDown());
|
||
this._equipInv.registerBtn('pgUpBtn', pgUp, () => UI.equipInv.pageUp());
|
||
}
|
||
return this._equipInv;
|
||
},
|
||
/** 物品背包 */
|
||
get itemInv() {
|
||
return (this.type === 'all') ? this.toolInv : this.equipInv;
|
||
},
|
||
/** 右侧显示选中物品详细信息的页面 */
|
||
get itemInfo() {
|
||
if (!this._itemInfo) {
|
||
this._itemInfo = new ItemInfoBoard(240, 0, core.__PIXELS__ - 240, core.__PIXELS__);
|
||
const hideBtn = new HideBtn(20, 380, 46, 24);
|
||
this._itemInfo.registerBtn('hideBtn', hideBtn, () => {
|
||
this.hideItem(UI.itemId);
|
||
this.itemInv.updateItemList();
|
||
this.itemInv.setIndex(this.itemInv.index);
|
||
this.itemInv.drawContent();
|
||
});
|
||
const markBtn = new MarkBtn(80, 380, 46, 24);
|
||
this._itemInfo.registerBtn('markBtn', markBtn, () => {
|
||
this.markItem(UI.itemId);
|
||
this.itemInv.updateItemList();
|
||
this.itemInv.setIndex(this.itemInv.index);
|
||
this.itemInv.drawContent();
|
||
});
|
||
const showHideBtn = new ShowHideBtn(20, 350, 95, 18);
|
||
this._itemInfo.registerBtn('showHideBtn', showHideBtn, () => {
|
||
this.switchShowHide();
|
||
this.itemInv.updateItemList();
|
||
this.itemInv.setIndex(this.itemInv.index);
|
||
this.itemInv.drawContent();
|
||
this.itemInfo.drawContent(); // 这里不论选中物品是否变化,itemInfo必定要重绘,因为按钮图案会变
|
||
});
|
||
const setHotkeyBtn = new IconBtn(145, 60, 24, 24, 'keyboard');
|
||
this.itemInfo.registerBtn('setHotkeyBtn', setHotkeyBtn, () => {
|
||
if (!UI.itemId) return;
|
||
[UI._back, UI.itemInv, UI._equipSlots, UI._itemInfo].forEach((menu) => {
|
||
if (menu) menu.endListen();
|
||
});
|
||
const hotkeySelect = hotkeySelectFactory(UI.itemId);
|
||
hotkeySelect.init();
|
||
});
|
||
}
|
||
return this._itemInfo;
|
||
},
|
||
get equipSlots() {
|
||
if (!this._equipSlots) {
|
||
this._equipSlots = new EquipSlots(7, 10, 240, 125, 137);
|
||
const config = { marginLeft: 4, marginTop: 3, marginRight: 2 };
|
||
const pgDown = new ArrowBtn(0, 56, 14, 14, 'left', config);
|
||
const pgUp = new ArrowBtn(224, 56, 14, 14, 'right', config);
|
||
this._equipSlots.registerBtn('pgDownBtn', pgDown, () => UI.equipSlots.pageDown());
|
||
this._equipSlots.registerBtn('pgUpBtn', pgUp, () => UI.equipSlots.pageUp());
|
||
}
|
||
return this._equipSlots;
|
||
},
|
||
/** 清空各个菜单的绘制和监听(不包括解除锁定) */
|
||
clearAll() {
|
||
[this._back, this._toolInv, this._equipInv, this._itemInfo, this._equipSlots].forEach((menu) => {
|
||
if (menu) menu.clear();
|
||
});
|
||
core.status.event.id = null;
|
||
},
|
||
initAll() {
|
||
[UI.back, UI.itemInv, UI.itemInfo].forEach((menu) => menu.init());
|
||
if (UI.type === 'equips') UI.equipSlots.init();
|
||
},
|
||
/** 解除锁定,退出道具栏/装备栏 */
|
||
exit() {
|
||
this.clearAll();
|
||
setTimeout(core.unlockControl, 0);
|
||
},
|
||
/** 在道具栏/装备栏模式中切换 */
|
||
switchType() {
|
||
this.type = (this.type === 'all') ? 'equips' : 'all';
|
||
if (this.type === 'all') {
|
||
this.equipSlots.clear();
|
||
this.equipInv.clear();
|
||
} else if (this.type === 'equips') {
|
||
this.toolInv.clear();
|
||
this.equipSlots.init();
|
||
}
|
||
this.itemInv.updateItemList();
|
||
this.back.drawContent();
|
||
this.itemInv.init();
|
||
if (this.type === 'all') {
|
||
this.itemInv.setIndex(this.itemInv.index);
|
||
}
|
||
else if (this.type === 'equips') {
|
||
if (UI.selectType === 'toolBox') {
|
||
this.itemInv.setIndex(this.itemInv.index);
|
||
}
|
||
else {
|
||
this.equipSlots.setIndex(this.equipSlots.index);
|
||
}
|
||
}
|
||
},
|
||
/** 隐藏 | 取消隐藏当前选中的物品 */
|
||
hideItem(itemId) {
|
||
if (!itemId) return;
|
||
let hideInfo = core.getFlag('hideInfo', {});
|
||
if (hideInfo.hasOwnProperty(itemId)) {
|
||
hideInfo[itemId] = !hideInfo[itemId];
|
||
} else {
|
||
hideInfo[itemId] = !core.material.items[itemId].hideInToolbox;
|
||
}
|
||
core.setFlag('hideInfo', hideInfo);
|
||
},
|
||
/** 置顶 | 取消置顶当前选中的物品 */
|
||
markItem(itemId) {
|
||
let markedItems = core.getFlag('markedItems', []);
|
||
if (markedItems.includes(itemId)) markedItems = markedItems.filter((currId) => currId !== itemId);
|
||
else markedItems.push(itemId);
|
||
core.setFlag('markedItems', markedItems);
|
||
},
|
||
/** 切入/切出显示已隐藏物品的模式 */
|
||
switchShowHide() {
|
||
core.setFlag('showHideItem', !core.getFlag('showHideItem', false));
|
||
}
|
||
}
|
||
|
||
// 程序入口,在_drawToolbox 与 _drawEquipbox处调用
|
||
/** @param {'all'|'equips'} currType */
|
||
function drawItemBox(currType) {
|
||
UI.clearAll();
|
||
if (UI._toolInv && UI._toolInv?.onDraw && currType === UI.type) return;
|
||
core.lockControl();
|
||
UI.type = currType;
|
||
UI.itemInv.updateItemList();
|
||
UI.itemInv.setIndex(UI.itemInv.index);
|
||
UI.initAll();
|
||
}
|
||
// #endregion
|
||
},
|
||
"autoChangeEquip": function () {
|
||
// 调用方法:在合适的位置调用函数figureEquip即可,例如在脚本编辑-按键处理加入case 89: core.plugin.figureEquip(); break;
|
||
// 即按Y键进入切装模式
|
||
|
||
let compareMode = false;
|
||
let equipStatus = [];
|
||
let equipIncluded;
|
||
|
||
////// 请在[]中填好不参与换装的装备孔的序号。
|
||
// 例如,0号,4号装备孔不参与换装,则 ignoreList 应设为[0,4]
|
||
// 所有装备孔都参与换装,则 ignoreList 应设为[]
|
||
let ignoreList = [];
|
||
|
||
////// 请在{}中根据装备的穿脱事件手动填写装备穿脱时要执行的函数,没有则不填。只填写有效的数值变化即可。
|
||
// 例如:{'sword3':{'equip':function(){core.setFlag('mms3',1);},'unequip':function(){core.setFlag('mms3',0);}}}
|
||
let equipEvents = {};
|
||
|
||
function compareEquip() {
|
||
|
||
return new Promise(function (res) {
|
||
|
||
const canvas = 'compareEquip',
|
||
width = core._PX_ || core.__PIXELS__,
|
||
height = core._PY_ || core.__PIXELS__;
|
||
|
||
core.lockControl();
|
||
|
||
function finish() {
|
||
compareMode = false;
|
||
core.unregisterAction('onclick', 'bestEquip');
|
||
core.deleteCanvas(canvas);
|
||
res(void 0);
|
||
}
|
||
|
||
core.createCanvas(canvas, 0, 0, width, height, 160);
|
||
core.setTextAlign(canvas, 'center');
|
||
core.fillText(canvas, '点击选择一个怪物,点击非怪物图块自动退出', width / 2, 20, 'red', '18px Arial');
|
||
|
||
core.registerAction('onclick', 'bestEquip', function (x, y, px, py) {
|
||
const cls = core.getBlockCls(x, y),
|
||
id = core.getBlockId(x, y);
|
||
if (!(cls === 'enemys' || cls === "enemy48")) {
|
||
finish();
|
||
return false;
|
||
}
|
||
figureBestEquip(id, x, y);
|
||
core.updateDamage();
|
||
finish();
|
||
}, 100);
|
||
})
|
||
}
|
||
|
||
function figureBestEquip(id, x, y) {
|
||
compareMode = true;
|
||
const equipNum = core.status.globalAttribute.equipName.length; // 装备总数量
|
||
|
||
// 角色初始各项数值,用于推算出最优切装后复原初始状态
|
||
const oriEffect = {
|
||
'value': { 'atk': core.status.hero.atk, 'def': core.status.hero.def, 'mdef': core.status.hero.mdef, },
|
||
'percentage': { 'atk': core.getBuff('atk'), 'def': core.getBuff('def'), 'mdef': core.getBuff('mdef'), },
|
||
'equipment': core.clone(core.status.hero.equipment),
|
||
}
|
||
|
||
if (!equipIncluded) equipIncluded = getEquipIncluded(equipNum);
|
||
|
||
const equipIncludedNum = equipIncluded.length;
|
||
const equipNameList = core.status.globalAttribute.equipName.filter((ele, i) => { return !ignoreList.includes(i); });
|
||
|
||
equipStatus = equipIncluded.map((ele) => core.getEquip(ele)); //当前参与计算的各个装备孔的装备
|
||
const equipOwned = getEquipOwned(equipNum);
|
||
let equipList = getEquipList(equipIncludedNum, equipOwned, equipNameList);
|
||
|
||
const equipCombination = traverseSetCombinations(equipList);
|
||
|
||
const bestCombination = findBestEquipComb(equipCombination, equipOwned, id, x, y);
|
||
|
||
['atk', 'def', 'mdef'].forEach((ele) => {
|
||
core.setStatus(ele, oriEffect.value[ele]);
|
||
core.setBuff(ele, oriEffect.percentage[ele]);
|
||
});
|
||
core.status.hero.equipment = core.clone(oriEffect.equipment);
|
||
|
||
equipBestComb(bestCombination, equipIncluded, equipNameList);
|
||
}
|
||
|
||
// 返回一个包含所有参与切装计算的装备孔的序号的数组。
|
||
// 例如,0,2,4号装备孔参与切装计算,则本函数返回[0,2,4]
|
||
function getEquipIncluded(equipNum) {
|
||
let equipIncluded = [];
|
||
for (let i = 0; i < equipNum; i++) {
|
||
if (!ignoreList.includes(i)) equipIncluded.push(i);
|
||
}
|
||
return equipIncluded;
|
||
}
|
||
|
||
function getEquipOwned(equipNum) {
|
||
// equipOwned:当前拥有的所有装备的数量
|
||
// 形如{sword1: 2, sword2: 1}
|
||
let equipOwned = core.clone(core.status.hero.items.equips);
|
||
for (let i = 0; i < equipNum; i++) {
|
||
if (ignoreList.includes(i)) continue;
|
||
const currEquip = core.getEquip(i);
|
||
if (currEquip !== null)
|
||
if (equipOwned.hasOwnProperty(currEquip)) equipOwned[currEquip]++;
|
||
else equipOwned[currEquip] = 1;
|
||
}
|
||
return equipOwned;
|
||
}
|
||
|
||
// 生成切装列表,为一个二维数组
|
||
function getEquipList(equipNum, equipOwned, equipNameList) {
|
||
// equipNameList:计入切装计算的装备格子的名称列表,可重复
|
||
// 形如['武器', '武器', '盾牌']
|
||
let equipList = Array(equipNameList.length).fill(void 0).map(() => new Set([null]));
|
||
|
||
//对每个装备孔展开
|
||
for (let i = 0, l = equipNameList.length; i < l; i++) {
|
||
for (let j in equipOwned) {
|
||
let equipType = core.material.items[j].equip.type;
|
||
switch (typeof equipType) {
|
||
case 'number':
|
||
for (let k = 0, l = equipOwned[j]; k < l; k++) {
|
||
equipList[equipIncluded.indexOf(equipType)].add(j);
|
||
}
|
||
break;
|
||
case 'string':
|
||
if (equipType === equipNameList[i])
|
||
for (let k = 0, l = equipOwned[j]; k < l; k++) { equipList[i].add(j); }
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return equipList;
|
||
}
|
||
|
||
function traverseSetCombinations(arr) {
|
||
const result = [];
|
||
const currentCombination = [];
|
||
|
||
function backtrack(index) {
|
||
if (index === arr.length) {
|
||
result.push([...currentCombination]);
|
||
return;
|
||
}
|
||
const currentSet = Array.from(arr[index]);
|
||
for (let value of currentSet) {
|
||
currentCombination[index] = value;
|
||
backtrack(index + 1);
|
||
}
|
||
}
|
||
backtrack(0);
|
||
return result;
|
||
}
|
||
|
||
function getEleCount(ele, arr) {
|
||
let count = 0;
|
||
for (let i = 0, l = arr.length; i < l; i++) {
|
||
if (arr[i] === ele) count++;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
function hasEnoughEquip(currComb, equipOwned) {
|
||
for (let i in equipOwned) {
|
||
const equipNeed = getEleCount(i, currComb);
|
||
if (equipOwned[i] < equipNeed) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 按照给定的列表aimStatus,形如['sword1','sword2',null,'sword1'],修改equipStatus进行模拟切装
|
||
function simulateEquip(equipStatus, aimStatus) {
|
||
equipIncluded.forEach((ele, i) => { core.status.hero.equipment[ele] = aimStatus[i]; })
|
||
for (let i = 0, l = equipStatus.length; i < l; i++) {
|
||
if (equipStatus[i] !== aimStatus[i]) {
|
||
if (aimStatus[i] === null) {
|
||
const unequipId = equipStatus[i];
|
||
if (equipEvents.hasOwnProperty(unequipId) &&
|
||
equipEvents[unequipId].hasOwnProperty('unequip'))
|
||
equipEvents[unequipId].unequip();
|
||
core.items._loadEquipEffect(null, unequipId);
|
||
} else {
|
||
const equipId = aimStatus[i];
|
||
if (equipEvents.hasOwnProperty(equipId) &&
|
||
equipEvents[equipId].hasOwnProperty('equip'))
|
||
equipEvents[equipId].equip();
|
||
core.items._loadEquipEffect(equipId, equipStatus[i]);
|
||
}
|
||
equipStatus[i] = aimStatus[i];
|
||
}
|
||
}
|
||
}
|
||
|
||
function findBestEquipComb(equipCombination, equipOwned, id, x, y) {
|
||
let minDamage = core.getDamage(id, x, y),
|
||
bestCombination = core.clone(equipStatus);
|
||
for (let i = 0, l = equipCombination.length; i < l; i++) {
|
||
const currComb = equipCombination[i];
|
||
if (!hasEnoughEquip(currComb, equipOwned)) continue;
|
||
simulateEquip(equipStatus, currComb);
|
||
let damage = core.getDamage(id, x, y);
|
||
if (damage !== null && (minDamage === null || damage < minDamage)) {
|
||
minDamage = damage;
|
||
bestCombination = core.clone(equipStatus);
|
||
}
|
||
}
|
||
return bestCombination;
|
||
}
|
||
|
||
function equipBestComb(bestCombination, equipIncluded, equipNameList) {
|
||
/** @type {Set<string>} */
|
||
const duplicatedName = new Set([]),
|
||
name = core.status.globalAttribute.equipName;
|
||
|
||
// 脱下重复装备
|
||
equipNameList.forEach((ele) => {
|
||
if (getEleCount(ele, equipNameList) > 1) duplicatedName.add(ele);
|
||
})
|
||
equipIncluded.forEach((ele) => {
|
||
if (duplicatedName.has(name[ele]) && core.getEquip(ele) !== null) {
|
||
core.unloadEquip(ele);
|
||
core.status.route.push("unEquip:" + ele.toString());
|
||
}
|
||
})
|
||
|
||
for (let i = 0, l = bestCombination.length; i < l; i++) {
|
||
const currEquip = bestCombination[i],
|
||
pos = equipIncluded[i];
|
||
if (core.getEquip(pos) === currEquip) continue;
|
||
else if (currEquip === null) {
|
||
core.unloadEquip(pos);
|
||
core.status.route.push("unEquip:" + pos.toString());
|
||
} else {
|
||
core.loadEquip(currEquip);
|
||
core.status.route.push("equip:" + currEquip.toString());
|
||
}
|
||
}
|
||
}
|
||
|
||
this.figureEquip = function () {
|
||
compareEquip().then(function (confirm) {
|
||
core.unlockControl();
|
||
})
|
||
}
|
||
},
|
||
"customizableToolBar": function () {
|
||
// 自定义工具栏显示项
|
||
// 本插件需要配合main.js, control.js等的修改
|
||
// 新的逻辑如下:
|
||
// 函数_updateStatusBar_setToolboxIcon在updateStatusBar中调用,仅用于切换录像replay/pause,和计算几个图标的透明度
|
||
// setToolbarButton根据输入的类型重新向状态栏填入元素
|
||
// resize添加一个只刷新工具栏的模式,此外,resize不调用setToolbarButton,相反,setToolbarButton后调用resize
|
||
// 以下地方调用setToolbarButton: hard的点击事件,回放录像的进入/退出事件
|
||
// 要注意一点, floor是不能作为toolBar元素的,statuBar具有同id元素,样式会相互冲突 复制了一个外形一样的图标叫view,作为浏览地图按钮
|
||
|
||
/**
|
||
* PC 默认6个键且不需要切入数字键的模式 最多9个键 手机 默认9个键 最多9个键(手机理论上可以塞10个键,但是懒得判定了)
|
||
* normal:普通模式 num:按下数字键切换到的模式 replay:录像模式 opacity:透明度
|
||
*/
|
||
const defaultConfig = {
|
||
normal: {
|
||
vertical: ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback'],
|
||
horizontal: ['book', 'fly', 'toolbox', 'save', 'load', 'settings']
|
||
},
|
||
num: {
|
||
vertical: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt'],
|
||
horizontal: ['btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btnAlt']
|
||
},
|
||
replay: {
|
||
vertical: ['play', 'stop', 'rewind', 'book', 'view', 'speedDown', 'speedUp', 'save'],
|
||
horizontal: ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'save']
|
||
},
|
||
hide: {
|
||
vertical: [], horizontal: [],
|
||
}
|
||
};
|
||
|
||
function isVertical() {
|
||
return core.domStyle.isVertical || core.flags.extendToolbar;
|
||
}
|
||
|
||
function getToolBarConfig(type) {
|
||
const currObj = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]);
|
||
return isVertical() ? currObj.vertical : currObj.horizontal;
|
||
}
|
||
this.getToolBarConfig = getToolBarConfig;
|
||
|
||
/**
|
||
*
|
||
* @param {string} type
|
||
* @param {number} index
|
||
* @param {string} value
|
||
* @example core.setToolBarConfig('normal', 3, null)
|
||
*/
|
||
function setToolBarConfig(type, index, value) {
|
||
const allToolType = ['normal', 'num', 'replay'];
|
||
const allTools = ['book', 'fly', 'toolbox', 'keyboard', 'shop', 'save', 'load', 'settings', 'rollback', 'undoRollback',
|
||
'btn1', 'btn2', 'btn3', 'btn4', 'btn5', 'btn6', 'btn7', 'btn8', 'btn9', 'btnAlt',
|
||
'equipbox', 'floor', 'play', 'rewind', 'speedDown', 'speedUp', 'stop', 'single', 'view'];
|
||
if (!allToolType.includes(type) || index < 0 || index > 8) {
|
||
core.drawFailTip('请选中工具栏的一个合法位置作为目标点!');
|
||
return;
|
||
}
|
||
if (value !== 'delete' && !allTools.includes(value)) {
|
||
core.drawFailTip('请选中一个图标作为替换目标!');
|
||
return;
|
||
}
|
||
const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]);
|
||
const key = isVertical() ? 'vertical' : 'horizontal';
|
||
if (type === 'replay' &&
|
||
(['fly', 'shop', 'load', 'settings', 'btnAlt', 'rollback', 'undoRollback', 'btnAlt'].includes(value))) {
|
||
core.drawFailTip('该按钮不允许放在录像模式下!');
|
||
return;
|
||
} // 录像模式下的按键处理有一套专门的逻辑,在_sys_onkeyUp_replay,实际上并不能读取自动档
|
||
if (type !== 'replay' && ['play', 'stop', 'rewind', 'speedDown', 'speedUp', 'single'].includes(value)) {
|
||
core.drawFailTip('该按钮不允许放在非录像模式下!');
|
||
return;
|
||
}
|
||
|
||
if (value === 'delete') toorBarConfig[key].splice(index, 1);
|
||
else {
|
||
if (index > toorBarConfig[key].length) {
|
||
core.drawFailTip('按钮中间不能有空白!');
|
||
return;
|
||
}
|
||
const oldIndex = toorBarConfig[key].indexOf(value);
|
||
if (oldIndex !== -1) { // 如果目标位置有图标,两者交换
|
||
if (toorBarConfig[key][index] === value) return;
|
||
const aimTool = toorBarConfig[key][index];
|
||
toorBarConfig[key][index] = value;
|
||
toorBarConfig[key][oldIndex] = aimTool;
|
||
}
|
||
else toorBarConfig[key][index] = value;
|
||
}
|
||
core.setLocalStorage('toorBarConfig' + type, toorBarConfig);
|
||
setToolbarButton(core.domStyle.toolbarBtn);
|
||
}
|
||
this.setToolBarConfig = setToolBarConfig;
|
||
|
||
function resetToolBarConfig() {
|
||
for (let type in defaultConfig) {
|
||
if (!defaultConfig.hasOwnProperty(type)) return;
|
||
const toorBarConfig = core.getLocalStorage('toorBarConfig' + type, defaultConfig[type]);
|
||
core.setLocalStorage('toorBarConfig' + type, toorBarConfig);
|
||
}
|
||
}
|
||
this.resetToolBarConfig = resetToolBarConfig;
|
||
|
||
/**
|
||
*
|
||
* @param {'normal'|'num'|'replay'|'hide'} type
|
||
*/
|
||
function setToolbarButton(type) {
|
||
if (main.replayChecking) return; // 录像验证必须干掉此函数 因为它操作了DOM
|
||
const currList = getToolBarConfig(type);
|
||
if (!currList) return;
|
||
const fragment = document.createDocumentFragment();
|
||
for (let i = 0, l = currList.length; i < l; i++) {
|
||
const iconId = currList[i];
|
||
const currEle = core.statusBar.image[iconId];
|
||
if (!currEle) continue;
|
||
currEle.style.display = 'block';
|
||
fragment.appendChild(currEle);
|
||
}
|
||
if (type !== "hide") {
|
||
core.domStyle.toolbarBtn = type;
|
||
}
|
||
core.domStyle.toolsCount = currList.length;
|
||
fragment.appendChild(core.dom.hard); // 难度一定会显示 因为难度所在位置要用于切换常规模式和数字模式 难度的尺寸是动态决定的
|
||
core.dom.toolBar.innerHTML = '';
|
||
core.dom.toolBar.appendChild(fragment);
|
||
core.control.resize('tools'); // 在这里计算难度的尺寸
|
||
}
|
||
this.setToolbarButton = setToolbarButton;
|
||
},
|
||
"setting": function () {
|
||
// 自绘设置界面
|
||
// 请保持本插件在所有插件的最下方
|
||
|
||
// #region 复写
|
||
// 复写resize,保证屏幕变化时此画布表现正常
|
||
const originResize = core.control.resize;
|
||
core.control.resize = function () {
|
||
originResize.apply(core.control, arguments);
|
||
const settingMenu = core.plugin.settingMenu;
|
||
if (settingMenu && settingMenu.onDraw) settingMenu.drawContent();
|
||
}
|
||
// #endregion
|
||
|
||
/**
|
||
* @typedef {{
|
||
* getName:()=>string,
|
||
* effect:()=>void,
|
||
* text:string,
|
||
* replay?:boolean,
|
||
* draw?:((ctx:string)=>void)
|
||
* }} Setting
|
||
*/
|
||
function invertFlag(name) {
|
||
core.setFlag(name, !core.hasFlag(name));
|
||
}
|
||
|
||
function invertLocalStorage(name) {
|
||
const value = core.getLocalStorage(name, false);
|
||
core.setLocalStorage(name, !value);
|
||
}
|
||
|
||
// #endregion
|
||
// #region 设置的具体内容,与相应的录像注册
|
||
// #endregion
|
||
// #region 按钮类
|
||
|
||
const { ButtonBase, RoundBtn, IconBtn, ExitBtn,
|
||
ArrowBtn, MenuBase, Pagination, KeyCodeEnum } = core.plugin.uiBase;
|
||
|
||
class TextButton extends ButtonBase {
|
||
constructor(x, y, w, h, text) {
|
||
super(x, y, w, h);
|
||
this.text = text;
|
||
this.draw = () => {
|
||
if (this.disable) return;
|
||
core.ui.fillText(this.ctx, this.text,
|
||
this.x + this.w / 2, this.y + this.h / 2 + 5, 'white', '16px Verdana');
|
||
}
|
||
}
|
||
}
|
||
// #endregion
|
||
// #region 菜单类
|
||
|
||
// #region 单个设置菜单的基类
|
||
/** 单个设置按钮的基类 */
|
||
class SettingButton extends ButtonBase {
|
||
constructor(x, y, w, h, ...drawArgs) {
|
||
super(x, y, w, h);
|
||
/** @type {Setting} 将在registerBtn时赋予此项 */ // @ts-ignore
|
||
this.setting;
|
||
this.drawArgs = drawArgs;
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const [x, y, w, h] = [this.x, this.y, this.w, this.h];
|
||
const setting = this.setting;
|
||
// 取消注释下面这一句将显示所有按钮的判定框
|
||
// core.strokeRect(ctx, this.x, this.y, this.w, this.h, 'yellow');
|
||
core.setTextAlign(ctx, 'start');
|
||
core.ui.fillText(ctx, setting.getName(), x, y + h / 2 + 5, 'white', '16px Verdana');
|
||
const drawFunc = setting.draw;
|
||
if (this.status === 'selected') {
|
||
core.drawUIEventSelector(0, "winskin.png", this.x, this.y, this.w, this.h, 137);
|
||
}
|
||
if (drawFunc) drawFunc.apply(this, [ctx, ...this.drawArgs]);
|
||
}
|
||
}
|
||
|
||
// @todo 悬浮按钮不改变时不触发事件 这个优化有没有做
|
||
|
||
/** 单个设置菜单的基类 */
|
||
class SettingOnePage extends MenuBase {
|
||
constructor(name, settingData) {
|
||
super(name, ['ondown', 'onmove', 'keyDown']);
|
||
this.text = '';
|
||
/** @type {string | undefined}*/
|
||
this.selectedPos;
|
||
/** @type {SettingButton | undefined}*/
|
||
this.selectedBtn;
|
||
/** @type {{[x:string]:Setting}}*/
|
||
this.settingData = settingData;
|
||
}
|
||
|
||
keyDownEvent(keyCode) {
|
||
let x, y;
|
||
const changePos = (newPos) => {
|
||
if (this.btnMap.has(newPos)) {
|
||
const button = this.btnMap.get(newPos);
|
||
this.focus(button, newPos);
|
||
}
|
||
}
|
||
if ([KeyCodeEnum.Left, KeyCodeEnum.Right, KeyCodeEnum.Up, KeyCodeEnum.Down].includes(keyCode)) {
|
||
if (!this.selectedBtn || !this.selectedPos) {
|
||
const button = this.btnMap.get('1,1');
|
||
if (button) this.focus(button, '1,1');
|
||
return;
|
||
} else {
|
||
[x, y] = this.selectedPos.split(',').map((x) => parseInt(x));
|
||
if (keyCode === KeyCodeEnum.Left) x--;
|
||
if (keyCode === KeyCodeEnum.Up) y--;
|
||
if (keyCode === KeyCodeEnum.Right) x++;
|
||
if (keyCode === KeyCodeEnum.Down) y++;
|
||
let newPos = x + ',' + y;
|
||
|
||
// 逻辑:左右,查找不到对应坐标就不动。
|
||
// 上下,查找不到对应坐标,只要该列存在第一个元素,就会移到该列。
|
||
|
||
if (keyCode === KeyCodeEnum.Left || keyCode === KeyCodeEnum.Right) {
|
||
changePos(newPos);
|
||
}
|
||
if (keyCode === KeyCodeEnum.Up || keyCode === KeyCodeEnum.Down) {
|
||
if (this.btnMap.has(newPos)) {
|
||
const button = this.btnMap.get(newPos);
|
||
this.focus(button, newPos);
|
||
} else {
|
||
newPos = '1,' + y;
|
||
changePos(newPos);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
switch (keyCode) {
|
||
case KeyCodeEnum.Enter: // Enter/Space
|
||
case KeyCodeEnum.SpaceBar:
|
||
if (this.selectedBtn) this.selectedBtn.ondown(-1, -1, -1, -1);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {SettingButton} btn
|
||
* @param {string} key 对应事件的索引
|
||
**/
|
||
execEffect(btn, key, ...eventArgs) {
|
||
const setting = btn.setting;
|
||
setting.effect.apply(this, eventArgs);
|
||
if (setting.replay) {
|
||
let actionString = 'cSet:' + key;
|
||
if (eventArgs && eventArgs.length > 0) {
|
||
actionString += ':' + eventArgs.map(arg => encodeURIComponent(arg)).join(':');
|
||
}
|
||
core.status.route.push(actionString);
|
||
}
|
||
this.drawContent();
|
||
}
|
||
|
||
/** 聚焦到指定的按钮并重绘菜单 */
|
||
focus(button, pos) {
|
||
if (this.selectedBtn === button) return; // 如果当前按钮已被选中,则不做任何操作
|
||
this.selectedPos = pos;
|
||
this.selectedBtn = button;
|
||
this.btnMap.forEach((currBtn) => {
|
||
currBtn.status = (currBtn === button) ? 'selected' : 'none';
|
||
});
|
||
if (button instanceof SettingButton) {
|
||
this.text = button.setting.text || '';
|
||
}
|
||
this.drawContent();
|
||
}
|
||
|
||
drawContent(ctx) {
|
||
if (!ctx) ctx = this.name;
|
||
super.drawContent();
|
||
if (this.text && this.text.length > 0) {
|
||
core.ui.drawTextContent(ctx, this.text, {
|
||
left: 30,
|
||
top: 78,
|
||
bold: false,
|
||
color: "white",
|
||
align: "left",
|
||
fontSize: 14,
|
||
maxWidth: 350
|
||
});
|
||
}
|
||
}
|
||
|
||
clear() {
|
||
core.clearUIEventSelector(0); // 光标的绘制在按钮中进行
|
||
super.clear();
|
||
}
|
||
|
||
/**
|
||
* @override
|
||
* @param {string} pos
|
||
* @param {string} key
|
||
* @param {SettingButton} btn
|
||
* @param {...(string|number)} eventArgs
|
||
*/
|
||
registerSettingBtn(pos, key, btn, ...eventArgs) {
|
||
super.registerBtn(pos, btn, {
|
||
'ondown': () => {
|
||
if (this.selectedBtn === btn) this.execEffect(btn, key, ...eventArgs);
|
||
else this.focus(btn, pos);
|
||
},
|
||
'onmove': () => {
|
||
this.focus(btn, pos);
|
||
}
|
||
});
|
||
btn.setting = this.settingData[key];
|
||
}
|
||
|
||
registerSettingBtns(arr) {
|
||
arr.forEach(ele => {
|
||
const [key, btn, event, ...eventArgs] = ele;
|
||
this.registerSettingBtn(key, btn, event, ...eventArgs);
|
||
});
|
||
}
|
||
}
|
||
// #endregion
|
||
|
||
// #region 功能菜单
|
||
function checkSkipFuncs() {
|
||
const skipText = core.getLocalStorage('skipText');
|
||
// 此函数用于检测是否处在录像模式下,是则将跳过所有非必要对话
|
||
core.events.__action_checkReplaying = skipText ? function () {
|
||
core.doAction();
|
||
return true;
|
||
}.bind(core.events) : events.prototype.__action_checkReplaying;
|
||
|
||
const skipPeform = core.getLocalStorage('skipPeform');
|
||
|
||
const instantMove = function (fromX, fromY, aimX, aimY, keep, callback) {
|
||
const [_block, blockInfo] = core.maps._getAndRemoveBlock(fromX, fromY);
|
||
if (keep) {
|
||
core.setBlock(blockInfo.number, aimX, aimY);
|
||
core.showBlock(aimX, aimY);
|
||
}
|
||
if (callback) callback();
|
||
}
|
||
|
||
core.maps.jumpBlock = skipPeform ? function (sx, sy, ex, ey, time, keep, callback) {
|
||
return instantMove(sx, sy, ex, ey, keep, callback);
|
||
}.bind(core.maps) : maps.prototype.jumpBlock;
|
||
|
||
core.maps.moveBlock = skipPeform ? function (x, y, steps, time, keep, callback) {
|
||
maps.prototype.moveBlock(x, y, steps, 1, keep, callback);
|
||
}.bind(core.maps) : maps.prototype.moveBlock;
|
||
|
||
core.maps.drawAnimate = skipPeform ? function (name, x, y, alignWindow, callback) {
|
||
if (callback) callback();
|
||
return -1;
|
||
}.bind(core.maps) : maps.prototype.drawAnimate;
|
||
|
||
core.maps.drawHeroAnimate = skipPeform ? function (name, callback) {
|
||
if (callback) callback();
|
||
return -1;
|
||
}.bind(core.maps) : maps.prototype.drawHeroAnimate;
|
||
|
||
core.events.jumpHero = skipPeform ? function (ex, ey, time, callback) {
|
||
const { x: sx, y: sy } = core.status.hero.loc;
|
||
if (ex == null) ex = sx;
|
||
if (ey == null) ey = sy;
|
||
core.setHeroLoc('x', ex);
|
||
core.setHeroLoc('y', ey);
|
||
core.clearMap('hero');
|
||
core.drawHero();
|
||
if (callback) callback();
|
||
}.bind(core.events) : events.prototype.jumpHero;
|
||
|
||
core.events.vibrate = skipPeform ? function (direction, time, speed, power, callback) {
|
||
if (callback) callback();
|
||
return;
|
||
}.bind(core.events) : events.prototype.vibrate;
|
||
|
||
core.events._action_sleep = skipPeform ? function (data, x, y, prefix) {
|
||
core.doAction();
|
||
}.bind(core.events) : events.prototype._action_sleep;
|
||
}
|
||
this.checkSkipFuncs = checkSkipFuncs;
|
||
/** @type {{[x:string]:Setting}} */
|
||
const gamePlaySetting = {
|
||
autoGet: {
|
||
getName: () => '自动拾取:' + (core.getFlag('autoGet', false) ? '开' : '关'),
|
||
effect: () => invertFlag('autoGet'),
|
||
text: '每走一步,自动拾取当前层可获得的道具。',
|
||
replay: true,
|
||
},
|
||
autoBattle: {
|
||
getName: () => '自动清怪:' + (core.getFlag('autoBattle', false) ? '开' : '关'),
|
||
effect: () => invertFlag('autoBattle'),
|
||
text: '每走一步,自动和当前层可到达位置伤害为0的敌人战斗。对部分特殊敌人无效。',
|
||
replay: true,
|
||
},
|
||
noRouting_HP: {
|
||
getName: () => '',
|
||
effect: () => invertFlag('noRouting_HP'),
|
||
text: '自动寻路时绕过加血物品。同时自动拾取也将忽略这类物品。',
|
||
replay: true,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.setAlpha(ctx, core.hasFlag('noRouting_HP') ? 1 : 0.3);
|
||
core.drawIcon(ctx, 'redPotion', this.x, this.y, this.w, this.h);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
},
|
||
noRouting_MDEF: {
|
||
getName: () => '',
|
||
effect: () => invertFlag('noRouting_MDEF'),
|
||
text: '自动寻路时绕过加护盾物品。同时自动拾取也将忽略这类物品。',
|
||
replay: true,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.setAlpha(ctx, core.hasFlag('noRouting_MDEF') ? 1 : 0.3);
|
||
core.drawIcon(ctx, 'greenGem', this.x, this.y, this.w, this.h);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
},
|
||
noRouting_ATK: {
|
||
getName: () => '',
|
||
effect: () => invertFlag('noRouting_ATK'),
|
||
text: '自动寻路时绕过加攻物品。同时自动拾取也将忽略这类物品。',
|
||
replay: true,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.setAlpha(ctx, core.hasFlag('noRouting_ATK') ? 1 : 0.3);
|
||
core.drawIcon(ctx, 'redGem', this.x, this.y, this.w, this.h);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
},
|
||
noRouting_DEF: {
|
||
getName: () => '',
|
||
effect: () => invertFlag('noRouting_DEF'),
|
||
text: '自动寻路时绕过加防物品。同时自动拾取也将忽略这类物品。',
|
||
replay: true,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.setAlpha(ctx, core.hasFlag('noRouting_DEF') ? 1 : 0.3);
|
||
core.drawIcon(ctx, 'blueGem', this.x, this.y, this.w, this.h);
|
||
core.setAlpha(ctx, 1);
|
||
}
|
||
},
|
||
clickMove: {
|
||
getName: () => '单击瞬移:' + (core.hasFlag('__noClickMove__') ? '关' : '开'),
|
||
effect: () => invertFlag('__noClickMove__'),
|
||
text: '系统设置。单击即可触发瞬移。',
|
||
replay: true,
|
||
},
|
||
moveSpeedDown: {
|
||
getName: () => ' < 步时:' + core.values.moveSpeed,
|
||
effect: () => core.actions._clickSwitchs_action_moveSpeed(-10),
|
||
text: '缩短步时。',
|
||
replay: false, // 录像中不可录入任何DOM操作
|
||
},
|
||
moveSpeedUp: {
|
||
getName: () => ' > ',
|
||
effect: () => core.actions._clickSwitchs_action_moveSpeed(10),
|
||
text: '增大步时。',
|
||
replay: false,
|
||
},
|
||
floorChangeTimeDown: {
|
||
getName: () => ' < 转场:' + core.values.floorChangeTime,
|
||
effect: () => core.actions._clickSwitchs_action_floorChangeTime(-100),
|
||
text: '缩短转场时间。',
|
||
replay: false, // 录像中不可录入任何DOM操作
|
||
},
|
||
floorChangeTimeUp: {
|
||
getName: () => ' > ',
|
||
effect: () => core.actions._clickSwitchs_action_floorChangeTime(100),
|
||
text: '增大转场时间。',
|
||
replay: false,
|
||
},
|
||
skipText: {
|
||
getName: () => '跳过剧情:' + (core.getLocalStorage('skipText', false) ? '开' : '关'),
|
||
effect: () => {
|
||
invertLocalStorage('skipText');
|
||
checkSkipFuncs();
|
||
},
|
||
text: '跳过全部文字对话。初见请勿开启此选项。',
|
||
replay: false,
|
||
},
|
||
skipPeform: {
|
||
getName: () => '跳过演出:' + (core.getLocalStorage('skipPerform', false) ? '开' : '关'),
|
||
effect: () => {
|
||
invertLocalStorage('skipPerform');
|
||
checkSkipFuncs();
|
||
},
|
||
text: '加速等待、播放动画等常见演出效果。',
|
||
replay: false,
|
||
},
|
||
comment: {
|
||
getName: () => '在线留言:' + (core.hasFlag('comment') ? '开' : '关'),
|
||
effect: () => {
|
||
if (core.hasFlag('comment')) {
|
||
core.setFlag('comment', false);
|
||
core.plugin.clearCommentSign();
|
||
} else {
|
||
core.setFlag('comment', true);
|
||
core.plugin.drawCommentSign();
|
||
}
|
||
},
|
||
text: '在地图上显示玩家的在线留言。',
|
||
replay: true,
|
||
},
|
||
autoHideFloor: {
|
||
getName: () => '自动隐藏楼层:' + (core.hasFlag('autoHideFloor') ? '开' : '关'),
|
||
effect: () => {
|
||
invertFlag('autoHideFloor');
|
||
},
|
||
text: '一个楼层已无物品、敌人、NPC(不含已忽略图块),且无未到达楼层传送口时可被自动隐藏,仅在首次进入此状态时在楼传界面触发。',
|
||
replay: true,
|
||
},
|
||
autoSaveBeforeUseItem: {
|
||
getName: () => '使用物品自动保存:' + (core.getLocalStorage('autoSaveBeforeUseItem') ? '开' : '关'),
|
||
effect: () => {
|
||
invertLocalStorage('autoSaveBeforeUseItem');
|
||
},
|
||
text: '使用消耗类物品(含破炸飞跳)前,以及即将走入滑冰、触发捕捉时自动存档。',
|
||
replay: false,
|
||
},
|
||
autoSaveBeforePickItem: {
|
||
getName: () => '拾取物品自动保存:' + (core.getLocalStorage('autoSaveBeforePickItem') ? '开' : '关'),
|
||
effect: () => {
|
||
invertLocalStorage('autoSaveBeforePickItem');
|
||
},
|
||
text: '拾取地上物品前自动存档。',
|
||
replay: false,
|
||
},
|
||
}
|
||
|
||
class GamePlay extends SettingOnePage {
|
||
constructor() {
|
||
super('gamePlay', gamePlaySetting);
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.fillText(ctx, '-- 自动 --', 40, 175, ' #FFE4B5', '18px Verdana');
|
||
core.fillText(ctx, '-- 瞬移 --', 40, 225, ' #FFE4B5', '18px Verdana');
|
||
core.fillText(ctx, '绕开', 220, 250, 'white', '16px Verdana');
|
||
core.fillText(ctx, '-- 杂项 --', 40, 275, ' #FFE4B5', '18px Verdana');
|
||
super.drawContent(ctx);
|
||
}
|
||
}
|
||
|
||
function gamePlayFactory() {
|
||
const gamePlayMenu = new GamePlay();
|
||
gamePlayMenu.registerSettingBtns([
|
||
['1,1', 'autoGet', new SettingButton(40, 180, 150, 30)],
|
||
['2,1', 'autoBattle', new SettingButton(220, 180, 150, 30)],
|
||
['1,2', 'clickMove', new SettingButton(40, 230, 150, 30)],
|
||
['2,2', 'noRouting_HP', new SettingButton(260, 234, 24, 24)],
|
||
['3,2', 'noRouting_MDEF', new SettingButton(290, 234, 24, 24)],
|
||
['4,2', 'noRouting_ATK', new SettingButton(320, 234, 24, 24)],
|
||
['5,2', 'noRouting_DEF', new SettingButton(350, 234, 24, 24)],
|
||
['1,3', 'moveSpeedDown', new SettingButton(40, 280, 25, 25)],
|
||
['2,3', 'moveSpeedUp', new SettingButton(140, 280, 25, 25)],
|
||
['3,3', 'floorChangeTimeDown', new SettingButton(220, 280, 25, 25)],
|
||
['4,3', 'floorChangeTimeUp', new SettingButton(340, 280, 25, 25)],
|
||
['1,4', 'skipText', new SettingButton(40, 305, 150, 25)],
|
||
['2,4', 'skipPeform', new SettingButton(220, 305, 150, 25)],
|
||
['1,5', 'comment', new SettingButton(40, 330, 150, 25)],
|
||
['2,5', 'autoHideFloor', new SettingButton(220, 330, 150, 25)],
|
||
['1,6', 'autoSaveBeforeUseItem', new SettingButton(40, 355, 150, 25)],
|
||
['2,6', 'autoSaveBeforePickItem', new SettingButton(220, 355, 150, 25)],
|
||
]);
|
||
return gamePlayMenu;
|
||
}
|
||
// #endregion
|
||
// #region 视图菜单
|
||
/** @type {{[x:string]:Setting}} */
|
||
const gameViewSetting = {
|
||
itemDetail: {
|
||
getName: () => '物品显示数据:' + (core.hasFlag('itemDetail') ? '开' : '关'),
|
||
effect: () => {
|
||
invertFlag('itemDetail');
|
||
core.control.updateStatusBar(); // 更新地图上物品的属性显示
|
||
},
|
||
text: '在地图上显示即捡即用道具和装备增加的属性值。',
|
||
replay: true,
|
||
},
|
||
zoomIn: {
|
||
getName: () => ' < 放缩:' + Math.max(core.domStyle.scale, 1) + 'x',
|
||
effect: () => core.actions._clickSwitchs_display_setSize(-1),
|
||
text: '放缩',
|
||
replay: false,
|
||
},
|
||
zoomOut: {
|
||
getName: () => ' > ',
|
||
effect: () => core.actions._clickSwitchs_display_setSize(1),
|
||
text: '放缩。',
|
||
replay: false,
|
||
},
|
||
HDCanvas: {
|
||
getName: () => '高清画面:' + (core.flags.enableHDCanvas ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_display_enableHDCanvas,
|
||
text: '高清画面。本功能开关后刷新游戏才能看到效果。',
|
||
replay: false,
|
||
},
|
||
enableEnemyPoint: {
|
||
getName: () => '定点怪显:' + (core.flags.enableEnemyPoint ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_display_enableEnemyPoint,
|
||
text: '怪物属性定点显示功能,即属性不同的怪物会在怪物手册单列。',
|
||
replay: false,
|
||
},
|
||
displayEnemyDamage: {
|
||
getName: () => '怪物显伤:' + (core.flags.displayEnemyDamage ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_display_enemyDamage,
|
||
text: '地图上显示怪物的伤害。',
|
||
replay: false,
|
||
},
|
||
displayCritical: {
|
||
getName: () => '临界显伤:' + (core.flags.displayCritical ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_display_critical,
|
||
text: '地图上显示怪物的临界值',
|
||
replay: false,
|
||
},
|
||
displayExtraDamage: {
|
||
getName: () => '领域显伤:' + (core.flags.displayExtraDamage ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_display_extraDamage,
|
||
text: '领域显伤',
|
||
replay: false,
|
||
},
|
||
extraDamageType: {
|
||
getName: () => '领域模式:' + (core.flags.extraDamageType == 2 ? '[最简]' : core.flags.extraDamageType == 1 ? '[半透明]' : '[完整]'),
|
||
effect: core.actions._clickSwitchs_display_extraDamageType,
|
||
text: '是否显示不可通行地块的领域伤害。',
|
||
replay: false,
|
||
},
|
||
autoScale: {
|
||
getName: () => '自动放缩:' + (core.getLocalStorage('autoScale') ? '开' : '关'),
|
||
effect: () => {
|
||
core.setLocalStorage('autoScale', !core.getLocalStorage('autoScale'));
|
||
},
|
||
text: '自动放缩。',
|
||
replay: false,
|
||
},
|
||
bgm: {
|
||
getName: () => '音乐:' + (core.musicStatus.bgmStatus ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_sounds_bgm,
|
||
text: '播放背景音乐。',
|
||
replay: false,
|
||
},
|
||
se: {
|
||
getName: () => '音效:' + (core.musicStatus.soundStatus ? '开' : '关'),
|
||
effect: core.actions._clickSwitchs_sounds_se,
|
||
text: '播放音效。',
|
||
replay: false,
|
||
},
|
||
decreaseVolume: {
|
||
getName: () => " < 音量:" + Math.round(Math.sqrt(100 * core.musicStatus.userVolume)),
|
||
effect: () => core.actions._clickSwitchs_sounds_userVolume(-1),
|
||
text: '减小音量。',
|
||
replay: false,
|
||
},
|
||
increaseVolume: {
|
||
getName: () => ' > ',
|
||
effect: () => core.actions._clickSwitchs_sounds_userVolume(1),
|
||
text: '增大音量。',
|
||
replay: false,
|
||
},
|
||
};
|
||
|
||
class GameView extends SettingOnePage {
|
||
constructor() {
|
||
super('gameView', gameViewSetting);
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.fillText(ctx, '-- 显示 --', 40, 175, ' #FFE4B5', '18px Verdana');
|
||
core.fillText(ctx, '-- 音效 --', 40, 320, ' #FFE4B5', '18px Verdana');
|
||
super.drawContent(ctx);
|
||
}
|
||
}
|
||
|
||
function gameViewFactory() {
|
||
const gameViewMenu = new GameView();
|
||
const advanceDisplayBtn = new RoundBtn(300, 280, 32, 18, "高级", {
|
||
font: '12px Verdana', fillStyle: "SlateGray", radius: 1, fontStyle: "yellow",
|
||
strokeStyle: "black"
|
||
});
|
||
gameViewMenu.registerBtn('openAdvanceDisplay', advanceDisplayBtn, () => {
|
||
const settingMenu = core.plugin.settingMenu;
|
||
if (settingMenu) {
|
||
settingMenu.endListen();
|
||
// 隐藏大菜单的按钮是为了避免视觉上的干扰
|
||
settingMenu.btnMap.forEach(btn => { btn.disable = true });
|
||
settingMenu.pageList[settingMenu.currPage].endListen();
|
||
settingMenu.drawContent();
|
||
}
|
||
core.ui.clearUIEventSelector(0);
|
||
const advanceDisplayMenu = advanceDisplayFactory();
|
||
advanceDisplayMenu.init();
|
||
});
|
||
gameViewMenu.registerSettingBtns([
|
||
['1,1', 'itemDetail', new SettingButton(40, 180, 150, 25)],
|
||
['1,2', 'displayEnemyDamage', new SettingButton(40, 205, 150, 25)],
|
||
['1,3', 'displayExtraDamage', new SettingButton(40, 230, 150, 25)],
|
||
['1,4', 'autoScale', new SettingButton(40, 255, 150, 25)],
|
||
['1,5', 'HDCanvas', new SettingButton(40, 280, 150, 25)],
|
||
['1,6', 'bgm', new SettingButton(40, 325, 150, 25)],
|
||
['1,7', 'decreaseVolume', new SettingButton(40, 350, 25, 25)],
|
||
['2,7', 'increaseVolume', new SettingButton(140, 350, 25, 25)],
|
||
['2,1', 'displayEnemyDamage', new SettingButton(220, 180, 150, 25)],
|
||
['2,2', 'displayCritical', new SettingButton(220, 205, 150, 25)],
|
||
['2,3', 'extraDamageType', new SettingButton(220, 230, 150, 25)],
|
||
['2,4', 'zoomIn', new SettingButton(220, 255, 25, 25)],
|
||
['3,4', 'zoomOut', new SettingButton(330, 255, 25, 25)],
|
||
['2,6', 'se', new SettingButton(220, 325, 150, 25)],
|
||
]);
|
||
return gameViewMenu;
|
||
}
|
||
// #endregion
|
||
// #region 快捷键菜单
|
||
/** @type {{[x:string]:Setting}} */
|
||
const keySetting = {
|
||
leftHand: {
|
||
getName: () => '左手模式:' + (core.flags.leftHandPrefer ? '开' : '关'),
|
||
effect: () => {
|
||
core.flags.leftHandPrefer = !core.flags.leftHandPrefer;
|
||
core.setLocalStorage('leftHandPrefer', core.flags.leftHandPrefer);
|
||
},
|
||
text: '系统设置。左手模式下WASD将用于移动角色,IJKL对应于原始的WASD进行存读档等操作。',
|
||
replay: true,
|
||
},
|
||
setHotKey: {
|
||
getName: () => '',
|
||
effect: /** @this {GameView} */ function (num) {
|
||
core.utils.myprompt('输入物品名。名称(例如:破墙镐)或英文ID(例如:pickaxe)均可。', '', (value) => {
|
||
const itemInfo = core.material.items;
|
||
const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value);
|
||
if (aimItem) {
|
||
if (['constants', 'tools'].includes(aimItem.cls)) {
|
||
for (let i = 1; i <= 9; i++) {
|
||
if (i !== num && core.getLocalStorage('hotkey' + i) === aimItem.id) {
|
||
core.setLocalStorage('hotkey' + i, null);
|
||
}
|
||
}
|
||
core.setLocalStorage('hotkey' + num, aimItem.id);
|
||
this.drawContent();
|
||
} else {
|
||
core.drawFailTip('错误:该类型的物品不支持快捷使用!');
|
||
}
|
||
} else {
|
||
core.drawFailTip('错误:找不到该名称的物品!');
|
||
}
|
||
}, () => { });
|
||
},
|
||
text: '给选定的数字键绑定一个可快捷使用的物品。',
|
||
replay: false,
|
||
draw: /** @this {SettingButton} */ function (ctx, num) {
|
||
const item = core.getLocalStorage('hotkey' + num, null);
|
||
let icon, itemName;
|
||
if (item && core.material.items.hasOwnProperty(item)) {
|
||
icon = item;
|
||
itemName = core.material.items[item].name;
|
||
} else {
|
||
const itemMap = {
|
||
'1': { icon: 'pickaxe', itemName: '破墙镐' },
|
||
'2': { icon: 'bomb', itemName: '炸弹' },
|
||
'3': { icon: 'centerFly', itemName: '中心飞' },
|
||
'4': { itemName: '杂物' },
|
||
'5': { itemName: '回退一步' },
|
||
'6': { itemName: '撤销回退' },
|
||
'7': { itemName: '轻按' }
|
||
};
|
||
if (num >= 1 && num <= 9) {
|
||
({ icon, itemName = '无' } = itemMap[num] || {});
|
||
}
|
||
}
|
||
const keyIcon = 'btn' + num;
|
||
core.drawIcon(ctx, keyIcon, this.x, this.y, 16, 16);
|
||
const hasItem = core.material.items.hasOwnProperty(icon);
|
||
if (hasItem) core.drawIcon(ctx, icon, this.x + 20, this.y, 16, 16);
|
||
core.setTextAlign(ctx, 'left');
|
||
core.setTextBaseline(ctx, 'alphabetic');
|
||
core.fillText(ctx, itemName || '无', this.x + (hasItem ? 40 : 20), this.y + 14, 'white', '16px Verdana');
|
||
}
|
||
},
|
||
clearHotKeys: {
|
||
getName: () => '',
|
||
effect: /** @this {GameView} */ function () {
|
||
for (let i = 1; i <= 9; i++) {
|
||
core.setLocalStorage('hotkey' + i, null);
|
||
}
|
||
this.drawContent();
|
||
core.drawSuccessTip('快捷键已重置到默认状态。');
|
||
},
|
||
text: '重置本页面所有快捷键到默认状态。',
|
||
replay: false,
|
||
draw: /** @this {SettingButton} */ function (ctx) {
|
||
core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#D3D3D3');
|
||
core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, '#888888');
|
||
core.fillText(ctx, '重置', this.x + 5, this.y + this.h / 2 + 5, '#333333', '16px Verdana');
|
||
}
|
||
}
|
||
};
|
||
class KeyMenu extends SettingOnePage {
|
||
constructor() {
|
||
super('key', keySetting);
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.fillText(ctx, '-- 快捷键 --', 40, 205, ' #FFE4B5', '18px Verdana');
|
||
core.fillText(ctx, "注意⚠️更推荐在背包", 160, 190, "rgb(255,255,51)", '12px Verdana');
|
||
core.fillText(ctx, "中点击右上角", 287, 190, "rgb(255,255,51)", '12px Verdana');
|
||
core.fillText(ctx, "图标设置单物品的快捷键", 160, 210, "rgb(255,255,51)", '12px Verdana');
|
||
core.drawIcon(ctx, "toolbox", 272, 178, 16, 16);
|
||
core.drawIcon(ctx, "keyboard", 359, 178, 16, 16);
|
||
super.drawContent(ctx);
|
||
}
|
||
}
|
||
|
||
function keyMenuFactory() {
|
||
const keyMenu = new KeyMenu();
|
||
keyMenu.registerSettingBtns([
|
||
['1,1', 'leftHand', new SettingButton(40, 160, 150, 25)],
|
||
['1,2', 'setHotKey', new SettingButton(40, 220, 150, 25, '1'), 1],
|
||
['2,2', 'setHotKey', new SettingButton(220, 220, 150, 25, '2'), 2],
|
||
['1,3', 'setHotKey', new SettingButton(40, 250, 150, 25, '3'), 3],
|
||
['2,3', 'setHotKey', new SettingButton(220, 250, 150, 25, '4'), 4],
|
||
['1,4', 'setHotKey', new SettingButton(40, 280, 150, 25, '5'), 5],
|
||
['2,4', 'setHotKey', new SettingButton(220, 280, 150, 25, '6'), 6],
|
||
['1,5', 'setHotKey', new SettingButton(40, 310, 150, 25, '7'), 7],
|
||
['1,6', 'clearHotKeys', new SettingButton(300, 350, 42, 25)],
|
||
]);
|
||
return keyMenu;
|
||
}
|
||
// #endregion
|
||
// #region 自定义工具栏界面
|
||
/** 自定义工具栏界面 */
|
||
class ToolBtn extends ButtonBase {
|
||
constructor(x, y, w, h, icon, text, config) {
|
||
super(x, y, w, h);
|
||
/** @type {ToolBarConfigPage} */ // @ts-ignore
|
||
this.menu;
|
||
this.icon = icon; // 特殊icon:delete 用于删除图标
|
||
/** @todo 这里需要重构 */
|
||
this.text = text;
|
||
this.config = config || {};
|
||
this.ondown = () => {
|
||
this.menu.selectedTool = this.icon;
|
||
this.menu.text = this.text;
|
||
this.menu.drawContent();
|
||
};
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const { strokeStyle = 'white', fillStyle = 'white', selectedStyle = 'gold' } = this.config;
|
||
core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, (this.menu.selectedTool === this.icon) ? selectedStyle : strokeStyle);
|
||
if (this.icon === 'delete') {
|
||
core.drawLine(ctx, this.x + 2, this.y + 2, this.x + this.w - 2, this.y + this.h - 2, 'red', 2);
|
||
core.drawLine(ctx, this.x + 2, this.y + this.h - 2, this.x + this.w - 2, this.y + 2, 'red', 2);
|
||
} else core.drawIcon(ctx, this.icon, this.x, this.y, this.w, this.h);
|
||
}
|
||
}
|
||
|
||
class ToolBarBtn extends ButtonBase {
|
||
constructor(x, y, size, type, length, text) {
|
||
super(x, y, length * size, size);
|
||
/** @type {ToolBarConfigPage} */ // @ts-ignore
|
||
this.menu;
|
||
this.type = type;
|
||
this.length = length;
|
||
/** @todo 这里需要重构 */
|
||
this.text = text;
|
||
this.ondown = (x, y, px, py) => {
|
||
const squareSize = this.h;
|
||
const index = Math.floor((px - this.x) / squareSize);
|
||
this.menu.type = type;
|
||
this.menu.index = index;
|
||
this.menu.drawContent();
|
||
}
|
||
}
|
||
|
||
draw() {
|
||
const ctx = this.ctx;
|
||
const squareSize = this.h;
|
||
const type = this.type;
|
||
const toolBarConfig = core.plugin.getToolBarConfig(type);
|
||
for (let i = 0; i < this.length; i++) {
|
||
const style = (this.menu.type === type && this.menu.index === i) ? 'gold' : 'white';
|
||
core.strokeRoundRect(ctx, this.x + squareSize * i + i, this.y, squareSize, squareSize, 3, style);
|
||
core.drawIcon(ctx, toolBarConfig[i], this.x + squareSize * i + i, this.y, squareSize, squareSize);
|
||
}
|
||
}
|
||
}
|
||
|
||
class ToolBarConfigPage extends MenuBase {
|
||
constructor() {
|
||
super('toolBarConfig', ['ondown']);
|
||
/** 当前选中的图标 */
|
||
this.selectedTool = 'none';
|
||
/** 当前选中了哪个类型的工具栏 */
|
||
this.type = 'none';
|
||
/** 当前选中工具栏哪个位置(1-9) */
|
||
this.index = -1;
|
||
this.text = '';
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.setTextAlign(ctx, 'left');
|
||
core.fillText(ctx, '常规', 40, 175, ' #FFE4B5', '16px Verdana');
|
||
core.fillText(ctx, '数字', 40, 205, ' #FFE4B5', '16px Verdana');
|
||
core.fillText(ctx, '录像', 40, 235, ' #FFE4B5', '16px Verdana');
|
||
core.fillText(ctx, '可选按钮', 40, 265, ' #FFE4B5', '16px Verdana');
|
||
if (this.text && this.text.length > 0) {
|
||
core.ui.drawTextContent(ctx, this.text, {
|
||
left: 30,
|
||
top: 78,
|
||
bold: false,
|
||
color: "white",
|
||
align: "left",
|
||
fontSize: 14,
|
||
maxWidth: 350
|
||
});
|
||
}
|
||
super.drawContent();
|
||
}
|
||
|
||
setToolBarConfig() {
|
||
core.setToolBarConfig(this.type, this.index, this.selectedTool);
|
||
this.drawContent();
|
||
}
|
||
}
|
||
|
||
function toolBarConfigFactory() {
|
||
// 名字不能叫toolBar 画布toolBar被系统占了
|
||
const toolBarMenu = new ToolBarConfigPage();
|
||
const changeToolBarBtn = new RoundBtn(320, 158, 42, 24, '执行', -1);
|
||
changeToolBarBtn.ondown = function () {
|
||
core.setToolBarConfig(this.menu.type, this.menu.index, this.menu.selectedTool);
|
||
this.menu.drawContent();
|
||
}.bind(changeToolBarBtn);
|
||
toolBarMenu.registerBtns([
|
||
['1,1', new ToolBarBtn(80, 158, 24, 'normal', 9, '常规模式下显示在工具栏中的图标。')],
|
||
['1,2', changeToolBarBtn, () => toolBarMenu.setToolBarConfig()],
|
||
['2,1', new ToolBarBtn(80, 188, 24, 'num', 9, '数字模式下显示在工具栏中的图标。')],
|
||
['3,1', new ToolBarBtn(80, 218, 24, 'replay', 9, '录像模式下显示在工具栏中的图标。')],
|
||
['5,1', new ToolBtn(40, 275, 24, 24, 'book', '打开怪物手册')],
|
||
['5,2', new ToolBtn(70, 275, 24, 24, 'fly', '进行楼层传送')],
|
||
['5,3', new ToolBtn(100, 275, 24, 24, 'toolbox', '打开物品背包')],
|
||
['5,4', new ToolBtn(130, 275, 24, 24, 'equipbox', '打开装备背包')],
|
||
['5,5', new ToolBtn(160, 275, 24, 24, 'keyboard', '打开虚拟键盘')],
|
||
['5,6', new ToolBtn(190, 275, 24, 24, 'shop', '打开快捷商店')],
|
||
['5,7', new ToolBtn(220, 275, 24, 24, 'save', '存档')],
|
||
['5,8', new ToolBtn(250, 275, 24, 24, 'load', '读档')],
|
||
['5,9', new ToolBtn(280, 275, 24, 24, 'settings', '打开系统设置')],
|
||
['5,10', new ToolBtn(310, 275, 24, 24, 'rollback', '读取自动存档')],
|
||
['5,11', new ToolBtn(340, 275, 24, 24, 'undoRollback', '取消读取自动存档')],
|
||
['6,1', new ToolBtn(40, 305, 24, 24, 'btn1', '数字键1')],
|
||
['6,2', new ToolBtn(70, 305, 24, 24, 'btn2', '数字键2')],
|
||
['6,3', new ToolBtn(100, 305, 24, 24, 'btn3', '数字键3')],
|
||
['6,4', new ToolBtn(130, 305, 24, 24, 'btn4', '数字键4')],
|
||
['6,5', new ToolBtn(160, 305, 24, 24, 'btn5', '数字键5')],
|
||
['6,6', new ToolBtn(190, 305, 24, 24, 'btn6', '数字键6')],
|
||
['6,7', new ToolBtn(220, 305, 24, 24, 'btn7', '数字键7')],
|
||
['6,8', new ToolBtn(250, 305, 24, 24, 'btn8', '数字键8')],
|
||
['6,9', new ToolBtn(280, 305, 24, 24, 'btn9', '数字键9')],
|
||
['6,10', new ToolBtn(310, 305, 24, 24, 'btnAlt', '开关Alt模式(需要配合数字键使用)')],
|
||
['7,1', new ToolBtn(40, 335, 24, 24, 'play', '播放/暂停录像')],
|
||
['7,2', new ToolBtn(70, 335, 24, 24, 'stop', '停止播放录像')],
|
||
['7,3', new ToolBtn(100, 335, 24, 24, 'rewind', '录像模式下回退')],
|
||
['7,4', new ToolBtn(130, 335, 24, 24, 'speedDown', '减速录像(最低0.2倍)')],
|
||
['7,5', new ToolBtn(160, 335, 24, 24, 'speedUp', '加速录像(最高24倍)')],
|
||
['7,6', new ToolBtn(190, 335, 24, 24, 'single', '单步播放录像')],
|
||
['7,7', new ToolBtn(220, 335, 24, 24, 'view', '浏览地图')],
|
||
['7,8', new ToolBtn(250, 335, 24, 24, 'delete', '删除已有图标')],
|
||
]);
|
||
return toolBarMenu;
|
||
}
|
||
// #endregion
|
||
// #region 控制台界面
|
||
/** @type {{[x:string]:Setting}} */
|
||
const consoleSetting = {
|
||
debug_wallHacking: {
|
||
getName: () => ' 穿墙:' + (core.hasFlag('debug_wallHacking') ? '开' : '关'),
|
||
effect: () => {
|
||
core.setFlag('debug', true);
|
||
invertFlag('debug_wallHacking');
|
||
},
|
||
text: '开启时将始终穿墙并无视各种事件,无论是否按下Ctrl。',
|
||
replay: false,
|
||
},
|
||
debug_statusName: {
|
||
getName: () => core.getFlag('debug_statusName', '??'),
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
const dictionary = {
|
||
'体力': 'hp',
|
||
'血量': 'hp',
|
||
'生命': 'hp',
|
||
'血': 'hp',
|
||
'体力上限': 'hpmax',
|
||
'血量上限': 'hpmax',
|
||
'生命上限': 'hpmax',
|
||
'血限': 'hpmax',
|
||
'攻击': 'atk',
|
||
'攻': 'atk',
|
||
'防御': 'def',
|
||
'防': 'def',
|
||
'魔防': 'mdef',
|
||
'护盾': 'mdef',
|
||
'mf': 'mdef',
|
||
'金币': 'money',
|
||
'金钱': 'money',
|
||
'钱': 'money',
|
||
'经验': 'exp',
|
||
'魔力': 'mana',
|
||
'魔': 'mana',
|
||
'蓝': 'mana',
|
||
};
|
||
core.utils.myprompt('输入要修改的属性名称', '', (value) => {
|
||
const heroStatus = core.status.hero;
|
||
if (dictionary.hasOwnProperty(value)) {
|
||
value = dictionary[value];
|
||
}
|
||
if (heroStatus && heroStatus.hasOwnProperty(value) && ['hp', 'hpmax', 'atk', 'def', 'mdef', 'money', 'exp', 'mana', 'manamax'].includes(value)) {
|
||
core.setFlag('debug_statusName', value);
|
||
this.menu.drawContent();
|
||
} else {
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
}
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_statusValue: {
|
||
getName: () => {
|
||
let value = core.getFlag('debug_statusValue', '??');
|
||
if (typeof value === 'number') return core.formatBigNumber(value, 5);
|
||
else return value;
|
||
},
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
core.utils.myprompt('输入要修改到的值', null, (input) => {
|
||
const value = parseInt(input);
|
||
if (!Number.isNaN(value)) {
|
||
core.setFlag('debug_statusValue', value);
|
||
this.menu.drawContent();
|
||
} else {
|
||
core.drawFailTip('错误:不合法的值!');
|
||
}
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_setStatus: {
|
||
getName: () => '',
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
const name = core.getFlag('debug_statusName'),
|
||
value = core.getFlag('debug_statusValue');
|
||
if (!(name && core.status.hero && core.status.hero.hasOwnProperty(name))) {
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
return;
|
||
}
|
||
if (!Number.isInteger(value)) {
|
||
core.drawFailTip('错误:不合法的值!');
|
||
return;
|
||
}
|
||
core.setFlag('debug', true);
|
||
core.setStatus(name, value);
|
||
core.updateStatusBar();
|
||
core.drawSuccessTip('设置成功!');
|
||
},
|
||
text: '将角色状态设为相应值。',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
|
||
core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
|
||
core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
|
||
},
|
||
},
|
||
debug_itemName: {
|
||
getName: () => core.getFlag('debug_itemName', '??'),
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
core.utils.myprompt('输入要修改的物品名称', null, (value) => {
|
||
const itemInfo = core.material.items;
|
||
if (itemInfo) {
|
||
const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value);
|
||
if (aimItem) {
|
||
core.setFlag('debug_itemName', aimItem.id);
|
||
this.menu.drawContent();
|
||
return;
|
||
}
|
||
}
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_itemValue: {
|
||
getName: () => core.getFlag('debug_itemValue', '??'),
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
core.setFlag('debug', true);
|
||
core.utils.myprompt('输入要修改到的值', null, (input) => {
|
||
const value = parseInt(input);
|
||
if (!Number.isNaN(value)) {
|
||
core.setFlag('debug_itemValue', value);
|
||
this.menu.drawContent();
|
||
} else {
|
||
core.drawFailTip('错误:不合法的值!');
|
||
}
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_setItem: {
|
||
getName: () => '',
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
const name = core.getFlag('debug_itemName'),
|
||
value = core.getFlag('debug_itemValue');
|
||
const itemInfo = core.material.items;
|
||
|
||
if (name && itemInfo) {
|
||
let itemExist = Object.values(itemInfo).some((item) => item.id === name);
|
||
if (!itemExist) {
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
return;
|
||
}
|
||
} else {
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
return;
|
||
}
|
||
|
||
if (!Number.isInteger(value)) {
|
||
core.drawFailTip('错误:不合法的值!');
|
||
return;
|
||
}
|
||
|
||
core.setFlag('debug', true);
|
||
core.setItem(name, value);
|
||
core.updateStatusBar();
|
||
core.drawSuccessTip('设置成功!');
|
||
},
|
||
text: '将道具数设为相应值。',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
|
||
core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
|
||
core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
|
||
},
|
||
},
|
||
debug_flagName: {
|
||
getName: () => core.getFlag('debug_flagName', '??'),
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
core.setFlag('debug', true);
|
||
core.utils.myprompt('输入要修改的变量名。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => {
|
||
if (!value.startsWith('debug')) {
|
||
core.setFlag('debug_flagName', value);
|
||
this.menu.drawContent();
|
||
} else {
|
||
core.drawFailTip('错误:不合法的名称!');
|
||
}
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_flagValue: {
|
||
getName: () => core.getFlag('debug_flagValue', '??'),
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
core.setFlag('debug', true);
|
||
core.utils.myprompt('输入要修改到的值。注意:如果您不了解修改变量的后果,请勿尝试。', null, (value) => {
|
||
let newValue;
|
||
try {
|
||
newValue = JSON.parse(value.trim());
|
||
} catch {
|
||
core.drawFailTip('错误:不合法的值,无法解析!');
|
||
return;
|
||
}
|
||
core.setFlag('debug_flagValue', newValue);
|
||
this.menu.drawContent();
|
||
}, () => { });
|
||
},
|
||
text: '',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
|
||
},
|
||
},
|
||
debug_setFlag: {
|
||
getName: () => '',
|
||
/** @this {ConsoleMenu} */
|
||
effect: function () {
|
||
const name = core.getFlag('debug_flagName'),
|
||
value = core.getFlag('debug_flagValue');
|
||
if (!name) {
|
||
core.drawFailTip('错误:不合法的变量名称!');
|
||
return;
|
||
}
|
||
core.setFlag('debug', true);
|
||
core.setFlag(name, value);
|
||
core.updateStatusBar();
|
||
core.drawSuccessTip('设置成功!');
|
||
},
|
||
text: '将变量设为相应值。',
|
||
replay: false,
|
||
/** @this {SettingButton} */
|
||
draw: function (ctx) {
|
||
core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
|
||
core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
|
||
core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
|
||
},
|
||
},
|
||
};
|
||
|
||
class ConsoleMenu extends SettingOnePage {
|
||
constructor() {
|
||
super('console', consoleSetting);
|
||
this.menu = this;
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
const consoleWarnText =
|
||
"本页面的功能仅供调试用。使用后相应存档将变红,录像不能通过,且无法提交。请读档到普通存档后正常游玩方可提交。";
|
||
core.setTextAlign(ctx, 'left');
|
||
core.setTextBaseline(ctx, 'alphabetic');
|
||
core.fillText(ctx, "本页面的功能仅供调试用。使用后相应存档将变红,录像", 30, 170, " #FFC0CB", '14px Verdana');
|
||
core.fillText(ctx, "不能通过,且无法提交。请读档到普通存档后正常游玩方", 30, 190, " #FFC0CB", '14px Verdana');
|
||
core.fillText(ctx, "可提交。", 30, 210, " #FFC0CB", '14px Verdana');
|
||
core.fillText(ctx, "属性", 45, 264, 'white', '16px Verdana');
|
||
core.fillText(ctx, "设为", 170, 264, 'white', '16px Verdana');
|
||
core.fillText(ctx, "物品", 45, 290, 'white', '16px Verdana');
|
||
core.fillText(ctx, "数量设为", 170, 290, 'white', '16px Verdana');
|
||
core.fillText(ctx, "变量", 45, 316, 'white', '16px Verdana');
|
||
core.fillText(ctx, "设为", 170, 316, 'white', '16px Verdana');
|
||
super.drawContent(ctx);
|
||
}
|
||
}
|
||
|
||
function consoleMenuFactory() {
|
||
const consoleMenu = new ConsoleMenu();
|
||
consoleMenu.registerSettingBtns([
|
||
['1,1', 'debug_wallHacking', new SettingButton(40, 220, 150, 25)],
|
||
['1,2', 'debug_statusName', new SettingButton(80, 250, 80, 20)],
|
||
['2,2', 'debug_statusValue', new SettingButton(210, 250, 80, 20)],
|
||
['3,2', 'debug_setStatus', new SettingButton(340, 250, 40, 20)],
|
||
['1,3', 'debug_itemName', new SettingButton(80, 276, 80, 20)],
|
||
['2,3', 'debug_itemValue', new SettingButton(240, 276, 80, 20)],
|
||
['3,3', 'debug_setItem', new SettingButton(340, 276, 40, 20)],
|
||
['1,4', 'debug_flagName', new SettingButton(80, 302, 80, 20)],
|
||
['2,4', 'debug_flagValue', new SettingButton(210, 302, 80, 20)],
|
||
['3,4', 'debug_setFlag', new SettingButton(340, 302, 40, 20)],
|
||
]);
|
||
return consoleMenu;
|
||
}
|
||
// #endregion
|
||
// #region 敌人信息显示调节界面
|
||
const infoNameMap = {
|
||
'damage': '伤害', 'critical': '临界', 'hp': core.control.getStatusLabel('hp'),
|
||
'atk': core.control.getStatusLabel('atk'), 'def': core.control.getStatusLabel('def'),
|
||
'money': core.control.getStatusLabel('money'), 'exp': core.control.getStatusLabel('exp'),
|
||
'criticalDamage': '临界减伤', 'defDamage': '1防减伤', 'special': '特殊属性',
|
||
}
|
||
|
||
class DisplayInfoBtn extends RoundBtn {
|
||
constructor(x, y, w, h, pos, index) {
|
||
super(x, y, w, h);
|
||
this.pos = pos;
|
||
this.index = index;
|
||
}
|
||
|
||
draw() {
|
||
super.draw();
|
||
const isEmpty = !this.infoName;
|
||
const ctx = this.ctx;
|
||
const { x, y, w, h } = this;
|
||
const [x0, y0, r, offset] = [x + w + 15, y + h / 2, h / 2 - 2, 2];
|
||
core.fillCircle(ctx, x0, y0, r, isEmpty ? 'lime' : 'red');
|
||
core.drawLine(ctx, x0 - r + offset, y0, x0 + r - offset, y0, 'white', 2);
|
||
if (isEmpty) core.drawLine(ctx, x0, y0 - r + offset, x0, y0 + r - offset, 'white', 2);
|
||
}
|
||
|
||
inRange(px, py) { // 实际的点击判定区是右边的圆形
|
||
const { x, y, w, h } = this;
|
||
return px >= x + w + 15 - h / 2 && px <= x + w + 15 + h / 2 && py >= y && py <= y + h;
|
||
}
|
||
}
|
||
|
||
class SpecialIconBtn extends RoundBtn { }
|
||
|
||
class AdvanceDisplayMenu extends MenuBase {
|
||
constructor() {
|
||
super('advanceDisplay', ['ondown']);
|
||
this.selectedBtn = null;
|
||
this.getData();
|
||
this.getAllSpecials();
|
||
this.getSpecialIconData();
|
||
this.specialIconPage = 0;
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = this.createCanvas();
|
||
core.setTextAlign(ctx, 'center');
|
||
core.setTextBaseline(ctx, 'alphabetic');
|
||
core.fillRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 320, 5, "rgba(50,50,50,1)");
|
||
core.fillText(ctx, "设定敌人左下角显示的数据", 110, 90, 'white', '14px Verdana');
|
||
core.fillText(ctx, "设定敌人右上角显示的数据", 110, 190, 'white', '14px Verdana');
|
||
core.fillText(ctx, "设定特殊属性的代表字符", 110, 290, 'white', '14px Verdana');
|
||
this.getData();
|
||
this.setInfoName();
|
||
this.getSpecialIconData();
|
||
super.drawContent(ctx);
|
||
}
|
||
|
||
clear() {
|
||
super.clear();
|
||
const settingMenu = core.plugin.settingMenu;
|
||
if (settingMenu) {
|
||
settingMenu.beginListen();
|
||
settingMenu.pageList[settingMenu.currPage].beginListen();
|
||
}
|
||
}
|
||
|
||
getData() {
|
||
const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} };
|
||
this.data = core.getLocalStorage('displayData', defaultData);
|
||
}
|
||
|
||
setData(pos, index, infoName) {
|
||
this.data[pos][index] = infoName;
|
||
core.setLocalStorage('displayData', this.data);
|
||
this.setInfoName();
|
||
}
|
||
|
||
setInfoName() {
|
||
this.btnMap.forEach(btn => {
|
||
if (!(btn instanceof DisplayInfoBtn)) return;
|
||
const pos = btn.pos;
|
||
const index = btn.index;
|
||
const infoName = this.data[pos][index];
|
||
if (infoName) {
|
||
btn.infoName = infoName;
|
||
const name = infoNameMap[infoName];
|
||
btn.text = name;
|
||
}
|
||
else {
|
||
btn.infoName = null;
|
||
btn.text = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
getAllSpecials() { // allSpecials是所有用到了的特殊属性的数组
|
||
let allSpecialsSet = new Set();
|
||
Object.values(core.material.enemys).forEach(enemy => {
|
||
const special = core.utils.parseSpecial(enemy.special);
|
||
allSpecialsSet = new Set([...allSpecialsSet, ...special]);
|
||
})
|
||
this.allSpecials = [...allSpecialsSet].sort((a, b) => a - b);
|
||
}
|
||
|
||
getSpecialIconData() {
|
||
const specialData = core.getLocalStorage('specialIconData', {});
|
||
this.specialIconData = specialData;
|
||
}
|
||
|
||
getCurrSpecialIconList() {
|
||
return this.allSpecials.slice(this.specialIconPage * 6,
|
||
this.specialIconPage * 6 + 6);
|
||
}
|
||
|
||
/** 更新每个按钮对应的文本,在翻页和修改内容时需要手动调用 */
|
||
setSpecialIconBtnText() {
|
||
const specialIconList = this.getCurrSpecialIconList();
|
||
for (let i = 0; i <= 5; i++) {
|
||
const key = 's' + (i + 1);
|
||
const btn = this.btnMap.get(key);
|
||
const index = this.specialIconPage * 6 + i;
|
||
if (index >= this.allSpecials.length) {
|
||
btn.text = '';
|
||
}
|
||
else {
|
||
const specialNum = this.allSpecials[index];
|
||
const specialIndexMap = core.enemys.getSpecialIndexMap();
|
||
const [key, name] = specialIndexMap[specialNum];
|
||
const icon = this.specialIconData[specialNum] || "无";
|
||
btn.text = `${key}:${name} ${icon}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
setSpecialIconData(index) {
|
||
const specialIconList = this.getCurrSpecialIconList();
|
||
const specialNum = specialIconList[index];
|
||
if (!specialNum) return;
|
||
core.utils.myprompt('输入该特殊属性的代表字符', null, (value) => {
|
||
if (value.length > 1) value = value[0]; // 最多保留一位字符
|
||
this.specialIconData[specialNum] = value;
|
||
core.setLocalStorage('specialIconData', this.specialIconData);
|
||
this.setSpecialIconBtnText();
|
||
this.drawContent();
|
||
});
|
||
}
|
||
|
||
pageUp() {
|
||
if (this.specialIconPage * 6 + 6 < this.allSpecials.length) {
|
||
this.specialIconPage++;
|
||
this.setSpecialIconBtnText();
|
||
this.drawContent();
|
||
}
|
||
}
|
||
|
||
pageDown() {
|
||
if (this.specialIconPage > 0) {
|
||
this.specialIconPage--;
|
||
this.setSpecialIconBtnText();
|
||
this.drawContent();
|
||
}
|
||
}
|
||
}
|
||
|
||
function advanceDisplayFactory() {
|
||
const advanceDisplayMenu = new AdvanceDisplayMenu();
|
||
const exitBtn = new ExitBtn(370, 80, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 })
|
||
advanceDisplayMenu.registerBtn('exitBtn', exitBtn, () => {
|
||
advanceDisplayMenu.clear();
|
||
const settingMenu = core.plugin.settingMenu;
|
||
settingMenu.btnMap.forEach(btn => { btn.disable = false });
|
||
settingMenu.drawContent();
|
||
});
|
||
const btn1 = new DisplayInfoBtn(50, 100, 75, 20, 'leftdown', 1),
|
||
btn2 = new DisplayInfoBtn(50, 125, 75, 20, 'leftdown', 2),
|
||
btn3 = new DisplayInfoBtn(50, 150, 75, 20, 'leftdown', 3),
|
||
btn4 = new DisplayInfoBtn(50, 200, 75, 20, 'rightup', 1),
|
||
btn5 = new DisplayInfoBtn(50, 225, 75, 20, 'rightup', 2),
|
||
btn6 = new DisplayInfoBtn(50, 250, 75, 20, 'rightup', 3);
|
||
const infoNameList = Object.keys(infoNameMap);
|
||
const l = infoNameList.length;
|
||
|
||
const setNewInfo = function (infoName) {
|
||
return function () {
|
||
const btn = advanceDisplayMenu.selectedBtn;
|
||
if (!btn) return;
|
||
advanceDisplayMenu.setData(btn.pos, btn.index, infoName);
|
||
advanceDisplayMenu.btnMap.forEach((btn, key) => {
|
||
if (btn.key.startsWith("temp")) btn.disable = true;
|
||
});
|
||
advanceDisplayMenu.selectedBtn = null;
|
||
advanceDisplayMenu.drawContent();
|
||
core.control.updateStatusBar(); // 手动刷新一下地图显伤
|
||
}
|
||
}
|
||
for (let i = 0; i < 5; i++) {
|
||
const tempBtn = new RoundBtn(200, 100 + i * 25, 80, 20, infoNameMap[infoNameList[i]], { fillStyle: 'Azure' });
|
||
tempBtn.disable = true;
|
||
advanceDisplayMenu.registerBtn('temp' + i, tempBtn, setNewInfo(infoNameList[i]));
|
||
}
|
||
for (let i = 5; i < l; i++) {
|
||
const tempBtn = new RoundBtn(300, 100 + (i - 5) * 25, 80, 20, infoNameMap[infoNameList[i]], { fillStyle: 'Azure' });
|
||
tempBtn.disable = true;
|
||
advanceDisplayMenu.registerBtn('temp' + i, tempBtn, setNewInfo(infoNameList[i]));
|
||
}
|
||
const change = function (btn) {
|
||
return function () {
|
||
if (btn.infoName) {
|
||
advanceDisplayMenu.setData(btn.pos, btn.index, null);
|
||
}
|
||
else {
|
||
if (advanceDisplayMenu.selectedBtn && advanceDisplayMenu.selectedBtn === btn) {
|
||
// 点击左边刚点过的按钮会收起展开菜单
|
||
advanceDisplayMenu.btnMap.forEach((btn, key) => {
|
||
if (btn.key.startsWith("temp")) btn.disable = true;
|
||
});
|
||
advanceDisplayMenu.selectedBtn = null;
|
||
advanceDisplayMenu.drawContent();
|
||
} else {
|
||
advanceDisplayMenu.selectedBtn = btn;
|
||
advanceDisplayMenu.btnMap.forEach((btn, key) => {
|
||
if (btn.key.startsWith("temp")) btn.disable = false;
|
||
});
|
||
}
|
||
}
|
||
advanceDisplayMenu.drawContent();
|
||
}
|
||
}
|
||
advanceDisplayMenu.registerBtns([
|
||
['1', btn1, change(btn1)], ['2', btn2, change(btn2)], ['3', btn3, change(btn3)],
|
||
['4', btn4, change(btn4)], ['5', btn5, change(btn5)], ['6', btn6, change(btn6)],
|
||
]);
|
||
const config = { "font": "12px Verdana" };
|
||
advanceDisplayMenu.registerBtn('s1', new SpecialIconBtn(50, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 0));
|
||
advanceDisplayMenu.registerBtn('s2', new SpecialIconBtn(150, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 1));
|
||
advanceDisplayMenu.registerBtn('s3', new SpecialIconBtn(250, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 2));
|
||
advanceDisplayMenu.registerBtn('s4', new SpecialIconBtn(50, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 3));
|
||
advanceDisplayMenu.registerBtn('s5', new SpecialIconBtn(150, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 4));
|
||
advanceDisplayMenu.registerBtn('s6', new SpecialIconBtn(250, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 5));
|
||
advanceDisplayMenu.registerBtn('pageDown', new ArrowBtn(50, 360, 16, 16, 'left'),
|
||
advanceDisplayMenu.pageDown.bind(advanceDisplayMenu));
|
||
advanceDisplayMenu.registerBtn('pageUp', new ArrowBtn(320, 360, 16, 16, 'right'),
|
||
advanceDisplayMenu.pageUp.bind(advanceDisplayMenu));
|
||
advanceDisplayMenu.setSpecialIconBtnText();
|
||
return advanceDisplayMenu;
|
||
}
|
||
// #endregion
|
||
|
||
class PageChangeBtn extends RoundBtn {
|
||
constructor(x, y, w, h, text) {
|
||
super(x, y, w, h, text);
|
||
/** @type {SettingBack} */ // @ts-ignore
|
||
this.menu;
|
||
this.ondown = () => this.menu.changePage(this.key);
|
||
}
|
||
}
|
||
|
||
class SettingBack extends Pagination {
|
||
constructor(pageList, currPage, name) {
|
||
super(pageList, currPage, name, ['ondown', 'keyDown']);
|
||
}
|
||
|
||
changePage(index) {
|
||
this.btnMap.forEach((btn, key) => {
|
||
btn.status = (key === index) ? 'selected' : 'none';
|
||
});
|
||
super.changePage(index);
|
||
this.drawContent();
|
||
}
|
||
|
||
/** @override */
|
||
keyDownEvent(keyCode) {
|
||
if (keyCode === KeyCodeEnum.PageDown) this.pageDown();
|
||
else if (keyCode === KeyCodeEnum.PageUp) this.pageUp();
|
||
else if (keyCode === KeyCodeEnum.Esc) this.quit();
|
||
}
|
||
|
||
quit() {
|
||
this.clear();
|
||
setTimeout(core.unlockControl, 0); // 消抖,防止点击关闭按钮的一瞬间触发瞬移。
|
||
}
|
||
|
||
drawSettingBackGround(ctx) {
|
||
core.strokeRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "white", 2);
|
||
core.fillRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "gray");
|
||
|
||
// 绘制设置说明的文本框
|
||
core.strokeRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 70, 3, "white");
|
||
core.fillRoundRect(ctx, 21, 71, core.__PIXELS__ - 42, 68, 3, " #555555");
|
||
|
||
// 绘制设置的框体
|
||
core.strokeRoundRect(ctx, 20, 150, core.__PIXELS__ - 40, 256, 3, "white");
|
||
core.fillRoundRect(ctx, 21, 151, core.__PIXELS__ - 42, 254, 3, " #999999");
|
||
|
||
core.setTextAlign(ctx, 'center');
|
||
core.ui.fillText(ctx, "设置", core.__PIXELS__ / 2, 25, 'white', '20px Verdana');
|
||
}
|
||
|
||
drawContent() {
|
||
const ctx = core.createCanvas(this.name, this.x, this.y, this.w, this.h, 136);
|
||
this.drawSettingBackGround(ctx);
|
||
super.drawContent();
|
||
// this.initOnePage(this.currPage);
|
||
}
|
||
}
|
||
|
||
const allSettings = {
|
||
...gamePlaySetting,
|
||
...gameViewSetting,
|
||
...keySetting,
|
||
...consoleSetting,
|
||
};
|
||
// 注册点击SettingButton的行为cSet, 只有replay为真时计入录像
|
||
core.registerReplayAction('cSet', (action) => {
|
||
const strArr = action.split(':');
|
||
if (strArr[0] !== 'cSet') return false;
|
||
const currSetting = allSettings[strArr[1]]; //@todo bugfix
|
||
if (!currSetting || !currSetting.replay || strArr[1].startsWith('debug')) return false;
|
||
|
||
let params = strArr.slice(2);
|
||
|
||
if (params.length > 0) {
|
||
currSetting.effect.apply(currSetting, params);
|
||
} else {
|
||
currSetting.effect.call(currSetting);
|
||
}
|
||
|
||
core.status.route.push(action);
|
||
core.replay();
|
||
return true;
|
||
});
|
||
|
||
this.openSetting = function () {
|
||
if (core.isReplaying()) return;
|
||
core.lockControl();
|
||
const ctx = 'setting';
|
||
|
||
const gamePlayMenu = gamePlayFactory();
|
||
const gameViewMenu = gameViewFactory();
|
||
const keyMenu = keyMenuFactory();
|
||
const toolBarMenu = toolBarConfigFactory();
|
||
const consoleMenu = consoleMenuFactory();
|
||
|
||
// 在此处添加新的菜单页面
|
||
const settingMenu = new SettingBack([gamePlayMenu, gameViewMenu, keyMenu, toolBarMenu, consoleMenu], 0, ctx);
|
||
|
||
// 主页面的按钮列表
|
||
const gamePlayBtn = new PageChangeBtn(32, 40, 46, 24, '功能'),
|
||
gameViewBtn = new PageChangeBtn(92, 40, 46, 24, '音画'),
|
||
keyBtn = new PageChangeBtn(152, 40, 46, 24, '按键'),
|
||
toolBarBtn = new PageChangeBtn(212, 40, 66, 24, '工具栏'),
|
||
consoleBtn = new PageChangeBtn(292, 40, 66, 24, '控制台');
|
||
const quit = new TextButton(360, 10, 45, 25, '[退出]');
|
||
|
||
settingMenu.registerBtns([
|
||
[0, gamePlayBtn],
|
||
[1, gameViewBtn],
|
||
[2, keyBtn],
|
||
[3, toolBarBtn],
|
||
[4, consoleBtn],
|
||
['quit', quit, () => settingMenu.quit()]
|
||
]);
|
||
|
||
// 放缩时重绘整个大menu
|
||
core.plugin.settingMenu = settingMenu;
|
||
// 设置初始时选中的按键为第一个按键
|
||
gamePlayBtn.status = 'selected';
|
||
|
||
settingMenu.init();
|
||
settingMenu.changePage(0);
|
||
}
|
||
// @todo 新版存档界面
|
||
},
|
||
"opusAdaptation": function () {
|
||
// 将__enable置为false将关闭插件
|
||
let __enable = true;
|
||
if (!__enable || main.mode === "editor") return;
|
||
const { OggOpusDecoderWebWorker } = window["ogg-opus-decoder"];
|
||
const { OggVorbisDecoderWebWorker } = window["ogg-vorbis-decoder"];
|
||
const { CodecParser } = window.CodecParser;
|
||
const { Transition, linear } = core.plugin.animate;
|
||
|
||
const audio = new Audio();
|
||
const AudioStatus = {
|
||
Playing: 0,
|
||
Pausing: 1,
|
||
Paused: 2,
|
||
Stoping: 3,
|
||
Stoped: 4,
|
||
};
|
||
const supportMap = new Map();
|
||
const AudioType = {
|
||
Mp3: "audio/mpeg",
|
||
Wav: 'audio/wav; codecs="1"',
|
||
Flac: "audio/flac",
|
||
Opus: 'audio/ogg; codecs="opus"',
|
||
Ogg: 'audio/ogg; codecs="vorbis"',
|
||
Aac: "audio/aac",
|
||
};
|
||
/**
|
||
* 检查一种音频类型是否能被播放
|
||
* @param type 音频类型 AudioType
|
||
*/
|
||
function isAudioSupport(type) {
|
||
if (supportMap.has(type)) return supportMap.get(type);
|
||
else {
|
||
const support = audio.canPlayType(type);
|
||
const canPlay = support === "maybe" || support === "probably";
|
||
supportMap.set(type, canPlay);
|
||
return canPlay;
|
||
}
|
||
}
|
||
|
||
const typeMap = new Map([
|
||
["ogg", AudioType.Ogg],
|
||
["mp3", AudioType.Mp3],
|
||
["wav", AudioType.Wav],
|
||
["flac", AudioType.Flac],
|
||
["opus", AudioType.Opus],
|
||
["aac", AudioType.Aac],
|
||
]);
|
||
|
||
/**
|
||
* 根据文件名拓展猜测其类型
|
||
* @param file 文件名 string
|
||
*/
|
||
function guessTypeByExt(file) {
|
||
const ext = /\.[a-zA-Z\d]+$/.exec(file);
|
||
if (!ext?.[0]) return "";
|
||
const type = ext[0].slice(1);
|
||
return typeMap.get(type.toLocaleLowerCase()) ?? "";
|
||
}
|
||
|
||
isAudioSupport(AudioType.Ogg);
|
||
isAudioSupport(AudioType.Mp3);
|
||
isAudioSupport(AudioType.Wav);
|
||
isAudioSupport(AudioType.Flac);
|
||
isAudioSupport(AudioType.Opus);
|
||
isAudioSupport(AudioType.Aac);
|
||
|
||
function isNil(value) {
|
||
return value === void 0 || value === null;
|
||
}
|
||
|
||
function sleep(time) {
|
||
return new Promise((res) => setTimeout(res, time));
|
||
}
|
||
class AudioEffect {
|
||
constructor(ac) { }
|
||
/**
|
||
* 连接至其他效果器
|
||
* @param target 目标输入 IAudioInput
|
||
* @param output 当前效果器输出通道 Number
|
||
* @param input 目标效果器的输入通道 Number
|
||
*/
|
||
connect(target, output, input) {
|
||
this.output.connect(target.input, output, input);
|
||
}
|
||
|
||
/**
|
||
* 与其他效果器取消连接
|
||
* @param target 目标输入 IAudioInput
|
||
* @param output 当前效果器输出通道 Number
|
||
* @param input 目标效果器的输入通道 Number
|
||
*/
|
||
disconnect(target, output, input) {
|
||
if (!target) {
|
||
if (!isNil(output)) {
|
||
this.output.disconnect(output);
|
||
} else {
|
||
this.output.disconnect();
|
||
}
|
||
} else {
|
||
if (!isNil(output)) {
|
||
if (!isNil(input)) {
|
||
this.output.disconnect(target.input, output, input);
|
||
} else {
|
||
this.output.disconnect(target.input, output);
|
||
}
|
||
} else {
|
||
this.output.disconnect(target.input);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class StereoEffect extends AudioEffect {
|
||
constructor(ac) {
|
||
super(ac);
|
||
const panner = ac.createPanner();
|
||
this.input = panner;
|
||
this.output = panner;
|
||
}
|
||
|
||
/**
|
||
* 设置音频朝向,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
||
* @param x 朝向x坐标 Number
|
||
* @param y 朝向y坐标 Number
|
||
* @param z 朝向z坐标 Number
|
||
*/
|
||
setOrientation(x, y, z) {
|
||
this.output.orientationX.value = x;
|
||
this.output.orientationY.value = y;
|
||
this.output.orientationZ.value = z;
|
||
}
|
||
/**
|
||
* 设置音频位置,x正方形水平向右,y正方形垂直于地面向上,z正方向垂直屏幕远离用户
|
||
* @param x 位置x坐标 Number
|
||
* @param y 位置y坐标 Number
|
||
* @param z 位置z坐标 Number
|
||
*/
|
||
setPosition(x, y, z) {
|
||
this.output.positionX.value = x;
|
||
this.output.positionY.value = y;
|
||
this.output.positionZ.value = z;
|
||
}
|
||
end() { }
|
||
|
||
start() { }
|
||
}
|
||
class VolumeEffect extends AudioEffect {
|
||
constructor(ac) {
|
||
super(ac);
|
||
const gain = ac.createGain();
|
||
this.input = gain;
|
||
this.output = gain;
|
||
}
|
||
|
||
/**
|
||
* 设置音量大小
|
||
* @param volume 音量大小 Number
|
||
*/
|
||
setVolume(volume) {
|
||
this.output.gain.value = volume;
|
||
}
|
||
|
||
/**
|
||
* 获取音量大小 Number
|
||
*/
|
||
getVolume() {
|
||
return this.output.gain.value;
|
||
}
|
||
|
||
end() { }
|
||
|
||
start() { }
|
||
}
|
||
class ChannelVolumeEffect extends AudioEffect {
|
||
/** 所有的音量控制节点 */
|
||
|
||
constructor(ac) {
|
||
super(ac);
|
||
/** 所有的音量控制节点 */
|
||
this.gain = [];
|
||
const splitter = ac.createChannelSplitter();
|
||
const merger = ac.createChannelMerger();
|
||
this.output = merger;
|
||
this.input = splitter;
|
||
for (let i = 0; i < 6; i++) {
|
||
const gain = ac.createGain();
|
||
splitter.connect(gain, i);
|
||
gain.connect(merger, 0, i);
|
||
this.gain.push(gain);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置某个声道的音量大小
|
||
* @param channel 要设置的声道,可填0-5 Number
|
||
* @param volume 这个声道的音量大小 Number
|
||
*/
|
||
setVolume(channel, volume) {
|
||
if (!this.gain[channel]) return;
|
||
this.gain[channel].gain.value = volume;
|
||
}
|
||
|
||
/**
|
||
* 获取某个声道的音量大小,可填0-5
|
||
* @param channel 要获取的声道 Number
|
||
*/
|
||
getVolume(channel) {
|
||
if (!this.gain[channel]) return 0;
|
||
return this.gain[channel].gain.value;
|
||
}
|
||
|
||
end() { }
|
||
|
||
start() { }
|
||
}
|
||
class DelayEffect extends AudioEffect {
|
||
constructor(ac) {
|
||
super(ac);
|
||
|
||
const delay = ac.createDelay();
|
||
this.input = delay;
|
||
this.output = delay;
|
||
}
|
||
|
||
/**
|
||
* 设置延迟时长
|
||
* @param delay 延迟时长,单位秒 Number
|
||
*/
|
||
setDelay(delay) {
|
||
this.output.delayTime.value = delay;
|
||
}
|
||
|
||
/**
|
||
* 获取延迟时长
|
||
*/
|
||
getDelay() {
|
||
return this.output.delayTime.value;
|
||
}
|
||
|
||
end() { }
|
||
|
||
start() { }
|
||
}
|
||
class EchoEffect extends AudioEffect {
|
||
constructor(ac) {
|
||
super(ac);
|
||
/** 当前增益 */
|
||
this.gain = 0.5;
|
||
/** 是否正在播放 */
|
||
this.playing = false;
|
||
const delay = ac.createDelay();
|
||
const gain = ac.createGain();
|
||
gain.gain.value = 0.5;
|
||
delay.delayTime.value = 0.05;
|
||
delay.connect(gain);
|
||
gain.connect(delay);
|
||
/** 延迟节点 */
|
||
this.delay = delay;
|
||
/** 反馈增益节点 */
|
||
this.gainNode = gain;
|
||
|
||
this.input = gain;
|
||
this.output = gain;
|
||
}
|
||
|
||
/**
|
||
* 设置回声反馈增益大小
|
||
* @param gain 增益大小,范围 0-1,大于等于1的视为0.5,小于0的视为0 Number
|
||
*/
|
||
setFeedbackGain(gain) {
|
||
const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain;
|
||
this.gain = resolved;
|
||
if (this.playing) this.gainNode.gain.value = resolved;
|
||
}
|
||
|
||
/**
|
||
* 设置回声间隔时长
|
||
* @param delay 回声时长,范围 0.01-Infinity,小于0.01的视为0.01 Number
|
||
*/
|
||
setEchoDelay(delay) {
|
||
const resolved = delay < 0.01 ? 0.01 : delay;
|
||
this.delay.delayTime.value = resolved;
|
||
}
|
||
|
||
/**
|
||
* 获取反馈节点增益
|
||
*/
|
||
getFeedbackGain() {
|
||
return this.gain;
|
||
}
|
||
|
||
/**
|
||
* 获取回声间隔时长
|
||
*/
|
||
getEchoDelay() {
|
||
return this.delay.delayTime.value;
|
||
}
|
||
|
||
end() {
|
||
this.playing = false;
|
||
const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10;
|
||
sleep(this.delay.delayTime.value * echoTime).then(() => {
|
||
if (!this.playing) this.gainNode.gain.value = 0;
|
||
});
|
||
}
|
||
|
||
start() {
|
||
this.playing = true;
|
||
this.gainNode.gain.value = this.gain;
|
||
}
|
||
}
|
||
|
||
class StreamLoader {
|
||
constructor(url) {
|
||
/** 传输目标 Set<IStreamReader> */
|
||
this.target = new Set();
|
||
this.loading = false;
|
||
}
|
||
|
||
/**
|
||
* 将加载流传递给字节流读取对象
|
||
* @param reader 字节流读取对象 IStreamReader
|
||
*/
|
||
pipe(reader) {
|
||
if (this.loading) {
|
||
console.warn(
|
||
"Cannot pipe new StreamReader object when stream is loading."
|
||
);
|
||
return;
|
||
}
|
||
this.target.add(reader);
|
||
reader.piped(this);
|
||
return this;
|
||
}
|
||
|
||
async start() {
|
||
if (this.loading) return;
|
||
this.loading = true;
|
||
const response = await window.fetch(this.url);
|
||
const stream = response.body;
|
||
if (!stream) {
|
||
console.error("Cannot get reader when fetching '" + this.url + "'.");
|
||
return;
|
||
}
|
||
// 获取读取器
|
||
this.stream = stream;
|
||
const reader = response.body?.getReader();
|
||
const targets = [...this.target];
|
||
|
||
await Promise.all(targets.map((v) => v.start(stream, this, response)));
|
||
if (reader && reader.read) {
|
||
// 开始流传输
|
||
while (true) {
|
||
const { value, done } = await reader.read();
|
||
await Promise.all(
|
||
targets.map((v) => v.pump(value, done, response))
|
||
);
|
||
if (done) break;
|
||
}
|
||
} else {
|
||
// 如果不支持流传输
|
||
const buffer = await response.arrayBuffer();
|
||
const data = new Uint8Array(buffer);
|
||
await Promise.all(targets.map((v) => v.pump(data, true, response)));
|
||
}
|
||
|
||
this.loading = false;
|
||
targets.forEach((v) => v.end(true));
|
||
}
|
||
|
||
cancel(reason) {
|
||
if (!this.stream) return;
|
||
this.stream.cancel(reason);
|
||
this.loading = false;
|
||
this.target.forEach((v) => v.end(false, reason));
|
||
}
|
||
}
|
||
|
||
/** @type {[string, number[]][]} */
|
||
const fileSignatures = [
|
||
[AudioType.Mp3, [0x49, 0x44, 0x33]],
|
||
[AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]],
|
||
[AudioType.Wav, [0x52, 0x49, 0x46, 0x46]],
|
||
[AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]],
|
||
[AudioType.Aac, [0xff, 0xf1]],
|
||
[AudioType.Aac, [0xff, 0xf9]],
|
||
];
|
||
/** @type {[string, number[]][]} */
|
||
const oggHeaders = [
|
||
[AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]],
|
||
];
|
||
|
||
function checkAudioType(data) {
|
||
let audioType = "";
|
||
// 检查头文件获取音频类型,仅检查前256个字节
|
||
const toCheck = data.slice(0, 256);
|
||
for (const [type, value] of fileSignatures) {
|
||
if (value.every((v, i) => toCheck[i] === v)) {
|
||
audioType = type;
|
||
break;
|
||
}
|
||
}
|
||
if (audioType === AudioType.Ogg) {
|
||
// 如果是ogg的话,进一步判断是不是opus
|
||
for (const [key, value] of oggHeaders) {
|
||
const has = toCheck.some((_, i) => {
|
||
return value.every((v, ii) => toCheck[i + ii] === v);
|
||
});
|
||
if (has) {
|
||
audioType = key;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return audioType;
|
||
}
|
||
class AudioDecoder {
|
||
/**
|
||
* 注册一个解码器
|
||
* @param type 要注册的解码器允许解码的类型
|
||
* @param decoder 解码器对象
|
||
*/
|
||
static registerDecoder(type, decoder) {
|
||
if (!this.decoderMap) this.decoderMap = new Map();
|
||
if (this.decoderMap.has(type)) {
|
||
console.warn(
|
||
"Audio stream decoder for audio type '" +
|
||
type +
|
||
"' has already existed."
|
||
);
|
||
return;
|
||
}
|
||
|
||
this.decoderMap.set(type, decoder);
|
||
}
|
||
|
||
/**
|
||
* 解码音频数据
|
||
* @param data 音频文件数据
|
||
* @param player AudioPlayer实例
|
||
*/
|
||
static async decodeAudioData(data, player) {
|
||
// 检查头文件获取音频类型,仅检查前256个字节
|
||
const toCheck = data.slice(0, 256);
|
||
const type = checkAudioType(data);
|
||
if (type === "") {
|
||
console.error(
|
||
"Unknown audio type. Header: '" +
|
||
[...toCheck]
|
||
.map((v) => v.toString().padStart(2, "0"))
|
||
.join(" ")
|
||
.toUpperCase() +
|
||
"'"
|
||
);
|
||
return null;
|
||
}
|
||
if (isAudioSupport(type)) {
|
||
if (data.buffer instanceof ArrayBuffer) {
|
||
return player.ac.decodeAudioData(data.buffer);
|
||
} else {
|
||
return null;
|
||
}
|
||
} else {
|
||
const Decoder = this.decoderMap.get(type);
|
||
if (!Decoder) {
|
||
return null;
|
||
} else {
|
||
const decoder = new Decoder();
|
||
await decoder.create();
|
||
const decodedData = await decoder.decode(data);
|
||
if (!decodedData) return null;
|
||
const buffer = player.ac.createBuffer(
|
||
decodedData.channelData.length,
|
||
decodedData.channelData[0].length,
|
||
decodedData.sampleRate
|
||
);
|
||
decodedData.channelData.forEach((v, i) => {
|
||
buffer.copyToChannel(v, i);
|
||
});
|
||
decoder.destroy();
|
||
return buffer;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class VorbisDecoder {
|
||
/**
|
||
* 创建音频解码器
|
||
*/
|
||
async create() {
|
||
this.decoder = new OggVorbisDecoderWebWorker();
|
||
await this.decoder.ready;
|
||
}
|
||
/**
|
||
* 摧毁这个解码器
|
||
*/
|
||
destroy() {
|
||
this.decoder?.free();
|
||
}
|
||
/**
|
||
* 解码流数据
|
||
* @param data 流数据
|
||
*/
|
||
|
||
async decode(data) {
|
||
return this.decoder?.decode(data);
|
||
}
|
||
/**
|
||
* 解码整个文件
|
||
* @param data 文件数据
|
||
*/
|
||
async decodeAll(data) {
|
||
return this.decoder?.decodeFile(data);
|
||
}
|
||
/**
|
||
* 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用
|
||
*/
|
||
async flush() {
|
||
return this.decoder?.flush();
|
||
}
|
||
}
|
||
|
||
class OpusDecoder {
|
||
/**
|
||
* 创建音频解码器
|
||
*/
|
||
async create() {
|
||
this.decoder = new OggOpusDecoderWebWorker();
|
||
await this.decoder.ready;
|
||
}
|
||
/**
|
||
* 摧毁这个解码器
|
||
*/
|
||
destroy() {
|
||
this.decoder?.free();
|
||
}
|
||
/**
|
||
* 解码流数据
|
||
* @param data 流数据
|
||
*/
|
||
async decode(data) {
|
||
return this.decoder?.decode(data);
|
||
}
|
||
/**
|
||
* 解码整个文件
|
||
* @param data 文件数据
|
||
*/
|
||
async decodeAll(data) {
|
||
return this.decoder?.decodeFile(data);
|
||
}
|
||
/**
|
||
* 当音频解码完成后,会调用此函数,需要返回之前还未解析或未返回的音频数据。调用后,该解码器将不会被再次使用
|
||
*/
|
||
async flush() {
|
||
return await this.decoder?.flush();
|
||
}
|
||
}
|
||
const mimeTypeMap = {
|
||
[AudioType.Aac]: "audio/aac",
|
||
[AudioType.Flac]: "audio/flac",
|
||
[AudioType.Mp3]: "audio/mpeg",
|
||
[AudioType.Ogg]: "application/ogg",
|
||
[AudioType.Opus]: "application/ogg",
|
||
[AudioType.Wav]: "application/ogg",
|
||
};
|
||
|
||
function isOggPage(data) {
|
||
return !isNil(data.isFirstPage);
|
||
}
|
||
class AudioStreamSource {
|
||
constructor(context) {
|
||
this.output = context.createBufferSource();
|
||
/** 是否已经完全加载完毕 */
|
||
this.loaded = false;
|
||
/** 是否正在播放 */
|
||
this.playing = false;
|
||
/** 已经缓冲了多长时间,如果缓冲完那么跟歌曲时长一致 */
|
||
this.buffered = 0;
|
||
/** 已经缓冲的采样点数量 */
|
||
this.bufferedSamples = 0;
|
||
/** 歌曲时长,加载完毕之前保持为 0 */
|
||
this.duration = 0;
|
||
/** 在流传输阶段,至少缓冲多长时间的音频之后才开始播放,单位秒 */
|
||
this.bufferPlayDuration = 1;
|
||
/** 音频的采样率,未成功解析出之前保持为 0 */
|
||
this.sampleRate = 0;
|
||
//是否循环播放
|
||
this.loop = false;
|
||
/** 上一次播放是从何时开始的 */
|
||
this.lastStartWhen = 0;
|
||
/** 开始播放时刻 */
|
||
this.lastStartTime = 0;
|
||
/** 上一次播放的缓存长度 */
|
||
this.lastBufferSamples = 0;
|
||
|
||
/** 是否已经获取到头文件 */
|
||
this.headerRecieved = false;
|
||
/** 音频类型 */
|
||
this.audioType = "";
|
||
/** 每多长时间组成一个缓存 Float32Array */
|
||
this.bufferChunkSize = 10;
|
||
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array,用于流式解码 */
|
||
this.audioData = [];
|
||
|
||
this.errored = false;
|
||
this.ac = context;
|
||
}
|
||
/** 当前已经播放了多长时间 */
|
||
get currentTime() {
|
||
return this.ac.currentTime - this.lastStartTime + this.lastStartWhen;
|
||
}
|
||
/**
|
||
* 设置每个缓存数据的大小,默认为10秒钟一个缓存数据
|
||
* @param size 每个缓存数据的时长,单位秒
|
||
*/
|
||
setChunkSize(size) {
|
||
if (this.controller?.loading || this.loaded) return;
|
||
this.bufferChunkSize = size;
|
||
}
|
||
|
||
piped(controller) {
|
||
this.controller = controller;
|
||
}
|
||
|
||
async pump(data, done) {
|
||
if (!data || this.errored) return;
|
||
if (!this.headerRecieved) {
|
||
// 检查头文件获取音频类型,仅检查前256个字节
|
||
const toCheck = data.slice(0, 256);
|
||
this.audioType = checkAudioType(data);
|
||
if (!this.audioType) {
|
||
console.error(
|
||
"Unknown audio type. Header: '" +
|
||
[...toCheck]
|
||
.map((v) => v.toString(16).padStart(2, "0"))
|
||
.join(" ")
|
||
.toUpperCase() +
|
||
"'"
|
||
);
|
||
return;
|
||
}
|
||
// 创建解码器
|
||
const Decoder = AudioDecoder.decoderMap.get(this.audioType);
|
||
if (!Decoder) {
|
||
this.errored = true;
|
||
console.error(
|
||
"Cannot decode stream source type of '" +
|
||
this.audioType +
|
||
"', since there is no registered decoder for that type."
|
||
);
|
||
return Promise.reject(
|
||
`Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.`
|
||
);
|
||
}
|
||
this.decoder = new Decoder();
|
||
// 创建数据解析器
|
||
const mime = mimeTypeMap[this.audioType];
|
||
const parser = new CodecParser(mime);
|
||
this.parser = parser;
|
||
await this.decoder.create();
|
||
this.headerRecieved = true;
|
||
}
|
||
|
||
const decoder = this.decoder;
|
||
const parser = this.parser;
|
||
if (!decoder || !parser) {
|
||
this.errored = true;
|
||
return Promise.reject(
|
||
"No parser or decoder attached in this AudioStreamSource"
|
||
);
|
||
}
|
||
|
||
await this.decodeData(data, decoder, parser);
|
||
if (done) await this.decodeFlushData(decoder, parser);
|
||
this.checkBufferedPlay();
|
||
}
|
||
|
||
/**
|
||
* 检查采样率,如果还未解析出采样率,那么将设置采样率,如果当前采样率与之前不同,那么发出警告
|
||
*/
|
||
checkSampleRate(info) {
|
||
for (const one of info) {
|
||
const frame = isOggPage(one) ? one.codecFrames[0] : one;
|
||
if (frame) {
|
||
const rate = frame.header.sampleRate;
|
||
if (this.sampleRate === 0) {
|
||
this.sampleRate = rate;
|
||
break;
|
||
} else {
|
||
if (rate !== this.sampleRate) {
|
||
console.warn("Sample rate in stream audio must be constant.");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析音频数据
|
||
*/
|
||
async decodeData(data, decoder, parser) {
|
||
// 解析音频数据
|
||
const audioData = await decoder.decode(data);
|
||
if (!audioData) return;
|
||
// @ts-expect-error 库类型声明错误
|
||
const audioInfo = [...parser.parseChunk(data)];
|
||
|
||
// 检查采样率
|
||
this.checkSampleRate(audioInfo);
|
||
// 追加音频数据
|
||
this.appendDecodedData(audioData, audioInfo);
|
||
}
|
||
|
||
/**
|
||
* 解码剩余数据
|
||
*/
|
||
async decodeFlushData(decoder, parser) {
|
||
const audioData = await decoder.flush();
|
||
if (!audioData) return;
|
||
// @ts-expect-error 库类型声明错误
|
||
const audioInfo = [...parser.flush()];
|
||
|
||
this.checkSampleRate(audioInfo);
|
||
this.appendDecodedData(audioData, audioInfo);
|
||
}
|
||
|
||
/**
|
||
* 追加音频数据
|
||
*/
|
||
appendDecodedData(data, info) {
|
||
const channels = data.channelData.length;
|
||
if (channels === 0) return;
|
||
if (this.audioData.length !== channels) {
|
||
this.audioData = [];
|
||
for (let i = 0; i < channels; i++) {
|
||
this.audioData.push([]);
|
||
}
|
||
}
|
||
// 计算出应该放在哪
|
||
const chunk = this.sampleRate * this.bufferChunkSize;
|
||
const sampled = this.bufferedSamples;
|
||
const pushIndex = Math.floor(sampled / chunk);
|
||
const bufferIndex = sampled % chunk;
|
||
const dataLength = data.channelData[0].length;
|
||
let buffered = 0;
|
||
let nowIndex = pushIndex;
|
||
let toBuffer = bufferIndex;
|
||
while (buffered < dataLength) {
|
||
const rest = toBuffer !== 0 ? chunk - bufferIndex : chunk;
|
||
|
||
for (let i = 0; i < channels; i++) {
|
||
const audioData = this.audioData[i];
|
||
if (!audioData[nowIndex]) {
|
||
audioData.push(new Float32Array(chunk));
|
||
}
|
||
const toPush = data.channelData[i].slice(buffered, buffered + rest);
|
||
|
||
audioData[nowIndex].set(toPush, toBuffer);
|
||
}
|
||
buffered += rest;
|
||
nowIndex++;
|
||
toBuffer = 0;
|
||
}
|
||
|
||
this.buffered +=
|
||
info.reduce((prev, curr) => prev + curr.duration, 0) / 1000;
|
||
this.bufferedSamples += info.reduce(
|
||
(prev, curr) => prev + curr.samples,
|
||
0
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 检查已缓冲内容,并在未开始播放时播放
|
||
*/
|
||
checkBufferedPlay() {
|
||
if (this.playing || this.sampleRate === 0) return;
|
||
const played = this.lastBufferSamples / this.sampleRate;
|
||
const dt = this.buffered - played;
|
||
if (this.loaded) {
|
||
this.playAudio(played);
|
||
return;
|
||
}
|
||
if (dt < this.bufferPlayDuration) return;
|
||
|
||
this.lastBufferSamples = this.bufferedSamples;
|
||
// 需要播放
|
||
this.mergeBuffers();
|
||
if (!this.buffer) return;
|
||
if (this.playing) this.output.stop();
|
||
this.createSourceNode(this.buffer);
|
||
this.output.loop = false;
|
||
this.output.start(0, played);
|
||
this.lastStartTime = this.ac.currentTime;
|
||
this.playing = true;
|
||
this.output.addEventListener("ended", () => {
|
||
this.playing = false;
|
||
this.checkBufferedPlay();
|
||
});
|
||
}
|
||
|
||
mergeBuffers() {
|
||
const buffer = this.ac.createBuffer(
|
||
this.audioData.length,
|
||
this.bufferedSamples,
|
||
this.sampleRate
|
||
);
|
||
const chunk = this.sampleRate * this.bufferChunkSize;
|
||
const bufferedChunks = Math.floor(this.bufferedSamples / chunk);
|
||
const restLength = this.bufferedSamples % chunk;
|
||
for (let i = 0; i < this.audioData.length; i++) {
|
||
const audio = this.audioData[i];
|
||
const data = new Float32Array(this.bufferedSamples);
|
||
for (let j = 0; j < bufferedChunks; j++) {
|
||
data.set(audio[j], chunk * j);
|
||
}
|
||
if (restLength !== 0) {
|
||
data.set(
|
||
audio[bufferedChunks].slice(0, restLength),
|
||
chunk * bufferedChunks
|
||
);
|
||
}
|
||
|
||
buffer.copyToChannel(data, i, 0);
|
||
}
|
||
this.buffer = buffer;
|
||
}
|
||
|
||
async start() {
|
||
delete this.buffer;
|
||
this.headerRecieved = false;
|
||
this.audioType = "";
|
||
this.errored = false;
|
||
this.buffered = 0;
|
||
this.sampleRate = 0;
|
||
this.bufferedSamples = 0;
|
||
this.duration = 0;
|
||
this.loaded = false;
|
||
if (this.playing) this.output.stop();
|
||
this.playing = false;
|
||
this.lastStartTime = this.ac.currentTime;
|
||
}
|
||
|
||
end(done, reason) {
|
||
if (done && this.buffer) {
|
||
this.loaded = true;
|
||
delete this.controller;
|
||
this.mergeBuffers();
|
||
|
||
this.duration = this.buffered;
|
||
this.audioData = [];
|
||
this.decoder?.destroy();
|
||
delete this.decoder;
|
||
delete this.parser;
|
||
} else {
|
||
console.warn(
|
||
"Unexpected end when loading stream audio, reason: '" +
|
||
(reason ?? "") +
|
||
"'"
|
||
);
|
||
}
|
||
}
|
||
|
||
playAudio(when) {
|
||
if (!this.buffer) return;
|
||
this.lastStartTime = this.ac.currentTime;
|
||
if (this.playing) this.output.stop();
|
||
if (this.route.status !== AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Playing;
|
||
}
|
||
this.createSourceNode(this.buffer);
|
||
this.output.start(0, when);
|
||
this.playing = true;
|
||
|
||
this.output.addEventListener("ended", () => {
|
||
this.playing = false;
|
||
if (this.route.status === AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Stoped;
|
||
}
|
||
if (this.loop && !this.output.loop) this.play(0);
|
||
});
|
||
}
|
||
/**
|
||
* 开始播放这个音频源
|
||
*/
|
||
play(when) {
|
||
if (this.playing || this.errored) return;
|
||
if (this.loaded && this.buffer) {
|
||
this.playing = true;
|
||
this.playAudio(when);
|
||
} else {
|
||
this.controller?.start();
|
||
}
|
||
}
|
||
|
||
createSourceNode(buffer) {
|
||
if (!this.target) return;
|
||
const node = this.ac.createBufferSource();
|
||
node.buffer = buffer;
|
||
if (this.playing) this.output.stop();
|
||
this.playing = false;
|
||
this.output = node;
|
||
node.connect(this.target.input);
|
||
node.loop = this.loop;
|
||
}
|
||
/**
|
||
* 停止播放这个音频源
|
||
* @returns 音频暂停的时刻 number
|
||
*/
|
||
stop() {
|
||
if (this.playing) this.output.stop();
|
||
this.playing = false;
|
||
return this.ac.currentTime - this.lastStartTime;
|
||
}
|
||
/**
|
||
* 连接到音频路由图上,每次调用播放的时候都会执行一次
|
||
* @param target 连接至的目标 IAudioInput
|
||
*/
|
||
connect(target) {
|
||
this.target = target;
|
||
}
|
||
/**
|
||
* 设置是否循环播放
|
||
* @param loop 是否循环 boolean)
|
||
*/
|
||
setLoop(loop) {
|
||
this.loop = loop;
|
||
}
|
||
}
|
||
class AudioElementSource {
|
||
constructor(context) {
|
||
const audio = new Audio();
|
||
audio.preload = "none";
|
||
this.output = context.createMediaElementSource(audio);
|
||
this.audio = audio;
|
||
this.ac = context;
|
||
audio.addEventListener("play", () => {
|
||
this.playing = true;
|
||
if (this.route.status !== AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Playing;
|
||
}
|
||
});
|
||
audio.addEventListener("ended", () => {
|
||
this.playing = false;
|
||
if (this.route.status === AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Stoped;
|
||
}
|
||
});
|
||
}
|
||
get duration() {
|
||
return this.audio.duration;
|
||
}
|
||
get currentTime() {
|
||
return this.audio.currentTime;
|
||
}
|
||
/**
|
||
* 设置音频源的路径
|
||
* @param url 音频路径
|
||
*/
|
||
setSource(url) {
|
||
this.audio.src = url;
|
||
}
|
||
|
||
play(when = 0) {
|
||
if (this.playing) return;
|
||
this.audio.currentTime = when;
|
||
this.audio.play();
|
||
}
|
||
|
||
stop() {
|
||
this.audio.pause();
|
||
this.playing = false;
|
||
if (this.route.status === AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Stoped;
|
||
}
|
||
return this.audio.currentTime;
|
||
}
|
||
|
||
connect(target) {
|
||
this.output.connect(target.input);
|
||
}
|
||
|
||
setLoop(loop) {
|
||
this.audio.loop = loop;
|
||
}
|
||
}
|
||
class AudioBufferSource {
|
||
constructor(context) {
|
||
this.output = context.createBufferSource();
|
||
/** 是否循环 */
|
||
this.loop = false;
|
||
/** 上一次播放是从何时开始的 */
|
||
this.lastStartWhen = 0;
|
||
/** 播放开始时刻 */
|
||
this.lastStartTime = 0;
|
||
this.duration = 0;
|
||
this.ac = context;
|
||
}
|
||
get currentTime() {
|
||
return this.ac.currentTime - this.lastStartTime + this.lastStartWhen;
|
||
}
|
||
|
||
/**
|
||
* 设置音频源数据
|
||
* @param buffer 音频源,可以是未解析的 ArrayBuffer,也可以是已解析的 AudioBuffer
|
||
*/
|
||
async setBuffer(buffer) {
|
||
if (buffer instanceof ArrayBuffer) {
|
||
this.buffer = await this.ac.decodeAudioData(buffer);
|
||
} else {
|
||
this.buffer = buffer;
|
||
}
|
||
this.duration = this.buffer.duration;
|
||
}
|
||
|
||
play(when) {
|
||
if (this.playing || !this.buffer) return;
|
||
this.playing = true;
|
||
this.lastStartTime = this.ac.currentTime;
|
||
if (this.route.status !== AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Playing;
|
||
}
|
||
this.createSourceNode(this.buffer);
|
||
this.output.start(0, when);
|
||
this.output.addEventListener("ended", () => {
|
||
this.playing = false;
|
||
if (this.route.status === AudioStatus.Playing) {
|
||
this.route.status = AudioStatus.Stoped;
|
||
}
|
||
if (this.loop && !this.output.loop) this.play(0);
|
||
});
|
||
}
|
||
|
||
createSourceNode(buffer) {
|
||
if (!this.target) return;
|
||
const node = this.ac.createBufferSource();
|
||
node.buffer = buffer;
|
||
this.output = node;
|
||
node.connect(this.target.input);
|
||
node.loop = this.loop;
|
||
}
|
||
|
||
stop() {
|
||
this.output.stop();
|
||
return this.ac.currentTime - this.lastStartTime;
|
||
}
|
||
|
||
connect(target) {
|
||
this.target = target;
|
||
}
|
||
|
||
setLoop(loop) {
|
||
this.loop = loop;
|
||
}
|
||
}
|
||
class AudioPlayer {
|
||
constructor() {
|
||
/** 音频播放上下文 */
|
||
this.ac = new AudioContext();
|
||
/** 音量节点 */
|
||
this.gain = this.ac.createGain();
|
||
this.gain.connect(this.ac.destination);
|
||
this.audioRoutes = new Map();
|
||
}
|
||
/**
|
||
* 解码音频数据
|
||
* @param data 音频数据
|
||
*/
|
||
decodeAudioData(data) {
|
||
return AudioDecoder.decodeAudioData(data, this);
|
||
}
|
||
/**
|
||
* 设置音量
|
||
* @param volume 音量
|
||
*/
|
||
setVolume(volume) {
|
||
this.gain.gain.value = volume;
|
||
}
|
||
|
||
/**
|
||
* 获取音量
|
||
*/
|
||
getVolume() {
|
||
return this.gain.gain.value;
|
||
}
|
||
|
||
/**
|
||
* 创建一个音频源
|
||
* @param Source 音频源类
|
||
*/
|
||
createSource(Source) {
|
||
return new Source(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个兼容流式音频源,可以与流式加载相结合,主要用于处理 opus ogg 不兼容的情况
|
||
*/
|
||
createStreamSource() {
|
||
return new AudioStreamSource(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个通过 audio 元素播放的音频源
|
||
*/
|
||
createElementSource() {
|
||
return new AudioElementSource(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个通过 AudioBuffer 播放的音频源
|
||
*/
|
||
createBufferSource() {
|
||
return new AudioBufferSource(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 获取音频目的地
|
||
*/
|
||
getDestination() {
|
||
return this.gain;
|
||
}
|
||
|
||
/**
|
||
* 创建一个音频效果器
|
||
* @param Effect 效果器类
|
||
*/
|
||
createEffect(Effect) {
|
||
return new Effect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个修改音量的效果器
|
||
* ```txt
|
||
* |----------|
|
||
* Input ----> | GainNode | ----> Output
|
||
* |----------|
|
||
* ```
|
||
*/
|
||
createVolumeEffect() {
|
||
return new VolumeEffect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个立体声效果器
|
||
* ```txt
|
||
* |------------|
|
||
* Input ----> | PannerNode | ----> Output
|
||
* |------------|
|
||
* ```
|
||
*/
|
||
createStereoEffect() {
|
||
return new StereoEffect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个修改单个声道音量的效果器
|
||
* ```txt
|
||
* |----------|
|
||
* -> | GainNode | \
|
||
* |--------------| / |----------| -> |------------|
|
||
* Input ----> | SplitterNode | ...... | MergerNode | ----> Output
|
||
* |--------------| \ |----------| -> |------------|
|
||
* -> | GainNode | /
|
||
* |----------|
|
||
* ```
|
||
*/
|
||
createChannelVolumeEffect() {
|
||
return new ChannelVolumeEffect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个延迟效果器
|
||
* |-----------|
|
||
* Input ----> | DelayNode | ----> Output
|
||
* |-----------|
|
||
*/
|
||
createDelay() {
|
||
return new DelayEffect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个回声效果器
|
||
* ```txt
|
||
* |----------|
|
||
* Input ----> | GainNode | ----> Output
|
||
* ^ |----------| |
|
||
* | |
|
||
* | |------------| ↓
|
||
* |-- | Delay Node | <--
|
||
* |------------|
|
||
* ```
|
||
*/
|
||
createEchoEffect() {
|
||
return new EchoEffect(this.ac);
|
||
}
|
||
|
||
/**
|
||
* 创建一个音频播放路由
|
||
* @param source 音频源
|
||
*/
|
||
createRoute(source) {
|
||
return new AudioRoute(source, this);
|
||
}
|
||
|
||
/**
|
||
* 添加一个音频播放路由,可以直接被播放
|
||
* @param id 这个音频播放路由的名称
|
||
* @param route 音频播放路由对象
|
||
*/
|
||
addRoute(id, route) {
|
||
if (!this.audioRoutes) this.audioRoutes = new Map();
|
||
if (this.audioRoutes.has(id)) {
|
||
console.warn(
|
||
"Audio route with id of '" +
|
||
id +
|
||
"' has already existed. New route will override old route."
|
||
);
|
||
}
|
||
this.audioRoutes.set(id, route);
|
||
}
|
||
|
||
/**
|
||
* 根据名称获取音频播放路由对象
|
||
* @param id 音频播放路由的名称
|
||
*/
|
||
getRoute(id) {
|
||
return this.audioRoutes.get(id);
|
||
}
|
||
/**
|
||
* 移除一个音频播放路由
|
||
* @param id 要移除的播放路由的名称
|
||
*/
|
||
removeRoute(id) {
|
||
this.audioRoutes.delete(id);
|
||
}
|
||
/**
|
||
* 播放音频
|
||
* @param id 音频名称
|
||
* @param when 从音频的哪个位置开始播放,单位秒
|
||
*/
|
||
play(id, when) {
|
||
const route = this.getRoute(id);
|
||
if (!route) {
|
||
console.warn(
|
||
"Cannot play audio route '" +
|
||
id +
|
||
"', since there is not added route named it."
|
||
);
|
||
return;
|
||
}
|
||
|
||
route.play(when);
|
||
}
|
||
|
||
/**
|
||
* 暂停音频播放
|
||
* @param id 音频名称
|
||
* @returns 当音乐真正停止时兑现
|
||
*/
|
||
pause(id) {
|
||
const route = this.getRoute(id);
|
||
if (!route) {
|
||
console.warn(
|
||
"Cannot pause audio route '" +
|
||
id +
|
||
"', since there is not added route named it."
|
||
);
|
||
return;
|
||
}
|
||
return route.pause();
|
||
}
|
||
|
||
/**
|
||
* 停止音频播放
|
||
* @param id 音频名称
|
||
* @returns 当音乐真正停止时兑现
|
||
*/
|
||
stop(id) {
|
||
const route = this.getRoute(id);
|
||
if (!route) {
|
||
console.warn(
|
||
"Cannot stop audio route '" +
|
||
id +
|
||
"', since there is not added route named it."
|
||
);
|
||
return;
|
||
}
|
||
return route.stop();
|
||
}
|
||
|
||
/**
|
||
* 继续音频播放
|
||
* @param id 音频名称
|
||
*/
|
||
resume(id) {
|
||
const route = this.getRoute(id);
|
||
if (!route) {
|
||
console.warn(
|
||
"Cannot pause audio route '" +
|
||
id +
|
||
"', since there is not added route named it."
|
||
);
|
||
return;
|
||
}
|
||
route.resume();
|
||
}
|
||
|
||
/**
|
||
* 设置听者位置,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
|
||
* @param x 位置x坐标
|
||
* @param y 位置y坐标
|
||
* @param z 位置z坐标
|
||
*/
|
||
setListenerPosition(x, y, z) {
|
||
const listener = this.ac.listener;
|
||
listener.positionX.value = x;
|
||
listener.positionY.value = y;
|
||
listener.positionZ.value = z;
|
||
}
|
||
|
||
/**
|
||
* 设置听者朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
|
||
* @param x 朝向x坐标
|
||
* @param y 朝向y坐标
|
||
* @param z 朝向z坐标
|
||
*/
|
||
setListenerOrientation(x, y, z) {
|
||
const listener = this.ac.listener;
|
||
listener.forwardX.value = x;
|
||
listener.forwardY.value = y;
|
||
listener.forwardZ.value = z;
|
||
}
|
||
|
||
/**
|
||
* 设置听者头顶朝向,x正方向水平向右,y正方向垂直于地面向上,z正方向垂直屏幕远离用户
|
||
* @param x 头顶朝向x坐标
|
||
* @param y 头顶朝向y坐标
|
||
* @param z 头顶朝向z坐标
|
||
*/
|
||
setListenerUp(x, y, z) {
|
||
const listener = this.ac.listener;
|
||
listener.upX.value = x;
|
||
listener.upY.value = y;
|
||
listener.upZ.value = z;
|
||
}
|
||
}
|
||
class AudioRoute {
|
||
constructor(source, player) {
|
||
source.route = this;
|
||
this.output = source.output;
|
||
|
||
/** 效果器路由图 */
|
||
this.effectRoute = [];
|
||
|
||
/** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */
|
||
this.endTime = 0;
|
||
/** 暂停时播放了多长时间 */
|
||
this.pauseCurrentTime = 0;
|
||
/** 当前播放状态 */
|
||
this.player = player;
|
||
this.status = AudioStatus.Stoped;
|
||
|
||
this.shouldStop = false;
|
||
/**
|
||
* 每次暂停或停止时自增,用于判断当前正在处理的情况。
|
||
* 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作
|
||
*/
|
||
this.stopIdentifier = 0;
|
||
/** 暂停时刻 */
|
||
this.pauseTime = 0;
|
||
this.source = source;
|
||
this.source.player = player;
|
||
}
|
||
/** 音频时长,单位秒 */
|
||
get duration() {
|
||
return this.source.duration;
|
||
}
|
||
/** 当前播放了多长时间,单位秒 */
|
||
get currentTime() {
|
||
if (this.status === AudioStatus.Paused) {
|
||
return this.pauseCurrentTime;
|
||
} else {
|
||
return this.source.currentTime;
|
||
}
|
||
}
|
||
set currentTime(time) {
|
||
this.source.stop();
|
||
this.source.play(time);
|
||
}
|
||
/**
|
||
* 设置结束时间,暂停或停止时,会经过这么长时间才终止音频的播放,这期间可以做一下音频淡出的效果。
|
||
* @param time 暂停或停止时,经过多长时间之后才会结束音频的播放
|
||
*/
|
||
setEndTime(time) {
|
||
this.endTime = time;
|
||
}
|
||
|
||
/**
|
||
* 当音频播放时执行的函数,可以用于音频淡入效果
|
||
* @param fn 音频开始播放时执行的函数
|
||
*/
|
||
onStart(fn) {
|
||
this.audioStartHook = fn;
|
||
}
|
||
|
||
/**
|
||
* 当音频暂停或停止时执行的函数,可以用于音频淡出效果
|
||
* @param fn 音频在暂停或停止时执行的函数,不填时表示取消这个钩子。
|
||
* 包含两个参数,第一个参数是结束时长,第二个参数是当前音频播放路由对象
|
||
*/
|
||
onEnd(fn) {
|
||
this.audioEndHook = fn;
|
||
}
|
||
|
||
/**
|
||
* 开始播放这个音频
|
||
* @param when 从音频的什么时候开始播放,单位秒
|
||
*/
|
||
async play(when = 0) {
|
||
if (this.status === AudioStatus.Playing) return;
|
||
this.link();
|
||
await this.player.ac.resume();
|
||
if (this.effectRoute.length > 0) {
|
||
const first = this.effectRoute[0];
|
||
this.source.connect(first);
|
||
const last = this.effectRoute.at(-1);
|
||
last.connect({ input: this.player.getDestination() });
|
||
} else {
|
||
this.source.connect({ input: this.player.getDestination() });
|
||
}
|
||
this.source.play(when);
|
||
this.status = AudioStatus.Playing;
|
||
this.pauseTime = 0;
|
||
this.audioStartHook?.(this);
|
||
this.startAllEffect();
|
||
if (this.status !== AudioStatus.Playing) {
|
||
this.status = AudioStatus.Playing;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 暂停音频播放
|
||
*/
|
||
async pause() {
|
||
if (this.status !== AudioStatus.Playing) return;
|
||
this.status = AudioStatus.Pausing;
|
||
this.stopIdentifier++;
|
||
const identifier = this.stopIdentifier;
|
||
if (this.audioEndHook) {
|
||
this.audioEndHook(this.endTime, this);
|
||
await sleep(this.endTime);
|
||
}
|
||
if (
|
||
this.status !== AudioStatus.Pausing ||
|
||
this.stopIdentifier !== identifier
|
||
) {
|
||
return;
|
||
}
|
||
this.pauseCurrentTime = this.source.currentTime;
|
||
const time = this.source.stop();
|
||
this.pauseTime = time;
|
||
if (this.shouldStop) {
|
||
this.status = AudioStatus.Stoped;
|
||
this.endAllEffect();
|
||
|
||
this.shouldStop = false;
|
||
} else {
|
||
this.status = AudioStatus.Paused;
|
||
this.endAllEffect();
|
||
}
|
||
this.endAllEffect();
|
||
}
|
||
|
||
/**
|
||
* 继续音频播放
|
||
*/
|
||
resume() {
|
||
if (this.status === AudioStatus.Playing) return;
|
||
if (
|
||
this.status === AudioStatus.Pausing ||
|
||
this.status === AudioStatus.Stoping
|
||
) {
|
||
this.audioStartHook?.(this);
|
||
|
||
return;
|
||
}
|
||
if (this.status === AudioStatus.Paused) {
|
||
this.play(this.pauseTime);
|
||
} else {
|
||
this.play(0);
|
||
}
|
||
this.status = AudioStatus.Playing;
|
||
this.pauseTime = 0;
|
||
this.audioStartHook?.(this);
|
||
this.startAllEffect();
|
||
}
|
||
|
||
/**
|
||
* 停止音频播放
|
||
*/
|
||
async stop() {
|
||
if (this.status !== AudioStatus.Playing) {
|
||
if (this.status === AudioStatus.Pausing) {
|
||
this.shouldStop = true;
|
||
}
|
||
return;
|
||
}
|
||
this.status = AudioStatus.Stoping;
|
||
this.stopIdentifier++;
|
||
const identifier = this.stopIdentifier;
|
||
if (this.audioEndHook) {
|
||
this.audioEndHook(this.endTime, this);
|
||
await sleep(this.endTime);
|
||
}
|
||
if (
|
||
this.status !== AudioStatus.Stoping ||
|
||
this.stopIdentifier !== identifier
|
||
) {
|
||
return;
|
||
}
|
||
this.source.stop();
|
||
this.status = AudioStatus.Stoped;
|
||
this.pauseTime = 0;
|
||
this.endAllEffect();
|
||
}
|
||
|
||
/**
|
||
* 添加效果器
|
||
* @param effect 要添加的效果,可以是数组,表示一次添加多个
|
||
* @param index 从哪个位置开始添加,如果大于数组长度,那么加到末尾,如果小于0,那么将会从后面往前数。默认添加到末尾
|
||
*/
|
||
addEffect(effect, index) {
|
||
if (isNil(index)) {
|
||
if (effect instanceof Array) {
|
||
this.effectRoute.push(...effect);
|
||
} else {
|
||
this.effectRoute.push(effect);
|
||
}
|
||
} else {
|
||
if (effect instanceof Array) {
|
||
this.effectRoute.splice(index, 0, ...effect);
|
||
} else {
|
||
this.effectRoute.splice(index, 0, effect);
|
||
}
|
||
}
|
||
this.setOutput();
|
||
if (this.source.playing) this.link();
|
||
}
|
||
|
||
/**
|
||
* 移除一个效果器
|
||
* @param effect 要移除的效果
|
||
*/
|
||
removeEffect(effect) {
|
||
const index = this.effectRoute.indexOf(effect);
|
||
if (index === -1) return;
|
||
this.effectRoute.splice(index, 1);
|
||
effect.disconnect();
|
||
this.setOutput();
|
||
if (this.source.playing) this.link();
|
||
}
|
||
|
||
setOutput() {
|
||
const effect = this.effectRoute.at(-1);
|
||
if (!effect) this.output = this.source.output;
|
||
else this.output = effect.output;
|
||
}
|
||
|
||
/**
|
||
* 连接音频路由图
|
||
*/
|
||
link() {
|
||
this.effectRoute.forEach((v) => v.disconnect());
|
||
this.effectRoute.forEach((v, i) => {
|
||
const next = this.effectRoute[i + 1];
|
||
if (next) {
|
||
v.connect(next);
|
||
}
|
||
});
|
||
}
|
||
|
||
startAllEffect() {
|
||
this.effectRoute.forEach((v) => v.start());
|
||
}
|
||
|
||
endAllEffect() {
|
||
this.effectRoute.forEach((v) => v.end());
|
||
}
|
||
}
|
||
|
||
const audioPlayer = new AudioPlayer();
|
||
|
||
class BgmController {
|
||
constructor(player) {
|
||
this.mainGain = player.createVolumeEffect();
|
||
this.player = player;
|
||
/** bgm音频名称的前缀 */
|
||
this.prefix = "bgms.";
|
||
/** 每个 bgm 的音量控制器 */
|
||
this.gain = new Map();
|
||
|
||
/** 正在播放的 bgm */
|
||
this.playingBgm = "";
|
||
/** 是否正在播放 */
|
||
this.playing = false;
|
||
|
||
/** 是否已经启用 */
|
||
this.enabled = true;
|
||
/** 是否屏蔽所有的音乐切换 */
|
||
this.blocking = false;
|
||
/** 渐变时长 */
|
||
this.transitionTime = 2000;
|
||
}
|
||
|
||
/**
|
||
* 设置音频渐变时长
|
||
* @param time 渐变时长
|
||
*/
|
||
setTransitionTime(time) {
|
||
this.transitionTime = time;
|
||
for (const [, value] of this.gain) {
|
||
value.transition.time(time);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 屏蔽音乐切换
|
||
*/
|
||
blockChange() {
|
||
this.blocking = true;
|
||
}
|
||
|
||
/**
|
||
* 取消屏蔽音乐切换
|
||
*/
|
||
unblockChange() {
|
||
this.blocking = false;
|
||
}
|
||
|
||
/**
|
||
* 设置总音量大小
|
||
* @param volume 音量大小
|
||
*/
|
||
setVolume(volume) {
|
||
this.mainGain.setVolume(volume);
|
||
this._volume = volume;
|
||
}
|
||
/**
|
||
* 获取总音量大小
|
||
*/
|
||
getVolume() {
|
||
return this.mainGain.getVolume();
|
||
}
|
||
/**
|
||
* 设置是否启用
|
||
* @param enabled 是否启用
|
||
*/
|
||
setEnabled(enabled) {
|
||
if (enabled) this.resume();
|
||
else this.stop();
|
||
this.enabled = enabled;
|
||
}
|
||
|
||
/**
|
||
* 设置 bgm 音频名称的前缀
|
||
*/
|
||
setPrefix(prefix) {
|
||
this.prefix = prefix;
|
||
}
|
||
|
||
getId(name) {
|
||
return `${this.prefix}${name}`;
|
||
}
|
||
|
||
/**
|
||
* 根据 bgm 名称获取其 AudioRoute 实例
|
||
* @param id 音频名称
|
||
*/
|
||
get(id) {
|
||
return this.player.getRoute(this.getId(id));
|
||
}
|
||
|
||
/**
|
||
* 添加一个 bgm
|
||
* @param id 要添加的 bgm 的名称
|
||
* @param url 指定 bgm 的加载地址
|
||
*/
|
||
addBgm(id, url = `project/bgms/${id}`) {
|
||
const type = guessTypeByExt(id);
|
||
if (!type) {
|
||
console.warn(
|
||
"Unknown audio extension name: '" +
|
||
id.split(".").slice(0, -1).join(".") +
|
||
"'"
|
||
);
|
||
return;
|
||
}
|
||
const gain = this.player.createVolumeEffect();
|
||
if (isAudioSupport(type)) {
|
||
const source = audioPlayer.createElementSource();
|
||
source.setSource(url);
|
||
source.setLoop(true);
|
||
const route = new AudioRoute(source, audioPlayer);
|
||
route.addEffect([gain, this.mainGain]);
|
||
audioPlayer.addRoute(this.getId(id), route);
|
||
this.setTransition(id, route, gain);
|
||
} else {
|
||
const source = audioPlayer.createStreamSource();
|
||
const stream = new StreamLoader(url);
|
||
stream.pipe(source);
|
||
source.setLoop(true);
|
||
const route = new AudioRoute(source, audioPlayer);
|
||
route.addEffect([gain, this.mainGain]);
|
||
audioPlayer.addRoute(this.getId(id), route);
|
||
this.setTransition(id, route, gain);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 移除一个 bgm
|
||
* @param id 要移除的 bgm 的名称
|
||
*/
|
||
removeBgm(id) {
|
||
this.player.removeRoute(this.getId(id));
|
||
const gain = this.gain.get(id);
|
||
gain?.transition.ticker.destroy();
|
||
this.gain.delete(id);
|
||
}
|
||
|
||
setTransition(id, route, gain) {
|
||
const transition = new Transition();
|
||
transition
|
||
.time(this.transitionTime)
|
||
.mode(linear())
|
||
.transition("volume", 0);
|
||
|
||
const tick = () => {
|
||
gain.setVolume(transition.value.volume);
|
||
};
|
||
|
||
/**
|
||
* @param expect 在结束时应该是正在播放还是停止
|
||
*/
|
||
const setTick = async (expect) => {
|
||
transition.ticker.remove(tick);
|
||
transition.ticker.add(tick);
|
||
const identifier = route.stopIdentifier;
|
||
await sleep(this.transitionTime + 500);
|
||
if (route.status === expect && identifier === route.stopIdentifier) {
|
||
transition.ticker.remove(tick);
|
||
if (route.status === AudioStatus.Playing) {
|
||
gain.setVolume(1);
|
||
} else {
|
||
gain.setVolume(0);
|
||
}
|
||
}
|
||
};
|
||
|
||
route.onStart(async () => {
|
||
transition.transition("volume", 1);
|
||
setTick(AudioStatus.Playing);
|
||
});
|
||
route.onEnd(() => {
|
||
transition.transition("volume", 0);
|
||
setTick(AudioStatus.Paused);
|
||
});
|
||
route.setEndTime(this.transitionTime);
|
||
|
||
this.gain.set(id, { effect: gain, transition });
|
||
}
|
||
|
||
/**
|
||
* 播放一个 bgm
|
||
* @param id 要播放的 bgm 名称
|
||
*/
|
||
play(id, when) {
|
||
if (this.blocking) return;
|
||
if (id !== this.playingBgm && this.playingBgm) {
|
||
this.player.pause(this.getId(this.playingBgm));
|
||
}
|
||
this.playingBgm = id;
|
||
if (!this.enabled) return;
|
||
this.player.play(this.getId(id), when);
|
||
this.playing = true;
|
||
}
|
||
|
||
/**
|
||
* 继续当前的 bgm
|
||
*/
|
||
resume() {
|
||
if (this.blocking || !this.enabled || this.playing) return;
|
||
if (this.playingBgm) {
|
||
this.player.resume(this.getId(this.playingBgm));
|
||
}
|
||
this.playing = true;
|
||
}
|
||
|
||
/**
|
||
* 暂停当前的 bgm
|
||
*/
|
||
pause() {
|
||
if (this.blocking || !this.enabled) return;
|
||
if (this.playingBgm) {
|
||
this.player.pause(this.getId(this.playingBgm));
|
||
}
|
||
this.playing = false;
|
||
}
|
||
|
||
/**
|
||
* 停止当前的 bgm
|
||
*/
|
||
stop() {
|
||
if (this.blocking || !this.enabled) return;
|
||
if (this.playingBgm) {
|
||
this.player.stop(this.getId(this.playingBgm));
|
||
}
|
||
this.playing = false;
|
||
}
|
||
}
|
||
const bgmController = new BgmController(audioPlayer);
|
||
|
||
class SoundPlayer {
|
||
constructor(player) {
|
||
/** 每个音效的唯一标识符 */
|
||
this.num = 0;
|
||
this.enabled = true;
|
||
this.gain = player.createVolumeEffect();
|
||
/** 每个音效的数据 */
|
||
this.buffer = new Map();
|
||
/** 所有正在播放的音乐 */
|
||
this.playing = new Set();
|
||
this.player = player;
|
||
}
|
||
/**
|
||
* 设置是否启用音效
|
||
* @param enabled 是否启用音效
|
||
*/
|
||
setEnabled(enabled) {
|
||
if (!enabled) this.stopAllSounds();
|
||
this.enabled = enabled;
|
||
}
|
||
|
||
/**
|
||
* 设置音量大小
|
||
* @param volume 音量大小
|
||
*/
|
||
setVolume(volume) {
|
||
this.gain.setVolume(volume);
|
||
}
|
||
/**
|
||
* 获取音量大小
|
||
*/
|
||
getVolume() {
|
||
return this.gain.getVolume();
|
||
}
|
||
/**
|
||
* 添加一个音效
|
||
* @param id 音效名称
|
||
* @param data 音效的Uint8Array数据
|
||
*/
|
||
async add(id, data) {
|
||
const buffer = await this.player.decodeAudioData(data);
|
||
if (!buffer) {
|
||
console.warn(
|
||
"Cannot decode sound '" +
|
||
id +
|
||
"', since audio file may not supported by 2.b."
|
||
);
|
||
return;
|
||
}
|
||
this.buffer.set(id, buffer);
|
||
}
|
||
|
||
/**
|
||
* 播放一个音效
|
||
* @param id 音效名称
|
||
* @param position 音频位置,[0, 0, 0]表示正中心,x轴指向水平向右,y轴指向水平向上,z轴指向竖直向上
|
||
* @param orientation 音频朝向,[0, 1, 0]表示朝向前方
|
||
*/
|
||
play(id, position = [0, 0, 0], orientation = [1, 0, 0]) {
|
||
if (!this.enabled || !id) return -1;
|
||
const buffer = this.buffer.get(id);
|
||
if (!buffer) {
|
||
console.warn(
|
||
"Cannot play sound '" +
|
||
id +
|
||
"', since there is no added data named it."
|
||
);
|
||
return -1;
|
||
}
|
||
const soundNum = this.num++;
|
||
|
||
const source = this.player.createBufferSource();
|
||
source.setBuffer(buffer);
|
||
const route = this.player.createRoute(source);
|
||
const stereo = this.player.createStereoEffect();
|
||
stereo.setPosition(position[0], position[1], position[2]);
|
||
stereo.setOrientation(orientation[0], orientation[1], orientation[2]);
|
||
route.addEffect([stereo, this.gain]);
|
||
this.player.addRoute(`sounds.${soundNum}`, route);
|
||
route.play();
|
||
source.output.addEventListener("ended", () => {
|
||
this.playing.delete(soundNum);
|
||
});
|
||
this.playing.add(soundNum);
|
||
return soundNum;
|
||
}
|
||
|
||
/**
|
||
* 停止一个音效
|
||
* @param num 音效的唯一 id
|
||
*/
|
||
stop(num) {
|
||
const id = `sounds.${num}`;
|
||
const route = this.player.getRoute(id);
|
||
if (route) {
|
||
route.stop();
|
||
this.player.removeRoute(id);
|
||
this.playing.delete(num);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止播放所有音效
|
||
*/
|
||
stopAllSounds() {
|
||
this.playing.forEach((v) => {
|
||
const id = `sounds.${v}`;
|
||
const route = this.player.getRoute(id);
|
||
if (route) {
|
||
route.stop();
|
||
this.player.removeRoute(id);
|
||
}
|
||
});
|
||
this.playing.clear();
|
||
}
|
||
}
|
||
const soundPlayer = new SoundPlayer(audioPlayer);
|
||
|
||
function loadAllBgm() {
|
||
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
||
for (const bgm of data.main.bgms) {
|
||
bgmController.addBgm(bgm);
|
||
}
|
||
}
|
||
loadAllBgm();
|
||
AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder);
|
||
AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder);
|
||
|
||
core.plugin.audioSystem = {
|
||
AudioType,
|
||
AudioDecoder,
|
||
AudioStatus,
|
||
checkAudioType,
|
||
isAudioSupport,
|
||
audioPlayer,
|
||
soundPlayer,
|
||
bgmController,
|
||
guessTypeByExt,
|
||
BgmController,
|
||
SoundPlayer,
|
||
EchoEffect,
|
||
DelayEffect,
|
||
ChannelVolumeEffect,
|
||
VolumeEffect,
|
||
StereoEffect,
|
||
AudioEffect,
|
||
AudioPlayer,
|
||
AudioRoute,
|
||
AudioStreamSource,
|
||
AudioElementSource,
|
||
AudioBufferSource,
|
||
loadAllBgm,
|
||
StreamLoader,
|
||
};
|
||
//bgm相关复写
|
||
control.prototype.playBgm = (bgm, when) => {
|
||
bgm = core.getMappedName(bgm);
|
||
if (main.mode != "play" || !core.material.bgms[bgm]) return;
|
||
// 如果不允许播放
|
||
if (!core.musicStatus.bgmStatus) {
|
||
try {
|
||
core.musicStatus.playingBgm = bgm;
|
||
core.musicStatus.lastBgm = bgm;
|
||
core.material.bgms[bgm].pause();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
return;
|
||
}
|
||
core.setMusicBtn();
|
||
|
||
try {
|
||
bgmController.play(bgm, when);
|
||
} catch (e) {
|
||
console.log("无法播放BGM " + bgm);
|
||
console.error(e);
|
||
core.musicStatus.playingBgm = null;
|
||
}
|
||
|
||
};
|
||
control.prototype.pauseBgm = () => {
|
||
bgmController.pause();
|
||
core.setMusicBtn();
|
||
};
|
||
|
||
control.prototype.resumeBgm = function () {
|
||
bgmController.resume();
|
||
core.setMusicBtn();
|
||
};
|
||
control.prototype.checkBgm = function () {
|
||
core.playBgm(bgmController.playingBgm || main.startBgm);
|
||
};
|
||
control.prototype.triggerBgm = function () {
|
||
core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus;
|
||
if (bgmController.playing) bgmController.pause();
|
||
else bgmController.resume();
|
||
core.setMusicBtn();
|
||
core.setLocalStorage("bgmStatus", core.musicStatus.bgmStatus);
|
||
};
|
||
//sound相关复写
|
||
control.prototype.playSound = function (
|
||
sound,
|
||
_pitch,
|
||
callback,
|
||
position,
|
||
orientation
|
||
) {
|
||
if (main.mode != "play" || !core.musicStatus.soundStatus) return callback?.();
|
||
const name = core.getMappedName(sound);
|
||
const num = soundPlayer.play(name, position, orientation);
|
||
const route = audioPlayer.getRoute(`sounds.${num}`);
|
||
if (!route) {
|
||
callback?.();
|
||
return -1;
|
||
} else {
|
||
sleep(route.duration * 1000).then(() => callback?.());
|
||
return num;
|
||
}
|
||
};
|
||
control.prototype.stopSound = function (id) {
|
||
if (isNil(id)) {
|
||
soundPlayer.stopAllSounds();
|
||
} else {
|
||
soundPlayer.stop(id);
|
||
}
|
||
};
|
||
control.prototype.getPlayingSounds = function () {
|
||
return [...soundPlayer.playing];
|
||
};
|
||
//sound加载复写
|
||
loader.prototype._loadOneSound_decodeData = function (name, data) {
|
||
if (data instanceof Blob) {
|
||
var blobReader = new zip.BlobReader(data);
|
||
blobReader.init(function () {
|
||
blobReader.readUint8Array(0, blobReader.size, function (uint8) {
|
||
//core.loader._loadOneSound_decodeData(name, uint8.buffer);
|
||
soundPlayer.add(name, uint8);
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
if (data instanceof ArrayBuffer) {
|
||
const uint8 = new Uint8Array(data);
|
||
soundPlayer.add(name, uint8);
|
||
}
|
||
};
|
||
//音量控制复写
|
||
soundPlayer.setVolume(
|
||
core.musicStatus.userVolume * core.musicStatus.designVolume
|
||
);
|
||
bgmController.setVolume(
|
||
core.musicStatus.userVolume * core.musicStatus.designVolume
|
||
);
|
||
actions.prototype._clickSwitchs_sounds_userVolume = function (delta) {
|
||
var value = Math.round(Math.sqrt(100 * core.musicStatus.userVolume));
|
||
if (value == 0 && delta < 0) return;
|
||
core.musicStatus.userVolume = core.clamp(
|
||
Math.pow(value + delta, 2) / 100,
|
||
0,
|
||
1
|
||
);
|
||
//audioContext 音效 不受designVolume 影响
|
||
if (core.musicStatus.gainNode != null)
|
||
core.musicStatus.gainNode.gain.value = core.musicStatus.userVolume;
|
||
soundPlayer.setVolume(
|
||
core.musicStatus.userVolume * core.musicStatus.designVolume
|
||
);
|
||
bgmController.setVolume(
|
||
core.musicStatus.userVolume * core.musicStatus.designVolume
|
||
);
|
||
core.setLocalStorage("userVolume", core.musicStatus.userVolume);
|
||
core.playSound("确定");
|
||
core.ui._drawSwitchs_sounds();
|
||
};
|
||
},
|
||
"platFly": function () {
|
||
// 本插件可以给平面塔启用一个带小地图的楼传,默认关闭
|
||
// 是否开启本插件,默认禁用;将此改成 true 将启用本插件。
|
||
var __enable = false;
|
||
if (!__enable) return;
|
||
|
||
/* 第一步:复制到插件
|
||
* 第二步:将flag:usePlatFly改为true可将带地图的楼传启用
|
||
* 第三步:将flag:__useMinimap__设为true可开启小地图,该操作需切换楼层
|
||
* 修改小地图的缩放可以修改flag:userScale,默认为1
|
||
* 注意:楼层转换必须使用楼层坐标,而不是前一楼、后一楼
|
||
* 关于操作:上楼和下楼操作为PgUp和PgDn,后退10层和前进10层操作为,(<)和.(>)
|
||
* 注意:请尽量保证地图传送的地方物理位置正好对准(即把地图拼接上箭头位置恰好对齐)否则连线的位置可能不准
|
||
* 分区说明:用普通事件的楼层转换(红点)代替楼层转换(绿点)即可
|
||
* 说明:紫色表示目前可以到达却没有到达过的地图(所以这是一个具有探索性的地图插件)
|
||
*/
|
||
// 录像验证直接干掉这个插件
|
||
if (main.replayChecking) return;
|
||
// *** --- 以下数据为用户可修改数据 修改后不影响该插件的基本功能 --- *** //
|
||
// 检测楼层转换的图块id
|
||
var leftPortal = "leftPortal", // 左
|
||
rightPortal = "rightPortal", // 右
|
||
upPortal = "upPortal", // 上
|
||
downPortal = "downPortal", // 下
|
||
upFloor = "upFloor", // 上楼
|
||
downFloor = "downFloor"; // 下楼
|
||
// 一些常用默认值
|
||
var defaultScale = 1, // 默认缩放比率
|
||
defaultLoop = 5, // 绘制地图时的循环检测地图路线次数,loop为5说明最远的地图可以用6步到达
|
||
defaultOpacity = 0.6, // 默认不透明度
|
||
defaultMinorAlpha = defaultOpacity / 2; // 3D绘图时,不与当前层处于同一高度层的默认初始不透明度,不透明度会随着层数的增加或减少而减少
|
||
// *** --- 用户修改区 END --- *** //
|
||
// 其余可自定义内容均用 //***--- 包裹着
|
||
|
||
/* -----------------------------------下面是一些调用案例
|
||
*
|
||
* 如果想在状态栏显示可以在状态栏自绘里这么写:
|
||
core.drawFlyMap(ctx, 你想要的左上角横坐标, 你想要的左上角纵坐标, 你想要的宽度, 你想要的高度, core.status.floorId, {fromUser: true, opacity: 1, loop: 3, use3D:false});
|
||
*
|
||
* -----------------------------------以下为高深区域,如果不必要可以不看
|
||
*
|
||
* 主要函数说明(这几个函数异常强大,善用可以有弄一些有意思的东西,既然作者都说强大了,那就是特别强大)
|
||
* core.drawFlyMap(ctx : string, x?: number, y?: number, width?:number, height?:number, floorId?:string, config?:any): void
|
||
* 参数说明:
|
||
* ctx:画布,如果不存在则会新建x,y,width,height参数的画布
|
||
* x,y,width,height:画布不存在时创建该数据的画布,如果存在,那么在画布的x,y位置绘制宽度为width,高度为height的地图
|
||
* floorId:以该楼层为中心绘制
|
||
* config:配制参数,包括以下内容(该参数为对象)
|
||
* fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层)
|
||
* oriFloor:这个不用管就好,具体原理比较复杂,不填或者和floorId一样就行
|
||
* scale:缩放比率,这个是绝对比率,与画布大小无关,默认为1
|
||
* interval:地图间的间距,默认为width / 24
|
||
* deltaH:3D地图的纵向高度差,默认为绘制高度(height)/ 4
|
||
* noErase:是否不清空画布再绘制,默认为false
|
||
* fromMini:是否是在小地图上绘制时调用的(无自动缩放和自动定位)
|
||
* loop:绘制层数,fromUser为true时才有用,默认为5
|
||
* opacity:不透明度,默认为0.6
|
||
* minorAlpha:3D地图的不与当前层在同一高度的楼层的初始不透明度,默认为opacity/2
|
||
* layer:平面模式下绘制的楼层高度,当前所在层为0,上楼则+1,下楼则-1,默认为0,只有当use3D为false且可以绘制3D地图时有效
|
||
* use3D:是否启用3D绘图,前提是所有地图不都在同一高度,默认为false
|
||
* reLeft:3D重新定位画布的左位置,默认为-240
|
||
* reTop:3D重新定位画布的上位置,默认为-240
|
||
* clearCache:是否清除缓存重算,默认为false
|
||
* map:这个是最强大的功能了,可以将自定义地图插入,以绘制自定义地图
|
||
* 自定义地图格式:{"left_0_7":"MT1", "right_2_7":"MT2"}以此类推也可以"left_0_7,right_3_7,up_1_0"这种套娃,使用的前提是指定位置得有相应方向的箭头,不然会出错,注意楼层id是到往的楼层的id,如果想绘制3D地图,可以这么写:{"top_1_3,left_0_7": "1_MT1"},前面的1_必须写,上楼为top,下楼为bottom
|
||
* 默认取自floorId地图
|
||
* 如有疑问可在造塔群@古祠
|
||
*
|
||
* core.getFlyMap(floorId?:string, fromUser?:boolean, oriFloor?:string, loop?:number):Object
|
||
* 参数说明:floorId:以该楼层为中心绘制
|
||
* fromUser:是否循环绘制(为false时只绘制一层,否则绘制5层)
|
||
* oriFloor:这个不用管就好,具体原理比较复杂,填null或者和floorId一样就行
|
||
* loop:绘制层数,fromUser为true时才有用,默认为5
|
||
*/
|
||
|
||
////// 绘制楼层传送器 //////
|
||
var originDrawFly = core.ui.drawFly;
|
||
ui.prototype.drawFly = function (page) {
|
||
if (!flags.usePlatFly || core.isReplaying()) return originDrawFly.call(core.ui, page);
|
||
core.status.event.data = page;
|
||
var floorId = core.floorIds[page];
|
||
core.clearMap('ui');
|
||
core.setAlpha('ui', 0.85);
|
||
core.fillRect('ui', 0, 0, this.PIXEL, this.PIXEL, '#000000');
|
||
core.setAlpha('ui', 1);
|
||
var size = this.PIXEL;
|
||
//***--- 楼传绘制 可根据自己的需求更改 更改之后注意更改点击操作的函数
|
||
// 背景
|
||
core.drawThumbnail(floorId, null, { ctx: 'ui', x: 0, y: 0, size: size, damage: true, fromFly: true });
|
||
core.fillRect("ui", 0, 65, 32, size - 130, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size, 65, -32, size - 130, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 65, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 65, size, size / 2 - 114, -32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size / 2 + 49, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size / 2 + 49, size, size / 2 - 114, -32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size / 2 - 47, 0, 94, 32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size / 2 - 47, size, 94, -32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 0, 0, 63, 32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size, 0, -63, 32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 0, 32, 32, 31, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 0, size - 32, 32, -31, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", 0, size, 63, -32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size, size, -63, -32, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size, size - 32, -32, -31, [0, 0, 0, 0.7]);
|
||
core.fillRect("ui", size, 32, -32, 31, [0, 0, 0, 0.7]);
|
||
core.setTextAlign("ui", "center");
|
||
// 文字
|
||
if (core.getFloorByDirection("left", floorId))
|
||
core.fillText("ui", "←", 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("left",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
|
||
if (core.getFloorByDirection("right", floorId))
|
||
core.fillText("ui", "→", size - 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("right",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
|
||
if (core.getFloorByDirection("up", floorId))
|
||
core.fillText("ui", "↑", size / 2, 28, core.hasVisitedFloor(core.getFloorByDirection("up",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
|
||
if (core.getFloorByDirection("down", floorId))
|
||
core.fillText("ui", "↓", size / 2, size - 4, core.hasVisitedFloor(core.getFloorByDirection("down",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
|
||
if (core.getFloorByDirection("top", floorId))
|
||
core.fillText("ui", "上楼", size / 4 + 8, 24, core.hasVisitedFloor(core.getFloorByDirection("top",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font);
|
||
if (core.getFloorByDirection("bottom", floorId))
|
||
core.fillText("ui", "下楼", size / 4 * 3 - 8, 24, core.hasVisitedFloor(core.getFloorByDirection("bottom",
|
||
floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font);
|
||
core.fillText("ui", "退10层", size / 4 + 8, size - 8, core.actions._getNextFlyFloor(-1) == page ? "#ff22ff" : "#ffffff",
|
||
"22px " + core.status.globalAttribute.font);
|
||
core.fillText("ui", "进10层", size / 4 * 3 - 8, size - 8, core.actions._getNextFlyFloor(1) == page ? "#ff22ff" : "#ffffff",
|
||
"22px " + core.status.globalAttribute.font);
|
||
core.fillText("ui", "退出", 32, 24, "#ffffff", "22px " + core.status.globalAttribute.font);
|
||
core.fillText("ui", "楼层名", 32, size - 8, "#ffffff", "22px " + core.status.globalAttribute.font);
|
||
core.fillText("ui", "(B)", 16, size - 40, "#ffffff", "22px " + core.status.globalAttribute.font);
|
||
core.createCanvas("mapOnUi", -240, -240, size + 480, size + 480, 150);
|
||
core.drawFlyMap("mapOnUi", 240, 240, size, size, floorId, { fromUser: true, oriFloor: floorId, use3D: flags.use3D });
|
||
if (core.can3D(floorId) && !flags.in3D)
|
||
core.fillText("ui", "3D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font);
|
||
if (flags.in3D)
|
||
core.fillText("ui", "2D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font);
|
||
if (core.can3D(floorId)) core.fillText("ui", "(Z)", size - 16, 56, "#ffffff", "20px " + core.status.globalAttribute.font);
|
||
if (flags.flyTitle) {
|
||
var style = document.getElementById("ui").getContext("2d");
|
||
style.shadowColor = "rgba(0, 0, 0, 1)";
|
||
style.shadowBlur = 5;
|
||
core.fillRect("ui", size / 4, size / 4, size / 2, size / 8, [180, 180, 180, 0.7]);
|
||
core.strokeRect("ui", size / 4, size / 4, size / 2, size / 8, [255, 255, 255, 0.7], 3);
|
||
style.shadowOffsetX = 4;
|
||
style.shadowOffsetY = 2;
|
||
core.fillText("ui", (core.status.maps[floorId] || {}).title, size / 2, size / 16 * 5 + 11, "#ffffff",
|
||
"32px " + core.status.globalAttribute.font);
|
||
style.shadowColor = "none";
|
||
style.shadowBlur = 0;
|
||
style.shadowOffsetX = 0;
|
||
style.shadowOffsetY = 0;
|
||
}
|
||
//***--- 楼传绘制
|
||
};
|
||
////// 楼层传送器界面时的点击操作 //////
|
||
var originClickFly = core.actions._clickFly;
|
||
actions.prototype._clickFly = function (x, y) {
|
||
if (!flags.usePlatFly || core.isReplaying()) return originClickFly.call(core.actions, x, y);
|
||
var page = core.status.event.data;
|
||
var floorId = core.floorIds[page];
|
||
//***--- 点击操作 可以修改的地方只有x,y坐标 其余不可修改
|
||
if (x <= 2 && y <= 1) {
|
||
core.playSound('取消');
|
||
core.deleteCanvas("mapOnUi")
|
||
core.ui.closePanel();
|
||
return;
|
||
}
|
||
// 3D模式
|
||
if (x >= core.__SIZE__ - 2 && y <= 1) {
|
||
if (core.can3D(floorId) && !flags.in3D)
|
||
flags.use3D = true;
|
||
if (flags.in3D) flags.use3D = false;
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(page);
|
||
return;
|
||
}
|
||
// 显示名称
|
||
if (x <= 1 && y >= core.__SIZE__ - 2) {
|
||
if (flags.flyTitle) flags.flyTitle = false;
|
||
else flags.flyTitle = true;
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(page);
|
||
return;
|
||
}
|
||
// 飞过去
|
||
if (x > 1 && x < core.__SIZE__ - 1 && y > 1 && y < core.__SIZE__ - 1) {
|
||
if (core.status.maps[core.status.floorId].canFlyFrom &&
|
||
core.status.maps[core.floorIds[core.status.event.data]].canFlyTo &&
|
||
core.hasVisitedFloor(core.floorIds[core.status.event.data])) {
|
||
core.deleteCanvas("mapOnUi");
|
||
}
|
||
core.flyTo(core.floorIds[core.status.event.data]);
|
||
}
|
||
// 前进10层 后退10层
|
||
if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(-1) != page &&
|
||
x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2) {
|
||
core.ui.drawFly(this._getNextFlyFloor(-10));
|
||
core.playSound("光标移动");
|
||
}
|
||
if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(1) != page &&
|
||
x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3) {
|
||
core.ui.drawFly(this._getNextFlyFloor(10));
|
||
core.playSound("光标移动");
|
||
}
|
||
// 获取索引
|
||
function getId(direction) {
|
||
var id = core.getFloorByDirection(direction, floorId);
|
||
for (var i in core.floorIds) {
|
||
if (core.floorIds[i] == id) return parseInt(i);
|
||
}
|
||
}
|
||
// 上下左右和上下楼
|
||
if (x < 1 && core.getFloorByDirection("left", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("left", floorId)) && y >= 2 && y < core.__SIZE__ - 2) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("left"));
|
||
}
|
||
if (x > core.__SIZE__ - 2 && core.getFloorByDirection("right", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("right", floorId)) && y >= 2 && y < core.__SIZE__ - 2) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("right"));
|
||
}
|
||
if (y < 1 && core.getFloorByDirection("up", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("up", floorId)) &&
|
||
x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("up"));
|
||
}
|
||
if (y > core.__SIZE__ - 2 && core.getFloorByDirection("down", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("down", floorId)) &&
|
||
x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("down"));
|
||
}
|
||
if (y < 1 && x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2 &&
|
||
core.getFloorByDirection("top", floorId) && core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("top"));
|
||
}
|
||
if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3 &&
|
||
core.getFloorByDirection("bottom", floorId) && core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) {
|
||
core.playSound("光标移动");
|
||
core.drawFly(getId("bottom"));
|
||
}
|
||
return;
|
||
//***--- 点击操作
|
||
};
|
||
////// 楼层传送器界面时,按下某个键的操作 //////
|
||
var originKeyDownFly = core.actions._keyDownFly;
|
||
actions.prototype._keyDownFly = function (keycode) {
|
||
if (!flags.usePlatFly || core.isReplaying()) return originKeyDownFly.call(core.actions, keycode);
|
||
var page = core.status.event.data;
|
||
var floorId = core.floorIds[page];
|
||
// 获取索引
|
||
function getId(direction) {
|
||
var id = core.getFloorByDirection(direction, floorId);
|
||
for (var i in core.floorIds) {
|
||
if (core.floorIds[i] == id) return parseInt(i);
|
||
}
|
||
}
|
||
//***--- 按键操作 只可以修改按键的keycode
|
||
if (keycode == 37 && core.getFloorByDirection("left", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("left", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("left"));
|
||
} else if (keycode == 38 && core.getFloorByDirection("up", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("up", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("up"));
|
||
} else if (keycode == 39 && core.getFloorByDirection("right", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("right", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("right"));
|
||
} else if (keycode == 40 && core.getFloorByDirection("down", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("down", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("down"));
|
||
} else if (keycode == 33 && core.getFloorByDirection("top", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("top"));
|
||
} else if (keycode == 34 && core.getFloorByDirection("bottom", floorId) &&
|
||
core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) {
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(getId("bottom"));
|
||
} else if (keycode == 90) {
|
||
if (core.can3D(floorId) && !flags.in3D)
|
||
flags.use3D = true;
|
||
if (flags.in3D) flags.use3D = false;
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(page);
|
||
} else if (keycode == 66) { // 地图名
|
||
if (flags.flyTitle) flags.flyTitle = false;
|
||
else flags.flyTitle = true;
|
||
core.playSound('光标移动');
|
||
core.ui.drawFly(page);
|
||
} else if (keycode == 188 && core.actions._getNextFlyFloor(-1) != page) { // 退10层
|
||
core.ui.drawFly(this._getNextFlyFloor(-10));
|
||
core.playSound("光标移动");
|
||
} else if (keycode == 190 && core.actions._getNextFlyFloor(1) != page) { // 进10层
|
||
core.ui.drawFly(this._getNextFlyFloor(10));
|
||
core.playSound("光标移动");
|
||
}
|
||
return;
|
||
//***--- 按键操作
|
||
};
|
||
////// 楼层传送器界面时,放开某个键的操作 //////
|
||
var originKeyUpFly = core.actions._keyUpFly;
|
||
actions.prototype._keyUpFly = function (keycode) {
|
||
if (!flags.usePlatFly || core.isReplaying()) return originKeyUpFly.call(core.actions, keycode);
|
||
if (keycode == 71 || keycode == 27 || keycode == 88) {
|
||
core.playSound('取消');
|
||
core.deleteCanvas("mapOnUi");
|
||
core.ui.closePanel();
|
||
}
|
||
if (keycode == 13 || keycode == 32 || keycode == 67)
|
||
this._clickFly(this.HSIZE - 1, this.HSIZE - 1);
|
||
return;
|
||
};
|
||
////// 点击楼层传送器时的打开操作 //////
|
||
events.prototype.useFly = function (fromUserAction) {
|
||
if (core.isReplaying()) return;
|
||
|
||
// 从“浏览地图”页面:尝试直接传送到该层
|
||
if (core.status.event.id == 'viewMaps') {
|
||
if (!core.hasItem('fly')) {
|
||
core.playSound('操作失败');
|
||
core.drawTip('你没有' + core.material.items['fly'].name, 'fly');
|
||
} else if (!core.canUseItem('fly')) {
|
||
core.playSound('操作失败');
|
||
core.drawTip('无法传送到当前层', 'fly');
|
||
} else {
|
||
core.flyTo(core.status.event.data.floorId);
|
||
}
|
||
return;
|
||
}
|
||
if (!this._checkStatus('fly', fromUserAction, true)) {
|
||
core.deleteCanvas('mapOnUi');
|
||
return;
|
||
}
|
||
if (core.flags.flyNearStair && !core.nearStair()) {
|
||
core.playSound('操作失败');
|
||
core.drawTip("只有在楼梯边才能使用" + core.material.items['fly'].name, 'fly');
|
||
core.unlockControl();
|
||
core.status.event.data = null;
|
||
core.status.event.id = null;
|
||
return;
|
||
}
|
||
if (!core.canUseItem('fly')) {
|
||
core.playSound('操作失败');
|
||
core.drawTip(core.material.items['fly'].name + "好像失效了", 'fly');
|
||
core.unlockControl();
|
||
core.status.event.data = null;
|
||
core.status.event.id = null;
|
||
return;
|
||
}
|
||
core.playSound('打开界面');
|
||
core.useItem('fly', true);
|
||
return;
|
||
};
|
||
// 获取区域平面地图
|
||
this.getFlyMap = function (floorId, fromUser, oriFloor, loop, clearCache) {
|
||
floorId = floorId || core.status.floorId;
|
||
if (floorId == oriFloor && !fromUser) return;
|
||
oriFloor = oriFloor || core.status.floorId;
|
||
if (!floorId) return;
|
||
// 判断是否需要缓存
|
||
function needCache(fromUser) {
|
||
if (fromUser && !core.status.flyMap[floorId]) return true;
|
||
if (!fromUser && !core.status.flyMap.cache[floorId]) return true;
|
||
return false;
|
||
}
|
||
// 缓存,加快运行速率
|
||
if (!core.status.flyMap) core.status.flyMap = {};
|
||
if (!core.status.flyMap.cache) core.status.flyMap.cache = {};
|
||
if (!core.status.layer) core.status.layer = {};
|
||
if (!core.status.layer[floorId]) core.status.layer[floorId] = {};
|
||
if (core.status.flyMap.cache[floorId] && fromUser) delete core.status.flyMap.cache[floorId]
|
||
if (core.status.flyMap[floorId] && !fromUser) delete core.status.flyMap[floorId]
|
||
if (needCache(fromUser) || clearCache) {
|
||
// 初始化
|
||
core.status.flyMap[floorId] = {};
|
||
core.status.flyMap[floorId].thisMap = {};
|
||
core.extractBlocks(floorId);
|
||
core.status.maps[floorId].blocks.forEach(function (block) {
|
||
var id = block.event.id;
|
||
var x = block.x,
|
||
y = block.y;
|
||
var trigger = block.event.trigger;
|
||
if (trigger != "changeFloor" && trigger != "upFloor" && trigger != "downFloor") return;
|
||
// 是箭头且可以切换地图
|
||
var toFloor = block.event.data.floorId;
|
||
// 加入相应位置
|
||
// 箭头
|
||
if (id == leftPortal) {
|
||
core.status.flyMap[floorId].thisMap["left_" + x + "_" + y] = toFloor;
|
||
}
|
||
if (id == upPortal) {
|
||
core.status.flyMap[floorId].thisMap["up_" + x + "_" + y] = toFloor;
|
||
}
|
||
if (id == rightPortal) {
|
||
core.status.flyMap[floorId].thisMap["right_" + x + "_" + y] = toFloor;
|
||
}
|
||
if (id == downPortal) {
|
||
core.status.flyMap[floorId].thisMap["down_" + x + "_" + y] = toFloor;
|
||
}
|
||
// 上下楼
|
||
if (id == upFloor) {
|
||
core.status.flyMap[floorId].thisMap["top_" + x + "_" + y] = toFloor;
|
||
core.status.layer[floorId].top = true;
|
||
}
|
||
if (id == downFloor) {
|
||
core.status.flyMap[floorId].thisMap["bottom_" + x + "_" + y] = toFloor;
|
||
core.status.layer[floorId].bottom = true;
|
||
}
|
||
});
|
||
// 把下几层接着检测出来
|
||
if (fromUser) {
|
||
var usedId = {};
|
||
for (var c = 1; c <= loop; c++) {
|
||
for (var i in core.status.flyMap[floorId].thisMap) {
|
||
var link = core.status.flyMap[floorId].thisMap;
|
||
if (!core.hasVisitedFloor(link[i]) || link[i] instanceof Object || usedId[link[i]]) continue;
|
||
usedId[link[i]] = true;
|
||
var next = core.getFlyMap(link[i], false, oriFloor);
|
||
for (var to in next) {
|
||
if (!core.status.layer[next[to]]) core.status.layer[next[to]] = {};
|
||
core.status.flyMap[floorId].thisMap[i + ',' + to] = next[to];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 把先上再下之类的去掉
|
||
if (fromUser) {
|
||
for (var i in core.status.flyMap[floorId].thisMap) {
|
||
var route = i.split(",");
|
||
for (var one = 0; one <= route.length - 2; one++) {
|
||
var step = route[one],
|
||
next = route[one + 1];
|
||
if ((step.startsWith("up") && next.startsWith("down")) ||
|
||
(step.startsWith("down") && next.startsWith("up")) ||
|
||
(step.startsWith("left") && next.startsWith("right")) ||
|
||
(step.startsWith("right") && next.startsWith("left")) ||
|
||
(step.startsWith("top") && next.startsWith("bottom")) ||
|
||
(step.startsWith("bottom") && next.startsWith("top"))) {
|
||
delete core.status.flyMap[floorId].thisMap[i];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 非当前层不能存此类缓存
|
||
if (!fromUser) {
|
||
core.status.flyMap.cache = {};
|
||
core.status.flyMap.cache[floorId] = {};
|
||
core.status.flyMap.cache[floorId].thisMap = core.status.flyMap[floorId].thisMap;
|
||
delete core.status.flyMap[floorId];
|
||
}
|
||
return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap;
|
||
} else { // 直接使用缓存
|
||
return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap;
|
||
}
|
||
};
|
||
this.can3D = function (floorId) {
|
||
var map = core.getFlyMap(floorId, true, floorId);
|
||
for (var route in map) {
|
||
if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0)
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
// 绘制地图
|
||
this.drawFlyMap = function (ctx, x, y, width, height, floorId, config) {
|
||
// 初始化配置项
|
||
//***--- 初始化 可以修改 || 后的默认值 参数说明在开头的高深区域中
|
||
var fromUser = config.fromUser || false,
|
||
oriFloor = config.oriFloor || core.status.floorId,
|
||
scale = config.scale || null,
|
||
interval = config.interval || (width / 24),
|
||
noErase = config.noErase || false,
|
||
fromMini = config.fromMini || false,
|
||
loop = config.loop || defaultLoop,
|
||
opacity = config.opacity || defaultOpacity,
|
||
layer = config.layer || 0,
|
||
use3D = config.use3D || false,
|
||
clearCache = config.clearCache || false,
|
||
map = config.map || null;
|
||
//***--- 初始化
|
||
map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache);
|
||
floorId = floorId || core.status.floorId;
|
||
if (!floorId) return;
|
||
// 检测是否需要3D绘图
|
||
if (!fromMini && use3D) {
|
||
for (var route in map) {
|
||
if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) {
|
||
config.map = map;
|
||
return core.draw3DFlyMap(ctx, x, y, width, height, floorId, config);
|
||
}
|
||
}
|
||
}
|
||
if (layer != 0 && !use3D) {
|
||
var canLayer = false
|
||
for (var route in map) {
|
||
if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) {
|
||
canLayer = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!canLayer) layer = 0;
|
||
}
|
||
flags.in3D = false;
|
||
// 初始化
|
||
var userScale = true;
|
||
var newCreate = false;
|
||
if (!scale) {
|
||
userScale = false;
|
||
scale = scale || defaultScale;
|
||
}
|
||
if (!core.dymCanvas[ctx]) {
|
||
core.createCanvas(ctx, x, y, width, height, 140);
|
||
newCreate = true;
|
||
}
|
||
x = x || 0;
|
||
y = y || 0;
|
||
// 获得canvas属性
|
||
width = width || document.getElementById(ctx).width;
|
||
height = height || document.getElementById(ctx).height;
|
||
var oLeft = document.getElementById(ctx).offsetLeft / core.domStyle.scale,
|
||
oTop = document.getElementById(ctx).offsetTop / core.domStyle.scale;
|
||
// 重置大地图和楼传地图的canvas位置
|
||
if (ctx == "mapOnUi") core.relocateCanvas("mapOnUi", -240, -240);
|
||
if (!noErase)
|
||
core.clearMap(ctx);
|
||
var horCenter = Math.floor(width / 2),
|
||
uprCenter = Math.floor(height / 2);
|
||
if (!newCreate) {
|
||
horCenter += x;
|
||
uprCenter += y;
|
||
}
|
||
var centerX = horCenter,
|
||
centerY = uprCenter;
|
||
var left = centerX,
|
||
right = centerX,
|
||
up = centerY,
|
||
down = centerY;
|
||
var used = {};
|
||
var haveLayer = {};
|
||
var nx = horCenter,
|
||
ny = uprCenter;
|
||
// 先把所在楼层绘制了
|
||
if (layer == 0) {
|
||
var nw = core.status.maps[floorId].width * 2 * scale,
|
||
nh = core.status.maps[floorId].height * 2 * scale;
|
||
core.setAlpha(ctx, 1);
|
||
core.fillRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#000000");
|
||
core.strokeRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#ffff22", 3 * scale);
|
||
// 当前层上下楼显示
|
||
if (!haveLayer[floorId]) {
|
||
core.setAlpha(ctx, 1);
|
||
var needLayer = core.status.layer[floorId];
|
||
if (needLayer.top && needLayer.bottom) {
|
||
core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale,
|
||
centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
|
||
core.drawIcon(ctx, "downFloor", centerX - nw / 2 + core.__SIZE__ * scale,
|
||
centerY - nh / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
|
||
}
|
||
if (needLayer.top && !needLayer.bottom) {
|
||
core.drawIcon(ctx, "upFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2,
|
||
Math.min(nw, nh), Math.min(nw, nh));
|
||
}
|
||
if (!needLayer.top && needLayer.bottom) {
|
||
core.drawIcon(ctx, "downFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2,
|
||
Math.min(nw, nh), Math.min(nw, nh));
|
||
}
|
||
haveLayer[floorId] = true;
|
||
}
|
||
// 四侧最远位置
|
||
if (left > centerX - nw / 2) left = centerX - nw / 2;
|
||
if (right < centerX + nw / 2) right = centerX + nw / 2;
|
||
if (down < centerY + nh / 2) down = centerY + nh / 2;
|
||
if (up > centerY - nh / 2) up = centerY - nh / 2;
|
||
}
|
||
core.setAlpha(ctx, opacity);
|
||
for (var route in map) { // 绘制楼层和线条
|
||
var rouArr = route.split(",");
|
||
// 检索路线及画线
|
||
// 初始化
|
||
centerX = nx;
|
||
centerY = ny;
|
||
var nowFloor = floorId || core.status.floorId;
|
||
var nowLayer = 0;
|
||
for (var one in rouArr) { // 一个一个检测
|
||
var step = rouArr[one].split("_");
|
||
var cx = step[1],
|
||
cy = step[2];
|
||
// 获得当前图块
|
||
core.getMapBlocksObj(nowFloor, true);
|
||
var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy];
|
||
if (!nowBlock) continue;
|
||
var toLoc = nowBlock.event.data.loc,
|
||
toFloor = nowBlock.event.data.floorId;
|
||
var needLayer = core.status.layer[toFloor];
|
||
// 当前层宽度和高度
|
||
var nw = core.status.maps[nowFloor].width * 2 * scale,
|
||
nh = core.status.maps[nowFloor].height * 2 * scale;
|
||
// 目标层宽度和高度
|
||
var tw = core.status.maps[toFloor].width * 2 * scale,
|
||
th = core.status.maps[toFloor].height * 2 * scale;
|
||
// 将当前层变为toFloor
|
||
nowFloor = toFloor;
|
||
// 超范围不画
|
||
if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height ||
|
||
centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue;
|
||
// 绘制toFloor层
|
||
core.setAlpha(ctx, opacity);
|
||
// 确定center 根据箭头自适配 同时绘制线条 我已经看不懂了
|
||
if (!use3D && (step[0] == "top" || step[0] == "bottom") && layer == 0) break;
|
||
if (step[0] == "top") nowLayer++;
|
||
if (step[0] == "bottom") nowLayer--;
|
||
if (step[0] == 'left') {
|
||
var shouldTo = th / 2,
|
||
realTo = toLoc[1] * 2 * scale;
|
||
var shouldFrom = nh / 2,
|
||
realFrom = step[2] * 2 * scale;
|
||
if (nowLayer == layer) {
|
||
core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom,
|
||
centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom,
|
||
centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale);
|
||
}
|
||
centerX -= nw / 2 + tw / 2 + interval;
|
||
centerY += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
if (step[0] == 'right') {
|
||
var shouldTo = th / 2,
|
||
realTo = toLoc[1] * 2 * scale;
|
||
var shouldFrom = nh / 2,
|
||
realFrom = step[2] * 2 * scale;
|
||
if (nowLayer == layer) {
|
||
core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom,
|
||
centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom,
|
||
centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale);
|
||
}
|
||
centerX += nw / 2 + tw / 2 + interval;
|
||
centerY += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
if (step[0] == 'up') {
|
||
var shouldTo = tw / 2,
|
||
realTo = toLoc[0] * 2 * scale;
|
||
var shouldFrom = nw / 2,
|
||
realFrom = step[1] * 2 * scale;
|
||
if (nowLayer == layer) {
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2,
|
||
centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2,
|
||
centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#000000", 2 * scale);
|
||
}
|
||
centerY -= nh / 2 + th / 2 + interval;
|
||
centerX += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
if (step[0] == 'down') {
|
||
var shouldTo = tw / 2,
|
||
realTo = toLoc[0] * 2 * scale;
|
||
var shouldFrom = nw / 2,
|
||
realFrom = step[1] * 2 * scale;
|
||
if (nowLayer == layer) {
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2,
|
||
centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2,
|
||
centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#000000", 2 * scale);
|
||
}
|
||
centerY += nh / 2 + th / 2 + interval;
|
||
centerX += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
// 只有和目标层高度相同时才绘制
|
||
if (nowLayer != layer) continue;
|
||
// 超范围的不画
|
||
if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height ||
|
||
centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue;
|
||
// 四侧最远位置
|
||
if (left > centerX - tw / 2) left = centerX - tw / 2;
|
||
if (right < centerX + tw / 2) right = centerX + tw / 2;
|
||
if (down < centerY + th / 2) down = centerY + th / 2;
|
||
if (up > centerY - th / 2) up = centerY - th / 2;
|
||
// 画过了不画
|
||
if (used[toFloor]) continue;
|
||
used[toFloor] = true;
|
||
// 画地图格
|
||
if (core.hasVisitedFloor(toFloor)) {
|
||
core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#000000");
|
||
core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale);
|
||
} else {
|
||
core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ff22ff");
|
||
core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale);
|
||
break;
|
||
}
|
||
// 上下楼显示
|
||
if (haveLayer[toFloor]) continue;
|
||
core.setAlpha(ctx, opacity);
|
||
if (needLayer.top && needLayer.bottom) {
|
||
core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale,
|
||
centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
|
||
core.drawIcon(ctx, "downFloor", centerX - tw / 2 + core.__SIZE__ * scale,
|
||
centerY - th / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
|
||
}
|
||
if (needLayer.top && !needLayer.bottom) {
|
||
core.drawIcon(ctx, "upFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2,
|
||
Math.min(tw, th), Math.min(tw, th));
|
||
}
|
||
if (!needLayer.top && needLayer.bottom) {
|
||
core.drawIcon(ctx, "downFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2,
|
||
Math.min(tw, th), Math.min(tw, th));
|
||
}
|
||
haveLayer[toFloor] = true;
|
||
}
|
||
}
|
||
// 自动缩放
|
||
if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) {
|
||
scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64));
|
||
var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, layer: layer, opacity: opacity, loop: loop };
|
||
return core.drawFlyMap(ctx, x, y, width, height, floorId, con);
|
||
}
|
||
// 大地图和楼层地图自适配定位
|
||
if (ctx == "mapOnUi" && !fromMini && (left - nx < -128 || right - nx > 128 ||
|
||
up - ny < -128 || down - ny > 128)) {
|
||
core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2);
|
||
}
|
||
};
|
||
// 3D绘图
|
||
this.draw3DFlyMap = function (ctx, x, y, width, height, floorId, config) {
|
||
// 初始化配置项
|
||
//***--- 初始化 同上一个初始化
|
||
var fromUser = config.fromUser || false,
|
||
oriFloor = config.oriFloor || core.status.floorId,
|
||
scale = config.scale || null,
|
||
interval = config.interval || (width / 24),
|
||
deltaH = config.deltaH || (height / 8),
|
||
noErase = config.noErase || false,
|
||
fromMini = config.fromMini || false,
|
||
loop = config.loop || defaultLoop,
|
||
opacity = config.opacity || defaultOpacity,
|
||
minorAlpha = config.minorAlpha || defaultMinorAlpha,
|
||
reLeft = config.reLeft || -240,
|
||
reTop = config.reTop || -240,
|
||
clearCache = config.clearCache || false,
|
||
map = config.map || null;
|
||
//***--- 初始化
|
||
map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache);
|
||
// 当前层是否一个人在一层
|
||
var alone = true;
|
||
for (var route in map) {
|
||
if (route.startsWith("left") || route.startsWith("right") ||
|
||
route.startsWith("up") || route.startsWith("down")) {
|
||
alone = false;
|
||
break;
|
||
}
|
||
}
|
||
// 是则增加一个先上再下的路径
|
||
if (alone) {
|
||
for (var route in map) {
|
||
if (route.startsWith("top")) {
|
||
var first = route.split(",")[0].split("_");
|
||
break;
|
||
}
|
||
}
|
||
var success = false;
|
||
core.getMapBlocksObj(nowFloor, true);
|
||
var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]];
|
||
var toFloor = nowBlock.event.data.floorId;
|
||
core.extractBlocks(toFloor);
|
||
core.status.maps[toFloor].blocks.forEach(function (block) {
|
||
var id = block.event.id;
|
||
var x = block.x,
|
||
y = block.y;
|
||
var trigger = block.event.trigger;
|
||
if (trigger != "changeFloor") return;
|
||
if (id == "downFloor") {
|
||
map["top_" + first[1] + "_" + first[2] + "," + "bottom_" + x + "_" + y] = floorId;
|
||
success = true;
|
||
return;
|
||
}
|
||
});
|
||
// 添加先上再下失败 尝试先下再上
|
||
if (!success) {
|
||
for (var route in map) {
|
||
if (route.startsWith("bottom")) {
|
||
var first = route.split(",")[0].split("_");
|
||
break;
|
||
}
|
||
}
|
||
var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]];
|
||
var toFloor = nowBlock.event.data.floorId;
|
||
core.extractBlocks(toFloor);
|
||
core.status.maps[toFloor].blocks.forEach(function (block) {
|
||
var id = block.event.id;
|
||
var x = block.x,
|
||
y = block.y;
|
||
var trigger = block.event.trigger;
|
||
if (trigger != "changeFloor") return;
|
||
if (id == "upFloor") {
|
||
map["bottom_" + first[1] + "_" + first[2] + "," + "top_" + x + "_" + y] = floorId;
|
||
success = true;
|
||
return;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
floorId = floorId || core.status.floorId;
|
||
if (!floorId) return;
|
||
flags.in3D = true;
|
||
// 初始化
|
||
// 获得排序过的楼层路径
|
||
map = core.sortFloor(map);
|
||
map = map.map;
|
||
var userScale = true;
|
||
var newCreate = false;
|
||
if (!scale) {
|
||
userScale = false;
|
||
scale = scale || defaultScale;
|
||
}
|
||
if (!core.dymCanvas[ctx]) {
|
||
core.createCanvas(ctx, x, y, width, height, 140);
|
||
newCreate = true;
|
||
}
|
||
x = x || 0;
|
||
y = y || 0;
|
||
// 获得canvas属性
|
||
width = width || document.getElementById(ctx).width;
|
||
height = height || document.getElementById(ctx).height;
|
||
// 重置canvas位置
|
||
core.relocateCanvas(ctx, reLeft, reTop);
|
||
if (!noErase)
|
||
core.clearMap(ctx);
|
||
var horCenter = Math.floor(width / 2),
|
||
uprCenter = Math.floor(height / 2);
|
||
if (!newCreate) {
|
||
horCenter += x;
|
||
uprCenter += y;
|
||
}
|
||
// 单元格的中心点 即水平线中点处
|
||
var centerX = horCenter,
|
||
centerY = uprCenter;
|
||
var left = centerX,
|
||
right = centerX,
|
||
up = centerY,
|
||
down = centerY;
|
||
var used = {};
|
||
var nx = horCenter,
|
||
ny = uprCenter;
|
||
// 开始绘制
|
||
for (var i = 0; i < map.length; i++) {
|
||
var route = map[i][0];
|
||
var nowLayer = map[i][1];
|
||
var everyLayer = 0;
|
||
route = route.split(",");
|
||
// 每条路线初始化
|
||
centerX = horCenter;
|
||
centerY = uprCenter;
|
||
centerX += core.status.maps[floorId].height * scale * Math.SQRT2 / 4;
|
||
if (flags.viewingLayer) {
|
||
centerY += deltaH * flags.viewingLayer;
|
||
}
|
||
var nowFloor = floorId || core.status.floorId;
|
||
for (var one = 0; one < route.length; one++) {
|
||
var step = route[one].split("_");
|
||
var cx = step[1],
|
||
cy = step[2];
|
||
// 检测高度,是否与nowLayer一致 不一致在处理完center以后不绘制
|
||
if (step[0] == "top") everyLayer++;
|
||
if (step[0] == "bottom") everyLayer--;
|
||
// 获得当前图块
|
||
core.getMapBlocksObj(nowFloor, true);
|
||
var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy];
|
||
if (!nowBlock) continue;
|
||
var toLoc = nowBlock.event.data.loc,
|
||
toFloor = nowBlock.event.data.floorId;
|
||
// 当前层宽度和高度
|
||
// 斜二测画法
|
||
var nw = core.status.maps[nowFloor].width * 2 * scale,
|
||
nh = core.status.maps[nowFloor].height * scale * Math.SQRT2 / 2;
|
||
// 目标层宽度和高度
|
||
var tw = core.status.maps[toFloor].width * 2 * scale,
|
||
th = core.status.maps[toFloor].height * scale * Math.SQRT2 / 2;
|
||
if (!(toLoc instanceof Array)) {
|
||
toLoc = [Math.floor(tw / 4 / scale), Math.floor(th / 4 / scale)];
|
||
}
|
||
// 绘制当前层
|
||
if (nowLayer == 0 && !used[floorId]) {
|
||
core.setAlpha(ctx, 1);
|
||
used[floorId] = true;
|
||
var nowW = core.status.maps[floorId].width * 2 * scale;
|
||
var nowH = core.status.maps[floorId].height * scale * Math.SQRT2 / 2;
|
||
var nodes = [
|
||
[centerX - nowW / 2 - nowH / 2, centerY + nowH / 2],
|
||
[centerX + nowW / 2 - nowH / 2, centerY + nowH / 2],
|
||
[centerX + nowW / 2 + nowH / 2, centerY - nowH / 2],
|
||
[centerX - nowW / 2 + nowH / 2, centerY - nowH / 2]
|
||
];
|
||
core.fillPolygon(ctx, nodes, "#000000");
|
||
core.strokePolygon(ctx, nodes, "#ffff22", 1.5 * scale);
|
||
// 四侧最远位置
|
||
if (left > centerX - nw / 2 - nh / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - nw / 2 - nh / 2;
|
||
if (right < centerX + nw / 2 + nh / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + nw / 2 + nh / 2;
|
||
}
|
||
// 将当前层变为toFloor
|
||
var fromFloor = nowFloor;
|
||
nowFloor = toFloor;
|
||
// 计算center 画同层间的线 我已经看不懂了
|
||
// 设置不透明度
|
||
if (nowLayer == (flags.viewingLayer || 0)) {
|
||
core.setAlpha(ctx, opacity);
|
||
} else {
|
||
core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
|
||
}
|
||
if (step[0] == "left") {
|
||
var shouldFrom = nh / 2,
|
||
realFrom = cy * scale * Math.SQRT2 / 2;
|
||
var shouldTo = th / 2,
|
||
realTo = toLoc[1] * scale * Math.SQRT2 / 2;
|
||
if (everyLayer == nowLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
|
||
core.drawLine(ctx, centerX - nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom,
|
||
centerX - nw / 2 - interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale);
|
||
centerX -= nw / 2 + tw / 2 + interval + shouldTo - realTo + realFrom - shouldFrom;
|
||
centerY += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
if (step[0] == "right") {
|
||
var shouldFrom = nh / 2,
|
||
realFrom = cy * scale * Math.SQRT2 / 2;
|
||
var shouldTo = th / 2,
|
||
realTo = toLoc[1] * scale * Math.SQRT2 / 2;
|
||
if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
|
||
core.drawLine(ctx, centerX + nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom,
|
||
centerX + nw / 2 + interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale);
|
||
centerX += nw / 2 + tw / 2 + interval - (shouldTo - realTo + realFrom - shouldFrom);
|
||
centerY += shouldTo - realTo + realFrom - shouldFrom;
|
||
}
|
||
if (step[0] == "up") {
|
||
var shouldTo = tw / 2,
|
||
realTo = toLoc[0] * scale * 2;
|
||
var shouldFrom = nw / 2,
|
||
realFrom = cx * scale * 2;
|
||
if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom + nh / 2, centerY - nh / 2, centerX + realFrom -
|
||
shouldFrom + interval * Math.SQRT2 / 4 + nh / 2, centerY - nh / 2 - interval * Math.SQRT2 / 4, "#ffffff", 2 * scale);
|
||
centerY -= nh / 2 + th / 2 + interval * Math.SQRT2 / 4;
|
||
centerX += shouldTo - realTo + realFrom - shouldFrom + (nh / 2 + th / 2 + interval * Math.SQRT2 / 4);
|
||
}
|
||
if (step[0] == "down") {
|
||
var shouldTo = tw / 2,
|
||
realTo = toLoc[0] * scale * 2;
|
||
var shouldFrom = nw / 2,
|
||
realFrom = cx * scale * 2;
|
||
if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
|
||
core.drawLine(ctx, centerX + realFrom - shouldFrom - nh / 2, centerY + nh / 2, centerX + realFrom -
|
||
shouldFrom - interval * Math.SQRT2 / 4 - nh / 2, centerY + nh / 2 + interval * Math.SQRT2 / 4, "#ffffff", 2 * scale);
|
||
centerY += nh / 2 + th / 2 + interval * Math.SQRT2 / 4;
|
||
centerX += shouldTo - realTo + realFrom - shouldFrom - (nh / 2 + th / 2 + interval * Math.SQRT2 / 4);
|
||
}
|
||
if (step[0] == "top") {
|
||
centerY -= deltaH;
|
||
}
|
||
if (step[0] == "bottom") {
|
||
centerY += deltaH;
|
||
}
|
||
if (everyLayer == nowLayer && step[0] != "top" && step[0] != "bottom") {
|
||
used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
|
||
used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
|
||
}
|
||
if (everyLayer != nowLayer) continue;
|
||
// 四侧最远位置
|
||
if (!flags.viewingLayer) {
|
||
if (left > centerX - tw / 2 - th / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - tw / 2 - th / 2;
|
||
if (right < centerX + tw / 2 + th / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + tw / 2 + th / 2;
|
||
if (down < centerY + th / 2 && nowLayer == (flags.viewingLayer || 0)) down = centerY + th / 2;
|
||
if (up > centerY - th / 2 && nowLayer == (flags.viewingLayer || 0)) up = centerY - th / 2;
|
||
}
|
||
// 不同高度层之间的连线
|
||
if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) {
|
||
core.setAlpha(ctx, opacity * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
|
||
if (step[0] == "top") {
|
||
core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#000000", 2 * scale);
|
||
used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
|
||
used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
|
||
}
|
||
}
|
||
if (!used[toFloor]) {
|
||
used[toFloor] = true;
|
||
// 设置不透明度
|
||
if (nowLayer == (flags.viewingLayer || 0)) {
|
||
core.setAlpha(ctx, opacity);
|
||
} else {
|
||
core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
|
||
}
|
||
// 画地图
|
||
var nodes = [
|
||
[centerX - tw / 2 - th / 2, centerY + th / 2], // 左下
|
||
[centerX + tw / 2 - th / 2, centerY + th / 2], // 右下
|
||
[centerX + tw / 2 + th / 2, centerY - th / 2], // 右上
|
||
[centerX - tw / 2 + th / 2, centerY - th / 2] // 左上
|
||
];
|
||
if (core.hasVisitedFloor(toFloor)) {
|
||
core.fillPolygon(ctx, nodes, "#000000");
|
||
} else {
|
||
core.fillPolygon(ctx, nodes, "#ff22ff");
|
||
}
|
||
core.strokePolygon(ctx, nodes, "#ffffff", 1.5 * scale);
|
||
}
|
||
// 不同高度层之间的连线
|
||
if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) {
|
||
if (step[0] == "bottom") {
|
||
core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#ffffff", 5 * scale);
|
||
core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#000000", 2 * scale);
|
||
used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
|
||
used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 自动缩放
|
||
if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) {
|
||
scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64));
|
||
var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, opacity: opacity, loop: loop, use3D: true };
|
||
return core.draw3DFlyMap(ctx, x, y, width, height, floorId, con);
|
||
}
|
||
// 大地图和楼层地图自适配定位
|
||
if (ctx == "mapOnUi")
|
||
core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2);
|
||
};
|
||
// 不同高度楼层排序
|
||
this.sortFloor = function (map) {
|
||
map = map || core.getFlyMap(null, true);
|
||
var totalLayer = 1,
|
||
topLayer = 0,
|
||
bottomLayer = 0,
|
||
nowLayer = 0;
|
||
// 拆分map
|
||
for (var i in map) {
|
||
var route = i.split(",");
|
||
nowLayer = 0;
|
||
for (var one in route) {
|
||
var step = route[one].split("_");
|
||
// 层数处理 并记录每一层的层数
|
||
if (step[0] == "top") {
|
||
nowLayer++;
|
||
map[i] = nowLayer + "_" + map[i];
|
||
if (nowLayer > topLayer) topLayer = nowLayer;
|
||
}
|
||
if (step[0] == "bottom") {
|
||
nowLayer--;
|
||
map[i] = nowLayer + "_" + map[i];
|
||
if (nowLayer < bottomLayer) bottomLayer = nowLayer;
|
||
}
|
||
}
|
||
}
|
||
// 总层数
|
||
totalLayer = topLayer - bottomLayer + 1;
|
||
// 按楼层高度由低到高排序
|
||
// 先变成数组
|
||
var mapArr = [];
|
||
for (var one in map) {
|
||
mapArr.push([one, parseInt(map[one]) || 0]);
|
||
}
|
||
// 再sort
|
||
mapArr.sort(function (a, b) { return a[1] - b[1]; });
|
||
return { map: mapArr, totalLayer: totalLayer, top: topLayer, bottom: bottomLayer };
|
||
};
|
||
// 由方向获得楼层坐标
|
||
this.getFloorByDirection = function (direction, floorId) {
|
||
floorId = floorId || core.status.floorId;
|
||
var route = core.getFlyMap(floorId);
|
||
for (var step in route) {
|
||
if (step.indexOf(direction) >= 0) {
|
||
return route[step];
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
////// 转换楼层结束的事件 检查小地图 //////
|
||
var originAfterChangeFloor = core.events.afterChangeFloor;
|
||
events.prototype.afterChangeFloor = function (floorId) {
|
||
if (!flags.__useMinimap__ || core.isReplaying()) {
|
||
core.deleteCanvas("minimap");
|
||
core.deleteCanvas("mapArrow");
|
||
core.unregisterAction("ondown", "closeMinimap");
|
||
core.unregisterAction("ondown", "openMinimap");
|
||
return originAfterChangeFloor.call(core.events, floorId);
|
||
}
|
||
if (main.mode != 'play') return;
|
||
this.eventdata.afterChangeFloor(floorId);
|
||
// 防止小地图出问题
|
||
core.unregisterAction("ondown", "closeMinimap");
|
||
core.unregisterAction("ondown", "openMinimap");
|
||
// 切换小地图
|
||
core.checkMinimap(true, true);
|
||
return;
|
||
};
|
||
////// 瞬间移动 检查小地图 //////
|
||
var originMoveDirectly = core.control.moveDirectly;
|
||
control.prototype.moveDirectly = function (destX, destY, ignoreSteps) {
|
||
if (!flags.__useMinimap__ || core.isReplaying())
|
||
return originMoveDirectly.call(core.control, destX, destY, ignoreSteps);
|
||
var canMoveDirectly = this.controldata.moveDirectly(destX, destY, ignoreSteps);
|
||
if (canMoveDirectly) core.checkMinimap();
|
||
return canMoveDirectly;
|
||
};
|
||
////// 每移动一格后执行的事件 检查小地图 //////
|
||
var originMoveOneStep = core.control.moveOneStep;
|
||
control.prototype.moveOneStep = function (callback) {
|
||
if (!flags.__useMinimap__ || core.isReplaying())
|
||
return originMoveOneStep.call(core.control, callback);
|
||
this.controldata.moveOneStep(callback);
|
||
core.checkMinimap();
|
||
};
|
||
// 检查小地图开闭情况 改变小地图位置
|
||
this.checkMinimap = function (fromUser, reDraw) {
|
||
if (!flags.__useMinimap__ || core.isReplaying()) {
|
||
core.unregisterAction("ondown", "closeMinimap");
|
||
core.unregisterAction("ondown", "openMinimap");
|
||
core.deleteCanvas("mapArrow");
|
||
core.deleteCanvas("minimap");
|
||
return;
|
||
}
|
||
reDraw = reDraw || false;
|
||
// 是否重绘
|
||
if (reDraw) {
|
||
if (flags.minimap) core.drawMinimap(flags.__onLeft__);
|
||
else core.drawClosedMap(flags.__onLeft__);
|
||
}
|
||
var hx = core.status.hero.loc.x;
|
||
var opened = flags.minimap;
|
||
var onLeft = hx >= Math.ceil(core.__SIZE__ / 3 * 2);
|
||
fromUser = fromUser || false; // 开关小地图相关
|
||
if (!flags.__onLeft__) flags.__onLeft__ = false;
|
||
// 如果地图上没有小地图 画到右边
|
||
if (!core.dymCanvas.mapArrow && !core.dymCanvas.minimap && !reDraw) {
|
||
if (flags.minimap) core.drawMinimap();
|
||
else core.drawClosedMap();
|
||
flags.__onLeft__ = false;
|
||
}
|
||
// 人物在中间 不执行
|
||
if (hx >= Math.ceil(core.__SIZE__ / 3 + core.bigmap.offsetX / 32) &&
|
||
hx <= Math.floor(core.__SIZE__ / 3 * 2 + core.bigmap.offsetX / 32)) return;
|
||
// 重定位画布 和 翻转
|
||
// 挪到右边
|
||
if (!onLeft && (flags.__onLeft__ || fromUser)) {
|
||
flags.__onLeft__ = false;
|
||
if (opened) {
|
||
core.relocateCanvas("minimap", core.__PIXELS__ - 120, 0);
|
||
core.relocateCanvas("mapArrow", core.__PIXELS__ - 140, 0);
|
||
document.getElementById('mapArrow').style.transform = 'none';
|
||
} else {
|
||
core.relocateCanvas("minimap", core.__PIXELS__, 0);
|
||
core.relocateCanvas("mapArrow", core.__PIXELS__ - 20, 0);
|
||
document.getElementById('mapArrow').style.transform = 'none';
|
||
}
|
||
}
|
||
// 挪到左边
|
||
if (onLeft && (!flags.__onLeft__ || fromUser)) {
|
||
flags.__onLeft__ = true;
|
||
if (opened) {
|
||
core.relocateCanvas("minimap", 0, 0);
|
||
core.relocateCanvas("mapArrow", 120, 0);
|
||
document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
|
||
} else {
|
||
core.relocateCanvas("minimap", -120, 0);
|
||
core.relocateCanvas("mapArrow", 0, 0);
|
||
document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
|
||
}
|
||
}
|
||
};
|
||
// 点击小地图的action
|
||
this.registerMinimapAction = function (open) {
|
||
if (!open) {
|
||
core.registerAction("ondown", "closeMinimap", function (x, y, px, py) {
|
||
if (!flags.__onLeft__) {
|
||
if (px >= core.__PIXELS__ - 140 && px <= core.__PIXELS__ - 120 &&
|
||
py >= 0 && py <= 120) {
|
||
core.closeMinimap();
|
||
core.unregisterAction("ondown", "closeMinimap");
|
||
return true;
|
||
}
|
||
if (px >= core.__PIXELS__ - 120 && py <= 120) {
|
||
core.playSound("打开界面");
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
} else {
|
||
if (px >= 120 && px <= 140 && py >= 0 && py <= 120) {
|
||
core.closeMinimap();
|
||
core.unregisterAction("ondown", "closeMinimap");
|
||
return true;
|
||
}
|
||
if (px <= 120 && py <= 120) {
|
||
core.playSound("打开界面");
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
}
|
||
}, 10);
|
||
} else {
|
||
core.registerAction("ondown", "openMinimap", function (x, y, px, py) {
|
||
if (!flags.__onLeft__) {
|
||
if (px >= core.__PIXELS__ - 20 && py <= 120) {
|
||
core.openMinimap();
|
||
core.unregisterAction("ondown", "openMinimap");
|
||
return true;
|
||
}
|
||
} else {
|
||
if (px <= 20 && py <= 120) {
|
||
core.openMinimap();
|
||
core.unregisterAction("ondown", "openMinimap");
|
||
return true;
|
||
}
|
||
}
|
||
}, 10);
|
||
}
|
||
};
|
||
// 地图上的小地图
|
||
this.drawMinimap = function (toLeft) {
|
||
if (!flags.__useMinimap__) {
|
||
core.deleteCanvas("mapArrow");
|
||
core.deleteCanvas("minimap");
|
||
return;
|
||
}
|
||
var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1);
|
||
if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale)
|
||
scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1);
|
||
// 绘制
|
||
core.createCanvas("minimap", core.__PIXELS__ - 120, 0, 120, 120, 100);
|
||
core.createCanvas("mapArrow", core.__PIXELS__ - 140, 0, 20, 120, 100);
|
||
if (toLeft) {
|
||
core.relocateCanvas("minimap", 0, 0);
|
||
core.relocateCanvas("mapArrow", 120, 0);
|
||
document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
|
||
}
|
||
core.clearMap("minimap");
|
||
core.clearMap("mapArrow");
|
||
// 黑色底
|
||
core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]);
|
||
var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true };
|
||
core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config);
|
||
// 向右箭头
|
||
core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]);
|
||
core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2);
|
||
core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2);
|
||
core.setTextAlign("mapArrow", "center");
|
||
core.fillText("mapArrow", ">", 10, 67, [100, 100, 100, 0.9], "20px Verdana");
|
||
core.registerMinimapAction(false);
|
||
};
|
||
// 关闭小地图
|
||
this.closeMinimap = function () {
|
||
if (!flags.__useMinimap__) {
|
||
core.deleteCanvas("mapArrow");
|
||
core.deleteCanvas("minimap");
|
||
return;
|
||
}
|
||
var onLeft = flags.__onLeft__;
|
||
var frame = 0;
|
||
var x = core.__PIXELS__,
|
||
a = 0.096,
|
||
speed = 4.8;
|
||
if (onLeft) {
|
||
x = 260;
|
||
a = -a;
|
||
speed = -speed;
|
||
}
|
||
var interval = setInterval(function () {
|
||
core.relocateCanvas("mapArrow", x - 140, 0);
|
||
if (!onLeft)
|
||
core.relocateCanvas("minimap", x - 120, 0);
|
||
else core.relocateCanvas("minimap", x - 260, 0);
|
||
speed -= a;
|
||
x += speed;
|
||
if (frame == 50) {
|
||
flags.minimap = false;
|
||
clearInterval(interval);
|
||
core.drawClosedMap(onLeft);
|
||
core.checkMinimap(true);
|
||
}
|
||
frame++;
|
||
}, 20);
|
||
};
|
||
// 合上的小地图
|
||
this.drawClosedMap = function (toLeft) {
|
||
if (!flags.__useMinimap__) {
|
||
core.deleteCanvas("mapArrow");
|
||
core.deleteCanvas("minimap");
|
||
return;
|
||
}
|
||
var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1);
|
||
if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale)
|
||
scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1);
|
||
// 绘制
|
||
core.createCanvas("minimap", core.__PIXELS__, 0, 120, 120, 100);
|
||
core.createCanvas("mapArrow", core.__PIXELS__ - 20, 0, 20, 120, 100);
|
||
core.clearMap("minimap");
|
||
core.clearMap("mapArrow");
|
||
if (toLeft) {
|
||
core.relocateCanvas("minimap", -120, 0);
|
||
core.relocateCanvas("mapArrow", 0, 0);
|
||
document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
|
||
}
|
||
// 黑色底
|
||
core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]);
|
||
var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true };
|
||
core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config);
|
||
// 向左箭头
|
||
core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]);
|
||
core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2);
|
||
core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2);
|
||
core.setTextAlign("mapArrow", "center");
|
||
core.fillText("mapArrow", "<", 10, 67, [100, 100, 100, 0.9], "20px Verdana");
|
||
core.registerMinimapAction(true);
|
||
};
|
||
// 打开小地图
|
||
this.openMinimap = function () {
|
||
if (!flags.__useMinimap__) {
|
||
core.deleteCanvas("mapArrow");
|
||
core.deleteCanvas("minimap");
|
||
return;
|
||
}
|
||
var onLeft = flags.__onLeft__;
|
||
var frame = 0;
|
||
var x = 120 + core.__PIXELS__,
|
||
a = 0.096,
|
||
speed = 4.8;
|
||
if (onLeft) {
|
||
x = 140;
|
||
a = -a;
|
||
speed = -speed;
|
||
}
|
||
var interval = setInterval(function () {
|
||
core.relocateCanvas("mapArrow", x - 140, 0);
|
||
if (!flags.__onLeft__)
|
||
core.relocateCanvas("minimap", x - 120, 0);
|
||
else core.relocateCanvas("minimap", x - 260, 0);
|
||
speed -= a;
|
||
x -= speed;
|
||
if (frame == 50) {
|
||
flags.minimap = true;
|
||
clearInterval(interval);
|
||
core.drawMinimap(onLeft);
|
||
core.checkMinimap(true);
|
||
}
|
||
frame++;
|
||
}, 20);
|
||
};
|
||
// 大地图
|
||
this.drawTotalMap = function (floorId) {
|
||
floorId = floorId || core.status.floorId;
|
||
core.status.event.id = "totalMap";
|
||
core.lockControl();
|
||
if (!flags.viewingLayer) flags.viewingLayer = 0;
|
||
var loop = 5;
|
||
if (flags.worldMap) loop = core.floorIds.length;
|
||
// 大地图时点击和键盘操作
|
||
core.registerAction("ondown", "onDownTmap", function (x, y) {
|
||
if (core.status.event.id == "totalMap") {
|
||
if (y < 1 && x <= Math.floor(core.__SIZE__ / 2) - 2) { // 上移一层
|
||
if (flags.viewingLayer < core.sortFloor().top) {
|
||
flags.viewingLayer++;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1) { // 下移一层
|
||
if (flags.viewingLayer > core.sortFloor().bottom) {
|
||
flags.viewingLayer--;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
if (y < 1 && x < Math.ceil(core.__SIZE__ / 2) + 1 && x > Math.floor(core.__SIZE__ / 2) - 2) {
|
||
// 区域地图
|
||
if (flags.worldMap) {
|
||
flags.worldMap = false;
|
||
flags.viewingLayer = 0;
|
||
} else flags.worldMap = true;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
if (y >= core.__SIZE__ - 1 && x <= Math.floor(core.__SIZE__ / 2)) { // 3D
|
||
if (core.can3D(floorId) && !flags.in3D) flags.use3D = true;
|
||
if (flags.in3D) flags.use3D = false;
|
||
flags.mapHint = false;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
if (y >= core.__SIZE__ - 1 && x >= Math.ceil(core.__SIZE__ / 2)) { // hint
|
||
if (flags.in3D) {
|
||
if (!flags.mapHint) flags.mapHint = true;
|
||
else flags.mapHint = false;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
flags.viewingLayer = 0;
|
||
core.playSound("取消")
|
||
core.deleteCanvas("mapOnUi");
|
||
core.deleteCanvas("back");
|
||
core.deleteCanvas("tips");
|
||
core.closePanel();
|
||
core.unregisterAction("ondown", "onDownTmap");
|
||
return true;
|
||
}
|
||
}, 110);
|
||
core.registerAction("keyUp", "keyUpTmap", function (keycode) {
|
||
if (core.status.event.id == "totalMap") {
|
||
if (keycode == 33) { // PgUp
|
||
if (flags.viewingLayer < core.sortFloor().top) {
|
||
flags.viewingLayer++;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
if (keycode == 34) { // PgDn
|
||
if (flags.viewingLayer > core.sortFloor().bottom) {
|
||
flags.viewingLayer--;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
if (keycode == 90) { // Z
|
||
if (core.can3D(floorId) && !flags.in3D) flags.use3D = true;
|
||
if (flags.in3D) flags.use3D = false;
|
||
flags.mapHint = false;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
if (keycode == 84) { // T
|
||
if (flags.in3D) {
|
||
if (!flags.mapHint) flags.mapHint = true;
|
||
else flags.mapHint = false;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
}
|
||
return true;
|
||
}
|
||
if (keycode == 87) { // W
|
||
if (flags.worldMap) {
|
||
flags.worldMap = false;
|
||
flags.viewingLayer = 0;
|
||
} else flags.worldMap = true;
|
||
core.playSound('光标移动');
|
||
core.drawTotalMap();
|
||
return true;
|
||
}
|
||
flags.viewingLayer = 0;
|
||
core.playSound("取消")
|
||
core.deleteCanvas("mapOnUi");
|
||
core.deleteCanvas("back");
|
||
core.deleteCanvas("tips");
|
||
core.closePanel();
|
||
core.unregisterAction("keyUp", "keyUpTmap");
|
||
return true;
|
||
}
|
||
}, 110);
|
||
// 开始画
|
||
core.createCanvas("mapOnUi", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 150);
|
||
core.createCanvas("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 140);
|
||
core.createCanvas("tips", 0, 0, core.__PIXELS__, core.__PIXELS__, 160);
|
||
var ctx = document.getElementById("tips").getContext("2d");
|
||
ctx.shadowOffsetX = 0;
|
||
ctx.shadowOffsetY = 0;
|
||
ctx.shadowBlur = 5;
|
||
ctx.shadowColor = "rgba(100, 100, 255, 1)";
|
||
core.fillRect("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, [0, 0, 0, 0.9]);
|
||
core.fillRect("tips", 0, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]);
|
||
core.fillRect("tips", core.__PIXELS__ / 2 + 50, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]);
|
||
core.fillRect("tips", core.__PIXELS__ / 2 - 46, 0, 92, 32, [200, 200, 200, 0.8]);
|
||
core.drawLine("tips", 0, 32, core.__PIXELS__ / 2 - 50, 32, [50, 50, 50, 0.8], 3);
|
||
core.drawLine("tips", core.__PIXELS__ / 2 + 50, 32, core.__PIXELS__, 32, [50, 50, 50, 0.8], 3);
|
||
core.drawLine("tips", core.__PIXELS__ / 2 - 46, 32, core.__PIXELS__ / 2 + 46, 32, [50, 50, 50, 0.8], 3);
|
||
core.fillRect("tips", 0, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]);
|
||
core.fillRect("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]);
|
||
core.drawLine("tips", 0, core.__PIXELS__ - 32, core.__PIXELS__ / 2 - 5, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3);
|
||
core.drawLine("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__ - 32, core.__PIXELS__, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3);
|
||
core.setTextAlign("tips", "center");
|
||
core.fillText("tips", "上移一层", core.__PIXELS__ / 4 - 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
core.fillText("tips", "下移一层", core.__PIXELS__ / 4 * 3 + 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
core.fillText("tips", flags.worldMap ? "小地图" : "区域地图", core.__PIXELS__ / 2, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
core.drawFlyMap("mapOnUi", 240, 240, core.__PIXELS__, core.__PIXELS__,
|
||
floorId, { fromUser: true, opacity: 1, oriFloor: floorId, noErase: true, use3D: flags.use3D, layer: flags.viewingLayer, loop: loop, clearCache: true });
|
||
if (flags.in3D)
|
||
core.fillText("tips", "参考线(T)", core.__PIXELS__ / 4 * 3, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
if (core.can3D(floorId) && !flags.in3D)
|
||
core.fillText("tips", "3D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
if (flags.in3D)
|
||
core.fillText("tips", "2D模式(Z)", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
|
||
if (flags.mapHint) {
|
||
core.drawLine("back", 240, 240 + core.__PIXELS__, 240 + core.__PIXELS__, 240, [100, 100, 240, 0.4], 2);
|
||
core.drawLine("back", 240 + core.__PIXELS__ / 2, 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, [100, 100, 240, 0.4], 2);
|
||
core.drawLine("back", 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, 240 + core.__PIXELS__ / 2, [100, 100, 240, 0.4], 2);
|
||
}
|
||
};
|
||
}
|
||
} |