From bdf47dae82144235985dd00b277d6487783ce208 Mon Sep 17 00:00:00 2001
From: unanmed <1319491857@qq.com>
Date: Tue, 3 Jan 2023 22:24:05 +0800
Subject: [PATCH] =?UTF-8?q?=E8=BF=BD=E9=80=90=E6=88=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
 public/libs/control.js        |   3 +-
 public/project/floors/MT14.js |   4 +-
 public/project/floors/MT15.js |   2 +-
 public/project/floors/MT16.js |  24 +-
 public/project/functions.js   |  21 +-
 public/project/plugins.js     | 595 ----------------------------------
 src/components/scroll.vue     |  36 +-
 src/data/desc.json            | 197 +++++++++--
 src/initPlugin.ts             |   4 +-
 src/plugin/chase/chase.ts     | 224 +++++++++++++
 src/plugin/chase/chase1.ts    | 587 +++++++++++++++++++++++++++++++++
 src/plugin/chase/data.ts      |  52 +++
 src/types/map.d.ts            |   7 +-
 src/types/plugin.d.ts         |   6 +
 src/types/ui.d.ts             |   4 +-
 15 files changed, 1096 insertions(+), 670 deletions(-)
 create mode 100644 src/plugin/chase/chase.ts
 create mode 100644 src/plugin/chase/chase1.ts
 create mode 100644 src/plugin/chase/data.ts
diff --git a/public/libs/control.js b/public/libs/control.js
index f5503f9..8c4f800 100644
--- a/public/libs/control.js
+++ b/public/libs/control.js
@@ -3677,7 +3677,8 @@ control.prototype._playBgm_play = function (bgm, startTime) {
     // 如果当前正在播放,且和本BGM相同,直接忽略
     if (
         core.musicStatus.playingBgm == bgm &&
-        !core.material.bgms[core.musicStatus.playingBgm].paused
+        !core.material.bgms[core.musicStatus.playingBgm].paused &&
+        !startTime
     ) {
         return;
     }
diff --git a/public/project/floors/MT14.js b/public/project/floors/MT14.js
index cc0db69..65be6af 100644
--- a/public/project/floors/MT14.js
+++ b/public/project/floors/MT14.js
@@ -20,7 +20,7 @@ main.floors.MT14=
             "name": "plot1.mp3"
         }
     ],
-    "parallelDo": "if (flags.chase) {\n\tcore.changeChaseView(true);\n}",
+    "parallelDo": "",
     "events": {
         "24,7": {
             "trigger": "action",
@@ -101,7 +101,7 @@ main.floors.MT14=
             "data": [
                 {
                     "type": "if",
-                    "condition": "flag:finishChase",
+                    "condition": "flag:finishChase1",
                     "true": [
                         {
                             "type": "function",
diff --git a/public/project/floors/MT15.js b/public/project/floors/MT15.js
index 602c927..a91ddc1 100644
--- a/public/project/floors/MT15.js
+++ b/public/project/floors/MT15.js
@@ -31,7 +31,7 @@ main.floors.MT15=
         "\t[野蛮人]\b[up,hero]山路开始崎岖多变了,要更小心一些"
     ],
     "eachArrive": [],
-    "parallelDo": "if (flags.chase) {\n\tcore.changeChaseView(true);\n}",
+    "parallelDo": "",
     "events": {
         "44,0": [
             "不愧是你!!!"
diff --git a/public/project/floors/MT16.js b/public/project/floors/MT16.js
index a0cd5c4..6497269 100644
--- a/public/project/floors/MT16.js
+++ b/public/project/floors/MT16.js
@@ -12,7 +12,7 @@ main.floors.MT16=
     "images": [],
     "ratio": 1,
     "defaultGround": "T331",
-    "bgm": null,
+    "bgm": "mount.mp3",
     "color": null,
     "weather": [
         "cloud",
@@ -37,7 +37,7 @@ main.floors.MT16=
             "type": "pauseBgm"
         }
     ],
-    "parallelDo": "if (flags.chase) {\n\tcore.changeChaseView(true);\n}",
+    "parallelDo": "",
     "events": {
         "23,19": [
             {
@@ -88,6 +88,9 @@ main.floors.MT16=
                 ],
                 "no": [
                     "追逐战后录像会进行自动修复,不用担心录像问题",
+                    {
+                        "type": "hideStatusBar"
+                    },
                     {
                         "type": "function",
                         "function": "function(){\ncore.status.maps.MT15.canFlyFrom = false\n}"
@@ -360,11 +363,11 @@ main.floors.MT16=
                 "time": 3000
             },
             {
-                "type": "autoSave"
+                "type": "function",
+                "function": "function(){\ncore.startChase(1);\n}"
             },
             {
-                "type": "function",
-                "function": "function(){\ncore.startChase();\n}"
+                "type": "autoSave"
             }
         ],
         "2,23": [
@@ -378,18 +381,11 @@ main.floors.MT16=
             "即将开始追逐战,最好打开背景音乐,有耳机尽量佩戴耳机,这样游戏体验更佳",
             "为了防止你撞上不该开的门,现在会将所有门打开,并删除所有物品",
             "追逐的时候不能用2技能,不能用楼传,逃跑后要原路返回山洞",
-            "逃跑方式:哪里爆炸走哪里",
-            "注意,如果失败了必须要刷新页面才能重新开始,否则会出问题",
-            "请认真跑,虽然开始追逐的时候有一个自动存档,但不能保证该存档不会出问题",
-            "逃跑时的路线基本固定,但可能有一定难度,过不去就直接跳就行了",
-            "注意!!!再说一遍!!!重新跑需要刷新页面!!!",
+            "追逐战分为两个难度,简单难度会显示逃跑路径,困难模式不显示,困难模式逃跑成功可以获得成就",
+            "前方会有大约40秒的剧情,之后开始追逐战并自动存档,如果逃跑失败需要重打,可以直接读自动存档",
             {
                 "type": "hide",
                 "remove": true
-            },
-            {
-                "type": "function",
-                "function": "function(){\ncore.initChase();\n}"
             }
         ]
     },
diff --git a/public/project/functions.js b/public/project/functions.js
index 17b4b98..2e90c3d 100644
--- a/public/project/functions.js
+++ b/public/project/functions.js
@@ -121,6 +121,8 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
             // 正在切换楼层过程中执行的操作;此函数的执行时间是“屏幕完全变黑“的那一刻
             // floorId为要切换到的楼层ID;heroLoc表示勇士切换到的位置
 
+            flags.floorChanging = true;
+
             // ---------- 此时还没有进行切换,当前floorId还是原来的 ---------- //
             var currentId = core.status.floorId || null; // 获得当前的floorId,可能为null
             var fromLoad = core.hasFlag('__fromLoad__'); // 是否是读档造成的切换
@@ -212,6 +214,13 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
             // 转换楼层结束的事件;此函数会在整个楼层切换完全结束后再执行
             // floorId是切换到的楼层
 
+            if (flags.onChase) {
+                flags.chaseTime ??= {};
+                flags.chaseTime[floorId] = Date.now();
+            }
+
+            flags.floorChanging = false;
+
             // 如果是读档,则进行检查(是否需要恢复事件)
             if (core.hasFlag('__fromLoad__')) {
                 core.events.recoverEvents(core.getFlag('__events__'));
@@ -1417,7 +1426,10 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
         },
         loadData: function (data, callback) {
             // 读档操作;从存储中读取了内容后的行为
-
+            if (window.flags && flags.onChase) {
+                flags.chase.end();
+                flags.onChase = true;
+            }
             // 重置游戏和路线
             core.resetGame(
                 data.hero,
@@ -1468,6 +1480,13 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
 
                 core.removeFlag('__fromLoad__');
                 if (callback) callback();
+
+                if (flags.onChase) {
+                    core.startChase(flags.chaseIndex);
+                    if (flags.chaseIndex === 1) {
+                        core.playBgm('escape.mp3', 43.5);
+                    }
+                }
             });
         },
         getStatusLabel: function (name) {
diff --git a/public/project/plugins.js b/public/project/plugins.js
index 9e43239..ecaa78b 100644
--- a/public/project/plugins.js
+++ b/public/project/plugins.js
@@ -3411,601 +3411,6 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
             this._checkBlock_repulse(core.status.checkBlock.repulse[loc]);
         };
     },
-    chase: function () {
-        // 山野追逐战
-        // 初始变量
-        // 视野路线 x, y, frame
-        var route = [
-            [10, 10, 0],
-            [0, 10, 100],
-            [0, 10, 200],
-            [49, 0, 500],
-            [49, 0, 550],
-            [45, 0, 640],
-            [40, 0, 760],
-            [40, 0, 820],
-            [41, 0, 850],
-            [37, 0, 950],
-            [31, 0, 1000],
-            [29, 0, 1020],
-            [29, 0, 1210],
-            [25, 0, 1270],
-            [12, 0, 1330],
-            [0, 0, 1470],
-            [0, 0, 2000],
-            [113, 0, 2500],
-            [109, 0, 2580],
-            [104, 0, 2600],
-            [104, 0, 2830],
-            [92, 0, 3000],
-            [84, 0, 3120],
-            [74, 0, 3300],
-            [65, 0, 3480],
-            [58, 0, 3600],
-            [47, 0, 3800],
-            [36, 0, 4000],
-            [0, 0, 4600]
-        ];
-        // 效果函数
-        var funcs = [[0, wolfRun], [550, shake1], [10000000]];
-        var parrallels = [para1, para2]; // 并行脚本
-        var speed = 0; // 速度
-        var index = 0; // 当前要到达的索引
-        var fIndex = 0; // 函数索引
-        var frame = 0; // 帧数
-        var acc = 0; // 加速度
-        var currX = route[0][1] * 32; // 当前x轴
-        var inBlack = false;
-        var x = core.getHeroLoc('x');
-        var y = core.getHeroLoc('y'); // 勇士坐标
-        // 初始化,删除门和道具
-        this.initChase = function () {
-            speed = 0; // 速度
-            index = 0; // 当前要到达的索引
-            fIndex = 0; // 函数索引
-            frame = 0; // 帧数
-            acc = 0; // 加速度
-            currX = route[0][1] * 32; // 当前x轴
-            inBlack = false;
-            x = core.getHeroLoc('x');
-            y = core.getHeroLoc('y'); // 勇士坐标
-            // 循环删除
-            for (var i = 13; i < 16; i++) {
-                var floorId = 'MT' + i;
-                // 不可瞬移
-                core.status.maps[floorId].cannotMoveDirectly = true;
-                core.extractBlocks(floorId);
-                for (
-                    var j = 0;
-                    j < core.status.maps[floorId].blocks.length;
-                    j++
-                ) {
-                    var block = core.status.maps[floorId].blocks[j];
-                    var cls = block.event.cls,
-                        id = block.event.id;
-                    if (
-                        (cls == 'animates' || cls == 'items') &&
-                        !id.endsWith('Portal')
-                    ) {
-                        core.removeBlock(block.x, block.y, floorId);
-                        j--;
-                    }
-                }
-            }
-        };
-        // 函数们
-        function wolfRun() {
-            core.moveBlock(
-                23,
-                17,
-                ['down', 'down', 'down', 'down', 'down', 'down'],
-                80
-            );
-            setTimeout(() => {
-                core.setBlock(508, 23, 23);
-                core.moveBlock(
-                    23,
-                    23,
-                    [
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left',
-                        'left'
-                    ],
-                    80,
-                    true
-                );
-            }, 500);
-        }
-        // MT15函数1
-        function shake1() {
-            core.vibrate('vertical', 1000, 25, 2);
-            for (var tx = 53; tx < 58; tx++) {
-                for (var ty = 3; ty < 8; ty++) {
-                    core.setBlock(336, tx, ty);
-                }
-            }
-            core.drawAnimate('explosion3', 55, 5);
-            core.drawAnimate('stone', 55, 5);
-            setTimeout(() => {
-                core.setBlock(336, 58, 9);
-                core.setBlock(336, 59, 9);
-                core.drawAnimate('explosion1', 58, 9);
-                core.drawAnimate('explosion1', 59, 9);
-            }, 250);
-            setTimeout(() => {
-                core.setBlock(336, 53, 8);
-                core.setBlock(336, 52, 8);
-                core.drawAnimate('explosion1', 53, 8);
-                core.drawAnimate('explosion1', 52, 8);
-            }, 360);
-            setTimeout(() => {
-                core.setBlock(336, 51, 7);
-                core.drawAnimate('explosion1', 51, 7);
-            }, 750);
-            setTimeout(() => {
-                core.vibrate('vertical', 6000, 25, 1);
-                core.setBlock(336, 47, 7);
-                core.setBlock(336, 49, 9);
-                core.drawAnimate('explosion1', 49, 9);
-                core.drawAnimate('explosion1', 47, 7);
-            }, 1000);
-        }
-        // 并行1
-        function para1() {
-            if (core.status.floorId != 'MT15') return;
-            if (x == 45 && y == 8 && !flags.p11) {
-                core.setBlock(336, 45, 9);
-                core.drawAnimate('explosion1', 45, 9);
-                flags.p11 = true;
-            }
-            if (x == 45 && y == 6 && !flags.p12) {
-                core.setBlock(336, 44, 6);
-                core.drawAnimate('explosion1', 44, 6);
-                flags.p12 = true;
-            }
-            if (x == 45 && y == 4 && !flags.p13) {
-                core.setBlock(336, 44, 4);
-                core.drawAnimate('explosion1', 44, 4);
-                core.drawAnimate('explosion1', 48, 6);
-                core.removeBlock(48, 6);
-                flags.p13 = true;
-            }
-            if (x == 41 && y == 3 && !flags.p14) {
-                core.setBlock(336, 41, 4);
-                core.setBlock(336, 32, 6);
-                core.drawAnimate('explosion1', 41, 4);
-                core.drawAnimate('explosion1', 32, 6);
-                flags.p14 = true;
-            }
-            if (x == 35 && y == 3 && !flags.p15) {
-                core.drawAnimate('explosion3', 37, 7);
-                core.vibrate('vertical', 1000, 25, 10);
-                for (var tx = 36; tx < 42; tx++) {
-                    for (var ty = 4; ty < 11; ty++) {
-                        core.setBlock(336, tx, ty);
-                    }
-                }
-                flags.p15 = true;
-            }
-            if (x == 31 && y == 5 && !flags.p16) {
-                core.vibrate('vertical', 10000, 25, 1);
-                core.removeBlock(34, 8);
-                core.removeBlock(33, 8);
-                core.drawAnimate('explosion1', 34, 8);
-                core.drawAnimate('explosion1', 33, 8);
-                flags.p16 = true;
-            }
-            if (x == 33 && y == 7 && !flags.p17) {
-                core.setBlock(336, 32, 9);
-                core.drawAnimate('explosion1', 32, 9);
-                flags.p17 = true;
-            }
-            if ((x == 33 || x == 34 || x == 35) && y == 9 && !flags.p18) {
-                core.removeBlock(32, 9);
-                core.drawAnimate('explosion1', 32, 9);
-                flags.p18 = true;
-            }
-            if (x > 18 && x < 31 && y == 11 && !flags['p19' + x]) {
-                core.setBlock(336, x + 1, 11);
-                core.drawAnimate('explosion1', x + 1, 11);
-                flags['p19' + x] = true;
-            }
-        }
-        // 并行2
-        function para2() {
-            if (core.status.floorId != 'MT14') return;
-            if (x == 126 && y == 7 && !flags.p21) {
-                core.setBlock(336, 126, 6);
-                core.setBlock(336, 124, 6);
-                core.setBlock(336, 124, 9);
-                core.setBlock(336, 126, 9);
-                core.drawAnimate('explosion1', 126, 6);
-                core.drawAnimate('explosion1', 124, 6);
-                core.drawAnimate('explosion1', 124, 9);
-                core.drawAnimate('explosion1', 126, 9);
-                flags.p21 = true;
-            }
-            if (x == 123 && y == 7 && !flags.p22) {
-                core.setBlock(508, 127, 7);
-                core.jumpBlock(127, 7, 112, 7, 500, true);
-                setTimeout(() => {
-                    core.setBlock(509, 112, 7);
-                }, 520);
-                core.drawHeroAnimate('amazed');
-                core.setBlock(336, 121, 6);
-                core.setBlock(336, 122, 6);
-                core.setBlock(336, 120, 8);
-                core.setBlock(336, 121, 8);
-                core.setBlock(336, 122, 8);
-                core.drawAnimate('explosion1', 121, 6);
-                core.drawAnimate('explosion1', 122, 6);
-                core.drawAnimate('explosion1', 120, 8);
-                core.drawAnimate('explosion1', 121, 8);
-                core.drawAnimate('explosion1', 122, 8);
-                flags.p22 = true;
-            }
-            if (x == 110 && y == 10 && !flags.p23) {
-                core.setBlock(336, 109, 11);
-                core.removeBlock(112, 8);
-                core.drawAnimate('explosion1', 109, 11);
-                core.drawAnimate('explosion1', 112, 8);
-                core.insertAction([
-                    { type: 'moveHero', time: 400, steps: ['backward:1'] }
-                ]);
-                flags.p23 = true;
-            }
-            if (x == 112 && y == 8 && !flags.p24 && flags.p23) {
-                core.jumpBlock(112, 7, 110, 4, 500, true);
-                core.drawHeroAnimate('amazed');
-                setTimeout(() => {
-                    core.setBlock(506, 110, 4);
-                }, 540);
-                flags.p24 = true;
-            }
-            if (x == 118 && y == 7 && !flags.p25) {
-                core.setBlock(336, 117, 6);
-                core.setBlock(336, 116, 6);
-                core.setBlock(336, 115, 6);
-                core.setBlock(336, 114, 6);
-                core.setBlock(336, 117, 8);
-                core.setBlock(336, 116, 8);
-                core.drawAnimate('explosion1', 117, 6);
-                core.drawAnimate('explosion1', 116, 6);
-                core.drawAnimate('explosion1', 115, 6);
-                core.drawAnimate('explosion1', 114, 6);
-                core.drawAnimate('explosion1', 116, 8);
-                core.drawAnimate('explosion1', 117, 8);
-                flags.p25 = true;
-            }
-            if (x == 112 && y == 7 && !flags.p26) {
-                core.setBlock(336, 112, 8);
-                core.setBlock(336, 113, 7);
-                core.drawAnimate('explosion1', 112, 8);
-                core.drawAnimate('explosion1', 113, 7);
-                flags.p26 = true;
-            }
-            if (x == 115 && y == 7 && !flags.p39) {
-                for (var tx = 111; tx <= 115; tx++) {
-                    core.setBlock(336, tx, 10);
-                    core.drawAnimate('explosion1', tx, 10);
-                }
-                core.setBlock(336, 112, 8);
-                core.drawAnimate('explosion1', 112, 8);
-                flags.p39 = true;
-            }
-            if (x == 110 && y == 7 && !flags.p27) {
-                core.jumpBlock(97, 4, 120, -3, 2000);
-                for (var tx = 109; tx <= 120; tx++) {
-                    for (var ty = 3; ty <= 11; ty++) {
-                        if (ty == 7) continue;
-                        core.setBlock(336, tx, ty);
-                    }
-                }
-                core.vibrate('vertical', 3000, 25, 10);
-                core.drawAnimate('explosion2', 119, 7);
-                core.insertAction([
-                    {
-                        type: 'autoText',
-                        text: '\t[原始人]\b[down,hero]卧槽!!吓死我了!!',
-                        time: 600
-                    }
-                ]);
-                core.removeBlock(105, 7);
-                core.drawAnimate('explosion1', 105, 7);
-                flags.p27 = true;
-            }
-            if (x == 97 && y == 3 && !flags.p28) {
-                core.setBlock(336, 95, 3);
-                core.setBlock(336, 93, 6);
-                core.drawAnimate('explosion1', 95, 3);
-                core.drawAnimate('explosion1', 93, 6);
-                flags.p28 = true;
-            }
-            if (x == 88 && y == 6 && !flags.p29) {
-                core.setBlock(336, 87, 4);
-                core.setBlock(336, 88, 5);
-                core.drawAnimate('explosion1', 87, 4);
-                core.drawAnimate('explosion1', 88, 5);
-                flags.p29 = true;
-            }
-            if (x == 86 && y == 6 && !flags.p30) {
-                core.setBlock(336, 84, 6);
-                core.setBlock(336, 85, 5);
-                core.setBlock(336, 86, 8);
-                core.drawAnimate('explosion1', 84, 6);
-                core.drawAnimate('explosion1', 85, 5);
-                core.drawAnimate('explosion1', 86, 8);
-                flags.p30 = true;
-            }
-            if (x == 81 && y == 9 && !flags.p31) {
-                core.setBlock(336, 81, 8);
-                core.setBlock(336, 82, 11);
-                core.drawAnimate('explosion1', 81, 8);
-                core.drawAnimate('explosion1', 82, 11);
-                flags.p31 = true;
-            }
-            if (x == 72 && y == 11 && !flags.p32) {
-                core.setBlock(336, 73, 8);
-                core.setBlock(336, 72, 4);
-                core.drawAnimate('explosion1', 73, 8);
-                core.drawAnimate('explosion1', 72, 4);
-                flags.p32 = true;
-            }
-            if (x == 72 && y == 7 && !flags.p33) {
-                for (var tx = 74; tx < 86; tx++) {
-                    for (var ty = 3; ty < 12; ty++) {
-                        core.setBlock(336, tx, ty);
-                    }
-                }
-                core.drawAnimate('explosion2', 79, 7);
-                core.vibrate('vertical', 4000, 25, 15);
-                setTimeout(() => {
-                    core.vibrate(10000, null, 4);
-                });
-                flags.p33 = true;
-            }
-            if (x == 68 && y == 5 && !flags.p34) {
-                core.setBlock(336, 68, 4);
-                core.setBlock(336, 67, 6);
-                core.drawAnimate('explosion1', 68, 4);
-                core.drawAnimate('explosion1', 67, 6);
-                flags.p34 = true;
-            }
-            if (x == 67 && y == 10 && !flags.p35) {
-                for (var tx = 65; tx <= 72; tx++) {
-                    for (var ty = 3; ty <= 9; ty++) {
-                        core.setBlock(336, tx, ty);
-                    }
-                }
-                core.setBlock(336, 72, 10);
-                core.setBlock(336, 72, 11);
-                core.drawAnimate('explosion3', 69, 5);
-                core.vibrate('vertical', 2000, 25, 7);
-                flags.p35 = true;
-            }
-            if (x == 64 && y == 11 && !flags.p36) {
-                core.setBlock(336, 63, 9);
-                core.setBlock(336, 60, 8);
-                core.setBlock(336, 56, 11);
-                core.drawAnimate('explosion1', 63, 9);
-                core.drawAnimate('explosion1', 60, 8);
-                core.drawAnimate('explosion1', 56, 11);
-                flags.p36 = true;
-            }
-            if (x == 57 && y == 9 && !flags.p37) {
-                for (tx = 58; tx <= 64; tx++) {
-                    for (var ty = 3; ty <= 11; ty++) {
-                        core.setBlock(336, tx, ty);
-                    }
-                }
-                core.drawAnimate('explosion2', 61, 7);
-                core.vibrate('vertical', 3000, 25, 12);
-                setTimeout(() => {
-                    core.vibrate(20000, null, 4);
-                }, 3000);
-                flags.p37 = true;
-            }
-            if (x <= 48 && !flags['p38' + x] && x >= 21) {
-                for (var ty = 3; ty <= 11; ty++) {
-                    core.setBlock(336, x + 4, ty);
-                    core.drawAnimate('explosion1', x + 4, ty);
-                }
-                flags['p38' + x] = true;
-            }
-            if (x == 21 && flags.p37) {
-                core.endChase();
-            }
-        }
-        // 开始追逐
-        this.startChase = function () {
-            flags.__lockViewport__ = true;
-            flags.chase = true;
-            speed = 0; // 速度
-            index = 0; // 当前要到达的索引
-            fIndex = 0; // 函数索引
-            frame = 0; // 帧数
-            acc = 0; // 加速度
-            currX = route[0][1] * 32; // 当前x轴
-            inBlack = false;
-            x = core.getHeroLoc('x');
-            y = core.getHeroLoc('y'); // 勇士坐标
-            core.values.moveSpeed = 100;
-            core.loadOneSound('quake.mp3');
-            core.drawHero();
-            core.pauseBgm();
-            core.playBgm('escape.mp3', 43.5);
-        };
-        // 视野变化 useAcc:是否匀变速
-        this.changeChaseView = function (useAcc) {
-            if (flags.haveLost) return;
-            var floorId = core.status.floorId;
-            if (frame >= 3600) useAcc = false;
-            // 刚进MT15时
-            if (floorId === 'MT15' && !inBlack) {
-                frame = 500;
-                index = 3;
-                fIndex = 1;
-                speed = 0;
-                acc = 0;
-                currX = 32 * 49;
-                inBlack = true;
-                core.setGameCanvasTranslate('hero', 224, 0);
-                flags.startShake = true;
-                core.playSound('地震');
-                core.insertAction([{ type: 'sleep', time: 500, noSkip: true }]);
-                var interval = setInterval(() => {
-                    core.playSound('地震');
-                    if (index >= route.length - 1) clearInterval(interval);
-                }, 15000);
-                core.vibrate('vertical', 1000, 25);
-                setTimeout(() => {
-                    core.blackEdge();
-                    core.insertAction([
-                        {
-                            type: 'autoText',
-                            text: '\t[原始人]\b[down,hero]糟糕,还地震了!',
-                            time: 1500
-                        },
-                        {
-                            type: 'autoText',
-                            text: '\t[原始人]\b[down,hero]快跑!',
-                            time: 1000
-                        }
-                    ]);
-                    flags.startShake = false;
-                }, 500);
-            }
-            // 超范围失败
-            if (x * 32 > currX + 480 + 64) {
-                flags.haveLost = true;
-                core.lose('逃跑失败');
-                return;
-            }
-            // 刚进MT14
-            if (floorId == 'MT14' && !flags.first14) {
-                frame = 2500;
-                index = 17;
-                fIndex = 2;
-                speed = 0;
-                acc = 0;
-                currX = 117 * 32;
-                core.vibrate('vertical', 10000, 25, 2);
-                core.setGameCanvasTranslate('hero', 224, 0);
-                flags.first14 = true;
-            }
-            // 停止运行
-            if (index >= route.length) return;
-            // 切换索引
-            if (frame > route[index][2]) {
-                index++;
-                if (index == 3 && floorId == 'MT16') {
-                    core.lose('逃跑失败');
-                }
-                if (index >= route.length) {
-                    return;
-                }
-                core.changeChaseIndex(useAcc);
-            }
-            // 碰到狼就死
-            if (floorId == 'MT16') {
-                if (x >= 6) {
-                    if (x > 25 - (frame - 29) / 5) {
-                        flags.haveLost = true;
-                        core.lose('逃跑失败');
-                        return;
-                    }
-                }
-            }
-            // 执行函数
-            if (frame == funcs[fIndex][0]) {
-                funcs[fIndex][1]();
-                fIndex++;
-            }
-            // 并行
-            for (var i in parrallels) {
-                parrallels[i]();
-            }
-            if (useAcc) speed += acc;
-            currX += speed;
-            if (floorId == 'MT16') core.setViewport(currX, 320);
-            else core.setViewport(currX, 0);
-            x = core.getHeroLoc('x');
-            y = core.getHeroLoc('y');
-            frame++;
-        };
-        // 路线索引切换
-        this.changeChaseIndex = function (useAcc) {
-            var fromR = route[index - 1],
-                toR = route[index];
-            var dt = toR[2] - fromR[2],
-                dx = (toR[0] - fromR[0]) * 32;
-            if (dx == 0) {
-                acc = 0;
-                speed = 0;
-            }
-            if (useAcc) {
-                acc = (2 * (dx - speed * dt)) / (dt * dt);
-            } else {
-                speed = dx / dt;
-            }
-        };
-        // 黑边
-        this.blackEdge = function () {
-            core.createCanvas('edge', 0, 0, 480, 480, 100);
-            var f = 0;
-            var h = 0,
-                s = 2.56;
-            // 初始动画
-            function start() {
-                core.clearMap('edge');
-                core.fillRect('edge', 0, 0, 480, h);
-                core.fillRect('edge', 0, 480, 480, -h);
-                f++;
-                s -= 0.0512;
-                h += s;
-                if (f == 50) clearInterval(interval);
-            }
-            var interval = setInterval(start, 20);
-        };
-        // 结束追逐
-        this.endChase = function () {
-            flags.chase = false;
-            flags.__lockViewport__ = false;
-            core.autoFixRouteBoss(false);
-            // 黑边消失
-            clearInterval(interval);
-            var f = 0;
-            var h = 64,
-                s = 0;
-            var interval = setInterval(() => {
-                core.clearMap('edge');
-                core.fillRect('edge', 0, 0, 480, h);
-                core.fillRect('edge', 0, 480, 480, -h);
-                f++;
-                s += 0.0512;
-                h -= s;
-                if (f == 50) {
-                    clearInterval(interval);
-                    core.deleteCanvas('edge');
-                    flags.finishChase = true;
-                }
-            });
-        };
-    },
     hotReload: function () {
         if (main.mode !== 'play' || main.replayChecking) return;
 
diff --git a/src/components/scroll.vue b/src/components/scroll.vue
index 7ce3db4..206d69c 100644
--- a/src/components/scroll.vue
+++ b/src/components/scroll.vue
@@ -88,27 +88,27 @@ function draw() {
  * 计算元素总长度
  */
 async function calHeight() {
-    await new Promise(res => {
-        const canvas = ctx.canvas;
-        const style2 = getComputedStyle(canvas);
-        canvas.style.width = `${width}px`;
-        canvas.width = width * scale;
-        canvas.height = parseFloat(style2.height) * scale;
-        if (props.noScroll) canvas.style.width = `0px`;
+    await sleep(20);
+    const canvas = ctx.canvas;
+    const style2 = getComputedStyle(canvas);
+    canvas.style.width = `${width}px`;
+    canvas.width = width * scale;
+    canvas.height = parseFloat(style2.height) * scale;
+    if (props.noScroll) canvas.style.width = `0px`;
 
-        if (props.type === 'horizontal') {
-            main.style.flexDirection = 'column';
-            canvas.style.height = `${width}px`;
-            canvas.style.width = '98%';
-            canvas.style.margin = '0 1% 0 1%';
-            canvas.width = parseFloat(style2.width) * scale;
-            canvas.height = width * scale;
-            if (props.noScroll) canvas.style.height = `0px`;
-        }
+    if (props.type === 'horizontal') {
+        main.style.flexDirection = 'column';
+        canvas.style.height = `${width}px`;
+        canvas.style.width = '98%';
+        canvas.style.margin = '0 1% 0 1%';
+        canvas.width = parseFloat(style2.width) * scale;
+        canvas.height = width * scale;
+        if (props.noScroll) canvas.style.height = `0px`;
+    }
+    await new Promise(res => {
         requestAnimationFrame(() => {
             const style = getComputedStyle(content);
             total = parseFloat(style[canvasAttr]);
-
             res('');
         });
     });
@@ -193,7 +193,7 @@ onMounted(async () => {
     useWheel(content, (x, y) => {
         fromSelf = true;
         const d = x !== 0 ? x : y;
-        if (Math.abs(d) > 50) {
+        if (Math.abs(d) > 30) {
             content.style.transition = `${cssTarget} 0.2s ease-out`;
         } else {
             content.style.transition = '';
diff --git a/src/data/desc.json b/src/data/desc.json
index 2c5d79d..ff7672d 100644
--- a/src/data/desc.json
+++ b/src/data/desc.json
@@ -6,29 +6,183 @@
             "这里显示本塔中需要注意的事项。",
             "
",
             "
",
-            "1. 本塔中几乎所有 ui 都可以纵向滚动,如果发现显示不全,",
+            "1. 本百科全书字数很多,可以选择性地阅读。",
+            "
",
+            "
",
+            "2. 本塔中几乎所有 ui 都可以纵向滚动,如果发现显示不全,",
             "可以尝试上下拖动,就像浏览网页一样。电脑端还可以使用滚轮上下滚动。",
             "大部分可以纵向滚动的 ui 都会在右方有一个滚动条,也可以拖动它进行滚动,例如本百科全书的条目列表和",
             "条目说明都是可以通过上述方式滚动的。",
             "
",
             "
",
-            "2. 本百科全书的内容会随着游戏的推进而增加新内容,",
+            "3. 本百科全书的内容会随着游戏的推进而增加新内容,",
             "同时每次增加新内容时都会有提示。",
             "
",
             "
",
-            "3. 本塔主要面向电脑端设计,",
+            "4. 本塔主要面向电脑端设计,",
             "建议使用电脑游玩以获得更好的游戏体验。但是手机依然可以游玩本塔,",
             "但部分操作可能不是很方便,ui 也可能不是很美观,不过依然可以完整体验本游戏。",
             "
",
             "
",
-            "4. 对于手机端,可以点击右下角的难度文字来切换工具栏至数字键。",
+            "5. 对于手机端,可以点击右下角的难度文字来切换工具栏至数字键。",
             "这样,你可以更加方便地进行使用技能等操作。",
             "
",
             "
",
-            "5. 本塔中几乎所有 ui 在打开时都会有一个0.6s的动画,如果不想要,可以在开头捡的系统设置里面关闭(默认关闭)。",
+            "6. 本塔中几乎所有 ui 在打开时都会有一个0.6s的动画,如果不想要,可以在开头捡的系统设置里面关闭(默认关闭)。",
             "同时,几乎所有 ui 的退出按钮都在左上角。"
         ]
     },
+    "tutorial": {
+        "text": "新手教程",
+        "condition": "true",
+        "desc": [
+            "本条目是魔塔游戏的新手教程,如果对魔塔有一定的了解,可以直接忽略。",
+            "
",
+            "
",
+            "魔塔是一种固定数值rpg游戏,在打怪的时候,遵循我打你一下,你打我一下",
+            "的原则,造成的伤害是己方攻击减去对方防御,最后怪物的伤害便是你在战斗中失去的生命值。当然,为了游戏体验,",
+            "战斗过程会被省略。",
+            "
",
+            "
",
+            "宝石可以增加你的属性,在大部分魔塔中,红宝石增加攻击,蓝宝石增加防御,本塔也不例外。血瓶可以增加你的生命值。",
+            "一般情况下,拾取宝物的优先级是红宝石 > 蓝宝石 > 血瓶,",
+            "但部分情况可能不是这样,这需要你自己的游玩经验等。",
+            "
",
+            "
",
+            "本塔还拥有升级机制,升级时能够给你增加大量的属性,因此,一般情况下当你接近升级时,需要尽快打怪升级。",
+            "
",
+            "
",
+            "然后是门。在魔塔中,很多门都不是必开的门,它们的作用一般是可以躲开怪物拿宝石,或者门里面有血瓶等。",
+            "当你血量足够时,这些门可以不用开,不然可能会有必开的门无法开启导致卡关。对于钥匙,每种颜色的钥匙开对应颜色的门,",
+            "价值是红 > 蓝 > 黄。",
+            "
",
+            "
",
+            "为了更加方便,本塔增加了宝石血瓶显示数据的功能,这样你可以清晰地知道每个宝石增加了多少属性。",
+            "
",
+            "
",
+            "下面是勇士基础属性的说明:",
+            "
",
+            "1. 生命值:",
+            "勇士的血量,当它归零时,游戏结束",
+            "
",
+            "2. 攻击:",
+            "勇士的攻击,攻击越高,每回合对怪物造成的伤害越高",
+            "
",
+            "3. 防御:",
+            "勇士的防御,防御越高,怪物每回合对你造成的伤害越低",
+            "
",
+            "4. 经验:",
+            "勇士的经验,到达一定值后会升级。本塔在状态栏中显示为距离升级剩余的经验",
+            "
",
+            "5. 金币:",
+            "勇士的金币,可以用于购买物品。本塔中在进入第二章后会有用",
+            "
",
+            "6. 护盾:",
+            "勇士的护盾,用处是能够在战后减少同等数值的伤害,可以使伤害变为负值。本塔中,在点开无上之盾技能后,",
+            "智慧会充当护盾。更多信息可以查看“勇士属性”条目。"
+        ]
+    },
+    "noun": {
+        "text": "名词解释",
+        "condition": "true",
+        "desc": [
+            "本条目会解释诸如临界等魔塔术语,对魔塔有一定了解的可以直接忽略。",
+            "
",
+            "
",
+            "1. 临界:",
+            "在魔塔中,临界是一个非常重要的东西。首先,我们很容易可以得到,吃攻击时只有当减少了战斗回合数时怪物的伤害会减少,",
+            "那么,吃攻击时怪物的减伤是不连续的。而距离下一次减少怪物的伤害需要加的攻击的量",
+            "便是临界。当我们吃一个攻击恰好使怪物伤害减少时,称为“踩临界”。一般情况下,踩临界的减伤要比吃防御要高,",
+            "因此,当能踩到临界时,我们应当先踩临界,再吃防御。",
+            "
",
+            "
",
+            "2. 加防:",
+            "加防指的是加防对怪物的减伤。在本塔中,会以“n防”的形式显示在怪物手册或其他地方。在本塔中,一般你不需要刻意计算",
+            "临界与加防减伤,你可以在怪物手册中查看减伤折线图,",
+            "更多信息请查看“怪物手册”条目。",
+            "
",
+            "
",
+            "3. 咸鱼:",
+            "一般来讲,开不必开的门,或者使用不必使用的道具被称为咸鱼,或者是咸门,咸道具。一般情况下,说“咸”便是指咸鱼。",
+            "一般情况下,门后面有宝石且无法通过其他方式进入的都是必开门,而只有血瓶的都是咸鱼门。"
+        ]
+    },
+    "shortcut": {
+        "text": "快捷键",
+        "condition": "true",
+        "desc": [
+            "这里包含本塔中所有的快捷键。对于手机端,可以点击工具栏的难度的位置切换工具栏至数字键。",
+            "
",
+            "
",
+            "下面是样板中的所有快捷键:",
+            "
",
+            "X:打开怪物手册",
+            "
",
+            "S:打开存档界面",
+            "
",
+            "D:打开读档界面",
+            "
",
+            "A或5:读取自动存档",
+            "
",
+            "W或6:撤销读取的自动存档",
+            "
",
+            "Q:打开装备栏",
+            "
",
+            "T:打开道具栏",
+            "
",
+            "G:打开楼层传送器",
+            "
",
+            "Z或单击勇士:勇士转向",
+            "
",
+            "空格或双击勇士或7:轻按(拾取勇士周围的宝物但不移动勇士)",
+            "
",
+            "Esc:打开游戏菜单",
+            "
",
+            "R:打开录像回放菜单",
+            "
",
+            "N:询问是否返回游戏主菜单",
+            "
",
+            "V:打开快捷商店",
+            "
",
+            "B:打开数据统计界面",
+            "
",
+            "Alt + 数字键:快速换装",
+            "
",
+            "PgUp或PgDn:浏览地图",
+            "
",
+            "P:打开评论区",
+            "
",
+            "
",
+            "下面是本塔中新增的快捷键:",
+            "
",
+            "M:快速标记怪物"
+        ]
+    },
+    "extraAttr": {
+        "text": "勇士属性",
+        "condition": "true",
+        "desc": [
+            "这里只对本塔中新增的勇士属性进行说明。",
+            "
",
+            "
",
+            "1. 智慧:",
+            "智慧是该塔的核心属性之一。智慧可用于智慧加点,该功能会在进入第一章后开启。使用智慧可以点技能树。",
+            "除此之外,智慧也有其它功能。例如点开无上之盾技能后智慧还可以充当护盾,第二章点开学习技能后可以使用智慧学习怪物技能等。",
+            "
",
+            "
",
+            "2. 生命回复:",
+            "生命回复指的是勇士每回合回复的生命值。当与怪物战斗时,勇士每回合都会回复对应量的生命值。因此,当吃攻击时,",
+            "与怪物战斗的回合数可能会减少,导致生命回复的总回复量减少。不过大部分情况下不需要在意这一点,",
+            "减少一回合并不会对吸的血造成很大的影响,除了一些特殊情况。",
+            "该项会显示在状态栏的生命值右方偏下的位置。",
+            "
",
+            "
",
+            "3. 额外攻击:",
+            "额外攻击指的是勇士每回合的额外造成的伤害。一般情况下,当勇士破了怪物的防御时,该项便会起作用。",
+            "额外攻击相当于魔攻,无法通过一般方式减免。当勇士攻击怪物时,每回合都会附加对应量的伤害,对坚固怪同样有效。",
+            "额外攻击会显示在状态栏的攻击右方偏下的位置。"
+        ]
+    },
     "statusBar": {
         "text": "状态栏",
         "condition": "true",
@@ -132,7 +286,7 @@
             "
",
             "点击一个怪物或者按下回车空格后,将进入怪物详细信息界面。这个界面分为多个栏,分别是特殊属性栏,详细临界栏,更多信息栏。",
             "进入怪物详细信息后默认在特殊属性栏,该栏可以查看怪物的特殊属性。",
-            "同时也是唯一一个会在点击后返回到怪物手册界面的栏,更多信息请查看本条目的最后一段。",
+            "同时也是唯一一个会在点击屏幕后会返回到怪物手册界面的栏,更多信息请查看本条目的最后一段。",
             "注意特殊属性依然可以纵向滚动。在特殊属性下方,",
             "是怪物的临界表,可以粗略地查看怪物的临界信息。在下方,你可以点击详细临界信息进入详细临界栏。",
             "
",
@@ -149,7 +303,7 @@
             "这一栏的核心功能是标记怪物。被标记的怪物会有一些非常方便的行为,这些行为可以在“标记怪物”条目中查看。",
             "
",
             "
",
-            "注意,在怪物详细信息中,只有特殊属性栏可以通过点击返回到怪物手册界面,详细临界与更多信息栏均不行。",
+            "注意,在怪物详细信息中,只有特殊属性栏可以通过点击屏幕返回到怪物手册界面,详细临界与更多信息栏均不行。",
             "如果你是电脑端,在任意栏目中按下X键会退出怪物手册,返回游戏,",
             "按下回车(Enter)键会回到怪物手册界面。"
         ]
@@ -168,8 +322,8 @@
             "首先,对于电脑端,最左侧显示区域信息,手机端则在上方的左侧。",
             "
",
             "
",
-            "然后,区域的右侧是小地图栏,这一栏会显示楼层的屏幕结构。你可以拖动,也可以使用滚轮或者双指放缩,当放缩到一定大小时,",
-            "会显示地图的缩略图。直接点击地图也可以选中地图,再次点击会传送至目标地图",
+            "然后,区域的右侧是小地图栏,这一栏会显示楼层的平面结构。你可以拖动,也可以使用滚轮或者双指放缩,当放缩到一定大小时,",
+            "会显示地图的缩略图。直接点击地图也可以选中地图,再次点击会传送至目标地图。",
             "
",
             "
",
             "对于电脑端,最右侧是当前选中的地图的缩略图,手机则在下方,点击缩略图也可以传送。缩略图的下方是当前选中的地图名,",
@@ -204,31 +358,6 @@
             "还会直接在勇士属性栏显示增加或减少的属性。"
         ]
     },
-    "extraAttr": {
-        "text": "勇士属性",
-        "condition": "true",
-        "desc": [
-            "这里只对本塔中新增的勇士属性进行说明。",
-            "
",
-            "
",
-            "1. 智慧:",
-            "智慧是该塔的核心属性之一。智慧可用于智慧加点,该功能会在进入第一章后开启。使用智慧可以点技能树。",
-            "除此之外,智慧也有其它功能。例如点开无上之盾技能后智慧还可以充当护盾,第二章点开学习技能后可以使用智慧学习怪物技能等。",
-            "
",
-            "
",
-            "2. 生命回复:",
-            "生命回复指的是勇士每回合回复的生命值。当与怪物战斗时,勇士每回合都会回复对应量的生命值。因此,当吃攻击时,",
-            "与怪物战斗的回合数可能会减少,导致生命回复的总回复量减少。不过大部分情况下不需要在意这一点,",
-            "减少一回合并不会对吸的血造成很大的影响,除了一些特殊情况。",
-            "该项会显示在状态栏的生命值右方偏下的位置。",
-            "
",
-            "
",
-            "3. 额外攻击:",
-            "额外攻击指的是勇士每回合的额外造成的伤害。一般情况下,当勇士破了怪物的防御时,该项便会起作用。",
-            "额外攻击相当于魔攻,无法通过一般方式减免。当勇士攻击怪物时,每回合都会附加对应量的伤害,对坚固怪同样有效。",
-            "额外攻击会显示在状态栏的攻击右方偏下的位置。"
-        ]
-    },
     "skillTree": {
         "text": "技能树",
         "condition": "flags.chapter > 0",
diff --git a/src/initPlugin.ts b/src/initPlugin.ts
index ea69e0f..8f9749d 100644
--- a/src/initPlugin.ts
+++ b/src/initPlugin.ts
@@ -9,6 +9,7 @@ import mark from './plugin/mark';
 import setting from './plugin/settings';
 import chapter from './plugin/ui/chapter';
 import fly from './plugin/ui/fly';
+import chase from './plugin/chase/chase';
 
 function forward() {
     // 每个引入的插件都要在这里执行,否则不会被转发
@@ -22,7 +23,8 @@ function forward() {
         mark(),
         setting(),
         chapter(),
-        fly()
+        fly(),
+        chase()
     ];
 
     // 初始化所有插件,并转发到core上
diff --git a/src/plugin/chase/chase.ts b/src/plugin/chase/chase.ts
new file mode 100644
index 0000000..4f6248a
--- /dev/null
+++ b/src/plugin/chase/chase.ts
@@ -0,0 +1,224 @@
+import { Animation, sleep, TimingFn } from 'mutate-animate';
+import { has } from '../utils';
+import { ChaseCameraData, ChasePath, getChaseDataByIndex } from './data';
+
+export default function init() {
+    return { startChase };
+}
+
+export function shake2(power: number, timing: TimingFn): TimingFn {
+    let r = 0;
+    return t => {
+        r += Math.PI / 2;
+        return Math.sin(r) * power * timing(t);
+    };
+}
+
+export class Chase {
+    /**
+     * 动画实例
+     */
+    ani: Animation = new Animation();
+
+    /**
+     * 追逐战的路径
+     */
+    path: ChasePath;
+
+    /**
+     * 是否展示路径
+     */
+    showPath: boolean = false;
+
+    /**
+     * 开始一个追逐战
+     * @param index 追逐战索引
+     * @param path 追逐战的路线
+     * @param fn 开始时执行的函数
+     */
+    constructor(
+        path: ChasePath,
+        fns: ((chase: Chase) => void)[],
+        camera: ChaseCameraData[],
+        showPath: boolean = false
+    ) {
+        this.path = path;
+        flags.__lockViewport__ = true;
+        flags.onChase = true;
+        flags.chaseTime = {
+            [core.status.floorId]: Date.now()
+        };
+        this.ani
+            .absolute()
+            .time(0)
+            .move(core.bigmap.offsetX / 32, core.bigmap.offsetY / 32);
+        fns.forEach(v => v(this));
+        const added: FloorIds[] = [];
+        const ctx = core.createCanvas('chasePath', 0, 0, 0, 0, 35);
+
+        for (const [id, x, y, start, time, mode, path] of camera) {
+            if (!added.includes(id)) {
+                this.on(
+                    id,
+                    0,
+                    () => {
+                        flags.__lockViewport__ = false;
+                        core.drawHero();
+                        flags.__lockViewport__ = true;
+                        this.ani
+                            .time(0)
+                            .move(
+                                core.bigmap.offsetX / 32,
+                                core.bigmap.offsetY / 32
+                            );
+                    },
+                    true
+                );
+                added.push(id);
+            }
+            if (!has(path)) {
+                this.on(id, start, () => {
+                    this.ani.time(time).mode(mode).move(x, y);
+                });
+            } else {
+                this.on(id, start, () => {
+                    this.ani.time(time).mode(mode).moveAs(path);
+                });
+            }
+        }
+
+        this.ani.ticker.add(() => {
+            if (!flags.floorChanging) {
+                core.setViewport(this.ani.x * 32, this.ani.y * 32);
+                core.relocateCanvas(ctx, -this.ani.x * 32, -this.ani.y * 32);
+            }
+        });
+
+        if (showPath) {
+            for (const [id, p] of Object.entries(path) as [
+                FloorIds,
+                LocArr[]
+            ][]) {
+                this.on(id, 0, () => {
+                    const floor = core.status.maps[id];
+                    core.resizeCanvas(ctx, floor.width * 32, floor.height * 32);
+                    ctx.beginPath();
+                    ctx.moveTo(p[0][0] * 32 + 16, p[1][1] * 32 + 24);
+                    ctx.lineJoin = 'round';
+                    ctx.lineWidth = 4;
+                    ctx.strokeStyle = 'cyan';
+                    ctx.globalAlpha = 0.3;
+                    p.forEach((v, i, a) => {
+                        if (i === 0) return;
+                        const [x, y] = v;
+                        ctx.lineTo(x * 32 + 16, y * 32 + 24);
+                    });
+                    ctx.stroke();
+                });
+            }
+        }
+    }
+
+    /**
+     * 在追逐战的某个时刻执行函数
+     * @param floorId 楼层id
+     * @param time 该楼层中经过的时间
+     * @param fn 执行的函数
+     */
+    on(
+        floorId: FloorIds,
+        time: number,
+        fn: (chase: Chase) => void,
+        first: boolean = false
+    ) {
+        const func = () => {
+            if (!flags.chaseTime?.[floorId]) return;
+            if (Date.now() - (flags.chaseTime?.[floorId] ?? 0) >= time) {
+                fn(this);
+                this.ani.ticker.remove(func);
+            }
+        };
+        this.ani.ticker.add(func, first);
+    }
+
+    /**
+     * 当勇士移动到某个点上时执行函数
+     * @param x 横坐标
+     * @param y 纵坐标
+     * @param floorId 楼层id
+     * @param fn 执行的函数
+     * @param mode 为0时,当传入数组时表示勇士在任意一个位置都执行,否则是每个位置执行一次
+     */
+    onHeroLoc(
+        floorId: FloorIds,
+        fn: (chase: Chase) => void,
+        x?: number | number[],
+        y?: number | number[],
+        mode: 0 | 1 = 0
+    ) {
+        if (mode === 1) {
+            if (typeof x === 'number') x = [x];
+            if (typeof y === 'number') y = [y];
+            x!.forEach(v => {
+                (y as number[]).forEach(vv => {
+                    this.onHeroLoc(floorId, fn, v, vv);
+                });
+            });
+            return;
+        }
+        const judge = () => {
+            if (core.status.floorId !== floorId) return false;
+            if (has(x)) {
+                if (typeof x === 'number') {
+                    if (core.status.hero.loc.x !== x) return false;
+                } else {
+                    if (!x.includes(core.status.hero.loc.x)) return false;
+                }
+            }
+            if (has(y)) {
+                if (typeof y === 'number') {
+                    if (core.status.hero.loc.y !== y) return false;
+                } else {
+                    if (!y.includes(core.status.hero.loc.y)) return false;
+                }
+            }
+            return true;
+        };
+        const func = () => {
+            if (judge()) {
+                fn(this);
+                try {
+                    this.ani.ticker.remove(func);
+                } catch {}
+            }
+        };
+        this.ani.ticker.add(func);
+    }
+
+    /**
+     * 设置路径显示状态
+     * @param show 是否显示路径
+     */
+    setPathShowStatus(show: boolean) {
+        this.showPath = show;
+    }
+
+    /**
+     * 结束这个追逐战
+     */
+    end() {
+        this.ani.ticker.destroy();
+        delete flags.onChase;
+        delete flags.chase;
+        flags.__lockViewport__ = false;
+        core.deleteCanvas('chasePath');
+    }
+}
+
+export async function startChase(index: number) {
+    const data = getChaseDataByIndex(index);
+    flags.chaseIndex = index;
+    flags.onChase = true;
+    await sleep(20);
+    flags.chase = new Chase(data.path, data.fns, data.camera);
+}
diff --git a/src/plugin/chase/chase1.ts b/src/plugin/chase/chase1.ts
new file mode 100644
index 0000000..a232ef2
--- /dev/null
+++ b/src/plugin/chase/chase1.ts
@@ -0,0 +1,587 @@
+import { Animation, bezier, hyper, linear, shake, sleep } from 'mutate-animate';
+import { Chase, shake2 } from './chase';
+import { ChaseCameraData } from './data';
+
+const ani = new Animation();
+ani.register('rect', 0);
+
+export const path1: Partial> = {
+    MT16: [
+        [23, 23],
+        [0, 23]
+    ],
+    MT15: [
+        [63, 4],
+        [61, 4],
+        [61, 5],
+        [58, 5],
+        [58, 8],
+        [54, 8],
+        [54, 11],
+        [51, 11],
+        [51, 8],
+        [45, 8],
+        [45, 4],
+        [47, 4],
+        [47, 6],
+        [51, 6],
+        [51, 5],
+        [52, 5],
+        [52, 3],
+        [50, 3],
+        [50, 5],
+        [48, 5],
+        [48, 3],
+        [35, 3],
+        [35, 5],
+        [31, 5],
+        [31, 7],
+        [34, 7],
+        [34, 9],
+        [31, 9],
+        [31, 11],
+        [12, 11],
+        [12, 8],
+        [1, 8],
+        [1, 7],
+        [0, 7]
+    ],
+    MT14: [
+        [127, 7],
+        [126, 7],
+        [126, 8],
+        [124, 8],
+        [124, 7],
+        [115.2, 7],
+        [115.2, 9.2],
+        [110.2, 9.2],
+        [110.2, 11],
+        [109.8, 11],
+        [109.8, 8.8],
+        [111.8, 8.8],
+        [111.8, 7],
+        [104, 7],
+        [104, 3],
+        [100, 3],
+        [100, 4],
+        [98, 4],
+        [98, 3],
+        [96, 3],
+        [96, 6],
+        [95, 6],
+        [95, 7],
+        [88, 7],
+        [88, 6],
+        [85, 6],
+        [85, 8],
+        [83, 8],
+        [83, 9],
+        [81, 9],
+        [81, 11],
+        [72, 11],
+        [72, 5],
+        [68, 5],
+        [68, 8],
+        [67, 8],
+        [67, 10],
+        [65, 10],
+        [65, 11],
+        [62, 11],
+        [62, 9],
+        [60, 9],
+        [60, 11],
+        [57, 11],
+        [57, 9],
+        [54, 9]
+    ]
+};
+
+export const camera1: ChaseCameraData[] = [
+    ['MT16', 0, 10, 0, 1600, hyper('sin', 'in')],
+    ['MT15', 45, 0, 0, 2324, hyper('sin', 'in')],
+    ['MT15', 40, 0, 2324, 1992, hyper('sin', 'out')],
+    ['MT15', 41, 0, 5312, 498, hyper('sin', 'in-out')],
+    ['MT15', 37, 0, 5810, 1660, hyper('sin', 'in')],
+    ['MT15', 29, 0, 7470, 830, hyper('sin', 'out')],
+    ['MT15', 25, 0, 11454, 996, hyper('sin', 'in')],
+    ['MT15', 12, 0, 12450, 996, linear()],
+    ['MT15', 0, 0, 13446, 1470, hyper('sin', 'out')],
+    ['MT14', 109, 0, 0, 1328, hyper('sin', 'in')],
+    ['MT14', 104, 0, 1328, 332, hyper('sin', 'out')],
+    ['MT14', 92, 0, 5478, 2822, hyper('sin', 'in')],
+    ['MT14', 84, 0, 8300, 1992, linear()],
+    ['MT14', 74, 0, 10292, 2988, linear()],
+    ['MT14', 65, 0, 13280, 2988, linear()],
+    ['MT14', 58, 0, 16268, 1992, linear()],
+    ['MT14', 47, 0, 18260, 3320, linear()],
+    ['MT14', 36, 0, 21580, 3320, linear()],
+    ['MT14', 0, 0, 24900, 9960, linear()]
+];
+
+/**
+ * 追逐战开始前的初始化函数,移除所有血瓶和门等
+ */
+export function init1() {
+    const ids: FloorIds[] = ['MT13', 'MT14', 'MT15'];
+    const toRemove: [number, number, FloorIds][] = [];
+    ids.forEach(v => {
+        core.status.maps[v].cannotMoveDirectly = true;
+        core.extractBlocks(v);
+        core.status.maps[v].blocks.forEach(vv => {
+            if (
+                ['animates', 'items'].includes(vv.event.cls) &&
+                !vv.event.id.endsWith('Portal')
+            ) {
+                toRemove.push([vv.x, vv.y, v]);
+            }
+        });
+    });
+    toRemove.forEach(v => {
+        core.removeBlock(...v);
+    });
+}
+
+export function chaseShake(chase: Chase) {
+    chase.ani
+        .mode(shake2(2 / 32, bezier(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)), true)
+        .time(50000)
+        .shake(1, 0);
+}
+
+export async function wolfMove(chase: Chase) {
+    core.moveBlock(23, 17, Array(6).fill('down'), 80);
+    await sleep(550);
+    core.setBlock(508, 23, 23);
+}
+
+export function judgeFail1(chase: Chase) {
+    chase.ani.ticker.add(() => {
+        if (core.status.hero.loc.x > core.bigmap.offsetX / 32 + 17) {
+            chase.end();
+            ani.time(750).apply('rect', 0);
+            core.lose('逃跑失败');
+        }
+    });
+}
+
+export function drawBack(chase: Chase) {
+    chase.on('MT15', 0, () => {
+        ani.mode(hyper('sin', 'out')).time(1500).absolute().apply('rect', 64);
+        const ctx = core.createCanvas('chaseBack', 0, 0, 480, 480, 120);
+        ctx.fillStyle = '#000';
+        const fn = () => {
+            if (!ctx) ani.ticker.remove(fn);
+            core.clearMap(ctx);
+            ctx.fillRect(0, 0, 480, ani.value.rect);
+            ctx.fillRect(0, 480, 480, -ani.value.rect);
+        };
+        ani.ticker.add(fn);
+    });
+}
+
+export function para1(chase: Chase) {
+    chase.on('MT15', 830, () => {
+        for (let tx = 53; tx < 58; tx++) {
+            for (let ty = 3; ty < 8; ty++) {
+                core.setBlock(336, tx, ty);
+            }
+        }
+        core.drawAnimate('explosion3', 55, 5);
+        core.drawAnimate('stone', 55, 5);
+    });
+    chase.on('MT15', 1080, () => {
+        core.setBlock(336, 58, 9);
+        core.setBlock(336, 59, 9);
+        core.drawAnimate('explosion1', 58, 9);
+        core.drawAnimate('explosion1', 59, 9);
+    });
+    chase.on('MT15', 1190, () => {
+        core.setBlock(336, 53, 8);
+        core.setBlock(336, 52, 8);
+        core.drawAnimate('explosion1', 53, 8);
+        core.drawAnimate('explosion1', 52, 8);
+    });
+    chase.on('MT15', 1580, () => {
+        core.setBlock(336, 51, 7);
+        core.drawAnimate('explosion1', 51, 7);
+    });
+    chase.on('MT15', 1830, () => {
+        core.setBlock(336, 47, 7);
+        core.setBlock(336, 49, 9);
+        core.drawAnimate('explosion1', 49, 9);
+        core.drawAnimate('explosion1', 47, 7);
+    });
+}
+
+export function para2(chase: Chase) {
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.setBlock(336, 45, 9);
+            core.drawAnimate('explosion1', 45, 9);
+        },
+        45,
+        8
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.setBlock(336, 44, 6);
+            core.drawAnimate('explosion1', 44, 6);
+        },
+        45,
+        6
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.setBlock(336, 44, 4);
+            core.drawAnimate('explosion1', 44, 4);
+            core.drawAnimate('explosion1', 48, 6);
+            core.removeBlock(48, 6);
+        },
+        45,
+        4
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.setBlock(336, 41, 4);
+            core.setBlock(336, 32, 6);
+            core.drawAnimate('explosion1', 41, 4);
+            core.drawAnimate('explosion1', 32, 6);
+        },
+        41,
+        3
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.drawAnimate('explosion3', 37, 7);
+            core.vibrate('vertical', 1000, 25, 10);
+            for (let tx = 36; tx < 42; tx++) {
+                for (let ty = 4; ty < 11; ty++) {
+                    core.setBlock(336, tx, ty);
+                }
+            }
+        },
+        35,
+        3
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.vibrate('vertical', 10000, 25, 1);
+            core.removeBlock(34, 8);
+            core.removeBlock(33, 8);
+            core.drawAnimate('explosion1', 34, 8);
+            core.drawAnimate('explosion1', 33, 8);
+        },
+        31,
+        5
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.setBlock(336, 32, 9);
+            core.drawAnimate('explosion1', 32, 9);
+        },
+        33,
+        7
+    );
+    chase.onHeroLoc(
+        'MT15',
+        () => {
+            core.removeBlock(32, 9);
+            core.drawAnimate('explosion1', 32, 9);
+        },
+        [33, 34, 34],
+        9
+    );
+    for (let x = 19; x < 31; x++) {
+        const xx = x;
+        chase.onHeroLoc(
+            'MT15',
+            () => {
+                core.setBlock(336, xx + 1, 11);
+                core.drawAnimate('explosion1', xx + 1, 11);
+            },
+            xx,
+            11
+        );
+    }
+}
+
+export function para3(chase: Chase) {
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 126, 6);
+            core.setBlock(336, 124, 6);
+            core.setBlock(336, 124, 9);
+            core.setBlock(336, 126, 9);
+            core.drawAnimate('explosion1', 126, 6);
+            core.drawAnimate('explosion1', 124, 6);
+            core.drawAnimate('explosion1', 124, 9);
+            core.drawAnimate('explosion1', 126, 9);
+        },
+        126,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(508, 127, 7);
+            core.jumpBlock(127, 7, 112, 7, 500, true);
+            setTimeout(() => {
+                core.setBlock(509, 112, 7);
+            }, 520);
+            core.drawHeroAnimate('amazed');
+            core.setBlock(336, 121, 6);
+            core.setBlock(336, 122, 6);
+            core.setBlock(336, 120, 8);
+            core.setBlock(336, 121, 8);
+            core.setBlock(336, 122, 8);
+            core.drawAnimate('explosion1', 121, 6);
+            core.drawAnimate('explosion1', 122, 6);
+            core.drawAnimate('explosion1', 120, 8);
+            core.drawAnimate('explosion1', 121, 8);
+            core.drawAnimate('explosion1', 122, 8);
+        },
+        123,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 109, 11);
+            core.removeBlock(112, 8);
+            core.drawAnimate('explosion1', 109, 11);
+            core.drawAnimate('explosion1', 112, 8);
+            core.insertAction([
+                { type: 'moveHero', time: 400, steps: ['backward:1'] }
+            ]);
+            chase.onHeroLoc(
+                'MT14',
+                () => {
+                    core.jumpBlock(112, 7, 110, 4, 500, true);
+                    core.drawHeroAnimate('amazed');
+                    setTimeout(() => {
+                        core.setBlock(506, 110, 4);
+                    }, 540);
+                },
+                112,
+                8
+            );
+        },
+        110,
+        10
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 117, 6);
+            core.setBlock(336, 116, 6);
+            core.setBlock(336, 115, 6);
+            core.setBlock(336, 114, 6);
+            core.setBlock(336, 117, 8);
+            core.setBlock(336, 116, 8);
+            core.drawAnimate('explosion1', 117, 6);
+            core.drawAnimate('explosion1', 116, 6);
+            core.drawAnimate('explosion1', 115, 6);
+            core.drawAnimate('explosion1', 114, 6);
+            core.drawAnimate('explosion1', 116, 8);
+            core.drawAnimate('explosion1', 117, 8);
+        },
+        118,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 112, 8);
+            core.setBlock(336, 113, 7);
+            core.drawAnimate('explosion1', 112, 8);
+            core.drawAnimate('explosion1', 113, 7);
+        },
+        112,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            for (let tx = 111; tx <= 115; tx++) {
+                core.setBlock(336, tx, 10);
+                core.drawAnimate('explosion1', tx, 10);
+            }
+            core.setBlock(336, 112, 8);
+            core.drawAnimate('explosion1', 112, 8);
+        },
+        115,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.jumpBlock(97, 4, 120, -3, 2000);
+            for (let tx = 109; tx <= 120; tx++) {
+                for (let ty = 3; ty <= 11; ty++) {
+                    if (ty == 7) continue;
+                    core.setBlock(336, tx, ty);
+                }
+            }
+            core.drawAnimate('explosion2', 119, 7);
+            core.removeBlock(105, 7);
+            core.drawAnimate('explosion1', 105, 7);
+        },
+        110,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 95, 3);
+            core.setBlock(336, 93, 6);
+            core.drawAnimate('explosion1', 95, 3);
+            core.drawAnimate('explosion1', 93, 6);
+        },
+        97,
+        3
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 87, 4);
+            core.setBlock(336, 88, 5);
+            core.drawAnimate('explosion1', 87, 4);
+            core.drawAnimate('explosion1', 88, 5);
+        },
+        88,
+        6
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 84, 6);
+            core.setBlock(336, 85, 5);
+            core.setBlock(336, 86, 8);
+            core.drawAnimate('explosion1', 84, 6);
+            core.drawAnimate('explosion1', 85, 5);
+            core.drawAnimate('explosion1', 86, 8);
+        },
+        86,
+        6
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 81, 8);
+            core.setBlock(336, 82, 11);
+            core.drawAnimate('explosion1', 81, 8);
+            core.drawAnimate('explosion1', 82, 11);
+        },
+        81,
+        9
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 73, 8);
+            core.setBlock(336, 72, 4);
+            core.drawAnimate('explosion1', 73, 8);
+            core.drawAnimate('explosion1', 72, 4);
+        },
+        72,
+        11
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            for (let tx = 74; tx < 86; tx++) {
+                for (let ty = 3; ty < 12; ty++) {
+                    core.setBlock(336, tx, ty);
+                }
+            }
+            core.drawAnimate('explosion2', 79, 7);
+            core.vibrate('vertical', 4000, 25, 15);
+        },
+        71,
+        7
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 68, 4);
+            core.setBlock(336, 67, 6);
+            core.drawAnimate('explosion1', 68, 4);
+            core.drawAnimate('explosion1', 67, 6);
+        },
+        68,
+        5
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            for (let tx = 65; tx <= 72; tx++) {
+                for (let ty = 3; ty <= 9; ty++) {
+                    core.setBlock(336, tx, ty);
+                }
+            }
+            core.setBlock(336, 72, 10);
+            core.setBlock(336, 72, 11);
+            core.drawAnimate('explosion3', 69, 5);
+        },
+        67,
+        10
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            core.setBlock(336, 63, 9);
+            core.setBlock(336, 60, 8);
+            core.setBlock(336, 56, 11);
+            core.drawAnimate('explosion1', 63, 9);
+            core.drawAnimate('explosion1', 60, 8);
+            core.drawAnimate('explosion1', 56, 11);
+        },
+        64,
+        11
+    );
+    chase.onHeroLoc(
+        'MT14',
+        () => {
+            for (let tx = 58; tx <= 64; tx++) {
+                for (let ty = 3; ty <= 11; ty++) {
+                    core.setBlock(336, tx, ty);
+                }
+            }
+            core.drawAnimate('explosion2', 61, 7);
+        },
+        57,
+        9
+    );
+    for (let x = 21; x < 49; x++) {
+        chase.onHeroLoc(
+            'MT14',
+            () => {
+                for (let ty = 3; ty <= 11; ty++) {
+                    core.setBlock(336, x + 4, ty);
+                    core.drawAnimate('explosion1', x + 4, ty);
+                }
+            },
+            x
+        );
+    }
+    chase.onHeroLoc(
+        'MT14',
+        async () => {
+            flags.finishChase1 = true;
+            ani.time(750).apply('rect', 0);
+            chase.end();
+            await sleep(750);
+            ani.ticker.destroy();
+            core.deleteCanvas('chaseBack');
+        },
+        21
+    );
+}
diff --git a/src/plugin/chase/data.ts b/src/plugin/chase/data.ts
new file mode 100644
index 0000000..0a79ff5
--- /dev/null
+++ b/src/plugin/chase/data.ts
@@ -0,0 +1,52 @@
+import { PathFn, TimingFn } from 'mutate-animate';
+import { Chase } from './chase';
+import {
+    camera1,
+    para1,
+    para2,
+    para3,
+    path1,
+    chaseShake,
+    wolfMove,
+    init1,
+    judgeFail1,
+    drawBack
+} from './chase1';
+
+export type ChaseCameraData = [
+    floorId: FloorIds, // 楼层
+    x: number, // 目标横坐标
+    y: number, // 目标纵坐标
+    start: number, // 开始时间
+    time: number, // 持续时间
+    mode: TimingFn, // 渐变函数
+    path?: PathFn // 路径函数
+];
+
+export type ChasePath = Partial>;
+
+interface ChaseData {
+    camera: ChaseCameraData[];
+    fns: ((chase: Chase) => void)[];
+    path: ChasePath;
+}
+
+export function getChaseDataByIndex(index: number): ChaseData {
+    if (index === 1) {
+        init1();
+        return {
+            camera: camera1,
+            fns: [
+                para1,
+                para2,
+                para3,
+                chaseShake,
+                wolfMove,
+                drawBack,
+                judgeFail1
+            ],
+            path: path1
+        };
+    }
+    throw new ReferenceError(`Deliver wrong chase index.`);
+}
diff --git a/src/types/map.d.ts b/src/types/map.d.ts
index dc9df48..fdff9f1 100644
--- a/src/types/map.d.ts
+++ b/src/types/map.d.ts
@@ -146,6 +146,11 @@ interface FloorBase {
      */
     cannotViewMap?: boolean;
 
+    /**
+     * 是否不能瞬移
+     */
+    cannotMoveDirectly?: boolean;
+
     /**
      * 是否是地下层
      */
@@ -1347,7 +1352,7 @@ interface Maps {
         name: AnimationIds | NameMapIn,
         x: number,
         y: number,
-        alignWindow: boolean,
+        alignWindow?: boolean,
         callback?: () => void
     ): number;
 
diff --git a/src/types/plugin.d.ts b/src/types/plugin.d.ts
index 9a0a934..d521aa7 100644
--- a/src/types/plugin.d.ts
+++ b/src/types/plugin.d.ts
@@ -108,6 +108,12 @@ interface PluginUtils {
         type: 'warn' | 'info' | 'success' | 'error' | 'warning' | 'loading',
         text: string
     ): void;
+
+    /**
+     * 开始一个追逐战
+     * @param index 追逐战索引
+     */
+    startChase(index: number): Promise;
 }
 
 interface PluginUis {
diff --git a/src/types/ui.d.ts b/src/types/ui.d.ts
index 73b05f8..5a2bd2c 100644
--- a/src/types/ui.d.ts
+++ b/src/types/ui.d.ts
@@ -784,7 +784,7 @@ interface Ui {
      * 重新定位一个自定义画布
      */
     relocateCanvas(
-        name: string,
+        name: CtxRefer,
         x: number,
         y: number,
         useDelta?: boolean
@@ -806,7 +806,7 @@ interface Ui {
      * @param isTempCanvas 是否是临时画布,如果填true,会将临时画布修改为高清画布
      */
     resizeCanvas(
-        name: string,
+        name: CtxRefer,
         x?: number,
         y?: number,
         styleOnly?: boolean,