adofai-lib 1.0.0 → 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/README.md +1 -0
- 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/dist/types/level.d.ts +17 -0
- 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 +11 -3
- package/jest.config.js +0 -6
- package/src/LevelDict.ts +0 -171
- package/src/__tests__/LevelFile.test.ts +0 -83
- package/src/index.ts +0 -2
- package/src/types/index.ts +0 -28
- package/tsconfig.json +0 -16
package/README.md
CHANGED
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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.
|
|
4
|
-
"description": "A TypeScript library for working with ADOFAI level files",
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -15,7 +20,7 @@
|
|
|
15
20
|
"adofai-lib-js",
|
|
16
21
|
"adofai-lib-js-v0w4n"
|
|
17
22
|
],
|
|
18
|
-
"author": "V0W4N
|
|
23
|
+
"author": "V0W4N",
|
|
19
24
|
"license": "MIT",
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"@types/jest": "^29.5.12",
|
|
@@ -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,171 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
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
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/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|