mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-08-28 00:23:55 +08:00
docs: 常见需求指南
This commit is contained in:
parent
138481e637
commit
a10af04b79
@ -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']
|
||||
|
@ -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');
|
||||
```
|
||||
|
@ -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)组件。
|
||||
|
||||
---
|
||||
|
||||
|
@ -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,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';
|
||||
```
|
||||
|
@ -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}`);
|
||||
});
|
||||
```
|
||||
|
256
docs/guide/implements/hotkey.md
Normal file
256
docs/guide/implements/hotkey.md
Normal 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)
|
17
docs/guide/implements/index.md
Normal file
17
docs/guide/implements/index.md
Normal 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)
|
115
docs/guide/implements/new-ui.md
Normal file
115
docs/guide/implements/new-ui.md
Normal 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);
|
||||
```
|
346
docs/guide/implements/skill.md
Normal file
346
docs/guide/implements/skill.md
Normal 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;
|
||||
// ... 其他判断
|
||||
}
|
||||
}
|
||||
```
|
288
docs/guide/implements/special.md
Normal file
288
docs/guide/implements/special.md
Normal 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;
|
||||
}
|
||||
```
|
114
docs/guide/implements/status-bar.md
Normal file
114
docs/guide/implements/status-bar.md
Normal 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.x,2.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
69
docs/guide/quick-start.md
Normal 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.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` 开源协议,这要求所有以此为基础开发的项目也必须完全开源,但考虑到很多作者不了解其中的细节,因此样板将会针对此问题进行处理,处理方案为:**将源码原封不动地打包为压缩包,放到构建完成的游戏中**,届时,只要在网站上下载游戏,就可以解压压缩包查看源码。
|
||||
|
||||
## 学会查阅此文档
|
||||
|
||||
此文档内容很丰富,大部分接口文档都有使用示例,善用此文档可以大幅提高开发效率。
|
@ -147,6 +147,8 @@ export function patchMyFunctions() {
|
||||
|
||||
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。
|
||||
|
||||
**渲染端和数据端均可以调用此接口来重写函数!**
|
||||
|
||||
::: warning
|
||||
**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容!
|
||||
:::
|
||||
|
@ -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` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 |
|
||||
|
||||
|
@ -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` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础。
|
||||
|
||||
## 不同设备的显示内容会不一样吗?
|
||||
|
||||
|
15
docs/guide/ui-future.md
Normal file
15
docs/guide/ui-future.md
Normal file
@ -0,0 +1,15 @@
|
||||
# UI 系统未来规划
|
||||
|
||||
本章介绍一下 UI 系统的未来更新计划。
|
||||
|
||||
## 样式系统
|
||||
|
||||
提供类似于 CSS 的样式系统,不再需要在标签中指定颜色,而只需要定义样式,即可实现丰富的样式设定,包含继承功能。
|
||||
|
||||
## 布局系统
|
||||
|
||||
提供类似于 `flex` `grid` 布局的布局系统,即列表布局和网格布局,此时不需要手动指定元素坐标即可自动分配布局。
|
||||
|
||||
## 调试工具
|
||||
|
||||
提供 UI 调试工具,查看渲染树的属性等。
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
||||
### 实际效益
|
||||
|
||||
- 性能优化:减少像频繁开关灯的资源浪费
|
||||
- 流畅保障:避免连续小改动导致的画面闪烁
|
||||
- 智能调度:优先处理用户可见区域的变化
|
||||
- 性能优化:减少像频繁开关灯的资源浪费
|
||||
- 流畅保障:避免连续小改动导致的画面闪烁
|
||||
- 智能调度:优先处理用户可见区域的变化
|
||||
|
@ -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']
|
||||
|
@ -9,7 +9,7 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 深度指南
|
||||
link: /guide/diff
|
||||
link: /guide/quick-start
|
||||
- theme: alt
|
||||
text: API列表
|
||||
link: /api/index
|
||||
|
@ -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<ChoicesProps>,
|
||||
@ -78,7 +79,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
|
||||
break;
|
||||
}
|
||||
case MainChoice.ViewMap: {
|
||||
// todo
|
||||
props.controller.open(ViewMapUI, { loc: [0, 0, 840, 840] });
|
||||
break;
|
||||
}
|
||||
case MainChoice.Replay: {
|
||||
|
@ -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<T> extends DefaultProps {
|
||||
loc: ElementLocator;
|
||||
status: T;
|
||||
@ -104,82 +125,80 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
|
||||
openViewMap(mainUIController, [0, 0, 840, 480]);
|
||||
};
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<container loc={p.loc} hidden={p.hidden}>
|
||||
return () => (
|
||||
<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={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)}
|
||||
text={`+${f(s.magicDef)}`}
|
||||
loc={right(198)}
|
||||
font={font3}
|
||||
fillStyle="#a7ffa7"
|
||||
fillStyle="#b0bdff"
|
||||
></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={`+${f(s.magicDef)}`}
|
||||
loc={right(198)}
|
||||
font={font3}
|
||||
fillStyle="#b0bdff"
|
||||
></text>
|
||||
)}
|
||||
<image image={mdefIcon} loc={iconLoc(3)}></image>
|
||||
<text text={f(s.mdef)} loc={textLoc(3)} font={font1}></text>
|
||||
<image image={moneyIcon} loc={iconLoc(4)}></image>
|
||||
<text text={f(s.money)} loc={textLoc(4)} font={font1} />
|
||||
<image image={expIcon} loc={iconLoc(5)}></image>
|
||||
<text text={f(s.exp)} loc={textLoc(5)} font={font1}></text>
|
||||
<text
|
||||
text={key(s.yellowKey)}
|
||||
loc={keyLoc(0)}
|
||||
font={font2}
|
||||
fillStyle="#fca"
|
||||
></text>
|
||||
<text
|
||||
text={key(s.blueKey)}
|
||||
loc={keyLoc(1)}
|
||||
font={font2}
|
||||
fillStyle="#aad"
|
||||
></text>
|
||||
<text
|
||||
text={key(s.redKey)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
)}
|
||||
<image image={mdefIcon} loc={iconLoc(3)}></image>
|
||||
<text text={f(s.mdef)} loc={textLoc(3)} font={font1}></text>
|
||||
<image image={moneyIcon} loc={iconLoc(4)}></image>
|
||||
<text text={f(s.money)} loc={textLoc(4)} font={font1} />
|
||||
<image image={expIcon} loc={iconLoc(5)}></image>
|
||||
<text text={f(s.exp)} loc={textLoc(5)} font={font1}></text>
|
||||
<text
|
||||
text={key(s.yellowKey)}
|
||||
loc={keyLoc(0)}
|
||||
font={font2}
|
||||
fillStyle="#fca"
|
||||
></text>
|
||||
<text
|
||||
text={key(s.blueKey)}
|
||||
loc={keyLoc(1)}
|
||||
font={font2}
|
||||
fillStyle="#aad"
|
||||
></text>
|
||||
<text
|
||||
text={key(s.redKey)}
|
||||
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
|
||||
);
|
||||
@ -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<StatusBarProps<IRightHeroStatus>>(
|
||||
p => {
|
||||
const font1 = new Font('normal', 18);
|
||||
|
@ -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<HeroStatus> = 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<HeroStatus> = 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<HeroStatus>
|
||||
): 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<HeroStatus>
|
||||
): 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;
|
||||
}
|
||||
|
||||
// 霜冻
|
||||
|
@ -1,3 +1,9 @@
|
||||
import { createMechanism } from './mechanism';
|
||||
|
||||
export function create() {
|
||||
createMechanism();
|
||||
}
|
||||
|
||||
export * from './enemy';
|
||||
export * from './mechanism';
|
||||
export * from './state';
|
||||
|
@ -1,2 +1,4 @@
|
||||
export function createMechanism() {}
|
||||
|
||||
export * from './misc';
|
||||
export * from './skillTree';
|
||||
|
@ -19,4 +19,5 @@ export function create() {
|
||||
|
||||
function createModule() {
|
||||
LegacyPluginData.createLegacy();
|
||||
DataState.create();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import { createServer } from 'vite';
|
||||
import { Server } from 'http';
|
||||
import { ensureDir, move, pathExists, remove } from 'fs-extra';
|
||||
|
Loading…
Reference in New Issue
Block a user