kipphi 2.1.2 → 2.1.3-beta.2

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/bpm.ts CHANGED
@@ -110,7 +110,12 @@ type BNOrHead = BPMNode | BPMNodeLike<NodeType.HEAD>;
110
110
  /**
111
111
  * BPM序列类,管理BPM变化序列
112
112
  * 拥有与事件类似的逻辑,每对节点之间代表一个BPM相同的片段
113
- * 片段之间BPM可以发生改变
113
+ * 片段之间BPM可以发生改变。
114
+ *
115
+ * @example
116
+ * const bpmList = [{ startTime: [0, 0, 0], bpm: 120 }];
117
+ * const bpmSequence = new BPMSequence(bpmList, 180);
118
+ * // JumpArray 在构造器中自动初始化。
114
119
  */
115
120
  export class BPMSequence extends EventNodeSequence {
116
121
  /** 头部节点 */
@@ -141,7 +146,7 @@ export class BPMSequence extends EventNodeSequence {
141
146
  BPMStartNode.connect(curPos, startNode);
142
147
  curPos = endNode;
143
148
  }
144
- const last = new BPMStartNode(next.startTime, next.bpm)
149
+ const last = new BPMStartNode(next.startTime, next.bpm);
145
150
  BPMStartNode.connect(curPos, last);
146
151
  BPMStartNode.connect(last, this.tail);
147
152
  this.initJump();
@@ -160,7 +165,9 @@ export class BPMSequence extends EventNodeSequence {
160
165
  }
161
166
 
162
167
  /**
163
- * 更新秒跳转数组
168
+ * 更新秒跳转数组。
169
+ *
170
+ * 缓存每个节点的秒数发生在这里。
164
171
  */
165
172
  updateSecondJump(): void {
166
173
  let integral = 0;
@@ -179,7 +186,7 @@ export class BPMSequence extends EventNodeSequence {
179
186
  node = endNode.next;
180
187
  }
181
188
  node.cachedStartIntegral = integral;
182
- if (this.effectiveBeats === 0) {
189
+ if (this.effectiveBeats === 0) {
183
190
  return;
184
191
  }
185
192
  const originalListLength = this.listLength;
@@ -188,7 +195,7 @@ export class BPMSequence extends EventNodeSequence {
188
195
  this.tail,
189
196
  originalListLength,
190
197
  this.duration,
191
- (node: AnyBN) => {
198
+ (node: BPMStartNode | BPMNodeLike<NodeType.TAIL> | BPMNodeLike<NodeType.HEAD>) => {
192
199
  if (node.type === NodeType.TAIL) {
193
200
  return [null, null];
194
201
  }
@@ -259,8 +266,9 @@ export class BPMSequence extends EventNodeSequence {
259
266
 
260
267
  /**
261
268
  * 根据拍数获取节点
269
+ *
262
270
  * @param beats 拍数
263
- * @param usePrev 是否使用前一个节点
271
+ * @param usePrev 是否使用前一个节点。假设有两个BPM片段,0-2拍,2-无穷,`getNodeAt(2, true)` 会返回第一个片段的开始节点
264
272
  * @returns 对应的BPM起始节点
265
273
  */
266
274
  getNodeAt(beats: number, usePrev?: boolean): BPMStartNode {
@@ -270,6 +278,15 @@ export class BPMSequence extends EventNodeSequence {
270
278
 
271
279
  /**
272
280
  * 时间计算器类,用于处理拍数与秒数之间的转换
281
+ *
282
+ * @example
283
+ * const bpmList = = [
284
+ * { bpm: 120, startTime: [0, 0, 1] }
285
+ * ];
286
+ * const tc = new TimeCalculator();
287
+ * tc.bpmList = bpmList;
288
+ * tc.duration = 131; // 这两者都是必需的。
289
+ * tc.initSequence();
273
290
  */
274
291
  export class TimeCalculator {
275
292
  /** BPM片段数据列表 */
@@ -289,6 +306,9 @@ export class TimeCalculator {
289
306
  * 初始化BPM序列
290
307
  */
291
308
  initSequence() {
309
+ if (!this.bpmList || !this.duration) {
310
+ throw new Error("TimeCalculator: bpmList and duration must be set before initSequence");
311
+ }
292
312
  const bpmList = this.bpmList;
293
313
  // @ts-expect-error 不在构造器中初始化的只读属性
294
314
  this.bpmSequence = new BPMSequence(bpmList, this.duration);
package/chart.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  NormalEasing,
11
11
  rpeEasingArray,
12
12
  SegmentedEasing,
13
+ TemplateEasing,
13
14
  TemplateEasingLib,
14
15
  } from "./easing";
15
16
 
@@ -149,6 +150,11 @@ export class Chart {
149
150
  /** 难度等级显示绑定的判定线 */
150
151
  levelAttach: JudgeLine | null = null;
151
152
 
153
+ /** 仅用于构造时检查
154
+ * @internal
155
+ */
156
+ segmentedTemplates: Map<(SegmentedEasing & { easing: TemplateEasing}), [string, TimeT]> = new Map();
157
+
152
158
  constructor() {}
153
159
 
154
160
  /**
@@ -169,7 +175,7 @@ export class Chart {
169
175
  */
170
176
  static fromRPEJSON(data: ChartDataRPE, duration: number) {
171
177
  const chart = new Chart();
172
- chart.judgeLineGroups = data.judgeLineGroup.map(group => new JudgeLineGroup(group));
178
+ chart.judgeLineGroups = (data.judgeLineGroup || ["Default"]).map(group => new JudgeLineGroup(group));
173
179
  chart.name = data.META.name;
174
180
  chart.level = data.META.level;
175
181
  chart.offset = data.META.offset;
@@ -286,6 +292,9 @@ export class Chart {
286
292
  for (let i = 0; i < len; i++) {
287
293
  const easingData = templateEasings[i];
288
294
  const sequence = chart.sequenceMap.get(easingData.content);
295
+ if (!sequence) {
296
+ continue; // 后面check的时候会错误处理
297
+ }
289
298
  if (sequence.type !== EventType.easing) {
290
299
  throw err.CANNOT_IMPLEMENT_TEMEAS_WITH_NON_EASING_ENS(easingData.name);
291
300
  }
@@ -294,7 +303,19 @@ export class Chart {
294
303
  }
295
304
  chart.templateEasingLib.implement(easingData.name, sequence as EventNodeSequence<number>);
296
305
  }
297
- chart.templateEasingLib.check()
306
+ for (let i = 0; i < len; i++) {
307
+ const easingData = templateEasings[i];
308
+ const sequence = chart.sequenceMap.get(easingData.content) as EventNodeSequence;
309
+ // 遍历该序列检查循环依赖
310
+ if (sequence.hasReferenceTo(sequence)) {
311
+ throw err.TEMPLATE_EASING_CIRCULAR_REFERENCE(easingData.name);
312
+ }
313
+ }
314
+
315
+ chart.templateEasingLib.check();
316
+
317
+ chart.checkSegmentedTemplates();
318
+
298
319
  for (const lineData of data.orphanLines) {
299
320
  const line: JudgeLine = JudgeLine.fromKPAJSON(data.version, chart, lineData.id, lineData, chart.templateEasingLib, chart.timeCalculator)
300
321
  chart.orphanLines.push(line)
@@ -718,6 +739,25 @@ export class Chart {
718
739
 
719
740
  }
720
741
  */
742
+ checkErrors() {
743
+ KPAError.flush();
744
+ for (const [_, seq] of this.sequenceMap) {
745
+ seq.checkErrors();
746
+ }
747
+ }
748
+ /**
749
+ * 用于构造谱面时检查
750
+ */
751
+ protected checkSegmentedTemplates() {
752
+ for (const [easing, [pos, time]] of this.segmentedTemplates) {
753
+ const inner = easing.easing;
754
+ if (inner.getValue(easing.left) === inner.getValue(easing.right)) {
755
+ err.EASING_DELTA_CANNOT_BE_ZERO(pos, time).warn();
756
+ }
757
+ }
758
+ this.segmentedTemplates.clear();
759
+ // 查完就清除,不再需要
760
+ }
721
761
  }
722
762
 
723
763
  /**
package/chartTypes.ts CHANGED
@@ -84,11 +84,27 @@ export interface NoteDataRPE {
84
84
  * Sets the Z index for the hit effects of the note. Defaults to 7.
85
85
  */
86
86
  zIndexHitEffects?: number;
87
- /** Sets the tint for the hit effects of the note. Defaults to null. */
87
+ /**
88
+ * Sets the tint for the hit effects of the note. Defaults to null.
89
+ *
90
+ * @alias color
91
+ */
88
92
  tint?: RGB;
93
+ /**
94
+ * @see {@linkcode tint}
95
+ */
96
+ color?: RGB;
89
97
  tintHitEffects?: RGB;
90
98
 
91
- /** Determines the width of the judgment area of the note. Defaults to size. */
99
+
100
+ /**
101
+ * Determines the width of the judgment area of the note. Defaults to size.
102
+ * @alias judgeSize
103
+ */
104
+ judgeArea?: number;
105
+ /**
106
+ * @see {@linkcode judgeArea}
107
+ */
92
108
  judgeSize?: number;
93
109
  }
94
110
 
@@ -113,7 +129,11 @@ export interface NoteDataKPA {
113
129
  type: NoteType;
114
130
  /** 音符可视时间(打击前多少秒开始显现,默认99999.0) */
115
131
  visibleTime?: number;
116
- /** y值偏移,使音符被打击时的位置偏离判定线 */
132
+ /**
133
+ * y值偏移,使音符被打击时的位置偏离判定线
134
+ *
135
+ * @deprecated 使用{@linkcode absoluteYOffset}代替。
136
+ */
117
137
  yOffset: number;
118
138
 
119
139
  // 下面是PhiZone Player扩展的内容
@@ -123,11 +143,12 @@ export interface NoteDataKPA {
123
143
  * Sets the Z index for the hit effects of the note. Defaults to 7.
124
144
  */
125
145
  zIndexHitEffects?: number;
126
- /** Sets the tint for the hit effects of the note. Defaults to null. */
146
+ /**
147
+ * Sets the tint for the hit effects of the note. Defaults to null.
148
+ */
127
149
  tint?: RGB;
128
150
  tintHitEffects?: RGB;
129
151
 
130
- /** Determines the width of the judgment area of the note. Defaults to size. */
131
152
  judgeSize?: number;
132
153
  visibleBeats?: number;
133
154
  absoluteYOffset: number;
package/easing.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type TemplateEasingBodyData, type EasingDataKPA2, EasingType, EventType, type SegmentedEasingData, type NormalEasingData, type BezierEasingData, type TemplateEasingData, WrapperEasingData, WrapperEasingBodyData } from "./chartTypes";
2
2
  import { type EventNodeSequence } from "./event";
3
- import { type TupleCoord } from "./util";
3
+ import { NodeType, type TupleCoord } from "./util";
4
4
  import Environment, { err } from "./env";
5
- import { type ExpressionEvaluator } from "./evaluator";
5
+ import { type EasedEvaluator, type ExpressionEvaluator } from "./evaluator";
6
6
 
7
7
 
8
8
  /// #declaration:global
@@ -318,6 +318,9 @@ export class TemplateEasing extends Easing {
318
318
  this.name = name;
319
319
  }
320
320
  getValue(t: number) {
321
+ if (t === 1) {
322
+ return 1;
323
+ }
321
324
  const seq = this.eventNodeSequence;
322
325
  const delta = this.valueDelta;
323
326
  if (delta === 0) {
@@ -339,6 +342,32 @@ export class TemplateEasing extends Easing {
339
342
  get headValue(): number {
340
343
  return this.eventNodeSequence.head.next.value;
341
344
  }
345
+
346
+ segmentedValueGetter(easingLeft: number, easingRight: number) {
347
+ // 由于模板缓动是可变的,所以不能在分段缓动构造时预先计算几个变化量
348
+ return (t: number) => {
349
+
350
+ const leftValue = this.getValue(easingLeft);
351
+ const rightValue = this.getValue(easingRight);
352
+ const timeDelta = easingRight - easingLeft;
353
+ const delta = rightValue - leftValue;
354
+ if (delta === 0) {
355
+ return 0;
356
+ }
357
+ return (this.getValue(easingLeft + timeDelta * t) - leftValue) / delta
358
+ };
359
+ }
360
+
361
+ static checkCircularReference(seq: EventNodeSequence, template: TemplateEasing) {
362
+ const seq2 = template.eventNodeSequence;
363
+ if (seq === seq2) {
364
+ return true;
365
+ }
366
+ if (seq2.hasReferenceTo(seq)) {
367
+ return true;
368
+ }
369
+ return false;
370
+ }
342
371
  }
343
372
 
344
373
  export class WrapperEasing extends Easing {
package/env.ts CHANGED
@@ -73,6 +73,8 @@ export enum ERROR_IDS {
73
73
  NODES_NOT_CONTINUOUS = EASING | INVALID_USAGE | 1,
74
74
  NODES_NOT_BELONG_TO_SAME_SEQUENCE = EASING | INVALID_USAGE | 2,
75
75
  NODES_HAS_ZERO_DELTA = EASING | INVALID_USAGE | 3,
76
+ TEMPLATE_EASING_CIRCULAR_REFERENCE = EASING | INVALID_USAGE | 4,
77
+ EASING_DELTA_CANNOT_BE_ZERO = EASING | INVALID_USAGE | 5,
76
78
 
77
79
  CANNOT_DIVIDE_EXPRESSION_EVALUATOR = EVALUATOR | INVALID_USAGE | 0,
78
80
  MISSING_MACRO_EVALUATOR_KEY = EVALUATOR | INVALID_DATA | 0,
@@ -181,6 +183,10 @@ export const ERRORS = {
181
183
  `EventNode is not dense. At ${pos}`,
182
184
  HOLD_HAS_NO_DURATION: () =>
183
185
  `Hold should have a duration.`,
186
+ TEMPLATE_EASING_CIRCULAR_REFERENCE: (temEasName: string) =>
187
+ `Template Easing '${temEasName}' has circular reference`,
188
+ EASING_DELTA_CANNOT_BE_ZERO: (seqName: string, time: TimeT) =>
189
+ `Easing delta cannot be zero. (at ${seqName}, ${toTimeString(time)}`,
184
190
  } satisfies Record<keyof typeof ERROR_IDS, (...args: any[]) => string>
185
191
 
186
192
  type EnumKeys<E extends Record<string, string | number>> = E[keyof E];
package/evaluator.ts CHANGED
@@ -13,6 +13,7 @@ import { EasingType, EvaluatorType, EventValueType, EventValueTypeOfType, Interp
13
13
  import type { JudgeLine } from "./judgeline";
14
14
  import { Chart } from "./chart";
15
15
  import { EVENT_MACROS } from "./macro";
16
+ import { type TimeCalculator } from "./bpm";
16
17
 
17
18
 
18
19
  /// #declaration:global
@@ -28,6 +29,7 @@ import { EVENT_MACROS } from "./macro";
28
29
  */
29
30
  export abstract class Evaluator<T extends EventValueESType> {
30
31
  abstract eval(event: NonLastStartNode<T>, beats: number): T;
32
+ abstract eval(event: NonLastStartNode<T>, seconds: number, timeCalculator: TimeCalculator): T;
31
33
  abstract dumpFor(node: EventStartNode<T>): EvaluatorDataKPA2<T>;
32
34
  }
33
35
 
@@ -38,17 +40,28 @@ export abstract class EasedEvaluator<T extends EventValueESType> extends Evaluat
38
40
  super();
39
41
  this.easing = easing;
40
42
  }
41
- override eval(startNode: NonLastStartNode<T>, beats: number): T {
43
+ override eval(startNode: NonLastStartNode<T>, beats: number): T;
44
+ override eval(startNode: NonLastStartNode<T>, seconds: number, timeCalculator: TimeCalculator): T;
45
+ override eval(startNode: NonLastStartNode<T>, beatsOrSeconds: number, timeCalculator?: TimeCalculator): T {
42
46
  const next = startNode.next;
43
- const timeDelta = TC.getDelta(next.time, startNode.time)
44
- const current = beats - TC.toBeats(startNode.time)
45
47
  const nextValue = startNode.next.value;
46
48
  const value = startNode.value;
47
49
  if (nextValue === value) {
48
50
  return value;
49
51
  }
50
- // 其他类型,包括普通缓动和非钩定模板缓动
51
- return this.convert(value, nextValue, this.easing.getValue(current / timeDelta));
52
+ if (timeCalculator) {
53
+ // 注意这个和下面那个API设计得不一样
54
+ const startSecs = timeCalculator.toSeconds(TC.toBeats(startNode.time));
55
+ const endSecs = timeCalculator.toSeconds(TC.toBeats(next.time));
56
+ const current = beatsOrSeconds - startSecs;
57
+ const timeDelta = endSecs - startSecs;
58
+ return this.convert(value, nextValue, this.easing.getValue(current / timeDelta));
59
+ } else {
60
+ const timeDelta = TC.getDelta(next.time, startNode.time)
61
+ const current = beatsOrSeconds - TC.toBeats(startNode.time)
62
+ // 其他类型,包括普通缓动和非钩定模板缓动
63
+ return this.convert(value, nextValue, this.easing.getValue(current / timeDelta));
64
+ }
52
65
  }
53
66
  abstract convert(start: T, end: T, progress: number): T;
54
67
  /**
@@ -159,11 +172,11 @@ export class TextEasedEvaluator extends EasedEvaluator<string> {
159
172
  if (interpretedAs === InterpreteAs.float) {
160
173
  const start = parseFloat(value);
161
174
  const delta = parseFloat(nextValue as string) - start;
162
- return start + progress * delta + "";
175
+ return (start + progress * delta).toFixed(3) + "";
163
176
  } else if (interpretedAs === InterpreteAs.int) {
164
177
  const start = parseInt(value);
165
178
  const delta = parseInt(nextValue as string) - start;
166
- return start + Math.round(progress * delta) + "";
179
+ return start + Math.floor(progress * delta) + "";
167
180
  } else
168
181
  if (value.startsWith(nextValue as string)) {
169
182
  const startLen = (nextValue as string).length;
@@ -218,8 +231,10 @@ export class MacroEvaluator<T extends EventValueESType> extends Evaluator<T> {
218
231
  node.evaluator = this;
219
232
  this.consumers.set(node, this.compile(node, chart));
220
233
  }
221
- eval(event: NonLastStartNode<T>, beats: number): T {
222
- return this.consumers.get(event)!.eval(event, beats);
234
+ override eval(startNode: NonLastStartNode<T>, beats: number): T;
235
+ override eval(startNode: NonLastStartNode<T>, seconds: number, timeCalculator: TimeCalculator): T;
236
+ eval(event: NonLastStartNode<T>, beats: number, timeCalculator?: TimeCalculator): T {
237
+ return this.consumers.get(event)!.eval(event, beats, timeCalculator);
223
238
  }
224
239
  dumpFor(node: EventStartNode<T>): MacroEvaluatorDataKPA2 {
225
240
  return {
@@ -242,11 +257,21 @@ export class ExpressionEvaluator<T extends EventValueESType> extends Evaluator<T
242
257
  super();
243
258
  this.func = new Function("t", "return " + jsExpr) as (t: number) => T;
244
259
  }
245
- override eval(startNode: NonLastStartNode<T>, beats: number): T {
246
- const next = startNode.next;
247
- const timeDelta = TC.getDelta(next.time, startNode.time)
248
- const current = beats - TC.toBeats(startNode.time)
249
- return this.func(current / timeDelta);
260
+ override eval(startNode: NonLastStartNode<T>, beats: number): T;
261
+ override eval(startNode: NonLastStartNode<T>, seconds: number, timeCalculator: TimeCalculator): T;
262
+ override eval(startNode: NonLastStartNode<T>, beatsOrSecs: number, timeCalculator?: TimeCalculator): T {
263
+ if (timeCalculator) {
264
+ const startSecs = timeCalculator.toSeconds(TC.toBeats(startNode.time));
265
+ const endSecs = timeCalculator.toSeconds(TC.toBeats(startNode.next.time));
266
+ const current = beatsOrSecs - startSecs;
267
+ const timeDelta = endSecs - startSecs;
268
+ return this.func(current / timeDelta);
269
+ } else {
270
+ const next = startNode.next;
271
+ const timeDelta = TC.getDelta(next.time, startNode.time)
272
+ const current = beatsOrSecs - TC.toBeats(startNode.time)
273
+ return this.func(current / timeDelta);
274
+ }
250
275
  }
251
276
  override dumpFor(): ExpressionEvaluatorDataKPA2 {
252
277
  return {
package/event.ts CHANGED
@@ -78,11 +78,6 @@ export abstract class EventNode<VT extends EventValueESType = number> extends Ev
78
78
  * @deprecated
79
79
  */
80
80
  static getEasing(data: EventDataKPA<EventValueESType>, templates: TemplateEasingLib, notSegmented = false): Easing {
81
- const left = data.easingLeft;
82
- const right = data.easingRight;
83
- if (!notSegmented && (left && right) && (left !== 0.0 || right !== 1.0)) {
84
- return new SegmentedEasing(EventNode.getEasing(data, templates, true), left, right)
85
- }
86
81
  if (data.bezier) {
87
82
  const bp = data.bezierPoints
88
83
  const easing = new BezierEasing([bp[0], bp[1]], [bp[2], bp[3]]);
@@ -346,14 +341,16 @@ export class EventStartNode<VT extends EventValueESType = number> extends EventN
346
341
  macroTime: this.macroTime?.dumpForNode(this),
347
342
  linkedMacro: [...this.linkedMacros].map(macro => macro.dumpLinkForNode(this)),
348
343
  }
349
- }
350
- getValueAt(beats: number): VT {
344
+ }
345
+ getValueAt(seconds: number, timeCalculator: TimeCalculator): VT;
346
+ getValueAt(beats: number): VT;
347
+ getValueAt(beatsOrSecs: number, timeCalculator?: TimeCalculator): VT {
351
348
  // 除了尾部的开始节点,其他都有下个节点
352
349
  // 钩定型缓动也有
353
350
  if (this.next.type === NodeType.TAIL) {
354
351
  return this.value;
355
352
  }
356
- return this.evaluator.eval(this as NonLastStartNode<VT>, beats);
353
+ return this.evaluator.eval(this as NonLastStartNode<VT>, beatsOrSecs, timeCalculator);
357
354
  }
358
355
  getSpeedValueAt(this: EventStartNode<number>, beats: number) {
359
356
  if (this.next.type === NodeType.TAIL) {
@@ -643,6 +640,12 @@ export class EventNodeSequence<VT extends EventValueESType = number> { // 泛型
643
640
  for (let index = 0; index < length; index++) {
644
641
  const event = data[index];
645
642
  const [start, end] = chart.createEventFromData<VT>(event, valueType, `${pos}.events[${index}]`);
643
+
644
+ // 收集被截的模板缓动
645
+ const evaluator = start.evaluator;
646
+ if (evaluator instanceof EasedEvaluator && evaluator.easing instanceof SegmentedEasing && evaluator.easing.easing instanceof TemplateEasing) {
647
+ chart.segmentedTemplates.set(evaluator.easing as any, [pos, start.time]);
648
+ }
646
649
  // 从前面复制了,复用性减一
647
650
  // KPA2没有更改RPE的按事件存储的机制。
648
651
  if (TC.lt(event.startTime, lastEndTime)) { // event.startTime < lastEndTime
@@ -798,6 +801,9 @@ export class EventNodeSequence<VT extends EventValueESType = number> { // 泛型
798
801
  getValueAt(beats: number, usePrev: boolean = false): VT {
799
802
  return this.getNodeAt(beats, usePrev).getValueAt(beats);
800
803
  }
804
+ getValueAtBySecs(beats: number, seconds: number, timeCalculator: TimeCalculator, usePrev: boolean = false) {
805
+ return this.getNodeAt(beats, usePrev).getValueAt(seconds, timeCalculator);
806
+ }
801
807
  getFloorPositionAt(this: EventNodeSequence<number>, beats: number, timeCalculator: TimeCalculator) {
802
808
  const node: EventStartNode<number> = this.getNodeAt(beats);
803
809
  const value = node.getLocalFloorPos(beats, timeCalculator) + node.floorPosition;
@@ -826,7 +832,7 @@ export class EventNodeSequence<VT extends EventValueESType = number> { // 泛型
826
832
  const prevStart = node.previous.previous;
827
833
  currentFP = prevStart.floorPosition + prevStart.getFullLocalFloorPos(tc);
828
834
  } else {
829
- currentFP = 0;
835
+ node.floorPosition = currentFP = 0;
830
836
  }
831
837
  while (true) {
832
838
  const canBeEnd = node.next;
@@ -956,15 +962,50 @@ export class EventNodeSequence<VT extends EventValueESType = number> { // 泛型
956
962
  }
957
963
 
958
964
  let lastEnd: EventEndNode<VT> = endNode;
965
+ currentNode = endNode.next;
959
966
  while (true) {
967
+ const evaluator = currentNode.evaluator;
968
+ if (this.type === EventType.easing && evaluator instanceof EasedEvaluator && evaluator.easing instanceof TemplateEasing) {
969
+ if (TemplateEasing.checkCircularReference(this as EventNodeSequence, evaluator.easing)) {
970
+ err.TEMPLATE_EASING_CIRCULAR_REFERENCE(this.id).warn();
971
+ }
972
+ }
973
+ if (evaluator instanceof EasedEvaluator && evaluator.easing instanceof SegmentedEasing) {
974
+ const easing = evaluator.easing;
975
+ const inner = easing.easing;
976
+ if (inner.getValue(easing.left) === inner.getValue(easing.right)) {
977
+ err.EASING_DELTA_CANNOT_BE_ZERO(this.id, currentNode.time).warn();
978
+ }
979
+ }
960
980
  const endNode = currentNode.next;
961
981
  if (endNode.type === NodeType.TAIL) {
962
982
  break;
963
983
  }
984
+ if (!TC.gt(endNode.time, currentNode.time)) {
985
+ err.EVENT_NODE_TIME_NOT_INCREMENTAL(`${this.id}, ${currentNode.time}`).warn();
986
+ }
964
987
  if (TC.ne(lastEnd.time, currentNode.time)) {
965
988
  err.EVENT_NODE_NOT_DENSE(`${this.id}, ${currentNode.time}`).warn();
966
989
  }
967
990
  currentNode = currentNode.next.next;
991
+ lastEnd = endNode;
992
+ }
993
+ }
994
+ hasReferenceTo(this: EventNodeSequence<number>, seq: EventNodeSequence<number>) {
995
+
996
+ let node = this.head.next;
997
+ while (true) {
998
+ const endNode = node.next;
999
+ if (endNode.type === NodeType.TAIL) {
1000
+ break;
1001
+ }
1002
+ const evaluator = node.evaluator;
1003
+ if (evaluator instanceof EasedEvaluator && evaluator.easing instanceof TemplateEasing) {
1004
+ if (TemplateEasing.checkCircularReference(seq, evaluator.easing as TemplateEasing)) {
1005
+ return true;
1006
+ }
1007
+ }
1008
+ node = endNode.next;
968
1009
  }
969
1010
  }
970
1011
  }