tja-parser 0.1.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.
@@ -0,0 +1,27 @@
1
+ import * as math from 'mathjs';
2
+
3
+ export class Item {
4
+ private timing: math.Fraction;
5
+
6
+ constructor(timing: math.Fraction) {
7
+ this.timing = timing;
8
+ }
9
+
10
+ setTiming(timing: math.Fraction){
11
+ this.timing = math.fraction(timing);
12
+ }
13
+
14
+ getTiming() {
15
+ return math.fraction(this.timing);
16
+ }
17
+
18
+ getTimingMS() {
19
+ return this.timing.valueOf();
20
+ }
21
+
22
+ toJSON(){
23
+ return {
24
+ timing: this.getTimingMS()
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,172 @@
1
+ import { Item } from "./Item.js";
2
+ import * as math from 'mathjs';
3
+
4
+ export abstract class Note extends Item {
5
+ static parse(char: string): Note | null {
6
+ if (char === "0") {
7
+ return new EmptyNote(Note.Type.EMPTY, math.fraction(0), math.fraction(0));
8
+ }
9
+ else if (char === "1") {
10
+ return new HitNote(Note.Type.DON_SMALL, math.fraction(0), math.fraction(0));
11
+ }
12
+ else if (char === "2") {
13
+ return new HitNote(Note.Type.KA_SMALL, math.fraction(0), math.fraction(0));
14
+ }
15
+ else if (char === "3") {
16
+ return new HitNote(Note.Type.DON_BIG, math.fraction(0), math.fraction(0));
17
+ }
18
+ else if (char === "4") {
19
+ return new HitNote(Note.Type.KA_BIG, math.fraction(0), math.fraction(0));
20
+ }
21
+ else if (char === "5") {
22
+ return new RollNote(Note.Type.ROLL_SMALL, math.fraction(0), math.fraction(0), math.fraction(0));
23
+ }
24
+ else if (char === "6") {
25
+ return new RollNote(Note.Type.ROLL_BIG, math.fraction(0), math.fraction(0), math.fraction(0));
26
+ }
27
+ else if (char === "7") {
28
+ return new BalloonNote(0, math.fraction(0), math.fraction(0), math.fraction(0));
29
+ }
30
+ else if (char === "8") {
31
+ return new RollEndNote(math.fraction(0), math.fraction(0));
32
+ }
33
+ return null;
34
+ }
35
+
36
+ protected type: Note.Type;
37
+ private delay: math.Fraction;
38
+ private scroll: number = 1;
39
+ private bpm: number = 160;
40
+ private noteLength: number = 1;
41
+ constructor(type: Note.Type, timing: math.Fraction, delay: math.Fraction) {
42
+ super(timing);
43
+ this.type = type;
44
+ this.delay = delay;
45
+ }
46
+
47
+ getScroll() {
48
+ return this.scroll;
49
+ }
50
+ setScroll(scroll: number) {
51
+ this.scroll = scroll;
52
+ }
53
+ getBPM() {
54
+ return this.bpm;
55
+ }
56
+ setBpm(bpm: number) {
57
+ this.bpm = bpm;
58
+ }
59
+ getDelay() {
60
+ return math.fraction(this.delay);
61
+ }
62
+ setDelay(delay: math.Fraction) {
63
+ this.delay = math.fraction(delay);
64
+ }
65
+ getNoteLength(){
66
+ return this.noteLength;
67
+ }
68
+ setNoteLength(noteLength: number){
69
+ this.noteLength = noteLength;
70
+ }
71
+
72
+ toJSON(){
73
+ return {
74
+ ...super.toJSON(),
75
+ type: this.type,
76
+ delay: this.delay.valueOf(),
77
+ scroll: this.scroll,
78
+ bpm: this.bpm,
79
+ noteLength: this.noteLength
80
+ }
81
+ }
82
+ }
83
+
84
+ export namespace Note {
85
+ export enum Type {
86
+ EMPTY,
87
+ DON_SMALL,
88
+ KA_SMALL,
89
+ DON_BIG,
90
+ KA_BIG,
91
+ ROLL_SMALL,
92
+ ROLL_BIG,
93
+ BALLOON,
94
+ ROLL_END
95
+ }
96
+
97
+ export type HitType = Type.DON_SMALL | Type.KA_SMALL | Type.DON_BIG | Type.KA_BIG;
98
+ export type RollType = Type.ROLL_SMALL | Type.ROLL_BIG | Type.BALLOON;
99
+ }
100
+
101
+ export class HitNote extends Note {
102
+ declare type: Note.HitType;
103
+ constructor(type: Note.HitType, timing: math.Fraction, delay: math.Fraction) {
104
+ super(type, timing, delay);
105
+ }
106
+ }
107
+
108
+ export class RollNote extends Note {
109
+ declare type: Note.RollType;
110
+ end: math.Fraction;
111
+ constructor(type: Note.RollType, timing: math.Fraction, end: math.Fraction, delay: math.Fraction) {
112
+ super(type, timing, delay);
113
+ this.end = end;
114
+ }
115
+
116
+ getEnd(){
117
+ return math.fraction(this.end);
118
+ }
119
+ setEnd(end: math.Fraction){
120
+ this.end = math.fraction(end);
121
+ }
122
+
123
+ toJSON(){
124
+ return {
125
+ ...super.toJSON(),
126
+ end: this.end.valueOf()
127
+ }
128
+ }
129
+ }
130
+
131
+ export class BalloonNote extends RollNote {
132
+ declare type: Note.Type.BALLOON;
133
+ private count: number;
134
+ constructor(count: number, timing: math.Fraction, end: math.Fraction, delay: math.Fraction) {
135
+ super(Note.Type.BALLOON, timing, end, delay);
136
+ this.count = count;
137
+ }
138
+
139
+ getCount(){
140
+ return this.count;
141
+ }
142
+ setCount(count: number){
143
+ this.count = count;
144
+ }
145
+
146
+ toJSON(){
147
+ return {
148
+ ...super.toJSON(),
149
+ count: this.count
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * `EmptyNote`는 파싱 과정에서만 사용합니다.
156
+ */
157
+ export class EmptyNote extends Note {
158
+ declare type: Note.Type.EMPTY;
159
+ constructor(type: Note.Type.EMPTY, timing: math.Fraction, delay: math.Fraction) {
160
+ super(type, timing, delay);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * `RollEndNote`는 파싱 과정에서만 사용합니다.
166
+ */
167
+ export class RollEndNote extends Note {
168
+ declare type: Note.Type.ROLL_END;
169
+ constructor(timing: math.Fraction, delay: math.Fraction) {
170
+ super(Note.Type.ROLL_END, timing, delay);
171
+ }
172
+ }
@@ -0,0 +1,88 @@
1
+ import { Course } from './Course.js';
2
+ import type { Difficulty } from '../types.js';
3
+ import { MetadataParseException } from '../exception/ParseException.js';
4
+
5
+ export class Song {
6
+ /**
7
+ * @throws {MetadataParseException}
8
+ */
9
+ static parse(tja: string): Song {
10
+ const lines = tja.split('\n').map((e) => e.trim()).filter((e) => e);
11
+
12
+ const metadata = new this.Metadata();
13
+ const song = new Song(metadata);
14
+
15
+ let i = 0;
16
+ for (; i < lines.length; i++) {
17
+ if (lines[i].startsWith('COURSE')) {
18
+ break;
19
+ }
20
+ const parsedMetadata = this.parseMetadata(lines[i]);
21
+ metadata[parsedMetadata.key] = parsedMetadata.value;
22
+ }
23
+
24
+ let courseTja: string[] = [];
25
+ for (; i < lines.length; i++) {
26
+ courseTja.push(lines[i]);
27
+ if (lines[i].startsWith('#END')) {
28
+ const course = Course.parse(courseTja, song);
29
+ song.course[course.difficulty] = course;
30
+ courseTja = [];
31
+ }
32
+ };
33
+
34
+ return song;
35
+ }
36
+
37
+ /**
38
+ * @throws {MetadataParseException}
39
+ */
40
+ static parseMetadata(line: string): { key: string, value: string } {
41
+ const colonIndex = line.indexOf(':');
42
+ if (colonIndex < 0) {
43
+ throw new MetadataParseException(line);
44
+ }
45
+ const key = line.slice(0, colonIndex).toLowerCase();
46
+ const value = line.slice(colonIndex + 1);
47
+ return {
48
+ key: key.trim(),
49
+ value: value.trim()
50
+ }
51
+ }
52
+
53
+ metadata: Song.Metadata;
54
+ course: Partial<Record<Difficulty, Course>> = {};
55
+
56
+ constructor(metadata: Song.Metadata) {
57
+ this.metadata = metadata;
58
+ }
59
+
60
+ /**
61
+ * `metadata`에서 bpm을 가져옴.
62
+ * `metadata`에 bpm이 존재하지 않으면 `160`을 반환.
63
+ */
64
+ getBPM(){
65
+ return Number(this.metadata.bpm) || 160;
66
+ }
67
+
68
+ toJSON(){
69
+ return {
70
+ metadata: this.metadata,
71
+ course: this.course
72
+ }
73
+ }
74
+ }
75
+
76
+ export namespace Song {
77
+ export class Metadata {
78
+ title?: string;
79
+ subtitle?: string;
80
+ bpm?: number;
81
+ offset?: number;
82
+ wave?: string;
83
+ songvol?: number;
84
+ sevol?: number;
85
+ demostart?: number;
86
+ [key: string]: string | number | undefined;
87
+ }
88
+ }
@@ -0,0 +1,13 @@
1
+ import { TjaException } from './TjaException.js';
2
+
3
+ export class ParseException extends TjaException {
4
+ line?: string;
5
+
6
+ constructor(line?: string) {
7
+ super();
8
+ this.line = line;
9
+ }
10
+ };
11
+ export class MetadataParseException extends ParseException { };
12
+ export class CourseParseException extends ParseException { };
13
+ export class UnknownCourseDifficultyException extends CourseParseException { };
@@ -0,0 +1 @@
1
+ export class TjaException{};
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './class/Song.js';
2
+ export * from './class/Course.js';
3
+
4
+ export type * from './types.js';
package/src/types.ts ADDED
@@ -0,0 +1 @@
1
+ export type Difficulty = 'easy' | 'normal' | 'hard' | 'oni' | 'edit';
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "include": [
3
+ "./src/**/*.ts"
4
+ ],
5
+ "compilerOptions": {
6
+ "target": "ESNext",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "removeComments": true,
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "module": "NodeNext",
16
+ "moduleResolution": "NodeNext",
17
+ "outDir": "./dist"
18
+ },
19
+ "exclude": [
20
+ "./node_modules",
21
+ "**/node_modules/*"
22
+ ]
23
+ }