上传文件至 /

This commit is contained in:
whr12386 2025-01-14 12:27:52 +08:00
parent 3f45b47256
commit b94e3740b1
76 changed files with 68730 additions and 201 deletions

57
L1.md Normal file
View File

@ -0,0 +1,57 @@
# 第一章伤害计算函数getDamageInfo
要求:读懂脚本编辑 - 伤害计算函数getDamageInfo的全部脚本代码无需理解支援的实现
回答如下几个问题:
1. 如下几个局部变量分别是什么意义?
`mon_atk`, `init_damage`, `per_damage`, `hero_per_damage`, `turn`, `damage`
2. 如何理解下面这几句话?
`if (core.hasSpecial(mon_special, 2)) per_damage = mon_atk;`
`var turn = Math.ceil(mon_hp / hero_per_damage);`
`var damage = init_damage + (turn - 1) * per_damage + turn * counterDamage;`
3. 负伤在哪里实现的?
依次实现如下几个怪物属性(仅允许修改`getDamageInfo`函数):
1. **闪避编号31** 受到伤害降低40%。
2. **穿刺编号32** 无视角色70%防御力。
3. **冰冻编号33** 怪物首先冰冻角色3回合冰冻期间每回合额外造成角色护盾的20%伤害。
4. **中子束编号34** 每回合普攻两次,魔攻一次。
5. **残暴斩杀编号35** 战斗开始时如果角色血量不大于怪物血量的200%,则直接暴毙。
6. **窥血为攻编号36** 战斗开始时自身攻击力变为角色当前生命值的10%,向下取整。
7. **匙之力编号37** 角色身上每存在一把黄钥匙最终伤害提升5%;蓝钥匙视为三把黄钥匙,红钥匙视为九把黄钥匙,线性叠加。
8. **崩甲编号38** 怪物每回合附加勇士战斗开始时的护盾数值的0.1倍作为伤害。
9. **混乱编号39** 战斗中,勇士攻防互换。
10. **强击编号40** 怪物第一回合三倍攻击。
11. **暗影庇护编号41** 处于无敌状态但每回合损耗自身2%的最大生命值。
再依次实现如下几个角色技能:
1. 角色每回合回复`flag:x1`%倍护盾的生命值。(如,`flag:x1`是10时每回合回复10%护盾值的生命)
2. 角色每回合造成和受到的伤害均提升`flag:x2`%。(如,`flag:x2`为10时每回合造成和受到伤害都提升10%
3. 角色攻击在战斗时减少`flag:x3`,防御力增加`flag:x3`。
4. 角色无视怪物的`flag:x4`%的防御力。(如,`flag:x4`是10时无视怪物的10%防御力)
5. 角色额外抢攻`flag:x5`回合。
[点此查看答案](L1_answer)

244
L1_answer.md Normal file
View File

@ -0,0 +1,244 @@
```js
function (enemy, hero, x, y, floorId) {
// 获得战斗伤害信息(实际伤害计算函数)
//
// 参数说明:
// enemy该怪物信息
// hero勇士的当前数据如果对应项不存在则会从core.status.hero中取。
// x,y该怪物的坐标查看手册和强制战斗时为undefined
// floorId该怪物所在的楼层
// 后面三个参数主要是可以在光环等效果上可以适用
floorId = floorId || core.status.floorId;
var hero_hp = core.getRealStatusOrDefault(hero, 'hp'),
hero_atk = core.getRealStatusOrDefault(hero, 'atk'),
hero_def = core.getRealStatusOrDefault(hero, 'def'),
hero_mdef = core.getRealStatusOrDefault(hero, 'mdef'),
origin_hero_hp = core.getStatusOrDefault(hero, 'hp'),
origin_hero_atk = core.getStatusOrDefault(hero, 'atk'),
origin_hero_def = core.getStatusOrDefault(hero, 'def');
// 勇士的负属性都按0计算
hero_hp = Math.max(0, hero_hp);
hero_atk = Math.max(0, hero_atk);
hero_def = Math.max(0, hero_def);
hero_mdef = Math.max(0, hero_mdef);
// 怪物的各项数据
// 对坚固模仿等处理扔到了脚本编辑-getEnemyInfo之中
var enemyInfo = core.enemys.getEnemyInfo(enemy, hero, x, y, floorId);
var mon_hp = enemyInfo.hp,
mon_atk = enemyInfo.atk,
mon_def = enemyInfo.def,
mon_special = enemyInfo.special;
// 混乱编号39战斗中勇士攻防互换。
if (core.hasSpecial(mon_special, 39)) {
var temp = hero_atk;
hero_atk = hero_def;
hero_def = temp;
}
// 残暴斩杀编号35战斗开始时如果角色血量不大于怪物血量的200%,则直接暴毙。
if (core.hasSpecial(mon_special, 35) && hero_hp < mon_hp * 2) {
return null;
}
// 窥血为攻编号36战斗开始时自身攻击力变为角色当前生命值的10%,向下取整。
if (core.hasSpecial(mon_special, 36)) {
mon_atk = Math.floor(hero_hp / 10);
}
// 技能3角色攻击在战斗时减少`flag:x3`,防御力增加`flag:x3`。
if (core.getFlag('skill', 0) == 3) {
// 注意这里直接改变的面板攻击力(经过装备增幅后的)而不是基础攻击力。
hero_atk -= core.getFlag('x3', 0);
hero_def += core.getFlag('x3', 0);
}
// 技能4角色无视怪物的`flag:x4`%的防御力。(如,`flag:x4`是10时无视怪物的10%防御力)
if (core.getFlag('skill', 0) == 4) {
mon_def -= Math.floor(core.getFlag('x4', 0) / 100);
}
// 如果是无敌属性,且勇士未持有十字架
if (core.hasSpecial(mon_special, 20) && !core.hasItem("cross"))
return null; // 不可战斗
// 战前造成的额外伤害(可被护盾抵消)
var init_damage = 0;
// 吸血
if (core.hasSpecial(mon_special, 11)) {
var vampire_damage = hero_hp * enemy.value;
// 如果有神圣盾免疫吸血等可以在这里写
// 也可以用hasItem和hasEquip来判定装备
// if (core.hasFlag('shield5')) vampire_damage = 0;
vampire_damage = Math.floor(vampire_damage) || 0;
// 加到自身
if (enemy.add) // 如果加到自身
mon_hp += vampire_damage;
init_damage += vampire_damage;
}
// 每回合怪物对勇士造成的战斗伤害
var per_damage = mon_atk - hero_def;
// 魔攻:战斗伤害就是怪物攻击力
if (core.hasSpecial(mon_special, 2)) per_damage = mon_atk;
// 穿刺编号32无视角色70%防御力。
if (core.hasSpecial(mon_special, 32)) per_damage = Math.floor(mon_atk - 0.3 * hero_def);
// 战斗伤害不能为负值
if (per_damage < 0) per_damage = 0;
// 中子束编号34每回合普攻两次魔攻一次。
if (core.hasSpecial(mon_special, 34)) {
per_damage *= 2;
per_damage += mon_atk;
}
// 崩甲编号38怪物每回合附加勇士战斗开始时的护盾数值的0.1倍作为伤害。
if (core.hasSpecial(mon_special, 38)) {
per_damage += Math.floor(hero_mdef * 0.1);
}
// 2连击 & 3连击 & N连击
if (core.hasSpecial(mon_special, 4)) per_damage *= 2;
if (core.hasSpecial(mon_special, 5)) per_damage *= 3;
if (core.hasSpecial(mon_special, 6)) per_damage *= (enemy.n || 4);
// 每回合的反击伤害;反击是按照勇士的攻击次数来计算回合
var counterDamage = 0;
if (core.hasSpecial(mon_special, 8))
counterDamage += Math.floor((enemy.atkValue || core.values.counterAttack) * hero_atk);
// 先攻
if (core.hasSpecial(mon_special, 1)) init_damage += per_damage;
// 破甲
if (core.hasSpecial(mon_special, 7))
init_damage += Math.floor((enemy.defValue || core.values.breakArmor) * hero_def);
// 净化
if (core.hasSpecial(mon_special, 9))
init_damage += Math.floor((enemy.n || core.values.purify) * hero_mdef);
// 冰冻编号33怪物首先冰冻角色3回合冰冻期间每回合额外造成角色护盾的20%伤害。
if (core.hasSpecial(mon_special, 33)) {
init_damage += Math.floor(3 * (per_damage + 0.2 * hero_mdef));
}
// 勇士每回合对怪物造成的伤害
var hero_per_damage = Math.max(hero_atk - mon_def, 0);
// 技能2角色每回合造成和受到的伤害均提升`flag:x2`%。(如,`flag:x2`为10时每回合造成和受到伤害都提升10%
if (core.getFlag("skill", 0) == 2) {
per_damage += Math.floor(per_damage * core.getFlag('x2', 0) / 100);
hero_per_damage += Math.floor(hero_per_damage * core.getFlag('x2', 0) / 100);
}
// 闪避编号31受到伤害降低40%。
if (core.hasSpecial(mon_special, 31)) {
hero_per_damage = Math.floor(hero_per_damage * 0.6);
}
// 如果没有破防,则不可战斗
if (hero_per_damage <= 0) return null;
// 技能5角色额外抢攻`flag:x5`回合。
if (core.getFlag("skill", 0) == 5) {
mon_hp -= core.getFlag('x5', 0) * hero_per_damage;
// 判定是否已经秒杀 -> 注意mon_hp不能小于等于0否则回合计算会出问题
if (mon_hp <= 0) mon_hp = 1;
}
// 暗影庇护编号41处于无敌状态但每回合损耗自身2%的最大生命值。
if (core.hasSpecial(mon_special, 41)) {
hero_per_damage = Math.floor(mon_hp * 0.02);
}
// 强击编号40怪物第一回合三倍攻击。
// 注意:需要判定角色是否秒杀怪物
if (core.hasSpecial(mon_special, 40) && hero_per_damage < mon_hp) {
init_damage += 2 * mon_atk;
}
// 勇士的攻击回合数;为怪物生命除以每回合伤害向上取整
var turn = Math.ceil(mon_hp / hero_per_damage);
// ------ 支援 ----- //
// 这个递归最好想明白为什么flag:__extraTurn__是怎么用的
var guards = core.getFlag("__guards__" + x + "_" + y, enemyInfo.guards);
var guard_before_current_enemy = false; // ------ 支援怪是先打(true)还是后打(false)
turn += core.getFlag("__extraTurn__", 0);
if (guards.length > 0) {
if (!guard_before_current_enemy) { // --- 先打当前怪物,记录当前回合数
core.setFlag("__extraTurn__", turn);
}
// 获得那些怪物组成小队战斗
for (var i = 0; i < guards.length; i++) {
var gx = guards[i][0],
gy = guards[i][1],
gid = guards[i][2];
// 递归计算支援怪伤害信息这里不传x,y保证不会重复调用
// 这里的mdef传0因为护盾应该只会被计算一次
var info = core.enemys.getDamageInfo(core.material.enemys[gid], { hp: origin_hero_hp, atk: origin_hero_atk, def: origin_hero_def, mdef: 0 });
if (info == null) { // 小队中任何一个怪物不可战斗直接返回null
core.removeFlag("__extraTurn__");
return null;
}
// 已经进行的回合数
core.setFlag("__extraTurn__", info.turn);
init_damage += info.damage;
}
if (guard_before_current_enemy) { // --- 先打支援怪物,增加当前回合数
turn += core.getFlag("__extraTurn__", 0);
}
}
core.removeFlag("__extraTurn__");
// ------ 支援END ------ //
// 最终伤害:初始伤害 + 怪物对勇士造成的伤害 + 反击伤害
var damage = init_damage + (turn - 1) * per_damage + turn * counterDamage;
// 再扣去护盾
damage -= hero_mdef;
// 匙之力编号37角色身上每存在一把黄钥匙最终伤害提升5%;蓝钥匙视为三把黄钥匙,红钥匙视为九把黄钥匙,线性叠加。
// 需要判定是否负伤
if (core.hasSpecial(mon_special, 37) && damage > 0) {
var cnt = core.itemCount("yellowKey") + 3 * core.itemCount("blueKey") + 9 * core.itemCount("redKey");
damage += Math.floor(damage * 0.05 * cnt);
}
// 检查是否允许负伤
if (!core.flags.enableNegativeDamage)
damage = Math.max(0, damage);
// 最后处理仇恨和固伤(因为这两个不能被护盾减伤)
if (core.hasSpecial(mon_special, 17)) { // 仇恨
damage += core.getFlag('hatred', 0);
}
if (core.hasSpecial(mon_special, 22)) { // 固伤
damage += enemy.damage || 0;
}
// 技能1角色每回合回复`flag:x1`%倍护盾的生命值。(如,`flag:x1`是10时每回合回复10%护盾值的生命)
if (core.getFlag("skill", 0) == 1) {
damage -= Math.floor(core.getFlag('x1', 0) / 100 * hero_mdef * turn);
}
return {
"mon_hp": Math.floor(mon_hp),
"mon_atk": Math.floor(mon_atk),
"mon_def": Math.floor(mon_def),
"init_damage": Math.floor(init_damage),
"per_damage": Math.floor(per_damage),
"hero_per_damage": Math.floor(hero_per_damage),
"turn": Math.floor(turn),
"damage": Math.floor(damage)
};
}
```

102
L2.md Normal file
View File

@ -0,0 +1,102 @@
# 第二章怪物特殊属性定义getSpecials表格配置
要求:读懂脚本编辑 - 怪物特殊属性定义getSpecials的实现。理解如何定义一个新的怪物属性。同时还需要理解如何给怪物表格增加新项目。
回答如下问题:
1. 如何定义一个新的怪物属性?
2. 如何取得当前怪物的各项数值?
3. 第五项参数在哪些情况下写1为什么需要
4. 如何给怪物表格定义新项目?如何在伤害计算等地方取用定义的新项目?
5. 当前,有一部分怪物属性使用了相同的表格项,如阻激夹域都使用了`value`等;这样无法同时给怪物增加吸血和领域两个属性。那应该怎么做才能同时支持这两项?
6. 理解《咕工智障》的下述怪物特殊属性并回答:
- 如果一个该属性怪物设置了`hpValue = 10, range = 2`,那么怪物手册中显示该怪物的特殊属性名和描述是什么?
- 如果一个该属性怪物设置了`hpValue = -20, defValue = 30`,那么怪物手册中显示该怪物的特殊属性名和描述是什么?
- 如果一个该属性怪物设置了`hpValue = 10, atkValue = 20, defValue = 30, add = true`,那么怪物手册中显示该怪物的特殊属性名和描述是什么?
- 如果怪物手册中的描述是`周围方形范围内5格的友军生命提升10%攻击降低20%,线性叠加`,那么该怪物的哪些项被设置了成了多少?该怪物的特殊属性名是什么?
- 如果怪物手册中的描述是`周围十字范围内3格的友军生命提升20%攻击提升10%防御提升5%,不可叠加`,那么该怪物的哪些项被设置了成了多少?该怪物的特殊属性名是什么?
```js
[25, function (enemy) {
if (enemy.auraName) return enemy.auraName;
if (enemy.hpValue && enemy.atkValue && enemy.defValue) return "魔力光环";
if (enemy.hpValue) {
if (enemy.hpValue > 0) return "活力光环";
return "懒散光环";
}
if (enemy.atkValue) {
if (enemy.atkValue > 0) return "狂暴光环";
return "静谧光环";
}
if (enemy.defValue) {
if (enemy.defValue > 0) return "坚毅光环";
return "软弱光环";
}
return "光环";
}, function (enemy) {
var str;
if (!enemy.range) {
str = "同楼层所有友军";
} else {
str = "周围" + (enemy.zoneSquare ? "方形" : "十字")
+ "范围内\r[yellow]" + (enemy.range || 1) + "\r格的友军";
}
if (enemy.hpValue) {
if (enemy.hpValue > 0) str += "生命提升\r[yellow]" + enemy.hpValue + "%\r";
else str += "生命降低\r[yellow]" + (-enemy.hpValue) + "%\r";
}
if (enemy.atkValue) {
if (enemy.atkValue > 0) str += "攻击提升\r[yellow]" + enemy.atkValue + "%\r";
else str += "攻击降低\r[yellow]" + (-enemy.atkValue) + "%\r";
}
if (enemy.defValue) {
if (enemy.defValue > 0) str += "防御提升\r[yellow]" + enemy.defValue + "%\r";
else str += "防御降低\r[yellow]" + (-enemy.defValue) + "%\r";
}
str += (enemy.add ? "线性叠加" : "不可叠加");
return str;
}, "#e6e099", 1],
```
7. 理解《咕工智障》的下述怪物特殊属性并回答:
- 吸血数值是如何计算在怪物手册之中的?
- 如何根据角色当前拥有的道具或者变量来修改怪物属性描述?
- 如果要让简单难度下,怪物吸血值减半,特殊属性定义中应该怎么修改?
```js
[11, "吸血", function (enemy) {
var str = "\r[\#e525ff]【红海技能】\r战斗前首先吸取对方的"
+ Math.floor(100 * enemy.value || 0) + "%生命(约"
+ Math.floor((enemy.value || 0) * core.getRealStatus('hp')) + "点)作为伤害"
+ (enemy.add ? ",并把伤害数值加到自身生命上" : "");
if (core.hasItem("I_noVampire")) str += "\r[yellow](对你无效)\r";
return str;
}, "#ff00d2"],
```
8. 定义同时满足如下条件的特殊属性(编号`28`),并和《咕工智障》的写法进行比较。
- 当怪物的 `value28 > 0` 时,特殊属性名为`迅捷光环`
- 当怪物的 `value28 <= 0` 时,特殊属性名为`怠惰光环`
- 当难度为简单(`flag:hard = 1`)时,描述为`此技能在简单难度下无效!`
- 当角色拥有道具 `I123` 时,描述为`此技能已被魔杖抵消!`
- 当怪物的 `range > 0` 时,描述存在`周围[方形/十字]范围内[?]格的友军`;其中这`[]`里面内容由怪物的 `zoneSquare``range` 决定。
- 当怪物的 `range = null` 时,描述存在`当前楼层的友军`
- 当怪物的 `value28 > 0` 时,描述存在`先攻X回合`其中X由`value28`决定(如,`value28 = 5`时,描述存在`先攻5回合`
- 当怪物的 `value28 <= 0` 时,描述存在`被先攻X回合`其中X由`value28`决定(如,`value28 = -5`时,描述存在`被先攻5回合`
9. 定义同时满足如下条件的特殊属性(编号`30`),并和《咕工智障》的写法进行比较。
- 特殊属性名:匙之力
- 当怪物的 `range > 0` 时,描述存在`周围[方形/十字]范围内[?]格内`;其中这`[]`里面内容由怪物的 `zoneSquare``range` 决定。
- 当怪物的 `range = null` 时,描述存在`同楼层内`
- 描述存在 `每存在一个黄钥匙生命增加X点攻击增加Y点防御增加Z点`,其中`X, Y, Z`由怪物的`value, atkValue, defValue`决定。如果其中某一项为0或null则不显示对应项。例如如果`value = 30, defValue = 20`则只显示`生命增加30点防御增加20点`
10. 将阻击、激光、领域、吸血所使用的特殊属性值(当前均为`value`)分离,并实现一个怪物同时拥有`阻击伤害100点激光伤害50点领域伤害200点且吸血20%`这四个特殊属性。
- 需在怪物手册正确显示
- 需正确实现对应的效果(需要修改`伤害计算getDamageInfo和阻激夹域伤害updateCheckBlock`两个函数)
[点此查看答案](L2_answer)

153
L2_answer.md Normal file
View File

@ -0,0 +1,153 @@
1. 在怪物特殊属性中增加一行,分别定义特殊属性的数字、名字、描述、颜色,以及是否是地图类技能(如光环)。
2. 使用`function (enemy) {}`,里面可以取用`enemy.xxx`调用数值;例如`enemy.value`可以调用怪物的value值。
3. 仅在地图类技能时需要写1。当怪物的该项为1时意味着该怪物的技能是和地图上其他图块有关的而不是怪物自身独立的。例如先攻魔攻这些技能就是自身独立和地图上其他图块无关而光环匙之力根据周边钥匙数量提升属性等这些技能就是与地图上其他怪物有关的。
4. 点击「配置表格」,找到怪物相关项目,并照葫芦画瓢即可。常见写法如下。
```js
"value": {
"_leaf": true,
"_type": "textarea",
"_docs": "特殊属性数值",
"_data": "特殊属性的数值\n如领域/阻激/激光怪的伤害值;吸血怪的吸血比例;光环怪增加生命的比例"
},
```
5. 可定义不同项目如v1, v2, v3等分别表示伤害值然后修改特殊描述和实际效果以取用v1, v2, v3等来代替原来的value。参见第10题解答。
6.
- 活力光环周围十字范围内2格的友军生命提升10%,不可叠加。
- 懒散光环同楼层所有友军生命降低20%防御提升30%,不可叠加。(注意,不会显示坚毅光环!)
- 魔力光环同楼层所有友军生命提升10%攻击提升20%防御提升30%,线性叠加。
- range=5, zoneSquare = true, hpValue=10, atkValue=-20, add=true活力光环。
- range=3, hpValue = 20, atkValue = 10, defValue = 5魔力光环。
7.
- 直接使用`enemy.value`获得当前吸血比例,乘以玩家当前生命值`core.getRealStatus('hp')`获得吸血数值,并取整再显示。
- 可以通过额外的if来修改具体的描述。
- 可以在简单难度下将value减半以达到效果。
```js
[11, "吸血", function (enemy) {
var value = enemy.value || 0; // 怪物吸血比例
if (flags.hard == 1) value /= 2; // 简单难度下吸血值减半
// 使用局部变量 value 代替 enemy.value
var str = "\r[\#e525ff]【红海技能】\r战斗前首先吸取对方的"
+ Math.floor(100 * value) + "%生命(约"
+ Math.floor(value * core.getRealStatus('hp')) + "点)作为伤害"
+ (enemy.add ? ",并把伤害数值加到自身生命上" : "");
if (core.hasItem("I_noVampire")) str += "\r[yellow](对你无效)\r";
return str;
}, "#ff00d2"],
```
8.
```js
[28, function (enemy) {
if (enemy.value28 > 0) return "迅捷光环";
return "怠惰光环";
}, function (enemy) {
var str;
// 直接 return 可以不显示具体技能内容。
if (flags.hard == 1) return '此技能在简单难度下无效!';
if (core.hasItem('I123')) return '此技能已被魔杖抵消!';
if (!enemy.range) {
str = "同楼层所有友军";
} else {
str = "周围" + (enemy.zoneSquare ? "方形" : "十字") + "范围内\r[yellow]" + (enemy.range || 1) + "\r格的友军";
}
if (enemy.value28 > 0) {
str += "先攻\r[yellow]" + (enemy.value28 || 0) + "\r回合。";
} else {
str += "被先攻\r[yellow]" + (-enemy.value28 || 0) + "\r回合。";
}
return str;
}, "#00dd00", 1],
```
9.
```js
[30, "匙之力", function (enemy) {
var str = "";
if (enemy.range) {
str += "周围" + (enemy.zoneSquare ? "方形" : "十字") + "范围内\r[yellow]" + enemy.range + "\r格每存在一把黄钥匙";
}
else str += "同楼层每存在一把黄钥匙,";
if (enemy.value) str += "生命增加\r[yellow]" + enemy.value + "\r点";
if (enemy.atkValue) str += "攻击增加\r[yellow]" + enemy.atkValue + "\r点";
if (enemy.defValue) str += "防御增加\r[yellow]" + enemy.defValue + "\r点";
str += "线性叠加。1把蓝钥匙=3把黄钥匙1把红钥匙=10把黄钥匙。";
return str;
}, "#A0A000", 1],
```
10. 使用配置表格增设如下项目也可以使用更好的名称如laserValue
```js
"v1": {
"_leaf": true,
"_type": "textarea",
"_docs": "阻击伤害"
},
"v2": {
"_leaf": true,
"_type": "textarea",
"_docs": "激光伤害"
},
"v3": {
"_leaf": true,
"_type": "textarea",
"_docs": "领域伤害"
},
"v4": {
"_leaf": true,
"_type": "textarea",
"_docs": "吸血比例"
},
```
然后特殊描述可以如下修改分别使用v1, v2, v3, v4代替value
```js
[11, "吸血", function (enemy) { return "战斗前,怪物首先吸取角色的" + Math.floor(100 * enemy.v4 || 0) + "%生命(约" + Math.floor((enemy.v4 || 0) * core.getStatus('hp')) + "点)作为伤害" + (enemy.add ? ",并把伤害数值加到自身生命上" : ""); }, "#dd4448"],
[15, "领域", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "范围内" + (enemy.range || 1) + "格时自动减生命" + (enemy.v3 || 0) + "点"; }, "#c677dd"],
[18, "阻击", function (enemy) { return "经过怪物周围" + (enemy.zoneSquare ? "九宫格" : "十字") + "时自动减生命" + (enemy.v1 || 0) + "点,同时怪物后退一格"; }, "#8888e6"],
[24, "激光", function (enemy) { return "经过怪物同行或同列时自动减生命" + (enemy.v2 || 0) + "点"; }, "#dda0dd"],
```
最后,修改实际效果。
```js
// 吸血getDamageInfo节选
if (core.hasSpecial(mon_special, 11)) {
/** 使用enemy.v4替代enemy.value **/
var vampire_damage = hero_hp * enemy.v4;
// 如果有神圣盾免疫吸血等可以在这里写
// 也可以用hasItem和hasEquip来判定装备
// if (core.hasFlag('shield5')) vampire_damage = 0;
vampire_damage = Math.floor(vampire_damage) || 0;
// 加到自身
if (enemy.add) // 如果加到自身
mon_hp += vampire_damage;
init_damage += vampire_damage;
}
```
```js
// 领域updateCheckBlock节选
for (var dx = -range; dx <= range; dx++) {
for (var dy = -range; dy <= range; dy++) {
if (dx == 0 && dy == 0) continue;
var nx = x + dx,
ny = y + dy,
currloc = nx + "," + ny;
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
// 如果是十字领域,则还需要满足 |dx|+|dy|<=range
if (!zoneSquare && Math.abs(dx) + Math.abs(dy) > range) continue;
/** 使用enemy.v3替代enemy.value **/
damage[currloc] = (damage[currloc] || 0) + (enemy.v3 || 0);
type[currloc] = type[currloc] || {};
type[currloc]["领域伤害"] = true;
}
}
```
激光和阻击同领域实现。

38
L3.md Normal file
View File

@ -0,0 +1,38 @@
# 第三章怪物真实属性获取getEnemyInfo
要求:读懂脚本编辑 - 怪物真实属性getEnemyInfo的实现无需理解支援。理解如何动态修改怪物的三维理解光环效果的实现并知晓如何新增光环效果理解如何定义新变量并在getDamageInfo中取用。
回答如下问题:
1. 为什么要存在`getEnemyInfo`函数以获得怪物真实属性?所有东西都写在`getDamageInfo`里面不好吗?
2. 实现**仿攻编号45**:怪物攻击不低于勇士攻击。
3. 实现**窥血为攻编号36** 战斗开始时自身攻击力变为角色当前生命值的10%,向下取整。这种写法比第一章中在`getDamageInfo`中写法效果更好,为什么?
4. 仅修改`getEnemyInfo`,实现一个怪物`E345`满足:
- 简单难度flag:hard==1无特殊属性。
- 普通难度flag:hard==2拥有先攻属性。
- 困难难度flag:hard==3拥有三连击属性。
- 噩梦难度flag:hard==4同时拥有先攻、魔攻、二连击属性。
- 如果玩家拥有道具`I123`,则直接无视此怪物所有属性。
5. 仅修改`getEnemyInfo`,实现一个道具`I234`**当玩家持有此道具时怪物的血攻防均降低10%。**
6. 仅修改`getEnemyInfo`,实现特殊属性 **「上位压制」特殊编号46**:玩家每低于怪物一个等级(通过`core.getStatus('lv')`获得当前玩家等级,通过`enemy.level`获得该怪物的等级怪物三维提升10%玩家每高于怪物一个等级怪物三维降低5%。
7. 解释光环的缓存原理。为什么光环需要缓存?
8. `index`是什么意思? `var cache = core.status.checkBlock.cache[index];` 是干什么的?
9. 在默认光环的实现中,`hp_buff`, `atk_buff`, `def_buff`, `usedEnemyIds` 分别是干什么的?
10. 在默认光环的实现中,怪物属性的`value`, `atkValue`和`defValue`用于控制怪物的比例增幅。将其效果修改成数值增幅(例如 `value = 50` 表示生命提升50点而不是50%)。
11. 局部光环的实现原理是什么?十字和九宫格光环是如何区分的?
12. 实现 **「协同作战」特殊属性47**:同楼层/周围X格由range和zoneSquare决定每存在一个该属性的队友包括自身生命和攻防对应比例提升提升比例由怪物value, atkValue, defValue决定线性叠加。并在游戏中实际验证效果。注意这个效果和默认光环的区别
13. 实现 **「迅捷光环」特殊属性28**:同楼层/周围X格由range和zoneSquare决定的怪物额外先攻X回合由此怪物的value28决定。并在游戏中实际验证效果。
14. 实现 **「吸血光环」特殊属性48**:同楼层/周围X格由range和zoneSquare决定的怪物拥有「吸血」特殊属性吸血比例由光环怪的`vampireValue`项决定。并在游戏中实际验证效果。

4242
MotaAction.g4 Normal file

File diff suppressed because it is too large Load Diff

10
_sidebar.md Normal file
View File

@ -0,0 +1,10 @@
- [快速上手](start)
- [元件说明](element)
- [事件编辑](event)
- [事件指令](instruction)
- [个性化](personalization)
- [脚本](script)
- [修改编辑器](editor)
- [UI编辑器](ui-editor)
- [附录API列表](api)

145
action.d.ts vendored Normal file
View File

@ -0,0 +1,145 @@
/**
*
*/
type MotaMouseFunc = (x: number, y: number, px: number, py: number) => boolean;
/**
*
*/
type MotaKeyboardFunc = (e: KeyboardEvent) => boolean;
/**
*
*/
interface RegisteredActionMap {
keyDown: (keyCode: number) => boolean;
keyDownCtrl: () => boolean;
keyUp: (keyCode: number, altKey: boolean, fromReplay: boolean) => boolean;
longClick: MotaMouseFunc;
onStatusBarClick: (px: number, py: number, vertical: boolean) => boolean;
ondown: MotaMouseFunc;
onkeyDown: MotaKeyboardFunc;
onkeyUp: MotaKeyboardFunc;
onmousewheel: (direct: 1 | -1) => boolean;
onmove: MotaMouseFunc;
onup: MotaMouseFunc;
pressKey: (keyCode: number) => boolean;
}
type ActionKey = keyof RegisteredActionMap;
/**
* void就变成了actions上的函数...
*/
type VoidedActionFuncs = {
[P in ActionKey]: (...params: Parameters<RegisteredActionMap[P]>) => void;
};
/**
*
*/
interface ClickLoc extends Loc {
/**
* 32
*/
size: 32;
}
interface RegisteredActionOf<K extends ActionKey> {
/**
*
*/
action: K;
/**
*
*/
name: string;
/**
*
*/
priority: number;
/**
*
*/
func: RegisteredActionMap[K];
}
/**
*
*/
interface Actions extends VoidedActionFuncs {
/**
*
*/
readonly LAST: number;
/**
*
*/
readonly _HX_: number;
/**
*
*/
readonly _HY_: number;
/**
*
*/
readonly actions: {
[P in ActionKey]: RegisteredActionOf<P>[];
};
/**
*
* @param action
* @param name 使
* @param func func返回true
* @param priority 0
*/
registerAction<K extends ActionKey>(
action: K,
name: string,
func: RegisteredActionMap[K],
priority?: number
): void;
/**
*
* @param action
* @param name
*/
unregisterAction(action: ActionKey, name: string): void;
/**
*
*/
doRegisteredAction<K extends ActionKey>(
action: K,
...params: Parameters<RegisteredActionMap[K]>
): void;
/**
* (_HX_ - 2, _HX_ + 2)
* @param x
*/
_out(x: number): boolean;
_getNextFlyFloor(delta: number, index: number): number;
_clickGameInfo_openComments();
_getClickLoc(
x: number,
y: number
): {
x: number;
y: number;
size: number;
};
}
declare const actions: new () => Actions;

3222
actions.js Normal file

File diff suppressed because it is too large Load Diff

2415
api.md Normal file

File diff suppressed because it is too large Load Diff

55
blocksdemo.md Normal file
View File

@ -0,0 +1,55 @@
http://127.0.0.1:1055/_docs/#/blocksdemo
方便测试用
这种写法不会生成按钮
```js
'run';
return bg.parseList(['行内']);
// return bg.parse(['asdasd','df'],'event')+'<br>'+bg.parseList(['asfewg','sty']);
```
并且是行内的
这种写法之后的版本会补一个按钮'添加到常用事件', 目前先这样吧
``` MotaAction.event
[
'显示文章',
'...'
]
```
``` MotaAction.shop
[{
"id": "shop1",
"text": "\t[贪婪之神,moneyShop]勇敢的武士啊, 给我${20+2*flag:shop1}金币就可以:",
"textInList": "1F金币商店",
"choices": [
{"text": "生命+800", "need": "status:money>=20+2*flag:shop1", "action": [
{"type": "setValue", "name": "status:money", "operator": "-=", "value": "20+2*flag:shop1"},
{"type": "setValue", "name": "flag:shop1", "operator": "+=", "value": "1"},
{"type": "setValue", "name": "status:hp", "operator": "+=", "value": "800"}
]}
]
},{
"id": "itemShop",
"item": true,
"textInList": "道具商店",
"choices": [
{"id": "yellowKey", "number": 10, "money": 10}
]
},{
"id": "keyShop1",
"textInList": "回收钥匙商店",
"commonEvent": "回收钥匙商店",
"args": ""
}]
```
``` MotaAction.action
'显示文章'
```
``` MotaAction
['MotaAction.action的anction可以省略','只写MotaAction']
```

1
config.json Normal file

File diff suppressed because one or more lines are too long

1095
control.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

3693
control.js Normal file

File diff suppressed because it is too large Load Diff

2252
core.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

548
core.js Normal file
View File

@ -0,0 +1,548 @@
/**
* 初始化 start
*/
"use strict";
// /**
// * @type {CoreMixin}
// */
// const core = (() => {
function core () {
this._WIDTH_ = 13;
this._HEIGHT_ = 13;
this._PX_ = this._WIDTH_ * 32;
this._PY_ = this._HEIGHT_ * 32;
this._HALF_WIDTH_ = Math.floor(this._WIDTH_ / 2);
this._HALF_HEIGHT_ = Math.floor(this._HEIGHT_ / 2);
this.__SIZE__ = main.mode == 'editor' ? 13 : this._HEIGHT_;
this.__PIXELS__ = this.__SIZE__ * 32;
this.__HALF_SIZE__ = Math.floor(this.__SIZE__ / 2);
this.material = {
'animates': {},
'images': {},
'bgms': {},
'sounds': {},
'items': {},
'enemys': {},
'icons': {},
'ground': null,
'grundCanvas': null,
'groundPattern': null,
'autotileEdges': {},
}
this.timeout = {
'turnHeroTimeout': null,
'onDownTimeout': null,
'sleepTimeout': null,
}
this.interval = {
'heroMoveInterval': null,
'onDownInterval': null,
}
this.animateFrame = {
'totalTime': 0,
'totalTimeStart': 0,
'globalAnimate': false,
'globalTime': 0,
'selectorTime': 0,
'selectorUp': true,
'animateTime': 0,
'moveTime': 0,
'lastLegTime': 0,
'leftLeg': true,
'weather': {
'time': 0,
'type': null,
'level': 1,
'nodes': [],
'data': null,
'fog': null,
'cloud': null,
'sun': null
},
"tip": null,
"asyncId": {},
"lastAsyncId": null
}
this.musicStatus = {
'audioContext': null, // WebAudioContext
'bgmStatus': false, // 是否播放BGM
'soundStatus': true, // 是否播放SE
'playingBgm': null, // 正在播放的BGM
'pauseTime': 0, // 上次暂停的时间
'lastBgm': null, // 上次播放的bgm
'gainNode': null,
'playingSounds': {}, // 正在播放的SE
'userVolume': 1.0, // 用户音量
'designVolume': 1.0, //设计音量
'bgmSpeed': 100, // 背景音乐速度
'bgmUsePitch': null, // 是否同时修改音调
'cachedBgms': [], // 缓存BGM内容
'cachedBgmCount': 8, // 缓存的bgm数量
}
this.platform = {
'isOnline': true, // 是否http
'isPC': true, // 是否是PC
'isAndroid': false, // 是否是Android
'isIOS': false, // 是否是iOS
'string': 'PC',
'isWeChat': false, // 是否是微信
'isQQ': false, // 是否是QQ
'isChrome': false, // 是否是Chrome
'supportCopy': false, // 是否支持复制到剪切板
'fileInput': null, // FileInput
'fileReader': null, // 是否支持FileReader
'successCallback': null, // 读取成功
'errorCallback': null, // 读取失败
}
// 样式
this.domStyle = {
scale: 1.0,
ratio: 1.0,
hdCanvas: ["damage", "ui", "data"],
availableScale: [],
isVertical: false,
showStatusBar: true,
toolbarBtn: false,
}
this.bigmap = {
canvas: ["bg", "event", "event2", "fg", "damage"],
offsetX: 0, // in pixel
offsetY: 0,
posX: 0, //
posY: 0,
width: main.mode == 'editor' ? this.__SIZE__ : this._WIDTH_, // map width and height
height: main.mode == 'editor' ? this.__SIZE__ : this._HEIGHT_,
v2: false,
threshold: 1024,
extend: 10,
scale: 1.0,
tempCanvas: null, // A temp canvas for drawing
cacheCanvas: null, // A cache canvas
}
this.saves = {
"saveIndex": null,
"ids": {},
"autosave": {
"data": null,
"time": 0,
"updated": false,
"storage": true, // 是否把自动存档写入文件a
"max": 20, // 自动存档最大回退数
"now": 0,
},
"favorite": [],
"favoriteName": {},
"cache": {}
}
this.initStatus = {
'played': false,
'gameOver': false,
// 勇士属性
'hero': {},
'heroCenter': { 'px': null, 'py': null },
// 当前地图
'floorId': null,
'thisMap': null,
'maps': null,
'bgmaps': {},
'fgmaps': {},
'mapBlockObjs': {},
'checkBlock': {}, // 每个点的阻激夹域信息
'damage': { // 每个点的显伤绘制
'posX': 0,
'posY': 0,
'data': [],
'extraData': [],
},
'lockControl': false,
// 勇士移动状态
'heroMoving': 0,
'heroStop': true,
// 自动寻路相关
'automaticRoute': {
'autoHeroMove': false,
'autoStep': 0,
'movedStep': 0,
'destStep': 0,
'destX': null,
'destY': null,
'offsetX': null,
'offsetY': null,
'autoStepRoutes': [],
'moveStepBeforeStop': [],
'lastDirection': null,
'cursorX': 0,
'cursorY': 0,
"moveDirectly": false,
},
// 按下键的时间:为了判定双击
'downTime': null,
'ctrlDown': false,
'preview': {
'enabled': false,
'prepareDragging': false,
'dragging': false,
'px': 0,
'py': 0,
},
// 路线&回放
'route': [],
'replay': {
'replaying': false,
'pausing': false,
'animate': false, // 正在某段动画中
'failed': false,
'toReplay': [],
'totalList': [],
'speed': 1.0,
'steps': 0,
'save': [],
},
// 录像折叠
'routeFolding': {},
// event事件
'shops': {},
'event': {
'id': null,
'data': null,
'selection': null,
'ui': null,
'interval': null,
},
'autoEvents': [],
'textAttribute': {
'position': "center",
"offset": 0,
"title": [255, 215, 0, 1],
"background": [0, 0, 0, 0.85],
"text": [255, 255, 255, 1],
"titlefont": 22,
"textfont": 16,
"bold": false,
"time": 0,
"letterSpacing": 0,
"animateTime": 0,
},
"globalAttribute": {
'equipName': main.equipName || [],
"statusLeftBackground": main.styles.statusLeftBackground || "url(project/materials/ground.png) repeat",
"statusTopBackground": main.styles.statusTopBackground || "url(project/materials/ground.png) repeat",
"toolsBackground": main.styles.toolsBackground || "url(project/materials/ground.png) repeat",
"borderColor": main.styles.borderColor || [204, 204, 204, 1],
"statusBarColor": main.styles.statusBarColor || [255, 255, 255, 1],
"floorChangingStyle": main.styles.floorChangingStyle || "background-color: black; color: white",
"selectColor": main.styles.selectColor || [255, 215, 0, 1],
"font": main.styles.font || "Verdana"
},
'curtainColor': null,
// 动画
'globalAnimateObjs': [],
'floorAnimateObjs': [],
'boxAnimateObjs': [],
'autotileAnimateObjs': [],
"globalAnimateStatus": 0,
'animateObjs': [],
};
// 标记的楼层列表,用于数据统计
this.markedFloorIds = {};
this.status = {};
this.dymCanvas = {};
if (main.mode == 'editor') {
document.documentElement.style.setProperty('--size', this.__SIZE__);
document.documentElement.style.setProperty('--pixel', this.__PIXELS__ + 'px');
}
}
/////////// 系统事件相关 ///////////
////// 初始化 //////
core.prototype.init = function (coreData, callback) {
this._forwardFuncs();
for (var key in coreData)
core[key] = coreData[key];
this._init_flags();
this._init_platform();
this._init_others();
this._init_plugins();
var b = main.mode == 'editor';
// 初始化画布
for (var name in core.canvas) {
if (core.domStyle.hdCanvas.indexOf(name) >= 0)
core.maps._setHDCanvasSize(core.canvas[name], b ? core.__PIXELS__ : core._PX_, b ? core.__PIXELS__ : core._PY_);
else {
core.canvas[name].canvas.width = (b ? core.__PIXELS__ : core._PX_);
core.canvas[name].canvas.height = (b ? core.__PIXELS__ : core._PY_);
}
}
core.loader._load(function () {
core.extensions._load(function () {
core._afterLoadResources(callback);
});
});
core.dom.musicBtn.style.display = 'block';
core.setMusicBtn();
}
core.prototype._init_flags = function () {
core.flags = core.clone(core.data.flags);
core.values = core.clone(core.data.values);
core.firstData = core.clone(core.data.firstData);
this._init_sys_flags();
// 让你总是拼错!
window.on = true;
window.off = false;
window.ture = true;
window.flase = false;
core.dom.versionLabel.innerText = core.firstData.version;
core.dom.logoLabel.innerText = core.firstData.title;
document.title = core.firstData.title + " - HTML5魔塔";
document.getElementById("startLogo").innerText = core.firstData.title;
(core.firstData.shops || []).forEach(function (t) { core.initStatus.shops[t.id] = t; });
core.maps._initFloors();
// 初始化怪物、道具等
core.material.enemys = core.enemys.getEnemys();
core.material.items = core.items.getItems();
core.material.icons = core.icons.getIcons();
// 初始化自动事件
for (var floorId in core.floors) {
var autoEvents = core.floors[floorId].autoEvent || {};
for (var loc in autoEvents) {
var locs = loc.split(","), x = parseInt(locs[0]), y = parseInt(locs[1]);
for (var index in autoEvents[loc]) {
var autoEvent = core.clone(autoEvents[loc][index]);
if (autoEvent && autoEvent.condition && autoEvent.data) {
autoEvent.floorId = floorId;
autoEvent.x = x;
autoEvent.y = y;
autoEvent.index = index;
autoEvent.symbol = floorId + "@" + x + "@" + y + "@" + index;
autoEvent.condition = core.replaceValue(autoEvent.condition);
autoEvent.data = core.precompile(autoEvent.data);
core.initStatus.autoEvents.push(autoEvent);
}
}
}
}
// 道具的穿上/脱下,视为自动事件
for (var equipId in core.material.items) {
var equip = core.material.items[equipId];
if (equip.cls != 'equips' || !equip.equip) continue;
if (!equip.equip.equipEvent && !equip.equip.unequipEvent) continue;
var equipFlag = '_equipEvent_' + equipId;
var autoEvent1 = {
symbol: "_equipEvent_" + equipId,
currentFloor: false,
multiExecute: true,
condition: "core.hasEquip('" + equipId + "') && !core.hasFlag('" + equipFlag + "')",
data: core.precompile([{ "type": "setValue", "name": "flag:" + equipFlag, "value": "true" }].concat(equip.equip.equipEvent || [])),
};
var autoEvent2 = {
symbol: "_unequipEvent_" + equipId,
currentFloor: false,
multiExecute: true,
condition: "!core.hasEquip('" + equipId + "') && core.hasFlag('" + equipFlag + "')",
data: core.precompile([{ "type": "setValue", "name": "flag:" + equipFlag, "value": "null" }].concat(equip.equip.unequipEvent || [])),
};
core.initStatus.autoEvents.push(autoEvent1);
core.initStatus.autoEvents.push(autoEvent2);
}
core.initStatus.autoEvents.sort(function (e1, e2) {
if (e1.floorId == null) return 1;
if (e2.floorId == null) return -1;
if (e1.priority != e2.priority) return e2.priority - e1.priority;
if (e1.floorId != e2.floorId) return core.floorIds.indexOf(e1.floorId) - core.floorIds.indexOf(e2.floorId);
if (e1.x != e2.x) return e1.x - e2.x;
if (e1.y != e2.y) return e1.y - e2.y;
return e1.index - e2.index;
})
}
core.prototype._init_sys_flags = function () {
if (core.flags.equipboxButton) core.flags.equipment = true;
core.flags.displayEnemyDamage = core.getLocalStorage('enemyDamage', true);
core.flags.displayCritical = core.getLocalStorage('critical', true);
core.flags.displayExtraDamage = core.getLocalStorage('extraDamage', true);
core.flags.enableEnemyPoint = core.getLocalStorage('enableEnemyPoint', core.flags.enableEnemyPoint);
core.flags.leftHandPrefer = core.getLocalStorage('leftHandPrefer', false);
core.flags.extraDamageType = core.getLocalStorage('extraDamageType', 2);
// 行走速度
core.values.moveSpeed = core.getLocalStorage('moveSpeed', core.values.moveSpeed || 100);
core.values.floorChangeTime = core.getLocalStorage('floorChangeTime', core.values.floorChangeTime);
if (core.values.floorChangeTime == null) core.values.floorChangeTime = 500;
core.flags.enableHDCanvas = core.getLocalStorage('enableHDCanvas', !core.platform.isIOS);
}
core.prototype._init_platform = function () {
core.platform.isOnline = location.protocol.indexOf("http") == 0;
if (!core.platform.isOnline) alert("请勿直接打开html文件使用启动服务或者APP进行离线游戏。");
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
core.musicStatus.bgmStatus = core.getLocalStorage('bgmStatus', true);
core.musicStatus.soundStatus = core.getLocalStorage('soundStatus', true);
//新增 userVolume 默认值0.7
core.musicStatus.userVolume = core.getLocalStorage('userVolume', 0.7);
core.musicStatus.userVolume2 = core.getLocalStorage('userVolume2', 0.7);
try {
core.musicStatus.audioContext = new window.AudioContext();
core.musicStatus.gainNode = core.musicStatus.audioContext.createGain();
core.musicStatus.gainNode.gain.value = core.musicStatus.userVolume2;
core.musicStatus.gainNode.connect(core.musicStatus.audioContext.destination);
} catch (e) {
console.log("该浏览器不支持AudioContext");
core.musicStatus.audioContext = null;
}
["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"].forEach(function (t) {
if (navigator.userAgent.indexOf(t) >= 0) {
if (t == 'iPhone' || t == 'iPad' || t == 'iPod') core.platform.isIOS = true;
if (t == 'Android') core.platform.isAndroid = true;
core.platform.isPC = false;
}
});
core.platform.string = core.platform.isPC ? "PC" : core.platform.isAndroid ? "Android" : core.platform.isIOS ? "iOS" : "";
core.platform.supportCopy = document.queryCommandSupported && document.queryCommandSupported("copy");
var chrome = /Chrome\/(\d+)\./i.exec(navigator.userAgent);
if (chrome && parseInt(chrome[1]) >= 50) core.platform.isChrome = true;
core.platform.isSafari = /Safari/i.test(navigator.userAgent) && !/Chrome/i.test(navigator.userAgent);
core.platform.isQQ = /QQ/i.test(navigator.userAgent);
core.platform.isWeChat = /MicroMessenger/i.test(navigator.userAgent);
if (window.FileReader) {
core.platform.fileReader = new FileReader();
core.platform.fileReader.onload = function () {
core.readFileContent(core.platform.fileReader.result);
};
core.platform.fileReader.onerror = function () {
if (core.platform.errorCallback)
core.platform.errorCallback();
}
}
core.flags.enableHDCanvas = core.getLocalStorage('enableHDCanvas', !core.platform.isIOS);
if (main.mode != 'editor') {
core.domStyle.scale = core.getLocalStorage('scale', 1);
if (core.flags.enableHDCanvas) core.domStyle.ratio = Math.max(window.devicePixelRatio || 1, core.domStyle.scale);
}
}
core.prototype._init_others = function () {
// 一些额外的东西
core.material.groundCanvas = document.createElement('canvas').getContext('2d');
core.material.groundCanvas.canvas.width = core.material.groundCanvas.canvas.height = 32;
core.material.groundPattern = core.material.groundCanvas.createPattern(core.material.groundCanvas.canvas, 'repeat');
core.bigmap.tempCanvas = document.createElement('canvas').getContext('2d');
core.bigmap.cacheCanvas = document.createElement('canvas').getContext('2d');
core.loadImage("materials", 'fog', function (name, img) { core.animateFrame.weather.fog = img; });
core.loadImage("materials", "cloud", function (name, img) { core.animateFrame.weather.cloud = img; })
core.loadImage("materials", "sun", function (name, img) { core.animateFrame.weather.sun = img; })
core.loadImage("materials", 'keyboard', function (name, img) { core.material.images.keyboard = img; });
// 记录存档编号
core.saves.saveIndex = core.getLocalStorage('saveIndex', 1);
core.control.getSaveIndexes(function (indexes) { core.saves.ids = indexes; });
}
core.prototype._afterLoadResources = function (callback) {
// 初始化地图
core.initStatus.maps = core.maps._initMaps();
core.control._setRequestAnimationFrame();
// 图片裁剪
(main.splitImages || []).forEach(function (one) {
var name = core.getMappedName(one.name);
if (!core.material.images.images[name]) {
console.warn('找不到图片:' + name + ',无法裁剪');
return;
}
if (!name.endsWith('.png')) {
console.warn('无法裁剪非png格式图片' + name);
return;
}
var arr = core.splitImage(core.material.images.images[name], one.width, one.height);
for (var i = 0; i < arr.length; ++i) {
core.material.images.images[(one.prefix || "") + i + '.png'] = arr[i];
}
});
if (core.plugin._afterLoadResources)
core.plugin._afterLoadResources();
core.showStartAnimate();
if (callback) callback();
}
core.prototype._init_plugins = function () {
core.plugin = new function () { };
for (var name in plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1) {
if (plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1[name] instanceof Function) {
try {
plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1[name].apply(core.plugin);
}
catch (e) {
console.error(e);
console.error("无法初始化插件" + name);
}
}
}
core._forwardFunc("plugin");
}
core.prototype._forwardFuncs = function () {
for (var i = 0; i < main.loadList.length; ++i) {
var name = main.loadList[i];
if (name == 'core') continue;
this._forwardFunc(name);
}
}
core.prototype._forwardFunc = function (name, funcname) {
if (funcname == null) {
for (funcname in core[name]) {
if (funcname.charAt(0) != "_" && core[name][funcname] instanceof Function) {
this._forwardFunc(name, funcname);
}
}
return;
}
if (core[funcname]) {
console.error("ERROR: 无法转发 " + name + " 中的函数 " + funcname + " 到 core 中!同名函数已存在。");
return;
}
var parameterInfo = /^\s*function\s*[\w_$]*\(([\w_,$\s]*)\)\s*\{/.exec(core[name][funcname].toString());
var parameters = (parameterInfo == null ? "" : parameterInfo[1]).replace(/\s*/g, '').replace(/,/g, ', ');
// core[funcname] = new Function(parameters, "return core."+name+"."+funcname+"("+parameters+");");
eval("core." + funcname + " = function (" + parameters + ") {\n\treturn core." + name + "." + funcname + ".apply(core." + name + ", arguments);\n}");
}
core.prototype.doFunc = function (func, _this) {
if (typeof func == 'string') {
func = core.plugin[func];
_this = core.plugin;
}
return func.apply(_this, Array.prototype.slice.call(arguments, 2));
}
// return new Core();
// })();
var core = new core();

152
data.d.ts vendored Normal file
View File

@ -0,0 +1,152 @@
interface MainData {
/**
* id
*/
readonly floorIds: FloorIds[];
/**
*
*/
readonly floorPartitions: [FloorIds, FloorIds?][];
/**
*
*/
readonly tilesets: string[];
/**
*
*/
readonly animates: AnimationIds[];
/**
* bgm
*/
readonly bgms: BgmIds[];
/**
*
*/
readonly images: ImageIds[];
/**
*
*/
readonly sounds: SoundIds[];
/**
*
*/
readonly fonts: FontIds[];
/**
*
*/
readonly nameMap: NameMap;
/**
*
*/
readonly levelChoose: LevelChooseEvent[];
/**
*
*/
readonly equipName: string[];
/**
* bgm
*/
readonly startBgm: BgmIds;
/**
*
*/
readonly styles: MainStyle;
/**
*
*/
readonly splitImages: SplitImageData;
readonly plugin: string[];
}
interface FirstData {
/**
*
*/
title: string;
/**
* mota.config.ts中的一致
*/
name: string;
/**
*
*/
version: string;
/**
*
*/
floorId: FloorIds;
/**
*
*/
hero: HeroStatus;
/**
*
*/
startCanvas: MotaEvent;
/**
*
*/
startText: MotaEvent;
/**
*
*/
shops: ShopEventOf<keyof ShopEventMap>[];
/**
*
*/
levelUp: LevelUpEvent;
/**
*
*/
author: string;
}
/**
*
*/
interface DataCore {
/**
* main信息
*/
readonly main: MainData;
/**
*
*/
readonly firstData: FirstData;
/**
*
*/
readonly values: CoreValues;
/**
*
*/
readonly flags: CoreFlags;
}
declare const data: new () => Omit<DataCore, 'main'>;

546
data.js Normal file
View File

@ -0,0 +1,546 @@
var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d =
{
"main": {
"floorIds": [
"MT0",
"zzt1",
"zhu1",
"zhu2",
"zhu3",
"zhu4",
"zhu5",
"zhu6",
"zhu7",
"zhu8",
"zhu9",
"zhu10",
"zhu11",
"zhu12",
"zhu13",
"zhu14",
"zhu15",
"zhu16",
"zhu17",
"zhu18",
"zhu19",
"zhu20",
"zhu21",
"zhu22",
"zhu23",
"zhu24",
"zhu25",
"dong1",
"dong2",
"dong3",
"dong4",
"dong5",
"dong6",
"dong7",
"dong8",
"dong9"
],
"floorPartitions": [],
"images": [
"Anvil.png",
"BrewingStand.png",
"CraftingTable.png",
"EnchantmentTable.png",
"Furnace.png",
"Smoker-cookingfish.gif",
"achi.png",
"alex2.png",
"anjian.png",
"attribute.png",
"bag.png",
"bear.png",
"bg.jpg",
"blackheart.png",
"blackheart2.png",
"blackheart3.png",
"blackheartw.png",
"blackheartw2.png",
"blackheartw3.png",
"box1.png",
"box10.png",
"box2.png",
"box3.png",
"box4.png",
"box5.png",
"box6.png",
"box7.png",
"box8.png",
"box9.png",
"brave.png",
"breathe.png",
"buttonA.png",
"buttonB.png",
"dragon.png",
"goldheart.png",
"goldheart2.png",
"goldheart3.png",
"goldheartw.png",
"goldheartw2.png",
"goldheartw3.png",
"greenheart.png",
"greenheart2.png",
"greenheart3.png",
"greenheartw.png",
"greenheartw2.png",
"greenheartw3.png",
"head1.png",
"heart.png",
"heart2.png",
"heart3.png",
"heartw.png",
"heartw2.png",
"heartw3.png",
"hero.png",
"heropic1.png",
"hunger.png",
"hunger2.png",
"hunger3.png",
"hungerw.png",
"hungerw2.png",
"hungerw3.png",
"inventory.png",
"jz1.jpg",
"jz2.png",
"jz3.png",
"jz4.png",
"jz5.png",
"mc.png",
"nongtian.png",
"playarrow.png",
"steve2.png",
"task.png",
"tech1.png",
"tech2.png",
"tech3.png",
"winskin.png"
],
"tilesets": [
"magictower.png"
],
"animates": [
"atk_d",
"atk_l",
"atk_r",
"atk_u",
"guangquan",
"hand",
"sword",
"zone"
],
"bgms": [
"bgm.mp3",
"desert1.mp3",
"desert2.mp3",
"forest2.mp3",
"forest5.mp3",
"menu1.mp3",
"menu3.mp3",
"ocean1.mp3",
"ocean3.mp3",
"snowfield1.mp3",
"snowfield2.mp3",
"zhu1.mp3",
"zhu10.mp3",
"zhu11.mp3",
"zhu12.mp3",
"zhu3.mp3",
"zhu4.mp3",
"zhu5.mp3",
"zhu6.mp3"
],
"sounds": [
"E1_1.ogg",
"E1_2.ogg",
"E1_3.ogg",
"E1_4.ogg",
"E21_1.ogg",
"E21_2.ogg",
"E21_3.ogg",
"E21_4.ogg",
"E21_5.ogg",
"E21_6.ogg",
"E25_1.ogg",
"E25_2.ogg",
"E25_3.ogg",
"E25_4.ogg",
"E25_5.ogg",
"E25_6.ogg",
"E33_1.ogg",
"E33_2.ogg",
"E33_3.ogg",
"E33_4.ogg",
"E33_5.ogg",
"E37_1.ogg",
"E37_10.ogg",
"E37_2.ogg",
"E37_3.ogg",
"E37_4.ogg",
"E37_5.ogg",
"E37_6.ogg",
"E37_7.ogg",
"E37_8.ogg",
"E37_9.ogg",
"E41_1.ogg",
"E41_2.ogg",
"E41_3.ogg",
"E41_4.ogg",
"E45_1.ogg",
"E45_2.ogg",
"E45_3.ogg",
"E53_1.ogg",
"E53_2.ogg",
"E53_3.ogg",
"E53_4.ogg",
"E53_5.ogg",
"E53_6.ogg",
"E53_7.ogg",
"E57_1.ogg",
"E57_2.ogg",
"E57_3.ogg",
"E57_4.ogg",
"E57_5.ogg",
"E57_6.ogg",
"E57_7.ogg",
"E57_8.ogg",
"E69_1.ogg",
"E69_2.ogg",
"E69_3.ogg",
"E69_4.ogg",
"E69_5.ogg",
"E9_1.ogg",
"E9_2.ogg",
"E9_3.ogg",
"E9_4.ogg",
"E9_5.ogg",
"E9_6.ogg",
"Villager1.mp3",
"Villager2.mp3",
"Villager3.ogg",
"Villager4.mp3",
"Villager5.mp3",
"Villager6.ogg",
"armour.ogg",
"attack.mp3",
"attack1.ogg",
"attack2.ogg",
"attack3.ogg",
"bomb.mp3",
"break1.ogg",
"break2.ogg",
"cancel.ogg",
"centerFly.mp3",
"chestclosed.ogg",
"chestopen.ogg",
"confirm.ogg",
"cursor.ogg",
"dig_grass.ogg",
"dig_sand.ogg",
"dig_snow.ogg",
"dig_stone.ogg",
"dig_wood.ogg",
"door.mp3",
"door_open.ogg",
"eat1.ogg",
"eat2.ogg",
"equip.mp3",
"error.mp3",
"floor.mp3",
"gem.mp3",
"glass1.ogg",
"glass2.ogg",
"glass3.ogg",
"grass1.ogg",
"grass2.ogg",
"grass3.ogg",
"grass4.ogg",
"gravel1.ogg",
"gravel2.ogg",
"gravel3.ogg",
"gravel4.ogg",
"hit1.ogg",
"hit2.ogg",
"hit3.ogg",
"icePickaxe.mp3",
"item.mp3",
"jump.mp3",
"lava.ogg",
"levelup.ogg",
"load.mp3",
"open_ui.ogg",
"orb.ogg",
"pickaxe.mp3",
"pop.ogg",
"recovery.mp3",
"sand1.ogg",
"sand2.ogg",
"sand3.ogg",
"sand4.ogg",
"save.mp3",
"shop.mp3",
"snow1.ogg",
"snow2.ogg",
"snow3.ogg",
"snow4.ogg",
"step_grass1.ogg",
"step_grass2.ogg",
"step_grass3.ogg",
"step_grass4.ogg",
"step_grass5.ogg",
"step_grass6.ogg",
"step_gravel1.ogg",
"step_gravel2.ogg",
"step_gravel3.ogg",
"step_gravel4.ogg",
"step_sand1.ogg",
"step_sand2.ogg",
"step_sand3.ogg",
"step_sand4.ogg",
"step_sand5.ogg",
"step_snow1.ogg",
"step_snow2.ogg",
"step_snow3.ogg",
"step_snow4.ogg",
"step_stone1.ogg",
"step_stone2.ogg",
"step_stone3.ogg",
"step_stone4.ogg",
"step_stone5.ogg",
"step_stone6.ogg",
"step_swim1.ogg",
"step_swim2.ogg",
"step_swim3.ogg",
"step_swim4.ogg",
"step_wood1.ogg",
"step_wood2.ogg",
"step_wood3.ogg",
"step_wood4.ogg",
"step_wood5.ogg",
"step_wood6.ogg",
"stone1.ogg",
"stone2.ogg",
"stone3.ogg",
"stone4.ogg",
"wood1.ogg",
"wood2.ogg",
"wood3.ogg",
"wood4.ogg",
"worldlevelup.ogg",
"zone.mp3"
],
"fonts": [
"IPix",
"hanyifengshanghei45w",
"number",
"pixelmix"
],
"nameMap": {
"确定": "confirm.ogg",
"取消": "cancel.ogg",
"操作失败": "error.mp3",
"光标移动": "cursor.ogg",
"打开界面": "open_ui.ogg",
"读档": "load.mp3",
"存档": "save.mp3",
"获得道具": "pop.ogg",
"回血": "recovery.mp3",
"炸弹": "bomb.mp3",
"飞行器": "centerFly.mp3",
"开关门": "door.mp3",
"上下楼": "floor.mp3",
"跳跃": "jump.mp3",
"破墙镐": "pickaxe.mp3",
"破冰镐": "icePickaxe.mp3",
"宝石": "gem.mp3",
"阻激夹域": "zone.mp3",
"穿脱装备": "equip.mp3",
"背景音乐": "bgm.mp3",
"攻击": "attack.mp3",
"背景图": "bg.jpg",
"商店": "shop.mp3",
"领域": "zone"
},
"levelChoose": null,
"equipName": [
"主手",
"副手",
"头盔",
"胸甲",
"护腿",
"靴子"
],
"startBgm": null,
"styles": {
"startBackground": "project/images/bg.jpg",
"startVerticalBackground": "project/images/jz1.jpg",
"startLogoStyle": "color: black",
"startButtonsStyle": "background-color: #32369F; opacity: 0.85; color: #FFFFFF; border: #FFFFFF 2px solid; caret-color: #FFD700;",
"statusLeftBackground": "url(\"project/materials/wood1.png\") repeat",
"statusTopBackground": "url(project/images/bg.jpg)",
"toolsBackground": "black",
"floorChangingStyle": "background-color: black; color: white",
"statusBarColor": [
255,
255,
255,
1
],
"borderColor": [
0,
0,
0,
1
],
"selectColor": [
255,
215,
0,
1
],
"font": "IPix"
},
"splitImages": [
{
"name": "dragon.png",
"width": 384,
"height": 96,
"prefix": "dragon_"
}
]
},
"firstData": {
"title": "我的世界:生存 体验版",
"name": "MC_survival0",
"version": "Ver 2.10.3",
"floorId": "zzt1",
"hero": {
"image": "steve2.png",
"animate": false,
"name": "史蒂夫",
"lv": 1,
"hpmax": 20,
"hp": 20,
"manamax": 0,
"mana": 0,
"atk": 2,
"def": 0,
"mdef": 0,
"money": 0,
"exp": 0,
"equipment": [],
"items": {
"constants": {},
"tools": {},
"equips": {}
},
"loc": {
"direction": "up",
"x": 6,
"y": 7
},
"flags": {},
"followers": [],
"steps": 0
},
"startCanvas": [],
"startText": [
{
"type": "if",
"condition": "(!core.isReplaying())",
"true": [
{
"type": "function",
"function": "function(){\ncore.plugin.title();\n}"
},
{
"type": "setValue",
"name": "flag:start",
"value": "true"
},
{
"type": "while",
"condition": "flag:start",
"data": [
{
"type": "sleep",
"time": 1,
"noSkip": true
}
]
}
],
"false": [
{
"type": "function",
"function": "function(){\ncore.plugin.replayHardSelect(core.status.replay.toReplay[0], true);\n}"
}
]
},
{
"type": "function",
"function": "function(){\n//core.hideStatusBar();\ncore.ui._createUIEvent();\n//侧边栏扩展插件让加的\nflags.showExtend = true;\n//extend.values.hideInVertical = true;\n}"
}
],
"shops": [],
"levelUp": [
{
"need": "值",
"title": "0",
"action": []
}
]
},
"values": {
"lavaDamage": 100,
"poisonDamage": 10,
"weakValue": 20,
"redGem": 3,
"blueGem": 3,
"greenGem": 5,
"redPotion": 100,
"bluePotion": 250,
"yellowPotion": 500,
"greenPotion": 800,
"breakArmor": 0.9,
"counterAttack": 0.1,
"purify": 3,
"hatred": 2,
"animateSpeed": 300,
"moveSpeed": 140,
"statusCanvasRowsOnMobile": 3,
"floorChangeTime": 500
},
"flags": {
"statusBarItems": [
"enableFloor",
"enableHPMax",
"enableHP",
"enableAtk",
"enableDef"
],
"autoScale": true,
"extendToolbar": false,
"flyNearStair": false,
"flyRecordPosition": false,
"itemFirstText": false,
"equipboxButton": false,
"enableAddPoint": false,
"enableNegativeDamage": false,
"betweenAttackMax": false,
"useLoop": true,
"startUsingCanvas": true,
"statusCanvas": true,
"enableEnemyPoint": true,
"enableGentleClick": false,
"ignoreChangeFloor": true,
"canGoDeadZone": true,
"enableMoveDirectly": false,
"enableRouteFolding": true,
"disableShopOnDamage": false,
"blurFg": true,
"hideLeftStatusBar": false
}
}

2
docsify.min.js vendored Normal file

File diff suppressed because one or more lines are too long

514
dynamicMapEditor.js Normal file
View File

@ -0,0 +1,514 @@
/**
* 运行时可编辑地图的扩展By fux2黄鸡
*/
"use strict";
function dynamicMapEditor() {
// 所有显示的ID
this.displayIds = [
'none', 'yellowWall', 'blueWall', 'whiteWall', 'yellowDoor', 'blueDoor', 'redDoor', 'star', 'lava', 'lavaNet',
'yellowKey', 'blueKey', 'redKey', 'redGem', 'blueGem', 'greenGem', 'yellowGem',
'redPotion', 'bluePotion', 'yellowPotion', 'greenPotion', 'pickaxe', 'bomb', 'centerFly',
'cls:autotile', 'cls:enemys', 'cls:enemy48'
];
this.items = [];
this.userChanged = [];
this.savedItems = [];
this.dom = null;
this.canvas = null;
this.mapRecord = {};
this.enemyModified = false;
this.valueModified = false;
this.pageId = 0;
this.pageMaxItems = 21;
this.pageMax = 0;
this.selectedIndex = 0;
this.selectedItem = null;
this._init();
}
// ------ init
dynamicMapEditor.prototype._init = function () {
this.dom = document.createElement("canvas");
this.dom.id = 'dynamicMapEditor';
this.dom.style.display = 'none';
this.dom.style.position = 'absolute';
this.dom.style.left = '3px';
this.dom.style.top = '3px';
this.dom.style.zIndex = 99999;
this.canvas = this.dom.getContext("2d");
core.dom.gameGroup.appendChild(this.dom);
this.initInfos();
this.pageMax = Math.ceil(this.items.length / this.pageMaxItems);
core.registerAction('onkeyUp', 'plugin_dme_keydown', this.onKeyUp.bind(this), 200);
core.registerAction('onclick', 'plugin_dme_click', this.onMapClick.bind(this), 200);
this.dom.addEventListener("click",this.onBoxClick.bind(this));
this.showInitHelp();
}
dynamicMapEditor.prototype.initInfos = function () {
this.items = [];
var ids = {};
this.displayIds.forEach(function (v) {
if (v.startsWith("cls:")) {
var cls = v.substr(4);
for (var id in core.maps.blocksInfo) {
var u = core.maps.blocksInfo[id];
if (u && u.cls == cls) {
if (ids[u.id]) continue;
this.items.push(core.getBlockInfo(u.id));
ids[u.id] = true;
}
}
} else if (v == 'none') {
this.items.push({"number": 0, "id": "none", "name": "清除块"});
} else {
this.items.push(core.getBlockInfo(v));
}
}, this);
this.items = this.items.filter(function (v) { return v && v.id && v.number >= 0; });
this.savedItems = core.getLocalStorage('_dynamicMapEditor_savedItems', []);
}
// ------ bind actions
dynamicMapEditor.prototype.isValid = function () {
return main.mode == 'play' && core.isPlaying() && !core.isReplaying() && !core.status.lockControl;
}
dynamicMapEditor.prototype.onKeyUp = function(e) {
if (!this.isValid()) return false;
if (e.keyCode == 219) {
this.openToolBox();
return true;
}
if (!this.isUsingTool) return false;
if (e.keyCode == 220) {
this.undo();
return true;
} else if (e.keyCode == 221) {
this.applyCurrentChange();
return true;
} else if (e.keyCode >= 48 && e.keyCode <= 57) {
// 0-9
if (e.altKey) {
this.savedItem(e.keyCode - 48);
} else {
this.loadItem(e.keyCode - 48);
}
return true;
}
return false;
}
dynamicMapEditor.prototype.onMapClick = function(x, y) {
if (!this.isValid()) return false;
if (!this.isUsingTool || !this.selectedItem) return false;
x += parseInt(core.bigmap.offsetX / 32);
y += parseInt(core.bigmap.offsetY / 32);
var number = this.selectedItem.number;
this.addOperation('put', number, x, y, core.status.floorId);
return true;
}
dynamicMapEditor.prototype.getClickLoc = function (e) {
return {
x: (e.clientX - core.dom.gameGroup.offsetLeft - 3) / core.domStyle.scale,
y: (e.clientY - core.dom.gameGroup.offsetTop - 3) / core.domStyle.scale,
};
}
dynamicMapEditor.prototype.onBoxClick = function (e) {
if (!this.isValid() || !this.isUsingTool) return false;
var loc = this.getClickLoc(e), x = loc.x, y = loc.y;
for(var i = 0; i < this.pageMaxItems; i++) {
var rect = this.itemRect(i);
if(x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h) {
this.onItemClick(i);
return;
}
}
if(y>=350 && y <= 370) {
if (x >= this.offsetX && x <= this.offsetX + 40) {
this.changePage(-1);
} else if (x >= this.offsetX + 40 && x <= this.offsetX + 80) {
this.showHelp(true);
} else if (x >= this.offsetX + 80 && x <= this.offsetX + 120) {
this.changePage(1);
}
}
}
dynamicMapEditor.prototype.onItemClick = function(index) {
var startIndex = this.pageId * this.pageMaxItems;
var item = this.items[startIndex + index];
if(!item) return;
if(index == this.selectedIndex) {
if (core.material.enemys[item.id]) {
var enemy = core.material.enemys[item.id];
var nowData = [enemy.hp, enemy.atk, enemy.def, enemy.special].join(';');
core.myprompt("请输入新怪物属性\n血;攻;防;能力,以分号分隔", nowData, function (result) {
if (result) {
try {
var finalData = result.split(';');
if (finalData.length < 4) throw "";
var hp = parseInt(finalData[0]) || 0;
var atk = parseInt(finalData[1]) || 0;
var def = parseInt(finalData[2]) || 0;
var special = finalData[3].replace(/[\[\]]/g, "")
.split(',').map(function (x) { return parseInt(x); });
if (special.length == 0) special = 0;
else if (special.length == 1) special = special[0];
dynamicMapEditor.addOperation('modify', item.id, hp, atk, def, special);
core.drawTip('已更新' + enemy.name + '的数据');
return;
} catch (e) {}
}
core.drawTip('无效的输入数据');
});
return;
}
if (core.values[item.id] != null) {
var nowData = core.values[item.id];
core.myprompt("请输入新" + (item.name || "") + "数值:", core.values[item.id], function (result) {
if (result) {
dynamicMapEditor.addOperation('value', item.id, parseInt(result) || 0, item.name || "");
core.drawTip('已更新' + (item.name || "") + "的数值");
return;
}
core.drawTip('无效的输入数据');
});
return;
}
} else {
this.selectedIndex = index;
this.selectedItem = item;
this.refreshToolBox();
}
}
// ------ methods
dynamicMapEditor.prototype.openToolBox = function() {
if (!this.isUsingTool && core.domStyle.isVertical) {
core.drawTip("竖屏模式下暂不支持此功能。");
return;
}
this.isUsingTool = !this.isUsingTool;
this.selectedItem = null;
this.selectedIndex = -1;
this.dom.style.display = this.isUsingTool ? 'block' : 'none';
this.dom.style.width = core.dom.statusCanvas.style.width;
this.dom.width = core.dom.statusCanvas.width / core.domStyle.ratio;
this.dom.style.height = core.dom.statusCanvas.style.height;
this.dom.height = core.dom.statusCanvas.height / core.domStyle.ratio;
this.offsetX = this.dom.width / 2 - 60;
this.refreshToolBox();
if (this.isUsingTool) this.showHelp();
}
dynamicMapEditor.prototype.addOperation = function() {
var operation = {};
var type = arguments[0];
operation.type = type;
if (type == 'put') {
operation.number = arguments[1];
operation.x = arguments[2];
operation.y = arguments[3];
operation.floorId = arguments[4];
operation.originNumber = core.floors[operation.floorId].map[operation.y][operation.x];
core.floors[operation.floorId].map[operation.y][operation.x] = operation.number;
core.setBlock(operation.number, operation.x, operation.y, operation.floorId);
this.mapRecord[operation.floorId] = true;
} else if (type == 'modify') {
operation.enemyId = arguments[1];
operation.hp = arguments[2];
operation.atk = arguments[3];
operation.def = arguments[4];
operation.special = arguments[5];
var enemy = core.material.enemys[operation.enemyId];
operation.originHp = enemy.hp;
operation.originAtk = enemy.atk;
operation.originDef = enemy.def;
operation.originSpecial = enemy.special;
enemy.hp = operation.hp;
enemy.atk = operation.atk;
enemy.def = operation.def;
enemy.special = operation.special;
this.enemyModified = true;
} else if (type == 'value') {
operation.id = arguments[1];
operation.value = arguments[2];
operation.name = arguments[3];
operation.originValue = core.values[operation.id];
core.values[operation.id] = operation.value;
this.valueModified = true;
}
this.userChanged.push(operation);
}
dynamicMapEditor.prototype.undo = function() {
var operation = this.userChanged.pop();
if(!operation) {
core.drawTip('没有动作可以撤销');
return;
}
var type = operation.type;
if(type == 'put') { // {originNumber, x, y, floorId}
var originNumber = operation.originNumber;
var x = operation.x;
var y = operation.y;
var floorId = operation.floorId;
core.floors[floorId].map[y][x] = originNumber;
core.setBlock(originNumber, x, y, floorId);
this.mapRecord[floorId] = true;
core.drawTip('已撤销' + floorId + '在(' + x + ',' + y + ')的图块操作');
} else if (type == 'modify') { // {enemyId, originHp, originAtk, originDef, originSpecial}
var enemyId = operation.enemyId;
var hp = operation.originHp;
var atk = operation.originAtk;
var def = operation.originDef;
var special = operation.originSpecial;
var enemy = core.material.enemys[enemyId];
enemy.hp = hp;
enemy.atk = atk;
enemy.def = def;
enemy.special = special;
core.drawTip('已撤销对' + enemy.name + '的属性修改');
this.enemyModified = true;
} else if (type == 'value') { // {id, value, originValue}
var id = operation.id;
var value = operation.originValue;
core.values[operation.id] = operation.originValue;
core.drawTip('已撤销对' + operation.name + "数值的修改");
this.valueModified = true;
}
}
dynamicMapEditor.prototype.changePage = function(delta) {
var newId = this.pageId + delta;
if (newId < 0 || newId >= this.pageMax) return;
this.pageId = newId;
this.selectedItem = null;
this.selectedIndex = -1;
this.refreshToolBox();
}
dynamicMapEditor.prototype.savedItem = function (number) {
if (!this.isUsingTool || this.selectedItem < 0) return;
this.savedItems[number] = [this.pageId, this.selectedIndex];
core.setLocalStorage('_dynamicMapEditor_savedItems', this.savedItems);
core.drawTip("已保存此图块");
}
dynamicMapEditor.prototype.loadItem = function (number) {
if (!this.isUsingTool) return;
var u = this.savedItems[number];
if (!u) return core.drawTip("没有保存的图块!");
this.pageId = u[0];
this.selectedIndex = u[1];
this.selectedItem = this.items[this.pageId * this.pageMaxItems + this.selectedIndex];
this.refreshToolBox();
}
// ------ draw
dynamicMapEditor.prototype.itemRect = function(index) {
return {
'x' : this.offsetX + (index % 3) * 40,
'y' : Math.floor(index / 3) * 50,
'w' : 40,
'h' : 50
};
}
dynamicMapEditor.prototype.refreshToolBox = function() {
if (!this.isUsingTool) return;
core.fillRect(this.canvas, 0, 0, this.dom.width, this.dom.height, '#000000');
var startIndex = this.pageId * this.pageMaxItems;
for (var i = 0; i < this.pageMaxItems; ++i) {
var item = this.items[startIndex + i];
if (!item) break;
var rect = this.itemRect(i);
if (item.image) core.drawImage(this.canvas, item.image, 0, item.height * item.posY, 32, 32, rect.x + 4, rect.y, 32, 32);
if (item.name) {
this.canvas.textAlign = 'center';
core.fillText(this.canvas, item.name, rect.x + 20, rect.y + 44, '#FFFFFF', '11px Verdana', 40);
}
if (core.material.enemys[item.id]) {
this.canvas.textAlign = 'left';
var damageString = core.enemys.getDamageString(item.id);
core.fillBoldText(this.canvas, damageString.damage, rect.x + 5, rect.y + 31, damageString.color, null, '11px Verdana');
var critical = core.enemys.nextCriticals(item.id, 1);
critical = core.formatBigNumber((critical[0]||[])[0], true);
if (critical == '???') critical = '?';
core.fillBoldText(this.canvas, critical, rect.x+5, rect.y+21, '#FFFFFF');
}
}
this.canvas.textAlign = 'center';
this.canvas.fillStyle = '#FFFFFF';
if(this.pageId > 0) core.fillText(this.canvas, '上一页', this.offsetX + 20, 365, '#FFFFFF', '11px Verdana');
if(this.pageId < this.pageMax-1) core.fillText(this.canvas, '下一页',this.offsetX + 100, 365, '#FFFFFF', '11px Verdana');
core.fillText(this.canvas, '帮助', this.offsetX + 60, 365, '#FFFFFF');
var text1 = core.formatBigNumber(core.getRealStatus('hp'), true) + "/" +
core.formatBigNumber(core.getRealStatus('atk'), true) + "/" +
core.formatBigNumber(core.getRealStatus("def"), true) + "/" +
core.formatBigNumber(core.getRealStatus("mdef"), true);
core.fillText(this.canvas, text1, this.offsetX + 60, 380, '#FF7F00', '11px Verdana', 120);
var text2 = core.formatBigNumber(core.getRealStatus('money', true)) + "/" +
core.formatBigNumber(core.getRealStatus('exp'), true) + "/" +
core.itemCount('yellowKey') + '/' + core.itemCount('blueKey') + '/' +
core.itemCount('redKey');
core.fillText(this.canvas, text2, this.offsetX + 60, 395, '#FF7F00', '11px Verdana', 120);
var text3 = core.itemCount('pickaxe') + '/' + core.itemCount('bomb') + '/' +
core.itemCount('centerFly');
if (core.hasFlag('poison')) text3 += "/毒";
if (core.hasFlag('weak')) text3 += "/衰";
if (core.hasFlag('curse')) text3 += "/咒";
core.fillText(this.canvas, text3, this.offsetX + 60, 410, '#FF7F00', '11px Verdana', 120);
if(this.selectedItem) {
var rect = this.itemRect(this.selectedIndex);
core.strokeRect(this.canvas, rect.x, rect.y, rect.w, rect.h, '#FF7F00', 4);
}
}
dynamicMapEditor.prototype.showInitHelp = function () {
if (main.mode != 'play' || core.getLocalStorage('_dynamicMapEditor_init')) return;
var text = "新拓展:运行时动态编辑地图!\n\n在此状态下你可以一边游戏一边编辑地图或者修改数据。\n\n";
text += "进游戏后按 [ 键可以激活,快来尝试吧!\n\n";
text += "点取消后将不再提示本页面。";
core.myconfirm(text, null, function () {
core.setLocalStorage('_dynamicMapEditor_init', true);
if (core.firstData.name != 'template') {
localStorage.removeItem('template__dynamicMapEditor_init');
localStorage.removeItem('template__dynamicMapEditor_help');
}
});
}
dynamicMapEditor.prototype.showHelp = function (fromButton) {
if (main.mode != 'play' || (!fromButton && core.getLocalStorage('_dynamicMapEditor_help'))) return;
var text = "欢迎使用黄鸡编写的运行时编辑拓展!你可以一边游戏一边编辑地图或者修改数据。\n\n";
text += "基本操作:\n - 点击图块再点地图可以放置;\n - 双击图块可以编辑数据;\n";
text += " - [ 键将开关此模式;\n - ] 键将会把改动保存到文件;\n - \\ 键将撤销上步操作。\n";
text += " - Alt+0~9 保存当前图块 \n - 0~9 读取当前图块\n";
text += "最下面三行数据分别是:\n"
text += "血攻防护盾金经黄蓝红破炸飞和debuff。";
if (!fromButton) text += "\n\n点取消将不再提示本页面。";
core.myconfirm(text, null, function () {
if (!fromButton) core.setLocalStorage("_dynamicMapEditor_help", true);
})
}
// ------ save
dynamicMapEditor.prototype.applyCurrentChange = function() {
this.saveEnemys();
this.saveValues();
this.saveFloors();
this.enemyModified = false;
this.valueModified = false;
this.mapRecord = {};
core.drawTip('已将所有改动应用到文件,记得刷新编辑器哦');
}
dynamicMapEditor.prototype.saveEnemys = function () {
if (!this.enemyModified) return;
core.enemys.enemys = core.clone(core.material.enemys);
var datastr = 'var enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80 = \n';
var emap = {};
var estr = JSON.stringify(core.enemys.enemys, function (k, v) {
var t = core.clone(v);
if (t.hp != null) {
delete t.id;
var id_ = ":" + k + ":";
emap[id_] = JSON.stringify(t);
return id_;
} else return t;
}, '\t');
for (var id_ in emap) {
estr = estr.replace('"' + id_ + '"', emap[id_])
}
datastr += estr;
fs.writeFile('project/enemys.js', core.encodeBase64(datastr), 'base64', function (e, d) {});
}
dynamicMapEditor.prototype.saveValues = function () {
if (!this.valueModified) return;
core.data.values = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.values = core.clone(core.values);
var datastr = 'var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d = \n' +
JSON.stringify(data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d, null, '\t');
fs.writeFile('project/data.js', core.encodeBase64(datastr), 'base64', function (e, d) {});
}
dynamicMapEditor.prototype.saveFloors = function () {
if (Object.keys(this.mapRecord).length == 0) return;
core.initStatus.maps = core.maps._initMaps();
for (var floorId in this.mapRecord) {
if (this.mapRecord[floorId]) {
this.saveFloor(floorId);
}
}
}
dynamicMapEditor.prototype.saveFloor = function (floorId) {
var floorData = core.floors[floorId];
var filename = 'project/floors/' + floorId + '.js';
var datastr = ['main.floors.', floorId, '=\n'];
var tempJsonObj = core.clone(floorData);
var tempMap = [['map', ':map:'], ['bgmap', ':bgmap:'], ['fgmap', ':fgmap:']];
tempMap.forEach(function (v) {
v[2] = tempJsonObj[v[0]];
tempJsonObj[v[0]] = v[1];
});
var tempJson = JSON.stringify(tempJsonObj, null, 4);
tempMap.forEach(function (v) {
tempJson = tempJson.replace('"' + v[1] + '"', '[\n' + this.formatMap(v[2], v[0] != 'map') + '\n]')
}, this);
datastr = datastr.concat([tempJson]);
datastr = datastr.join('');
fs.writeFile(filename, core.encodeBase64(datastr), 'base64', function (e, d) {});
}
dynamicMapEditor.prototype.formatMap = function (mapArr, trySimplify) {
if (!mapArr || JSON.stringify(mapArr) == JSON.stringify([])) return '';
if (trySimplify) {
//检查是否是全0二维数组
var jsoncheck = JSON.stringify(mapArr).replace(/\D/g, '');
if (jsoncheck == Array(jsoncheck.length + 1).join('0')) return '';
}
//把二维数组格式化
var formatArrStr = '';
var si = mapArr.length - 1, sk = mapArr[0].length - 1;
for (var i = 0; i <= si; i++) {
formatArrStr += ' [';
for (var k = 0; k <= sk; k++) {
var num = parseInt(mapArr[i][k]);
formatArrStr += Array(Math.max(4 - String(num).length, 0)).join(' ') + num + (k == sk ? '' : ',');
}
formatArrStr += ']' + (i == si ? '' : ',\n');
}
return formatArrStr;
}
// ------ rewrite
var dynamicMapEditor = new dynamicMapEditor();
dynamicMapEditor._resize_statusBar = core.control._resize_statusBar;
core.control._resize_statusBar = function (obj) {
dynamicMapEditor._resize_statusBar.call(this,obj);
dynamicMapEditor.refreshToolBox();
}
dynamicMapEditor.updateStatusBar = core.control.updateStatusBar;
core.control.updateStatusBar = function () {
dynamicMapEditor.refreshToolBox();
dynamicMapEditor.updateStatusBar.apply(core.control, Array.prototype.slice.call(arguments));
}

1054
editor.js Normal file

File diff suppressed because it is too large Load Diff

322
editor.md Normal file
View File

@ -0,0 +1,322 @@
# 修改编辑器
?> 在这一节中,让我们来了解如何修改编辑器(包括配置表格和事件编辑器)
在改动core时, 有时会相应的更改project中存储数据的结构, 而表格和事件编辑器不做相应更改的话就无法顺畅的编辑改动了结构的数据了, 此文档帮助造塔者进行`配置表格`的修改, 以及修改_server/MotaAction.g4和其他相关文件来调整事件编辑器的图块.
## 修改表格
### demo - 给怪物增加 _速度_ 属性
点击素材区的怪物后点击配置表格, 或者直接打开_server/table/comment.js
拉到`【怪物】相关的表格配置`的部分
``` js
// --------------------------- 【怪物】相关的表格配置 --------------------------- //
"enemys": {
"_type": "object",
"_data": {
"id": {
"_leaf": true,
"_type": "disable",
"_docs": "怪物ID",
"_data": "怪物ID可于页面底部修改"
},
"name": {
"_leaf": true,
"_type": "textarea",
"_string": true,
"_data": "名称"
},
"displayIdInBook": {
"_leaf": true,
"_type": "textarea",
"_string": true,
"_docs": "手册映射ID",
"_data": "在怪物手册中映射到的怪物ID。如果此项不为null则在怪物手册中将用目标ID来替换该怪物原本的ID。常被运用在同一个怪物的多朝向上。"
},
"hp": {
"_leaf": true,
"_type": "textarea",
"_data": "生命值"
},
"atk": {
"_leaf": true,
"_type": "textarea",
"_data": "攻击力"
},
```
可以看到, project/enemys.js 中怪物的属性
```js
"greenSlime": {"name":"绿头怪","hp":100,"atk":120,"def":0,"money":1,"exp":1,"point":0,"special":[1,5,7,8]},
```
被逐条列出并解释
把hp的部分复制一份并修改
```js
"speed": {
"_leaf": true,
"_type": "textarea",
"_data": "速度"
},
```
刷新之后, 怪物的表格在hp下面就多出了speed一项, 编辑后出现在了怪物属性中
```js
"greenSlime": {"name":"绿头怪","hp":100,"atk":120,"def":0,"money":1,"exp":1,"point":0,"special":[1,5,7,8],"speed":123123},
```
### 表格配置项含义
可以看出表格配置中的对象是一层套一层的结构, `_`开头的和数据的名字间隔着展开, 忽略所有`_`的层级的话, 这个对象和project中的对象是有相同的结构
**_leaf** 为真的项对应的数据将作为表格的编辑的内容, 数字/字符串/空对象和数组会被自动计算为true, 非空的对象和数组会被计算为false, 手动填写_leaf的值可以强制不展开对象和数组来进行编辑
**_type** 决定了表格如何展示该数据
+ select 使用下拉菜单展示
+ checkbox 使用勾选框展示
+ checkboxSet 使用勾选框组展示
+ 其他 使用文本框展示, 根据类型区分, 双击或点编辑会有以下行为
- textarea 文本编辑器(_type的默认值)
- event 事件编辑器
- material 素材选取
- color 取色器
- point 地图选点
- disable 不允许编辑
- popCheckBoxSet 以弹窗形式多选框
当某个event例如 门信息编辑 无法适应修改后的数据结构, 可以修改事件编辑器, 也可以把_type改成textarea
以上类型的格式要如何写请搜索例如`"_type": "checkboxSet"`来查找例子, 此处不展示
**_range** 被编辑的值会作为`thiseval`来进行检测, 当字符串为真时才接收这个值, 同时当删除表格时会判定_range是否接收null
**拓展性**
注意这是js文件, 可以使用表达式, 如下例子引用了别处的数据作为下拉菜单
```js
"bgm": {
"_leaf": true,
"_type": "select",
"_select": {
"values": [null].concat(Object.keys(editor.core.material.bgms))
},
"_docs": "背景音乐",
"_data": "到达该层后默认播放的BGM"
},
```
同时`_`开头的项可以使用函数. 同一级中`_data`最先被计算. 复杂的结构的注释可以利用函数动态生成
`_data`的参数`key`是各子项的名字, 其他`_`开头的参数是`args`详见editor_table.prototype.objToTable的注释
例如: 自动事件数组, `"_leaf": false`强制展开, 通过`_action`函数即使为空也显示两个空白的项, 同时`_data`函数给自动事件的每一项标记为event
```js
"autoEvent": {
"_type": "object",
"_leaf": false,
"_action": function (args) {
args.vobj = args.vobj || {};
for (var ii = 0; ii < 2; ii++) {
args.vobj[ii] = args.vobj[ii] || null;
}
},
"_data": function (key) {
return {
"_leaf": true,
"_type": "event",
"_event": "autoEvent",
"_data": "自动事件"
}
}
},
```
## 修改事件编辑器
_type为event的表格项, 在双击时会进入事件编辑器. 是由[antlr-blockly](https://github.com/zhaouv/antlr-blockly)生成的图块式的可视化编辑器
_event的内容例如autoEvent则是使用的入口图块的类型
请先查看[antlr-blockly的文档](https://zhaouv.github.io/antlr-blockly/docs/#/README)来了解.g4语法文件和可视化编辑器blockly以及库antlr-blockly的基础知识
### 事件编辑器的运行机制简介
事件编辑器编辑一个表格项的流程有以下几步
+ 加载表格中的json, 将其根据类别转化成图块
+ 通过拖拽填写图块编辑内容
+ 将图块转化回json置入表格
其中图块到json 是由_server/MotaAction.g4完成的
json到图块 是由_server/MotaActionParser.js完成的, 并依赖图块在g4中的定义
图块到json首先由入口方块出发, 入口方块又依次去获取了嵌入在其中的方块和域, 嵌入的方块再递归访问其嵌入的内容, 从而访问了所有方块上域中的信息.
例如 event_m 通过 action_0 获取了其中嵌入的所有语句生成的json, 插入到其自身的键'data'中
```antlr
//事件 事件编辑器入口之一
event_m
: '事件' BGNL? Newline '覆盖触发器' Bool '启用' Bool '通行状态' B_0_List '显伤' Bool BGNL? Newline action+ BEND
/* event_m
tooltip : 编辑魔塔的事件
helpUrl : https://h5mota.com/games/template/_docs/#/event
default : [false,null,null,null,null]
B_0_List_0=eval(B_0_List_0);
var code = {
'trigger': Bool_0?'action':null,
'enable': Bool_1,
'noPass': B_0_List_0,
'displayDamage': Bool_2,
'data': 'data_asdfefw'
}
if (!Bool_0 && Bool_1 && (B_0_List_0===null) && Bool_2) code = 'data_asdfefw';
code=JSON.stringify(code,null,2).split('"data_asdfefw"').join('[\n'+action_0+']\n');
return code;
*/;
```
值得注意的是shoplist和action这样的语句集合, 语句集合会选择其中的最后一项作为父方块的默认块, 所以在希望其允许空时, 要制作一个空项放在语句集合定义的末尾, 例如
```antlr
shoplist
: shopsub
| shopitem
| shopcommonevent
| emptyshop
;
emptyshop
: Newline
/* emptyshop
var code = ' \n';
return code;
*/;
```
json到图块则是从ActionParser.prototype.parse出发, 递归的将js对象变为图块, 例如event类型的json, 使用event_m入口图块, 将obj.trigger/enable/noPass/displayDamage填到域中, obj.data交给this.parseList转化为语句的图块, 与g4中注入的语句做相反的事情
```js
ActionParser.prototype.parse = function (obj,type) {
switch (type) {
case 'event':
if(!obj)obj={};
if(typeof(obj)===typeof('')) obj={'data':[obj]};
if(obj instanceof Array) obj={'data':obj};
return MotaActionBlocks['event_m'].xmlText([
obj.trigger==='action',obj.enable,obj.noPass,obj.displayDamage,this.parseList(obj.data)
]);
```
### demo - 给已有图块添加新域
例如给'播放背景音乐'图块再额外加一个参数列表
不熟悉时, 对任意图块来说这都算是最简单粗暴的修改方式了, 复杂的修改需求也只需要改这一下, 只是填图块时没那么方便
编辑g4时推荐使用vscode, 搜索并安装插件mota-js
<pre><code>playBgm_s
: '播放背景音乐' EvalString '开始播放秒数' Int '持续到下个本事件' Bool <b style='color:green'>'参数列表' JsonEvalString?</b> Newline
/* playBgm_s
tooltip : playBgm: 播放背景音乐
helpUrl : https://h5mota.com/games/template/_docs/#/event?id=playbgm%EF%BC%9A%E6%92%AD%E6%94%BE%E8%83%8C%E6%99%AF%E9%9F%B3%E4%B9%90
default : ["bgm.mp3", 0, true]
allBgms : ['EvalString_0']
colour : this.soundColor
Int_0 = Int_0 ? (', "startTime": '+Int_0) : '';
Bool_0 = Bool_0 ? ', "keep": true' : '';
<b style='color:green'>if (JsonEvalString_0) {
JsonEvalString_0 = ', "args": ' +JsonEvalString_0;
}</b>
var code = '{"type": "playBgm", "name": "'+EvalString_0+'"'+Int_0+Bool_0+<b style='color:green'>JsonEvalString_0+</b>'},\n';
return code;
*/;</code></pre>
使用了类型JsonEvalString, 并在注入的代码中处理了其新增出的JsonEvalString_0要如何体现在这个方块产生的json
此处使用了antlr-blockly产生的'类型名_次数'的默认名称, 也可以开头加一行`name : [null,null,null,'args']`来把这个变量命名为args, 对于setText_s这种域比较多的块会很有帮助.
在MotaAction中, 除了默认的Int/Number等
常规的字符串使用EvalString
ID类型的使用IdString
允许空的自然数使用IntString
位置坐标使用PosString
不转义的多行文本使用RawEvalString
json类型的文本使用JsonEvalString
相应的也需要改json到图块的部分
<pre><code> break;
case "playBgm":
this.next = MotaActionBlocks['playBgm_s'].xmlText([
data.name,data.startTime||0,data.keep||false<b style='color:green'>,data.args</b>,this.next]);
break
case "pauseBgm":</code></pre>
### demo - 增加新语句图块
假设已经制作了一个名为`陨石坠落`的公共事件, 其参数列表是长度3的数组, 依次是陨石颜色, 陨石位置, 爆炸范围.
每次要使用插入公共事件并要填`陨石坠落`和形如`[[231,231,41],[5,7],1]`的长而且带有格式的内容, 并且无法使用取色器和地图选点功能. 此时就可以借助'注册一个自定义事件'并填加新语句图块来解决这个问题.
首先注册成名为'meteorite'的事件, 在插件编写的init中添加以下内容
```js
core.registerEvent('meteorite', function(data){
core.insertAction({"type": "insert", "name": "陨石坠落", "args": [data.color, data.loc, data.range]});
core.doAction();
})
```
然后在g4中修改以下位置(请善用搜索来定位)
<pre><code> | drawSelector_1_s
| unknown_s
| function_s<b style='color:green'>
| meteorite_s</b>
| pass_s
;
text_0_s</code></pre>
并在分号后面添加
```antlr
meteorite_s
: '陨石坠落' '颜色' ColorString? Colour 'x' PosString ',' 'y' PosString '爆炸范围' Int Newline
/* meteorite_s
tooltip : 陨石坠落
helpUrl : https://h5mota.com/games/template/_docs/#/event
default : ["255,0,0,1",'rgba(255,0,0,1)',"0","0",1]
selectPoint : ["PosString_0", "PosString_1"]
colour : this.soundColor
var loc = ', "loc": ['+PosString_0+','+PosString_1+']';
ColorString_0 = ColorString_0 ? (', "color": ['+ColorString_0+']') : '';
var code = '{"type": "meteorite"'+ColorString_0+loc+', "range": '+Int_0+'},\n';
return code;
*/;
```
希望是特效的颜色, 于是在g4搜索'画面闪烁', 找到了`colour : this.soundColor`, 抄出其中的颜色的写法, 地图选点的写法同理, 搜索'强制战斗'后模仿其写法
在MotaActionParser.js中添加json到图块的代码, 同理模仿'画面闪烁'和'强制战斗'
<pre><code> break;
case "playBgm":
this.next = MotaActionBlocks['playBgm_s'].xmlText([
data.name,data.startTime||0,data.keep||false,this.next]);
break;<b style='color:green'>
case "meteorite":
data.color = this.Colour(data.color)
this.next = MotaActionBlocks['meteorite_s'].xmlText([
data.color,'rgba('+data.color+')',data.loc[0],data.loc[1],data.range,this.next]);
break;
</b>case "pauseBgm":</code></pre>
最后在editor_blocklyconfig.js中将其加入到工具栏中
<pre><code> '特效/声音':[<b style='color:green'>
MotaActionBlocks['meteorite_s'].xmlText(),
</b>MotaActionBlocks['sleep_s'].xmlText(),</code></pre>
==========================================================================================
[继续阅读下一章UI编辑器](ui-editor)

1213
editor_blockly.js Normal file

File diff suppressed because it is too large Load Diff

705
editor_blocklyconfig.js Normal file
View File

@ -0,0 +1,705 @@
editor_blocklyconfig=(function(){
// start mark sfergsvae
(function(){
var getCategory = function(name,custom){
for(var node of document.getElementById('toolbox').children) {
if(node.getAttribute('name')==name) return node;
}
var node = document.createElement('category');
node.setAttribute('name',name);
if(custom)node.setAttribute('custom',custom);
document.getElementById('toolbox').appendChild(node);
return node;
}
var toolboxObj = {
'入口方块':[
'<label text="入口方块会根据当前类型在此数组中筛选,具体控制在editor_blockly.entranceCategoryCallback中"></label>',
MotaActionFunctions.actionParser.parse([
"欢迎使用事件编辑器",
"本事件触发一次后会消失",
{"type": "hide", "time": 500},
],'event'),
MotaActionFunctions.actionParser.parse({
"condition": "flag:__door__===2",
"currentFloor": true,
"priority": 0,
"delayExecute": false,
"multiExecute": false,
"data": [
{"type": "openDoor", "loc": [10,5]}
],
},'autoEvent'),
MotaActionBlocks['changeFloor_m'].xmlText(),
MotaActionFunctions.actionParser.parse([{
"id": "shop1",
"text": "\t[贪婪之神,moneyShop]勇敢的武士啊, 给我${20+2*flag:shop1}金币就可以:",
"textInList": "1F金币商店",
"choices": [
{"text": "生命+800", "need": "status:money>=20+2*flag:shop1", "action": [
{"type": "comment", "text": "新版商店中需要手动扣减金币和增加访问次数"},
{"type": "setValue", "name": "status:money", "operator": "-=", "value": "20+2*flag:shop1"},
{"type": "setValue", "name": "flag:shop1", "operator": "+=", "value": "1"},
{"type": "setValue", "name": "status:hp", "operator": "+=", "value": "800"}
]}
]
},{
"id": "itemShop",
"item": true,
"textInList": "道具商店",
"choices": [
{"id": "yellowKey", "number": 10, "money": 10}
]
},{
"id": "keyShop1",
"textInList": "回收钥匙商店",
"commonEvent": "回收钥匙商店",
"args": ""
}],'shop'),
MotaActionBlocks['common_m'].xmlText(),
MotaActionBlocks['beforeBattle_m'].xmlText(),
MotaActionBlocks['afterBattle_m'].xmlText(),
MotaActionBlocks['afterGetItem_m'].xmlText(),
MotaActionBlocks['afterOpenDoor_m'].xmlText(),
MotaActionBlocks['firstArrive_m'].xmlText(),
MotaActionBlocks['eachArrive_m'].xmlText(),
MotaActionBlocks['level_m'].xmlText(),
MotaActionFunctions.actionParser.parse([
['MTx', '']
], 'floorPartition'),
MotaActionBlocks['commonEvent_m'].xmlText(),
MotaActionBlocks['item_m'].xmlText(),
MotaActionFunctions.actionParser.parse([
{"title":"简单", "name": "Easy", "hard": 1, "action": [
{"type": "comment", "text": "在这里写该难度需执行的事件"}
]}
], 'levelChoose'),
MotaActionFunctions.actionParser.parse({
"type": 0, "value": {"atk": 10}, "percentage": {"speed": 10},
}, 'equip'),
MotaActionFunctions.actionParser.parse([{
"name": "bg.jpg", "x": 0, "y": 0, "canvas": "bg"
}], 'floorImage'),
MotaActionFunctions.actionParser.parse({
"time": 160, "openSound": "door.mp3", "closeSound": "door.mp3", "keys": {"yellowKey": 1, "orangeKey": 1}
}, 'doorInfo'),
MotaActionBlocks['faceIds_m'].xmlText(),
MotaActionBlocks['mainStyle_m'].xmlText(),
MotaActionFunctions.actionParser.parse({
"背景音乐": "bgm.mp3", "确定": "confirm.mp3", "攻击": "attack.mp3", "背景图": "bg.jpg", "领域": "zone", "文件名": "file.jpg"
}, 'nameMap'),
MotaActionFunctions.actionParser.parse([
{"name": "hero.png", "width": 32, "height": 32, "prefix": "hero_"},
], 'splitImages'),
],
'显示文字':[
MotaActionBlocks['text_0_s'].xmlText(),
MotaActionBlocks['text_1_s'].xmlText(),
MotaActionFunctions.actionParser.parseList("\t[小妖精,fairy]\f[fairy.png,0,0]欢迎使用事件编辑器(双击方块可直接预览)"),
MotaActionBlocks['moveTextBox_s'].xmlText(),
MotaActionBlocks['clearTextBox_s'].xmlText(),
MotaActionBlocks['comment_s'].xmlText(),
MotaActionBlocks['autoText_s'].xmlText(),
MotaActionBlocks['scrollText_s'].xmlText(),
MotaActionBlocks['setText_s'].xmlText(),
MotaActionBlocks['tip_s'].xmlText(),
MotaActionBlocks['confirm_s'].xmlText(),
MotaActionBlocks['choices_s'].xmlText([
'选择剑或者盾','流浪者','man',0,'',MotaActionBlocks['choicesContext'].xmlText([
'剑','','',null,'','',MotaActionFunctions.actionParser.parseList([{"type": "openDoor", "loc": [3,3]}]),
])
]),
MotaActionBlocks['win_s'].xmlText(),
MotaActionBlocks['lose_s'].xmlText(),
MotaActionBlocks['restart_s'].xmlText(),
],
'数据相关':[
MotaActionBlocks['setValue_s'].xmlText([
MotaActionBlocks['idIdList_e'].xmlText(['status','生命']), '=', '', false
]),
MotaActionBlocks['setEnemy_s'].xmlText(),
MotaActionBlocks['setEnemyOnPoint_s'].xmlText(),
MotaActionBlocks['resetEnemyOnPoint_s'].xmlText(),
MotaActionBlocks['moveEnemyOnPoint_s'].xmlText(),
MotaActionBlocks['moveEnemyOnPoint_1_s'].xmlText(),
MotaActionBlocks['setEquip_s'].xmlText(),
MotaActionBlocks['setFloor_s'].xmlText(),
MotaActionBlocks['setGlobalAttribute_s'].xmlText(),
MotaActionBlocks['setGlobalValue_s'].xmlText(),
MotaActionBlocks['setGlobalFlag_s'].xmlText(),
MotaActionBlocks['setNameMap_s'].xmlText(),
MotaActionBlocks['input_s'].xmlText(),
MotaActionBlocks['input2_s'].xmlText(),
MotaActionBlocks['update_s'].xmlText(),
MotaActionBlocks['moveAction_s'].xmlText(),
MotaActionBlocks['changeFloor_s'].xmlText(),
MotaActionBlocks['changePos_s'].xmlText(),
MotaActionBlocks['battle_s'].xmlText(),
MotaActionBlocks['useItem_s'].xmlText(),
MotaActionBlocks['loadEquip_s'].xmlText(),
MotaActionBlocks['unloadEquip_s'].xmlText(),
MotaActionBlocks['openShop_s'].xmlText(),
MotaActionBlocks['disableShop_s'].xmlText(),
MotaActionBlocks['setHeroIcon_s'].xmlText(),
MotaActionBlocks['follow_s'].xmlText(),
MotaActionBlocks['unfollow_s'].xmlText(),
],
'地图处理':[
MotaActionBlocks['battle_1_s'].xmlText(),
MotaActionBlocks['openDoor_s'].xmlText(),
MotaActionBlocks['closeDoor_s'].xmlText(),
MotaActionBlocks['show_s'].xmlText(),
MotaActionBlocks['hide_s'].xmlText(),
MotaActionBlocks['setBlock_s'].xmlText(),
MotaActionBlocks['setBlockOpacity_s'].xmlText(),
MotaActionBlocks['setBlockFilter_s'].xmlText(),
MotaActionBlocks['turnBlock_s'].xmlText(),
MotaActionBlocks['moveHero_s'].xmlText(),
MotaActionBlocks['move_s'].xmlText(),
MotaActionBlocks['jumpHero_s'].xmlText(),
MotaActionBlocks['jumpHero_1_s'].xmlText(),
MotaActionBlocks['jump_s'].xmlText(),
MotaActionBlocks['jump_1_s'].xmlText(),
MotaActionBlocks['showBgFgMap_s'].xmlText(),
MotaActionBlocks['hideBgFgMap_s'].xmlText(),
MotaActionBlocks['setBgFgBlock_s'].xmlText(),
MotaActionBlocks['showFloorImg_s'].xmlText(),
MotaActionBlocks['hideFloorImg_s'].xmlText(),
],
'事件控制':[
MotaActionBlocks['if_1_s'].xmlText(),
MotaActionBlocks['if_s'].xmlText(),
MotaActionFunctions.actionParser.parseList({"type": "switch", "condition": "判别值", "caseList": [
{"action": [{"type": "comment", "text": "当判别值是值的场合执行此事件"}]},
{"case": "default", "action": [{"type": "comment", "text": "当没有符合的值的场合执行default事件"}]},
]}),
MotaActionFunctions.actionParser.parseList({"type": "for", "name": "temp:A", "from": "0", "to": "12", "step": "1", "data": []}),
MotaActionFunctions.actionParser.parseList({"type": "forEach", "name": "temp:A", "list": ["status:atk","status:def"], "data": []}),
MotaActionBlocks['while_s'].xmlText(),
MotaActionBlocks['dowhile_s'].xmlText(),
MotaActionBlocks['break_s'].xmlText(),
MotaActionBlocks['continue_s'].xmlText(),
MotaActionBlocks['exit_s'].xmlText(),
MotaActionBlocks['trigger_s'].xmlText(),
MotaActionBlocks['insert_1_s'].xmlText(),
MotaActionBlocks['insert_2_s'].xmlText(),
],
'特效表现':[
MotaActionBlocks['sleep_s'].xmlText(),
MotaActionFunctions.actionParser.parseList({"type": "wait", "timeout": 0, "data": [
{"case": "keyboard", "keycode": "13,32", "action": [{"type": "comment", "text": "当按下回车(keycode=13)或空格(keycode=32)时执行此事件\n超时剩余时间会写入flag:timeout"}]},
{"case": "mouse", "px": [0,32], "py": [0,32], "action": [{"type": "comment", "text": "当点击地图左上角时执行此事件\n超时剩余时间会写入flag:timeout"}]},
{"case": "condition", "condition": "flag:type==0\n&&flag:keycode==13", "action": [{"type": "comment", "text": "当满足自定义条件时会执行此事件\n超时剩余时间会写入flag:timeout"}]},
{"case": "timeout", "action": [{"type": "comment", "text": "当超时未操作时执行此事件"}]},
]}),
MotaActionBlocks['waitAsync_s'].xmlText(),
MotaActionBlocks['stopAsync_s'].xmlText(),
MotaActionBlocks['vibrate_s'].xmlText(),
MotaActionBlocks['animate_s'].xmlText(),
MotaActionBlocks['animate_1_s'].xmlText(),
MotaActionBlocks['stopAnimate_s'].xmlText(),
MotaActionBlocks['setViewport_s'].xmlText(),
MotaActionBlocks['setViewport_1_s'].xmlText(),
MotaActionBlocks['lockViewport_s'].xmlText(),
MotaActionBlocks['showStatusBar_s'].xmlText(),
MotaActionBlocks['hideStatusBar_s'].xmlText(),
MotaActionBlocks['setHeroOpacity_s'].xmlText(),
MotaActionBlocks['setCurtain_0_s'].xmlText(),
MotaActionBlocks['setCurtain_1_s'].xmlText(),
MotaActionBlocks['screenFlash_s'].xmlText(),
MotaActionBlocks['setWeather_s'].xmlText(),
MotaActionBlocks['callBook_s'].xmlText(),
MotaActionBlocks['callSave_s'].xmlText(),
MotaActionBlocks['autoSave_s'].xmlText(),
MotaActionBlocks['forbidSave_s'].xmlText(),
MotaActionBlocks['callLoad_s'].xmlText(),
],
'音像处理':[
MotaActionBlocks['showImage_s'].xmlText(),
MotaActionBlocks['showImage_1_s'].xmlText(),
MotaActionBlocks['hideImage_s'].xmlText(),
MotaActionBlocks['showTextImage_s'].xmlText(),
MotaActionBlocks['moveImage_s'].xmlText(),
MotaActionBlocks['rotateImage_s'].xmlText(),
MotaActionBlocks['scaleImage_s'].xmlText(),
MotaActionBlocks['showGif_s'].xmlText(),
MotaActionBlocks['playBgm_s'].xmlText(),
MotaActionBlocks['pauseBgm_s'].xmlText(),
MotaActionBlocks['resumeBgm_s'].xmlText(),
MotaActionBlocks['loadBgm_s'].xmlText(),
MotaActionBlocks['freeBgm_s'].xmlText(),
MotaActionBlocks['playSound_s'].xmlText(),
MotaActionBlocks['playSound_1_s'].xmlText(),
MotaActionBlocks['stopSound_s'].xmlText(),
MotaActionBlocks['setVolume_s'].xmlText(),
MotaActionBlocks['setBgmSpeed_s'].xmlText(),
],
'UI绘制':[
MotaActionBlocks['previewUI_s'].xmlText(),
MotaActionBlocks['clearMap_s'].xmlText(),
MotaActionBlocks['setAttribute_s'].xmlText(),
MotaActionBlocks['setFilter_s'].xmlText(),
MotaActionBlocks['fillText_s'].xmlText(),
MotaActionBlocks['fillBoldText_s'].xmlText(),
MotaActionBlocks['drawTextContent_s'].xmlText(),
MotaActionBlocks['fillRect_s'].xmlText(),
MotaActionBlocks['strokeRect_s'].xmlText(),
MotaActionBlocks['drawLine_s'].xmlText(),
MotaActionBlocks['drawArrow_s'].xmlText(),
MotaActionBlocks['fillPolygon_s'].xmlText(),
MotaActionBlocks['strokePolygon_s'].xmlText(),
MotaActionBlocks['fillEllipse_s'].xmlText(),
MotaActionBlocks['strokeEllipse_s'].xmlText(),
MotaActionBlocks['fillArc_s'].xmlText(),
MotaActionBlocks['strokeArc_s'].xmlText(),
MotaActionBlocks['drawImage_s'].xmlText(),
MotaActionBlocks['drawImage_1_s'].xmlText(),
MotaActionBlocks['drawIcon_s'].xmlText(),
MotaActionBlocks['drawBackground_s'].xmlText(),
MotaActionBlocks['drawSelector_s'].xmlText(),
MotaActionBlocks['drawSelector_1_s'].xmlText(),
],
'原生脚本':[
MotaActionBlocks['function_s'].xmlText(),
MotaActionBlocks['unknown_s'].xmlText(),
],
'值块':[
MotaActionBlocks['setValue_s'].xmlText([
MotaActionBlocks['idIdList_e'].xmlText(['status','生命']), '=', '', false
]),
MotaActionBlocks['expression_arithmetic_0'].xmlText(),
MotaActionBlocks['idFlag_e'].xmlText(),
MotaActionBlocks['idTemp_e'].xmlText(),
MotaActionBlocks['negate_e'].xmlText(),
MotaActionBlocks['unaryOperation_e'].xmlText(),
MotaActionBlocks['bool_e'].xmlText(),
MotaActionBlocks['idString_e'].xmlText(),
MotaActionBlocks['idIdList_e'].xmlText(),
MotaActionBlocks['idFixedList_e'].xmlText(),
MotaActionBlocks['enemyattr_e'].xmlText(),
MotaActionBlocks['blockId_e'].xmlText(),
MotaActionBlocks['blockNumber_e'].xmlText(),
MotaActionBlocks['blockCls_e'].xmlText(),
MotaActionBlocks['hasEquip_e'].xmlText(),
MotaActionBlocks['equip_e'].xmlText(),
MotaActionBlocks['nextXY_e'].xmlText(),
MotaActionBlocks['isReplaying_e'].xmlText(),
MotaActionBlocks['hasVisitedFloor_e'].xmlText(),
MotaActionBlocks['isShopVisited_e'].xmlText(),
MotaActionBlocks['canBattle_e'].xmlText(),
MotaActionBlocks['damage_e'].xmlText(),
MotaActionBlocks['damage_1_e'].xmlText(),
MotaActionBlocks['rand_e'].xmlText(),
MotaActionBlocks['evalString_e'].xmlText(),
],
'常见事件模板':[
'<label text="检测音乐如果没有开启则系统提示开启"></label>',
MotaActionFunctions.actionParser.parseList({"type": "if", "condition": "!core.musicStatus.bgmStatus",
"true": [
"\t[系统提示]你当前音乐处于关闭状态,本塔开音乐游戏效果更佳"
],
"false": []
}),
'<label text="仿新新魔塔一次性商人"></label>',
MotaActionFunctions.actionParser.parse([
{
"type": "if",
"condition": "switch:A",
"true": [
"\t[行商,trader]\b[this]这是购买我的道具后我给玩家的提示。",
{
"type": "comment",
"text": "下一条指令可视情况使用或不使用"
},
{
"type": "hide",
"remove": true,
"time": 250
}
],
"false": [
{
"type": "confirm",
"text": "我有3把黄钥匙\n你出50金币就卖给你。",
"yes": [
{
"type": "if",
"condition": "status:money>=50",
"true": [
{
"type": "setValue",
"name": "status:money",
"operator": "-=",
"value": "50"
},
{
"type": "setValue",
"name": "item:yellowKey",
"operator": "+=",
"value": "3"
},
{
"type": "playSound",
"name": "确定",
"stop": true
},
{
"type": "setValue",
"name": "switch:A",
"value": "true"
}
],
"false": [
{
"type": "playSound",
"name": "操作失败"
},
"\t[行商,trader]\b[this]你的金币不足!"
]
}
],
"no": []
}
]
}
], 'event'),
'<label text="全地图选中一个点"></label>',
MotaActionFunctions.actionParser.parse([
{
"type": "comment",
"text": "全地图选中一个点,需要用鼠标或触屏操作"
},
{
"type": "setValue",
"name": "temp:X",
"value": "status:x"
},
{
"type": "setValue",
"name": "temp:Y",
"value": "status:y"
},
{
"type": "tip",
"text": "再次点击闪烁位置确认"
},
{
"type": "while",
"condition": "true",
"data": [
{
"type": "drawSelector",
"image": "winskin.png",
"code": 1,
"x": "32*temp:X",
"y": "32*temp:Y",
"width": 32,
"height": 32
},
{
"type": "wait"
},
{
"type": "if",
"condition": "(flag:type === 1)",
"true": [
{
"type": "if",
"condition": "((temp:X===flag:x)&&(temp:Y===flag:y))",
"true": [
{
"type": "break",
"n": 1
}
]
},
{
"type": "setValue",
"name": "temp:X",
"value": "flag:x"
},
{
"type": "setValue",
"name": "temp:Y",
"value": "flag:y"
}
]
}
]
},
{
"type": "drawSelector",
"code": 1
},
{
"type": "comment",
"text": "流程进行到这里可以对[X,Y]点进行处理,比如"
},
{
"type": "closeDoor",
"id": "yellowDoor",
"loc": [
"temp:X",
"temp:Y"
]
}
],'event'),
'<label text="多阶段Boss战斗"></label>',
MotaActionFunctions.actionParser.parse([
{
"type": "comment",
"text": "多阶段boss请直接作为战后事件使用"
},
{
"type": "setValue",
"name": "switch:A",
"operator": "+=",
"value": "1"
},
{
"type": "switch",
"condition": "switch:A",
"caseList": [
{
"case": "1",
"action": [
{
"type": "setBlock",
"number": "redSlime"
},
"\t[2阶段boss,redSlime]\b[this]你以为你已经打败我了吗?没听说过史莱姆有九条命吗?"
]
},
{
"case": "2",
"action": [
{
"type": "setBlock",
"number": "blackSlime"
},
"\t[3阶段boss,blackSlime]\b[this]不能消灭我的,只会让我更强大!"
]
},
{
"case": "3",
"action": [
{
"type": "setBlock",
"number": "slimelord"
},
"\t[4阶段boss,slimelord]\b[this]我还能打!"
]
},
{
"case": "4",
"action": [
"\t[4阶段boss,slimelord]我一定会回来的!"
]
}
]
}
],'afterBattle'),
],
'最近使用事件':[
'<label text="此处只是占位符,实际定义在editor_blockly.searchBlockCategoryCallback中"></label>',
]
}
var toolboxgap = '<sep gap="5"></sep>'
//xml_text = MotaActionFunctions.actionParser.parse(obj,type||'event')
//MotaActionBlocks['idString_e'].xmlText()
for (var name in toolboxObj){
var custom = null;
if(name=='最近使用事件')custom='searchBlockCategory';
if(name=='入口方块')custom='entranceCategory';
getCategory(name,custom).innerHTML = toolboxObj[name].join(toolboxgap);
}
var blocklyArea = document.getElementById('blocklyArea');
var blocklyDiv = document.getElementById('blocklyDiv');
var workspace = Blockly.inject(blocklyDiv,{
media: '_server/blockly/media/',
toolbox: document.getElementById('toolbox'),
zoom:{
controls: true,
wheel: false,//滚轮改为上下(shift:左右)翻滚
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.08
},
trashcan: false,
});
editor_blockly.isCommonEntry = function () {
var commonEntries = ['beforeBattle', 'afterBattle', 'afterOpenDoor', 'firstArrive', 'eachArrive', 'commonEvent', 'item'];
return commonEntries.indexOf(editor_blockly.entryType) >= 0;
}
editor_blockly.entranceCategoryCallback = function(workspace) {
var list=toolboxObj['入口方块']
var xmlList = [];
var eventType = (editor_blockly.isCommonEntry() ? 'common' : editor_blockly.entryType)+'_m';
for(var ii=0,blockText;blockText=list[ii];ii++){
if(new RegExp('<block type="'+eventType+'">').exec(blockText)){
var block = Blockly.Xml.textToDom('<xml>'+blockText+'</xml>').firstChild;
block.setAttribute("gap", 5);
xmlList.push(block);
}
}
return xmlList;
}
workspace.registerToolboxCategoryCallback(
'entranceCategory', editor_blockly.entranceCategoryCallback);
editor_blockly.searchBlockCategoryCallback = function(workspace) {
var xmlList = [];
var labels = editor_blockly.searchBlock();
for (var i = 0; i < labels.length; i++) {
var blockText = '<xml>' +
MotaActionBlocks[labels[i]].xmlText() +
'</xml>';
var block = Blockly.Xml.textToDom(blockText).firstChild;
block.setAttribute("gap", 5);
xmlList.push(block);
}
return xmlList;
};
workspace.registerToolboxCategoryCallback(
'searchBlockCategory', editor_blockly.searchBlockCategoryCallback);
var onresize = function(e) {
blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
Blockly.svgResize(workspace);
};
if(typeof editor !== "undefined" && !editor.isMobile)window.addEventListener('resize', onresize, false);
onresize();
//Blockly.svgResize(workspace);
//Blockly.bindEventWithChecks_(workspace.svgGroup_,"wheel",workspace,function(e){});
document.getElementById('blocklyDiv').onmousewheel = function(e){
//console.log(e);
e.preventDefault();
var hvScroll = e.shiftKey?'hScroll':'vScroll';
var mousewheelOffsetValue=20/380*workspace.scrollbar[hvScroll].handleLength_*3;
workspace.scrollbar[hvScroll].handlePosition_+=( ((e.deltaY||0)+(e.detail||0)) >0?mousewheelOffsetValue:-mousewheelOffsetValue);
workspace.scrollbar[hvScroll].onScroll_();
// workspace.setScale(workspace.scale);
}
var doubleClickCheck=[[0,'abc']];
function omitedcheckUpdateFunction(event) {
if(event.type==='create'){
editor_blockly.addIntoLastUsedType(event.blockId);
}
if(event.type==='ui' && event.element == 'click'){
var newClick = [new Date().getTime(),event.blockId];
var lastClick = doubleClickCheck.shift();
doubleClickCheck.push(newClick);
if(newClick[0]-lastClick[0]<500){
if(newClick[1]===lastClick[1]){
editor_blockly.doubleClickBlock(newClick[1]);
}
}
}
// Only handle these events
if (["create", "move", "change", "delete"].indexOf(event.type) < 0) return;
if(editor_blockly.workspace.topBlocks_.length>=2){
editor_blockly.setValue('入口方块只能有一个');
return;
}
var eventType = editor_blockly.entryType;
if(editor_blockly.workspace.topBlocks_.length==1){
var blockType = editor_blockly.workspace.topBlocks_[0].type;
if(blockType!==eventType+'_m' && !(editor_blockly.isCommonEntry() && blockType == 'common_m')){
editor_blockly.setValue('入口方块类型错误');
return;
}
}
try {
var code = Blockly.JavaScript.workspaceToCode(workspace).replace(/\\(i|c|d|e|g|z)/g, '\\\\$1');
editor_blockly.setValue(code);
} catch (error) {
editor_blockly.setValue(String(error));
if (error instanceof OmitedError){
var blockName = error.blockName;
var varName = error.varName;
var block = error.block;
}
// console.log(error);
}
}
workspace.addChangeListener(omitedcheckUpdateFunction);
workspace.addChangeListener(Blockly.Events.disableOrphans);
editor_blockly.workspace = workspace;
MotaActionFunctions.workspace = function(){
return editor_blockly.workspace;
}
// 因为在editor_blockly.parse里已经HTML转义过一次了,所以这里要覆盖掉以避免在注释中出现&lt;等
MotaActionFunctions.xmlText = function (ruleName,inputs,isShadow,comment,collapsed,disabled) {
var rule = MotaActionBlocks[ruleName];
var blocktext = isShadow?'shadow':'block';
var xmlText = [];
xmlText.push('<'+blocktext+' type="'+ruleName+'"'+(collapsed ? ' collapsed="true"' : '')+(disabled ? ' disabled="true"' : '')+'>');
if(!inputs)inputs=[];
for (var ii=0,inputType;inputType=rule.argsType[ii];ii++) {
var input = inputs[ii];
var _input = '';
var noinput = (input===null || input===undefined);
if(noinput && inputType==='field' && MotaActionBlocks[rule.argsGrammarName[ii]].type!=='field_dropdown') continue;
if(noinput && inputType==='field') {
noinput = false;
input = rule.fieldDefault(rule.args[ii])
}
if(noinput) input = '';
if(inputType==='field' && MotaActionBlocks[rule.argsGrammarName[ii]].type==='field_checkbox')input=input?'TRUE':'FALSE';
if(inputType!=='field') {
var subList = false;
var subrulename = rule.argsGrammarName[ii];
var subrule = MotaActionBlocks[subrulename];
if (subrule instanceof Array) {
subrulename=subrule[subrule.length-1];
subrule = MotaActionBlocks[subrulename];
subList = true;
}
_input = subrule.xmlText([],true);
if(noinput && !subList && !isShadow) {
//无输入的默认行为是: 如果语句块的备选方块只有一个,直接代入方块
input = subrule.xmlText();
}
}
xmlText.push('<'+inputType+' name="'+rule.args[ii]+'">');
xmlText.push(_input+input);
xmlText.push('</'+inputType+'>');
}
if(comment){
xmlText.push('<comment>');
xmlText.push(comment);
xmlText.push('</comment>');
}
var next = inputs[rule.args.length];
if (next) {//next
xmlText.push('<next>');
xmlText.push(next);
xmlText.push('</next>');
}
xmlText.push('</'+blocktext+'>');
return xmlText.join('');
}
})();
// end mark sfergsvae
}).toString().split('// start mark sfergsvae')[1].split('// end mark sfergsvae')[0]

52
editor_config.js Normal file
View File

@ -0,0 +1,52 @@
function editor_config() {
this.address = "_server/config.json";
this._isWriting = false;
}
editor_config.prototype.load = function(callback) {
var _this = this;
fs.readFile(this.address, "utf-8", function(e, d) {
if (e) {
console.error("无法读取配置文件, 已重新生成");
_this.config = {};
_this.save(callback);
} else {
try {
_this.config = JSON.parse(d);
if (callback) callback();
} catch (e) {
console.error(e);
_this.config = {};
_this.save(callback);
}
}
});
}
editor_config.prototype.get = function(key, defaultValue) {
value = this.config[key];
return value != null ? value : defaultValue;
}
editor_config.prototype.set = function(key, value, callback) {
this.config[key] = value;
if (callback !== false) this.save(callback);
}
editor_config.prototype.save = function(callback) {
// 读写锁防止写文件冲突
if (this._isWriting) return;
try {
this._isWriting = true;
var _this = this;
fs.writeFile(this.address, JSON.stringify(this.config) ,'utf-8', function(e) {
_this._isWriting = false;
if (e) console.error("写入配置文件失败");
if (callback instanceof Function) callback();
})
} catch (e) {
this._isWriting = false;
console.error(e);
if (callback instanceof Function) callback();
}
}

1228
editor_datapanel.js Normal file

File diff suppressed because it is too large Load Diff

1118
editor_file.js Normal file

File diff suppressed because it is too large Load Diff

199
editor_game.js Normal file
View File

@ -0,0 +1,199 @@
editor_game_wrapper = function (editor, main, core) {
// 原则上重构后只有此文件允许`\s(main|core)`形式的调用, 以及其初始化 editor_game_wrapper(editor, main, core)
editor_game = function () {
this.replacerRecord = {}
}
//////////////////// 修改数据相关 ////////////////////
// 三个 replacer 和 replacerRecord 暂时放在此处
editor_game.prototype.replacerForLoading = function (_key, value) {
var rmap = editor.game.replacerRecord;
if (value instanceof Function) {
var guid_ = editor.util.guid()
rmap[guid_] = value.toString()
return guid_
} else if (value === null) {
// 为了包含plugins的新建
var guid_ = editor.util.guid()
rmap[guid_] = null
return guid_
}
return value
}
editor_game.prototype.replacerForSaving = function (_key, value) {
var rmap = editor.game.replacerRecord;
if (rmap.hasOwnProperty(value)) {
return rmap[value]
}
return value
}
editor_game.prototype.getValue = function (field) {
var rmap = editor.game.replacerRecord;
var value = eval(field)
if (rmap.hasOwnProperty(oldval)) {
return rmap[value]
} else {
return value
}
}
editor_game.prototype.setValue = function (field, value) {
var rmap = editor.game.replacerRecord;
var oldval = eval(field)
if (rmap.hasOwnProperty(oldval)) {
rmap[value] = eval(value)
} else {
eval(field + '=' + value)
}
}
editor_game.prototype.replacerWithoutRecord = function (_key, value) {
if (value instanceof Function) {
return value.toString()
} else return value
}
editor_game.prototype.fixFunctionInGameData = function () {
var rf = editor.game.replacerWithoutRecord
core.floors = JSON.parse(JSON.stringify(core.floors, rf));
core.data = JSON.parse(JSON.stringify(core.data, rf));
data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d = JSON.parse(JSON.stringify(data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d, rf));
}
//////////////////// 加载游戏数据相关 ////////////////////
// 初始化数字与地图图块的对应
editor_game.prototype.idsInit = function (maps, icons) {
editor.ids = [0];
editor.indexs = [];
var MAX_NUM = 0;
var keys = Object.keys(maps_90f36752_8815_4be8_b32b_d7fad1d0542e);
for (var ii = 0; ii < keys.length; ii++) {
var v = ~~keys[ii];
if (v > MAX_NUM && v < core.icons.tilesetStartOffset) MAX_NUM = v;
}
editor.MAX_NUM = MAX_NUM;
var getInfoById = function (id) {
var block = maps.initBlock(0, 0, id);
if (Object.prototype.hasOwnProperty.call(block, 'event')) {
return block;
}
}
var point = 0;
for (var i = 0; i <= MAX_NUM; i++) {
var indexBlock = getInfoById(i);
editor.indexs[i] = [];
if (indexBlock) {
var id = indexBlock.event.id;
var indexId = indexBlock.id;
var allCls = Object.keys(icons);
if (i == 17) {
editor.ids.push({ 'idnum': 17, 'id': id, 'images': 'terrains' });
point++;
editor.indexs[i].push(point);
continue;
}
for (var j = 0; j < allCls.length; j++) {
if (id in icons[allCls[j]]) {
editor.ids.push({ 'idnum': indexId, 'id': id, 'images': allCls[j], 'y': icons[allCls[j]][id] });
point++;
editor.indexs[i].push(point);
}
}
}
}
editor.indexs[0] = [0];
var startOffset = core.icons.tilesetStartOffset;
for (var i in core.tilesets) {
var imgName = core.tilesets[i];
var img = core.material.images.tilesets[imgName];
var width = Math.floor(img.width / 32), height = Math.floor(img.height / 32);
if (img.width % 32 || img.height % 32) {
// alert(imgName + '的长或宽不是32的整数倍, 请修改后刷新页面');
console.warn(imgName + '的长或宽不是32的整数倍, 请修改后刷新页面');
}
if (img.width * img.height > 32 * 32 * 3000) {
// alert(imgName + '上的图块数量超过了3000请修改后刷新页面');
console.warn(imgName + '上的图块数量超过了3000请修改后刷新页面');
}
for (var id = startOffset; id < startOffset + width * height; id++) {
var x = (id - startOffset) % width, y = parseInt((id - startOffset) / width);
var indexBlock = getInfoById(id);
editor.ids.push({ 'idnum': id, 'id': indexBlock.event.id, 'images': imgName, "x": x, "y": y, isTile: true });
point++;
editor.indexs[id] = [point];
}
startOffset += core.icons.tilesetStartOffset;
}
}
// 获取当前地图
editor_game.prototype.fetchMapFromCore = function () {
var mapArray = core.getMapArray(core.status.floorId);
editor.map = mapArray.map(function (v) {
return v.map(function (v) {
var x = parseInt(v), y = editor.indexs[x];
if (y == null) {
printe("素材数字" + x + "未定义。是不是忘了注册或者接档时没有覆盖icons.js和maps.js");
y = [0];
}
return editor.ids[y[0]]
})
});
editor.currentFloorId = core.status.floorId;
editor.currentFloorData = core.floors[core.status.floorId];
// 补出缺省的数据
editor.currentFloorData.autoEvent = editor.currentFloorData.autoEvent || {};
//
for (var ii = 0, name; name = editor.dom.canvas[ii]; ii++) {
name += 'map';
var mapArray = editor.currentFloorData[name];
if (!mapArray || JSON.stringify(mapArray) == JSON.stringify([])) {//未设置或空数组
//与editor.map同形的全0
mapArray = eval('[' + Array(editor.map.length + 1).join('[' + Array(editor.map[0].length + 1).join('0,') + '],') + ']');
}
mapArray = core.maps._processInvalidMap(mapArray, editor.map[0].length, editor.map.length);
editor[name] = mapArray.map(function (v) {
return v.map(function (v) {
var x = parseInt(v), y = editor.indexs[x];
if (y == null) {
printe("素材数字" + x + "未定义。是不是忘了注册或者接档时没有覆盖icons.js和maps.js");
y = [0];
}
return editor.ids[y[0]]
})
});
}
}
// 获取地图列表
editor_game.prototype.getFloorFileList = function (callback) {
// callback([Array<String>,err:String])
editor.util.checkCallback(callback);
/* editor.fs.readdir('project/floors',function(err, data){
callback([data,err]);
}); */
callback([editor.core.floorIds, null]);
}
editor_game.prototype.doCoreFunc = function (funcname) {
return core[funcname].apply(core, Array.prototype.slice.call(arguments, 1));
}
editor_game.prototype.getEnemy = function (id) {
return core.material.enemys[id];
}
editor_game.prototype.getFirstData = function () {
return core.firstData;
}
editor.constructor.prototype.game = new editor_game();
}
//editor_game_wrapper(editor);

193
editor_listen.js Normal file
View File

@ -0,0 +1,193 @@
editor_listen_wrapper = function (editor) {
editor.constructor.prototype.listen = function () {
editor.dom.body.onmousedown = editor.uifunctions.body_click;
editor.dom.eui.oncontextmenu = function (e) { e.preventDefault() } // 自定义了右键菜单, 阻止默认行为
editor.dom.midMenu.oncontextmenu = function (e) { e.preventDefault() }
editor.dom.eui.ondblclick = editor.uifunctions.map_doubleClick
editor.dom.eui.onmousedown = editor.uifunctions.map_ondown
editor.dom.eui.onmousemove = editor.uifunctions.map_onmove
editor.dom.eui.onmouseup = editor.uifunctions.map_onup
editor.dom.eui.onmouseout = editor.uifunctions.map_onmoveout
editor.dom.mid.onmousewheel = editor.uifunctions.map_mousewheel
editor.uivalues.shortcut = editor.config.get('shortcut', { 48: 0, 49: 0, 50: 0, 51: 0, 52: 0, 53: 0, 54: 0, 55: 0, 56: 0, 57: 0 });
editor.dom.body.onkeydown = editor.uifunctions.body_shortcut
editor.uivalues.scrollBarHeight = editor.uifunctions.getScrollBarHeight();
editor.dom.iconExpandBtn.style.display = 'block';
editor.dom.iconExpandBtn.innerText = editor.uivalues.folded ? "展开素材区" : "折叠素材区";
editor.dom.iconExpandBtn.onclick = editor.uifunctions.fold_material_click
editor.dom.iconLib.onmousedown = editor.uifunctions.material_ondown
editor.dom.iconLib.onmousemove = editor.uifunctions.material_onmove
editor.dom.iconLib.onmouseup = editor.uifunctions.material_onup
editor.dom.iconLib.oncontextmenu = function (e) { e.preventDefault() }
editor.dom.extraEvent.onmouseup = editor.uifunctions.extraEvent_click
editor.dom.chooseThis.onmouseup = editor.uifunctions.chooseThis_click
editor.dom.chooseInRight.onmouseup = editor.uifunctions.chooseInRight_click
editor.dom.copyLoc.onmouseup = editor.uifunctions.copyLoc_click
editor.dom.pasteLoc.onmouseup = editor.uifunctions.pasteLoc_click
editor.dom.clearEvent.onmouseup = editor.uifunctions.clearEvent_click
editor.dom.clearLoc.onmouseup = editor.uifunctions.clearLoc_click
editor.dom.undoFloor.onclick = editor.uifunctions.undoFloor_click
editor.dom.selectFloorBtn.onclick = editor.uifunctions.selectFloorBtn_click
editor.dom.editorTheme.onchange = editor.uifunctions.editorTheme_onchange
editor.dom.lastUsed.onmouseup = editor.uifunctions.lastUsed_click;
editor.dom.lastUsed.oncontextmenu = function (e) { e.preventDefault(); }
editor.dom.clearLastUsedBtn.onclick = editor.uifunctions.clearLastUsedBtn_click;
editor.dom.showMovable.onchange = editor.uifunctions.showMovable_onchange;
editor.dom.brushMod.onchange = editor.uifunctions.brushMod_onchange
if (editor.dom.brushMod2) editor.dom.brushMod2.onchange = editor.uifunctions.brushMod2_onchange;
if (editor.dom.brushMod3) editor.dom.brushMod3.onchange = editor.uifunctions.brushMod3_onchange;
if (editor.dom.brushMod4) editor.dom.brushMod4.onchange = editor.uifunctions.brushMod4_onchange;
editor.dom.layerMod.onchange = editor.uifunctions.layerMod_onchange
if (editor.dom.layerMod2) editor.dom.layerMod2.onchange = editor.uifunctions.layerMod2_onchange;
if (editor.dom.layerMod3) editor.dom.layerMod3.onchange = editor.uifunctions.layerMod3_onchange;
editor.uifunctions.viewportButtons_func()
window.onbeforeunload = function () {
var saveFloor = document.getElementById('saveFloor');
if (saveFloor && saveFloor.classList.contains('highlight')) {
return '你尚未保存地图,确定退出么?';
}
return null;
}
}
editor.constructor.prototype.mobile_listen = function () {
if (!editor.isMobile) return;
var mobileview = document.getElementById('mobileview');
var mid = document.getElementById('mid');
var right = document.getElementById('right');
// var mobileeditdata = document.getElementById('mobileeditdata');
editor.showdataarea = function (callShowMode) {
mid.style = 'z-index:-1;opacity: 0;';
right.style = 'z-index:-1;opacity: 0;';
// mobileeditdata.style = '';
if (callShowMode) editor.mode.showMode(editor.dom.editModeSelect.value);
editor.uifunctions.hideMidMenu();
}
mobileview.children[0].onclick = function () {
editor.showdataarea(true)
}
mobileview.children[1].onclick = function () {
mid.style = 'z-index:110';
right.style = 'z-index:-1;opacity: 0;';
// mobileeditdata.style = 'z-index:-1;opacity: 0;';
editor.lastClickId = '';
}
mobileview.children[3].onclick = function () {
mid.style = 'z-index:-1;opacity: 0;';
right.style = 'z-index:110';
// mobileeditdata.style = 'z-index:-1;opacity: 0;';
editor.lastClickId = '';
}
/*
var gettrbyid = function () {
if (!editor.lastClickId) return false;
thisTr = document.getElementById(editor.lastClickId);
input = thisTr.children[2].children[0].children[0];
field = thisTr.children[0].getAttribute('title');
cobj = JSON.parse(thisTr.children[1].getAttribute('cobj'));
return [thisTr, input, field, cobj];
}
mobileeditdata.children[0].onclick = function () {
var info = gettrbyid()
if (!info) return;
info[1].ondblclick()
}
mobileeditdata.children[1].onclick = function () {
var info = gettrbyid()
if (!info) return;
printf(info[2])
}
mobileeditdata.children[2].onclick = function () {
var info = gettrbyid()
if (!info) return;
printf(info[0].children[1].getAttribute('title'))
}
*/
//=====
document.body.ontouchstart = document.body.onmousedown;
document.body.onmousedown = null;
editor.dom.eui.ontouchstart = editor.dom.eui.onmousedown
editor.dom.eui.onmousedown = null
editor.dom.eui.ontouchmove = editor.dom.eui.onmousemove
editor.dom.eui.onmousemove = null
editor.dom.eui.ontouchend = editor.dom.eui.onmouseup
editor.dom.eui.onmouseup = null
editor.dom.chooseThis.ontouchstart = editor.dom.chooseThis.onmousedown
editor.dom.chooseThis.onmousedown = null
editor.dom.chooseInRight.ontouchstart = editor.dom.chooseInRight.onmousedown
editor.dom.chooseInRight.onmousedown = null
editor.dom.copyLoc.ontouchstart = editor.dom.copyLoc.onmousedown
editor.dom.copyLoc.onmousedown = null
editor.dom.pasteLoc.ontouchstart = editor.dom.pasteLoc.onmousedown
editor.dom.pasteLoc.onmousedown = null
editor.dom.clearLoc.ontouchstart = editor.dom.clearLoc.onmousedown
editor.dom.clearLoc.onmousedown = null
// 不使用以下6语句, 会使得素材区手机无法拖动, 手机的框选素材只能放弃, 要通过弹框实现框选
// editor.dom.iconLib.ontouchstart = editor.dom.iconLib.onmousedown
// editor.dom.iconLib.onmousedown = null
// editor.dom.iconLib.ontouchmove = editor.dom.iconLib.onmousemove
// editor.dom.iconLib.onmousemove = null
// editor.dom.iconLib.ontouchend = editor.dom.iconLib.onmouseup
// editor.dom.iconLib.onmouseup = null
}
editor.constructor.prototype.mode_listen = function (callback) {
// 这里的函数还没有写jsdoc, 通过_func()的方式先完成分类
editor.uifunctions.newIdIdnum_func()
editor.uifunctions.changeId_func()
editor.uifunctions.copyPasteEnemyItem_func();
editor.uifunctions.selectFloor_func()
editor.uifunctions.saveFloor_func()
editor.uifunctions.openDoc_func();
editor.uifunctions.newMap_func()
editor.uifunctions.createNewMaps_func()
editor.uifunctions.changeFloorId_func()
editor.uifunctions.changeFloorSize_func()
// editor.uifunctions.fixCtx_func()
editor.uifunctions.appendPic_func();
/*
editor.uifunctions.selectAppend_func()
editor.uifunctions.selectFileBtn_func()
editor.uifunctions.changeColorInput_func()
editor.uifunctions.picClick_func()
editor.uifunctions.appendConfirm_func()
*/
editor.dom.editModeSelect.onchange = editor.mode.editModeSelect_onchange
if (Boolean(callback)) callback();
}
}

1190
editor_mappanel.js Normal file

File diff suppressed because it is too large Load Diff

243
editor_materialpanel.js Normal file
View File

@ -0,0 +1,243 @@
editor_materialpanel_wrapper = function (editor) {
// 由于历史遗留原因, 以下变量作为全局变量使用
// selectBox
window.selectBox=document.getElementById('selectBox')
selectBox._isSelected=false
selectBox.isSelected=function(value){
if(value!=null){
selectBox._isSelected=value;
editor.dom.dataSelection.style.display=value?'':'none'
}
return selectBox._isSelected
}
var locToPos = function (loc) {
return { 'x': ~~(loc.x / loc.size), 'y': ~~(loc.y / loc.size) };
}
editor.uifunctions.getScrollBarHeight = function () {
var outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
var widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
var inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
var widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
/**
* editor.dom.iconExpandBtn.onclick
*/
editor.uifunctions.fold_material_click = function () {
if (editor.uivalues.folded) {
if (confirm("你想要展开素材吗?\n展开模式下将显示全素材内容。")) {
editor.config.set('folded', false, function() {
window.location.reload();
});
}
} else {
var perCol = parseInt(prompt("请输入折叠素材模式下每列的个数:", "50")) || 0;
if (perCol > 0) {
editor.config.set('foldPerCol', perCol, false);
editor.config.set('folded', true, function() {
window.location.reload();
});
}
}
}
/**
* editor.dom.iconLib.onmousedown
* 素材区的单击/拖拽事件
*/
editor.uifunctions.material_ondown = function (e) {
e.stopPropagation();
e.preventDefault();
editor.uivalues.lastMoveMaterE=e;
if (!editor.isMobile && e.clientY >= editor.dom.iconLib.offsetHeight - editor.uivalues.scrollBarHeight) return;
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
editor.uivalues.startLoc={
'x': scrollLeft + e.clientX + editor.dom.iconLib.scrollLeft - right.offsetLeft - editor.dom.iconLib.offsetLeft,
'y': scrollTop + e.clientY + editor.dom.iconLib.scrollTop - right.offsetTop - editor.dom.iconLib.offsetTop,
'px': e.clientX,
'py': e.clientY,
'size': 32
};
}
/**
* editor.dom.iconLib.onmousemove
* 素材区的单击/拖拽事件
*/
editor.uifunctions.material_onmove = function (e) {
e.stopPropagation();
e.preventDefault();
editor.uivalues.lastMoveMaterE=e;
if (!editor.uivalues.startLoc) return;
var pos0 = locToPos(editor.uivalues.startLoc);
editor.dom.dataSelection.style.left = 32 * pos0.x + 'px';
editor.dom.dataSelection.style.top = 32 * pos0.y + 'px';
editor.dom.dataSelection.style.width = e.clientX - editor.uivalues.startLoc.px + 'px';
editor.dom.dataSelection.style.height = e.clientY - editor.uivalues.startLoc.py + 'px';
editor.dom.dataSelection.style.display = 'block';
}
/**
* editor.dom.iconLib.onmouseup
* 素材区的单击/拖拽事件
*/
editor.uifunctions.material_onup = function (ee) {
var startLoc = editor.uivalues.startLoc;
editor.uivalues.startLoc = null;
var e=editor.uivalues.lastMoveMaterE;
if (!editor.isMobile && e.clientY >= editor.dom.iconLib.offsetHeight - editor.uivalues.scrollBarHeight) return;
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
var loc = {
'x': scrollLeft + e.clientX + editor.dom.iconLib.scrollLeft - right.offsetLeft - editor.dom.iconLib.offsetLeft,
'y': scrollTop + e.clientY + editor.dom.iconLib.scrollTop - right.offsetTop - editor.dom.iconLib.offsetTop,
'size': 32
};
editor.loc = loc;
editor.uivalues.tileSize = [1,1];
var pos0 = locToPos(startLoc);
var pos = locToPos(loc);
for (var spriter in editor.widthsX) {
if (pos.x >= editor.widthsX[spriter][1] && pos.x < editor.widthsX[spriter][2]) {
var ysize = spriter.endsWith('48') ? 48 : 32;
loc.ysize = ysize;
pos.images = editor.widthsX[spriter][0];
pos.y = ~~(loc.y / loc.ysize);
if (!editor.uivalues.folded && core.tilesets.indexOf(pos.images) == -1) pos.x = editor.widthsX[spriter][1];
var autotiles = core.material.images['autotile'];
if (pos.images == 'autotile') {
var imNames = Object.keys(autotiles);
if (editor.uivalues.folded) {
pos.y = Math.min(pos.y, imNames.length - 1);
pos.images = imNames[pos.y];
} else {
if ((pos.y + 1) * ysize > editor.widthsX[spriter][3])
pos.y = ~~(editor.widthsX[spriter][3] / ysize) - 4;
else {
for (var i = 0; i < imNames.length; i++) {
if (pos.y >= 4 * i && pos.y < 4 * (i + 1)) {
pos.images = imNames[i];
pos.y = 4 * i;
}
}
}
}
}
else {
var height = editor.widthsX[spriter][3], col = height / ysize;
if (spriter == 'terrains') col += 2;
if (editor.uivalues.folded && core.tilesets.indexOf(pos.images) == -1) {
col = (pos.x == editor.widthsX[spriter][2] - 1) ? ((col - 1) % editor.uivalues.foldPerCol + 1) : editor.uivalues.foldPerCol;
}
pos.y = Math.min(pos.y, col - 1);
}
selectBox.isSelected(true);
// console.log(pos,core.material.images[pos.images].height)
editor.dom.dataSelection.style.left = pos.x * 32 + 'px';
editor.dom.dataSelection.style.top = pos.y * ysize + 'px';
editor.dom.dataSelection.style.height = ysize - 6 + 'px';
editor.dom.dataSelection.style.width = 32 - 6 + 'px';
if (pos.x == 0 && pos.y == 0) {
// editor.info={idnum:0, id:'empty','images':'清除块', 'y':0};
editor.info = 0;
} else if (pos.x == 0 && pos.y == 1) {
editor.info = editor.ids[editor.indexs[17]];
} else {
if (autotiles[pos.images]) editor.info = { 'images': pos.images, 'y': 0 };
else if (core.tilesets.indexOf(pos.images) != -1) editor.info = { 'images': pos.images, 'y': pos.y, 'x': pos.x - editor.widthsX[spriter][1] };
else {
var y = pos.y;
if (editor.uivalues.folded) {
y += editor.uivalues.foldPerCol * (pos.x - editor.widthsX[spriter][1]);
}
if (pos.images == 'terrains') y -= 2;
editor.info = { 'images': pos.images, 'y': y }
}
for (var idindex = 0; idindex < editor.ids.length; idindex++) {
if ((core.tilesets.indexOf(pos.images) != -1 && editor.info.images == editor.ids[idindex].images
&& editor.info.y == editor.ids[idindex].y && editor.info.x == editor.ids[idindex].x)
|| (Object.prototype.hasOwnProperty.call(autotiles, pos.images) && editor.info.images == editor.ids[idindex].id
&& editor.info.y == editor.ids[idindex].y)
|| (core.tilesets.indexOf(pos.images) == -1 && editor.info.images == editor.ids[idindex].images
&& editor.info.y == editor.ids[idindex].y)
) {
editor.info = editor.ids[idindex];
break;
}
}
if (editor.info.isTile && (editor.isMobile || e.button == 2)) { //这段改一改之类的应该能给手机用,就不删了
var v = prompt("请输入该额外素材区域绑定宽高,以逗号分隔", "1,1");
if (v != null && /^\d+,\d+$/.test(v)) {
v = v.split(",");
var x = parseInt(v[0]), y = parseInt(v[1]);
var widthX = editor.widthsX[editor.info.images];
if (x <= 0 || y <= 0 || editor.info.x + x > widthX[2] - widthX[1] || 32*(editor.info.y + y) > widthX[3]) {
alert("不合法的输入范围,已经越界");
} else {
editor.uivalues.tileSize = [x, y];
editor.dom.dataSelection.style.left = pos.x * 32 + 'px';
editor.dom.dataSelection.style.top = pos.y * ysize + 'px';
editor.dom.dataSelection.style.height = ysize*y - 6 + 'px';
editor.dom.dataSelection.style.width = 32*x - 6 + 'px';
}
}
}
if (editor.info.isTile && !editor.isMobile && e.button != 2) { //左键拖拽框选
var x = pos.x-pos0.x+1, y = pos.y-pos0.y+1;
var widthX = editor.widthsX[editor.info.images];
// 懒得仔细处理了, 只允许左上往右下拉
if (x <= 0 || y <= 0 || pos0.x < widthX[1]){
} else {
editor.info = editor.ids[idindex-(x-1)-(y-1)*(widthX[2]-widthX[1])];
editor.uivalues.tileSize = [x, y];
editor.dom.dataSelection.style.left = pos0.x * 32 + 'px';
editor.dom.dataSelection.style.top = pos0.y * ysize + 'px';
editor.dom.dataSelection.style.height = ysize*y - 6 + 'px';
editor.dom.dataSelection.style.width = 32*x - 6 + 'px';
}
}
}
editor.uifunctions.showBlockInfo(JSON.parse(JSON.stringify(editor.info)));
editor_mode.onmode('nextChange');
editor_mode.onmode('enemyitem');
editor.updateLastUsedMap();
//editor_mode.enemyitem();
}
}
}
}

371
editor_mode.js Normal file
View File

@ -0,0 +1,371 @@
editor_mode = function (editor) {
var core = editor.core;
function editor_mode() {
this.ids = {
'loc': 'left2',
'enemyitem': 'left3',
'floor': 'left4',
'tower': 'left5',
'functions': 'left8',
'map': 'left',
'appendpic': 'left1',
'commonevent': 'left9',
'plugins': 'left10',
}
this._ids = {}
this.dom = {}
this.actionList = [];
this.mode = 'tower'; // 初始默认显示全塔属性
this.info = {};
this.appendPic = {};
this.doubleClickMode = 'change';
}
editor_mode.prototype.init = function (callback) {
if (Boolean(callback)) callback();
}
editor_mode.prototype.init_dom_ids = function (callback) {
Object.keys(editor_mode.ids).forEach(function (v) {
editor_mode.dom[v] = document.getElementById(editor_mode.ids[v]);
editor_mode._ids[editor_mode.ids[v]] = v;
});
if (Boolean(callback)) callback();
}
editor_mode.prototype.indent = function (field) {
var num = '\t';
if (field.indexOf("['main']") === 0) return 0;
if (field === "['special']") return 0;
return num;
}
editor_mode.prototype.addAction = function (action) {
editor_mode.actionList.push(action);
}
editor_mode.prototype.doActionList = function (mode, actionList, callback) {
if (actionList.length == 0) return;
printf('修改中...');
var cb = function (objs_) {
if (objs_.slice(-1)[0] != null) {
printe(objs_.slice(-1)[0]);
throw (objs_.slice(-1)[0])
}
;
var str = '修改成功!';
if (data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.name == 'template')
str += '<br/>请注意全塔属性的name尚未修改请及时予以设置。';
if (mode == 'enemyitem') {
if (editor.info && editor.info.idnum) {
var block = editor.core.maps.blocksInfo[editor.info.idnum];
if (block.doorInfo != null && block.doorInfo.keys != null && Object.keys(block.doorInfo.keys).length > 0
&& block.trigger != 'openDoor') {
str += "<br/>你修改了门信息但触发器未改成openDoor请修改否则无法撞击开门。"
}
}
if (editor_mode.info.images == 'enemys' || editor_mode.info.images == 'enemy48') {
if (core.getFaceDownId(editor_mode.info.id) != editor_mode.info.id) {
str += "<br/>绑定行走图朝向后只需要对应设置朝下怪物的属性,会自动同步而无需修改其他朝向的属性。"
}
}
}
printf(str);
if (callback) callback();
}
switch (mode) {
case 'loc':
editor.file.editLoc(editor_mode.pos.x, editor_mode.pos.y, actionList, function (objs_) {
cb(objs_);
editor.drawPosSelection();
});
break;
case 'enemyitem':
if (editor_mode.info.images == 'enemys' || editor_mode.info.images == 'enemy48') {
editor.file.editEnemy(editor_mode.info.id, actionList, cb);
} else if (editor_mode.info.images == 'items') {
editor.file.editItem(editor_mode.info.id, actionList, cb);
} else {
editor.file.editMapBlocksInfo(editor_mode.info.idnum, actionList, cb);
}
break;
case 'floor':
editor.file.editFloor(actionList, cb);
break;
case 'tower':
editor.file.editTower(actionList, cb);
break;
case 'functions':
editor.file.editFunctions(actionList, cb);
break;
case 'commonevent':
editor.file.editCommonEvent(actionList, cb);
break;
case 'plugins':
editor.file.editPlugins(actionList, cb);
break;
default:
break;
}
}
editor_mode.prototype.onmode = function (mode, callback) {
if (editor_mode.mode != mode) {
if (mode === 'save') editor_mode.doActionList(editor_mode.mode, editor_mode.actionList, callback);
if (editor_mode.mode === 'nextChange' && mode) editor_mode.showMode(mode);
if (mode !== 'save') editor_mode.mode = mode;
editor_mode.actionList = [];
}
}
editor_mode.prototype.showMode = function (mode) {
for (var name in this.dom) {
editor_mode.dom[name].style = 'z-index:-1;opacity: 0;';
}
editor_mode.dom[mode].style = '';
editor_mode.doubleClickMode = 'change';
// clear
editor.drawEventBlock();
if (editor_mode[mode]) editor_mode[mode]();
editor.dom.editModeSelect.value = mode;
if (!selectBox.isSelected()) editor.uifunctions.showTips();
}
editor_mode.prototype.change = function (value) {
editor_mode.onmode('nextChange');
editor_mode.onmode(value);
if (editor.isMobile) editor.showdataarea(false);
}
editor_mode.prototype.checkUnique = function (thiseval) {
if (!(thiseval instanceof Array)) return false;
var map = {};
for (var i = 0; i < thiseval.length; ++i) {
if (map[thiseval[i]]) {
alert("警告:存在重复定义!");
return false;
}
map[thiseval[i]] = true;
}
return true;
}
editor_mode.prototype.checkFloorIds = function (thiseval) {
if (!editor_mode.checkUnique(thiseval)) return false;
var oldvalue = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.main.floorIds;
fs.readdir('project/floors', function (err, data) {
if (err) {
printe(err);
throw Error(err);
}
var newfiles = thiseval.map(function (v) { return v + '.js' });
var notExist = '';
for (var name, ii = 0; name = newfiles[ii]; ii++) {
if (data.indexOf(name) === -1) notExist = name;
}
if (notExist) {
var discard = confirm('文件' + notExist + '不存在, 保存会导致工程无法打开, 是否放弃更改');
if (discard) {
editor.file.editTower([['change', "['main']['floorIds']", oldvalue]], function (objs_) {//console.log(objs_);
if (objs_.slice(-1)[0] != null) {
printe(objs_.slice(-1)[0]);
throw (objs_.slice(-1)[0])
}
; printe('已放弃floorIds的修改请F5进行刷新');
});
}
}
});
return true
}
editor_mode.prototype.checkImages = function (thiseval, directory) {
if (!directory) return true;
if (!editor_mode.checkUnique(thiseval)) return false;
fs.readdir(directory, function (err, data) {
if (err) {
printe(err);
throw Error(err);
}
var notExist = null;
thiseval.map(function (v) {
var name = v.indexOf('.') < 0 ? (v+'.png') : v;
if (data.indexOf(name) < 0) notExist = name;
return name;
});
if (notExist) {
alert('警告!图片' + notExist + '不存在!保存可能导致工程无法打开,请及时修改!');
}
});
return true;
}
editor_mode.prototype.changeDoubleClickModeByButton = function (mode) {
({
delete: function () {
printf('下一次双击表格的项删除,切换下拉菜单可取消;编辑后需刷新浏览器生效。');
editor_mode.doubleClickMode = mode;
},
add: function () {
printf('下一次双击表格的项则在同级添加新项,切换下拉菜单可取消;编辑后需刷新浏览器生效。');
editor_mode.doubleClickMode = mode;
}
}[mode])();
}
/////////////////////////////////////////////////////////////////////////////
editor_mode.prototype.loc = function (callback) {
//editor.pos={x: 0, y: 0};
if (!core.isset(editor.pos)) return;
editor_mode.pos = editor.pos;
document.getElementById('pos_a6771a78_a099_417c_828f_0a24851ebfce').innerText = editor_mode.pos.x + ',' + editor_mode.pos.y;
var objs = [];
editor.file.editLoc(editor_mode.pos.x, editor_mode.pos.y, [], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_3d846fc4_7644_44d1_aa04_433d266a73df').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
editor.drawPosSelection();
if (Boolean(callback)) callback();
}
editor_mode.prototype.enemyitem = function (callback) {
//editor.info=editor.ids[editor.indexs[201]];
if (!core.isset(editor.info)) return;
if (Object.keys(editor.info).length !== 0 && editor.info.idnum != 17) editor_mode.info = editor.info;//避免editor.info被清空导致无法获得是物品还是怪物
if (!core.isset(editor_mode.info.id)) {
// document.getElementById('table_a3f03d4c_55b8_4ef6_b362_b345783acd72').innerHTML = '';
document.getElementById('newIdIdnum').style.display = 'block';
document.getElementById('enemyItemTable').style.display = 'none';
document.getElementById('changeId').style.display = 'none';
return;
}
document.getElementById('newIdIdnum').style.display = 'none';
document.getElementById('enemyItemTable').style.display = 'block';
document.getElementById('changeId').style.display = 'block';
var objs = [];
if (editor_mode.info.images == 'enemys' || editor_mode.info.images == 'enemy48') {
editor.file.editEnemy(editor_mode.info.id, [], function (objs_) {
objs = objs_;
//console.log(objs_)
});
} else if (editor_mode.info.images == 'items') {
editor.file.editItem(editor_mode.info.id, [], function (objs_) {
objs = objs_;
//console.log(objs_)
});
} else {
/* document.getElementById('table_a3f03d4c_55b8_4ef6_b362_b345783acd72').innerHTML='';
return; */
editor.file.editMapBlocksInfo(editor_mode.info.idnum, [], function (objs_) {
objs = objs_;
//console.log(objs_)
});
}
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_a3f03d4c_55b8_4ef6_b362_b345783acd72').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
editor_mode.prototype.floor = function (callback) {
var objs = [];
editor.file.editFloor([], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_4a3b1b09_b2fb_4bdf_b9ab_9f4cdac14c74').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
editor_mode.prototype.tower = function (callback) {
var objs = [];
editor.file.editTower([], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_b6a03e4c_5968_4633_ac40_0dfdd2c9cde5').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
editor_mode.prototype.functions = function (callback) {
var objs = [];
editor.file.editFunctions([], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_e260a2be_5690_476a_b04e_dacddede78b3').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
editor_mode.prototype.commonevent = function (callback) {
var objs = [];
editor.file.editCommonEvent([], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_b7bf0124_99fd_4af8_ae2f_0017f04a7c7d').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
editor_mode.prototype.plugins = function (callback) {
var objs = [];
editor.file.editPlugins([], function (objs_) {
objs = objs_;
//console.log(objs_)
});
//只查询不修改时,内部实现不是异步的,所以可以这么写
var tableinfo = editor.table.objToTable(objs[0], objs[1]);
document.getElementById('table_e2c034ec_47c6_48ae_8db8_4f8f32fea2d6').innerHTML = tableinfo.HTML;
tableinfo.listen(tableinfo.guids);
if (Boolean(callback)) callback();
}
/////////////////////////////////////////////////////////////////////////////
/**
* editor.dom.editModeSelect.onchange
*/
editor_mode.prototype.editModeSelect_onchange = function () {
editor_mode.change(editor.dom.editModeSelect.value);
}
editor_mode.prototype.listen = function (callback) {
// 移动至 editor_listen.js -> editor.constructor.prototype.mode_listen
}
var editor_mode = new editor_mode();
editor_mode.init_dom_ids();
return editor_mode;
}
//editor_mode = editor_mode(editor);

515
editor_multi.js Normal file
View File

@ -0,0 +1,515 @@
editor_multi = function () {
var editor_multi = {};
var extraKeys = {
"Ctrl-/": function (cm) { cm.toggleComment(); },
"Ctrl-B": function (cm) { ternServer.jumpToDef(cm); },
"Ctrl-Q": function (cm) { ternServer.rename(cm); },
"Cmd-F": CodeMirror.commands.findPersistent,
"Ctrl-F": CodeMirror.commands.findPersistent,
"Ctrl-R": CodeMirror.commands.replaceAll,
"Ctrl-D": function (cm) { cm.foldCode(cm.getCursor()); },
"Ctrl-O": function () { editor_multi.openUrl('/_docs/#/api'); },
"Ctrl-P": function () { editor_multi.openUrl('https://h5mota.com/plugins/'); }
};
var codeEditor = CodeMirror.fromTextArea(document.getElementById("multiLineCode"), {
lineNumbers: true,
matchBrackets: true,
indentUnit: 4,
tabSize: 4,
indentWithTabs: true,
smartIndent: true,
mode: { name: "javascript", globalVars: true, localVars: true },
lineWrapping: true,
continueComments: "Enter",
gutters: ["CodeMirror-lint-markers", "CodeMirror-linenumbers", "CodeMirror-foldgutter"],
lint: true,
autocomplete: true,
autoCloseBrackets: true,
styleActiveLine: true,
extraKeys: extraKeys,
foldGutter: true,
inputStyle: "textarea",
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true }
});
var commandsName = {
'Ctrl-/': '注释当前选中行Ctrl+/',
'Ctrl-B': '跳转到定义Ctrl+B',
'Ctrl-Q': '重命名变量Ctrl+Q',
'Ctrl-F': '查找Ctrl+F',
'Ctrl-R': '全部替换Ctrl+R',
'Ctrl-D': '折叠或展开块Ctrl+D',
'Ctrl-O': '打开API列表Ctrl+O',
'Ctrl-P': '打开在线插件列表Ctrl+P'
};
document.getElementById('codemirrorCommands').innerHTML =
"<option value='' selected>执行操作...</option>" +
Object.keys(commandsName).map(function (name) {
return "<option value='" + name + "'>" + commandsName[name] + "</option>"
}).join('');
var coredef = terndefs_f6783a0a_522d_417e_8407_94c67b692e50[2];
Object.keys(core.material.enemys).forEach(function (name) {
coredef.core.material.enemys[name] = {
"!type": "enemy",
"!doc": core.material.enemys[name].name || "怪物"
}
});
Object.keys(core.material.bgms).forEach(function (name) {
coredef.core.material.bgms[name] = {
"!type": "audio",
"!doc": "背景音乐"
}
});
Object.keys(core.material.sounds).forEach(function (name) {
coredef.core.material.sounds[name] = {
"!type": "audio",
"!doc": "音效"
}
});
Object.keys(core.material.animates).forEach(function (name) {
coredef.core.material.animates[name] = {
"!type": "animate",
"!doc": "动画"
}
});
Object.keys(core.material.images).forEach(function (name) {
if (core.material.images[name] instanceof Image) {
coredef.core.material.images[name] = {
"!type": "image",
"!doc": "系统图片"
}
} else {
coredef.core.material.images[name] = {
"!doc": name == 'autotile' ? '自动元件' : name == 'tilesets' ? '额外素材' : name == 'images' ? '自定义图片' : '系统图片'
}
for (var v in core.material.images[name]) {
coredef.core.material.images[name][v] = {
"!type": "image",
}
}
}
})
Object.keys(core.material.items).forEach(function (name) {
coredef.core.material.items[name] = {
"!type": "item",
"!doc": core.material.items[name].name || "道具"
}
});
functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a.enemys.getSpecials().forEach(function (one) {
var name = one[1];
if (name instanceof Function) name = name({});
coredef.core.enemys.hasSpecial["!doc"] += name + "(" + one[0] + "); ";
});
Object.keys(core.canvas).forEach(function (name) {
coredef.core.canvas[name] = {
"!type": "CanvasRenderingContext2D",
"!doc": "系统画布"
}
});
Object.keys(core.status.maps).forEach(function (name) {
coredef.core.status.maps[name] = {
"!type": "floor",
"!doc": core.status.maps[name].title || ''
}
coredef.core.status.bgmaps[name] = {
"!type": "[[number]]",
"!doc": core.status.maps[name].title || ''
}
coredef.core.status.fgmaps[name] = {
"!type": "[[number]]",
"!doc": core.status.maps[name].title || ''
}
});
Object.keys(core.status.shops).forEach(function (id) {
coredef.core.status.shops[id] = {
"!doc": core.status.shops[id].textInList || "全局商店"
}
});
Object.keys(core.status.textAttribute).forEach(function (id) {
coredef.core.status.textAttribute[id] = {};
});
// --- 转发函数
for (var name in coredef.core) {
if (typeof coredef.core[name] === 'object') {
for (var funcname in coredef.core[name]) {
var one = coredef.core[name][funcname] || {};
var type = one["!type"] || "";
if (type.startsWith("fn(")) {
var forwardname = (functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a[name] || {})[funcname] ? '脚本编辑' : name;
coredef.core[funcname] = {
"!type": one["!type"],
"!doc": one["!doc"] + "<br/>(转发到" + forwardname + "中)"
};
if (one["!url"]) coredef.core[funcname]["!url"] = one["!url"];
}
}
for (var funcname in core[name]) {
if (!(core[name][funcname] instanceof Function) || funcname.charAt(0) == '_' || coredef.core[name][funcname]) continue;
var parameterInfo = /^\s*function\s*[\w_$]*\(([\w_,$\s]*)\)\s*\{/.exec(core[name][funcname].toString());
var parameters = (parameterInfo == null ? "" : parameterInfo[1])
.replace(/\s*/g, '').replace(/,/g, ', ').split(', ')
.filter(function (one) { return one.trim() != ''; })
.map(function (one) { return one.trim() + ': ?'; }).join(', ');
coredef.core[funcname] = coredef.core[name][funcname] = {
"!type": "fn(" + parameters + ")"
}
}
}
}
Object.keys(core.values).forEach(function (id) {
var one = data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc._data.values._data[id];
if (!one) return;
coredef.core.values[id] = {
"!type": "number",
"!doc": one._data,
}
});
Object.keys(core.flags).forEach(function (id) {
var one = data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc._data.flags._data[id];
if (!one) return;
coredef.core.flags[id] = {
"!type": id == 'statusBarItems' ? '[string]' : 'bool',
"!doc": one._data,
}
});
var ternServer = new CodeMirror.TernServer({
defs: terndefs_f6783a0a_522d_417e_8407_94c67b692e50,
plugins: {
doc_comment: true,
complete_strings: true,
},
useWorker: false
});
editor_multi.ternServer = ternServer;
editor_multi.codeEditor = codeEditor;
codeEditor.on("cursorActivity", function (cm) {
var cursor = cm.getCursor();
if (codeEditor.getOption("autocomplete") && !(cursor.line == 0 && cursor.ch == 0)) {
ternServer.updateArgHints(cm);
ternServer.showDocs(cm);
}
});
var ctrlRelease = new Date();
codeEditor.on("keyup", function (cm, event) {
var date = new Date();
if (event.keyCode == 17 || event.keyCode == 91) { // ctrl, cmd
ctrlRelease = date;
}
else if (codeEditor.getOption("autocomplete") && !event.ctrlKey && date - ctrlRelease >= 1000 && (
(event.keyCode >= 65 && event.keyCode <= 90) ||
(!event.shiftKey && event.keyCode == 190) || (event.shiftKey && event.keyCode == 189))) {
try {
ternServer.complete(cm);
} catch (e) {
}
}
});
editor_multi.id = '';
editor_multi.isString = false;
editor_multi.lintAutocomplete = false;
var lastOffset = {};
editor_multi.show = function () {
if (typeof (selectBox) !== typeof (undefined)) selectBox.isSelected(false);
var valueNow = codeEditor.getValue();
//try{eval('function _asdygakufyg_() { return '+valueNow+'\n}');editor_multi.lintAutocomplete=true;}catch(ee){}
if (valueNow.slice(0, 8) === 'function') editor_multi.lintAutocomplete = true;
editor_multi.setLint();
document.getElementById('left7').style = '';
}
editor_multi.hide = function () {
document.getElementById('left7').style = 'z-index:-1;opacity: 0;';
}
editor_multi.setLint = function () {
if (editor_multi.lintAutocomplete) {
codeEditor.setOption("lint", {
options: {
esversion: 2021
}
});
} else {
codeEditor.setOption("lint", false);
}
codeEditor.setOption("autocomplete", editor_multi.lintAutocomplete);
document.getElementById("lintCheckbox").checked = editor_multi.lintAutocomplete;
}
editor_multi.toggerLint = function () {
editor_multi.lintAutocomplete = document.getElementById("lintCheckbox").checked;
editor_multi.setLint();
}
editor_multi.indent = function (field) {
if (typeof (editor) !== typeof (undefined) && editor && editor.mode && editor.mode.indent) return editor.mode.indent(field);
return '\t';
}
var _format = function () {
if (!editor_multi.lintAutocomplete) return;
var offset = (codeEditor.getScrollInfo() || {}).top || 0;
_setValue(beautifier.js(codeEditor.getValue(), {
brace_style: "collapse-preserve-inline",
indent_with_tabs: true,
jslint_happy: true
}));
codeEditor.scrollTo(0, offset);
}
var _setValue = function (val) {
codeEditor.setValue(val || '');
ternServer.delDoc('doc');
ternServer.addDoc('doc', new CodeMirror.Doc(val || '', 'javascript'));
}
editor_multi.format = function () {
if (!editor_multi.lintAutocomplete) {
alert("只有代码才能进行格式化操作!");
return;
}
_format();
}
editor_multi.hasError = function () {
if (!editor_multi.lintAutocomplete) return false;
return JSHINT.errors.filter(function (e) {
return e.code.startsWith("E")
}).length > 0;
}
var _previewButton = document.getElementById('editor_multi_preview');
_previewButton.onclick = function () {
if (!editor_multi.preview) return;
_format();
if (editor_multi.hasError()) {
alert("当前好像存在严重的语法错误,请处理后再预览。");
return;
}
editor.uievent.previewEditorMulti(editor_multi.preview, codeEditor.getValue());
}
editor_multi.import = function (id_, args) {
var thisTr = document.getElementById(id_);
if (!thisTr) return false;
var input = thisTr.children[2].children[0].children[0];
var field = thisTr.children[0].getAttribute('title');
var comment = thisTr.children[1].getAttribute('title');
if (!input.type || input.type !== 'textarea') return false;
editor_multi.id = id_;
editor_multi.isString = false;
editor_multi.lintAutocomplete = false;
editor_multi.preview = args.preview;
_previewButton.style.display = editor_multi.preview ? 'inline' : 'none';
if (args.lint === true) editor_multi.lintAutocomplete = true;
if ((!input.value || input.value == 'null') && args.template)
input.value = '"' + args.template + '"';
if ((!input.value || input.value == 'null') && editor_mode.mode == 'plugins')
input.value = '"function () {\\n\\t// 在此增加新插件\\n\\t\\n}"';
// if ((!input.value || input.value == 'null') && args)
if (input.value.slice(0, 1) === '"' || args.string) {
editor_multi.isString = true;
_setValue(JSON.parse(input.value) || '');
} else {
var num = editor_multi.indent(field);
eval('var tobj=' + (input.value || 'null'));
var tmap = {};
var tstr = JSON.stringify(tobj, function (k, v) {
if (typeof (v) === typeof ('') && v.slice(0, 8) === 'function') {
var id_ = editor.util.guid();
tmap[id_] = v.toString();
return id_;
} else return v
}, num);
for (var id_ in tmap) {
tstr = tstr.replace('"' + id_ + '"', tmap[id_])
}
_setValue(tstr || '');
}
editor_multi.show();
codeEditor.scrollTo(0, lastOffset[editor_multi.id] || 0);
return true;
}
editor_multi.cancel = function () {
if (editor_multi.id && editor_multi.id != 'callFromBlockly' && editor_multi.id != 'importFile') {
lastOffset[editor_multi.id] = (codeEditor.getScrollInfo() || {}).top;
}
editor_multi.hide();
editor_multi.id = '';
multiLineArgs = [null, null, null];
}
editor_multi.confirm = function (keep) {
if (editor_multi.hasError()) {
alert("当前好像存在严重的语法错误,请处理后再保存。\n严重的语法错误可能会导致整个编辑器的崩溃。");
return;
}
if (!editor_multi.id) {
editor_multi.id = '';
return;
}
if (editor_multi.id === 'callFromBlockly') {
// ----- 自动格式化
_format();
editor_multi.multiLineDone(keep);
return;
}
if (editor_multi.id === 'importFile') {
_format();
editor_multi.writeFileDone(keep);
return;
}
var setvalue = function (value) {
var thisTr = document.getElementById(editor_multi.id);
var input = thisTr.children[2].children[0].children[0];
if (editor_multi.isString) {
input.value = JSON.stringify(value);
} else {
eval('var tobj=' + (value || 'null'));
var tmap = {};
var tstr = JSON.stringify(tobj, function (k, v) {
if (v instanceof Function) {
var id_ = editor.util.guid();
tmap[id_] = v.toString();
return id_;
} else return v
}, 4);
for (var id_ in tmap) {
tstr = tstr.replace('"' + id_ + '"', JSON.stringify(tmap[id_]))
}
input.value = tstr;
}
if (!keep) {
editor_multi.id = '';
editor_multi.hide();
} else {
alert('写入成功!');
}
input.onchange();
}
lastOffset[editor_multi.id] = (codeEditor.getScrollInfo() || {}).top;
// ----- 自动格式化
_format();
setvalue(codeEditor.getValue() || '');
}
editor_multi.doCommand = function (select) {
var value = select.value;
select.selectedIndex = 0;
if (extraKeys[value]) {
extraKeys[value](codeEditor);
}
}
editor_multi.openUrl = function (url) {
if (editor.isMobile && !confirm('你确定要离开本页面么?')) return;
window.open(url, '_blank');
}
var multiLineArgs = [null, null, null];
editor_multi.multiLineEdit = function (value, b, f, args, callback) {
editor_multi.id = 'callFromBlockly';
_setValue(value.split('\\n').join('\n') || '');
multiLineArgs[0] = b;
multiLineArgs[1] = f;
multiLineArgs[2] = callback;
editor_multi.lintAutocomplete = Boolean(args.lint);
editor_multi.show();
}
editor_multi.multiLineDone = function (keep) {
if (!multiLineArgs[0] || !multiLineArgs[1] || !multiLineArgs[2]) return;
var newvalue = codeEditor.getValue() || '';
multiLineArgs[2](newvalue, multiLineArgs[0], multiLineArgs[1])
if (!keep) {
editor_multi.id = '';
editor_multi.hide();
} else {
alert('写入成功!');
}
}
var _fileValues = ['']
editor_multi.importFile = function (filename) {
editor_multi.id = 'importFile'
_fileValues[0] = filename
_setValue('loading')
editor_multi.show();
fs.readFile(filename, 'base64', function (e, d) {
if (e) {
_setValue('加载文件失败:\n' + e)
editor_multi.id = ''
return;
}
var str = editor.util.decode64(d)
_setValue(str)
_fileValues[1] = str
})
}
editor_multi.writeFileDone = function (keep) {
fs.writeFile(_fileValues[0], editor.util.encode64(codeEditor.getValue() || ''), 'base64', function (err, data) {
if (err) printe('文件写入失败,请手动粘贴至' + _fileValues[0] + '\n' + err);
else {
if (!keep) {
editor_multi.id = '';
editor_multi.hide();
} else {
alert('写入成功!');
}
printf(_fileValues[0] + " 写入成功F5刷新后生效");
}
});
}
editor_multi.editCommentJs = function (mod) {
var dict = {
loc: '_server/table/comment.js',
enemyitem: '_server/table/comment.js',
floor: '_server/table/comment.js',
tower: '_server/table/data.comment.js',
functions: '_server/table/functions.comment.js',
commonevent: '_server/table/events.comment.js',
plugins: '_server/table/plugins.comment.js',
}
editor_multi.lintAutocomplete = true
editor_multi.setLint()
editor_multi.importFile(dict[mod])
}
// 字体大小
{
const CONFIG_KEY = "editor_multi.fontSize";
let fontsize = editor.config.get(CONFIG_KEY, 14);
const input = document.getElementById("editor_multi_fontsize");
const check = document.getElementById("editor_multi_fontweight")
input.value = fontsize;
editor_multi.setFontSize = function () {
const value = Number(input.value);
editor.config.set(CONFIG_KEY, value);
const ele = codeEditor.getWrapperElement()
ele.style.fontSize = `${value}px`;
ele.style.fontWeight = `${check.checked ? 'bold' : 'normal'}`
}
}
return editor_multi;
}
//editor_multi=editor_multi();

602
editor_table.js Normal file
View File

@ -0,0 +1,602 @@
editor_table_wrapper = function (editor) {
editor_table = function () {
}
/////////////////////////////////////////////////////////////////////////////
// HTML模板
editor_table.prototype.select = function (value, values) {
if (values.indexOf(value) < 0) values = [value].concat(values);
var content = values.map(function (v) {
return editor.table.option(v, v == value)
}).join('')
return /* html */`<select>\n${content}</select>\n`
}
editor_table.prototype.option = function (value, selected) {
return /* html */`<option value='${JSON.stringify(value)}' ${selected ? 'selected' : ''}>${JSON.stringify(value)}</option>\n`
}
editor_table.prototype.text = function (value) {
return /* html */`<input type='text' spellcheck='false' value='${JSON.stringify(value)}'/>\n`
}
editor_table.prototype.checkbox = function (value) {
return /* html */`<input type='checkbox' class='checkbox' ${(value ? 'checked ' : '')}/>\n`
}
editor_table.prototype.textarea = function (value, indent, disable) {
return /* html */`<textarea spellcheck='false' ${disable ? 'disabled readonly' : ''}>${JSON.stringify(value, null, indent || 0)}</textarea>\n`
}
editor_table.prototype.checkboxSet = function (value, keys, prefixStrings) {
if (value == null) value = [];
if (!(value instanceof Array)) {
if (value == 0) value = [];
else value = [value];
}
keys=Array.from(keys)
prefixStrings=Array.from(prefixStrings)
for (var index = 0; index < value.length; index++) {
if (keys.indexOf(value[index])==-1) {
keys.push(value[index])
prefixStrings.push('<br>'+value[index]+': ')
}
}
var content=[]
for (var index = 0; index < keys.length; index++) {
content.push(editor.table.checkboxSetMember(value.indexOf(keys[index])!=-1,keys[index],prefixStrings[index]))
}
return /* html */`<div class='checkboxSet'>${content.join('')}</div>\n`;
}
editor_table.prototype.checkboxSetMember = function (value,key,prefixString) {
return /* html */`${prefixString}<input key='${key}' ctype='${typeof key}' type='checkbox' class='checkboxSetMember' onchange='editor.table.checkboxSetMemberOnchange(this)' ${(value ? 'checked ' : '')}/>\n`;
}
editor_table.prototype.editGrid = function (showComment, type) {
var list = [];
if (showComment) list.push("<button onclick='editor.table.onCommentBtnClick(this)'>注释</button>");
if (type != 'select' && type != 'checkbox' && type != 'checkboxSet' && type != 'popCheckboxSet' && type != 'disable')
list.push("<button onclick='editor.table.onEditBtnClick(this)' class='editorTableEditBtn'>编辑</button>");
if (type == 'popCheckboxSet')
list.push("<button onclick='editor.table.onEditBtnClick(this)' class='editorTableEditBtn'>多选框编辑</button>");
if (type == 'disable') list.push("<button onclick='editor.table.onCopyBtnClick(this)'>复制</button>");
return list.join(' ');
}
editor_table.prototype.title = function () {
return /* html */`\n<tr><td>条目</td><td>注释</td><td>值</td><td>操作</td></tr>\n`
}
editor_table.prototype.gap = function (field) {
var tokenlist = field.slice(2, -2).split("']['");
var rule = tokenlist.join("-");
tokenlist.pop();
var self = tokenlist.join("-");
var status = !!tokenPool[rule];
return /* html */`<tr data-gap="${rule}" data-field="${self}">
<td>----</td>
<td>----</td>
<td>${field}</td>
<td><button class='editorTableFoldBtn' onclick='editor.table.onFoldBtnClick(this)' data-fold="${ status ? "true" : "false" }">${ status ? "展开" : "折叠" }</button></td>
</tr>\n`
}
editor_table.prototype.tr = function (guid, field, shortField, commentHTMLescape, cobjstr, shortComment, tdstr, type) {
return /* html */`<tr id="${guid}" data-field="${field.slice(2, -2).split("']['").join("-")}">
<td title="${field}">${shortField}</td>
<td title="${commentHTMLescape}" cobj="${cobjstr}">${shortComment || commentHTMLescape}</td>
<td><div class="etableInputDiv ${type}">${tdstr}</div></td>
<td>${editor.table.editGrid(shortComment, type)}</td>
</tr>\n`
}
/**
* checkboxset中checkbox的onchange
* 这个函数本质是模板editor_table.prototype.checkboxSetMember的一部分
* 故放在HTML模板分类下
*/
editor_table.prototype.checkboxSetMemberOnchange = function (onemember) {
var thisset=onemember.parentNode
var inputs=thisset.children
var value=[]
for (var i in inputs) {
if (inputs[i].nodeName == 'INPUT') {
if (inputs[i].checked) {
var one = inputs[i].getAttribute('key');
if (inputs[i].getAttribute('ctype') == 'number') one = parseFloat(one);
value.push(one);
}
}
}
thiseval = value;
// if (value.length == 0) thiseval = null;
thisset.value=JSON.stringify(thiseval)
thisset.onchange()
}
/////////////////////////////////////////////////////////////////////////////
// 表格生成的控制
/**
* 注释对象的默认值
*/
editor_table.prototype.defaultcobj = {
// 默认是文本域
_type: 'textarea',
_data: '',
_string: function (args) {//object~[field,cfield,vobj,cobj]
var thiseval = args.vobj;
return (typeof (thiseval) === typeof ('')) && thiseval[0] === '"';
},
// 默认情况下 非对象和数组的视为叶节点
_leaf: function (args) {//object~[field,cfield,vobj,cobj]
var thiseval = args.vobj;
if (thiseval == null || thiseval == undefined) return true;//null,undefined
if (typeof (thiseval) === typeof ('')) return true;//字符串
if (Object.keys(thiseval).length === 0) return true;//数字,true,false,空数组,空对象
return false;
},
}
/**
* 把来自数据文件的obj和来自*comment.js的commentObj组装成表格
* commentObj在无视['_data']的意义下与obj同形
* : commentObj['_data']['a']['_data']['b'] obj['a']['b'] 是对应的
* 在此意义下, 两者的结构是一致的
* 在commentObj没有被定义的obj的分支, 会取defaultcobj作为默认值
* 因此在深度优先遍历时,维护
* field="['a']['b']"
* cfield="['_data']['a']['_data']['b']"
* vobj=obj['a']['b']
* cobj=commentObj['_data']['a']['_data']['b']
* cobj
* cobj = Object.assign({}, defaultcobj, pcobj['_data'][ii])
* 每一项若未定义,就从defaultcobj中取
* 当其是函数不是具体值时,把args = {field: field, cfield: cfield, vobj: vobj, cobj: cobj}代入算出该值
* 得到的叶节点的<tr>结构如下
* tr>td[title=field]
* >td[title=comment,cobj=cobj:json]
* >td>div>input[value=thiseval]
* 返回结果
* 返回一个对象, 假设被命名为tableinfo
* 在把一个 table innerHTML 赋值为 tableinfo.HTML
* 再调 tableinfo.listen(tableinfo.guids) 进行绑定事件
* @param {Object} obj
* @param {Object} commentObj
* @returns {{"HTML":String,"guids":String[],"listen":Function}}
*/
editor_table.prototype.objToTable = function (obj, commentObj) {
// 表格抬头
var outstr = [editor.table.title()];
var guids = [];
var defaultcobj = this.defaultcobj
/**
* 深度优先遍历, p*即为父节点的四个属性
* @param {String} pfield
* @param {String} pcfield
* @param {Object} pvobj
* @param {Object} pcobj
*/
var recursionParse = function (pfield, pcfield, pvobj, pcobj) {
var keysForTableOrder = {};
var voidMark = {};
// 1. 按照pcobj排序生成
if (pcobj && pcobj['_data']) {
for (var ii in pcobj['_data']) keysForTableOrder[ii] = voidMark;
}
// 2. 对每个pvobj且不在pcobj的再添加到最后
keysForTableOrder = Object.assign(keysForTableOrder, pvobj)
for (var ii in keysForTableOrder) {
// 3. 对于pcobj有但是pvobj中没有的, 弹出提示, (正常情况下editor_file会补全成null)
// 事实上能执行到这一步工程没崩掉打不开,就继续吧..
if (keysForTableOrder[ii] === voidMark) {
if (typeof id_815975ad_ee6f_4684_aac7_397b7e392702 === "undefined") {
// alert('comment和data不匹配,请在群 HTML5造塔技术交流群 959329661 内反馈')
console.error('comment和data不匹配,请在群 HTML5造塔技术交流群 959329661 内反馈')
id_815975ad_ee6f_4684_aac7_397b7e392702 = 1;
}
pvobj[ii] = null;
}
var field = pfield + "['" + ii + "']";
var cfield = pcfield + "['_data']['" + ii + "']";
var vobj = pvobj[ii];
var cobj = null;
if (pcobj && pcobj['_data'] && pcobj['_data'][ii]) {
// cobj存在时直接取
cobj = Object.assign({}, defaultcobj, pcobj['_data'][ii]);
} else {
// 当其函数时代入参数算出cobj, 不存在时只取defaultcobj
if (pcobj && (pcobj['_data'] instanceof Function)) cobj = Object.assign({}, defaultcobj, pcobj['_data'](ii));
else cobj = Object.assign({}, defaultcobj);
}
var args = { field: field, cfield: cfield, vobj: vobj, cobj: cobj }
// 当cobj的参数为函数时,代入args算出值
for (var key in cobj) {
if (key === '_data') continue;
if (cobj[key] instanceof Function) cobj[key] = cobj[key](args);
}
pvobj[ii] = vobj = args.vobj;
// 标记为_hide的属性不展示
if (cobj._hide) continue;
if (!cobj._leaf) {
// 不是叶节点时, 插入展开的标记并继续遍历, 此处可以改成按钮用来添加新项或折叠等
outstr.push(editor.table.gap(field));
recursionParse(field, cfield, vobj, cobj);
} else {
// 是叶节点时, 调objToTr_渲染<tr>
var leafnode = editor.table.objToTr(obj, commentObj, field, cfield, vobj, cobj);
outstr.push(leafnode[0]);
guids.push(leafnode[1]);
}
}
}
// 开始遍历
recursionParse("", "", obj, commentObj);
var listen = function (guids) {
// 每个叶节点的事件绑定
var tableid = editor.util.guid();
editor.mode.currentTable=tableid;
guids.forEach(function (guid) {
editor.table.guidListen(guid, tableid, obj, commentObj)
});
}
return { "HTML": outstr.join(''), "guids": guids, "listen": listen };
}
/**
* 返回叶节点<tr>形如
* tr>td[title=field]
* >td[title=comment,cobj=cobj:json]
* >td>div>input[value=thiseval]
* 参数意义在 objToTable 中已解释
* @param {Object} obj
* @param {Object} commentObj
* @param {String} field
* @param {String} cfield
* @param {Object} vobj
* @param {Object} cobj
*/
editor_table.prototype.objToTr = function (obj, commentObj, field, cfield, vobj, cobj) {
var guid = editor.util.guid();
var thiseval = vobj;
var comment = String(cobj._data);
// var charlength = 15;
// "['a']['b']" => "b"
var shortField = field.split("']").slice(-2)[0].split("['").slice(-1)[0];
// 把长度超过 charlength 的字符改成 固定长度+...的形式
// shortField = (shortField.length < charlength ? shortField : shortField.slice(0, charlength) + '...');
// 完整的内容转义后供悬停查看
var commentHTMLescape = editor.util.HTMLescape(comment);
// 把长度超过 charlength 的字符改成 固定长度+...的形式
// var shortCommentHTMLescape = (comment.length < charlength ? commentHTMLescape : editor.util.HTMLescape(comment.slice(0, charlength)) + '...');
var cobjstr = Object.assign({}, cobj);
delete cobjstr._data;
// 把cobj塞到第二个td的[cobj]中, 方便绑定事件时取
cobjstr = editor.util.HTMLescape(JSON.stringify(cobjstr));
var tdstr = editor.table.objToTd(obj, commentObj, field, cfield, vobj, cobj)
var outstr = editor.table.tr(guid, field, shortField, commentHTMLescape, cobjstr, cobj._docs, tdstr, cobj._type)
return [outstr, guid];
}
editor_table.prototype.objToTd = function (obj, commentObj, field, cfield, vobj, cobj) {
var thiseval = vobj;
switch (cobj._type) {
case 'select':
return editor.table.select(thiseval, cobj._select.values);
case 'checkbox':
return editor.table.checkbox(thiseval);
case 'checkboxSet':
return editor.table.checkboxSet(thiseval, cobj._checkboxSet.key, cobj._checkboxSet.prefix);
default:
return editor.table.textarea(thiseval, cobj.indent || 0, cobj._type == 'disable');
}
}
/////////////////////////////////////////////////////////////////////////////
// 表格的用户交互
/**
* 检查一个值是否允许被设置为当前输入
* @param {Object} cobj
* @param {*} thiseval
*/
editor_table.prototype.checkRange = function (cobj, thiseval) {
if (cobj._range) {
return eval(cobj._range);
}
if (cobj._select) {
return cobj._select.values.indexOf(thiseval) !== -1;
}
if (cobj._bool) {
return [true, false].indexOf(thiseval) !== -1;
}
return true;
}
/**
* 监听一个guid对应的表格项
* @param {String} guid
*/
editor_table.prototype.guidListen = function (guid, tableid, obj, commentObj) {
// tr>td[title=field]
// >td[title=comment,cobj=cobj:json]
// >td>div>input[value=thiseval]
var thisTr = document.getElementById(guid);
var input = thisTr.children[2].children[0].children[0];
var field = thisTr.children[0].getAttribute('title');
var cobj = JSON.parse(thisTr.children[1].getAttribute('cobj'));
var modeNode = thisTr.parentNode;
thisTr.setAttribute('tableid',tableid)
while (!editor_mode._ids.hasOwnProperty(modeNode.getAttribute('id'))) {
modeNode = modeNode.parentNode;
}
input.onchange = function () {
editor.table.onchange(guid, obj, commentObj, thisTr, input, field, cobj, modeNode)
}
// 用检测两次单击的方式来实现双击(以支持手机端的双击)
var doubleClickCheck = [0];
thisTr.onclick = function () {
var newClick = new Date().getTime();
var lastClick = doubleClickCheck.shift();
doubleClickCheck.push(newClick);
if (newClick - lastClick < 500) {
editor.table.dblclickfunc(guid, obj, commentObj, thisTr, input, field, cobj, modeNode)
}
}
}
/**
* 表格的值变化时
*/
editor_table.prototype.onchange = function (guid, obj, commentObj, thisTr, input, field, cobj, modeNode) {
editor_mode.onmode(editor_mode._ids[modeNode.getAttribute('id')]);
if (editor.mode.currentTable!=thisTr.getAttribute('tableid')) return;
var thiseval = null;
if (input.checked != null) input.value = input.checked;
try {
if (input.value == '') input.value = 'null';
thiseval = JSON.parse(input.value);
} catch (ee) {
printe(field + ' : ' + ee);
throw ee;
}
if (editor.table.checkRange(cobj, thiseval)) {
editor_mode.addAction(['change', field, thiseval]);
editor_mode.onmode('save');//自动保存 删掉此行的话点保存按钮才会保存
} else {
printe(field + ' : 输入的值不合要求,请鼠标放置在注释上查看说明');
}
}
var tokenPool = {};
var tokenstyle = document.createElement("style");
document.body.appendChild(tokenstyle);
var tokenPoolRender = function() {
var content = "";
Object.keys(tokenPool).forEach(function(k) {
content += /* CSS */`[data-field|=${k}]{ display: none }`;
})
tokenstyle.innerHTML = content;
}
/**
* "折叠"被按下时
*/
editor_table.prototype.onFoldBtnClick = function (button) {
var tr = button.parentNode.parentNode;
if (button.dataset.fold == "true") {
delete tokenPool[tr.dataset.gap];
tokenPoolRender();
button.dataset.fold = "false";
button.innerText = "折叠";
} else {
tokenPool[tr.dataset.gap] = true;
tokenPoolRender();
button.dataset.fold = "true";
button.innerText = "展开";
}
}
/**
* "显示完整注释"被按下时
*/
editor_table.prototype.onCommentBtnClick = function (button) {
var tr = button.parentNode.parentNode;
printf(tr.children[1].getAttribute('title'));
}
/**
* "编辑表格内容"被按下时
*/
editor_table.prototype.onEditBtnClick = function (button) {
var tr = button.parentNode.parentNode;
var guid = tr.getAttribute('id');
var cobj = JSON.parse(tr.children[1].getAttribute('cobj'));
var input = tr.children[2].children[0].children[0];
if (cobj._type === 'event') editor_blockly.import(guid, { type: cobj._event });
if (cobj._type === 'textarea') editor_multi.import(guid, { lint: cobj._lint, string: cobj._string, template: cobj._template, preview: cobj._preview });
if (cobj._type === 'material') editor.table.selectMaterial(input, cobj);
if (cobj._type === 'color') editor.table.selectColor(input);
if (cobj._type === 'point') editor.table.selectPoint(input);
if (cobj._type === 'popCheckboxSet') editor.table.popCheckboxSet(input, cobj);
}
editor_table.prototype.onCopyBtnClick = function (button) {
var tr = button.parentNode.parentNode;
var input = tr.children[2].children[0].children[0];
var value = JSON.parse(input.value);
if (value == null) {
printe('没有赋值的内容');
return;
}
if (core.copy(value.toString())) {
printf('复制成功!');
} else {
printe('无法复制此内容,请手动选择复制');
}
}
/**
* 双击表格时
* 正常编辑: 尝试用事件编辑器或多行文本编辑器打开
* 添加: 在该项的同一级创建一个内容为null新的项, 刷新后生效并可以继续编辑
* 删除: 删除该项, 刷新后生效
* 在点击按钮 添加/删除 ,下一次双击将被视为 添加/删除
*/
editor_table.prototype.dblclickfunc = function (guid, obj, commentObj, thisTr, input, field, cobj, modeNode) {
if (editor_mode.doubleClickMode === 'change') {
if (cobj._type === 'event') editor_blockly.import(guid, { type: cobj._event });
if (cobj._type === 'textarea') editor_multi.import(guid, { lint: cobj._lint, string: cobj._string, template: cobj._template, preview: cobj._preview });
if (cobj._type === 'material') editor.table.selectMaterial(input, cobj);
if (cobj._type === 'color') editor.table.selectColor(input);
if (cobj._type === 'point') editor.table.selectPoint(input);
if (cobj._type === 'popCheckboxSet') editor.table.popCheckboxSet(input, cobj);
} else if (editor_mode.doubleClickMode === 'add') {
editor_mode.doubleClickMode = 'change';
editor.table.addfunc(guid, obj, commentObj, thisTr, input, field, cobj, modeNode)
} else if (editor_mode.doubleClickMode === 'delete') {
editor_mode.doubleClickMode = 'change';
editor.table.deletefunc(guid, obj, commentObj, thisTr, input, field, cobj, modeNode)
}
}
editor_table.prototype.selectMaterial = function (input, cobj) {
editor.uievent.selectMaterial(input.value, cobj._docs || cobj._data || '请选择素材', cobj._directory, function (one) {
if (!/^[-A-Za-z0-9_.]+$/.test(one)) return null;
if (cobj._transform) return eval("("+cobj._transform+")(one)");
return one;
}, function (data) {
input.value = JSON.stringify(cobj._onconfirm ? eval("("+cobj._onconfirm+")(JSON.parse(input.value), data)") : data);
input.onchange();
})
}
editor_table.prototype.selectColor = function (input) {
if (input.value != null) {
var str = input.value.toString().replace(/[^\d.,]/g, '');
if (/^[0-9 ]+,[0-9 ]+,[0-9 ]+(,[0-9. ]+)?$/.test(str)) {
document.getElementById('colorPicker').value = str;
}
}
var boundingBox = input.getBoundingClientRect();
openColorPicker(boundingBox.x, boundingBox.y + boundingBox.height, function (value) {
value = value.replace(/[^\d.,]/g, '');
input.value = '[' + value +']';
input.onchange();
})
}
editor_table.prototype.selectPoint = function (input) {
var x = 0, y = 0, value = input.value;
if (value != null) {
try {
var loc = JSON.parse(value);
if (loc instanceof Array && loc.length == 2) {
x = loc[0];
y = loc[1];
}
} catch (e) {}
}
editor.uievent.selectPoint(editor.currentFloorId, x, y, false, function (floorId, x, y) {
input.value = '['+x+','+y+']';
input.onchange();
})
}
editor_table.prototype.popCheckboxSet = function (input, cobj) {
editor.uievent.popCheckboxSet(JSON.parse(input.value), cobj._checkboxSet, cobj._docs || cobj._data || '请选择多选项', function (value) {
input.value = JSON.stringify(value);
input.onchange();
})
}
/**
* 删除表格项
*/
editor_table.prototype.deletefunc = function (guid, obj, commentObj, thisTr, input, field, cobj, modeNode) {
editor_mode.onmode(editor_mode._ids[modeNode.getAttribute('id')]);
if (editor.table.checkRange(cobj, null)) {
editor_mode.addAction(['delete', field, undefined]);
editor_mode.onmode('save', function () {
printf('删除成功,刷新后生效。')
});
} else {
printe(field + ' : 该值不允许为null无法删除');
}
}
/**
* 添加表格项
*/
editor_table.prototype.addfunc = function (guid, obj, commentObj, thisTr, input, field, cobj, modeNode) {
if (modeNode) {
editor_mode.onmode(editor_mode._ids[modeNode.getAttribute('id')]);
}
var mode = editor.dom.editModeSelect.value;
var supportText = mode === 'commonevent' || mode === 'plugins';
if (obj == null) {
if (mode === 'commonevent') obj = events_c12a15a8_c380_4b28_8144_256cba95f760;
else if (mode === 'plugins') obj = plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1;
else return;
}
// 1.输入id
var newid = '2';
if (mode == 'loc') {
var ae = editor.currentFloorData.autoEvent[editor_mode.pos.x + ',' + editor_mode.pos.y];
if (ae != null) {
var testid;
for (testid = 2; Object.hasOwnProperty.call(ae, testid); testid++); // 从3开始是因为comment中设置了始终显示012
newid = testid + '';
}
} else {
newid = prompt(supportText ? '请输入新项的ID支持中文' : '请输入新项的ID数字字母下划线');
if (newid == null || newid.length == 0) {
return;
}
}
// 2.检查id是否符合规范或与已有id重复
if (!supportText) {
if (!/^[a-zA-Z0-9_]+$/.test(newid)) {
printe('id不符合规范, 请使用大小写字母数字下划线来构成');
return;
}
}
var conflict = true;
var basefield = (field || "").replace(/\[[^\[]*\]$/, '');
if (basefield === "['main']") {
printe("全塔属性 ~ ['main'] 不允许添加新值");
return;
}
try {
var baseobj = eval('obj' + basefield);
conflict = newid in baseobj;
} catch (ee) {
// 理论上这里不会发生错误
printe(ee);
throw ee;
}
if (conflict) {
printe('id已存在, 请直接修改该项的值');
return;
}
// 3.添加
editor_mode.addAction(['add', basefield + "['" + newid + "']", null]);
editor_mode.onmode('save', function () {
printf('添加成功,刷新后生效;也可以继续新增其他项目。')
});//自动保存 删掉此行的话点保存按钮才会保存
}
/////////////////////////////////////////////////////////////////////////////
editor.constructor.prototype.table = new editor_table();
}
//editor_table_wrapper(editor);

339
editor_ui.js Normal file
View File

@ -0,0 +1,339 @@
editor_ui_wrapper = function (editor) {
var tip=document.getElementById('tip');
var print = function (msg, cls) {
if (msg == '') {
tip.innerHTML = '';
return;
}
tip.innerHTML = '<p class="'+cls+'">' + msg + "</p>";
}
window.printf = function (msg) {
selectBox.isSelected(false);
print(msg, 'successText');
}
window.printe = function (msg) {
selectBox.isSelected(false);
print(msg, 'warnText');
}
window.printi = function (msg) {
print(msg, 'infoText');
}
editor.uifunctions.showBlockInfo = function (value) {
if (value == 0) {
printi("当前选择为清除块,可擦除地图上块");
return;
}
var hasId = 'id' in value;
if (hasId && value.idnum == 17) {
printi("当前选择为空气墙, 在编辑器中可视, 在游戏中隐藏的墙, 用来配合前景/背景的贴图");
return;
}
var isAutotile = hasId && value.images == "autotile";
tip.innerHTML = (hasId?`<p>图块编号:<span class="infoText">${ value['idnum'] }</span></p>
<p>图块ID<span class="infoText">${ value['id'] }</span></p>`:`
<p class="warnText">该图块无对应的数字或ID存在请先前往icons.js和maps.js中进行定义</p>`)+`
<p>图块所在素材<span class="infoText">${ value['images'] + (isAutotile ? '( '+value['id']+' )' : '') }</span>
</p>
<p>图块索引<span class="infoText">${ value['y'] }</span></p>`;
}
editor.uifunctions.showTips = function (value) {
var tips = [
'表格的文本域可以双击进行编辑',
'双击地图可以选中素材,右键可以弹出菜单',
'双击事件编辑器的图块可以进行长文本编辑/脚本编辑/地图选点/UI绘制预览等操作',
'ESC或点击空白处可以自动保存当前修改',
'H键可以打开操作帮助哦',
'tileset平铺模式可以在地图上拖动来平铺框选的图形',
'可以拖动地图上的图块和事件或按Ctrl+C, Ctrl+X和Ctrl+V进行复制剪切和粘贴Delete删除右键也可以拉框选择区域',
'Alt+数字键保存图块,数字键读取保存的图块',
];
if (value == null) value = Math.floor(Math.random() * tips.length);
printf('tips: ' + tips[value])
}
/**
* 根据鼠标点击, 得到从元素向上到body的所有id
*/
editor.uifunctions.getClickpath = function (e) {
//console.log(e);
var clickpath = [];
var getpath = function (e) {
var path = [];
var currentElem = e.target;
while (currentElem) {
path.push(currentElem);
currentElem = currentElem.parentElement;
}
if (path.indexOf(window) === -1 && path.indexOf(document) === -1)
path.push(document);
if (path.indexOf(window) === -1)
path.push(window);
return path;
}
getpath(e).forEach(function (node) {
if (!node.getAttribute) return;
var id_ = node.getAttribute('id');
if (id_) {
if (['left', 'left1', 'left2', 'left3', 'left4', 'left5', 'left8', 'mobileview'].indexOf(id_) !== -1) clickpath.push('edit');
clickpath.push(id_);
}
});
return clickpath;
}
/**
* editor.dom.body.onmousedown
* 检测鼠标点击,
* + 如果选中了绘图区域之外, 就保存地图
* + 维护绘图区的菜单的隐藏
* + 记录最后一次点击的id(主要为了数据区服务)
*/
editor.uifunctions.body_click = function (e) {
var clickpath = editor.uifunctions.getClickpath(e);
var unselect = true;
for (var ii = 0, thisId; thisId = ['edit', 'tip', 'brushMod', 'brushMod2', 'brushMod3', 'brushMode4', 'layerMod', 'layerMod2', 'layerMod3', 'viewportButtons'][ii]; ii++) {
if (clickpath.indexOf(thisId) !== -1) {
unselect = false;
break;
}
}
if (unselect && !editor.uivalues.lockMode) {
if (clickpath.indexOf('eui') === -1 && clickpath.indexOf('lastUsed') === -1) {
if (selectBox.isSelected()) {
editor_mode.onmode('');
editor.file.saveFloorFile(function (err) {
if (err) {
printe(err);
throw (err)
}
; printf('地图保存成功');
editor.uifunctions.unhighlightSaveFloorButton();
});
}
selectBox.isSelected(false);
editor.info = {};
}
}
//editor.mode.onmode('');
if (e.button != 2 && !editor.isMobile && clickpath.indexOf('midMenu') === -1) {
editor.uifunctions.hideMidMenu();
}
if (clickpath.indexOf('down') !== -1 && clickpath.indexOf('midMenu') === -1 && editor.isMobile) {
editor.uifunctions.hideMidMenu();
}
if (clickpath.length >= 2 && clickpath[0].indexOf('id_') === 0) { editor.lastClickId = clickpath[0] }
}
/**
* editor.dom.body.onkeydown
* 绑定快捷键
*/
editor.uifunctions.body_shortcut = function (e) {
editor.uivalues.tileSize = [1,1];
// UI预览 & 地图选点
if (editor.uievent && editor.uievent.isOpen) {
editor.uievent.onKeyDown(e);
return;
}
// 监听Ctrl+S保存
if (e.ctrlKey && e.keyCode == 83) {
e.preventDefault();
if (editor_multi.id != "") {
editor_multi.confirm(); // 保存脚本编辑器
}
else if (editor_blockly.id != "") {
editor_blockly.confirm(); // 保存事件编辑器
}
else {
editor_mode.saveFloor();
}
return;
}
// 如果是开启事件/脚本编辑器状态,则忽略
if (editor_multi.id != "" || editor_blockly.id != "")
return;
// PGUP和PGDOWN切换楼层
if (e.keyCode == 33 || e.keyCode == 34) {
e.preventDefault();
var saveFloor = document.getElementById('saveFloor');
if (saveFloor && saveFloor.classList.contains('highlight')) {
return;
}
var index = editor.core.floorIds.indexOf(editor.currentFloorId);
var nextIndex = index + (e.keyCode == 33 ? 1 : -1);
if (nextIndex >= 0 && nextIndex < editor.core.floorIds.length) {
var toId = editor.core.floorIds[nextIndex];
editor_mode.onmode('nextChange');
editor_mode.onmode('floor');
document.getElementById('selectFloor').value = toId;
editor.uivalues.recentFloors.push(editor.currentFloorId);
editor.changeFloor(toId);
}
return;
}
var focusElement = document.activeElement;
if (!focusElement || focusElement.tagName.toLowerCase() == 'body'
|| focusElement.id == 'selectFloor' || focusElement.id == 'bigmapBtn'
|| focusElement.id.startsWith('layerMod')) {
//Ctrl+z 撤销上一步undo
if (e.keyCode == 90 && e.ctrlKey) {
e.preventDefault();
if (editor.uivalues.preMapData.length > 0) {
var data = editor.uivalues.preMapData.pop();
editor.dom.maps.forEach(function (one) {
editor[one] = JSON.parse(JSON.stringify(data[one]));
});
editor.updateMap();
editor.uivalues.postMapData.push(data);
editor.uifunctions.highlightSaveFloorButton();
printf("已撤销此操作,你可能需要重新保存地图。");
}
return;
}
//Ctrl+y 重做一步redo
if (e.keyCode == 89 && e.ctrlKey) {
e.preventDefault();
if (editor.uivalues.postMapData.length > 0) {
var data = editor.uivalues.postMapData.pop();
editor.dom.maps.forEach(function (one) {
editor[one] = JSON.parse(JSON.stringify(data[one]));
});
editor.updateMap();
editor.uivalues.preMapData.push(data);
editor.uifunctions.highlightSaveFloorButton();
printf("已重做此操作,你可能需要重新保存地图。");
}
return;
}
// Ctrl+C, Ctrl+X, Ctrl+V
if (e.ctrlKey && e.keyCode == 67 && !selectBox.isSelected()) {
e.preventDefault();
editor.uivalues.copyedInfo = editor.copyFromPos(editor.uivalues.selectedArea);
printf('该点事件已复制;请注意右键地图拉框可以复制一个区域;若有时复制失灵请多点几下空白处');
return;
}
if (e.ctrlKey && e.keyCode == 88 && !selectBox.isSelected()) {
e.preventDefault();
editor.savePreMap();
editor.uivalues.copyedInfo = editor.copyFromPos(editor.uivalues.selectedArea);
editor.clearPos(true, editor.uivalues.selectedArea, function () {
printf('该点事件已剪切;请注意右键地图拉框可以剪切一个区域;若有时剪切失灵请多点几下空白处');
editor.uifunctions.unhighlightSaveFloorButton();
})
return;
}
if (e.ctrlKey && e.keyCode == 86 && !selectBox.isSelected()) {
e.preventDefault();
if (!editor.uivalues.copyedInfo) {
printe("没有复制的事件");
return;
}
editor.savePreMap();
editor.pasteToPos(editor.uivalues.copyedInfo);
editor.updateMap();
editor.file.saveFloorFile(function (err) {
if (err) {
printe(err);
throw (err)
}
; printf('粘贴事件成功;若有时粘贴失灵请多点几下空白处');
editor.uifunctions.unhighlightSaveFloorButton();
editor.drawPosSelection();
});
return;
}
// DELETE
if (e.keyCode == 46 && !selectBox.isSelected()) {
editor.savePreMap();
editor.clearPos(true, editor.uivalues.selectedArea, function () {
printf('该点事件已删除;请注意右键地图拉框可以删除一个区域;;若有时删除失灵请多点几下空白处');
editor.uifunctions.unhighlightSaveFloorButton();
})
return;
}
// ESC
if (e.keyCode == 27) {
if (selectBox.isSelected()) {
editor_mode.onmode('');
editor.file.saveFloorFile(function (err) {
if (err) {
printe(err);
throw (err)
}
; printf('地图保存成功');
});
}
selectBox.isSelected(false);
editor.info = {};
return;
}
//alt + 0~9 改变快捷图块
if (e.altKey && [48, 49, 50, 51, 52, 53, 54, 55, 56, 57].indexOf(e.keyCode) !== -1) {
var infoToSave = JSON.stringify(editor.info || 0);
if (infoToSave == JSON.stringify({})) return;
editor.uivalues.shortcut[e.keyCode] = JSON.parse(infoToSave);
printf('已保存该快捷图块, 数字键 ' + (e.keyCode - 48) + ' 使用.')
editor.config.set('shortcut', editor.uivalues.shortcut);
return;
}
//ctrl + 0~9 切换到快捷图块
if ([48, 49, 50, 51, 52, 53, 54, 55, 56, 57].indexOf(e.keyCode) !== -1) {
editor.setSelectBoxFromInfo(JSON.parse(JSON.stringify(editor.uivalues.shortcut[e.keyCode] || 0)));
return;
}
switch (e.keyCode) {
// WASD
case 87: editor.moveViewport(0, -1); break;
case 65: editor.moveViewport(-1, 0); break;
case 83: editor.moveViewport(0, 1); break;
case 68: editor.moveViewport(1, 0); break;
// F
case 70: editor.uifunctions.triggerBigmap(); break;
// Z~.
case 90: editor_mode.change('map'); break; // Z
case 88: editor_mode.change('loc'); break; // X
case 67: editor_mode.change('enemyitem'); break; // C
case 86: editor_mode.change('floor'); break; // V
case 66: editor_mode.change('tower'); break; // B
case 78: editor_mode.change('functions'); break; // N
case 77: editor_mode.change('appendpic'); break; // M
case 188: editor_mode.change('commonevent'); break; // ,
case 190: editor_mode.change('plugins'); break; // .
// H
case 72: editor.uifunctions.showHelp(); break;
}
return;
}
}
editor.uifunctions.showHelp = function () {
alert(
"快捷操作帮助:\n" +
"ESC / 点击空白处:自动保存当前修改\n" +
"F切换大地图\n" +
"WASD / 长按箭头:平移大地图\n" +
"PgUp, PgDn / 鼠标滚轮:上下切换楼层\n" +
"Z~.(键盘的第三排):快捷切换标签\n" +
"双击地图:选中对应点的素材\n" +
"右键地图:弹出菜单栏\n" +
"Alt+0~9保存当前使用的图块\n" +
"0~9选中保存的图块\n" +
"Ctrl+Z / Ctrl+Y撤销/重做上次绘制\n" +
"Ctrl+S事件与脚本编辑器的保存并退出\n" +
"双击事件编辑器:长文本编辑/脚本编辑/地图选点/UI绘制预览"
);
}
}

1068
editor_uievent.js Normal file

File diff suppressed because it is too large Load Diff

173
editor_util.js Normal file
View File

@ -0,0 +1,173 @@
editor_util_wrapper = function (editor) {
editor_util = function () {
}
editor_util.prototype.guid = function () {
return 'id_' + 'xxxxxxxx_xxxx_4xxx_yxxx_xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
editor_util.prototype.HTMLescape = function (str_) {
return String(str_).split('').map(function (v) {
return '&#' + v.charCodeAt(0) + ';'
}).join('');
}
editor_util.prototype.getPixel = function (imgData, x, y) {
var offset = (x + y * imgData.width) * 4;
var r = imgData.data[offset + 0];
var g = imgData.data[offset + 1];
var b = imgData.data[offset + 2];
var a = imgData.data[offset + 3];
return [r, g, b, a];
}
editor_util.prototype.setPixel = function (imgData, x, y, rgba) {
var offset = (x + y * imgData.width) * 4;
imgData.data[offset + 0] = rgba[0];
imgData.data[offset + 1] = rgba[1];
imgData.data[offset + 2] = rgba[2];
imgData.data[offset + 3] = rgba[3];
}
// rgbToHsl hue2rgb hslToRgb from https://github.com/carloscabo/colz.git
//--------------------------------------------
// The MIT License (MIT)
//
// Copyright (c) 2014 Carlos Cabo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//--------------------------------------------
// https://github.com/carloscabo/colz/blob/master/public/js/colz.class.js
var round = Math.round;
var rgbToHsl = function (rgba) {
var arg, r, g, b, h, s, l, d, max, min;
arg = rgba;
if (typeof arg[0] === 'number') {
r = arg[0];
g = arg[1];
b = arg[2];
} else {
r = arg[0][0];
g = arg[0][1];
b = arg[0][2];
}
r /= 255;
g /= 255;
b /= 255;
max = Math.max(r, g, b);
min = Math.min(r, g, b);
l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
//CARLOS
h = round(h * 360);
s = round(s * 100);
l = round(l * 100);
return [h, s, l];
}
//
var hue2rgb = function (p, q, t) {
if (t < 0) { t += 1; }
if (t > 1) { t -= 1; }
if (t < 1 / 6) { return p + (q - p) * 6 * t; }
if (t < 1 / 2) { return q; }
if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; }
return p;
}
var hslToRgb = function (hsl) {
var arg, r, g, b, h, s, l, q, p;
arg = hsl;
if (typeof arg[0] === 'number') {
h = arg[0] / 360;
s = arg[1] / 100;
l = arg[2] / 100;
} else {
h = arg[0][0] / 360;
s = arg[0][1] / 100;
l = arg[0][2] / 100;
}
if (s === 0) {
r = g = b = l; // achromatic
} else {
q = l < 0.5 ? l * (1 + s) : l + s - l * s;
p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [round(r * 255), round(g * 255), round(b * 255)];
}
editor_util.prototype.rgbToHsl = rgbToHsl
editor_util.prototype.hue2rgb = hue2rgb
editor_util.prototype.hslToRgb = hslToRgb
editor_util.prototype.encode64 = function (str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}))
}
editor_util.prototype.decode64 = function (str) {
return decodeURIComponent(atob(str.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
}
editor_util.prototype.isset = function (val) {
return val != null && !(typeof val == 'number' && isNaN(val));
}
editor_util.prototype.checkCallback = function (callback) {
if (!editor.util.isset(callback)) {
editor.printe('未设置callback');
throw ('未设置callback')
}
}
editor.constructor.prototype.util = new editor_util();
}
//editor_util_wrapper(editor);

467
element.md Normal file
View File

@ -0,0 +1,467 @@
# 元件说明
?> 在这个部分,将详细讲解编辑器的每个部件的用法。
## 素材区
素材区在展开状态下,从左到右分为若干列:
1. `project\materials\terrains.png`:位于素材区第一列(最上面两个图块不在这张图片里,它们分别是擦除和空气墙),其中从楼梯开始往下有系统含义,请勿随意修改其图块属性。
2. `project\materials\animates.png`位于素材区第二列共4帧。主要为星空、岩浆、三色墙、六色门、四向出入口箭头、四种路障。
3. `project\materials\enemys.png`32×32px像素下同的怪物您可以随意修改它们的任何属性。如果嫌两帧太少还可以作为32×48px怪物画在靠下2/3部分。
4. `project\materials\enemy48.png`32×48px的怪物自带只有四只。您可以随意修改它们的任何属性。
5. `project\materials\npcs.png`32×32px的NPC如老人、商人、小偷、公主、仙子、木牌、魔龙和章鱼的其他8块您可以随意修改它们的任何属性。
6. `project\materials\npc48.png`32×48的NPC自带只有样板0层的小姐姐但您也可以用它来制作32×48px的门。
7. `project\autotiles`:自动元件,会随着在地图上的连续摆放而自动采取适当的绘制方式。
8. `project\tilesets`额外素材用来突破其他素材合计不得超过10000个的限制。您可以在这个区域拖动来批量框选再在地图区单击成片绘制或拖动平铺。
V2.7.3中,`terrains.png`追加了薄墙图块红线V2.8中它们被挪到了最下方,可供利用。
V2.8中自动元件在素材区折叠模式下会只显示左上角一格与RPG Maker一致。
## 地图编辑快捷键Z
![image](img/editor.jpg)
如图所示,您可以在此对地图进行清空或删除操作,也可以新建或批量新建任意宽高的空白地图。
其中“导出并复制地图”是指显示出左侧的矩阵并复制(一般用来跨塔复制地图),您也可以直接改动其中的数字(例如将绿色史莱姆批量替换为红色史莱姆),再点击“从框中导入地图”就能将改动的结果同步到地图上。
下面的“楼层ID、中文名、状态栏名”分别对应楼层属性快捷键V的floorId、title和name其中floorId也作为文件名不能使用中文注意大小写问题title会显示在楼传界面和楼层切换黑屏name也允许使用中文但请注意控制字数。
## 图块属性快捷键C
![image](img/mapsc.jpg)
如上图,除怪物和道具外,所有素材的图块属性都定义在`project\maps.js`中。道具和怪物属性也支持清空和批量复制,下面逐一讲解各条目的含义和用法:
1. **图块ID**图块的唯一标识符`core.getBlockId(x, y, floorId, showDisable)`,不允许使用中文和纯数字。请注意,额外素材`tileset`的图块ID由素材图片的顺序和图块在图片上的位置确定无法更改也请勿随意调换图片的顺序。样板已注册的图块中建议只修改怪物和NPC的图块ID修改方法为上图最下方的**修改图块id为**
2. **图块数字:**见前面的描述额外素材的数字由ID去掉字母X得到。
3. **图块类别:**图块素材的类型。
4. **图块名称:**怪物在手册中、道具在道具栏中、其他图块在剧情对话中的默认名称,可以随意修改。但原则上不推荐不同的怪物和道具有重复的名称,否则会影响事件编辑器的中文替换功能。
V2.8中,图块名称在有罗马数字、希腊字母、日文假名的情况下也支持事件编辑器中的中文替换了,这大大方便了一些科幻或魔法题材的作品。
V2.8中,道具名称支持使用${表达式计算}语法,但这样做会使其在事件编辑器中的中文替换失效。
你可以随时使用 `core.getBlockId(x, y, floorId, showDisable)` 获得地图上任何一个点的图块ID`core.getBlockCls(x, y, floorId, showDisable)` 或的地图上任何一个点的图块类别;详见[API列表](api)。
在讲解其他属性之前,这里简单介绍一下素材的注册机制:
* 除自动元件和额外素材外其余图块只有在注册后才拥有上面所说的ID和数字。
* 未注册的图块则只有“索引”索引为n表示该图块在图片的第n+1
* ID和索引的对应关系定义在`project\icons.js`中。
* 尝试用未注册的图块如利用便捷PS工具新追加的图块在地图上绘制就会出现红色的问号方框。
* 此时请在数据区手动注册此图块只需填写一个新ID和数字10000以内即可。
* 也可以点击“自动注册”按钮批量注册该图片的所有未注册素材自动注册出的ID无任何语义一般是一个表示图块类别的大写字母加几个数字建议手动修改成有语义的内容如史莱姆以Slime结尾
自动元件的注册与此不同除了替换样板现有的几个外如果还需要追加新的请在地图区下方的下拉框中切换到“追加素材”快捷键M然后导入文件到画板autotile再点击“追加”按钮即可。非Windows系统追加其他素材也主要依靠这种方式具体用法请自行探索。
V2.7起,电脑端支持将文件直接拖动到素材区对应的列,进行追加。具体规则如下:
1. 道具的图片宽高必须为32的倍数每格会被追加为一个新道具
2. 其他类别的多帧图块不包括自动元件和tileset规格必须为128×128、128×192、96×128、96×192之一分别对应32×32和32×48的四行四列或四行三列后者为RPG Maker VX Ace格式
3. 向两帧的enemys.png或npcs.png追加时四列的图片取中间两列三列的图片取两边两列。
4. 向四帧的animates.png、enemy48.png、npc48.png追加时三列的图片按2123排布。
V2.8起图块支持“注销并删除”操作执行后将对已注册的删除该图块在maps.js和icons.js的信息如果是怪物和道具则还会删除对应的信息然后将该图块从图片上删除图片上更靠下的图块会被统一上移一格。
### 非怪物非道具属性
1. **触发器:**当碰触到地图上此图块时触发的系统事件,详见[事件](event)。
* **battle**: (未列出)战斗;当撞上一个怪物且没有覆盖触发器时(参见[事件](event))将自动调用此触发器产生战斗,并触发战前和战后事件。
* **getItem**: (未列出)拾获道具;当撞上/轻拾一个道具且没有覆盖触发器时(参见[事件](event))将自动调用此触发器获得它,并触发(拾获)道具后事件。
* **changeFloor**: (未列出)楼层切换;对于地图上绑定的绿点(常见于楼梯或彩色箭头)将自动调用此触发器产生楼层切换事件。
* **openDoor**: 用于制作门效果,当撞上此图块时将尝试开门(仅对`animates`和`npc48`生效),并触发开门后事件;具体开门动画参见下面的门信息。
* **pushBox**: 推箱子;请勿对非箱子使用此触发器。
* **ski**: 滑冰;拥有此触发器的图块放置在背景层时,走上去将触发滑冰效果。
* **custom**: 自定义系统触发器;你可以使用 `core.registerSystemEvent` 来自己定义一个系统触发器,参见[API列表](api)。
2. **可通行性:**勾选后勇士才可以踏入此图块,否则只能撞击此图块。(怪物被锁定为不可通行,道具被锁定为可通行,如有需要可以修改点上的不可通行性)
3. **碰触脚本/碰触事件:**勇士踏入或撞击此图块时执行的脚本该项会被eval相当于一种自定义的触发器您可以参考踩灯和四种路障去填写它。V2.8起,新增了“碰触事件”(会覆盖该点的普通事件),使用起来更方便也更安全。
4. **不可出入方向:**对三个图层的图块都有效。不可出方向指的是勇士站在这种图块上不能向哪个方向走包括撞击不可入方向指的是勇士不能从哪个方向走向这种图块包括撞击。例如不可入方向勾选了“上”则不能“从上方、向下”走向这个图块。请参考素材区第一列的四个灰色箭头样板1层右下角也有
* V2.8.1起,编辑器地图区下方提供了“通行度”勾选框,您可以随时勾选它来查看“不可出入方向”(包括图块的和点的)在地图上的实际效果。
5. **可破震:**勾选后此图块将成为破墙镐pickaxe和地震卷轴earthquake这两个道具的目标。
6. **动画帧数:**您可以修改此帧数来让本来有4帧的图块只用前2或3帧循环播放另外制作门时请务必将此帧数改为1表示门在打开前静止在第1帧。
7. **门信息:**只对`animates`和`npc48`有效您可以点击“编辑按钮”来填写此图块作为门的开关耗时、开关音效以及需要哪些钥匙各多少把可以填写任何消耗类道具也可以选择某些道具只需持有一定数量而不消耗。修改此信息后您需要将上面的“动画帧数”改为1并可能需要将“触发器”改为openDoor不改的话将无法通过撞击来开门但可以像三色墙一样用来制作暗墙
* 该项在V2.7首次提供,只支持“同时需要多种钥匙各多少把、消耗其中哪几种”的“且运算”条件。
* 如果您需要指定“或运算”条件如优先消耗一种钥匙没有再消耗另一种或“消耗量”少于“需要持有量”或者需要判断的条件不是道具而是其他条件比如勇士状态或flag请使用“碰触事件”但这样将无法自动存档。
* V2.8新增了(批量)开门后事件,它和(单点)开门后事件的执行先后顺序,由“脚本编辑——开门后脚本”指定,默认先执行单点的。
* 因此,复杂的开门条件也可以换个角度思考,把开门条件设为什么都不需要(先斩后奏,这一思想在道具使用条件中也很常见),开门后事件中再进行判断,如果开门成功就按照条件扣除钥匙,开门失败就再把门关上就好了,这样就保留了自动存档功能。
* V2.8.1起npc48支持绑定大型贴图您可以制作大型门了开关门的动画会更为震撼
8. **行走图朝向:**设置后当勇士撞击该图块时图块会尝试转身面向勇士对话事件结束前请使用“事件转向”指令将其转回去。走动时也会尝试自动转向请参考样板0层使用的小姐姐。
* V2.8起,“行走图朝向”可以用于怪物,绑定后只需手动设置脸朝下的怪物属性(包括下面的绑定贴图)而不用管其他的,手册显示、实际战斗、阻激夹域(尤其是夹击)、漏怪检测(`hasEnemyLeft`)等都会强制读取脸朝下的怪物,游戏中动态修改任何一个朝向的怪物属性都会立即强制同步到四个怪物身上。
* npc在设置行走图朝向以后其他属性不具有自同步机制包括下面的绑定贴图请自行注意。
* 该项的四个方向甚至可以写不同的图块类别,后果未知。
9. **绑定贴图:**V2.8.1新增可以用于NPC和怪物。这很大程度上弥补了H5魔塔比起RPG Maker不能使用大型行走图的缺憾。
* 点击此项时将会直接弹窗请求选择文件文件首先会判断总高度和总宽度的比例是否大于0.5。
* 【1×4】如果这个比例不大于0.5(这个临界值可以通过复写`core.maps._getBigImageInfo`来修改那么整张图视为1行4列每列的高度至多为宽度的2倍即一个图块的单向4帧行走图。方向默认视为朝下如需修改可以在其“行走图朝向”只绑定一个需要的方向id写自己
* 【4×4】如果这个比例大于0.5那么整张图视为4行4列共16块即4个朝向图块各自的4帧行走图从上到下的朝向分别为“下、左、右、上”因此您需要注册四个最好是同类别的图块并绑定好行走图朝向贴图则都绑定这一张。
* 该图块放在地图的事件层以后本体会透明化,并且根据朝向(默认向下)选择行走图的绘制偏移量。
* 例如,朝下的图块,行走图在绘制时会让本体位于图片下缘中央,其他方向同理。
* 而在编辑器中1:1显示模式下大贴图会被缩小到1格如需预览原始尺寸的效果请点击地图区下方的“大地图”按钮。
* 游戏中该图块会全程使用绑定的贴图(选择项的子选项图标和文本的`\\i[]`转义序列仍然使用本体)进行显示,包括在怪物手册和显示文章的`\t[]`效果(这两种都会被缩小到一定尺寸)。而显示文章的`\b[]`效果则还是根据图块类别使用原始高度不过V2.8.1中您可以手动去掉尖角然后指定对话框的左上角坐标和限宽。
* 对该图块执行“显隐事件、转变图块、开关门npc48、不透明度设置和渐变、移动跳跃”都会直接操作大贴图这比起2.7.x的楼层贴图要好用许多
* 但是如果您有一张图片希望分成16小格但是每行对应完全独立的四个图块而不具有行走图朝向关系该怎么做呢
* “插件编写”的第一项init中提供了“资源加载后的操作”您可以在里面用`splitImage`将该图的四行提前裁剪成四张图片并重新命名,就可以使用啦!
* 手动使用脚本切分出的新图片无法通过弹窗选取,必须手动填写,敬请谅解。
* 如果希望能够弹窗选取,可以使用全塔属性中的“图片切分”功能。
``` js
this._afterLoadResources = function () {
// 这是一个将4by4.png假设为384*384按行拆分成四个图片并保存的样例
// 可以用来在没有条件ps如原图尺寸不支持便捷ps或者手机造塔时的条件下
// 预处理图片来得到独立朝向的大型行走图1of4.png到4of4.png
var arr = core.splitImage('4by4.png', 384, 96); // 以宽386高96进行切分图片
for (var i = 1; i <= arr.length; ++i)
core.material.images.images[i + 'of4.png'] = arr[i - 1];
}
```
### 道具属性
样板自带的道具都在样板0层摆好了您可以直接进入游戏捡起它们就会看到该道具的注意事项这里不再赘述。
1. **道具类别:**虽然和图块类别的英文缩写都是cls但有本质区别请注意区分。道具的图块类别都是items而道具类别分为以下几种
* items是的你没看错又是`items`这个词,请注意和图块类别的`items`相区分。它表示即捡即用类不进背包的道具,如四种血瓶、三种宝石等。这类道具需要用到的其他属性有“即捡即用效果”、“即捡即用提示”、“碰触或使用事件”。
* tools进背包的消耗类道具如钥匙和解药瓶、便携式血瓶蓝瓶生命魔杖、破震炸飞和跳跃靴等。这类道具需要用到的其他属性有“道具描述”、“不显示在道具栏”、“回放不绘制道具栏”、“碰触或使用事件”、“使用效果”、“能否使用或装备”。
* constants进背包的永久道具每件在背包的数量要么为1要么为0如手册、楼传、幸运金币、十字架、护符、二倍斩等这类道具需要用到的其他属性和tools一致。
* equips装备它需要用到的其他属性有“道具描述”、“道具的装备属性”、“能否使用或装备”。
* 例如如果您想把四种血瓶和三种宝石改为便携式只需把其道具类别改为tools当然楼层属性中的ratio一项也就失效了
* 如果想把大黄门钥匙变为钥匙盒红黄蓝钥匙各一把只需把其道具类别从tools改为items
* 如果想把剑盾变成装备,只需把其道具类别改为`equips`
* 如果想修改破墙镐/炸弹/冰冻徽章的目标个数V2.8支持改为八方向,使用`core.utils.scan2`即可)或让炸弹能够获得金经/触发战后事件,请修改它们的使用效果。
2. **道具描述:**对除即捡即用类外的道具都有效。一个字符串,为道具在道具栏里的描述,也作为首次捡到时的提示信息的一部分(如果全塔属性中开启了这一提示功能的话)。支持使用`${表达式计算}`语法(如四种血瓶和三种宝石那样,但不支持中文替换),此语法的详细规则见“显示文章正文的转义序列”,和“值块和冒号缩写量”。
3. **不显示在道具栏:**对tools和constants有效勾选此项后该道具在背包中将不显示出来。常用于不能主动使用或已有专门的使用按钮的道具来节省显示篇幅如手册、楼传、幸运金币、十字架、护符和钥匙等。该属性的详细原理见“脚本编辑N键”最下面的“道具栏显示项”。
4. **回放不绘制道具栏:**勾选此项后,录像回放中使用此道具将不显示黑黑的道具栏。常用于频繁使用的道具,如楼传、技能和冰冻徽章等。
5. **即捡即用效果:**如题该项会被eval一般为一行下述的代码 `core.status.hero.xxx += yyy * core.status.thisMap.ratio`
* 其中xxx为勇士的某种状态如生命hp、生命上限hpmax、魔力mana、魔力上限manamax、护盾mdef、攻防、金经
* yyy为此道具的基础效果如四种血瓶和三种宝石的基础效果定义在了全塔属性中
* `core.status.thisMap.ratio`则是指该道具所在楼层的“楼层属性”最下面的“宝石血瓶效果”。
* 此效果会在“B键数据统计”被模拟执行使用道具所在楼层的ratio然后计算执行前后勇士状态差异因此请尽量只在里面用脚本直接操作`core.status.hero`而不要使用tip或特效。
* 可以在此项中使用`core.insertAction([...])`或`core.insertCommonEvent()`插入事件或公共事件,但不会计入数据统计。
6. **即捡即用提示:**实际显示时会被接在“获得xxx”后面所以该项总是一个以逗号开头的字符串同样支持`${表达式计算}`语法。
7. **碰触或使用事件:**对除equips外都有效。该项用于代替“即捡即用效果”但会使勇士停下脚步且会晚于地图上的afterGetItem事件被执行且不计入B键数据统计和“使用效果”如样板中的黄宝石和生命魔杖。如果您的js语法基础薄弱那么它将是您的不二之选。
8. **使用效果:**对tools和constants有效。该项会被eval一般为一个js函数较为简单的使用效果如解药瓶也可能是一行代码破炸冰的目标个数请直接在该项中修改。总的来说因为事件比起脚本更容易实现异步特效且录像安全性更好所以如非必要不建议用此项。
9. **能否使用或装备:**对tools、constants、equips有效。该项也会被eval一般为一个js函数较为简单的使用条件如解药瓶也可能是一行形如`"core.hasFlag('xxx')"`的代码。
* 如果该道具在任何情况下都不能主动使用,请留`null`(并最好勾选“不显示在道具栏”以节约显示篇幅)。如果该道具在任何情况下都可以主动使用,请填`"true"`。
* 如果使用条件较为复杂,也推荐直接填`"true"`先斩后奏,在使用效果中再行判定,并在使用失败的场合使用`core.addItem('xxx')`静默返还一件该道具,如样板中的破墙镐和炸弹。
* 如果用于装备,那么`null`表示任何情况下都可以装备。但是请注意装上以后,即使条件变得不满足也不会自动脱下。
10. **道具的装备属性:**在介绍此项之前请先留意一下“全塔属性”中的“装备孔”一项。该项为一个字符串数组最多允许6项13×13样板或8项15×15样板。每一项为装备的类型名称建议是两个汉字如“武器”、“防具”。类型允许重复如可以让勇士最多同时装备两块盾牌。
* 装备类型一个自然数和前面的“装备孔”对应如0表示武器1表示防具。如果装备孔有重复的名称则这里也直接写名称不用加引号穿戴时会自动尝试寻找第一个同类型的空闲装备位如果同类型的装备位唯一则会不管空闲与否直接替换没有空闲的话会提示玩家先卸下一件。
* 普攻动画:`project\animates`文件夹中任何一个文件的名称(不带后缀,但需要在全塔属性中注册过,支持别名),只对第一个装备孔有效。普攻动画会播放在和勇士战斗的怪物位置处,如果是强制战斗的天降怪物,则会播放在勇士身上并跟随,请自行注意。详见“文件注册”使用动画。
* 数值提升项若干个键值对表示该装备增加属性的常数值支持负数。7个常用属性可以通过下拉框选取自定义的新属性也可以手动输入。
* 百分比提升项:若干个键值对,表示该装备增加属性的百分比(支持负数,如填-10就表示减少10%),修改方法同上。
* 请注意两种提升项只能写常数请不要认为写个flag变量就能自动变化了。
* V2.8中两种提升项可以在游戏过程中通过事件指令来修改包括已经穿在身上的装备但是修改后的值仍然视为常数同时每件装备新增了“穿脱时事件”属性该事件以自动事件方式实现在换好装备后关闭装备栏的瞬间触发。而且下述各种buff可以使用冒号缩写量来读写了详见[事件](event)。
* “穿脱时事件”指的是身上某件装备“从无到有”和“从有到无”,不包括“从多到少”和“从少到多”(在装备孔有重复名称的情况下)。
* “穿脱时事件”的自动事件定义在`libs\core.js`如需修改例如改为检测“多少”而不是“有无”请在插件“init”中直接修改`core.initStatus.autoEvents`。
* 这是一个一维数组,数组的每项为一个自动事件对象,对象的`symbol`属性含有`"equipEvent_"`时就表明这是一个穿脱时事件。您可以修改其触发条件以及执行内容的第一项flag变化
* 装备对属性的影响原理:在穿脱装备时,会根据数值提升项和百分比提升项,分别调用`core.status.hero.xxx += yyy`和`core.addBuff('xxx', yyy)`这两个API衰弱的附加和解除同理而状态栏的显示值和战斗中的使用值则是`core.getStatus('xxx')`和buff值相乘再向下取整所得。
* 可以按Alt+0~9快速更换套装在装备界面按这个组合键则是保存套装手机端的Alt键在V2.8被提供为粘滞键,点击状态栏右下角的难度标签就能看到。
道具相关API请阅读[API列表](api)。
### 怪物属性
1. **手册ID**【已弃用】原本是V2.8以前用来不完全实现多朝向怪物和大型怪物缩略图的手段,现在建议全部用新增的“行走图朝向”属性和“单点图块不透明度设置”代替。
* 该项设置后怪物将在手册中不显示为原怪物而显示为对应的怪物ID比“行走图朝向”更优先如果对应ID的怪物在本楼层恰好也存在那么看上去就像原怪物在手册中消失了即RMXP魔塔的“伪装”属性可以适当利用这一点。
* 漏怪检测(`hasEnemyLeft`和阻激夹域尤其是夹击依然只会使用原怪物ID如有需求请使用“行走图朝向”属性。
2. **生命、攻防、金经:**如题注意金经必须在“全塔属性”快捷键B中的“状态栏显示项”中也勾选才能真正被启用。持有幸运金币时打怪获得的金币翻倍附加诅咒状态时打怪不获得金经。
3. **加点:**若全塔属性勾选了“加点”,则此项为正数时将作为与该怪物每场战斗胜利后传递给“公共事件——加点事件”的参数(即那里的`flag:arg1`,默认表示加点的倍率),您可以自行修改该事件。
4. **不可炸:**勾选后该怪物不会成为炸弹的目标有阻击怪在场的情况下请务必给有单点战后事件的怪物如机关门守卫和boss勾选此项否则玩家可能会偷梁换柱地炸掉该怪物并把阻击怪推过去打死来触发战后事件。不过V2.8.1起,炸弹的使用效果中提供了炸单点怪物触发战后事件的示例,可供利用。
5. **(批量)战前/战后事件:**V2.8新增,用于解决阻击怪等会移动的怪物不能使用地图上的战前/战后事件的问题。
* 两种战前事件都会在“撞击怪物、判定可以战胜、自动存档”后触发,这比起曾经的“覆盖触发器+天降强制战斗”对玩家更为友好。
* 批量战前事件默认晚于单点战前事件(修改需要复写函数),批量战后事件与单点战后事件的执行顺序可以直接在“脚本编辑——战后脚本”中调整,默认也是单点先执行。
* 两种战前事件在强制战斗时都不会触发,并且都早于支援怪的跳跃动画,因此捕捉怪的战前事件也不会触发。
* 两种战前事件中如果使用“立刻结束当前事件”就会取消战斗,必要时您可以利用这一点制作回合制战斗。
* 批量战前事件可以方便地制作“怪物先打一下勇士,再被勇士打死”的效果,同时延迟了怪物的消失,使得怪物根据“行走图朝向”转身后的效果能够来得及被看到。
* 批量战后事件可以方便地制作“被打败后立即变身为另一种怪物/路障,或掉落一种道具”的效果。
* 您新增加的类似先攻、破甲、净化、吸血、仇恨、固伤的属性可以使用战前/战后事件更便捷地实现并无视护盾同时也避免了下面提到的各项value冲突的问题但这样做的后果是不计入显伤详见下面的解释。如果一定要这样实现建议通过额外的说明提醒玩家。
6. **特殊属性:**一个由正整数组成的一维数组,您可以点击“多选框编辑”按钮来修改它。所有特殊属性都定义在“脚本编辑——怪物特殊属性”,您可以在那里追加新的。它们大体分为四类:
1. 手册中属性值的修正:(按照结算顺序)模仿、坚固、光环,修正后的属性也将被用于战斗,详见“脚本编辑——怪物真实属性”。
2. 战损的修正这类最多先攻、魔攻、连击次数为n、破甲比例为defValue、反击比例为atkValue回合数为勇士的攻击回合数、净化倍数为n1表示单纯无视护盾、吸血比例为value是否加到自身为add、仇恨每场战斗的仇恨增值由全塔属性指定、无敌、固伤数值为damage、支援。其中又以仇恨和固伤不能被护盾直接抵消而和无敌较为特殊详见“脚本编辑——战斗伤害信息”。
3. 战后的影响中毒、衰弱、诅咒、仇恨的累加和减半、自爆、退化扣减值分别为atkValue和defValue、重生详见“脚本编辑——战后脚本”和“脚本编辑——毒衰咒处理”。由于上面的“批量战前/战后事件”的存在,这种技能不再推荐由脚本实现。
4. 阻激夹域捕捉即对主角行走的妨害详见“脚本编辑——阻激夹域伤害”该函数也负责了血网图块ID为lavaNet请勿修改的伤害。
您会发现一个可怕的事情那就是上述1和2会影响显伤而3例如自爆和战前/战后事件不会且后者对勇士生命的影响默认是无视护盾的。这对于魔塔来说可能是致命的毕竟没有玩家想看到“一个明明显伤不是红色的怪物打了却会死”等情况。当然也有一种办法是像flash版新新魔塔1和2那样“战斗过程带有随机性或QTE手册只显示预估伤害”这种设计模式在rm和h5魔塔界并不常用请谨慎尝试。
支援怪在进行支援时护盾默认只计算一次不具有坐标类似天降强制战斗因此不支持V2.8的“定点设置怪物属性”,同时基于坐标的一些判定也会失效。
发生支援时,金经(以及加点塔的加点值)都会累加,但加点事件只执行一次,也就是这次所有点数必须加在同一项属性,如需修改,请修改公共事件。
阻激域的伤害都为value且在夹击之前结算领域的形状和半径与光环一致。如果需要更复杂的形状如米字形激光请自行研究该函数。
V2.8起阻击和捕捉支持九宫格形状由zoneSquare属性指定。如需实现九宫格夹击请仿照它们。阻击默认可以推到已隐藏的事件处如需禁止请修改所在的函数。
您甚至可以给同一种怪物设置阻击和捕捉,那么它会先后退然后在原位置留下一个残影和勇士战斗,从而起到刷金经的作用。
样板的光环是作用于怪物的,如果想制作作用于勇士的光环,需要注意缓存问题以免卡顿。
可以看到怪物属性中有很多值是彼此互相冲突的。请自行注意比如设计新属性时分散给各项而不要都吊死在三个value上。最后介绍一些和怪物相关的API
``` js
core.status.hero.flags.no_repulse = true; // 禁用阻击,包括伤害和后退效果
core.status.hero.flags.no_laser = true; // 禁用激光
core.status.hero.flags.no_betweenAttack = true; // 禁用夹击
core.status.hero.flags.no_zone = true; // 禁用领域
core.status.hero.flags.no_ambush = true; // 禁用捕捉
core.getItem('amulet'); // 禁用血网等路障
core.setEnemy('greenSlime', 'atk', 100); // 设置怪物属性,并计入存档
core.getDamageString(enemy, x, y, floorId); // 获取某只怪的地图显伤字符串和颜色
core.getCurrentEnemys(floorId); // 获取某层楼的(映射后)怪物列表,按战损递增排列
core.hasEnemyLeft(enemyId, floorId); // 漏怪检测,两个参数都允许使用一维数组
core.hasSpecial(special, test); // 检测special是否有test这一个特殊属性
```
如果您想在数据区的表格中追加新的属性项,或修改已有项的格式、范围和长短注释,请点击数据区顶部的“配置表格”按钮,并参照已有的项去追加和修改,具体可查阅[修改编辑器](editor)。
至此您会发现每种怪物不管出现在地图上的什么地方其属性都是一样的除非受到局部光环的影响。所幸的是V2.8提供了读写/移动单点怪物属性的API和事件这种属性会在“脚本编辑——怪物真实属性”中最早生效战后脚本中重置。此外因受此属性或光环影响而导致同层多个同种怪物在手册中的数值不一致时玩家可以选择作为多种怪物分别显示包括具体坐标详见游戏中的Esc菜单。
## 楼层属性快捷键V
![image](img/floor.jpg)
V2.7.1起楼层支持使用最大128×128的超大地图您可以在地图区下方点击“大地图”按钮切换到全景进行绘制或在1:1模式下使用wsad进行视角移动。
V2.8.1新增了怪物的大贴图绑定编辑器1:1模式下大贴图会被缩小到一格中您可以点击“大地图”按钮让大贴图按原始尺寸绘制。
1. **楼层ID**`project/floors`中的文件名不允许使用中文也不能直接修改。修改方法见上图底部修改后必须立即刷新浏览器页面该项需要注意大小写问题。另外作品发布时会统计楼层总数如果有一些剧情层或连载塔半成品层不希望被统计进去请使用“sample”+纯数字楼层ID即可就像三个样板层一样。
2. **楼层名:**楼层在楼传、上下楼黑屏和浏览地图界面的名称。
3. **状态栏显示:**勇士在此楼层时状态栏左上角“上楼梯”图标右边的文字,允许使用中文,但请注意控制字数。
4. **地图宽度和高度:**可在表格最下方修改。
* 如果地图被加宽或加高,则“偏移”表示右移或下移的格子数(左边缘或上边缘用空格子填补)。
* 如果地图被减窄或减矮,则“偏移”表示左移或上移的格子数(被移出左边缘或上边缘的图块将丢失)。
* 在大地图中,您可能需要对地图中的绝对坐标和视野中的相对坐标进行互相转换。这需要用到`core.bigmap`的以下几个属性:
* `core.bigmap.width`和`core.bigmap.height`表示大地图的宽高(单位为格子)。
* `core.bigmap.offsetX`和`core.bigmap.offsetY`表示视野左上角在大地图中的绝对像素坐标再除以32就是格子坐标。
* 因此,对于任意绝对坐标(x,y),它在视野中的相对坐标就是(x-offsetX/32,y-offsetY/32)。
* 在大地图的非边缘区域,`hero.loc.x-core.bigmap.offsetX/32`和`hero.loc.y-core.bigmap.offsetY/32`总是都为6或7样板尺寸的一半
* 反之,已知相对坐标求绝对坐标就用加法,请举一反三。
5. **几个勾选框:**
* 可楼传飞到如果不勾选则此楼层禁止成为楼传的目标楼层。该项的具体原理见“脚本编辑——flyTo楼层飞行”。
* 可楼传飞出:如果不勾选,则勇士在此楼层禁止使用楼传。
* 快捷商店:如果不勾选,则勇士在此楼层禁止快捷使用全局商店。事件中的“启用全局商店同时打开”不受影响,详见“插件编写——全局商店”。
* 不可浏览:如果勾选,则此楼层无法通过滚轮/PageUp/PageDown键浏览也不会计入全塔B键数据统计。
* 不可瞬移如果勾选则勇士在此楼层无法用E键和点击瞬移常用于用自动事件去监听勇士坐标时或者需要严格的计步属性时如每走10步失去一把黄钥匙
* 是否是地下层:如果勾选,则楼传非平面模式下(或平面模式下该层还没有锚点时)勇士在此楼层原地使用楼传会传送到上楼点,详见“脚本编辑——楼层飞行”。
6. **首次到达事件、每次到达事件:**如题,详见“脚本编辑——切换楼层后”。每层的这两个事件是共用独立开关的,常见用法有“进入新区时清空已到达的楼层列表,只保留当前楼层”从而实现勇士不能再飞回从前区域的效果。
7. **并行处理脚本:**一个字符串为勇士在此楼层时浏览器每帧都会执行一次eval的脚本最快每秒60次。一般用来制作一些定时特效如bgs、bgv详见“脚本编辑——并行脚本”。
8. **上下楼点:**两个自然数构成的一维数组,将作为“楼层转换”事件(在地图上以图块左下角出现绿色小方块作为标记)和“楼层切换”指令中“上下楼梯”以及非平面楼传的目标坐标。
* 如果不设置,则在传送时会尝试从地图中搜索上下楼梯图块。因此当某个楼层没有楼梯或有多个楼梯时(如《[新新魔塔](http://h5mota.com/games/xinxin/editor.html)》),请务必设置这个属性。点击“编辑”按钮从地图选点即可。
9. **楼传落点:**格式和设置方法同上。如果设置了此项,则楼传在此层的落点将强制变为该点,无视平面模式下的离开点和上面的上下楼点以及该层实际的楼梯位置。
10. **地面图块:**可以填写任何一个图块ID需要加引号或数字如1是墙6是冰块此项也会作为手册和剧情对话中的帧动画背景。填写tileset时可直接填写数字不需要带字母X和引号。
11. **色调:**一行四列的数组前三项为小于256的自然数分别表示红、绿、蓝最后一项为0到1的浮点数表示不透明度可以点击“编辑”按钮用调色器调色。
* 值得一提的是,很多事件也以颜色作为参数,这些都是可以使用调色器调色的。
12. **天气:**一行两列的数组第一项为字符串“rain”、“snow”、“fog”、“cloud”、“sun”第二项为不大于10的正整数分别表示1—10级的雨天雨丝数量和等级正相关、雪天由大小不一的白色实心圆组成详见样板1层雪花的数量和横飘速度和等级正相关、雾天、多云、晴天左上角洒下金色的阳光
* 色调层在天气层上方、UI层下方如不透明色调会遮盖天气浏览地图看不到色调关于图层的详细说明参见“个性化”
13. **背景音乐:**如题,当在游戏中触发楼层切换时(包括读档),如果`flag:__color__、flag:__weather__、flag:__bgm__`没有值,游戏当时的画面色调、天气、背景音乐就会变为楼层属性中的这三个设置项,详见“脚本编辑——切换楼层中”。
* V2.8起,该项允许多选,会随机播放其中一个。
14. **宝石血瓶效果:**如题,必须填写且必须为正数。此项的用法为`core.status.thisMap.ratio`,请参考四种血瓶和三种宝石的捡拾效果。
* V2.8起手册中的“1防”改为“加防”表示加ratio点防御能减伤多少。
* 您还可以将其用于其他各种场合作为系数,如血网的伤害、中毒后每步的损血等。
15. **楼层贴图:**【V2.8.1起】怪物和npc可以直接绑定4帧大贴图了因此楼层贴图主要用于布景。
* 由于样板提供的图块只有32×32px和32×48px两种尺寸且后者只能画在事件层每个图块最多只能有4帧因此对于一些多帧大图块十分不便如npcs.png中被大卸八块的魔龙和章鱼。
* 你可以使用“楼层贴图”,该项允许您使用任何尺寸、任何帧数的素材,唯一的缺点是不支持移动跳跃和淡入淡出效果。
* 点击“编辑”按钮进入事件编辑器,每张图片的写法为(可从入口方块拖出,然后双击预览第一帧的效果):
1. 图片名name如题图片需要放在`project/images`文件夹并注册。
2. 翻转(:x/:y/:o您可以对贴图的每帧进行三种翻转当然帧顺序在原图中依然是从左到右的。
3. 图层bg/fg/auto此项决定贴图绘制在哪个图层您可以全部画在背景层或前景层。也可以选择“自适配”让贴图的上半部分画在前景层下半部分画在背景层比如树木等。如果选择了自适配最好让下面的绘制坐标和宽高都是32的倍数。
4. 绘制坐标xy贴图在地图中的左上角像素坐标譬如x和y都填32则表示贴图左上角和“地图左上角格子的右下角”重合。
5. 初始禁用Y/N如果勾选了此项则此贴图初始时不显示您可以在事件中再将其显示出来。
6. 裁剪起点坐标xy和宽高wh此项规定了贴图在按帧切分前从原图中取哪一部分x和y为所取部分在原图中的左上角坐标不填视为两个0w和h为所取部分的宽高不填表示一直取到右下角
7. 帧数frame不填视为1如果填写了大于1的整数就会把上述裁剪得到的结果再从左到右等分为若干份并在实际绘制时从左到右逐帧可能还带有翻转循环绘制每帧的持续时间和其他图块一致。
* 贴图本身只具有观赏性,您仍然需要使用空气墙等手段去控制其绘制区域各个点的通行性。
* 在使用贴图来表现魔龙和章鱼这类大型怪物时需要预先准备一种32×322帧或32×484帧的行走图并注册为怪物放在地图上时指定该点的不透明度为0最后在该点的战后事件中隐藏贴图即可。
* 你可以在插件中复写`drawBg`和`drawFg`函数以控制贴图和图块的绘制顺序(默认先绘制图块),详见[脚本](script)。
``` js
////// 绘制背景层 //////
core.maps.drawBg = function (floorId, ctx) {
var onMap = ctx == null;
if (onMap) {
ctx = core.canvas.bg;
core.clearMap(ctx);
}
this._drawBg_drawBackground(floorId, ctx);
// ------ 调整这两行的顺序来控制是先绘制贴图还是先绘制背景图块
// 后绘制的覆盖先绘制的。
this._drawFloorImages(floorId, ctx, 'bg');
this._drawBgFgMap(floorId, ctx, 'bg', onMap);
}
```
## 全塔属性快捷键B
全塔属性共分为四部分:文件注册、初始勇士、全局数值、系统开关,您可以随时折叠其中任何一个部分。
![image](img/firstdatab.jpg)
### 文件注册
这部分基本上都是经由多选框半自动完成的,下面逐一讲解:
1. **楼层列表:**`project/floors`文件夹中的文件名不含后缀但请务必注意大小写问题此数组的顺序决定了楼传和上下楼器fly、upFly、downFly的顺序。
* 如果您不慎将勇士的出生楼层注销了或不慎删除了某些楼层的js文件导致编辑器页面打开后一片白屏请手动打开`project/data.js`去小心地修改floorIds以和实际的文件名相匹配并将出生楼层改为一个存在的楼层。
* 其他更复杂的白屏请在控制台根据报错信息安卓手机则使用ES文件浏览器查看日志去小心地修改文件如某个楼层文件有问题则可以注销它如果难以独立解决欢迎加QQ群959329661寻求帮助。
2. **分区指定:**一行两列的数组,可以使用事件编辑器编辑,每行表示塔的一个区域,该行的两列分别表示该区域的第一层和最后一层(后者不填表示到塔顶),不在任何区域的楼层组成公区。
* 游戏中,除了公区和勇士当前所在区以外的地图都将处于冻结状态,无法被浏览、无法被飞到、无法触发其自动事件、地图中的图块无法被读写。冻结状态的地图不会存入存档,从而节约了存档大小并加快了存读档速度,上百层的高层塔必备!
3. **使用图片、图片切分:**`project/images`文件夹中的文件名需要后缀必须全英数单击“编辑”按钮编辑器会自动扫描文件系统中格式合适的图片如jpg、png和gif
* 您可以预览并将需要的图片勾选。请注意,勇士的初始行走图必须在这里注册。另外,`winskin.png`只许替换为相同规格的图片而不要注销,否则道具商店等插件无法正常绘制。
* V2.8.1起新增了“图片切分”功能用于代替插件init中的“资源加载后操作”函数。
* 该项最核心的用法就是将4行4列共16块的大型行走图切成4行绑定给没有朝向关系的独立图块。
* 具体用法是指定一张宽高分别为mw和nh的图片限png格式按照w和h的单位宽高裁剪就会得到mn个小图。
* 每个小图会被重命名为您指定的前缀+数字从0起按正常的Z字形文本顺序逐行从左到右编号。
4. **额外素材:**`project/tilesets`中的文件名需要后缀只支持png
* 注册方法同上,最大的区别在于这个数组的顺序必须保持好。如果随意调换其中的顺序,或注销不在数组末尾的图片,就会导致地图上最终呈现的素材发生错位。因此,新勾选的图片总会被自动追加到数组的末尾。
* 比起常规素材,额外素材最大的好处有几点:
1. 图片没有数量限制。常规素材的总数量最多只允许不到10000个而额外素材每张图片上的图块数量最多允许3000个。
2. 查看和绘制更为方便。常规素材每个图块独占一行,每列为一帧,导致不方便观察,且用多个图块拼图必须逐图块绘制。额外素材都是静止的,所以每个图块只占一格,多个图块可以在准备素材时就直接以相邻的样子绘制在同一张图片上,绘制地图时直接从素材区拖动来批量框选,再在地图区单击成片绘制或拖动平铺。
3. 批量替换也更为方便。譬如您如果想制作形如“一场大战/天灾过后/多年以后,村庄/城镇化为了一片废墟”的效果,可以预先准备两张甚至更多张相同规格的额外素材图片,然后在适当的时候遍历某个/某些楼层的图块ID将以“X1”开头的图块统一替换为“X2”开头等。发布单机版游戏时您也可以提供多张相同规格的额外素材图片供玩家直接替换皮肤风格。
5. **使用动画:**`project/animates`文件夹中的文件名(不含后缀,请注意与`animates.png`相区分)。
* 要使用动画您可以使用“RM动画导出”工具从RPG Maker XP 1.03及其制作的游戏中导出动画也可以用动画编辑器修改已有的动画或用图片新建。但这些办法都只适用于Windows系统非Windows系统建议直接从我们的官网下载他人的作品取用其中的动画。
* 每个动画将其用到的各张图片直接以base64硬编码进同一个animate文件每个动画为一个animate文件。这样做的缺点是如果多个动画使用了相同的图片那么图片会被重复存储优点则是跨作品迁移动画更加方便。
* animate文件为json格式的文本文件文件末尾记录了动画的帧信息文件开头则记录了动画的伸缩比和所有音效第几帧用什么音效音调是多高
* 在导出动画时,会出现一个输入框并提示动画的第一音效文件名。不管该文件名是什么语言,请直接点击下一步。音效文件会被尝试自动复制,随后您只需手动注册该动画并在编辑器中预览,然后重新按帧绑定需要的音效。
* 可以使用如下动画相关的脚本对动画进行播放,或在事件中使用「播放动画」事件。
``` js
core.drawAnimate(name, x, y, alignWindow, callback);
// 播放一个动画name为不带后缀的动画文件名x和y为播放的格子坐标。
// alignWindow表示在大地图中该坐标是绝对坐标还是视野中的相对坐标填true表示相对坐标。
// 相对坐标模式下坐标应填写为小于13或15的自然数譬如对13×13样板来说
// 填两个6就会强制播放在视野中心。
// callback为动画播放完毕后(和音效无关)的回调函数,因为动画播放本身是异步的。
core.drawHeroAnimate(name, callback); // 和上面类似,但该动画会跟随勇士移动。
// 每场战斗后,都会根据怪物坐标尝试用前者播放普攻动画。若坐标不存在,
// (即天降强制战斗),则会尝试用后者播放。看上去就像勇士在打自己,请自行注意。
core.stopAnimate(id, doCallback); // 停止一段动画id为上面两个函数的返回值
// 第二个参数表示本次停止后是否依然执行前两个函数的回调
// 您可以用前两个函数回调自己,从而做到一个动画反复循环,
// 但务必要将每次的id记录在flags中以便于第三个函数去停止它。
```
6. **使用音乐:**`project/bgms`文件夹中的文件名需要后缀默认只支持wav、mp3、ogg、m4a、flac
* 如果玩家使用的是手机且没有连接WiFiiOS和部分浏览器无法获知网络状态将始终视为流量党那么背景音乐默认不会开启可以在标题画面点击右下角的圆形按钮来开启。
* V2.8起,这个圆形的音乐开关旁边还增加了一个放大镜按钮,您可以在标题画面连续点击它来获得一个最佳的屏幕缩放倍率。
* 发布到官网的作品还可以从<https://dl.h5mota.com/music/>远程加载背景音乐,您可以点击此链接试听和下载其他作品的背景音乐。
* 是否启用远程加载、以及启用时远程加载的根目录由main.js指定。因此从官网下载其他作品的离线版本后请先关闭远程加载功能才能正常加载本地注册的音乐。
* 使用`core.material.bgms`可以查看所有的背景音乐,如果需要(不存档的情况下)同时播放多个背景音乐并独立控制时刻、音量、速度、音调,请直接对它们使用`play()`和`pause()`方法并按照下图操作。
* `duration`表示总时长(秒,音效也适用),`currentTime`表示当前进度(秒,可读写)
* `volume`表示音量0到1`playbackRate`表示播放的倍速
* `preservesPitch`表示变速播放时是否不变调,`false`表示变调
* V2.8起背景音乐“变速不变调”和“变速且变调”、音效“变速且变调”正式作为样板API提供同时也有对应的事件指令这是样板在追逐RPG Maker上迈出的一大步且行且珍惜
* V2.8.1起动画的音效支持“变速且变调”同时编辑器中试听音频也支持了但bgm试听不能只变速不变调
![image](img/bgm.jpg)
7. **使用音效:**`project/sounds`文件夹中的文件名(格式要求和写法同上)。
* V2.8起音效的播放和动画一样会返回一个ID并且有回调。音效不能像bgm一样只变速不变调而是必须一起变。
* 可以指定ID来提前停止某个音效不指定ID则全部停止但不能像停止动画时一样取消回调。
* 对玩家来说音效的音量和bgm是绑定的无法分开调节。此外样板没有背景音效bgs、bgv的默认实现。如有需求请使用并行脚本或自我回调处理。
* 音乐和音效在使用多选框注册时都支持试听但此时不支持变速变调您可以看到它们的总时长和已播时长精确到秒从而指定音乐的开播秒数或配合使用“等待n毫秒”事件或并行脚本处理。
8. **使用字体:**project\fonts文件夹中的文件名只支持ttf格式不写后缀。不建议给在线游戏版本添加中文字体因为文件真的很大...
9. **文件别名:**如前所述,样板所有需要加载的文件名都必须全部是英文或数字。这一项允许你给文件取别名,别名可以使用任何语言的文字。
* V2.8起,此项改由事件编辑器编辑,同时样板新增了大量系统音效,可供灵活配置,务必善用!
10. **装备孔:**见“道具的装备属性”。
11. **标题音乐:**如题,请注意部分浏览器不会在刚打开某个页面时就播放音频,必须用户操作一下才行。
* V2.8起背景音乐支持“变速不变调”或“变速且变调”,但无法直接用于标题音乐。如有需求,请复写`core.control.showStartAnimate`函数。
12. **主样式:**一些css设置项单击“编辑”按钮可以看到具体含义和用法这里不再赘述横竖屏标题画面背景支持gif动图
13. **游戏名:**标题画面和网页选项卡上显示的名字,可以和官网别的作品重名。
14. **唯一英文标识符:** **必须修改,且不得和官网别的作品重名**。只能使用字母数字下划线,如果您不确定一个标识符是否已被别的作品使用,请输入`https://h5mota.com/games/xxx`如出现404就说明此名称未被使用。
15. **游戏版本:**当您的游戏发生版本更迭后旧版本的存档直接读取可能会出现bug.因此届时您可以修改此项来让旧存档无法直接读取,只能回放其录像。
16. **难度分歧:**单击“编辑”按钮进入事件编辑器,每个难度分为以下几项。
* 名称:标题界面单击“开始游戏”后出现的二级菜单中的文字。一般为该难度的最简要的介绍,如减伤比例、初始福利等。
* 简写:横屏状态栏左下角(竖屏右下角,也作为数字键切换按钮)和存读档界面缩略图上的文字,也会出现在在线游戏排行榜和旧版官网的作品卡片上。允许使用中文但请注意控制字数,用`core.status.hard`表示。
* 变量hard值即`core.status.hero.flags.hard`的值建议填自然数。若同一结局有多个难度有人通关则站点排行榜只统计其中此值最高的难度。请务必保证有一个难度的此值为0理由见下。
* 颜色:上述“简写”的颜色,用`core.status.hero.flags.__hardColor__`表示,默认为红色。详见“脚本编辑——更新状态栏”。
* 事件:此事件比下述的“开场剧情”更早执行,一般用来设置初始福利。
* 如果将难度分歧数组留空,那么标题界面单击“开始游戏”就会直接进入开场剧情。
* 请注意,游戏中的难度分歧最好只通过`core.status.hero.flags.hard`进行,这是因为除非开启了标题界面事件化,否则`core.status.hard`是可以由玩家在标题画面任意指定的而未定义过的难度的“变量hard”值都会为0这也是为什么上面要求务必要有一个难度指定0值的原因。
* 关于如何在标题画面任意指定难度和随机种子,参见[事件](event)一章的最底部。
### 初始勇士
1. **初始楼层、朝向和坐标:**如题,请注意初始楼层必须在上述的“楼层列表”中。初始坐标一般通过右击地图上的空地快速绑定,但您也可以手动在这里填写负数或超出初始楼层宽高的值。然后使用“无视地形移动勇士”或“跳跃勇士”等改变勇士位置的事件指令,做出“勇士从地图外进入”的演出效果。
* 如需根据难度分歧或用户选项等条件来改变它们,请在“开场剧情”中修改`core.firstData.floorId`和`core.firstData.hero.loc`
2. **行走图:**如题必须在“使用图片”中注册过。宽高必须为4的倍数宽度至少为128px即每帧32px。高度不限剧情对话中和状态栏中会尝试保持比例压缩到每帧32px宽。
* 在游戏中,勇士当前的行走图文件名用`core.status.hero.image`表示(只读)
* 游戏中如需更改行走图请使用事件指令,但请注意它并不会计入站点录像验证,而且更换新行走图后如果立即重绘勇士就会重置视角。
3. **帧动画:**勾选此项后,勇士在剧情对话中(朝上视为朝下)和原地不动时会循环播放四帧踏步动画,一般用于长翅膀的勇士。但是勾选后会导致视角随时锁定在勇士身上,也就是说大地图中操作视角的指令都会失效!
4. **勇士名:**如题,也会作为剧情对话中`\t[hero]`的默认标题。
5. **初始等级:**如果开启了自动进阶功能,请不要修改此项。
6. **生命上限、魔力上限、初始生命/魔力/攻防/护盾/金经:**如题。注意生命上限和金经需要在系统开关中勾选后才会启用,魔力上限填负数代表没有上限。
7. **初始装备、游戏变量:**建议留空事件中的变量初始时都会视为0脚本中也有办法做到
8. **永久道具、消耗道具、初始拥有装备个数:**点击“注释”按钮,按照提示进行修改。
9. **标题事件:**需要配合系统开关中勾选“标题开启事件化”来使用,可以在“开始游戏”、“读取存档”之余添加一些额外的功能。如成就系统、音像和回想鉴赏,但这并不是唯一的方法,请自行研究。
* 使用此项的后果主要是开局的若干操作也会计入录像,观感较差。
* 使用此项后,难度分歧会通过用户操作(如显示选择项)实现,因而成为了录像的一部分。
* 使用此项后,会导致因为无法像默认标题一样做成长方形,只能采用“黑色边框+状态栏黑底+隐藏状态栏”的妥协方法得到一个偏右的正方形画面。
* V2.8提供了一个插件,可以将这个正方形画面暂时居中,这是没有办法的办法。
10. **开场剧情:**会在难度分歧事件之后执行,可以在这里设置各种变量的初始值、穿上初始拥有的装备、隐藏勇士和一些初始不希望显示的图层块、追加跟随者等。
* 需要注意的是,此时`core.status.floorId`还没有值,因此无法进行相关操作,包括存档!
11. **全局商店:**详见“QTE与全局商店”。
12. **等级提升:**需要配合系统开关中勾选“等级”、“经验”和“升级”来使用,每个等级分为以下几项:
* 需求:刷新状态栏时,如果勇士当前等级是此等级的前一级,且经验值大于等于此需求,就会触发升级。
* 称号状态栏显示的等级默认是个正整数会尝试替换为这里的称号调用core.getLvName()函数),请注意控制字数。
* 是否扣除经验:如果勾选了此项,则触发升级时经验值会扣除需求值,这一项主要是为了应对一次升多级的情况。
* 事件:触发升级时执行的事件,如全面提升属性。事件中甚至可以降低等级,从而反复触发升同一级。
### 全局数值
![image](img/values_flagsb.jpg)
这个类型的数值会保存在core.values中可以直接在游戏中修改。
1. **血网伤害和中毒伤害:**如题,如果不想用常数,请修改“脚本编辑”的“阻激夹域伤害”和“每步后操作”。
2. **衰弱效果:**填小于1的正数代表扣减的比例否则为扣减的常数。扣减和恢复的原理和装备相同详见“脚本编辑——毒衰咒处理”。
3. **三种宝石和四种血瓶的值:**如题,此值为基础值。实际效果还需乘以楼层属性最下面的“宝石血瓶效果”(限即捡即用类,详见这七种道具的属性)。
4. **反击、破甲、净化比例:**如果反击、破甲、净化怪物没有指定atkValue、defValue和n就会用这三个值。请注意反击的总回合数是勇士的攻击回合数净化比例填1表示单纯无视护盾。
5. **仇恨增值:**每进行一场战斗core.status.hero.flags.hatred的增加量。如果不想用常数请修改“脚本编辑——战后脚本”。
6. **全局帧动画时间:**即怪物和NPC的振动速度建议改为300毫秒。
7. **竖状态栏自绘行数:**需要配合系统开关“开启自绘状态栏”使用建议改为4.
8. **楼层切换时间:**此项可被玩家更改单位为毫秒必须为0.1秒的0到10倍。
如有需求,可以把勇士移速(可被玩家更改)通过“配置表格”追加到这里,并修改`libs\core.js`对上述数值的初始化行为。
### 系统开关
这个类型的开关会保存在core.flags中只读请注意和core.status.hero.flags相区分。如需在游戏中修改请使用“设置系统开关”事件。
1. **状态栏显示项:**如题总个数请控制在12个以内否则竖屏可能塞不下。
* 这些项的图标都在`project\materials\icons.png`中。该文件夹下也提供了一个`icons_old.png`可供替换。
* “血限”、“金币”和“经验”必须勾选才会启用(指会处理生命溢出、金经会显示在手册、打怪也会掉落),
* 必须勾选“升级”才会启用自动进阶“升级扣除模式”如果不勾选就会同时显示下一级的需求NEXT和当前经验EXP如果勾选了的话否则会只显示两者的差依然用NEXT图标“扣除”这个词用的不太好。
* 如果同时勾选了“钥匙”和“绿钥”,则每种钥匙的数量都会被缩小尺寸显示,因此如非必要请不要勾选“绿钥”。
2. **楼传需在楼梯边:**如果勾选了此项则只有勇士在楼梯旁边包括六种portal旁边时才允许使用楼传。
* 请注意,此项是在楼传道具使用条件之外额外进行的判定,目的是给出不同的提示信息。
* 因此如果您要修改楼传的使用条件(指总的使用条件,具体能否飞到某层的条件则在“脚本编辑——楼层飞行”),则可能需要一并取消此勾选。
* V2.8起浏览地图界面按下G键或点击楼传图标则会尝试飞到当前正在浏览的楼层这大大方便了玩家。
3. **楼传开平面模式:**如果勾选了此项,则勇士在使用楼传飞往某层时会落在上次离开该层的位置(锚点)。
* 此项和上一项建议要么同时勾选要么同时不勾选同时勾选以后建议在“脚本编辑——flyTo楼层飞行”中禁止同层传送否则可能有意想不到的后果。
* 楼层切换过程中可以根据局部变量`fromLoad`和`isFlying`进行不同的处理(例如播放不同的音效),前者表示是否为读档,后者表示是否为楼传,建议由这两者导致的楼层切换一律不更新锚点。
4. **铁门不消耗钥匙:**如果勾选了此项,则铁门无需钥匙也能撞开。【不建议使用!】因为此项会导致撞铁门时不再自动存档,因此建议修改铁门的`doorInfo`属性将钥匙栏留空即可。
5. **首次道具进行提示:**勾选后,首次捡到非即捡即用类道具都会弹窗提示(晚于`afterGetItem`事件被执行)。
6. **状态栏装备按钮:**勾选后,状态栏的楼传按钮会变为装备栏按钮,但玩家仍然可以双击道具栏来呼出装备栏。
7. **加点:**勾选后怪物的加点值会在“脚本编辑——战后脚本”中作为参数`core.status.hero.flags.arg1`被传递给“公共事件——加点事件”。
8. **负伤:**勾选后,战斗结束时如果勇士的护盾没有完全被打破,则剩余的护盾值会加到其生命上。所以勾选此项后,护盾可以这样“间接”抵消掉仇恨伤害和固伤。
9. **夹击不超伤害值:**勾选此项后,夹击伤害将封顶至夹击怪的战损。同时被四只怪夹击时,取两个战损中较小的。
10. **二分临界:**我们知道打败怪物所需的回合数取决于勇士的攻击减去怪物的防御。这个值并非每增大1都能使回合数减少因而有了“临界”的说法即“再至少增加多少攻击力才能减少回合数”。然而当您修改“脚本编辑——战斗伤害信息”函数后攻击力的增加可能反而导致回合数也增加于是临界值计算出错。您可以勾选此开关来修复代价是游戏可能较卡请自行权衡。
* 目前样板的临界只有回合数法和二分法被真正采用,而循环法则只是保留了代码。如需启用,请修改`main.js`中“循环临界的分界”。
* V2.8.1起,未破防怪的临界表中,第一项将用负数表示刚刚破防时的伤害,后面的项正常表示破防后再加攻击的减伤。
11. **标题开启事件化:**勾选此项后标题画面将改为执行前述的“标题事件”请自行研究V2.8起建议配合“标题事件居中”插件。
12. **开启自绘状态栏:**勾选此项后,状态栏将改用“脚本编辑——自绘状态栏”来绘制,同时“脚本编辑——点击状态栏”也将启用,请自行研究。
13. **四个显伤:**略,玩家依然可以在设置菜单中开关之,其中“定点怪显”指的是手册中把受到额外影响而数值不同的同种怪物分开显示。
14. **允许轻按:**勾选此项后,玩家可以按下空格/大键盘数字7/双击勇士来拾取相邻的道具(优先面前)。
15. **允许穿透楼梯:**在狭窄的区域拦路放置一个可通行的“楼层转换”事件时(图块左下角出现绿色标记),玩家可能希望勇士能直接走过去。您可以逐个去设置其能否被这样走过,或者让其依据本勾选项。
* 值得注意的是,当玩家从允许穿透的楼梯向一个不可走的方向(如旁边是墙,或不勾选下一个开关时的致命领域)手动寻路时,可以停在楼梯上(进而再轻按拾取周围物品等)。不建议您利用这类极端情况去迫使玩家进行非常规操作,毕竟穿透楼梯和不能踏入致命领域的本意是方便玩家,不是么?
* 确切地说,即使旁边没有障碍物,录像意义下勇士也一定能停在任何可穿透的楼梯上。因此笔者再次强调,非常不建议利用这样的盲点戏弄玩家。
16. **允许将死领域:**“脚本编辑——阻激夹域伤害”会将地图中每个点的阻激夹域和血网伤害加总,如果不勾选此开关,则当勇士生命小于等于相邻空格子的总伤害(没有伤害则没关系)时,勇士无法走向该格子。
* 值得注意的是这种判定方式并没有考虑“走这一步后、结算该点伤害前”可能的加血或该点伤害变化因此如有必要可根据“脚本编辑——每步后操作”去修改core.canMoveHero()函数。
* 另外,此项的保护作用只会在“勇士可以自由行动”(`core.status.lockControl`为`false`)时生效,因此事件中(滑冰等场合)踩到致命领域仍然会死亡。
17. **允许瞬移:**若不勾选此开关,将全程禁用瞬移功能。一般只建议在需要的楼层逐层勾选禁止瞬移,或通过`flags.cannotMoveDirectly`来控制。
18. **录像折叠:**勾选此项后,将开启录像折叠功能。录像折叠将尽可能优化掉在一个地方无意义的行走,从而减少录像长度并提升播放观感。
* 当经过一段时间的行走、转向和瞬移后,若勇士的坐标、朝向和状态(步数除外)和之前某个时刻完全相同,那么将会直接删除这中间的录像记录。
* 当中毒状态、触发任何系统或自定义事件、图块脚本、楼层切换、受到阻激夹域伤害等等时,将清除录像折叠信息。
* 请注意:录像折叠将会优化步数,所以如果游戏和步数有直接关系(比如步数算分)请关闭录像折叠功能。另外,如果你的塔存在楼层并行脚本且对游戏数据有直接影响,也请关闭录像折叠功能。
19. **伤害禁用商店:**勾选此项后每当勇士踩到阻激夹域和血网并受到伤害时所有全局商店都将被禁用需要重新去启用譬如勇士去撞击该商店的实体NPC
20. **虚化前景层:**前景层会遮挡事件层,这对魔塔来说有时可能不太友好。勾选此项后,事件层有东西(如道具)时将虚化该格子的前景层,使得该东西以半透明状态可见。
* 出于布景需要某点事件层为自动元件或tileset时必须有“碰触脚本/碰触事件”,该点前景层才会虚化。
上面就是整个样板中的各个元件说明。通过这种方式,你就已经可以做出一部没有任何事件的塔了。
尝试着做一个两到三层的塔吧!
==========================================================================================
[继续阅读下一章:事件](event)

258
enemy.d.ts vendored Normal file
View File

@ -0,0 +1,258 @@
type PartialNumbericEnemyProperty =
| 'value'
| 'zone'
| 'repulse'
| 'laser'
| 'breakArmor'
| 'counterAttack'
| 'vampire'
| 'hpBuff'
| 'atkBuff'
| 'defBuff'
| 'range'
| 'haloRange'
| 'n'
| 'purify'
| 'atkValue'
| 'defValue'
| 'damage';
type BooleanEnemyProperty =
| 'zoneSquare'
| 'haloSquare'
| 'notBomb'
| 'add'
| 'haloAdd'
| 'specialAdd';
type DetailedEnemy<I extends EnemyIds = EnemyIds> = {
specialText: string[];
toShowSpecial: string[];
toShowColor: Color[];
specialColor: Color[];
damageColor: Color;
criticalDamage: number;
critical: number;
defDamage: number;
} & Enemy<I>;
type Enemy<I extends EnemyIds = EnemyIds> = {
/**
* id
*/
id: I;
/**
*
*/
name: string;
/**
*
*/
description: string;
/**
* IDnullID来替换该怪物原本的ID
*
*/
displayIdInBook: EnemyIds;
/**
*
*/
faceIds: Record<Dir, EnemyIds>;
/**
*
*/
beforeBattle: MotaEvent;
/**
*
*/
afterBattle: MotaEvent;
} & {
[P in PartialNumbericEnemyProperty]?: number;
} & {
[P in BooleanEnemyProperty]: boolean;
} & EnemyInfoBase;
/**
*
*/
type EnemySpecialDeclaration = [
id: number,
name: string | ((enemy: EnemySpecialBase) => string),
desc: string | ((enemy: EnemySpecialBase) => string),
color: Color,
extra?: number
];
interface DamageString {
/**
*
*/
damage: string;
/**
*
*/
color: Color;
}
interface EnemySpecialBase {
/**
*
*/
special: number[];
/**
*
*/
specialHalo?: number[];
}
interface EnemyInfoBase extends EnemySpecialBase {
/**
*
*/
hp: number;
/**
*
*/
atk: number;
/**
*
*/
def: number;
/**
*
*/
money: number;
/**
*
*/
exp: number;
/**
*
*/
point: number;
}
interface EnemyInfo extends EnemyInfoBase {
/**
*
*/
guards: [x: number, y: number, id: EnemyIds];
}
interface DamageInfo {
/**
*
*/
mon_hp: number;
/**
*
*/
mon_atk: number;
/**
*
*/
mon_def: number;
/**
*
*/
init_damage: number;
/**
*
*/
per_damage: number;
/**
*
*/
hero_per_damage: number;
/**
*
*/
turn: number;
/**
*
*/
damage: number;
}
interface BookEnemyInfo extends Enemy, EnemyInfo {
/**
*
*/
locs?: [x: number, y: number][];
/**
*
*/
name: string;
/**
*
*/
specialText: string[];
/**
*
*/
specialColor: Color[];
/**
*
*/
damage: number;
/**
*
*/
critical: number;
/**
*
*/
criticalDamage: number;
/**
* ratio防减伤
*/
defDamage: number;
}
/**
*
*/
interface Enemys {
/**
*
*/
readonly enemys: {
[P in EnemyIds]: Enemy<P>;
};
/**
*
*/
getEnemys(): {
[P in EnemyIds]: Enemy<P>;
};
}
declare const enemys: new () => Enemys;

232
enemys.js Normal file
View File

@ -0,0 +1,232 @@
var enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80 =
{
"greenSlime": {"name":"史莱姆","hp":4,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redSlime": {"name":"红头怪","hp":4,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"value":10,"faceIds":{"down":"E434","left":"redSlime","right":"blackSlime","up":"slimelord"}},
"blackSlime": {"name":"青头怪","hp":30,"atk":2,"def":1,"money":0,"exp":0,"point":0,"special":0,"faceIds":{"down":"E434","left":"redSlime","right":"blackSlime","up":"slimelord"}},
"slimelord": {"name":"怪王","hp":100,"atk":120,"def":0,"money":10,"exp":0,"point":0,"special":[],"faceIds":{"down":"E434","left":"redSlime","right":"blackSlime","up":"slimelord"}},
"bat": {"name":"小蝙蝠","hp":100,"atk":120,"def":0,"money":2,"exp":0,"point":0,"special":0},
"bigBat": {"name":"大蝙蝠","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redBat": {"name":"红蝙蝠","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"vampire": {"name":"冥灵魔王","hp":888,"atk":888,"def":888,"money":888,"exp":888,"point":0,"special":0,"n":8},
"skeleton": {"name":"骷髅人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"skeletonCaptain": {"name":"骷髅队长","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"zombie": {"name":"兽人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"zombieKnight": {"name":"兽人武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"rock": {"name":"石头人","hp":50,"atk":50,"def":0,"money":3,"exp":0,"point":0,"special":3},
"bluePriest": {"name":"初级法师","hp":100,"atk":120,"def":0,"money":3,"exp":0,"point":1,"special":[9]},
"redPriest": {"name":"高级法师","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"brownWizard": {"name":"初级巫师","hp":100,"atk":120,"def":0,"money":16,"exp":0,"point":0,"special":15,"value":100,"range":2},
"redWizard": {"name":"高级巫师","hp":1000,"atk":1200,"def":0,"money":160,"exp":0,"point":0,"special":15,"value":200,"zoneSquare":true},
"swordsman": {"name":"双手剑士","hp":100,"atk":120,"def":0,"money":6,"exp":0,"point":0,"special":4},
"soldier": {"name":"冥战士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"yellowKnight": {"name":"金骑士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redKnight": {"name":"红骑士","hp":500,"atk":200,"def":50,"money":0,"exp":0,"point":0,"special":[7]},
"darkKnight": {"name":"黑骑士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"blueKnight": {"name":"蓝骑士","hp":100,"atk":120,"def":0,"money":9,"exp":0,"point":0,"special":8},
"goldSlime": {"name":"黄头怪","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"poisonSkeleton": {"name":"紫骷髅","hp":50,"atk":60,"def":70,"money":80,"exp":0,"point":0,"special":13},
"poisonBat": {"name":"紫蝙蝠","hp":100,"atk":120,"def":0,"money":14,"exp":0,"point":0,"special":13},
"skeletonPriest": {"name":"骷髅法师","hp":100,"atk":100,"def":0,"money":0,"exp":0,"point":0,"special":18,"value":20},
"skeletonKing": {"name":"骷髅王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"evilHero": {"name":"迷失勇者","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"demonPriest": {"name":"魔神法师","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"goldHornSlime": {"name":"金角怪","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"silverSlime": {"name":"银头怪","hp":100,"atk":120,"def":0,"money":15,"exp":0,"point":0,"special":14},
"whiteHornSlime": {"name":"尖角怪","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redSwordsman": {"name":"剑王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"poisonZombie": {"name":"绿兽人","hp":100,"atk":120,"def":0,"money":13,"exp":0,"point":0,"special":[12]},
"octopus": {"name":"血影","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0,"bigImage":"dragon_1.png"},
"princessEnemy": {"name":"假公主","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"silverSlimelord": {"name":"银怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"goldSlimelord": {"name":"金怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"skeletonWarrior": {"name":"骷髅士兵","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"whiteSlimeman": {"name":"水银战士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"slimeman": {"name":"影子战士","hp":100,"atk":0,"def":0,"money":11,"exp":0,"point":0,"special":[9],"atkValue":2,"defValue":3},
"yellowGateKeeper": {"name":"初级卫兵","hp":100,"atk":120,"def":0,"money":10,"exp":0,"point":0,"special":0},
"blueGateKeeper": {"name":"中级卫兵","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redGateKeeper": {"name":"高级卫兵","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"magicMaster": {"name":"黑暗大法师","hp":100,"atk":120,"def":0,"money":12,"exp":0,"point":0,"special":11,"value":0.3333333333333333,"add":true,"notBomb":true},
"devilWarrior": {"name":"魔神武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"fairyEnemy": {"name":"仙子","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"dragon": {"name":"魔龙","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0,"bigImage":"dragon_0.png"},
"skeletonKnight": {"name":"骷髅武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"skeletonPresbyter": {"name":"骷髅巫师","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"ironRock": {"name":"铁面人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"grayRock": {"name":"灰色石头人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"yellowPriest": {"name":"中级法师","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"evilPrincess": {"name":"痛苦魔女","hp":1000,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[10]},
"blademaster": {"name":"剑圣","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"evilFairy": {"name":"黑暗仙子","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"greenKnight": {"name":"强盾骑士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"bowman": {"name":"初级弓兵","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"watcherSlime": {"name":"邪眼怪","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"devilKnight": {"name":"恶灵骑士","hp":150,"atk":100,"def":50,"money":0,"exp":0,"point":0,"special":[1,5,7,8]},
"grayPriest": {"name":"混沌法师","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"greenGateKeeper": {"name":"卫兵队长","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"ghostSoldier": {"name":"冥队长","hp":200,"atk":100,"def":50,"money":0,"exp":0,"point":0,"special":0},
"frostBat": {"name":"寒蝙蝠","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"blackKing": {"name":"黑衣魔王","hp":1000,"atk":500,"def":0,"money":1000,"exp":1000,"point":0,"special":0,"notBomb":true},
"yellowKing": {"name":"黄衣魔王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"greenKing": {"name":"青衣武士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"redKing": {"name":"红衣魔王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"blueKing": {"name":"白衣武士","hp":100,"atk":120,"def":0,"money":17,"exp":0,"point":0,"special":16},
"keiskeiFairy": {"name":"铃兰花妖","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"tulipFairy": {"name":"郁金香花妖","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"purpleBowman": {"name":"高级弓兵","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0},
"E434": {"name":"史莱姆","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E434","left":"redSlime","right":"blackSlime","up":"slimelord"}},
"E435": {"name":"红史莱姆","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E436": {"name":"僵尸","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E437": {"name":"尸壳","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E438": {"name":"溺尸","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1},
"E439": {"name":"骷髅","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E440": {"name":"苦力怕","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E441": {"name":"闪电苦力怕","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E442": {"name":"蜘蛛","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E443": {"name":"末影人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E444": {"name":"狼","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E445": {"name":"守卫者","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E446": {"name":"远古守卫者","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E447": {"name":"幻翼","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E448": {"name":"掠夺者","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E449": {"name":"唤魔者","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E450": {"name":"恼鬼","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E451": {"name":"卫道士","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E452": {"name":"劫掠兽","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E453": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E454": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E455": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E456": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E457": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E458": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E459": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E460": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E461": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E462": {"name":"新敌人","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]},
"E987": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E988": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E989": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E990": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E991": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E992": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E993": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E994": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E995": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E996": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E997": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E998": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E999": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1000": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1001": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1002": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1003": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1004": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1005": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1006": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1007": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1008": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1009": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1010": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1011": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1012": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1013": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1014": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1015": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1016": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1017": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1018": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1019": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1020": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1021": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1022": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1023": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1024": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1025": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1026": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1027": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1028": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1029": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1030": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1031": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1032": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1033": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1034": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1035": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1036": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1037": {"name":"怪","hp":1,"atk":1,"def":0,"as":1000,"ms":1000,"ar":1,"dr":3,"money":0,"exp":0,"point":0,"special":[],"type":0},
"E1": {"name":"史莱姆","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0,"faceIds":{"down":"E1","left":"E2","right":"E3","up":"E4"},"type":0},
"E2": {"name":"史莱姆","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0,"faceIds":{"down":"E1","left":"E2","right":"E3","up":"E4"},"type":0},
"E3": {"name":"铁守卫","hp":50,"atk":50,"def":50,"money":0,"exp":0,"point":0,"special":[18],"value":20,"faceIds":{"down":"E1","left":"E2","right":"E3","up":"E4"},"type":0},
"E4": {"name":"邪恶蝙蝠","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":0,"faceIds":{"down":"E1","left":"E2","right":"E3","up":"E4"},"type":0},
"E5": {"name":"红史莱姆","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"faceIds":{"down":"E5","left":"E6","right":"E7","up":"E8"},"bigImage":null,"type":0},
"E6": {"name":"僵尸","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"faceIds":{"down":"E5","left":"E6","right":"E7","up":"E8"},"type":0},
"E7": {"name":"僵尸","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"faceIds":{"down":"E5","left":"E6","right":"E7","up":"E8"},"type":0},
"E8": {"name":"僵尸","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[],"faceIds":{"down":"E5","left":"E6","right":"E7","up":"E8"},"type":0},
"E9": {"name":"僵尸","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E9","left":"E10","right":"E11","up":"E12"}},
"E10": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E9","left":"E10","right":"E11","up":"E12"}},
"E11": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E9","left":"E10","right":"E11","up":"E12"}},
"E12": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E9","left":"E10","right":"E11","up":"E12"}},
"E13": {"name":"尸壳","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E13","left":"E14","right":"E15","up":"E16"}},
"E14": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E13","left":"E14","right":"E15","up":"E16"}},
"E15": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E13","left":"E14","right":"E15","up":"E16"}},
"E16": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E13","left":"E14","right":"E15","up":"E16"}},
"E17": {"name":"溺尸","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E17","left":"E18","right":"E19","up":"E20"}},
"E18": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E17","left":"E18","right":"E19","up":"E20"}},
"E19": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E17","left":"E18","right":"E19","up":"E20"}},
"E20": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E17","left":"E18","right":"E19","up":"E20"}},
"E21": {"name":"骷髅","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E21","left":"E22","right":"E23","up":"E24"}},
"E22": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E21","left":"E22","right":"E23","up":"E24"}},
"E23": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E21","left":"E22","right":"E23","up":"E24"}},
"E24": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E21","left":"E22","right":"E23","up":"E24"}},
"E25": {"name":"苦力怕","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E25","left":"E26","right":"E27","up":"E28"}},
"E26": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E25","left":"E26","right":"E27","up":"E28"}},
"E27": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E25","left":"E26","right":"E27","up":"E28"}},
"E28": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E25","left":"E26","right":"E27","up":"E28"}},
"E29": {"name":"闪电苦力怕","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E29","left":"E30","right":"E31","up":"E32"}},
"E30": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E29","left":"E30","right":"E31","up":"E32"}},
"E31": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E29","left":"E30","right":"E31","up":"E32"}},
"E32": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E29","left":"E30","right":"E31","up":"E32"}},
"E33": {"name":"蜘蛛","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E33","left":"E34","right":"E35","up":"E36"}},
"E34": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E33","left":"E34","right":"E35","up":"E36"}},
"E35": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E33","left":"E34","right":"E35","up":"E36"}},
"E36": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E33","left":"E34","right":"E35","up":"E36"}},
"E37": {"name":"末影人","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E37","left":"E38","right":"E39","up":"E40"}},
"E38": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E37","left":"E38","right":"E39","up":"E40"}},
"E39": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E37","left":"E38","right":"E39","up":"E40"}},
"E40": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E37","left":"E38","right":"E39","up":"E40"}},
"E41": {"name":"恶狼","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E41","left":"E42","right":"E43","up":"E44"}},
"E42": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E41","left":"E42","right":"E43","up":"E44"}},
"E43": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E41","left":"E42","right":"E43","up":"E44"}},
"E44": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E41","left":"E42","right":"E43","up":"E44"}},
"E45": {"name":"守卫者","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E45","left":"E46","right":"E47","up":"E48"}},
"E46": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E45","left":"E46","right":"E47","up":"E48"}},
"E47": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E45","left":"E46","right":"E47","up":"E48"}},
"E48": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E45","left":"E46","right":"E47","up":"E48"}},
"E49": {"name":"远古守卫者","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E49","left":"E50","right":"E51","up":"E52"}},
"E50": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E49","left":"E50","right":"E51","up":"E52"}},
"E51": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E49","left":"E50","right":"E51","up":"E52"}},
"E52": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":1,"faceIds":{"down":"E49","left":"E50","right":"E51","up":"E52"}},
"E53": {"name":"幻翼","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E53","left":"E54","right":"E55","up":"E56"}},
"E54": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E53","left":"E54","right":"E55","up":"E56"}},
"E55": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E53","left":"E54","right":"E55","up":"E56"}},
"E56": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E53","left":"E54","right":"E55","up":"E56"}},
"E57": {"name":"掠夺者","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E57","left":"E58","right":"E59","up":"E60"}},
"E58": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E57","left":"E58","right":"E59","up":"E60"}},
"E59": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E57","left":"E58","right":"E59","up":"E60"}},
"E60": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E57","left":"E58","right":"E59","up":"E60"}},
"E61": {"name":"唤魔者","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E61","left":"E62","right":"E63","up":"E64"}},
"E62": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E61","left":"E62","right":"E63","up":"E64"}},
"E63": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E61","left":"E62","right":"E63","up":"E64"}},
"E64": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E61","left":"E62","right":"E63","up":"E64"}},
"E65": {"name":"恼鬼","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E65","left":"E66","right":"E67","up":"E68"}},
"E66": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E65","left":"E66","right":"E67","up":"E68"}},
"E67": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E65","left":"E66","right":"E67","up":"E68"}},
"E68": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":2,"faceIds":{"down":"E65","left":"E66","right":"E67","up":"E68"}},
"E69": {"name":"卫道士","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E69","left":"E70","right":"E71","up":"E72"}},
"E70": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E69","left":"E70","right":"E71","up":"E72"}},
"E71": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E69","left":"E70","right":"E71","up":"E72"}},
"E72": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E69","left":"E70","right":"E71","up":"E72"}},
"E73": {"name":"劫掠兽","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E73","left":"E74","right":"E75","up":"E76"}},
"E74": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E73","left":"E74","right":"E75","up":"E76"}},
"E75": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E73","left":"E74","right":"E75","up":"E76"}},
"E76": {"name":"怪","hp":1,"atk":1,"def":0,"money":0,"exp":0,"point":0,"special":[],"type":0,"faceIds":{"down":"E73","left":"E74","right":"E75","up":"E76"}}
}

762
event.d.ts vendored Normal file
View File

@ -0,0 +1,762 @@
/**
*
*/
type SystemEventFunc = (data: any, callback: (...params: any[]) => any) => void;
/**
*
*/
type EventFunc = (data: any, x?: number, y?: number, prefix?: string) => void;
/**
*
*/
interface Events extends EventData {
/**
*
*/
eventdata: EventData;
/**
*
*/
commonEvent: Record<EventDeclaration, MotaEvent>;
/**
*
*/
systemEvent: Record<string, SystemEventFunc>;
/**
*
*/
actions: Record<string, EventFunc>;
/**
*
* @example core.startGame('咸鱼乱撞', 0, ''); // 开始一局咸鱼乱撞难度的新游戏随机种子为0
* @param hard
* @param seed
* @param route base64压缩后的录像
* @param callback
*/
startGame(
hard: string,
seed?: number,
route?: string,
callback?: () => void
): void;
/**
*
* @example core.gameOver(); // 游戏失败
* @param ending
* @param fromReplay true表示在播放录像
* @param norank true表示不计入榜单
*/
gameOver(ending?: string, fromReplay?: boolean, norank?: boolean): void;
/**
*
*/
restart(): void;
/**
*
*/
confirmRestart(): void;
/**
*
* @param type
* @param func (data,callback)
*/
registerSystemEvent(type: string, func: SystemEventFunc): void;
/**
*
* @param type
*/
unregisterSystemEvent(type: string): void;
/**
*
* @param type
* @param data
* @param callback
*/
doSystemEvent(
type: string,
data: any,
callback?: (...params: any[]) => any
): void;
/**
* (x,y)script属性
* @param x
* @param y
* @param callback
*/
trigger(x: number, y: number, callback?: () => void): void;
/**
*
* @example core.openDoor(0, 0, true, core.jumpHero); // 打开左上角的门,需要钥匙,然后主角原地跳跃半秒
* @param x
* @param y
* @param needKey true表示需要钥匙
* @param callback
*/
openDoor(
x: number,
y: number,
needKey?: boolean,
callback?: () => void
): void;
/**
*
* @example core.getItem('book'); // 获得敌人手册并提示
* @param id id
* @param num 1
* @param x
* @param y
* @param callback
*/
getItem(
id: AllIdsOf<'items'>,
num?: number,
x?: number,
y?: number,
callback?: () => void
): void;
/**
*
* @param noRoute true则不计入录像
*/
getNextItem(noRoute?: boolean): void;
/**
*
* @example core.changeFloor('MT0'); // 传送到主塔0层主角坐标和朝向不变黑屏时间取用户设置值
* @param floorId id':before'':after'
* @param stair ':now',':symmetry',':symmetry_x',':symmetry_y'id'downFloor''upFloor'
* @param heroLoc
* @param time
* @param callback
*/
changeFloor(
floorId: FloorIds,
stair?: FloorChangeStair | AllIds,
heroLoc?: Partial<Loc>,
time?: number,
callback?: () => void
): void;
/**
*
* @param floorId id
*/
hasVisitedFloor(floorId?: FloorIds): boolean;
/**
*
* @param floorId id
*/
visitFloor(floorId?: FloorIds): void;
/**
*
* @param data
*/
pushBox(data?: Block): void;
/**
*
*/
onSki(number?: number): boolean;
/**
*
* @param type
* @param func (data, x, y, prefix)
* data为事件内容x和y为当前点坐标nullprefix为当前点前缀
*/
registerEvent(type: string, func: EventFunc): void;
/**
*
* @param type
*/
unregisterEvent(type: string): void;
/**
*
* @param data
* @param x
* @param y
* @param prefix
*/
doEvent(data: any, x?: number, y?: number, prefix?: string): void;
/**
*
* @param list
* @param x
* @param y
* @param callback
*/
setEvents(
list: MotaEvent,
x?: number,
y?: number,
callback?: () => void
): void;
/**
*
* @param list
* @param x
* @param y
* @param callback
*/
startEvents(
list?: MotaEvent,
x?: number,
y?: number,
callback?: () => void
): void;
/**
*
* @example
* // 事件中的原生脚本,配合勾选“不自动执行下一个事件”来达到此改变色调只持续到下次场景切换的效果
* core.setCurtain([0,0,0,1], undefined, null, core.doAction);
*/
doAction(): void;
/**
* core.insertCommonEvent
* @example core.insertAction('一段文字'); // 插入一个显示文章
* @param action
* @param x
* @param y
* @param callback
* @param addToLast true表示插入到末尾
*/
insertAction(
action: MotaEvent | MotaAction,
x?: number,
y?: number,
callback?: () => void,
addToLast?: boolean
): void;
/**
*
* @example core.insertCommonEvent('加点事件', [3]);
* @param name
* @param args flag:arg1, flag:arg2, ...
* @param x
* @param y
* @param callback
* @param addToLast true表示插入到末尾
*/
insertCommonEvent(
name: EventDeclaration,
args?: any[],
x?: number,
y?: number,
callback?: () => void,
addToLast?: boolean
): void;
/**
*
* @param name
*/
getCommonEvent(name: EventDeclaration): any;
/**
*
* @param data
*/
recoverEvents(data?: any): boolean;
/**
*
*/
checkAutoEvents(): void;
/**
*
* @param symbol
* @param value
*/
autoEventExecuting(symbol?: string, value?: any): boolean;
/**
*
* @param symbol
* @param value
*/
autoEventExecuted(symbol?: string, value?: any): boolean;
/**
*
*/
pushEventLoc(x: number, y: number, floorId?: FloorIds): void;
/**
*
*/
popEventLoc(): void;
/**
*
* @param data
*/
precompile(data?: any): any;
/**
*
* @param fromUserAction
*/
openBook(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
useFly(fromUserAction?: boolean): void;
/** 点击装备栏时的打开操作 */
openEquipbox(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
openToolbox(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
openQuickShop(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
openKeyBoard(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
save(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
load(fromUserAction?: boolean): void;
/**
*
* @param fromUserAction
*/
openSettings(fromUserAction?: boolean): void;
/**
*
*/
hasAsync(): boolean;
/**
*
*/
stopAsync(): void;
/**
*
*/
hasAsyncAnimate(): boolean;
/**
*
* @param name 4x4的行走图名称
*/
follow(name: ImageIds | NameMapIn<ImageIds>): void;
/**
*
* @param name
*/
unfollow(name?: ImageIds | NameMapIn<ImageIds>): void;
/**
*
* @param name
* @param operator
* @param value
* @param prefix
*/
setValue(
name: `${EventValuePreffix}:${string}`,
operator: MotaOperator,
value: number,
prefix?: string
): void;
/**
* @deprecated
*
* @example core.setEnemy('greenSlime', 'def', 0); // 把绿头怪的防御设为0
* @param id id
* @param name
* @param value
* @param operator
* @param prefix
* @param norefresh
*/
setEnemy<K extends keyof Enemy>(
id: AllIdsOf<'enemys' | 'enemy48'>,
name: K,
value: Enemy[K],
operator?: MotaOperator,
prefix?: string,
norefresh?: boolean
): void;
/**
* @deprecated
*
* @param x
* @param y
* @param floorId id
* @param name
* @param value
* @param operator
* @param prefix
* @param norefresh
*/
setEnemyOnPoint<K extends keyof Enemy>(
x: number,
y: number,
floorId: FloorIds,
name: K,
value: Enemy[K],
operator?: MotaOperator,
prefix?: string,
norefresh?: boolean
): void;
/**
* @deprecated
*
* @param x
* @param y
* @param floorId id
* @param norefresh
*/
resetEnemyOnPoint(
x: number,
y: number,
floorId?: FloorIds,
norefresh?: boolean
): void;
/**
* @deprecated
*
* @param fromX
* @param fromY
* @param toX
* @param toY
* @param floorId id
* @param norefresh
*/
moveEnemyOnPoint(
fromX: number,
fromY: number,
toX: number,
toY: number,
floorId?: FloorIds,
norefresh?: boolean
): void;
/**
*
* @example core.setFloorInfo('ratio', 2, 'MT0'); // 把主塔0层的血瓶和宝石变为双倍效果
* @param name
* @param values
* @param floorId id
* @param prefix
*/
setFloorInfo<K extends keyof Floor>(
name: K,
values?: Floor[K],
floorId?: FloorIds,
prefix?: string
): void;
/**
*
* @param name
* @param value
*/
setGlobalAttribute<K extends keyof GlobalAttribute>(
name: K,
value: GlobalAttribute[K]
): void;
/**
*
* @example core.setGlobalFlag('steelDoorWithoutKey', true); // 使全塔的所有铁门都不再需要钥匙就能打开
* @param name
* @param value !core.flags[name]
*/
setGlobalFlag<K extends keyof CoreFlags>(
name: K,
value: CoreFlags[K]
): void;
/**
*
* @param name
* @param value
*/
setNameMap(name: string, value?: SourceIds): void;
/**
*
*/
setTextAttribute(data: Partial<TextAttribute>): void;
/**
*
* @param code
* @param loc
* @param relative
* @param moveMode
* @param time
* @param callback
*/
moveTextBox(
code: number,
loc: LocArr,
relative?: boolean,
moveMode?: EaseMode,
time?: number,
callback?: () => void
): void;
/**
*
* @param code
* @param callback
*/
clearTextBox(code: number, callback: () => void): void;
/**
*
* @example core.closeDoor(0, 0, 'yellowWall', core.jumpHero); // 在左上角关掉一堵黄墙,然后主角原地跳跃半秒
* @param x
* @param y
* @param id id
* @param callback
*/
closeDoor(
x: number,
y: number,
id: AllIdsOf<Exclude<Cls, 'enemys' | 'enemy48'>>,
callback?: () => void
): void;
/**
*
* @example
* // 裁剪winskin.png的最左边128×128px放大到铺满整个视野1秒内淡入到50%透明编号为1
* core.showImage(1, core.material.images.images['winskin.png'], [0,0,128,128], [0,0,416,416], 0.5, 1000);
* @param code 50100z值z值为125UI层为140
* @param image
* @param sloc
* @param loc
* @param opacityVal 11
* @param time 0
* @param callback
*/
showImage(
code: number,
image: ImageIds | NameMapIn<ImageIds> | ImageSource,
sloc?: [number, number, number, number],
loc?: [number, number, number?, number?],
opacityVal?: number,
time?: number,
callback?: () => void
): void;
/**
*
* @example core.hideImage(1, 1000, core.jumpHero); // 1秒内淡出1号图片然后主角原地跳跃半秒
* @param code
* @param time
* @param callback
*/
hideImage(code: number, time?: number, callback?: () => void): void;
/**
* /
* @example core.moveImage(1, null, 0.5); // 1秒内把1号图片变为50%透明
* @param code
* @param to
* @param opacityVal
* @param moveMode
* @param time 1
* @param callback
*/
moveImage(
code: number,
to?: LocArr,
opacityVal?: number,
moveMode?: string,
time?: number,
callback?: () => void
): void;
/**
*
* @param code
* @param center
* @param angle
* @param moveMode
* @param time 1
* @param callback
*/
rotateImage(
code: number,
center?: LocArr,
angle?: number,
moveMode?: EaseMode,
time?: number,
callback?: () => void
): void;
/**
*
* @param code
* @param center
* @param scale
* @param moveMode
* @param time 1
* @param callback
*/
scaleImage(
code: number,
center?: LocArr,
scale?: number,
moveMode?: string,
time?: number,
callback?: () => void
): void;
/**
*
* @example core.showGif(); // 擦除所有动图
* @param name
* @param x
* @param y
*/
showGif(
name?:
| Extract<ImageIds, EndsWith<'.gif'>>
| NameMapIn<Extract<ImageIds, EndsWith<'.gif'>>>,
x?: number,
y?: number
): void;
/**
* bgm的音量
* @example core.setVolume(0, 100, core.jumpHero); // 0.1秒内淡出bgm然后主角原地跳跃半秒
* @param value 01
* @param time 1000
* @param callback
*/
setVolume(value: number, time?: number, callback?: () => void): void;
/**
*
* @example core.vibrate(); // 视野左右抖动1秒
* @param direction
* @param time
* @param speed
* @param power
* @param callback
*/
vibrate(
direction?: string,
time?: number,
speed?: number,
power?: number,
callback?: () => void
): void;
/**
* 退
* @example core.eventMoveHero(['forward'], 125, core.jumpHero); // 主角强制前进一步用时1/8秒然后主角原地跳跃半秒
* @param steps 退
* @param time 00.1
* @param callback
*/
eventMoveHero(steps: Step[], time?: number, callback?: () => void): void;
/**
* ex和ey为目标点的坐标null表示原地跳跃time为总跳跃时间
* @example core.jumpHero(); // 主角原地跳跃半秒
* @param ex
* @param ey
* @param time
* @param callback
*/
jumpHero(
ex?: number,
ey?: number,
time?: number,
callback?: () => void
): void;
/**
*
* @example core.setHeroIcon('npc48.png', true); // 把主角从阳光变成样板0层左下角的小姐姐但不立即刷新
* @param name core.status.hero.image
* @param noDraw true表示不立即刷新
*/
setHeroIcon(name: string, noDraw?: boolean): void;
/** 检查升级事件 */
checkLvUp(): void;
/**
* 使
* @example core.tryUseItem('pickaxe'); // 尝试使用破墙镐
* @param itemId id
*/
tryUseItem(
itemId: ItemIdOf<'tools' | 'constants'>,
noRoute?: boolean,
callback?: () => void
): void;
_sys_battle(data: Block, callback?: () => void): void;
_action_battle(data: any, x?: number, y?: number, prefix?: any): void;
__action_getLoc(data: any, x?: number, y?: number, prefix?: any): any;
_changeFloor_beforeChange(info: any, callback: () => void): void;
}
declare const events: new () => Events;

329
event.md Normal file
View File

@ -0,0 +1,329 @@
# 事件
?> 在这一节中,让我们来了解事件系统的基本原理
## 事件编辑地图选点快捷键X
![image](img/events.jpg)
样板所有的事件都是依靠“触发器”完成的。例如勇士踩到绑定好的楼梯可以触发changeFloor碰到门可以触发openDoor碰到怪物可以触发battle踩到/轻拾道具可以触发getItem推到箱子可以触发pushBox走上冰面背景层可以触发ski.
这些触发器都是系统自带的称为系统事件已经存在处理机制不需要我们操心。我们真正所需要关心的其实只是自定义的事件如NPC
地图选点快捷键X一共有八项在地图中以图块左下角用八色小方块标记。
* **红色:**普通事件,也包括该点的一些特效。
* **橙色:**自动事件,可以设置为“仅执行一次”、“仅在本层检测”。
* **暗绿色:**战前事件,强制战斗时不触发。
* **黄色:**战后事件。
* **亮绿色:**楼层转换,可以设置为“可穿透”。
* **青色:**获得道具后事件,可以设置为“轻按时不触发”。
* **靛色:**不是事件,为勇士站在该点不能朝哪些方向走。
* **紫(粉)色:**开门后事件。
此外还有一些事件不在地图上,如“首次/每次到达”(这两个的触发原理,详见“脚本编辑——切换楼层后”)、“道具效果”、“碰触事件”、“(批量)战前/战后事件”、“公共事件”和全塔属性中那五个事件。
### 事件的机制
地图上的所有事件都存在两种状态:启用和禁用。
* 启用状态下,该事件才处于可见状态,可被触发、交互与处理。
* 禁用状态下,该事件几乎相当于不存在(默认可以被阻击怪和箱子推上去),不可见、不可被触发、不可交互,只能通过“插入事件”指令远程调用。
所有事件默认情况下初始都是启用的,只有普通事件可以通过不勾选“启用”来初始隐藏。
在事件列表中使用“显示事件”和“隐藏事件”可以将一个禁用事件给启用,或将一个启用事件给禁用,甚至直接从地图中删除。
**【重要警告】:**
1. 打怪开门捡道具后,如果有对应的坐标,则该点的事件会被从地图中删除(重生怪除外,它只会被暂时隐藏)。然后会尝试执行该点的战后事件、开门后事件或(拾获)道具后事件(您可以勾选“轻按时不触发”)。如果执行,则“当前点坐标”(`core.status.event.data`的坐标)也会变为该点。
* 所以这三个XX后事件其实是有可能被多次触发或意外触发的如打死或炸掉某个有战后事件的怪推了个阻击怪过去打死请自行注意。
2. “移动事件”(如阻击和捕捉)和“跳跃事件”(如支援)会将起点处的事件从地图中删除,如果不勾选“不消失”(如阻击),则会在终点处“转变图块”并“显示事件”。但事件的内容不会被跟着移动到终点,推箱子同理,但在终点只转变图块而不予显示。
### 楼梯、传送门事件
![image](img/changefloor.jpg)
当您在地图上绘制楼梯、或绘制四种三帧箭头并右击绑定后就创建了一个“楼层转换”事件您可以在事件编辑器右侧看到一行代码json请注意对照。
此事件与`core.changeFloor(floorId, stair, heroLoc, time, callback)`函数相对应,只是多了一项“穿透性”。
每个这样的事件都是上图的写法,下面逐一讲解其各项的含义和用法:
1. **目标楼层floorId**如题如果选择了“楼层ID”就需要在第二个框中输入目标楼层的ID会自动提示补全也可以双击整个紫色块从地图选点。如果选择了其他三种则json代码中`:before`、`:next`、`:now`分别表示“前一楼”、“后一楼”(顺序由“全塔属性——楼层列表”指定,上下楼器同理)和“当前楼”,函数中也是一样的写法。
2. **目标位置stair**如果选择了“坐标”则需要在后面两个框里输入(同样可以地图选点,在函数中则将`stair`填`null`并填写`heroLoc`),也可以选择下拉框中的其他几项(在函数中则将`heroLoc`填`null`),如保持不变(`:now`)、中心对称(`:symmetry`)、左右对称(`:symmetry_x`)、上下对称(`:symmetry_y`或任何一个图块ID.
* 填写三种对称位置时请注意,坐标计算以【当前楼层的勇士】位置(对不可踏入的楼梯来说,这个位置与楼梯相邻而不重合)而不是目标楼层或该楼梯为准,注意防止勇士出界。
* 填写图块ID时请注意“上下楼梯”和“楼传落点”提供在了下拉框中实际传送时会优先尝试取用目标层的“楼层属性——上下楼点”。其次尝试像其他ID必须写在右侧的json区再点击解析一样从目标楼层搜索因此请确保该图块在目标楼层中存在且唯一。
3. **朝向:**有八种写法,可以直接填写改变后的朝向(`up, down, left, right`),也可以填写转身的角度(`:left, :right, :back`)或不变。
4. **动画时间:**指黑屏的毫秒数可以填0或不小于100的整数不填则使用玩家设定值。
* V2.8起,楼层切换的音效被延迟到了“屏幕完全变黑”的瞬间(脚本编辑——切换楼层中)才播放,以方便作者根据条件设置不同音效,因此上下楼时间建议尽量缩短。
5. **穿透性:**见[系统开关](start)允许穿透楼梯。
值得注意的是,绑定了楼层转换事件的门、怪物、道具、箱子,其系统行为(开门、战斗、拾取、推行)将不再发生(阻激夹域等地图属性不受影响),而是被覆盖为上下楼。
## 普通事件(红)
普通事件在启用后可以被勇士碰触而触发,它们都有着这样的开头:
```
覆盖触发器(Y/N) 启用(Y/N) 通行状态(Y/N?) 显伤(Y/N)
V2.8新增:不透明度(0~1) 虚化 色相(0~359) 灰度(0~1) 反色(Y/N) 阴影
```
1. **覆盖触发器:**如前所述门、怪物、道具、箱子都已经被绑定了系统触发器。如果您需要让地图上的个别这类图块在被勇士碰触时不发生系统行为开门、战斗、拾取、推行而是执行您在下面自定义的事件比如需要制作一个“怪物中的叛徒”它虽然有显伤但其实是个npc请勾选此项。阻激夹域捕捉支援等地图属性不受影响
2. **启用:**如果不勾选此项,则此事件初始时是隐藏的。
* 非常不推荐仅仅为了初始隐藏一些图块(如战利品、陷阱门墙、埋伏怪等)就去绑定一堆没有指令的普通事件。
* 更安全的做法是“从0转变图块”支持淡入效果和“关门”这样还有音效多香
* 事实上,除“覆盖触发器”外,其余所有参数本不该作为“普通事件”的一部分而应该移出去放在表格中,这是样板的历史遗留问题。
* 如今V2.8加入了六项特效,可能导致无指令的普通事件越来越不可避免,请自行注意。
3. **通行状态:**除怪物和道具外,所有图块本身都具有“可通行性”属性。
* 您可以设置此项去覆盖地图中这一点的可通行性譬如强行让一格空地不可通行勇士撞击时出现一堵墙51层魔塔第23层的效果
4. **显伤:**在对怪物使用覆盖触发器后,表面上看不出它和一般怪物的不同。
* 如有必要(比如触碰这怪物根本不战斗),可不勾选显伤以示区别。
* 如数量较多可注册一个相同素材的NPC比如样板中的仙子。
* 如需在一段剧情中关闭所有显伤,可以简单地暂时移除怪物手册
5. **不透明度:**如题0表示完全不透明1表示完全隐身。可以设为1用来配合楼层贴图实现大型怪物或者设为半透明来表示灵魂等状态的角色。本属性可以在事件中用指令动态修改支持渐变。
6. **虚化:**必须为自然数,设定后图块会变模糊,可以用来配合快速移动/跳跃来表示幻影状态的角色。不建议对多帧图块长时间使用,因为会随着不断的抖动把周围越描越黑。
7. **色相:**必须为0359的整数180表示互补色。常用于npc以避免过于单调也可以在游戏的教学环节用此项高亮强调部分图块。
8. **灰度:**如题1表示完全变灰必要时可以全图变灰来表示回忆/梦境/濒死等场合。
9. **反色:**勾选后此图块将反色,必要时可以全图瞬间反色来表示惊吓。
10. **阴影:**必须为自然数,设定后图块周围将出现阴影描边,在草地上效果更佳哦。
可以使用`core.setBlockFilter`更改任何一点的最后五项特效。
## 自动事件(橙)
在“右键绑定机关门”中,我们已经初见了自动事件的端倪。
在游戏中我们经常需要监听大量乱七八糟的状态根据状态的变化去做出及时的处理比如勇士生命小于等于0会触发游戏失败。比如51层魔塔第39层监听9扇黄门的状态来触发中心对称飞行器的出现、第49层监听8名魔法警卫的状态来封印假魔王。
如果给9扇黄门都绑定类似的开门后事件、给8名警卫都绑定类似的战后事件就非常繁琐。
而自动事件则不然,它不需要您去主动考虑所有“改变状态”的原因,而是简单地在每次刷新状态栏时,检查是否满足触发条件,满足的话就执行。
每个自动事件具有以下几个属性:
1. **触发条件:**自动事件最重要的属性,为支持“冒号缩写量”的逻辑表达式。
* 譬如上述两个例子中触发条件就是“某两个点没有图块而某7个点图块为黄门”和“某四个点没有图块而某四个点图块为魔法警卫”。
* 关于冒号缩写量,详见下面的“值块和冒号缩写量”。
* V2.8起自动事件的触发条件和“值块”支持多行编辑eval时会忽略\n
* 因此高级作者可以将此条件写成无名函数这样就可以在里面使用var进行复杂的多步计算了。
2. **优先级:**一般不需要改动。当多个自动事件的条件同时满足时,优先执行此级别较大的。
* 此级别也相等时,允许跨层触发则优先执行楼层较矮的。
* 同一楼层优先执行横坐标较小的,横坐标相等时优先执行纵坐标较小的,同一点处优先执行页码较小的。
3. **仅在本层检测:**如题一般不需要改动。除非您要制作一些全局的效果如“勇士生命低于上限的1/3时改变背景音乐”。
* V2.8起,即使不勾选此项,也不会在分区砍层模式下触发冻结状态楼层的自动事件!
4. **事件流中延迟执行:**勾选此项后,在执行其他事件时触发此自动事件则会将其内容追加到待执行事件的末尾,否则插队到待执行事件的开头。
5. **允许多次执行:**如果不勾选,则此事件最多只会被触发一次。即使勾选了,每次执行的过程中也会暂时禁止自己被触发。勾选后,请记得在此事件的指令中将条件变得不满足。
每个点初始提供了两个自动事件页,您还可以再在数据区最上面单击“添加自动事件页”。
## 公共事件(逗号键)
有几个事件,如战后加点、回收钥匙商店,会在战后脚本或全局商店等处频繁用到。于是它们被整理成了公共事件,您也可以追加自己的。
公共事件在内容上和其他事件并无不同,只是在插入公共事件时(如`core.insertCommonEvent("回收钥匙商店",[...])`)可以提供一个`参数列表`。
参数列表是一个一维数组,其每项会被依次代入`core.status.hero.flags.arg1、arg2、……`,而`arg0`则会记录公共事件的中文名。
在整个事件流结束后即勇士恢复行动请注意不是公共事件结束后这些以arg加纯数字命名的变量、以及以@temp@开头的临时变量都会被清空。
## 滑冰事件
滑冰ski是一个非常特殊的触发器使用此触发器的图块需要画在背景层。
冰上可以画门、怪物、道具等任何图块您还可以在前景层画上四种单向通行箭头样板1层右下方那些或红线薄墙素材区第一列最下方那些把三个图层的游戏性都发挥出来。
勇士走上冰面后,将一直“前进一格或撞击”。直到滑出冰面或无法再前进,如撞到墙或事件。
比如撞到怪物会强制战斗打不过直接游戏失败。走到道具上会捡起来撞到门则要看有没有钥匙有的话会把门打开。V2.7.3起,踩到致命领域会死亡而不是停在前一格。
默认情况下,触发事件后勇士会停下来。如果要继续滑冰,可以在这些点的战后事件、开门后事件、(拾获)道具后事件(这里最好勾选“轻按时不触发”)中判断勇士还在不在冰上(`core.onSki()`函数),在冰上的话就命令“勇士前进一格或撞击”。
## 事件编辑器
![image](img/blockly.jpg)
当您从数据区单击任何一个事件类属性的“编辑”按钮时,就会呼出事件编辑器,上图给出了事件编辑器的全貌。
图中左下角为指令类别,可以点击它们呼出各类指令菜单,如左上方的值块菜单。
中央为指令区,被一个标识事件类型的紫色块包覆(即左下角的“入口方块”,图中为开门后事件),所有的指令都塞在里面。
右上角为json区会随着指令区的编辑而自动变化。`core.insertAction()`函数的自变量就是它们所以学有余力的话也建议您经常左右对照去了解每个指令对应的json写法。
右下角为调色器,当您单击指令块中的颜色按钮时(如图中“画面闪烁”指令的白块)就会自动呼出,可以进行诸如十进制和十六进制颜色格式互转并预览等操作。
左上角为功能区,各功能的含义和用法如下:
1. **确认Ctrl+S**将指令区的指令组翻译为json写回表格和文件并关闭事件编辑器。如果手动修改了json区却没有点击变黄的解析按钮或指令区存在异步事件没有用“等待所有异步事件执行完毕”图中第二条褐色指令去等待则单击确认按钮会弹窗警告。此时如果想放弃对json区的修改请随便点一下指令区的一个块即可。
2. **解析:**将手动修改后的json区翻译回指令组同步到指令区一般用于跨作品复制事件或长距离批量挪动和复制指令。此外由于下拉框不支持任意输入如新增的怪物属性、楼层切换的任意目标图块ID所以也必须这样做。
3. **取消:**放弃本次编辑,关闭事件编辑器,表格和文件会停留在打开事件编辑器前的状态。
4. **搜索事件块:**当您想不起来需要的指令在哪个类别时,请使用此项,如输入“勇”字就会列出所有带有“勇士”一词的指令。
5. **地图选点:**在编辑事件尤其是大篇幅事件的过程中如果需要频繁保存并退出事件编辑器去浏览地图尤其是浏览别的楼层就很不方便。单击此按钮您可以浏览所有楼层及其全景。通过点击地图去确认一个点的坐标并复制任何一个楼层ID以填入需要的地方。需要填坐标的指令也可以直接双击其指令块从地图选点填入坐标和楼层从而避免了肉眼数格子和手敲的麻烦和出错的隐患。
6. **变量出现位置搜索:**您可以搜索所有以“变量xxx”形式出现在事件中的变量的出现位置形式必须是冒号缩写量位置必须是事件而不是脚本。也就是说`flags.xxx`和`core.insertAction([...])`中的变量都是搜索不到的。
7. **开启中文名替换:**勾选此项后“flag、status、item、switch、temp、global、enemy”等冒号缩写量的前缀以及怪物和道具的ID才会允许用中文在指令块中书写如“变量、状态、物品、独立开关、临时变量、全局存储、怪物”。当然json中还是只能用英文的。此外如果您需要使用名称相同的道具或怪物或在`${}`以外的文本中将上述几个词和冒号连用则也可能需要关闭此功能以避免blockly和json互译出错。
8. **展开值块逻辑运算:**在值块菜单中我们可以看到第一个就是二元运算块但是它并不完整比如没有位运算。因此如果解析的优先级与js的优先级不一致请在表达式中适当的位置添加圆括弧或取消勾选此项。
## 值块和冒号缩写量
![image](img/values.jpg)
值块并不能单独构成指令,但它们可以被嵌入数值操作、设置怪物属性和很多事件控制类指令。而冒号缩写量的用法就更多了,比如用于`${表达式计算}`和其他任何支持填表达式的地方。
值块大体上分为三种:勾选框与运算块、只读块、可读写块,其中后两类对应着冒号缩写量。
### 勾选框与运算块附js比较运算大图鉴
包括勾选框块(`true`和`false`两个逻辑常量)、否定运算块和二元运算块。
否定运算块(一个“非”字,脚本中写作一个叹号)会把`false、0、null、undefined、NaN`和空字符串”(以下简称广义`false`)变为`true`,而把别的量都变为`false`.
二元运算块包括四则运算、取余、乘方、比较运算和三种逻辑运算。
四则运算中两个整数相除会得到小数两个0的乘方会得到1数学上无意义
八种比较运算中,四种比大小的运算如果两项中有一项是数字,就会把另一项也转换成数字去比,所以会出现这些:
`'2' < 10; 10 <= '10'; '10' < 2; null >= 0; null <= 0; 1/0 > 1/-0`
![image](img/compare.jpg)
弱(不)等于(==和!=)非常难用,建议放弃,统一使用(不)等于(===和!==)。
`NaN`一般是`0/0`或字符串瞎做减乘除得到的,它跟谁都没有可比性,包括自己。
纯数字字符串(包括十六进制)和对应的数字,都是弱相等关系。如`'0x10' == 16`
数组和对象,跟自己都是既大于等于又小于等于的关系(因为不是同一个)。
单个数字(或什么都没有)用方括号还是引号括起来,对外的比较行为都一致。
`undefined`倒是不用担心,它除了和`null`弱相等外,和其他值都没有可比性。
三种逻辑运算的原则是,“且”的优先级高于“或”,它俩都不满足交换律。
若干个由“且”(&&)连起来的量中,取第一个广义`false`(没有则取最后一个量),
若干个由“或”(||)连起来的量中,取第一个不是广义`false`的量(没有则同上),
若干个由“异或”(^)连起来的`true`或`false`,总结果取决于其中`true`的个数的奇偶,奇数个`true`则总结果为1偶数个`true`则总结果为0.
所以样板中充斥着形如`x=x||y`和`z=x||y`的代码y算是x的默认值。
这种做法并不安全,如果您的作品不打算发布到我们的`h5mota.com`,则更建议使用`x??y`它只会在“x为`null`或`undefined`时”才用y代替x。
双问号运算在早期浏览器并不被支持,因此如需在我们的站点发布则需要写`if(x==null){x=y;}`。
再次强调,广义`false`不一定和`false`弱相等(比如`null`),而和`false`弱相等的也不一定是广义`false`(如`[]==![]`)。
V2.7.3起,一元运算块追加了一些数学函数,如四种取整、开平方、绝对值。还追加了“变量类型”(typeof),只有两个类型相同的变量才有可能满足“相等”(===)。
V2.8起二元运算块追加了一些js函数如“取较大/较小”(`Math.max`和`Math.min`)、“开始于/结束于”(字符串的`startsWith`和`endsWith`)、“包含”(数组和字符串的`includes`),进阶作者可以学习使用。
### 只读块怪物属性、图块ID、图块类别、装备孔
尽管这几个值块是只读的,但您仍然可以使用“设置怪物属性”、“穿脱装备”和“转变图块”等指令去改变它们。
1. **怪物属性:**冒号缩写量写作`怪物:绿头怪:生命`json代码中写作`enemy:greenSlime:hp`。怪物名称有重复的话可以在事件编辑器顶部关闭中文替换功能,道具名称同理。
* 常见的怪物属性都提供在了下拉框中,如果下拉框里没有(如通过配置表格新增的属性),可以修改`_server\MotaAction.g4`文件最后的那些表来追加其中文简称,也可以放弃解析回中文块而是就用缩写量。
* V2.8起,怪物支持“行走图朝向”绑定,此值块将始终获取脸朝下的怪物属性。
2. **图块ID**冒号缩写量写作`图块IDx,y`json代码中写作`blockId:x,y`。请注意逗号始终要用英文的此值块实际直接调用的API为`core.getBlockId(x,y)`即本值块和对应的冒号缩写量只支持本层可以获知本层某个点的图块ID.
3. **图块数字:**冒号缩写量写作`图块数字x,y`json代码中写作`blockNumberx,y`。同上,实际调用`core.getBlockNumber(x,y)`,可以获知本层某个点的图块数字。
4. **图块类别:**冒号缩写量写作`图块类别x,y`json代码中写作`blockCls:x,y`。同上,实际调用`core.getBlockCls(x,y)`,可以获知本层某个点的图块类别。
* V2.8起这三个值块中的坐标支持填写表达式会在json区直接翻译为API调用但这样填写也意味着无法再解析回值块而且本质上这已经不是冒号缩写量了不能直接用于${表达式计算}等场合。
5. **第n格装备孔**冒号缩写量写作`装备孔n`json代码中写作`equip:n`。实际调用`core.getEquip(n)`可以获知勇士第n个装备孔中的装备IDn从0开始
### 可读写块status、item、flag、switch、temp、global、buff
比起只读块,可读写块允许作为“数值操作”指令的左块:
1. **状态:**冒号缩写量为`状态:生命`等json代码中写作`status:hp`等。
* 作为左块时会调用`core.setStatus()`,其他时候会调用`core.getStatus()`
2. **物品:**冒号缩写量为`物品:炸弹`等json代码中写作`item:bomb`等
* 作为左块时会调用`core.getItem()`或`core.setItem()`,其他时候会调用`core.itemCount()`来统计背包中的道具数量。
* 此外,最常用的一些勇士状态(包括坐标和朝向)、三色钥匙、三围增益都提供在了下拉框中。您可以修改`_server/MotaAction.g4`文件最后的`FixedId_List`来追加向“楼层转换”、“门信息”和“装备属性”的下拉框追加新的楼梯ID、钥匙ID和勇士状态名也是一样的道理当然不追加也不影响手写如果指令只提供了下拉框版本那就必须写在json区再解析
3. **变量:**冒号缩写量为`变量hatred`等json代码中写作`flag:hatred`等。
* 作为左块时会调用`core.setFlag()`,其他时候会调用`core.getFlag(xxx, 0)`
* 请注意:变量类型支持中文,如`变量:开门个数`
* 这两个API实际操作的就是前文多次提到的`core.status.hero.flags`只不过在事件中未定义的变量都视为0更为安全。
* 如果您忘记了自己在哪些事件用过哪些变量,请善用事件编辑器顶部的“搜索变量出现位置”。
4. **独立开关:**如果您有一大批NPC都具有“首次对话不同于之后各次对话”之类的特点那么为他们设置一大批不重名的变量就很繁琐。独立开关叫独立变量更准确就是为这种场合而生的它对每层楼首次到达和每次到达中和每个坐标都是独立的。
* 冒号缩写量写作`独立开关A—Z`json代码中写作`switch:A—Z`。
* 每个坐标处的独立开关有26个用大写拉丁字母A—Z表示。MTn层xy点的独立开关A本质上是一个flag变量——`flag:MTn@x@y@A`,如果需要在其他点访问,就需要用这样的写法。
* 首次到达/每次到达楼层的事件中上述x和y会保留小写字母而不代入任何数字。
* 标题事件/开场剧情中前缀的“楼层ID”视为“`:f`”。
* 可以在事件编辑器左侧菜单的“常用事件模板”中看到一个仿51层/新新魔塔的一次性商人的例子。
5. **临时变量:**冒号缩写量写作“临时变量A—Z”json代码中写作`temp:A-Z`
* 临时变量泛指所有以`@temp@`开头的flag变量一共也有26个。
* 临时变量一般用于计数循环和数组迭代每当事件流结束勇士恢复行动临时变量和参数变量以arg+纯数字命名)都会被清空。
* 全局商店状态下,临时变量`@temp@shop`会被启用,从而实现长按连续购买等操作。
* 上面提到的“一次性商人”模板中多次出现出售数量和价格常数,如果使用临时变量就很方便,不需要修改多个地方了。
6. **全局存储:**冒号缩写量写作`全局存储xxx`json代码中写作`global:xxx`
* 和变量一样,冒号右侧本身支持中文。全局存储一般用来保存一些跨存档的信息,如成就系统、回想和音像鉴赏的收集进度、多周目数据等。
* 用于左块时会调用`core.setGlobal()`,其他时候会调用`core.getGlobal()`。
* `core.setGlobal()`在录像中会被忽略,`core.getGlobal()`在正常游戏中会将读取到的值写进录像,而在录像播放中直接读取这个写进去的值,从而复原了那次游戏时的样子。
* 然而,由于`core.getGlobal()`的调用频率在正常游戏和录像播放中可能不一致,因而可能会导致录像中的记录次数过多、过少或位置不合适。
* V2.8对这个问题进行了一定的修复,请放心使用。
7. **增益:**冒号缩写量写作`增益:生命上限`等json代码中写作`buff:hpmax`等,冒号右侧支持自定义的勇士新属性英文名。
* 用于左块时会调用`core.setBuff()`,其他时候会调用`core.getBuff()`。
* 增益这个概念在前面讲解装备属性时提到过,所有的百分比装备和百分比衰弱都是靠它发挥作用。
* `buff:xxx`本质上就是`flag:__buff_xxx__`但前者更安全默认值为1
## 随机数(扩展内容,了解即可)
此外V2.8还新增了几个样板API的值块包括“当前是否身穿某装备”、“当前是否在录像播放中”、“是否到达过某楼层”、“是否开启了某全局商店”、“能否打败某怪物”、“勇士面前第n格的坐标负数表示身后”、“随机数”。
样板包括两种随机数,一种是上面说的值块提供的`core.rand(n)`会得到小于正整数n的随机自然数n不填则会得到小于1的随机正小数。
一种则是`core.rand2(n)`同样会得到小于n的随机自然数n必须填正整数不要省略
rand本质上是`flag:__rand__`不断一步步推进的结果,初始值为`flag:__seed__`,初始值(又叫随机种子)和游戏难度可以在标题画面通过`core.startGame(hard,seed)`函数指定(如果开启了标题界面事件化,则该函数还必须作为游戏重启函数`core.hideStartAnimate()`的回调来使用,但这样只能指定种子),种子必须为正整数。
rand2则是通过js自带的`Math.random()`函数得到的真随机小数经过整数化处理的结果,会被直接写入录像(`random:n`),因此多次调用会导致录像(和存档)变得很庞大。而且正因为是写入录像,所以玩家也可以直接编辑录像文件或`core.status.route`来控制随机的结果。
直观地说rand的结果只和它的调用次数有关因此玩家不能通过简单的SL操作来优化结果必须调整路线或干脆重开游戏。
而rand2的结果可以通过SL来改变如果需要连续大量使用“可被SL改变的随机数”期间不能存档但又不希望录像变得太庞大可以先在存档后使用一次`n=core.rand2(114514)`再使用n次`core.rand()`来推进效果就能让人满意了。新新魔塔1就是这样的思路每次撞怪时先自动存档然后生成n值战斗中只使用rand
然而如果rand和rand2的使用场合不当那么其调用频率在正常游戏中和录像回放时可能不一致。rand会因为推进步数不一致而导致游戏流程发生变化rand2则会导致录像中记录的随机数出现多余或缺失。
V2.8对rand2以及“全局存储”、“弹窗输入”、“选择项”、“确认框”的此问题进行了一定的修复如果录像中出现了多余的随机数记录就会在播放时跳过并在控制台警告如果需要rand2但未记录或记录越界则会现场重新随机生成并补录。在V2.8之前这两种情况都会直接报错而在V2.8中会导致首次播放结果的二次记录不一致请不要惊慌按R键从头再次播放一遍即可。
总之,不计入录像的纯观赏性的随机效果(如捡到某道具随机播放一个音效甚至随机指定音调高低,以及样板雨雪效果中的随机)建议直接用`Math.random()`实现例如V2.8每次上下楼后会在楼层属性的背景音乐中随机播放一个)。
## 录像回放(扩展内容,了解即可)
魔塔具有时空离散性和完全可重复性,因此可以像棋类运动一样记录类似棋谱的东西,我们称之为【录像】。
正常游戏中,已录制的内容被保存在一维数组`core.status.route`中并不断从尾部延长,录像回放时,即将播放的内容会保存在一维数组`core.status.replay.toReplay`中并不断从头部缩减。
V2.8.1起,录像中勇士死亡将会报错并询问是否回到上一个节点,比起只能读取自动存档更为友好。
您可以在正常游戏中自由行动时随时按下R键进行回放上述数组具体的内容含义如下
```
up down left right 勇士向某个方向行走一步
item:ID 打开背包使用某件道具如item:bomb表示使用炸弹
unEquip:n 卸掉身上第n件装备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项选择项中n从0开始确认框界面0表示确定1表示取消此项越界将报错。此项缺失的话确认框将选择作者指定的默认项初始光标位置选择项将弹窗请求补选后台录像验证中则选默认项
shop:ID 打开某个全局商店如shop:itemShop表示打开道具商店。因此连载塔千万不要中途修改商店ID
turn 单击勇士Z键转身
turn:dir 勇士转向某个方向dir可以为up down left right注意此项在正常游戏中无法随意触发。
getNext 轻按获得身边道具,优先获得面前的,身边如果没有道具则此项会被跳过。
input:none “等待用户操作事件”中超时(作者未设置超时时间则此项会导致报错)
input:xxx 可能表示“等待用户操作事件”的一个操作也可能表示一个“接受用户输入数字”的输入后者的情况下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()得到纯英文数字的内容)需要用(半角圆括弧)括起来。
```
[在线插件库](https://h5mota.com/plugins/)中提供了“录像自助精修”插件,手机也适用,可供研究。
开门打怪前会先转身再自动存档,因此该存档被读取后,已录制内容的最后一步将变成转身,导致录像变长,请自行注意。
==========================================================================================
[继续阅读下一章:事件指令](instruction)

313
eventDec.d.ts vendored Normal file
View File

@ -0,0 +1,313 @@
type MotaAction = any;
type MotaEvent = any[];
/**
*
*/
type ShopEventOf<T extends keyof ShopEventMap> = ShopEventMap[T];
interface ShopEventMap {
/**
*
*/
common: CommonShopEvent;
/**
*
*/
item: ItemShopEvent;
/**
*
*/
event: CommonEventShopEvent;
}
interface ShopEventBase {
/**
* id
*/
id: string;
/**
*
*/
textInList: string;
/**
*
*/
mustEnable: boolean;
/**
*
*/
disablePreview: boolean;
}
/**
*
*/
interface CommonShopChoice {
/**
*
*/
text: string;
/**
*
*/
need: string;
/**
*
*/
icon: AllIds;
/**
*
*/
color: Color;
/**
*
*/
action: MotaEvent;
}
/**
*
*/
interface CommonShopEvent extends ShopEventBase {
/**
*
*/
text: string;
/**
*
*/
choices: CommonShopChoice[];
}
/**
*
*/
interface ItemShopChoice {
/**
* id
*/
id: AllIdsOf<'items'>;
/**
*
*/
number: number;
/**
* ${}
*/
money: string;
/**
*
*/
sell: string;
/**
*
*/
condition: string;
}
/**
*
*/
interface ItemShopEvent extends ShopEventBase {
/**
*
*/
item: true;
/**
* 西
*/
use: 'money' | 'exp';
/**
*
*/
choices: ItemShopChoice[];
}
interface CommonEventShopEvent {
/**
* 使
*/
commonEvent: EventDeclaration;
}
interface AutoEventBase {
/**
*
*/
condition: string;
/**
*
*/
currentFloor: boolean;
/**
*
*/
priority: number;
/**
*
*/
delayExecute: boolean;
/**
*
*/
multiExecute: boolean;
/**
*
*/
data: MotaEvent;
}
interface AutoEvent extends AutoEventBase {
/**
* id
*/
floorId: FloorIds;
/**
*
*/
index: string;
/**
*
*/
x: number;
/**
*
*/
y: number;
/**
*
*/
symbol: string;
}
interface LevelChooseEvent {
/**
*
*/
title: string;
/**
*
*/
name: string;
/**
* hard值
*/
hard: number;
/**
*
*/
color: RGBArray;
/**
*
*/
action: MotaEvent;
}
interface LevelUpEvent {
/**
*
*/
need: number;
/**
*
*/
title: string;
/**
*
*/
action: MotaEvent;
}
/**
*
*/
interface DoorInfo {
/**
*
*/
time: number;
/**
*
*/
openSound: SoundIds;
/**
*
*/
closeSound: SoundIds;
/**
*
*/
keys: Partial<Record<ItemIdOf<'tools'> | `${ItemIdOf<'tools'>}:o`, number>>;
/**
*
*/
afterOpenDoor?: MotaEvent;
}
interface ChangeFloorEvent {
/**
*
*/
floorId: ':before' | ':after' | ':now' | FloorIds;
/**
* stair就无效了
*/
loc?: LocArr;
/**
*
*/
stair?: FloorChangeStair;
/**
*
*/
direction?: HeroTurnDir;
/**
*
*/
time?: number;
/**
* 穿
*/
ignoreChangeFloor?: boolean;
}

456
eventStatus.d.ts vendored Normal file
View File

@ -0,0 +1,456 @@
interface EventStatusDataMap {
/**
*
*/
action: ActionStatusData;
/**
*
*/
book: number;
/**
*
*/
fly: number;
/**
*
*/
viewMaps: ViewMapStatusData;
/**
*
*/
equipbox: EquipboxStatusData;
/**
*
*/
toolbox: ToolboxStatusData;
/**
*
*/
save: SaveStatusData;
load: SaveStatusData;
replayLoad: SaveStatusData;
replayRemain: SaveStatusData;
replaySince: SaveStatusData;
/**
*
*/
text: TextStatusData;
/**
*
*/
confirmBox: ConfirmStatusData;
/**
*
* 西
*/
about: null;
help: null;
'book-detail': null;
keyBoard: null;
switchs: null;
'switchs-sounds': null;
'switchs-display': null;
'switchs-action': null;
settings: null;
selectShop: null;
notes: null;
syncSave: null;
syncSelect: null;
localSaveSelect: null;
storageRemove: null;
cursor: null;
replay: null;
gameInfo: null;
}
interface _EventStatusSelectionMap {
/**
*
*/
action: number;
/**
*
*/
equipbox: number;
/**
*
*/
toolbox: number;
/**
*
*/
save: boolean;
load: boolean;
/**
* (0)(1)
*/
confirmBox: 0 | 1;
/**
*
*/
switchs: number;
'switchs-sounds': number;
'switchs-display': number;
'switchs-action': number;
settings: number;
notes: number;
syncSave: number;
syncSelect: number;
localSaveSelect: number;
storageRemove: number;
replay: number;
gameInfo: number;
}
interface _EventStatusUiMap {
/**
*
*/
action: ActionStatusUi;
/**
* id
*/
book: number;
/**
*
*/
confirmBox: string;
/**
*
*/
'switchs-display': SwitchsStatusData;
/**
*
*/
settings: SwitchsStatusData;
/**
*
*/
selectShop: SelectShopStatusUi;
notes: SelectShopStatusUi;
syncSave: SelectShopStatusUi;
syncSelect: SelectShopStatusUi;
localSaveSelect: SelectShopStatusUi;
storageRemove: SelectShopStatusUi;
gameInfo: SelectShopStatusUi;
}
interface _EventStatusIntervalMap {
/**
* (?
*/
action: ActionStatusData;
/**
* 退
*/
book: ActionStatusData;
/**
* 退
*/
save: ActionStatusData;
load: ActionStatusData;
}
interface _EventStatusTimeoutMap {
/**
*
*/
action: number;
}
interface _EventStatusAnimateUiMap {
/**
*
*/
action: number;
}
type EventStatus = keyof EventStatusDataMap;
type _FillEventStatus<T> = {
[P in EventStatus]: P extends keyof T ? T[P] : null;
};
type EventStatusSelectionMap = _FillEventStatus<_EventStatusSelectionMap>;
type EventStatusUiMap = _FillEventStatus<_EventStatusUiMap>;
type EventStatusIntervalMap = _FillEventStatus<_EventStatusIntervalMap>;
type EventStatusTimeoutMap = _FillEventStatus<_EventStatusTimeoutMap>;
type EventStatusAnimateUiMap = _FillEventStatus<_EventStatusAnimateUiMap>;
/**
*
*/
interface EventStatusOf<T extends EventStatus = EventStatus> {
/**
*
*/
id: T;
/**
*
*/
data: EventStatusDataMap[T];
/**
*
*/
selection: EventStatusSelectionMap[T];
/**
* ui信息
*/
ui: EventStatusUiMap[T];
/**
*
*/
interval: EventStatusIntervalMap[T];
/**
*
*/
timeout: EventStatusTimeoutMap[T];
/**
*
*/
animateUi: EventStatusAnimateUiMap[T];
}
interface ActionStatusEventList {
/**
*
*/
todo: MotaEvent;
/**
*
*/
total: MotaEvent;
/**
*
*/
contidion: string;
}
interface ActionLocStackInfo {
/**
*
*/
x: number;
/**
*
*/
y: number;
/**
* id
*/
floorId: FloorIds;
}
/**
*
*/
interface ActionStatusData {
/**
*
*/
list: DeepReadonly<ActionStatusEventList>;
/**
*
*/
x?: number;
/**
*
*/
y?: number;
/**
*
*/
callback?: () => void;
/**
*
*/
appendingEvents: MotaEvent[];
/**
*
*/
locStack: any[];
/**
*
*/
type: string;
/**
*
*/
current: MotaAction;
}
interface ActionStatusUi {
/**
*
*/
text: string;
/**
*
*/
yes?: MotaEvent;
/**
*
*/
no?: MotaEvent;
/**
*
*/
choices?: string[];
/**
*
*/
width?: number;
}
interface ViewMapStatusData {
/**
*
*/
index: number;
/**
*
*/
damage: boolean;
/**
*
*/
all: boolean;
/**
*
*/
x: number;
/**
*
*/
y: number;
}
interface EquipboxStatusData {
/**
*
*/
page: number;
/**
*
*/
selectId: ItemIdOf<'equips'>;
}
interface ToolboxStatusData {
/**
*
*/
toolsPage: number;
/**
*
*/
constantsPage: number;
/**
* id
*/
selectId: ItemIdOf<'constants' | 'tools'>;
}
interface SaveStatusData {
/**
*
*/
page: number;
/**
*
*/
offset: number;
/**
* fav表示收藏all表示所有存档
*/
mode: 'fav' | 'all';
}
interface TextStatusData {
/**
*
*/
list: string[];
/**
*
*/
callback: () => void;
}
interface ConfirmStatusData {
/**
*
*/
yes: () => void;
/**
*
*/
no: () => void;
}
interface SwitchsStatusData {
/**
*
*/
choices: string[];
}
interface SelectShopStatusUi {
/**
*
*/
offset: number;
}

147
events.js Normal file
View File

@ -0,0 +1,147 @@
var events_c12a15a8_c380_4b28_8144_256cba95f760 =
{
"commonEvent": {
"加点事件": [
{
"type": "comment",
"text": "通过传参flag:arg1 表示当前应该的加点数值"
},
{
"type": "choices",
"choices": [
{
"text": "攻击+${1*flag:arg1}",
"action": [
{
"type": "setValue",
"name": "status:atk",
"operator": "+=",
"value": "1*flag:arg1"
}
]
},
{
"text": "防御+${2*flag:arg1}",
"action": [
{
"type": "setValue",
"name": "status:def",
"operator": "+=",
"value": "2*flag:arg1"
}
]
},
{
"text": "生命+${200*flag:arg1}",
"action": [
{
"type": "setValue",
"name": "status:hp",
"operator": "+=",
"value": "200*flag:arg1"
}
]
}
]
}
],
"回收钥匙商店": [
{
"type": "comment",
"text": "此事件在全局商店中被引用了(全局商店keyShop)"
},
{
"type": "comment",
"text": "解除引用前勿删除此事件"
},
{
"type": "comment",
"text": "玩家在快捷列表V键中可以使用本公共事件"
},
{
"type": "while",
"condition": "1",
"data": [
{
"type": "choices",
"text": "\t[商人,trader]你有多余的钥匙想要出售吗?",
"choices": [
{
"text": "黄钥匙10金币",
"color": [
255,
255,
0,
1
],
"action": [
{
"type": "if",
"condition": "item:yellowKey >= 1",
"true": [
{
"type": "setValue",
"name": "item:yellowKey",
"operator": "-=",
"value": "1"
},
{
"type": "setValue",
"name": "status:money",
"operator": "+=",
"value": "10"
}
],
"false": [
"\t[商人,trader]你没有黄钥匙!"
]
}
]
},
{
"text": "蓝钥匙50金币",
"color": [
0,
0,
255,
1
],
"action": [
{
"type": "if",
"condition": "item:blueKey >= 1",
"true": [
{
"type": "setValue",
"name": "item:blueKey",
"operator": "-=",
"value": "1"
},
{
"type": "setValue",
"name": "status:money",
"operator": "+=",
"value": "50"
}
],
"false": [
"\t[商人,trader]你没有蓝钥匙!"
]
}
]
},
{
"text": "离开",
"action": [
{
"type": "exit"
}
]
}
]
}
]
}
]
}
}

51
extensions.js Normal file
View File

@ -0,0 +1,51 @@
/*
extensions.js负责拓展插件
*/
"use strict";
function extensions () {
}
extensions.prototype._load = function (callback) {
if (main.replayChecking) return callback();
if (!window.fs) {
this._loadJs('_server/fs.js', function () {
core.extensions._listExtensions(callback);
}, callback);
} else this._listExtensions(callback);
}
extensions.prototype._loadJs = function (file, callback, onerror) {
var script = document.createElement('script');
script.src = file + '?v=' + main.version;
script.onload = callback;
script.onerror = onerror;
main.dom.body.appendChild(script);
}
extensions.prototype._listExtensions = function (callback) {
if (!window.fs) return callback();
fs.readdir('extensions', function (error, data) {
if (error || !(data instanceof Array)) return callback();
var list = [];
data.forEach(function (name) {
if (/^[\w.-]+\.js$/.test(name)) {
list.push(name);
}
});
list.sort();
core.extensions._loadExtensions(list, callback);
});
}
extensions.prototype._loadExtensions = function (list, callback) {
var i = 0;
var load = function () {
if (i == list.length) return callback();
core.extensions._loadJs('extensions/' + list[i++], load, load);
}
load();
}

186
fs.js Normal file
View File

@ -0,0 +1,186 @@
(function () {
window.fs = {};
var _isset = function (val) {
if (val == undefined || val == null || (typeof val == 'number' && isNaN(val))) {
return false;
}
return true
}
var _http = function (type, url, formData, success, error, mimeType, responseType) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
if (_isset(mimeType))
xhr.overrideMimeType(mimeType);
if (_isset(responseType))
xhr.responseType = responseType;
xhr.onload = function (e) {
if (xhr.status == 200) {
if (_isset(success)) {
success(xhr.response);
}
}
else {
if (_isset(error))
error("HTTP " + xhr.status);
}
};
xhr.onabort = function () {
if (_isset(error))
error("Abort");
}
xhr.ontimeout = function () {
if (_isset(error))
error("Timeout");
}
xhr.onerror = function () {
if (_isset(error))
error("Error on Connection");
}
if (_isset(formData))
xhr.send(formData);
else xhr.send();
}
var postsomething = function (data, _ip, callback) {
if (typeof (data) == typeof ([][0]) || data == null) data = JSON.stringify({ 1: 2 });
_http("POST", _ip, data, function (data) {
if (data.slice(0, 6) == 'error:') {
callback(data, null);
}
else {
callback(null, data);
}
}, function (e) {
if (window.main != null) console.log(e);
else console.log(e);
callback(e + ":请检查启动服务是否处于正常运行状态。");
}, "text/plain; charset=x-user-defined");
}
fs.readFile = function (filename, encoding, callback) {
if (typeof (filename) != typeof (''))
throw 'Type Error in fs.readFile';
if (encoding == 'utf-8') {
//读文本文件
//filename:支持"/"做分隔符
//callback:function(err, data)
//data:字符串
var data = '';
data += 'type=utf8&';
data += 'name=' + filename;
postsomething(data, '/readFile', callback);
return;
}
if (encoding == 'base64') {
//读二进制文件
//filename:支持"/"做分隔符
//callback:function(err, data)
//data:base64字符串
var data = '';
data += 'type=base64&';
data += 'name=' + filename;
postsomething(data, '/readFile', callback);
return;
}
throw 'Type Error in fs.readFile';
}
fs.writeFile = function (filename, datastr, encoding, callback) {
if (typeof (filename) != typeof ('') || typeof (datastr) != typeof (''))
throw 'Type Error in fs.writeFile';
if (encoding == 'utf-8') {
//写文本文件
//filename:支持"/"做分隔符
//callback:function(err)
//datastr:字符串
var data = '';
data += 'type=utf8&';
data += 'name=' + filename;
data += '&value=' + datastr;
postsomething(data, '/writeFile', callback);
return;
}
if (encoding == 'base64') {
//写二进制文件
//filename:支持"/"做分隔符
//callback:function(err)
//datastr:base64字符串
var data = '';
data += 'type=base64&';
data += 'name=' + filename;
data += '&value=' + datastr;
postsomething(data, '/writeFile', callback);
return;
}
throw 'Type Error in fs.writeFile';
}
fs.writeMultiFiles = function (filenames, datastrs, callback) {
postsomething('name=' + filenames.join(';') + '&value=' + datastrs.join(';'), '/writeMultiFiles', callback);
}
fs.readdir = function (path, callback) {
//callback:function(err, data)
//path:支持"/"做分隔符,不以"/"结尾
//data:[filename1,filename2,..] filename是字符串,只包含文件不包含目录
if (typeof (path) != typeof (''))
throw 'Type Error in fs.readdir';
var data = '';
data += 'name=' + path;
postsomething(data, '/listFile', function (err, data) {
try {
data = JSON.parse(data);
} catch (e) {
err = "Invalid /listFile";
data = null;
}
callback(err, data);
});
return;
}
/**
* @param {string} path 支持"/"做分隔符
* @param {() => {err: string, data}} callback
*/
fs.mkdir = function (path, callback) {
//callback:function(err, data)
if (typeof (path) != typeof (''))
throw 'Type Error in fs.readdir';
var data = '';
data += 'name=' + path;
postsomething(data, '/makeDir', callback);
return;
}
/**
* @param {string} path 支持"/"做分隔符, 不以"/"结尾
* @param {() => {err: string, data}} callback
*/
fs.moveFile = function (src, dest, callback) {
if (typeof (src) != typeof ('') || typeof (dest) != typeof (''))
throw 'Type Error in fs.readdir';
var data = '';
data += 'src=' + src + "&dest=" + dest;
postsomething(data, '/moveFile', callback);
return;
}
/**
* @param {string} path 支持"/"做分隔符, 不以"/"结尾
* @param {() => {err: string, data}} callback
*/
fs.deleteFile = function (path, callback) {
if (typeof (path) != typeof (''))
throw 'Type Error in fs.readdir';
var data = '';
data += 'name=' + path;
postsomething(data, '/deleteFile', callback);
return;
}
})();

70
fsTest_cs.html Normal file
View File

@ -0,0 +1,70 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="./fs.js"></script>
<script>
fs.writeFile('_test.txt', '123中a文bc', 'utf-8', function (e, d) {
console.log(e, d);
})
setTimeout(function () {
fs.writeFile('_test_bin.txt', 'abc=', 'base64', function (e, d) {
console.log(e, d);
})
}, 1000);
setTimeout(function () {
fs.readFile('_test.txt', 'utf-8', function (e, d) {
console.log(e, d);
})
}, 2000);
setTimeout(function () {
fs.readFile('_test_bin.txt', 'base64', function (e, d) {
console.log(e, d);
})
}, 3000);
setTimeout(function () {
fs.readdir('.', function (e, d) {
console.log(e, d);
})
}, 4000);
setTimeout(function () {
fs.writeMultiFiles(['_test.txt','_test_multi.txt'], ['abc=','abe='], function (e, d) {
console.log(e, d);
})
}, 5000);
setTimeout(function () {
fs.mkdir('__test__', function (e, d) {
console.log(e, d);
})
}, 6000);
setTimeout(function () {
fs.moveFile('_test_bin.txt', '__test__/_test_bin.txt', function (e, d) {
console.log(e, d);
})
}, 7000);
setTimeout(function () {
fs.moveFile('_test.txt', '__test__/_test.txt', function (e, d) {
console.log(e, d);
})
}, 8000);
setTimeout(function () {
fs.moveFile('_test_multi.txt', '__test__/_test.txt', function (e, d) {
console.log(e, d);
})
}, 8000);
setTimeout(function () {
fs.deleteFile('__test__/_test_bin.txt', function (e, d) {
console.log(e, d);
})
}, 9000);
setTimeout(function () {
fs.deleteFile('__test__', function (e, d) {
console.log(e, d);
})
}, 10000);
</script>
</body>
</html>

162
function.d.ts vendored Normal file
View File

@ -0,0 +1,162 @@
interface ActionData {
/**
* @deprecated
*
* @param keyCode keyCode
* @param altKey alt键
*/
onKeyUp(keyCode: number, altKey: boolean): boolean;
}
interface ControlData {
/**
*
*/
saveData(): Save;
/**
*
* @param data
* @param callback
*/
loadData(data: Save, callback?: () => void): void;
/**
*
*/
updateStatusBar(): void;
/**
*
* @param callback
*/
moveOneStep(callback?: () => void): void;
/**
*
* @param x
* @param y
* @param ignoreSteps
*/
moveDirectly(x: number, y: number, ignoreSteps?: number): boolean;
}
interface UiData {
/**
*
*/
drawStatistics(): AllIdsOf<'items'>[];
}
interface EventData {
/**
*
* @param hero
* @param hard
* @param floorId
* @param maps
* @param values
*/
resetGame(
hero: HeroStatus,
hard: string,
floorId: FloorIds,
maps: GameStatus['maps'],
values: Partial<CoreValues>
): void;
/**
*
* @param reason
* @param norank
* @param noexit 退
*/
win(reason: string, norank?: boolean, noexit?: boolean): void;
/**
*
* @param reason
*/
lose(reason?: string): void;
/**
*
* @param floorId
* @param heroLoc
*/
changingFloor(floorId: FloorIds, heroLoc: Loc): void;
/**
*
* @param floorId
*/
afterChangeFloor(floorId: FloorIds): void;
/**
*
* @param toId
* @param callback
*/
flyTo(toId: FloorIds, callback?: () => void): boolean;
/**
*
* @param enemyId
* @param x
* @param y
*/
afterBattle(enemyId: any, x?: number, y?: number): void;
/**
*
* @param doorId id
* @param x
* @param y
*/
afterOpenDoor(
doorId: AllIdsOf<Exclude<Cls, 'enemys' | 'enemy48'>>,
x: number,
y: number
): void;
/**
*
* @param itemId id
* @param x
* @param y
* @param isGentleClick
*/
afterGetItem(
itemId: AllIdsOf<'items'>,
x: number,
y: number,
isGentleClick?: boolean
): void;
/**
*
*/
afterPushBox(): void;
}
interface FunctionsData {
/**
*
*/
actions: ActionData;
/**
*
*/
control: ControlData;
/**
* ui信息
*/
ui: UiData;
/**
*
*/
events: EventData;
}

2165
functions.js Normal file

File diff suppressed because it is too large Load Diff

77
icon.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
type IconIds =
| keyof MaterialIcon['animates']
| keyof MaterialIcon['autotile']
| keyof MaterialIcon['enemy48']
| keyof MaterialIcon['enemys']
| keyof MaterialIcon['hero']
| keyof MaterialIcon['items']
| keyof MaterialIcon['items']
| keyof MaterialIcon['npc48']
| keyof MaterialIcon['npcs']
| keyof MaterialIcon['terrains'];
interface IconOffsetInfo {
/**
* id
*/
image: string;
/**
*
*/
x: number;
/**
*
*/
y: number;
}
/**
*
*/
interface Icons {
/**
*
*/
readonly icons: MaterialIcon;
/**
*
*/
readonly tilesetStartOffset: 10000;
/**
* id
*/
readonly allIconIds: IconIds;
/**
*
*/
getIcons(): MaterialIcon;
/**
* ID获得图块类型
*/
getClsFromId<T extends AllIds>(id: T): ClsOf<T>;
/**
* ID
*/
getAllIconIds(): IconIds;
/**
* ID获得所在的tileset和坐标信息
* @param id id
*/
getTilesetOffset(id: string | number): IconOffsetInfo | null;
/**
*
* @param cls
*/
getAnimateFrames<T extends Cls>(cls: T): FrameOf<T>;
}
declare const icons: new () => Icons;

1046
icons.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,208 +1,197 @@
<!DOCTYPE html>
<html>
<html lang="ch_ZN">
<head>
<meta http-equiv='content-type' content='text/html' charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=Edge, chrome=1'>
<meta name='author' content='ckcz123'>
<meta name='viewport'
content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=yes'>
<title>HTML5魔塔</title>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="screen-orientation" content="portrait">
<meta name="full-screen" content="yes">
<meta name="browsermode" content="application">
<meta name="x5-orientation" content="portrait">
<meta name="x5-fullscreen" content="true">
<meta name="x5-page-mode" content="app">
<link type='text/css' href='styles.css' rel='stylesheet'>
<meta charset="UTF-8">
<title>HTML5魔塔样板</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link href="vue.css" rel="stylesheet">
<script>
//先下载着
/*
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://cdn.bootcss.com/docsify/4.5.5/docsify.min.js', true);
xhr.send(null);
xhr = new XMLHttpRequest();
xhr.open('GET', 'https://cdn.bootcss.com/docsify/4.5.5/plugins/search.min.js', true);
xhr.send(null);
(function(){
window.bg={replaceToken:{}}
bg.guid=function () {
return 'id_' + 'xxxxxxxx_xxxx_4xxx_yxxx_xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
bg.table=function(ths,tdss,args){
return `<table class="${((args||{}).class||[]).join(' ')}"><thead><tr>${ths.map(v=>`<th style="text-align:left">${v}</th>`).join('')}</tr></thead><tbody>${tdss.map(tds=>`<tr>${tds.map(v=>`<td style="text-align:left">${v}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
}
bg.pattern=/```\s*MotaAction.*?\r?\n[^]*?\r?\n\s*```/g;
bg.replaceFunc=function(str){
var content=null;
try {
content=eval(str.split('\n').slice(1,-1).join('\n'))
} catch (ee) {
}
var match=/```\s*MotaAction(\.)?(\w+)?/.exec(str);
var blocksvg=''
if (!match[2] || match[2]=='action')blocksvg= bg.parseList(content);
else blocksvg=bg.parse(content,match[2]);
return '<p>'+blocksvg+'</p>\n\n';
}
bg.parse=function(obj,type){
MotaActionFunctions.workspace().clear();
xml_text = MotaActionFunctions.actionParser.parse(obj,type||'event');
xml = Blockly.Xml.textToDom('<xml>'+xml_text+'</xml>');
Blockly.Xml.domToWorkspace(xml, MotaActionFunctions.workspace());
svgBlock=document.querySelector("g.blocklyBlockCanvas > g.blocklyDraggable")
svgBlock.setAttribute('transform','translate(0,0)')
tmp={width:svgBlock.getBBox().width,height:svgBlock.getBBox().height}
var id=`<span>${bg.guid()}</span>`
bg.replaceToken[id]=`<svg width="${tmp.width}px" height="${tmp.height}px">`+svgBlock.outerHTML+'</svg>'
return id;
}
bg.parseList=function(obj){
MotaActionFunctions.workspace().clear();
xml_text = MotaActionFunctions.actionParser.parseList(obj);
xml = Blockly.Xml.textToDom('<xml>'+xml_text+'</xml>');
Blockly.Xml.domToWorkspace(xml, MotaActionFunctions.workspace());
svgBlock=document.querySelector("g.blocklyBlockCanvas > g.blocklyDraggable")
svgBlock.setAttribute('transform','translate(0,0)')
tmp={width:svgBlock.getBBox().width,height:svgBlock.getBBox().height}
var id=`<span>${bg.guid()}</span>`
bg.replaceToken[id]=`<svg width="${tmp.width}px" height="${tmp.height}px">`+svgBlock.outerHTML+'</svg>'
return id;
}
})()
*/
</script>
</head>
<body>
<!-- <div id="stars"></div>
<div id="stars2"></div>
<div id="stars3"></div> -->
<div id='startImageBackgroundDiv'>
<div id='startImageDiv'></div>
<img id='startImageLogo' />
</div>
<div id="app"></div>
<xml id="toolbox" style="display:none"></xml>
<div id="blocklyArea" style="opacity: 0;z-index: -1;"><div id="blocklyDiv"></div></div>
<textarea id="codeArea" style="display:none" spellcheck="false"></textarea>
<script>
(function () {
var startImageBackgroundDiv = document.getElementById('startImageBackgroundDiv');
var startImageLogo = document.getElementById('startImageLogo');
var startImageDiv = document.getElementById('startImageDiv');
startImageLogo.onload = function () {
startImageBackgroundDiv.style.display = 'block';
var onAnimationEnd = function () {
startImageBackgroundDiv.style.display = 'none';
startImageLogo.classList.remove("startImageAnimation");
startImageDiv.classList.remove("startImageDivAnimation");
}
startImageDiv.addEventListener("webkitAnimationEnd", onAnimationEnd);
startImageDiv.addEventListener("animationend", onAnimationEnd);
startImageLogo.classList.add("startImageAnimation");
startImageDiv.classList.add("startImageDivAnimation");
// 注释下面这句话以禁止单击立刻跳过开场动画
startImageBackgroundDiv.onclick = onAnimationEnd;
}
startImageLogo.onerror = function () { }
startImageLogo.src = "logo.png";
})();
</script>
<!-- injection -->
<div id='gameGroup'>
<p id='mainTips'>请稍候...</p>
<img id='musicBtn'>
<div id='startPanel'>
<div id='startTop'>
<div id='startTopProgressBar'>
<div id='startTopProgress'></div>
</div>
<p id='startTopLoadTips'>资源即将开始加载</p>
<p id='startTopHint'>HTML5魔塔游戏平台享受更多魔塔游戏<br />https://h5mota.com/</p>
</div>
<img id='startBackground'>
<p id='startLogo'></p>
<div id='startButtonGroup'>
<div id='startButtons'>
<span class='startButton' id='playGame'>开始游戏</span>
<span class='startButton' id='loadGame'>载入游戏</span>
<span class='startButton' id='replayGame'>录像回放</span>
</div>
<div id='levelChooseButtons'></div>
</div>
</div>
<div id='floorMsgGroup'>
<p id='logoLabel'></p>
<p id='versionLabel'></p>
<p id='floorNameLabel'></p>
</div>
<div id='statusBar' class="clearfix">
<div class="status" id="floorCol">
<img id="img-floor">
<p class='statusLabel statusText' id='floor'></p>
</div>
<div class="status" id="nameCol">
<img id="img-name">
<p class='statusLabel statusText' id='name'></p>
</div>
<div class="status" id="lvCol">
<img id="img-lv">
<p class='statusLabel statusText' id='lv'></p>
</div>
<div class="status" id='hpmaxCol'>
<img id="img-hpmax">
<p class='statusLabel statusText' id='hpmax'></p>
</div>
<div class="status" id='hpCol'>
<img id="img-hp">
<p class='statusLabel statusText' id='hp'></p>
</div>
<div class="status" id='manaCol'>
<img id="img-mana">
<p class='statusLabel statusText' id='mana'></p>
</div>
<div class="status" id='atkCol'>
<img id="img-atk">
<p class='statusLabel statusText' id='atk'></p>
</div>
<div class="status" id='defCol'>
<img id="img-def">
<p class='statusLabel statusText' id='def'></p>
</div>
<div class="status" id="mdefCol">
<img id="img-mdef">
<p class='statusLabel statusText' id='mdef'></p>
</div>
<div class="status" id="moneyCol">
<img id="img-money">
<p class='statusLabel statusText' id='money'></p>
</div>
<div class="status" id="expCol">
<img id="img-exp">
<p class='statusLabel statusText' id='exp'></p>
</div>
<div class="status" id="upCol">
<img id="img-up">
<p class='statusLabel statusText' id='up'></p>
</div>
<div class="status" id="skillCol">
<img id="img-skill">
<p class='statusLabel statusText' id='skill' style='font-style: normal'></p>
</div>
<div class="status" id='keyCol'>
<span class='statusLabel' id='yellowKey' style="color:#FFCCAA"></span>
<span class='statusLabel' id='blueKey' style="color:#AAAADD"></span>
<span class='statusLabel' id='redKey' style="color:#FF8888"></span>
<span class='statusLabel' id='greenKey' style="color:#88FF88"></span>
</div>
<div class="status" id='pzfCol'>
<span class='statusLabel' id='pickaxe' style="color: #BC6E27"></span>
<span class='statusLabel' id='bomb' style="color: #FA14B9"></span>
<span class='statusLabel' id='fly' style="color: #8DB600"></span>
</div>
<div class="status" id="debuffCol">
<span class='statusLabel' id='poison' style="color: #AFFCA8;"></span>
<span class='statusLabel' id='weak' style="color: #FECCD0;"></span>
<span class='statusLabel' id='curse' style="color: #C2F4E7;"></span>
</div>
window.$docsify = {
homepage: 'index.md',
loadSidebar: true,
name: 'HTML5魔塔样板',
repo: 'https://github.com/ckcz123/mota-js',
// basepath: '../docs/',
<!-- 状态栏canvas化 -->
<canvas id="statusCanvas" style="position: absolute; left: 0; top: 0;"></canvas>
</div>
<div id="toolBar" class="clearfix">
<img class="tools" id='img-book'>
<img class="tools" id='img-fly'>
<img class="tools" id='img-toolbox'>
<img class="tools" id='img-keyboard'>
<img class="tools" id='img-shop'>
<img class="tools" id='img-save'>
<img class="tools" id='img-load'>
<img class="tools" id='img-settings'>
<img class="tools" id='img-btn1' style='display:none'>
<img class="tools" id='img-btn2' style='display:none'>
<img class="tools" id='img-btn3' style='display:none'>
<img class="tools" id='img-btn4' style='display:none'>
<img class="tools" id='img-btn5' style='display:none'>
<img class="tools" id='img-btn6' style='display:none'>
<img class="tools" id='img-btn7' style='display:none'>
<img class="tools" id='img-btn8' style='display:none'>
<p class="statusLabel tools" id="hard"></p>
</div>
<div id="gameDraw">
<div id="gif"></div>
<div id="gif2"></div>
<canvas class='gameCanvas anti-aliasing' id='bg'></canvas>
<canvas class='gameCanvas anti-aliasing' id='event'></canvas>
<canvas class='gameCanvas anti-aliasing' id='hero'></canvas>
<canvas class='gameCanvas anti-aliasing' id='event2'></canvas>
<canvas class='gameCanvas anti-aliasing' id='fg'></canvas>
<canvas class='gameCanvas' id='damage'></canvas>
<canvas class='gameCanvas' id='animate'></canvas>
<canvas class='gameCanvas' id='curtain'></canvas>
<canvas class='gameCanvas' id='ui'></canvas>
<canvas class='gameCanvas' id='data'>此浏览器不支持HTML5</canvas>
<div id="next"></div>
</div>
</div>
<div id='inputDiv'>
<div id='inputDialog'>
<p id="inputMessage">请输入文字...</p>
<input id='inputBox' type="text" autocomplete="off" />
<button id='inputYes'>确定</button>
<button id='inputNo'>取消</button>
</div>
</div>
<div id="ui-editor"></div>
<!-- injection -->
<script src='libs/thirdparty/lz-string.min.js'></script>
<script src='libs/thirdparty/priority-queue.min.js'></script>
<script src='libs/thirdparty/localforage.min.js'></script>
<script src='libs/thirdparty/zip.min.js'></script>
<script id='mainScript' src='main.js'></script>
<script>main.init('play'); main.listen();</script>
<script>
// Search Support
search: {
maxAge: 43200000, // 过期时间,单位毫秒,默认一天
paths: 'auto',
placeholder: {
'/': '搜索文档...',
},
noData: {
'/': '找不到结果',
},
},
// load sidebar from _sidebar.md
loadSidebar: '_sidebar',
subMaxLevel: 2,
autoHeader: true,
auto2top: true,
mergeNavbar: true,
formatUpdated: '{YYYY}-{MM}-{DD} {HH}:{mm}:{ss}',
plugins: [
/*
function(hook){
var renderScriptNode=function(str){
return str.replace(/```.*?\r?\n['"]run['"];[^]*?\r?\n```/g,function(x){
return eval(`(function(){${x.replace(/```.*?\r?\n['"]run['"];/,'').slice(0,-3)}})()`)
})
}
var renderMotaAction=function(str){
return str.replace(bg.pattern,function(x){
return bg.replaceFunc(x)
})
}
hook.beforeEach(function(content){
return renderMotaAction(renderScriptNode(
content
))
})
hook.doneEach(function(){
var map=bg.replaceToken
var node=document.querySelector('.markdown-section')
var str=node.innerHTML
for(var id in map){
str=str.replace(id,map[id])
}
node.innerHTML=str
})
}
*/
]
}
</script>
<!-- 为了保证时序用脚本加载这两个 -->
<script src="docsify.min.js"></script>
<script src="search.min.js"></script>
<!--
<script src="../_server/blockly/Converter.bundle.min.js"></script>
<script src="../_server/blockly/blockly_compressed.js"></script>
<script src="../_server/blockly/blocks_compressed.js"></script>
<script src="../_server/blockly/javascript_compressed.js"></script>
<script src="../_server/blockly/zh-hans.js"></script>
<script src='../_server/MotaActionParser.js'></script>
<script>
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState != 4) return;
if (xhr.status != 200) {
alert("图块描述文件加载失败, 请在'启动服务.exe'中打开编辑器");
return;
}
var grammerFile = xhr.responseText;
converter = new Converter().init();
converter.generBlocks(grammerFile);
//printf(converter.blocks);
converter.renderGrammerName();
//converter.generToolbox();
converter.generMainFile();
//printf(converter.mainFile.join(''));
//console.log(converter);
var script = document.createElement('script');
script.innerHTML = converter.mainFile[5] + converter.mainFile[6];
window.core={material:{items:[],enemys:[]}}
document.body.appendChild(script);
MotaActionFunctions.disableReplace = true;
MotaActionFunctions.disableExpandCompare = true;
script = document.createElement('script');
script.src='https://cdn.bootcss.com/docsify/4.5.5/docsify.min.js'
document.body.appendChild(script);
script = document.createElement('script');
script.src='https://cdn.bootcss.com/docsify/4.5.5/plugins/search.min.js'
document.body.appendChild(script);
}
xhr.open('GET', '../_server/MotaAction.g4', true);
xhr.send(null);
</script> -->
</body>
</html>

25
index.md Normal file
View File

@ -0,0 +1,25 @@
# HTML5 魔塔样板说明文档
当您打开这份帮助文档的瞬间,相信您一定是抱着幼年时的游戏开发梦想前来的。众所周知,即时游戏的开发要比非即时游戏难上许多,像素级游戏的开发又要比网格地图游戏难上许多。
在非即时网格地图游戏譬如策略战棋有一类叫做“固定数值RPG”简称“魔塔”。这是一种基于运筹学的数学优化建模游戏虽然小众却不失有自己的圈子。
在当下魔塔的趋势是向移动端发展网络上也常常能见到“求手机魔塔”的提问。然而现有的工具中NekoRPG有着比较大的局限性游戏感较差更是完全没法在iOS运行。而一些APP的魔塔虽然可用但是必须要下载安装对于安卓和苹果还必须开发不同的版本非常麻烦。
但是现在我们有了HTML5。
HTML5的画布canvas以及它被Android/iOS内置浏览器所支持的特性可以让我们做出真正意义上的全平台覆盖的魔塔。
然而一般而言使用非RPG
Maker制作魔塔往往需要一定的编程技术HTML5魔塔自然也不例外。但是为了能让大家更加注重于“做塔”本身而不用考虑做塔以外的各种脚本问题@艾之葵GitHub
ckcz123特意制作了这样一部HTML5魔塔样板。
这个魔塔样板可以让你在完全不懂任何编程语言的情况下做出自己的H5魔塔。不会代码没关系只要你想做就能做出来
继续查看文档的详细介绍让你学会如何使用这一个样板来制作属于自己的HTML5魔塔或者……任何非即时的网格地图游戏。
* [新版视频教程](https://www.bilibili.com/video/BV1SB4y1p7bg?share_source=copy_web)
* [脚本教程](https://www.bilibili.com/video/BV1uL411J7yZ?share_source=copy_web)
==========================================================================================
[继续阅读下一章现在就做出自己的第一部H5魔塔](start)

656
instruction.md Normal file
View File

@ -0,0 +1,656 @@
# 事件指令
?> 在这一节中,让我们来了解每一类事件的具体介绍
本样板之所以敢宣称“零编程基础的您也能大展身手”就是因为它搭载了强大的图形化json编辑器blockly
熟悉Antlr语法的读者可以根据[修改编辑器](editor)去自行修改`_server\MotaAction.g4`等文件去扩展其功能。
json代码本身则可以作为`core.insertAction()`函数的自变量,去插入一段事件执行。
下述提到的“当前点”坐标均指`core.status.event.data.x`和`core.status.event.data.y`。
## 指令的分类(注意区分块的颜色和地图的七彩点)
尽管事件指令和`core.events._action_xxx()`函数是一一对应的但它们进一步调用的底层函数却分布在libs文件夹的不同文件中大致上
* 显示文字类严格来说需要玩家操作的事件还涉及到actions.js和UI绘制类在ui.js
* 数据相关类这类也有不少道具相关在items.js和特效声音类在control.js
* 地图处理类在maps.js
* 事件控制类或许应该叫流程控制类则在events.js请注意区分libs和project的同名文件。
另一种分类方法则是按照同步和异步,分成以下几类:
1. **瞬间就能执行完的:**如UI绘制、设置XX属性、显隐和转变图层块等。
2. **阻塞直到玩家操作的:**如显示文章/选择项/确认框、接受用户输入、等待用户操作、呼出xxx等。
3. **阻塞一段固定时间的:**如开关门、显示动画(观赏性)、移动跳跃、淡入淡出等。
4. **耗时但不阻塞的:**如播放音效V2.8支持阻塞)、显示提示等,一般为纯观赏性指令,会和后面的指令同时执行。
上述第3类指令都可以勾选“不等待执行完毕”即前面提到的异步事件变为第4类从而实现诸如同步开关多个门的效果。
在json区每条指令的格式为`{"type": "xxx", "param1": ..., "param2": ..., ......}`
实际执行的函数为`core.events._action_xxx(data, x, y, prefix)`
data为整个指令对象x和y为当前点坐标prefix为独立开关的楼层前缀。
您可以自行使用`core.registerEvent`注册一个新的事件。如果需要把新指令做成像已有的指令一样有类别、名称、取色器、勾选框、下拉框、输入框等部件,请查阅[修改编辑器](editor)。
V2.8.1起下拉框中没有的项都可以通过在json区输入并点击“解析”按钮来临时追加刷新网页后失效如需永久追加请查阅[修改编辑器](editor)。
与此同时,显示文章、显示选择项、显示确认框都支持双击预览,预览前请先摆一个“设置剧情文本的属性”设置您预览时需要的属性然后双击它。
另外原本“显示文字类”的图片相关指令和“特效声音类”的音频相关指令在V2.8.1被移出来合并到了一个新类“音像处理类”,请知悉。
## 显示文字类(黄色)
![image](img/images_texty.jpg)
这个类别的指令会负责UI层图文的处理如图片的移动和淡入淡出游戏的胜败和重启等。
### 显示文章
最基本的就是最灵活的。本指令的讲解将占用大量篇幅,请做好准备。
上述函数中,第一个自变量为字符串数组或单个字符串,每个字符串为一段需要玩家按下确认键或点击屏幕才会消失的剧情文本,第二个自变量(可选)为全部文本消失后的回调函数。
每条显示文章分为五部分:标题、图像、对话框效果、正文、立绘。
写成一个字符串就是`\t[标题,图像]\b[对话框效果]\f[立绘]正文`。
1. **标题:**可选一般填说话人的名字。如果不填则尝试根据图像取中文名道具除外道具不会说话所以只填图像不填标题就会没有标题。如图像填hero但不填标题则以勇士名字作为标题标题还可以填`null`强制不显示标题(如`\t[null,hero]`只显示勇士头像)。
2. **图像:**可选可以填hero勇士行走图如果勇士开了帧动画则会取当前朝向但朝上会视为朝下或任何图块ID或者填this来尝试取当前点图块。
* 也可以填一个png格式的图片文件名需要后缀图像甚至还可以填null来避免以图块ID为标题被解释成图像如`\t[bomb,null]`会以英文单词bomb为标题而没有图像但单独的`\t[bomb]`则会没有标题但以炸弹道具为图像)。
* 只填写图像而不填写标题时,会被优先解析到标题框中,请不要惊慌,这并不影响效果。
3. **对话框效果:**可选,填法非常灵活,如下(坐标在大地图中均为绝对坐标,省略则取当前点)。
1. `up,x,y`:对话框显示在点(x,y)上方尖角朝下指着这个点具体指的高度取决于图像。没有图像则判断该点是32×32px还是32×48px的图块但是不管有无图像若该点没有图块则都没有尖角
2. `down,x,y`:对话框显示在点(x,y)下方,尖角朝上指着这个点。比起上面,这个没有高度问题,不过该点没有图块的话则还是没有尖角。
* 上述两种写法中如果把坐标换成hero则显示在勇士上方或下方尖角朝下的话高度取决于勇士的行走图
3. `this,x,y`:在大地图中,点(x,y)可能位于视野上半部分也可能位于下半部分写this就能让对话框自动适配上下来防止越界。
4. `hero`在大地图上下边缘或小地图勇士可能位于视野上半部分也可能位于下半部分只写hero也能自动适配。
5. `hero,n`n为正整数尖角指向勇士的第n名跟随者自动适配上下将hero改为up或down则手动指定上下。
6. `up,null`显示在屏幕最上方同理up换成down则为最下方。
7. `center`:强制显示在屏幕中央,宽度为视野宽度。
8. 除最后两种外,其余写法都会给对话框进行宽度自适配:
* 如果正文没有自动换行,则会先尝试取一个让文本总行数最接近“几行半”的宽度,可能会再适当加宽来防止标题出界。
* 如果正文有自动换行,则会尝试连同标题在内取最长的一行作为宽度。
* V2.7起,文本绘制一定程度上支持了排版标点禁则,成对符号的左半部分不会出现在行尾,右半部分和其他某些标点不会出现在行首。
9. 最终绘制时会尽量让尖角居中,除非尖角所指的点实在太靠左或太靠右。
10. 值得注意的是,使用`project/images/winskin.png`或类似的图片作为文章背景时尖角的绘制用的是用图片右下角64×32px的两格进行的所以往往需要您自己准备好。
* 技术群`959329661`的群文件“常用素材”提供了一些已经制作好的这样的图片,敬请取用。
4. **正文:**双击指令块,进入多行编辑。正文中允许使用很多转义序列,当您键入一个\字符时就会提示补全,后面逐一介绍。
5. **立绘:**显示文章的同时可以绘制一张或多张立绘图,请双击预览各张图的效果或右击整个指令预览所有立绘。每张立绘由一大堆参数组成:`\f[name(:x/:y/:o,sx,sy,sw,sh,)x,y(,w,h,alpha,angle)]`
1. **文件名:**需要放在project\images文件夹中并注册这里需要带后缀。
2. **翻转:**和楼层贴图一样支持三种翻转在json代码中以文件的后缀名之后追加“:x、:y、:o”来表示。
3. **绘制坐标:**立绘在视野中的左上角像素坐标,后面的参数一旦省略其中一个则必须省略其后所有。
4. **绘制的宽高:**立绘被伸缩后在视野中实际显示的宽高,必须同时填写,不填则不伸缩。
5. **裁剪坐标和宽高:**必须同时填写,为从原图中裁剪区域的左上角坐标和区域宽高,不填则取全图。
6. **不透明度和旋转角度:**可选前者填一个不大于1的正数请自行双击预览。
6. **左上角坐标、限宽:**V2.8.1新增,该项不能和上面的`\b[对话框效果]`同时使用。
* 使用此项后本指令将变为正常的json格式而不是字符串格式。
* “左上角坐标”指视野中的相对像素坐标,“限宽”必须和左上角坐标一起指定,不能单独指定。
* “限宽”的下限在没有图像的情况下大约为64px在有图像的情况下大约为128px可以双击预览来确认到底能否正常显示。
* 一种推荐的用法是在指定下面的非0编号后使用然后“显示选择项/确认框”这样就和RPG Maker的行为一致了。
* 而且还能充分利用两侧的空间讲清楚每个子选项,这是在“选择项的提示文字无法随光标位置而变化”的现状下的一种妥协做法。
7. **编号:**V2.8.1新增,可以同时显示多个不同编号的对话框,常用来表现多名角色的嘈杂对话。
* 和上一项一样此项填写非0值后指令将变为正常的json格式而不是字符串格式。
* 非0编号的对话框将不会自动消失甚至勇士恢复自由行动后也是可以被同编号的对话框覆盖或手动清除。
* 自由行动时,该项常用来做一些常驻提示,如技能的开关状态、毒衰咒状态、当前剧情任务进度等。
* 您甚至可以像图片一样去移动对话框如和一个npc一起移动支持四种变速效果立绘也会被一起移动。
立绘是画在UI层的下一个指令执行前就会擦除。如需持续显示请使用“显示图片”指令另外立绘会被“图像”遮挡。
### 显示文章正文的转义序列
1. **表达式计算:**使用`${}`可以计算eval一个js表达式式子中允许使用所有的冒号缩写量和API详见`core.calValue()`函数。
* 此语法也可以用于“道具名称”、“道具描述”和“即捡即用提示”,只不过那里就不支持中文替换了。
* 如`勇士当前的攻防相乘是\${状态:攻击\*状态:防御}`(中文替换),`持有三色钥匙共\${item:yellowKey+item:blueKey+item:redKey}把。`json
* V2.8和更早的版本中,样板对右花括弧的处理是(非贪心)正则匹配,因此`${内部}`不能再出现右花括弧,这也意味着您无法使用对象、复杂的流程控制语句和函数定义,只能使用简单的运算符(包括三元运算)和函数调用。
* V2.8.1中,匹配方式改为了堆栈匹配,上述问题得到解决,您可以放心使用匿名函数了,这对道具名称/道具描述这种场合是个福音。
2. **局部文字变色:**使用`\r[颜色英文名]`或`\r[\#RrGgBb]`(十六进制)来将这之后的文本变为另一种颜色。
* 最常用的17种颜色提供了自动补全十六进制颜色可以随便找个有颜色参数的指令呼出调色器去自己调配。只使用\r不带方括号则变回默认颜色。
3. **局部字号调节:**使用`\\c[正整数]`改变这之后文本的字号,只使用`\\c`不加方括号则恢复默认字号。
4. **手动换行、局部加粗和斜体:**退出多行编辑后,手动换行写作`\n`,另外可以使用`\\d`将局部文本加粗或取消加粗,使用`\\e`将局部文本变为斜体或取消斜体。
5. **32×32px图标的绘制**使用`\\i[图标ID]`绘制一个32×32px的图块的第一帧或系统图标您可以使用`core.statusBar.icons`查看所有的系统图标。
* 出于历史遗留问题图块ID可以和系统图标ID重复此时优先使用图块ID.
6. **打字速度调节:**开启打字机效果后,文本的打字速度总是匀速的。所以样板提供了名为“时间占位符”的转义序列,使用`\\z[正整数]`可以暂停相当于打这么多个字的时间。
除`\n,\t,\b,\r,\f`外其余转义序列的反斜杠在json中必须写两个
### 其他文字类指令
1. **自动剧情文本:**和上面的显示文章基本相同,只不过不是由玩家按下确认键或点击屏幕,而是一定毫秒后自动消失,录像回放中则忽略。
* 比起那个这个不能通过长按Ctrl键或长按屏幕快进大量使用时一般用来搭配语音。否则对魔塔这种快餐游戏来说可能会非常不友好建议统一塞进“显示确认框”指令的场合之一。
2. **滚动剧情文本:**将一段文字从屏幕最下方滚动到最上方不支持自动换行常用于op和ed.
* 该指令对应`core.drawScrollText(content, time, lineHeight, callback)`。
3. **显示提示:**即诸如打败怪物、捡到道具、打不开门时左上角的提示,只支持`${表达式计算}`和`\r[变色]`。
* 可以填写一个图标ID显示在提示文本的左侧支持32×48px但只画靠上的2/3也可以使用系统图标
* 此指令对应`core.drawTip(text, icon, frame)`函数。自然数`frame`表示绘制第几帧默认为0表示第一帧但没有在事件中提供
* 此指令看似显示出的提示会过一会才消失,但此指令本身依然是瞬间完成的,不属于异步事件!
* V2.6.4短暂提供了“同时显示多个tip”的功能这在事件中连续获得多个道具时很友好
* 但是玩家纷纷表示自由行动时此功能遮挡了太多的视野因而在V2.6.5中就取消了。
* 如果需要此功能(包括自定义底色),请在本项目的[github](https://github.com/ckcz123/mota-js/archive/refs/tags/v2.6.4-release.zip)站点自行下载V2.6.4的发布版本并对照研究。
4. **游戏胜败和重启:**游戏胜败分别对应“脚本编辑”快捷键N的`win`和`lose`函数,在线游戏排行榜中每个结局的每个难度都有一张榜。
* 但同一结局只有最高难度有效您可以勾选“不计入榜单”来让这个本来有效的结局也无效。还可以勾选“不结束游戏”来先停止录像的录制再演出ed.
* win和lose函数最终都会调用`core.gameOver(ending)`函数区别在于lose不填写ending. 但是事件中使用gameOver这一异步函数需要额外的技巧详见“原生脚本”。
* 重启游戏对应的函数为`core.showStartAnimate()`
5. **设置剧情文本的属性:**可用`core.status.textAttribute`获取当前的剧情文本属性,各项含义:
1. **位置:**“显示文章”不使用`\b`对话框效果时文本的位置,默认为屏幕中央。如果您有大量集中的剧情文本都欲使用`up,null`的对话框效果,则可以直接将此项设置为“顶部”,而将剩余的个别剧情文本使用`center`或`down,null`的对话框效果,反之亦然。
2. **偏移像素:**上面的“位置”选择“顶部”或“底部”时和屏幕上下边缘的距离,也作为滚动剧情文本和左边缘的距离。
3. **对齐:**默认为左对齐,可以修改此项来让显示文章的标题和正文都居中或都右对齐。
4. **标题色:**准确地说是“标题和图像边框色”,格式和楼层画面色调一致,可以点击调色器按钮呼出调色器调色。
5. **正文色:**如题,修改方法同上,从而避免频繁使用\r进行局部文本变色。
6. **背景色:**如题,修改方法同上。但比起前两个,这个也允许填写一个类似`winskin.png`的图片文件名。
7. **标题和正文字号:**如题,正文字号推荐设置为偶数。
8. **行距和字符间距:**如题,单位都是像素,行距推荐为正文字号的一倍半。
9. **粗体Y/N**文本是否默认加粗,推荐在大量粗体文本中穿插少量细体文本时使用,以免频繁的`\\d`切换。
10. **打字间隔:**0表示不启用打字机效果而是一次显示完正整数表示每打一个字的毫秒数也作为`\\z`暂停的单位时间。
11. **淡入淡出时间:**V2.8新增,指定此项后,每个“显示文章”指令都会增加淡入淡出效果,建议用于大段的剧情层。
可以使用`core.clone(core.status.textAttribute)`将文本属性备份到`hero.flags`中,从而做到临时使用另一套文本属性绘制部分内容。
### 图片类指令
1. **显示图片:**和立绘的语法基本类似只不过多了编号1—50和淡入时间。
* 可以双击预览效果,还可以勾选“不等待执行完毕”来和后面的指令同时执行,比如同时淡入两张图片,或者淡入一张同时淡出/移动另一张。
* 编号较大的图片会遮盖较小的1—24号图片会被色调层遮盖25—40号图片会遮盖色调层但被UI层遮盖41—50号图片会遮盖UI层。
* 此指令对应`core.showImage()`函数,编号真正的意义,详见[个性化](personalization)
2. **清除图片:**如题需要指定要清除的图片编号和淡出时间显隐图片的时间都可以填0表示瞬间完成
* 此指令对应`core.hideImage(code, time, callback)`
3. **图片移动:**其实还包括了透明度渐变,“终点像素位置”指移动结束后的图片在视野中的左上角像素坐标(不填则表示单纯的透明度渐变),“不透明度”指渐变结束后的新的不透明度(不填表示单纯的移动)。对应`core.moveImage(code, to, opacityVal, moveMode, time, callback)`
* V2.8起,图片和视野的移动支持加速度,分为“匀速、加速、减速、先加速再减速”四种,请任意选用。
4. **图片旋转:**V2.8新增,同样支持加速度,旋转中心坐标不填则取图片中心。
* 此指令对应`core.rotateImage(code, center, angle, moveMode, time, callback)`函数。
* 比起移动,旋转本身不支持同时透明度渐变,您可以先写一个不指定终点的移动指令且“不等待执行完毕”来实现单纯的淡入淡出,然后再写一个耗时相同或略长的旋转指令,这样两个指令就会一起执行了。
* 当不指定旋转中心时,本指令可以和移动指令同时使用,从而得到“图片的中心做直线运动、同时图片还在绕着中心自转”的效果。
5. **图片放缩:**V2.8.1新增,同样支持加速度,放缩中心坐标不填则取图片中心。
* 此指令对应`core.scaleImage(code, center, scale, moveMode, time, callback)`函数。
* 可以和“图片移动/旋转”一起使用做出PowerPoint中常见的动画效果。
5. **显示或清除动图:**需要填写动图的文件名(带.gif后缀“起点像素位置”含义如前且必须填写可以双击指令块来预览第一帧的效果。
* 动图不支持淡入淡出和伸缩移动,如果不填任何参数则清除所有动图(只支持全部清除)。
* 该指令对应`core.showGif(name, x, y)`函数。
6. **显示图片化文本:**这是您唯一显示镜像文字的机会。
* 显示出来后就会视为一张通常的图片,可以被清除、移动、淡入淡出、旋转。
### 显示确认框选择项QTE与全局商店
QTE即快速反应事件。一般表现为需要玩家在收到某信号后尽快做出正确操作如新新魔塔2中面对白银史莱姆王的猜拳战斗就需要根据其出拳的颜色尽快按下相克的数字键。
样板同样支持这类事件,一共有三种,这里先介绍两种。
一是**显示确认框**,它会显示一段支持`${表达式求值}`但不支持自动换行、淡入淡出和其他转义序列的文字。然后要求玩家在一定毫秒数内选择“确定”或“取消”之一,如果超时就视为哪个都没选,直接继续执行后面的事件。
您可以指定闪烁光标的初始停留位置是确定还是取消还可以指定超时毫秒数为0表示不限时间但玩家必须做出二选一。
当指定了超时时间并且及时做出选择时剩余时间会被写入“变量timeout”可以用来做一些处理音游
此指令对应`core.drawConfirmBox(text, yesCallback, noCallback)`函数其中两个Callback分别为选择确定和取消后的回调函数。
V2.8起,显示确认框在录像回放时如果录像中没有记录该选哪一项(或者明明此事件未设置超时时间但录像中记录了超时),就会选择默认项(也就是光标的默认位置),请注意。
二是**显示选择项**和RPG Maker不同我们的选择项不会和它之前的“显示文章”同时出现可以直接配上除对话框、打字机、淡入淡出外的所有文字效果。
此指令对应`core.drawChoices(content, choices)`函数其中content为提示文字choices为各子选项文字组成的字符串数组。是的比起上面的函数这个不直接支持回调。
在没有提示文字的情况下一次能同时显示的子选项最多为13或15个。和确认框一样选择项的超时值填0表示不限时间但玩家必须从中选择一个。大于0的话超时视为什么都没选直接继续执行后面的事件。
每个子选项的文字只支持`${表达式求值}`和整行变色,请注意控制字数。文字左侧也支持追加一个图标(多帧图块取第一帧),支持系统图标。
每个子选项还可以指定“出现条件”(不指定则一定出现),条件的写法和自动事件的触发条件一样,从而做出形如“怒气值满才显示大招选项”的效果。
如果实际执行时所有子选项都不满足出现条件,则直接跳过。但是如果出现的项都不满足下面的“启用条件”就会导致所有项都不可选,然后游戏卡死,请务必注意这个问题。
V2.7.2起,每个子选项还可以指定“启用条件”,当出现条件和启用条件都满足时才能真正选中这一项。
如果只满足前者但不满足后者,该项会变灰,尝试选择时会播放“操作失败”系统音效并显示提示(超时倒计时不会重置)。
当指定了超时时间并且及时做出有效选择时剩余时间同样会被写入“变量timeout”可以用来做一些处理。
您或许会疑惑提示文字为什么不做成随光标位置而变化(这在很多电脑/手柄游戏中很常见),这是因为触屏设备无法改变光标位置,如有需要,请自行在此界面提供虚拟方向键(不打算发布触屏版游戏的则无所谓),然后在提示文字中使用${}对`core.status.event.selection`进行判定,从而改变提示文字。
V2.8起显示选择项在录像回放时如果录像中没有记录该选哪一项或者明明此事件未设置超时时间但录像中记录了超时就会弹窗询问玩家是否补选一项此时玩家可以输入某项的序号从0起来修复录像当然也可以在弹窗时点取消来放弃修复。
提交成绩后,站点后端的录像验证时如果发生了同样的问题,则因为无法询问玩家,会选择默认项(即初始光标位置,下面会提到)。
如果录像回放中尝试选择不满足“启用条件”的灰项V2.8起会直接报错。这种处理方式比上面的“弹窗请求补选”更为严厉,如您在造塔测试时遇到了这种问题,可以先“回退到上一个节点”,然后在控制台输入`core.status.replay.toReplay`查看接下来该播放的内容可以按N键单步播放并在这个一维数组中删除掉那个非法的选项值。
而如果录像中记录了多余的确认框或选择项(`choices:n`V2.8起就会在播放时跳过并警告。
V2.8.1起“显示选择项”支持限宽和手动指定默认项了注意最好指定一个100%出现且启用的项),配合“带编号的显示文章”效果更佳哦!
![image](img/quickshops.jpg)
在“全塔属性——全局商店”中可以编辑各个商店,商店一共有三种:
1. **公共事件商店:**最简单的一种商店,或者应该叫做给玩家准备的快捷功能更合适,因为它的内容完全不一定非得是个做买卖做交易的“商店”,也可以是诸如“开启或关闭主动技能”、“快速换上最强套装”之类的便捷功能。
* 多说一句鉴于全局商店列表最多只能同时显示12或14项还有一项是关闭列表因此您也可以准备一些永久道具设置适当的使用条件并在使用效果事件中去实现这些给玩家的快捷功能。当然全局商店的使用条件更加统一请自行权衡。
* 公共事件商店在用法上和一般的“插入公共事件”并无二致,同样可以提供一个参数列表。
2. **道具商店:**这种商店也很简单由第三种QTE指令实现但没有限时。
* 您可以在其中随意填写买卖的道具ID、存量、买卖价和出现条件。
* 存量不填视为无限,买入价不填视为只能卖(回收),卖出价不填视为只能买,出现条件的含义和选择项一致。
* 如果需要在游戏中对买卖价和对存量进行读写,请读写`core.status.shops`
* 请注意,使用道具商店的话务必保留`project/images/winskin.png`及其注册信息,可以换成相同规格的同名图片。
3. **新版商店:**用法非常灵活的一种商店,其外形酷似“显示选择项”但有一些不同。
* 首先和其他两种商店一样它多出了“商店id、快捷名称、未开启不显示”。
* 商店id只能使用全英数且必须两两不同。
* “快捷名称”为显示在V键快捷菜单的名称请注意控制字数最好也两两不同以免玩家混淆。
* 勾选“未开启不显示”则此商店在开启前或禁用后不会出现在V键菜单中当商店总个数超过12或14个且随着游戏流程进度依次开新的关旧的时这个勾选项就很有必要了。
* 其次和其他两种商店不同您可以允许玩家预览它前提是V键菜单中显示了这对魔塔这种倡导完全信息的游戏来说非常有意义。
* 最后比起常规的“显示选择项”它不能指定超时毫秒数但是V2.8起)允许长按连续购买。
* 实际执行中在所有子选项的最后会自动追加一个“离开”选项,选择其他子选项并执行后商店并不会立即关闭而是停在那个界面。就像胖老鼠和两部新新魔塔一样。
* “出现条件”和“使用条件”相搭配,让您能够很轻松地做出形如“消耗金币和各种材料的装备合成路线”这样的设定。
* 在预览模式下除“离开”外的子选项、以及交易模式下不满足“使用条件”的子选项,都会显示为灰色,尝试选择时会播放“操作失败”系统音效并提示失败原因。
* 子选项的执行内容中需要手动处理扣费等问题此外在制作像两部新新魔塔一样会涨价的商店时您也需要自己准备变量变量名不必与商店id相同去记录已购次数或者直接记录价格并手动处理涨价问题。
有关全局商店的详细实现,请参见“插件编写”(句号键,`project/plugin.js`)。
其中还提供了一个`core.canUseQuickShop(id)`函数来控制勇士什么时候可以通过V键菜单快捷使用哪些商店自变量id为商店id.
本质上,新版商店是套在一个死循环里的。您可以在子选项的执行内容中使用“跳出当前循环”指令来打断该子选项剩余的未执行内容而强制离开商店,
或使用“提前结束本轮循环”来打断未执行内容而强制重启商店。
同理,公共事件(包括公共事件商店)和自动事件本质上是“一次性”的条件为`false`的后置条件循环,因此使用这两个指令都能跳出它们。
另外,全局商店在录像中的记录方式是`"shop:id"`紧接着显示选择项的记录,也就是说“离开”项的序号可能会不固定(尤其是在连载塔中),请稍加留心。
## 数据相关类(绿色)
![image](img/control_itemsg.jpg)
这类的指令会设置各种数据(如怪物属性、楼层属性、全塔属性、七大可读写块),处理弹窗输入和开关全局商店,以及控制玩家最最关心的勇士的各种行为。
### 设置各种数据的指令
1. **数值操作:**最简单的就是最灵活的,本指令能够修改七大可读写块(状态、物品、变量、独立开关、临时变量、全局存储、增益)的值。
* 修改的运算符有十种,“设为”会将右块的值代入左块,“增加、减少、乘以、除以”则是对左块的值进行增减和扩大缩小。
* 除法如想只保留整数商(向零靠近)则改用“除以并取商”,如想要余数(例如想取勇士生命值的后两位)则使用“除以并取余”。
* “乘方”指的是将若干个(不一定是正整数)左块连乘起来的积代入左块。
* “设为不大于”和“设为不小于”是指在左块大于/小于右块的时候将右块代入左块,也就是“封顶”和“保底”的作用。
* 指令的右块为一表达式可以使用任何值块和运算块甚至直接使用API.
* 如果需要连续使用本指令建议除最后一个外都勾选“不刷新状态栏”以降低刷新状态栏的性能损耗并且避免意外触发自动事件、生命和魔力溢出甚至死亡生命小于等于0
2. **设置怪物属性:**可以设置怪物的任何一项属性并计入存档。
* 怪物ID在blockly块中也可以填中文要求没有重复有的话请在事件编辑器顶部关闭“中文替换”功能需要设置的属性项在下拉框中选取。通过配置表格自行新增的属性在下拉框里没有但可以写在json区再单击变黄的“解析”按钮或修改`_server\MotaAction.g4`文件最后的部分去追加。
* 本指令对应`core.setEnemy(id, name, value, operator, prefix)`函数完全等价。注意value会被eval因此字符串需要额外套一层引号
* 最后的“值”和“数值操作”的右块写法一致,注意设置怪物名称需要加引号,设置逻辑值(是否)需要填写`true`或`false`,设置“特殊属性”需要填数组且只支持结果值。
* V2.8起,怪物支持“行走图朝向”功能,您在四个朝向的怪物中任意设置一个的属性都会强制同步到其他三个。
3. **定点设置/移动或重置怪物属性:**V2.8新增,该指令主要是为了实现一些诸如“发动技能后指定某个点,该点怪物被削弱/增强,并计入存档”的功能。
* 该指令支持设置的属性有“名称、生命、攻击、防御、金币、经验、加点”,设置后,该属性会在“脚本编辑——怪物真实属性”中最先被使用,然后可以被光环等影响。
* 使用时,需要指定楼层和坐标,楼层不写则取当前层,坐标不填则取当前点,支持双击从地图选点,(除移动外)支持选多个点。
* “移动某点怪物属性”支持写增量dx、dy如写[4,-2]就会让某个点的怪物属性移动到向右4格、向上2格的位置。
* 发生战斗后,该点会被自动重置定点属性。怪物移动跳跃时(如阻击)定点属性会自动一起被移动,您无需手动移动。
* 该组指令实际调用的API为
```
core.setEnemyOnPoint(x, y, floorId, ...)
core.moveEnemyOnPoint(fromX, fromY, toX, toY, floorId)
core.resetEnemyOnPoint(x, y, floorId)
core.enemys.getEnemyValue(enemy, name, x, y, floorId) // 读取
```
4. **设置装备属性:**V2.8新增,该项可以制作一些“随剧情推进而强化装备”的效果并计入存档。
* 使用时需要指定装备ID、要修改的是常数值还是增益、要修改的属性英文名等。
5. **设置楼层属性:**除了贴图和两个到达事件,其他属性都可以方便地修改。
* 楼层ID不填则视为当前楼层可以去“地图选点”浏览各楼层并复制ID.
* 注意修改“楼层中文名”、“状态栏中名称”、“默认地面ID”、“背景音乐”需要后缀名这些字符串类型都需要加引号V2.8起背景音乐支持一维数组),几个“能否/是否”只支持修改为`true`或`false`,三个坐标和天气、色调这些数组类型都需要加方括弧。本指令对应`core.events.setFloorInfo(name, value, floorId, prefix)`
* 修改当前层的天气/色调/背景音乐后不会立即生效,如需生效请补一个对应的特效指令(如“恢复画面色调”)并且不要勾选“持续”。
6. **设置全局属性:**即全塔属性中的“主样式”(无需再加引号)和装备孔列表。
* 修改装备孔列表时请注意,如果装备的道具属性中填写的装备类型是自然数,则可以【在列表结尾】随着游戏流程的推进解锁新的装备孔或移除装备孔(请先将身上的此类装备脱下)。
* 而如果装备的道具属性中填写的装备类型是装备孔名称,则可以随着游戏流程的推进修改装备孔的类型组成,如本来是两把剑一块盾改成一把剑两块盾(同样需要注意已经穿上的装备问题)。
* 本指令对应`core.events.setGlobalAttribute(name, value)`函数。
7. **设置全局数值:**如题,可以修改四种宝石和三种血瓶的基础值等,必要时也可以修改图块的每帧时间以及上下楼时间以得到一些演出效果。
* 如需使用脚本,请直接修改`core.values`,完全等价。但是竖屏状态栏的自绘行数如果动态修改有可能会出问题,请注意。
8. **设置系统开关:**如题,可以用来随着游戏流程的推进解锁/移除状态栏的显示项或改动其他开关。
* 比如中途开关生命上限、加点和负伤,中途显隐魔力、技能、绿钥匙和破炸飞毒衰咒。
* 在游戏胜利时会将生命值作为分数上传到在线游戏排行榜,因此请在胜利前关闭生命上限再修改生命,比如根据钥匙等道具的持有情况进行加分。
* 请注意,即使您在游戏中途才将楼传变为平面塔模式,访问过的楼层依然已经记录了最后离开的位置。
* 本指令对应`core.setGlobalFlag(name, value)`函数,实际会修改`core.flags`(但请不要用脚本直接修改它)
9. **设置文件别名:**V2.8新增,您可以修改一个中文名实际指向的英文文件名,从而做到随剧情推进采用不同的系统音效(如上下楼)等效果,如果英文文件名不填则表示恢复到全塔属性中的默认值。
### 导致勇士位置变化的指令
这类指令都支持填写负坐标、超出地图宽高的坐标或小数坐标(大地图中请慎用小数坐标),
当勇士在这些异常坐标时【除第一个指令外】都可以正常执行。
可以用于一些特殊演出,但请记得在事件结束(玩家恢复行动)前改回正常。
1. **勇士前进一格或撞击:**如题,会让勇士像自由行动时一样尝试前进一格。
* 如果可以前进但前方不可被踏入如门、怪物、箱子、NPC则会撞击并触发事件走到道具、踩灯或路障或用普通事件制作的陷阱等也会触发。
* 本指令可以正常触发跑毒和阻激夹域捕捉(可以致死),滑冰事件就是在冰上执行了它。
* 本指令对应`core.moveAction(callback)`函数,但请勿直接调用它。
2. **无视地形移动勇士:**“动画时间”为每步的时间不填则取玩家设定值该值从V2.8起允许在移动过程中修改最少为16
* 可以勾选“不等待执行完毕”来和后面的指令同时执行比如让勇士和一个NPC肩并肩走。
* 本指令不会触发跑毒和阻激夹域捕捉,且会无视地形可进出性、可通行性。
* 移动过程中不会触发任何事件就像开启调试模式时按住Ctrl键移动一样与之不同的是也可以移动出地图外
* 勇士后退时,跟随者们会照常前进,数不清楚格子时记得善用地图选点功能浏览地图。
* V2.8起支持斜向移动支持移动过程中单纯转向步数填0
* 斜向移动时行走图以左右为准,但“后退”依然以勇士朝向为准而不考虑上一步的行走方向(这点和图块的移动不同,勇士不可能斜向后退但图块可能)。
* 本指令对应`core.eventMoveHero(steps, time, callback)`函数,请注意不是`core.moveHero()`
* 多说一句您可能会发现勇士在移动时会在行走图的2、4两帧之间反复切换尤其是在大地图中心时很明显这和图块以及RPG Maker的行为很不一致而且观感较差如需改成1234帧循环请启用“勇士四帧行走动画”插件V2.8该插件有更新从V2.7.x接档的用户需要重新抄写一下
3. **跳跃勇士:**可以填写目标点坐标支持双击从地图选点坐标允许使用带有冒号缩写量甚至API的表达式。
* 比如`["core.nextX(2)", "core.nextY(2)"]`json表示勇士越过面前一格即道具“跳跃靴”的效果。
* V2.7.3起跳跃的目标坐标支持写增量dx、dy如写[4,-2]就会让勇士跳到向右4格向上2格的位置。
* 跳跃高度和距离有关,原地跳跃的高度只有半格(可在下述函数中修改)。跳跃过程中跟随者消失,跳跃结束时跟随者聚集。
* 跳跃也支持异步效果如和NPC一起跳对应`core.jumpHero(ex, ey, time, callback)`函数,其中`callback`为异步跳跃完毕的回调函数。
* 跳跃默认没有音效,您可以自行像支援怪和道具“跳跃靴”一样配上音效(具体方法在“插件复写”一节有讲)。
* 和“无视地形移动勇士”一样,勇士跳跃也会无视目标点的地形和阻激夹域捕捉,不会触发目标点的任何事件。
* “无视地形移动勇士”和“跳跃勇士”因为经常和图块的这两个行为一起使用进行演出且都没有碰撞效果因此V2.7.3起移动到了“地图处理类”,请注意。
4. **楼层切换:**和前面的“楼梯、传送门”绿点事件用法完全一样,但不可穿透。
* 此指令同样支持双击从地图选点坐标支持表达式和在json区填写传送的目标点图块ID在目标层唯一再点击变黄的“解析”按钮。
* 另外,正如本小节开头提到的,本指令比起“楼梯、传送门”事件更多地用于演出,因此您可以填写异常坐标。
* 楼层ID只能填写常量如需使用变量请使用“原生脚本”插入事件。
5. **位置朝向切换:**“跳跃勇士”不会改变勇士朝向,“楼层切换”又会导致重生怪复活。且这两个都会导致跟随者聚集,所以同楼层内改变勇士位置可以使用本指令(坐标和跳跃一样支持双击从地图选点以及表达式)。
* 本指令还可以用来让勇士原地转身不填坐标这样也不会聚集跟随者支持4种绝对朝向和4种相对转向。
### “数据相关”类的其他杂牌指令
以下杂牌指令负责弹窗输入、显伤战斗、道具装备、全局商店、行走图和队伍:
1. **接受用户输入:**弹窗请求用户输入一个自然数或字符串,提示文字允许使用`${表达式计算}`。
* 请求输入自然数支持十六进制负整数会被取绝对值。小数会被向0取整其他非法输入会变为0“输入自然数”在录像中会记录为`input:n`。
* 请求输入字符串时,玩家点击取消则视为输入了空字符串。
* 输入的结果会保存在值块“`变量input`(`flag:input`)”中,可供后续处理。
* 比如样板的生命魔杖就是一个例子,它允许玩家一次使用多个同种道具。
* 读取“全局存储”这一行为,在录像中和“输入字符串”的记录方式一致(`input2:base64`)。
* V2.8起,录像回放中如果出现了多余的`input:`或`input2:`都会警告并跳过。反之“接受用户输入”事件在录像中缺失了值则会使用0或空字符串并补录。
2. **更新状态栏和地图显伤:**如题,可以勾选“不检查自动事件”来不检查。
* 本指令实际执行“脚本编辑——更新状态栏”,即`core.updateStatusBar(doNotCheckAutoEvents);`
3. **强制战斗(点名):**和天降怪物指定ID中文替换只支持不重复的中文名强制战斗。
* 此指令战后不会从地图删除图块也不会触发各点的战后事件(黄点),但可以触发战后脚本和怪物属性中的(批量)战后事件。
* 此指令的战斗是强制的打不过直接死亡V2.8可以用值块预先判定能否打过)。
* 此指令一般用于boss战通过走到某个点或开启某个门来触发可以制作战前剧情然后强制战斗。
* 战后boss不立即消失从而避免基于漏怪检测的自动事件被误触发可以继续进行一些演出如51层魔塔四区骑士队长的逃跑效果。
* 因为是天降怪物(没有坐标),所以对这只怪物在属性修正以及战损计算等处涉及到怪物坐标的代码一律不起作用。
* 比如它不会受局部光环的加成也不会被任何怪支援也无法被V2.8的“定点设置怪物属性”影响。
* 另一种强制战斗指令在“地图处理类”指定的是坐标而不是怪物ID.
* V2.8新增了两种战前事件,两种强制战斗指令都不会触发它们,如需触发,请使用“触发系统事件”指令。
* 由于V2.8提供了战前事件,因此不再建议使用曾经的“覆盖触发器+天降强制战斗”方式实现战前剧情,因为这样做不会自动存档,对玩家不太友好。
4. **尝试使用道具和穿脱装备:**使用道具和穿戴装备需要指定ID中文替换规则和强制战斗一样
* 不能使用怪物手册(请使用“特效声音类”的“呼出怪物手册”指令)和楼层传送器(如果“覆盖楼传事件”则没有关系),使用中心对称飞行器则会跳过确认画面。实际对应`core.useItem(itemId)`函数。
* 穿脱装备对应`core.loadEquip(equipId)`和`core.unloadEquip(type)`函数。脱下装备需要指定类型,这里只能写自然数不能写名称。
* 道具使用失败或穿不上装备(比如没有或不满足条件)时会播放音效并提示。
5. **开关全局商店:**本指令可以设置一个全局商店的启用和禁用状态,设为启用时也支持立即打开。
* 一般用于商店的实体NPC处再配合独立开关可以让勇士首次接触时先进行一些对话然后启用并打开全局商店。
* V2.8新增了值块可以用来判定一个全局商店是否是开启状态。
6. **更改角色行走图:**如题,文件名必须填写(支持双击选文件)。
* 文件必须放在`project/images`文件夹并注册且规格必须符合要求4帧总宽度至少128px高度不限。宽高必须为4的倍数
* 如果勾选“不重绘”就不会立即刷新,从而避免大地图视角重置到以勇士为中心。本指令对应`core.setHeroIcon(image, noDraw)`函数。
7. **跟随者入队和离队:**您可以用这一对指令来让跟随者入队和离队,同样支持双击选文件。本指令对应`core.follow()`和`core.unfollow()`函数。
* 行走图和勇士的规格要求(尺寸不需要一样)、文件位置和注册方法相同。
* 离队可以不填文件名表示解散队伍只留勇士,如果填写文件名则尝试踢出队伍中第一个以此为行走图的跟随者。
* 入队成功后、以及尝试离队后队伍都会聚拢,大地图视角也会重置。
## 地图处理类(浅蓝)
![image](img/maps_waits_raw.jpg)
这个类型的指令会影响三层地图矩阵的阵元,如果您觉得三层还不够用,“插件编写”(句号键)五图层插件欢迎您。
开始介绍前,首先明确一点:改变地图数据不会立即影响事件的进程(自动事件除外)。
比如因为踩灯、路障和阻激夹域捕捉怪的分布变化导致勇士行走被妨害的区域发生变化,但不会立即触发妨害效果,而是要等到勇士下次行走。
在勇士所在点转变成(显示)一个门/怪物/道具/箱子/楼梯什么的(包括在脚下转变成/显示冰面)都不会立即触发事件,把这些图块移动/跳跃到勇士身上也是。
反之“隐藏事件”转变图块为0也不会立即中止当前的事件处理只是下次不会触发。
1. **强制战斗(定点):**这是另一种强制战斗它指定坐标而不是怪物ID.
* 可以双击从地图选点(只能选当前楼层的,不填则取当前点),也可以用表达式指定坐标,坐标一次只能写【一个点】。
* 战斗后会自动从地图删除该点的怪物(重生怪则是隐藏),并尝试插入该点的战后事件(黄点)以及怪物属性的(批量)战后事件,成功插入前者时会改变当前点坐标到该点。
* V2.8新增了两种战前事件,它们无法被“强制战斗”指令触发,如需触发,请使用“触发系统事件”指令。
2. **开关门:**坐标写法同上限1个点同层开门时楼层ID可以略去不写。
* 关门的位置必须是空地,“需要钥匙”只对同层开门有效。跨层开门请自己加判定,本指令对应`core.openDoor(x, y, needKey, callback)`函数。
* 这对指令支持所有完整填写了“门信息”的四帧图块(自动元件除外),比如样板自带的三色墙和六色门。
* 可以勾选“不等待执行完毕”来实现异步效果(如同时开关多个门,具体速度取决于门信息)。
* 和上面的强制战斗一样,开门后将尝试插入该点的开门后事件(紫点),成功插入时会改变当前点坐标到该点。
3. **显隐事件和图层块:**这两对指令可以令三层地图矩阵的某些阵元在0与非0之间切换。
* 还以51层魔塔为例二楼右下角的小偷在游戏开始时是不显示的勇士进入四区后才出现。
* 也就是说这个小偷是一个“普通事件”内容是一些对话和打开35层魔龙处的暗道只不过没有勾选“启用”。
* 在适当的时候这个例子中是和29楼小偷对话后执行一个“显示MT2层1212点处的事件”指令就能显示出二楼的小偷。
* 同理勇士接触此小偷并处理事件事件结束前执行一个“隐藏同时删除当前点事件500毫秒”指令小偷就会从画面中淡出勇士可以任意在小偷存在过的位置走来走去而不会再触发什么。
* 所以,一次性陷阱(走到某个地方关上墙/机关门、冒出埋伏怪在触发后一定要及时隐藏。不然就会反复触发样板1层有例子可供参考。
* “显隐事件”都可以双击从地图选点支持选多个点只想要楼层ID的话可以点击“复制楼层ID”按钮。在指令块中可以使用表达式作为坐标但这样只能写一个点多个点可以把横坐标依次填在x处而纵坐标对应填在y处json中写作多行两列的二维数组但只支持常数从而同时显隐多个点。
* 楼层ID省略则取当前楼层“动画时间”用于同层显隐从而表现出淡入淡出的效果。
* “不等待执行完毕”的含义如前,您可以淡入一些点同时淡出另一些点。
* 值得注意的是,“隐藏事件”还提供了一个“同时删除”勾选框,勾选后无法再用“显示事件”指令显示出来(例如永久移除一个重生怪)。
* 请注意,隐藏或删除后不影响正在进行的事件流(不会立刻结束),您可以把该点安全地直接转变为别的图块或让别的图块移动/跳跃到此点,比如把箱子/阻击怪推过来(根据该点是否覆盖触发器,推过来以后的行为可能有变化)。
* 其他两个图层的图块也支持显隐,对游戏性的影响主要体现在显隐背景层的滑冰图块以及两个图层的单向通行箭头/薄墙。坐标和楼层ID的填法同上只不过这两个就没有淡入淡出效果了。因为其他两个图层的图块不支持什么初始隐藏如有需要可以在“开场剧情”中统一提前隐藏。
* 显隐事件对应`core.showBlock(x, y, floorId)`和`core.hideBlock(x, y, floorId)`,同时删除对应`core.removeBlock(x, y, floorId)`函数;显隐图层块对应`core.maps._triggerFloorImage(type, loc, floorId, callback)`
4. **转变图块和图层块、事件转向:**这组指令可以修改三层地图矩阵的阵元。
* 先说图层块吧(前景、背景),坐标和楼层的填法同上,不支持淡入淡出。转变图层块后,块的显隐状态不变,原来是显示/隐藏还是显示/隐藏。
* 接着说事件层,坐标和楼层的填法同上。有几个注意事项:
1. 新图块为0时“动画时间”全部用来淡出用于没有普通事件和“楼梯、传送门”的点会视为删除。
2. 转变图块也不影响显隐状态,该点原来是显示/隐藏还是显示/隐藏。
3. 同层把一种非0图块转变为另一种非0图块空气墙`airwall`算非0“动画时间”的前一半用来淡出原图块后一半用来淡入新图块。
4. 同层把0图块转变为非0图块“动画时间”全部用来淡入。
* 这对指令可以填写新图块的ID也可以填写数字如17是空气墙201起是怪物
* 如需让绑定了“行走图朝向”的图块转向也可以直接使用“事件转向”指令和勇士一样支持7种转法从而避免一个个手写行走图ID的麻烦。
* 转变图块和图层块对应`core.setBlock(number, x, y, floorId)`和`core.maps._triggerBgFgMap(type, name, loc, floorId, callback)`
5. **设置图块不透明度和特效:**如题V2.8新增。前者支持渐变效果,可以用来制作亡灵状态的角色或配合楼层贴图实现大型多帧怪物。
6. **显隐贴图:**这个指令可以用来显隐之前在“楼层属性”中介绍的楼层贴图。
* 显隐贴图不支持淡入淡出坐标为贴图左上角的【像素坐标】因此不支持地图选点楼层ID不填则取当前层。实际执行`core.maps._triggerFloorImage(type, loc, floorId, callback)`
7. **移动和跳跃事件:**这两个指令可以将地图一点的图块转移到另一点。
* 首先明确一点,这两个指令转移的【仅仅是图块】。起点的七彩事件不会被一同转移(但不消失的情况下定点属性会一同转移),终点的七彩事件也不会被覆盖。
* 从游戏性上讲,最终的效果是起点被“隐藏事件+同时删除”,勾选“不消失”时终点被“转变图块+显示事件”(终点原来的图块被覆盖)。
* 比如,阻击怪是“移动后不消失”,捕捉怪是“移动后消失”,支援怪是“跳跃后消失”。
* 这两个指令一次只能转移一个图块,双击从地图选点选择的是移动的起点和跳跃的终点(跳跃的起点请右击选取)。
* 任何一个坐标不填都视为当前点,比如“跳跃事件”什么坐标都不填就会让当前点图块原地跳跃。
* 和无视地形移动勇士一样,“移动事件”也没有碰撞效果,移动过程中会穿过勇士和一切地形。
* “动画时间”为每步移动的时间或跳跃的用时,以及不勾选“不消失”时淡出的时间。
* 和“跳跃勇士”一样,“跳跃事件”默认也没有音效,可以自己搭配。
* V2.7.3起跳跃的目标坐标支持写增量dx、dy如写[4,-2]就会让某个图块跳到其原位置向右4格向上2格的位置。
* 移动和跳跃实际对应`core.moveBlock(x, y, steps, time, keep, callback)`和`core.jumpBlock(sx, sy, ex, ey, time, keep, callback)`函数。
* V2.8起,这两个函数在“不消失”的情况下,会将起点处的定点怪物数据也一并移动到终点,这对阻击怪来说是个福音。
* V2.8起“移动事件”指令支持斜向移动行走图仍取左右、中途变速速度不得小于16、中途转向步数填0“后退”指令如果用在开头则必须是绑定了“行走图朝向”的图块如果用在中途则会根据上一步移动/转向的方向后退(注意这一点和勇士不同,勇士是不可能斜向后退的)。
* “不等待执行完毕”的用法如前,但几个图块再加上勇士以各异的速度和总步数移动时安排起来很麻烦,需要用到下述的“等待三姐妹”。
## 等待三姐妹、注释和原生js/json
在讲解“事件控制”(流程控制)类指令之前,这里插播几个比较杂牌的指令。
1. **等待固定时间:**如题,可以用来实现复杂的集体移动、跳跃效果。
* 比如说51层魔塔一区结尾的骷髅埋伏圈就是九只骷髅和四扇机关门组成的复杂演出。
* 每只骷髅开始移动时都“不等待执行完毕”,但又需要“等待一小段时间”再让下一只骷髅开始移动。
* 本指令还提供了一个勾选项“不可被Ctrl跳过”如果不勾选此项且当前【没有】正在执行的异步事件动画、音效、气泡提示不算则Ctrl可以跳过等待。
2. **等待所有异步事件执行完毕:**让我们来想象这样一个情景。
* 您使用了“移动事件”来移动一只怪物到勇士面前,并且“不等待执行完毕”。而下一条指令是“勇士前进一格或撞击”,以期触发战斗。然而因为怪物移动需要时间,两个指令同时执行,所以战斗没法触发。
* 类似地,如果您在一个异步事件执行完毕之前就结束了整个事件流,让勇士恢复行动,那么可能这些异步事件还没来得及在游戏性方面生效,导致接下来会发生的事取决于玩家操作的时机和勇士的移速。
* 考虑到录像系统,在录像回放时很多耗时的东西和所有需要用户响应的东西会被跳过,勇士的移速又可以很快(倍速播放),导致回放结果和原游戏不一致。
* 总之,当您希望确保一些异步事件完全生效后再开始执行新的指令或结束事件流,“等待所有异步事件执行完毕”就是您的不二之选了,事件编辑器也会在发现缺少本指令时弹窗警告。
* 动画默认会被等待而音效不会被默认等待V2.8.1提供了两个勾选框允许您对其进行控制。
* 另外,您可以随时使用`core.getPlayAnimates()`和`core.getSounds()`获取当前未结束的所有动画和音效的id支持自变量填名称来筛选
3. **等待用户操作并获得键鼠/触屏信息:**前面提到三种QTE指令这是最后一种。
* 之前提到的“确认框”和“选择项”可以复现RPG Maker的回合制战斗但没法做出更复杂的交互界面比如技能/天赋树,以及样板的道具商店,这就需要用到本指令了。
* 本指令会阻塞事件的执行直到玩家按下键盘上的某个键滚轮视为PageUp/PageDown键、或点击【视野】中的某个点、或经过了超时毫秒数不设置则不限时
* 解除阻塞后,下列值块可能发生变化:
1. `变量type`(`flag:type`)解除的原因0表示按键1表示点击-1表示超时。
2. `变量keycode`(`flag:keycode`)按键情况下的键值48—57为大键盘0—965—90为字母键A—Z其他键请右击该子块查询查不到的自己百度
3. `变量timeout`(`flag:timeout`),进行有效操作后,距离超时的剩余倒计时,可以用来进行一些处理(音游?)。
4. `变量x`和`变量y`(`flag:x`和`flag:y`)点击情况下所点格子在视野中的相对坐标一定在0—12或0—14范围。
5. `变量px`和`变量py`(`flag:px`和`flag:py`)点击情况下所点像素在视野中的相对坐标一定在0—415或0—479范围。
* 上述后两项如需转换为大地图中的绝对坐标,请查阅“楼层属性——修改楼层宽高”一节。
* 您可以根据这些值块去做流程控制,较为简单的场合(如几个键执行同一段指令,或横平竖直的单个矩形点击区)也可直接使用图中的场合块。
* 其中点击的场合块还支持双击预览判定区,用半透明的红色标出。
* 默认情况下,一次操作同时满足多个场合块时会依次执行这几个块的内容,除非您使用了下面的“不进行剩余判定”。
* V2.7.3起,场合块支持“不进行剩余判定”,当满足勾选了此项的场合块时,它下面的其他场合块会全部视为不满足。这允许您实现“点击地图上某个小区域做某些事,点击其他地方做另一些事”而不需要在后者的场合中专门排除前者。
* V2.8起,提供了超时场合块和自定义条件的场合块,同时还支持“只检测子块”。
* 在“只检测子块”模式下,如果玩家进行了不符合任何子块的操作,那么会继续阻塞而不是进入下一个指令,超时倒计时也不会重置。
* 但是如果设置了超时时间,即使未提供超时场合,超时时依然会解除阻塞直接进入下面的指令。
* 此指令获取到的玩家操作,在录像中会像“接受用户输入数字”一样记录为`input:...`。例如不带超时的键盘按键,就会记录为`input:keycode`。
* 录像回放时,如果出现了多余的`input:...`,就会跳过并警告。如果遇到本指令但录像中未记录操作或记录了非法/无效操作,就会直接报错。
* 非法操作指“本指令没有设置超时时间但录像中记录为超时”,无效操作指“本指令设置为只检测子块,但录像中记录的是一个不满足任何子块的操作”。
4. **添加注释:**在上述的场合块里您还可以看到两个注释块。
* 注释块在游戏中会被忽略,一般用来帮您记住写的指令是用来做什么的。
* 极端情况下,样板某些场合会检测指令数组是否为空及其长度,此时您可能需要被迫用注释指令来占位。
5. **自定义事件:**自带的指令毕竟有限,但事件可以与脚本任意联动。
* 原生脚本分为两种原生js和原生json.其中后者会实际执行您通过`core.registerEvent`注册的事件处理函数(即`{"type":"xxx",...}`对应`core.events._action_xxx()`),请注意这类函数在执行结束前务必调用`core.doAction()`函数去继续执行下一条指令。
* 如果学有余力,还可根据[修改编辑器](editor)来代替原生json就可以使用调色器、地图选点、选文件等功能啦。
6. **原生JS脚本执行**允许您执行任意JS脚本例如造塔测试时发现事件流程不符合预期此时可以使用`console.log()`语句在控制台输出日志信息进行检查。
原生js的用法就更广了首先它可以做一些事件做不到的事比如
如果用事件增加道具的话就会有提示和音效,而这有时不是我们需要的,尤其是在设计新道具时将“能否使用”填`"true"`并在使用效果事件中使用失败的场合返还道具时;因此我们可以直接调用脚本:
```js
core.addItem(itemId, n); // 静默增加n个道具n可为负数不填视为1
```
其次受制于Antlr—blockly的数据类型很多指令的参数只能写常数比如楼层ID。这时我们就需要在原生js中使用万能的`core.insertAction()`大法,来突破这些限制。
比如说我们有一座20层高的塔楼层ID为MT0—MT19某个事件中我们想让勇士随机传送到某个楼层坐标不变。那么就可以使用下列原生js
```js
core.insertAction({"type": "changeFloor", "floorId": "MT" + core.rand2(20)})
```
连续使用多条json指令时请先声明一个空的指令数组`var todo = [];`)然后将需要的指令分批追加到其末尾(`core.push(todo, [...]);`),最后再一次性`core.insertAction(todo);`插入执行,可以选择插入到剩余事件的开头或结尾。
另外您可能会问既然都用js了为什么不直接用之前提到的`core.changeFloor()`函数呢?
这是因为原生js在不勾选“不自动执行下一个事件”的情况下**只能使用瞬间完成的函数或者drawTip、动画和音效这种虽然耗时但不影响游戏性的不能使用任何异步函数包括阻塞直到玩家操作的**
系统常见可能会被造塔用到的API都在[API列表](api)中给出一般而言异步API的最后一个自变量都叫`callback`回调。在勾选“不自动执行下一个事件”的情况下原生js可以使用一个异步API只需将其`callback`参数填`core.doAction`,请谨慎使用。
比如说我们知道天降强制战斗没有坐标所以不受光环等影响也无法触发单点战后事件那捕捉怪的战斗是怎么实现的呢答案是在原生js中使用了异步的`core.battle(id, x, y, force, callback)`函数,这里`force`填`true`表示强制战斗,`callback`填`core.doAction`表示战斗结束后继续处理事件。
熟练运用后还可以使用多个异步API每个以下一个作为回调。
## 事件控制类(深蓝)
![image](img/flowctrl.jpg)
在三个QTE指令中我们已经初见了流程控制的端倪。只不过它们的流程走向是由玩家的选择直接左右的。能否通过对值块的比较等操作自动走向不同的流程分支呢答案是肯定的。
1. **条件分歧:**和“显示确认框”很相似,只不过这个是自动的。
* 和js的`if (...) {...;} else {...;}`完全等价,本指令需要内嵌一个值块(可以填逻辑表达式,常常是两个值块的比较,比如某道具是否足够)。
* 当此值块的值不为`false、0、null、undefined、NaN和空字符串`(即之前提到的六大广义`false`)时,执行“如果:”和“否则:”之间的指令,当此值块的值为这六个值之一时,执行“否则:”后面的指令。
2. **多重分歧:**本指令和js的`switch`语句有一定差别即使您有js或其他编程语言基础也请务必仔细阅读接下来的说明。
* 事件执行到多重分歧指令时,会先计算一次“判别值”,结果记作`key`.然后准备一个空的指令数组,记作`list`
* 接下来从上到下扫描每个场合块,如果一个场合块的“值”为`default`或计算结果和`key`相等,就把这个场合的指令组追加到`list`的末尾,即`core.push(list, [...]);`。
* 每次成功追加后,如果被追加的此场合块未勾选“不跳出”,则无视下面的所有场合块直接结束扫描,否则继续扫描下一个场合块。
* 扫描完全结束后,调用`core.insertAction(list);`付诸执行。
* 所以各场合块的顺序一定要安排好,比如`default`(可以没有此场合)如果不勾选“不跳出”则一定要放在最后。
* 多重分歧常见的用法是,判别值填未知量,所有场合块都不勾选“不跳出”且“值”填写为两两不同的常量(如果有相同的则只有第一个有效)。从而执行唯一的相等场合,没有的话还可以补一个`default`场合类似js的`else if`语法。
* 或者判别值填常量各场合块填未知量且都勾选“不跳出”。把所有相等的场合筛选出来依次执行类似js的`filter`语法。
* 第二种用法中,最常见的是判别值填`true`,各场合块填逻辑表达式。此时如果都勾选“不跳出”并且不提供`default`场合,就相当于一堆没有`else`的if语句。
* 而如果在这个基础上改成都不勾选“不跳出”就相当于正常的`else if`语句(可以把`default`场合提供在最后或不提供),而且比起上面的“条件分歧”指令不需要嵌套了。
3. **循环遍历(计数):**使用临时变量A—Z事件流结束时会和arg+纯数字的变量一起被清空)可以进行循环遍历,它们都属于前置条件循环。
* 循环遍历有两种,第一种是计数型。`从`和`到`之间的框填写临时变量的初始值,`到`和`步增`之间的框填写临时变量允许的边界值。
* 每轮循环开始前会【重新计算】边界值和步增,如果临时变量位于界外(减去边界值后和步增同号)则跳出循环。
* 每轮循环结束时,临时变量会加上步增值(可以为负数,请注意变号问题)。
4. **循环遍历(迭代):**这是另一种循环遍历,可以迭代一个列表。
* 每轮循环前,会检查列表是否已空。已空则跳出循环,否则将列表的第一项【移除并代入】临时变量。
* 使用此项时请注意,列表中的字符串不会被自动解释为冒号缩写量。显示文章等处如有需求,请使用`${core.calValue(temp:A)}`来计算。
* 分歧和循环都可以嵌套,嵌套循环遍历请给内外层使用不同的临时变量。
5. **前/后置条件循环:**
* 前置条件循环和一个没有“否则”分支的条件分歧很相似,区别仅仅在于每次执行完分支内容后都会【跳转】回到条件检测之前,从而再次检测条件,还满足就再执行,如此往复。
* 而后置条件循环比起前置,区别在于第一轮循环是无视条件强制执行的。
* 它们对应的js语法分别为`while (...) {...;}`和`do {...;} while(...);`
6. **跳出当前循环:**如题遇到此指令时将检测当前是否在某个循环中还包括自动事件、公共事件、全局商店并跳出所在的【最内层】循环V2.7.3起支持指定跳出层数。如果不在任何循环中则什么也不做。大致相当于js的`break;`
7. **提前结束本轮循环:**生效范围同上,不过是结束最内层的【本轮】循环。换言之:
* 对一般的前/后置条件循环会立刻跳转到下次检测条件前;
* 对循环遍历(计数)会立刻跳转到下次计算边界和步增并累加前;
* 对循环遍历(迭代)会立刻跳转到下次检查列表是否为空前。
* 还可用来“重启新版商店或道具商店”。若不在任何循环中,则什么也不做。
* 大致相当于js的`continue;`V2.7.3起支持指定层数。
8. **立刻结束当前事件:**此指令将清空临时变量(以`@temp@`开头)和参数变量(`arg+纯数字`),清空事件列表,中断一切事件处理,恢复勇士行动。
* V2.8起,您可以在两种“战前事件”中使用它取消战斗。
9. **触发系统事件:**模拟勇士撞击/踩踏本楼层的某个点(实际执行`core.trigger(x, y, callback)`),支持双击从地图选点。
* 该指令把触发的事件(包括图块属性的“碰触脚本/碰触事件”,不包括阻激夹域捕捉和血网)插队到当前事件列表中。
* 譬如该点是道具则会捡起来,是怪物则会触发战前事件然后战斗,是门则会尝试开门,还会连带触发对应的`afterXxx`事件。
* 如果该点是普通事件(红)则会触发之,是楼梯事件(绿)则会直接触发传送(目标点是“保持不变”或三种对称点也以勇士位置而不是楼梯位置计算)。
* 该点是路障则会触发对应的毒衰咒(血网除外),是踩灯则会把勇士脚下变成踩过的灯。滑冰在背景层,所以无法触发。至于推箱子,请自行探索。
* V2.8起,您可以使用“强制战斗(定点)”指令跳过战前事件强制与地图上某个点的怪物战斗,也可以用“触发系统事件”指令指定某个有怪物的点然后先触发战前事件再战斗,请注意区分。
10. **插入公共事件:**如题,“参数列表”为一数组,其各项会被依次代入值块`变量arg1`、`变量arg2`...而`变量arg0`会记录公共事件的名称。
* 实际执行`core.insertCommonEvent(name, args, x, y, callback, addToLast)`
11. **插入事件:**此指令可以无视目标点启用与否跨楼层插入任何点的普通事件不需要覆盖触发器、战前事件、afterXxx事件执行。支持双击从地图选点。
## 特效声音类(褐色)
![image](img/blockly.jpg)
这个类别的指令会负责动画、视角、色调、天气、音频、存读档等其他一些细节。
1. **画面震动:**会让画面震动,可以选择四种振动方向,可以指定总时间、震动速度和振幅。
* 实际执行`core.vibrate(direction, time, speed, power, callback)`函数。
2. **显示动画:**如题,可以双击选文件并预览(预览的坐标锁定为视野中心)和试听/修改音效。
* 如需从地图选点请右击指令块,也可用另一个指令让动画跟随勇士移动。
* 坐标不写则取当前点如果勾选“相对窗口坐标”则坐标应填写为0—12或0—14表示视野中的相对坐标如13×13样板填两个6表示视野中心
* 如果不勾选“不等待执行完毕”,则等待的实际时长只取决于动画,和音效无关。
* 实际调用`core.drawAnimate(name, x, y, alignWindow, callback)`和`core.drawHeroAnimate(name, callback)`
3. **设置视角:**设置视角支持双击从地图选点,不填坐标则重置视角。动画时间指移动的总时间,支持四种变速移动。
* 勇士的`status:animate`为`true`(原地抖动开启)时,禁止使用本指令!
* V2.7.3起,坐标支持写增量,如[4,-2]表示视角直线移动到向右4格、向上2格的位置。
* 请注意,勇士重绘时(`core.drawHero()`函数视角也会随之重置。所以视角变化后勇士的坐标、朝向、显隐、行走图事件和API都提供了不重绘参数和跟随者情况暂时都不要动。
* 实际调用`core.setViewport(x, y)`和`core.moveViewport(x, y, moveMode, time, callback)`其中前者的自变量为【像素坐标】且不必为32的倍数必要时可作为原生js使用来实现特殊的演出。
* V2.8.1起,支持暂时锁定视角,视角锁定期间,只有上下楼后才会将视角重置到勇士身上(但依然保持锁定)。
4. **显隐状态栏:**如题,如果隐藏状态栏期间勇士需要恢复行动,则建议不隐藏竖屏工具栏以方便手机玩家。
* 实际调用`core.showStatusBar()`和`core.hideStatusBar(showToolbox)`
5. **设置勇士不透明度:**如题,动画时间为淡入淡出时间,异步勾选框用法如前。
* 实际调用`core.setHeroOpacity(opacity, moveMode, time, callback)`
6. **更改画面色调:**色调可以用调色器调配,“动画时间”为渐变的总时间。
* 请注意渐变是在RGBA颜色空间中直线运动V2.8.1支持加速度),因此效果可能不好,画面闪烁同理。
* 如需在勇士自由行动时反复执行,请使用并行脚本或自我回调。
7. **恢复画面色调:**指将更改后的色调恢复到楼层的默认色调,更改当前层的默认色调后您可以使用此指令刷新。
8. **画面闪烁:**“单次时间”必须为3的倍数前1/3时间用于将画面色调转变为目标色调后2/3时间用于恢复当前色调执行次数如题。
* 实际调用`screenFlash(color, time, times, callback)`
9. **更改天气:**如题可以选择“无、雨、雪、雾、晴”之一强度需填小于等于10的正整数。
10. **播放背景音乐:**如题,可以双击选文件并试听(支持别名),并指定开始播放的秒数。
* 当在游戏中触发楼层切换时(包括读档),如果`flag:__color__、flag:__weather__、flag:__bgm__`这三个值块没有值,游戏当时的画面色调、天气、背景音乐就会变为楼层属性中的这三个设置项。
* 以上几个指令都提供了“持续到下个本事件”勾选框,勾选后,本次设置的值将会计入这三个值块。它们的拦截行为在“脚本编辑——切换楼层中”。
* 若不勾选,或恢复画面色调、设置天气为无(晴),就会清除对应的值块。您也可以随时对这三个值块进行手动干预。
* V2.8.1起支持由作者注册新的自定义天气新天气在下拉框中默认未提供可以手动填写在json区再点击“解析”按钮。
11. **暂停和恢复背景音乐:**如题,暂停时会记录暂停在了第几秒,恢复时可以选择从这个秒数继续或从头重播。
12. **预加载背景音乐和释放其缓存:**在线游戏使用,前者的原理是静音播放。
* 最多同时缓存8首背景音乐由`libs/core.js`控制),会自动释放最久未使用的,但您也可以手动释放。
13. **播放音效和停止所有音效:**如题开播一个音效的同时可以停止其他的。V2.8播放系统音效可以从下拉框选取,其他音效也支持使用别名。
* V2.8起播放音效支持“等待播放完毕”支持同时“变调且变速”。100表示正常速度和音调可以填30到300之间的值。
14. **设置音量:**只对背景音乐有效音量为小于等于100的自然数玩家设置值其实被开了平方渐变时间如题。
15. **设置背景音乐播放速度和音调:**V2.8新增需要填30到300之间的值。100表示正常速度和音调可以只改变速度RPG Maker做不到哦也可以同时改变速度和音调。
* 该项的效果不会进存档,必要时您可以配合使用“禁止存档”指令。
16. **呼出怪物手册和SL界面**
* 呼出手册只在勇士持有手册时生效,关闭手册后事件继续。
* 呼出存档界面最多只能存一个档然后事件继续(读档后事件现场会恢复)。
* 呼出读档界面如果不读档则事件继续,录像回放中这三条指令都被忽略。
17. **自动存档:**读档后也会恢复事件现场,录像回放中会照常存档。
* 如不勾选“不提示”则会`core.drawTip(“已自动存档”)`(即使在下面禁止的情况下!),此指令一般用于选择项/确认框之前。
18. **是否禁止存档:**该项允许暂时禁止玩家存档包括撞怪撞门的自动存档不包括上面的呼出存档界面和事件中自动存档的指令从而做出RPG中常见的“迷宫中只有特定位置可以存档或者必须消耗道具进行存档”的效果。
## UI绘制类瞬间
![image](img/uievent.jpg)
这个类别的指令全部是瞬间完成的有一一对应的API请放心使用除“绘制多行文本”外都可以逐个双击预览多行文本需要右击预览。游戏中这些指令都是画在`uievent`层的。
1. **UI绘制并预览**您可以把一堆UI绘制类指令塞进去然后双击黄框整体预览。
2. **清除画布:**擦除`uievent`层的一部分x和y为左上角坐标。
* 四个参数都支持使用表达式,任何一个参数不填都会删除整个`uievent`层。
* 实际调用`core.clearMap("uievent",x,y,width,height)`和`core.deleteCanvas("uievent")`,熟练后对其他画布使用也是可以的。
3. **设置画布属性:**
1. 字体:`italic和bold`表示斜体和加粗,可省略,字号和字体用于绘制文本。
2. 填充样式:绘制实心图形时的默认填色,可用调色器调配。
3. 边框样式:绘制空心图形时的默认边框颜色,可用调色器调配。
4. 线宽度:绘制线段、箭头和空心图形时的默认线宽,单位为像素。
5. 不透明度不大于1的非负数此项为“画笔”的不透明度。只影响接下来画的内容已经画上去的不受影响。
6. 对齐:绘制单行文本的对齐方式,左对齐、左右居中、右对齐。
7. 基准线:绘制单行文本的基准线,有六种写法。绘制单行文本的坐标其实是文本的基准点,
8. z值初始为135z值较大的画布将覆盖较小的详见“个性化”。闪烁光标的z值总是比上述的值大1即默认为136.
4. **绘制文本:**
* 这里仅绘制单行文本,坐标为基准点的像素坐标,需配合上述“对齐”和“基准线”使用。
* 如果设置了最大宽度,那么在超出此宽度时就会保持比例压缩到这个宽度。
* 正文只支持`${js表达式求值}`和`\r[变色]`,不支持其他一切转义序列,更不能手动换行。
* 实际执行`core.fillText("uievent", text, x, y, style, font, maxWidth)`
5. **绘制描边文本:**同上,但不支持限宽,描边效果同状态栏数字的黑边。
* 底层实现为`ctx.strokeText()`,描边颜色可以自己指定。
* 本指令对应的js函数为`core.ui.fillBoldText('uievent', text, x, y, style, font)`
6. **绘制多行文本:**双击进入多行编辑,预览请右击。
* 起点像素为左上角,只有设置了最大行宽才会对齐、居中和自动换行。
* 如果不设置颜色和字号,就会采用“设置剧情文本的属性”中的正文设置。
* 不设置行距就会采用字体大小的1.3倍建议采用偶数字号和1.5倍行距。
* 多行文本不支持字体样式的设置,使用的是全塔属性中的全局字体`Verdana`
* 如有需要,请使用“设置全局属性”指令来设置字体样式。
7. **绘制几何图形:**对应的js函数为`core.ui.fillXxx()`和`core.ui.strokeXxx()`
8. **绘制图片:**同“显示图片”指令但功能有出入,实际执行`core.drawImage('uievent',img,x,y,w,h,x1,y1,w1,h1)`
9. **绘制图标:**支持图块id和系统图标支持伸缩和选择哪一帧。支持32×32和32×48两种尺寸实际执行`core.drawIcon(name, id, x, y, w, h, frame)`
10. **绘制背景图:**背景色支持颜色数组,也支持类似`winskin.png`的图片名。使用图片时的不透明度以及纯色背景时的边框颜色由“设置画布属性”指定。本指令对应的js函数为`core.ui.drawBackground(left, top, right, bottom, posInfo)`
11. **绘制和清除闪烁光标:**如题光标的z值总是比`uievent`层大1.
12. **设置画布特效:**V2.8新增,允许给画布设置像图块一样的“虚化、色相、灰度、反色、阴影”属性。
==========================================================================================
[继续阅读下一章:个性化](personalization)

300
item.d.ts vendored Normal file
View File

@ -0,0 +1,300 @@
interface Item<I extends AllIdsOf<'items'>> {
/**
* id
*/
id: I;
/**
*
*/
cls: ItemClsOf<I>;
/**
*
*/
name: string;
/**
*
*/
text?: string;
/**
*
*/
hideInToolBox: boolean;
/**
*
*/
equip: ItemClsOf<I> extends 'equips' ? Equip : never;
/**
* 使使
*/
hideInReplay: boolean;
/**
*
*/
itemEffect?: string;
/**
*
*/
itemEffectTip?: string;
/**
* 使
*/
useItemEvent?: MotaEvent;
/**
* 使
*/
useItemEffect?: string;
/**
* 使
*/
canUseItemEffect?: string | boolean;
}
interface EquipBase {
/**
*
*/
value: Record<keyof SelectType<HeroStatus, number>, number>;
/**
*
*/
percentage: Record<keyof SelectType<HeroStatus, number>, number>;
}
interface Equip extends EquipBase {
/**
*
*/
type: number | string;
/**
*
*/
animate: AnimationIds;
/**
* 穿
*/
equipEvent?: MotaEvent;
/**
*
*/
unequipEvent?: MotaEvent;
}
/**
*
*/
interface Items {
/**
*
*/
getItems(): {
[P in AllIdsOf<'items'>]: Item<P>;
};
/**
*
* @example core.getItemEffect('redPotion', 10) // 执行获得10瓶红血的效果
* @param itemId id
* @param itemNum 1
*/
getItemEffect(itemId: AllIdsOf<'items'>, itemNum?: number): void;
/**
*
* @example core.getItemEffectTip(redPotion) // (获得 红血瓶)',生命+100'
* @param itemId id
* @returns itemEffectTip的内容
*/
getItemEffectTip(itemId: AllIdsOf<'items'>): string;
/**
* 使
* @example core.useItem('pickaxe', true) // 使用破墙镐,不计入录像,无回调
* @param itemId id
* @param noRoute 使true
* @param callback 使使
*/
useItem(
itemId: ItemIdOf<'tools' | 'constants'>,
noRoute?: boolean,
callback?: () => void
): void;
/**
* 使
* @example core.canUseItem('pickaxe') // 能否使用破墙镐
* @param itemId id
* @returns true表示可以使用
*/
canUseItem(itemId: AllIdsOf<'items'>): boolean;
/**
*
* @example core.itemCount('yellowKey') // 持有多少把黄钥匙
* @param itemId id
* @returns 穿
*/
itemCount(itemId: AllIdsOf<'items'>): number;
/**
* (穿)
* @example core.hasItem('yellowKey') // 主角是否持有黄钥匙
* @param itemId id
* @returns true表示持有
*/
hasItem(itemId: AllIdsOf<'items'>): boolean;
/**
* 穿
* @example core.hasEquip('sword5') // 主角是否装备了神圣剑
* @param itemId id
* @returns true表示已装备
*/
hasEquip(itemId: ItemIdOf<'equips'>): boolean;
/**
*
* @example core.getEquip(1) // 主角目前装备了什么盾牌
* @param equipType
* @returns idnull表示未穿戴
*/
getEquip(equipType: number): ItemIdOf<'equips'> | null;
/**
*
* @example core.setItem('yellowKey', 3) // 设置黄钥匙为3把
* @param itemId id
* @param itemNum 0
*/
setItem(itemId: AllIdsOf<'items'>, itemNum?: number): void;
/**
*
* @example core.addItem('yellowKey', -2) // 没收两把黄钥匙
* @param itemId id
* @param itemNum
*/
addItem(itemId: AllIdsOf<'items'>, itemNum?: number): void;
/**
* @deprecated 使addItem代替
* addItem(itemId, -n);
* @param itemId id
* @param itemNum
*/
removeItem(itemId?: AllIdsOf<'items'>, itemNum?: number): void;
/**
*
* @param equipId
*/
getEquipTypeByName(name?: ItemIdOf<'equips'>): number;
/**
*
* @example core.getEquipTypeById('shield5') // 1盾牌
* @param equipId id
* @returns
*/
getEquipTypeById(equipId: ItemIdOf<'equips'>): number;
/**
* 穿
* @example core.canEquip('sword5', true) // 主角可以装备神圣剑吗,如果不能会有提示
* @param equipId id
* @param hint 穿
* @returns true表示可以穿上false表示无法穿上
*/
canEquip(equipId: ItemIdOf<'equips'>, hint?: boolean): boolean;
/**
* 穿
* @example core.loadEquip('sword5') // 尝试装备上背包里的神圣剑,无回调
* @param equipId id
* @param callback 穿
*/
loadEquip(equipId: ItemIdOf<'equips'>, callback?: () => void): void;
/**
*
* @example core.unloadEquip(1) // 卸下盾牌,无回调
* @param equipType
* @param callback
*/
unloadEquip(equipType: number, callback?: () => void): void;
/**
*
* @example core.compareEquipment('sword5', 'shield5') // 比较神圣剑和神圣盾的优劣
* @param compareEquipId id
* @param beComparedEquipId id
* @returns 0
*/
compareEquipment<F extends ItemIdOf<'equips'>>(
compareEquipId: F,
beComparedEquipId: Exclude<ItemIdOf<'equips'>, F>
): EquipBase;
/**
*
* @example core.quickSaveEquip(1) // 将当前套装保存为1号套装
* @param index
*/
quickSaveEquip(index: number): void;
/**
*
* @example core.quickLoadEquip(1) // 快速换上1号套装
* @param index
*/
quickLoadEquip(index: number): void;
/**
*
* @example core.setEquip('sword1', 'value', 'atk', 300, '+='); // 设置铁剑的攻击力数值再加300
* @param equipId id
* @param valueType valuepercentage
* @param name atk
* @param value
* @param operator +=
* @param prefix
*/
setEquip(
equipId: ItemIdOf<'equips'>,
valueType: 'value' | 'percentage',
name: keyof SelectType<HeroStatus, number>,
value: number,
operator?: MotaOperator,
prefix?: string
): void;
/**
* 穿
* @param type
* @param loadId
* @param unloadId
* @param callback
*/
_realLoadEquip(
type: number,
loadId?: ItemIdOf<'equips'>,
unloadId?: ItemIdOf<'equips'>,
callback?: () => void
): void;
}
declare const items: new () => Items;

3385
items.js Normal file

File diff suppressed because it is too large Load Diff

73
loader.d.ts vendored Normal file
View File

@ -0,0 +1,73 @@
/**
*
*/
interface Loader {
/**
*
* @param dir
* @param names
* @param toSave
* @param callback
*/
loadImages(
dir: string,
names: string[],
toSave: Record<string, HTMLImageElement>,
callback?: () => void
): void;
/**
*
* @param dir
* @param imgName
* @param callback
*/
loadImage(
dir: string,
imgName: string,
callback?: (name: string, img: HTMLImageElement) => void
): void;
/**
* zip中加载一系列图片
* @param url
* @param names
*/
loadImagesFromZip(
url: string,
names: string,
toSave: Record<string, HTMLImageElement>,
onprogress?: (loaded: number, total: number) => void,
onfinished?: () => void
): void;
/**
*
* @param name
*/
loadOneMusic(name: BgmIds): void;
/**
*
* @param name
*/
loadOneSound(name: SoundIds): void;
/**
* bgm
* @param name bgm的id或名称
*/
loadBgm(name: BgmIds | NameMapIn<BgmIds>): void;
/**
* bgm的缓存
* @param name bgm的id或名称
*/
freeBgm(name: BgmIds | NameMapIn<BgmIds>): void;
_loadMaterials_afterLoad(): void;
_loadAnimate(data: string): Animate;
}
declare const loader: new () => Loader;

516
loader.js Normal file
View File

@ -0,0 +1,516 @@
/*
loader.js负责对资源的加载
*/
"use strict";
function loader () {
this._init();
}
loader.prototype._init = function () {
}
////// 设置加载进度条进度 //////
loader.prototype._setStartProgressVal = function (val) {
core.dom.startTopProgress.style.width = val + '%';
}
////// 设置加载进度条提示文字 //////
loader.prototype._setStartLoadTipText = function (text) {
core.dom.startTopLoadTips.innerText = text;
}
loader.prototype._load = function (callback) {
if (main.useCompress) {
this._load_async(callback);
} else {
this._load_sync(callback);
}
}
loader.prototype._load_sync = function (callback) {
this._loadAnimates_sync();
this._loadMusic_sync();
core.loader._loadMaterials_sync(function () {
core.loader._loadExtraImages_sync(function () {
core.loader._loadAutotiles_sync(function () {
core.loader._loadTilesets_sync(callback);
})
})
});
}
loader.prototype._load_async = function (callback) {
core.loader._setStartLoadTipText('正在加载资源文件...');
var all = {};
var _makeOnProgress = function (name) {
if (!all[name]) all[name] = { loaded: 0, total: 0, finished: false };
return function (loaded, total) {
all[name].loaded = loaded;
all[name].total = total;
var allLoaded = 0, allTotal = 0;
for (var one in all) {
allLoaded += all[one].loaded;
allTotal += all[one].total;
}
if (allTotal > 0) {
if (allLoaded == allTotal) {
core.loader._setStartLoadTipText("正在处理资源文件... 请稍候...");
} else {
core.loader._setStartLoadTipText('正在加载资源文件... ' +
core.formatSize(allLoaded) + " / " + core.formatSize(allTotal) +
" (" + (allLoaded / allTotal * 100).toFixed(2) + "%)");
}
core.loader._setStartProgressVal(allLoaded / allTotal * 100);
}
};
}
var _makeOnFinished = function (name) {
return function () {
setTimeout(function () {
all[name].finished = true;
for (var one in all) {
if (!all[one].finished) return;
}
callback();
});
}
}
this._loadAnimates_async(_makeOnProgress('animates'), _makeOnFinished('animates'));
this._loadMusic_async(_makeOnProgress('sounds'), _makeOnFinished('sounds'));
this._loadMaterials_async(_makeOnProgress('materials'), _makeOnFinished('materials'));
this._loadExtraImages_async(_makeOnProgress('images'), _makeOnFinished('images'));
this._loadAutotiles_async(_makeOnProgress('autotiles'), _makeOnFinished('autotiles'));
this._loadTilesets_async(_makeOnProgress('tilesets'), _makeOnFinished('tilesets'));
}
// ----- 加载资源文件 ------ //
loader.prototype._loadMaterials_sync = function (callback) {
this._setStartLoadTipText("正在加载资源文件...");
this.loadImages("materials", core.materials, core.material.images, function () {
core.loader._loadMaterials_afterLoad();
callback();
});
}
loader.prototype._loadMaterials_async = function (onprogress, onfinished) {
this.loadImagesFromZip('project/materials/materials.h5data', core.materials, core.material.images, onprogress, function () {
core.loader._loadMaterials_afterLoad();
onfinished();
});
}
loader.prototype._loadMaterials_afterLoad = function () {
var images = core.splitImage(core.material.images['icons']);
for (var key in core.statusBar.icons) {
if (typeof core.statusBar.icons[key] == 'number') {
core.statusBar.icons[key] = images[core.statusBar.icons[key]];
if (core.statusBar.image[key] != null)
core.statusBar.image[key].src = core.statusBar.icons[key].src;
}
}
}
// ------ 加载使用的图片 ------ //
loader.prototype._loadExtraImages_sync = function (callback) {
core.material.images.images = {};
this._setStartLoadTipText("正在加载图片文件...");
core.loadImages("images", core.images, core.material.images.images, callback);
}
loader.prototype._loadExtraImages_async = function (onprogress, onfinished) {
core.material.images.images = {};
var images = core.images;
// Check .gif
var gifs = images.filter(function (name) {
return name.toLowerCase().endsWith('.gif');
});
images = images.filter(function (name) {
return !name.toLowerCase().endsWith('.gif');
});
this.loadImagesFromZip('project/images/images.h5data', images, core.material.images.images, onprogress, onfinished);
// gif没有被压缩在zip中延迟加载...
gifs.forEach(function (gif) {
this.loadImage("images", gif, function (id, image) {
if (image != null) {
core.material.images.images[gif] = image;
}
});
}, this);
}
// ------ 加载自动元件 ------ //
loader.prototype._loadAutotiles_sync = function (callback) {
core.material.images.autotile = {};
var keys = Object.keys(core.material.icons.autotile);
var autotiles = {};
this._setStartLoadTipText("正在加载自动元件...");
this.loadImages("autotiles", keys, autotiles, function () {
core.loader._loadAutotiles_afterLoad(keys, autotiles);
callback();
});
}
loader.prototype._loadAutotiles_async = function (onprogress, onfinished) {
core.material.images.autotile = {};
var keys = Object.keys(core.material.icons.autotile);
var autotiles = {};
this.loadImagesFromZip('project/autotiles/autotiles.h5data', keys, autotiles, onprogress, function () {
core.loader._loadAutotiles_afterLoad(keys, autotiles);
onfinished();
});
}
loader.prototype._loadAutotiles_afterLoad = function (keys, autotiles) {
// autotile需要保证顺序
keys.forEach(function (v) {
core.material.images.autotile[v] = autotiles[v];
});
setTimeout(function () {
core.maps._makeAutotileEdges();
});
}
// ------ 加载额外素材 ------ //
loader.prototype._loadTilesets_sync = function (callback) {
core.material.images.tilesets = {};
this._setStartLoadTipText("正在加载额外素材...");
this.loadImages("tilesets", core.tilesets, core.material.images.tilesets, function () {
core.loader._loadTilesets_afterLoad();
callback();
});
}
loader.prototype._loadTilesets_async = function (onprogress, onfinished) {
core.material.images.tilesets = {};
this.loadImagesFromZip('project/tilesets/tilesets.h5data', core.tilesets, core.material.images.tilesets, onprogress, function () {
core.loader._loadTilesets_afterLoad();
onfinished();
});
}
loader.prototype._loadTilesets_afterLoad = function () {
// 检查宽高是32倍数如果出错在控制台报错
for (var imgName in core.material.images.tilesets) {
var img = core.material.images.tilesets[imgName];
if (img.width % 32 != 0 || img.height % 32 != 0) {
console.warn("警告!" + imgName + "的宽或高不是32的倍数");
}
if (img.width * img.height > 32 * 32 * 3000) {
console.warn("警告!" + imgName + "上的图块素材个数大于3000");
}
}
}
// ------ 实际加载一系列图片 ------ //
loader.prototype.loadImages = function (dir, names, toSave, callback) {
if (!names || names.length == 0) {
if (callback) callback();
return;
}
var items = 0;
for (var i = 0; i < names.length; i++) {
this.loadImage(dir, names[i], function (id, image) {
core.loader._setStartLoadTipText('正在加载图片 ' + id + "...");
if (toSave[id] !== undefined) {
if (image != null)
toSave[id] = image;
return;
}
toSave[id] = image;
items++;
core.loader._setStartProgressVal(items * (100 / names.length));
if (items == names.length) {
if (callback) callback();
}
})
}
}
loader.prototype.loadImage = function (dir, imgName, callback) {
try {
var name = imgName;
if (name.indexOf(".") < 0)
name = name + ".png";
var image = new Image();
image.onload = function () {
image.setAttribute('_width', image.width);
image.setAttribute('_height', image.height);
callback(imgName, image);
}
image.onerror = function () {
callback(imgName, null);
}
image.src = 'project/' + dir + '/' + name + "?v=" + main.version;
if (name.endsWith('.gif'))
callback(imgName, null);
}
catch (e) {
console.error(e);
}
}
// ------ 从zip中加载一系列图片 ------ //
loader.prototype.loadImagesFromZip = function (url, names, toSave, onprogress, onfinished) {
if (!names || names.length == 0) {
if (onfinished) onfinished();
return;
}
core.unzip(url + "?v=" + main.version, function (data) {
var cnt = 1;
names.forEach(function (name) {
var imgName = name;
if (imgName.indexOf('.') < 0) imgName += '.png';
if (imgName in data) {
var img = new Image();
var url = URL.createObjectURL(data[imgName]);
cnt++;
img.onload = function () {
cnt--;
URL.revokeObjectURL(url);
img.setAttribute('_width', img.width);
img.setAttribute('_height', img.height);
if (cnt == 0 && onfinished) onfinished();
}
img.src = url;
toSave[name] = img;
}
});
cnt--;
if (cnt == 0 && onfinished) onfinished();
}, null, false, onprogress);
}
// ------ 加载动画文件 ------ //
loader.prototype._loadAnimates_sync = function () {
this._setStartLoadTipText("正在加载动画文件...");
if (main.supportBunch) {
if (core.animates.length > 0) {
core.http('GET', '__all_animates__?v=' + main.version + '&id=' + core.animates.join(','), null, function (content) {
var u = content.split('@@@~~~###~~~@@@');
for (var i = 0; i < core.animates.length; ++i) {
if (u[i] != '') {
core.material.animates[core.animates[i]] = core.loader._loadAnimate(u[i]);
} else {
console.error('无法找到动画文件' + core.animates[i] + '');
}
}
}, "text/plain; charset=x-user-defined");
}
return;
}
core.animates.forEach(function (t) {
core.http('GET', 'project/animates/' + t + ".animate?v=" + main.version, null, function (content) {
core.material.animates[t] = core.loader._loadAnimate(content);
}, function (e) {
console.error(e);
core.material.animates[t] = null;
}, "text/plain; charset=x-user-defined")
});
}
loader.prototype._loadAnimates_async = function (onprogress, onfinished) {
core.unzip('project/animates/animates.h5data?v=' + main.version, function (animates) {
for (var name in animates) {
if (name.endsWith(".animate")) {
var t = name.substring(0, name.length - 8);
if (core.animates.indexOf(t) >= 0)
core.material.animates[t] = core.loader._loadAnimate(animates[name]);
}
}
onfinished();
}, null, true, onprogress);
}
loader.prototype._loadAnimate = function (content) {
try {
content = JSON.parse(content);
var data = {};
data.ratio = content.ratio;
data.se = content.se;
data.pitch = content.pitch;
data.images = [];
content.bitmaps.forEach(function (t2) {
if (!t2) {
data.images.push(null);
}
else {
try {
var image = new Image();
image.src = t2;
data.images.push(image);
} catch (e) {
console.error(e);
data.images.push(null);
}
}
})
data.frame = content.frame_max;
data.frames = [];
content.frames.forEach(function (t2) {
var info = [];
t2.forEach(function (t3) {
info.push({
'index': t3[0],
'x': t3[1],
'y': t3[2],
'zoom': t3[3],
'opacity': t3[4],
'mirror': t3[5] || 0,
'angle': t3[6] || 0,
})
})
data.frames.push(info);
});
return data;
}
catch (e) {
console.error(e);
return null;
}
}
// ------ 加载音乐和音效 ------ //
loader.prototype._loadMusic_sync = function () {
this._setStartLoadTipText("正在加载音效文件...");
core.bgms.forEach(function (t) {
core.loader.loadOneMusic(t);
});
core.sounds.forEach(function (t) {
core.loader.loadOneSound(t);
});
// 直接开始播放
core.playBgm(main.startBgm);
}
loader.prototype._loadMusic_async = function (onprogress, onfinished) {
core.bgms.forEach(function (t) {
core.loader.loadOneMusic(t);
});
core.unzip('project/sounds/sounds.h5data?v=' + main.version, function (data) {
// 延迟解析
setTimeout(function () {
for (var name in data) {
if (core.sounds.indexOf(name) >= 0) {
core.loader._loadOneSound_decodeData(name, data[name]);
}
}
});
onfinished();
}, null, false, onprogress);
// 直接开始播放
core.playBgm(main.startBgm);
}
loader.prototype.loadOneMusic = function (name) {
var music = new Audio();
music.preload = 'none';
if (main.bgmRemote) music.src = main.bgmRemoteRoot + core.firstData.name + '/' + name;
else music.src = 'project/bgms/' + name;
music.loop = 'loop';
core.material.bgms[name] = music;
}
loader.prototype.loadOneSound = function (name) {
core.http('GET', 'project/sounds/' + name + "?v=" + main.version, null, function (data) {
core.loader._loadOneSound_decodeData(name, data);
}, function (e) {
console.error(e);
core.material.sounds[name] = null;
}, null, 'arraybuffer');
}
loader.prototype._loadOneSound_decodeData = function (name, data) {
if (data instanceof Blob) {
var blobReader = new zip.BlobReader(data);
blobReader.init(function () {
blobReader.readUint8Array(0, blobReader.size, function (uint8) {
core.loader._loadOneSound_decodeData(name, uint8.buffer);
})
});
return;
}
try {
core.musicStatus.audioContext.decodeAudioData(data, function (buffer) {
core.material.sounds[name] = buffer;
}, function (e) {
console.error(e);
core.material.sounds[name] = null;
})
}
catch (e) {
console.error(e);
core.material.sounds[name] = null;
}
}
loader.prototype.loadBgm = function (name) {
name = core.getMappedName(name);
if (!core.material.bgms[name]) return;
// 如果没开启音乐,则不预加载
if (!core.musicStatus.bgmStatus) return;
// 是否已经预加载过
var index = core.musicStatus.cachedBgms.indexOf(name);
if (index >= 0) {
core.musicStatus.cachedBgms.splice(index, 1);
}
else {
// 预加载BGM
this._preloadBgm(core.material.bgms[name]);
// core.material.bgms[name].load();
// 清理尾巴
if (core.musicStatus.cachedBgms.length == core.musicStatus.cachedBgmCount) {
this.freeBgm(core.musicStatus.cachedBgms.pop());
}
}
// 移动到缓存最前方
core.musicStatus.cachedBgms.unshift(name);
}
loader.prototype._preloadBgm = function (bgm) {
bgm.volume = 0;
bgm.play();
}
loader.prototype.freeBgm = function (name) {
name = core.getMappedName(name);
if (!core.material.bgms[name]) return;
// 从cachedBgms中删除
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(function (t) {
return t != name;
});
// 清掉缓存
core.material.bgms[name].removeAttribute("src");
core.material.bgms[name].load();
core.material.bgms[name] = null;
if (name == core.musicStatus.playingBgm) {
core.musicStatus.playingBgm = null;
}
// 三秒后重新加载
setTimeout(function () {
core.loader.loadOneMusic(name);
}, 3000);
}

110
localSave.js Normal file
View File

@ -0,0 +1,110 @@
/**
* 离线游戏使用本地存储扩展
* 开启本拓展后将会把所有存档存至 _saves 目录下
* 需配合样板V2.8.2+使用
*/
"use strict";
(function () {
// 将这一行改成 false 可以禁用本拓展
var __enabled = true;
if (window.jsinterface || !window.fs || !__enabled) return;
function rewrite() {
core.utils._setLocalForage_set = function (name, str, callback) {
var data = LZString.compressToBase64(str);
core.saves.cache[name] = data;
fs.writeFile('_saves/' + name, data, 'utf-8', callback);
}
core.utils._getLocalForage_get = function (name, callback) {
fs.readFile('_saves/' + name, 'utf-8', function (err, data) {
if (err) return callback(err);
callback(null, data);
});
}
core.utils.decompress = function (data) {
try {
return JSON.parse(LZString.decompressFromBase64(data))
} catch (e) {
return null;
}
}
core.utils._removeLocalForage_remove = function (name, callback) {
fs.deleteFile('_saves/' + name, callback);
}
core.utils.clearLocalForage = function (callback) {
fs.deleteFile('_saves', function () {
fs.mkdir('_saves', callback);
})
}
core.utils.iterateLocalForage = function (iter, callback) {
fs.readdir('_saves', function (err, data) {
if (err) callback(err);
else {
data.forEach(function (one) {
iter(null, one, null);
});
callback();
}
});
}
core.utils.keysLocalForage = function (callback) {
fs.readdir('_saves', callback);
}
core.utils.lengthLocalForage = function (callback) {
fs.readdir('_saves', function (err, data) {
if (err) callback(err);
else callback(null, data.length);
});
}
}
var _export = function () {
var toExport = [];
localforage.iterate(function (value, key, n) {
if (value == null || !key.startsWith(core.firstData.name)) return;
value = core.decompress(value);
if (value == null) return;
var str = JSON.stringify(value).replace(/[\u007F-\uFFFF]/g, function (chr) {
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
});
str = LZString.compressToBase64(str);
toExport.push(key);
core.saves.cache[key] = str;
fs.writeFile('_saves/' + key, str, 'utf-8', function () {});
}, function () {
if (toExport.length > 0) {
alert('提示!本塔已开启存档本地化!原始存档已全部导出至 _saves/ 目录下。');
}
fs.writeFile('_saves/.exported', '1', 'utf-8', function () {});
rewrite();
core.control.getSaveIndexes(function (indexes) { core.saves.ids = indexes; });
});
}
fs.mkdir('_saves', function (err) {
if (err) return;
fs.readFile('_saves/.exported', 'utf-8', function(err, data) {
if (!err && data) {
rewrite();
core.control.getSaveIndexes(function (indexes) { core.saves.ids = indexes; });
return;
}
_export();
});
});
})();

1402
map.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

1002
maps.js Normal file

File diff suppressed because it is too large Load Diff

441
personalization.md Normal file
View File

@ -0,0 +1,441 @@
# 个性化
?> 在这一节中,让我们来了解更多对样板个性化的修改
有时候只靠样板本身可能是不够的。我们需要一些个性化、自定义的素材,道具效果,怪物属性,等等。
## 图层的说明
HTML5魔塔是使用画布canvas来绘制存在若干个图层它们之间有一个覆盖关系后面的图层将覆盖前面的图层。
所有图层从低往高依次如下:(加[B]的代表该层是大地图,[D]代表由系统按需动态创建z-index代表该层的纵向高度
- bg**[B]**背景层绘制背景图层素材bgmap和背景贴图 (z-index: 10)
- event**[B]**事件层所有事件道具、墙壁、NPC、怪物等都绘制在这一层进行处理 (z-index: 30)
- hero勇士层主要用来绘制勇士 (z-index: 40)
- event2**[B]**事件2层本层主要用来绘制48x32的图片素材的上半部分避免和勇士错位 (z-index: 50)
- fg**[B]**前景层绘制前景图层素材fgmap和前景贴图 (z-index: 60)
- damage**[B]**:显伤层;主要用来绘制怪物显伤和领域显伤 (z-index: 65)
- animate动画层主要用来绘制动画。 (z-index: 70)
- weather**[D]**:天气层;主要用来绘制天气(雨/雪/雾) (z-index: 80)
- route**[D]**:路线层;主要用来绘制勇士的行走路线图。 (z-index: 95)
- curtain色调层用来控制当前楼层的画面色调 (z-index: 125)
- image1\~50**[D]**图片层用来绘制图片等操作。z-index: 100+code, 101~150
- uievent**[D]**自定义UI绘制层用来进行自定义UI绘制等操作。z-index135可以通过事件设置该值
- uiUI层用来绘制一切UI窗口如剧情文本、怪物手册、楼传器、系统菜单等等 (z-index: 140)
- data数据层用来绘制一些顶层的或更新比较快的数据如左上角的提示战斗界面中数据的变化等等。 (z-index: 170)
请注意显示图片事件将自动创建一个图片层z-index是100+图片编号。
色调层的z-index是25ui层的z-index是140因此图片编号在1~24的将被色调层遮挡25~40的将被ui层遮挡41~50的将遮挡UI层。
uievent层为自定义UI绘制所在的层其z值初始是135可以通过事件设置自定义绘制的闪烁光标所在层的z值永远比该值大1。
### 动态创建canvas
从V2.5.3开始可以在H5样板中任意动态创建canvas并进行使用。
使用`core.createCanvas(name, x, y, w, h, z)`来动态创建一个画布。
其中name为动态canvas名称x,y,w,h为创建的画布相对窗口左上角的像素坐标和长宽z为画布的纵向高度。
例如:`core.createCanvas('test', 10, 20, 100, 200, 74)` 创建了一个名为test的画布其左上角相对窗口的像素坐标为(10,20)宽100高200纵向高度74在动画层和天气层之间
该函数会返回画布的context也可以通过 `core.dymCanvas[name]` 来获得;例如 `core.dymCanvas.test` 就是我们上面创建的画布的context然后进行操作。
也可以简单的使用`core.fillText()`, `core.fillRect()`, `core.strokeRect()`等等对画布进行任意绘制。
``` js
core.fillText('test', '这是一段文字', 10, 30, '#FF0000', '16px Verdana'); // 绘制一段文本
```
使用 `core.deleteCanvas(name)` 删除一个动态创建的画布,例如 `core.deleteCanvas('test')`
`core.deleteAllCanvas()`可以删除所有动态创建的画布,`core.relocateCanvas(name, x, y)`和`core.resizeCanvas(name, x, y)`可以对画布的位置和大小进行改变。
更多详细API请参见[API列表](api)。
## 单点多事件
带有独立开关的商人算是一个最为简单的单点多事件的例子了(提供在了事件编辑器左侧的“常用事件模板”),单点多事件最常用的指令就是“转变图块”(或关门)和“条件分歧”。下面以几个具体例子来进行详细说明:
1. 打怪变成门或道具、开门变成怪或道具怪物、门、道具都是系统触发器直接利用afterXxx事件。如
* 战后事件:关门`yellowDoor`(原地关上了一扇黄门)
* 战后事件:转变图块为`yellowKey`(怪物掉落了黄钥匙)
* 开门后事件:转变图块为`greenSlime`或`yellowKey`(开门变成了史莱姆或黄钥匙)
* 打怪变成别的怪、开门关上别的门、门和怪来回变的,用独立开关计数。
* 如有批量需求,请使用“脚本编辑 - `afterXxx`函数或怪物的(批量)战前/战后事件。
2. 打怪/开门/捡道具后变成传送点或npc
* 这时的传送点就不能用“楼梯、传送门”事件了,而应该用只有一条“场景切换”指令的普通事件。
* 此事件不勾选“覆盖触发器”,这样怪物、门和道具的系统触发器会触发。
* 在对应的`afterXxx`事件中“转变图块为”传送点或npc淡入效果更佳哦。
* 请注意因为道具是可通行的如果勇士走到道具上就会和npc重合。
3. (懒人的选择)利用`图块类别x,y`和`图块IDx,y`等值块集中处理:
* 如果您实在搞不清楚“覆盖触发器”和“通行状态”这两项,那就干脆勾选前者并把后者设为不可通行,本希望可以通行的场合就得用“无视地形移动勇士”指令前进一步。
* 用自动事件去转变图块,然后利用上述两个值块判定当前图块的状态等,再用“强制战斗”、“开门(需要钥匙)”和“增加道具”分别处理吧。
## 并行脚本:即时制的希望?
如前所述,自动事件可以让您不用考虑可能导致各值块发生变化的缘由,而是简单地在刷新状态栏时检测这些值块是否满足某些特定的关系。
然而,自动事件依然是通过插入到当前待执行指令队列的开头,或追加到队列的结尾来执行的。换言之,它占用的是事件流本身的线程资源。
那形如bgs、bgv、飘云飘雾、地图背景旋转等即使玩家挂机也会照常执行的特效该怎么办呢
js语言其实是没有多线程的但我们可以写一些浏览器帧刷新时执行的脚本也就是“并行脚本”。
并行脚本分为两种,全塔并行和楼层并行。前者在“脚本编辑—并行脚本”,后者在楼层属性。一般来说,当您有多个楼层需要执行相同的并行脚本时,建议写在前者并通过对`core.status.floorId`的范围进行判定(注意判空!)来决定具体执行的内容。
并行脚本将被系统反复执行执行的时机是“浏览器帧刷新”换言之相邻两次执行的间隔取决于浏览器或设备的性能上限为60fps即每秒60次。
如果有一个bgs是1秒长的心跳声我们把它注册到了全塔属性的音效中。假设这个bgs要被用于MT0层那么我们在MT0层的“楼层属性——并行脚本”中写这样的代码
``` js
if (core.hasFlag('frame')) {
// 剧情事件中将“变量frame”设为正整数来开启bgs设为0来关闭
core.status.hero.flags.frame %= 60000; // 防止挂机太久导致溢出
if (core.getFlag('frame', 0) % 60 === 0)
core.playSound('heartBeat.mp3'); // 每60帧即1秒播放一次还可以指定音调。
core.status.hero.flags.frame++; // 帧数加1
} // 其他特效也是一样的用法。
```
在并行脚本(全塔或楼层)里可以使用`timestamp`作为参数,表示从游戏开始到当前总共过了多少毫秒。
## 覆盖楼传事件
对于特殊的塔,我们可以考虑修改楼传事件来完成一些特殊的要求,比如镜子可以按楼传来切换表里。
要修改楼传事件,需要进行如下两步:
1. 重写楼传的点击事件。在插件中对`core.control.useFly`进行重写。详细代码参见[重写点击楼传事件](script#重写点击楼传事件)。
2. 修改楼传的使用事件。和其他永久道具一样,在地图编辑器的图块属性中修改楼传的`useItemEvent`(或`useItemEffect`)和`canUseItemEffect`两个内容。例如:
``` js
"useItemEvent": "[...]" // 执行某段自定义事件
"canUseItemEffect": "true" // 任何时候可用
```
3. 将全塔属性里的`楼梯边楼传`的勾去掉。
除了覆盖楼传事件外,对于快捷商店、虚拟键盘等等也可以进行覆盖,只不过是仿照上述代码重写对应的函数(`openQuickShop`,`openKeyBoard`)即可。
## 自定义快捷键
如果需要绑定某个快捷键为处理一段事件,也是可行的。
要修改按键,我们可以在脚本编辑的`onKeyUp`进行处理:
我们设置一个快捷键进行绑定,比如 `Y`,其 `keycode``89`
(大键盘数字键 `0-9``keycode``48-57, A-Z` 键的 `keycode``65-90` ,其他键的 `keycode` 搜一下就能得到)
然后在脚本编辑的`onKeyUp`函数的`switch`中进行处理。
``` js
case 89: // 使用该按键的keyCode比如Y键就是89
// 还可以再判定altKey是否被按下即 if (altKey) { ...
// ... 在这里写你要执行脚本
// **强烈建议所有新增的自定义快捷键均能给个对应的道具可点击,以方便手机端的行为**
if (core.hasItem('...')) {
core.status.route.push("key:0"); // 记录按键到录像中
core.useItem('...', true); // 第二个参数true代表该次使用道具是被按键触发的使用过程不计入录像
}
break;
```
强烈建议所有新增的自定义非数字快捷键均给个对应的永久道具可点击,以方便手机端的行为。
使用`core.status.route.push("key:"+keyCode)`可以将这次按键记录在录像中。
!> 如果记录了按键且使用道具的话需要将useItem的第二个参数设为true避免重复记录
可以使用`altKey`来判断Alt键是否被同时按下V2.8起手机端的Alt键以粘滞键方式提供
## 左手模式
V2.7.3起样板面向玩家提供了“左手模式”可在ESC菜单中开启。开启后wsad将用于勇士移动ijkl将代替原本wsad的功能存读档等
## 插件系统
在H5中提供了“插件”系统。在V2.6中提供了一个插件下拉框,用户可以自行创建和写插件。
在插件编写的过程中,我们可以使用任何[常见API](api)里面的代码调用;也可以通过`core.insertAction`来插入自定义事件执行。
下面是一个很简单的例子我编写一个插件函数其效果是让勇士生命值变成原来的x倍并令面前的图块消失。
``` js
this.myfunc = function(x) {
core.status.hero.hp *= x; // 勇士生命翻若干倍
core.insertAction([ // 自定义事件:令面前的图块消失。
{"type": "setValue", "name": "flag:x", "value": "core.nextX()"},
{"type": "setValue", "name": "flag:y", "value": "core.nextY()"},
{"type": "hide", "loc": ["flag:x", "flag:y"]}
]);
}
```
然后比如我们在某个道具的使用效果 `useItemEffect` 中写 `core.plugin.myfunc(2)` 即可调用此插件函数。也可以在战后事件或自定义脚本等位置来写。
网站上也提供了一个[插件库](https://h5mota.com/plugins/),欢迎大家把自己写的插件进行共享。
从V2.6开始,在插件中用`this.xxx`定义的函数将会被转发到core中。例如上述的`myfunc`除了`core.plugin.myfunc`外也可以直接`core.myfunc`调用。
详见[函数的转发](script#函数的转发)。
## 手机端按键模式
从V2.5.3以后,我们可以给手机端增加按键了,这样将非常有利于技能的释放。
用户在竖屏模式下点击难度标签,就会在工具栏按钮和快捷键模式之间进行切换。
切换到快捷键模式后可以点1-7分别等价于在电脑端按键1-7。
V2.8起点击最后的A键则相当于电脑端按下/松开Alt键即粘滞键可以用来快捷换装。
可以在脚本编辑的`onKeyUp`中定义每个快捷键的使用效果,比如使用道具或释放技能等。
默认值下1使用破2使用炸3使用飞4使用其他存在的道具5读取上一个自动存档6读取下一个自动存档7轻按。可以相应修改成自己的效果。
也可以替换icons.png中的对应图标以及修改main.js中`main.statusBar.image.btn1~8`中的onclick事件来自定义按钮和对应按键。
非竖屏模式下、回放录像中、隐藏状态栏中,将不允许进行切换。
## 自绘状态栏
从V2.5.3开始允许自绘状态栏。要自绘状态栏,则应该打开全塔属性中的`statusCanvas`开关。
自绘模式下,全塔属性中的`statusCanvasRowsOnMobile`将控制竖屏模式下的状态栏行数(下面的`rows`)。
开启自绘模式后,可以在脚本编辑的`drawStatusBar`中自行进行绘制。
横屏模式下的状态栏为`129x416`15x15则是`149x480`);竖屏模式下的状态栏为`416*(32*rows+9)`15x15是480
具体可详见脚本编辑的`drawStatusBar`函数。
自绘状态栏开启后,金币图标将失去打开快捷商店的功能,您可以修改脚本编辑的 `onStatusBarCLick` 函数来适配。
## 自定义状态栏的显示项
在V2.2以后,我们可以自定义状态栏背景图(全塔属性 - 主样式)等等。
但是,如果我们还想新增其他项目的显示,比如攻速或者暴击,该怎么办?
我们可以[自绘状态栏](#自绘状态栏),或者采用下面两个方式之一来新增。
### 利用已有项目
一个最为简单的方式是,直接利用已有项目。
例如,如果本塔中没有技能栏,则可以使用技能栏所对应的显示项。
1. 覆盖project/icons.png中技能的图标
2. 打开全塔属性的enableSkill开关
3. 在脚本编辑-updateStatusBar中可以直接替换技能栏的显示内容
```
// 替换成你想显示的内容比如你定义的一个flag:abc。
core.setStatusBarInnerHTML('skill', core.getFlag("abc", 0));
```
### 额外新增新项目
如果是在需要给状态栏新定义项目,则需要进行如下几个操作:
1. 定义ID比如攻速我就定义speed暴击可以简单的定义baoji你也可以定义其他的ID但是不能和已有的重复。这里以speed为例。
2. 在index.html的statusBar中进行该状态栏项的定义。仿照其他几项插在其应当显示的位置注意替换掉相应的ID。
``` html
<div class="status" id="speedCol">
<img id="img-speed">
<p class='statusLabel' id='speed'></p>
</div>
```
4. 使用便捷PS工具打开project/icons.png新增一行并将魔力的图标P上去记下其索引比如37从0开始数
5. 在main.js的this.statusBar中增加图片、图标和内容的定义。
``` js
this.statusBar = {
'images': {
// ...其他略
'speed': document.getElementById("img-speed"), // 图片的定义
},
'icons': {
// ...其他略
'speed': 37, // 图标的定义这里对应的是icons.png中的索引
},
// ...其他略
'speed': document.getElementById('speed'), // 显示内容(数据)的定义
}
```
6. 显示内容的设置。在脚本编辑的updateStatusBar函数可以对该状态栏显示内容进行设置下面是几个例子。
``` js
// 设置其显示内容为status:speed值需要在project/data.js中firstData的hero那里新增初始值`"speed": 0`。
core.setStatusBarInnerHTML('speed', core.getStatus('speed'));
// 设置其显示内容为flag:speed值无需额外进行定义。
core.setStatusBarInnerHTML('speed', core.getFlag('speed', 0));
```
总的来说不建议这样做,因为`main.js`和各种html文件不在`project`文件夹,会导致随样板更新迁移接档变得困难。
## 技能塔的支持
从V2.5开始,内置了"二倍斩"技能,可以仿照其制作自己的技能。要支持技能塔,可能需要如下几个方面:
- 魔力(和上限)的添加;技能的定义
- 状态栏的显示
- 技能的触发(按键与录像问题)
- 技能的效果
### 魔力的定义添加;技能的定义
从V2.5开始,提供了 `status:mana` 选项,可以直接代表当前魔力值。可以在全塔属性勾选来启用它。
如果需要魔力上限,则可以使用 `status:manaMax` 来表示当前的魔力最大值,负数表示没有上限。
同时我们可以使用flag:skill表示当前开启的技能编号flag:skillName表示当前开启的技能名称。
如果flag:skill不为0则代表当前处于某个技能开启状态且状态栏显示flag:skillName值。伤害计算函数中只需要对flag:skill进行处理即可。
### 状态栏的显示
从V2.5开始,魔力值和技能名的状态栏项目已经被添加,可以直接使用。
在脚本编辑-updateStatusBar中可以对状态栏显示内容进行修改。带有上限时如果一行放不下可以想办法像生命一样拆成两行。
``` js
// 设置魔力值; status:manamax 只有在非负时才生效。
if (core.status.hero.manamax != null && core.status.hero.manamax >= 0) {
core.status.hero.mana = Math.min(core.status.hero.mana, core.status.hero.manamax);
core.setStatusBarInnerHTML('mana', core.status.hero.mana + "/" + core.status.hero.manamax);
}
else {
core.setStatusBarInnerHTML("mana", core.status.hero.mana);
}
// 设置技能栏
// 可以用flag:skill表示当前开启的技能类型flag:skillName显示技能名
core.setStatusBarInnerHTML('skill', core.getFlag('skillName', '无'));
```
### 技能的触发
#### 使用道具作为技能
由于手机端按字母键不方便,虚拟键盘不好用,因此强烈推荐**给每个字母键技能设置一个道具,在道具栏点击使用!**
下面是个很简单的例子,要制作一个技能"二倍斩"。
我们可以设置一个道具其cls是`constants`永久道具ID比如是`skill1`。
该道具的使用判定`canUseItemEffect`是`true`(表示任意时候都可使用),使用效果`useItemEffect`是:
``` js
if (core.getFlag('skill', 0)==0) { // 判断当前是否已经开了技能
if (core.getStatus('mana')>=5) { // 这里要写当前能否开技能的条件判断,比如魔力值至少要多少
core.setFlag('skill', 1); // 开技能1
core.setFlag('skillName', '二倍斩'); // 设置技能名
}
else {
core.drawTip("魔力不足,无法开技能");
}
}
else { // 关闭技能
core.setFlag('skill', 0); // 关闭技能状态
core.setFlag('skillName', '无');
}
```
简单的说用flag:skill判断当前开启的技能flag:skillName表示该技能名。可在状态栏显示
该(技能)道具任何时候都可被使用;使用时,判断当前是否开启了技能,如果开启则关闭,没开则再判断是否允许开启(魔力值够不够等)。
V2.6.6起,道具提供了`useItemEvent`项,建议使用它而不是上面的脚本。'-
#### 快捷键触发技能
在PC端我们还可以按键触发技能。
在技能的道具定义完毕后,再将该道具绑定到一个快捷键上。有关绑定按键请参见[自定义快捷键](#自定义快捷键)。
下面是一个很简单的例子,当勇士按下 `F` 后,触发我们上面定义的二倍斩技能。
``` js
case 70: // F开启技能“二倍斩”
// 是否拥有“二倍斩”这个技能道具
if (core.hasItem('skill1')) {
core.status.route.push("key:70");
core.useItem('skill1', true);
}
break;
```
在勇士处于停止的条件下,按下 `F` 键时,判断技能的道具是否存在,如果存在再使用它。
!> 由于现在手机端存在拓展键盘也强烈建议直接覆盖1-7的使用效果这样手机端使用也非常方便。
### 技能的效果
最后一点就是技能的效果其实到了这里就和RM差不多了。
技能的效果要分的话有地图类技能,战斗效果类技能,后续影响类技能什么的,这里只介绍最简单的战斗效果类技能。
其他的几类技能根据需求可能更为麻烦,有兴趣可自行进行研究。
战斗效果内技能要改两个地方:战斗伤害计算,战后扣除魔力值。
战斗伤害计算在脚本编辑的`getDamageInfo`函数,有需求直接修改这个函数即可。
战后扣除魔力值则在脚本编辑的`afterBattle`中进行编辑即可。
举个例子我设置一个勇士的技能二倍斩开启技能消耗5点魔力下一场战斗攻击力翻倍。
那么,直接在脚本编辑的`getDamageInfo`中进行判断:
``` js
if (core.getFlag('skill', 0)==1) { // 开启了技能1
hero_atk *= 2; // 计算时攻击力翻倍
}
```
然后在脚本编辑的`afterBattle`中进行魔力值的扣除:
``` js
// 战后的技能处理,比如扣除魔力值
if (core.flags.statusBarItems.indexOf('enableSkill')>=0) {
// 检测当前开启的技能类型
var skill = core.getFlag('skill', 0);
if (skill==1) { // 技能1二倍斩
core.status.hero.mana-=5; // 扣除5点魔力值
}
// 关闭技能
core.setFlag('skill', 0);
core.setFlag('skillName', '无');
}
```
!> 开启技能后,建议将全塔属性的`useLoop`置为`true`,即改用循环计算临界值,这样临界计算才不会出问题!
&nbsp;
通过上述这几种方式我们就能成功的让H5支持技能啦
## 系统使用的flag变量
众所周知自定义flag变量都可以任意定义并取用未定义直接取用的flag默认值为0
下面是一些可能会被系统设置或取用的flag变量
- **`flag:hard`**: 当前的难度标志此flag变量在setInitData中被定义可以直接取用来判定当前难度分歧。上传成绩时将根据此flag来对不同难度进行排序。
- **`flag:posion`**, **`flag:weak`**, **`flag:curse`**: 中毒、衰弱、诅咒状态。
- **`flag:no_zone`**, **`flag:no_repulse`**, **`flag:no_laser`**, **`flag:no_betweenAttack`**: 是否分别免疫领域、阻击、激光、夹击效果。
- **`flag:hatred`**: 当前的仇恨数值。
- **`flag:commonTimes`**: 全局商店共用次数时的访问次数。
- **`flag:input`**: 接受用户输入的事件后,存放用户输入的结果。
- **`flag:type`**, **`flag:keycode`**, **`flag:x`**, **`flag:y`**, **`flag:px`**, **`flag:py`**: 等待用户操作后用户的操作类型按键keycode或点击/像素坐标。
- **`flag:skill`**, **`flag:skillName`**: 开启的技能编号和技能名。
- **`flag:saveEquips`**: 快速换装时保存的套装。
- **`flag:__visited__`**: 当前访问过的楼层。
- **`flag:__atk_buff__`**, **`flag:__def_buff__`**, **`flag:__mdef_buff__`**: 当前攻防护盾的实际计算比例加成。
- **`flag:__color__`**, **`flag:__weather__`**, **`flag:__volume__`**: 当前的画面色调、天气和音量。
- **`flag:__events__`**: 当前保存的事件列表,读档时会恢复(适用于在事件中存档)
- **`flag:textAttribute`**, **`flag:globalAttribute`**, **`flag:globalFlags`**: 当前的剧情文本属性,当前的全局属性,当前的全局开关。
- **`flag:cannotMoveDirectly`**, **`flag:__noClickMove__`**: 当前是否不允许瞬间移动,当前用户是否开启了单击瞬移。
- **`flag:hideStatusBar`**, **`flag:showToolbox`**: 是否隐藏状态栏,是否显示工具栏。
- **`flag:debug`**: 当前是否开启了调试模式。
- **`flag:__seed__`**, **`flag:__rand__`**: 伪随机数生成种子和当前的状态
==========================================================================================
[继续阅读脚本](script)

32
plugin.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
// 这里包含所有插件导出的函数及变量声明声明的函数会在类型标注中标注到core上
type Ref<T> = {
value: T;
};
type DragFn = (x: number, y: number, e: MouseEvent | TouchEvent) => void;
type CanParseCss = keyof {
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
? P extends string
? P
: never
: never]: CSSStyleDeclaration[P];
};
interface PluginDeclaration {}
type Forward<T> = {
[K in keyof T as T[K] extends Function
? K extends `_${string}`
? never
: K
: never]: T[K];
};
type ForwardKeys<T> = keyof Forward<T>;
declare const Mota: import('../index.d.ts').IMota;
interface Window {
Mota: import('../index.d.ts').IMota;
}

14397
plugins.js Normal file

File diff suppressed because one or more lines are too long

739
script.md Normal file
View File

@ -0,0 +1,739 @@
# 脚本
?> 在这一节中,让我们来了解如何使用控制台和使用脚本!
在V2.6版本中,基本对整个项目代码进行了重写,更加方便造塔者的使用。
* [脚本教程视频](https://www.bilibili.com/video/BV1uL411J7yZ?share_source=copy_web)
## 控制台的使用
在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, onup, keyDown 等等。
name: 你的自定义名称,可被注销使用;同名重复注册将后者覆盖前者。
func: 执行函数。
如果func返回true则不会再继续执行其他的交互函数否则会继续执行其他的交互函数。
priority: 优先级优先级高的将会被执行。此项可不填默认为0
```
`registerAction`为register系列最常用的功能之一它允许你任意监听用户的交互事件包括且不仅限于按键、点击、拖动、长按等等。
下面是一个例子:
```js
// 当flag:abc是true时点击屏幕左上角可以使用道具破墙镐
// 注入一个 ondown 事件,名称为 my_pickaxe
core.registerAction('ondown', '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('ondown', '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` 【已废弃】
- 从V2.8.2起,此交互已被废弃,注册一个`onclick`会被直接转发至`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为当前点坐标可为nullprefix为当前点前缀
```
`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)

1
search.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){"use strict";function e(e){var n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;"};return String(e).replace(/[&<>"'\/]/g,function(e){return n[e]})}function n(e){var n=[];return h.dom.findAll("a:not([data-nosearch])").map(function(t){var o=t.href,i=t.getAttribute("href"),r=e.parse(o).path;r&&-1===n.indexOf(r)&&!Docsify.util.isAbsolutePath(i)&&n.push(r)}),n}function t(e){localStorage.setItem("docsify.search.expires",Date.now()+e),localStorage.setItem("docsify.search.index",JSON.stringify(g))}function o(e,n,t,o){void 0===n&&(n="");var i,r=window.marked.lexer(n),a=window.Docsify.slugify,s={};return r.forEach(function(n){if("heading"===n.type&&n.depth<=o)i=t.toURL(e,{id:a(n.text)}),s[i]={slug:i,title:n.text,body:""};else{if(!i)return;s[i]?s[i].body?s[i].body+="\n"+(n.text||""):s[i].body=n.text:s[i]={slug:i,title:"",body:""}}}),a.clear(),s}function i(n){var t=[],o=[];Object.keys(g).forEach(function(e){o=o.concat(Object.keys(g[e]).map(function(n){return g[e][n]}))}),n=n.trim();var i=n.split(/[\s\-\\\\/]+/);1!==i.length&&(i=[].concat(n,i));for(var r=0;r<o.length;r++)!function(n){var r=o[n],a=!1,s="",c=r.title&&r.title.trim(),l=r.body&&r.body.trim(),f=r.slug||"";if(c&&l&&(i.forEach(function(n,t){var o=new RegExp(n,"gi"),i=-1,r=-1;if(i=c&&c.search(o),r=l&&l.search(o),i<0&&r<0)a=!1;else{a=!0,r<0&&(r=0);var f=0,d=0;f=r<11?0:r-10,d=0===f?70:r+n.length+60,d>l.length&&(d=l.length);var p="..."+e(l).substring(f,d).replace(o,'<em class="search-keyword">'+n+"</em>")+"...";s+=p}}),a)){var d={title:e(c),content:s,url:f};t.push(d)}}(r);return t}function r(e,i){h=Docsify;var r="auto"===e.paths,a=localStorage.getItem("docsify.search.expires")<Date.now();if(g=JSON.parse(localStorage.getItem("docsify.search.index")),a)g={};else if(!r)return;var s=r?n(i.router):e.paths,c=s.length,l=0;s.forEach(function(n){if(g[n])return l++;h.get(i.router.getFile(n)).then(function(r){g[n]=o(n,r,i.router,e.depth),c===++l&&t(e.maxAge)})})}function a(){Docsify.dom.style("\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 7px;\n line-height: 22px;\n font-size: 14px;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}")}function s(e,n){void 0===n&&(n="");var t='<input type="search" value="'+n+'" /><div class="results-panel"></div></div>',o=Docsify.dom.create("div",t),i=Docsify.dom.find("aside");Docsify.dom.toggleClass(o,"search"),Docsify.dom.before(i,o)}function c(e){var n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,".results-panel");if(!e)return t.classList.remove("show"),void(t.innerHTML="");var o=i(e),r="";o.forEach(function(e){r+='<div class="matching-post">\n<a href="'+e.url+'"> \n<h2>'+e.title+"</h2>\n<p>"+e.content+"</p>\n</a>\n</div>"}),t.classList.add("show"),t.innerHTML=r||'<p class="empty">'+y+"</p>"}function l(){var e,n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,"input");Docsify.dom.on(n,"click",function(e){return"A"!==e.target.tagName&&e.stopPropagation()}),Docsify.dom.on(t,"input",function(n){clearTimeout(e),e=setTimeout(function(e){return c(n.target.value.trim())},100)})}function f(e,n){var t=Docsify.dom.getNode('.search input[type="search"]');if(t)if("string"==typeof e)t.placeholder=e;else{var o=Object.keys(e).filter(function(e){return n.indexOf(e)>-1})[0];t.placeholder=e[o]}}function d(e,n){if("string"==typeof e)y=e;else{var t=Object.keys(e).filter(function(e){return n.indexOf(e)>-1})[0];y=e[t]}}function p(e,n){var t=n.router.parse().query.s;a(),s(e,t),l(),t&&setTimeout(function(e){return c(t)},500)}function u(e,n){f(e.placeholder,n.route.path),d(e.noData,n.route.path)}var h,g={},y="",m={placeholder:"Type to search",noData:"No Results!",paths:"auto",depth:2,maxAge:864e5},v=function(e,n){var t=Docsify.util,o=n.config.search||m;Array.isArray(o)?m.paths=o:"object"==typeof o&&(m.paths=Array.isArray(o.paths)?o.paths:"auto",m.maxAge=t.isPrimitive(o.maxAge)?o.maxAge:m.maxAge,m.placeholder=o.placeholder||m.placeholder,m.noData=o.noData||m.noData,m.depth=o.depth||m.depth);var i="auto"===m.paths;e.mounted(function(e){p(m,n),!i&&r(m,n)}),e.doneEach(function(e){u(m,n),i&&r(m,n)})};$docsify.plugins=[].concat(v,$docsify.plugins)}();

99
source.d.ts vendored Normal file
View File

@ -0,0 +1,99 @@
/**
*
*/
type Cls =
| 'autotile'
| 'animates'
| 'enemys'
| 'items'
| 'npcs'
| 'terrains'
| 'enemy48'
| 'npc48'
| 'tileset';
/**
*
*/
type AnimatableCls = Exclude<Cls, 'items' | 'terrains' | 'tileset'>;
/**
*
*/
type ItemCls = 'tools' | 'items' | 'equips' | 'constants';
/**
* id
*/
type AllIds = keyof IdToNumber;
/**
*
*/
type AllNumbers = keyof NumberToId | 0;
/**
* id
*/
type AllIdsOf<T extends Cls> = keyof {
[P in keyof IdToCls as IdToCls[P] extends T ? P : never]: P;
};
/**
* id
*/
type ItemIdOf<T extends ItemCls> = keyof {
[P in keyof ItemDeclaration as ItemDeclaration[P] extends T ? P : never]: P;
};
/**
*
*/
type ItemClsOf<T extends AllIdsOf<'items'>> = ItemDeclaration[T];
/**
*
*/
type ClsOf<T extends AllIds> = IdToCls[T];
/**
*
*/
type AllNumbersOf<T extends Cls> = IdToNumber[AllIdsOf<T>];
/**
*
*/
type NameMapIn<T extends string> = keyof {
[P in keyof NameMap as NameMap[P] extends T ? P : never]: NameMap[P];
};
/**
* id
*/
type EnemyIds = AllIdsOf<'enemys' | 'enemy48'>;
/**
*
*/
interface FrameNumbers {
autotile: 4;
animates: 4;
enemys: 2;
items: 1;
npcs: 2;
terrains: 1;
enemy48: 4;
npc48: 4;
tileset: 1;
}
/**
*
*/
type FrameOf<T extends Cls> = FrameNumbers[T];
/**
*
*/
type SourceIds = ImageIds | AnimationIds | SoundIds | BgmIds | FontIds;

129
start.md Normal file
View File

@ -0,0 +1,129 @@
# 快速上手
?> 在这一节中,将详细介绍做一部塔的流程。现在,让我们来做一部单层塔!
* [新版视频教程](https://www.bilibili.com/video/BV1SB4y1p7bg?share_source=copy_web)
* [脚本教程](https://www.bilibili.com/video/BV1uL411J7yZ?share_source=copy_web)
## 前置需求
你需要满足如下条件才能进行制作:[样板下载地址](https://github.com/ckcz123/mota-js/releases)。
1. 操作系统:
- Win8或更高版本Win7则需要安装 .Net Framework
4.0(能打开根目录的“启动服务.exe”即可
- 其他电脑则需安装[python
3.9.6](https://www.python.org/downloads/)或更高版本能运行根目录的server.py即可Windows也可以这样做
- 安卓手机需要安装[HTML5安卓造塔器](http://h5mota.com/games/_client/H5motaMaker.apk)推荐搭配ES文件浏览器。
2. [谷歌chrome浏览器](http://google.cn/chrome)或win10自带的新版Edge浏览器其他浏览器可能导致本地服务闪退。
3. (强烈推荐)[VScode](http://code.visualstudio.com/download)最适合HTML5项目的文本编辑器能进行跨文件的正则搜索和替换也能完整发挥根目录的runtime.d.ts文件的作用。
只要满足了上述条件,你就可以开始做自己的塔啦!
## 启动HTTP服务
与编辑器闭源的RPG Maker MV/MZ不同本样板对文件的绝大部分修改是通过网页编辑器经由本地HTTP服务完成的这样做也有助于编辑器跨平台并最大限度地复用运行时代码还可以让玩家在在线游戏时查看游戏工程。
在根目录下有一个“启动服务.exe”运行之。非Windows电脑则需使用命令行运行`server.py`,安卓手机则使用造塔器)
!> 使用python启动服务的话整个塔的绝对路径必须是全英文
![image](img/server.jpg)
* 启动游戏:打开[http://127.0.0.1:1055/index.html](http://127.0.0.1:1055/index.html)同时开启多个启动服务则1056、1057顺延下同。你能在里面看到现在游戏的效果。
* 启动编辑器:打开[http://127.0.0.1:1055/editor.html](http://127.0.0.1:1055/editor.html)竖屏则为editor-mobile.html这是您整个制作流程的核心页面
以下为Windows专用的一些辅助工具位于“辅助工具”文件夹由C#编写:
* 便捷PS工具能方便地替换和新增32×32、32×48素材。V2.8.1对该工具进行了大幅强化您甚至可以指定32×16的格子尺寸作为前述两种尺寸的过渡操作。
* 地图生成器识别RPG Maker魔塔的地图截图生成HTML5魔塔的地图数据。
* 怪物数据导出从RPG Maker XP 1.03游戏导出怪物数据用于HTML5魔塔或使用Excel查看。
* RM动画导出从RPG Maker XP 1.03游戏导出动画用于HTML5魔塔。
* JS代码压缩对js代码限es5和音像素材背景音乐除外进行压缩从而减小文件体积加快在线游戏的加载。一般无需手动运行发布后我们的服务器会处理的。
* 动画编辑器:编辑`project\animates`文件夹中的动画文件,或利用图片制作全新的动画。
* 伤害和临界值计算器、帮助文档:如题,后者会打开本文档。
!> **整个造塔过程中,启动服务必须全程处于开启状态!切不可手滑关闭,否则做的都是无用功!**
![image](img/V2_8server.jpg)
V2.7.x中“地图生成器”因为图片文件被拆分到两个文件夹的缘故而无法再使用该问题已在V2.8中修复,同时该工具更名为“截图识别器”。
V2.8中“额外素材合并”不再在本地进行而是改在作品发布时在服务器端用python实现并把实现原理改为“将未用到的图块透明化后全图裁剪到可能的最小尺寸”以避免图块id错乱的问题。
## 编辑器页面的结构
![image](img/editor.jpg)
如上图,编辑器页面的结构分为三大部分。左边叫**数据区**,中央叫**地图区**,右侧叫**素材区**,竖屏状态下同时只能显示其中一个,需要经常来回切换。
请尤其注意中央下方的下拉框您可以随时按下Z、X、…、句号键字母键第三行让数据区在这些模式之间切换。更多键鼠快捷操作请按下H键查看这里列出一部分
1. **Alt+0~9、0~9**给素材区的图块绑定数字快捷键,并使用。(您也可以用中央下方的“最近/最常使用图块”和置顶来代替)
2. **WASD、或单击/长按四个箭头按钮:**滚动大地图,还可以单击“大地图”按钮观看全景。
3. **Ctrl+W/A/S/Z/X/C/V/Y**关闭、全选、保存、撤销、剪切、复制、粘贴、重做绘图等操作。
4. **PageUp/PageDown或滚轮**切换楼层。
5. **ESC、Delete**取消选中并保存、删除选中点的图块和事件。(需要保存时“保存地图”按钮也会变色提示)
6. **地图上单击、双击、右击:**地图选点、选中该点的素材并自动定位到素材区、弹出菜单(您可以进行出生点、机关门、上下楼的快速绑定等操作)
7. **地图上左键或右键拖动:**交换两个点的图块和事件、框选一些点供Ctrl+X/C/V剪切复制。
8. **素材区最右侧的tileset区域左键拖动**框选一批素材,供在地图区单击批量绘制或左键拖动平铺。(右击或触屏点击则是手动输入所需的宽高)
9. **事件编辑器中Ctrl+S/Z/X/C/V/Y、右击、双击等**执行相应操作,如双击可以进入多行编辑/绘制预览/弹窗选文件/地图选点,地图选点时右击可以选多个点。
## 快速上手
针对红海塔作者,这里给出一个极简版的造塔流程,您可以据此造出一座没有任何事件(包括但不限于难度分歧、老人、木牌、商人和商店等)的塔:
1. 编辑勇士的出生点和初始属性:
1. 滚轮切换到主塔0层右击地图任意位置绑定出生点为此点会有一个大大的白色S字母显示出来
2. 按下B键切换到“全塔属性”填写`core.firstData.hero`中勇士的各项初始属性以及一些全局的数值如四种血瓶和三种宝石的效果、破甲反击净化的比例等注意“唯一英文标识符”name一定要修改
3. 在数据区使用滚轮向下翻(您可以随时折叠全塔属性的几大部分),按需编辑下面的大量勾选框(主要就是状态栏的那些显示项)。
2. 从素材区选择各种各样的非NPC图块绘制在地图上如门、怪物、道具、楼梯、路障、箭头、踩灯、箱子等。每当选中一个素材时数据区就进入了“图块属性”模式您可以去填写道具的一些说明、以及修改其他一些图块的可通行性等。注意滑冰触发器为ski要画在背景层。如果您需要制作机关门请简单地将机关门和守卫不支持阻击怪和炸弹画在地图上再右击机关门快速绑定即可。看到机关门左下角出现橙色小方块、守卫们左下角出现黄色小方块即说明绑定成功
3. 如果您需要制作多个楼层只需按下Z键将数据区切换到“地图编辑”模式然后“新建空白地图”即可不同楼层之间简单地通过楼梯来往返您可以将楼梯画在地图上再右击快速绑定即可。看到楼梯左下角出现绿色小方块即说明绑定成功各个楼层的属性可以通过按下V键将数据区切换到“楼层属性”模式来填写如能否使用楼传、是否为地下层、画面色调、宝石血瓶倍率等。
4. 从素材区选择您所使用的各种怪物,在数据区填写它们的各项属性,其中“特殊属性”是通过多选框来编辑的。
5. 游戏胜利的触发滚轮切换到样板1层单击地图上的公主按下Ctrl+C复制。滚轮切换回您的boss层记得给boss设置不可被炸哦单击boss身后的任何一个空格子按下Ctrl+V粘贴即可。这样玩家触碰到公主游戏就会胜利。
## 控制台调试
![image](img/console2.jpg)
HTML5项目都是可以进行控制台调试的下面以edge浏览器为例介绍其部分操作
首先按下F12键部分键盘没有此键或需与Fn键一起按或Ctrl+Shift+I打开开发人员界面。
可以看到它分为“元素”、“控制台”、“调试程序”、“性能”等多个部分:
1. **元素:**您可以在这里对游戏和编辑器的各HTML和css元素进行查看和临时的修改譬如您想观察游戏在竖屏的表现只需将窗口拉到瘦高。
2. **性能:**您可以在这里对游戏的任何一段脚本进行性能分析,观察其中各行的执行频率和耗时,从而确定优化的方向。
3. **调试程序:**您可以在这里查看游戏的源码包括project文件夹的functions.js和plugin.js脚本编辑和插件编写以及整个libs文件夹并进行断点调试。
4. **控制台:**最常使用的部分当编辑器或游戏打不开、卡死、或者不按您的预想运作时您就需要查看这里的报错信息。这里也是各种API输入的地方譬如上图中您可以看到全部的插件函数。
在控制台中,我们可以输入一些命令对游戏进行调试,常见的命令有:
- `core.status.floorId` 获得当前层的floorId。
- `core.status.thisMap` 获得当前地图信息。例如`core.status.thisMap.blocks`可以获得当前层所有图块。
- `core.floors` 获得所有楼层的初始信息。例如`core.floors[core.status.floorId].events`可以获得当前层所有事件。
- `core.status.hero` 或简写为hero获得当前勇士状态信息。例如`core.status.hero.atk`就是当前勇士的攻击力数值。
- `core.material.enemys` 获得所有怪物信息。例如`core.material.enemys.greenSlime`就是获得绿色史莱姆的属性数据。
- `core.material.items` 获得所有道具的信息。例如`core.material.items.pickaxe`就是获得破墙镐的信息。
- `core.debug()` 开启调试模式此模式下可以按住Ctrl键进行穿墙。
- `core.updateStatusBar()` 立刻更新状态栏、地图显伤并触发自动事件。
- `core.setStatus('atk', 1000)` 直接设置勇士的某项属性。本句等价于 `core.status.hero.atk = 1000`
- `core.getStatus('atk')` 返回勇士当前某项属性数值。本句等价于 `core.status.hero.atk`
- `core.setItem('pickaxe', 10)` 直接设置勇士某个道具的个数。这里可以需要写道具的ID。
- `core.getItem('pickaxe', 2)` 令勇士获得两个破墙镐。
- `core.itemCount('pickaxe')` 返回勇士某个道具的个数。
- `core.hasItem('pickaxe')` 返回勇士是否拥有某个道具。等价于`core.itemCount('pickaxe')>0`。
- `core.getEquip(0)` 返回0号装备类型武器的当前装备的itemId不存在则返回`null`
- `core.hasEquip('sword1')` 返回某个装备当前是否处于被装备状态
- `core.setFlag('xxx', 1)` 设置某个flag/自定义变量的值,可以设为`null`表示删除。
- `core.getFlag('xxx', 10)` 获得某个flag/自定义变量的值;如果该项不存在(未被定义),则返回第二个参数的值。
- `core.hasFlag('xxx')` 返回是否存在某个变量且不为0。等价于`!!core.getFlag('xxx', 0)`。
- `core.removeFlag('xxx')` 删除某个flag/自定义变量
- `core.insertAction(list)` 执行一段自定义事件。比如 `core.insertAction(["剧情文本"])` 将执行一个剧情文本显示事件。
- `core.changeFloor('MT2', 'downFloor')` 立刻执行楼层切换到MT2层的下楼点位置。
- `core.changeFloor('MT5', null, {'x': 4, 'y': 7})` 立刻切换楼层到MT5层的(4,7)点。
- `core.getBlock(3, 5, 'MT1')` 获得当前地图上某一个块的信息。第三个参数为floorId可省略表示当前楼层。
- `core.getBlockId(3, 5, 'MT1')` 获得当前地图上某一个点的图块ID。第三个参数为floorId可省略表示当前楼层。
- `core.resetMap()` 重置当前层地图。
- ……
更多关于控制台调试和脚本的信息可参见[脚本](script)和[API列表](api)。
==========================================================================================
[继续阅读下一章:元件说明](element)

974
status.d.ts vendored Normal file
View File

@ -0,0 +1,974 @@
/**
* buff缓存
*/
interface EnemyBuffCache {
/**
*
*/
hp_buff: number;
/**
*
*/
atk_buff: number;
/**
*
*/
def_buff: number;
/**
*
*/
guards: [number, number, string][];
}
interface CheckBlockStatus {
/**
*
*/
ambush: Record<LocString, [number, number, string, Dir]>;
/**
*
*/
repulse: Record<LocString, [number, number, string, Dir]>;
/**
* 0
*/
damage: Record<LocString, number>;
/**
*
*/
needCache: boolean;
/**
*
*/
type: Record<LocString, Record<string, number>>;
/**
*
*/
cache: Record<string, DeepReadonly<EnemyBuffCache>>;
/**
*
*/
halo: Record<LocString, string[]>;
}
interface DamageStatus {
/**
* v2优化下当前的偏移横坐标
*/
posX: number;
/**
* v2优化下当前的偏移纵坐标
*/
posY: number;
/**
*
*/
data: DamageStatusData[];
/**
*
*/
extraData: DamageStatusExtraData[];
/**
*
*/
dir: DamageDirData[];
}
interface DamageStatusData {
/**
*
*/
text: string;
/**
*
*/
px: number;
/**
*
*/
py: number;
/**
*
*/
color: Color;
}
interface DamageStatusExtraData extends DamageStatusData {
/**
*
*/
alpha: number;
}
interface DamageDirData {
x: number;
y: number;
dir: Dir;
color: Color;
}
interface AutomaticRouteStatus {
/**
*
*/
autoHeroMove: boolean;
/**
*
*/
autoStep: number;
/**
*
*/
movedStep: number;
/**
*
*/
destStep: number;
/**
*
*/
destX: number;
/**
*
*/
destY: number;
/**
*
*/
offsetX: number;
/**
*
*/
offsetY: number;
/**
* 线
*/
autoStepRoutes: AutoStep[];
/**
* 线
*/
moveStepBeforeStop: AutoStep[];
/**
*
*/
lastDirection: Dir;
/**
* (E时的界面)
*/
cursorX: number;
/**
* (E时的界面)
*/
cursorY: number;
/**
*
*/
moveDirectly: boolean;
}
interface AutoStep {
/**
*
*/
step: number;
/**
*
*/
direction: Dir;
}
interface ReplaySaveBase {
/**
*
*/
toReplay: string[];
/**
*
*/
totalList: string[];
/**
* 退
*/
steps: number;
}
interface ReplayStatus extends ReplaySaveBase {
/**
* core.isReplaying()
*/
replaying: boolean;
/**
*
*/
pausing: boolean;
/**
*
*/
animate: boolean;
/**
*
*/
failed: boolean;
/**
*
*/
speed: number;
/**
* 退
*/
save: ReplaySave[];
}
interface ReplaySave {
/**
* 退
*/
data: Save;
/**
* 退
*/
replay: ReplaySaveBase;
}
interface TextAttribute {
/**
*
*/
position: TextPosition;
/**
*
*/
align: 'left' | 'center' | 'right';
/**
*
*/
offset: number;
/**
*
*/
title: RGBArray;
/**
*
*/
background: RGBArray | ImageIds;
/**
*
*/
text: RGBArray;
/**
*
*/
titlefont: number;
/**
*
*/
textfont: number;
/**
*
*/
bold: boolean;
/**
*
*/
time: number;
/**
*
*/
letterSpacing: number;
/**
*
*/
animateTime: number;
/**
*
*/
lineHeight: number;
}
interface StatusStyle {
/**
*
*/
borderColor: Color;
/**
*
*/
statusBarColor: Color;
/**
* css字符串
*/
floorChangingStyle: string;
/**
*
*/
font: string;
}
interface GlobalAttribute extends StatusStyle {
/**
*
*/
equipName: string[];
}
interface FloorAnimate {
/**
*
*/
canvas: 'bg' | 'fg';
/**
*
*/
name: ImageIds;
/**
*
*/
x: number;
/**
*
*/
y: number;
/**
*
*/
sx?: number;
/**
*
*/
sy?: number;
/**
*
*/
w?: number;
/**
*
*/
h?: number;
/**
*
*/
frame?: number;
/**
*
*/
reverse?: ':x' | ':y' | ':o';
/**
*
*/
disable?: boolean;
}
interface BoxAnimate {
/**
*
*/
animate: number;
/**
*
*/
bgHeight: number;
/**
*
*/
bgWidth: number;
/**
*
*/
bgx: number;
/**
*
*/
bgy: number;
/**
*
*/
height: 32 | 48;
/**
*
*/
img: HTMLImageElement;
/**
*
*/
pos: number;
/**
*
*/
x: number;
/**
*
*/
y: number;
}
interface BigImageBoxAnimate {
/**
*
*/
bigImage: HTMLImageElement;
/**
*
*/
face: Dir;
/**
*
*/
centerX: number;
/**
*
*/
centerY: number;
/**
*
*/
max_width: number;
/**
*
*/
ctx: CtxRefer;
}
interface AnimateObj {
/**
*
*/
name: AnimationIds;
/**
*
*/
id: number;
/**
*
*/
animate: Animate;
/**
*
*/
centerX: number;
/**
*
*/
centerY: number;
/**
*
*/
index: number;
/**
*
*/
callback: () => void;
}
interface ActionsPreview {
/**
*
*/
dragging: boolean;
/**
*
*/
enabled: boolean;
/**
*
*/
prepareDragging: boolean;
/**
*
*/
px: number;
/**
*
*/
py: number;
}
interface RouteFolding {
/**
*
*/
hero: Omit<SelectType<HeroStatus, number>, 'steps'>;
/**
*
*/
length: number;
}
/**
*
*/
interface InitGameStatus {
/**
*
*/
played: boolean;
/**
*
*/
gameOver: boolean;
/**
*
*/
maps: {
[P in FloorIds]: Floor<P>;
};
/**
*
*/
bgmaps: Record<FloorIds, number[][]>;
/**
*
*/
fgmaps: Record<FloorIds, number[][]>;
/**
*
*/
mapBlockObjs: Record<FloorIds, Record<LocString, Block>>;
/**
*
*/
damage: DamageStatus;
/**
*
*/
lockControl: boolean;
/**
* libs翻西西
*/
heroMoving: number;
/**
*
*/
heroStop: boolean;
/**
*
*/
automaticRoute: DeepReadonly<AutomaticRouteStatus>;
/**
*
*/
downTime: number;
/**
* ctrl键是否倍按下
*/
ctrlDown: boolean;
/**
*
*/
route: string[];
/**
*
*/
replay: DeepReadonly<ReplayStatus>;
/**
*
*/
shops: Record<string, ShopEventOf<keyof ShopEventMap>>;
/**
*
*/
event: EventStatusOf;
/**
*
*/
autoEvents: DeepReadonly<AutoEvent[]>;
/**
*
*/
textAttribute: TextAttribute;
/**
*
*/
globalAttribute: GlobalAttribute;
/**
*
*/
curtainColor: Color;
/**
*
*/
globalAnimateObjs: Block<
IdToNumber[AllIdsOf<Exclude<AnimatableCls, 'autotile'>>]
>[];
/**
*
*/
floorAnimateObjs: FloorAnimate[];
/**
* BoxAnimate信息
*/
boxAnimateObjs: (BoxAnimate | BigImageBoxAnimate)[];
/**
*
*/
autotileAnimateObjs: Block<IdToNumber[AllIdsOf<'autotile'>]>[];
/**
* 便
*/
globalAnimateStatus: number;
/**
*
*/
animateObjs: AnimateObj[];
/**
*
*/
hard: string;
/**
*
*/
heroCenter: Record<'px' | 'py', number>;
/**
*
*/
holdingKeys: number[];
/**
* id转数字
*/
id2number: IdToNumber;
/**
*
*/
number2block: {
[P in keyof NumberToId]: Block<P>;
};
/**
*
*/
preview: ActionsPreview;
/**
*
*/
routeFolding: Record<`${LocString},${FirstCharOf<Dir>}`, RouteFolding>;
}
/**
*
*/
interface GameStatus extends InitGameStatus {
/**
* floorId
*/
floorId: FloorIds;
/**
* core.status.maps[core.status.floorId]
*/
thisMap: Floor;
/**
*
*/
checkBlock: Readonly<CheckBlockStatus>;
/**
* core.status.hero.atk就是当前勇士的攻击力数值
*/
hero: HeroStatus;
}
interface Follower {
/**
* id
*/
name: ImageIds;
}
interface HeroStatistics {
/**
*
*/
battle: number;
/**
*
*/
battleDamage: number;
/**
*
*/
currentTime: number;
/**
*
*/
exp: number;
/**
*
*/
extraDamage: number;
/**
*
*/
hp: number;
/**
*
*/
ignoreSteps: number;
/**
*
*/
money: number;
/**
*
*/
moveDirectly: number;
/**
*
*/
poisonDamage: number;
/**
*
*/
startTime: number;
/**
*
*/
totalTime: number;
}
/**
*
*/
interface HeroStatus {
/**
*
*/
animate: boolean;
/**
*
*/
hp: number;
/**
*
*/
hpmax: number;
/**
*
*/
atk: number;
/**
*
*/
def: number;
/**
*
*/
mdef: number;
/**
*
*/
lv: number;
/**
*
*/
exp: number;
/**
*
*/
money: number;
/**
*
*/
mana: number;
/**
*
*/
manamax: number;
/**
*
*/
name: string;
/**
*
*/
steps: number;
/**
*
*/
image: ImageIds;
/**
*
*/
equipment: ItemIdOf<'equips'>[];
/**
*
*/
loc: DiredLoc;
/**
*
*/
flags: Flags;
/**
*
*/
followers: Follower[];
statistics: HeroStatistics;
/**
*
*/
items: {
[P in Exclude<ItemCls, 'items'>]: Record<ItemIdOf<P>, number>;
};
/**
*
*/
special: {
num: number[];
last: number[];
[k: string]: any;
};
x?: number;
y?: number;
floorId?: FloorIds;
}

864
ui-editor.md Normal file
View File

@ -0,0 +1,864 @@
# UI编辑器
## 基础操作
打开编辑界面后即可编辑通过添加新操作可以向ui中添加新的内容。点击一个操作后可以选中该操作被选中的操作将呈现黄色可按住ctrl多选。此时点击鼠标右键可以打开右键菜单包括在上方插入、复制等操作。执行在上方或下方插入时会在鼠标点击的操作上方或下方插入执行复制或剪切时会复制或剪切所有被选中的操作被剪切的操作将会呈现灰色。当ui编辑到一半时可以点击保存按钮进行保存当ui未保存时保存按钮应当显示为黄色点击保存后如果保存成功按钮会短暂变成绿色。当ui编辑完成后可以点击预览按钮从头开始预览ui确认符合预期后可以点击编译按钮将操作编译为js代码然后将该代码按照编译完成的注释操作将代码放入插件中在需要的地方插入即可。
## 注意事项
- 设置过渡不会立刻生效会在下一个动画帧中生效因此设置之后可能需要大概20~50ms的等待操作
- 画布无法更改名称
- 对于绘制成段文本,如果设置了打字机速度,那么两个该操作不能同时进行
- 临时禁用只对预览有效,编译时被临时禁用的操作仍会被编译
- 遇到问题或bug、建议等请在造塔群及时反馈
## 编译
该操作会将当前编辑器显示的所有操作编译为js代码。编译完成后应当放到插件中一个形式为`this.xxx = function () { ui内容 }`的函数中在需要显示该ui时只需执行代码`core.xxx()`
示例:
```js
// 插件中
this.myUi = function() {
// 请复制以下内容至插件中以使用
// 使用案例:
/*
插件中:
this.showMyUi = function () {
函数内容
}
调用时:
core.showMyUi();
*/
var _sprite_0 = new Sprite(100, 100, 100, 100, 200, 'game', '_sprite_0');
_sprite_0.setCss('display: none;');
action_0_6();
function action_0_6() {
_sprite_0.setCss('display: block;');
_sprite_0.move(100, 100);
_sprite_0.resize(100, 100);
_sprite_0.canvas.style.zIndex = '200';
core.setAlpha(_sprite_0.context, 0.5);
core.fillEllipse(_sprite_0.context, 50, 50, 30, 50, 0, '');
var ctx = _sprite_0.context;
ctx.moveTo(0, 20);
ctx.bezierCurveTo(10, 10, 50, 50, 50, 100);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 5;
ctx.stoke();
_sprite_0.context.globalCompositeOperation = 'difference';
core.fillRect(_sprite_0.context, 0, 0, 100, 100, '');
setTimeout(action_7_8, 1000);
}
function action_7_8() {
var res = _sprite_0.canvas.style.transition;
if (res !== '') res += ', ';
res += 'left 300ms ease-out 0ms';
_sprite_0.canvas.style.transition = res;
setTimeout(action_9_9, 50);
}
function action_9_9() {
_sprite_0.resize(100, 100, true);
}
}
// 需要显示ui时
core.myUi();
```
## 预览
预览分为两种,一种是编辑时的即时预览,这种预览会显示所有的画布,并无视等待和删除,确保你能够看到所有的绘制内容,如果你想要隐藏某个操作,可以打开该操作的详细信息,然后将临时禁用勾上,这样就能隐藏该操作了。
第二种是点击预览按钮时发生的这种预览会尽可能地还原编译后的执行结果让你能够完整地预览ui。
## 保存
显而易见该操作可以保存现在正在编辑的ui。注意ui会保存在样板根目录下的_ui文件夹以base64的形式进行保存。当你编辑了ui时保存按钮会呈现黄色说明此时你还没有保存此时切出编辑页面会弹出提示。点击保存后会出现短暂的保存中字样保存快的话可能看不清如果保存成功会出现成功字样并会将背景短暂地改成绿色
## 临时禁用
该操作可以让你能够禁用某个操作,让你在不删除操作的情况下看到删除之后的效果。该操作只对预览有效,这意味着编译仍会编译被禁用的操作。
## 编辑
### 创建画布
| 事件名称 | 创建画布 |
| --- | --- |
| 事件原形 | 创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深) |
| 功能描述 | 创建一个新的画布 |
| 输入参数 1 | 名称:画布的名称 |
| 输入参数 2 | 横坐标:画布的起点横坐标(左上角) |
| 输入参数 3 | 纵坐标:画布的起点纵坐标(左上角) |
| 输入参数 4 | 宽度:画布的宽度 |
| 输入参数 5 | 高度:画布的高度 |
| 输入参数 6 | 纵深:画布的显示优先级,数值越大优先级越高 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 创建画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
……
```
### 等待
| 事件名称 | 等待 |
| --- | --- |
| 事件原形 | 等待(毫秒数) |
| 功能描述 | 等待一定时间后再执行后续操作 |
| 输入参数 1 | 毫秒数:等待的时间长度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 等待 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
等待(毫秒数);
```
### 设置过渡
| 事件名称 | 设置过渡 |
| --- | --- |
| 事件原形 | 设置过渡(作用画布, 作用效果, 过渡时间, 过渡方式, 延迟) |
| 功能描述 | 指定画布前两个命令之间的渐变式变化 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 作用效果可用于transition的style |
| 输入参数 3 | 过渡时间:变化的时间长度 |
| 输入参数 4 | 过渡方式过渡方式包括ease-out ease-in ease-in-out linear bezier-curve()等 |
| 输入参数 5 | 延迟:当指定属性变化后,多长时间后开始过渡 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
| 常见作用效果 | 说明 |
| --- | --- |
| left | x轴方向的位置 |
| top | y轴方向的位置 |
| opacity | 不透明度 |
| background-color | 背景色 |
| transform | 2D和3D变换 |
| filter | 滤镜 |
| border | 边框 |
| 过渡方式 | 说明 |
| --- | --- |
| ease-out | 先快后慢 |
| ease-in | 先慢后快 |
| ease-in-out | 慢-快-慢 |
| linear | 线性变化(匀速) |
| bezier-curve() | 指定按照贝塞尔曲线的形式变化1表示到达终点0表示起点 |
例:
```js
/* 这是UI编辑器中 设置过渡 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置过渡(作用画布, 作用效果, 过渡时间, 过渡方式, 延迟);
移动画布(名称, 横坐标, 纵坐标);
```
### 删除画布
| 事件名称 | 删除画布 |
| --- | ----------- |
| 事件原形 | 删除画布(作用画布) |
| 功能描述 | 删除一个已经存在的画布 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 画布已经存在 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 删除画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
删除画布(名称);
```
### 移动画布
| 事件名称 | 移动画布 |
| --- | --- |
| 事件原形 | 移动画布(作用画布, 横坐标, 纵坐标, 移动模式) |
| 功能描述 | 移动指定画布一定指定距离 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标:画布的起点横坐标(左上角) |
| 输入参数 3 | 纵坐标:画布的起点纵坐标(左上角) |
| 输入参数 4 | 移动模式:是,则为相对坐标模式,否则为绝对坐标模式 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
| 移动模式 | 说明 |
| --- | --- |
| 绝对坐标 | 横纵坐标为画布移动后左上角的坐标位置 |
| 相对坐标 | 横纵坐标为画布在横向与纵向上移动的距离值 |
例:
```js
/* 这是UI编辑器中 移动画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
移动画布(名称, 横坐标, 纵坐标false);
```
### 缩放画布
| 事件名称 | 缩放画布 |
| --- | --- |
| 事件原形 | 缩放画布(作用画布, 宽度, 高度, 修改样式) |
| 功能描述 | 调整画布至指定尺寸大小 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 宽度:画布新的宽度尺寸 |
| 输入参数 3 | 高度:画布新的高度尺寸 |
| 输入参数 4 | 修改样式如果是那么只会修改css的长宽导致模糊如果不是那么会修改画布的长宽清晰但会清空已绘制内容 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 缩放画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
缩放画布(名称, 宽度, 高度, false);
```
### 旋转画布
| 事件名称 | 旋转画布 |
| --- | --- |
| 事件原形 | 旋转画布(作用画布, 中心横坐标, 中心纵坐标, 角度) |
| 功能描述 | 指定画布以指定的中心点旋转一定的角度 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 中心横坐标:旋转中心的横坐标 |
| 输入参数 3 | 中心纵坐标:旋转中心的纵坐标 |
| 输入参数 4 | 角度:旋转的角度,正数顺时针旋转,负数逆时针旋转 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 旋转画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
旋转画布(名称, 中心横坐标, 中心纵坐标, 角度);
```
### 擦除画布
| 事件名称 | 擦除画布 |
| --- | ----------- |
| 事件原形 | 擦除画布(作用画布, 横坐标, 纵坐标, 宽度, 高度) |
| 功能描述 | 擦除画布指定区域内的所有内容 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标:区域的起点横坐标(左上角) |
| 输入参数 3 | 纵坐标:区域的起点纵坐标(左上角) |
| 输入参数 4 | 宽度:区域的宽度 |
| 输入参数 5 | 高度:区域的高度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
如果不填的话,默认擦除全部
例:
```js
/* 这是UI编辑器中 擦除画布 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
擦除画布(名称, 横坐标, 纵坐标, 宽度, 高度);
```
### 设置CSS效果
| 事件名称 | 设置CSS效果 |
| --- | ----------- |
| 事件原形 | 设置CSS效果(作用画布, CSS效果) |
| 功能描述 | 修改指定画布的样式 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | CSS效果编辑框内输入自定义的CSS效果 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
| 常用CSS效果 | 说明 |
| --- | --- |
| opacity | 不透明度一个浮点型数字在0~1区间内 |
| background-color | 背景色,可以使用`#rgba #rrggbbaa #rgb #rrggbb rgb(r,g,b) rgba(r,g,b,a) 英文单词`共7种形式 |
| background-image | 背景图片,可使用`url(project/images/image.png)`的形式 |
| box-shadow | 元素阴影,形式为 `x偏移 y偏移 阴影大小 颜色, x偏移 y偏移 阴影大小 颜色` |
| transform | 元素的2D和3D转换用法过多请百度搜索或在mdn搜索 |
| filter | 元素滤镜,同上 |
例:
```js
/* 这是UI编辑器中 设置CSS效果 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置CSS效果(名称, CSS效果);
```
### 设置混合方式
| 事件名称 | 设置混合方式 |
| --- | ----------- |
| 事件原形 | 设置混合方式(作用画布, 混合方式) |
| 功能描述 | 设置新绘制的内容与已绘制的内容间的叠加方式 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 混合方式:设置画布的混合方式 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
混合方式说明:包括`color color-burn color-dodge copy darken destination-atop destination-in destination-out destination-over difference exclusion hard-light hue lighten lighter luminosity multiply overlay saturation screen soft-light source-atop source-in source-out source-over xor`共26种默认为source-over其余效果请自行百度或尝试
例:
```js
/* 这是UI编辑器中 设置混合方式 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置混合方式(名称, 混合方式);
```
### 设置绘制滤镜
| 事件名称 | 设置绘制滤镜 |
| --- | ----------- |
| 事件原形 | 设置绘制滤镜(作用画布, 滤镜) |
| 功能描述 | 设置在绘制时的滤镜,不影响已绘制内容 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 滤镜:编辑框内输入自定义的滤镜 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
| 常见滤镜 | 说明 |
| --- | --- |
| blur | 高斯模糊用法示例blur(5px) |
| brightness | 亮度100%为原始亮度用法示例brightness(120%) |
| contrast | 对比度100%为原始对比度用法示例contrast(80%) |
| grayscale | 灰度0%为原始灰度设为100%将会变成黑白用法示例grayscale(75%) |
| opacity | 不透明度100%为原始不透明度设为0%将会完全透明用法示例opacity(80%) |
滤镜填写示例:`blur(5px)brightness(120%)grayscale(50%)`,注意会完全覆盖之前的效果,也就是说之前的效果会全部失效
例:
```js
/* 这是UI编辑器中 设置绘制滤镜 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置绘制滤镜(名称, 滤镜);
```
### 设置阴影
| 事件名称 | 设置阴影 |
| --- | ----------- |
| 事件原形 | 设置阴影(作用画布, 阴影模糊, 阴影颜色, 阴影横坐标, 阴影纵坐标) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 阴影模糊阴影的高斯模糊与设置绘制滤镜的blur相同 |
| 输入参数 3 | 阴影颜色与设置css效果中对background-color的描述相同 |
| 输入参数 4 | 阴影横坐标:阴影偏移绘制位置的横坐标 |
| 输入参数 5 | 阴影纵坐标:阴影偏移绘制位置的纵坐标 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 设置阴影 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置阴影(名称, 阴影模糊, 阴影颜色, 宽度, 高度);
```
### 设置文本属性
| 事件名称 | 设置文本属性 |
| --- | ----------- |
| 事件原形 | 设置文本属性(作用画布, 文本方向, 文本对齐, 文本基线) |
| 功能描述 | 设置文本方向、文本对齐、文本基线 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 文本方向:文字阅读的方向 |
| 输入参数 3 | 文本对齐:文本水平对齐的方向 |
| 输入参数 4 | 文本基线:文本竖直对齐的方向 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
| 文本方向 | 说明 |
| --- | --- |
| ltr | 从左往右 |
| rtl | 从右往左 |
| 文本对齐 | 说明 |
| --- | --- |
| left | 左对齐 |
| center | 居中对齐 |
| right | 右对齐 |
| 文本基线 | 说明 |
| --- | --- |
| bottom | 底部对齐 |
| middle | 居中对齐 |
| top | 顶部对齐 |
例:
```js
/* 这是UI编辑器中 设置文本信息 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置文本属性(名称, 文本方向, 文本对齐, 文本基线);
```
### 设置不透明度
| 事件名称 | 设置不透明度 |
| --- | ----------- |
| 事件原形 | 设置不透明度(作用画布, 文本不透明度) |
| 功能描述 | 设置画布之后绘制的不透明度 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 不透明度:要设置到的不透明度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
该操作对已绘制内容没有影响如果想要对已绘制内容也产生影响请使用设置css
例:
```js
/* 这是UI编辑器中 设置不透明度 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
设置不透明度(名称, 不透明度);
```
### 保存画布属性
| 事件名称 | 保存画布属性 |
| --- | ----------- |
| 事件原形 | 保存画布属性(作用画布) |
| 功能描述 | 保存画布属性,注意之会保存之前设置的属性,如文本属性,不会保存绘制内容 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 保存画布属性 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
保存画布属性(名称);
```
### 回退画布属性
| 事件名称 | 回退画布属性 |
| --- | ----------- |
| 事件原形 | 回退画布属性(作用画布) |
| 功能描述 | 回退画布属性,注意只会回退画布的属性,如文本属性,不会回退绘制内容 |
| 输入参数 1 | 作用画布:画布的名称 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 回退画布属性 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
回退画布属性(名称);
```
### 绘制直线
| 事件名称 | 绘制直线 |
| --- | ----------- |
| 事件原形 | 绘制直线(作用画布, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 连线宽度, 连线颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 起点横坐标: |
| 输入参数 3 | 起点纵坐标: |
| 输入参数 4 | 终点横坐标: |
| 输入参数 4 | 终点纵坐标: |
| 输入参数 7 | 连线宽度: |
| 输入参数 8 | 连线颜色: |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制直线 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制直线(名称, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 连线宽度, 连线颜色);
```
### 绘制弧线
| 事件名称 | 绘制弧线 |
| --- | ----------- |
| 事件原形 | 绘制弧线(作用画布, 中心横坐标, 中心纵坐标, 半径, 起始弧度, 终止弧度, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标 |
| 输入参数 3 | 纵坐标 |
| 输入参数 4 | 半径 |
| 输入参数 4 | 起始弧度 |
| 输入参数 4 | 终止弧度 |
| 输入参数 6 | 是否为边框 |
| 输入参数 7 | 线条宽度 |
| 输入参数 8 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制弧线 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制弧线(名称, 中心横坐标, 中心纵坐标, 半径, 起始弧度, 终止弧度, 是否为边框, 线条宽度, 颜色);
```
### 绘制圆
| 事件名称 | 绘制圆 |
| --- | ----------- |
| 事件原形 | 绘制圆(作用画布, 中心横坐标, 中心纵坐标, 半径, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标 |
| 输入参数 3 | 纵坐标 |
| 输入参数 4 | 半径 |
| 输入参数 6 | 是否为边框 |
| 输入参数 7 | 线条宽度 |
| 输入参数 8 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制圆 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制圆(名称, 中心横坐标, 中心纵坐标, 半径, 是否为边框, 线条宽度, 颜色);
```
### 绘制矩形
| 事件名称 | 绘制矩形 |
| --- | ----------- |
| 事件原形 | 绘制矩形(作用画布, 横坐标, 纵坐标, 宽度, 高度, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标 |
| 输入参数 3 | 纵坐标 |
| 输入参数 4 | 宽度 |
| 输入参数 5 | 高度 |
| 输入参数 6 | 是否为边框 |
| 输入参数 7 | 线条宽度 |
| 输入参数 8 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制矩形 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制矩形(名称, 横坐标, 纵坐标, 宽度, 高度, 是否为边框, 线条宽度, 颜色);
```
### 绘制圆角矩形
| 事件名称 | 绘制圆角矩形 |
| --- | ----------- |
| 事件原形 | 绘制圆角矩形(作用画布, 横坐标, 纵坐标, 宽度, 高度, 圆角半径, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 横坐标 |
| 输入参数 3 | 纵坐标 |
| 输入参数 4 | 宽度 |
| 输入参数 5 | 高度 |
| 输入参数 5 | 圆角半径 |
| 输入参数 6 | 是否为边框 |
| 输入参数 7 | 线条宽度 |
| 输入参数 8 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制圆角矩形 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制圆角矩形(名称, 横坐标, 纵坐标, 宽度, 高度, 圆角半径, 是否为边框, 线条宽度, 颜色);
```
### 绘制多边形
| 事件名称 | 绘制多边形 |
| --- | ----------- |
| 事件原形 | 绘制多边形(作用画布, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 是否为边框 |
| 输入参数 3 | 线条宽度 |
| 输入参数 4 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制多边形 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制多边形(名称, 是否为边框, 线条宽度, 颜色);
```
### 绘制椭圆
| 事件名称 | 绘制椭圆 |
| --- | ----------- |
| 事件原形 | 绘制椭圆(作用画布, 中心横坐标, 中心纵坐标, 半长轴, 半短轴, 旋转角度, 是否为边框, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 中心横坐标 |
| 输入参数 3 | 中心纵坐标 |
| 输入参数 4 | 半长轴 |
| 输入参数 5 | 半短轴 |
| 输入参数 6 | 旋转角度 |
| 输入参数 7 | 是否为边框 |
| 输入参数 8 | 线条宽度 |
| 输入参数 9 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制椭圆 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制椭圆(名称, 中心横坐标, 中心纵坐标, 半长轴, 半短轴, 旋转角度, 是否为边框, 线条宽度, 颜色)
```
### 绘制箭头
| 事件名称 | 绘制箭头 |
| --- | ----------- |
| 事件原形 | 绘制箭头(作用画布, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 连线宽度, 连线颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 起点横坐标 |
| 输入参数 3 | 起点纵坐标 |
| 输入参数 4 | 终点横坐标 |
| 输入参数 5 | 终点纵坐标 |
| 输入参数 6 | 连线宽度 |
| 输入参数 7 | 连线颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制箭头 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制箭头(名称, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 连线宽度, 连线颜色);
```
### 绘制贝塞尔曲线
| 事件名称 | 绘制贝塞尔曲线 |
| --- | ----------- |
| 事件原形 | 绘制贝塞尔曲线(作用画布, 控制点1横坐标, 控制点1纵坐标, 控制点2横坐标, 控制点2纵坐标, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 线条宽度, 颜色) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 控制点1横坐标 |
| 输入参数 3 | 控制点1纵坐标 |
| 输入参数 4 | 控制点2横坐标 |
| 输入参数 5 | 控制点2纵坐标 |
| 输入参数 6 | 起点横坐标 |
| 输入参数 7 | 起点纵坐标 |
| 输入参数 8 | 终点横坐标 |
| 输入参数 9 | 终点纵坐标 |
| 输入参数 10 | 线条宽度 |
| 输入参数 11 | 颜色 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制贝塞尔曲线 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制贝塞尔曲线(名称, 控制点1横坐标, 控制点1纵坐标, 控制点2横坐标, 控制点2纵坐标, 起点横坐标, 起点纵坐标, 终点横坐标, 终点纵坐标, 线条宽度, 颜色);
```
### 绘制图片
| 事件名称 | 绘制图片 |
| --- | ----------- |
| 事件原形 | 绘制图片(作用画布, 图片, 裁切点横坐标, 裁切点纵坐标, 裁切宽度, 裁切高度, 绘制点横坐标, 绘制点纵坐标, 绘制宽度, 绘制高度) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 图片 |
| 输入参数 3 | 裁切点横坐标 |
| 输入参数 4 | 裁切点纵坐标 |
| 输入参数 5 | 裁切宽度 |
| 输入参数 6 | 裁切高度 |
| 输入参数 7 | 绘制点横坐标 |
| 输入参数 8 | 绘制点纵坐标 |
| 输入参数 9 | 绘制宽度 |
| 输入参数 10 | 绘制高度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制图片 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制图片(名称, 图片, 裁切点横坐标, 裁切点纵坐标, 裁切宽度, 裁切高度, 绘制点横坐标, 绘制点纵坐标, 绘制宽度, 绘制高度);
```
### 绘制图标
| 事件名称 | 绘制图标 |
| --- | ----------- |
| 事件原形 | 绘制图标(作用画布, 图标id, 横坐标, 纵坐标, 帧数) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 图标id |
| 输入参数 3 | 横坐标 |
| 输入参数 4 | 纵坐标 |
| 输入参数 5 | 宽度 |
| 输入参数 6 | 高度 |
| 输入参数 7 | 帧数:绘制图标的第几帧 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制图标 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制图标(名称, 图标id, 横坐标, 纵坐标, 帧数);
```
### 绘制窗口皮肤
| 事件名称 | 绘制窗口皮肤 |
| --- | ----------- |
| 事件原形 | 绘制窗口皮肤(作用画布, 皮肤背景, 横坐标, 纵坐标, 宽度, 高度) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 皮肤背景:需为已注册的图片 |
| 输入参数 3 | 横坐标 |
| 输入参数 4 | 纵坐标 |
| 输入参数 5 | 宽度 |
| 输入参数 6 | 高度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制窗口皮肤 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制窗口皮肤(名称, 皮肤背景, 横坐标, 纵坐标, 宽度, 高度);
```
### 绘制文本
| 事件名称 | 绘制文本 |
| --- | ----------- |
| 事件原形 | 绘制文本(作用画布, 文字, 横坐标, 纵坐标, 添加描边, 斜体, 字体, 字体大小, 字体粗细, 字体颜色, 描边颜色, 最大宽度) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 文本 |
| 输入参数 3 | 横坐标 |
| 输入参数 4 | 纵坐标 |
| 输入参数 5 | 添加描边 |
| 输入参数 6 | 斜体 |
| 输入参数 7 | 字体 |
| 输入参数 8 | 字体大小 |
| 输入参数 9 | 字体粗细 |
| 输入参数 10 | 字体颜色 |
| 输入参数 11 | 描边颜色 |
| 输入参数 12 | 最大宽度 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制文本 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制文本(名称, 文字, 横坐标, 纵坐标, 添加描边, 斜体, 字体, 字体大小, 字体粗细, 字体颜色, 描边颜色, 最大宽度);
```
### 绘制成段文本
| 事件名称 | 绘制成段文本 |
| --- | ----------- |
| 事件原形 | 绘制成段文本(作用画布, 文本, 横坐标, 纵坐标, 宽度, 颜色, 对齐, 字体大小, 行间距, 打字机时间间隔, 字体, 字符间距, 加粗, 斜体) |
| 功能描述 | |
| 输入参数 1 | 作用画布:画布的名称 |
| 输入参数 2 | 文本 |
| 输入参数 3 | 横坐标 |
| 输入参数 4 | 纵坐标 |
| 输入参数 5 | 宽度 |
| 输入参数 6 | 颜色 |
| 输入参数 7 | 对齐 |
| 输入参数 8 | 字体大小 |
| 输入参数 9 | 行间距 |
| 输入参数 10 | 打字机时间间隔 |
| 输入参数 11 | 字体 |
| 输入参数 12 | 字符间距 |
| 输入参数 13 | 加粗 |
| 输入参数 14 | 斜体 |
| 输出参数 | 无 |
| 返回值 | 无 |
| 先决条件 | 无 |
| 调用函数 | 无 |
例:
```js
/* 这是UI编辑器中 绘制成段文本 的参考例程 */
创建画布(名称, 横坐标, 纵坐标, 宽度, 高度, 纵深);
绘制成段文本(名称, 文本, 横坐标, 纵坐标, 宽度, 颜色, 对齐, 字体大小, 行间距, 打字机时间间隔, 字体, 字符间距, 加粗, 斜体);
```
==========================================================================================
[继续阅读下一章API](api)

831
ui.d.ts vendored Normal file
View File

@ -0,0 +1,831 @@
/**
*
*/
type CanvasStyle = string | CanvasGradient | CanvasPattern;
type ImageSource =
| CanvasImageSource
| ImageIds
| `${ImageIds}${ImageReverse}`
| NameMapIn<ImageIds>
| `${NameMapIn<ImageIds>}${ImageReverse}`;
interface BackgroundPosInfo {
/**
*
*/
px: number;
/**
*
*/
py: number;
/**
*
*/
noPeak: boolean;
/**
*
*/
xoffset: number;
/**
*
*/
yoffset: number;
/**
* ui
*/
ctx: CtxRefer;
/**
*
*/
position: 'up' | 'bottom';
}
interface TextContentConfig {
left: number;
top: number;
/**
*
*/
maxWidth: number;
/**
* \r
*/
color: Color;
/**
*
*/
align: 'left' | 'center' | 'right';
/**
*
*/
fontSize: number;
/**
*
*/
lineHeight: number;
/**
*
*/
time: number;
/**
*
*/
font: string;
/**
*
*/
letterSpacing: number;
/**
*
*/
bold: boolean;
/**
*
*/
italic: boolean;
}
interface TextContentBlock {
left: number;
top: number;
width: number;
height: number;
line: number;
marginLeft: number;
marginTop: number;
}
interface ReturnedTextContentConfig extends TextContentConfig {
right: number;
/**
*
*/
defaultFont: string;
/**
*
*/
index: number;
/**
*
*/
currcolor: Color;
/**
*
*/
currfont: string;
/**
*
*/
lineMargin: number;
/**
*
*/
topMargin: number;
/**
*
*/
offsetX: number;
/**
*
*/
offsetY: number;
/**
*
*/
line: number;
/**
*
*/
blocks: TextContentBlock[];
/**
*
*/
isHD: boolean;
/**
*
*/
lineMaxHeight: number;
/**
*
*/
forceChangeLine: boolean;
}
interface TextBoxConfig {
/**
*
*/
ctx: CtxRefer;
/**
*
*/
pos: TextBoxPos;
/**
*
*/
showAll: boolean;
/**
*
*/
async: boolean;
}
/**
* UI窗口的绘制
*/
interface Ui {
/**
* ui数据
*/
uidata: UiData;
/**
* contextnull
* context自身
*/
getContextByName(canvas: CtxRefer): CanvasRenderingContext2D | null;
/**
*
* name为画布名context本身
* name也可以是'all'all则为清空所有系统画布
*/
clearMap(
name: CtxRefer,
x?: number,
y?: number,
w?: number,
h?: number
): void;
/**
*
* @param text
* @param style
* @param font
* @param maxWidth 使
*/
fillText(
name: CtxRefer,
text: string,
x: number,
y: number,
style?: CanvasStyle,
font?: string,
maxWidth?: number
): void;
/**
*
* @param name
* @param text
* @param maxWidth
* @param font
*/
setFontForMaxWidth(
name: CtxRefer,
text: string,
maxWidth: number,
font?: string
): void;
/**
*
* @param text
* @param style
* @param strokeStyle
* @param font
* @param lineWidth 线
*/
fillBoldText(
name: CtxRefer,
text: string,
x: number,
y: number,
style?: CanvasStyle,
strokeStyle?: CanvasStyle,
font?: string,
maxWidth?: number,
lineWidth?: number
): void;
/**
*
* @param style
* @param angle
*/
fillRect(
name: CtxRefer,
x: number,
y: number,
width: number,
height: number,
style?: CanvasStyle,
angle?: number
): void;
/**
*
* @param style
* @param angle
*/
strokeRect(
name: CtxRefer,
x: number,
y: number,
width: number,
height: number,
style?: CanvasStyle,
lineWidth?: number,
angle?: number
): void;
/**
* canvas上绘制一个圆角矩形
*/
fillRoundRect(
name: CtxRefer,
x: number,
y: number,
width: number,
height: number,
radius: number,
style?: CanvasStyle,
angle?: number
): void;
/**
* canvas上绘制一个圆角矩形的边框
*/
strokeRoundRect(
name: CtxRefer,
x: number,
y: number,
width: number,
height: number,
radius: number,
style?: CanvasStyle,
lineWidth?: number,
angle?: number
): void;
/**
* canvas上绘制一个多边形
*/
fillPolygon(
name: CtxRefer,
nodes?: [x: number, y: number][],
style?: CanvasStyle
): void;
/**
* canvas上绘制一个多边形的边框
*/
strokePolygon(
name: CtxRefer,
nodes?: [x: number, y: number][],
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas上绘制一个椭圆
* @param a
* @param b
* @param angle
*/
fillEllipse(
name: CtxRefer,
x: number,
y: number,
a: number,
b: number,
angle?: number,
style?: CanvasStyle
): void;
/**
* canvas上绘制一个圆
*/
fillCircle(
name: CtxRefer,
x: number,
y: number,
r: number,
style?: CanvasStyle
): void;
/**
* canvas上绘制一个椭圆的边框
* @param a
* @param b
* @param angle
*/
strokeEllipse(
name: CtxRefer,
x: number,
y: number,
a: number,
b: number,
angle?: number,
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas上绘制一个圆的边框
*/
strokeCircle(
name: CtxRefer,
x: number,
y: number,
r: any,
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas上绘制一个扇形
*/
fillArc(
name: CtxRefer,
x: number,
y: number,
r: number,
start: number,
end: number,
style?: CanvasStyle
): void;
/**
* canvas上绘制一段弧
*/
strokeArc(
name: CtxRefer,
x: number,
y: number,
r: number,
start: number,
end: number,
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas上绘制一条线
*/
drawLine(
name: CtxRefer,
x1: number,
y1: number,
x2: number,
y2: number,
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas上绘制一个箭头
*/
drawArrow(
name: CtxRefer,
x1: number,
y1: number,
x2: number,
y2: number,
style?: CanvasStyle,
lineWidth?: number
): void;
/**
* canvas的文字字体
*/
setFont(name: CtxRefer, font: string): void;
/**
* canvas的线宽度
*/
setLineWidth(name: CtxRefer, lineWidth: number): void;
/**
* canvas状态
*/
saveCanvas(name: CtxRefer): void;
/**
* 退canvas状态
*/
loadCanvas(name: CtxRefer): void;
/**
* canvas的绘制不透明度
* @returns
*/
setAlpha(name: CtxRefer, alpha: number): number;
/**
*
*/
setOpacity(name: CtxRefer, opacity: number): void;
/**
* canvas的滤镜
*/
setFilter(name: CtxRefer, filter?: string): void;
/**
* canvas的填充样式
*/
setFillStyle(name: CtxRefer, style: CanvasStyle): void;
/**
* canvas描边样式
*/
setStrokeStyle(name: CtxRefer, style: CanvasStyle): void;
/**
* canvas的文字左右对齐方式
*/
setTextAlign(name: CtxRefer, align: CanvasTextAlign): void;
/**
* canvas的文字上下对齐方式
*/
setTextBaseline(name: CtxRefer, baseline: CanvasTextBaseline): void;
/**
*
*/
calWidth(name: CtxRefer, text: string, font?: string): number;
/**
*
*/
splitLines(
name: CtxRefer,
text: string,
maxWidth?: number,
font?: string
): string[];
/**
*
* @param dx
* @param dy
*/
drawImage(name: CtxRefer, image: ImageSource, dx: number, dy: number): void;
/**
*
* @param dx
* @param dy
* @param dw
* @param dh
*/
drawImage(
name: CtxRefer,
image: ImageSource,
dx: number,
dy: number,
dw: number,
dh: number
): void;
/**
*
* @param sx
* @param sy
* @param sw
* @param sh
* @param dx
* @param dy
* @param dw
* @param dh
*/
drawImage(
name: CtxRefer,
image: ImageSource,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void;
/**
* canvas上绘制一个图标
* @param frame
*/
drawIcon(
name: CtxRefer,
id: AllIds,
x: number,
y: number,
w?: number,
h?: number,
frame?: number
): void;
/**
* UI窗口
*/
closePanel(): void;
/**
* UI层内容
*/
clearUI(): void;
/**
*
* @param text
* @param id ID
* @param frame
*/
drawTip(text: string, id?: AllIds, frame?: number): void;
/**
*
*/
drawText(contents: string, callback?: () => void): void;
/**
*
*/
drawUIEventSelector(
code: number,
background: RGBArray | ImageIds,
x: number,
y: number,
w: number,
h: number,
z?: number
): void;
/**
*
* @param code
*/
clearUIEventSelector(code?: number | number[]): void;
/**
* WindowSkin
* @param direction
*/
drawWindowSkin(
background: any,
ctx: CtxRefer,
x: number,
y: number,
w: number,
h: number,
direction?: 'up' | 'down',
px?: number,
py?: number
): void;
/**
* winskin或纯色背景
*/
drawBackground(
left: string,
top: string,
right: string,
bottom: string,
posInfo?: Partial<BackgroundPosInfo>
): void;
/**
*
* @param ctx
* @param content \n, \r[...], \i[...], \c[...], \d, \e
* @param config
* @returns
*/
drawTextContent(
ctx: CtxRefer,
content: string,
config: Partial<TextContentConfig>
): ReturnedTextContentConfig;
/**
*
*/
getTextContentHeight(
content: string,
config: Partial<TextContentConfig>
): number;
/**
*
*/
drawTextBox(content: string, config?: TextBoxConfig): void;
/**
*
*/
drawScrollText(
content: string,
time?: number,
lineHeight?: number,
callback?: () => void
): void;
/**
*
*/
textImage(content: string, lineHeight?: number): HTMLCanvasElement;
/**
*
*/
drawChoices(
content: string,
choices: string[],
width?: number,
ctx?: CtxRefer
): void;
/**
*
*/
drawConfirmBox(
text: string,
yesCallback?: () => void,
noCallback?: () => void,
ctx?: CtxRefer
): void;
/**
*
*/
drawWaiting(text: string): void;
/**
*
*/
drawPagination(page: number, totalPage: number, y?: number): void;
/**
*
*/
drawBook(index: number): void;
/**
*
*/
drawFly(page: number): void;
/**
*
*/
getToolboxItems<T extends Exclude<ItemCls, 'items'>>(cls: T): ItemIdOf<T>[];
/**
*
* @param name
* @param x
* @param y
* @param width
* @param height
* @param zIndex
* @param nonAntiAliasing 齿
*/
createCanvas(
name: string,
x: number,
y: number,
width: number,
height: number,
zIndex?: number,
nonAntiAliasing?: boolean
): CanvasRenderingContext2D;
/**
*
*/
relocateCanvas(
name: CtxRefer,
x: number,
y: number,
useDelta?: boolean
): void;
/**
*
*/
rotateCanvas(
name: CtxRefer,
angle: number,
centerX?: number,
centerY?: number
): void;
/**
*
* @param styleOnly styletrue
* @param isTempCanvas true
*/
resizeCanvas(
name: CtxRefer,
x?: number,
y?: number,
styleOnly?: boolean,
isTempCanvas?: boolean
): void;
/**
*
*/
deleteCanvas(name: string | ((name: string) => boolean)): void;
/**
*
*/
deleteAllCanvas(): void;
/**
*
*/
_drawViewMaps(): void;
_drawReplay(): void;
_drawStatistics(): void;
}
declare const ui: new () => Ui;

3519
ui.js Normal file

File diff suppressed because it is too large Load Diff

912
util.d.ts vendored Normal file
View File

@ -0,0 +1,912 @@
/** 工具类 主要用来进行一些辅助函数的计算 */
interface Utils {
/**
*
*/
readonly scan: DeepReadonly<Scan>;
/**
*
*/
readonly scan2: DeepReadonly<Scan2>;
/**
* ${}
* @example
* // 把主角的生命值和持有的黄钥匙数量代入这句话
* core.replaceText('衬衫的价格是${status:hp}镑${item:yellowKey}便士。');
* @param text 使${}js表达式
* @param prefix
* @returns
*/
replaceText(text: string, prefix?: string): string;
/**
* 如status:xxx等
*
* @example
* // 把这两个冒号表达式替换为core.getStatus('hp')和core.itemCount('yellowKey')这样的函数调用
* core.replaceValue('衬衫的价格是${status:hp}镑${item:yellowKey}便士。');
* @param value
* @returns
*/
replaceValue(value: string): string;
/**
* 支持status:xxx等的计算
* @example core.calValue('status:hp + status:def'); // 计算主角的生命值加防御力
* @param value
* @param prefix
* @returns
*/
calValue(value: string | Function, prefix?: string): any;
/**
* @deprecated
* ba的开头Array.unshift就行
* @example core.unshift(todo, {type: 'unfollow'}); // 在事件指令数组todo的开头插入“取消所有跟随者”指令
* @param a
* @param b
* @returns a本身得到的
*/
unshift<A extends any[], B extends any[]>(a: A, b: B): [...B, ...A];
/**
* @deprecated
* ba的末尾Array.push就行
* @example core.push(todo, {type: 'unfollow'}); // 在事件指令数组todo的末尾插入“取消所有跟随者”指令
* @param a
* @param b
* @returns a本身得到的
*/
push<A extends any[], B extends any[]>(a: A, b: B): [...A, ...B];
/**
*
* @param
*/
decompress(value: string): any;
/**
* //@deprecated
*
* @param key
* @param value
*/
setLocalStorage(key: string, value?: any): void;
/**
* //@deprecated
*
* @param key
* @param defaultValue
*/
getLocalStorage<T>(key: string, defaultValue?: T): T;
/**
* @deprecated
*
* @param key
*/
removeLocalStorage(key: string): void;
/**
* localforage
* @param key
* @param value
* @param successCallback
* @param errorCallback
*/
setLocalForage(
key: string,
value?: any,
successCallback?: () => void,
errorCallback?: (err: Error) => void
): void;
/**
* localforage读出一段数据
*/
getLocalForage<T>(
key: string,
defaultValue?: T,
successCallback?: (data: T) => void,
errorCallback?: (err: Error) => void
): void;
/**
* localforage的数据
*/
removeLocalForage(
key: string,
successCallback?: () => void,
errorCallback?: (err: Error) => void
): void;
/**
* localforage所有的数据
* @param callback
*/
clearLocalForage(callback?: (err?: Error) => void): void;
/**
* localforage的数据
* @param iteratee
* @param callback
*/
iterateLocalForage<T, U>(
iteratee: (value: T, key: string, iterationNumber: number) => U,
callback?: (err: any, result: U) => void
): void;
/**
* localforage数据的所有的键
* @param callback
*/
keysLocalForage(callback?: (err: any, keys: string[]) => void): void;
/**
* localforage数据的数据量
* @param callback
*/
lengthLocalForage(
callback?: (err: any, numberOfKeys: number) => void
): void;
/**
* @deprecated
* 适用于global:xxx
* @example core.setBlobal('一周目已通关', true); // 设置全局存储“一周目已通关”为true方便二周目游戏中的新要素。
* @param key
* @param value null表示清除此全局存储
*/
setGlobal(key: string, value?: any): void;
/**
* @deprecated
* 适用于global:xxx
* @example if (core.getGlobal('一周目已通关', false) === true) core.getItem('dagger'); // 二周目游戏进行到此处时会获得一把屠龙匕首
* @param key
* @param defaultValue nullundefined时
* @returns
*/
getGlobal<T>(key: string, defaultValue?: T): T;
/**
* ()
* @example core.clone(core.status.hero, (name, value) => (name == 'items' || typeof value == 'number'), false); // 深拷贝主角的属性和道具
* @param data
* @param filter data为数组或对象时拷贝哪些项或属性true表示拷贝
* @param recursion true表示过滤器也被递归
* @returns
*/
clone<T>(
data: T,
filter?: (name: string, value: any) => boolean,
recursion?: boolean
): T;
/**
* 1D或2D的数组
* @param data
*/
cloneArray<T extends any[]>(data: T): T;
/**
*
* @example core.splitImage(core.material.images.images['npc48.png'], 32, 48); // 把npc48.png切分成若干32×48px的小人
* @param image []
* @param width 32
* @param height
* @returns
*/
splitImage(
image: NameMapIn<ImageIds> | ImageIds | HTMLImageElement,
width?: number,
height?: number
): HTMLImageElement[];
/**
*
* @param date
* @returns 格式: yyyy-mm-dd hh:mm:ss
*/
formatDate(date?: Date): string;
/**
*
* @param date
* @returns 格式: yyyymmddhhmmss
*/
formatDate2(date?: Date): string;
/**
*
* @param time
* @returns 格式: hh:mm:ss
*/
formatTime(time: number): string;
/**
* @deprecated
* 使setDigits代替
*/
setTwoDigits(x: number): string;
/**
* n位数显示
* @param x
* @param n
*/
setDigits(x: number, n: number): string;
/**
*
* @param size
* @returns xx.xxB KB MB
*/
formatSize(size: number): string;
/**
* 10000w,e,z,j,g
* @example core.formatBigNumber(123456789); // "12346w"
* @param x
* @param onMap
* @returns
*/
formatBigNumber<T extends string>(x: T, onMap?: number): T;
formatBigNumber(x: number | string, onMap?: number | boolean): string;
/**
* @deprecated
* mutate-animate代替
* @param mode
*/
applyEasing(mode?: EaseMode): (x: number) => number;
/**
*
* @example core.arrayToRGB([102, 204, 255]); // "#66ccff",加载画面的宣传色
* @param color 255
* @returns 使
*/
arrayToRGB(color: RGBArray): _RGBA;
/**
*
* @example core.arrayToRGBA([102, 204, 255]); // "rgba(102,204,255,1)"
* @param color 255
* 011
* @returns
*/
arrayToRGBA(color: Color): _RGBA;
/**
* base64压缩
* @example core.encodeRoute(core.status.route); // 一压当前录像
* @param route 0-9A-Za-z和下划线
* JSON.stringify预处理再base64压缩才能交由一压
* @returns
*/
encodeRoute(route: string[]): string;
/**
*
* @example core.decodeRoute(core.encodeRoute(core.status.route)); // 一压当前录像再解压-_-|
* @param route
* @returns
*/
decodeRoute(route: string): string[];
/**
* nullundefined和NaN
* @example core.isset(0/0); // false因为0/0等于NaN
* @param v
* @returns false表示待测值为nullundefinedNaN或未填写true表示为其他值
*/
isset(v?: any): boolean;
/**
*
* @example core.subarray(['ad', '米库', '小精灵', '小破草', '小艾'], ['ad', '米库', '小精灵']); // ['小破草', '小艾']
* @param a b短将返回null
* @param b a长将返回null
* @returns b不是a的前缀将返回nulla去掉此前缀后的剩余数组
*/
subarray(a: any[], b: any[]): any[] | null;
/**
* @deprecated
* array是不是一个数组element是否在该数组中使Array.includes代替
* @param array false
* @param element
* @returns array为数组且具有element这项truefalse
*/
inArray(array?: any, element?: any): boolean;
/**
* x限定在[a,b]a和b可交换
* @example core.clamp(1200, 1, 1000); // 1000
* @param x !x为true时x一律视为0
* @param a b将导致与b交换
* @param b a将导致与a交换
*/
clamp(x: number, a: number, b: number): number;
/**
* 访cookie
*/
getCookie(name: string): string;
/**
* @deprecated
*
* @example
* // 更新状态栏中的主角生命,使用加载画面的宣传色
* core.setStatusBarInnerHTML('hp', core.status.hero.hp, 'color: #66CCFF');
* @param name 'hp', 'atk', 'def'core.statusBar中的一个合法项
* @param value 6
* @param css css样式
*/
setStatusBarInnerHTML(
name: string,
value: string | number,
css?: string
): void;
/**
* Verdana不是等宽字体
* @example core.strlen('无敌ad'); // 6
* @param str
* @returns 2ASCII字符为1
*/
strlen(str: string): number;
/**
*
* @param turn
* @param direction
*/
turnDirection(turn: HeroTurnDir, direction?: Dir): string;
/**
*
* @example core.playSound(core.matchWildcard('*Key', itemId) ? 'item.mp3' : 'door.mp3'); // 判断捡到的是钥匙还是别的道具,从而播放不同的音效
* @param pattern 0
* @param string
* @returns true表示匹配成功false表示匹配失败
*/
matchWildcard(pattern: string, string: string): boolean;
/**
* /RegExp/.test(str)
* @param pattern
* @param string
*/
matchRegex(pattern: string, string: string): string;
/**
* base64加密
* @example
* core.encodeBase64('If you found this note in a small wooden box with a heart on it');
* // "SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0"
* @param str
* @returns
*/
encodeBase64(str: string): string;
/**
* base64解密
* @example
* core.decodeBase64('SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0');
* // "If you found this note in a small wooden box with a heart on it"
* @param str
* @returns
*/
decodeBase64(str: string): string;
/**
* SL的随机数
* @exmaple 1 + core.rand(6); // 随机生成一个小于7的正整数模拟骰子的效果
* @param num num的随机自然数1
* @returns 使
*/
rand(num?: number): number;
/**
* SL的随机数
* @exmaple 1 + core.rand2(6); // 随机生成一个小于7的正整数模拟骰子的效果
* @param num 02147483648
* @returns [0, num)
*/
rand2(num?: number): number;
/**
* []
* @param success
* @param error
* @param accept input元素的accept属性
* @param readType DataUrl形式读取
*/
readFile(
success: (obj: any) => void,
error: () => void,
accept: string,
readType: boolean
): void;
/**
* []
* @param content
*/
readFileContent(content: string): void;
/**
*
* @example core.download('route.txt', core.status.route); // 弹窗请求下载录像
* @param filename
* @param content
*/
download(filename: string, content: string | string[]): void;
/**
*
* @param data 西
*/
copy(data: string): void;
/**
* core.drawConfirmBox()
* @example core.myconfirm('重启游戏?', core.restart); // 弹窗询问玩家是否重启游戏
* @param hint
* @param yesCallback
* @param noCallback
*/
myconfirm(
hint: string,
yesCallback: () => void,
noCallback?: () => void
): void;
/**
*
*/
myprompt(
hint: string,
value: string,
callback?: (data?: string) => void
): void;
/**
* @deprecated
* vue了Transition组件和css的transition比这个强得多
*/
showWithAnimate(
obj?: HTMLElement,
speed?: number,
callback?: () => any
): void;
/**
* @deprecated
* 使
*/
hideWithAnimate(
obj?: HTMLElement,
speed?: number,
callback?: () => any
): void;
/**
* guid
*/
getGuid(): string;
/**
*
* @param obj
*/
hashCode(obj: any): number;
/**
* ,
* @example core.same(['1', 2], ['1', 2]); // true
*/
same(a: any, b: any): boolean;
/**
*
*/
unzip(
blobOrUrl: string | Blob,
success?: (data: any) => void,
error?: (error: string) => void,
convertToText?: boolean,
onprogress?: (loaded: number, total: number) => void
): void;
/**
* HTTP请求 []
* @param type
* @param url
* @param formData POST请求则为表单数据
* @param success
* @param error
*/
http(
type: 'GET' | 'POST',
url: string,
formData?: FormData,
success?: (res: any) => void,
error?: (err: string) => void,
mimeType?: string,
responseType?: XMLHttpRequestResponseType,
onProgress?: (loaded: number, total: number) => void
): void;
}
declare const utils: new () => Utils;
/**
* APP接口使
*/
interface JSInterface {
/** 强制横屏 */
requireLandscape(): void;
}
declare const jsinterface: JSInterface;
interface Window {
readonly jsinterface: JSInterface;
}
/**
*
*/
type Step = Move | 'backward';
/**
*
*/
type LocString = `${number},${number}`;
type _RGBA =
| `rgb(${number},${number},${number})`
| `rgba(${number},${number},${number},${number})`;
/**
* RGBA颜色数组
*/
type RGBArray = [number, number, number, number?];
/**
*
*/
type Color = `#${string}` | _RGBA | RGBArray | 'transparent';
/**
*
*/
type Dir = 'up' | 'down' | 'left' | 'right';
/**
*
*/
type Dir2 = Dir | 'leftup' | 'rightup' | 'leftdown' | 'rightdown';
/**
*
*/
type TurnDir = Dir | ':left' | ':right' | ':back';
/**
*
*/
type HeroTurnDir = TurnDir | ':hero' | ':backhero';
/**
*
*/
type TextPosition = 'up' | 'center' | 'down';
/**
*
*/
type Move = 'forward' | Dir;
/**
*
*/
type EaseMode = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
/**
* \
* += \
* -= \
* *= \
* /= \
* //= 除以并取商\
* **= \
* %= \
* min= \
* max= \
*
*/
type MotaOperator =
| '+='
| '-='
| '*='
| '/='
| '//='
| '**='
| '%='
| 'min='
| 'max='
| '=';
/**
*
*/
type LocArr = [x: number, y: number];
/**
*
*/
interface Loc {
/**
*
*/
x: number;
/**
*
*/
y: number;
}
/**
*
*/
interface DiredLoc extends Loc {
/**
*
*/
direction: Dir;
}
interface CompressedStep {
/**
*
*/
direction: Dir;
/**
*
*/
step: number;
}
/**
*
*/
type Scan = {
[D in Dir]: Loc;
};
/**
*
*/
type Scan2 = {
[D in Dir2]: Loc;
};
/**
*
*/
type ImageReverse = ':o' | ':x' | ':y';
/**
*
*/
type TextBoxDir = 'up' | 'down';
/**
*
*/
type TextBoxPos =
| `${TextBoxDir},hero`
| `${TextBoxDir},${number},${number}`
| TextPosition;
/**
*
*/
type CtxRefer = string | CanvasRenderingContext2D | HTMLCanvasElement;
/**
*
*/
type MotaTrigger =
| 'battle'
| 'pusBox'
| 'openDoor'
| 'ski'
| 'custom'
| 'getItem'
| 'changeFloor'
| 'null';
/**
*
*/
type FloorChangeStair =
| 'upFloor'
| 'downFloor'
| ':symmetry'
| ':symmetry_x'
| ':symmetry_y'
| 'flyPoint';
/**
*
*/
type EventValuePreffix =
| 'status'
| 'flag'
| 'item'
| 'buff'
| 'switch'
| 'temp'
| 'global';
interface Animate {
/**
* s
*/
frame: number;
/**
*
*/
frames: FrameObj[][];
/**
*
*/
images: HTMLImageElement[];
/**
*
*/
ratio: number;
/**
*
*/
se: string;
}
type Save = DeepReadonly<{
/**
* id
*/
floorId: FloorIds;
/**
*
*/
hero: HeroStatus;
/**
*
*/
hard: number;
/**
*
*/
maps: Record<string, ResolvedFloor>;
/**
*
*/
route: string;
/**
*
*/
values: CoreValues;
/**
*
*/
version: string;
/**
* guid
*/
guid: string;
/**
*
*/
time: number;
}>;
type ValueType = number | string | boolean | undefined | null | bigint | symbol;
/**
* 使
*/
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends ValueType ? T[P] : DeepReadonly<T[P]>;
};
/**
* 使
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends ValueType ? T[P] : DeepPartial<T[P]>;
};
/**
* 使
*/
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends ValueType ? T[P] : DeepRequired<T[P]>;
};
/**
* 使
*/
type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
/**
* 使
*/
type DeepWritable<T> = {
-readonly [P in keyof T]: T[P] extends ValueType
? T[P]
: DeepWritable<T[P]>;
};
/**
*
*/
type SelectType<R, T> = {
[P in keyof R as R[P] extends T ? P : never]: R[P];
};
/**
*
*/
type SelectKey<R, T> = keyof SelectType<R, T>;
/**
*
*/
type FirstCharOf<T extends string> = T extends `${infer F}${infer A}`
? F
: never;
/**
*
*/
type NonObject = number | string | boolean;
/**
*
*/
type NonObjectOf<T> = SelectType<T, NonObject>;
/**
*
*/
type EndsWith<T extends string> = `${string}${T}`;
type KeyExcludesUnderline<T> = Exclude<keyof T, `_${string}`>;
type ValueOf<T> = T[keyof T];

1340
utils.js Normal file

File diff suppressed because it is too large Load Diff

1
vue.css Normal file

File diff suppressed because one or more lines are too long