adofai 2.10.0 → 2.11.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 CHANGED
@@ -1,112 +1,112 @@
1
- # ADOFAI
2
-
3
- A Javascript library for ADOFAI levels.
4
-
5
- ## Usage
6
- Preview / Edit the `.adofai` file.
7
-
8
- Re_ADOJAS(A Level Player of ADOFAI) uses `adofai` to parse ADOFAI Level file.
9
-
10
- ## Installation
11
-
12
- ```bash
13
- npm install adofai
14
- # or
15
- yarn add adofai
16
- # or
17
- pnpm install adofai
18
- ```
19
-
20
- if you want to display highlight of adofai file, you can use `Rhythm Game Syntax Highlighter` vscode extension.
21
-
22
- ## Got Started
23
-
24
- ### Import
25
-
26
- For Commonjs:
27
- ```ts
28
- const adofai = require('adofai');
29
- ```
30
-
31
- For ES6 Modules:
32
- ```ts
33
- import * as adofai from 'adofai';
34
- ```
35
-
36
- ### Create a Level
37
-
38
- ```ts
39
- const file = new adofai.Level(adofaiFileContent);
40
-
41
- //or
42
-
43
- const parser = new adofai.Parsers.StringParser();
44
- const file = new adofai.Level(adofaiFileContent,parser);
45
-
46
- //The advantage of the latter over the former is that it pre-initializes the Parser, avoiding multiple instantiations.
47
- ```
48
-
49
- Format:
50
- ```ts
51
- class Level {
52
- constructor(opt: string | LevelOptions, provider?: ParseProvider)
53
- }
54
-
55
- ```
56
- Available ParseProviders:
57
- `StringParser` `ArrayBufferParser` `BufferParser`
58
-
59
-
60
- Usually,only `StringParser` is needed.
61
- but you can use `BufferParser` to parse ADOFAI files in Node environment.
62
-
63
- On browser, you can also use `ArrayBuffer` to parse ADOFAI files.
64
- (`BufferParser` is not available in browser,but you can use browserify `Buffer` to polyfill)
65
-
66
- ### Load Level
67
- ```ts
68
- file.on('load'() => {
69
- //logic...
70
- })
71
- file.load()
72
- ```
73
-
74
- or you can use `then()`
75
- ```ts
76
- file.load().then(() => {
77
-
78
- })
79
- ```
80
-
81
- ### Export Level
82
- ```ts
83
- type FileType = 'string'|'object'
84
-
85
- file.export(type: FileType = 'string',indent?:number,useAdofaiStyle:boolean = true)
86
- ```
87
-
88
- method `export()` returns a Object or String.
89
-
90
- Object: return ADOFAI Object.
91
- String: return ADOFAI String.
92
-
93
- ```ts
94
- import fs from 'fs'
95
- type FileType = 'string'|'object'
96
-
97
- const content = file.export('string',null,true);
98
- fs.writeFileSync('output.adofai',content)
99
- ```
100
-
101
-
102
- ## Data Operation
103
-
104
- See interfaces to see all data.
105
-
106
- ```ts
107
- //Get AngleDatas:
108
- const angleDatas = file.angleData;
109
-
110
-
111
-
1
+ # ADOFAI
2
+
3
+ A Javascript library for ADOFAI levels.
4
+
5
+ ## Usage
6
+ Preview / Edit the `.adofai` file.
7
+
8
+ Re_ADOJAS(A Level Player of ADOFAI) uses `adofai` to parse ADOFAI Level file.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install adofai
14
+ # or
15
+ yarn add adofai
16
+ # or
17
+ pnpm install adofai
18
+ ```
19
+
20
+ if you want to display highlight of adofai file, you can use `Rhythm Game Syntax Highlighter` vscode extension.
21
+
22
+ ## Got Started
23
+
24
+ ### Import
25
+
26
+ For Commonjs:
27
+ ```ts
28
+ const adofai = require('adofai');
29
+ ```
30
+
31
+ For ES6 Modules:
32
+ ```ts
33
+ import * as adofai from 'adofai';
34
+ ```
35
+
36
+ ### Create a Level
37
+
38
+ ```ts
39
+ const file = new adofai.Level(adofaiFileContent);
40
+
41
+ //or
42
+
43
+ const parser = new adofai.Parsers.StringParser();
44
+ const file = new adofai.Level(adofaiFileContent,parser);
45
+
46
+ //The advantage of the latter over the former is that it pre-initializes the Parser, avoiding multiple instantiations.
47
+ ```
48
+
49
+ Format:
50
+ ```ts
51
+ class Level {
52
+ constructor(opt: string | LevelOptions, provider?: ParseProvider)
53
+ }
54
+
55
+ ```
56
+ Available ParseProviders:
57
+ `StringParser` `ArrayBufferParser` `BufferParser`
58
+
59
+
60
+ Usually,only `StringParser` is needed.
61
+ but you can use `BufferParser` to parse ADOFAI files in Node environment.
62
+
63
+ On browser, you can also use `ArrayBuffer` to parse ADOFAI files.
64
+ (`BufferParser` is not available in browser,but you can use browserify `Buffer` to polyfill)
65
+
66
+ ### Load Level
67
+ ```ts
68
+ file.on('load'() => {
69
+ //logic...
70
+ })
71
+ file.load()
72
+ ```
73
+
74
+ or you can use `then()`
75
+ ```ts
76
+ file.load().then(() => {
77
+
78
+ })
79
+ ```
80
+
81
+ ### Export Level
82
+ ```ts
83
+ type FileType = 'string'|'object'
84
+
85
+ file.export(type: FileType = 'string',indent?:number,useAdofaiStyle:boolean = true)
86
+ ```
87
+
88
+ method `export()` returns a Object or String.
89
+
90
+ Object: return ADOFAI Object.
91
+ String: return ADOFAI String.
92
+
93
+ ```ts
94
+ import fs from 'fs'
95
+ type FileType = 'string'|'object'
96
+
97
+ const content = file.export('string',null,true);
98
+ fs.writeFileSync('output.adofai',content)
99
+ ```
100
+
101
+
102
+ ## Data Operation
103
+
104
+ See interfaces to see all data.
105
+
106
+ ```ts
107
+ //Get AngleDatas:
108
+ const angleDatas = file.angleData;
109
+
110
+
111
+
112
112
  ```
@@ -14,6 +14,10 @@ export declare class Level {
14
14
  private _twirlCount;
15
15
  constructor(opt: string | LevelOptions, provider?: ParseProvider);
16
16
  generateGUID(): string;
17
+ /**
18
+ * 触发进度事件
19
+ */
20
+ private _emitProgress;
17
21
  load(): Promise<boolean>;
18
22
  on(eventName: string, callback: Function): string;
19
23
  trigger(eventName: string, data: any): void;
@@ -37,6 +41,12 @@ export declare class Level {
37
41
  actions: AdofaiEvent[];
38
42
  };
39
43
  calculateTileCoordinates(): void;
44
+ /**
45
+ * 计算所有 Tile 的坐标位置
46
+ * 触发 parse:tilePosition 和 parse:progress 事件报告进度
47
+ *
48
+ * 性能优化:预先构建 PositionTrack 索引,避免循环内重复遍历
49
+ */
40
50
  calculateTilePosition(): number[][];
41
51
  floorOperation(info?: {
42
52
  type: 'append' | 'insert' | 'delete';
@@ -35,10 +35,26 @@ export class Level {
35
35
  generateGUID() {
36
36
  return `event_${uuid()}`;
37
37
  }
38
+ /**
39
+ * 触发进度事件
40
+ */
41
+ _emitProgress(stage, current, total, data) {
42
+ const progressEvent = {
43
+ stage,
44
+ current,
45
+ total,
46
+ percent: total > 0 ? Math.round((current / total) * 100) : 0,
47
+ data
48
+ };
49
+ this.trigger('parse:progress', progressEvent);
50
+ this.trigger(`parse:${stage}`, progressEvent);
51
+ }
38
52
  load() {
39
53
  return new Promise((resolve, reject) => {
40
54
  let opt = this._options;
41
55
  let options;
56
+ // 阶段1: 解析输入
57
+ this._emitProgress('start', 0, 0);
42
58
  switch (typeof opt) {
43
59
  case 'string':
44
60
  try {
@@ -56,18 +72,31 @@ export class Level {
56
72
  reject("Options must be String or Object");
57
73
  return;
58
74
  }
59
- if (options && typeof options === 'object' && options !== null && typeof options.pathData !== 'undefined') {
60
- this.angleData = pathData.parseToangleData(options['pathData']);
75
+ // 阶段2: 处理 pathData angleData
76
+ const hasPathData = options && typeof options === 'object' && options !== null && typeof options.pathData !== 'undefined';
77
+ const hasAngleData = options && typeof options === 'object' && options !== null && typeof options.angleData !== 'undefined';
78
+ if (hasPathData) {
79
+ const pathDataStr = options['pathData'];
80
+ // 开始转换 pathData
81
+ this._emitProgress('pathData', 0, pathDataStr.length, { source: pathDataStr });
82
+ this.angleData = pathData.parseToangleData(pathDataStr);
83
+ // 转换完成,返回结果
84
+ this._emitProgress('pathData', pathDataStr.length, pathDataStr.length, {
85
+ source: pathDataStr,
86
+ processed: this.angleData
87
+ });
88
+ }
89
+ else if (hasAngleData) {
90
+ this.angleData = options['angleData'];
91
+ this._emitProgress('angleData', this.angleData.length, this.angleData.length, {
92
+ processed: this.angleData
93
+ });
61
94
  }
62
95
  else {
63
- if (options && typeof options === 'object' && options !== null && typeof options.angleData !== 'undefined') {
64
- this.angleData = options['angleData'];
65
- }
66
- else {
67
- reject("There is not any angle datas.");
68
- return;
69
- }
96
+ reject("There is not any angle datas.");
97
+ return;
70
98
  }
99
+ // 阶段3: 提取其他数据
71
100
  if (options && typeof options === 'object' && options !== null && typeof options.actions !== 'undefined') {
72
101
  this.actions = options['actions'];
73
102
  }
@@ -90,9 +119,11 @@ export class Level {
90
119
  this.tiles = [];
91
120
  this._angleDir = -180;
92
121
  this._twirlCount = 0;
122
+ // 阶段4: 创建 Tile 数组(带进度回调)
93
123
  this._createArray(this.angleData.length, { angleData: this.angleData, actions: this.actions, decorations: this.__decorations })
94
124
  .then(e => {
95
125
  this.tiles = e;
126
+ this._emitProgress('complete', this.angleData.length, this.angleData.length);
96
127
  this.trigger('load', this);
97
128
  resolve(true);
98
129
  }).catch(e => {
@@ -131,16 +162,36 @@ export class Level {
131
162
  }
132
163
  _createArray(xLength, opt) {
133
164
  return __awaiter(this, void 0, void 0, function* () {
134
- let m = Array.from({ length: xLength }, (_, i) => ({
135
- direction: opt.angleData[i],
136
- _lastdir: opt.angleData[i - 1] || 0,
137
- actions: this._filterByFloor(opt.actions, i),
138
- angle: this._parseAngle(opt.angleData, i, this._twirlCount % 2),
139
- addDecorations: this._filterByFloorwithDeco(opt.decorations, i),
140
- twirl: this._twirlCount,
141
- extraProps: {}
142
- }));
143
- return m;
165
+ const tiles = [];
166
+ const batchSize = Math.max(1, Math.floor(xLength / 100)); // 每批处理的数量,至少1个
167
+ for (let i = 0; i < xLength; i++) {
168
+ // 计算相对角度(会更新 _twirlCount)
169
+ const angle = this._parseAngle(opt.angleData, i, this._twirlCount % 2);
170
+ const tile = {
171
+ direction: opt.angleData[i],
172
+ _lastdir: opt.angleData[i - 1] || 0,
173
+ actions: this._filterByFloor(opt.actions, i),
174
+ angle: angle,
175
+ addDecorations: this._filterByFloorwithDeco(opt.decorations, i),
176
+ twirl: this._twirlCount,
177
+ extraProps: {}
178
+ };
179
+ tiles.push(tile);
180
+ // 每处理一批或最后一个时触发进度事件
181
+ if (i % batchSize === 0 || i === xLength - 1) {
182
+ this._emitProgress('relativeAngle', i + 1, xLength, {
183
+ tileIndex: i,
184
+ tile: tile,
185
+ angle: opt.angleData[i],
186
+ relativeAngle: angle
187
+ });
188
+ // 让出事件循环,避免阻塞
189
+ if (i % (batchSize * 10) === 0) {
190
+ yield new Promise(r => setTimeout(r, 0));
191
+ }
192
+ }
193
+ }
194
+ return tiles;
144
195
  });
145
196
  }
146
197
  _changeAngle() {
@@ -259,46 +310,72 @@ export class Level {
259
310
  calculateTileCoordinates() {
260
311
  console.warn("calculateTileCoordinates is deprecated. Use calculateTilePosition instead.");
261
312
  }
313
+ /**
314
+ * 计算所有 Tile 的坐标位置
315
+ * 触发 parse:tilePosition 和 parse:progress 事件报告进度
316
+ *
317
+ * 性能优化:预先构建 PositionTrack 索引,避免循环内重复遍历
318
+ */
262
319
  calculateTilePosition() {
263
- let angles = this.angleData;
264
- let floats = [];
265
- let positions = [];
266
- let startPos = [0, 0];
267
- for (let i = 0; i < this.tiles.length; i++) {
268
- let value = angles[i];
269
- if (value === 999) {
270
- value = angles[i - 1] + 180;
320
+ const angles = this.angleData;
321
+ const totalTiles = this.tiles.length;
322
+ const positions = [];
323
+ const startPos = [0, 0];
324
+ // 性能优化:预先构建 PositionTrack 索引 Map,O(n) 预处理
325
+ const positionTrackMap = new Map();
326
+ for (const action of this.actions) {
327
+ if (action.eventType === 'PositionTrack' && action.positionOffset) {
328
+ if (action.editorOnly !== true && action.editorOnly !== 'Enabled') {
329
+ positionTrackMap.set(action.floor, action);
330
+ }
271
331
  }
272
- floats.push(value);
273
332
  }
274
- for (let i = 0; i <= floats.length; i++) {
275
- let angle1 = Number((i === floats.length) ? floats[i - 1] : floats[i]) || 0;
276
- let angle2 = Number((i === 0) ? 0 : floats[i - 1]) || 0;
277
- let currentTile = this.tiles[i];
278
- if (this.getActionsByIndex('PositionTrack', i).count > 0) {
279
- let pevent = this.getActionsByIndex('PositionTrack', i).actions[0];
280
- if (pevent.positionOffset) {
281
- if (pevent['editorOnly'] !== true && pevent['editorOnly'] !== 'Enabled') {
282
- startPos[0] += pevent['positionOffset'][0];
283
- startPos[1] += pevent['positionOffset'][1];
284
- }
285
- }
333
+ // 触发开始事件
334
+ this._emitProgress('tilePosition', 0, totalTiles);
335
+ // 预处理 floats 数组
336
+ const floats = new Array(totalTiles);
337
+ for (let i = 0; i < totalTiles; i++) {
338
+ floats[i] = angles[i] === 999 ? angles[i - 1] + 180 : angles[i];
339
+ }
340
+ // 进度事件触发频率:每 1% 或最少每 100 tile 触发一次
341
+ const progressInterval = Math.max(100, Math.floor(totalTiles / 100));
342
+ for (let i = 0; i <= totalTiles; i++) {
343
+ const isLastTile = i === totalTiles;
344
+ const angle1 = isLastTile ? (floats[i - 1] || 0) : floats[i];
345
+ const angle2 = i === 0 ? 0 : (floats[i - 1] || 0);
346
+ const currentTile = this.tiles[i];
347
+ // 使用索引 Map 直接查询,O(1) 复杂度
348
+ const posTrack = positionTrackMap.get(i);
349
+ if (posTrack === null || posTrack === void 0 ? void 0 : posTrack.positionOffset) {
350
+ startPos[0] += posTrack.positionOffset[0];
351
+ startPos[1] += posTrack.positionOffset[1];
286
352
  }
287
- let tempPos = [
288
- Number(startPos[0]),
289
- Number(startPos[1])
290
- ];
353
+ const tempPos = [startPos[0], startPos[1]];
291
354
  positions.push(tempPos);
292
- if (typeof currentTile !== 'undefined') {
355
+ if (currentTile) {
293
356
  currentTile.position = tempPos;
294
- ;
295
357
  currentTile.extraProps.angle1 = angle1;
296
358
  currentTile.extraProps.angle2 = angle2 - 180;
297
- currentTile.extraProps.cangle = i === floats.length ? floats[i - 1] + 180 : floats[i];
359
+ currentTile.extraProps.cangle = isLastTile ? floats[i - 1] + 180 : floats[i];
360
+ }
361
+ // 更新位置
362
+ const rad = angle1 * Math.PI / 180;
363
+ startPos[0] += Math.cos(rad);
364
+ startPos[1] += Math.sin(rad);
365
+ // 触发进度事件(降低频率)
366
+ if (i % progressInterval === 0 || isLastTile) {
367
+ this._emitProgress('tilePosition', i, totalTiles, {
368
+ tileIndex: i,
369
+ tile: currentTile,
370
+ position: tempPos,
371
+ angle: angle1
372
+ });
298
373
  }
299
- startPos[0] += Math.cos(angle1 * Math.PI / 180);
300
- startPos[1] += Math.sin(angle1 * Math.PI / 180);
301
374
  }
375
+ // 触发完成事件
376
+ this._emitProgress('tilePosition', totalTiles, totalTiles, {
377
+ processed: positions.flat()
378
+ });
302
379
  return positions;
303
380
  }
304
381
  floorOperation(info = { type: 'append', direction: 0 }) {
@@ -31,3 +31,26 @@ export interface Tile {
31
31
  export interface ParseProvider {
32
32
  parse(t: string): LevelOptions;
33
33
  }
34
+ export interface ParseProgressEvent {
35
+ stage: 'start' | 'pathData' | 'angleData' | 'relativeAngle' | 'tilePosition' | 'complete';
36
+ current: number;
37
+ total: number;
38
+ percent: number;
39
+ /** 当前阶段产生的数据 */
40
+ data?: {
41
+ /** pathData 阶段: 原始 pathData 字符串; angleData 阶段: 解析后的角度数组 */
42
+ source?: string | number[];
43
+ /** 已处理的部分数据 */
44
+ processed?: number[];
45
+ /** 当前处理的 tile 数据 */
46
+ tile?: Tile;
47
+ /** 当前处理的 tile 索引 */
48
+ tileIndex?: number;
49
+ /** angleData: 当前角度值 */
50
+ angle?: number;
51
+ /** relativeAngle: 计算出的相对角度 */
52
+ relativeAngle?: number;
53
+ /** tilePosition: 当前坐标 */
54
+ position?: number[];
55
+ };
56
+ }