34 Commits
Author SHA1 Message Date
obugs fa4b7790ec 修改方法查询效果实现 2026-10-05 18:55:40 +08:00
obugs 3230511345 Merge branch 'main' into check 2026-10-05 17:57:34 +08:00
obugs 2af05f761e 添加一个简单的自定义方法查询功能 2026-10-05 17:55:10 +08:00
obugs 52474483e8 将 rdview 底层全量替换为 rdtkview 2026-10-05 16:32:09 +08:00
obugs 7d77bad610 Merge pull request 'merge' (#61) from check into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#61
2026-08-05 15:53:07 +08:00
zyxandobugs b3579bbd07 关于1.1.2的一些更新 (#60)
文本装饰、二拍摇晃、gotolevel相关的更新

Reviewed-on: RDCNWebs/rd.rdlevel.cn#60
Co-authored-by: Zyx2333Zyx <zyx_______123456@163.com>
2026-08-02 18:11:18 +08:00
obugs 3828811b3f Merge pull request '加了一些内容' (#59) from kuanpan/rd.rdlevel.cn:check into check
Reviewed-on: RDCNWebs/rd.rdlevel.cn#59
2026-06-26 13:26:58 +08:00
kuanpan a606de738a 更新 pages/moreclassicbeats.md 2026-06-20 14:52:37 +08:00
kuanpan 3c8733312c 更新 pages/moreclassicbeats.md 2026-06-20 14:51:33 +08:00
kuanpan 2b9a5a008c 更新 pages/decoration.md 2026-06-18 18:53:37 +08:00
kuanpan 39c0db4373 更新 pages/decoration.md 2026-06-18 18:53:02 +08:00
kuanpan 05ae18d2ff 更新 pages/decoration.md 2026-06-18 18:52:10 +08:00
kuanpan a51b4fea03 更新 pages/decoration.md 2026-06-18 18:51:26 +08:00
kuanpan 98b5707c60 更新 pages/decoration.md 2026-06-18 18:50:53 +08:00
kuanpan d6bbec8601 更新 pages/WindowDance.md 2026-06-18 18:38:25 +08:00
kuanpan d62a6164a0 更新 pages/moreoneshotbeats.md 2026-06-18 18:33:27 +08:00
kuanpan 38fa5cffcc 更新 pages/moreclassicbeats.md 2026-06-18 18:15:59 +08:00
kuanpan dfb2c9cf4e 上传文件至 images 2026-06-18 18:03:18 +08:00
kuanpan 08b0b338b4 更新 pages/vfx.md 2026-06-18 17:58:50 +08:00
kuanpan 74a156ab43 更新 pages/captions.md 2026-06-18 17:53:30 +08:00
kuanpan e61c719989 更新 pages/captions.md 2026-06-18 17:53:03 +08:00
kuanpan a8162f59df 上传文件至 images 2026-06-18 17:49:04 +08:00
kuanpan 74e4ec7ef3 更新 pages/settheme.md 2026-06-18 17:48:28 +08:00
kuanpan 58ee445c39 更新 pages/rowfx.md 2026-06-18 17:40:49 +08:00
kuanpan 22ea4b8319 更新 pages/moverow.md 2026-06-18 17:37:11 +08:00
kuanpan bfd781a545 上传文件至 images 2026-06-18 17:34:42 +08:00
kuanpan 661cf0ff74 更新 pages/holdbeat.md 2026-06-18 17:31:06 +08:00
kuanpan 3928730839 更新 pages/classicbeat.md 2026-06-18 17:25:10 +08:00
kuanpan c446bd296c 更新 pages/createbuiltincharacter.md 2026-06-18 17:21:50 +08:00
kuanpan accc3e46cf 更新 pages/camera.md 2026-06-18 17:16:54 +08:00
kuanpan 54a4b7fdc1 更新 pages/camera.md 2026-06-18 17:16:00 +08:00
kuanpan b44f808592 上传文件至 images 2026-06-18 17:10:44 +08:00
obugs 3b3a6d4090 Merge pull request '更新日志' (#58) from check into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#58
2026-06-06 12:21:05 +08:00
obugs 6cd73fd1bd Merge pull request 'check' (#57) from check into main
Reviewed-on: RDCNWebs/rd.rdlevel.cn#57
2026-06-06 12:07:22 +08:00
54 changed files with 8886 additions and 2326 deletions
+3 -3
View File
@@ -30,9 +30,9 @@
如果成功安装,那么以下内容的 markdown 预览应当是一个事件栏。
```rdview 7 5 1
es.PlaySong 0 0[];
es.SetCrotchetsPerBar_4 0 1[];
eb.AddClassicBeat 0 2[tick=6];
PlaySong 0 0[];
SetCrotchetsPerBar 0 1[crotchetsPerBar=4];
AddClassicBeat 0 2[tick=6];
```
### RhythmBase.View
+1 -1
View File
@@ -39,7 +39,6 @@
- [<img src="/indexicon/camera.png"/><p>镜头移动</p>](/pages/camera.md)
- [<img src="/indexicon/roomfx.png"/><p>房间视效</p>](/pages/roomfx.md)
- [<img src=""/><p>视觉干扰</p>](/pages/vfxpro.md)
- [<img src="/indexicon/camerafx.png"/><p>摄像机视效</p>](/pages/camerafx.md)
- [<img src="/indexicon/hallofmirror.png"/><p>镜厅</p>](/pages/hallofmirror.md)
- [<img src="/indexicon/windowmove.png"/><p>窗口相关的动画</p>](/pages/WindowDance.md)
- [<img src="/indexicon/hand.png"/><p>手</p>](/pages/hand.md)
@@ -51,6 +50,7 @@
- [<img src="/indexicon/custommethod.png"/><p>自定义方法</p>](/pages/custommethod.md)
- [<img src="/indexicon/condition.png"/><p>条件</p>](/pages/condition.md)
- [<img src="/indexicon/variable.png"/><p>表达式</p>](/pages/expression.md)
- [<img src="/indexicon/setplaystyleandgotolevel.png"/><p>关卡播放与跳转<p>](/pages/setplaystyleandgotolevel.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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+4 -81
View File
@@ -15,93 +15,15 @@
<link rel="stylesheet" href="/style/dark.css">
<link rel="stylesheet" href="/style/color.css">
<link rel="stylesheet" href="/style/rdview.css">
<link rel="stylesheet" href="/style/rdview-rdtk.css">
<link rel="stylesheet" href="/style/otto.css">
<link rel="stylesheet" href="/style/special.css">
<script src="/script/config.js"></script>
</head>
<body>
<div id="app">加载中...</div>
<script>
$docsify = {
name: "节奏医生编辑器教程",
notFoundPage: "/404.md",
homepage: "/pages/intro.md",
loadSidebar: true,
loadNavbar: true,
coverpage: false,
maxLevel: 5,
subMaxLevel: 4,
mergeNavbar: true,
};
</script>
<script type="module">
import "./script/rdview.js";
import {render} from "./script/rdtkview_render.js";
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
loadRDView();
const originalCodeRenderer = $docsify.markdown
?.renderer?.code;
function codeblockRender(code, lang) {
if (lang.trim().startsWith("rdview")) {
return RDViewRender(lang, code);
}
// else if (lang.trim().startsWith("rdtkview")) {
// const args = lang.slice(8).trim().split(" ");
// const width = parseInt(args[0]) || 8;
// const height = parseInt(args[1]) || 4;
// const scale = parseFloat(args[2]) || 2.0;
// const result = render(code, width, height, scale);
// result.toString = () => result;
// return result;
// }
return originalCodeRenderer
? originalCodeRenderer(code, lang)
: `<pre data-lang="${lang}"><code>${
escapeHtml(code)
}</code></pre>`;
}
$docsify = {
...$docsify,
...{
markdown: {
renderer: {
code: (code, lang) => {
return codeblockRender(code, lang);
},
image: function (src, title, alt) {
return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${
alt ? `<p>${alt}</p>` : ""
}</div>`;
},
},
},
switchLightDarkMode: {
useSwitchMode: true,
top: 130,
right: 26,
svgColor: "#7d7b75",
},
search: {
noData: {
"/": "没有找到结果!",
},
paths: "auto",
placeholder: {
"/": "搜索",
},
hideOtherSidebarContent: true,
},
},
};
</script>
<script type="module" src="./script/rdview-init.js"></script>
<script type="module" src="./script/special.js"></script>
<!-- <script src="/script/docsify.min.js"></script> -->
<script src="/script/zoom-image.min.js"></script>
@@ -110,6 +32,7 @@
<!-- <script src="/script/docsify-copy-code.min.js"></script> -->
<script src="/script/switchLightDarkMode.js"></script>
<script src="/script/otto.js"></script>
<script src="/script/custommethod-data.js"></script>
<script src="/script/custommethod-search.js"></script>
<!-- <script
src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-yaml.js"
Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

+2
View File
@@ -217,6 +217,8 @@
注意到这个缓速的一部分会超出所设置的值,所以可以先将窗口缩小到`(1%,1%)`大小,然后使用`OutElastic`缓速将窗口缩放到`(200%,200%)`大小,当窗口变大的过程中会有几帧是占满整个屏幕的,此时放一个大小为`(200%,200%)`、时长为999、缓速为`Linear`的**窗口大小重置事件**,窗口大小就会固定成全屏的了。
> 使用注释指令`()=>windowSize(float SizeX%, float SizeY%, float duration)`可以直接将窗口缩放到200%以上,但这个方法只能控制窗口1的大小,还没有缓速。
### 更多窗口(暂时不确定官方会不会修这个)
在谱面中放置一个窗口相关的事件(这个例子用`隐藏窗口`事件),记住这个事件的小节、节拍和在第几行,然后打开这个谱面的文件(xxx.rdlevel),找到{ "bar": <小节>, "beat": <节拍>, "y": <行数减一>, "type": "HideWindow", "show": true },这一行,<>里面应该是你记住的数据,将"y": 后面的数替换成一个大于3的数,保存并返回编辑器,重新打开这个谱面,就会发现这个事件到第4行下面了,接着当正常窗口使用就行。
+8 -8
View File
@@ -1,11 +1,11 @@
# RDView Playground
<div class="rdview"><div class="canvas"></div><div class="editor"><div class="number"></div><textarea class="area" spellcheck="false" placeholder="Enter code here">
rdview 8 5
es.Comment 2[if=true];
Comment 2[tab=Sounds,if=true];
g 1 1 5 4 #0ff{
eb.SetRowXs[rowxs=--x-xx];
eb.AddClassicBeat 2[tick=3,swing=0.333,grayscale=100];
eb.AddOneshotBeat 2 1[
SetRowXs[rowxs=--x-xx];
AddClassicBeat 2[tick=0.5,swing=0.333,grayscale=100];
AddOneshotBeat 2 1[
type=freezeshot subdivision,
interval=2,
delay=0.25,
@@ -13,10 +13,10 @@ g 1 1 5 4 #0ff{
loop=2,
offset_grayscale=100
];
es.SayReadyGetSetGo_Get 0 2;
es.SayReadyGetSetGo_Set 1 2;
es.SayReadyGetSetGo_Go 2 2;
es.SayReadyGetSetGo_GetSetGo 0 3;
SayReadyGetSetGo 0 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 1 2[phraseToSay=JustSaySet];
SayReadyGetSetGo 2 2[phraseToSay=JustSayGo];
SayReadyGetSetGo 0 3[phraseToSay=SayGetSetGo];
};
</textarea></div></div>
+18 -18
View File
@@ -39,12 +39,12 @@
**心爆 3** :三次及以上累计的按拍 / 三个及以上重叠的按拍产生的心爆。
```rdview 3 5
eb.AddFreeTimeBeat 1 1[pulse=7];
eb.AddFreeTimeBeat 1 2[pulse=7];
eb.AddFreeTimeBeat 1 2[pulse=7];
eb.AddFreeTimeBeat 1 3[pulse=7];
eb.AddFreeTimeBeat 1 3[pulse=7];
eb.AddFreeTimeBeat 1 3[pulse=7];
AddFreeTimeBeat 1 1[pulse=6];
AddFreeTimeBeat 1 2[pulse=6];
AddFreeTimeBeat 1 2[pulse=6];
AddFreeTimeBeat 1 3[pulse=6];
AddFreeTimeBeat 1 3[pulse=6];
AddFreeTimeBeat 1 3[pulse=6];
```
### 设置心跳音量
@@ -73,19 +73,19 @@ eb.AddFreeTimeBeat 1 3[pulse=7];
```rdview 4 9
g 1 1{
g 1 0 0 1 #08f{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1.25 3 1.75 1 #08f{
g{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1.5 5 1.5 1 #08f{
g{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1.75 7 1.25 1 #08f{
g{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
```
@@ -97,35 +97,35 @@ g 1.75 7 1.25 1 #08f{
```rdview 7 9
g 1 1 0.75 1 #08f{
g 0 0 0.25{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1 3 1.25 1 #08f{
g 0 0 0.75{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1 5 1.25{
g 1.25 0 0 1 #08f{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 1 7 1.75 {
g 1.75 0 0 1 #08f{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 4 1 1 1 #08f{
g 0 0 0.5{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 4 3 1.5 1 #08f{
g{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 4 5 1.5{
g 1.5 0 0 1 #08f{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
g 4 7 2 {
g 2 0 0 1 #08f{};
eb.AddFreeTimeBeat [pulse=7];
AddFreeTimeBeat [pulse=6];
};
```
+25 -25
View File
@@ -47,15 +47,15 @@
首先,剧情模式出现两次五拍子机制,它们出现的位置在节奏上都是连续附点的,即两个心跳之间的间隔都是1.5拍,这样五拍子的有声拍长也是6,和普通的普通拍子是完全一致的。在这种连续附点的压迫感之下设置五拍,与音乐非常契合。
```rdview 14 4
eb.AddClassicBeat 6 1[tick=6];
eb.SetRowXs 1 2[rowxs=xx----];
eb.AddFreeTimeBeat 3 2[pulse=1];
eb.PulseFreeTimeBeat 4.5 2[pulse=9];
eb.PulseFreeTimeBeat 6 2[pulse=9];
eb.PulseFreeTimeBeat 7.5 2[pulse=9];
eb.PulseFreeTimeBeat 9 2[pulse=9];
eb.PulseFreeTimeBeat 10.5 2[pulse=9];
eb.PulseFreeTimeBeat 12 2[pulse=9];
AddClassicBeat 6 1[tick=6];
SetRowXs 1 2[rowxs=xx----];
AddFreeTimeBeat 3 2[pulse=1];
PulseFreeTimeBeat 4.5 2[pulse=9];
PulseFreeTimeBeat 6 2[pulse=9];
PulseFreeTimeBeat 7.5 2[pulse=9];
PulseFreeTimeBeat 9 2[pulse=9];
PulseFreeTimeBeat 10.5 2[pulse=9];
PulseFreeTimeBeat 12 2[pulse=9];
```
而我们自选音乐时很多时候遇到的情况都是连续五个采音点,但间隔都是1拍,这种情况与其精准采音`xx----`反而不如额外增加一个不存在的采音变成`-x----`或者`-----x`,这样既不损失与音乐的契合,又能把节奏归化简单。
@@ -89,22 +89,22 @@ eb.PulseFreeTimeBeat 12 2[pulse=9];
**三、**每一条轨道的**节奏功能做出严格的区分**,如果场上本身就有多轨,不要给一条轨道设计过多差距太大的节奏类型。同时,非必要尽量不要增添太多轨道。让很多轨道展现出花里胡哨的节奏但却在同一拍按键也会让人感觉很牵强。
```rdview 14 4
eb.AddClassicBeat 0 0[tick=1.5,swing=0.25];
eb.AddClassicBeat 2 0[tick=3,swing=0.5];
eb.AddClassicBeat 6 0[tick=3,hold=0.5,swing=0.5];
eb.AddFreeTimeBeat 10 0[pulse=1];
eb.PulseFreeTimeBeat 10.25[pulse=9];
eb.PulseFreeTimeBeat 11[pulse=9];
eb.PulseFreeTimeBeat 11.25[pulse=9];
eb.PulseFreeTimeBeat 12[pulse=9];
eb.PulseFreeTimeBeat 12.25[pulse=9];
eb.PulseFreeTimeBeat 13[pulse=9];
eb.AddClassicBeat 0 1[tick=6];
eb.AddClassicBeat 7 1[tick=1.5,swing=0.25];
eb.AddClassicBeat 9 1[tick=1.5,swing=0.25];
eb.AddClassicBeat 0 2[tick=3,hold=0.5,swing=0.5];
eb.AddClassicBeat 4 2[tick=3,swing=0.5];
eb.AddClassicBeat 7 2[tick=6];
AddClassicBeat 0 0[tick=1.5,swing=0.25];
AddClassicBeat 2 0[tick=3,swing=0.5];
AddClassicBeat 6 0[tick=3,hold=0.5,swing=0.5];
AddFreeTimeBeat 10 0[pulse=1];
PulseFreeTimeBeat 10.25[pulse=9];
PulseFreeTimeBeat 11[pulse=9];
PulseFreeTimeBeat 11.25[pulse=9];
PulseFreeTimeBeat 12[pulse=9];
PulseFreeTimeBeat 12.25[pulse=9];
PulseFreeTimeBeat 13[pulse=9];
AddClassicBeat 0 1[tick=6];
AddClassicBeat 7 1[tick=1.5,swing=0.25];
AddClassicBeat 9 1[tick=1.5,swing=0.25];
AddClassicBeat 0 2[tick=3,hold=0.5,swing=0.5];
AddClassicBeat 4 2[tick=3,swing=0.5];
AddClassicBeat 7 2[tick=6];
```
**四、**不要过度采音。更不要为了精准采音做出种类繁多,重复次数又少的自由拍子。节奏医生的游戏机制是和一般的音乐游戏不同的。音乐游戏是可以给每一个节奏点都设计按键(Note)的,但节奏医生却不能这样。节奏医生的本质是“提示拍 + 按键拍”的组合,**所有的提示拍全都应该按照没有按键的节奏点来设计**。如果像其他音乐游戏一样来设计节奏医生的谱面,得到的一定是塞爆谱。
+2
View File
@@ -101,6 +101,8 @@
**1、**所谓的移动摄像头,画一个三维空间出来也许可以让你更好地理解。它就像是我们举着一台摄像机拍摄自己的家一样,你的手或者身子移动,那么摄像头跟着移动,拍摄下来的整体画面位置就会不同。
如果“真实运动”选择的是开启,那么摄像头就是记录房间内的画面,怎么移动都在房间内;选择关闭则是像看录像一样,移动时会露出边缘。
![拍摄房间里的爱心和海莉](../images/camera-haleyandheart.gif)
**2、**移动摄像头最基本的用法就是把摄像头跟着节奏晃来晃去,与全程静止不动的背景相比,这样会给谱面增加一些动感。这个轨道移动的弹跳设计原理是一样的,仅仅是呈现的画面效果稍显不同。
+24 -10
View File
@@ -20,6 +20,10 @@
![](../images/floatingtext-position.png)
**字体**:可以强制文本是哪种字体,有像素字体(不支持中文)、矢量字体(中文默认显示的字体)和粗体字体(`2-X`判定文字同款)。点击旁边的文件夹图标可以自行导入ttf或otf格式字体文件。
![](../images/captions_FONT.png)
**位置**:指的是基准的位置。可以按一下准星符号再在预览画面窗口中点选位置。有条件精准找到位置的话还是建议在`X`和`Y`栏中输入具体数值。**点击右边的`%`按键可以切换屏幕像素点和屏幕百分比两种输入方式**。
![不同基准在同一位置下的不同效果](../images/floatingtext-align.png)
@@ -35,25 +39,25 @@
**创建文本递进**:如果想要把一整句歌词按歌词节奏一个一个地显示出来,就需要在文本中使用“`/`”将它们隔开。如果想要一个一个地覆盖掉之前显示的内容,就需要在文本中使用`回车键`将他们隔开。之后点击下面的“创建文本递进”,在工作区里面可以拖动右箭头事件来调整时机。如果你做好了又不想要了,那么再把下面的开启改成关闭即可,无需手动删除。如果你又反悔了,还是想要,再改成开启,又会恢复记录。
```rdview 8 3
ea.FloatingText 1 1;
ea.AdvanceText 2 1;
ea.AdvanceText 3 1;
ea.AdvanceText 4 1;
ea.AdvanceText 5 1;
ea.AdvanceText 6 1;
ea.AdvanceText 7 1;
FloatingText 1 1;
AdvanceText 2 1;
AdvanceText 3 1;
AdvanceText 4 1;
AdvanceText 5 1;
AdvanceText 6 1;
AdvanceText 7 1;
```
音节的消失方式选择**淡出**就是慢慢消失,**立即**就是瞬间消失,音节显示的时长是按创建文本递进的最后一个事件往后数拍子的。
```rdview 13 3
ea.FloatingText 1 1;
FloatingText 1 1;
g 3 1 4 1;
g 7 1 0 1 #f80;
ea.AdvanceText 3 1;
AdvanceText 3 1;
g 8 1 4 1;
g 12 1 0 1 #f80;
ea.AdvanceText 8 1;
AdvanceText 8 1;
```
## 更丰富的效果和技巧
@@ -147,6 +151,16 @@ ea.AdvanceText 8 1;
有没有发现不管是颜色效果还是显示效果,在设计上都和歌词本身有着些许联系,也就让平淡无奇的歌词字幕一下子变得生动起来。
## 文本装饰
你可能已经注意到,**浮动文字**事件并没有移动文字的功能。如果想要移动文字,请移步绿色板块,点击加号创建装饰,选择“文本”。
![创建文本装饰](../images/captions-textdecoration.png)
创建一个**设置文本**事件,你会发现这个事件和浮动文字基本一样,相信你已经知道怎么用了。
这里的文本比起用浮动文字事件创建的文本功能更加齐全,不仅可以**移动装饰**,还可以**显示/隐藏**、**重排序:装饰**,等等。总之和精灵图差不多(如果想了解精灵图请看[这里](../pages/decoration.md#制作一个精灵)),这里就不过多赘述了。
## 显示对话
在不少剧情关卡里面角色都会有聊天的过程,这在编辑器内也是可以轻松实现的,需要的是紫色版块中的**显示对话**功能。
+2
View File
@@ -28,6 +28,8 @@
如何加入类似于`1-2亲密`的`X`记号呢?在生成一个节奏之后,点击“**切换为X字拍子设置**”就可以调节。需要注意的是:你需要在一串普通拍子开始的**同时**或是**之前**就放置好修饰拍子设置。如果在普通拍子已经响起之后再添加`X`,虽然画面上能即时生效,但是节拍音效是没法立刻改变的。
如果你在单发类型的轨道上放置`切换为X字拍子设置`,那么时间轴会卡住无法播放。
> 在没有节奏的地方放置一连串的“切换为X字拍子设置”来改变`X`的排布也可以视为一种画面演出。
当然,这个功能也不仅仅是为轨道加`X`,按下鼠标左键是加入`X`记号,按下鼠标右键一次是`↑`箭头记号,两次是`↓`箭头记号。按下鼠标中键一次是摇摆心跳记号,两次是抖动心跳记号。
+1 -1
View File
@@ -20,7 +20,7 @@
![轨道的设置界面](../images/createbuiltincharacter-rowinfo.png)
**类型:**“**普通**”表示像武士一样的普通拍子节奏,“**单发**”表示像咖啡师一样的单发拍子节奏。需要注意的是,切换轨道类型的同时也会清空轨道上的所有节拍,因此建议在一开始就确定好轨道的类型。
**类型:**“**普通**”表示像武士一样的普通拍子节奏,“**单发**”表示像咖啡师一样的单发拍子节奏。虽然现在单发和普通拍子可以共存,但需要注意的是,切换轨道类型的同时也会清空轨道上的所有节拍,因此建议在一开始就确定好轨道的类型。
**玩家:**P1,P2表示玩家1和玩家2,CPU表示电脑演示。
+14
View File
@@ -93,6 +93,20 @@
## 快捷查询
<div class="custommethods-search">
<div class="cm-search">
<input class="cm-query" type="text" placeholder="输入自定义方法名,如 ChangeCharacter" autocomplete="off">
<ul class="cm-suggest"></ul>
</div>
<div class="cm-options"></div>
<div class="cm-selected" hidden>
<div class="cm-head">
<span class="cm-name"></span>
<span class="cm-short"></span>
</div>
<div class="cm-call"></div>
<code class="cm-preview"></code>
<div class="cm-desc"></div>
</div>
</div>
## 我知道你还想问什么
+15
View File
@@ -110,6 +110,21 @@
**时长与缓速:**同其他事件。
## 混合精灵图
可以将精灵图的颜色与场景混合。
混合模式将进行以下计算(超过255的按255算,小于0按0算,小数四舍五入):
假设背景色为RGB(r1,g1,b1)
精灵图颜色为RGB(r2.g2.b2)
**附加(add 线性减淡)**模式显示的颜色为RGB(r1+r2,g1+g2,b1+b2)
**叠加(mul 正片叠底)**模式显示的颜色为RGB(r1*r2/255.g1*g2/255.b1*b2/255)
**反转(inv 反色)**模式显示的颜色为RGB(rl-r2,b1-b2,g1-g2)
## 精灵的使用技巧
**1、作为场景物体**
+2
View File
@@ -86,6 +86,8 @@
**另存为**:保存为额外的一个文件。
**测试**:直接开始游玩关卡。
**导出以发布**:导出以发布:将谱面导出为 .rdzip 文件,或将谱面上传至创意工坊。
**退出**:退出编辑器,记得先保存。
+21 -21
View File
@@ -6,11 +6,11 @@
![](../images/holdbeat-lenght.png)
如果不对其添加任何其他设置,那么轨道就会自动变成 `—x—x—x————♥` 的模式,像`4-1`这关的矿工一样,也就是在1,3,5,7拍有按压的动画效果。当然你可以在“设置X标记”这个项目里面选择跳过节拍的形式,默认有三种情况,如果想使用“切换为修饰拍子设置”来自由决定跳过也是可以的,先在“设置X标记”里面选择NO,然后设置好自己决定的修饰拍子就行了。
如果不对其添加任何其他设置,那么轨道会延续上一个“切换为修饰拍子设置”的模式,当然你可以在“设置X标记”这个项目里面选择跳过节拍的形式,默认有三种情况,比如选`—x—x—x`轨道就会自动变成 `—x—x—x————♥` 的模式,像`4-1`这关的矿工一样,也就是在1,3,5,7拍有按压的动画效果。
```rdview 7 3
eb.AddClassicBeat 3 1[tick=3,hold=0.3,swing=0.5];
eb.SetRowXs 1 1[rowxs=---x--];
AddClassicBeat 3 1[tick=0.5,hold=0.3,swing=0.5];
SetRowXs 1 1[rowxs=---x--];
```
另外长按拍子在编辑器里面也可以分解为自由节拍,使用自由节拍时,需将脉冲出现与脉冲消失分别构成两个合法的普通拍,也即所有脉冲出现的相隔时长相同,且所有脉冲消失的相隔时长相同。
@@ -24,12 +24,12 @@ g 5 2 0 5 #f00{};
g 5.5 3 0 4 #d20{};
g 6 4 0 3 #b40{};
g 6.5 5 0 2 #960{};
eb.AddFreeTimeBeat 1 1[offset=0 1,hold=4];
eb.PulseFreeTimeBeat 2 1[offset=0 2,hold=3.5,pulse=3];
eb.PulseFreeTimeBeat 3 1[offset=0 3,hold=3,pulse=5];
eb.PulseFreeTimeBeat 4 1[offset=0 4,hold=2.5,pulse=7];
eb.AddClassicBeat 1 6[tick=3,swing=0.5];
eb.AddClassicBeat 5 6[tick=1.5,swing=0.25];
AddFreeTimeBeat 1 1[pulse=0,offset=0 1,hold=4];
PulseFreeTimeBeat 2 1[offset=0 2,hold=3.5,action=Custom,customPulse=2];
PulseFreeTimeBeat 3 1[offset=0 3,hold=3,action=Custom,customPulse=4];
PulseFreeTimeBeat 4 1[offset=0 4,hold=2.5,action=Custom,customPulse=6];
AddClassicBeat 1 6[tick=0.5,swing=2];
AddClassicBeat 5 6[tick=0.25,swing=2];
```
## 演示关卡
@@ -41,23 +41,23 @@ eb.AddClassicBeat 5 6[tick=1.5,swing=0.25];
**1、**在长按拍松手的时刻可以有其他的按拍。这在曾经的编辑器版本里是不允许的,目前官方修正了这一机制。
```rdview 9 4
eb.AddClassicBeat 1 1[tick=6,hold=1];
eb.AddClassicBeat 2 2[tick=6];
AddClassicBeat 1 1[tick=1,hold=1];
AddClassicBeat 2 2[tick=1];
```
**2、**当同时存在多个长按拍子时,这些长按拍的长按时间段必须完全一致。也就是说,长按时间段之间不能是包含、交叉或首尾相连的关系。
```rdview 16 7
eb.AddClassicBeat 1 1[tick=3,hold=1,swing=0.5];
eb.AddClassicBeat 1 2[tick=3,hold=0.5,swing=0.5];
eb.AddClassicBeat 6 1[tick=3,hold=1,swing=0.5];
eb.AddClassicBeat 6.25 2[tick=3,hold=0.5,swing=0.5];
eb.AddClassicBeat 11 1[tick=3,hold=1,swing=0.5];
eb.AddClassicBeat 11.5 2[tick=3,hold=1,swing=0.5];
eb.AddClassicBeat 1.5 4[tick=1.5,hold=1,swing=0.25];
eb.AddClassicBeat 1 5[tick=3,hold=0.5,swing=0.5];
eb.AddClassicBeat 6 4[tick=3,hold=1,swing=0.5];
eb.AddClassicBeat 6.5 5[tick=3,hold=0.5,swing=0.5];
AddClassicBeat 1 1[tick=0.5,hold=1,swing=0.5];
AddClassicBeat 1 2[tick=0.5,hold=0.5,swing=0.5];
AddClassicBeat 6 1[tick=0.5,hold=1,swing=0.5];
AddClassicBeat 6.25 2[tick=0.5,hold=0.5,swing=0.5];
AddClassicBeat 11 1[tick=0.5,hold=1,swing=0.5];
AddClassicBeat 11.5 2[tick=0.5,hold=1,swing=0.5];
AddClassicBeat 1.5 4[tick=0.5,hold=1,swing=0.25];
AddClassicBeat 1 5[tick=0.5,hold=0.5,swing=0.5];
AddClassicBeat 6 4[tick=0.5,hold=1,swing=0.5];
AddClassicBeat 6.5 5[tick=0.5,hold=0.5,swing=0.5];
```
**3、**凹兔的自动演示对于长按拍的处理十分诡异,可能会提前松掉长按拍子,但不必惊慌,这不是你的问题。
+3 -3
View File
@@ -45,9 +45,9 @@
接下来在左下角点击紫色星星图标进入紫色的版块,在能看到波形结束的地方点一下,然后往下翻到**结束关卡**按钮,这样的按钮一共必须放三个。
```rdview 5 5
ea.FinishLevel 1 1;
ea.FinishLevel 2 2;
ea.FinishLevel 3 3;
FinishLevel 1 1;
FinishLevel 2 2;
FinishLevel 3 3;
```
最后我们回到红色的版块,点击**关卡设置**,选择上方的导出,有几个空栏是必须要填写的:
+78 -75
View File
@@ -7,13 +7,13 @@
我们把一个普通的普通拍子分解为自由时长节拍后,可以看到它变成了一个大的`1`和六个小的`>` 。
```rdview 8 2
eb.AddFreeTimeBeat 1 0[pulse=1];
eb.PulseFreeTimeBeat 2 0[pulse=9];
eb.PulseFreeTimeBeat 3 0[pulse=9];
eb.PulseFreeTimeBeat 4 0[pulse=9];
eb.PulseFreeTimeBeat 5 0[pulse=9];
eb.PulseFreeTimeBeat 6 0[pulse=9];
eb.PulseFreeTimeBeat 7 0[pulse=9];
AddFreeTimeBeat 1 0[pulse=0];
PulseFreeTimeBeat 2 0[action=Increment];
PulseFreeTimeBeat 3 0[action=Increment];
PulseFreeTimeBeat 4 0[action=Increment];
PulseFreeTimeBeat 5 0[action=Increment];
PulseFreeTimeBeat 6 0[action=Increment];
PulseFreeTimeBeat 7 0[action=Increment];
```
我们选中数字1对应的节拍,发现这个是设置起始脉冲位置的。你甚至可以将它改为第七拍,然后把六个小的`>`删掉,这样就没有任何节拍提示。如果你设计的关卡是一个和节奏医生没什么关联的创意谱(梦幻联动?),且按键密度很大,则可以考虑这样使用。相比于工作区里面重叠摆放一大堆普通普通拍子,这样会看上去更清爽。但如果你设计的就是节奏医生,**请不要在完全无节奏提示的情况下直接响起第七拍**。
@@ -23,20 +23,20 @@ eb.PulseFreeTimeBeat 7 0[pulse=9];
剩下的大于号`>`即意味着心跳向右移动一格,数到7的时候则需要按拍。点选之后,把它改成 `<`,心跳还能向左移动一格;点击自定义,还可以任意指定到第几个节拍。每一组自由拍**必须使用较大的节拍作为开头**,较小的则数量不限,直到数到第七拍为止。
```rdview 10 2
eb.AddFreeTimeBeat 1 0[pulse=1];
eb.PulseFreeTimeBeat 2 0[pulse=9];
eb.PulseFreeTimeBeat 2.75 0[pulse=9];
eb.PulseFreeTimeBeat 3.5 0[pulse=9];
eb.PulseFreeTimeBeat 4 0[pulse=9];
eb.PulseFreeTimeBeat 4.5 0[pulse=9];
eb.PulseFreeTimeBeat 5 0[pulse=9];
eb.AddFreeTimeBeat 5 0[pulse=1];
eb.PulseFreeTimeBeat 6 0[pulse=9];
eb.PulseFreeTimeBeat 6.75 0[pulse=9];
eb.PulseFreeTimeBeat 7.5 0[pulse=9];
eb.PulseFreeTimeBeat 8 0[pulse=9];
eb.PulseFreeTimeBeat 8.5 0[pulse=9];
eb.PulseFreeTimeBeat 9 0[pulse=9];
AddFreeTimeBeat 1 0[pulse=0];
PulseFreeTimeBeat 2 0[action=Increment];
PulseFreeTimeBeat 2.75 0[action=Increment];
PulseFreeTimeBeat 3.5 0[action=Increment];
PulseFreeTimeBeat 4 0[action=Increment];
PulseFreeTimeBeat 4.5 0[action=Increment];
PulseFreeTimeBeat 5 0[action=Increment];
AddFreeTimeBeat 5 0[pulse=0];
PulseFreeTimeBeat 6 0[action=Increment];
PulseFreeTimeBeat 6.75 0[action=Increment];
PulseFreeTimeBeat 7.5 0[action=Increment];
PulseFreeTimeBeat 8 0[action=Increment];
PulseFreeTimeBeat 8.5 0[action=Increment];
PulseFreeTimeBeat 9 0[action=Increment];
```
如果你想让一串自由节拍最后不需要按拍,而是节奏直接消失,就把这一组自由节拍的最后一个点选**垃圾桶**符号。
@@ -49,33 +49,36 @@ eb.PulseFreeTimeBeat 9 0[pulse=9];
## 切分音(Syncopation)
切分音的概念不难理解,拍子的数量仍然为七个,但是最后一个拍子响起的时间是在6.5拍的时候,即提早了半拍。这少的半拍时间是可以发生在任意相邻两拍之间的。早期版本的编辑器需要使用自由拍来制作切分音,现在在**切换为X字拍子设置**里,点亮激活拍子下方的`->>>>>-`符号,就会将这一拍的时间缩短一半。每个轨道最多只能设置一个切分音位置。
切分音的概念不难理解,拍子的数量仍然为七个,但是最后一个拍子响起的时间是在6.5拍的时候,即提早了半拍。这少的半拍时间是可以发生在任意相邻两拍之间的。早期版本的编辑器需要使用自由拍来制作切分音,现在在**切换为X字拍子设置**里,点亮激活拍子下方的`> > >`符号,就会将这一拍的时间缩短一半并自动添加提示音。每个轨道最多只能设置一个切分音位置。
![](../images/moreclassicbeats-syn.png)
**如何提示**
添加完后会发现多了几个选项,各个选项效果如下:
> 1. 将轨道边缘染成黄色(参考颜色: `#FFFF00FF` )
> 2. 在切分发生前的奇数拍使用“切分音嘟声-低”音效
> 3. 在切分发生后的第一拍使用“切分音嘟声-中”音效
> 4. 在第7拍使用“切分音嘟声-高”音效
**摇摆**:设置切分后的按拍点距离原来的提前了(拍长*摇摆)拍。
需要额外注意的是,无论拍子之中是否有`X`,切分的第一个提示音、最后一个提示音以及可能存在的切分中音效都要保留,只有剩下的切分低音效是可以自行选择放置与否的。
**音量&音高**:设置提示音的音量和音高。
**样式**:设置提示音的样式,鸣声是剧情模式里用到的,嘟声则是社区创造的,用哪种都可以。
**修饰拍子出现/消失音效**:只在使用鸣声样式才会有,用来提示切分音的出现和消失,嘟声则不需要。
需要额外注意的是,无论拍子之中是否有`X`,切分出现/消失和切分后第一拍的音效必须保留,只有剩下的音效是可以自行选择放置与否的。
## 八拍子(8 Beats)
八拍子就更好理解了,我们直接额外增加一个节拍,让按拍的时机在第八拍响起的时候。但是轨道上是只能显示七个拍子的,所以我们依然要依赖**分解成自由拍**,以`2 1 2 3 4 5 6 7`的形式响起节拍。设置的方法如下图所示:
```rdview 10 3
eb.AddClassicBeat 1 0[tick=6];
eb.AddFreeTimeBeat 1 1[pulse=2];
eb.PulseFreeTimeBeat 2 1[pulse=8];
eb.PulseFreeTimeBeat 3 1[pulse=9];
eb.PulseFreeTimeBeat 4 1[pulse=9];
eb.PulseFreeTimeBeat 5 1[pulse=9];
eb.PulseFreeTimeBeat 6 1[pulse=9];
eb.PulseFreeTimeBeat 7 1[pulse=9];
eb.PulseFreeTimeBeat 8 1[pulse=9];
AddClassicBeat 1 0[tick=1];
AddFreeTimeBeat 1 1[pulse=1];
PulseFreeTimeBeat 2 1[action=Decrement];
PulseFreeTimeBeat 3 1[action=Increment];
PulseFreeTimeBeat 4 1[action=Increment];
PulseFreeTimeBeat 5 1[action=Increment];
PulseFreeTimeBeat 6 1[action=Increment];
PulseFreeTimeBeat 7 1[action=Increment];
PulseFreeTimeBeat 8 1[action=Increment];
```
**如何提示**
@@ -83,20 +86,20 @@ eb.PulseFreeTimeBeat 8 1[pulse=9];
> 1. 将轨道边缘染成红色(参考颜色: `#FF0000FF` )
> 2. 在第1拍使用 `beep_mid.ogg` 自定义音效
不要使用内置的 `sndBeepMid` 音效!
也可以使用内置的 `sndBeepMid` 音效。
八拍子也可以是摇摆拍,只需要将所有偶数拍选择并拖动即可变成摇摆拍:
```rdview 10 3
eb.AddClassicBeat 1 0[tick=6,swing=0.5];
eb.AddFreeTimeBeat 1 1[pulse=2];
eb.PulseFreeTimeBeat 2.5 1[pulse=8];
eb.PulseFreeTimeBeat 3 1[pulse=9];
eb.PulseFreeTimeBeat 4.5 1[pulse=9];
eb.PulseFreeTimeBeat 5 1[pulse=9];
eb.PulseFreeTimeBeat 6.5 1[pulse=9];
eb.PulseFreeTimeBeat 7 1[pulse=9];
eb.PulseFreeTimeBeat 8.5 1[pulse=9];
AddClassicBeat 1 0[tick=1,swing=0.5];
AddFreeTimeBeat 1 1[pulse=1];
PulseFreeTimeBeat 2.5 1[action=Decrement];
PulseFreeTimeBeat 3 1[action=Increment];
PulseFreeTimeBeat 4.5 1[action=Increment];
PulseFreeTimeBeat 5 1[action=Increment];
PulseFreeTimeBeat 6.5 1[action=Increment];
PulseFreeTimeBeat 7 1[action=Increment];
PulseFreeTimeBeat 8.5 1[action=Increment];
```
在八拍子中,**激活拍子**与八拍子的对应关系如下图所示。不要给第1拍和第3拍设置X拍子,这样做它就相当于变回普通拍了。
@@ -115,33 +118,33 @@ eb.PulseFreeTimeBeat 8.5 1[pulse=9];
整数拍和附点拍是交错排列的,即`0.25<->0.375<->0.5<->0.75<->1<->1.5<->2<->3<->4`。附点变速拍可以将整拍长变成附点拍长,也可以反过来,但只能变化到排列里它**相邻**的拍长上。
```rdview 9 3
eb.AddFreeTimeBeat 1 0;
eb.PulseFreeTimeBeat 2 0[pulse=9];
eb.PulseFreeTimeBeat 3 0[pulse=9];
eb.PulseFreeTimeBeat 4 0[pulse=9];
AddFreeTimeBeat 1 0;
PulseFreeTimeBeat 2 0[action=Increment];
PulseFreeTimeBeat 3 0[action=Increment];
PulseFreeTimeBeat 4 0[action=Increment];
g 4 0 0.5 1 #08f[offset=0 0 #08f0]{
eb.PulseFreeTimeBeat 1 0[pulse=9];
PulseFreeTimeBeat 1 0[action=Increment];
g 1 0 0.5 1 #000[offset=0 0 #0000]{
g 0 0 0 0 #0000[offset=0.25]{
eb.PulseFreeTimeBeat 0.75 0[pulse=9,offset=0.25];
PulseFreeTimeBeat 0.75 0[action=Increment,offset=0.25];
g 0.75 0 0.5 1 #0000[offset=0.25]{
eb.PulseFreeTimeBeat 0.75 0[pulse=9];
PulseFreeTimeBeat 0.75 0[action=Increment];
g -1.75 0 3 1 #0000{};
};
};
};
};
eb.AddFreeTimeBeat 1 1;
eb.PulseFreeTimeBeat 2 1[pulse=9];
eb.PulseFreeTimeBeat 3 1[pulse=9];
eb.PulseFreeTimeBeat 4 1[pulse=9];
AddFreeTimeBeat 1 1;
PulseFreeTimeBeat 2 1[action=Increment];
PulseFreeTimeBeat 3 1[action=Increment];
PulseFreeTimeBeat 4 1[action=Increment];
g 4 1 0.5 1 #80f[offset=0 0 #08f0]{
eb.PulseFreeTimeBeat 1 0[pulse=9];
PulseFreeTimeBeat 1 0[action=Increment];
g 1 0 0.5 1 #000[offset=0 0 #0000]{
g 0 0 0 0 #0000[offset=-0.5]{
eb.PulseFreeTimeBeat 1.5 0[pulse=9];
PulseFreeTimeBeat 1.5 0[action=Increment];
g 1.5 0 0.5 1 #0000[offset=-0.5]{
eb.PulseFreeTimeBeat 1.5 0[pulse=9];
PulseFreeTimeBeat 1.5 0[action=Increment];
g -2.5 0 4.5 1 #0000{};
};
};
@@ -168,33 +171,33 @@ g 4 1 0.5 1 #80f[offset=0 0 #08f0]{
变速普通拍的另一种形式是倍速变速拍,变速是拍长翻倍或减半,注意与附点变速拍区分。
```rdview 11 3
eb.AddFreeTimeBeat 1 0;
eb.PulseFreeTimeBeat 2 0[pulse=9];
eb.PulseFreeTimeBeat 3 0[pulse=9];
eb.PulseFreeTimeBeat 4 0[pulse=9];
AddFreeTimeBeat 1 0;
PulseFreeTimeBeat 2 0[action=Increment];
PulseFreeTimeBeat 3 0[action=Increment];
PulseFreeTimeBeat 4 0[action=Increment];
g 4 0 0.5 1 #08f[offset=0 0 #08f0]{
eb.PulseFreeTimeBeat 1 0[pulse=9];
PulseFreeTimeBeat 1 0[action=Increment];
g 1 0 0.5 1 #000[offset=0 0 #0000]{
g 0 0 0 0 #0000[offset=0.5]{
eb.PulseFreeTimeBeat 0.5 0[pulse=9];
PulseFreeTimeBeat 0.5 0[action=Increment];
g 0.5 0 0.5 1 #0000[offset=0.5]{
eb.PulseFreeTimeBeat 0.5 0[pulse=9];
PulseFreeTimeBeat 0.5 0[action=Increment];
g -1.75 0 3 1 #0000{};
};
};
};
};
eb.AddFreeTimeBeat 1 1;
eb.PulseFreeTimeBeat 2 1[pulse=9];
eb.PulseFreeTimeBeat 3 1[pulse=9];
eb.PulseFreeTimeBeat 4 1[pulse=9];
AddFreeTimeBeat 1 1;
PulseFreeTimeBeat 2 1[action=Increment];
PulseFreeTimeBeat 3 1[action=Increment];
PulseFreeTimeBeat 4 1[action=Increment];
g 4 1 0.5 1 #08f[offset=0 0 #08f0]{
eb.PulseFreeTimeBeat 1 0[pulse=9];
PulseFreeTimeBeat 1 0[action=Increment];
g 1 0 0.5 1 #000[offset=0 0 #0000]{
g 0 0 0 0 #0000[offset=-1]{
eb.PulseFreeTimeBeat 2 0[pulse=9];
PulseFreeTimeBeat 2 0[action=Increment];
g 2 0 0.5 1 #0000[offset=-1]{
eb.PulseFreeTimeBeat 2 0[pulse=9];
PulseFreeTimeBeat 2 0[action=Increment];
g -2.5 0 4.5 1 #0000{};
};
};
+119 -119
View File
@@ -7,10 +7,10 @@
我们前面说过,护士提示的 Rea 和 Get 对应脉冲,Dy 和 Set 对应按拍,Go 代表单发拍开始,这种规则当然不仅仅适用于普通单发拍。先来看一个简单的摇摆单发拍:
```rdview 11 3
es.SayReadyGetSetGo_Get 1[];
es.SayReadyGetSetGo_Set 2.5[];
es.SayReadyGetSetGo_Go 5[];
eb.AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4];
SayReadyGetSetGo 1[phraseToSay=JustSayGet];
SayReadyGetSetGo 2.5[phraseToSay=JustSaySet];
SayReadyGetSetGo 5[phraseToSay=JustSayGo];
AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4];
```
可以看到,虽然 Get-Set 与 Set-Go 之间的间隔不同,但依然保持了上述的对应关系。实际上,这就是`4-4`使用的单发拍。
@@ -18,31 +18,31 @@ eb.AddOneshotBeat 5 1[loop=1,tick=1.5,interval=4];
在`2-3`中也出现了摇摆单发拍,可以试着自己理解一下这关的单发拍提示:
```rdview 10 2
eb.AddOneshotBeat 1[];
eb.AddOneshotBeat 3[];
eb.AddOneshotBeat 5[tick=0.75];
eb.AddOneshotBeat 6[tick=0.75];
eb.AddOneshotBeat 7[tick=0.75];
eb.AddOneshotBeat 8[tick=0.75];
AddOneshotBeat 1[];
AddOneshotBeat 3[];
AddOneshotBeat 5[tick=0.75];
AddOneshotBeat 6[tick=0.75];
AddOneshotBeat 7[tick=0.75];
AddOneshotBeat 8[tick=0.75];
```
如果我们在此基础上更进一步,会发生什么呢?来看看`4-4N`中出现过的这段单发拍:
```rdview 12 4
g 1 1 4 2[offset=4]{
es.SayReadyGetSetGo_Rea 0 0[];
es.SayReadyGetSetGo_Dy 0.75 1[];
es.SayReadyGetSetGo_Get 1.5 0[];
es.SayReadyGetSetGo_Set 2.25 1[];
es.SayReadyGetSetGo_Go 4 0[];
SayReadyGetSetGo 0 0[phraseToSay=JustSayRea];
SayReadyGetSetGo 0.75 1[phraseToSay=JustSayDy];
SayReadyGetSetGo 1.5 0[phraseToSay=JustSayGet];
SayReadyGetSetGo 2.25 1[phraseToSay=JustSaySet];
SayReadyGetSetGo 4 0[phraseToSay=JustSayGo];
g 0 -1 0.75 3 #0f8[]{};
g 1.5 -1 0.75 3 #0f8[]{};
g 4 -1 0 3 #0f8[]{};
};
eb.AddOneshotBeat 5[tick=0.75];
eb.AddOneshotBeat 6.5[tick=0.75];
eb.AddOneshotBeat 9[tick=0.75];
eb.AddOneshotBeat 10.5[tick=0.75];
AddOneshotBeat 5[tick=0.75];
AddOneshotBeat 6.5[tick=0.75];
AddOneshotBeat 9[tick=0.75];
AddOneshotBeat 10.5[tick=0.75];
```
可以看到,图中的单发拍已不再局限于一个单发拍不断重复了,而是变成了若干个单发拍构成的单发拍组的不断重复,但前面讲过的与护士提示之间的对应关系仍保持不变。由此我们可以得到单发拍的核心规则:
@@ -53,24 +53,24 @@ eb.AddOneshotBeat 10.5[tick=0.75];
```rdview 13 4
g 1 1 4 2[offset=4]{
es.SayReadyGetSetGo_Rea 0 0[];
es.SayReadyGetSetGo_Dy 1 0[];
es.SayReadyGetSetGo_Rea 2 0[];
es.SayReadyGetSetGo_Dy 2.5 1[];
es.SayReadyGetSetGo_Get 3 0[];
es.SayReadyGetSetGo_Set 3.5 1[];
es.SayReadyGetSetGo_Go 4 0[];
SayReadyGetSetGo 0 0[phraseToSay=JustSayRea];
SayReadyGetSetGo 1 0[phraseToSay=JustSayDy];
SayReadyGetSetGo 2 0[phraseToSay=JustSayRea];
SayReadyGetSetGo 2.5 1[phraseToSay=JustSayDy];
SayReadyGetSetGo 3 0[phraseToSay=JustSayGet];
SayReadyGetSetGo 3.5 1[phraseToSay=JustSaySet];
SayReadyGetSetGo 4 0[phraseToSay=JustSayGo];
g 0 -1 1 2 #0f8[]{};
g 2 -1 0.5 3 #0f8[]{};
g 3 -1 0.5 3 #0f8[]{};
g 4 -1 0 3 #0f8[]{};
};
eb.AddOneshotBeat 5[];
eb.AddOneshotBeat 7[tick=0.5];
eb.AddOneshotBeat 8[tick=0.5];
eb.AddOneshotBeat 9[];
eb.AddOneshotBeat 11[tick=0.5];
eb.AddOneshotBeat 12[tick=0.5];
AddOneshotBeat 5[];
AddOneshotBeat 7[tick=0.5];
AddOneshotBeat 8[tick=0.5];
AddOneshotBeat 9[];
AddOneshotBeat 11[tick=0.5];
AddOneshotBeat 12[tick=0.5];
```
当你不确定一段单发拍提示是否正确时,可以把这段单发拍提示向后复制几份,首尾相连,如果发现所有的 Dy 和 Set 均对齐了按拍时机,就说明单发拍提示是正确的。
@@ -84,13 +84,13 @@ eb.AddOneshotBeat 12[tick=0.5];
以下单发拍均是合法的:
```rdview 9 2
eb.AddOneshotBeat 1[];
eb.AddOneshotBeat 3[];
eb.AddOneshotBeat 3.5[tick=0.5];
eb.AddOneshotBeat 4.75[tick=1.25];
eb.AddOneshotBeat 5[];
eb.AddOneshotBeat 5.75[tick=0.25];
eb.AddOneshotBeat 7.5[tick=0.5];
AddOneshotBeat 1[];
AddOneshotBeat 3[];
AddOneshotBeat 3.5[tick=0.5];
AddOneshotBeat 4.75[tick=1.25];
AddOneshotBeat 5[];
AddOneshotBeat 5.75[tick=0.25];
AddOneshotBeat 7.5[tick=0.5];
```
![](../images/moreoneshotbeats-farmer.gif)
@@ -103,31 +103,31 @@ eb.AddOneshotBeat 7.5[tick=0.5];
举个例子:Rea-Dy-Get-Set-Go 与 Rea-Dy--GetSet-Go,尽管两者的按拍时机相同,但不同的 Rea、Get 位置会给人带来不同的游玩体验。
```rdview 12 6
eb.AddOneshotBeat 5[];
eb.AddOneshotBeat 7.5[tick=0.5];
eb.AddOneshotBeat 9[];
eb.AddOneshotBeat 11.5[tick=0.5];
AddOneshotBeat 5[];
AddOneshotBeat 7.5[tick=0.5];
AddOneshotBeat 9[];
AddOneshotBeat 11.5[tick=0.5];
g 1 1 4 1 [offset=4]{
es.SayReadyGetSetGo_Rea 0[];
es.SayReadyGetSetGo_Dy 1[];
es.SayReadyGetSetGo_Get 2.5[];
es.SayReadyGetSetGo_Set 3[];
es.SayReadyGetSetGo_Go 4[];
SayReadyGetSetGo 0[phraseToSay=JustSayRea];
SayReadyGetSetGo 1[phraseToSay=JustSayDy];
SayReadyGetSetGo 2.5[phraseToSay=JustSayGet];
SayReadyGetSetGo 3[phraseToSay=JustSaySet];
SayReadyGetSetGo 4[phraseToSay=JustSayGo];
g 0 -1 1 2 #0f8[]{};
g 2.5 -1 0.5 2 #0f8[]{};
g 4 -1 0 2 #0f8[]{};
};
eb.AddOneshotBeat 5 3[];
eb.AddOneshotBeat 7 3[];
eb.AddOneshotBeat 9 3[];
eb.AddOneshotBeat 11 3[];
AddOneshotBeat 5 3[];
AddOneshotBeat 7 3[];
AddOneshotBeat 9 3[];
AddOneshotBeat 11 3[];
g 1 4 4 1 [offset=4]{
es.SayReadyGetSetGo_Rea 0[];
es.SayReadyGetSetGo_Dy 1[];
es.SayReadyGetSetGo_Get 2[];
es.SayReadyGetSetGo_Set 3[];
es.SayReadyGetSetGo_Go 4[];
SayReadyGetSetGo 0[phraseToSay=JustSayRea];
SayReadyGetSetGo 1[phraseToSay=JustSayDy];
SayReadyGetSetGo 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 3[phraseToSay=JustSaySet];
SayReadyGetSetGo 4[phraseToSay=JustSayGo];
g 0 -1 1 2 #0f8[]{};
g 2 -1 1 2 #0f8[]{};
g 4 -1 0 2 #0f8[]{};
@@ -143,13 +143,13 @@ g 1 4 4 1 [offset=4]{
需要注意的是,跳过单发拍会跳过下一次**击拍**,而不是下一个循环节。
```rdview 7 4
eb.AddOneshotBeat 1[type=skipshot];
eb.AddOneshotBeat 5[];
eb.AddOneshotBeat 1 2[type=skipshot];
eb.AddOneshotBeat 3 2[tick=3];
eb.AddOneshotBeat 3.75 2[tick=2.25];
eb.AddOneshotBeat 4.5 2[tick=1.5];
eb.AddOneshotBeat 5.25 2[tick=0.75];
AddOneshotBeat 1[type=skipshot];
AddOneshotBeat 5[];
AddOneshotBeat 1 2[type=skipshot];
AddOneshotBeat 3 2[tick=3];
AddOneshotBeat 3.75 2[tick=2.25];
AddOneshotBeat 4.5 2[tick=1.5];
AddOneshotBeat 5.25 2[tick=0.75];
```
另外,在剧情关卡中,跳过拍的位置如果按下空格会计算成失误,但编辑器中默认是不会失误的。
@@ -167,10 +167,10 @@ eb.AddOneshotBeat 5.25 2[tick=0.75];
除此之外,方块单发拍也需要护士提示。这种护士提示与普通单发拍的提示类似,唯一的不同点在于最后的 Go 被替换为了 One。
```rdview 5 3
eb.AddOneshotBeat 3[];
es.SayReadyGetSetGo_Get 1 1[];
es.SayReadyGetSetGo_Set 2 1[];
es.SayReadyGetSetGo_Go 3 1[];
AddOneshotBeat 3[];
SayReadyGetSetGo 1 1[phraseToSay=JustSayGet];
SayReadyGetSetGo 2 1[phraseToSay=JustSaySet];
SayReadyGetSetGo 3 1[];
```
如果相同拍长的方块单发拍连续出现了多次,无需为每次出现添加完整的护士提示。可以逐步减少护士提示,直到完全没有护士提示,只剩下特殊提示音。
@@ -186,32 +186,32 @@ es.SayReadyGetSetGo_Go 3 1[];
![](../images/moreoneshotbeats-amountofsubdivision.png)
```rdview 4 5
eb.AddOneshotBeat 1[type=subdivision,subdivision=2];
eb.AddOneshotBeat 1 1[type=subdivision,subdivision=3];
eb.AddOneshotBeat 1 2[type=subdivision,subdivision=4];
eb.AddOneshotBeat 1 3[type=subdivision,subdivision=5];
AddOneshotBeat 1[type=subdivision,subdivision=2];
AddOneshotBeat 1 1[type=subdivision,subdivision=3];
AddOneshotBeat 1 2[type=subdivision,subdivision=4];
AddOneshotBeat 1 3[type=subdivision,subdivision=5];
```
在护士提示方面,不同于方块单发拍,细分拍需要在出拍前的半拍长开始数拍提示。
```rdview 13
eb.AddOneshotBeat 1[type=subdivision,subdivision=2];
AddOneshotBeat 1[type=subdivision,subdivision=2];
g 1 1 0.5 1 #0f0[]{};
g 1 1 1 1 #0f0[]{};
g 0.5 1 0.5 1[offset=0.5]{
es.SayReadyGetSetGo_2 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count2];
};
eb.AddOneshotBeat 4[tick=2,type=subdivision,subdivision=2,interval=4];
AddOneshotBeat 4[tick=2,type=subdivision,subdivision=2,interval=4];
g 4 1 1 1 #0f0[]{};
g 4 1 2 1 #0f0[]{};
g 3 1 1 1[offset=1]{
es.SayReadyGetSetGo_2 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count2];
};
eb.AddOneshotBeat 9[tick=2,type=subdivision,subdivision=3,interval=4];
AddOneshotBeat 9[tick=2,type=subdivision,subdivision=3,interval=4];
g 9 1 1 1 #0f0[]{};
g 9 1 2 1 #0f0[]{};
g 8 1 1 1[offset=1]{
es.SayReadyGetSetGo_3 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count3];
};
```
@@ -228,14 +228,14 @@ g 8 1 1 1[offset=1]{
单独细分的提示**与正常单发拍的提示类似**,Rea Dy 或 Get Set 之间的间隔同样用于提示细分拍的拍长,且数字提示**和 Go! 提示一样不需要提前**。
```rdview 14 3
eb.AddOneshotBeat 3[type=subdivision,subdivision=2];
es.SayReadyGetSetGo_Get 1 1[];
es.SayReadyGetSetGo_Set 2 1[];
es.SayReadyGetSetGo_2 3 1[];
eb.AddOneshotBeat 9[tick=2,type=subdivision,subdivision=4,interval=4];
es.SayReadyGetSetGo_Get 5 1[];
es.SayReadyGetSetGo_Set 7 1[];
es.SayReadyGetSetGo_4 9 1[];
AddOneshotBeat 3[type=subdivision,subdivision=2];
SayReadyGetSetGo 1 1[phraseToSay=JustSayGet];
SayReadyGetSetGo 2 1[phraseToSay=JustSaySet];
SayReadyGetSetGo 3 1[phraseToSay=Count2];
AddOneshotBeat 9[tick=2,type=subdivision,subdivision=4,interval=4];
SayReadyGetSetGo 5 1[phraseToSay=JustSayGet];
SayReadyGetSetGo 7 1[phraseToSay=JustSaySet];
SayReadyGetSetGo 9 1[phraseToSay=Count4];
```
## 冻结拍🧊
@@ -249,14 +249,14 @@ es.SayReadyGetSetGo_4 9 1[];
![](../images/moreoneshotbeats-freezeshot.png)
```rdview 8 2
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
eb.AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2,tick=2];
AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2,tick=2];
```
对于一个冻结拍而言,一共有五个关键的时间点:左十字,右十字,出拍点(绿线),原按拍点(蓝线),实际按拍点(黄线)。
```rdview 4 2
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
```
- **左十字**和**右十字**会显示两次冰冻特效并分别播放不同的冰冻音效,用于预示拍延长的时间。
@@ -274,20 +274,20 @@ eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
- **左十字**必须和上一个普通单发拍的按拍位置重合。
```rdview 5 3
eb.AddOneshotBeat 1[];
eb.AddOneshotBeat 3[type=freezeshot,delay=0.5,interval=2];
eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
AddOneshotBeat 1[];
AddOneshotBeat 3[type=freezeshot,delay=0.5,interval=2];
AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
```
如果需要在冻结拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
```rdview 5 3
eb.AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
AddOneshotBeat 2[type=freezeshot,delay=0.5,interval=2];
g 2 -1 2 2 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 1[];
AddOneshotBeat 0.5 1[];
g 0 -2 2 3 #0000[offset=0 1]{
eb.AddOneshotBeat -1 3[tick=2.5];
AddOneshotBeat -1 3[tick=2.5];
};
};
```
@@ -305,7 +305,7 @@ g 2 -1 2 2 #0000[offset=0 1]{
类似地,对于一个灼热拍,一共有四个关键的时间点:左十字,右十字,出拍点(绿线),按拍点。为作区分且方便表述,将会把按拍点称为红线。
```rdview 5 2
eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
```
- **左十字**和**右十字**会显示两次热浪特效并分别播放不同的灼热音效,用于预示拍实际的时间。
@@ -320,20 +320,20 @@ eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
- **左十字**必须和上一个普通单发拍的原定出拍位置重合。
```rdview 5 3
eb.AddOneshotBeat 1[];
eb.AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
eb.AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
eb.AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
AddOneshotBeat 1[];
AddOneshotBeat 3[tick=0.5,type=burnshot,interval=2];
AddOneshotBeat 1 1[grayscale=100,offset_grayscale=80];
AddOneshotBeat 3 1[grayscale=100,offset_grayscale=80];
```
如果需要在灼热拍上叠加多个拍子来凸显鼓点,可以将他们的按拍点放在同一位置。
```rdview 5 3
eb.AddOneshotBeat 2[type=burnshot,delay=0.5,interval=2];
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];
AddOneshotBeat 1.5 1[tick=1.5];
g 0 -1 3 3 #0000[offset=0 1]{
eb.AddOneshotBeat 0.5 2[tick=2.5];
AddOneshotBeat 0.5 2[tick=2.5];
};
};
```
@@ -387,32 +387,32 @@ g 0 -1 3 2 #0000[offset=0 1]{
最后我们针对细分拍、冻结拍、灼热拍做一个技术总结:
```rdview 19 7
eb.AddOneshotBeat 1[loop=1];
eb.AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2];
eb.AddOneshotBeat 1 1[loop=1];
eb.AddOneshotBeat 5 1[];
eb.AddOneshotBeat 1 2[loop=1];
eb.AddOneshotBeat 5 2[tick=0.5,type=burnshot,interval=2];
eb.AddOneshotBeat 1 4[loop=1];
eb.AddOneshotBeat 5 4[tick=1,type=subdivision,subdivision=2,interval=2];
eb.AddOneshotBeat 7 4[tick=0.5,loop=1,interval=1];
eb.AddOneshotBeat 9 4[tick=0.5,type=subdivision,subdivision=2,interval=1];
eb.AddOneshotBeat 10 4[tick=2];
eb.AddOneshotBeat 14 4[tick=2,type=subdivision,subdivision=3,interval=4];
AddOneshotBeat 1[loop=1];
AddOneshotBeat 5[type=freezeshot,delay=0.5,interval=2];
AddOneshotBeat 1 1[loop=1];
AddOneshotBeat 5 1[];
AddOneshotBeat 1 2[loop=1];
AddOneshotBeat 5 2[tick=0.5,type=burnshot,interval=2];
AddOneshotBeat 1 4[loop=1];
AddOneshotBeat 5 4[tick=1,type=subdivision,subdivision=2,interval=2];
AddOneshotBeat 7 4[tick=0.5,loop=1,interval=1];
AddOneshotBeat 9 4[tick=0.5,type=subdivision,subdivision=2,interval=1];
AddOneshotBeat 10 4[tick=2];
AddOneshotBeat 14 4[tick=2,type=subdivision,subdivision=3,interval=4];
g 5 5 0.5 1 #0f0[]{};
g 5 5 1 1 #0f0[]{};
g 4.5 5 0.5 1[offset=0.5]{
es.SayReadyGetSetGo_2 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count2];
};
g 9 5 0.25 1 #0f0[]{};
g 9 5 0.5 1 #0f0[]{};
g 8.75 5 0.25 1[offset=0.25]{
es.SayReadyGetSetGo_2 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count2];
};
g 14 5 1 1 #0f0[]{};
g 14 5 2 1 #0f0[]{};
g 13 5 1 1[offset=1]{
es.SayReadyGetSetGo_2 0 1[];
SayReadyGetSetGo 0 1[phraseToSay=Count2];
};
```
+3 -1
View File
@@ -12,6 +12,8 @@
![](../images/settheme-rowposition.png)
> 此图片已过时,正确的请看下面:
<div class="theme-positions" id="theme-positions">
<div class="column ccharacter"></div>
<div class="column c1"></div>
@@ -158,7 +160,7 @@
![](../images/moverow6_SpinningRows_Merge.png)
将`轨道`转到`角度`度是**合并**,no key是选择是否有烟雾。
将`轨道`转到`角度`度是**合并**。
### 分割
+70 -70
View File
@@ -15,14 +15,14 @@
在“**循环**”的空栏里输入次数,这个单发拍就会被**额外**重复这么多次。当然了,直接输入循环次数的方法并不够方便,因为很多时候不听具体音乐自己也不知道该循环多少次。更方便的方法是,把鼠标移动到工作区的这个单发拍子上,右上角就会出现一个**红色的箭头符号**,按住它并拖动即可。
```rdview 7 3
eb.AddOneshotBeat 1 1[tick=1,loop=2];
AddOneshotBeat 1 1[tick=1,loop=2];
```
在循环设置后,左上角又会多出一个“**间隔**”的设置,指的是两个绿线之间的时间长度。大多数时候,我们会将间隔设为拍长的两倍,这也就构成了一个普通单发拍子。但如果你想设计更为复杂的单发拍子节奏型,也可以试着将间隔改为其他数值:例如,把间隔改成`1`,拍长改成`0.75`,就变成了一种摇摆单发拍子。
```rdview 10 3
eb.AddOneshotBeat 1 1[loop=1];
eb.AddOneshotBeat 5 1[tick=0.75,loop=3,interval=1];
AddOneshotBeat 1 1[loop=1];
AddOneshotBeat 5 1[tick=0.75,loop=3,interval=1];
```
配置好带有循环的单发拍之后,按一下“**分解为单发节拍**”,会发现它被拆散成了若干个没有循环的单发拍事件。分解后的单发拍更加便于修改,你可以为它们单独添加修饰符或额外的脉冲。
@@ -42,80 +42,80 @@ eb.AddOneshotBeat 5 1[tick=0.75,loop=3,interval=1];
将护士语音的拍长与单发拍的拍长保持一致,并将护士**喊出`Go!`的位置**与 **第一个普通单发拍的脉冲位置** 对齐即可,这样就完成了对一个普通单发拍的提示。
```rdview 16 12
eb.AddOneshotBeat 5 1[tick=2];
eb.AddOneshotBeat 9 1[tick=2];
eb.AddOneshotBeat 13 1[tick=2];
AddOneshotBeat 5 1[tick=2];
AddOneshotBeat 9 1[tick=2];
AddOneshotBeat 13 1[tick=2];
g 1 2 4 1[offset=4 0]{
g 0 -1 0 2 #0f08{
es.SayReadyGetSetGo_Get 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayGet];
};
g 2 -1 0 2 #ff08{
es.SayReadyGetSetGo_Set 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSaySet];
};
g 4 -1 0 2 #f008{
es.SayReadyGetSetGo_Go 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayGo];
};
};
eb.AddOneshotBeat 5 4;
eb.AddOneshotBeat 7 4;
eb.AddOneshotBeat 9 4;
eb.AddOneshotBeat 11 4;
eb.AddOneshotBeat 13 4;
AddOneshotBeat 5 4;
AddOneshotBeat 7 4;
AddOneshotBeat 9 4;
AddOneshotBeat 11 4;
AddOneshotBeat 13 4;
g 1 5 4 2[offset=4 0]{
es.SayReadyGetSetGo_ReaDyGetSetGo 0 0;
SayReadyGetSetGo 0 0[phraseToSay=SayReaDyGetSetGoNew];
g 0 -1 0 3 #0f08{
es.SayReadyGetSetGo_Rea 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayRea];
};
g 1 -1 0 3 #ff08{
es.SayReadyGetSetGo_Dy 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayDy];
};
g 2 -1 0 3 #0f08{
es.SayReadyGetSetGo_Get 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayGet];
};
g 3 -1 0 3 #ff08{
es.SayReadyGetSetGo_Set 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSaySet];
};
g 4 -1 0 3 #F008{
es.SayReadyGetSetGo_Go 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayGo];
};
};
eb.AddOneshotBeat 5 8[tick=0.5];
eb.AddOneshotBeat 6 8[tick=0.5];
eb.AddOneshotBeat 7 8[tick=0.5];
eb.AddOneshotBeat 8 8[tick=0.5];
eb.AddOneshotBeat 9 8[tick=0.5];
eb.AddOneshotBeat 10 8[tick=0.5];
eb.AddOneshotBeat 11 8[tick=0.5];
eb.AddOneshotBeat 12 8[tick=0.5];
eb.AddOneshotBeat 13 8[tick=0.5];
eb.AddOneshotBeat 14 8[tick=0.5];
AddOneshotBeat 5 8[tick=0.5];
AddOneshotBeat 6 8[tick=0.5];
AddOneshotBeat 7 8[tick=0.5];
AddOneshotBeat 8 8[tick=0.5];
AddOneshotBeat 9 8[tick=0.5];
AddOneshotBeat 10 8[tick=0.5];
AddOneshotBeat 11 8[tick=0.5];
AddOneshotBeat 12 8[tick=0.5];
AddOneshotBeat 13 8[tick=0.5];
AddOneshotBeat 14 8[tick=0.5];
g 1 9 4 2[offset=4 0]{
g 0 -1 0 2 #0f08{
es.SayReadyGetSetGo_Rea 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayRea];
};
g 0.5 -1 0 3 #ff08{
es.SayReadyGetSetGo_Dy 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayDy];
};
g 1 -1 0 2 #0f08{
es.SayReadyGetSetGo_Rea 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayRea];
};
g 1.5 -1 0 3 #ff08{
es.SayReadyGetSetGo_Dy 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayDy];
};
g 2 -1 0 2 #0f08{
es.SayReadyGetSetGo_Rea 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayRea];
};
g 2.5 -1 0 3 #ff08{
es.SayReadyGetSetGo_Dy 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSayDy];
};
g 3 -1 0 2 #0f08{
es.SayReadyGetSetGo_Get 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayGet];
};
g 3.5 -1 0 3 #ff08{
es.SayReadyGetSetGo_Set 0 2;
SayReadyGetSetGo 0 2[phraseToSay=JustSaySet];
};
g 4 -1 0 2 #F008{
es.SayReadyGetSetGo_Go 0 1;
SayReadyGetSetGo 0 1[phraseToSay=JustSayGo];
};
};
```
@@ -131,30 +131,30 @@ g 1 9 4 2[offset=4 0]{
也可以让单发拍子在几种拍速之间来回切换,提示方法与前面所讲的相同。只需要注意一点:单发拍子的节奏型应该在**Go!的瞬间**完成切换,Go之前的护士提示不会影响当前正在进行的单发拍子,而 Go 之后也不应再出现前一种单发拍子的节拍。
```rdview 20 5
eb.AddOneshotBeat 3 1;
eb.AddOneshotBeat 5 1;
eb.AddOneshotBeat 7 1[tick=0.5];
eb.AddOneshotBeat 8 1[tick=0.5];
eb.AddOneshotBeat 9 1[tick=0.5];
eb.AddOneshotBeat 10 1[tick=0.5];
eb.AddOneshotBeat 11 1;
eb.AddOneshotBeat 13 1;
eb.AddOneshotBeat 15 1[tick=0.75];
eb.AddOneshotBeat 16 1[tick=0.75];
eb.AddOneshotBeat 17 1[tick=0.75];
eb.AddOneshotBeat 18 1[tick=0.75];
es.SayReadyGetSetGo_Get 1 2;
es.SayReadyGetSetGo_Set 2 2;
es.SayReadyGetSetGo_Go 3 2;
es.SayReadyGetSetGo_Get 6 2;
es.SayReadyGetSetGo_Set 6.5 3;
es.SayReadyGetSetGo_Go 7 2;
es.SayReadyGetSetGo_Get 9 2;
es.SayReadyGetSetGo_Set 10 2;
es.SayReadyGetSetGo_Go 11 2;
es.SayReadyGetSetGo_Get 14 2;
es.SayReadyGetSetGo_Set 14.75 3;
es.SayReadyGetSetGo_Go 15 2;
AddOneshotBeat 3 1;
AddOneshotBeat 5 1;
AddOneshotBeat 7 1[tick=0.5];
AddOneshotBeat 8 1[tick=0.5];
AddOneshotBeat 9 1[tick=0.5];
AddOneshotBeat 10 1[tick=0.5];
AddOneshotBeat 11 1;
AddOneshotBeat 13 1;
AddOneshotBeat 15 1[tick=0.75];
AddOneshotBeat 16 1[tick=0.75];
AddOneshotBeat 17 1[tick=0.75];
AddOneshotBeat 18 1[tick=0.75];
SayReadyGetSetGo 1 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 2 2[phraseToSay=JustSaySet];
SayReadyGetSetGo 3 2[phraseToSay=JustSayGo];
SayReadyGetSetGo 6 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 6.5 3[phraseToSay=JustSaySet];
SayReadyGetSetGo 7 2[phraseToSay=JustSayGo];
SayReadyGetSetGo 9 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 10 2[phraseToSay=JustSaySet];
SayReadyGetSetGo 11 2[phraseToSay=JustSayGo];
SayReadyGetSetGo 14 2[phraseToSay=JustSayGet];
SayReadyGetSetGo 14.75 3[phraseToSay=JustSaySet];
SayReadyGetSetGo 15 2[phraseToSay=JustSayGo];
```
一个单发拍子不可能无限循环下去,总会有停下来的时候,可以使用护士提示中的`Stop`告知玩家单发拍子的结束。通常情况下,应使`Stop`对齐单发拍子的**最后一次击拍位置**。
@@ -164,12 +164,12 @@ es.SayReadyGetSetGo_Go 15 2;
如果单发拍子的停止时机足够符合直觉,可以省略单发拍子的Stop,但必须保证玩家下意识多打出的拍子不会判Miss。最稳妥的方法永远是为所有单发拍子的停止都添加 Stop 提示。
```rdview 10 3
eb.AddClassicBeat 2.5[tick=6];
eb.AddOneshotBeat 1 1[];
eb.AddOneshotBeat 3 1[];
eb.AddOneshotBeat 5 1[];
AddClassicBeat 2.5[tick=1];
AddOneshotBeat 1 1[];
AddOneshotBeat 3 1[];
AddOneshotBeat 5 1[];
g 7 1 1 1{
eb.AddOneshotBeat[grayscale=100];
AddOneshotBeat[grayscale=100];
};
```
@@ -182,12 +182,12 @@ g 7 1 1 1{
![设置波](../images/oneshotbeat-oneshotwave.png)
![设置波效果](../images/oneshotbeat-nicolewave.gif)
`摇晃`就是像`4-4`那关一样的摇晃效果。
选中一个单发拍子,按住`Shift + H`,会发现多出了一个爱心脉冲的修饰符。启用这个修饰符,可以将单发拍的形状变成`2-X`结尾的超大爱心。
![隐藏的爱心](../images/oneshotbeat-heart.png)
> 此外像`4-4`关的抖动轨道单发拍子,需调用自定义方法来完成,语句为`wobblyLines = true`。如果你还不明白自定义方法是什么,需要先翻到[自定义方法](../pages/custommethod.md)看看。考虑到这玩意问的人比较多,所以就先写在此处了。
## 演示关卡
最后依然是呈上一个示范谱面。
+2
View File
@@ -18,6 +18,8 @@
> 当你把透明度调节为`0%`并打开轨道边框时,就会看到轨道线是插入进人物和心脏的像素图的。
**心:**修改心脏的贴图,比如马克的分心、万圣节的南瓜心、被网好卡菌感染的紫心等。
**时长与缓速:**和其他特效中的完全一致,不过多赘述。
> 如果你想直观地看看各种移动方式的速度区别,可以[看看这里](https://easings.net/zh-cn)。
+54
View File
@@ -0,0 +1,54 @@
# 关卡播放与跳转
## 设置播放方式
**设置播放方式**(在紫色板块中),作用是设置关卡的播放方式,比如说,跳转到某个小节,或者循环播放某个小节。
![设置播放方式](../images/setplaystyleandgotolevel-setplaystyle.png)
**下一小节**是将要跳转到的小节,如果勾选了`相对`,那么这个数字就是相对于当前小节的数字。
> 比如说,当前小节是第3小节,**下一小节**填写了4,则若勾选`相对`将跳转到第7小节,不勾选`相对`则跳转到第4小节。
播放方式有以下几种:
- **正常**:正常播放完当前小节,然后跳转。
- **循环**:循环播放当前小节。(如果选择**循环**,则没有**下一小节**)
- **延长**:~~据笔者实验,似乎是无限延长当前小节~~笔者不确定它的作用,待补充。
- **立即**:播放完这一拍之后,立即跳转。
- **超级立即**: 立即跳转。
- **延长下一拍**:~~据笔者实验,似乎与**正常**差别不大,但若在小节最后一拍,不会跳转~~笔者不确定它的作用,待补充。
## 前往关卡
**前往关卡**(也在紫色板块中),顾名思义,就是前往某个关卡。(为了方便叙述~~偷懒~~,下文中将**当前关卡**称为**a关卡**,**要前往的关卡**称作**b关卡**)但要注意,a关卡和b关卡需在同一文件夹中。导出a关卡时,b关卡会打包进a关卡的rdzip里。
**前往关卡**事件有两个用法,一个是直接`前往关卡`,另一个是先`设置下一个`关卡再`前往下一个`关卡。后者需要两个**前往关卡**事件。
你可能会问,为什么要用两个事件,先`设置下一个`再`前往下一个`~~,这不是脱裤子放屁吗~~?这是因为,只有先`设置下一个`关卡,才能有`跳过`功能。这里我们先按下不表,详见后文[制作教程关卡](#制作教程关卡)。
![前往关卡](../images/setplaystyleandgotolevel-gotolevel.png)
**淡入**:设置是否淡入。
**立即开始**:设置在b关卡开始前,是否显示`按空格键开始`的画面。
**保留失误**:这条很好理解,设置是否将a关卡的失误数带入b关卡。
**阻止重启按钮**:这条的名称可能有点迷惑。它的作用实际上是:在**已经跳转到b关卡后**,当点击重启按钮时,如果它被勾选,那么将**从a关卡开始**,而不是从b关卡开始;反之,如果它未被勾选,则从b关卡开始。
## 制作教程关卡
为什么把这两个事件一起介绍呢?原因就是,它们最常见的场景都是在**教程关卡**中使用。什么是**教程关卡**?相信只要你玩过主线,对它不会陌生,它是在正式关卡开始前,对**新出现的机制或节奏**的介绍与教学。一般都长这样:
![2-1的教程](../images/setplaystyleandgotolevel-tutorial.png)
想要实现在教程结束之后跳转到正式关卡,就需要用**前往关卡**事件。
你想必知道,官方的**教程关卡**都可以在菜单里`跳过`,要实现这个功能,就得在关卡开头放上`动作`为`设置下一个关卡`的**前往关卡**事件,并且勾选**允许跳过按钮**。再在结尾放上`动作`为`前往下一个关卡`的**前往关卡**事件。
那么,**设置播放方式**在**教程关卡**中有什么用呢?如果你节奏感一般~~像我一样~~,可能就会发现,官方的**教程关卡**中,如果你在一段中失误太多,就会重新开始这一段。这就需要用到**设置播放方式**,以跳转到之前的小节。当然,也要结合上之前学过的[条件](../pages/condition.md),以判断失误数是否超过某个值。([atLeastNPerfects](https://docs.qq.com/sheet/DQUJxb0puSFFOVWpz?tab=000003)可能会派上用场)
> 最后,做个关于关卡设计的提醒:制作**教程关卡**时,请添加`晕影`或`闪烁晕影`特效并添加`轨道涂色`,以确保`轨道`清晰可见。
![2-1的教程](../images/setplaystyleandgotolevel-tutorialvfx.png)
+1 -1
View File
@@ -16,7 +16,7 @@
甚至轨道可以大部分位置都不可见。比如制作只显示脉冲心跳部位的单发拍子,并做成纯色等。
> 调用自定义方法`invisibleHeart = true`可以让轨道上的心脏消失。如果不选择角色,那么就只剩下一根线了,在一些极简色块画风的关卡里效果拔群。
> 使用轨道涂色或调用自定义方法`invisibleHeart = true`可以让轨道上的心脏消失。如果不选择角色,那么就只剩下一根线了,在一些极简色块画风的关卡里效果拔群。
## 背景和前景图片
+190 -190
View File
@@ -9,58 +9,58 @@
```rdview 18 3
g 1 0 4 2 #ff0{
g 0 -1 0 3 #08f[]{};
ea.MoveRow [tag=true];
ea.MoveRow 1[tag=true];
ea.MoveCamera 2[tag=true];
ea.Flash 2.5[tag=true];
ea.HideRow 0 1[tag=true];
ea.SetVFXPreset 1.5 1[tag=true];
ea.SetVFXPreset 2 1[tag=true];
ea.HideRow 3 1[tag=true];
MoveRow [tag=action];
MoveRow 1[tag=action];
MoveCamera 2[tag=action];
Flash 2.5[tag=action];
HideRow 0 1[tag=action];
SetVFXPreset 1.5 1[tag=action];
SetVFXPreset 2 1[tag=action];
HideRow 3 1[tag=action];
g 5 0 0 0 #0000[offset=0 -1]{
ea.TagAction[offset_opacity=0];
TagAction[offset_opacity=0,Tag=action];
g 0 1 4 2 #ff0[opacity=0,offset_opacity=100]{
g 0 -1 0 3 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
ea.MoveCamera 2[];
ea.Flash 2.5[];
ea.HideRow 0 1[];
ea.SetVFXPreset 1.5 1[];
ea.SetVFXPreset 2 1[];
ea.HideRow 3 1[];
MoveRow [];
MoveRow 1[];
MoveCamera 2[];
Flash 2.5[];
HideRow 0 1[];
SetVFXPreset 1.5 1[];
SetVFXPreset 2 1[];
HideRow 3 1[];
};
};
g 9 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[offset_opacity=0,Tag=action];
g 0 1 4 2 #ff0[opacity=0,offset_opacity=100]{
g 0 -1 0 3 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
ea.MoveCamera 2[];
ea.Flash 2.5[];
ea.HideRow 0 1[];
ea.SetVFXPreset 1.5 1[];
ea.SetVFXPreset 2 1[];
ea.HideRow 3 1[];
MoveRow [];
MoveRow 1[];
MoveCamera 2[];
Flash 2.5[];
HideRow 0 1[];
SetVFXPreset 1.5 1[];
SetVFXPreset 2 1[];
HideRow 3 1[];
};
};
g 13 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[offset_opacity=0,Tag=action];
g 0 1 4 2 #ff0[opacity=0,offset_opacity=100]{
g 0 -1 0 3 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
ea.MoveCamera 2[];
ea.Flash 2.5[];
ea.HideRow 0 1[];
ea.SetVFXPreset 1.5 1[];
ea.SetVFXPreset 2 1[];
ea.HideRow 3 1[];
MoveRow [];
MoveRow 1[];
MoveCamera 2[];
Flash 2.5[];
HideRow 0 1[];
SetVFXPreset 1.5 1[];
SetVFXPreset 2 1[];
HideRow 3 1[];
};
};
};
@@ -111,75 +111,75 @@ g 1 0 4 2 #ff0{
```rdview 16 5
g 1 1 2 1 #df0{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [tag=true];
ea.MoveRow 1[tag=true];
MoveRow [tag=move];
MoveRow 1[tag=move];
g 4 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
};
};
g 6 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
};
};
g 8 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
};
};
g 10 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
g 0 -1 1 1 #0000[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
};
};
};
g 1 3 3 1 #df0{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [tag=true];
ea.MoveRow 1[tag=true];
ea.TagAction 2[tag=true];
MoveRow [tag=move];
MoveRow 1[tag=move];
TagAction 2[tag=move];
g 4 0 0 0 #0000[offset=0 -1]{
ea.TagAction[offset_opacity=0];
TagAction[offset_opacity=0,Tag=move];
g 0 1 3 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
g 2 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[offset_opacity=0,Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
g 2 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[offset_opacity=0,Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
g 2 0 0 0 #0000[offset=0 -1]{
ea.TagAction[];
TagAction[offset_opacity=0,Tag=move];
g 0 1 2 1 #ff0[opacity=20,offset_opacity=100]{
g 0 -1 0 2 #08f[]{};
ea.MoveRow [];
ea.MoveRow 1[];
MoveRow [];
MoveRow 1[];
g -6 -4 1 5 #0000[]{};
};
};
@@ -189,7 +189,7 @@ g 1 3 3 1 #df0{
};
};
};
ea.TagAction 12[];
TagAction 12[];
};
```
@@ -211,143 +211,143 @@ g 1 3 3 1 #df0{
```rdview 19 4
g 0 0 19 1 #f00[]{
ea.Comment [];
ea.FloatingText 0.5[];
ea.FloatingText 1[];
ea.FloatingText 1.5[];
ea.FloatingText 2[];
ea.FloatingText 2.5[];
ea.FloatingText 3[];
ea.FloatingText 3.5[];
ea.FloatingText 4[];
ea.AdvanceText 4.5[];
ea.AdvanceText 5[];
ea.AdvanceText 5.5[];
ea.AdvanceText 6[];
ea.AdvanceText 6.5[];
ea.AdvanceText 7[];
ea.AdvanceText 7.5[];
ea.FloatingText 8[];
ea.FloatingText 8.75[];
ea.Comment 9.75[];
ea.FloatingText 11.75[];
ea.FloatingText 12.5[];
ea.FloatingText 13.5[];
ea.FloatingText 14[];
ea.FloatingText 14.5[];
ea.FloatingText 15[];
ea.FloatingText 15.5[];
ea.FloatingText 16[];
ea.FloatingText 16.5[];
ea.FloatingText 17[];
ea.FloatingText 17.5[];
Comment [color=ffff00];
FloatingText 0.5[outlineColor=000000];
FloatingText 1[outlineColor=000000];
FloatingText 1.5[outlineColor=000000];
FloatingText 2[outlineColor=000000];
FloatingText 2.5[outlineColor=000000];
FloatingText 3[outlineColor=000000];
FloatingText 3.5[outlineColor=000000];
FloatingText 4[outlineColor=000000];
AdvanceText 4.5[];
AdvanceText 5[];
AdvanceText 5.5[];
AdvanceText 6[];
AdvanceText 6.5[];
AdvanceText 7[];
AdvanceText 7.5[];
FloatingText 8[outlineColor=000000];
FloatingText 8.75[outlineColor=000000];
Comment 9.75[tab=Actions];
FloatingText 11.75[outlineColor=000000];
FloatingText 12.5[outlineColor=000000];
FloatingText 13.5[outlineColor=000000];
FloatingText 14[outlineColor=000000];
FloatingText 14.5[outlineColor=000000];
FloatingText 15[outlineColor=000000];
FloatingText 15.5[outlineColor=000000];
FloatingText 16[outlineColor=000000];
FloatingText 16.5[outlineColor=000000];
FloatingText 17[outlineColor=000000];
FloatingText 17.5[outlineColor=000000];
};
g 0 1 6.25 1 #08f[]{
ea.MoveCamera [];
ea.MoveCamera 0.5[];
ea.MoveCamera 1[];
ea.MoveCamera 1.5[];
ea.MoveCamera 2[];
ea.MoveCamera 2.5[];
ea.MoveCamera 3[];
ea.MoveCamera 3.5[];
ea.MoveCamera 4[];
ea.MoveCamera 5[];
ea.SetForeground 5.25[];
MoveCamera [];
MoveCamera 0.5[];
MoveCamera 1[];
MoveCamera 1.5[];
MoveCamera 2[];
MoveCamera 2.5[];
MoveCamera 3[];
MoveCamera 3.5[];
MoveCamera 4[];
MoveCamera 5[];
SetForeground 5.25[];
};
ea.Comment 6.25 1[];
ea.MoveRow 7.75 1[];
ea.MoveRow 8 1[];
ea.MoveRow 8.75 1[];
ea.MoveCamera 12 1[];
ea.SetForeground 12.25 1[];
ea.FloatingText 14.5 1[];
ea.FloatingText 15 1[];
ea.FloatingText 15.5 1[];
ea.FloatingText 16 1[];
ea.FloatingText 16.5 1[];
ea.SetVFXPreset 18 1[];
Comment 6.25 1[tab=Actions];
MoveRow 7.75 1[];
MoveRow 8 1[];
MoveRow 8.75 1[];
MoveCamera 12 1[];
SetForeground 12.25 1[];
FloatingText 14.5 1[outlineColor=000000];
FloatingText 15 1[outlineColor=000000];
FloatingText 15.5 1[outlineColor=000000];
FloatingText 16 1[outlineColor=000000];
FloatingText 16.5 1[outlineColor=000000];
SetVFXPreset 18 1[];
g 0 2 13.875 1 #06f[]{
ea.MoveRow [];
ea.MoveRow [];
ea.Comment 1.75[];
ea.MoveRow 2.75[];
ea.MoveRow 3[];
ea.MoveRow 4.5[];
ea.MoveRow 4.75[];
ea.MoveRow 5[];
ea.MoveRow 5.25[];
ea.MoveRow 6.75[];
ea.MoveRow 7[];
ea.MoveRow 7.5[];
ea.MoveRow 7.75[];
ea.MoveRow 8.5[];
ea.MoveRow 10[];
ea.MoveRow 10.25[];
ea.HideRow 11.5[];
ea.HideRow 11.5[];
ea.MoveRow 12.5[];
ea.MoveRow 12.75[];
MoveRow [];
MoveRow [];
Comment 1.75[tab=Actions];
MoveRow 2.75[];
MoveRow 3[];
MoveRow 4.5[];
MoveRow 4.75[];
MoveRow 5[];
MoveRow 5.25[];
MoveRow 6.75[];
MoveRow 7[];
MoveRow 7.5[];
MoveRow 7.75[];
MoveRow 8.5[];
MoveRow 10[];
MoveRow 10.25[];
HideRow 11.5[];
HideRow 11.5[];
MoveRow 12.5[];
MoveRow 12.75[];
};
ea.Comment 14 2[];
Comment 14 2[tab=Actions];
g 0 3 18 1 #f80[]{
ea.FloatingText 0.5[];
ea.AdvanceText 1[];
ea.AdvanceText 1.5[];
ea.AdvanceText 2.5[];
ea.AdvanceText 3[];
ea.AdvanceText 4.5[];
ea.AdvanceText 5[];
ea.AdvanceText 5.5[];
ea.AdvanceText 6[];
ea.AdvanceText 6.5[];
ea.AdvanceText 7[];
ea.AdvanceText 8[];
ea.AdvanceText 10.5[];
ea.AdvanceText 11.5[];
ea.AdvanceText 12[];
ea.AdvanceText 12.5[];
ea.AdvanceText 13[];
ea.AdvanceText 13.5[];
ea.AdvanceText 14[];
ea.AdvanceText 14.5[];
ea.AdvanceText 15[];
ea.AdvanceText 15.5[];
ea.AdvanceText 16[];
ea.AdvanceText 16.5[];
ea.AdvanceText 17[];
ea.AdvanceText 17.5[];
FloatingText 0.5[outlineColor=000000];
AdvanceText 1[];
AdvanceText 1.5[];
AdvanceText 2.5[];
AdvanceText 3[];
AdvanceText 4.5[];
AdvanceText 5[];
AdvanceText 5.5[];
AdvanceText 6[];
AdvanceText 6.5[];
AdvanceText 7[];
AdvanceText 8[];
AdvanceText 10.5[];
AdvanceText 11.5[];
AdvanceText 12[];
AdvanceText 12.5[];
AdvanceText 13[];
AdvanceText 13.5[];
AdvanceText 14[];
AdvanceText 14.5[];
AdvanceText 15[];
AdvanceText 15.5[];
AdvanceText 16[];
AdvanceText 16.5[];
AdvanceText 17[];
AdvanceText 17.5[];
};
```
2. 如果有一串同类型的事件紧密排布,例如每1/4格或者1/2格放一个,那么可以考虑不放在同一行,而是上下交错排开。
```rdview 16 4
ea.MoveRow [];
ea.MoveRow 1[];
ea.MoveRow 2[];
ea.MoveRow 4[];
ea.MoveRow 5[];
ea.MoveRow 6[];
ea.MoveRow 8[];
ea.MoveRow 9[];
ea.MoveRow 10[];
ea.MoveRow 12[];
ea.MoveRow 13[];
ea.MoveRow 14[];
ea.MoveRow 0.5 1[];
ea.MoveRow 1.5 1[];
ea.MoveRow 3 1[];
ea.MoveRow 4.5 1[];
ea.MoveRow 5.5 1[];
ea.MoveRow 7 1[];
ea.MoveRow 8.5 1[];
ea.MoveRow 9.5 1[];
ea.MoveRow 11 1[];
ea.MoveRow 12.5 1[];
ea.MoveRow 13.5 1[];
ea.MoveRow 15 1[];
ea.Comment 4 3[];
MoveRow [];
MoveRow 1[];
MoveRow 2[];
MoveRow 4[];
MoveRow 5[];
MoveRow 6[];
MoveRow 8[];
MoveRow 9[];
MoveRow 10[];
MoveRow 12[];
MoveRow 13[];
MoveRow 14[];
MoveRow 0.5 1[];
MoveRow 1.5 1[];
MoveRow 3 1[];
MoveRow 4.5 1[];
MoveRow 5.5 1[];
MoveRow 7 1[];
MoveRow 8.5 1[];
MoveRow 9.5 1[];
MoveRow 11 1[];
MoveRow 12.5 1[];
MoveRow 13.5 1[];
MoveRow 15 1[];
Comment 4 3[tab=Actions];
```
3. 对于事件总体数量不太高的情况,如果有操作房间和轨道类型的特效,可以考虑把他们放在对应数量的行里,找起来会更加方便。
+2 -1
View File
@@ -11,6 +11,7 @@
| 震动类型 | 效果 |
| :--- | :--- |
| **屏幕震动** | 整个画面抖动,分为轻微、中等、剧烈三档 |
| **自定义震动** | 自由度更高,有多种模式 |
| **按键震屏** | 每次按拍时画面抖动一下,适合强调节拍的"打击感" |
| **心跳震屏** | 每次脉冲时画面轻微抖动,适合营造心跳般的律动感 |
| **重低音震动** | 大幅度色像差 + 大幅度屏幕缩放,视觉冲击力最强 |
@@ -27,7 +28,7 @@
**闪烁:** 最简单的闪光,屏幕白色一闪,分为短、中、长三种时长。适合用在副歌进入、重拍等需要强调的时刻。
**自定义闪烁:** 可以自由设置闪烁的颜色、透明度变化、持续时间和缓速模式。其中有一个"闪烁版"选项,可以控制闪烁出现在前景图之上还是背景图之上,灵活度很高。
**自定义闪烁:** 可以自由设置闪烁的颜色、透明度变化、持续时间和缓速模式。其中有一个"闪烁版"选项,可以控制闪烁出现在前景图之上还是背景图之上,灵活度很高。最下面的“减弱闪烁强度”则是当玩家开启“减弱闪烁强度”时使用这个设置的闪烁。
> 频繁闪烁的谱面请务必加上癫痫警告!
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

+36
View File
@@ -0,0 +1,36 @@
window.$docsify = {
name: "节奏医生编辑器教程",
notFoundPage: "/404.md",
homepage: "/pages/intro.md",
loadSidebar: true,
loadNavbar: true,
coverpage: false,
maxLevel: 5,
subMaxLevel: 4,
mergeNavbar: true,
markdown: {
renderer: {
image: function (src, title, alt) {
return `<div class="image-tip"><img class="medium-zoom-image" src="${src}" alt="${alt}">${
alt ? `<p>${alt}</p>` : ""
}</div>`;
},
},
},
switchLightDarkMode: {
useSwitchMode: true,
top: 130,
right: 26,
svgColor: "#7d7b75",
},
search: {
noData: {
"/": "没有找到结果!",
},
paths: "auto",
placeholder: {
"/": "搜索",
},
hideOtherSidebarContent: true,
},
};
File diff suppressed because it is too large Load Diff
+710 -30
View File
@@ -1,31 +1,711 @@
const customMethods = [
{ type: 'mod',}
]
const editDistance = (a, b) => {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1),
);
}
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(function () {
"use strict";
let ParameterType;
(function (ParameterType) {
ParameterType["String"] = "string";
ParameterType["Number"] = "number";
ParameterType["Boolean"] = "boolean";
ParameterType["Enum"] = "enum";
})(ParameterType || (ParameterType = {}));
let MethodType;
(function (MethodType) {
MethodType["Mod"] = "mod";
MethodType["Surgery"] = "surgery";
MethodType["Comment"] = "comment";
MethodType["CustomMethod"] = "custom_method";
MethodType["Ink"] = "ink";
MethodType["Variable"] = "variable";
MethodType["Condition"] = "condition";
MethodType["CustomClass"] = "custom_class";
})(MethodType || (MethodType = {}));
const enumTypeMap = {
Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"],
Size: ["Small", "Medium", "Large"],
Shape: ["Circle", "Square", "Triangle"],
};
class NumericRange {
constructor(min, max, isInteger = false) {
this.min = min;
this.max = max;
this.isInteger = isInteger;
}
}
}
return matrix[b.length][a.length];
}
class Parameter {
constructor(name, type, description, enumType, numericRange) {
this.name = name;
this.type = type;
this.description = description;
this.enumType = enumType;
this.numericRange = numericRange;
}
}
class Method {
constructor(name, type, shortDescription, description) {
this.name = name;
this.type = type;
this.shortDescription = shortDescription;
this.description = description;
}
}
class Mod extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.Mod, shortDescription, description);
}
}
class Surgery extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.Surgery, shortDescription, description);
}
}
class CommentMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Comment, shortDescription, description);
this.parameters = parameters;
}
}
class InkMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Ink, shortDescription, description);
this.parameters = parameters;
}
}
class CustomMethods extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.CustomMethod, shortDescription, description);
this.parameters = parameters;
}
}
class VariableMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Variable, shortDescription, description);
this.parameters = parameters;
}
}
class ConditionMethod extends Method {
constructor(name, shortDescription, description, parameters) {
super(name, MethodType.Condition, shortDescription, description);
this.parameters = parameters;
}
}
class CustomClassMethod extends Method {
constructor(name, shortDescription, description) {
super(name, MethodType.CustomClass, shortDescription, description);
}
}
function editDistance(a, b, caseSensitive = true) {
if (a.length === 0)
return b.length;
if (b.length === 0)
return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const ac = a.charAt(j - 1);
const bc = b.charAt(i - 1);
if (caseSensitive ? bc === ac : bc.toLowerCase() === ac.toLowerCase()) {
matrix[i][j] = matrix[i - 1][j - 1];
}
else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1));
}
}
}
return matrix[b.length][a.length];
}
function parametersOf(method) {
var _a;
return (_a = method.parameters) !== null && _a !== void 0 ? _a : [];
}
// 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
const strategies = {
string: "prefix",
number: "constant",
boolean: "constant",
};
// ---------------------------------------------------------------------
// 外置数据 hydrate(script/custommethod-data.js)
// ---------------------------------------------------------------------
function hydrateMethod(raw) {
var _a, _b, _c, _d;
const shortDescription = (_b = (_a = raw.shortDescription) !== null && _a !== void 0 ? _a : raw.description) !== null && _b !== void 0 ? _b : "";
const description = (_d = (_c = raw.description) !== null && _c !== void 0 ? _c : raw.shortDescription) !== null && _d !== void 0 ? _d : "";
const parameters = Array.isArray(raw.parameters)
? raw.parameters.map((item) => {
var _a, _b;
return new Parameter(item.name, (_a = item.type) !== null && _a !== void 0 ? _a : ParameterType.String, (_b = item.description) !== null && _b !== void 0 ? _b : "", item.enumType, item.numericRange
? new NumericRange(item.numericRange.min, item.numericRange.max, item.numericRange.isInteger)
: undefined);
})
: [];
switch (raw.type) {
case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
default: return new CustomMethods(raw.name, shortDescription, description, parameters);
}
}
let methods = [];
function loadExternalData() {
const data = window.CustomMethodSearchData;
if (!data)
return;
if (data.enumTypes) {
for (const key of Object.keys(data.enumTypes)) {
enumTypeMap[key] = data.enumTypes[key];
}
}
if (Array.isArray(data.methods)) {
const index = new Map();
methods.forEach((method, i) => index.set(method.name.toLowerCase(), i));
for (const raw of data.methods) {
if (!raw || !raw.name)
continue;
const name = String(raw.name).toLowerCase();
const hydrated = hydrateMethod(raw);
if (index.has(name)) {
methods[index.get(name)] = hydrated;
}
else {
index.set(name, methods.length);
methods.push(hydrated);
}
}
}
}
loadExternalData();
// ---------------------------------------------------------------------
// 匹配
// ---------------------------------------------------------------------
function searchMethods(query) {
const q = query.trim();
if (!q)
return [];
const ql = q.toLowerCase();
const maxDistance = Math.max(2, Math.floor(q.length / 2));
return methods
.map((method) => {
const name = method.name;
const lower = name.toLowerCase();
let score;
if (name === q) {
score = 0;
}
else if (lower.startsWith(ql)) {
score = 1;
}
else if (lower.indexOf(ql) >= 0) {
score = 2;
}
else {
score = 3 + editDistance(name, q, false);
}
return { method, score };
})
.filter((item) => item.score <= 2 || item.score <= 3 + maxDistance)
.sort((a, b) => a.score - b.score || a.method.name.localeCompare(b.method.name))
.slice(0, 8)
.map((item) => item.method);
}
// ---------------------------------------------------------------------
// DOM 工具
// ---------------------------------------------------------------------
function el(tag, className) {
const node = document.createElement(tag);
if (className)
node.className = className;
return node;
}
function resolveEnumOptions(parameter) {
if (parameter.enumType && enumTypeMap[parameter.enumType])
return enumTypeMap[parameter.enumType];
if (enumTypeMap[parameter.description])
return enumTypeMap[parameter.description];
if (enumTypeMap[parameter.name])
return enumTypeMap[parameter.name];
return [];
}
// 布尔可用 true/false,或布尔变量 b0~b9
const BOOLEAN_OPTIONS = ["true", "false"];
for (let i = 0; i < 10; i++) {
BOOLEAN_OPTIONS.push("b" + i);
}
function makeCombo(parameter, options, onChange) {
const combo = el("span", "cm-combo");
const input = el("input", "cm-input cm-combo-input");
input.type = "text";
input.placeholder = parameter.name;
input.autocomplete = "off";
const list = el("ul", "cm-combo-list");
list.hidden = true;
function populate() {
list.textContent = "";
if (options.length === 0) {
list.hidden = true;
return;
}
const query = input.value.trim();
const ql = query.toLowerCase();
const maxDistance = Math.max(1, Math.floor(query.length / 2));
const shown = (query
? options.filter((option) => option.toLowerCase().indexOf(ql) >= 0 ||
editDistance(option, query, false) <= maxDistance)
: options).slice(0, 10);
if (shown.length === 0) {
list.hidden = true;
return;
}
for (const option of shown) {
const item = el("li", "cm-combo-item");
item.textContent = option;
item.title = option;
item.addEventListener("mousedown", (event) => {
event.preventDefault();
input.value = option;
list.hidden = true;
onChange();
});
list.appendChild(item);
}
list.hidden = false;
}
input.addEventListener("input", () => { populate(); onChange(); });
input.addEventListener("focus", populate);
input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
combo.appendChild(input);
combo.appendChild(list);
return combo;
}
function makeModeToggle(initial, onChange) {
const button = el("button", "cm-mode");
button.type = "button";
let mode = initial;
const render = () => {
button.textContent = mode === "expression" ? "表达式" : "常量";
button.title = "切换 常量 / 表达式";
button.classList.toggle("is-expression", mode === "expression");
};
render();
button.addEventListener("click", (event) => {
event.preventDefault();
mode = mode === "expression" ? "constant" : "expression";
render();
onChange();
});
return { el: button, get: () => mode };
}
function stringValue(value) {
if (value.length === 0)
return "<文本>";
return strategies.string === "quote" ? `"${value}"` : `str:${value}`;
}
function makeField(parameter, onChange) {
switch (parameter.type) {
case ParameterType.Boolean: {
const wrap = el("span", "cm-field");
const checkbox = el("input", "cm-input cm-check");
checkbox.type = "checkbox";
checkbox.addEventListener("change", onChange);
const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
const comboInput = combo.querySelector("input");
let mode = strategies.boolean;
const toggle = makeModeToggle(strategies.boolean, () => {
mode = toggle.get();
applyMode();
onChange();
});
function applyMode() {
const expression = mode === "expression";
checkbox.hidden = expression;
combo.hidden = !expression;
}
applyMode();
wrap.appendChild(toggle.el);
wrap.appendChild(checkbox);
wrap.appendChild(combo);
return {
el: wrap,
read: () => {
if (mode === "constant") {
return { text: checkbox.checked ? "true" : "false", valid: true, invalid: false };
}
const value = comboInput.value.trim();
return value.length > 0
? { text: value, valid: true, invalid: false }
: { text: "<布尔>", valid: false, invalid: true };
},
};
}
case ParameterType.Number: {
const wrap = el("span", "cm-field");
const input = el("input", "cm-input");
input.type = "text";
input.inputMode = "decimal";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
const mode = makeModeToggle(strategies.number, onChange);
wrap.appendChild(mode.el);
wrap.appendChild(input);
return {
el: wrap,
read: () => {
const value = input.value.trim();
if (value.length === 0)
return { text: "<数字>", valid: false, invalid: true };
if (mode.get() === "expression")
return { text: value, valid: true, invalid: false };
const number = Number(value);
let valid = isFinite(number);
const range = parameter.numericRange;
if (valid && range) {
if (range.isInteger && !Number.isInteger(number))
valid = false;
if (range.min !== undefined && number < range.min)
valid = false;
if (range.max !== undefined && number > range.max)
valid = false;
}
return { text: value, valid, invalid: !valid };
},
};
}
case ParameterType.Enum: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const combo = makeCombo(parameter, resolveEnumOptions(parameter), onChange);
const input = combo.querySelector("input");
wrap.appendChild(open);
wrap.appendChild(combo);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: stringValue(value), valid, invalid: !valid };
},
};
}
default: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const input = el("input", "cm-input");
input.type = "text";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
wrap.appendChild(open);
wrap.appendChild(input);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
return { text: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
},
};
}
}
}
function methodSkeleton(method, hasParams) {
switch (method.type) {
case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
case MethodType.Condition:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
case MethodType.Surgery: return { prefix: method.name, suffix: "" };
default:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
}
}
function formatMethod(method, states) {
const args = states.map((state) => state.text).join(", ");
switch (method.type) {
case MethodType.Comment: return `()=>${method.name}(${args})`;
case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
case MethodType.CustomClass: return `"customClass": "${method.name}"`;
case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.Surgery: return method.name;
default: return states.length ? `${method.name}(${args})` : method.name;
}
}
function copyText(text) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
yield navigator.clipboard.writeText(text);
return true;
}
}
catch (error) {
// fall through
}
try {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
const ok = document.execCommand("copy");
document.body.removeChild(area);
return ok;
}
catch (error) {
return false;
}
});
}
// ---------------------------------------------------------------------
// 渲染
// ---------------------------------------------------------------------
function renderContainer(container) {
if (container.dataset.cmReady === "1")
return;
container.dataset.cmReady = "1";
let currentMethod = null;
let query = container.querySelector(".cm-query");
let suggest = container.querySelector(".cm-suggest");
let selected = container.querySelector(".cm-selected");
let nameEl = container.querySelector(".cm-name");
let shortEl = container.querySelector(".cm-short");
let call = container.querySelector(".cm-call");
let preview = container.querySelector(".cm-preview");
let desc = container.querySelector(".cm-desc");
let options = container.querySelector(".cm-options");
if (!query || !suggest || !selected || !nameEl || !shortEl || !call || !preview || !desc) {
container.textContent = "";
const searchBar = el("div", "cm-search");
query = el("input", "cm-query");
query.type = "text";
query.placeholder = "输入自定义方法名,如 ChangeCharacter";
query.autocomplete = "off";
suggest = el("ul", "cm-suggest");
searchBar.appendChild(query);
searchBar.appendChild(suggest);
selected = el("div", "cm-selected");
selected.hidden = true;
const head = el("div", "cm-head");
nameEl = el("span", "cm-name");
shortEl = el("span", "cm-short");
head.appendChild(nameEl);
head.appendChild(shortEl);
call = el("div", "cm-call");
preview = el("code", "cm-preview");
desc = el("div", "cm-desc");
selected.appendChild(head);
selected.appendChild(call);
selected.appendChild(preview);
selected.appendChild(desc);
container.appendChild(searchBar);
container.appendChild(selected);
}
// 搜索框下的策略选项
options = container.querySelector(".cm-options");
if (!options) {
options = el("div", "cm-options");
query.parentElement.insertAdjacentElement("afterend", options);
}
if (!options.querySelector(".cm-option-select")) {
const optionsBox = options;
function addOption(label, key, values, current) {
const wrap = el("label", "cm-option");
const text = el("span", "cm-option-label");
text.textContent = label;
const select = el("select", "cm-option-select");
for (const [value, caption] of values) {
const opt = el("option");
opt.value = value;
opt.textContent = caption;
select.appendChild(opt);
}
select.value = current;
select.addEventListener("change", () => {
strategies[key] = select.value;
if (currentMethod) {
selectMethod(currentMethod);
}
});
wrap.appendChild(text);
wrap.appendChild(select);
optionsBox.appendChild(wrap);
}
addOption("字符串", "string", [["prefix", "str: 前缀"], ["quote", "引号 \"…\""]], strategies.string);
}
const queryEl = query;
const suggestEl = suggest;
const selectedEl = selected;
const nameNode = nameEl;
const shortNode = shortEl;
const callEl = call;
const previewEl = preview;
const descNode = desc;
const copyBtn = el("button", "cm-copy");
copyBtn.type = "button";
copyBtn.textContent = "复制";
copyBtn.hidden = true;
copyBtn.title = "复制生成的代码";
selectedEl.appendChild(copyBtn);
copyBtn.addEventListener("click", () => {
var _a;
copyText((_a = previewEl.textContent) !== null && _a !== void 0 ? _a : "").then((ok) => {
copyBtn.textContent = ok ? "已复制" : "复制失败";
window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200);
});
});
function clearSuggest() {
suggestEl.textContent = "";
}
function selectMethod(method) {
currentMethod = method;
clearSuggest();
queryEl.value = method.name;
selectedEl.hidden = false;
nameNode.textContent = method.name;
shortNode.textContent = method.shortDescription || "";
descNode.textContent = method.description || "";
callEl.textContent = "";
previewEl.textContent = "";
copyBtn.hidden = true;
const parameters = parametersOf(method);
const fields = [];
function updatePreview() {
const states = fields.map((field) => field.read());
fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
previewEl.textContent = formatMethod(method, states);
copyBtn.hidden = !states.every((state) => state.valid);
}
const skeleton = methodSkeleton(method, parameters.length > 0);
const code = el("code", "cm-code");
const prefix = el("span", "cm-lit");
prefix.textContent = skeleton.prefix;
code.appendChild(prefix);
parameters.forEach((parameter, index) => {
if (index > 0) {
const sep = el("span", "cm-lit");
sep.textContent = ", ";
code.appendChild(sep);
}
const field = makeField(parameter, updatePreview);
field.el.title = parameter.description;
code.appendChild(field.el);
fields.push(field);
});
if (skeleton.suffix) {
const suffix = el("span", "cm-lit");
suffix.textContent = skeleton.suffix;
code.appendChild(suffix);
}
callEl.appendChild(code);
if (parameters.length === 0) {
const empty = el("span", "cm-empty");
empty.textContent = "(无参数)";
callEl.appendChild(empty);
}
updatePreview();
}
function updateSuggestions() {
clearSuggest();
for (const method of searchMethods(queryEl.value)) {
const item = el("li", "cm-suggest-item");
const nameSpan = el("span", "cm-suggest-name");
nameSpan.textContent = method.name;
const descSpan = el("span", "cm-suggest-desc");
descSpan.textContent = method.shortDescription || method.description;
item.appendChild(nameSpan);
item.appendChild(descSpan);
item.addEventListener("mousedown", (event) => {
event.preventDefault();
selectMethod(method);
});
suggestEl.appendChild(item);
}
}
queryEl.addEventListener("input", () => {
updateSuggestions();
selectedEl.hidden = true;
});
queryEl.addEventListener("focus", updateSuggestions);
queryEl.addEventListener("blur", () => {
window.setTimeout(clearSuggest, 120);
});
if (queryEl.value) {
updateSuggestions();
}
}
function initAll() {
document.querySelectorAll(".custommethods-search").forEach((container) => renderContainer(container));
}
function register() {
const docsify = window.$docsify;
if (docsify) {
docsify.plugins = [].concat(docsify.plugins, (hook) => {
hook.doneEach(() => initAll());
});
}
else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initAll());
}
else {
initAll();
}
}
window.CustomMethodSearch = {
search: searchMethods,
editDistance,
renderAll: initAll,
strategies,
getMethods: () => methods,
setMethods: (list) => { methods = list; initAll(); },
Parameter,
ParameterType,
Method,
MethodType,
Mod,
Surgery,
CommentMethod,
InkMethod,
CustomMethods,
VariableMethod,
ConditionMethod,
CustomClassMethod,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", register);
}
else {
register();
}
})();
+742 -27
View File
@@ -1,32 +1,747 @@
class CustomMethods{
type: "mod"|"surgery"|"comment"|"custom_method";
}
(function () {
"use strict";
function editDistance(a: String, b: String): number {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix: number[][] = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1),
);
}
enum ParameterType {
String = "string",
Number = "number",
Boolean = "boolean",
Enum = "enum",
}
}
return matrix[b.length][a.length];
}
enum MethodType {
Mod = "mod",
Surgery = "surgery",
Comment = "comment",
CustomMethod = "custom_method",
Ink = "ink",
Variable = "variable",
Condition = "condition",
CustomClass = "custom_class",
}
type ValueMode = "constant" | "expression";
type StringStrategy = "prefix" | "quote";
const enumTypeMap: Record<string, string[]> = {
Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"],
Size: ["Small", "Medium", "Large"],
Shape: ["Circle", "Square", "Triangle"],
};
class NumericRange {
min?: number;
max?: number;
isInteger: boolean;
public constructor(min?: number, max?: number, isInteger: boolean = false) {
this.min = min;
this.max = max;
this.isInteger = isInteger;
}
}
class Parameter {
name: string;
type: ParameterType;
enumType?: string;
numericRange?: NumericRange;
description: string;
public constructor(name: string, type: ParameterType, description: string, enumType?: string, numericRange?: NumericRange) {
this.name = name;
this.type = type;
this.description = description;
this.enumType = enumType;
this.numericRange = numericRange;
}
}
class Method {
name: string;
type: MethodType;
shortDescription: string;
description: string;
public constructor(name: string, type: MethodType, shortDescription: string, description: string) {
this.name = name;
this.type = type;
this.shortDescription = shortDescription;
this.description = description;
}
}
class Mod extends Method {
public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.Mod, shortDescription, description);
}
}
class Surgery extends Method {
public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.Surgery, shortDescription, description);
}
}
class CommentMethod extends Method {
parameters: Parameter[];
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Comment, shortDescription, description);
this.parameters = parameters;
}
}
class InkMethod extends Method {
parameters: Parameter[];
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Ink, shortDescription, description);
this.parameters = parameters;
}
}
class CustomMethods extends Method {
parameters: Parameter[];
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.CustomMethod, shortDescription, description);
this.parameters = parameters;
}
}
class VariableMethod extends Method {
parameters: Parameter[];
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Variable, shortDescription, description);
this.parameters = parameters;
}
}
class ConditionMethod extends Method {
parameters: Parameter[];
public constructor(name: string, shortDescription: string, description: string, parameters: Parameter[]) {
super(name, MethodType.Condition, shortDescription, description);
this.parameters = parameters;
}
}
class CustomClassMethod extends Method {
public constructor(name: string, shortDescription: string, description: string) {
super(name, MethodType.CustomClass, shortDescription, description);
}
}
export { editDistance };
function editDistance(a: string, b: string, caseSensitive: boolean = true): number {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix: number[][] = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const ac = a.charAt(j - 1);
const bc = b.charAt(i - 1);
if (caseSensitive ? bc === ac : bc.toLowerCase() === ac.toLowerCase()) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1),
);
}
}
}
return matrix[b.length][a.length];
}
function parametersOf(method: Method): Parameter[] {
return (method as CommentMethod | InkMethod | CustomMethods | VariableMethod | ConditionMethod).parameters ?? [];
}
// 全局策略:字符串写法、数字/布尔默认的常量/表达式状态
const strategies: { string: StringStrategy; number: ValueMode; boolean: ValueMode } = {
string: "prefix",
number: "constant",
boolean: "constant",
};
// ---------------------------------------------------------------------
// 外置数据 hydrate(script/custommethod-data.js)
// ---------------------------------------------------------------------
function hydrateMethod(raw: any): Method {
const shortDescription = raw.shortDescription ?? raw.description ?? "";
const description = raw.description ?? raw.shortDescription ?? "";
const parameters: Parameter[] = Array.isArray(raw.parameters)
? raw.parameters.map((item: any) => new Parameter(
item.name,
item.type ?? ParameterType.String,
item.description ?? "",
item.enumType,
item.numericRange
? new NumericRange(item.numericRange.min, item.numericRange.max, item.numericRange.isInteger)
: undefined,
))
: [];
switch (raw.type) {
case MethodType.Mod: return new Mod(raw.name, shortDescription, description);
case MethodType.Surgery: return new Surgery(raw.name, shortDescription, description);
case MethodType.Comment: return new CommentMethod(raw.name, shortDescription, description, parameters);
case MethodType.Ink: return new InkMethod(raw.name, shortDescription, description, parameters);
case MethodType.Variable: return new VariableMethod(raw.name, shortDescription, description, parameters);
case MethodType.Condition: return new ConditionMethod(raw.name, shortDescription, description, parameters);
case MethodType.CustomClass: return new CustomClassMethod(raw.name, shortDescription, description);
default: return new CustomMethods(raw.name, shortDescription, description, parameters);
}
}
let methods: Method[] = [];
function loadExternalData(): void {
const data = (window as any).CustomMethodSearchData;
if (!data) return;
if (data.enumTypes) {
for (const key of Object.keys(data.enumTypes)) {
enumTypeMap[key] = data.enumTypes[key];
}
}
if (Array.isArray(data.methods)) {
const index = new Map<string, number>();
methods.forEach((method, i) => index.set(method.name.toLowerCase(), i));
for (const raw of data.methods) {
if (!raw || !raw.name) continue;
const name = String(raw.name).toLowerCase();
const hydrated = hydrateMethod(raw);
if (index.has(name)) {
methods[index.get(name)!] = hydrated;
} else {
index.set(name, methods.length);
methods.push(hydrated);
}
}
}
}
loadExternalData();
// ---------------------------------------------------------------------
// 匹配
// ---------------------------------------------------------------------
function searchMethods(query: string): Method[] {
const q = query.trim();
if (!q) return [];
const ql = q.toLowerCase();
const maxDistance = Math.max(2, Math.floor(q.length / 2));
return methods
.map((method) => {
const name = method.name;
const lower = name.toLowerCase();
let score: number;
if (name === q) {
score = 0;
} else if (lower.startsWith(ql)) {
score = 1;
} else if (lower.indexOf(ql) >= 0) {
score = 2;
} else {
score = 3 + editDistance(name, q, false);
}
return { method, score };
})
.filter((item) => item.score <= 2 || item.score <= 3 + maxDistance)
.sort((a, b) => a.score - b.score || a.method.name.localeCompare(b.method.name))
.slice(0, 8)
.map((item) => item.method);
}
// ---------------------------------------------------------------------
// DOM 工具
// ---------------------------------------------------------------------
function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string): HTMLElementTagNameMap[K] {
const node = document.createElement(tag);
if (className) node.className = className;
return node;
}
function resolveEnumOptions(parameter: Parameter): string[] {
if (parameter.enumType && enumTypeMap[parameter.enumType]) return enumTypeMap[parameter.enumType];
if (enumTypeMap[parameter.description]) return enumTypeMap[parameter.description];
if (enumTypeMap[parameter.name]) return enumTypeMap[parameter.name];
return [];
}
// 布尔可用 true/false,或布尔变量 b0~b9
const BOOLEAN_OPTIONS: string[] = ["true", "false"];
for (let i = 0; i < 10; i++) {
BOOLEAN_OPTIONS.push("b" + i);
}
function makeCombo(parameter: Parameter, options: string[], onChange: () => void): HTMLElement {
const combo = el("span", "cm-combo");
const input = el("input", "cm-input cm-combo-input") as HTMLInputElement;
input.type = "text";
input.placeholder = parameter.name;
input.autocomplete = "off";
const list = el("ul", "cm-combo-list");
list.hidden = true;
function populate(): void {
list.textContent = "";
if (options.length === 0) { list.hidden = true; return; }
const query = input.value.trim();
const ql = query.toLowerCase();
const maxDistance = Math.max(1, Math.floor(query.length / 2));
const shown = (query
? options.filter((option) =>
option.toLowerCase().indexOf(ql) >= 0 ||
editDistance(option, query, false) <= maxDistance)
: options
).slice(0, 10);
if (shown.length === 0) { list.hidden = true; return; }
for (const option of shown) {
const item = el("li", "cm-combo-item");
item.textContent = option;
item.title = option;
item.addEventListener("mousedown", (event) => {
event.preventDefault();
input.value = option;
list.hidden = true;
onChange();
});
list.appendChild(item);
}
list.hidden = false;
}
input.addEventListener("input", () => { populate(); onChange(); });
input.addEventListener("focus", populate);
input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); });
combo.appendChild(input);
combo.appendChild(list);
return combo;
}
function makeModeToggle(initial: ValueMode, onChange: () => void): { el: HTMLButtonElement; get: () => ValueMode } {
const button = el("button", "cm-mode") as HTMLButtonElement;
button.type = "button";
let mode = initial;
const render = (): void => {
button.textContent = mode === "expression" ? "表达式" : "常量";
button.title = "切换 常量 / 表达式";
button.classList.toggle("is-expression", mode === "expression");
};
render();
button.addEventListener("click", (event) => {
event.preventDefault();
mode = mode === "expression" ? "constant" : "expression";
render();
onChange();
});
return { el: button, get: () => mode };
}
interface ParamState {
text: string;
valid: boolean;
invalid: boolean;
}
interface Field {
el: HTMLElement;
read: () => ParamState;
}
function stringValue(value: string): string {
if (value.length === 0) return "<文本>";
return strategies.string === "quote" ? `"${value}"` : `str:${value}`;
}
function makeField(parameter: Parameter, onChange: () => void): Field {
switch (parameter.type) {
case ParameterType.Boolean: {
const wrap = el("span", "cm-field");
const checkbox = el("input", "cm-input cm-check") as HTMLInputElement;
checkbox.type = "checkbox";
checkbox.addEventListener("change", onChange);
const combo = makeCombo(parameter, BOOLEAN_OPTIONS, onChange);
const comboInput = combo.querySelector("input") as HTMLInputElement;
let mode: ValueMode = strategies.boolean;
const toggle = makeModeToggle(strategies.boolean, () => {
mode = toggle.get();
applyMode();
onChange();
});
function applyMode(): void {
const expression = mode === "expression";
checkbox.hidden = expression;
combo.hidden = !expression;
}
applyMode();
wrap.appendChild(toggle.el);
wrap.appendChild(checkbox);
wrap.appendChild(combo);
return {
el: wrap,
read: () => {
if (mode === "constant") {
return { text: checkbox.checked ? "true" : "false", valid: true, invalid: false };
}
const value = comboInput.value.trim();
return value.length > 0
? { text: value, valid: true, invalid: false }
: { text: "<布尔>", valid: false, invalid: true };
},
};
}
case ParameterType.Number: {
const wrap = el("span", "cm-field");
const input = el("input", "cm-input") as HTMLInputElement;
input.type = "text";
input.inputMode = "decimal";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
const mode = makeModeToggle(strategies.number, onChange);
wrap.appendChild(mode.el);
wrap.appendChild(input);
return {
el: wrap,
read: () => {
const value = input.value.trim();
if (value.length === 0) return { text: "<数字>", valid: false, invalid: true };
if (mode.get() === "expression") return { text: value, valid: true, invalid: false };
const number = Number(value);
let valid = isFinite(number);
const range = parameter.numericRange;
if (valid && range) {
if (range.isInteger && !Number.isInteger(number)) valid = false;
if (range.min !== undefined && number < range.min) valid = false;
if (range.max !== undefined && number > range.max) valid = false;
}
return { text: value, valid, invalid: !valid };
},
};
}
case ParameterType.Enum: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const combo = makeCombo(parameter, resolveEnumOptions(parameter), onChange);
const input = combo.querySelector("input") as HTMLInputElement;
wrap.appendChild(open);
wrap.appendChild(combo);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
const options = resolveEnumOptions(parameter);
const valid = options.length > 0
? options.some((option) => option.toLowerCase() === value.toLowerCase())
: value.length > 0;
return { text: stringValue(value), valid, invalid: !valid };
},
};
}
default: {
const wrap = el("span", "cm-field cm-field-string");
const open = el("span", "cm-lit");
const close = el("span", "cm-lit");
open.textContent = strategies.string === "quote" ? "\"" : "str:";
close.textContent = strategies.string === "quote" ? "\"" : "";
const input = el("input", "cm-input") as HTMLInputElement;
input.type = "text";
input.placeholder = parameter.name;
input.addEventListener("input", onChange);
wrap.appendChild(open);
wrap.appendChild(input);
wrap.appendChild(close);
return {
el: wrap,
read: () => {
const value = input.value.trim();
return { text: stringValue(value), valid: value.length > 0, invalid: value.length === 0 };
},
};
}
}
}
interface Skeleton {
prefix: string;
suffix: string;
}
function methodSkeleton(method: Method, hasParams: boolean): Skeleton {
switch (method.type) {
case MethodType.Comment: return { prefix: `()=>${method.name}(`, suffix: ")" };
case MethodType.Ink: return { prefix: `DOTHIS{${method.name}(`, suffix: ")}" };
case MethodType.Variable: return { prefix: `${method.name} = `, suffix: "" };
case MethodType.Mod: return { prefix: `"mods": [..., "`, suffix: `"]` };
case MethodType.CustomClass: return { prefix: `"customClass": "`, suffix: `"` };
case MethodType.Condition:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
case MethodType.Surgery: return { prefix: method.name, suffix: "" };
default:
return hasParams ? { prefix: `${method.name}(`, suffix: ")" } : { prefix: method.name, suffix: "" };
}
}
function formatMethod(method: Method, states: ParamState[]): string {
const args = states.map((state) => state.text).join(", ");
switch (method.type) {
case MethodType.Comment: return `()=>${method.name}(${args})`;
case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`;
case MethodType.Variable: return `${method.name} = ${states.length ? states[0].text : ""}`;
case MethodType.Mod: return `"mods": [..., "${method.name}"]`;
case MethodType.CustomClass: return `"customClass": "${method.name}"`;
case MethodType.Condition: return states.length ? `${method.name}(${args})` : method.name;
case MethodType.Surgery: return method.name;
default: return states.length ? `${method.name}(${args})` : method.name;
}
}
async function copyText(text: string): Promise<boolean> {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (error) {
// fall through
}
try {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
const ok = document.execCommand("copy");
document.body.removeChild(area);
return ok;
} catch (error) {
return false;
}
}
// ---------------------------------------------------------------------
// 渲染
// ---------------------------------------------------------------------
function renderContainer(container: HTMLElement): void {
if (container.dataset.cmReady === "1") return;
container.dataset.cmReady = "1";
let currentMethod: Method | null = null;
let query = container.querySelector<HTMLInputElement>(".cm-query");
let suggest = container.querySelector<HTMLUListElement>(".cm-suggest");
let selected = container.querySelector<HTMLElement>(".cm-selected");
let nameEl = container.querySelector<HTMLElement>(".cm-name");
let shortEl = container.querySelector<HTMLElement>(".cm-short");
let call = container.querySelector<HTMLElement>(".cm-call");
let preview = container.querySelector<HTMLElement>(".cm-preview");
let desc = container.querySelector<HTMLElement>(".cm-desc");
let options = container.querySelector<HTMLElement>(".cm-options");
if (!query || !suggest || !selected || !nameEl || !shortEl || !call || !preview || !desc) {
container.textContent = "";
const searchBar = el("div", "cm-search");
query = el("input", "cm-query") as HTMLInputElement;
query.type = "text";
query.placeholder = "输入自定义方法名,如 ChangeCharacter";
query.autocomplete = "off";
suggest = el("ul", "cm-suggest");
searchBar.appendChild(query);
searchBar.appendChild(suggest);
selected = el("div", "cm-selected");
selected.hidden = true;
const head = el("div", "cm-head");
nameEl = el("span", "cm-name");
shortEl = el("span", "cm-short");
head.appendChild(nameEl);
head.appendChild(shortEl);
call = el("div", "cm-call");
preview = el("code", "cm-preview");
desc = el("div", "cm-desc");
selected.appendChild(head);
selected.appendChild(call);
selected.appendChild(preview);
selected.appendChild(desc);
container.appendChild(searchBar);
container.appendChild(selected);
}
// 搜索框下的策略选项
options = container.querySelector<HTMLElement>(".cm-options");
if (!options) {
options = el("div", "cm-options");
(query.parentElement as HTMLElement).insertAdjacentElement("afterend", options);
}
if (!options.querySelector(".cm-option-select")) {
const optionsBox = options;
function addOption(label: string, key: "string" | "number" | "boolean", values: [string, string][], current: string): void {
const wrap = el("label", "cm-option");
const text = el("span", "cm-option-label");
text.textContent = label;
const select = el("select", "cm-option-select") as HTMLSelectElement;
for (const [value, caption] of values) {
const opt = el("option");
opt.value = value;
opt.textContent = caption;
select.appendChild(opt);
}
select.value = current;
select.addEventListener("change", () => {
(strategies as any)[key] = select.value;
if (currentMethod) { selectMethod(currentMethod); }
});
wrap.appendChild(text);
wrap.appendChild(select);
optionsBox.appendChild(wrap);
}
addOption("字符串", "string", [["prefix", "str: 前缀"], ["quote", "引号 \"…\""]], strategies.string);
}
const queryEl = query;
const suggestEl = suggest;
const selectedEl = selected;
const nameNode = nameEl;
const shortNode = shortEl;
const callEl = call;
const previewEl = preview;
const descNode = desc;
const copyBtn = el("button", "cm-copy") as HTMLButtonElement;
copyBtn.type = "button";
copyBtn.textContent = "复制";
copyBtn.hidden = true;
copyBtn.title = "复制生成的代码";
selectedEl.appendChild(copyBtn);
copyBtn.addEventListener("click", () => {
copyText(previewEl.textContent ?? "").then((ok) => {
copyBtn.textContent = ok ? "已复制" : "复制失败";
window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200);
});
});
function clearSuggest(): void {
suggestEl.textContent = "";
}
function selectMethod(method: Method): void {
currentMethod = method;
clearSuggest();
queryEl.value = method.name;
selectedEl.hidden = false;
nameNode.textContent = method.name;
shortNode.textContent = method.shortDescription || "";
descNode.textContent = method.description || "";
callEl.textContent = "";
previewEl.textContent = "";
copyBtn.hidden = true;
const parameters = parametersOf(method);
const fields: Field[] = [];
function updatePreview(): void {
const states = fields.map((field) => field.read());
fields.forEach((field, i) => field.el.classList.toggle("cm-invalid", states[i].invalid));
previewEl.textContent = formatMethod(method, states);
copyBtn.hidden = !states.every((state) => state.valid);
}
const skeleton = methodSkeleton(method, parameters.length > 0);
const code = el("code", "cm-code");
const prefix = el("span", "cm-lit");
prefix.textContent = skeleton.prefix;
code.appendChild(prefix);
parameters.forEach((parameter, index) => {
if (index > 0) {
const sep = el("span", "cm-lit");
sep.textContent = ", ";
code.appendChild(sep);
}
const field = makeField(parameter, updatePreview);
field.el.title = parameter.description;
code.appendChild(field.el);
fields.push(field);
});
if (skeleton.suffix) {
const suffix = el("span", "cm-lit");
suffix.textContent = skeleton.suffix;
code.appendChild(suffix);
}
callEl.appendChild(code);
if (parameters.length === 0) {
const empty = el("span", "cm-empty");
empty.textContent = "(无参数)";
callEl.appendChild(empty);
}
updatePreview();
}
function updateSuggestions(): void {
clearSuggest();
for (const method of searchMethods(queryEl.value)) {
const item = el("li", "cm-suggest-item");
const nameSpan = el("span", "cm-suggest-name");
nameSpan.textContent = method.name;
const descSpan = el("span", "cm-suggest-desc");
descSpan.textContent = method.shortDescription || method.description;
item.appendChild(nameSpan);
item.appendChild(descSpan);
item.addEventListener("mousedown", (event) => {
event.preventDefault();
selectMethod(method);
});
suggestEl.appendChild(item);
}
}
queryEl.addEventListener("input", () => {
updateSuggestions();
selectedEl.hidden = true;
});
queryEl.addEventListener("focus", updateSuggestions);
queryEl.addEventListener("blur", () => {
window.setTimeout(clearSuggest, 120);
});
if (queryEl.value) {
updateSuggestions();
}
}
function initAll(): void {
document.querySelectorAll<HTMLElement>(".custommethods-search").forEach((container) => renderContainer(container));
}
function register(): void {
const docsify = (window as any).$docsify;
if (docsify) {
docsify.plugins = ([] as any[]).concat(docsify.plugins, (hook: any) => {
hook.doneEach(() => initAll());
});
} else if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initAll());
} else {
initAll();
}
}
(window as any).CustomMethodSearch = {
search: searchMethods,
editDistance,
renderAll: initAll,
strategies,
getMethods: () => methods,
setMethods: (list: Method[]) => { methods = list; initAll(); },
Parameter,
ParameterType,
Method,
MethodType,
Mod,
Surgery,
CommentMethod,
InkMethod,
CustomMethods,
VariableMethod,
ConditionMethod,
CustomClassMethod,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", register);
} else {
register();
}
})();
+27
View File
@@ -0,0 +1,27 @@
export declare class IconStyle {
enabled: boolean;
active: boolean;
hover: boolean;
scale: number;
showDuration: boolean;
}
export interface HTMLLayerElement extends HTMLDivElement {
onStateChange?: (style: IconStyle) => void;
filterData: unknown;
updateFilter: () => void;
}
export interface HTMLEventElement extends HTMLDivElement {
onStateChange: (style: IconStyle) => void;
eventStyle: IconStyle;
eventData: object;
moveTo: (x: number, y: number) => void;
}
export declare function createElementEvent(
obj: any,
style: IconStyle,
): HTMLEventElement | null;
export declare function colorOf(tab: string): any;
+965 -391
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
import "./rdview.js";
import "./rdtkview_render.js";
import "./rdview_render.js";
window.loadRDView();
+312 -583
View File
File diff suppressed because it is too large Load Diff
+293 -616
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
window.$docsify.plugins = [].concat(window.$docsify.plugins, (hook) => {
hook.doneEach(() => {
if (typeof window.RDViewRender !== "function") {
return;
}
const codeBlocks = document.querySelectorAll("pre[data-lang]");
codeBlocks.forEach((codeBlock) => {
const lang = codeBlock.getAttribute("data-lang") || "";
if (!lang.trim().startsWith("rdview")) {
return;
}
const codeEl = codeBlock.querySelector("code");
const code = codeEl ? codeEl.innerText : codeBlock.innerText;
let node;
try {
node = window.RDViewRender(lang, code);
} catch (e) {
console.error("rdview render failed:", lang, e);
return;
}
if (!node) {
return;
}
const parent = document.createElement("div");
parent.className = "rdtk-rdview-wrap";
parent.appendChild(node);
codeBlock.replaceWith(parent);
});
});
});
+43 -46
View File
@@ -228,7 +228,7 @@ function rdview() {
function initCanvas() {
const lang = textarea.value.split("\n")[0];
const code = textarea.value.split("\n").slice(1).join("\n");
rdcanvas.innerHTML = RDViewRender(lang, code);
rdcanvas.replaceChildren(RDViewRender(lang, code));
}
initCanvas();
initLineNumbers();
@@ -322,55 +322,52 @@ function rdview2() {
}
const eventStyle = { "scale": 2.0, "showDuration": false };
let maxy = 0;
const elemPromises = [];
const elems = [];
for (let obj of objs) {
const style = { ...eventStyle, enabled: obj.active ?? true };
const elem = createElementEvent(obj, style).then((elem) => {
elem.eventStyle = style;
elem.style.position = "absolute";
elem.style.left = `${
((((obj.bar ?? 1) - 1) * 8) + (obj.beat ?? 1) - 1) * 14 *
eventStyle.scale
}px`;
elem.style.top = `${
(obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale
}px`;
if (
((obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale) +
elem.offsetHeight > maxy
) {
maxy = ((obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale) +
elem.offsetHeight;
}
elem.addEventListener("mousedown", (event) => {
const eStyle = elem.eventStyle;
eStyle.active = true;
elem.onStateChange(eStyle);
});
elem.addEventListener("mouseover", (event) => {
const eStyle = elem.eventStyle;
eStyle.hover = true;
elem.onStateChange(eStyle);
});
elem.addEventListener("mouseout", (event) => {
const eStyle = elem.eventStyle;
eStyle.hover = false;
eStyle.active = false;
elem.onStateChange(eStyle);
});
elem.addEventListener("mouseup", (event) => {
const eStyle = elem.eventStyle;
eStyle.active = false;
elem.onStateChange(eStyle);
});
return elem;
const elem = createElementEvent(obj, style);
if (!elem) continue;
elem.eventStyle = style;
elem.style.position = "absolute";
elem.style.left = `${
((((obj.bar ?? 1) - 1) * 8) + (obj.beat ?? 1) - 1) * 14 *
eventStyle.scale
}px`;
elem.style.top = `${
(obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale
}px`;
if (
((obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale) +
elem.offsetHeight > maxy
) {
maxy = ((obj.y ?? obj.row ?? 0) * 14 * eventStyle.scale) +
elem.offsetHeight;
}
elem.addEventListener("mousedown", (event) => {
const eStyle = elem.eventStyle;
eStyle.active = true;
elem.onStateChange(eStyle);
});
elemPromises.push(elem);
elem.addEventListener("mouseover", (event) => {
const eStyle = elem.eventStyle;
eStyle.hover = true;
elem.onStateChange(eStyle);
});
elem.addEventListener("mouseout", (event) => {
const eStyle = elem.eventStyle;
eStyle.hover = false;
eStyle.active = false;
elem.onStateChange(eStyle);
});
elem.addEventListener("mouseup", (event) => {
const eStyle = elem.eventStyle;
eStyle.active = false;
elem.onStateChange(eStyle);
});
elems.push(elem);
}
Promise.all(elemPromises).then((elems) => {
elems.forEach((elem) => {
rdcanvas.appendChild(elem);
});
elems.forEach((elem) => {
rdcanvas.appendChild(elem);
});
rdcanvas.style.position = "relative";
rdcanvas.style.height = `${maxy + 4 * 14 * eventStyle.scale}px`;
+6 -4
View File
@@ -3,10 +3,11 @@
"target": "es2015",
"lib": [
"es2017",
"dom"
"dom",
"dom.iterable"
],
"module": "es6",
"moduleResolution": "node",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@@ -14,6 +15,7 @@
"resolveJsonModule": true
},
"files": [
"rdview.ts"
"rdview.ts",
"custommethod-search.ts"
],
}
+66
View File
@@ -0,0 +1,66 @@
.rdtk-rdview-wrap {
margin: 0;
}
.rdtk-rdview {
position: relative;
overflow: visible;
}
.rdtk-event {
position: absolute;
cursor: pointer;
z-index: 0;
transition: transform 0.3s, filter 0.3s;
}
.rdtk-event.hover {
z-index: 2;
}
.rdtk-group {
position: absolute;
outline-style: solid;
outline-width: 2px;
transition: transform 0.3s, filter 0.3s, outline-color 0.3s;
}
.rdtk-group.hover {
z-index: 1;
}
.rdtk-condition {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 12px;
background-size: contain;
background-repeat: no-repeat;
pointer-events: none;
z-index: 5;
}
.rdtk-condition.condition-true {
background-image: url(./assets/condition-true.png);
}
.rdtk-condition.condition-false {
background-image: url(./assets/condition-false.png);
}
.rdtk-condition.condition-both {
background-image: url(./assets/condition-both.png);
}
.rdtk-unknown {
color: red;
font-weight: bold;
font-size: 12px;
}
.rdtk-error {
color: red;
font-weight: bold;
white-space: pre-wrap;
}
+311
View File
@@ -266,4 +266,315 @@ div.decoration-depth {
}
}
}
}
div.custommethods-search {
position: relative;
margin: 16px 0;
& .cm-search {
position: relative;
}
& .cm-query {
box-sizing: border-box;
width: 100%;
padding: 8px 12px;
font-family: d9c, d9, monospace;
font-size: 15px;
color: inherit;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 6px;
&:focus {
outline: none;
border-color: #3b8e59;
}
}
& .cm-suggest {
position: absolute;
left: 0;
right: 0;
top: calc(100% + 4px);
z-index: 30;
margin: 0;
padding: 4px;
list-style: none;
max-height: 260px;
overflow-y: auto;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 6px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
&:empty {
display: none;
}
}
& .cm-suggest-item {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: rgba(127, 127, 127, 0.16);
}
}
& .cm-suggest-name {
font-family: d9c, d9, monospace;
}
& .cm-suggest-desc {
font-size: 12px;
opacity: 0.6;
text-align: right;
}
& .cm-selected {
position: relative;
margin-top: 10px;
padding: 12px;
border: 1px solid var(--background-border-color);
border-radius: 6px;
background-color: rgba(127, 127, 127, 0.06);
}
& .cm-head {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 10px;
padding-right: 56px;
}
& .cm-name {
font-family: d9c, d9, monospace;
font-weight: bold;
}
& .cm-desc {
font-size: 12px;
opacity: 0.6;
}
& .cm-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
& .cm-param {
display: inline-flex;
align-items: center;
gap: 6px;
}
& .cm-param-name {
font-family: d9c, d9, monospace;
font-size: 13px;
opacity: 0.8;
}
& .cm-input {
padding: 4px 6px;
font-size: 14px;
color: inherit;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 4px;
&:focus {
outline: none;
border-color: #3b8e59;
}
}
& select.cm-input {
min-width: 90px;
}
& input.cm-input[type="number"] {
width: 96px;
}
& input.cm-input[type="text"] {
width: 150px;
}
& .cm-preview {
display: block;
margin-top: 12px;
padding: 8px 10px;
font-family: d9c, d9, monospace;
white-space: pre-wrap;
word-break: break-all;
background-color: rgba(127, 127, 127, 0.12);
border-radius: 6px;
}
& .cm-empty {
font-size: 13px;
opacity: 0.6;
}
& .cm-input.cm-invalid,
& .cm-combo.cm-invalid .cm-input {
border-color: #e5484d;
box-shadow: 0 0 0 1px #e5484d;
}
& .cm-combo {
position: relative;
display: inline-block;
}
& .cm-combo-input {
width: 150px;
}
& .cm-combo-list {
position: absolute;
top: calc(100% + 2px);
left: 0;
min-width: 100%;
z-index: 40;
margin: 0;
padding: 4px;
list-style: none;
max-height: 200px;
overflow-y: auto;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 4px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
& .cm-combo-item {
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: rgba(127, 127, 127, 0.16);
}
}
& .cm-copy {
position: absolute;
top: 8px;
right: 8px;
padding: 2px 10px;
font-size: 12px;
color: inherit;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 10px;
cursor: pointer;
&:hover {
color: #3b8e59;
border-color: #3b8e59;
}
}
& .cm-options {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 6px;
font-size: 12px;
}
& .cm-option {
display: inline-flex;
align-items: center;
gap: 4px;
opacity: 0.85;
}
& .cm-option-select {
padding: 2px 4px;
font-size: 12px;
color: inherit;
background-color: var(--background-color);
border: 1px solid var(--background-border-color);
border-radius: 4px;
}
& .cm-short {
font-size: 12px;
opacity: 0.7;
}
& .cm-call {
margin-top: 8px;
}
& .cm-code {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
padding: 8px 10px;
font-family: d9c, d9, monospace;
background-color: rgba(127, 127, 127, 0.12);
border-radius: 6px;
}
& .cm-lit {
font-family: d9c, d9, monospace;
opacity: 0.85;
white-space: pre;
}
& .cm-field {
display: inline-flex;
align-items: center;
gap: 3px;
& [hidden] {
display: none !important;
}
}
& .cm-field.cm-invalid .cm-input {
border-color: #e5484d;
box-shadow: 0 0 0 1px #e5484d;
}
& .cm-check {
width: auto;
}
& .cm-desc {
margin-top: 8px;
font-size: 12px;
line-height: 1.5;
opacity: 0.75;
white-space: pre-wrap;
}
& .cm-mode {
padding: 1px 6px;
font-size: 11px;
color: inherit;
background-color: transparent;
border: 1px solid var(--background-border-color);
border-radius: 8px;
cursor: pointer;
&.is-expression {
color: #3b8e59;
border-color: #3b8e59;
}
}
}
-1
View File
@@ -18,7 +18,6 @@
"如果你的窗口太抖点不到暂停键的话,就按 P 键来暂停吧!",
"单发拍子界面按住Shift + H就能看到巨大爱心拍子~❤",
"4-4关的~抖动轨道~用的是自定义方法:wobblyLines = true",
"关卡内的屏幕像素大小是352*198px",
"352*198px记住了吗",
"针管图标的像素大小是19*15或者其整倍数",