docs: 常见需求指南

This commit is contained in:
unanmed 2025-08-22 16:45:47 +08:00
parent 138481e637
commit a10af04b79
28 changed files with 1448 additions and 190 deletions

View File

@ -29,9 +29,12 @@ export default defineConfig({
{ {
text: '深度指南', text: '深度指南',
items: [ items: [
{ text: '快速开始', link: '/guide/quick-start' },
{ text: '差异说明', link: '/guide/diff' }, { text: '差异说明', link: '/guide/diff' },
{ text: '系统说明', link: '/guide/system' }, { text: '系统说明', link: '/guide/system' },
{ text: '代码编写', link: '/guide/coding' }, { text: '代码编写', link: '/guide/coding' },
{ text: '音频系统', link: '/guide/audio' },
{ {
text: 'UI 系统', text: 'UI 系统',
collapsed: false, collapsed: false,
@ -40,10 +43,40 @@ export default defineConfig({
{ text: 'UI 优化', link: '/guide/ui-perf' }, { text: 'UI 优化', link: '/guide/ui-perf' },
{ text: 'UI 系统', link: '/guide/ui-system' }, { text: 'UI 系统', link: '/guide/ui-system' },
{ text: 'UI 元素', link: '/guide/ui-elements' }, { 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: { vite: {
// @ts-expect-error 类型错误
plugins: [MermaidPlugin()], plugins: [MermaidPlugin()],
optimizeDeps: { optimizeDeps: {
include: ['mermaid'] include: ['mermaid']

View File

@ -2,12 +2,13 @@
`@motajs/client` 包含多个模块: `@motajs/client` 包含多个模块:
- [`@motajs/client-base`](../motajs-client-base/) - [`@motajs/client-base`](../motajs-client-base/)
示例: 示例:
```ts ```ts
import { KeyCode } from '@motajs/client'; import { KeyCode } from '@motajs/client';
const { KeyCOde } = Mota.require('@motajs/client'); // 等价于
const { KeyCode } = Mota.require('@motajs/client');
``` ```

View File

@ -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)组件。
--- ---

View File

@ -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` 2. 考虑到浏览器兼容性,不建议在颜色中填写一些新标准的语法,例如 `rgb(0.3, 0.6, 0.8 / 0.6)` `#rgba`

View File

@ -2,15 +2,15 @@
此模块包含如下模块的内容,可以直接引用: 此模块包含如下模块的内容,可以直接引用:
- [@motajs/render-core](../motajs-render-core/) - [@motajs/render-core](../motajs-render-core/)
- [@motajs/render-elements](../motajs-render-elements/) - [@motajs/render-elements](../motajs-render-elements/)
- [@motajs/render-style](../motajs-render-style/) - [@motajs/render-style](../motajs-render-style/)
- [@motajs/render-vue](../motajs-render-vue/) - [@motajs/render-vue](../motajs-render-vue/)
引入示例: 引入示例:
```ts ```ts
import { Container } from '@motajs/render'; import { Container } from '@motajs/render';
// 二者等价,不需要单独使用一个量来接收,注意与 @motajs/client 引入方式区分 // 二者等价,不需要单独使用一个量来接收
import { Container } from '@motajs/render-core'; import { Container } from '@motajs/render-core';
``` ```

View File

@ -34,9 +34,9 @@ graph LR
function constructor(id: string, name: string): Hotkey; function constructor(id: string, name: string): Hotkey;
``` ```
- **参数** - **参数**
- `id`: 控制器的唯一标识符 - `id`: 控制器的唯一标识符
- `name`: 控制器的显示名称 - `name`: 控制器的显示名称
**示例** **示例**
@ -56,7 +56,7 @@ function register(data: RegisterHotkeyData): this;
注册一个按键配置。 注册一个按键配置。
- **参数** - **参数**
```typescript ```typescript
interface RegisterHotkeyData { interface RegisterHotkeyData {
id: string; // 按键唯一标识(可含数字后缀,如 "copy_1" id: string; // 按键唯一标识(可含数字后缀,如 "copy_1"
@ -89,10 +89,10 @@ function realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig): this;
为按键绑定触发逻辑。 为按键绑定触发逻辑。
- **参数** - **参数**
- `id`: 目标按键 ID无需后缀 - `id`: 目标按键 ID无需后缀
- `func`: 触发时执行的函数 - `func`: 触发时执行的函数
- `config`: 触发类型配置(节流/超时等) - `config`: 触发类型配置(节流/超时等)
**示例** **示例**
@ -116,10 +116,10 @@ function group(id: string, name: string, keys?: RegisterHotkeyData[]): this;
创建按键分组,后续注册的按键自动加入该组。 创建按键分组,后续注册的按键自动加入该组。
- **参数** - **参数**
- `id`: 分组唯一标识 - `id`: 分组唯一标识
- `name`: 分组显示名称 - `name`: 分组显示名称
- `keys`: 可选,预注册的按键列表 - `keys`: 可选,预注册的按键列表
--- ---
@ -131,11 +131,11 @@ function set(id: string, key: KeyCode, assist: number, emit?: boolean): void;
动态修改按键绑定。 动态修改按键绑定。
- **参数** - **参数**
- `id`: 目标按键 ID - `id`: 目标按键 ID
- `key`: 新按键代码 - `key`: 新按键代码
- `assist`: 辅助键状态二进制位Ctrl=1<<0, Shift=1<<1, Alt=1<<2 - `assist`: 辅助键状态二进制位Ctrl=1<<0, Shift=1<<1, Alt=1<<2
- `emit`: 是否触发 `set` 事件(默认 `true` - `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` 方法绑定的逻辑将关联到该作用域。 切换当前作用域,后续 `realize` 方法绑定的逻辑将关联到该作用域。
- **参数** - **参数**
- `symbol`: 唯一作用域标识符 - `symbol`: 唯一作用域标识符
--- ---
@ -203,8 +203,8 @@ function dispose(symbol?: symbol): void;
释放指定作用域及其绑定的所有按键逻辑。 释放指定作用域及其绑定的所有按键逻辑。
- **参数** - **参数**
- `symbol`(可选): 要释放的作用域(默认释放当前作用域) - `symbol`(可选): 要释放的作用域(默认释放当前作用域)
**示例** **示例**
@ -230,13 +230,13 @@ function emitKey(
手动触发按键事件(可用于模拟按键操作)。 手动触发按键事件(可用于模拟按键操作)。
- **参数** - **参数**
- `key`: 按键代码 - `key`: 按键代码
- `assist`: 辅助键状态(二进制位:Ctrl=1<<0, Shift=1<<1, Alt=1<<2 - `assist`: 辅助键状态(二进制位:`Ctrl=1<<0`, `Shift=1<<1`, `Alt=1<<2`
- `type`: 事件类型(`'up'` 或 `'down'` - `type`: 事件类型(`'up'` 或 `'down'`
- `ev`: 原始键盘事件对象 - `ev`: 原始键盘事件对象
- **返回值** - **返回值**
`true` 表示事件被成功处理,`false` 表示无匹配逻辑 `true` 表示事件被成功处理,`false` 表示无匹配逻辑
**示例** **示例**
@ -276,7 +276,7 @@ function get(id: string): Hotkey | undefined;
**事件监听示例** **事件监听示例**
```typescript ```typescript
editorHotkey.on('emit', (key, assist) => { gameKey.on('emit', (key, assist) => {
console.log(`按键 ${KeyCode[key]} 触发,辅助键状态:${assist}`); console.log(`按键 ${KeyCode[key]} 触发,辅助键状态:${assist}`);
}); });
``` ```

View File

@ -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 () => <container></container>;
});
```
### 通用按键复用
我们会有一些通用按键,例如确认、关闭,这些按键我们不希望每个 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 () => <container></container>;
});
```
实际上,你甚至可以在一个 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)

View File

@ -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)

View File

@ -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<MyComProps>;
// 定义组件内容
export const MyCom = defineComponent<MyComProps>(props => {
// 在这里编写你的 UI 即可
return () => <container></container>;
}, 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<MyComProps>(props => {
// 使用 props.controller适配不同 UI 控制器
props.controller.open(MyUI2, {});
return () => <container></container>;
}, myComProps);
```
## 关闭 UI
在 UI 内关闭自身使用:
```tsx
export const MyCom = defineComponent<MyComProps>(props => {
// 关闭自身
props.controller.close(props.instance);
return () => <container></container>;
}, 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 () => <container></container>;
});
// 这是不行的!同时 ts 也会为你贴心报错!
export const MyComponentUI = new GameUI('my-component', MyComponent);
// --------------------
interface MyComProps extends DefaultProps, UIComponentProps {}
// 一个包含 controller, instance 的组件,此处省略 myComProps 定义
export const MyCom = defineComponent<MyComProps>(props => {
return () => <container></container>;
}, myComProps);
// 这是可以的,可以被 UIController 打开!
export const MyComUI = new GameUI('my-com', MyCom);
```

View File

@ -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<HeroStatus>
): 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<StatusBarProps<ILeftHeroStatus>>(
p => {
return () => (
<container>
{/* ... 原有内容 */}
{/* 新增一个 text 标签,点击时执行 toggleSkill1 切换技能 */}
<text // [!code ++]
text="切换二倍斩" // [!code ++]
cursor="pointer" // [!code ++]
onClick={toggleSkill1} // [!code ++]
/>
</container>
);
}
);
```
:::
## 拓展-多技能设计思路
很多时候我们可能会有多个技能,且多个技能间互斥,即每次只能开启一个技能,这时候如果我们给每个技能都单独编写一套 `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<HeroStatus>
): 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<HeroStatus>
): 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;
// ... 其他判断
}
}
```

View File

@ -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<HeroStatus>
): 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<HeroStatus>
): 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<string, MapDamage> = {},
hero: Partial<HeroStatus> = 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<HeroStatus>
) {
// ... 原本 calDamageWith 的计算逻辑,记得删除最后返回伤害的那一行返回值
// 返回回合数和伤害
return { turn, damage };
}
export function calDamageWith(info: UserEnemyInfo, hero: Partial<HeroStatus>) {
// 调用单独提出的函数计算伤害值
const damageInfo = calDamageWithTurn(info, hero);
// 如果伤害不存在,那么返回无穷大
return damageInfo?.damage ?? Infinity;
}
```

View File

@ -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<StatusBarProps<ILeftHeroStatus>>(
p => {
// ... 原有组件 setup 内容
return () => (
<container>
{/* 原有组件内容 */}
{/* 然后编写一个 text 标签即可显示,位置可以用 loc 参数指定,字体可以用 font 参数指定 */}
<text text={s.myFlag.toString()} />
</container>;
);
}
);
```
## 传入属性值
`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.x2.B 在交互上会方便地多,如果要添加一个可交互的按钮,我们只需要给标签加上 `onClick` 属性,就可以在点击时执行函数了:
```tsx {7-13}
import { IActionEvent } from '@motajs/render';
export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
p => {
// ... 原有组件 setup 内容
const clickText = (ev: IActionEvent) => {
// 这里编写点击按钮后的效果,例如切换技能
toggleSkill();
// 参数 ev 还包含一些属性和方法,例如可以调用 stopPropagation 终止冒泡传播
// 这个调用如果不理解不建议随便用,参考 UI 系统的教学文档来理解这句话的含义
ev.stopPropagation();
};
return () => (
<container>
{/* 原有组件内容 */}
<text
text={s.myFlag.toString()}
// 当用户点击时执行 clickText // [!code ++]
onClick={clickText} // [!code ++]
// 鼠标样式变成小手 // [!code ++]
cursor="pointer" // [!code ++]
/>
</container>;
);
}
);
```
## 拓展-了解 UI 编写的基本逻辑
参考[此文档](./ui.md),此文档将会教你如何从头开始编写一个 UI并解释 UI 运行与渲染的基本逻辑。

69
docs/guide/quick-start.md Normal file
View File

@ -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.x2.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` 开源协议,这要求所有以此为基础开发的项目也必须完全开源,但考虑到很多作者不了解其中的细节,因此样板将会针对此问题进行处理,处理方案为:**将源码原封不动地打包为压缩包,放到构建完成的游戏中**,届时,只要在网站上下载游戏,就可以解压压缩包查看源码。
## 学会查阅此文档
此文档内容很丰富,大部分接口文档都有使用示例,善用此文档可以大幅提高开发效率。

View File

@ -147,6 +147,8 @@ export function patchMyFunctions() {
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。 然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。
**渲染端和数据端均可以调用此接口来重写函数!**
::: warning ::: warning
**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容! **注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容!
::: :::

View File

@ -89,7 +89,7 @@ type ElementLocator = [
### 缓存属性 ### 缓存属性
可以通过 `cache``nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `cache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下: 可以通过 `cache``nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `nocache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
```tsx ```tsx
// 内部渲染内容比较简单,不需要启用缓存 // 内部渲染内容比较简单,不需要启用缓存
@ -504,7 +504,7 @@ export interface WinskinProps extends BaseProps {
| 属性分类 | 关键参数 | 说明 | | 属性分类 | 关键参数 | 说明 |
| -------------- | ------------------------- | -------------------------------------------------------- | | -------------- | ------------------------- | -------------------------------------------------------- |
| **填充与描边** | `fill` `stroke` | 控制是否填充/描边(不同元素默认值不同) | | **填充与描边** | `fill` `stroke` | 控制是否填充/描边(不同元素默认值不同) |
| **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色/渐变等(如 `'#f00'` | | **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色等(如 `'#f00'` |
| **线型设置** | `lineWidth` `lineDash` | 线宽、虚线模式(如 `[5, 3]` 表示 5 像素实线+3 像素间隙) | | **线型设置** | `lineWidth` `lineDash` | 线宽、虚线模式(如 `[5, 3]` 表示 5 像素实线+3 像素间隙) |
| **高级控制** | `fillRule` `actionStroke` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 | | **高级控制** | `fillRule` `actionStroke` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 |

View File

@ -31,7 +31,7 @@ watch(data, () => mySprite.value?.update());
## 我的 UI 很卡 ## 我的 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` 方法,来确保可以被垃圾回收 - 在删除你手动存储的元素的同时,调用它们的 `destroy` 方法,来确保可以被垃圾回收
- 在控制台输入 `Mota.require('@motajs/render').MotaOffscreenCanvas2D.list` 来查看当前还有哪些画布正在使用,游玩一段时间后再次输入,检查数量是否增长,如果增长,说明发生了内存泄漏
- 确保组件卸载时已经清空了定时器等内容 - 确保组件卸载时已经清空了定时器等内容
- 如果需要每帧执行函数,请使用 `onTick` 接口,而非其他方法 - 如果需要每帧执行函数,请使用 `onTick` 接口,而非其他方法
如果你直接使用 `MotaOffscreenCanvas2D` 接口,请确保:
- 在使用前调用了 `activate` 方法
- 在使用后调用了 `deactivate` 方法
- 如果不需要再修改画布属性,只需要绘制,请调用 `freeze` 方法
- 如果之后不再使用该画布,请调用 `destroy` 方法
## 为什么我的滤镜不显示? ## 为什么我的滤镜不显示?
很遗憾截止目前2.B 发布日期IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础,可以在造塔群询问我或造塔辅助 AI 很遗憾截止目前2.B 发布日期IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础。
## 不同设备的显示内容会不一样吗? ## 不同设备的显示内容会不一样吗?

15
docs/guide/ui-future.md Normal file
View File

@ -0,0 +1,15 @@
# UI 系统未来规划
本章介绍一下 UI 系统的未来更新计划。
## 样式系统
提供类似于 CSS 的样式系统,不再需要在标签中指定颜色,而只需要定义样式,即可实现丰富的样式设定,包含继承功能。
## 布局系统
提供类似于 `flex` `grid` 布局的布局系统,即列表布局和网格布局,此时不需要手动指定元素坐标即可自动分配布局。
## 调试工具
提供 UI 调试工具,查看渲染树的属性等。

View File

@ -12,9 +12,9 @@ lang: zh-CN
画布渲染树的深度遍历特性使得: 画布渲染树的深度遍历特性使得:
- 每个独立容器的更新会触发子树的重新渲染 - 每个独立容器的更新会触发子树的重新渲染
- 容器层级过深会增加递归调用栈开销 - 容器层级过深会增加递归调用栈开销
- 合理分组可将高频/低频更新元素隔离 - 合理分组可将高频/低频更新元素隔离
下面是代码示例: 下面是代码示例:
@ -100,7 +100,7 @@ const render = () => {
## 使用 `cache``nocache` 标识 ## 使用 `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 ```tsx
const render = (canvas: MotaOffscreenCanvas2D) => { const render = (canvas: MotaOffscreenCanvas2D) => {

View File

@ -6,6 +6,8 @@
样板提供 `UIController` 类,允许你在自己的一个 UI 中创建自己的 UI 管理器,例如在样板中,游戏画面本身包含一个 UI 管理器,分为了封面、加载界面、游戏界面三种,其中游戏界面里面还有一个游戏 UI 管理器,我们常用的就是最后一个游戏 UI 管理器。 样板提供 `UIController` 类,允许你在自己的一个 UI 中创建自己的 UI 管理器,例如在样板中,游戏画面本身包含一个 UI 管理器,分为了封面、加载界面、游戏界面三种,其中游戏界面里面还有一个游戏 UI 管理器,我们常用的就是最后一个游戏 UI 管理器。
多数情况下,样板自带的 UI 管理器已经足够,不需要自己创建 UI 管理器。我们最常用的管理器就是 `mainUIController`,它控制了游戏界面下的 UI。
### 创建 UIController 实例 ### 创建 UIController 实例
我们从 `@motajs/system-ui` 引入 `UIController` 类,然后对其实例化: 我们从 `@motajs/system-ui` 引入 `UIController` 类,然后对其实例化:
@ -70,11 +72,11 @@ myController.lastOnly(false);
方法说明如下: 方法说明如下:
- `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI - `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI
- `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭 - `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭
- `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示 - `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示
- `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示 - `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示
- `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏 - `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏
那么,假如我们要做一个反向 `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);
### 实际效益 ### 实际效益
- 性能优化:减少像频繁开关灯的资源浪费 - 性能优化:减少像频繁开关灯的资源浪费
- 流畅保障:避免连续小改动导致的画面闪烁 - 流畅保障:避免连续小改动导致的画面闪烁
- 智能调度:优先处理用户可见区域的变化 - 智能调度:优先处理用户可见区域的变化

View File

@ -16,10 +16,10 @@ lang: zh-CN
```tsx ```tsx
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { GameUI, UIComponentProps } from '@motajs/system-ui'; import { GameUI, UIComponentProps, DefaultProps } from '@motajs/system-ui';
import { SetupComponentOptions } from '../components'; import { SetupComponentOptions } from '../components';
export interface MyBookProps extends UIComponentProps {} export interface MyBookProps extends UIComponentProps, DefaultProps {}
const myBookProps = { const myBookProps = {
props: ['controller', 'instance'] props: ['controller', 'instance']

View File

@ -9,7 +9,7 @@ hero:
actions: actions:
- theme: brand - theme: brand
text: 深度指南 text: 深度指南
link: /guide/diff link: /guide/quick-start
- theme: alt - theme: alt
text: API列表 text: API列表
link: /api/index link: /api/index

View File

@ -23,6 +23,7 @@ import { getInput } from '../components';
import { openStatistics } from './statistics'; import { openStatistics } from './statistics';
import { saveWithExist } from './save'; import { saveWithExist } from './save';
import { compressToBase64 } from 'lz-string'; import { compressToBase64 } from 'lz-string';
import { ViewMapUI } from './viewmap';
export interface MainSettingsProps export interface MainSettingsProps
extends Partial<ChoicesProps>, extends Partial<ChoicesProps>,
@ -78,7 +79,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
break; break;
} }
case MainChoice.ViewMap: { case MainChoice.ViewMap: {
// todo props.controller.open(ViewMapUI, { loc: [0, 0, 840, 840] });
break; break;
} }
case MainChoice.Replay: { case MainChoice.Replay: {

View File

@ -42,6 +42,27 @@ export interface ILeftHeroStatus {
magicDef: number; 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<T> extends DefaultProps { interface StatusBarProps<T> extends DefaultProps {
loc: ElementLocator; loc: ElementLocator;
status: T; status: T;
@ -104,82 +125,80 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
openViewMap(mainUIController, [0, 0, 840, 480]); openViewMap(mainUIController, [0, 0, 840, 480]);
}; };
return () => { return () => (
return ( <container loc={p.loc} hidden={p.hidden}>
<container loc={p.loc} hidden={p.hidden}> <text
text={floorName.value}
loc={central(24)}
font={font1}
cursor="pointer"
onClick={viewMap}
></text>
<text text={s.lv} loc={central(54)} font={font1}></text>
<image image={hpIcon} loc={iconLoc(0)}></image>
<text text={f(s.hp)} loc={textLoc(0)} font={font1}></text>
<text
text={`+${f(s.regen)}/t`}
loc={right(110)}
font={font3}
fillStyle="#a7ffa7"
></text>
<image image={atkIcon} loc={iconLoc(1)}></image>
<text text={f(s.atk)} loc={textLoc(1)} font={font1}></text>
<text
text={`+${f(s.exAtk)}`}
loc={right(154)}
font={font3}
fillStyle="#ffd3d3"
></text>
<image image={defIcon} loc={iconLoc(2)}></image>
<text text={f(s.def)} loc={textLoc(2)} font={font1}></text>
{s.magicDef > 0 && (
<text <text
text={floorName.value} text={`+${f(s.magicDef)}`}
loc={central(24)} loc={right(198)}
font={font1}
cursor="pointer"
onClick={viewMap}
></text>
<text text={s.lv} loc={central(54)} font={font1}></text>
<image image={hpIcon} loc={iconLoc(0)}></image>
<text text={f(s.hp)} loc={textLoc(0)} font={font1}></text>
<text
text={`+${f(s.regen)}/t`}
loc={right(110)}
font={font3} font={font3}
fillStyle="#a7ffa7" fillStyle="#b0bdff"
></text> ></text>
<image image={atkIcon} loc={iconLoc(1)}></image> )}
<text text={f(s.atk)} loc={textLoc(1)} font={font1}></text> <image image={mdefIcon} loc={iconLoc(3)}></image>
<text <text text={f(s.mdef)} loc={textLoc(3)} font={font1}></text>
text={`+${f(s.exAtk)}`} <image image={moneyIcon} loc={iconLoc(4)}></image>
loc={right(154)} <text text={f(s.money)} loc={textLoc(4)} font={font1} />
font={font3} <image image={expIcon} loc={iconLoc(5)}></image>
fillStyle="#ffd3d3" <text text={f(s.exp)} loc={textLoc(5)} font={font1}></text>
></text> <text
<image image={defIcon} loc={iconLoc(2)}></image> text={key(s.yellowKey)}
<text text={f(s.def)} loc={textLoc(2)} font={font1}></text> loc={keyLoc(0)}
{s.magicDef > 0 && ( font={font2}
<text fillStyle="#fca"
text={`+${f(s.magicDef)}`} ></text>
loc={right(198)} <text
font={font3} text={key(s.blueKey)}
fillStyle="#b0bdff" loc={keyLoc(1)}
></text> font={font2}
)} fillStyle="#aad"
<image image={mdefIcon} loc={iconLoc(3)}></image> ></text>
<text text={f(s.mdef)} loc={textLoc(3)} font={font1}></text> <text
<image image={moneyIcon} loc={iconLoc(4)}></image> text={key(s.redKey)}
<text text={f(s.money)} loc={textLoc(4)} font={font1} /> loc={keyLoc(2)}
<image image={expIcon} loc={iconLoc(5)}></image> font={font2}
<text text={f(s.exp)} loc={textLoc(5)} font={font1}></text> fillStyle="#f88"
<text ></text>
text={key(s.yellowKey)} <text
loc={keyLoc(0)} text="技能树"
font={font2} loc={central(396)}
fillStyle="#fca" font={font1}
></text> cursor="pointer"
<text ></text>
text={key(s.blueKey)} <text
loc={keyLoc(1)} text="查看技能"
font={font2} loc={central(428)}
fillStyle="#aad" font={font1}
></text> cursor="pointer"
<text ></text>
text={key(s.redKey)} </container>
loc={keyLoc(2)} );
font={font2}
fillStyle="#f88"
></text>
<text
text="技能树"
loc={central(396)}
font={font1}
cursor="pointer"
></text>
<text
text="查看技能"
loc={central(428)}
font={font1}
cursor="pointer"
></text>
</container>
);
};
}, },
statusBarProps statusBarProps
); );
@ -191,27 +210,6 @@ interface RightStatusBarMisc {
valueColor: string; 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<StatusBarProps<IRightHeroStatus>>( export const RightStatusBar = defineComponent<StatusBarProps<IRightHeroStatus>>(
p => { p => {
const font1 = new Font('normal', 18); const font1 = new Font('normal', 18);

View File

@ -18,8 +18,6 @@ import {
} from '@motajs/types'; } from '@motajs/types';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
// todo: 光环划分优先级,从而可以实现光环的多级运算
export interface UserEnemyInfo extends EnemyInfo { export interface UserEnemyInfo extends EnemyInfo {
togetherNum?: number; togetherNum?: number;
} }
@ -253,7 +251,7 @@ export class DamageEnemy implements IDamageEnemy {
for (const [key, value] of Object.entries(enemy)) { for (const [key, value] of Object.entries(enemy)) {
if (!(key in this.info) && has(value)) { if (!(key in this.info) && has(value)) {
// @ts-ignore // @ts-expect-error 无法推导
this.info[key] = value; this.info[key] = value;
} }
} }
@ -580,7 +578,6 @@ export class DamageEnemy implements IDamageEnemy {
* *
*/ */
calDamage(hero: Partial<HeroStatus> = core.status.hero) { calDamage(hero: Partial<HeroStatus> = core.status.hero) {
// todo: 缓存怪物伤害
const enemy = this.getRealInfo(); const enemy = this.getRealInfo();
return this.calEnemyDamageOf(hero, enemy); return this.calEnemyDamageOf(hero, enemy);
} }
@ -756,7 +753,6 @@ export class DamageEnemy implements IDamageEnemy {
num: number = 1, num: number = 1,
hero: Partial<HeroStatus> = core.status.hero hero: Partial<HeroStatus> = core.status.hero
): CriticalDamageDelta[] { ): CriticalDamageDelta[] {
// todo: 缓存临界
const origin = this.calDamage(hero); const origin = this.calDamage(hero);
const seckill = this.getSeckillAtk(); const seckill = this.getSeckillAtk();
return this.calCriticalWith(num, seckill, origin, hero); return this.calCriticalWith(num, seckill, origin, hero);
@ -775,7 +771,6 @@ export class DamageEnemy implements IDamageEnemy {
origin: DamageInfo, origin: DamageInfo,
hero: Partial<HeroStatus> hero: Partial<HeroStatus>
): CriticalDamageDelta[] { ): CriticalDamageDelta[] {
// todo: 可以优化,根据之前的计算可以直接确定下一个临界的范围
if (!isFinite(seckill)) return []; if (!isFinite(seckill)) return [];
const res: CriticalDamageDelta[] = []; const res: CriticalDamageDelta[] = [];
@ -830,6 +825,7 @@ export class DamageEnemy implements IDamageEnemy {
start = curr; start = curr;
} }
if (i++ >= 10000) { if (i++ >= 10000) {
// eslint-disable-next-line no-console
console.warn( console.warn(
`Unexpected endless loop in calculating critical.` + `Unexpected endless loop in calculating critical.` +
`Enemy Id: ${this.id}. Loc: ${this.x},${this.y}. Floor: ${this.floorId}` `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( export function calDamageWith(
info: UserEnemyInfo, info: UserEnemyInfo,
hero: Partial<HeroStatus> hero: Partial<HeroStatus>
): number | null { ): number {
const { hp, mdef } = core.status.hero; const { mdef } = core.status.hero;
let { atk, def, hpmax, mana, magicDef } = hero as HeroStatus; const { def, mana, magicDef } = hero as HeroStatus;
let { hp: monHp, atk: monAtk, def: monDef, special, enemy } = info; const { hp: monHp, def: monDef, special, enemy } = info;
let { atk, hpmax } = hero as HeroStatus;
let { atk: monAtk } = info;
// 赏金,优先级最高 // 赏金,优先级最高
if (special.has(34)) return 0; if (special.has(34)) return 0;
@ -969,15 +967,15 @@ export function calDamageWith(
// 绝对防御 // 绝对防御
if (special.has(9)) { if (special.has(9)) {
heroPerDamage = atk + mana - monDef; heroPerDamage = atk + mana - monDef;
if (heroPerDamage <= 0) return null; if (heroPerDamage <= 0) return Infinity;
} else if (special.has(3)) { } else if (special.has(3)) {
// 由于坚固的特性,只能放到这来计算了 // 由于坚固的特性,只能放到这来计算了
if (atk > enemy.def) heroPerDamage = 1 + mana; if (atk > enemy.def) heroPerDamage = 1 + mana;
else return null; else return Infinity;
} else { } else {
heroPerDamage = atk - monDef; heroPerDamage = atk - monDef;
if (heroPerDamage > 0) heroPerDamage += mana; if (heroPerDamage > 0) heroPerDamage += mana;
else return null; else return Infinity;
} }
// 霜冻 // 霜冻

View File

@ -1,3 +1,9 @@
import { createMechanism } from './mechanism';
export function create() {
createMechanism();
}
export * from './enemy'; export * from './enemy';
export * from './mechanism'; export * from './mechanism';
export * from './state'; export * from './state';

View File

@ -1,2 +1,4 @@
export function createMechanism() {}
export * from './misc'; export * from './misc';
export * from './skillTree'; export * from './skillTree';

View File

@ -19,4 +19,5 @@ export function create() {
function createModule() { function createModule() {
LegacyPluginData.createLegacy(); LegacyPluginData.createLegacy();
DataState.create();
} }

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { createServer } from 'vite'; import { createServer } from 'vite';
import { Server } from 'http'; import { Server } from 'http';
import { ensureDir, move, pathExists, remove } from 'fs-extra'; import { ensureDir, move, pathExists, remove } from 'fs-extra';