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 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
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/__tests__/**/*.test.ts'],
5
+ moduleFileExtensions: ['ts', 'js'],
6
+ };
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
+ }
@@ -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,2 @@
1
+ export * from './types';
2
+ export * from './LevelDict';
@@ -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
+ }