sonolus-next-rush-plus-engine 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/EngineConfiguration +0 -0
- package/dist/EnginePlayData +0 -0
- package/dist/EnginePreviewData +0 -0
- package/dist/EngineRom +0 -0
- package/dist/EngineTutorialData +0 -0
- package/dist/EngineWatchData +0 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +31 -0
- package/dist/sus/analyze.d.ts +32 -0
- package/dist/sus/analyze.js +251 -0
- package/dist/sus/convert.d.ts +5 -0
- package/dist/sus/convert.js +320 -0
- package/dist/thumbnail.png +0 -0
- package/dist/usc/convert.d.ts +3 -0
- package/dist/usc/convert.js +472 -0
- package/dist/usc/index.d.ts +96 -0
- package/dist/usc/index.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/EngineRom
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { susToUSC } from './sus/convert.js';
|
|
2
|
+
export { uscToLevelData } from './usc/convert.js';
|
|
3
|
+
export * from './usc/index.js';
|
|
4
|
+
export declare const version = "1.1.6";
|
|
5
|
+
export declare const databaseEngineItem: {
|
|
6
|
+
readonly name: "next-rush-plus";
|
|
7
|
+
readonly version: 13;
|
|
8
|
+
readonly title: {
|
|
9
|
+
readonly en: "Next RUSH+";
|
|
10
|
+
};
|
|
11
|
+
readonly subtitle: {
|
|
12
|
+
readonly en: "Next RUSH+";
|
|
13
|
+
};
|
|
14
|
+
readonly author: {
|
|
15
|
+
readonly en: "Next RUSH+";
|
|
16
|
+
};
|
|
17
|
+
readonly description: {
|
|
18
|
+
readonly en: string;
|
|
19
|
+
readonly ko: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { susToUSC } from './sus/convert.js';
|
|
2
|
+
export { uscToLevelData } from './usc/convert.js';
|
|
3
|
+
export * from './usc/index.js';
|
|
4
|
+
export const version = '1.1.6';
|
|
5
|
+
export const databaseEngineItem = {
|
|
6
|
+
name: 'next-rush-plus',
|
|
7
|
+
version: 13,
|
|
8
|
+
title: {
|
|
9
|
+
en: 'Next RUSH+',
|
|
10
|
+
},
|
|
11
|
+
subtitle: {
|
|
12
|
+
en: 'Next RUSH+',
|
|
13
|
+
},
|
|
14
|
+
author: {
|
|
15
|
+
en: 'Next RUSH+',
|
|
16
|
+
},
|
|
17
|
+
description: {
|
|
18
|
+
en: [
|
|
19
|
+
"Next SEKAI's expansion engine",
|
|
20
|
+
`Version: ${version}`,
|
|
21
|
+
'',
|
|
22
|
+
'https://github.com/Next-SEKAI/sonolus-next-sekai-engine',
|
|
23
|
+
].join('\n'),
|
|
24
|
+
ko: [
|
|
25
|
+
'Next SEKAI의 확장 엔진',
|
|
26
|
+
`버전: ${version}`,
|
|
27
|
+
'',
|
|
28
|
+
'https://github.com/Next-SEKAI/sonolus-next-sekai-engine',
|
|
29
|
+
].join('\n'),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type Meta = Map<string, string[]>;
|
|
2
|
+
export type TimeScaleChangeObject = {
|
|
3
|
+
tick: number;
|
|
4
|
+
timeScale: number;
|
|
5
|
+
};
|
|
6
|
+
export type BpmChangeObject = {
|
|
7
|
+
tick: number;
|
|
8
|
+
bpm: number;
|
|
9
|
+
};
|
|
10
|
+
export type NoteObject = {
|
|
11
|
+
tick: number;
|
|
12
|
+
lane: number;
|
|
13
|
+
width: number;
|
|
14
|
+
type: number;
|
|
15
|
+
timeScaleGroup: number;
|
|
16
|
+
};
|
|
17
|
+
export type SlideObject = {
|
|
18
|
+
type: number;
|
|
19
|
+
notes: NoteObject[];
|
|
20
|
+
};
|
|
21
|
+
export type Score = {
|
|
22
|
+
offset: number;
|
|
23
|
+
ticksPerBeat: number;
|
|
24
|
+
timeScaleChanges: TimeScaleChangeObject[][];
|
|
25
|
+
bpmChanges: BpmChangeObject[];
|
|
26
|
+
tapNotes: NoteObject[];
|
|
27
|
+
directionalNotes: NoteObject[];
|
|
28
|
+
slides: SlideObject[];
|
|
29
|
+
meta: Meta;
|
|
30
|
+
};
|
|
31
|
+
export declare const analyze: (sus: string) => Score;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
export const analyze = (sus) => {
|
|
2
|
+
if (!sus || sus.trim().length === 0) {
|
|
3
|
+
throw new Error('The sus file does not exist');
|
|
4
|
+
}
|
|
5
|
+
const { lines, measureChanges, timeScaleGroupChanges, meta } = parse(sus);
|
|
6
|
+
const offset = -+(meta.get('WAVEOFFSET') || '0');
|
|
7
|
+
if (Number.isNaN(offset))
|
|
8
|
+
throw new Error('Unexpected offset');
|
|
9
|
+
const ticksPerBeat = getTicksPerBeat(meta);
|
|
10
|
+
if (!ticksPerBeat)
|
|
11
|
+
throw new Error('Missing or unexpected ticks per beat');
|
|
12
|
+
const barLengths = getBarLengths(lines, measureChanges);
|
|
13
|
+
const toTick = getToTick(barLengths, ticksPerBeat);
|
|
14
|
+
const bpms = new Map();
|
|
15
|
+
const bpmChanges = [];
|
|
16
|
+
const timeScaleGroups = new Map();
|
|
17
|
+
const timeScaleChanges = [];
|
|
18
|
+
const tapNotes = [];
|
|
19
|
+
const directionalNotes = [];
|
|
20
|
+
const streams = new Map();
|
|
21
|
+
for (const [, timeScaleGroup] of timeScaleGroupChanges) {
|
|
22
|
+
if (timeScaleGroups.has(timeScaleGroup))
|
|
23
|
+
continue;
|
|
24
|
+
timeScaleGroups.set(timeScaleGroup, timeScaleGroups.size);
|
|
25
|
+
timeScaleChanges.push([]);
|
|
26
|
+
}
|
|
27
|
+
// Time Scale Changes
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const [header] = line;
|
|
30
|
+
if (header.length === 5 && header.startsWith('TIL')) {
|
|
31
|
+
const timeScaleGroup = header.substring(3, 5);
|
|
32
|
+
const timeScaleIndex = timeScaleGroups.get(timeScaleGroup);
|
|
33
|
+
if (timeScaleIndex === undefined) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
timeScaleChanges[timeScaleIndex].push(...toTimeScaleChanges(line, toTick));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
lines.forEach((line, index) => {
|
|
40
|
+
const [header, data] = line;
|
|
41
|
+
const measureOffset = measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0;
|
|
42
|
+
const timeScaleGroupName = timeScaleGroupChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? '00';
|
|
43
|
+
let timeScaleGroup = timeScaleGroups.get(timeScaleGroupName);
|
|
44
|
+
if (timeScaleGroup === undefined) {
|
|
45
|
+
timeScaleGroup = timeScaleGroups.size;
|
|
46
|
+
timeScaleGroups.set(timeScaleGroupName, timeScaleGroups.size);
|
|
47
|
+
timeScaleChanges.push([]);
|
|
48
|
+
}
|
|
49
|
+
// Hispeed definitions
|
|
50
|
+
if (header.length === 5 && header.startsWith('TIL')) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// BPM
|
|
54
|
+
if (header.length === 5 && header.startsWith('BPM')) {
|
|
55
|
+
bpms.set(header.substring(3), +data);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// BPM Changes
|
|
59
|
+
if (header.length === 5 && header.endsWith('08')) {
|
|
60
|
+
bpmChanges.push(...toBpmChanges(line, measureOffset, bpms, toTick));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Tap Notes
|
|
64
|
+
if (header.length === 5 && header[3] === '1') {
|
|
65
|
+
tapNotes.push(...toNotes(line, measureOffset, timeScaleGroup, toTick));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Streams
|
|
69
|
+
if (header.length === 6 && (header[3] === '3' || header[3] === '9')) {
|
|
70
|
+
const key = `${header[5]}-${header[3]}`;
|
|
71
|
+
const stream = streams.get(key);
|
|
72
|
+
if (stream) {
|
|
73
|
+
stream.notes.push(...toNotes(line, measureOffset, timeScaleGroup, toTick));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
streams.set(key, {
|
|
77
|
+
type: +header[3],
|
|
78
|
+
notes: toNotes(line, measureOffset, timeScaleGroup, toTick),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Directional Notes
|
|
84
|
+
if (header.length === 5 && header[3] === '5') {
|
|
85
|
+
directionalNotes.push(...toNotes(line, measureOffset, timeScaleGroup, toTick));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const slides = [...streams.values()].flatMap(toSlides);
|
|
90
|
+
return {
|
|
91
|
+
offset,
|
|
92
|
+
ticksPerBeat,
|
|
93
|
+
timeScaleChanges,
|
|
94
|
+
bpmChanges,
|
|
95
|
+
tapNotes,
|
|
96
|
+
directionalNotes,
|
|
97
|
+
slides,
|
|
98
|
+
meta,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const parse = (sus) => {
|
|
102
|
+
const lines = [];
|
|
103
|
+
const measureChanges = [];
|
|
104
|
+
const timeScaleGroupChanges = [];
|
|
105
|
+
const meta = new Map();
|
|
106
|
+
sus.split('\n')
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => line.startsWith('#'))
|
|
109
|
+
.forEach((line) => {
|
|
110
|
+
const isLine = line.includes(':');
|
|
111
|
+
const index = line.indexOf(isLine ? ':' : ' ');
|
|
112
|
+
if (index === -1)
|
|
113
|
+
return;
|
|
114
|
+
const left = line.substring(1, index).trim();
|
|
115
|
+
const right = line.substring(index + 1).trim();
|
|
116
|
+
if (isLine) {
|
|
117
|
+
lines.push([left, right]);
|
|
118
|
+
}
|
|
119
|
+
else if (left === 'MEASUREBS') {
|
|
120
|
+
measureChanges.unshift([lines.length, +right]);
|
|
121
|
+
}
|
|
122
|
+
else if (left === 'HISPEED') {
|
|
123
|
+
timeScaleGroupChanges.unshift([lines.length, right]);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!meta.has(left))
|
|
127
|
+
meta.set(left, []);
|
|
128
|
+
meta.get(left)?.push(right);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
lines,
|
|
133
|
+
measureChanges,
|
|
134
|
+
timeScaleGroupChanges,
|
|
135
|
+
meta,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const getTicksPerBeat = (meta) => {
|
|
139
|
+
const request = meta.get('REQUEST');
|
|
140
|
+
if (!request)
|
|
141
|
+
return;
|
|
142
|
+
const tpbRequest = request.find((r) => JSON.parse(r).startsWith('ticks_per_beat'));
|
|
143
|
+
if (!tpbRequest)
|
|
144
|
+
return;
|
|
145
|
+
return +JSON.parse(tpbRequest).split(' ')[1];
|
|
146
|
+
};
|
|
147
|
+
const getBarLengths = (lines, measureChanges) => {
|
|
148
|
+
const barLengths = [];
|
|
149
|
+
lines.forEach((line, index) => {
|
|
150
|
+
const [header, data] = line;
|
|
151
|
+
if (header.length !== 5)
|
|
152
|
+
return;
|
|
153
|
+
if (!header.endsWith('02'))
|
|
154
|
+
return;
|
|
155
|
+
const measure = +header.substring(0, 3) +
|
|
156
|
+
(measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0);
|
|
157
|
+
if (Number.isNaN(measure))
|
|
158
|
+
return;
|
|
159
|
+
barLengths.push({ measure, length: +data });
|
|
160
|
+
});
|
|
161
|
+
return barLengths;
|
|
162
|
+
};
|
|
163
|
+
const getToTick = (barLengths, ticksPerBeat) => {
|
|
164
|
+
let ticks = 0;
|
|
165
|
+
const bars = barLengths
|
|
166
|
+
.sort((a, b) => a.measure - b.measure)
|
|
167
|
+
.map(({ measure, length }, i, values) => {
|
|
168
|
+
if (i) {
|
|
169
|
+
const prev = values[i - 1];
|
|
170
|
+
ticks += (measure - prev.measure) * prev.length * ticksPerBeat;
|
|
171
|
+
}
|
|
172
|
+
return { measure, ticksPerMeasure: length * ticksPerBeat, ticks };
|
|
173
|
+
})
|
|
174
|
+
.reverse();
|
|
175
|
+
return (measure, p, q) => {
|
|
176
|
+
const bar = bars.find((bar) => measure >= bar.measure);
|
|
177
|
+
if (!bar)
|
|
178
|
+
throw new Error('Unexpected missing bar');
|
|
179
|
+
return (bar.ticks +
|
|
180
|
+
(measure - bar.measure) * bar.ticksPerMeasure +
|
|
181
|
+
(p * bar.ticksPerMeasure) / q);
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
const toBpmChanges = (line, measureOffset, bpms, toTick) => toRaws(line, measureOffset, toTick).map(({ tick, value }) => ({
|
|
185
|
+
tick,
|
|
186
|
+
bpm: bpms.get(value) ?? 0,
|
|
187
|
+
}));
|
|
188
|
+
const toTimeScaleChanges = ([, data], toTick) => {
|
|
189
|
+
if (!data.startsWith('"') || !data.endsWith('"'))
|
|
190
|
+
throw new Error('Unexpected time scale changes');
|
|
191
|
+
return data
|
|
192
|
+
.slice(1, -1)
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((segment) => segment.trim())
|
|
195
|
+
.filter((segment) => !!segment)
|
|
196
|
+
.map((segment) => {
|
|
197
|
+
const [l, rest] = segment.split("'");
|
|
198
|
+
const [m, r] = rest.split(':');
|
|
199
|
+
const measure = +l;
|
|
200
|
+
const tick = +m;
|
|
201
|
+
const timeScale = +r;
|
|
202
|
+
if (Number.isNaN(measure) || Number.isNaN(tick) || Number.isNaN(timeScale))
|
|
203
|
+
throw new Error('Unexpected time scale change');
|
|
204
|
+
return {
|
|
205
|
+
tick: toTick(measure, 0, 1) + tick,
|
|
206
|
+
timeScale,
|
|
207
|
+
};
|
|
208
|
+
})
|
|
209
|
+
.sort((a, b) => a.tick - b.tick);
|
|
210
|
+
};
|
|
211
|
+
const toNotes = (line, measureOffset, timeScaleGroup, toTick) => {
|
|
212
|
+
const [header] = line;
|
|
213
|
+
const lane = parseInt(header[4], 36);
|
|
214
|
+
return toRaws(line, measureOffset, toTick).map(({ tick, value }) => {
|
|
215
|
+
const width = parseInt(value[1], 36);
|
|
216
|
+
return {
|
|
217
|
+
tick,
|
|
218
|
+
lane,
|
|
219
|
+
width,
|
|
220
|
+
type: parseInt(value[0], 36),
|
|
221
|
+
timeScaleGroup,
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const toSlides = (stream) => {
|
|
226
|
+
const slides = [];
|
|
227
|
+
let notes;
|
|
228
|
+
for (const note of stream.notes.sort((a, b) => a.tick - b.tick)) {
|
|
229
|
+
if (!notes) {
|
|
230
|
+
notes = [];
|
|
231
|
+
slides.push({
|
|
232
|
+
type: stream.type,
|
|
233
|
+
notes,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
notes.push(note);
|
|
237
|
+
if (note.type === 2) {
|
|
238
|
+
notes = undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return slides;
|
|
242
|
+
};
|
|
243
|
+
const toRaws = ([header, data], measureOffset, toTick) => {
|
|
244
|
+
const measure = +header.substring(0, 3) + measureOffset;
|
|
245
|
+
return (data.match(/.{2}/g) ?? [])
|
|
246
|
+
.map((value, i, values) => value !== '00' && {
|
|
247
|
+
tick: toTick(measure, i, values.length),
|
|
248
|
+
value,
|
|
249
|
+
})
|
|
250
|
+
.filter((object) => !!object);
|
|
251
|
+
};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { analyze } from './analyze.js';
|
|
2
|
+
/** Convert a SUS to a USC */
|
|
3
|
+
export const susToUSC = (sus) => chsLikeToUSC(analyze(sus));
|
|
4
|
+
export const chsLikeToUSC = (score) => {
|
|
5
|
+
const flickMods = new Map();
|
|
6
|
+
const traceMods = new Set();
|
|
7
|
+
const criticalMods = new Set();
|
|
8
|
+
const tickRemoveMods = new Set();
|
|
9
|
+
const slideStartEndRemoveMods = new Set();
|
|
10
|
+
const easeMods = new Map();
|
|
11
|
+
const preventSingles = new Set();
|
|
12
|
+
const dedupeSingles = new Set();
|
|
13
|
+
const dedupeSlides = new Map();
|
|
14
|
+
const requests = {
|
|
15
|
+
sideLane: false,
|
|
16
|
+
laneOffset: 0,
|
|
17
|
+
};
|
|
18
|
+
const requestsRaw = score.meta.get('REQUEST');
|
|
19
|
+
if (requestsRaw) {
|
|
20
|
+
for (const request of requestsRaw) {
|
|
21
|
+
try {
|
|
22
|
+
const [key, value] = JSON.parse(request).split(' ', 2);
|
|
23
|
+
switch (key) {
|
|
24
|
+
case 'side_lane':
|
|
25
|
+
requests.sideLane = value === 'true';
|
|
26
|
+
break;
|
|
27
|
+
case 'lane_offset':
|
|
28
|
+
requests.laneOffset = Number(value);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (_e) {
|
|
33
|
+
// Noop
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const slide of score.slides) {
|
|
38
|
+
if (slide.type !== 3)
|
|
39
|
+
continue;
|
|
40
|
+
for (const note of slide.notes) {
|
|
41
|
+
const key = getKey(note);
|
|
42
|
+
switch (note.type) {
|
|
43
|
+
case 1:
|
|
44
|
+
case 2:
|
|
45
|
+
case 3:
|
|
46
|
+
case 5:
|
|
47
|
+
preventSingles.add(key);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const note of score.directionalNotes) {
|
|
53
|
+
const key = getKey(note);
|
|
54
|
+
switch (note.type) {
|
|
55
|
+
case 1:
|
|
56
|
+
flickMods.set(key, 'up');
|
|
57
|
+
break;
|
|
58
|
+
case 3:
|
|
59
|
+
flickMods.set(key, 'left');
|
|
60
|
+
break;
|
|
61
|
+
case 4:
|
|
62
|
+
flickMods.set(key, 'right');
|
|
63
|
+
break;
|
|
64
|
+
case 2:
|
|
65
|
+
easeMods.set(key, 'in');
|
|
66
|
+
break;
|
|
67
|
+
case 5:
|
|
68
|
+
case 6:
|
|
69
|
+
easeMods.set(key, 'out');
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const note of score.tapNotes) {
|
|
74
|
+
const key = getKey(note);
|
|
75
|
+
switch (note.type) {
|
|
76
|
+
case 2:
|
|
77
|
+
criticalMods.add(key);
|
|
78
|
+
break;
|
|
79
|
+
case 5:
|
|
80
|
+
traceMods.add(key);
|
|
81
|
+
break;
|
|
82
|
+
case 6:
|
|
83
|
+
traceMods.add(key);
|
|
84
|
+
criticalMods.add(key);
|
|
85
|
+
break;
|
|
86
|
+
case 3:
|
|
87
|
+
tickRemoveMods.add(key);
|
|
88
|
+
break;
|
|
89
|
+
case 7:
|
|
90
|
+
slideStartEndRemoveMods.add(key);
|
|
91
|
+
break;
|
|
92
|
+
case 8:
|
|
93
|
+
criticalMods.add(key);
|
|
94
|
+
slideStartEndRemoveMods.add(key);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const objects = [];
|
|
99
|
+
for (const timeScaleChanges of score.timeScaleChanges) {
|
|
100
|
+
objects.push({
|
|
101
|
+
type: 'timeScaleGroup',
|
|
102
|
+
changes: timeScaleChanges.map((timeScaleChange) => ({
|
|
103
|
+
beat: timeScaleChange.tick / score.ticksPerBeat,
|
|
104
|
+
timeScale: timeScaleChange.timeScale,
|
|
105
|
+
})),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const bpmChange of score.bpmChanges) {
|
|
109
|
+
objects.push({
|
|
110
|
+
type: 'bpm',
|
|
111
|
+
beat: bpmChange.tick / score.ticksPerBeat,
|
|
112
|
+
bpm: bpmChange.bpm,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const note of score.tapNotes) {
|
|
116
|
+
if (note.lane === 0 && note.type === 4) {
|
|
117
|
+
objects.push({
|
|
118
|
+
type: 'skill',
|
|
119
|
+
beat: note.tick / score.ticksPerBeat,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (note.lane === 15 && note.type === 1) {
|
|
124
|
+
objects.push({
|
|
125
|
+
type: 'feverChance',
|
|
126
|
+
beat: note.tick / score.ticksPerBeat,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (note.lane === 15 && note.type === 2) {
|
|
131
|
+
objects.push({
|
|
132
|
+
type: 'feverStart',
|
|
133
|
+
beat: note.tick / score.ticksPerBeat,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!requests.sideLane && (note.lane <= 1 || note.lane >= 14))
|
|
138
|
+
continue;
|
|
139
|
+
if (note.type !== 1 &&
|
|
140
|
+
note.type !== 2 &&
|
|
141
|
+
note.type !== 5 &&
|
|
142
|
+
note.type !== 6 &&
|
|
143
|
+
note.type !== 4)
|
|
144
|
+
continue;
|
|
145
|
+
const key = getKey(note);
|
|
146
|
+
if (preventSingles.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
if (dedupeSingles.has(key))
|
|
149
|
+
continue;
|
|
150
|
+
dedupeSingles.add(key);
|
|
151
|
+
let object;
|
|
152
|
+
switch (note.type) {
|
|
153
|
+
case 1:
|
|
154
|
+
case 2:
|
|
155
|
+
case 5:
|
|
156
|
+
case 6: {
|
|
157
|
+
object = {
|
|
158
|
+
type: 'single',
|
|
159
|
+
beat: note.tick / score.ticksPerBeat,
|
|
160
|
+
lane: note.lane - 8 + note.width / 2 + requests.laneOffset,
|
|
161
|
+
size: note.width / 2,
|
|
162
|
+
critical: [2, 6].includes(note.type),
|
|
163
|
+
trace: [5, 6].includes(note.type),
|
|
164
|
+
timeScaleGroup: note.timeScaleGroup,
|
|
165
|
+
};
|
|
166
|
+
const flickMod = flickMods.get(key);
|
|
167
|
+
if (flickMod)
|
|
168
|
+
object.direction = flickMod;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 4:
|
|
172
|
+
object = {
|
|
173
|
+
type: 'damage',
|
|
174
|
+
beat: note.tick / score.ticksPerBeat,
|
|
175
|
+
lane: note.lane - 8 + note.width / 2 + requests.laneOffset,
|
|
176
|
+
size: note.width / 2,
|
|
177
|
+
timeScaleGroup: note.timeScaleGroup,
|
|
178
|
+
};
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
objects.push(object);
|
|
184
|
+
}
|
|
185
|
+
for (const slide of score.slides) {
|
|
186
|
+
const startNote = slide.notes.find(({ type }) => type === 1 || type === 2);
|
|
187
|
+
if (!startNote)
|
|
188
|
+
continue;
|
|
189
|
+
const object = {
|
|
190
|
+
type: slide.type === 3 ? 'slide' : 'guide',
|
|
191
|
+
critical: criticalMods.has(getKey(startNote)),
|
|
192
|
+
connections: [],
|
|
193
|
+
};
|
|
194
|
+
for (const note of slide.notes) {
|
|
195
|
+
const key = getKey(note);
|
|
196
|
+
const beat = note.tick / score.ticksPerBeat;
|
|
197
|
+
const lane = note.lane - 8 + note.width / 2 + requests.laneOffset;
|
|
198
|
+
const size = note.width / 2;
|
|
199
|
+
const timeScaleGroup = note.timeScaleGroup;
|
|
200
|
+
const critical = ('critical' in object && object.critical) || criticalMods.has(key);
|
|
201
|
+
const ease = easeMods.get(key) ?? 'linear';
|
|
202
|
+
let judgeType = 'normal';
|
|
203
|
+
if (traceMods.has(key))
|
|
204
|
+
judgeType = 'trace';
|
|
205
|
+
switch (note.type) {
|
|
206
|
+
case 1: {
|
|
207
|
+
if (object.type == 'guide' || slideStartEndRemoveMods.has(key)) {
|
|
208
|
+
const connection = {
|
|
209
|
+
type: 'start',
|
|
210
|
+
beat,
|
|
211
|
+
lane,
|
|
212
|
+
size,
|
|
213
|
+
critical,
|
|
214
|
+
ease,
|
|
215
|
+
judgeType: 'none',
|
|
216
|
+
timeScaleGroup,
|
|
217
|
+
};
|
|
218
|
+
object.connections.push(connection);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const connection = {
|
|
222
|
+
type: 'start',
|
|
223
|
+
beat,
|
|
224
|
+
lane,
|
|
225
|
+
size,
|
|
226
|
+
critical,
|
|
227
|
+
ease: easeMods.get(key) ?? 'linear',
|
|
228
|
+
judgeType,
|
|
229
|
+
timeScaleGroup,
|
|
230
|
+
};
|
|
231
|
+
object.connections.push(connection);
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 2: {
|
|
236
|
+
if (object.type == 'guide' || slideStartEndRemoveMods.has(key)) {
|
|
237
|
+
const connection = {
|
|
238
|
+
type: 'end',
|
|
239
|
+
beat,
|
|
240
|
+
lane,
|
|
241
|
+
size,
|
|
242
|
+
critical,
|
|
243
|
+
judgeType: 'none',
|
|
244
|
+
timeScaleGroup,
|
|
245
|
+
};
|
|
246
|
+
object.connections.push(connection);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const connection = {
|
|
250
|
+
type: 'end',
|
|
251
|
+
beat,
|
|
252
|
+
lane,
|
|
253
|
+
size,
|
|
254
|
+
critical,
|
|
255
|
+
judgeType,
|
|
256
|
+
timeScaleGroup,
|
|
257
|
+
};
|
|
258
|
+
const flickMod = flickMods.get(key);
|
|
259
|
+
if (flickMod)
|
|
260
|
+
connection.direction = flickMod;
|
|
261
|
+
object.connections.push(connection);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 3: {
|
|
266
|
+
if (tickRemoveMods.has(key)) {
|
|
267
|
+
const connection = {
|
|
268
|
+
type: 'attach',
|
|
269
|
+
beat,
|
|
270
|
+
critical,
|
|
271
|
+
timeScaleGroup,
|
|
272
|
+
};
|
|
273
|
+
object.connections.push(connection);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const connection = {
|
|
277
|
+
type: 'tick',
|
|
278
|
+
beat,
|
|
279
|
+
lane,
|
|
280
|
+
size,
|
|
281
|
+
critical,
|
|
282
|
+
ease,
|
|
283
|
+
judgeType,
|
|
284
|
+
timeScaleGroup,
|
|
285
|
+
};
|
|
286
|
+
object.connections.push(connection);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 5: {
|
|
291
|
+
if (tickRemoveMods.has(key))
|
|
292
|
+
break;
|
|
293
|
+
const connection = {
|
|
294
|
+
type: 'tick',
|
|
295
|
+
beat,
|
|
296
|
+
lane,
|
|
297
|
+
size,
|
|
298
|
+
ease,
|
|
299
|
+
timeScaleGroup,
|
|
300
|
+
};
|
|
301
|
+
object.connections.push(connection);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
objects.push(object);
|
|
307
|
+
if (object.type == 'guide')
|
|
308
|
+
continue;
|
|
309
|
+
const key = getKey(startNote);
|
|
310
|
+
const dupe = dedupeSlides.get(key);
|
|
311
|
+
if (dupe)
|
|
312
|
+
objects.splice(objects.indexOf(dupe), 1);
|
|
313
|
+
dedupeSlides.set(key, object);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
offset: score.offset,
|
|
317
|
+
objects,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
const getKey = (note) => `${note.lane}-${note.tick}`;
|
|
Binary file
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
const SONOLUS_DIRECTIONS = {
|
|
2
|
+
left: 1,
|
|
3
|
+
up: 0,
|
|
4
|
+
right: 2,
|
|
5
|
+
};
|
|
6
|
+
const SONOLUS_CONNECTOR_EASES = {
|
|
7
|
+
outin: 5,
|
|
8
|
+
out: 3,
|
|
9
|
+
linear: 1,
|
|
10
|
+
in: 2,
|
|
11
|
+
inout: 4,
|
|
12
|
+
};
|
|
13
|
+
const mapUscEaseToSonolusEase = (ease) => {
|
|
14
|
+
const uscEase = ease ?? 'linear';
|
|
15
|
+
return uscEase;
|
|
16
|
+
};
|
|
17
|
+
const SONOLUS_GUIDE_COLORS = {
|
|
18
|
+
neutral: 101,
|
|
19
|
+
red: 102,
|
|
20
|
+
green: 103,
|
|
21
|
+
blue: 104,
|
|
22
|
+
yellow: 105,
|
|
23
|
+
purple: 106,
|
|
24
|
+
cyan: 107,
|
|
25
|
+
black: 108,
|
|
26
|
+
};
|
|
27
|
+
const EPSILON = 1e-6;
|
|
28
|
+
export const uscToLevelData = (usc, offset = 0) => {
|
|
29
|
+
const allUscObjects = usc.objects ?? [];
|
|
30
|
+
const allIntermediateEntities = [];
|
|
31
|
+
const simLineEligibleNotes = [];
|
|
32
|
+
const timeScaleGroupIntermediates = [];
|
|
33
|
+
const createIntermediate = (archetype, data, isSimEligible = false) => {
|
|
34
|
+
const intermediateEntity = { archetype, data };
|
|
35
|
+
allIntermediateEntities.push(intermediateEntity);
|
|
36
|
+
if (isSimEligible) {
|
|
37
|
+
simLineEligibleNotes.push(intermediateEntity);
|
|
38
|
+
}
|
|
39
|
+
return intermediateEntity;
|
|
40
|
+
};
|
|
41
|
+
createIntermediate('Initialization', {});
|
|
42
|
+
const bpmChanges = allUscObjects.filter((o) => o.type === 'bpm');
|
|
43
|
+
const timeScaleGroups = allUscObjects.filter((o) => o.type === 'timeScaleGroup');
|
|
44
|
+
const singleNotes = allUscObjects.filter((o) => o.type === 'single');
|
|
45
|
+
const damageNotes = allUscObjects.filter((o) => o.type === 'damage');
|
|
46
|
+
const slideNotes = allUscObjects.filter((o) => o.type === 'slide' || (o.type === 'guide' && !('midpoints' in o)));
|
|
47
|
+
const guideNotes = allUscObjects.filter((o) => o.type === 'guide' && 'midpoints' in o);
|
|
48
|
+
const skills = allUscObjects.filter((o) => o.type === 'skill');
|
|
49
|
+
const feverChance = allUscObjects
|
|
50
|
+
.filter((o) => o.type === 'feverChance')
|
|
51
|
+
.sort((a, b) => a.beat - b.beat)
|
|
52
|
+
.slice(0, 1);
|
|
53
|
+
const feverStart = allUscObjects
|
|
54
|
+
.filter((o) => o.type === 'feverStart')
|
|
55
|
+
.sort((a, b) => a.beat - b.beat)
|
|
56
|
+
.slice(0, 1);
|
|
57
|
+
if (bpmChanges.length === 0) {
|
|
58
|
+
bpmChanges.push({ type: 'bpm', beat: 0, bpm: 160 });
|
|
59
|
+
}
|
|
60
|
+
for (const bpmChange of bpmChanges) {
|
|
61
|
+
createIntermediate('#BPM_CHANGE', {
|
|
62
|
+
'#BEAT': bpmChange.beat,
|
|
63
|
+
'#BPM': bpmChange.bpm,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (timeScaleGroups.length === 0) {
|
|
67
|
+
timeScaleGroups.push({
|
|
68
|
+
type: 'timeScaleGroup',
|
|
69
|
+
changes: [{ beat: 0, timeScale: 1 }],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
for (const skill of skills) {
|
|
73
|
+
createIntermediate('Skill', {
|
|
74
|
+
'#BEAT': skill.beat,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const fc of feverChance) {
|
|
78
|
+
createIntermediate('FeverChance', {
|
|
79
|
+
'#BEAT': fc.beat,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const fs of feverStart) {
|
|
83
|
+
createIntermediate('FeverStart', {
|
|
84
|
+
'#BEAT': fs.beat,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const timeScaleGroup of timeScaleGroups) {
|
|
88
|
+
const groupIntermediateEntity = createIntermediate('#TIMESCALE_GROUP', {});
|
|
89
|
+
timeScaleGroupIntermediates.push(groupIntermediateEntity);
|
|
90
|
+
let lastChangeIntermediate = null;
|
|
91
|
+
const changes = [...timeScaleGroup.changes].sort((a, b) => a.beat - b.beat);
|
|
92
|
+
for (const timeScaleChange of changes) {
|
|
93
|
+
const newChangeIntermediate = createIntermediate('#TIMESCALE_CHANGE', {
|
|
94
|
+
'#BEAT': timeScaleChange.beat,
|
|
95
|
+
'#TIMESCALE': timeScaleChange.timeScale === 0 ? 0.000001 : timeScaleChange.timeScale,
|
|
96
|
+
'#TIMESCALE_SKIP': 0,
|
|
97
|
+
'#TIMESCALE_GROUP': groupIntermediateEntity,
|
|
98
|
+
'#TIMESCALE_EASE': 0,
|
|
99
|
+
});
|
|
100
|
+
if (lastChangeIntermediate === null) {
|
|
101
|
+
groupIntermediateEntity.data['first'] = newChangeIntermediate;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lastChangeIntermediate.data['next'] = newChangeIntermediate;
|
|
105
|
+
}
|
|
106
|
+
lastChangeIntermediate = newChangeIntermediate;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const singleNote of singleNotes) {
|
|
110
|
+
const name_parts = [];
|
|
111
|
+
name_parts.push(singleNote.critical ? 'Critical' : 'Normal');
|
|
112
|
+
if (singleNote.direction === undefined || singleNote.direction === null) {
|
|
113
|
+
name_parts.push(singleNote.trace ? 'Trace' : 'Tap');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
name_parts.push(singleNote.trace ? 'TraceFlick' : 'Flick');
|
|
117
|
+
}
|
|
118
|
+
name_parts.push('Note');
|
|
119
|
+
const archetype = name_parts.join('');
|
|
120
|
+
const timeScaleGroupRef = timeScaleGroupIntermediates[singleNote.timeScaleGroup ?? 0];
|
|
121
|
+
let sonolusDirName;
|
|
122
|
+
if (singleNote.direction === undefined || singleNote.direction === null) {
|
|
123
|
+
sonolusDirName = 'up';
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
sonolusDirName = singleNote.direction;
|
|
127
|
+
}
|
|
128
|
+
const data = {
|
|
129
|
+
'#BEAT': singleNote.beat,
|
|
130
|
+
lane: singleNote.lane,
|
|
131
|
+
size: singleNote.size,
|
|
132
|
+
isAttached: 0,
|
|
133
|
+
connectorEase: SONOLUS_CONNECTOR_EASES.linear,
|
|
134
|
+
isSeparator: 0,
|
|
135
|
+
segmentKind: singleNote.critical ? 2 : 1,
|
|
136
|
+
segmentAlpha: 1,
|
|
137
|
+
'#TIMESCALE_GROUP': timeScaleGroupRef,
|
|
138
|
+
};
|
|
139
|
+
const directionValue = SONOLUS_DIRECTIONS[sonolusDirName];
|
|
140
|
+
if (directionValue !== undefined) {
|
|
141
|
+
data['direction'] = directionValue;
|
|
142
|
+
}
|
|
143
|
+
createIntermediate(archetype, data, true);
|
|
144
|
+
}
|
|
145
|
+
for (const damageNote of damageNotes) {
|
|
146
|
+
const archetype = 'DamageNote';
|
|
147
|
+
const timeScaleGroupRef = timeScaleGroupIntermediates[damageNote.timeScaleGroup ?? 0];
|
|
148
|
+
createIntermediate(archetype, {
|
|
149
|
+
'#BEAT': damageNote.beat,
|
|
150
|
+
lane: damageNote.lane,
|
|
151
|
+
size: damageNote.size,
|
|
152
|
+
direction: SONOLUS_DIRECTIONS.up,
|
|
153
|
+
isAttached: 0,
|
|
154
|
+
connectorEase: SONOLUS_CONNECTOR_EASES.linear,
|
|
155
|
+
isSeparator: 0,
|
|
156
|
+
segmentKind: 1,
|
|
157
|
+
segmentAlpha: 1,
|
|
158
|
+
'#TIMESCALE_GROUP': timeScaleGroupRef,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
for (const slideNote of slideNotes) {
|
|
162
|
+
let prevJointIntermediate = null;
|
|
163
|
+
let prevNoteIntermediate = null;
|
|
164
|
+
let headNoteIntermediate = null;
|
|
165
|
+
let currentSegmentHead = null;
|
|
166
|
+
const queuedAttachIntermediates = [];
|
|
167
|
+
const connectors = [];
|
|
168
|
+
const pendingSegmentConnectors = [];
|
|
169
|
+
const connections = [...slideNote.connections].sort((a, b) => a.beat - b.beat);
|
|
170
|
+
if (connections.length === 0)
|
|
171
|
+
continue;
|
|
172
|
+
const stepSize = Math.max(1, connections.length - 1);
|
|
173
|
+
let nextHiddenTickBeat = Math.floor(connections[0].beat * 2 + 1) / 2;
|
|
174
|
+
let stepIdx = 0;
|
|
175
|
+
for (const connectionNote of connections) {
|
|
176
|
+
let isSimLineEligible = false;
|
|
177
|
+
let isAttached = false;
|
|
178
|
+
const name_parts = [];
|
|
179
|
+
switch (connectionNote.type) {
|
|
180
|
+
case 'start':
|
|
181
|
+
if (connectionNote.judgeType === 'none') {
|
|
182
|
+
name_parts.push('Anchor');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
name_parts.push(connectionNote.critical ? 'Critical' : 'Normal');
|
|
186
|
+
name_parts.push('Head');
|
|
187
|
+
if (connectionNote.judgeType === 'trace')
|
|
188
|
+
name_parts.push('Trace');
|
|
189
|
+
else if (connectionNote.judgeType === 'normal')
|
|
190
|
+
name_parts.push('Tap');
|
|
191
|
+
isSimLineEligible = true;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'end':
|
|
195
|
+
if (connectionNote.judgeType === 'none') {
|
|
196
|
+
name_parts.push('Anchor');
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
name_parts.push(connectionNote.critical ? 'Critical' : 'Normal');
|
|
200
|
+
name_parts.push('Tail');
|
|
201
|
+
if (connectionNote.direction === undefined ||
|
|
202
|
+
connectionNote.direction === null) {
|
|
203
|
+
if (connectionNote.judgeType === 'trace')
|
|
204
|
+
name_parts.push('Trace');
|
|
205
|
+
else if (connectionNote.judgeType === 'normal')
|
|
206
|
+
name_parts.push('Release');
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
if (connectionNote.judgeType === 'trace')
|
|
210
|
+
name_parts.push('TraceFlick');
|
|
211
|
+
else if (connectionNote.judgeType === 'normal')
|
|
212
|
+
name_parts.push('Flick');
|
|
213
|
+
}
|
|
214
|
+
isSimLineEligible = true;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
case 'tick':
|
|
218
|
+
if (connectionNote.critical !== undefined) {
|
|
219
|
+
name_parts.push(connectionNote.critical ? 'Critical' : 'Normal');
|
|
220
|
+
if (connectionNote.judgeType === 'trace')
|
|
221
|
+
name_parts.push('Trace');
|
|
222
|
+
else
|
|
223
|
+
name_parts.push('Tick');
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
name_parts.push('Anchor');
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
case 'attach':
|
|
230
|
+
isAttached = true;
|
|
231
|
+
if (connectionNote.critical !== undefined) {
|
|
232
|
+
name_parts.push(connectionNote.critical ? 'Critical' : 'Normal');
|
|
233
|
+
name_parts.push('Tick');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
name_parts.push('TransientHiddenTick');
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
name_parts.push('Note');
|
|
241
|
+
const archetype = name_parts.join('');
|
|
242
|
+
const timeScaleGroupRef = timeScaleGroupIntermediates[connectionNote.timeScaleGroup ?? 0];
|
|
243
|
+
let sonolusDirName;
|
|
244
|
+
const direction = connectionNote.direction;
|
|
245
|
+
if (direction === undefined || direction === null) {
|
|
246
|
+
sonolusDirName = 'up';
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
sonolusDirName = direction;
|
|
250
|
+
}
|
|
251
|
+
const sonolusEaseValue = SONOLUS_CONNECTOR_EASES[mapUscEaseToSonolusEase('ease' in connectionNote ? connectionNote.ease : undefined)];
|
|
252
|
+
let segmentAlpha = 1;
|
|
253
|
+
if (slideNote.type === 'guide') {
|
|
254
|
+
// 공식: 1 - (0.8 * (현재단계 / 전체구간)) -> 1에서 0.2까지 감소
|
|
255
|
+
segmentAlpha = 1 - 0.8 * (stepIdx / stepSize);
|
|
256
|
+
}
|
|
257
|
+
const isSeparatorValue = slideNote.type == 'slide' ? 0 : 1;
|
|
258
|
+
const data = {
|
|
259
|
+
'#BEAT': connectionNote.beat,
|
|
260
|
+
lane: 'lane' in connectionNote ? connectionNote.lane : 0,
|
|
261
|
+
size: 'size' in connectionNote ? connectionNote.size : 0,
|
|
262
|
+
isAttached: isAttached ? 1 : 0,
|
|
263
|
+
connectorEase: sonolusEaseValue,
|
|
264
|
+
isSeparator: isSeparatorValue,
|
|
265
|
+
segmentKind: slideNote.type == 'slide'
|
|
266
|
+
? slideNote.critical
|
|
267
|
+
? 2
|
|
268
|
+
: 1
|
|
269
|
+
: slideNote.critical
|
|
270
|
+
? 105
|
|
271
|
+
: 103,
|
|
272
|
+
segmentAlpha: segmentAlpha,
|
|
273
|
+
segmentLayer: slideNote.type == 'slide' ? 0 : 1,
|
|
274
|
+
'#TIMESCALE_GROUP': timeScaleGroupRef,
|
|
275
|
+
};
|
|
276
|
+
const directionValue = SONOLUS_DIRECTIONS[sonolusDirName];
|
|
277
|
+
if (directionValue !== undefined) {
|
|
278
|
+
data['direction'] = directionValue;
|
|
279
|
+
}
|
|
280
|
+
const connectionIntermediate = createIntermediate(archetype, data, isSimLineEligible);
|
|
281
|
+
if (headNoteIntermediate === null) {
|
|
282
|
+
headNoteIntermediate = connectionIntermediate;
|
|
283
|
+
}
|
|
284
|
+
if (currentSegmentHead === null) {
|
|
285
|
+
currentSegmentHead = connectionIntermediate;
|
|
286
|
+
}
|
|
287
|
+
connectionIntermediate.data['activeHead'] = headNoteIntermediate;
|
|
288
|
+
if (isAttached) {
|
|
289
|
+
queuedAttachIntermediates.push(connectionIntermediate);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
if (prevJointIntermediate !== null) {
|
|
293
|
+
for (const attachIntermediate of queuedAttachIntermediates) {
|
|
294
|
+
attachIntermediate.data['attachHead'] = prevJointIntermediate;
|
|
295
|
+
attachIntermediate.data['attachTail'] = connectionIntermediate;
|
|
296
|
+
}
|
|
297
|
+
queuedAttachIntermediates.length = 0;
|
|
298
|
+
while (slideNote.type == 'slide' &&
|
|
299
|
+
nextHiddenTickBeat + EPSILON <
|
|
300
|
+
connectionIntermediate.data['#BEAT']) {
|
|
301
|
+
createIntermediate('TransientHiddenTickNote', {
|
|
302
|
+
'#BEAT': nextHiddenTickBeat,
|
|
303
|
+
'#TIMESCALE_GROUP': timeScaleGroupIntermediates[0],
|
|
304
|
+
lane: connectionIntermediate.data['lane'],
|
|
305
|
+
size: connectionIntermediate.data['size'],
|
|
306
|
+
direction: SONOLUS_DIRECTIONS.up,
|
|
307
|
+
isAttached: 1,
|
|
308
|
+
connectorEase: SONOLUS_CONNECTOR_EASES.linear,
|
|
309
|
+
isSeparator: 0,
|
|
310
|
+
segmentKind: 1,
|
|
311
|
+
segmentAlpha: 0,
|
|
312
|
+
activeHead: headNoteIntermediate,
|
|
313
|
+
attachHead: prevJointIntermediate,
|
|
314
|
+
attachTail: connectionIntermediate,
|
|
315
|
+
});
|
|
316
|
+
nextHiddenTickBeat += 0.5;
|
|
317
|
+
}
|
|
318
|
+
const connectorIntermediate = createIntermediate('Connector', {
|
|
319
|
+
head: prevJointIntermediate,
|
|
320
|
+
tail: connectionIntermediate,
|
|
321
|
+
});
|
|
322
|
+
connectors.push(connectorIntermediate);
|
|
323
|
+
pendingSegmentConnectors.push(connectorIntermediate);
|
|
324
|
+
}
|
|
325
|
+
prevJointIntermediate = connectionIntermediate;
|
|
326
|
+
}
|
|
327
|
+
if (isSeparatorValue === 1) {
|
|
328
|
+
for (const conn of pendingSegmentConnectors) {
|
|
329
|
+
conn.data['segmentHead'] = currentSegmentHead;
|
|
330
|
+
conn.data['segmentTail'] = connectionIntermediate;
|
|
331
|
+
}
|
|
332
|
+
pendingSegmentConnectors.length = 0;
|
|
333
|
+
currentSegmentHead = connectionIntermediate;
|
|
334
|
+
}
|
|
335
|
+
if (prevNoteIntermediate !== null) {
|
|
336
|
+
prevNoteIntermediate.data['next'] = connectionIntermediate;
|
|
337
|
+
}
|
|
338
|
+
prevNoteIntermediate = connectionIntermediate;
|
|
339
|
+
stepIdx++;
|
|
340
|
+
}
|
|
341
|
+
if (!headNoteIntermediate || !prevJointIntermediate) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (currentSegmentHead) {
|
|
345
|
+
for (const conn of pendingSegmentConnectors) {
|
|
346
|
+
conn.data['segmentHead'] = currentSegmentHead;
|
|
347
|
+
conn.data['segmentTail'] = prevJointIntermediate;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const connectorIntermediate of connectors) {
|
|
351
|
+
connectorIntermediate.data['activeHead'] = headNoteIntermediate;
|
|
352
|
+
connectorIntermediate.data['activeTail'] = prevJointIntermediate;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const guideNote of guideNotes) {
|
|
356
|
+
const connections = [...guideNote.midpoints].sort((a, b) => a.beat - b.beat);
|
|
357
|
+
let prevMidpointIntermediate = null;
|
|
358
|
+
let headMidpointIntermediate = null;
|
|
359
|
+
const guideConnectors = [];
|
|
360
|
+
for (const midpointNote of connections) {
|
|
361
|
+
const timeScaleGroupRef = timeScaleGroupIntermediates[midpointNote.timeScaleGroup ?? 0];
|
|
362
|
+
const midpointIntermediate = createIntermediate('AnchorNote', {
|
|
363
|
+
'#BEAT': midpointNote.beat,
|
|
364
|
+
lane: midpointNote.lane,
|
|
365
|
+
size: midpointNote.size,
|
|
366
|
+
direction: SONOLUS_DIRECTIONS.up,
|
|
367
|
+
isAttached: 0,
|
|
368
|
+
connectorEase: SONOLUS_CONNECTOR_EASES[mapUscEaseToSonolusEase(midpointNote.ease)],
|
|
369
|
+
isSeparator: 0,
|
|
370
|
+
segmentKind: SONOLUS_GUIDE_COLORS[guideNote.color],
|
|
371
|
+
segmentAlpha: 1,
|
|
372
|
+
'#TIMESCALE_GROUP': timeScaleGroupRef,
|
|
373
|
+
});
|
|
374
|
+
if (headMidpointIntermediate === null) {
|
|
375
|
+
headMidpointIntermediate = midpointIntermediate;
|
|
376
|
+
}
|
|
377
|
+
if (prevMidpointIntermediate !== null) {
|
|
378
|
+
const connectorIntermediate = createIntermediate('Connector', {
|
|
379
|
+
head: prevMidpointIntermediate,
|
|
380
|
+
tail: midpointIntermediate,
|
|
381
|
+
});
|
|
382
|
+
guideConnectors.push(connectorIntermediate);
|
|
383
|
+
prevMidpointIntermediate.data['next'] = midpointIntermediate;
|
|
384
|
+
}
|
|
385
|
+
prevMidpointIntermediate = midpointIntermediate;
|
|
386
|
+
}
|
|
387
|
+
if (!headMidpointIntermediate || !prevMidpointIntermediate) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
for (const connectorIntermediate of guideConnectors) {
|
|
391
|
+
connectorIntermediate.data['segmentHead'] = headMidpointIntermediate;
|
|
392
|
+
connectorIntermediate.data['segmentTail'] = prevMidpointIntermediate;
|
|
393
|
+
connectorIntermediate.data['activeHead'] = headMidpointIntermediate;
|
|
394
|
+
connectorIntermediate.data['activeTail'] = prevMidpointIntermediate;
|
|
395
|
+
}
|
|
396
|
+
switch (guideNote.fade) {
|
|
397
|
+
case 'in':
|
|
398
|
+
headMidpointIntermediate.data['segmentAlpha'] = 0;
|
|
399
|
+
break;
|
|
400
|
+
case 'out':
|
|
401
|
+
prevMidpointIntermediate.data['segmentAlpha'] = 0;
|
|
402
|
+
break;
|
|
403
|
+
case 'none':
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
simLineEligibleNotes.sort((noteA, noteB) => {
|
|
408
|
+
const beatA = noteA.data['#BEAT'];
|
|
409
|
+
const beatB = noteB.data['#BEAT'];
|
|
410
|
+
if (beatA !== beatB)
|
|
411
|
+
return beatA - beatB;
|
|
412
|
+
const laneA = noteA.data['lane'];
|
|
413
|
+
const laneB = noteB.data['lane'];
|
|
414
|
+
return laneA - laneB;
|
|
415
|
+
});
|
|
416
|
+
const simGroups = [];
|
|
417
|
+
let currentSimGroup = [];
|
|
418
|
+
for (const simNote of simLineEligibleNotes) {
|
|
419
|
+
if (currentSimGroup.length === 0 ||
|
|
420
|
+
Math.abs(simNote.data['#BEAT'] - currentSimGroup[0].data['#BEAT']) < 1e-2) {
|
|
421
|
+
currentSimGroup.push(simNote);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
simGroups.push(currentSimGroup);
|
|
425
|
+
currentSimGroup = [simNote];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (currentSimGroup.length > 0) {
|
|
429
|
+
simGroups.push(currentSimGroup);
|
|
430
|
+
}
|
|
431
|
+
for (const simGroup of simGroups) {
|
|
432
|
+
for (let i = 0; i < simGroup.length - 1; i++) {
|
|
433
|
+
const leftNote = simGroup[i];
|
|
434
|
+
const rightNote = simGroup[i + 1];
|
|
435
|
+
createIntermediate('SimLine', {
|
|
436
|
+
left: leftNote,
|
|
437
|
+
right: rightNote,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const entities = [];
|
|
442
|
+
const intermediateToRef = new Map();
|
|
443
|
+
let entityRefCounter = 0;
|
|
444
|
+
const getRef = (intermediateEntity) => {
|
|
445
|
+
let ref = intermediateToRef.get(intermediateEntity);
|
|
446
|
+
if (ref)
|
|
447
|
+
return ref;
|
|
448
|
+
ref = (entityRefCounter++).toString(16);
|
|
449
|
+
intermediateToRef.set(intermediateEntity, ref);
|
|
450
|
+
return ref;
|
|
451
|
+
};
|
|
452
|
+
for (const intermediateEntity of allIntermediateEntities) {
|
|
453
|
+
const entity = {
|
|
454
|
+
archetype: intermediateEntity.archetype,
|
|
455
|
+
name: getRef(intermediateEntity),
|
|
456
|
+
data: [],
|
|
457
|
+
};
|
|
458
|
+
for (const [dataName, dataValue] of Object.entries(intermediateEntity.data)) {
|
|
459
|
+
if (typeof dataValue === 'number') {
|
|
460
|
+
entity.data.push({ name: dataName, value: dataValue });
|
|
461
|
+
}
|
|
462
|
+
else if (dataValue !== undefined) {
|
|
463
|
+
entity.data.push({ name: dataName, ref: getRef(dataValue) });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
entities.push(entity);
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
bgmOffset: usc.offset + offset,
|
|
470
|
+
entities,
|
|
471
|
+
};
|
|
472
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type USC = {
|
|
2
|
+
offset: number;
|
|
3
|
+
objects: USCObject[];
|
|
4
|
+
};
|
|
5
|
+
export type USCObject = USCBpmChange | USCTimeScaleChange | USCSingleNote | USCSlideNote | USCGuideNote | USCDamageNote | USCEvent;
|
|
6
|
+
type BaseUSCObject = {
|
|
7
|
+
beat: number;
|
|
8
|
+
timeScaleGroup: number;
|
|
9
|
+
};
|
|
10
|
+
export type USCBpmChange = Omit<BaseUSCObject, 'timeScaleGroup'> & {
|
|
11
|
+
type: 'bpm';
|
|
12
|
+
bpm: number;
|
|
13
|
+
};
|
|
14
|
+
export type USCTimeScaleChange = {
|
|
15
|
+
type: 'timeScaleGroup';
|
|
16
|
+
changes: {
|
|
17
|
+
beat: number;
|
|
18
|
+
timeScale: number;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
type BaseUSCNote = BaseUSCObject & {
|
|
22
|
+
lane: number;
|
|
23
|
+
size: number;
|
|
24
|
+
};
|
|
25
|
+
export type USCSingleNote = BaseUSCNote & {
|
|
26
|
+
type: 'single';
|
|
27
|
+
critical: boolean;
|
|
28
|
+
trace: boolean;
|
|
29
|
+
direction?: 'left' | 'up' | 'right' | 'none';
|
|
30
|
+
};
|
|
31
|
+
export type USCDamageNote = BaseUSCNote & {
|
|
32
|
+
type: 'damage';
|
|
33
|
+
};
|
|
34
|
+
export type USCConnectionStartNote = BaseUSCNote & {
|
|
35
|
+
type: 'start';
|
|
36
|
+
critical: boolean;
|
|
37
|
+
ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
|
|
38
|
+
judgeType: 'normal' | 'trace' | 'none';
|
|
39
|
+
};
|
|
40
|
+
export type USCConnectionTickNote = BaseUSCNote & {
|
|
41
|
+
type: 'tick';
|
|
42
|
+
critical?: boolean;
|
|
43
|
+
ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
|
|
44
|
+
judgeType?: 'normal' | 'trace';
|
|
45
|
+
};
|
|
46
|
+
export type USCConnectionAttachNote = Omit<BaseUSCObject, 'timeScaleGroup'> & {
|
|
47
|
+
type: 'attach';
|
|
48
|
+
critical?: boolean;
|
|
49
|
+
timeScaleGroup?: number;
|
|
50
|
+
};
|
|
51
|
+
export type USCConnectionEndNote = BaseUSCNote & {
|
|
52
|
+
type: 'end';
|
|
53
|
+
critical: boolean;
|
|
54
|
+
direction?: 'left' | 'up' | 'right';
|
|
55
|
+
judgeType: 'normal' | 'trace' | 'none';
|
|
56
|
+
};
|
|
57
|
+
export type USCSlideNote = {
|
|
58
|
+
type: 'slide' | 'guide';
|
|
59
|
+
critical: boolean;
|
|
60
|
+
connections: [
|
|
61
|
+
USCConnectionStartNote,
|
|
62
|
+
...(USCConnectionTickNote | USCConnectionAttachNote)[],
|
|
63
|
+
USCConnectionEndNote
|
|
64
|
+
];
|
|
65
|
+
};
|
|
66
|
+
export declare const USCColor: {
|
|
67
|
+
neutral: number;
|
|
68
|
+
red: number;
|
|
69
|
+
green: number;
|
|
70
|
+
blue: number;
|
|
71
|
+
yellow: number;
|
|
72
|
+
purple: number;
|
|
73
|
+
cyan: number;
|
|
74
|
+
black: number;
|
|
75
|
+
};
|
|
76
|
+
export type USCColor = keyof typeof USCColor;
|
|
77
|
+
export type USCGuideMidpointNote = BaseUSCNote & {
|
|
78
|
+
ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
|
|
79
|
+
};
|
|
80
|
+
export declare const USCFade: {
|
|
81
|
+
in: number;
|
|
82
|
+
out: number;
|
|
83
|
+
none: number;
|
|
84
|
+
};
|
|
85
|
+
export type USCFade = keyof typeof USCFade;
|
|
86
|
+
export type USCGuideNote = {
|
|
87
|
+
type: 'guide';
|
|
88
|
+
color: USCColor;
|
|
89
|
+
fade: USCFade;
|
|
90
|
+
midpoints: USCGuideMidpointNote[];
|
|
91
|
+
};
|
|
92
|
+
export type USCEvent = {
|
|
93
|
+
type: 'skill' | 'feverChance' | 'feverStart';
|
|
94
|
+
beat: number;
|
|
95
|
+
};
|
|
96
|
+
export {};
|