kipphi 2.0.1 → 2.1.1

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 CHANGED
@@ -1,9 +1,14 @@
1
- # Parse your chart into an editor-friendly format with Kipphi!
1
+ # 用“奇谱”谱面内核将谱面解析为编辑器友好的格式!
2
+ 这是一个Phigros KPA、RPE谱面的解析器兼谱面操作系统。它是[KPA(奇谱发生器)](https://github.com/TeamZincs/KPA)的子项目。
2
3
 
3
- This is a Phigros KipphiApparatus/Re:PhiEdit ChartJSON Parser. It is a subproject of [KPA](https://github.com/TeamZincs/KPA).
4
+ “奇谱”来自荷兰化学家启普,他发明了“启普发生器”,用于生产气体并随时停止反应。
4
5
 
5
- "Kipphi" comes from the Dutch chemist Kipp, who invented Kipp's Apparatus, which was used to produce gas and stop the reaction at any time.
6
+ 在(Phigros 自制谱Wiki)[https://pgrfm.miraheze.org/wiki]了解更多。
6
7
 
8
+ # Parse your chart into an editor-friendly format with Kipphi!
7
9
 
8
- ## Usage
10
+ This is a Phigros KipphiApparatus/Re:PhiEdit ChartJSON Parser and Chart Operating System. It is a subproject of [KPA](https://github.com/TeamZincs/KPA).
11
+
12
+ "Kipphi" comes from the Dutch chemist Kipp, who invented Kipp's Apparatus, which was used to produce gas and stop the reaction at any time.
9
13
 
14
+ Learn more at [Phigros Custom Chart Wiki](https://pgrfm.miraheze.org/wiki).
package/basic.ts ADDED
@@ -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
+ "needsupdate": OpEvent;
16
+ }
17
+
18
+ // 创建一个类型来检测意外的 override
19
+ // AI太好用了你知道吗
20
+ type CheckFinalOverrides<T> = {
21
+ [K in keyof T]: K extends keyof OpEventMap ?
22
+ T[K] extends OpEventMap[K] ? T[K] : never :
23
+ T[K]
24
+ };
25
+
26
+ interface OpEventMap extends CheckFinalOverrides<DirectlyInstaciableEventMap> {
27
+ "error": OperationErrorEvent;
28
+ "maxcombochanged": MaxComboChangeEvent;
29
+ "undo": OperationEvent;
30
+ "redo": OperationEvent;
31
+ "do": 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", 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 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)) {
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(OpEvent.create("needsupdate"));
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: typeof this): 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
+ }
package/bpm.ts CHANGED
@@ -8,58 +8,116 @@ import TC from "./time";
8
8
  /// #declaration:global
9
9
 
10
10
  /**
11
- *
11
+ * BPM起始节点类,表示BPM变化的开始点
12
+ * 每个BPMStartNode代表一个BPM值的开始,直到下一个BPM节点
12
13
  */
13
14
  export class BPMStartNode extends EventStartNode {
15
+ /** 每拍的秒数(Seconds Per Beat) */
14
16
  spb: number;
17
+ /** 缓存的起始积分值,用于时间计算 */
15
18
  cachedStartIntegral?: number;
19
+ /** 缓存的积分值,用于时间计算 */
16
20
  cachedIntegral?: number;
21
+ /** 下一个BPM结束节点或尾部节点 */
17
22
  override next: BPMEndNode | BPMNodeLike<NodeType.TAIL>;
23
+ /** 上一个BPM结束节点或头部节点 */
18
24
  override previous: BPMEndNode | BPMNodeLike<NodeType.HEAD>;
25
+
26
+ /**
27
+ * 创建一个新的BPM起始节点
28
+ * @param startTime 节点开始时间
29
+ * @param bpm BPM值
30
+ */
19
31
  constructor(startTime: TimeT, bpm: number) {
20
32
  super(startTime, bpm);
21
33
  this.spb = 60 / bpm;
22
34
  }
35
+
36
+ /**
37
+ * 计算指定拍数对应的秒数
38
+ * @param beats 拍数
39
+ * @returns 对应的秒数
40
+ */
23
41
  getSeconds(beats: number): number {
24
42
  return (beats - TC.toBeats(this.time)) * 60 / this.value;
25
43
  }
44
+
45
+ /**
46
+ * 获取当前BPM段的完整持续时间(秒)
47
+ * @this NonLastBPMStartNode 非最后一个BPM节点
48
+ * @returns 当前BPM段的持续时间(秒)
49
+ */
26
50
  getFullSeconds(this: NonLastBPMStartNode): number {
27
51
  return (TC.toBeats(this.next.time) - TC.toBeats(this.time)) * 60 / this.value;
28
52
  }
29
53
  }
54
+
55
+ /**
56
+ * BPM结束节点类,表示BPM段的结束点
57
+ * 用于标记BPM段的结束位置
58
+ */
30
59
  export class BPMEndNode extends EventEndNode {
60
+ /** 每拍的秒数(Seconds Per Beat) */
31
61
  spb: number;
62
+ /** 前一个BPM起始节点 */
32
63
  override previous: BPMStartNode;
64
+ /** 下一个BPM起始节点 */
33
65
  override next: BPMStartNode;
66
+
67
+ /**
68
+ * 创建一个新的BPM结束节点
69
+ * @param endTime 节点结束时间
70
+ */
34
71
  constructor(endTime: TimeT) {
35
72
  super(endTime, null);
36
73
  }
37
74
  }
38
75
 
76
+ /** 非最后一个BPM起始节点类型 */
39
77
  type NonLastBPMStartNode = BPMStartNode & { next: BPMEndNode };
40
78
 
79
+ /**
80
+ * BPM节点接口,定义了BPM节点的基本结构
81
+ */
41
82
  interface BPMNodeLike<T extends NodeType> extends EventNodeLike<T> {
83
+ /** 下一个节点 */
42
84
  next: [BPMStartNode, null, BNOrTail][T] | null;
85
+ /** 上一个节点 */
43
86
  previous: [null, BPMStartNode, BNOrHead][T] | null;
44
87
  }
88
+
89
+ /** BPM节点类型 */
45
90
  type BPMNode = BPMStartNode | BPMEndNode;
91
+
92
+ /** 任意BPM节点类型 */
46
93
  type AnyBN = (BPMNode | BPMNodeLike<NodeType.TAIL> | BPMNodeLike<NodeType.HEAD>);
94
+
95
+ /** BPM节点或尾部节点类型 */
47
96
  type BNOrTail = BPMNode | BPMNodeLike<NodeType.TAIL>;
97
+
98
+ /** BPM节点或头部节点类型 */
48
99
  type BNOrHead = BPMNode | BPMNodeLike<NodeType.HEAD>;
49
100
 
50
101
  /**
51
- * 拥有与事件类似的逻辑
52
- * 每对节点之间代表一个BPM相同的片段
102
+ * BPM序列类,管理BPM变化序列
103
+ * 拥有与事件类似的逻辑,每对节点之间代表一个BPM相同的片段
53
104
  * 片段之间BPM可以发生改变
54
105
  */
55
-
56
106
  export class BPMSequence extends EventNodeSequence {
107
+ /** 头部节点 */
57
108
  declare head: BPMNodeLike<NodeType.HEAD>;
109
+ /** 尾部节点 */
58
110
  declare tail: BPMNodeLike<NodeType.TAIL>;
59
- /** 从拍数访问节点 */
60
- override jump: JumpArray<AnyEN>;
61
- /** 以秒计时的跳数组,处理从秒访问节点 */
111
+ /** 从拍数访问节点的跳转数组 */
112
+ declare jump: JumpArray<AnyBN>;
113
+ /** 以秒计时的跳转数组,处理从秒访问节点 */
62
114
  secondJump: JumpArray<AnyBN>;
115
+
116
+ /**
117
+ * 创建BPM序列
118
+ * @param bpmList BPM片段数据列表
119
+ * @param duration 总持续时间
120
+ */
63
121
  constructor(bpmList: BPMSegmentData[], public duration: number) {
64
122
  super(EventType.bpm, null);
65
123
  let curPos: BPMNodeLike<NodeType.HEAD> | BPMEndNode = this.head;
@@ -79,8 +137,11 @@ export class BPMSequence extends EventNodeSequence {
79
137
  BPMStartNode.connect(last, this.tail);
80
138
  this.initJump();
81
139
  }
140
+
141
+ /**
142
+ * 初始化跳转数组
143
+ */
82
144
  override initJump(): void {
83
- console.log(this)
84
145
  this.effectiveBeats = TC.toBeats(this.tail.previous.time)
85
146
  if (this.effectiveBeats !== 0) {
86
147
  super.initJump(); // 为0可以跳过jumpArray,用不到
@@ -88,6 +149,10 @@ export class BPMSequence extends EventNodeSequence {
88
149
  }
89
150
  this.updateSecondJump();
90
151
  }
152
+
153
+ /**
154
+ * 更新秒跳转数组
155
+ */
91
156
  updateSecondJump(): void {
92
157
  let integral = 0;
93
158
  // 计算积分并缓存到BPMNode
@@ -114,7 +179,7 @@ export class BPMSequence extends EventNodeSequence {
114
179
  this.tail,
115
180
  originalListLength,
116
181
  this.duration,
117
- (node: BPMStartNode) => {
182
+ (node: AnyBN) => {
118
183
  if (node.type === NodeType.TAIL) {
119
184
  return [null, null];
120
185
  }
@@ -135,11 +200,22 @@ export class BPMSequence extends EventNodeSequence {
135
200
  }
136
201
  );
137
202
  }
203
+
204
+ /**
205
+ * 更新跳转数组
206
+ * @param from 起始节点
207
+ * @param to 结束节点
208
+ */
138
209
  override updateJump(from: ENOrHead, to: ENOrTail): void {
139
210
  super.updateJump(from, to);
140
211
  this.updateSecondJump();
141
212
  }
142
213
 
214
+ /**
215
+ * 根据秒数获取BPM起始节点
216
+ * @param seconds 秒数
217
+ * @returns 对应的BPM起始节点
218
+ */
143
219
  getNodeBySeconds(seconds: number): BPMStartNode {
144
220
  if (this.effectiveBeats === 0) {
145
221
  return this.tail.previous
@@ -150,6 +226,11 @@ export class BPMSequence extends EventNodeSequence {
150
226
  }
151
227
  return node as BPMStartNode;
152
228
  }
229
+
230
+ /**
231
+ * 导出BPM数据
232
+ * @returns BPM片段数据数组
233
+ */
153
234
  dumpBPM(): BPMSegmentData[] {
154
235
  let cur = this.head.next;
155
236
  const ret: BPMSegmentData[] = [];
@@ -166,43 +247,81 @@ export class BPMSequence extends EventNodeSequence {
166
247
  }
167
248
  return ret;
168
249
  }
250
+
251
+ /**
252
+ * 根据拍数获取节点
253
+ * @param beats 拍数
254
+ * @param usePrev 是否使用前一个节点
255
+ * @returns 对应的BPM起始节点
256
+ */
169
257
  getNodeAt(beats: number, usePrev?: boolean): BPMStartNode {
170
258
  return super.getNodeAt(beats, usePrev) as BPMStartNode;
171
259
  }
172
260
  }
173
261
 
262
+ /**
263
+ * 时间计算器类,用于处理拍数与秒数之间的转换
264
+ */
174
265
  export class TimeCalculator {
266
+ /** BPM片段数据列表 */
175
267
  bpmList: BPMSegmentData[];
176
- bpmSequence: BPMSequence;
268
+ /** BPM序列 */
269
+ readonly bpmSequence: BPMSequence;
270
+ /** 总持续时间 */
177
271
  duration: number;
178
272
 
273
+ /**
274
+ * 创建时间计算器
275
+ */
179
276
  constructor() {
180
277
  }
181
278
 
279
+ /**
280
+ * 初始化BPM序列
281
+ */
182
282
  initSequence() {
183
283
  const bpmList = this.bpmList;
284
+ // @ts-expect-error 不在构造器中初始化的只读属性
184
285
  this.bpmSequence = new BPMSequence(bpmList, this.duration);
185
286
  }
287
+
288
+ /**
289
+ * 将拍数转换为秒数
290
+ * @param beats 拍数
291
+ * @returns 对应的秒数
292
+ */
186
293
  toSeconds(beats: number) {
187
294
  const node: BPMStartNode = this.bpmSequence.getNodeAt(beats);
188
295
  return node.cachedStartIntegral + node.getSeconds(beats)
189
296
  }
297
+
190
298
  /**
191
299
  * 获取从beats1到beats2的秒数
192
- * @param beats1
193
- * @param beats2
194
- * @returns
300
+ * @param beats1 起始拍数
301
+ * @param beats2 结束拍数
302
+ * @returns 两拍数之间的秒数差
195
303
  */
196
304
  segmentToSeconds(beats1: number, beats2: number): number {
197
305
  const ret = this.toSeconds(beats2) - this.toSeconds(beats1)
198
306
  return ret
199
307
  }
308
+
309
+ /**
310
+ * 将秒数转换为拍数
311
+ * @param seconds 秒数
312
+ * @returns 对应的拍数
313
+ */
200
314
  secondsToBeats(seconds: number) {
201
315
  const node = this.bpmSequence.getNodeBySeconds(seconds);
202
316
  // console.log("node:", node)
203
317
  const beats = (seconds - node.cachedStartIntegral) / node.spb;
204
318
  return TC.toBeats(node.time) + beats
205
319
  }
320
+
321
+ /**
322
+ * 导出BPM数据
323
+ * @returns BPM片段数据数组
324
+ */
206
325
  dump(): BPMSegmentData[] {
207
326
  return this.bpmSequence.dumpBPM();
208
327
  }
@@ -210,4 +329,4 @@ export class TimeCalculator {
210
329
  }
211
330
 
212
331
 
213
- /// #enddeclaration
332
+ /// #enddeclaration