diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index df2593a..aecb0c6 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -29,9 +29,12 @@ export default defineConfig({
{
text: '深度指南',
items: [
+ { text: '快速开始', link: '/guide/quick-start' },
+
{ text: '差异说明', link: '/guide/diff' },
{ text: '系统说明', link: '/guide/system' },
{ text: '代码编写', link: '/guide/coding' },
+ { text: '音频系统', link: '/guide/audio' },
{
text: 'UI 系统',
collapsed: false,
@@ -40,10 +43,40 @@ export default defineConfig({
{ text: 'UI 优化', link: '/guide/ui-perf' },
{ text: 'UI 系统', link: '/guide/ui-system' },
{ text: 'UI 元素', link: '/guide/ui-elements' },
- { text: 'UI 常见问题', link: '/guide/ui-faq' }
+ { text: 'UI 常见问题', link: '/guide/ui-faq' },
+ { text: '未来规划', link: '/guide/ui-future' }
]
},
- { text: '音频系统', link: '/guide/audio' }
+ {
+ text: '常见需求指南',
+ collapsed: false,
+ items: [
+ {
+ text: '快速浏览',
+ link: '/guide/implements/index'
+ },
+ {
+ text: '怪物特殊属性',
+ link: '/guide/implements/special'
+ },
+ {
+ text: '修改状态栏',
+ link: '/guide/implements/status-bar'
+ },
+ {
+ text: '新增 UI',
+ link: '/guide/implements/new-ui'
+ },
+ {
+ text: '主动技能',
+ link: '/guide/implements/skill'
+ },
+ {
+ text: '自定义按键',
+ link: '/guide/implements/hotkey'
+ }
+ ]
+ }
]
}
],
@@ -110,6 +143,7 @@ export default defineConfig({
}
},
vite: {
+ // @ts-expect-error 类型错误
plugins: [MermaidPlugin()],
optimizeDeps: {
include: ['mermaid']
diff --git a/docs/api/motajs-client/index.md b/docs/api/motajs-client/index.md
index 5c5b673..3ea7788 100644
--- a/docs/api/motajs-client/index.md
+++ b/docs/api/motajs-client/index.md
@@ -2,12 +2,13 @@
`@motajs/client` 包含多个模块:
-- [`@motajs/client-base`](../motajs-client-base/)
+- [`@motajs/client-base`](../motajs-client-base/)
示例:
```ts
import { KeyCode } from '@motajs/client';
-const { KeyCOde } = Mota.require('@motajs/client');
+// 等价于
+const { KeyCode } = Mota.require('@motajs/client');
```
diff --git a/docs/api/motajs-render-vue/标签 container.md b/docs/api/motajs-render-vue/标签 container.md
index 3a7e0b4..59ef047 100644
--- a/docs/api/motajs-render-vue/标签 container.md
+++ b/docs/api/motajs-render-vue/标签 container.md
@@ -127,7 +127,7 @@ return () => (
### 大数据量处理方案
-考虑使用[滚动条](../user-client-modules/组件 Scroll.md)或[分页](../user-client-modules/组件 Page.md)组件。
+考虑使用[滚动条](../user-client-modules/组件%20Scroll.md)或[分页](../user-client-modules/组件%20Page.md)组件。
---
diff --git a/docs/api/motajs-render-vue/标签 text.md b/docs/api/motajs-render-vue/标签 text.md
index 1438705..8b4efd7 100644
--- a/docs/api/motajs-render-vue/标签 text.md
+++ b/docs/api/motajs-render-vue/标签 text.md
@@ -156,5 +156,5 @@ import { Font } from '@motajs/render-style';
## 注意事项
-1. 如果需要显示多行文本,考虑使用 [TextContent](../user-client-modules/组件 TextContent.md)
+1. 如果需要显示多行文本,考虑使用 [TextContent](../user-client-modules/组件%20TextContent.md)
2. 考虑到浏览器兼容性,不建议在颜色中填写一些新标准的语法,例如 `rgb(0.3, 0.6, 0.8 / 0.6)` `#rgba` 等
diff --git a/docs/api/motajs-render/index.md b/docs/api/motajs-render/index.md
index 0bb0fed..f4012da 100644
--- a/docs/api/motajs-render/index.md
+++ b/docs/api/motajs-render/index.md
@@ -2,15 +2,15 @@
此模块包含如下模块的内容,可以直接引用:
-- [@motajs/render-core](../motajs-render-core/)
-- [@motajs/render-elements](../motajs-render-elements/)
-- [@motajs/render-style](../motajs-render-style/)
-- [@motajs/render-vue](../motajs-render-vue/)
+- [@motajs/render-core](../motajs-render-core/)
+- [@motajs/render-elements](../motajs-render-elements/)
+- [@motajs/render-style](../motajs-render-style/)
+- [@motajs/render-vue](../motajs-render-vue/)
引入示例:
```ts
import { Container } from '@motajs/render';
-// 二者等价,不需要单独使用一个量来接收,注意与 @motajs/client 引入方式区分
+// 二者等价,不需要单独使用一个量来接收
import { Container } from '@motajs/render-core';
```
diff --git a/docs/api/motajs-system-action/Hotkey.md b/docs/api/motajs-system-action/Hotkey.md
index 60f600d..64bbdb5 100644
--- a/docs/api/motajs-system-action/Hotkey.md
+++ b/docs/api/motajs-system-action/Hotkey.md
@@ -34,9 +34,9 @@ graph LR
function constructor(id: string, name: string): Hotkey;
```
-- **参数**
- - `id`: 控制器的唯一标识符
- - `name`: 控制器的显示名称
+- **参数**
+ - `id`: 控制器的唯一标识符
+ - `name`: 控制器的显示名称
**示例**
@@ -56,7 +56,7 @@ function register(data: RegisterHotkeyData): this;
注册一个按键配置。
-- **参数**
+- **参数**
```typescript
interface RegisterHotkeyData {
id: string; // 按键唯一标识(可含数字后缀,如 "copy_1")
@@ -89,10 +89,10 @@ function realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig): this;
为按键绑定触发逻辑。
-- **参数**
- - `id`: 目标按键 ID(无需后缀)
- - `func`: 触发时执行的函数
- - `config`: 触发类型配置(节流/超时等)
+- **参数**
+ - `id`: 目标按键 ID(无需后缀)
+ - `func`: 触发时执行的函数
+ - `config`: 触发类型配置(节流/超时等)
**示例**
@@ -116,10 +116,10 @@ function group(id: string, name: string, keys?: RegisterHotkeyData[]): this;
创建按键分组,后续注册的按键自动加入该组。
-- **参数**
- - `id`: 分组唯一标识
- - `name`: 分组显示名称
- - `keys`: 可选,预注册的按键列表
+- **参数**
+ - `id`: 分组唯一标识
+ - `name`: 分组显示名称
+ - `keys`: 可选,预注册的按键列表
---
@@ -131,11 +131,11 @@ function set(id: string, key: KeyCode, assist: number, emit?: boolean): void;
动态修改按键绑定。
-- **参数**
- - `id`: 目标按键 ID
- - `key`: 新按键代码
- - `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2)
- - `emit`: 是否触发 `set` 事件(默认 `true`)
+- **参数**
+ - `id`: 目标按键 ID
+ - `key`: 新按键代码
+ - `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2)
+ - `emit`: 是否触发 `set` 事件(默认 `true`)
---
@@ -147,8 +147,8 @@ function when(fn: () => boolean): this;
为当前作用域的按键绑定添加触发条件。
-- **参数**
- - `fn`: 条件函数,返回 `true` 时允许触发按键逻辑
+- **参数**
+ - `fn`: 条件函数,返回 `true` 时允许触发按键逻辑
**示例**
@@ -190,8 +190,8 @@ function use(symbol: symbol): void;
切换当前作用域,后续 `realize` 方法绑定的逻辑将关联到该作用域。
-- **参数**
- - `symbol`: 唯一作用域标识符
+- **参数**
+ - `symbol`: 唯一作用域标识符
---
@@ -203,8 +203,8 @@ function dispose(symbol?: symbol): void;
释放指定作用域及其绑定的所有按键逻辑。
-- **参数**
- - `symbol`(可选): 要释放的作用域(默认释放当前作用域)
+- **参数**
+ - `symbol`(可选): 要释放的作用域(默认释放当前作用域)
**示例**
@@ -230,13 +230,13 @@ function emitKey(
手动触发按键事件(可用于模拟按键操作)。
-- **参数**
- - `key`: 按键代码
- - `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2)
- - `type`: 事件类型(`'up'` 或 `'down'`)
- - `ev`: 原始键盘事件对象
-- **返回值**
- `true` 表示事件被成功处理,`false` 表示无匹配逻辑
+- **参数**
+ - `key`: 按键代码
+ - `assist`: 辅助键状态(二进制位:`Ctrl=1<<0`, `Shift=1<<1`, `Alt=1<<2`)
+ - `type`: 事件类型(`'up'` 或 `'down'`)
+ - `ev`: 原始键盘事件对象
+- **返回值**
+ `true` 表示事件被成功处理,`false` 表示无匹配逻辑
**示例**
@@ -276,7 +276,7 @@ function get(id: string): Hotkey | undefined;
**事件监听示例**
```typescript
-editorHotkey.on('emit', (key, assist) => {
+gameKey.on('emit', (key, assist) => {
console.log(`按键 ${KeyCode[key]} 触发,辅助键状态:${assist}`);
});
```
diff --git a/docs/guide/implements/hotkey.md b/docs/guide/implements/hotkey.md
new file mode 100644
index 0000000..c439e10
--- /dev/null
+++ b/docs/guide/implements/hotkey.md
@@ -0,0 +1,256 @@
+# 自定义按键
+
+在 2.B 中新增按键很方便,且可以为你的 UI 单独配置按键信息,玩家也可以修改快捷键设置。
+
+下面以设置一个全局的切换技能按键为例,展示如何新增一个按键。
+
+:::warning
+按键系统在未来可能会有小幅重构,但逻辑不会大幅变动。
+:::
+
+## 定义按键信息
+
+我们打开 `packages-user/client-modules/src/action/hotkey.ts`,找到 `//#region 按键实现` 分段,在分段前可以看到一个 `// #endregion`,然后我们在上面的一系列 `register` 后面新增:
+
+```ts {6-17}
+gameKey
+ .group(/* ... */)
+ .register({})
+ // ... 原有内容,注意原本内容最后的分号别忘了删除
+
+ //#region 主动技能
+ // 分组,这样既可以方便管理,也可以让玩家设置按键时直接根据分组设置
+ .group('skill', '主动技能')
+ // 注册按键信息
+ .register({
+ // 按键的 id
+ id: '@skill_doubleAttack',
+ // 按键显示的名称
+ name: '二倍斩',
+ // 默认按键,数字 1(非小键盘)
+ defaults: KeyCode.Digit1
+ });
+```
+
+此时我们打开游戏,按下 `Esc`,选择 `系统设置->操作设置->自定义按键`,就可以看到我们新增的按键信息了,不过现在按它没有任何作用,因为我们只是定义了按键,还没有编写它的触发效果。
+
+## 实现按键效果
+
+我们回到 `hotkey.ts`,翻到文件最后,在最后几行的上面会有一系列 `realize`,这就是实现按键操作的地方,我们在后面新增一个 `@skill_doubleAttack` 的实现:
+
+```ts {6-10}
+gameKey
+ .when(/* ... */)
+ .realize(/* ... */)
+ // ... 原有内容
+
+ // 实现刚刚定义的按键
+ .realize('@skill_doubleAttack', () => {
+ // 切换技能
+ toggleSkill();
+ });
+```
+
+## 拓展-添加辅助按键
+
+如果我们需要一个按键默认情况下需要按下 `Ctrl` 时才能触发,例如 `Ctrl+A`,我们可以这么写:
+
+```ts
+gameKey.register({
+ id: '@skill_doubleAttack',
+ name: '二倍斩',
+ defaults: KeyCode.Digit1,
+ // 设置 ctrl 属性为 true 即可,包括 alt 和 shift 也是一样
+ ctrl: true // [!code ++]
+});
+```
+
+## 拓展-在 UI 内实现按键
+
+有时候,我们需要在一个 UI 界面中提供按键操作支持,样板提供了专门的接口来实现这一点。
+
+### 定义按键信息
+
+与[这一节](#定义按键信息)相同,直接定义按键信息即可:
+
+```ts
+gameKey
+ //#region 自定义UI
+ .group('@ui_myUI', '自定义UI')
+ .register({
+ id: '@myUI_key',
+ name: '自定义UI',
+ // 默认使用 H 键
+ defaults: KeyCode.KeyH
+ });
+```
+
+### 在 UI 内实现按键操作
+
+按键实现方式略有变动,我们需要使用 `useKey` 接口来实现按键。假设我们在 `packages-user/client-modules/src/render/ui` 文件夹下编写 UI,那么可以这么写:
+
+```tsx {7-13}
+// 引入 useKey 接口
+// 文件在 packages-user/client-modules/src/render/utils/use.ts,注意路径关系
+import { useKey } from '../utils/use'; // [!code ++]
+
+// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
+export const MyCom = defineComponent(props => {
+ // 调用 useKey
+ const [key] = useKey();
+ // 直接开始实现,本例按键效果为显示一个提示
+ key.realize('@myUI_key', () => {
+ // 调用 drawTip 显示提示
+ core.drawTip('这是一个提示');
+ });
+
+ return () => ;
+});
+```
+
+### 通用按键复用
+
+我们会有一些通用按键,例如确认、关闭,这些按键我们不希望每个 UI 或场景都定义一遍,一来写代码不方便,二来玩家如果要自定义的话需要每个界面都设置一遍,很麻烦。此时我们建议按键复用。与一般的按键一致,我们直接实现 `exit` `confirm` 等按键即可,不需额外操作:
+
+```tsx {12-21}
+import { useKey } from '../utils/use';
+
+// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
+export const MyCom = defineComponent(props => {
+ // 调用 useKey
+ const [key] = useKey();
+ // 直接开始实现,本例按键效果为显示一个提示
+ key.realize('@myUI_key', () => {
+ // 调用 drawTip 显示提示
+ core.drawTip('这是一个提示');
+ })
+ // 关闭操作
+ .realize('exit', () => {
+ // 调用关闭函数
+ props.controller.close(props.instance);
+ })
+ // 确认操作
+ .realize('confirm', () => {
+ // 弹出提示说明按下了确认键
+ core.drawTip('按下了确认键!');
+ });
+
+ return () => ;
+});
+```
+
+实际上,你甚至可以在一个 UI 中实现另一个 UI 定义的按键,虽然这么做非常离谱。
+
+## 拓展-单功能多按键
+
+在游戏中可以发现退出、确认等功能可以设定多个按键,为了实现这种按键,我们只需要在定义按键时加上 `_num` 后缀即可,例如:
+
+```ts {4,10,16}
+gameKey
+ .register({
+ // 添加 _1 后缀
+ id: '@skill_doubleAttack_1',
+ name: '二倍斩',
+ defaults: KeyCode.Digit1
+ })
+ .register({
+ // 添加 _2 后缀
+ id: '@skill_doubleAttack_2',
+ name: '二倍斩',
+ defaults: KeyCode.Digit2
+ })
+ .register({
+ // 添加 _3 后缀
+ id: '@skill_doubleAttack_3',
+ name: '二倍斩',
+ defaults: KeyCode.Digit3
+ });
+```
+
+这样,在自定义按键界面就会显示为可以自定义三个按键。而在实现时,我们不需要添加后缀:
+
+```ts {2}
+// 这里不需要添加后缀!
+gameKey.realize('@skill_doubleAttack', () => {
+ toggleSkill();
+});
+```
+
+或者,添加后缀的话,会精确匹配到对应后缀的按键:
+
+```ts {2}
+// 只有按下 @skill_doubleAttack_1 对应的按键才会触发,而 @skill_doubleAttack_2 等不会触发!
+gameKey.realize('@skill_doubleAttack_1', () => {
+ toggleSkill();
+});
+```
+
+## 拓展-按下时触发
+
+默认情况下,我们实现的按键都是在按键抬起时触发,如果我们需要按下时触发,我们需要在调用 `realize` 函数时额外传入一个配置项:
+
+:::code-group
+
+```ts [down]
+gameKey.realize(
+ '@skill_doubleAttack',
+ () => {
+ toggleSkill();
+ },
+ // 按下时单次触发
+ { type: 'down' } // [!code ++]
+);
+```
+
+```ts [down-repeat]
+gameKey.realize(
+ '@skill_doubleAttack',
+ () => {
+ toggleSkill();
+ },
+ // 按下时持续触发
+ { type: 'down-repeat' } // [!code ++]
+);
+```
+
+```ts [down-throttle]
+gameKey.realize(
+ '@skill_doubleAttack',
+ () => {
+ toggleSkill();
+ },
+ // 按下时节流触发,节流间隔为 100ms
+ { type: 'down-throttle', throttle: 100 } // [!code ++]
+);
+```
+
+```ts [down-timeout]
+gameKey.realize(
+ '@skill_doubleAttack',
+ () => {
+ toggleSkill();
+ },
+ // 按下时延迟触发,延迟 1000ms
+ { type: 'down-timeout', timeout: 1000 } // [!code ++]
+);
+```
+
+:::
+
+这里的 `type` 可以填这些值:
+
+- `up`: 抬起时触发,默认就是它。
+- `down`: 按下时触发,只触发一次。
+- `down-repeat`: 按下时触发,且会重复触发。这一操作可能会与键盘或系统设置有关,一般来说首次触发后会有 `500ms` 的延时,然后每帧触发一次。
+- `down-throttle`: 按下时节流触发,在 `down-repeat` 的基础上,每隔一段时间才会触发一次,例如可以设定为 `100ms` 触发一次。
+- `down-timeout`: 按下后延迟触发,会在按下后延迟一段时间触发。
+
+## 拓展-样板为什么不会在 UI 中触发全局按键?
+
+这是按键系统最实用的功能之一,这个功能允许我们在 UI 中不会触发全局按键,例如在怪物手册中不会触发打开楼传,也不会触发打开系统菜单。你可能会好奇,我们在上面的讲述中似乎并没有哪一行执行了这一操作,那么是如何实现的呢?
+
+实际上,按键系统内部有一个栈,而我们调用 `useKey` 时就会自动创建一个新的作用域,同时在关闭 UI 时释放作用域。这样的话,我们在打开 UI 时,按键实现就会遵循新创建的作用域,关闭时自动回到上一层,这就实现了上述功能。
+
+## 拓展-API 参考
+
+[Hotkey API 文档](../../api/motajs-system-action/Hotkey.md)
diff --git a/docs/guide/implements/index.md b/docs/guide/implements/index.md
new file mode 100644
index 0000000..33c9fb6
--- /dev/null
+++ b/docs/guide/implements/index.md
@@ -0,0 +1,17 @@
+# 常见需求的实现指南
+
+一般情况下,我们只需要在 `packages-user` 文件夹下编写代码。而在此文件夹中,多数文件夹又是样板处理,我们基本只会在这些文件夹中编写:
+
+- `packages-user/client-modules`:客户端代码
+- `packages-user/data-state`:数据端代码
+
+## 客户端内容
+
+- [修改状态栏显示](./status-bar.md)
+- [编写新 UI](./new-ui.md)
+- [自定义按键](./hotkey.md)
+
+## 数据端内容
+
+- [怪物伤害计算](./damage.md)
+- [主动技能](./skill.md)
diff --git a/docs/guide/implements/new-ui.md b/docs/guide/implements/new-ui.md
new file mode 100644
index 0000000..732a014
--- /dev/null
+++ b/docs/guide/implements/new-ui.md
@@ -0,0 +1,115 @@
+# 编写新 UI
+
+在 `packages-user/client-modules/src/render/ui` 文件夹下创建一个新的 UI 文件,编写完 UI 后在你需要打开此 UI 的地方调用 `mainUIController.open` 即可。
+
+## UI 模板
+
+UI 编写模板如下:
+
+```tsx
+// 引入必要接口
+import { defineComponent } from 'vue';
+import { GameUI, UIComponentProps, DefaultProps } from '@motajs/system-ui';
+import { SetupComponentOptions } from '../components';
+
+// 定义组件的参数
+export interface MyComProps extends UIComponentProps, DefaultProps {}
+
+// 定义组件的参数,需要传递给 vue
+const myComProps = {
+ // 这两个参数不能少
+ props: ['controller', 'instance']
+} satisfies SetupComponentOptions;
+
+// 定义组件内容
+export const MyCom = defineComponent(props => {
+ // 在这里编写你的 UI 即可
+ return () => ;
+}, myComProps);
+
+// 定义 UI 对象
+export const MyUI = new GameUI('my-ui', MyCom);
+```
+
+## 打开 UI
+
+在需要打开 UI 的地方调用:
+
+```ts
+// 在 client-modules 模块外引入
+import { mainUIController } from '@user/client-modules';
+// 在 client-modules 模块内引入
+// 应该从 client-modules/src/render/ui/controller.tsx 中引入,自行根据路径关系引入,或者使用 vscode 的自动补全时会自动帮你引入
+import { mainUIController } from './ui/controller';
+// 引入你自己的 UI
+import { MyUI } from './myUI';
+
+// 在需要打开时调用,第二个参数为传递给 UI 的参数,即 Props
+mainUIController.open(MyUI, {});
+```
+
+如果需要在 UI 内打开 UI,推荐使用如下方式:
+
+```tsx
+export const MyCom = defineComponent(props => {
+ // 使用 props.controller,适配不同 UI 控制器
+ props.controller.open(MyUI2, {});
+ return () => ;
+}, myComProps);
+```
+
+## 关闭 UI
+
+在 UI 内关闭自身使用:
+
+```tsx
+export const MyCom = defineComponent(props => {
+ // 关闭自身
+ props.controller.close(props.instance);
+ return () => ;
+}, myComProps);
+```
+
+而如果在 UI 外关闭的话,需要接受 `controller.open` 的返回值:
+
+```ts
+// 接收返回值
+const ins = controller.open(MyUI, {});
+
+// 关闭此 UI
+controller.close(ins);
+```
+
+## UI 编写参考
+
+参考[此文档](../ui.md),此文档将会教你如何从头开始编写一个 UI,并解释 UI 运行与渲染的基本逻辑。
+
+## 拓展-UI 与组件的区别
+
+UI 包含 `controller` `instance` 两个参数,且必须通过 UI 控制器打开,而组件不包含这两个参数,不能由 UI 控制器打开,需要作为组件或 UI 内的组件调用(类似于标签)。可以自行阅读样板自带 UI 与组件,来理解二者的区别。
+
+或者用模块的角度来说,组件是函数,而 UI 是一整个模块,函数可以调用函数,而自然组件也可以调用组件。UI 由组件和元素构成,就像模块可以由函数和变量构成。
+
+除此之外,组件不能被定义为 `GameUI`,只有 UI 可以。例如:
+
+```tsx
+// 一个没有 controller, instance 的组件
+export const MyComponent = defineComponent(() => {
+ return () => ;
+});
+
+// 这是不行的!同时 ts 也会为你贴心报错!
+export const MyComponentUI = new GameUI('my-component', MyComponent);
+
+// --------------------
+
+interface MyComProps extends DefaultProps, UIComponentProps {}
+
+// 一个包含 controller, instance 的组件,此处省略 myComProps 定义
+export const MyCom = defineComponent(props => {
+ return () => ;
+}, myComProps);
+
+// 这是可以的,可以被 UIController 打开!
+export const MyComUI = new GameUI('my-com', MyCom);
+```
diff --git a/docs/guide/implements/skill.md b/docs/guide/implements/skill.md
new file mode 100644
index 0000000..0dabfa9
--- /dev/null
+++ b/docs/guide/implements/skill.md
@@ -0,0 +1,346 @@
+# 主动技能
+
+在数据端新增一个文件定义技能开启与关闭的行为,然后在数据端处理录像,最后处理交互。
+
+对于键盘,在 `packages-user/client-modules/src/action/hotkey.ts` 中自定义技能按键,并在此处实现。
+
+对于触屏和鼠标,在 `packages-user/client-modules/src/render/ui/statusBar.tsx` 中提供技能按钮。
+
+下面以技能“二倍斩”为例,展示如何实现主动技能。
+
+## 技能开启关闭行为
+
+在 `packages-user/data-state/src/mechainism` 文件夹下新增一个文件 `skill.ts`,然后打开同一文件夹下的 `index.ts`,写入 `export * from './skill.ts';`。回到 `skill.ts`,开始编写技能的开启与关闭行为。
+
+由于二倍斩技能本质上是修改战斗函数,因此我们只需要一个变量来存储当前是否开启了技能即可。因此写出如下内容:
+
+```ts
+/** 二倍斩技能是否已经开启 */
+let skill1 = false;
+
+/** 开启二倍斩技能 */
+export function enableSkill1() {
+ // 将变量设为 true
+ skill1 = true;
+ // 更新状态栏
+ core.updateStatusBar();
+}
+
+/** 关闭二倍斩技能 */
+export function disableSkill1() {
+ skill1 = false;
+ core.updateStatusBar();
+}
+
+/** 获取二倍斩技能是否已经开启 */
+export function getSkill1Enabled() {
+ return skill1;
+}
+
+/** 切换二倍斩技能,如果开启则关闭,否则开启 */
+export function toggleSkill1() {
+ if (skill1) disableSkill1();
+ else enableSkill1();
+}
+```
+
+## 修改伤害计算
+
+打开 `packages-user/data-state/src/enemy/damage.ts`,在最后找到 `calDamageWith` 函数,在里面修改勇士的 `heroPerDamage` 即可:
+
+```ts {12-14}
+// 文件开头引入刚刚编写的 skill.ts,可以使用自动补全自动引入
+import { getSkill1Enabled } from '../machanism/skill'; // [!code ++]
+
+export function calDamageWith(
+ info: UserEnemyInfo,
+ hero: Partial
+): number | null {
+ // ... 原有内容
+
+ // 在特定位置将勇士伤害乘以 2
+ // 注意需要再回合计算前乘,不然没有效果
+ if (getSkill1Enabled()) {
+ heroPerDamage *= 2;
+ }
+
+ // ... 原有内容
+}
+```
+
+## 录像处理
+
+录像处理其实很简单,我们只需要简单修改我们刚刚编写的几个函数,并注册一个新录像即可。
+
+我们先在 `skill.ts` 中编写一个 `createSkill` 函数,注册录像行为:
+
+```ts
+export function createSkill() {
+ // 样板接口,注册录像行为
+ core.registerReplayAction('skill1', action => {
+ // action 可能是 skill1:1 或者 skill1:0
+ // 前者表示开启技能,后者表示关闭
+ if (!action.startsWith('skill1:')) return;
+ // 获取应该开启还是关闭
+ const [, param] = action.split(':');
+ const enable = parseInt(param) === 1;
+ // 执行开启或关闭行为
+ // 由于是在同一个文件,因此是不需要引入的
+ if (enable) enableSkill1();
+ else disableSkill1();
+
+ // 这一句不能少
+ core.replay();
+ });
+}
+```
+
+然后我们再次进入 `index.ts`,在 `createMechanism` 函数中调用 `createSkill`:
+
+```ts
+import { createSkill } from './skill'; // [!code ++]
+
+export function createMechanism() {
+ // ... 原有内容
+ createSkill(); // [!code ++]
+}
+```
+
+最后简单修改一下 `enableSkill1` 和 `disableSkill1` 即可:
+
+```ts
+/** 开启二倍斩技能 */
+export function enableSkill1() {
+ skill1 = true;
+ core.updateStatusBar();
+ // 将开启技能行为计入录像
+ core.status.route.push('skill1:1'); // [!code ++]
+}
+
+/** 关闭二倍斩技能 */
+export function disableSkill1() {
+ skill1 = false;
+ core.updateStatusBar();
+ // 将关闭技能行为计入录像
+ core.status.route.push('skill1:0'); // [!code ++]
+}
+```
+
+## 按键交互与点击交互
+
+按键交互参考[此文档](./hotkey.md)
+
+点击交互参考[此文档](./status-bar.md#拓展-可交互按钮)
+
+最终实现参考(按键和点击):
+
+:::code-group
+
+```ts [按键]
+// 引入刚刚编写的函数
+import { toggleSkill1 } from '@user/data-state';
+
+gameKey
+ // 按键分组
+ .group('skill', '主动技能')
+ // 按键注册
+ .register({
+ id: 'skill1',
+ name: '二倍斩',
+ defaults: KeyCode.Digit1
+ });
+
+// 按键实现
+gameKey.realize('skill1', toggleSkill1);
+```
+
+```tsx [点击]
+// 引入刚刚编写的函数
+import { toggleSkill1 } from '@user/data-state'; // [!code ++]
+
+// 在状态栏新增
+export const LeftStatusBar = defineComponent>(
+ p => {
+ return () => (
+
+ {/* ... 原有内容 */}
+
+ {/* 新增一个 text 标签,点击时执行 toggleSkill1 切换技能 */}
+
+
+ );
+ }
+);
+```
+
+:::
+
+## 拓展-多技能设计思路
+
+很多时候我们可能会有多个技能,且多个技能间互斥,即每次只能开启一个技能,这时候如果我们给每个技能都单独编写一套 `enable` `disable` `toggle` 会显得很啰嗦,也不易维护。
+
+### 枚举定义
+
+此时我们可以考虑使用枚举方式来定义:
+
+```ts
+export const enum SkillType {
+ None, // 未开启技能
+ DoubleAttack, // 二倍斩
+ TripleAttack // 三倍斩
+ // ... 其他技能
+}
+```
+
+### 修改开启关闭行为
+
+然后给 `enable` 系列函数添加一个参数,来指定开启某个技能:
+
+```ts
+/** 当前开启了什么技能 */
+let enabled: SkillType = SkillType.None;
+
+export function enableSkill(skill: SkillType) {
+ // 如果要开启的和当前技能一致,则不做任何事
+ if (enabled === skill) return;
+ // 否则切换当前技能
+ enabled = skill;
+ // 更新状态栏
+ core.updateStatusBar();
+ // 计入录像,直接计入当前开启了什么技能
+ core.status.route.push(`skill:${skill}`);
+}
+
+export function disableSkill() {
+ // 关闭技能相当于切换至无技能
+ enableSkill(SkillType.None);
+}
+
+export function toggleSkill(skill: SkillType) {
+ // 改为判断是否与当前技能一致,一致则关闭,否则切换至目标技能
+ if (enabled === skill) disableSkill();
+ else enableSkill(skill);
+}
+
+export function getEnabledSkill() {
+ return enabled;
+}
+```
+
+### 技能判断
+
+在其他地方直接判断当前技能,可以使用 `if` 或 `switch`:
+
+:::code-group
+
+```ts [if判断]
+import { getEnabledSkill, SkillType } from './skill';
+
+export function calDamageWith(
+ info: UserEnemyInfo,
+ hero: Partial
+): number | null {
+ // ... 原有内容
+
+ // 获取当前开启了什么技能
+ const enabled = getEnabledSkill();
+ // 使用 if 判断
+ if (enabled === SkillType.DoubleAttack) heroPerDamage *= 2;
+ else if (enabled === SkillType.TripleAttack) heroPerDamage *= 3;
+
+ // ... 原有内容
+}
+```
+
+```ts [switch判断]
+import { getEnabledSkill, SkillType } from './skill';
+
+export function calDamageWith(
+ info: UserEnemyInfo,
+ hero: Partial
+): number | null {
+ // ... 原有内容
+
+ // 获取当前开启了什么技能
+ const enabled = getEnabledSkill();
+ // 使用 switch 判断
+ switch (enabled) {
+ case SkillType.DoubleAttack:
+ heroPerDamage *= 2;
+ break;
+ case SkillType.TripleAttack:
+ heroPerDamage *= 3;
+ break;
+ }
+
+ // ... 原有内容
+}
+```
+
+:::
+
+### 录像处理
+
+录像直接改为开启目标技能即可:
+
+```ts
+export function createSkill() {
+ // 样板接口,注册录像行为
+ core.registerReplayAction('skill', action => {
+ if (!action.startsWith('skill:')) return;
+ // 获取应该开启的技能
+ const [, param] = action.split(':');
+ const skill = parseInt(param);
+ // 开启目标技能,由于关闭技能就是开启 SkillType.None,因此这里直接这么写就行
+ enableSkill(skill);
+
+ // 这一句不能少
+ core.replay();
+ });
+}
+```
+
+## 拓展-战后自动关闭技能
+
+可以使用战后的钩子实现,写在 `createSkill` 函数中,具体实现方式如下:
+
+```ts {7-10}
+import { hook } from '@user/data-base';
+
+export function createSkill() {
+ // ... 原有内容
+
+ // 战后钩子,会在战后自动执行
+ hook.on('afterBattle', () => {
+ // 战后直接关闭技能即可
+ disableSkill();
+ });
+}
+```
+
+## 拓展-在开启或关闭技能时执行内容
+
+直接在 `enableSkill` 里面编写即可,如果是单技能,那么直接编写内容,否则需要判断:
+
+```ts {5-15}
+export function enableSkill(skill: SkillType) {
+ // ... 原有内容
+
+ // 使用 switch 判断
+ switch (skill) {
+ case SkillType.None:
+ // 显示提示
+ core.drawTip('已关闭技能!');
+ break;
+ case SkillType.DoubleAttack:
+ // 显示提示
+ core.drawTip('已开启二倍斩!');
+ break;
+ // ... 其他判断
+ }
+}
+```
diff --git a/docs/guide/implements/special.md b/docs/guide/implements/special.md
new file mode 100644
index 0000000..4b1022a
--- /dev/null
+++ b/docs/guide/implements/special.md
@@ -0,0 +1,288 @@
+# 怪物特殊属性
+
+下面以特殊属性“勇士造成的伤害减少 10%”为例,展示如何自定义一个仅修改伤害计算的特殊属性。
+
+:::warning
+伤害计算将会在 2.C 中重构,不过逻辑并不会有大幅变动。
+:::
+
+## 编写属性定义
+
+打开 `packages-user/data-state/src/enemy/special.ts`,在最后添加一个属性定义:
+
+```ts
+export const specials: SpecialDeclaration[] = [
+ // ... 原有属性定义
+
+ // 自定义属性
+ {
+ code: 30, // 特殊属性代码,用于 hasSpecial 判断 // [!code ++]
+ name: '自定义特殊属性', // 特殊属性名称 // [!code ++]
+ desc: '勇士对该怪物造成的伤害减少 10%', // 特殊属性描述 // [!code ++]
+ color: '#ffd' // 特殊属性显示的颜色 // [!code ++]
+ }
+];
+```
+
+## 实现特殊属性
+
+打开 `packages-user/data-state/src/enemy/damage.ts`,在文件最后的 `calDamageWith` 函数中编写:
+
+```ts
+export function calDamageWith(
+ info: UserEnemyInfo,
+ hero: Partial
+): number | null {
+ // ... 原有内容
+
+ // 在需要降低勇士伤害的地方将勇士伤害乘以 0.9 即可
+ // 注意需要再回合计算前乘,不然没有效果
+ heroPerDamage *= 0.9; // [!code ++]
+
+ // ... 原有内容
+}
+```
+
+## 拓展-用函数声明属性
+
+特殊属性的属性名和介绍可以使用函数来声明,这允许属性名和描述根据怪物属性变化。下面我们以特殊属性“勇士伤害减少`n%`”为例,展示如何声明这种类型的属性。
+
+### 编辑表格
+
+首先我们打开编辑器,选中任意一个怪物,在左侧属性栏上方找到`编辑表格`,然后点击它打开,找到`【怪物】相关的表格配置`,在 `_data` 属性下仿照攻击或其他属性新增一项,注意不要忘记了逗号:
+
+```js {4-10}
+"enemys": {
+ "_data": {
+ // 属性名为 myAttr
+ "myAttr": {
+ "_leaf": true,
+ "_type": "textarea",
+ // 属性说明
+ "_docs": "伤害减免",
+ "_data": "伤害减免"
+ },
+ }
+}
+```
+
+### 类型声明
+
+然后打开 `src/types/declaration/event.d.ts`,找到开头的 `type PartialNumbericEnemyProperty =`,在后面新增一行:
+
+```ts
+type PartialNumbericEnemyProperty =
+ | 'value'
+ // ... 其他属性声明
+
+ // 新增自己的 myAttr 属性
+ // 注意不要忘记删除前一行最后的分号
+ | 'myAttr'; // [!code ++]
+```
+
+### 属性定义
+
+最后在 `special.ts` 中新增属性定义即可:
+
+```ts
+export const specials: SpecialDeclaration[] = [
+ // ... 原有属性定义
+
+ // 自定义属性
+ {
+ code: 30, // 特殊属性代码,用于 hasSpecial 判断
+ name: enemy => `${enemy.myAttr ?? 0}%减伤`, // 特殊属性名称 // [!code ++]
+ desc: enemy => `勇士对该怪物造成的伤害减少${enemy.myAttr ?? 0}%`, // 特殊属性描述 // [!code ++]
+ color: '#ffd' // 特殊属性显示的颜色
+ }
+];
+```
+
+此时,如果给怪物的 `myAttr` 栏填写 `10`,那么特殊属性名称就会显示 `10%减伤`,属性描述会显示 `勇士对该怪物造成的伤害减少10%`。
+
+### 属性实现
+
+修改 `damage.ts` `calDamageWith` 中的实现:
+
+```ts
+export function calDamageWith(
+ info: UserEnemyInfo,
+ hero: Partial
+): number | null {
+ // ... 原有内容
+
+ // 在乘以 1 - (myAttr / 100),除以 100 是因为 myAttr 是百分制
+ heroPerDamage *= 1 - (info.myAttr ?? 0) / 100; // [!code ++]
+
+ // ... 原有内容
+}
+```
+
+## 拓展-地图伤害
+
+同样在 `damage.ts`,找到 `DamageEnemy.calMapDamage` 方法,直接 `ctrl+F` 搜索 `calMapDamage` 即可找到,然后在其中编写地图伤害即可。以领域为例,它是这么写的:
+
+```ts
+class DamageEnemy {
+ calMapDamage(
+ damage: Record = {},
+ hero: Partial = getHeroStatusOn(realStatus)
+ ) {
+ // 判断是否包含领域属性
+ if (this.info.special.has(15)) {
+ // 计算领域范围
+ const range = enemy.range ?? 1;
+ const startX = Math.max(0, this.x - range);
+ const startY = Math.max(0, this.y - range);
+ const endX = Math.min(floor.width - 1, this.x + range);
+ const endY = Math.min(floor.height - 1, this.y + range);
+ // 伤害量
+ const dam = enemy.value ?? 0;
+ const objs = core.getMapBlocksObj(this.floorId);
+
+ for (let x = startX; x <= endX; x++) {
+ for (let y = startY; y <= endY; y++) {
+ if (
+ !enemy.zoneSquare &&
+ // 判断非九宫格领域,使用曼哈顿距离判断
+ manhattan(x, y, this.x, this.y) > range
+ ) {
+ continue;
+ }
+ const loc = `${x},${y}` as LocString;
+ if (objs[loc]?.event.noPass) continue;
+ // 存储地图伤害
+ this.setMapDamage(damage, loc, dam, '领域');
+ }
+ }
+ }
+ }
+}
+```
+
+## 拓展-光环属性
+
+光环计算目前分为两个优先级,高优先级的可以影响低优先级的,这意味着你可以做出来加光环的光环属性。不过高级光环的逻辑比较绕,而且需求不高,这里不再介绍。如果需要的话可以自行理解这部分逻辑或在造塔群里询问。这里以攻击光环为例,展示如何制作一个普通光环。
+
+我们假设使用 `atkHalo` 作为光环增幅,`haloRange` 作为光环范围,属性代码为 `30`,九宫格光环。我们在 `damage.ts` 中找到 `DamageEnemy.provideHalo` 方法,直接 `ctrl+F` 搜索 `provideHalo` 就能找到。
+
+### 光环逻辑
+
+我们直接调用 `applyHalo` 即可,如下编写代码:
+
+```ts
+class DamageEnemy {
+ provideHalo() {
+ // ... 原有逻辑
+
+ // 施加光环
+ col.applyHalo(
+ // 光环形状为正方形。目前支持 square 矩形和 rect 矩形
+ 'square',
+ // 正方形形状参数
+ {
+ x: this.x, // 中心横坐标
+ y: this.y, // 中心纵坐标
+ d: this.info.haloRange * 2 + 1 // 边长
+ },
+ this, // 填 this 即可
+ (e, enemy) => {
+ // 这里的 e 是指被加成的怪物,enemy 是当前施加光环的怪物
+ // 直接加到 atkBuff_ 属性上即可
+ e.atkBuff_ += enemy.atkHalo;
+ }
+ );
+
+ // 在地图上显示光环,这部分可选,如果不想显示也可以不写
+ col.haloList.push({
+ // 光环形状
+ type: 'square',
+ // 形状参数
+ data: { x: this.x, y: this.y, d: this.info.haloRange * 2 + 1 },
+ // 特殊属性代码
+ special: 30,
+ // 施加的怪物
+ from: this
+ });
+ }
+}
+```
+
+### 自定义形状
+
+如果想要自定义光环形状,我们打开 `packages-user/data-utils/src/range.ts`,拉到最后可以看到形状定义,目前包含两个:
+
+- `square`: 中心点+边长的正方形
+- `rect`: 左上角坐标+宽高的矩形
+
+我们以曼哈顿距离为例,展示如何自定义形状。
+
+首先在开头找到 `interface RangeTypeData`,在其中添加必要的参数类型:
+
+```ts
+interface RangeTypeData {
+ // ... 原有内容
+
+ // 自定义的曼哈顿范围参数,包含中心坐标和半径
+ manhattan: { x: number; y: number; dis: number }; // [!code ++]
+}
+```
+
+然后在文件最后定义形状即可:
+
+```ts
+// 这里的第一个参数就是我们的形状名称,填 manhattan 即可
+// 第二个参数是一个函数,目的是判断 item 是否在范围内
+Range.register('manhattan', (item, { x, y, dis }) => {
+ // 如果 item 连坐标属性都不存在,那么直接判定不在范围内
+ if (isNil(item.x) || isNil(item.y)) return false;
+ // 计算与中心的坐标差
+ const dx = Math.abs(item.x - x);
+ const dy = Math.abs(item.y - y);
+ // 坐标差之和小于半径则在范围内,否则在范围外
+ return dx + dy < dis;
+});
+```
+
+在光环中,我们就可以直接使用这种形状了:
+
+```ts {2-9}
+col.applyHalo(
+ // 使用自定义形状
+ 'manhattan',
+ // 自定义形状的参数
+ {
+ x: this.x, // 中心横坐标
+ y: this.y, // 中心纵坐标
+ dis: this.info.haloRange // 半径
+ },
+ this,
+ (e, enemy) => {
+ e.atkBuff_ += enemy.atkHalo;
+ }
+);
+```
+
+## 拓展-输出回合数
+
+样板默认的 `calDamageWith` 函数只允许输出伤害值,而有时候我们可能会需要战斗的回合数,这时候我们需要修改一下这部分内容,将伤害计算逻辑单独提出来,然后在 `calDamageWith` 中调用它。在需要回合数的时候,我们调用提出了的函数即可,如下例所示:
+
+```ts
+/** 包含回合数的伤害计算 */
+export function calDamageWithTurn(
+ info: UserEnemyInfo,
+ hero: Partial
+) {
+ // ... 原本 calDamageWith 的计算逻辑,记得删除最后返回伤害的那一行返回值
+
+ // 返回回合数和伤害
+ return { turn, damage };
+}
+
+export function calDamageWith(info: UserEnemyInfo, hero: Partial) {
+ // 调用单独提出的函数计算伤害值
+ const damageInfo = calDamageWithTurn(info, hero);
+ // 如果伤害不存在,那么返回无穷大
+ return damageInfo?.damage ?? Infinity;
+}
+```
diff --git a/docs/guide/implements/status-bar.md b/docs/guide/implements/status-bar.md
new file mode 100644
index 0000000..b156c85
--- /dev/null
+++ b/docs/guide/implements/status-bar.md
@@ -0,0 +1,114 @@
+# 修改状态栏显示
+
+在 `packages-user/client-modules/src/render/ui/statusBar.tsx` 中编写,内部包含两个组件 `LeftStatusBar` 和 `RightStatusBar`,分别代表左侧状态栏和右侧状态栏。
+
+在编写完 UI 之后,还需要在 `packages-user/client-modules/src/render/ui/main.tsx` 中传入必要的参数。
+
+下面以添加一个自定义 `flag` 的显示为例说明如何新增。
+
+## 添加属性声明
+
+首先在 `statusBar.tsx` 中声明:
+
+```tsx
+// 这个是文件中自带的接口声明,直接在原有声明的基础上添加即可
+interface ILeftHeroStatus {
+ // ... 原有声明
+
+ /**
+ * 自己添加的声明,这里使用这种 jsDoc 注释可以在自动补全中直接查看到。
+ * 自定义 flag 为数字类型。
+ */
+ myFlag: number; // [!code ++]
+}
+```
+
+## 添加显示
+
+然后在 `LeftStatusBar` 中添加此项的显示:
+
+```tsx {9}
+export const LeftStatusBar = defineComponent>(
+ p => {
+ // ... 原有组件 setup 内容
+ return () => (
+
+ {/* 原有组件内容 */}
+
+ {/* 然后编写一个 text 标签即可显示,位置可以用 loc 参数指定,字体可以用 font 参数指定 */}
+
+ ;
+ );
+ }
+);
+```
+
+## 传入属性值
+
+在 `main.tsx` 中传入自定义 `flag`。首先找到 `//#region 状态更新` 分段,在其上方有 `leftStatus` 的定义,此时它会报错,是因为你在 `ILeftHeroStatus` 中定义了 `myFlag`,而此处没有定义其初始值,为其赋初始值 0 即可:
+
+```tsx
+const leftStatus: ILeftHeroStatus = reactive({
+ // ...原有定义
+
+ // 然后添加自己的定义,注意不要忘记了在前一个属性后面加逗号
+ myFlag: 0 // [!code ++]
+});
+```
+
+在 `//#region` 分段下方找到 `updateStatus` 函数,它内部会有一系列 `leftStatus` 的赋值:
+
+```tsx
+leftStatus.atk = getHeroStatusOn('atk');
+leftStatus.hp = getHeroStatusOn('hp');
+leftStatus.def = getHeroStatusOn('def');
+// ...其他赋值
+```
+
+我们在其后面添加一个 `myFlag` 的赋值即可:
+
+```tsx
+// 将 flags.myFlag 赋值到 leftStatus.myFlag
+leftStatus.myFlag = flags.myFlag;
+```
+
+这样,我们就成功新增了一个新的显示项。这一系列操作虽然比 2.x 更复杂,但是其性能表现、规范程度都要更高,你需要习惯这种代码编写风格。
+
+## 拓展-可交互按钮
+
+相比于 2.x,2.B 在交互上会方便地多,如果要添加一个可交互的按钮,我们只需要给标签加上 `onClick` 属性,就可以在点击时执行函数了:
+
+```tsx {7-13}
+import { IActionEvent } from '@motajs/render';
+
+export const LeftStatusBar = defineComponent>(
+ p => {
+ // ... 原有组件 setup 内容
+
+ const clickText = (ev: IActionEvent) => {
+ // 这里编写点击按钮后的效果,例如切换技能
+ toggleSkill();
+ // 参数 ev 还包含一些属性和方法,例如可以调用 stopPropagation 终止冒泡传播
+ // 这个调用如果不理解不建议随便用,参考 UI 系统的教学文档来理解这句话的含义
+ ev.stopPropagation();
+ };
+
+ return () => (
+
+ {/* 原有组件内容 */}
+
+ ;
+ );
+ }
+);
+```
+
+## 拓展-了解 UI 编写的基本逻辑
+
+参考[此文档](./ui.md),此文档将会教你如何从头开始编写一个 UI,并解释 UI 运行与渲染的基本逻辑。
diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md
new file mode 100644
index 0000000..beb216b
--- /dev/null
+++ b/docs/guide/quick-start.md
@@ -0,0 +1,69 @@
+# 快速开始
+
+本章节主要帮助你快速上手 2.B 样板。
+
+## 常见需求实现指南
+
+参考[此文档](./implements.md)
+
+## 编辑器
+
+编辑器与 2.x 的编辑器差别不大,但 2.B 的编辑器多了一些功能:
+
+- 图片支持 `webp` 格式
+- 音频支持 `opus` 格式
+- 新增自动元件自定义连接
+- 部分事件有改动
+
+:::info
+由于 `opus` 格式的音频在体积上有其他音频格式无可比拟的优势,因此建议所有音频使用 `opus` 格式。同样,由于 `webp` 格式的图片在多数情况下比 `png` `jpg` 格式更小,因此建议图片换用无损 `webp` 格式。
+:::
+
+### 代码编写
+
+在 2.B 中,代码编写不再建议使用样板编辑器,因为在插件编写或脚本编辑中调用新接口较为复杂,且无法体验到代码补全、类型检查等实用功能。为了能有更好的开发体验,建议使用 `vscode` 等软件开发,如果不了解 `vscode`,建议查看[这篇文章](https://h5mota.com/bbs/thread/?tid=1018&p=1#p3),这篇文章介绍了 `vscode` 的安装与一些必要插件的安装。或者当你用 `vscode` 打开样板时,`vscode` 也会提示推荐安装的插件,全部安装即可。
+
+相比于 2.x,2.B 的 `main.js` 允许在 `function main` 的声明之后使用 `ES6+` 语法。
+
+### TypeScript 类型检查
+
+`TypeScript` 提供了非常严格的类型检查功能,这可以避免九成因粗心而犯的错误,包括变量名拼写错误、参数传递错误等。`TypeScript` 是 `JavaScript` 的超集,也就是说你可以在 `ts` 中写 `js` 代码,在语法上完全合理。如果不了解 `ts`,可以查看[我编写的教程](https://h5mota.com/bbs/thread/?tid=1018&p=3#p41),不过由于难度较大,如果不能理解也不会有影响,你完全可以在 `ts` 中写 `js`,然后将所有的类型标记为 `any`,虽然这不规范,但是也确实可以避免类型报错。
+
+## 游戏构建
+
+由于 2.B 使用了与 2.x 完全不同的技术栈,它并不能直接在浏览器上运行,必须经过构建步骤。2.B 提供了一个新的启动服务来运行样板,它包含两个部分,一部分是开发服务器,这部分运行你直接开发,而不经过构建步骤,包括热重载等非常实用的功能;另一部分是构建,当你做完游戏后,点击构建按钮即可开始构建。需要注意的是,构建要求整个项目没有类型错误,如果包含类型错误,则会在输出中提到(`eslint` 报错不影响)。构建流程基本如下:
+
+1. 构建代码
+2. 压缩字体
+3. 资源分离压缩
+4. 将游戏压缩为 `zip` 压缩包
+
+构建流程的自动化程度极高,因此你可以完全专注于游戏的制作,而完全不用关心资源加载等问题(也不需要添加分块加载插件)。
+
+### 构建代码
+
+2.B 的代码分为两部分,一部分是客户端(渲染端),另一部分是数据端(服务端),其中客户端会单向引用数据端,而数据端不能直接引用客户端,必须通过必要的接口。这么做是为了让渲染与数据分离,所有的必要逻辑运算都在数据端,这也意味着录像验证只会运行数据端,而不会运行客户端,因此客户端不应该出现会影响录像正确性与一致性的操作。
+
+由于系统无法判断一个模块属于客户端还是数据端,因此构建时会打包两次:第一次以客户端为入口打包,包含客户端及数据端两部分;第二次以数据端为入口打包,只包含数据端。当在线游戏时,会以客户端入口打开游戏,当录像验证时,会以数据端入口来验证。
+
+因此,必须要注意的是数据端不能直接引用任何客户端内容,这会直接导致录像验证报错,因为数据端打包也把客户端打包了进去,这是绝对不能出现的情况!而且样板没有办法来检测是否出现了这种情况!如何在数据端引用客户端内容请查看[此文档](./system.md#渲染端与数据端通信)
+
+### 压缩字体
+
+字体会使用 `Fontmin` 工具自动压缩,它会扫描项目中所有使用到的文字(不包括代码注释中的,也不包括使用 `String.fromCharCode` 方式创建的文字),然后处理字体,将所有不包含的文字的字体数据排除在外,只包含出现过的文字。这个操作可以大大降低字体大小。
+
+### 资源分离压缩
+
+类似于插件库的分块加载优化,不过 2.B 样板的实现与插件完全不同。样板会将所有资源混合打包,即图片可能会与音效打包在同一分块中。每个分块的大小默认为 `2MB`,可以在 `scripts/build-game.ts` 中修改。
+
+### 压缩游戏
+
+在上述操作执行完毕后,样板会自动处理一些剩余杂项,然后将打包结果放在 `dist` 文件夹中,此文件即为打包结果,可以直接用旧样板的启动服务打开。除此之外,样板还会自动将此文件夹压缩为 `zip` 压缩包,如果需要更新游戏或发塔,直接上传此压缩包即可,不需要任何额外处理。
+
+### 协议问题
+
+2.B 样板换用了 `GPL3.0` 开源协议,这要求所有以此为基础开发的项目也必须完全开源,但考虑到很多作者不了解其中的细节,因此样板将会针对此问题进行处理,处理方案为:**将源码原封不动地打包为压缩包,放到构建完成的游戏中**,届时,只要在网站上下载游戏,就可以解压压缩包查看源码。
+
+## 学会查阅此文档
+
+此文档内容很丰富,大部分接口文档都有使用示例,善用此文档可以大幅提高开发效率。
diff --git a/docs/guide/system.md b/docs/guide/system.md
index 637c623..8139fd6 100644
--- a/docs/guide/system.md
+++ b/docs/guide/system.md
@@ -147,6 +147,8 @@ export function patchMyFunctions() {
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。
+**渲染端和数据端均可以调用此接口来重写函数!**
+
::: warning
**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容!
:::
diff --git a/docs/guide/ui-elements.md b/docs/guide/ui-elements.md
index 52506ae..73b19a0 100644
--- a/docs/guide/ui-elements.md
+++ b/docs/guide/ui-elements.md
@@ -89,7 +89,7 @@ type ElementLocator = [
### 缓存属性
-可以通过 `cache` 和 `nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `cache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
+可以通过 `cache` 和 `nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `nocache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
```tsx
// 内部渲染内容比较简单,不需要启用缓存
@@ -504,7 +504,7 @@ export interface WinskinProps extends BaseProps {
| 属性分类 | 关键参数 | 说明 |
| -------------- | ------------------------- | -------------------------------------------------------- |
| **填充与描边** | `fill` `stroke` | 控制是否填充/描边(不同元素默认值不同) |
-| **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色/渐变等(如 `'#f00'`) |
+| **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色等(如 `'#f00'`) |
| **线型设置** | `lineWidth` `lineDash` | 线宽、虚线模式(如 `[5, 3]` 表示 5 像素实线+3 像素间隙) |
| **高级控制** | `fillRule` `actionStroke` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 |
diff --git a/docs/guide/ui-faq.md b/docs/guide/ui-faq.md
index 9c28b09..a32dc7c 100644
--- a/docs/guide/ui-faq.md
+++ b/docs/guide/ui-faq.md
@@ -31,7 +31,7 @@ watch(data, () => mySprite.value?.update());
## 我的 UI 很卡
-可能使用了平铺式布局,建议使用 `Scroll` 组件或者 `Page` 组件来对平铺内容分割,从而提高渲染效率。可以参考对应的 [API 文档](../api/user-client-modules/组件 Scroll)。
+可能使用了平铺式布局,建议使用 `Scroll` 组件或者 `Page` 组件来对平铺内容分割,从而提高渲染效率。可以参考对应的 [API 文档](../api/user-client-modules/组件%20Scroll.md)。
## 玩着玩着突然黑屏了一下,然后画面就不显示了
@@ -40,21 +40,13 @@ watch(data, () => mySprite.value?.update());
关于这个问题的最佳实践:
- 如果你手动存储了一些元素,确保在卸载时将它们删除
-- 在删除它们的同时,调用它们的 `destroy` 方法,来确保可以被垃圾回收
-- 在控制台输入 `Mota.require('@motajs/render').MotaOffscreenCanvas2D.list` 来查看当前还有哪些画布正在使用,游玩一段时间后再次输入,检查数量是否增长,如果增长,说明发生了内存泄漏
+- 在删除你手动存储的元素的同时,调用它们的 `destroy` 方法,来确保可以被垃圾回收
- 确保组件卸载时已经清空了定时器等内容
- 如果需要每帧执行函数,请使用 `onTick` 接口,而非其他方法
-如果你直接使用 `MotaOffscreenCanvas2D` 接口,请确保:
-
-- 在使用前调用了 `activate` 方法
-- 在使用后调用了 `deactivate` 方法
-- 如果不需要再修改画布属性,只需要绘制,请调用 `freeze` 方法
-- 如果之后不再使用该画布,请调用 `destroy` 方法
-
## 为什么我的滤镜不显示?
-很遗憾,截止目前(2.B 发布日期),IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础,可以在造塔群询问我或造塔辅助 AI。
+很遗憾,截止目前(2.B 发布日期),IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础。
## 不同设备的显示内容会不一样吗?
diff --git a/docs/guide/ui-future.md b/docs/guide/ui-future.md
new file mode 100644
index 0000000..7c9b839
--- /dev/null
+++ b/docs/guide/ui-future.md
@@ -0,0 +1,15 @@
+# UI 系统未来规划
+
+本章介绍一下 UI 系统的未来更新计划。
+
+## 样式系统
+
+提供类似于 CSS 的样式系统,不再需要在标签中指定颜色,而只需要定义样式,即可实现丰富的样式设定,包含继承功能。
+
+## 布局系统
+
+提供类似于 `flex` `grid` 布局的布局系统,即列表布局和网格布局,此时不需要手动指定元素坐标即可自动分配布局。
+
+## 调试工具
+
+提供 UI 调试工具,查看渲染树的属性等。
diff --git a/docs/guide/ui-perf.md b/docs/guide/ui-perf.md
index 1c91895..08e322a 100644
--- a/docs/guide/ui-perf.md
+++ b/docs/guide/ui-perf.md
@@ -12,9 +12,9 @@ lang: zh-CN
画布渲染树的深度遍历特性使得:
-- 每个独立容器的更新会触发子树的重新渲染
-- 容器层级过深会增加递归调用栈开销
-- 合理分组可将高频/低频更新元素隔离
+- 每个独立容器的更新会触发子树的重新渲染
+- 容器层级过深会增加递归调用栈开销
+- 合理分组可将高频/低频更新元素隔离
下面是代码示例:
@@ -100,7 +100,7 @@ const render = () => {
## 使用 `cache` 和 `nocache` 标识
-`cache` 和 `nocache` 表示可以让你更加精确地控制渲染树的缓存行为,从而更好地优化渲染性能。默认情况下,这些元素是会被缓存的:`container` `container-custom` `template` `sprite` `image` `icon` `layer` `layer-group` `animation`,对于这些元素,你可以使用 `nocache` 标识来禁用它们的缓存,对于其本身或其子元素的渲染较为简单的场景,禁用缓存后渲染效率可能会更高。其他元素默认是禁用缓存的,如果你的渲染内容比较复杂,例如 `g-path` 元素的路径很复杂,可以使用 `cache` 表示来启用缓存,从而提高渲染效率。示例代码如下:
+`cache` 和 `nocache` 表示可以让你更加精确地控制渲染树的缓存行为,从而更好地优化渲染性能。默认情况下,这些元素是会被缓存的:`container` `container-custom` `template` `sprite` `icon` `layer` `layer-group` `animation`,对于这些元素,你可以使用 `nocache` 标识来禁用它们的缓存,对于其本身或其子元素的渲染较为简单的场景,禁用缓存后渲染效率可能会更高。其他元素默认是禁用缓存的,如果你的渲染内容比较复杂,例如 `g-path` 元素的路径很复杂,可以使用 `cache` 表示来启用缓存,从而提高渲染效率。示例代码如下:
```tsx
const render = (canvas: MotaOffscreenCanvas2D) => {
diff --git a/docs/guide/ui-system.md b/docs/guide/ui-system.md
index a16b912..e20b98a 100644
--- a/docs/guide/ui-system.md
+++ b/docs/guide/ui-system.md
@@ -6,6 +6,8 @@
样板提供 `UIController` 类,允许你在自己的一个 UI 中创建自己的 UI 管理器,例如在样板中,游戏画面本身包含一个 UI 管理器,分为了封面、加载界面、游戏界面三种,其中游戏界面里面还有一个游戏 UI 管理器,我们常用的就是最后一个游戏 UI 管理器。
+多数情况下,样板自带的 UI 管理器已经足够,不需要自己创建 UI 管理器。我们最常用的管理器就是 `mainUIController`,它控制了游戏界面下的 UI。
+
### 创建 UIController 实例
我们从 `@motajs/system-ui` 引入 `UIController` 类,然后对其实例化:
@@ -70,11 +72,11 @@ myController.lastOnly(false);
方法说明如下:
-- `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI
-- `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭
-- `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示
-- `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示
-- `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏
+- `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI
+- `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭
+- `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示
+- `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示
+- `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏
那么,假如我们要做一个反向 `lastOnly`,即只显示第一个,添加 UI 时添加至队列开头,我们可以这么写:
@@ -190,15 +192,15 @@ myController.closeAll(EnemyInfo);
想象一棵倒着生长的树:
-- 根节点:相当于画布本身,是所有元素的起点
-- 枝干节点:类似文件夹,可以包含其他元素
-- 叶子节点:实际显示的内容,如图片、文字等
+- 根节点:相当于画布本身,是所有元素的起点
+- 枝干节点:类似文件夹,可以包含其他元素
+- 叶子节点:实际显示的内容,如图片、文字等
### 运作特点
-- 层级管理:子元素永远在父元素的"内部"显示
-- 自动排序:像叠扑克牌一样,后添加的元素默认盖在之前元素上方,不过也可以通过参数来调整顺序
-- 智能裁剪:父元素就像相框,超出范围的内容自动隐藏
+- 层级管理:子元素永远在父元素的"内部"显示
+- 自动排序:像叠扑克牌一样,后添加的元素默认盖在之前元素上方,不过也可以通过参数来调整顺序
+- 智能裁剪:父元素就像相框,超出范围的内容自动隐藏
## 渲染系统的事件系统
@@ -212,9 +214,9 @@ myController.closeAll(EnemyInfo);
### 特殊处理机制
-- 紧急拦截:任何环节都可以标记"无需继续传递"
-- 批量处理:多个事件自动合并减少处理次数
-- 智能过滤:自动忽略不可见区域的事件
+- 紧急拦截:任何环节都可以标记"无需继续传递"
+- 批量处理:多个事件自动合并减少处理次数
+- 智能过滤:自动忽略不可见区域的事件
## 冒泡更新
@@ -224,9 +226,9 @@ myController.closeAll(EnemyInfo);
### 设计优势
-- 精准定位:只更新受影响的部分画面
-- 避免重复:多个子元素变化只需一次整体计算
-- 顺序保障:始终从最深层开始逐层处理
+- 精准定位:只更新受影响的部分画面
+- 避免重复:多个子元素变化只需一次整体计算
+- 顺序保障:始终从最深层开始逐层处理
## 懒更新机制
@@ -238,6 +240,6 @@ myController.closeAll(EnemyInfo);
### 实际效益
-- 性能优化:减少像频繁开关灯的资源浪费
-- 流畅保障:避免连续小改动导致的画面闪烁
-- 智能调度:优先处理用户可见区域的变化
+- 性能优化:减少像频繁开关灯的资源浪费
+- 流畅保障:避免连续小改动导致的画面闪烁
+- 智能调度:优先处理用户可见区域的变化
diff --git a/docs/guide/ui.md b/docs/guide/ui.md
index f530b5e..353d7f4 100644
--- a/docs/guide/ui.md
+++ b/docs/guide/ui.md
@@ -16,10 +16,10 @@ lang: zh-CN
```tsx
import { defineComponent } from 'vue';
-import { GameUI, UIComponentProps } from '@motajs/system-ui';
+import { GameUI, UIComponentProps, DefaultProps } from '@motajs/system-ui';
import { SetupComponentOptions } from '../components';
-export interface MyBookProps extends UIComponentProps {}
+export interface MyBookProps extends UIComponentProps, DefaultProps {}
const myBookProps = {
props: ['controller', 'instance']
diff --git a/docs/index.md b/docs/index.md
index d347f0d..6b8bb35 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,7 +9,7 @@ hero:
actions:
- theme: brand
text: 深度指南
- link: /guide/diff
+ link: /guide/quick-start
- theme: alt
text: API列表
link: /api/index
diff --git a/packages-user/client-modules/src/render/ui/settings.tsx b/packages-user/client-modules/src/render/ui/settings.tsx
index 98d7497..63e724c 100644
--- a/packages-user/client-modules/src/render/ui/settings.tsx
+++ b/packages-user/client-modules/src/render/ui/settings.tsx
@@ -23,6 +23,7 @@ import { getInput } from '../components';
import { openStatistics } from './statistics';
import { saveWithExist } from './save';
import { compressToBase64 } from 'lz-string';
+import { ViewMapUI } from './viewmap';
export interface MainSettingsProps
extends Partial,
@@ -78,7 +79,7 @@ export const MainSettings = defineComponent(props => {
break;
}
case MainChoice.ViewMap: {
- // todo
+ props.controller.open(ViewMapUI, { loc: [0, 0, 840, 840] });
break;
}
case MainChoice.Replay: {
diff --git a/packages-user/client-modules/src/render/ui/statusBar.tsx b/packages-user/client-modules/src/render/ui/statusBar.tsx
index 9168100..15f7c3a 100644
--- a/packages-user/client-modules/src/render/ui/statusBar.tsx
+++ b/packages-user/client-modules/src/render/ui/statusBar.tsx
@@ -42,6 +42,27 @@ export interface ILeftHeroStatus {
magicDef: number;
}
+export interface IRightHeroStatus {
+ /** 自动切换技能 */
+ autoSkill: boolean;
+ /** 当前开启的技能 */
+ skillName: string;
+ /** 技能描述 */
+ skillDesc: string;
+ /** 跳跃剩余次数,-1 表示未开启,-2表示当前楼层不能跳 */
+ jumpCount: number;
+ /** 治愈之泉剩余次数,-1 表示未开启 */
+ springCount: number;
+ /** 当前楼层 */
+ floor: FloorIds;
+ /** 是否正在录像播放 */
+ replaying: boolean;
+ /** 录像播放状态 */
+ replayStatus: ReplayingStatus;
+ /** 极昼永夜 */
+ night: number;
+}
+
interface StatusBarProps extends DefaultProps {
loc: ElementLocator;
status: T;
@@ -104,82 +125,80 @@ export const LeftStatusBar = defineComponent>(
openViewMap(mainUIController, [0, 0, 840, 480]);
};
- return () => {
- return (
-
+ return () => (
+
+
+
+
+
+
+
+
+
+
+
+ {s.magicDef > 0 && (
-
-
-
-
-
-
-
-
-
- {s.magicDef > 0 && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
},
statusBarProps
);
@@ -191,27 +210,6 @@ interface RightStatusBarMisc {
valueColor: string;
}
-export interface IRightHeroStatus {
- /** 自动切换技能 */
- autoSkill: boolean;
- /** 当前开启的技能 */
- skillName: string;
- /** 技能描述 */
- skillDesc: string;
- /** 跳跃剩余次数,-1 表示未开启,-2表示当前楼层不能跳 */
- jumpCount: number;
- /** 治愈之泉剩余次数,-1 表示未开启 */
- springCount: number;
- /** 当前楼层 */
- floor: FloorIds;
- /** 是否正在录像播放 */
- replaying: boolean;
- /** 录像播放状态 */
- replayStatus: ReplayingStatus;
- /** 极昼永夜 */
- night: number;
-}
-
export const RightStatusBar = defineComponent>(
p => {
const font1 = new Font('normal', 18);
diff --git a/packages-user/data-state/src/enemy/damage.ts b/packages-user/data-state/src/enemy/damage.ts
index c6ab52a..98252b3 100644
--- a/packages-user/data-state/src/enemy/damage.ts
+++ b/packages-user/data-state/src/enemy/damage.ts
@@ -18,8 +18,6 @@ import {
} from '@motajs/types';
import { isNil } from 'lodash-es';
-// todo: 光环划分优先级,从而可以实现光环的多级运算
-
export interface UserEnemyInfo extends EnemyInfo {
togetherNum?: number;
}
@@ -253,7 +251,7 @@ export class DamageEnemy implements IDamageEnemy {
for (const [key, value] of Object.entries(enemy)) {
if (!(key in this.info) && has(value)) {
- // @ts-ignore
+ // @ts-expect-error 无法推导
this.info[key] = value;
}
}
@@ -580,7 +578,6 @@ export class DamageEnemy implements IDamageEnemy {
* 计算怪物伤害
*/
calDamage(hero: Partial = core.status.hero) {
- // todo: 缓存怪物伤害
const enemy = this.getRealInfo();
return this.calEnemyDamageOf(hero, enemy);
}
@@ -756,7 +753,6 @@ export class DamageEnemy implements IDamageEnemy {
num: number = 1,
hero: Partial = core.status.hero
): CriticalDamageDelta[] {
- // todo: 缓存临界
const origin = this.calDamage(hero);
const seckill = this.getSeckillAtk();
return this.calCriticalWith(num, seckill, origin, hero);
@@ -775,7 +771,6 @@ export class DamageEnemy implements IDamageEnemy {
origin: DamageInfo,
hero: Partial
): CriticalDamageDelta[] {
- // todo: 可以优化,根据之前的计算可以直接确定下一个临界的范围
if (!isFinite(seckill)) return [];
const res: CriticalDamageDelta[] = [];
@@ -830,6 +825,7 @@ export class DamageEnemy implements IDamageEnemy {
start = curr;
}
if (i++ >= 10000) {
+ // eslint-disable-next-line no-console
console.warn(
`Unexpected endless loop in calculating critical.` +
`Enemy Id: ${this.id}. Loc: ${this.x},${this.y}. Floor: ${this.floorId}`
@@ -945,10 +941,12 @@ const skills: HeroSkill.Skill[] = [HeroSkill.Blade, HeroSkill.Shield];
export function calDamageWith(
info: UserEnemyInfo,
hero: Partial
-): number | null {
- const { hp, mdef } = core.status.hero;
- let { atk, def, hpmax, mana, magicDef } = hero as HeroStatus;
- let { hp: monHp, atk: monAtk, def: monDef, special, enemy } = info;
+): number {
+ const { mdef } = core.status.hero;
+ const { def, mana, magicDef } = hero as HeroStatus;
+ const { hp: monHp, def: monDef, special, enemy } = info;
+ let { atk, hpmax } = hero as HeroStatus;
+ let { atk: monAtk } = info;
// 赏金,优先级最高
if (special.has(34)) return 0;
@@ -969,15 +967,15 @@ export function calDamageWith(
// 绝对防御
if (special.has(9)) {
heroPerDamage = atk + mana - monDef;
- if (heroPerDamage <= 0) return null;
+ if (heroPerDamage <= 0) return Infinity;
} else if (special.has(3)) {
// 由于坚固的特性,只能放到这来计算了
if (atk > enemy.def) heroPerDamage = 1 + mana;
- else return null;
+ else return Infinity;
} else {
heroPerDamage = atk - monDef;
if (heroPerDamage > 0) heroPerDamage += mana;
- else return null;
+ else return Infinity;
}
// 霜冻
diff --git a/packages-user/data-state/src/index.ts b/packages-user/data-state/src/index.ts
index 572daa1..5f6dc7d 100644
--- a/packages-user/data-state/src/index.ts
+++ b/packages-user/data-state/src/index.ts
@@ -1,3 +1,9 @@
+import { createMechanism } from './mechanism';
+
+export function create() {
+ createMechanism();
+}
+
export * from './enemy';
export * from './mechanism';
export * from './state';
diff --git a/packages-user/data-state/src/mechanism/index.ts b/packages-user/data-state/src/mechanism/index.ts
index a9adaa7..ef1554b 100644
--- a/packages-user/data-state/src/mechanism/index.ts
+++ b/packages-user/data-state/src/mechanism/index.ts
@@ -1,2 +1,4 @@
+export function createMechanism() {}
+
export * from './misc';
export * from './skillTree';
diff --git a/packages-user/entry-data/src/create.ts b/packages-user/entry-data/src/create.ts
index 6cde425..4f04e5e 100644
--- a/packages-user/entry-data/src/create.ts
+++ b/packages-user/entry-data/src/create.ts
@@ -19,4 +19,5 @@ export function create() {
function createModule() {
LegacyPluginData.createLegacy();
+ DataState.create();
}
diff --git a/script/dev.ts b/script/dev.ts
index 8a23aa6..ed0898c 100644
--- a/script/dev.ts
+++ b/script/dev.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
import { createServer } from 'vite';
import { Server } from 'http';
import { ensureDir, move, pathExists, remove } from 'fs-extra';