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.
@@ -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 head = getNote(headRef);
241
- const tail = getNote(getField(e, 'tail'));
242
- const segmentHead = getNote(startRef);
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 segmentTail = getNote(endRef);
245
- if (!segmentTail) {
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
- segmentTail = getNote(ultimateTailRef);
467
+ activeTail = getNote(ultimateTailRef);
261
468
  }
262
- if (!segmentTail) {
263
- segmentTail = tail;
469
+ if (!activeTail) {
470
+ activeTail = tail;
264
471
  }
265
- if (!head || !tail || !segmentHead || !segmentTail)
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 connector = new EntityBuilder('Connector');
270
- connector.set('head', head);
271
- connector.set('tail', tail);
272
- connector.set('segmentHead', segmentHead);
273
- connector.set('segmentTail', segmentTail);
274
- connector.set('activeHead', segmentHead);
275
- connector.set('activeTail', segmentTail);
276
- head.set('connectorEase', ease);
277
- 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
+ }
278
504
  tail.set('segmentKind', kind);
279
- segmentHead.set('segmentKind', kind);
280
- segmentHead.set('segmentAlpha', 1);
281
- segmentTail.set('segmentAlpha', 1);
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, connector);
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.2";
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.2';
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.2",
3
+ "version": "1.0.4",
4
4
  "description": "Perspective-lane rhythm game for Sonolus",
5
5
  "author": "Hyeon2",
6
6
  "repository": {