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