adofai-lib 1.0.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 +71 -0
- package/dist/index.js +18 -0
- package/jest.config.js +6 -0
- package/package.json +27 -0
- package/src/LevelDict.ts +171 -0
- package/src/__tests__/LevelFile.test.ts +83 -0
- package/src/index.ts +2 -0
- package/src/types/index.ts +28 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADOFAI Library
|
|
2
|
+
|
|
3
|
+
A TypeScript library for working with ADOFAI level files. This library provides a simple interface for reading, modifying, and writing ADOFAI level files.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install adofai-lib
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { LevelDict } from 'adofai-lib';
|
|
15
|
+
|
|
16
|
+
// Load an ADOFAI level file
|
|
17
|
+
const level = new LevelDict('path/to/level.adofai');
|
|
18
|
+
|
|
19
|
+
// Get all angles in the level
|
|
20
|
+
const angles = level.getAngles();
|
|
21
|
+
|
|
22
|
+
// Get relative angles (degrees between each pair of tiles)
|
|
23
|
+
const relativeAngles = level.getAnglesRelative();
|
|
24
|
+
|
|
25
|
+
// Get all actions in the level
|
|
26
|
+
const actions = level.getActions();
|
|
27
|
+
|
|
28
|
+
// Get all decorations in the level
|
|
29
|
+
const decorations = level.getDecorations();
|
|
30
|
+
|
|
31
|
+
// Modify the level...
|
|
32
|
+
|
|
33
|
+
// Save the modified level
|
|
34
|
+
level.writeToFile('path/to/output.adofai');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Read and write ADOFAI level files
|
|
40
|
+
- Get and set tile angles
|
|
41
|
+
- Get relative angles between tiles
|
|
42
|
+
- Manage actions and decorations
|
|
43
|
+
- Support for both angle-based and path-based level formats
|
|
44
|
+
|
|
45
|
+
## API Reference
|
|
46
|
+
|
|
47
|
+
### LevelDict
|
|
48
|
+
|
|
49
|
+
The main class for working with ADOFAI level files.
|
|
50
|
+
|
|
51
|
+
#### Constructor
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
new LevelDict(filename?: string, encoding?: string)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `filename`: Path to the ADOFAI level file (optional)
|
|
58
|
+
- `encoding`: File encoding (default: 'utf-8')
|
|
59
|
+
|
|
60
|
+
#### Methods
|
|
61
|
+
|
|
62
|
+
- `getAngles()`: Returns an array of all tile angles
|
|
63
|
+
- `setAngles(angles: number[])`: Sets the angles for all tiles
|
|
64
|
+
- `getAnglesRelative(ignoreTwirls?: boolean, padMidspins?: boolean)`: Returns relative angles between tiles
|
|
65
|
+
- `getActions(condition?: (action: Action) => boolean)`: Returns all actions matching the condition
|
|
66
|
+
- `getDecorations(condition?: (decoration: Decoration) => boolean)`: Returns all decorations matching the condition
|
|
67
|
+
- `writeToFile(filename?: string)`: Saves the level to a file
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./LevelDict"), exports);
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adofai-lib",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A TypeScript library for working with ADOFAI level files",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"adofai",
|
|
14
|
+
"adofai-lib",
|
|
15
|
+
"adofai-lib-js",
|
|
16
|
+
"adofai-lib-js-v0w4n"
|
|
17
|
+
],
|
|
18
|
+
"author": "V0W4N (original by M1n3c4rt)",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jest": "^29.5.12",
|
|
22
|
+
"@types/node": "^20.11.24",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"ts-jest": "^29.1.2",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/LevelDict.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { Action, Decoration, Settings, Tile, InvalidLevelDictException } from './types';
|
|
3
|
+
|
|
4
|
+
export class LevelFile {
|
|
5
|
+
private filename: string;
|
|
6
|
+
private encoding: BufferEncoding;
|
|
7
|
+
private tiles: Tile[];
|
|
8
|
+
private nonFloorDecos: Decoration[];
|
|
9
|
+
private settings: Settings;
|
|
10
|
+
|
|
11
|
+
constructor(filename: string = '', encoding: BufferEncoding = 'utf-8') {
|
|
12
|
+
this.filename = filename;
|
|
13
|
+
this.encoding = encoding;
|
|
14
|
+
this.tiles = [];
|
|
15
|
+
this.nonFloorDecos = [];
|
|
16
|
+
this.settings = {};
|
|
17
|
+
|
|
18
|
+
const leveldict = this.getFileDict();
|
|
19
|
+
if (!leveldict.actions || !leveldict.settings || (!leveldict.angleData && !leveldict.pathData)) {
|
|
20
|
+
throw new InvalidLevelDictException(`The provided .adofai file is invalid. (missing: ${!leveldict.actions ? 'actions' : ''} ${!leveldict.settings ? 'settings' : ''} ${!leveldict.angleData && !leveldict.pathData ? 'angleData or pathData' : ''})`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let angleData: number[];
|
|
24
|
+
if ('angleData' in leveldict) {
|
|
25
|
+
angleData = leveldict.angleData;
|
|
26
|
+
} else {
|
|
27
|
+
const pathchars: { [key: string]: number } = {
|
|
28
|
+
'R': 0, 'p': 15, 'J': 30, 'E': 45, 'T': 60, 'o': 75, 'U': 90,
|
|
29
|
+
'q': 105, 'G': 120, 'Q': 135, 'H': 150, 'W': 165, 'L': 180,
|
|
30
|
+
'x': 195, 'N': 210, 'Z': 225, 'F': 240, 'V': 255, 'D': 270,
|
|
31
|
+
'Y': 285, 'B': 300, 'C': 315, 'M': 330, 'A': 345, '!': 999
|
|
32
|
+
};
|
|
33
|
+
angleData = Array.from(leveldict.pathData as string).map(char => pathchars[char]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
angleData.push(angleData[angleData.length - 1] !== 999 ? angleData[angleData.length - 1] : (angleData[angleData.length - 2] + 180) % 360);
|
|
37
|
+
|
|
38
|
+
const actions = leveldict.actions;
|
|
39
|
+
const decorations = leveldict.decorations || [];
|
|
40
|
+
|
|
41
|
+
this.nonFloorDecos = decorations.filter((j: Decoration) => !('floor' in j)).map((j: Decoration) => ({ ...j }));
|
|
42
|
+
this.settings = filename !== '' ? { ...leveldict.settings } : {};
|
|
43
|
+
|
|
44
|
+
// Initialize tiles
|
|
45
|
+
this.tiles = angleData.map(angle => ({
|
|
46
|
+
angle,
|
|
47
|
+
actions: [],
|
|
48
|
+
decorations: []
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Add actions
|
|
52
|
+
actions.forEach((action: Action) => {
|
|
53
|
+
if (action.floor !== undefined && action.floor < this.tiles.length) {
|
|
54
|
+
this.tiles[action.floor].actions.push({ ...action });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Add decorations
|
|
59
|
+
decorations.forEach((deco: Decoration) => {
|
|
60
|
+
if ('floor' in deco) {
|
|
61
|
+
if (deco.floor! >= this.tiles.length) {
|
|
62
|
+
this.tiles[this.tiles.length - 1].decorations.push({ ...deco });
|
|
63
|
+
} else {
|
|
64
|
+
this.tiles[deco.floor!].decorations.push({ ...deco });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private getFileString(): string {
|
|
71
|
+
if (!this.filename) return '';
|
|
72
|
+
return fs.readFileSync(this.filename, { encoding: this.encoding });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private getFileDict(): any {
|
|
76
|
+
if (!this.filename) {
|
|
77
|
+
return {
|
|
78
|
+
angleData: [0],
|
|
79
|
+
settings: {},
|
|
80
|
+
actions: [],
|
|
81
|
+
decorations: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = this.getFileString();
|
|
86
|
+
return JSON.parse(content);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public getAngles(): number[] {
|
|
90
|
+
return this.tiles.map(tile => tile.angle);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public setAngles(angles: number[]): void {
|
|
94
|
+
if (angles.length > this.tiles.length) {
|
|
95
|
+
this.tiles.push({
|
|
96
|
+
angle: angles[angles.length - 1],
|
|
97
|
+
actions: [],
|
|
98
|
+
decorations: []
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
this.tiles = this.tiles.slice(0, angles.length);
|
|
102
|
+
this.tiles.forEach((tile, i) => {
|
|
103
|
+
tile.angle = angles[i];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public getAnglesRelative(ignoreTwirls: boolean = false, padMidspins: boolean = false): number[] {
|
|
108
|
+
const absangles = this.getAngles().slice(0, -1);
|
|
109
|
+
|
|
110
|
+
if (!ignoreTwirls) {
|
|
111
|
+
const twirls = this.getActions(action => action.eventType === 'Twirl')
|
|
112
|
+
.map(event => event.floor!);
|
|
113
|
+
|
|
114
|
+
for (const twirl of twirls.reverse()) {
|
|
115
|
+
absangles.splice(twirl, absangles.length - twirl,
|
|
116
|
+
...absangles.slice(twirl).map(angle =>
|
|
117
|
+
angle !== 999 ? (2 * absangles[twirl - 1] - angle) % 360 : 999
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const midspins = absangles.map((angle, idx) => angle === 999 ? idx : -1).filter(idx => idx !== -1);
|
|
124
|
+
for (const midspin of midspins.reverse()) {
|
|
125
|
+
absangles.splice(midspin + 1, absangles.length - (midspin + 1),
|
|
126
|
+
...absangles.slice(midspin + 1).map(angle =>
|
|
127
|
+
angle !== 999 ? (angle + 180) % 360 : 999
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!padMidspins) {
|
|
133
|
+
absangles.splice(0, absangles.length, ...absangles.filter(angle => angle !== 999));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return absangles.map((angle, idx) => {
|
|
137
|
+
if (angle === 999) return 0;
|
|
138
|
+
if (idx === 0) return ((0 - angle + 180 - 1) % 360) + 1;
|
|
139
|
+
if (absangles[idx - 1] === 999) {
|
|
140
|
+
return ((absangles[idx - 2] - angle + 180 - 1) % 360) + 1;
|
|
141
|
+
}
|
|
142
|
+
return ((absangles[idx - 1] - angle + 180 - 1) % 360) + 1;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public getActions(condition: (action: Action) => boolean = () => true): Action[] {
|
|
147
|
+
return this.tiles.flatMap(tile => tile.actions.filter(condition));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public getDecorations(condition: (decoration: Decoration) => boolean = () => true): Decoration[] {
|
|
151
|
+
return [
|
|
152
|
+
...this.tiles.flatMap(tile => tile.decorations.filter(condition)),
|
|
153
|
+
...this.nonFloorDecos.filter(condition)
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public writeToFile(filename?: string): void {
|
|
158
|
+
const final = {
|
|
159
|
+
angleData: this.tiles.map(tile => tile.angle),
|
|
160
|
+
settings: { ...this.settings },
|
|
161
|
+
actions: this.tiles.flatMap(tile => tile.actions),
|
|
162
|
+
decorations: [
|
|
163
|
+
...this.tiles.flatMap(tile => tile.decorations),
|
|
164
|
+
...this.nonFloorDecos
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const outputFile = filename || this.filename;
|
|
169
|
+
fs.writeFileSync(outputFile, JSON.stringify(final, null, 4), { encoding: this.encoding });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LevelFile } from '../LevelDict';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
// Mock fs module
|
|
6
|
+
jest.mock('fs');
|
|
7
|
+
|
|
8
|
+
describe('LevelFile', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should create an empty level file when no filename is provided', () => {
|
|
14
|
+
const level = new LevelFile();
|
|
15
|
+
expect(level.getAngles()).toEqual([0,0]);
|
|
16
|
+
expect(level.getActions()).toEqual([]);
|
|
17
|
+
expect(level.getDecorations()).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should throw InvalidLevelDictException for invalid file', () => {
|
|
21
|
+
(fs.readFileSync as jest.Mock).mockReturnValue('{}');
|
|
22
|
+
|
|
23
|
+
expect(() => {
|
|
24
|
+
new LevelFile('invalid.adofai');
|
|
25
|
+
}).toThrow('The provided .adofai file is invalid');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should load a valid level file', () => {
|
|
29
|
+
const mockLevelData = {
|
|
30
|
+
angleData: [0, 90, 180],
|
|
31
|
+
settings: { bpm: 120 },
|
|
32
|
+
actions: [],
|
|
33
|
+
decorations: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
37
|
+
|
|
38
|
+
const level = new LevelFile('test.adofai');
|
|
39
|
+
expect(level.getAngles()).toEqual([0, 90, 180, 180]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should write level file correctly', () => {
|
|
43
|
+
const level = new LevelFile();
|
|
44
|
+
level.setAngles([0, 90, 180]);
|
|
45
|
+
|
|
46
|
+
level.writeToFile('output.adofai');
|
|
47
|
+
|
|
48
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
49
|
+
'output.adofai',
|
|
50
|
+
expect.stringMatching(/"angleData"\s*:\s*\[\s*0\s*,\s*90\s*,\s*180\s*\]/),
|
|
51
|
+
{ encoding: 'utf-8' }
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle path-based level data', () => {
|
|
56
|
+
const mockLevelData = {
|
|
57
|
+
pathData: 'RUL',
|
|
58
|
+
settings: { bpm: 120 },
|
|
59
|
+
actions: [],
|
|
60
|
+
decorations: []
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
64
|
+
|
|
65
|
+
const level = new LevelFile('test.adofai');
|
|
66
|
+
expect(level.getAngles()).toEqual([0, 90, 180, 180]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should get relative angles correctly', () => {
|
|
70
|
+
const mockLevelData = {
|
|
71
|
+
angleData: [0, 90, 180, 180],
|
|
72
|
+
settings: { bpm: 120 },
|
|
73
|
+
actions: [],
|
|
74
|
+
decorations: []
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
78
|
+
|
|
79
|
+
const level = new LevelFile('test.adofai');
|
|
80
|
+
const relativeAngles = level.getAnglesRelative();
|
|
81
|
+
expect(relativeAngles).toEqual([180, 90, 90, 180]);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface Action {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
floor?: number;
|
|
4
|
+
eventType?: string;
|
|
5
|
+
angleOffset?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Decoration {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
floor?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Settings {
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Tile {
|
|
18
|
+
angle: number;
|
|
19
|
+
actions: Action[];
|
|
20
|
+
decorations: Decoration[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class InvalidLevelDictException extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'InvalidLevelDictException';
|
|
27
|
+
}
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2019",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"lib": ["es2019", "dom"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
16
|
+
}
|