feat: 指南

This commit is contained in:
unanmed 2024-02-08 16:50:58 +08:00
parent 53fd0493e1
commit 877a6644c8
38 changed files with 4130 additions and 150 deletions

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ meeting.md
special.csv
script/special.ts
docs/.vitepress/dist
docs/.vitepress/cache

View File

@ -9,5 +9,6 @@
"vueIndentScriptAndStyle": false,
"arrowParens": "avoid",
"trailingComma": "none",
"endOfLine": "auto"
"endOfLine": "auto",
"embeddedLanguageFormatting": "off"
}

85
docs/.vitepress/config.ts Normal file
View File

@ -0,0 +1,85 @@
import { defineConfig } from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: 'HTML5魔塔样板',
description: 'HTML5魔塔样板V2.A的说明文档',
base: '/docs/',
themeConfig: {
outline: [2, 3],
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: '主页', link: '/' },
{ text: '指南', link: '/guide/diff' },
{ text: 'API', link: '/api/' }
],
sidebar: {
'/guide/': [
{
text: '指南',
items: [
{ text: '差异说明', link: '/guide/diff' },
{ text: '系统说明', link: '/guide/system' },
{ text: '战斗系统', link: '/guide/battle' },
{ text: 'UI编写', link: '/guide/ui' },
{ text: 'UI系统', link: '/guide/ui-control' },
{ text: '事件触发系统', link: '/guide/event-emitter' },
{
text: '音频系统',
link: '/guide/audio',
items: [
{
text: 'BGM系统',
link: '/guide/audio#bgm-系统'
},
{
text: '音效系统',
link: '/guide/audio#音效系统'
}
]
},
{ text: '设置系统', link: '/guide/setting' },
{ text: '存储系统', link: '/guide/storage' },
{ text: '按键系统', link: '/guide/hotkey' }
]
}
],
'/api/': [{ text: 'API列表', items: [] }]
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
],
search: {
provider: 'local',
options: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
}
}
}
}
}
}
}
},
locales: {
root: {
lang: 'zh',
label: '中文'
}
}
});

0
docs/api/class.md Normal file
View File

0
docs/api/function.md Normal file
View File

0
docs/api/index.md Normal file
View File

0
docs/api/interface.md Normal file
View File

0
docs/api/var.md Normal file
View File

194
docs/guide/audio.md Normal file
View File

@ -0,0 +1,194 @@
# 音频系统
新样板也对音频系统进行了完全性重构,在新的音频系统下,你能够更好地控制音频的播放。
## BGM 系统
### 切换 BGM
使用 `changeTo` 函数进行切歌即可:
```ts
function changeTo(id: string, when?: number, noStack?: boolean): void
```
其中 `id` 表示要切换至的歌,`when` 表示起始时间,可以不填,默认从头开始播(如果目标歌曲已经完全停止播放了),`noStack` 表示是否不计入修改栈,一般不需要填写。
在切歌时,会有渐变效果,即当前歌曲的音量会慢慢变小,同时目标歌曲的音量会慢慢变大,直至切换完毕。在未切换完毕时,如果又切回原来的歌,那么不会从头开始播放,或者从设置的时刻开始播放,而是会接着当前时刻继续播放。
如果不想要渐变切换,可以使用 `play` 函数
```ts
function play(id: string, when?: number, noStack: boolean): void
```
其用法与 `changeTo` 完全一样,效果为没有渐变的切歌,其余的与 `changeTo` 完全一致。
```js
const { bgm } = Mota.requireAll('var');
bgm.changeTo('myBgm.mp3');
bgm.changeTo('myBgm2.mp3', 10); // 从第10秒开始播
bgm.play('myBgm.mp3'); // 无渐变的切歌
```
### 暂停与继续、撤销与恢复
可以使用 `pause``resume` 函数暂停或者继续播放,它们都可以传入一个函数,表示暂停或者继续过程是否渐变
```js
const { bgm } = Mota.requireAll('var');
bgm.pause(); // 有渐变暂停
bgm.resume(false); // 无渐变继续播放
```
除此之外,还可以通过 `undo``redo` 实现撤销与恢复的功能:
```ts
function undo(transition: boolean = true, when?: number): void
function redo(transition: boolean = true, when?: number): void
```
第一个参数表示是否渐变,默认渐变,第二个参数表示从哪开始播放,生效条件与 `changeTo` 一致。
```js
const { bgm } = Mota.requireAll('var');
bgm.changeTo('myBgm1.mp3');
bgm.changeTo('myBgm2.mp3');
bgm.changeTo('myBgm3.mp3');
// 当前播放 myBgm3
bgm.undo(); // 当前播放 myBgm2
bgm.undo(); // 当前播放 myBgm1
bgm.redo(); // 当前播放 myBgm2
bgm.changeTo('myBgm1.mp3'); // 当前播放 myBgm1
bgm.redo(); // 无法切换,保持当前播放,因为调用 changeTo 会清空恢复栈
bgm.undo(); // 当前播放 myBgm2
```
### 设置渐变参数
可以通过 `setTransition` 函数设置渐变参数:
```ts
function setTransition(time?: number, fn?: TimingFn): void
```
其中 `time` 参数表示渐变切歌的时长,即经过多长时间后切歌结束。`fn` 表示的是切歌的音量曲线,默认是线性。参数类型是 `TimingFn`,接受一个时间完成度参数,输出一个数字,表示动画完成度。参考库 `mutate-animate`(高级动画)
```js
const { bgm } = Mota.requireAll('var');
const { hyper } = Mota.Package.require('mutate-animate');
// 设置为渐变时间 5000ms音量曲线是双曲正弦函数
bgm.setTransition(5000, hyper('sin', 'in-out'));
```
## 音效系统
### 播放与停止音效
使用 `play` 函数可以播放音效:
```ts
function play(id: string, end?: () => void): number
```
该函数接受两个参数,`id` 参数表示要播放的音效,`end` 参数表示当以 `id` 为名称的任意一个音效播放完毕后执行的函数。函数的返回值是本次音效播放的唯一标识符,用于停止音效播放。
```js
const { sound } = Mota.requireAll('var');
const num = sound.play('mySound.mp3');
```
可以通过 `stop` 函数或者 `stopById` 函数停止音效的播放:
```js
sound.stop(num); // 根据标识符停止音效播放
sound.stopById('mySound.mp3'); // 根据名称停止音效播放
sound.stopAll(); // 停止所有音效的播放
```
### 播放立体声
新样板相比于旧样板的音效系统最大的提升就是允许你播放立体声了,通过一些简单的配置,你可以设置某一种音效的音源位置(暂时还不能让每一次播放都处在不同的位置)。你可以设置听者(也就是玩家)的位置与朝向,然后设置音源的位置与朝向,就能实现立体声的效果。同时,你还可以动态修改音源位置,实现 3D 环绕音等效果。
不过由于所有音源共用一个音频目的地(也就是扬声器),因此设置听者的位置与朝向的话会引起所有音效的相对位置都发生改变,因此一般情况下只修改音源位置即可。
要设置音源位置,首先要获取音效实例,使用 `get` 函数:
```ts
function get(id: string): SoundEffect
```
然后使用音效实例上面的 `setPanner` 函数设置音源位置与听者位置:
```ts
function setPanner(source?: any, listener?: any): void
```
两个参数都是一个对象,分别可以填下列属性,对于除了这些之外的属性,请参考[PannerNode MDN 文档](https://developer.mozilla.org/en-US/docs/Web/API/PannerNode)、[AudioListener MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/AudioListener)、[Web Audio 空间化文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API/Web_audio_spatialization_basics)
- `source`:
- `orientationX`: 音源方向向量 X 分量
- `orientationY`: 音源方向向量 Y 分量
- `orientationZ`: 音源方向向量 Z 分量
- `positionX`: 音源位置坐标 X 分量
- `positionY`: 音源位置坐标 Y 分量
- `positionZ`: 音源位置坐标 Z 分量
- `listener`: (不建议设置)
- `positionX`: 听者的位置坐标 X 分量
- `positionY`: 听者的位置坐标 Y 分量
- `positionZ`: 听者的位置坐标 Z 分量
- `upX`: 听者的头部位置坐标 X 分量
- `upY`: 听者的头部位置坐标 Y 分量
- `upZ`: 听者的头部位置坐标 Z 分量
- `forwardX`: 听者的面朝方向向量 X 分量
- `forwardY`: 听者的面朝方向向量 Y 分量
- `forwardZ`: 听者的面朝方向向量 Z 分量
在右手笛卡尔坐标系(也就是立体声所使用的坐标系)下,我们可以认为水平向右是 X 轴正方向,竖直向上是 Y 轴正方向,垂直屏幕向外是 Z 轴正方向。对于听者的初始位置,直接按照上述坐标描述方式进行设置音源位置即可。于是,我们就可以设置一个音效的位置了:
```js
const { sound } = Mota.requireAll('var');
const se = sound.get('mySound');
// 将音源位置设置为相对于听者右上的位置
se.setPanner({
positionX: 1,
positionY: 1,
positionZ: 0
});
se.playSE(); // 播放音效,等价于 sound.play('mySound');
```
### 自定义音频路由
`SoundEffect` 实例通过将音频源(`AudioBufferSourceNode`)连接到若干个路由根节点上实现对音频的特效处理。例如,对于上面所说的立体声,就是通过自定义音频路由实现的,它的音频路由如下:
```txt
AudioBufferSourceNode(音频源) -> PannerNode(立体声处理模块) ->
GainNode(音量处理模块) -> destination(音频目的地)
```
你也可以自定义自己的音频路由,然后把它放到音效实例的 `baseNode` 属性上。例如,这里我给音效加一个线性卷积节点,然后输出到目的地。音频路由参考[Web Audio API](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API)。
```js
const { AudioPlayer } = Mota.requireAll('class');
const sound = Mota.requireAll('var');
const ac = AudioPlayer.ac;
// 创建线性卷积节点,用于实现混响效果
const convolver = ac.createConvolver();
// 设置混响效果的音频缓冲器,一般是脉冲反应,这里使用了其他的音效替代,最终效果可能会比较差
convolver.buffer = await ac.decodeAudioData(sound.get('mySound2.mp3').data);
// 连接至音频目的地
convolver.connect(ac.destination);
const se = sound.get('mySound.mp3');
// 添加至路由根节点,播放时就会经过这个路由节点的处理
se.baseNode.push(convolver);
// 或者覆盖系统自带的根节点
se.baseNode = [convolver];
```

169
docs/guide/battle.md Normal file
View File

@ -0,0 +1,169 @@
# 战斗系统
新样板对战斗系统进行了完全性重构,大大提高了代码的结构性与执行效率。对于大部分情况,你只需要注重于战斗脚本与光环处理,就能做出任何你想要的怪物特殊属性,或者更改战斗流程,同时不会引起任何副作用。
## 战斗脚本
与旧样板不同的是,新样板中的战斗函数不再接收楼层、坐标等信息,取而代之的是怪物信息。返回值也变成了一个伤害值,不再需要返回一系列属性。
```ts
function calDamageWith(info: EnemyInfo, hero: Partial<HeroStatus>): number | null
```
你可以通过`info`来获取的怪物的真实信息,`info.x`与`info.y`获取怪物坐标,`info.floorId`获取怪物所在楼层,这三者可能是`undefined`,表示怪物不在任何楼层中。除此之外它还包含所有编辑器中可以编辑的怪物属性(如果存在且不为`null`和`undefined`),以及下面几个属性:
- `atkBuff`: buff 攻击加成
- `defBuff`: buff 防御加成
- `hpBuff`: buff 生命加成
- `enemy`: 怪物原始信息,即编辑器中的怪物原始信息,不能修改,只可以读取
- `guard`: 支援信息
对于战斗流程,新样板并没有进行过多改动,与旧样板相差不大。
战斗脚本可以在插件`battle`中修改。
## 光环处理
新样板的光环处理可以说是非常强大了,它通过`provide`和`inject`的方式进行光环处理,这使得光环处理不会产生副作用,同时提供了`preProvideHalo`函数,可以对那些能给其他怪物增加特殊属性甚至光环的光环进行处理。只要你的光环没有副作用(施加顺序不会影响结果,顺序无关性),就可以实现加光环的光环,甚至是加光环的光环的光环。同时,借助于`Range`类,你可以自定义你的光环范围。光环范围扫描只会扫描所有的怪物,因此性能相比于旧样板大有提升。
在处理光环时,会用到下列函数:
```ts
// 光环函数,第一个参数是被施加光环的怪物,第二个参数是施加光环的怪物
type HaloFn = (e: EnemyInfo, enemy: EnemyInfo) => void
// 在 DamageEnemy 类上,即每个怪物
function provideHalo(): void
function preProvideHalo(): void
function injectHalo(halo: HaloFn, enemy: Enemy): void
// 在 EnemyCollection 类上,即每一层的怪物集合
function applyHalo(
type: string,
data: any,
enemy: DamageEnemy,
halo: HaloFn | HaloFn[],
recursion: boolean = false
): void
```
对于光环,其施加流程大致如下:
1. 定义光环范围,使用`Range.registerRangeType`函数,见[范围处理](#范围处理)
2. 在`provideHalo`或`preProvideHalo`函数中添加对应的光环处理
3. 在光环处理中编写光环函数,并使用`applyHalo`施加光环
对于光环函数,它会接收被施加的怪物和施加光环的怪物作为参数,`e`是被施加光环的怪物,`enemy`是施加光环的怪物。在处理时,不应当更改后者的信息,仅应当更改前者的信息。
示例
```js
// 在 provideHalo 中,以下内容应放在样板自带的光环处理函数的循环中
// 施加光环
col.applyHalo(
'square', // 方形范围
{ x: this.x, y: this.y, d: 7 }, // 方形以怪物为中心7为边长
this, // 施加光环的怪,一般就是自己
(e, enemy) => {
e.hp += 100; // 光环效果是被施加的怪物增加100点生命值注意光环也可以作用于自身因此e和enemy有可能相等
}
);
```
## 范围处理
范围处理是一个通用接口,在当前版本样板中主要用于光环处理。它是一个类,因此应该通过`Mota.require('class', 'Range')`获取。它的类型如下:
```ts
class Range<C extends Partial<Loc>> {
collection: RangeCollection<C>;
cache: Record<string, any>;
static rangeType: Record<string, RangeType<Partial<Loc>>>;
constructor(collection: RangeCollection<C>);
/**
* 扫描 collection 中在范围内的物品
* @param type 范围类型
* @param data 范围数据
* @returns 在范围内的物品列表
*/
scan(type: string, data: any): C[];
inRange(type: string, data: any, item: Partial<Loc>): boolean;
clearCache(): void;
static registerRangeType(type: string, scan: RangeScanFn<Partial<Loc>>, inRange: InRangeFn<Partial<Loc>>): void;
}
```
当然,大部分情况下,你不需要理解每个 api 及其类型,在这里,我们重点关注`registerRangeType`函数。
它用于注册一个你自己的范围类型,接收三个参数,分别是:
- `type`: 范围类型名称
- `scan`: 范围扫描函数,用于获取`collection`中所有在范围内的元素
- `inRange`: 范围判断函数,用于判断一个元素是否在范围内
对于范围扫描函数,它接收`collection`和范围参数作为参数,返回一个数组,表示在范围内的元素列表。
对于范围判断函数,它会比范围扫描函数多一个参数,表示要判断的元素。返回布尔值,表示是否在范围内。
这里`collection`描述的是一个列表,表示所有要判断的元素。范围参数指的是该范围类型的参数,例如上面提到过的方形范围,其范围参数就是`{ x: this.x, y: this.y, d: 7 }`。值得注意的是,每个元素不一定包含横纵坐标两个属性。
系统自带两种范围,方形范围与曼哈顿范围(横纵坐标相加小于一个值),我们来看看方形范围是如何注册的:
```js
Range.registerRangeType(
'square',
(col, { x, y, d }) => {
// 获取要判断的列表
const list = col.collection.list;
const r = Math.floor(d / 2);
// 获取在范围内的列表,对每个元素进行判断是否在范围内即可
return list.filter(v => {
return (
has(v.x) && // 这两个用于判断是否存在横纵坐标参数
has(v.y) &&
Math.abs(v.x - x) <= r &&
Math.abs(v.y - y) <= r
);
});
},
(col, { x, y, d }, item) => {
const r = Math.floor(d / 2);
return (
has(item.x) &&
has(item.y) &&
Math.abs(item.x - x) <= r &&
Math.abs(item.y - y) <= r
);
}
);
```
## 地图伤害
与旧样板相比,地图伤害的处理并没有进行过多改动,其核心大致相同。计算地图伤害的流程是:
1. 遍历每个怪物,对于单个怪物,执行下列行为
2. 判断是否存在有地图伤害的属性,有则处理
3. 将地图伤害加入列表中
在计算地图伤害过程中,会用到下列函数:
```ts
// 在 DamageEnemy 上
function calMapDamage(
damage: Record<string, MapDamage> = {},
hero: Partial<HeroStatus> = getHeroStatusOn(Damage.realStatus)
): void
function setMapDamage(
damage: Record<string, MapDamage>,
loc: string,
dam: number,
type?: string
): void
```
前者是计算地图伤害的函数,也就是在插件中被复写的函数,一般不需要调用。它传入`damage`和`hero`作为参数,表示地图伤害要存入的对象,以及勇士信息。
后者是设置地图伤害的函数,当我们讲该怪物在改点的伤害计算完毕后,应该调用它将这一点的伤害信息记录下来。
示例请参考插件中的地图伤害计算。

36
docs/guide/diff.md Normal file
View File

@ -0,0 +1,36 @@
---
lang: zh-CN
---
# 差异说明
本文档暂时只会对新样板新增内容进行说明,其余请查看[旧样板文档](https://h5mota.com/games/template/_docs/#/)。
本指南建立在你已经大致了解 js 的基础语法的基础上。如果还不了解可以尝试对指南内容进行模仿,或者查看[人类塔解析](https://h5mota.com/bbs/thread/?tid=1018&p=1)
## 注意事项
对于新样板,由于拥有了近乎完整的类型标注,因此更推荐使用 `VS Code` 进行代码编写,这样你可以获取到完整的类型标注,而由于类型标注的复杂性,样板编辑器完全无法部署,因此样板编辑器不会有任何新版的类型标注。
由于在之后的更新中,样板 API 会进行大幅度的改动,因此每次更新都可能会弃用一部分 API同时这些 API 会在若干个版本后被彻底删除。因此如果你的代码中使用到了弃用的 API请尽快更换写法以保证可以向后接档。
## 差异内容
新样板中主要对以下内容做了改动:
- [战斗系统](./battle.md)
- [插件系统](./system.md#插件接口与第三方库接口)
- [音频系统](./audio.md)
- [加载流程](./system.md#游戏加载流程)
- [UI 系统](./ui.md)
- [设置系统](./setting.md)
- [存储系统](./storage.md)
## 新增内容
新样板中新增了以下内容:
- [事件监听](./event-emitter.md)
- 一系列新的 UI
- 较为完整的类型标注
- 一系列自定义功能,包括[自定义快捷键](./hotkey.md)、自定义工具栏等

192
docs/guide/event-emitter.md Normal file
View File

@ -0,0 +1,192 @@
# 事件触发系统
事件触发系统是一个可以监测游戏运行时的系统,通过这个系统,你可以在游戏运行的特定时刻执行一些函数,而因此可以大大减少复写的量,代码也会更加清晰。
事件触发系统依赖于两个类 `EventEmitter``IndexedEventEmitter`,这两个类在游戏进程和渲染进程都可以直接使用。本页面将会着重介绍这两个类的使用方法。
样板内置的所有包含事件触发功能的内容都基于 `EventEmitter` 类,而不基于 `IndexedEventEmitter`
## 监听事件
通过 `on` 函数监听事件:
```ts
function on(event: string, fn: (...params: any) => any, config?: any): void
```
其中 `event` 参数表示要监听的事件类型,`fn` 是这个事件被触发时执行的函数,接受任意数量的参数,这取决于触发事件时传入了哪些参数。也可以有返回值,返回值可以被触发函数获取到。
例如,我想监听 UI 控制器的 `end` 事件,就可以这么写:
```js
const { mainUi } = Mota.requireAll('var');
// 监听的 end 事件无参数
mainUi.on('end', () => {
console.log('event emitted!');
});
```
这时,如果 UI 控制器触发了 `end` 事件,那么传入的函数就会被执行,也就会在控制台打印 `event emitted!`。对于有参数的事件,可以这么写:
```js
// 监听 splice 事件,并在控制台打印所有被关闭的 UI 名称
mainUi.on('splice', (items) => {
console.log(items.map(v => v.id));
});
```
对于 `IndexedEventEmitter`,还拥有 `onIndex` 函数,可以为这次监听的函数指定一个 id从而在取消监听时可以通过这个 id 删除,不再需要传入原函数的引用。值得注意的是,样板内置的所有拥有事件触发功能的内容都基于 `EventEmitter`,也就是说它们不能使用 `onIndex` 函数。
```ts
function onIndex(
event: string,
symbol: string | number | symbol,
fn: (...params: any) => any,
config?: any
): void
```
这里的 `symbol` 便是指定的 `id`
```js
eventEmitter.onIndex('end', 'myListener', () => {
console.log('emitted!');
});
```
## 取消监听
可以通过 `off` 函数来取消监听:
```ts
function off(event: string, fn: (...params: any) => any): void
```
示例
```js
const fn = () => console.log('event emitted!');
mainUi.on('end', fn);
// 取消监听,注意要求函数引用相同,内容一样是没用的!
mainUi.off('end', fn);
```
对于 `IndexedEventEmitter`,还可以通过 `offIndex` 来取消监听:
```js
// 传入监听时指定的 id 即可,不需要传入函数
eventEmitter.offIndex('end', 'myListener');
```
除此之外,对于两种事件触发器,都还可以通过 `removeAllListeners` 来取消监听所有事件,或者是一个事件的所有内容
```ts
function removeAllListeners(event?: string): void
```
当不传入 `event` 时,会将所有事件的所有监听器全部取消,这时所有的监听器都将失效。当传入 `event` 时,会将指定事件的所有监听器全部取消。
```js
// 取消监听 mainUi 上的所有内容
mainUi.removeAllListeners();
// 取消监听 mainUi 上的 end 事件
mainUi.removeAllListeners('end');
```
::: warning
不要对样板内置变量执行 `removeAllListeners` 函数,因为样板内置内容很多都基于事件触发。
:::
## 触发事件
可以通过 `emit` 函数触发事件:
```ts
function emit(event: string, ...params: any): any[]
```
这个函数的 `event` 参数表示的是要触发哪个事件,后面的参数表示的是要传递给监听器的参数。其返回值是每个监听器的返回值组成的数组,顺序不确定。除此之外,如果设置了[自定义触发器](#自定义触发器),那么返回值也会跟随自定义触发器而改变。
例如,如果我想触发 `mainUi``end` 事件:
```js
mainUi.emit('end'); // 触发事件,无参数
mainUi.emit('end', 123, '123'); // 触发事件,有两个参数传入监听器,监听器可以通过参数获取
```
## 监听配置
对于 `on` 函数以及 `onIndex` 函数的最后一个参数,表示的是这个监听器的配置选项。在目前,它包含两个选项:
- `once`: 表示这个监听器只会触发一次,触发一次后即自动删除
- `immediate`: 当这个监听器被添加的时候,会立刻被触发一次,不传入任何参数
对于 `once` 配置项,还可以通过 `once` 函数来设置:
```ts
function once(event: string, fn: (...params: any) => any): void
function onceIndex(event: string, symbol: string | number | symbol, fn: (...params: any) => any): void
```
这样,我们就可以通过 `once` 函数来添加一个只会触发一次的监听器了:
```js
mainUi.once('end', () => {
console.log('这个事件只会触发一次');
});
```
## 自定义触发器
你还可以通过 `setEmitter` 函数来自定义你的事件触发器:
```ts
function setEmitter(event: string, fn?: (event: any[], ...params: any) => any): void
```
其中 `event` 参数表示的是要设置的时间,`fn` 参数表示的是触发器函数,它接收事件信息作为第一个参数,之后的参数是事件触发时传入的参数。
示例
```js
eventEmitter.setEmitter('myEmitter', (event, ...params) => {
const returns = [];
for (const ev of event) {
// 修改触发器的返回值,在后面加上'-myEmitter'字符串
returns.push(ev.fn(...params) + '-myEmitter');
}
return returns;
})
```
## 样板中基于事件触发器的接口
- `AudioPlayer`
- `Disposable`
- `IndexedEventEmitter`
- `ShaderEffect`
- `ResourceController`
- `MotaSetting`
- `SettingDisplayer`
- `Hotkey`
- `Keyboard`
- `CustomToolbar`
- `Focus`
- `GameUi`
变量
- `mainUi`
- `fixedUi`
- `bgm`
- `sound`
- `gameKey`
- `mainSetting`
- `hook`
- `gameListener`
- `loading`

238
docs/guide/hotkey.md Normal file
View File

@ -0,0 +1,238 @@
# 按键系统
新样板提供了一个新的按键系统,允许你注册自己的按键,同时可以让玩家修改按键映射。
## 注册按键
一般情况下,我们建议直接将按键注册在游戏主按键变量 `gameKey` 上。注册按键使用 `register` 函数:
```ts
function register(data: RegisterHotkeyData): this
```
参数表明的是注册信息,一般我们会填写下列信息:
- `id`: 按键 id
- `name`: 按键的显示名称
- `defaults`: 默认按键
对于按键,我们使用变量 `KeyCode` 来获取,它来自库 `monaco-editor`,也就是 VSCode 使用的编辑器。
```js
const { KeyCode, gameKey } = Mota.requireAll('var');
gameKey
.register({
id: 'myKey1', // 按键 id
name: '按键1', // 按键显示名称
defaults: KeyCode.KeyL // 默认 L 键触发
})
.register({
id: 'myKey2',
name: '按键2',
defaults: KeyCode.Enter // 默认回车键触发
})
```
## 同 id 按键
很多时候我们需要让多个按键触发同一个功能,例如样板里面的 `A``5`,都是读取自动存档按键,这时,我们可以给按键的 id 加上一个以下划线开头,后面紧跟数字的后缀,即可实现多个按键触发同一个功能:
```js
gameKey
.register({
id: 'myKey3_1',
name: '按键_1', // 名称没有后缀也可以
defaults: KeyCode.Digit1
})
.register({
id: 'myKey3_2', // 后缀必须是全数字,不能包含其他字符
name: '按键_2',
defaults: KeyCode.KeyA
})
```
## 实现按键功能
### 分配作用域
一般情况下,我们不希望一个按键在任何时刻都有作用。例如,当我们打开怪物手册时,必然不想让读取自动存档的按键起作用。这时,如果我们要实现按键功能,首先要为它分配作用域:
```ts
function use(symbol: symbol): this
```
其中的 `symbol` 表示的即是作用域的标识符,是一个 `symbol` 类型的变量。
```js
const myScope = Symbol(); // 创建 symbol
gameKey.use(myScope); // 使用 myScope 作为作用域
```
### 实现功能
接下来,我们可以使用 `realize` 函数来实现按键的功能了:
```ts
function realize(id: string, func: HotkeyFunc): this
```
其中 `id` 表示的是要实现的按键的 id对于同 id 按键,不填数字后缀表示实现所有按键,填写数字后缀表示只实现那一个按键。第二个参数 `func` 表示的便是按键被触发时执行的函数了。
```js
gameKey
.use(myScope) // 实现按键前要先分配作用域,除非你的按键是类似于怪物手册按键一样,在没有任何 UI 打开时触发
.realize('myKey1', () => {
// 按键被触发时执行这个函数,在控制台打印内容
console.log('myKey1 emitted!');
})
// 触发函数还可以接受三个参数
.realize('myKey2', (id, code, ev) => {
// id: 包含数字后缀的按键id可以依此来区分不同后缀的按键
// code: 按键触发的 KeyCode例如可能是 KeyCode.Enter
// ev: 按键触发时的 KeyboardEvent
console.log(id, code, ev);
})
.realize('myKey3', (id) => {
// 对于同 id 按键,实现功能时不需要填写后缀
console.log(id); // 输出 id包含数字后缀
})
```
### 释放作用域
在大部分情况下,按键都是用于 UI 的,每次打开 UI 的时候,我们为其分配一个新作用域,在关闭 UI 时,就必须把作用域释放,使用 `dispose` 函数:
```js
// 打开 UI 时
gameKey
.use(myScope)
// ... 实现代码
// 关闭 UI 时,也可以填写参数,表示将这个作用域之后的所有作用域都释放
gameKey.dispose();
```
::: tip
如果想要在任何 UI 都没有打开时实现按键,例如像打开怪物手册,或者是打开自己的 UI直接在插件中经由渲染进程包裹注册及实现按键即可。
:::
## 按键分组
如果你打开样板的自定义按键的界面,会发现它会把按键分为 `ui界面` `功能按键` 等多个组别,这个功能是由按键分组实现的:
```ts
function group(id: string, name: string): this
```
这个函数调用后,在其之后注册的按键会被分类至 `id` 组,`name` 参数表示这个组的显示名称。
```js
gameKey
.group('myGroup1', '分组1')
// 这时注册的按键会被分类至 myGroup1 组
.register({
id: 'myKey4',
name: '按键4',
defaults: KeyCode.KeyA
})
.group('myGroup2', '分组2')
// z这时注册的按键会被分类至 myGroup2 组
.register({
id: 'myKey5',
name: '按键5',
defaults: KeyCode.KeyB
});
```
## 按键控制
你可以通过 `when` `enable` `disable` 三个函数来控制按键什么时候有效:
```js
gameKey
.use(myScope)
.when(() => Math.random() > 0.5) // 在当前作用域下,满足条件时按键才有效
.disable() // 全面禁止按键操作,不单单是当前作用域
.enable(); // 全面启用按键操作
```
## 样板内置按键
下面是样板内置的按键及分组,你可以通过 `realize` 函数覆盖其功能
- `ui`ui 界面)
- `book`: 怪物手册
- `save`: 存档界面
- `load`: 读档界面
- `toolbox`: 道具栏
- `equipbox`: 装备栏
- `fly`: 楼层传送
- `menu`: 菜单
- `replay`: 录像回放
- `shop`: 全局商店
- `statictics`: 统计信息
- `viewMap_1` / `viewMap_2`: 浏览地图
- `function` 组(功能按键)
- `undo_1` / `undo_2`: 回退(读取自动存档)
- `redo_1` / `redo_2`: 恢复(撤销读取自动存档)
- `turn`: 勇士转向
- `getNext_1` / `getNext_2`: 轻按
- `num1`: 破墙镐
- `num2`: 炸弹
- `num3`: 飞行器
- `num4`: 其他道具
- `mark`: 标记怪物
- `special`: 鼠标位置怪物属性
- `critical`: 鼠标位置怪物临界
- `quickEquip_1` / `quickEquip_2` / ... / `quickEquip_9` / `quickEquip_0`: 快捷换装(暂未实现)
- `system` 组(系统按键)
- `restart`: 回到开始界面
- `comment`: 评论区
- `general` 组(通用按键)
- `exit_1` / `exit_2`: 退出 ui 界面
- `confirm_1` / `confirm_2` / `confirm_3`: 确认
- `@ui_book` 组(怪物手册)
- `@book_up`: 上移光标
- `@book_down`: 下移光标
- `@book_pageDown_1` / `@book_pageDown_2`: 下移 5 个怪物
- `@book_pageUp_1` / `@book_pageUp_2`: 上移 5 个怪物
- `@ui_toolbox` 组(道具栏)
- `@toolbox_right`: 光标右移
- `@toolbox_left`: 光标左移
- `@toolbox_up`: 光标上移
- `@toolbox_down`: 光标下移
- `@ui_shop` 组(商店)
- `@shop_up`: 上移光标
- `@shop_down`: 下移光标
- `@shop_add`: 增加购买量
- `@shop_min`: 减少购买量
- `@ui_fly` 组(楼层传送)
- `@fly_left`: 左移地图
- `@fly_right`: 右移地图
- `@fly_up`: 上移地图
- `@fly_down`: 下移地图
- `@fly_last`: 上一张地图
- `@fly_next`: 下一张地图
- `@ui_fly_tradition` 组(楼层传送-传统按键)
- `@fly_down_t`: 上一张地图
- `@fly_up_t`: 下一张地图
- `@fly_left_t_1` / `@fly_left_t_2`: 前 10 张地图
- `@fly_right_t_1` / `@fly_right_t_2`: 后 10 张地图
## 默认辅助按键
你可以在注册的时候为按键添加辅助按键 `ctrl` `alt` `shift`:
```js
// 注册一个要按下 Ctrl + Shift + Alt + X 才能触发的按键!
gameKey.register({
id: 'myKey',
name: '按键',
defaults: KeyCode.KeyX,
ctrl: true,
shift: true,
alt: true
});
```

358
docs/guide/setting.md Normal file
View File

@ -0,0 +1,358 @@
# 设置系统
新样板创建了一种新的设置系统,它允许你自定义设置列表,以及设置的编辑组件,它是 `MotaSetting` 类。
## 注册设置
想要添加你自己的设置,就需要注册一个设置。对于新样板的设置,全部在 `mainSetting` 变量里面,然后调用 `register` 函数即可注册:
```ts
function register(
key: string,
name: string,
value: number | boolean | MotaSetting,
com?: SettingComponent,
step?: [min: number, max: number, step: number]
): this
```
参数说明:
- `key`: 要注册的设置的 id不能包含英文句号 `.`
- `name`: 设置的显示名称
- `value`: 设置的初始值
- `com`: 设置的编辑组件,即打开 UI 设置界面后用户修改设置的值的组件
- `step`: 数字型设置的步长信息,是一个数组,第一个元素表示最小值,第二个元素表示最大值,第三个元素表示设置步长
对于 `value` 参数,如果填入了一个新的 `MotaSetting` 实例,那么会被认为是级联设置,即子设置。例如:
```js
const { mainSetting } = Mota.requireAll('var');
const { MotaSetting } = Mota.requireAll('class');
const { createSettingComponent } = Mota.require('module', 'CustomComponents');
// 获取系统自带的设置编辑组件
const COM = createSettingComponent();
mainSetting
.register('mySetting1', '设置1', false, COM.Boolean) // 添加一个布尔型设置
.register('mySetting2', '设置2', 100, COM.Number, [0, 1000, 50]) // 添加一个数字型设置
.register(
'mySetting3',
'级联设置1',
new MotaSetting()
.register('mySetting4', '设置4', true, COM.Boolean) // 在级联设置中添加一个布尔值设置
.register(
'mySetting5',
'级联设置2',
new MotaSetting() // 在级联设置中再加一个级联设置
.register('mySetting6', '设置6', 6, COM.Number, [0, 28, 1])
)
);
```
## 获取设置
使用 `getSetting``getValue` 函数可以通过类似于获取对象的值的方式获取设置及设置的值:
```ts
function getSetting(key: string): Readonly<any>
function getValue(key: string, defaultValue?: any): number | boolean
```
例如,对于上面我们注册的设置,可以通过这些方式获取:
```js
// 获取设置1
const setting1 = mainSetting.getSetting('mySetting1'); // 还可以读取这个设置的值,但是不建议修改
// 获取级联设置1
const setting3 = mainSetting.getSetting('mySetting3');
// 还可以继续对这个设置进行注册
setting3.value.register('mySetting7', '设置7', false, COM.Boolean);
// 获取级联设置中的设置,使用类似获取对象的值的方式进行获取,注意不能使用[]获取,只能使用点
const setting4 = mainSetting.getSetting('mySetting3.mySetting4');
// 获取二层级联设置中的设置
const setting5 = mainSetting.getSetting('mySetting3.mySetting5.mySetting6');
```
```js
// 获取设置1的值
const value1 = mainSetting.getValue('mySetting1');
// 获取设置2的值同时标有默认值即假如这个设置不存在那么就会返回默认值
const value2 = mainSetting.getValue('mySetting2', 100);
// 获取级联设置中的值
const value4 = mainSetting.getValue('mySetting3.mySetting4'. true);
// 注意如果获取到的是另一个设置实例MotaSetting那么会返回 undefined或者是默认值
const value3 = mainSetting.getValue('mySetting3', true); // 最后会返回 true
```
::: tip
`getSetting` 函数的第一个参数是从当前级开始获取设置,如果从级联设置中获取其子设置,直接填写子设置的名称即可,不需要填写完整的名称,例如:
```js
const setting3 = mainSetting.getSetting('mySetting3').value;
const setting4 = setting3.getSetting('mySetting4');
```
:::
## 修改设置
一般我们不提倡在无关场景修改设置的值,因为这可能会违背玩家的意愿,但样板还是提供了修改设置的值的 api
```ts
function setValue(key: string, value: number | boolean): void
function addValue(key: string, value: number): void
```
前者用于直接设置一个设置的值,后者用于增加或减少一个数字型设置的值
## 初始化设置
对于设置,我们不仅可以注册与游戏逻辑(录像)无关的,还可以注册与之有关的。对于无关的,可以在游戏加载时进行初始化,对于后者,可以在每次读档后进行初始化。但是值得注意的是,设置是一个渲染进程类,你不能直接用于游戏进程,如果想做出与录像有关的设置,需要 `flags`,以及对应的录像处理。
我们可以通过 `reset` 函数对设置进行初始化:
```ts
function reset(setting: Record<string, number | boolean>): void
```
例如,对于上面我们注册的设置,可以这么初始化:
```js
mainSetting.reset({
'mySetting1': false,
'mySetting2': 100,
'mySetting3.mySetting4': true,
'mySetting3.mySetting5.mySetting6': 6
});
```
初始化设置一般会与游戏存储系统共同使用,参考[存储系统](./storage.md#与设置系统共用)
:::tip
建议新增一个插件,并在插件的顶层中(即插件最外层函数中)经过渲染进程包裹进行初始化
:::
## 设置说明
可以通过 `setDescription`设置一个设置的说明文字:
```ts
function setDescription(key: string, desc: string): this
```
```js
mainSetting
.setDescription('mySetting1', `这是我注册的第一个设置!`) // 直接传入字符串即可
.setDescription('mySetting2', `可以通过<br />换行`) // 换行符是html的 <br> 标签,<br> <br />都可以换行
.setDescription('mySetting3.mySetting4', `
使用\n换行不会有效果。还可以使用html语法<span style="color: red">设置样式</span>
`) // 说明内容完全兼容 html 语法
```
## 样板内置设置
样板目前共有以下这些内置设置:
- `screen`:
- `screen.fullscreen`: 是否全屏
- `screen.itemDetail`: 血瓶宝石显伤
- `screen.transition`: 是否启用 UI 开启与关闭动画
- `screen.antiAlias`: 抗锯齿
- `screen.fontSize`: 字体大小
- `screen.smoothView`: 大地图平滑移动镜头
- `screen.criticalGem`: 临界是否显示为宝石数
- `screen.keyScale`: 虚拟键盘缩放
- `action`:
- `action.fixed`: 是否开启定点查看
- `action.hotkey`: 快捷键
- `action.toolbar`: 自定义工具栏
- `audio`:
- `audio.bgmEnabled`: 是否开启背景音乐
- `audio.bgmVolume`: 背景音乐音量
- `audio.soundEnabled`: 是否开启音效
- `audio.soundVolume`: 音效音量
- `utils`:
- `utils.autoScale`: 自动放缩
- `fx`:
- `fx.frag`: 打怪特效
- `ui`:
- `ui.mapScale`: 小地图楼传缩放
如果你想要在样板内置设置中新增,需要先通过 `mainSetting.getSetting` 获取到对应的级联配置,然后注册。
## 设置显示函数
如果你打开过样板的系统设置,并进入显示设置中看过,会发现临界显示一项的值会显示为 `宝石数` 或者 `攻击`,这个功能便是由设置显示函数完成的:
```ts
function setDisplayFunc(key: string, func: (value: number | boolean) => string): this
```
这个函数可以设置在显示的时候,显示什么内容,它的第二个参数即是显示函数,接受当前设置的值,输出一个字符串。例如,对于上面注册的设置,可以这么设置显示函数:
```js
mainSetting
.setDisplayFunc('mySetting1', value => value ? '宝石数' : '攻击')
.setDisplayFunc('mySetting2', value => `${value}%`)
```
## 创建你自己的编辑组件
设置的编辑组件实际上只是一种特殊的组件,它接收下面三个参数(`props`
- `item`: 这个设置的信息,一般只会用到 `value`,也就是这个设置的值,其他信息请参考[API 列表](../api/class.md)
- `setting`: 根级设置实例,例如对于上面注册的设置,就是 `mainSetting`,而不是其任意一级的级联设置
- `displayer`: 设置渲染控制器,一般情况下,当设置完值后,调用 `displayer.update` 即可
借助这三个参数,我们可以做出自己的编辑组件了。组件可以直接就是一个函数,函数接受 `props` 作为参数,返回 `VNode`,也可以是一个导出组件。考虑到函数式组件的方便性,我们更推荐使用函数式组件。如果使用导出组件,请注意要定义参数。
```js
const { MComponent } = Mota.require('class');
const { text, h } = Mota.require('module', 'MCGenerator');
// 这里以布尔值的编辑组件为例
function MySettingComponent(props) {
const { setting, displayer, item } = props;
const changeValue = () => {
// 可以在这里写出你自己对设置的处理
// 设置值,基本上是模式化的写法
setting.setValue(displayer.selectStack.join('.'), !item.value);
displayer.update();
};
// 返回 VNode因此要经过 vNode 包裹,使用 vNodeS 也可以
return MComponent.vNode([
// 渲染一个按钮上去,使用 html 的 button 标签
h('button', [text('修改设置')], {
props: {
// 当点击这个按钮时,将设置取反并设置
onClick: () => changeValue()
}
})
]);
}
// 之后便可以在你注册的设置里面使用这个组件了,第四个参数传入组件即可
mainSetting.register('mySetting', '设置', true, MySettingComponent);
```
## 样板内置编辑组件
以下的 `COM` 指的是函数 `createSettingComponent` 的返回值,即[注册设置](#注册设置)中获取的系统自带设置编辑组件
- `COM.Default`: 默认编辑组件,不会显示任何内容
- `COM.Boolean`: 布尔值组件,拥有一个按钮来修改设置
- `COM.Number`: 数字型设置组件,拥有一个输入框和两个按钮,输入框内也包含递增和递减按钮
- `COM.Radio(items: string[])`:
单选列表,在使用时调用并传入单选列表的名称即可,例如:
```js
mainSetting.register('mySetting', '设置', COM.Radio(['第一个选项', '第二个选项']));
```
注意这个组件是为数字型设置所用的,选择 `第一个选项` 时,这个设置的值就是 0以此类推。
- `COM.HotkeySetting`: 专为快捷键所用的设置组件
- `COM.ToolbarEditor`: 专为自定义工具栏所用的组件
## 监听设置的修改事件
如果我们想要在一个设置被修改时执行一些代码,可以监听设置的修改事件,也就是 `valueChange` 事件:
```js
mainSetting.on('valueChange', (key, newValue, oldValue) => {
// 这里的 key 表示被修改的设置的名称newValue 表示的是设置为的值oldValue 表示的是之前的值
// 可以使用一个 switch 语句进行判断
switch (key) {
// 当设置名称为 mySetting1 时
case 'mySetting1': {
// 做一些处理,例如可以在控制台打印新值
console.log(newValue);
break;
}
case 'mySetting2.mySetting3': {
// 对于级联设置也是如此
break;
}
}
})
```
## 创建自己的设置
到目前为止,我们都是在样板默认的设置 `mainSetting` 上进行的。但这个设置系统能力不止如此,你还可以创建自己的设置,并通过设置 UI 显示出来。我们可以直接创建一个设置实例:
```js
const { MotaSetting } = Mota.requireAll('class');
// 创建你自己的设置
const mySetting = new MotaSetting()
.register('mySetting1', '设置1', true, COM.Boolean)
.register('mySetting2', '设置2', 10, COM.Number, [0, 100, 10])
```
然后,我们可以直接将这个设置作为参数传递给设置 UI让它来渲染
```js
const { mainUi } = Mota.requireAll('var');
// 这样,你就可以将你自己的设置显示出来了!
mainUi.open('settings', { info: mySetting });
```
## 创建自己的设置 UI
除此之外,样板还提供了能够用于创建自己的设置 UI 的 API它是类 `SettingDisplayer`,你可以为你自己的 UI 创建一个其实例,从而辅助你去创建自己的显示 UI。
创建实例时,要求传入设置作为参数。你可以通过 `add` 来选择一个设置,通过 `cut` 来截断一个设置,最终呈现出来的选择栈会存储在属性 `selectStack` 中,要渲染的信息会存储在属性 `displayInfo` 中。当一个设置被更改时,可以通过 `update` 函数来刷新显示。这样,我们就可以写出自己的设置 UI 了。对于这些内容的详细说明,请参考[API 列表](../api/class.md)。下面是一个极度简易的设置 UI 示例,其中省略了非常多的部分。
```js
const { MotaSetting, SettingDisplayer, MComponent } = Mota.requireAll('class');
const { m } = Mota.requireAll('fn');
const { div, vfor, span, text, com, f } = Mota.require('module', 'MCGenerator');
const mySettingUI = m()
.defineProps({
info: MotaSetting
})
.setup((props, ctx) => {
const displayer = new SettingDisplayer(props.info);
const selected = computed(() => displayer.displayInfo.at(-1)?.item)
// ... 选择设置等内容省略
return () => {
return MComponent.vNode([
// 渲染每一级的级联设置
vfor(displayer.displayInfo, (value) => {
return MComponent.vNodes(div([
// 每一级的级联设置中,渲染这个级联设置的内容
vfor(Object.entries(value.list), ([key, item]) => {
return MComponent.vNodeS(div(text(item.name)));
})
]));
}),
// 渲染设置的信息
div(selected.value ? [
// 设置说明
span(text(display.at(-1)?.text)),
// 设置编辑组件,使用动态组件
com(selected.value.controller, {
// 传入所需的三个参数
props: {
item: f(selected.value),
displayer: f(displayer),
setting: f(props.info)
}
})
] : [])
]);
}
})
.export();
```

96
docs/guide/storage.md Normal file
View File

@ -0,0 +1,96 @@
# 存储系统
新样板新增了一种新的存储系统,它允许你在不同的塔之间共用存储,常用于设置系统。但是你依然可以使用旧样板的存储方式,使用 `setLocalStorage` 等函数,这些函数目前还不会被弃用。
## GameStorage 类
新的存储系统依赖于 `GameStorage`,它通过一些简单的 api 来进行存储。在存储之前,我们需要创建一个存储实例:
```ts
interface GameStorage {
new(key: string): GameStorage
}
```
其中的 `key` 参数表示的是这个存储的名称,我们建议使用 `GameStorage.fromGame``GameStorage.fromAuthor` 函数生成:
```js
const { GameStorage } = Mota.requireAll('class');
// fromAuthor 函数指明了同一个作者,只要是同一个作者,而且存储名称相同,那么就会在不同塔之间共通
// fromAuthor 第一个参数无用,随便填就行,第二个参数是存储名称
// 作者名称需要在全塔属性中填写
const myStorage1 = new GameStorage(GameStorage.fromAuthor('', 'myStorage'));
// fromGame 函数指明了一个游戏,表明该存储只在当前塔有效,对于其他塔是无效的,参数表示存储名称
// 游戏名称指的是全塔属性中的游戏英文名
const myStorage2 = new GameStorage(GamrStorage.fromGame('myStorage'));
```
## 存入与读取
可以使用 `setValue``getValue` 存入和读取一个存储:
```ts
function setValue(key: string, value: any): void
function getValue(key: string, defaultValue?: any): any
```
其中 `key` 参数表明的是要设置的存储的名称(注意与 `GameStorage` 的存储名称区分)
```js
myStorage1.setValue('myStore', 1); // 在 myStorage1 这个存储中,将 myStore 存储设置为1
myStorage1.setValue('myStore2', [1, 2, 3]); // 也可以设置为一个数组
const value = myStorage1.getValue('myStore2', []); // 获取存储,第二个参数表示默认值
```
对于每个存储值,必须是[可序列化对象](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#%E6%94%AF%E6%8C%81%E7%9A%84%E7%B1%BB%E5%9E%8B)
## 实际写入与读取
实际上,在执行完 `setValue``getValue` 时,存入的值并不会立刻存入本地存储,它会在内存中暂时存储。你可以调用 `write``read` 来写入本地存储或者从本地存储读取
```js
myStorage1.write(); // 实际写入本地存储
myStorage1.read(); // 从本地存储读取,会完全覆盖之前的信息
```
当然,大部分情况下你是不需要执行这些的,样板会在一些时刻自动写入所有本地存储,例如当页面被关闭或者失焦时,会立刻写入本地存储,所以一般情况下是不需要你手动写入的。当一个存储实例被创建后,它会立刻从本地存储中读取信息,因此大部分情况你也不需要手动读取本地存储。
## 遍历存储
可以通过 `keys` `values` `entries` 三个函数遍历这个存储实例的 键、值、键和值:
```js
for (const key of myStorage1.keys()) {
console.log(key); // 输出每一个存储键
}
for (const value of myStorage1.values()) {
console.log(value); // 输出每一个值
}
for (const [key, value] of myStorage.entries()) {
console.log(key, value); // 输出每一个键和值
}
```
## 与设置系统共用
对于直接注册到 `mainSetting` 上的设置,会自动写入本地存储。如果由于一些原因,你不想让它写入本地存储,比如通过 `flags` 存储的设置,或者是全屏这种不能在打开页面时直接呈现的设置,可以通过给 `MotaSetting` 类上的静态变量 `noStorage` 数组添加设置名称即可:
```js
const { MotaSetting } = Mota.requireAll('class');
// 这样这两个设置就不会自动写入本地存储了
MotaSetting.noStorage.push('mySetting1', 'mySetting2.mySetting3');
```
在初始化时,我们可以直接调用存储进行初始化。样板系统设置使用 `settingStorage` 变量进行存储。
```js
const { settingStorage: storage, mainSetting } = Mota.requireAll('var');
mainSetting.reset({
'mySetting1': !!storage.getValue('mySetting1', false), // 使用 !! 确保值是布尔值
'mySetting2': storage.getValue('mySetting2', 100), // 存储名称与设置名称一致
'mySetting3.mySetting4': storage.getValue('mySetting3.mySetting4') // 级联设置中也是如此
})
```

224
docs/guide/system.md Normal file
View File

@ -0,0 +1,224 @@
---
lang: zh-CN
---
# 系统说明
相比于旧样板中`core`包揽一切,新样板使用了`Mota`作为系统主接口,通过模块化管理,让 API 结构更加清晰。
## Mota
`Mota`作为新样板的主接口,一共包含三个功能函数,三个接口函数,以及两个接口对象。对于`Mota`对象的具体内容,请参考 [API 列表](../api/index.md)。
## 获取样板接口
你可以通过以下两个函数获取样板的接口。
```ts
function require(type: 'var' | 'fn' | 'class' | 'module', key: string): any;
function requireAll(type: 'var' | 'fn' | 'class' | 'module'): any;
```
在这里,`type`描述的是要获取的接口类型,可以填写下面这四个内容:
- `var`: 获取变量
- `fn`: 获取函数
- `class`: 获取类
- `module`: 获取模块
对于`Mota.require`函数,还需要接受第二个参数,表示的是要获取的接口名称。
::: warning
对于不存在的接口,获取之后会直接报错!
:::
示例
```js
// ----- Mota.require
const getHeroStatusOn = Mota.require('fn', 'getHeroStatusOn'); // 获取勇士真实属性的接口
const KeyCode = Mota.require('var', 'KeyCode'); // 获取KeyCode接口
const DamageEnemy = Mota.require('class', 'DamageEnemy'); // 获取怪物接口
const Damage = Mota.require('module', 'Damage');
// 对于模块,每个模块都是对象,因此还可以使用对象解构语法
const { calDamageWith, getSingleEnemy } = Mota.require('module', 'Damage');
// ----- Mota.requireAll
// 它会获取到一种类型的所有接口,因此搭配对象解构语法最为合适
const { hook, KeyCode, loading, mainSetting } = Mota.requireAll('var');
// 它等价于
const hook = Mota.require('var', 'hook');
const KeyCode = Mota.require('var', 'KeyCode');
const loading = Mota.require('var', 'loading');
const mainSetting = Mota.require('vaar', 'mainSetting');
```
## 插件接口与第三方库接口
与获取样板接口类似,插件和第三方库也有对应的接口,它们分别是`Mota.Plugin`和`Mota.Package`。
### Mota.Plugin
它用于获取与注册插件。对于获取,依然是`require`与`requireAll`两个函数:
```ts
function require(plugin: string): any;
function requireAll(): any;
```
与系统接口不同的是,插件不再拥有`type`参数,直接填入插件名称或者获取全部即可。
示例
```js
// 注意,这里的 _r 后缀表示该插件定义于渲染进程_g 后缀表示定义于游戏进程
// 对于渲染进程与游戏进程的信息请查看本页的`进程分离`栏
const shadow = Mota.Plugin.require('shadow_r'); // 获取点光源插件
const remainEnemy = Mota.Plugin.require('remainEnemy_g'); // 获取漏怪检测插件
// 同样你也可以使用requireAll
const { shadow_r: shadow, remainEnemy_g: remainEnemy } = Mota.Plugin.requireAll();
```
::: warning
与系统接口同样,获取不存在的插件时会报错
:::
### Mota.Package
它与插件接口的用法完全相同,这里不再赘述。
## 注册插件
你可以使用`register`函数来注册一个插件
```ts
function register<K extends string, D>(plugin: K, data: D, init?: (plugin: K, data: D) => void): void
function register<K extends string>(plugin: K, init: (plugin: K) => any): void
```
这个函数共有上述两种用法,对于第一种用法,第一个参数传入插件名称,第二个参数传入插件暴露出的内容,例如可以是一系列函数、类、变量等。第三参数可选,是插件的初始化函数,初始化函数会在游戏加载中初始化完毕。初始化函数共有两个参数,接受`plugin`插件名,和`data`插件内容。
对于第二种用法,第一个参数传入插件名称,第二个参数传入插件的初始化函数,同时要求必须有返回值,返回值作为插件的内容。初始化函数接受`plugin`插件名作为参数。
如果你使用了第二种用法,同时没有返回值,那么如果通过`require`获取插件,会获取到`undefined`。
插件加载流程见[游戏加载流程](./system.md#游戏加载流程)
示例
::: code-group
```js [第一种用法]
let cnt = 0;
function init() {
core.registerAnimationFrame('example', true, () => {
cnt++;
})
}
function getCnt() {
return cnt;
}
// 注册插件,内容使用对象内容简写语法,将 init 和 getCnt 函数作为插件内容,
// init 作为初始化函数
Mota.Plugin.register('example', { init, getCnt }, init);
```
```js [第二种用法]
function init() {
let cnt = 0;
// 直接在这里初始化即可
core.registerAnimationFrame('example', true, () => {
cnt++;
})
function getCnt() {
return cnt;
}
return { getCnt };
}
// 注册插件
Mota.Plugin.register('example', init);
```
:::
## 函数复写
新样板还提供了函数复写的接口,它是`Mota.rewrite`
```ts
// 类型已经经过了大幅简化
function rewrite(
base: any,
key: string,
type: 'full' | 'add' | 'front',
re: Function,
bind?: any,
rebind?: any
): Function
```
这个函数共有六个参数,后两个参数可选。
- `base`: 函数所在对象
- `key`: 函数在对象中的名称
- `type`: 复写类型,`full`表示全量复写,`add`表示在原函数之后新增内容,`front`表示在原函数之前新增内容
- `re`: 复写函数,在`add`模式下,第一个参数会变成原函数的返回值,同时原参数会向后平移一位
- `bind`: 原函数调用对象,即原函数的`this`指向,默认是 `base`
- `rebind`: 复写函数的调用对象,即复写函数中的`this`指向,默认是`base`
::: warning
全量复写会覆盖之前的`add`与`front`模式复写!!!
:::
样板插件中已经自带了很多复写案例,可以查看插件来获取具体用法。
## 进程分离
新样板中对进程进行了分离,分为了渲染进程与游戏进程。
对于渲染进程,任何不会直接或间接影响到录像验证的内容都会放在渲染进程。在录像验证与编辑器中,渲染进程都不会执行,因此也无法获取渲染进程的任何插件与系统接口,同样所有的第三方库也无法获取。
对于游戏进程,不论在什么情况下都会执行,因此任何会影响录像的内容都应该放在游戏进程。对于构建版样板(即群文件版),你所编写的代码一般都是直接在游戏进程下执行的。因此,为了能够在游戏进程下能够执行渲染进程的内容,新样板提供了下面两个接口:
```ts
function r(fn: Function): void
function rf(fn: Function): Function
```
对于`r`函数,它是将传入的函数在渲染进程下执行,对于`rf`函数,它是将传入的函数包裹为渲染进程函数并输出,之后直接调用即可。
示例
```js
Mota.r(() => {
const mainSetting = Mota.require('var', 'mainSetting');
mainSetting.setValue('screen.fontSize', 20);
});
// 它等价于
const f = Mota.rf(() => {
const mainSetting = Mota.require('var', 'mainSetting');
mainSetting.setValue('screen.fontSize', 20);
});
f();
```
## 游戏加载流程
新样板对游戏加载流程进行了部分改动,现在它的加载流程如下:
1. 加载`project/data.js`
2. 加载游戏进程,同时注册游戏进程插件
3. 加载渲染进程,同时注册渲染进程插件与第三方库
4. 加载`plugins.js`,执行其包含的每一个函数
5. 初始化`core`,开始加载游戏资源
6. 初始化插件
7. 等待游戏资源加载完毕
因此,如果直接在`plugins.js`的函数中直接调用`core`会报错。同时插件在初始化之前获取也会报错,即使没有初始化函数,因此`plugins.js`顶层函数中只能获取系统接口与第三方库,不能获取插件。

197
docs/guide/ui-control.md Normal file
View File

@ -0,0 +1,197 @@
# UI 系统
相比于[上一页](./ui.md),本页主要注重于 UI 控制系统的说明,通过了解控制系统,你可以让你的 UI 以你想要的方式展现出来。
## mainUi 与 fixedUi
样板一个提供了两种类型的 UI 控制器,分别是 `mainUi` `fixedUi`,它们两个都能控制 UI 的显示,但是显示方式有所不同。
对于 `mainUi`,它主要注重于主要 UI 的显示,使用它所显示的 UI 会有明显的嵌套关系。例如,从怪物手册中打开怪物详细信息界面,从设置中打开快捷键设置界面等,这些都是由父 UI 又打开了一个子 UI有嵌套关系。而同时如果关闭了一个 UI那么它的所有子级 UI 也会一并被关闭。
而对于 `fixedUi`,它所显示的 UI 不再拥有嵌套关系,所有 UI 的关系都是平等的,都会一并显示,关闭 UI 时也只会关闭当前 UI不会关闭其他 UI。例如像状态栏、自定义工具栏、标记怪物等 UI 就是基于 `fixedUi` 的。
这两个 UI 都是 UI 控制器类 `UiController` 的实例。
下面我们来说明一下显示与控制 UI 的流程。
## 注册 UI
想要让 UI 控制器知道你想打开哪个 UI首先就要注册 UI使用 `register` 函数:
```ts
function register(...list: GameUi[]): void
```
注册的 UI 要求是一个 `GameUi` 实例,可以通过 `new` 来创建:
```ts
interface GameUi {
new(id: string, ui: Component): GameUi
}
```
这里的 `id` 指这个 UI 的名称,`ui` 表示这个 UI 的内容,需要是一个导出组件,或者是函数式组件(参考 Vue 官方文档的渲染函数页面)。注册 UI 是一个模式化的方式,一般直接照葫芦画瓢即可:
```js
const { mainUi, fixedUi } = Mota.requireAll('var');
const { m } = Mota.requireAll('fn');
const { GameUi } = Mota.requireAll('class');
const myUI = m().export();
const myUI2 = m().export();
// 注册 UI
mainUi.register(new GameUi('myUI', myUI));
// 或者一次性注册多个
mainUi.register(
new GameUi('myUI', myUI),
new GameUi('myUI2', myUI2)
);
```
## 打开与关闭 UI
可以使用 `open` 函数来打开一个 UI
```ts
function open(ui: string, vBind?: any, vOn?: any): number
```
其中 `ui` 参数表示要打开的 UI后面两个参数请参考[传递参数与监听](#传递参数与监听)。该函数会返回一个数字,表示被打开的 UI 的唯一标识符,注意每次打开 UI 都会生成一个新的标识符,即使打开的 UI 是同一个 UI。
```js
// 打开刚刚注册的 UI
const num = mainUi.open('myUI');
```
如果想要关闭 UI可以使用下面这两个函数
```ts
function close(num: number): void
function closeByName(name: string): void
```
前者是根据 UI 的标识符关闭 UI后者是根据 UI 名称关闭 UI对于后者会将所有名称匹配的 UI 都关闭,如果是 `mainUi` 上,第一个匹配的 UI 之后的所有 UI 也会全部关闭。
```js
mainUi.close(num);
mainUi.closeByName('myUI');
```
## 传递参数与监听
与在 UI 编写页面中传递参数与监听事件类似,打开 UI 的时候也可以传递参数或监听事件,通过 `open` 函数的后两个参数实现。
- `vBind`参数:向 UI 传递参数
- `vOn`参数:监听 UI 的 `emits`
这两个参数都要求传入一个对象,对于 `vBind`,键表示参数(`props`)名称,值表示其值,与 UI 编写不同的是不再需要传入函数。而对于 `vOn`,键表示名称,不再包含 `on` 作为前缀,直接填写 `emits` 的名称即可,值是一个函数,表示事件触发时执行的内容。
对于 `vBind`,一定会包括下面两个参数:
- `num`: 本次打开的 UI 的标识符
- `ui`: UI 实例,类型是 `GameUi`
```js
const myUI = m()
.defineProps({
id: String,
// 直接由 UI 控制系统打开的 UI 必须拥有这两个参数
num: Number,
ui: GameUi
})
.defineEmits(['myEmits'])
.export();
// ...此处省略注册
mainUi.open(
'myUi',
{
id: 'redSlime' // 传入 id 参数,值为 'redSlime',直接是值即可,不需要是函数
},
{
// 监听 myEmits 事件,触发时在控制台打印 'emitted!'
myEmits: () => {
console.log('emitted!');
}
}
)
```
## 显示方式
对于 `mainUi`,有两种显示方式,分别是全部显示与仅显示最后一个 UI可以通过下面两个函数设置
```ts
function showAll(): void
function showEnd(): void
```
其中前者是设置为全部显示,后者是只显示最后一个。在大部分时刻,`mainUi` 都是处于全部显示的状态。
```js
mainUi.showAll();
mainUi.open('myUI');
mainUi.open('myUI2'); // 此时 myUI 与 myUI2 都会显示
mainUi.showEnd(); // 此时只会显示 myUI2因为它是最后一个打开的
```
## 防闪烁处理
如果你把所有 UI 全部关闭,然后立刻打开一个新的 UI会出现闪烁现象观感很差于是样板提供了下面这个函数用于防闪烁处理
```ts
function holdOn(): { end(): void }
```
它的作用是暂时维持下一次 UI 全部关闭时不会引起闪烁现象,之后便失效(注意只会触发一次)。该功能的原理是在调用后,如果 UI 全部关闭,那么维持 UI 根组件不会关闭,从而防止了闪烁现象。如果调用之后一直没有新的 UI 打开,那么会引起一直处于 UI 根组件打开状态,导致 UI 假死,请注意避免这种情况。
对于返回值,它是一个对象,包含一个 `end` 方法,调用后可以立刻关闭 UI即结束这一次的维持。
```js
const num = mainUi.open('myUI');
// 防闪烁
const { end } = mainUi.holdOn();
mainUi.close(num);
// 这时 UI 整体不会被关闭,除非调用 end或者打开新的 UI
mainUi.open('myUI2');
// 此时防闪烁功能便会失效
```
## 高级用法
### 获取 GameUi 实例
可以使用 `get` 函数获取已经注册的 `GameUi` 实例:
```ts
function get(id: string): GameUi
```
例如,我们注册了 `myUI` 这个 UI可以这样获取
```js
const myUI = mainUi.get('myUI');
```
### 事件监听
`GameUi``UiController` 都继承自 `EventEmitter`(详见[事件触发系统](./event-emitter.md))。
对于 `GameUi`,它有下列事件可以被监听:
- `open()`: 这个 UI 被打开时触发,无参数
- `close()`: 这个 UI 被关闭时触发,无参数
对于 `UiController`,它有下列事件可以被监听:
- `focus(before, after)`: 当 UI 被聚焦时触发,`before` 参数表示之前聚焦的内容,也可能不存在被聚焦的内容,会是 `null``after` 参数表示聚焦至哪一个 UI。该事件会在 `mainUi.focusByNum` 或者 `mainUi.focus` 函数执行后触发,这两个函数在样板中没有调用案例,如果需要可以自行调用。该事件继承自类 [`Focus`](../api/class.md#Focus) 的事件。
- `unfocus(before)`: 取消任何聚焦时触发,与 `focus` 事件类似。该事件继承自类 [`Focus`](../api/class.md#Focus) 的事件。
- `add(item)`: 打开新 UI 时触发,参数是打开的 UI`GameUi` 实例。该事件继承自类 [`Focus`](../api/class.md#Focus) 的事件。
- `pop(item)`: 弹出最后一个 UI 时触发,参数是被弹出的 `GameUi` 实例。注意关闭 UI 不会触发此事件,因为关闭 UI 会使用 `splice` 而不是 `pop`。该事件继承自类 [`Focus`](../api/class.md#Focus) 的事件。
- `splice(spliced)`: 当 UI 被截断(关闭)时触发,参数是被关闭的 UI 数组。该事件继承自类 [`Focus`](../api/class.md#Focus) 的事件。
- `register(item)`: 当 UI 被注册时触发,参数是注册列表,是由 `GameUi` 组成的数组。
- `unregister(item)`: 当 UI 被取消注册时触发,参数是取消注册的列表。该事件与 `register` 事件类似。
- `start()`: 当 UI 根组件被打开时触发。当 UI 控制器从没有任何 UI 变成有至少一个 UI 被显示时,也即当没有 UI 打开的情况下任何 UI 被打开时,会触发此事件。无参数。
- `end()`: 当 UI 根组件被关闭时触发,即当所有 UI 都被关闭时触发。无参数。

837
docs/guide/ui.md Normal file
View File

@ -0,0 +1,837 @@
# UI 编写
新样板对 UI 系统进行了完全性重构,构建了一种新的 UI 系统。当然,你还可以继续使用旧的`createCanvas`流程进行 UI 编写,但是在新版 UI 的加持下,你可以更方便地管理多级 UI 以及内嵌 UI。同时新版 UI 系统也提供了画布 API你依然可以通过画布进行 UI 绘制及交互处理。
注意,本页所说内容全部基于[渲染进程](./system.md#进程分离),请注意进程分离。
本页面注重于 UI 编写的指引,如果你想了解新版 UI 系统,请查看[UI 系统](./ui-control.md)
## VNode
新版 UI 系统基于 Vue 的 `h` 函数,它用于生成出 `VNode`,当显示时可以直接渲染到页面上。而对于新版 UI可以认为是一种 `VNode` 生成器,用于生成出模板化的 `VNode`,从而简化了 API避免写出冗长的 `h` 函数链。因此,应当注意区分 `VNode` 与新版 UI 系统。
## MComponent
对于新版 UI应当使用系统提供的类 `MComponent` 进行编写。它的类型过于复杂,这里不再列出。下面我们将会以怪物手册为例,一步步做出一个属于你自己的怪物手册。
## 创建组件
想要写出一个 UI就要先创建一个组件我们可以直接构造`MComponent`类,也可以通过函数`m`进行创建:
```js
const MComponent = Mota.require('class', 'MComponent');
const m = Mota.require('fn', 'm');
const myUI = new MComponent();
// 等价于
const myUI = m();
```
因此,方便起见,我们更多地使用`m`函数创建组件。
## 编写 UI
创建组件之后,我们就可以编写 UI 了。我们首先来看下面这些基础函数:
```ts
// 在 MComponent 类上,也就是创建的组件上
function div(children?: any | any[], config?: any): this
function span(children?: any | any[], config?: any): this
function canvas(config?: any): this
function text(text: string | (() => string), config?: any): this
```
这些函数都是渲染单独内容的,分别是渲染 `div` `span` `canvas` 和文字。例如,我想在 UI 上渲染出一个画布,和一句话,可以写么写:
```js
myUI
.canvas()
.text('这是要渲染的内容');
```
这便是这四个函数的最基础的用法。除此之外,文字还可以传入一个函数,每次渲染 UI 的时候都会获取其返回值作为渲染内容,也就可以实现单个组件在不同条件下渲染内容不同了。例如:
```js
let cnt = 0;
core.registerAnimationFrame('example', true, () => {
cnt++; // 每帧让 cnt 加一
});
myUi.text(() => cnt.toString()); // 显示文字文字是一个函数返回cnt
// 其效果便是每次显示这个组件的时候都会显示出cnt的值注意并不会实时更新因为cnt不是一个响应式变量
// 如果想要实时更新请了解vue的响应式机制
```
对于 `div``span`,还可以添加子内容,例如可以在 `div` 里面套一个 `div`,再套一个 `span`,再套一个 `canvas`。每个子内容都可以是一个 `MComponent` 组件,或其数组:
```js
myUi
.div(
m()
.div(
m()
.span(
m().canvas()
)
)
);
```
这样,我们就实现了元素的嵌套功能,布局也能够更加灵活。对于 `config` 参数,由于其非常复杂,请参考本页之后的内容。
## 导出并渲染 UI
当我们将 UI 编写完毕后,我们并不能直接渲染在页面上,我们需要将它导出,使用 `export` 函数:
```js
const show = myUI.export();
```
然后我们需要将其注册为一个 `GameUi`。这里都是模式化的编写方式,直接照葫芦画瓢即可,一般不需要理解很多。详见[UI 系统](./ui-control.md)
```js
// 这里 mainUi 指的是有优先级的 UI例如怪物手册及怪物详细信息
// 而 fixedUi 指的是没有优先级的 UI例如状态栏与自定义工具栏多个自定义工具栏等
const { mainUi, fixedUi } = Mota.requireAll('var');
const { GameUi } = Mota.requireAll('class');
// 注册使用 register 函数
// 这里的 'myUI' 指的是 UI 名称,而第二个参数指的是 UI 内容。
mainUi.register(new GameUi('myUI', myUI));
// fixedUi 注册也是同样mainUi 与 fixedUi 间可以重名
fixedUi.register(new GameUi('myUI', myUI));
```
下面我们可以来打开这个 UI 了:
```js
// 直接调用 open 函数即可打开 UI
mainUi.open('myUI');
```
::: tip
UI 的导出具有顺序无关性,也就是说,如果导出后继续新增内容,新增的内容不需要重新导出也可以显示。
:::
## 列表渲染
我们可以对一个列表进行渲染,例如我们要渲染怪物手册,就需要获取每个怪物信息,这时,我们就需要用到列表渲染了:
```ts
function vfor(items: any[] | (() => any[]), map: (value: any, index: number) => VNode): this
```
函数要求传入两个参数:
- `items`: 要渲染的列表,是一个数组,或者是一个返回数组的函数。
- `map`: 渲染函数,接受列表项与索引作为参数,返回 `VNode`,注意不是 `MComponent` 组件。
除此之外,我们还会用到 `MComponent` 上的静态函数 `vNodeS`
```ts
function vNodeS(mc: MotaComponent, mount?: number): VNode
```
这个函数用于将单个渲染内容生成为单个的 `VNode`
为了生成单个渲染内容,我们需要获取到模块 `MCGenerator`,使用它上面的函数,`MComponent` 上的函数也是基于它的。
例如,我获取了所有怪物的信息,需要渲染每个怪物的名称,就可以这么写:
```js
// 首先获取渲染内容生成器,用于生成渲染内容
const { text, div } = Mota.require('module', 'MCGenerator');
// 定义渲染列表,例如这里我们就要渲染当前楼层的所有怪物
// 由于当前楼层id会随着游戏进行而变化因此是一个动态列表因此需要函数包裹这样可以确保每次获取的都是正确的楼层
const enemys = () => core.getCurrentEnemys();
myUI.vfor(enemys, (value, index) => {
// 要求返回一个 VNode因此需要经过 MComponent.vNodeS 的包裹
return MComponent.vNodeS(
// 输出内容是一个由div包裹的文字显示
div(text(value.enemy.enemy.name))
);
})
```
这样,我们就把每个怪物的名称渲染出来了!相比于直接使用 `canvas` 绘制,是不是简便了很多。
## 渲染组件
假如我们在很多组件里面需要共用一部分内容,每个组件里面都写一遍显然很麻烦,因此我们可以把这部分组件单独拿出来,然后做成一个组件,供其他组件使用。而这也是 `Vue` 组件一个相当强大的功能,在 `MComponent` 组件中也有相应的功能。
例如,我们先编写一个组件,内容是显示 `lhjnb`
```js
const lhjnb = m()
.div(
m().text('lhjnb')
)
.export();
```
然后,我们可以通过调用 `com` 函数来渲染组件。`com` 函数用法如下:
```ts
function com(component: any, config?: any): this
```
我们就可以将这个组件添加到我们的 UI 中了:
```js
// 传入 lbjnb 组件
myUI.com(lhjnb);
```
对于传入的组件,也可以是一个 `MComponent` 未导出组件,这样的话,这个未导出组件会被视为与当前组件在同一级,对于组件分级,请查看[组件嵌套关系](#组件嵌套关系)
## 内置组件
样板还内置了部分组件,以及一些包装好的函数可以使用。你甚至还可以把样板自带的 UI 渲染到你的 UI 上,当然由于样式的改变,效果可能会不尽人意。例如,样板就内置了一个渲染图标的组件,它使用 `canvas` 逐帧绘制实现:
```ts
// 在模块 MCGenerator 中
function icon(
id: string,
width?: number,
height?: number,
noBoarder?: number // 怪物图标是否设置为无边框无背景形式
): VNode
```
例如,这个时候我就可以给怪物手册中添加一个图标了:
```js {8-9}
const { text, div, icon } = Mota.require('module', 'MCGenerator');
const enemys = () => core.getCurrentEnemys();
myUI.vfor(enemys, (value, index) => {
return MComponent.vNodeS(
// 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children
div([
// 渲染图标
icon(value.enemy.id),
// 渲染怪物名称
text(value.enemy.enemy.name)
])
);
})
```
## 组件嵌套关系
对于任何组件,其子组件都不应该影响父组件本身,而在这里也是如此。除非你使用全局变量作为桥梁,任何子组件都不会影响父组件的渲染与参数等。
在渲染子组件时,我们可以选择子组件是否经过 `export`,而经不经过导出处理,最终结果是不一样的。具体表现如下:
- 如果子组件经过了 `export` 处理,那么子组件会被视为新一级的组件
- 而如果没有经过 `export` 处理,则会视为当前级组件
- 组件是否为当前级影响着画布的获取,一个组件在接口中只能获取到当前级的画布,而不能获取其子级画布
获取画布接口请参考[钩子](#钩子)
如果不使用 `MComponent` 进行渲染,直接调用 `MComponent.vNode` 等函数生成 `VNode`,依然会被视为一级新的组件。
## 条件渲染
很多时候我们会根据一些条件去判断一个内容是否渲染,样板同样有相关的接口。接口在 `config` 参数中:
```ts
interface Config {
vif: () => boolean
velse: boolean
}
```
其中`vif`表示渲染条件,不填时视为永远满足,当满足这个条件的时候渲染内容,`velse`表示是否是否则选项,当`velse`与`vif`同时出现时,效果等同于`else if`。于是,我们便可以依此来判断怪物的特殊属性,如果没有特殊属性,那么就渲染为`无属性`,否则渲染怪物的属性列表。
```js {16-25}
const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator');
const enemys = () => core.getCurrentEnemys();
// 用于获取怪物应该显示在手册上的信息的函数
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
myUI.vfor(enemys, (value, index) => {
// 首先要获取怪物的详细信息
const detail = getDetailedEnemy(value.enemy);
return MComponent.vNodeS(
// 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children
div([
// 渲染图标
icon(value.enemy.id),
// 渲染怪物名称
text(value.enemy.enemy.name),
// 当怪物拥有特殊属性时
div([
// 渲染怪物的特殊属性,还需要一层 for 嵌套
vfor(detail.special, ([name, desc, color], index) => {
// 这里的special是一个数组第一项表示名称第二项表示说明第三项表示名称颜色
// 这里我们只渲染名称
return span(text(name))
})
], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时
div(span(text('无属性')), { velse: true }) // 否则渲染无属性
])
);
})
```
## 传递参数
我们会发现,`icon` 函数可以将怪物 id 传入组件中并显示出来,这种行为叫做传递`props`,在这里我们也称为传递参数。拥有了传递参数的功能,我们可以接收来自父组件的参数,同时根据父组件的意愿渲染出对应的内容。
例如,就拿样板内置的 `BoxAnimate` 组件为例,也就是 `icon` 函数渲染的组件,我们可以通过 `config` 传递参数。对于每个 `props`,都要求是一个函数,其返回值作为真正传递的参数。如果想要传递函数,那么就需要是一个返回函数的函数。如果只想传递一个常量,又不想写一个函数,那么可以使用 `MCGenerator` 提供的 `f` 函数。
```js
const { BoxAnimate } = Mota.require('module', 'UIComponents');
const { f } = Mota.require('module', 'MCGenerator');
myUI.com(BoxAnimate, { props: { id: f('redSlime') } });
// 这个就等效于使用 icon 函数:
myUI.h(icon('redSlime'));
```
## 定义参数
如果我们想给自己的组件定义一些参数,可以使用 `defineProps` 函数:
```ts
function defineProps(props: Record<string, any>): this
```
这个函数传入一个对象,其键表示参数名称,值表示参数类型,例如:
```js
const com = m().defineProps({
id: String, // 对于字符串、数字、布尔值类型表示为String, Number, Boolean
num: Number,
ui: GameUi // 对于对象,可以表示为 Object 或者类名
})
```
这样,我们就可以向这个组件传递参数了,如果参数类型不正确,或者传递的参数并没有定义,依然可以正确运行,不过会在控制台输出警告。例如:
```js
const { f } = Mota.require('module', 'MCGenerator');
// 传入lhjnb作为参数但是由于还有两个参数没有传递会在控制台有报错不过依然可以正确运行
myUI.com(com, { props: { id: f('lhjnb') } });
```
对于如何获取传入的参数,请参考[钩子](#钩子)
## 传递 Attribute
我们还可以向浏览器的 DOM 元素中添加信息,只要我们将值传递给 `props` 即可,例如我们可以给一个 `span` 元素添加 id设置样式等。就拿上面的怪物特殊属性举例我们如果想要让不同的属性显示出对应的颜色那么可以这么写
```js {15-18}
const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator');
const enemys = () => core.getCurrentEnemys();
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
myUI.vfor(enemys, (value, index) => {
const detail = getDetailedEnemy(value.enemy);
return MComponent.vNodeS(
div([
// 渲染图标
icon(value.enemy.id),
// 渲染怪物名称
text(value.enemy.enemy.name),
// 当怪物拥有特殊属性时
div([
vfor(detail.special, ([name, desc, color], index) => {
// 将 style 作为 props 传入即可,这样颜色就能正确显示出来了
return span(text(name), { props: { style: f(`color: ${color}`) } });
})
], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时
div(span(text('无属性')), { velse: true }) // 否则渲染无属性
])
);
})
```
## 监听事件与 emits
对于 `props`,有一类特殊的属性,由 `on` 开头,然后紧跟着大写字母,这种形式的 `props` 会被视为监听函数。例如,我想监听一个 `div` 的点击事件,当玩家点击这个 `div` 时,在控制台输出 `clicked!`,那么可以这么写:
```js
myUI.div(
[], // 没有子元素,填空数组
{
props: {
onClick: f((e) => {
// 当点击时,输出 clicked! 与点击位置相对于浏览器左上角的坐标
console.log('clicked!', e.clientX, e.clientY);
})
}
}
)
```
对于自定义组件,依然可以使用同样的方法监听其 `emits``emits` 可以认为是组件的事件,可以在组件内部触发。例如,对于样板自带的 `Column` 组件,它有一个 `close` 事件,我们便可以用这种方式进行监听:
```js
const { Column } = Mota.require('module', 'UIComponents');
myUI.com(
Column,
{
props: {
// 监听close事件所以名为 onClose
onClose: f(() => {
// 当 close 事件触发时,会在控制台输出 close emitted!
console.log('close emitted!');
})
}
}
)
```
你也可以为自己的组件定义自己的 `emits`,通过 `defineEmits` 函数:
```js
myUI.defineEmits(['myEmit']); // 传入一个字符串数组,表示组件的 emits 名称列表
```
这样,你就可以在其他地方监听这个组件的 `emits`
## 渲染函数
`MComponent` 上也有一个名为 `h` 的渲染函数,但是它与 `Vue` 的渲染函数不同。在这里,`h` 函数的用法如下:
```ts
function h(type: any, children?: any | any[], config?: any): this
```
上面用到的所有函数,包括 `div` `span` `canvas` `text` `com` 在内的函数都基于这个函数。借助于这个函数,我们可以更加灵活地编写我们的 UI。
对于第一个参数,它可以填写这些内容:
- `text`: 表示渲染文字,等价于 `text` 函数,需要填写 `config` 参数的 `innerText` 属性
- `component`: 表示渲染组件,需要在 `config` 参数中填写 `component` 属性或 `dComponent` 属性,详见[动态组件](#动态组件)
- 任何 DOM 元素的名称,例如 `span` `li` `input` 等,表示渲染 DOM 元素
- 任何 `MComponent` 组件,表示渲染组件,等价于填写 `component`,然后填写对应的 `config` 属性,也等价于 `com` 函数
- 任何导出后的组件,或者是样板自带组件,与填写 `MComponent` 时类似
例如:
```js
myUI
.h('div')
// 注意,如果你确保递归调用组件没问题,那么你可以按下面这样递归调用自身作为组件,否则出问题概不负责!
.h('component', [], { component: myUI })
.h('text', [], { innerText: 'lhjnb' });
```
## 动态组件
`h` 函数传入 `component` 作为类型时,可以作为动态组件使用,要求填写 `config` 参数的 `component` 属性或者 `dComponent` 属性。填写 `component` 属性时,等价于 `com` 函数,这里不再赘述,下面我们来看填写 `dComponent` 属性时的情况。
这个属性要求传入一个函数,函数返回一个导出组件,注意不能是 `MComponent` 组件,也不能是 `VNode`。例如:
```js
const randomComponent = [BoxAnimate, Box, Column];
// 随机渲染一个组件上去!
myUI.h(
'component',
[],
{ dComponent: () => randomComponent[Math.floor(Math.random() * 3)] }
);
```
## 传递插槽
插槽的功能是向子组件中渲染内容,在目前样板中,没有提供定义插槽的方法,只能向子组件中传递插槽。这也意味着,你只能向样板内置组件或者 `Vue` 内置组件中传递插槽。不过,在[自定义 setup](#自定义-setup)中,提供了另一种方式可以渲染插槽(定义插槽)
你可以通过 `config` 参数的 `slots` 属性传递插槽,每个插槽都是一个函数,要求返回一个 `VNode` 或其数组用于渲染。例如,对于滚动条组件 `Scroll`,它有一个默认插槽 `default`,你可以这样传递插槽:
```js
const { icon, div } = Mota.require('module', 'MCGenerator');
myUI.com(Scroll, [], {
slots: {
// 传递默认插槽
default: () => {
// 返回 VNode使用 MComponent 上的静态函数 vNode它可以将 MCGenerator 生成的渲染内容生成为 vNode
return MComponent.vNode([
div(icon('greenSlime')),
div(icon('redSlime'))
]);
}
}
})
```
## 钩子
下面是样板提供的几个钩子接口。什么是钩子呢?就是在组件渲染的不同时刻执行传入的函数。目前样板中,提供了两个接口,分别是:
- `onSetup`: 当组件开始渲染时,这时组件内容还没有挂载到页面上
- `onMounted`: 当组件渲染完毕时,这时组件已经完全渲染到了页面上
这两个函数都会接收两个参数,分别是 `props``ctx`
- `props`: 表示父组件传递过来的参数(`props`
- `ctx`: `setup` 上下文,用于触发 `emits`,渲染插槽(定义插槽)等,不过在这里渲染插槽没有用处
例如:
```js
myUI.onSetup((props, ctx) => {
// 输出父组件传递的参数
console.log(props);
// 触发名为 close 的 emits同时将 123 作为数据输出,数据可以由父组件的监听函数的参数中获取
ctx.emits('close', 123);
});
```
对于 `onMounted` 函数,还会另外多一个参数,描述的是当前级组件(不包括子组件,但包括 `MComponent` 未导出组件,详见[组件嵌套关系](#组件嵌套关系))中所有的画布。例如:
```js
myUI
// 创建一个 id 为 my-canvas 的画布
.canvas({ props: { id: f('my-canvas') } })
// 当组件渲染完毕时
.onMounted((props, ctx, canvas) => {
const myCanvas = canvas[0];
if (myCanvas?.id === 'my-canvas') {
// 获取画布的绘制上下文
const ctx = myCanvas.getContext('2d');
// 绘制一条线,也可以使用样板的 core.drawLine 等 api 绘制
ctx.moveTo(0, 0);
ctx.lineTo(200, 200);
ctx.stroke();
}
});
```
## 自定义 setup
自定义 `setup` 允许你完全控制组件的渲染内容,并在组件渲染时执行脚本。它包含两个接口:
- `ret`: 设置 `setup` 的返回值,即组件的最终渲染函数
- `setup`: 完全重写设置组件的 `setup` 函数,让组件完全自定义
`setup` 的含义是,这个组件被渲染的时候,执行的函数
例如,你可以通过 `ret` 接口来修改你的组件的渲染内容,而由于可以直接操作渲染内容,你便可以做到渲染插槽(定义插槽)
```js
const { icon, div, span, text } = Mota.require('module', 'MCGenerator');
myUI.ret((props, ctx) => {
// 函数要求返回 VNode因此要有 vNode 函数包裹
// 注意在这里请尽量避免使用导出组件直接作为返回值,这会对性能造成一定的影响
return MComponent.vNode([
span(text('lhjnb')),
div(icon('greenSlime')),
ctx.slots.defaults(), // 渲染插槽(定义插槽)
]);
})
```
还可以通过 `setup` 函数完全重写组件。对于稍微复杂点的组件,我们都需要使用这个功能。
::: tip
重写 setup 函数后,应当重新 export 组件,否则内容不会变化
:::
```js
// 这里就需要获取 vue 的函数了,还可以使用 ref 实现响应式布局
const { onMounted, ref } = Mota.Package.require('vue');
myUI.setup((props, ctx) => {
// 当组件渲染完毕时
onMounted(() => {
console.log('mounted!');
});
const cnt = ref(0);
// setup 函数要返回一个函数,函数返回值为 VNode
return () => {
return MComponent.vNode([
span(text('lhjnb')),
div(icon('greenSlime')),
span(text(cnt.value)) // 使用响应式变量cnt当其更改时渲染内容也会随之更改
]);
}
})
```
实际上,样板自带的 `setup` 函数也是使用它来实现的:
```ts
const setup = (props, ctx) => {
const mountNum = MComponent.mountNum++;
this.onSetupFn?.(props, ctx);
onMounted(() => {
this.onMountedFn?.(
props,
ctx,
Array.from(
document.getElementsByClassName(
`--mota-component-canvas-${mountNum}`
) as HTMLCollectionOf<HTMLCanvasElement>
)
);
});
if (this.retFn) return () => this.retFn!(props, ctx);
else {
return () => {
const vNodes = MComponent.vNode(
this.content,
mountNum
);
return vNodes;
};
}
}
```
## 怪物手册示例
下面我们将做一个简易的怪物手册作为 UI 示例。在这里,我们使用样板组件 `Column` 进行编写,它可以让一个 UI 分为两栏,同时还可以设置大小,及左右栏占比,对于制作说明式的 UI 非常有用。示例中包含了按键操作的说明,详见[按键系统](./hotkey.md)
内容较长,可以复制到编辑器内查看。
```js
// 首先引入需要的接口,注意以下所有内容均基于渲染进程
const { MComponent, GameUi } = Mota.requireAll('class');
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
const { Column } = Mota.require('module' ,'UIComponents');
const { com, div, span, text, icon, f, vfor } = Mota.require('module', 'MCGenerator');
const { mainUi, gameKey } = Mota.requireAll('var');
const { m } = Mota.requireAll('fn');
const { ref, computed, onUnmounted } = Mota.Package.require('vue');
// 怪物手册属于较为复杂的内容,必须要完全重写 setup
const myBook = m().setup((props, ctx) => {
const close = () => {
// 对于每个直接由 UI 系统打开的 UI都会有一个 num 参数,表示这个 UI 的标识符
// 这个标识符可以用于关闭 UI例如下面就是关闭这个 UI
// 更多内容请查阅 UI 系统章节
mainUi.close(props.num);
}
onUnmounted(() => {
// 当关闭 UI 时,跳出当前层按键作用域
// 注意由于 UI 可能是被其他原因被关闭的,不会执行 close 函数,因此要使用 onUnmounted 钩子
gameKey.dispose(props.ui.symbol);
});
const enemys = core.getCurrentEnemys();
// 对于 number string boolean要做响应式需要 ref对于对象需要 reactive更多信息请了解 vue 的响应式
const selected = ref(0);
// computed 会自动追踪用到的响应式变量,同时在响应式变量更改时更改 enemy 的值
const enemy = computed(() => enemys[selected.value]);
// 按键系统,首先创建新作用域,然后实现按键操作
gameKey
.use(props.ui.symbol)
// 上移一位
.realize('@book_up', () => {
if (selected.value > 0) selected.value--;
})
// 下移一位
.realize('@book_down', () => {
if (selected.value < enemys.length - 1) selected.value++;
})
// 下移5位
.realize('@book_pageDown', () => {
if (selected.value >= enemys.length - 5) {
selected.value = enemys.length - 1;
} else {
selected.value += 5;
}
})
// 上移5位
.realize('@book_pageUp', () => {
if (selected.value <= 4) {
selected.value = 0;
} else {
selected.value -= 5;
}
})
// 退出
.realize('exit', () => {
close();
});
// 要渲染的怪物属性
const status = [
['hp', '生命', 'lightgreen']
['atk', '攻击', 'lightcoral'],
['def', '防御', 'lightblue'],
['exp', '经验', 'lightgreen'],
['gold', '金币', 'lightyellow']
];
// 简写数据格式化
const format = core.formatBigNumber;
// 属性样式,由于多次用到在这里声明为一个常量
const statusStyle = {
props: {
style: f(`
display: flex;
flex-direction: row;
color: ${color};
`)
}
};
const statusSpanStyle = {
props: {
style: f(`
flex-basis: 30%;
text-align: right;
margin-right: 5%;
`)
}
}
return () => {
// 要求返回 VNode。对于大部分情况如果你不确定是否要包裹 vNode 的话,包裹一层不会有任何副作用
return MComponent.vNode([
com(Column, {
// Column 组件共有四个参数,分别是宽高,与左右占比,单位都是百分比
// 宽高作为百分比时描述的一般是相对于父组件宽高的占比,对于根组件自然就是屏幕的占比了
props: {
width: f(70),
height: f(70),
left: f(30),
right: f(70),
onClose: f(close) // Column 组件自带关闭的功能,监听 close 事件即可,然后将 close 函数传入
},
// Column 组件包含名为 left 和 right 的插槽,向其中传入要渲染的内容即可
// 在这里,左侧渲染怪物列表,右侧渲染怪物信息
slots: {
// 左侧内容,为当前楼层的怪物列表
left: () => {
return MComponent.vNode([
// 列表渲染,遍历函数要返回单个 VNode
vfor(enemys, (value, index) => {
return MComponent.vNodeS(
div(
[
// 渲染怪物的图标,设置为无边框与背景
icon(value.enemy.id, void 0, void 0, true),
// 然后是怪物名称
span(text(value.enemy.enemy.name))
],
{
props: {
// selectable 是样板内置的样式类,选中时会有选中动画
class: f('selectable'),
// selected 属性表示是否选中
selected: () => selected.value === index,
style: f(`
margin: 0 0 4px 0;
display: flex;
flex-direction: row;
align-items: center;
`)
},
// 当点击这个怪物时
onClick: f(() => {
selected.value = index;
})
}
)
);
});
]);
},
// 右侧内容,显示怪物的详细信息
right: () => {
return MComponent.vNode([
// 顶部,渲染怪物图标与名称
div([
// 没有边框
icon(enemy.value.enemy.id, void 0, void 0, true);
span(text(enemy.value.enemy.enemy.name), {
props: {
// 注意所有的文字尽量使用百分比进行大小限定,这样可以被设置所更改
style: f(`font-size: 200%`)
}
})
],
{
props: {
// 顶部样式
style: f(`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
padding-bottom: 5%;
border-bottom: dashed 1px #ddd8;
`)
}
}),
// 然后是下面的部分,包括怪物的详细信息
// 首先是怪物的基础属性
div(
[
// 使用列表渲染,对上面定义的怪物属性进行渲染
vfor(status, ([key, name, color]) => {
return MComponent.vNodeS(
div([
// 怪物属性名称,右对齐
span(text(name), statusSpanStyle),
span(text(format(enemy.value.enemy.info[key])))
], statusStyle)
);
}),
// 然后是伤害、临界
div([
span(text('伤害'), statusSpanStyle),
span(text(format(enemy.value.damage)))
], statusStyle),
div([
span(text('临界'), statusSpanStyle),
span(text(format(enemy.value.critical)))
], statusStyle),
div([
span(text('临界减伤'), statusSpanStyle),
span(text(format(enemy.value.criticalDam)))
], statusStyle),
div([
span(text(core.status.thisMap.ratio + '防'), statusSpanStyle),
span(text(format(enemy.value.defDam)))
], statusStyle),
// 下面是特殊属性,依然要用列表渲染
vfor(enemy.value.showSpecial, ([name, desc, color]) => {
return MComponent.vNodeS(div(
span(text(name), { props: { styles: f(`color: ${color}`) } }),
span(text(': ')),
span(text(desc))
));
})
],
{
props: {
style: f(`
display: flex;
flex-direction: column;
width: 100%;
`)
}
}
)
]);
}
}
});
]);
}
})
// 由 UI 系统直接打开的 UI 都需要包含这两个参数
.defineProps({
num: Number,
ui: GameUi
})
.export();
// 注册 UI
mainUi.register(new GameUi('myBook', myBook));
```

27
docs/index.md Normal file
View File

@ -0,0 +1,27 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: 'mota-js'
text: 'HTML5魔塔样板V2.A'
tagline: HTML5魔塔样板从 2.x 到 3.0 的过渡版本
actions:
- theme: brand
text: 快速开始
link: /guide/diff
- theme: alt
text: API列表
link: /api-examples
- theme: alt
text: 旧样板文档
link: https://h5mota.com/games/template/_docs/#/
features:
- title: 高适配性
details: 不论你是在电脑上还是手机上甚至是电视上游戏机上只要有一个浏览器就能游玩HTML5魔塔不论你使用触屏还是键盘不论是鼠标还是手柄都能流畅地操作
- title: 高扩展性
details: HTML5魔塔样板提供了非常丰富的API借助于插件API你可以做出任何你想要的东西
- title: 上手难度低
details: 哪怕你不会代码你也可以轻松地造出一个HTML5魔塔借助于样板的事件系统与网站插件库你也可以让你的魔塔更有个性
---

View File

@ -11,7 +11,10 @@
"declare": "ts-node-esm script/declare.ts",
"type": "vue-tsc --noEmit",
"lines": "ts-node-esm script/lines.ts",
"build-dts": "ts-node-esm script/buildDeclaration.ts"
"build-dts": "ts-node-esm script/buildDeclaration.ts",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@ant-design/icons-vue": "^6.1.0",
@ -58,6 +61,7 @@
"unplugin-vue-components": "^0.22.12",
"vite": "^4.4.9",
"vite-plugin-dts": "^3.7.2",
"vitepress": "1.0.0-rc.41",
"vue-tsc": "^1.8.8",
"ws": "^8.13.0"
}

File diff suppressed because it is too large Load Diff

View File

@ -1047,7 +1047,8 @@ editor_file = function (editor, callback) {
for (var id_ in fmap) {
fraw = fraw.replace('"' + id_ + '"', fmap[id_])
}
var datastr = '///<reference path="../../src/types/core.d.ts" />\nvar functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = \n';
var datastr = `///<reference path="../types/core.d.ts" />
var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = \n`;
datastr += fraw;
fs.writeFile('project/functions.js', encode(datastr), 'base64', function (err, data) {
callback(err);
@ -1101,7 +1102,8 @@ editor_file = function (editor, callback) {
for (var id_ in plmap) {
plraw = plraw.replace('"' + id_ + '"', plmap[id_])
}
var datastr = '///<reference path="../../src/types/core.d.ts"/>\nvar plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = \n';
var datastr = `///<reference path="../types/core.d.ts" />
var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = \n`;
datastr += plraw;
fs.writeFile('project/plugins.js', encode(datastr), 'base64', function (err, data) {
callback(err);

View File

@ -317,6 +317,7 @@ events.prototype.restart = function () {
core.hideStatusBar();
core.showStartAnimate();
core.playBgm(main.startBgm);
Mota.require('var', 'fixedUi').open('start');
};
////// 询问是否需要重新开始 //////

View File

@ -890,6 +890,10 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
if (info.special.includes(10)) {
return Infinity;
}
// 吸血
if (info.special.includes(11) && info.add) {
return add + core.status.hero.hp * (info.vampire ?? 0);
}
return add;
}
@ -1112,6 +1116,7 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
);
// ----- 计算第一类光环
// 特殊属性对于的特殊属性数值
const changeable = Mota.require('module', 'Damage').changeableHaloValue;
changeable
.set(21, ['atkValue', 'defValue'])
@ -1156,12 +1161,15 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
(e, enemy) => {
const s = enemy.specialHalo;
e.special.push(...s);
// 如果是自身,就不进行特殊属性数值处理了
if (enemy === this.info) return;
// 然后计算特殊属性数值
for (const spec of s) {
const toChange = changeable.get(spec);
if (!toChange) continue;
for (const key of toChange) {
if (enemy.specialMultiply) {
// 这种光环应该获取怪物的原始数值,而不是真实数值
if (enemy.enemy.specialMultiply) {
e[key] = s[key] ?? 1;
e[key] *= enemy[key];
} else {

View File

@ -107,13 +107,7 @@ const compress = type === 'dist';
// 6. 部分修改类型标注
try {
const indexDTS = await fs.readFile('./dist/index.d.ts', 'utf-8');
const re =
'///<reference path="./types/core.d.ts" />\n' +
indexDTS
.replaceAll('export declare', 'declare')
.replace(/export\s*\{\s*\};?/, '')
.replace(/import.*;/gu, '') +
`declare var Mota: IMota`;
const re = '///<reference path="./types/core.d.ts" />\n' + indexDTS;
await fs.writeFile('./dist/index.d.ts', re, 'utf-8');
const pluginDTS = await fs.readFile(
@ -125,7 +119,10 @@ const compress = type === 'dist';
interface Window {
Mota: import('../game/system').IMota;
}`,
''
`declare const Mota: import('./index.d.ts').IMota;
interface Window {
Mota: import('./index.d.ts').IMota;
}`
);
await fs.writeFile('./dist/types/plugin.d.ts', rep);
@ -134,8 +131,7 @@ interface Window {
const info = await fs.readFile('./dist/project/' + file, 'utf-8');
const re = info.replace(
/\/\/\/\<reference\s*path=('|").*('|")\s*\/>/g,
`///<reference path="../index.d.ts" />
///<reference path="../types/core.d.ts" />`
`///<reference path="../types/core.d.ts" />`
);
await fs.writeFile('./dist/project/' + file, re, 'utf-8');
}

View File

@ -78,16 +78,7 @@ export class SoundEffect extends AudioPlayer {
if (stereo) {
this.panner = ac.createPanner();
this.panner.connect(this.gain);
if (channel === 1) {
this.merger = ac.createChannelMerger();
this.merger.connect(this.panner);
this.baseNode = [
{ node: this.merger, channel: 0 },
{ node: this.merger, channel: 1 }
];
} else {
this.baseNode = [{ node: this.panner }];
}
this.baseNode = [{ node: this.panner }];
} else {
this.baseNode = [{ node: this.gain }];
}
@ -136,15 +127,18 @@ export class SoundEffect extends AudioPlayer {
* @param source
* @param listener
*/
setPanner(source: Partial<Panner>, listener: Partial<Listener>) {
setPanner(source?: Partial<Panner>, listener?: Partial<Listener>) {
if (!this.panner) return;
console.log(2);
for (const [key, value] of Object.entries(source)) {
this.panner[key as keyof Panner].value = value;
if (source) {
for (const [key, value] of Object.entries(source)) {
this.panner[key as keyof Panner].value = value;
}
}
const l = AudioPlayer.ac.listener;
for (const [key, value] of Object.entries(listener)) {
l[key as keyof Listener].value = value;
if (listener) {
const l = AudioPlayer.ac.listener;
for (const [key, value] of Object.entries(listener)) {
l[key as keyof Listener].value = value;
}
}
}
}

View File

@ -154,7 +154,7 @@ export class IndexedEventEmitter<
event: K,
symbol: IndexedSymbol,
fn: T[K],
options: Partial<ListenerOptions>
options?: Partial<ListenerOptions>
) {
const map = this.ensureMap(event);
if (map.has(symbol)) {

View File

@ -26,7 +26,7 @@ import {
} from './main/custom/hotkey';
import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard';
import './main/layout';
import { MComponent, icon, m } from './main/layout';
import { MComponent, m } from './main/layout';
import { createSettingComponents } from './main/init/settings';
import {
createToolbarComponents,
@ -45,6 +45,17 @@ import * as fixedTools from '@/plugin/ui/fixed';
import * as flyTools from '@/plugin/ui/fly';
import * as statusBarTools from '@/plugin/ui/statusBar';
import * as toolboxTools from '@/plugin/ui/toolbox';
import * as UI from '@ui/index';
import Box from '@/components/box.vue';
import BoxAnimate from '@/components/boxAnimate.vue.vue';
import Colomn from '@/components/colomn.vue.vue';
import EnemyOne from '@/components/enemyOne.vue.vue';
import Scroll from '@/components/scroll.vue.vue';
import EnemyCritical from '@/panel/enemyCritical.vue';
import EnemySpecial from '@/panel/enemySpecial.vue.vue';
import EnemyTarget from '@/panel/enemyTarget.vue.vue';
import KeyboardPanel from '@/panel/keyboard.vue.vue';
import { MCGenerator } from './main/layout';
// ----- 类注册
Mota.register('class', 'AudioPlayer', AudioPlayer);
@ -63,7 +74,6 @@ Mota.register('class', 'UiController', UiController);
Mota.register('class', 'MComponent', MComponent);
// ----- 函数注册
Mota.register('fn', 'm', m);
Mota.register('fn', 'icon', icon);
Mota.register('fn', 'unwrapBinary', unwarpBinary);
Mota.register('fn', 'checkAssist', checkAssist);
Mota.register('fn', 'isAssist', isAssist);
@ -81,7 +91,6 @@ Mota.register('var', 'KeyCode', KeyCode);
Mota.register('var', 'ScanCode', ScanCode);
Mota.register('var', 'settingStorage', settingStorage);
Mota.register('var', 'status', status);
// ----- 模块注册
Mota.register('module', 'CustomComponents', {
createSettingComponents,
@ -104,6 +113,19 @@ Mota.register('module', 'UITools', {
statusBar: statusBarTools,
toolbox: toolboxTools
});
Mota.register('module', 'UI', UI);
Mota.register('module', 'UIComponents', {
Box,
BoxAnimate,
Colomn,
EnemyOne,
Scroll,
EnemyCritical,
EnemySpecial,
EnemyTarget,
Keyboard: KeyboardPanel
});
Mota.register('module', 'MCGenerator', MCGenerator);
main.renderLoaded = true;
Mota.require('var', 'hook').emit('renderLoaded');

View File

@ -137,7 +137,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
* 退
* @param symbol symbol
*/
dispose(symbol: symbol) {
dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()) {
for (const key of Object.values(this.data)) {
key.func.delete(symbol);
}

View File

@ -74,16 +74,6 @@ gameKey
name: '浏览地图_2',
defaults: KeyCode.PageDown
})
.register({
id: 'skillTree',
name: '技能树',
defaults: KeyCode.KeyJ
})
.register({
id: 'desc',
name: '百科全书',
defaults: KeyCode.KeyH
})
// --------------------
.group('function', '功能按键')
.register({
@ -256,20 +246,6 @@ gameKey
defaults: KeyCode.KeyC
})
// --------------------
.group('@ui_start', '开始界面')
.register({
id: '@start_up',
name: '上移光标',
defaults: KeyCode.UpArrow,
type: 'down'
})
.register({
id: '@start_down',
name: '下移光标',
defaults: KeyCode.DownArrow,
type: 'down'
})
// --------------------
.group('@ui_book', '怪物手册')
.register({
id: '@book_up',

View File

@ -9,7 +9,7 @@ interface Components {
Number: SettingComponent;
HotkeySetting: SettingComponent;
ToolbarEditor: SettingComponent;
RadioSetting: (items: string[]) => SettingComponent;
Radio: (items: string[]) => SettingComponent;
}
export type { Components as SettingDisplayComponents };
@ -21,7 +21,7 @@ export function createSettingComponents() {
Number: NumberSetting,
HotkeySetting,
ToolbarEditor,
RadioSetting
Radio: RadioSetting
};
return com;
}

View File

@ -5,10 +5,12 @@ import {
VNode,
VNodeChild,
defineComponent,
h,
h as hVue,
isVNode,
onMounted
} from 'vue';
import BoxAnimate from '@/components/boxAnimate.vue';
import { ensureArray } from '@/plugin/utils';
interface VForRenderer {
type: '@v-for';
@ -18,7 +20,7 @@ interface VForRenderer {
interface MotaComponent extends MotaComponentConfig {
type: string;
children: MComponent[] | MComponent;
children: (MComponent | MotaComponent | VNode)[];
}
interface MotaComponentConfig {
@ -32,7 +34,7 @@ interface MotaComponentConfig {
velse?: boolean;
}
type OnSetupFunction = (props: Record<string, any>) => void;
type OnSetupFunction = (props: Record<string, any>, ctx: SetupContext) => void;
type SetupFunction = (
props: Record<string, any>,
ctx: SetupContext
@ -43,6 +45,7 @@ type RetFunction = (
) => VNodeChild | VNodeChild[];
type OnMountedFunction = (
props: Record<string, any>,
ctx: SetupContext,
canvas: HTMLCanvasElement[]
) => void;
@ -51,6 +54,12 @@ type NonComponentConfig = Omit<
'innerText' | 'component' | 'slots' | 'dComponent'
>;
type MComponentChildren =
| (MComponent | MotaComponent | VNode)[]
| MComponent
| MotaComponent
| VNode;
export class MComponent {
static mountNum: number = 0;
@ -88,7 +97,7 @@ export class MComponent {
* @param children
* @param config {@link MComponent.h}
*/
div(children?: MComponent[] | MComponent, config?: NonComponentConfig) {
div(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('div', children, config);
}
@ -97,7 +106,7 @@ export class MComponent {
* @param children
* @param config {@link MComponent.h}
*/
span(children?: MComponent[] | MComponent, config?: NonComponentConfig) {
span(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('span', children, config);
}
@ -136,11 +145,7 @@ export class MComponent {
* MComponent.vNode函数生成
*/
vfor<T>(items: T[] | (() => T[]), map: (value: T, index: number) => VNode) {
this.content.push({
type: '@v-for',
items,
map
});
this.content.push(MCGenerator.vfor(items, map));
return this;
}
@ -180,32 +185,10 @@ export class MComponent {
*/
h(
type: string | Component | MComponent,
children?: MComponent[] | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): this {
if (typeof type === 'string') {
this.content.push({
type,
children: children ?? [],
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: config.component
});
} else {
this.content.push({
type: 'component',
children: children ?? [],
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: type
});
}
this.content.push(MCGenerator.h(type, children, config));
return this;
}
@ -253,11 +236,12 @@ export class MComponent {
return defineComponent(
(props, ctx) => {
const mountNum = MComponent.mountNum++;
this.onSetupFn?.(props);
this.onSetupFn?.(props, ctx);
onMounted(() => {
this.onMountedFn?.(
props,
ctx,
Array.from(
document.getElementsByClassName(
`--mota-component-canvas-${mountNum}`
@ -269,8 +253,6 @@ export class MComponent {
if (this.retFn) return () => this.retFn!(props, ctx);
else {
return () => {
console.log(ctx.slots.default);
const vNodes = MComponent.vNode(
this.content,
mountNum
@ -312,12 +294,19 @@ export class MComponent {
* @param children VNode的内容列表
* @param mount id
*/
static vNode(children: (MotaComponent | VForRenderer)[], mount?: number) {
static vNode(
children: (MotaComponent | VForRenderer | VNode)[],
mount?: number
) {
const mountNum = mount ?? this.mountNum++;
const res: VNode[] = [];
const vifRes: Map<number, boolean> = new Map();
children.forEach((v, i) => {
if (isVNode(v)) {
res.push(v);
return;
}
if (v.type === '@v-for') {
const node = v as VForRenderer;
const items =
@ -346,7 +335,7 @@ export class MComponent {
);
}
if (v.dComponent) {
res.push(h(v.dComponent(), props, v.slots));
res.push(hVue(v.dComponent(), props, v.slots));
} else {
if (v.component instanceof MComponent) {
res.push(
@ -356,12 +345,12 @@ export class MComponent {
)
);
} else {
res.push(h(v.component!, props, v.slots));
res.push(hVue(v.component!, props, v.slots));
}
}
} else if (v.type === 'text') {
res.push(
h(
hVue(
'span',
typeof v.innerText === 'function'
? v.innerText()
@ -372,15 +361,17 @@ export class MComponent {
const cls = `--mota-component-canvas-${mountNum}`;
const mix = !!props.class ? cls + ' ' + props.class : cls;
props.class = mix;
res.push(h('canvas', props, node.slots));
res.push(hVue('canvas', props, node.slots));
} else {
// 这个时候不可能会有插槽,只会有子内容,因此直接渲染子内容
const content = [node.children].flat(2);
const content = node.children;
const vn = this.vNode(
content.map(v => v.content).flat(),
content
.map(v => (v instanceof MComponent ? v.content : v))
.flat(),
mountNum
);
res.push(h(v.type, props, vn));
res.push(hVue(v.type, props, vn));
}
}
});
@ -406,7 +397,7 @@ export class MComponent {
* @param props props
*/
static prop(component: Component, props: Record<string, any>) {
return h(component, props);
return hVue(component, props);
}
}
@ -418,18 +409,101 @@ export function m() {
return new MComponent();
}
/**
* VNode
* @param id id
* @param width
* @param height
* @param noBoarder
*/
export function icon(
id: AllIds,
width?: number,
height?: number,
noBoarder?: number
) {
return h(BoxAnimate, { id, width, height, noBoarder });
export namespace MCGenerator {
export function h(
type: string | Component | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): MotaComponent {
if (typeof type === 'string') {
return {
type,
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: config.component
};
} else {
return {
type: 'component',
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: type
};
}
}
export function div(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('div', children, config);
}
export function span(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('span', children, config);
}
export function canvas(config?: NonComponentConfig): MotaComponent {
return h('canvas', [], config);
}
export function text(
text: string | (() => string),
config: NonComponentConfig = {}
): MotaComponent {
return h('text', [], { ...config, innerText: text });
}
export function com(
component: Component | MComponent,
config: Omit<MotaComponentConfig, 'innerText' | 'component'>
): MotaComponent {
return h(component, [], config);
}
/**
* VNode
* @param id id
* @param width
* @param height
* @param noBoarder
*/
export function icon(
id: AllIds,
width?: number,
height?: number,
noBoarder?: number
): VNode {
return hVue(BoxAnimate, { id, width, height, noBoarder });
}
export function vfor<T>(
items: T[] | (() => T[]),
map: (value: T, index: number) => VNode
): VForRenderer {
return {
type: '@v-for',
items,
map
};
}
/**
*
* @param value
*/
export function f<T>(value: T): () => T {
return () => value;
}
}

View File

@ -213,7 +213,7 @@ export class MotaSetting extends EventEmitter<SettingEvent> {
let now: MotaSetting = this;
for (let i = 0; i < list.length - 1; i++) {
const item = now.list[list[i]].value;
const item = now.list[list[i]]?.value;
if (!(item instanceof MotaSetting)) {
throw new Error(
`Cannot get setting. The parent isn't a MotaSetting instance.` +
@ -267,7 +267,7 @@ export class SettingDisplayer extends EventEmitter<SettingDisplayerEvent> {
this.displayInfo = [];
for (let i = 0; i < list.length - 1; i++) {
const item = now.list[list[i]].value;
const item = now.list[list[i]]?.value;
if (!(item instanceof MotaSetting)) {
throw new Error(
`Cannot get setting. The parent isn't a MotaSetting instance.` +
@ -443,13 +443,9 @@ mainSetting
.register(
'ui',
'ui设置',
new MotaSetting().register(
'mapScale',
'小地图楼传缩放',
100,
COM.Number,
[50, 1000, 50]
)
new MotaSetting()
.register('mapScale', '小地图缩放', 100, COM.Number, [50, 1000, 50])
.setDisplayFunc('mapScale', value => `${value}%`)
);
const loading = Mota.require('var', 'loading');

View File

@ -78,7 +78,7 @@ export class GameStorage<T extends object = any> {
* @param key
*/
static fromGame(key: string) {
return `HumanBreak_${key}`;
return `${data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.firstData.name}_${key}`;
}
/**

View File

@ -21,7 +21,7 @@ interface HaloType {
};
}
interface EnemyInfo extends Partial<SelectType<Enemy, number | undefined>> {
interface EnemyInfo extends Partial<Enemy> {
atk: number;
def: number;
hp: number;
@ -68,7 +68,7 @@ interface CriticalDamageDelta extends Omit<DamageDelta, 'info'> {
atkDelta: number;
}
type HaloFn = (info: EnemyInfo, enemy: Enemy) => void;
type HaloFn = (info: EnemyInfo, enemy: EnemyInfo) => void;
export class EnemyCollection implements RangeCollection<DamageEnemy> {
floorId: FloorIds;
@ -165,13 +165,13 @@ export class EnemyCollection implements RangeCollection<DamageEnemy> {
if (!recursion) {
arr.forEach(v => {
enemys.forEach(e => {
e.injectHalo(v, enemy.enemy);
e.injectHalo(v, enemy.info);
});
});
} else {
enemys.forEach(e => {
arr.forEach(v => {
e.injectHalo(v, enemy.enemy);
e.injectHalo(v, enemy.info);
e.preProvideHalo();
});
});
@ -446,7 +446,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
/**
*
*/
injectHalo(halo: HaloFn, enemy: Enemy) {
injectHalo(halo: HaloFn, enemy: EnemyInfo) {
halo(this.info, enemy);
}

View File

@ -27,7 +27,7 @@ import type { specials } from './enemy/special';
import type { Range } from '@/plugin/game/range';
import type { KeyCode, ScanCode } from '@/plugin/keyCodes';
import type { Ref } from 'vue';
import type { MComponent, m, icon } from '@/core/main/layout';
import type { MComponent, m, MCGenerator } from '@/core/main/layout';
import type { addAnimate, removeAnimate } from '@/plugin/animateController';
import type { createSettingComponents } from '@/core/main/init/settings';
import type {
@ -72,6 +72,16 @@ import type * as _lodash from 'lodash-es';
import type * as _lzString from 'lz-string';
import type * as _mutateAnimate from 'mutate-animate';
import type * as _vue from 'vue';
import type * as UI from '@ui/index';
import type Box from '@/components/box.vue';
import type BoxAnimate from '@/components/boxAnimate.vue.vue';
import type Colomn from '@/components/colomn.vue.vue';
import type EnemyOne from '@/components/enemyOne.vue.vue';
import type Scroll from '@/components/scroll.vue.vue';
import type EnemyCritical from '@/panel/enemyCritical.vue';
import type EnemySpecial from '@/panel/enemySpecial.vue.vue';
import type EnemyTarget from '@/panel/enemyTarget.vue.vue';
import type KeyboardPanel from '@/panel/keyboard.vue.vue';
export interface ClassInterface {
// 渲染进程与游戏进程通用
@ -105,7 +115,6 @@ type _IHero = typeof hero;
export interface FunctionInterface extends _IBattle, _IHero {
// 定义于渲染进程,录像验证中会出错
m: typeof m;
icon: typeof icon;
unwrapBinary: typeof unwarpBinary;
checkAssist: typeof checkAssist;
isAssist: typeof isAssist;
@ -150,6 +159,19 @@ export interface ModuleInterface {
typeof statusBarTools &
typeof toolboxTools;
Damage: typeof Damage;
UI: typeof UI;
UIComponents: {
Box: typeof Box;
BoxAnimate: typeof BoxAnimate;
Colomn: typeof Colomn;
EnemyOne: typeof EnemyOne;
Scroll: typeof Scroll;
EnemyCritical: typeof EnemyCritical;
EnemySpecial: typeof EnemySpecial;
EnemyTarget: typeof EnemyTarget;
KeyboardPanel: typeof KeyboardPanel;
};
MCGenerator: typeof MCGenerator;
}
export interface SystemInterfaceMap {

View File

@ -1,11 +1,20 @@
import { loading } from '@/game/game';
let _shouldProcessKeyUp = true;
export function openShop(shopId: string, noRoute: boolean) {
const shop = core.status.shops[shopId] as ItemShopEvent;
const shop = core.status.shops[shopId] as any;
// Step 1: 检查能否打开此商店
if (!canOpenShop(shopId)) {
core.drawTip('该商店尚未开启');
return false;
}
// Step 2: (如有必要)记录打开商店的脚本事件
if (!noRoute && !shop.item) {
core.status.route.push('shop:' + shopId);
}
// Step 3: 检查道具商店 or 公共事件
if (shop.item) {
Mota.r(() => {
@ -17,9 +26,124 @@ export function openShop(shopId: string, noRoute: boolean) {
});
return;
}
if (shop.commonEvent) {
core.insertCommonEvent(shop.commonEvent, shop.args);
return;
}
_shouldProcessKeyUp = true;
// Step 4: 执行标准公共商店
core.insertAction(_convertShop(shop));
return true;
}
////// 将一个全局商店转变成可预览的公共事件 //////
function _convertShop(shop: any) {
return [
{
type: 'function',
function: "function() {core.addFlag('@temp@shop', 1);}"
},
{
type: 'while',
condition: 'true',
data: [
// 检测能否访问该商店
{
type: 'if',
condition: "core.isShopVisited('" + shop.id + "')",
true: [
// 可以访问,直接插入执行效果
{
type: 'function',
function:
"function() { core.plugin._convertShop_replaceChoices('" +
shop.id +
"', false) }"
}
],
false: [
// 不能访问的情况下:检测能否预览
{
type: 'if',
condition: shop.disablePreview,
true: [
// 不可预览,提示并退出
{ type: 'playSound', name: '操作失败' },
'当前无法访问该商店!',
{ type: 'break' }
],
false: [
// 可以预览:将商店全部内容进行替换
{
type: 'tip',
text: '当前处于预览模式,不可购买'
},
{
type: 'function',
function:
"function() { core.plugin._convertShop_replaceChoices('" +
shop.id +
"', true) }"
}
]
}
]
}
]
},
{
type: 'function',
function: "function() {core.addFlag('@temp@shop', -1);}"
}
];
}
function _convertShop_replaceChoices(shopId: string, previewMode: boolean) {
var shop = core.status.shops[shopId] as any;
var choices = (shop.choices || [])
.filter(function (choice: any) {
if (choice.condition == null || choice.condition == '') return true;
try {
return core.calValue(choice.condition);
} catch (e) {
return true;
}
})
.map(function (choice: any) {
var ableToBuy = core.calValue(choice.need);
return {
text: choice.text,
icon: choice.icon,
color:
ableToBuy && !previewMode
? choice.color
: [153, 153, 153, 1],
action:
ableToBuy && !previewMode
? [{ type: 'playSound', name: '商店' }].concat(
choice.action
)
: [
{ type: 'playSound', name: '操作失败' },
{
type: 'tip',
text: previewMode
? '预览模式下不可购买'
: '购买条件不足'
}
]
};
})
.concat({
text: '离开',
action: [{ type: 'playSound', name: '取消' }, { type: 'break' }]
});
core.insertAction({ type: 'choices', text: shop.text, choices: choices });
}
/// 是否访问过某个快捷商店
export function isShopVisited(id: string) {
flags.__shops__ ??= {};
@ -64,3 +188,109 @@ export function canUseQuickShop() {
return '当前楼层不能使用快捷商店。';
return null;
}
loading.once('coreInit', () => {
/// 允许商店X键退出
core.registerAction(
'keyUp',
'shops',
function (keycode) {
if (!core.status.lockControl || core.status.event.id != 'action')
return false;
if ((keycode == 13 || keycode == 32) && !_shouldProcessKeyUp) {
_shouldProcessKeyUp = true;
return true;
}
if (
!core.hasFlag('@temp@shop') ||
// @ts-ignore
core.status.event.data!.type != 'choices'
)
return false;
// @ts-ignore
var data = core.status.event.data.current;
var choices = data.choices;
// @ts-ignore
var topIndex = core.actions._getChoicesTopIndex(choices.length);
if (keycode == 88 || keycode == 27) {
// X, ESC
// @ts-ignore
core.actions._clickAction(
core._HALF_WIDTH_ || core.__HALF_SIZE__,
topIndex + choices.length - 1
);
return true;
}
return false;
},
60
);
/// 允许长按空格或回车连续执行操作
core.registerAction(
'keyDown',
'shops',
function (keycode) {
if (
!core.status.lockControl ||
!core.hasFlag('@temp@shop') ||
core.status.event.id != 'action'
)
return false;
// @ts-ignore
if (core.status.event.data.type != 'choices') return false;
// @ts-ignore
core.status.onShopLongDown = true;
// @ts-ignore
var data = core.status.event.data.current;
var choices = data.choices;
// @ts-ignore
var topIndex = core.actions._getChoicesTopIndex(choices.length);
if (keycode == 13 || keycode == 32) {
// Space, Enter
// @ts-ignore
core.actions._clickAction(
core._HALF_WIDTH_ || core.__HALF_SIZE__,
topIndex + core.status.event.selection
);
_shouldProcessKeyUp = false;
return true;
}
return false;
},
60
);
// 允许长按屏幕连续执行操作
core.registerAction(
'longClick',
'shops',
function (x, y, px, py) {
if (
!core.status.lockControl ||
!core.hasFlag('@temp@shop') ||
core.status.event.id != 'action'
)
return false;
// @ts-ignore
if (core.status.event.data.type != 'choices') return false;
// @ts-ignore
var data = core.status.event.data.current;
var choices = data.choices;
// @ts-ignore
var topIndex = core.actions._getChoicesTopIndex(choices.length);
if (
Math.abs(x - (core._HALF_WIDTH_ || core.__HALF_SIZE__)) <= 2 &&
y >= topIndex &&
y < topIndex + choices.length
) {
// @ts-ignore
core.actions._clickAction(x, y);
return true;
}
return false;
},
60
);
});

View File

@ -68,16 +68,14 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { computed, onUnmounted, ref, shallowRef } from 'vue';
import {
mainSetting,
MotaSetting,
MotaSettingItem,
SettingDisplayer,
SettingDisplayInfo,
SettingText
SettingDisplayInfo
} from '../core/main/setting';
import settingText from '../data/settings.json';
import { RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
import { splitText } from '../plugin/utils';
import Scroll from '../components/scroll.vue';
@ -88,13 +86,11 @@ import { mainUi } from '@/core/main/init/ui';
const props = defineProps<{
info?: MotaSetting;
text?: SettingText;
num: number;
ui: GameUi;
}>();
const setting = props.info ?? mainSetting;
const text = props.text ?? (settingText as SettingText);
const display = shallowRef<SettingDisplayInfo[]>([]);
const selectedItem = computed(() => display.value.at(-1)?.item);
const update = ref(false);