# 脚本

?> 在这一节中，让我们来了解如何使用控制台和使用脚本！

在V2.6版本中，基本对整个项目代码进行了重写，更加方便造塔者的使用。

可配合基础js教学视频 https://www.bilibili.com/video/BV1uL411J7yZ/ 使用。

## 控制台的使用

在Chrome浏览器中，按（Ctrl+Shift+I）可打开控制台。

![](img/console.jpg)

控制台中有很多的标签，最常用的是`Console`, `Sources`和`Elements`。

有关更详尽的控制台使用可自行搜索[Chrome开发者工具](https://www.baidu.com/s?wd=chrome%20%E5%BC%80%E5%8F%91%E8%80%85%E5%B7%A5%E5%85%B7)了解更多。

### Console：命令行

Console页为命令行。可以在这里输入一些命令进行调试。

比如，进入游戏后，输入`core.status.hero.atk`即可获得勇士的当前攻击力数值。`core.status.hero.atk=100`可以设置攻击力为100。

更多的API可参见[附录：API列表](#附录：API列表)。

除此以外，游戏中的报错等信息也是可以在Console中进行查看的。

![](img/console1.jpg)

### Sources：断点调试

Sources页可以查看JS源代码，并进行断点调试等。

例如，如果相对脚本编辑中的伤害计算函数进行断点调试：
1. 在左边找到`project/functions.js`，单击打开文件
2. 并找到对应的行（可以Ctrl+F搜索），比如搜索`getDamageInfo`
3. 在行号上点一下打断点，会出现一个蓝色标签

之后，当代码运行到你的断点处时，将自动停止运行。

![](img/sources.jpg)

可以将鼠标移动到变量上，将弹窗形式显示这个变量的各项数值，从而查看变量值是否符合预期。

图中红色框内有几个按钮，从左到右分别是：**继续执行**，**执行到下一行**，**进入当前函数**，**跳出当前函数**，**单步执行**。

通过这几个按钮，可以一行一行的对代码进行执行，执行过程中能不断查看各个变量的数值变化，从而定位问题所在。

红圈下方是Call Stack，即当前的函数调用链（从哪些地方调用过来的）。

Sources还有更多有趣的功能，在此不做介绍，有兴趣的可自行网上搜索了解。

### Elements：网页元素查看

Elements页可以查看网页的源代码，调整css布局等。

![](img/elements.jpg)

不过对魔塔样板来说，最重要的是红圈中的按钮。点击此按钮可以进入**手机模式**。

手机模式下，左边可以对屏幕分辨率进行调整和模拟。

这可以很有效的帮我们进行测试样板在手机端的表现。

## 整体项目架构

``` text
├── /_server/        # 为可视化地图编辑器提供一些支持的目录
├── /libs/           # ---- 系统库目录 ----
│ ├─ /thirdparty/    # 游戏所用到的第三方库文件
│ ├─ actions.js      # 用户交互处理
│ ├─ core.js         # 系统核心文件（游戏入口，接口&转发）
│ ├─ control.js      # 游戏逻辑控制
│ ├─ data.js         # 全塔属性等
│ ├─ enemys.js       # 怪物相关处理
│ ├─ events.js       # 各个事件的执行
│ ├─ icons.js        # 图标和素材
│ ├─ items.js        # 道具效果
│ ├─ loader.js       # 各个资源加载
│ ├─ maps.js         # 地图数据和绘制
│ ├─ ui.js           # UI窗口绘制
│ └─ utils.js        # 工具类函数
├── /project/        # ---- 项目目录 ----
│ ├─ /animates/      # 动画目录
│ ├─ /floors/        # 楼层文件
│ ├─ /images/        # 图片素材
│ ├─ /sounds/        # bgm和音效
│ ├─ data.js         # 全塔属性
│ ├─ enemys.js       # 怪物属性
│ ├─ events.js       # 公共事件
│ ├─ functions.js    # 脚本编辑
│ ├─ icons.js        # 素材和ID的对应关系定义
│ ├─ items.js        # 道具的定义和效果
│ ├─ maps.js         # 地图和数字的对应关系
│ └─ plugins.js      # 自定义插件
├── /常用工具/        # 辅助造塔的小工具
├── editor.html      # 地图编辑器
├── editor-mobile.html      # 手机版的地图编辑器
├── index.html       # 主程序，游戏的入口
├── main.js          # JS程序的入口，将动态对所需JS进行加载
├── style.css        # 游戏所需要用到的样式表
└── 启动服务.exe      # 一个本地的HTTP服务器，通过它来运行游戏
```

`_server`为**地图编辑器目录**，里面存放了地图编辑器相关的各项内容。

`libs`为**系统库目录**，里面存放了各个系统核心函数。

从V2.6开始，请勿直接修改libs下的代码，如有需要修改系统库函数请尝试在插件中[复写函数](#复写函数)。

`project`为**项目目录**，你所造的塔的数据全部存放在project下。在不同样板之间接档也是直接迁移project目录即可。

## 函数的转发

在本样板中，`core.js`里面基本是没有定义什么函数的，所有的游戏内函数都在其他几个文件中实现。

例如，常见的获得某个变量值`getFlag`是定义在`control.js`中的：

```js
////// 获得某个自定义变量或flag //////
control.prototype.getFlag = function(name, defaultValue) {
    if (!core.status.hero) return defaultValue;
    var value = core.status.hero.flags[name];
    return value != null ? value : defaultValue;
}
```

也就是，我们可以通过`core.control.getFlag(name, value)`来调用此函数。

但是这样会十分不便，我们希望能直接调用`core.getFlag(name, value)`，而不需要中间的control。

为了达到这个目的，样板设置了**函数转发**，即**将其他文件中定义的函数，转发到core中执行**。

上述`getFlag`代码的转发实际上是增加了如下函数：

```js
////// getFlag函数的转发 //////
core.getFlag = function (name, defaultValue) {
    return core.control.getFlag(name, defaultValue);
}
// 转发后，即可通过 core.getFlag() 来实际调用 core.control.getFlag()
```

转发是自动完成的，其满足如下两条规则：
- **在libs中其他文件定义的函数，如果不以下划线`_`开头，就会进行转发。**
- **如果core中已经存在同名函数，则会在控制台中打出一条报错信息，并不转发该函数。**

具体函数的转发实现代码可参见`core.js`的`_forwardFunc`函数。

!> 除此以外，插件中以`this.xxx`来定义的函数也会被转发！

例如，你可以直接调用`core.drawLight()`来实际调用插件中的`core.plugin.drawLight`。

## 插件编写

插件编写是H5魔塔的一个重大特点，从V2.0.1引入，并逐渐发扬光大。

对于有一定脚本经验的人来说，可以编写插件来实现各种各样的功能，包括且不仅限于拓展功能的实现，系统代码的复写等等。

在V2.5.5以前，插件位置都在脚本编辑中；从V2.6开始则迁移到了新的下拉框中，并进行了切分。

你也可以创建自己的插件。

![](img/plugin.jpg)

新的插件切分和原来的单插件使用方法完全一致，单纯进行了切分而已。可参见已有的`init`和`drawLight`的样例。

拆分的意义主要是将各个可能的功能独立出来，避免单个框内内容太长，过大和混杂等。

在V2.6中，应当每个独立的额外功能实现都新建一个自己的插件，这样也方便进行拓展，例如打包迁移到别的塔上，或发布在网页插件库中。

另外一点需要注意的是，所有插件的初始化都会在系统资源加载之前，此时图片等资源尚未进行加载。

在所有资源加载完毕时，将会执行init插件中的_afterLoadResources函数，可以在这里对资源进行一些操作，比如切分图片等。

```js
function () {
	console.log("插件编写测试");

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


	this._afterLoadResources = function () {
		// 本函数将在所有资源加载完毕后，游戏开启前被执行
		// 可以在这个函数里面对资源进行一些操作，比如切分图片等。

		// 这是一个将assets.png拆分成若干个32x32像素的小图片并保存的样例。
		// var arr = core.splitImage("assets.png", 32, 32);
		// for (var i = 0; i < arr.length; i++) {
		//     core.material.images.images["asset"+i+".png"] = arr[i];
		// }

	}

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

网站上提供了一个插件库，[https://h5mota.com/plugins/](https://h5mota.com/plugins/)，上面有一些大家分享的插件，可供使用。

可以查看附录中的[API列表](api)来查看所有的系统API内容。

## register系列函数

在API列表中，有一系列的函数以`register`开头；这一系列函数允许你将一部分自定义的代码注入到样板中，从而可以监听某一系列行为并进行处理。

下面会对这系列函数进行逐个介绍。

### registerAction

```
registerAction: fn(action: string, name: string, func: string|fn(params: ?), priority?: number)
此函数将注册一个用户交互行为。
action: 要注册的交互类型，如 ondown, onclick, keyDown 等等。
name: 你的自定义名称，可被注销使用；同名重复注册将后者覆盖前者。
func: 执行函数。
如果func返回true，则不会再继续执行其他的交互函数；否则会继续执行其他的交互函数。
priority: 优先级；优先级高的将会被执行。此项可不填，默认为0
```

`registerAction`为register系列最常用的功能之一，它允许你任意监听用户的交互事件，包括且不仅限于按键、点击、拖动、长按，等等。

下面是一个例子：

```js
// 当flag:abc是true时，点击屏幕左上角可以使用道具破墙镐
// 注入一个 onclick 事件，名称为 my_pickaxe
core.registerAction('onclick', 'my_pickaxe', function (x, y, px, py) {
    // 如果当前正在执行某个事件，则忽略之。
    if (core.status.lockControl) return false;
    // 如果勇士正在行走中，则忽略之。
    if (core.isMoving()) return false;
    // x, y 为点击坐标；px, py 为点击的像素坐标
    // 检查flag是否为true，且点击了(0,0)点
    if (core.hasFlag('abc') && x == 0 && y == 0) {
        // 检查能否使用破墙镐
        if (core.canUseItem('pickaxe')) {
            core.useItem('pickaxe');
            // 返回true将拦截其他交互函数对于点击的处理
            return true;
        }
    }
    // 返回false后将会继续执行其他的交互函数（例如寻路）
    return false;

// 优先级设置为100，比系统优先级高，因此将会优先执行此注入的项目。
}, 100);

// 当你不再需要上述监听时，可以通过下面这一句取消注入。
// core.unregisterActon('onclick', 'my_pickaxe');
```

下面是另一个例子：

```js
// 假设我们通过事件流做了一个商店，希望能通过”长按空格“和“长按屏幕”进行连续确认购买
// 此时，需要使用一个flag，表示是否在商店中；例如 flag:inShop

// 注入一个keyDown（按下）事件；请注意，当按下按键且不释放时，会连续触发keyDown。
core.registerAction('keyDown', 'my_shop', function (keycode) {
    // 检查是否确实在该商店事件中；进该商店前需要将flag:inShop设为true，退出后需要设为false
    if (!core.hasFlag('inShop')) return false;
    // 检查当前事件是一个”显示选择项“
    if (core.status.event.id != 'action' || core.status.event.data.type != 'choices') {
        return false;
    }
    // 获得当前“显示选择项”的各个选项
    var choices = core.status.event.data.current.choices;
    // 获得当前选项第一项在屏幕上的y坐标
    var topIndex = core.actions._getChoicesTopIndex(choices.length);
    // 检查按键的键值，是否是空格或者回车
    if (keycode == 13 || keycode == 32) {
        // 如果是，则视为直接点击屏幕上的选项
        // core.status.event.selection为当前光标选择项；第一项为0
        // 点击的x为HSIZE，即屏幕左右居中；y为topIndex + core.status.event.selection为该选择项
		core.actions._clickAction(core.actions.HSIZE, topIndex + core.status.event.selection);
        // 不再执行其他的交互函数
        return true;
    }
    // 否则，继续执行其他的交互函数（如上下键光标选择）
    return false;
}, 100);


// 还需要注册一个keyUp（放开）事件，拦截空格和回车
// 这是因为，按一下空格会先触发keyDown再触发keyUp；这里不拦截的话会购买两次。
var _my_shop_up = function (keycode) {
    if (!core.hasFlag('inShop')) return false;
    if (core.status.event.id != 'action' || core.status.event.data.type != 'choices') {
        return false;
    }
    // 检查是否是空格或者回车，如果是则拦截操作。
    if (keycode == 13 || keycode == 32) return true;
    return false;
};
// 这里可以传入前面定义的函数体。
core.registerAction('keyUp', 'my_shop', _my_shop_up, 100);


// 注册一个长按事件，允许长按连续购买
this._my_shop_longClick = function (x, y, px, py) {
    if (!core.hasFlag('inShop')) return false;
    if (core.status.event.id != 'action' || core.status.event.data.type != 'choices') {
        return false;
    }
    // 直接视为点击了此位置
    core.actions._clickAction(x, y, px, py);
	return true;
}
// 如果是插件中使用`this.xxx`定义的函数，也可以直接传入函数名。
core.registerAction('longClick', 'my_shop', '_my_shop_longClick', 100);
```

简单地说，`registerAction`这个函数**允许你在任何时候监听和拦截任何用户交互行为**。

目前，`registerAction`支持监听和拦截如下交互：

- `keyDown`: 当一个键被按下时
  - 对应的函数参数：`function (keycode)`，为此时按下键的键值
  - 请注意：如果长按键不放开的话，会连续触发`keyDown`事件
- `keyUp`: 当一个键被放开时
  - 对应的函数参数：`function (keycode, altKey, fromReplay)`，为此时放开键的键值、当前alt是否被按下、是否是录像播放中模拟的按键
- `onKeyDown`: 当一个键被按下时
  - 对应的函数参数：`function (e)`，为此时按键的信息。这里的`e`是一个`KeyboardEvent`。
  - 请注意：如果长按键不放开的话，会连续触发`onKeyDown`事件。
  - 和上述`keyDown`的主要区别就是参数为原始的`KeyboardEvent`；仍然推荐直接使用`keyDown`。
- `onKeyUp`: 当一个键被放开时
  - 对应的函数参数：`function (e)`，为此时按键的信息。这里的`e`是一个`KeyboardEvent`。
  - 和上述`keyUp`的主要区别就是参数为原始的`KeyboardEvent`；仍然推荐直接使用`keyUp`。
- `ondown`: 当屏幕被鼠标或手指按下时
  - 对应的函数参数：`function (x, y, px, py)`，为此时按下时的格子坐标和像素坐标。
- `onmove`: 当屏幕被鼠标滑动，或手指拖动时
  - 对应的函数参数：`function (x, y, px, py)`，为此时滑动或拖动时的格子坐标和像素坐标。
  - 如果是鼠标，只要在屏幕上滑动就会触发`onmove`，无需处于点击状态（也就是悬浮也是可以的）。
  - 如果是触摸屏，则只有手指按下滑动时才会触发`onmove`（并不存在什么悬浮的说法）。
- `onup`: 当屏幕被鼠标或手指放开时
  - 对应的函数参数：`function (x, y, px, py)`，为此时放开时的格子坐标和像素坐标。
- `onclick`: 当屏幕被鼠标或手机点击时
  - 对应的函数参数：`function (x, y, px, py, stepPostfix)`，为此时点击的格子坐标、像素坐标，和拖动路径
  - 此函数会在两种情况下被调用：
    - 在锁定状态下（即角色不可以自由移动），会在`ondown`时直接触发`onclick`，此时`stepPostfix`为空数组。
    - 在自由状态下（即角色可以自由移动），会在`onup`时触发`onclick`；此时`stepPostfix`为拖动寻路的路径数组。
  - 推荐自定义的点击监听都使用`ondown`处理。
- `onmousewheel`: 当鼠标滚轮滚动时
  - 对应的函数参数：`function (direct)`，为此时滚轮方向。向下滚动是-1，向上滚动是1。
  - 目前在楼传、怪物手册、存读档、浏览地图等多个地方绑定了鼠标滚轮事件。
- `keyDownCtrl`: 当Ctrl键处于被长按不放时
  - 对应的函数参数：`function ()`，即无参数。
  - 目前主要用于跳过剧情对话。
- `longClick`: 当鼠标或手指长按屏幕时
  - 对应的函数参数：`function (x, y, px, py)`，为长按屏幕对应的格子坐标和像素坐标
  - 目前主要用于虚拟键盘，和跳过剧情对话。
- `onStatusBarClick`: 当点击状态栏时
  - 对应的函数参数：`function (px, py, vertical)`，为状态栏点击的像素坐标。
  - 如果参数`vertical`不为`null`，表示该状态栏点击是录像播放时触发的，其值为录像记录时的横竖屏状态。

### registerAnimationFrame

```
registerAnimationFrame: fn(name: string, needPlaying: bool, func?: fn(timestamp: number))
注册一个 animationFrame
name: 名称，可用来作为注销使用
needPlaying: 是否只在游戏运行时才执行（在标题界面不执行）
func: 要执行的函数，或插件中的函数名；可接受timestamp（从页面加载完毕到当前所经过的时间）作为参数
```

`registerAnimationFrame`为其次常用的功能，可以用于需要反复执行的脚本，常用于动画之类。

目前，样板的全局动画（即怪物的帧振动）、人物行走动画、普通动画、天气效果、游戏计时、以及楼层并行事件等，都是通过`registerAnimationFrame`进行实现的。

通过`registerAnimationFrame`注册的函数会平均每16.6ms执行一次（视浏览器的性能而定，例如手机上可能执行频率会变慢）。可以通过函数的`timestamp`参数来获得从页面加载完毕到当前所经过的毫秒数（从而可以进一步控制执行频率，如每500ms才执行一次）。

**推荐所有的需要反复执行的脚本（如动画相关）都通过此函数来注册，而不是使用`setInterval`，以获得更好的性能。**

下面是“人物血量动态变化”的插件的例子。

```js
var speed = 0.05; // 动态血量变化速度，越大越快。
var _currentHp = null; // 当前正在变化的hp值
var _lastStatus = null; // 上次人物的属性对象

// 注册一个
core.registerAnimationFrame('dynamicHp', true, function (timestamp) {
    // 检查人物所指向的对象是否发生了变化
    // 在例如读档后，core.status.hero被重置了，此时需要将_currentHp重置为正确的生命值。
    if (_lastStatus != core.status.hero) {
        _lastStatus = core.status.hero;
        _currentHp = core.status.hero.hp;
    }
    // 如果显示的hp值和当前实际hp值不同
    if (core.status.hero.hp != _currentHp) {
        // 计算hp差值，并获得下个血量变化值。
        var dis = (_currentHp - core.status.hero.hp) * speed;
        if (Math.abs(dis) < 2) {
            // 如果很接近了，直接设为实际hp值。
            _currentHp = core.status.hero.hp;
        } else {
            // 否则，改变显示值使其更接近。
            _currentHp -= dis;
        }
        // 然后设置状态栏中生命的显示项。
        core.setStatusBarInnerHTML('hp', _currentHp);
    }
});
```

### registerReplayAction

```
registerReplayAction: fn(name: string, func: fn(action?: string) -> bool)
注册一个录像行为
name: 自定义名称，可用于注销使用
func: 具体执行录像的函数，可为一个函数或插件中的函数名；
需要接受一个action参数，代表录像回放时的下一个操作
func返回true代表成功处理了此录像行为，false代表没有处理此录像行为。
```

`registerReplayAction`允许你自己定义和注册一个录像行为。

在游戏时，你的所有操作会被记录为一个数组`core.status.route`，如按上就是`up`，使用破墙镐就是`item:pickaxe`，瞬移到(1,2)就是`move:1:2`，等等。

在录像播放时，会依次从数组中取出一个个的录像操作，并模拟执行对应的操作。这里对于每个录像操作，都是靠`registerReplayAction`进行注册的。

下面是一个例子：

```js
// 当flag:abc为true时，点击屏幕(0,0)，(0,1)，(0,2)，(0,3)会分别执行不同的公共事件。

// 先通过registerAction注册屏幕点击事件
core.registerAction('onclick', 'commonEvent', function (x, y, px, py) {
    // 检查是否是自由状态、是否在移动，是否存在flag。
    if (core.status.lockControl || core.isMoving() || !core.hasFlag('abc')) return false;
    // 公共事件名
    var eventName = null;
    if (x == 0 && y == 0) {
        eventName = "公共事件0";
    } else if (x == 0 && y == 1) {
        eventName = "公共事件1";
    } else if (x == 0 && y == 2) {
        eventName = "公共事件2";
    } // ... 可以继续写下去。

    // 如果存在需要执行的公共事件
    if (eventName != null) {
        // 重要！往录像中写入一个自定义的`commonEvent:`项，记录执行的公共事件名称。
        // 由于录像编码时只支持部分ascii码，而eventName作为公共事件名可能带有中文，
        // 因此需要使用 core.encodeBase64() 对eventName进行编码
        core.status.route.push("commonEvent:" + core.encodeBase64(eventName));
        // 插入公共事件
        core.insertCommonEvent(eventName);
        return true;
    }
    return false;
});

// 注册一个录像处理行为来处理上述的`commonEvent:`项。
core.registerReplayAction('commonEvent', function (action) {
    // 参数action为此时录像播放时执行到的项目。
    // 如果不是`commonEvent:`，则不应该被此函数处理。
    if (action.indexOf('commonEvent:') !== 0) return false;
    // 二次检查flag:abc是否存在
    if (!core.hasFlag('abc')) {
        core.control._replay_error(action);
        return true;
    }
    // 如果是我们的录像行为，获得具体的公共事件名。
    // 上面使用encodeBase64()编码了录像，现在同样需要解码。
    var eventName = core.decodeBase64(action.substring(12));
    // 由于录像播放中存在二次记录，还需要将action写入到当前播放过程中的录像中。
    // 这样录像播放完毕后再次直接重头播放不会出现问题。
    core.status.route.push(action);
    // 执行该公共事件。
    core.insertCommonEvent(eventName);
    // 继续播放录像。
    core.replay();
    // 返回true表示我们已经成功处理了此录像行为。
    return true;
});
```

### registerWeather

```
registerWeather: fn(name: string, initFunc: fn(level: number), frameFunc?: fn(timestamp: number, level: number))
注册一个天气
name: 要注册的天气名
initFunc: 当切换到此天气时的初始化；接受level（天气等级）为参数；可用于创建多个节点（如初始化雪花）
frameFunc: 每帧的天气效果变化；可接受timestamp（从页面加载完毕到当前所经过的时间）和level（天气等级）作为参数
天气应当仅在weather层进行绘制，推荐使用core.animateFrame.weather.nodes用于节点信息。
```

`registerWeather`允许你注册一个天气。

在游戏时，楼层属性中可以设置天气如 `["snow", 5]`，或者脚本 `core.setWeather("snow", 5)` 来切换天气。

下面是一个例子：

```js
// 注册一个”血“天气，每200ms就随机在界面上的绘制红色斑点
core.registerWeather('blood', function (level) {
    // 切换到此天气时应当执行的脚本吗，如播放一个音效
    core.playSound('blood.mp3');
}, function (timestamp, level) {
    // 我们希望每200ms就界面上随机绘制 level^2 个红点，半径在0~32像素之间

    // 检查是否经过了200ms
    if (timestamp - core.animateFrame.weather.time < 200) return;
    // 当且仅当在weather层上绘制
    core.clearMap('weather');
    for (var i = 0; i < level * level; ++i) {
        // 随机界面中的一个点，半径在0~32之间
        var px = Math.random() * core.__PIXELS__;
        var py = Math.random() * core.__PIXELS__;
        var r = Math.random() * 32;
        core.fillCircle('weather', px, py, r, 'red');
    }
    // 设置本次天气调用的时间
    core.animateFrame.weather.time = timestamp;
});
```

值得注意的是，天气当且仅当在`weather`层进行绘制，推荐使用或设置`core.animateFrame.weather.time`作为上次天气调用的时间避免太过于频繁的调用。

推荐使用`core.animateFrame.weather.nodes`来存储天气的节点，这样会在取消天气时自动被移除。

样板的云`cloud`和雾`fog`均由多个图片叠加移动实现；如果你想实现类似效果，可直接使用`core.control.__animateFrame_weather_image`作为`frameFunc`，详见样板的云雾实现。

另外注意的是，注册的天气无法在事件编辑器的下拉框中选择；你可以选择脚本调用`core.setWeather`，或者修改`_server/MotaAction.g4`中的`Weather_List`:

```js
Weather_List
    :   '无'|'雨'|'雪'|'晴'|'雾'|'云'
    /*Weather_List ['null','rain','snow','sun','fog','cloud']*/;
```

### registerSystemEvent

```
registerSystemEvent: fn(type: string, func: fn(data?: ?, callback?: fn()))
注册一个系统事件
type: 事件名
func: 为事件的处理函数，可接受(data,callback)参数
```

`registerSystemEvent`允许你注册一个系统事件。

在图块属性中，存在一个`trigger`选项，可以设置图块的触发器；当玩家碰触到对应的图块时，会根据对应图块的触发器来执行对应系统事件，即`registerSystemEvent`所注册的。

这里的`data`为触发该图块时的`block`信息，和`core.getBlock()`的返回值相同。`callback`为执行完毕时的回调。

下面是一个例子：

```js
// 假设存在一个图块，当触碰时会触发一个公共事件，且需要图块坐标作为公共事件参数。
// 首先需要将该图块的触发器设置为`custom`。

// 注册一个`custom`的系统事件；当角色碰触到触发器为`custom`的图块时会被执行。
core.registerSystemEvent("custom", function (data, callback) {
    // 这里的`data`为碰触到的图块信息。
    console.log(data);
    // 插入一个公共事件（如“图块触碰”），把图块坐标作为公共事件参数传入。
    core.insertCommonEvent("图块触碰", /*args*/ [data.x, data.y], data.x, data.y);
    if (callback) callback();
})
```

`registerSystemEvent`系列最大的好处是，他是和“图块”相关而不是和“点”相关的。也就是说，可以任意通过`setBlock`之类的函数移动图块，不会影响到事件的触发（而这个是靠红点所无法完成的）。

也可以通过`registerSystemEvent`来拦截默认的系统事件，例如 `core.registerSystemEvent("battle", ...)` 可以拦截当遇到没有覆盖触发器的怪物的系统处理（即不再直接战斗）。

### registerEvent

```
registerEvent: fn(type: string, func: fn(data: ?, x?: number, y?: number, prefix?: string))
注册一个自定义事件
type: 事件类型
func: 事件的处理函数，可接受(data, x, y, prefix)参数
data为事件内容，x和y为当前点坐标（可为null），prefix为当前点前缀
```

`registerEvent`允许你注册一个自定义事件，即事件流中 type:xxx 的行为。

```js
// 注册一个type:abc事件
core.registerEvent("abc", function (data, x, y, prefix) {
    // 直接打出当前事件信息
    console.log(data.name);
    core.doAction();
});

// 执行一个abc事件
// 控制台会打出 "hahaha"
// core.insertAction([{"type": "abc", "name": "hahaha"}]);
```

此函数一般不是很常用，但是配合“编辑器修改”（即给事件编辑器修改或者增加事件图块）会有奇效。

具体例子可参见[修改编辑器](editor)中的`demo - 增加新语句图块`。

### registerResize

```
registerResize: fn(name: string, func: fn(obj: ?))
注册一个resize函数
name: 名称，可供注销使用
func: 可以是一个函数，或者是插件中的函数名；可以接受obj参数，详见resize函数。
```

`registerResize`允许你重写一个屏幕重定位函数。

此函数会在游戏开始前，以及每次屏幕大小发生改变时调用。系统通过此函数来对游戏界面进行调整，如横竖屏自适应等。

此函数会被较少用到。

## 复写函数

样板的功能毕竟是写死的，有时候我们也需要修改样板的一些行为。

在V2.6以前，需要直接打开libs目录下的对应文件并进行修改。但是开libs下的文件就会出现各种问题：

- 不容易记得自己修改过什么，而且如果改错了很麻烦
  - 例如，直接修改了某函数加了新功能，结果过段时间发现不需要，想删掉，但是这时候已经很难找到自己改过了什么了。
  - 或者，如果代码改错了，不断往上面打补丁，也只会使得libs越来越乱，最后连自己做过什么都不记得。
- 不容易随着新样板接档进行迁移
- 不方便能整理成新的插件在别的塔使用（总不能让别的塔也去修改libs吧）
- ……

好消息是，从V2.6开始，我们再也不需要开文件了，而是可以直接在插件中对原始函数进行复写。

函数复写的好处如下：

- 不会影响系统原有代码。
  - 即使写错了或不需要了，也只用把插件中的函数注释或删除即可，不会对原来的系统代码产生任何影响。
- 清晰明了。很容易方便知道自己修改过什么，尤其是可以和系统原有代码进行对比。
- 方便整理成新的插件，给其他的塔使用。

一般而言，复写规则如下：

**对xxx文件中的yyy函数进行复写，规则是`core.xxx.yyy = function (参数列表) { ... }`。**

但是，对于`register`系列函数是无效的，例如直接复写`core.control._animationFrame_globalAnimate`函数是没有效果的。对于这种情况引入的函数，需要注册同名函数，可参见最下面的样例。

下面是几个例子，从简单到复杂。

### 重写怪物手册的背景图绘制，使用winskin而不是默认的黑色

直接重写怪物手册的背景图绘制，使用`core.drawBackground`来用winskin绘制一个背景图。

```js
// 重写ui.js中的_drawBook_drawBackground函数
core.ui._drawBook_drawBackground = function () {
    // core.__PIXELS__为定义的一个宏，对于13x13的值是416，对于15x15的值是480
    core.drawBackground(0, 0, core.__PIXELS__, core.__PIXELS__);
}
```

### 重写点击楼传事件

重写点击楼传事件，使得点击楼传按钮时能使用一个道具（比如item:fly）。

```js
// 重写events.js的useFly函数，即点击楼传按钮时的事件
core.events.useFly = function (fromUserAction) {
    if (core.isMoving()) {
        core.drawTip("请先停止勇士行动");
        return;
    }
    if (core.status.lockControl || core.status.event.id != null) return;
    
    if (core.canUseItem('fly')) core.useItem('fly');
    else core.drawTip("当前无法使用"+core.material.items.fly.name);
}
```

其他的几个按钮，如快捷商店`openQuickShop`，虚拟键盘`openKeyBoard`的重写也几乎完全一样。

### 关门时播放一个动画

关门是在`events.js`中的`closeDoor`函数，因此需要对其进行重写。

然而，我们只需要在这个函数执行之前插一句播放动画，所以并不需要重写整个函数，而是直接插入一行就行。

```js
var closeDoor = core.events.closeDoor; // 先把原始函数用一个变量记录下来
core.events.closeDoor = function (x, y, id, callback) {
    core.drawAnimate('closeDoor', x, y); // 播放 closeDoor 这个动画
    return closeDoor(x, y, id, callback); // 直接调用原始函数
}
```

### 每次跳跃角色前播放一个音效

跳跃角色在`events.js`的`jumpHero`函数，因此需要对其进行重写。

由于只需要额外在函数执行前增加一句音效播放，所以直接插入一行即可。

但是需要注意的是，`jumpHero`中使用了`this._jumpHero_doJump()`，因此使用函数时需要用`call`或者`apply`来告知this是什么。

```js
var jumpHero = core.events.jumpHero; // 先把原始函数用一个变量记录下来
core.events.jumpHero = function (ex, ey, time, callback) {
    core.playSound("jump.mp3"); // 播放一个音效
    return jumpHero.call(core.events, ex, ey, time, callback); // 需要使用`call`来告知this是core.events
}
```

详见[call和apply的用法](https://www.jianshu.com/p/80ea0d1c04f8)。

### 复写全局动画绘制函数

全局动画绘制在`control.js`的`_animationFrame_globalAnimate`函数。

注意到此函数是由`registerAnimationFrame`注册的，因此直接复写是无效的。

其在control.js的注册的定义如下：

```js
// 注册全局动画函数
this.registerAnimationFrame("globalAnimate", true, this._animationFrame_globalAnimate);
```

因此，可以在插件中自行注册一个**同名**的函数来覆盖原始的内容。

```js
// 插件中复写全局动画绘制函数
this.myGlobalAnimate = function (timestamp) {
    // ...... 实际复写的函数内容
}

// 注册同名（globalAnimate）函数来覆盖系统原始内容
core.registerAnimationFrame("globalAnimate", true, "myGlobalAnimate");
```


==========================================================================================

[继续阅读下一章：修改编辑器](editor)


