///<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();
			// 氛围特效：火把闪烁、雾、萤火虫、暗角、呼吸魔法光等。
			if (core.plugin.installAmbientFx) core.plugin.installAmbientFx();
		}
	},
	"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();
		}
	},

	"ambientFx": function () {

	    // ============================================================
	    // 氛围特效 v2 - 带诊断模式
	    //
	    // 左上角会出现一个【红绿闪烁的方块】= 诊断指示器，只要它出现
	    // 就说明插件已经在运行。不想要可以把下方 DEBUG_INDICATOR 改成 false。
	    // ============================================================

	    var DEBUG_INDICATOR = false; // 左上角调试方块开关
	    var VERBOSE_LOG     = false; // 控制台打印日志

	    // —— 每层楼配置 ——
	    // 单位：格子（左上(0,0)，每格32px），可用小数
	    // 通用模板：只保留萤火虫，其他全关
	    var DUNGEON = {
	        torchColor:     [255, 165,  70],
	        torchCoreColor: [255, 230, 170],
	        embers:    { enabled: false },
	        fireflies: { enabled: true, count: 5, hueRange: [55, 100] },
	        fog:       { enabled: false },
	        tint:      null,
	        vignette:  0
	    };

	    var CONFIG = {
	        // ★ 1楼，背景 333.png ★
	        "MT0": Object.assign({}, DUNGEON, {
	            torches: [],  // 火把全关
	            magicLights: [
	                // 蓝色水晶光晕
	                { x: 6.0, y: 7.0, r: 45, color: [110, 200, 255], period: 2.8 },
	                // 绿色水晶光晕（往下 0.4 格）
	                { x: 6.0, y: 8.9, r: 38, color: [120, 255, 160], period: 2.2 }
	            ],
	            // ★ 冰封模式（轮回/复活后）：萤火虫青色、水晶冷光 ★
	            iceOverride: {
	                fireflies: { hueRange: [175, 205] },  // 青→淡蓝
	                magicLights: [
	                    { x: 6.0, y: 7.0, r: 50, color: [140, 230, 255], period: 2.8 },
	                    { x: 6.0, y: 8.9, r: 42, color: [180, 240, 255], period: 2.2 }
	                ]
	            }
	        }),
	        // 2楼，背景 335.png
	        "MT1": Object.assign({}, DUNGEON, {
	            torches: [],
	            magicLights: []
	        })
	    };

	    // —— 内部状态 ——
	    var state = {
	        currentFloor: null,
	        timeMs:       0,
	        lastTs:       0,
	        fireflies:    [],
	        embers:       [],
	        fogTex:       null,
	        frame:        0,
	        heroDust:     [],
	        heroPrints:   [],
	        heroLastPx:   -1,
	        heroLastPy:   -1,
	        heroLastCell: '',
	        heroDustTime: 0,
	        bursts:       [],
	        damageNums:   [],
	        shake:        { intensity: 0, time: 0, duration: 0, _wasSet: false },
	        hurt:         { intensity: 0, time: 0, duration: 0 },
	        reveal:       { active: false, time: 0, blackDur: 0.5, sweepDur: 1.0, fadeDur: 0.18, locked: false },
	        weatherP:     [],          // 天气粒子
	        statTracker:  { floorId: null, atk: null, def: null, hp: null }  // 成长/寄生 用
	    };

	    // —— 朝向→单位向量 ——
	    var DIR_DELTA = {
	        up:    { x:  0, y: -1 },
	        down:  { x:  0, y:  1 },
	        left:  { x: -1, y:  0 },
	        right: { x:  1, y:  0 }
	    };

	    // 取角色实际像素位置（含移动动画中间帧）
	    function getHeroPixel() {
	        if (!core.getHeroLoc) return null;
	        var loc = core.getHeroLoc();
	        if (!loc) return null;
	        var moving = (core.status && core.status.heroMoving) || 0;
	        var d = DIR_DELTA[loc.direction] || { x: 0, y: 0 };
	        return {
	            x: loc.x * 32 + 16 + d.x * moving * 4,
	            y: loc.y * 32 + 16 + d.y * moving * 4,
	            gx: loc.x, gy: loc.y,
	            dir: loc.direction,
	            moving: moving > 0
	        };
	    }

	    function smoothNoise(t) {
	        return (Math.sin(t * 1.7) * 0.5
	              + Math.sin(t * 0.9 + 1.3) * 0.3
	              + Math.sin(t * 2.3 + 2.1) * 0.2) * 0.5 + 0.5;
	    }

	    function buildFogTexture() {
	        var size = 256;
	        var c = document.createElement('canvas');
	        c.width = size; c.height = size;
	        var ctx = c.getContext('2d');
	        var img = ctx.createImageData(size, size);
	        for (var y = 0; y < size; y++) {
	            for (var x = 0; x < size; x++) {
	                var u = x / size, v = y / size;
	                var n =
	                    Math.sin(u * Math.PI * 2)     * Math.cos(v * Math.PI * 2)     * 0.40 +
	                    Math.sin(u * Math.PI * 4 + 1.7) * Math.cos(v * Math.PI * 4 + 0.4) * 0.25 +
	                    Math.sin(u * Math.PI * 8 + 3.1) * Math.cos(v * Math.PI * 6 + 2.2) * 0.18 +
	                    Math.sin(u * Math.PI * 16 + 5.5) * Math.cos(v * Math.PI * 12 + 4.4) * 0.10;
	                n = (n + 1) * 0.5;
	                n = Math.pow(n, 1.6);
	                var i = (y * size + x) * 4;
	                img.data[i  ] = 255;
	                img.data[i+1] = 255;
	                img.data[i+2] = 255;
	                img.data[i+3] = Math.floor(n * 230);
	            }
	        }
	        ctx.putImageData(img, 0, 0);
	        return c;
	    }

	    function initParticles(cfg, W, H) {
	        state.fireflies = [];
	        if (cfg.fireflies && cfg.fireflies.enabled) {
	            for (var i = 0; i < cfg.fireflies.count; i++) {
	                var h0 = cfg.fireflies.hueRange[0], h1 = cfg.fireflies.hueRange[1];
	                state.fireflies.push({
	                    x: Math.random() * W, y: Math.random() * H,
	                    vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3,
	                    phase: Math.random() * Math.PI * 2,
	                    speed: 1.5 + Math.random() * 2.0,
	                    hue: h0 + Math.random() * (h1 - h0),
	                    size: 1.0 + Math.random() * 0.8,         // 萤火虫整体大小系数
	                    angle: Math.random() * Math.PI * 2,       // 朝向
	                    wingPhase: Math.random() * Math.PI * 2,   // 翅膀拍打相位
	                    wingSpeed: 18 + Math.random() * 8,        // 翅膀拍打频率
	                    restTimer: 0                              // 偶尔悬停
	                });
	            }
	        }
	        state.embers = [];
	        if (cfg.embers && cfg.embers.enabled && cfg.torches) {
	            cfg.torches.forEach(function (t) {
	                for (var k = 0; k < cfg.embers.perTorch; k++) {
	                    state.embers.push({
	                        sx: t.x * 32 + 16, sy: t.y * 32 + 16,
	                        x:  t.x * 32 + 16, y:  t.y * 32 + 16,
	                        vx: 0, vy: 0,
	                        life: Math.random() * 2,
	                        maxLife: 1.4 + Math.random() * 1.2
	                    });
	                }
	            });
	        }
	        if (!state.fogTex) state.fogTex = buildFogTexture();
	        initWeather(cfg, W, H);
	    }

	    // ============================================================
	    // 天气系统：雪 / 魔法尘屑 / 雨 / 落叶 / 火星
	    // ============================================================
	    function initWeather(cfg, W, H) {
	        state.weatherP = [];
	        var wcfg = cfg && cfg.weather;
	        if (!wcfg || !wcfg.type) return;

	        var intensity = Math.max(1, Math.min(10, wcfg.intensity || 5));
	        var count = Math.round(intensity * 14);

	        if (wcfg.type === 'snow') {
	            for (var i = 0; i < count; i++) {
	                state.weatherP.push({
	                    type: 'snow',
	                    x: Math.random() * W,
	                    y: Math.random() * H,
	                    vy: 0.30 + Math.random() * 0.75,
	                    r:  0.9 + Math.random() * 1.8,
	                    wind: Math.random() * Math.PI * 2,
	                    windSpeed: 0.6 + Math.random() * 0.6,
	                    windAmp: 0.3 + Math.random() * 0.5
	                });
	            }
	        }
	        else if (wcfg.type === 'sparkles') {
	            for (var i = 0; i < count; i++) {
	                state.weatherP.push({
	                    type: 'sparkles',
	                    x: Math.random() * W,
	                    y: Math.random() * H,
	                    vy: -0.10 - Math.random() * 0.20,  // 向上飘
	                    vx: (Math.random() - 0.5) * 0.06,
	                    r: 0.7 + Math.random() * 1.3,
	                    twinkle: Math.random() * Math.PI * 2,
	                    twinkleSpeed: 2 + Math.random() * 3,
	                    hue: 38 + Math.random() * 26       // 暖金~黄
	                });
	            }
	        }
	        else if (wcfg.type === 'rain') {
	            for (var i = 0; i < count * 1.5; i++) {
	                state.weatherP.push({
	                    type: 'rain',
	                    x: Math.random() * (W + 60) - 30,
	                    y: Math.random() * H,
	                    vy: 4 + Math.random() * 3,
	                    vx: 1.2,
	                    length: 10 + Math.random() * 10
	                });
	            }
	        }
	        else if (wcfg.type === 'leaves') {
	            for (var i = 0; i < count * 0.5; i++) {
	                state.weatherP.push({
	                    type: 'leaves',
	                    x: Math.random() * W,
	                    y: Math.random() * H,
	                    vy: 0.3 + Math.random() * 0.4,
	                    spin: Math.random() * Math.PI * 2,
	                    vspin: (Math.random() - 0.5) * 0.06,
	                    swing: Math.random() * Math.PI * 2,
	                    swingSpeed: 0.7 + Math.random() * 0.8,
	                    size: 3 + Math.random() * 2.5,
	                    hue: 18 + Math.random() * 35       // 棕→橙
	                });
	            }
	        }
	        else if (wcfg.type === 'embers') {
	            for (var i = 0; i < count; i++) {
	                state.weatherP.push({
	                    type: 'embers',
	                    x: Math.random() * W,
	                    y: Math.random() * H,
	                    vy: -0.4 - Math.random() * 0.5,
	                    vx: (Math.random() - 0.5) * 0.15,
	                    r: 1.2 + Math.random() * 1.4,
	                    life: Math.random(),
	                    maxLife: 2 + Math.random() * 2
	                });
	            }
	        }
	    }

	    function drawWeather(ctx, dt, W, H) {
	        if (!state.weatherP.length) return;
	        var dtSec = dt / 1000;
	        var prev = ctx.globalCompositeOperation;

	        state.weatherP.forEach(function (p) {
	            // ===== SNOW =====
	            if (p.type === 'snow') {
	                p.wind += dtSec * p.windSpeed;
	                p.x += Math.sin(p.wind) * p.windAmp;
	                p.y += p.vy;
	                if (p.y > H + 5) { p.y = -5; p.x = Math.random() * W; }
	                if (p.x < -5)    { p.x = W + 5; }
	                if (p.x > W + 5) { p.x = -5; }

	                ctx.globalCompositeOperation = 'source-over';
	                ctx.fillStyle = 'rgba(255,255,255,' + (0.55 + p.r * 0.08).toFixed(3) + ')';
	                ctx.beginPath();
	                ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
	                ctx.fill();
	            }
	            // ===== SPARKLES（魔法尘屑） =====
	            else if (p.type === 'sparkles') {
	                p.x += p.vx + Math.sin(p.twinkle * 0.5) * 0.08;
	                p.y += p.vy;
	                p.twinkle += dtSec * p.twinkleSpeed;
	                if (p.y < -5) { p.y = H + 5; p.x = Math.random() * W; }
	                if (p.x < -5) p.x = W + 5;
	                if (p.x > W + 5) p.x = -5;

	                var glow = Math.sin(p.twinkle) * 0.5 + 0.5;
	                var a = 0.15 + glow * 0.65;

	                ctx.globalCompositeOperation = 'lighter';
	                // 外辉光
	                var g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 4);
	                g.addColorStop(0, 'hsla(' + p.hue + ',100%,75%,' + (a * 0.8).toFixed(3) + ')');
	                g.addColorStop(1, 'hsla(' + p.hue + ',100%,75%,0)');
	                ctx.fillStyle = g;
	                ctx.beginPath();
	                ctx.arc(p.x, p.y, p.r * 4, 0, Math.PI * 2);
	                ctx.fill();
	                // 亮核
	                ctx.fillStyle = 'hsla(' + p.hue + ',100%,92%,' + a.toFixed(3) + ')';
	                ctx.beginPath();
	                ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
	                ctx.fill();
	            }
	            // ===== RAIN =====
	            else if (p.type === 'rain') {
	                p.x += p.vx;
	                p.y += p.vy;
	                if (p.y > H + 5) { p.y = -5 - Math.random() * 20; p.x = Math.random() * (W + 60) - 30; }
	                if (p.x > W + 5) p.x = -5;

	                ctx.globalCompositeOperation = 'source-over';
	                ctx.strokeStyle = 'rgba(170,200,225,0.55)';
	                ctx.lineWidth = 1;
	                ctx.beginPath();
	                ctx.moveTo(p.x, p.y);
	                ctx.lineTo(p.x - p.vx * 1.5, p.y - p.length);
	                ctx.stroke();
	            }
	            // ===== LEAVES =====
	            else if (p.type === 'leaves') {
	                p.swing += dtSec * p.swingSpeed;
	                p.x += Math.sin(p.swing) * 0.6;
	                p.y += p.vy;
	                p.spin += p.vspin;
	                if (p.y > H + 8) { p.y = -8; p.x = Math.random() * W; }

	                ctx.globalCompositeOperation = 'source-over';
	                ctx.save();
	                ctx.translate(p.x, p.y);
	                ctx.rotate(p.spin);
	                ctx.fillStyle = 'hsl(' + p.hue + ',70%,52%)';
	                // 简单叶片形（菱形）
	                ctx.beginPath();
	                ctx.moveTo(0, -p.size);
	                ctx.lineTo(p.size * 0.6, 0);
	                ctx.lineTo(0, p.size);
	                ctx.lineTo(-p.size * 0.6, 0);
	                ctx.closePath();
	                ctx.fill();
	                ctx.restore();
	            }
	            // ===== EMBERS =====
	            else if (p.type === 'embers') {
	                p.x += p.vx;
	                p.y += p.vy;
	                p.life += dtSec;
	                if (p.life >= p.maxLife || p.y < -5) {
	                    p.life = 0;
	                    p.x = Math.random() * W;
	                    p.y = H + 5;
	                    p.vy = -0.4 - Math.random() * 0.5;
	                }
	                var k = p.life / p.maxLife;
	                var a = (1 - k * 0.7) * 0.85;
	                ctx.globalCompositeOperation = 'lighter';
	                var col = [255, Math.floor(180 - k * 80), Math.floor(60 + k * 20)];
	                var g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3);
	                g.addColorStop(0, 'rgba(' + col.join(',') + ',' + a.toFixed(3) + ')');
	                g.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
	                ctx.fillStyle = g;
	                ctx.beginPath();
	                ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2);
	                ctx.fill();
	                ctx.fillStyle = 'rgba(' + col.join(',') + ',' + a.toFixed(3) + ')';
	                ctx.beginPath();
	                ctx.arc(p.x, p.y, p.r * 0.6, 0, Math.PI * 2);
	                ctx.fill();
	            }
	        });

	        ctx.globalCompositeOperation = prev;
	    }

	    function drawTorches(ctx, cfg, t) {
	        if (!cfg.torches) return;
	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';
	        cfg.torches.forEach(function (torch, idx) {
	            var fast = smoothNoise(t * 0.012 + idx * 1.7);
	            var slow = smoothNoise(t * 0.0018 + idx * 4.3);
	            var intensity = 0.55 + fast * 0.15 + slow * 0.15;
	            var r = (torch.r || 70) * (0.86 + slow * 0.22 + fast * 0.05);
	            var cx = torch.x * 32 + 16;
	            var cy = torch.y * 32 + 16;
	            var col = cfg.torchColor || [255, 165, 70];
	            var grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
	            grd.addColorStop(0.00, 'rgba(' + col.join(',') + ',' + (0.38 * intensity).toFixed(3) + ')');
	            grd.addColorStop(0.35, 'rgba(' + col.join(',') + ',' + (0.15 * intensity).toFixed(3) + ')');
	            grd.addColorStop(1.00, 'rgba(' + col.join(',') + ',0)');
	            ctx.fillStyle = grd;
	            ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
	            var core = cfg.torchCoreColor || [255, 230, 170];
	            var rc = r * 0.22 * (0.9 + fast * 0.15);
	            var gc = ctx.createRadialGradient(cx, cy, 0, cx, cy, rc);
	            gc.addColorStop(0, 'rgba(' + core.join(',') + ',' + (0.55 * intensity).toFixed(3) + ')');
	            gc.addColorStop(1, 'rgba(' + core.join(',') + ',0)');
	            ctx.fillStyle = gc;
	            ctx.beginPath(); ctx.arc(cx, cy, rc, 0, Math.PI * 2); ctx.fill();
	        });
	        ctx.globalCompositeOperation = prev;
	    }

	    function drawMagic(ctx, cfg, tMs) {
	        if (!cfg.magicLights) return;
	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';
	        cfg.magicLights.forEach(function (m, idx) {
	            var period = (m.period || 2.5) * 1000;
	            var phase = (tMs / period) * Math.PI * 2 + idx;
	            var pulse = (Math.sin(phase) * 0.5 + 0.5);
	            var intensity = 0.6 + pulse * 0.5;
	            var r = (m.r || 50) * (0.8 + pulse * 0.35);
	            var cx = m.x * 32 + 16;
	            var cy = m.y * 32 + 16;
	            var c = m.color || [110, 200, 255];
	            var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
	            g.addColorStop(0.00, 'rgba(' + c.join(',') + ',' + (0.80 * intensity).toFixed(3) + ')');
	            g.addColorStop(0.40, 'rgba(' + c.join(',') + ',' + (0.30 * intensity).toFixed(3) + ')');
	            g.addColorStop(1.00, 'rgba(' + c.join(',') + ',0)');
	            ctx.fillStyle = g;
	            ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
	        });
	        ctx.globalCompositeOperation = prev;
	    }

	    function drawEmbers(ctx, dt) {
	        if (!state.embers.length) return;
	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';
	        var dtSec = dt / 1000;
	        state.embers.forEach(function (e) {
	            e.life += dtSec;
	            if (e.life > e.maxLife) {
	                e.life = 0;
	                e.x = e.sx + (Math.random() - 0.5) * 4;
	                e.y = e.sy;
	                e.vx = (Math.random() - 0.5) * 0.3;
	                e.vy = -0.5 - Math.random() * 0.6;
	            }
	            e.vx += (Math.random() - 0.5) * 0.05;
	            e.vy += -0.005;
	            e.x += e.vx;
	            e.y += e.vy;
	            var k = e.life / e.maxLife;
	            var alpha = (1 - k) * 0.9;
	            var size = 2.0 * (1 - k * 0.5);
	            var rr = 255;
	            var gg = Math.floor(200 - k * 130);
	            var bb = Math.floor(80 - k * 70);
	            ctx.fillStyle = 'rgba(' + rr + ',' + gg + ',' + bb + ',' + alpha.toFixed(3) + ')';
	            ctx.beginPath(); ctx.arc(e.x, e.y, size, 0, Math.PI * 2); ctx.fill();
	        });
	        ctx.globalCompositeOperation = prev;
	    }

	    function drawFireflies(ctx, dt, W, H) {
	        if (!state.fireflies.length) return;
	        var dtSec = dt / 1000;
	        state.fireflies.forEach(function (f) {
	            // —— 运动：偶尔悬停，否则蜿蜒飘动 ——
	            f.restTimer -= dtSec;
	            if (f.restTimer <= 0) {
	                if (Math.random() < 0.002) {
	                    // 进入悬停状态 0.4-1.0s（不要太久，要继续逛）
	                    f.restTimer = 0.4 + Math.random() * 0.6;
	                    f.vx *= 0.3; f.vy *= 0.3;
	                } else {
	                    f.vx += (Math.random() - 0.5) * 0.12;
	                    f.vy += (Math.random() - 0.5) * 0.12;
	                }
	            } else {
	                // 悬停时只做微小摆动
	                f.vx += (Math.random() - 0.5) * 0.015;
	                f.vy += (Math.random() - 0.5) * 0.015;
	            }
	            // 限制最大速度并保留更多惯性，让萤火虫飞得更远
	            f.vx *= 0.985; f.vy *= 0.985;
	            var maxV = 1.4;
	            var sp0 = Math.sqrt(f.vx * f.vx + f.vy * f.vy);
	            if (sp0 > maxV) {
	                f.vx = f.vx / sp0 * maxV;
	                f.vy = f.vy / sp0 * maxV;
	            }
	            f.x += f.vx; f.y += f.vy;
	            if (f.x < 6)     { f.x = 6;     f.vx = Math.abs(f.vx); }
	            if (f.x > W - 6) { f.x = W - 6; f.vx = -Math.abs(f.vx); }
	            if (f.y < 6)     { f.y = 6;     f.vy = Math.abs(f.vy); }
	            if (f.y > H - 6) { f.y = H - 6; f.vy = -Math.abs(f.vy); }

	            // 朝向逐渐转向运动方向
	            var sp = Math.sqrt(f.vx * f.vx + f.vy * f.vy);
	            if (sp > 0.05) {
	                var targetAngle = Math.atan2(f.vy, f.vx);
	                var diff = targetAngle - f.angle;
	                while (diff > Math.PI)  diff -= Math.PI * 2;
	                while (diff < -Math.PI) diff += Math.PI * 2;
	                f.angle += diff * 0.15;
	            }

	            // 翅膀拍打
	            f.wingPhase += dtSec * f.wingSpeed;
	            var wingFlap = Math.abs(Math.sin(f.wingPhase));  // 0..1

	            // 屁股发光呼吸
	            f.phase += dtSec * f.speed;
	            var glow = Math.sin(f.phase) * 0.5 + 0.5;

	            // ============ 开始绘制单只萤火虫 ============
	            ctx.save();
	            ctx.translate(f.x, f.y);
	            // 朝向：让虫的"头"朝飞行方向，加+PI/2因为身体竖着画
	            ctx.rotate(f.angle + Math.PI / 2);

	            var s = f.size;
	            var hue = f.hue;

	            // —— 翅膀（淡淡的扇动光晕，没有躯体）——
	            var wingScale = 0.35 + wingFlap * 0.65;
	            var wingAlpha = 0.10 + wingFlap * 0.10;
	            ctx.fillStyle = 'rgba(230, 240, 255,' + wingAlpha.toFixed(3) + ')';
	            ctx.beginPath();
	            ctx.ellipse(-2 * s * wingScale, 0, 2.6 * s * wingScale, 1.2 * s, 0, 0, Math.PI * 2);
	            ctx.fill();
	            ctx.beginPath();
	            ctx.ellipse( 2 * s * wingScale, 0, 2.6 * s * wingScale, 1.2 * s, 0, 0, Math.PI * 2);
	            ctx.fill();

	            // —— 屁股的灯（核心发光体）——
	            ctx.globalCompositeOperation = 'lighter';
	            var glowR = (4.5 + glow * 3) * s;
	            var grd = ctx.createRadialGradient(0, 0, 0, 0, 0, glowR);
	            var a0 = (0.65 + glow * 0.35).toFixed(3);
	            grd.addColorStop(0, 'hsla(' + hue + ',100%,80%,' + a0 + ')');
	            grd.addColorStop(0.5, 'hsla(' + hue + ',100%,70%,' + (a0 * 0.4) + ')');
	            grd.addColorStop(1, 'hsla(' + hue + ',100%,65%,0)');
	            ctx.fillStyle = grd;
	            ctx.beginPath();
	            ctx.arc(0, 0, glowR, 0, Math.PI * 2);
	            ctx.fill();
	            // 亮核
	            ctx.fillStyle = 'hsla(' + hue + ',100%,95%,' + (0.8 + glow * 0.2).toFixed(3) + ')';
	            ctx.beginPath();
	            ctx.arc(0, 0, 1.0 * s, 0, Math.PI * 2);
	            ctx.fill();

	            ctx.restore();
	        });
	    }

	    function drawFog(ctx, cfg, tMs, W, H) {
	        if (!cfg.fog || !cfg.fog.enabled || !state.fogTex) return;
	        var op = cfg.fog.opacity != null ? cfg.fog.opacity : 0.2;
	        var spd = cfg.fog.speed   != null ? cfg.fog.speed   : 0.012;
	        var col = cfg.fog.color   || [200, 215, 225];
	        var prevOp = ctx.globalCompositeOperation;
	        ctx.globalAlpha = op;
	        ctx.globalCompositeOperation = 'screen';
	        var ox1 = (tMs * spd) % 256;
	        var oy1 = (tMs * spd * 0.35) % 256;
	        var ox2 = (-tMs * spd * 0.6) % 256;
	        var oy2 = (tMs * spd * 0.5) % 256;
	        for (var i = -1; i <= Math.ceil(W / 256); i++) {
	            for (var j = -1; j <= Math.ceil(H / 256); j++) {
	                ctx.drawImage(state.fogTex, i * 256 - ox1, j * 256 - oy1);
	            }
	        }
	        ctx.globalAlpha = op * 0.7;
	        for (var i = -1; i <= Math.ceil(W / 256); i++) {
	            for (var j = -1; j <= Math.ceil(H / 256); j++) {
	                ctx.drawImage(state.fogTex, i * 256 - ox2, j * 256 - oy2);
	            }
	        }
	        ctx.globalAlpha = 1;
	        ctx.globalCompositeOperation = 'multiply';
	        ctx.fillStyle = 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',' + (op * 0.6).toFixed(3) + ')';
	        ctx.fillRect(0, 0, W, H);
	        ctx.globalCompositeOperation = prevOp;
	    }

	    function drawTintVignette(ctx, cfg, W, H) {
	        if (cfg.tint) {
	            ctx.fillStyle = cfg.tint;
	            ctx.fillRect(0, 0, W, H);
	        }
	        if (cfg.vignette && cfg.vignette > 0) {
	            var cx = W / 2, cy = H / 2;
	            var rInner = Math.min(W, H) * 0.30;
	            var rOuter = Math.max(W, H) * 0.75;
	            var g = ctx.createRadialGradient(cx, cy, rInner, cx, cy, rOuter);
	            g.addColorStop(0, 'rgba(0,0,0,0)');
	            g.addColorStop(1, 'rgba(0,0,0,' + cfg.vignette + ')');
	            ctx.fillStyle = g;
	            ctx.fillRect(0, 0, W, H);
	        }
	    }

	    // ============================================================
	    // 物品呼吸 + 拾取爆光
	    // ============================================================

	    // 物品ID映射到主题色（RGB）
	    function getItemRGB(itemId) {
	        if (!itemId) return [255, 230, 150];
	        var id = String(itemId).toLowerCase();
	        if (id.indexOf('yellowkey')  >= 0) return [255, 215, 100];
	        if (id.indexOf('bluekey')    >= 0) return [120, 180, 255];
	        if (id.indexOf('redkey')     >= 0) return [255, 110, 110];
	        if (id.indexOf('greenkey')   >= 0) return [120, 255, 160];
	        if (id.indexOf('redgem')     >= 0 || id.indexOf('redpotion')    >= 0) return [255, 110, 110];
	        if (id.indexOf('bluegem')    >= 0 || id.indexOf('bluepotion')   >= 0) return [120, 170, 255];
	        if (id.indexOf('greengem')   >= 0 || id.indexOf('greenpotion')  >= 0) return [120, 255, 160];
	        if (id.indexOf('yellowgem')  >= 0 || id.indexOf('yellowpotion') >= 0) return [255, 220, 110];
	        if (id.indexOf('pinkgem')    >= 0 || id.indexOf('pinkpotion')   >= 0) return [255, 150, 200];
	        if (id.indexOf('potion')     >= 0) return [255, 150, 200];
	        if (id.indexOf('sword')      >= 0) return [220, 230, 255];
	        if (id.indexOf('shield')     >= 0) return [200, 220, 240];
	        if (id.indexOf('book')       >= 0) return [180, 140, 255];
	        if (id.indexOf('gold')       >= 0 || id.indexOf('coin') >= 0) return [255, 220, 120];
	        return [255, 230, 150];     // 默认暖金
	    }

	    function drawItemHighlights(ctx, tMs, iceMode) {
	        if (!core.getMapBlocksObj) return;
	        var blocks;
	        try { blocks = core.getMapBlocksObj(); } catch (e) { return; }
	        if (!blocks) return;

	        var prev = ctx.globalCompositeOperation;
	        var t = tMs * 0.001;

	        for (var key in blocks) {
	            var b = blocks[key];
	            if (!b || !b.event || b.event.trigger !== 'getItem' || b.disable) continue;

	            var cx = b.x * 32 + 16;
	            var cy = b.y * 32 + 16;
	            var rgb = getItemRGB(b.event.id);

	            // 每个物品用自己的相位，避免全屏一起呼吸
	            var phase = (b.x * 13 + b.y * 7) * 0.5;
	            var pulse = Math.sin(t * 1.8 + phase) * 0.5 + 0.5;       // 0..1, ~3.5s
	            var bob   = Math.sin(t * 1.4 + phase) * 1.2;             // 上下浮动 ±1.2px

	            // —— 1) 物品下方的接地阴影（与浮动相反：上去时影子小）——
	            var shadowR = 7 - bob * 0.5;
	            var shadowA = 0.18 - bob * 0.02;
	            ctx.globalCompositeOperation = 'source-over';
	            ctx.fillStyle = 'rgba(0,0,0,' + Math.max(0.05, shadowA).toFixed(3) + ')';
	            ctx.beginPath();
	            ctx.ellipse(cx, cy + 13, Math.max(3, shadowR), Math.max(3, shadowR) * 0.4, 0, 0, Math.PI * 2);
	            ctx.fill();

	            // —— 2) 物品的呼吸光晕（additive，颜色基于物品类型）——
	            ctx.globalCompositeOperation = 'lighter';
	            var r = 14 + pulse * 5;
	            var a = 0.16 + pulse * 0.20;
	            var grd = ctx.createRadialGradient(cx, cy + bob, 0, cx, cy + bob, r);
	            grd.addColorStop(0,   'rgba(' + rgb.join(',') + ',' + a.toFixed(3) + ')');
	            grd.addColorStop(0.5, 'rgba(' + rgb.join(',') + ',' + (a * 0.4).toFixed(3) + ')');
	            grd.addColorStop(1,   'rgba(' + rgb.join(',') + ',0)');
	            ctx.fillStyle = grd;
	            ctx.beginPath();
	            ctx.arc(cx, cy + bob, r, 0, Math.PI * 2);
	            ctx.fill();

	            // —— 3) 顶部小高光点缀（亮度峰值时才显示，类似闪烁宝石）——
	            if (pulse > 0.75) {
	                var sparkleA = (pulse - 0.75) * 4;     // 0..1
	                ctx.fillStyle = 'rgba(255,255,255,' + (sparkleA * 0.7).toFixed(3) + ')';
	                ctx.beginPath();
	                ctx.arc(cx - 4, cy + bob - 6, 1.2, 0, Math.PI * 2);
	                ctx.fill();
	            }
	        }

	        ctx.globalCompositeOperation = prev;
	    }

	    // 魂锁连接链：把本层所有魂锁怪两两相连，画发光的紫色锁链
	    function drawSoulChains(ctx, tMs) {
	        if (!core.control || !core.control.controldata || !core.control.controldata.getSoulLockPositions) return;
	        var positions;
	        try { positions = core.control.controldata.getSoulLockPositions(); }
	        catch (e) { return; }
	        if (!positions || positions.length < 2) return;

	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';

	        // 整体节奏脉冲，让链条随时间忽明忽暗
	        var pulse = 0.6 + 0.4 * Math.sin(tMs * 0.0025);

	        // 两两相连
	        for (var i = 0; i < positions.length; i++) {
	            for (var j = i + 1; j < positions.length; j++) {
	                var x1 = positions[i].x * 32 + 16;
	                var y1 = positions[i].y * 32 + 16;
	                var x2 = positions[j].x * 32 + 16;
	                var y2 = positions[j].y * 32 + 16;

	                // 沿线分多段，每段用 sin 偏移形成"链条节点"
	                var dx = x2 - x1, dy = y2 - y1;
	                var dist = Math.sqrt(dx * dx + dy * dy);
	                if (dist < 1) continue;

	                // 主光带（外层柔光）
	                var grd = ctx.createLinearGradient(x1, y1, x2, y2);
	                var aBase = 0.35 * pulse;
	                grd.addColorStop(0,    'rgba(170,110,255,' + (aBase * 0.4).toFixed(3) + ')');
	                grd.addColorStop(0.5,  'rgba(200,150,255,' + aBase.toFixed(3) + ')');
	                grd.addColorStop(1,    'rgba(170,110,255,' + (aBase * 0.4).toFixed(3) + ')');
	                ctx.strokeStyle = grd;
	                ctx.lineWidth = 4;
	                ctx.lineCap = 'round';
	                ctx.beginPath();
	                ctx.moveTo(x1, y1);
	                ctx.lineTo(x2, y2);
	                ctx.stroke();

	                // 细亮核
	                ctx.strokeStyle = 'rgba(230, 200, 255, ' + (0.65 * pulse).toFixed(3) + ')';
	                ctx.lineWidth = 1.2;
	                ctx.stroke();

	                // 沿线流动的小光球（每条链 4 颗），位置随时间相位推移
	                var ballCount = 4;
	                for (var k = 0; k < ballCount; k++) {
	                    var phase = ((tMs * 0.0006) + k / ballCount) % 1;
	                    var bx = x1 + dx * phase;
	                    var by = y1 + dy * phase;
	                    var brad = 2.5;
	                    var bgrd = ctx.createRadialGradient(bx, by, 0, bx, by, brad * 3);
	                    bgrd.addColorStop(0, 'rgba(230,200,255,' + (0.85 * pulse).toFixed(3) + ')');
	                    bgrd.addColorStop(1, 'rgba(170,110,255,0)');
	                    ctx.fillStyle = bgrd;
	                    ctx.beginPath();
	                    ctx.arc(bx, by, brad * 3, 0, Math.PI * 2);
	                    ctx.fill();
	                }
	            }
	        }

	        // 每只魂锁怪头顶一圈淡紫晕（标识它属于这一族）
	        for (var p = 0; p < positions.length; p++) {
	            var px = positions[p].x * 32 + 16;
	            var py = positions[p].y * 32 + 16;
	            var r = 14 + Math.sin(tMs * 0.003 + p) * 1.5;
	            var rgrd = ctx.createRadialGradient(px, py, 0, px, py, r);
	            rgrd.addColorStop(0,   'rgba(180,130,255,0)');
	            rgrd.addColorStop(0.6, 'rgba(180,130,255,' + (0.18 * pulse).toFixed(3) + ')');
	            rgrd.addColorStop(1,   'rgba(180,130,255,0)');
	            ctx.fillStyle = rgrd;
	            ctx.beginPath();
	            ctx.arc(px, py, r, 0, Math.PI * 2);
	            ctx.fill();
	        }

	        ctx.globalCompositeOperation = prev;
	    }

	    // 共生连接链：把本层【活着的】共生怪两两相连，画青绿藤蔓状链；
	    // 同时检测复活事件标志位，在复活位置爆出青绿粒子。
	    function drawSymbiosisChains(ctx, tMs) {
	        // 1) 复活动画检测：functions.js 设的 __lastSymbiosisRevive__ flag
	        try {
	            var rev = core.getFlag && core.getFlag('__lastSymbiosisRevive__', null);
	            if (rev && rev.positions && rev.floorId === (core.status && core.status.floorId)) {
	                var now = Date.now();
	                if (now - rev.timestamp < 200) {
	                    rev.positions.forEach(function (p) {
	                        spawnSymbiosisRevive(p.x, p.y);
	                    });
	                    core.setFlag('__lastSymbiosisRevive__', { positions: [], timestamp: 0, floorId: '' });
	                }
	            }
	        } catch (e) {}

	        // 1b) 吞噬吸收动画检测：functions.js 设的 __lastDevourAbsorb__ flag
	        try {
	            var dvr = core.getFlag && core.getFlag('__lastDevourAbsorb__', null);
	            if (dvr && dvr.targets && dvr.floorId === (core.status && core.status.floorId)) {
	                var now2 = Date.now();
	                if (now2 - dvr.timestamp < 200) {
	                    spawnDevourAbsorb(dvr.fromX, dvr.fromY, dvr.targets);
	                    core.setFlag('__lastDevourAbsorb__', { targets: [], timestamp: 0, floorId: '' });
	                }
	            }
	        } catch (e) {}

	        // 2) 活着的共生怪链条
	        if (!core.control || !core.control.controldata || !core.control.controldata.getSymbiosisLivePositions) return;
	        var positions;
	        try { positions = core.control.controldata.getSymbiosisLivePositions(); }
	        catch (e) { return; }
	        if (!positions || positions.length < 2) return;

	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';

	        // 较缓的脉冲（共生比魂锁更柔和）
	        var pulse = 0.55 + 0.45 * Math.sin(tMs * 0.0018);

	        for (var i = 0; i < positions.length; i++) {
	            for (var j = i + 1; j < positions.length; j++) {
	                var x1 = positions[i].x * 32 + 16;
	                var y1 = positions[i].y * 32 + 16;
	                var x2 = positions[j].x * 32 + 16;
	                var y2 = positions[j].y * 32 + 16;
	                var dx = x2 - x1, dy = y2 - y1;
	                var dist = Math.sqrt(dx * dx + dy * dy);
	                if (dist < 1) continue;

	                // 主光带（青绿渐变）
	                var grd = ctx.createLinearGradient(x1, y1, x2, y2);
	                var aBase = 0.35 * pulse;
	                grd.addColorStop(0,   'rgba(80,220,180,' + (aBase * 0.4).toFixed(3) + ')');
	                grd.addColorStop(0.5, 'rgba(140,250,210,' + aBase.toFixed(3) + ')');
	                grd.addColorStop(1,   'rgba(80,220,180,' + (aBase * 0.4).toFixed(3) + ')');
	                ctx.strokeStyle = grd;
	                ctx.lineWidth = 3.5;
	                ctx.lineCap = 'round';
	                ctx.beginPath();
	                ctx.moveTo(x1, y1);
	                ctx.lineTo(x2, y2);
	                ctx.stroke();

	                // 细亮核（淡白绿）
	                ctx.strokeStyle = 'rgba(220,255,235,' + (0.55 * pulse).toFixed(3) + ')';
	                ctx.lineWidth = 1;
	                ctx.stroke();

	                // 沿线藤蔓节点（每条链 5 颗小光球，慢速漂浮）
	                var ballCount = 5;
	                for (var k = 0; k < ballCount; k++) {
	                    var phase = ((tMs * 0.0004) + k / ballCount) % 1;
	                    var bx = x1 + dx * phase;
	                    var by = y1 + dy * phase;
	                    var brad = 2;
	                    var bgrd = ctx.createRadialGradient(bx, by, 0, bx, by, brad * 3);
	                    bgrd.addColorStop(0, 'rgba(180,255,220,' + (0.7 * pulse).toFixed(3) + ')');
	                    bgrd.addColorStop(1, 'rgba(80,220,180,0)');
	                    ctx.fillStyle = bgrd;
	                    ctx.beginPath();
	                    ctx.arc(bx, by, brad * 3, 0, Math.PI * 2);
	                    ctx.fill();
	                }
	            }
	        }

	        // 每只共生怪头顶一圈青绿晕
	        for (var p = 0; p < positions.length; p++) {
	            var px = positions[p].x * 32 + 16;
	            var py = positions[p].y * 32 + 16;
	            var r = 13 + Math.sin(tMs * 0.0024 + p) * 1.2;
	            var rgrd = ctx.createRadialGradient(px, py, 0, px, py, r);
	            rgrd.addColorStop(0,   'rgba(120,240,200,0)');
	            rgrd.addColorStop(0.6, 'rgba(120,240,200,' + (0.18 * pulse).toFixed(3) + ')');
	            rgrd.addColorStop(1,   'rgba(120,240,200,0)');
	            ctx.fillStyle = rgrd;
	            ctx.beginPath();
	            ctx.arc(px, py, r, 0, Math.PI * 2);
	            ctx.fill();
	        }

	        ctx.globalCompositeOperation = prev;
	    }

	    // 确定性记录 成长(42)/寄生(43)：
	    // 不能只靠动画帧扫描 hero 属性差值；录像/回退时动画帧可能跳过，导致偶尔漏加。
	    // 所以所有明确的属性变化入口在执行后都调用这里，并同步 tracker 基准，避免下一帧重复累计。
	    function recordGrowthParasiteDiff(before, after) {
	        if (!before || !after || !core.status || !core.status.floorId) return;
	        var dAtk = (after.atk || 0) - (before.atk || 0);
	        var dHp  = (after.hp  || 0) - (before.hp  || 0);
	        if (dAtk > 0) {
	            core.setFlag('__growthAccum__', core.getFlag('__growthAccum__', 0) + dAtk);
	        }
	        if (dHp > 0) {
	            core.setFlag('__parasiteAccum__', core.getFlag('__parasiteAccum__', 0) + dHp);
	        }
	        // 同步动画帧追踪基准，防止同一次回血/加攻被下一帧再次累计。
	        state.statTracker.floorId = core.status.floorId;
	        state.statTracker.atk = after.atk || 0;
	        state.statTracker.hp = after.hp || 0;
	    }

	    function snapshotHeroAtkHp() {
	        var hero = core.status && core.status.hero;
	        if (!hero) return null;
	        return { atk: hero.atk || 0, hp: hero.hp || 0 };
	    }

	    // 勇士属性差值追踪：用于 成长(42) 和 寄生(43)
	    // 每帧调用：检测 hero.atk/hp 比上次记录大多少，把正差值累加进 flag
	    // 切楼层或读档时重置追踪基准，避免假差值
	    function updateGrowthParasiteTracking() {
	        if (!core.status || !core.status.hero) return;
	        var hero = core.status.hero;
	        var floorId = core.status.floorId;
	        if (!floorId) return;
	        var tracker = state.statTracker;
	        var fromLoad = false;
	        var justRevived = false;
	        try { fromLoad = !!core.getFlag('__fromLoad__', false); } catch (e) {}
	        try { justRevived = !!core.getFlag('__justRevived__', false); } catch (e) {}

	        // 重置条件：第一次见 / 切楼 / 读档 / 刚轮回
	        if (tracker.floorId !== floorId || fromLoad || justRevived || tracker.atk === null) {
	            tracker.floorId = floorId;
	            tracker.atk = hero.atk || 0;
	            tracker.hp  = hero.hp  || 0;
	            if (justRevived) { try { core.removeFlag('__justRevived__'); } catch (e) {} }
	            return;
	        }

	        var newAtk = hero.atk || 0;
	        var newHp  = hero.hp  || 0;

	        var dAtk = newAtk - tracker.atk;
	        var dHp  = newHp  - tracker.hp;

	        // 只累正差值
	        if (dAtk > 0) {
	            var growth = core.getFlag('__growthAccum__', 0);
	            core.setFlag('__growthAccum__', growth + dAtk);
	        }
	        if (dHp > 0) {
	            var parasite = core.getFlag('__parasiteAccum__', 0);
	            core.setFlag('__parasiteAccum__', parasite + dHp);
	        }

	        tracker.atk = newAtk;
	        tracker.hp  = newHp;
	    }

	    // 克飞(39) / 克跳(40) / 克金(41) 头顶 +N 徽章
	    // 每帧扫描本层 blocks，对带这三个 special 的怪显示加成数字
	    function drawCounterBadges(ctx, tMs) {
	        if (!core.status || !core.status.floorId) return;
	        if (!core.material || !core.material.enemys) return;
	        var floorId = core.status.floorId;
	        var blocks;
	        try { blocks = core.getMapBlocksObj(floorId) || {}; } catch (e) { return; }

	        // 一次性算资源数（每帧算一次，多怪共用）
	        var flyCount = (core.itemCount && (
	            (core.itemCount('centerFly') || 0)
	            + (core.itemCount('upFly')   || 0)
	            + (core.itemCount('downFly')|| 0)
	            + (core.itemCount('fly')    || 0)
	        )) || 0;
	        var jumpCount = (core.itemCount && (core.itemCount('jumpShoes') || 0)) || 0;
	        var goldCount = (core.status.hero && core.status.hero.money) || 0;

	        var flyBonus  = 3 * flyCount;
	        var jumpBonus = 3 * jumpCount;
	        var goldBonus = goldCount;

	        // 成长/寄生 累计值（也实时显示）
	        var growthAccum = core.getFlag('__growthAccum__', 0);
	        var parasiteAccum = core.getFlag('__parasiteAccum__', 0);

	        // 颜色（跟特殊属性表对应）
	        var COLOR_KFLY    = '#56cfd4';
	        var COLOR_KJMP    = '#a8e860';
	        var COLOR_KAU     = '#d4af37';
	        var COLOR_GROWTH  = '#5fc472';
	        var COLOR_PARASITE = '#c25fa3';

	        ctx.save();
	        ctx.font = 'bold 10px sans-serif';
	        ctx.textAlign = 'center';
	        ctx.textBaseline = 'bottom';

	        for (var loc in blocks) {
	            var block = blocks[loc];
	            if (!block || block.disable || !block.event) continue;
	            var enemyId = block.event.id;
	            var enemy = core.material.enemys[enemyId];
	            if (!enemy) continue;
	            var special = core.getEnemyValue(enemy, 'special', block.x, block.y, floorId);
	            var hasKFly    = core.enemys.hasSpecial(special, 39);
	            var hasKJmp    = core.enemys.hasSpecial(special, 40);
	            var hasKAu     = core.enemys.hasSpecial(special, 41);
	            var hasGrowth  = core.enemys.hasSpecial(special, 42);
	            var hasPara    = core.enemys.hasSpecial(special, 43);
	            if (!hasKFly && !hasKJmp && !hasKAu && !hasGrowth && !hasPara) continue;

	            var px = block.x * 32 + 16;        // 中心 x
	            var topY = block.y * 32;            // 怪 sprite 顶部
	            // 多个徽章纵向叠在头顶上方
	            var yCursor = topY - 1;
	            // 微微的呼吸感
	            var pulse = 0.85 + 0.15 * Math.sin(tMs * 0.004 + block.x + block.y);

	            function drawBadge(text, color) {
	                // 描边（黑色外描，提高在浅地图上的可读性）
	                ctx.lineWidth = 3;
	                ctx.strokeStyle = 'rgba(0,0,0,' + (0.85 * pulse).toFixed(3) + ')';
	                ctx.strokeText(text, px, yCursor);
	                // 主色
	                ctx.fillStyle = color;
	                ctx.globalAlpha = pulse;
	                ctx.fillText(text, px, yCursor);
	                ctx.globalAlpha = 1;
	                yCursor -= 11;  // 下一徽章上移
	            }

	            // 顺序从下到上
	            if (hasPara && parasiteAccum > 0)  drawBadge('+' + parasiteAccum + 'HP', COLOR_PARASITE);
	            if (hasGrowth && growthAccum > 0)  drawBadge('+' + growthAccum + '攻', COLOR_GROWTH);
	            if (hasKAu)  drawBadge('+' + goldBonus + 'HP', COLOR_KAU);
	            if (hasKJmp) drawBadge('+' + jumpBonus,        COLOR_KJMP);
	            if (hasKFly) drawBadge('+' + flyBonus,         COLOR_KFLY);
	        }

	        ctx.restore();
	    }

	    // 共生怪复活时的爆光（链条被中断、所有 pending 复活）
	    function spawnSymbiosisRevive(gx, gy) {
	        var cx = gx * 32 + 16, cy = gy * 32 + 16;
	        var rgbCool = [120, 250, 200];
	        var rgbBright = [200, 255, 230];

	        // 中心强闪
	        state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [255, 255, 255], life: 0,    maxLife: 0.22 });
	        state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: rgbBright,       life: 0.06, maxLife: 0.34 });

	        // 双层青绿冲击波
	        state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: rgbCool,   life: 0,    maxLife: 0.6 });
	        state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: rgbBright, life: 0.10, maxLife: 0.55 });
	        state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: rgbCool,   life: 0.22, maxLife: 0.50 });

	        // 向外爆 20 颗藤蔓粒子
	        for (var i = 0; i < 20; i++) {
	            var ang = Math.random() * Math.PI * 2;
	            var spd = 1.5 + Math.random() * 2.2;
	            state.bursts.push({
	                kind: 'particle',
	                x: cx, y: cy, rgb: rgbCool,
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd - 0.4,
	                life: 0,
	                maxLife: 0.6 + Math.random() * 0.4,
	                size: 1.4 + Math.random() * 1.4
	            });
	        }
	        // 向上升腾 6 颗（复活的"生机"感）
	        for (var i = 0; i < 6; i++) {
	            state.bursts.push({
	                kind: 'lift',
	                x: cx + (Math.random() - 0.5) * 14,
	                y: cy + 2,
	                rgb: rgbBright,
	                vx: (Math.random() - 0.5) * 0.2,
	                vy: -0.8 - Math.random() * 0.5,
	                life: 0,
	                maxLife: 0.8 + Math.random() * 0.4,
	                size: 1.4 + Math.random()
	            });
	        }
	    }

	    // 吞噬吸收动画：从死亡位置流向每只吞噬怪 + 吞噬怪本体短暂吸气脉冲
	    function spawnDevourAbsorb(fromX, fromY, targets) {
	        var fx = fromX * 32 + 16, fy = fromY * 32 + 16;
	        var rgbCore = [220, 70, 140];
	        var rgbBright = [255, 180, 220];

	        targets.forEach(function (t) {
	            var tx = t.x * 32 + 16, ty = t.y * 32 + 16;

	            // 1) 紫红"喂养线"
	            state.bursts.push({
	                kind: 'streak',
	                x1: fx, y1: fy, x2: tx, y2: ty,
	                rgb: rgbCore,
	                life: 0,
	                maxLife: 0.45
	            });

	            // 2) 沿路径散落小光斑（吸食感）
	            var dx = tx - fx, dy = ty - fy;
	            var dist = Math.sqrt(dx * dx + dy * dy);
	            if (dist > 1) {
	                var n = Math.max(4, Math.floor(dist / 14));
	                for (var i = 1; i <= n; i++) {
	                    var p = i / (n + 1);
	                    state.bursts.push({
	                        kind: 'softGlow',
	                        x: fx + dx * p,
	                        y: fy + dy * p,
	                        rgb: rgbCore,
	                        life: -p * 0.20,
	                        maxLife: 0.45 + Math.random() * 0.2,
	                        r: 2.5 + Math.random() * 1.5
	                    });
	                }
	            }

	            // 3) 终点吸气脉冲（吞噬怪本体）
	            state.bursts.push({ kind: 'ring', x: tx, y: ty, rgb: rgbCore,   life: 0.30, maxLife: 0.40 });
	            state.bursts.push({ kind: 'flash', x: tx, y: ty, rgb: rgbBright, life: 0.32, maxLife: 0.25 });
	            for (var k = 0; k < 6; k++) {
	                var ang = Math.random() * Math.PI * 2;
	                var spd = 0.6 + Math.random() * 1.0;
	                state.bursts.push({
	                    kind: 'particle',
	                    x: tx + Math.cos(ang) * 18,
	                    y: ty + Math.sin(ang) * 18,
	                    rgb: rgbBright,
	                    vx: -Math.cos(ang) * spd,
	                    vy: -Math.sin(ang) * spd,
	                    life: 0.30,
	                    maxLife: 0.45 + Math.random() * 0.2,
	                    size: 1.2 + Math.random()
	                });
	            }

	            // 4) "吞"字飘字（提示效果触发）
	            state.damageNums.push({
	                x: tx + (Math.random() - 0.5) * 6,
	                y: ty - 4,
	                vx: 0,
	                vy: -1.2,
	                text: '吞',
	                color: rgbBright,
	                life: 0.40,
	                maxLife: 0.95,
	                scale: 1.3
	            });
	        });
	    }

	    function spawnPickupBurst(gx, gy, itemId) {
	        var cx = gx * 32 + 16;
	        var cy = gy * 32 + 16;
	        var rgb = getItemRGB(itemId);

	        // 中心闪光（短）
	        state.bursts.push({
	            kind: 'flash', x: cx, y: cy, rgb: rgb,
	            life: 0, maxLife: 0.18
	        });
	        // 扩散光环（长）
	        state.bursts.push({
	            kind: 'ring', x: cx, y: cy, rgb: rgb,
	            life: 0, maxLife: 0.55
	        });
	        // 第二圈光环（错时）
	        state.bursts.push({
	            kind: 'ring', x: cx, y: cy, rgb: rgb,
	            life: -0.12, maxLife: 0.55
	        });
	        // 飞溅粒子
	        var count = 14;
	        for (var i = 0; i < count; i++) {
	            var ang = (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.4;
	            var spd = 1.6 + Math.random() * 1.6;
	            state.bursts.push({
	                kind: 'particle',
	                x: cx, y: cy, rgb: rgb,
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd - 0.4,
	                life: 0,
	                maxLife: 0.55 + Math.random() * 0.4,
	                size: 1.4 + Math.random() * 1.4
	            });
	        }
	        // 向上飘的金色升腾光
	        for (var i = 0; i < 4; i++) {
	            state.bursts.push({
	                kind: 'lift',
	                x: cx + (Math.random() - 0.5) * 12,
	                y: cy + 4, rgb: rgb,
	                vx: (Math.random() - 0.5) * 0.15,
	                vy: -0.6 - Math.random() * 0.5,
	                life: 0,
	                maxLife: 0.75 + Math.random() * 0.35,
	                size: 1.6 + Math.random()
	            });
	        }
	    }

	    // 拾取后的属性变化飘字（绿血/红攻/蓝防/青魔防 + 道具）
	    // kindOrDef 可以是预设字符串，也可以是 {label, color} 自定义对象
	    // isNegative=true 时显示 "-N" 而不是 "+N"（用于碎甲/破功 debuff 飘字）
	    function spawnStatNumber(gx, gy, value, kindOrDef, index, isNegative) {
	        var cx = gx * 32 + 16;
	        var cy = gy * 32 + 16;
	        var color, label;
	        if (typeof kindOrDef === 'object' && kindOrDef) {
	            color = kindOrDef.color || [255, 230, 150];
	            label = kindOrDef.label || '';
	        } else {
	            switch (kindOrDef) {
	                case 'hp':   color = [ 80, 220, 110]; label = '血'; break;
	                case 'atk':  color = [255, 100, 100]; label = '攻'; break;
	                case 'def':  color = [110, 170, 255]; label = '防'; break;
	                case 'mdef': color = [140, 235, 200]; label = '魔'; break;
	                case 'gold': color = [255, 215, 100]; label = '金'; break;
	                case 'exp':  color = [200, 160, 255]; label = '经'; break;
	                default:     color = [255, 230, 150]; label = '';   break;
	            }
	        }
	        state.damageNums.push({
	            x:   cx + (Math.random() - 0.5) * 5,
	            y:   cy - 6 - (index || 0) * 14,
	            vx:  (Math.random() - 0.5) * 0.3,
	            vy:  -1.45 - Math.random() * 0.4,
	            text: label + (isNegative ? '-' : '+') + value,
	            color: color,
	            life: -(index || 0) * 0.08,
	            maxLife: 1.05,
	            scale: 1.25
	        });
	    }

	    // 道具/钥匙的显示规则（label + 颜色）
	    // 没列出来的道具不弹字（不污染屏幕）
	    var TOOL_DISPLAY = {
	        // 移动类
	        jumpShoes:   { label: '跳', color: [180, 230, 100] },   // 黄绿
	        centerFly:   { label: '飞', color: [120, 200, 230] },   // 青
	        upFly:       { label: '飞', color: [120, 200, 230] },
	        downFly:     { label: '飞', color: [120, 200, 230] },
	        // 钥匙类
	        yellowKey:   { label: '黄钥', color: [255, 215, 100] },
	        blueKey:     { label: '蓝钥', color: [120, 180, 255] },
	        redKey:      { label: '红钥', color: [255, 110, 110] },
	        greenKey:    { label: '绿钥', color: [120, 255, 160] },
	        bigKey:      { label: '大钥', color: [255, 200,  80] },
	        steelKey:    { label: '铁钥', color: [200, 210, 230] },
	        // 工具类
	        pickaxe:     { label: '镐',  color: [210, 170, 110] },
	        icePickaxe:  { label: '冰镐', color: [180, 220, 255] },
	        bomb:        { label: '弹',  color: [220, 110,  80] },
	        earthquake:  { label: '震',  color: [220, 160, 100] },
	        hammer:      { label: '锤',  color: [200, 200, 200] },
	        // 解毒类
	        poisonWine:  { label: '解毒', color: [120, 220, 120] },
	        weakWine:    { label: '解衰', color: [220, 200, 120] },
	        curseWine:   { label: '解咒', color: [200, 120, 220] },
	        superWine:   { label: '万能', color: [240, 240, 200] },
	        lifeWand:    { label: '生命', color: [255, 180, 200] }
	    };

	    function drawBursts(ctx, dt) {
	        if (!state.bursts.length) return;
	        var dtSec = dt / 1000;
	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';

	        for (var i = state.bursts.length - 1; i >= 0; i--) {
	            var b = state.bursts[i];
	            b.life += dtSec;
	            if (b.life >= b.maxLife) { state.bursts.splice(i, 1); continue; }
	            if (b.life < 0) continue;     // 延迟出生

	            var k = b.life / b.maxLife;       // 0..1
	            var rgb = b.rgb;

	            if (b.kind === 'flash') {
	                var a = (1 - k) * 0.9;
	                var r = 18 + k * 8;
	                var g1 = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r);
	                g1.addColorStop(0, 'rgba(255,255,255,' + a.toFixed(3) + ')');
	                g1.addColorStop(0.4, 'rgba(' + rgb.join(',') + ',' + (a * 0.7).toFixed(3) + ')');
	                g1.addColorStop(1, 'rgba(' + rgb.join(',') + ',0)');
	                ctx.fillStyle = g1;
	                ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill();
	            }
	            else if (b.kind === 'ring') {
	                var rr = 6 + k * 28;
	                var a = (1 - k) * 0.85;
	                ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + a.toFixed(3) + ')';
	                ctx.lineWidth = 3 * (1 - k * 0.6);
	                ctx.beginPath(); ctx.arc(b.x, b.y, rr, 0, Math.PI * 2); ctx.stroke();
	            }
	            else if (b.kind === 'particle') {
	                b.x += b.vx; b.y += b.vy;
	                b.vy += 0.08;      // 重力
	                b.vx *= 0.97; b.vy *= 0.97;
	                var a = (1 - k) * 0.95;
	                var sz = b.size * (1 - k * 0.45);
	                var g2 = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, sz * 3);
	                g2.addColorStop(0, 'rgba(' + rgb.join(',') + ',' + a.toFixed(3) + ')');
	                g2.addColorStop(1, 'rgba(' + rgb.join(',') + ',0)');
	                ctx.fillStyle = g2;
	                ctx.beginPath(); ctx.arc(b.x, b.y, sz * 3, 0, Math.PI * 2); ctx.fill();
	                ctx.fillStyle = 'rgba(255,255,255,' + (a * 0.9).toFixed(3) + ')';
	                ctx.beginPath(); ctx.arc(b.x, b.y, sz * 0.6, 0, Math.PI * 2); ctx.fill();
	            }
	            else if (b.kind === 'lift') {
	                b.x += b.vx; b.y += b.vy;
	                b.vy *= 0.985;
	                var a = (1 - k) * 0.7;
	                var sz = b.size * (1 - k * 0.5);
	                ctx.fillStyle = 'rgba(' + rgb.join(',') + ',' + a.toFixed(3) + ')';
	                ctx.beginPath(); ctx.arc(b.x, b.y, sz, 0, Math.PI * 2); ctx.fill();
	            }
	            else if (b.kind === 'slash') {
	                // 斜击痕迹：一道有宽度的发光线条，长度先大后小
	                var len = 26 * (1 - k * 0.4);
	                var w = 4 * (1 - k);
	                var alpha = (1 - k) * 0.9;
	                ctx.save();
	                ctx.translate(b.x, b.y);
	                ctx.rotate(b.angle);
	                // 外发光
	                var g3 = ctx.createLinearGradient(-len, 0, len, 0);
	                g3.addColorStop(0,   'rgba(255,240,200,0)');
	                g3.addColorStop(0.5, 'rgba(255,240,200,' + alpha.toFixed(3) + ')');
	                g3.addColorStop(1,   'rgba(255,240,200,0)');
	                ctx.fillStyle = g3;
	                ctx.fillRect(-len, -w/2, len * 2, w);
	                // 亮核
	                ctx.fillStyle = 'rgba(255,255,255,' + (alpha * 0.9).toFixed(3) + ')';
	                ctx.fillRect(-len * 0.9, -w/4, len * 1.8, w/2);
	                ctx.restore();
	            }
	            else if (b.kind === 'beam') {
	                // 直线光束（用于豹跃时空穿梭）
	                var alpha = (1 - k) * 0.85;
	                // 外发光（粗）
	                ctx.lineCap = 'round';
	                ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + (alpha * 0.25).toFixed(3) + ')';
	                ctx.lineWidth = 9;
	                ctx.beginPath();
	                ctx.moveTo(b.x1, b.y1);
	                ctx.lineTo(b.x2, b.y2);
	                ctx.stroke();
	                // 中
	                ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + (alpha * 0.7).toFixed(3) + ')';
	                ctx.lineWidth = 4;
	                ctx.stroke();
	                // 亮核
	                ctx.strokeStyle = 'rgba(255,255,255,' + alpha.toFixed(3) + ')';
	                ctx.lineWidth = 1.4;
	                ctx.stroke();
	            }
	            else if (b.kind === 'bolt') {
	                // 闪电：三层描边（外发光-中色-亮核）
	                var alpha = (1 - k) * 0.95;
	                var pts = b.points;
	                if (!pts || pts.length < 2) continue;
	                ctx.lineCap = 'round';
	                ctx.lineJoin = 'round';
	                // 外发光（粗、低alpha）
	                ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + (alpha * 0.25).toFixed(3) + ')';
	                ctx.lineWidth = 7;
	                ctx.beginPath();
	                ctx.moveTo(pts[0].x, pts[0].y);
	                for (var pi = 1; pi < pts.length; pi++) ctx.lineTo(pts[pi].x, pts[pi].y);
	                ctx.stroke();
	                // 中层
	                ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + (alpha * 0.7).toFixed(3) + ')';
	                ctx.lineWidth = 3.2;
	                ctx.stroke();
	                // 亮核（白）
	                ctx.strokeStyle = 'rgba(255,255,255,' + alpha.toFixed(3) + ')';
	                ctx.lineWidth = 1.2;
	                ctx.stroke();
	            }
	            else if (b.kind === 'streak') {
	                // 丝滑光带：3层渐宽渐淡的线条，两端淡入淡出
	                var alpha = (1 - k) * 0.85;
	                var grd = ctx.createLinearGradient(b.x1, b.y1, b.x2, b.y2);
	                grd.addColorStop(0.00, 'rgba(' + rgb.join(',') + ',0)');
	                grd.addColorStop(0.25, 'rgba(' + rgb.join(',') + ',' + alpha.toFixed(3) + ')');
	                grd.addColorStop(0.75, 'rgba(' + rgb.join(',') + ',' + alpha.toFixed(3) + ')');
	                grd.addColorStop(1.00, 'rgba(' + rgb.join(',') + ',0)');
	                ctx.lineCap = 'round';
	                // 外发光层（粗 + 半透明）
	                ctx.strokeStyle = grd;
	                ctx.lineWidth = 14;
	                ctx.beginPath(); ctx.moveTo(b.x1, b.y1); ctx.lineTo(b.x2, b.y2); ctx.stroke();
	                // 中层
	                ctx.lineWidth = 6;
	                ctx.stroke();
	                // 白色亮核
	                var whiteGrd = ctx.createLinearGradient(b.x1, b.y1, b.x2, b.y2);
	                whiteGrd.addColorStop(0.00, 'rgba(255,255,255,0)');
	                whiteGrd.addColorStop(0.30, 'rgba(255,255,255,' + (alpha * 0.95).toFixed(3) + ')');
	                whiteGrd.addColorStop(0.70, 'rgba(255,255,255,' + (alpha * 0.95).toFixed(3) + ')');
	                whiteGrd.addColorStop(1.00, 'rgba(255,255,255,0)');
	                ctx.strokeStyle = whiteGrd;
	                ctx.lineWidth = 1.6;
	                ctx.stroke();
	            }
	            else if (b.kind === 'fadeRing') {
	                // 柔和缩放的圆形辉光（无锐边）
	                var alpha = (1 - k) * 0.75;
	                var r = (b.startR || 4) + ((b.endR || 16) - (b.startR || 4)) * k;
	                var grd = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r);
	                grd.addColorStop(0,   'rgba(' + rgb.join(',') + ',' + alpha.toFixed(3) + ')');
	                grd.addColorStop(0.5, 'rgba(' + rgb.join(',') + ',' + (alpha * 0.5).toFixed(3) + ')');
	                grd.addColorStop(1,   'rgba(' + rgb.join(',') + ',0)');
	                ctx.fillStyle = grd;
	                ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2); ctx.fill();
	            }
	            else if (b.kind === 'softGlow') {
	                // 沿路径的散落小光斑（先入后出）
	                var ka = (k < 0.3) ? (k / 0.3) : ((1 - k) / 0.7);
	                var alpha = ka * 0.55;
	                var r = b.r || 3;
	                var grd = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r * 2);
	                grd.addColorStop(0, 'rgba(' + rgb.join(',') + ',' + alpha.toFixed(3) + ')');
	                grd.addColorStop(1, 'rgba(' + rgb.join(',') + ',0)');
	                ctx.fillStyle = grd;
	                ctx.beginPath(); ctx.arc(b.x, b.y, r * 2, 0, Math.PI * 2); ctx.fill();
	            }
	        }

	        ctx.globalCompositeOperation = prev;
	    }

	    // ============================================================
	    // 战斗手感：屏幕震动 / 战斗爆光 / 飘字伤害 / 受伤红雾
	    // ============================================================

	    function triggerShake(intensity, duration) {
	        state.shake.intensity = Math.max(state.shake.intensity, intensity);
	        state.shake.time = 0;
	        state.shake.duration = duration || 0.32;
	    }

	    function updateShake(dt) {
	        if (state.shake.intensity <= 0) {
	            if (state.shake._wasSet && core.dom && core.dom.gameDraw) {
	                try { core.dom.gameDraw.style.transform = ''; } catch (e) {}
	                state.shake._wasSet = false;
	            }
	            return;
	        }
	        state.shake.time += dt / 1000;
	        var t = state.shake.time / state.shake.duration;
	        if (t >= 1) {
	            state.shake.intensity = 0;
	            if (core.dom && core.dom.gameDraw) {
	                try { core.dom.gameDraw.style.transform = ''; } catch (e) {}
	            }
	            state.shake._wasSet = false;
	            return;
	        }
	        var decay = (1 - t) * (1 - t);    // 二次衰减
	        var amp = state.shake.intensity * decay * 7;
	        var dx = (Math.random() - 0.5) * 2 * amp;
	        var dy = (Math.random() - 0.5) * 2 * amp;
	        if (core.dom && core.dom.gameDraw) {
	            try {
	                core.dom.gameDraw.style.transform = 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px)';
	                state.shake._wasSet = true;
	            } catch (e) {}
	        }
	    }

	    function triggerHurt(intensity) {
	        state.hurt.intensity = Math.max(state.hurt.intensity, Math.min(1, intensity));
	        state.hurt.time = 0;
	        state.hurt.duration = 0.55 + state.hurt.intensity * 0.4;
	    }

	    function drawHurt(ctx, dt, W, H) {
	        if (state.hurt.intensity <= 0) return;
	        state.hurt.time += dt / 1000;
	        var t = state.hurt.time / state.hurt.duration;
	        if (t >= 1) { state.hurt.intensity = 0; return; }
	        // 入快出慢
	        var ease = t < 0.2 ? (t / 0.2) : (1 - (t - 0.2) / 0.8);
	        var alpha = state.hurt.intensity * ease * 0.55;
	        var g = ctx.createRadialGradient(W/2, H/2, W*0.18, W/2, H/2, W*0.75);
	        g.addColorStop(0, 'rgba(255,30,30,0)');
	        g.addColorStop(0.55, 'rgba(255,30,30,' + (alpha * 0.3).toFixed(3) + ')');
	        g.addColorStop(1, 'rgba(255,30,30,' + alpha.toFixed(3) + ')');
	        ctx.fillStyle = g;
	        ctx.fillRect(0, 0, W, H);
	    }

	    function spawnBattleHit(gx, gy, damage, isBloodWar, isSoulLock, isSymbiosis, isDevour) {
	        var cx = gx * 32 + 16;
	        var cy = gy * 32 + 16;

	        // 1) 中心闪：血战深红 / 魂锁紫 / 共生青绿 / 吞噬深紫红 / 普通白
	        if (isBloodWar) {
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [255, 60, 60], life: 0, maxLife: 0.22 });
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [180, 20, 30], life: 0.06, maxLife: 0.30 });
	        } else if (isDevour) {
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [220, 80, 150], life: 0, maxLife: 0.22 });
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [140, 20, 80],  life: 0.06, maxLife: 0.34 });
	        } else if (isSoulLock) {
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [200, 150, 255], life: 0, maxLife: 0.20 });
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [140, 80, 220],  life: 0.05, maxLife: 0.32 });
	        } else if (isSymbiosis) {
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [120, 230, 200], life: 0, maxLife: 0.22 });
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [60, 200, 170],  life: 0.05, maxLife: 0.30 });
	        } else {
	            state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [255, 255, 255], life: 0, maxLife: 0.16 });
	        }

	        // 2) 扩散光环
	        if (isBloodWar) {
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [200, 30, 40], life: 0,    maxLife: 0.50 });
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [255, 80, 80], life: 0.12, maxLife: 0.45 });
	        } else if (isDevour) {
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [180, 40, 100],  life: 0,    maxLife: 0.50 });
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [220, 100, 160], life: 0.10, maxLife: 0.55 });
	        } else if (isSoulLock) {
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [170, 110, 255], life: 0,    maxLife: 0.50 });
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [220, 180, 255], life: 0.10, maxLife: 0.55 });
	        } else if (isSymbiosis) {
	            // 青绿双层光环（带共生纠缠感）
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [80, 220, 180], life: 0,    maxLife: 0.55 });
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [140, 250, 210], life: 0.12, maxLife: 0.50 });
	        } else {
	            state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [255, 240, 200], life: 0, maxLife: 0.40 });
	        }

	        // 3) 斜击痕迹（保留）
	        var slashAngle = Math.PI * 0.18 + Math.random() * 0.3;
	        state.bursts.push({ kind: 'slash', x: cx, y: cy, angle: slashAngle,                  life: 0, maxLife: 0.22 });
	        state.bursts.push({ kind: 'slash', x: cx, y: cy, angle: slashAngle + Math.PI / 2.2, life: -0.05, maxLife: 0.22 });

	        // 4) 主粒子
	        var pCount = isBloodWar ? 26 : (isDevour ? 22 : (isSoulLock ? 18 : (isSymbiosis ? 18 : 14)));
	        for (var i = 0; i < pCount; i++) {
	            var ang = Math.random() * Math.PI * 2;
	            var spd = (isBloodWar ? 1.8 : (isDevour ? 1.5 : (isSoulLock ? 1.0 : (isSymbiosis ? 1.3 : 1.4)))) + Math.random() * 2.4;
	            var r, g, b;
	            if (isBloodWar) {
	                r = 200 + Math.floor(Math.random() * 55);
	                g = 20  + Math.floor(Math.random() * 30);
	                b = 20  + Math.floor(Math.random() * 30);
	            } else if (isDevour) {
	                r = 180 + Math.floor(Math.random() * 60);
	                g = 30  + Math.floor(Math.random() * 50);
	                b = 90  + Math.floor(Math.random() * 60);
	            } else if (isSoulLock) {
	                r = 170 + Math.floor(Math.random() * 60);
	                g = 110 + Math.floor(Math.random() * 50);
	                b = 230 + Math.floor(Math.random() * 25);
	            } else if (isSymbiosis) {
	                r = 60  + Math.floor(Math.random() * 80);
	                g = 200 + Math.floor(Math.random() * 50);
	                b = 160 + Math.floor(Math.random() * 60);
	            } else {
	                r = 255;
	                g = 70 + Math.floor(Math.random() * 40);
	                b = 60;
	            }
	            state.bursts.push({
	                kind: 'particle',
	                x: cx, y: cy,
	                rgb: [r, g, b],
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd - (isSoulLock ? 0.7 : (isSymbiosis ? 0.5 : (isDevour ? 0.3 : 0.4))),
	                life: 0,
	                maxLife: (isBloodWar ? 0.6 : (isDevour ? 0.7 : (isSoulLock ? 0.85 : (isSymbiosis ? 0.75 : 0.45)))) + Math.random() * 0.4,
	                size: (isBloodWar ? 1.8 : (isDevour ? 1.7 : (isSoulLock ? 1.6 : (isSymbiosis ? 1.5 : 1.4)))) + Math.random() * 1.5
	            });
	        }

	        // 5) 火星/魂芒
	        var sparkCount = isBloodWar ? 3 : (isDevour ? 4 : (isSoulLock ? 6 : (isSymbiosis ? 5 : 7)));
	        for (var i = 0; i < sparkCount; i++) {
	            var ang = Math.random() * Math.PI * 2;
	            var spd = 2.2 + Math.random() * 1.8;
	            var sRgb = isDevour ? [255, 180, 220]
	                     : (isSoulLock ? [230, 200, 255]
	                     : (isSymbiosis ? [180, 255, 220] : [255, 220, 100]));
	            state.bursts.push({
	                kind: 'particle',
	                x: cx, y: cy,
	                rgb: sRgb,
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd - 0.5,
	                life: 0,
	                maxLife: 0.3 + Math.random() * 0.3,
	                size: 1.0 + Math.random()
	            });
	        }

	        // 6) 飘字伤害
	        if (damage != null && damage >= 0) {
	            var heavy = damage > (core.status && core.status.hero ? core.status.hero.hpmax * 0.15 : 50);
	            var dmgColor, dmgScale;
	            if (isBloodWar) {
	                dmgColor = [220, 30, 50]; dmgScale = 1.75;
	            } else if (isDevour) {
	                dmgColor = [220, 70, 140]; dmgScale = heavy ? 1.6 : 1.4;
	            } else if (isSoulLock) {
	                dmgColor = [200, 140, 255]; dmgScale = heavy ? 1.6 : 1.35;
	            } else if (isSymbiosis) {
	                dmgColor = [80, 220, 170]; dmgScale = heavy ? 1.5 : 1.3;
	            } else {
	                dmgColor = heavy ? [255, 80, 80] : [255, 210, 100];
	                dmgScale = heavy ? 1.55 : 1.2;
	            }
	            state.damageNums.push({
	                x: cx + (Math.random() - 0.5) * 6,
	                y: cy - 6,
	                vx: (Math.random() - 0.5) * 0.4,
	                vy: -1.6 - Math.random() * 0.5,
	                text: '-' + damage,
	                color: dmgColor,
	                life: 0,
	                maxLife: isBloodWar ? 1.25 : (isDevour ? 1.2 : (isSoulLock ? 1.15 : (isSymbiosis ? 1.1 : 1.0))),
	                scale: dmgScale
	            });
	        }

	        // 7) 屏幕震动 + 红雾
	        var hpmax = (core.status && core.status.hero) ? (core.status.hero.hpmax || 100) : 100;
	        var dmgRatio = (damage || 0) / Math.max(1, hpmax);
	        var shakeMul = isBloodWar ? 1.4 : (isDevour ? 1.1 : (isSoulLock ? 0.9 : (isSymbiosis ? 0.9 : 1.0)));
	        triggerShake((0.45 + Math.min(0.55, dmgRatio * 1.2)) * shakeMul);
	        if (damage > 0) {
	            triggerHurt((0.35 + Math.min(0.65, dmgRatio * 2)) * (isBloodWar ? 1.3 : 1));
	        }
	    }

	    function drawDamageNums(ctx, dt) {
	        if (!state.damageNums.length) return;
	        var dtSec = dt / 1000;
	        for (var i = state.damageNums.length - 1; i >= 0; i--) {
	            var d = state.damageNums[i];
	            d.life += dtSec;
	            if (d.life >= d.maxLife) { state.damageNums.splice(i, 1); continue; }
	            // 运动
	            d.x += d.vx;
	            d.y += d.vy;
	            d.vy *= 0.96;       // 上升减速
	            var k = d.life / d.maxLife;
	            // 入大→稳→出淡
	            var scale;
	            if (k < 0.15) scale = d.scale * (0.6 + k / 0.15 * 0.4);   // 弹出
	            else if (k < 0.30) scale = d.scale * (1.0 + (1 - (k - 0.15) / 0.15) * 0.15);  // 回弹
	            else scale = d.scale;
	            var alpha = k < 0.7 ? 1 : Math.max(0, 1 - (k - 0.7) / 0.3);
	            // 绘制
	            ctx.save();
	            ctx.translate(d.x, d.y);
	            ctx.scale(scale, scale);
	            ctx.font = 'bold 14px "Microsoft YaHei",sans-serif';
	            ctx.textAlign = 'center';
	            ctx.textBaseline = 'middle';
	            ctx.lineWidth = 4;
	            ctx.strokeStyle = 'rgba(0,0,0,' + alpha.toFixed(3) + ')';
	            ctx.strokeText(d.text, 0, 0);
	            ctx.fillStyle = 'rgba(' + d.color.join(',') + ',' + alpha.toFixed(3) + ')';
	            ctx.fillText(d.text, 0, 0);
	            ctx.restore();
	        }
	    }

	    // ============================================================
	    // 屠龙分裂：化身闪电、一化为二
	    // ============================================================

	    // 用与游戏原函数完全相同的逻辑搜寻分裂目标格子
	    function findDragonSplitTargets(centerX, centerY, battleDir, floorId) {
	        if (!core.floors || !core.floors[floorId]) return [];
	        var width  = core.floors[floorId].width;
	        var height = core.floors[floorId].height;
	        var dirs;
	        if (battleDir == 'left' || battleDir == 'right') {
	            dirs = [{ x: 0, y: -1 }, { x: 0, y: 1 }];
	        } else {
	            dirs = [{ x: -1, y: 0 }, { x: 1, y: 0 }];
	        }
	        var targets = [];
	        var hx = core.getHeroLoc('x'), hy = core.getHeroLoc('y');
	        dirs.forEach(function (dir) {
	            var nx = centerX + dir.x, ny = centerY + dir.y;
	            while (nx >= 0 && nx < width && ny >= 0 && ny < height) {
	                if (nx == hx && ny == hy) { nx += dir.x; ny += dir.y; continue; }
	                if (core.getBlock(nx, ny, floorId) == null) {
	                    targets.push({ x: nx, y: ny });
	                    break;
	                }
	                nx += dir.x; ny += dir.y;
	            }
	        });
	        return targets;
	    }

	    // 生成一道闪电的折线（中段抖动幅度大、端点不偏）
	    function generateLightning(x1, y1, x2, y2, segments, displacement) {
	        var pts = [{ x: x1, y: y1 }];
	        var dx = x2 - x1, dy = y2 - y1;
	        var len = Math.sqrt(dx * dx + dy * dy);
	        if (len < 1) return [{ x: x1, y: y1 }, { x: x2, y: y2 }];
	        var px = -dy / len, py = dx / len;     // 垂直单位向量
	        for (var i = 1; i < segments; i++) {
	            var t = i / segments;
	            var bx = x1 + dx * t;
	            var by = y1 + dy * t;
	            // 三角形包络：两端0，中点最大
	            var falloff = 1 - Math.abs(t - 0.5) * 2;
	            var d = (Math.random() - 0.5) * 2 * displacement * falloff;
	            pts.push({ x: bx + px * d, y: by + py * d });
	        }
	        pts.push({ x: x2, y: y2 });
	        return pts;
	    }

	    function spawnDragonSplit(centerX, centerY, battleDir, floorId) {
	        var cx = centerX * 32 + 16;
	        var cy = centerY * 32 + 16;
	        var targets = findDragonSplitTargets(centerX, centerY, battleDir, floorId);

	        // —— 1) 中心大闪光（先白后蓝白）——
	        state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [255, 255, 255], life: 0,    maxLife: 0.20 });
	        state.bursts.push({ kind: 'flash', x: cx, y: cy, rgb: [180, 220, 255], life: 0.05, maxLife: 0.30 });

	        // —— 2) 双扩散冲击波 ——
	        state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [220, 235, 255], life: 0,    maxLife: 0.45 });
	        state.bursts.push({ kind: 'ring', x: cx, y: cy, rgb: [140, 200, 255], life: 0.10, maxLife: 0.45 });

	        // —— 3) 向每个分裂位置射出闪电 ——
	        targets.forEach(function (t, idx) {
	            var tx = t.x * 32 + 16;
	            var ty = t.y * 32 + 16;

	            // 主闪电：3次抖动重生，模拟闪烁
	            for (var phase = 0; phase < 3; phase++) {
	                state.bursts.push({
	                    kind: 'bolt',
	                    x1: cx, y1: cy, x2: tx, y2: ty,
	                    points: generateLightning(cx, cy, tx, ty, 9, 9),
	                    rgb: [180, 220, 255],
	                    life: -phase * 0.08,
	                    maxLife: 0.18
	                });
	            }

	            // 终点爆裂
	            state.bursts.push({ kind: 'flash', x: tx, y: ty, rgb: [200, 230, 255], life: 0.22, maxLife: 0.18 });
	            state.bursts.push({ kind: 'ring',  x: tx, y: ty, rgb: [180, 220, 255], life: 0.22, maxLife: 0.30 });
	            // 终点的电火花
	            for (var i = 0; i < 10; i++) {
	                var ang = Math.random() * Math.PI * 2;
	                var spd = 1.5 + Math.random() * 2.2;
	                state.bursts.push({
	                    kind: 'particle',
	                    x: tx, y: ty,
	                    rgb: [180, 220, 255],
	                    vx: Math.cos(ang) * spd,
	                    vy: Math.sin(ang) * spd,
	                    life: 0.22 + Math.random() * 0.05,
	                    maxLife: 0.4 + Math.random() * 0.3,
	                    size: 1.2 + Math.random() * 1.2
	                });
	            }
	        });

	        // —— 4) 中心冲击波粒子（向四面八方）——
	        for (var i = 0; i < 16; i++) {
	            var ang = Math.random() * Math.PI * 2;
	            var spd = 2 + Math.random() * 2;
	            state.bursts.push({
	                kind: 'particle',
	                x: cx, y: cy,
	                rgb: [220, 240, 255],
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd,
	                life: 0,
	                maxLife: 0.45 + Math.random() * 0.3,
	                size: 1.4 + Math.random() * 1.3
	            });
	        }

	        // —— 5) 屏幕震动（重）+ 短暂蓝光叠层（用 hurt 借用，改成蓝）——
	        triggerShake(0.75, 0.45);
	    }

	    // ============================================================
	    // 豹跃时空穿梭：起点收束 → 光束相连 → 终点炸开
	    // ============================================================
	    function spawnTeleport(gx0, gy0, gx1, gy1) {
	        var x0 = gx0 * 32 + 16, y0 = gy0 * 32 + 16;
	        var x1 = gx1 * 32 + 16, y1 = gy1 * 32 + 16;
	        var rgbCool   = [170, 200, 255];   // 青蓝（穿梭主色）
	        var rgbViolet = [200, 150, 255];   // 紫（能量色）

	        // ===== 起点收束 =====
	        state.bursts.push({ kind: 'flash', x: x0, y: y0, rgb: [255, 255, 255], life: 0,    maxLife: 0.18 });
	        state.bursts.push({ kind: 'flash', x: x0, y: y0, rgb: rgbViolet,       life: 0.04, maxLife: 0.28 });
	        state.bursts.push({ kind: 'ring',  x: x0, y: y0, rgb: rgbCool,         life: 0,    maxLife: 0.40 });
	        state.bursts.push({ kind: 'ring',  x: x0, y: y0, rgb: rgbViolet,       life: 0.08, maxLife: 0.42 });

	        // 起点向外抛 12 颗粒子
	        for (var i = 0; i < 12; i++) {
	            var ang = (i / 12) * Math.PI * 2 + Math.random() * 0.4;
	            var spd = 1.2 + Math.random() * 1.6;
	            state.bursts.push({
	                kind: 'particle',
	                x: x0, y: y0, rgb: rgbCool,
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd,
	                life: 0,
	                maxLife: 0.45 + Math.random() * 0.3,
	                size: 1.0 + Math.random() * 1.1
	            });
	        }

	        // ===== 时空光束（起点 → 终点）=====
	        // 主光束
	        state.bursts.push({
	            kind: 'beam',
	            x1: x0, y1: y0, x2: x1, y2: y1,
	            rgb: rgbCool,
	            life: 0,
	            maxLife: 0.26
	        });
	        // 第二道光束（错时）
	        state.bursts.push({
	            kind: 'beam',
	            x1: x0, y1: y0, x2: x1, y2: y1,
	            rgb: rgbViolet,
	            life: 0.08,
	            maxLife: 0.22
	        });

	        // ===== 光束路径上的散落粒子（有一点垂直方向上的偏移）=====
	        var dx = x1 - x0, dy = y1 - y0;
	        var dist = Math.sqrt(dx * dx + dy * dy);
	        if (dist > 1) {
	            var px = -dy / dist;
	            var py = dx / dist;
	            var n = Math.max(6, Math.floor(dist / 12));
	            for (var i = 0; i < n; i++) {
	                var t = (i + 1) / (n + 1);
	                var bx = x0 + dx * t;
	                var by = y0 + dy * t;
	                var drift = (Math.random() - 0.5) * 10;
	                state.bursts.push({
	                    kind: 'particle',
	                    x: bx + px * drift,
	                    y: by + py * drift,
	                    rgb: rgbViolet,
	                    vx: (Math.random() - 0.5) * 0.4,
	                    vy: (Math.random() - 0.5) * 0.4 - 0.2,
	                    life: -t * 0.12,                       // 沿路径错时出现
	                    maxLife: 0.35 + Math.random() * 0.3,
	                    size: 1.0 + Math.random() * 1.0
	                });
	            }
	        }

	        // ===== 终点爆光 =====
	        var arrDelay = 0.16;
	        state.bursts.push({ kind: 'flash', x: x1, y: y1, rgb: [255, 255, 255], life: arrDelay,        maxLife: 0.20 });
	        state.bursts.push({ kind: 'flash', x: x1, y: y1, rgb: rgbViolet,       life: arrDelay + 0.05, maxLife: 0.30 });
	        state.bursts.push({ kind: 'ring',  x: x1, y: y1, rgb: rgbCool,         life: arrDelay,        maxLife: 0.50 });
	        state.bursts.push({ kind: 'ring',  x: x1, y: y1, rgb: [220, 180, 255], life: arrDelay + 0.10, maxLife: 0.48 });

	        // 终点向外爆 16 颗粒子
	        for (var i = 0; i < 16; i++) {
	            var ang = Math.random() * Math.PI * 2;
	            var spd = 1.5 + Math.random() * 2.0;
	            state.bursts.push({
	                kind: 'particle',
	                x: x1, y: y1, rgb: rgbCool,
	                vx: Math.cos(ang) * spd,
	                vy: Math.sin(ang) * spd - 0.3,
	                life: arrDelay,
	                maxLife: 0.4 + Math.random() * 0.4,
	                size: 1.2 + Math.random() * 1.3
	            });
	        }
	        // 终点向外升腾 5 道紫光
	        for (var i = 0; i < 5; i++) {
	            state.bursts.push({
	                kind: 'lift',
	                x: x1 + (Math.random() - 0.5) * 14,
	                y: y1 + 2, rgb: rgbViolet,
	                vx: (Math.random() - 0.5) * 0.2,
	                vy: -0.7 - Math.random() * 0.5,
	                life: arrDelay,
	                maxLife: 0.7 + Math.random() * 0.3,
	                size: 1.4 + Math.random()
	            });
	        }

	        // 轻微震动
	        triggerShake(0.35, 0.30);
	    }

	    // ============================================================
	    // 点击瞬移丝滑特效：柔光带 + 起点/终点淡环 + 路径散光斑
	    // ============================================================
	    function spawnSmoothMove(gx0, gy0, gx1, gy1, iceMode) {
	        var x0 = gx0 * 32 + 16, y0 = gy0 * 32 + 16;
	        var x1 = gx1 * 32 + 16, y1 = gy1 * 32 + 16;
	        // 颜色随冰封模式切换（冷色/暖白）——跟角色光晕风格保持一致
	        var rgb = iceMode ? [200, 230, 255] : [255, 235, 195];

	        // 1) 主光带：从起点到终点的丝滑渐变线条
	        state.bursts.push({
	            kind: 'streak',
	            x1: x0, y1: y0, x2: x1, y2: y1,
	            rgb: rgb,
	            life: 0,
	            maxLife: 0.38
	        });

	        // 2) 起点：从小到大的扩散环（"离开"感）
	        state.bursts.push({
	            kind: 'fadeRing',
	            x: x0, y: y0,
	            rgb: rgb,
	            life: 0,
	            maxLife: 0.32,
	            startR: 5, endR: 22
	        });

	        // 3) 终点：从大到小的收缩环（"到达"感）
	        state.bursts.push({
	            kind: 'fadeRing',
	            x: x1, y: y1,
	            rgb: rgb,
	            life: 0.05,
	            maxLife: 0.40,
	            startR: 26, endR: 8
	        });

	        // 4) 路径散光斑：每 16-20px 一颗，沿路径轻微偏移，按位置错时
	        var dx = x1 - x0, dy = y1 - y0;
	        var dist = Math.sqrt(dx * dx + dy * dy);
	        if (dist > 1) {
	            var nx = dx / dist, ny = dy / dist;
	            var px = -ny, py = nx;     // 垂直方向单位向量
	            var n = Math.max(4, Math.floor(dist / 18));
	            for (var i = 1; i <= n; i++) {
	                var t = i / (n + 1);
	                var ox = x0 + dx * t;
	                var oy = y0 + dy * t;
	                var perpD = (Math.random() - 0.5) * 5;
	                state.bursts.push({
	                    kind: 'softGlow',
	                    x: ox + px * perpD,
	                    y: oy + py * perpD,
	                    rgb: rgb,
	                    life: -t * 0.12,        // 沿路径错时
	                    maxLife: 0.42 + Math.random() * 0.18,
	                    r: 3 + Math.random() * 1.5
	                });
	            }
	        }
	        // 不做屏幕震动；保持"丝滑"感
	    }

	    // ============================================================
	    // 轮回剑光开屏：黑屏 → 延迟 → 扇形剑光扫亮
	    // ============================================================
	    function triggerScreenReveal() {
	        state.reveal.active = true;
	        state.reveal.time = 0;
	        // 锁定键盘输入直到动画完成
	        try {
	            if (core.lockControl && !state.reveal.locked) {
	                core.lockControl();
	                state.reveal.locked = true;
	            }
	        } catch (e) {}
	    }

	    function endScreenReveal() {
	        state.reveal.active = false;
	        if (state.reveal.locked) {
	            try { if (core.unlockControl) core.unlockControl(); } catch (e) {}
	            state.reveal.locked = false;
	        }
	    }

	    function drawScreenReveal(ctx, dt, W, H) {
	        if (!state.reveal.active) return;
	        state.reveal.time += dt / 1000;
	        var r = state.reveal;
	        var total = r.blackDur + r.sweepDur + r.fadeDur;
	        if (r.time >= total) {
	            ctx.clearRect(0, 0, W, H);
	            endScreenReveal();
	            return;
	        }

	        ctx.clearRect(0, 0, W, H);
	        var t = r.time;

	        // —— Phase 1: 全黑 ——
	        if (t < r.blackDur) {
	            ctx.fillStyle = 'rgba(0,0,0,1)';
	            ctx.fillRect(0, 0, W, H);
	            return;
	        }

	        // —— Phase 2: 扇形剑光扫亮 ——
	        if (t < r.blackDur + r.sweepDur) {
	            var sw = (t - r.blackDur) / r.sweepDur;
	            // 缓出，开头快后段慢
	            var swEase = 1 - Math.pow(1 - sw, 2.2);

	            // 1. 全屏铺黑
	            ctx.fillStyle = 'rgba(0,0,0,1)';
	            ctx.fillRect(0, 0, W, H);

	            // 2. 扇形枢轴：主角重生点 (6,12) 的中心
	            var pivotX = 6  * 32 + 16;      // = 208
	            var pivotY = 12 * 32 + 16;      // = 400
	            var R = Math.sqrt(W * W + H * H) + 120;
	            // 从"指向左"开始 → 顺时针经过"指向上" → 到"指向右"
	            // 在canvas坐标里：PI=左、3PI/2=上、2PI(=0)=右
	            var fixedAngle = Math.PI - 0.04;             // 起点略往上一点点（消除边缘锯齿）
	            var sweepRange = Math.PI + 0.08;             // ~180°+，略多覆盖
	            var leadAngle  = fixedAngle + swEase * sweepRange;

	            // 3. 用 destination-out 切掉扇形区域（露出底下画面）
	            ctx.save();
	            ctx.globalCompositeOperation = 'destination-out';
	            ctx.beginPath();
	            ctx.moveTo(pivotX, pivotY);
	            ctx.arc(pivotX, pivotY, R, fixedAngle, leadAngle, false);
	            ctx.closePath();
	            ctx.fill();
	            ctx.restore();

	            // 4. 在前沿（leadAngle方向）画剑光
	            ctx.save();
	            ctx.translate(pivotX, pivotY);
	            ctx.rotate(leadAngle);

	            // 4a. 剑身整体宽光带：从枢轴向外渐宽变亮
	            var swLen = R * 0.95;
	            var w0 = 0;                                  // 起点宽度（near pivot）
	            var w1 = 14;                                 // 末端宽度
	            var bladeGrad = ctx.createLinearGradient(0, 0, swLen, 0);
	            bladeGrad.addColorStop(0.00, 'rgba(255,235,180,0.00)');
	            bladeGrad.addColorStop(0.15, 'rgba(255,235,180,0.40)');
	            bladeGrad.addColorStop(0.55, 'rgba(255,250,210,0.85)');
	            bladeGrad.addColorStop(0.90, 'rgba(255,255,255,1.00)');
	            bladeGrad.addColorStop(1.00, 'rgba(255,255,255,0.85)');
	            ctx.fillStyle = bladeGrad;
	            // 用梯形（三角形+矩形混合）画
	            ctx.beginPath();
	            ctx.moveTo(0, -w0);
	            ctx.lineTo(swLen, -w1);
	            ctx.lineTo(swLen + 14, 0);
	            ctx.lineTo(swLen, w1);
	            ctx.lineTo(0, w0);
	            ctx.closePath();
	            ctx.fill();

	            // 4b. 剑身中央亮核
	            var coreGrad = ctx.createLinearGradient(0, 0, swLen, 0);
	            coreGrad.addColorStop(0, 'rgba(255,255,255,0)');
	            coreGrad.addColorStop(0.5, 'rgba(255,255,255,1)');
	            coreGrad.addColorStop(1, 'rgba(255,255,255,1)');
	            ctx.fillStyle = coreGrad;
	            ctx.fillRect(0, -1.5, swLen, 3);

	            // 4c. 剑尖外面再加一团圆形光晕
	            var tipR = 60;
	            var tipGrad = ctx.createRadialGradient(swLen, 0, 0, swLen, 0, tipR);
	            tipGrad.addColorStop(0, 'rgba(255,255,255,0.95)');
	            tipGrad.addColorStop(0.3, 'rgba(255,240,200,0.55)');
	            tipGrad.addColorStop(1, 'rgba(255,200,120,0)');
	            ctx.fillStyle = tipGrad;
	            ctx.beginPath();
	            ctx.arc(swLen, 0, tipR, 0, Math.PI * 2);
	            ctx.fill();

	            ctx.restore();

	            // 5. 沿剑光路径喷洒几颗向外抛的火星（在每帧的某些时刻补）
	            if (Math.random() < 0.5) {
	                var sx = pivotX + Math.cos(leadAngle) * swLen;
	                var sy = pivotY + Math.sin(leadAngle) * swLen;
	                // 不放进 bursts（那是 fx_above 层），这里直接画几个：
	                ctx.save();
	                ctx.globalCompositeOperation = 'lighter';
	                for (var k = 0; k < 4; k++) {
	                    var ox = sx + (Math.random() - 0.5) * 40;
	                    var oy = sy + (Math.random() - 0.5) * 40;
	                    ctx.fillStyle = 'rgba(255,240,200,' + (0.3 + Math.random() * 0.4).toFixed(3) + ')';
	                    ctx.beginPath();
	                    ctx.arc(ox, oy, 1.2 + Math.random() * 1.5, 0, Math.PI * 2);
	                    ctx.fill();
	                }
	                ctx.restore();
	            }
	            return;
	        }

	        // —— Phase 3: 收尾 — 一个非常短暂的亮屏（让剑光"散"开）——
	        var fadeT = (t - r.blackDur - r.sweepDur) / r.fadeDur;
	        var glowA = (1 - fadeT) * 0.18;
	        if (glowA > 0) {
	            ctx.fillStyle = 'rgba(255,245,210,' + glowA.toFixed(3) + ')';
	            ctx.fillRect(0, 0, W, H);
	        }
	    }

	    // ============================================================
	    // 角色相关效果：光晕跟随 / 脚下扬尘 / 踩痕
	    // ============================================================
	    function drawHeroFx(ctx, dt, iceMode) {
	        var h = getHeroPixel();
	        if (!h) return;
	        var dtSec = dt / 1000;

	        // —— 1. 光晕跟随（柔光环绕角色，冷暖随冰封模式切换）——
	        var prev = ctx.globalCompositeOperation;
	        ctx.globalCompositeOperation = 'lighter';
	        var haloR = 42;
	        var col = iceMode ? [160, 220, 255] : [255, 220, 160];   // 冰封：青；常态：暖
	        var pulse = Math.sin(state.timeMs * 0.003) * 0.08 + 1;
	        var grd = ctx.createRadialGradient(h.x, h.y, 0, h.x, h.y, haloR * pulse);
	        grd.addColorStop(0,   'rgba(' + col.join(',') + ',0.28)');
	        grd.addColorStop(0.5, 'rgba(' + col.join(',') + ',0.10)');
	        grd.addColorStop(1,   'rgba(' + col.join(',') + ',0)');
	        ctx.fillStyle = grd;
	        ctx.beginPath();
	        ctx.arc(h.x, h.y, haloR * pulse, 0, Math.PI * 2);
	        ctx.fill();
	        ctx.globalCompositeOperation = prev;

	        // —— 2. 脚下扬尘：移动中按节奏喷粒子 ——
	        if (h.moving) {
	            if (state.timeMs - state.heroDustTime > 80) {
	                state.heroDustTime = state.timeMs;
	                for (var i = 0; i < 3; i++) {
	                    state.heroDust.push({
	                        x: h.x + (Math.random() - 0.5) * 8,
	                        y: h.y + 12 + (Math.random() - 0.5) * 3,    // 脚下
	                        vx: (Math.random() - 0.5) * 0.4,
	                        vy: -0.15 - Math.random() * 0.25,
	                        life: 0,
	                        maxLife: 0.45 + Math.random() * 0.25,
	                        size: 1.2 + Math.random() * 1.0
	                    });
	                }
	            }
	        }
	        // 更新+绘制粒子
	        for (var i = state.heroDust.length - 1; i >= 0; i--) {
	            var p = state.heroDust[i];
	            p.life += dtSec;
	            if (p.life >= p.maxLife) { state.heroDust.splice(i, 1); continue; }
	            p.vy += 0.005;                    // 微重力
	            p.x += p.vx;
	            p.y += p.vy;
	            var k = p.life / p.maxLife;
	            var alpha = (1 - k) * 0.55;
	            var size = p.size * (1 + k * 0.8);
	            // 颜色：冰封时偏冷蓝白，常态偏暖灰
	            if (iceMode) {
	                ctx.fillStyle = 'rgba(200, 220, 240,' + alpha.toFixed(3) + ')';
	            } else {
	                ctx.fillStyle = 'rgba(190, 175, 145,' + alpha.toFixed(3) + ')';
	            }
	            ctx.beginPath();
	            ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
	            ctx.fill();
	        }

	        // —— 3. 踩痕（替代草弯折）：每换一格留一个慢慢淡化的暗影 ——
	        var cellKey = h.gx + ',' + h.gy;
	        if (cellKey !== state.heroLastCell && h.moving) {
	            state.heroLastCell = cellKey;
	            state.heroPrints.push({
	                x: h.gx * 32 + 16,
	                y: h.gy * 32 + 22,           // 在格子底部
	                life: 0,
	                maxLife: 1.2
	            });
	            // 控制队列长度，最多保留6个
	            if (state.heroPrints.length > 6) state.heroPrints.shift();
	        }
	        for (var i = state.heroPrints.length - 1; i >= 0; i--) {
	            var pr = state.heroPrints[i];
	            pr.life += dtSec;
	            if (pr.life >= pr.maxLife) { state.heroPrints.splice(i, 1); continue; }
	            var k = pr.life / pr.maxLife;
	            var alpha = (1 - k) * 0.22;
	            // 椭圆暗影，模拟踩压痕迹
	            var g2 = ctx.createRadialGradient(pr.x, pr.y, 0, pr.x, pr.y, 10);
	            g2.addColorStop(0, 'rgba(0,0,0,' + alpha.toFixed(3) + ')');
	            g2.addColorStop(1, 'rgba(0,0,0,0)');
	            ctx.fillStyle = g2;
	            ctx.beginPath();
	            ctx.ellipse(pr.x, pr.y, 10, 4, 0, 0, Math.PI * 2);
	            ctx.fill();
	        }
	    }

	    function drawDebugIndicator(ctx, frame) {
	        // 左上角 16x16 红绿闪烁方块 + 帧数文字
	        var blink = (frame % 60) < 30;
	        ctx.fillStyle = blink ? '#ff0040' : '#00ff40';
	        ctx.fillRect(4, 4, 16, 16);
	        ctx.strokeStyle = '#fff';
	        ctx.lineWidth = 1;
	        ctx.strokeRect(4, 4, 16, 16);
	        ctx.font = 'bold 10px monospace';
	        ctx.fillStyle = '#fff';
	        ctx.strokeStyle = '#000';
	        ctx.lineWidth = 3;
	        ctx.strokeText('FX#' + frame, 24, 16);
	        ctx.fillText('FX#' + frame,  24, 16);
	    }

	    function drawTorchDebug(ctx, cfg) {
	        if (!cfg.torches) return;
	        ctx.save();
	        ctx.font = 'bold 10px monospace';
	        ctx.lineWidth = 2;
	        cfg.torches.forEach(function (t, i) {
	            var cx = t.x * 32 + 16, cy = t.y * 32 + 16;
	            ctx.strokeStyle = '#ff0';
	            ctx.strokeRect(cx - 6, cy - 6, 12, 12);
	            ctx.strokeStyle = '#000';
	            ctx.strokeText('T' + i, cx + 8, cy - 4);
	            ctx.fillStyle = '#ff0';
	            ctx.fillText('T' + i, cx + 8, cy - 4);
	        });
	        ctx.restore();
	    }

	    this.installAmbientFx = function () {

	        if (!core || !core.registerAnimationFrame) {
	            console.warn('[ambientFx] core 未就绪，跳过');
	            return;
	        }
	        if (core.__ambientFxInstalled) return;
	        core.__ambientFxInstalled = true;

	        // —— 钩 getItem：拾取物品时爆光 + 属性变化飘字 + 道具计数飘字 ——
	        if (core.events && core.events.getItem && !core.events.__ambientFxHooked) {
	            var __origGetItem = core.events.getItem;

	            // 快照工具/装备/常量计数
	            function snapshotItemCounts(hero) {
	                var snap = { tools: {}, constants: {}, equips: {} };
	                if (!hero || !hero.items) return snap;
	                ['tools', 'constants', 'equips'].forEach(function (cls) {
	                    var bucket = hero.items[cls];
	                    if (!bucket) return;
	                    Object.keys(bucket).forEach(function (k) { snap[cls][k] = bucket[k] || 0; });
	                });
	                return snap;
	            }
	            function diffItemCounts(before, hero) {
	                var diffs = [];
	                if (!hero || !hero.items || !before) return diffs;
	                ['tools', 'constants', 'equips'].forEach(function (cls) {
	                    var cur = hero.items[cls] || {};
	                    Object.keys(cur).forEach(function (k) {
	                        var d = (cur[k] || 0) - (before[cls][k] || 0);
	                        if (d > 0) diffs.push({ id: k, diff: d });
	                    });
	                });
	                return diffs;
	            }

	            core.events.getItem = function (id, num, x, y, isGentleClick, callback) {
	                var hero = core.status && core.status.hero;
	                var before = hero ? {
	                    hp:   hero.hp    || 0,
	                    atk:  hero.atk   || 0,
	                    def:  hero.def   || 0,
	                    mdef: hero.mdef  || 0,
	                    gold: hero.money || hero.gold || 0,   // 本工程用 money
	                    exp:  hero.exp   || 0
	                } : null;
	                var beforeItems = snapshotItemCounts(hero);

	                try {
	                    if (typeof x === 'number' && typeof y === 'number') {
	                        spawnPickupBurst(x, y, id);
	                    }
	                } catch (e) { console.warn('[ambientFx] burst error', e); }

	                var ret = __origGetItem.call(this, id, num, x, y, isGentleClick, callback);

	                // 比对前后变化，弹出对应飘字
	                try {
	                    if (before && hero && typeof x === 'number' && typeof y === 'number') {
	                        var idx = 0;
	                        var dHp   = (hero.hp   || 0) - before.hp;
	                        var dAtk  = (hero.atk  || 0) - before.atk;
	                        var dDef  = (hero.def  || 0) - before.def;
	                        var dMdef = (hero.mdef || 0) - before.mdef;
	                        var dGold = ((hero.money != null ? hero.money : hero.gold) || 0) - before.gold;
	                        var dExp  = (hero.exp  || 0) - before.exp;
	                        // 成长/寄生必须跟随实际属性变化确定性累计，录像/回退不能依赖动画帧。
	                        recordGrowthParasiteDiff({ atk: before.atk, hp: before.hp }, { atk: hero.atk || 0, hp: hero.hp || 0 });
	                        if (dHp   > 0) spawnStatNumber(x, y, dHp,   'hp',   idx++);
	                        if (dAtk  > 0) spawnStatNumber(x, y, dAtk,  'atk',  idx++);
	                        if (dDef  > 0) spawnStatNumber(x, y, dDef,  'def',  idx++);
	                        if (dMdef > 0) spawnStatNumber(x, y, dMdef, 'mdef', idx++);
	                        if (dGold > 0) spawnStatNumber(x, y, dGold, 'gold', idx++);
	                        if (dExp  > 0) spawnStatNumber(x, y, dExp,  'exp',  idx++);

	                        // 道具/钥匙/装备等计数变化
	                        var itemDiffs = diffItemCounts(beforeItems, hero);
	                        itemDiffs.forEach(function (d) {
	                            var disp = TOOL_DISPLAY[d.id];
	                            if (disp) spawnStatNumber(x, y, d.diff, disp, idx++);
	                        });
	                    }
	                } catch (e) { console.warn('[ambientFx] stat number error', e); }

	                return ret;
	            };
	            core.events.__ambientFxHooked = true;
	        }

	        // —— 钩 _useItemEffect：直接使用道具时也要记录成长/寄生（例如背包里使用血瓶）——
	        if (core.items && core.items._useItemEffect && !core.items.__growthParasiteUseItemHooked) {
	            var __origUseItemEffectForGP = core.items._useItemEffect;
	            core.items._useItemEffect = function (itemId) {
	                var before = snapshotHeroAtkHp();
	                var ret = __origUseItemEffectForGP.apply(this, arguments);
	                try {
	                    recordGrowthParasiteDiff(before, snapshotHeroAtkHp());
	                } catch (e) { console.warn('[ambientFx] growth/parasite use item track error', e); }
	                return ret;
	            };
	            core.items.__growthParasiteUseItemHooked = true;
	        }

	        // —— 钩 battle：战斗瞬间触发反馈 ——
	        if (core.events && core.events.battle && !core.events.__ambientFxBattleHooked) {
	            var __origBattle = core.events.battle;
	            core.events.battle = function (id, x, y, force, callback) {
	                try {
	                    var actualId = id || (typeof x === 'number' ? core.getBlockId(x, y) : null);
	                    if (actualId && typeof x === 'number' && typeof y === 'number') {
	                        var dmg = null;
	                        try { dmg = core.enemys.getDamage(actualId, x, y); } catch (e) {}
	                        // 检测特殊技能：血战（33）= 血腥红；魂锁（34）= 紫色灵魂；共生（37）= 青绿；吞噬（38）= 深紫红
	                        var isBloodWar = false, isSoulLock = false;
	                        var isArmorShatter = false, isSkillBreak = false;
	                        var isSymbiosis = false, isDevour = false;
	                        try {
	                            var enemy = core.material && core.material.enemys && core.material.enemys[actualId];
	                            if (enemy && core.enemys && core.enemys.hasSpecial) {
	                                isBloodWar     = core.enemys.hasSpecial(enemy, 33);
	                                isSoulLock     = core.enemys.hasSpecial(enemy, 34);
	                                isArmorShatter = core.enemys.hasSpecial(enemy, 35);
	                                isSkillBreak   = core.enemys.hasSpecial(enemy, 36);
	                                isSymbiosis    = core.enemys.hasSpecial(enemy, 37);
	                                isDevour       = core.enemys.hasSpecial(enemy, 38);
	                            }
	                        } catch (e) {}
                        // 躲避怪如果本次能躲开，beforeBattle 会取消真正战斗；这里不要提前飘扣血数字/战斗爆光。
                        var willDodgeWithoutBattle = false;
                        try {
                            var actualSpecial = enemy ? core.getEnemyValue(enemy, 'special', x, y) : null;
                            var cd = core.control && core.control.controldata;
                            willDodgeWithoutBattle = !!(enemy && core.enemys && core.enemys.hasSpecial(actualSpecial, 45)
                                && cd && cd.getMonsterDodgeTarget && cd.getMonsterDodgeTarget(x, y, core.status.floorId));
                        } catch (eDodgeSkip) {}

                        // dmg == null 表示打不过此怪物，不触发战斗特效
                        if (!willDodgeWithoutBattle && dmg != null && dmg >= 0) {
                            spawnBattleHit(x, y, dmg, isBloodWar, isSoulLock, isSymbiosis, isDevour);

	                            // 碎甲/破功：弹"防-1"/"攻-1"飘字
	                            // 仅在【打得过且不会死】时才弹（实际数值变更由 afterBattle 处理）
	                            // 因为 getDamage 在"必死"情况下仍返回数字，单纯 dmg != null 拦不住
	                            try {
	                                var hero = core.status && core.status.hero;
	                                var willSurvive = hero && dmg < (hero.hp || 1);
	                                if (willSurvive) {
	                                    if (isArmorShatter && (hero.def || 0) > 0) {
	                                        spawnStatNumber(x, y, 1, 'def', 0, true);
	                                    }
	                                    if (isSkillBreak && (hero.atk || 0) > 0) {
	                                        spawnStatNumber(x, y, 1, 'atk', isArmorShatter ? 1 : 0, true);
	                                    }
	                                }
	                            } catch (e) {}
	                        }
	                    }
	                } catch (e) { console.warn('[ambientFx] battle effect error', e); }
	                return __origBattle.call(this, id, x, y, force, callback);
	            };
	            core.events.__ambientFxBattleHooked = true;
	        }

	        // —— 钩 reviveDragonDescendants：屠龙分裂时化身闪电（录像跳过）——
	        // controldata 在 functions.js 里，需要在它加载之后才能钩；这里延迟到第一次调用前钩
	        function tryHookControlData() {
	            var cd = core.control && core.control.controldata;
	            if (!cd) return false;
	            var hooked = false;

	            // 1) 屠龙分裂闪电
	            if (cd.reviveDragonDescendants && !cd.__ambientFxDragonHooked) {
	                var __orig1 = cd.reviveDragonDescendants;
	                cd.reviveDragonDescendants = function (enemyId, centerX, centerY, floorId, enemyPointInfo, battleDir) {
	                    var isReplaying = core.status && core.status.replay && core.status.replay.replaying;
	                    if (!isReplaying) {
	                        try { spawnDragonSplit(centerX, centerY, battleDir, floorId || core.status.floorId); }
	                        catch (e) { console.warn('[ambientFx] dragon split fx error', e); }
	                    }
	                    return __orig1.apply(this, arguments);
	                };
	                cd.__ambientFxDragonHooked = true;
	                hooked = true;
	            }

	            // 2) 轮回重生剑光
	            if (cd.tryFirstBattleFailRevive && !cd.__ambientFxReviveHooked) {
	                var __orig2 = cd.tryFirstBattleFailRevive;
	                cd.tryFirstBattleFailRevive = function () {
	                    var result = __orig2.apply(this, arguments);
	                    // 只有成功触发轮回时才放动画
	                    if (result === true) {
	                        var isReplaying = core.status && core.status.replay && core.status.replay.replaying;
	                        if (!isReplaying) {
	                            try { triggerScreenReveal(); }
	                            catch (e) { console.warn('[ambientFx] reveal fx error', e); }
	                        }
	                    }
	                    return result;
	                };
	                cd.__ambientFxReviveHooked = true;
	                hooked = true;
	            }

	            // 3) 豹跃时空穿梭
	            if (cd.performLeopardLeapTo && !cd.__ambientFxLeopardHooked) {
	                var __orig3 = cd.performLeopardLeapTo;
	                cd.performLeopardLeapTo = function (tx, ty, fromReplay, routeAction) {
	                    // 捕获起点（原函数会把勇士瞬移走）
	                    var gx0 = -1, gy0 = -1;
	                    try {
	                        if (core.getHeroLoc) {
	                            var loc = core.getHeroLoc();
	                            if (loc) { gx0 = loc.x; gy0 = loc.y; }
	                        }
	                    } catch (e) {}

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

	                    // 录像里完全跳过：fromReplay 是入参显式标志；replaying 是全局状态
	                    var isReplaying = core.status && core.status.replay && core.status.replay.replaying;
	                    if (!fromReplay && !isReplaying && result !== false && gx0 >= 0 && gy0 >= 0) {
	                        // 起点终点相同（理论上不该发生）就别画了
	                        if (gx0 !== tx || gy0 !== ty) {
	                            try { spawnTeleport(gx0, gy0, tx, ty); }
	                            catch (e) { console.warn('[ambientFx] teleport fx error', e); }
	                        }
	                    }
	                    return result;
	                };
	                cd.__ambientFxLeopardHooked = true;
	                hooked = true;
	            }

	            // 4) 穿梭：击杀后传送到最近同名怪物身上。复用时空穿梭光束，但颜色/节奏更像被怪物牵引。
	            if (cd.performMonsterShuttleTo && !cd.__ambientFxShuttleHooked) {
	                var __origShuttle = cd.performMonsterShuttleTo;
	                cd.performMonsterShuttleTo = function (tx, ty, fromReplay, routeAction, info) {
	                    var gx0 = -1, gy0 = -1;
	                    try {
	                        if (core.getHeroLoc) {
	                            var loc = core.getHeroLoc();
	                            if (loc) { gx0 = loc.x; gy0 = loc.y; }
	                        }
	                    } catch (e) {}

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

	                    var isReplaying = core.status && core.status.replay && core.status.replay.replaying;
	                    if (!fromReplay && !isReplaying && result !== false && gx0 >= 0 && gy0 >= 0) {
	                        if (gx0 !== tx || gy0 !== ty) {
	                            try {
	                                spawnTeleport(gx0, gy0, tx, ty);
	                                // 额外补一圈冷蓝冲击，和普通豹跃区分。
	                                var x1 = tx * 32 + 16, y1 = ty * 32 + 16;
	                                state.bursts.push({ kind: 'ring', x: x1, y: y1, rgb: [110, 235, 255], life: 0.04, maxLife: 0.52 });
	                                state.bursts.push({ kind: 'flash', x: x1, y: y1, rgb: [220, 255, 255], life: 0, maxLife: 0.20 });
	                                triggerShake(0.32, 0.22);
	                            }
	                            catch (e) { console.warn('[ambientFx] shuttle fx error', e); }
	                        }
	                    }
	                    return result;
	                };
	                cd.__ambientFxShuttleHooked = true;
	                hooked = true;
	            }

	            // 5) 点击瞬移丝滑特效
	            if (cd.moveDirectly && !cd.__ambientFxMoveDirectlyHooked) {
	                var __orig4 = cd.moveDirectly;
	                cd.moveDirectly = function (x, y, ignoreSteps) {
	                    // 起点：调用前的勇士位置
	                    var gx0 = -1, gy0 = -1;
	                    try {
	                        if (core.getHeroLoc) {
	                            var loc = core.getHeroLoc();
	                            if (loc) { gx0 = loc.x; gy0 = loc.y; }
	                        }
	                    } catch (e) {}

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

	                    // 录像跳过
	                    var isReplaying = core.status && core.status.replay && core.status.replay.replaying;
	                    if (!isReplaying && result === true && gx0 >= 0 && gy0 >= 0) {
	                        // 终点用调用后的真实位置（屠龙触发时可能不到 x,y）
	                        var newLoc = core.getHeroLoc ? core.getHeroLoc() : null;
	                        if (newLoc && (newLoc.x !== gx0 || newLoc.y !== gy0)) {
	                            try {
	                                // 检测冰封模式
	                                var floorId = core.status && core.status.floorId;
	                                var reviveUsed = (core.getFlag && core.getFlag('__firstBattleFailReviveUsed__', {})) || {};
	                                var iceMode = !!reviveUsed[floorId];
	                                spawnSmoothMove(gx0, gy0, newLoc.x, newLoc.y, iceMode);
	                            } catch (e) { console.warn('[ambientFx] smooth move fx error', e); }
	                        }
	                    }
	                    return result;
	                };
	                cd.__ambientFxMoveDirectlyHooked = true;
	                hooked = true;
	            }

	            return cd.__ambientFxDragonHooked && cd.__ambientFxReviveHooked && cd.__ambientFxLeopardHooked && cd.__ambientFxShuttleHooked && cd.__ambientFxMoveDirectlyHooked;
	        }
	        // 立即尝试；失败则在动画帧里再次尝试（最多20次）
	        if (!tryHookControlData()) {
	            var hookAttempts = 0;
	            var hookInterval = setInterval(function () {
	                if (tryHookControlData() || ++hookAttempts > 20) clearInterval(hookInterval);
	            }, 200);
	        }

	        // 暴露调试钩子
	        core.plugin._ambientFxConfig = CONFIG;
	        core.plugin.ambientFxDebug   = false;   // 显示火把坐标方框（调位置用）

	        // ===== 怪物手册特殊属性说明区滚动支持 =====
	        // 注：UI 层（ui.js）已自带 ▲/▼ 点击翻页，这里只补桌面端鼠标滚轮
	        if (core.actions && core.actions._sys_onmousewheel && !core.actions._sys_onmousewheel.__bookScrollHooked) {
	            var __origSysWheel = core.actions._sys_onmousewheel;
	            core.actions._sys_onmousewheel = function (direct) {
	                if (core.status.lockControl && core.status.event && core.status.event.id == 'book') {
	                    var step = Math.max(1, core.status._bookV2SpecialPageStep || 4);
	                    var max  = Math.max(0, core.status._bookV2SpecialMaxScroll || 0);
	                    var cur  = Math.max(0, core.status._bookV2SpecialScroll || 0);
	                    if (direct == 1)       cur = Math.max(0, cur - step);
	                    else if (direct == -1) cur = Math.min(max, cur + step);
	                    core.status._bookV2SpecialScroll = cur;
	                    core.ui.drawBook(core.status.event.data);
	                    return;
	                }
	                return __origSysWheel.call(this, direct);
	            };
	            core.actions._sys_onmousewheel.__bookScrollHooked = true;
	        }
	        // （点击翻页 ui.js 内部 _drawBookV2_installRightSpecialScrollPatch 自己挂了，这里不重复挂）

	        if (VERBOSE_LOG) {
	            console.log('%c[ambientFx] 已安装 ✅', 'background:#0c8;color:#fff;padding:2px 8px;font-weight:bold');
	            console.log('[ambientFx] 配置楼层:', Object.keys(CONFIG));
	            console.log('[ambientFx] 进入游戏后，左上角红绿闪烁方块=插件运行中');
	            console.log('[ambientFx] 如要关闭调试方块: core.plugin._ambientFxConfig.HIDE_INDICATOR=true 然后切换楼层');
	        }

	        core.registerAnimationFrame('ambientFx', true, function (timestamp) {
	            if (!timestamp) return;
	            var W = (core._PX_ || core.__PIXELS__ || 416);
	            var H = (core._PY_ || core.__PIXELS__ || 416);
	            var dt = state.lastTs ? Math.min(64, timestamp - state.lastTs) : 16;
	            state.lastTs = timestamp;
	            state.timeMs += dt;
	            state.frame++;

	            // 每帧追踪 hero 属性差值（成长/寄生用）
	            try { updateGrowthParasiteTracking(); } catch (e) {}

	            var floorId = core.status && core.status.floorId;

	            // 每60帧打印一次进度（仅verbose）
	            if (VERBOSE_LOG && state.frame % 60 === 1) {
	                console.log('[ambientFx] frame', state.frame, 'floor=', floorId, 'cfg=', !!CONFIG[floorId]);
	            }

	            var cfg = CONFIG[floorId];
	            if (!cfg) {
	                // 没显式配置的楼层：fallback 到 DUNGEON 模板，享受萤火虫等默认环境特效
	                // 但没火把、没水晶光晕（这些需手动配置位置）
	                cfg = Object.assign({}, DUNGEON, { torches: [], magicLights: [] });
	            }

	            // 检测冰封模式：游戏在 functions.js 用此 flag 标记每层是否已轮回过
	            var reviveUsed = (core.getFlag && core.getFlag('__firstBattleFailReviveUsed__', {})) || {};
	            var iceMode = !!(cfg && reviveUsed[floorId]);

	            // 合成有效配置（冰封模式下用 iceOverride 覆盖颜色相关字段）
	            var ecfg = cfg;
	            if (iceMode && cfg.iceOverride) {
	                ecfg = Object.assign({}, cfg);
	                if (cfg.iceOverride.magicLights) ecfg.magicLights = cfg.iceOverride.magicLights;
	                if (cfg.iceOverride.torchColor)  ecfg.torchColor  = cfg.iceOverride.torchColor;
	                if (cfg.iceOverride.tint)        ecfg.tint        = cfg.iceOverride.tint;
	                if (cfg.iceOverride.fireflies)   ecfg.fireflies   = Object.assign({}, cfg.fireflies, cfg.iceOverride.fireflies);
	            }

	            // 楼层切换或冰封状态切换：清画布、重新初始化粒子
	            if (state.currentFloor !== floorId || state.iceMode !== iceMode) {
	                if (VERBOSE_LOG) console.log('[ambientFx] floor/ice change:', state.currentFloor, '/', state.iceMode, '->', floorId, '/', iceMode);
	                state.currentFloor = floorId;
	                state.iceMode = iceMode;
	                try { core.deleteCanvas('fx_below');     } catch (e) {}
	                try { core.deleteCanvas('fx_above');     } catch (e) {}
	                try { core.deleteCanvas('fx_top');       } catch (e) {}
	                try { core.deleteCanvas('fx_indicator'); } catch (e) {}
	                if (ecfg) initParticles(ecfg, W, H);
	            }

	            // 即使没有配置，也画一个调试方块（这样如果方块不出现 = 插件没跑）
	            if (DEBUG_INDICATOR && !CONFIG._HIDE_INDICATOR) {
	                var ctxI = core.getContextByName('fx_indicator');
	                if (!ctxI) {
	                    ctxI = core.createCanvas('fx_indicator', 0, 0, W, H, 130);
	                    if (ctxI) ctxI.imageSmoothingEnabled = false;
	                }
	                if (ctxI) {
	                    ctxI.clearRect(0, 0, W, H);
	                    drawDebugIndicator(ctxI, state.frame);
	                }
	            }

	            if (!ecfg) {
	                // 无楼层配置：跳过环境特效（火把、雾、萤火等），但仍需画战斗特效、徽章、屏震、伤害飘字
	                var ctxA0 = core.getContextByName('fx_above');
	                if (!ctxA0) {
	                    ctxA0 = core.createCanvas('fx_above', 0, 0, W, H, 85);
	                    if (ctxA0) ctxA0.imageSmoothingEnabled = false;
	                }
	                if (ctxA0) {
	                    ctxA0.clearRect(0, 0, W, H);
	                    drawSoulChains(ctxA0, state.timeMs);
	                    drawSymbiosisChains(ctxA0, state.timeMs);
	                    drawCounterBadges(ctxA0, state.timeMs);
	                    drawHeroFx(ctxA0, dt, iceMode);
	                    drawBursts(ctxA0, dt);
	                    drawDamageNums(ctxA0, dt);
	                }
	                updateShake(dt);
	                return;
	            }

	            // —— 层1 角色下方 (z=15)：tint + 雾 ——
	            var ctxB = core.getContextByName('fx_below');
	            if (!ctxB) {
	                ctxB = core.createCanvas('fx_below', 0, 0, W, H, 15);
	                if (ctxB) ctxB.imageSmoothingEnabled = false;
	            }
	            if (ctxB) {
	                ctxB.clearRect(0, 0, W, H);
	                drawTintVignette(ctxB, { tint: ecfg.tint }, W, H);
	                drawFog(ctxB, ecfg, state.timeMs, W, H);
	            }

	            // —— 层2 角色上方 (z=85)：光、火星、萤火虫、呼吸光 ——
	            var ctxA = core.getContextByName('fx_above');
	            if (!ctxA) {
	                ctxA = core.createCanvas('fx_above', 0, 0, W, H, 85);
	                if (ctxA) ctxA.imageSmoothingEnabled = false;
	            }
	            if (ctxA) {
	                ctxA.clearRect(0, 0, W, H);
	                drawTorches(ctxA, ecfg, state.timeMs);
	                drawMagic(ctxA, ecfg, state.timeMs);
	                drawEmbers(ctxA, dt);
	                drawItemHighlights(ctxA, state.timeMs, iceMode);
	                drawSoulChains(ctxA, state.timeMs);
	                drawSymbiosisChains(ctxA, state.timeMs);
	                drawCounterBadges(ctxA, state.timeMs);
	                drawFireflies(ctxA, dt, W, H);
	                drawHeroFx(ctxA, dt, iceMode);
	                drawWeather(ctxA, dt, W, H);
	                drawBursts(ctxA, dt);
	                drawDamageNums(ctxA, dt);
	                if (core.plugin.ambientFxDebug) drawTorchDebug(ctxA, ecfg);
	            }

	            // 屏幕震动（每帧更新CSS transform）
	            updateShake(dt);

	            // —— 层3 最顶 (z=92)：暗角 + 受伤红雾 ——
	            var ctxT = core.getContextByName('fx_top');
	            if (!ctxT) {
	                ctxT = core.createCanvas('fx_top', 0, 0, W, H, 92);
	                if (ctxT) ctxT.imageSmoothingEnabled = false;
	            }
	            if (ctxT) {
	                ctxT.clearRect(0, 0, W, H);
	                drawTintVignette(ctxT, { vignette: ecfg.vignette }, W, H);
	                drawHurt(ctxT, dt, W, H);
	            }

	            // —— 层4 帘幕 (z=128)：轮回剑光开屏（最高，盖住所有游戏画面）——
	            if (state.reveal.active) {
	                var ctxC = core.getContextByName('fx_curtain');
	                if (!ctxC) {
	                    ctxC = core.createCanvas('fx_curtain', 0, 0, W, H, 128);
	                    if (ctxC) ctxC.imageSmoothingEnabled = false;
	                }
	                if (ctxC) drawScreenReveal(ctxC, dt, W, H);
	            } else {
	                // 不在动画期间就清理这个画布
	                if (core.getContextByName('fx_curtain')) {
	                    try { core.deleteCanvas('fx_curtain'); } catch (e) {}
	                }
	            }
	        });

	        // 临时关闭调试方块的开关
	        core.plugin.setAmbientFxIndicator = function (on) {
	            CONFIG._HIDE_INDICATOR = !on;
	            try { core.deleteCanvas('fx_indicator'); } catch (e) {}
	        };
	    };
	},

	"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;
			// 进入MT4龙魂王战后直到通关前，没有新的轮回次数；状态栏直接显示0，避免误导玩家。
			try { if (core.hasFlag && core.hasFlag('__MT4BossNoRevive__')) return 0; } catch (e) {}
			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._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';

			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');
				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);
				}
				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');
			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);
			}
		})();
	}
	,
	"mt4BossIntroFx": function () {
		// MT4 BOSS入场演出：纯Canvas绘制，不依赖BOSS素材图。
		// 入口：core.plugin.playMT4BossIntro();
		this.hideMT4BossHud = function () {
			try { core.deleteCanvas('mt4BossHud'); } catch (e) {}
		};

		// 读档/回退后动态画布会被清掉；进入或恢复到MT4时用这个函数把BOSS本体和血条补回来。
		this.ensureMT4BossScene = function () {
			if (main.mode == 'editor') return;
			if (!core.status || core.status.floorId !== 'MT4') return;
			try {
				// A回档/读档会清空动态画布，但插件对象上的运行标记可能还没来得及复位。
				// 如果画布不存在，先强制复位，保证下面能重新创建代码BOSS本体。
				if (!core.dymCanvas || !core.dymCanvas.mt4BossBody) core.plugin.__mt4BossBodyRunning = false;
				var phase = parseInt(core.getFlag('__MT4BossPhase__', 1)) || 1;
				var hideP4Boss = phase == 4 && !core.hasFlag('__MT4BossP4Unlocked__');
				if (hideP4Boss) {
					if (core.plugin.hideMT4BossBody) core.plugin.hideMT4BossBody();
				} else if (core.plugin.showMT4BossBody) core.plugin.showMT4BossBody();
				if (core.plugin.drawMT4BossHud) core.plugin.drawMT4BossHud(core.getFlag('__MT4BossHpSegments__', 6));
				if (core.getFlag('__MT4BossPhase__', 1) == 3 && !core.hasFlag('__MT4BossP3SealsCleared__')) {
					if (core.plugin.showMT4BossP3SealFx) core.plugin.showMT4BossP3SealFx();
				} else if (core.plugin.hideMT4BossP3SealFx) {
					core.plugin.hideMT4BossP3SealFx();
				}
				if (core.getFlag('__MT4BossPhase__', 1) == 4 && !core.hasFlag('__MT4BossP4Unlocked__')) {
					if (core.plugin.showMT4BossP4AltarFx) core.plugin.showMT4BossP4AltarFx();
				} else if (core.plugin.hideMT4BossP4AltarFx) {
					core.plugin.hideMT4BossP4AltarFx();
				}
			} catch (e) {}
		};

		this.drawMT4BossHud = function (filledSegments) {
			if (main.mode == 'editor') return;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var ctx = core.getContextByName('mt4BossHud');
			if (!ctx) ctx = core.createCanvas('mt4BossHud', 0, 0, W, H, 128);
			if (!ctx) return;
			ctx.canvas.style.pointerEvents = 'none';
			ctx.clearRect(0, 0, W, H);
			if (filledSegments == null) filledSegments = core.getFlag('__MT4BossHpSegments__', 6);
			filledSegments = Math.max(0, Math.min(6, filledSegments));

			var scale = Math.min(W, H) / 416;
			var x = W * 0.13, y = 10 * scale, w = W * 0.74, h = 22 * scale;
			var titleH = 13 * scale;
			ctx.save();
			ctx.globalCompositeOperation = 'source-over';
			ctx.fillStyle = 'rgba(0,0,0,0.86)';
			ctx.fillRect(x - 8 * scale, y - 4 * scale, w + 16 * scale, h + titleH + 9 * scale);
			ctx.strokeStyle = 'rgba(255,225,120,0.95)';
			ctx.lineWidth = Math.max(1, 2 * scale);
			ctx.strokeRect(x - 8 * scale, y - 4 * scale, w + 16 * scale, h + titleH + 9 * scale);
			ctx.font = 'bold ' + Math.max(9, Math.round(10 * scale)) + 'px Arial';
			ctx.textAlign = 'center';
			ctx.textBaseline = 'middle';
			ctx.fillStyle = '#FFD76A';
			ctx.fillText('龙魂王', x + w / 2, y + 5 * scale);

			var gap = 3 * scale;
			var segW = (w - gap * 5) / 6;
			var sy = y + titleH;
			for (var i = 0; i < 6; i++) {
				var sx = x + i * (segW + gap);
				ctx.fillStyle = 'rgba(35,8,10,0.95)';
				ctx.fillRect(sx, sy, segW, h);
				ctx.strokeStyle = 'rgba(255,210,120,0.65)';
				ctx.strokeRect(sx, sy, segW, h);
				if (i < filledSegments) {
					var g = ctx.createLinearGradient(sx, sy, sx, sy + h);
					g.addColorStop(0, '#FFB0A0');
					g.addColorStop(0.18, '#FF3E35');
					g.addColorStop(0.65, '#B00018');
					g.addColorStop(1, '#4A0008');
					ctx.fillStyle = g;
					ctx.fillRect(sx + 2 * scale, sy + 2 * scale, segW - 4 * scale, h - 4 * scale);
					ctx.fillStyle = 'rgba(255,255,255,0.28)';
					ctx.fillRect(sx + 3 * scale, sy + 3 * scale, segW - 6 * scale, Math.max(1, h * 0.18));
				}
			}
			ctx.restore();
		};


		this.setMT4BossHpSegments = function (segments) {
			segments = Math.max(0, Math.min(6, segments == null ? 6 : segments));
			try { core.setFlag('__MT4BossHpSegments__', segments); } catch (e) {}
			try { core.plugin.drawMT4BossHud(segments); } catch (e) {}
		};

		this.drawMT4BossBodyModel = function (ctx, t, alpha, scaleMul) {
			if (!ctx) return;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var cx = W / 2, cy = H / 2 + 9 * scale;
			alpha = alpha == null ? 1 : alpha;
			scaleMul = scaleMul == null ? 1 : scaleMul;
			// 龙魂王阶段视觉：P2 比 P1 更亮、更凶；后续阶段会依次加强，不影响实际战斗和录像。
			var phase = 1;
			try { phase = Math.max(1, Math.min(6, parseInt(core.getFlag('__MT4BossPhase__', 1)) || 1)); } catch (e) {}
			var palettes = [
				{ aura:[210,80,255], body:[70,0,95], eye:[255,60,35], soulA:[170,80,255], soulB:[255,90,50] },
				{ aura:[255,64,64], body:[95,0,35], eye:[255,210,80], soulA:[255,60,80], soulB:[255,180,60] },
				{ aura:[80,160,255], body:[15,20,105], eye:[80,240,255], soulA:[90,170,255], soulB:[180,255,255] },
				{ aura:[80,255,170], body:[0,90,60], eye:[180,255,80], soulA:[80,255,170], soulB:[255,230,80] },
				{ aura:[255,190,60], body:[100,55,0], eye:[255,245,110], soulA:[255,190,60], soulB:[255,90,40] },
				{ aura:[195,80,255], body:[80,0,120], eye:[255,90,255], soulA:[195,80,255], soulB:[255,80,180] },
				{ aura:[255,90,185], body:[120,0,75], eye:[255,210,255], soulA:[255,90,185], soulB:[255,150,90] },
				{ aura:[80,255,255], body:[0,80,110], eye:[220,255,255], soulA:[80,255,255], soulB:[80,150,255] },
				{ aura:[255,255,110], body:[120,100,0], eye:[255,255,210], soulA:[255,255,110], soulB:[255,120,60] },
				{ aura:[255,35,35], body:[130,0,0], eye:[255,255,255], soulA:[255,35,35], soulB:[255,220,90] }
			];
			var pal = palettes[phase - 1] || palettes[0];
			var heat = 0.32 + (phase - 1) * 0.035;
			ctx.save();
			ctx.translate(cx, cy - 3 * scale);
			var breath = 1 + Math.sin(t / 260) * 0.025;
			ctx.scale(scaleMul * breath, scaleMul * breath);
			ctx.globalAlpha = alpha;

			// 背后暗紫气场
			ctx.globalCompositeOperation = 'lighter';
			var aura = ctx.createRadialGradient(0, 6 * scale, 0, 0, 16 * scale, 112 * scale);
			aura.addColorStop(0, 'rgba(' + pal.aura.join(',') + ',' + heat.toFixed(3) + ')');
			aura.addColorStop(0.45, 'rgba(' + pal.aura.join(',') + ',' + (heat * 0.50).toFixed(3) + ')');
			aura.addColorStop(1, 'rgba(0,0,0,0)');
			ctx.fillStyle = aura;
			ctx.beginPath(); ctx.ellipse(0, 12 * scale, 112 * scale, 95 * scale, 0, 0, Math.PI * 2); ctx.fill();
			// 阶段越高，背后能量环越多。
			ctx.strokeStyle = 'rgba(' + pal.aura.join(',') + ',' + Math.min(0.85, 0.18 + phase * 0.05).toFixed(3) + ')';
			ctx.lineWidth = Math.max(1, (1 + phase * 0.13) * scale);
			for (var ring = 0; ring < Math.min(4, Math.ceil(phase / 3)); ring++) {
				ctx.beginPath();
				ctx.ellipse(0, 12 * scale, (72 + ring * 18 + Math.sin(t / 300 + ring) * 3) * scale, (48 + ring * 13) * scale, t * 0.0007 * (ring % 2 ? -1 : 1), 0, Math.PI * 2);
				ctx.stroke();
			}

			// 主体黑影
			ctx.globalCompositeOperation = 'source-over';
			var body = ctx.createRadialGradient(0, 5 * scale, 10 * scale, 0, 18 * scale, 92 * scale);
			body.addColorStop(0, 'rgba(' + pal.body.join(',') + ',0.96)');
			body.addColorStop(0.55, 'rgba(20,0,35,0.97)');
			body.addColorStop(1, 'rgba(0,0,0,0)');
			ctx.fillStyle = body;
			ctx.beginPath(); ctx.ellipse(0, 18 * scale, 86 * scale, 76 * scale, 0, 0, Math.PI * 2); ctx.fill();

			// 角、翼状轮廓
			ctx.fillStyle = 'rgba(5,0,15,0.99)';
			ctx.beginPath();
			ctx.moveTo(-72 * scale, -18 * scale);
			ctx.lineTo(-112 * scale, -64 * scale);
			ctx.lineTo(-55 * scale, -44 * scale);
			ctx.lineTo(0, -82 * scale);
			ctx.lineTo(55 * scale, -44 * scale);
			ctx.lineTo(112 * scale, -64 * scale);
			ctx.lineTo(72 * scale, -18 * scale);
			ctx.closePath(); ctx.fill();

			// 单眼发光
			ctx.globalCompositeOperation = 'lighter';
			var eyePulse = 0.8 + Math.sin(t / 190) * 0.18;
			var eg = ctx.createRadialGradient(0, -3 * scale, 0, 0, -3 * scale, 38 * scale);
			eg.addColorStop(0, 'rgba(255,255,210,' + (0.95 * eyePulse).toFixed(3) + ')');
			eg.addColorStop(0.25, 'rgba(' + pal.eye.join(',') + ',0.95)');
			eg.addColorStop(1, 'rgba(' + pal.eye.join(',') + ',0)');
			ctx.fillStyle = eg;
			ctx.beginPath(); ctx.ellipse(0, -3 * scale, 52 * scale, 23 * scale, 0, 0, Math.PI * 2); ctx.fill();
			ctx.globalCompositeOperation = 'source-over';
			ctx.fillStyle = 'rgba(0,0,0,0.96)';
			ctx.beginPath(); ctx.ellipse(Math.sin(t / 300) * 5 * scale, -3 * scale, 8 * scale, 22 * scale, 0, 0, Math.PI * 2); ctx.fill();

			// 细小魂火
			ctx.globalCompositeOperation = 'lighter';
			for (var i = 0; i < 18; i++) {
				var a = i * Math.PI * 2 / 18 + t * 0.00055 * (i % 2 ? 1 : -1);
				var r = (58 + (i % 5) * 8 + Math.sin(t / 400 + i) * 3) * scale;
				var x = Math.cos(a) * r, y = Math.sin(a) * r + 8 * scale;
				var s = (1.4 + (i % 3) * 0.4) * scale;
				var sc = i % 2 ? pal.soulA : pal.soulB;
				ctx.fillStyle = 'rgba(' + sc.join(',') + ',' + (i % 2 ? 0.55 : 0.45) + ')';
				ctx.beginPath(); ctx.arc(x, y, s, 0, Math.PI * 2); ctx.fill();
			}
			ctx.restore();

			// P3阵眼存在期间，主BOSS左右各出现一个稍小分身。
			if (phase >= 3 && core.status && core.status.floorId === 'MT4' && !core.hasFlag('__MT4BossP3SealsCleared__')) {
				try {
					core.plugin.drawMT4BossMiniEcho(ctx, t, W * 0.22, cy + 12 * scale, 0.58, 0.86, -1);
					core.plugin.drawMT4BossMiniEcho(ctx, t, W * 0.78, cy + 12 * scale, 0.58, 0.86, 1);
				} catch (e) {}
			}
		};

		this.drawMT4BossMiniEcho = function (ctx, t, cx, cy, scaleMul, alpha, side) {
			if (!ctx) return;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, core._PY_ || core.__PIXELS__ || 416) / 416;
			var hue = side < 0 ? [80, 190, 255] : [255, 80, 150];
			var eye = side < 0 ? [170, 245, 255] : [255, 220, 230];
			ctx.save();
			ctx.translate(cx, cy);
			var breath = 1 + Math.sin(t / 240 + side) * 0.035;
			ctx.scale((scaleMul || 0.58) * breath, (scaleMul || 0.58) * breath);
			ctx.globalAlpha = alpha == null ? 1 : alpha;
			ctx.globalCompositeOperation = 'lighter';
			var g = ctx.createRadialGradient(0, 0, 0, 0, 0, 72 * scale);
			g.addColorStop(0, 'rgba(' + hue.join(',') + ',0.45)');
			g.addColorStop(1, 'rgba(' + hue.join(',') + ',0)');
			ctx.fillStyle = g;
			ctx.beginPath(); ctx.ellipse(0, 6 * scale, 76 * scale, 62 * scale, 0, 0, Math.PI * 2); ctx.fill();
			ctx.globalCompositeOperation = 'source-over';
			ctx.fillStyle = 'rgba(8,0,22,0.96)';
			ctx.beginPath();
			ctx.moveTo(-48 * scale, -8 * scale);
			ctx.lineTo(-78 * scale, -42 * scale);
			ctx.lineTo(-34 * scale, -30 * scale);
			ctx.lineTo(0, -58 * scale);
			ctx.lineTo(34 * scale, -30 * scale);
			ctx.lineTo(78 * scale, -42 * scale);
			ctx.lineTo(48 * scale, -8 * scale);
			ctx.closePath(); ctx.fill();
			ctx.beginPath(); ctx.ellipse(0, 16 * scale, 58 * scale, 52 * scale, 0, 0, Math.PI * 2); ctx.fill();
			ctx.globalCompositeOperation = 'lighter';
			var eg = ctx.createRadialGradient(0, -2 * scale, 0, 0, -2 * scale, 24 * scale);
			eg.addColorStop(0, 'rgba(255,255,255,0.95)');
			eg.addColorStop(0.35, 'rgba(' + eye.join(',') + ',0.9)');
			eg.addColorStop(1, 'rgba(' + eye.join(',') + ',0)');
			ctx.fillStyle = eg;
			ctx.beginPath(); ctx.ellipse(0, -2 * scale, 34 * scale, 15 * scale, 0, 0, Math.PI * 2); ctx.fill();
			ctx.globalCompositeOperation = 'source-over';
			ctx.fillStyle = 'rgba(0,0,0,0.96)';
			ctx.beginPath(); ctx.ellipse(Math.sin(t / 260 + side) * 4 * scale, -2 * scale, 5 * scale, 14 * scale, 0, 0, Math.PI * 2); ctx.fill();
			ctx.restore();
		};

		this.showMT4BossBody = function () {
			if (main.mode == 'editor') return;
			if (!core.status || core.status.floorId !== 'MT4') return;
			if (core.plugin.__mt4BossBodyRunning) {
				if (core.dymCanvas && core.dymCanvas.mt4BossBody) return;
				core.plugin.__mt4BossBodyRunning = false;
			}
			core.plugin.__mt4BossBodyRunning = true;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			try { core.deleteCanvas('mt4BossBody'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossBody', 0, 0, W, H, 118);
			if (!ctx) return;
			ctx.canvas.style.pointerEvents = 'none';
			var start = null;
			function loop(ts) {
				if (!core.plugin.__mt4BossBodyRunning || !core.status || core.status.floorId !== 'MT4') {
					core.plugin.__mt4BossBodyRunning = false;
					try { core.deleteCanvas('mt4BossBody'); } catch (e) {}
					return;
				}
				if (start == null) start = ts;
				var t = ts - start;
				ctx.clearRect(0, 0, W, H);
				try { core.plugin.drawMT4BossBodyModel(ctx, t, 1, 1); } catch (e) {}
				requestAnimationFrame(loop);
			}
			requestAnimationFrame(loop);
		};

		this.hideMT4BossBody = function () {
			core.plugin.__mt4BossBodyRunning = false;
			try { core.deleteCanvas('mt4BossBody'); } catch (e) {}
		};


		this.playMT4BossPhaseTransition = function (phase, title, callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossPhaseFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossPhaseFx', 0, 0, W, H, 190);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), total = 1320;
			var colors = [
				[180, 70, 255], [255, 80, 70], [80, 210, 255], [255, 210, 60],
				[120, 255, 150], [255, 90, 190], [140, 120, 255], [255, 140, 50], [90, 255, 230], [255, 255, 255]
			];
			var col = colors[(Math.max(1, phase || 1) - 1) % colors.length];
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function easeIn(v){v=clamp(v,0,1);return v*v*v;}
			function frame(now) {
				var t = now - start, p = clamp(t / total, 0, 1);
				ctx.clearRect(0, 0, W, H);
				// 震动：进场和爆闪时更强。
				if (core.dom && core.dom.gameDraw) {
					var shake = (1 - p) * (phase >= 3 ? 7 : 5) * scale;
					if (t > 420 && t < 720) shake += 4 * scale;
					var dx = (Math.random() - 0.5) * shake, dy = (Math.random() - 0.5) * shake;
					try { core.dom.gameDraw.style.transform = 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px)'; } catch (e) {}
				}
				var fadeOut = clamp((total - t) / 240, 0, 1);
				ctx.fillStyle = 'rgba(0,0,0,' + (0.15 + 0.32 * fadeOut).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				// 全屏闪烁/变色。
				var flash = Math.max(0, 1 - Math.abs(t - 560) / 180);
				if (flash > 0) {
					ctx.fillStyle = 'rgba(' + col.join(',') + ',' + (0.18 * flash).toFixed(3) + ')';
					ctx.fillRect(0, 0, W, H);
				}
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				var cx = W / 2, cy = 91 * scale;
				// 横向能量流：从两侧扫入，再向两侧炸开。
				var inP = easeOut(t / 340), outP = easeIn((t - 950) / 260);
				var barW = W * (0.28 + 0.55 * inP) * (1 + outP * 0.45);
				var x0 = cx - barW / 2 + outP * W * 0.35;
				var x1 = cx + barW / 2 - outP * W * 0.35;
				var bg = ctx.createLinearGradient(x0, cy, x1, cy);
				bg.addColorStop(0, 'rgba(' + col.join(',') + ',0)');
				bg.addColorStop(0.16, 'rgba(' + col.join(',') + ',0.65)');
				bg.addColorStop(0.5, 'rgba(255,255,220,0.88)');
				bg.addColorStop(0.84, 'rgba(' + col.join(',') + ',0.65)');
				bg.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
				ctx.fillStyle = bg;
				ctx.fillRect(x0, cy - 19 * scale, x1 - x0, 38 * scale);
				for (var i = 0; i < 3; i++) {
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.45 - i * 0.10).toFixed(3) + ')';
					ctx.lineWidth = (2 + i) * scale;
					ctx.beginPath();
					ctx.moveTo(0, cy - (28 + i * 9) * scale + Math.sin(t / 90 + i) * 5 * scale);
					ctx.lineTo(W, cy - (28 + i * 9) * scale + Math.cos(t / 80 + i) * 5 * scale);
					ctx.stroke();
					ctx.beginPath();
					ctx.moveTo(0, cy + (28 + i * 9) * scale + Math.cos(t / 85 + i) * 5 * scale);
					ctx.lineTo(W, cy + (28 + i * 9) * scale + Math.sin(t / 95 + i) * 5 * scale);
					ctx.stroke();
				}
				ctx.restore();
				// 文本平移进场、停留、平移出场。
				var textIn = easeOut(t / 300), textOut = easeIn((t - 980) / 260);
				var tx = W / 2 - W * (1 - textIn) + W * textOut;
				var main = title || ('第' + phase + '阶段');
				ctx.save();
				ctx.translate(tx, cy);
				ctx.rotate((-7 * (1 - textIn) + 7 * textOut) * Math.PI / 180);
				ctx.textAlign = 'center';
				ctx.textBaseline = 'middle';
				ctx.font = 'bold ' + Math.max(20, Math.round(24 * scale)) + 'px Arial';
				ctx.lineWidth = Math.max(3, 5 * scale);
				ctx.strokeStyle = 'rgba(0,0,0,0.96)';
				ctx.strokeText(main, 0, 0);
				ctx.shadowColor = 'rgb(' + col.join(',') + ')';
				ctx.shadowBlur = 24 * scale;
				ctx.fillStyle = '#FFF0A0';
				ctx.fillText(main, 0, 0);
				ctx.restore();
				if (t < total && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else {
					try { if (core.dom && core.dom.gameDraw) core.dom.gameDraw.style.transform = ''; } catch (e) {}
					try { core.deleteCanvas('mt4BossPhaseFx'); } catch (e) {}
					if (callback) callback();
				}
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};


		this.playMT4BossRebirthToMT0Transition = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossRebirthFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossRebirthFx', 0, 0, W, H, 195);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), total = 2100;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function easeInOut(v){v=clamp(v,0,1);return v<0.5?2*v*v:1-Math.pow(-2*v+2,2)/2;}
			function frame(now) {
				var t = now - start, p = clamp(t / total, 0, 1);
				ctx.clearRect(0, 0, W, H);
				var cx = W / 2, cy = H / 2;
				// 黑幕吞噬
				ctx.fillStyle = 'rgba(0,0,0,' + (0.18 + 0.72 * easeInOut(p)).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				// 震动
				if (core.dom && core.dom.gameDraw && t < 1650) {
					var amp = (8 * (1 - p) + (t > 650 && t < 950 ? 8 : 0)) * scale;
					var dx = (Math.random() - 0.5) * amp, dy = (Math.random() - 0.5) * amp;
					try { core.dom.gameDraw.style.transform = 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px)'; } catch (e) {}
				}
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				// 破碎血条能量向中心倒灌
				for (var i = 0; i < 18; i++) {
					var a = i / 18 * Math.PI * 2 + t / 260;
					var rr = (180 - 130 * easeOut(p)) * scale;
					var x = cx + Math.cos(a) * rr;
					var y = cy + Math.sin(a * 1.7) * rr * 0.38;
					var g = ctx.createRadialGradient(x, y, 0, x, y, (18 + 14 * Math.sin(t / 80 + i)) * scale);
					g.addColorStop(0, 'rgba(255,60,80,0.90)');
					g.addColorStop(0.55, 'rgba(120,60,255,0.35)');
					g.addColorStop(1, 'rgba(0,0,0,0)');
					ctx.fillStyle = g;
					ctx.beginPath(); ctx.arc(x, y, 30 * scale, 0, Math.PI * 2); ctx.fill();
				}
				// 中心传送门
				var portal = easeOut((t - 300) / 700);
				for (var r = 0; r < 5; r++) {
					ctx.strokeStyle = 'rgba(' + (80 + r * 35) + ',' + (160 + r * 10) + ',255,' + (0.8 - r * 0.12).toFixed(3) + ')';
					ctx.lineWidth = (2 + r * 1.5) * scale;
					ctx.beginPath();
					ctx.ellipse(cx, cy, (35 + r * 22 + Math.sin(t / 110 + r) * 6) * scale * portal, (12 + r * 9) * scale * portal, t / 420 + r, 0, Math.PI * 2);
					ctx.stroke();
				}
				// 光柱
				if (t > 700) {
					var bp = easeOut((t - 700) / 480) * (1 - clamp((t - 1600) / 400, 0, 1));
					var lg = ctx.createLinearGradient(cx - 40 * scale, 0, cx + 40 * scale, 0);
					lg.addColorStop(0, 'rgba(120,200,255,0)');
					lg.addColorStop(0.45, 'rgba(180,230,255,' + (0.65 * bp).toFixed(3) + ')');
					lg.addColorStop(0.5, 'rgba(255,255,255,' + bp.toFixed(3) + ')');
					lg.addColorStop(0.55, 'rgba(180,230,255,' + (0.65 * bp).toFixed(3) + ')');
					lg.addColorStop(1, 'rgba(120,200,255,0)');
					ctx.fillStyle = lg;
					ctx.fillRect(cx - 52 * scale, 0, 104 * scale, H);
				}
				ctx.restore();
				// 字幕
				ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				ctx.font = 'bold ' + Math.max(18, Math.round(22 * scale)) + 'px Arial';
				var tp = easeOut(t / 460) * (1 - clamp((t - 1680) / 260, 0, 1));
				ctx.lineWidth = 5 * scale;
				ctx.strokeStyle = 'rgba(0,0,0,0.95)';
				var targetFloor = (core.getFlag ? core.getFlag('__MT4BossNextTrialFloor__', 'MT0') : 'MT0');
				var rebirthText = '轮回裂隙开启 · ' + targetFloor + '试炼';
				ctx.strokeText(rebirthText, cx, 74 * scale + (1 - tp) * 30 * scale);
				ctx.fillStyle = 'rgba(255,230,150,' + tp.toFixed(3) + ')';
				ctx.fillText(rebirthText, cx, 74 * scale + (1 - tp) * 30 * scale);
				if (t < total) requestAnimationFrame(frame);
				else {
					try { if (core.dom && core.dom.gameDraw) core.dom.gameDraw.style.transform = ''; } catch (e) {}
					try { core.deleteCanvas('mt4BossRebirthFx'); } catch (e) {}
					if (callback) callback();
				}
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossMT0RevealTransition = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossMT0RevealFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossMT0RevealFx', 0, 0, W, H, 198);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), total = 2350;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function easeIn(v){v=clamp(v,0,1);return v*v*v;}
			function easeInOut(v){v=clamp(v,0,1);return v<0.5?2*v*v:1-Math.pow(-2*v+2,2)/2;}
			// 碎片：先完整盖住黑幕，随后从中心爆炸式炸开，露出MT0。
			var pieces = [];
			var cols = 9, rows = 9, cw = W / cols, rh = H / rows;
			var cx = W / 2, cy = H / 2;
			for (var yy = 0; yy < rows; yy++) {
				for (var xx = 0; xx < cols; xx++) {
					var x0 = xx * cw, y0 = yy * rh;
					var mx = x0 + cw / 2, my = y0 + rh / 2;
					var ang = Math.atan2(my - cy, mx - cx) + (Math.random() - 0.5) * 0.55;
					var dist = Math.sqrt((mx - cx) * (mx - cx) + (my - cy) * (my - cy));
					var speed = (90 + dist * 0.75 + Math.random() * 150) * scale;
					pieces.push({
						x: mx, y: my, w: cw + 3 * scale, h: rh + 3 * scale,
						ang: ang, dx: Math.cos(ang) * speed, dy: Math.sin(ang) * speed,
						rot: (Math.random() - 0.5) * 2.8, delay: Math.random() * 280,
						shade: 10 + Math.random() * 22
					});
				}
			}
			var sparks = [];
			for (var i = 0; i < 90; i++) {
				var a = Math.random() * Math.PI * 2, sp = (80 + Math.random() * 260) * scale;
				sparks.push({ x: cx, y: cy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, r: (1 + Math.random() * 2.4) * scale, life: 0.55 + Math.random() * 0.7, hue: Math.random() < 0.5 ? [130,210,255] : [255,210,110] });
			}
			function frame(now) {
				var t = now - start;
				ctx.clearRect(0, 0, W, H);
				// 最开始必须是一整块黑幕。
				if (t < 260) {
					ctx.fillStyle = '#000000';
					ctx.fillRect(0, 0, W, H);
					// 黑幕内部裂纹预热
					var pre = easeOut(t / 260);
					ctx.save(); ctx.globalCompositeOperation = 'lighter';
					for (var i = 0; i < 10; i++) {
						var a = i / 10 * Math.PI * 2 + Math.sin(t / 80 + i) * 0.12;
						ctx.strokeStyle = 'rgba(120,210,255,' + (0.22 * pre).toFixed(3) + ')';
						ctx.lineWidth = (1.4 + i % 3) * scale;
						ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(a) * 190 * scale * pre, cy + Math.sin(a) * 150 * scale * pre); ctx.stroke();
					}
					ctx.restore();
					requestAnimationFrame(frame);
					return;
				}
				var tp = t - 260;
				// 爆炸震动
				if (core.dom && core.dom.gameDraw && t < 1500) {
					var amp = (12 * (1 - clamp((t - 260) / 1100, 0, 1)) + (t < 560 ? 8 : 0)) * scale;
					var sx = (Math.random() - 0.5) * amp, sy = (Math.random() - 0.5) * amp;
					try { core.dom.gameDraw.style.transform = 'translate(' + sx.toFixed(1) + 'px,' + sy.toFixed(1) + 'px)'; } catch (e) {}
				}
				// 背景暗角，越到后面越淡。
				var shade = 0.28 * (1 - clamp((t - 1150) / 900, 0, 1));
				if (shade > 0) {
					ctx.fillStyle = 'rgba(0,0,0,' + shade.toFixed(3) + ')';
					ctx.fillRect(0, 0, W, H);
				}
				// 黑幕碎片飞散。
				for (var p = 0; p < pieces.length; p++) {
					var one = pieces[p];
					var q = easeOut((tp - one.delay) / 1150);
					if (q <= 0) {
						ctx.fillStyle = '#000000';
						ctx.fillRect(one.x - one.w / 2, one.y - one.h / 2, one.w + 1, one.h + 1);
						continue;
					}
					if (q >= 1 && tp > 1550) continue;
					var px = one.x + one.dx * q;
					var py = one.y + one.dy * q + 70 * scale * easeIn(q);
					var rot = one.rot * q;
					var alpha = 1 - clamp((q - 0.72) / 0.28, 0, 1);
					var sc = 1 - 0.28 * q;
					ctx.save();
					ctx.translate(px, py);
					ctx.rotate(rot);
					ctx.fillStyle = 'rgba(' + one.shade + ',' + one.shade + ',' + (one.shade + 8) + ',' + alpha.toFixed(3) + ')';
					ctx.fillRect(-one.w * sc / 2, -one.h * sc / 2, one.w * sc, one.h * sc);
					ctx.strokeStyle = 'rgba(120,210,255,' + (0.25 * alpha).toFixed(3) + ')';
					ctx.lineWidth = 1 * scale;
					ctx.strokeRect(-one.w * sc / 2, -one.h * sc / 2, one.w * sc, one.h * sc);
					ctx.restore();
				}
				// 中心爆光和粒子。
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				var boom = easeOut(tp / 420) * (1 - clamp((tp - 620) / 600, 0, 1));
				if (boom > 0) {
					var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, 190 * scale * boom);
					g.addColorStop(0, 'rgba(255,255,255,' + (0.80 * boom).toFixed(3) + ')');
					g.addColorStop(0.35, 'rgba(120,220,255,' + (0.48 * boom).toFixed(3) + ')');
					g.addColorStop(1, 'rgba(120,220,255,0)');
					ctx.fillStyle = g;
					ctx.beginPath(); ctx.arc(cx, cy, 190 * scale * boom, 0, Math.PI * 2); ctx.fill();
				}
				for (var s = 0; s < sparks.length; s++) {
					var spk = sparks[s];
					var u = clamp(tp / (spk.life * 1000), 0, 1);
					if (u >= 1) continue;
					var x = spk.x + spk.vx * u;
					var y = spk.y + spk.vy * u + 80 * scale * u * u;
					var a = 1 - u;
					ctx.fillStyle = 'rgba(' + spk.hue.join(',') + ',' + a.toFixed(3) + ')';
					ctx.beginPath(); ctx.arc(x, y, spk.r * (1 - u * 0.4), 0, Math.PI * 2); ctx.fill();
				}
				ctx.restore();
				// 不同试炼层追加不同“黑幕撕裂”花样：MT0爆裂、MT1螺旋、MT2剑痕、MT3镜碎。
				var floorName = core.getFlag ? core.getFlag('__MT4BossRevealFloor__', 'MT0') : 'MT0';
				var palette = floorName == 'MT1' ? [255, 210, 120] : (floorName == 'MT2' ? [120, 255, 210] : (floorName == 'MT3' ? [210, 140, 255] : [120, 220, 255]));
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				var themeP = easeOut(clamp((t - 380) / 1000, 0, 1)) * (1 - clamp((t - 1700) / 480, 0, 1));
				if (themeP > 0) {
					if (floorName == 'MT1') {
						for (var ring = 0; ring < 7; ring++) {
							ctx.strokeStyle = 'rgba(' + palette.join(',') + ',' + (0.22 * themeP).toFixed(3) + ')';
							ctx.lineWidth = (1.5 + ring * 0.3) * scale;
							ctx.beginPath();
							for (var a0 = 0; a0 < Math.PI * 2; a0 += 0.16) {
								var rr0 = (18 + ring * 13 + a0 * 13 + Math.sin(a0 * 4 + t / 110) * 5) * scale * themeP;
								var xx0 = cx + Math.cos(a0 + t / 420) * rr0;
								var yy0 = cy + Math.sin(a0 + t / 420) * rr0;
								if (a0 === 0) ctx.moveTo(xx0, yy0); else ctx.lineTo(xx0, yy0);
							}
							ctx.stroke();
						}
					} else if (floorName == 'MT2') {
						for (var cut = -4; cut <= 4; cut++) {
							var off = cut * 52 * scale + Math.sin(t / 100 + cut) * 9 * scale;
							ctx.strokeStyle = 'rgba(' + palette.join(',') + ',' + (0.30 * themeP).toFixed(3) + ')';
							ctx.lineWidth = (2.5 + Math.abs(cut % 2)) * scale;
							ctx.beginPath();
							ctx.moveTo(cx - 260 * scale + off, -20 * scale);
							ctx.lineTo(cx + 70 * scale + off, H + 30 * scale);
							ctx.stroke();
						}
					} else if (floorName == 'MT3') {
						for (var shard = 0; shard < 18; shard++) {
							var aa = shard / 18 * Math.PI * 2 + t / 370;
							var rr = (70 + (shard % 4) * 25) * scale * themeP;
							var sx = cx + Math.cos(aa) * rr;
							var sy = cy + Math.sin(aa * 1.3) * rr * 0.72;
							ctx.strokeStyle = 'rgba(' + palette.join(',') + ',' + (0.32 * themeP).toFixed(3) + ')';
							ctx.lineWidth = 2 * scale;
							ctx.beginPath();
							ctx.moveTo(sx - 18 * scale, sy - 9 * scale);
							ctx.lineTo(sx + 21 * scale, sy - 15 * scale);
							ctx.lineTo(sx + 9 * scale, sy + 18 * scale);
							ctx.closePath();
							ctx.stroke();
						}
					}
				}
				ctx.restore();
				// 字幕
				var textIn = easeOut((t - 520) / 420), textOut = clamp((t - 1850) / 350, 0, 1);
				var ta = textIn * (1 - textOut);
				if (ta > 0) {
					ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
					ctx.font = 'bold ' + Math.max(18, Math.round(22 * scale)) + 'px Arial';
					ctx.lineWidth = 5 * scale;
					ctx.strokeStyle = 'rgba(0,0,0,0.95)';
					var yy = 72 * scale + (1 - textIn) * 26 * scale - textOut * 28 * scale;
					var floorName2 = core.getFlag ? core.getFlag('__MT4BossRevealFloor__', 'MT0') : 'MT0';
					var revealText = floorName2 + ' · 黑幕撕裂';
					ctx.strokeText(revealText, cx, yy);
					ctx.shadowColor = 'rgba(120,220,255,' + ta.toFixed(3) + ')'; ctx.shadowBlur = 18 * scale;
					ctx.fillStyle = 'rgba(255,240,160,' + ta.toFixed(3) + ')';
					ctx.fillText(revealText, cx, yy);
					ctx.shadowBlur = 0;
				}
				if (t < total) requestAnimationFrame(frame);
				else {
					try { if (core.dom && core.dom.gameDraw) core.dom.gameDraw.style.transform = ''; } catch (e) {}
					try { core.deleteCanvas('mt4BossMT0RevealFx'); } catch (e2) {}
					if (callback) callback();
				}
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('破墙'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.setMT4BossInkBlackWhiteMode = function (enabled) {
			enabled = !!enabled;
			try {
				if (enabled) core.setFlag('__MT4BossInkBlackWhiteMode__', true);
				else core.removeFlag('__MT4BossInkBlackWhiteMode__');
			} catch (e) {}
			try {
				var id = 'mt4BossInkBWStyle';
				var style = document.getElementById(id);
				if (!style) {
					style = document.createElement('style');
					style.id = id;
					style.type = 'text/css';
					style.innerHTML = [
						'body.mt4-boss-ink-bw #gameDraw canvas:not(#mt4BossP5InkFx):not(#mt4BossP6P7InkFx){filter:grayscale(1) contrast(1.06);}',
						'body.mt4-boss-ink-bw #statusBar,body.mt4-boss-ink-bw #toolBar,body.mt4-boss-ink-bw #floorMsgGroup{filter:grayscale(1) contrast(1.08);}',
						'body.mt4-boss-ink-bw #statusBar img,body.mt4-boss-ink-bw #toolBar img{filter:grayscale(1) contrast(1.08);}',
						'body.mt4-boss-ink-bw #gif,body.mt4-boss-ink-bw #gif2,body.mt4-boss-ink-bw #next{filter:grayscale(1) contrast(1.06);}',
						'body.mt4-boss-ink-bw #mt4BossP5InkFx{filter:none!important;}',
						'body.mt4-boss-ink-bw #mt4BossP6P7InkFx{filter:none!important;}'
					].join('\n');
					document.head.appendChild(style);
				}
				if (document.body && document.body.classList) {
					if (enabled) document.body.classList.add('mt4-boss-ink-bw');
					else document.body.classList.remove('mt4-boss-ink-bw');
				}
			} catch (e2) {}
		};


		this.playMT4BossP5InkSequence = function (callback) {
			// P5击破后30秒黑白水墨演出。严格按用户分镜：演出结束后外部再切342黑白底图和黑白UI。
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			try { if (core.plugin && core.plugin.setMT4BossInkBlackWhiteMode) core.plugin.setMT4BossInkBlackWhiteMode(false); } catch (eBW0) {}
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var S = Math.min(W, H) / 416;
			var cx = W / 2, cy = H / 2;
			var total = 30000;
			var nowFunc = (window.performance && performance.now) ? function(){ return performance.now(); } : function(){ return Date.now(); };
			var startTime = nowFunc();
			try { core.deleteCanvas('mt4BossP5InkFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP5InkFx', 0, 0, W, H, 206);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			ctx.canvas.style.filter = 'none';
			var scene = document.createElement('canvas'); scene.width = W; scene.height = H;
			var sctx = scene.getContext('2d');
			var charCanvas = document.createElement('canvas'); charCanvas.width = W; charCanvas.height = H;
			var maskCanvas = document.createElement('canvas'); maskCanvas.width = W; maskCanvas.height = H;

			function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
			function easeOut(v) { v = clamp(v,0,1); return 1 - Math.pow(1 - v, 3); }
			function easeIn(v) { v = clamp(v,0,1); return v * v * v; }
			function easeInOut(v) { v = clamp(v,0,1); return v < 0.5 ? 4*v*v*v : 1 - Math.pow(-2*v + 2, 3) / 2; }
			function easeBack(v) { v = clamp(v,0,1); var c1=1.70158, c3=c1+1; return 1 + c3*Math.pow(v-1,3) + c1*Math.pow(v-1,2); }
			function rnd(seed) { var x = Math.sin(seed * 129.898 + 78.233) * 43758.5453; return x - Math.floor(x); }
			function cubic(p0,p1,p2,p3,t) { var u=1-t; return {x:u*u*u*p0.x+3*u*u*t*p1.x+3*u*t*t*p2.x+t*t*t*p3.x, y:u*u*u*p0.y+3*u*u*t*p1.y+3*u*t*t*p2.y+t*t*t*p3.y}; }
			function pathCubic(p0,p1,p2,p3,n) { var a=[]; for(var i=0;i<=n;i++) a.push(cubic(p0,p1,p2,p3,i/n)); return a; }

			var splash = [];
			for (var si=0; si<76; si++) splash.push({
				a: rnd(si+10) * Math.PI * 2,
				d: (28 + rnd(si+20) * 158) * S,
				yscale: 0.55 + rnd(si+30) * 0.58,
				r: (1.2 + rnd(si+40) * 7.8) * S,
				w: rnd(si+50),
				seed: si * 7.13
			});
			var mountain1 = pathCubic({x:24*S,y:263*S},{x:86*S,y:205*S},{x:126*S,y:150*S},{x:171*S,y:213*S},34)
				.concat(pathCubic({x:171*S,y:213*S},{x:209*S,y:257*S},{x:235*S,y:142*S},{x:286*S,y:177*S},38))
				.concat(pathCubic({x:286*S,y:177*S},{x:329*S,y:207*S},{x:360*S,y:133*S},{x:407*S,y:158*S},34));
			var mountain2 = pathCubic({x:33*S,y:272*S},{x:111*S,y:230*S},{x:155*S,y:182*S},{x:211*S,y:224*S},36)
				.concat(pathCubic({x:211*S,y:224*S},{x:270*S,y:274*S},{x:314*S,y:171*S},{x:398*S,y:190*S},44));
			var clouds = [
				pathCubic({x:35*S,y:294*S},{x:104*S,y:273*S},{x:175*S,y:305*S},{x:254*S,y:282*S},60),
				pathCubic({x:85*S,y:318*S},{x:151*S,y:302*S},{x:208*S,y:334*S},{x:288*S,y:310*S},56),
				pathCubic({x:182*S,y:300*S},{x:255*S,y:276*S},{x:326*S,y:318*S},{x:405*S,y:292*S},64)
			];
			var slashAngle = -0.76;

			function irregularPaperPath(c, p, time) {
				p = clamp(p, 0, 1);
				var left = cx - (cx + 26*S) * p, right = cx + (W - cx + 26*S) * p;
				var top = cy - (cy + 22*S) * p, bottom = cy + (H - cy + 22*S) * p;
				var amp = (5 + 7 * (1-p)) * S;
				c.beginPath();
				var n = 22, i, x, y;
				for (i=0;i<=n;i++) { x = left + (right-left)*i/n; y = top + (rnd(100+i)*2-1)*amp + Math.sin(i*0.9+time*0.001)*amp*0.25; if(i===0)c.moveTo(x,y); else c.lineTo(x,y); }
				for (i=1;i<=n;i++) { y = top + (bottom-top)*i/n; x = right + (rnd(200+i)*2-1)*amp; c.lineTo(x,y); }
				for (i=1;i<=n;i++) { x = right - (right-left)*i/n; y = bottom + (rnd(300+i)*2-1)*amp; c.lineTo(x,y); }
				for (i=1;i<=n;i++) { y = bottom - (bottom-top)*i/n; x = left + (rnd(400+i)*2-1)*amp; c.lineTo(x,y); }
				c.closePath();
			}
			function paperNoise(c, alpha) {
				c.save(); c.globalAlpha = alpha;
				for (var i=0;i<520;i++) {
					var x = rnd(i+700)*W, y = rnd(i+900)*H;
					var dark = rnd(i+910) > 0.56;
					c.fillStyle = dark ? 'rgba(55,43,24,'+(0.018+rnd(i+920)*0.04)+')' : 'rgba(255,250,226,'+(0.022+rnd(i+930)*0.04)+')';
					c.fillRect(x,y,(0.7+rnd(i+940)*2.2)*S,(0.7+rnd(i+950)*1.7)*S);
				}
				for (var j=0;j<42;j++) {
					c.strokeStyle = 'rgba(94,70,34,'+(0.018+rnd(j+960)*0.035)+')'; c.lineWidth = (0.7+rnd(j+970)*1.3)*S;
					var yy = rnd(j+980)*H;
					c.beginPath(); c.moveTo(-20*S, yy);
					c.bezierCurveTo(W*0.35, yy+(rnd(j+981)-0.5)*24*S, W*0.70, yy+(rnd(j+982)-0.5)*28*S, W+20*S, yy+(rnd(j+983)-0.5)*22*S);
					c.stroke();
				}
				c.restore();
			}
			function drawPaper(c, t, forceFull) {
				c.fillStyle = 'rgb(0,0,0)'; c.fillRect(0,0,W,H);
				var p = forceFull ? 1 : easeOut((t - 2000) / 3000);
				if (p <= 0) return;
				c.save(); irregularPaperPath(c, p, t); c.clip();
				var g = c.createRadialGradient(cx,cy,0,cx,cy,Math.max(W,H)*0.72);
				g.addColorStop(0,'rgb(252,245,220)'); g.addColorStop(0.57,'rgb(239,228,195)'); g.addColorStop(1,'rgb(205,188,143)');
				c.fillStyle = g; c.fillRect(0,0,W,H);
				paperNoise(c, forceFull ? 0.96 : easeOut((t - 4400) / 650));
				c.restore();
			}
			function drawInkBlob(c, x, y, rx, ry, alpha, seed) {
				c.save();
				var g = c.createRadialGradient(x,y,0,x,y,Math.max(rx,ry));
				g.addColorStop(0,'rgba(0,0,0,'+(alpha*0.98)+')'); g.addColorStop(0.55,'rgba(0,0,0,'+(alpha*0.55)+')'); g.addColorStop(0.88,'rgba(0,0,0,'+(alpha*0.18)+')'); g.addColorStop(1,'rgba(0,0,0,0)');
				c.fillStyle = g; c.beginPath();
				var n=34;
				for(var k=0;k<=n;k++) { var a=k/n*Math.PI*2; var wob=1+Math.sin(a*5+seed)*0.10+Math.sin(a*12-seed*.71)*0.055; var px=x+Math.cos(a)*rx*wob, py=y+Math.sin(a)*ry*wob; if(k===0)c.moveTo(px,py); else c.lineTo(px,py); }
				c.closePath(); c.fill(); c.restore();
			}
			function drawSplash(c, t) {
				if (t < 6000) return;
				var fall = clamp((t - 6000) / 1000, 0, 1);
				var y = -34*S + (cy - 6*S + 34*S) * easeIn(fall);
				var stretch = 1 + 0.75 * easeIn(fall);
				if (t < 7000) { drawInkBlob(c, cx, y, 9*S/stretch, 14*S*stretch, 0.98, t*0.004); return; }
				var hold = t < 7085 ? 0 : clamp((t - 7085) / 915, 0, 1);
				drawInkBlob(c, cx, cy+3*S, (20 + 11*hold)*S, (11 + 4*hold)*S, 0.98, 3.5);
				for (var i=0;i<splash.length;i++) {
					var p = easeOut(hold), sp = splash[i];
					var px = cx + Math.cos(sp.a) * sp.d * p;
					var py = cy + 3*S + Math.sin(sp.a) * sp.d * sp.yscale * p;
					var r = sp.r * (1 - 0.18 * p);
					drawInkBlob(c, px, py, r*(1.05+sp.w*.7), r*(0.65+sp.w*.55), 0.72 + sp.w * 0.25, sp.seed);
				}
			}
			function brushStroke(c, pts, width, alpha, progress, seed, pale) {
				progress = clamp(progress,0,1); if (progress <= 0) return;
				var max = Math.max(2, Math.floor((pts.length-1)*progress));
				c.save(); c.lineCap='round'; c.lineJoin='round'; c.strokeStyle = pale ? 'rgba(0,0,0,'+alpha+')' : 'rgba(0,0,0,'+alpha+')'; c.lineWidth = width;
				c.beginPath(); c.moveTo(pts[0].x, pts[0].y); for(var i=1;i<=max && i<pts.length;i++) c.lineTo(pts[i].x, pts[i].y); c.stroke();
				// 飞白：在墨线上刮出几道纸色裂隙。
				if (!pale && progress > 0.24) {
					c.globalCompositeOperation = 'destination-out'; c.strokeStyle='rgba(255,255,255,0.35)'; c.lineWidth = Math.max(1.2*S, width*0.12);
					for (var k=0;k<14;k++) {
						var idx = Math.floor((3 + rnd(seed+k)*Math.max(4,max-4))); if (idx >= max) continue;
						var pt = pts[idx], len=(7+rnd(seed+k+90)*24)*S;
						c.beginPath(); c.moveTo(pt.x-len*0.5, pt.y+(rnd(seed+k+91)-0.5)*width*0.55); c.lineTo(pt.x+len*0.5, pt.y+(rnd(seed+k+92)-0.5)*width*0.55); c.stroke();
					}
				}
				c.restore();
			}
			function mountainProgress(t) {
				if (t < 9000) return 0;
				if (t < 10000) return 0.18 * easeInOut((t-9000)/1000);
				if (t < 12000) return 0.18 + 0.67 * easeOut((t-10000)/2000);
				return 0.85 + 0.15 * easeInOut((t-12000)/1000);
			}
			function drawMountain(c, t) {
				var p = mountainProgress(t);
				brushStroke(c, mountain1, 8.5*S, 0.68, p, 10, false);
				brushStroke(c, mountain2, 4.5*S, 0.25, p, 60, true);
				var cloudStart = [13000, 13650, 14300];
				for (var i=0;i<3;i++) {
					var q = easeOut((t - cloudStart[i]) / 700);
					if (q <= 0) continue;
					var a = [0.18,0.09,0.055][i];
					brushStroke(c, clouds[i], (13 - i*2.2)*S, a, q, 120+i, true);
					brushStroke(c, clouds[i], (3.2 - i*0.45)*S, a*1.8, q, 180+i, true);
				}
			}
			function drawInkMass(c, t) {
				if (t < 15000) return;
				var gather = clamp((t - 15000) / 2000, 0, 1);
				var human = clamp((t - 17000) / 2000, 0, 1);
				var p = Math.max(gather, human);
				var baseY = cy + 45*S;
				c.save(); c.translate(cx, baseY);
				if (gather > 0 && human < 1) {
					c.globalAlpha = 1 - human*0.55;
					for (var i=0;i<9;i++) {
						var a = rnd(i+300)*Math.PI*2 + t*0.0007*(i%2?1:-1);
						var rr = (22 + rnd(i+310)*58) * S * (0.45 + gather*0.9 - human*0.28);
						var x = Math.cos(a) * rr * 0.36, y = Math.sin(a) * rr * 0.22;
						drawInkBlob(c, x, y, rr*(0.72+rnd(i+320)*0.38), rr*(0.42+rnd(i+330)*0.35), (0.18 + gather*0.17), i + t*0.002);
					}
				}
				if (human > 0) {
					var hp = easeOut(human);
					var lift = easeInOut((t - 19000) / 1000);
					c.translate(0, -5*S*hp);
					c.globalAlpha = hp;
					if (c.filter !== undefined) c.filter = 'blur(' + (5*(1-hp)).toFixed(2) + 'px)';
					c.fillStyle='rgba(0,0,0,0.95)';
					// 背对观众的人形剪影。
					c.beginPath(); c.arc(0, -55*S, 13*S, 0, Math.PI*2); c.fill();
					c.beginPath();
					c.moveTo(-16*S, -38*S); c.bezierCurveTo(-30*S, -4*S, -28*S, 39*S, -14*S, 72*S);
					c.quadraticCurveTo(0, (82 + Math.sin(t*0.009)*3)*S, 15*S, 72*S);
					c.bezierCurveTo(29*S, 36*S, 28*S, -4*S, 16*S, -38*S); c.quadraticCurveTo(0, -28*S, -16*S, -38*S); c.fill();
					// 衣袂墨随风动。
					c.beginPath(); c.moveTo(-10*S, 40*S); c.quadraticCurveTo((-29 + Math.sin(t*0.008)*5)*S, 63*S, (-22 + Math.sin(t*0.007)*7)*S, 88*S); c.lineTo(-2*S, 73*S); c.fill();
					c.beginPath(); c.moveTo(12*S, 42*S); c.quadraticCurveTo((32 + Math.sin(t*0.007+1)*6)*S, 65*S, (24 + Math.sin(t*0.009+2)*5)*S, 87*S); c.lineTo(2*S, 74*S); c.fill();
					c.filter = 'none';
					c.lineCap='round'; c.lineJoin='round'; c.strokeStyle='rgba(0,0,0,0.96)'; c.lineWidth=8*S;
					c.beginPath(); c.moveTo(-12*S,-26*S); c.lineTo(-38*S,9*S); c.stroke();
					c.beginPath(); c.moveTo(13*S,-26*S); c.lineTo((31-18*lift)*S, (-2-48*lift)*S); c.lineTo((42-13*lift)*S, (17-95*lift)*S); c.stroke();
					if (lift > 0.05) { c.strokeStyle='rgba(0,0,0,'+(0.88*lift)+')'; c.lineWidth=4*S; c.beginPath(); c.moveTo((36-9*lift)*S, (-72-14*lift)*S); c.lineTo((62+32*lift)*S, (-121-28*lift)*S); c.stroke(); }
				}
				c.restore();
			}
			function drawSword(c, t) {
				if (t < 20000) return;
				var p = clamp((t - 20000) / 1000, 0, 1), ep = easeOut(p);
				var x1 = cx + 20*S, y1 = cy - 70*S;
				var x2 = cx + (55 + 245*ep)*S, y2 = cy - (105 + 220*ep)*S;
				c.save(); c.globalCompositeOperation='lighter'; c.lineCap='round';
				c.strokeStyle='rgba(255,255,255,'+(0.98*(1-p*0.08))+')'; c.lineWidth=(11+28*Math.sin(p*Math.PI))*S; c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
				c.globalCompositeOperation='source-over'; c.strokeStyle='rgba(0,0,0,0.92)'; c.lineWidth=(2.5+7*Math.sin(p*Math.PI))*S; c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
				c.globalCompositeOperation='lighter';
				for (var i=0;i<86;i++) { var u=rnd(i+500)*ep, off=(rnd(i+510)-0.5)*84*S*ep; var px=x1+(x2-x1)*u+Math.cos(slashAngle+Math.PI/2)*off; var py=y1+(y2-y1)*u+Math.sin(slashAngle+Math.PI/2)*off; c.fillStyle='rgba(255,255,255,'+(0.22+rnd(i+520)*0.55)+')'; c.beginPath(); c.arc(px,py,(1+rnd(i+530)*3.7)*S,0,Math.PI*2); c.fill(); }
				c.restore();
			}
			function drawScene(c, t, fullPaper) {
				c.clearRect(0,0,W,H);
				drawPaper(c, t, fullPaper);
				drawSplash(c, t);
				drawMountain(c, t);
				drawInkMass(c, t);
				drawSword(c, t);
			}
			function drawSplit(c, t) {
				var p = easeInOut((t - 21000) / 2000);
				sctx.clearRect(0,0,W,H); drawScene(sctx, 21000, true);
				c.clearRect(0,0,W,H); c.fillStyle='rgb(0,0,0)'; c.fillRect(0,0,W,H);
				var dist = p * 310 * S;
				function half(sign) {
					c.save(); c.translate(cx,cy); c.rotate(slashAngle + sign*0.08*p);
					c.beginPath(); if (sign < 0) c.rect(-W*2, -H*2, W*4, H*2); else c.rect(-W*2, 0, W*4, H*2); c.clip();
					c.rotate(-(slashAngle + sign*0.08*p)); c.translate(-cx,-cy);
					var nx = -Math.sin(slashAngle), ny = Math.cos(slashAngle);
					c.globalAlpha = 1 - p*0.16;
					c.drawImage(scene, nx*dist*sign, ny*dist*sign + Math.sin(p*Math.PI+sign)*13*S);
					c.restore();
				}
				half(-1); half(1);
				c.save(); c.translate(cx,cy); c.rotate(slashAngle); c.globalCompositeOperation='lighter'; c.strokeStyle='rgba(255,255,255,'+(0.92*(1-p*0.1))+')'; c.lineWidth=(14*(1-p)+2)*S; c.beginPath(); c.moveTo(-W,0); c.lineTo(W,0); c.stroke(); c.restore();
			}
			var strokeDefs = {
				'龙': [[-.38,-.38,.15,-.38],[.02,-.55,-.15,.12],[-.32,-.05,.22,-.05],[-.18,.08,-.36,.44],[-.02,.05,.18,.44],[.08,.18,.36,.02],[.19,.28,.43,.48]],
				'魂': [[-.42,-.35,-.02,-.35],[-.30,-.50,-.30,.36],[-.47,-.02,-.05,-.02],[-.14,-.46,.35,-.46],[.12,-.54,.03,.43],[.04,.01,.42,.01],[.20,.12,.42,.38]],
				'王': [[-.40,-.38,.40,-.38],[-.32,0,.32,0],[-.44,.42,.44,.42],[0,-.48,0,.48]]
			};
			function drawOneRedChar(c, ch, x, y, size, p) {
				p = clamp(p,0,1); if (p <= 0) return;
				var cc = charCanvas.getContext('2d'), mc = maskCanvas.getContext('2d');
				cc.clearRect(0,0,W,H); mc.clearRect(0,0,W,H);
				cc.save(); cc.textAlign='center'; cc.textBaseline='middle'; cc.font='bold '+Math.round(size)+'px STXingkai, KaiTi, STKaiti, SimSun, serif'; cc.shadowColor='rgba(190,0,0,0.65)'; cc.shadowBlur=12*S; cc.lineWidth=5*S; cc.strokeStyle='rgba(65,0,0,0.95)'; cc.fillStyle='rgba(185,0,0,0.98)'; cc.strokeText(ch,x,y); cc.fillText(ch,x,y); cc.restore();
				var strokes = strokeDefs[ch] || strokeDefs['王']; var each = 1 / strokes.length;
				mc.save(); mc.strokeStyle='white'; mc.lineCap='round'; mc.lineJoin='round'; mc.lineWidth=size*0.24;
				for (var i=0;i<strokes.length;i++) {
					var local = clamp((p - i*each) / each, 0, 1); if (local <= 0) continue;
					var st=strokes[i], x1=x+st[0]*size, y1=y+st[1]*size, x2=x+st[2]*size, y2=y+st[3]*size;
					mc.beginPath(); mc.moveTo(x1,y1); mc.lineTo(x1+(x2-x1)*easeOut(local), y1+(y2-y1)*easeOut(local)); mc.stroke();
				}
				mc.restore(); cc.globalCompositeOperation='destination-in'; cc.drawImage(maskCanvas,0,0); cc.globalCompositeOperation='source-over'; c.drawImage(charCanvas,0,0);
			}
			function drawRedTitle(c, t) {
				c.clearRect(0,0,W,H); c.fillStyle='rgb(0,0,0)'; c.fillRect(0,0,W,H);
				var fade = 1 - clamp((t - 29000) / 1000, 0, 1);
				c.save(); c.globalAlpha = fade;
				var chars = ['龙','魂','王']; var size = Math.max(58*S, 72*S); var spacing = 78*S;
				var allP = clamp((t - 24000) / 4000, 0, 1);
				for (var i=0;i<chars.length;i++) {
					var local = clamp((allP - i*0.22) / 0.58, 0, 1);
					drawOneRedChar(c, chars[i], cx + (i-1)*spacing, cy - 18*S, size, local);
				}
				if (t >= 28000) {
					var sp = clamp((t - 28000) / 1000, 0, 1);
					var scale = sp < 0.45 ? (1.85 - 0.95*easeOut(sp/0.45)) : (0.92 + 0.08*easeOut((sp-0.45)/0.55));
					c.save(); c.translate(cx, cy + 86*S); c.scale(scale, scale); c.rotate(-0.035*Math.sin(sp*Math.PI));
					c.shadowColor='rgba(210,0,0,0.8)'; c.shadowBlur=14*S; c.fillStyle='rgba(165,0,0,0.98)'; c.fillRect(-27*S,-27*S,54*S,54*S);
					c.strokeStyle='rgba(255,120,120,0.36)'; c.lineWidth=3*S; c.strokeRect(-22*S,-22*S,44*S,44*S);
					c.fillStyle='rgba(35,0,0,0.55)'; c.font='bold '+Math.round(28*S)+'px SimSun, serif'; c.textAlign='center'; c.textBaseline='middle'; c.fillText('印',0,2*S);
					c.restore();
				}
				if (t >= 29000) { c.fillStyle='rgba(185,0,0,'+(fade*0.95)+')'; c.beginPath(); c.arc(cx, cy + 86*S, Math.max(1.2*S, 4*S*fade), 0, Math.PI*2); c.fill(); }
				c.restore();
			}
			function sound(kind) {
				try {
					var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
					var ac = core.plugin.__mt4InkAudioContext || new AC(); core.plugin.__mt4InkAudioContext = ac; if (ac.state == 'suspended' && ac.resume) ac.resume();
					var gain = ac.createGain(); gain.connect(ac.destination);
					if (kind == 'rustle' || kind == 'brush' || kind == 'tear') {
						var dur = kind == 'brush' ? 1.2 : (kind == 'tear' ? 0.55 : 0.75), len = Math.floor(ac.sampleRate * dur), buf = ac.createBuffer(1, len, ac.sampleRate), data = buf.getChannelData(0);
						for (var i=0;i<len;i++) data[i] = (Math.random()*2-1) * Math.pow(1-i/len, kind=='brush'?0.35:0.8);
						var src=ac.createBufferSource(); src.buffer=buf; var f=ac.createBiquadFilter(); f.type=kind=='tear'?'bandpass':'highpass'; f.frequency.value=kind=='brush'?650:1200; gain.gain.value=kind=='brush'?0.050:0.060; src.connect(f); f.connect(gain); src.start(); return;
					}
					var osc = ac.createOscillator(); osc.type = kind=='hum'?'sawtooth':'triangle'; gain.gain.value = kind=='impact'?0.09:(kind=='sword'?0.075:(kind=='hum'?0.035:0.045));
					osc.frequency.value = kind=='drop'?900:(kind=='impact'?110:(kind=='sword'?1180:(kind=='hum'?82:520)));
					osc.connect(gain); osc.start(); gain.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + (kind=='hum'?1.6:0.28)); osc.stop(ac.currentTime + (kind=='hum'?1.65:0.3));
				} catch (e) {}
			}
			var played = {};
			function cue(t) {
				if (t > 2000 && !played.rustle) { played.rustle = true; sound('rustle'); }
				if (t > 6000 && !played.drop) { played.drop = true; sound('drop'); }
				if (t > 7000 && !played.impact) { played.impact = true; sound('impact'); }
				if (t > 9000 && !played.brush) { played.brush = true; sound('brush'); }
				if (t > 15000 && !played.hum) { played.hum = true; sound('hum'); }
				if (t > 20000 && !played.sword) { played.sword = true; sound('sword'); }
				if (t > 21000 && !played.tear) { played.tear = true; sound('tear'); }
				if (t > 28000 && !played.stamp) { played.stamp = true; sound('impact'); }
			}
			function frame() {
				var t = nowFunc() - startTime;
				cue(t);
				try { if (core.status && core.status.floorId !== 'MT4') throw new Error('floor changed'); } catch (ee) { try { core.deleteCanvas('mt4BossP5InkFx'); } catch (e0) {} if (callback) callback(); return; }
				if (t < 23000) drawScene(ctx, t, false);
				else if (t < 24000) { ctx.clearRect(0,0,W,H); ctx.fillStyle='rgb(0,0,0)'; ctx.fillRect(0,0,W,H); }
				else drawRedTitle(ctx, t);
				if (t >= 21000 && t < 23000) drawSplit(ctx, t);
				if (t < total) requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP5InkFx'); } catch (e) {} if (callback) callback(); }
			}
			requestAnimationFrame(frame);
		};

		this.playMT4BossP6ToP7InkSequence = function (callback) {
			// P6 -> P7 胜利感水墨收束CG：黑色入场的反面，最后定格纯白。
			// 录像回放跳过动画，只同步最终阶段状态，避免30秒异步演出影响路线。
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var S = Math.min(W, H) / 416;
			var cx = W / 2, cy = H / 2;
			var total = 30000;
			var nowFunc = (window.performance && performance.now) ? function(){ return performance.now(); } : function(){ return Date.now(); };
			var startTime = nowFunc();
			try { core.deleteCanvas('mt4BossP6P7InkFx'); } catch (e0) {}
			var ctx = core.createCanvas('mt4BossP6P7InkFx', 0, 0, W, H, 208);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			ctx.canvas.style.filter = 'none';
			var tmp = document.createElement('canvas'); tmp.width = W; tmp.height = H;
			var tctx = tmp.getContext('2d');
			var charCanvas = document.createElement('canvas'); charCanvas.width = W; charCanvas.height = H;
			var maskCanvas = document.createElement('canvas'); maskCanvas.width = W; maskCanvas.height = H;

			function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
			function easeOut(v){ v=clamp(v,0,1); return 1 - Math.pow(1-v,3); }
			function easeIn(v){ v=clamp(v,0,1); return v*v*v; }
			function easeInOut(v){ v=clamp(v,0,1); return v < 0.5 ? 4*v*v*v : 1 - Math.pow(-2*v+2,3)/2; }
			function rnd(seed){ var x = Math.sin(seed * 127.119 + 61.71) * 43758.5453; return x - Math.floor(x); }
			function cubic(p0,p1,p2,p3,t){ var u=1-t; return { x:u*u*u*p0.x+3*u*u*t*p1.x+3*u*t*t*p2.x+t*t*t*p3.x, y:u*u*u*p0.y+3*u*u*t*p1.y+3*u*t*t*p2.y+t*t*t*p3.y }; }
			function pathCubic(p0,p1,p2,p3,n){ var arr=[]; for(var i=0;i<=n;i++) arr.push(cubic(p0,p1,p2,p3,i/n)); return arr; }

			var bossX = 6 * 32 + 16, bossY = 9 * 32 + 16;
			var particles = [];
			for (var pi=0; pi<190; pi++) {
				var a = rnd(pi+10) * Math.PI * 2;
				var spd = (18 + rnd(pi+20) * 170) * S;
				particles.push({
					a: a,
					d: spd,
					r: (0.9 + rnd(pi+30) * 3.8) * S,
					drift: (rnd(pi+40) - 0.5) * 54 * S,
					seed: pi * 4.7
				});
			}

			var mountain1 = pathCubic({x:24*S,y:263*S},{x:86*S,y:205*S},{x:126*S,y:150*S},{x:171*S,y:213*S},34)
				.concat(pathCubic({x:171*S,y:213*S},{x:209*S,y:257*S},{x:235*S,y:142*S},{x:286*S,y:177*S},38))
				.concat(pathCubic({x:286*S,y:177*S},{x:329*S,y:207*S},{x:360*S,y:133*S},{x:407*S,y:158*S},34));
			var mountain2 = pathCubic({x:33*S,y:272*S},{x:111*S,y:230*S},{x:155*S,y:182*S},{x:211*S,y:224*S},36)
				.concat(pathCubic({x:211*S,y:224*S},{x:270*S,y:274*S},{x:314*S,y:171*S},{x:398*S,y:190*S},44));
			var clouds = [
				pathCubic({x:35*S,y:294*S},{x:104*S,y:273*S},{x:175*S,y:305*S},{x:254*S,y:282*S},60),
				pathCubic({x:85*S,y:318*S},{x:151*S,y:302*S},{x:208*S,y:334*S},{x:288*S,y:310*S},56),
				pathCubic({x:182*S,y:300*S},{x:255*S,y:276*S},{x:326*S,y:318*S},{x:405*S,y:292*S},64)
			];
			var birdPath = [
				{x:W-98*S,y:H-130*S},{x:W-122*S,y:H-142*S},{x:W-151*S,y:H-128*S},
				{x:W-181*S,y:H-145*S},{x:W-210*S,y:H-136*S},{x:W-244*S,y:H-151*S}
			];

			function irregularPaperPath(c, p, time) {
				p = clamp(p, 0, 1);
				var left = cx - (cx + 26*S) * p, right = cx + (W - cx + 26*S) * p;
				var top = cy - (cy + 22*S) * p, bottom = cy + (H - cy + 22*S) * p;
				var amp = (5 + 8 * (1-p)) * S;
				c.beginPath();
				var n = 22, i, x, y;
				for (i=0;i<=n;i++) { x = left + (right-left)*i/n; y = top + (rnd(100+i)*2-1)*amp + Math.sin(i*0.9+time*0.001)*amp*0.18; if(i===0)c.moveTo(x,y); else c.lineTo(x,y); }
				for (i=1;i<=n;i++) { y = top + (bottom-top)*i/n; x = right + (rnd(200+i)*2-1)*amp; c.lineTo(x,y); }
				for (i=1;i<=n;i++) { x = right - (right-left)*i/n; y = bottom + (rnd(300+i)*2-1)*amp; c.lineTo(x,y); }
				for (i=1;i<=n;i++) { y = bottom - (bottom-top)*i/n; x = left + (rnd(400+i)*2-1)*amp; c.lineTo(x,y); }
				c.closePath();
			}
			function paperNoise(c, alpha) {
				c.save(); c.globalAlpha = alpha;
				for (var i=0;i<470;i++) {
					var x = rnd(i+700)*W, y = rnd(i+900)*H;
					var dark = rnd(i+910) > 0.56;
					c.fillStyle = dark ? 'rgba(55,43,24,'+(0.014+rnd(i+920)*0.032)+')' : 'rgba(255,250,226,'+(0.018+rnd(i+930)*0.038)+')';
					c.fillRect(x,y,(0.7+rnd(i+940)*2.2)*S,(0.7+rnd(i+950)*1.7)*S);
				}
				for (var j=0;j<34;j++) {
					c.strokeStyle = 'rgba(94,70,34,'+(0.014+rnd(j+960)*0.028)+')'; c.lineWidth = (0.6+rnd(j+970)*1.2)*S;
					var yy = rnd(j+980)*H;
					c.beginPath(); c.moveTo(-20*S, yy);
					c.bezierCurveTo(W*0.35, yy+(rnd(j+981)-0.5)*22*S, W*0.70, yy+(rnd(j+982)-0.5)*24*S, W+20*S, yy+(rnd(j+983)-0.5)*20*S);
					c.stroke();
				}
				c.restore();
			}
			function drawFullPaper(c, time, alpha) {
				c.save();
				c.globalAlpha = alpha == null ? 1 : alpha;
				irregularPaperPath(c, 1, time); c.clip();
				var g = c.createRadialGradient(cx,cy,0,cx,cy,Math.max(W,H)*0.72);
				g.addColorStop(0,'rgb(254,249,226)'); g.addColorStop(0.59,'rgb(239,229,199)'); g.addColorStop(1,'rgb(210,193,150)');
				c.fillStyle = g; c.fillRect(0,0,W,H);
				paperNoise(c, 0.9);
				c.restore();
			}
			function drawBornPaper(c, t) {
				c.fillStyle = 'rgb(0,0,0)'; c.fillRect(0,0,W,H);
				var p = clamp((t - 5000) / 2000, 0, 1);
				var r = (2 + easeOut(p) * Math.max(W,H) * 0.92) * S;
				var g = c.createRadialGradient(cx, cy, 0, cx, cy, r);
				g.addColorStop(0, 'rgba(255,255,245,1)');
				g.addColorStop(0.42, 'rgba(255,250,220,' + (0.85 + 0.15*p).toFixed(3) + ')');
				g.addColorStop(1, 'rgba(255,255,255,0)');
				c.fillStyle = g; c.beginPath(); c.arc(cx, cy, r, 0, Math.PI*2); c.fill();
				if (p > 0.46) {
					var pp = easeOut((p - 0.46) / 0.54);
					drawFullPaper(c, t, pp);
				}
			}
			function drawInkBlob(c, x, y, rx, ry, alpha, seed) {
				c.save();
				var g = c.createRadialGradient(x,y,0,x,y,Math.max(rx,ry));
				g.addColorStop(0,'rgba(0,0,0,'+(alpha*0.98)+')'); g.addColorStop(0.58,'rgba(0,0,0,'+(alpha*0.48)+')'); g.addColorStop(0.9,'rgba(0,0,0,'+(alpha*0.16)+')'); g.addColorStop(1,'rgba(0,0,0,0)');
				c.fillStyle = g; c.beginPath();
				var n=30;
				for(var k=0;k<=n;k++) { var a=k/n*Math.PI*2; var wob=1+Math.sin(a*5+seed)*0.11+Math.sin(a*11-seed*.67)*0.055; var px=x+Math.cos(a)*rx*wob, py=y+Math.sin(a)*ry*wob; if(k===0)c.moveTo(px,py); else c.lineTo(px,py); }
				c.closePath(); c.fill(); c.restore();
			}
			function drawBossSilhouette(c, x, y, alpha, scale, broken) {
				c.save(); c.translate(x,y); c.scale(scale || 1, scale || 1); c.globalAlpha = alpha == null ? 1 : alpha;
				c.fillStyle='rgba(0,0,0,0.92)';
				c.beginPath(); c.arc(0, -25*S, 15*S, 0, Math.PI*2); c.fill();
				c.beginPath(); c.moveTo(-22*S,-8*S); c.bezierCurveTo(-40*S,20*S,-34*S,58*S,-12*S,72*S); c.quadraticCurveTo(0,82*S,14*S,72*S); c.bezierCurveTo(35*S,56*S,39*S,18*S,21*S,-8*S); c.quadraticCurveTo(0,-2*S,-22*S,-8*S); c.fill();
				c.strokeStyle='rgba(0,0,0,0.94)'; c.lineCap='round'; c.lineJoin='round'; c.lineWidth=8*S;
				c.beginPath(); c.moveTo(-16*S,0); c.lineTo(-40*S,27*S); c.stroke();
				c.beginPath(); c.moveTo(17*S,0); c.lineTo(42*S,24*S); c.stroke();
				if (broken) {
					c.globalCompositeOperation='destination-out'; c.strokeStyle='rgba(255,255,255,0.75)'; c.lineWidth=2.2*S;
					for(var i=0;i<8;i++){ var xx=(-19+rnd(i+1)*38)*S; c.beginPath(); c.moveTo(xx,(-37+rnd(i+3)*28)*S); c.lineTo(xx+(rnd(i+8)-0.5)*18*S,(8+rnd(i+9)*55)*S); c.stroke(); }
				}
				c.restore();
			}
			function brushStroke(c, pts, width, alpha, progress, seed) {
				progress = clamp(progress,0,1); if (progress <= 0) return;
				var max = Math.max(2, Math.floor((pts.length-1)*progress));
				c.save(); c.lineCap='round'; c.lineJoin='round'; c.strokeStyle='rgba(0,0,0,'+alpha+')'; c.lineWidth = width;
				c.beginPath(); c.moveTo(pts[0].x, pts[0].y); for(var i=1;i<=max && i<pts.length;i++) c.lineTo(pts[i].x, pts[i].y); c.stroke();
				if (progress > 0.20) {
					c.globalCompositeOperation='destination-out'; c.strokeStyle='rgba(255,255,255,0.28)'; c.lineWidth=Math.max(1*S,width*0.10);
					for (var k=0;k<8;k++) { var idx=Math.floor(2+rnd(seed+k)*Math.max(3,max-3)); if (idx>=max) continue; var pt=pts[idx], len=(5+rnd(seed+k+90)*20)*S; c.beginPath(); c.moveTo(pt.x-len*0.5, pt.y+(rnd(seed+k+91)-0.5)*width*0.45); c.lineTo(pt.x+len*0.5, pt.y+(rnd(seed+k+92)-0.5)*width*0.45); c.stroke(); }
				}
				c.restore();
			}
			function drawOldInkWorld(c, time, amount) {
				amount = clamp(amount, 0, 1);
				// amount=1代表旧墨迹完整；amount=0代表被纸吸收。
				var mnt = clamp((amount - 0.03) / 0.97, 0, 1);
				var cloud = clamp((amount - 0.22) / 0.78, 0, 1);
				var mass = clamp((amount - 0.46) / 0.54, 0, 1);
				var human = clamp((amount - 0.68) / 0.32, 0, 1);
				brushStroke(c, mountain1, 8.5*S, 0.60*mnt, mnt, 10);
				brushStroke(c, mountain2, 4.5*S, 0.22*mnt, mnt, 60);
				for (var i=0;i<3;i++) if (cloud>0) brushStroke(c, clouds[i], (13-i*2.2)*S, [0.17,0.09,0.055][i]*cloud, cloud, 120+i);
				if (mass>0) {
					for (var j=0;j<8;j++) {
						var a = rnd(j+300)*Math.PI*2 + time*0.00055*(j%2?1:-1);
						var rr = (20 + rnd(j+310)*52) * S * (0.7 + mass*0.35);
						drawInkBlob(c, cx + Math.cos(a)*rr*0.28, cy+45*S + Math.sin(a)*rr*0.18, rr*(0.68+rnd(j+320)*0.32), rr*(0.38+rnd(j+330)*0.3), (0.10+0.22*mass)*mass, j+time*0.002);
					}
				}
				if (human>0) drawBossSilhouette(c, cx, cy+45*S, 0.88*human, 1.0, false);
			}
			function drawBamboo(c, t) {
				var p = clamp((t - 11000) / 2000, 0, 1);
				if (p <= 0) return;
				var x = W - 74*S, y = H - 72*S;
				c.save(); c.strokeStyle='rgba(0,0,0,0.72)'; c.lineCap='round'; c.lineJoin='round';
				var stemP = clamp(p/0.48,0,1);
				c.lineWidth=4*S; c.beginPath(); c.moveTo(x, y); c.bezierCurveTo(x-8*S, y-35*S*easeOut(stemP), x+2*S, y-78*S*easeOut(stemP), x-10*S, y-122*S*easeOut(stemP)); c.stroke();
				for (var i=0;i<5;i++) {
					var lp = clamp((p - (0.28+i*0.10)) / 0.22,0,1); if(lp<=0) continue;
					var by = y - (40 + i*17)*S, bx = x - (i%2?4:-9)*S;
					var dir = (i%2?1:-1);
					c.lineWidth=(3-i*0.18)*S; c.beginPath(); c.moveTo(bx,by); c.quadraticCurveTo(bx + dir*(18+4*i)*S*easeOut(lp), by - (8+i)*S*easeOut(lp), bx + dir*(35+4*i)*S*easeOut(lp), by - (14+i*2)*S*easeOut(lp)); c.stroke();
					c.fillStyle='rgba(0,0,0,'+(0.50*lp)+')'; c.beginPath(); c.ellipse(bx+dir*(29+4*i)*S*easeOut(lp), by-(12+i*2)*S*easeOut(lp), (11-i*0.6)*S, (3.4-i*0.15)*S, -dir*0.45, 0, Math.PI*2); c.fill();
				}
				c.restore();
			}
			function drawTinyBird(c, x, y, p, flap) {
				p = p == null ? 1 : p; if(p<=0) return;
				c.save(); c.translate(x,y); c.globalAlpha = p; c.fillStyle='rgba(0,0,0,0.82)';
				c.beginPath(); c.ellipse(0, 0, 8*S, 5*S, -0.12, 0, Math.PI*2); c.fill();
				c.beginPath(); c.moveTo(-7*S, 1*S); c.lineTo(-17*S, -4*S); c.lineTo(-10*S, 5*S); c.closePath(); c.fill();
				c.beginPath(); c.arc(7*S, -2*S, 2.2*S, 0, Math.PI*2); c.fill();
				c.strokeStyle='rgba(0,0,0,0.76)'; c.lineWidth=2*S; c.lineCap='round';
				var wing = Math.sin(flap || 0) * 8*S;
				c.beginPath(); c.moveTo(1*S, -2*S); c.quadraticCurveTo(7*S, -14*S-wing, 17*S, -8*S-wing*0.35); c.stroke();
				c.beginPath(); c.moveTo(1*S, 2*S); c.quadraticCurveTo(7*S, 12*S+wing, 15*S, 7*S+wing*0.25); c.stroke();
				c.restore();
			}
			function drawTinyHuman(c, x, y, scale, alpha, walk) {
				scale = scale || 1; alpha = alpha == null ? 1 : alpha; if(alpha<=0) return;
				c.save(); c.translate(x,y); c.scale(scale,scale); c.globalAlpha=alpha; c.fillStyle='rgba(0,0,0,0.84)';
				c.beginPath(); c.arc(0,-12*S,3*S,0,Math.PI*2); c.fill();
				c.fillRect(-2.2*S,-9*S,4.4*S,12*S);
				c.strokeStyle='rgba(0,0,0,0.84)'; c.lineWidth=1.4*S; c.lineCap='round';
				var w = Math.sin((walk||0))*2.2*S;
				c.beginPath(); c.moveTo(0,3*S); c.lineTo(-4*S+w,11*S); c.stroke();
				c.beginPath(); c.moveTo(0,3*S); c.lineTo(4*S-w,11*S); c.stroke();
				c.restore();
			}
			function warmHorizon(c, p) {
				if (p <= 0) return;
				var g = c.createLinearGradient(0,H*0.68,0,H*0.20);
				g.addColorStop(0,'rgba(255,108,48,'+(0.20*p).toFixed(3)+')');
				g.addColorStop(0.35,'rgba(255,164,82,'+(0.12*p).toFixed(3)+')');
				g.addColorStop(1,'rgba(255,224,180,0)');
				c.fillStyle=g; c.fillRect(0,0,W,H);
			}
			function drawSun(c, t) {
				var p = clamp((t - 23000) / 3000, 0, 1); if(p<=0) return;
				var y = H*0.74 - easeOut(p) * 126*S;
				c.save(); c.globalCompositeOperation='source-over';
				var rg = c.createRadialGradient(cx, y, 0, cx, y, 78*S);
				rg.addColorStop(0,'rgba(196,24,18,0.95)'); rg.addColorStop(0.34,'rgba(204,40,25,0.72)'); rg.addColorStop(1,'rgba(255,155,83,0)');
				c.fillStyle=rg; c.beginPath(); c.arc(cx, y, 58*S, 0, Math.PI*2); c.fill();
				c.fillStyle='rgba(185,28,22,0.94)'; c.beginPath(); c.arc(cx, y, 31*S, 0, Math.PI*2); c.fill();
				c.restore();
			}
			var calligraphyStrokes = {
				'卷': [[-.42,-.42,.36,-.42],[-.32,-.24,.42,-.24],[-.22,-.54,-.05,-.28],[.16,-.55,.03,-.28],[-.34,-.04,.34,-.04],[-.28,.12,.24,.12],[-.24,.12,-.34,.42],[-.34,.42,.28,.42],[.28,.42,.37,.18]],
				'终': [[-.42,-.38,-.20,-.15],[-.20,-.15,-.43,.16],[-.43,.16,-.14,.05],[-.08,-.38,.37,-.38],[.11,-.50,.00,.00],[.00,.00,.39,.00],[.14,.10,.36,.36],[-.04,.34,.30,.50]]
			};
			function drawBrushChar(c, ch, x, y, size, p) {
				p = clamp(p,0,1); if(p<=0) return;
				var cc=charCanvas.getContext('2d'), mc=maskCanvas.getContext('2d');
				cc.clearRect(0,0,W,H); mc.clearRect(0,0,W,H);
				cc.save(); cc.textAlign='center'; cc.textBaseline='middle'; cc.font='bold '+Math.round(size)+'px STXingkai, KaiTi, STKaiti, SimSun, serif'; cc.shadowColor='rgba(205,32,18,0.40)'; cc.shadowBlur=10*S; cc.fillStyle='rgba(186,32,22,0.96)'; cc.fillText(ch,x,y); cc.restore();
				var strokes=calligraphyStrokes[ch] || calligraphyStrokes['终'], each=1/strokes.length;
				mc.save(); mc.strokeStyle='white'; mc.lineCap='round'; mc.lineJoin='round'; mc.lineWidth=size*0.25;
				for(var i=0;i<strokes.length;i++) {
					var local=clamp((p-i*each)/each,0,1); if(local<=0) continue;
					var st=strokes[i], x1=x+st[0]*size, y1=y+st[1]*size, x2=x+st[2]*size, y2=y+st[3]*size;
					mc.beginPath(); mc.moveTo(x1,y1); mc.lineTo(x1+(x2-x1)*easeOut(local), y1+(y2-y1)*easeOut(local)); mc.stroke();
				}
				mc.restore(); cc.globalCompositeOperation='destination-in'; cc.drawImage(maskCanvas,0,0); cc.globalCompositeOperation='source-over'; c.drawImage(charCanvas,0,0);
			}
			function drawCalligraphy(c, t) {
				var p = clamp((t - 26000) / 3000, 0, 1); if(p<=0) return;
				var size = 54*S, y = H*0.66;
				// 从右往左写出“卷终”：先右侧“卷”，再左侧“终”。
				drawBrushChar(c, '卷', cx+30*S, y, size, clamp(p/0.58,0,1));
				drawBrushChar(c, '终', cx-30*S, y, size, clamp((p-0.42)/0.58,0,1));
			}
			function drawSceneAfterBirth(c, t) {
				c.clearRect(0,0,W,H);
				drawFullPaper(c, t, 1);
				if (t < 11000) {
					var amount = 1 - easeInOut((t - 8000) / 3000);
					drawOldInkWorld(c, t, amount);
				}
				var warmP = clamp((t - 20000) / 3200, 0, 1);
				warmHorizon(c, warmP);
				drawSun(c, t);
				drawBamboo(c, t);
				if (t >= 13000 && t < 18000) {
					drawTinyBird(c, W-104*S, H-142*S, clamp((t-13000)/500,0,1), t*0.018);
				}
				if (t >= 15000 && t < 23000) {
					var pp = clamp((t-15000)/8000,0,1);
					var hx = cx - 24*S + pp*72*S, hy = H*0.73 - pp*30*S;
					var sc = 1 - pp*0.58;
					var al = 1 - clamp((t-22500)/900,0,1);
					drawTinyHuman(c, hx, hy, sc, al, t*0.006);
				}
				if (t >= 18000 && t < 23000) {
					var bp = clamp((t-18000)/5000,0,1);
					var seg = Math.min(birdPath.length-2, Math.floor(bp*(birdPath.length-1)));
					var lt = bp*(birdPath.length-1)-seg;
					var b0 = birdPath[seg], b1 = birdPath[seg+1];
					var bx = b0.x + (b1.x-b0.x)*easeInOut(lt), by = b0.y + (b1.y-b0.y)*easeInOut(lt);
					c.save(); c.strokeStyle='rgba(0,0,0,'+(0.10*(1-bp)).toFixed(3)+')'; c.lineWidth=1.5*S; c.beginPath(); c.moveTo(W-104*S,H-142*S); for(var bi=1;bi<=seg;bi++) c.lineTo(birdPath[bi].x,birdPath[bi].y); c.lineTo(bx,by); c.stroke(); c.restore();
					drawTinyBird(c, bx, by, 1-clamp((t-22200)/800,0,1), t*0.022);
				}
				drawCalligraphy(c, t);
				if (t >= 29000) {
					var fp = clamp((t - 29000) / 1000, 0, 1);
					c.fillStyle = 'rgba(255,255,255,' + easeInOut(fp).toFixed(3) + ')';
					c.fillRect(0,0,W,H);
					// 最后一帧只留一点朱红，随后也被白吞没。
					if (fp < 0.94) { c.fillStyle='rgba(190,32,22,'+(0.65*(1-fp)).toFixed(3)+')'; c.beginPath(); c.arc(cx, H*0.66, Math.max(1*S, 3*S*(1-fp)), 0, Math.PI*2); c.fill(); }
				}
			}
			function drawShatter(c, t) {
				c.save();
				c.fillStyle='rgba(0,0,0,0.56)'; c.fillRect(0,0,W,H);
				var p = clamp((t - 2000) / 2000, 0, 1);
				var burst = clamp((t - 2000) / 500, 0, 1);
				drawBossSilhouette(c, bossX, bossY, (1 - easeOut(p))*0.92, 1, true);
				for (var i=0;i<particles.length;i++) {
					var q = p < 0.25 ? easeOut(p/0.25)*0.38 : 0.38 + easeOut((p-0.25)/0.75)*0.62;
					var sp=particles[i];
					var px = bossX + Math.cos(sp.a) * sp.d * q + Math.sin(p*2.1 + sp.seed) * sp.drift * p;
					var py = bossY + Math.sin(sp.a) * sp.d * q * 0.62 - 18*S*p + Math.cos(p*1.7 + sp.seed) * sp.drift * 0.25*p;
					var a = (1-p) * (0.42 + rnd(i+99)*0.58);
					c.fillStyle='rgba(0,0,0,'+a.toFixed(3)+')'; c.beginPath(); c.arc(px,py,sp.r*(1+burst*0.65),0,Math.PI*2); c.fill();
				}
				c.restore();
			}
			function drawStar(c, t) {
				c.fillStyle='rgb(0,0,0)'; c.fillRect(0,0,W,H);
				var p = clamp((t - 4000) / 1000, 0, 1);
				c.fillStyle='rgba(255,255,255,1)'; c.beginPath(); c.arc(cx, cy, (1.2 + 2.0*Math.sin(p*Math.PI))*S, 0, Math.PI*2); c.fill();
			}
			function sound(kind) {
				try {
					var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
					var ac = core.plugin.__mt4InkAudioContext || new AC(); core.plugin.__mt4InkAudioContext = ac; if (ac.state == 'suspended' && ac.resume) ac.resume();
					var gain = ac.createGain(); gain.connect(ac.destination);
					if (kind == 'absorb' || kind == 'brush') {
						var dur = kind == 'absorb' ? 1.8 : 0.7, len = Math.floor(ac.sampleRate * dur), buf = ac.createBuffer(1, len, ac.sampleRate), data = buf.getChannelData(0);
						for (var i=0;i<len;i++) data[i] = (Math.random()*2-1) * Math.pow(1-i/len, kind=='absorb'?0.28:0.82);
						var src=ac.createBufferSource(); src.buffer=buf; var f=ac.createBiquadFilter(); f.type=kind=='absorb'?'lowpass':'highpass'; f.frequency.value=kind=='absorb'?740:1350; gain.gain.value=kind=='absorb'?0.045:0.038; src.connect(f); f.connect(gain); src.start(); return;
					}
					var osc = ac.createOscillator();
					osc.type = kind=='hum' ? 'sawtooth' : 'triangle';
					gain.gain.value = kind=='hum'?0.040:(kind=='shatter'?0.055:(kind=='bell'?0.050:(kind=='bird'?0.035:0.032)));
					osc.frequency.value = kind=='hum'?70:(kind=='shatter'?720:(kind=='bell'?440:(kind=='bird'?1260:520)));
					osc.connect(gain); osc.start();
					gain.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + (kind=='hum'?1.9:(kind=='bell'?1.35:0.35)));
					osc.stop(ac.currentTime + (kind=='hum'?2.0:(kind=='bell'?1.4:0.38)));
				} catch (e) {}
			}
			var played = {};
			function cue(t) {
				if (t > 80 && !played.hum) { played.hum = true; sound('hum'); }
				if (t > 2000 && !played.shatter) { played.shatter = true; sound('shatter'); }
				if (t > 5000 && !played.bell1) { played.bell1 = true; sound('bell'); }
				if (t > 8000 && !played.absorb) { played.absorb = true; sound('absorb'); }
				if (t > 11000 && !played.bamboo) { played.bamboo = true; sound('brush'); }
				if (t > 18000 && !played.bird) { played.bird = true; sound('bird'); }
				if (t > 23000 && !played.bell2) { played.bell2 = true; sound('bell'); }
				if (t > 26000 && !played.words) { played.words = true; sound('brush'); }
			}
			function frame() {
				var t = nowFunc() - startTime;
				cue(t);
				try { if (core.status && core.status.floorId !== 'MT4') throw new Error('floor changed'); } catch (ee) { try { core.deleteCanvas('mt4BossP6P7InkFx'); } catch (e1) {} if (callback) callback(); return; }
				ctx.clearRect(0,0,W,H);
				if (t < 2000) {
					// 0-2秒：冻结最后一击，BOSS轮廓仍在原位，低频拉长。
					ctx.fillStyle='rgba(0,0,0,0.20)'; ctx.fillRect(0,0,W,H);
					drawBossSilhouette(ctx, bossX, bossY, 0.78 + 0.10*Math.sin(t*0.006), 1, false);
				} else if (t < 4000) {
					drawShatter(ctx, t);
				} else if (t < 5000) {
					drawStar(ctx, t);
				} else if (t < 7000) {
					drawBornPaper(ctx, t);
				} else if (t < 8000) {
					drawFullPaper(ctx, t, 1);
				} else {
					drawSceneAfterBirth(ctx, t);
				}
				if (t < total) requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP6P7InkFx'); } catch (e2) {} if (callback) callback(); }
			}
			requestAnimationFrame(frame);
		};


		this.playMT4BossReturnFromMT0Transition = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossReturnFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossReturnFx', 0, 0, W, H, 195);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), total = 1500;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function frame(now) {
				var t = now - start, p = clamp(t / total, 0, 1);
				ctx.clearRect(0, 0, W, H);
				ctx.fillStyle = 'rgba(0,0,0,' + (0.78 * (1 - p)).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				var cx = W/2, cy = H/2;
				for (var i = 0; i < 4; i++) {
					var r = (40 + i * 35 + easeOut(p) * 120) * scale;
					ctx.strokeStyle = 'rgba(255,' + (190 + i * 12) + ',80,' + (0.65 * (1 - p * 0.4)).toFixed(3) + ')';
					ctx.lineWidth = (4 - i * 0.5) * scale;
					ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
				}
				ctx.restore();
				ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				ctx.font = 'bold ' + Math.max(18, Math.round(22 * scale)) + 'px Arial';
				ctx.lineWidth = 5 * scale; ctx.strokeStyle = 'rgba(0,0,0,0.95)';
				var returnTitle = (core.getFlag ? core.getFlag('__MT4BossReturnTitle__', '下一阶段开启') : '下一阶段开启');
				var returnText = '力量归还 · ' + returnTitle;
				ctx.strokeText(returnText, cx, 82 * scale);
				ctx.fillStyle = '#FFF0A0'; ctx.fillText(returnText, cx, 82 * scale);
				if (t < total) requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossReturnFx'); } catch(e){} if (callback) callback(); }
			}
			try { core.playSound('zone.mp3'); } catch (e) {}
			requestAnimationFrame(frame);
		};

		this.playMT4BossP1Counter = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			// 录像回放只执行确定性结果，不播放异步激光动画，避免影响录像时序。
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var bx = 6 * 32 + 16, by = 6 * 32 + 16;
			var tx = 6 * 32 + 16, ty = 12 * 32 + 16;
			var wasLocked = !!(core.status && core.status.lockControl);
			try { core.lockControl(); } catch (e) {}
			try { core.deleteCanvas('mt4BossActionFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossActionFx', 0, 0, W, H, 182);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = null, total = 1050;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function frame(ts) {
				if (start == null) start = ts;
				var t = ts - start;
				ctx.clearRect(0, 0, W, H);
				// 暗屏压迫
				ctx.fillStyle = 'rgba(0,0,0,' + (0.20 + 0.28 * (1 - clamp((t - 760) / 280,0,1))).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				// BOSS眼部充能
				var charge = easeOut(t / 360);
				ctx.globalCompositeOperation = 'lighter';
				var rg = ctx.createRadialGradient(bx, by - 22 * scale, 0, bx, by - 22 * scale, 80 * scale * charge);
				rg.addColorStop(0, 'rgba(255,255,220,0.95)');
				rg.addColorStop(0.3, 'rgba(255,50,30,0.75)');
				rg.addColorStop(1, 'rgba(255,0,0,0)');
				ctx.fillStyle = rg;
				ctx.beginPath(); ctx.arc(bx, by - 22 * scale, 80 * scale * charge, 0, Math.PI * 2); ctx.fill();
				// 激光束
				if (t > 260) {
					var p = easeOut((t - 260) / 220);
					var fade = 1 - clamp((t - 760) / 260, 0, 1);
					var w = (8 + 18 * Math.sin(t / 35) * Math.sin(t / 35)) * scale;
					var len = (ty - by) * p;
					var g = ctx.createLinearGradient(bx - w, by, bx + w, by);
					g.addColorStop(0, 'rgba(255,30,20,0)');
					g.addColorStop(0.45, 'rgba(255,80,40,' + (0.95 * fade).toFixed(3) + ')');
					g.addColorStop(0.5, 'rgba(255,255,220,' + fade.toFixed(3) + ')');
					g.addColorStop(0.55, 'rgba(255,80,40,' + (0.95 * fade).toFixed(3) + ')');
					g.addColorStop(1, 'rgba(255,30,20,0)');
					ctx.fillStyle = g;
					ctx.fillRect(bx - w, by - 5 * scale, w * 2, len + 12 * scale);
					// 命中冲击圈
					ctx.strokeStyle = 'rgba(255,210,90,' + (0.9 * fade).toFixed(3) + ')';
					ctx.lineWidth = Math.max(1, 3 * scale);
					ctx.beginPath(); ctx.arc(tx, ty, (10 + 34 * p) * scale, 0, Math.PI * 2); ctx.stroke();
				}
				ctx.globalCompositeOperation = 'source-over';
				// 震屏
				if (core.dom && core.dom.gameDraw && t > 260 && t < 850) {
					var amp = 5 * scale * (1 - clamp((t - 260) / 600, 0, 1));
					var dx = (Math.random() - 0.5) * 2 * amp, dy = (Math.random() - 0.5) * 2 * amp;
					try { core.dom.gameDraw.style.transform = 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px)'; } catch (e) {}
				}
				if (t < total && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else {
					try { if (core.dom && core.dom.gameDraw) core.dom.gameDraw.style.transform = ''; } catch (e) {}
					try { core.deleteCanvas('mt4BossActionFx'); } catch (e) {}
					if (!wasLocked) { try { core.unlockControl(); } catch (e) {} }
					if (callback) callback();
				}
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('attack.mp3'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};


		this.playMT4BossP2SummonSequence = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			// 录像回放只应用最终地图结果，不播放投送动画，避免影响实际路线。
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var waves = (core.control && core.control.controldata && core.control.controldata.getMT4BossP2SummonWaves)
				? core.control.controldata.getMT4BossP2SummonWaves() : [];
			try { core.lockControl(); } catch (e) {}
			try { core.deleteCanvas('mt4BossSummonFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossSummonFx', 0, 0, W, H, 184);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';

			var waveIndex = 0, waveStart = null;
			var waveDur = 920;
			var holdAfter = 260;
			var spawned = {};

			function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
			function easeOut(v) { v = clamp(v, 0, 1); return 1 - Math.pow(1 - v, 3); }
			function drawSubtitle(title, t) {
				// 护法字幕：绚丽平移进场 -> 停留 -> 平移出场，整体下移一些，避免贴着血条太近。
				var y = 76 * scale;
				var inDur = 260, outDur = 260;
				var stayEnd = waveDur - outDur;
				var pin = easeOut(t / inDur);
				var pout = easeOut((t - stayEnd) / outDur);
				var alpha = clamp(t / 120, 0, 1) * clamp((waveDur - t) / 170, 0, 1);
				// 从左侧高速滑入，退出时向右飞出。
				var x = W / 2 + (-W * 0.72) * (1 - pin) + (W * 0.72) * pout;
				var tilt = (-8 * (1 - pin) + 8 * pout) * Math.PI / 180;
				var boxW = W * 0.56, boxH = 32 * scale;
				var left = x - boxW / 2, top = y - boxH / 2;
				ctx.save();
				ctx.globalAlpha = alpha;
				ctx.translate(x, y);
				ctx.rotate(tilt);
				ctx.translate(-x, -y);

				// 飞入飞出残影光带。
				ctx.globalCompositeOperation = 'lighter';
				for (var s = 0; s < 4; s++) {
					var tail = (18 + s * 16) * scale;
					var tailAlpha = (0.23 - s * 0.04) * (1 - Math.min(pin, 1) * 0.35 + pout * 0.65);
					var lg = ctx.createLinearGradient(left - tail, y, left + boxW * 0.38, y);
					lg.addColorStop(0, 'rgba(255,90,40,0)');
					lg.addColorStop(0.55, 'rgba(255,80,40,' + Math.max(0, tailAlpha).toFixed(3) + ')');
					lg.addColorStop(1, 'rgba(255,230,130,0)');
					ctx.fillStyle = lg;
					ctx.fillRect(left - tail, y - (12 + s * 2) * scale, boxW * 0.55 + tail, (24 + s * 4) * scale);
				}

				// 主字幕黑金底牌。
				ctx.globalCompositeOperation = 'source-over';
				var rg = ctx.createLinearGradient(left, top, left + boxW, top + boxH);
				rg.addColorStop(0, 'rgba(20,0,0,0.95)');
				rg.addColorStop(0.5, 'rgba(0,0,0,0.94)');
				rg.addColorStop(1, 'rgba(35,8,0,0.95)');
				ctx.fillStyle = rg;
				ctx.beginPath();
				ctx.moveTo(left + 14 * scale, top);
				ctx.lineTo(left + boxW - 20 * scale, top);
				ctx.lineTo(left + boxW, y);
				ctx.lineTo(left + boxW - 20 * scale, top + boxH);
				ctx.lineTo(left + 14 * scale, top + boxH);
				ctx.lineTo(left, y);
				ctx.closePath();
				ctx.fill();

				// 边框和扫光。
				ctx.strokeStyle = 'rgba(255,215,90,0.95)';
				ctx.lineWidth = Math.max(1, 2 * scale);
				ctx.stroke();
				ctx.globalCompositeOperation = 'lighter';
				var sweep = (t % 520) / 520;
				var sx = left + boxW * sweep;
				var sg = ctx.createLinearGradient(sx - 36 * scale, top, sx + 36 * scale, top);
				sg.addColorStop(0, 'rgba(255,255,255,0)');
				sg.addColorStop(0.5, 'rgba(255,255,220,0.55)');
				sg.addColorStop(1, 'rgba(255,255,255,0)');
				ctx.fillStyle = sg;
				ctx.fillRect(sx - 36 * scale, top + 2 * scale, 72 * scale, boxH - 4 * scale);

				// 两侧能量尖角。
				ctx.fillStyle = 'rgba(255,70,30,0.75)';
				ctx.beginPath();
				ctx.moveTo(left - 18 * scale, y);
				ctx.lineTo(left - 2 * scale, top + 4 * scale);
				ctx.lineTo(left - 2 * scale, top + boxH - 4 * scale);
				ctx.closePath();
				ctx.fill();
				ctx.beginPath();
				ctx.moveTo(left + boxW + 18 * scale, y);
				ctx.lineTo(left + boxW + 2 * scale, top + 4 * scale);
				ctx.lineTo(left + boxW + 2 * scale, top + boxH - 4 * scale);
				ctx.closePath();
				ctx.fill();

				// 文字：外描边 + 内发光。
				ctx.globalCompositeOperation = 'source-over';
				ctx.font = 'bold ' + Math.max(16, Math.round(20 * scale)) + 'px Arial';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'middle';
				ctx.lineWidth = Math.max(2, 4 * scale);
				ctx.strokeStyle = 'rgba(0,0,0,0.95)';
				ctx.strokeText(title, x, y);
				ctx.shadowColor = '#FF5A26';
				ctx.shadowBlur = 18 * scale;
				ctx.fillStyle = '#FFE77A';
				ctx.fillText(title, x, y);
				ctx.restore();
			}
			function drawEggPortal(gx, gy, t, color) {
				var cx = gx * 32 + 16, cy = gy * 32 + 16;
				var p = easeOut(t / 560);
				var fade = clamp((waveDur - t) / 280, 0, 1);
				var rgb = color || [190, 80, 255];
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				// 地面传送阵
				ctx.strokeStyle = 'rgba(' + rgb.join(',') + ',' + (0.75 * fade).toFixed(3) + ')';
				ctx.lineWidth = Math.max(1, 2 * scale);
				for (var i = 0; i < 3; i++) {
					ctx.beginPath();
					ctx.ellipse(cx, cy + 10 * scale, (8 + i * 7 + p * 10) * scale, (3 + i * 2) * scale, 0, 0, Math.PI * 2);
					ctx.stroke();
				}
				// 竖向光柱
				var hg = ctx.createLinearGradient(cx, cy - 70 * scale, cx, cy + 18 * scale);
				hg.addColorStop(0, 'rgba(' + rgb.join(',') + ',0)');
				hg.addColorStop(0.42, 'rgba(' + rgb.join(',') + ',' + (0.55 * fade).toFixed(3) + ')');
				hg.addColorStop(1, 'rgba(255,255,220,' + (0.45 * fade).toFixed(3) + ')');
				ctx.fillStyle = hg;
				ctx.fillRect(cx - (5 + 11 * p) * scale, cy - 70 * scale, (10 + 22 * p) * scale, 88 * scale);
				// “下蛋”能量卵
				var eggY = cy - (70 * (1 - p)) * scale;
				var eg = ctx.createRadialGradient(cx, eggY, 0, cx, eggY, (7 + 16 * p) * scale);
				eg.addColorStop(0, 'rgba(255,255,230,' + (0.95 * fade).toFixed(3) + ')');
				eg.addColorStop(0.35, 'rgba(' + rgb.join(',') + ',' + (0.72 * fade).toFixed(3) + ')');
				eg.addColorStop(1, 'rgba(' + rgb.join(',') + ',0)');
				ctx.fillStyle = eg;
				ctx.beginPath();
				ctx.ellipse(cx, eggY, (7 + 10 * p) * scale, (11 + 13 * p) * scale, 0, 0, Math.PI * 2);
				ctx.fill();
				// 爆点粒子
				if (t > 430) {
					var q = clamp((t - 430) / 300, 0, 1);
					for (var k = 0; k < 14; k++) {
						var a = k * Math.PI * 2 / 14 + gx * 0.31 + gy * 0.17;
						var r = (5 + q * 28) * scale;
						ctx.fillStyle = 'rgba(255,235,160,' + ((1 - q) * 0.9).toFixed(3) + ')';
						ctx.beginPath(); ctx.arc(cx + Math.cos(a) * r, cy + Math.sin(a) * r, Math.max(1, 1.6 * scale), 0, Math.PI * 2); ctx.fill();
					}
				}
				ctx.restore();
			}
			function frame(ts) {
				if (!core.status || core.status.floorId !== 'MT4') {
					try { core.deleteCanvas('mt4BossSummonFx'); } catch (e) {}
					if (callback) callback();
					return;
				}
				if (waveIndex >= waves.length) {
					try { core.deleteCanvas('mt4BossSummonFx'); } catch (e) {}
					if (callback) callback();
					return;
				}
				if (waveStart == null) waveStart = ts;
				var t = ts - waveStart;
				ctx.clearRect(0, 0, W, H);
				ctx.fillStyle = 'rgba(0,0,0,0.16)';
				ctx.fillRect(0, 0, W, H);
				var wave = waves[waveIndex] || {};
				drawSubtitle(wave.title || '', t);
				var summonColors = [
					[255, 74, 58],   // 屠龙：赤红
					[90, 170, 255],  // 灭虎：蓝光
					[255, 190, 58],  // 屠虎：金光
					[70, 255, 165],  // 豹跃：青绿
					[190, 95, 255],  // 魂锁：紫光
					[255, 90, 185],  // 共生：粉紫
					[255, 65, 65],   // 红宝石：红光
					[70, 135, 255]   // 蓝宝石：蓝光
				];
				var color = summonColors[waveIndex % summonColors.length];
				(wave.blocks || []).forEach(function (one, idx) {
					var localT = t - idx * 90;
					if (localT >= 0) drawEggPortal(one[1], one[2], localT, color);
				});
				if (t >= 560 && !spawned[waveIndex]) {
					spawned[waveIndex] = true;
					try {
						if (core.control && core.control.controldata && core.control.controldata.spawnMT4BossP2Wave) {
							core.control.controldata.spawnMT4BossP2Wave(waveIndex);
						}
						core.drawMap('MT4');
						if (core.plugin && core.plugin.ensureMT4BossScene) core.plugin.ensureMT4BossScene();
					} catch (e) {}
				}
				if (t >= waveDur + holdAfter) {
					waveIndex++;
					waveStart = ts;
				}
				requestAnimationFrame(frame);
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

        this.hideMT4BossP3SealFx = function () {
			core.plugin.__mt4BossP3SealRunning = false;
			try { core.deleteCanvas('mt4BossP3SealFx'); } catch (e) {}
		};

		this.showMT4BossP3SealFx = function () {
			if (!core.status || core.status.floorId !== 'MT4') return;
			if (core.getFlag('__MT4BossPhase__', 1) != 3 || core.hasFlag('__MT4BossP3SealsCleared__')) {
				return core.plugin.hideMT4BossP3SealFx && core.plugin.hideMT4BossP3SealFx();
			}
			if (core.plugin.__mt4BossP3SealRunning && core.dymCanvas && core.dymCanvas.mt4BossP3SealFx) return;
			core.plugin.__mt4BossP3SealRunning = true;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			try { core.deleteCanvas('mt4BossP3SealFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP3SealFx', 0, 0, W, H, 119);
			var scale = Math.min(W, H) / 416;
			function getSeals() {
				var cd = core.control && core.control.controldata;
				return cd && cd.getMT4BossP3Seals ? cd.getMT4BossP3Seals() : [];
			}
			function frame(ts) {
				if (!core.plugin.__mt4BossP3SealRunning || !core.status || core.status.floorId !== 'MT4' || core.getFlag('__MT4BossPhase__', 1) != 3 || core.hasFlag('__MT4BossP3SealsCleared__')) {
					core.plugin.__mt4BossP3SealRunning = false;
					try { core.deleteCanvas('mt4BossP3SealFx'); } catch (e) {}
					return;
				}
				ctx.clearRect(0, 0, W, H);
				var active = core.getFlag('__MT4BossP3Seals__', {});
				var list = getSeals().filter(function (s) { return active[s.key]; });
				if (!list.length) return requestAnimationFrame(frame);
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				ctx.lineWidth = 2.2 * scale;
				for (var i = 0; i < list.length; i++) {
					for (var j = i + 1; j < list.length; j++) {
						var a = list[i], b = list[j];
						var ax = a.x * 32 * scale + 16 * scale, ay = a.y * 32 * scale + 16 * scale;
						var bx = b.x * 32 * scale + 16 * scale, by = b.y * 32 * scale + 16 * scale;
						var grd = ctx.createLinearGradient(ax, ay, bx, by);
						grd.addColorStop(0, 'rgba(' + a.color.join(',') + ',0.42)');
						grd.addColorStop(1, 'rgba(' + b.color.join(',') + ',0.42)');
						ctx.strokeStyle = grd;
						ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
					}
				}
				list.forEach(function (s, idx) {
					var cx = s.x * 32 * scale + 16 * scale, cy = s.y * 32 * scale + 16 * scale;
					var pulse = Math.sin(ts / 220 + idx) * 0.5 + 0.5;
					var r = (18 + pulse * 5) * scale;
					var col = s.color;
					var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r * 1.8);
					g.addColorStop(0, 'rgba(255,255,255,0.8)');
					g.addColorStop(0.35, 'rgba(' + col.join(',') + ',0.8)');
					g.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
					ctx.fillStyle = g;
					ctx.beginPath(); ctx.arc(cx, cy, r * 1.8, 0, Math.PI * 2); ctx.fill();
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',0.9)';
					ctx.lineWidth = 2 * scale;
					ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
					ctx.save(); ctx.translate(cx, cy); ctx.rotate(ts / 550 + idx);
					ctx.beginPath();
					for (var k = 0; k < 6; k++) {
						var ang = k * Math.PI / 3;
						var px = Math.cos(ang) * r * 0.85, py = Math.sin(ang) * r * 0.85;
						if (k == 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
					}
					ctx.closePath(); ctx.stroke(); ctx.restore();
				});
				ctx.restore();
				requestAnimationFrame(frame);
			}
			requestAnimationFrame(frame);
		};

		this.playMT4BossP3SealBreak = function (seal) {
			if (!seal || !core.status || core.status.floorId !== 'MT4') return;
			// 录像回放：阵眼破除只是纯视觉特效，破阵的实际逻辑在别处；回放时跳过动画，避免干扰录像。
			if (core.isReplaying && core.isReplaying()) return;
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var cx = seal.x * 32 * scale + 16 * scale, cy = seal.y * 32 * scale + 16 * scale;
			try { core.deleteCanvas('mt4BossSealBreakFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossSealBreakFx', 0, 0, W, H, 186);
			var start = performance.now();
			function frame(now) {
				var t = now - start, p = Math.min(1, t / 520);
				ctx.clearRect(0, 0, W, H);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				var col = seal.color;
				ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (1 - p).toFixed(3) + ')';
				ctx.lineWidth = (5 - p * 3) * scale;
				ctx.beginPath(); ctx.arc(cx, cy, (8 + p * 72) * scale, 0, Math.PI * 2); ctx.stroke();
				ctx.fillStyle = 'rgba(255,255,255,' + (0.65 * (1 - p)).toFixed(3) + ')';
				ctx.beginPath(); ctx.arc(cx, cy, (14 + p * 18) * scale, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
				if (p < 1 && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossSealBreakFx'); } catch (e) {} }
			}
			requestAnimationFrame(frame);
		};

		this.playMT4BossP3StepLaser = function (heroX, heroY, damages) {
			if (!core.status || core.status.floorId !== 'MT4') return;
			if (core.isReplaying && core.isReplaying()) return;
			damages = damages || [5, 5, 5];
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var hx = heroX * 32 * scale + 16 * scale, hy = heroY * 32 * scale + 16 * scale;
			try { core.deleteCanvas('mt4BossP3LaserFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP3LaserFx', 0, 0, W, H, 185);
			var sources = [
				{ x: W / 2, y: H / 2 + 6 * scale, c: [255, 50, 50] },
				{ x: W * 0.22, y: H / 2 + 30 * scale, c: [80, 190, 255] },
				{ x: W * 0.78, y: H / 2 + 30 * scale, c: [255, 80, 150] }
			];
			var start = performance.now();
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function frame(now) {
				var t = now - start, p = Math.min(1, t / 520);
				ctx.clearRect(0, 0, W, H);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				sources.forEach(function (s, idx) {
					var delay = idx * 85;
					var q = clamp((t - delay) / 170, 0, 1);
					if (q <= 0) return;
					var fade = 1 - clamp((t - delay - 140) / 240, 0, 1);
					var col = s.c;
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.95 * fade).toFixed(3) + ')';
					ctx.lineWidth = (7 - idx * 0.8 + Math.sin(t / 22) * 1.2) * scale;
					ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(hx, hy - 9 * scale); ctx.stroke();
					ctx.strokeStyle = 'rgba(255,255,255,' + (0.82 * fade).toFixed(3) + ')';
					ctx.lineWidth = 2 * scale;
					ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(hx, hy - 9 * scale); ctx.stroke();
					// 头顶掉血数字，每道激光各跳一次。
					var nT = clamp((t - delay) / 360, 0, 1);
					var ny = hy - (34 + idx * 12 + nT * 30) * scale;
					var nx = hx + (idx - 1) * 16 * scale + Math.sin(nT * Math.PI) * (idx - 1) * 8 * scale;
					var a = 1 - nT;
					ctx.save();
					ctx.globalCompositeOperation = 'source-over';
					ctx.font = 'bold ' + Math.max(13, Math.round(16 * scale)) + 'px Arial';
					ctx.textAlign = 'center';
					ctx.textBaseline = 'middle';
					ctx.lineWidth = Math.max(2, 4 * scale);
					ctx.strokeStyle = 'rgba(0,0,0,' + (0.95 * a).toFixed(3) + ')';
					ctx.strokeText('-' + (damages[idx] || 5), nx, ny);
					ctx.fillStyle = 'rgba(255,70,50,' + a.toFixed(3) + ')';
					ctx.fillText('-' + (damages[idx] || 5), nx, ny);
					ctx.restore();
				});
				ctx.restore();
				if (p < 1 && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP3LaserFx'); } catch (e) {} }
			}
			requestAnimationFrame(frame);
		};


		this.playMT4BossP3SpawnSequence = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			var cd = core.control && core.control.controldata;
			var waves = cd && cd.getMT4BossP3SpawnWaves ? cd.getMT4BossP3SpawnWaves() : [];
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossP3SpawnFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP3SpawnFx', 0, 0, W, H, 184);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var waveIndex = 0, waveStart = null, waveDur = 880, hold = 220, spawned = {};
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function easeOut(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function drawText(title, t, col) {
				var y = 79 * scale;
				var pIn = easeOut(t / 220), pOut = easeOut((t - 650) / 220);
				var x = W / 2 - W * 0.65 * (1 - pIn) + W * 0.65 * pOut;
				var a = clamp(t / 110, 0, 1) * clamp((waveDur - t) / 170, 0, 1);
				ctx.save(); ctx.globalAlpha = a; ctx.globalCompositeOperation = 'lighter';
				var bw = W * 0.50, bh = 27 * scale;
				var g = ctx.createLinearGradient(x - bw / 2, y, x + bw / 2, y);
				g.addColorStop(0, 'rgba(' + col.join(',') + ',0)');
				g.addColorStop(0.25, 'rgba(' + col.join(',') + ',0.66)');
				g.addColorStop(0.5, 'rgba(255,255,230,0.72)');
				g.addColorStop(0.75, 'rgba(' + col.join(',') + ',0.66)');
				g.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
				ctx.fillStyle = g; ctx.fillRect(x - bw / 2, y - bh / 2, bw, bh);
				ctx.globalCompositeOperation = 'source-over';
				ctx.font = 'bold ' + Math.max(15, Math.round(18 * scale)) + 'px Arial';
				ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				ctx.lineWidth = Math.max(2, 4 * scale); ctx.strokeStyle = 'rgba(0,0,0,0.95)';
				ctx.strokeText(title, x, y);
				ctx.shadowColor = 'rgb(' + col.join(',') + ')'; ctx.shadowBlur = 16 * scale;
				ctx.fillStyle = '#FFF2A0'; ctx.fillText(title, x, y);
				ctx.restore();
			}
			function drawPortal(gx, gy, t, col) {
				var cx = gx * 32 * scale + 16 * scale, cy = gy * 32 * scale + 16 * scale;
				var p = easeOut(t / 520), fade = clamp((waveDur - t) / 280, 0, 1);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.75 * fade).toFixed(3) + ')';
				ctx.lineWidth = Math.max(1, 2 * scale);
				for (var i = 0; i < 4; i++) {
					ctx.beginPath();
					ctx.ellipse(cx, cy + 9 * scale, (7 + i * 6 + p * 11) * scale, (3 + i * 2) * scale, 0, 0, Math.PI * 2);
					ctx.stroke();
				}
				var hg = ctx.createLinearGradient(cx, cy - 74 * scale, cx, cy + 18 * scale);
				hg.addColorStop(0, 'rgba(' + col.join(',') + ',0)');
				hg.addColorStop(0.45, 'rgba(' + col.join(',') + ',' + (0.52 * fade).toFixed(3) + ')');
				hg.addColorStop(1, 'rgba(255,255,230,' + (0.50 * fade).toFixed(3) + ')');
				ctx.fillStyle = hg; ctx.fillRect(cx - (4 + p * 12) * scale, cy - 74 * scale, (8 + p * 24) * scale, 94 * scale);
				var eggY = cy - 72 * (1 - p) * scale;
				var rg = ctx.createRadialGradient(cx, eggY, 0, cx, eggY, (9 + 18 * p) * scale);
				rg.addColorStop(0, 'rgba(255,255,235,' + (0.95 * fade).toFixed(3) + ')');
				rg.addColorStop(0.38, 'rgba(' + col.join(',') + ',' + (0.82 * fade).toFixed(3) + ')');
				rg.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
				ctx.fillStyle = rg; ctx.beginPath(); ctx.ellipse(cx, eggY, (8 + 10 * p) * scale, (12 + 12 * p) * scale, 0, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
			}
			function frame(ts) {
				if (!core.status || core.status.floorId !== 'MT4') { try { core.deleteCanvas('mt4BossP3SpawnFx'); } catch (e) {}; if (callback) callback(); return; }
				if (waveIndex >= waves.length) { try { core.deleteCanvas('mt4BossP3SpawnFx'); } catch (e) {}; if (callback) callback(); return; }
				if (waveStart == null) waveStart = ts;
				var t = ts - waveStart;
				var wave = waves[waveIndex] || {}, col = wave.color || [180,80,255];
				ctx.clearRect(0, 0, W, H);
				ctx.fillStyle = 'rgba(0,0,0,0.13)'; ctx.fillRect(0, 0, W, H);
				drawText(wave.title || '', t, col);
				var pts = (wave.points || []).slice();
				(wave.blocks || []).forEach(function (b) { pts.push([b[1], b[2]]); });
				pts.forEach(function (pt, idx) { var lt = t - idx * 80; if (lt >= 0) drawPortal(pt[0], pt[1], lt, col); });
				if (t >= 530 && !spawned[waveIndex]) {
					spawned[waveIndex] = true;
					try { if (cd && cd.spawnMT4BossP3Wave) cd.spawnMT4BossP3Wave(waveIndex); if (core.drawMap) core.drawMap('MT4'); if (core.plugin.ensureMT4BossScene) core.plugin.ensureMT4BossScene(); if (core.plugin.showMT4BossP3SealFx) core.plugin.showMT4BossP3SealFx(); } catch (e) {}
				}
				if (t >= waveDur + hold) { waveIndex++; waveStart = ts; }
				requestAnimationFrame(frame);
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossP3Transform = function (callback) {
			if (!core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			// 录像回放：变身演出是纯视觉过场，阶段切换逻辑在 callback 里；回放时跳过动画直接执行 callback。
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossP3TransformFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP3TransformFx', 0, 0, W, H, 187);
			var start = performance.now(), total = 2200;
			function drawSigil(x, y, r, col, p, rot) {
				ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
				ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.25 + p * 0.55).toFixed(3) + ')';
				ctx.lineWidth = 2 * scale;
				ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.stroke();
				ctx.beginPath();
				for (var i = 0; i < 6; i++) {
					var a = i * Math.PI / 3;
					var px = Math.cos(a) * r * 0.75, py = Math.sin(a) * r * 0.75;
					if (i == 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
				}
				ctx.closePath(); ctx.stroke(); ctx.restore();
			}
			function frame(now) {
				var t = now - start, p = Math.min(1, t / total);
				ctx.clearRect(0, 0, W, H);
				ctx.save();
				ctx.fillStyle = 'rgba(0,0,0,' + (0.28 * (1 - Math.abs(p - 0.5))).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				ctx.globalCompositeOperation = 'lighter';
				var cx = W / 2, cy = H / 2 + 18 * scale;
				var pulse = Math.sin(t / 120) * 0.5 + 0.5;
				ctx.strokeStyle = 'rgba(120,210,255,' + (0.35 + pulse * 0.35).toFixed(3) + ')';
				ctx.lineWidth = 3 * scale;
				ctx.beginPath(); ctx.arc(cx, cy, (45 + p * 90) * scale, 0, Math.PI * 2); ctx.stroke();
				drawSigil(3 * 32 * scale + 16 * scale, 3 * 32 * scale + 16 * scale, 22 * scale, [255,80,80], p, t / 320);
				drawSigil(9 * 32 * scale + 16 * scale, 3 * 32 * scale + 16 * scale, 22 * scale, [90,170,255], p, -t / 300);
				drawSigil(3 * 32 * scale + 16 * scale, 9 * 32 * scale + 16 * scale, 22 * scale, [120,255,170], p, -t / 360);
				drawSigil(9 * 32 * scale + 16 * scale, 9 * 32 * scale + 16 * scale, 22 * scale, [255,215,80], p, t / 280);
				try {
					var off = (1 - Math.min(1, p * 1.4)) * W * 0.22;
					core.plugin.drawMT4BossMiniEcho(ctx, t, W * 0.22 + off, H / 2 + 30 * scale, 0.62, Math.min(1, p * 1.6), -1);
					core.plugin.drawMT4BossMiniEcho(ctx, t, W * 0.78 - off, H / 2 + 30 * scale, 0.62, Math.min(1, p * 1.6), 1);
				} catch (e) {}
				ctx.restore();
				if (t < total && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP3TransformFx'); } catch (e) {}; if (callback) callback(); }
			}
			requestAnimationFrame(frame);
		};

		this.hideMT4BossP4AltarFx = function () {
			core.plugin.__mt4BossP4AltarRunning = false;
			try { core.deleteCanvas('mt4BossP4AltarFx'); } catch (e) {}
		};

		this.showMT4BossP4AltarFx = function () {
			if (!core.status || core.status.floorId !== 'MT4') return;
			if (core.getFlag('__MT4BossPhase__', 1) != 4 || core.hasFlag('__MT4BossP4Unlocked__')) {
				return core.plugin.hideMT4BossP4AltarFx && core.plugin.hideMT4BossP4AltarFx();
			}
			if (core.plugin.__mt4BossP4AltarRunning && core.dymCanvas && core.dymCanvas.mt4BossP4AltarFx) return;
			core.plugin.__mt4BossP4AltarRunning = true;
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossP4AltarFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP4AltarFx', 0, 0, W, H, 119);
			function getAltars() {
				var cd = core.control && core.control.controldata;
				return cd && cd.getMT4BossP4Altars ? cd.getMT4BossP4Altars() : [];
			}
			function starPath(ctx, r1, r2, n) {
				ctx.beginPath();
				for (var i = 0; i < n * 2; i++) {
					var a = -Math.PI / 2 + i * Math.PI / n;
					var r = i % 2 ? r2 : r1;
					var x = Math.cos(a) * r, y = Math.sin(a) * r;
					if (i == 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
				}
				ctx.closePath();
			}
			function frame(ts) {
				if (!core.plugin.__mt4BossP4AltarRunning || !core.status || core.status.floorId !== 'MT4' || core.getFlag('__MT4BossPhase__', 1) != 4 || core.hasFlag('__MT4BossP4Unlocked__')) {
					core.plugin.__mt4BossP4AltarRunning = false;
					try { core.deleteCanvas('mt4BossP4AltarFx'); } catch (e) {}
					return;
				}
				ctx.clearRect(0, 0, W, H);
				var list = getAltars();
				if (!list.length) return requestAnimationFrame(frame);
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				// 三祭坛形成流动三角献祭领域；边线上有能量流，视觉比静态线更有冲击。
				for (var i = 0; i < list.length; i++) {
					var a = list[i], b = list[(i + 1) % list.length];
					var ax = a.x * 32 * scale + 16 * scale, ay = a.y * 32 * scale + 16 * scale;
					var bx = b.x * 32 * scale + 16 * scale, by = b.y * 32 * scale + 16 * scale;
					var grd = ctx.createLinearGradient(ax, ay, bx, by);
					var pulse = Math.sin(ts / 160 + i) * 0.5 + 0.5;
					grd.addColorStop(0, 'rgba(' + a.color.join(',') + ',' + (0.35 + pulse * 0.30).toFixed(3) + ')');
					grd.addColorStop(0.5, 'rgba(255,255,230,' + (0.15 + pulse * 0.18).toFixed(3) + ')');
					grd.addColorStop(1, 'rgba(' + b.color.join(',') + ',' + (0.35 + pulse * 0.30).toFixed(3) + ')');
					ctx.strokeStyle = grd;
					ctx.lineWidth = (2.5 + pulse * 1.8) * scale;
					ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
					for (var q = 0; q < 3; q++) {
						var u = ((ts / 900 + q / 3 + i * 0.09) % 1);
						var px = ax + (bx - ax) * u, py = ay + (by - ay) * u;
						var rg = ctx.createRadialGradient(px, py, 0, px, py, 7 * scale);
						rg.addColorStop(0, 'rgba(255,255,255,0.85)');
						rg.addColorStop(0.35, 'rgba(' + a.color.join(',') + ',0.70)');
						rg.addColorStop(1, 'rgba(' + a.color.join(',') + ',0)');
						ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(px, py, 7 * scale, 0, Math.PI * 2); ctx.fill();
					}
				}
				list.forEach(function (a, idx) {
					var cx = a.x * 32 * scale + 16 * scale, cy = a.y * 32 * scale + 16 * scale;
					var pulse = Math.sin(ts / 190 + idx * 1.7) * 0.5 + 0.5;
					var col = a.color;
					var r = (17 + pulse * 8) * scale;
					var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r * 3.4);
					g.addColorStop(0, 'rgba(255,255,255,0.95)');
					g.addColorStop(0.22, 'rgba(' + col.join(',') + ',0.95)');
					g.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
					ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, cy, r * 3.4, 0, Math.PI * 2); ctx.fill();
					ctx.save(); ctx.translate(cx, cy);
					ctx.rotate(ts / (430 + idx * 80));
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',0.98)';
					ctx.lineWidth = 2.2 * scale;
					starPath(ctx, r * 1.08, r * 0.46, 6); ctx.stroke();
					ctx.rotate(-ts / (290 + idx * 60));
					ctx.beginPath(); ctx.arc(0, 0, r * 0.78, 0, Math.PI * 2); ctx.stroke();
					ctx.beginPath(); ctx.arc(0, 0, r * 1.55, 0, Math.PI * 2); ctx.stroke();
					ctx.restore();
					// 上升的祭坛柱光
					var pillar = ctx.createLinearGradient(cx, cy - 74 * scale, cx, cy + 22 * scale);
					pillar.addColorStop(0, 'rgba(' + col.join(',') + ',0)');
					pillar.addColorStop(0.55, 'rgba(' + col.join(',') + ',' + (0.14 + pulse * 0.10).toFixed(3) + ')');
					pillar.addColorStop(1, 'rgba(255,255,230,' + (0.25 + pulse * 0.15).toFixed(3) + ')');
					ctx.fillStyle = pillar;
					ctx.fillRect(cx - (7 + pulse * 4) * scale, cy - 74 * scale, (14 + pulse * 8) * scale, 96 * scale);
				});
				ctx.restore();
				requestAnimationFrame(frame);
			}
			requestAnimationFrame(frame);
		};

		this.playMT4BossP4AltarIntro = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			var cd = core.control && core.control.controldata;
			var altars = cd && cd.getMT4BossP4Altars ? cd.getMT4BossP4Altars() : [];
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossP4IntroFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP4IntroFx', 0, 0, W, H, 188);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), shown = false;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function ease(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function drawAltarRise(a, idx, t) {
				var local = t - idx * 180;
				if (local < 0) return;
				var p = ease(local / 640), fade = clamp((1450 - local) / 360, 0, 1);
				var cx = a.x * 32 * scale + 16 * scale, cy = a.y * 32 * scale + 16 * scale;
				var col = a.color || [255,160,80];
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				var shock = ctx.createRadialGradient(cx, cy, 0, cx, cy, (12 + p * 95) * scale);
				shock.addColorStop(0, 'rgba(255,255,255,' + (0.58 * fade).toFixed(3) + ')');
				shock.addColorStop(0.22, 'rgba(' + col.join(',') + ',' + (0.50 * fade).toFixed(3) + ')');
				shock.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
				ctx.fillStyle = shock; ctx.beginPath(); ctx.arc(cx, cy, (12 + p * 95) * scale, 0, Math.PI * 2); ctx.fill();
				ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.95 * fade).toFixed(3) + ')';
				ctx.lineWidth = (5 - p * 2) * scale;
				for (var r = 0; r < 4; r++) { ctx.beginPath(); ctx.arc(cx, cy, (8 + r * 9 + p * 32) * scale, 0, Math.PI * 2); ctx.stroke(); }
				var pillar = ctx.createLinearGradient(cx, cy - (140 - 45 * p) * scale, cx, cy + 24 * scale);
				pillar.addColorStop(0, 'rgba(' + col.join(',') + ',0)');
				pillar.addColorStop(0.35, 'rgba(' + col.join(',') + ',' + (0.48 * fade).toFixed(3) + ')');
				pillar.addColorStop(1, 'rgba(255,255,255,' + (0.72 * fade).toFixed(3) + ')');
				ctx.fillStyle = pillar;
				ctx.fillRect(cx - (10 + p * 8) * scale, cy - (140 - 45 * p) * scale, (20 + p * 16) * scale, (164 - 45 * p) * scale);
				ctx.restore();
			}
			function frame(now) {
				if (!core.status || core.status.floorId !== 'MT4') { try { core.deleteCanvas('mt4BossP4IntroFx'); } catch (e) {}; if (callback) callback(); return; }
				var t = now - start;
				ctx.clearRect(0, 0, W, H);
				var bg = clamp(t / 260, 0, 1) * clamp((1850 - t) / 380, 0, 1);
				ctx.fillStyle = 'rgba(0,0,0,' + (0.34 * bg).toFixed(3) + ')'; ctx.fillRect(0, 0, W, H);
				ctx.save(); ctx.font = 'bold ' + Math.max(18, Math.round(23 * scale)) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				var a = clamp(t / 220, 0, 1) * clamp((1800 - t) / 340, 0, 1);
				ctx.lineWidth = 5 * scale; ctx.strokeStyle = 'rgba(0,0,0,' + (0.92*a).toFixed(3) + ')'; ctx.strokeText('献祭祭坛降临', W / 2, H * 0.18);
				ctx.fillStyle = 'rgba(255,226,130,' + a.toFixed(3) + ')'; ctx.fillText('献祭祭坛降临', W / 2, H * 0.18); ctx.restore();
				for (var i = 0; i < altars.length; i++) drawAltarRise(altars[i], i, t);
				if (t >= 1050 && !shown) {
					shown = true;
					try { if (core.plugin.showMT4BossP4AltarFx) core.plugin.showMT4BossP4AltarFx(); } catch (e) {}
				}
				if (t < 1900) requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP4IntroFx'); } catch (e) {}; if (callback) callback(); }
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossGauntletDropRestore = function (floorId, blocks, callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			blocks = blocks || [];
			if (!blocks.length) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossTrialDropFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossTrialDropFx', 0, 0, W, H, 197);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), total = Math.min(2200, 850 + blocks.length * 18);
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function ease(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function drawDrop(one, idx, t) {
				var local = t - (idx % 28) * 18;
				if (local < 0) return;
				var p = ease(local / 620), fade = clamp((total - local) / 420, 0, 1);
				var cx = one.x * 32 * scale + 16 * scale;
				var cy = one.y * 32 * scale + 16 * scale;
				var hue = (idx * 41 + (floorId == 'MT1' ? 80 : floorId == 'MT2' ? 200 : floorId == 'MT3' ? 290 : 25)) % 360;
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				var ring = 4 + p * 24;
				ctx.strokeStyle = 'hsla(' + hue + ',100%,65%,' + (0.72 * fade).toFixed(3) + ')';
				ctx.lineWidth = Math.max(1, 2.2 * scale);
				ctx.beginPath(); ctx.arc(cx, cy, ring * scale, 0, Math.PI * 2); ctx.stroke();
				ctx.beginPath(); ctx.arc(cx, cy, (ring * 0.55) * scale, 0, Math.PI * 2); ctx.stroke();
				var boltY = cy - (115 * (1 - p) + 8 * Math.sin(local / 45 + idx)) * scale;
				var g = ctx.createLinearGradient(cx, boltY - 40 * scale, cx, cy + 16 * scale);
				g.addColorStop(0, 'hsla(' + hue + ',100%,70%,0)');
				g.addColorStop(0.35, 'hsla(' + hue + ',100%,70%,' + (0.36 * fade).toFixed(3) + ')');
				g.addColorStop(1, 'rgba(255,255,255,' + (0.62 * fade).toFixed(3) + ')');
				ctx.fillStyle = g;
				ctx.fillRect(cx - 5 * scale, boltY - 40 * scale, 10 * scale, cy - boltY + 58 * scale);
				var rg = ctx.createRadialGradient(cx, boltY, 0, cx, boltY, (8 + 14 * p) * scale);
				rg.addColorStop(0, 'rgba(255,255,255,' + (0.95 * fade).toFixed(3) + ')');
				rg.addColorStop(0.4, 'hsla(' + hue + ',100%,68%,' + (0.8 * fade).toFixed(3) + ')');
				rg.addColorStop(1, 'hsla(' + hue + ',100%,68%,0)');
				ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(cx, boltY, (8 + 12 * p) * scale, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
			}
			function frame(now) {
				var t = now - start;
				ctx.clearRect(0, 0, W, H);
				var alpha = 0.18 * clamp(t / 200, 0, 1) * clamp((total - t) / 320, 0, 1);
				ctx.fillStyle = 'rgba(0,0,0,' + alpha.toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				var ta = clamp(t / 220, 0, 1) * clamp((total - t) / 360, 0, 1);
				ctx.font = 'bold ' + Math.max(16, Math.round(20 * scale)) + 'px Arial';
				ctx.lineWidth = 4 * scale; ctx.strokeStyle = 'rgba(0,0,0,' + (0.9 * ta).toFixed(3) + ')';
				ctx.strokeText(floorId + ' 试炼重构', W / 2, 58 * scale);
				ctx.fillStyle = 'rgba(255,235,150,' + ta.toFixed(3) + ')';
				ctx.fillText(floorId + ' 试炼重构', W / 2, 58 * scale);
				ctx.restore();
				for (var i = 0; i < blocks.length; i++) drawDrop(blocks[i], i, t);
				if (t < total) requestAnimationFrame(frame);
				else {
					try { core.deleteCanvas('mt4BossTrialDropFx'); } catch (e) {}
					if (callback) callback();
				}
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossP4SpawnSequence = function (callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			var cd = core.control && core.control.controldata;
			var pts = cd && cd.getMT4BossP4Minions ? cd.getMT4BossP4Minions() : [];
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			try { core.deleteCanvas('mt4BossP4SpawnFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP4SpawnFx', 0, 0, W, H, 188);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = performance.now(), spawned = false;
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function ease(v){v=clamp(v,0,1);return 1-Math.pow(1-v,3);}
			function drawOne(gx, gy, idx, t) {
				var local = t - (idx % 9) * 35;
				if (local < 0) return;
				var p = ease(local / 520), fade = clamp((1050 - local) / 300, 0, 1);
				var cx = gx * 32 * scale + 16 * scale, cy = gy * 32 * scale + 16 * scale;
				var hue = (idx * 31 + 20) % 360;
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				ctx.strokeStyle = 'hsla(' + hue + ',100%,65%,' + (0.75 * fade).toFixed(3) + ')';
				ctx.lineWidth = 2 * scale;
				for (var r = 0; r < 3; r++) {
					ctx.beginPath(); ctx.arc(cx, cy, (5 + r * 7 + p * 22) * scale, 0, Math.PI * 2); ctx.stroke();
				}
				var boltX = cx + Math.sin(idx * 1.9) * 9 * scale;
				var g = ctx.createLinearGradient(boltX, cy - 100 * scale, cx, cy + 14 * scale);
				g.addColorStop(0, 'hsla(' + hue + ',100%,70%,0)');
				g.addColorStop(0.42, 'hsla(' + hue + ',100%,70%,' + (0.52 * fade).toFixed(3) + ')');
				g.addColorStop(1, 'rgba(255,255,230,' + (0.68 * fade).toFixed(3) + ')');
				ctx.fillStyle = g; ctx.fillRect(cx - (5 + p * 10) * scale, cy - 100 * scale, (10 + p * 20) * scale, 122 * scale);
				var orbY = cy - (82 * (1 - p)) * scale;
				var rg = ctx.createRadialGradient(cx, orbY, 0, cx, orbY, (9 + 14 * p) * scale);
				rg.addColorStop(0, 'rgba(255,255,255,' + (0.95 * fade).toFixed(3) + ')');
				rg.addColorStop(0.35, 'hsla(' + hue + ',100%,65%,' + (0.85 * fade).toFixed(3) + ')');
				rg.addColorStop(1, 'hsla(' + hue + ',100%,65%,0)');
				ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(cx, orbY, (9 + p * 12) * scale, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
			}
			function frame(now) {
				if (!core.status || core.status.floorId !== 'MT4') { try { core.deleteCanvas('mt4BossP4SpawnFx'); } catch (e) {}; if (callback) callback(); return; }
				var t = now - start;
				ctx.clearRect(0, 0, W, H);
				ctx.fillStyle = 'rgba(0,0,0,' + (0.18 * clamp(t/250,0,1) * clamp((1450-t)/320,0,1)).toFixed(3) + ')'; ctx.fillRect(0, 0, W, H);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				ctx.strokeStyle = 'rgba(255,220,140,' + (0.35 * clamp(t/250,0,1)).toFixed(3) + ')';
				ctx.lineWidth = 2 * scale;
				ctx.beginPath(); ctx.moveTo(W * 0.12, H * 0.18); ctx.lineTo(W * 0.88, H * 0.18); ctx.stroke();
				ctx.restore();
				ctx.save(); ctx.font = 'bold ' + Math.max(17, Math.round(21 * scale)) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				var a = clamp(t / 180, 0, 1) * clamp((1450 - t) / 260, 0, 1);
				ctx.lineWidth = 4 * scale; ctx.strokeStyle = 'rgba(0,0,0,' + (0.9*a).toFixed(3) + ')'; ctx.strokeText('祭品投送', W / 2, H * 0.18);
				ctx.fillStyle = 'rgba(255,235,155,' + a.toFixed(3) + ')'; ctx.fillText('祭品投送', W / 2, H * 0.18); ctx.restore();
				for (var i = 0; i < pts.length; i++) drawOne(pts[i][0], pts[i][1], i, t);
				if (t >= 700 && !spawned) {
					spawned = true;
					try { if (cd && cd.spawnMT4BossP4Minions) cd.spawnMT4BossP4Minions(); core.drawMap('MT4'); if (core.plugin.ensureMT4BossScene) core.plugin.ensureMT4BossScene(); } catch (e) {}
				}
				if (t < 1500) requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP4SpawnFx'); } catch (e) {}; if (callback) callback(); }
			}
			try { core.playSound('zone.mp3'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossP4SacrificeFx = function (altar, callback) {
			if (!altar || !core.status || core.status.floorId !== 'MT4') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var cx = altar.x * 32 * scale + 16 * scale, cy = altar.y * 32 * scale + 16 * scale;
			var col = altar.color || [255, 100, 100];
			try { core.deleteCanvas('mt4BossP4SacrificeFx'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossP4SacrificeFx', 0, 0, W, H, 189);
			var start = performance.now();
			function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
			function frame(now) {
				var t = now - start, p = clamp(t / 780, 0, 1), fade = 1 - p;
				ctx.clearRect(0, 0, W, H);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				// 三种祭坛各有不同爆发形状
				if (altar.key == 'top') {
					var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, (30 + p * 120) * scale);
					g.addColorStop(0, 'rgba(255,255,255,' + (0.9 * fade).toFixed(3) + ')');
					g.addColorStop(0.25, 'rgba(' + col.join(',') + ',' + (0.85 * fade).toFixed(3) + ')');
					g.addColorStop(1, 'rgba(' + col.join(',') + ',0)');
					ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, cy, (30 + p * 120) * scale, 0, Math.PI * 2); ctx.fill();
				} else if (altar.key == 'left') {
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.9 * fade).toFixed(3) + ')';
					ctx.lineWidth = (8 - p * 4) * scale;
					for (var i = 0; i < 6; i++) { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(i*Math.PI/3+p*3) * (60+p*80) * scale, cy + Math.sin(i*Math.PI/3+p*3) * (60+p*80) * scale); ctx.stroke(); }
				} else {
					ctx.strokeStyle = 'rgba(' + col.join(',') + ',' + (0.9 * fade).toFixed(3) + ')';
					ctx.lineWidth = (6 - p * 3) * scale;
					for (var r = 0; r < 5; r++) { ctx.beginPath(); ctx.arc(cx, cy, (10 + r*11 + p*88) * scale, 0, Math.PI * 2); ctx.stroke(); }
				}
				// 能量飞向BOSS
				var bx = 6 * 32 * scale + 16 * scale, by = 9 * 32 * scale + 16 * scale;
				var q = Math.min(1, Math.max(0, (t - 160) / 420));
				var ex = cx + (bx - cx) * q, ey = cy + (by - cy) * q;
				ctx.fillStyle = 'rgba(255,255,220,' + (0.95 * fade).toFixed(3) + ')';
				ctx.beginPath(); ctx.arc(ex, ey, (7 + Math.sin(t/40)*2) * scale, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
				if (p < 1 && core.status && core.status.floorId === 'MT4') requestAnimationFrame(frame);
				else { try { core.deleteCanvas('mt4BossP4SacrificeFx'); } catch (e) {}; if (callback) callback(); }
			}
			try { core.playSound('破墙镐'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
			requestAnimationFrame(frame);
		};

		this.playMT4BossIntro = function (callback) {
			if (main.mode == 'editor') return;
			// 录像回放：开场 CG（约6.6秒）是纯前置演出，不影响战斗状态；回放时跳过，避免干扰录像时序。
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416;
			var H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var cx = W / 2, cy = H / 2 + 12 * scale;
			var total = 6600;
			var hpStart = 4200, hpEnd = 6100;
			var bossName = '龙魂王';
			var wasLocked = !!(core.status && core.status.lockControl);
			var lastBeep = 0;
			var start = null;

			try { core.deleteCanvas('mt4BossIntroFx'); } catch (e) {}
			try { core.deleteCanvas('mt4BossHud'); } catch (e) {}
			var ctx = core.createCanvas('mt4BossIntroFx', 0, 0, W, H, 180);
			if (!ctx) return;
			ctx.canvas.style.pointerEvents = 'none';
			try { core.lockControl(); } catch (e) {}

			function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
			function easeOut(t) { t = clamp(t, 0, 1); return 1 - Math.pow(1 - t, 3); }
			function easeInOut(t) { t = clamp(t, 0, 1); return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
			function safeSound(name) { try { core.playSound(name); } catch (e) {} }

			var particles = [];
			for (var i = 0; i < 90; i++) {
				var a = i * 2.399963 + 0.4;
				var r = 18 + (i % 17) * 6 + (i % 5) * 3;
				particles.push({
					a: a,
					r: r * scale,
					s: (1.2 + (i % 6) * 0.45) * scale,
					spin: (i % 2 ? 1 : -1) * (0.8 + (i % 9) * 0.07),
					color: i % 3 === 0 ? [255,80,60] : (i % 3 === 1 ? [170,80,255] : [255,210,90])
				});
			}

			function drawCurtain(t) {
				var p = easeOut(t / 850);
				ctx.fillStyle = 'rgba(0,0,0,' + (0.25 + p * 0.58).toFixed(3) + ')';
				ctx.fillRect(0, 0, W, H);
				var ch = H * 0.52 * p;
				var g = ctx.createLinearGradient(0, 0, 0, ch);
				g.addColorStop(0, 'rgba(0,0,0,1)');
				g.addColorStop(1, 'rgba(0,0,0,0.15)');
				ctx.fillStyle = g;
				ctx.fillRect(0, 0, W, ch);
			}

			function drawVignette(t) {
				var p = clamp((t - 600) / 1200, 0, 1);
				var g = ctx.createRadialGradient(cx, cy, Math.min(W, H) * 0.18, cx, cy, Math.max(W, H) * 0.72);
				g.addColorStop(0, 'rgba(0,0,0,0)');
				g.addColorStop(1, 'rgba(0,0,0,' + (0.78 * p).toFixed(3) + ')');
				ctx.fillStyle = g;
				ctx.fillRect(0, 0, W, H);
			}

			function drawMagicCircle(t) {
				var p = easeOut((t - 650) / 1200);
				if (p <= 0) return;
				ctx.save();
				ctx.translate(cx, cy);
				ctx.globalCompositeOperation = 'lighter';
				ctx.globalAlpha = p;
				var time = t / 1000;
				for (var layer = 0; layer < 3; layer++) {
					var rr = (48 + layer * 23 + Math.sin(time * 2 + layer) * 3) * scale;
					ctx.rotate((layer % 2 ? -1 : 1) * time * (0.55 + layer * 0.15));
					ctx.strokeStyle = layer === 0 ? 'rgba(120,220,255,0.85)' : (layer === 1 ? 'rgba(190,90,255,0.65)' : 'rgba(255,210,80,0.55)');
					ctx.lineWidth = Math.max(1, (2 - layer * 0.25) * scale);
					ctx.beginPath(); ctx.arc(0, 0, rr, 0, Math.PI * 2); ctx.stroke();
					var n = 6 + layer * 2;
					ctx.beginPath();
					for (var i = 0; i <= n; i++) {
						var a = Math.PI * 2 * i / n;
						var x = Math.cos(a) * rr, y = Math.sin(a) * rr;
						if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
					}
					ctx.stroke();
				}
				var runes = ['◆','◇','✦','✧','✶','✷','✹','✺'];
				ctx.font = 'bold ' + Math.max(10, Math.round(14 * scale)) + 'px serif';
				ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				for (var j = 0; j < 12; j++) {
					var a2 = Math.PI * 2 * j / 12 - time * 0.65;
					var rx = Math.cos(a2) * 86 * scale, ry = Math.sin(a2) * 86 * scale;
					ctx.fillStyle = j % 2 ? 'rgba(150,220,255,0.95)' : 'rgba(255,210,100,0.9)';
					ctx.fillText(runes[j % runes.length], rx, ry);
				}
				ctx.restore();
			}

			function drawCracks(t) {
				var p = easeOut((t - 1200) / 1400);
				if (p <= 0) return;
				ctx.save();
				ctx.strokeStyle = 'rgba(255,85,55,' + (0.55 * p).toFixed(3) + ')';
				ctx.lineWidth = Math.max(1, 2 * scale);
				ctx.shadowColor = 'rgba(255,40,30,0.8)';
				ctx.shadowBlur = 8 * scale;
				for (var i = 0; i < 14; i++) {
					var a = i * Math.PI * 2 / 14 + Math.sin(i * 4.1) * 0.15;
					var len = (48 + (i % 5) * 20) * scale * p;
					var steps = 4 + (i % 3);
					ctx.beginPath();
					ctx.moveTo(cx, cy);
					for (var s = 1; s <= steps; s++) {
						var k = s / steps;
						var bend = Math.sin(i * 7 + s * 2.3) * 8 * scale;
						var x = cx + Math.cos(a) * len * k + Math.cos(a + Math.PI / 2) * bend;
						var y = cy + Math.sin(a) * len * k + Math.sin(a + Math.PI / 2) * bend;
						ctx.lineTo(x, y);
					}
					ctx.stroke();
				}
				ctx.restore();
			}

			function drawBeam(t) {
				var p = easeInOut((t - 1750) / 1050);
				var fade = 1 - clamp((t - 3300) / 1200, 0, 1);
				var a = p * fade;
				if (a <= 0) return;
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				var bw = (42 + Math.sin(t / 90) * 5) * scale;
				var g = ctx.createLinearGradient(cx - bw, 0, cx + bw, 0);
				g.addColorStop(0, 'rgba(80,180,255,0)');
				g.addColorStop(0.5, 'rgba(190,240,255,' + (0.72 * a).toFixed(3) + ')');
				g.addColorStop(1, 'rgba(80,180,255,0)');
				ctx.fillStyle = g;
				ctx.fillRect(cx - bw, 0, bw * 2, H);
				ctx.restore();
			}

			function drawBossModel(t) {
				var p = easeOut((t - 1600) / 1200);
				if (p <= 0) return;
				ctx.save();
				ctx.translate(cx, cy - 3 * scale);
				var breath = 1 + Math.sin(t / 260) * 0.025;
				ctx.scale(p * breath, p * breath);
				ctx.globalCompositeOperation = 'source-over';
				var body = ctx.createRadialGradient(0, 5 * scale, 10 * scale, 0, 18 * scale, 92 * scale);
				body.addColorStop(0, 'rgba(70,0,95,0.95)');
				body.addColorStop(0.55, 'rgba(20,0,35,0.96)');
				body.addColorStop(1, 'rgba(0,0,0,0)');
				ctx.fillStyle = body;
				ctx.beginPath(); ctx.ellipse(0, 18 * scale, 86 * scale, 76 * scale, 0, 0, Math.PI * 2); ctx.fill();
				ctx.fillStyle = 'rgba(5,0,15,0.98)';
				ctx.beginPath();
				ctx.moveTo(-72 * scale, -18 * scale);
				ctx.lineTo(-112 * scale, -64 * scale);
				ctx.lineTo(-55 * scale, -44 * scale);
				ctx.lineTo(0, -82 * scale);
				ctx.lineTo(55 * scale, -44 * scale);
				ctx.lineTo(112 * scale, -64 * scale);
				ctx.lineTo(72 * scale, -18 * scale);
				ctx.closePath(); ctx.fill();
				ctx.globalCompositeOperation = 'lighter';
				var eg = ctx.createRadialGradient(0, -3 * scale, 0, 0, -3 * scale, 36 * scale);
				eg.addColorStop(0, 'rgba(255,255,210,1)');
				eg.addColorStop(0.25, 'rgba(255,60,35,0.95)');
				eg.addColorStop(1, 'rgba(255,30,20,0)');
				ctx.fillStyle = eg;
				ctx.beginPath(); ctx.ellipse(0, -3 * scale, 52 * scale, 23 * scale, 0, 0, Math.PI * 2); ctx.fill();
				ctx.globalCompositeOperation = 'source-over';
				ctx.fillStyle = 'rgba(0,0,0,0.95)';
				ctx.beginPath(); ctx.ellipse(Math.sin(t / 300) * 5 * scale, -3 * scale, 8 * scale, 22 * scale, 0, 0, Math.PI * 2); ctx.fill();
				ctx.restore();
			}

			function drawParticles(t) {
				var p = clamp((t - 2450) / 1200, 0, 1);
				if (p <= 0) return;
				var fade = 1 - clamp((t - 4200) / 1200, 0, 1);
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				particles.forEach(function (q) {
					var d = q.r * easeOut(p);
					var x = cx + Math.cos(q.a + t * 0.00015 * q.spin) * d;
					var y = cy + Math.sin(q.a + t * 0.00015 * q.spin) * d;
					var col = q.color;
					ctx.fillStyle = 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',' + (0.85 * fade).toFixed(3) + ')';
					ctx.beginPath(); ctx.arc(x, y, q.s * (1 + p), 0, Math.PI * 2); ctx.fill();
				});
				ctx.restore();
			}

			function drawName(t) {
				var p = easeOut((t - 2700) / 800) * (1 - clamp((t - 5600) / 900, 0, 1) * 0.15);
				if (p <= 0) return;
				ctx.save();
				ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				ctx.font = 'bold ' + Math.max(20, Math.round(30 * scale)) + 'px serif';
				ctx.shadowColor = 'rgba(255,0,0,0.85)'; ctx.shadowBlur = 14 * scale;
				ctx.globalAlpha = p;
				ctx.strokeStyle = 'rgba(0,0,0,0.95)'; ctx.lineWidth = 5 * scale;
				ctx.strokeText(bossName, cx, H * 0.72);
				ctx.fillStyle = '#FFD56A';
				ctx.fillText(bossName, cx, H * 0.72);
				ctx.restore();
			}

			function drawHpBar(t) {
				var appear = easeOut((t - 3600) / 650);
				if (appear <= 0) return;
				var fillP = clamp((t - hpStart) / (hpEnd - hpStart), 0, 1);
				var seg = Math.floor(fillP * 6 + 0.0001);
				if (fillP >= 1) seg = 6;
				if (seg > lastBeep) {
					for (var b = lastBeep; b < seg; b++) safeSound('光标移动');
					lastBeep = seg;
				}
				ctx.save();
				ctx.globalAlpha = appear;
				// 直接复用HUD绘制思路，画在入场动画层上。
				var x = W * 0.13, y = 10 * scale, w = W * 0.74, h = 22 * scale, titleH = 13 * scale;
				ctx.fillStyle = 'rgba(0,0,0,0.9)';
				ctx.fillRect(x - 8 * scale, y - 4 * scale, w + 16 * scale, h + titleH + 9 * scale);
				ctx.strokeStyle = 'rgba(255,225,120,0.95)'; ctx.lineWidth = Math.max(1, 2 * scale);
				ctx.strokeRect(x - 8 * scale, y - 4 * scale, w + 16 * scale, h + titleH + 9 * scale);
				ctx.font = 'bold ' + Math.max(9, Math.round(10 * scale)) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
				ctx.fillStyle = '#FFD76A'; ctx.fillText(bossName, x + w / 2, y + 5 * scale);
				var gap = 3 * scale, segW = (w - gap * 5) / 6, sy = y + titleH;
				for (var i = 0; i < 6; i++) {
					var sx = x + i * (segW + gap);
					ctx.fillStyle = 'rgba(35,8,10,0.95)'; ctx.fillRect(sx, sy, segW, h);
					ctx.strokeStyle = 'rgba(255,210,120,0.65)'; ctx.strokeRect(sx, sy, segW, h);
					if (i < seg) {
						var g = ctx.createLinearGradient(sx, sy, sx, sy + h);
						g.addColorStop(0, '#FFB0A0'); g.addColorStop(0.18, '#FF3E35'); g.addColorStop(0.65, '#B00018'); g.addColorStop(1, '#4A0008');
						ctx.fillStyle = g; ctx.fillRect(sx + 2 * scale, sy + 2 * scale, segW - 4 * scale, h - 4 * scale);
						ctx.fillStyle = 'rgba(255,255,255,0.28)'; ctx.fillRect(sx + 3 * scale, sy + 3 * scale, segW - 6 * scale, Math.max(1, h * 0.18));
					}
				}
				ctx.restore();
			}

			function updateShake(t) {
				var p = 0;
				if (t > 1750 && t < 3350) p = (1 - Math.abs(t - 2550) / 800);
				p = clamp(p, 0, 1);
				if (p > 0 && core.dom && core.dom.gameDraw) {
					var amp = 4.5 * scale * p;
					var dx = (Math.random() - 0.5) * 2 * amp;
					var dy = (Math.random() - 0.5) * 2 * amp;
					try { core.dom.gameDraw.style.transform = 'translate(' + dx.toFixed(1) + 'px,' + dy.toFixed(1) + 'px)'; } catch (e) {}
				} else if (core.dom && core.dom.gameDraw) {
					try { core.dom.gameDraw.style.transform = ''; } catch (e) {}
				}
			}

			function frame(ts) {
				if (start == null) start = ts;
				var t = ts - start;
				ctx.clearRect(0, 0, W, H);
				drawCurtain(t);
				drawVignette(t);
				drawCracks(t);
				drawMagicCircle(t);
				drawBeam(t);
				drawBossModel(t);
				drawParticles(t);
				drawName(t);
				drawHpBar(t);
				updateShake(t);
				if (t < total && core.status && core.status.floorId === 'MT4') {
					requestAnimationFrame(frame);
				} else {
					try { if (core.dom && core.dom.gameDraw) core.dom.gameDraw.style.transform = ''; } catch (e) {}
					try { core.deleteCanvas('mt4BossIntroFx'); } catch (e) {}
					try { core.plugin.setMT4BossHpSegments(6); } catch (e) { try { core.plugin.drawMT4BossHud(6); } catch (e2) {} }
					try { core.plugin.showMT4BossBody(); } catch (e) {}
					if (!wasLocked) {
						try { core.unlockControl(); } catch (e) {}
					}
					if (callback) callback();
				}
			}

			safeSound('操作成功');
			requestAnimationFrame(frame);
		};
	}
,
	"monsterDodgeFx": function () {
		this.playMonsterDodgeFx = function (fromX, fromY, toX, toY, enemyId, callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!core.status || !core.status.floorId) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var name = 'monsterDodgeFx';
			try { core.deleteCanvas(name); } catch (e0) {}
			var ctx = core.createCanvas(name, 0, 0, W, H, 190);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var sx = (fromX * 32 + 16) * scale, sy = (fromY * 32 + 16) * scale;
			var tx = (toX * 32 + 16) * scale, ty = (toY * 32 + 16) * scale;
			var dx = tx - sx, dy = ty - sy;
			var ang = Math.atan2(dy, dx);
			var start = null, total = 360;
			var particles = [];
			for (var i = 0; i < 36; i++) {
				var a = ang + Math.PI + (Math.random() - 0.5) * 1.6;
				particles.push({
					x: sx + (Math.random() - 0.5) * 12 * scale,
					y: sy + (Math.random() - 0.5) * 12 * scale,
					vx: Math.cos(a) * (1.3 + Math.random() * 2.2) * scale,
					vy: Math.sin(a) * (1.3 + Math.random() * 2.2) * scale,
					r: (1.2 + Math.random() * 2.2) * scale,
					life: Math.random() * 0.10
				});
			}
			function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
			function easeOut(v){ v=clamp(v,0,1); return 1-Math.pow(1-v,3); }
			function easeInOut(v){ v=clamp(v,0,1); return v<0.5 ? 2*v*v : 1-Math.pow(-2*v+2,2)/2; }
			function drawWindRing(x, y, r, a, rot) {
				ctx.save(); ctx.translate(x, y); ctx.rotate(rot || 0); ctx.globalAlpha = a; ctx.globalCompositeOperation = 'lighter';
				ctx.strokeStyle = 'rgba(170,255,150,0.95)'; ctx.lineWidth = 2.2 * scale;
				for (var k = 0; k < 4; k++) {
					ctx.rotate(Math.PI / 2);
					ctx.beginPath(); ctx.arc(0, 0, r, -0.25, 1.25); ctx.stroke();
				}
				ctx.strokeStyle = 'rgba(255,255,255,0.75)'; ctx.lineWidth = 1.1 * scale;
				ctx.beginPath(); ctx.arc(0, 0, r * 0.62, 0, Math.PI * 2); ctx.stroke();
				ctx.restore();
			}
			function drawMonsterGhost(x, y, p, alpha) {
				ctx.save(); ctx.globalAlpha = alpha; ctx.globalCompositeOperation = 'lighter';
				ctx.shadowColor = 'rgba(145,255,120,0.95)'; ctx.shadowBlur = 16 * scale;
				var r = (10 + Math.sin(p * Math.PI) * 2) * scale;
				var g = ctx.createRadialGradient(x - 3*scale, y - 5*scale, 1, x, y, r * 1.4);
				g.addColorStop(0, 'rgba(255,255,255,0.92)');
				g.addColorStop(0.32, 'rgba(110,255,145,0.82)');
				g.addColorStop(1, 'rgba(20,120,60,0.05)');
				ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
				ctx.shadowBlur = 0;
				ctx.strokeStyle = 'rgba(255,255,255,0.88)'; ctx.lineWidth = 1.4 * scale; ctx.stroke();
				ctx.restore();
			}
			function frame(ts) {
				if (start == null) {
					start = ts;
					try { core.playSound('跳跃'); } catch (e) { try { core.playSound('光标移动'); } catch (e2) {} }
				}
				var t = ts - start, p = clamp(t / total, 0, 1), q = easeInOut(p);
				ctx.clearRect(0, 0, W, H);
				var mx = sx + dx * q, my = sy + dy * q;
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				var beam = ctx.createLinearGradient(sx, sy, tx, ty);
				beam.addColorStop(0, 'rgba(190,255,150,0)');
				beam.addColorStop(0.45, 'rgba(150,255,130,' + (0.34 * (1-p)).toFixed(3) + ')');
				beam.addColorStop(1, 'rgba(255,255,255,0)');
				ctx.strokeStyle = beam; ctx.lineWidth = (5 + 5 * Math.sin(Math.PI * p)) * scale; ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(tx, ty); ctx.stroke();
				ctx.restore();
				drawWindRing(sx, sy, (12 + 34 * easeOut(p)) * scale, 0.8 * (1-p), -p * 5);
				drawWindRing(tx, ty, (8 + 22 * easeOut(p)) * scale, 0.25 + 0.55 * p, p * 4);
				for (var j = 0; j < 4; j++) {
					var gp = clamp(q - j * 0.08, 0, 1);
					drawMonsterGhost(sx + dx * gp, sy + dy * gp, gp, (0.16 + j*0.04) * (1 - p * 0.3));
				}
				drawMonsterGhost(mx, my, p, 0.72);
				ctx.save(); ctx.globalCompositeOperation = 'lighter';
				for (var i = 0; i < particles.length; i++) {
					var pt = particles[i], lp = clamp(p - pt.life, 0, 1);
					if (lp <= 0) continue;
					ctx.fillStyle = 'rgba(180,255,130,' + (0.65 * (1-lp)).toFixed(3) + ')';
					ctx.beginPath(); ctx.arc(pt.x + pt.vx * lp * 28, pt.y + pt.vy * lp * 28, pt.r * (1-lp*0.35), 0, Math.PI * 2); ctx.fill();
				}
				ctx.restore();
				if (p > 0.58) {
					var fp = clamp((p - 0.58) / 0.42, 0, 1);
					ctx.save(); ctx.globalAlpha = 1 - fp; ctx.font = 'bold ' + Math.round(15 * scale) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
					ctx.lineWidth = 4 * scale; ctx.strokeStyle = 'rgba(0,0,0,0.78)'; ctx.strokeText('+20', tx, ty - (20 + 20 * fp) * scale);
					ctx.fillStyle = 'rgba(140,255,100,0.98)'; ctx.fillText('+20', tx, ty - (20 + 20 * fp) * scale); ctx.restore();
				}
				if (p < 1 && core.status) requestAnimationFrame(frame);
				else {
					try { core.deleteCanvas(name); } catch (e3) {}
					if (callback) callback();
				}
			}
			requestAnimationFrame(frame);
		};
	},
	"monsterCowardiceFx": function () {
		this.playMonsterCowardiceFx = function (changes, floorId, callback) {
			if (main.mode == 'editor') { if (callback) callback(); return; }
			if (core.isReplaying && core.isReplaying()) { if (callback) callback(); return; }
			if (!changes || !changes.length || !core.status || core.status.floorId !== floorId) { if (callback) callback(); return; }
			var W = core._PX_ || core.__PIXELS__ || 416, H = core._PY_ || core.__PIXELS__ || 416;
			var scale = Math.min(W, H) / 416;
			var name = 'monsterCowardiceFx';
			try { core.deleteCanvas(name); } catch (e0) {}
			var ctx = core.createCanvas(name, 0, 0, W, H, 191);
			if (!ctx) { if (callback) callback(); return; }
			ctx.canvas.style.pointerEvents = 'none';
			var start = null, total = 820;
			var pts = [];
			for (var i = 0; i < changes.length; i++) {
				var c = changes[i];
				pts.push({
					x: (c.x * 32 + 16) * scale,
					y: (c.y * 32 + 16) * scale,
					delay: Math.min(260, i * 18),
					seed: (c.x * 37 + c.y * 53 + i * 11) % 1000
				});
			}
			function clamp(v,a,b){ return Math.max(a, Math.min(b, v)); }
			function easeOut(v){ v=clamp(v,0,1); return 1-Math.pow(1-v,3); }
			function drawOne(pt, t) {
				var local = t - pt.delay;
				if (local < 0) return;
				var p = clamp(local / 620, 0, 1), fade = 1 - clamp((local - 480) / 180, 0, 1);
				var q = easeOut(p);
				ctx.save();
				ctx.globalCompositeOperation = 'lighter';
				// 向内收缩的蓝紫畏缩气环
				ctx.strokeStyle = 'rgba(150,165,255,' + (0.75 * fade).toFixed(3) + ')';
				ctx.lineWidth = Math.max(1, 2.2 * scale);
				ctx.beginPath(); ctx.arc(pt.x, pt.y, (28 - 16 * q) * scale, 0, Math.PI * 2); ctx.stroke();
				ctx.strokeStyle = 'rgba(70,90,210,' + (0.55 * fade).toFixed(3) + ')';
				ctx.beginPath(); ctx.arc(pt.x, pt.y, (18 + 6 * Math.sin(p * Math.PI)) * scale, 0, Math.PI * 2); ctx.stroke();
				// 下坠的衰弱碎光
				for (var k = 0; k < 10; k++) {
					var a = (pt.seed + k * 61) * 0.017;
					var rx = Math.cos(a) * (7 + (k % 4) * 4) * scale;
					var yy = pt.y - 16 * scale + q * (22 + (k % 3) * 7) * scale;
					ctx.fillStyle = 'rgba(125,145,255,' + (0.48 * fade).toFixed(3) + ')';
					ctx.beginPath(); ctx.arc(pt.x + rx, yy, (1.1 + (k % 3) * 0.45) * scale, 0, Math.PI * 2); ctx.fill();
				}
				ctx.restore();
				// 文字提示
				if (local > 120) {
					var tp = clamp((local - 120) / 500, 0, 1), tf = 1 - clamp((local - 430) / 170, 0, 1);
					ctx.save(); ctx.globalAlpha = tf; ctx.font = 'bold ' + Math.round(12 * scale) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
					var ty = pt.y - (22 + 24 * tp) * scale;
					ctx.lineWidth = 4 * scale; ctx.strokeStyle = 'rgba(0,0,0,0.82)'; ctx.strokeText('攻防-1', pt.x, ty);
					ctx.fillStyle = 'rgba(160,175,255,0.98)'; ctx.fillText('攻防-1', pt.x, ty);
					ctx.restore();
				}
			}
			function frame(ts) {
				if (start == null) {
					start = ts;
					try { core.playSound('光标移动'); } catch (e) {}
				}
				var t = ts - start;
				ctx.clearRect(0, 0, W, H);
				for (var i = 0; i < pts.length; i++) drawOne(pts[i], t);
				if (t < total && core.status && core.status.floorId === floorId) requestAnimationFrame(frame);
				else {
					try { core.deleteCanvas(name); } catch (e2) {}
					if (callback) callback();
				}
			}
			requestAnimationFrame(frame);
		};
	}

}