adofai 3.2.0 → 3.3.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,11 +1,17 @@
1
1
  # ADOFAI
2
2
 
3
- A Javascript library for ADOFAI levels.
3
+ A zero-dependency JavaScript/TypeScript library for parsing, editing, and exporting ADOFAI level files. Fully browser-compatible.
4
4
 
5
- ## Usage
6
- Preview / Edit the `.adofai` file.
5
+ ## Features
7
6
 
8
- Re_ADOJAS(A Level Player of ADOFAI) uses `adofai` to parse ADOFAI Level file.
7
+ - **Multiple Parsers** `StringParser`, `BufferParser`, `ArrayBufferParser`, `LargeFileParser` for incremental large-file parsing
8
+ - **Level Management** — load, edit, and export `.adofai` files with full tile and event access
9
+ - **Typed Events** — 57 typed event interfaces covering all ADOFAI event types
10
+ - **Shared Types** — const enums and utility types for ADOFAI-specific values (angles, hitboxes, filters, etc.)
11
+ - **PathData Conversion** — convert between pathData string and angleData array
12
+ - **Effect Filtering** — preset and custom event filtering (clear effects, keep/exclude events)
13
+ - **Precompute Mode** — batch-process and cache progress events for rendering pipelines
14
+ - **Lightweight Data** — memory-efficient tile data extraction for large levels
9
15
 
10
16
  ## Installation
11
17
 
@@ -14,99 +20,465 @@ npm install adofai
14
20
  # or
15
21
  yarn add adofai
16
22
  # or
17
- pnpm install adofai
23
+ pnpm add adofai
18
24
  ```
19
25
 
20
- if you want to display highlight of adofai file, you can use `Rhythm Game Syntax Highlighter` vscode extension.
26
+ ## Import
21
27
 
22
- ## Got Started
23
-
24
- ### Import
28
+ **ESM:**
29
+ ```ts
30
+ import * as adofai from 'adofai';
31
+ import { Level, Parsers, Types, Events, Structure } from 'adofai';
32
+ ```
25
33
 
26
- For Commonjs:
34
+ **CommonJS:**
27
35
  ```ts
28
36
  const adofai = require('adofai');
29
37
  ```
30
38
 
31
- For ES6 Modules:
39
+ **Subpath imports:**
32
40
  ```ts
33
- import * as adofai from 'adofai';
41
+ import { StringParser } from 'adofai/parser/string';
42
+ import { BufferParser } from 'adofai/parser/buffer';
43
+ import { ArrayBufferParser } from 'adofai/parser/array-buffer';
44
+ import * as Types from 'adofai/types';
45
+ import * as Events from 'adofai/event';
34
46
  ```
35
47
 
36
- ### Create a Level
48
+ ---
49
+
50
+ ## Parsers
51
+
52
+ Four parsers handle different input formats. All are zero-dependency and browser-compatible.
53
+
54
+ ### StringParser
55
+
56
+ Parses ADOFAI JSON from a string. Handles non-standard formatting (trailing commas, raw newlines in strings).
57
+
58
+ ```ts
59
+ import { StringParser } from 'adofai';
60
+
61
+ const parser = new StringParser();
62
+ const data = parser.parse(`{ "angleData": [...], "settings": {...}, "actions": [...] }`);
63
+ ```
64
+
65
+ ### BufferParser (Uint8Array)
66
+
67
+ Parses ADOFAI JSON directly from a `Uint8Array` binary stream using a byte-level state machine. No intermediate string conversion, handles BOM stripping automatically.
37
68
 
38
69
  ```ts
39
- const file = new adofai.Level(adofaiFileContent);
70
+ import { BufferParser } from 'adofai';
40
71
 
41
- //or
72
+ const parser = new BufferParser();
73
+ const u8 = new Uint8Array(await file.arrayBuffer());
74
+ const data = parser.parse(u8);
75
+ ```
76
+
77
+ ### ArrayBufferParser
42
78
 
43
- const parser = new adofai.Parsers.StringParser();
44
- const file = new adofai.Level(adofaiFileContent,parser);
79
+ Accepts `ArrayBuffer` or `string`. Handles BOM stripping, trailing comma normalization, and UTF-8 decoding.
80
+
81
+ ```ts
82
+ import { ArrayBufferParser } from 'adofai';
45
83
 
46
- //The advantage of the latter over the former is that it pre-initializes the Parser, avoiding multiple instantiations.
84
+ const parser = new ArrayBufferParser();
85
+ const buffer = await response.arrayBuffer();
86
+ const data = parser.parse(buffer);
47
87
  ```
48
88
 
49
- Format:
89
+ ### LargeFileParser
90
+
91
+ Memory-optimized parser for large `.adofai` files. Scans raw bytes to find JSON root properties, then parses sections incrementally without loading the entire file into a JS string. Ideal for files with massive `angleData` or `action` arrays.
92
+
50
93
  ```ts
51
- class Level {
52
- constructor(opt: string | LevelOptions, provider?: ParseProvider)
53
- }
94
+ import { LargeFileParser } from 'adofai';
54
95
 
96
+ const parser = new LargeFileParser((stage, percent) => {
97
+ console.log(`[${stage}] ${percent}%`);
98
+ }, {
99
+ skipLargeActions: false, // skip actions if > 100MB
100
+ maxActions: 0 // limit parsed actions count
101
+ });
102
+
103
+ const result = parser.parse(arrayBuffer);
104
+ // result: { settings?, angleData?, pathData?, actions?, decorations? }
55
105
  ```
56
- Available ParseProviders:
57
- `StringParser` `ArrayBufferParser` `BufferParser`
58
106
 
107
+ Key behaviors:
108
+ - **< 50MB** — sections are fully JSON.parsed normally.
109
+ - **> 50MB** — `actions` is parsed incrementally (each object parsed independently).
110
+ - **> 100MB** — actions can be skipped entirely via `skipLargeActions: true`.
111
+ - **Any size** — `angleData` is always parsed incrementally (number-by-number).
112
+ - BOM is automatically stripped.
113
+
114
+ ---
59
115
 
60
- Usually,only `StringParser` is needed.
61
- but you can use `BufferParser` to parse ADOFAI files in Node environment.
116
+ ## Level
62
117
 
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)
118
+ The `Level` class is the core data structure. It accepts ADOFAI file content (string, object, ArrayBuffer, Uint8Array, or Buffer) and provides tile management and export.
119
+
120
+ ### Create & Load
65
121
 
66
- ### Load Level
67
122
  ```ts
68
- file.on('load'() => {
69
- //logic...
70
- })
71
- file.load()
123
+ import { Level } from 'adofai';
124
+
125
+ // From string
126
+ const level = new Level(adofaiJsonString);
127
+ await level.load();
128
+
129
+ // With custom parser provider
130
+ const level = new Level(rawData, bufferParser);
131
+ await level.load();
132
+
133
+ // From already-parsed object
134
+ const level = new Level({
135
+ angleData: [...],
136
+ settings: { ... },
137
+ actions: [...],
138
+ decorations: [...]
139
+ });
140
+ await level.load();
141
+
142
+ // Event-based loading
143
+ level.on('load', () => {
144
+ console.log('Level loaded:', level.tiles.length, 'tiles');
145
+ });
146
+ level.load();
147
+
148
+ // Progress events
149
+ level.on('parse:progress', (event) => {
150
+ // { stage: 'relativeAngle', current: 500, total: 1000, percent: 50 }
151
+ console.log(`${event.stage}: ${event.percent}%`);
152
+ });
153
+ ```
154
+
155
+ Progress stages: `start` → `pathData` | `angleData` → `relativeAngle` → `tilePosition` → `complete`
156
+
157
+ ### Data Model Overview
158
+
159
+ After loading, the level data is organized into two layers:
160
+
161
+ ```
162
+ ┌──────────────────────────────────────────┐
163
+ │ Source Data (read-only initial values) │
164
+ │ level.angleData — raw angle array │
165
+ │ level.actions — flat event list │
166
+ │ level.__decorations — flat deco list │
167
+ │ level.settings — level settings │
168
+ ├──────────────────────────────────────────┤
169
+ │ Working Data (primary operation target) │
170
+ │ level.tiles — Tile[] │
171
+ └──────────────────────────────────────────┘
72
172
  ```
73
173
 
74
- or you can use `then()`
174
+ **`level.tiles` is where all data operations happen.** The source arrays (`angleData`, `actions`, `decorations`) are initial inputs and are **not** kept in sync when you modify tiles. When you export, `angleData`, `actions`, and `decorations` are reconstructed from `level.tiles`.
175
+
176
+ ### Tile Structure
177
+
178
+ Each tile in `level.tiles` has the following structure:
179
+
75
180
  ```ts
76
- file.load().then(() => {
181
+ interface Tile {
182
+ direction?: number; // Original angle data value (incl. 999)
183
+ angle?: number; // Computed relative angle (for rendering)
184
+ _lastdir?: number; // Previous tile's direction
185
+ twirl?: number; // Accumulated twirl count up to this tile
186
+ actions: ActionData[]; // Events belonging to this tile
187
+ addDecorations?: ActionData[]; // Decorations on this tile
188
+ position?: number[]; // Computed [x, y] position
189
+ extraProps?: Record<string, any>; // Extra computed data (angle1, angle2, cangle)
190
+ }
191
+ ```
77
192
 
78
- })
193
+ ### Working with Tiles
194
+
195
+ **Read tile data:**
196
+ ```ts
197
+ // Total tile count
198
+ level.tiles.length;
199
+
200
+ // Access a specific tile
201
+ const tile = level.tiles[42];
202
+ tile.direction; // raw angle value
203
+ tile.angle; // relative angle
204
+ tile.actions; // events on this tile
205
+ tile.addDecorations; // decorations on this tile
206
+ tile.twirl; // twirl count
207
+ tile.position; // [x, y] (after calculateTilePosition)
79
208
  ```
80
209
 
81
- ### Export Level
210
+ **Modify tiles:**
82
211
  ```ts
83
- type FileType = 'string'|'object'
212
+ // Append a new tile
213
+ level.floorOperation({ type: 'append', direction: 180 });
84
214
 
85
- file.export(type: FileType = 'string',indent?:number,useAdofaiStyle:boolean = true)
215
+ // Insert at specific index
216
+ level.floorOperation({ type: 'insert', direction: 90, id: 10 });
217
+
218
+ // Delete a tile
219
+ level.floorOperation({ type: 'delete', id: 10 });
86
220
  ```
87
221
 
88
- method `export()` returns a Object or String.
222
+ **Query events across tiles:**
223
+ ```ts
224
+ // Find all tiles with a specific event type
225
+ const results = level.filterActionsByEventType('Flash');
226
+ // returns { index: number, action: ActionData }[]
89
227
 
90
- Object: return ADOFAI Object.
91
- String: return ADOFAI String.
228
+ // Get events at a specific tile index
229
+ const { count, actions } = level.getActionsByIndex('MoveTrack', 5);
230
+ ```
231
+
232
+ ### Calculate Tile Positions
233
+
234
+ Populates `tile.position` and `tile.extraProps` for all tiles.
92
235
 
93
236
  ```ts
94
- import fs from 'fs'
95
- type FileType = 'string'|'object'
237
+ const positions = level.calculateTilePosition();
238
+ // returns number[][] — [x, y] for each tile including endpoint
96
239
 
97
- const content = file.export('string',null,true);
98
- fs.writeFileSync('output.adofai',content)
240
+ // After this call, each tile.position is set
241
+ level.tiles[5].position; // [x, y]
242
+ level.tiles[5].extraProps; // { angle1, angle2, cangle }
99
243
  ```
100
244
 
245
+ ### Effect Filtering (operates on tiles)
246
+
247
+ All effect operations modify `level.tiles` in-place.
248
+
249
+ ```ts
250
+ import { Presets } from 'adofai';
251
+
252
+ // Using a preset
253
+ level.clearEffect('preset_noeffect');
101
254
 
102
- ## Data Operation
255
+ // Custom filter — keep only specific events
256
+ level.clearEvent({ type: 'include', events: ['SetSpeed', 'Twirl'] });
103
257
 
104
- See interfaces to see all data.
258
+ // Custom filter exclude specific events
259
+ level.clearEvent({ type: 'exclude', events: ['Flash', 'Bloom'] });
260
+
261
+ // Clear all decorations from all tiles
262
+ level.clearDeco();
263
+ ```
264
+
265
+ ### Export (reconstructs from tiles)
266
+
267
+ ```ts
268
+ // Export as formatted ADOFAI JSON string
269
+ const str = level.export('string', 0, true);
270
+ // fs.writeFileSync('output.adofai', str);
271
+
272
+ // Export as object
273
+ const obj = level.export('object', 0, true);
274
+ // { angleData, settings, actions, decorations }
275
+ // All three arrays are reconstructed from level.tiles
276
+ ```
277
+
278
+ ### Event System
105
279
 
106
280
  ```ts
107
- //Get AngleDatas:
108
- const angleDatas = file.angleData;
281
+ // Listen to lifecycle events
282
+ const guid = level.on('load', (level) => { /* ... */ });
283
+
284
+ // Remove listener by GUID
285
+ level.off(guid);
286
+
287
+ // Trigger custom events
288
+ level.trigger('custom:event', data);
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Types
294
+
295
+ Shared ADOFAI-specific types and const enums, exported via `Types.*` or `adofai/types`.
296
+
297
+ ### Const Enums
298
+
299
+ ```ts
300
+ import { Types } from 'adofai';
301
+
302
+ Types.TextAnchor.UpperLeft // 'UpperLeft'
303
+ Types.Hitbox.None // 'None'
304
+ Types.FilterType.Bloom // 'Bloom'
305
+ Types.FlashStyle.Flash // 'Flash'
306
+ Types.RelativeTo.Tile // 'Tile'
307
+ Types.TargetPlanet.All // 'All'
308
+ Types.AngleCorrectionDir.CW // 'CW'
309
+ Types.InputEventState.Subscribe // 'Subscribe'
310
+ Types.InputEventTarget.Pressed // 'Pressed'
311
+ Types.Condition.IfPassed // 'IfPassed'
312
+ Types.BgDisplayMode.FitToScreen // 'FitToScreen'
313
+ Types.BgShapeType.Circle // 'Circle'
314
+ Types.HitsoundType.Kick // 'Kick'
315
+ Types.HoldMidSoundTimingRelativeTo.Start // 'Start'
316
+ ```
317
+
318
+ ### Utility Types
319
+
320
+ ```ts
321
+ Types.Vec2 // [number, number]
322
+ Types.ABoolean // boolean | 'Enabled' | 'Disabled' | 'true' | 'false'
323
+ Types.TileReference // [number, TileReferenceType]
324
+ Types.TileReferenceType // 'ThisTile' | 'Start' | 'End'
325
+ Types.Vec2Like // [number, number] | { x: number; y: number }
326
+ ```
327
+
328
+ ### Utility Functions
329
+
330
+ ```ts
331
+ // Evaluate ABoolean values
332
+ Types.isEventEnabled(value, defaultValue); // boolean
333
+
334
+ // Resolve relative tile references
335
+ Types.resolveTileReference([2, 'End'], currentTileId, totalTiles);
336
+ // = totalTiles - 1 + 2
337
+
338
+ Types.resolveTileReference([-1, 'ThisTile'], 5, 100);
339
+ // = 4
340
+
341
+ // Normalize ADOFAI position formats (array and object forms)
342
+ Types.normalizeVec2([3, 5]); // [3, 5]
343
+ Types.normalizeVec2({ x: 3, y: 5 }); // [3, 5]
344
+ ```
345
+
346
+ ---
347
+
348
+ ## PathData
349
+
350
+ Convert pathData strings (e.g. `"REJW"`) into angleData arrays.
351
+
352
+ ```ts
353
+ import { pathData } from 'adofai';
354
+
355
+ // Character → angle mapping table
356
+ pathData.pathDataTable;
357
+ // { R: 0, p: 15, J: 30, E: 45, T: 60, ... }
358
+
359
+ // Convert path string to angle numbers
360
+ const angleData = pathData.parseToangleData("REJW");
361
+ // [0, 45, 30, 165]
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Event Interfaces
367
+
368
+ 57 typed event interfaces covering all ADOFAI event types. Each extends `AdofaiEvent` with `floor` and `eventType` fields.
369
+
370
+ ```ts
371
+ import type { Events } from 'adofai';
372
+
373
+ // Each event type has a matching interface:
374
+ const flash: Events.Flash = {
375
+ floor: 0,
376
+ eventType: 'Flash',
377
+ duration: 1,
378
+ color: 'ffffff',
379
+ flashStyle: Types.FlashStyle.Flash,
380
+ opacity: 100,
381
+ };
382
+
383
+ const setSpeed: Events.SetSpeed = {
384
+ floor: 0,
385
+ eventType: 'SetSpeed',
386
+ speed: 1.0,
387
+ };
388
+
389
+ const moveTrack: Events.MoveTrack = {
390
+ floor: 5,
391
+ eventType: 'MoveTrack',
392
+ duration: 4,
393
+ positionOffset: [0, 3],
394
+ rotation: 90,
395
+ easing: 'Linear',
396
+ };
397
+ ```
398
+
399
+ Full list of event types: `SetSpeed`, `Twirl`, `Checkpoint`, `MoveCamera`, `CustomBackground`, `ChangeTrack`, `ColorTrack`, `AnimateTrack`, `RecolorTrack`, `MoveTrack`, `SetText`, `Flash`, `SetHitsound`, `SetFilter`, `SetFilterAdvanced`, `SetPlanetRotation`, `HallOfMirrors`, `ShakeScreen`, `MoveDecorations`, `PositionTrack`, `RepeatEvents`, `Bloom`, `Hold`, `SetHoldSound`, `SetConditionalEvents`, `ScreenTile`, `ScreenScroll`, `EditorComment`, `Bookmark`, `CallMethod`, `AddComponent`, `PlaySound`, `MultiPlanet`, `FreeRoam`, `FreeRoamTwirl`, `FreeRoamRemove`, `Pause`, `AutoPlayTiles`, `Hide`, `ScaleMargin`, `ScaleRadius`, `Multitap`, `TileDimensions`, `KillPlayer`, `ScalePlanets`, `SetFloorIcon`, `AddDecoration`, `AddText`, `AddObject`, `SetObject`, `SetDefaultText`, `SetFrameRate`, `AddParticle`, `SetParticle`, `EmitParticle`, `SetInputEvent`.
400
+
401
+ ---
402
+
403
+ ---
404
+
405
+ ## Effect Presets
406
+
407
+ ## Advanced: Precompute Mode
408
+
409
+ For rendering pipelines that need deterministic event replay. Instead of firing progress events during load, events are cached and can be polled.
410
+
411
+ ```ts
412
+ level.enablePrecomputeMode();
413
+ await level.load();
414
+ level.calculateTilePosition();
415
+
416
+ const events = level.getPrecomputedEvents();
417
+ // { start: [...], pathData: [...], angleData: [...], ... }
418
+
419
+ // Get events up to a specific progress percentage
420
+ const at50 = level.getEventsAtPercent(50);
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Advanced: Lightweight Tile Data
426
+
427
+ For large levels where storing full `Tile[]` objects is memory-prohibitive. Precomputes only rendering-essential data.
428
+
429
+ ```ts
430
+ // Precompute lightweight data (angles + positions + twirl flags)
431
+ level.precomputeLightweight();
432
+
433
+ // Access the compact data
434
+ const data = level.getLightweightData();
435
+ // { totalTiles, angles: number[], positions: [number,number][], twirlFlags: boolean[] }
436
+
437
+ // Get a range of tiles (chunked access)
438
+ const chunk = level.getLightweightDataRange(0, 100);
439
+ // { angles: [...], positions: [...], twirlFlags: [...] }
440
+
441
+ // Get single tile render data
442
+ const tile = level.getTileRenderData(42);
443
+ // { angle, position, hasTwirl }
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Structure Interfaces
449
+
450
+ Available via `adofai/structure`:
451
+
452
+ ```ts
453
+ import type { AdofaiEvent, LevelOptions, Tile, ParseProgressEvent } from 'adofai/structure';
454
+
455
+ // AdofaiEvent: { floor: number, eventType: string, [key: string]: any }
456
+ // LevelOptions: { pathData?: string, angleData?: number[], actions, settings, decorations }
457
+ // Tile: { direction?, angle?, actions, addDecorations?, position?, ... }
458
+ // ParseProgressEvent: { stage, current, total, percent, data? }
459
+ ```
460
+
461
+ ---
462
+
463
+ ## Package Exports
464
+
465
+ | Import path | Contents |
466
+ |---|---|
467
+ | `adofai` | Main: Level, Parsers, Types, Events, Structure, Presets, pathData |
468
+ | `adofai/parser` | Parser classes |
469
+ | `adofai/parser/string` | StringParser |
470
+ | `adofai/parser/buffer` | BufferParser |
471
+ | `adofai/parser/array-buffer` | ArrayBufferParser |
472
+ | `adofai/types` | Types (const enums, utilities) |
473
+ | `adofai/event` | All event type interfaces |
474
+ | `adofai/structure` | Core interfaces (LevelOptions, Tile, etc.) |
475
+ | `adofai/filter` | Filter presets |
476
+ | `adofai/filter/effect-processor` | Low-level effect processor |
477
+ | `adofai/pathdata` | PathData conversion table |
478
+ | `adofai/types` | Utility types and functions |
109
479
 
480
+ ---
110
481
 
482
+ ## License
111
483
 
112
- ```
484
+ BSD-3-Clause
@@ -1,8 +1,5 @@
1
- interface PathDataTable {
2
- [key: string]: number;
3
- }
4
1
  declare const _default: {
5
- pathDataTable: PathDataTable;
2
+ pathDataTable: Record<string, number>;
6
3
  parseToangleData: (pathdata: string) => number[];
7
4
  };
8
5
  export default _default;
@@ -1,5 +1,48 @@
1
- const pathDataTable = { "R": 0, "p": 15, "J": 30, "E": 45, "T": 60, "o": 75, "U": 90, "q": 105, "G": 120, "Q": 135, "H": 150, "W": 165, "L": 180, "x": 195, "N": 210, "Z": 225, "F": 240, "V": 255, "D": 270, "Y": 285, "B": 300, "C": 315, "M": 330, "A": 345, "5": 555, "6": 666, "7": 777, "8": 888, "!": 999 };
2
- const parseToangleData = (pathdata) => Array.from(pathdata).map(t => pathDataTable[t]);
1
+ /** Standard direction characters absolute angle */
2
+ const pathDataTable = {
3
+ "R": 0, "p": 15, "J": 30, "E": 45, "T": 60, "o": 75, "U": 90, "q": 105,
4
+ "G": 120, "Q": 135, "H": 150, "W": 165, "L": 180, "x": 195, "N": 210,
5
+ "Z": 225, "F": 240, "V": 255, "D": 270, "Y": 285, "B": 300, "C": 315,
6
+ "M": 330, "A": 345, "!": 999
7
+ };
8
+ /**
9
+ * Special offset characters — these are NOT absolute angles.
10
+ * Instead, they represent a relative change from the previous angle:
11
+ * result = previous_angle + offset
12
+ */
13
+ const offsetMap = {
14
+ "5": 72,
15
+ "6": -72,
16
+ "7": 52,
17
+ "8": -52,
18
+ "9": -30,
19
+ "h": 120,
20
+ "j": -120,
21
+ "t": 60,
22
+ "y": 300,
23
+ };
24
+ const parseToangleData = (pathdata) => {
25
+ const result = new Array(pathdata.length);
26
+ let prev = 0;
27
+ for (let i = 0; i < pathdata.length; i++) {
28
+ const c = pathdata[i];
29
+ if (c in pathDataTable) {
30
+ // Standard character: absolute angle
31
+ result[i] = pathDataTable[c];
32
+ prev = pathDataTable[c];
33
+ }
34
+ else if (c in offsetMap) {
35
+ // Special character: relative offset from previous angle
36
+ result[i] = prev + offsetMap[c];
37
+ prev = result[i];
38
+ }
39
+ else {
40
+ // Unknown character: keep current angle
41
+ result[i] = prev;
42
+ }
43
+ }
44
+ return result;
45
+ };
3
46
  export default {
4
47
  pathDataTable,
5
48
  parseToangleData