mirror of
https://github.com/motajs/template.git
synced 2026-11-15 08:14:30 +08:00
init: 2.B
This commit is contained in:
commit
6bf32174d8
610 files changed
+151444
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# 动画效果
|
||||
|
||||
2.B 提供了专门的动画接口,允许你用短短几行就可以做出一个效果不错的动画。本节主要讲述的是 UI 中的动画。
|
||||
|
||||
:::warning
|
||||
`mutate-animate` 库在未来会重构,可能会修改引入方式,但整体逻辑不会怎么变。
|
||||
:::
|
||||
|
||||
## 定义动画属性
|
||||
|
||||
我们以一个自定义 UI 为例,来讲述如何编写一段动画。自定义 UI 参考[此指南](./new-ui.md)和[此教程](../../guide/ui/ui.md)。
|
||||
|
||||
假设自定义 UI 在 `packages-user/client-modules/src/render/ui` 文件夹下,我们需要引入 `transitioned` 接口,并调用它定义动画属性:
|
||||
|
||||
```tsx {8-13}
|
||||
// 引入接口,如果文件夹不同,注意路径关系
|
||||
import { transitioned } from '../use';
|
||||
// 从 mutate-animate 库中引入速率曲线函数
|
||||
import { hyper } from 'mutate-animate';
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 定义动画属性
|
||||
const value = transitioned(
|
||||
1, // 初始值是 1
|
||||
500, // 动画时长 500ms
|
||||
hyper('sin', 'out') // 速率曲线是双曲正弦函数(此曲线观感较好)
|
||||
)!;
|
||||
// 上一行的感叹号为非空断言,因为此接口在组件或 UI 外调用会返回 null,
|
||||
// 而我们在 UI 内,因此我们需要断言其不可能为 null
|
||||
|
||||
return () => <container></container>;
|
||||
});
|
||||
```
|
||||
|
||||
注意 `transitioned` 只能在 UI 或组件的顶层调用,不能在 UI 的函数内调用,也不可在 UI 外调用,在其他地方调用会返回 `null`!
|
||||
|
||||
## 添加到元素属性
|
||||
|
||||
定义属性后,我们需要把属性添加到元素上,直接使用 `value.ref.value` 即可,这里以不透明度 `alpha` 属性为例:
|
||||
|
||||
```tsx {9}
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 定义动画属性
|
||||
const value = transitioned(1, 500, hyper('sin', 'out'))!;
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
{/* 将 value.ref.value 赋值给 alpha 属性 */}
|
||||
<g-rect rect={[0, 0, 100, 100]} alpha={value.ref.value} />
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 执行动画
|
||||
|
||||
最后,在我们需要的时候执行动画即可,使用 `value.set`:
|
||||
|
||||
```tsx
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 定义动画属性
|
||||
const value = transitioned(1, 500, hyper('sin', 'out'))!;
|
||||
|
||||
// 例如在 UI 挂载完毕后执行
|
||||
onMounted(() => {
|
||||
// 动画属性调成 0,效果就是矩形逐渐从不透明变成完全透明
|
||||
value.set(0);
|
||||
});
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<g-rect rect={[0, 0, 100, 100]} alpha={value.ref.value} />
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 拓展-颜色动画
|
||||
|
||||
除了一般数值的动画外,样板还支持颜色动画,使用 `transitionedColor` 接口,用法与 `transitioned` 基本一致:
|
||||
|
||||
```tsx {23}
|
||||
// 引入接口,如果文件夹不同,注意路径关系
|
||||
import { transitionedColor } from '../use'; // [!code ++]
|
||||
// 从 mutate-animate 库中引入速率曲线函数
|
||||
import { hyper } from 'mutate-animate';
|
||||
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 定义颜色动画
|
||||
const color = transitionedColor(
|
||||
'#fff', // 初始颜色,设定为白色 // [!code ++]
|
||||
1000, // 动画时长 1s
|
||||
hyper('sin', 'out') // 动画速率曲线
|
||||
)!;
|
||||
|
||||
// 例如在 UI 挂载完毕后执行
|
||||
onMounted(() => {
|
||||
// 颜色调成纯黑
|
||||
color.set('#000'); // [!code ++]
|
||||
});
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
{/* 将 fillStyle 赋值为 color.ref.value */}
|
||||
<g-rect rect={[0, 0, 100, 100]} fillStyle={color.ref.value} />
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
颜色目前仅支持输入 `#RGB` `#RGBA` `#RRGGBB` `#RRGGBBAA` `rgb(r,g,b)` `rgba(r,g,b,a)` 几种格式的字符串。
|
||||
|
||||
## 拓展-配合交互
|
||||
|
||||
我们可以配合交互实现以下几种功能:
|
||||
|
||||
### 鼠标移入移出
|
||||
|
||||
当鼠标移入时高亮,当移出时恢复:
|
||||
|
||||
```tsx {8-11,19-20}
|
||||
import { transitionedColor } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 初始颜色为灰色
|
||||
const color = transitionedColor('#aaa', 200, hyper('sin', 'out'))!;
|
||||
|
||||
/** 鼠标移入时变成白色 */
|
||||
const enter = () => color.set('#fff');
|
||||
/** 鼠标移出时恢复为灰色 */
|
||||
const leave = () => color.set('#aaa');
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<g-rect
|
||||
rect={[0, 0, 100, 100]}
|
||||
fillStyle={color.ref.value}
|
||||
// 监听 enter 和 leave 事件
|
||||
onEnter={enter}
|
||||
onLeave={leave}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 点击切换状态
|
||||
|
||||
鼠标点击或触碰点击后高亮,再次点击恢复:
|
||||
|
||||
```tsx {8-15,23}
|
||||
import { transitionedColor } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
|
||||
export const MyCom = defineComponent(() => {
|
||||
// 初始颜色为灰色
|
||||
const color = transitionedColor('#aaa', 200, hyper('sin', 'out'))!;
|
||||
|
||||
let highlight = false;
|
||||
/** 点击时触发 */
|
||||
const click = () => {
|
||||
// 如果已经高亮,那么恢复为灰色,否则高亮为白色
|
||||
color.set(highlight ? '#aaa' : '#fff');
|
||||
// 切换状态
|
||||
highlight = !highlight;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<g-rect
|
||||
rect={[0, 0, 100, 100]}
|
||||
fillStyle={color.ref.value}
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 拓展-API 参考
|
||||
|
||||
参考[此 API 文档](../../api/user-client-modules/functions.md#transitioned)。
|
||||
@@ -0,0 +1,281 @@
|
||||
# 选择框与确认框
|
||||
|
||||
2.B 提供的简单的确认框与选择框接口,允许你用几行代码就可以弹出一个确认框让用户确认,或是弹出选择框让用户选择,不再需要像 2.x 一样用非常不好用的 `myconfirm` 或 `insertAction` 事件流了。
|
||||
|
||||
## 确认框
|
||||
|
||||
假设我们需要在点击一个按钮后弹出应该确认框,让玩家确认操作,可以使用 `getConfirm` 或 `routedConfirm` 接口。假设我们在 `packages-user/client-modules/src/render/ui` 文件夹下的 UI 中实现这一需求,我们可以这么写:
|
||||
|
||||
```tsx
|
||||
// 从 components 文件夹引入接口,注意路径关系
|
||||
import { getConfirm } from '../components';
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(props => {
|
||||
/** 当鼠标点击时触发,由于 getConfirm 是异步函数,这里的 click 也需要异步 */
|
||||
const click = async () => {
|
||||
// 调用接口,并等待执行完毕,获取异步返回值
|
||||
const confirm = await getConfirm(
|
||||
props.controller, // 使用打开当前 UI 的控制器作为 UI 控制器
|
||||
'确认要 XXX 吗?', // 提示文字
|
||||
[240, 240, void 0, void 0, 0.5, 0.5], // 确认框所处的位置,这里使用居中对齐,中心点在 (240,240) 位置
|
||||
240, // 确认框的宽度
|
||||
{
|
||||
// 这一参数具体看后面的解释
|
||||
selFill: 'gold' // 选项文字使用金色
|
||||
}
|
||||
);
|
||||
if (confirm) {
|
||||
// 如果用户选择了确认,可以执行确认内容,例如显示一个提示
|
||||
core.drawTip('用户确认');
|
||||
} else {
|
||||
// 如果用户选择了取消,可以执行取消内容
|
||||
core.drawTip('用户取消');
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<text
|
||||
text="这是一个按钮"
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
其中 `getConfirm` 的最后一个参数是一个对象,可以传入 `ConfirmBox` 组件的参数,具体可以参考[此文档](../../api/user-client-modules/组件%20ConfirmBox.md)。例如 `selFill` 是 `ConfirmBox` 控制选项填充样式的参数。
|
||||
|
||||
除此之外,还有一个 `routedConfirm` 接口,此接口与 `getConfirm` 引入方式相同,主要差别为 `routedConfirm` 会自动处理录像。但是这并不意味着用 `routedConfirm` 比 `getConfirm` 更好,因为我们一般会在客户端(渲染端)调用它,而客户端的内容在录像验证时是不会执行的,这就可能导致录像出错。一般情况下,我们只需要使用 `getConfirm` 接口,而需要使用 `routedConfirm` 的场景已经在样板中处理。
|
||||
|
||||
**总结成一句话就是**:一般情况下不要使用 `routedConfirm`,使用 `getConfirm` 即可。
|
||||
|
||||
## 选择框
|
||||
|
||||
选择框会给玩家提供一系列选项,让玩家选择某一项,并返回玩家选择的内容。使用 `getChoice` 接口,示例如下:
|
||||
|
||||
```tsx
|
||||
// 从 components 文件夹引入接口,注意路径关系
|
||||
import { getChoice } from '../components'; // [!code ++]
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(props => {
|
||||
// 选项内容,第一项是 id,第二项是该选项的显示内容
|
||||
// 第一项 id 可以填字符串或者数字
|
||||
const choices: ChoiceItem[] = [
|
||||
['key1', '选项1'], // 第一个选项
|
||||
['key2', '选项2'] // 第二个选项
|
||||
];
|
||||
|
||||
/** 同样,由于 getChoice 是异步函数,这里的 click 也需要异步 */
|
||||
const click = async () => {
|
||||
// 调用接口,并等待执行完毕,获取异步返回值
|
||||
const choice = await getChoice(
|
||||
props.controller, // 使用打开当前 UI 的控制器作为 UI 控制器
|
||||
choices, // 在这里传入选项内容
|
||||
[240, 240, void 0, void 0, 0.5, 0.5], // 选择框所处的位置
|
||||
240, // 选择框的宽度
|
||||
{
|
||||
// 可选参数,例如显示提示文字
|
||||
text: '请选择一项'
|
||||
}
|
||||
);
|
||||
// 使用 switch 判断
|
||||
switch (choice) {
|
||||
case 'key1':
|
||||
// 选择了第一个选项时
|
||||
core.drawTip('选择了第一项');
|
||||
break;
|
||||
// ... 其他判断
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<text
|
||||
text="这是一个按钮"
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
同样,`getChoice` 也有一个对应的 `routedChoice`,依然建议一般情况下只使用 `getChoice`,不使用 `routedChoice`。
|
||||
|
||||
## 拓展-使用枚举定义选择框
|
||||
|
||||
如果使用字符串定义选择框的 `id`,一来没有严格的类型标注,二来字符串的性能也较差,因此我们推荐使用枚举定义选择框的 `id`。首先我们先编写一段枚举:
|
||||
|
||||
```ts
|
||||
// 定义选项枚举,可以写一些有意义的单词辅助记忆
|
||||
const enum MyChoice {
|
||||
Choice1, // 第一个选项
|
||||
Choice2, // 第二个选项
|
||||
Choice3 // 第三个选项
|
||||
}
|
||||
```
|
||||
|
||||
然后在选择框定义中使用枚举定义:
|
||||
|
||||
```ts
|
||||
const choices: Choices<MyChoice>[] = [
|
||||
[MyChoice.Choice1, '选项1'],
|
||||
[MyChoice.Choice2, '选项2'],
|
||||
[MyChoice.Choice3, '选项3']
|
||||
];
|
||||
|
||||
// 这里不变
|
||||
const choice = await getChoice(
|
||||
props.controller
|
||||
choices
|
||||
[240, 240, void 0, void 0, 0.5, 0.5]
|
||||
240
|
||||
);
|
||||
|
||||
// 可以用 switch 判断
|
||||
switch (choice) {
|
||||
case MyChoice.Choice1:
|
||||
// 选择选项1时
|
||||
break;
|
||||
// ... 其他内容
|
||||
}
|
||||
```
|
||||
|
||||
## 拓展-等待框
|
||||
|
||||
等待框也是一种类似于确认框的东西,不过它用来等待一个操作执行完毕,同时给用户显示一个界面,可以是复杂逻辑运算,也可以是网络请求等。以等待网络请求为例:
|
||||
|
||||
```tsx
|
||||
// 从 components 文件夹引入接口,注意路径关系
|
||||
import { waitbox } from '../components';
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(props => {
|
||||
const click = async () => {
|
||||
// 等待网络请求,同时展示一个等待框让玩家不孤单
|
||||
const response = await waitbox(
|
||||
props.controller, // 控制器
|
||||
[240, 240, void 0, void 0, 0.5, 0.5], // 位置
|
||||
240, // 宽度
|
||||
fetch('/api/example') // 一个 fetch 请求
|
||||
);
|
||||
// 之后就可以直接使用了
|
||||
const body = response.body;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<text
|
||||
text="这是一个按钮"
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 拓展-输入框
|
||||
|
||||
与选择框、确认框类似,只不过允许玩家输入一段内容,然后返回给程序。使用 `getInput` 来让玩家输入字符串,使用 `getInputNumber` 来让玩家输入数字。
|
||||
|
||||
输入框包含一个确认键和取消键,玩家点击确认键时会将输入结果返回,而如果点击了取消,那么会返回空字符串或 `NaN`。
|
||||
|
||||
### 输入字符串
|
||||
|
||||
使用 `getInput` 接口:
|
||||
|
||||
```tsx
|
||||
import { getInput } from '../components';
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(props => {
|
||||
const click = async () => {
|
||||
// 调用接口,并等待执行完毕,获取异步返回值
|
||||
const inputData = await getInput(
|
||||
props.controller, // UI 控制器
|
||||
'请输入一句话', // 显示的文字
|
||||
[240, 240, void 0, void 0, 0.5, 0.5], // 输入框位置
|
||||
240, // 输入框宽度
|
||||
// 其他参数配置,参考 API 文档
|
||||
{
|
||||
// 例如设置一个占位符
|
||||
input: {
|
||||
placeholder: '输入一句话'
|
||||
}
|
||||
}
|
||||
);
|
||||
if (inputData.length === 0) {
|
||||
// 如果用户没有输入任何内容或点了取消
|
||||
} else {
|
||||
// 如果用户输入了内容
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<text
|
||||
text="这是一个按钮"
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 输入数字
|
||||
|
||||
与 `getInput` 类似,不过要用 `getInputNumber` 接口:
|
||||
|
||||
```tsx
|
||||
import { getInputNumber } from '../components';
|
||||
|
||||
// UI 模板及如何编写 UI 参考 “新增 UI” 需求指南,这里只给出必要的修改部分,模板部分不再给出
|
||||
export const MyCom = defineComponent(props => {
|
||||
const click = async () => {
|
||||
// 调用接口,并等待执行完毕,获取异步返回值
|
||||
const num = await getInputNumber(
|
||||
props.controller, // UI 控制器
|
||||
'请输入一个数字', // 显示的文字
|
||||
[240, 240, void 0, void 0, 0.5, 0.5], // 输入框位置
|
||||
240, // 输入框宽度
|
||||
// 其他参数配置,参考 API 文档
|
||||
{
|
||||
// 例如设置一个占位符
|
||||
input: {
|
||||
placeholder: '输入数字'
|
||||
}
|
||||
}
|
||||
);
|
||||
if (isNaN(num)) {
|
||||
// 如果用户输入的不是数字或点了取消
|
||||
} else {
|
||||
// 如果用户输入了数字
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<text
|
||||
text="这是一个按钮"
|
||||
// 监听 click 事件
|
||||
onClick={click}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## 拓展-API参考
|
||||
|
||||
- [ConfirmBox](../../api/user-client-modules/组件%20ConfirmBox.md)
|
||||
- [Choices](../../api/user-client-modules/组件%20Choices.md)
|
||||
- [WaitBox](../../api/user-client-modules/组件%20WaitBox.md)
|
||||
- [getConfirm](../../api/user-client-modules/functions.md#getconfirm)
|
||||
- [getChoice](../../api/user-client-modules/functions.md#getchoice)
|
||||
- [waitbox 方法](../../api/user-client-modules/functions.md#waitbox)
|
||||
@@ -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/use.ts,注意路径关系
|
||||
import { useKey } from '../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 '../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)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 常见需求的实现指南
|
||||
|
||||
一般情况下,我们只需要在 `packages-user` 文件夹下编写代码。而在此文件夹中,多数文件夹又是样板处理,我们基本只会在这些文件夹中编写:
|
||||
|
||||
- `packages-user/client-modules`:客户端代码
|
||||
- `packages-user/data-state`:数据端代码
|
||||
|
||||
## 客户端内容
|
||||
|
||||
以下内容中,一级列表是基础需求指南,二级列表是拓展需求指南。
|
||||
|
||||
- [修改状态栏显示](./status-bar.md)
|
||||
- [可交互按钮](./status-bar.md#拓展-可交互按钮)
|
||||
- [新增勇士属性](./status-bar.md#拓展-新增勇士属性) (既包含客户端,也包含数据端)
|
||||
- [编写新 UI](./new-ui.md)
|
||||
- [UI 与组件的区别](./new-ui.md#拓展-ui-与组件的区别)
|
||||
- [UI 编写参考](../ui/ui.md)
|
||||
- [新增按键](./hotkey.md)
|
||||
- [添加辅助按键](./hotkey.md#拓展-添加辅助按键)
|
||||
- [在 UI 内实现按键](./hotkey.md#拓展-在-ui-内实现按键)
|
||||
- [单功能多按键](./hotkey.md#拓展-单功能多按键)
|
||||
- [按下时触发](./hotkey.md#拓展-按下时触发)
|
||||
- [动画效果](./animate.md)
|
||||
- [颜色动画](./animate.md#拓展-颜色动画)
|
||||
- [配合交互](./animate.md#拓展-配合交互)
|
||||
- [选择框与确认框](./choice.md)
|
||||
- [使用枚举定义选择框](./choice.md#拓展-使用枚举定义选择框)
|
||||
- [等待框](./choice.md#拓展-等待框)
|
||||
- [输入框](./choice.md#拓展-输入框)
|
||||
|
||||
## 数据端内容
|
||||
|
||||
以下内容中,一级列表是基础需求指南,二级列表是拓展需求指南。
|
||||
|
||||
- [怪物特殊属性](./special.md)
|
||||
- [用函数声明属性](./special.md#拓展-用函数声明属性)
|
||||
- [地图伤害](./special.md#拓展-地图伤害)
|
||||
- [光环属性](./special.md#拓展-光环属性)
|
||||
- [主动技能](./skill.md)
|
||||
- [多技能设计思路](./skill.md#拓展-多技能设计思路)
|
||||
- [战后自动关闭技能](./skill.md#拓展-战后自动关闭技能)
|
||||
- [在开启或关闭技能时执行内容](./skill.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<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/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);
|
||||
```
|
||||
@@ -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: EnemyInfo,
|
||||
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: EnemyInfo,
|
||||
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: EnemyInfo,
|
||||
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;
|
||||
// ... 其他判断
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,265 @@
|
||||
# 怪物特殊属性
|
||||
|
||||
下面以特殊属性“勇士造成的伤害减少 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`,在文件最后的 `calDamageWithTurn` 函数中编写:
|
||||
|
||||
```ts
|
||||
export function calDamageWithTurn(
|
||||
info: EnemyInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): DamageWithTurn {
|
||||
// ... 原有内容
|
||||
|
||||
// 在需要降低勇士伤害的地方将勇士伤害乘以 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` `calDamageWithTurn` 中的实现:
|
||||
|
||||
```ts
|
||||
export function calDamageWithTurn(
|
||||
info: EnemyInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): DamageWithTurn {
|
||||
// ... 原有内容
|
||||
|
||||
// 在乘以 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`: 左上角坐标+宽高的矩形
|
||||
- `manhattan`: 曼哈顿距离,坐标之和小于半径
|
||||
|
||||
我们以曼哈顿距离为例,展示如何自定义形状。
|
||||
|
||||
首先在开头找到 `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;
|
||||
}
|
||||
);
|
||||
```
|
||||
@@ -0,0 +1,227 @@
|
||||
# 修改状态栏显示
|
||||
|
||||
在 `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>;
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## 拓展-新增勇士属性
|
||||
|
||||
在上例中,展示了如何显示一个自定义的 `flag`,但有时候我们需要自定义一个勇士属性,例如攻速、减伤等,这些属性可能会受到全局 `buff` 的影响,这时候使用 `flag` 就不方便,我们推荐使用自定义属性的方式。
|
||||
|
||||
### 定义属性
|
||||
|
||||
我们打开编辑器的全塔属性界面,点击左侧上方的编辑表格按钮,向下滑动找到 `勇士攻击` 等勇士属性(约第 250 行)的位置,仿照这些属性增加一个新的属性,例如添加攻速,取属性名为 `atkSpeed`:
|
||||
|
||||
```js
|
||||
{
|
||||
"mana": {
|
||||
"_leaf": true,
|
||||
"_type": "textarea",
|
||||
"_data": "初始魔力"
|
||||
},
|
||||
"atkSpeed": { // [!code ++]
|
||||
"_leaf": true, // [!code ++]
|
||||
"_type": "textarea", // [!code ++]
|
||||
"_data": "初始攻速" // [!code ++]
|
||||
}, // [!code ++]
|
||||
"atk": {
|
||||
"_leaf": true,
|
||||
"_type": "textarea",
|
||||
"_data": "初始攻击"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
保存后刷新页面,再次进入全塔属性界面,可以看到在勇士属性部分多了一项 `初始攻速`,我们可以输入初始值,例如设为初始值 `1`,表示攻速为 `100%`。
|
||||
|
||||
接下来回到 `vscode`,打开文件 `src/types/declaration/status.d.ts`,按下 `ctrl+F` 搜索 `interface HeroStatus`,在其中新增一个属性值 `atkSpeed`:
|
||||
|
||||
```ts
|
||||
interface HeroStatus {
|
||||
// ... 原有内容
|
||||
|
||||
/** 勇士攻速 */
|
||||
atkSpeed: number; // [!code ++]
|
||||
|
||||
// ... 原有内容
|
||||
}
|
||||
```
|
||||
|
||||
### 容错处理
|
||||
|
||||
为了保证属性添加前的存档还能正常加载,我们需要容错处理,打开编辑器,进入脚本编辑界面,编辑 `重置游戏` 这一脚本编辑项。我们在函数的最后进行容错处理:
|
||||
|
||||
```js
|
||||
function () {
|
||||
// ... 原有内容
|
||||
|
||||
// 容错处理
|
||||
core.status.hero.atkSpeed ??= 1; // [!code ++]
|
||||
}
|
||||
```
|
||||
|
||||
由于样板编辑器的限制,上述代码会有语法报错,但实际上不会有任何问题,关闭语法检查再保存即可。
|
||||
|
||||
### 状态栏显示
|
||||
|
||||
与本文章最初的示例基本一致,只有传入属性值时需要略加变动,其他操作包括属性定义等不改变,将赋值行为改为 `getHeroStatusOn`:
|
||||
|
||||
```ts
|
||||
leftStatus.atk = getHeroStatusOn('atk');
|
||||
leftStatus.hp = getHeroStatusOn('hp');
|
||||
leftStatus.def = getHeroStatusOn('def');
|
||||
// ... 原有内容
|
||||
|
||||
// 改为 getHeroStatusOn
|
||||
leftStatus.atkSpeed = getHeroStatusOn('atkSpeed');
|
||||
```
|
||||
|
||||
状态栏组件中:
|
||||
|
||||
```tsx
|
||||
// 使用模板字符串,显示百分比
|
||||
<text text={`${s.atkSpeed * 100}%`} />
|
||||
```
|
||||
|
||||
### 属性实现
|
||||
|
||||
为了实现勇士属性,我们需要修改伤害计算逻辑。我们打开 `packages-user/data-state/src/enemy/damage.ts`,翻到最后找到 `calDamageWith` 函数,在它上面有一个名为 `realStatus` 的数组,我们在这里新增一项 `atkSpeed`:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 计算伤害时会用到的勇士属性,攻击防御,其余的不会有buff加成,直接从core.status.hero取
|
||||
*/
|
||||
const realStatus: (keyof HeroStatus)[] = [
|
||||
'atk',
|
||||
'def',
|
||||
// ... 原有内容
|
||||
|
||||
// 新增 atkSpeed 属性
|
||||
'atkSpeed' // [!code ++]
|
||||
];
|
||||
```
|
||||
|
||||
然后在 `calDamageWith` 伤害计算中修改伤害计算,给勇士每回合造成的伤害乘以攻速,注意放置的位置:
|
||||
|
||||
```ts
|
||||
export function calDamageWith(
|
||||
info: EnemyInfo,
|
||||
hero: Partial<HeroStatus>
|
||||
): number {
|
||||
// ... 原有逻辑
|
||||
|
||||
// 乘以攻速
|
||||
heroPerDamage *= hero.atkSpeed ?? 1;
|
||||
|
||||
// ... 原有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 拓展-了解 UI 编写的基本逻辑
|
||||
|
||||
参考[此文档](../ui/ui.md),此文档将会教你如何从头开始编写一个 UI,并解释 UI 运行与渲染的基本逻辑。
|
||||
Reference in new issue
Block a user