// @ts-check
/// <reference path="../runtime.d.ts" />
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = 
{
    "init": function () {

		console.log("插件编写测试");

		// 可以写一些直接执行的代码
		// 在这里写的代码将会在【资源加载前】被执行，此时图片等资源尚未被加载。
		// 请勿在这里对包括bgm，图片等资源进行操作。

		core.plugin._afterLoadResources = function () {
			// 本函数将在所有资源加载完毕后，游戏开启前被执行
			// 可以在这个函数里面对资源进行一些操作。
			// 若需要进行切分图片，可以使用 core.splitImage() 函数，或直接在全塔属性-图片切分中操作
		}

		// 可以在任何地方（如afterXXX或自定义脚本事件）调用函数，方法为 core.plugin.xxx();
		// 从V2.6开始，插件中用this.XXX方式定义的函数也会被转发到core中，详见文档-脚本-函数的转发。

		// 贪婪加成显示在手册金币行
		var _origDrawRow2 = core.ui._drawBook_drawRow2.bind(core.ui);
		core.ui._drawBook_drawRow2 = function (index, enemy, top, left, width, position) {
			var greedyLv = core.getFlag('__talent_greedy__', 0);
			if (greedyLv > 0) {
				var enemyWithGreedy = Object.assign({}, enemy, { money: (enemy.money || 0) + greedyLv });
				return _origDrawRow2(index, enemyWithGreedy, top, left, width, position);
			}
			return _origDrawRow2(index, enemy, top, left, width, position);
		};
	},
    "shop": function () {
		// 【全局商店】相关的功能
		/**
		 * 打开一个全局商店
		 * @param {string} shopId 
		 * @param {boolean} noRoute 
		 */
		core.plugin.openShop = function (shopId, noRoute) {
			const shop = core.status.shops[shopId];
			// Step 1: 检查能否打开此商店
			if (!this.canOpenShop(shopId)) {
				core.drawTip("该商店尚未开启");
				return false;
			}

			// Step 2: （如有必要）记录打开商店的脚本事件
			if (!noRoute) {
				core.status.route.push("shop:" + shopId);
			}

			// Step 3: 检查道具商店 or 公共事件
			if (shop.item) {
				if (core.openItemShop) {
					core.openItemShop(shopId);
				} else {
					core.playSound('操作失败');
					core.insertAction("道具商店插件不存在！请检查是否存在该插件！");
				}
				return;
			}
			if (shop.commonEvent) {
				core.insertCommonEvent(shop.commonEvent, shop.args);
				return;
			}

			_shouldProcessKeyUp = true;

			// Step 4: 执行标准公共商店    
			core.insertAction(this._convertShop(shop));
			return true;
		}

		/** 
		 * @param {shop} shop
		 * 将一个全局商店转变成可预览的公共事件 
		 */
		core.plugin._convertShop = function (shop) {
			return [
				{ "type": "function", "function": "function() {core.addFlag('@temp@shop', 1);}" },
				{
					"type": "while",
					"condition": "true",
					"data": [
						// 检测能否访问该商店
						{
							"type": "if",
							"condition": "core.isShopVisited('" + shop.id + "')",
							"true": [
								// 可以访问，直接插入执行效果
								{ "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', false) }" },
							],
							"false": [
								// 不能访问的情况下：检测能否预览
								{
									"type": "if",
									"condition": shop.disablePreview,
									"true": [
										// 不可预览，提示并退出
										{ "type": "playSound", "name": "操作失败" },
										"当前无法访问该商店！",
										{ "type": "break" },
									],
									"false": [
										// 可以预览：将商店全部内容进行替换
										{ "type": "tip", "text": "当前处于预览模式，不可购买" },
										{ "type": "function", "function": "function() { core.plugin._convertShop_replaceChoices('" + shop.id + "', true) }" },
									]
								}
							]
						}
					]
				},
				{ "type": "function", "function": "function() {core.addFlag('@temp@shop', -1);}" }
			];
		}

		/**
		 * @param {string} shopId 
		 * @param {boolean} previewMode 
		 */
		core.plugin._convertShop_replaceChoices = function (shopId, previewMode) {
			var shop = core.status.shops[shopId];
			var choices = (shop.choices || []).filter(function (choice) {
				if (choice.condition == null || choice.condition == '') return true;
				try { return core.calValue(choice.condition); } catch (e) { return true; }
			}).map(function (choice) {
				var ableToBuy = core.calValue(choice.need);
				return {
					"text": choice.text,
					"icon": choice.icon,
					"color": ableToBuy && !previewMode ? choice.color : [153, 153, 153, 1],
					"action": ableToBuy && !previewMode ? [{ "type": "playSound", "name": "商店" }].concat(choice.action) : [
						{ "type": "playSound", "name": "操作失败" },
						{ "type": "tip", "text": previewMode ? "预览模式下不可购买" : "购买条件不足" }
					]
				};
			}).concat({ "text": "离开", "action": [{ "type": "playSound", "name": "取消" }, { "type": "break" }] });
			core.insertAction({ "type": "choices", "text": shop.text, "choices": choices });
		}

		/**
		 * 是否访问过某个快捷商店
		 */
		core.plugin.isShopVisited = function (id) {
			if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {});
			const shops = core.getFlag("__shops__");
			if (!shops[id]) shops[id] = {};
			return shops[id].visited;
		}

		/// 当前应当显示的快捷商店列表
		core.plugin.listShopIds = function () {
			return Object.keys(core.status.shops).filter(function (id) {
				return core.isShopVisited(id) || !core.status.shops[id].mustEnable;
			});
		}

		/** 是否能够打开某个商店 **/
		core.plugin.canOpenShop = function (id) {
			if (this.isShopVisited(id)) return true;
			const shop = core.status.shops[id];
			if (shop.item || shop.commonEvent || shop.mustEnable) return false;
			return true;
		}

		/** 启用或禁用某个快捷商店 */
		core.plugin.setShopVisited = function (id, visited) {
			if (!core.hasFlag("__shops__")) core.setFlag("__shops__", {});
			var shops = core.getFlag("__shops__");
			if (!shops[id]) shops[id] = {};
			if (visited) shops[id].visited = true;
			else delete shops[id].visited;
		}

		/**
		 * 能否使用快捷商店
		 * @param {string} id 
		 * @returns {'当前楼层不能使用快捷商店。'|null}
		 */
		core.plugin.canUseQuickShop = function (id) {
			// 如果返回一个字符串，表示不能，字符串为不能使用的提示
			// 返回null代表可以使用

			// 检查当前楼层的canUseQuickShop选项是否为false
			if (core.status.thisMap.canUseQuickShop === false)
				return '当前楼层不能使用快捷商店。';
			return null;
		}

		let _shouldProcessKeyUp = true;

		/// 允许商店X键退出
		core.registerAction('keyUp', 'shops', function (keycode) {
			if (!core.status.lockControl || core.status.event.id != 'action') return false;
			if ((keycode == 13 || keycode == 32) && !_shouldProcessKeyUp) {
				_shouldProcessKeyUp = true;
				return true;
			}

			if (!core.hasFlag("@temp@shop") || core.status.event.data.type != 'choices') return false;
			var data = core.status.event.data.current;
			var choices = data.choices;
			var topIndex = core.actions._getChoicesTopIndex(choices.length);
			if (keycode == 88 || keycode == 27) { // X, ESC
				core.actions._clickAction(core.actions.HSIZE, topIndex + choices.length - 1);
				return true;
			}
			return false;
		}, 60);

		/// 允许长按空格或回车连续执行操作
		core.registerAction('keyDown', 'shops', function (keycode) {
			if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false;
			if (core.status.event.data.type != 'choices') return false;
			core.status.onShopLongDown = true;
			var data = core.status.event.data.current;
			var choices = data.choices;
			var topIndex = core.actions._getChoicesTopIndex(choices.length);
			if (keycode == 13 || keycode == 32) { // Space, Enter
				core.actions._clickAction(core.actions.HSIZE, topIndex + core.status.event.selection);
				_shouldProcessKeyUp = false;
				return true;
			}
			return false;
		}, 60);

		// 允许长按屏幕连续执行操作
		core.registerAction('longClick', 'shops', function (x, y, px, py) {
			if (!core.status.lockControl || !core.hasFlag("@temp@shop") || core.status.event.id != 'action') return false;
			if (core.status.event.data.type != 'choices') return false;
			var data = core.status.event.data.current;
			var choices = data.choices;
			var topIndex = core.actions._getChoicesTopIndex(choices.length);
			if (x >= core.actions.CHOICES_LEFT && x <= core.actions.CHOICES_RIGHT && y >= topIndex && y < topIndex + choices.length) {
				core.actions._clickAction(x, y);
				return true;
			}
			return false;
		}, 60);
	},
    "removeMap": function () {
		// 高层塔砍层插件，删除后不会存入存档，不可浏览地图也不可飞到。
		// 推荐用法：
		// 对于超高层或分区域塔，当在1区时将2区以后的地图删除；1区结束时恢复2区，进二区时删除1区地图，以此类推
		// 这样可以大幅减少存档空间，以及加快存读档速度

		/**
		 * 删除楼层
		 * @example // 删除MT1~MT300之间的全部层
		 * core.removeMaps("MT1", "MT300") 
		 * @example // 只删除MT10层
		 * core.removeMaps("MT10")
		 * @param {string} fromId 
		 * @param {string} toId 
		 */
		function removeMaps(fromId, toId) {
			toId = toId || fromId;
			var fromIndex = core.floorIds.indexOf(fromId),
				toIndex = core.floorIds.indexOf(toId);
			if (toIndex < 0) toIndex = core.floorIds.length - 1;
			flags.__visited__ = flags.__visited__ || {};
			flags.__removed__ = flags.__removed__ || [];
			flags.__disabled__ = flags.__disabled__ || {};
			flags.__leaveLoc__ = flags.__leaveLoc__ || {};
			for (var i = fromIndex; i <= toIndex; ++i) {
				var floorId = core.floorIds[i];
				if (core.status.maps[floorId].deleted) continue;
				delete flags.__visited__[floorId];
				flags.__removed__.push(floorId);
				delete flags.__disabled__[floorId];
				delete flags.__leaveLoc__[floorId];
				(core.status.autoEvents || []).forEach(function (event) {
					if (event.floorId == floorId && event.currentFloor) {
						core.autoEventExecuting(event.symbol, false);
						core.autoEventExecuted(event.symbol, false);
					}
				});
				core.status.maps[floorId].deleted = true;
				core.status.maps[floorId].canFlyTo = false;
				core.status.maps[floorId].canFlyFrom = false;
				core.status.maps[floorId].cannotViewMap = true;
			}
		}
		core.plugin.removeMaps = removeMaps;

		/**
		 * 恢复楼层
		 * @example // 恢复MT1~MT300之间的全部层
		 * core.resumeMaps("MT1", "MT300")
		 * @example // 只恢复MT10层
		 * core.resumeMaps("MT10")
		 * @param {string} fromId 
		 * @param {string} toId 
		 */
		function resumeMaps(fromId, toId) {
			toId = toId || fromId;
			var fromIndex = core.floorIds.indexOf(fromId),
				toIndex = core.floorIds.indexOf(toId);
			if (toIndex < 0) toIndex = core.floorIds.length - 1;
			flags.__removed__ = flags.__removed__ || [];
			for (var i = fromIndex; i <= toIndex; ++i) {
				var floorId = core.floorIds[i];
				if (!core.status.maps[floorId].deleted) continue;
				flags.__removed__ = flags.__removed__.filter(function (f) { return f != floorId; });
				core.status.maps[floorId] = core.loadFloor(floorId);
			}
		}
		core.plugin.resumeMaps = resumeMaps;

		/** 分区砍层相关
		 * @param {string} floorId
		 */
		const inAnyPartition = function (floorId) {
			let inPartition = false;
			(core.floorPartitions || []).forEach(function (floor) {
				const fromIndex = core.floorIds.indexOf(floor[0]);
				let toIndex = core.floorIds.indexOf(floor[1]);
				const index = core.floorIds.indexOf(floorId);
				if (fromIndex < 0 || index < 0) return;
				if (toIndex < 0) toIndex = core.floorIds.length - 1;
				if (index >= fromIndex && index <= toIndex) inPartition = true;
			});
			return inPartition;
		}

		/**
		 * 分区砍层
		 * @param {string} floorId
		 **/
		function autoRemoveMaps(floorId) {
			if (main.mode != 'play' || !inAnyPartition(floorId)) return;
			// 根据分区信息自动砍层与恢复
			(core.floorPartitions || []).forEach(function (floor) {
				var fromIndex = core.floorIds.indexOf(floor[0]);
				var toIndex = core.floorIds.indexOf(floor[1]);
				var index = core.floorIds.indexOf(floorId);
				if (fromIndex < 0 || index < 0) return;
				if (toIndex < 0) toIndex = core.floorIds.length - 1;
				if (index >= fromIndex && index <= toIndex) {
					core.resumeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]);
				} else {
					core.removeMaps(core.floorIds[fromIndex], core.floorIds[toIndex]);
				}
			});
		}
		core.plugin.autoRemoveMaps = autoRemoveMaps;
	},
    "fiveLayers": function () {
		// 是否启用五图层（增加背景2层和前景2层） 将__enable置为true即会启用；启用后请保存后刷新编辑器
		// 背景层2将会覆盖背景层 被事件层覆盖 前景层2将会覆盖前景层
		// 另外 请注意加入两个新图层 会让大地图的性能降低一些
		// 插件作者：ad
		var __enable = false;
		if (!__enable) return;

		// 创建新图层
		function createCanvas(name, zIndex) {
			if (!name) return;
			const canvas = document.createElement('canvas');
			canvas.id = name;
			canvas.className = 'gameCanvas';
			// 编辑器模式下设置zIndex会导致加入的图层覆盖优先级过高
			if (main.mode != "editor") canvas.style.zIndex = zIndex || 0;
			// 将图层插入进游戏内容
			document.getElementById('gameDraw')?.appendChild(canvas);
			const ctx = canvas.getContext('2d');
			if (ctx) core.canvas[name] = ctx;
			canvas.width = core.__PIXELS__;
			canvas.height = core.__PIXELS__;
			return canvas;
		}

		const bg2Canvas = createCanvas('bg2', 20);
		const fg2Canvas = createCanvas('fg2', 63);
		// 大地图适配
		core.bigmap.canvas = ["bg2", "fg2", "bg", "event", "event2", "fg", "damage"];
		core.initStatus.bg2maps = {};
		core.initStatus.fg2maps = {};

		if (main.mode == 'editor') {
			/*插入编辑器的图层 不做此步新增图层无法在编辑器显示*/
			// 编辑器图层覆盖优先级 eui > efg > fg(前景层) > event2(48*32图块的事件层) > event(事件层) > bg(背景层)
			// 背景层2(bg2) 插入事件层(event)之前(即bg与event之间)
			if (bg2Canvas) document.getElementById('mapEdit')?.insertBefore(bg2Canvas, document.getElementById('event'));
			// 前景层2(fg2) 插入编辑器前景(efg)之前(即fg之后)
			if (fg2Canvas) document.getElementById('mapEdit')?.insertBefore(fg2Canvas, document.getElementById('ebm'));
			// 原本有三个图层 从4开始添加
			var num = 4;
			// 新增图层存入editor.dom中
			editor.dom.bg2c = core.canvas.bg2.canvas;
			editor.dom.bg2Ctx = core.canvas.bg2;
			editor.dom.fg2c = core.canvas.fg2.canvas;
			editor.dom.fg2Ctx = core.canvas.fg2;
			editor.dom.maps.push('bg2map', 'fg2map');
			editor.dom.canvas.push('bg2', 'fg2');

			// 创建编辑器上的按钮
			const createCanvasBtn = function (name) {
				// 电脑端创建按钮
				const input = document.createElement('input');
				// layerMod4/layerMod5
				const id = 'layerMod' + num++;
				// bg2map/fg2map
				const value = name + 'map';
				input.type = 'radio';
				input.name = 'layerMod';
				input.id = id;
				input.value = value;
				editor.dom[id] = input;
				input.onchange = function () {
					editor.uifunctions.setLayerMod(value);
				}
				return input;
			};

			const createCanvasBtn_mobile = function (name) {
				// 手机端往选择列表中添加子选项
				const input = document.createElement('option');
				const id = 'layerMod' + num++;
				const value = name + 'map';
				input.name = 'layerMod';
				input.value = value;
				editor.dom[id] = input;
				return input;
			};
			if (!editor.isMobile) {
				const input = createCanvasBtn('bg2');
				const input2 = createCanvasBtn('fg2');
				// 获取事件层及其父节点
				const child = document.getElementById('layerMod'),
					parent = child?.parentNode;
				if (parent) {
					// 背景层2插入事件层前
					parent.insertBefore(input, child);
					// 不能直接更改背景层2的innerText 所以创建文本节点
					var txt = document.createTextNode('bg2');
					// 插入事件层前(即新插入的背景层2前)
					parent.insertBefore(txt, child);
					// 向最后插入前景层2(即插入前景层后)
					parent.appendChild(input2);
					var txt2 = document.createTextNode('fg2');
					parent.appendChild(txt2);
					parent.childNodes[2].replaceWith("bg");
					parent.childNodes[6].replaceWith("事件");
					parent.childNodes[8].replaceWith("fg");
				}
			} else {
				const input = createCanvasBtn_mobile('bg2');
				const input2 = createCanvasBtn_mobile('fg2');
				// 手机端因为是选项 所以可以直接改innerText
				input.innerText = '背景层2';
				input2.innerText = '前景层2';
				const parent = document.getElementById('layerMod');
				if (parent) {
					parent.insertBefore(input, parent.children[1]);
					parent.appendChild(input2);
				}
			}
		}

		var _loadFloor_doNotCopy = core.maps._loadFloor_doNotCopy;
		core.maps._loadFloor_doNotCopy = function () {
			return ["bg2map", "fg2map"].concat(_loadFloor_doNotCopy());
		}
		////// 绘制背景和前景层 //////
		core.maps._drawBg_draw = function (floorId, toDrawCtx, cacheCtx, config) {
			config.ctx = cacheCtx;
			core.maps._drawBg_drawBackground(floorId, config);
			// ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制背景图块；后绘制的覆盖先绘制的。
			core.maps._drawFloorImages(floorId, config.ctx, 'bg', null, null, config.onMap);
			core.maps._drawBgFgMap(floorId, 'bg', config);
			if (config.onMap) {
				core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
				core.clearMap('bg2');
				core.clearMap(cacheCtx);
			}
			core.maps._drawBgFgMap(floorId, 'bg2', config);
			if (config.onMap) core.drawImage('bg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
			config.ctx = toDrawCtx;
		}
		core.maps._drawFg_draw = function (floorId, toDrawCtx, cacheCtx, config) {
			config.ctx = cacheCtx;
			// ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制前景图块；后绘制的覆盖先绘制的。
			core.maps._drawFloorImages(floorId, config.ctx, 'fg', null, null, config.onMap);
			core.maps._drawBgFgMap(floorId, 'fg', config);
			if (config.onMap) {
				core.drawImage(toDrawCtx, cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
				core.clearMap('fg2');
				core.clearMap(cacheCtx);
			}
			core.maps._drawBgFgMap(floorId, 'fg2', config);
			if (config.onMap) core.drawImage('fg2', cacheCtx.canvas, core.bigmap.v2 ? -32 : 0, core.bigmap.v2 ? -32 : 0);
			config.ctx = toDrawCtx;
		}
		////// 移动判定 //////
		core.maps._generateMovableArray_arrays = function (floorId) {
			return {
				bgArray: this.getBgMapArray(floorId),
				fgArray: this.getFgMapArray(floorId),
				eventArray: this.getMapArray(floorId),
				bg2Array: this._getBgFgMapArray('bg2', floorId),
				fg2Array: this._getBgFgMapArray('fg2', floorId)
			};
		}

		// @todo 测试五图层插件在此处是否表现正常
		// @todo 五图层配合autotile是否有bug 待测试
		// 楼层贴图绘制
		core.maps._drawFloorImage = function (ctx, name, one, image, currStatus, onMap) {
			var height = image.height;
			var imageName = one.name + (one.reverse || '');
			var width = parseInt((one.w == null ? image.width : one.w) / (one.frame || 1));
			var height = one.h == null ? image.height : one.h;
			var sx = (one.sx || 0) + (currStatus || 0) % (one.frame || 1) * width;
			var sy = one.sy || 0;
			var x = one.x || 0, y = one.y || 0;
			if (onMap && core.bigmap.v2) {
				if (x > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || x + width < 32 * core.bigmap.posX - 32 ||
					y > 32 * core.bigmap.posX + core.__PIXELS__ + 32 || y + height < 32 * core.bigmap.posY - 32) {
					return;
				}
				x -= 32 * core.bigmap.posX;
				y -= 32 * core.bigmap.posY;
			}

			if (one.canvas != 'auto' && one.canvas != name) return;
			if (one.canvas != 'auto') {
				if (currStatus != null) core.clearMap(ctx, x, y, width, height);
				core.drawImage(ctx, imageName, sx, sy, width, height, x, y, width, height);
			} else {
				if (name === 'bg' || name === 'bg2') {
					if (currStatus != null) core.clearMap(ctx, x, y + height - 32, width, 32);
					core.drawImage(ctx, imageName, sx, sy + height - 32, width, 32, x, y + height - 32, width, 32);
				} else if (name == 'fg' || name === 'fg2') {
					if (currStatus != null) core.clearMap(ctx, x, y, width, height - 32);
					core.drawImage(ctx, imageName, sx, sy, width, height - 32, x, y, width, height - 32);
				}
			}
		}
	},
    "itemShop": function () {
		// 道具商店相关的插件
		// 可在全塔属性-全局商店中使用「道具商店」事件块进行编辑（如果找不到可以在入口方块中找）

		var shopId = null; // 当前商店ID
		var type = 0; // 当前正在选中的类型，0买入1卖出
		/** 当前正在选中的道具
		 * @type {number | null} 
		 */
		var selectItem = 0;
		var selectCount = 0; // 当前已经选中的数量
		var page = 0;
		var totalPage = 0;
		var totalMoney = 0;
		var list = [];
		var shopInfo = null; // 商店信息
		var choices = []; // 商店选项
		var use = 'money';
		var useText = '金币';

		var bigFont = core.ui._buildFont(20, false),
			middleFont = core.ui._buildFont(18, false);

		this._drawItemShop = function () {
			// 绘制道具商店

			// Step 1: 背景和固定的几个文字
			core.ui._createUIEvent();
			core.clearMap('uievent');
			core.ui.clearUIEventSelector();
			core.setTextAlign('uievent', 'left');
			core.setTextBaseline('uievent', 'top');
			core.fillRect('uievent', 0, 0, 416, 416, 'black');
			core.drawWindowSkin('winskin.png', 'uievent', 0, 0, 416, 56);
			core.drawWindowSkin('winskin.png', 'uievent', 0, 56, 312, 56);
			core.drawWindowSkin('winskin.png', 'uievent', 0, 112, 312, 304);
			core.drawWindowSkin('winskin.png', 'uievent', 312, 56, 104, 56);
			core.drawWindowSkin('winskin.png', 'uievent', 312, 112, 104, 304);
			core.setFillStyle('uievent', 'white');
			core.setStrokeStyle('uievent', 'white');
			core.fillText("uievent", "购买", 32, 74, 'white', bigFont);
			core.fillText("uievent", "卖出", 132, 74);
			core.fillText("uievent", "离开", 232, 74);
			core.fillText("uievent", "当前" + useText, 324, 66, null, middleFont);
			core.setTextAlign("uievent", "right");
			core.fillText("uievent", core.formatBigNumber(core.status.hero[use]), 405, 89);
			core.setTextAlign("uievent", "left");
			core.ui.drawUIEventSelector(1, "winskin.png", 22 + 100 * type, 66, 60, 33);
			if (selectItem != null) {
				core.setTextAlign('uievent', 'center');
				core.fillText("uievent", type == 0 ? "买入个数" : "卖出个数", 364, 320, null, bigFont);
				core.fillText("uievent", "<   " + selectCount + "   >", 364, 350);
				core.fillText("uievent", "确定", 364, 380);
			}

			// Step 2：获得列表并展示
			list = choices.filter(function (one) {
				if (one.condition != null && one.condition != '') {
					try { if (!core.calValue(one.condition)) return false; } catch (e) { }
				}
				return (type == 0 && one.money != null) || (type == 1 && one.sell != null);
			});
			var per_page = 6;
			totalPage = Math.ceil(list.length / per_page);
			page = Math.floor((selectItem || 0) / per_page) + 1;

			// 绘制分页
			if (totalPage > 1) {
				var half = 156;
				core.setTextAlign('uievent', 'center');
				core.fillText('uievent', page + " / " + totalPage, half, 388, null, middleFont);
				if (page > 1) core.fillText('uievent', '上一页', half - 80, 388);
				if (page < totalPage) core.fillText('uievent', '下一页', half + 80, 388);
			}
			core.setTextAlign('uievent', 'left');

			// 绘制每一项
			var start = (page - 1) * per_page;
			for (var i = 0; i < per_page; ++i) {
				var curr = start + i;
				if (curr >= list.length) break;
				var item = list[curr];
				core.drawIcon('uievent', item.id, 10, 125 + i * 40);
				core.setTextAlign('uievent', 'left');
				core.fillText('uievent', core.material.items[item.id].name, 50, 132 + i * 40, null, bigFont);
				core.setTextAlign('uievent', 'right');
				core.fillText('uievent', (type == 0 ? core.calValue(item.money) : core.calValue(item.sell)) + useText + "/个", 300, 133 + i * 40, null, middleFont);
				core.setTextAlign("uievent", "left");
				if (curr == selectItem) {
					// 绘制描述，文字自动放缩
					var text = core.material.items[item.id].text || "该道具暂无描述";
					try { text = core.replaceText(text); } catch (e) { }
					for (var fontSize = 20; fontSize >= 8; fontSize -= 2) {
						var config = { left: 10, fontSize: fontSize, maxWidth: 403 };
						var height = core.getTextContentHeight(text, config);
						if (height <= 50) {
							config.top = (56 - height) / 2;
							core.drawTextContent("uievent", text, config);
							break;
						}
					}
					core.ui.drawUIEventSelector(2, "winskin.png", 8, 120 + i * 40, 295, 40);
					if (type == 0 && item.number != null) {
						core.fillText("uievent", "存货", 324, 132, null, bigFont);
						core.setTextAlign("uievent", "right");
						core.fillText("uievent", item.number, 406, 132, null, null, 40);
					} else if (type == 1) {
						core.fillText("uievent", "数量", 324, 132, null, bigFont);
						core.setTextAlign("uievent", "right");
						core.fillText("uievent", core.itemCount(item.id), 406, 132, null, null, 40);
					}
					core.setTextAlign("uievent", "left");
					core.fillText("uievent", "预计" + useText, 324, 250);
					core.setTextAlign("uievent", "right");
					totalMoney = selectCount * (type == 0 ? core.calValue(item.money) : core.calValue(item.sell));
					core.fillText("uievent", core.formatBigNumber(totalMoney), 405, 280);

					core.setTextAlign("uievent", "left");
					core.fillText("uievent", type == 0 ? "已购次数" : "已卖次数", 324, 170);
					core.setTextAlign("uievent", "right");
					core.fillText("uievent", (type == 0 ? item.money_count : item.sell_count) || 0, 405, 200);
				}
			}

			core.setTextAlign('uievent', 'left');
			core.setTextBaseline('uievent', 'alphabetic');
		}

		var _add = function (item, delta) {
			if (item == null) return;
			selectCount = core.clamp(
				selectCount + delta, 0,
				Math.min(type == 0 ? Math.floor(core.status.hero[use] / core.calValue(item.money)) : core.itemCount(item.id),
					type == 0 && item.number != null ? item.number : Number.MAX_SAFE_INTEGER)
			);
		}

		var _confirm = function (item) {
			if (item == null || selectCount == 0) return;
			if (type == 0) {
				core.status.hero[use] -= totalMoney;
				core.getItem(item.id, selectCount);
				core.stopSound();
				core.playSound('确定');
				if (item.number != null) item.number -= selectCount;
				item.money_count = (item.money_count || 0) + selectCount;
			} else {
				core.status.hero[use] += totalMoney;
				core.removeItem(item.id, selectCount);
				core.playSound('确定');
				core.drawTip("成功卖出" + selectCount + "个" + core.material.items[item.id].name, item.id);
				if (item.number != null) item.number += selectCount;
				item.sell_count = (item.sell_count || 0) + selectCount;
			}
			selectCount = 0;
		}

		this._performItemShopKeyBoard = function (keycode) {
			var item = list[selectItem] || null;
			// 键盘操作
			switch (keycode) {
				case 38: // up
					if (selectItem == null) break;
					if (selectItem == 0) selectItem = null;
					else selectItem--;
					selectCount = 0;
					break;
				case 37: // left
					if (selectItem == null) {
						if (type > 0) type--;
						break;
					}
					_add(item, -1);
					break;
				case 39: // right
					if (selectItem == null) {
						if (type < 2) type++;
						break;
					}
					_add(item, 1);
					break;
				case 40: // down
					if (selectItem == null) {
						if (list.length > 0) selectItem = 0;
						break;
					}
					if (list.length == 0) break;
					selectItem = Math.min(selectItem + 1, list.length - 1);
					selectCount = 0;
					break;
				case 13:
				case 32: // Enter/Space
					if (selectItem == null) {
						if (type == 2)
							core.insertAction({ "type": "break" });
						else if (list.length > 0)
							selectItem = 0;
						break;
					}
					_confirm(item);
					break;
				case 27: // ESC
					if (selectItem == null) {
						core.insertAction({ "type": "break" });
						break;
					}
					selectItem = null;
					break;
			}
		}

		this._performItemShopClick = function (px, py) {
			var item = list[selectItem] || null;
			// 鼠标操作
			if (px >= 22 && px <= 82 && py >= 71 && py <= 102) {
				// 买
				if (type != 0) {
					type = 0;
					selectItem = null;
					selectCount = 0;
				}
				return;
			}
			if (px >= 122 && px <= 182 && py >= 71 && py <= 102) {
				// 卖
				if (type != 1) {
					type = 1;
					selectItem = null;
					selectCount = 0;
				}
				return;
			}
			if (px >= 222 && px <= 282 && py >= 71 && py <= 102) // 离开
				return core.insertAction({ "type": "break" });
			// < >
			if (px >= 318 && px <= 341 && py >= 348 && py <= 376)
				return _add(item, -1);
			if (px >= 388 && px <= 416 && py >= 348 && py <= 376)
				return _add(item, 1);
			// 确定
			if (px >= 341 && px <= 387 && py >= 380 && py <= 407)
				return _confirm(item);

			// 上一页/下一页
			if (px >= 45 && px <= 105 && py >= 388) {
				if (page > 1) {
					selectItem -= 6;
					selectCount = 0;
				}
				return;
			}
			if (px >= 208 && px <= 268 && py >= 388) {
				if (page < totalPage) {
					selectItem = Math.min(selectItem + 6, list.length - 1);
					selectCount = 0;
				}
				return;
			}

			// 实际区域
			if (px >= 9 && px <= 300 && py >= 120 && py < 360) {
				if (list.length == 0) return;
				var index = parseInt((py - 120) / 40);
				var newItem = 6 * (page - 1) + index;
				if (newItem >= list.length) newItem = list.length - 1;
				if (newItem != selectItem) {
					selectItem = newItem;
					selectCount = 0;
				}
				return;
			}
		}

		this._performItemShopAction = function () {
			if (flags.type == 0) return this._performItemShopKeyBoard(flags.keycode);
			else return this._performItemShopClick(flags.px, flags.py);
		}

		this.openItemShop = function (itemShopId) {
			shopId = itemShopId;
			type = 0;
			page = 0;
			selectItem = null;
			selectCount = 0;
			core.isShopVisited(itemShopId);
			shopInfo = flags.__shops__[shopId];
			if (shopInfo.choices == null) shopInfo.choices = core.clone(core.status.shops[shopId].choices);
			choices = shopInfo.choices;
			use = core.status.shops[shopId].use;
			if (use != 'exp') use = 'money';
			useText = use == 'money' ? '金币' : '经验';

			core.insertAction([{
				"type": "while",
				"condition": "true",
				"data": [
					{ "type": "function", "function": "function () { core.plugin._drawItemShop(); }" },
					{ "type": "wait" },
					{ "type": "function", "function": "function() { core.plugin._performItemShopAction(); }" }
				]
			},
			{
				"type": "function",
				"function": "function () { core.deleteCanvas('uievent'); core.ui.clearUIEventSelector(); }"
			}
			]);
		}

	},
    "enemyLevel": function () {
		// 此插件将提供怪物手册中的怪物境界显示
		// 使用此插件需要先给每个怪物定义境界，方法如下：
		// 点击怪物的【配置表格】，找到“【怪物】相关的表格配置”，然后在【名称】仿照增加境界定义：
		/*
		 "level": {
			  "_leaf": true,
			  "_type": "textarea",
			  "_string": true,
			  "_data": "境界"
		 },
		 */
		// 然后保存刷新，可以看到怪物的属性定义中出现了【境界】。再开启本插件即可。

		// 是否开启本插件，默认禁用；将此改成 true 将启用本插件。
		var __enable = false;
		if (!__enable) return;

		// 这里定义每个境界的显示颜色；可以写'red', '#RRGGBB' 或者[r,g,b,a]四元数组
		var levelToColors = {
			"萌新一阶": "red",
			"萌新二阶": "#FF0000",
			"萌新三阶": [255, 0, 0, 1],
		};

		// 复写 _drawBook_drawName
		var originDrawBook = core.ui._drawBook_drawName;
		core.ui._drawBook_drawName = function (index, enemy, top, left, width) {
			// 如果没有境界，则直接调用原始代码绘制
			if (!enemy.level) return originDrawBook.call(core.ui, index, enemy, top, left, width);
			// 存在境界，则额外进行绘制
			core.setTextAlign('ui', 'center');
			if (enemy.specialText.length == 0) {
				core.fillText('ui', enemy.name, left + width / 2,
					top + 27, '#DDDDDD', this._buildFont(17, true));
				core.fillText('ui', enemy.level, left + width / 2,
					top + 51, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true));
			} else {
				core.fillText('ui', enemy.name, left + width / 2,
					top + 20, '#DDDDDD', this._buildFont(17, true), width);
				switch (enemy.specialText.length) {
					case 1:
						core.fillText('ui', enemy.specialText[0], left + width / 2,
							top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'),
							this._buildFont(14, true), width);
						break;
					case 2:
						// Step 1: 计算字体
						var text = enemy.specialText[0] + "  " + enemy.specialText[1];
						core.setFontForMaxWidth('ui', text, width, this._buildFont(14, true));
						// Step 2: 计算总宽度
						var totalWidth = core.calWidth('ui', text);
						var leftWidth = core.calWidth('ui', enemy.specialText[0]);
						var rightWidth = core.calWidth('ui', enemy.specialText[1]);
						// Step 3: 绘制
						core.fillText('ui', enemy.specialText[0], left + (width + leftWidth - totalWidth) / 2,
							top + 38, core.arrayToRGBA((enemy.specialColor || [])[0] || '#FF6A6A'));
						core.fillText('ui', enemy.specialText[1], left + (width + totalWidth - rightWidth) / 2,
							top + 38, core.arrayToRGBA((enemy.specialColor || [])[1] || '#FF6A6A'));
						break;
					default:
						core.fillText('ui', '多属性...', left + width / 2,
							top + 38, '#FF6A6A', this._buildFont(14, true), width);
				}
				core.fillText('ui', enemy.level, left + width / 2,
					top + 56, core.arrayToRGBA(levelToColors[enemy.level] || '#DDDDDD'), this._buildFont(14, true));
			}
		}

		// 也可以复写其他的属性颜色如怪物攻防等，具体参见下面的例子的注释部分
		core.ui._drawBook_drawRow1 = function (index, enemy, top, left, width, position) {
			// 绘制第一行
			core.setTextAlign('ui', 'left');
			var b13 = this._buildFont(13, true),
				f13 = this._buildFont(13, false);
			var col1 = left,
				col2 = left + width * 9 / 25,
				col3 = left + width * 17 / 25;
			core.fillText('ui', '生命', col1, position, '#DDDDDD', f13);
			core.fillText('ui', core.formatBigNumber(enemy.hp || 0), col1 + 30, position, /*'red' */ null, b13);
			core.fillText('ui', '攻击', col2, position, null, f13);
			core.fillText('ui', core.formatBigNumber(enemy.atk || 0), col2 + 30, position, /* '#FF0000' */ null, b13);
			core.fillText('ui', '防御', col3, position, null, f13);
			core.fillText('ui', core.formatBigNumber(enemy.def || 0), col3 + 30, position, /* [255, 0, 0, 1] */ null, b13);
		}


	},
    "multiHeros": function () {
		// 多角色插件
		// Step 1: 启用本插件
		// Step 2: 定义每个新的角色各项初始数据（参见下方注释）
		// Step 3: 在游戏中的任何地方都可以调用 `core.changeHero()` 进行切换；也可以 `core.changeHero(1)` 来切换到某个具体的角色上

		// 是否开启本插件，默认禁用；将此改成 true 将启用本插件。
		var __enable = false;
		if (!__enable) return;

		// 在这里定义全部的新角色属性
		// 请注意，在这里定义的内容不会多角色共用，在切换时会进行恢复。
		// 你也可以自行新增或删除，比如不共用金币则可以加上"money"的初始化，不共用道具则可以加上"items"的初始化，
		// 多角色共用hp的话则删除hp，等等。总之，不共用的属性都在这里进行定义就好。
		var hero1 = {
			"floorId": "MT0", // 该角色初始楼层ID；如果共用楼层可以注释此项
			"image": "brave.png", // 角色的行走图名称；此项必填不然会报错
			"name": "1号角色",
			"lv": 1,
			"hp": 10000, // 如果HP共用可注释此项
			"atk": 1000,
			"def": 1000,
			"mdef": 0,
			// "money": 0, // 如果要不共用金币则取消此项注释
			// "exp": 0, // 如果要不共用经验则取消此项注释
			"loc": { "x": 0, "y": 0, "direction": "up" }, // 该角色初始位置；如果共用位置可注释此项
			"items": {
				"tools": {}, // 如果共用消耗道具（含钥匙）则可注释此项
				// "constants": {}, // 如果不共用永久道具（如手册）可取消注释此项
				"equips": {}, // 如果共用在背包的装备可注释此项
			},
			"equipment": [], // 如果共用装备可注释此项；此项和上面的「共用在背包的装备」需要拥有相同状态，不然可能出现问题
		};
		// 也可以类似新增其他角色
		// 新增的角色，各项属性共用与不共用的选择必须和上面完全相同，否则可能出现问题。
		// var hero2 = { ...

		var heroCount = 2; // 包含默认角色在内总共多少个角色，该值需手动修改。

		this.initHeros = function () {
			core.setFlag("hero1", core.clone(hero1)); // 将属性值存到变量中
			// core.setFlag("hero2", core.clone(hero2)); // 更多的角色也存入变量中；每个定义的角色都需要新增一行

			// 检测是否存在装备
			if (hero1.equipment) {
				if (!hero1.items || !hero1.items.equips) {
					alert('多角色插件的equipment和道具中的equips必须拥有相同状态！');
				}
				// 存99号套装为全空
				var saveEquips = core.getFlag("saveEquips", []);
				saveEquips[99] = [];
				core.setFlag("saveEquips", saveEquips);
			} else {
				if (hero1.items && hero1.items.equips) {
					alert('多角色插件的equipment和道具中的equips必须拥有相同状态！');
				}
			}
		}

		// 在游戏开始注入initHeros
		var _startGame_setHard = core.events._startGame_setHard;
		core.events._startGame_setHard = function () {
			_startGame_setHard.call(core.events);
			core.initHeros();
		}

		// 切换角色
		// 可以使用 core.changeHero() 来切换到下一个角色
		// 也可以 core.changeHero(1) 来切换到某个角色（默认角色为0）
		this.changeHero = function (toHeroId) {
			var currHeroId = core.getFlag("heroId", 0); // 获得当前角色ID
			if (toHeroId == null) {
				toHeroId = (currHeroId + 1) % heroCount;
			}
			if (currHeroId == toHeroId) return;

			var saveList = Object.keys(hero1);

			// 保存当前内容
			var toSave = {};
			// 暂时干掉 drawTip 和 音效，避免切装时的提示
			var _drawTip = core.ui.drawTip;
			core.ui.drawTip = function () { };
			var _playSound = core.control.playSound;
			core.control.playSound = function () { return undefined; };
			// 记录当前录像，因为可能存在换装问题
			core.clearRouteFolding();
			var routeLength = core.status.route.length;
			// 优先判定装备
			if (hero1.equipment) {
				core.items.quickSaveEquip(100 + currHeroId);
				core.items.quickLoadEquip(99);
			}

			saveList.forEach(function (name) {
				if (name == 'floorId') toSave[name] = core.status.floorId; // 楼层单独设置
				else if (name == 'items') {
					toSave.items = core.clone(core.status.hero.items);
					Object.keys(toSave.items).forEach(function (one) {
						if (!hero1.items[one]) delete toSave.items[one];
					});
				} else toSave[name] = core.clone(core.status.hero[name]); // 使用core.clone()来创建新对象
			});

			core.setFlag("hero" + currHeroId, toSave); // 将当前角色信息进行保存
			var data = core.getFlag("hero" + toHeroId); // 获得要切换的角色保存内容

			// 设置角色的属性值
			saveList.forEach(function (name) {
				if (name == "items") {
					Object.keys(core.status.hero.items).forEach(function (one) {
						if (data.items[one]) core.status.hero.items[one] = core.clone(data.items[one]);
					});
				} else {
					core.status.hero[name] = core.clone(data[name]);
				}
			});
			// 最后装上装备
			if (hero1.equipment) {
				core.items.quickLoadEquip(100 + toHeroId);
			}

			core.ui.drawTip = _drawTip;
			core.control.playSound = _playSound;
			core.status.route = core.status.route.slice(0, routeLength);
			core.control._bindRoutePush();

			// 插入事件：改变角色行走图并进行楼层切换
			var toFloorId = data.floorId || core.status.floorId;
			var toLoc = data.loc || core.status.hero.loc;
			core.insertAction([
				{ "type": "setHeroIcon", "name": data.image || "hero.png" }, // 改变行走图
				// 同层则用changePos，不同层则用changeFloor；这是为了避免共用楼层造成触发eachArrive
				toFloorId != core.status.floorId ? {
					"type": "changeFloor",
					"floorId": toFloorId,
					"loc": [toLoc.x, toLoc.y],
					"direction": toLoc.direction,
					"time": 0 // 可以在这里设置切换时间
				} : { "type": "changePos", "loc": [toLoc.x, toLoc.y], "direction": toLoc.direction }
				// 你还可以在这里执行其他事件，比如增加或取消跟随效果
			]);
			core.setFlag("heroId", toHeroId); // 保存切换到的角色ID
		}
	},
    "heroFourFrames": function () {
		// 样板的勇士/跟随者移动时只使用2、4两帧，观感较差。本插件可以将四帧全用上。

		// 是否启用本插件
		var __enable = false;
		if (!__enable) return;

		["up", "down", "left", "right"].forEach(function (one) {
			// 指定中间帧动画
			core.material.icons.hero[one].midFoot = 2;
		});

		var heroMoving = function (timestamp) {
			if (core.status.heroMoving <= 0) return;
			if (timestamp - core.animateFrame.moveTime > core.values.moveSpeed) {
				core.animateFrame.leftLeg++;
				core.animateFrame.moveTime = timestamp;
			}
			core.drawHero(['stop', 'leftFoot', 'midFoot', 'rightFoot'][core.animateFrame.leftLeg % 4], 4 * core.status.heroMoving);
		}
		core.registerAnimationFrame('heroMoving', true, heroMoving);

		core.events._eventMoveHero_moving = function (step, moveSteps) {
			var curr = moveSteps[0];
			var direction = curr[0],
				x = core.getHeroLoc('x'),
				y = core.getHeroLoc('y');
			// ------ 前进/后退
			var o = direction == 'backward' ? -1 : 1;
			if (direction == 'forward' || direction == 'backward') direction = core.getHeroLoc('direction');
			var faceDirection = direction;
			if (direction == 'leftup' || direction == 'leftdown') faceDirection = 'left';
			if (direction == 'rightup' || direction == 'rightdown') faceDirection = 'right';
			core.setHeroLoc('direction', direction);
			if (curr[1] <= 0) {
				core.setHeroLoc('direction', faceDirection);
				moveSteps.shift();
				return true;
			}
			if (step <= 4) core.drawHero('stop', 4 * o * step);
			else if (step <= 8) core.drawHero('leftFoot', 4 * o * step);
			else if (step <= 12) core.drawHero('midFoot', 4 * o * (step - 8));
			else if (step <= 16) core.drawHero('rightFoot', 4 * o * (step - 8)); // if (step == 8) {
			if (step == 8 || step == 16) {
				core.setHeroLoc('x', x + o * core.utils.scan2[direction].x, true);
				core.setHeroLoc('y', y + o * core.utils.scan2[direction].y, true);
				core.updateFollowers();
				curr[1]--;
				if (curr[1] <= 0) moveSteps.shift();
				core.setHeroLoc('direction', faceDirection);
				return step == 16;
			}
			return false;
		}
	},
    "startCanvas": function () {
		// 使用本插件可以将自绘的标题界面居中。仅在【标题开启事件化】后才有效。
		// 由于一些技术性的原因，标题界面事件化无法应用到覆盖状态栏的整个界面。
		// 这是一个较为妥协的插件，会在自绘标题界面时隐藏状态栏、工具栏和边框，并将画布进行居中。
		// 本插件仅在全塔属性的 "startCanvas" 生效；进入 "startText" 时将会离开居中状态，回归正常界面。

		// 是否开启本插件，默认禁用；将此改成 true 将启用本插件。
		var __enable = false;
		if (!__enable) return;

		// 检查【标题开启事件化】是否开启
		if (!core.flags.startUsingCanvas || main.mode != 'play') return;

		var _isTitleCanvasEnabled = false;
		var _getClickLoc = core.actions._getClickLoc;
		this._setTitleCanvas = function () {
			if (_isTitleCanvasEnabled) return;
			_isTitleCanvasEnabled = true;

			// 禁用窗口resize
			window.onresize = function () { };
			core.resize = function () { }

			// 隐藏状态栏
			core.dom.statusBar.style.display = 'none';
			core.dom.statusCanvas.style.display = 'none';
			core.dom.toolBar.style.display = 'none';
			// 居中画布
			if (core.domStyle.isVertical) {
				core.dom.gameDraw.style.top =
					(parseInt(core.dom.gameGroup.style.height) - parseInt(core.dom.gameDraw.style.height)) / 2 + "px";
			} else {
				core.dom.gameDraw.style.right =
					(parseInt(core.dom.gameGroup.style.width) - parseInt(core.dom.gameDraw.style.width)) / 2 + "px";
			}
			core.dom.gameDraw.style.border = '3px transparent solid';
			core.actions._getClickLoc = function (x, y) {
				var left = core.dom.gameGroup.offsetLeft + core.dom.gameDraw.offsetLeft + 3;
				var top = core.dom.gameGroup.offsetTop + core.dom.gameDraw.offsetTop + 3;
				var loc = { 'x': Math.max(x - left, 0), 'y': Math.max(y - top, 0), 'size': 32 * core.domStyle.scale };
				return loc;
			}
		}

		this._resetTitleCanvas = function () {
			if (!_isTitleCanvasEnabled) return;
			_isTitleCanvasEnabled = false;
			window.onresize = function () { try { main.core.resize(); } catch (ee) { console.error(ee) } }
			core.resize = function () { return core.control.resize(); }
			core.resize();
			core.actions._getClickLoc = _getClickLoc;
		}

		// 复写“开始游戏”
		core.events._startGame_start = function (hard, seed, route, callback) {
			console.log('开始游戏');
			core.resetGame(core.firstData.hero, hard, null, core.cloneArray(core.initStatus.maps));
			core.setHeroLoc('x', -1);
			core.setHeroLoc('y', -1);

			if (seed != null) {
				core.setFlag('__seed__', seed);
				core.setFlag('__rand__', seed);
			} else core.utils.__init_seed();

			core.clearStatusBar();
			core.plugin._setTitleCanvas();

			var todo = [];
			core.hideStatusBar();
			core.push(todo, core.firstData.startCanvas);
			core.push(todo, { "type": "function", "function": "function() { core.plugin._resetTitleCanvas(); core.events._startGame_setHard(); }" })
			core.push(todo, core.firstData.startText);
			this.insertAction(todo, null, null, function () {
				core.events._startGame_afterStart(callback);
			});

			if (route != null) core.startReplay(route);
		}

		var _loadData = core.control.loadData;
		core.control.loadData = function (data, callback) {
			core.plugin._resetTitleCanvas();
			_loadData.call(core.control, data, callback);
		}
	},
    "platFly": function () {
		// 本插件可以给平面塔启用一个带小地图的楼传，默认关闭
		// 是否开启本插件，默认禁用；将此改成 true 将启用本插件。
		var __enable = false;
		if (!__enable) return;

		/* 第一步：复制到插件
		 * 第二步：将flag:usePlatFly改为true可将带地图的楼传启用
		 * 第三步：将flag:__useMinimap__设为true可开启小地图，该操作需切换楼层
		 * 修改小地图的缩放可以修改flag:userScale，默认为1
		 * 注意：楼层转换必须使用楼层坐标，而不是前一楼、后一楼
		 * 关于操作：上楼和下楼操作为PgUp和PgDn，后退10层和前进10层操作为,（<）和.（>）
		 * 注意：请尽量保证地图传送的地方物理位置正好对准（即把地图拼接上箭头位置恰好对齐）否则连线的位置可能不准
		 * 分区说明：用普通事件的楼层转换（红点）代替楼层转换（绿点）即可
		 * 说明：紫色表示目前可以到达却没有到达过的地图（所以这是一个具有探索性的地图插件）
		 */
		// 录像验证直接干掉这个插件
		if (main.replayChecking) return;
		// *** --- 以下数据为用户可修改数据 修改后不影响该插件的基本功能 --- *** //
		// 检测楼层转换的图块id
		var leftPortal = "leftPortal", // 左
			rightPortal = "rightPortal", // 右
			upPortal = "upPortal", // 上
			downPortal = "downPortal", // 下
			upFloor = "upFloor", // 上楼
			downFloor = "downFloor"; // 下楼
		// 一些常用默认值
		var defaultScale = 1, // 默认缩放比率
			defaultLoop = 5, // 绘制地图时的循环检测地图路线次数，loop为5说明最远的地图可以用6步到达
			defaultOpacity = 0.6, // 默认不透明度
			defaultMinorAlpha = defaultOpacity / 2; // 3D绘图时，不与当前层处于同一高度层的默认初始不透明度，不透明度会随着层数的增加或减少而减少
		// *** --- 用户修改区 END --- *** //
		//  其余可自定义内容均用 //***--- 包裹着

		/* -----------------------------------下面是一些调用案例
		 * 
		 * 如果想在状态栏显示可以在状态栏自绘里这么写：
		 core.drawFlyMap(ctx, 你想要的左上角横坐标, 你想要的左上角纵坐标, 你想要的宽度, 你想要的高度, core.status.floorId, {fromUser: true, opacity: 1, loop: 3, use3D:false});
		 * 
		 * -----------------------------------以下为高深区域，如果不必要可以不看
		 * 
		 * 主要函数说明（这几个函数异常强大，善用可以有弄一些有意思的东西，既然作者都说强大了，那就是特别强大）
		 * core.drawFlyMap(ctx : string, x?: number, y?: number, width?:number, height?:number, floorId?:string, config?:any): void
		 * 参数说明：
		 * ctx:画布，如果不存在则会新建x,y,width,height参数的画布
		 * x,y,width,height:画布不存在时创建该数据的画布，如果存在，那么在画布的x,y位置绘制宽度为width，高度为height的地图
		 * floorId:以该楼层为中心绘制
		 * config:配制参数，包括以下内容（该参数为对象）
		 * 	 fromUser:是否循环绘制（为false时只绘制一层，否则绘制5层）
		 * 	 oriFloor:这个不用管就好，具体原理比较复杂，不填或者和floorId一样就行
		 * 	 scale:缩放比率，这个是绝对比率，与画布大小无关，默认为1
		 * 	 interval:地图间的间距，默认为width / 24
		 * 	 deltaH:3D地图的纵向高度差，默认为绘制高度（height）/ 4
		 * 	 noErase:是否不清空画布再绘制，默认为false
		 * 	 fromMini:是否是在小地图上绘制时调用的（无自动缩放和自动定位）
		 * 	 loop:绘制层数，fromUser为true时才有用，默认为5
		 * 	 opacity:不透明度，默认为0.6
		 * 	 minorAlpha:3D地图的不与当前层在同一高度的楼层的初始不透明度，默认为opacity/2
		 * 	 layer:平面模式下绘制的楼层高度，当前所在层为0，上楼则+1，下楼则-1，默认为0，只有当use3D为false且可以绘制3D地图时有效
		 * 	 use3D:是否启用3D绘图，前提是所有地图不都在同一高度，默认为false
		 * 	 reLeft:3D重新定位画布的左位置，默认为-240
		 * 	 reTop:3D重新定位画布的上位置，默认为-240
		 * 	 clearCache:是否清除缓存重算，默认为false
		 * 	 map:这个是最强大的功能了，可以将自定义地图插入，以绘制自定义地图
		 * 	 自定义地图格式：{"left_0_7":"MT1", "right_2_7":"MT2"}以此类推也可以"left_0_7,right_3_7,up_1_0"这种套娃，使用的前提是指定位置得有相应方向的箭头，不然会出错，注意楼层id是到往的楼层的id，如果想绘制3D地图，可以这么写：{"top_1_3,left_0_7": "1_MT1"}，前面的1_必须写，上楼为top，下楼为bottom
		 * 	 默认取自floorId地图
		 * 如有疑问可在造塔群@古祠
		 * 
		 * core.getFlyMap(floorId?:string, fromUser?:boolean, oriFloor?:string, loop?:number):Object
		 * 参数说明:floorId:以该楼层为中心绘制
		 * fromUser:是否循环绘制（为false时只绘制一层，否则绘制5层）
		 * oriFloor:这个不用管就好，具体原理比较复杂，填null或者和floorId一样就行
		 * loop:绘制层数，fromUser为true时才有用，默认为5
		 */

		////// 绘制楼层传送器 //////
		var originDrawFly = core.ui.drawFly;
		ui.prototype.drawFly = function (page) {
			if (!flags.usePlatFly || core.isReplaying()) return originDrawFly.call(core.ui, page);
			core.status.event.data = page;
			var floorId = core.floorIds[page];
			core.clearMap('ui');
			core.setAlpha('ui', 0.85);
			core.fillRect('ui', 0, 0, this.PIXEL, this.PIXEL, '#000000');
			core.setAlpha('ui', 1);
			var size = this.PIXEL;
			//***--- 楼传绘制 可根据自己的需求更改 更改之后注意更改点击操作的函数
			// 背景
			core.drawThumbnail(floorId, null, { ctx: 'ui', x: 0, y: 0, size: size, damage: true, fromFly: true });
			core.fillRect("ui", 0, 65, 32, size - 130, [0, 0, 0, 0.7]);
			core.fillRect("ui", size, 65, -32, size - 130, [0, 0, 0, 0.7]);
			core.fillRect("ui", 65, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]);
			core.fillRect("ui", 65, size, size / 2 - 114, -32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size / 2 + 49, 0, size / 2 - 114, 32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size / 2 + 49, size, size / 2 - 114, -32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size / 2 - 47, 0, 94, 32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size / 2 - 47, size, 94, -32, [0, 0, 0, 0.7]);
			core.fillRect("ui", 0, 0, 63, 32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size, 0, -63, 32, [0, 0, 0, 0.7]);
			core.fillRect("ui", 0, 32, 32, 31, [0, 0, 0, 0.7]);
			core.fillRect("ui", 0, size - 32, 32, -31, [0, 0, 0, 0.7]);
			core.fillRect("ui", 0, size, 63, -32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size, size, -63, -32, [0, 0, 0, 0.7]);
			core.fillRect("ui", size, size - 32, -32, -31, [0, 0, 0, 0.7]);
			core.fillRect("ui", size, 32, -32, 31, [0, 0, 0, 0.7]);
			core.setTextAlign("ui", "center");
			// 文字
			if (core.getFloorByDirection("left", floorId))
				core.fillText("ui", "←", 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("left",
					floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
			if (core.getFloorByDirection("right", floorId))
				core.fillText("ui", "→", size - 16, size / 2 + 10, core.hasVisitedFloor(core.getFloorByDirection("right",
					floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
			if (core.getFloorByDirection("up", floorId))
				core.fillText("ui", "↑", size / 2, 28, core.hasVisitedFloor(core.getFloorByDirection("up",
					floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
			if (core.getFloorByDirection("down", floorId))
				core.fillText("ui", "↓", size / 2, size - 4, core.hasVisitedFloor(core.getFloorByDirection("down",
					floorId)) ? "#ffffff" : "#ff22ff", "26px Verdana");
			if (core.getFloorByDirection("top", floorId))
				core.fillText("ui", "上楼", size / 4 + 8, 24, core.hasVisitedFloor(core.getFloorByDirection("top",
					floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font);
			if (core.getFloorByDirection("bottom", floorId))
				core.fillText("ui", "下楼", size / 4 * 3 - 8, 24, core.hasVisitedFloor(core.getFloorByDirection("bottom",
					floorId)) ? "#ffffff" : "#ff22ff", "22px " + core.status.globalAttribute.font);
			core.fillText("ui", "退10层", size / 4 + 8, size - 8, core.actions._getNextFlyFloor(-1) == page ? "#ff22ff" : "#ffffff",
				"22px " + core.status.globalAttribute.font);
			core.fillText("ui", "进10层", size / 4 * 3 - 8, size - 8, core.actions._getNextFlyFloor(1) == page ? "#ff22ff" : "#ffffff",
				"22px " + core.status.globalAttribute.font);
			core.fillText("ui", "退出", 32, 24, "#ffffff", "22px " + core.status.globalAttribute.font);
			core.fillText("ui", "楼层名", 32, size - 8, "#ffffff", "22px " + core.status.globalAttribute.font);
			core.fillText("ui", "（B）", 16, size - 40, "#ffffff", "22px " + core.status.globalAttribute.font);
			core.createCanvas("mapOnUi", -240, -240, size + 480, size + 480, 150);
			core.drawFlyMap("mapOnUi", 240, 240, size, size, floorId, { fromUser: true, oriFloor: floorId, use3D: flags.use3D });
			if (core.can3D(floorId) && !flags.in3D)
				core.fillText("ui", "3D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font);
			if (flags.in3D)
				core.fillText("ui", "2D模式", size - 32, 24, "#ffffff", "20px " + core.status.globalAttribute.font);
			if (core.can3D(floorId)) core.fillText("ui", "（Z）", size - 16, 56, "#ffffff", "20px " + core.status.globalAttribute.font);
			if (flags.flyTitle) {
				var style = document.getElementById("ui").getContext("2d");
				style.shadowColor = "rgba(0, 0, 0, 1)";
				style.shadowBlur = 5;
				core.fillRect("ui", size / 4, size / 4, size / 2, size / 8, [180, 180, 180, 0.7]);
				core.strokeRect("ui", size / 4, size / 4, size / 2, size / 8, [255, 255, 255, 0.7], 3);
				style.shadowOffsetX = 4;
				style.shadowOffsetY = 2;
				core.fillText("ui", (core.status.maps[floorId] || {}).title, size / 2, size / 16 * 5 + 11, "#ffffff",
					"32px " + core.status.globalAttribute.font);
				style.shadowColor = "none";
				style.shadowBlur = 0;
				style.shadowOffsetX = 0;
				style.shadowOffsetY = 0;
			}
			//***--- 楼传绘制
		};
		////// 楼层传送器界面时的点击操作 //////
		var originClickFly = core.actions._clickFly;
		actions.prototype._clickFly = function (x, y) {
			if (!flags.usePlatFly || core.isReplaying()) return originClickFly.call(core.actions, x, y);
			var page = core.status.event.data;
			var floorId = core.floorIds[page];
			//***--- 点击操作 可以修改的地方只有x,y坐标 其余不可修改
			if (x <= 2 && y <= 1) {
				core.playSound('取消');
				core.deleteCanvas("mapOnUi")
				core.ui.closePanel();
				return;
			}
			// 3D模式
			if (x >= core.__SIZE__ - 2 && y <= 1) {
				if (core.can3D(floorId) && !flags.in3D)
					flags.use3D = true;
				if (flags.in3D) flags.use3D = false;
				core.playSound('光标移动');
				core.ui.drawFly(page);
				return;
			}
			// 显示名称
			if (x <= 1 && y >= core.__SIZE__ - 2) {
				if (flags.flyTitle) flags.flyTitle = false;
				else flags.flyTitle = true;
				core.playSound('光标移动');
				core.ui.drawFly(page);
				return;
			}
			// 飞过去
			if (x > 1 && x < core.__SIZE__ - 1 && y > 1 && y < core.__SIZE__ - 1) {
				if (core.status.maps[core.status.floorId].canFlyFrom &&
					core.status.maps[core.floorIds[core.status.event.data]].canFlyTo &&
					core.hasVisitedFloor(core.floorIds[core.status.event.data])) {
					core.deleteCanvas("mapOnUi");
				}
				core.flyTo(core.floorIds[core.status.event.data]);
			}
			// 前进10层 后退10层
			if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(-1) != page &&
				x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2) {
				core.ui.drawFly(this._getNextFlyFloor(-10));
				core.playSound("光标移动");
			}
			if (y > core.__SIZE__ - 2 && core.actions._getNextFlyFloor(1) != page &&
				x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3) {
				core.ui.drawFly(this._getNextFlyFloor(10));
				core.playSound("光标移动");
			}
			// 获取索引
			function getId(direction) {
				var id = core.getFloorByDirection(direction, floorId);
				for (var i in core.floorIds) {
					if (core.floorIds[i] == id) return parseInt(i);
				}
			}
			// 上下左右和上下楼
			if (x < 1 && core.getFloorByDirection("left", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("left", floorId)) && y >= 2 && y < core.__SIZE__ - 2) {
				core.playSound("光标移动");
				core.drawFly(getId("left"));
			}
			if (x > core.__SIZE__ - 2 && core.getFloorByDirection("right", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("right", floorId)) && y >= 2 && y < core.__SIZE__ - 2) {
				core.playSound("光标移动");
				core.drawFly(getId("right"));
			}
			if (y < 1 && core.getFloorByDirection("up", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("up", floorId)) &&
				x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) {
				core.playSound("光标移动");
				core.drawFly(getId("up"));
			}
			if (y > core.__SIZE__ - 2 && core.getFloorByDirection("down", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("down", floorId)) &&
				x >= Math.floor(core.__SIZE__ / 2) - 1 && x <= Math.ceil(core.__SIZE__ / 2)) {
				core.playSound("光标移动");
				core.drawFly(getId("down"));
			}
			if (y < 1 && x >= 2 && x <= Math.floor(core.__SIZE__ / 2) - 2 &&
				core.getFloorByDirection("top", floorId) && core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) {
				core.playSound("光标移动");
				core.drawFly(getId("top"));
			}
			if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1 && x <= core.__SIZE__ - 3 &&
				core.getFloorByDirection("bottom", floorId) && core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) {
				core.playSound("光标移动");
				core.drawFly(getId("bottom"));
			}
			return;
			//***--- 点击操作
		};
		////// 楼层传送器界面时，按下某个键的操作 //////
		var originKeyDownFly = core.actions._keyDownFly;
		actions.prototype._keyDownFly = function (keycode) {
			if (!flags.usePlatFly || core.isReplaying()) return originKeyDownFly.call(core.actions, keycode);
			var page = core.status.event.data;
			var floorId = core.floorIds[page];
			// 获取索引
			function getId(direction) {
				var id = core.getFloorByDirection(direction, floorId);
				for (var i in core.floorIds) {
					if (core.floorIds[i] == id) return parseInt(i);
				}
			}
			//***--- 按键操作 只可以修改按键的keycode
			if (keycode == 37 && core.getFloorByDirection("left", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("left", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("left"));
			} else if (keycode == 38 && core.getFloorByDirection("up", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("up", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("up"));
			} else if (keycode == 39 && core.getFloorByDirection("right", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("right", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("right"));
			} else if (keycode == 40 && core.getFloorByDirection("down", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("down", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("down"));
			} else if (keycode == 33 && core.getFloorByDirection("top", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("top", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("top"));
			} else if (keycode == 34 && core.getFloorByDirection("bottom", floorId) &&
				core.hasVisitedFloor(core.getFloorByDirection("bottom", floorId))) {
				core.playSound('光标移动');
				core.ui.drawFly(getId("bottom"));
			} else if (keycode == 90) {
				if (core.can3D(floorId) && !flags.in3D)
					flags.use3D = true;
				if (flags.in3D) flags.use3D = false;
				core.playSound('光标移动');
				core.ui.drawFly(page);
			} else if (keycode == 66) { // 地图名
				if (flags.flyTitle) flags.flyTitle = false;
				else flags.flyTitle = true;
				core.playSound('光标移动');
				core.ui.drawFly(page);
			} else if (keycode == 188 && core.actions._getNextFlyFloor(-1) != page) { // 退10层
				core.ui.drawFly(this._getNextFlyFloor(-10));
				core.playSound("光标移动");
			} else if (keycode == 190 && core.actions._getNextFlyFloor(1) != page) { // 进10层
				core.ui.drawFly(this._getNextFlyFloor(10));
				core.playSound("光标移动");
			}
			return;
			//***--- 按键操作
		};
		////// 楼层传送器界面时，放开某个键的操作 //////
		var originKeyUpFly = core.actions._keyUpFly;
		actions.prototype._keyUpFly = function (keycode) {
			if (!flags.usePlatFly || core.isReplaying()) return originKeyUpFly.call(core.actions, keycode);
			if (keycode == 71 || keycode == 27 || keycode == 88) {
				core.playSound('取消');
				core.deleteCanvas("mapOnUi");
				core.ui.closePanel();
			}
			if (keycode == 13 || keycode == 32 || keycode == 67)
				this._clickFly(this.HSIZE - 1, this.HSIZE - 1);
			return;
		};
		////// 点击楼层传送器时的打开操作 //////
		events.prototype.useFly = function (fromUserAction) {
			if (core.isReplaying()) return;

			// 从“浏览地图”页面：尝试直接传送到该层
			if (core.status.event.id == 'viewMaps') {
				if (!core.hasItem('fly')) {
					core.playSound('操作失败');
					core.drawTip('你没有' + core.material.items['fly'].name, 'fly');
				} else if (!core.canUseItem('fly')) {
					core.playSound('操作失败');
					core.drawTip('无法传送到当前层', 'fly');
				} else {
					core.flyTo(core.status.event.data.floorId);
				}
				return;
			}
			if (!this._checkStatus('fly', fromUserAction, true)) {
				core.deleteCanvas('mapOnUi');
				return;
			}
			if (core.flags.flyNearStair && !core.nearStair()) {
				core.playSound('操作失败');
				core.drawTip("只有在楼梯边才能使用" + core.material.items['fly'].name, 'fly');
				core.unlockControl();
				core.status.event.data = null;
				core.status.event.id = null;
				return;
			}
			if (!core.canUseItem('fly')) {
				core.playSound('操作失败');
				core.drawTip(core.material.items['fly'].name + "好像失效了", 'fly');
				core.unlockControl();
				core.status.event.data = null;
				core.status.event.id = null;
				return;
			}
			core.playSound('打开界面');
			core.useItem('fly', true);
			return;
		};
		// 获取区域平面地图
		this.getFlyMap = function (floorId, fromUser, oriFloor, loop, clearCache) {
			floorId = floorId || core.status.floorId;
			if (floorId == oriFloor && !fromUser) return;
			oriFloor = oriFloor || core.status.floorId;
			if (!floorId) return;
			// 判断是否需要缓存
			function needCache(fromUser) {
				if (fromUser && !core.status.flyMap[floorId]) return true;
				if (!fromUser && !core.status.flyMap.cache[floorId]) return true;
				return false;
			}
			// 缓存，加快运行速率
			if (!core.status.flyMap) core.status.flyMap = {};
			if (!core.status.flyMap.cache) core.status.flyMap.cache = {};
			if (!core.status.layer) core.status.layer = {};
			if (!core.status.layer[floorId]) core.status.layer[floorId] = {};
			if (core.status.flyMap.cache[floorId] && fromUser) delete core.status.flyMap.cache[floorId]
			if (core.status.flyMap[floorId] && !fromUser) delete core.status.flyMap[floorId]
			if (needCache(fromUser) || clearCache) {
				// 初始化
				core.status.flyMap[floorId] = {};
				core.status.flyMap[floorId].thisMap = {};
				core.extractBlocks(floorId);
				core.status.maps[floorId].blocks.forEach(function (block) {
					var id = block.event.id;
					var x = block.x,
						y = block.y;
					var trigger = block.event.trigger;
					if (trigger != "changeFloor" && trigger != "upFloor" && trigger != "downFloor") return;
					// 是箭头且可以切换地图
					var toFloor = block.event.data.floorId;
					// 加入相应位置
					// 箭头
					if (id == leftPortal) {
						core.status.flyMap[floorId].thisMap["left_" + x + "_" + y] = toFloor;
					}
					if (id == upPortal) {
						core.status.flyMap[floorId].thisMap["up_" + x + "_" + y] = toFloor;
					}
					if (id == rightPortal) {
						core.status.flyMap[floorId].thisMap["right_" + x + "_" + y] = toFloor;
					}
					if (id == downPortal) {
						core.status.flyMap[floorId].thisMap["down_" + x + "_" + y] = toFloor;
					}
					// 上下楼
					if (id == upFloor) {
						core.status.flyMap[floorId].thisMap["top_" + x + "_" + y] = toFloor;
						core.status.layer[floorId].top = true;
					}
					if (id == downFloor) {
						core.status.flyMap[floorId].thisMap["bottom_" + x + "_" + y] = toFloor;
						core.status.layer[floorId].bottom = true;
					}
				});
				// 把下几层接着检测出来
				if (fromUser) {
					var usedId = {};
					for (var c = 1; c <= loop; c++) {
						for (var i in core.status.flyMap[floorId].thisMap) {
							var link = core.status.flyMap[floorId].thisMap;
							if (!core.hasVisitedFloor(link[i]) || link[i] instanceof Object || usedId[link[i]]) continue;
							usedId[link[i]] = true;
							var next = core.getFlyMap(link[i], false, oriFloor);
							for (var to in next) {
								if (!core.status.layer[next[to]]) core.status.layer[next[to]] = {};
								core.status.flyMap[floorId].thisMap[i + ',' + to] = next[to];
							}
						}
					}
				}
				// 把先上再下之类的去掉
				if (fromUser) {
					for (var i in core.status.flyMap[floorId].thisMap) {
						var route = i.split(",");
						for (var one = 0; one <= route.length - 2; one++) {
							var step = route[one],
								next = route[one + 1];
							if ((step.startsWith("up") && next.startsWith("down")) ||
								(step.startsWith("down") && next.startsWith("up")) ||
								(step.startsWith("left") && next.startsWith("right")) ||
								(step.startsWith("right") && next.startsWith("left")) ||
								(step.startsWith("top") && next.startsWith("bottom")) ||
								(step.startsWith("bottom") && next.startsWith("top"))) {
								delete core.status.flyMap[floorId].thisMap[i];
							}
						}
					}
				}
				// 非当前层不能存此类缓存
				if (!fromUser) {
					core.status.flyMap.cache = {};
					core.status.flyMap.cache[floorId] = {};
					core.status.flyMap.cache[floorId].thisMap = core.status.flyMap[floorId].thisMap;
					delete core.status.flyMap[floorId];
				}
				return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap;
			} else { // 直接使用缓存
				return fromUser ? core.status.flyMap[floorId].thisMap : core.status.flyMap.cache[floorId].thisMap;
			}
		};
		this.can3D = function (floorId) {
			var map = core.getFlyMap(floorId, true, floorId);
			for (var route in map) {
				if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0)
					return true;
			}
			return false;
		};
		// 绘制地图
		this.drawFlyMap = function (ctx, x, y, width, height, floorId, config) {
			// 初始化配置项
			//***--- 初始化 可以修改 || 后的默认值 参数说明在开头的高深区域中
			var fromUser = config.fromUser || false,
				oriFloor = config.oriFloor || core.status.floorId,
				scale = config.scale || null,
				interval = config.interval || (width / 24),
				noErase = config.noErase || false,
				fromMini = config.fromMini || false,
				loop = config.loop || defaultLoop,
				opacity = config.opacity || defaultOpacity,
				layer = config.layer || 0,
				use3D = config.use3D || false,
				clearCache = config.clearCache || false,
				map = config.map || null;
			//***--- 初始化
			map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache);
			floorId = floorId || core.status.floorId;
			if (!floorId) return;
			// 检测是否需要3D绘图
			if (!fromMini && use3D) {
				for (var route in map) {
					if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) {
						config.map = map;
						return core.draw3DFlyMap(ctx, x, y, width, height, floorId, config);
					}
				}
			}
			if (layer != 0 && !use3D) {
				var canLayer = false
				for (var route in map) {
					if (route.indexOf("top") >= 0 || route.indexOf("bottom") >= 0) {
						canLayer = true;
						break;
					}
				}
				if (!canLayer) layer = 0;
			}
			flags.in3D = false;
			// 初始化
			var userScale = true;
			var newCreate = false;
			if (!scale) {
				userScale = false;
				scale = scale || defaultScale;
			}
			if (!core.dymCanvas[ctx]) {
				core.createCanvas(ctx, x, y, width, height, 140);
				newCreate = true;
			}
			x = x || 0;
			y = y || 0;
			// 获得canvas属性
			width = width || document.getElementById(ctx).width;
			height = height || document.getElementById(ctx).height;
			var oLeft = document.getElementById(ctx).offsetLeft / core.domStyle.scale,
				oTop = document.getElementById(ctx).offsetTop / core.domStyle.scale;
			// 重置大地图和楼传地图的canvas位置
			if (ctx == "mapOnUi") core.relocateCanvas("mapOnUi", -240, -240);
			if (!noErase)
				core.clearMap(ctx);
			var horCenter = Math.floor(width / 2),
				uprCenter = Math.floor(height / 2);
			if (!newCreate) {
				horCenter += x;
				uprCenter += y;
			}
			var centerX = horCenter,
				centerY = uprCenter;
			var left = centerX,
				right = centerX,
				up = centerY,
				down = centerY;
			var used = {};
			var haveLayer = {};
			var nx = horCenter,
				ny = uprCenter;
			// 先把所在楼层绘制了
			if (layer == 0) {
				var nw = core.status.maps[floorId].width * 2 * scale,
					nh = core.status.maps[floorId].height * 2 * scale;
				core.setAlpha(ctx, 1);
				core.fillRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#000000");
				core.strokeRect(ctx, centerX - nw / 2, centerY - nh / 2, nw, nh, "#ffff22", 3 * scale);
				// 当前层上下楼显示
				if (!haveLayer[floorId]) {
					core.setAlpha(ctx, 1);
					var needLayer = core.status.layer[floorId];
					if (needLayer.top && needLayer.bottom) {
						core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale,
							centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
						core.drawIcon(ctx, "downFloor", centerX - nw / 2 + core.__SIZE__ * scale,
							centerY - nh / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
					}
					if (needLayer.top && !needLayer.bottom) {
						core.drawIcon(ctx, "upFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2,
							Math.min(nw, nh), Math.min(nw, nh));
					}
					if (!needLayer.top && needLayer.bottom) {
						core.drawIcon(ctx, "downFloor", centerX - Math.min(nw, nh) / 2, centerY - Math.min(nw, nh) / 2,
							Math.min(nw, nh), Math.min(nw, nh));
					}
					haveLayer[floorId] = true;
				}
				// 四侧最远位置
				if (left > centerX - nw / 2) left = centerX - nw / 2;
				if (right < centerX + nw / 2) right = centerX + nw / 2;
				if (down < centerY + nh / 2) down = centerY + nh / 2;
				if (up > centerY - nh / 2) up = centerY - nh / 2;
			}
			core.setAlpha(ctx, opacity);
			for (var route in map) { // 绘制楼层和线条
				var rouArr = route.split(",");
				// 检索路线及画线
				// 初始化
				centerX = nx;
				centerY = ny;
				var nowFloor = floorId || core.status.floorId;
				var nowLayer = 0;
				for (var one in rouArr) { // 一个一个检测
					var step = rouArr[one].split("_");
					var cx = step[1],
						cy = step[2];
					// 获得当前图块
					core.getMapBlocksObj(nowFloor, true);
					var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy];
					if (!nowBlock) continue;
					var toLoc = nowBlock.event.data.loc,
						toFloor = nowBlock.event.data.floorId;
					var needLayer = core.status.layer[toFloor];
					// 当前层宽度和高度
					var nw = core.status.maps[nowFloor].width * 2 * scale,
						nh = core.status.maps[nowFloor].height * 2 * scale;
					// 目标层宽度和高度
					var tw = core.status.maps[toFloor].width * 2 * scale,
						th = core.status.maps[toFloor].height * 2 * scale;
					// 将当前层变为toFloor
					nowFloor = toFloor;
					// 超范围不画
					if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height ||
						centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue;
					// 绘制toFloor层
					core.setAlpha(ctx, opacity);
					// 确定center 根据箭头自适配 同时绘制线条 我已经看不懂了
					if (!use3D && (step[0] == "top" || step[0] == "bottom") && layer == 0) break;
					if (step[0] == "top") nowLayer++;
					if (step[0] == "bottom") nowLayer--;
					if (step[0] == 'left') {
						var shouldTo = th / 2,
							realTo = toLoc[1] * 2 * scale;
						var shouldFrom = nh / 2,
							realFrom = step[2] * 2 * scale;
						if (nowLayer == layer) {
							core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom,
								centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX - nw / 2, centerY + realFrom - shouldFrom,
								centerX - nw / 2 - interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale);
						}
						centerX -= nw / 2 + tw / 2 + interval;
						centerY += shouldTo - realTo + realFrom - shouldFrom;
					}
					if (step[0] == 'right') {
						var shouldTo = th / 2,
							realTo = toLoc[1] * 2 * scale;
						var shouldFrom = nh / 2,
							realFrom = step[2] * 2 * scale;
						if (nowLayer == layer) {
							core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom,
								centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX + nw / 2, centerY + realFrom - shouldFrom,
								centerX + nw / 2 + interval, centerY + realFrom - shouldFrom, "#000000", 2 * scale);
						}
						centerX += nw / 2 + tw / 2 + interval;
						centerY += shouldTo - realTo + realFrom - shouldFrom;
					}
					if (step[0] == 'up') {
						var shouldTo = tw / 2,
							realTo = toLoc[0] * 2 * scale;
						var shouldFrom = nw / 2,
							realFrom = step[1] * 2 * scale;
						if (nowLayer == layer) {
							core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2,
								centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY - nh / 2,
								centerX + realFrom - shouldFrom, centerY - nh / 2 - interval, "#000000", 2 * scale);
						}
						centerY -= nh / 2 + th / 2 + interval;
						centerX += shouldTo - realTo + realFrom - shouldFrom;
					}
					if (step[0] == 'down') {
						var shouldTo = tw / 2,
							realTo = toLoc[0] * 2 * scale;
						var shouldFrom = nw / 2,
							realFrom = step[1] * 2 * scale;
						if (nowLayer == layer) {
							core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2,
								centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX + realFrom - shouldFrom, centerY + nh / 2,
								centerX + realFrom - shouldFrom, centerY + nh / 2 + interval, "#000000", 2 * scale);
						}
						centerY += nh / 2 + th / 2 + interval;
						centerX += shouldTo - realTo + realFrom - shouldFrom;
					}
					// 只有和目标层高度相同时才绘制
					if (nowLayer != layer) continue;
					// 超范围的不画
					if ((centerX > oLeft + x + width || centerX < oLeft + x || centerY > oTop + y + height ||
						centerY < oTop + y) && userScale && !fromMini && ctx != "mapOnUi") continue;
					// 四侧最远位置
					if (left > centerX - tw / 2) left = centerX - tw / 2;
					if (right < centerX + tw / 2) right = centerX + tw / 2;
					if (down < centerY + th / 2) down = centerY + th / 2;
					if (up > centerY - th / 2) up = centerY - th / 2;
					// 画过了不画
					if (used[toFloor]) continue;
					used[toFloor] = true;
					// 画地图格
					if (core.hasVisitedFloor(toFloor)) {
						core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#000000");
						core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale);
					} else {
						core.fillRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ff22ff");
						core.strokeRect(ctx, centerX - tw / 2, centerY - th / 2, tw, th, "#ffffff", 3 * scale);
						break;
					}
					// 上下楼显示
					if (haveLayer[toFloor]) continue;
					core.setAlpha(ctx, opacity);
					if (needLayer.top && needLayer.bottom) {
						core.drawIcon(ctx, "upFloor", centerX - core.__SIZE__ * scale,
							centerY - core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
						core.drawIcon(ctx, "downFloor", centerX - tw / 2 + core.__SIZE__ * scale,
							centerY - th / 2 + core.__SIZE__ * scale, core.__SIZE__ * scale, core.__SIZE__ * scale);
					}
					if (needLayer.top && !needLayer.bottom) {
						core.drawIcon(ctx, "upFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2,
							Math.min(tw, th), Math.min(tw, th));
					}
					if (!needLayer.top && needLayer.bottom) {
						core.drawIcon(ctx, "downFloor", centerX - Math.min(tw, th) / 2, centerY - Math.min(tw, th) / 2,
							Math.min(tw, th), Math.min(tw, th));
					}
					haveLayer[toFloor] = true;
				}
			}
			// 自动缩放
			if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) {
				scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64));
				var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, layer: layer, opacity: opacity, loop: loop };
				return core.drawFlyMap(ctx, x, y, width, height, floorId, con);
			}
			// 大地图和楼层地图自适配定位
			if (ctx == "mapOnUi" && !fromMini && (left - nx < -128 || right - nx > 128 ||
				up - ny < -128 || down - ny > 128)) {
				core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2);
			}
		};
		// 3D绘图
		this.draw3DFlyMap = function (ctx, x, y, width, height, floorId, config) {
			// 初始化配置项
			//***--- 初始化 同上一个初始化
			var fromUser = config.fromUser || false,
				oriFloor = config.oriFloor || core.status.floorId,
				scale = config.scale || null,
				interval = config.interval || (width / 24),
				deltaH = config.deltaH || (height / 8),
				noErase = config.noErase || false,
				fromMini = config.fromMini || false,
				loop = config.loop || defaultLoop,
				opacity = config.opacity || defaultOpacity,
				minorAlpha = config.minorAlpha || defaultMinorAlpha,
				reLeft = config.reLeft || -240,
				reTop = config.reTop || -240,
				clearCache = config.clearCache || false,
				map = config.map || null;
			//***--- 初始化
			map = map || core.getFlyMap(floorId, fromUser, oriFloor, loop, clearCache);
			// 当前层是否一个人在一层
			var alone = true;
			for (var route in map) {
				if (route.startsWith("left") || route.startsWith("right") ||
					route.startsWith("up") || route.startsWith("down")) {
					alone = false;
					break;
				}
			}
			// 是则增加一个先上再下的路径
			if (alone) {
				for (var route in map) {
					if (route.startsWith("top")) {
						var first = route.split(",")[0].split("_");
						break;
					}
				}
				var success = false;
				core.getMapBlocksObj(nowFloor, true);
				var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]];
				var toFloor = nowBlock.event.data.floorId;
				core.extractBlocks(toFloor);
				core.status.maps[toFloor].blocks.forEach(function (block) {
					var id = block.event.id;
					var x = block.x,
						y = block.y;
					var trigger = block.event.trigger;
					if (trigger != "changeFloor") return;
					if (id == "downFloor") {
						map["top_" + first[1] + "_" + first[2] + "," + "bottom_" + x + "_" + y] = floorId;
						success = true;
						return;
					}
				});
				// 添加先上再下失败 尝试先下再上
				if (!success) {
					for (var route in map) {
						if (route.startsWith("bottom")) {
							var first = route.split(",")[0].split("_");
							break;
						}
					}
					var nowBlock = core.status.mapBlockObjs[floorId][first[1] + "," + first[2]];
					var toFloor = nowBlock.event.data.floorId;
					core.extractBlocks(toFloor);
					core.status.maps[toFloor].blocks.forEach(function (block) {
						var id = block.event.id;
						var x = block.x,
							y = block.y;
						var trigger = block.event.trigger;
						if (trigger != "changeFloor") return;
						if (id == "upFloor") {
							map["bottom_" + first[1] + "_" + first[2] + "," + "top_" + x + "_" + y] = floorId;
							success = true;
							return;
						}
					});
				}
			}
			floorId = floorId || core.status.floorId;
			if (!floorId) return;
			flags.in3D = true;
			// 初始化
			// 获得排序过的楼层路径
			map = core.sortFloor(map);
			map = map.map;
			var userScale = true;
			var newCreate = false;
			if (!scale) {
				userScale = false;
				scale = scale || defaultScale;
			}
			if (!core.dymCanvas[ctx]) {
				core.createCanvas(ctx, x, y, width, height, 140);
				newCreate = true;
			}
			x = x || 0;
			y = y || 0;
			// 获得canvas属性
			width = width || document.getElementById(ctx).width;
			height = height || document.getElementById(ctx).height;
			// 重置canvas位置
			core.relocateCanvas(ctx, reLeft, reTop);
			if (!noErase)
				core.clearMap(ctx);
			var horCenter = Math.floor(width / 2),
				uprCenter = Math.floor(height / 2);
			if (!newCreate) {
				horCenter += x;
				uprCenter += y;
			}
			// 单元格的中心点 即水平线中点处
			var centerX = horCenter,
				centerY = uprCenter;
			var left = centerX,
				right = centerX,
				up = centerY,
				down = centerY;
			var used = {};
			var nx = horCenter,
				ny = uprCenter;
			// 开始绘制
			for (var i = 0; i < map.length; i++) {
				var route = map[i][0];
				var nowLayer = map[i][1];
				var everyLayer = 0;
				route = route.split(",");
				// 每条路线初始化
				centerX = horCenter;
				centerY = uprCenter;
				centerX += core.status.maps[floorId].height * scale * Math.SQRT2 / 4;
				if (flags.viewingLayer) {
					centerY += deltaH * flags.viewingLayer;
				}
				var nowFloor = floorId || core.status.floorId;
				for (var one = 0; one < route.length; one++) {
					var step = route[one].split("_");
					var cx = step[1],
						cy = step[2];
					// 检测高度，是否与nowLayer一致 不一致在处理完center以后不绘制
					if (step[0] == "top") everyLayer++;
					if (step[0] == "bottom") everyLayer--;
					// 获得当前图块
					core.getMapBlocksObj(nowFloor, true);
					var nowBlock = core.status.mapBlockObjs[nowFloor][cx + ',' + cy];
					if (!nowBlock) continue;
					var toLoc = nowBlock.event.data.loc,
						toFloor = nowBlock.event.data.floorId;
					// 当前层宽度和高度
					// 斜二测画法
					var nw = core.status.maps[nowFloor].width * 2 * scale,
						nh = core.status.maps[nowFloor].height * scale * Math.SQRT2 / 2;
					// 目标层宽度和高度
					var tw = core.status.maps[toFloor].width * 2 * scale,
						th = core.status.maps[toFloor].height * scale * Math.SQRT2 / 2;
					if (!(toLoc instanceof Array)) {
						toLoc = [Math.floor(tw / 4 / scale), Math.floor(th / 4 / scale)];
					}
					// 绘制当前层
					if (nowLayer == 0 && !used[floorId]) {
						core.setAlpha(ctx, 1);
						used[floorId] = true;
						var nowW = core.status.maps[floorId].width * 2 * scale;
						var nowH = core.status.maps[floorId].height * scale * Math.SQRT2 / 2;
						var nodes = [
							[centerX - nowW / 2 - nowH / 2, centerY + nowH / 2],
							[centerX + nowW / 2 - nowH / 2, centerY + nowH / 2],
							[centerX + nowW / 2 + nowH / 2, centerY - nowH / 2],
							[centerX - nowW / 2 + nowH / 2, centerY - nowH / 2]
						];
						core.fillPolygon(ctx, nodes, "#000000");
						core.strokePolygon(ctx, nodes, "#ffff22", 1.5 * scale);
						// 四侧最远位置
						if (left > centerX - nw / 2 - nh / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - nw / 2 - nh / 2;
						if (right < centerX + nw / 2 + nh / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + nw / 2 + nh / 2;
					}
					// 将当前层变为toFloor
					var fromFloor = nowFloor;
					nowFloor = toFloor;
					// 计算center 画同层间的线 我已经看不懂了
					// 设置不透明度
					if (nowLayer == (flags.viewingLayer || 0)) {
						core.setAlpha(ctx, opacity);
					} else {
						core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
					}
					if (step[0] == "left") {
						var shouldFrom = nh / 2,
							realFrom = cy * scale * Math.SQRT2 / 2;
						var shouldTo = th / 2,
							realTo = toLoc[1] * scale * Math.SQRT2 / 2;
						if (everyLayer == nowLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
							core.drawLine(ctx, centerX - nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom,
								centerX - nw / 2 - interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale);
						centerX -= nw / 2 + tw / 2 + interval + shouldTo - realTo + realFrom - shouldFrom;
						centerY += shouldTo - realTo + realFrom - shouldFrom;
					}
					if (step[0] == "right") {
						var shouldFrom = nh / 2,
							realFrom = cy * scale * Math.SQRT2 / 2;
						var shouldTo = th / 2,
							realTo = toLoc[1] * scale * Math.SQRT2 / 2;
						if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
							core.drawLine(ctx, centerX + nw / 2 + nh / 2 - realFrom, centerY + realFrom - shouldFrom,
								centerX + nw / 2 + interval + nh / 2 - realFrom, centerY + realFrom - shouldFrom, "#ffffff", 2 * scale);
						centerX += nw / 2 + tw / 2 + interval - (shouldTo - realTo + realFrom - shouldFrom);
						centerY += shouldTo - realTo + realFrom - shouldFrom;
					}
					if (step[0] == "up") {
						var shouldTo = tw / 2,
							realTo = toLoc[0] * scale * 2;
						var shouldFrom = nw / 2,
							realFrom = cx * scale * 2;
						if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
							core.drawLine(ctx, centerX + realFrom - shouldFrom + nh / 2, centerY - nh / 2, centerX + realFrom -
								shouldFrom + interval * Math.SQRT2 / 4 + nh / 2, centerY - nh / 2 - interval * Math.SQRT2 / 4, "#ffffff", 2 * scale);
						centerY -= nh / 2 + th / 2 + interval * Math.SQRT2 / 4;
						centerX += shouldTo - realTo + realFrom - shouldFrom + (nh / 2 + th / 2 + interval * Math.SQRT2 / 4);
					}
					if (step[0] == "down") {
						var shouldTo = tw / 2,
							realTo = toLoc[0] * scale * 2;
						var shouldFrom = nw / 2,
							realFrom = cx * scale * 2;
						if (nowLayer == everyLayer && !used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy])
							core.drawLine(ctx, centerX + realFrom - shouldFrom - nh / 2, centerY + nh / 2, centerX + realFrom -
								shouldFrom - interval * Math.SQRT2 / 4 - nh / 2, centerY + nh / 2 + interval * Math.SQRT2 / 4, "#ffffff", 2 * scale);
						centerY += nh / 2 + th / 2 + interval * Math.SQRT2 / 4;
						centerX += shouldTo - realTo + realFrom - shouldFrom - (nh / 2 + th / 2 + interval * Math.SQRT2 / 4);
					}
					if (step[0] == "top") {
						centerY -= deltaH;
					}
					if (step[0] == "bottom") {
						centerY += deltaH;
					}
					if (everyLayer == nowLayer && step[0] != "top" && step[0] != "bottom") {
						used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
						used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
					}
					if (everyLayer != nowLayer) continue;
					// 四侧最远位置
					if (!flags.viewingLayer) {
						if (left > centerX - tw / 2 - th / 2 && nowLayer == (flags.viewingLayer || 0)) left = centerX - tw / 2 - th / 2;
						if (right < centerX + tw / 2 + th / 2 && nowLayer == (flags.viewingLayer || 0)) right = centerX + tw / 2 + th / 2;
						if (down < centerY + th / 2 && nowLayer == (flags.viewingLayer || 0)) down = centerY + th / 2;
						if (up > centerY - th / 2 && nowLayer == (flags.viewingLayer || 0)) up = centerY - th / 2;
					}
					// 不同高度层之间的连线
					if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) {
						core.setAlpha(ctx, opacity * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
						if (step[0] == "top") {
							core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX, centerY + deltaH, centerX, centerY, "#000000", 2 * scale);
							used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
							used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
						}
					}
					if (!used[toFloor]) {
						used[toFloor] = true;
						// 设置不透明度
						if (nowLayer == (flags.viewingLayer || 0)) {
							core.setAlpha(ctx, opacity);
						} else {
							core.setAlpha(ctx, minorAlpha * Math.max(0, 1 - 0.34 * Math.abs(nowLayer - (flags.viewingLayer || 0))));
						}
						// 画地图
						var nodes = [
							[centerX - tw / 2 - th / 2, centerY + th / 2], // 左下
							[centerX + tw / 2 - th / 2, centerY + th / 2], // 右下
							[centerX + tw / 2 + th / 2, centerY - th / 2], // 右上
							[centerX - tw / 2 + th / 2, centerY - th / 2] // 左上
						];
						if (core.hasVisitedFloor(toFloor)) {
							core.fillPolygon(ctx, nodes, "#000000");
						} else {
							core.fillPolygon(ctx, nodes, "#ff22ff");
						}
						core.strokePolygon(ctx, nodes, "#ffffff", 1.5 * scale);
					}
					// 不同高度层之间的连线
					if (!used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy]) {
						if (step[0] == "bottom") {
							core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#ffffff", 5 * scale);
							core.drawLine(ctx, centerX, centerY, centerX, centerY - deltaH, "#000000", 2 * scale);
							used[fromFloor + "_" + toFloor + "_" + cx + "_" + cy] = true;
							used[toFloor + "_" + fromFloor + "_" + toLoc[0] + "_" + toLoc[1]] = true;
						}
					}
				}
			}
			// 自动缩放
			if ((right - left > core.__PIXELS__ - 64 || down - up > core.__PIXELS__ - 64) && !userScale && !fromMini) {
				scale = 1 / (Math.max(right - left, down - up) / (core.__PIXELS__ - 64));
				var con = { fromUser: fromUser, oriFloor: oriFloor, scale: scale, interval: interval * scale, opacity: opacity, loop: loop, use3D: true };
				return core.draw3DFlyMap(ctx, x, y, width, height, floorId, con);
			}
			// 大地图和楼层地图自适配定位
			if (ctx == "mapOnUi")
				core.relocateCanvas("mapOnUi", -240 + (-left - right + 2 * nx) / 2, -240 + (-up - down + 2 * ny) / 2);
		};
		// 不同高度楼层排序
		this.sortFloor = function (map) {
			map = map || core.getFlyMap(null, true);
			var totalLayer = 1,
				topLayer = 0,
				bottomLayer = 0,
				nowLayer = 0;
			// 拆分map
			for (var i in map) {
				var route = i.split(",");
				nowLayer = 0;
				for (var one in route) {
					var step = route[one].split("_");
					// 层数处理 并记录每一层的层数
					if (step[0] == "top") {
						nowLayer++;
						map[i] = nowLayer + "_" + map[i];
						if (nowLayer > topLayer) topLayer = nowLayer;
					}
					if (step[0] == "bottom") {
						nowLayer--;
						map[i] = nowLayer + "_" + map[i];
						if (nowLayer < bottomLayer) bottomLayer = nowLayer;
					}
				}
			}
			// 总层数
			totalLayer = topLayer - bottomLayer + 1;
			// 按楼层高度由低到高排序
			// 先变成数组
			var mapArr = [];
			for (var one in map) {
				mapArr.push([one, parseInt(map[one]) || 0]);
			}
			// 再sort
			mapArr.sort(function (a, b) { return a[1] - b[1]; });
			return { map: mapArr, totalLayer: totalLayer, top: topLayer, bottom: bottomLayer };
		};
		// 由方向获得楼层坐标
		this.getFloorByDirection = function (direction, floorId) {
			floorId = floorId || core.status.floorId;
			var route = core.getFlyMap(floorId);
			for (var step in route) {
				if (step.indexOf(direction) >= 0) {
					return route[step];
				}
			}
			return null;
		};
		////// 转换楼层结束的事件 检查小地图 //////
		var originAfterChangeFloor = core.events.afterChangeFloor;
		events.prototype.afterChangeFloor = function (floorId) {
			if (!flags.__useMinimap__ || core.isReplaying()) {
				core.deleteCanvas("minimap");
				core.deleteCanvas("mapArrow");
				core.unregisterAction("ondown", "closeMinimap");
				core.unregisterAction("ondown", "openMinimap");
				return originAfterChangeFloor.call(core.events, floorId);
			}
			if (main.mode != 'play') return;
			this.eventdata.afterChangeFloor(floorId);
			// 防止小地图出问题
			core.unregisterAction("ondown", "closeMinimap");
			core.unregisterAction("ondown", "openMinimap");
			// 切换小地图
			core.checkMinimap(true, true);
			return;
		};
		////// 瞬间移动 检查小地图 //////
		var originMoveDirectly = core.control.moveDirectly;
		control.prototype.moveDirectly = function (destX, destY, ignoreSteps) {
			if (!flags.__useMinimap__ || core.isReplaying())
				return originMoveDirectly.call(core.control, destX, destY, ignoreSteps);
			var canMoveDirectly = this.controldata.moveDirectly(destX, destY, ignoreSteps);
			if (canMoveDirectly) core.checkMinimap();
			return canMoveDirectly;
		};
		////// 每移动一格后执行的事件 检查小地图 //////
		var originMoveOneStep = core.control.moveOneStep;
		control.prototype.moveOneStep = function (callback) {
			if (!flags.__useMinimap__ || core.isReplaying())
				return originMoveOneStep.call(core.control, callback);
			this.controldata.moveOneStep(callback);
			core.checkMinimap();
		};
		// 检查小地图开闭情况 改变小地图位置
		this.checkMinimap = function (fromUser, reDraw) {
			if (!flags.__useMinimap__ || core.isReplaying()) {
				core.unregisterAction("ondown", "closeMinimap");
				core.unregisterAction("ondown", "openMinimap");
				core.deleteCanvas("mapArrow");
				core.deleteCanvas("minimap");
				return;
			}
			reDraw = reDraw || false;
			// 是否重绘
			if (reDraw) {
				if (flags.minimap) core.drawMinimap(flags.__onLeft__);
				else core.drawClosedMap(flags.__onLeft__);
			}
			var hx = core.status.hero.loc.x;
			var opened = flags.minimap;
			var onLeft = hx >= Math.ceil(core.__SIZE__ / 3 * 2);
			fromUser = fromUser || false; // 开关小地图相关
			if (!flags.__onLeft__) flags.__onLeft__ = false;
			// 如果地图上没有小地图 画到右边
			if (!core.dymCanvas.mapArrow && !core.dymCanvas.minimap && !reDraw) {
				if (flags.minimap) core.drawMinimap();
				else core.drawClosedMap();
				flags.__onLeft__ = false;
			}
			// 人物在中间 不执行
			if (hx >= Math.ceil(core.__SIZE__ / 3 + core.bigmap.offsetX / 32) &&
				hx <= Math.floor(core.__SIZE__ / 3 * 2 + core.bigmap.offsetX / 32)) return;
			// 重定位画布 和 翻转
			// 挪到右边
			if (!onLeft && (flags.__onLeft__ || fromUser)) {
				flags.__onLeft__ = false;
				if (opened) {
					core.relocateCanvas("minimap", core.__PIXELS__ - 120, 0);
					core.relocateCanvas("mapArrow", core.__PIXELS__ - 140, 0);
					document.getElementById('mapArrow').style.transform = 'none';
				} else {
					core.relocateCanvas("minimap", core.__PIXELS__, 0);
					core.relocateCanvas("mapArrow", core.__PIXELS__ - 20, 0);
					document.getElementById('mapArrow').style.transform = 'none';
				}
			}
			// 挪到左边
			if (onLeft && (!flags.__onLeft__ || fromUser)) {
				flags.__onLeft__ = true;
				if (opened) {
					core.relocateCanvas("minimap", 0, 0);
					core.relocateCanvas("mapArrow", 120, 0);
					document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
				} else {
					core.relocateCanvas("minimap", -120, 0);
					core.relocateCanvas("mapArrow", 0, 0);
					document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
				}
			}
		};
		// 点击小地图的action
		this.registerMinimapAction = function (open) {
			if (!open) {
				core.registerAction("ondown", "closeMinimap", function (x, y, px, py) {
					if (!flags.__onLeft__) {
						if (px >= core.__PIXELS__ - 140 && px <= core.__PIXELS__ - 120 &&
							py >= 0 && py <= 120) {
							core.closeMinimap();
							core.unregisterAction("ondown", "closeMinimap");
							return true;
						}
						if (px >= core.__PIXELS__ - 120 && py <= 120) {
							core.playSound("打开界面");
							core.drawTotalMap();
							return true;
						}
					} else {
						if (px >= 120 && px <= 140 && py >= 0 && py <= 120) {
							core.closeMinimap();
							core.unregisterAction("ondown", "closeMinimap");
							return true;
						}
						if (px <= 120 && py <= 120) {
							core.playSound("打开界面");
							core.drawTotalMap();
							return true;
						}
					}
				}, 10);
			} else {
				core.registerAction("ondown", "openMinimap", function (x, y, px, py) {
					if (!flags.__onLeft__) {
						if (px >= core.__PIXELS__ - 20 && py <= 120) {
							core.openMinimap();
							core.unregisterAction("ondown", "openMinimap");
							return true;
						}
					} else {
						if (px <= 20 && py <= 120) {
							core.openMinimap();
							core.unregisterAction("ondown", "openMinimap");
							return true;
						}
					}
				}, 10);
			}
		};
		// 地图上的小地图
		this.drawMinimap = function (toLeft) {
			if (!flags.__useMinimap__ || !core.status.thisMap) {
				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.status.thisMap) {
				core.deleteCanvas("mapArrow");
				core.deleteCanvas("minimap");
				return;
			}
			// var scale = 1.3 / core.status.thisMap.width * 15 * (flags.userScale || 1);
			// if (1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1) < scale)
			// 	scale = 1.3 / core.status.thisMap.height * 15 * (flags.userScale || 1);
			// // 绘制
			// core.createCanvas("minimap", core.__PIXELS__, 0, 120, 120, 100);
			// core.createCanvas("mapArrow", core.__PIXELS__ - 20, 0, 20, 120, 100);
			// core.clearMap("minimap");
			// core.clearMap("mapArrow");
			// if (toLeft) {
			// 	core.relocateCanvas("minimap", -120, 0);
			// 	core.relocateCanvas("mapArrow", 0, 0);
			// 	document.getElementById('mapArrow').style.transform = 'rotateY(180deg)';
			// }
			// // 黑色底
			// core.fillRect("minimap", 0, 0, 120, 120, [0, 0, 0, 0.6]);
			// var config = { fromUser: true, oriFloor: core.status.floorId, scale: scale, interval: 10, noErase: true, fromMini: true };
			// core.drawFlyMap("minimap", 0, 0, 120, 120, core.status.floorId, config);
			// // 向左箭头
			// core.fillRect("mapArrow", 0, 0, 20, 120, [230, 230, 230, 0.9]);
			// core.drawLine("mapArrow", 0, 20, 20, 20, [100, 100, 100, 0.9], 2);
			// core.drawLine("mapArrow", 0, 100, 20, 100, [100, 100, 100, 0.9], 2);
			// core.setTextAlign("mapArrow", "center");
			// core.fillText("mapArrow", "<", 10, 67, [100, 100, 100, 0.9], "20px Verdana");
			// core.registerMinimapAction(true);
		};
		// 打开小地图
		this.openMinimap = function () {
			if (!flags.__useMinimap__) {
				core.deleteCanvas("mapArrow");
				core.deleteCanvas("minimap");
				return;
			}
			var onLeft = flags.__onLeft__;
			var frame = 0;
			var x = 120 + core.__PIXELS__,
				a = 0.096,
				speed = 4.8;
			if (onLeft) {
				x = 140;
				a = -a;
				speed = -speed;
			}
			var interval = setInterval(function () {
				core.relocateCanvas("mapArrow", x - 140, 0);
				if (!flags.__onLeft__)
					core.relocateCanvas("minimap", x - 120, 0);
				else core.relocateCanvas("minimap", x - 260, 0);
				speed -= a;
				x -= speed;
				if (frame == 50) {
					flags.minimap = true;
					clearInterval(interval);
					core.drawMinimap(onLeft);
					core.checkMinimap(true);
				}
				frame++;
			}, 20);
		};
		// 大地图
		this.drawTotalMap = function (floorId) {
			floorId = floorId || core.status.floorId;
			core.status.event.id = "totalMap";
			core.lockControl();
			if (!flags.viewingLayer) flags.viewingLayer = 0;
			var loop = 5;
			if (flags.worldMap) loop = core.floorIds.length;
			// 大地图时点击和键盘操作
			core.registerAction("ondown", "onDownTmap", function (x, y) {
				if (core.status.event.id == "totalMap") {
					if (y < 1 && x <= Math.floor(core.__SIZE__ / 2) - 2) { // 上移一层
						if (flags.viewingLayer < core.sortFloor().top) {
							flags.viewingLayer++;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					if (y < 1 && x >= Math.ceil(core.__SIZE__ / 2) + 1) { // 下移一层
						if (flags.viewingLayer > core.sortFloor().bottom) {
							flags.viewingLayer--;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					if (y < 1 && x < Math.ceil(core.__SIZE__ / 2) + 1 && x > Math.floor(core.__SIZE__ / 2) - 2) {
						// 区域地图
						if (flags.worldMap) {
							flags.worldMap = false;
							flags.viewingLayer = 0;
						} else flags.worldMap = true;
						core.playSound('光标移动');
						core.drawTotalMap();
						return true;
					}
					if (y >= core.__SIZE__ - 1 && x <= Math.floor(core.__SIZE__ / 2)) { // 3D
						if (core.can3D(floorId) && !flags.in3D) flags.use3D = true;
						if (flags.in3D) flags.use3D = false;
						flags.mapHint = false;
						core.playSound('光标移动');
						core.drawTotalMap();
						return true;
					}
					if (y >= core.__SIZE__ - 1 && x >= Math.ceil(core.__SIZE__ / 2)) { // hint
						if (flags.in3D) {
							if (!flags.mapHint) flags.mapHint = true;
							else flags.mapHint = false;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					flags.viewingLayer = 0;
					core.playSound("取消")
					core.deleteCanvas("mapOnUi");
					core.deleteCanvas("back");
					core.deleteCanvas("tips");
					core.closePanel();
					core.unregisterAction("ondown", "onDownTmap");
					return true;
				}
			}, 110);
			core.registerAction("keyUp", "keyUpTmap", function (keycode) {
				if (core.status.event.id == "totalMap") {
					if (keycode == 33) { // PgUp
						if (flags.viewingLayer < core.sortFloor().top) {
							flags.viewingLayer++;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					if (keycode == 34) { // PgDn
						if (flags.viewingLayer > core.sortFloor().bottom) {
							flags.viewingLayer--;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					if (keycode == 90) { // Z
						if (core.can3D(floorId) && !flags.in3D) flags.use3D = true;
						if (flags.in3D) flags.use3D = false;
						flags.mapHint = false;
						core.playSound('光标移动');
						core.drawTotalMap();
						return true;
					}
					if (keycode == 84) { // T
						if (flags.in3D) {
							if (!flags.mapHint) flags.mapHint = true;
							else flags.mapHint = false;
							core.playSound('光标移动');
							core.drawTotalMap();
						}
						return true;
					}
					if (keycode == 87) { // W
						if (flags.worldMap) {
							flags.worldMap = false;
							flags.viewingLayer = 0;
						} else flags.worldMap = true;
						core.playSound('光标移动');
						core.drawTotalMap();
						return true;
					}
					flags.viewingLayer = 0;
					core.playSound("取消")
					core.deleteCanvas("mapOnUi");
					core.deleteCanvas("back");
					core.deleteCanvas("tips");
					core.closePanel();
					core.unregisterAction("keyUp", "keyUpTmap");
					return true;
				}
			}, 110);
			// 开始画
			core.createCanvas("mapOnUi", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 150);
			core.createCanvas("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, 140);
			core.createCanvas("tips", 0, 0, core.__PIXELS__, core.__PIXELS__, 160);
			var ctx = document.getElementById("tips").getContext("2d");
			ctx.shadowOffsetX = 0;
			ctx.shadowOffsetY = 0;
			ctx.shadowBlur = 5;
			ctx.shadowColor = "rgba(100, 100, 255, 1)";
			core.fillRect("back", -240, -240, core.__PIXELS__ + 480, core.__PIXELS__ + 480, [0, 0, 0, 0.9]);
			core.fillRect("tips", 0, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]);
			core.fillRect("tips", core.__PIXELS__ / 2 + 50, 0, core.__PIXELS__ / 2 - 50, 32, [200, 200, 200, 0.8]);
			core.fillRect("tips", core.__PIXELS__ / 2 - 46, 0, 92, 32, [200, 200, 200, 0.8]);
			core.drawLine("tips", 0, 32, core.__PIXELS__ / 2 - 50, 32, [50, 50, 50, 0.8], 3);
			core.drawLine("tips", core.__PIXELS__ / 2 + 50, 32, core.__PIXELS__, 32, [50, 50, 50, 0.8], 3);
			core.drawLine("tips", core.__PIXELS__ / 2 - 46, 32, core.__PIXELS__ / 2 + 46, 32, [50, 50, 50, 0.8], 3);
			core.fillRect("tips", 0, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]);
			core.fillRect("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__, core.__PIXELS__ / 2 - 5, -32, [200, 200, 200, 0.8]);
			core.drawLine("tips", 0, core.__PIXELS__ - 32, core.__PIXELS__ / 2 - 5, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3);
			core.drawLine("tips", core.__PIXELS__ / 2 + 5, core.__PIXELS__ - 32, core.__PIXELS__, core.__PIXELS__ - 32, [50, 50, 50, 0.8], 3);
			core.setTextAlign("tips", "center");
			core.fillText("tips", "上移一层", core.__PIXELS__ / 4 - 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			core.fillText("tips", "下移一层", core.__PIXELS__ / 4 * 3 + 23, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			core.fillText("tips", flags.worldMap ? "小地图" : "区域地图", core.__PIXELS__ / 2, 24, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			core.drawFlyMap("mapOnUi", 240, 240, core.__PIXELS__, core.__PIXELS__,
				floorId, { fromUser: true, opacity: 1, oriFloor: floorId, noErase: true, use3D: flags.use3D, layer: flags.viewingLayer, loop: loop, clearCache: true });
			if (flags.in3D)
				core.fillText("tips", "参考线（T）", core.__PIXELS__ / 4 * 3, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			if (core.can3D(floorId) && !flags.in3D)
				core.fillText("tips", "3D模式（Z）", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			if (flags.in3D)
				core.fillText("tips", "2D模式（Z）", core.__PIXELS__ / 4, core.__PIXELS__ - 8, [255, 255, 255, 0.8], "24px " + core.status.globalAttribute.font);
			if (flags.mapHint) {
				core.drawLine("back", 240, 240 + core.__PIXELS__, 240 + core.__PIXELS__, 240, [100, 100, 240, 0.4], 2);
				core.drawLine("back", 240 + core.__PIXELS__ / 2, 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, [100, 100, 240, 0.4], 2);
				core.drawLine("back", 240, 240 + core.__PIXELS__ / 2, 240 + core.__PIXELS__, 240 + core.__PIXELS__ / 2, [100, 100, 240, 0.4], 2);
			}
		};
	},
    "advancedAnimation": function () {
		// -------------------- 插件说明 -------------------- //
		// github仓库：https://github.com/unanmed/animate
		// npm包名：mutate-animate
		// npm地址：https://www.npmjs.com/package/mutate-animate

		// 是否开启本插件，默认启用；将此改成 false 将禁用本插件。
		var __enable = true;

		if (main.replayChecking) __enable = false;
		if (!__enable) {
			core.plugin.animate = {};
			this.tickerSet = new Set();
			this.deleteAllAnis = () => { };
			return;
		}

		/** 该集合中的所有Ticker在跨层时需要被摧毁 */
		this.tickerSet = new Set();

		/** 对Map中所有Ticker执行摧毁事件 */
		this.deleteAllAnis = function () {
			core.plugin.tickerSet.forEach((ticker) => ticker.destroy());
		}

		let w = [];
		const k = (n) => {
			for (const i of w)
				if (i.status === "running")
					try {
						for (const t of i.funcs)
							t(n - i.startTime);
					} catch (t) {
						i.destroy(), console.error(t);
					}
			requestAnimationFrame(k);
		};
		requestAnimationFrame(k);

		/** Ticker类 */
		class I {
			constructor() {
				this.funcs = /* @__PURE__ */ new Set();
				this.status = "stop";
				this.startTime = 0;
				this.status = "running", w.push(this), requestAnimationFrame((i) => this.startTime = i);
			}
			add(i) {
				return this.funcs.add(i), this;
			}
			remove(i) {
				return this.funcs.delete(i), this;
			}
			clear() {
				this.funcs.clear();
			}
			destroy() {
				core.plugin.tickerSet.delete(this);
				this.clear(), this.stop();
			}
			stop() {
				this.status = "stop", w = w.filter((i) => i !== this);
			}
		}
		/** AnimationBase类 */
		class F {
			constructor() {
				this.timing = Date.now;
				this.relation = "absolute";
				this.easeTime = 0;
				this.applying = {};
				this.getTime = Date.now;
				const ticker = new I();
				this.ticker = ticker;
				this.value = {};
				this.listener = {};
				this.timing = (i) => i;
			}
			async all() {
				if (Object.values(this.applying).every((i) => i === !0))
					throw new ReferenceError("There is no animates to be waited.");
				await new Promise((i) => {
					const t = () => {
						Object.values(this.applying).every((e) => e === !1) && (this.unlisten("end", t), i("all animated."));
					};
					this.listen("end", t);
				});
			}
			async n(i) {
				const t = Object.values(this.applying).filter((s) => s === !0).length;
				if (t < i)
					throw new ReferenceError(
						`You are trying to wait ${i} animate, but there are only ${t} animate animating.`
					);
				let e = 0;
				await new Promise((s) => {
					const r = () => {
						e++, e === i && (this.unlisten("end", r), s(`${i} animated.`));
					};
					this.listen("end", r);
				});
			}
			async w(i) {
				if (this.applying[i] === !1)
					throw new ReferenceError(`The ${i} animate is not animating.`);
				await new Promise((t) => {
					const e = () => {
						this.applying[i] === !1 && (this.unlisten("end", e), t(`${i} animated.`));
					};
					this.listen("end", e);
				});
			}
			listen(i, t) {
				var e, s;
				(s = (e = this.listener)[i]) != null || (e[i] = []), this.listener[i].push(t);
			}
			unlisten(i, t) {
				const e = this.listener[i].findIndex((s) => s === t);
				if (e === -1)
					throw new ReferenceError(
						"You are trying to remove a nonexistent listener."
					);
				this.listener[i].splice(e, 1);
			}
			hook(...i) {
				const t = Object.entries(this.listener).filter(
					(e) => i.includes(e[0])
				);
				for (const [e, s] of t)
					for (const r of s)
						r(this, e);
			}
		}

		function y(n) {
			return n != null;
		}
		async function R(n) {
			return new Promise((i) => setTimeout(i, n));
		}
		/** Animation类 */
		class j extends F {
			constructor() {
				super();
				this.shakeTiming;
				this.path;
				this.multiTiming;
				this.value = {};
				this.size = 1;
				this.angle = 0;
				this.targetValue = {
					system: {
						move: [0, 0],
						moveAs: [0, 0],
						resize: 0,
						rotate: 0,
						shake: 0,
						/** @type {number[]} */
						"@@bind": []
					},
					custom: {}
				};
				this.animateFn = {
					system: {
						move: [() => { }, () => { }],
						moveAs: () => { },
						resize: () => { },
						rotate: () => { },
						shake: () => { },
						"@@bind": () => { }
					},
					custom: {}
				};
				this.ox = 0;
				this.oy = 0;
				this.sx = 0;
				this.sy = 0;
				this.bindInfo = [];
				this.timing = (t) => t, this.shakeTiming = (t) => t, this.multiTiming = (t) => [t, t], this.path = (t) => [t, t], this.applying = {
					move: !1,
					scale: !1,
					rotate: !1,
					shake: !1
				}, this.ticker.add(() => {
					const { running: t } = this.listener;
					if (y(t))
						for (const e of t)
							e(this, "running");
				});
			}
			get x() {
				return this.ox + this.sx;
			}
			get y() {
				return this.oy + this.sy;
			}
			mode(t, e = !1) {
				return typeof t(0) == "number" ? e ? this.shakeTiming = t : this.timing = t : this.multiTiming = t, this;
			}
			time(t) {
				return this.easeTime = t, this;
			}
			relative() {
				return this.relation = "relative", this;
			}
			absolute() {
				return this.relation = "absolute", this;
			}
			bind(...t) {
				return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = t, this;
			}
			unbind() {
				return this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.bindInfo = [], this;
			}
			move(t, e) {
				return this.applying.move && this.end(!0, "move"), this.applySys("ox", t, "move"), this.applySys("oy", e, "move"), this;
			}
			rotate(t) {
				return this.applySys("angle", t, "rotate"), this;
			}
			scale(t) {
				return this.applySys("size", t, "resize"), this;
			}
			shake(t, e) {
				this.applying.shake === !0 && this.end(!0, "shake"), this.applying.shake = !0;
				const { easeTime: s, shakeTiming: r } = this, l = this.getTime();
				if (this.hook("start", "shakestart"), s <= 0)
					return this.end(!1, "shake"), this;
				const a = () => {
					const c = this.getTime() - l;
					if (c > s) {
						this.ticker.remove(a), this.applying.shake = !1, this.sx = 0, this.sy = 0, this.hook("end", "shakeend");
						return;
					}
					const h = c / s,
						m = r(h);
					this.sx = m * t, this.sy = m * e;
				};
				return this.ticker.add(a), this.animateFn.system.shake = a, this;
			}
			moveAs(t) {
				this.applying.moveAs && this.end(!0, "moveAs"), this.applying.moveAs = !0, this.path = t;
				const { easeTime: e, relation: s, timing: r } = this, l = this.getTime(), [a, u] = [this.x, this.y], [c, h] = (() => {
					if (s === "absolute")
						return t(1); {
						const [d, f] = t(1);
						return [a + d, u + f];
					}
				})();
				if (this.hook("start", "movestart"), e <= 0)
					return this.end(!1, "moveAs"), this;
				const m = () => {
					const f = this.getTime() - l;
					if (f > e) {
						this.end(!0, "moveAs");
						return;
					}
					const g = f / e,
						[v, x] = t(r(g));
					s === "absolute" ? (this.ox = v, this.oy = x) : (this.ox = a + v, this.oy = u + x);
				};
				return this.ticker.add(m), this.animateFn.system.moveAs = m, this.targetValue.system.moveAs = [c, h], this;
			}
			register(t, e) {
				if (typeof this.value[t] == "number")
					return this.error(
						`Property ${t} has been regietered twice.`,
						"reregister"
					);
				this.value[t] = e, this.applying[t] = !1;
			}
			apply(t, e) {
				this.applying[t] === !0 && this.end(!1, t), t in this.value || this.error(
					`You are trying to execute nonexistent property ${t}.`
				), this.applying[t] = !0;
				const s = this.value[t],
					r = this.getTime(),
					{ timing: l, relation: a, easeTime: u } = this,
					c = a === "absolute" ? e - s : e;
				if (this.hook("start"), u <= 0)
					return this.end(!1, t), this;
				const h = () => {
					const d = this.getTime() - r;
					if (d > u) {
						this.end(!1, t);
						return;
					}
					const f = d / u,
						g = l(f);
					this.value[t] = s + g * c;
				};
				return this.ticker.add(h), this.animateFn.custom[t] = h, this.targetValue.custom[t] = c + s, this;
			}
			applyMulti() {
				this.applying["@@bind"] === !0 && this.end(!1, "@@bind"), this.applying["@@bind"] = !0;
				const t = this.bindInfo,
					e = t.map((h) => this.value[h]),
					s = this.getTime(),
					{ multiTiming: r, relation: l, easeTime: a } = this,
					u = r(1);
				if (u.length !== e.length)
					throw new TypeError(
						`The number of binded animate attributes and timing function returns's length does not match. binded: ${t.length}, timing: ${u.length}`
					);
				if (this.hook("start"), a <= 0)
					return this.end(!1, "@@bind"), this;
				const c = () => {
					const m = this.getTime() - s;
					if (m > a) {
						this.end(!1, "@@bind");
						return;
					}
					const d = m / a,
						f = r(d);
					t.forEach((g, v) => {
						l === "absolute" ? this.value[g] = f[v] : this.value[g] = e[v] + f[v];
					});
				};
				return this.ticker.add(c), this.animateFn.custom["@@bind"] = c, this.targetValue.system["@@bind"] = u, this;
			}
			applySys(t, e, s) {
				s !== "move" && this.applying[s] === !0 && this.end(!0, s), this.applying[s] = !0;
				const r = this[t],
					l = this.getTime(),
					a = this.timing,
					u = this.relation,
					c = this.easeTime,
					h = u === "absolute" ? e - r : e;
				if (this.hook("start", `${s}start`), c <= 0)
					return this.end(!0, s);
				const m = () => {
					const f = this.getTime() - l;
					if (f > c) {
						this.end(!0, s);
						return;
					}
					const g = f / c,
						v = a(g);
					this[t] = r + h * v, t !== "oy" && this.hook(s);
				};
				this.ticker.add(m), t === "ox" ? this.animateFn.system.move[0] = m : t === "oy" ? this.animateFn.system.move[1] = m : this.animateFn.system[s] = m, s === "move" ? (t === "ox" && (this.targetValue.system.move[0] = h + r), t === "oy" && (this.targetValue.system.move[1] = h + r)) : s !== "shake" && (this.targetValue.system[s] = h + r);
			}
			error(t, e) {
				throw e === "repeat" ? new Error(
					`Cannot execute the same animation twice. Info: ${t}`
				) : e === "reregister" ? new Error(
					`Cannot register an animated property twice. Info: ${t}`
				) : new Error(t);
			}
			end(t, e) {
				if (t === !0)
					if (this.applying[e] = !1, e === "move" ? (this.ticker.remove(this.animateFn.system.move[0]), this.ticker.remove(this.animateFn.system.move[1])) : e === "moveAs" ? this.ticker.remove(this.animateFn.system.moveAs) : e === "@@bind" ? this.ticker.remove(this.animateFn.system["@@bind"]) : this.ticker.remove(
						this.animateFn.system[e]
					), e === "move") {
						const [s, r] = this.targetValue.system.move;
						this.ox = s, this.oy = r, this.hook("moveend", "end");
					} else if (e === "moveAs") {
						const [s, r] = this.targetValue.system.moveAs;
						this.ox = s, this.oy = r, this.hook("moveend", "end");
					} else
						e === "rotate" ? (this.angle = this.targetValue.system.rotate, this.hook("rotateend", "end")) : e === "resize" ? (this.size = this.targetValue.system.resize, this.hook("resizeend", "end")) : e === "@@bind" ? this.bindInfo.forEach((r, l) => {
							this.value[r] = this.targetValue.system["@@bind"][l];
						}) : (this.sx = 0, this.sy = 0, this.hook("shakeend", "end"));
				else
					this.applying[e] = !1, this.ticker.remove(this.animateFn.custom[e]), this.value[e] = this.targetValue.custom[e], this.hook("end");
			}
		}
		class O extends F {
			constructor() {
				super();
				this.now = {};
				this.target = {};
				this.transitionFn = {};
				this.value = undefined;
				this.handleSet = (t, e, s) => (this.transition(e, s), !0);
				this.handleGet = (t, e) => this.now[e];
				this.timing = (t) => t, this.value = new Proxy(this.target, {
					set: this.handleSet,
					get: this.handleGet
				});
			}
			mode(t) {
				return this.timing = t, this;
			}
			time(t) {
				return this.easeTime = t, this;
			}
			relative() {
				return this.relation = "relative", this;
			}
			absolute() {
				return this.relation = "absolute", this;
			}
			transition(t, e) {
				if (e === this.target[t])
					return this;
				if (!y(this.now[t]))
					return this.now[t] = e, this;
				this.applying[t] && this.end(t, !0), this.applying[t] = !0, this.hook("start");
				const s = this.getTime(),
					r = this.easeTime,
					l = this.timing,
					a = this.now[t],
					u = e + (this.relation === "absolute" ? 0 : a),
					c = u - a;
				this.target[t] = u;
				const h = () => {
					const d = this.getTime() - s;
					if (d >= r) {
						this.end(t);
						return;
					}
					const f = d / r;
					this.now[t] = l(f) * c + a, this.hook("running");
				};
				return this.transitionFn[t] = h, this.ticker.add(h), r <= 0 ? (this.end(t), this) : this;
			}
			end(t, e = !1) {
				const s = this.transitionFn[t];
				if (!y(s))
					throw new ReferenceError(
						`You are trying to end an ended transition: ${t}`
					);
				this.ticker.remove(this.transitionFn[t]), delete this.transitionFn[t], this.applying[t] = !1, this.hook("end"), e || (this.now[t] = this.target[t]);
			}
		}
		const T = (...n) => n.reduce((i, t) => i + t, 0),
			b = (n) => {
				if (n === 0)
					return 1;
				let i = n;
				for (; n > 1;)
					n--, i *= n;
				return i;
			},
			A = (n, i) => Math.round(b(i) / (b(n) * b(i - n))),
			p = (n, i, t = (e) => 1 - i(1 - e)) => n === "in" ? i : n === "out" ? t : n === "in-out" ? (e) => e < 0.5 ? i(e * 2) / 2 : 0.5 + t((e - 0.5) * 2) / 2 : (e) => e < 0.5 ? t(e * 2) / 2 : 0.5 + i((e - 0.5) * 2) / 2,
			$ = Math.cosh(2),
			z = Math.acosh(2),
			V = Math.tanh(3),
			P = Math.atan(5);

		function Y() {
			return (n) => n;
		}

		function q(...n) {
			const i = [0].concat(n);
			i.push(1);
			const t = i.length,
				e = Array(t).fill(0).map((s, r) => A(r, t - 1));
			return (s) => {
				const r = e.map((l, a) => l * i[a] * (1 - s) ** (t - a - 1) * s ** a);
				return T(...r);
			};
		}

		function U(n, i) {
			if (n === "sin") {
				const t = (s) => Math.sin(s * Math.PI / 2);
				return p(i, (s) => 1 - t(1 - s), t);
			}
			if (n === "sec") {
				const t = (s) => 1 / Math.cos(s);
				return p(i, (s) => t(s * Math.PI / 3) - 1);
			}
			throw new TypeError(
				"Unexpected parameters are delivered in trigo timing function."
			);
		}

		function C(n, i) {
			if (!Number.isInteger(n))
				throw new TypeError(
					"The first parameter of power timing function only allow integer."
				);
			return p(i, (e) => e ** n);
		}

		function G(n, i) {
			if (n === "sin")
				return p(i, (e) => (Math.cosh(e * 2) - 1) / ($ - 1));
			if (n === "tan") {
				const t = (s) => Math.tanh(s * 3) * 1 / V;
				return p(i, (s) => 1 - t(1 - s), t);
			}
			if (n === "sec") {
				const t = (s) => 1 / Math.cosh(s);
				return p(i, (s) => 1 - (t(s * z) - 0.5) * 2);
			}
			throw new TypeError(
				"Unexpected parameters are delivered in hyper timing function."
			);
		}

		function N(n, i) {
			if (n === "sin") {
				const t = (s) => Math.asin(s) / Math.PI * 2;
				return p(i, (s) => 1 - t(1 - s), t);
			}
			if (n === "tan") {
				const t = (s) => Math.atan(s * 5) / P;
				return p(i, (s) => 1 - t(1 - s), t);
			}
			throw new TypeError(
				"Unexpected parameters are delivered in inverse trigo timing function."
			);
		}
		/** @param {(input:number) => number} [i=() => 1] */
		function B(n, i = () => 1) {
			let t = -1;
			return (e) => (t *= -1, e < 0.5 ? n * i(e * 2) * t : n * i((1 - e) * 2) * t);
		}

		function D(n, i = 1, t = [0, 0], e = 0, s = (l) => 1, r = !1) {
			return (l) => {
				const a = i * l * Math.PI * 2 + e * Math.PI / 180,
					u = Math.cos(a),
					c = Math.sin(a),
					h = n * s(s(r ? 1 - l : l));
				return [h * u + t[0], h * c + t[1]];
			};
		}

		function H(n, i, ...t) {
			const e = [n].concat(t);
			e.push(i);
			const s = e.length,
				r = Array(s).fill(0).map((l, a) => A(a, s - 1));
			return (l) => {
				const a = r.map((c, h) => c * e[h][0] * (1 - l) ** (s - h - 1) * l ** h),
					u = r.map((c, h) => c * e[h][1] * (1 - l) ** (s - h - 1) * l ** h);
				return [T(...a), T(...u)];
			};
		}

		core.plugin.animate = {
			Animation: j,
			AnimationBase: F,
			Ticker: I,
			Transition: O,
			bezier: q,
			bezierPath: H,
			circle: D,
			hyper: G,
			inverseTrigo: N,
			linear: Y,
			power: C,
			shake: B,
			sleep: R,
			trigo: U,
		}

	},
    "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 = 500;
			}

			/**
			 * 设置音频渐变时长
			 * @param time 渐变时长
			 */
			setTransitionTime(time) {
				this.transitionTime = time;
				for (const [, value] of this.gain) {
					value.transition.time(time);
				}
			}

			/**
			 * 屏蔽音乐切换
			 */
			blockChange() {
				this.blocking = true;
			}

			/**
			 * 取消屏蔽音乐切换
			 */
			unblockChange() {
				this.blocking = false;
			}

			/**
			 * 设置总音量大小
			 * @param volume 音量大小
			 */
			setVolume(volume) {
				this.mainGain.setVolume(volume);
				this._volume = volume;
			}
			/**
			 * 获取总音量大小
			 */
			getVolume() {
				return this.mainGain.getVolume();
			}
			/**
			 * 设置是否启用
			 * @param enabled 是否启用
			 */
			setEnabled(enabled) {
				if (enabled) this.resume();
				else this.stop();
				this.enabled = enabled;
			}

			/**
			 * 设置 bgm 音频名称的前缀
			 */
			setPrefix(prefix) {
				this.prefix = prefix;
			}

			getId(name) {
				return `${this.prefix}${name}`;
			}

			/**
			 * 根据 bgm 名称获取其 AudioRoute 实例
			 * @param id 音频名称
			 */
			get(id) {
				return this.player.getRoute(this.getId(id));
			}

			/**
			 * 添加一个 bgm
			 * @param id 要添加的 bgm 的名称
			 * @param url 指定 bgm 的加载地址
			 */
			addBgm(id, url = `project/bgms/${id}`) {
				const type = guessTypeByExt(id);
				if (!type) {
					console.warn(
						"Unknown audio extension name: '" +
						id.split(".").slice(0, -1).join(".") +
						"'"
					);
					return;
				}
				const gain = this.player.createVolumeEffect();
				if (isAudioSupport(type)) {
					const source = audioPlayer.createElementSource();
					source.setSource(url);
					source.setLoop(true);
					const route = new AudioRoute(source, audioPlayer);
					route.addEffect([gain, this.mainGain]);
					audioPlayer.addRoute(this.getId(id), route);
					this.setTransition(id, route, gain);
				} else {
					const source = audioPlayer.createStreamSource();
					const stream = new StreamLoader(url);
					stream.pipe(source);
					source.setLoop(true);
					const route = new AudioRoute(source, audioPlayer);
					route.addEffect([gain, this.mainGain]);
					audioPlayer.addRoute(this.getId(id), route);
					this.setTransition(id, route, gain);
				}
			}

			/**
			 * 移除一个 bgm
			 * @param id 要移除的 bgm 的名称
			 */
			removeBgm(id) {
				this.player.removeRoute(this.getId(id));
				const gain = this.gain.get(id);
				gain?.transition.ticker.destroy();
				this.gain.delete(id);
			}

			setTransition(id, route, gain) {
				const transition = new Transition();
				transition
					.time(this.transitionTime)
					.mode(linear())
					.transition("volume", 0);

				const tick = () => {
					gain.setVolume(transition.value.volume);
				};

				/**
				 * @param expect 在结束时应该是正在播放还是停止
				 */
				const setTick = async (expect) => {
					transition.ticker.remove(tick);
					transition.ticker.add(tick);
					const identifier = route.stopIdentifier;
					await sleep(this.transitionTime + 500);
					if (route.status === expect && identifier === route.stopIdentifier) {
						transition.ticker.remove(tick);
						if (route.status === AudioStatus.Playing) {
							gain.setVolume(1);
						} else {
							gain.setVolume(0);
						}
					}
				};

				route.onStart(async () => {
					transition.transition("volume", 1);
					setTick(AudioStatus.Playing);
				});
				route.onEnd(() => {
					transition.transition("volume", 0);
					setTick(AudioStatus.Paused);
				});
				route.setEndTime(this.transitionTime);

				this.gain.set(id, { effect: gain, transition });
			}

			/**
			 * 播放一个 bgm
			 * @param id 要播放的 bgm 名称
			 */
			play(id, when) {
				if (this.blocking) return;
				if (id !== this.playingBgm && this.playingBgm) {
					this.player.pause(this.getId(this.playingBgm));
				}
				this.playingBgm = id;
				if (!this.enabled) return;
				this.player.play(this.getId(id), when);
				this.playing = true;
			}

			/**
			 * 继续当前的 bgm
			 */
			resume() {
				if (this.blocking || !this.enabled || this.playing) return;
				if (this.playingBgm) {
					this.player.resume(this.getId(this.playingBgm));
				}
				this.playing = true;
			}

			/**
			 * 暂停当前的 bgm
			 */
			pause() {
				if (this.blocking || !this.enabled) return;
				if (this.playingBgm) {
					this.player.pause(this.getId(this.playingBgm));
				}
				this.playing = false;
			}

			/**
			 * 停止当前的 bgm
			 */
			stop() {
				if (this.blocking || !this.enabled) return;
				if (this.playingBgm) {
					this.player.stop(this.getId(this.playingBgm));
				}
				this.playing = false;
			}
		}
		const bgmController = new BgmController(audioPlayer);

		class SoundPlayer {
			constructor(player) {
				/** 每个音效的唯一标识符 */
				this.num = 0;
				this.enabled = true;
				this.gain = player.createVolumeEffect();
				/** 每个音效的数据 */
				this.buffer = new Map();
				/** 所有正在播放的音乐 */
				this.playing = new Set();
				this.player = player;
			}
			/**
			 * 设置是否启用音效
			 * @param enabled 是否启用音效
			 */
			setEnabled(enabled) {
				if (!enabled) this.stopAllSounds();
				this.enabled = enabled;
			}

			/**
			 * 设置音量大小
			 * @param volume 音量大小
			 */
			setVolume(volume) {
				this.gain.setVolume(volume);
			}
			/**
			 * 获取音量大小
			 */
			getVolume() {
				return this.gain.getVolume();
			}
			/**
			 * 添加一个音效
			 * @param id 音效名称
			 * @param data 音效的Uint8Array数据
			 */
			async add(id, data) {
				const buffer = await this.player.decodeAudioData(data);
				if (!buffer) {
					console.warn(
						"Cannot decode sound '" +
						id +
						"', since audio file may not supported by 2.b."
					);
					return;
				}
				this.buffer.set(id, buffer);
			}

			/**
			 * 播放一个音效
			 * @param id 音效名称
			 * @param position 音频位置，[0, 0, 0]表示正中心，x轴指向水平向右，y轴指向水平向上，z轴指向竖直向上
			 * @param orientation 音频朝向，[0, 1, 0]表示朝向前方
			 */
			play(id, position = [0, 0, 0], orientation = [1, 0, 0]) {
				if (!this.enabled || !id) return -1;
				const buffer = this.buffer.get(id);
				if (!buffer) {
					console.warn(
						"Cannot play sound '" +
						id +
						"', since there is no added data named it."
					);
					return -1;
				}
				const soundNum = this.num++;

				const source = this.player.createBufferSource();
				source.setBuffer(buffer);
				const route = this.player.createRoute(source);
				const stereo = this.player.createStereoEffect();
				stereo.setPosition(position[0], position[1], position[2]);
				stereo.setOrientation(orientation[0], orientation[1], orientation[2]);
				route.addEffect([stereo, this.gain]);
				this.player.addRoute(`sounds.${soundNum}`, route);
				route.play();
				source.output.addEventListener("ended", () => {
					this.playing.delete(soundNum);
				});
				this.playing.add(soundNum);
				return soundNum;
			}

			/**
			 * 停止一个音效
			 * @param num 音效的唯一 id
			 */
			stop(num) {
				const id = `sounds.${num}`;
				const route = this.player.getRoute(id);
				if (route) {
					route.stop();
					this.player.removeRoute(id);
					this.playing.delete(num);
				}
			}

			/**
			 * 停止播放所有音效
			 */
			stopAllSounds() {
				this.playing.forEach((v) => {
					const id = `sounds.${v}`;
					const route = this.player.getRoute(id);
					if (route) {
						route.stop();
						this.player.removeRoute(id);
					}
				});
				this.playing.clear();
			}
		}
		const soundPlayer = new SoundPlayer(audioPlayer);

		function loadAllBgm() {
			const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
			for (const bgm of data.main.bgms) {
				bgmController.addBgm(bgm);
			}
		}
		loadAllBgm();
		AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder);
		AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder);

		core.plugin.audioSystem = {
			AudioType,
			AudioDecoder,
			AudioStatus,
			checkAudioType,
			isAudioSupport,
			audioPlayer,
			soundPlayer,
			bgmController,
			guessTypeByExt,
			BgmController,
			SoundPlayer,
			EchoEffect,
			DelayEffect,
			ChannelVolumeEffect,
			VolumeEffect,
			StereoEffect,
			AudioEffect,
			AudioPlayer,
			AudioRoute,
			AudioStreamSource,
			AudioElementSource,
			AudioBufferSource,
			loadAllBgm,
			StreamLoader,
		};
		//bgm相关复写
		control.prototype.playBgm = (bgm, when) => {
			bgm = core.getMappedName(bgm);
			if (main.mode != "play" || !core.material.bgms[bgm]) return;
			// 如果不允许播放
			if (!core.musicStatus.bgmStatus) {
				try {
					core.musicStatus.playingBgm = bgm;
					core.musicStatus.lastBgm = bgm;
					core.material.bgms[bgm].pause();
				} catch (e) {
					console.error(e);
				}
				return;
			}
			core.setMusicBtn();

			try {
				bgmController.play(bgm, when);
			} catch (e) {
				console.log("无法播放BGM " + bgm);
				console.error(e);
				core.musicStatus.playingBgm = null;
			}

		};
		control.prototype.pauseBgm = () => {
			bgmController.pause();
			core.setMusicBtn();
		};

		control.prototype.resumeBgm = function () {
			bgmController.resume();
			core.setMusicBtn();
		};
		control.prototype.checkBgm = function () {
			core.playBgm(bgmController.playingBgm || main.startBgm);
		};
		control.prototype.triggerBgm = function () {
			core.musicStatus.bgmStatus = !core.musicStatus.bgmStatus;
			if (bgmController.playing) bgmController.pause();
			else bgmController.resume();
			core.setMusicBtn();
			core.setLocalStorage("bgmStatus", core.musicStatus.bgmStatus);
		};
		//sound相关复写
		control.prototype.playSound = function (
			sound,
			_pitch,
			callback,
			position,
			orientation
		) {
			if (main.mode != "play" || !core.musicStatus.soundStatus) return callback?.();
			const name = core.getMappedName(sound);
			const num = soundPlayer.play(name, position, orientation);
			const route = audioPlayer.getRoute(`sounds.${num}`);
			if (!route) {
				callback?.();
				return -1;
			} else {
				sleep(route.duration * 1000).then(() => callback?.());
				return num;
			}
		};
		control.prototype.stopSound = function (id) {
			if (isNil(id)) {
				soundPlayer.stopAllSounds();
			} else {
				soundPlayer.stop(id);
			}
		};
		control.prototype.getPlayingSounds = function () {
			return [...soundPlayer.playing];
		};
		//sound加载复写
		loader.prototype._loadOneSound_decodeData = function (name, data) {
			if (data instanceof Blob) {
				var blobReader = new zip.BlobReader(data);
				blobReader.init(function () {
					blobReader.readUint8Array(0, blobReader.size, function (uint8) {
						//core.loader._loadOneSound_decodeData(name, uint8.buffer);
						soundPlayer.add(name, uint8);
					});
				});
				return;
			}
			if (data instanceof ArrayBuffer) {
				const uint8 = new Uint8Array(data);
				soundPlayer.add(name, uint8);
			}
		};
		//音量控制复写
		soundPlayer.setVolume(
			core.musicStatus.userVolume * core.musicStatus.designVolume
		);
		bgmController.setVolume(
			core.musicStatus.userVolume * core.musicStatus.designVolume
		);
		actions.prototype._clickSwitchs_sounds_userVolume = function (delta) {
			var value = Math.round(Math.sqrt(100 * core.musicStatus.userVolume));
			if (value == 0 && delta < 0) return;
			core.musicStatus.userVolume = core.clamp(
				Math.pow(value + delta, 2) / 100,
				0,
				1
			);
			//audioContext 音效 不受designVolume 影响
			if (core.musicStatus.gainNode != null)
				core.musicStatus.gainNode.gain.value = core.musicStatus.userVolume;
			soundPlayer.setVolume(
				core.musicStatus.userVolume * core.musicStatus.designVolume
			);
			bgmController.setVolume(
				core.musicStatus.userVolume * core.musicStatus.designVolume
			);
			core.setLocalStorage("userVolume", core.musicStatus.userVolume);
			core.playSound("确定");
			core.ui._drawSwitchs_sounds();
		};
	},
    "drawItemDetail": function () {
		/* 宝石血瓶左下角显示数值
			 * 需要将 变量：itemDetail改为true才可正常运行
			 * 请尽量减少勇士的属性数量，否则可能会出现严重卡顿（划掉，现在你放一万个属性也不会卡）
		 * 注意：这里的属性必须是core.status.hero里面的，flag无法显示
			 * 如果不想显示，可以core.setFlag("itemDetail", false);
		 * 然后再core.getItemDetail();
		 * 如有bug在大群或造塔群@古祠
		 */

		// 忽略的道具
		const ignore = ['superPotion'];

		// 取消注释下面这句可以减少超大地图的判定。
		// 如果地图宝石过多，可能会略有卡顿，可以尝试取消注释下面这句话来解决。
		// core.bigmap.threshold = 256;
		const origin = core.control.updateStatusBar;
		core.updateStatusBar = core.control.updateStatusBar = function () {
			if (core.hasFlag('__statistics__')) return;
			else return origin.apply(core.control, arguments);
		}

		core.control.updateDamage = function (floorId, ctx) {
			floorId = floorId || core.status.floorId;
			if (!floorId || core.status.gameOver || main.mode != 'play') return;
			const onMap = ctx == null;

			// 没有怪物手册
			if (!core.hasItem('book')) return;
			core.status.damage.posX = core.bigmap.posX;
			core.status.damage.posY = core.bigmap.posY;
			if (!onMap) {
				const width = core.floors[floorId].width,
					height = core.floors[floorId].height;
				// 地图过大的缩略图不绘制显伤
				if (width * height > core.bigmap.threshold) return;
			}
			this._updateDamage_damage(floorId, onMap);
			this._updateDamage_extraDamage(floorId, onMap);
			core.getItemDetail(floorId); // 宝石血瓶详细信息
			this.drawDamage(ctx);
		};

		function getRatio() {
			let ratio = (core.status.thisMap?.ratio) ?? 1;
			const currEvent = core.status.event;
			if (!currEvent) return ratio;
			switch (currEvent.id) {
				case 'viewMaps': //调整浏览地图时的倍率
					if (currEvent.data) {
						const viewMapFloorId = (currEvent.data.floorId);
						ratio = core.status.maps[viewMapFloorId].ratio;
					}
					break;
				case 'fly': //调整在楼传界面浏览地图时的倍率
					ratio = core.status.maps[core.floorIds[currEvent.data]].ratio;
					break;
			}
			return ratio;
		}

		// 因为core.status.hero是进存档的，必定可序列化
		const clone = (typeof structuredClone === 'function') ? structuredClone : (ele) => JSON.parse(JSON.stringify(ele));

		// 数值类型的属性(包含装备的百分比提升项)
		const statusName = new Set(['atk', 'atkper', 'def', 'defper', 'mdef', 'mdefper', 'hp',
			'hpmax', 'hpmaxper', 'mana', 'manamax', 'money', 'exp']);
		// 获取宝石信息 并绘制
		this.getItemDetail = function (floorId) {
			if (!core.getFlag('itemDetail')) return;
			if (!core.status.thisMap) return;
			floorId = floorId ?? core.status.thisMap.floorId;
			const beforeRatio = core.status.thisMap.ratio;
			core.status.thisMap.ratio = core.status.maps[floorId].ratio;
			let diff = {};
			const before = core.status.hero;
			const hero = clone(core.status.hero);
			const handler = {
				set(target, key, v) {
					if (!statusName.has(key)) return true; // 非数值类型的属性就不用记了，显示也没有意义
					diff[key] = v - (target[key] || 0);
					if (!diff[key]) diff[key] = void 0;
					return true;
				}
			};
			core.status.hero = new Proxy(hero, handler);
			core.status.maps[floorId].blocks.forEach(function (block) {
				if (
					block.event.cls !== 'items' ||
					ignore.includes(block.event.id) ||
					block.disable
				)
					return;
				const x = block.x,
					y = block.y;
				// v2优化，只绘制范围内的部分
				if (core.bigmap.v2) {
					if (
						x < core.bigmap.posX - core.bigmap.extend ||
						x > core.bigmap.posX + core.__SIZE__ + core.bigmap.extend ||
						y < core.bigmap.posY - core.bigmap.extend ||
						y > core.bigmap.posY + core.__SIZE__ + core.bigmap.extend
					) {
						return;
					}
				}
				diff = {};
				const id = block.event.id;
				const item = core.material.items[id];
				switch (item.cls) {
					case 'equips': {
						// 装备也显示
						diff = item.equip.value ?? {};
						const per = item.equip.percentage ?? {};
						for (const name in per) {
							diff[name + 'per'] = per[name].toString() + '%';
						}
						break;
					}
					case 'items': {
						// 跟数据统计原理一样 执行效果 前后比较
						core.setFlag('__statistics__', true);
						if (item.itemEffect != null) {
							try {
								eval(item.itemEffect);
							} catch (error) { }
						}
						const ratio = getRatio();
						const effectObj = core.getItemEffectValue(id, ratio);
						for (let statusName in effectObj) {
							if (!effectObj.hasOwnProperty(statusName)) continue;
							if (!diff.hasOwnProperty(statusName)) diff[statusName] = 0;
							diff[statusName] += effectObj[statusName];
						}
						break;
					}
				}
				drawItemDetail(diff, x, y);
			});
			core.status.thisMap.ratio = beforeRatio;
			core.status.hero = before;
			window.hero = before;
			window.flags = before.flags;
		};

		// 绘制
		function drawItemDetail(diff, x, y) {
			const px = 32 * x + 2,
				py = 32 * y + 30;
			let content = '';
			// 获得数据和颜色
			let i = 0;
			for (const name in diff) {
				if (!diff[name]) continue;
				let color = '#fff';

				if (typeof diff[name] === 'number')
					content = core.formatBigNumber(diff[name], true);
				else content = diff[name];
				switch (name) {
					case 'atk':
					case 'atkper':
						color = '#FF7A7A';
						break;
					case 'def':
					case 'defper':
						color = '#00E6F1';
						break;
					case 'mdef':
					case 'mdefper':
						color = '#6EFF83';
						break;
					case 'hp':
						color = '#A4FF00';
						break;
					case 'hpmax':
					case 'hpmaxper':
						color = '#F9FF00';
						break;
					case 'mana':
					case 'manamax':
						color = '#CC6666';
						break;
					case 'money':
						color = '#FFD700';
						break;
					case 'exp':
						color = '#BA55D3';
						break;
				}
				// 绘制
				core.status.damage.data.push({
					text: content,
					px: px,
					py: py - 10 * i,
					color: color
				});
				i++;
			}
		}
	},
    "autoClear": function () {
		// 在此增加新插件
		/**
		 * --------------- 使用说明 ---------------
		 * 变量autoGet控制自动拾取开关
		 * 变量autoBattle控制自动清怪开关
		 */
		const ctxName = 'autoClear';

		// 每走一步后自动拾取的判定要放在阻击结算之后，见libs，并不写在本插件当中

		core.plugin.autoClear = auto;

		function willLvUp(exp) {
			const nextExp = core.getNextLvUpNeed();
			if (typeof exp === 'number' && typeof nextExp === 'number' && exp >= nextExp) return true;
			return false;
		}

		/**
		 * 是否清这个怪，可以修改这里来实现对不同怪的不同操作
		 * @param {string} enemy
		 * @param {number} x
		 * @param {number} y
		 */
		function canBattle(enemy, x, y) {
			const loc = `${x},${y}`;
			const floor = core.floors[core.status.floorId];
			const e = core.getEnemyValue(enemy, null, x, y);
			const hasEvent =
				has(floor.afterBattle[loc]) || has(floor.beforeBattle[loc]) ||
				has(e.beforeBattle) || has(e.afterBattle) ||
				has(floor.events[loc]) || willLvUp(e.exp); // 防止有升级后事件

			// 有事件，不清
			if (hasEvent) return false;

			const cache = core.status.checkBlock.cache;
			const hasGuards = has(cache) && has(cache[loc]) &&
				has(cache[loc]["guards"]) && cache[loc]["guards"].length > 0; // 该敌人会被支援
			if (hasGuards) return false;

			// 有特定特殊属性的怪不清
			if (
				core.hasSpecial(e.special, 12) || // 中毒
				core.hasSpecial(e.special, 13) || // 衰弱
				core.hasSpecial(e.special, 14) || // 诅咒
				core.hasSpecial(e.special, 19) || // 自爆	
				core.hasSpecial(e.special, 21) || // 退化
				core.hasSpecial(e.special, 26) || // 支援
				core.hasSpecial(e.special, 27) || // 捕捉:逻辑上应该让怪物来找角色
				core.hasSpecial(e.special, 28) || // 追猎:逻辑上应该让怪物来找角色
				core.hasSpecial(e.special, 29) // 败移:特殊战后事件
			) {
				return false;
			}
			const damage = core.getDamageInfo(enemy, void 0, x, y)?.damage;
			// 0伤或负伤，清
			if (has(damage) && damage <= 0) return true;
			return false;
		}

		/**
		 * 判断一个点是否能遍历
		 */
		function judge(block, nx, ny, tx, ty, dir, floorId, autoBattle, autoGet) {
			if (!has(block)) { // 说明什么都没有，没事件也没图块
				return { type: "none", canGoThrough: true };
			}
			const cls = block.event.cls;
			const loc = `${tx},${ty}`;
			const floor = core.floors[floorId];
			const changeFloor = floor.changeFloor[loc];
			const isEnemy = autoBattle && cls.startsWith('enemy'),
				isItem = autoGet && cls === 'items';
			// 因为没有判定往来图块的通行性，这里宁可严格一点，非空地(block.id === 0)一律不给穿
			if (has(changeFloor)) {
				if ((changeFloor.ignoreChangeFloor ?? core.flags.ignoreChangeFloor) && block.id === 0) {
					return { type: "unknown", canGoThrough: true };
				}
				return { type: "unknown", canGoThrough: false };
			}

			if (has(core.floors[floorId].events[loc])) return { type: "unknown", canGoThrough: false };

			if (isEnemy) return { type: "enemy", canGoThrough: true };
			if (isItem) return { type: "item", canGoThrough: true };

			return { type: "unknown", canGoThrough: false };
		}

		/**
		 * 是否捡拾这个物品
		 */
		function canGetItem(item, loc, floorId = core.status.floorId) {
			// 可以用于检测道具是否应该被捡起，例如如果捡起后血量超过80%则不捡起可以这么写：
			// if (item.cls === 'items') {
			//     let diff = {};
			//     const before = core.status.hero;
			//     const hero = core.clone(core.status.hero);
			//     const handler = {
			//         set(target, key, v) {
			//             diff[key] = v - (target[key] || 0);
			//             if (!diff[key]) diff[key] = void 0;
			//             return true;
			//         }
			//     };
			//     core.status.hero = new Proxy(hero, handler);

			//     eval(item.itemEffect);

			//     core.status.hero = before;
			//     window.hero = before;
			//     window.flags = before.flags;
			//     if (
			//         diff.hp &&
			//         diff.hp + core.status.hero.hp > core.status.hero.hpmax * 0.8
			//     )
			//         return false;
			// }
			const floor = core.floors[floorId];
			if (has(floor.afterGetItem[loc])) return false;
			if (item.cls === 'items') {
				const itemEffectType = core.getItemEffectType(item.id);
				if (core.hasFlag('noRouting_HP') && itemEffectType.includes('hp')) return false;
				if (core.hasFlag('noRouting_MDEF') && itemEffectType.includes('mdef')) return false;
				if (core.hasFlag('noRouting_ATK') && itemEffectType.includes('atk')) return false;
				if (core.hasFlag('noRouting_DEF') && itemEffectType.includes('def')) return false;
			}
			return true;
		}

		/**
		 * @template T
		 * @param {T} v
		 * @returns {v is NonNullable<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 PagedMenu extends MenuBase {
			constructor(name, toListen, x, y, w, h, zIndex, capacity, data) {
				super(name, toListen, x, y, w, h, zIndex);
				this.page = 0;
				/** @type {any[]} */
				this.totalItemList = data;
				/** @type {any[]} */
				this.currItemList;
				this.capacity = capacity;
			}

			drawContent() {
				const pageDownBtn = this.btnMap.get("pageDown"),
					pageUpBtn = this.btnMap.get("pageUp");
				if (pageDownBtn) pageDownBtn.alpha = this.canPageDown() ? 1 : 0.3;
				if (pageUpBtn) pageUpBtn.alpha = this.canPageUp() ? 1 : 0.3;
				super.drawContent();
			}

			/** 此类初始化时需要调用一次 **/
			updateCurrItemList() {
				this.currItemList = this.totalItemList.slice(this.page * this.capacity, (this.page + 1) * this.capacity);
			}

			canPageDown() {
				return this.page > 0;
			}

			pageDown() {
				if (!this.canPageDown()) return;
				this.page--;
				this.updateCurrItemList();
				this.drawContent();
			}

			canPageUp() {
				return (this.page + 1) * this.capacity < this.totalItemList.length
			}

			pageUp() {
				if (!this.canPageUp()) return;
				this.page++;
				this.updateCurrItemList();
				this.drawContent();
			}
		}

		class MultiTypePagedMenu extends MenuBase {
			/**
			 * @param {string} name - 菜单名称
			 * @param {any} toListen - 要监听的事件集合
			 * @param {number} x - X坐标
			 * @param {number} y - Y坐标
			 * @param {number} w - 宽度
			 * @param {number} h - 高度
			 * @param {number} zIndex - 层级
			 * @param {Object<string, {capacity: number, totalItemList: any[], pageDownBtnKey: string, pageUpBtnKey: string}>} pagesConfig 
			 *        配置对象，格式示例:
			 *        {
			 *          'items': { capacity: 5, totalItemList: [...], pageDownBtnKey: 'btnItemPrev', pageUpBtnKey: 'btnItemNext' },
			 *          'logs': { capacity: 10, totalItemList: [...], pageDownBtnKey: 'btnLogPrev', pageUpBtnKey: 'btnLogNext' }
			 *        }
			 */
			constructor(name, toListen, x, y, w, h, zIndex, pagesConfig) {
				super(name, toListen, x, y, w, h, zIndex);

				// 内部状态存储
				this.page = {}; // { type: currentPageIndex }
				this.totalItemList = {}; // { type: fullData[] }
				this.currItemList = {}; // { type: currentSlice[] }
				this.capacityConfig = {}; // { type: capacityNum }

				// 按钮键名映射存储: { type: { down: key, up: key } }
				this.btnKeyMap = {};

				for (const type in pagesConfig) {
					const config = pagesConfig[type];
					this.capacityConfig[type] = config.capacity;
					this.totalItemList[type] = config.totalItemList;
					this.page[type] = 0;
					this.btnKeyMap[type] = {
						down: config.pageDownBtnKey,
						up: config.pageUpBtnKey
					};
					this.updateCurrItemList(type);
				}
			}

			drawContent() {
				for (const type in this.btnKeyMap) {
					const keys = this.btnKeyMap[type];

					if (keys.down) {
						const btn = this.btnMap.get(keys.down);
						if (btn) {
							btn.alpha = this.canPageDown(type) ? 1 : 0.3;
						}
					}

					if (keys.up) {
						const btn = this.btnMap.get(keys.up);
						if (btn) {
							btn.alpha = this.canPageUp(type) ? 1 : 0.3;
						}
					}
				}
				super.drawContent();
			}

			/**
			 * @param {string} [pageType] 
			 */
			updateCurrItemList(pageType) {
				if (!pageType) {
					for (const type in this.page) {
						this.updateCurrItemList(type);
					}
					return;
				}
				const currentPage = this.page[pageType] || 0;
				const capacity = this.capacityConfig[pageType];

				const start = currentPage * capacity;
				const end = start + capacity;

				this.currItemList[pageType] = this.totalItemList[pageType].slice(start, end);
			}

			/**
			 * 检查指定类型是否可以向下翻页 (回到上一页)
			 * @param {string} pageType 
			 * @returns {boolean}
			 */
			canPageDown(pageType) {
				return (this.page[pageType] || 0) > 0;
			}

			/**
			 * 执行向下翻页 (页码 -1)
			 * @param {string} pageType 
			 */
			pageDown(pageType) {
				if (!this.canPageDown(pageType)) return;

				this.page[pageType]--;
				this.updateCurrItemList(pageType);
				this.drawContent();
			}

			/**
			 * 检查指定类型是否可以向上翻页 (进入下一页)
			 * @param {string} pageType 
			 * @returns {boolean}
			 */
			canPageUp(pageType) {
				const currentPage = this.page[pageType] || 0;
				const capacity = this.capacityConfig[pageType];
				const totalLen = this.totalItemList[pageType] ? this.totalItemList[pageType].length : 0;

				return (currentPage + 1) * capacity < totalLen;
			}

			/**
			 * 执行向上翻页 (页码 +1)
			 * @param {string} pageType 
			 */
			pageUp(pageType) {
				if (!this.canPageUp(pageType)) return;

				this.page[pageType]++;
				this.updateCurrItemList(pageType);
				this.drawContent(); // 重绘以更新按钮状态和内容
			}

			// --- 辅助方法 ---
			/**
			 * 获取指定类型的当前页码 (从0开始)
			 * @param {string} pageType
			 * @returns {number}
			 */
			getCurrentPage(pageType) {
				return this.page[pageType] || 0;
			}

			/**
			 * 获取指定类型的总页数
			 * @param {string} pageType
			 * @returns {number}
			 */
			getTotalPages(pageType) {
				const totalLen = this.totalItemList[pageType] ? this.totalItemList[pageType].length : 0;
				const capacity = this.capacityConfig[pageType];
				if (!capacity || capacity <= 0) return 0;
				return Math.ceil(totalLen / capacity);
			}
		}

		class Pagination extends MenuBase {
			constructor(pageList, currPage, name, toListen, x, y, w, h, zIndex) {
				super(name, toListen, x, y, w, h, zIndex);
				/**
				 * 当前页面列表
				 * @type {Array<MenuBase>}
				 */
				this.pageList = pageList;
				/**
				 * 当前页的序号
				 * @type {number}
				 */
				this.currPage = currPage || 0;
			}

			initOnePage(index) {
				this.currPage = index;
				this.pageList[index].init();
			}

			changePage(num) {
				if (num !== this.currPage) {
					const beforeMenu = this.pageList[this.currPage];
					beforeMenu.clear();
				}
				this.initOnePage(num);
			}

			pageDown() {
				if (this.currPage > 0) this.changePage(this.currPage - 1);
			}

			pageUp() {
				if (this.currPage < this.pageList.length - 1) this.changePage(this.currPage + 1);
			}

			clear() {
				this.pageList.forEach((page) => page.clear());
				super.clear();
			}
		}

		// 圆角带文字的按钮
		class RoundBtn extends ButtonBase {
			constructor(x, y, w, h, text, config) {
				super(x, y, w, h);
				this.text = text;
				this.config = config || {};
			}

			draw() {
				const ctx = this.ctx;
				const { x, y, w, h } = this;
				const {
					fillStyle = 'rgb(204, 204, 204)', strokeStyle = 'black', fontStyle = 'black',
					selectedFillStyle = 'rgb(255, 51, 153)', selectedstrokeStyle = 'black', selectedFontStyle = 'white',
					radius = 3, lineWidth = 1, angle = null, font = '16px Verdana'
				} = this.config || {};
				core.setTextAlign(ctx, 'center');
				core.setTextBaseline(ctx, 'alphabetic');
				if (this.status === 'selected') {
					core.fillRoundRect(ctx, x, y, w, h, radius, selectedFillStyle, angle);
					core.strokeRoundRect(ctx, x, y, w, h, radius, selectedstrokeStyle, lineWidth, angle);
					core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, selectedFontStyle, font);
				} else {
					core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle);
					core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle);
					core.fillText(ctx, this.text, x + w / 2, y + h / 2 + 5, fontStyle, font);
				}
			}
		}

		class IconBtn extends ButtonBase {
			constructor(x, y, w, h, icon, config) {
				super(x, y, w, h);
				this.icon = icon;
				this.config = config || {};
			}

			draw() {
				const ctx = this.ctx;
				const { x, y, w, h } = this;
				const {
					strokeStyle = 'black', fillStyle = 'white',
					radius = 3, lineWidth = 1, angle = null, frame = 0,
					iconX = x, iconY = y, iconW = w, iconH = h,
					crossline1 = false, crossline2 = false, crossLineOffset = 2,
					crossLineStyle = 'red', crossLineWidth = 2,
				} = this.config || {};
				if (fillStyle !== 'none') core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle, angle);
				if (strokeStyle !== 'none') core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle, lineWidth, angle);
				core.drawIcon(ctx, this.icon, iconX, iconY, iconW, iconH, frame);
				if (crossline1) {
					core.drawLine(ctx, x + crossLineOffset, y + crossLineOffset,
						x + w - crossLineOffset, y + h - crossLineOffset,
						crossLineStyle, crossLineWidth);
				}
				if (crossline2) {
					core.drawLine(ctx, x + crossLineOffset, y + h - crossLineOffset,
						x + w - crossLineOffset, y + crossLineOffset,
						crossLineStyle, crossLineWidth);
				}
			}
		}

		class ExitBtn extends ButtonBase {
			constructor(x, y, w, h, config) {
				super(x, y, w, h);
				this.config = config || {};
			}

			draw() {
				const ctx = this.ctx;
				const {
					strokeStyle = ' #D32F2F', fillStyle = ' #EF5350', lineStyle = 'white',
					radius = 3, lineOffsetX = 5, lineWidthX = 3,
				} = this.config || {};
				const [x, y, w, h] = [this.x, this.y, this.w, this.h];
				core.fillRoundRect(ctx, x, y, w, h, radius, fillStyle);
				core.strokeRoundRect(ctx, x, y, w, h, radius, strokeStyle);
				core.drawLine(ctx, x + lineOffsetX, y + lineOffsetX, x + w - lineOffsetX, y + h - lineOffsetX, lineStyle, lineWidthX);
				core.drawLine(ctx, x + lineOffsetX, y + h - lineOffsetX, x + w - lineOffsetX, y + lineOffsetX, lineStyle, lineWidthX);
			}
		}

		class ArrowBtn extends ButtonBase {
			constructor(x, y, w, h, dir, config) {
				super(x, y, w, h);
				this.config = config || {};
				/** @type {'left'|'right'|'up'|'down'} */
				this.dir = dir;
				this.alpha = 1;
			}

			draw() {
				const isVertical = (this.dir === "up") || (this.dir === "down");
				const {
					/** 箭头尖端到所指的边的距离 **/
					marginTip = isVertical ? 5 : 4,
					/** 箭头两侧到各自最接近的边的距离 **/
					marginSide = isVertical ? 5 : 5,
					/**  箭头尾部到最接近的边的距离 **/
					marginTail = isVertical ? 5 : 6,
					backStyle = 'gray',
					arrowStyle = 'black'
				} = this.config || {};

				const { x, y, w, h, ctx, alpha } = this;

				const originAlpha = core.setAlpha(ctx, alpha);

				core.fillRoundRect(ctx, x, y, w, h, 3, backStyle);

				let points = [];

				if (this.dir === 'left') {
					points = [
						[x + marginTip, y + h / 2],
						[x + w - marginTail, y + marginSide],
						[x + w - marginTail, y + h - marginSide]
					];
				} else if (this.dir === 'right') {
					points = [
						[x + w - marginTip, y + h / 2],
						[x + marginTail, y + marginSide],
						[x + marginTail, y + h - marginSide]
					];
				} else if (this.dir === 'up') {
					points = [
						[x + w / 2, y + marginTip],
						[x + marginSide, y + h - marginTail],
						[x + w - marginSide, y + h - marginTail]
					];
				} else if (this.dir === 'down') {
					points = [
						[x + w / 2, y + h - marginTip],
						[x + marginSide, y + marginTail],
						[x + w - marginSide, y + marginTail]
					];
				}

				if (points.length > 0) {
					core.fillPolygon(ctx, points, arrowStyle);
				}
				core.setAlpha(ctx, originAlpha);
			}
		}
		const registerResize = (menu) => {
			core.control.registerDymCanvasResizeEvent(menu.name, function () {
				if (menu && menu.onDraw) menu.drawContent();
			});
		}
		const unregisterResize = (menu) => core.control.unregisterDymCanvasResizeEvent(menu.name);
		core.plugin.uiBase = {
			ButtonBase,
			RoundBtn,
			IconBtn,
			ExitBtn,
			ArrowBtn,
			MenuBase,
			PagedMenu,
			MultiTypePagedMenu,
			Pagination,
			KeyCodeEnum,
			registerResize,
			unregisterResize,
		};
	},
    "newBackpackLook": function () {
		// 本插件定义了一些用于绘制的基类

		let __enable = true;
		if (!__enable) return;

		/** @todo 尝试干掉redraw */
		// #region 复写

		core.ui._drawToolbox = function () { drawItemBox('all'); }.bind(core.ui);
		core.ui._drawEquipbox = function () { drawItemBox('equips'); }.bind(core.ui);
		core.actions._keyDownToolbox = core.actions._keyDownEquipbox = function (keyCode) { return true; }.bind(core.actions);
		core.actions._clickToolbox = core.actions._clickEquipbox = function (x, y, px, py) { return true; }.bind(core.actions);
		core.actions._keyUpToolbox = core.actions._keyUpEquipbox = function (keyCode) { return true; }.bind(core.actions);
		// 暂不考虑修改core.status.event.id，该变量牵涉太多，作用不完全清楚
		// 录像模式下下列函数会进行检测，不处于特定模式时，阻止自定义监听事件 
		core.actions._checkReplaying = function () {
			if (core.isReplaying() && !UI._back?.onDraw && ['save', 'book', 'book-detail', 'viewMaps', 'toolbox', 'equipbox', 'text'].indexOf(core.status.event.id) < 0) {
				return true;
			}
			return false;
		}.bind(core.actions);

		const oriClosePanel = core.ui.closePanel;
		core.ui.closePanel = function () {
			oriClosePanel.apply(core.ui, [arguments]);
			UI.clearAll();
		}

		core.control._replayAction_item = function (action) {
			if (action.indexOf("item:") != 0) return false;
			const itemId = action.substring(5);
			if (!core.canUseItem(itemId)) return false;
			if (core.material.items[itemId].hideInReplay || core.status.replay.speed == 24) {
				core.useItem(itemId, false, core.replay);
				return true;
			}
			core.ui._drawToolbox(0);
			const itemInv = UI.itemInv;
			const totalIndex = itemInv.allItemList.indexOf(itemId);
			const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0);
			const currIndex = totalIndex - page * itemInv.pageCap;
			itemInv.page = page;
			itemInv.setIndex(currIndex);
			itemInv.drawContent();
			setTimeout(function () {
				core.ui.closePanel();
				core.useItem(itemId, false, core.replay);
			}, core.control.__replay_getTimeout());
			return true;
		}

		core.control._replayAction_equip = function (action) {
			if (action.indexOf("equip:") != 0) return false;
			const equipId = action.substring(6);
			if (!core.hasItem(equipId)) {
				core.removeFlag('__doNotCheckAutoEvents__');
				return false;
			}

			const callbackFunc = function () {
				const next = core.status.replay.toReplay[0] || "";
				if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) {
					core.removeFlag('__doNotCheckAutoEvents__');
					core.checkAutoEvents();
				}
				core.replay();
			}
			core.setFlag('__doNotCheckAutoEvents__', true);

			core.status.route.push(action);
			if (core.material.items[equipId].hideInReplay || core.status.replay.speed == 24) {
				core.loadEquip(equipId, callbackFunc);
				return true;
			}
			core.ui._drawEquipbox(0);
			const { itemId, itemInv, equipSlots } = UI;
			const totalIndex = itemInv.allItemList.indexOf(itemId);
			const page = Math.max(Math.ceil(totalIndex / itemInv.pageCap) - 1, 0);
			const currIndex = totalIndex - page * itemInv.pageCap;
			itemInv.page = page;
			itemInv.setIndex(currIndex);
			itemInv.drawContent();
			equipSlots.drawContent();
			setTimeout(function () {
				core.ui.closePanel();
				core.loadEquip(equipId, callbackFunc);
			}, core.control.__replay_getTimeout());
			return true;
		}

		core.control._replayAction_unEquip = function (action) {
			if (action.indexOf("unEquip:") != 0) return false;
			const equipType = parseInt(action.substring(8));
			if (!core.isset(equipType)) {
				core.removeFlag('__doNotCheckAutoEvents__');
				return false;
			}

			const callback = function () {
				[UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => {
					if (menu && menu.onDraw) menu.drawContent();
				});
				core.ui.closePanel();
				const next = core.status.replay.toReplay[0] || "";
				if (!next.startsWith('equip:') && !next.startsWith('unEquip:')) {
					core.removeFlag('__doNotCheckAutoEvents__');
					core.checkAutoEvents();
				}
				core.replay();
			}
			core.setFlag('__doNotCheckAutoEvents__', true);

			core.status.route.push(action);
			if (core.status.replay.speed == 24) {
				core.unloadEquip(equipType, callback);
				return true;
			}
			const { itemInv, equipSlots } = UI;
			const page = Math.max(Math.ceil(equipType / itemInv.pageCap) - 1, 0);
			const currIndex = equipType - page * itemInv.pageCap;
			itemInv.page = page;
			itemInv.setIndex(currIndex);
			itemInv.drawContent();
			equipSlots.drawContent();
			core.ui._drawEquipbox(0);
			setTimeout(function () {
				core.unloadEquip(equipType, callback);
			}, core.control.__replay_getTimeout());
			return true;
		}
		core.registerReplayAction("item", core.control._replayAction_item);
		core.registerReplayAction("equip", core.control._replayAction_equip);
		core.registerReplayAction("unEquip", core.control._replayAction_unEquip);

		// 复写control.startReplay
		const oriStartReplay = core.control.startReplay; // 进入播放录像模式时清空道具栏选中目标的缓存
		core.control.startReplay = function (list) {
			clearItemBoxCache();
			oriStartReplay.apply(core.control, [list, ...arguments]);
		}

		// 复写items._afterUseItem
		// flag:itemsUsedCount {[itemId:string]:boolean} 成功使用了的道具的计数
		const origin__afterUseItem = items.prototype._afterUseItem;
		items.prototype._afterUseItem = function (itemId) {
			const itemsUsedCount = core.getFlag('itemsUsedCount', {});
			if (!itemsUsedCount.hasOwnProperty(itemId)) itemsUsedCount[itemId] = 0;
			itemsUsedCount[itemId]++;
			core.setFlag('itemsUsedCount', itemsUsedCount);
			origin__afterUseItem(itemId);
		}

		// 复写ui.getToolboxItems
		// flag:markedItems string[] 在道具栏中置顶的道具的列表
		// flag:hideInfo {[itemId:string]:boolean} 手动选择了显示/隐藏的道具的列表
		core.ui.getToolboxItems = function (cls, showHide, sortFunc) {
			const markedItems = core.getFlag('markedItems', []);

			// 暂时不采用按使用次数排序这个函数 因为导致物品排序频繁变动，反而体验不好
			// const itemsUsedCount = core.getFlag('itemsUsedCount', {});
			// if (!sortFunc) sortFunc = (itemId1, itemId2) => {
			// 	const item1Count = itemsUsedCount[itemId1] || 0,
			// 		item2Count = itemsUsedCount[itemId2] || 0;
			// 	return item2Count - item1Count;
			// }

			let list = [];
			if (cls === 'all') {
				for (let name in core.status.hero.items) {
					if (name === "equips") continue;
					list = list.concat(Object.keys(core.status.hero.items[name])); // 获取'constants'和'tools'整体的列表
				}
			} else if (core.status.hero.items[cls]) {
				list = Object.keys(core.status.hero.items[cls] || {});
			}
			const markedList = list.filter((itemId) => markedItems.includes(itemId)).sort(sortFunc),
				unmarkedList = list.filter((itemId) => !markedItems.includes(itemId)).sort(sortFunc);
			list = [...markedList, ...unmarkedList];
			const hideInfo = core.getFlag('hideInfo', {});
			if (!showHide) list = list.filter(function (id) {
				if (hideInfo[id]) return false;
				return !core.material.items[id].hideInToolbox;
			})
			return list;
		}

		// 复写resize，保证屏幕变化时此画布表现正常
		const originResize = core.control.resize;
		core.control.resize = function () {
			originResize.apply(core.control, arguments);
			const { _back, itemInv, _equipSlots: _equipChangeBoard, _itemInfo: _itemInfoBoard } = UI;
			[_back, itemInv, _equipChangeBoard, _itemInfoBoard].forEach((menu) => { if (menu && menu.onDraw) menu.drawContent(); });
		}

		// #endregion
		const { ButtonBase, RoundBtn, IconBtn, ExitBtn, MenuBase, ArrowBtn, KeyCodeEnum } = core.plugin.uiBase;
		// #region 绘制用到的按钮类

		/** 隐藏物品的按钮 */
		class HideBtn extends RoundBtn {
			constructor(x, y, w, h, config) {
				super(x, y, w, h, '隐藏', config);
			}

			draw() {
				const itemId = UI.itemId;
				if (core.material.items[itemId]) {
					const hideInfo = core.getFlag('hideInfo', {});
					if (hideInfo.hasOwnProperty(itemId)) this.text = hideInfo[itemId] ? "显示" : "隐藏";
					else this.text = core.material.items[itemId].hideInToolbox ? "显示" : "隐藏";
				}
				super.draw();
			}
		}

		/** 置顶物品的按钮 */
		class MarkBtn extends RoundBtn {
			constructor(x, y, w, h, config) {
				super(x, y, w, h, '置顶', config);
			}

			draw() {
				const itemId = UI.itemId;
				const markedItems = core.getFlag('markedItems', []);
				this.text = markedItems.includes(itemId) ? "取消" : "置顶";
				super.draw();
			}
		}

		/** 切换到显示指定分类的物品(如：永久,消耗)的模式的按钮 */
		class ClassifyBtn extends RoundBtn {
			constructor(x, y, w, h, text, subType, config) {
				super(x, y, w, h, text, config);
				this.subType = subType;
			}

			draw() {
				const { type, toolInv } = UI;
				if (toolInv.subType === this.subType) this.status = 'selected';
				else this.status = "none";
				super.draw();
			}
		}

		/** 控制是否显示隐藏物品的按钮 */
		class ShowHideBtn extends ButtonBase {
			draw() {
				const ctx = this.ctx;
				const squareSize = this.h;
				core.strokeRect(ctx, this.x, this.y, squareSize, squareSize, 'black');
				core.fillRect(ctx, this.x + 1, this.y + 1, squareSize - 2, squareSize - 2, 'white');
				const font = core.ui._buildFont(this.h - 4);
				core.setTextAlign(ctx, 'left');
				core.setTextBaseline(ctx, 'middle');
				if (core.hasFlag('showHideItem')) core.fillText(ctx, '√', this.x + 3, this.y + 10, 'red', font);
				core.fillText(ctx, '查看隐藏', this.x + squareSize + 6, this.y + 10, 'white', font);
			}
		}

		/** 切换道具栏和装备栏的按钮 */
		class SwitchBtn extends IconBtn {
			constructor(x, y, w, h, config) {
				super(x, y, w, h, 'toolbox', config);
			}

			draw() {
				this.icon = (UI.type === 'all') ? 'toolbox' : 'equipbox';
				super.draw();
			}
		}

		class NoHoverBtn extends IconBtn {
			draw() {
				super.draw();
				const ctx = this.ctx;
				const { x, y, w, h } = this;
				core.drawImage(ctx, "mousewheel.png", x, y, w, h);
			}
		}

		/** 切换装备面板的单个装备选框 */
		class EquipBox extends ButtonBase {
			constructor(x, y, w, h) {
				super(x, y, w, h);
				/** @type {EquipSlots} */ // @ts-ignore
				this.menu;
				this.key = -1;
			}

			draw() {
				const ctx = this.ctx;
				const [x, y, w, h] = [this.x, this.y, this.w, this.h];
				const space = 2,
					lineWidth = 2,
					squareSize = w;
				const equipId = core.getEquip(this.menu.getTotalIndex(this.key));
				if (equipId) core.drawIcon(ctx, equipId, x + 4, y + 4, squareSize - 8, squareSize - 8);
				const color = (UI.selectType === 'equipBox' && this.menu.index === this.key) ? 'gold' : 'white';
				core.strokeRect(ctx, x, y, squareSize, squareSize, color, lineWidth);
				core.setTextAlign(ctx, "center");
				core.setTextBaseline(ctx, "top");
				const tx = x + w / 2,
					ty = y + squareSize + space;
				core.fillText(ctx, this.menu.currItemList[this.key], tx, ty, color, '14px Verdana');
			}
		}

		// #endregion
		// #region 绘制用到的菜单类

		// 道具栏/装备栏的背景
		class ItemBoxBack extends MenuBase {
			constructor() {
				// 装备栏和道具栏共用同一个光标，故所有按键事件全部写在这里处理
				super('itemBoxBack', ['ondown', 'keyDown', 'keyUp'], null, null, null, null, 136);
			}

			keyDownEvent(keyCode) {
				const { type, selectType, itemInv, equipSlots } = UI;
				if (keyCode === KeyCodeEnum.Left) { // left	
					if (selectType === 'toolBox') itemInv.pageDown();
					else if (selectType === 'equipBox') {
						if (equipSlots.index === 0) {
							equipSlots.pageDown();
						} else {
							equipSlots.setIndex(equipSlots.index - 1);
						}
					}
				} else if (keyCode === KeyCodeEnum.Right) { // right		
					if (selectType === 'toolBox') itemInv.pageUp();
					else if (selectType === 'equipBox') {
						if (equipSlots.index === equipSlots.currItemList.length - 1) {
							equipSlots.pageUp();
						} else {
							equipSlots.setIndex(equipSlots.index + 1);
						}
					}
				} else if (keyCode === KeyCodeEnum.Up) { // up
					if (selectType === 'toolBox') {
						if (itemInv.index === 0) {
							if (type === 'equips') { // 在仅物品栏模式下点上键到顶，切换到上一页，否则会切换到装备栏
								equipSlots.setIndex(equipSlots.currItemList.length - 1);
							} else {
								itemInv.pageDown(); // 向上到顶将翻到上一页
							}
						} else {
							itemInv.setIndex(itemInv.index - 1);
						}
					} else if (selectType === 'equipBox') {
						if (equipSlots.index >= equipSlots.rowMax) {
							equipSlots.index -= equipSlots.rowMax;
						}
					}
				} else if (keyCode === KeyCodeEnum.Down) { // down
					if (selectType === 'toolBox') {
						if (itemInv.index < itemInv.currItemList.length - 1) {
							itemInv.setIndex(itemInv.index + 1);
						} else {
							itemInv.pageUp(); // 向下到底将翻到下一页
						}
					} else if (selectType === 'equipBox') {
						let newIndex = equipSlots.index + equipSlots.rowMax;
						if (newIndex < equipSlots.currItemList.length - 1) {
							equipSlots.setIndex(newIndex);
						} else {
							equipSlots.setIndex(0);
						}
					}
				}
			}

			keyUpEvent(keyCode, altKey) {
				const { itemId, selectType, itemInv, equipSlots: equipChangeBoard } = UI;
				if (keyCode === KeyCodeEnum.Q) { // Q
					if (UI.type === "equips") UI.exit();
					else UI.switchType();
				} else if (keyCode === KeyCodeEnum.T) { // T
					if (UI.type === "all") UI.exit();
					else UI.switchType();
				} else if (keyCode === KeyCodeEnum.BackSpace || keyCode === KeyCodeEnum.Esc) { // BackSpace/Esc
					UI.exit();
				} else if (keyCode === KeyCodeEnum.Enter || keyCode === KeyCodeEnum.SpaceBar || keyCode === KeyCodeEnum.C) { // Enter/SpaceBar/C
					if (selectType === "toolBox") {
						if (core.material.items[itemId]) itemInv.triggerItem();
					} else if (selectType === "equipBox") {
						equipChangeBoard.triggerItem();
						[UI._equipSlots, UI._equipInv, UI._itemInfo].forEach((menu) => {
							if (menu && menu.onDraw) menu.drawContent();
						});
					} else {
						itemInv.setIndex(0);
					}
				} else if (altKey && keyCode >= 48 && keyCode <= 57) { // 都有自动切装了还有神人想要这个Alt换装 服了
					core.items.quickSaveEquip(keyCode - 48);
					return;
				}
			}

			drawContent() {
				const ctx = this.createCanvas();
				this.drawBackGround(ctx);
				super.drawContent();
			}

			drawBackGround(ctx) {
				core.strokeRoundRect(ctx, 2, 2, 412, 412, 5, 'white', 2);
				core.fillRoundRect(ctx, 3, 3, 410, 410, 5, 'rgb(108, 187, 219)');
				core.drawLine(ctx, 248, 3, 248, 413, 'white', 2); // 左栏和右栏的分界线
				if (UI.type === 'equips') core.drawLine(ctx, 3, 140, 248, 140, 'white', 2); // 装备栏和道具栏的分界线
			}
		}

		// 物品列表和换装界面的共用基类 
		class ItemListBase extends MenuBase {
			constructor(name, x, y, w, h, zIndex) {
				super(name, ['ondown', 'onmove'], x, y, w, h, zIndex);
				/** 当前页 */
				this.page = 0;
				/** 一页最多可容纳的道具数量 */
				this.pageCap = 0;
				/** 当前最大页数，page应小于此值*/
				this.pageMax = 1;
				/** @type {string[]} 此界面总的物品列表 */
				this.allItemList = [];
				/** @type {string[]} 此界面当前页展示的物品列表 */
				this.currItemList = [];
				/** 当前选中了第几个道具 */
				this.index = 0;
			}

			/** 
			 * @abstract 获取最新的物品列表
			 * @returns {string[]}
			 */
			getItemList() { return []; }

			/** 
			 * @virtual 
			 * @description 选中指定位置 
			 * @param {number} index 
			 **/
			setIndex(index) { }

			/** 
			 * @abstract 尝试使用当前选中的物品 
			 */
			triggerItem() { }

			/** 更新物品列表 */
			updateItemList() {
				this.allItemList = this.getItemList();
				this.pageMax = Math.ceil(this.allItemList.length / this.pageCap);
				if (this.pageMax < 1) this.pageMax = 1;
				if (this.page >= this.pageMax) this.page = this.pageMax - 1;
				this.currItemList = this.allItemList.slice(this.page * this.pageCap, (this.page + 1) * this.pageCap);
				if (this.index >= this.currItemList.length && this.currItemList.length > 0) this.setIndex(this.currItemList.length - 1);
			}

			canPageUp() {
				return this.page < this.pageMax - 1;
			}

			canPageDown() {
				return this.page > 0;
			}

			/** 翻页，更新物品列表，并重绘该菜单自身 */
			pageUp() {
				if (!this.canPageUp()) return;
				this.page++;
				this.updateItemList();
				this.setIndex(Math.min(this.index, this.currItemList.length - 1));
				this.drawContent();
			}

			/** 翻页，更新物品列表，并重绘该菜单自身 */
			pageDown() {
				if (!this.canPageDown()) return;
				this.page--;
				this.updateItemList();
				this.setIndex(this.index);
				this.drawContent();
			}
		}

		/** 展示角色当前已穿戴的装备的面板 */
		class EquipSlots extends ItemListBase {
			constructor(x, y, w, h, zIndex) {
				super('equipChangeBoard', x, y, w, h, zIndex);
				this.columnMax = 4;
				this.rowMax = 2;
				this.pageCap = this.columnMax * this.rowMax;
				this.updateItemList();
				const currNameList = this.currItemList;

				const columnCount = Math.min(currNameList.length, this.columnMax), // 判断装备孔数量是否小于最大列数
					rowCount = Math.min(Math.ceil(currNameList.length / this.columnMax), this.rowMax);
				const [boxWidth, boxHeight] = [36, 52];
				const spaceX = (this.w - columnCount * boxWidth) / (1 + columnCount),
					spaceY = (this.h - rowCount * boxHeight) / (1 + rowCount);
				let [xi, yi] = [spaceX, spaceY];

				// 装备孔的按钮在这里注册
				for (let i = 0; i < this.pageCap; i++) {
					if (!this.btnMap.has(i)) {
						const btn = new EquipBox(xi, yi, boxWidth, boxHeight);
						this.registerBtn(i, btn, {
							ondown: function () {
								if (this.index !== i) {
									this.setIndex(i);
								} else this.triggerItem(i);
							}.bind(this),
							onmove: function () {
								if (UI.isNoHover) return;
								if (this.index !== i) {
									this.setIndex(i);
								}
							}.bind(this),
						});
						if ((i >= this.currItemList.length)) btn.disable = true;
					} else {
						const btn = this.btnMap.get(i);
						if (btn) btn.disable = (i >= this.currItemList.length);
					}
					if ((i + 1) % this.columnMax === 0) {
						xi = spaceX;
						yi += spaceY + boxHeight;
					} else { xi += spaceX + boxWidth; }
				}
			}

			drawContent() {
				const ctx = this.createCanvas();
				if (this.pageMax > 1) {
					core.setTextAlign(ctx, "center");
					core.setTextBaseline(ctx, "alphabetic");
					core.fillText(ctx, this.page + 1 + '/' + this.pageMax, this.w / 2, this.h - 2, 'white', '12px Verdana');
				}
				// 切装面板只有1页时不激活翻页按钮
				if (this.allItemList.length < this.pageCap) {
					const pgDown = this.btnMap.get('pgDownBtn');
					const pgUp = this.btnMap.get('pgUpBtn');
					if (pgDown) pgDown.disable = true;
					if (pgUp) pgUp.disable = true;
				}

				super.drawContent();
			}

			/**  注意，对于装备切换面板来说，它的装备列表不是装备本身，而是角色的装备孔 */
			getItemList() {
				return core.status.globalAttribute.equipName;
			}

			/** @param {number} index 根据当前页选中的序号换算对应装备在角色装备中的总序号 */
			getTotalIndex(index) {
				if (index == null) index = this.index;
				return this.page * this.pageCap + index;
			}

			/** @param {number} index */
			changePageByTotalIndex(index) {
				const newPage = Math.floor(index / this.pageCap);
				this.page = newPage;
				this.updateItemList();
			}

			/** 脱下指定位置的装备 */
			triggerItem(index) {
				const totalIndex = this.getTotalIndex(index);
				if (core.status.hero.equipment[totalIndex]) {
					core.unloadEquip(totalIndex);
					core.status.route.push("unEquip:" + totalIndex);
					this.updateItemList();
					this.drawContent();
					UI.itemInv.updateItemList(); //穿脱装备是双向的过程，装备栏和道具栏的物品列表组成都会变
					UI.itemInv.drawContent();
					UI.itemId = '';
				}
			}

			setIndex(index) {
				this.index = index;
				UI.equipInv.index = -1;
				core.ui.clearUIEventSelector(1);
				// 被选中的装备框变色
				this.btnMap.forEach((ele, key) => {
					if (ele instanceof EquipBox) {
						if (key === index) ele.status = 'selected';
						else ele.status = 'none';
					}
				})
				UI.selectType = 'equipBox';
				const totalIndex = this.getTotalIndex(index);
				UI.itemId = core.status.hero.equipment[totalIndex];
				this.drawButtonContent();
			}

			pageUp() {
				if (!this.canPageUp()) return;
				this.page++;
				this.updateItemList();
				this.btnMap.forEach((btn, key) => {
					if (btn instanceof EquipBox) {
						if (Number(key) >= this.currItemList.length) btn.disable = true;
					}
				});
				this.setIndex(Math.min(this.index, this.currItemList.length - 1));
				this.drawContent();
			}

			pageDown() {
				if (!this.canPageDown()) return;
				this.page--;
				this.updateItemList();
				this.btnMap.forEach((btn, key) => {
					if (btn instanceof EquipBox) {
						if (Number(key) <= this.currItemList.length - 1) btn.disable = false;
					}
				});
				this.setIndex(this.index);
				this.drawContent();
			}
		}

		/** 展示角色当前背包物品的面板，有道具/装备两种模式 */
		class InventoryBase extends ItemListBase {
			/**
			 * @param {*} name 
			 * @param {string} x 
			 * @param {number} y 
			 * @param {number} w 
			 * @param {number} h 
			 * @param {number} zIndex 
			 * @param {number} pageCap 单个页面显示的物品数
			 */
			constructor(name, x, y, w, h, zIndex, pageCap) {
				super(name, x, y, w, h, zIndex);

				/** @type {number} 单个物品占据的列宽 */
				this.oneItemHeight = 30;
				/** @type {number} 单个页面显示的物品数, -1是因为最后一行要留给换行按钮*/
				this.pageCap = pageCap;
				this.marginLeft = 15; // 物品栏左边距
				this.marginTop = 40; // 物品栏上边距
				this.x0 = this.x + this.marginLeft; // 物品栏左边距
				this.y0 = this.y + this.marginTop;
				this.w0 = this.w - this.marginLeft - 5; // 物品栏真实宽度
				this.h0 = this.oneItemHeight * (this.pageCap + 1); // +1是因为最后一行要留给翻页按钮 
			}

			drawContent() {
				const ctx = this.createCanvas();
				const { x0, y0, w0, h0, marginTop, marginLeft } = this;
				const { w, h } = this;
				core.fillRect(ctx, marginLeft, marginTop, w0, h0, 'rgb(0, 105, 148)');

				core.setTextBaseline(ctx, "middle");
				for (let i = 0; i < this.currItemList.length; i++) {
					this.drawOneItem(i);
				}
				core.setTextAlign(ctx, "center");
				core.setTextBaseline(ctx, "alphabetic");
				core.fillText(ctx, (this.page + 1) + '/' + this.pageMax, w / 2, y0 + h0 - 4, 'white', '12px Verdana');
				super.drawContent();
			}

			/** @param {number} currIndex  */
			drawOneItem(currIndex) {
				const itemId = this.currItemList[currIndex];
				const ctx = core.dymCanvas[this.name];
				const { x0, y0, marginTop } = this;
				const dy = this.oneItemHeight * currIndex + marginTop;
				/** @type {Item|{}} */
				const item = core.material.items[itemId] || {};
				const num = core.formatBigNumber(core.itemCount(itemId), 5) || 0; // 道具数量过大时需要format

				// 被隐藏的道具在显示时需要半透明
				const hideInfo = core.getFlag('hideInfo', {});
				if (item && (hideInfo.hasOwnProperty(itemId) ? hideInfo[itemId] : item.hideInToolbox)) core.setAlpha(ctx, 0.5);

				// 绘制物品图标
				if (core.material.items[itemId]) core.drawIcon(ctx, itemId, x0 + 4, dy + 6, 18, 18);

				core.setTextAlign(ctx, "right");
				core.setTextBaseline(ctx, "middle");
				// 绘制物品数量 ×几
				const numText = "×" + num;
				core.fillText(ctx, numText, x0 + 218, dy + this.oneItemHeight / 2, 'white', '18px Verdana');

				// 绘制物品名称
				const markedItems = core.getFlag('markedItems', []);
				const name = item.name || "???";
				core.setTextAlign(ctx, "left");
				core.fillText(ctx, name, x0 + 24, dy + this.oneItemHeight / 2, markedItems.includes(itemId) ? 'gold' : 'white', '18px Verdana', 180);
				core.setAlpha(ctx, 1);
			}

			clear() {
				core.clearUIEventSelector(1);
				super.clear();
			}

			/** 绘制选中物品的光标，在selectType或index改变时自动执行绘制/擦除
			 * @param {number} index 选中物品在当前页的序号
			 */
			drawSelector(index) {
				const [x0, y0, w0] = [this.x0, this.y0, this.w0]; // 光标绘制是绝对坐标
				core.drawUIEventSelector(1, 'winskin.png', x0, y0 + index * this.oneItemHeight, w0, this.oneItemHeight, 140);
			}

			/** 选中指定序号的位置，改变选中道具的ID，重绘光标 
			 * @param {number} index 选中物品在当前页的序号
			 */
			setIndex(index) {
				this.index = index;
				if (UI.type === 'equips') {
					UI.equipSlots.index = -1;
					UI.equipSlots.drawButtonContent(); // 清除装备栏的选中状态
					UI.selectType = 'toolBox';
				}
				this.drawSelector(index);
				UI.itemId = this.currItemList[index];
			}

			ondownEvent(_x, _y, px, py) {
				py -= this.marginTop; // 转换为相对于物品栏内部的坐标
				const index = Math.floor(py / this.oneItemHeight);
				if (index < 0 || index >= this.currItemList.length) return;
				if (UI.selectType !== 'toolBox' || this.index !== index) {
					this.setIndex(index);
				} else {
					this.triggerItem();
				}
			}

			onmoveEvent(_x, _y, px, py) {
				if (UI.isNoHover) return;
				py -= this.marginTop; // 转换为相对于物品栏内部的坐标
				const index = Math.floor(py / this.oneItemHeight);
				if (index < 0 || index >= this.currItemList.length) return;
				if (UI.selectType !== 'toolBox' || this.index !== index) {
					this.setIndex(index);
				}
			}
		}

		class ToolInventory extends InventoryBase {
			constructor(x, y, w, h, zIndex, pageCap) {
				super('toolInventory', x, y, w, h, zIndex, pageCap);
				/** @type {'all'|'tools'|'constants'} 当前显示哪个子菜单 */
				this.subType = 'all';

				/** 各个子页面当前选中的位置，用于在切换后显示原位置 */
				this.cache = {
					all: { page: 0, index: 0 },
					tools: { page: 0, index: 0 },
					constants: { page: 0, index: 0 }
				}
			}

			getItemList() {
				return core.getToolboxItems(this.subType, core.hasFlag('showHideItem'));
			}

			triggerItem() {
				const itemId = UI.itemId;
				if (!core.canUseItem(itemId) && itemId !== 'centerFly') {
					core.drawFailTip("当前无法使用" + core.material.items[itemId].name, itemId);
					return;
				}
				UI.clearAll();
				setTimeout(() => {
					core.unlockControl();
					core.tryUseItem(itemId);
				}, 0);
			}

			/** 物品栏仅显示指定类型物品 */
			classify(subType) {
				if (UI.type !== 'all') {
					// 非物品栏类型的菜单不支持分类
					return;
				}
				if (this.subType !== subType) {
					const oldConfig = this.cache[this.subType],
						newConfig = this.cache[subType];
					oldConfig.page = this.page;
					oldConfig.index = this.index;
					this.page = newConfig.page;
					this.index = newConfig.index;
					this.subType = subType;
					this.updateItemList();
					this.setIndex(this.index);
					this.drawContent();
					UI.back.drawContent(); // 切换物品栏类型时需要重绘背景
				}
			}
		}

		class EquipInventory extends InventoryBase {
			getItemList() {
				return core.getToolboxItems('equips', core.hasFlag('showHideItem'));
			}

			triggerItem() {
				const equip = UI.itemId;
				if (!core.canEquip(equip, true)) return;
				const equipPos = core.getEquipTypeById(equip);
				UI.equipSlots.changePageByTotalIndex(equipPos);
				core.loadEquip(equip);
				core.status.route.push("equip:" + equip);
				this.updateItemList(); // 穿上装备会导致道具数量变化，需要重新生成装备列表
				this.setIndex(this.index);
				this.drawContent();
				UI.equipSlots.drawContent();
			}
		}

		class ItemInfoBoard extends MenuBase {
			constructor(x, y, w, h) {
				super('itemInfoBox', ['ondown'], x, y, w, h, 137);
			}

			drawContent() {
				const ctx = this.createCanvas();
				core.strokeRoundRect(ctx, 23, 27, 32, 32, 2, 'white', 2);
				const itemId = UI.itemId;
				if (itemId) core.drawIcon(ctx, itemId, 24, 28, 30, 30);

				// 修改这里可以编辑未选中道具时的默认值
				const defaultItem = { cls: "constants", name: "无道具", id: "-", text: "没有道具最永久" };
				const defaultEquip = { cls: "equips", name: "无装备", id: "-", text: "一无所有，又何尝不是一种装备", equip: { type: "装备" } };
				let item = core.material.items[itemId];
				if (!item) item = (UI.type === 'all' ? defaultItem : defaultEquip);
				core.setTextAlign(ctx, "left");
				core.setTextBaseline(ctx, "middle");
				core.fillText(ctx, item.name, 66, 46, 'black', 'bold 18px Verdana', 98); // 物品名字 e.g.护符
				core.fillText(ctx, "类型", 20, 75, 'crimson', '14px Verdana');
				core.fillText(ctx, "【" + getItemClsName(item) + "】", 50, 75, 'rgb(47, 49, 54)', '14px Verdana'); // 物品类型 e.g.【永久道具】

				if (UI.type === 'all') { // 显示物品累计使用的次数
					core.fillText(ctx, "累计使用", 20, 95, 'crimson', '14px Verdana');
					const itemsUsedCount = core.getFlag('itemsUsedCount', {});
					core.fillText(ctx, itemsUsedCount[itemId] || 0, 80, 95, 'rgb(47, 49, 54)', '14px Verdana');
				}
				const rawItemText = core.replaceText(item.text) ?? "";
				const itemText = rawItemText + ((UI.type === "equips") ? this.getEquipCompareInfo(item) : ""); // 物品描述信息
				core.drawTextContent(ctx, itemText, {
					left: 20,
					top: 110,
					bold: false,
					color: "black",
					align: "left",
					fontSize: 15,
					maxWidth: 150
				});
				const currItemHotKey = HotkeySelect.getHotkeyNum(itemId);
				// 获取快捷键设置按钮当前的图标

				const setHotkeyBtn = /** @type {IconBtn} */ (this.btnMap.get('setHotkeyBtn'));
				if (setHotkeyBtn) {
					setHotkeyBtn.disable = (UI.type === 'equips');
					setHotkeyBtn.icon = (currItemHotKey == null) ? 'keyboard' : ('btn' + currItemHotKey);
				}
				super.drawContent();
			}

			/*** @param {Item} item */
			getEquipCompareInfo(item) {
				let str = '';
				if (UI.type !== "equips") return str;

				let equipType = item.equip?.type;
				if (!equipType) return str;
				if (typeof equipType == "string") equipType = core.getEquipTypeByName(equipType);
				let compare;
				/** @todo 准备卸下装备时显示卸下的比较信息 */
				if (UI.selectType == "equipBox") compare = core.compareEquipment(null, item.id);
				else compare = core.compareEquipment(item.id, core.getEquip(equipType));
				// --- 变化值...
				for (const name in core.status.hero) {
					if (typeof core.status.hero[name] != 'number') continue;
					let nowValue = core.getRealStatus(name);
					let newValue = Math.floor((core.getStatus(name) + (compare.value[name] || 0)) *
						(core.getBuff(name) * 100 + (compare.percentage[name] || 0)) / 100);
					if (nowValue === newValue) continue;
					const color = newValue > nowValue ? '#00FF00' : '#FF0000';
					const [nowValueStr, newValueStr] = [nowValue, newValue].map(value => core.formatBigNumber(value));
					str += "\n" + core.getStatusLabel(name) + " " + nowValueStr + "->\r[" + color + "]" + newValueStr + "\r";
				}
				return str;
			}
		}

		/** 取消设置快捷键的按钮 */
		class SetNullBtn extends ButtonBase {
			draw() {
				const [x, y, w, h] = [this.x, this.y, this.w, this.h];
				core.strokeRect(this.ctx, x, y, w, h, 'red', 2);
				core.drawLine(this.ctx, x, y, x + w, y + h, 'red', 2);
			}
		}

		// 为当前道具设定一个快捷键
		class HotkeySelect extends MenuBase {
			constructor(itemId, x, y, w, h, zIndex) {
				super('hotkeySelect', ['ondown', 'keyDown'], x, y, w, h, zIndex);
				this.itemId = itemId;
				/** @type {number | null} null代表当前道具没有快捷键 */
				this.hotkeyNum = HotkeySelect.getHotkeyNum(this.itemId);
			}

			drawContent() {
				const ctx = this.createCanvas();
				const [x, y, w, h] = [this.x, this.y, this.w, this.h];
				core.fillRect(ctx, 3, 3, w - 6, h - 6, ' #A8CABA');
				core.strokeRect(ctx, 0, 0, w, h, ' #004B23', 3);
				core.setTextAlign(ctx, 'center');
				core.setTextBaseline(ctx, 'alphabetic');
				core.fillText(ctx, '为当前道具选择一个快捷键', this.w / 2, 40, 'black', '20px Verdana');
				core.fillText(ctx, '无自定义设置时样板的默认快捷键', this.w / 2, 60, 'gray', '16px Verdana');
				core.fillText(ctx, '(如123分别对应破炸飞)不可在此设置', this.w / 2, 80, 'gray', '16px Verdana');
				// 无自定义设置时样板的默认快捷键（如123分别对应破炸飞），不可在此设置
				// 绘制指向当前快捷键的监听
				if (this.hotkeyNum != null) {
					const btn = this.btnMap.get('btn' + this.hotkeyNum);
					if (btn) {
						core.fillPolygon(ctx, [
							[btn.x + 12, btn.y + btn.h + 10],
							[btn.x + btn.w - 12, btn.y + btn.h + 10],
							[btn.x + btn.w / 2, btn.y + btn.h + 2]
						], 'black');
					}
				}
				super.drawContent();
			}
			/* @__PURE__ */
			static getHotkeyNum(itemId) {
				for (let i = 1; i <= 9; i++) {
					const currHotkey = core.getLocalStorage('hotkey' + i, null);
					if (currHotkey === itemId) {
						return i;
					}
				}
				return null;
			}

			deleteHotkey() {
				if (this.hotkeyNum != null) core.setLocalStorage('hotkey' + this.hotkeyNum, null);
			}

			setHotkey(num) {
				this.deleteHotkey();
				core.setLocalStorage('hotkey' + num, this.itemId);
				this.hotkeyNum = Number(num);
			}

			clear() {
				super.clear();
				const { back, itemInv, equipSlots, itemInfo } = UI;
				[back, itemInv, equipSlots, itemInfo].forEach(menu => {
					if (menu.onDraw) menu.beginListen(); // 注意本来就没在绘制的则不监听
				});
				UI.itemInfo.drawContent(); // 快捷键图标会发生变化
			}
		}

		/** @param {string} itemId */
		function hotkeySelectFactory(itemId) {
			const hotkeySelect = new HotkeySelect(itemId, 60, 100, 296, 206, 141); // 应当比物品背包的选择光标大，遮盖住前者
			const setHotkeyNum = function (i) {
				const num = this.key.replace('btn', '');
				hotkeySelect.setHotkey(num);
				hotkeySelect.clear();
				UI.itemInfo.drawContent();
			}

			const [btnSize, btnInterval] = [32, 20];
			const leftMargin = hotkeySelect.w / 2 - 2.5 * btnSize - 2 * btnInterval;
			const style = { strokeStyle: 'none', fillStyle: 'none' };

			for (let i = 0; i < 9; i++) {
				const num = i + 1;
				const row = (i <= 4) ? 1 : 2;
				let btn;
				if (row === 1) btn = new IconBtn(leftMargin + i * (btnSize + btnInterval), 100, btnSize, btnSize, 'btn' + num, style);
				else btn = new IconBtn(leftMargin + (i - 5) * (btnSize + btnInterval), 150, btnSize, btnSize, 'btn' + num, style);
				hotkeySelect.registerBtn('btn' + num, btn, () => setHotkeyNum.call(btn, i));
			}
			const setNullBtn = new SetNullBtn(leftMargin + 4 * (btnSize + btnInterval), 150, btnSize, btnSize);
			hotkeySelect.registerBtn('setNullBtn', setNullBtn, () => {
				hotkeySelect.deleteHotkey();
				hotkeySelect.clear();
				UI.itemInfo.drawContent();
			});
			const exitBtn = new ExitBtn(274, 5, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 });
			hotkeySelect.registerBtn('exitBtn', exitBtn, () => hotkeySelect.clear());
			return hotkeySelect;
		}

		// #endregion
		// #region 核心功能函数和全局变量
		function getItemClsName(item) {
			const itemClsName = {
				"constants": "永久道具",
				"tools": "消耗道具",
			}
			if (item == null) return "未知";
			if (item.cls == "equips") {
				if (typeof item.equip.type == "string") return item.equip.type;
				const type = core.getEquipTypeById(item.id);
				return core.status.globalAttribute.equipName[type];
			} else return itemClsName[item.cls] || item.cls;
		}

		function clearItemBoxCache() {
			UI._itemId = '';
			UI.selectType = 'toolBox';
			[UI._toolInv, UI._equipInv, UI._equipSlots].forEach(menu => {
				if (menu) {
					menu.index = 0;
					menu.page = 0;
				}
			});
		} // 每次存读档，及进行录像回放时调用，清空之前选中的道具信息
		this.clearItemBoxCache = clearItemBoxCache;

		// 以下是本插件范围内的全局变量

		const UI = {
			/** 当前打开的是道具页还是装备页 
			 * @type {'all'|'equips'} 
			 **/
			type: 'all',
			/** @type {'toolBox'|'equipBox'} 当前选中了哪一个子界面（切装面板/物品栏） */
			selectType: 'toolBox',
			_itemId: '',
			/** @type {string} 当前选中的物品ID，变化时自动触发右侧栏的信息变化 */
			get itemId() {
				return this._itemId;
			},
			set itemId(value) {
				if (this._itemId !== value) {
					this._itemId = value;
					this.itemInfo.drawContent();
				}
			},
			/** @type {undefined|ItemBoxBack} 物品页面的背景 */
			_back: undefined,
			/** @type {undefined|ToolInventory} 道具背包 */
			_toolInv: undefined,
			/** @type {undefined|EquipInventory} 装备背包 */
			_equipInv: undefined,
			/** @type {undefined|ItemInfoBoard} 右侧显示选中物品详细信息的页面 */
			_itemInfo: undefined,
			/** @type {undefined|EquipSlots} 显示已穿戴装备的面板 */
			_equipSlots: undefined,
			/** 是否监听onMove事件 */
			isNoHover: core.getLocalStorage('itemBoxNoHover'),
			/** 物品页面的背景 */
			get back() {
				if (!this._back) {
					this._back = new ItemBoxBack();
					const switchModeBtn = new SwitchBtn(385, 5, 24, 24, { strokeStyle: ' #8B4513', fillStyle: ' #D2691E' });
					const exitBtn = new ExitBtn(385, 385, 24, 24);
					this._back.registerBtns([
						['exitBtn', exitBtn, UI.exit.bind(UI)],
						['switchModeBtn', switchModeBtn, UI.switchType.bind(UI)]
					]);
				}
				return this._back;
			},
			/** 道具背包 */
			get toolInv() {
				if (!this._toolInv) {
					this._toolInv = new ToolInventory(0, 0, 240, 416, 137, 11);
					const pgDown = new ArrowBtn(20, 375, 20, 20, 'left');
					const pgUp = new ArrowBtn(215, 375, 20, 20, 'right');
					this.toolInv.registerBtns([
						['pgDownBtn', pgDown, () => UI.toolInv.pageDown()],
						['pgUpBtn', pgUp, () => UI.toolInv.pageUp()]
					]);
					const allBtn = new ClassifyBtn(20, 10, 44, 24, "全部", "all"),
						toolsBtn = new ClassifyBtn(80, 10, 44, 24, "消耗", "tools"),
						constantsBtn = new ClassifyBtn(140, 10, 44, 24, "永久", "constants");
					const noHoverBtn = new NoHoverBtn(210, 10, 24, 24, null, { crossline1: this.isNoHover });
					const noHover = () => {
						UI.isNoHover = !UI.isNoHover;
						core.setLocalStorage('itemBoxNoHover', UI.isNoHover);
						noHoverBtn.config.crossline1 = UI.isNoHover;
						UI._toolInv?.drawButtonContent();
					}
					this._toolInv.registerBtns([
						['allBtn', allBtn, () => this.toolInv.classify('all')],
						['toolsBtn', toolsBtn, () => this.toolInv.classify('tools')],
						['constantsBtn', constantsBtn, () => this.toolInv.classify('constants')],
						['noHoverBtn', noHoverBtn, noHover]
					]);
					if (!core.platform.isPC) noHoverBtn.disable = true; // 非PC端本来就触发不了onmove事件，故隐藏此按钮
				}
				return this._toolInv;
			},
			/** 装备背包 */
			get equipInv() {
				if (!this._equipInv) {
					this._equipInv = new EquipInventory('equipInventory', 0, 120, 240, 416, 137, 7);
					const pgDown = new ArrowBtn(20, 255, 20, 20, 'left');
					const pgUp = new ArrowBtn(215, 255, 20, 20, 'right');;
					this._equipInv.registerBtn('pgDownBtn', pgDown, () => UI.equipInv.pageDown());
					this._equipInv.registerBtn('pgUpBtn', pgUp, () => UI.equipInv.pageUp());
				}
				return this._equipInv;
			},
			/** 物品背包 */
			get itemInv() {
				return (this.type === 'all') ? this.toolInv : this.equipInv;
			},
			/** 右侧显示选中物品详细信息的页面 */
			get itemInfo() {
				if (!this._itemInfo) {
					this._itemInfo = new ItemInfoBoard(240, 0, core.__PIXELS__ - 240, core.__PIXELS__);
					const hideBtn = new HideBtn(20, 380, 46, 24);
					this._itemInfo.registerBtn('hideBtn', hideBtn, () => {
						this.hideItem(UI.itemId);
						this.itemInv.updateItemList();
						this.itemInv.setIndex(this.itemInv.index);
						this.itemInv.drawContent();
					});
					const markBtn = new MarkBtn(80, 380, 46, 24);
					this._itemInfo.registerBtn('markBtn', markBtn, () => {
						this.markItem(UI.itemId);
						this.itemInv.updateItemList();
						this.itemInv.setIndex(this.itemInv.index);
						this.itemInv.drawContent();
					});
					const showHideBtn = new ShowHideBtn(20, 350, 95, 18);
					this._itemInfo.registerBtn('showHideBtn', showHideBtn, () => {
						this.switchShowHide();
						this.itemInv.updateItemList();
						this.itemInv.setIndex(this.itemInv.index);
						this.itemInv.drawContent();
						this.itemInfo.drawContent(); // 这里不论选中物品是否变化，itemInfo必定要重绘，因为按钮图案会变
					});
					const setHotkeyBtn = new IconBtn(145, 60, 24, 24, 'keyboard');
					this.itemInfo.registerBtn('setHotkeyBtn', setHotkeyBtn, () => {
						if (!UI.itemId) return;
						[UI._back, UI.itemInv, UI._equipSlots, UI._itemInfo].forEach((menu) => {
							if (menu) menu.endListen();
						});
						const hotkeySelect = hotkeySelectFactory(UI.itemId);
						hotkeySelect.init();
					});
				}
				return this._itemInfo;
			},
			get equipSlots() {
				if (!this._equipSlots) {
					this._equipSlots = new EquipSlots(7, 10, 240, 125, 137);
					const config = { marginTail: 4, marginSide: 3, marginTip: 2 };
					const pgDown = new ArrowBtn(0, 56, 14, 14, 'left', config);
					const pgUp = new ArrowBtn(224, 56, 14, 14, 'right', config);
					this._equipSlots.registerBtn('pgDownBtn', pgDown, () => UI.equipSlots.pageDown());
					this._equipSlots.registerBtn('pgUpBtn', pgUp, () => UI.equipSlots.pageUp());
				}
				return this._equipSlots;
			},
			/** 清空各个菜单的绘制和监听(不包括解除锁定) */
			clearAll() {
				[this._back, this._toolInv, this._equipInv, this._itemInfo, this._equipSlots].forEach((menu) => {
					if (menu) menu.clear();
				});
				core.status.event.id = null;
			},
			initAll() {
				[UI.back, UI.itemInv, UI.itemInfo].forEach((menu) => menu.init());
				if (UI.type === 'equips') UI.equipSlots.init();
			},
			/** 解除锁定，退出道具栏/装备栏 */
			exit() {
				this.clearAll();
				setTimeout(core.unlockControl, 0);
			},
			/** 在道具栏/装备栏模式中切换 */
			switchType() {
				this.type = (this.type === 'all') ? 'equips' : 'all';
				if (this.type === 'all') {
					this.equipSlots.clear();
					this.equipInv.clear();
				} else if (this.type === 'equips') {
					this.toolInv.clear();
					this.equipSlots.init();
				}
				this.itemInv.updateItemList();
				this.back.drawContent();
				this.itemInv.init();
				if (this.type === 'all') {
					this.itemInv.setIndex(this.itemInv.index);
				} else if (this.type === 'equips') {
					if (UI.selectType === 'toolBox') {
						this.itemInv.setIndex(this.itemInv.index);
					} else {
						this.equipSlots.setIndex(this.equipSlots.index);
					}
				}
			},
			/** 隐藏 | 取消隐藏当前选中的物品 */
			hideItem(itemId) {
				if (!itemId) return;
				let hideInfo = core.getFlag('hideInfo', {});
				if (hideInfo.hasOwnProperty(itemId)) {
					hideInfo[itemId] = !hideInfo[itemId];
				} else {
					hideInfo[itemId] = !core.material.items[itemId].hideInToolbox;
				}
				core.setFlag('hideInfo', hideInfo);
			},
			/** 置顶 | 取消置顶当前选中的物品 */
			markItem(itemId) {
				let markedItems = core.getFlag('markedItems', []);
				if (markedItems.includes(itemId)) markedItems = markedItems.filter((currId) => currId !== itemId);
				else markedItems.push(itemId);
				core.setFlag('markedItems', markedItems);
			},
			/** 切入/切出显示已隐藏物品的模式 */
			switchShowHide() {
				core.setFlag('showHideItem', !core.getFlag('showHideItem', false));
			}
		}

		// 程序入口，在_drawToolbox 与 _drawEquipbox处调用
		/** @param {'all'|'equips'} currType  */
		function drawItemBox(currType) {
			UI.clearAll();
			if (UI._toolInv && UI._toolInv?.onDraw && currType === UI.type) return;
			core.lockControl();
			UI.type = currType;
			UI.itemInv.updateItemList();
			UI.itemInv.setIndex(UI.itemInv.index);
			UI.initAll();
		}
		// #endregion
	},
    "autoChangeEquip": function () {
		// 调用方法：在合适的位置调用函数figureEquip即可，例如在脚本编辑-按键处理加入case 89: core.plugin.figureEquip(); break;
		// 即按Y键进入切装模式

		let compareMode = false;
		let equipStatus = [];
		let equipIncluded;

		////// 请在[]中填好不参与换装的装备孔的序号。
		// 例如，0号，4号装备孔不参与换装，则 ignoreList 应设为[0,4]
		// 所有装备孔都参与换装，则 ignoreList 应设为[]
		let ignoreList = [];

		////// 请在{}中根据装备的穿脱事件手动填写装备穿脱时要执行的函数，没有则不填。只填写有效的数值变化即可。
		// 例如:{'sword3':{'equip':function(){core.setFlag('mms3',1);},'unequip':function(){core.setFlag('mms3',0);}}}
		let equipEvents = {};

		function compareEquip() {

			return new Promise(function (res) {

				const canvas = 'compareEquip',
					width = core._PX_ || core.__PIXELS__,
					height = core._PY_ || core.__PIXELS__;

				core.lockControl();

				function finish() {
					compareMode = false;
					core.unregisterAction('onclick', 'bestEquip');
					core.deleteCanvas(canvas);
					res(void 0);
				}

				core.createCanvas(canvas, 0, 0, width, height, 160);
				core.setTextAlign(canvas, 'center');
				core.fillText(canvas, '点击选择一个怪物,点击非怪物图块自动退出', width / 2, 20, 'red', '18px Arial');

				core.registerAction('onclick', 'bestEquip', function (x, y, px, py) {
					const cls = core.getBlockCls(x, y),
						id = core.getBlockId(x, y);
					if (!(cls === 'enemys' || cls === "enemy48")) {
						finish();
						return false;
					}
					figureBestEquip(id, x, y);
					core.updateDamage();
					finish();
				}, 100);
			})
		}

		function figureBestEquip(id, x, y) {
			compareMode = true;
			const equipNum = core.status.globalAttribute.equipName.length; // 装备总数量

			// 角色初始各项数值，用于推算出最优切装后复原初始状态
			const oriEffect = {
				'value': { 'atk': core.status.hero.atk, 'def': core.status.hero.def, 'mdef': core.status.hero.mdef, },
				'percentage': { 'atk': core.getBuff('atk'), 'def': core.getBuff('def'), 'mdef': core.getBuff('mdef'), },
				'equipment': core.clone(core.status.hero.equipment),
			}

			if (!equipIncluded) equipIncluded = getEquipIncluded(equipNum);

			const equipIncludedNum = equipIncluded.length;
			const equipNameList = core.status.globalAttribute.equipName.filter((ele, i) => { return !ignoreList.includes(i); });

			equipStatus = equipIncluded.map((ele) => core.getEquip(ele)); //当前参与计算的各个装备孔的装备
			const equipOwned = getEquipOwned(equipNum);
			let equipList = getEquipList(equipIncludedNum, equipOwned, equipNameList);

			const equipCombination = traverseSetCombinations(equipList);

			const bestCombination = findBestEquipComb(equipCombination, equipOwned, id, x, y);

			['atk', 'def', 'mdef'].forEach((ele) => {
				core.setStatus(ele, oriEffect.value[ele]);
				core.setBuff(ele, oriEffect.percentage[ele]);
			});
			core.status.hero.equipment = core.clone(oriEffect.equipment);

			equipBestComb(bestCombination, equipIncluded, equipNameList);
		}

		// 返回一个包含所有参与切装计算的装备孔的序号的数组。
		// 例如，0，2，4号装备孔参与切装计算，则本函数返回[0,2,4]
		function getEquipIncluded(equipNum) {
			let equipIncluded = [];
			for (let i = 0; i < equipNum; i++) {
				if (!ignoreList.includes(i)) equipIncluded.push(i);
			}
			return equipIncluded;
		}

		function getEquipOwned(equipNum) {
			// equipOwned:当前拥有的所有装备的数量
			// 形如{sword1: 2, sword2: 1}
			let equipOwned = core.clone(core.status.hero.items.equips);
			for (let i = 0; i < equipNum; i++) {
				if (ignoreList.includes(i)) continue;
				const currEquip = core.getEquip(i);
				if (currEquip !== null)
					if (equipOwned.hasOwnProperty(currEquip)) equipOwned[currEquip]++;
					else equipOwned[currEquip] = 1;
			}
			return equipOwned;
		}

		// 生成切装列表，为一个二维数组
		function getEquipList(equipNum, equipOwned, equipNameList) {
			// equipNameList:计入切装计算的装备格子的名称列表，可重复
			// 形如['武器', '武器', '盾牌']
			let equipList = Array(equipNameList.length).fill(void 0).map(() => new Set([null]));

			//对每个装备孔展开
			for (let i = 0, l = equipNameList.length; i < l; i++) {
				for (let j in equipOwned) {
					let equipType = core.material.items[j].equip.type;
					switch (typeof equipType) {
						case 'number':
							for (let k = 0, l = equipOwned[j]; k < l; k++) {
								equipList[equipIncluded.indexOf(equipType)].add(j);
							}
							break;
						case 'string':
							if (equipType === equipNameList[i])
								for (let k = 0, l = equipOwned[j]; k < l; k++) { equipList[i].add(j); }
							break;
					}
				}
			}
			return equipList;
		}

		function traverseSetCombinations(arr) {
			const result = [];
			const currentCombination = [];

			function backtrack(index) {
				if (index === arr.length) {
					result.push([...currentCombination]);
					return;
				}
				const currentSet = Array.from(arr[index]);
				for (let value of currentSet) {
					currentCombination[index] = value;
					backtrack(index + 1);
				}
			}
			backtrack(0);
			return result;
		}

		function getEleCount(ele, arr) {
			let count = 0;
			for (let i = 0, l = arr.length; i < l; i++) {
				if (arr[i] === ele) count++;
			}
			return count;
		}

		function hasEnoughEquip(currComb, equipOwned) {
			for (let i in equipOwned) {
				const equipNeed = getEleCount(i, currComb);
				if (equipOwned[i] < equipNeed) return false;
			}
			return true;
		}

		// 按照给定的列表aimStatus，形如['sword1','sword2',null,'sword1'],修改equipStatus进行模拟切装
		function simulateEquip(equipStatus, aimStatus) {
			equipIncluded.forEach((ele, i) => { core.status.hero.equipment[ele] = aimStatus[i]; })
			for (let i = 0, l = equipStatus.length; i < l; i++) {
				if (equipStatus[i] !== aimStatus[i]) {
					if (aimStatus[i] === null) {
						const unequipId = equipStatus[i];
						if (equipEvents.hasOwnProperty(unequipId) &&
							equipEvents[unequipId].hasOwnProperty('unequip'))
							equipEvents[unequipId].unequip();
						core.items._loadEquipEffect(null, unequipId);
					} else {
						const equipId = aimStatus[i];
						if (equipEvents.hasOwnProperty(equipId) &&
							equipEvents[equipId].hasOwnProperty('equip'))
							equipEvents[equipId].equip();
						core.items._loadEquipEffect(equipId, equipStatus[i]);
					}
					equipStatus[i] = aimStatus[i];
				}
			}
		}

		function findBestEquipComb(equipCombination, equipOwned, id, x, y) {
			let minDamage = core.getDamage(id, x, y),
				bestCombination = core.clone(equipStatus);
			for (let i = 0, l = equipCombination.length; i < l; i++) {
				const currComb = equipCombination[i];
				if (!hasEnoughEquip(currComb, equipOwned)) continue;
				simulateEquip(equipStatus, currComb);
				let damage = core.getDamage(id, x, y);
				if (damage !== null && (minDamage === null || damage < minDamage)) {
					minDamage = damage;
					bestCombination = core.clone(equipStatus);
				}
			}
			return bestCombination;
		}

		function equipBestComb(bestCombination, equipIncluded, equipNameList) {
			/** @type {Set<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);
		/** @type {SettingBack} */
		const settingMenu = core.plugin.settingMenu;
		if (settingMenu && settingMenu.onDraw) {
			settingMenu.drawContent();
			settingMenu.pageList[settingMenu.currPage].drawContent();
		}
	}
	// #endregion

	/**
	 * @typedef {{
	 * getName:()=>string,
	 * effect:()=>void,
	 * text:string,
	 * replay?:boolean,
	 * draw?:((ctx:string)=>void)
	 * }} Setting
	 */
	function invertFlag(name) {
		core.setFlag(name, !core.hasFlag(name));
	}

	function invertLocalStorage(name) {
		const value = core.getLocalStorage(name, false);
		core.setLocalStorage(name, !value);
	}

	// #endregion 
	// #region 设置的具体内容，与相应的录像注册
	// #endregion
	// #region 按钮类

	const {
		ButtonBase,
		RoundBtn,
		IconBtn,
		ExitBtn,
		ArrowBtn,
		MenuBase,
		Pagination,
		KeyCodeEnum
	} = core.plugin.uiBase;

	class TextButton extends ButtonBase {
		constructor(x, y, w, h, text) {
			super(x, y, w, h);
			this.text = text;
			this.draw = () => {
				if (this.disable) return;
				core.ui.fillText(this.ctx, this.text,
					this.x + this.w / 2, this.y + this.h / 2 + 5, 'white', '16px Verdana');
			}
		}
	}
	// #endregion
	// #region 菜单类

	// #region 单个设置菜单的基类
	/** 单个设置按钮的基类 */
	class SettingButton extends ButtonBase {
		constructor(x, y, w, h, ...drawArgs) {
			super(x, y, w, h);
			/** @type {Setting} 将在registerBtn时赋予此项 */ // @ts-ignore
			this.setting;
			this.drawArgs = drawArgs;
		}

		draw() {
			const ctx = this.ctx;
			const [x, y, w, h] = [this.x, this.y, this.w, this.h];
			const setting = this.setting;
			// 取消注释下面这一句将显示所有按钮的判定框
			// core.strokeRect(ctx, this.x, this.y, this.w, this.h, 'yellow');
			core.setTextAlign(ctx, 'start');
			core.ui.fillText(ctx, setting.getName(), x, y + h / 2 + 5, 'white', '16px Verdana');
			const drawFunc = setting.draw;
			if (this.status === 'selected') {
				core.drawUIEventSelector(0, "winskin.png", this.x, this.y, this.w, this.h, 137);
			}
			if (drawFunc) drawFunc.apply(this, [ctx, ...this.drawArgs]);
		}
	}

	// @todo 悬浮按钮不改变时不触发事件 这个优化有没有做

	/** 单个设置菜单的基类 */
	class SettingOnePage extends MenuBase {
		constructor(name, settingData) {
			super(name, ['ondown', 'onmove', 'keyDown']);
			this.text = '';
			/** @type {string | undefined}*/
			this.selectedPos;
			/** @type {SettingButton | undefined}*/
			this.selectedBtn;
			/** @type {{[x:string]:Setting}}*/
			this.settingData = settingData;
		}

		keyDownEvent(keyCode) {
			let x, y;
			const changePos = (newPos) => {
				if (this.btnMap.has(newPos)) {
					const button = this.btnMap.get(newPos);
					this.focus(button, newPos);
				}
			}
			if ([KeyCodeEnum.Left, KeyCodeEnum.Right, KeyCodeEnum.Up, KeyCodeEnum.Down].includes(keyCode)) {
				if (!this.selectedBtn || !this.selectedPos) {
					const button = this.btnMap.get('1,1');
					if (button) this.focus(button, '1,1');
					return;
				} else {
					[x, y] = this.selectedPos.split(',').map((x) => parseInt(x));
					if (keyCode === KeyCodeEnum.Left) x--;
					if (keyCode === KeyCodeEnum.Up) y--;
					if (keyCode === KeyCodeEnum.Right) x++;
					if (keyCode === KeyCodeEnum.Down) y++;
					let newPos = x + ',' + y;

					// 逻辑：左右，查找不到对应坐标就不动。
					// 上下，查找不到对应坐标，只要该列存在第一个元素，就会移到该列。

					if (keyCode === KeyCodeEnum.Left || keyCode === KeyCodeEnum.Right) {
						changePos(newPos);
					}
					if (keyCode === KeyCodeEnum.Up || keyCode === KeyCodeEnum.Down) {
						if (this.btnMap.has(newPos)) {
							const button = this.btnMap.get(newPos);
							this.focus(button, newPos);
						} else {
							newPos = '1,' + y;
							changePos(newPos);
						}
					}
				}
			} else {
				switch (keyCode) {
				case KeyCodeEnum.Enter: // Enter/Space
				case KeyCodeEnum.SpaceBar:
					if (this.selectedBtn) this.selectedBtn.ondown(-1, -1, -1, -1);
					break;
				}
			}
		}

		/** 
		 * @param {SettingButton} btn  
		 * @param {string} key 对应事件的索引
		 **/
		execEffect(btn, key, ...eventArgs) {
			const setting = btn.setting;
			setting.effect.apply(this, eventArgs);
			if (setting.replay) {
				let actionString = 'cSet:' + key;
				if (eventArgs && eventArgs.length > 0) {
					actionString += ':' + eventArgs.map(arg => encodeURIComponent(arg)).join(':');
				}
				core.status.route.push(actionString);
			}
			this.drawContent();
		}

		/** 聚焦到指定的按钮并重绘菜单 */
		focus(button, pos) {
			if (this.selectedBtn === button) return; // 如果当前按钮已被选中，则不做任何操作
			this.selectedPos = pos;
			this.selectedBtn = button;
			this.btnMap.forEach((currBtn) => {
				currBtn.status = (currBtn === button) ? 'selected' : 'none';
			});
			if (button instanceof SettingButton) {
				this.text = button.setting.text || '';
			}
			this.drawContent();
		}

		drawContent(ctx) {
			if (!ctx) ctx = this.name;
			super.drawContent();
			if (this.text && this.text.length > 0) {
				core.ui.drawTextContent(ctx, this.text, {
					left: 30,
					top: 78,
					bold: false,
					color: "white",
					align: "left",
					fontSize: 14,
					maxWidth: 350
				});
			}
		}

		clear() {
			core.clearUIEventSelector(0); // 光标的绘制在按钮中进行
			super.clear();
		}

		/**
		 * @override
		 * @param {string} pos 
		 * @param {string} key
		 * @param {SettingButton} btn 
		 * @param {...(string|number)} eventArgs
		 */
		registerSettingBtn(pos, key, btn, ...eventArgs) {
			super.registerBtn(pos, btn, {
				'ondown': () => {
					if (this.selectedBtn === btn) this.execEffect(btn, key, ...eventArgs);
					else this.focus(btn, pos);
				},
				'onmove': () => {
					this.focus(btn, pos);
				}
			});
			btn.setting = this.settingData[key];
		}

		registerSettingBtns(arr) {
			arr.forEach(ele => {
				const [key, btn, event, ...eventArgs] = ele;
				this.registerSettingBtn(key, btn, event, ...eventArgs);
			});
		}
	}
	// #endregion

	// #region 功能菜单
	function checkSkipFuncs() {
		const skipText = core.getLocalStorage('skipText');
		// 此函数用于检测是否处在录像模式下，是则将跳过所有非必要对话
		core.events.__action_checkReplaying = skipText ? function () {
			core.doAction();
			return true;
		}.bind(core.events) : events.prototype.__action_checkReplaying;

		const skipPeform = core.getLocalStorage('skipPeform');

		const instantMove = function (fromX, fromY, aimX, aimY, keep, callback) {
			const [_block, blockInfo] = core.maps._getAndRemoveBlock(fromX, fromY);
			if (keep) {
				core.setBlock(blockInfo.number, aimX, aimY);
				core.showBlock(aimX, aimY);
			}
			if (callback) callback();
		}

		core.maps.jumpBlock = skipPeform ? function (sx, sy, ex, ey, time, keep, callback) {
			return instantMove(sx, sy, ex, ey, keep, callback);
		}.bind(core.maps) : maps.prototype.jumpBlock;

		core.maps.moveBlock = skipPeform ? function (x, y, steps, time, keep, callback) {
			maps.prototype.moveBlock(x, y, steps, 1, keep, callback);
		}.bind(core.maps) : maps.prototype.moveBlock;

		core.maps.drawAnimate = skipPeform ? function (name, x, y, alignWindow, callback) {
			if (callback) callback();
			return -1;
		}.bind(core.maps) : maps.prototype.drawAnimate;

		core.maps.drawHeroAnimate = skipPeform ? function (name, callback) {
			if (callback) callback();
			return -1;
		}.bind(core.maps) : maps.prototype.drawHeroAnimate;

		core.events.jumpHero = skipPeform ? function (ex, ey, time, callback) {
			const { x: sx, y: sy } = core.status.hero.loc;
			if (ex == null) ex = sx;
			if (ey == null) ey = sy;
			core.setHeroLoc('x', ex);
			core.setHeroLoc('y', ey);
			core.clearMap('hero');
			core.drawHero();
			if (callback) callback();
		}.bind(core.events) : events.prototype.jumpHero;

		core.events.vibrate = skipPeform ? function (direction, time, speed, power, callback) {
			if (callback) callback();
			return;
		}.bind(core.events) : events.prototype.vibrate;

		core.events._action_sleep = skipPeform ? function (data, x, y, prefix) {
			core.doAction();
		}.bind(core.events) : events.prototype._action_sleep;
	}
	this.checkSkipFuncs = checkSkipFuncs;
	/** @type {{[x:string]:Setting}} */
	const gamePlaySetting = {
		autoGet: {
			getName: () => '自动拾取:' + (core.getFlag('autoGet', false) ? '开' : '关'),
			effect: () => {},
			text: '每走一步，自动拾取当前层可获得的道具。',
			replay: true,
		},
		autoBattle: {
			getName: () => '自动清怪:' + (core.getFlag('autoBattle', false) ? '开' : '关'),
			effect: () => {},
			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: function () {
				const bc =
					core.plugin.audioSystem &&
					core.plugin.audioSystem.bgmController;
				if (bc) {
					const saved = bc.transitionTime;
					bc.setTransitionTime(0);
					try {
						core.actions._clickSwitchs_sounds_bgm();
					} finally {
						setTimeout(function () {
							bc.setTransitionTime(saved);
						}, 0);
					}
				} else {
					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],
			['2,5', 'setHotKey', new SettingButton(220, 310, 150, 25, '8'), 8],
			['1,6', 'setHotKey', new SettingButton(40, 340, 150, 25, '9'), 9],
			['2,6', 'clearHotKeys', new SettingButton(300, 350, 42, 25)],
		]);
		return keyMenu;
	}
	// #endregion
	// #region 自定义工具栏界面
	/** 自定义工具栏界面 */
	class ToolBtn extends ButtonBase {
		constructor(x, y, w, h, icon, text, config) {
			super(x, y, w, h);
			/** @type {ToolBarConfigPage} */ // @ts-ignore
			this.menu;
			this.icon = icon; // 特殊icon:delete 用于删除图标
			/** @todo 这里需要重构 */
			this.text = text;
			this.config = config || {};
			this.ondown = () => {
				this.menu.selectedTool = this.icon;
				this.menu.text = this.text;
				this.menu.drawContent();
			};
		}

		draw() {
			const ctx = this.ctx;
			const { strokeStyle = 'white', fillStyle = 'white', selectedStyle = 'gold' } = this.config;
			core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, (this.menu.selectedTool === this.icon) ? selectedStyle : strokeStyle);
			if (this.icon === 'delete') {
				core.drawLine(ctx, this.x + 2, this.y + 2, this.x + this.w - 2, this.y + this.h - 2, 'red', 2);
				core.drawLine(ctx, this.x + 2, this.y + this.h - 2, this.x + this.w - 2, this.y + 2, 'red', 2);
			} else core.drawIcon(ctx, this.icon, this.x, this.y, this.w, this.h);
		}
	}

	class ToolBarBtn extends ButtonBase {
		constructor(x, y, size, type, length, text) {
			super(x, y, length * size, size);
			/** @type {ToolBarConfigPage} */ // @ts-ignore
			this.menu;
			this.type = type;
			this.length = length;
			/** @todo 这里需要重构 */
			this.text = text;
			this.ondown = (x, y, px, py) => {
				const squareSize = this.h;
				const index = Math.floor((px - this.x) / squareSize);
				this.menu.type = type;
				this.menu.index = index;
				this.menu.drawContent();
			}
		}

		draw() {
			const ctx = this.ctx;
			const squareSize = this.h;
			const type = this.type;
			const toolBarConfig = core.plugin.getToolBarConfig(type);
			for (let i = 0; i < this.length; i++) {
				const style = (this.menu.type === type && this.menu.index === i) ? 'gold' : 'white';
				core.strokeRoundRect(ctx, this.x + squareSize * i + i, this.y, squareSize, squareSize, 3, style);
				core.drawIcon(ctx, toolBarConfig[i], this.x + squareSize * i + i, this.y, squareSize, squareSize);
			}
		}
	}

	class ToolBarConfigPage extends MenuBase {
		constructor() {
			super('toolBarConfig', ['ondown']);
			/** 当前选中的图标 */
			this.selectedTool = 'none';
			/** 当前选中了哪个类型的工具栏 */
			this.type = 'none';
			/** 当前选中工具栏哪个位置(1-9) */
			this.index = -1;
			this.text = '';
		}

		drawContent() {
			const ctx = this.createCanvas();
			core.setTextAlign(ctx, 'left');
			core.fillText(ctx, '常规', 40, 175, ' #FFE4B5', '16px Verdana');
			core.fillText(ctx, '数字', 40, 205, ' #FFE4B5', '16px Verdana');
			core.fillText(ctx, '录像', 40, 235, ' #FFE4B5', '16px Verdana');
			core.fillText(ctx, '可选按钮', 40, 265, ' #FFE4B5', '16px Verdana');
			if (this.text && this.text.length > 0) {
				core.ui.drawTextContent(ctx, this.text, {
					left: 30,
					top: 78,
					bold: false,
					color: "white",
					align: "left",
					fontSize: 14,
					maxWidth: 350
				});
			}
			super.drawContent();
		}

		setToolBarConfig() {
			core.setToolBarConfig(this.type, this.index, this.selectedTool);
			this.drawContent();
		}
	}

	function toolBarConfigFactory() {
		// 名字不能叫toolBar 画布toolBar被系统占了
		const toolBarMenu = new ToolBarConfigPage();
		const changeToolBarBtn = new RoundBtn(320, 158, 42, 24, '执行', -1);
		changeToolBarBtn.ondown = function () {
			core.setToolBarConfig(this.menu.type, this.menu.index, this.menu.selectedTool);
			this.menu.drawContent();
		}.bind(changeToolBarBtn);
		toolBarMenu.registerBtns([
			['1,1', new ToolBarBtn(80, 158, 24, 'normal', 9, '常规模式下显示在工具栏中的图标。')],
			['1,2', changeToolBarBtn, () => toolBarMenu.setToolBarConfig()],
			['2,1', new ToolBarBtn(80, 188, 24, 'num', 9, '数字模式下显示在工具栏中的图标。')],
			['3,1', new ToolBarBtn(80, 218, 24, 'replay', 9, '录像模式下显示在工具栏中的图标。')],
			['5,1', new ToolBtn(40, 275, 24, 24, 'book', '打开怪物手册')],
			['5,2', new ToolBtn(70, 275, 24, 24, 'fly', '进行楼层传送')],
			['5,3', new ToolBtn(100, 275, 24, 24, 'toolbox', '打开物品背包')],
			['5,4', new ToolBtn(130, 275, 24, 24, 'equipbox', '打开装备背包')],
			['5,5', new ToolBtn(160, 275, 24, 24, 'keyboard', '打开虚拟键盘')],
			['5,6', new ToolBtn(190, 275, 24, 24, 'shop', '打开快捷商店')],
			['5,7', new ToolBtn(220, 275, 24, 24, 'save', '存档')],
			['5,8', new ToolBtn(250, 275, 24, 24, 'load', '读档')],
			['5,9', new ToolBtn(280, 275, 24, 24, 'settings', '打开系统设置')],
			['5,10', new ToolBtn(310, 275, 24, 24, 'rollback', '读取自动存档')],
			['5,11', new ToolBtn(340, 275, 24, 24, 'undoRollback', '取消读取自动存档')],
			['6,1', new ToolBtn(40, 305, 24, 24, 'btn1', '数字键1')],
			['6,2', new ToolBtn(70, 305, 24, 24, 'btn2', '数字键2')],
			['6,3', new ToolBtn(100, 305, 24, 24, 'btn3', '数字键3')],
			['6,4', new ToolBtn(130, 305, 24, 24, 'btn4', '数字键4')],
			['6,5', new ToolBtn(160, 305, 24, 24, 'btn5', '数字键5')],
			['6,6', new ToolBtn(190, 305, 24, 24, 'btn6', '数字键6')],
			['6,7', new ToolBtn(220, 305, 24, 24, 'btn7', '数字键7')],
			['6,8', new ToolBtn(250, 305, 24, 24, 'btn8', '数字键8')],
			['6,9', new ToolBtn(280, 305, 24, 24, 'btn9', '数字键9')],
			['6,10', new ToolBtn(310, 305, 24, 24, 'btnAlt', '开关Alt模式(需要配合数字键使用)')],
			['7,1', new ToolBtn(40, 335, 24, 24, 'play', '播放/暂停录像')],
			['7,2', new ToolBtn(70, 335, 24, 24, 'stop', '停止播放录像')],
			['7,3', new ToolBtn(100, 335, 24, 24, 'rewind', '录像模式下回退')],
			['7,4', new ToolBtn(130, 335, 24, 24, 'speedDown', '减速录像(最低0.2倍)')],
			['7,5', new ToolBtn(160, 335, 24, 24, 'speedUp', '加速录像(最高24倍)')],
			['7,6', new ToolBtn(190, 335, 24, 24, 'single', '单步播放录像')],
			['7,7', new ToolBtn(220, 335, 24, 24, 'view', '浏览地图')],
			['7,8', new ToolBtn(250, 335, 24, 24, 'delete', '删除已有图标')],
		]);
		return toolBarMenu;
	}
	// #endregion
	// #region 控制台界面
	/** @type {{[x:string]:Setting}} */
	const consoleSetting = {
		debug_wallHacking: {
			getName: () => ' 穿墙:' + (core.hasFlag('debug_wallHacking') ? '开' : '关'),
			effect: () => {
				core.setFlag('debug', true);
				invertFlag('debug_wallHacking');
			},
			text: '开启时将始终穿墙并无视各种事件，无论是否按下Ctrl。',
			replay: false,
		},
		debug_statusName: {
			getName: () => core.getFlag('debug_statusName', '??'),
			/** @this {ConsoleMenu} */
			effect: function () {
				const dictionary = {
					'体力': 'hp',
					'血量': 'hp',
					'生命': 'hp',
					'血': 'hp',
					'体力上限': 'hpmax',
					'血量上限': 'hpmax',
					'生命上限': 'hpmax',
					'血限': 'hpmax',
					'攻击': 'atk',
					'攻': 'atk',
					'防御': 'def',
					'防': 'def',
					'魔防': 'mdef',
					'护盾': 'mdef',
					'mf': 'mdef',
					'金币': 'money',
					'金钱': 'money',
					'钱': 'money',
					'经验': 'exp',
					'魔力': 'mana',
					'魔': 'mana',
					'蓝': 'mana',
				};
				core.utils.myprompt('输入要修改的属性名称', '', (value) => {
					const heroStatus = core.status.hero;
					if (dictionary.hasOwnProperty(value)) {
						value = dictionary[value];
					}
					if (heroStatus && heroStatus.hasOwnProperty(value) && ['hp', 'hpmax', 'atk', 'def', 'mdef', 'money', 'exp', 'mana', 'manamax'].includes(value)) {
						core.setFlag('debug_statusName', value);
						this.menu.drawContent();
					} else {
						core.drawFailTip('错误：不合法的名称!');
					}
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_statusValue: {
			getName: () => {
				let value = core.getFlag('debug_statusValue', '??');
				if (typeof value === 'number') return core.formatBigNumber(value, 5);
				else return value;
			},
			/** @this {ConsoleMenu} */
			effect: function () {
				core.utils.myprompt('输入要修改到的值', null, (input) => {
					const value = parseInt(input);
					if (!Number.isNaN(value)) {
						core.setFlag('debug_statusValue', value);
						this.menu.drawContent();
					} else {
						core.drawFailTip('错误：不合法的值!');
					}
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_setStatus: {
			getName: () => '',
			/** @this {ConsoleMenu} */
			effect: function () {
				const name = core.getFlag('debug_statusName'),
					value = core.getFlag('debug_statusValue');
				if (!(name && core.status.hero && core.status.hero.hasOwnProperty(name))) {
					core.drawFailTip('错误：不合法的名称!');
					return;
				}
				if (!Number.isInteger(value)) {
					core.drawFailTip('错误：不合法的值!');
					return;
				}
				core.setFlag('debug', true);
				core.setStatus(name, value);
				core.updateStatusBar();
				core.drawSuccessTip('设置成功!');
			},
			text: '将角色状态设为相应值。',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
				core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
				core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
			},
		},
		debug_itemName: {
			getName: () => core.getFlag('debug_itemName', '??'),
			/** @this {ConsoleMenu} */
			effect: function () {
				core.utils.myprompt('输入要修改的物品名称', null, (value) => {
					const itemInfo = core.material.items;
					if (itemInfo) {
						const aimItem = Object.values(itemInfo).find((item) => item.name === value || item.id === value);
						if (aimItem) {
							core.setFlag('debug_itemName', aimItem.id);
							this.menu.drawContent();
							return;
						}
					}
					core.drawFailTip('错误：不合法的名称!');
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_itemValue: {
			getName: () => core.getFlag('debug_itemValue', '??'),
			/** @this {ConsoleMenu} */
			effect: function () {
				core.setFlag('debug', true);
				core.utils.myprompt('输入要修改到的值', null, (input) => {
					const value = parseInt(input);
					if (!Number.isNaN(value)) {
						core.setFlag('debug_itemValue', value);
						this.menu.drawContent();
					} else {
						core.drawFailTip('错误：不合法的值!');
					}
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_setItem: {
			getName: () => '',
			/** @this {ConsoleMenu} */
			effect: function () {
				const name = core.getFlag('debug_itemName'),
					value = core.getFlag('debug_itemValue');
				const itemInfo = core.material.items;

				if (name && itemInfo) {
					let itemExist = Object.values(itemInfo).some((item) => item.id === name);
					if (!itemExist) {
						core.drawFailTip('错误：不合法的名称!');
						return;
					}
				} else {
					core.drawFailTip('错误：不合法的名称!');
					return;
				}

				if (!Number.isInteger(value)) {
					core.drawFailTip('错误：不合法的值!');
					return;
				}

				core.setFlag('debug', true);
				core.setItem(name, value);
				core.updateStatusBar();
				core.drawSuccessTip('设置成功!');
			},
			text: '将道具数设为相应值。',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
				core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
				core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
			},
		},
		debug_flagName: {
			getName: () => core.getFlag('debug_flagName', '??'),
			/** @this {ConsoleMenu} */
			effect: function () {
				core.setFlag('debug', true);
				core.utils.myprompt('输入要修改的变量名。注意：如果您不了解修改变量的后果，请勿尝试。', null, (value) => {
					if (!value.startsWith('debug')) {
						core.setFlag('debug_flagName', value);
						this.menu.drawContent();
					} else {
						core.drawFailTip('错误：不合法的名称!');
					}
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_flagValue: {
			getName: () => core.getFlag('debug_flagValue', '??'),
			/** @this {ConsoleMenu} */
			effect: function () {
				core.setFlag('debug', true);
				core.utils.myprompt('输入要修改到的值。注意：如果您不了解修改变量的后果，请勿尝试。', null, (value) => {
					let newValue;
					try {
						newValue = JSON.parse(value.trim());
					} catch {
						core.drawFailTip('错误：不合法的值，无法解析!');
						return;
					}
					core.setFlag('debug_flagValue', newValue);
					this.menu.drawContent();
				}, () => {});
			},
			text: '',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.strokeRect(ctx, this.x, this.y, this.w, this.h, ' #708090');
			},
		},
		debug_setFlag: {
			getName: () => '',
			/** @this {ConsoleMenu} */
			effect: function () {
				const name = core.getFlag('debug_flagName'),
					value = core.getFlag('debug_flagValue');
				if (!name) {
					core.drawFailTip('错误：不合法的变量名称!');
					return;
				}
				core.setFlag('debug', true);
				core.setFlag(name, value);
				core.updateStatusBar();
				core.drawSuccessTip('设置成功!');
			},
			text: '将变量设为相应值。',
			replay: false,
			/** @this {SettingButton} */
			draw: function (ctx) {
				core.fillRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #D3D3D3');
				core.strokeRoundRect(ctx, this.x, this.y, this.w, this.h, 3, ' #888888');
				core.fillText(ctx, '执行', this.x + 5, this.y + this.h / 2 + 5, ' #333333', '16px Verdana');
			},
		},
	};

	class ConsoleMenu extends SettingOnePage {
		constructor() {
			super('console', consoleSetting);
			this.menu = this;
		}

		drawContent() {
			const ctx = this.createCanvas();
			const consoleWarnText =
				"本页面的功能仅供调试用。使用后相应存档将变红，录像不能通过，且无法提交。请读档到普通存档后正常游玩方可提交。";
			core.setTextAlign(ctx, 'left');
			core.setTextBaseline(ctx, 'alphabetic');
			core.fillText(ctx, "本页面的功能仅供调试用。使用后相应存档将变红，录像", 30, 170, " #FFC0CB", '14px Verdana');
			core.fillText(ctx, "不能通过，且无法提交。请读档到普通存档后正常游玩方", 30, 190, " #FFC0CB", '14px Verdana');
			core.fillText(ctx, "可提交。", 30, 210, " #FFC0CB", '14px Verdana');
			core.fillText(ctx, "属性", 45, 264, 'white', '16px Verdana');
			core.fillText(ctx, "设为", 170, 264, 'white', '16px Verdana');
			core.fillText(ctx, "物品", 45, 290, 'white', '16px Verdana');
			core.fillText(ctx, "数量设为", 170, 290, 'white', '16px Verdana');
			core.fillText(ctx, "变量", 45, 316, 'white', '16px Verdana');
			core.fillText(ctx, "设为", 170, 316, 'white', '16px Verdana');
			super.drawContent(ctx);
		}
	}

	function consoleMenuFactory() {
		const consoleMenu = new ConsoleMenu();
		consoleMenu.registerSettingBtns([
			['1,1', 'debug_wallHacking', new SettingButton(40, 220, 150, 25)],
			['1,2', 'debug_statusName', new SettingButton(80, 250, 80, 20)],
			['2,2', 'debug_statusValue', new SettingButton(210, 250, 80, 20)],
			['3,2', 'debug_setStatus', new SettingButton(340, 250, 40, 20)],
			['1,3', 'debug_itemName', new SettingButton(80, 276, 80, 20)],
			['2,3', 'debug_itemValue', new SettingButton(240, 276, 80, 20)],
			['3,3', 'debug_setItem', new SettingButton(340, 276, 40, 20)],
			['1,4', 'debug_flagName', new SettingButton(80, 302, 80, 20)],
			['2,4', 'debug_flagValue', new SettingButton(210, 302, 80, 20)],
			['3,4', 'debug_setFlag', new SettingButton(340, 302, 40, 20)],
		]);
		return consoleMenu;
	}
	// #endregion
	// #region 敌人信息显示调节界面
	const infoNameMap = {
		'damage': '伤害',
		'critical': '临界',
		'hp': core.control.getStatusLabel('hp'),
		'atk': core.control.getStatusLabel('atk'),
		'def': core.control.getStatusLabel('def'),
		'money': core.control.getStatusLabel('money'),
		'exp': core.control.getStatusLabel('exp'),
		'criticalDamage': '临界减伤',
		'defDamage': '1防减伤',
		'special': '特殊属性',
		'notBomb': '不可炸',
		'vampireDiff': '吸血额外受伤'
	}

	class DisplayInfoBtn extends RoundBtn {
		constructor(x, y, w, h, pos, index) {
			super(x, y, w, h);
			this.pos = pos;
			this.index = index;
		}

		draw() {
			super.draw();
			const isEmpty = !this.infoName;
			const ctx = this.ctx;
			const { x, y, w, h } = this;
			const [x0, y0, r, offset] = [x + w + 15, y + h / 2, h / 2 - 2, 2];
			core.fillCircle(ctx, x0, y0, r, isEmpty ? 'lime' : 'red');
			core.drawLine(ctx, x0 - r + offset, y0, x0 + r - offset, y0, 'white', 2);
			if (isEmpty) core.drawLine(ctx, x0, y0 - r + offset, x0, y0 + r - offset, 'white', 2);
		}

		inRange(px, py) { // 实际的点击判定区是右边的圆形
			const { x, y, w, h } = this;
			return px >= x + w + 15 - h / 2 && px <= x + w + 15 + h / 2 && py >= y && py <= y + h;
		}
	}

	class SpecialIconBtn extends RoundBtn {}

	class AdvanceDisplayMenu extends MenuBase {
		constructor() {
			super('advanceDisplay', ['ondown']);
			this.selectedBtn = null;
			this.getData();
			this.getAllSpecials();
			this.getSpecialIconData();
			this.specialIconPage = 0;
		}

		drawContent() {
			const ctx = this.createCanvas();
			core.setTextAlign(ctx, 'center');
			core.setTextBaseline(ctx, 'alphabetic');
			core.fillRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 320, 5, "rgba(50,50,50,1)");
			core.fillText(ctx, "设定敌人左下角显示的数据", 110, 90, 'white', '14px Verdana');
			core.fillText(ctx, "设定敌人右上角显示的数据", 110, 190, 'white', '14px Verdana');
			core.fillText(ctx, "设定特殊属性的代表字符", 110, 290, 'white', '14px Verdana');
			this.getData();
			this.setInfoName();
			this.getSpecialIconData();
			super.drawContent(ctx);
		}

		clear() {
			super.clear();
			const settingMenu = core.plugin.settingMenu;
			if (settingMenu) {
				settingMenu.beginListen();
				settingMenu.pageList[settingMenu.currPage].beginListen();
			}
		}

		getData() {
			const defaultData = { leftdown: { 1: 'damage', 2: 'critical' }, rightup: {} };
			this.data = core.getLocalStorage('displayData', defaultData);
		}

		setData(pos, index, infoName) {
			this.data[pos][index] = infoName;
			core.setLocalStorage('displayData', this.data);
			this.setInfoName();
		}

		setInfoName() {
			this.btnMap.forEach(btn => {
				if (!(btn instanceof DisplayInfoBtn)) return;
				const pos = btn.pos;
				const index = btn.index;
				const infoName = this.data[pos][index];
				if (infoName) {
					btn.infoName = infoName;
					const name = infoNameMap[infoName];
					btn.text = name;
					if (name.length >= 5) {
						if (!btn.config) btn.config = {};
						btn.config.font = "12px Verdana";
					}
				} else {
					btn.infoName = null;
					btn.text = '';
				}
			});
		}

		getAllSpecials() { // allSpecials是所有用到了的特殊属性的数组
			let allSpecialsSet = new Set();
			Object.values(core.material.enemys).forEach(enemy => {
				const special = core.utils.parseSpecial(enemy.special);
				allSpecialsSet = new Set([...allSpecialsSet, ...special]);
			})
			this.allSpecials = [...allSpecialsSet].sort((a, b) => a - b);
		}

		getSpecialIconData() {
			const specialData = core.getLocalStorage('specialIconData', {});
			this.specialIconData = specialData;
		}

		getCurrSpecialIconList() {
			return this.allSpecials.slice(this.specialIconPage * 6,
				this.specialIconPage * 6 + 6);
		}

		/** 更新每个按钮对应的文本，在翻页和修改内容时需要手动调用 */
		setSpecialIconBtnText() {
			const specialIconList = this.getCurrSpecialIconList();
			for (let i = 0; i <= 5; i++) {
				const key = 's' + (i + 1);
				const btn = this.btnMap.get(key);
				const index = this.specialIconPage * 6 + i;
				if (index >= this.allSpecials.length) {
					btn.text = '';
				} else {
					const specialNum = this.allSpecials[index];
					const specialIndexMap = core.enemys.getSpecialIndexMap();
					const [key, name] = specialIndexMap[specialNum];
					const icon = this.specialIconData[specialNum] || "无";
					btn.text = `${key}:${name} ${icon}`;
				}
			}
		}

		setSpecialIconData(index) {
			const specialIconList = this.getCurrSpecialIconList();
			const specialNum = specialIconList[index];
			if (!specialNum) return;
			core.utils.myprompt('输入该特殊属性的代表字符', null, (value) => {
				if (value.length > 1) value = value[0]; // 最多保留一位字符
				this.specialIconData[specialNum] = value;
				core.setLocalStorage('specialIconData', this.specialIconData);
				this.setSpecialIconBtnText();
				this.drawContent();
			});
		}

		pageUp() {
			if (this.specialIconPage * 6 + 6 < this.allSpecials.length) {
				this.specialIconPage++;
				this.setSpecialIconBtnText();
				this.drawContent();
			}
		}

		pageDown() {
			if (this.specialIconPage > 0) {
				this.specialIconPage--;
				this.setSpecialIconBtnText();
				this.drawContent();
			}
		}
	}

	function advanceDisplayFactory() {
		const advanceDisplayMenu = new AdvanceDisplayMenu();
		const exitBtn = new ExitBtn(370, 80, 16, 16, { radius: 1, lineOffsetX: 2, lineWidthX: 2 })
		advanceDisplayMenu.registerBtn('exitBtn', exitBtn, () => {
			advanceDisplayMenu.clear();
			const settingMenu = core.plugin.settingMenu;
			settingMenu.btnMap.forEach(btn => { btn.disable = false });
			settingMenu.drawContent();
		});
		const btn1 = new DisplayInfoBtn(50, 100, 75, 20, 'leftdown', 1),
			btn2 = new DisplayInfoBtn(50, 125, 75, 20, 'leftdown', 2),
			btn3 = new DisplayInfoBtn(50, 150, 75, 20, 'leftdown', 3),
			btn4 = new DisplayInfoBtn(50, 200, 75, 20, 'rightup', 1),
			btn5 = new DisplayInfoBtn(50, 225, 75, 20, 'rightup', 2),
			btn6 = new DisplayInfoBtn(50, 250, 75, 20, 'rightup', 3);
		/** 一行显示几个选项 */
		const ROW_INFONAME = 6;
		const infoNameList = Object.keys(infoNameMap);
		const l = infoNameList.length;

		const setNewInfo = function (infoName) {
			return function () {
				const btn = advanceDisplayMenu.selectedBtn;
				if (!btn) return;
				advanceDisplayMenu.setData(btn.pos, btn.index, infoName);
				advanceDisplayMenu.btnMap.forEach((btn, key) => {
					if (btn.key.startsWith("temp")) btn.disable = true;
				});
				advanceDisplayMenu.selectedBtn = null;
				advanceDisplayMenu.drawContent();
				core.control.updateStatusBar(); // 手动刷新一下地图显伤
			}
		}
		for (let i = 0; i < l; i++) {
			const name = infoNameMap[infoNameList[i]];
			const config = {
				fillStyle: 'Azure',
				font: name.length >= 5 ? "12px Verdana" : "14px Verdana",
			}
			const x = (i >= ROW_INFONAME) ? 300 : 200;
			const y = 100 + 25 * ((i >= ROW_INFONAME) ? (i - ROW_INFONAME) : i);
			const tempBtn = new RoundBtn(x, y, 80, 20, name, config);
			tempBtn.disable = true;
			advanceDisplayMenu.registerBtn('temp' + i, tempBtn, setNewInfo(infoNameList[i]));
		}

		const change = function (btn) {
			return function () {
				if (btn.infoName) {
					advanceDisplayMenu.setData(btn.pos, btn.index, null);
				} else {
					if (advanceDisplayMenu.selectedBtn && advanceDisplayMenu.selectedBtn === btn) {
						// 点击左边刚点过的按钮会收起展开菜单
						advanceDisplayMenu.btnMap.forEach((btn, key) => {
							if (btn.key.startsWith("temp")) btn.disable = true;
						});
						advanceDisplayMenu.selectedBtn = null;
						advanceDisplayMenu.drawContent();
					} else {
						advanceDisplayMenu.selectedBtn = btn;
						advanceDisplayMenu.btnMap.forEach((btn, key) => {
							if (btn.key.startsWith("temp")) btn.disable = false;
						});
					}
				}
				advanceDisplayMenu.drawContent();
			}
		}
		advanceDisplayMenu.registerBtns([
			['1', btn1, change(btn1)],
			['2', btn2, change(btn2)],
			['3', btn3, change(btn3)],
			['4', btn4, change(btn4)],
			['5', btn5, change(btn5)],
			['6', btn6, change(btn6)],
		]);
		const config = { "font": "12px Verdana" };
		advanceDisplayMenu.registerBtn('s1', new SpecialIconBtn(50, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 0));
		advanceDisplayMenu.registerBtn('s2', new SpecialIconBtn(150, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 1));
		advanceDisplayMenu.registerBtn('s3', new SpecialIconBtn(250, 300, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 2));
		advanceDisplayMenu.registerBtn('s4', new SpecialIconBtn(50, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 3));
		advanceDisplayMenu.registerBtn('s5', new SpecialIconBtn(150, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 4));
		advanceDisplayMenu.registerBtn('s6', new SpecialIconBtn(250, 330, 80, 20, "", config), advanceDisplayMenu.setSpecialIconData.bind(advanceDisplayMenu, 5));
		advanceDisplayMenu.registerBtn('pageDown', new ArrowBtn(50, 360, 16, 16, 'left'),
			advanceDisplayMenu.pageDown.bind(advanceDisplayMenu));
		advanceDisplayMenu.registerBtn('pageUp', new ArrowBtn(320, 360, 16, 16, 'right'),
			advanceDisplayMenu.pageUp.bind(advanceDisplayMenu));
		advanceDisplayMenu.setSpecialIconBtnText();
		return advanceDisplayMenu;
	}
	// #endregion

	class PageChangeBtn extends RoundBtn {
		constructor(x, y, w, h, text) {
			super(x, y, w, h, text);
			/** @type {SettingBack} */ // @ts-ignore
			this.menu;
			this.ondown = () => this.menu.changePage(this.key);
		}
	}

	class SettingBack extends Pagination {
		constructor(pageList, currPage, name) {
			super(pageList, currPage, name, ['ondown', 'keyDown']);
		}

		changePage(index) {
			this.btnMap.forEach((btn, key) => {
				btn.status = (key === index) ? 'selected' : 'none';
			});
			super.changePage(index);
			this.drawContent();
		}

		/** @override */
		keyDownEvent(keyCode) {
			if (keyCode === KeyCodeEnum.PageDown) this.pageDown();
			else if (keyCode === KeyCodeEnum.PageUp) this.pageUp();
			else if (keyCode === KeyCodeEnum.Esc) this.quit();
		}

		quit() {
			this.clear();
			setTimeout(core.unlockControl, 0); // 消抖，防止点击关闭按钮的一瞬间触发瞬移。
		}

		drawSettingBackGround(ctx) {
			core.strokeRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "white", 2);
			core.fillRoundRect(ctx, 0, 0, core.__PIXELS__, core.__PIXELS__, 5, "gray");

			// 绘制设置说明的文本框
			core.strokeRoundRect(ctx, 20, 70, core.__PIXELS__ - 40, 70, 3, "white");
			core.fillRoundRect(ctx, 21, 71, core.__PIXELS__ - 42, 68, 3, " #555555");

			// 绘制设置的框体
			core.strokeRoundRect(ctx, 20, 150, core.__PIXELS__ - 40, 256, 3, "white");
			core.fillRoundRect(ctx, 21, 151, core.__PIXELS__ - 42, 254, 3, " #999999");

			core.setTextAlign(ctx, 'center');
			core.ui.fillText(ctx, "设置", core.__PIXELS__ / 2, 25, 'white', '20px Verdana');
		}

		drawContent() {
			const ctx = core.createCanvas(this.name, this.x, this.y, this.w, this.h, 136);
			this.drawSettingBackGround(ctx);
			super.drawContent();
			// this.initOnePage(this.currPage);
		}
	}

	const allSettings = {
		...gamePlaySetting,
		...gameViewSetting,
		...keySetting,
		...consoleSetting,
	};
	// 注册点击SettingButton的行为cSet， 只有replay为真时计入录像
	core.registerReplayAction('cSet', (action) => {
		const strArr = action.split(':');
		if (strArr[0] !== 'cSet') return false;
		const currSetting = allSettings[strArr[1]]; //@todo bugfix
		if (!currSetting || !currSetting.replay || strArr[1].startsWith('debug')) return false;

		let params = strArr.slice(2);

		if (params.length > 0) {
			currSetting.effect.apply(currSetting, params);
		} else {
			currSetting.effect.call(currSetting);
		}

		core.status.route.push(action);
		core.replay();
		return true;
	});

	this.openSetting = function () {
		if (core.isReplaying()) return;
		core.lockControl();
		const ctx = 'setting';

		const gamePlayMenu = gamePlayFactory();
		const gameViewMenu = gameViewFactory();
		const keyMenu = keyMenuFactory();
		const toolBarMenu = toolBarConfigFactory();
		const consoleMenu = consoleMenuFactory();

		// 在此处添加新的菜单页面
		const settingMenu = new SettingBack([gamePlayMenu, gameViewMenu, keyMenu, toolBarMenu, consoleMenu], 0, ctx);

		// 主页面的按钮列表
		const gamePlayBtn = new PageChangeBtn(32, 40, 46, 24, '功能'),
			gameViewBtn = new PageChangeBtn(92, 40, 46, 24, '音画'),
			keyBtn = new PageChangeBtn(152, 40, 46, 24, '按键'),
			toolBarBtn = new PageChangeBtn(212, 40, 66, 24, '工具栏'),
			consoleBtn = new PageChangeBtn(292, 40, 66, 24, '控制台');
		const quit = new TextButton(360, 10, 45, 25, '[退出]');

		settingMenu.registerBtns([
			[0, gamePlayBtn],
			[1, gameViewBtn],
			[2, keyBtn],
			[3, toolBarBtn],
			[4, consoleBtn],
			['quit', quit, () => settingMenu.quit()]
		]);

		// 放缩时重绘整个大menu
		core.plugin.settingMenu = settingMenu;
		// 设置初始时选中的按键为第一个按键
		gamePlayBtn.status = 'selected';

		settingMenu.init();
		settingMenu.changePage(0);
	}
	// @todo 新版存档界面
},
    "statistics": function () {
		// ==================== 0. 底层 Sprite 核心 (视觉表现层) ====================
		const sprites = {};
		class Sprite {
			constructor(x, y, w, h, z, reference, name) {
				this.x = x;
				this.y = y;
				this.width = w;
				this.height = h;
				this.zIndex = z;
				this.reference = reference || 'game';
				this.name = name || `_sprite_${Sprite.count++}`;
				this.init();
			}
			init() {
				if (this.reference === 'window') {
					const canvas = document.createElement('canvas');
					this.canvas = canvas;
					this.context = canvas.getContext('2d');
					canvas.width = this.width;
					canvas.height = this.height;
					canvas.style.cssText = `position:absolute;top:${this.y}px;left:${this.x}px;z-index:${this.zIndex};pointer-events:none;`;
					document.body.appendChild(canvas);
				} else {
					this.context = core.createCanvas(this.name, this.x, this.y, this.width, this.height, this.zIndex);
					this.canvas = this.context.canvas;
					this.canvas.style.pointerEvents = 'none';
				}
				sprites[this.name] = this;
			}
			move(x, y, isDelta) {
				if (isDelta) {
					this.x += (x || 0);
					this.y += (y || 0);
				} else { if (x !== null) this.x = x; if (y !== null) this.y = y; }
				if (this.reference === 'window') {
					this.canvas.style.left = this.x + 'px';
					this.canvas.style.top = this.y + 'px';
				} else core.relocateCanvas(this.context, this.x, this.y);
				return this;
			}
			destroy() {
				if (this.reference === 'window') { if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); } else core.deleteCanvas(this.name);
				delete sprites[this.name];
			}
		}
		Sprite.count = 0;
		window.Sprite = Sprite;

		// ==================== 1. 辅助逻辑 ====================
		// 【修复5】skillAutosave加保护，避免route异常
		this.skillAutosave = function () {
			if (core.isReplaying()) return;
			if (core.getLocalStorage("autoSaveBeforeUseItem")) {
				let lastRoute = null;
				try {
					if (core.status.route && core.status.route.length > 0) {
						lastRoute = core.status.route.pop();
					}
					core.control.autosave();
				} catch (e) {
					console.warn("skillAutosave error:", e);
				}
				if (lastRoute !== null && core.status.route) {
					core.status.route.push(lastRoute);
				}
			}
		}
		this.addSkillCount = function (skill) {
			const floorId = core.status.floorId;
			const skillCount = core.getFlag("skillCount", {});
			if (!skillCount[floorId]) skillCount[floorId] = {};
			skillCount[floorId][skill] = (skillCount[floorId][skill] || 0) + 1;
			core.setFlag("skillCount", skillCount);
		}
		this.getSkillRemainCount = function (skill) {
			const skillCount = core.getFlag("skillCount", {});
			const floorId = core.status.floorId;
			return 2 - ((skillCount[floorId] && skillCount[floorId][skill]) || 0);
		}

		// 颜色主题解析器（公用）
		const getColorTheme = (id, clsName) => {
			if (!id) return { rgb: '255, 255, 255', glow: '#FFFFFF' };
			id = id.toLowerCase();
			if (id.includes('yellow')) return { rgb: '255, 230, 100', glow: '#FFE664' };
			if (id.includes('blue')) return { rgb: '120, 200, 255', glow: '#78C8FF' };
			if (id.includes('red')) return { rgb: '255, 130, 130', glow: '#FF8282' };
			if (id.includes('green')) return { rgb: '130, 240, 150', glow: '#82F096' };
			if (clsName && clsName.startsWith('enemy')) {
				const mon = core.material.enemys[id];
				if (mon && mon.notBomb) return { rgb: '255, 80, 80', glow: '#FF5050' };
				return { rgb: '200, 130, 255', glow: '#C882FF' };
			}
			if (id.includes('wall') || (clsName && clsName === 'terrains')) return { rgb: '170, 255, 255', glow: '#00FFFF' };
			return { rgb: '255, 150, 200', glow: '#FF96C8' };
		};

		// ==================== 2. 暴力推箭 (I344) ====================
		// 【修复6】数据变更在lockControl之前，动画纯视觉
		this.pushArrow = function () {
			if (this.getSkillRemainCount("I344") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];

			let target = null;
			for (let i = 1; i <= 12; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b) { target = b; break; }
			}
			if (!target) return core.drawFailTip("前方没东西");

			let steps = 0;
			const maxWidth = core.bigmap ? core.bigmap.width : 13;
			const maxHeight = core.bigmap ? core.bigmap.height : 13;
			while (true) {
				const nx = target.x + (steps + 1) * scan.x,
					ny = target.y + (steps + 1) * scan.y;
				if (nx < 0 || nx >= maxWidth || ny < 0 || ny >= maxHeight || core.getBlock(nx, ny)) break;
				steps++;
			}
			if (steps === 0) return core.drawFailTip("前方没有空间");

			const tid = target.event.id,
				cls = target.event.cls;
			const ox = target.x,
				oy = target.y;
			const tx = ox + scan.x * steps,
				ty = oy + scan.y * steps;
			const theme = getColorTheme(tid, cls);

			// ★ 先存档、计数、执行数据变更（录像/非录像一致）
			this.skillAutosave();
			this.addSkillCount("I344");
			core.removeBlock(ox, oy);
			core.setBlock(tid, tx, ty);
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效（不影响数据） ====================
			core.lockControl();

			const s = new Sprite(ox * 32, oy * 32, 32, 32, 140, 'game');
			core.drawIcon(s.context, tid, 0, 0);
			s.canvas.style.filter = "brightness(1.5)";
			s.canvas.style.boxShadow = `0 0 20px ${theme.glow}`;
			// 隐藏已setBlock的目标位置的真实图块（动画结束后刷新地图恢复）
			core.removeBlock(tx, ty);

			const overlay = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 90, 'game');
			overlay.canvas.style.backgroundColor = "rgba(0, 20, 50, 0.5)";
			overlay.canvas.style.transition = "opacity 0.2s";

			const createChargeAura = (callback) => {
				core.playSound('jump.mp3');
				const aura = new Sprite(hx * 32 - 32, hy * 32 - 32, 96, 96, 140, 'game');
				let f = 0;
				const t = setInterval(() => {
					f++;
					const ctx = aura.context;
					if (!ctx || f > 15) {
						clearInterval(t);
						aura.destroy(); if (callback) callback(); return;
					}
					core.clearMap(ctx);
					const radius = 48 - f * 2.5;
					if (radius > 0) {
						ctx.beginPath();
						ctx.arc(48, 48, radius, 0, Math.PI * 2);
						ctx.strokeStyle = `rgba(${theme.rgb}, ${f / 15})`;
						ctx.lineWidth = 3;
						ctx.stroke();
					}
					ctx.fillStyle = `rgba(${theme.rgb}, ${1 - f / 15})`;
					for (let i = 0; i < 4; i++) {
						const ang = Math.random() * Math.PI * 2;
						const r = radius + Math.random() * 15;
						ctx.fillRect(48 + Math.cos(ang) * r, 48 + Math.sin(ang) * r, 2.5, 2.5);
					}
				}, 16);
			};

			const createImpactResolution = (x, y) => {
				const burst = new Sprite(x * 32 - 48, y * 32 - 48, 128, 128, 140, 'game');
				let f = 0;
				const t = setInterval(() => {
					f++;
					const ctx = burst.context;
					if (!ctx || f > 20) {
						clearInterval(t);
						burst.destroy(); return;
					}
					core.clearMap(ctx);
					const op = Math.max(0, 1 - f / 20);
					if (f <= 8) {
						ctx.fillStyle = `rgba(${theme.rgb}, ${op * 0.8})`;
						ctx.beginPath();
						ctx.arc(64, 64, f * 5, 0, Math.PI * 2);
						ctx.fill();
					}
					ctx.fillStyle = `rgba(${theme.rgb}, ${op})`;
					for (let i = 0; i < 10; i++) {
						ctx.fillRect(64 + i * 2.5 - 12.5, 64 - f * (3 + Math.random() * 2) + f * f * 0.15, 3, 3);
					}
				}, 16);
			};

			createChargeAura(() => {
				core.playSound('attack.mp3');
				const trail = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
				let trailParticles = [];
				const totalFrames = steps * 2.5;
				const dx = (tx * 32 - ox * 32) / totalFrames;
				const dy = (ty * 32 - oy * 32) / totalFrames;
				let moveF = 0;

				const mTimer = setInterval(() => {
					moveF++;
					if (!s.canvas) { clearInterval(mTimer); return; }
					s.move(dx, dy, true);
					trailParticles.push({ x: s.x + 16, y: s.y + 16, life: 1.0, size: 10 + Math.random() * 6 });
					const tCtx = trail.context;
					core.clearMap(tCtx);
					for (let i = trailParticles.length - 1; i >= 0; i--) {
						const p = trailParticles[i];
						p.life -= 0.12;
						if (p.life <= 0) { trailParticles.splice(i, 1); continue; }
						tCtx.fillStyle = `rgba(${theme.rgb}, ${p.life})`;
						tCtx.beginPath();
						tCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
						tCtx.fill();
					}
					if (moveF >= totalFrames) {
						clearInterval(mTimer);
						s.destroy();
						core.playSound('bomb.mp3');
						createImpactResolution(tx, ty);
						const gameDom = document.getElementById('gameCanvas');
						let shake = 14;
						const st = setInterval(() => {
							if (gameDom) gameDom.style.transform = `translate(${Math.random() * shake - shake / 2}px, ${Math.random() * shake - shake / 2}px)`;
							shake *= 0.7;
							if (shake < 0.5) { clearInterval(st); if (gameDom) gameDom.style.transform = ''; }
						}, 30);
						overlay.canvas.style.opacity = 0;
						let fadeT = 1.0;
						const clearT = setInterval(() => {
							fadeT -= 0.1;
							if (trail.canvas) trail.canvas.style.opacity = fadeT;
							if (fadeT <= 0) {
								clearInterval(clearT);
								trail.destroy();
								overlay.destroy();
							}
						}, 30);
						// ★ 动画结束：恢复真实地图并解锁
						core.setBlock(tid, tx, ty);
						core.drawMap();
						core.unlockControl();
					}
				}, 16);
			});
		}

		// ==================== 3. 回力箭 (I345) ====================
		// 【修复6】数据变更在lockControl之前
		this.pullArrow = function () {
			if (this.getSkillRemainCount("I345") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];
			const revDir = { "up": "down", "down": "up", "left": "right", "right": "left" }[direction];

			let target = null;
			for (let i = 1; i <= 12; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b) { target = b; break; }
			}
			if (!target || (Math.abs(target.x - hx) <= 1 && Math.abs(target.y - hy) <= 1)) return core.drawFailTip("前方太近或没东西");

			const tx = target.x,
				ty = target.y;
			const tid = target.event.id,
				cls = target.event.cls;
			const endX = hx + scan.x,
				endY = hy + scan.y;
			const theme = getColorTheme(tid, cls);

			// ★ 先存档、计数、执行数据变更
			this.skillAutosave();
			this.addSkillCount("I345");
			core.removeBlock(tx, ty);
			core.setBlock(tid, endX, endY);
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效 ====================
			core.lockControl();
			core.playSound('jump.mp3');

			// 暂时隐藏目标位置的真实图块
			core.removeBlock(endX, endY);

			const overlay = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 90, 'game');
			overlay.canvas.style.backgroundColor = "rgba(0, 20, 50, 0.5)";
			overlay.canvas.style.transition = "opacity 0.2s";

			const windArrow = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 140, 'game');
			let waF = 0;
			const waTimer = setInterval(() => {
				waF++;
				const waCtx = windArrow.context;
				if (!waCtx || waF > 6) {
					clearInterval(waTimer);
					windArrow.destroy();
					startPullAnim(); return;
				}
				core.clearMap(waCtx);
				const progress = waF / 6;
				const startX = hx * 32 + 16,
					startY = hy * 32 + 16;
				const currentX = startX + (tx * 32 + 16 - startX) * progress;
				const currentY = startY + (ty * 32 + 16 - startY) * progress;
				waCtx.fillStyle = `rgba(${theme.rgb}, 0.9)`;
				waCtx.shadowBlur = 10;
				waCtx.shadowColor = theme.glow;
				waCtx.beginPath();
				waCtx.arc(currentX, currentY, 6, 0, Math.PI * 2);
				waCtx.fill();
				waCtx.strokeStyle = `rgba(${theme.rgb}, ${1 - progress})`;
				waCtx.lineWidth = 4;
				waCtx.lineCap = "round";
				waCtx.beginPath();
				waCtx.moveTo(startX + (currentX - startX) * Math.max(0, progress - 0.4), startY + (currentY - startY) * Math.max(0, progress - 0.4));
				waCtx.lineTo(currentX, currentY);
				waCtx.stroke();
			}, 16);

			const startPullAnim = () => {
				const s = new Sprite(tx * 32, ty * 32, 32, 32, 140, 'game');
				core.drawIcon(s.context, tid, 0, 0);
				s.canvas.style.boxShadow = `0 0 15px ${theme.glow}`;
				core.playSound('jump.mp3');

				const totalDist = Math.max(Math.abs(tx - endX), Math.abs(ty - endY)) * 32;
				let currentDist = 0,
					velocity = 2,
					acceleration = 1.8;

				const timer = setInterval(() => {
					velocity += acceleration;
					currentDist += velocity;
					const progress = Math.min(currentDist / totalDist, 1);
					if (!s.canvas) { clearInterval(timer); return; }
					const stretch = 1 + (velocity / 30),
						squeeze = 1 / stretch;
					s.canvas.style.transform = (revDir === 'up' || revDir === 'down') ? `scale(${squeeze}, ${stretch})` : `scale(${stretch}, ${squeeze})`;
					s.move(tx * 32 + (endX - tx) * 32 * progress, ty * 32 + (endY - ty) * 32 * progress);

					if (progress >= 1) {
						clearInterval(timer);
						if (overlay.canvas) overlay.canvas.style.opacity = 0;
						core.playSound('item.mp3');
						let bounce = 0;
						const bt = setInterval(() => {
							bounce++;
							if (!s.canvas) { clearInterval(bt); return; }
							const bS = 1 + Math.sin(bounce * 1.5) * 0.3 * (1 - bounce / 12);
							s.canvas.style.transform = `scale(${bS}, ${2 - bS})`;
							if (bounce >= 12) {
								clearInterval(bt);
								s.destroy();
								overlay.destroy();
								// ★ 恢复真实地图
								core.setBlock(tid, endX, endY);
								core.drawMap();
								core.unlockControl();
							}
						}, 20);
					}
				}, 16);
			};
		}

		// ==================== 4. 御箭术 (I346) ====================
		// 【修复4】数据逻辑在动画前完成，消除setTimeout嵌套中的数据操作风险
		this.goToDoorArrow = function () {
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];

			let door = null;
			for (let i = 1; i <= 12; i++) {
				const x = hx + i * scan.x,
					y = hy + i * scan.y;
				const b = core.getBlock(x, y);
				if (b && b.event.trigger === "openDoor") { door = b; break; }
			}
			if (this.getSkillRemainCount("I346") <= 0) return core.drawFailTip("本层次数已满");
			if (!door) return core.drawFailTip("前方没有门");

			const tx = door.x,
				ty = door.y,
				tid = door.event.id;
			const theme = getColorTheme(tid, '');

			// ★ 先存档、计数、执行数据变更（录像/非录像一致）
			this.skillAutosave();
			this.addSkillCount("I346");
			core.setHeroLoc('x', tx);
			core.setHeroLoc('y', ty);
			core.drawHero();
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效 ====================
			core.lockControl();

			const mask = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
			const mCtx = mask.context;
			mask.canvas.style.opacity = 0;
			mask.canvas.style.transition = "opacity 0.2s";

			const s = new Sprite(hx * 32, hy * 32, 32, 32, 140, 'game');
			const sCtx = s.context;
			const grd = sCtx.createRadialGradient(16, 16, 2, 16, 16, 16);
			grd.addColorStop(0, '#FFFFFF');
			grd.addColorStop(0.5, `rgba(${theme.rgb}, 0.8)`);
			grd.addColorStop(1, 'rgba(0,0,0,0)');
			sCtx.fillStyle = grd;
			sCtx.beginPath();
			sCtx.arc(16, 16, 16, 0, Math.PI * 2);
			sCtx.fill();
			s.canvas.style.boxShadow = `0 0 30px ${theme.glow}`;

			const dustCanvas = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 135, 'game');
			let dustParticles = [];

			if (core.canvas.hero) core.canvas.hero.canvas.style.opacity = 0;
			core.playSound('jump.mp3');

			s.canvas.style.transform = "scale(0.1)";
			s.canvas.style.transition = "transform 0.1s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
			setTimeout(() => { if (s.canvas) s.canvas.style.transform = "scale(1.2)"; }, 10);

			setTimeout(() => {
				mask.canvas.style.opacity = 1;
				if (s.canvas) s.canvas.style.transition = "";

				const gameDom = document.getElementById('gameCanvas');
				if (gameDom) {
					gameDom.style.transition = "transform 0.3s ease";
					gameDom.style.transform = "scale(1.02)";
				}

				const dist = Math.max(Math.abs(tx - hx), Math.abs(ty - hy)) * 32;
				let curDist = 0;

				const timer = setInterval(() => {
					curDist += 24;
					const progress = Math.min(curDist / dist, 1);
					const curX = hx * 32 + (tx - hx) * 32 * progress;
					const curY = hy * 32 + (ty - hy) * 32 * progress;

					if (s.canvas) {
						s.canvas.style.transform = (direction === 'up' || direction === 'down') ? `scale(0.4, 2.5)` : `scale(2.5, 0.4)`;
						s.move(curX, curY);
						core.clearMap(mCtx);
						mCtx.fillStyle = "rgba(0, 0, 0, 0.85)";
						mCtx.fillRect(0, 0, core.__PIXELS__, core.__PIXELS__);
						mCtx.globalCompositeOperation = 'destination-out';
						const lightGrd = mCtx.createRadialGradient(curX + 16, curY + 16, 0, curX + 16, curY + 16, 64);
						lightGrd.addColorStop(0, 'rgba(255, 255, 255, 1)');
						lightGrd.addColorStop(1, 'rgba(255, 255, 255, 0)');
						mCtx.fillStyle = lightGrd;
						mCtx.beginPath();
						mCtx.arc(curX + 16, curY + 16, 64, 0, Math.PI * 2);
						mCtx.fill();
						mCtx.globalCompositeOperation = 'source-over';
						if (progress < 1 && Math.random() > 0.3) {
							dustParticles.push({ x: curX + 16 + (Math.random() * 10 - 5), y: curY + 16 + (Math.random() * 10 - 5), life: 1.0, size: 1.5 + Math.random() * 1.5 });
						}
						const dCtx = dustCanvas.context;
						core.clearMap(dCtx);
						dCtx.fillStyle = `rgba(${theme.rgb}, 1)`;
						for (let i = dustParticles.length - 1; i >= 0; i--) {
							const p = dustParticles[i];
							p.life -= 0.1;
							if (p.life <= 0) { dustParticles.splice(i, 1); continue; }
							dCtx.globalAlpha = p.life;
							dCtx.beginPath();
							dCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
							dCtx.fill();
						}
						dCtx.globalAlpha = 1.0;
					}

					if (progress >= 1) {
						clearInterval(timer);
						core.playSound('door.mp3');
						if (gameDom) {
							gameDom.style.transform = "";
							setTimeout(() => { gameDom.style.transition = ""; }, 300);
						}

						s.destroy();
						if (core.canvas.hero) {
							core.canvas.hero.canvas.style.opacity = 0;
							core.canvas.hero.canvas.style.transform = "translateY(10px)";
							core.canvas.hero.canvas.style.transition = "opacity 0.2s, transform 0.2s ease-out";
							setTimeout(() => {
								if (core.canvas.hero) {
									core.canvas.hero.canvas.style.opacity = 1;
									core.canvas.hero.canvas.style.transform = "translateY(0)";
								}
							}, 20);
						}

						let fadeOp = 1;
						const ft = setInterval(() => {
							fadeOp -= 0.1;
							if (mask.canvas) mask.canvas.style.opacity = fadeOp;
							if (dustCanvas.canvas) dustCanvas.canvas.style.opacity = fadeOp;
							if (fadeOp <= 0) {
								clearInterval(ft);
								mask.destroy();
								dustCanvas.destroy();
								if (core.canvas.hero) {
									core.canvas.hero.canvas.style.transition = "";
									core.canvas.hero.canvas.style.transform = "";
								}
								core.unlockControl();
							}
						}, 30);
					}
				}, 16);
			}, 100);
		}

		// ==================== 5. 贯穿箭 (I347) ====================
		// 【修复1,2】数据逻辑全部在lockControl之前完成，insertAction也在解锁之后
		this.strikeArrow = function () {
			if (this.getSkillRemainCount("I347") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];
			const floorId = core.status.floorId;

			let enemy = null,
				eDist = 0;
			for (let i = 2; i <= 3; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b && b.event && b.event.cls && b.event.cls.startsWith("enemy")) {
					enemy = b;
					eDist = i; break;
				}
			}
			if (!enemy) return core.drawFailTip("2-3格范围内没有可以锁定的猎物");

			core.setFlag("strikeArrowOn", true);
			const predictDamage = core.getDamage(enemy.event.id, enemy.x, enemy.y);
			core.setFlag("strikeArrowOn", false);

			if (predictDamage === null || predictDamage >= core.status.hero.hp) {
				const mon = core.material.enemys[enemy.event.id];
				if (mon && mon.notBomb) return core.drawFailTip("面对BOSS你无法减伤，血量不足！");
				else return core.drawFailTip("即使减伤50%你也无法生存！");
			}

			let next = null;
			for (let j = eDist + 1; j <= 12; j++) {
				const b = core.getBlock(hx + j * scan.x, hy + j * scan.y);
				if (b && b.event) { next = b; break; }
			}

			const theme1 = getColorTheme(enemy.event.id, enemy.event.cls);

			// ★ 先存档、计数
			this.skillAutosave();
			this.addSkillCount("I347");

			// ★ 【修复1,2】数据逻辑：在锁定控制之前完成所有数据变更
			// 收集需要在战后执行的事件
			const pendingActions = [];

			const executeStrikeLogic = () => {
				core.setFlag("strikeArrowOn", true);
				core.setFlag('__forbidSave__', true);
				core.battle(enemy.event.id, enemy.x, enemy.y, true);
				core.setFlag("strikeArrowOn", false);
				core.setFlag('__forbidSave__', false);

				if (next && next.event && next.event.id) {
					const nid = next.event.id,
						nCls = next.event.cls || "";
					if (nCls.startsWith("enemy")) {
						const m2 = core.material.enemys[nid];
						if (m2 && m2.notBomb) {
							if (!core.isReplaying()) core.drawTip("第二目标不可炸，攻击被格挡！");
						} else {
							core.status.hero.money += (m2.money || 0);
							core.status.hero.experience += (m2.experience || 0);
							let actions = [];
							const locEvent2 = (core.floors[floorId].afterBattle || {})[next.x + "," + next.y];
							if (locEvent2) actions = actions.concat(locEvent2);
							if (next.event && next.event.afterBattle) actions = actions.concat(next.event.afterBattle);
							core.removeBlock(next.x, next.y);
							// ★ 【修复2】不在这里insertAction，先收集起来
							if (actions.length > 0) pendingActions.push(...actions);
						}
					} else if (nCls === "items") {
						if (core.getItemEffect) core.getItemEffect(nid, 1);
						else core.getItem(nid, 1, null, null);
						core.removeBlock(next.x, next.y);
					} else if (next.event.trigger === "openDoor") {
						core.removeBlock(next.x, next.y);
					} else if (!nCls.startsWith("npc") && (nCls !== "terrains" || nid.toLowerCase().includes("box") || nid.toLowerCase().includes("chest") || nid.toLowerCase().includes("arrow"))) {
						core.removeBlock(next.x, next.y);
					}
				}
				core.updateStatusBar();
			};

			// ★ 先执行数据逻辑（录像/非录像一致）
			// 但在非录像模式下，先用Sprite盖住怪物图案，避免视觉上怪物提前消失
			let enemyCover = null,
				nextCover = null;
			if (!core.isReplaying()) {
				enemyCover = new Sprite(enemy.x * 32, enemy.y * 32, 32, 32, 145, 'game');
				core.drawIcon(enemyCover.context, enemy.event.id, 0, 0);
				if (next && next.event && next.event.id) {
					nextCover = new Sprite(next.x * 32, next.y * 32, 32, 32, 145, 'game');
					core.drawIcon(nextCover.context, next.event.id, 0, 0);
				}
			}

			executeStrikeLogic();

			// ★ 【修复2】录像模式下直接插入待执行事件
			if (core.isReplaying()) {
				if (pendingActions.length > 0) core.insertAction(pendingActions);
				return;
			}

			// ==================== 纯视觉特效 ====================
			core.lockControl();

			const createDivineExplosion = function (x, y, colorRgb, colorHex) {
				const blast = new Sprite(x * 32 - 48, y * 32 - 48, 128, 128, 171, 'game');
				let bf = 0;
				const bt = setInterval(() => {
					bf++;
					const ctx = blast.context;
					if (!ctx || bf >= 20) { clearInterval(bt); return blast.destroy(); }
					core.clearMap(ctx);
					const op = 1 - bf / 20;
					ctx.fillStyle = `rgba(${colorRgb}, ${op * 0.5})`;
					ctx.shadowBlur = 15;
					ctx.shadowColor = colorHex;
					ctx.beginPath();
					ctx.arc(64, 64, bf * 4.5, 0, Math.PI * 2);
					ctx.fill();
					ctx.strokeStyle = `rgba(255, 255, 255, ${op})`;
					ctx.lineWidth = 2;
					for (let i = 0; i < 6; i++) {
						const ang = (i / 6) * Math.PI * 2 + (bf * 0.1);
						ctx.beginPath();
						ctx.moveTo(64 + Math.cos(ang) * bf * 2, 64 + Math.sin(ang) * bf * 2);
						ctx.lineTo(64 + Math.cos(ang) * (bf * 5 + Math.random() * 10), 64 + Math.sin(ang) * (bf * 5 + Math.random() * 10));
						ctx.stroke();
					}
				}, 16);
			};

			const createDivineColorfulParticles = function (x, y) {
				const pCanvas = new Sprite(x * 32 - 48, y * 32 - 48, 128, 128, 172, 'game');
				const ctx = pCanvas.context;
				const ps = [];
				for (let i = 0; i < 35; i++) {
					const ang = Math.random() * Math.PI * 2,
						spd = 1.8 + Math.random() * 2.5;
					ps.push({ x: 64, y: 64, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, r: 2.5 + Math.random() * 2.5, c: `hsl(${i * (360 / 35)}, 100%, 75%)` });
				}
				let f = 0,
					rot = 0;
				const pt = setInterval(() => {
					f++;
					if (!ctx || f >= 80) { clearInterval(pt); return pCanvas.destroy(); }
					core.clearMap(ctx);
					rot += 1.5;
					ctx.save();
					ctx.translate(64, 64);
					ctx.rotate(rot * Math.PI / 180);
					ctx.translate(-64, -64);
					ctx.globalAlpha = Math.max(0, 1 - f / 80);
					ps.forEach(p => {
						p.x += p.vx;
						p.y += p.vy;
						p.vx *= 0.96;
						p.vy *= 0.96;
						ctx.fillStyle = p.c;
						ctx.beginPath();
						ctx.arc(p.x, p.y, p.r * Math.max(0, (1 - f / 90)), 0, Math.PI * 2);
						ctx.fill();
					});
					ctx.restore();
				}, 16);
			};

			core.playSound('jump.mp3');
			const mask = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
			const mCtx = mask.context;
			mask.canvas.style.opacity = 0;
			mask.canvas.style.transition = "opacity 0.2s";

			let lockFrame = 0;
			const drawSpotlights = function () {
				lockFrame++;
				if (!mCtx) return;
				core.clearMap(mCtx);
				mCtx.fillStyle = "rgba(0, 0, 0, 0.85)";
				mCtx.fillRect(0, 0, core.__PIXELS__, core.__PIXELS__);
				mCtx.globalCompositeOperation = 'destination-out';
				const drawHalo = (x, y, r) => {
					const grd = mCtx.createRadialGradient(x * 32 + 16, y * 32 + 16, 0, x * 32 + 16, y * 32 + 16, r);
					grd.addColorStop(0, "rgba(255,255,255,1)");
					grd.addColorStop(1, "rgba(255,255,255,0)");
					mCtx.fillStyle = grd;
					mCtx.beginPath();
					mCtx.arc(x * 32 + 16, y * 32 + 16, r, 0, Math.PI * 2);
					mCtx.fill();
				};
				drawHalo(hx, hy, 75);
				drawHalo(enemy.x, enemy.y, 65);
				if (next) drawHalo(next.x, next.y, 65);
				mCtx.globalCompositeOperation = 'source-over';
				const drawLockOn = (x, y, color) => {
					const cx = x * 32 + 16,
						cy = y * 32 + 16;
					mCtx.save();
					mCtx.translate(cx, cy);
					mCtx.rotate(lockFrame * 0.15);
					mCtx.strokeStyle = color;
					mCtx.lineWidth = 1.5;
					mCtx.beginPath();
					mCtx.arc(0, 0, 20, 0, Math.PI * 2);
					mCtx.stroke();
					for (let i = 0; i < 4; i++) {
						mCtx.rotate(Math.PI / 2);
						mCtx.beginPath();
						mCtx.moveTo(0, 15);
						mCtx.lineTo(0, 25);
						mCtx.stroke();
					}
					mCtx.restore();
				};
				drawLockOn(enemy.x, enemy.y, 'rgba(255, 50, 50, 0.8)');
				if (next) drawLockOn(next.x, next.y, 'rgba(0, 255, 255, 0.8)');
			};

			const lockTimer = setInterval(drawSpotlights, 16);
			setTimeout(() => { if (mask.canvas) mask.canvas.style.opacity = 1; }, 10);

			setTimeout(() => {
				clearInterval(lockTimer);
				core.playSound('attack.mp3');

				const gameDom = document.getElementById('gameCanvas');
				if (gameDom) {
					gameDom.style.filter = "contrast(1.8) brightness(1.2)";
					gameDom.style.transform = `scale(1.03) translate(${scan.x * -8}px, ${scan.y * -8}px)`;
					setTimeout(() => {
						if (gameDom) {
							gameDom.style.transform = "";
							gameDom.style.filter = "";
						}
					}, 200);
				}

				const recoil = new Sprite(hx * 32 - 32, hy * 32 - 32, 96, 96, 135, 'game');
				let rf = 0;
				const rt = setInterval(() => {
					rf++;
					if (!recoil.context || rf >= 10) { clearInterval(rt); return recoil.destroy(); }
					core.clearMap(recoil.context);
					recoil.context.strokeStyle = `rgba(0, 255, 255, ${1 - rf / 10})`;
					recoil.context.lineWidth = 4;
					recoil.context.beginPath();
					recoil.context.ellipse(48, 48, rf * 5, rf * 3, 0, 0, Math.PI * 2);
					recoil.context.stroke();
				}, 16);

				const beam = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 160, 'game');
				let beamF = 0;
				const sx = hx * 32 + 16,
					sy = hy * 32 + 16;
				const ex = (hx + scan.x * 12) * 32 + 16,
					ey = (hy + scan.y * 12) * 32 + 16;
				const distance = Math.hypot(ex - sx, ey - sy);
				const angle = Math.atan2(ey - sy, ex - sx);
				const pAngle = angle + Math.PI / 2;

				// 在蓄力时就播放爆炸特效（数据已经变更完毕，这里只是视觉）
				// ★ 销毁遮挡Sprite，露出爆炸效果
				if (enemyCover) {
					enemyCover.destroy();
					enemyCover = null;
				}
				if (nextCover) {
					nextCover.destroy();
					nextCover = null;
				}
				createDivineExplosion(enemy.x, enemy.y, theme1.rgb, theme1.glow);
				if (next && next.event && next.event.cls) {
					const isBlocked = next.event.cls.startsWith("enemy") && core.material.enemys[next.event.id]?.notBomb;
					if (!isBlocked) createDivineColorfulParticles(next.x, next.y);
				}

				const beamTimer = setInterval(() => {
					beamF++;
					const bCtx = beam.context;
					if (!bCtx || beamF >= 20) {
						clearInterval(beamTimer);
						beam.destroy();
						if (mask.canvas) mask.canvas.style.opacity = 0;
						setTimeout(() => {
							mask.destroy();
							// ★ 【修复2】解锁后再插入待执行事件
							core.unlockControl();
							if (pendingActions.length > 0) core.insertAction(pendingActions);
						}, 200);
						return;
					}
					core.clearMap(bCtx);
					const op = Math.max(0, 1 - beamF / 20);
					bCtx.lineCap = 'round';
					bCtx.globalCompositeOperation = 'lighter';
					bCtx.strokeStyle = `rgba(255, 255, 255, ${op})`;
					bCtx.lineWidth = 3 + (op * 4);
					bCtx.beginPath();
					bCtx.moveTo(sx, sy);
					bCtx.lineTo(ex, ey);
					bCtx.stroke();
					const amplitude = 12 * op,
						frequency = 0.08,
						phaseShift = beamF * 1.5;
					bCtx.strokeStyle = `rgba(255, 50, 50, ${op * 0.9})`;
					bCtx.lineWidth = 2 + (op * 3);
					bCtx.beginPath();
					for (let d = 0; d <= distance; d += 4) {
						const offset = Math.sin(d * frequency - phaseShift) * amplitude;
						const px = sx + Math.cos(angle) * d + Math.cos(pAngle) * offset;
						const py = sy + Math.sin(angle) * d + Math.sin(pAngle) * offset;
						if (d === 0) bCtx.moveTo(px, py);
						else bCtx.lineTo(px, py);
					}
					bCtx.stroke();
					bCtx.strokeStyle = `rgba(50, 150, 255, ${op * 0.9})`;
					bCtx.lineWidth = 2 + (op * 3);
					bCtx.beginPath();
					for (let d = 0; d <= distance; d += 4) {
						const offset = Math.sin(d * frequency - phaseShift + Math.PI) * amplitude;
						const px = sx + Math.cos(angle) * d + Math.cos(pAngle) * offset;
						const py = sy + Math.sin(angle) * d + Math.sin(pAngle) * offset;
						if (d === 0) bCtx.moveTo(px, py);
						else bCtx.lineTo(px, py);
					}
					bCtx.stroke();
					bCtx.globalCompositeOperation = 'source-over';
				}, 16);
			}, 300);
		};

		this.getCountsLimit = function (skill) { return 2; };
	},
    "技能": function () {
		// ==================== 0. 底层 Sprite 核心 (视觉表现层) ====================
		const sprites = {};
		class Sprite {
			constructor(x, y, w, h, z, reference, name) {
				this.x = x;
				this.y = y;
				this.width = w;
				this.height = h;
				this.zIndex = z;
				this.reference = reference || 'game';
				this.name = name || `_sprite_${Sprite.count++}`;
				this.init();
			}
			init() {
				if (this.reference === 'window') {
					const canvas = document.createElement('canvas');
					this.canvas = canvas;
					this.context = canvas.getContext('2d');
					canvas.width = this.width;
					canvas.height = this.height;
					canvas.style.cssText = `position:absolute;top:${this.y}px;left:${this.x}px;z-index:${this.zIndex};pointer-events:none;`;
					document.body.appendChild(canvas);
				} else {
					this.context = core.createCanvas(this.name, this.x, this.y, this.width, this.height, this.zIndex);
					this.canvas = this.context.canvas;
					this.canvas.style.pointerEvents = 'none';
				}
				sprites[this.name] = this;
			}
			move(x, y, isDelta) {
				if (isDelta) {
					this.x += (x || 0);
					this.y += (y || 0);
				} else { if (x !== null) this.x = x; if (y !== null) this.y = y; }
				if (this.reference === 'window') {
					this.canvas.style.left = this.x + 'px';
					this.canvas.style.top = this.y + 'px';
				} else core.relocateCanvas(this.context, this.x, this.y);
				return this;
			}
			destroy() {
				if (this.reference === 'window') { if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); } else core.deleteCanvas(this.name);
				delete sprites[this.name];
			}
		}
		Sprite.count = 0;
		window.Sprite = Sprite;

		// ==================== 1. 辅助逻辑 ====================
		// 【修复5】skillAutosave加保护，避免route异常
		this.skillAutosave = function () {
			if (core.isReplaying()) return;
			if (core.getLocalStorage("autoSaveBeforeUseItem")) {
				let lastRoute = null;
				try {
					if (core.status.route && core.status.route.length > 0) {
						lastRoute = core.status.route.pop();
					}
					core.control.autosave();
				} catch (e) {
					console.warn("skillAutosave error:", e);
				}
				if (lastRoute !== null && core.status.route) {
					core.status.route.push(lastRoute);
				}
			}
		}
		this.addSkillCount = function (skill) {
			const floorId = core.status.floorId;
			const skillCount = core.getFlag("skillCount", {});
			if (!skillCount[floorId]) skillCount[floorId] = {};
			skillCount[floorId][skill] = (skillCount[floorId][skill] || 0) + 1;
			core.setFlag("skillCount", skillCount);
		}
		this.getSkillRemainCount = function (skill) {
			const skillCount = core.getFlag("skillCount", {});
			const floorId = core.status.floorId;
			return 2 - ((skillCount[floorId] && skillCount[floorId][skill]) || 0);
		}

		// 颜色主题解析器（公用）
		const getColorTheme = (id, clsName) => {
			if (!id) return { rgb: '255, 255, 255', glow: '#FFFFFF' };
			id = id.toLowerCase();
			if (id.includes('yellow')) return { rgb: '255, 230, 100', glow: '#FFE664' };
			if (id.includes('blue')) return { rgb: '120, 200, 255', glow: '#78C8FF' };
			if (id.includes('red')) return { rgb: '255, 130, 130', glow: '#FF8282' };
			if (id.includes('green')) return { rgb: '130, 240, 150', glow: '#82F096' };
			if (clsName && clsName.startsWith('enemy')) {
				const mon = core.material.enemys[id];
				if (mon && mon.notBomb) return { rgb: '255, 80, 80', glow: '#FF5050' };
				return { rgb: '200, 130, 255', glow: '#C882FF' };
			}
			if (id.includes('wall') || (clsName && clsName === 'terrains')) return { rgb: '170, 255, 255', glow: '#00FFFF' };
			return { rgb: '255, 150, 200', glow: '#FF96C8' };
		};

		// ==================== 2. 暴力推箭 (I344) ====================
		// 【修复6】数据变更在lockControl之前，动画纯视觉
		this.pushArrow = function () {
			if (this.getSkillRemainCount("I344") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];

			let target = null;
			for (let i = 1; i <= 12; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b) { target = b; break; }
			}
			if (!target) return core.drawFailTip("前方没东西");

			let steps = 0;
			const maxWidth = core.bigmap ? core.bigmap.width : 13;
			const maxHeight = core.bigmap ? core.bigmap.height : 13;
			while (true) {
				const nx = target.x + (steps + 1) * scan.x,
					ny = target.y + (steps + 1) * scan.y;
				if (nx < 0 || nx >= maxWidth || ny < 0 || ny >= maxHeight || core.getBlock(nx, ny)) break;
				steps++;
			}
			if (steps === 0) return core.drawFailTip("前方没有空间");

			const tid = target.event.id,
				cls = target.event.cls;
			const ox = target.x,
				oy = target.y;
			const tx = ox + scan.x * steps,
				ty = oy + scan.y * steps;
			const theme = getColorTheme(tid, cls);

			// ★ 先存档、计数、执行数据变更（录像/非录像一致）
			this.skillAutosave();
			this.addSkillCount("I344");
			core.removeBlock(ox, oy);
			core.setBlock(tid, tx, ty);
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效（不影响数据） ====================
			core.lockControl();
			const gameDom = document.getElementById('gameCanvas');

			const s = new Sprite(ox * 32, oy * 32, 32, 32, 140, 'game');
			core.drawIcon(s.context, tid, 0, 0);
			s.canvas.style.filter = "brightness(1.5)";
			s.canvas.style.boxShadow = `0 0 25px ${theme.glow}`;
			core.removeBlock(tx, ty);

			const overlay = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 90, 'game');
			overlay.canvas.style.backgroundColor = "rgba(0, 20, 50, 0.55)";
			overlay.canvas.style.transition = "opacity 0.3s";

			// ★ 蓄力：画面变暗 + 物块颤抖 + 能量汇聚
			const createChargeAura = (callback) => {
				core.playSound('jump.mp3');
				const aura = new Sprite(hx * 32 - 48, hy * 32 - 48, 128, 128, 140, 'game');
				let shakeTimer = setInterval(() => {
					if (s.canvas) s.canvas.style.transform = `translate(${(Math.random() - 0.5) * 4}px, ${(Math.random() - 0.5) * 4}px)`;
				}, 30);
				if (gameDom) {
					gameDom.style.transition = "filter 0.3s";
					gameDom.style.filter = "brightness(0.7) contrast(1.2)";
				}
				let f = 0;
				const t = setInterval(() => {
					f++;
					const ctx = aura.context;
					if (!ctx || f > 22) {
						clearInterval(t);
						clearInterval(shakeTimer);
						aura.destroy();
						if (s.canvas) s.canvas.style.transform = '';
						if (gameDom) {
							gameDom.style.filter = '';
							gameDom.style.transition = '';
						}
						if (callback) callback();
						return;
					}
					core.clearMap(ctx);
					const progress = f / 22;
					for (let ring = 0; ring < 2; ring++) {
						const radius = (60 - f * 2.2) * (ring === 0 ? 1 : 0.65);
						if (radius > 0) {
							ctx.beginPath();
							ctx.arc(64, 64, radius, 0, Math.PI * 2);
							ctx.strokeStyle = `rgba(${theme.rgb}, ${progress * (ring === 0 ? 1 : 0.5)})`;
							ctx.lineWidth = ring === 0 ? 2.5 : 1.5;
							ctx.stroke();
						}
					}
					ctx.strokeStyle = `rgba(${theme.rgb}, ${progress * 0.9})`;
					ctx.lineWidth = 2;
					for (let i = 0; i < 8; i++) {
						const ang = (i / 8) * Math.PI * 2 + f * 0.12;
						const outerR = 60 - progress * 25,
							innerR = Math.max(3, outerR - 30 * progress);
						ctx.beginPath();
						ctx.moveTo(64 + Math.cos(ang) * outerR, 64 + Math.sin(ang) * outerR);
						ctx.lineTo(64 + Math.cos(ang) * innerR, 64 + Math.sin(ang) * innerR);
						ctx.stroke();
					}
					ctx.fillStyle = `rgba(${theme.rgb}, ${1 - progress * 0.3})`;
					for (let i = 0; i < 8; i++) {
						const ang = Math.random() * Math.PI * 2,
							r = (1 - progress) * 55 + Math.random() * 8;
						const sz = 1.5 + (1 - progress) * 2.5;
						ctx.fillRect(64 + Math.cos(ang) * r - sz / 2, 64 + Math.sin(ang) * r - sz / 2, sz, sz);
					}
					const grd = ctx.createRadialGradient(64, 64, 0, 64, 64, 6 + progress * 12);
					grd.addColorStop(0, `rgba(255, 255, 255, ${progress})`);
					grd.addColorStop(0.4, `rgba(${theme.rgb}, ${progress * 0.7})`);
					grd.addColorStop(1, 'rgba(0, 0, 0, 0)');
					ctx.fillStyle = grd;
					ctx.beginPath();
					ctx.arc(64, 64, 6 + progress * 12, 0, Math.PI * 2);
					ctx.fill();
					ctx.strokeStyle = `rgba(${theme.rgb}, ${progress * 0.4})`;
					ctx.lineWidth = 1;
					for (let i = 0; i < 4; i++) {
						const ang = (i / 4) * Math.PI * 2 + 0.5,
							len = progress * 18;
						ctx.beginPath();
						ctx.moveTo(64, 64);
						ctx.lineTo(64 + Math.cos(ang) * len, 64 + Math.sin(ang) * len);
						ctx.stroke();
					}
				}, 16);
			};

			// ★ 撞墙：全屏闪白 + 滤镜 + 碎片 + 冲击波 + 裂纹 + 烟尘
			const createImpactResolution = (impX, impY) => {
				const flashScreen = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 200, 'game');
				flashScreen.canvas.style.backgroundColor = 'rgba(255,255,255,0.7)';
				flashScreen.canvas.style.transition = "opacity 0.15s";
				setTimeout(() => { if (flashScreen.canvas) flashScreen.canvas.style.opacity = 0; }, 30);
				setTimeout(() => { flashScreen.destroy(); }, 200);
				if (gameDom) {
					gameDom.style.filter = "contrast(1.6) brightness(1.3) saturate(1.5)";
					setTimeout(() => { if (gameDom) gameDom.style.filter = ''; }, 180);
				}
				const burst = new Sprite(impX * 32 - 64, impY * 32 - 64, 160, 160, 142, 'game');
				const debris = [],
					dust = [];
				for (let i = 0; i < 18; i++) {
					const ang = Math.random() * Math.PI * 2,
						spd = 3 + Math.random() * 5;
					debris.push({ x: 80, y: 80, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd - 3, size: 2.5 + Math.random() * 4, rot: Math.random() * 360, rotSpd: (Math.random() - 0.5) * 25, life: 1.0 });
				}
				let f = 0;
				const t = setInterval(() => {
					f++;
					const ctx = burst.context;
					if (!ctx || f > 40) {
						clearInterval(t);
						burst.destroy();
						return;
					}
					core.clearMap(ctx);
					const op = Math.max(0, 1 - f / 40);
					if (f <= 18) {
						for (let i = 0; i < 3; i++) {
							const r = f * 4.5 + i * 10,
								ringOp = (1 - f / 18) * (0.9 - i * 0.25);
							if (ringOp > 0) {
								ctx.strokeStyle = `rgba(${theme.rgb},${ringOp})`;
								ctx.lineWidth = 3.5 - i * 0.8;
								ctx.beginPath();
								ctx.arc(80, 80, r, 0, Math.PI * 2);
								ctx.stroke();
							}
						}
					}
					if (f <= 12) {
						const lo = 1 - f / 12,
							ll = f * 7;
						ctx.strokeStyle = `rgba(255,255,255,${lo})`;
						ctx.lineWidth = 2.5;
						ctx.shadowBlur = 10;
						ctx.shadowColor = theme.glow;
						ctx.beginPath();
						ctx.moveTo(80, 80 - ll);
						ctx.lineTo(80, 80 + ll);
						ctx.stroke();
						ctx.beginPath();
						ctx.moveTo(80 - ll, 80);
						ctx.lineTo(80 + ll, 80);
						ctx.stroke();
						const dl = ll * 0.7;
						ctx.strokeStyle = `rgba(${theme.rgb},${lo * 0.5})`;
						ctx.lineWidth = 1.5;
						ctx.beginPath();
						ctx.moveTo(80 - dl, 80 - dl);
						ctx.lineTo(80 + dl, 80 + dl);
						ctx.stroke();
						ctx.beginPath();
						ctx.moveTo(80 + dl, 80 - dl);
						ctx.lineTo(80 - dl, 80 + dl);
						ctx.stroke();
						ctx.shadowBlur = 0;
					}
					if (f <= 8) {
						const fo = 1 - f / 8;
						const grd = ctx.createRadialGradient(80, 80, 0, 80, 80, 30);
						grd.addColorStop(0, `rgba(255,255,255,${fo})`);
						grd.addColorStop(0.3, `rgba(${theme.rgb},${fo * 0.6})`);
						grd.addColorStop(1, 'rgba(0,0,0,0)');
						ctx.fillStyle = grd;
						ctx.beginPath();
						ctx.arc(80, 80, 30, 0, Math.PI * 2);
						ctx.fill();
					}
					if (f >= 3) {
						ctx.strokeStyle = `rgba(${theme.rgb},${op * 0.5})`;
						ctx.lineWidth = 1.5;
						for (let i = 0; i < 6; i++) {
							const ang = (i / 6) * Math.PI * 2 + 0.2,
								len = Math.min((f - 3) * 3.5, 35);
							ctx.beginPath();
							ctx.moveTo(80, 80);
							let cx = 80,
								cy = 80;
							for (let j = 0; j < 4; j++) {
								const sl = len / 4;
								cx += Math.cos(ang + (((i * 7 + j * 3) % 5) - 2) * 0.25) * sl;
								cy += Math.sin(ang + (((i * 3 + j * 7) % 5) - 2) * 0.25) * sl;
								ctx.lineTo(cx, cy);
							}
							ctx.stroke();
						}
					}
					debris.forEach(d => {
						if (d.life <= 0) return;
						d.x += d.vx;
						d.y += d.vy;
						d.vy += 0.18;
						d.vx *= 0.97;
						d.rot += d.rotSpd;
						d.life -= 0.022;
						ctx.save();
						ctx.translate(d.x, d.y);
						ctx.rotate(d.rot * Math.PI / 180);
						ctx.fillStyle = `rgba(${theme.rgb},${d.life})`;
						ctx.shadowBlur = 4;
						ctx.shadowColor = theme.glow;
						ctx.fillRect(-d.size / 2, -d.size / 2, d.size, d.size);
						ctx.shadowBlur = 0;
						ctx.restore();
					});
					if (f >= 4 && f <= 22 && f % 2 === 0) { for (let i = 0; i < 2; i++) dust.push({ x: 80 + (Math.random() - 0.5) * 25, y: 80 + (Math.random() - 0.5) * 15, r: 6 + Math.random() * 10, life: 1.0, vx: (Math.random() - 0.5) * 2, vy: -Math.random() * 2 }); }
					dust.forEach(d => {
						if (d.life <= 0) return;
						d.x += d.vx;
						d.y += d.vy;
						d.r += 0.4;
						d.life -= 0.03;
						ctx.fillStyle = `rgba(140,130,120,${d.life * 0.35})`;
						ctx.beginPath();
						ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
						ctx.fill();
					});
				}, 16);
			};

			createChargeAura(() => {
				core.playSound('attack.mp3');
				const trail = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
				const speedLines = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 125, 'game');
				let trailParticles = [];
				const totalFrames = Math.max(steps * 2.5, 4);
				const moveDx = (tx * 32 - ox * 32) / totalFrames;
				const moveDy = (ty * 32 - oy * 32) / totalFrames;
				let moveF = 0;

				const mTimer = setInterval(() => {
					moveF++;
					if (!s.canvas) { clearInterval(mTimer); return; }
					s.move(moveDx, moveDy, true);

					// 飞行拉伸 + 发光脉冲
					const speed = Math.hypot(moveDx, moveDy);
					const stretchMain = 1 + speed / 30,
						stretchSide = 1 / (1 + speed / 50);
					if (direction === 'left' || direction === 'right') s.canvas.style.transform = `scale(${stretchMain}, ${stretchSide})`;
					else s.canvas.style.transform = `scale(${stretchSide}, ${stretchMain})`;
					s.canvas.style.boxShadow = `0 0 ${20 + 8 * Math.sin(moveF * 0.6)}px ${theme.glow}`;

					// 彗星火焰拖尾
					for (let i = 0; i < 3; i++) trailParticles.push({ x: s.x + 16 + (Math.random() - 0.5) * 10, y: s.y + 16 + (Math.random() - 0.5) * 10, life: 0.6 + Math.random() * 0.4, size: i === 0 ? 10 + Math.random() * 6 : 3 + Math.random() * 4 });
					const tCtx = trail.context;
					core.clearMap(tCtx);
					// 尾焰光柱
					tCtx.save();
					const tailLen = Math.min(moveF * 4, 50);
					const tailGrd = tCtx.createLinearGradient(s.x + 16, s.y + 16, s.x + 16 - scan.x * tailLen, s.y + 16 - scan.y * tailLen);
					tailGrd.addColorStop(0, `rgba(${theme.rgb}, 0.5)`);
					tailGrd.addColorStop(1, 'rgba(0,0,0,0)');
					tCtx.strokeStyle = tailGrd;
					tCtx.lineWidth = 8;
					tCtx.lineCap = 'round';
					tCtx.beginPath();
					tCtx.moveTo(s.x + 16, s.y + 16);
					tCtx.lineTo(s.x + 16 - scan.x * tailLen, s.y + 16 - scan.y * tailLen);
					tCtx.stroke();
					tCtx.restore();
					for (let i = trailParticles.length - 1; i >= 0; i--) {
						const p = trailParticles[i];
						p.life -= 0.07;
						if (p.life <= 0) { trailParticles.splice(i, 1); continue; } tCtx.fillStyle = `rgba(${theme.rgb}, ${p.life * 0.8})`;
						tCtx.beginPath();
						tCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
						tCtx.fill();
					}

					// 速度线
					const slCtx = speedLines.context;
					core.clearMap(slCtx);
					slCtx.strokeStyle = `rgba(${theme.rgb}, 0.3)`;
					slCtx.lineWidth = 1.5;
					for (let i = 0; i < 5; i++) {
						const offset = (Math.random() - 0.5) * 35,
							lineLen = 15 + Math.random() * 30;
						slCtx.beginPath();
						if (direction === 'left' || direction === 'right') {
							slCtx.moveTo(s.x + 16 - lineLen, s.y + 16 + offset);
							slCtx.lineTo(s.x + 16 + lineLen, s.y + 16 + offset);
						} else {
							slCtx.moveTo(s.x + 16 + offset, s.y + 16 - lineLen);
							slCtx.lineTo(s.x + 16 + offset, s.y + 16 + lineLen);
						}
						slCtx.stroke();
					}

					if (moveF >= totalFrames) {
						clearInterval(mTimer);
						if (s.canvas) s.canvas.style.transform = '';
						s.destroy();
						speedLines.destroy();
						core.playSound('bomb.mp3');
						createImpactResolution(tx, ty);
						let shake = 18;
						const st = setInterval(() => {
							if (gameDom) gameDom.style.transform = `translate(${Math.random() * shake - shake / 2}px, ${Math.random() * shake - shake / 2}px)`;
							shake *= 0.68;
							if (shake < 0.5) { clearInterval(st); if (gameDom) gameDom.style.transform = ''; }
						}, 25);
						overlay.canvas.style.opacity = 0;
						let fadeT = 1.0;
						const clearT = setInterval(() => {
							fadeT -= 0.08;
							if (trail.canvas) trail.canvas.style.opacity = fadeT;
							if (fadeT <= 0) {
								clearInterval(clearT);
								trail.destroy();
								overlay.destroy();
							}
						}, 30);
						core.setBlock(tid, tx, ty);
						core.drawMap();
						core.unlockControl();
					}
				}, 16);
			});

		}

		// ==================== 3. 回力箭 (I345) ====================
		// 【修复6】数据变更在lockControl之前
		this.pullArrow = function () {
			if (this.getSkillRemainCount("I345") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];
			const revDir = { "up": "down", "down": "up", "left": "right", "right": "left" }[direction];

			let target = null;
			for (let i = 1; i <= 12; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b) { target = b; break; }
			}
			if (!target || (Math.abs(target.x - hx) <= 1 && Math.abs(target.y - hy) <= 1)) return core.drawFailTip("前方太近或没东西");

			const tx = target.x,
				ty = target.y;
			const tid = target.event.id,
				cls = target.event.cls;
			const endX = hx + scan.x,
				endY = hy + scan.y;
			const theme = getColorTheme(tid, cls);

			// ★ 先存档、计数、执行数据变更
			this.skillAutosave();
			this.addSkillCount("I345");
			core.removeBlock(tx, ty);
			core.setBlock(tid, endX, endY);
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效 ====================
			core.lockControl();
			core.playSound('jump.mp3');
			const gameDom = document.getElementById('gameCanvas');

			// 暂时隐藏目标位置的真实图块
			core.removeBlock(endX, endY);

			const overlay = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 90, 'game');
			overlay.canvas.style.backgroundColor = "rgba(0, 20, 50, 0.5)";
			overlay.canvas.style.transition = "opacity 0.2s";

			// ★ 第一阶段：能量锁链射出（从勇士飞向目标，锁定目标后闪光）
			const chainFx = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 140, 'game');
			let waF = 0;
			const chainDuration = 10;
			const waTimer = setInterval(() => {
				waF++;
				const waCtx = chainFx.context;
				if (!waCtx || waF > chainDuration) {
					clearInterval(waTimer);
					chainFx.destroy();
					startPullAnim();
					return;
				}
				core.clearMap(waCtx);
				const progress = waF / chainDuration;
				const startX = hx * 32 + 16,
					startY = hy * 32 + 16;
				const targetX = tx * 32 + 16,
					targetY = ty * 32 + 16;
				const currentX = startX + (targetX - startX) * progress;
				const currentY = startY + (targetY - startY) * progress;

				// 能量链（锯齿闪电线连接勇士和前端）
				waCtx.strokeStyle = `rgba(${theme.rgb}, ${0.7 + 0.3 * Math.sin(waF * 0.8)})`;
				waCtx.lineWidth = 2.5;
				waCtx.shadowBlur = 8;
				waCtx.shadowColor = theme.glow;
				waCtx.beginPath();
				waCtx.moveTo(startX, startY);
				const segments = 8;
				for (let i = 1; i <= segments; i++) {
					const t = (i / segments) * progress;
					const lx = startX + (targetX - startX) * t + (Math.random() - 0.5) * 8;
					const ly = startY + (targetY - startY) * t + (Math.random() - 0.5) * 8;
					waCtx.lineTo(lx, ly);
				}
				waCtx.stroke();
				waCtx.shadowBlur = 0;

				// 前端能量球
				const grd = waCtx.createRadialGradient(currentX, currentY, 0, currentX, currentY, 10);
				grd.addColorStop(0, `rgba(255, 255, 255, 0.9)`);
				grd.addColorStop(0.5, `rgba(${theme.rgb}, 0.7)`);
				grd.addColorStop(1, 'rgba(0,0,0,0)');
				waCtx.fillStyle = grd;
				waCtx.beginPath();
				waCtx.arc(currentX, currentY, 10, 0, Math.PI * 2);
				waCtx.fill();

				// 飞行粒子
				waCtx.fillStyle = `rgba(${theme.rgb}, ${0.6})`;
				for (let i = 0; i < 4; i++) {
					waCtx.fillRect(currentX + (Math.random() - 0.5) * 14, currentY + (Math.random() - 0.5) * 14, 2, 2);
				}

				// 快到目标时：目标位置闪光锁定圈
				if (progress > 0.7) {
					const lockOp = (progress - 0.7) / 0.3;
					waCtx.strokeStyle = `rgba(${theme.rgb}, ${lockOp * 0.8})`;
					waCtx.lineWidth = 2;
					waCtx.beginPath();
					waCtx.arc(targetX, targetY, 20 - lockOp * 5, 0, Math.PI * 2);
					waCtx.stroke();
					waCtx.beginPath();
					waCtx.arc(targetX, targetY, 12 + lockOp * 3, 0, Math.PI * 2);
					waCtx.stroke();
				}
			}, 16);

			// ★ 第二阶段：物块被拉回（加速飞行 + 拖尾 + 勇士身边磁力漩涡）
			const startPullAnim = () => {
				const s = new Sprite(tx * 32, ty * 32, 32, 32, 140, 'game');
				core.drawIcon(s.context, tid, 0, 0);
				s.canvas.style.boxShadow = `0 0 20px ${theme.glow}`;
				core.playSound('jump.mp3');

				// 磁力漩涡（勇士面前持续旋转）
				const vortex = new Sprite(endX * 32 - 16, endY * 32 - 16, 64, 64, 135, 'game');
				let vortexF = 0;
				const vortexTimer = setInterval(() => {
					vortexF++;
					const vCtx = vortex.context;
					if (!vCtx) { clearInterval(vortexTimer); return; }
					core.clearMap(vCtx);
					const vOp = 0.3 + 0.2 * Math.sin(vortexF * 0.15);
					vCtx.save();
					vCtx.translate(32, 32);
					vCtx.rotate(vortexF * 0.1);
					for (let i = 0; i < 3; i++) {
						const r = 12 + i * 6;
						vCtx.strokeStyle = `rgba(${theme.rgb}, ${vOp * (1 - i * 0.25)})`;
						vCtx.lineWidth = 1.5;
						vCtx.beginPath();
						vCtx.arc(0, 0, r, i * 0.8, i * 0.8 + Math.PI * 1.2);
						vCtx.stroke();
					}
					vCtx.restore();
				}, 16);

				// 拉回拖尾
				const trail = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
				let trailParticles = [];

				const totalDist = Math.max(Math.abs(tx - endX), Math.abs(ty - endY)) * 32;
				let currentDist = 0,
					velocity = 2,
					acceleration = 1.8;

				const timer = setInterval(() => {
					velocity += acceleration;
					currentDist += velocity;
					const progress = Math.min(currentDist / totalDist, 1);
					if (!s.canvas) { clearInterval(timer); return; }

					// 拉伸变形
					const stretch = 1 + (velocity / 30),
						squeeze = 1 / stretch;
					s.canvas.style.transform = (revDir === 'up' || revDir === 'down') ? `scale(${squeeze}, ${stretch})` : `scale(${stretch}, ${squeeze})`;
					// 发光脉冲
					s.canvas.style.boxShadow = `0 0 ${15 + velocity * 0.8}px ${theme.glow}`;

					const curX = tx * 32 + (endX - tx) * 32 * progress;
					const curY = ty * 32 + (endY - ty) * 32 * progress;
					s.move(curX, curY);

					// 拖尾粒子（反向散射）
					for (let i = 0; i < 3; i++) {
						trailParticles.push({
							x: curX + 16 + (Math.random() - 0.5) * 8,
							y: curY + 16 + (Math.random() - 0.5) * 8,
							life: 0.5 + Math.random() * 0.5,
							size: i === 0 ? 8 + Math.random() * 5 : 2 + Math.random() * 3
						});
					}
					const tCtx = trail.context;
					core.clearMap(tCtx);
					// 能量链（从目标到物块的渐变线）
					if (progress < 0.9) {
						const chainGrd = tCtx.createLinearGradient(curX + 16, curY + 16, endX * 32 + 16, endY * 32 + 16);
						chainGrd.addColorStop(0, `rgba(${theme.rgb}, ${0.4 * (1 - progress)})`);
						chainGrd.addColorStop(1, `rgba(${theme.rgb}, ${0.15 * (1 - progress)})`);
						tCtx.strokeStyle = chainGrd;
						tCtx.lineWidth = 2;
						tCtx.lineCap = 'round';
						tCtx.beginPath();
						tCtx.moveTo(curX + 16, curY + 16);
						tCtx.lineTo(endX * 32 + 16, endY * 32 + 16);
						tCtx.stroke();
					}
					// 粒子渲染
					for (let i = trailParticles.length - 1; i >= 0; i--) {
						const p = trailParticles[i];
						p.life -= 0.08;
						if (p.life <= 0) { trailParticles.splice(i, 1); continue; }
						tCtx.fillStyle = `rgba(${theme.rgb}, ${p.life * 0.7})`;
						tCtx.beginPath();
						tCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
						tCtx.fill();
					}

					if (progress >= 1) {
						clearInterval(timer);
						clearInterval(vortexTimer);
						vortex.destroy();
						if (overlay.canvas) overlay.canvas.style.opacity = 0;
						core.playSound('item.mp3');

						// ★ 第三阶段：落地冲击（冲击波 + 弹跳 + 碎片 + 闪光）
						// 落地闪光
						const flashFx = new Sprite(endX * 32 - 16, endY * 32 - 16, 64, 64, 145, 'game');
						let flashF = 0;
						const flashTimer = setInterval(() => {
							flashF++;
							const fctx = flashFx.context;
							if (!fctx || flashF > 10) {
								clearInterval(flashTimer);
								flashFx.destroy();
								return;
							}
							core.clearMap(fctx);
							const fop = 1 - flashF / 10;
							// 冲击波环
							for (let i = 0; i < 2; i++) {
								fctx.strokeStyle = `rgba(${theme.rgb}, ${fop * (0.8 - i * 0.3)})`;
								fctx.lineWidth = 2.5 - i;
								fctx.beginPath();
								fctx.arc(32, 32, flashF * 3.5 + i * 6, 0, Math.PI * 2);
								fctx.stroke();
							}
							// 中心白光
							const grd = fctx.createRadialGradient(32, 32, 0, 32, 32, 15 * fop);
							grd.addColorStop(0, `rgba(255,255,255,${fop})`);
							grd.addColorStop(1, 'rgba(0,0,0,0)');
							fctx.fillStyle = grd;
							fctx.beginPath();
							fctx.arc(32, 32, 15 * fop, 0, Math.PI * 2);
							fctx.fill();
							// 碎片飞溅
							fctx.fillStyle = `rgba(${theme.rgb}, ${fop})`;
							for (let i = 0; i < 6; i++) {
								const ang = (i / 6) * Math.PI * 2 + flashF * 0.3;
								const r = flashF * 3;
								fctx.fillRect(32 + Math.cos(ang) * r, 32 + Math.sin(ang) * r, 2.5, 2.5);
							}
						}, 16);

						// 轻微震屏
						if (gameDom) {
							let shake = 8;
							const st = setInterval(() => {
								gameDom.style.transform = `translate(${Math.random() * shake - shake / 2}px, ${Math.random() * shake - shake / 2}px)`;
								shake *= 0.72;
								if (shake < 0.5) {
									clearInterval(st);
									gameDom.style.transform = '';
								}
							}, 30);
						}

						// 弹跳
						let bounce = 0;
						const bt = setInterval(() => {
							bounce++;
							if (!s.canvas) { clearInterval(bt); return; }
							const bS = 1 + Math.sin(bounce * 1.5) * 0.3 * (1 - bounce / 14);
							s.canvas.style.transform = `scale(${bS}, ${2 - bS})`;
							if (bounce >= 14) {
								clearInterval(bt);
								s.destroy();
								overlay.destroy();
								trail.destroy();
								core.setBlock(tid, endX, endY);
								core.drawMap();
								core.unlockControl();
							}
						}, 20);
					}
				}, 16);
			};

		}

		// ==================== 4. 御箭术 (I346) ====================
		// 【修复4】数据逻辑在动画前完成，消除setTimeout嵌套中的数据操作风险
		this.goToDoorArrow = function () {
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];

			let door = null;
			for (let i = 1; i <= 12; i++) {
				const x = hx + i * scan.x,
					y = hy + i * scan.y;
				const b = core.getBlock(x, y);
				if (b && b.event.trigger === "openDoor") { door = b; break; }
			}
			if (this.getSkillRemainCount("I346") <= 0) return core.drawFailTip("本层次数已满");
			if (!door) return core.drawFailTip("前方没有门");

			const tx = door.x,
				ty = door.y,
				tid = door.event.id;
			const theme = getColorTheme(tid, '');

			// ★ 先存档、计数、执行数据变更（录像/非录像一致）
			this.skillAutosave();
			this.addSkillCount("I346");
			core.setHeroLoc('x', tx);
			core.setHeroLoc('y', ty);
			core.drawHero();
			core.updateStatusBar();

			if (core.isReplaying()) return;

			// ==================== 纯视觉特效 ====================
			core.lockControl();
			const gameDom = document.getElementById('gameCanvas');

			const mask = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
			const mCtx = mask.context;
			mask.canvas.style.opacity = 0;
			mask.canvas.style.transition = "opacity 0.2s";

			// 能量光球（多层渐变发光）
			const s = new Sprite(hx * 32 - 8, hy * 32 - 8, 48, 48, 140, 'game');
			const sCtx = s.context;
			const drawLightOrb = (ctx, size) => {
				const c = size / 2;
				// 外层光晕
				const grd2 = ctx.createRadialGradient(c, c, 0, c, c, c);
				grd2.addColorStop(0, 'rgba(255,255,255,0)');
				grd2.addColorStop(0.5, `rgba(${theme.rgb}, 0.15)`);
				grd2.addColorStop(1, 'rgba(0,0,0,0)');
				ctx.fillStyle = grd2;
				ctx.beginPath();
				ctx.arc(c, c, c, 0, Math.PI * 2);
				ctx.fill();
				// 核心光球
				const grd = ctx.createRadialGradient(c, c, 1, c, c, c * 0.6);
				grd.addColorStop(0, '#FFFFFF');
				grd.addColorStop(0.4, `rgba(${theme.rgb}, 0.9)`);
				grd.addColorStop(1, 'rgba(0,0,0,0)');
				ctx.fillStyle = grd;
				ctx.beginPath();
				ctx.arc(c, c, c * 0.6, 0, Math.PI * 2);
				ctx.fill();
			};
			drawLightOrb(sCtx, 48);
			s.canvas.style.boxShadow = `0 0 35px ${theme.glow}`;

			const dustCanvas = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 135, 'game');
			let dustParticles = [];
			const trailBurn = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 125, 'game');

			if (core.canvas.hero) core.canvas.hero.canvas.style.opacity = 0;
			core.playSound('jump.mp3');

			// ★ 起飞阶段：勇士化光 + 能量球膨胀 + 起飞冲击波
			s.canvas.style.transform = "scale(0.05)";
			s.canvas.style.transition = "transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275)";
			setTimeout(() => { if (s.canvas) s.canvas.style.transform = "scale(1.3)"; }, 10);

			// 起飞冲击波
			const launchFx = new Sprite(hx * 32 - 32, hy * 32 - 32, 96, 96, 138, 'game');
			let launchF = 0;
			const launchTimer = setInterval(() => {
				launchF++;
				const lctx = launchFx.context;
				if (!lctx || launchF > 15) {
					clearInterval(launchTimer);
					launchFx.destroy();
					return;
				}
				core.clearMap(lctx);
				const lop = 1 - launchF / 15;
				// 扩散环
				for (let i = 0; i < 2; i++) {
					lctx.strokeStyle = `rgba(${theme.rgb}, ${lop * (0.7 - i * 0.3)})`;
					lctx.lineWidth = 2 - i * 0.5;
					lctx.beginPath();
					lctx.arc(48, 48, launchF * 3 + i * 8, 0, Math.PI * 2);
					lctx.stroke();
				}
				// 起飞粒子
				lctx.fillStyle = `rgba(${theme.rgb}, ${lop * 0.8})`;
				for (let i = 0; i < 5; i++) {
					const ang = Math.random() * Math.PI * 2;
					const r = launchF * 2.5;
					lctx.fillRect(48 + Math.cos(ang) * r, 48 + Math.sin(ang) * r, 2, 2);
				}
			}, 16);

			// 画面暗化
			if (gameDom) {
				gameDom.style.transition = "filter 0.15s";
				gameDom.style.filter = "brightness(0.6)";
			}

			setTimeout(() => {
				mask.canvas.style.opacity = 1;
				if (s.canvas) s.canvas.style.transition = "";
				if (gameDom) {
					gameDom.style.transition = "transform 0.3s ease, filter 0.3s";
					gameDom.style.transform = "scale(1.02)";
					gameDom.style.filter = "brightness(0.5)";
				}

				const dist = Math.max(Math.abs(tx - hx), Math.abs(ty - hy)) * 32;
				let curDist = 0;
				let flyF = 0;

				const timer = setInterval(() => {
					curDist += 24;
					flyF++;
					const progress = Math.min(curDist / dist, 1);
					const curX = hx * 32 + (tx - hx) * 32 * progress;
					const curY = hy * 32 + (ty - hy) * 32 * progress;

					if (s.canvas) {
						// 飞行拉伸 + 呼吸发光
						s.canvas.style.transform = (direction === 'up' || direction === 'down') ? `scale(0.4, 2.5)` : `scale(2.5, 0.4)`;
						s.canvas.style.boxShadow = `0 0 ${30 + 10 * Math.sin(flyF * 0.5)}px ${theme.glow}`;
						s.move(curX - 8, curY - 8);

						// 黑暗遮罩（只露出光球周围）
						core.clearMap(mCtx);
						mCtx.fillStyle = "rgba(0, 0, 0, 0.88)";
						mCtx.fillRect(0, 0, core.__PIXELS__, core.__PIXELS__);
						mCtx.globalCompositeOperation = 'destination-out';
						const lightGrd = mCtx.createRadialGradient(curX + 16, curY + 16, 0, curX + 16, curY + 16, 72);
						lightGrd.addColorStop(0, 'rgba(255, 255, 255, 1)');
						lightGrd.addColorStop(0.7, 'rgba(255, 255, 255, 0.5)');
						lightGrd.addColorStop(1, 'rgba(255, 255, 255, 0)');
						mCtx.fillStyle = lightGrd;
						mCtx.beginPath();
						mCtx.arc(curX + 16, curY + 16, 72, 0, Math.PI * 2);
						mCtx.fill();
						mCtx.globalCompositeOperation = 'source-over';

						// ★ 飞行路径灼烧痕迹
						const bCtx = trailBurn.context;
						const burnGrd = bCtx.createRadialGradient(curX + 16, curY + 16, 0, curX + 16, curY + 16, 10);
						burnGrd.addColorStop(0, `rgba(${theme.rgb}, 0.3)`);
						burnGrd.addColorStop(1, 'rgba(0,0,0,0)');
						bCtx.fillStyle = burnGrd;
						bCtx.beginPath();
						bCtx.arc(curX + 16, curY + 16, 10, 0, Math.PI * 2);
						bCtx.fill();

						// ★ 速度线
						const dCtx = dustCanvas.context;
						core.clearMap(dCtx);
						// 速度线
						dCtx.strokeStyle = `rgba(${theme.rgb}, 0.3)`;
						dCtx.lineWidth = 1;
						for (let i = 0; i < 4; i++) {
							const offset = (Math.random() - 0.5) * 30;
							const lineLen = 12 + Math.random() * 20;
							if (direction === 'left' || direction === 'right') {
								dCtx.beginPath();
								dCtx.moveTo(curX + 16 - lineLen, curY + 16 + offset);
								dCtx.lineTo(curX + 16 + lineLen, curY + 16 + offset);
								dCtx.stroke();
							} else {
								dCtx.beginPath();
								dCtx.moveTo(curX + 16 + offset, curY + 16 - lineLen);
								dCtx.lineTo(curX + 16 + offset, curY + 16 + lineLen);
								dCtx.stroke();
							}
						}
						// 飞行粒子
						if (progress < 1 && Math.random() > 0.2) {
							dustParticles.push({ x: curX + 16 + (Math.random() * 12 - 6), y: curY + 16 + (Math.random() * 12 - 6), life: 1.0, size: 1.5 + Math.random() * 2 });
						}
						dCtx.fillStyle = `rgba(${theme.rgb}, 1)`;
						for (let i = dustParticles.length - 1; i >= 0; i--) {
							const p = dustParticles[i];
							p.life -= 0.08;
							if (p.life <= 0) { dustParticles.splice(i, 1); continue; }
							dCtx.globalAlpha = p.life;
							dCtx.beginPath();
							dCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
							dCtx.fill();
						}
						dCtx.globalAlpha = 1.0;
					}

					if (progress >= 1) {
						clearInterval(timer);
						core.playSound('door.mp3');
						if (gameDom) {
							gameDom.style.transform = "";
							gameDom.style.filter = "";
							setTimeout(() => { gameDom.style.transition = ""; }, 300);
						}

						s.destroy();

						// ★ 着陆特效：旋风 + 冲击波 + 光柱
						const landFx = new Sprite(tx * 32 - 32, ty * 32 - 32, 96, 96, 145, 'game');
						let landF = 0;
						const windBlades = [0, Math.PI * 0.66, Math.PI * 1.33];
						const landTimer = setInterval(() => {
							landF++;
							const lctx = landFx.context;
							if (!lctx || landF > 25) {
								clearInterval(landTimer);
								landFx.destroy();
								return;
							}
							core.clearMap(lctx);
							const lop = 1 - landF / 25;

							// 旋风叶片
							lctx.lineCap = "round";
							windBlades.forEach((angleOffset, index) => {
								const currentAngle = angleOffset + (landF * 0.35);
								const radiusX = 15 + landF * 1.5;
								const radiusY = 5 + landF * 0.5;
								const yOffset = 48 - (landF * 1.2);
								lctx.strokeStyle = index === 0 ? `rgba(255,255,255,${lop})` : `rgba(${theme.rgb}, ${lop})`;
								lctx.lineWidth = 2 + (landF / 6);
								lctx.beginPath();
								lctx.ellipse(48, yOffset, radiusX, radiusY, currentAngle, 0, Math.PI * 1.2);
								lctx.stroke();
							});

							// 底部扩散椭圆
							lctx.strokeStyle = `rgba(${theme.rgb}, ${lop * 0.5})`;
							lctx.lineWidth = 3;
							lctx.beginPath();
							lctx.ellipse(48, 48 + 16, landF * 2.5, landF * 1.2, 0, 0, Math.PI * 2);
							lctx.stroke();

							// 冲击波环
							if (landF <= 12) {
								for (let i = 0; i < 2; i++) {
									lctx.strokeStyle = `rgba(${theme.rgb}, ${(1 - landF / 12) * (0.6 - i * 0.2)})`;
									lctx.lineWidth = 2 - i * 0.5;
									lctx.beginPath();
									lctx.arc(48, 48, landF * 3 + i * 8, 0, Math.PI * 2);
									lctx.stroke();
								}
							}

							// 光柱（从上方射下）
							if (landF <= 15) {
								const pillarOp = (1 - landF / 15) * 0.5;
								const pillarGrd = lctx.createLinearGradient(48, 0, 48, 48);
								pillarGrd.addColorStop(0, `rgba(${theme.rgb}, 0)`);
								pillarGrd.addColorStop(0.5, `rgba(${theme.rgb}, ${pillarOp})`);
								pillarGrd.addColorStop(1, `rgba(255,255,255, ${pillarOp})`);
								lctx.fillStyle = pillarGrd;
								const pw = 8 + (1 - landF / 15) * 10;
								lctx.fillRect(48 - pw / 2, 0, pw, 50);
							}
						}, 16);

						// 轻微震屏
						if (gameDom) {
							let shake = 6;
							const st = setInterval(() => {
								gameDom.style.transform = `translate(${Math.random() * shake - shake / 2}px, ${Math.random() * shake - shake / 2}px)`;
								shake *= 0.75;
								if (shake < 0.5) {
									clearInterval(st);
									gameDom.style.transform = '';
								}
							}, 30);
						}

						// 勇士出现
						if (core.canvas.hero) {
							core.canvas.hero.canvas.style.opacity = 0;
							core.canvas.hero.canvas.style.transform = "translateY(10px)";
							core.canvas.hero.canvas.style.transition = "opacity 0.2s, transform 0.2s ease-out";
							setTimeout(() => {
								if (core.canvas.hero) {
									core.canvas.hero.canvas.style.opacity = 1;
									core.canvas.hero.canvas.style.transform = "translateY(0)";
								}
							}, 20);
						}

						// 遮罩淡出
						let fadeOp = 1;
						const ft = setInterval(() => {
							fadeOp -= 0.08;
							if (mask.canvas) mask.canvas.style.opacity = fadeOp;
							if (dustCanvas.canvas) dustCanvas.canvas.style.opacity = fadeOp;
							if (trailBurn.canvas) trailBurn.canvas.style.opacity = fadeOp;
							if (fadeOp <= 0) {
								clearInterval(ft);
								mask.destroy();
								dustCanvas.destroy();
								trailBurn.destroy();
								if (core.canvas.hero) {
									core.canvas.hero.canvas.style.transition = "";
									core.canvas.hero.canvas.style.transform = "";
								}
								core.unlockControl();
							}
						}, 30);
					}
				}, 16);
			}, 120);

		}

		// ==================== 5. 贯穿箭 (I347) ====================
		// 【修复1,2】数据逻辑全部在lockControl之前完成，insertAction也在解锁之后
		this.strikeArrow = function () {
			if (this.getSkillRemainCount("I347") <= 0) return core.drawFailTip("本层次数已满");
			const { x: hx, y: hy, direction } = core.status.hero.loc;
			const scan = core.utils.scan[direction];
			const floorId = core.status.floorId;

			let enemy = null,
				eDist = 0;
			for (let i = 2; i <= 3; i++) {
				const b = core.getBlock(hx + i * scan.x, hy + i * scan.y);
				if (b && b.event && b.event.cls && b.event.cls.startsWith("enemy")) {
					enemy = b;
					eDist = i;
					break;
				}
			}
			if (!enemy) return core.drawFailTip("2-3格范围内没有可以锁定的猎物");

			core.setFlag("strikeArrowOn", true);
			const predictDamage = core.getDamage(enemy.event.id, enemy.x, enemy.y);
			core.setFlag("strikeArrowOn", false);

			if (predictDamage === null || predictDamage >= core.status.hero.hp) {
				const mon = core.material.enemys[enemy.event.id];
				if (mon && mon.notBomb) return core.drawFailTip("面对BOSS你无法减伤，血量不足！");
				else return core.drawFailTip("即使减伤50%你也无法生存！");
			}

			let next = null;
			for (let j = eDist + 1; j <= 12; j++) {
				const b = core.getBlock(hx + j * scan.x, hy + j * scan.y);
				if (b && b.event) { next = b; break; }
			}

			const theme1 = getColorTheme(enemy.event.id, enemy.event.cls);

			// ★ 先存档、计数
			this.skillAutosave();
			this.addSkillCount("I347");

			// ★ 【修复1,2】数据逻辑：在锁定控制之前完成所有数据变更
			// 收集需要在战后执行的事件
			const pendingActions = [];

			const executeStrikeLogic = () => {
				core.setFlag("strikeArrowOn", true);
				core.setFlag('__forbidSave__', true);
				core.battle(enemy.event.id, enemy.x, enemy.y, true);
				core.setFlag("strikeArrowOn", false);
				core.setFlag('__forbidSave__', false);

				if (next && next.event && next.event.id) {
					const nid = next.event.id,
						nCls = next.event.cls || "";
					if (nCls.startsWith("enemy")) {
						const m2 = core.material.enemys[nid];
						if (m2 && m2.notBomb) {
							if (!core.isReplaying()) core.drawTip("第二目标不可炸，攻击被格挡！");
						} else {
							core.status.hero.money += (m2.money || 0);
							core.status.hero.experience += (m2.experience || 0);
							let actions = [];
							const locEvent2 = (core.floors[floorId].afterBattle || {})[next.x + "," + next.y];
							if (locEvent2) actions = actions.concat(locEvent2);
							if (next.event && next.event.afterBattle) actions = actions.concat(next.event.afterBattle);
							core.removeBlock(next.x, next.y);
							// ★ 【修复2】不在这里insertAction，先收集起来
							if (actions.length > 0) pendingActions.push(...actions);
						}
					} else if (nCls === "items") {
						if (core.getItemEffect) core.getItemEffect(nid, 1);
						else core.getItem(nid, 1, null, null);
						core.removeBlock(next.x, next.y);
					} else if (next.event.trigger === "openDoor") {
						core.removeBlock(next.x, next.y);
					} else if (!nCls.startsWith("npc") && (nCls !== "terrains" || nid.toLowerCase().includes("box") || nid.toLowerCase().includes("chest") || nid.toLowerCase().includes("arrow"))) {
						core.removeBlock(next.x, next.y);
					}
				}
				core.updateStatusBar();
			};

			// ★ 先执行数据逻辑（录像/非录像一致）
			// 但在非录像模式下，先用Sprite盖住怪物图案，避免视觉上怪物提前消失
			let enemyCover = null,
				nextCover = null;
			if (!core.isReplaying()) {
				enemyCover = new Sprite(enemy.x * 32, enemy.y * 32, 32, 32, 145, 'game');
				core.drawIcon(enemyCover.context, enemy.event.id, 0, 0);
				if (next && next.event && next.event.id) {
					nextCover = new Sprite(next.x * 32, next.y * 32, 32, 32, 145, 'game');
					core.drawIcon(nextCover.context, next.event.id, 0, 0);
				}
			}

			executeStrikeLogic();

			// ★ 【修复2】录像模式下直接插入待执行事件
			if (core.isReplaying()) {
				if (pendingActions.length > 0) core.insertAction(pendingActions);
				return;
			}

			// ==================== 纯视觉特效 ====================
			core.lockControl();


			// 【★ 切割分裂特效：被激光切成两半 → 垂直滑出 → 七彩粒子爆发】
			// 适用于任何物块（怪物、门、墙、道具等）
			const createSliceExplosion = function (x, y, blockId, scanDir) {
				const px = x * 32,
					py = y * 32;
				const isHorizontalBeam = (scanDir.x !== 0);

				// 半1
				const half1 = new Sprite(px, py, 32, 32, 173, 'game');
				const h1ctx = half1.context;
				core.drawIcon(h1ctx, blockId, 0, 0);
				if (isHorizontalBeam) {
					h1ctx.clearRect(0, 16, 32, 16); // 横切：保留上半
				} else {
					h1ctx.clearRect(16, 0, 16, 32); // 竖切：保留左半
				}

				// 半2
				const half2 = new Sprite(px, py, 32, 32, 173, 'game');
				const h2ctx = half2.context;
				core.drawIcon(h2ctx, blockId, 0, 0);
				if (isHorizontalBeam) {
					h2ctx.clearRect(0, 0, 32, 16); // 横切：保留下半
				} else {
					h2ctx.clearRect(0, 0, 16, 32); // 竖切：保留右半
				}

				// 滑出动画
				let slideF = 0;
				const totalFrames = 28;
				const slideTimer = setInterval(() => {
					slideF++;
					if (slideF > totalFrames) {
						clearInterval(slideTimer);
						half1.destroy();
						half2.destroy();
						createRainbowBurst(x, y);
						return;
					}
					const progress = slideF / totalFrames;
					const ease = 1 - Math.pow(1 - progress, 2.5);
					const mainDist = ease * 24;
					const sideDist = ease * 3;
					const fadeOp = Math.max(0, 1 - progress * 0.9);
					const rotAngle = progress * 8;

					if (half1.canvas) {
						half1.canvas.style.opacity = fadeOp;
						if (isHorizontalBeam) {
							half1.move(px - sideDist, py - mainDist); // 横切：上半往上
						} else {
							half1.move(px - mainDist, py - sideDist); // 竖切：左半往左
						}
						half1.canvas.style.transform = `rotate(${-rotAngle}deg)`;
					}
					if (half2.canvas) {
						half2.canvas.style.opacity = fadeOp;
						if (isHorizontalBeam) {
							half2.move(px + sideDist, py + mainDist); // 横切：下半往下
						} else {
							half2.move(px + mainDist, py + sideDist); // 竖切：右半往右
						}
						half2.canvas.style.transform = `rotate(${rotAngle}deg)`;
					}
				}, 16);
			};

			// 【★ 七彩粒子爆发】
			const createRainbowBurst = function (x, y) {
				const pCanvas = new Sprite(x * 32 - 48, y * 32 - 48, 128, 128, 174, 'game');
				const ctx = pCanvas.context;
				const numParticles = 45;
				const ps = [];
				for (let i = 0; i < numParticles; i++) {
					const ang = Math.random() * Math.PI * 2;
					const spd = 2.0 + Math.random() * 3.5;
					ps.push({
						x: 64,
						y: 64,
						vx: Math.cos(ang) * spd,
						vy: Math.sin(ang) * spd,
						r: 2 + Math.random() * 3,
						hue: i * (360 / numParticles),
						life: 1.0,
						spin: (Math.random() - 0.5) * 4
					});
				}
				const flash = new Sprite(x * 32 - 16, y * 32 - 16, 64, 64, 176, 'game');
				let flashF = 0;
				const flashTimer = setInterval(() => {
					flashF++;
					const fctx = flash.context;
					if (!fctx || flashF > 10) {
						clearInterval(flashTimer);
						flash.destroy();
						return;
					}
					core.clearMap(fctx);
					const op = 1 - flashF / 10;
					const grd = fctx.createRadialGradient(32, 32, 0, 32, 32, 30 * op + 5);
					grd.addColorStop(0, `rgba(255, 255, 255, ${op})`);
					grd.addColorStop(0.3, `rgba(255, 200, 100, ${op * 0.6})`);
					grd.addColorStop(1, 'rgba(0, 0, 0, 0)');
					fctx.fillStyle = grd;
					fctx.beginPath();
					fctx.arc(32, 32, 30 * op + 5, 0, Math.PI * 2);
					fctx.fill();
				}, 16);

				let f = 0,
					rot = 0;
				const pt = setInterval(() => {
					f++;
					if (!ctx || f >= 55) { clearInterval(pt); return pCanvas.destroy(); }
					core.clearMap(ctx);
					rot += 2;
					const globalOp = Math.max(0, 1 - f / 55);
					ctx.save();
					ctx.translate(64, 64);
					ctx.rotate(rot * Math.PI / 180);
					ctx.translate(-64, -64);
					ps.forEach(p => {
						p.x += p.vx;
						p.y += p.vy;
						p.vx *= 0.965;
						p.vy *= 0.965;
						p.vy += 0.08;
						p.life -= 0.018;
						p.hue += p.spin;
						if (p.life <= 0) return;
						const size = p.r * p.life;
						ctx.globalAlpha = p.life * globalOp;
						ctx.fillStyle = `hsl(${p.hue}, 100%, 70%)`;
						ctx.shadowBlur = 6;
						ctx.shadowColor = `hsl(${p.hue}, 100%, 50%)`;
						ctx.beginPath();
						ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
						ctx.fill();
						ctx.globalAlpha = p.life * globalOp * 0.3;
						ctx.beginPath();
						ctx.arc(p.x - p.vx * 2, p.y - p.vy * 2, size * 0.6, 0, Math.PI * 2);
						ctx.fill();
					});
					ctx.restore();
				}, 16);
			};


			core.playSound('jump.mp3');
			const mask = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 130, 'game');
			const mCtx = mask.context;
			mask.canvas.style.opacity = 0;
			mask.canvas.style.transition = "opacity 0.2s";

			let lockFrame = 0;
			const drawSpotlights = function () {
				lockFrame++;
				if (!mCtx) return;
				core.clearMap(mCtx);
				mCtx.fillStyle = "rgba(0, 0, 0, 0.85)";
				mCtx.fillRect(0, 0, core.__PIXELS__, core.__PIXELS__);
				mCtx.globalCompositeOperation = 'destination-out';
				const drawHalo = (x, y, r) => {
					const grd = mCtx.createRadialGradient(x * 32 + 16, y * 32 + 16, 0, x * 32 + 16, y * 32 + 16, r);
					grd.addColorStop(0, "rgba(255,255,255,1)");
					grd.addColorStop(1, "rgba(255,255,255,0)");
					mCtx.fillStyle = grd;
					mCtx.beginPath();
					mCtx.arc(x * 32 + 16, y * 32 + 16, r, 0, Math.PI * 2);
					mCtx.fill();
				};
				drawHalo(hx, hy, 75);
				drawHalo(enemy.x, enemy.y, 65);
				if (next) drawHalo(next.x, next.y, 65);
				mCtx.globalCompositeOperation = 'source-over';
				const drawLockOn = (x, y, color) => {
					const cx = x * 32 + 16,
						cy = y * 32 + 16;
					mCtx.save();
					mCtx.translate(cx, cy);
					mCtx.rotate(lockFrame * 0.15);
					mCtx.strokeStyle = color;
					mCtx.lineWidth = 1.5;
					mCtx.beginPath();
					mCtx.arc(0, 0, 20, 0, Math.PI * 2);
					mCtx.stroke();
					for (let i = 0; i < 4; i++) {
						mCtx.rotate(Math.PI / 2);
						mCtx.beginPath();
						mCtx.moveTo(0, 15);
						mCtx.lineTo(0, 25);
						mCtx.stroke();
					}
					mCtx.restore();
				};
				drawLockOn(enemy.x, enemy.y, 'rgba(255, 50, 50, 0.8)');
				if (next) drawLockOn(next.x, next.y, 'rgba(0, 255, 255, 0.8)');
			};

			const lockTimer = setInterval(drawSpotlights, 16);
			setTimeout(() => { if (mask.canvas) mask.canvas.style.opacity = 1; }, 10);

			setTimeout(() => {
				clearInterval(lockTimer);
				core.playSound('attack.mp3');

				const gameDom = document.getElementById('gameCanvas');
				if (gameDom) {
					gameDom.style.filter = "contrast(1.8) brightness(1.2)";
					gameDom.style.transform = `scale(1.03) translate(${scan.x * -8}px, ${scan.y * -8}px)`;
					setTimeout(() => {
						if (gameDom) {
							gameDom.style.transform = "";
							gameDom.style.filter = "";
						}
					}, 200);
				}

				const recoil = new Sprite(hx * 32 - 32, hy * 32 - 32, 96, 96, 135, 'game');
				let rf = 0;
				const rt = setInterval(() => {
					rf++;
					if (!recoil.context || rf >= 10) { clearInterval(rt); return recoil.destroy(); }
					core.clearMap(recoil.context);
					recoil.context.strokeStyle = `rgba(0, 255, 255, ${1 - rf / 10})`;
					recoil.context.lineWidth = 4;
					recoil.context.beginPath();
					recoil.context.ellipse(48, 48, rf * 5, rf * 3, 0, 0, Math.PI * 2);
					recoil.context.stroke();
				}, 16);

				const beam = new Sprite(0, 0, core.__PIXELS__, core.__PIXELS__, 160, 'game');
				let beamF = 0;
				const sx = hx * 32 + 16,
					sy = hy * 32 + 16;
				const ex = (hx + scan.x * 12) * 32 + 16,
					ey = (hy + scan.y * 12) * 32 + 16;
				const distance = Math.hypot(ex - sx, ey - sy);
				const angle = Math.atan2(ey - sy, ex - sx);
				const pAngle = angle + Math.PI / 2;

				// 在蓄力时就播放爆炸特效（数据已经变更完毕，这里只是视觉）
				// ★ 销毁遮挡Sprite，露出切割效果
				if (enemyCover) {
					enemyCover.destroy();
					enemyCover = null;
				}
				if (nextCover) {
					nextCover.destroy();
					nextCover = null;
				}
				createSliceExplosion(enemy.x, enemy.y, enemy.event.id, scan);
				if (next && next.event && next.event.id) {
					const isBlocked = next.event.cls && next.event.cls.startsWith("enemy") && core.material.enemys[next.event.id]?.notBomb;
					if (!isBlocked) createSliceExplosion(next.x, next.y, next.event.id, scan);
				}

				const beamTimer = setInterval(() => {
					beamF++;
					const bCtx = beam.context;
					if (!bCtx || beamF >= 20) {
						clearInterval(beamTimer);
						beam.destroy();
						if (mask.canvas) mask.canvas.style.opacity = 0;
						setTimeout(() => {
							mask.destroy();
							// ★ 【修复2】解锁后再插入待执行事件
							core.unlockControl();
							if (pendingActions.length > 0) core.insertAction(pendingActions);
						}, 200);
						return;
					}
					core.clearMap(bCtx);
					const op = Math.max(0, 1 - beamF / 20);
					bCtx.lineCap = 'round';
					bCtx.globalCompositeOperation = 'lighter';
					bCtx.strokeStyle = `rgba(255, 255, 255, ${op})`;
					bCtx.lineWidth = 3 + (op * 4);
					bCtx.beginPath();
					bCtx.moveTo(sx, sy);
					bCtx.lineTo(ex, ey);
					bCtx.stroke();
					const amplitude = 12 * op,
						frequency = 0.08,
						phaseShift = beamF * 1.5;
					bCtx.strokeStyle = `rgba(255, 50, 50, ${op * 0.9})`;
					bCtx.lineWidth = 2 + (op * 3);
					bCtx.beginPath();
					for (let d = 0; d <= distance; d += 4) {
						const offset = Math.sin(d * frequency - phaseShift) * amplitude;
						const px = sx + Math.cos(angle) * d + Math.cos(pAngle) * offset;
						const py = sy + Math.sin(angle) * d + Math.sin(pAngle) * offset;
						if (d === 0) bCtx.moveTo(px, py);
						else bCtx.lineTo(px, py);
					}
					bCtx.stroke();
					bCtx.strokeStyle = `rgba(50, 150, 255, ${op * 0.9})`;
					bCtx.lineWidth = 2 + (op * 3);
					bCtx.beginPath();
					for (let d = 0; d <= distance; d += 4) {
						const offset = Math.sin(d * frequency - phaseShift + Math.PI) * amplitude;
						const px = sx + Math.cos(angle) * d + Math.cos(pAngle) * offset;
						const py = sy + Math.sin(angle) * d + Math.sin(pAngle) * offset;
						if (d === 0) bCtx.moveTo(px, py);
						else bCtx.lineTo(px, py);
					}
					bCtx.stroke();
					bCtx.globalCompositeOperation = 'source-over';
				}, 16);
			}, 300);
		};

		this.getCountsLimit = function (skill) { return 2; };
	},
    "血量变化": function () {
		// --- ✨ 强化版血量动画插件 (防报错 + 全自动初始化) ✨ ---
		this.updateHpWithAnim = function (targetHp) {
			// 1. 自动初始化检查：如果变量不存在，立刻创建它
			if (!core.plugin.uiAnims) {
				core.plugin.uiAnims = {
					displayHp: core.getRealStatus('hp') || 0,
					hpColor: "#5A9E5A",
					hpScale: 1,
					animating: false
				};
			}

			const data = core.plugin.uiAnims;
			const oldHp = data.displayHp;

			// 如果数值没变，或者正在播动画，就不重复触发
			if (targetHp === oldHp || data.animating) return;

			const isDamage = targetHp < oldHp;

			// 2. 设置初始演出状态
			data.hpColor = isDamage ? "#D15B5B" : "#F1C40F";
			data.animating = true;

			const duration = 500;
			const startTime = performance.now();

			const step = (currentTime) => {
				const elapsed = currentTime - startTime;
				const progress = Math.min(elapsed / duration, 1);

				// 3. 数值弹性滚动曲线
				const easeOut = 1 - Math.pow(1 - progress, 3);
				data.displayHp = Math.floor(oldHp + (targetHp - oldHp) * easeOut);

				// 4. 形状变化
				if (isDamage) {
					// 掉血：颤抖
					if (progress < 0.8) {
						data.hpScale = 1 + Math.sin(progress * 40) * 0.12;
					} else {
						data.hpScale = 1;
					}
				} else {
					// 加血：果冻膨胀
					data.hpScale = 1 + (0.4 * (1 - easeOut));
				}

				// 强制触发重绘
				core.drawStatusBar();

				if (progress < 1) {
					requestAnimationFrame(step);
				} else {
					// 5. 演出彻底结束，恢复原状
					data.displayHp = targetHp;
					data.hpColor = "#5A9E5A";
					data.hpScale = 1;
					data.animating = false;
					core.drawStatusBar();
				}
			};
			requestAnimationFrame(step);
		};
	},
    "全屏": function () {
		// 横屏下，使标题画面可以覆盖状态栏 554*422
		// 竖屏下，标题画面依然和游戏画面大小相同 422*422
		// 可能存在bug。遇到bug的话，可以在技术群内，找“理派四阵”反馈

		//本插件需要开启“标题事件化”才能使用，且同时具有“标题”和“难度分歧”2个功能

		return;
		if (!core.flags.startUsingCanvas) return;

		var hard = ["简单"], //请按照“低-高”的顺序，填写各个难度的英文名；默认的flags.hard依次为1、2、3……以此类推
			hardColor = ["#3333DD"]; //和hard同样的顺序，填写各个难度的颜色
		//请将上述2个变量，按照自己的需求修改好

		//非常重要！！！
		//非常重要！！！
		//请完成下面1-3的步骤，不然本插件无法正常运行！

		//1、请在“脚本编辑”-“重置游戏”的末尾，加入下面这句话
		//if (document.getElementById("title")) document.getElementById("title").style.display = 'none';

		//2、请将“标题事件”和“难度分歧”全部清空
		//3、请在“开场剧情”的最开头，加上下面这段内容
		/*
		  {"type": "if", "condition": "(!core.isReplaying())",
		  "true": [
			{"type": "function", "function": "function(){core.plugin.title();}"},
			{"type": "setValue", "name": "flag:start", "value": "true"},
			{"type": "while", "condition": "flag:start",
			"data": [
			  {"type": "sleep", "time": 1, "noSkip": true},
			],
			},
		  ],
		  "false": [
			{"type": "function", "function": "function(){core.plugin.replayHardSelect(core.status.replay.toReplay[0], true);}"},
		  ]},
		 */

		//以下部分，需要自行编写具体内容

		this.drawTitle = function () { //绘制部分：需要横竖屏分别绘制
			var ctx = document.getElementById("title").getContext('2d');
			core.clearMap(ctx); //每次绘制之前，先清空画布

			if (core.domStyle.isVertical) { //竖屏和横屏需要分别绘制
				switch (core.getFlag("title", 0)) {
					case "start":
						core.drawImage(ctx, "start_vertical.png", 0, 0);
						break;
					case "hard":
						core.drawImage(ctx, "hard_vertical.png", 0, 0);
						break;
				}
			} else { //flags.title是预先内设的变量，为"start"表示当前处于开始界面，为"hard"表示当前处于难度选择界面，你也可以自行设置更多的
				switch (core.getFlag("title", 0)) {
					case "start":
						core.drawImage(ctx, "start.png", 0, 0);
						break;
					case "hard":
						core.drawImage(ctx, "hard.png", 0, 0);
						break;
				}
			}
		}

		this.performTitle = function (e) { //处理玩家的点击操作，比如点击开始游戏、点击难度选项等等
			var px = parseInt(e.offsetX / core.domStyle.scale),
				py = parseInt(e.offsetY / core.domStyle.scale);
			//你可以console.log(px, py)来查看点击的坐标

			//if (core.domStyle.isVertical) { //竖屏和横屏，需要分别处理
			//	
			//} else { //根据当前的页面，进行不同的处理
			switch (core.getFlag("title", 0)) {
				case "start":
					core.plugin.performTitle_startPage(px, py);
					break;
				case "hard":
					core.plugin.performTitle_hardSelect(px, py);
					break;
			}
			//}  ////这里横竖屏的坐标一样，就没有单独适配
		}

		this.performTitle_startPage = function (px, py) { //横屏下，开始页面的点击处理
			if (px >= 20 && px <= 180 && py >= 211 && py <= 253) core.setFlag("title", "hard"); //点击“开始游戏”,则进入“难度分歧”
			if (px >= 73 && px <= 235 && py >= 270 && py <= 314) core.plugin.openSL(); //点击“读取存档”
			if (px >= 140 && px <= 304 && py >= 331 && py <= 374) core.chooseReplayFile(); //点击“录像回放”
			//你还可以自行添加其他的功能
		}
		this.performTitle_hardSelect = function (px, py) { //横屏下，难度分歧页面的点击处理
			if (px >= 20 && px <= 100 && py >= 211 && py <= 253) core.plugin.selectHard(1);
			if (px >= 73 && px <= 153 && py >= 270 && py <= 314) core.plugin.selectHard(2);
			if (px >= 140 && px <= 222 && py >= 331 && py <= 374) core.plugin.selectHard(3); //这里的1、2、3，就是你希望的flags.hard取几。强烈建议按照1、2、3……的顺序，否则需要修改很多关联内容
			//你可以设置更多的难度
		}

		////之后的部分请勿修改
		////之后的部分请勿修改
		////之后的部分请勿修改

		this.title = function () { //主程序
			//resize后，数值初始化
			var width = parseFloat(document.getElementById('statusBar').style.width.slice(0, -2));
			if (document.getElementById("title")) { //重启游戏时
				document.getElementById("title").style.display = "block";
				core.setFlag("title", "start");
				core.plugin.drawTitle();
				return;
			}
			//第一次打开页面时
			var newCanvas = document.createElement("canvas");
			newCanvas.id = "title";
			newCanvas.style.display = 'block';
			newCanvas.style.left = '0px';
			newCanvas.style.top = '0px';
			newCanvas.style.left = '0px';
			newCanvas.style.top = '0px';
			newCanvas.style.zIndex = 210;
			newCanvas.style.position = 'absolute';
			if (core.domStyle.isVertical) {
				newCanvas.style.top = core.dom.gameDraw.style.top;
				newCanvas.width = newCanvas.height = 422;
				newCanvas.style.width = newCanvas.style.height = width + 'px';
				core.maps._setHDCanvasSize(newCanvas.getContext('2d'), 422, 422);
			} else {
				newCanvas.width = 554;
				newCanvas.height = 422;
				newCanvas.style.width = '554px';
				newCanvas.style.height = '422px';
				core.maps._setHDCanvasSize(newCanvas.getContext('2d'), 554, 422);
			}
			newCanvas.onclick = function (e) { //点击-处理-重绘，如此循环。和自绘ui一个原理
				e.stopPropagation();
				core.plugin.performTitle(e);
				core.plugin.drawTitle();
			}

			core.dom.gameGroup.appendChild(newCanvas); //注册该画布
			core.control.resize(); //调整分辨率

			core.setFlag("title", "start");
			core.plugin.drawTitle();
		}

		this.selectHard = function (num) { //选中了某难度时，num表示flags.hard应设为num
			core.setFlag("hard", num);
			core.status.hard = hard[num - 1];
			core.setFlag('__hardColor__', hardColor[num - 1]);
			core.status.route.push("hard:" + num);
			//隐藏标题画布，继续后面的“开场剧情”
			core.removeFlag("start");
			document.getElementById("title").style.display = "none";
		}

		this.openSL = function () {
			document.getElementById("title").style.display = "none";
			core.insertAction([
				{ "type": "callLoad" },
				{ "type": "function", "function": "function(){ if (core.status.event.id === 'action') {document.getElementById('title').style.display = 'block'} }" }
			]);
		}

		this.replayHardSelect = function (action, fromOpening) {
			if (action.indexOf("hard:") < 0) return false;
			if (!fromOpening) { //把难度选项移到了开场剧情中执行，实际回放时不执行以免录像重复
				setTimeout(core.replay, core.control.__replay_getTimeout());
				return true;
			}
			var num = parseInt(action.substr(5));

			if (!Number.isInteger(num)) return false;

			core.status.route.push(action);
			core.setFlag("hard", num);
			core.status.hard = hard[num - 1];
			core.setFlag('__hardColor__', hardColor[num - 1]);
			setTimeout(core.replay, core.control.__replay_getTimeout());
			return true;
		}

		core.control._resize_canvas = function (obj) {
			var innerSize = (obj.CANVAS_WIDTH * core.domStyle.scale) + "px";
			for (var i = 0; i < core.dom.gameCanvas.length; ++i)
				core.dom.gameCanvas[i].style.width = core.dom.gameCanvas[i].style.height = innerSize;
			core.dom.gif.style.width = core.dom.gif.style.height = innerSize;
			core.dom.gif2.style.width = core.dom.gif2.style.height = innerSize;
			core.dom.gameDraw.style.width = core.dom.gameDraw.style.height = innerSize;
			core.dom.gameDraw.style.top = obj.statusBarHeightInVertical + "px";
			core.dom.gameDraw.style.right = 0;
			core.dom.gameDraw.style.border = obj.border;
			// resize bigmap
			core.bigmap.canvas.forEach(function (cn) {
				var ratio = core.canvas[cn].canvas.hasAttribute('isHD') ? core.domStyle.ratio : 1;
				core.canvas[cn].canvas.style.width = core.canvas[cn].canvas.width / ratio * core.domStyle.scale + "px";
				core.canvas[cn].canvas.style.height = core.canvas[cn].canvas.height / ratio * core.domStyle.scale + "px";
			});
			// resize dynamic canvas
			for (var name in core.dymCanvas) {
				var ctx = core.dymCanvas[name],
					canvas = ctx.canvas;
				var ratio = canvas.hasAttribute('isHD') ? core.domStyle.ratio : 1;
				canvas.style.width = canvas.width / ratio * core.domStyle.scale + "px";
				canvas.style.height = canvas.height / ratio * core.domStyle.scale + "px";
				canvas.style.left = parseFloat(canvas.getAttribute("_left")) * core.domStyle.scale + "px";
				canvas.style.top = parseFloat(canvas.getAttribute("_top")) * core.domStyle.scale + "px";
			}
			// resize next
			main.dom.next.style.width = main.dom.next.style.height = 5 * core.domStyle.scale + "px";
			main.dom.next.style.borderBottomWidth = main.dom.next.style.borderRightWidth = 4 * core.domStyle.scale + "px";
			//resize title
			var title = document.getElementById('title');
			if (title) {
				if (core.domStyle.isVertical) {
					title.style.width = newCanvas.style.height = 422 * core.domStyle.scale + 'px';
				} else {
					title.style.width = 554 * core.domStyle.scale + 'px';
					title.style.height = 422 * core.domStyle.scale + 'px';
				}
			}
		}
		core.control.registerResize("canvas", core.control._resize_canvas);
		core.control.registerReplayAction("hard", core.plugin.replayHardSelect);
	},
    "模糊": function () {
		// 在此增加新插件
		// 基于canvas的sprite化，摘编整理自万宁魔塔
		// 
		// ----------------------------------- 第一部分 必装 js代码 ----------------------------------- //
		// 
		// 关于新增的on方法说明：
		// 该方法类似与样板的registerAction，它允许你能够使用与样板类似的api对sprite进行操作
		// on(type, handler)
		// 其中type为操作类型，常见的有click mousedown mouseup mousemove mouseenter mouseleave wheel keydown keyup touchstart touchend touchmove
		// 这些操作类型分为四类，第一类是鼠标事件，上述的操作中wheel之前的事件均为鼠标事件，它的监听形式为
		// sprite.on(type, (px, py) => { code });  其中px, py为参数，为点击的横纵坐标（相对sprite左上角）
		// 第二类是滚轮事件，只有一个事件wheel，它的监听形式为
		// sprite.on('wheel', (dy, dx, dz) => { code });  dy为滚轮的纵向滑动量，dx为滚轮的横向滑动量，dz为滚轮在垂直屏幕方向上的滑动量（？很奇怪，但是html确实提供了这个东西
		// 第三类是键盘事件，keydown和keyup都是键盘事件，它的监听形式为
		// sprite.on(type, (key, keyCode, altKey, ctrlKey, shiftKey) => { code });
		// 其中key为按键名称，可以通过console.log自行尝试，需要注意的是这个东西是区分大小写的，keyCode是与样板相同的keycode，可自行在网上查询
		// 后面三个都是布尔值（true or false）分别为alt键、ctrl键、shift键是否被按下
		// 第四类是触摸屏操作（注意click可能不会被触屏触发），包括touchstart touchend touchmove，有两种使用方法
		// 第一种：sprite.on(type, ([px, py]) => { code }); px py是点击的坐标，相对于sprite左上角
		// 第二种：sprite.on(type, (...locs) => { code }); locs是由[px, py]组成的数组，比如locs[0][0]就是第一个点击点的横坐标，locs[2][1]就是第三个点击点的纵坐标

		/* ---------------- 用法说明 ---------------- *
		 * 1. 创建sprite: const sprite = new Sprite(x, y, w, h, z, reference, name);
		 *   其中x y w h为画布的横纵坐标及长宽，reference为参考系，只能填game（相对于游戏画面）和window（相对于窗口）
		 *   且当为相对游戏画面时，长宽与坐标将会乘以放缩比例（相当于用createCanvas创建）
		 *   z为纵深，表示不同元素之间的覆盖关系，大的覆盖小的
		 *   name为自定义名称，可以不填
		 * 2. 删除: sprite.destroy();
		 * 3. 设置css特效: sprite.setCss(css);
		 *   其中css直接填 box-shadow: 0px 0px 10px black;的形式即可，与style标签与css文件内写法相同
		 *   对于已设置的特效，如果之后不需要再次设置，可以不填
		 * 4. 添加事件监听器: sprite.addEventListener(); 用法与html元素的addEventListener完全一致
		 * 5. 移除事件监听器: sprite.removeEventListener(); 用法与html元素的removeEventListener完全一致
		 * 6. 属性列表
		 *   (1) sprite.x | sprite.y | sprite.width | sprite.height | sprite.zIndex | sprite.reference 顾名思义
		 *   (2) sprite.canvas 该sprite的画布
		 *   (3) sprite.context 该画布的CanvasRenderingContext2d对象，即样板中常见的ctx
		 *   (4) sprite.count 不要改这个玩意
		 * 7. 使用样板api进行绘制
		 *   示例：
		 *   var ctx = sprite.context;
		 *   core.fillText(ctx, 'xxx', 100, 100);
		 *   core.fillRect(ctx, 0, 0, 50, 50);
		 *   当然也可以使用原生js
		 *   ctx.moveTo(0, 0);
		 *   ctx.bezierCurveTo(50, 50, 100, 0, 100, 50);
		 *   ctx.stroke();
		 * ---------------- 用法说明 ---------------- */

		const sprites = {};

		// 终于能用es6了（恼
		class Sprite {
			constructor(x, y, w, h, z, reference, name) {
				this.x = x;
				this.y = y;
				this.width = w;
				this.height = h;
				this.zIndex = z;
				this.reference = reference;
				/** @type {HTMLCanvasElement} */
				this.canvas = null;
				/** @type {CanvasRenderingContext2D} */
				this.context = null;
				this.count = 0;
				this.name = name;
				this.key = [];
				this.init();
			}

			init() {
				const name = this.name || `_sprite_${Sprite.count}`;
				this.name = name;
				if (this.reference === 'window') {
					const canvas = document.createElement('canvas');
					this.canvas = canvas;
					this.context = canvas.getContext('2d');
					canvas.width = this.width;
					canvas.height = this.height;
					canvas.style.width = this.width + 'px';
					canvas.style.height = this.height + 'px';
					canvas.style.position = 'absolute';
					canvas.style.top = this.y + 'px';
					canvas.style.left = this.x + 'px';
					canvas.style.zIndex = this.zIndex.toString();
					document.body.appendChild(canvas);
				} else {
					this.context = core.createCanvas(name, this.x, this.y, this.width, this.height, this.zIndex);
					this.canvas = this.context.canvas;
					this.count = Sprite.count;
					this.canvas.style.pointerEvents = 'auto';
				}
				Sprite.count++;
				sprites[this.name] = this;
			}

			setCss(css) {
				css = css.replace('\n', ';').replace(';;', ';');
				const effects = css.split(';');
				const canvas = this.canvas;
				effects.forEach(v => {
					const content = v.split(':');
					let name = content[0];
					let value = content[1];
					name = name.trim().split('-').reduce((pre, curr, i, a) => {
						if (i === 0 && curr !== '')
							return curr;
						if (a[0] === '' && i === 1)
							return curr;
						return pre + curr.toUpperCase()[0] + curr.slice(1);
					}, '');
					if (name in canvas.style)
						canvas.style[name] = value;
				});
				return this;
			}

			move(x, y, isDelta) {
				if (x !== undefined && x !== null)
					this.x = x;
				if (y !== undefined && y !== null)
					this.y = y;
				if (this.reference === 'window') {
					var ele = this.canvas;
					ele.style.left = x + (isDelta ? parseFloat(ele.style.left) : 0) + 'px';
					ele.style.top = y + (isDelta ? parseFloat(ele.style.top) : 0) + 'px';
				} else
					core.relocateCanvas(this.context, x, y, isDelta);
				return this;
			}

			resize(w, h, styleOnly) {
				if (w !== undefined && w !== null)
					this.width = w;
				if (h !== undefined && h !== null)
					this.height = h;
				if (this.reference === 'window') {
					const ele = this.canvas;
					ele.style.width = w + 'px';
					ele.style.height = h + 'px';
					if (!styleOnly) {
						ele.width = w;
						ele.height = h;
					}
				} else
					core.resizeCanvas(this.context, w, h, styleOnly);
				return this;
			}

			rotate(angle, cx, cy) {
				if (this.reference === 'window') {
					const left = this.x;
					const top = this.y;
					this.canvas.style.transformOrigin = (cx - left) + 'px ' + (cy - top) + 'px';
					if (angle === 0) {
						canvas.style.transform = '';
					} else {
						canvas.style.transform = 'rotate(' + angle + 'deg)';
					}
				} else {
					core.rotateCanvas(this.context, angle, cx, cy);
				}
				return this;
			}

			destroy() {
				if (this.reference === 'window') {
					if (this.canvas)
						document.body.removeChild(this.canvas);
				} else {
					core.deleteCanvas(this.name);
				}
				this.key.forEach(v => document.removeEventListener(v[0], v[1]));
				sprites[this.name] = void 0;
			}

			/**
			 * 类似样板registerAction接口，但是是以该sprite的左上角为(0,0)计算的
			 * @param {keyof HTMLElementEventMap} type 
			 * @param {(...param: any[]) => void} handler 
			 */
			on(type, handler) {
				if (this.reference !== 'game') throw new ReferenceError(`当sprite的reference为window时，不可使用该函数`);
				const mouse = [
					'auxclick', 'click', 'contextmenu', 'dblclick', 'mousedown', 'mouseup',
					'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover'
				];
				const key = [
					'keydown', 'keypress', 'keyup'
				];
				const touch = [
					'touchstart', 'touchend', 'touchcancel', 'touchmove'
				]
				if (mouse.includes(type)) {
					this.addEventListener(type, e => {
						const px = e.offsetX / core.domStyle.scale,
							py = e.offsetY / core.domStyle.scale;
						handler(px, py);
					})
				} else if (type === 'wheel') {
					this.addEventListener('wheel', e => {
						handler(e.deltaY, e.deltaX, e.deltaZ);
					});
				} else if (key.includes(type)) {
					// 键盘事件只能加到document上
					const listener = e => {
						handler(e.key, e.keyCode, e.altKey, e.ctrlKey, e.shiftKey);
					}
					this.key.push([type, listener]);
					document.addEventListener(type, listener)
				} else if (touch.includes(type)) {
					this.addEventListener(type, e => {
						/** @type {TouchList} */
						const touches = e.touches;
						const locs = [];
						for (let i = 0; i < touches.length; i++) {
							const t = touches[i];
							const { x, y } = core.actions._getClickLoc(t.clientX, t.clientY);
							const px = x / core.domStyle.scale,
								py = y / core.domStyle.scale;
							locs.push([px, py]);
						}
						handler(...locs);
					})
				}
			}

			addEventListener() {
				this.canvas.addEventListener.apply(this.canvas, arguments);
			}

			removeEventListener() {
				this.canvas.removeEventListener.apply(this.canvas, arguments);
			}
		}

		this.getSprite = function (name) {
			const s = sprites[name];
			if (!s) throw new ReferenceError(`不能获得不存在的sprite`);
			return sprites[name];
		}

		Sprite.count = 0;

		window.Sprite = Sprite;


	},
    "战斗美化": function () {
		// ==================== 战斗拾取特效 v5 ====================
		if (main.replayChecking) return;

		// 统一开关
		this.toggleItemFx = function () {
			var isOff = core.getFlag('itemFxOff', false);
			core.setFlag('itemFxOff', !isOff);
			core.drawTip('战斗拾取特效：' + (isOff ? '已开启' : '已关闭'));
		};

		var isFxOff = function () { return core.getFlag('itemFxOff', false); };

		// 提示条保持原版，不做美化

		// ==================== 2. 拾取道具飞行特效（rAF流畅版） ====================
		var _origGetItem = events.prototype.getItem;
		events.prototype.getItem = function (id, num, x, y, isGentleClick, callback) {
			if (!core.isReplaying() && x != null && y != null && !isFxOff()) {
				try { createItemFlyFx(id, x, y); } catch (e) { }
			}
			_origGetItem.apply(this, arguments);
		};

		var createItemFlyFx = function (itemId, x, y) {
			var block = core.getBlock(x, y);
			if (!block || !block.event) return;
			var startX = x * 32,
				startY = y * 32;

			var flyItem = new Sprite(startX, startY, 32, 32, 200, 'game');
			core.drawIcon(flyItem.context, block.event.id, 0, 0);

			var flash = new Sprite(startX - 8, startY - 8, 48, 48, 195, 'game');
			var flashF = 0;
			var endX = -40,
				endY = -20;
			var totalFrames = 18,
				f = 0;

			var loop = function () {
				f++;
				if (flashF <= 6) {
					flashF++;
					var fctx = flash.context;
					if (fctx) {
						core.clearMap(fctx);
						var op = 1 - flashF / 6;
						fctx.fillStyle = 'rgba(255,255,200,' + op + ')';
						fctx.beginPath();
						fctx.arc(24, 24, 16 - flashF, 0, Math.PI * 2);
						fctx.fill();
					}
				} else if (flash.canvas) { flash.destroy(); }

				if (!flyItem.canvas || f > totalFrames) {
					if (flyItem.canvas) flyItem.destroy();
					if (flash.canvas) flash.destroy();
					return;
				}
				var progress = f / totalFrames;
				var curX = startX + (endX - startX) * progress;
				var curY = startY + (endY - startY) * progress - Math.sin(progress * Math.PI) * 50;
				flyItem.move(curX, curY);
				flyItem.canvas.style.transform = 'scale(' + (1 - progress * 0.6) + ') rotate(' + (progress * 300) + 'deg)';
				flyItem.canvas.style.opacity = 1 - progress * 0.5;
				requestAnimationFrame(loop);
			};
			requestAnimationFrame(loop);
		};

		// ==================== 3. 普通战斗分裂特效（完整版，setInterval） ====================
		var _origAfterBattle = events.prototype.afterBattle;
		events.prototype.afterBattle = function (enemyId, x, y) {
			// ★ 在removeBlock之前先保存数据
			var savedBlockId = null;
			var savedDir = null;
			if (!core.isReplaying() && x != null && y != null && !isFxOff()) {
				var block = core.getBlock(x, y);
				if (block && block.event) {
					savedBlockId = block.event.id;
					savedDir = core.status.hero.loc.direction;
				}
			}

			var result = _origAfterBattle.apply(this, arguments);

			// ★ removeBlock之后播放动画
			if (savedBlockId) {
				try { createBattleSliceFx(savedBlockId, x, y, savedDir); } catch (e) { }
			}
			return result;
		};

		var createBattleSliceFx = function (blockId, x, y, dir) {
			var px = x * 32,
				py = y * 32;
			var isHorizontal = (dir === 'left' || dir === 'right');

			setTimeout(function () {
				// 两半Sprite
				var half1 = new Sprite(px, py, 32, 32, 173, 'game');
				var h1ctx = half1.context;
				core.drawIcon(h1ctx, blockId, 0, 0);
				if (isHorizontal) { h1ctx.clearRect(0, 16, 32, 16); } else { h1ctx.clearRect(16, 0, 16, 32); }

				var half2 = new Sprite(px, py, 32, 32, 173, 'game');
				var h2ctx = half2.context;
				core.drawIcon(h2ctx, blockId, 0, 0);
				if (isHorizontal) { h2ctx.clearRect(0, 0, 32, 16); } else { h2ctx.clearRect(0, 0, 16, 32); }

				// 切割闪光
				var slashFx = new Sprite(px - 4, py - 4, 40, 40, 180, 'game');
				var slashF = 0;
				var slashTimer = setInterval(function () {
					slashF++;
					var sctx = slashFx.context;
					if (!sctx || slashF > 6) {
						clearInterval(slashTimer);
						slashFx.destroy();
						return;
					}
					core.clearMap(sctx);
					var sop = 1 - slashF / 6;
					sctx.strokeStyle = 'rgba(255,255,255,' + sop + ')';
					sctx.lineWidth = 3 - slashF * 0.3;
					sctx.shadowBlur = 10;
					sctx.shadowColor = '#FFFFFF';
					sctx.beginPath();
					if (isHorizontal) {
						sctx.moveTo(2, 20);
						sctx.lineTo(38, 20);
					} else {
						sctx.moveTo(20, 2);
						sctx.lineTo(20, 38);
					}
					sctx.stroke();
					sctx.shadowBlur = 0;
				}, 16);

				// 分裂滑出
				var slideF = 0;
				var totalFrames = 20;
				var slideTimer = setInterval(function () {
					slideF++;
					if (slideF > totalFrames) {
						clearInterval(slideTimer);
						half1.destroy();
						half2.destroy();
						createMiniParticleBurst(x, y);
						return;
					}
					var progress = slideF / totalFrames;
					var ease = 1 - Math.pow(1 - progress, 2);
					var mainDist = ease * 18;
					var sideDist = ease * 2;
					var fadeOp = Math.max(0, 1 - progress * 1.1);
					var rotAngle = progress * 6;

					if (half1.canvas) {
						half1.canvas.style.opacity = fadeOp;
						if (isHorizontal) { half1.move(px - sideDist, py - mainDist); } else { half1.move(px - mainDist, py - sideDist); }
						half1.canvas.style.transform = 'rotate(' + (-rotAngle) + 'deg)';
					}
					if (half2.canvas) {
						half2.canvas.style.opacity = fadeOp;
						if (isHorizontal) { half2.move(px + sideDist, py + mainDist); } else { half2.move(px + mainDist, py + sideDist); }
						half2.canvas.style.transform = 'rotate(' + rotAngle + 'deg)';
					}
				}, 16);
			}, 30);
		};

		// 粒子爆发
		var createMiniParticleBurst = function (x, y) {
			var burstCanvas = new Sprite(x * 32 - 24, y * 32 - 24, 80, 80, 172, 'game');
			var ctx = burstCanvas.context;
			var ps = [];
			for (var i = 0; i < 15; i++) {
				var ang = Math.random() * Math.PI * 2;
				var spd = 1.5 + Math.random() * 2.5;
				ps.push({ x: 40, y: 40, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, r: 1.5 + Math.random() * 2, hue: i * (360 / 15), life: 1.0 });
			}
			var f = 0;
			var timer = setInterval(function () {
				f++;
				if (!ctx || f >= 30) {
					clearInterval(timer);
					burstCanvas.destroy();
					return;
				}
				core.clearMap(ctx);
				var op = Math.max(0, 1 - f / 30);
				for (var i = 0; i < ps.length; i++) {
					var p = ps[i];
					if (p.life <= 0) continue;
					p.x += p.vx;
					p.y += p.vy;
					p.vx *= 0.95;
					p.vy *= 0.95;
					p.vy += 0.1;
					p.life -= 0.03;
					ctx.globalAlpha = p.life * op;
					ctx.fillStyle = 'hsl(' + p.hue + ',90%,65%)';
					ctx.beginPath();
					ctx.arc(p.x, p.y, p.r * p.life, 0, Math.PI * 2);
					ctx.fill();
				}
				ctx.globalAlpha = 1;
			}, 16);
		};
	},
    "状态栏美化": function () {
		if (main.replayChecking) return;

		this.toggleStatusBar = function () {
			var cur = core.getFlag('__customStatusBar__', true);
			core.setFlag('__customStatusBar__', !cur);
			core.drawTip(cur ? '已切换为原始状态栏' : '已切换为玄幻状态栏');
			core.updateStatusBar();
		};

		var _originalDraw = core.ui.uidata.drawStatusBar;
		var fC = '"楷体",STKaiti,"Microsoft YaHei",sans-serif',
			fN = "'Georgia','Nunito',serif";

		var BG1 = '#180828',
			BG2 = '#0c0418';
		var BORDER = '#5a2a9a';
		var LBL = 'rgba(200,170,255,0.55)';
		var NC = {
			hp: '#ff9090',
			atk: '#ff7070',
			def: '#70b0ff',
			mdef: '#80ffb0',
			money: '#ffd060',
			yKey: '#e0c030',
			bKey: '#3080e0',
			rKey: '#d03030'
		};

		var rr = function (c, x, y, w, h, r) {
			c.beginPath();
			c.moveTo(x + r, y);
			c.lineTo(x + w - r, y);
			c.arcTo(x + w, y, x + w, y + r, r);
			c.lineTo(x + w, y + h - r);
			c.arcTo(x + w, y + h, x + w - r, y + h, r);
			c.lineTo(x + r, y + h);
			c.arcTo(x, y + h, x, y + h - r, r);
			c.lineTo(x, y + r);
			c.arcTo(x, y, x + r, y, r);
			c.closePath();
		};
		var fRR = function (c, x, y, w, h, r, co) {
			rr(c, x, y, w, h, r);
			c.fillStyle = co;
			c.fill();
		};
		var sRR = function (c, x, y, w, h, r, co, lw) {
			rr(c, x, y, w, h, r);
			c.strokeStyle = co;
			c.lineWidth = lw || 1;
			c.stroke();
		};

		var dI = function (ctx, id, x, y, sz) {
			try {
				var idx = core.material.icons.items[id];
				if (idx == null) return;
				var img = core.material.images.items;
				if (img) ctx.drawImage(img, 0, idx * 32, 32, 32, x, y, sz, sz);
			} catch (e) { }
		};

		var drawBg = function (ctx, x, y, w, h) {
			var bg = ctx.createLinearGradient(x, y, x, y + h);
			bg.addColorStop(0, BG1);
			bg.addColorStop(1, BG2);
			fRR(ctx, x, y, w, h, 8, bg);
			sRR(ctx, x, y, w, h, 8, BORDER, 1);
		};

		var drawTalentBig = function (ctx, x, y, w, h) {
			fRR(ctx, x, y, w, h, 7, 'rgba(120,60,200,0.18)');
			sRR(ctx, x, y, w, h, 7, 'rgba(160,80,255,0.4)', 1);
			var cx = x + w / 2,
				cy = y + h / 2 - 6,
				R = 20;
			ctx.save();
			ctx.strokeStyle = 'rgba(180,100,255,0.45)';
			ctx.lineWidth = 0.8;
			ctx.beginPath();
			for (var i = 0; i < 6; i++) {
				var a = i * Math.PI / 3 - Math.PI / 2;
				i === 0 ? ctx.moveTo(cx + R * Math.cos(a), cy + R * Math.sin(a)) : ctx.lineTo(cx + R * Math.cos(a), cy + R * Math.sin(a));
			}
			ctx.closePath();
			ctx.stroke();
			ctx.strokeStyle = 'rgba(180,100,255,0.3)';
			ctx.lineWidth = 0.5;
			ctx.beginPath();
			for (var i = 0; i < 6; i++) {
				var a = i * Math.PI / 3 - Math.PI / 2;
				i === 0 ? ctx.moveTo(cx + R * 0.6 * Math.cos(a), cy + R * 0.6 * Math.sin(a)) : ctx.lineTo(cx + R * 0.6 * Math.cos(a), cy + R * 0.6 * Math.sin(a));
			}
			ctx.closePath();
			ctx.stroke();
			ctx.strokeStyle = 'rgba(180,100,255,0.25)';
			ctx.lineWidth = 0.4;
			for (var i = 0; i < 3; i++) {
				var a1 = i * Math.PI / 3 - Math.PI / 2,
					a2 = a1 + Math.PI;
				ctx.beginPath();
				ctx.moveTo(cx + R * Math.cos(a1), cy + R * Math.sin(a1));
				ctx.lineTo(cx + R * Math.cos(a2), cy + R * Math.sin(a2));
				ctx.stroke();
			}
			ctx.beginPath();
			ctx.arc(cx, cy, 6, 0, Math.PI * 2);
			ctx.fillStyle = 'rgba(180,100,255,0.3)';
			ctx.fill();
			ctx.strokeStyle = 'rgba(200,130,255,0.6)';
			ctx.lineWidth = 1;
			ctx.stroke();
			ctx.restore();
			ctx.textAlign = 'center';
			ctx.font = '700 9px ' + fC;
			ctx.fillStyle = 'rgba(200,160,255,0.85)';
			ctx.fillText('商  店', cx, y + h - 7);
		};

		var drawTalentSmall = function (ctx, x, y, w, h) {
			fRR(ctx, x, y, w, h, 6, 'rgba(120,60,200,0.18)');
			sRR(ctx, x, y, w, h, 6, 'rgba(160,80,255,0.4)', 1);
			var iconX = x + 14,
				iconY = y + h / 2,
				R = 10;
			ctx.save();
			ctx.strokeStyle = 'rgba(180,100,255,0.5)';
			ctx.lineWidth = 0.8;
			ctx.beginPath();
			for (var i = 0; i < 6; i++) {
				var a = i * Math.PI / 3 - Math.PI / 2;
				i === 0 ? ctx.moveTo(iconX + R * Math.cos(a), iconY + R * Math.sin(a)) : ctx.lineTo(iconX + R * Math.cos(a), iconY + R * Math.sin(a));
			}
			ctx.closePath();
			ctx.stroke();
			ctx.beginPath();
			ctx.arc(iconX, iconY, 3, 0, Math.PI * 2);
			ctx.fillStyle = 'rgba(180,100,255,0.4)';
			ctx.fill();
			ctx.restore();
			ctx.textAlign = 'left';
			ctx.font = '700 8px ' + fC;
			ctx.fillStyle = 'rgba(200,160,255,0.9)';
			ctx.fillText('商店', x + 28, y + h / 2 - 1);
			ctx.font = '700 7px ' + fC;
			ctx.fillStyle = 'rgba(160,100,220,0.45)';
			ctx.fillText('点击展开', x + 28, y + h / 2 + 9);
		};

		var drawSkillSlots = function (ctx, x, y, totalW, totalH, cols) {
			cols = cols || 2;
			var bridgeCount = 0,
				ladderCount = 0;
			try { bridgeCount = core.itemCount('pickaxe') || 0; } catch (e) { }
			try { ladderCount = core.itemCount('icePickaxe') || 0; } catch (e) { }
			var slots = [
				{ label: '造桥', val: String(bridgeCount), color: bridgeCount > 0 ? '#c49a6c' : 'rgba(180,140,100,0.3)' },
				{ label: '造梯', val: String(ladderCount), color: ladderCount > 0 ? '#80c8ff' : 'rgba(100,160,200,0.3)' },
			];
			var slotCount = 2;
			var slotW = (totalW - 3) / 2;
			var slotH = cols === 2 ? (totalH - 3) / 2 : totalH;
			for (var i = 0; i < slotCount; i++) {
				var sx, sy;
				if (cols === 2) {
					sx = x + (i % 2) * (slotW + 3);
					sy = y + Math.floor(i / 2) * (slotH + 3);
				} else {
					slotW = (totalW - 3) / 2;
					slotH = totalH;
					sx = x + i * (slotW + 3);
					sy = y;
				}
				fRR(ctx, sx, sy, slotW, slotH, 5, 'rgba(255,255,255,0.04)');
				sRR(ctx, sx, sy, slotW, slotH, 5, 'rgba(255,255,255,0.09)', 0.8);
				var slot = slots[i];
				ctx.textAlign = 'center';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = slot.color;
				ctx.fillText(slot.label, sx + slotW / 2, sy + slotH * 0.42);
				ctx.font = '700 9px ' + fC;
				ctx.fillText(slot.val, sx + slotW / 2, sy + slotH * 0.78);
			}
		};

		// ====== 悬浮信息 ======
		var hoverInfo = null;
		var _getHover = function (mx, my) {
			var b = core.getBlock(mx, my);
			if (b && b.event) {
				var cls = b.event.cls || '';
				if (cls.indexOf('enemy') >= 0) return { type: 'enemy', id: b.event.id, x: mx, y: my };
				return { type: 'tile', id: b.event.id, x: mx, y: my, cls: cls };
			}
			return { type: 'ground', id: null, x: mx, y: my };
		};
		var _hoverEq = function (a, b) {
			if (!a && !b) return true;
			if (!a || !b) return false;
			return a.type === b.type && a.id === b.id && a.x === b.x && a.y === b.y;
		};

		setTimeout(function () {
			core.registerAction('onmove', 'sb_h', function (x, y, px, py) {
				if (core.status.lockControl) return false;
				var mx = parseInt((px + core.bigmap.offsetX) / 32),
					my = parseInt((py + core.bigmap.offsetY) / 32);
				var nw = _getHover(mx, my);
				if (!_hoverEq(hoverInfo, nw)) {
					hoverInfo = nw;
					core.updateStatusBar();
				}
				return false;
			}, 50);
			core.registerAction('ondown', 'sb_vc', function (x, y, px, py) {
				if (core.status.lockControl) return false;
				var mx = parseInt((px + core.bigmap.offsetX) / 32),
					my = parseInt((py + core.bigmap.offsetY) / 32);
				hoverInfo = _getHover(mx, my);
				core.updateStatusBar();
				return false;
			}, 45);

			// 移动后刷新状态栏
			core.registerAction('afterMove', 'sb_battleBtn', function () {
				try { core.updateStatusBar(); } catch (e) { }
				return false;
			}, 50);

			var _origSetHeroLoc = core.control.setHeroLoc;
			core.control.setHeroLoc = function (x, y, direction, floorId) {
				var r = _origSetHeroLoc.apply(this, arguments);
				try { core.updateStatusBar(); } catch (e) { }
				return r;
			};

			// 点击状态栏商店区域打开商店
			core.dom.statusCanvas.addEventListener('click', function (e) {
				if (!core.isPlaying || !core.isPlaying()) return;
				if (core.status.lockControl) return;
				var rect = core.dom.statusCanvas.getBoundingClientRect();
				var scaleX = core.dom.statusCanvas.width / rect.width;
				var scaleY = core.dom.statusCanvas.height / rect.height;
				var px = (e.clientX - rect.left) * scaleX;
				var py = (e.clientY - rect.top) * scaleY;
				if (!core.domStyle.isVertical) {
					// 横屏：商店区域
					var W = 129,
						p = 5,
						cW = W - p * 2;
					// 商店图标在Y=152~220附近（键之后+分隔线+天赋区域）
					var shopY1 = 390,
						shopY2 = 490;
					if (px >= p && px <= p + cW && py >= shopY1 && py <= shopY2) {
						try { core.openShop('shop1', false); } catch (e) { }
					}
				} else {
					// 竖屏：天赋小图标区域
					var p = 5,
						rH = 34,
						tp = 4,
						r1y = tp + 11;
					var y2 = r1y + rH + 4;
					var kBlockW = 28;
					var talentX = p + 3 * (kBlockW + 2) + 4;
					var talentW = 76;
					if (px >= 200 && px <= 330 && py >= 100 && py <= 190) {
						try { core.openShop('shop1', false); } catch (e) { }
					}
				}
			});
		}, 300);

		// ====== 主绘制函数 ======
		core.ui.uidata.drawStatusBar = function () {
			if (!core.getFlag('__customStatusBar__', true)) {
				if (_originalDraw) return _originalDraw.call(this);
				return;
			}
			if (!core.flags.statusCanvas) return;
			var ctx = core.dom.statusCanvasCtx;
			core.clearMap(ctx);
			if (!core.domStyle.showStatusBar) return;

			var hero = core.status.hero || {},
				rHp = core.getRealStatus('hp') || 0,
				atk = core.getRealStatus('atk') || 0,
				def = core.getRealStatus('def') || 0,
				mdef = core.getRealStatus('mdef') || 0,
				money = hero.money || 0;
			var mn = (core.status && core.status.thisMap) ? (core.status.thisMap.name || '?') : '...';

			if (!core.domStyle.isVertical) {
				var W = 129,
					H = 457,
					p = 5,
					cW = W - p * 2;
				drawBg(ctx, 0, 0, W, H);
				var Y = 6;

				ctx.textAlign = 'center';
				ctx.font = '700 8px ' + fC;
				ctx.fillStyle = 'rgba(200,160,255,0.35)';
				ctx.fillText('* M O T A *', W / 2, Y + 8);
				Y += 14;
				ctx.font = '900 14px ' + fC;
				ctx.fillStyle = '#e0c8ff';
				ctx.fillText(mn, W / 2, Y + 12);
				Y += 18;

				fRR(ctx, p, Y, cW, 46, 6, 'rgba(200,50,50,0.15)');
				sRR(ctx, p, Y, cW, 46, 6, 'rgba(200,80,80,0.3)', 1);
				ctx.textAlign = 'left';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = '#e08080';
				ctx.fillText('♥ 生命', p + 5, Y + 11);
				ctx.textAlign = 'right';
				ctx.font = '900 18px ' + fN;
				ctx.fillStyle = NC.hp;
				ctx.fillText(rHp, p + cW - 5, Y + 28);
				var bY = Y + 34,
					bW = cW - 12;
				fRR(ctx, p + 6, bY, bW, 5, 2, 'rgba(255,255,255,0.08)');
				var hr = Math.min(rHp / 1000, 1);
				if (hr > 0) {
					var hg = ctx.createLinearGradient(p + 6, 0, p + 6 + bW * hr, 0);
					hg.addColorStop(0, '#c03030');
					hg.addColorStop(1, '#ff7070');
					fRR(ctx, p + 6, bY, Math.max(bW * hr, 6), 5, 2, hg);
				}
				Y += 52;

				var s4 = [
					{ l: '攻击', v: atk, c: NC.atk },
					{ l: '防御', v: def, c: NC.def },
					{ l: '护盾', v: mdef, c: NC.mdef },
					{ l: '金币', v: money, c: NC.money }
				];
				var hW = (cW - 4) / 2;
				for (var i = 0; i < 4; i++) {
					var col = i % 2,
						row = Math.floor(i / 2);
					var bx = p + col * (hW + 4),
						by = Y + row * 36;
					fRR(ctx, bx, by, hW, 33, 6, 'rgba(255,255,255,0.06)');
					sRR(ctx, bx, by, hW, 33, 6, 'rgba(255,255,255,0.1)', 0.8);
					ctx.textAlign = 'left';
					ctx.font = '700 7px ' + fC;
					ctx.fillStyle = LBL;
					ctx.fillText(s4[i].l, bx + 5, by + 12);
					ctx.textAlign = 'center';
					ctx.font = '900 15px ' + fN;
					ctx.fillStyle = s4[i].c;
					ctx.fillText(s4[i].v, bx + hW / 2, by + 27);
				}
				Y += 76;

				var kW = cW / 3;
				var keys = [
					{ id: 'yellowKey', nc: NC.yKey },
					{ id: 'blueKey', nc: NC.bKey },
					{ id: 'redKey', nc: NC.rKey }
				];
				for (var k = 0; k < 3; k++) {
					var kx = p + k * kW;
					fRR(ctx, kx + 1, Y, kW - 2, 30, 4, 'rgba(255,255,255,0.05)');
					sRR(ctx, kx + 1, Y, kW - 2, 30, 4, 'rgba(255,255,255,0.09)', 0.8);
					dI(ctx, keys[k].id, kx + (kW - 18) / 2, Y + 2, 18);
					ctx.textAlign = 'center';
					ctx.font = '900 11px ' + fN;
					ctx.fillStyle = keys[k].nc;
					ctx.fillText(core.itemCount(keys[k].id), kx + kW / 2, Y + 27);
				}
				Y += 36;

				ctx.save();
				ctx.setLineDash([3, 3]);
				ctx.strokeStyle = 'rgba(180,100,255,0.2)';
				ctx.lineWidth = 0.8;
				ctx.beginPath();
				ctx.moveTo(p + 6, Y + 3);
				ctx.lineTo(p + cW - 6, Y + 3);
				ctx.stroke();
				ctx.restore();
				Y += 10;

				var talentH = 68;
				drawTalentBig(ctx, p, Y, cW, talentH);
				Y += talentH + 5;

				ctx.save();
				ctx.setLineDash([3, 3]);
				ctx.strokeStyle = 'rgba(180,100,255,0.2)';
				ctx.lineWidth = 0.8;
				ctx.beginPath();
				ctx.moveTo(p + 6, Y);
				ctx.lineTo(p + cW - 6, Y);
				ctx.stroke();
				ctx.restore();
				Y += 6;

				ctx.textAlign = 'center';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = 'rgba(200,160,255,0.28)';
				ctx.fillText('◈  技  能  ◈', W / 2, Y + 7);
				Y += 12;

				var skH = H - Y - p;
				drawSkillSlots(ctx, p, Y, cW, skH, 2);

			} else {
				var W = 416,
					H = 105,
					p = 5;
				drawBg(ctx, 0, 0, W * 2, H * 2);
				var rH = 34,
					tp = 4;

				ctx.textAlign = 'center';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = 'rgba(200,160,255,0.3)';
				ctx.fillText('* M O T A *', W / 2, tp + 8);

				var r1y = tp + 11;
				fRR(ctx, p, r1y, 50, rH, 6, 'rgba(180,100,255,0.12)');
				sRR(ctx, p, r1y, 50, rH, 6, 'rgba(180,100,255,0.3)', 1);
				ctx.textAlign = 'center';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = '#a080d0';
				ctx.fillText('当前层', p + 25, r1y + 11);
				ctx.font = '900 13px ' + fC;
				ctx.fillStyle = '#e0c8ff';
				ctx.fillText(mn, p + 25, r1y + 27);

				fRR(ctx, p + 54, r1y, 82, rH, 6, 'rgba(200,50,50,0.12)');
				sRR(ctx, p + 54, r1y, 82, rH, 6, 'rgba(200,80,80,0.25)', 1);
				ctx.textAlign = 'left';
				ctx.font = '700 7px ' + fC;
				ctx.fillStyle = '#e08080';
				ctx.fillText('♥ 生命', p + 58, r1y + 11);
				ctx.textAlign = 'right';
				ctx.font = '900 15px ' + fN;
				ctx.fillStyle = NC.hp;
				ctx.fillText(rHp, p + 132, r1y + 24);
				fRR(ctx, p + 58, r1y + 26, 74, 4, 2, 'rgba(255,255,255,0.08)');
				var vr = Math.min(rHp / 1000, 1);
				if (vr > 0) {
					var vg = ctx.createLinearGradient(p + 58, 0, p + 58 + 74 * vr, 0);
					vg.addColorStop(0, '#c03030');
					vg.addColorStop(1, '#ff7070');
					fRR(ctx, p + 58, r1y + 26, Math.max(74 * vr, 5), 4, 2, vg);
				}

				var v4 = [
					{ l: '攻击', v: atk, c: NC.atk },
					{ l: '防御', v: def, c: NC.def },
					{ l: '护盾', v: mdef, c: NC.mdef },
					{ l: '金币', v: money, c: NC.money }
				];
				var statX = p + 140;
				var statW = (W - statX - p) / 4 - 2;
				for (var vi = 0; vi < 4; vi++) {
					var vx = statX + vi * (statW + 2);
					fRR(ctx, vx, r1y, statW, rH, 5, 'rgba(255,255,255,0.06)');
					sRR(ctx, vx, r1y, statW, rH, 5, 'rgba(255,255,255,0.1)', 0.8);
					ctx.textAlign = 'center';
					ctx.font = '700 7px ' + fC;
					ctx.fillStyle = LBL;
					ctx.fillText(v4[vi].l, vx + statW / 2, r1y + 11);
					ctx.font = '900 13px ' + fN;
					ctx.fillStyle = v4[vi].c;
					ctx.fillText(v4[vi].v, vx + statW / 2, r1y + 27);
				}

				var y2 = r1y + rH + 4;
				var rowH = H - y2 - tp;

				var ks2 = [
					{ id: 'yellowKey', nc: NC.yKey },
					{ id: 'blueKey', nc: NC.bKey },
					{ id: 'redKey', nc: NC.rKey }
				];
				var kBlockW = 28;
				for (var ki = 0; ki < 3; ki++) {
					var kbx = p + ki * (kBlockW + 2);
					fRR(ctx, kbx, y2, kBlockW, rowH, 5, 'rgba(255,255,255,0.05)');
					sRR(ctx, kbx, y2, kBlockW, rowH, 5, 'rgba(255,255,255,0.09)', 0.8);
					dI(ctx, ks2[ki].id, kbx + (kBlockW - 18) / 2, y2 + 2, 18);
					ctx.textAlign = 'center';
					ctx.font = '900 10px ' + fN;
					ctx.fillStyle = ks2[ki].nc;
					ctx.fillText(core.itemCount(ks2[ki].id), kbx + kBlockW / 2, y2 + rowH - 3);
				}

				var talentX = p + 3 * (kBlockW + 2) + 4;
				var talentW = 76;
				drawTalentSmall(ctx, talentX, y2, talentW, rowH);

				var skX = talentX + talentW + 4;
				var skW4 = W - skX - p;
				drawSkillSlots(ctx, skX, y2, skW4, rowH, 4);
			}
		};
	},
    "工具栏美化": function () {
		if (main.replayChecking) return;

		var fC = '"楷体", STKaiti, "Microsoft YaHei", sans-serif';

		var BG1 = '#180828',
			BG2 = '#0c0418';
		var BTN_BG = 'rgba(255,255,255,0.06)';
		var BTN_BD = 'rgba(180,100,255,0.2)';
		var HARD_BG = 'rgba(120,60,200,0.15)';
		var HARD_BD = 'rgba(180,100,255,0.35)';
		var TALENT_BG = 'rgba(120,60,200,0.18)';
		var TALENT_BD = 'rgba(160,80,255,0.4)';

		var btnInfo = {
			book: { name: '手册', c: '#ff8080' },
			fly: { name: '楼传', c: '#50d8d8' },
			toolbox: { name: '道具', c: '#e8c050' },
			keyboard: { name: '键盘', c: '#88aacc' },
			shop: { name: '商店', c: '#c090ff' },
			save: { name: '存档', c: '#e8c050' },
			load: { name: '读档', c: '#60a8e0' },
			settings: { name: '设置', c: '#a888d8' },
			rollback: { name: '回退', c: '#e06060' },
			undoRollback: { name: '前进', c: '#60c880' },
			equipbox: { name: '装备', c: '#c09050' },
			view: { name: '地图', c: '#80b880' },
			btn1: { name: '1', c: '#e08040' },
			btn2: { name: '2', c: '#3080c0' },
			btn3: { name: '3', c: '#40a060' },
			btn4: { name: '4', c: '#d04040' },
			btn5: { name: '5', c: '#b8860b' },
			btn6: { name: '6', c: '#8868b8' },
			btn7: { name: '7', c: '#2898a0' },
			btn8: { name: '8', c: '#a07030' },
			btn9: { name: '9', c: '#606080' },
			btnAlt: { name: 'ALT', c: '#c06020' },
			play: { name: '播放', c: '#40c060' },
			stop: { name: '停止', c: '#e04040' },
			rewind: { name: '回退', c: '#3080c0' },
			speedDown: { name: '减速', c: '#2898a0' },
			speedUp: { name: '加速', c: '#c06828' },
			single: { name: '单步', c: '#8868b8' },
		};

		var rr = function (c, x, y, w, h, r) {
			c.beginPath();
			c.moveTo(x + r, y);
			c.lineTo(x + w - r, y);
			c.arcTo(x + w, y, x + w, y + r, r);
			c.lineTo(x + w, y + h - r);
			c.arcTo(x + w, y + h, x + w - r, y + h, r);
			c.lineTo(x + r, y + h);
			c.arcTo(x, y + h, x, y + h - r, r);
			c.lineTo(x, y + r);
			c.arcTo(x, y, x + r, y, r);
			c.closePath();
		};
		var fillRR = function (c, x, y, w, h, r, co) {
			rr(c, x, y, w, h, r);
			c.fillStyle = co;
			c.fill();
		};
		var strokeRR = function (c, x, y, w, h, r, co, lw) {
			rr(c, x, y, w, h, r);
			c.strokeStyle = co;
			c.lineWidth = lw || 1;
			c.stroke();
		};

		var drawToolIcon = function (ctx, id, x, y, sz) {
			var cx = x + sz / 2,
				cy = y + sz / 2,
				r = sz / 2;
			switch (id) {
				case 'book':
					ctx.fillStyle = '#E84038';
					fillRR(ctx, x + r * 0.08, y + r * 0.05, sz * 0.84, sz * 0.9, r * 0.18);
					ctx.fillStyle = '#F06058';
					fillRR(ctx, x + r * 0.16, y + r * 0.12, sz * 0.68, sz * 0.35, r * 0.12);
					ctx.fillStyle = '#FFE040';
					ctx.font = '900 ' + Math.round(sz * 0.48) + 'px serif';
					ctx.textAlign = 'center';
					ctx.fillText('?', cx, cy + r * 0.35);
					break;
				case 'fly':
					// 楼传：多层横线+向上箭头
					ctx.strokeStyle = '#2898A0';
					ctx.lineWidth = Math.max(1.5, r * 0.1);
					for (var i = 0; i < 3; i++) {
						var ly = y + sz * (0.75 - i * 0.22);
						ctx.globalAlpha = 0.5 + i * 0.25;
						ctx.beginPath();
						ctx.moveTo(x + sz * 0.15, ly);
						ctx.lineTo(x + sz * 0.85, ly);
						ctx.stroke();
					}
					ctx.globalAlpha = 1;
					ctx.fillStyle = '#50D8D8';
					ctx.beginPath();
					ctx.moveTo(cx, y + sz * 0.08);
					ctx.lineTo(cx + sz * 0.22, y + sz * 0.32);
					ctx.lineTo(cx - sz * 0.22, y + sz * 0.32);
					ctx.closePath();
					ctx.fill();
					break;
				case 'toolbox':
					ctx.fillStyle = '#D4A030';
					fillRR(ctx, x + r * 0.1, cy - r * 0.15, sz * 0.8, sz * 0.58, r * 0.12);
					ctx.fillStyle = '#E8C050';
					fillRR(ctx, x + r * 0.1, y + r * 0.08, sz * 0.8, sz * 0.35, r * 0.12);
					ctx.fillStyle = '#C08020';
					ctx.beginPath();
					ctx.arc(cx, cy - r * 0.02, r * 0.16, 0, Math.PI * 2);
					ctx.fill();
					ctx.fillStyle = '#FFE080';
					ctx.beginPath();
					ctx.arc(cx, cy - r * 0.02, r * 0.09, 0, Math.PI * 2);
					ctx.fill();
					break;
				case 'keyboard':
					ctx.fillStyle = '#6880A0';
					fillRR(ctx, x + r * 0.05, y + r * 0.2, sz * 0.9, sz * 0.6, r * 0.1);
					ctx.fillStyle = '#FFF';
					for (var ro = 0; ro < 2; ro++)
						for (var co = 0; co < 3; co++) fillRR(ctx, x + r * 0.2 + co * r * 0.38, y + r * 0.3 + ro * r * 0.28, r * 0.28, r * 0.2, 2);
					break;
				case 'shop':
					// 天赋图：六芒星阵
					ctx.save();
					ctx.strokeStyle = '#c090ff';
					ctx.lineWidth = Math.max(1, r * 0.08);
					ctx.beginPath();
					for (var i = 0; i < 6; i++) {
						var a = i * Math.PI / 3 - Math.PI / 2;
						i === 0 ? ctx.moveTo(cx + r * 0.78 * Math.cos(a), cy + r * 0.78 * Math.sin(a)) :
							ctx.lineTo(cx + r * 0.78 * Math.cos(a), cy + r * 0.78 * Math.sin(a));
					}
					ctx.closePath();
					ctx.stroke();
					ctx.strokeStyle = 'rgba(180,100,255,0.4)';
					ctx.lineWidth = Math.max(0.8, r * 0.05);
					ctx.beginPath();
					for (var i = 0; i < 6; i++) {
						var a = i * Math.PI / 3 - Math.PI / 2;
						i === 0 ? ctx.moveTo(cx + r * 0.45 * Math.cos(a), cy + r * 0.45 * Math.sin(a)) :
							ctx.lineTo(cx + r * 0.45 * Math.cos(a), cy + r * 0.45 * Math.sin(a));
					}
					ctx.closePath();
					ctx.stroke();
					ctx.beginPath();
					ctx.arc(cx, cy, r * 0.18, 0, Math.PI * 2);
					ctx.fillStyle = 'rgba(180,100,255,0.5)';
					ctx.fill();
					ctx.restore();
					break;
				case 'save':
					ctx.fillStyle = '#D4A030';
					fillRR(ctx, x + r * 0.15, y + r * 0.05, sz * 0.7, sz * 0.9, r * 0.08);
					ctx.fillStyle = '#FFF';
					fillRR(ctx, x + r * 0.35, y + r * 0.12, sz * 0.35, sz * 0.28, r * 0.04);
					ctx.fillStyle = '#B08020';
					fillRR(ctx, x + r * 0.25, cy + r * 0.15, sz * 0.5, sz * 0.28, r * 0.04);
					break;
				case 'load':
					ctx.fillStyle = '#3080C0';
					fillRR(ctx, x + r * 0.1, cy - r * 0.15, sz * 0.8, sz * 0.58, r * 0.1);
					ctx.fillStyle = '#50A8E0';
					fillRR(ctx, x + r * 0.08, y + r * 0.1, sz * 0.45, sz * 0.3, r * 0.08);
					ctx.fillStyle = '#FFF';
					ctx.font = '900 ' + Math.round(sz * 0.3) + 'px serif';
					ctx.textAlign = 'center';
					ctx.fillText('\u2191', cx, cy + r * 0.3);
					break;
				case 'settings':
					ctx.fillStyle = '#8868B8';
					ctx.beginPath();
					ctx.arc(cx, cy, r * 0.6, 0, Math.PI * 2);
					ctx.fill();
					for (var g = 0; g < 6; g++) {
						var ga = g * Math.PI / 3;
						ctx.beginPath();
						ctx.arc(cx + Math.cos(ga) * r * 0.55, cy + Math.sin(ga) * r * 0.55, r * 0.18, 0, Math.PI * 2);
						ctx.fill();
					}
					ctx.fillStyle = '#E0D0F0';
					ctx.beginPath();
					ctx.arc(cx, cy, r * 0.28, 0, Math.PI * 2);
					ctx.fill();
					break;
				case 'rollback':
					ctx.fillStyle = '#D04040';
					ctx.beginPath();
					ctx.moveTo(cx - r * 0.7, cy);
					ctx.lineTo(cx, cy - r * 0.6);
					ctx.lineTo(cx, cy - r * 0.25);
					ctx.lineTo(cx + r * 0.45, cy - r * 0.25);
					ctx.quadraticCurveTo(cx + r * 0.7, cy - r * 0.25, cx + r * 0.7, cy + r * 0.05);
					ctx.quadraticCurveTo(cx + r * 0.7, cy + r * 0.5, cx + r * 0.2, cy + r * 0.6);
					ctx.lineTo(cx + r * 0.2, cy + r * 0.3);
					ctx.quadraticCurveTo(cx + r * 0.5, cy + r * 0.2, cx + r * 0.5, cy + r * 0.05);
					ctx.lineTo(cx, cy + r * 0.05);
					ctx.lineTo(cx, cy + r * 0.6);
					ctx.closePath();
					ctx.fill();
					break;
				case 'view':
					ctx.fillStyle = '#508050';
					fillRR(ctx, x + r * 0.1, y + r * 0.1, sz * 0.8, sz * 0.8, r * 0.12);
					ctx.strokeStyle = 'rgba(255,255,255,0.6)';
					ctx.lineWidth = Math.max(1, r * 0.07);
					for (var vi = 0; vi < 3; vi++) {
						var vy = y + sz * (0.28 + vi * 0.22);
						ctx.beginPath();
						ctx.moveTo(x + sz * 0.22, vy);
						ctx.lineTo(x + sz * 0.78, vy);
						ctx.stroke();
					}
					break;
				case 'play':
					ctx.fillStyle = '#40B060';
					ctx.beginPath();
					ctx.moveTo(cx - r * 0.4, cy - r * 0.6);
					ctx.lineTo(cx + r * 0.6, cy);
					ctx.lineTo(cx - r * 0.4, cy + r * 0.6);
					ctx.closePath();
					ctx.fill();
					break;
				case 'stop':
					ctx.fillStyle = '#D04040';
					fillRR(ctx, cx - r * 0.45, cy - r * 0.45, r * 0.9, r * 0.9, r * 0.08);
					break;
				case 'rewind':
					ctx.fillStyle = '#3080C0';
					ctx.beginPath();
					ctx.moveTo(cx + r * 0.3, cy - r * 0.5);
					ctx.lineTo(cx - r * 0.45, cy);
					ctx.lineTo(cx + r * 0.3, cy + r * 0.5);
					ctx.closePath();
					ctx.fill();
					ctx.fillRect(cx - r * 0.6, cy - r * 0.5, r * 0.15, r);
					break;
				case 'speedDown':
					ctx.fillStyle = '#2898A0';
					ctx.font = '900 ' + Math.round(sz * 0.5) + 'px serif';
					ctx.textAlign = 'center';
					ctx.fillText('《', cx, cy + r * 0.2);
					break;
				case 'speedUp':
					ctx.fillStyle = '#C06828';
					ctx.font = '900 ' + Math.round(sz * 0.5) + 'px serif';
					ctx.textAlign = 'center';
					ctx.fillText('》', cx, cy + r * 0.2);
					break;
				case 'single':
					ctx.fillStyle = '#8868B8';
					ctx.beginPath();
					ctx.moveTo(cx - r * 0.3, cy - r * 0.5);
					ctx.lineTo(cx + r * 0.4, cy);
					ctx.lineTo(cx - r * 0.3, cy + r * 0.5);
					ctx.closePath();
					ctx.fill();
					ctx.fillRect(cx + r * 0.4, cy - r * 0.5, r * 0.18, r);
					break;
				default:
					var numC = ['#E08040', '#3080C0', '#40A060', '#D04040', '#B8860B', '#8868B8', '#2898A0', '#A07030', '#606080'];
					var num = id.replace('btn', '');
					var ci = parseInt(num) - 1;
					ctx.fillStyle = id === 'btnAlt' ? '#C06030' : (numC[ci] || '#888');
					fillRR(ctx, x + r * 0.1, y + r * 0.1, sz * 0.8, sz * 0.8, r * 0.18);
					ctx.fillStyle = '#FFF';
					ctx.font = '900 ' + Math.round(sz * 0.45) + 'px ' + fC;
					ctx.textAlign = 'center';
					ctx.fillText(id === 'btnAlt' ? 'A' : num, cx, cy + r * 0.18);
					break;
			}
		};

		var hCanvas = document.createElement('canvas');
		hCanvas.id = 'hToolCanvas';
		hCanvas.style.cssText = 'position:fixed;z-index:9999;display:none;';
		document.body.appendChild(hCanvas);
		var hCtx = hCanvas.getContext('2d');

		var vCanvas = null,
			vCtx = null;
		var ensureVCanvas = function () {
			if (vCanvas && vCanvas.parentNode === core.dom.toolBar) return;
			vCanvas = document.createElement('canvas');
			vCanvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;z-index:100;';
			vCtx = vCanvas.getContext('2d');
			core.dom.toolBar.style.position = 'relative';
			core.dom.toolBar.appendChild(vCanvas);
			vCanvas.addEventListener('mousedown', function (e) {
				e.stopPropagation();
				e.preventDefault();
				doClick(e.clientX, e.clientY, vCanvas, vBtnRects);
			});
			vCanvas.addEventListener('touchstart', function (e) {
				e.stopPropagation();
				e.preventDefault();
				var t = e.touches[0];
				if (t) doClick(t.clientX, t.clientY, vCanvas, vBtnRects);
			});
		};

		var hBtnRects = [],
			vBtnRects = [];

		var drawButtons = function (ctx, W, H, btns, mode, rects) {
			// 玄幻深紫背景
			var bg = ctx.createLinearGradient(0, 0, 0, H);
			bg.addColorStop(0, BG1);
			bg.addColorStop(1, BG2);
			ctx.fillStyle = bg;
			ctx.fillRect(0, 0, W, H);
			// 外边框
			ctx.strokeStyle = '#4a2080';
			ctx.lineWidth = 1;
			ctx.strokeRect(0.5, 0.5, W - 1, H - 1);

			var showHard = (mode === 'normal' || mode === 'num');
			var hardW = showHard ? 38 : 0;
			var pad = 3,
				gap = 2;
			var areaW = W - pad * 2 - hardW - (hardW ? gap : 0);
			var btnH = H - pad * 2;
			var btnW = Math.floor((areaW - gap * (btns.length - 1)) / btns.length);
			var totalW = btns.length * btnW + (btns.length - 1) * gap;
			var startX = pad + Math.floor((areaW - totalW) / 2);

			rects.length = 0;
			for (var i = 0; i < btns.length; i++) {
				var id = btns[i];
				var bx = startX + i * (btnW + gap),
					by = pad;
				var info = btnInfo[id] || { name: id, c: '#aaa' };
				rects.push({ x: bx, y: by, w: btnW, h: btnH, id: id });

				// 天赋按钮特殊背景
				if (id === 'shop') {
					fillRR(ctx, bx, by, btnW, btnH, 5, TALENT_BG);
					strokeRR(ctx, bx, by, btnW, btnH, 5, TALENT_BD, 1);
				} else {
					fillRR(ctx, bx, by, btnW, btnH, 5, BTN_BG);
					strokeRR(ctx, bx, by, btnW, btnH, 5, BTN_BD, 1);
				}

				var iSz = Math.round(Math.min(btnH * 0.65, btnW * 0.42));
				if (iSz < 14) iSz = 14;
				drawToolIcon(ctx, id, bx + (btnW - iSz) / 2, by + 1, iSz);

				var fs = Math.max(8, Math.min(13, Math.floor(btnW / 1.1)));
				ctx.textAlign = 'center';
				ctx.font = '700 ' + fs + 'px ' + fC;
				ctx.fillStyle = info.c;
				ctx.fillText(info.name, bx + btnW / 2, by + btnH + 1);
			}

			if (showHard) {
				var hx = W - pad - hardW,
					hy = pad;
				fillRR(ctx, hx, hy, hardW, btnH, 5, HARD_BG);
				strokeRR(ctx, hx, hy, hardW, btnH, 5, HARD_BD, 1);
				ctx.textAlign = 'center';
				var hfs = Math.max(8, Math.floor(btnH / 3.5));
				ctx.font = '700 ' + hfs + 'px ' + fC;
				ctx.fillStyle = '#d0a0ff';
				ctx.fillText(core.status.hard || '', hx + hardW / 2, hy + btnH / 2 + hfs * 0.35);
				ctx.font = '700 ' + Math.max(7, hfs - 2) + 'px ' + fC;
				ctx.fillStyle = 'rgba(180,140,255,0.45)';
				ctx.fillText(mode === 'normal' ? '切换' : '返回', hx + hardW / 2, hy + btnH - 2);
				rects.push({ x: hx, y: hy, w: hardW, h: btnH, id: '__hard__' });
			}
		};

		var drawToolBar = function () {
			var isTitle = !core.isPlaying || !core.isPlaying() || !core.status.played ||
				(core.dom.startPanel && core.dom.startPanel.style.display !== 'none');
			if (isTitle) {
				hCanvas.style.display = 'none';
				if (vCanvas) vCanvas.style.display = 'none';
				core.dom.toolBar.style.visibility = '';
				return;
			}
			if (!core.dom.toolBar || core.dom.toolBar.style.display === 'none') {
				hCanvas.style.display = 'none';
				if (vCanvas) vCanvas.style.display = 'none';
				return;
			}

			var mode = core.domStyle.toolbarBtn || 'normal';
			var btns = [];
			try { btns = core.plugin.getToolBarConfig(mode) || []; } catch (e) { }
			if (btns.length === 0) return;

			if (core.domStyle.isVertical) {
				hCanvas.style.display = 'none';
				core.dom.toolBar.style.visibility = '';
				ensureVCanvas();
				var imgs = core.dom.toolBar.querySelectorAll('img.tools');
				for (var i = 0; i < imgs.length; i++) {
					imgs[i].style.opacity = '0';
					imgs[i].style.pointerEvents = 'none';
				}
				if (core.dom.hard) {
					core.dom.hard.style.opacity = '0';
					core.dom.hard.style.pointerEvents = 'none';
				}
				var W = core.dom.toolBar.clientWidth,
					H = core.dom.toolBar.clientHeight;
				if (W < 10 || H < 10) return;
				vCanvas.style.display = 'block';
				vCanvas.width = W;
				vCanvas.height = H;
				vCanvas.style.width = W + 'px';
				vCanvas.style.height = H + 'px';
				drawButtons(vCtx, W, H, btns, mode, vBtnRects);
			} else {
				if (vCanvas) vCanvas.style.display = 'none';
				core.dom.toolBar.style.visibility = 'hidden';
				hCanvas.style.display = 'block';
				var gdRect = core.dom.gameDraw.getBoundingClientRect();
				var tbRect = core.dom.toolBar.getBoundingClientRect();
				var W = Math.round(gdRect.width),
					H = Math.round(tbRect.height);
				if (W < 10 || H < 10) return;
				hCanvas.style.left = Math.round(gdRect.left) + 'px';
				hCanvas.style.top = Math.round(tbRect.top) + 'px';
				hCanvas.width = W;
				hCanvas.height = H;
				hCanvas.style.width = W + 'px';
				hCanvas.style.height = H + 'px';
				drawButtons(hCtx, W, H, btns, mode, hBtnRects);
			}
		};

		var doClick = function (clientX, clientY, canvas, rects) {
			var r = canvas.getBoundingClientRect();
			var cx = clientX - r.left,
				cy = clientY - r.top;
			for (var i = 0; i < rects.length; i++) {
				var b = rects[i];
				if (cx >= b.x && cx <= b.x + b.w && cy >= b.y && cy <= b.y + b.h) {
					if (b.id === '__hard__') {
						if (core.isReplaying()) return;
						if (core.domStyle.toolbarBtn === 'normal') core.setToolbarButton('num');
						else if (core.domStyle.toolbarBtn === 'num') core.setToolbarButton('normal');
						return;
					}
					var imgEl = main.statusBar.image[b.id];
					if (imgEl && imgEl.onclick) imgEl.onclick(new MouseEvent('click', { bubbles: false }));
					return;
				}
			}
		};

		hCanvas.addEventListener('mousedown', function (e) {
			e.stopPropagation();
			e.preventDefault();
			doClick(e.clientX, e.clientY, hCanvas, hBtnRects);
		});
		hCanvas.addEventListener('touchstart', function (e) {
			e.stopPropagation();
			e.preventDefault();
			var t = e.touches[0];
			if (t) doClick(t.clientX, t.clientY, hCanvas, hBtnRects);
		});
		hCanvas.addEventListener('dblclick', function (e) {
			e.stopPropagation();
			var r = hCanvas.getBoundingClientRect();
			var cx = e.clientX - r.left,
				cy = e.clientY - r.top;
			for (var i = 0; i < hBtnRects.length; i++) {
				var b = hBtnRects[i];
				if (b.id === 'toolbox' && cx >= b.x && cx <= b.x + b.w && cy >= b.y && cy <= b.y + b.h) {
					var imgEl = main.statusBar.image.toolbox;
					if (imgEl && imgEl.ondblclick) imgEl.ondblclick(new MouseEvent('dblclick'));
					return;
				}
			}
		});

		var _origUpdate = core.control.updateStatusBar;
		core.control.updateStatusBar = function () {
			_origUpdate.apply(this, arguments);
			try { drawToolBar(); } catch (e) { }
		};
		var _origResize = core.control.resize;
		core.control.resize = function () {
			_origResize.apply(this, arguments);
			try { setTimeout(drawToolBar, 80); } catch (e) { }
		};
		var _origShowStart = core.control.showStartAnimate;
		core.control.showStartAnimate = function () {
			hCanvas.style.display = 'none';
			if (vCanvas) vCanvas.style.display = 'none';
			if (core.dom.toolBar) core.dom.toolBar.style.visibility = '';
			if (_origShowStart) return _origShowStart.apply(this, arguments);
		};
		setTimeout(function () {
			if (core.plugin.setToolbarButton && !core.plugin.setToolbarButton.__hooked) {
				var _o = core.plugin.setToolbarButton;
				core.plugin.setToolbarButton = function () {
					_o.apply(this, arguments);
					try { setTimeout(drawToolBar, 50); } catch (e) { }
				};
				core.plugin.setToolbarButton.__hooked = true;
			}
			drawToolBar();
		}, 500);
		setTimeout(drawToolBar, 1500);
	},
    "行动": function () {
		// 在此增加新插件
		// 重写 canMoveHero，实现高低差通行规则
		var _orig_canMoveHero = core.maps._canMoveHero_checkPoint.bind(core.maps);
		core.maps._canMoveHero_checkPoint = function (x, y, direction, floorId, arrays) {
			floorId = floorId || core.status.floorId;
			arrays = arrays || this._generateMovableArray_arrays(floorId);
			var nx = x + core.utils.scan[direction].x;
			var ny = y + core.utils.scan[direction].y;
			if (nx < 0 || ny < 0 || nx >= core.floors[floorId].width || ny >= core.floors[floorId].height)
				return false;
			var curBg = (arrays.bgArray[y] || [])[x] || 0;
			var nextBg = (arrays.bgArray[ny] || [])[nx] || 0;
			// const curEvent = (arrays.eventArray[y] || [])[x] || 0;
			// const nextEvent = (arrays.eventArray[ny] || [])[nx] || 0;
			const curFg = (arrays.fgArray[y] || [])[x] || 0;
			const nextFg = (arrays.fgArray[ny] || [])[nx] || 0;
			// 151：完全不可通行
			if (nextBg === 151) return false;
			// 152：只能上下走
			if (nextBg === 152 && (direction === 'left' || direction === 'right')) return false;
			if (curBg === 152 && (direction === 'left' || direction === 'right')) return false;

			// 10100 桥
			// 在1楼桥只可以通往其它桥和1楼地面
			if (curFg === 10100 && core.getFlag('heroHeight') === 1) {
				if (nextBg !== 20 && nextFg !== 10100) return false;
			}

			// 20的铁规则：在0楼时只能从20或152进入，只能走到20或152，但在1楼
			// 在1楼时只能从20或152或前景10100的图块进入，只能走到20或152或前景10100的图块

			if (curBg === 20 && nextBg !== 20 && nextBg !== 152 &&
				!(nextFg === 10100 && core.getFlag('heroHeight') === 1)
			) return false;
			if (nextBg === 20 && curBg !== 20 && curBg !== 152 &&
				!(curFg === 10100 && core.getFlag('heroHeight') === 1)
			) return false;
			// 地面和梯子不能走上桥
			// if ((curBg === 0 || curBg === 152) && nextFg === 10100) return false;
			return _orig_canMoveHero(x, y, direction, floorId, arrays);
		};
	},
    "新的手册": function () {
		/*if (main.replayChecking) return;
	
		var SPECIAL_DESC = {
			1: { name: '先攻', color: '#ffcc33', desc: '战斗开始时先手攻击玩家一次，再进入正常战斗流程，无法通过防御规避。' },
			2: { name: '魔法', color: '#cc80ff', desc: '攻击无视玩家防御，直接以攻击力造成伤害。' },
			3: { name: '坚固', color: '#c0b088', desc: '怪物防御不小于角色攻击-1，极难造成伤害。' },
			4: { name: '2连击', color: '#ffee77', desc: '每回合攻击玩家两次，造成双倍伤害。' },
			5: { name: '3连击', color: '#ffee77', desc: '每回合攻击玩家三次，造成三倍伤害。' },
			6: { name: 'N连击', color: '#ffee77', desc: '每回合攻击玩家N次，造成N倍伤害。' },
			7: { name: '破甲', color: '#88c0ff', desc: '战斗前，怪物附加玩家防御的一定比例作为额外伤害。' },
			8: { name: '反击', color: '#ffaa44', desc: '战斗时每回合附加玩家攻击的一定比例作为伤害，无视防御。' },
			9: { name: '净化', color: '#80eed6', desc: '战斗前，怪物附加玩家护盾的一定倍数作为额外伤害。' },
			10: { name: '模仿', color: '#b0c0dd', desc: '怪物的攻防与玩家攻防相等，随玩家成长而增强。' },
			11: { name: '吸血', color: '#dd4448', desc: '战斗前吸取玩家一定比例的生命值作为伤害。' },
			12: { name: '中毒', color: '#99ee88', desc: '战斗后玩家陷入中毒状态，每步持续损失生命值。' },
			13: { name: '衰弱', color: '#f0bbcc', desc: '战斗后玩家攻防暂时下降，持续数步。' },
			14: { name: '诅咒', color: '#bbeef0', desc: '战斗后玩家陷入诅咒状态，战斗无法获得金币和经验。' },
			15: { name: '领域', color: '#c677dd', desc: '怪物周围存在领域，进入领域范围每步扣除固定生命值。' },
			16: { name: '夹击', color: '#bb99ee', desc: '经过两只相同的怪物中间时，受到该怪物的战斗伤害。' },
			17: { name: '仇恨', color: '#b0b666', desc: '战斗前附加积累的仇恨值作为伤害，战斗后释放一半仇恨值。' },
			18: { name: '击退', color: '#8888e6', desc: '经过怪物周围时自动减少生命值，同时怪物后退一格。' },
			19: { name: '自爆', color: '#ff6666', desc: '战斗后玩家的生命值变为1。' },
			20: { name: '无敌', color: '#aaaaaa', desc: '玩家无法打败此怪物，除非拥有十字架道具。' },
		};
	
		function getSpecials(special) {
			if (!special && special !== 0) return [];
			var arr = Array.isArray(special) ? special : [special];
			return arr.filter(function (s) { return s !== 0 && SPECIAL_DESC[s]; });
		}
	
		function buildCritTable(eid, ex, ey) {
			var rows = [];
			try {
				var heroAtk = core.getRealStatus('atk') || 0;
				var d0 = core.getDamageInfo(eid, null, ex, ey);
				if (!d0 || d0.damage == null) return rows;
				var baseDmg = d0.damage;
				rows.push({ atk: heroAtk, label: heroAtk + '（当前）', dmg: baseDmg, reduce: null });
				var seen = {};
				seen[heroAtk] = true;
				try {
					var crits = core.enemys.nextCriticals(eid, 5, ex, ey);
					if (crits && crits.length) {
						crits.forEach(function (c) {
							if (!c || !c[0]) return;
							var needAtk = heroAtk + c[0];
							if (seen[needAtk]) return;
							seen[needAtk] = true;
							var d1 = core.getDamageInfo(eid, { atk: needAtk }, ex, ey);
							var newDmg = (d1 && d1.damage != null) ? d1.damage : 0;
							rows.push({ atk: needAtk, label: needAtk, dmg: newDmg, reduce: baseDmg - newDmg });
							var nextAtk = needAtk + 1;
							if (!seen[nextAtk]) {
								seen[nextAtk] = true;
								var d2 = core.getDamageInfo(eid, { atk: nextAtk }, ex, ey);
								var dmg2 = (d2 && d2.damage != null) ? d2.damage : newDmg;
								rows.push({ atk: nextAtk, label: nextAtk, dmg: dmg2, reduce: baseDmg - dmg2 });
							}
						});
					}
				} catch (e) {}
			} catch (e) {}
			return rows;
		}
	
		function formatNum(n) {
			if (n == null) return '?';
			return core.formatBigNumber ? core.formatBigNumber(n) : String(n);
		}
	
		function getGreedyBonus() {
			try {
				return core.getFlag('__talent_greedy__', 0);
			} catch (e) { return 0; }
		}
		this.getGreedyBonus = getGreedyBonus;
	
		function buildDOM(floorId) {
			var enemys = core.enemys.getCurrentEnemys(floorId);
			var heroAtk = core.getRealStatus('atk') || 0;
			var heroDef = core.getRealStatus('def') || 0;
			var greedyBonus = getGreedyBonus();
	
			var gameCanvas = document.querySelector('canvas.gameCanvas') || document.querySelector('canvas');
			var rect = gameCanvas ? gameCanvas.getBoundingClientRect() : null;
			var CH = rect ? rect.height : 500;
			var CW = rect ? rect.width : 400;
	
			var headerH = 38,
				footerH = 28,
				theadH = 30;
			var availH = CH - headerH - footerH - theadH;
			var rowH = Math.max(36, Math.floor(availH / Math.max(enemys.length, 1)));
			rowH = Math.min(rowH, 72);
			var fs = Math.max(10, Math.floor(rowH * 0.26));
			var fsSm = Math.max(9, Math.floor(fs * 0.85));
			var iconSz = Math.max(18, Math.floor(rowH * 0.5));
	
			var overlay = document.createElement('div');
			overlay.id = '_monsterBookOverlay';
			overlay.style.cssText = 'position:fixed;z-index:9999;font-family:"楷体",STKaiti,"Microsoft YaHei",sans-serif;background:rgba(0,0,0,0.75);';
			if (rect) {
				overlay.style.left = rect.left + 'px';
				overlay.style.top = rect.top + 'px';
				overlay.style.width = CW + 'px';
				overlay.style.height = CH + 'px';
			} else {
				overlay.style.cssText += 'left:0;top:0;width:100%;height:100%;';
			}
	
			var panel = document.createElement('div');
			panel.style.cssText = 'width:100%;height:100%;background:#04020c;border:1px solid #4a2080;display:flex;flex-direction:column;overflow:hidden;';
	
			var header = document.createElement('div');
			header.style.cssText = 'background:linear-gradient(90deg,#1a0838,#0e0420);border-bottom:1px solid #4a2080;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;height:' + headerH + 'px;box-sizing:border-box;';
			var titleEl = document.createElement('span');
			titleEl.style.cssText = 'font-size:' + Math.max(13, Math.floor(fs * 1.2)) + 'px;font-weight:700;color:#d0a0ff;letter-spacing:3px;';
			titleEl.textContent = '怪 物 手 册';
			var closeBtn = document.createElement('span');
			closeBtn.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(180,140,255,0.6);background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);border-radius:4px;padding:3px 12px;cursor:pointer;';
			closeBtn.textContent = '✕ 关闭';
			closeBtn.onclick = function () { closeBook(); };
			header.appendChild(titleEl);
			header.appendChild(closeBtn);
	
			// 列宽：给数字更多空间，特性列稍窄
			var cols = '2.2fr 1.1fr 1.1fr 1.1fr 1fr 1.2fr 1.1fr 1.1fr 1.3fr';
			var thead = document.createElement('div');
			thead.style.cssText = 'display:grid;grid-template-columns:' + cols + ';background:rgba(80,40,160,0.25);border-bottom:1px solid rgba(80,40,160,0.35);flex-shrink:0;padding:0 6px;height:' + theadH + 'px;box-sizing:border-box;';
			['名称', '生命', '攻击', '防御', '金币', '伤害', '临界', '1防减', '特性'].forEach(function (t, i) {
				var th = document.createElement('div');
				th.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(180,140,255,0.6);display:flex;align-items:center;justify-content:' + (i === 0 ? 'flex-start' : 'center') + ';' + (i === 0 ? 'padding-left:4px;' : '');
				th.textContent = t;
				thead.appendChild(th);
			});
	
			var body = document.createElement('div');
			body.style.cssText = 'flex:1;overflow-y:auto;';
			body.style.scrollbarWidth = 'thin';
	
			var expandedRow = null;
			var expandEl = null;
	
			enemys.forEach(function (enemy, idx) {
				var eid = enemy.id;
				var ex = enemy.x,
					ey = enemy.y;
				var ei;
				try { ei = core.getEnemyInfo(eid, null, ex, ey); } catch (e) { ei = enemy; }
				var ename = enemy.name || ei.name || eid;
	
				var dmgInfo;
				try { dmgInfo = core.getDamageString(eid, ex, ey); } catch (e) { dmgInfo = { damage: '?' }; }
				var dmg = dmgInfo ? dmgInfo.damage : '?';
	
				var critVal = '—',
					defReduce = '—';
				try {
					var crits = core.enemys.nextCriticals(eid, 1, ex, ey);
					if (crits && crits[0] && crits[0][0] > 0) critVal = '+' + formatNum(crits[0][0]);
				} catch (e) {}
				try {
					var d0 = core.getDamageInfo(eid, null, ex, ey);
					var d1 = core.getDamageInfo(eid, { def: heroDef + 1 }, ex, ey);
					if (d0 && d1 && d0.damage != null && d1.damage != null) {
						var dr = d0.damage - d1.damage;
						defReduce = dr > 0 ? '-' + dr : '0';
					}
				} catch (e) {}
	
				var specials = getSpecials(ei.special);
	
				var row = document.createElement('div');
				row.style.cssText = 'display:grid;grid-template-columns:' + cols + ';border-bottom:1px solid rgba(255,255,255,0.05);cursor:pointer;padding:0 6px;height:' + rowH + 'px;box-sizing:border-box;';
	
				function makeTd(content, color, isFirst) {
					var td = document.createElement('div');
					td.style.cssText = 'font-size:' + fs + 'px;display:flex;align-items:center;justify-content:' + (isFirst ? 'flex-start' : 'center') + ';overflow:hidden;';
					if (color) td.style.color = color;
					if (typeof content === 'string' || typeof content === 'number') td.textContent = content;
					else td.appendChild(content);
					return td;
				}
	
				var nameTd = document.createElement('div');
				nameTd.style.cssText = 'display:flex;align-items:center;gap:4px;padding-left:4px;overflow:hidden;';
				var icon = document.createElement('div');
				icon.style.cssText = 'width:' + iconSz + 'px;height:' + iconSz + 'px;background:rgba(100,50,180,0.2);border:1px solid rgba(120,70,200,0.3);border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;';
				try {
					var blockInfo = core.getBlockInfo(eid);
					if (blockInfo && blockInfo.image) {
						var c = document.createElement('canvas');
						c.width = iconSz;
						c.height = iconSz;
						var ctx = c.getContext('2d');
						ctx.drawImage(blockInfo.image, 32 * blockInfo.posX, blockInfo.height * blockInfo.posY, 32, blockInfo.height, 0, 0, iconSz, iconSz);
						icon.appendChild(c);
					} else {
						icon.textContent = '?';
						icon.style.color = 'rgba(200,170,255,0.4)';
						icon.style.fontSize = fsSm + 'px';
					}
				} catch (e) {
					icon.textContent = '?';
					icon.style.color = 'rgba(200,170,255,0.4)';
					icon.style.fontSize = fsSm + 'px';
				}
				var nm = document.createElement('span');
				nm.style.cssText = 'font-size:' + fs + 'px;color:#d0b0ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
				nm.textContent = ename;
				nameTd.appendChild(icon);
				nameTd.appendChild(nm);
	
				var spTd = document.createElement('div');
				spTd.style.cssText = 'display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:2px;padding:2px;';
				if (specials.length === 0) {
					var b = document.createElement('span');
					b.style.cssText = 'font-size:' + fsSm + 'px;border-radius:3px;padding:1px 4px;background:rgba(200,200,200,0.06);color:rgba(200,200,200,0.38);';
					b.textContent = '普通';
					spTd.appendChild(b);
				} else {
					specials.forEach(function (s) {
						var info = SPECIAL_DESC[s];
						var b = document.createElement('span');
						b.style.cssText = 'font-size:' + fsSm + 'px;border-radius:3px;padding:1px 4px;background:rgba(255,255,255,0.07);color:' + (info ? info.color : '#fff') + ';border:1px solid rgba(255,255,255,0.1);white-space:nowrap;';
						b.textContent = info ? info.name : ('特性' + s);
						spTd.appendChild(b);
					});
				}
	
				// 金币（含贪婪加成）
				var moneyStr = greedyBonus > 0 ?
					formatNum(ei.money || 0) + '+' + greedyBonus :
					formatNum(ei.money || 0);
				var moneyColor = greedyBonus > 0 ? '#ffee60' : '#ffd060';
	
				row.appendChild(nameTd);
				row.appendChild(makeTd(formatNum(ei.hp), '#ff9090'));
				row.appendChild(makeTd(formatNum(ei.atk), '#ff7060'));
				row.appendChild(makeTd(formatNum(ei.def), '#88aaff'));
				row.appendChild(makeTd(moneyStr, moneyColor));
				var dmgTd = makeTd(dmg === '**' ? '免疫' : String(dmg), '#ff6060');
				dmgTd.style.fontWeight = '700';
				row.appendChild(dmgTd);
				row.appendChild(makeTd(critVal, '#ffaa40'));
				row.appendChild(makeTd(defReduce, '#88ccff'));
				row.appendChild(spTd);
	
				row.onclick = function () {
					if (expandedRow === idx) {
						if (expandEl && expandEl.parentNode) expandEl.parentNode.removeChild(expandEl);
						expandEl = null;
						expandedRow = null;
						row.style.background = '';
						return;
					}
					if (expandEl && expandEl.parentNode) expandEl.parentNode.removeChild(expandEl);
					body.querySelectorAll('[data-sel]').forEach(function (r) {
						r.style.background = '';
						r.removeAttribute('data-sel');
					});
					expandedRow = idx;
					row.style.background = 'rgba(120,60,200,0.18)';
					row.setAttribute('data-sel', '1');
					expandEl = buildExpand(eid, ex, ey, specials, fs, fsSm);
					row.parentNode.insertBefore(expandEl, row.nextSibling);
					expandEl.scrollIntoView({ block: 'nearest' });
				};
	
				body.appendChild(row);
			});
	
			var footer = document.createElement('div');
			footer.style.cssText = 'background:rgba(20,8,50,0.8);border-top:1px solid rgba(80,40,160,0.3);padding:5px 14px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;height:' + footerH + 'px;box-sizing:border-box;';
			var fl1 = document.createElement('span');
			fl1.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(180,140,255,0.4);';
			fl1.textContent = '共 ' + enemys.length + ' 只怪物 · 点击行展开特殊属性与临界表';
			var fl2 = document.createElement('span');
			fl2.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(180,140,255,0.4);';
			fl2.textContent = greedyBonus > 0 ?
				'攻击：' + heroAtk + ' · 防御：' + heroDef + ' · 贪婪+1💰' :
				'当前攻击：' + heroAtk + ' · 防御：' + heroDef;
			footer.appendChild(fl1);
			footer.appendChild(fl2);
	
			panel.appendChild(header);
			panel.appendChild(thead);
			panel.appendChild(body);
			panel.appendChild(footer);
			overlay.appendChild(panel);
			overlay.onclick = function (e) { if (e.target === overlay) closeBook(); };
			return overlay;
		}
	
		function buildExpand(eid, ex, ey, specials, fs, fsSm) {
			var wrap = document.createElement('div');
			wrap.style.cssText = 'background:rgba(30,8,70,0.6);border-bottom:1px solid rgba(80,40,160,0.3);padding:12px 16px;display:flex;gap:16px;';
	
			var left = document.createElement('div');
			left.style.cssText = 'width:220px;flex-shrink:0;';
			var leftTitle = document.createElement('div');
			leftTitle.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,160,255,0.45);letter-spacing:1px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid rgba(120,60,200,0.2);';
			leftTitle.textContent = '◈ 特殊属性说明';
			left.appendChild(leftTitle);
	
			if (specials.length === 0) {
				var none = document.createElement('div');
				none.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,170,255,0.3);padding:4px 0;';
				none.textContent = '该怪物无特殊属性';
				left.appendChild(none);
			} else {
				specials.forEach(function (s) {
					var info = SPECIAL_DESC[s];
					if (!info) return;
					var card = document.createElement('div');
					card.style.cssText = 'background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.07);border-radius:5px;padding:6px 10px;margin-bottom:5px;';
					var sn = document.createElement('div');
					sn.style.cssText = 'font-size:' + fs + 'px;font-weight:700;color:' + info.color + ';margin-bottom:3px;';
					sn.textContent = info.name;
					var sd = document.createElement('div');
					sd.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,170,255,0.55);line-height:1.6;';
					sd.textContent = info.desc;
					card.appendChild(sn);
					card.appendChild(sd);
					left.appendChild(card);
				});
			}
	
			var right = document.createElement('div');
			right.style.cssText = 'flex:1;';
			var rightTitle = document.createElement('div');
			rightTitle.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,160,255,0.45);letter-spacing:1px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid rgba(120,60,200,0.2);';
			rightTitle.textContent = '◈ 临界攻击表（当前攻击 ' + (core.getRealStatus('atk') || 0) + '）';
			right.appendChild(rightTitle);
	
			var critRows = buildCritTable(eid, ex, ey);
			if (critRows.length === 0) {
				var nd = document.createElement('div');
				nd.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,170,255,0.3);';
				nd.textContent = '无法计算临界';
				right.appendChild(nd);
			} else {
				var tbl = document.createElement('table');
				tbl.style.cssText = 'width:100%;border-collapse:collapse;';
				var hrow = document.createElement('tr');
				['需要攻击', '+增加', '受到伤害', '净减伤'].forEach(function (t, i) {
					var th = document.createElement('th');
					th.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(180,140,255,0.5);padding:4px 6px;text-align:' + (i === 0 ? 'left' : 'center') + ';border-bottom:1px solid rgba(80,40,160,0.3);font-weight:400;';
					th.textContent = t;
					hrow.appendChild(th);
				});
				tbl.appendChild(hrow);
				var heroAtk = core.getRealStatus('atk') || 0;
				critRows.forEach(function (r) {
					var tr = document.createElement('tr');
					var td0 = document.createElement('td');
					td0.style.cssText = 'font-size:' + fsSm + 'px;color:rgba(200,170,255,0.6);padding:4px 6px;border-bottom:1px solid rgba(255,255,255,0.04);text-align:left;';
					td0.textContent = r.label;
					var td1 = document.createElement('td');
					td1.style.cssText = 'font-size:' + fs + 'px;padding:4px 6px;border-bottom:1px solid rgba(255,255,255,0.04);text-align:center;color:rgba(200,170,255,0.5);';
					td1.textContent = r.atk === heroAtk ? '—' : '+' + (r.atk - heroAtk);
					var td2 = document.createElement('td');
					td2.style.cssText = 'font-size:' + fs + 'px;padding:4px 6px;border-bottom:1px solid rgba(255,255,255,0.04);text-align:center;color:' + (r.dmg > 0 ? '#ff7070' : '#80ff80') + ';';
					td2.textContent = formatNum(r.dmg);
					var td3 = document.createElement('td');
					td3.style.cssText = 'font-size:' + fs + 'px;padding:4px 6px;border-bottom:1px solid rgba(255,255,255,0.04);text-align:center;color:' + (r.reduce > 0 ? '#80ff80' : 'rgba(200,170,255,0.35)') + ';';
					td3.textContent = r.reduce == null ? '—' : (r.reduce > 0 ? '-' + formatNum(r.reduce) : '0');
					tr.appendChild(td0);
					tr.appendChild(td1);
					tr.appendChild(td2);
					tr.appendChild(td3);
					tbl.appendChild(tr);
				});
				right.appendChild(tbl);
			}
	
			wrap.appendChild(left);
			wrap.appendChild(right);
			return wrap;
		}
	
		function closeBook() {
			var el = document.getElementById('_monsterBookOverlay');
			if (!el) return;
			el.parentNode.removeChild(el);
			core.playSound('取消');
			core.ui.closePanel = _origClosePanel;
			_origClosePanel();
		}
	
		var _origClosePanel = core.ui.closePanel.bind(core.ui);
	
		core.ui.drawBook = function (index) {
			var floorId = core.floorIds[(core.status.event.ui || {}).index] || core.status.floorId;
			core.clearUI();
			core.clearMap('data');
			var old = document.getElementById('_monsterBookOverlay');
			if (old) old.parentNode.removeChild(old);
			core.ui.closePanel = function () {
				var el = document.getElementById('_monsterBookOverlay');
				if (el) {
					el.parentNode.removeChild(el);
					core.ui.closePanel = _origClosePanel;
					_origClosePanel();
					return;
				}
				core.ui.closePanel = _origClosePanel;
				_origClosePanel();
			};
			document.body.appendChild(buildDOM(floorId));
		};*/
	},
    "造桥": function () {
		// 	// 游戏开始时设定一次heroHeight为0
		// 	function afterHeroLocXYChange() {
		// 		const { x, y } = core.status.hero.loc;
		// 		const bgNum = core.getBgNumber(x, y);
		// 		const eventBlock = core.getBlock(x, y);
		// 		const eventNum = eventBlock ? eventBlock.id : 0;
		// 		// 上下楼梯也算地面 不然地上的怪变成空气墙会下不来
		// 		if ((bgNum === 0 || bgNum === 152) && eventNum !== 10100) {
		// 			// 说明现在走到了地上
		// 			if (core.getFlag('heroHeight') === 1) {
		// 				// 角色从2楼到地上
		// 				// 桥从事件层上升到前景层
		// 				// 地上背景层的怪物物品回到事件层
		// 				for (let i = 0; i <= 12; i++) {
		// 					for (let j = 0; j <= 12; j++) {
		// 						const num = core.getBgNumber(i, j);
		// 						const currBlock = core.getBlock(i, j);
		// 						if (currBlock && currBlock.id === 10100) {
		// 							core.setBgFgBlock('fg', 10100, i, j);
		// 							core.setBlock(0, i, j);
		// 						}
		// 						if (num === 0) continue; // 说明这格背景层没东西
		// 						else if (num === 20 || num === 151 || num === 152) {
		// 							continue; // 说明是三种墙体图块
		// 						} else {
		// 							const block = core.getBlockByNumber(num);
		// 							if (!block) continue;
		// 							if (block.event.cls === "enemys" || block.event.cls === "items") {
		// 								core.setBlock(num, i, j);
		// 								core.setBgFgBlock('bg', 0, i, j);
		// 								core.removeGlobalAnimate(i, j, 'bg');
		// 							}
		// 						}
		// 					}
		// 				}
		// 			}
		// 			core.setFlag('heroHeight', 0);
		// 		} else {
		// 			// 说明现在走到了2楼
		// 			if (core.getFlag('heroHeight') === 0) {
		// 				// 从地上到2楼
		// 				// 事件层的怪物物品回到背景层
		// 				// 桥从前景层下降到事件层
		// 				for (let i = 0; i <= 12; i++) {
		// 					for (let j = 0; j <= 12; j++) {
		// 						const num = core.getBgNumber(i, j);
		// 						const block = core.getBlock(i, j);
		// 						const frontNum = core.getFgNumber(i, j);
		// 						const isBridge = frontNum === 10100;
		// 						if (num === 0 && block) {
		// 							// 说明物品本身在地上，要藏到背景层去
		// 							const blockNum = block.id;
		// 							if (block.event.cls === "enemys" || block.event.cls === "items") {
		// 								core.setBlock(17, i, j); // 17是空气墙图块
		// 								core.setBgFgBlock('bg', blockNum, i, j);
		// 							}
		// 						}
		// 						if (isBridge) {
		// 							core.setBlock(10100, i, j);
		// 							core.setBgFgBlock('fg', 0, i, j);
		// 						}
		// 					}
		// 				}
		// 			}
		// 			core.setFlag('heroHeight', 1);
		// 		}
		// 	}

		// 我想到一个天才的方案，比黑海桥的方案快多了，不用搞这花里胡哨的，卡死了
		// 具体来说，桥永远在前景层，上2楼的时候，1楼的怪和物品进入"隐身"状态，不交互
		// 在2楼不允许瞬移到1楼，不和1楼的怪及物品交互

		// 拦截canMoveDirectly没有用，因为实际走路直接拿的canMoveDirectlyArray
		// const ori_canMoveDirectly = core.maps.canMoveDirectly;
		// core.maps.canMoveDirectly = function (x, y) {
		// 	const bgNum = core.getBgNumber(x, y);
		// 	const heroHeight = core.getFlag('heroHeight');
		// 	if (heroHeight === 1) { // 在1楼时，不允许瞬移到0层
		// 		if (bgNum === 0 || bgNum === 152) {
		// 			return -1;
		// 		}
		// 	} else { // 20是1楼地面的图块，在0层时也不允许瞬移到1层
		// 		if (bgNum === 20) {
		// 			return -1;
		// 		}
		// 	}
		// 	return ori_canMoveDirectly.apply(core.maps, [x, y]);
		// }

		core.maps._canMoveDirectly_bfs = function (sx, sy, locs, number, ans, canMoveArray, floorId) {
			canMoveArray = canMoveArray || this.generateMovableArray(floorId);
			var blocksObj = this.getMapBlocksObj(floorId);
			// 滑冰
			var bgMap = this.getBgMapArray(floorId);

			var visited = [],
				queue = [];
			visited[sx + "," + sy] = 0;
			queue.push(sx + "," + sy);

			while (queue.length > 0) {
				var now = queue.shift().split(","),
					x = parseInt(now[0]),
					y = parseInt(now[1]);
				for (var direction in core.utils.scan) {
					if (!core.inArray(canMoveArray[x][y], direction)) continue;
					var nx = x + core.utils.scan[direction].x,
						ny = y + core.utils.scan[direction].y,
						nindex = nx + "," + ny;
					if (visited[nindex]) continue;
					if (core.onSki(bgMap[ny][nx])) continue;

					// 在1楼不允许瞬移到0楼，在0楼也不允许瞬移到1楼
					const heroHeight = core.getFlag('heroHeight');
					if (heroHeight === 1) {
						if (bgMap[ny][nx] === 0 || bgMap[ny][nx] === 152) {
							continue;
						}
					} else {
						if (bgMap[ny][nx] === 20) {
							continue;
						}
					}

					if (!this._canMoveDirectly_checkNextPoint(blocksObj, nx, ny, floorId)) continue;
					visited[nindex] = visited[now] + 1;
					// if (nx == ex && ny == ey) return visited[nindex];
					for (var i in ans) {
						if (locs[i][0] == nx && locs[i][1] == ny && ans[i] == null) {
							// 不可以绿点为终点
							var block = blocksObj[nx + "," + ny];
							if (block && !block.disable && block.event.trigger) {
								ans[i] = -1;
							} else {
								ans[i] = visited[nindex];
							}
							number--;
							if (number == 0) return ans;
						}
					}
					queue.push(nindex);
				}
			}

			for (var i in ans) {
				if (ans[i] == null) ans[i] = -1;
			}
			return ans;
		}.bind(core.maps);

		function checkBlockPassability() {
			const floorId = core.status.floorId;
			core.extractBlocks(floorId);
			const bgMap = core.getBgMapArray(floorId);
			const heroHeight = core.hasFlag('heroHeight');
			core.status.maps[floorId].blocks.forEach(block => {
				if (bgMap[block.y][block.x] === 0) {
					if (['enemys', 'enemy48'].includes(block.event.cls) ||
						block.event.trigger === 'openDoor') {
						block.event.noPass = !heroHeight; // 1楼能穿，0楼不能
					}
				}
			});
		}
		this.checkBlockPassability = checkBlockPassability;

		function afterHeroLocXYChange() {
			const { x, y } = core.status.hero.loc;
			const floorId = core.status.floorId;
			const bgNum = core.getBgNumber(x, y);
			const eventBlock = core.getBlock(x, y);
			const eventNum = eventBlock ? eventBlock.id : 0;
			const fgNum = core.getFgNumber(x, y);

			// 目标是桥图块则不改变通行性
			// 现在桥图块永远在前景层
			if (fgNum === 10100) return;
			if ((bgNum === 0 || bgNum === 152)) {
				if (core.getFlag('heroHeight') === 1) {
					core.setFlag('heroHeight', 0);
					checkBlockPassability();
					drawBridge();
				}
			} else {
				if (!core.hasFlag('heroHeight')) {
					core.setFlag('heroHeight', 1);
					checkBlockPassability();
					drawBridge();
				}
			}
		}

		this.afterHeroLocXYChange = afterHeroLocXYChange;

		function setBridge(x, y) {
			const bridge = core.getFlag('bridge', {});
			const floorId = core.status.floorId;
			if (!bridge[floorId]) {
				bridge[floorId] = [];
			}
			bridge[floorId].push(`${x},${y}`);
			core.setFlag('bridge', bridge);
		}
		this.setBridge = setBridge;

		function getFloorBridgeList(floorId) {
			const bridge = core.getFlag('bridge', {});
			if (!bridge[floorId]) return [];
			return bridge[floorId];
		}
		this.getFloorBridgeList = getFloorBridgeList;

		function hasBridge(x, y) {
			const bridge = core.getFlag('bridge', {});
			const floorId = core.status.floorId;
			if (!bridge[floorId]) return false;
			return bridge[floorId].includes(`${x},${y}`);
		}
		this.hasBridge = hasBridge;

		function drawBridge() {
			const bridgeList = getFloorBridgeList(core.status.floorId);
			const heroHeight = core.getFlag('heroHeight');
			// 0楼时桥在前景层，1楼时桥的zIndex为35
			core.setOpacity('fg', (heroHeight === 0) ? 1 : 0);
			if (heroHeight === 0) {
				core.deleteCanvas('bridge');
				return;
			}
			// z值大于事件层，小于勇士
			const ctx = core.createCanvas('bridge', 0, 0, 416, 416, 35);
			bridgeList.forEach(bridge => {
				const [x, y] = bridge.split(',').map(Number);
				core.drawIcon(ctx, "X10100", x * 32, y * 32, 32, 32);
			});
		}
		this.drawBridge = drawBridge;

		/**
		 * 判断单个格子是否在地面上
		 */
		function isPosOnGround(x, y) {
			return core.getBgNumber(x, y) === 0;
		}

		const execCallBackFunc = (callback) => {
			if (typeof callback === "function") return callback();
			return null;
		}

		const ori_sys_battle = core.events._sys_battle;
		core.events._sys_battle = function (data, callback) {
			if (core.getFlag('heroHeight') === 1 && isPosOnGround(data.x, data.y)) return execCallBackFunc(callback);
			return ori_sys_battle.apply(core.events, [data, callback]);
		}

		const ori_sys_getItem = core.events._sys_getItem;
		core.events._sys_getItem = function (data, callback) {
			if (core.getFlag('heroHeight') === 1 && isPosOnGround(data.x, data.y)) return execCallBackFunc(callback);
			return ori_sys_getItem.apply(core.events, [data, callback]);
		}

		const ori_sys_openDoor = core.events._sys_openDoor;
		core.events._sys_openDoor = function (data, callback) {
			if (core.getFlag('heroHeight') === 1 && isPosOnGround(data.x, data.y)) return execCallBackFunc(callback);
			return ori_sys_openDoor.apply(core.events, [data, callback]);
		}

		const ori_trigger = core.events.trigger;
		core.events.trigger = function (x, y, callback) {
			var _executeCallback = function () {
				// 因为trigger之后还有可能触发其他同步脚本（比如阻激夹域检测）
				// 所以这里强制callback被异步触发
				if (callback) {
					setTimeout(callback, 1); // +1是为了录像检测系统 
				}
				return;
			}
			if (core.status.gameOver) return _executeCallback();
			if (core.status.event.id == 'action') {
				core.insertAction({ "type": "function", "function": "function () { core.events._trigger_inAction(" + x + "," + y + "); }", "async": true },
					null, null, null, true);
				return _executeCallback();
			}
			if (core.status.event.id) return _executeCallback();

			var block = core.getBlock(x, y);
			if (block == null) return _executeCallback();

			// 执行该点的脚本
			if (block.event.script) {
				core.clearRouteFolding();
				try {
					eval(block.event.script);
				} catch (ee) { console.error(ee) }
			}

			// 碰触事件
			if (block.event.event) {
				core.clearRouteFolding();
				core.insertAction(block.event.event, block.x, block.y);
				// 不再执行该点的系统事件
				return _executeCallback();
			}

			if (block.event.trigger && block.event.trigger != 'null') {
				var noPass = block.event.noPass,
					trigger = block.event.trigger;
				if (noPass) core.clearAutomaticRouteNode(x, y);

				// 新增，1楼时不触发切换楼层事件
				if (trigger == 'changeFloor' && core.getBgNumber(block.x, block.y) === 0 &&
					(block.event.id === 'downFloor' || block.event.id === 'upFloor') &&
					core.hasFlag('heroHeight')) {
					return _executeCallback();
				}
				// 转换楼层能否穿透
				if (trigger == 'changeFloor' && !noPass && this._trigger_ignoreChangeFloor(block))
					return _executeCallback();
				core.status.automaticRoute.moveDirectly = false;
				this.doSystemEvent(trigger, block);
			}
			return _executeCallback();
		}
	}
}