18 Commits
Author SHA1 Message Date
OLDREDSTONE d2f80e8390 webhoop 2025-10-27 19:20:48 +08:00
OLDREDSTONE 8faac81f8f 1026 2025-10-26 22:32:11 +08:00
OLDREDSTONE 139f1eef71 样式更新:表格行颜色交替显示优化,提升可读性 2025-10-21 22:00:11 +08:00
OLDREDSTONE 90af884c2c 添加精灵相关内容 2025-10-18 14:10:53 +08:00
OLDREDSTONE 96a90c364d 日常维护 2025-10-18 11:39:30 +08:00
OLDREDSTONE f902bd4ec6 更新字体 2025-10-16 11:57:01 +08:00
OLDREDSTONE 91737a0f95 banner 又加回来了 2025-10-11 23:06:03 +08:00
OLDREDSTONE ca02d2e195 添加了 7th beat games 授权 2025-09-27 16:48:46 +08:00
OLDREDSTONE 30d84f613b 删除 Banner 2025-08-31 19:20:45 +08:00
OLDREDSTONE 4f08e1e53a 对不起巧麦( 2025-08-01 22:11:00 +08:00
OLDREDSTONE ac39d732b7 添加了本地状态记录 2025-07-21 00:48:56 +08:00
OLDREDSTONE 239e15c01d Merge branch 'others' of https://git.obugs.cn/RDCNWebs/rd.rdlevel.cn into others 2025-07-19 21:17:58 +08:00
OLDREDSTONE bfa44b33c4 添加了使用协议 2025-07-19 21:17:48 +08:00
obugs fac884caf2 Merge branch 'main' into others 2025-07-19 18:55:41 +08:00
OLDREDSTONE e3d6c8c572 0719 更新 2025-07-19 18:49:07 +08:00
obugs 5e91af323b Merge pull request 'others' (#1) from others into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#1
2025-06-28 16:50:20 +08:00
OLDREDSTONE c7b6c35ddc 0628 更新 2025-06-28 16:48:49 +08:00
OLDREDSTONE 5dab36288b 修改了 README 2025-06-26 14:10:14 +08:00
48 changed files with 2115 additions and 279 deletions
+69
View File
@@ -0,0 +1,69 @@
项目综合授权条款
1. 源代码与原创资源
路径:
所有源码文件、
/indexicon/ 下的所有文件
所有以下未提及部分
授权:MIT License
2. 文档内容
路径:
/pages/ 下的所有 .md 文件
授权: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
3. 其他作者的美术
路径:
/resources/ 下的所有文件
/images/intro-qqgroup.png
/images/otto-dark.gif
/images/otto-light.gif
/images/otto-puzzled-original.png
/images/otto-puzzled.png
/images/otto-rd.png
/images/otto-rd2-1.png
/images/otto-rd2-xmas-compressed.png
/images/otto-rd2-xmas.png
/images/otto-traditional.png
/images/otto-wyym.png
授权: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
4. 受限游戏美术
路径:
/images/ 内的截图、临摹图
授权:仅限本教程在线演示,禁止复制、再分发
5. 商业字体
字体文件未包含在仓库
说明:已购个人授权,不可再分发;本地部署请另行购买或改用免费可商用字体
Copyright (c) 2025 节奏医生饭制部制作组
Comprehensive License Notice
1. Source Code & Original Assets
Scope:
All source files
All files in /indexicon/
All other unmentioned parts
License: MIT License
2. Documentation
Scope:
All .md files in /pages/
License: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
3. Other Authors' Art
Scope:
/resources/ directory
/images/intro-qqgroup.png
/images/otto-dark.gif
/images/otto-light.gif
/images/otto-puzzled-original.png
/images/otto-puzzled.png
/images/otto-rd.png
/images/otto-rd2-1.png
/images/otto-rd2-xmas-compressed.png
/images/otto-rd2-xmas.png
/images/otto-traditional.png
/images/otto-wyym.png
License: Creative Commons Attribution-NonCommercial 4.0 International (CC-BY-NC 4.0)
4. Restricted Game Art
Scope:
Screenshots or derivative art in /images/
License: Display-only for this tutorial; no copying or redistribution
5. Commercial Fonts
Font files are NOT included in the repository
Note: personal license only; for local builds obtain separate license or replace with free-for-commercial-use fonts
Copyright (c) 2025 节奏医生饭制部制作组
+20 -10
View File
@@ -1,20 +1,30 @@
# 节奏医生中文编辑器教程网站
> 此版本网页正在维护。 ## 预览和编辑
# 节奏医生中文编辑器教程 ### [Visual Studio Code](https://code.visualstudio.com/)
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。未经授权不得以图文,视频等形式转载,也不得以本教程内素材进行二次创作。 此项目基于 [docsify](https://docsify.js.org/#/) 编辑,并使用 [RDView](https://github.com/OLDRedstone/RDView) 显示编辑器可交互模块。
编辑器视频教程点击[此链接](https://www.bilibili.com/video/BV1pr4y1k7gP)。教程内所有演示关卡下载放置于[百度网盘](https://pan.baidu.com/s/1Tb6ywVmge25ztMFnpa6q_Q),提取码:rdlv。 - 确保 [安装 markdown-rdview-support](https://github.com/OLDRedstone/markdown-rdview-support) 即可预览 RDView 内容。
如果成功安装,那么以下内容的 markdown 预览应当是一个事件栏。
> 如果你看到[更新信息](/pages/changelog.md)增加了,但是又感觉什么也没更新,那就试着按一下`Ctrl + F5`,再不行就清空浏览器缓存。 ```rdview 7 5 1
es.PlaySong 0 0[];
es.SetCrotchetsPerBar_4 0 1[];
eb.AddClassicBeat 0 2[tick=6];
```
## 联系我们 - 确保 [安装 docsify](https://docsify.js.org/#/quickstart) 以实时预览页面。
![我们的 QQ 群号是:951070053](/images/intro-02.png) ```bash
npm i docsify-cli -g
```
如果你有做谱相关的问题,或是对本教程有什么意见和建议,可以点击链接加入 QQ 群 [951070053](https://jq.qq.com/?_wv=1027&k=nsPA09E4) 告知我们。你也可以在群里上传谱面给我们试玩,群友们都会热心为你提供谱面优化建议。真诚欢迎各位做谱萌新和大佬的加入。 - 运行以下命令以启动本地服务器实时预览。
(悄悄透露一句:官方也入驻了这个群哦~) ```bash
docsify s
```
戳这只看板娘可以获得随机 TIPS → Game assets courtesy of [7th Beat Games](https://7thbe.at/). Game assets are excluded from this site's license.
-1
View File
@@ -1 +0,0 @@
<!-- _navbar.md -->
+6 -3
View File
@@ -28,11 +28,12 @@
- [<img src="/indexicon/settheme.png"/><p>场景与主题</p>](/pages/settheme.md) - [<img src="/indexicon/settheme.png"/><p>场景与主题</p>](/pages/settheme.md)
- [<img src="/indexicon/rowfx.png"/><p>轨道视效</p>](/pages/rowfx.md) - [<img src="/indexicon/rowfx.png"/><p>轨道视效</p>](/pages/rowfx.md)
- 进阶:节奏设计 - 进阶:节奏设计
- [<img src="/indexicon/moreclassicbeats.png"/><p>普通拍的衍生节奏型</p>](/pages/moreclassicbeats.md) - [<img src="/indexicon/moreclassicbeats.png"/><p>普通拍的衍生节奏型</p>](/pages/moreclassicbeats.md)
- [<img src="/indexicon/moreoneshotbeats.png"/><p>单发拍的衍生节奏型</p>](/pages/moreoneshotbeats.md) - [<img src="/indexicon/moreoneshotbeats.png"/><p>单发拍的衍生节奏型</p>](/pages/moreoneshotbeats.md)
- [<img src="/indexicon/beatpatterndesign.png"/><p>采音和节奏设计</p>](/pages/beatpatterndesign.md) - [<img src="/indexicon/beatpatterndesign.png"/><p>采音和节奏设计</p>](/pages/beatpatterndesign.md)
- [<img src="/indexicon/chartdesign.png"/><p>关卡设计</p>](/pages/chartdesign.md) - [<img src="/indexicon/chartdesign.png"/><p>关卡设计</p>](/pages/chartdesign.md)
- [<img src="/indexicon/audiodesign.png"/><p>音效设计</p>](/pages/audiodesign.md) - [<img src="/indexicon/audiodesign.png"/><p>音效设计</p>](/pages/audiodesign.md)
- [<img src="/indexicon/twoplayers.png" width="64px"/><p>双人模式设计</p>](/pages/twoplayers.md)
- [<img src="/indexicon/accessibledesign.png"/><p>无障碍设计</p>](/pages/accessibledesign.md) - [<img src="/indexicon/accessibledesign.png"/><p>无障碍设计</p>](/pages/accessibledesign.md)
- 进阶:视觉特效 - 进阶:视觉特效
- [<img src="/indexicon/room.png"/><p>房间</p>](/pages/room.md) - [<img src="/indexicon/room.png"/><p>房间</p>](/pages/room.md)
@@ -52,6 +53,9 @@
- [<img src="/indexicon/custommethod.png"/><p>自定义方法</p>](/pages/custommethod.md) - [<img src="/indexicon/custommethod.png"/><p>自定义方法</p>](/pages/custommethod.md)
- [<img src="/indexicon/condition.png"/><p>条件</p>](/pages/condition.md) - [<img src="/indexicon/condition.png"/><p>条件</p>](/pages/condition.md)
- [<img src="/indexicon/variable.png"/><p>变量</p>](/pages/variable.md) - [<img src="/indexicon/variable.png"/><p>变量</p>](/pages/variable.md)
- 高级:第三方工具
- [<img src="/indexicon/3rdparty_coderd.png"/><p>[Python]CodeRD</p>](/pages/3rdparty_coderd.md)
- [<img src="/indexicon/3rdparty_rdtk.png"/><p>[C#]RhythmBase</p>](/pages/3rdparty_rdtk.md)
- 附录 - 附录
- [<img src="/indexicon/events.png"/><p>事件列表</p>](/pages/appendix_events.md) - [<img src="/indexicon/events.png"/><p>事件列表</p>](/pages/appendix_events.md)
- [<img src="/indexicon/vfxs.png"/><p>预设特效列表</p>](/pages/appendix_vfxs.md) - [<img src="/indexicon/vfxs.png"/><p>预设特效列表</p>](/pages/appendix_vfxs.md)
@@ -61,6 +65,5 @@
- [<img src="/indexicon/custommethods.png"/><p>自定义方法表</p>](/pages/appendix_custommethods.md) - [<img src="/indexicon/custommethods.png"/><p>自定义方法表</p>](/pages/appendix_custommethods.md)
- [<img src="/indexicon/specifications.png"/><p>制谱规范</p>](/pages/appendix_levelrules.md) - [<img src="/indexicon/specifications.png"/><p>制谱规范</p>](/pages/appendix_levelrules.md)
- [<img src="/indexicon/comparison.png"/><p>英汉对照</p>](/pages/appendix_translations.md) - [<img src="/indexicon/comparison.png"/><p>英汉对照</p>](/pages/appendix_translations.md)
- [<img src="/indexicon/coding.png"/><p>其他编写关卡的手段</p>](/pages/appendix_coding.md)
- [<img src="/indexicon/tools.png" width="64px"/><p>工具</p>](/pages/appendix_tools.md) - [<img src="/indexicon/tools.png" width="64px"/><p>工具</p>](/pages/appendix_tools.md)
- [<img src="/indexicon/credits.png"/><p>制作人员</p>](/pages/credits.md) - [<img src="/indexicon/credits.png"/><p>制作人员</p>](/pages/credits.md)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 4.9 MiB

+5 -4
View File
@@ -26,7 +26,7 @@
homepage: '/pages/intro.md', homepage: '/pages/intro.md',
loadSidebar: true, loadSidebar: true,
loadNavbar: true, loadNavbar: true,
coverpage: true, coverpage: false,
maxLevel: 5, maxLevel: 5,
subMaxLevel: 4, subMaxLevel: 4,
mergeNavbar: true, mergeNavbar: true,
@@ -43,6 +43,7 @@
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
loadRDView() loadRDView()
const originalCodeRenderer = $docsify.markdown?.renderer?.code;
$docsify = { $docsify = {
...$docsify, ...$docsify,
...{ ...{
@@ -52,10 +53,10 @@
if (lang.trim().startsWith('rdview')) { if (lang.trim().startsWith('rdview')) {
return RDViewRender(lang, code); return RDViewRender(lang, code);
} }
return `<pre><code>${escapeHtml(code)}</code></pre>`; return originalCodeRenderer ? originalCodeRenderer(code, lang) : `<pre data-lang="${lang}"><code>${escapeHtml(code)}</code></pre>`;
}, },
image: function (src, title, alt) { image: function (src, title, alt) {
return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${alt?`<p>${alt}</p>`:""}</div>`; return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${alt ? `<p>${alt}</p>` : ""}</div>`;
}, },
} }
}, },
@@ -85,7 +86,7 @@
<script src="/script/docsify-copy-code.min.js"></script> <script src="/script/docsify-copy-code.min.js"></script>
<script src="/script/switchLightDarkMode.js"></script> <script src="/script/switchLightDarkMode.js"></script>
<script src="/script/otto.js"></script> <script src="/script/otto.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-yaml.js"></script>
<script type="module" src="/script/special.js"></script> <script type="module" src="/script/special.js"></script>
</body> </body>
</html> </html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

@@ -1,3 +1,4 @@
[项目地址](https://github.com/RDCN-Community-Developers/CodeRD)
# 代码编写关卡 # 代码编写关卡
点击[此处](../resources/compiler.exe)下载。项目作者:硫酸铜。项目来源:[PyRD - 为视障谱师写节奏医生谱子的一种全新的尝试](https://github.com/qwedc001/pyrd)。 点击[此处](../resources/compiler.exe)下载。项目作者:硫酸铜。项目来源:[PyRD - 为视障谱师写节奏医生谱子的一种全新的尝试](https://github.com/qwedc001/pyrd)。
+769
View File
@@ -0,0 +1,769 @@
[项目地址](https://github.com/RDCN-Community-Developers/RhythmToolkit)
[English](/pages/3rdparty_rdtk_EN.md) | 中文
# RhythmBase 使用教程
本项目为节奏医生关卡开发者服务,旨在为开发人员提供更加系统、直观的关卡编辑媒介。
感谢节奏医生饭制部玩家对这个项目的支持。
欢迎来到 RhythmBase 教程。本指南将帮助您开始在项目中设置和使用 RhythmBase。
## 安装 Nuget 包
要安装 RhythmBase NuGet 包,请按照以下步骤操作:
1. 打开 Visual Studio 或使用命令行工具。
2. 在 Visual Studio 中,依次点击 **工具** > **NuGet 包管理器** > **程序包管理器控制台**
3. 在控制台中输入以下命令:
```bash
Install-Package RhythmBase -Version 1.2.0-rc2
```
4. 等待安装完成,并确保项目已引用所需的 NuGet 包。
5. 如果使用 .NET CLI,请输入:
```bash
dotnet add package RhythmBase -version 1.2.0-rc2
```
## 编写
### 创建关卡
关卡是一个事件集合, 如果使用 C# 12 或更高版本, 可以使用[集合表达式](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/collection-expressions)以创建空关卡。
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel emptyLevel = [];
Console.WriteLine(emptyLevel); // "" Count = 0
```
也可以使用自带的模板以创建具有基础事件的关卡。
此模板即为节奏医生编辑器默认创建的关卡模板。
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel defaultLevel = RDLevel.Default;
Console.WriteLine(defaultLevel); // "" Count = 3
```
### 读取和写入
可以直接以文件路径读取和导出文件。将以默认读写设置读写文件。
导出时不会打包为关卡包。
```cs
using RhythmBase.RhythmDoctor.Components;
// 直接读取关卡文件
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel");
// 读取关卡包文件
using RDLevel rdlevel2 = RDLevel.Read(@"your\level.rdzip");
// 读取关卡压缩包
using RDLevel rdlevel3 = RDLevel.Read(@"your\level.zip");
// 写入关卡文件
rdlevel1.Write(@"your\outLevel.rdlevel");
```
可以添加自定义读写设置 `LevelReadOrWriteSettings` 以读写关卡。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Settings;
// 编写自定义读写设置
LevelReadOrWriteSettings settings = new()
{
// 对未激活事件的处理方式
InactiveEventsHandling = InactiveEventsHandling.Store,
// 对读取出现异常的事件的处理方式
// 常见于精灵事件未与精灵轨道绑定等
UnreadableEventsHandling = UnreadableEventHandling.Store,
// 是否启用缩进
Indented = true,
};
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel", settings);
```
也可以生成 json 对象或 json 字符串以进行进一步操作。
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Components;
JObject jobject = rdlevel.ToJObject();
string json = rdlevel.ToRDLevelJson(settings);
```
`LevelReadOrWriteSettings` 添加了 `BeforeReading`, `AfterReading`, `BeforeWriting`, `AfterWriting` 事件,分别会在关卡读取之前、之后,写入之前、之后触发。
可在这些事件上添加监听以达到特定效果。
```cs
using RhythmBase.RhythmDoctor.Settings;
settings.AfterWriting += Settings_AfterReading;
// 将在写入结束时触发
void Settings_AfterReading(object? sender, EventArgs e)
{
throw new NotImplementedException();
}
rdlevel.Write(@"your\outLevel.rdlevel", settings);
```
> 在读取关卡压缩包文件时请使用 `using` 语句或主动调用 `RDLevel.Dispose()` 方法以保证被解压的临时文件被及时销毁。
### 查找和获取事件
`OrderedEventCollection` 类型用于存储事件集合, `RDLevel` 继承此类型。
可以使用针对节奏医生关卡事件的查询操作的扩展方法, 用以简化查询操作。
例如通过事件类型及其父类型、事件实现的接口、节拍范围、自定义谓词等。
提供 `AddRange()`, `RemoveRange()`, `Where()`, `RemoveAll()`, `First()`, `FirstOrDefault()`, `Last()`, `LastOrDefault()`, `TakeWhile()` 等扩展方法。
推荐使用这些经过效率优化的方法。
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// 查找在第 3 到第 5 小节、在事件栏的第 0 到第 2 行的移动轨道事件
var list = rdlevel.Where<MoveRow>(
i => 0 <= i.Y && i.Y < 3, // 在事件栏的第 0 到第 2 行
3..5 // 第 3 到第 5 小节
);
```
`Row` 和 `Decoration` 也继承 `OrderedEventCollection`, 所以轨道和精灵也支持这些扩展方法。
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// 查找在第 11 小节第 1 拍到第 13 小节第 1 拍的普通拍子事件
var list = rdlevel.Decorations[0].Where<Tint>(
new Beat(11, 1), // 查找起点为第 11 小节第 1 拍
new Beat(13, 1) // 查找终点为第 13 小节第 1 拍
);
```
### 创建节拍
`RDBeat` 是一个结构体, 它缓存三个信息 `BeatOnly`, `BarBeat`和`TimeSpan`。
可以创建一个不与关卡相关的 `RDBeat` 实例, 但因为缺失和关卡的联系, 其功能可能不完善。
可以查看它的 `IsEmpty` 属性以了解此实例是否可用。
在没有关卡联系的情况下, 调用它的 `ToString()` 方法会显示此实例拥有的信息和缺失的信息。
```cs
using RhythmBase.RhythmDoctor.Components;
// 创建与关卡无关联的节拍
RDBeat beat1 = new(11);
RDBeat beat2 = new(2, 3);
RDBeat beat3 = new(TimeSpan.FromSeconds(11.45));
Console.WriteLine(beat1); // [10,?,?]
Console.WriteLine(beat2); // [?,(2, 3),?]
Console.WriteLine(beat3); // [?,?,00:00:11.4500000]
```
可以依靠 `BeatCalculator` 实例或 `RDLevel` 实例创建一个和关卡关联的 `RDBeat` 实例。
其中 `BeatCalculator` 伴随 `RDLevel` 的创建而创建, 可通过 `RDLevel.Calculator` 访问。
在有关卡联系的情况下, 调用它的 `ToString()` 方法会显示 `BarBeat` 属性。
只有与关卡关联, 三个属性才能建立联系, 节拍才能参与所有运算。否则只有相应属性存在数据才能运算。
关卡内事件和书签的节拍属性都是与关卡相关联的, 而移出的事件会断开与关卡的关联。
```cs
using RhythmBase.RhythmDoctor.Components;
// 创建与关卡有关联的节拍
RDBeat beat1 = rdlevel.BeatOf(11);
RDBeat beat2 = rdlevel.Calculator.BeatOf(2, 3);
RDBeat beat3 = beat1 - 10 + TimeSpan.FromSeconds(11.45);
Console.WriteLine(beat1); // [2,3]
Console.WriteLine(beat2); // [2,3]
Console.WriteLine(beat3); // [3,4.083334]
```
在节拍之间参与运算时, 若都有所链接的关卡, 需确保其指向的关卡相同。
可调用 `FromSameLevel()` 或 `FromSameLevelOrNull()` 方法检查其是否指向相同关卡。
可调用 `WithoutLink()` 返回一个脱离关卡的新节拍实例。
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat beat1 = rdlevel.BeatOf(1);
RDBeat beat2 = beat1.WithoutLink();
Console.WriteLine(beat1.FromSameLevel(beat2)); // False
Console.WriteLine(beat1.FromSameLevelOrNull(beat2)); // True
```
`BeatCalculator` 也带有时间转换的方法, 可以对不同的时间单位进行转换。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Utils;
(float, float) barbeat = rdlevel.Calculator.TimeSpanToBarBeat(TimeSpan.FromSeconds(19.19)); // (4, 8.983334)
```
`RDLevel` 带有一个和此实例关联的节拍数为 1 的默认节拍。
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat @default = RDLevel.DefaultBeat;
```
`RDRange` 是一个与 `Range` 相似的数据类型, 用以表示一个节拍范围。
常用于查询事件。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Extension;
var result = rdlevel.Where(new RDRange(rdlevel.DefaultBeat + 10, null));
```
### 扩展数据类型
以 `RD` 开头, 带有 `Point`, `Size`, `Rect`, `RotatedRect` 的名称的类型都是 `RhythmBase` 里与平面几何相关的数据类型。
后缀带有 `I` 的为整数类型, 其所有数据属性都为 `int`, 如 `RDPointI.X`。
后缀带有 `N` 的为非空类型, 其所有数据属性都为不可空类型, 如 `RDSizeN.Height`。
后缀带有 `E` 的为表达式类型, 其所有数据属性都为 `RDExpression`, 如 `RDRectE.Size`。
`RotatedRect` 的 `Angle` 不受命名规则 `I` 约束, 其始终为浮点数类型。
`RDExpression` 用以存储节奏医生表达式并尝试解析和求值(没做完), 以字符串创建, 并支持简单的运算。
底层为字符串拼接, 所以当然版本运算会导致表达式嵌套多层括号是正常现象。
```cs
using RhythmBase.RhythmDoctor.Components;
RDExpression exp1 = new("i2+1");
RDExpression exp2 = new(30);
RDExpression exp3 = new("25.5");
RDExpression result = exp1 - exp2 * exp3;
Console.WriteLine(result); // i2+1-765
```
### 创建事件和增删事件
所有事件都直接或间接实现 `IBaseEvent` 接口并继承 `BaseEvent` 抽象类型。
可以将这些接口与抽象类型作为查询扩展方法的泛型参数以筛选事件。
例如,
`BaseRowAction`, `BaseDecorationAction` 分别是轨道事件和精灵事件,
`IRoomEvent` 是拥有多房间属性的事件。
创建事件所使用的节拍参数可以与关卡无关联, 当事件被添加进关卡时会创建与关卡的关联, 而移除关卡时也会切断关联。
若不提供节拍参数则默认为关卡的第 1 拍。
当调用事件的 `ToString()` 方法时, 将会以事件的节拍, 事件的类型, 事件的可显示数据的形式返回一个字符串。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
Comment comment = new() { Beat = new(12), Text = "My_comment." };
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
rdlevel.Add(comment);
Console.WriteLine(comment); // [2,4] Comment My_comment.
rdlevel.Remove(comment);
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
```
特别地,添加、更改、移除 `SetCrotchetsPerBar` 事件时会更新此事件之后的时间线,所以无需担心修改其数值时会影响事件的顺序、排布等;它们会按其自身的绝对节拍固定在相应的位置。关卡也会尝试增加新的 `SetCrotchetsPerBar` 事件或移除相同 `CrotchetsPerBar` 属性的相邻事件以维持其他片段的稳定。
轨道和精灵事件需要在相应的轨道或精灵上调用 `Add()` 进行添加, 而移除可以在轨道, 精灵或关卡的任意一处调用 `Remove()` 方法移除。
重复添加不会有任何效果。
事件类型 `Comment` 和 `TintRows` 不受此限制。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RDLevel rdlevel = RDLevel.Default;
MoveRow tr = new();
Console.WriteLine(rdlevel); // "" Count = 3
rdlevel.Add(tint); // "" Count = 3
rdlevel.Rows[0].Add(tr);
Console.WriteLine(rdlevel); // "" Count = 4
rdlevel.Remove(tr);
Console.WriteLine(rdlevel); // "" Count = 3
```
### 自定义事件
如果此程序集没有需要的事件类型,可以继承 `CustomEvent`, `CustomRowEvent` 或 `CustomDecorationEvent` 以实现类型。
```cs
using Newtonsoft.RhythmDoctor.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
// 创建 MyEvent 类型
// 继承自 CustomEvent 类型
public class MyEvent : CustomEvent
{
// 重写属性
public override Tabs Tab => Tabs.Actions;
// 实现的属性都需要和 CustomEvent.Data 字段内的数据绑定和判空。
// 实现一个 RDPointE 类型属性
public RDPointE? MyProperty
{
get
{
// 在 Data 字段内获取所需要的内容并判空
var value = Data["myProperty"];
return value?.ToObject<RDPointE?>() ?? new RDPointE(0, 0);
}
set
{
// 将内容保存在 Data 字段内
Data["myProperty"] =
value.HasValue ?
new JArray(
value?.X ?? null,
value?.Y ?? null) :
null;
}
}
// 在构造函数内初始化类型
public MyEvent()
{
// 初始化 RealType 属性。
ActureType = nameof(MyEvent);
}
}
```
编写好类型后可以像正常事件一样被读写。
值得注意的是,`Type` 仍然是 `EventType.CustomEvent`, 而 `ActureType` 是自定义类型名。
```cs
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
MyEvent myEvent = new();
rdlevel.Add(myEvent);
myEvent.Beat = new(8);
Console.WriteLine(myEvent.Type); // CustomEvent
Console.WriteLine(myEvent.ActureType); // MyEvent
```
另外,如果读取关卡时意外出现未知事件类型,其也会被读取为相应的 `CustomEvent`, `CustomRowEvent` 或 `CustomDecorationEvent` 类型事件。
### 事件类型与枚举
事件都拥有属性 `EaseType`, 可以通过 `EventTypeUtils` 内的方法转换得到对应的类型。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(EventType.Tint.ToType()); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToType("Tint")); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToEnum(typeof(Tint))); // Tint
Console.WriteLine(EventTypeUtils.ToEnum<Tint>()); // Tint
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums(typeof(IBarBeginningEvent)))); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums<IBarBeginningEvent>())); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
```
`EventTypeUtils` 也包含一些事件类型的归类,如:
```cs
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(string.Join(",\n", EventTypeUtils.DecorationTypes));
// Comment,
// CustomDecorationEvent,
// Move,
// PlayAnimation,
// SetVisible,
// Tile,
// Tint
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForCameraFX));
// MoveCamera,
// ShakeScreen,
// FlipScreen,
// PulseCamera
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForUtility));
// Comment,
// TagAction,
// CallCustomMethod
```
### 富文本和对话组件
富文本组件位于 `RhythmBase.Components.RichText` 命名空间下,可以通过 `+` 运算组合自定义颜色的富文本。同时支持富文本的序列化和反序列化。
`RDLine<>` 是一个完整的富文本。
`RDPhrase<>` 是富文本的一个样式片段,其遵循单个样式。
使用实现了 `IRDRichStringStyle<>` 的结构体以指明此富文本遵循的样式规则。下例的 `RDRichStringStyle` 即为仅带有颜色样式的富文本。
都可以从 string 隐式转换。注意,转换的文字为不带样式的富文本。
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo");
Console.WriteLine(line.ToString()); // Hello
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color>
line +=
new RDPhrase<RDRichStringStyle>(" Rhythm")
{
Style = new()
{
Color = RDColor.Lime
}
};
line += " Doctor!";
Console.WriteLine(line.ToString()); // Hello Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo Rhythm</color> Doctor!
```
`RDLine<>` 和 `RDPhrase<>` 都可以使用索引访问和修改其片段。
```cs
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo Rhythm</color> Doctor!");
Console.WriteLine(line[6..].ToString()); // Rhythm Doctor!
Console.WriteLine(line[6..].Serialize()); // <color=lime>Rhythm</color> Doctor!
line[5] = " and Welcome to ";
Console.WriteLine(line.ToString()); // Hello and Welcome to Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color> and Welcome to <color=lime>Rhythm</color> Doctor!
return;
```
此包内也提供一整套适配节奏医生对话格式的对话组件,用以模块化构造对话事件的文本内容,减少错误率。
```cs
using RhythmBase.Global.Components.RichText;
RDDialogueExchange exchange =
[
new RDDialogueBlock()
{
Character = "Paige",
Expression = "neutral",
Content = RDLine<RDDialoguePhraseStyle>.Deserialize("Hel<color=#00FF00>lo [2]<shake>Rhythm</color> Doctor</shake>!"),
},
new RDDialogueBlock()
{
Character = "Ian",
Content = "Hello Paige!",
},
new RDDialogueBlock()
{
Character = "Paige",
Expression = "happy",
Content = new RDPhrase<RDDialoguePhraseStyle>("What a good day!")
{
Events =
[
new RDDialogueTone(RDDialogueToneType.VerySlow,6),
new RDDialogueTone(RDDialogueToneType.Static,11),
],
Style = new RDDialoguePhraseStyle()
{
Volume = 0.5f,
Bold = true,
},
}
}
];
Console.WriteLine(exchange.Serialize());
// Paige_neutral:Hel<color=lime>lo [2]<shake>Rhythm</color> Doctor</shake>!
// Ian:Hello Paige!
// Paige_happy:<volume=0.5><bold>What a[vslow] good[static] day!</volume></bold>
```
### 缓动
引入 `RhythmBase.Global.Components.Easing` 之后,可以很方便地使用 `EaseType` 枚举常量。
同时也可以使用扩展方法 `Calculate()` 快速计算过渡值。
```cs
using RhythmBase.Global.Components.Easing;
double var1 = EaseType.InSine.Calculate(0.25);
double var2 = EaseType.Linear.Calculate(0.5,4,9);
Console.WriteLine(var1); // 0.07612046748871326
Console.WriteLine(var2); // 6.5
```
`EaseValue` 是一个简单但强大的结构体。得益于 mfgujhgh 的算法,通过使用静态方法 `Fit()` 处理任意数据,`EaseValue` 可以使用给定的 `EaseType` 选择列表拟合出可被节奏医生事件使用的曲线。
```cs
using RhythmBase.Global.Components.Easing;
// 使用点集和阈值拟合
EaseValue data1 = EaseValue.Fit([
(0, 0),
(1, 1)
], 3f);
// 使用初始值、点集、可选缓动类型列表和阈值拟合
EaseValue data2 = EaseValue.Fit(0, [
(0, 0),
(1, 1)
], [EaseType.Linear, EaseType.InSine], 3f);
// 从缓动数据中获取指定时刻的值
float value = data1.GetValue(2.5f);
```
`RhythmBase.Extensions.EasePropertyExtensions` 为实现了 `IEaseEvent` 的类型添加了一个 `GetEaseProperties` 方法,用于获取这一系列事件的各个属性的缓动曲线。
```cs
using RhythmBase.Global.Components;
using RhythmBase.Global.Components.Easing;
using RhythmBase.Global.Events;
using RhythmBase.RhythmDoctor.Extensions;
var deco = level.Decorations[0];
Move[] moves =
[
new(){ Beat = level.BeatOf(1), Position = new(0, (RDExpression?)null), Duration = 1,Ease = EaseType.Linear, Angle = "2" },
new(){ Beat = level.BeatOf(2.1f), Position = new(10, 90), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(2.2f), Position = new(90, 10), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3), Position = new(10, 70), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.5f), Position = new(10, 10), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.8f), Position = new(30, 50), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.9f), Position = new(20, (RDExpression?)null), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.1f), Position = new(70, 20), Duration = 1,Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.4f), Position = new((RDExpression?)null, 0), Duration = 1,Ease = EaseType.Linear },
];
deco.AddRange(moves);
var eases = EasePropertyExtensions.GetEaseProperties(moves);
foreach(var e in eases)
Console.WriteLine(e);
// [Position, RhythmBase.Components.Easing.EasePropertyPoint]
// [Scale, RhythmBase.Components.Easing.EasePropertySize]
// [Angle, RhythmBase.Components.Easing.EasePropertyFloat]
// [Pivot, RhythmBase.Components.Easing.EasePropertyPoint]
```
实现 `IEaseProperty<>` 接口的类型,例如 `EasePropertyColor``EasePropertyFloat``EasePropertyPoint``EasePropertySize`,用于存储来自缓动事件列表的内容。
同样也可以获取相应时刻的值。
> 在调用之前请检查属性的值类型,并应用相应的类型转换。
```cs
using RhythmBase.Global.Components.Easing;
var result = ((EasePropertyPoint)eases["Position"]).GetValue(rdlevel.BeatOf(3.2f));
Console.WriteLine(result); // [59.759995, 21.840006]
```
### 表达式(重写中)
`RhythmBase.Components.RDLang.RDLang` 提供一个 `TryRun()` 方法,用于运行节奏医生表达式。
> 注意,若表达式不正确则会返回 `false`,同时结果为 `0`。
`RDLang` 同时有一个静态字段 `Variables`,用于存储所有常用变量与方法。在执行 `TryRun` 之前修改字段会影响执行时的值。
`RDLang` 也支持三个常用方法 `Rand()``atLeastRank()``atLeastNPerfects()`,这些方法都可以在 `RDVariables` 上正常访问。
```cs
using RhythmBase.RhythmDoctor.Components.RDLang;
RDLang.Variables.i[1] = 9;
RDLang.TryRun("numMistakesP2 = 3", out float result); // 3
RDLang.TryRun("numMistakesP2+i1", out result); // 12
RDLang.TryRun("atLeastRank(A)", out result); // 1
```
由于此库不支持动态播放关卡,可使用以下字段对后两个函数的效果进行模拟:
- `atLeastRank()`
使用 `RDVariables.SimulateCurrentRank` 属性更改模拟关卡评级状态。
当表达式访问 `atLeastRank()` 方法时使用此值进行模拟。
- `atLeastNPerfects()`
使用 `RDVariables.SimulateAtLeastNPerfectsSuccessRate` 属性更改模拟成功击拍百分比。
当表达式访问 `atLeastNPerfects()` 方法时使用此值进行模拟。
### 事件组
像一个新事件一样声明并使用它,它会按照您指定的方式生成关卡事件!
像下面这样继承 `Group` 类,实现新的逻辑后,您可以像其他事件一样自由地操纵它。
这与 `CustomEvent` 不同,它会用指定的事件序列写入关卡,而不是它本身。
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
public class GroupData1
{
public RDSize Size;
public int RowIndex;
}
public class MoveCameraRectangle : Group<GroupData1>
{
public RDSize Size
{
get => Data.Size;
set => Data.Size = value;
}
public Row Row
{
get => Rows?[Data.RowIndex] ?? [];
set => Data.RowIndex = value.Index;
}
public MoveCameraRectangle() { }
public override IEnumerable<BaseEvent> GenerateEvents()
{
yield return new MoveCamera() { Beat = new(1), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(2), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(3), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(4), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return SetParent(new MoveRow() { Beat = new(1), RowPosition = new(50, 50), CustomPosition = true, Duration = 0 }, Row);
yield return SetParent(new MoveRow() { Beat = new(1.001f), RowPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(2), RowPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(3), RowPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(4), RowPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
}
}
```
值得注意的是,这样的操作会比较消耗资源。所以 `LevelReadOrWriteSettings` 会默认禁用这个选项。
其底层逻辑大致为,它会为事件序列中的每个事件附加一个特殊的标签以标记它是“被生成”的,以供下次读取事件时清理这些被生成的事件。对于其中已有标签的事件,它会有一层额外逻辑以封装这些事件。
```cs
using RhythmBase.Global.Settings;
using RhythmBase.RhythmDoctor.Components;
Console.WriteLine(level);
LevelReadOrWriteSettings settings = new()
{
EnableGroupEvent = true,
InactiveEventsHandling = InactiveEventsHandling.Retain,
Indented = true
};
using RDLevel level = RDLevel.Default;
level.Decorations.Add(new Decoration() { Room = RDRoomIndex.Room1 });
var re1 = new MoveCameraRectangle() { Beat = new(4), Size = new RDSize(80, 80) };
var re2 = new MoveCameraRectangle() { Beat = new(9), Y = 2, Size = new RDSize(20, 20) };
level.Add(re1);
level.Add(re2);
level.Write(dstPath, settings);
// 会生成这些事件:
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"Comment","tab":"Song","show":false,"text":"$RhythmBase_GroupData$\r\n/* Generated by RhythmBase */\r\n@MoveCameraRectangle\r\n@MoveCameraRectangle2\r\n@RhythmBase.RhythmDoctor.Events.Group\r\n@RhythmBase.RhythmDoctor.Events.Group`1\r\n{\"size\":[80.0,80.0],\"rowIndex\":0}\r\n{\"size\":[20.0,20.0],\"rowIndex\":0}\r\n","color":"F2E644","y":-1},
// {"bar":1,"beat":4,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000000","y":0,"tag":"","Action":"Run"},
// {"bar":2,"beat":1,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000001","y":2,"tag":"","Action":"Run"},
```
## 案例
### 合并采音关卡与视效关卡
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Extensions;
// 读取视效关卡文件
using RDLevel vfxLevel = RDLevel.Read(@"vfx.rdlevel");
// 读取采音关卡文件
using RDLevel audioLevel = RDLevel.Read(@"beat.rdlevel");
// 移除视效关卡内所有轨道
RowEventCollection[] vfxrows = [.. vfxLevel.Rows];
foreach (var row in vfxrows)
vfxLevel.Rows.Remove(row);
// 复制采音关卡所有轨道的内容到新的关卡内
foreach (var row in audioLevel.Rows)
{
// 复制轨道信息
Row row2 = new()
{
Rooms = row.Rooms,
Character = row.Character,
Sound = row.Sound,
RowType = row.RowType
};
vfxLevel.Rows.Add(row2);
// 复制轨道内的事件
BaseBeat[] evts = [.. row.Where<BaseBeat>()];
foreach (var evt in evts)
row2.Add(evt);
i++;
}
// 复制需要的音效事件
foreach (var sound in audioLevel.Where(e =>
e.Tab == Tabs.Sounds && // 事件位于音效栏中
e is not BaseRowAction && // 音效事件内包含轨道事件,如果在这里添加轨道事件会导致引用出错
e is not PlaySong && // 播放音乐事件相同的情况下不需要复制
e is not SetCrotchetsPerBar)) // 复制事件的时间计算是无关四分音符数的,所以可以不用添加
{
vfxLevel.Add(sound);
}
// 写入到新的关卡文件中
vfxLevel.Write(@"result.rdlevel");
```
+763
View File
@@ -0,0 +1,763 @@
[Repository](https://github.com/RDCN-Community-Developers/RhythmToolkit)
English | [中文](/pages/3rdparty_rdtk.md)
# RhythmBase Tutorial
This project serves Rhythm Doctor level developers, aiming to provide a more systematic and intuitive level editing medium for developers.
Thanks to the Rhythm Doctor fan community for their support of this project.
Welcome to the RhythmBase tutorial. This guide will help you get started with setting up and using RhythmBase in your project.
## Install NuGet Package
To install the RhythmBase NuGet package, follow these steps:
1. Open Visual Studio or use the command line tools.
2. In Visual Studio, go to **Tools** > **NuGet Package Manager** > **Package Manager Console**.
3. In the console, enter the following command:
```
Install-Package RhythmBase -Version 1.2.0-rc2
```
4. Wait for the installation to complete and ensure your project references the required NuGet package.
5. To install using the .NET CLI, use:
```
dotnet add package RhythmBase -version 1.2.0-rc2
```
## Coding
### Creating a Level
A level is a collection of events.
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel emptyLevel = [];
Console.WriteLine(emptyLevel); // "" Count = 0
```
You can also use the built-in template to create a level with basic events.
This template is the same as the default level template created by the Rhythm Doctor Level Editor.
```cs
using RhythmBase.RhythmDoctor.Components;
using RDLevel defaultLevel = RDLevel.Default;
Console.WriteLine(defaultLevel); // "" Count = 3
```
### Reading and Writing
You can directly import and export files using file paths. The default read/write settings will be used.
Exporting does not package the level as a `.rdzip` file.
```cs
using RhythmBase.RhythmDoctor.Components;
// Directly read a level file
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel");
// Read a level pack file
using RDLevel rdlevel2 = RDLevel.Read(@"your\level.rdzip");
// Read a compressed level pack
using RDLevel rdlevel3 = RDLevel.Read(@"your\level.zip");
// Write a level file
rdlevel1.Write(@"your\outLevel.rdlevel");
```
You can add custom read/write settings with `LevelReadOrWriteSettings` when reading or writing levels.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Settings;
// Create custom read/write settings
LevelReadOrWriteSettings settings = new()
{
// Handling of inactive events
InactiveEventsHandling = InactiveEventsHandling.Store,
// Handling of unreadable events
// Common when sprite events are not bound to sprite tracks, etc.
UnreadableEventsHandling = UnreadableEventHandling.Store,
// Enable indentation
Indented = true,
};
using RDLevel rdlevel1 = RDLevel.Read(@"your\level.rdlevel", settings);
```
You can also generate a JSON object or JSON string for further operations.
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Components;
JObject jobject = rdlevel.ToJObject();
string json = rdlevel.ToRDLevelJson(settings);
```
`LevelReadOrWriteSettings` provides `BeforeReading`, `AfterReading`, `BeforeWriting`, and `AfterWriting` events, which are triggered before/after reading or writing a level.
You can add listeners to these events for custom behaviors.
```cs
using RhythmBase.RhythmDoctor.Settings;
settings.AfterWriting += Settings_AfterReading;
// This will be triggered after writing is finished
void Settings_AfterReading(object? sender, EventArgs e)
{
throw new NotImplementedException();
}
rdlevel.Write(@"your\outLevel.rdlevel", settings);
```
> When reading compressed level pack files, please use the `using` statement or actively call the `RDLevel.Dispose()` method to ensure that temporary files extracted during decompression are properly cleaned up.
### Finding and Retrieving Events
The `OrderedEventCollection` type is used to store collections of events, and `RDLevel` inherits from this type.
You can use extension methods designed for querying Rhythm Doctor level events to simplify your queries.
For example, you can filter by event type and its base types, interfaces implemented by events, beat ranges, custom predicates, and more.
Methods such as `AddRange()`, `RemoveRange()`, `Where()`, `RemoveAll()`, `First()`, `FirstOrDefault()`, `Last()`, `LastOrDefault()`, and `TakeWhile()` are provided.
It is recommended to use these optimized methods for better performance.
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// Find MoveRow events between measures 3 and 5, and in event rows 0 to 2
var list = rdlevel.Where<MoveRow>(
i => 0 <= i.Y && i.Y < 3, // In event rows 0 to 2
3..5 // From bar 3 to 5
);
```
`Row` and `Decoration` also inherit from `OrderedEventCollection`, so rows and decorations also support these extension methods.
```cs
using RhythmBase.RhythmDoctor.Extensions;
using RhythmBase.RhythmDoctor.Components;
// Find the AddClassicBeat event in the decoration between beat (11,1) and (13,1)
var list = rdlevel.Decorations[0].Where<Tint>(
new Beat(11, 1), // Start searching from bar 11, beat 1
new Beat(13, 1) // End searching at bar 13, beat 1
);
```
### Creating a Beat
`RDBeat` is a struct that stores three pieces of information: `BeatOnly`, `BarBeat`, and `TimeSpan`.
You can create a `RDBeat` instance that is not associated with a level, but due to the lack of a level context, its functionality may be limited.
You can check its `IsEmpty` property to determine whether the instance is valid.
When not associated with a level, calling its `ToString()` method will display the information it contains and indicate any missing information.
```cs
using RhythmBase.RhythmDoctor.Components;
// Create a beat not associated with a level
RDBeat beat1 = new(11);
RDBeat beat2 = new(2, 3);
RDBeat beat3 = new(TimeSpan.FromSeconds(11.45));
Console.WriteLine(beat1); // [10,?,?]
Console.WriteLine(beat2); // [?,(2, 3),?]
Console.WriteLine(beat3); // [?,?,00:00:11.4500000]
```
You can create a beat associated with a level using a `BeatCalculator` instance or an `RDLevel` instance.
A `BeatCalculator` is created along with an `RDLevel` and can be accessed via `RDLevel.Calculator`.
When associated with a level, calling its `ToString()` method will display the `BarBeat` property.
Only when associated with a level can all three properties be linked, and the beat can participate in all operations. Otherwise, only the properties with data can be used in calculations.
The beat properties of events and bookmarks within a level are all associated with the level, while events removed from the level will lose this association.
```cs
using RhythmBase.RhythmDoctor.Components;
// Create a beat associated with a level
RDBeat beat1 = rdlevel.BeatOf(11);
RDBeat beat2 = rdlevel.Calculator.BeatOf(2, 3);
RDBeat beat3 = beat1 - 10 + TimeSpan.FromSeconds(11.45);
Console.WriteLine(beat1); // [2,3]
Console.WriteLine(beat2); // [2,3]
Console.WriteLine(beat3); // [3,4.083334]
```
When performing operations between beats, if both are linked to a level, you must ensure they refer to the same level.
You can call the `FromSameLevel()` or `FromSameLevelOrNull()` methods to check if they refer to the same level.
You can call `WithoutLink()` to return a new beat instance detached from any level.
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat beat1 = rdlevel.BeatOf(1);
RDBeat beat2 = beat1.WithoutLink();
Console.WriteLine(beat1.FromSameLevel(beat2)); // False
Console.WriteLine(beat1.FromSameLevelOrNull(beat2)); // True
```
`BeatCalculator` also provides methods for time conversion, allowing you to convert between different time units.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Utils;
(float, float) barbeat = rdlevel.Calculator.TimeSpanToBarBeat(TimeSpan.FromSeconds(19.19)); // (4, 8.983334)
```
`RDLevel` provides a default beat associated with the instance, with a beat count of 1.
```cs
using RhythmBase.RhythmDoctor.Components;
RDBeat @default = RDLevel.DefaultBeat;
```
`RDRange` is a data type similar to `Range`, used to represent a range of beats.
It is commonly used for querying events.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Extension;
var result = rdlevel.Where(new RDRange(rdlevel.DefaultBeat + 10, null));
```
### Extended Data Types
Types in RhythmBase that start with `RD` and contain names like `Point`, `Size`, `Rect`, or `RotatedRect` are data types related to planar geometry.
Types ending with `I` are integer types, where all data properties are of type `int`, such as `RDPointI.X`.
Types ending with `N` are non-nullable types, where all data properties are non-nullable, such as `RDSizeN.Height`.
Types ending with `E` are expression types, where all data properties are of type `RDExpression`, such as `RDRectE.Size`.
The `Angle` property of `RotatedRect` is always a floating-point type and is not affected by the `I` naming convention.
`RDExpression` is used to store Rhythm Doctor expressions and attempts to parse and evaluate them (not fully implemented yet).
It is created from a string and supports simple operations.
The underlying implementation is string concatenation, so it is normal for nested parentheses to appear when performing multiple operations.
```cs
using RhythmBase.RhythmDoctor.Components;
RDExpression exp1 = new("i2+1");
RDExpression exp2 = new(30);
RDExpression exp3 = new("25.5");
RDExpression result = exp1 - exp2 * exp3;
Console.WriteLine(result.ExpressionValue); // i2+1-765
```
### Creating and Modifying Events
All events directly or indirectly implement the `IBaseEvent` interface and inherit from the `BaseEvent` abstract type.
You can use these interfaces and abstract types as generic parameters for query extension methods to filter events.
For example,
`BaseRowAction` and `BaseDecorationAction` are track events and sprite events, respectively,
while `IRoomEvent` represents events with multi-room properties.
The beat parameter used when creating an event can be unbound from any level; when the event is added to a level, it will be associated with that level, and when removed, the association will be broken.
If no beat parameter is provided, it defaults to the first beat of the level.
When calling the event's `ToString()` method, it will return a string in the format of the event's beat, event type, and displayable data.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
Comment comment = new() { Beat = new(12), Text = "My_comment." };
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
rdlevel.Add(comment);
Console.WriteLine(comment); // [2,4] Comment My_comment.
rdlevel.Remove(comment);
Console.WriteLine(comment); // [11,?,?] Comment My_comment.
```
In particular, adding, modifying, or removing a `SetCrotchetsPerBar` event will update the timeline after this event, so you don't need to worry about changes affecting the order or arrangement of events; they will remain fixed at their absolute beat positions. The level will also attempt to add new `SetCrotchetsPerBar` events or remove adjacent events with the same `CrotchetsPerBar` property to maintain the stability of other segments.
Row and decoration events need to be added using `Add()` on the corresponding row or decoration, while removal can be done from the row, decoration, or level using the `Remove()` method.
Repeated additions have no effect.
The event types `Comment` and `TintRows` are not subject to this restriction.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RDLevel rdlevel = RDLevel.Default;
MoveRow tr = new();
Console.WriteLine(rdlevel); // "" Count = 3
rdlevel.Add(tint); // "" Count = 3
rdlevel.Rows[0].Add(tr);
Console.WriteLine(rdlevel); // "" Count = 4
rdlevel.Remove(tr);
Console.WriteLine(rdlevel); // "" Count = 3
```
### Custom Events
If the required event type is not available in this assembly, you can implement your own by inheriting from `CustomEvent`, `CustomRowEvent`, or `CustomDecorationEvent`.
```cs
using Newtonsoft.RhythmDoctor.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
// Create a MyEvent type
// Inherit from CustomEvent
public class MyEvent : CustomEvent
{
// Override property
public override Tabs Tab => Tabs.Actions;
// All implemented properties need to be bound to and checked for null in the CustomEvent.Data field.
// Implement an RDPointE type property
public RDPointE? MyProperty
{
get
{
// Get the required content from the Data field and check for null
var value = Data["myProperty"];
return value?.ToObject<RDPointE?>() ?? new RDPointE(0, 0);
}
set
{
// Save the content in the Data field
Data["myProperty"] =
value.HasValue ?
new JArray(
value?.X ?? null,
value?.Y ?? null) :
null;
}
}
// Initialize the type in the constructor
public MyEvent()
{
// Initialize the ActureType property.
ActureType = nameof(MyEvent);
}
}
```
After writing your type, it can be read and written like a normal event.
Note that `Type` is still `EventType.CustomEvent`, while `ActureType` is the custom type name.
```cs
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Components;
MyEvent myEvent = new();
rdlevel.Add(myEvent);
myEvent.Beat = new(8);
Console.WriteLine(myEvent.Type); // CustomEvent
Console.WriteLine(myEvent.ActureType); // MyEvent
```
Additionally, if an unknown event type is encountered when reading a level, it will also be read as the corresponding `CustomEvent`, `CustomRowEvent`, or `CustomDecorationEvent` type event.
### Event Types and Enums
All events have the `EaseType` property, and you can use methods in `EventTypeUtils` to convert and obtain the corresponding type.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(EventType.Tint.ToType()); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToType("Tint")); // RhythmBase.Events.Tint
Console.WriteLine(EventTypeUtils.ToEnum(typeof(Tint))); // Tint
Console.WriteLine(EventTypeUtils.ToEnum<Tint>()); // Tint
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums(typeof(IBarBeginningEvent)))); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
Console.WriteLine(string.Join(", ", EventTypeUtils.ToEnums<IBarBeginningEvent>())); // PlaySong,SetCrotchetsPerBar, SetHeartExplodeVolume
```
`EventTypeUtils` also includes some event type classifications, such as:
```cs
using RhythmBase.RhythmDoctor.Utils;
Console.WriteLine(string.Join(",\n", EventTypeUtils.DecorationTypes));
// Comment,
// CustomDecorationEvent,
// Move,
// PlayAnimation,
// SetVisible,
// Tile,
// Tint
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForCameraFX));
// MoveCamera,
// ShakeScreen,
// FlipScreen,
// PulseCamera
Console.WriteLine(string.Join(",\n", EventTypeUtils.EventTypeEnumsForUtility));
// Comment,
// TagAction,
// CallCustomMethod
```
### Rich Text and Dialogue Components
The rich text components are located in the `RhythmBase.Components.RichText` namespace. You can use the `+` operator to combine rich text with custom colors. Serialization and deserialization of rich text are also supported.
`RDLine<>` represents a complete rich text line.
`RDPhrase<>` is a style fragment of rich text, which follows a single style.
You can use a struct that implements `IRDRichStringStyle<>` to specify the style rules for the rich text. In the example below, `RDRichStringStyle` is a rich text style that only includes color.
All can be implicitly converted from a string. Note that the converted text will be plain rich text without any style.
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo");
Console.WriteLine(line.ToString()); // Hello
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color>
line +=
new RDPhrase<RDRichStringStyle>(" Rhythm")
{
Style = new()
{
Color = RDColor.Lime
}
};
line += " Doctor!";
Console.WriteLine(line.ToString()); // Hello Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo Rhythm</color> Doctor!
```
Both `RDLine<>` and `RDPhrase<>` support index access and modification of their fragments.
```cs
using RhythmBase.Global.Components.RichText;
RDLine<RDRichStringStyle> line = RDLine<RDRichStringStyle>.Deserialize("Hel<color=#00FF00>lo Rhythm</color> Doctor!");
Console.WriteLine(line[6..].ToString()); // Rhythm Doctor!
Console.WriteLine(line[6..].Serialize()); // <color=lime>Rhythm</color> Doctor!
line[5] = " and Welcome to ";
Console.WriteLine(line.ToString()); // Hello and Welcome to Rhythm Doctor!
Console.WriteLine(line.Serialize()); // Hel<color=lime>lo</color> and Welcome to <color=lime>Rhythm</color> Doctor!
return;
```
This package also provides a complete set of dialogue components adapted to the Rhythm Doctor dialogue format, allowing you to modularly construct the text content of dialogue events and reduce error rates.
```cs
using RhythmBase.Global.Components.RichText;
RDDialogueExchange exchange =
[
new RDDialogueBlock()
{
Character = "Paige",
Expression = "neutral",
Content = RDLine<RDDialoguePhraseStyle>.Deserialize("Hel<color=#00FF00>lo [2]<shake>Rhythm</color> Doctor</shake>!"),
},
new RDDialogueBlock()
{
Character = "Ian",
Content = "Hello Paige!",
},
new RDDialogueBlock()
{
Character = "Paige",
Expression = "happy",
Content = new RDPhrase<RDDialoguePhraseStyle>("What a good day!")
{
Events =
[
new RDDialogueTone(RDDialogueToneType.VerySlow,6),
new RDDialogueTone(RDDialogueToneType.Static,11),
],
Style = new RDDialoguePhraseStyle()
{
Volume = 0.5f,
Bold = true,
},
}
}
];
Console.WriteLine(exchange.Serialize());
// Paige_neutral:Hel<color=lime>lo [2]<shake>Rhythm</color> Doctor</shake>!
// Ian:Hello Paige!
// Paige_happy:<volume=0.5><bold>What a[vslow] good[static] day!</volume></bold>
```
### Easing
After importing `RhythmBase.Global.Components.Easing`, you can easily use the `EaseType` enum constants.
You can also use the extension method `Calculate()` to quickly compute eased values.
```cs
using RhythmBase.Global.Components.Easing;
double var1 = EaseType.InSine.Calculate(0.25);
double var2 = EaseType.Linear.Calculate(0.5, 4, 9);
Console.WriteLine(var1); // 0.07612046748871326
Console.WriteLine(var2); // 6.5
```
`EaseValue` is a simple yet powerful struct. Thanks to mfgujhgh's algorithm, by using the static method `Fit()` to process arbitrary data, `EaseValue` can fit a curve usable by Rhythm Doctor events with a given selection of `EaseType`.
```cs
using RhythmBase.Global.Components.Easing;
// Fit using a set of points and a threshold
EaseValue data1 = EaseValue.Fit([
(0, 0),
(1, 1)
], 3f);
// Fit using an initial value, a set of points, an optional list of ease types, and a threshold
EaseValue data2 = EaseValue.Fit(0, [
(0, 0),
(1, 1)
], [EaseType.Linear, EaseType.InSine], 3f);
// Get the value at a specific time from the easing data
float value = data1.GetValue(2.5f);
```
`RhythmBase.Extensions.EasePropertyExtensions` adds a `GetEaseProperties` method to types that implement `IEaseEvent`, which is used to obtain the easing curves for each property in a series of events.
```cs
using RhythmBase.Global.Components;
using RhythmBase.Global.Components.Easing;
using RhythmBase.Global.Events;
using RhythmBase.RhythmDoctor.Extensions;
var deco = level.Decorations[0];
Move[] moves =
[
new(){ Beat = level.BeatOf(1), Position = new(0, (RDExpression?)null), Duration = 1, Ease = EaseType.Linear, Angle = "2" },
new(){ Beat = level.BeatOf(2.1f), Position = new(10, 90), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(2.2f), Position = new(90, 10), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3), Position = new(10, 70), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.5f), Position = new(10, 10), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.8f), Position = new(30, 50), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(3.9f), Position = new(20, (RDExpression?)null), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.1f), Position = new(70, 20), Duration = 1, Ease = EaseType.Linear },
new(){ Beat = level.BeatOf(4.4f), Position = new((RDExpression?)null, 0), Duration = 1, Ease = EaseType.Linear },
];
deco.AddRange(moves);
var eases = EasePropertyExtensions.GetEaseProperties(moves);
foreach(var e in eases)
Console.WriteLine(e);
// [Position, RhythmBase.Components.Easing.EasePropertyPoint]
// [Scale, RhythmBase.Components.Easing.EasePropertySize]
// [Angle, RhythmBase.Components.Easing.EasePropertyFloat]
// [Pivot, RhythmBase.Components.Easing.EasePropertyPoint]
```
Types that implement the `IEaseProperty<>` interface, such as `EasePropertyColor`, `EasePropertyFloat`, `EasePropertyPoint`, and `EasePropertySize`, are used to store the content from a list of easing events.
You can also get the value at a specific time.
> Before calling, please check the value type of the property and apply the appropriate type conversion.
```cs
using RhythmBase.Global.Components.Easing;
var result = ((EasePropertyPoint)eases["Position"]).GetValue(rdlevel.BeatOf(3.2f));
Console.WriteLine(result); // [59.759995, 21.840006]
```
### RDCode (Under Revision)
`RhythmBase.Components.RDLang.RDLang` provides a `TryRun()` method for evaluating Rhythm Doctor expressions.
> Note: If the expression is incorrect, it will return `false` and the result will be `0`.
`RDLang` also has a static field `Variables` for storing all commonly used variables and methods. Modifying this field before executing `TryRun` will affect the values during execution.
`RDLang` also supports three common methods: `Rand()`, `atLeastRank()`, and `atLeastNPerfects()`. These methods can also be accessed via `RDVariables`.
```cs
using RhythmBase.RhythmDoctor.Components.RDLang;
RDLang.Variables.i[1] = 9;
RDLang.TryRun("numMistakesP2 = 3", out float result); // 3
RDLang.TryRun("numMistakesP2+i1", out result); // 12
RDLang.TryRun("atLeastRank(A)", out result); // 1
```
Since this library does not support dynamic level playback, you can use the following fields to simulate the effects of the last two functions:
- `atLeastRank()`
Use the `RDVariables.SimulateCurrentRank` property to change the simulated level rank state.
When the expression accesses the `atLeastRank()` method, this value will be used for simulation.
- `atLeastNPerfects()`
Use the `RDVariables.SimulateAtLeastNPerfectsSuccessRate` property to change the simulated percentage of perfect hits.
When the expression accesses the `atLeastNPerfects()` method, this value will be used for simulation.
### Event Groups
Declare and use it like a new event, and it will generate level events according to your specifications!
By inheriting from the `Group` class and implementing new logic as shown below, you can freely manipulate it just like other events.
Unlike `CustomEvent`, it writes the specified sequence of events into the level instead of itself.
```cs
using Newtonsoft.Json.Linq;
using RhythmBase.RhythmDoctor.Events;
public class GroupData1
{
public RDSize Size;
public int RowIndex;
}
public class MoveCameraRectangle : Group<GroupData1>
{
public RDSize Size
{
get => Data.Size;
set => Data.Size = value;
}
public Row Row
{
get => Rows?[Data.RowIndex] ?? [];
set => Data.RowIndex = value.Index;
}
public MoveCameraRectangle() { }
public override IEnumerable<BaseEvent> GenerateEvents()
{
yield return new MoveCamera() { Beat = new(1), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(2), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(3), Rooms = new(true, 0), CameraPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return new MoveCamera() { Beat = new(4), Rooms = new(true, 0), CameraPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), Duration = 1 };
yield return SetParent(new MoveRow() { Beat = new(1), RowPosition = new(50, 50), CustomPosition = true, Duration = 0 }, Row);
yield return SetParent(new MoveRow() { Beat = new(1.001f), RowPosition = new(50 - Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(2), RowPosition = new(50 + Size.Width / 2, 50 - Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(3), RowPosition = new(50 + Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
yield return SetParent(new MoveRow() { Beat = new(4), RowPosition = new(50 - Size.Width / 2, 50 + Size.Height / 2), CustomPosition = true, Duration = 1 }, Row);
}
}
```
Note that this operation can be resource-intensive, so `LevelReadOrWriteSettings` disables this option by default.
The underlying logic is that it attaches a special tag to each event in the sequence to mark it as "generated", so that these generated events can be cleaned up the next time events are read. For events that already have tags, there is an extra layer of logic to encapsulate them.
```cs
using RhythmBase.Global.Settings;
using RhythmBase.RhythmDoctor.Components;
LevelReadOrWriteSettings settings = new()
{
EnableGroupEvent = true,
InactiveEventsHandling = InactiveEventsHandling.Retain,
Indented = true
};
using RDLevel level = RDLevel.Default;
level.Decorations.Add(new Decoration() { Room = RDRoomIndex.Room1 });
var re1 = new MoveCameraRectangle() { Beat = new(4), Size = new RDSize(80, 80) };
var re2 = new MoveCameraRectangle() { Beat = new(9), Y = 2, Size = new RDSize(20, 20) };
level.Add(re1);
level.Add(re2);
level.Write(dstPath, settings);
// The following events will be generated:
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,10],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,10],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[90,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[90,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[10,90],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[10,90],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000000"},
// {"bar":1,"beat":1,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[50,50],"duration":0,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1.001,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,40],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":2,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,40],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveCamera","rooms":[0],"cameraPosition":[60,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":3,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[60,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveCamera","rooms":[0],"cameraPosition":[40,60],"duration":1,"ease":"Linear","y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":4,"type":"MoveRow","customPosition":true,"target":"WholeRow","rowPosition":[40,60],"duration":1,"ease":"Linear","row":0,"y":-1,"tag":"$RhythmBase_GroupEvent$0000000000000001"},
// {"bar":1,"beat":1,"type":"Comment","tab":"Song","show":false,"text":"$RhythmBase_GroupData$\r\n/* Generated by RhythmBase */\r\n@MoveCameraRectangle\r\n@MoveCameraRectangle2\r\n@RhythmBase.RhythmDoctor.Events.Group\r\n@RhythmBase.RhythmDoctor.Events.Group`1\r\n{\"size\":[80.0,80.0],\"rowIndex\":0}\r\n{\"size\":[20.0,20.0],\"rowIndex\":0}\r\n","color":"F2E644","y":-1},
// {"bar":1,"beat":4,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000000","y":0,"tag":"","Action":"Run"},
// {"bar":2,"beat":1,"type":"TagAction","Tag":"$RhythmBase_GroupEvent$0000000000000001","y":2,"tag":"","Action":"Run"},
```
## Examples
### Merging Audio and Visual Levels
```cs
using RhythmBase.RhythmDoctor.Components;
using RhythmBase.RhythmDoctor.Events;
using RhythmBase.RhythmDoctor.Extensions;
// Read the visual effects level file
using RDLevel vfxLevel = RDLevel.Read(@"vfx.rdlevel");
// Read the audio level file
using RDLevel audioLevel = RDLevel.Read(@"beat.rdlevel");
// Remove all rows from the visual effects level
RowEventCollection[] vfxrows = [.. vfxLevel.Rows];
foreach (var row in vfxrows)
vfxLevel.Rows.Remove(row);
// Copy all rows from the audio level into the new level
foreach (var row in audioLevel.Rows)
{
// Copy row information
Row row2 = new()
{
Rooms = row.Rooms,
Character = row.Character,
Sound = row.Sound,
RowType = row.RowType
};
vfxLevel.Rows.Add(row2);
// Copy events within the row
BaseBeat[] evts = [.. row.Where<BaseBeat>()];
foreach (var evt in evts)
row2.Add(evt);
}
// Copy necessary sound events
foreach (var sound in audioLevel.Where(e =>
e.Tab == Tabs.Sounds && // Event is in the Sounds tab
e is not BaseRowAction && // Sound events contain row events; adding row events here would cause reference errors
e is not PlaySong && // No need to copy PlaySong if the music is the same
e is not SetCrotchetsPerBar)) // The timing of these events is independent of the number of crotchets per bar, so they don't need to be added
{
vfxLevel.Add(sound);
}
// Write to a new level file
vfxLevel.Write(@"result.rdlevel");
```
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz)
<iframe single src="https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz"></iframe> <iframe single src="https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz"></iframe>
+3 -3
View File
@@ -8,7 +8,7 @@
### 带X拍子 ### 带X拍子
在编辑器中翻译为X字拍子。心跳有时候会**跳过一些拍子**,但我们仍然需要在拍子抵达时抓住时机按键。当电图线路上出现**红色的X字**时,就意味着这些拍子会被跳过。这个时候你需要在头脑里继续数数,然后照常在第二拍处按键。 在编辑器中翻译为X字拍子。心跳有时候会**跳过一些拍子**,但我们仍然需要在拍子抵达时抓住时机按键。当电图线路上出现**红色的X字**时,就意味着这些拍子会被跳过。这个时候你需要在头脑里继续数数,然后照常在处按键。
`1-2 亲密`中被首次引入,且在大量关卡中出现。 `1-2 亲密`中被首次引入,且在大量关卡中出现。
@@ -22,13 +22,13 @@
### 摇摆普通拍子 ### 摇摆普通拍子
也称心脏梗阻。他们的心跳会出现不规则的拍子,因为心跳信号的工作不正常,节拍也会呈现不规则的感觉。无论如何,也请在第拍按下空格。 也称心脏梗阻。他们的心跳会出现不规则的拍子,因为心跳信号的工作不正常,节拍也会呈现不规则的感觉。无论如何,也请在第拍按下空格。
`3-3 分离二重唱`中被首次引入。 `3-3 分离二重唱`中被首次引入。
### 网好卡菌 ### 网好卡菌
在编辑器中应设置为电击拍子。无论发生什么都要在音乐的第拍按下空格。 在编辑器中应设置为电击拍子。无论发生什么都要在音乐的第拍按下空格。
`1-X 东方不眠夜`中被首次引入。 `1-X 东方不眠夜`中被首次引入。
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn)
<iframe single src="https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn"></iframe> <iframe single src="https://docs.qq.com/sheet/DUGpSTXFZQkNGWWdn"></iframe>
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp)
<iframe single src="https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp"></iframe> <iframe single src="https://docs.qq.com/sheet/DZU1LTmJGVkhhQ2Vp"></iframe>
+2
View File
@@ -1 +1,3 @@
[去往原页面](https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI)
<iframe single src="https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI"></iframe> <iframe single src="https://docs.qq.com/sheet/DZGlHSExJSllSZ0tI"></iframe>
+1 -1
View File
@@ -22,7 +22,7 @@
## 设置游戏音效 ## 设置游戏音效
除了节奏音效和按拍音效之外,游戏里还有一些地方会出现音效,主要是失误的时候(分为微小失误和严重失误),没有节拍却按了空格的时候(默认是“啵”一声,所以在编辑器里面叫“手掌-啵音”),第拍按下之后第八拍“咚”地一声回音(叫做心爆,这时候心脏图标也会小抖一下),以及跳过拍(嘎↘的声音),长按拍,冻结拍,灼热拍的音效。 除了节奏音效和按拍音效之外,游戏里还有一些地方会出现音效,主要是失误的时候(分为微小失误和严重失误),没有节拍却按了空格的时候(默认是“啵”一声,所以在编辑器里面叫“手掌-啵音”),第拍按下之后第八拍“咚”地一声回音(叫做心爆,这时候心脏图标也会小抖一下),以及跳过拍(嘎↘的声音),长按拍,冻结拍,灼热拍的音效。
这些音效在设置的时候是无法下拉列表选择更改的,只能通过键入游戏内置的音效文件名,或者导入自定义的音效来改变。目前已公开的内置音效文件名因为数量太多,集中收录在了[附录3](../appendix/3.html)中。 这些音效在设置的时候是无法下拉列表选择更改的,只能通过键入游戏内置的音效文件名,或者导入自定义的音效来改变。目前已公开的内置音效文件名因为数量太多,集中收录在了[附录3](../appendix/3.html)中。
+15 -15
View File
@@ -13,28 +13,28 @@
**需要注意的是:** **需要注意的是:**
**1、**你可以一次性输入多行文本,这样在实际显示时就会在同一对话框内逐步刷新每一行台词;如果使用多个显示对话事件,每个事件只输入一行文本,那么对话框有可能在一个事件之后消失再生成,影响连贯性。 1. 你可以一次性输入多行文本,这样在实际显示时就会在同一对话框内逐步刷新每一行台词;如果使用多个显示对话事件,每个事件只输入一行文本,那么对话框有可能在一个事件之后消失再生成,影响连贯性。
**2、**显示对话事件一次只能显示一个对话框,如果前一个事件的台词没有走完下一个事件就开始了,那么前一个会直接被顶替掉。即使设置为屏幕上方和屏幕下方各一个也会被顶替。 2. 显示对话事件一次只能显示一个对话框,如果前一个事件的台词没有走完下一个事件就开始了,那么前一个会直接被顶替掉。即使设置为屏幕上方和屏幕下方各一个也会被顶替。
**3、**一个对话框只能显示两行文字,多出来的会无法显示,请注意分段。分段后需要再输入一次立绘人物,否则立绘就不再显示了。 3. 一个对话框只能显示两行文字,多出来的会无法显示,请注意分段。分段后需要再输入一次立绘人物,否则立绘就不再显示了。
**4、** 如果需要实现像官方教程一样的可以手动跳过的对话框效果,可以添加自定义方法`activeDialogues = true``activeDialoguesImmediately = true` 4. 如果需要实现像官方教程一样的可以手动跳过的对话框效果,可以添加自定义方法`activeDialogues = true``activeDialoguesImmediately = true`
- 如果启用`activeDialogues = true` - 如果启用`activeDialogues = true`
在对话事件所在小节已经播放完毕而对话仍未结束时,游戏会等待至游戏音乐的下一个小节开始时继续播放; 在对话事件所在小节已经播放完毕而对话仍未结束时,游戏会等待至游戏音乐的下一个小节开始时继续播放;
- 如果启用`activeDialoguesImmediately = true` - 如果启用`activeDialoguesImmediately = true`
- 如果对话事件结束时所在小节仍然未播放完毕, - 如果对话事件结束时所在小节仍然未播放完毕,
游戏会等待至游戏音乐的下一个节拍开始时立即从时间轴的下一个小节继续播放。 游戏会等待至游戏音乐的下一个节拍开始时立即从时间轴的下一个小节继续播放。
- 如果对话事件所在小节已经播放完毕而对话仍未结束, - 如果对话事件所在小节已经播放完毕而对话仍未结束,
游戏会等待至游戏音乐的下一个节拍开始时继续播放。 游戏会等待至游戏音乐的下一个节拍开始时继续播放。
**5、**编辑器会自动识别标点符号,并在标点符号的位置自动添加不同程度的语句停顿,非常智能。但要注意,数学上的小数点“.”是会被判定为英文的句号的,从而产生停顿,非常不智能。 5. 编辑器会自动识别标点符号,并在标点符号的位置自动添加不同程度的语句停顿,非常智能。但要注意,数学上的小数点“.”是会被判定为英文的句号的,从而产生停顿,非常不智能。
**6、**从外界复制粘贴进入的文本中,换行符是有问题的,要退格两次才能被删掉,否则台词会叠在一起;最好的做法当然是不要复制进多余的换行符号。 6. 从外界复制粘贴进入的文本中,换行符是有问题的,要退格两次才能被删掉,否则台词会叠在一起;最好的做法当然是不要复制进多余的换行符号。
![对话事件](../images/captions-dialoguecontent.png) ![对话事件](../images/captions-dialoguecontent.png)
**7、**可以使用对话标签控制对话的显示效果,这些标签总是被包裹在中括号里的。支持的标签如下: 7. 可以使用对话标签控制对话的显示效果,这些标签总是被包裹在中括号里的。支持的标签如下:
- `[vslow]` - 非常慢 - `[vslow]` - 非常慢
- `[slow]` - 慢 - `[slow]` - 慢
@@ -54,7 +54,7 @@
- `[swirlSpeed=X]` - `[swirlSpeed=X]`
- `[static]` - 让此前设置移动方式的标签失效,位于该标签后的文本将不受前面的标签影响。 - `[static]` - 让此前设置移动方式的标签失效,位于该标签后的文本将不受前面的标签影响。
**8、**可以使用颜色标签更改文字的颜色,只需按以下格式输入:`<color=文字颜色>要更改颜色的文字</color>`。代码中的颜色可以使用十六进制颜色值(不透明的 #RRGGBB 或包含透明度信息的 #RRGGBBAA ,也可以使用[Unity富文本的颜色名](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/StyledText.html) **8、** 可以使用颜色标签更改文字的颜色,只需按以下格式输入:`<color=文字颜色>要更改颜色的文字</color>`。代码中的颜色可以使用十六进制颜色值(不透明的 #RRGGBB 或包含透明度信息的 #RRGGBBAA ,也可以使用[Unity富文本的颜色名](https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/StyledText.html)
| 颜色(中文) | 颜色(英文) | RGB | 示例 | | 颜色(中文) | 颜色(英文) | RGB | 示例 |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
+33
View File
@@ -1,5 +1,38 @@
# 更新信息 # 更新信息
#### 2025-10-27
- 添加 Webhook
#### 2025-10-26
- 文档补充
- 修复代码块渲染异常
- 首页添加互动模块引导
#### 2025-10-18
- [自制角色与精灵资源](../pages/spritesheet.md)页添加了音效和表情命名规则相关内容。
#### 2025-10-16
- 更新了字体版本
#### 2025-09-27
- 添加了 7th beat games 授权
#### 2025-07-21
- 添加了本地状态存储
#### 2025-07-19
- 补充了目录图标
- 整理并添加了新的板块
- 为嵌入页面添加了跳转外链功能
- 更换了插图
- 添加了使用协议
#### 2025-06-28
- 添加了~~吐槽~~样式
- 修复了批量替换遗留问题
- 修复了样式混淆
- 添加了[双人模式设计页面](../pages/twoplayers.md)
#### 2025-06-08 #### 2025-06-08
- 修复了一些死链 - 修复了一些死链
- 添加了新的 otto 插图(没错作者又更了)并修改了更换逻辑 - 添加了新的 otto 插图(没错作者又更了)并修改了更换逻辑
+2 -2
View File
@@ -7,7 +7,7 @@
- [Sterix_Craft砹氪硒](https://space.bilibili.com/349090681) - [Sterix_Craft砹氪硒](https://space.bilibili.com/349090681)
- [T_Terrence](https://space.bilibili.com/455443751) - [T_Terrence](https://space.bilibili.com/455443751)
- [mfgujhgh](https://space.bilibili.com/1369651) - [mfgujhgh](https://space.bilibili.com/1369651)
- [喜欢精灵宝可梦的黄黄](https://space.bilibili.com/589687399) - [龙俟城](https://space.bilibili.com/589687399)
- [卢毅](https://space.bilibili.com/26101629) - [卢毅](https://space.bilibili.com/26101629)
- [RaYmondCheung](https://space.bilibili.com/1579208) - [RaYmondCheung](https://space.bilibili.com/1579208)
- [NoMathExpectation](https://space.bilibili.com/184862524) - [NoMathExpectation](https://space.bilibili.com/184862524)
@@ -40,7 +40,7 @@
- 表格维护: - 表格维护:
- [自定义方法表](/pages/appendix_custommethods.md) - [自定义方法表](/pages/appendix_custommethods.md)
- [RaYmondCheung](https://space.bilibili.com/1579208) - [RaYmondCheung](https://space.bilibili.com/1579208)
- [ChocoWeat](https://space.bilibili.com/588693542) - [ChocoWheat](https://space.bilibili.com/588693542)
- [制谱规范](/pages/appendix_levelrules.md) - [制谱规范](/pages/appendix_levelrules.md)
- [o0O0o0l0o0O0o](https://space.bilibili.com/272324550) - [o0O0o0l0o0O0o](https://space.bilibili.com/272324550)
- [相关资源](/pages/appendix_resources.md) - [相关资源](/pages/appendix_resources.md)
+2 -2
View File
@@ -49,14 +49,14 @@
</div> </div>
<!-- <!--
| 普通拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  | | 普通拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 轨道1 | -0.5 | 1 | 0.5 | 1 | 0 | -2 | -2.5 | | 轨道1 | -0.5 | 1 | 0.5 | 1 | 0 | -2 | -2.5 |
| 轨道2 | -10.5 | -9 | -9.5 | -9 | -10 | -12 | -12.5 | | 轨道2 | -10.5 | -9 | -9.5 | -9 | -10 | -12 | -12.5 |
| 轨道3 | -20.5 | -19 | -19.5 | -19 | -20 | -22 | -22.5 | | 轨道3 | -20.5 | -19 | -19.5 | -19 | -20 | -22 | -22.5 |
| 轨道4 | -30.5 | -29 | -29.5 | -29 | -30 | -32 | -32.5| | 轨道4 | -30.5 | -29 | -29.5 | -29 | -30 | -32 | -32.5|
| 单发拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  | | 单发拍子轨道 | 人物  | 前六拍左侧 | 前六拍中段 | 第拍左侧 | 第拍右侧 | 心脏之前 | 心脏  |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
--> -->
**删除**:删除这个精灵。 **删除**:删除这个精灵。
+13 -6
View File
@@ -1,14 +1,17 @@
<div class="intro"></div> <div class="intro"></div>
<div onclick="location.href='https://www.bilibili.com/opus/1095830097776082950';" class="notice">
<div onclick="location.href='https://docs.qq.com/form/page/DUFNwc1dQdHJtbXF5';" class="notice"> <p>2026节奏医生拜年纪活动正在招募中!</p>
<p>节奏医生夏季打谱活动正在招募中!</p>
</div> </div>
# 节奏医生中文编辑器教程 # 节奏医生中文编辑器教程
> 此网页正在维护中。如有任何问题,欢迎指出。 > 此网页正在维护中。
网页已开源至[此处](https://git.obugs.cn/RDCNWebs/rd.rdlevel.cn),欢迎投稿。
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。未经授权不得以图文,视频等形式转载,也不得以本教程内素材进行二次创作。 > 页面内的大部分事件栏板块包含点击互动。尝试点击以获得更生动的演示。
也可以在[此处](../pages/appendix_tools.md)游玩事件栏板块。
《节奏医生》编辑器中文教程旨在以教学的形式帮助您快速了解编辑器的界面,基础功能,设计技巧和做谱规范。
编辑器视频教程点击[此链接](https://www.bilibili.com/video/BV1pr4y1k7gP)。教程内所有演示关卡下载放置于[百度网盘](https://pan.baidu.com/s/1Tb6ywVmge25ztMFnpa6q_Q),提取码:rdlv。 编辑器视频教程点击[此链接](https://www.bilibili.com/video/BV1pr4y1k7gP)。教程内所有演示关卡下载放置于[百度网盘](https://pan.baidu.com/s/1Tb6ywVmge25ztMFnpa6q_Q),提取码:rdlv。
@@ -20,4 +23,8 @@
(悄悄透露一句:官方也入驻了这个群哦~) (悄悄透露一句:官方也入驻了这个群哦~)
> 戳这只看板娘可以获得随机 TIPS → > 戳这只看板娘可以获得随机 TIPS →
本网站内容分模块授权,请务必阅读对应<a href='/LICENSE' download='LICENSE'>条款</a>后再使用。
Game assets courtesy of [7th Beat Games](https://7thbe.at/). Game assets are excluded from this site's license.
+6 -6
View File
@@ -1,4 +1,4 @@
# 普通拍的衍生节奏型 # 普通拍的衍生节奏型
## 分解为自由时长节拍 ## 分解为自由时长节拍
@@ -16,11 +16,11 @@ eb.PulseFreeTimeBeat 6 0[pulse=9];
eb.PulseFreeTimeBeat 7 0[pulse=9]; eb.PulseFreeTimeBeat 7 0[pulse=9];
``` ```
我们选中数字1对应的节拍,发现这个是设置起始脉冲位置的。你甚至可以将它改为第拍,然后把六个小的`>`删掉,这样就没有任何节拍提示。如果你设计的关卡是一个和节奏医生没什么关联的创意谱(梦幻联动?),且按键密度很大,则可以考虑这样使用。相比于工作区里面重叠摆放一大堆普通普通拍子,这样会看上去更清爽。但如果你设计的就是节奏医生,**请不要在完全无节奏提示的情况下直接响起第拍**。 我们选中数字1对应的节拍,发现这个是设置起始脉冲位置的。你甚至可以将它改为第拍,然后把六个小的`>`删掉,这样就没有任何节拍提示。如果你设计的关卡是一个和节奏医生没什么关联的创意谱(梦幻联动?),且按键密度很大,则可以考虑这样使用。相比于工作区里面重叠摆放一大堆普通普通拍子,这样会看上去更清爽。但如果你设计的就是节奏医生,**请不要在完全无节奏提示的情况下直接响起第拍**。
![](../images/moreclassicbeats-endpulse.png) ![](../images/moreclassicbeats-endpulse.png)
剩下的大于号`>`即意味着心跳向右移动一格,数到7的时候则需要按拍。点选之后,把它改成 `<`,心跳还能向左移动一格;点击自定义,还可以任意指定到第几个节拍。每一组自由拍**必须使用较大的节拍作为开头**,较小的则数量不限,直到数到第拍为止。 剩下的大于号`>`即意味着心跳向右移动一格,数到7的时候则需要按拍。点选之后,把它改成 `<`,心跳还能向左移动一格;点击自定义,还可以任意指定到第几个节拍。每一组自由拍**必须使用较大的节拍作为开头**,较小的则数量不限,直到数到第拍为止。
```rdview 10 2 ```rdview 10 2
eb.AddFreeTimeBeat 1 0[pulse=1]; eb.AddFreeTimeBeat 1 0[pulse=1];
@@ -41,7 +41,7 @@ eb.PulseFreeTimeBeat 9 0[pulse=9];
如果你想让一串自由节拍最后不需要按拍,而是节奏直接消失,就把这一组自由节拍的最后一个点选**垃圾桶**符号。 如果你想让一串自由节拍最后不需要按拍,而是节奏直接消失,就把这一组自由节拍的最后一个点选**垃圾桶**符号。
> 如果你设计了一段无法数到第拍的节奏且不放置垃圾桶,那么心电图就会一直停在轨道中间。这很诡异。 > 如果你设计了一段无法数到第拍的节奏且不放置垃圾桶,那么心电图就会一直停在轨道中间。这很诡异。
接下来介绍一些社区机制。社区玩家们通过合理设计自由拍,并加以适当的提示,创造出了一些全新的节奏类型。其中一些节奏被广泛使用,并在社区中形成共识,从而延续下来。使用这些机制制作自定义关卡无需额外设计教学环节。当然如果你担心玩家不了解社区,或是你有其它自己的想法,也可以为这些节奏类型自行设计节奏提示的方法,唯一的宗旨是保证提示恰当准确。 接下来介绍一些社区机制。社区玩家们通过合理设计自由拍,并加以适当的提示,创造出了一些全新的节奏类型。其中一些节奏被广泛使用,并在社区中形成共识,从而延续下来。使用这些机制制作自定义关卡无需额外设计教学环节。当然如果你担心玩家不了解社区,或是你有其它自己的想法,也可以为这些节奏类型自行设计节奏提示的方法,唯一的宗旨是保证提示恰当准确。
@@ -85,7 +85,7 @@ eb.PulseFreeTimeBeat 8 1[pulse=9];
不要使用内置的 `sndBeepMid` 音效! 不要使用内置的 `sndBeepMid` 音效!
八拍子也可以是摇摆拍,只需要将所有偶数拍选择并拖动即可变成摇摆拍: 八拍子也可以是摇摆拍,只需要将所有偶数拍选择并拖动即可变成摇摆拍:
```rdview 10 3 ```rdview 10 3
eb.AddClassicBeat 1 0[tick=6,swing=0.5]; eb.AddClassicBeat 1 0[tick=6,swing=0.5];
@@ -99,7 +99,7 @@ eb.PulseFreeTimeBeat 7 1[pulse=9];
eb.PulseFreeTimeBeat 8.5 1[pulse=9]; eb.PulseFreeTimeBeat 8.5 1[pulse=9];
``` ```
在八拍子中,**激活拍子**与八拍子的对应关系如下图所示。不要给第1拍和第3拍设置X拍子,这样做它就相当于变回普通拍了。 在八拍子中,**激活拍子**与八拍子的对应关系如下图所示。不要给第1拍和第3拍设置X拍子,这样做它就相当于变回普通拍了。
![](../images/moreclassicbeats-8beat.png) ![](../images/moreclassicbeats-8beat.png)
+91 -67
View File
@@ -1,6 +1,6 @@
# 单发拍的衍生节奏型 # 单发拍的衍生节奏型
## 单发拍进阶用法 ## 单发拍进阶用法
本小节的内容可能会稍显冗长和晦涩,希望大家能保持耐心多多琢磨。 本小节的内容可能会稍显冗长和晦涩,希望大家能保持耐心多多琢磨。
@@ -13,9 +13,9 @@ es.SayReadyGetSetGo_Go 5[];
eb.AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4]; eb.AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4];
``` ```
可以看到,虽然 Get-Set 与 Set-Go 之间的间隔不同,但依然保持了上述的对应关系。实际上,这就是`4-4`使用的单发拍 可以看到,虽然 Get-Set 与 Set-Go 之间的间隔不同,但依然保持了上述的对应关系。实际上,这就是`4-4`使用的单发拍。
在`2-3`中也出现了摇摆单发拍,可以试着自己理解一下这关的单发拍提示: 在`2-3`中也出现了摇摆单发拍,可以试着自己理解一下这关的单发拍提示:
```rdview 10 2 ```rdview 10 2
eb.AddOneshotBeat 1[]; eb.AddOneshotBeat 1[];
@@ -26,7 +26,7 @@ eb.AddOneshotBeat 7[tick=0.75];
eb.AddOneshotBeat 8[tick=0.75]; eb.AddOneshotBeat 8[tick=0.75];
``` ```
如果我们在此基础上更进一步,会发生什么呢?来看看`4-4N`中出现过的这段单发拍 如果我们在此基础上更进一步,会发生什么呢?来看看`4-4N`中出现过的这段单发拍:
```rdview 12 4 ```rdview 12 4
g 1 1 4 2[offset=4]{ g 1 1 4 2[offset=4]{
@@ -45,11 +45,11 @@ eb.AddOneshotBeat 9[tick=0.75];
eb.AddOneshotBeat 10.5[tick=0.75]; eb.AddOneshotBeat 10.5[tick=0.75];
``` ```
可以看到,图中的单发拍已不再局限于一个单发拍不断重复了,而是变成了若干个单发拍构成的单发拍组的不断重复,但前面讲过的与护士提示之间的对应关系仍保持不变。由此我们可以得到单发拍的核心规则: 可以看到,图中的单发拍已不再局限于一个单发拍不断重复了,而是变成了若干个单发拍构成的单发拍组的不断重复,但前面讲过的与护士提示之间的对应关系仍保持不变。由此我们可以得到单发拍的核心规则:
单发拍本质上是对一段节奏(循环节)的不断重复,这段节奏可以由一个或多个单发拍构成。在单发拍开始之前,会使用护士提示,以 Rea、Dy、Get、Set 的形式,重复一次(或多次)这段单发拍循环节,并在最后用 Go 表示单发拍的开始。 单发拍本质上是对一段节奏(循环节)的不断重复,这段节奏可以由一个或多个单发拍构成。在单发拍开始之前,会使用护士提示,以 Rea、Dy、Get、Set 的形式,重复一次(或多次)这段单发拍循环节,并在最后用 Go 表示单发拍的开始。
当对一段较为复杂的单发拍进行提示的时候,由于循环节由多个单发拍组成,护士必然会提示多个脉冲和按拍的位置,这时仍然需要遵循只在最后一对脉冲和按拍处使用 Get Set,其余部分均使用 Rea Dy。 当对一段较为复杂的单发拍进行提示的时候,由于循环节由多个单发拍组成,护士必然会提示多个脉冲和按拍的位置,这时仍然需要遵循只在最后一对脉冲和按拍处使用 Get Set,其余部分均使用 Rea Dy。
```rdview 13 4 ```rdview 13 4
g 1 1 4 2[offset=4]{ g 1 1 4 2[offset=4]{
@@ -77,11 +77,11 @@ eb.AddOneshotBeat 12[tick=0.5];
## 复杂脉冲 ## 复杂脉冲
正如`2-1`所说的那样,单发拍的脉冲位置并不重要,只要保证按拍位置不变,脉冲位置是可以任意变化的。 正如`2-1`所说的那样,单发拍的脉冲位置并不重要,只要保证按拍位置不变,脉冲位置是可以任意变化的。
同时,一个单发拍也可能拥有不止一个脉冲。若要实现这种效果,只需将几个单发拍的尾端对齐,首端放在不同位置即可。 同时,一个单发拍也可能拥有不止一个脉冲。若要实现这种效果,只需将几个单发拍的尾端对齐,首端放在不同位置即可。
以下单发拍均是合法的: 以下单发拍均是合法的:
```rdview 9 2 ```rdview 9 2
eb.AddOneshotBeat 1[]; eb.AddOneshotBeat 1[];
@@ -96,9 +96,9 @@ eb.AddOneshotBeat 7.5[tick=0.5];
但这样一来,前面所说的“Rea、Get 对应单发拍脉冲”就没有意义了吗?其实不然,Rea、Get 的作用如下: 但这样一来,前面所说的“Rea、Get 对应单发拍脉冲”就没有意义了吗?其实不然,Rea、Get 的作用如下:
1. 护士提示的第一个词必然是 Rea 或 Get,也就意味着这个词代表单发拍循环节的开始,与末尾的 Go 对应。 1. 护士提示的第一个词必然是 Rea 或 Get,也就意味着这个词代表单发拍循环节的开始,与末尾的 Go 对应。
2. Rea、Get 可以辅助玩家掌握单发拍的节奏型。在实际游玩中,玩家通常是会以 Rea、Get 为基础来进行单发拍的演奏的。所以通常要使 Rea、Get 契合音乐或节拍设计,来达到方便玩家记忆或丰富游戏体验的效果。 2. Rea、Get 可以辅助玩家掌握单发拍的节奏型。在实际游玩中,玩家通常是会以 Rea、Get 为基础来进行单发拍的演奏的。所以通常要使 Rea、Get 契合音乐或节拍设计,来达到方便玩家记忆或丰富游戏体验的效果。
举个例子:Rea-Dy-Get-Set-Go 与 Rea-Dy--GetSet-Go,尽管两者的按拍时机相同,但不同的 Rea、Get 位置会给人带来不同的游玩体验。 举个例子:Rea-Dy-Get-Set-Go 与 Rea-Dy--GetSet-Go,尽管两者的按拍时机相同,但不同的 Rea、Get 位置会给人带来不同的游玩体验。
@@ -134,13 +134,13 @@ g 1 4 4 1 [offset=4]{
}; };
``` ```
## 跳过单发拍 ## 跳过单发拍
游戏在`2-1N`中引入了一种全新的单发拍机制:跳过单发拍。这种单发拍的规则很简单:跳过单发拍之后的一次单发拍按拍会被跳过。跳过单发拍无需手动添加提示,只需在单发拍修饰符中选择跳过单发拍,相关的视效与音效提示会被自动添加到关卡中。 游戏在`2-1N`中引入了一种全新的单发拍机制:跳过单发拍。这种单发拍的规则很简单:跳过单发拍之后的一次单发拍按拍会被跳过。跳过单发拍无需手动添加提示,只需在单发拍修饰符中选择跳过单发拍,相关的视效与音效提示会被自动添加到关卡中。
当然,在被跳过的单发拍的这段空白时间里,也可以放置多个单发拍脉冲,以丰富谱面的节奏设计。跳过拍的“间隔”参数会影响跳过符号的动画时长。 当然,在被跳过的单发拍的这段空白时间里,也可以放置多个单发拍脉冲,以丰富谱面的节奏设计。跳过拍的“间隔”参数会影响跳过符号的动画时长。
需要注意的是,跳过单发拍会跳过下一次**击拍**,而不是下一个循环节。 需要注意的是,跳过单发拍会跳过下一次**击拍**,而不是下一个循环节。
```rdview 7 4 ```rdview 7 4
eb.AddOneshotBeat 1[type=skipshot]; eb.AddOneshotBeat 1[type=skipshot];
@@ -152,19 +152,19 @@ eb.AddOneshotBeat 4.5 2[tick=1.5];
eb.AddOneshotBeat 5.25 2[tick=0.75]; eb.AddOneshotBeat 5.25 2[tick=0.75];
``` ```
另外,在剧情关卡中,跳过拍的位置如果按下空格会计算成失误,但编辑器中默认是不会失误的。 另外,在剧情关卡中,跳过拍的位置如果按下空格会计算成失误,但编辑器中默认是不会失误的。
## 方块单发拍 ## 方块单发拍
常规的单发拍是针对一个节奏型的不断重复,而**方块单发拍**是“一次性”的单发拍,即没有重复,只需按拍一次。 常规的单发拍是针对一个节奏型的不断重复,而**方块单发拍**是“一次性”的单发拍,即没有重复,只需按拍一次。
方块单发拍最早出现于`MD-1`,在第五章更新后也出现在了`5-X`用于预警细分拍的出现。 方块单发拍最早出现于`MD-1`,在第五章更新后也出现在了`5-X`用于预警细分拍的出现。
选中一个单发拍,在修饰符中选择方块符号``,将细分数量设为 1 ,便创建了一个方块单发拍。方块单发拍出现时需要添加特殊音效,在方块单发拍的声音栏选择“特殊”即可自动添加提示音。 选中一个单发拍,在修饰符中选择方块符号``,将细分数量设为 1 ,便创建了一个方块单发拍。方块单发拍出现时需要添加特殊音效,在方块单发拍的声音栏选择“特殊”即可自动添加提示音。
![](../images/moreoneshotbeats-subdivision.png) ![](../images/moreoneshotbeats-subdivision.png)
除此之外,方块单发拍也需要护士提示。这种护士提示与普通单发拍的提示类似,唯一的不同点在于最后的 Go 被替换为了 One。 除此之外,方块单发拍也需要护士提示。这种护士提示与普通单发拍的提示类似,唯一的不同点在于最后的 Go 被替换为了 One。
```rdview 5 3 ```rdview 5 3
eb.AddOneshotBeat 3[]; eb.AddOneshotBeat 3[];
@@ -173,15 +173,15 @@ es.SayReadyGetSetGo_Set 2 1[];
es.SayReadyGetSetGo_Go 3 1[]; es.SayReadyGetSetGo_Go 3 1[];
``` ```
如果相同拍长的方块单发拍连续出现了多次,无需为每次出现添加完整的护士提示。可以逐步减少护士提示,直到完全没有护士提示,只剩下特殊提示音。 如果相同拍长的方块单发拍连续出现了多次,无需为每次出现添加完整的护士提示。可以逐步减少护士提示,直到完全没有护士提示,只剩下特殊提示音。
## 细分拍 ## 细分拍
与方块单发拍的概念类似,**细分拍**也是“一次性”的单发拍,但是会产生多次等间距的心跳脉冲,按键的次数也变为多次。 与方块单发拍的概念类似,**细分拍**也是“一次性”的单发拍,但是会产生多次等间距的心跳脉冲,按键的次数也变为多次。
细分拍最早出现于`MD-1`,当时官方谱面没有官方名称,且最多仅有两个三角拍;后来第五章更新后三角拍首次出现在`5-1`并正式更名为**细分拍**,并出现了多个细分。 细分拍最早出现于`MD-1`,当时官方谱面没有官方名称,且最多仅有两个三角拍;后来第五章更新后三角拍首次出现在`5-1`并正式更名为**细分拍**,并出现了多个细分。
在修饰符中选择方块后,将细分数量设为大于 1 的值或者点击细分符号``,即可创建细分拍。特殊音效的添加与方块单发拍类似。最多可以将细分数量设置为 10 。 在修饰符中选择方块后,将细分数量设为大于 1 的值或者点击细分符号``,即可创建细分拍。特殊音效的添加与方块单发拍类似。最多可以将细分数量设置为 10 。
![](../images/moreoneshotbeats-amountofsubdivision.png) ![](../images/moreoneshotbeats-amountofsubdivision.png)
@@ -192,7 +192,7 @@ eb.AddOneshotBeat 1 2[type=subdivision,subdivision=4];
eb.AddOneshotBeat 1 3[type=subdivision,subdivision=5]; eb.AddOneshotBeat 1 3[type=subdivision,subdivision=5];
``` ```
在护士提示方面,不同于方块单发拍,细分拍需要在出拍前的半拍长开始数拍提示。 在护士提示方面,不同于方块单发拍,细分拍需要在出拍前的半拍长开始数拍提示。
```rdview 13 ```rdview 13
eb.AddOneshotBeat 1[type=subdivision,subdivision=2]; eb.AddOneshotBeat 1[type=subdivision,subdivision=2];
@@ -219,13 +219,13 @@ g 8 1 1 1[offset=1]{
![](../images/moreoneshotbeats-setnursesay.png) ![](../images/moreoneshotbeats-setnursesay.png)
> 注意:非必要情况下,不要关闭方块单发拍和细分拍本身的声音。这对于仅靠听觉游玩的玩家而言是必要的。 > 注意:非必要情况下,不要关闭方块单发拍和细分拍本身的声音。这对于仅靠听觉游玩的玩家而言是必要的。
### 单独的细分拍 ### 单独的细分拍
跟 `MD-1` 一样,你可以通过 `Rea Dy Get Set Two~Ten` 来提示单独出现的细分 2 至细分 10 。 跟 `MD-1` 一样,你可以通过 `Rea Dy Get Set Two~Ten` 来提示单独出现的细分 2 至细分 10 。
单独细分的提示**与正常单发拍的提示类似**,Rea Dy 或 Get Set 之间的间隔同样用于提示细分拍的拍长,且数字提示**和 Go! 提示一样不需要提前**。 单独细分的提示**与正常单发拍的提示类似**,Rea Dy 或 Get Set 之间的间隔同样用于提示细分拍的拍长,且数字提示**和 Go! 提示一样不需要提前**。
```rdview 14 3 ```rdview 14 3
eb.AddOneshotBeat 3[type=subdivision,subdivision=2]; eb.AddOneshotBeat 3[type=subdivision,subdivision=2];
@@ -238,13 +238,13 @@ es.SayReadyGetSetGo_Set 7 1[];
es.SayReadyGetSetGo_4 9 1[]; es.SayReadyGetSetGo_4 9 1[];
``` ```
## 冻结拍🧊 ## 冻结拍🧊
**冻结拍**会将单发拍冻住,使下一拍相比正常按拍位置延后一些。 **冻结拍**会将单发拍冻住,使下一拍相比正常按拍位置延后一些。
冻结拍很早就存在于编辑器中,但尚未在官方关卡出现,第五章更新后终于在`5-2`首次露面,并且有了新的特效。 冻结拍很早就存在于编辑器中,但尚未在官方关卡出现,第五章更新后终于在`5-2`首次露面,并且有了新的特效。
在修饰符中选中冰块符号`🧊`,就创建了一个冻结拍。创建一个冻结拍后,会多出“延迟”与“间隔”选项,同时相比于更新前,冻结拍在编辑器内的样式也有所变化。 在修饰符中选中冰块符号`🧊`,就创建了一个冻结拍。创建一个冻结拍后,会多出“延迟”与“间隔”选项,同时相比于更新前,冻结拍在编辑器内的样式也有所变化。
![](../images/moreoneshotbeats-freezeshot.png) ![](../images/moreoneshotbeats-freezeshot.png)
@@ -253,25 +253,25 @@ eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
eb.AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2,tick=2]; eb.AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2,tick=2];
``` ```
对于一个冻结拍而言,一共有五个关键的时间点:左十字,右十字,出拍点(绿线),原按拍点(蓝线),实际按拍点(黄线)。 对于一个冻结拍而言,一共有五个关键的时间点:左十字,右十字,出拍点(绿线),原按拍点(蓝线),实际按拍点(黄线)。
```rdview 4 2 ```rdview 4 2
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2]; eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
``` ```
- **左十字**和**右十字**会显示两次冰冻特效并分别播放不同的冰冻音效,用于预示拍延长的时间。 - **左十字**和**右十字**会显示两次冰冻特效并分别播放不同的冰冻音效,用于预示拍延长的时间。
- **绿线**代表单发拍会在此响一次。 - **绿线**代表单发拍会在此响一次。
- **蓝线**处会将人物冻成冰块,再显示一次冰冻特效并播放一次冰冻音效,表示拍延长的开始。 - **蓝线**处会将人物冻成冰块,再显示一次冰冻特效并播放一次冰冻音效,表示拍延长的开始。
- **黄线**处拍会解冻并播放最后一次冰冻音效,也是玩家应该按拍的时机。 - **黄线**处拍会解冻并播放最后一次冰冻音效,也是玩家应该按拍的时机。
- **绿线**与**蓝线**之间的间隔就是`拍长`的时间。 - **绿线**与**蓝线**之间的间隔就是`拍长`的时间。
- **左十字**与**右十字**或者**蓝线**与**黄线**之间的间隔就是`延迟`的时间。 - **左十字**与**右十字**或者**蓝线**与**黄线**之间的间隔就是`延迟`的时间。
- **左十字**与**蓝线**或者**右十字**与**黄线**之间的间隔就是`间隔`的时间。 - **左十字**与**蓝线**或者**右十字**与**黄线**之间的间隔就是`间隔`的时间。
- 以前由于冻结拍尚不完善导致**绿线**不允许夹在**两个十字**之间,现在这种操作已经允许了。 - 以前由于冻结拍尚不完善导致**绿线**不允许夹在**两个十字**之间,现在这种操作已经允许了。
若设计冻结拍,还必须满足以下两个时间点正确: 若设计冻结拍,还必须满足以下两个时间点正确:
- **蓝线**所在时间应在正常单发拍的按拍位置上; - **蓝线**所在时间应在正常单发拍的按拍位置上;
- **左十字**必须和上一个普通单发拍的按拍位置重合。 - **左十字**必须和上一个普通单发拍的按拍位置重合。
```rdview 5 3 ```rdview 5 3
eb.AddOneshotBeat 1[]; eb.AddOneshotBeat 1[];
@@ -280,32 +280,44 @@ eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80]; eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
``` ```
## 灼热拍子🔥 如果需要在冻结拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
与**冻结拍子**相反,**灼热拍子**会形成一股热浪,使下一拍的按拍位置提前一些。 ```rdview 5 3
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
g 2 -1 2 2 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 1[];
g 0 -2 2 3 #0000[offset=0 1]{
eb.AddOneshotBeat -1 3[tick=2.5];
};
};
```
第五章更新后,灼热拍子在`5-2N`首次出现。 ## 灼热拍🔥
在修饰符中选中火焰符号`🔥`,就创建了一个灼热拍子。创建一个灼热拍子后,会多出“间隔”选项 与**冻结拍**相反,**灼热拍**会形成一股热浪,使下一拍的按拍位置提前一些
第五章更新后,灼热拍在`5-2N`首次出现。
在修饰符中选中火焰符号`🔥`,就创建了一个灼热拍。创建一个灼热拍后,会多出“间隔”选项。
![](../images/moreoneshotbeats-burnshot.png) ![](../images/moreoneshotbeats-burnshot.png)
类似地,对于一个灼热拍,一共有四个关键的时间点:左十字,右十字,出拍点(绿线),按拍点。为作区分且方便表述,将会把按拍点称为红线。 类似地,对于一个灼热拍,一共有四个关键的时间点:左十字,右十字,出拍点(绿线),按拍点。为作区分且方便表述,将会把按拍点称为红线。
```rdview 5 2 ```rdview 5 2
eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2]; eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
``` ```
- **左十字**和**右十字**会显示两次热浪特效并分别播放不同的灼热音效,用于预示拍实际的时间。 - **左十字**和**右十字**会显示两次热浪特效并分别播放不同的灼热音效,用于预示拍实际的时间。
- **绿线**处会再显示一次热浪特效并播放一次灼热特效,同时单发拍会在此响一次。 - **绿线**处会再显示一次热浪特效并播放一次灼热特效,同时单发拍会在此响一次。
- **红线**处拍会解除灼热状态并播放最后一次灼热音效,也是玩家应该按拍的时机。 - **红线**处拍会解除灼热状态并播放最后一次灼热音效,也是玩家应该按拍的时机。
- **左十字**与**右十字**或者**绿线**与**红线**之间的间隔就是`拍长`的时间。 - **左十字**与**右十字**或者**绿线**与**红线**之间的间隔就是`拍长`的时间。
- **左十字**与**绿线**或者**右十字**与**红线**之间的间隔就是`间隔`的时间。 - **左十字**与**绿线**或者**右十字**与**红线**之间的间隔就是`间隔`的时间。
若设计灼热拍,还必须满足以下两个时间点正确: 若设计灼热拍,还必须满足以下两个时间点正确:
- **绿线**所在时间应在正常单发拍的原定出拍位置上; - **绿线**所在时间应在正常单发拍的原定出拍位置上;
- **左十字**必须和上一个普通单发拍的原定出拍位置重合。 - **左十字**必须和上一个普通单发拍的原定出拍位置重合。
```rdview 5 3 ```rdview 5 3
eb.AddOneshotBeat 1[]; eb.AddOneshotBeat 1[];
@@ -314,7 +326,19 @@ eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80]; eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
``` ```
最后我们针对细分拍子、冻结拍子、灼热拍子做一个技术总结: 如果需要在灼热拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
```rdview 5 3
eb.AddOneshotBeat 2[type=burnshot,delay=0.5,interval=2];
g 0 -1 3 2 #0000[offset=0 1]{
eb.AddOneshotBeat 1.5 1[tick=1.5];
g 0 -1 3 3 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 2[tick=2.5];
};
};
```
最后我们针对细分拍、冻结拍、灼热拍做一个技术总结:
```rdview 19 7 ```rdview 19 7
eb.AddOneshotBeat 1[loop=1]; eb.AddOneshotBeat 1[loop=1];
@@ -346,28 +370,28 @@ g 13 5 1 1[offset=1]{
}; };
``` ```
## 冲击拍 ## 冲击拍
**冲击拍**属于一种单发拍,但在通常情况下相比正常的单发拍而言更快,护士在喊口令的同时会伴有新的哨声提示。 **冲击拍**属于一种单发拍,但在通常情况下相比正常的单发拍而言更快,护士在喊口令的同时会伴有新的哨声提示。
第五章更新后,冲击拍在`5-X`首次出现。虽然`2-2`中最快的单发拍也相当快,但并不属于冲击拍 第五章更新后,冲击拍在`5-X`首次出现。虽然`2-2`中最快的单发拍也相当快,但并不属于冲击拍。
冲击拍的本质是**护士提示带有哨声的单发拍**,**正拍按拍**以及**快速**是其显著特征,这两点在`5-X`中也有所体现。 冲击拍的本质是**护士提示带有哨声的单发拍**,**正拍按拍**以及**快速**是其显著特征,这两点在`5-X`中也有所体现。
冲击拍除了正常的护士提示之外,开始前后还会分别会伴随四声哨子,顺序分别为**短低短低**和**高高高高**,每声哨子之间的间隔相同,且四声哨子 - 高音效与前四下按拍重合,如下图上半部分。 冲击拍除了正常的护士提示之外,开始前后还会分别会伴随四声哨子,顺序分别为**短低短低**和**高高高高**,每声哨子之间的间隔相同,且四声哨子 - 高音效与前四下按拍重合,如下图上半部分。
同时,仅当面临如下图下半部分特定的冲击像正常单发拍转化时,最后三个冲击拍的按拍会伴随三声哨子,顺序分别为**高高低**。 同时,仅当面临如下图下半部分特定的冲击像正常单发拍转化时,最后三个冲击拍的按拍会伴随三声哨子,顺序分别为**高高低**。
![](../images/moreoneshotbeats-blastbeat.png) ![](../images/moreoneshotbeats-blastbeat.png)
![](/images/13-23.png) ![](/images/13-23.png)
*请注意,灰色的护士提示事件只是为了方便理解单发拍机制,实际只需要放置红色护士语音事件即可。* *请注意,灰色的护士提示事件只是为了方便理解单发拍机制,实际只需要放置红色护士语音事件即可。*
虽然`5-X`引入了冲击拍,但有几点仍需注意: 虽然`5-X`引入了冲击拍,但有几点仍需注意:
1. **冲击拍依旧是一种单发拍**,其护士提示方式与正常拍长的单发拍基本无异,只不过在拍长较短时需要多加两个 Rea Dy 延长预先提示。 1. **冲击拍依旧是一种单发拍**,其护士提示方式与正常拍长的单发拍基本无异,只不过在拍长较短时需要多加两个 Rea Dy 延长预先提示。
2. **冲击拍并没有正拍按拍以及高速度的强制要求**,只要是一组循环的单发拍,无论正反拍都可以加上哨声提示当作冲击拍使用,只需注意使用场景、合理性与便捷性。 2. **冲击拍并没有正拍按拍以及高速度的强制要求**,只要是一组循环的单发拍,无论正反拍都可以加上哨声提示当作冲击拍使用,只需注意使用场景、合理性与便捷性。
3. 哨声虽然是判定冲击拍的指标,但是**这只是一种新的提示方式**,只是为了让玩家更好地应对单发拍,请勿为了创造冲击拍而随意添加哨声堆砌提示,否则会显得相当冗杂。 3. 哨声虽然是判定冲击拍的指标,但是**这只是一种新的提示方式**,只是为了让玩家更好地应对单发拍,请勿为了创造冲击拍而随意添加哨声堆砌提示,否则会显得相当冗杂。
简言之,冲击拍并不是什么新东西,只是加了哨子提示的单发拍,仅此而已。尽管在`5-X`里其速度和正反按拍切换令人印象深刻,但只需像正常单发拍一样使用即可。 简言之,冲击拍并不是什么新东西,只是加了哨子提示的单发拍,仅此而已。尽管在`5-X`里其速度和正反按拍切换令人印象深刻,但只需像正常单发拍一样使用即可。
总的来说,冲击拍只是为了符合`5-X`剧情而设计的机制,其提示方式相较于正常单发拍更为复杂,如果有需要可以添加哨声提示以创造出冲击拍,但是只使用护士语音提示也是完全合理的,需根据个人需求进行选择。~~虽然我不推荐用就是了,提示整得那么麻烦还容易出错。~~ 总的来说,冲击拍只是为了符合`5-X`剧情而设计的机制,其提示方式相较于正常单发拍更为复杂,如果有需要可以添加哨声提示以创造出冲击拍,但是只使用护士语音提示也是完全合理的,需根据个人需求进行选择。~~虽然我不推荐用就是了,提示整得那么麻烦还容易出错。~~
+1 -1
View File
@@ -29,7 +29,7 @@ eb.AddOneshotBeat 5 1[tick=0.75,loop=3,interval=1];
## 单发拍子护士提示 ## 单发拍子护士提示
单发拍子与普通拍子不同之处在于,普通拍子可以观察前六次脉冲,推测出第拍的正确按键时机;但单发拍子只有一次脉冲,玩家无法仅通过单发拍子的脉冲得知按键时机。因此,为了能让玩家正确击打单发拍子,**必须为单发拍子添加护士提示**。 单发拍子与普通拍子不同之处在于,普通拍子可以观察前六次脉冲,推测出第拍的正确按键时机;但单发拍子只有一次脉冲,玩家无法仅通过单发拍子的脉冲得知按键时机。因此,为了能让玩家正确击打单发拍子,**必须为单发拍子添加护士提示**。
![护士语音设置](../images/oneshotbeat-whatshouldnursesay.png) ![护士语音设置](../images/oneshotbeat-whatshouldnursesay.png)
+59 -29
View File
@@ -30,7 +30,7 @@
老话说得好,知其然也要知其所以然。json文件也是可以手动写一个的,只要你能明白文件的编排方式就行。如果需要使用,只需要把这一堆复制下来保存到.txt中,数据按自己的来改动,最后把文件后缀名改成.json即可。**注意这个文件的文件名必须和精灵图的完全一致**。模板在下面: 老话说得好,知其然也要知其所以然。json文件也是可以手动写一个的,只要你能明白文件的编排方式就行。如果需要使用,只需要把这一堆复制下来保存到.txt中,数据按自己的来改动,最后把文件后缀名改成.json即可。**注意这个文件的文件名必须和精灵图的完全一致**。模板在下面:
<details><summary><strong>点我展开</strong></summary> <details><summary><strong>点我展开</strong></summary>
```json <pre data-lang="json"><code>
{ {
"size": [64,64], "size": [64,64],
"name":"吾夜犹明", "name":"吾夜犹明",
@@ -80,7 +80,6 @@
}, },
] ]
} }
```
</code></pre> </code></pre>
</details> </details>
@@ -100,34 +99,64 @@
显示这个缩略图的时候,画面正中心偏移多少像素。 显示这个缩略图的时候,画面正中心偏移多少像素。
- **`"clips"`** - **`"clips"`**
里面是对每个表情分别进行设置。 里面是对每个表情分别进行设置。
- **`"name"`** - **`"name"`**
后续出现的name指的是人物的各个表情, 后续出现的name指的是人物的各个表情,
以下四个表情是默认存在的,因为和按拍效果有关,但你也可以在此基础上增加新的表情,自己命名一个其他表情,这会在紫色版块**播放表情**中有效。 以下四个表情是默认存在的,因为和按拍效果有关,但你也可以在此基础上增加新的表情,自己命名一个其他表情,这会在紫色版块**播放表情**中有效。
- `neutral`表示普通表情, - `neutral` 表示普通状态下的默认表情,
- `happy`表示拍子按对时的表情, - `happy` 是**击中**时的表情,
- `barely`表示拍子按歪时的表情, - `barely` 是**失误**的表情,
- `missed`表示漏过没按时的表情。 - `missed` 是**错失**的表情。
- **`"frames"`** > `neutral` 表情是**必需**的,无论是轨道角色还是精灵资源。
这是每种状态所涉及的帧数。在精灵图里从左往右,从0开始计算。例如: 在早期,以上四个表情在轨道角色上时也是必需的,不过在后来的版本中移除了其他三个表情的限制。
- `[0,1]`:第1张和第2张组合成两帧的动图; 此字段需要满足[这些要求](../pages/spritesheet.md#自定义角色的表情机制)。
- `[3,5]`:第4张和第6张组合成两帧的动图; - **`"frames"`**
- `[0,1,1,1]`第1张和第2张组合成四帧的动图,其中第2张播放三次。 这是每种状态所涉及的帧数。在精灵图里从左往右,从0开始计算。例如:
- **`"loop"`** - `[0,1]`:第1张和第2张组合成两帧的动图;
动图的循环模式,如果是`no`那么播放完就会自动回到neutral状态,如果是`onbeat`就会一直循环。 - `[3,5]`:第4张和第6张组合成两帧的动图;
- **`"loopStart"`** - `[0,1,1,1]`第1张和第2张组合成四帧的动图,其中第2张播放三次。
如果设置为循环,首先按列表播放一次完整动画,接着再从列表内第loopstart+1项播放到结尾,并以此为循环。打比方做了一个5帧的骑车的动画`[助力,上车,坐下,蹬左脚,蹬右脚]`,loopstart:3,则实际效果为骑上车后循环骑车动作。 - **`"loop"`**
- **`"fps"`** 动图的循环模式
动图播放的帧数,填大于0的帧数,就是按填写的帧数播放;填0就自动随着音乐bpm改变 - `no`:播放一次回到 neutral 状态
- **`"portaitOffset"`** - `yes`:按 `fps` 值循环。
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘的起始位置。以每一帧画面的左下角作为原点,第一个数字为横坐标,第二个数字为纵坐标。 - `onBeat`:按节拍循环。
- **`"portaitSize"`** > 当 `loop` 值不为 `onBeat` 且 `neutral` 表情没有帧时会导致精灵缩放效果在播放至第三拍时停止(无论是从哪个小节开始)。这是一个已知 bug。
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘图片的像素大小。第一个数字为立绘的宽度,第二个数字为立绘的高度。 - **`"loopStart"`**
- **`"portaitScale"`** 在每轮循环帧播放完毕后回到 `frames` 中此值所指处的帧继续播放。
图片放大比例,这是显示对话时的立绘用的,因为立绘画面一般都只是人物的头部和上身,且立绘窗口也比较大,这里最好填写大于1的数字 例:一个 5 帧的动画`[5,6,7,8,9]`loopstart3,实际效果为 `[5,6,7,8,9,8,9,8,9,8,9,...]`
- **`"fps"`**
播放帧率,填大于0的帧率,就是按填写的帧率播放;填0就自动随着音乐bpm改变。可以为小数。
- **`"portaitOffset"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘的起始位置。以每一帧画面的左下角作为原点,第一个数字为横坐标,第二个数字为纵坐标。
- **`"portaitSize"`**
截取动图中的一部分作为显示对话时的立绘,两个数字表示截取立绘图片的像素大小。第一个数字为立绘的宽度,第二个数字为立绘的高度。
- **`"portaitScale"`**
图片放大比例,这是显示对话时的立绘用的,因为立绘画面一般都只是人物的头部和上身,且立绘窗口也比较大,这里最好填写大于1的数字。
## 回到编辑器添加人物 ## 自定义角色的音效
一切准备就绪,现在进入编辑器添加一个人物,然后在人物设置里面点一下文件夹图标,选择刚才我们写好的.json文件。 游戏在运行到**显示对话**事件时,如果遇到对应的表情 [ID](../pages/spritesheet.md#自定义角色的表情机制) 时,会尝试:
- 寻找此自定义角色的同名 ID 音频文件
- 若无,寻找此自定义角色的默认 ID 音频文件
- 若无,设为默认对话音频文件。
游戏会尝试在对话框内每弹出一个字就播放一次。
## 自定义角色的表情机制
一个自定义角色可以拥有多个表情,每个表情都对应一个 ID,
为便于理解,这里假设我们的精灵名为 `吾夜犹明`
ID 的规则如下:
- `neutral` 表情对应无后缀的精灵名,即 `吾夜犹明`
- 其他表情对应 `[精灵名]_[表情名]`,例如 `happy` 对应 `吾夜犹明_happy`
此规则会影响**显示对话**事件的角色表情选取和音效文件选取。
这也是为什么表情名不能含有 `_` 和文件名不合法字符。
## 回到编辑器添加自定义角色
一切准备就绪,现在进入编辑器添加一个自定义角色,然后在人物设置里面点一下文件夹图标,选择刚才我们写好的.json文件。
![](../images/spritesheet-openfile.png) ![](../images/spritesheet-openfile.png)
@@ -148,7 +177,8 @@
## 选择立绘 ## 选择立绘
在[这里](../pages/captions.md#显示对话)我们已经提到过,显示对话的对话框中手动写下立绘名字也是可以插入立绘的。如果我们在这里导入一个文件名为“吾夜犹明”的猫猫,然后在对话框的开头写下`吾夜犹明:`,那么播放的立绘就是默认的neutral那一项。**依然需要注意是英文的冒号**。如果想要选择其他表情作为立绘,需要额外写一个下划线,后面加上表情名,例如`吾夜犹明_happy:``吾夜犹明_barely:``吾夜犹明_missed:` 在[这里](../pages/captions.md#显示对话)我们已经提到过,显示对话的对话框中手动写下立绘名字也是可以插入立绘的。如果我们需要让对话显示立绘,需要在一行中输入 `[表情 ID]:[对话内容]`。表情格式见[表情 ID](../pages/spritesheet.md#自定义角色的表情机制)。
例如 `吾夜犹明_happy:今天真是美好的一天。`
如果我们在json文件中自己添加了名为`Error`自定义表情,例如: 如果我们在json文件中自己添加了名为`Error`自定义表情,例如:
+5 -5
View File
@@ -207,7 +207,7 @@ g 1 3 3 1 #df0{
考虑到还是会有一些情况下事件并非都是重复的,而是零零散散真的有一大堆。这种情况下把事件摆放清楚会在作谱的过程中事倍功半。 考虑到还是会有一些情况下事件并非都是重复的,而是零零散散真的有一大堆。这种情况下把事件摆放清楚会在作谱的过程中事倍功半。
**1、**每一行放些什么事件最好能由明确的区分,方便后期梳理自己设计了一些什么。比如把贯穿全程的浮动文字歌词都放在同一行,把移动轨道都放在同一行等。 1. 每一行放些什么事件最好能由明确的区分,方便后期梳理自己设计了一些什么。比如把贯穿全程的浮动文字歌词都放在同一行,把移动轨道都放在同一行等。
```rdview 19 4 ```rdview 19 4
g 0 0 19 1 #f00[]{ g 0 0 19 1 #f00[]{
@@ -320,7 +320,7 @@ g 0 3 18 1 #f80[]{
}; };
``` ```
**2、**如果有一串同类型的事件紧密排布,例如每1/4格或者1/2格放一个,那么可以考虑不放在同一行,而是上下交错排开。 2. 如果有一串同类型的事件紧密排布,例如每1/4格或者1/2格放一个,那么可以考虑不放在同一行,而是上下交错排开。
```rdview 16 4 ```rdview 16 4
ea.MoveRow []; ea.MoveRow [];
@@ -350,10 +350,10 @@ ea.MoveRow 15 1[];
ea.Comment 4 3[]; ea.Comment 4 3[];
``` ```
**3、**对于事件总体数量不太高的情况,如果有操作房间和轨道类型的特效,可以考虑把他们放在对应数量的行里,找起来会更加方便。 3. 对于事件总体数量不太高的情况,如果有操作房间和轨道类型的特效,可以考虑把他们放在对应数量的行里,找起来会更加方便。
**4、**如果你发现在音乐初始位置需要叠放大量特效来给各个轨道和主题等做初始化,那么也最好把同类特效叠在一起。这分为两种情况:一种是相同的特效叠在一起;另一种是操作相同的对象的叠在一起。这两种都是非常直观的。 4. 如果你发现在音乐初始位置需要叠放大量特效来给各个轨道和主题等做初始化,那么也最好把同类特效叠在一起。这分为两种情况:一种是相同的特效叠在一起;另一种是操作相同的对象的叠在一起。这两种都是非常直观的。
**5、**如果需要花费一个比较长的时间跨度来制作一个谱面,那么最好放置一些注释来解释每一个小段的设计思路,以免在咕咕了一阵子之后完全失去了方向。**注释**事件在可以在红色、紫色、绿色、黄色的版块里找到。 5. 如果需要花费一个比较长的时间跨度来制作一个谱面,那么最好放置一些注释来解释每一个小段的设计思路,以免在咕咕了一阵子之后完全失去了方向。**注释**事件在可以在红色、紫色、绿色、黄色的版块里找到。
![](../images/tag-comment.png) ![](../images/tag-comment.png)
+21
View File
@@ -0,0 +1,21 @@
# 双人模式设计
> 此页面处于初稿阶段,欢迎补充!
## 切换至双人模式
[在编辑器预览栏下](../pages/editorinterface.md)打开双人模式选项,这时候预览画面中的所有玩家操控的轨道会相应地变换。
由玩家 1 操控的轨道的默认颜色变为蓝色(`#0050FF`),心脏处会增加一个蓝色的 P1 图标;
由玩家 2 操控的轨道的默认颜色变为红色(`#AC0929`),心脏处会增加一个红色的 P2 图标。
指示轨道控制者的图标(P1,P2,CPU)都不会受轨道染色事件影响。
在播放关卡过程中会影响以下游戏元素的行为。
- 切换玩家轨道事件是为改变轨道控制者而设计的。将下拉单选框的选项改为`双人模式``单人或双人模式`时,事件就会在双人模式下执行。
- 编辑条件时将类型改为玩家模式,再在下面的玩家模式中选择双人,即可使应用了这个条件的事件仅在双人模式下执行。
## 多轨道设计
官方在 v0.18.0 时重新设计了大量关卡的双人模式版本。
在设计双人模式关卡时,最核心的要点就是:需要让玩家分清他所控制的轨道。这同时也适用于在关卡中将某个轨道托付给 Otto 时的情况。
在为单发拍使用护士提示时需要用两种护士音效以区分玩家,通常为 P1 使用的语音为**护士**,P2 使用的语音为**伊恩**。你也可以根据实际场景交换这两者。
轨道的音效使用左右声道作区分(例如左声道只播放 P1 需要操控的轨道的音效而右声道相反)也是一个不错的选择。
当 P1、P2 交换轨道控制权时,需要使用护士语音 `Switch` 以提示即将交换轨道控制权。
+22 -12
View File
@@ -29,9 +29,9 @@
可写变量与只读变量类似,其参数可以随游戏进程变化,与只读变量不同的是,可写变量的值可人为更改。可写变量大致可分为三类: 可写变量与只读变量类似,其参数可以随游戏进程变化,与只读变量不同的是,可写变量的值可人为更改。可写变量大致可分为三类:
> 1. **整型**(int):整型变量可以存储整数,变量名写作i0、i1、...、i9。 - **整型**(int):整型变量可以存储整数,变量名写作i0、i1、...、i9。
> 2. **浮点型**(float):浮点型变量可以存储浮点数(也就是小数。特别地,形如1.0的值为浮点数),变量名写作f0、f1、...、f9。 - **浮点型**(float):浮点型变量可以存储浮点数(也就是小数。特别地,形如1.0的值为浮点数),变量名写作f0、f1、...、f9。
> 3. **布尔型**boolean, 也可简写为 bool):布尔型变量可以存储布尔值。布尔值包含且仅包含True(真)和False(伪)。布尔型变量名写作b0、b1、...、b9。 - **布尔型**boolean/bool):布尔型变量可以存储布尔值。布尔值包含且仅包含True(真)和False(伪)。布尔型变量名写作b0、b1、...、b9。
需要注意的是上述三类变量,每类有且仅有所列举的10个,且无法修改变量名。一般地,整形变量的值属于区间[-2147483648, 2147483647],而浮点型变量取值范围相对广泛。 需要注意的是上述三类变量,每类有且仅有所列举的10个,且无法修改变量名。一般地,整形变量的值属于区间[-2147483648, 2147483647],而浮点型变量取值范围相对广泛。
@@ -55,14 +55,24 @@
| < | 小于 | | < | 小于 |
| > | 大于 | | > | 大于 |
> 为了保留更高的兼容性,参与计算的数据都会转换为浮点数,也就意味着:
> - int 和 bool 实际上都会转换为 float
> - 转换为 int 的操作是四舍五入,并且偏好于最临近的偶数,例如:
> - `i0 = 2.75` => i0: 3
> - `i0 = 2.5` => i0: 2
> - `i0 = 3.5` => i0: 4
> - 转换为 bool 的操作是非 0 即为 True,例如:
> - `b0 = -3` => b0: True
> - `b0 = -0.0` => b0: False
变量可以在调用自定义方法;浮动文字、显示对话、显示状态牌;条件中使用。 变量可以在调用自定义方法;浮动文字、显示对话、显示状态牌;条件中使用。
> 1. **调用自定义方法** - **调用自定义方法**
> 可以在调用自定义方法的表达式框里对变量进行读写、赋值,可使用赋值运算符。 可以在调用自定义方法的表达式框里对变量进行读写、赋值,可使用赋值运算符。
> 例如:i1++(使i1的值+1),i4=4(将i4的值赋为4) 例如:i1++(使i1的值+1),i4=4(将i4的值赋为4)
> 2. **浮动文字、显示对话、显示状态牌** - **浮动文字、显示对话、显示状态牌**
> 变量可以嵌入到这些事件的显示内容中,使用时将表达式用大括号包裹起来,可使用算数运算符连接。 变量可以嵌入到这些事件的显示内容中,使用时将表达式用大括号包裹起来,可使用算数运算符连接。
> 例如:“武士有{i0}个朋友,洛根比武士多{i1}个朋友,所以洛根有{i0+i1}个朋友。”这句话在i0==4且i1==6时输出为“武士有4个朋友,洛根比武士多6个朋友,所以洛根有10个朋友。” 例如:“武士有{i0}个朋友,洛根比武士多{i1}个朋友,所以洛根有{i0+i1}个朋友。”这句话在i0==4且i1==6时输出为“武士有4个朋友,洛根比武士多6个朋友,所以洛根有10个朋友。”
> 3. **条件** - **条件**
> 变量可用于自定义类型条件的表达式框中。通常与比较运算符结合使用。 变量可用于自定义类型条件的表达式框中。通常与比较运算符结合使用。
> 例如:i0==10i0等于10)、i5<i0i5小于i0 例如:i0==10i0等于10)、i5\<i0i5小于i0
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+1 -1
View File
@@ -5,5 +5,5 @@
* (c) 2017-2023 JP Erasmus <jperasmus11@gmail.com> * (c) 2017-2023 JP Erasmus <jperasmus11@gmail.com>
* MIT license * MIT license
*/ */
!function(){"use strict";function e(o){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(o)}!function(e,o){void 0===o&&(o={});var t=o.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=e:c.appendChild(document.createTextNode(e))}}(".docsify-copy-code-button,.docsify-copy-code-button>span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{background:grey;background:var(--theme-color,grey);border:0;border-radius:0;color:#fff;font-size:1em;opacity:0;outline:0;overflow:visible;padding:.65em .8em;position:absolute;right:0;top:0;z-index:1}.docsify-copy-code-button>span{background:inherit;border-radius:3px;pointer-events:none}.docsify-copy-code-button>.error,.docsify-copy-code-button>.success{font-size:.825em;opacity:0;padding:.5em .65em;position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:-100}.docsify-copy-code-button.error>.error,.docsify-copy-code-button.success>.success{opacity:1;right:100%;transform:translate(-25%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}.docsify-copy-code-button>[aria-live]{height:1px;left:-10000px;overflow:hidden;position:absolute;top:auto;width:1px}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(e,o){e.ready((function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")}))}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,t){var n={buttonText:"Copy to clipboard",errorText:"Error",successText:"Copied"};o.doneEach((function(){var o=Array.from(document.querySelectorAll("pre[data-lang]"));t.config.copyCode&&Object.keys(n).forEach((function(o){var c=t.config.copyCode[o];"string"==typeof c?n[o]=c:"object"===e(c)&&Object.keys(c).some((function(e){var t=location.href.indexOf(e)>-1;return n[o]=t?c[e]:n[o],t}))}));var c=['<button class="docsify-copy-code-button">','<span class="label">'.concat(n.buttonText,"</span>"),'<span class="error" aria-hidden="hidden">'.concat(n.errorText,"</span>"),'<span class="success" aria-hidden="hidden">'.concat(n.successText,"</span>"),'<span aria-live="polite"></span>',"</button>"].join("");o.forEach((function(e){e.insertAdjacentHTML("beforeend",c)}))})),o.mounted((function(){var e=document.querySelector(".content");e&&e.addEventListener("click",(function(e){if(e.target.classList.contains("docsify-copy-code-button")){var o="BUTTON"===e.target.tagName?e.target:e.target.parentNode,t=document.createRange(),c=o.parentNode.querySelector("code"),i=o.querySelector("[aria-live]"),r=window.getSelection();t.selectNode(c),r&&(r.removeAllRanges(),r.addRange(t));try{document.execCommand("copy")&&(o.classList.add("success"),i.innerText=n.successText,setTimeout((function(){o.classList.remove("success"),i.innerText=""}),1e3))}catch(e){console.error("docsify-copy-code: ".concat(e)),o.classList.add("error"),i.innerText=n.errorText,setTimeout((function(){o.classList.remove("error"),i.innerText=""}),1e3)}(r=window.getSelection())&&("function"==typeof r.removeRange?r.removeRange(t):"function"==typeof r.removeAllRanges&&r.removeAllRanges())}}))}))}].concat(window.$docsify.plugins||[])}(); !function(){"use strict";function e(o){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(o)}!function(e,o){void 0===o&&(o={});var t=o.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=e:c.appendChild(document.createTextNode(e))}}(".docsify-copy-code-button,.docsify-copy-code-button>span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{background:grey;background:var(--theme-color,grey);border:0;border-radius:0;color:#fff;font-size:1em;opacity:0;outline:0;overflow:visible;padding:.65em .8em;position:absolute;right:0;top:0;z-index:1}.docsify-copy-code-button>span{background:inherit;border-radius:3px;pointer-events:none}.docsify-copy-code-button>.error,.docsify-copy-code-button>.success{font-size:.825em;opacity:0;padding:.5em .65em;position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:-100}.docsify-copy-code-button.error>.error,.docsify-copy-code-button.success>.success{opacity:1;right:100%;transform:translate(-25%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}.docsify-copy-code-button>[aria-live]{height:1px;left:-10000px;overflow:hidden;position:absolute;top:auto;width:1px}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(e,o){e.ready((function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")}))}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,t){var n={buttonText:"复制",errorText:"错误",successText:"已复制"};o.doneEach((function(){var o=Array.from(document.querySelectorAll("pre[data-lang]"));t.config.copyCode&&Object.keys(n).forEach((function(o){var c=t.config.copyCode[o];"string"==typeof c?n[o]=c:"object"===e(c)&&Object.keys(c).some((function(e){var t=location.href.indexOf(e)>-1;return n[o]=t?c[e]:n[o],t}))}));var c=['<button class="docsify-copy-code-button">','<span class="label">'.concat(n.buttonText,"</span>"),'<span class="error" aria-hidden="hidden">'.concat(n.errorText,"</span>"),'<span class="success" aria-hidden="hidden">'.concat(n.successText,"</span>"),'<span aria-live="polite"></span>',"</button>"].join("");o.forEach((function(e){e.insertAdjacentHTML("beforeend",c)}))})),o.mounted((function(){var e=document.querySelector(".content");e&&e.addEventListener("click",(function(e){if(e.target.classList.contains("docsify-copy-code-button")){var o="BUTTON"===e.target.tagName?e.target:e.target.parentNode,t=document.createRange(),c=o.parentNode.querySelector("code"),i=o.querySelector("[aria-live]"),r=window.getSelection();t.selectNode(c),r&&(r.removeAllRanges(),r.addRange(t));try{document.execCommand("copy")&&(o.classList.add("success"),i.innerText=n.successText,setTimeout((function(){o.classList.remove("success"),i.innerText=""}),1e3))}catch(e){console.error("docsify-copy-code: ".concat(e)),o.classList.add("error"),i.innerText=n.errorText,setTimeout((function(){o.classList.remove("error"),i.innerText=""}),1e3)}(r=window.getSelection())&&("function"==typeof r.removeRange?r.removeRange(t):"function"==typeof r.removeAllRanges&&r.removeAllRanges())}}))}))}].concat(window.$docsify.plugins||[])}();
//# sourceMappingURL=docsify-copy-code.min.js.map //# sourceMappingURL=docsify-copy-code.min.js.map
+11 -8
View File
@@ -10,8 +10,11 @@ var ottos = [
"/images/otto-rd2-xmas-compressed.png", "/images/otto-rd2-xmas-compressed.png",
"/images/otto-traditional.png", "/images/otto-traditional.png",
] ]
function getCurrentImage() {
ottoIndex = (ottoIndex) % ottos.length;
return ottos[ottoIndex];
}
var ottoIndex = 0; var ottoIndex = 0;
function showOtto(callback) { function showOtto(callback) {
node.style.opacity = 1; node.style.opacity = 1;
anim = node.animate([ anim = node.animate([
@@ -37,13 +40,13 @@ function hideOtto(callback) {
} }
} }
window.onload = function () { window.addEventListener("load", function () {
fetch("tips.json").then(response => response.json()).then(data => { fetch("tips.json").then(response => response.json()).then(data => {
tipTexts = data; tipTexts = data;
}); });
addOtto(document); addOtto(document);
showOtto() showOtto()
} })
window.$docsify.onOttoShow = function () { window.$docsify.onOttoShow = function () {
@@ -57,9 +60,14 @@ window.$docsify.onOttoShow = function () {
} else { } else {
hideOtto(); hideOtto();
ottoIndex++; ottoIndex++;
window.localStorage.setItem('ottoIndex', ottoIndex);
} }
} }
function addOtto(document) { function addOtto(document) {
let localStorageOttoIndex = window.localStorage.getItem('ottoIndex');
if (localStorageOttoIndex !== null) {
ottoIndex = parseInt(localStorageOttoIndex);
}
node = document.createElement("div"); node = document.createElement("div");
node.className = "otto"; node.className = "otto";
image = document.createElement("img"); image = document.createElement("img");
@@ -117,8 +125,3 @@ function playTips() {
function randomTips() { function randomTips() {
return tipTexts[Math.floor(Math.random() * tipTexts.length)]; return tipTexts[Math.floor(Math.random() * tipTexts.length)];
} }
function getCurrentImage() {
ottoIndex = (ottoIndex) % ottos.length;
return ottos[ottoIndex];
}
+8 -8
View File
@@ -5,22 +5,22 @@ let columny;
let positions = { let positions = {
"c1": { "x": 64, "name": "第一拍" }, "c1": { "x": 64, "name": "第一拍" },
"c2": { "x": 88, "name": "第单发拍" }, "c2": { "x": 88, "name": "第拍" },
"c3": { "x": 112, "name": "第三拍" }, "c3": { "x": 112, "name": "第三拍" },
"c4": { "x": 136, "name": "第四拍" }, "c4": { "x": 136, "name": "第四拍" },
"c5": { "x": 160, "name": "第五拍" }, "c5": { "x": 160, "name": "第五拍" },
"c6": { "x": 184, "name": "第六拍" }, "c6": { "x": 184, "name": "第六拍" },
"c7": { "x": 249, "name": "第拍" }, "c7": { "x": 249, "name": "第拍" },
"ccharacter": { "x": 35, "name": "角色" }, "ccharacter": { "x": 35, "name": "角色" },
"ccenter": { "x": 176, "name": "中心" }, "ccenter": { "x": 176, "name": "中心" },
"cheart": { "x": 317, "name": "心" }, "cheart": { "x": 317, "name": "心" },
"r1": { "y": 162, "name": "四轨时的第轨" }, "r1": { "y": 162, "name": "四轨时的第轨" },
"r2": { "y": 141, "name": "三轨时的第轨" }, "r2": { "y": 141, "name": "三轨时的第轨" },
"r3": { "y": 120, "name": "二轨时的第一轨/四轨时的第轨" }, "r3": { "y": 120, "name": "二轨时的第一轨/四轨时的第轨" },
"r4": { "y": 99, "name": "一轨时的第一轨/三轨时的第二轨" }, "r4": { "y": 99, "name": "一轨时的第一轨/三轨时的第二轨" },
"r5": { "y": 78, "name": "二轨时的第二轨/四轨时的第轨" }, "r5": { "y": 78, "name": "二轨时的第二轨/四轨时的第轨" },
"r6": { "y": 57, "name": "三轨时的第轨" }, "r6": { "y": 57, "name": "三轨时的第轨" },
"r7": { "y": 36, "name": "四轨时的第轨" }, "r7": { "y": 36, "name": "四轨时的第轨" },
} }
function themePosition() { function themePosition() {
+65 -59
View File
@@ -14,37 +14,22 @@ var switchLightDarkModeOptions = {
svgColor: '#7d7b75', svgColor: '#7d7b75',
} }
window.$docsify.currentThemeModeIndex = 2; const themeModes = ['light', 'dark', 'auto',]
// Docsify plugin functions var switchSpan = document.createElement('span')
function plugin(hook, vm) {
if (!switchLightDarkModeOptions.useSwitchMode) { switchSpan.id = 'switchLightDarkModeDivBeforeArticle'
return switchSpan.style.position = 'fixed'
} switchSpan.style.right = switchLightDarkModeOptions.right.toString() + 'px'
switchSpan.style.top = switchLightDarkModeOptions.top.toString() + 'px'
let themeModes = ['light', 'dark', 'auto',] const lightModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 18a6 6 0 100-12 6 6 0 000 12zM22 12h1M12 2V1M12 23v-1M20 20l-1-1M20 4l-1 1M4 20l1-1M4 4l1 1M1 12h1" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const darkModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 11.507a9.493 9.493 0 0018 4.219c-8.507 0-12.726-4.22-12.726-12.726A9.494 9.494 0 003 11.507z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
let currentThemeModeIndex = 2 const autoModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 15c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M3 20c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M19 10a7 7 0 10-14 0" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomInIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M8 11h3m3 0h-3m0 0V8m0 3v3M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
hook.mounted(function () { const zoomOutIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0zM8 11h6" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
// let lightTheme = Docsify.dom.findAll('[href="/style/light.css"]')[0] const zoomDefaultIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 19a7 7 0 100-14 7 7 0 000 14zM12 19v2M5 12H3M12 5V3M19 12h2" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
// let darkTheme = Docsify.dom.findAll('[href="/style/dark.css"]')[0] const ottoIconXml = `
var switchSpan = document.createElement('span')
switchSpan.id = 'switchLightDarkModeDivBeforeArticle'
switchSpan.style.position = 'fixed'
switchSpan.style.right = switchLightDarkModeOptions.right.toString() + 'px'
switchSpan.style.top = switchLightDarkModeOptions.top.toString() + 'px'
const lightModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 18a6 6 0 100-12 6 6 0 000 12zM22 12h1M12 2V1M12 23v-1M20 20l-1-1M20 4l-1 1M4 20l1-1M4 4l1 1M1 12h1" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const darkModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 11.507a9.493 9.493 0 0018 4.219c-8.507 0-12.726-4.22-12.726-12.726A9.494 9.494 0 003 11.507z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const autoModeIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M3 15c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M3 20c2.483 0 4.345-3 4.345-3s1.862 3 4.345 3c2.482 0 4.965-3 4.965-3s2.483 3 4.345 3M19 10a7 7 0 10-14 0" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomInIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M8 11h3m3 0h-3m0 0V8m0 3v3M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0z" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomOutIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M17 17l4 4M3 11a8 8 0 1016 0 8 8 0 00-16 0zM8 11h6" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const zoomDefaultIconXml = '<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="' + switchLightDarkModeOptions.svgColor + '"><path d="M12 19a7 7 0 100-14 7 7 0 000 14zM12 19v2M5 12H3M12 5V3M19 12h2" stroke="' + switchLightDarkModeOptions.svgColor + '" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
const ottoIconXml = `
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="30px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" color="` + switchLightDarkModeOptions.svgColor + `" stroke="` + switchLightDarkModeOptions.svgColor + `" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="26px" height="30px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" color="` + switchLightDarkModeOptions.svgColor + `" stroke="` + switchLightDarkModeOptions.svgColor + `" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path class="st0" d="M17.5,12.9c0,1-.1,1.8-1,1.8s-1.6-.8-1.6-1.8.7-1.8,1.6-1.8,1,.8,1,1.8Z"/> <path class="st0" d="M17.5,12.9c0,1-.1,1.8-1,1.8s-1.6-.8-1.6-1.8.7-1.8,1.6-1.8,1,.8,1,1.8Z"/>
@@ -61,38 +46,58 @@ function plugin(hook, vm) {
<line class="st0" x1="16.8" y1="11.1" x2="16.8" y2="1.4"/> <line class="st0" x1="16.8" y1="11.1" x2="16.8" y2="1.4"/>
</svg> </svg>
` `
let setThemeMode = function (currentTheme) { let setThemeMode = function (currentTheme) {
switch (currentTheme) { switch (currentTheme) {
case 'light': case 'light':
// lightTheme.disabled = false document.body.classList.remove('dark')
// darkTheme.disabled = true document.body.classList.add('light')
document.body.classList.remove('dark') switchSpan.innerHTML = lightModeIconXml
document.body.classList.add('light') break
switchSpan.innerHTML = lightModeIconXml case 'dark':
break document.body.classList.remove('light')
case 'dark': document.body.classList.add('dark')
// lightTheme.disabled = true switchSpan.innerHTML = darkModeIconXml
// darkTheme.disabled = false break
document.body.classList.remove('light') case 'auto':
document.body.classList.add('dark') var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
switchSpan.innerHTML = darkModeIconXml if (isDarkMode) {
break document.body.classList.remove('light')
case 'auto': document.body.classList.add('dark')
var isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
// lightTheme.disabled = isDarkMode
// darkTheme.disabled = !isDarkMode
if (isDarkMode) {
document.body.classList.remove('light')
document.body.classList.add('dark')
}
else {
document.body.classList.remove('dark')
document.body.classList.add('light')
}
switchSpan.innerHTML = autoModeIconXml
break
}
} }
else {
document.body.classList.remove('dark')
document.body.classList.add('light')
}
switchSpan.innerHTML = autoModeIconXml
break
}
}
window.$docsify.currentThemeModeIndex = 2;
// Docsify plugin functions
function plugin(hook, vm) {
if (!switchLightDarkModeOptions.useSwitchMode) {
return
}
let localStorageCurrentThemeModeIndex = window.localStorage.getItem('currentThemeModeIndex')
let currentThemeModeIndex = 2
if (localStorageCurrentThemeModeIndex !== null) {
currentThemeModeIndex = parseInt(window.localStorage.getItem('currentThemeModeIndex'))
}
window.$docsify.currentThemeModeIndex = currentThemeModeIndex
setThemeMode(themeModes[currentThemeModeIndex])
window.localStorage.setItem('currentThemeModeIndex', currentThemeModeIndex)
if (window.$docsify.onLightDarkModeChange) {
window.$docsify.onLightDarkModeChange();
}
hook.mounted(function () {
// let lightTheme = Docsify.dom.findAll('[href="/style/light.css"]')[0]
// let darkTheme = Docsify.dom.findAll('[href="/style/dark.css"]')[0]
setThemeMode(themeModes[currentThemeModeIndex]) setThemeMode(themeModes[currentThemeModeIndex])
let preferredThemeChangeEventListenerFunction = function () { let preferredThemeChangeEventListenerFunction = function () {
@@ -111,6 +116,7 @@ function plugin(hook, vm) {
} }
window.$docsify.currentThemeModeIndex = currentThemeModeIndex window.$docsify.currentThemeModeIndex = currentThemeModeIndex
setThemeMode(themeModes[currentThemeModeIndex]) setThemeMode(themeModes[currentThemeModeIndex])
window.localStorage.setItem('currentThemeModeIndex', currentThemeModeIndex)
if (window.$docsify.onLightDarkModeChange) { if (window.$docsify.onLightDarkModeChange) {
window.$docsify.onLightDarkModeChange(); window.$docsify.onLightDarkModeChange();
} }
+78 -14
View File
@@ -5,6 +5,20 @@
src: url(/font/DinkieBitmap-7px.woff2) format('woff2'); src: url(/font/DinkieBitmap-7px.woff2) format('woff2');
} }
@font-face {
font-family: 'd7i';
font-style: normal;
font-weight: 400;
src: url(/font/DinkieBitmap-9pxItalic.woff2) format('woff2');
}
@font-face {
font-family: 'd7c';
font-style: normal;
font-weight: 400;
src: url(/font/DinkieBitmap-9pxCode.woff2) format('woff2');
}
@font-face { @font-face {
font-family: 'd9'; font-family: 'd9';
font-style: normal; font-style: normal;
@@ -172,6 +186,7 @@ div.image-tip img {
div.image-tip { div.image-tip {
pointer-events: none; pointer-events: none;
& p { & p {
pointer-events: auto; pointer-events: auto;
height: 0; height: 0;
@@ -182,7 +197,8 @@ div.image-tip {
justify-self: center; justify-self: center;
transition: all 0.2s; transition: all 0.2s;
} }
& img{
& img {
pointer-events: auto; pointer-events: auto;
} }
} }
@@ -410,12 +426,12 @@ main.hidden {
} }
.sidebar-nav>ul>li>ul>li>a:has(>img):hover { .sidebar-nav>ul>li>ul>li>a:has(>img):hover {
transition: transform .4s cubic-bezier(.16,-0.17,.65,-0.5); transition: transform .4s cubic-bezier(.16, -0.17, .65, -0.5);
transform: translateX(-103px); transform: translateX(-103px);
} }
.sidebar-nav>ul>li>a:has(>img):hover { .sidebar-nav>ul>li>a:has(>img):hover {
transition: transform .4s cubic-bezier(.16,-0.17,.65,-0.5); transition: transform .4s cubic-bezier(.16, -0.17, .65, -0.5);
transform: translateX(-83px); transform: translateX(-83px);
} }
@@ -471,7 +487,7 @@ main.hidden {
} }
.sidebar-toggle { .sidebar-toggle {
background: linear-gradient(to right, transparent 50px, #0ff8); background: linear-gradient(to right, #0dd 50px, #0ff8);
border: 0; border: 0;
outline: none; outline: none;
padding: 10px; padding: 10px;
@@ -538,10 +554,25 @@ body.sticky .sidebar-toggle {
.markdown-section:has(iframe[single]) { .markdown-section:has(iframe[single]) {
height: 100%; height: 100%;
&>p {
display: block;
position: absolute;
padding: 10px;
border-radius: 0 10px 0 10px;
opacity: 50%;
top: 0;
transition: background-color 0.3s, opacity 0.3s;
&:hover {
opacity: 100%;
transition: opacity 0.3s;
}
}
} }
.markdown-section iframe { .markdown-section iframe {
border: 1px solid #eee; border: 1px solid #000;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-width: 100% min-width: 100%
@@ -918,7 +949,8 @@ section.cover blockquote>p>a {
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5));
transition: transform 0.3s, background-color 0.3s, color 0.3s; transition: transform 0.3s, background-color 0.3s, color 0.3s;
&:hover{
&:hover {
transform: translate(0, -5px) rotate(.3deg); transform: translate(0, -5px) rotate(.3deg);
transition: transform 0.3s, background-color 0.3s, color 0.3s; transition: transform 0.3s, background-color 0.3s, color 0.3s;
} }
@@ -929,10 +961,6 @@ section.cover blockquote>p>a {
margin-left: 0; margin-left: 0;
} }
.markdown-section iframe {
margin: 1em 0
}
.markdown-section em { .markdown-section em {
color: #7f8c8d color: #7f8c8d
} }
@@ -950,7 +978,7 @@ section.cover blockquote>p>a {
.markdown-section output, .markdown-section output,
.markdown-section pre { .markdown-section pre {
margin: 1.2em 0; margin: 1em 0;
position: relative position: relative
} }
@@ -967,7 +995,7 @@ section.cover blockquote>p>a {
} }
.markdown-section code { .markdown-section code {
border-radius: 2px; border-radius: 4px;
margin: 0 2px; margin: 0 2px;
padding: 3px 5px; padding: 3px 5px;
white-space: pre-wrap white-space: pre-wrap
@@ -986,8 +1014,8 @@ section.cover blockquote>p>a {
.markdown-section pre>code { .markdown-section pre>code {
color: #525252; color: #525252;
font-size: .8rem; font-size: .8rem;
padding: 2.2em 5px; padding: 1em 5px;
line-height: inherit; line-height: 1.2em;
margin: 0 2px; margin: 0 2px;
max-width: inherit; max-width: inherit;
overflow: inherit; overflow: inherit;
@@ -1130,6 +1158,7 @@ div.bv {
height: 0; height: 0;
padding-bottom: 56.25%; padding-bottom: 56.25%;
margin-bottom: 30px; margin-bottom: 30px;
&>iframe { &>iframe {
position: absolute; position: absolute;
background-color: transparent; background-color: transparent;
@@ -1137,6 +1166,7 @@ div.bv {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 1em 0;
filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 10px 10px rgba(0, 0, 0, 0.5));
} }
} }
@@ -1224,7 +1254,41 @@ article.markdown-section:has(.intro)>.notice {
article.markdown-section:has(.intro)>.notice[onclick] { article.markdown-section:has(.intro)>.notice[onclick] {
cursor: pointer; cursor: pointer;
transition: all 0.5s; transition: all 0.5s;
&:hover { &:hover {
transition: all 0.5s; transition: all 0.5s;
} }
}
div.medium-zoom-overlay {
opacity: 0.5 !important;
}
del {
text-decoration: none;
background-color: #5C5D67;
color: #5C5D67;
transition: background-color 0.1s, color 0.1s;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #7B7C84;
color: #7B7C84;
transition: background-color 0.1s, color 0.1s;
}
&:active {
background-color: inherit;
color: inherit;
transition: background-color 0.1s, color 0.1s;
}
}
.docsify-copy-code-button{
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.3));
padding: 0;
margin: 0;
border-radius: 5px;
font: 300 12px 'd9c', 'd9';
} }
+21 -18
View File
@@ -7,12 +7,16 @@
.markdown-section blockquote{ .markdown-section blockquote{
background-color: var(--section-blockquote-background-color); background-color: var(--section-blockquote-background-color);
color: var(--section-blockquote-color); color: var(--main-color);
} }
.markdown-section p.tip, .markdown-section p.tip,
.markdown-section tr:nth-child(2n) { .markdown-section tr:nth-child(2n) {
background-color: var(--section-background-color); background-color: var(--table-2nd-row-background-color);
}
.markdown-section tr:nth-child(2n-1) {
background-color: var(--table-1st-row-background-color);
} }
.progress { .progress {
@@ -128,11 +132,11 @@ body {
.markdown-section h3, .markdown-section h3,
.markdown-section h4, .markdown-section h4,
.markdown-section strong { .markdown-section strong {
color: var(--section-background-color); color: var(--section-color);
} }
.markdown-section a { .markdown-section a {
color: var(--theme-color, var(--main-theme-color)); color: var(--theme-color, var(--section-blockquote-color));
} }
.markdown-section blockquote { .markdown-section blockquote {
@@ -142,7 +146,7 @@ body {
.markdown-section code { .markdown-section code {
background-color: var(--section-main-color); background-color: var(--section-main-color);
color: var(--section-background-color); color: var(--section-blockquote-color);
font-family: d9c, d9; font-family: d9c, d9;
} }
@@ -151,9 +155,6 @@ body {
-webkit-font-smoothing: initial; -webkit-font-smoothing: initial;
background-color: var(--section-main-color); background-color: var(--section-main-color);
font-family: d9c, d9; font-family: d9c, d9;
margin: 1.2em 0;
padding: 0 1.4rem;
position: relative;
} }
.token.string { .token.string {
@@ -172,22 +173,14 @@ body {
-moz-osx-font-smoothing: initial; -moz-osx-font-smoothing: initial;
-webkit-font-smoothing: initial; -webkit-font-smoothing: initial;
background-color: var(--section-main-color); background-color: var(--section-main-color);
border-radius: 2px; color: var(--section-blockquote-color);
color: var(--section-background-color);
display: block;
font-family: d9c, d9; font-family: d9c, d9;
} }
.markdown-section code:after,
.markdown-section code:before {
letter-spacing: .05rem
}
pre:after { pre:after {
color: #ccc; color: #ccc;
content: attr(data-lang); content: attr(data-lang);
top: 0 top: 0;
} }
.markdown-section p.tip { .markdown-section p.tip {
@@ -220,4 +213,14 @@ article.markdown-section:has(.intro)>.notice {
&[onclick]:hover{ &[onclick]:hover{
background-color: var(--section-main-color); background-color: var(--section-main-color);
} }
}
div.medium-zoom-overlay{
background-color: var(--background-color) !important;
}
.markdown-section:has(iframe[single]) {
&>p{
background-color: var(--background-color);
}
} }
+6 -1
View File
@@ -2,11 +2,12 @@ body.dark {
--main-color: #c8c8c8; --main-color: #c8c8c8;
--main-theme-color: #008080; --main-theme-color: #008080;
--background-color: #1f1f1f; --background-color: #1f1f1f;
--background-border-color: #303030;
--section-main-color: #0f0f0f; --section-main-color: #0f0f0f;
--section-background-color: #003333; --section-background-color: #003333;
--section-table-header-border-color: #303030; --section-table-header-border-color: #303030;
--section-blockquote-background-color: #003a3a; --section-blockquote-background-color: #003a3a;
--section-blockquote-color: #d4d4d4; --section-blockquote-color: #7cdcfe;
--section-tip-background-color: #161616; --section-tip-background-color: #161616;
--section-tip-color: #3f3f3fcc; --section-tip-color: #3f3f3fcc;
--section-p-warn-background-color: #ea6f5a1a; --section-p-warn-background-color: #ea6f5a1a;
@@ -14,7 +15,11 @@ body.dark {
--notice-background-color: #3f3f3fcc; --notice-background-color: #3f3f3fcc;
--notice-border-color: #303030; --notice-border-color: #303030;
--notice-hover-background-color: #525252cc; --notice-hover-background-color: #525252cc;
--table-1st-row-background-color: #00000038;
--table-2nd-row-background-color: #000000cc;
-webkit-tap-highlight-color: rgb(0, 0, 0); -webkit-tap-highlight-color: rgb(0, 0, 0);
--special-theme-position-backgound-color: #177500;
} }
body.dark .sidebar li>a:has(>img) { body.dark .sidebar li>a:has(>img) {
+5
View File
@@ -2,6 +2,7 @@ body.light {
--main-color: #202020; --main-color: #202020;
--main-theme-color: #008585; --main-theme-color: #008585;
--background-color: #dddddd; --background-color: #dddddd;
--background-border-color: #b1b1b1;
--section-main-color: #00c9c9; --section-main-color: #00c9c9;
--section-background-color: #009999; --section-background-color: #009999;
--section-table-header-border-color: #b1b1b1; --section-table-header-border-color: #b1b1b1;
@@ -14,7 +15,11 @@ body.light {
--notice-background-color: #f0f0f0; --notice-background-color: #f0f0f0;
--notice-border-color: #b1b1b1; --notice-border-color: #b1b1b1;
--notice-hover-background-color: #e0e0e0; --notice-hover-background-color: #e0e0e0;
--table-1st-row-background-color: #08c5ff26;
--table-2nd-row-background-color: #08c5ff5b;
-webkit-tap-highlight-color: rgb(255, 255, 255); -webkit-tap-highlight-color: rgb(255, 255, 255);
--special-theme-position-backgound-color: #05a000;
} }
body.light .sidebar li>a:has(>img) { body.light .sidebar li>a:has(>img) {
+3 -3
View File
@@ -114,9 +114,9 @@ div.theme-positions>.infobox {
width: max-content; width: max-content;
height: max-content; height: max-content;
padding: 3px; padding: 3px;
border: 2px solid #535353; border: 2px solid var( --background-border-color);
border-radius: 2px; border-radius: 2px;
background-color: #363636; background-color: var(--background-color);
text-align: center; text-align: center;
z-index: 20; z-index: 20;
font-size: 15px; font-size: 15px;
@@ -130,7 +130,7 @@ div.theme-positions>.infobox {
font-weight: bold; font-weight: bold;
&.c { &.c {
background-color: #177500; background-color: var(--special-theme-position-backgound-color);
border-radius: 3px; border-radius: 3px;
} }
} }