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.
@@ -0,0 +1,457 @@
1
+ import { Operation, ComplexOperation, UnionOperation } from "./basic";
2
+
3
+ import TC from "../time";
4
+ import { NoteType, TimeT } from "../chartTypes";
5
+ import { JudgeLine } from "../judgeline";
6
+ import { Note, notePropTypes, NoteNode, HNList, NNList } from "../note";
7
+ import { checkType } from "../util";
8
+ import { err } from "../env";
9
+
10
+
11
+ type NotePropNamePhiZone = "judgeSize" | "tint" | "tintHitEffects";
12
+
13
+ export type NotePropName = "speed" | "type" | "positionX" | "startTime" | "endTime" | "alpha" | "size" | "visibleBeats" | "yOffset" | "above" | "isFake" | NotePropNamePhiZone;
14
+
15
+ /**
16
+ * 修改 Note 的任意属性值
17
+ * @example
18
+ * // 修改 note 的 above 属性
19
+ * operationList.do(new NotePropChangeOperation(note, "above", true));
20
+ * // 修改 note 的 speed 属性
21
+ * // 一般不会这样做,因为修改 note 的 speed 需要把note换到另一个序列(`NNList`)中
22
+ * // 应使用NoteSpeedChangeOperation。YOffset也是同样的道理。
23
+ * operationList.do(new NotePropChangeOperation(note, "speed", 1.5));
24
+ * @template T - 要修改的属性名,必须是 `NotePropName` 类型
25
+ */
26
+ export class NotePropChangeOperation<T extends NotePropName> extends Operation {
27
+ field: T;
28
+ note: Note;
29
+ previousValue: Note[T]
30
+ value: Note[T];
31
+ updatesEditor = true;
32
+ constructor(note: Note, field: T, value: Note[T]) {
33
+ super()
34
+ this.field = field
35
+ this.note = note;
36
+ this.value = value;
37
+ if (!checkType(value, notePropTypes[field])) {
38
+ throw err.INVALID_NOTE_PROP_TYPE(field, value, notePropTypes[field]);
39
+ }
40
+ this.previousValue = note[field]
41
+ if (value === note[field]) {
42
+ this.ineffective = true
43
+ } else if (field === "isFake") {
44
+ this.comboDelta = value ? -1 : 1;
45
+ }
46
+ }
47
+ do() {
48
+ this.note[this.field] = this.value
49
+ }
50
+ undo() {
51
+ this.note[this.field] = this.previousValue
52
+ }
53
+ override rewrite(operation: NotePropChangeOperation<T>): boolean {
54
+ if (operation.note === this.note && this.field === operation.field) {
55
+ this.value = operation.value;
56
+ this.note[this.field] = operation.value
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ }
62
+ export class NoteRemoveOperation extends Operation {
63
+ noteNode: NoteNode;
64
+ note: Note;
65
+ isHold: boolean;
66
+ constructor(note: Note) {
67
+ super()
68
+ this.note = note // In memory of forgettting to add this(
69
+ this.isHold = note.type === NoteType.hold;
70
+ if (!note.parentNode) {
71
+ this.ineffective = true
72
+ } else {
73
+ this.noteNode = note.parentNode
74
+ }
75
+ // 移除假Note不改变物量
76
+ // 这里,一般没有人会在修改一个isFake值之后删除它,因此一般不用懒操作
77
+ this.comboDelta = note.isFake ? 0 : -1;
78
+ }
79
+ do() {
80
+ const {note, noteNode} = this;
81
+ noteNode.remove(note);
82
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime)
83
+ if (needsUpdate) {
84
+ const endBeats = TC.toBeats(note.endTime);
85
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
86
+ const updateFrom = tailJump.header
87
+ const updateTo = tailJump.tailer;
88
+ // tailJump.getPreviousOf(noteNode, endBeats);
89
+ tailJump.updateRange(updateFrom, noteNode.next);
90
+ }
91
+ }
92
+ undo() {
93
+ const {note, noteNode} = this;
94
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime);
95
+ if (needsUpdate) {
96
+ const endBeats = TC.toBeats(note.endTime);
97
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
98
+ const updateFrom = tailJump.getNodeAt(endBeats).previous;
99
+ noteNode.add(note)
100
+ tailJump.updateRange(updateFrom, noteNode.next);
101
+ } else {
102
+ noteNode.add(note);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 删除一个 note(语义层面的删除)
109
+ * 从语义上删除 Note 要用这个操作
110
+ * 注意:实际移除由 NoteRemoveOperation 完成,NoteDeleteOperation 仅负责更新编辑器
111
+ * @example
112
+ * operationList.do(new NoteDeleteOperation(note));
113
+ */
114
+ export class NoteDeleteOperation extends NoteRemoveOperation {
115
+ updatesEditor = true
116
+ }
117
+
118
+ export class MultiNoteDeleteOperation extends ComplexOperation<NoteDeleteOperation[]> {
119
+ updatesEditor = true
120
+ constructor(notes: Set<Note> | Note[]) {
121
+ if (notes instanceof Set) {
122
+ notes = [...notes];
123
+ }
124
+ super(...notes.map(note => new NoteDeleteOperation(note)))
125
+ if (notes.length === 0) {
126
+ this.ineffective = true
127
+ }
128
+ }
129
+
130
+ }
131
+
132
+
133
+
134
+ export class NoteAddOperation extends Operation {
135
+ noteNode: NoteNode
136
+ note: Note;
137
+ isHold: boolean;
138
+ updatesEditor = true;
139
+ constructor(note: Note, node: NoteNode) {
140
+ super()
141
+ this.note = note;
142
+ this.isHold = note.type === NoteType.hold;
143
+ this.noteNode = node;
144
+ // 一般来说,操作是对于在谱里面的NoteNode,谱外面的不需要操作
145
+ this.comboDelta = note.isFake ? 0 : +1;
146
+ }
147
+ do() {
148
+ const {note, noteNode} = this;
149
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime);
150
+ if (needsUpdate) {
151
+ const endBeats = TC.toBeats(note.endTime);
152
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
153
+ const updateFrom = tailJump.header
154
+ // tailJump.getNodeAt(endBeats).previous;
155
+ noteNode.add(note)
156
+ tailJump.updateRange(updateFrom, noteNode.next);
157
+ } else {
158
+ noteNode.add(note);
159
+ }
160
+ }
161
+ undo() {
162
+ const {note, noteNode} = this;
163
+ noteNode.remove(note);
164
+ const needsUpdate = this.isHold && TC.lt(noteNode.endTime, note.endTime)
165
+ if (needsUpdate) {
166
+ const endBeats = TC.toBeats(note.endTime);
167
+ const tailJump = (noteNode.parentSeq as HNList).holdTailJump;
168
+ const updateFrom = tailJump.getPreviousOf(noteNode, endBeats);
169
+ tailJump.updateRange(updateFrom, noteNode.next);
170
+ }
171
+ }
172
+ }
173
+
174
+ export class MultiNoteAddOperation extends ComplexOperation<NoteAddOperation[]> {
175
+ updatesEditor = true;
176
+ constructor(notes: Set<Note> | Note[], judgeLine: JudgeLine) {
177
+ if (notes instanceof Set) {
178
+ notes = [...notes];
179
+ }
180
+ super(...notes.map(note => {
181
+ const node = judgeLine.getNode(note, true)
182
+ return new NoteAddOperation(note, node);
183
+ }));
184
+ // 以上,一般能数清楚加了多少物量
185
+ if (notes.length === 0) {
186
+ this.ineffective = true
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * 修改 Note 的时间位置(不直接修改 startTime,而是将 Note 移动到另一个 NoteNode)
193
+ * 注意:不能直接修改 note.startTime 属性,需要通过 getNodeOf 获取目标时间的 NoteNode
194
+ * @example
195
+ * // 将 note 移动到目标时间位置
196
+ * const targetNode = note.parentNode.parentSeq.getNodeOf(targetTime);
197
+ * operationList.do(new NoteTimeChangeOperation(note, targetNode));
198
+ */
199
+ export class NoteTimeChangeOperation extends ComplexOperation<[
200
+ NoteRemoveOperation,
201
+ NotePropChangeOperation<"startTime">,
202
+ NoteAddOperation,
203
+ UnionOperation<NotePropChangeOperation<"endTime"> | null>
204
+ ]>
205
+ {
206
+ note: Note;
207
+ comboDelta = 0;
208
+ constructor(note: Note, noteNode: NoteNode) {
209
+ super(
210
+ new NoteRemoveOperation(note),
211
+ new NotePropChangeOperation(note, "startTime", noteNode.startTime),
212
+ new NoteAddOperation(note, noteNode),
213
+ new UnionOperation(() => {
214
+ if (note.type !== NoteType.hold) { // 非hold,endTime跟随startTime
215
+ return new NotePropChangeOperation(note, "endTime", noteNode.startTime)
216
+ }
217
+ })
218
+ );
219
+ this.updatesEditor = true;
220
+ if (note.type === NoteType.hold && !TC.gt(note.endTime, noteNode.startTime)) {
221
+ this.ineffective = true
222
+ }
223
+ this.note = note
224
+ if (note.parentNode === noteNode) {
225
+ this.ineffective = true
226
+ }
227
+ }
228
+ // 真的是巨坑啊
229
+ rewrite(operation: NoteTimeChangeOperation): boolean {
230
+ if (operation.note === this.note) {
231
+ this.subOperations[0] = new NoteRemoveOperation(this.note)
232
+ if (!this.subOperations[0].ineffective) {
233
+ this.subOperations[0].do()
234
+ }
235
+ this.subOperations[1].value = operation.subOperations[1].value
236
+ this.subOperations[1].do()
237
+ this.subOperations[2].noteNode = operation.subOperations[2].noteNode
238
+ this.subOperations[2].do()
239
+ if (this.subOperations[3].operation) {
240
+ this.subOperations[3].operation.value = operation.subOperations[3].operation.value;
241
+ this.subOperations[3].operation.do();
242
+ } else {
243
+ this.subOperations[3] = operation.subOperations[3];
244
+ this.subOperations[3].do();
245
+ }
246
+ return true;
247
+ }
248
+ return false
249
+ }
250
+ }
251
+
252
+ /**
253
+ * 修改 hold 音符的 endTime 属性
254
+ * 直接修改 hold 音符的 endTime(不同于 NoteTimeChangeOperation)
255
+ * @example
256
+ * // 修改 hold 音符的结束时间
257
+ * operationList.do(new HoldEndTimeChangeOperation(note, newEndTime));
258
+ */
259
+ export class HoldEndTimeChangeOperation extends NotePropChangeOperation<"endTime"> {
260
+ constructor(note: Note, value: TimeT) {
261
+ super(note, "endTime", value)
262
+ if (!TC.gt(value, note.startTime)) {
263
+ this.ineffective = true
264
+ }
265
+ }
266
+ do() {
267
+ super.do()
268
+ const node = this.note.parentNode;
269
+ node.sort(this.note);
270
+ const tailJump = (node.parentSeq as HNList).holdTailJump;
271
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
272
+ }
273
+ undo() {
274
+ super.undo()
275
+ const node = this.note.parentNode;
276
+ node.sort(this.note);
277
+ const tailJump = (node.parentSeq as HNList).holdTailJump;
278
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
279
+ }
280
+ rewrite(operation: HoldEndTimeChangeOperation): boolean { // 看懂了,不重写的话会出问题
281
+ if (operation.note === this.note && this.field === operation.field) {
282
+ if (operation.value === this.value) {
283
+ return true;
284
+ }
285
+ this.value = operation.value;
286
+ this.note[this.field] = operation.value;
287
+ const tailJump = (this.note.parentNode.parentSeq as HNList).holdTailJump;
288
+ tailJump.updateRange(tailJump.header, tailJump.tailer);
289
+ return true;
290
+ }
291
+ return false;
292
+ }
293
+ }
294
+
295
+
296
+
297
+ /**
298
+ * 修改 Note 的 speed 属性
299
+ * 需要传入 JudgeLine 参数,因为 speed 变化会影响 note 在何 NNList 中
300
+ * @example
301
+ * // 修改 note 的 speed 值
302
+ * operationList.do(new NoteSpeedChangeOperation(note, 2.0, judgeLine));
303
+ */
304
+ export class NoteSpeedChangeOperation
305
+ extends ComplexOperation<[NotePropChangeOperation<"speed">, NoteRemoveOperation, NoteAddOperation]> {
306
+ updatesEditor = true
307
+ constructor(public note: Note, value: number, line: JudgeLine) {
308
+ const valueChange = new NotePropChangeOperation(note, "speed", value);
309
+ const tree = line.getNNList(value, note.yOffset, note.type === NoteType.hold, true)
310
+ const node = tree.getNodeOf(note.startTime);
311
+ const removal = new NoteRemoveOperation(note);
312
+ const insert = new NoteAddOperation(note, node)
313
+ super(valueChange, removal, insert);
314
+ }
315
+ }
316
+
317
+ export class NoteYOffsetChangeOperation
318
+ extends ComplexOperation<[NotePropChangeOperation<"yOffset">, NoteRemoveOperation, NoteAddOperation]> {
319
+ updatesEditor = true
320
+ constructor(public note: Note, value: number, line: JudgeLine) {
321
+ const valueChange = new NotePropChangeOperation(note, "yOffset", value);
322
+ const tree = line.getNNList(note.speed, value, note.type === NoteType.hold, true)
323
+ const node = tree.getNodeOf(note.startTime);
324
+ const removal = new NoteRemoveOperation(note);
325
+ const insert = new NoteAddOperation(note, node)
326
+ super(valueChange, removal, insert);
327
+ }
328
+ }
329
+
330
+
331
+ /**
332
+ * 修改 Note 的 type 属性(音符类型)
333
+ * 在 tap/drag/flick/hold 之间切换,切换到/从 hold 时需要重新定位 NoteNode
334
+ * @example
335
+ * // 将 note 改为 hold 类型
336
+ * operationList.do(new NoteTypeChangeOperation(note, NoteType.hold));
337
+ */
338
+ export class NoteTypeChangeOperation
339
+ extends ComplexOperation<[NotePropChangeOperation<"type">, NoteRemoveOperation, NoteAddOperation] | [NotePropChangeOperation<"type">]> {
340
+ note: Note;
341
+ value: number;
342
+ constructor(note: Note, value: number) {
343
+ const isHold = note.type === NoteType.hold
344
+ const valueChange = new NotePropChangeOperation(note, "type", value);
345
+ if (isHold !== (value === NoteType.hold)) {
346
+ const tree = note.parentNode.parentSeq.parentLine.getNNList(note.speed, note.yOffset, !isHold, true)
347
+ const node = tree.getNodeOf(note.startTime);
348
+ const removal = new NoteRemoveOperation(note);
349
+ const insert = new NoteAddOperation(note, node);
350
+ super(valueChange, removal, insert);
351
+ } else {
352
+ super(valueChange);
353
+ }
354
+ this.note = note;
355
+ this.value = value;
356
+ this.updatesEditor = true;
357
+ }
358
+ }
359
+
360
+ class NoteTreeChangeOperation extends NoteAddOperation {
361
+
362
+ }
363
+
364
+
365
+ // 按照规矩,音符节点的时间不可变,所以不会对音符节点动手。
366
+ // 依旧不用组合的NoteTimeChangeOperation的方式,因为那需要多次更新跳数组。
367
+ /**
368
+ * @author Zes Minkey Young
369
+ */
370
+ // @ts-expect-error 我需要明确禁用掉lazy静态方法,这不会破坏类型安全。
371
+ export class MultiNoteOffsetOperation extends Operation {
372
+ constructor(public nnList: NNList, public notes: readonly Note[], public offset: TimeT) {
373
+ super();
374
+ }
375
+ do() {
376
+ const offset = this.offset;
377
+ const notes = this.notes;
378
+ const len = notes.length;
379
+ const nnList = this.nnList;
380
+ for (let i = 0; i < len; i++) {
381
+ const note = notes[i];
382
+ const startTime = TC.vadd(note.startTime, offset);
383
+ note.startTime = startTime;
384
+ note.endTime = TC.vadd(note.endTime, offset);
385
+ note.parentNode.remove(note);
386
+ nnList.getNodeOf(startTime).add(note);
387
+ }
388
+ nnList.jump.updateRange(nnList.head, nnList.tail);
389
+ if (nnList instanceof HNList) {
390
+ nnList.holdTailJump.updateRange(nnList.head, nnList.tail);
391
+ }
392
+ }
393
+ undo() {
394
+ const offset = this.offset;
395
+ const notes = this.notes;
396
+ const len = notes.length;
397
+ const nnList = this.nnList;
398
+ for (let i = 0; i < len; i++) {
399
+ const note = notes[i];
400
+ const startTime = TC.vsub(note.startTime, offset);
401
+ note.startTime = startTime;
402
+ note.endTime = TC.vsub(note.endTime, offset);
403
+ note.parentNode.remove(note);
404
+ nnList.getNodeOf(startTime).add(note);
405
+ }
406
+ nnList.jump.updateRange(nnList.head, nnList.tail);
407
+ if (nnList instanceof HNList) {
408
+ nnList.holdTailJump.updateRange(nnList.head, nnList.tail);
409
+ }
410
+ }
411
+ // 禁用,因为无效
412
+ private static lazy() {}
413
+ }
414
+
415
+
416
+ type TimeRange = [TimeT, TimeT]
417
+
418
+ export class NNListTimeRangeDeleteOperation extends ComplexOperation<[MultiNoteDeleteOperation, MultiNoteOffsetOperation]> {
419
+ constructor(public nnList: NNList, public timeRange: TimeRange, public updatesJump: boolean = true) {
420
+ const delNodes = nnList.getNodesFromOneAndRangeRight(nnList.getNodeOf(timeRange[0]), timeRange[1]);
421
+ const delNotes = [];
422
+ const dlen = delNodes.length;
423
+ for (let i = 0; i < dlen; i++) {
424
+ delNotes.push(...delNodes[i].notes);
425
+ }
426
+ const offsetNodes = nnList.getNodesAfterOne(delNodes[dlen - 1]);
427
+ const offsetNotes = [];
428
+ const olen = offsetNodes.length;
429
+ for (let i = 0; i < olen; i++) {
430
+ offsetNotes.push(...offsetNodes[i].notes);
431
+ }
432
+ super(new MultiNoteDeleteOperation(delNotes), new MultiNoteOffsetOperation(nnList, offsetNotes, TC.vsub(timeRange[0], timeRange[1])));
433
+ }
434
+ do() {
435
+ super.do();
436
+ this.nnList.clearEmptyNodes(this.updatesJump)
437
+ }
438
+ undo() {
439
+ super.undo();
440
+ this.nnList.clearEmptyNodes(this.updatesJump)
441
+ }
442
+ }
443
+
444
+ export class NNListAddBlankOperation extends MultiNoteOffsetOperation {
445
+ updatesEditor = true;
446
+ constructor(nnList: NNList, pos: TimeT, length: TimeT) {
447
+ const nns = nnList.getNodesAfterOne(nnList.getNodeOf(pos));
448
+ const notes = [];
449
+ const len = nns.length;
450
+ for (let i = 0; i < len; i++) {
451
+ notes.push(...nns[i].notes);
452
+ }
453
+ super(nnList, notes, length);
454
+ }
455
+ }
456
+
457
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kipphi",
3
3
  "description": "Parse your Phigros Chart(.rpe.json or .kpa.json) into an editor-friendly format.",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "author": "Team Zincs (https://github.com/TeamZincs)",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -16,5 +16,11 @@
16
16
  },
17
17
  "peerDependencies": {
18
18
  "typescript": "^5"
19
+ },
20
+ "exports": {
21
+ "./operation": null,
22
+ ".": {
23
+ "import": "./index.ts"
24
+ }
19
25
  }
20
26
  }