sonolus-next-rush-engine 1.0.2 → 1.0.4
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/dist/extended/convert.js +248 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/extended/convert.js
CHANGED
|
@@ -143,6 +143,35 @@ function getNum(e, name, def = 0) {
|
|
|
143
143
|
const val = getField(e, name);
|
|
144
144
|
return typeof val === 'number' ? val : def;
|
|
145
145
|
}
|
|
146
|
+
function nearlyEqual(a, b) {
|
|
147
|
+
return Math.abs(a - b) < 1e-6;
|
|
148
|
+
}
|
|
149
|
+
function lerp(a, b, t) {
|
|
150
|
+
return a + (b - a) * t;
|
|
151
|
+
}
|
|
152
|
+
function unlerp(a, b, x) {
|
|
153
|
+
return (x - a) / (b - a);
|
|
154
|
+
}
|
|
155
|
+
function clamp01(x) {
|
|
156
|
+
return Math.min(1, Math.max(0, x));
|
|
157
|
+
}
|
|
158
|
+
function applyEase(type, x) {
|
|
159
|
+
const t = clamp01(x);
|
|
160
|
+
switch (type) {
|
|
161
|
+
case EaseType.IN_QUAD:
|
|
162
|
+
return t * t;
|
|
163
|
+
case EaseType.OUT_QUAD:
|
|
164
|
+
return 1 - (1 - t) * (1 - t);
|
|
165
|
+
case EaseType.IN_OUT_QUAD:
|
|
166
|
+
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
167
|
+
case EaseType.OUT_IN_QUAD:
|
|
168
|
+
return t < 0.5
|
|
169
|
+
? (1 - (1 - 2 * t) * (1 - 2 * t)) / 2
|
|
170
|
+
: 0.5 + ((2 * t - 1) * (2 * t - 1)) / 2;
|
|
171
|
+
default:
|
|
172
|
+
return t;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
146
175
|
function resolveOriginal(ext, ref) {
|
|
147
176
|
if (typeof ref === 'number')
|
|
148
177
|
return ext.get(ref);
|
|
@@ -157,6 +186,42 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
157
186
|
finalEntities.push(defaultTsg);
|
|
158
187
|
const init = new EntityBuilder('Initialization');
|
|
159
188
|
finalEntities.push(init);
|
|
189
|
+
const bpmChanges = ext
|
|
190
|
+
.getByArch('#BPM_CHANGE')
|
|
191
|
+
.map(({ e }) => ({ beat: getNum(e, '#BEAT'), bpm: getNum(e, '#BPM') }))
|
|
192
|
+
.sort((a, b) => a.beat - b.beat);
|
|
193
|
+
const bpmChangeInfos = [];
|
|
194
|
+
let lastBeat = 0;
|
|
195
|
+
let lastTime = 0;
|
|
196
|
+
let lastBpm = bpmChanges[0]?.bpm ?? 120;
|
|
197
|
+
for (const change of bpmChanges) {
|
|
198
|
+
lastTime += ((change.beat - lastBeat) * 60) / lastBpm;
|
|
199
|
+
bpmChangeInfos.push({ ...change, time: lastTime });
|
|
200
|
+
lastBeat = change.beat;
|
|
201
|
+
lastBpm = change.bpm;
|
|
202
|
+
}
|
|
203
|
+
function beatToTime(beat) {
|
|
204
|
+
if (bpmChangeInfos.length === 0)
|
|
205
|
+
return (beat * 60) / 120;
|
|
206
|
+
let current = bpmChangeInfos[0];
|
|
207
|
+
for (const change of bpmChangeInfos) {
|
|
208
|
+
if (change.beat > beat)
|
|
209
|
+
break;
|
|
210
|
+
current = change;
|
|
211
|
+
}
|
|
212
|
+
return current.time + ((beat - current.beat) * 60) / current.bpm;
|
|
213
|
+
}
|
|
214
|
+
function timeToBeat(time) {
|
|
215
|
+
if (bpmChangeInfos.length === 0)
|
|
216
|
+
return (time * 120) / 60;
|
|
217
|
+
let current = bpmChangeInfos[0];
|
|
218
|
+
for (const change of bpmChangeInfos) {
|
|
219
|
+
if (change.time > time)
|
|
220
|
+
break;
|
|
221
|
+
current = change;
|
|
222
|
+
}
|
|
223
|
+
return current.beat + ((time - current.time) * current.bpm) / 60;
|
|
224
|
+
}
|
|
160
225
|
for (const { e } of ext.getByArch('#BPM_CHANGE')) {
|
|
161
226
|
const bpm = new EntityBuilder('#BPM_CHANGE');
|
|
162
227
|
bpm.set('#BEAT', getNum(e, '#BEAT'));
|
|
@@ -165,6 +230,8 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
165
230
|
}
|
|
166
231
|
const timescaleGroupsByIndex = new Map();
|
|
167
232
|
const timescaleGroupsByName = new Map();
|
|
233
|
+
const timescaleChangesByIndex = new Map();
|
|
234
|
+
const timescaleChangesByName = new Map();
|
|
168
235
|
for (const { idx, e } of ext.getByArch('TimeScaleGroup')) {
|
|
169
236
|
const group = new EntityBuilder('#TIMESCALE_GROUP');
|
|
170
237
|
finalEntities.push(group);
|
|
@@ -173,10 +240,15 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
173
240
|
timescaleGroupsByName.set(e.name, group);
|
|
174
241
|
let rawRef = getField(e, 'first');
|
|
175
242
|
const changes = [];
|
|
243
|
+
const changeInfos = [];
|
|
176
244
|
while (rawRef !== undefined) {
|
|
177
245
|
const raw = resolveOriginal(ext, rawRef);
|
|
178
246
|
if (!raw)
|
|
179
247
|
break;
|
|
248
|
+
changeInfos.push({
|
|
249
|
+
beat: getNum(raw, '#BEAT'),
|
|
250
|
+
timeScale: getNum(raw, 'timeScale'),
|
|
251
|
+
});
|
|
180
252
|
const change = new EntityBuilder('#TIMESCALE_CHANGE');
|
|
181
253
|
change.set('#BEAT', getNum(raw, '#BEAT'));
|
|
182
254
|
change.set('#TIMESCALE', getNum(raw, 'timeScale'));
|
|
@@ -197,6 +269,10 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
197
269
|
group.set('first', changes[0]);
|
|
198
270
|
}
|
|
199
271
|
finalEntities.push(...changes);
|
|
272
|
+
changeInfos.sort((a, b) => a.beat - b.beat);
|
|
273
|
+
timescaleChangesByIndex.set(idx, changeInfos);
|
|
274
|
+
if (e.name)
|
|
275
|
+
timescaleChangesByName.set(e.name, changeInfos);
|
|
200
276
|
}
|
|
201
277
|
function getTSG(ref) {
|
|
202
278
|
if (typeof ref === 'number')
|
|
@@ -205,6 +281,59 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
205
281
|
return timescaleGroupsByName.get(ref);
|
|
206
282
|
return undefined;
|
|
207
283
|
}
|
|
284
|
+
function getTSGChanges(ref) {
|
|
285
|
+
if (typeof ref === 'number')
|
|
286
|
+
return timescaleChangesByIndex.get(ref) ?? [];
|
|
287
|
+
if (typeof ref === 'string')
|
|
288
|
+
return timescaleChangesByName.get(ref) ?? [];
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
function timeToScaledTime(time, changes) {
|
|
292
|
+
if (changes.length === 0)
|
|
293
|
+
return time;
|
|
294
|
+
const firstTime = beatToTime(changes[0].beat);
|
|
295
|
+
if (time < firstTime)
|
|
296
|
+
return time;
|
|
297
|
+
let scaledTime = firstTime;
|
|
298
|
+
for (let i = 0; i < changes.length; i++) {
|
|
299
|
+
const start = changes[i];
|
|
300
|
+
const startTime = beatToTime(start.beat);
|
|
301
|
+
const endTime = i === changes.length - 1 ? undefined : beatToTime(changes[i + 1].beat);
|
|
302
|
+
if (endTime === undefined || time < endTime) {
|
|
303
|
+
return scaledTime + (time - startTime) * start.timeScale;
|
|
304
|
+
}
|
|
305
|
+
scaledTime += (endTime - startTime) * start.timeScale;
|
|
306
|
+
}
|
|
307
|
+
return time;
|
|
308
|
+
}
|
|
309
|
+
function scaledTimeToTime(scaledTime, changes) {
|
|
310
|
+
if (changes.length === 0)
|
|
311
|
+
return scaledTime;
|
|
312
|
+
const firstTime = beatToTime(changes[0].beat);
|
|
313
|
+
if (scaledTime < firstTime)
|
|
314
|
+
return scaledTime;
|
|
315
|
+
let currentScaledTime = firstTime;
|
|
316
|
+
for (let i = 0; i < changes.length; i++) {
|
|
317
|
+
const start = changes[i];
|
|
318
|
+
const startTime = beatToTime(start.beat);
|
|
319
|
+
const endTime = i === changes.length - 1 ? undefined : beatToTime(changes[i + 1].beat);
|
|
320
|
+
if (endTime === undefined) {
|
|
321
|
+
if (start.timeScale === 0)
|
|
322
|
+
return Number.POSITIVE_INFINITY;
|
|
323
|
+
return startTime + (scaledTime - currentScaledTime) / start.timeScale;
|
|
324
|
+
}
|
|
325
|
+
const nextScaledTime = currentScaledTime + (endTime - startTime) * start.timeScale;
|
|
326
|
+
const minScaledTime = Math.min(currentScaledTime, nextScaledTime);
|
|
327
|
+
const maxScaledTime = Math.max(currentScaledTime, nextScaledTime);
|
|
328
|
+
if (minScaledTime <= scaledTime && scaledTime <= maxScaledTime) {
|
|
329
|
+
if (Math.abs(nextScaledTime - currentScaledTime) < 1e-6)
|
|
330
|
+
return startTime;
|
|
331
|
+
return lerp(startTime, endTime, unlerp(currentScaledTime, nextScaledTime, scaledTime));
|
|
332
|
+
}
|
|
333
|
+
currentScaledTime = nextScaledTime;
|
|
334
|
+
}
|
|
335
|
+
return scaledTime;
|
|
336
|
+
}
|
|
208
337
|
const notesByIndex = new Map();
|
|
209
338
|
const notesByName = new Map();
|
|
210
339
|
const connectorsByIndex = new Map();
|
|
@@ -234,15 +363,93 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
234
363
|
return notesByName.get(ref);
|
|
235
364
|
return undefined;
|
|
236
365
|
}
|
|
366
|
+
function shouldUseStartAsHead(startRef, headRef) {
|
|
367
|
+
if (startRef === headRef)
|
|
368
|
+
return false;
|
|
369
|
+
const start = resolveOriginal(ext, startRef);
|
|
370
|
+
const head = resolveOriginal(ext, headRef);
|
|
371
|
+
if (!start || !head)
|
|
372
|
+
return false;
|
|
373
|
+
if (head.archetype !== 'HiddenSlideStartNote')
|
|
374
|
+
return false;
|
|
375
|
+
if (!['NormalSlideStartNote', 'CriticalSlideStartNote'].includes(start.archetype))
|
|
376
|
+
return false;
|
|
377
|
+
return (nearlyEqual(getNum(start, '#BEAT'), getNum(head, '#BEAT')) &&
|
|
378
|
+
nearlyEqual(getNum(start, 'lane'), getNum(head, 'lane')) &&
|
|
379
|
+
nearlyEqual(getNum(start, 'size'), getNum(head, 'size')));
|
|
380
|
+
}
|
|
381
|
+
function createConnectorAnchor(beat, lane, size, tsg, kind) {
|
|
382
|
+
const anchor = new EntityBuilder('AnchorNote');
|
|
383
|
+
anchor.set('#BEAT', beat);
|
|
384
|
+
anchor.set('lane', lane);
|
|
385
|
+
anchor.set('size', size);
|
|
386
|
+
anchor.set('direction', FlickDirection.UP_OMNI);
|
|
387
|
+
anchor.set('#TIMESCALE_GROUP', tsg || defaultTsg);
|
|
388
|
+
anchor.set('isAttached', 0);
|
|
389
|
+
anchor.set('connectorEase', EaseType.LINEAR);
|
|
390
|
+
anchor.set('isSeparator', 1);
|
|
391
|
+
anchor.set('segmentKind', kind);
|
|
392
|
+
anchor.set('segmentAlpha', 1);
|
|
393
|
+
anchor.set('segmentLayer', 0);
|
|
394
|
+
finalEntities.push(anchor);
|
|
395
|
+
return anchor;
|
|
396
|
+
}
|
|
397
|
+
function getConnectorSplitAnchors(headOriginal, tailOriginal, tsg, kind, ease) {
|
|
398
|
+
const headBeat = getNum(headOriginal, '#BEAT');
|
|
399
|
+
const tailBeat = getNum(tailOriginal, '#BEAT');
|
|
400
|
+
if (tailBeat <= headBeat)
|
|
401
|
+
return [];
|
|
402
|
+
const headTsgRef = getField(headOriginal, 'timeScaleGroup');
|
|
403
|
+
const tailTsgRef = getField(tailOriginal, 'timeScaleGroup');
|
|
404
|
+
const headChanges = getTSGChanges(headTsgRef);
|
|
405
|
+
const tailChanges = getTSGChanges(tailTsgRef);
|
|
406
|
+
const splitBeats = headChanges
|
|
407
|
+
.map(({ beat }) => beat)
|
|
408
|
+
.filter((beat) => headBeat + 1e-6 < beat && beat < tailBeat - 1e-6);
|
|
409
|
+
if (splitBeats.length === 0)
|
|
410
|
+
return [];
|
|
411
|
+
const headScaledTime = timeToScaledTime(beatToTime(headBeat), headChanges);
|
|
412
|
+
const tailScaledTime = timeToScaledTime(beatToTime(tailBeat), tailChanges);
|
|
413
|
+
if (Math.abs(tailScaledTime - headScaledTime) < 1e-6)
|
|
414
|
+
return [];
|
|
415
|
+
if (ease !== EaseType.LINEAR) {
|
|
416
|
+
const sampleCount = 8;
|
|
417
|
+
for (let i = 1; i < sampleCount; i++) {
|
|
418
|
+
const scaledTime = lerp(headScaledTime, tailScaledTime, i / sampleCount);
|
|
419
|
+
const beat = timeToBeat(scaledTimeToTime(scaledTime, headChanges));
|
|
420
|
+
if (Number.isFinite(beat) && headBeat + 1e-6 < beat && beat < tailBeat - 1e-6) {
|
|
421
|
+
splitBeats.push(beat);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const uniqueSplitBeats = [...splitBeats]
|
|
426
|
+
.sort((a, b) => a - b)
|
|
427
|
+
.filter((beat, i, beats) => i === 0 || !nearlyEqual(beat, beats[i - 1]));
|
|
428
|
+
const headLane = getNum(headOriginal, 'lane');
|
|
429
|
+
const tailLane = getNum(tailOriginal, 'lane');
|
|
430
|
+
const headSize = getNum(headOriginal, 'size');
|
|
431
|
+
const tailSize = getNum(tailOriginal, 'size');
|
|
432
|
+
return uniqueSplitBeats.map((beat) => {
|
|
433
|
+
const scaledTime = timeToScaledTime(beatToTime(beat), headChanges);
|
|
434
|
+
const frac = unlerp(headScaledTime, tailScaledTime, scaledTime);
|
|
435
|
+
const easedFrac = applyEase(ease, frac);
|
|
436
|
+
return createConnectorAnchor(beat, lerp(headLane, tailLane, easedFrac), lerp(headSize, tailSize, easedFrac), tsg, kind);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
237
439
|
for (const { idx, e } of ext.connectors) {
|
|
238
440
|
const startRef = getField(e, 'start');
|
|
239
441
|
const headRef = getField(e, 'head');
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
442
|
+
const tailRef = getField(e, 'tail');
|
|
443
|
+
const rawHead = getNote(headRef);
|
|
444
|
+
const tail = getNote(tailRef);
|
|
445
|
+
const activeHead = getNote(startRef);
|
|
446
|
+
const usesStartAsHead = shouldUseStartAsHead(startRef, headRef);
|
|
447
|
+
const head = usesStartAsHead ? activeHead : rawHead;
|
|
448
|
+
const headOriginal = resolveOriginal(ext, usesStartAsHead ? startRef : headRef);
|
|
449
|
+
const tailOriginal = resolveOriginal(ext, tailRef);
|
|
243
450
|
const endRef = getField(e, 'end');
|
|
244
|
-
let
|
|
245
|
-
if (!
|
|
451
|
+
let activeTail = getNote(endRef);
|
|
452
|
+
if (!activeTail) {
|
|
246
453
|
const currentTailRef = getField(e, 'tail');
|
|
247
454
|
let ultimateTailRef = currentTailRef;
|
|
248
455
|
const visited = new Set();
|
|
@@ -257,32 +464,49 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
257
464
|
break;
|
|
258
465
|
}
|
|
259
466
|
}
|
|
260
|
-
|
|
467
|
+
activeTail = getNote(ultimateTailRef);
|
|
261
468
|
}
|
|
262
|
-
if (!
|
|
263
|
-
|
|
469
|
+
if (!activeTail) {
|
|
470
|
+
activeTail = tail;
|
|
264
471
|
}
|
|
265
|
-
if (!head || !tail || !
|
|
472
|
+
if (!head || !tail || !activeHead || !activeTail)
|
|
266
473
|
continue;
|
|
267
474
|
const kind = activeConnectorKindMapping[e.archetype];
|
|
268
475
|
const ease = easeTypeMapping[getNum(e, 'ease')] ?? EaseType.LINEAR;
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
476
|
+
const tsg = headOriginal ? getTSG(getField(headOriginal, 'timeScaleGroup')) : undefined;
|
|
477
|
+
const splitAnchors = headOriginal && tailOriginal
|
|
478
|
+
? getConnectorSplitAnchors(headOriginal, tailOriginal, tsg, kind, ease)
|
|
479
|
+
: [];
|
|
480
|
+
const segmentEase = splitAnchors.length > 0 ? EaseType.LINEAR : ease;
|
|
481
|
+
const segmentNotes = [head, ...splitAnchors, tail];
|
|
482
|
+
for (let i = 0; i < segmentNotes.length - 1; i++) {
|
|
483
|
+
const segmentHead = segmentNotes[i];
|
|
484
|
+
const segmentTail = segmentNotes[i + 1];
|
|
485
|
+
const connector = new EntityBuilder('Connector');
|
|
486
|
+
connector.set('head', segmentHead);
|
|
487
|
+
connector.set('tail', segmentTail);
|
|
488
|
+
connector.set('segmentHead', segmentHead);
|
|
489
|
+
connector.set('segmentTail', segmentTail);
|
|
490
|
+
connector.set('activeHead', activeHead);
|
|
491
|
+
connector.set('activeTail', activeTail);
|
|
492
|
+
finalEntities.push(connector);
|
|
493
|
+
}
|
|
494
|
+
const connectorLink = new EntityBuilder('Connector');
|
|
495
|
+
connectorLink.set('head', head);
|
|
496
|
+
connectorLink.set('tail', tail);
|
|
497
|
+
connectorLink.set('activeHead', activeHead);
|
|
498
|
+
connectorLink.set('activeTail', activeTail);
|
|
499
|
+
for (const segmentHead of segmentNotes.slice(0, -1)) {
|
|
500
|
+
segmentHead.set('connectorEase', segmentEase);
|
|
501
|
+
segmentHead.set('segmentKind', kind);
|
|
502
|
+
segmentHead.set('segmentAlpha', 1);
|
|
503
|
+
}
|
|
278
504
|
tail.set('segmentKind', kind);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
finalEntities.push(connector);
|
|
283
|
-
connectorsByIndex.set(idx, connector);
|
|
505
|
+
tail.set('segmentAlpha', 1);
|
|
506
|
+
activeHead.set('segmentKind', kind);
|
|
507
|
+
connectorsByIndex.set(idx, connectorLink);
|
|
284
508
|
if (e.name)
|
|
285
|
-
connectorsByName.set(e.name,
|
|
509
|
+
connectorsByName.set(e.name, connectorLink);
|
|
286
510
|
}
|
|
287
511
|
function getConn(ref) {
|
|
288
512
|
if (typeof ref === 'number')
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { USC } from './usc/index.js';
|
|
|
7
7
|
export * from './usc/index.js';
|
|
8
8
|
export { type ExtendedEntityData, type ExtendedEntityDataField, extendedToLevelData, mmwsToUSC, susToUSC, ucmmwsToLevelData, uscToLevelData, };
|
|
9
9
|
export declare const convertToLevelData: (input: string | Uint8Array | USC | LevelData, offset?: number) => LevelData;
|
|
10
|
-
export declare const version = "1.0.
|
|
10
|
+
export declare const version = "1.0.4";
|
|
11
11
|
export declare const databaseEngineItem: {
|
|
12
12
|
readonly name: "next-rush";
|
|
13
13
|
readonly version: 13;
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ export const convertToLevelData = (input, offset = 0) => {
|
|
|
65
65
|
}
|
|
66
66
|
return uscToLevelData(usc, offset, true, true);
|
|
67
67
|
};
|
|
68
|
-
export const version = '1.0.
|
|
68
|
+
export const version = '1.0.4';
|
|
69
69
|
export const databaseEngineItem = {
|
|
70
70
|
name: 'next-rush',
|
|
71
71
|
version: 13,
|