///<reference path='../runtime.d.ts'/>
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 =
{
	"init": function () {
		this._afterLoadResources = function () {
			// 本函数将在所有资源加载完毕后，游戏开启前被执行
			// 安装自绘状态栏：干掉旧 DOM 状态栏，改用 canvas 绘制。
			if (core.plugin.installPastelStatusBar) core.plugin.installPastelStatusBar();
			// 地图道具数值显示：血瓶、宝石、装备等直接在地图上显示实际加成。
			if (core.plugin.installMapItemValueDisplay) core.plugin.installMapItemValueDisplay();
			// 豹跃：在地图上画黄色落点框并拦截点击。
			if (core.plugin.installLeopardLeapSelector) core.plugin.installLeopardLeapSelector();
		}
	},
	"drawLight": function () {

		// 绘制灯光/漆黑层效果。调用方式 core.plugin.drawLight(...)
		// 【参数说明】
		// name：必填，要绘制到的画布名；可以是一个系统画布，或者是个自定义画布；如果不存在则创建
		// color：可选，只能是一个0~1之间的数，为不透明度的值。不填则默认为0.9。
		// lights：可选，一个数组，定义了每个独立的灯光。
		//        其中每一项是三元组 [x,y,r] x和y分别为该灯光的横纵坐标，r为该灯光的半径。
		// lightDec：可选，0到1之间，光从多少百分比才开始衰减（在此范围内保持全亮），不设置默认为0。
		//        比如lightDec为0.5代表，每个灯光部分内圈50%的范围全亮，50%以后才开始快速衰减。
		// 【调用样例】
		// core.plugin.drawLight('curtain'); // 在curtain层绘制全图不透明度0.9，等价于更改画面色调为[0,0,0,0.9]。
		// core.plugin.drawLight('ui', 0.95, [[25,11,46]]); // 在ui层绘制全图不透明度0.95，其中在(25,11)点存在一个半径为46的灯光效果。
		// core.plugin.drawLight('test', 0.2, [[25,11,46,0.1]]); // 创建一个test图层，不透明度0.2，其中在(25,11)点存在一个半径为46的灯光效果，灯光中心不透明度0.1。
		// core.plugin.drawLight('test2', 0.9, [[25,11,46],[105,121,88],[301,221,106]]); // 创建test2图层，且存在三个灯光效果，分别是中心(25,11)半径46，中心(105,121)半径88，中心(301,221)半径106。
		// core.plugin.drawLight('xxx', 0.3, [[25,11,46],[105,121,88,0.2]], 0.4); // 存在两个灯光效果，它们在内圈40%范围内保持全亮，40%后才开始衰减。
		this.drawLight = function (name, color, lights, lightDec) {

			// 清空色调层；也可以修改成其它层比如animate/weather层，或者用自己创建的canvas
			var ctx = core.getContextByName(name);
			if (ctx == null) {
				if (typeof name == 'string')
					ctx = core.createCanvas(name, 0, 0, core._PX_ || core.__PIXELS__, core._PY_ || core.__PIXELS__, 98);
				else return;
			}

			ctx.mozImageSmoothingEnabled = false;
			ctx.webkitImageSmoothingEnabled = false;
			ctx.msImageSmoothingEnabled = false;
			ctx.imageSmoothingEnabled = false;

			core.clearMap(name);
			// 绘制色调层，默认不透明度
			if (color == null) color = 0.9;
			ctx.fillStyle = "rgba(0,0,0," + color + ")";
			ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

			lightDec = core.clamp(lightDec, 0, 1);

			// 绘制每个灯光效果
			ctx.globalCompositeOperation = 'destination-out';
			lights.forEach(function (light) {
				// 坐标，半径，中心不透明度
				var x = light[0],
					y = light[1],
					r = light[2];
				// 计算衰减距离
				var decDistance = parseInt(r * lightDec);
				// 正方形区域的直径和左上角坐标
				var grd = ctx.createRadialGradient(x, y, decDistance, x, y, r);
				grd.addColorStop(0, "rgba(0,0,0,1)");
				grd.addColorStop(1, "rgba(0,0,0,0)");
				ctx.beginPath();
				ctx.fillStyle = grd;
				ctx.arc(x, y, r, 0, 2 * Math.PI);
				ctx.fill();
			});
			ctx.globalCompositeOperation = 'source-over';
			// 可以在任何地方（如afterXXX或自定义脚本事件）调用函数，方法为  core.plugin.xxx();
		}
	},

	"pastelStatusBar": function () {
		// 竹叶暖色风格状态栏（全 canvas 绘制）。
		// 默认显示：楼层、生命、攻击、防御、魔防、金币、黄蓝红钥匙。
		// 装备与技能区域默认留空；如需绘制 3 个装备技能，可设置 flag：statusEquipSkills。

		this.installPastelStatusBar = function () {
			// 开启状态栏 canvas 化。
			// 编辑器保存 data.js 后，statusCanvas 可能会被写回 false；
			// 所以这里每次安装、每次绘制都强制恢复一次。
			this._psbEnableCanvas();
			this._psbBindDrawHook();

			// 强制重新布局一次，否则 statusCanvas 可能仍处于 display:none。
			setTimeout(function () {
				if (!core.plugin || !core.plugin._psbEnableCanvas) return;
				core.plugin._psbEnableCanvas();
				try { if (core.resize) core.resize(); } catch (e) {}
				try { if (core.plugin.drawPastelStatusBar) core.plugin.drawPastelStatusBar(); } catch (e) { console.error(e); }
			}, 0);
		};

		this._psbEnableCanvas = function () {
			core.flags = core.flags || {};
			core.flags.statusCanvas = true;
			core.flags.hideLeftStatusBar = false;
			core.values = core.values || {};
			// 手机竖屏顶部状态栏需要更多空间显示属性、道具和本层死亡次数。
			if (!core.values.statusCanvasRowsOnMobile || core.values.statusCanvasRowsOnMobile < 4) core.values.statusCanvasRowsOnMobile = 4;

			if (core.status && core.status.globalAttribute) {
				core.status.globalAttribute.statusLeftBackground = 'transparent';
				core.status.globalAttribute.statusTopBackground = 'transparent';
			}

			// 即使 resize 还没重新跑，也尽量把 canvas 显示出来、把旧 DOM 状态栏隐藏掉。
			if (core.dom && core.dom.statusCanvas) core.dom.statusCanvas.style.display = 'block';
			if (core.dom && core.dom.status) {
				for (var i = 0; i < core.dom.status.length; i++) {
					core.dom.status[i].style.display = 'none';
				}
			}
		};

		this._psbBindDrawHook = function () {
			if (!core.ui) return;
			// 比只改 uidata 更稳：有些编辑器操作/资源重载会重新生成 uidata。
			core.ui.drawStatusBar = function () {
				if (core.plugin && core.plugin._psbEnableCanvas) core.plugin._psbEnableCanvas();
				if (core.plugin && core.plugin.drawPastelStatusBar) core.plugin.drawPastelStatusBar();
			};
			if (core.ui.uidata) {
				core.ui.uidata.drawStatusBar = function () {
					if (core.plugin && core.plugin._psbEnableCanvas) core.plugin._psbEnableCanvas();
					if (core.plugin && core.plugin.drawPastelStatusBar) core.plugin.drawPastelStatusBar();
				};
			}
		};

		this.drawPastelStatusBar = function () {
			this._psbEnableCanvas();
			var ctx = core.dom.statusCanvasCtx;
			if (!ctx) return;

			var scale = core.domStyle && core.domStyle.scale ? core.domStyle.scale : 1;
			var styleW = parseFloat(core.dom.statusCanvas.style.width) || 0;
			var styleH = parseFloat(core.dom.statusCanvas.style.height) || 0;
			var w = Math.round(styleW / scale) || (core.domStyle.isVertical ? core._PX_ : Math.round(core._PY_ * 0.31));
			var h = Math.round(styleH / scale) || (core.domStyle.isVertical ? 105 : core._PY_ + 41);

			ctx.save();
			ctx.clearRect(0, 0, w + 8, h + 8);
			ctx.imageSmoothingEnabled = false;
			if (core.domStyle.isVertical) {
				core.plugin._drawPastelTopStatus(ctx, w, h);
			}
			else {
				core.plugin._drawPastelLeftStatus(ctx, w, h);
			}
			ctx.restore();
		};

		this.installMapItemValueDisplay = function () {
			// 将道具加成数值绘制到 damage 层。这里不修改地图图块，只是额外叠字。
			if (core.__mapItemValueDisplayInstalled || !core.control) return;
			core.__mapItemValueDisplayInstalled = true;
			var originUpdateDamage = core.control.updateDamage;
			core.control.updateDamage = function (floorId, ctx) {
				floorId = floorId || core.status.floorId;
				if (!floorId || core.status.gameOver || main.mode != 'play') return;

				var hasBook = false;
				try { hasBook = core.hasItem && core.hasItem('book'); } catch (e) { hasBook = false; }

				// 有怪物手册时保留原显伤；没有手册时也允许只显示道具加成。
				if (hasBook) originUpdateDamage.call(this, floorId, ctx);
				else if (ctx == null) core.clearMap('damage');

				if (core.plugin && core.plugin.drawMapItemValues) {
					core.plugin.drawMapItemValues(floorId, ctx);
				}
			};
		};

		this.getMapItemValueText = function (itemId, floorId) {
			var item = (core.material && core.material.items || {})[itemId];
			if (!item) return null;

			// 只显示血瓶、宝石、装备、钱袋/金币类道具的实际加成；
			// 钥匙、飞行器、跳跃靴等工具不显示。
			var isPotion = /Potion$/.test(itemId);
			var isGem = /Gem$/.test(itemId);
			var isEquip = item.equip != null;
			var isMoneyItem = itemId == 'pack' || /金币|钱袋/.test(String(item.name || '') + String(item.itemEffectTip || '')) || /hero\.money/.test(String(item.itemEffect || ''));
			if (!isPotion && !isGem && !isEquip && !isMoneyItem) return null;

			var value = null;
			try {
				// 血瓶/宝石/钱袋优先从 itemEffectTip 里取运行期真实数值（会自动带楼层倍率）。
				var tip = item.itemEffectTip ? core.replaceText(item.itemEffectTip) : '';
				var match = String(tip).match(/[-+]?\d+(?:\.\d+)?/);
				if (match) value = match[0];
			} catch (e) {}

			if (value == null && item.equip && item.equip.value) {
				// 装备直接读取装备本身的实际加成。若有多个属性，只显示第一个非零数字，避免压住图标。
				var v = item.equip.value;
				var keys = ['hp', 'atk', 'def', 'mdef'];
				for (var i = 0; i < keys.length; i++) {
					var one = v[keys[i]];
					if (one != null && one !== 0) {
						value = String(one);
						break;
					}
				}
			}

			if (value == null && item.itemEffect) {
				// 兜底：从 itemEffect 里取第一个数字，例如钱袋 core.status.hero.money += 500。
				var match2 = String(item.itemEffect).match(/[-+]?\d+(?:\.\d+)?/);
				if (match2) value = match2[0];
			}

			if (value == null) return null;
			return String(value).replace(/\.0$/, '');
		};

		this.getMapItemValueColor = function (itemId) {
			// 血瓶统一用浅绿色；金币/钱袋用金色；其他按属性/道具颜色区分。
			if (/Potion$/.test(itemId)) return '#9cff9c';
			if (itemId == 'pack') return '#ffd45a';
			if (itemId == 'redGem') return '#ff5f5f';
			if (itemId == 'blueGem') return '#66a8ff';
			if (itemId == 'greenGem') return '#60d982';
			if (itemId == 'yellowGem') return '#ffd45a';
			if (/^sword/.test(itemId)) return '#ffd66b';
			if (/^shield/.test(itemId)) return '#8ec8ff';
			return '#fff0a8';
		};

		this.drawMapItemValues = function (floorId, ctx) {
			floorId = floorId || core.status.floorId;
			if (!floorId || !core.status || !core.status.maps || !core.status.maps[floorId]) return;
			var drawCtx = ctx || core.canvas.damage;
			if (!drawCtx) return;
			var onMap = ctx == null;

			core.extractBlocks(floorId);
			var blocks = core.status.maps[floorId].blocks || [];
			drawCtx.save();
			drawCtx.font = 'bold 9px Arial, sans-serif';
			drawCtx.textAlign = 'left';
			drawCtx.textBaseline = 'alphabetic';
			for (var i = 0; i < blocks.length; i++) {
				var block = blocks[i];
				if (!block || block.disable || !block.event || block.event.cls != 'items') continue;
				var itemId = block.event.id;
				var text = this.getMapItemValueText(itemId, floorId);
				if (!text) continue;

				// 左下角小数字，不画大框，不写HP/攻/防等文字。
				var px = 32 * block.x + 2;
				var py = 32 * block.y + 30;
				if (onMap && core.bigmap.v2) {
					px -= core.bigmap.posX * 32;
					py -= core.bigmap.posY * 32;
					if (px < -32 || px > core._PX_ + 32 || py < -32 || py > core._PY_ + 32) continue;
				}

				// 黑色描边保证浅色背景也能看清。
				drawCtx.lineWidth = 2.5;
				drawCtx.strokeStyle = 'rgba(40,28,20,0.85)';
				drawCtx.strokeText(text, px, py);
				drawCtx.fillStyle = this.getMapItemValueColor(itemId);
				drawCtx.fillText(text, px, py);
			}
			drawCtx.restore();
		};

		this._psbHero = function () {
			return (core.status && core.status.hero) || null;
		};

		this._psbFormat = function (value) {
			if (value == null || value === '') return '';
			try {
				return core.formatBigNumber ? core.formatBigNumber(value) : String(value);
			} catch (e) {
				return String(value);
			}
		};

		this._psbHeroStatus = function (name) {
			var hero = this._psbHero();
			if (!hero || hero[name] == null) return '';
			return this._psbFormat(hero[name]);
		};

		this._psbRealStatus = function (name) {
			var hero = this._psbHero();
			if (!hero) return '';
			try {
				return this._psbFormat(core.getRealStatus(name));
			} catch (e) {
				return this._psbHeroStatus(name);
			}
		};

		this._psbFloorName = function () {
			if (!core.status) return '';
			return (core.status.thisMap || {}).name || core.status.floorId || '';
		};

		this._psbItemCount = function (itemId) {
			var hero = this._psbHero();
			if (!hero || !hero.items) return 0;
			try {
				return core.itemCount(itemId) || 0;
			} catch (e) {
				var items = hero.items || {};
				return ((items.tools || {})[itemId] || (items.constants || {})[itemId] || (items.equips || {})[itemId] || 0);
			}
		};

		this._psbDeathChanceLeft = function () {
			if (!core.status || !core.status.floorId) return 1;
			var used = core.getFlag ? core.getFlag('__firstBattleFailReviveUsed__', {}) : {};
			return used[core.status.floorId] ? 0 : 1;
		};

		this._psbGetStatusBarBgImage = function () {
			// 横版状态栏背景图。你只需要把图片放到 project/images/statusbar_bg.png。
			// 不强制依赖 data.js 的 images 注册；如果没注册，也会直接按路径加载。
			var name = 'statusbar_bg.png';
			var images = core.material && core.material.images && core.material.images.images;
			if (images && images[name]) return images[name];

			if (!this.__psbStatusBarBgImage) {
				var img = new Image();
				var self = this;
				img.onload = function () {
					try {
						if (core.plugin && core.plugin.drawPastelStatusBar) core.plugin.drawPastelStatusBar();
					} catch (e) {}
				};
				img.src = 'project/images/' + name + '?v=' + (main.version || '') + '&_=' + Date.now();
				this.__psbStatusBarBgImage = img;
			}
			return this.__psbStatusBarBgImage;
		};

		this._psbDrawImageStatusBarBg = function (ctx, w, h) {
			var img = this._psbGetStatusBarBgImage();
			if (img && img.complete && (img.naturalWidth || img.width) > 0) {
				ctx.drawImage(img, 0, 0, w, h);
				return true;
			}
			// 图片还没加载出来时先给一个暗底，图片 onload 后会自动重绘。
			ctx.fillStyle = '#111';
			ctx.fillRect(0, 0, w, h);
			return false;
		};

		this._psbDrawOverlayNumber = function (ctx, text, x, y, size, color, align) {
			if (text == null || text === '') return;
			text = String(text);
			ctx.save();
			ctx.font = 'bold ' + Math.max(10, Math.round(size)) + 'px Arial, "Microsoft YaHei", "SimHei", sans-serif';
			ctx.textAlign = align || 'center';
			ctx.textBaseline = 'middle';
			ctx.lineJoin = 'round';
			ctx.shadowColor = 'rgba(0,0,0,0.45)';
			ctx.shadowBlur = 2;
			ctx.shadowOffsetY = 1;
			ctx.lineWidth = Math.max(3, Math.round(size / 5));
			ctx.strokeStyle = 'rgba(42, 20, 8, 0.92)';
			ctx.strokeText(text, x, y);
			ctx.fillStyle = color || '#fff1c9';
			ctx.fillText(text, x, y);
			ctx.restore();
		};


		this._psbHideStatusNumbers = function () {
			// 只隐藏 H5 2D 自绘状态栏叠加出来的数字，不改 3D 状态栏，也不改游戏内部数值。
			return true;
		};

		this._drawPastelLeftStatus = function (ctx, w, h) {
			// 横版左侧状态栏：使用图片作为底图，所有数值由 canvas 叠加绘制。
			this._psbDrawImageStatusBarBg(ctx, w, h);

			// 这些坐标按你这张无数字状态栏图的比例定位，图片缩放后仍能对齐。
			// 右侧属性数字：缩小字号，并右对齐到每行右边空位。
			var valueX = w * 0.855;
			var statSize = Math.max(11, Math.min(16, h * 0.034));
			var statColor = '#fff1c9';

			if (!this._psbHideStatusNumbers()) {
				this._psbDrawOverlayNumber(ctx, this._psbFloorName(), valueX, h * 0.153, statSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbRealStatus('hp'), valueX, h * 0.216, statSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbRealStatus('atk'), valueX, h * 0.281, statSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbRealStatus('def'), valueX, h * 0.346, statSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbRealStatus('mdef'), valueX, h * 0.412, statSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbHeroStatus('money'), valueX, h * 0.477, statSize, statColor, 'right');

				// 飞行器 / 跳跃靴数量：缩小，放在各自道具框右下侧，右对齐。
				var toolSize = Math.max(12, Math.min(18, h * 0.039));
				this._psbDrawOverlayNumber(ctx, this._psbItemCount('centerFly') || 0, w * 0.350, h * 0.648, toolSize, statColor, 'right');
				this._psbDrawOverlayNumber(ctx, this._psbItemCount('jumpShoes') || 0, w * 0.715, h * 0.648, toolSize, statColor, 'right');

				// 本层可死亡次数：继续往左，位置保持在底部数字框内。
				var deathSize = Math.max(20, Math.min(34, h * 0.070));
				this._psbDrawOverlayNumber(ctx, this._psbDeathChanceLeft(), w * 0.600, h * 0.908, deathSize, '#ff6a2c', 'right');
			}
		};

		this._drawPastelTopStatus = function (ctx, w, h) {
			// 手机竖屏：顶部横向状态栏。和横屏状态栏保持同一套画风与数据来源。
			ctx.fillStyle = '#f4dfb7';
			ctx.fillRect(0, 0, w, h);
			this._psbBricks(ctx, 0, 0, w, 15, true);
			this._psbBricks(ctx, 0, h - 13, w, 13, false);
			this._psbRoundRect(ctx, 8, 18, w - 16, h - 35, 8);
			ctx.fillStyle = 'rgba(255,249,230,0.76)';
			ctx.fill();
			ctx.strokeStyle = 'rgba(171,138,88,0.45)';
			ctx.stroke();

			var gap = 6;
			var y1 = 24, cellH = 25;
			var cellW = Math.floor((w - 22 - gap * 2) / 3);
			var x0 = 11;
			var self = this;
			var drawMini = function (x, y, ww, icon, label, value) {
				self._psbBox(ctx, x, y, ww, cellH, 5, 'rgba(255,246,221,0.55)');
				self._psbIcon(ctx, icon, x + 5, y + 4);
				self._psbText(ctx, label, x + 25, y + 17, 12, '#5d3a24', 'left', 'bold');
				if (!self._psbHideStatusNumbers()) self._psbText(ctx, String(value || ''), x + ww - 5, y + 17, 12, '#5d3a24', 'right', 'bold');
			};
			drawMini(x0, y1, cellW, 'floor', '楼', this._psbFloorName());
			drawMini(x0 + (cellW + gap), y1, cellW, 'hp', '生', this._psbRealStatus('hp'));
			drawMini(x0 + (cellW + gap) * 2, y1, cellW, 'money', '金', this._psbHeroStatus('money'));

			var y2 = y1 + cellH + 5;
			drawMini(x0, y2, cellW, 'atk', '攻', this._psbRealStatus('atk'));
			drawMini(x0 + (cellW + gap), y2, cellW, 'def', '防', this._psbRealStatus('def'));
			drawMini(x0 + (cellW + gap) * 2, y2, cellW, 'mdef', '魔防', this._psbRealStatus('mdef'));

			var y3 = y2 + cellH + 7;
			var toolW = Math.floor((w - 24 - gap * 2) / 3);
			var drawTool = function (x, itemId) {
				self._psbBox(ctx, x, y3, toolW, 30, 5, 'rgba(255,246,221,0.55)');
				if (!self._psbDrawItemIcon(ctx, itemId, x + 6, y3 + 3, 24)) {
					self._psbIcon(ctx, itemId == 'jumpShoes' ? 'jumpShoes' : 'fly', x + 9, y3 + 6);
				}
				if (!self._psbHideStatusNumbers()) self._psbText(ctx, String(self._psbItemCount(itemId) || 0), x + toolW - 7, y3 + 21, 15, '#5d3a24', 'right', 'bold');
			};
			drawTool(x0, 'centerFly');
			drawTool(x0 + toolW + gap, 'jumpShoes');

			// 本层可死亡次数
			this._psbBox(ctx, x0 + (toolW + gap) * 2, y3, toolW, 30, 5, 'rgba(255,246,221,0.55)');
			this._psbText(ctx, '', x0 + (toolW + gap) * 2 + 8, y3 + 21, 14, '#5d3a24', 'left', 'bold');
			if (!this._psbHideStatusNumbers()) this._psbText(ctx, String(this._psbDeathChanceLeft()), x0 + (toolW + gap) * 2 + toolW - 7, y3 + 22, 18, '#b64b3d', 'right', 'bold');
		};

		this._psbPanel = function (ctx, x, y, w, h) {
			var g = ctx.createLinearGradient(0, 0, w, h);
			g.addColorStop(0, '#fff1cf');
			g.addColorStop(0.55, '#f9e5bd');
			g.addColorStop(1, '#f1d6aa');
			ctx.fillStyle = g;
			ctx.fillRect(x, y, w, h);

			// 砖顶/砖底
			this._psbBricks(ctx, x, y, w, 18, true);
			this._psbBricks(ctx, x, h - 18, w, 18, false);

			// 竹子侧边
			this._psbBamboo(ctx, 3, 16, 7, h - 32);
			this._psbBamboo(ctx, w - 10, 16, 7, h - 32);

			// 纸张内框
			this._psbRoundRect(ctx, 14, 21, w - 28, h - 42, 6);
			ctx.fillStyle = 'rgba(255, 249, 230, 0.70)';
			ctx.fill();
			ctx.strokeStyle = 'rgba(171, 138, 88, 0.45)';
			ctx.lineWidth = 1;
			ctx.stroke();
			this._psbLeaves(ctx, 5, 18, false);
			this._psbLeaves(ctx, w - 26, 18, true);
			this._psbFlower(ctx, 16, h - 27);
		};

		this._psbBricks = function (ctx, x, y, w, h, top) {
			ctx.fillStyle = '#c98565';
			ctx.fillRect(x, y, w, h);
			var bw = 24;
			ctx.lineWidth = 1;
			for (var i = 0; i < Math.ceil(w / bw); i++) {
				var bx = x + i * bw - (top ? 0 : 12);
				var grad = ctx.createLinearGradient(bx, y, bx, y + h);
				grad.addColorStop(0, '#d89b79');
				grad.addColorStop(1, '#aa644a');
				ctx.fillStyle = grad;
				this._psbRoundRect(ctx, bx + 1, y + 1, bw - 2, h - 2, 3);
				ctx.fill();
				ctx.strokeStyle = 'rgba(104,65,44,0.35)';
				ctx.stroke();
			}
		};

		this._psbBamboo = function (ctx, x, y, w, h) {
			var g = ctx.createLinearGradient(x, y, x + w, y);
			g.addColorStop(0, '#7c9151');
			g.addColorStop(0.5, '#d7d889');
			g.addColorStop(1, '#829453');
			ctx.fillStyle = g;
			this._psbRoundRect(ctx, x, y, w, h, 4);
			ctx.fill();
			ctx.strokeStyle = 'rgba(82,89,47,0.65)';
			ctx.stroke();
			for (var yy = y + 28; yy < y + h; yy += 42) {
				ctx.fillStyle = 'rgba(88,99,51,0.35)';
				ctx.fillRect(x - 1, yy, w + 2, 2);
				ctx.fillStyle = 'rgba(255,255,200,0.20)';
				ctx.fillRect(x + 1, yy + 2, w - 2, 1);
			}
		};

		this._psbTitle = function (ctx, x, y, w, h, text) {
			this._psbRoundRect(ctx, x, y, w, h, 8);
			ctx.fillStyle = '#eadfb5';
			ctx.fill();
			ctx.strokeStyle = 'rgba(132,112,70,0.55)';
			ctx.stroke();
			this._psbLeaf(ctx, x + 11, y + h / 2, 0.8, false);
			this._psbLeaf(ctx, x + w - 11, y + h / 2, 0.8, true);
			this._psbText(ctx, text, x + w / 2, y + h - 5, 15, '#5a3823', 'center', 'bold');
		};

		this._psbStatRow = function (ctx, x, y, w, h, row) {
			this._psbIcon(ctx, row.icon, x + 12, y + 6);
			this._psbText(ctx, row.label, x + 32, y + 21, 13, '#5d3a24', 'left', 'bold');
			this._psbText(ctx, String(row.value || ''), x + w - 2, y + 21, 13, '#5d3a24', 'right', 'bold');
			ctx.strokeStyle = 'rgba(173,139,92,0.35)';
			ctx.lineWidth = 1;
			ctx.beginPath();
			ctx.moveTo(x + 5, y + h - 1);
			ctx.lineTo(x + w - 5, y + h - 1);
			ctx.stroke();
			ctx.fillStyle = 'rgba(173,139,92,0.45)';
			ctx.fillRect(x + w / 2 - 1, y + h - 3, 3, 3);
		};


		this._psbDrawItemIcon = function (ctx, itemId, x, y, size) {
			size = size || 32;
			try {
				if (!core.material || !core.material.icons || !core.material.images) return false;
				var icon = core.material.icons.items && core.material.icons.items[itemId];
				var image = core.material.images.items;
				if (icon == null || !image) return false;
				ctx.save();
				ctx.imageSmoothingEnabled = false;
				ctx.drawImage(image, 0, 32 * icon, 32, 32, x, y, size, size);
				ctx.restore();
				return true;
			} catch (e) {
				return false;
			}
		};

		this._psbIcon = function (ctx, type, x, y) {
			ctx.save();
			ctx.translate(x, y);
			ctx.lineWidth = 1.2;
			if (type == 'floor') {
				ctx.fillStyle = '#c99b78'; ctx.strokeStyle = '#80563e';
				for (var i = 0; i < 4; i++) { ctx.fillRect(i * 3, 12 - i * 3, 12 - i * 2, 3); ctx.strokeRect(i * 3, 12 - i * 3, 12 - i * 2, 3); }
			} else if (type == 'hp') {
				ctx.fillStyle = '#ef6d82'; ctx.strokeStyle = '#9d4050';
				ctx.beginPath(); ctx.moveTo(8, 16); ctx.bezierCurveTo(-4, 8, 0, 0, 8, 4); ctx.bezierCurveTo(16, 0, 20, 8, 8, 16); ctx.fill(); ctx.stroke();
				ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.fillRect(5, 4, 4, 3);
			} else if (type == 'atk') {
				ctx.strokeStyle = '#5a65a2'; ctx.fillStyle = '#e9edf8';
				ctx.beginPath(); ctx.moveTo(3,16); ctx.lineTo(15,4); ctx.lineTo(18,6); ctx.lineTo(6,18); ctx.closePath(); ctx.fill(); ctx.stroke();
				ctx.strokeStyle = '#b66d4c'; ctx.beginPath(); ctx.moveTo(3,18); ctx.lineTo(8,13); ctx.stroke();
			} else if (type == 'def') {
				ctx.fillStyle = '#7ba4e5'; ctx.strokeStyle = '#405b9f';
				ctx.beginPath(); ctx.moveTo(8,1); ctx.lineTo(17,4); ctx.lineTo(15,13); ctx.lineTo(8,18); ctx.lineTo(1,13); ctx.lineTo(0,4); ctx.closePath(); ctx.fill(); ctx.stroke();
				ctx.fillStyle = '#d8e9ff'; ctx.fillRect(7,5,2,8); ctx.fillRect(4,8,8,2);
			} else if (type == 'mdef') {
				ctx.fillStyle = '#b179ee'; ctx.strokeStyle = '#7041a5'; ctx.beginPath(); ctx.arc(8,9,8,0,Math.PI*2); ctx.fill(); ctx.stroke();
				ctx.strokeStyle = '#e9d7ff'; ctx.beginPath(); ctx.arc(8,9,4,0.1,Math.PI*1.6); ctx.stroke();
			} else if (type == 'money') {
				ctx.fillStyle = '#e3aa55'; ctx.strokeStyle = '#9d6b2d'; ctx.beginPath(); ctx.arc(8,9,8,0,Math.PI*2); ctx.fill(); ctx.stroke();
				ctx.strokeRect(5,6,6,6);
			} else if (type == 'fly') {
				ctx.fillStyle = '#d7f0ff'; ctx.strokeStyle = '#4c7aa5';
				ctx.beginPath(); ctx.moveTo(2,13); ctx.quadraticCurveTo(8,1,15,13); ctx.quadraticCurveTo(8,9,2,13); ctx.fill(); ctx.stroke();
				ctx.fillStyle = '#f6d67b'; ctx.strokeStyle = '#a07328'; ctx.fillRect(7,4,3,12); ctx.strokeRect(7,4,3,12);
				ctx.fillStyle = '#75a8ff'; ctx.fillRect(5,15,7,2);
			} else if (type == 'jumpShoes') {
				ctx.fillStyle = '#8ab46f'; ctx.strokeStyle = '#4e713d';
				ctx.beginPath(); ctx.moveTo(2,13); ctx.lineTo(10,13); ctx.lineTo(15,16); ctx.lineTo(4,17); ctx.closePath(); ctx.fill(); ctx.stroke();
				ctx.fillStyle = '#d8e9ff'; ctx.fillRect(5,9,5,4);
				ctx.strokeStyle = '#4e713d'; ctx.beginPath(); ctx.moveTo(11,12); ctx.quadraticCurveTo(15,8,18,11); ctx.stroke();
			}
			ctx.restore();
		};

		this._psbKey = function (ctx, x, y, fill, stroke) {
			ctx.save(); ctx.translate(x, y); ctx.fillStyle = fill; ctx.strokeStyle = stroke; ctx.lineWidth = 1.2;
			ctx.beginPath(); ctx.arc(6,5,5,0,Math.PI*2); ctx.fill(); ctx.stroke();
			ctx.fillRect(10,4,11,3); ctx.strokeRect(10,4,11,3);
			ctx.fillRect(17,7,3,4); ctx.fillRect(13,7,3,3);
			ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.fillRect(4,2,3,2);
			ctx.restore();
		};

		this._psbSmallSlot = function (ctx, x, y, w, h) {
			this._psbRoundRect(ctx, x, y, w, h, 3);
			ctx.fillStyle = 'rgba(255,246,221,0.65)'; ctx.fill();
			ctx.strokeStyle = 'rgba(171,138,88,0.45)'; ctx.stroke();
		};

		this._psbEquipRow = function (ctx, x, y, w, h, equip) {
			this._psbRoundRect(ctx, x, y, w, h, 6);
			ctx.fillStyle = 'rgba(255,250,232,0.60)'; ctx.fill();
			ctx.strokeStyle = 'rgba(171,138,88,0.45)'; ctx.stroke();
			this._psbSmallSlot(ctx, x + 5, y + 5, 30, 30);
			var slot = equip.slot || equip.name || '';
			var skill = equip.skill || {};
			this._psbText(ctx, slot, x + 42, y + 17, 12, '#5d3a24', 'left', 'bold');
			this._psbText(ctx, skill.name || '', x + 42, y + 33, 12, '#5d3a24', 'left', 'bold');
			if (skill.current != null && skill.max != null)
				this._psbText(ctx, skill.current + '/' + skill.max, x + w - 5, y + 30, 12, '#5d3a24', 'right', 'bold');
		};

		this._psbDivider = function (ctx, x, y, h) {
			ctx.strokeStyle = 'rgba(174,137,88,0.25)';
			ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + h); ctx.stroke();
			ctx.fillStyle = 'rgba(174,137,88,0.35)'; ctx.fillRect(x - 1, y + h / 2 - 1, 3, 3);
		};

		this._psbBox = function (ctx, x, y, w, h, r, fill) {
			this._psbRoundRect(ctx, x, y, w, h, r || 6);
			ctx.fillStyle = fill || 'rgba(255,250,232,0.55)';
			ctx.fill();
			ctx.strokeStyle = 'rgba(171,138,88,0.42)';
			ctx.lineWidth = 1;
			ctx.stroke();
		};

		this._psbText = function (ctx, text, x, y, size, color, align, weight) {
			ctx.textAlign = align || 'left';
			ctx.textBaseline = 'alphabetic';
			ctx.font = (weight ? weight + ' ' : '') + size + 'px "Microsoft YaHei", "PingFang SC", SimHei, sans-serif';
			ctx.lineWidth = 3;
			ctx.strokeStyle = 'rgba(255,248,224,0.82)';
			ctx.strokeText(text, x, y);
			ctx.fillStyle = color || '#5d3a24';
			ctx.fillText(text, x, y);
		};

		this._psbRoundRect = function (ctx, x, y, w, h, r) {
			r = Math.min(r || 0, w / 2, h / 2);
			ctx.beginPath();
			ctx.moveTo(x + r, y);
			ctx.lineTo(x + w - r, y);
			ctx.quadraticCurveTo(x + w, y, x + w, y + r);
			ctx.lineTo(x + w, y + h - r);
			ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
			ctx.lineTo(x + r, y + h);
			ctx.quadraticCurveTo(x, y + h, x, y + h - r);
			ctx.lineTo(x, y + r);
			ctx.quadraticCurveTo(x, y, x + r, y);
			ctx.closePath();
		};

		this._psbLeaf = function (ctx, x, y, s, flip) {
			ctx.save(); ctx.translate(x, y); ctx.scale(flip ? -s : s, s); ctx.fillStyle = '#9bab67';
			ctx.beginPath(); ctx.moveTo(0,0); ctx.quadraticCurveTo(8,-5,12,0); ctx.quadraticCurveTo(6,5,0,0); ctx.fill(); ctx.restore();
		};

		this._psbLeaves = function (ctx, x, y, flip) {
			ctx.save(); ctx.translate(x, y); if (flip) ctx.scale(-1, 1);
			for (var i = 0; i < 4; i++) { this._psbLeaf(ctx, i * 4, i * 4, 0.7, false); }
			ctx.restore();
		};

		this._psbFlower = function (ctx, x, y) {
			ctx.save(); ctx.translate(x, y); ctx.fillStyle = '#e99092';
			for (var i = 0; i < 5; i++) { ctx.beginPath(); ctx.arc(Math.cos(i*1.26)*4, Math.sin(i*1.26)*4, 3, 0, Math.PI*2); ctx.fill(); }
			ctx.fillStyle = '#ffd36b'; ctx.beginPath(); ctx.arc(0,0,2,0,Math.PI*2); ctx.fill(); ctx.restore();
		};
	},

	"shop": function () {
		// 【全局商店】相关的功能
		// 
		// 打开一个全局商店
		// shopId：要打开的商店id；noRoute：是否不计入录像
		this.openShop = function (shopId, noRoute) {
			var shop = core.status.shops[shopId];
			// Step 1: 检查能否打开此商店
			if (!this.canOpenShop(shopId)) {
				core.drawTip("该商店尚未开启");
				return false;
			}

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

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

			_shouldProcessKeyUp = true;

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

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

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

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

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

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

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

		/// 能否使用快捷商店
		this.canUseQuickShop = function (id) {
			// 如果返回一个字符串，表示不能，字符串为不能使用的提示
			// 返回null代表可以使用

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

		var _shouldProcessKeyUp = true;

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

			if (!core.hasFlag("@temp@shop") || core.status.event.data.type != 'choices') return false;
			var data = core.status.event.data.current;
			var choices = data.choices;
			var topIndex = core.actions._getChoicesTopIndex(choices.length);
			if (keycode == 88 || keycode == 27) { // X, ESC
				core.actions._clickAction(core._HALF_WIDTH_ || core.__HALF_SIZE__, 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._HALF_WIDTH_ || core.__HALF_SIZE__, 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 (Math.abs(x - (core._HALF_WIDTH_ || core.__HALF_SIZE__)) <= 2 && y >= topIndex && y < topIndex + choices.length) {
				core.actions._clickAction(x, y);
				return true;
			}
			return false;
		}, 60);
	},
	"removeMap": function () {
		// 高层塔砍层插件，删除后不会存入存档，不可浏览地图也不可飞到。
		// 推荐用法：
		// 对于超高层或分区域塔，当在1区时将2区以后的地图删除；1区结束时恢复2区，进二区时删除1区地图，以此类推
		// 这样可以大幅减少存档空间，以及加快存读档速度

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

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

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

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

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

		var bg2Canvas = createCanvas('bg2', 20);
		var 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之间)
			document.getElementById('mapEdit').insertBefore(bg2Canvas, document.getElementById('event'));
			// 前景层2(fg2) 插入编辑器前景(efg)之前(即fg之后)
			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');

			// 创建编辑器上的按钮
			var createCanvasBtn = function (name) {
				// 电脑端创建按钮
				var input = document.createElement('input');
				// layerMod4/layerMod5
				var id = 'layerMod' + num++;
				// bg2map/fg2map
				var 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;
			};

			var createCanvasBtn_mobile = function (name) {
				// 手机端往选择列表中添加子选项
				var input = document.createElement('option');
				var id = 'layerMod' + num++;
				var value = name + 'map';
				input.name = 'layerMod';
				input.value = value;
				editor.dom[id] = input;
				return input;
			};
			if (!editor.isMobile) {
				var input = createCanvasBtn('bg2');
				var input2 = createCanvasBtn('fg2');
				// 获取事件层及其父节点
				var child = document.getElementById('layerMod'),
					parent = child.parentNode;
				// 背景层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 {
				var input = createCanvasBtn_mobile('bg2');
				var input2 = createCanvasBtn_mobile('fg2');
				// 手机端因为是选项 所以可以直接改innerText
				input.innerText = '背景层2';
				input2.innerText = '前景层2';
				var parent = document.getElementById('layerMod');
				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)
			};
		}
	},
	"itemShop": function () {
		// 道具商店相关的插件
		// 可在全塔属性-全局商店中使用「道具商店」事件块进行编辑（如果找不到可以在入口方块中找）

		var shopId = null; // 当前商店ID
		var type = 0; // 当前正在选中的类型，0买入1卖出
		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 () { }
			// 记录当前录像，因为可能存在换装问题
			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 == "floorId");
				else 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 = true;
		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;
		}
	},
	"routeFixing": function () {
		// 是否开启本插件，true 表示启用，false 表示禁用。
		var __enable = true;
		if (!__enable) return;
		/*
		 使用说明：启用本插件后，录像回放时您可以用数字键1或6分别切换到原速或24倍速，
		 暂停播放时按数字键7（电脑按N）可以单步播放。（手机端可以点击难度单词切换出数字键）
		 数字键2-5可以进行录像自助精修，具体描述见下（实际弹窗请求您输入时不要带有任何空格）：
		 
		 up down left right 勇士向某个方向「行走一步或撞击」
		 item:ID 使用某件道具，如 item:bomb 表示使用炸弹
		 unEquip:n 卸掉身上第(n+1)件装备（n从0开始），如 unEquip:1 默认表示卸掉盾牌
		 equip:ID 穿上某件装备，如 equip:sword1 表示装上铁剑
		 saveEquip:n 将身上的当前套装保存到第n套快捷套装（n从0开始）
		 loadEquip:n 快捷换上之前保存好的第n套套装
		 fly:ID 使用楼传飞到某一层，如 fly:MT10 表示飞到主塔10层
		 choices:none 确认框/选择项「超时」（作者未设置超时时间则此项视为缺失）
		 choices:n 确认框/选择项选择第(n+1)项（选择项n从0开始，确认框n为0表示「确定」，1表示「取消」）
		 选择项n为负数时表示选择倒数第 -n 项，如 -1 表示最后一项（V2.8.2起标准全局商店的「离开」项）
		 此项缺失的话，确认框将选择作者指定的默认项（初始光标位置），选择项将弹窗请求补选（后台录像验证中选最后一项，可以复写函数来修改）
		 shop:ID 打开某个全局商店，如 shop:itemShop 表示打开道具商店。因此连载塔千万不要中途修改商店ID！
		 turn 单击勇士（Z键）转身，core.turnHero() 会产生此项，因此通过事件等方式强制让勇士转向应该用 core.setHeroLoc()
		 turn:dir 勇士转向某个方向，dir 可以为 up down left right（此项一般是读取自动存档产生的，属于样板的不良特性，请勿滥用）
		 getNext 轻按获得身边道具，优先获得面前的（面前没有则按上下左右顺序依次获得），身边如果没有道具则此项会被跳过
		 input:none “等待用户操作事件”中超时（作者未设置超时时间则此项会导致报错）
		 input:xxx 可能表示“等待用户操作事件”的一个操作（如按键操作将直接记录 input:keycode ），
		 也可能表示一个“接受用户输入数字”的输入，后者的情况下 xxx 为输入的整数。此项缺失的话前者将直接报错，后者将用0代替（后者现在支持负数了）
		 input2:xxx 可能表示“读取全局存储（core.getGlobal）”读取到的值，也可能表示一个“接受用户输入文本”的输入，
		 两种情况下 xxx 都为 base64 编码。此项缺失的话前者将重新现场读取，后者将用空字符串代替
		 no 走到可穿透的楼梯上不触发楼层切换事件，通过本插件可以让勇士停在旁边没有障碍物的楼梯上哦～
		 move:x:y 尝试瞬移到 [x,y] 点（不改变朝向），该点甚至可以和勇士相邻或者位于视野外
		 key:n 松开键值为n的键，如 key:49 表示松开大键盘数字键1，默认会触发使用破墙镐
		 click:n:px:py 点击自绘状态栏，n为0表示横屏1表示竖屏，[px,py] 为点击的像素坐标
		 random:n 生成了随机数n，即 core.rand2(num) 的返回结果，n必须在 [0,num-1] 范围，num必须为正整数。此项缺失将导致现场重新随机生成数值，可能导致回放结果不一致！
		 作者自定义的新项（一般为js对象，可以先JSON.stringify()再core.encodeBase64()得到纯英文数字的内容）需要用(半角圆括弧)括起来。
		 
		 当您使用数字键5将一些项追加到即将播放内容的开头时，请注意要逆序逐项追加，或者每追加一项就按下数字键7或字母键N单步播放一步。
		 但是【input input2 random choices】是被动读取的，单步播放如果触发了相应的事件就会连续读取，这时候只能提前逐项追加好。
		 电脑端熟练以后推荐直接在控制台操作 core.status.route 和 core.status.replay.toReplay（后者录像回放时才有），配合 core.push() 和 core.unshift() 更加灵活自由哦！
		 */
		core.actions.registerAction('onkeyUp', '_sys_onkeyUp_replay', function (e) {
			if (this._checkReplaying()) {
				if (e.keyCode == 27) // ESCAPE
					core.stopReplay();
				else if (e.keyCode == 90) // Z
					core.speedDownReplay();
				else if (e.keyCode == 67) // C
					core.speedUpReplay();
				else if (e.keyCode == 32) // SPACE
					core.triggerReplay();
				else if (e.keyCode == 65) // A
					core.rewindReplay();
				else if (e.keyCode == 83) // S
					core.control._replay_SL();
				else if (e.keyCode == 88) // X
					core.control._replay_book();
				else if (e.keyCode == 33 || e.keyCode == 34) // PgUp/PgDn
					core.control._replay_viewMap();
				else if (e.keyCode == 78) // N
					core.stepReplay();
				else if (e.keyCode == 84) // T
					core.control._replay_toolbox();
				else if (e.keyCode == 81) // Q
					core.control._replay_equipbox();
				else if (e.keyCode == 66) // B
					core.ui._drawStatistics();
				else if (e.keyCode == 49 || e.keyCode == 54) // 1/6，原速/24倍速播放
					core.setReplaySpeed(e.keyCode == 49 ? 1 : 24);
				else if (e.keyCode > 49 && e.keyCode < 54) { // 2-5，录像精修
					switch (e.keyCode - 48) {
						case 2: // pop
							alert("您已移除已录制内容的最后一项：" + core.status.route.pop());
							break;
						case 3: // push
							core.utils.myprompt("请输入您要追加到已录制内容末尾的项：", "", function (value) {
								if (value != null) core.status.route.push(value);
							});
							break;
						case 4: // shift
							alert("您已移除即将播放内容的第一项：" + core.status.replay.toReplay.shift());
							break;
						case 5: // unshift
							core.utils.myprompt("请输入您要追加到即将播放内容开头的项：", "", function (value) {
								if (value != null) core.status.replay.toReplay.unshift(value);
							});
					}
				}
				return true;
			}
		}, 100);
	},
	"numpad": function () {
		// 样板自带的整数输入事件为白屏弹窗且可以误输入任意非法内容但不支持负整数，观感较差。本插件可以将其美化成仿RM样式，使其支持负整数同时带有音效
		// 另一方面，4399等第三方平台不允许使用包括 core.myprompt() 和 core.myconfirm() 在内的弹窗，因此也需要此插件来替代，不然类似生命魔杖的道具就不好实现了
		// 关于负整数输入，V2.8.2原生支持其录像的压缩和解压，只是默认的 core.events._action_input() 函数将负数取了绝对值，可以只复写下面的 core.isReplaying() 部分来取消

		// 是否启用本插件，false表示禁用，true表示启用
		var __enable = true;
		if (!__enable) return;

		core.events._action_input = function (data, x, y, prefix) { // 复写整数输入事件
			if (core.isReplaying()) { // 录像回放时，处理方式不变，但增加负整数支持
				core.events.__action_getInput(core.replaceText(data.text, prefix), false, function (value) {
					value = parseInt(value) || 0; // 去掉了取绝对值的步骤
					core.status.route.push("input:" + value);
					core.setFlag("input", value);
					core.doAction();
				});
			} else {
				// 正常游戏中，采用暂停录制的方式然后用事件流循环“绘制-等待-变量操作”三板斧实现（按照13*13适配的）。
				// 您可以自行修改循环内的内容来适配15*15或其他需求，或干脆作为公共事件编辑。
				core.insertAction([
					// 记录当前录像长度，下面的循环结束后裁剪。达到“暂停录制”的效果
					{ "type": "function", "function": "function(){flags['@temp@length']=core.status.route.length}" },
					{ "type": "setValue", "name": "flag:input", "value": "0" },
					{
						"type": "while",
						"condition": "true",
						"data": [
							{ "type": "drawBackground", "background": "winskin.png", "x": 16, "y": 16, "width": 384, "height": 384 },
							{ "type": "drawIcon", "id": "X10181", "x": 32, "y": 288 },
							{ "type": "drawIcon", "id": "X10185", "x": 64, "y": 288 },
							{ "type": "drawIcon", "id": "X10186", "x": 96, "y": 288 },
							{ "type": "drawIcon", "id": "X10187", "x": 128, "y": 288 },
							{ "type": "drawIcon", "id": "X10188", "x": 160, "y": 288 },
							{ "type": "drawIcon", "id": "X10189", "x": 192, "y": 288 },
							{ "type": "drawIcon", "id": "X10193", "x": 224, "y": 288 },
							{ "type": "drawIcon", "id": "X10194", "x": 256, "y": 288 },
							{ "type": "drawIcon", "id": "X10195", "x": 288, "y": 288 },
							{ "type": "drawIcon", "id": "X10196", "x": 320, "y": 288 },
							{ "type": "drawIcon", "id": "X10197", "x": 352, "y": 288 },
							{ "type": "drawIcon", "id": "X10286", "x": 32, "y": 352 },
							{ "type": "drawIcon", "id": "X10169", "x": 96, "y": 352 },
							{ "type": "drawIcon", "id": "X10232", "x": 128, "y": 352 },
							{ "type": "drawIcon", "id": "X10185", "x": 320, "y": 352 },
							{ "type": "drawIcon", "id": "X10242", "x": 352, "y": 352 },
							{ "type": "fillBoldText", "x": 48, "y": 256, "style": [255, 255, 255, 1], "font": "bold 32px Consolas", "text": "${flag:input}" },
							{ "type": "fillBoldText", "x": 32, "y": 48, "style": [255, 255, 255, 1], "font": "16px Consolas", "text": core.replaceText(data.text, prefix) },
							{
								"type": "wait",
								"forceChild": true,
								"data": [{
									"case": "keyboard",
									"keycode": "48,49,50,51,52,53,54,55,56,57",
									"action": [
										// 按下数字键，追加到已输入内容的末尾，但禁止越界。变量：keycode-48就是末位数字
										{ "type": "playSound", "name": "光标移动" },
										{
											"type": "if",
											"condition": "(flag:input<0)",
											"true": [
												{ "type": "setValue", "name": "flag:input", "value": "10*flag:input-(flag:keycode-48)" },
											],
											"false": [
												{ "type": "setValue", "name": "flag:input", "value": "10*flag:input+(flag:keycode-48)" },
											]
										},
										{ "type": "setValue", "name": "flag:input", "value": "core.clamp(flag:input,-9e15,9e15)" },
									]
								},
								{
									"case": "keyboard",
									"keycode": "189",
									"action": [
										// 按下减号键，变更已输入内容的符号
										{ "type": "playSound", "name": "跳跃" },
										{ "type": "setValue", "name": "flag:input", "value": "-flag:input" },
									]
								},
								{
									"case": "keyboard",
									"keycode": "8",
									"action": [
										// 按下退格键，从已输入内容的末尾删除一位
										{ "type": "playSound", "name": "取消" },
										{ "type": "setValue", "name": "flag:input", "operator": "//=", "value": "10" },
									]
								},
								{
									"case": "keyboard",
									"keycode": "27",
									"action": [
										// 按下ESC键，清空已输入内容
										{ "type": "playSound", "name": "读档" },
										{ "type": "setValue", "name": "flag:input", "value": "0" },
									]
								},
								{
									"case": "keyboard",
									"keycode": "13",
									"action": [
										// 按下回车键，确定
										{ "type": "break", "n": 1 },
									]
								},
								{
									"case": "mouse",
									"px": [32, 63],
									"py": [288, 320],
									"action": [
										// 点击减号，变号。右边界写63防止和下面重叠
										{ "type": "playSound", "name": "跳跃" },
										{ "type": "setValue", "name": "flag:input", "value": "-flag:input" },
									]
								},
								{
									"case": "mouse",
									"px": [64, 384],
									"py": [288, 320],
									"action": [
										// 点击数字，追加到已输入内容的末尾，但禁止越界。变量：x-2就是末位数字
										{ "type": "playSound", "name": "光标移动" },
										{
											"type": "if",
											"condition": "(flag:input<0)",
											"true": [
												{ "type": "setValue", "name": "flag:input", "value": "10*flag:input-(flag:x-2)" },
											],
											"false": [
												{ "type": "setValue", "name": "flag:input", "value": "10*flag:input+(flag:x-2)" },
											]
										},
										{ "type": "setValue", "name": "flag:input", "value": "core.clamp(flag:input,-9e15,9e15)" },
									]
								},
								{
									"case": "mouse",
									"px": [32, 64],
									"py": [352, 384],
									"action": [
										// 点击左箭头，退格
										{ "type": "playSound", "name": "取消" },
										{ "type": "setValue", "name": "flag:input", "operator": "//=", "value": "10" },
									]
								},
								{
									"case": "mouse",
									"px": [96, 160],
									"py": [352, 384],
									"action": [
										// 点击CE，清空
										{ "type": "playSound", "name": "读档" },
										{ "type": "setValue", "name": "flag:input", "value": "0" },
									]
								},
								{
									"case": "mouse",
									"px": [320, 384],
									"py": [352, 384],
									"action": [
										// 点击OK，确定
										{ "type": "break", "n": 1 },
									]
								}
								]
							}
						]
					},
					{ "type": "clearMap" },
					// 裁剪录像，只保留'input:n'，然后继续录制
					{ "type": "function", "function": "function(){core.status.route.splice(flags['@temp@length']);core.status.route.push('input:'+core.getFlag('input',0))}" }
				], x, y);
				core.events.doAction();
			}
		}
	},
	"leopardLeapSelector": function () {
		// 豹跃落点选择：地图半黑 + 4个黄色可点击框。
		this.installLeopardLeapSelector = function () {
			if (core.__leopardLeapSelectorInstalled || !core.actions || !core.actions.registerAction) return;
			core.__leopardLeapSelectorInstalled = true;
			core.actions.registerAction('ondown', '_user_leopardLeapSelector', function (x, y, px, py) {
				if (core.control && core.control.controldata && core.control.controldata.handleLeopardLeapClick) {
					return core.control.controldata.handleLeopardLeapClick(x, y, px, py);
				}
				return false;
			}, 90);
		};

		this.drawLeopardLeapSelector = function () {
			var data = core.status && core.status.__leopardLeapSelector;
			if (!data || !data.active) {
				try { core.clearMap('leopardLeap'); } catch (e) {}
				return;
			}
			var w = core._PX_ || core.__PIXELS__ || 416;
			var h = core._PY_ || core.__PIXELS__ || 416;
			var ctx = core.getContextByName && core.getContextByName('leopardLeap');
			if (!ctx) ctx = core.createCanvas('leopardLeap', 0, 0, w, h, 180);
			if (!ctx) return;
			ctx.canvas.width = w;
			ctx.canvas.height = h;
			ctx.imageSmoothingEnabled = false;
			core.clearMap('leopardLeap');

			ctx.save();
			ctx.fillStyle = 'rgba(0,0,0,0.45)';
			ctx.fillRect(0, 0, w, h);
			var posX = (core.bigmap && core.bigmap.posX) || 0;
			var posY = (core.bigmap && core.bigmap.posY) || 0;
			var targets = data.targets || [];
			for (var i = 0; i < targets.length; i++) {
				var loc = targets[i].loc || [];
				var sx = (loc[0] - posX) * 32;
				var sy = (loc[1] - posY) * 32;
				if (sx < -32 || sy < -32 || sx > w || sy > h) continue;
				ctx.fillStyle = 'rgba(255, 220, 40, 0.22)';
				ctx.fillRect(sx + 2, sy + 2, 28, 28);
				ctx.lineWidth = 3;
				ctx.strokeStyle = 'rgba(255, 230, 38, 0.98)';
				ctx.strokeRect(sx + 2.5, sy + 2.5, 27, 27);
				ctx.lineWidth = 1;
				ctx.strokeStyle = 'rgba(80, 45, 0, 0.8)';
				ctx.strokeRect(sx + 5.5, sy + 5.5, 21, 21);
			}
			ctx.restore();
		};
	},


	"sprites": function () {
		// 基于canvas的sprite化，摘编整理自万宁魔塔
		// 
		// ---------------------------------------- 第一部分 js代码 （必装） --------------------------------------- //

		/* ---------------- 用法说明 ---------------- *
		 * 1. 创建sprite: var 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();
		 * ---------------- 用法说明 ---------------- */

		var count = 0;

		/** 创建一个sprite画布
		 * @param {number} x
		 * @param {number} y
		 * @param {number} w
		 * @param {number} h
		 * @param {number} z
		 * @param {'game' | 'window'} reference 参考系，游戏画面或者窗口
		 * @param {string} name 可选，sprite的名称，方便通过core.dymCanvas获取
		 */
		function Sprite (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;
			this.canvas = null;
			this.context = null;
			this.count = 0;
			this.name = name || '_sprite_' + count;
			this.style = null;
			/** 初始化 */
			this.init = function () {
				if (reference === 'window') {
					var canvas = document.createElement('canvas');
					this.canvas = canvas;
					this.context = canvas.getContext('2d');
					canvas.width = w;
					canvas.height = h;
					canvas.style.width = w + 'px';
					canvas.style.height = h + 'px';
					canvas.style.position = 'absolute';
					canvas.style.top = y + 'px';
					canvas.style.left = x + 'px';
					canvas.style.zIndex = z.toString();
					document.body.appendChild(canvas);
					this.style = canvas.style;
				} else {
					this.context = core.createCanvas(this.name || '_sprite_' + count, x, y, w, h, z);
					this.canvas = this.context.canvas;
					this.canvas.style.pointerEvents = 'auto';
					this.style = this.canvas.style;
				}
				this.count = count;
				count++;
			}
			this.init();

			/** 设置css特效
			 * @param {string} css
			 */
			this.setCss = function (css) {
				css = css.replace('\n', ';').replace(';;', ';');
				var effects = css.split(';');
				var self = this;
				effects.forEach(function (v) {
					var content = v.split(':');
					var name = content[0];
					var value = content[1];
					name = name.trim().split('-').reduce(function (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);
					}, '');
					var canvas = self.canvas;
					if (name in canvas.style) canvas.style[name] = value;
				});
				return this;
			}

			/** 
			 * 移动sprite
			 * @param {boolean} isDelta 是否是相对位置，如果是，那么sprite会相对于原先的位置进行移动
			 */
			this.move = function (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;
			}

			/** 
			 * 重新设置sprite的大小
			 * @param {boolean} styleOnly 是否只修改css效果，如果是，那么将会不高清，如果不是，那么会清空画布
			 */
			this.resize = function (w, h, styleOnly) {
				if (w !== undefined && w !== null) this.w = w;
				if (h !== undefined && h !== null) this.h = h;
				if (reference === 'window') {
					var 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;
			}

			/**
			 * 旋转画布
			 */
			this.rotate = function (angle, cx, cy) {
				if (this.reference === 'window') {
					var left = this.x;
					var 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;
			}

			/**
			 * 清除sprite
			 */
			this.clear = function (x, y, w, h) {
				if (this.reference === 'window') {
					this.context.clearRect(x, y, w, h);
				} else {
					core.clearMap(this.context, x, y, w, h);
				}
				return this;
			}

			/** 删除 */
			this.destroy = function () {
				if (this.reference === 'window') {
					if (this.canvas) document.body.removeChild(this.canvas);
				} else {
					core.deleteCanvas(this.name || '_sprite_' + this.count);
				}
			}

			/** 添加事件监听器 */
			this.addEventListener = function () {
				this.canvas.addEventListener.apply(this.canvas, arguments);
			}

			/** 移除事件监听器 */
			this.removeEventListener = function () {
				this.canvas.removeEventListener.apply(this.canvas, arguments);
			}
		}

		window.Sprite = Sprite;
	},
	"hotReload": function () {
		/* ---------- 功能说明 ---------- *

		1. 当 libs/ main.js index.html 中的任意一个文件被更改后，会自动刷新塔的页面
		2. 修改楼层文件后自动在塔的页面上显示出来，不需要刷新
		3. 修改脚本编辑或插件编写后也能自动更新更改的插件或脚本，但不保证不会出问题（一般都不会有问题的
		4. 修改图块属性、怪物属性等后会自动更新
		5. 当全塔属性被修改时，会自动刷新塔的页面
		6. 样板的 styles.css 被修改后也可以直接显示，不需要刷新
		7. 其余内容修改后不会自动更新也不会刷新

		/* ---------- 使用方式 ---------- *

		1. 前往 https://nodejs.org/en/ 下载node.js的LTS版本（点左边那个绿色按钮）并安装
		2. 将该插件复制到插件编写中
		3. 在造塔群的群文件-魔塔样板·改中找到server.js，下载并放到塔的根目录（与启动服务同一级）
		4. 在该目录下按下shift+鼠标右键（win11只按右键即可），选择在终端打开或在powershell打开
		5. 运行node server.js即可

		*/

		if (main.mode !== 'play' || main.replayChecking) return;

		/**
		 * 发送请求
		 * @param {string} url
		 * @param {string} type
		 * @param {string} data
		 * @returns {Promise<string>}
		 */
		async function post(url, type, data) {
			const xhr = new XMLHttpRequest();
			xhr.open(type, url);
			xhr.send(data);
			const res = await new Promise(res => {
				xhr.onload = e => {
					if (xhr.status !== 200) {
						console.error(`hot reload: http ${xhr.status}`);
						res('@error');
					} else res('success');
				};
				xhr.onerror = e => {
					res('@error');
					console.error(`hot reload: error on connection`);
				};
			});
			if (res === 'success') return xhr.response;
			else return '@error';
		}

		/**
		 * 热重载css
		 * @param {string} data
		 */
		function reloadCss(data) {
			const all = Array.from(document.getElementsByTagName('link'));
			all.forEach(v => {
				if (v.rel !== 'stylesheet') return;
				if (v.href === `http://127.0.0.1:3000/${data}`) {
					v.remove();
					const link = document.createElement('link');
					link.rel = 'stylesheet';
					link.type = 'text/css';
					link.href = data;
					document.head.appendChild(link);
					console.log(`css hot reload: ${data}`);
				}
			});
		}

		/**
		 * 热重载楼层
		 * @param {string} data
		 */
		async function reloadFloor(data) {
			// 首先重新加载main.floors对应的楼层
			await import(`/project/floors/${data}.js?v=${Date.now()}`);
			// 然后写入core.floors并解析
			core.floors[data] = main.floors[data];
			const floor = core.loadFloor(data);
			if (core.isPlaying()) {
				core.status.maps[data] = floor;
				delete core.status.mapBlockObjs[data];
				core.extractBlocks(data);
				if (data === core.status.floorId) {
					core.drawMap(data);
					core.setWeather(
						core.animateFrame.weather.type,
						core.animateFrame.weather.level
					);
				}
				core.updateStatusBar(true, true);
			}
			console.log(`floor hot reload: ${data}`);
		}

		/**
		 * 热重载脚本编辑及插件编写
		 * @param {string} data
		 */
		async function reloadScript(data) {
			if (data === 'plugins') {
				// 插件编写比较好办
				const before = plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1;
				// 这里不能用动态导入，因为动态导入会变成模块，变量就不是全局的了
				const script = document.createElement('script');
				script.src = `/project/plugins.js?v=${Date.now()}`;
				document.body.appendChild(script);
				await new Promise(res => {
					script.onload = () => res('success');
				});
				const after = plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1;
				// 找到差异的函数
				for (const id in before) {
					const fn = before[id];
					if (typeof fn !== 'function') continue;
					if (fn.toString() !== after[id]?.toString()) {
						try {
							core.plugin[id] = after[id];
							core.plugin[id].call(core.plugin);
							core.updateStatusBar(true, true);
							console.log(`plugin hot reload: ${id}`);
						} catch (e) {
							console.error(e);
						}
					}
				}
			} else if (data === 'functions') {
				// 脚本编辑略微麻烦点
				const before = functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a;
				// 这里不能用动态导入，因为动态导入会变成模块，变量就不是全局的了
				const script = document.createElement('script');
				script.src = `/project/functions.js?v=${Date.now()}`;
				document.body.appendChild(script);
				await new Promise(res => {
					script.onload = () => res('success');
				});
				const after = functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a;
				// 找到差异的函数
				for (const mod in before) {
					const fns = before[mod];
					for (const id in fns) {
						const fn = fns[id];
						if (typeof fn !== 'function' || id === 'hasSpecial')
							continue;
						const now = after[mod][id];
						if (fn.toString() !== now.toString()) {
							try {
								if (mod === 'events') {
									core.events.eventdata[id] = now;
								} else if (mod === 'enemys') {
									core.enemys.enemydata[id] = now;
								} else if (mod === 'actions') {
									core.actions.actionsdata[id] = now;
								} else if (mod === 'control') {
									core.control.controldata[id] = now;
								} else if (mod === 'ui') {
									core.ui.uidata[id] = now;
								}
								core.updateStatusBar(true, true);
								console.log(
									`function hot reload: ${mod}.${id}`
								);
							} catch (e) {
								console.error(e);
							}
						}
					}
				}
			}
		}

		/**
		 * 属性热重载，包括全塔属性等
		 * @param {string} data
		 */
		async function reloadData(data) {
			const script = document.createElement('script');
			script.src = `/project/${data}.js?v=${Date.now()}`;
			document.body.appendChild(script);
			await new Promise(res => {
				script.onload = () => res('success');
			});

			let after;
			if (data === 'data')
				after = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
			if (data === 'enemys')
				after = enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80;
			if (data === 'icons')
				after = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
			if (data === 'items')
				after = items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a;
			if (data === 'maps')
				after = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
			if (data === 'events')
				after = events_c12a15a8_c380_4b28_8144_256cba95f760;

			if (data === 'enemys') {
				core.enemys.enemys = after;
				for (var enemyId in after) {
					core.enemys.enemys[enemyId].id = enemyId;
				}
				core.material.enemys = core.getEnemys();
			} else if (data === 'icons') {
				core.icons.icons = after;
				core.material.icons = core.getIcons();
			} else if (data === 'items') {
				core.items.items = after;
				for (var itemId in after) {
					core.items.items[itemId].id = itemId;
				}
				core.material.items = core.getItems();
			} else if (data === 'maps') {
				core.maps.blocksInfo = after;
				core.status.mapBlockObjs = {};
				core.status.number2block = {};
				Object.values(core.status.maps).forEach(v => delete v.blocks);
				core.extractBlocks();
				core.setWeather(
					core.animateFrame.weather.type,
					core.animateFrame.weather.level
				);
				core.drawMap();
			} else if (data === 'events') {
				core.events.commonEvent = after.commonEvent;
			} else if (data === 'data') {
				location.reload();
			}
			core.updateStatusBar(true, true);
			console.log(`data hot reload: ${data}`);
		}

		// 初始化
		(async function () {
			const data = await post('/reload', 'POST', 'test');
			if (data === '@error') {
				console.log(`未检测到node服务，热重载插件将无法使用`);
			} else {
				console.log(`热重载插件加载成功`);
				// reload
				setInterval(async () => {
					const res = await post('/reload', 'POST');
					if (res === '@error') return;
					if (res === 'true') location.reload();
					else return;
				}, 1000);

				// hot reload
				setInterval(async () => {
					const res = await post('/hotReload', 'POST');
					const data = res.split('@@');
					data.forEach(v => {
						if (v === '') return;
						const [type, file] = v.split(':');
						if (type === 'css') reloadCss(file);
						if (type === 'data') reloadData(file);
						if (type === 'floor') reloadFloor(file);
						if (type === 'script') reloadScript(file);
					});
				}, 1000);
			}
		})();
	}
}
;
// ============================================================================
// Mota3D：HUD 工具/技能（3D 友好版）
//
// 共 4 个 HUD 槽，玩家点击 Godot 状态栏触发：
//   I342  锚定术    选地图空格 → 本层受伤达到阈值时传送回锚点
//   I343  错位术    选地图点  → 上下/左右各取第一个非空块互换
//   pickaxe  破墙   破面前墙   → 破除该墙
//   centerFly 飞行 → 立即传送到中心对称点
//
// 选点流程：
//   1. Godot 槽位点击 → JS 调对应入口函数（如 anchorPlace）
//   2. 入口设置 flag:skill_selecting='anchor'，桥接传给 Godot
//   3. Godot 在 HUD 显示提示，等待玩家点 3D 地图
//   4. 玩家点 3D 地图 → Godot 调 Confirm 函数（如 anchorPlaceConfirm(x,y)）
//   5. Confirm 应用效果 + 清掉 selecting flag
//
// 没用 mota 的 canvas 选点 UI，所以 H5 隐藏也不影响。
// ============================================================================
;
(function () {
    function installSkills() {
        if (!window.core || !core.plugin || !core.status) {
            setTimeout(installSkills, 100);
            return;
        }
        var P = core.plugin;
        if (P.__mota3dSkillsInstalled__) return;
        P.__mota3dSkillsInstalled__ = true;

        // ---------- 共用：进入选点模式 / 取消 ----------
        // 幂等且 toggle：
        //   - 当前没在选点：开始
        //   - 当前在选同一个：再点一次 = 取消（用户要求）
        //   - 当前在选别的：切换到新的
        function startSelect(mode, tip) {
            var cur = core.getFlag('skill_selecting', '');
            if (cur === mode) {
                // 再点一次 = 取消
                endSelect();
                clearSkillHint();
                return true;
            }
            core.setFlag('skill_selecting', '');
            core.setFlag('fly_preview_target', null);
            core.setFlag('skill_selecting', mode);
            setSkillHint(tip);
            return true;
        }
        function endSelect() {
            core.setFlag('skill_selecting', '');
            core.setFlag('fly_preview_target', null);
        }
        P.cancelSkillSelect = function () { endSelect(); clearSkillHint(); };

        // 给 Godot 发"技能特效事件"：自增 seq，Godot 看到 seq 变了就播一次特效。
        function emitSkillEvent(type, data) {
            var seq = (Number(core.getFlag('mota3d_skill_seq', 0)) || 0) + 1;
            core.setFlag('mota3d_skill_seq', seq);
            core.setFlag('mota3d_last_skill', {
                seq: seq,
                type: type,
                data: data || {}
            });
        }

        var __mota3dLastBattleFxKey = '';
        var __mota3dLastBattleFxTime = 0;
        function emitBattleStartedOnce(enemyId, x, y, floorId, ev) {
            try {
                floorId = floorId || (core.status && core.status.floorId) || '';
                var key = String(floorId) + ':' + String(x) + ':' + String(y) + ':' + String(enemyId);
                var now = Date.now();
                if (__mota3dLastBattleFxKey === key && now - __mota3dLastBattleFxTime < 900) return;
                __mota3dLastBattleFxKey = key;
                __mota3dLastBattleFxTime = now;
                ev = ev || ((core.getBlock && core.getBlock(x, y, floorId) || {}).event || {});
                emitSkillEvent('battle_started', {
                    id: String(enemyId || ev.id || ''),
                    enemyId: String(enemyId || ev.id || ''),
                    x: Number(x),
                    y: Number(y),
                    floorId: String(floorId || ''),
                    number: Number(ev.number || ev.idnum || 0)
                });
            } catch (e) {}
        }

        // 使用技能后自动存档：把当前状态压进 mota 的 autosave 栈。
        // 玩家按 A 时会调 core.doSL('autoSave', 'load') 弹出最近一档。
        function autoSaveAfterSkill() {
            try {
                if (core.autosave) core.autosave();
            } catch (e) {
                console.error('[Mota3D autosave]', e);
            }
        }

        function isReplayNow() {
            try { return !!(core.isReplaying && core.isReplaying()); } catch (e) { return false; }
        }

        function recordSkillRoute(action) {
            // 3D 自定义技能不使用 mota 原生选择框；这里手动把“最终选择”写入录像。
            // 回放时由 functions.js 里的自定义 replay action 直接套最终状态。
            if (!action || isReplayNow()) return;
            clearMota3DPendingClick('recordSkillRoute:' + action);
            try {
                if (core.status && core.status.route) core.status.route.push(action);
            } catch (e) {
                console.error('[Mota3D skill route]', e);
            }
        }

        // 使用 1-4 号道具/技能前先自动存档，方便玩家随时读档。
        function autoSaveBeforeSkill(tag) {
            try {
                if (core.autosave) core.autosave();
                core.setFlag('mota3d_last_presave_tool', String(tag || ''));
            } catch (e) {
                console.error('[Mota3D pre autosave]', e);
            }
        }

        function getFloorIndexForSkill(floorId) {
            var text = String(floorId || '');
            var matches = text.match(/-?\d+/g);
            if (!matches || !matches.length) return 0;
            var n = Number(matches[matches.length - 1]);
            if (!isFinite(n)) return 0;
            return Math.max(0, Math.floor(n));
        }

        function getAnchorDamageThreshold(floorId) {
            // 第一层 MT0 = 50；后续每层 +10。
            return 50 + getFloorIndexForSkill(floorId) * 10;
        }

        function cleanSkillTip(tip) {
            return String(tip || '')
                .replace(/3D\s*地图/g, '地图')
                .replace(/3D地图/g, '地图')
                .replace(/3D/g, '')
                .replace(/瞄定术/g, '锚定术')
                .replace(/瞄定/g, '锚定')
                .replace(/瞄/g, '锚')
                .replace(/描点/g, '锚点');
        }

        function setSkillHint(tip, skipDrawTip) {
            // 技能/道具操作提示只显示在黄色技能提示区，
            // 不再走 drawTip，避免又被 drawTip mirror 同步到蓝色文字提示区造成重复。
            tip = cleanSkillTip(tip);
            core.setFlag('mota3d_skill_hint', tip);
            try { core.updateStatusBar(); } catch (e) {}
            return true;
        }

        function clearSkillHint() {
            core.setFlag('mota3d_skill_hint', '');
            try { core.updateStatusBar(); } catch (e) {}
        }

        // 通用文字提示：拾取血瓶/宝石、获得属性、无法战斗等，统一显示到 3D 左侧状态栏。
        function setActionHint(tip) {
            tip = cleanSkillTip(tip);
            if (!tip) return false;
            // 蓝色文字提示只显示通用结果/拾取/战斗限制；
            // 不显示技能选择、破墙、飞行、错位、锚定等操作提示，避免和黄色技能提示重复。
            if (/锚定术|锚点|错位术|空间错位|破墙|飞行器|飞行完成|点击地图|选一处|中心点|相邻|本层.*已用完/.test(tip)) {
                return false;
            }
            core.setFlag('mota3d_action_hint', tip);
            core.setFlag('mota3d_action_hint_seq', (Number(core.getFlag('mota3d_action_hint_seq', 0)) || 0) + 1);
            try { core.updateStatusBar(); } catch (e) {}
            return true;
        }

        // 把 H5 原本左上角的 drawTip 文本镜像到 3D 状态栏。
        // 这样拾取血瓶、宝石、钥匙、获得属性等原生提示都会同步显示。
        if (!P.__mota3dDrawTipMirrorInstalled__) {
            P.__mota3dDrawTipMirrorInstalled__ = true;
            var __mota3dOrigDrawTip = core.drawTip;
            core.drawTip = function (text) {
                var ret = null;
                try { ret = __mota3dOrigDrawTip.apply(this, arguments); }
                catch (e) { console.error('[Mota3D drawTip mirror]', e); }
                try { if (text != null) setActionHint(String(text)); } catch (e2) {}
                return ret;
            };
        }

        function getHeroFrontCell() {
            var hx = core.getHeroLoc('x'), hy = core.getHeroLoc('y');
            var dir = '';
            try { dir = String(core.getHeroLoc('direction') || ''); } catch (e) {}
            if (!dir && core.status && core.status.hero && core.status.hero.loc)
                dir = String(core.status.hero.loc.direction || '');
            if (!dir) dir = 'up';
            if (dir === 'down') return { x: hx, y: hy + 1, direction: dir };
            if (dir === 'left') return { x: hx - 1, y: hy, direction: dir };
            if (dir === 'right') return { x: hx + 1, y: hy, direction: dir };
            return { x: hx, y: hy - 1, direction: 'up' };
        }

        function clearMota3DPendingClick(reason) {
            // 破墙后“乱跑”的根因通常是：之前点墙后面的自动寻路还在短延迟重试。
            // 技能直接改地图前后都清一次，保证录像/实战只按已记录 route 走。
            try { window.__mota3d_pending_click_cell = null; } catch (e) {}
            try { if (core.stopAutomaticRoute) core.stopAutomaticRoute(); } catch (e) {}
            try { if (core.clearContinueAutomaticRoute) core.clearContinueAutomaticRoute(); } catch (e) {}
        }
        // Godot 3D 地图左键点击格子时，交给 H5 魔塔原生点击逻辑寻路/拾取/开门/战斗。
        // v27：增强点击稳定性。H5 短时间锁控制/正在移动时会吞点击，这里做一次短延迟重试。
        window.mota3dClickCell = function (x, y) {
            x = Math.floor(Number(x));
            y = Math.floor(Number(y));
            if (!isFinite(x) || !isFinite(y)) {
                console.warn('[Mota3D click] invalid coords', x, y);
                return false;
            }
            // 调试日志：看每次点击 mota 端的关键状态
            try {
                var selectingDbg = core.getFlag && core.getFlag('skill_selecting', '');
                var lockDbg = !!(core.status && core.status.lockControl);
                var movingDbg = false;
                try { movingDbg = !!(core.isMoving && core.isMoving()); } catch (eMv) {}
                var playingDbg = !!(core.isPlaying && core.isPlaying());
                var hxDbg = '?', hyDbg = '?';
                try { hxDbg = core.getHeroLoc ? core.getHeroLoc('x') : (core.status.hero.loc || {}).x; } catch (eH) {}
                try { hyDbg = core.getHeroLoc ? core.getHeroLoc('y') : (core.status.hero.loc || {}).y; } catch (eH2) {}
                console.log('[Mota3D click]', 'target=(' + x + ',' + y + ')',
                    'hero=(' + hxDbg + ',' + hyDbg + ')',
                    'selecting=' + JSON.stringify(selectingDbg),
                    'lockControl=' + lockDbg,
                    'isMoving=' + movingDbg,
                    'isPlaying=' + playingDbg);
            } catch (eDbg) {}
            window.__mota3d_pending_click_cell = { x: x, y: y, t: Date.now(), tries: 0 };
            return window.__mota3dRunPendingClickCell(true);
        };

        window.__mota3dRunPendingClickCell = function (firstTry) {
            try {
                var req = window.__mota3d_pending_click_cell;
                if (!req) return false;
                if (core && core.isReplaying && core.isReplaying()) {
                    // 录像回放时禁止 3D 点击桥的旧重试继续生效；回放只能吃 route。
                    window.__mota3d_pending_click_cell = null;
                    return false;
                }
                if (!core || !core.isPlaying || !core.isPlaying()) {
                    if (req.tries < 25) {
                        req.tries++;
                        setTimeout(function () { try { window.__mota3dRunPendingClickCell(false); } catch (e) {} }, 100);
                        return true;
                    }
                    return false;
                }

                // 技能选点模式优先走技能逻辑。
                var selecting = core.getFlag && core.getFlag('skill_selecting', '');
                if (selecting && window.mota3dSkillClickCell) {
                    if (window.mota3dSkillClickCell(req.x, req.y)) {
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    }
                }

                // 如果此刻正在走路/事件锁控制，稍后重试，避免“有时点了没反应”。
                var locked = false;
                try { locked = !!(core.status && core.status.lockControl); } catch (e0) {}
                try { if (core.isMoving && core.isMoving()) locked = true; } catch (e1) {}
                if (locked && req.tries < 25) {
                    req.tries++;
                    setTimeout(function () { try { window.__mota3dRunPendingClickCell(false); } catch (e) {} }, 100);
                    return true;
                }

                var hx = Number(core.getHeroLoc ? core.getHeroLoc('x') : ((core.status.hero.loc || {}).x));
                var hy = Number(core.getHeroLoc ? core.getHeroLoc('y') : ((core.status.hero.loc || {}).y));
                var dx = req.x - hx, dy = req.y - hy;

                // 相邻格：优先直接走一步，撞怪/开门/拾取都会走原生逻辑。
                // 注意：mota 的 moveHero 如果当前 heroMoving!=0 会直接 return undefined
                // （不是 false），所以不能只判断 !== false。要看 hero 是不是真的开始动了。
                if (Math.abs(dx) + Math.abs(dy) === 1 && typeof core.moveHero === 'function') {
                    var dir = dx > 0 ? 'right' : dx < 0 ? 'left' : dy > 0 ? 'down' : 'up';
                    try {
                        core.moveHero(dir);
                        // 检验是不是真开始动了
                        var moving = false;
                        try { moving = !!(core.status && (core.status.heroMoving !== 0 || !core.status.heroStop)); } catch (eMv) {}
                        if (moving) {
                            window.__mota3d_pending_click_cell = null;
                            return true;
                        }
                        // 没动起来：可能是相邻格被墙挡，让 setAutomaticRoute 试找路径绕
                        console.log('[Mota3D click] adjacent moveHero(' + dir + ') did not start; trying autoRoute');
                    } catch (e2) {}
                }

                // 自动寻路兜底。不同 H5Mota 版本函数名不同。
                // 关键：必须传第三个参数 stepPostfix=[]。mota 源码
                // _setAutomaticRoute_clickMoveDirectly 里直接访问 stepPostfix.length，
                // 不传会 TypeError 被吞掉，setAutomaticRoute 静默失败。
                if (typeof core.setAutomaticRoute === 'function') {
                    try {
                        core.setAutomaticRoute(req.x, req.y, []);
                        // 不再做 autoHeroMove 校验——短距离走 tryMoveDirectly 路径
                        // 不会设这个 flag，校验严了会误判成 failed 然后死磕重试刷屏。
                        // 既然 mota 源码在 setAutomaticRoute() 里有完整的拒绝判定
                        // （played / lockControl / 各种已经在走），它没抛错就当成功。
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e3) {
                        console.warn('[Mota3D click] setAutomaticRoute threw:', e3 && e3.message);
                    }
                }
                if (core.actions && typeof core.actions.setAutomaticRoute === 'function') {
                    try {
                        core.actions.setAutomaticRoute(req.x, req.y, []);
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e4) {}
                }

                // 鼠标点击原生逻辑兜底：本工程 _clickAction 更稳定吃格子坐标；像素坐标只做最后兜底。
                if (core.actions && typeof core.actions._clickAction === 'function') {
                    try {
                        core.actions._clickAction(req.x, req.y);
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e5) {}
                    try {
                        core.actions._clickAction(req.x * 32 + 16, req.y * 32 + 16);
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e6) {}
                }
                if (core.actions && typeof core.actions._sys_onmouseclick === 'function') {
                    try {
                        core.actions._sys_onmouseclick(req.x * 32 + 16, req.y * 32 + 16);
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e7) {}
                    try {
                        core.actions._sys_onmouseclick(req.x, req.y);
                        window.__mota3d_pending_click_cell = null;
                        return true;
                    } catch (e8) {}
                }
            } catch (e) {
                console.error('[Mota3D click cell]', e);
            }
            try {
                var req2 = window.__mota3d_pending_click_cell;
                if (req2 && Date.now() - Number(req2.t || 0) < 3500 && Number(req2.tries || 0) < 25) {
                    req2.tries = Number(req2.tries || 0) + 1;
                    setTimeout(function () { try { window.__mota3dRunPendingClickCell(false); } catch (e) {} }, 120);
                    return true;
                }
            } catch (e9) {}
            if (!firstTry && window.__mota3d_pending_click_cell) window.__mota3d_pending_click_cell = null;
            return false;
        };

        // 开门特效：包住 H5 原生 openDoor，确认门块消失/变化后再通知 Godot 播放开门光效。
        if (!core.__mota3d_open_door_fx_wrapped__ && typeof core.openDoor === 'function') {
            core.__mota3d_open_door_fx_wrapped__ = true;
            var __mota3dOrigOpenDoor = core.openDoor && core.openDoor.__mota3dOrig
                ? core.openDoor.__mota3dOrig
                : core.openDoor;
            var __mota3dOpenDoorWrapper = function (x, y) {
                var floorId = core.status && core.status.floorId;
                var bx = Math.floor(Number(x)), by = Math.floor(Number(y));
                var before = null, doorId = '';
                try {
                    if (isFinite(bx) && isFinite(by) && core.getBlock) {
                        before = core.getBlock(bx, by, floorId);
                        if (before && before.event) doorId = String(before.event.id || '');
                    }
                } catch (e0) {}
                var ret = __mota3dOrigOpenDoor.apply(this, arguments);
                try {
                    if (before && before.event) {
                        setTimeout(function () {
                            try {
                                var after = core.getBlock ? core.getBlock(bx, by, floorId) : null;
                                var afterId = after && after.event ? String(after.event.id || '') : '';
                                if (afterId !== doorId) {
                                    emitSkillEvent('door_opened', {
                                        x: bx, y: by, floorId: String(floorId || ''), id: doorId
                                    });
                                }
                            } catch (e1) {}
                        }, 90);
                    }
                } catch (e2) {}
                return ret;
            };
            __mota3dOpenDoorWrapper.__mota3dOrig = __mota3dOrigOpenDoor;
            core.openDoor = __mota3dOpenDoorWrapper;
        }

        // 拦截 1-6 数字键 + X/ESC：H5 焦点时也能触发 HUD 和怪物手册。
        // Godot 也有自己的处理；这里是当 H5 父页面有焦点时的兜底。
        if (!window.__mota3d_digit_keys_installed__) {
            window.__mota3d_digit_keys_installed__ = true;
            document.addEventListener('keyup', function (e) {
                var slot = null;
                if (e.keyCode >= 49 && e.keyCode <= 54) {
                    slot = e.keyCode - 49;  // 0..5
                }
                else if (e.keyCode === 88) {
                    slot = 88; // X：打开/关闭怪物手册
                }
                else if (e.keyCode === 27) {
                    slot = 27; // ESC：关闭怪物手册/信息面板
                }
                else return;
                if (!core || !core.isPlaying || !core.isPlaying()) return;
                if (core.status && core.status.event && core.status.event.id) return;
                e.stopImmediatePropagation();
                e.preventDefault();
                window.mota3d_hud_key_seq = (Number(window.mota3d_hud_key_seq) || 0) + 1;
                window.mota3d_hud_key_press = {
                    seq: window.mota3d_hud_key_seq,
                    slot: slot
                };
            }, true);  // capture 阶段
        }

        // A 键读档后，有些 H5 魔塔版本会重建事件/战斗入口，
        // 导致 3D 战斗动画 hook 失效。读档后延迟重装一次插件 hook。
        function reinstallMota3DAfterLoad() {
            setTimeout(function () {
                try {
                    __mota3dLastBattleFxKey = '';
                    __mota3dLastBattleFxTime = 0;
                    if (core) {
                        core.__mota3d_block_battle_wrapped__ = false;
                        if (core.events) core.events.__mota3d_before_battle_fx_wrapped__ = false;
                    }
                    if (core && core.plugin) core.plugin.__mota3dSkillsInstalled__ = false;
                    installSkills();
                    if (window.mota3d_pushState) window.mota3d_pushState();
                } catch (e) {
                    console.error('[Mota3D reinstall after load]', e);
                }
            }, 300);
        }

        // 拦截 A 键：原 mota 里 A=移动左（keyMap 65→37）。
        // 改成 A=读取自动存档。在 capture 阶段拦下，stopImmediatePropagation 阻止
        // mota 后续把它当左方向键。
        if (!window.__mota3d_a_key_installed__) {
            window.__mota3d_a_key_installed__ = true;
            document.addEventListener('keyup', function (e) {
                if (e.keyCode !== 65) return;
                if (!core || !core.isPlaying || !core.isPlaying()) return;
                if (core.status && core.status.event && core.status.event.id) return;
                e.stopImmediatePropagation();
                e.preventDefault();
                try {
                    if (core.saves && core.saves.autosave &&
                        core.saves.autosave.data && core.saves.autosave.data.length > 0) {
                        core.doSL('autoSave', 'load');
                        reinstallMota3DAfterLoad();
                    } else {
                        core.drawTip('暂无自动存档');
                    }
                } catch (err) {
                    console.error('[Mota3D A-load]', err);
                }
            }, true);
        }

        // 走向无法战斗的高伤害怪时，给 3D 左侧状态栏同步提示，并拦截本次移动/战斗。
        function checkBlockedBattleByDirection(dir) {
            try {
                if (!dir || !core || !core.status || !core.status.floorId) return false;
                var hx = Number(core.getHeroLoc('x')), hy = Number(core.getHeroLoc('y'));
                var x = hx, y = hy;
                if (dir === 'up') y--;
                else if (dir === 'down') y++;
                else if (dir === 'left') x--;
                else if (dir === 'right') x++;
                else return false;

                var floorId = core.status.floorId;
                var block = core.getBlock(x, y, floorId);
                if (!block || !block.event) return false;
                var ev = block.event || {};
                var id = String(ev.id || '');
                var cls = String(ev.cls || '');
                if (!(cls.indexOf('enemy') === 0 || (core.material && core.material.enemys && core.material.enemys[id]))) return false;

                var hp = Number(core.status.hero && core.status.hero.hp || 0);
                var damage = null;
                try { damage = core.getDamage(id, x, y, floorId); } catch (e) { damage = null; }
                var enemy = (core.material && core.material.enemys && core.material.enemys[id]) || {};
                var name = String(enemy.name || ev.name || id || '怪物');
                if (damage == null || damage === '???') {
                    setActionHint('无法战斗：' + name + ' 无法破防');
                    return true;
                }
                var dmg = Number(damage);
                if (!isFinite(dmg)) {
                    setActionHint('无法战斗：' + name);
                    return true;
                }
                if (dmg >= hp) {
                    setActionHint('无法战斗：' + name + ' 预计伤害 ' + Math.floor(dmg) + '，当前生命 ' + Math.floor(hp));
                    return true;
                }
                return false;
            } catch (e) {
                console.error('[Mota3D blocked battle hint]', e);
                return false;
            }
        }

        function checkBlockedBattleAt(enemyId, x, y, floorId) {
            try {
                floorId = floorId || (core.status && core.status.floorId);
                if (!enemyId || x == null || y == null || !floorId) return false;
                var hp = Number(core.status.hero && core.status.hero.hp || 0);
                var damage = null;
                try { damage = core.getDamage(enemyId, x, y, floorId); } catch (e) { damage = null; }
                var enemy = (core.material && core.material.enemys && core.material.enemys[enemyId]) || {};
                var name = String(enemy.name || enemyId || '怪物');
                if (damage == null || damage === '???') {
                    setActionHint('无法战斗：' + name + ' 无法破防');
                    return true;
                }
                var dmg = Number(damage);
                if (!isFinite(dmg)) {
                    setActionHint('无法战斗：' + name);
                    return true;
                }
                if (dmg >= hp) {
                    setActionHint('无法战斗：' + name + ' 预计伤害 ' + Math.floor(dmg) + '，当前生命 ' + Math.floor(hp));
                    return true;
                }
                return false;
            } catch (e) {
                console.error('[Mota3D blocked battle at]', e);
                return false;
            }
        }

        // 更硬的保险：不只拦键盘，也直接包住 core.battle。
        // 这样点击寻路撞怪、自动路线撞怪、Godot 转发方向键都不会把生命打到 0。
        // 读档后可能重建 core.battle，所以这里写成可重复安装。
        if (!core.__mota3d_block_battle_wrapped__ && typeof core.battle === 'function') {
            core.__mota3d_block_battle_wrapped__ = true;
            var __mota3dOrigBattle = core.battle && core.battle.__mota3dOrig
                ? core.battle.__mota3dOrig
                : core.battle;
            var __mota3dBattleWrapper = function (enemyId, x, y) {
                var floorId = core.status && core.status.floorId;
                if (checkBlockedBattleAt(enemyId, x, y, floorId)) return false;
                try {
                    var block = core.getBlock ? core.getBlock(x, y, floorId) : null;
                    var ev = block && block.event ? block.event : {};
                    emitBattleStartedOnce(enemyId || ev.id || '', x, y, floorId, ev);
                } catch (e) {}
                return __mota3dOrigBattle.apply(this, arguments);
            };
            __mota3dBattleWrapper.__mota3dOrig = __mota3dOrigBattle;
            core.battle = __mota3dBattleWrapper;
        }

        // 有些版本不是从 core.battle 进入；再包一层 beforeBattle，确保 3D 战斗动作一定能触发。
        // 读档后可能重建 core.events.beforeBattle，所以这里也写成可重复安装。
        if (core.events && !core.events.__mota3d_before_battle_fx_wrapped__ && typeof core.events.beforeBattle === 'function') {
            core.events.__mota3d_before_battle_fx_wrapped__ = true;
            var __mota3dOrigBeforeBattle = core.events.beforeBattle && core.events.beforeBattle.__mota3dOrig
                ? core.events.beforeBattle.__mota3dOrig
                : core.events.beforeBattle;
            var __mota3dBeforeBattleWrapper = function (enemyId, x, y) {
                var floorId = core.status && core.status.floorId;
                if (checkBlockedBattleAt(enemyId, x, y, floorId)) return false;
                emitBattleStartedOnce(enemyId, x, y, floorId, null);
                return __mota3dOrigBeforeBattle.apply(this, arguments);
            };
            __mota3dBeforeBattleWrapper.__mota3dOrig = __mota3dOrigBeforeBattle;
            core.events.beforeBattle = __mota3dBeforeBattleWrapper;
        }

        if (!window.__mota3d_blocked_battle_hint_installed__) {
            window.__mota3d_blocked_battle_hint_installed__ = true;
            document.addEventListener('keydown', function (e) {
                if (!core || !core.isPlaying || !core.isPlaying()) return;
                if (core.status && core.status.event && core.status.event.id) return;
                var dir = '';
                var key = String(e.key || '').toLowerCase();
                if (e.keyCode === 37 || key === 'arrowleft') dir = 'left';
                else if (e.keyCode === 38 || key === 'arrowup') dir = 'up';
                else if (e.keyCode === 39 || key === 'arrowright') dir = 'right';
                else if (e.keyCode === 40 || key === 'arrowdown') dir = 'down';
                if (dir && checkBlockedBattleByDirection(dir)) {
                    e.stopImmediatePropagation();
                    e.preventDefault();
                    return false;
                }
            }, true);
        }

        // ---------- 锚定术 I342 ----------
        P.anchorPlace = function () {
            var floorId = core.status.floorId;
            if (core.getFlag('anchor_used_' + floorId, 0) >= 2)
                return setSkillHint('本层锚定术已用完');
            if (core.getFlag('anchor_mark_' + floorId, null))
                return setSkillHint('本层已有锚点，等待触发');
            autoSaveBeforeSkill('anchor');
            startSelect('anchor', '锚定术：点击地图空地设置锚点');
        };

        P.anchorPlaceConfirm = function (x, y) {
            if (core.getFlag('skill_selecting', '') !== 'anchor') return;
            var floorId = core.status.floorId;
            // 空地校验：getBlock 返回 null 即没块
            if (core.getBlock(x, y, floorId) != null)
                return setSkillHint('锚点必须放在空地上');
            recordSkillRoute('anchorPlace:' + x + ':' + y);
            core.setFlag('anchor_mark_' + floorId, { x: x, y: y });
            var stat = core.status.hero.statistics || {};
            var baseline = (stat.battleDamage || 0) + (stat.extraDamage || 0);
            core.setFlag('anchor_start_sum_' + floorId, baseline);
            core.setFlag('anchor_used_' + floorId,
                core.getFlag('anchor_used_' + floorId, 0) + 1);
            endSelect();
            setSkillHint('锚点已设 (' + x + ',' + y + ')；本层受伤≥' + getAnchorDamageThreshold(floorId) + '后返回');
            emitSkillEvent('anchor_placed', { x: x, y: y });
            autoSaveAfterSkill();
            core.updateStatusBar();
        };

        // 伤害监控（hook 一次）：累计伤害超 50 → 传送
        (function installAnchorHook() {
            if (core.getFlag('__anchor_hooked__', false)) return;
            core.setFlag('__anchor_hooked__', true);
            var orig = core.updateStatusBar;
            core.updateStatusBar = function () {
                var ret = orig.apply(this, arguments);
                try {
                    var floorId = core.status.floorId;
                    var mark = core.getFlag('anchor_mark_' + floorId, null);
                    if (mark) {
                        var stat = core.status.hero.statistics || {};
                        var sum = (stat.battleDamage || 0) + (stat.extraDamage || 0);
                        var base = core.getFlag('anchor_start_sum_' + floorId, 0);
                        if (sum - base >= getAnchorDamageThreshold(floorId)) {
                            // 清锚点，直接 setHeroLoc 传送，绕开 mota 的 insertAction
                            // 事件队列 —— 那条路上任何字符串项都会被当 text 事件弹对话框，
                            // 在 3D 模式下对话框看不见，会卡死。
                            var markX = mark.x, markY = mark.y;
                            core.setFlag('anchor_mark_' + floorId, null);
                            core.setFlag('anchor_start_sum_' + floorId, 0);
                            core.setFlag('mota3d_skill_hint', '锚定术触发：已返回锚点');
                            emitSkillEvent('anchor_triggered', { x: markX, y: markY });
                            try {
                                core.setHeroLoc('x', markX);
                                core.setHeroLoc('y', markY);
                                core.drawHero();
                                core.playSound('centerFly.mp3');
                            } catch (e2) {}
                        }
                    }
                } catch (e) {}
                return ret;
            };
        })();

        // ---------- 错位术 I343 ----------
        P.spaceSwap = function () {
            var floorId = core.status.floorId;
            if (core.getFlag('swap_used_' + floorId, 0) >= 2)
                return setSkillHint('本层错位术已用完');
            autoSaveBeforeSkill('spaceSwap');
            startSelect('swap', '错位术：点击地图中心点');
        };

        P.spaceSwapConfirm = function (x, y) {
            if (core.getFlag('skill_selecting', '') !== 'swap') return;
            var floorId = core.status.floorId;
            var thisMap = core.status.maps[floorId];
            var width = thisMap.width, height = thisMap.height;
            if (x <= 0 || x >= width - 1 || y <= 0 || y >= height - 1)
                return setSkillHint('错位术不能在边界使用');

            // 找各方向第一个非空块
            function scan(dx, dy) {
                var nx = x, ny = y;
                while (true) {
                    nx += dx; ny += dy;
                    if (nx < 0 || nx >= width || ny < 0 || ny >= height) return null;
                    var b = core.getBlock(nx, ny, floorId);
                    if (b != null) return { x: nx, y: ny, num: b.id };
                }
            }
            var up = scan(0, -1), down = scan(0, 1);
            var left = scan(-1, 0), right = scan(1, 0);
            if (!up || !down || !left || !right)
                return setSkillHint('四方向各须有可置换物块');

            recordSkillRoute('spaceSwap:' + x + ':' + y);

            // 1. 先发事件，让 Godot 立即开始播 3D 置换动画（在真 swap 之前！）
            //    除了坐标，也把被交换物块的 id/cls/name 带给 Godot；
            //    Godot 会优先克隆当前 3D 场景里的真实物体飞起来，找不到时才用这些信息画兜底模型。
            function skillBlockInfo(p) {
                var b = core.getBlock(p.x, p.y, floorId);
                var ev = (b && b.event) || {};
                var id = String(ev.id || '');
                var cls = String(ev.cls || '');
                var kind = cls;
                if (cls.indexOf('enemy') === 0) kind = 'enemy';
                else if (cls === 'items' || cls === 'items48' || cls === 'items32') kind = 'item';
                else if (id.indexOf('Door') >= 0 || id.indexOf('door') >= 0) kind = 'door';
                else if (cls === 'tileset') kind = ev.noPass === true ? 'wall' : 'terrain';
                else if (id.indexOf('wall') >= 0 || id.indexOf('Wall') >= 0) kind = 'wall';
                else if (cls === 'terrains' || cls === 'tilesets' || cls === 'autotile') kind = 'terrain';
                return {
                    x: p.x, y: p.y,
                    id: id,
                    cls: cls,
                    kind: kind,
                    number: Number(ev.number || p.num || 0),
                    noPass: ev.noPass === true,
                    name: String(ev.name || id || cls || '')
                };
            }
            emitSkillEvent('space_swapped', {
                center: [x, y],
                up: [up.x, up.y], down: [down.x, down.y],
                left: [left.x, left.y], right: [right.x, right.y],
                upBlock: skillBlockInfo(up),
                downBlock: skillBlockInfo(down),
                leftBlock: skillBlockInfo(left),
                rightBlock: skillBlockInfo(right),
                swapDelayMs: 1350
            });

            // 2. 消耗次数 + 退出选点模式
            core.setFlag('swap_used_' + floorId,
                core.getFlag('swap_used_' + floorId, 0) + 1);
            endSelect();
            setSkillHint('空间错位完成');

            // 3. 真实的 mota swap 延迟 1350ms 触发 —— 对上 Godot 的“物体升空→飞行→落地”中后段。
            //    这时 3D 克隆物正在目标上方，地图块切换不会突兀。
            setTimeout(function () {
                try {
                    var swapPair = function (a, b) {
                        core.removeBlock(a.x, a.y, floorId);
                        core.removeBlock(b.x, b.y, floorId);
                        core.setBlock(b.num, a.x, a.y, floorId);
                        core.setBlock(a.num, b.x, b.y, floorId);
                    };
                    swapPair(up, down);
                    swapPair(left, right);
                    autoSaveAfterSkill();
                    core.updateStatusBar();
                } catch (err) {
                    console.error('[Mota3D delayed swap]', err);
                }
            }, 1350);
        };

        // ---------- 破墙镐（面前直接破墙版）----------
        P.pickaxeFriendly = function () {
            if (!core.itemCount('pickaxe'))
                return setSkillHint('没有破墙镐');
            clearMota3DPendingClick('pickaxe-start');
            autoSaveBeforeSkill('pickaxe');
            var front = getHeroFrontCell();
            return P.pickaxeFriendlyConfirm(front.x, front.y, true, false);
        };

        // 兼容旧录像里已经存在的 key:49：回放时直接按面前墙套最终结果，不播动画、不延迟。
        P.pickaxeFriendlyReplayFront = function () {
            var front = getHeroFrontCell();
            return P.pickaxeFriendlyConfirm(front.x, front.y, true, true);
        };

        P.pickaxeFriendlyConfirm = function (x, y, directFront, replayInstant) {
            // directFront=true：HUD 直接使用，破面前墙；false/空：兼容旧的地图点选调用。
            replayInstant = !!replayInstant || isReplayNow();
            clearMota3DPendingClick(replayInstant ? 'pickaxe-replay' : 'pickaxe-confirm');
            var floorId = core.status.floorId;
            var block = core.getBlock(x, y, floorId);
            if (!block) {
                endSelect();
                return setSkillHint(directFront ? '前方没有可破墙' : '请选墙');
            }
            var hx = core.getHeroLoc('x'), hy = core.getHeroLoc('y');
            if (Math.abs(x - hx) + Math.abs(y - hy) !== 1) {
                endSelect();
                return setSkillHint('只能破除相邻方块');
            }
            var ev = block.event || {};
            var cls = String(ev.cls || '');
            var id = String(ev.id || '');
            if (cls.indexOf('enemy') === 0) {
                endSelect();
                return setSkillHint('不能用镐破怪物');
            }
            if (cls === 'items' || cls === 'items48' || cls === 'items32') {
                endSelect();
                return setSkillHint('不能用镐破道具');
            }
            if (id.toLowerCase().indexOf('door') >= 0) {
                endSelect();
                return setSkillHint('不能用镐破门');
            }
            if (id === 'upFloor' || id === 'downFloor' || id === 'portal') {
                endSelect();
                return setSkillHint('不能用镐破楼梯');
            }

            // 1. 记录录像：只记最终被破坏的格子，回放时不播动画，直接套结果。
            if (!replayInstant) recordSkillRoute('pickaxeBreak:' + x + ':' + y);

            // 2. 回放/旧 key:49 兼容：同步破墙，绝不启动自动寻路。
            if (replayInstant) {
                try {
                    core.removeBlock(x, y, floorId);
                    if (core.itemCount && core.itemCount('pickaxe') > 0) core.removeItem('pickaxe');
                    clearMota3DPendingClick('pickaxe-replay-after');
                    endSelect();
                    setSkillHint('破墙！');
                    core.updateStatusBar();
                    try { core.updateDamage(); } catch (e0) {}
                    try { core.drawMap(); } catch (e1) {}
                    try { if (window.mota3d_pushState) window.mota3d_pushState(); } catch (e2) {}
                } catch (err0) {
                    console.error('[Mota3D replay pickaxe front]', err0);
                }
                return true;
            }

            // 3. 先发特效事件，Godot 立刻播闪电劈下来
            emitSkillEvent('pickaxe_broke', { x: x, y: y });

            // 4. 退出选点
            endSelect();
            setSkillHint('破墙！');
            core.playSound('破墙镐');

            // 5. 延迟 200ms 才真正破墙 —— 让闪电先打到位再爆裂
            setTimeout(function () {
                try {
                    clearMota3DPendingClick('pickaxe-before-remove');
                    core.removeBlock(x, y, floorId);
                    core.removeItem('pickaxe');
                    clearMota3DPendingClick('pickaxe-after-remove');
                    autoSaveAfterSkill();
                    core.updateStatusBar();
                } catch (err) {
                    console.error('[Mota3D delayed pickaxe]', err);
                }
            }, 200);
        };

        // ---------- 中心对称飞行器（2 步式）----------
        // 第一次点 HUD 槽：计算目标 + 进入预览模式（3D 地图上显示明显标记）。
        // 第二次点 HUD 槽：真正传送。
        // 中间点 3D 地图 = 取消预览。
        P.centerFlyFriendly = function () {
            if (!core.itemCount('centerFly'))
                return setSkillHint('没有飞行器');
            autoSaveBeforeSkill('centerFly');

            var mode = core.getFlag('skill_selecting', '');

            // 第二次点击：确认传送
            if (mode === 'fly_preview') {
                var tgt = core.getFlag('fly_preview_target', null);
                if (tgt) {
                    var oldHx = core.getHeroLoc('x'), oldHy = core.getHeroLoc('y');
                    var ntx = Number(tgt.x), nty = Number(tgt.y);
                    // 先发事件让起点动画播起来
                    emitSkillEvent('fly_executed', {
                        fromX: oldHx, fromY: oldHy,
                        toX: ntx, toY: nty
                    });
                    core.playSound('centerFly.mp3');
                    recordSkillRoute('centerFly:' + ntx + ':' + nty);
                    // 延迟 300ms 真传送：让起点光柱+闪电先放完
                    setTimeout(function () {
                        try {
                            core.setHeroLoc('x', ntx);
                            core.setHeroLoc('y', nty);
                            core.removeItem('centerFly');
                            core.drawHero();
                            autoSaveAfterSkill();
                            core.setFlag('mota3d_skill_hint', '飞行完成');
                            core.updateStatusBar();
                        } catch (err) {
                            console.error('[Mota3D delayed fly]', err);
                        }
                    }, 300);
                }
                endSelect();
                return;
            }

            // 第一次点击：算对称点，进入预览
            // 如果当前在其他选点模式，先取消
            if (mode !== '') endSelect();

            var floorId = core.status.floorId;
            var thisMap = core.status.maps[floorId];
            var w = thisMap.width, h = thisMap.height;
            var hx = core.getHeroLoc('x'), hy = core.getHeroLoc('y');
            var tx = w - 1 - hx, ty = h - 1 - hy;
            if (core.getBlock(tx, ty, floorId) != null)
                return setSkillHint('此地无法飞行');

            core.setFlag('skill_selecting', 'fly_preview');
            core.setFlag('fly_preview_target', { x: tx, y: ty });
            setSkillHint('飞行预览：再点飞行确认，点地图取消');
            core.updateStatusBar();
        };
    }
    installSkills();
})();
;
// ============================================================================
// Godot 3D 显示层桥接 Step 1
// 作用：不改游戏逻辑，只把当前 H5 魔塔状态导出给 window.mota3d_state。
// 后续 Godot Web 可以读取 window.mota3d_state 来绘制 3D 地图。
// ============================================================================
;
(function () {
    if (!window) return;

    window.mota3d_enabled = true;
    window.mota3d_state = "";
    window.mota3d_state_version = 0;
    window.mota3d_last_error = "";

    function safeClone(obj) {
        try { return JSON.parse(JSON.stringify(obj)); }
        catch (e) { return null; }
    }

    function blockTo3DInfo(block) {
        if (!block) return null;
        var event = block.event || {};
        var info = {
            x: block.x,
            y: block.y,
            id: event.id || "",
            cls: event.cls || "",
            number: event.number || 0,
            trigger: event.trigger || "",
            noPass: event.noPass === true,
            name: event.name || ""
        };

        // 粗分类，Godot 第一版可以直接用 kind 判断画什么。
        var id = String(info.id || "");
        var cls = String(info.cls || "");

        if (cls.indexOf("enemy") === 0) info.kind = "enemy";
        else if (cls === "items" || cls === "items48" || cls === "items32") info.kind = "item";
        // mota 给自定义 tileset 块设的 cls 是 "tileset"（单数）；带 noPass 的就是墙，
        // 否则视为可走的地形装饰。
        else if (cls === "tileset") info.kind = info.noPass ? "wall" : "terrain";
        else if (cls === "terrains" || cls === "tilesets" || cls === "autotile") {
            // 单向箭头（arrowUp/Down/Left/Right）也是 terrains，单独标记给 Godot 画方向。
            if (/^arrow(Up|Down|Left|Right)$/.test(id)) info.kind = "arrow";
            // airwall 是隐形空气墙，没图但 noPass=true，3D 端不画。
            else if (id === "airwall") info.kind = "airwall";
            else info.kind = "terrain";
        }
        else if (id.indexOf("Door") >= 0 || id.indexOf("door") >= 0) info.kind = "door";
        else if (id.indexOf("wall") >= 0 || id.indexOf("Wall") >= 0) info.kind = "wall";
        else info.kind = cls || "unknown";

        // 给怪物带上手册/3D显示常用信息。
        if (info.kind === "enemy" && core.enemys) {
            try {
                var enemy = core.material && core.material.enemys && core.material.enemys[id];
                if (enemy) {
                    info.enemy = {
                        name: enemy.name || id,
                        hp: enemy.hp || 0,
                        atk: enemy.atk || 0,
                        def: enemy.def || 0,
                        money: enemy.money || 0,
                        special: enemy.special || 0
                    };
                    // 如果当前坐标有动态属性覆盖，也一起带出去。
                    var floorId = core.status && core.status.floorId;
                    var point = (((flags.enemyOnPoint || {})[floorId] || {})[block.x + "," + block.y] || {});
                    if (point && Object.keys(point).length > 0) info.enemyPoint = safeClone(point);

                    // 实际属性（含坐标特殊属性覆盖 + 楼层 ratio）。
                    // 用 H5 引擎的 getEnemyInfo，保证和 H5 状态栏完全一致。
                    try {
                        if (typeof core.enemys.getEnemyInfo === 'function' && floorId) {
                            var eff = core.enemys.getEnemyInfo(id, null, block.x, block.y, floorId);
                            if (eff) {
                                info.enemyEffective = {
                                    hp: Number(eff.hp) || 0,
                                    atk: Number(eff.atk) || 0,
                                    def: Number(eff.def) || 0,
                                    money: Number(eff.money) || 0,
                                    exp: Number(eff.exp) || 0,
                                    point: Number(eff.point) || 0
                                };
                            }
                        }
                    } catch (e1) {}

                    // 临界表：取最多 8 个临界点（和 H5 怪物手册一致）。
                    // 每项 [再加多少攻击, 减伤多少]。
                    try {
                        if (typeof core.enemys.nextCriticals === 'function' && floorId) {
                            var crits = core.enemys.nextCriticals(id, 8, block.x, block.y, floorId);
                            if (crits && crits.length > 0) {
                                // 去掉前导 [0,0]（H5 手册的逻辑：已经达到最大减伤）。
                                while (crits.length > 0 && crits[0] && crits[0][0] == 0 && crits[0][1] == 0) crits.shift();
                                var critList = [];
                                for (var ci = 0; ci < crits.length && ci < 8; ci++) {
                                    if (crits[ci] && crits[ci].length >= 2) {
                                        critList.push({
                                            atk: Number(crits[ci][0]) || 0,
                                            damage: Number(crits[ci][1]) || 0
                                        });
                                    }
                                }
                                if (critList.length > 0) info.enemyCriticals = critList;
                                // 同时保留第一项给老代码用（向后兼容）
                                if (critList.length > 0) info.enemyCritical = critList[0];
                            }
                        }
                    } catch (e2) {}

                    // 防御临界：再加多少防御能少受 1 点伤害。
                    try {
                        if (typeof core.enemys.getDefDamage === 'function' && floorId) {
                            var ratio = (core.status.maps && core.status.maps[floorId] && core.status.maps[floorId].ratio) || 1;
                            var defDamage = core.enemys.getDefDamage(id, ratio, block.x, block.y, floorId);
                            if (defDamage !== null && defDamage !== undefined && !isNaN(defDamage)) {
                                info.enemyDefDamage = Number(defDamage);
                            }
                        }
                    } catch (e3) {}

                    // 特技文字（数组，每条对应一种 special）+ 颜色（hex 字符串）。
                    try {
                        if (typeof core.enemys.getSpecialText === 'function') {
                            var st = core.enemys.getSpecialText(enemy);
                            if (st) info.enemySpecialText = st;
                        }
                        if (typeof core.enemys.getSpecialColor === 'function') {
                            var sc = core.enemys.getSpecialColor(enemy);
                            if (sc) info.enemySpecialColor = sc;
                        }
                        // v33g：推送每个 special 的完整描述（hint），跟 enemySpecialText 一一对应
                        // 这样 Godot 端的怪物手册/信息面板能显示 "烈火：战斗前造成500点固伤..." 而不只是"烈火"
                        if (typeof core.enemys.getSpecials === 'function'
                            && typeof core.enemys.hasSpecial === 'function'
                            && typeof core.enemys._calSpecialContent === 'function') {
                            var allSp = core.enemys.getSpecials() || [];
                            var enemySp = enemy.special;
                            var hints = [];
                            for (var sk = 0; sk < allSp.length; sk++) {
                                if (core.enemys.hasSpecial(enemySp, allSp[sk][0])) {
                                    hints.push(core.enemys._calSpecialContent(enemy, allSp[sk][2]));
                                }
                            }
                            if (hints.length) info.enemySpecialHints = hints;
                        }
                    } catch (e4) {}
                }
            } catch (e) {}
        }

        // 显伤：直接用 H5 引擎自己的 core.getDamage，保证和 H5 状态栏完全一致。
        // 玩家攻防、楼层 ratio、领域/连击/坚固等所有 special 都由引擎处理，我们不重复实现。
        if (info.kind === "enemy") {
            try {
                var floorIdForDmg = core.status && core.status.floorId;
                if (floorIdForDmg && typeof core.getDamage === 'function') {
                    var dmg = core.getDamage(id, block.x, block.y, floorIdForDmg);
                    if (dmg !== null && dmg !== undefined && !isNaN(dmg)) {
                        info.damage = Math.max(0, Math.round(Number(dmg)));
                    }
                }
            } catch (e) {}
        }

        // 道具数值提示：调用引擎 core.items.getItemEffectTip(id)，内部走 core.replaceText
        // 自动按 core.values / 当前楼层 ratio 填模板。改 core.values 即时生效，不需要重写桥接。
        // 注意：某些道具的 itemEffectTip 模板里有 "core.status.thisMap.ratio"，
        // 楼层切换瞬间 thisMap 可能是 null，引擎内部 try 抓不住外层的 console.error，
        // 所以必须先确认 thisMap 存在，否则桥接会一直刷错误。
        if (core.items && typeof core.items.getItemEffectTip === 'function'
            && core.status && core.status.thisMap) {
            try {
                var tip = core.items.getItemEffectTip(id);
                if (tip) {
                    // 引擎返回 "，攻击+1" 这种带前导标点的文本，是给"获得XXX道具"后缀用的；
                    // 这里去掉前导标点和空格，单独显示更干净。
                    tip = String(tip).replace(/^[，,、:：\s]+/, '');
                    if (tip) info.itemEffectTip = tip;
                }
            } catch (e) {}
        }

        return info;
    }


    function getHeroDisplayLoc(hero) {
        var loc = hero.loc || {};
        var x = Number(loc.x || 0);
        var y = Number(loc.y || 0);
        var direction = loc.direction || "down";
        var displayX = x;
        var displayY = y;

        try {
            var moving = Number(core.status && core.status.heroMoving || 0);
            if (moving > 0) {
                var progress = Math.max(0, Math.min(1, moving / 8));
                var scan = core.utils && core.utils.scan && core.utils.scan[direction];
                if (scan) {
                    displayX = x + Number(scan.x || 0) * progress;
                    displayY = y + Number(scan.y || 0) * progress;
                }
            }
        } catch (e) {}

        return {
            x: x,
            y: y,
            displayX: displayX,
            displayY: displayY,
            direction: direction,
            moving: Number(core.status && core.status.heroMoving || 0)
        };
    }

    function build3DState() {
        if (!window.core || !core.status || !core.status.floorId) return null;
        // 主角坐标检查：resetGame 内部是先 clearStatus，再设 hero/floorId 的，
        // 中间这段时间 hero.loc 可能还是 undefined。如果这时推 state，主角会被
        // 渲染到 (0,0) 角落，玩家走一步才"出现"。这里多一道防线。
        var heroCheck = core.status.hero || {};
        var locCheck = heroCheck.loc || {};
        if (locCheck.x == null || locCheck.y == null) return null;

        var floorId = core.status.floorId;
        var floor = (core.status.maps && core.status.maps[floorId]) || (core.floors && core.floors[floorId]) || {};
        var hero = core.status.hero || {};
        var loc = hero.loc || {};
        var displayLoc = getHeroDisplayLoc(hero);
        var blocksObj = {};
        try { blocksObj = core.getMapBlocksObj(floorId) || {}; } catch (e) { blocksObj = {}; }

        var blocks = [];
        Object.keys(blocksObj).forEach(function (key) {
            var one = blockTo3DInfo(blocksObj[key]);
            if (one) blocks.push(one);
        });

        return {
            engine: "h5mota",
            bridgeVersion: 1,
            time: Date.now(),
            floorId: floorId,
            width: floor.width || core._WIDTH_ || 13,
            height: floor.height || core._HEIGHT_ || 13,
            hero: {
                x: displayLoc.x,
                y: displayLoc.y,
                displayX: displayLoc.displayX,
                displayY: displayLoc.displayY,
                direction: displayLoc.direction,
                moving: displayLoc.moving,
                hp: hero.hp || 0,
                atk: hero.atk || 0,
                def: hero.def || 0,
                mdef: hero.mdef || 0,
                money: hero.money || 0,
                exp: hero.exp || 0,
                lv: hero.lv || hero.level || 1,
                // 道具数量（用于状态栏 3 色"魔杖"显示，实际就是黄/蓝/红钥匙）。
                // 用 core.itemCount() 自动处理 cls 分类。
                items: (function () {
                    function n(id) {
                        try { return (typeof core.itemCount === "function") ? Number(core.itemCount(id) || 0) : 0; }
                        catch (e) { return 0; }
                    }
                    return {
                        yellowKey: n("yellowKey"),
                        blueKey: n("blueKey"),
                        redKey: n("redKey")
                    };
                })()
            },
            blocks: blocks,
            flags: {
                // 已取消“每层第一次死亡轮回”机制。
                deathLeft: null
            },
            // 对话框状态：mota 的 event.id 标记当前打开了什么对话框（save/load/toolbox/...）。
            // Godot 端读这个字段决定要不要弹镜像 UI。
            dialog: getDialogState(),
            // HUD 工具栏状态：4 个槽（破墙/飞行/错位术/锚定术）。
            // Godot 状态栏会画这 4 个槽位 + 第 5 个本地显示开关。
            hudTools: getHudToolsState(),
            // 录像播放状态：3D 端用来决定要不要显示顶部控制条
            replay: getReplayState()
        };
    }

    function getReplayState() {
        try {
            if (!core.isReplaying || !core.isReplaying()) return null;
            var r = (core.status && core.status.replay) || {};
            var total = (r.totalList && r.totalList.length) || 0;
            var left = (r.toReplay && r.toReplay.length) || 0;
            return {
                replaying: true,
                pausing: !!r.pausing,
                speed: Number(r.speed) || 1,
                steps: total - left,
                total: total
            };
        } catch (e) { return null; }
    }

    function getMota3DAnchorThreshold(floorId) {
        var text = String(floorId || '');
        var matches = text.match(/-?\d+/g);
        var n = 0;
        if (matches && matches.length) {
            n = Number(matches[matches.length - 1]);
            if (!isFinite(n)) n = 0;
        }
        return 50 + Math.max(0, Math.floor(n)) * 10;
    }

    function getHudToolsState() {
        try {
            var floorId = core.status && core.status.floorId;
            var anchorUsed = Number(core.getFlag('anchor_used_' + floorId, 0)) || 0;
            var swapUsed = Number(core.getFlag('swap_used_' + floorId, 0)) || 0;
            var anchorMark = core.getFlag('anchor_mark_' + floorId, null);
            var flyPreview = core.getFlag('fly_preview_target', null);
            var lastSkill = core.getFlag('mota3d_last_skill', null);
            return {
                selecting: String(core.getFlag('skill_selecting', '') || ''),
                skillHint: String(core.getFlag('mota3d_skill_hint', '') || ''),
                actionHint: String(core.getFlag('mota3d_action_hint', '') || ''),
                actionHintSeq: Number(core.getFlag('mota3d_action_hint_seq', 0)) || 0,
                anchorThreshold: getMota3DAnchorThreshold(floorId),
                anchorMark: anchorMark ? { x: Number(anchorMark.x), y: Number(anchorMark.y) } : null,
                flyPreview: flyPreview ? { x: Number(flyPreview.x), y: Number(flyPreview.y) } : null,
                lastSkillEvent: lastSkill ? {
                    seq: Number(lastSkill.seq || 0),
                    type: String(lastSkill.type || ''),
                    data: lastSkill.data || {}
                } : null,
                slots: [
                    {
                        id: 'pickaxe',
                        name: '破墙',
                        count: Number(core.itemCount('pickaxe')) || 0,
                        max: -1,
                        enabled: !!core.itemCount('pickaxe'),
                        plugin: 'pickaxeFriendly'
                    },
                    {
                        id: 'centerFly',
                        name: '飞行',
                        count: Number(core.itemCount('centerFly')) || 0,
                        max: -1,
                        enabled: !!core.itemCount('centerFly'),
                        plugin: 'centerFlyFriendly'
                    },
                    {
                        id: 'I343',
                        name: '错位',
                        count: Math.max(0, 2 - swapUsed),
                        max: 2,
                        enabled: swapUsed < 2,
                        plugin: 'spaceSwap'
                    },
                    {
                        id: 'I342',
                        name: '锚定',
                        count: Math.max(0, 2 - anchorUsed),
                        max: 2,
                        enabled: !anchorMark && anchorUsed < 2,
                        plugin: 'anchorPlace'
                    }
                ]
            };
        } catch (e) {
            return null;
        }
    }

    // 提取当前对话框信息给 Godot 渲染镜像 UI。返回 null 表示没在任何对话框里。
    function getDialogState() {
        var ev = core.status && core.status.event;
        if (!ev || !ev.id) return null;
        var id = ev.id;

        // ---- 存档 / 读档 + 录像 SL 面板（同一套 UI）----
        // mota 内部 replayLoad / replayRemain / replaySince 走 _keyDownSL，
        // 复用 save/load 镜像。但要给 Godot 端不同的标题。
        if (id === 'save' || id === 'load'
            || id === 'replayLoad' || id === 'replayRemain' || id === 'replaySince') {
            var data = ev.data || {};
            var page = Number(data.page || 0);       // 0-indexed
            var offset = Number(data.offset || 1);   // 1~5，offset 0 是自动存档
            var slots = [];
            for (var i = 1; i <= 5; i++) {
                var slotId = page * 10 + i;
                slots.push({
                    slotId: slotId,
                    hasData: !!(core.hasSave && core.hasSave(slotId))
                });
            }
            var autoSlot = {
                slotId: 'autoSave',
                hasData: !!(core.saves && core.saves.autosave && core.saves.autosave.data)
            };
            var maxPage = (typeof main !== 'undefined' && main.savePages) ? Number(main.savePages) : 30;
            return {
                id: id,
                page: page,
                maxPage: maxPage,
                offset: offset,
                slots: slots,
                autoSlot: autoSlot
            };
        }

        // ---- 选项列表（录像主菜单 / 同步存档系列 5 种菜单都用这）----
        // mota 用 drawChoices 把选项存在 event.ui.choices
        if (id === 'replay' || id === 'syncSave' || id === 'syncSelect'
            || id === 'localSaveSelect' || id === 'storageRemove') {
            var choicesRaw = (ev.ui && ev.ui.choices) || [];
            var choices = [];
            for (var ci = 0; ci < choicesRaw.length; ci++) {
                var c = choicesRaw[ci];
                if (typeof c === 'string') choices.push({ text: c });
                else if (c && typeof c === 'object') choices.push({
                    text: String(c.text || ''),
                    color: c.color || null,
                    disabled: !!c.disabled
                });
            }
            // 同步存档主菜单：把最后一项"返回主菜单"改成"返回游戏"
            if (id === 'syncSave' && choices.length > 0
                && choices[choices.length - 1].text === '返回主菜单') {
                choices[choices.length - 1].text = '返回游戏';
            }
            var selection = Number(ev.selection != null ? ev.selection : 0);
            var titleMap = {
                replay: '录像',
                syncSave: '存档同步',
                syncSelect: '同步存档到服务器',
                localSaveSelect: '存档至本地文件',
                storageRemove: '清空本地存档'
            };
            return {
                id: id,
                title: titleMap[id] || '选择',
                choices: choices,
                selection: selection
            };
        }

        // ---- 文本对话框（drawText）----
        // mota：event.id = 'text', event.data.list = ['...', '...']
        // list 第 0 项就是当前正在显示的那段；玩家点确定后 list.shift() 出下一段。
        if (id === 'text') {
            var list = (ev.data && ev.data.list) || [];
            var current = list[0];
            var textStr = '';
            if (typeof current === 'string') textStr = current;
            else if (current && typeof current === 'object') textStr = String(current.text || '');
            // 去掉 mota 特殊标记 \t[...] 和 \r[...] 颜色码（简单清理一下，方便 Godot 渲染）
            textStr = textStr.replace(/\\t\[[^\]]*\]/g, '').replace(/\\r\[[^\]]*\]/g, '');
            return {
                id: id,
                text: textStr,
                hasMore: list.length > 1
            };
        }

        // ---- 确认对话框（drawConfirmBox）----
        // mota：event.id = 'confirmBox', event.ui = 字符串(text),
        //       event.data = { yes: yesCallback, no: noCallback }
        // 录像播完时弹的就是这个："录像播放完毕，你想退出播放吗？"
        if (id === 'confirmBox') {
            var cbText = String(ev.ui || '');
            cbText = cbText.replace(/\\t\[[^\]]*\]/g, '').replace(/\\r\[[^\]]*\]/g, '');
            return {
                id: id,
                text: cbText,
                selection: Number(ev.selection != null ? ev.selection : 0)
            };
        }

        // ---- 其他对话框：先只告诉 id，让 Godot 知道有东西打开了 ----
        return { id: id };
    }

    function push3DState() {
        if (!window.mota3d_enabled) return false;
        try {
            var state = build3DState();
            if (!state) return false;

            // 低延迟同步版：
            // 1. 定时器从 100ms 降到 33ms；
            // 2. 但如果状态没有变化，就不增加 version，避免 Godot 无意义刷新。
            var oldTime = state.time;
            state.time = 0;
            var compareJson = JSON.stringify(state);
            state.time = oldTime;
            if (compareJson === window.__mota3d_last_compare_json) return false;
            window.__mota3d_last_compare_json = compareJson;

            window.mota3d_state = JSON.stringify(state);
            window.mota3d_state_obj = state;
            window.mota3d_state_version = (window.mota3d_state_version || 0) + 1;
            window.mota3d_last_error = "";
            return true;
        } catch (e) {
            window.mota3d_last_error = String(e && e.stack || e);
            return false;
        }
    }

    // 暴露给控制台和后续 Godot 读取。
    window.mota3d_buildState = build3DState;
    window.mota3d_pushState = push3DState;
    window.mota3d_debug = function () {
        push3DState();
        console.log("mota3d_state_version =", window.mota3d_state_version);
        console.log(window.mota3d_state_obj);
        return window.mota3d_state_obj;
    };

    // =============================================================
    // 录像系统：3D 端调用 mota 录像菜单的桥
    // =============================================================
    window.mota3dOpenReplay = function () {
        try {
            if (!core || !core.isPlaying || !core.isPlaying()) {
                console.warn('[Mota3D replay] game not playing');
                return false;
            }
            // 不能在录像播放中再开菜单（mota 自身限制）
            if (core.isReplaying && core.isReplaying()) {
                console.warn('[Mota3D replay] already replaying; press space to pause/resume');
                return false;
            }
            // 不能在锁控制中（事件、对话中等）
            if (core.status && core.status.lockControl && core.status.event && core.status.event.id) {
                console.warn('[Mota3D replay] event in progress, id=', core.status.event.id);
                return false;
            }
            core.ui._drawReplay();
            return true;
        } catch (e) {
            console.error('[Mota3D replay] open failed:', e);
            return false;
        }
    };

    // 打开存档同步菜单（E 键 / 系统按钮）
    window.mota3dOpenSyncSave = function () {
        try {
            if (!core || !core.isPlaying || !core.isPlaying()) {
                console.warn('[Mota3D syncSave] game not playing');
                return false;
            }
            if (core.isReplaying && core.isReplaying()) {
                console.warn('[Mota3D syncSave] cannot open during replay');
                return false;
            }
            if (core.status && core.status.lockControl && core.status.event && core.status.event.id) {
                console.warn('[Mota3D syncSave] event in progress, id=', core.status.event.id);
                return false;
            }
            core.ui._drawSyncSave();
            return true;
        } catch (e) {
            console.error('[Mota3D syncSave] open failed:', e);
            return false;
        }
    };

    // 3D 模式下 mota 的 inputDiv 被 CSS 隐藏到屏幕外（index.html .mota3d-active 规则）
    // myprompt / myconfirm 调出来玩家看不到。改用浏览器原生 prompt / confirm。
    // hook 的具体安装移到 setInterval 里执行（那时候 core / utils 已经就绪）
    function _installMota3DNativeDialogs() {
        if (window.__mota3d_native_dialogs_installed__) return;
        if (typeof core === 'undefined' || !core) return;   // core 还没就绪
        window.__mota3d_native_dialogs_installed__ = true;
        var nativePrompt = function (hint, value, callback) {
            // 清掉 HTML 标签（mota 内部可能 hint 含 <br/>）
            var text = String(hint || '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
            var r = window.prompt(text, String(value == null ? '' : value));
            if (typeof callback === 'function') {
                try { callback(r); } catch (e) { console.error('[Mota3D prompt cb]', e); }
            }
        };
        var nativeConfirm = function (hint, yesCb, noCb) {
            var text = String(hint || '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
            if (window.confirm(text)) {
                if (typeof yesCb === 'function') {
                    try { yesCb(); } catch (e) { console.error('[Mota3D confirm yes]', e); }
                }
            } else {
                if (typeof noCb === 'function') {
                    try { noCb(); } catch (e) { console.error('[Mota3D confirm no]', e); }
                }
            }
        };
        core.myprompt = nativePrompt;
        core.myconfirm = nativeConfirm;
        if (typeof utils !== 'undefined' && utils.prototype) {
            utils.prototype.myprompt = nativePrompt;
            utils.prototype.myconfirm = nativeConfirm;
        }
        console.log('[Mota3D] native prompt/confirm hooked');
    }

    // Godot 点击选项菜单的某一项时调这个。
    // menuId = 当前 event.id（用来防错），index = 选项序号（0-based）
    window.mota3dChoiceSelect = function (menuId, index) {
        try {
            var ev = core.status && core.status.event;
            if (!ev || ev.id !== menuId) {
                console.warn('[Mota3D choice] id mismatch:', menuId, 'actual:', ev && ev.id);
                return false;
            }
            var choices = (ev.ui && ev.ui.choices) || [];
            if (index < 0 || index >= choices.length) return false;
            // mota 内部模拟点击：调对应 _clickXxx(centerX, topIndex + index)
            var topIndex = core.actions._getChoicesTopIndex(choices.length);
            var centerX = core._HALF_WIDTH_ || core.__HALF_SIZE__ || 6;
            var handler = null;
            switch (menuId) {
                case 'replay':         handler = core.actions._clickReplay; break;
                case 'syncSave':       handler = core.actions._clickSyncSave; break;
                case 'syncSelect':     handler = core.actions._clickSyncSelect; break;
                case 'localSaveSelect':handler = core.actions._clickLocalSaveSelect; break;
                case 'storageRemove':  handler = core.actions._clickStorageRemove; break;
                default:               handler = null;
            }
            if (!handler) {
                console.warn('[Mota3D choice] no handler for', menuId);
                return false;
            }
            ev.selection = index;
            handler.call(core.actions, centerX, topIndex + index);
            return true;
        } catch (e) {
            console.error('[Mota3D choice]', e);
            return false;
        }
    };

    // 文本对话框：玩家点"确定"或按空格时调这个，推进到下一段或关闭。
    window.mota3dCloseTextDialog = function () {
        try {
            var ev = core.status && core.status.event;
            if (!ev || ev.id !== 'text') return false;
            // mota.ui.drawText() 无参就是 "推进到下一段或关闭"
            core.ui.drawText();
            return true;
        } catch (e) {
            console.error('[Mota3D text close]', e);
            return false;
        }
    };

    // 确认对话框（drawConfirmBox）应答：yes=true 调"确定"，false 调"取消"
    // 录像播完弹的就是这个。
    window.mota3dConfirmBoxAnswer = function (yes) {
        try {
            var ev = core.status && core.status.event;
            if (!ev || ev.id !== 'confirmBox') return false;
            var data = ev.data || {};
            // closePanel 必须先调，避免 callback 里又开 panel 时状态冲突
            if (core.ui && core.ui.closePanel) core.ui.closePanel();
            if (yes) {
                if (typeof data.yes === 'function') data.yes();
            } else {
                if (typeof data.no === 'function') data.no();
            }
            return true;
        } catch (e) {
            console.error('[Mota3D confirmBox]', e);
            return false;
        }
    };

    // 取消当前选项菜单（ESC / 返回）。每种菜单 mota 都把"返回"做成最后一个选项，
    // 所以等价于点击最后一项。
    window.mota3dChoiceCancel = function () {
        try {
            var ev = core.status && core.status.event;
            if (!ev || !ev.id) return false;
            var choices = (ev.ui && ev.ui.choices) || [];
            if (choices.length === 0) return false;
            return window.mota3dChoiceSelect(ev.id, choices.length - 1);
        } catch (e) { return false; }
    };

    // =============================================================
    // 录像播放控制：暂停/恢复/加速/减速/单步/停止
    // 3D 端顶部控制条按钮直接调用这些。
    // =============================================================
    window.mota3dReplayToggle = function () {
        try { if (core.triggerReplay) core.triggerReplay(); } catch (e) {}
    };
    window.mota3dReplaySpeedUp = function () {
        try { if (core.speedUpReplay) core.speedUpReplay(); } catch (e) {}
    };
    window.mota3dReplaySpeedDown = function () {
        try { if (core.speedDownReplay) core.speedDownReplay(); } catch (e) {}
    };
    window.mota3dReplaySetSpeed = function (s) {
        try { if (core.setReplaySpeed) core.setReplaySpeed(Number(s) || 1); } catch (e) {}
    };
    window.mota3dReplayStep = function () {
        try { if (core.stepReplay) core.stepReplay(); } catch (e) {}
    };
    window.mota3dReplayRewind = function () {
        try { if (core.rewindReplay) core.rewindReplay(); } catch (e) {}
    };
    window.mota3dReplayStop = function () {
        try { if (core.stopReplay) core.stopReplay(); } catch (e) {}
    };
    // 录像中存档（H5 中是 S 键的功能）— 调 core.control._replay_SL
    // 注意：mota 要求必须先暂停录像才能存档（内部会自己 drawTip 提示）
    window.mota3dReplaySL = function () {
        try {
            if (core.control && core.control._replay_SL) {
                core.control._replay_SL();
            } else if (core._replay_SL) {
                core._replay_SL();
            }
        } catch (e) { console.error('[Mota3D replay SL]', e); }
    };

    // Step5C2：低延迟同步。
    // 原来是 100ms，视觉上会感觉 Godot 慢一拍；
    // 现在改成约 30fps 同步，同时 push3DState 内部会过滤无变化状态。
    if (window.__mota3d_timer) clearInterval(window.__mota3d_timer);
    window.__mota3d_timer = setInterval(function () {
        if (window.core && core.status && core.status.floorId) {
            _installMota3DNativeDialogs();   // core 就绪后一次性 hook 原生对话
            push3DState();
        }
    }, 33);


        // Mota3D key policy：彻底禁用 WASD 作为移动/视角键。
        // 用户键位固定：A=回档，S=存档，D=读档，X=手册，R=回放预留，V=商店，ESC=只取消。
        if (!window.__mota3d_user_key_policy_installed__) {
            window.__mota3d_user_key_policy_installed__ = true;
            function sendHudKey(slot) {
                window.mota3d_hud_key_seq = (Number(window.mota3d_hud_key_seq) || 0) + 1;
                window.mota3d_hud_key_press = { seq: window.mota3d_hud_key_seq, slot: slot };
            }
            function isBusy() {
                try { return !core || !core.isPlaying || !core.isPlaying(); } catch (e) { return true; }
            }
            function stop(ev) {
                try { ev.stopImmediatePropagation(); ev.stopPropagation(); ev.preventDefault(); } catch (e) {}
                return false;
            }
            function loadAuto() {
                try {
                    if (core.saves && core.saves.autosave && core.saves.autosave.data && core.saves.autosave.data.length > 0) {
                        core.doSL('autoSave', 'load');
                        if (typeof reinstallMota3DAfterLoad === 'function') reinstallMota3DAfterLoad();
                    } else core.drawTip('暂无自动存档');
                } catch (err) { console.error('[Mota3D load auto]', err); }
            }
            function openSaveDialog() {
                // mota 原生 S 键：core.save(true) 弹存档面板
                try { if (core.save) core.save(true); }
                catch (err) { console.error('[Mota3D save dlg]', err); }
            }
            function openLoadDialog() {
                // mota 原生 D 键：core.load(true) 弹读档面板
                try { if (core.load) core.load(true); }
                catch (err) { console.error('[Mota3D load dlg]', err); }
            }
            function openShop() {
                try {
                    var el = document.getElementById('img-shop');
                    if (el) { el.click(); return; }
                    core.drawTip('商店功能预留');
                } catch (err) { console.error('[Mota3D shop key]', err); }
            }
            function isTextEditingEvent(e) {
                try {
                    var t = e && e.target;
                    if (!t && document.activeElement) t = document.activeElement;
                    if (!t) return false;
                    var tag = (t.tagName || '').toLowerCase();
                    if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
                    if (t.isContentEditable) return true;
                    if (typeof t.closest === 'function' && t.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')) return true;
                } catch (err) {}
                return false;
            }
            document.addEventListener('keydown', function (e) {
                if (isTextEditingEvent(e)) return;
                var code = e.keyCode || 0;
                // W/A/S/D 一律先拦下，避免它们被 H5 或相机当成方向。
                // E (69) 也拦：用作打开存档同步菜单
                if (code === 87 || code === 65 || code === 83 || code === 68 || code === 88 || code === 82 || code === 86 || code === 27 || code === 69) return stop(e);
            }, true);
            document.addEventListener('keyup', function (e) {
                if (isTextEditingEvent(e)) return;
                var code = e.keyCode || 0;
                if (code === 87) return stop(e); // W 禁用
                if (isBusy() && code !== 27) return;
                if (code === 65) { loadAuto(); return stop(e); }
                if (code === 83) { openSaveDialog(); return stop(e); }   // S：弹存档界面
                if (code === 68) { openLoadDialog(); return stop(e); }   // D：弹读档界面
                if (code === 88) { sendHudKey(88); return stop(e); }
                if (code === 82) {
                    // R 键：打开录像菜单（Godot 镜像）
                    try { if (typeof window.mota3dOpenReplay === 'function') window.mota3dOpenReplay(); } catch (eR) {}
                    return stop(e);
                }
                if (code === 69) {
                    // E 键：打开存档同步菜单（Godot 镜像）
                    try { if (typeof window.mota3dOpenSyncSave === 'function') window.mota3dOpenSyncSave(); } catch (eE) {}
                    return stop(e);
                }
                if (code === 86) { openShop(); return stop(e); }
                if (code === 27) { sendHudKey(27); return stop(e); }
            }, true);
        }

})();
