kipphi 2.0.0 → 2.1.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/README.md +1 -3
- package/basic.ts +285 -0
- package/bpm.ts +332 -0
- package/chart.ts +418 -106
- package/chartType.schema.json +584 -0
- package/chartType2.schema.json +1107 -0
- package/chartTypes.ts +131 -30
- package/easing.ts +125 -90
- package/env.ts +208 -0
- package/evaluator.ts +106 -20
- package/event.ts +357 -255
- package/index.d.ts +3055 -0
- package/index.js +5530 -0
- package/index.ts +17 -11
- package/judgeline.ts +395 -94
- package/jumparray.ts +10 -11
- package/line.ts +246 -0
- package/macro.ts +215 -0
- package/note.ts +32 -55
- package/operation/basic.ts +285 -0
- package/operation/chart.ts +21 -0
- package/operation/event.ts +511 -0
- package/operation/index.ts +6 -0
- package/operation/line.ts +304 -0
- package/operation/macro.ts +60 -0
- package/operation/note.ts +457 -0
- package/package.json +7 -1
- package/rpeChartCompiler.ts +133 -98
- package/time.ts +35 -223
- package/tsconfig.json +2 -3
- package/util.ts +21 -1
- package/version.ts +2 -1
package/note.ts
CHANGED
|
@@ -5,30 +5,17 @@ import type { Chart } from "./chart";
|
|
|
5
5
|
import { NoteType, type NNListDataKPA, type NoteDataKPA, type NoteDataRPE, type NoteNodeDataKPA, type TimeT } from "./chartTypes";
|
|
6
6
|
import type { JudgeLine } from "./judgeline";
|
|
7
7
|
import { JumpArray } from "./jumparray";
|
|
8
|
-
import { TimeCalculator } from "./
|
|
8
|
+
import { type TimeCalculator } from "./bpm";
|
|
9
|
+
import TC from "./time";
|
|
9
10
|
import { hex2rgb, NodeType, rgb2hex } from "./util";
|
|
10
11
|
|
|
11
12
|
/// #declaration:global
|
|
12
13
|
|
|
13
|
-
const TC = TimeCalculator;
|
|
14
14
|
|
|
15
15
|
export type HEX = number;
|
|
16
16
|
|
|
17
17
|
|
|
18
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
19
|
|
|
33
20
|
|
|
34
21
|
export const notePropTypes = {
|
|
@@ -98,18 +85,18 @@ export class Note {
|
|
|
98
85
|
// posNext?: Note;
|
|
99
86
|
// posPreviousSibling?: Note;
|
|
100
87
|
// posNextSibling: Note;
|
|
101
|
-
constructor(data: NoteDataRPE) {
|
|
88
|
+
constructor(data: NoteDataRPE | NoteDataKPA) {
|
|
102
89
|
this.above = data.above === 1;
|
|
103
90
|
this.alpha = data.alpha ?? 255;
|
|
104
|
-
this.endTime = data.type === NoteType.hold ?
|
|
91
|
+
this.endTime = data.type === NoteType.hold ? TC.validateIp(data.endTime) : TC.validateIp([...data.startTime]);
|
|
105
92
|
this.isFake = Boolean(data.isFake);
|
|
106
93
|
this.positionX = data.positionX;
|
|
107
94
|
this.size = data.size ?? 1.0;
|
|
108
95
|
this.speed = data.speed ?? 1.0;
|
|
109
|
-
this.startTime =
|
|
96
|
+
this.startTime = TC.validateIp(data.startTime);
|
|
110
97
|
this.type = data.type;
|
|
111
98
|
this.visibleTime = data.visibleTime;
|
|
112
|
-
// @ts-expect-error
|
|
99
|
+
// @ts-expect-error 若data是RPE数据,则为undefined,无影响。
|
|
113
100
|
this.yOffset = data.absoluteYOffset ?? data.yOffset * this.speed;
|
|
114
101
|
// @ts-expect-error 若data是RPE数据,则为undefined,无影响。
|
|
115
102
|
// 当然也有可能是KPA数据但是就是没有给
|
|
@@ -137,7 +124,7 @@ export class Note {
|
|
|
137
124
|
this.visibleBeats = Infinity;
|
|
138
125
|
return;
|
|
139
126
|
}
|
|
140
|
-
const hitBeats =
|
|
127
|
+
const hitBeats = TC.toBeats(this.startTime);
|
|
141
128
|
const hitSeconds = timeCalculator.toSeconds(hitBeats);
|
|
142
129
|
const visabilityChangeSeconds = hitSeconds - this.visibleTime;
|
|
143
130
|
const visabilityChangeBeats = timeCalculator.secondsToBeats(visabilityChangeSeconds);
|
|
@@ -150,8 +137,8 @@ export class Note {
|
|
|
150
137
|
*/
|
|
151
138
|
clone(offset: TimeT) {
|
|
152
139
|
const data = this.dumpKPA();
|
|
153
|
-
data.startTime =
|
|
154
|
-
data.endTime =
|
|
140
|
+
data.startTime = TC.add(data.startTime, offset);
|
|
141
|
+
data.endTime = TC.add(data.endTime, offset); // 踩坑
|
|
155
142
|
return new Note(data);
|
|
156
143
|
}
|
|
157
144
|
/*
|
|
@@ -167,7 +154,7 @@ export class Note {
|
|
|
167
154
|
dumpRPE(timeCalculator: TimeCalculator): NoteDataRPE {
|
|
168
155
|
let visibleTime: number;
|
|
169
156
|
if (this.visibleBeats !== Infinity) {
|
|
170
|
-
const beats =
|
|
157
|
+
const beats = TC.toBeats(this.startTime);
|
|
171
158
|
this.visibleBeats = timeCalculator.segmentToSeconds(beats - this.visibleBeats, beats);
|
|
172
159
|
} else {
|
|
173
160
|
visibleTime = 99999.0
|
|
@@ -198,7 +185,7 @@ export class Note {
|
|
|
198
185
|
size: this.size,
|
|
199
186
|
startTime: this.startTime,
|
|
200
187
|
type: this.type,
|
|
201
|
-
visibleBeats: this.visibleBeats,
|
|
188
|
+
visibleBeats: this.visibleBeats === Infinity ? undefined : this.visibleBeats, // 无穷不要保存,节约空间
|
|
202
189
|
yOffset: this.yOffset / this.speed,
|
|
203
190
|
/** 新KPAJSON认为YOffset就应该是个绝对的值,不受速度影响 */
|
|
204
191
|
/** 但是有历史包袱,所以加字段 */
|
|
@@ -211,7 +198,6 @@ export class Note {
|
|
|
211
198
|
}
|
|
212
199
|
}
|
|
213
200
|
|
|
214
|
-
type Connectee = NoteNode | NNNode
|
|
215
201
|
|
|
216
202
|
|
|
217
203
|
|
|
@@ -254,13 +240,13 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
|
|
|
254
240
|
id: number;
|
|
255
241
|
constructor(time: TimeT) {
|
|
256
242
|
super(NodeType.MIDDLE);
|
|
257
|
-
this.startTime =
|
|
243
|
+
this.startTime = TC.validateIp([...time]);
|
|
258
244
|
this.notes = [];
|
|
259
245
|
this.id = NoteNode.count++;
|
|
260
246
|
}
|
|
261
247
|
static fromKPAJSON(data: NoteNodeDataKPA, timeCalculator: TimeCalculator) {
|
|
262
248
|
const node = new NoteNode(data.startTime);
|
|
263
|
-
for (
|
|
249
|
+
for (const noteData of data.notes) {
|
|
264
250
|
const note = Note.fromKPAJSON(noteData, timeCalculator);
|
|
265
251
|
node.add(note);
|
|
266
252
|
}
|
|
@@ -276,7 +262,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
|
|
|
276
262
|
return (this.notes.length === 0 || this.notes[0].type !== NoteType.hold) ? this.startTime : this.notes[0].endTime
|
|
277
263
|
}
|
|
278
264
|
add(note: Note) {
|
|
279
|
-
if (!
|
|
265
|
+
if (!TC.eq(note.startTime, this.startTime)) {
|
|
280
266
|
console.warn("Wrong addition!")
|
|
281
267
|
}
|
|
282
268
|
this.notes.push(note);
|
|
@@ -303,7 +289,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
|
|
|
303
289
|
const note = notes[index];
|
|
304
290
|
for (let i = index; i > 0; i--) {
|
|
305
291
|
const prev = notes[i - 1];
|
|
306
|
-
if (
|
|
292
|
+
if (TC.lt(prev.endTime, note.endTime)) {
|
|
307
293
|
// swap
|
|
308
294
|
notes[i] = prev;
|
|
309
295
|
notes[i - 1] = note;
|
|
@@ -313,7 +299,7 @@ export class NoteNode extends NoteNodeLike<NodeType.MIDDLE> {
|
|
|
313
299
|
}
|
|
314
300
|
for (let i = index; i < notes.length - 1; i++) {
|
|
315
301
|
const next = notes[i + 1];
|
|
316
|
-
if (
|
|
302
|
+
if (TC.gt(next.endTime, note.endTime)) {
|
|
317
303
|
// swap
|
|
318
304
|
notes[i] = next;
|
|
319
305
|
notes[i + 1] = note;
|
|
@@ -410,7 +396,7 @@ export class NNList {
|
|
|
410
396
|
if (prev.type === NodeType.HEAD) {
|
|
411
397
|
return;
|
|
412
398
|
}
|
|
413
|
-
this.effectiveBeats =
|
|
399
|
+
this.effectiveBeats = TC.toBeats(prev.endTime)
|
|
414
400
|
}
|
|
415
401
|
const effectiveBeats: number = this.effectiveBeats;
|
|
416
402
|
this.jump = new JumpArray<AnyNN>(
|
|
@@ -423,12 +409,11 @@ export class NNList {
|
|
|
423
409
|
return [null, null]
|
|
424
410
|
}
|
|
425
411
|
const nextNode = node.next;
|
|
426
|
-
const startTime = (node.type === NodeType.HEAD) ? 0 :
|
|
412
|
+
const startTime = (node.type === NodeType.HEAD) ? 0 : TC.toBeats(node.startTime)
|
|
427
413
|
return [startTime, nextNode]
|
|
428
414
|
},
|
|
429
|
-
// @ts-ignore
|
|
430
415
|
(note: NoteNode, beats: number) => {
|
|
431
|
-
return
|
|
416
|
+
return TC.toBeats(note.startTime) >= beats ? false : <NoteNode>note.next; // getNodeAt有guard
|
|
432
417
|
})
|
|
433
418
|
}
|
|
434
419
|
/**
|
|
@@ -447,12 +432,12 @@ export class NNList {
|
|
|
447
432
|
* @returns
|
|
448
433
|
*/
|
|
449
434
|
getNodeOf(time: TimeT): NoteNode {
|
|
450
|
-
let node = this.getNodeAt(
|
|
435
|
+
let node = this.getNodeAt(TC.toBeats(time), false)
|
|
451
436
|
.previous;
|
|
452
437
|
|
|
453
438
|
|
|
454
|
-
let isEqual = node.type !== NodeType.HEAD &&
|
|
455
|
-
if (node.next.type !== NodeType.TAIL &&
|
|
439
|
+
let isEqual = node.type !== NodeType.HEAD && TC.eq((node as NoteNode).startTime, time)
|
|
440
|
+
if (node.next.type !== NodeType.TAIL && TC.eq((node.next as NoteNode).startTime, time)) {
|
|
456
441
|
isEqual = true;
|
|
457
442
|
node = node.next;
|
|
458
443
|
}
|
|
@@ -567,14 +552,12 @@ export class HNList extends NNList {
|
|
|
567
552
|
if (node.type === NodeType.TAIL) {
|
|
568
553
|
return [null, null]
|
|
569
554
|
}
|
|
570
|
-
if (!node) debugger
|
|
571
555
|
const nextNode = node.next;
|
|
572
|
-
const endTime = node.type === NodeType.HEAD ? 0 :
|
|
556
|
+
const endTime = node.type === NodeType.HEAD ? 0 : TC.toBeats(node.endTime)
|
|
573
557
|
return [endTime, nextNode]
|
|
574
558
|
},
|
|
575
|
-
// @ts-ignore
|
|
576
559
|
(node: NoteNode, beats: number) => {
|
|
577
|
-
return
|
|
560
|
+
return TC.toBeats(node.endTime) >= beats ? false : <NoteNode>node.next; // getNodeAt有guard
|
|
578
561
|
}
|
|
579
562
|
)
|
|
580
563
|
}
|
|
@@ -596,7 +579,7 @@ export type NNNOrHead = NNNode | NNNodeLike<NodeType.HEAD>;
|
|
|
596
579
|
export type NNNOrTail = NNNode | NNNodeLike<NodeType.TAIL>;
|
|
597
580
|
type AnyNNN = NNNode | NNNodeLike<NodeType.HEAD> | NNNodeLike<NodeType.TAIL>;
|
|
598
581
|
|
|
599
|
-
class NNNodeLike<T extends NodeType> {
|
|
582
|
+
export class NNNodeLike<T extends NodeType> {
|
|
600
583
|
previous: NNNOrHead;
|
|
601
584
|
next: NNNOrTail;
|
|
602
585
|
startTime: TimeT;
|
|
@@ -618,7 +601,7 @@ export class NNNode extends NNNodeLike<NodeType.MIDDLE> {
|
|
|
618
601
|
super(NodeType.MIDDLE);
|
|
619
602
|
this.noteNodes = []
|
|
620
603
|
this.holdNodes = [];
|
|
621
|
-
this.startTime =
|
|
604
|
+
this.startTime = TC.validateIp([...time])
|
|
622
605
|
}
|
|
623
606
|
get endTime() {
|
|
624
607
|
let latest: TimeT = this.startTime;
|
|
@@ -681,7 +664,7 @@ export class NNNList {
|
|
|
681
664
|
const originalListLength = this.timesWithNotes || 512;
|
|
682
665
|
/*
|
|
683
666
|
if (!this.effectiveBeats) {
|
|
684
|
-
this.effectiveBeats =
|
|
667
|
+
this.effectiveBeats = TC.toBeats(this.tail.previous.endTime)
|
|
685
668
|
}
|
|
686
669
|
*/
|
|
687
670
|
const effectiveBeats: number = this.effectiveBeats;
|
|
@@ -695,25 +678,19 @@ export class NNNList {
|
|
|
695
678
|
return [null, null]
|
|
696
679
|
}
|
|
697
680
|
const nextNode = node.next;
|
|
698
|
-
const startTime = node.type === NodeType.HEAD ? 0 :
|
|
681
|
+
const startTime = node.type === NodeType.HEAD ? 0 : TC.toBeats((node as NNNode).startTime)
|
|
699
682
|
return [startTime, nextNode]
|
|
700
683
|
},
|
|
701
|
-
// @ts-ignore
|
|
702
684
|
(note: NNNode, beats: number) => {
|
|
703
|
-
return
|
|
704
|
-
}
|
|
705
|
-
/*,
|
|
706
|
-
(note: Note) => {
|
|
707
|
-
const prev = note.previous;
|
|
708
|
-
return prev.type === NodeType.HEAD ? note : prev
|
|
709
|
-
})*/)
|
|
685
|
+
return TC.toBeats(note.startTime) >= beats ? false : <NNNode>note.next; // getNodeAt有guard
|
|
686
|
+
})
|
|
710
687
|
}
|
|
711
688
|
getNodeAt(beats: number, beforeEnd=false): NNNode | NNNodeLike<NodeType.TAIL> {
|
|
712
689
|
return this.jump.getNodeAt(beats) as NNNode | NNNodeLike<NodeType.TAIL>;
|
|
713
690
|
}
|
|
714
691
|
getNode(time: TimeT): NNNode {
|
|
715
|
-
const node = this.getNodeAt(
|
|
716
|
-
if (node.type === NodeType.HEAD ||
|
|
692
|
+
const node = this.getNodeAt(TC.toBeats(time), false).previous;
|
|
693
|
+
if (node.type === NodeType.HEAD || TC.ne((node as NNNode).startTime, time)) {
|
|
717
694
|
const newNode = new NNNode(time);
|
|
718
695
|
const next = node.next
|
|
719
696
|
NNNode.insert(node, newNode, next);
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Chart } from "../chart";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export type OpEventType = "do" | "undo" | "redo" | "error" | "needsupdate" | "maxcombochanged" | "noundo" | "noredo" | "firstmodified" | "needsreflow";
|
|
7
|
+
|
|
8
|
+
// 最讲类型安全的一集(
|
|
9
|
+
// 当然要有,不然的话编辑器那边检测的时候逆变会出问题
|
|
10
|
+
|
|
11
|
+
interface DirectlyInstaciableEventMap {
|
|
12
|
+
"noundo": OpEvent;
|
|
13
|
+
"noredo": OpEvent;
|
|
14
|
+
"firstmodified": OpEvent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 创建一个类型来检测意外的 override
|
|
18
|
+
// AI太好用了你知道吗
|
|
19
|
+
type CheckFinalOverrides<T> = {
|
|
20
|
+
[K in keyof T]: K extends keyof OpEventMap ?
|
|
21
|
+
T[K] extends OpEventMap[K] ? T[K] : never :
|
|
22
|
+
T[K]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface OpEventMap extends CheckFinalOverrides<DirectlyInstaciableEventMap> {
|
|
26
|
+
"error": OperationErrorEvent;
|
|
27
|
+
"maxcombochanged": MaxComboChangeEvent;
|
|
28
|
+
"undo": OperationEvent;
|
|
29
|
+
"redo": OperationEvent;
|
|
30
|
+
"do": OperationEvent;
|
|
31
|
+
"needsupdate": OperationEvent;
|
|
32
|
+
"needsreflow": NeedsReflowEvent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OpEvent extends Event {
|
|
37
|
+
protected constructor(type: OpEventType) {
|
|
38
|
+
super(type);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 如果这个类型没有对应子类应该用这个
|
|
42
|
+
*/
|
|
43
|
+
static create(type: keyof DirectlyInstaciableEventMap) {
|
|
44
|
+
return new OpEvent(type);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class NeedsReflowEvent extends OpEvent {
|
|
49
|
+
constructor(public condition: number) {
|
|
50
|
+
super("needsreflow");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class OperationEvent extends OpEvent {
|
|
55
|
+
constructor(t: "do" | "undo" | "redo" | "error" | "needsupdate", public operation: Operation) {
|
|
56
|
+
super(t);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class OperationErrorEvent extends OperationEvent {
|
|
61
|
+
constructor(operation: Operation, public error: Error) {
|
|
62
|
+
super("error", operation);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class MaxComboChangeEvent extends OpEvent {
|
|
67
|
+
constructor(public comboDelta: number) {
|
|
68
|
+
super("maxcombochanged");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export class OperationList extends EventTarget {
|
|
75
|
+
operations: Operation[];
|
|
76
|
+
undoneOperations: Operation[];
|
|
77
|
+
constructor(public readonly chart: Chart) {
|
|
78
|
+
super()
|
|
79
|
+
this.operations = [];
|
|
80
|
+
this.undoneOperations = [];
|
|
81
|
+
}
|
|
82
|
+
undo() {
|
|
83
|
+
const op = this.operations.pop()
|
|
84
|
+
if (op) {
|
|
85
|
+
if (!this.chart.modified){
|
|
86
|
+
this.chart.modified = true;
|
|
87
|
+
this.dispatchEvent(OpEvent.create("firstmodified"))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
op.undo(this.chart);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
this.dispatchEvent(new OperationErrorEvent(op, e as Error))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
this.undoneOperations.push(op)
|
|
97
|
+
this.dispatchEvent(new OperationEvent("undo", op))
|
|
98
|
+
this.processFlags(op);
|
|
99
|
+
} else {
|
|
100
|
+
this.dispatchEvent(OpEvent.create("noundo"))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
redo() {
|
|
104
|
+
const op = this.undoneOperations.pop()
|
|
105
|
+
if (op) {
|
|
106
|
+
if (!this.chart.modified){
|
|
107
|
+
this.chart.modified = true;
|
|
108
|
+
this.dispatchEvent(OpEvent.create("firstmodified"))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
op.do(this.chart);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
this.dispatchEvent(new OperationErrorEvent(op, e as Error))
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
this.operations.push(op)
|
|
118
|
+
this.dispatchEvent(new OperationEvent("redo", op))
|
|
119
|
+
this.processFlags(op);
|
|
120
|
+
} else {
|
|
121
|
+
this.dispatchEvent(OpEvent.create("noredo"))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
do(operation: Operation) {
|
|
125
|
+
if (operation.ineffective) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (!this.chart.modified){
|
|
129
|
+
this.chart.modified = true;
|
|
130
|
+
this.dispatchEvent(OpEvent.create("firstmodified"))
|
|
131
|
+
}
|
|
132
|
+
// 如果上一个操作是同一个构造器的,那么试图修改上一个操作而不是立即推入新的操作
|
|
133
|
+
if (this.operations.length !== 0) {
|
|
134
|
+
|
|
135
|
+
const lastOp = this.operations[this.operations.length - 1]
|
|
136
|
+
if (operation.constructor === lastOp.constructor) {
|
|
137
|
+
// 返回值指示是否重写成功
|
|
138
|
+
if (lastOp.rewrite(operation, this.chart)) {
|
|
139
|
+
this.processFlags(operation)
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
operation.do(this.chart);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
this.dispatchEvent(new OperationErrorEvent(operation, e as Error))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
this.dispatchEvent(new OperationEvent("do", operation));
|
|
151
|
+
this.processFlags(operation);
|
|
152
|
+
this.operations.push(operation);
|
|
153
|
+
}
|
|
154
|
+
processFlags(operation: Operation) {
|
|
155
|
+
|
|
156
|
+
if (operation.updatesEditor) {
|
|
157
|
+
this.dispatchEvent(new OperationEvent("needsupdate", operation));
|
|
158
|
+
}
|
|
159
|
+
if (operation.comboDelta) {
|
|
160
|
+
this.dispatchEvent(new MaxComboChangeEvent(operation.comboDelta));
|
|
161
|
+
}
|
|
162
|
+
if (operation.reflows) {
|
|
163
|
+
this.dispatchEvent(new NeedsReflowEvent(operation.reflows))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
clear() {
|
|
167
|
+
this.operations = [];
|
|
168
|
+
}
|
|
169
|
+
addEventListener<T extends OpEventType>(type: T, listener: (event: OpEventMap[T]) => void, options?: boolean | AddEventListenerOptions): void {
|
|
170
|
+
super.addEventListener(type, listener, options);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
export abstract class Operation {
|
|
176
|
+
ineffective: boolean;
|
|
177
|
+
updatesEditor: boolean;
|
|
178
|
+
// 用于判定线编辑区的重排,若操作完成时的布局为这个值就会重排
|
|
179
|
+
reflows: number;
|
|
180
|
+
/**
|
|
181
|
+
* 此操作对谱面总物量产生了多少影响,正增负减。
|
|
182
|
+
*
|
|
183
|
+
* 如果操作自身无法评估,应返回NaN,导致全谱重新数清物量
|
|
184
|
+
*/
|
|
185
|
+
comboDelta: number;
|
|
186
|
+
constructor() {
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
abstract do(chart: Chart): void
|
|
190
|
+
abstract undo(chart: Chart): void
|
|
191
|
+
rewrite(op: this, chart: Chart): boolean {return false;}
|
|
192
|
+
toString(): string {
|
|
193
|
+
return this.constructor.name;
|
|
194
|
+
}
|
|
195
|
+
static lazy<C extends new (...args: any[]) => any = typeof this>(this: C, ...args: ConstructorParameters<C>) {
|
|
196
|
+
return new LazyOperation<C>(this, ...args)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 懒操作,实例化的时候不记录任何数据,do的时候才执行真正实例化
|
|
205
|
+
* 防止连续的操作中状态改变导致的错误
|
|
206
|
+
*/
|
|
207
|
+
export class LazyOperation<C extends new (...args: any[]) => any> extends Operation {
|
|
208
|
+
public operationClass: C;
|
|
209
|
+
public args: ConstructorParameters<C>;
|
|
210
|
+
public operation: InstanceType<C> | null = null;
|
|
211
|
+
constructor(
|
|
212
|
+
operationClass: C,
|
|
213
|
+
...args: ConstructorParameters<C>
|
|
214
|
+
) {
|
|
215
|
+
super();
|
|
216
|
+
this.operationClass = operationClass;
|
|
217
|
+
this.args = args;
|
|
218
|
+
}
|
|
219
|
+
do(chart: Chart) {
|
|
220
|
+
this.operation = new this.operationClass(...this.args);
|
|
221
|
+
this.operation.do(chart);
|
|
222
|
+
}
|
|
223
|
+
undo(chart: Chart) {
|
|
224
|
+
this.operation.undo(chart);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* C语言借来的概念
|
|
231
|
+
*
|
|
232
|
+
* 一个不确定类型的子操作
|
|
233
|
+
*
|
|
234
|
+
* 注意这个操作不懒,会在构造时就实例化子操作
|
|
235
|
+
*/
|
|
236
|
+
export class UnionOperation<T extends Operation> extends Operation {
|
|
237
|
+
operation: T;
|
|
238
|
+
constructor(matcher: () => T) {
|
|
239
|
+
super();
|
|
240
|
+
this.operation = matcher();
|
|
241
|
+
if (!this.operation) {
|
|
242
|
+
this.ineffective = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
|
|
246
|
+
do(chart?: Chart) {
|
|
247
|
+
this.operation?.do(chart);
|
|
248
|
+
}
|
|
249
|
+
undo(chart?: Chart) {
|
|
250
|
+
this.operation?.undo(chart);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
export class ComplexOperation<T extends Operation[]> extends Operation {
|
|
256
|
+
subOperations: T;
|
|
257
|
+
length: number;
|
|
258
|
+
constructor(...sub: T) {
|
|
259
|
+
super()
|
|
260
|
+
this.subOperations = sub
|
|
261
|
+
this.length = sub.length
|
|
262
|
+
this.reflows = sub.reduce((prev, op) => prev | op.reflows, 0);
|
|
263
|
+
this.updatesEditor = sub.some((op) => op.updatesEditor);
|
|
264
|
+
this.comboDelta = sub.reduce((prev, op) => prev + op.comboDelta, 0);
|
|
265
|
+
}
|
|
266
|
+
// 这样子写不够严密,如果要继承这个类,并且子操作需要谱面,就要重写这个方法的签名
|
|
267
|
+
do(chart?: Chart) {
|
|
268
|
+
const length = this.length
|
|
269
|
+
for (let i = 0; i < length; i++) {
|
|
270
|
+
const op = this.subOperations[i]
|
|
271
|
+
if (op.ineffective) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
op.do(chart)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
undo(chart?: Chart) {
|
|
278
|
+
const length = this.length
|
|
279
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
280
|
+
const op = this.subOperations[i]
|
|
281
|
+
if (op.ineffective) { continue; }
|
|
282
|
+
op.undo(chart)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Chart } from "../chart";
|
|
2
|
+
import { Operation } from "./basic";
|
|
3
|
+
|
|
4
|
+
export type ChartPropName = "name" | "level" | "composer" | "illustrator" | "charter" | "offset"
|
|
5
|
+
|
|
6
|
+
export class ChartPropChangeOperation<T extends ChartPropName> extends Operation {
|
|
7
|
+
originalValue: Chart[T];
|
|
8
|
+
constructor(public chart: Chart, public field: T, public value: Chart[T]) {
|
|
9
|
+
super();
|
|
10
|
+
this.originalValue = chart[field];
|
|
11
|
+
if (field === "level" || field === "name") {
|
|
12
|
+
this.updatesEditor = true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
do() {
|
|
16
|
+
this.chart[this.field] = this.value;
|
|
17
|
+
}
|
|
18
|
+
undo() {
|
|
19
|
+
this.chart[this.field] = this.originalValue;
|
|
20
|
+
}
|
|
21
|
+
}
|