kipphi 2.0.0

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/note.ts ADDED
@@ -0,0 +1,731 @@
1
+ /**
2
+ * @author Zes M Young
3
+ */
4
+ import type { Chart } from "./chart";
5
+ import { NoteType, type NNListDataKPA, type NoteDataKPA, type NoteDataRPE, type NoteNodeDataKPA, type TimeT } from "./chartTypes";
6
+ import type { JudgeLine } from "./judgeline";
7
+ import { JumpArray } from "./jumparray";
8
+ import { TimeCalculator } from "./time";
9
+ import { hex2rgb, NodeType, rgb2hex } from "./util";
10
+
11
+ /// #declaration:global
12
+
13
+ const TC = TimeCalculator;
14
+
15
+ export type HEX = number;
16
+
17
+
18
+
19
+ const node2string = (node: AnyNN) => {
20
+ if (!node) {
21
+ return "" + node
22
+ }
23
+ if (node.type === NodeType.HEAD || node.type === NodeType.TAIL) {
24
+ return node.type === NodeType.HEAD ? "H" : node.type === NodeType.TAIL ? "T" : "???"
25
+ }
26
+ if (!node.notes) {
27
+ return "EventNode"
28
+ }
29
+ return `NN(${node.notes.length}) at ${node.startTime}`
30
+ }
31
+
32
+
33
+
34
+ export const notePropTypes = {
35
+ above: "boolean",
36
+ alpha: "number",
37
+ endTime: ["number", "number", "number"],
38
+ isFake: "boolean",
39
+ positionX: "number",
40
+ size: "number",
41
+ speed: "number",
42
+ startTime: ["number", "number", "number"],
43
+ type: "number",
44
+ visibleTime: "number",
45
+ visibleBeats: "number",
46
+ yOffset: "number",
47
+ tint: ["number", "number", "number"],
48
+ tintHitEffects: ["number", "number", "number"],
49
+ judgeSize: "number"
50
+ }
51
+
52
+ /**
53
+ * 音符
54
+ * Basic element in music game.
55
+ * Has 4 types: tap, drag, flick and hold.
56
+ * Only hold has endTime; others' endTime is equal to startTime.
57
+ * For this reason, holds are store in a special list (HNList),
58
+ * which is sorted by both startTime and endTime,
59
+ * so that they are accessed correctly and rapidly in the renderer.
60
+ * Note that Hold and HoldNode are not individually-declared classes.
61
+ * Hold is a note with type being NoteType.hold,
62
+ * while HoldNode is a node that contains holds.
63
+ */
64
+ export class Note {
65
+ above: boolean;
66
+ alpha: number;
67
+ endTime: [number, number, number]
68
+ isFake: boolean;
69
+ /** x coordinate in the judge line */
70
+ positionX: number;
71
+ size: number;
72
+ speed: number;
73
+ startTime: [number, number, number];
74
+ type: NoteType;
75
+ /** @deprecated */
76
+ visibleTime: number;
77
+ visibleBeats: number;
78
+ yOffset: number;
79
+ /*
80
+ * 和打击位置的距离,与yOffset和上下无关,为负不可见
81
+ positionY: number;
82
+ endPositionY?: number;
83
+ */
84
+ /*
85
+ next: NNOrTail;
86
+ previousSibling?: Note;
87
+ nextSibling: Note;
88
+ */
89
+
90
+ parentNode: NoteNode;
91
+ tint: HEX;
92
+ tintHitEffects: HEX;
93
+ judgeSize: number;
94
+
95
+ // readonly chart: Chart;
96
+ // readonly judgeLine: JudgeLine
97
+ // posPrevious?: Note;
98
+ // posNext?: Note;
99
+ // posPreviousSibling?: Note;
100
+ // posNextSibling: Note;
101
+ constructor(data: NoteDataRPE) {
102
+ this.above = data.above === 1;
103
+ this.alpha = data.alpha ?? 255;
104
+ this.endTime = data.type === NoteType.hold ? TimeCalculator.validateIp(data.endTime) : TimeCalculator.validateIp([...data.startTime]);
105
+ this.isFake = Boolean(data.isFake);
106
+ this.positionX = data.positionX;
107
+ this.size = data.size ?? 1.0;
108
+ this.speed = data.speed ?? 1.0;
109
+ this.startTime = TimeCalculator.validateIp(data.startTime);
110
+ this.type = data.type;
111
+ this.visibleTime = data.visibleTime;
112
+ // @ts-expect-error
113
+ this.yOffset = data.absoluteYOffset ?? data.yOffset * this.speed;
114
+ // @ts-expect-error 若data是RPE数据,则为undefined,无影响。
115
+ // 当然也有可能是KPA数据但是就是没有给
116
+ this.visibleBeats = data.visibleBeats;
117
+
118
+ this.tint = data.tint ? rgb2hex(data.tint) : undefined;
119
+ this.tintHitEffects = data.tintHitEffects ? rgb2hex(data.tintHitEffects) : undefined;
120
+ this.judgeSize = data.judgeSize ?? this.size;
121
+ /*
122
+ this.previous = null;
123
+ this.next = null;
124
+ this.previousSibling = null;
125
+ this.nextSibling = null;
126
+ */
127
+ }
128
+ static fromKPAJSON(data: NoteDataKPA, timeCalculator: TimeCalculator) {
129
+ const note = new Note(data);
130
+ if (!note.visibleBeats) {
131
+ note.computeVisibleBeats(timeCalculator);
132
+ }
133
+ return note;
134
+ }
135
+ computeVisibleBeats(timeCalculator: TimeCalculator) {
136
+ if (!this.visibleTime || this.visibleTime >= 90000) {
137
+ this.visibleBeats = Infinity;
138
+ return;
139
+ }
140
+ const hitBeats = TimeCalculator.toBeats(this.startTime);
141
+ const hitSeconds = timeCalculator.toSeconds(hitBeats);
142
+ const visabilityChangeSeconds = hitSeconds - this.visibleTime;
143
+ const visabilityChangeBeats = timeCalculator.secondsToBeats(visabilityChangeSeconds);
144
+ this.visibleBeats = hitBeats - visabilityChangeBeats;
145
+ }
146
+ /**
147
+ *
148
+ * @param offset
149
+ * @returns
150
+ */
151
+ clone(offset: TimeT) {
152
+ const data = this.dumpKPA();
153
+ data.startTime = TimeCalculator.add(data.startTime, offset);
154
+ data.endTime = TimeCalculator.add(data.endTime, offset); // 踩坑
155
+ return new Note(data);
156
+ }
157
+ /*
158
+ static connectPosSibling(note1: Note, note2: Note) {
159
+ note1.posNextSibling = note2;
160
+ note2.posPreviousSibling = note1;
161
+ }
162
+ static connectPos(note1: Note, note2: Note) {
163
+ note1.posNext = note2;
164
+ note2.posPrevious = note1;
165
+ }
166
+ */
167
+ dumpRPE(timeCalculator: TimeCalculator): NoteDataRPE {
168
+ let visibleTime: number;
169
+ if (this.visibleBeats !== Infinity) {
170
+ const beats = TimeCalculator.toBeats(this.startTime);
171
+ this.visibleBeats = timeCalculator.segmentToSeconds(beats - this.visibleBeats, beats);
172
+ } else {
173
+ visibleTime = 99999.0
174
+ }
175
+ return {
176
+ above: this.above ? 1 : 0,
177
+ alpha: this.alpha,
178
+ endTime: this.endTime,
179
+ isFake: this.isFake ? 1 : 0,
180
+ positionX: this.positionX,
181
+ size: this.size,
182
+ startTime: this.startTime,
183
+ type: this.type,
184
+ visibleTime: visibleTime,
185
+ yOffset: this.yOffset / this.speed,
186
+ speed: this.speed,
187
+ tint: this.tint !== undefined ? hex2rgb(this.tint) : undefined,
188
+ tintHitEffects: this.tint !== undefined ? hex2rgb(this.tintHitEffects) : undefined
189
+ }
190
+ }
191
+ dumpKPA(): NoteDataKPA {
192
+ return {
193
+ above: this.above ? 1 : 0,
194
+ alpha: this.alpha,
195
+ endTime: this.endTime,
196
+ isFake: this.isFake ? 1 : 0,
197
+ positionX: this.positionX,
198
+ size: this.size,
199
+ startTime: this.startTime,
200
+ type: this.type,
201
+ visibleBeats: this.visibleBeats,
202
+ yOffset: this.yOffset / this.speed,
203
+ /** 新KPAJSON认为YOffset就应该是个绝对的值,不受速度影响 */
204
+ /** 但是有历史包袱,所以加字段 */
205
+ absoluteYOffset: this.yOffset,
206
+ speed: this.speed,
207
+ tint: this.tint !== undefined ? hex2rgb(this.tint) : undefined,
208
+ tintHitEffects: this.tint !== undefined ? hex2rgb(this.tintHitEffects) : undefined,
209
+ judgeSize: this.judgeSize && this.judgeSize !== 1.0 ? this.judgeSize : undefined,
210
+ }
211
+ }
212
+ }
213
+
214
+ type Connectee = NoteNode | NNNode
215
+
216
+
217
+
218
+ export type NNOrHead = NoteNode | NoteNodeLike<NodeType.HEAD>
219
+ export type NNOrTail = NoteNode | NoteNodeLike<NodeType.TAIL>
220
+ export type AnyNN = NoteNode | NoteNodeLike<NodeType.HEAD> | NoteNodeLike<NodeType.TAIL>
221
+
222
+ export class NoteNodeLike<T extends NodeType> {
223
+ type: T;
224
+ next: NNOrTail;
225
+ _previous: WeakRef<NNOrHead> | null = null;
226
+ parentSeq: NNList;
227
+ get previous() {
228
+ if (!this._previous) return null;
229
+ return this._previous.deref()
230
+ }
231
+ set previous(val) {
232
+ if (!val) {
233
+ this._previous = null;
234
+ return;
235
+ }
236
+ this._previous = new WeakRef(val)
237
+ }
238
+ constructor(type: T) {
239
+ this.type = type;
240
+ }
241
+ }
242
+
243
+ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
244
+ totalNode: NNNode;
245
+ readonly startTime: TimeT
246
+ /**
247
+ * The notes it contains.
248
+ * If they are holds, they are ordered by their endTime, from late to early.
249
+ */
250
+ readonly notes: Note[];
251
+ override parentSeq: NNList
252
+ chart: Chart;
253
+ private static count = 0;
254
+ id: number;
255
+ constructor(time: TimeT) {
256
+ super(NodeType.MIDDLE);
257
+ this.startTime = TimeCalculator.validateIp([...time]);
258
+ this.notes = [];
259
+ this.id = NoteNode.count++;
260
+ }
261
+ static fromKPAJSON(data: NoteNodeDataKPA, timeCalculator: TimeCalculator) {
262
+ const node = new NoteNode(data.startTime);
263
+ for (let noteData of data.notes) {
264
+ const note = Note.fromKPAJSON(noteData, timeCalculator);
265
+ node.add(note);
266
+ }
267
+ return node
268
+ }
269
+ get isHold() {
270
+ return this.parentSeq instanceof HNList
271
+ }
272
+ get endTime(): TimeT {
273
+ if (this.notes.length === 0) {
274
+ return this.startTime; // 改了半天这个逻辑本来就是对的()
275
+ }
276
+ return (this.notes.length === 0 || this.notes[0].type !== NoteType.hold) ? this.startTime : this.notes[0].endTime
277
+ }
278
+ add(note: Note) {
279
+ if (!TimeCalculator.eq(note.startTime, this.startTime)) {
280
+ console.warn("Wrong addition!")
281
+ }
282
+ this.notes.push(note);
283
+ note.parentNode = this
284
+ this.sort(this.notes.length - 1);
285
+ }
286
+ sort(note: Note): void;
287
+ /**
288
+ * 其他部分均已有序,通过冒泡排序把发生变更的NoteNode移动到正确的位置
289
+ * @param index 待排序的Note的索引
290
+ */
291
+ sort(index: number): void;
292
+ sort(index: number | Note) {
293
+ if (typeof index !== "number") {
294
+ index = this.notes.indexOf(index);
295
+ if (index === -1) {
296
+ return;
297
+ }
298
+ }
299
+ if (!this.isHold) {
300
+ return;
301
+ }
302
+ const {notes} = this;
303
+ const note = notes[index];
304
+ for (let i = index; i > 0; i--) {
305
+ const prev = notes[i - 1];
306
+ if (TimeCalculator.lt(prev.endTime, note.endTime)) {
307
+ // swap
308
+ notes[i] = prev;
309
+ notes[i - 1] = note;
310
+ } else {
311
+ break;
312
+ }
313
+ }
314
+ for (let i = index; i < notes.length - 1; i++) {
315
+ const next = notes[i + 1];
316
+ if (TimeCalculator.gt(next.endTime, note.endTime)) {
317
+ // swap
318
+ notes[i] = next;
319
+ notes[i + 1] = note;
320
+ } else {
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ remove(note: Note) {
326
+ this.notes.splice(this.notes.indexOf(note), 1)
327
+ note.parentNode = null
328
+ }
329
+ static disconnect(note1: NNOrHead, note2: NNOrTail) {
330
+ if (note1) {
331
+ note1.next = null;
332
+ }
333
+ if (note2) {
334
+ note2.previous = null;
335
+ }
336
+
337
+ }
338
+ static connect(note1: NNOrHead, note2: NNOrTail) {
339
+ if (note1) {
340
+ note1.next = note2;
341
+ }
342
+ if (note2) {
343
+ note2.previous = note1;
344
+ }
345
+ if (note1 && note2) {
346
+ note2.parentSeq = note1.parentSeq
347
+ }
348
+ }
349
+ static insert(note1: NNOrHead, inserted: NoteNode, note2: NNOrTail) {
350
+ this.connect(note1, inserted);
351
+ this.connect(inserted, note2);
352
+ }
353
+ dump(): NoteNodeDataKPA {
354
+ return {
355
+ notes: this.notes.map(note => note.dumpKPA()),
356
+ startTime: this.startTime
357
+ }
358
+ }
359
+ }
360
+
361
+ export class NNList {
362
+ /** 格式为#xxoxx或$xxoxx,亦可自命名 */
363
+ id: string;
364
+ head: NoteNodeLike<NodeType.HEAD>;
365
+ tail: NoteNodeLike<NodeType.TAIL>;
366
+ currentPoint: NNOrHead;
367
+ /** 定位上个Note头已过,本身未到的Note */
368
+ jump: JumpArray<AnyNN>;
369
+ timesWithNotes: number;
370
+ timeRanges: [number, number][];
371
+ effectiveBeats: number;
372
+
373
+ parentLine: JudgeLine;
374
+ constructor(
375
+ public speed: number,
376
+ public medianYOffset: number = 0,
377
+
378
+
379
+ effectiveBeats?: number
380
+ ) {
381
+ this.head = new NoteNodeLike(NodeType.HEAD);
382
+ this.head.parentSeq = this;
383
+ this.currentPoint = this.head;
384
+ // this.currentBranchPoint = <NoteNode>{startTime: [-1, 0, 1]}
385
+ this.tail = new NoteNodeLike(NodeType.TAIL);
386
+ this.tail.parentSeq = this;
387
+ this.timesWithNotes = 0;
388
+ this.effectiveBeats = effectiveBeats
389
+ }
390
+ /** 此方法永远用于最新KPAJSON */
391
+ static fromKPAJSON<T extends boolean>(isHold: T, effectiveBeats: number, data: NNListDataKPA, nnnList: NNNList, timeCalculator: TimeCalculator): T extends true ? HNList : NNList {
392
+ const list: T extends true ? HNList : NNList = isHold ? new HNList(data.speed, data.medianYOffset, effectiveBeats) : new NNList(data.speed, data.medianYOffset, effectiveBeats)
393
+ const nnlength = data.noteNodes.length
394
+ let cur: NNOrHead = list.head;
395
+ for (let i = 0; i < nnlength; i++) {
396
+ const nnData = data.noteNodes[i];
397
+ const nn = NoteNode.fromKPAJSON(nnData, timeCalculator);
398
+ NoteNode.connect(cur, nn);
399
+ cur = nn;
400
+ nnnList.addNoteNode(nn);
401
+ }
402
+ NoteNode.connect(cur, list.tail);
403
+ list.initJump();
404
+ return list
405
+ }
406
+ initJump() {
407
+ const originalListLength = this.timesWithNotes;
408
+ if (!this.effectiveBeats) {
409
+ const prev = this.tail.previous
410
+ if (prev.type === NodeType.HEAD) {
411
+ return;
412
+ }
413
+ this.effectiveBeats = TimeCalculator.toBeats(prev.endTime)
414
+ }
415
+ const effectiveBeats: number = this.effectiveBeats;
416
+ this.jump = new JumpArray<AnyNN>(
417
+ this.head,
418
+ this.tail,
419
+ originalListLength,
420
+ effectiveBeats,
421
+ (node: AnyNN) => {
422
+ if (node.type === NodeType.TAIL) {
423
+ return [null, null]
424
+ }
425
+ const nextNode = node.next;
426
+ const startTime = (node.type === NodeType.HEAD) ? 0 : TimeCalculator.toBeats(node.startTime)
427
+ return [startTime, nextNode]
428
+ },
429
+ // @ts-ignore
430
+ (note: NoteNode, beats: number) => {
431
+ return TimeCalculator.toBeats(note.startTime) >= beats ? false : <NoteNode>note.next; // getNodeAt有guard
432
+ })
433
+ }
434
+ /**
435
+ *
436
+ * @param beats 目标位置
437
+ * @param beforeEnd 指定选取该时刻之前还是之后第一个Node,对于非Hold无影响
438
+ * @param pointer 指针,实现查询位置缓存
439
+ * @returns
440
+ */
441
+ getNodeAt(beats: number, beforeEnd=false): NNOrTail {
442
+ return this.jump.getNodeAt(beats) as NNOrTail;
443
+ }
444
+ /**
445
+ * Get or create a node of given time
446
+ * @param time
447
+ * @returns
448
+ */
449
+ getNodeOf(time: TimeT): NoteNode {
450
+ let node = this.getNodeAt(TimeCalculator.toBeats(time), false)
451
+ .previous;
452
+
453
+
454
+ let isEqual = node.type !== NodeType.HEAD && TimeCalculator.eq((node as NoteNode).startTime, time)
455
+ if (node.next.type !== NodeType.TAIL && TimeCalculator.eq((node.next as NoteNode).startTime, time)) {
456
+ isEqual = true;
457
+ node = node.next;
458
+ }
459
+
460
+ if (!isEqual) {
461
+ const newNode = new NoteNode(time);
462
+ const next = node.next
463
+ NoteNode.insert(node, newNode, next);
464
+ // console.log("created:", node2string(newNode))
465
+ this.jump.updateRange(node, next);
466
+ // console.log("pl", this.parentLine)
467
+
468
+ if (this.parentLine?.chart) {
469
+ this.parentLine.chart.nnnList.getNode(time).add(newNode)
470
+ }
471
+
472
+ return newNode
473
+ } else {
474
+ return node;
475
+ }
476
+ }
477
+ dumpKPA(): NNListDataKPA {
478
+ const nodes: NoteNodeDataKPA[] = []
479
+ let node: NNOrTail = this.head.next
480
+ while (node.type !== NodeType.TAIL) {
481
+ nodes.push(node.dump())
482
+ node = node.next
483
+ }
484
+ return {
485
+ speed: this.speed,
486
+ medianYOffset: this.medianYOffset,
487
+ noteNodes: nodes
488
+ }
489
+ }
490
+
491
+ getNodesFromOneAndRangeRight(node: NoteNode, rangeRight: TimeT) {
492
+ const arr: NoteNode[] = []
493
+ for (; !TC.gt(node.startTime, rangeRight); ) {
494
+ arr.push(node);
495
+ const next = node.next;
496
+ if (next.type === NodeType.TAIL) {
497
+ break;
498
+ }
499
+ node = next;
500
+ }
501
+ return arr;
502
+ }
503
+ getNodesAfterOne(node: NoteNode) {
504
+ const arr: NoteNode[] = []
505
+ while (true) {
506
+ const next = node.next;
507
+ if (next.type === NodeType.TAIL) {
508
+ break;
509
+ }
510
+ node = next;
511
+ arr.push(node);
512
+ }
513
+ return arr;
514
+ }
515
+
516
+ clearEmptyNodes(updatesJump: boolean = true) {
517
+ let node = this.head.next;
518
+ let lastNonEmptyNode: NoteNode = null;
519
+ while (node.type !== NodeType.TAIL) {
520
+ if (node.notes.length === 0) {
521
+ const next = node.next;
522
+ NoteNode.disconnect(node.previous, node);
523
+ node = next;
524
+ } else {
525
+ if (lastNonEmptyNode !== node.previous) {
526
+ NoteNode.disconnect(node.previous, node);
527
+ NoteNode.connect(lastNonEmptyNode, node);
528
+ }
529
+ lastNonEmptyNode = node;
530
+ node = node.next;
531
+ }
532
+ }
533
+ if (updatesJump) {
534
+ this.jump.updateRange(this.head, this.tail);
535
+ if (this instanceof HNList) {
536
+ this.holdTailJump.updateRange(this.head, this.tail);
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+
543
+ /**
544
+ * HoldNode的链表
545
+ * HN is the abbreviation of HoldNode, which is not individually declared.
546
+ * A NN that contains holds (a type of note) is a HN.
547
+ */
548
+ export class HNList extends NNList {
549
+ /**
550
+ * 最早的还未结束Hold
551
+ */
552
+ holdTailJump: JumpArray<AnyNN>;
553
+ constructor(speed: number, medianYOffset: number, effectiveBeats?: number) {
554
+ super(speed, medianYOffset, effectiveBeats)
555
+ }
556
+ override initJump(): void {
557
+ super.initJump()
558
+ const originalListLength = this.timesWithNotes;
559
+ const effectiveBeats: number = this.effectiveBeats;
560
+
561
+ this.holdTailJump = new JumpArray<AnyNN>(
562
+ this.head,
563
+ this.tail,
564
+ originalListLength,
565
+ effectiveBeats,
566
+ (node) => {
567
+ if (node.type === NodeType.TAIL) {
568
+ return [null, null]
569
+ }
570
+ if (!node) debugger
571
+ const nextNode = node.next;
572
+ const endTime = node.type === NodeType.HEAD ? 0 : TimeCalculator.toBeats(node.endTime)
573
+ return [endTime, nextNode]
574
+ },
575
+ // @ts-ignore
576
+ (node: NoteNode, beats: number) => {
577
+ return TimeCalculator.toBeats(node.endTime) >= beats ? false : <NoteNode>node.next; // getNodeAt有guard
578
+ }
579
+ )
580
+ }
581
+
582
+ override getNodeAt(beats: number, beforeEnd=false): NNOrTail {
583
+ return beforeEnd ? this.holdTailJump.getNodeAt(beats) as NNOrTail : this.jump.getNodeAt(beats) as NNOrTail;
584
+ }
585
+ // unused
586
+ insertNoteJumpUpdater(note: NoteNode): () => void {
587
+ const {previous, next} = note
588
+ return () => {
589
+ this.jump.updateRange(previous, next)
590
+ this.holdTailJump.updateRange(previous, next)
591
+ }
592
+ }
593
+ }
594
+
595
+ export type NNNOrHead = NNNode | NNNodeLike<NodeType.HEAD>;
596
+ export type NNNOrTail = NNNode | NNNodeLike<NodeType.TAIL>;
597
+ type AnyNNN = NNNode | NNNodeLike<NodeType.HEAD> | NNNodeLike<NodeType.TAIL>;
598
+
599
+ class NNNodeLike<T extends NodeType> {
600
+ previous: NNNOrHead;
601
+ next: NNNOrTail;
602
+ startTime: TimeT;
603
+ constructor(public type: T) {
604
+ if (type === NodeType.HEAD) {
605
+ this.startTime = [0, 0, 1];
606
+ } else if (type === NodeType.TAIL) {
607
+ this.startTime = [Infinity, 0, 1];
608
+ }
609
+ }
610
+ }
611
+
612
+ export class NNNode extends NNNodeLike<NodeType.MIDDLE> {
613
+ readonly noteNodes: NoteNode[];
614
+ readonly holdNodes: NoteNode[];
615
+ override readonly startTime: TimeT;
616
+ noteOfType: [number, number, number, number]
617
+ constructor(time: TimeT) {
618
+ super(NodeType.MIDDLE);
619
+ this.noteNodes = []
620
+ this.holdNodes = [];
621
+ this.startTime = TimeCalculator.validateIp([...time])
622
+ }
623
+ get endTime() {
624
+ let latest: TimeT = this.startTime;
625
+ for (let index = 0; index < this.holdNodes.length; index++) {
626
+ const element = this.holdNodes[index];
627
+ if (TC.gt(element.endTime, latest)) {
628
+ latest = element.endTime
629
+ }
630
+ }
631
+ return latest
632
+ }
633
+ add(node: NoteNode) {
634
+ if (node.isHold) {
635
+ this.holdNodes.push(node)
636
+ } else {
637
+
638
+ this.noteNodes.push(node)
639
+ }
640
+ node.totalNode = this;
641
+ }
642
+
643
+ static connect(note1: NNNOrHead, note2: NNNOrTail) {
644
+ if (note1) {
645
+ note1.next = note2;
646
+ }
647
+ if (note2) {
648
+ note2.previous = note1;
649
+ }
650
+ }
651
+ static insert(note1: NNNOrHead, inserted: NNNode, note2: NNNOrTail) {
652
+ this.connect(note1, inserted);
653
+ this.connect(inserted, note2);
654
+ }
655
+ }
656
+
657
+
658
+ /**
659
+ * 二级音符节点链表
660
+ * contains NNNs
661
+ * NNN is the abbreviation of NoteNodeNode, which store note (an element in music game) nodes with same startTime
662
+ * NN is the abbreviation of NoteNode, which stores the notes with the same startTime.
663
+ */
664
+ export class NNNList {
665
+ jump: JumpArray<AnyNNN>
666
+ parentChart: Chart;
667
+ head: NNNodeLike<NodeType.HEAD>;
668
+ tail: NNNodeLike<NodeType.TAIL>;
669
+
670
+
671
+ effectiveBeats: number;
672
+ timesWithNotes: number;
673
+ constructor(effectiveBeats: number) {
674
+ this.effectiveBeats = effectiveBeats;
675
+ this.head = new NNNodeLike(NodeType.HEAD);
676
+ this.tail = new NNNodeLike(NodeType.TAIL);
677
+ NNNode.connect(this.head, this.tail)
678
+ this.initJump()
679
+ }
680
+ initJump() {
681
+ const originalListLength = this.timesWithNotes || 512;
682
+ /*
683
+ if (!this.effectiveBeats) {
684
+ this.effectiveBeats = TimeCalculator.toBeats(this.tail.previous.endTime)
685
+ }
686
+ */
687
+ const effectiveBeats: number = this.effectiveBeats;
688
+ this.jump = new JumpArray<AnyNNN>(
689
+ this.head,
690
+ this.tail,
691
+ originalListLength,
692
+ effectiveBeats,
693
+ (node: NNNOrHead | NNNodeLike<NodeType.TAIL>) => {
694
+ if (node.type === NodeType.TAIL) {
695
+ return [null, null]
696
+ }
697
+ const nextNode = node.next;
698
+ const startTime = node.type === NodeType.HEAD ? 0 : TimeCalculator.toBeats((node as NNNode).startTime)
699
+ return [startTime, nextNode]
700
+ },
701
+ // @ts-ignore
702
+ (note: NNNode, beats: number) => {
703
+ return TimeCalculator.toBeats(note.startTime) >= beats ? false : <NNNode>note.next; // getNodeAt有guard
704
+ }
705
+ /*,
706
+ (note: Note) => {
707
+ const prev = note.previous;
708
+ return prev.type === NodeType.HEAD ? note : prev
709
+ })*/)
710
+ }
711
+ getNodeAt(beats: number, beforeEnd=false): NNNode | NNNodeLike<NodeType.TAIL> {
712
+ return this.jump.getNodeAt(beats) as NNNode | NNNodeLike<NodeType.TAIL>;
713
+ }
714
+ getNode(time: TimeT): NNNode {
715
+ const node = this.getNodeAt(TimeCalculator.toBeats(time), false).previous;
716
+ if (node.type === NodeType.HEAD || TimeCalculator.ne((node as NNNode).startTime, time)) {
717
+ const newNode = new NNNode(time);
718
+ const next = node.next
719
+ NNNode.insert(node, newNode, next);
720
+ this.jump.updateRange(node, next)
721
+ return newNode
722
+ } else {
723
+ return node as NNNode;
724
+ }
725
+ }
726
+ addNoteNode(noteNode: NoteNode): void {
727
+ this.getNode(noteNode.startTime).add(noteNode);
728
+ }
729
+ }
730
+
731
+ /// #enddeclaration