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.
- package/format.mediawiki +715 -0
- package/package.json +14 -0
- package/src/class/Bar.ts +63 -0
- package/src/class/BarLine.ts +24 -0
- package/src/class/Branch.ts +34 -0
- package/src/class/Command.ts +139 -0
- package/src/class/Course.ts +425 -0
- package/src/class/Item.ts +27 -0
- package/src/class/Note.ts +172 -0
- package/src/class/Song.ts +88 -0
- package/src/exception/ParseException.ts +13 -0
- package/src/exception/TjaException.ts +1 -0
- package/src/index.ts +4 -0
- package/src/types.ts +1 -0
- package/tsconfig.json +23 -0
package/package.json
ADDED
package/src/class/Bar.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Command } from "./Command.js";
|
|
2
|
+
import { Item } from "./Item.js";
|
|
3
|
+
import * as math from 'mathjs';
|
|
4
|
+
import { Note } from "./Note.js";
|
|
5
|
+
|
|
6
|
+
export class Bar {
|
|
7
|
+
private items: Item[] = [];
|
|
8
|
+
private notes: Note[] = [];
|
|
9
|
+
private commands: Command[] = [];
|
|
10
|
+
private start: math.Fraction;
|
|
11
|
+
private end: math.Fraction;
|
|
12
|
+
private barLength = 0;
|
|
13
|
+
constructor(start: math.Fraction, end: math.Fraction) {
|
|
14
|
+
this.start = start;
|
|
15
|
+
this.end = end;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pushItem(...items: Item[]) {
|
|
19
|
+
this.items.push(...items);
|
|
20
|
+
this.notes.push(...items.filter((item) => item instanceof Note));
|
|
21
|
+
this.commands.push(...items.filter((item) => item instanceof Command));
|
|
22
|
+
}
|
|
23
|
+
getItems() {
|
|
24
|
+
return Array.from(this.items);
|
|
25
|
+
}
|
|
26
|
+
getNotes() {
|
|
27
|
+
return Array.from(this.notes);
|
|
28
|
+
}
|
|
29
|
+
getCommands() {
|
|
30
|
+
return Array.from(this.commands);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getStart() {
|
|
34
|
+
return math.fraction(this.start);
|
|
35
|
+
}
|
|
36
|
+
setStart(start: math.Fraction) {
|
|
37
|
+
this.start = math.fraction(start);
|
|
38
|
+
}
|
|
39
|
+
getEnd() {
|
|
40
|
+
return math.fraction(this.end);
|
|
41
|
+
}
|
|
42
|
+
setEnd(end: math.Fraction) {
|
|
43
|
+
this.end = math.fraction(end);
|
|
44
|
+
}
|
|
45
|
+
getBarLength(){
|
|
46
|
+
return this.barLength;
|
|
47
|
+
}
|
|
48
|
+
setBarLength(barLength: number){
|
|
49
|
+
this.barLength = barLength;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toJSON(): any {
|
|
53
|
+
return {
|
|
54
|
+
start: this.start.valueOf(),
|
|
55
|
+
end: this.end.valueOf(),
|
|
56
|
+
items: this.items,
|
|
57
|
+
notes: this.notes,
|
|
58
|
+
commands: this.commands,
|
|
59
|
+
barLength: this.barLength,
|
|
60
|
+
type: 'bar'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Item } from "./Item";
|
|
2
|
+
|
|
3
|
+
export class BarLine extends Item {
|
|
4
|
+
type = 'barline'
|
|
5
|
+
private hidden: boolean = false;
|
|
6
|
+
|
|
7
|
+
isHidden() {
|
|
8
|
+
return this.hidden;
|
|
9
|
+
}
|
|
10
|
+
hide() {
|
|
11
|
+
this.hidden = true;
|
|
12
|
+
}
|
|
13
|
+
show(){
|
|
14
|
+
this.hidden = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toJSON(){
|
|
18
|
+
return {
|
|
19
|
+
...super.toJSON(),
|
|
20
|
+
type: this.type,
|
|
21
|
+
hidden: this.hidden
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as math from 'mathjs';
|
|
2
|
+
import { Bar } from './Bar.js';
|
|
3
|
+
|
|
4
|
+
export class Branch extends Bar {
|
|
5
|
+
type: Branch.Type;
|
|
6
|
+
criteria: [number, number];
|
|
7
|
+
normal?: Bar[];
|
|
8
|
+
advanced?: Bar[];
|
|
9
|
+
master?: Bar[];
|
|
10
|
+
|
|
11
|
+
constructor(type: Branch.Type, criteria1: [number, number], start: math.Fraction, end: math.Fraction) {
|
|
12
|
+
super(start, end);
|
|
13
|
+
this.type = type;
|
|
14
|
+
this.criteria = criteria1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toJSON(){
|
|
18
|
+
const superJSON = super.toJSON();
|
|
19
|
+
return {
|
|
20
|
+
start: superJSON.start,
|
|
21
|
+
end: superJSON.end,
|
|
22
|
+
normal: this.normal,
|
|
23
|
+
advanced: this.advanced,
|
|
24
|
+
master: this.master,
|
|
25
|
+
criteria: this.criteria,
|
|
26
|
+
type: 'branch',
|
|
27
|
+
branchType: this.type === Branch.Type.ROLL ? 'roll' : 'acc'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export namespace Branch {
|
|
33
|
+
export enum Type { ROLL, ACCURACY }
|
|
34
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Item } from "./Item.js";
|
|
2
|
+
import * as math from 'mathjs';
|
|
3
|
+
|
|
4
|
+
export class Command extends Item {
|
|
5
|
+
static parse(line: string): Command | null {
|
|
6
|
+
if (line === '#BARLINEON') {
|
|
7
|
+
return new BarlineCommand(false, math.fraction(0));
|
|
8
|
+
}
|
|
9
|
+
else if (line === "#BARLINEOFF") {
|
|
10
|
+
return new BarlineCommand(true, math.fraction(0));
|
|
11
|
+
}
|
|
12
|
+
else if (line === "#GOGOSTART") {
|
|
13
|
+
return new GOGOCommand(GOGOCommand.Type.START, math.fraction(0));
|
|
14
|
+
}
|
|
15
|
+
else if (line === "#GOGOEND") {
|
|
16
|
+
return new GOGOCommand(GOGOCommand.Type.END, math.fraction(0));
|
|
17
|
+
}
|
|
18
|
+
else if (line === "#SECTION") {
|
|
19
|
+
return new SectionCommand(math.fraction(0));
|
|
20
|
+
}
|
|
21
|
+
else if (line.startsWith('#BPMCHANGE')) {
|
|
22
|
+
const value = Number(line.replace('#BPMCHANGE', ''));
|
|
23
|
+
if (Number.isNaN(value)) return null;
|
|
24
|
+
return new BPMChangeCommand(value, math.fraction(0));
|
|
25
|
+
}
|
|
26
|
+
else if (line.startsWith('#MEASURE')) {
|
|
27
|
+
return new MeasureCommand(math.fraction(line.replace('#MEASURE', '').trim()), math.fraction(0));
|
|
28
|
+
}
|
|
29
|
+
else if (line.startsWith('#SCROLL')) {
|
|
30
|
+
const value = Number(line.replace('#SCROLL', ''));
|
|
31
|
+
if (Number.isNaN(value)) return null;
|
|
32
|
+
return new ScrollCommand(value, math.fraction(0));
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(timing: math.Fraction) {
|
|
38
|
+
super(timing);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class BarlineCommand extends Command {
|
|
43
|
+
private hide: boolean;
|
|
44
|
+
constructor(hide: boolean, timing: math.Fraction) {
|
|
45
|
+
super(timing);
|
|
46
|
+
this.hide = hide;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getHide(){
|
|
50
|
+
return this.hide;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON(){
|
|
54
|
+
return {
|
|
55
|
+
...super.toJSON(),
|
|
56
|
+
type: 'command-barline',
|
|
57
|
+
hide: this.hide
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class BPMChangeCommand extends Command {
|
|
63
|
+
value: number;
|
|
64
|
+
constructor(value: number, timing: math.Fraction) {
|
|
65
|
+
super(timing);
|
|
66
|
+
this.value = value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toJSON(){
|
|
70
|
+
return {
|
|
71
|
+
...super.toJSON(),
|
|
72
|
+
type: 'command-bpmchange',
|
|
73
|
+
value: this.value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class MeasureCommand extends Command {
|
|
79
|
+
value: math.Fraction;
|
|
80
|
+
constructor(value: math.Fraction, timing: math.Fraction) {
|
|
81
|
+
super(timing);
|
|
82
|
+
this.value = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
toJSON(){
|
|
86
|
+
return {
|
|
87
|
+
...super.toJSON(),
|
|
88
|
+
type: 'command-measure',
|
|
89
|
+
value: this.value.toString()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class ScrollCommand extends Command {
|
|
95
|
+
value: number;
|
|
96
|
+
constructor(value: number, timing: math.Fraction) {
|
|
97
|
+
super(timing);
|
|
98
|
+
this.value = value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toJSON(){
|
|
102
|
+
return {
|
|
103
|
+
...super.toJSON(),
|
|
104
|
+
type: 'command-scroll',
|
|
105
|
+
value: this.value
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class SectionCommand extends Command {
|
|
111
|
+
toJSON() {
|
|
112
|
+
return {
|
|
113
|
+
...super.toJSON(),
|
|
114
|
+
type: 'command-section'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class GOGOCommand extends Command {
|
|
120
|
+
type: GOGOCommand.Type;
|
|
121
|
+
constructor(type: GOGOCommand.Type, timing: math.Fraction) {
|
|
122
|
+
super(timing);
|
|
123
|
+
this.type = type;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
toJSON(){
|
|
127
|
+
return {
|
|
128
|
+
...super.toJSON(),
|
|
129
|
+
type: 'command-gogo',
|
|
130
|
+
start: this.type === GOGOCommand.Type.START
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export namespace GOGOCommand {
|
|
135
|
+
export enum Type {
|
|
136
|
+
START,
|
|
137
|
+
END
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { UnknownCourseDifficultyException, MetadataParseException } from "../exception/ParseException.js";
|
|
2
|
+
import type { Difficulty } from "../types.js";
|
|
3
|
+
import { Bar } from "./Bar.js";
|
|
4
|
+
import { BarLine } from "./BarLine.js";
|
|
5
|
+
import { Branch } from "./Branch.js";
|
|
6
|
+
import { BarlineCommand, BPMChangeCommand, Command, MeasureCommand, ScrollCommand } from "./Command.js";
|
|
7
|
+
import type { Item } from "./Item.js";
|
|
8
|
+
import { BalloonNote, EmptyNote, HitNote, Note, RollEndNote, RollNote } from "./Note.js";
|
|
9
|
+
import { Song } from "./Song.js";
|
|
10
|
+
import * as math from 'mathjs';
|
|
11
|
+
|
|
12
|
+
export class Course {
|
|
13
|
+
/**
|
|
14
|
+
* @throws {MetadataParseException}
|
|
15
|
+
* @throws {CourseParseException}
|
|
16
|
+
* @todo command와 note 파싱할 것
|
|
17
|
+
*/
|
|
18
|
+
static parse(courseTja: string | string[], song?: Song): Course {
|
|
19
|
+
if (typeof (courseTja) === "string") {
|
|
20
|
+
courseTja = courseTja.split('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const metadata = new Course.Metadata();
|
|
24
|
+
let difficulty: Difficulty | undefined;
|
|
25
|
+
|
|
26
|
+
let i = 0;
|
|
27
|
+
for (; i < courseTja.length; i++) {
|
|
28
|
+
if (courseTja[i].startsWith('#START')) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const parsedMetadata = Song.parseMetadata(courseTja[i]);
|
|
32
|
+
if (parsedMetadata.key === "course") {
|
|
33
|
+
if(parsedMetadata.value === "0" || parsedMetadata.value === "Easy"){
|
|
34
|
+
difficulty = 'easy';
|
|
35
|
+
}
|
|
36
|
+
else if(parsedMetadata.value === "1" || parsedMetadata.value === "Normal"){
|
|
37
|
+
difficulty = 'normal';
|
|
38
|
+
}
|
|
39
|
+
else if(parsedMetadata.value === "2" || parsedMetadata.value === "Hard"){
|
|
40
|
+
difficulty = 'hard';
|
|
41
|
+
}
|
|
42
|
+
else if(parsedMetadata.value === "3" || parsedMetadata.value === "Oni"){
|
|
43
|
+
difficulty = 'oni';
|
|
44
|
+
}
|
|
45
|
+
else if(parsedMetadata.value === "4" || parsedMetadata.value === "Edit"){
|
|
46
|
+
difficulty = 'edit';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
metadata[parsedMetadata.key] = parsedMetadata.value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!difficulty) {
|
|
55
|
+
throw new UnknownCourseDifficultyException(courseTja[0]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const course = new Course(difficulty, metadata, song);
|
|
59
|
+
course.pushBar(...this.parseBar(courseTja.slice(i), course.getBalloonIterator(), course.getBPM()));
|
|
60
|
+
|
|
61
|
+
return course;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static parseBar(lines: string[], getNextBalloon: () => number, bpmInit: number): Bar[] {
|
|
65
|
+
const lineGroups = this.groupLines(lines);
|
|
66
|
+
|
|
67
|
+
return this.convertLineGroupToBar(lineGroups, getNextBalloon, bpmInit).bars;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tja line을 마디 별로 묶음
|
|
72
|
+
*/
|
|
73
|
+
private static groupLines(lines: string[]) {
|
|
74
|
+
const LineGroup = Course.LineGroup;
|
|
75
|
+
const BranchedLineGroup = Course.BranchedLineGroup;
|
|
76
|
+
type LineGroup = Course.LineGroup;
|
|
77
|
+
type BranchedLineGroup = Course.BranchedLineGroup;
|
|
78
|
+
|
|
79
|
+
const bars: (LineGroup | BranchedLineGroup)[] = [];
|
|
80
|
+
let currentBar: LineGroup | null = null;
|
|
81
|
+
let currentBranchedLineGroup: BranchedLineGroup | null = null;
|
|
82
|
+
let currentCourse: 'normal' | 'advanced' | 'master' | null = null;
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (line === "#START") continue;
|
|
85
|
+
if (line === "#END") break;
|
|
86
|
+
|
|
87
|
+
if (line.startsWith("#BRANCHSTART")) {
|
|
88
|
+
const branchRawDatas = line.replaceAll('#BRANCHSTART', '').split(',').map(e => e.trim()) as [string, string, string];
|
|
89
|
+
const type = branchRawDatas[0] === "r" ? Branch.Type.ROLL : Branch.Type.ACCURACY;
|
|
90
|
+
const criteria: [number, number] = [Number(branchRawDatas[1]) || 0, Number(branchRawDatas[2]) || 0];
|
|
91
|
+
|
|
92
|
+
currentCourse = null;
|
|
93
|
+
currentBranchedLineGroup = new BranchedLineGroup(type, criteria);
|
|
94
|
+
currentBar = new LineGroup();
|
|
95
|
+
bars.push(currentBranchedLineGroup);
|
|
96
|
+
}
|
|
97
|
+
else if (line === "#BRANCHEND") {
|
|
98
|
+
currentCourse = null;
|
|
99
|
+
currentBar = new LineGroup();
|
|
100
|
+
bars.push(currentBar);
|
|
101
|
+
}
|
|
102
|
+
else if (line === "#N") {
|
|
103
|
+
currentCourse = "normal";
|
|
104
|
+
currentBar = new LineGroup();
|
|
105
|
+
currentBranchedLineGroup?.addNormal(currentBar);
|
|
106
|
+
}
|
|
107
|
+
else if (line === "#E") {
|
|
108
|
+
currentCourse = 'advanced';
|
|
109
|
+
currentBar = new LineGroup();
|
|
110
|
+
currentBranchedLineGroup?.addAdvanced(currentBar);
|
|
111
|
+
}
|
|
112
|
+
else if (line === "#M") {
|
|
113
|
+
currentCourse = 'master';
|
|
114
|
+
currentBar = new LineGroup();
|
|
115
|
+
currentBranchedLineGroup?.addMaster(currentBar);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (!currentBar) {
|
|
119
|
+
currentBar = new LineGroup();
|
|
120
|
+
bars.push(currentBar);
|
|
121
|
+
}
|
|
122
|
+
currentBar.add(line);
|
|
123
|
+
if (line.endsWith(',')) {
|
|
124
|
+
if (!currentCourse) {
|
|
125
|
+
currentBar = new LineGroup();
|
|
126
|
+
bars.push(currentBar);
|
|
127
|
+
}
|
|
128
|
+
else if (currentCourse === "normal") {
|
|
129
|
+
currentBar = new LineGroup();
|
|
130
|
+
currentBranchedLineGroup?.addNormal(currentBar);
|
|
131
|
+
}
|
|
132
|
+
else if (currentCourse === "advanced") {
|
|
133
|
+
currentBar = new LineGroup();
|
|
134
|
+
currentBranchedLineGroup?.addAdvanced(currentBar);
|
|
135
|
+
}
|
|
136
|
+
else if (currentCourse === "master") {
|
|
137
|
+
currentBar = new LineGroup();
|
|
138
|
+
currentBranchedLineGroup?.addMaster(currentBar);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return bars;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `LineGroup`을 `Bar`로 변환
|
|
149
|
+
*/
|
|
150
|
+
private static convertLineGroupToBar(
|
|
151
|
+
lineGroups: (Course.LineGroup | Course.BranchedLineGroup)[],
|
|
152
|
+
getNextBalloon: () => number,
|
|
153
|
+
currentBPM: number,
|
|
154
|
+
currentTiming: math.Fraction = math.fraction(0),
|
|
155
|
+
currentMeasure: math.Fraction = math.fraction(1),
|
|
156
|
+
currentScroll: number = 1,
|
|
157
|
+
barlineHidden: boolean = false,
|
|
158
|
+
): {
|
|
159
|
+
bars: Bar[],
|
|
160
|
+
getNextBalloon: () => number,
|
|
161
|
+
currentBPM: number,
|
|
162
|
+
currentTiming: math.Fraction,
|
|
163
|
+
currentMeasure: math.Fraction,
|
|
164
|
+
currentScroll: number,
|
|
165
|
+
barlineHidden: boolean
|
|
166
|
+
} {
|
|
167
|
+
/**
|
|
168
|
+
* 1000ms * (240 / BPM) * currentMeasure
|
|
169
|
+
*/
|
|
170
|
+
function getBarLength(): math.Fraction {
|
|
171
|
+
return math.fraction(math.divide(math.multiply(1000, 240, currentMeasure), math.fraction(currentBPM)) as math.Fraction);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const bars: Bar[] = [];
|
|
175
|
+
for (const lineGroup of lineGroups) {
|
|
176
|
+
if (lineGroup instanceof Course.LineGroup) {
|
|
177
|
+
if (lineGroup.lines.length === 0) continue;
|
|
178
|
+
|
|
179
|
+
let barlinePushed = false;
|
|
180
|
+
let barLength = 0;
|
|
181
|
+
let items: Item[] = [];
|
|
182
|
+
for (const line of lineGroup.lines) {
|
|
183
|
+
if (line.startsWith('#')) {
|
|
184
|
+
const command = Command.parse(line);
|
|
185
|
+
if (command) items.push(command);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
if (!barlinePushed) {
|
|
189
|
+
items.push(new BarLine(math.fraction(0)));
|
|
190
|
+
barlinePushed = true;
|
|
191
|
+
}
|
|
192
|
+
for (const char of line) {
|
|
193
|
+
if (char === ',') break;
|
|
194
|
+
const note = Note.parse(char);
|
|
195
|
+
if (note) {
|
|
196
|
+
items.push(note);
|
|
197
|
+
barLength++;
|
|
198
|
+
};
|
|
199
|
+
if(note instanceof BalloonNote){
|
|
200
|
+
note.setCount(getNextBalloon());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const bar = new Bar(math.fraction(currentTiming), math.fraction(currentTiming));
|
|
207
|
+
bar.setBarLength(barLength);
|
|
208
|
+
if (barLength === 0) {
|
|
209
|
+
items.forEach((item) => {
|
|
210
|
+
item.setTiming(currentTiming);
|
|
211
|
+
});
|
|
212
|
+
currentTiming = math.add(currentTiming, getBarLength());
|
|
213
|
+
bar.setEnd(currentTiming);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// 시작 타이밍 계산
|
|
217
|
+
const notes: Note[] = [];
|
|
218
|
+
items.forEach((item) => {
|
|
219
|
+
item.setTiming(currentTiming);
|
|
220
|
+
if (item instanceof Note) {
|
|
221
|
+
const delay = math.fraction(math.divide(getBarLength(), barLength) as math.Fraction);
|
|
222
|
+
item.setDelay(delay);
|
|
223
|
+
item.setBpm(currentBPM);
|
|
224
|
+
item.setScroll(currentScroll);
|
|
225
|
+
currentTiming = math.add(currentTiming, delay);
|
|
226
|
+
notes.push(item);
|
|
227
|
+
}
|
|
228
|
+
else if (item instanceof Command) {
|
|
229
|
+
if (item instanceof MeasureCommand) {
|
|
230
|
+
currentMeasure = math.fraction(item.value);
|
|
231
|
+
}
|
|
232
|
+
else if (item instanceof BPMChangeCommand) {
|
|
233
|
+
currentBPM = item.value;
|
|
234
|
+
}
|
|
235
|
+
else if (item instanceof ScrollCommand) {
|
|
236
|
+
currentScroll = item.value;
|
|
237
|
+
}
|
|
238
|
+
else if (item instanceof BarlineCommand) {
|
|
239
|
+
barlineHidden = item.getHide();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (item instanceof BarLine) {
|
|
243
|
+
if (barlineHidden) {
|
|
244
|
+
item.hide();
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
item.show();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
bar.setEnd(currentTiming);
|
|
252
|
+
|
|
253
|
+
// 노트의 delay와 연타 노트의 end 조정
|
|
254
|
+
let rollEnd: math.Fraction | null = null;
|
|
255
|
+
let delaySum: math.Fraction = math.fraction(0);
|
|
256
|
+
let lengthSum: number = 1;
|
|
257
|
+
for (let i = notes.length - 1; i >= 0; i--) {
|
|
258
|
+
const note = notes[i];
|
|
259
|
+
if (note instanceof EmptyNote) {
|
|
260
|
+
delaySum = math.add(delaySum, note.getDelay());
|
|
261
|
+
lengthSum++;
|
|
262
|
+
}
|
|
263
|
+
else if (note instanceof RollEndNote) {
|
|
264
|
+
delaySum = math.add(delaySum, note.getDelay());
|
|
265
|
+
rollEnd = note.getTiming();
|
|
266
|
+
lengthSum++;
|
|
267
|
+
}
|
|
268
|
+
else if (note instanceof HitNote) {
|
|
269
|
+
note.setDelay(math.add(delaySum, note.getDelay()));
|
|
270
|
+
note.setNoteLength(lengthSum);
|
|
271
|
+
delaySum = math.fraction(0);
|
|
272
|
+
lengthSum = 1;
|
|
273
|
+
}
|
|
274
|
+
else if (note instanceof RollNote || note instanceof BalloonNote) {
|
|
275
|
+
note.setDelay(math.add(delaySum, note.getDelay()));
|
|
276
|
+
note.setNoteLength(lengthSum);
|
|
277
|
+
delaySum = math.fraction(0);
|
|
278
|
+
lengthSum = 1;
|
|
279
|
+
note.end = rollEnd ? rollEnd : note.getTiming();
|
|
280
|
+
rollEnd = null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
items = items.filter((item) => {
|
|
285
|
+
if (item instanceof EmptyNote || item instanceof RollEndNote) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
bar.pushItem(...items);
|
|
292
|
+
}
|
|
293
|
+
bars.push(bar);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const branch = new Branch(lineGroup.type, lineGroup.criteria, math.fraction(currentTiming), math.fraction(currentTiming));
|
|
297
|
+
let result;
|
|
298
|
+
if (lineGroup.normal) {
|
|
299
|
+
result = this.convertLineGroupToBar(lineGroup.normal, getNextBalloon, currentBPM, currentTiming, currentMeasure, currentScroll, barlineHidden);
|
|
300
|
+
branch.normal = result.bars;
|
|
301
|
+
}
|
|
302
|
+
if (lineGroup.advanced) {
|
|
303
|
+
result = this.convertLineGroupToBar(lineGroup.advanced, getNextBalloon, currentBPM, currentTiming, currentMeasure, currentScroll, barlineHidden);
|
|
304
|
+
branch.advanced = result.bars;
|
|
305
|
+
}
|
|
306
|
+
if (lineGroup.master) {
|
|
307
|
+
result = this.convertLineGroupToBar(lineGroup.master, getNextBalloon, currentBPM, currentTiming, currentMeasure, currentScroll, barlineHidden);
|
|
308
|
+
branch.master = result.bars;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (result) {
|
|
312
|
+
currentBPM = result.currentBPM;
|
|
313
|
+
currentTiming = result.currentTiming;
|
|
314
|
+
currentMeasure = result.currentMeasure;
|
|
315
|
+
barlineHidden = result.barlineHidden;
|
|
316
|
+
branch.setEnd(result.currentTiming);
|
|
317
|
+
}
|
|
318
|
+
bars.push(branch);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
bars,
|
|
324
|
+
getNextBalloon,
|
|
325
|
+
currentBPM,
|
|
326
|
+
currentTiming,
|
|
327
|
+
currentMeasure,
|
|
328
|
+
currentScroll,
|
|
329
|
+
barlineHidden
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
difficulty: Difficulty;
|
|
334
|
+
metadata: Course.Metadata;
|
|
335
|
+
bars: Bar[] = [];
|
|
336
|
+
song?: Song;
|
|
337
|
+
|
|
338
|
+
constructor(difficulty: Difficulty, metadata: Course.Metadata, song?: Song) {
|
|
339
|
+
this.difficulty = difficulty;
|
|
340
|
+
this.metadata = metadata;
|
|
341
|
+
this.song = song;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `song`의 `getBPM`을 호출하여 반환함.
|
|
346
|
+
* `song`이 존재하지 않으면 160을 반환함.
|
|
347
|
+
*/
|
|
348
|
+
getBPM() {
|
|
349
|
+
return this.song?.getBPM() || 160;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* `metadata`에서 level을 가져옴.
|
|
354
|
+
* `metadata`에 level이 존재하지 않으면 1을 반환함.
|
|
355
|
+
*/
|
|
356
|
+
getLevel() {
|
|
357
|
+
return Number(this.metadata.level) || 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pushBar(...bars: Bar[]) {
|
|
361
|
+
this.bars.push(...bars);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getBalloon(){
|
|
365
|
+
return (this.metadata.balloon as string ?? '').split(',').map((e) => Number(e));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getBalloonIterator(){
|
|
369
|
+
let index = 0;
|
|
370
|
+
const balloons = this.getBalloon();
|
|
371
|
+
function getNext(){
|
|
372
|
+
const balloon = balloons[index];
|
|
373
|
+
index++;
|
|
374
|
+
return balloon;
|
|
375
|
+
}
|
|
376
|
+
return getNext;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
toJSON(){
|
|
380
|
+
return {
|
|
381
|
+
metadata: this.metadata,
|
|
382
|
+
difficulty: this.difficulty,
|
|
383
|
+
bars: this.bars
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export namespace Course {
|
|
389
|
+
export class Metadata {
|
|
390
|
+
level?: number;
|
|
391
|
+
[key: string]: string | number | undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export class LineGroup {
|
|
395
|
+
lines: string[] = [];
|
|
396
|
+
add(line: string) {
|
|
397
|
+
this.lines.push(line);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
export class BranchedLineGroup {
|
|
401
|
+
type: Branch.Type;
|
|
402
|
+
criteria: [number, number];
|
|
403
|
+
normal?: LineGroup[];
|
|
404
|
+
advanced?: LineGroup[];
|
|
405
|
+
master?: LineGroup[];
|
|
406
|
+
|
|
407
|
+
constructor(type: Branch.Type, criteria: [number, number]) {
|
|
408
|
+
this.type = type;
|
|
409
|
+
this.criteria = criteria;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
addNormal(lineGroup: LineGroup) {
|
|
413
|
+
if (!this.normal) this.normal = [];
|
|
414
|
+
this.normal.push(lineGroup);
|
|
415
|
+
}
|
|
416
|
+
addAdvanced(lineGroup: LineGroup) {
|
|
417
|
+
if (!this.advanced) this.advanced = [];
|
|
418
|
+
this.advanced.push(lineGroup);
|
|
419
|
+
}
|
|
420
|
+
addMaster(lineGroup: LineGroup) {
|
|
421
|
+
if (!this.master) this.master = [];
|
|
422
|
+
this.master.push(lineGroup);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|