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.
@@ -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(getField(e, 'tail'));
260
- const segmentHead = getNote(startRef);
261
- const head = shouldUseStartAsHead(startRef, headRef) ? segmentHead : rawHead;
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 segmentTail = getNote(endRef);
264
- if (!segmentTail) {
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
- segmentTail = getNote(ultimateTailRef);
467
+ activeTail = getNote(ultimateTailRef);
280
468
  }
281
- if (!segmentTail) {
282
- segmentTail = tail;
469
+ if (!activeTail) {
470
+ activeTail = tail;
283
471
  }
284
- if (!head || !tail || !segmentHead || !segmentTail)
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 connector = new EntityBuilder('Connector');
289
- connector.set('head', head);
290
- connector.set('tail', tail);
291
- connector.set('segmentHead', segmentHead);
292
- connector.set('segmentTail', segmentTail);
293
- connector.set('activeHead', segmentHead);
294
- connector.set('activeTail', segmentTail);
295
- head.set('connectorEase', ease);
296
- head.set('segmentKind', kind);
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
- segmentHead.set('segmentKind', kind);
299
- segmentHead.set('segmentAlpha', 1);
300
- segmentTail.set('segmentAlpha', 1);
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, connector);
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.3";
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.3';
68
+ export const version = '1.0.4';
69
69
  export const databaseEngineItem = {
70
70
  name: 'next-rush',
71
71
  version: 13,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonolus-next-rush-engine",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Perspective-lane rhythm game for Sonolus",
5
5
  "author": "Hyeon2",
6
6
  "repository": {