sonolus-next-rush-engine 1.0.3 → 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 +229 -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
|
@@ -146,6 +146,32 @@ function getNum(e, name, def = 0) {
|
|
|
146
146
|
function nearlyEqual(a, b) {
|
|
147
147
|
return Math.abs(a - b) < 1e-6;
|
|
148
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
|
+
}
|
|
149
175
|
function resolveOriginal(ext, ref) {
|
|
150
176
|
if (typeof ref === 'number')
|
|
151
177
|
return ext.get(ref);
|
|
@@ -160,6 +186,42 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
160
186
|
finalEntities.push(defaultTsg);
|
|
161
187
|
const init = new EntityBuilder('Initialization');
|
|
162
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
|
+
}
|
|
163
225
|
for (const { e } of ext.getByArch('#BPM_CHANGE')) {
|
|
164
226
|
const bpm = new EntityBuilder('#BPM_CHANGE');
|
|
165
227
|
bpm.set('#BEAT', getNum(e, '#BEAT'));
|
|
@@ -168,6 +230,8 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
168
230
|
}
|
|
169
231
|
const timescaleGroupsByIndex = new Map();
|
|
170
232
|
const timescaleGroupsByName = new Map();
|
|
233
|
+
const timescaleChangesByIndex = new Map();
|
|
234
|
+
const timescaleChangesByName = new Map();
|
|
171
235
|
for (const { idx, e } of ext.getByArch('TimeScaleGroup')) {
|
|
172
236
|
const group = new EntityBuilder('#TIMESCALE_GROUP');
|
|
173
237
|
finalEntities.push(group);
|
|
@@ -176,10 +240,15 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
176
240
|
timescaleGroupsByName.set(e.name, group);
|
|
177
241
|
let rawRef = getField(e, 'first');
|
|
178
242
|
const changes = [];
|
|
243
|
+
const changeInfos = [];
|
|
179
244
|
while (rawRef !== undefined) {
|
|
180
245
|
const raw = resolveOriginal(ext, rawRef);
|
|
181
246
|
if (!raw)
|
|
182
247
|
break;
|
|
248
|
+
changeInfos.push({
|
|
249
|
+
beat: getNum(raw, '#BEAT'),
|
|
250
|
+
timeScale: getNum(raw, 'timeScale'),
|
|
251
|
+
});
|
|
183
252
|
const change = new EntityBuilder('#TIMESCALE_CHANGE');
|
|
184
253
|
change.set('#BEAT', getNum(raw, '#BEAT'));
|
|
185
254
|
change.set('#TIMESCALE', getNum(raw, 'timeScale'));
|
|
@@ -200,6 +269,10 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
200
269
|
group.set('first', changes[0]);
|
|
201
270
|
}
|
|
202
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);
|
|
203
276
|
}
|
|
204
277
|
function getTSG(ref) {
|
|
205
278
|
if (typeof ref === 'number')
|
|
@@ -208,6 +281,59 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
208
281
|
return timescaleGroupsByName.get(ref);
|
|
209
282
|
return undefined;
|
|
210
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
|
+
}
|
|
211
337
|
const notesByIndex = new Map();
|
|
212
338
|
const notesByName = new Map();
|
|
213
339
|
const connectorsByIndex = new Map();
|
|
@@ -252,16 +378,78 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
252
378
|
nearlyEqual(getNum(start, 'lane'), getNum(head, 'lane')) &&
|
|
253
379
|
nearlyEqual(getNum(start, 'size'), getNum(head, 'size')));
|
|
254
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
|
+
}
|
|
255
439
|
for (const { idx, e } of ext.connectors) {
|
|
256
440
|
const startRef = getField(e, 'start');
|
|
257
441
|
const headRef = getField(e, 'head');
|
|
442
|
+
const tailRef = getField(e, 'tail');
|
|
258
443
|
const rawHead = getNote(headRef);
|
|
259
|
-
const tail = getNote(
|
|
260
|
-
const
|
|
261
|
-
const
|
|
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);
|
|
262
450
|
const endRef = getField(e, 'end');
|
|
263
|
-
let
|
|
264
|
-
if (!
|
|
451
|
+
let activeTail = getNote(endRef);
|
|
452
|
+
if (!activeTail) {
|
|
265
453
|
const currentTailRef = getField(e, 'tail');
|
|
266
454
|
let ultimateTailRef = currentTailRef;
|
|
267
455
|
const visited = new Set();
|
|
@@ -276,32 +464,49 @@ export const extendedToLevelData = (data, offset = 0) => {
|
|
|
276
464
|
break;
|
|
277
465
|
}
|
|
278
466
|
}
|
|
279
|
-
|
|
467
|
+
activeTail = getNote(ultimateTailRef);
|
|
280
468
|
}
|
|
281
|
-
if (!
|
|
282
|
-
|
|
469
|
+
if (!activeTail) {
|
|
470
|
+
activeTail = tail;
|
|
283
471
|
}
|
|
284
|
-
if (!head || !tail || !
|
|
472
|
+
if (!head || !tail || !activeHead || !activeTail)
|
|
285
473
|
continue;
|
|
286
474
|
const kind = activeConnectorKindMapping[e.archetype];
|
|
287
475
|
const ease = easeTypeMapping[getNum(e, 'ease')] ?? EaseType.LINEAR;
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
504
|
tail.set('segmentKind', kind);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
finalEntities.push(connector);
|
|
302
|
-
connectorsByIndex.set(idx, connector);
|
|
505
|
+
tail.set('segmentAlpha', 1);
|
|
506
|
+
activeHead.set('segmentKind', kind);
|
|
507
|
+
connectorsByIndex.set(idx, connectorLink);
|
|
303
508
|
if (e.name)
|
|
304
|
-
connectorsByName.set(e.name,
|
|
509
|
+
connectorsByName.set(e.name, connectorLink);
|
|
305
510
|
}
|
|
306
511
|
function getConn(ref) {
|
|
307
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,
|