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/LICENSE +21 -0
- package/README.md +9 -0
- package/chart.ts +481 -0
- package/chartTypes.ts +512 -0
- package/easing.ts +543 -0
- package/env.ts +7 -0
- package/evaluator.ts +173 -0
- package/event.ts +853 -0
- package/index.ts +11 -0
- package/judgeline.ts +605 -0
- package/jumparray.ts +234 -0
- package/note.ts +731 -0
- package/package.json +20 -0
- package/rpeChartCompiler.ts +425 -0
- package/time.ts +308 -0
- package/tsconfig.json +30 -0
- package/util.ts +26 -0
- package/version.ts +8 -0
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
|