adofai-lib 1.0.1 → 1.0.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/dist/Level.d.ts +17 -0
- package/dist/Level.js +175 -0
- package/dist/LevelDict.d.ts +17 -0
- package/dist/LevelDict.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -18
- package/dist/types/index.d.ts +38 -0
- package/dist/types/index.js +6 -0
- package/{src/types/level.ts → dist/types/level.d.ts} +17 -19
- package/dist/types/level.js +1 -0
- package/dist/utils/levelUtils.d.ts +5 -0
- package/dist/utils/levelUtils.js +16 -0
- package/package.json +9 -1
- package/jest.config.js +0 -6
- package/src/LevelDict.ts +0 -165
- package/src/__tests__/LevelFile.test.ts +0 -94
- package/src/index.ts +0 -2
- package/src/types/index.ts +0 -28
- package/src/utils/levelUtils.ts +0 -18
- package/tsconfig.json +0 -18
package/dist/Level.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Action, Decoration } from './types';
|
|
2
|
+
export declare class Level {
|
|
3
|
+
private filename;
|
|
4
|
+
private encoding;
|
|
5
|
+
private tiles;
|
|
6
|
+
private nonFloorDecos;
|
|
7
|
+
private settings;
|
|
8
|
+
constructor(filename?: string, encoding?: BufferEncoding);
|
|
9
|
+
private getFileString;
|
|
10
|
+
private getFileDict;
|
|
11
|
+
getAngles(): number[];
|
|
12
|
+
setAngles(angles: number[]): void;
|
|
13
|
+
getAnglesRelative(ignoreTwirls?: boolean, padMidspins?: boolean): number[];
|
|
14
|
+
getActions(condition?: (action: Action) => boolean): Action[];
|
|
15
|
+
getDecorations(condition?: (decoration: Decoration) => boolean): Decoration[];
|
|
16
|
+
writeToFile(filename?: string): void;
|
|
17
|
+
}
|
package/dist/Level.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Level = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const types_1 = require("./types");
|
|
39
|
+
class Level {
|
|
40
|
+
constructor(filename = '', encoding = 'utf-8') {
|
|
41
|
+
this.filename = filename;
|
|
42
|
+
this.encoding = encoding;
|
|
43
|
+
this.tiles = [];
|
|
44
|
+
this.nonFloorDecos = [];
|
|
45
|
+
this.settings = {};
|
|
46
|
+
const leveldict = this.getFileDict();
|
|
47
|
+
if (!leveldict.actions || !leveldict.settings || (!leveldict.angleData && !leveldict.pathData)) {
|
|
48
|
+
throw new types_1.InvalidLevelException(`The provided .adofai file is invalid. (missing: ${!leveldict.actions ? 'actions' : ''} ${!leveldict.settings ? 'settings' : ''} ${!leveldict.angleData && !leveldict.pathData ? 'angleData or pathData' : ''})`);
|
|
49
|
+
}
|
|
50
|
+
let angleData;
|
|
51
|
+
if ('angleData' in leveldict) {
|
|
52
|
+
angleData = leveldict.angleData;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const pathchars = {
|
|
56
|
+
'R': 0, 'p': 15, 'J': 30, 'E': 45, 'T': 60, 'o': 75, 'U': 90,
|
|
57
|
+
'q': 105, 'G': 120, 'Q': 135, 'H': 150, 'W': 165, 'L': 180,
|
|
58
|
+
'x': 195, 'N': 210, 'Z': 225, 'F': 240, 'V': 255, 'D': 270,
|
|
59
|
+
'Y': 285, 'B': 300, 'C': 315, 'M': 330, 'A': 345, '!': 999
|
|
60
|
+
};
|
|
61
|
+
angleData = Array.from(leveldict.pathData).map(char => pathchars[char]);
|
|
62
|
+
}
|
|
63
|
+
angleData.push(angleData[angleData.length - 1] !== 999 ? angleData[angleData.length - 1] : (angleData[angleData.length - 2] + 180) % 360);
|
|
64
|
+
const actions = leveldict.actions;
|
|
65
|
+
const decorations = leveldict.decorations || [];
|
|
66
|
+
this.nonFloorDecos = decorations.filter((j) => !('floor' in j)).map((j) => ({ ...j }));
|
|
67
|
+
this.settings = filename !== '' ? { ...leveldict.settings } : {};
|
|
68
|
+
// Initialize tiles
|
|
69
|
+
this.tiles = angleData.map(angle => ({
|
|
70
|
+
angle,
|
|
71
|
+
actions: [],
|
|
72
|
+
decorations: []
|
|
73
|
+
}));
|
|
74
|
+
// Add actions
|
|
75
|
+
actions.forEach((action) => {
|
|
76
|
+
if (action.floor !== undefined && action.floor < this.tiles.length) {
|
|
77
|
+
this.tiles[action.floor].actions.push({ ...action });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// Add decorations
|
|
81
|
+
decorations.forEach((deco) => {
|
|
82
|
+
if ('floor' in deco) {
|
|
83
|
+
if (deco.floor >= this.tiles.length) {
|
|
84
|
+
this.tiles[this.tiles.length - 1].decorations.push({ ...deco });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.tiles[deco.floor].decorations.push({ ...deco });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
getFileString() {
|
|
93
|
+
if (!this.filename)
|
|
94
|
+
return '';
|
|
95
|
+
return fs.readFileSync(this.filename, { encoding: this.encoding });
|
|
96
|
+
}
|
|
97
|
+
getFileDict() {
|
|
98
|
+
if (!this.filename) {
|
|
99
|
+
return {
|
|
100
|
+
angleData: [0],
|
|
101
|
+
settings: {},
|
|
102
|
+
actions: [],
|
|
103
|
+
decorations: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const content = this.getFileString();
|
|
107
|
+
return JSON.parse(content);
|
|
108
|
+
}
|
|
109
|
+
getAngles() {
|
|
110
|
+
return this.tiles.map(tile => tile.angle);
|
|
111
|
+
}
|
|
112
|
+
setAngles(angles) {
|
|
113
|
+
if (angles.length > this.tiles.length) {
|
|
114
|
+
this.tiles.push({
|
|
115
|
+
angle: angles[angles.length - 1],
|
|
116
|
+
actions: [],
|
|
117
|
+
decorations: []
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
this.tiles = this.tiles.slice(0, angles.length);
|
|
121
|
+
this.tiles.forEach((tile, i) => {
|
|
122
|
+
tile.angle = angles[i];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
getAnglesRelative(ignoreTwirls = false, padMidspins = false) {
|
|
126
|
+
const absangles = this.getAngles().slice(0, -1);
|
|
127
|
+
if (!ignoreTwirls) {
|
|
128
|
+
const twirls = this.getActions(action => action.eventType === 'Twirl')
|
|
129
|
+
.map(event => event.floor);
|
|
130
|
+
for (const twirl of twirls.reverse()) {
|
|
131
|
+
absangles.splice(twirl, absangles.length - twirl, ...absangles.slice(twirl).map(angle => angle !== 999 ? (2 * absangles[twirl - 1] - angle) % 360 : 999));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const midspins = absangles.map((angle, idx) => angle === 999 ? idx : -1).filter(idx => idx !== -1);
|
|
135
|
+
for (const midspin of midspins.reverse()) {
|
|
136
|
+
absangles.splice(midspin + 1, absangles.length - (midspin + 1), ...absangles.slice(midspin + 1).map(angle => angle !== 999 ? (angle + 180) % 360 : 999));
|
|
137
|
+
}
|
|
138
|
+
if (!padMidspins) {
|
|
139
|
+
absangles.splice(0, absangles.length, ...absangles.filter(angle => angle !== 999));
|
|
140
|
+
}
|
|
141
|
+
return absangles.map((angle, idx) => {
|
|
142
|
+
if (angle === 999)
|
|
143
|
+
return 0;
|
|
144
|
+
if (idx === 0)
|
|
145
|
+
return ((0 - angle + 180 - 1) % 360) + 1;
|
|
146
|
+
if (absangles[idx - 1] === 999) {
|
|
147
|
+
return ((absangles[idx - 2] - angle + 180 - 1) % 360) + 1;
|
|
148
|
+
}
|
|
149
|
+
return ((absangles[idx - 1] - angle + 180 - 1) % 360) + 1;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getActions(condition = () => true) {
|
|
153
|
+
return this.tiles.flatMap(tile => tile.actions.filter(condition));
|
|
154
|
+
}
|
|
155
|
+
getDecorations(condition = () => true) {
|
|
156
|
+
return [
|
|
157
|
+
...this.tiles.flatMap(tile => tile.decorations.filter(condition)),
|
|
158
|
+
...this.nonFloorDecos.filter(condition)
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
writeToFile(filename) {
|
|
162
|
+
const final = {
|
|
163
|
+
angleData: this.tiles.map(tile => tile.angle),
|
|
164
|
+
settings: { ...this.settings },
|
|
165
|
+
actions: this.tiles.flatMap(tile => tile.actions),
|
|
166
|
+
decorations: [
|
|
167
|
+
...this.tiles.flatMap(tile => tile.decorations),
|
|
168
|
+
...this.nonFloorDecos
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
const outputFile = filename || this.filename;
|
|
172
|
+
fs.writeFileSync(outputFile, JSON.stringify(final, null, 4), { encoding: this.encoding });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.Level = Level;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Action, Decoration } from './types';
|
|
2
|
+
export declare class LevelDict {
|
|
3
|
+
private filename;
|
|
4
|
+
private encoding;
|
|
5
|
+
private tiles;
|
|
6
|
+
private nonFloorDecos;
|
|
7
|
+
private settings;
|
|
8
|
+
constructor(filename?: string, encoding?: BufferEncoding);
|
|
9
|
+
private getFileString;
|
|
10
|
+
private getFileDict;
|
|
11
|
+
getAngles(): number[];
|
|
12
|
+
setAngles(angles: number[]): void;
|
|
13
|
+
getAnglesRelative(ignoreTwirls?: boolean, padMidspins?: boolean): number[];
|
|
14
|
+
getActions(condition?: (action: Action) => boolean): Action[];
|
|
15
|
+
getDecorations(condition?: (decoration: Decoration) => boolean): Decoration[];
|
|
16
|
+
writeToFile(filename?: string): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as utils from './utils/levelUtils';
|
|
3
|
+
import { InvalidLevelDictException } from './types';
|
|
4
|
+
export class LevelDict {
|
|
5
|
+
constructor(filename = '', encoding = 'utf-8') {
|
|
6
|
+
this.filename = filename;
|
|
7
|
+
this.encoding = encoding;
|
|
8
|
+
this.tiles = [];
|
|
9
|
+
this.nonFloorDecos = [];
|
|
10
|
+
this.settings = {};
|
|
11
|
+
const leveldict = this.getFileDict(filename);
|
|
12
|
+
if (!leveldict.actions || !leveldict.settings || (!leveldict.angleData && !leveldict.pathData)) {
|
|
13
|
+
throw new InvalidLevelDictException(`The provided .adofai file is invalid. (missing: ${!leveldict.actions ? 'actions' : ''} ${!leveldict.settings ? 'settings' : ''} ${!leveldict.angleData && !leveldict.pathData ? 'angleData or pathData' : ''})`);
|
|
14
|
+
}
|
|
15
|
+
let angleData;
|
|
16
|
+
if ('angleData' in leveldict) {
|
|
17
|
+
angleData = leveldict.angleData;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const pathchars = {
|
|
21
|
+
'R': 0, 'p': 15, 'J': 30, 'E': 45, 'T': 60, 'o': 75, 'U': 90,
|
|
22
|
+
'q': 105, 'G': 120, 'Q': 135, 'H': 150, 'W': 165, 'L': 180,
|
|
23
|
+
'x': 195, 'N': 210, 'Z': 225, 'F': 240, 'V': 255, 'D': 270,
|
|
24
|
+
'Y': 285, 'B': 300, 'C': 315, 'M': 330, 'A': 345, '!': 999
|
|
25
|
+
};
|
|
26
|
+
angleData = Array.from(leveldict.pathData).map(char => pathchars[char]);
|
|
27
|
+
}
|
|
28
|
+
const actions = leveldict.actions;
|
|
29
|
+
const decorations = leveldict.decorations || [];
|
|
30
|
+
this.nonFloorDecos = decorations.filter((j) => !('floor' in j)).map((j) => ({ ...j }));
|
|
31
|
+
this.settings = filename !== '' ? { ...leveldict.settings } : {};
|
|
32
|
+
// Initialize tiles
|
|
33
|
+
this.tiles = angleData.map(angle => ({
|
|
34
|
+
angle,
|
|
35
|
+
actions: [],
|
|
36
|
+
decorations: []
|
|
37
|
+
}));
|
|
38
|
+
console.log('mapping tiles', angleData, this.tiles);
|
|
39
|
+
// Add actions
|
|
40
|
+
actions.forEach((action) => {
|
|
41
|
+
if (action.floor !== undefined && action.floor < this.tiles.length) {
|
|
42
|
+
this.tiles[action.floor].actions.push({ ...action });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Add decorations
|
|
46
|
+
decorations.forEach((deco) => {
|
|
47
|
+
if ('floor' in deco) {
|
|
48
|
+
if (deco.floor >= this.tiles.length) {
|
|
49
|
+
this.tiles[this.tiles.length - 1].decorations.push({ ...deco });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.tiles[deco.floor].decorations.push({ ...deco });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getFileString(filename) {
|
|
58
|
+
if (!filename)
|
|
59
|
+
return '';
|
|
60
|
+
return fs.readFileSync(filename, { encoding: this.encoding });
|
|
61
|
+
}
|
|
62
|
+
getFileDict(filename) {
|
|
63
|
+
if (!filename) {
|
|
64
|
+
return {
|
|
65
|
+
angleData: [0],
|
|
66
|
+
settings: {},
|
|
67
|
+
actions: [],
|
|
68
|
+
decorations: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const content = this.getFileString(filename);
|
|
72
|
+
return utils.safeParseJSON(content);
|
|
73
|
+
}
|
|
74
|
+
getAngles() {
|
|
75
|
+
return this.tiles.map(tile => tile.angle);
|
|
76
|
+
}
|
|
77
|
+
setAngles(angles) {
|
|
78
|
+
while (angles.length > this.tiles.length) {
|
|
79
|
+
this.tiles.push({
|
|
80
|
+
angle: angles[this.tiles.length],
|
|
81
|
+
actions: [],
|
|
82
|
+
decorations: []
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
this.tiles = this.tiles.slice(0, angles.length);
|
|
86
|
+
this.tiles.forEach((tile, i) => {
|
|
87
|
+
tile.angle = angles[i];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
getAnglesRelative(ignoreTwirls = false, padMidspins = false) {
|
|
91
|
+
const absangles = this.getAngles();
|
|
92
|
+
if (!ignoreTwirls) {
|
|
93
|
+
const twirls = this.getActions(action => action.eventType === 'Twirl')
|
|
94
|
+
.map(event => event.floor);
|
|
95
|
+
for (const twirl of twirls.reverse()) {
|
|
96
|
+
absangles.splice(twirl, absangles.length - twirl, ...absangles.slice(twirl).map(angle => angle !== 999 ? (2 * absangles[twirl - 1] - angle) % 360 : 999));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const midspins = absangles.map((angle, idx) => angle === 999 ? idx : -1).filter(idx => idx !== -1);
|
|
100
|
+
for (const midspin of midspins.reverse()) {
|
|
101
|
+
absangles.splice(midspin + 1, absangles.length - (midspin + 1), ...absangles.slice(midspin + 1).map(angle => angle !== 999 ? (angle + 180) % 360 : 999));
|
|
102
|
+
}
|
|
103
|
+
if (!padMidspins) {
|
|
104
|
+
absangles.splice(0, absangles.length, ...absangles.filter(angle => angle !== 999));
|
|
105
|
+
}
|
|
106
|
+
return absangles.map((angle, idx) => {
|
|
107
|
+
if (angle === 999)
|
|
108
|
+
return 0;
|
|
109
|
+
if (idx === 0)
|
|
110
|
+
return ((0 - angle + 180 - 1) % 360) + 1;
|
|
111
|
+
if (absangles[idx - 1] === 999) {
|
|
112
|
+
return ((absangles[idx - 2] - angle + 180 - 1) % 360) + 1;
|
|
113
|
+
}
|
|
114
|
+
return ((absangles[idx - 1] - angle + 180 - 1) % 360) + 1;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
getActions(condition = () => true) {
|
|
118
|
+
return this.tiles.flatMap(tile => tile.actions.filter(condition));
|
|
119
|
+
}
|
|
120
|
+
getDecorations(condition = () => true) {
|
|
121
|
+
return this.tiles.flatMap(tile => tile.decorations.filter(condition))
|
|
122
|
+
.concat(this.nonFloorDecos.filter(condition));
|
|
123
|
+
}
|
|
124
|
+
writeToFile(filename) {
|
|
125
|
+
const final = {
|
|
126
|
+
angleData: this.getAngles(),
|
|
127
|
+
settings: { ...this.settings },
|
|
128
|
+
actions: this.getActions(),
|
|
129
|
+
decorations: this.getDecorations()
|
|
130
|
+
};
|
|
131
|
+
console.log(final);
|
|
132
|
+
const outputFile = filename || this.filename;
|
|
133
|
+
fs.writeFileSync(outputFile, JSON.stringify(final, null, 4), { encoding: this.encoding });
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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);
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './LevelDict';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface Action {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
floor?: number;
|
|
4
|
+
eventType?: string;
|
|
5
|
+
angleOffset?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface Decoration {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
floor?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Settings {
|
|
12
|
+
requiredMods?: string[];
|
|
13
|
+
songFilename?: string;
|
|
14
|
+
artist?: string;
|
|
15
|
+
song?: string;
|
|
16
|
+
author?: string;
|
|
17
|
+
difficulty?: string;
|
|
18
|
+
bpm?: string;
|
|
19
|
+
backgroundColor?: string;
|
|
20
|
+
bgImage?: string;
|
|
21
|
+
showDefaultBGIfNoImage?: string;
|
|
22
|
+
zoom?: number;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
export interface Tile {
|
|
26
|
+
angle: number;
|
|
27
|
+
actions: Action[];
|
|
28
|
+
decorations: Decoration[];
|
|
29
|
+
}
|
|
30
|
+
export interface LevelJSON {
|
|
31
|
+
actions?: Array<Record<string, any>>;
|
|
32
|
+
decorations?: Array<Record<string, any>>;
|
|
33
|
+
settings?: Settings;
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
36
|
+
export declare class InvalidLevelDictException extends Error {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { Settings } from ".";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
dropFilters?: Set<string>;
|
|
19
|
-
}
|
|
1
|
+
import { Settings } from ".";
|
|
2
|
+
export interface LevelJSON {
|
|
3
|
+
actions?: Array<Record<string, any>>;
|
|
4
|
+
decorations?: Array<Record<string, any>>;
|
|
5
|
+
settings?: Settings;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
export interface TransformOptions {
|
|
9
|
+
keepEventTypes?: Set<string>;
|
|
10
|
+
dropEventTypes?: Set<string>;
|
|
11
|
+
baseCameraZoom?: number;
|
|
12
|
+
extraProtectedEventTypes?: Set<string>;
|
|
13
|
+
additionalPatterns?: Set<RegExp>;
|
|
14
|
+
constantBackgroundColor?: string;
|
|
15
|
+
removeForegroundFlash?: boolean;
|
|
16
|
+
dropFilters?: Set<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import JSON5 from 'json5';
|
|
2
|
+
/**
|
|
3
|
+
* Ensure bad comma handling
|
|
4
|
+
*/
|
|
5
|
+
export function safeParseJSON(str) {
|
|
6
|
+
try {
|
|
7
|
+
const cleaned = str
|
|
8
|
+
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas before closing brackets
|
|
9
|
+
.replace(/([}\]])[\s\n]*([}\]])/g, '$1,$2') // Ensure comma between closing brackets
|
|
10
|
+
.replace(/([}\]])[\s\n]*("decorations"|"actions"|"settings")/g, '$1,$2'); // Ensure comma before main sections
|
|
11
|
+
return JSON5.parse(cleaned);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
throw new Error('Failed to parse level JSON');
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adofai-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A TypeScript library for working with ADOFAI level files. Original by M1n3c4rt",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
7
12
|
"scripts": {
|
|
8
13
|
"build": "tsc",
|
|
9
14
|
"test": "jest",
|
|
@@ -23,5 +28,8 @@
|
|
|
23
28
|
"jest": "^29.7.0",
|
|
24
29
|
"ts-jest": "^29.1.2",
|
|
25
30
|
"typescript": "^5.3.3"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"json5": "^2.2.3"
|
|
26
34
|
}
|
|
27
35
|
}
|
package/jest.config.js
DELETED
package/src/LevelDict.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as utils from './utils/levelUtils';
|
|
3
|
-
import { Action, Decoration, Settings, Tile, InvalidLevelDictException } from './types';
|
|
4
|
-
import { LevelJSON } from './types/level';
|
|
5
|
-
|
|
6
|
-
export class LevelDict {
|
|
7
|
-
private filename: string;
|
|
8
|
-
private encoding: BufferEncoding;
|
|
9
|
-
private tiles: Tile[];
|
|
10
|
-
private nonFloorDecos: Decoration[];
|
|
11
|
-
private settings: Settings;
|
|
12
|
-
|
|
13
|
-
constructor(filename: string = '', encoding: BufferEncoding = 'utf-8') {
|
|
14
|
-
this.filename = filename;
|
|
15
|
-
this.encoding = encoding;
|
|
16
|
-
this.tiles = [];
|
|
17
|
-
this.nonFloorDecos = [];
|
|
18
|
-
this.settings = {};
|
|
19
|
-
|
|
20
|
-
const leveldict = this.getFileDict(filename);
|
|
21
|
-
if (!leveldict.actions || !leveldict.settings || (!leveldict.angleData && !leveldict.pathData)) {
|
|
22
|
-
throw new InvalidLevelDictException(`The provided .adofai file is invalid. (missing: ${!leveldict.actions ? 'actions' : ''} ${!leveldict.settings ? 'settings' : ''} ${!leveldict.angleData && !leveldict.pathData ? 'angleData or pathData' : ''})`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let angleData: number[];
|
|
26
|
-
if ('angleData' in leveldict) {
|
|
27
|
-
angleData = leveldict.angleData;
|
|
28
|
-
} else {
|
|
29
|
-
const pathchars: { [key: string]: number } = {
|
|
30
|
-
'R': 0, 'p': 15, 'J': 30, 'E': 45, 'T': 60, 'o': 75, 'U': 90,
|
|
31
|
-
'q': 105, 'G': 120, 'Q': 135, 'H': 150, 'W': 165, 'L': 180,
|
|
32
|
-
'x': 195, 'N': 210, 'Z': 225, 'F': 240, 'V': 255, 'D': 270,
|
|
33
|
-
'Y': 285, 'B': 300, 'C': 315, 'M': 330, 'A': 345, '!': 999
|
|
34
|
-
};
|
|
35
|
-
angleData = Array.from(leveldict.pathData as string).map(char => pathchars[char]);
|
|
36
|
-
}
|
|
37
|
-
const actions = leveldict.actions;
|
|
38
|
-
const decorations = leveldict.decorations || [];
|
|
39
|
-
|
|
40
|
-
this.nonFloorDecos = decorations.filter((j: Decoration) => !('floor' in j)).map((j: Decoration) => ({ ...j }));
|
|
41
|
-
this.settings = filename !== '' ? { ...leveldict.settings } : {};
|
|
42
|
-
|
|
43
|
-
// Initialize tiles
|
|
44
|
-
this.tiles = angleData.map(angle => ({
|
|
45
|
-
angle,
|
|
46
|
-
actions: [],
|
|
47
|
-
decorations: []
|
|
48
|
-
}));
|
|
49
|
-
console.log('mapping tiles', angleData, this.tiles);
|
|
50
|
-
// Add actions
|
|
51
|
-
actions.forEach((action: Action) => {
|
|
52
|
-
if (action.floor !== undefined && action.floor < this.tiles.length) {
|
|
53
|
-
this.tiles[action.floor].actions.push({ ...action });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Add decorations
|
|
58
|
-
decorations.forEach((deco: Decoration) => {
|
|
59
|
-
if ('floor' in deco) {
|
|
60
|
-
if (deco.floor! >= this.tiles.length) {
|
|
61
|
-
this.tiles[this.tiles.length - 1].decorations.push({ ...deco });
|
|
62
|
-
} else {
|
|
63
|
-
this.tiles[deco.floor!].decorations.push({ ...deco });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private getFileString(filename: string): string {
|
|
70
|
-
if (!filename) return '';
|
|
71
|
-
return fs.readFileSync(filename, { encoding: this.encoding });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private getFileDict(filename: string): LevelJSON {
|
|
75
|
-
if (!filename) {
|
|
76
|
-
return {
|
|
77
|
-
angleData: [0],
|
|
78
|
-
settings: {},
|
|
79
|
-
actions: [],
|
|
80
|
-
decorations: []
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const content = this.getFileString(filename);
|
|
85
|
-
return utils.safeParseJSON(content);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public getAngles(): number[] {
|
|
89
|
-
return this.tiles.map(tile => tile.angle);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
public setAngles(angles: number[]): void {
|
|
93
|
-
while (angles.length > this.tiles.length) {
|
|
94
|
-
this.tiles.push({
|
|
95
|
-
angle: angles[this.tiles.length],
|
|
96
|
-
actions: [],
|
|
97
|
-
decorations: []
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
this.tiles = this.tiles.slice(0, angles.length);
|
|
101
|
-
this.tiles.forEach((tile, i) => {
|
|
102
|
-
tile.angle = angles[i];
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public getAnglesRelative(ignoreTwirls: boolean = false, padMidspins: boolean = false): number[] {
|
|
107
|
-
const absangles = this.getAngles();
|
|
108
|
-
|
|
109
|
-
if (!ignoreTwirls) {
|
|
110
|
-
const twirls = this.getActions(action => action.eventType === 'Twirl')
|
|
111
|
-
.map(event => event.floor!);
|
|
112
|
-
|
|
113
|
-
for (const twirl of twirls.reverse()) {
|
|
114
|
-
absangles.splice(twirl, absangles.length - twirl,
|
|
115
|
-
...absangles.slice(twirl).map(angle =>
|
|
116
|
-
angle !== 999 ? (2 * absangles[twirl - 1] - angle) % 360 : 999
|
|
117
|
-
)
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const midspins = absangles.map((angle, idx) => angle === 999 ? idx : -1).filter(idx => idx !== -1);
|
|
123
|
-
for (const midspin of midspins.reverse()) {
|
|
124
|
-
absangles.splice(midspin + 1, absangles.length - (midspin + 1),
|
|
125
|
-
...absangles.slice(midspin + 1).map(angle =>
|
|
126
|
-
angle !== 999 ? (angle + 180) % 360 : 999
|
|
127
|
-
)
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!padMidspins) {
|
|
132
|
-
absangles.splice(0, absangles.length, ...absangles.filter(angle => angle !== 999));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return absangles.map((angle, idx) => {
|
|
136
|
-
if (angle === 999) return 0;
|
|
137
|
-
if (idx === 0) return ((0 - angle + 180 - 1) % 360) + 1;
|
|
138
|
-
if (absangles[idx - 1] === 999) {
|
|
139
|
-
return ((absangles[idx - 2] - angle + 180 - 1) % 360) + 1;
|
|
140
|
-
}
|
|
141
|
-
return ((absangles[idx - 1] - angle + 180 - 1) % 360) + 1;
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
public getActions(condition: (action: Action) => boolean = () => true): Action[] {
|
|
146
|
-
return this.tiles.flatMap(tile => tile.actions.filter(condition));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
public getDecorations(condition: (decoration: Decoration) => boolean = () => true): Decoration[] {
|
|
150
|
-
return this.tiles.flatMap(tile => tile.decorations.filter(condition))
|
|
151
|
-
.concat(this.nonFloorDecos.filter(condition));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public writeToFile(filename?: string): void {
|
|
155
|
-
const final = {
|
|
156
|
-
angleData: this.getAngles(),
|
|
157
|
-
settings: { ...this.settings },
|
|
158
|
-
actions: this.getActions(),
|
|
159
|
-
decorations: this.getDecorations()
|
|
160
|
-
};
|
|
161
|
-
console.log(final);
|
|
162
|
-
const outputFile = filename || this.filename;
|
|
163
|
-
fs.writeFileSync(outputFile, JSON.stringify(final, null, 4), { encoding: this.encoding });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { LevelDict } from '../LevelDict';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
|
|
4
|
-
// Mock fs module
|
|
5
|
-
jest.mock('fs');
|
|
6
|
-
|
|
7
|
-
describe('LevelDict', () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
jest.clearAllMocks();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should create an empty level file when no filename is provided', () => {
|
|
13
|
-
const level = new LevelDict();
|
|
14
|
-
expect(level.getAngles()).toEqual([0]);
|
|
15
|
-
expect(level.getActions()).toEqual([]);
|
|
16
|
-
expect(level.getDecorations()).toEqual([]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should throw InvalidLevelException for invalid file', () => {
|
|
20
|
-
(fs.readFileSync as jest.Mock).mockReturnValue('{}');
|
|
21
|
-
|
|
22
|
-
expect(() => {
|
|
23
|
-
new LevelDict('invalid.adofai');
|
|
24
|
-
}).toThrow('The provided .adofai file is invalid');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should load a valid level file', () => {
|
|
28
|
-
const mockLevelData = {
|
|
29
|
-
angleData: [0, 90, 180],
|
|
30
|
-
settings: { bpm: 120 },
|
|
31
|
-
actions: [],
|
|
32
|
-
decorations: []
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
36
|
-
|
|
37
|
-
const level = new LevelDict('test.adofai');
|
|
38
|
-
expect(level.getAngles()).toEqual([0, 90, 180]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should write level file correctly', () => {
|
|
42
|
-
const level = new LevelDict();
|
|
43
|
-
level.setAngles([0, 90, 180]);
|
|
44
|
-
|
|
45
|
-
level.writeToFile('output.adofai');
|
|
46
|
-
|
|
47
|
-
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
48
|
-
'output.adofai',
|
|
49
|
-
expect.stringMatching(/"angleData"\s*:\s*\[\s*0\s*,\s*90\s*,\s*180\s*\]/),
|
|
50
|
-
{ encoding: 'utf-8' }
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should handle path-based level data', () => {
|
|
55
|
-
const mockLevelData = {
|
|
56
|
-
pathData: 'RUL',
|
|
57
|
-
settings: { bpm: 120 },
|
|
58
|
-
actions: [],
|
|
59
|
-
decorations: []
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
63
|
-
|
|
64
|
-
const level = new LevelDict('test.adofai');
|
|
65
|
-
expect(level.getAngles()).toEqual([0, 90, 180]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should get relative angles correctly', () => {
|
|
69
|
-
const mockLevelData = {
|
|
70
|
-
angleData: [0, 90, 180, 180],
|
|
71
|
-
settings: { bpm: 120 },
|
|
72
|
-
actions: [],
|
|
73
|
-
decorations: []
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockLevelData));
|
|
77
|
-
|
|
78
|
-
const level = new LevelDict('test.adofai');
|
|
79
|
-
const relativeAngles = level.getAnglesRelative();
|
|
80
|
-
expect(relativeAngles).toEqual([180, 90, 90, 180]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should parse malformed parameter sequence', () => {
|
|
84
|
-
const mockBadLevelData = `{
|
|
85
|
-
"pathData": "RURU",
|
|
86
|
-
"settings": { "bpm": 120 },
|
|
87
|
-
"actions": []
|
|
88
|
-
"decorations": []
|
|
89
|
-
}`;
|
|
90
|
-
(fs.readFileSync as jest.Mock).mockReturnValue(mockBadLevelData);
|
|
91
|
-
const level = new LevelDict('test.adofai');
|
|
92
|
-
expect(level.getAngles()).toEqual([0, 90, 0, 90]);
|
|
93
|
-
});
|
|
94
|
-
});
|
package/src/index.ts
DELETED
package/src/types/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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/src/utils/levelUtils.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import JSON5 from 'json5';
|
|
2
|
-
import { LevelJSON } from '../types/level';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Ensure bad comma handling
|
|
6
|
-
*/
|
|
7
|
-
export function safeParseJSON(str: string): LevelJSON {
|
|
8
|
-
try {
|
|
9
|
-
const cleaned = str
|
|
10
|
-
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas before closing brackets
|
|
11
|
-
.replace(/([}\]])[\s\n]*([}\]])/g, '$1,$2') // Ensure comma between closing brackets
|
|
12
|
-
.replace(/([}\]])[\s\n]*("decorations"|"actions"|"settings")/g, '$1,$2'); // Ensure comma before main sections
|
|
13
|
-
|
|
14
|
-
return JSON5.parse(cleaned);
|
|
15
|
-
} catch (e) {
|
|
16
|
-
throw new Error('Failed to parse level JSON');
|
|
17
|
-
}
|
|
18
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2019",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"moduleResolution": "node",
|
|
13
|
-
"lib": ["es2019", "dom"],
|
|
14
|
-
"composite": false
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*"],
|
|
17
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
18
|
-
}
|