sonolus-pjsekai-js 1.1.12 → 1.2.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,339 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uscToSUS = void 0;
4
+ const TICKS_PER_BEAT = 480;
5
+ const uscToSUS = (uscData) => {
6
+ const usc = uscData.usc ? uscData.usc : uscData;
7
+ const susLines = [];
8
+ const noteData = new Map();
9
+ let slideChannelCounter = 0;
10
+ // (메타데이터 및 전역 정의 생성은 이전과 동일)
11
+ susLines.push(`#TITLE ""`, `#ARTIST ""`, `#DESIGNER ""`, `#WAVEOFFSET ${-usc.offset}`, `#REQUEST "ticks_per_beat ${TICKS_PER_BEAT}"`, ``);
12
+ const ticksPerMeasure = TICKS_PER_BEAT * 4;
13
+ const bpmObjects = usc.objects.filter((obj) => obj.type === 'bpm');
14
+ const bpmDefinitions = new Map();
15
+ let bpmCounter = 1;
16
+ for (const bpmObject of bpmObjects) {
17
+ if (!bpmDefinitions.has(bpmObject.bpm)) {
18
+ const bpmId = bpmCounter.toString().padStart(2, '0');
19
+ bpmDefinitions.set(bpmObject.bpm, bpmId);
20
+ susLines.push(`#BPM${bpmId}: ${bpmObject.bpm}`);
21
+ bpmCounter++;
22
+ }
23
+ }
24
+ susLines.push(``);
25
+ const timeScaleGroups = usc.objects.filter((obj) => obj.type === 'timeScaleGroup');
26
+ const tilIdMap = new Map();
27
+ timeScaleGroups.forEach((group, index) => {
28
+ const tilId = index.toString().padStart(2, '0');
29
+ tilIdMap.set(index, tilId);
30
+ const changesString = group.changes
31
+ .map((change) => {
32
+ const totalTicks = change.beat * TICKS_PER_BEAT;
33
+ const measure = Math.floor(totalTicks / ticksPerMeasure);
34
+ const tickInMeasure = totalTicks % ticksPerMeasure;
35
+ return `${measure}'${tickInMeasure}:${change.timeScale}`;
36
+ })
37
+ .join(', ');
38
+ susLines.push(`#TIL${tilId}: "${changesString}"`);
39
+ });
40
+ susLines.push(``);
41
+ addNoteData(0, '00002', 0, '4', 0);
42
+ for (const bpmObject of bpmObjects) {
43
+ const tick = bpmObject.beat * TICKS_PER_BEAT;
44
+ const measure = Math.floor(tick / ticksPerMeasure);
45
+ const tickInMeasure = tick % ticksPerMeasure;
46
+ const bpmId = bpmDefinitions.get(bpmObject.bpm);
47
+ if (bpmId) {
48
+ addNoteData(measure, `${measure.toString().padStart(3, '0')}08`, tickInMeasure, bpmId, 0);
49
+ }
50
+ }
51
+ for (const obj of usc.objects) {
52
+ switch (obj.type) {
53
+ case 'single':
54
+ case 'damage':
55
+ processSingleOrDamage(obj);
56
+ break;
57
+ case 'slide':
58
+ processSlide(obj);
59
+ slideChannelCounter++;
60
+ break;
61
+ case 'guide':
62
+ processGuide(obj);
63
+ slideChannelCounter++;
64
+ break;
65
+ case 'timeScaleGroup':
66
+ case 'bpm':
67
+ break;
68
+ }
69
+ }
70
+ // ▼▼▼ 노트 겹침 및 BPM 겹침 오류를 모두 해결한 최종 렌더링 로직 ▼▼▼
71
+ const sortedMeasures = Array.from(noteData.keys()).sort((a, b) => a - b);
72
+ let activeTimeScaleGroup = -1;
73
+ for (const measure of sortedMeasures) {
74
+ const headerMap = noteData.get(measure);
75
+ const sortedHeaders = Array.from(headerMap.keys()).sort();
76
+ for (const header of sortedHeaders) {
77
+ const notes = headerMap.get(header);
78
+ if (!notes || notes.length === 0)
79
+ continue;
80
+ const noteTimeScaleGroup = notes[0].timeScaleGroup;
81
+ const tilId = tilIdMap.get(noteTimeScaleGroup);
82
+ if (noteTimeScaleGroup !== activeTimeScaleGroup && tilId !== undefined) {
83
+ susLines.push(`#HISPEED ${tilId}`);
84
+ activeTimeScaleGroup = noteTimeScaleGroup;
85
+ }
86
+ if (header.endsWith('02')) {
87
+ susLines.push(`#${header}: ${notes[0].value}`);
88
+ continue;
89
+ }
90
+ const granularity = 1920;
91
+ const data = Array(granularity).fill('00');
92
+ const finalNotes = new Map();
93
+ const getPriority = (value) => {
94
+ const typeChar = value.charAt(0);
95
+ // Hidden 노트(타입 5)는 우선순위가 낮습니다.
96
+ if (typeChar === '5')
97
+ return 2;
98
+ // 그 외 모든 노트(BPM 변경 포함)는 우선순위가 높습니다.
99
+ return 1;
100
+ };
101
+ for (const note of notes) {
102
+ const index = Math.floor((note.tick / ticksPerMeasure) * granularity);
103
+ if (index >= granularity)
104
+ continue;
105
+ const existingNoteValue = finalNotes.get(index);
106
+ const newNoteValue = note.value;
107
+ if (!existingNoteValue ||
108
+ getPriority(newNoteValue) <= getPriority(existingNoteValue)) {
109
+ finalNotes.set(index, newNoteValue);
110
+ }
111
+ }
112
+ for (const [index, value] of finalNotes.entries()) {
113
+ data[index] = value;
114
+ }
115
+ susLines.push(`#${header}: ${data.join('')}`);
116
+ }
117
+ }
118
+ return susLines.join('\r\n');
119
+ // --- Helper Functions ---
120
+ function addNoteData(measure, header, tickInMeasure, value, timeScaleGroup) {
121
+ if (!noteData.has(measure))
122
+ noteData.set(measure, new Map());
123
+ const headerMap = noteData.get(measure);
124
+ if (!headerMap.has(header))
125
+ headerMap.set(header, []);
126
+ headerMap.get(header).push({ tick: tickInMeasure, value, timeScaleGroup });
127
+ }
128
+ function getSusLaneAndWidth(uscLane, uscSize) {
129
+ const susWidth = Math.round(uscSize * 2);
130
+ const susLane = Math.round(uscLane - uscSize + 8);
131
+ return [Math.max(0, susLane), Math.max(1, susWidth)];
132
+ }
133
+ function addTapNote(beat, lane, width, noteType, timeScaleGroup) {
134
+ const tick = beat * TICKS_PER_BEAT;
135
+ const measure = Math.floor(tick / ticksPerMeasure);
136
+ const tickInMeasure = tick % ticksPerMeasure;
137
+ const laneHex = lane.toString(36);
138
+ const widthHex = width.toString(36);
139
+ const header = `${measure.toString().padStart(3, '0')}1${laneHex}`;
140
+ const value = `${noteType}${widthHex}`;
141
+ addNoteData(measure, header, tickInMeasure, value, timeScaleGroup);
142
+ }
143
+ function processSingleOrDamage(note) {
144
+ if (note.type === 'damage') {
145
+ const [lane, width] = getSusLaneAndWidth(note.lane, note.size);
146
+ addTapNote(note.beat, lane, width, '4', note.timeScaleGroup);
147
+ return;
148
+ }
149
+ const [lane, width] = getSusLaneAndWidth(note.lane, note.size);
150
+ let susNoteType = '1';
151
+ if (note.critical && note.trace)
152
+ susNoteType = '6';
153
+ else if (note.critical)
154
+ susNoteType = '2';
155
+ else if (note.trace)
156
+ susNoteType = '5';
157
+ addTapNote(note.beat, lane, width, susNoteType, note.timeScaleGroup);
158
+ if (note.direction && note.direction !== 'none') {
159
+ let directionalType;
160
+ switch (note.direction) {
161
+ case 'up':
162
+ directionalType = '1';
163
+ break;
164
+ case 'left':
165
+ directionalType = '3';
166
+ break;
167
+ case 'right':
168
+ directionalType = '4';
169
+ break;
170
+ default:
171
+ return;
172
+ }
173
+ const [dirLane, dirWidth] = getSusLaneAndWidth(note.lane, note.size);
174
+ const tick = note.beat * TICKS_PER_BEAT;
175
+ const measure = Math.floor(tick / ticksPerMeasure);
176
+ const tickInMeasure = tick % ticksPerMeasure;
177
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${dirLane.toString(36)}`;
178
+ const dirValue = `${directionalType}${dirWidth.toString(36)}`;
179
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, note.timeScaleGroup);
180
+ }
181
+ }
182
+ // ▼▼▼ `ease` 처리 로직을 최종 수정한 `processSlide` 함수 ▼▼▼
183
+ function processSlide(slide) {
184
+ const channel = (slideChannelCounter % 36).toString(36);
185
+ const sortedConnections = [...slide.connections].sort((a, b) => a.beat - b.beat);
186
+ let lastKnownLane = 0;
187
+ let lastKnownWidth = 0;
188
+ for (const conn of sortedConnections) {
189
+ const tick = conn.beat * TICKS_PER_BEAT;
190
+ const measure = Math.floor(tick / ticksPerMeasure);
191
+ const tickInMeasure = tick % ticksPerMeasure;
192
+ const timeScaleGroup = conn.timeScaleGroup ?? 0;
193
+ const isCritical = slide.critical || conn.critical;
194
+ let lane, width;
195
+ if (conn.type !== 'attach' && 'lane' in conn && 'size' in conn) {
196
+ ;
197
+ [lane, width] = getSusLaneAndWidth(conn.lane, conn.size);
198
+ lastKnownLane = lane;
199
+ lastKnownWidth = width;
200
+ }
201
+ else {
202
+ lane = lastKnownLane;
203
+ width = lastKnownWidth;
204
+ }
205
+ const laneHex = lane.toString(36);
206
+ const widthHex = width.toString(36);
207
+ // --- 마커 및 방향성 노트 생성 로직 ---
208
+ // 1. Easing 마커 (start, end, tick 모두에 적용)
209
+ if ('ease' in conn && conn.ease) {
210
+ let easeType = conn.ease;
211
+ if (easeType === 'inout')
212
+ easeType = 'in';
213
+ if (easeType === 'outin')
214
+ easeType = 'out';
215
+ let directionalType = null;
216
+ if (easeType === 'in')
217
+ directionalType = '2';
218
+ else if (easeType === 'out')
219
+ directionalType = '5';
220
+ if (directionalType) {
221
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${laneHex}`;
222
+ const dirValue = `${directionalType}${widthHex}`;
223
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, timeScaleGroup);
224
+ }
225
+ }
226
+ // 2. 기타 마커 및 Flick (start, end 에만 적용)
227
+ if (conn.type === 'start' || conn.type === 'end') {
228
+ let markerType = null;
229
+ if ('judgeType' in conn) {
230
+ if (conn.judgeType === 'none')
231
+ markerType = isCritical ? '8' : '7';
232
+ else if (conn.judgeType === 'trace')
233
+ markerType = isCritical ? '6' : '5';
234
+ }
235
+ if (conn.type === 'start' && isCritical && markerType === null) {
236
+ markerType = '2';
237
+ }
238
+ if (markerType) {
239
+ addTapNote(conn.beat, lane, width, markerType, timeScaleGroup);
240
+ }
241
+ if (conn.type === 'end' && conn.direction) {
242
+ let directionalType;
243
+ switch (conn.direction) {
244
+ case 'up':
245
+ directionalType = '1';
246
+ break;
247
+ case 'left':
248
+ directionalType = '3';
249
+ break;
250
+ case 'right':
251
+ directionalType = '4';
252
+ break;
253
+ default:
254
+ continue;
255
+ }
256
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${laneHex}`;
257
+ const dirValue = `${directionalType}${widthHex}`;
258
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, timeScaleGroup);
259
+ }
260
+ }
261
+ // --- 실제 슬라이드 경로 노트 생성 로직 ---
262
+ let noteType = null;
263
+ switch (conn.type) {
264
+ case 'start':
265
+ noteType = '1';
266
+ break;
267
+ case 'end':
268
+ noteType = '2';
269
+ break;
270
+ case 'tick':
271
+ noteType = conn.critical !== undefined ? '3' : '5';
272
+ break;
273
+ case 'attach':
274
+ // `convert.ts` 원리: '보이는 경유점(타입 3)' + '틱 제거 마커(타입 3)' 조합으로 변환
275
+ const attachHeader = `${measure.toString().padStart(3, '0')}3${laneHex}${channel}`;
276
+ const attachValue = `3${widthHex}`; // 보이는 경유점
277
+ addNoteData(measure, attachHeader, tickInMeasure, attachValue, timeScaleGroup);
278
+ // 틱 제거용 마커(SUS 탭 타입 3)
279
+ addTapNote(conn.beat, lane, width, '3', timeScaleGroup);
280
+ continue;
281
+ }
282
+ if (noteType) {
283
+ const header = `${measure.toString().padStart(3, '0')}3${laneHex}${channel}`;
284
+ const value = `${noteType}${widthHex}`;
285
+ addNoteData(measure, header, tickInMeasure, value, timeScaleGroup);
286
+ }
287
+ }
288
+ }
289
+ function processGuide(guide) {
290
+ const channel = (slideChannelCounter % 36).toString(36);
291
+ const sortedMidpoints = [...guide.midpoints].sort((a, b) => a.beat - b.beat);
292
+ sortedMidpoints.forEach((midpoint, index) => {
293
+ const tick = midpoint.beat * TICKS_PER_BEAT;
294
+ const measure = Math.floor(tick / ticksPerMeasure);
295
+ const tickInMeasure = tick % ticksPerMeasure;
296
+ let noteType;
297
+ if (index === 0)
298
+ noteType = '1';
299
+ else if (index === sortedMidpoints.length - 1)
300
+ noteType = '2';
301
+ else
302
+ noteType = '3';
303
+ const [lane, width] = getSusLaneAndWidth(midpoint.lane, midpoint.size);
304
+ const laneHex = lane.toString(36);
305
+ const widthHex = width.toString(36);
306
+ if (guide.fade === 'in' && index === 0) {
307
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${laneHex}`;
308
+ const dirValue = `2${widthHex}`;
309
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, midpoint.timeScaleGroup);
310
+ }
311
+ if (guide.fade === 'out' && index === sortedMidpoints.length - 1) {
312
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${laneHex}`;
313
+ const dirValue = `5${widthHex}`;
314
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, midpoint.timeScaleGroup);
315
+ }
316
+ if (midpoint.ease) {
317
+ let easeType = midpoint.ease;
318
+ if (easeType === 'inout')
319
+ easeType = 'in';
320
+ if (easeType === 'outin')
321
+ easeType = 'out';
322
+ let directionalType = null;
323
+ if (easeType === 'in')
324
+ directionalType = '2';
325
+ else if (easeType === 'out')
326
+ directionalType = '5';
327
+ if (directionalType) {
328
+ const dirHeader = `${measure.toString().padStart(3, '0')}5${laneHex}`;
329
+ const dirValue = `${directionalType}${widthHex}`;
330
+ addNoteData(measure, dirHeader, tickInMeasure, dirValue, midpoint.timeScaleGroup);
331
+ }
332
+ }
333
+ const header = `${measure.toString().padStart(3, '0')}9${laneHex}${channel}`;
334
+ const value = `${noteType}${widthHex}`;
335
+ addNoteData(measure, header, tickInMeasure, value, midpoint.timeScaleGroup);
336
+ });
337
+ }
338
+ };
339
+ exports.uscToSUS = uscToSUS;
@@ -0,0 +1 @@
1
+ export declare const uscToSUS: (uscData: any) => string;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.USCFade = exports.USCColor = void 0;
4
+ exports.USCColor = {
5
+ neutral: 0,
6
+ red: 1,
7
+ green: 2,
8
+ blue: 3,
9
+ yellow: 4,
10
+ purple: 5,
11
+ cyan: 6,
12
+ black: 7,
13
+ };
14
+ exports.USCFade = {
15
+ in: 2,
16
+ out: 0,
17
+ none: 1,
18
+ };
@@ -0,0 +1,91 @@
1
+ export type USC = {
2
+ offset: number;
3
+ objects: USCObject[];
4
+ };
5
+ export type USCObject = USCBpmChange | USCTimeScaleChange | USCSingleNote | USCSlideNote | USCGuideNote | USCDamageNote;
6
+ type BaseUSCObject = {
7
+ beat: number;
8
+ timeScaleGroup: number;
9
+ };
10
+ export type USCBpmChange = Omit<BaseUSCObject, 'timeScaleGroup'> & {
11
+ type: 'bpm';
12
+ bpm: number;
13
+ };
14
+ export type USCTimeScaleChange = {
15
+ type: 'timeScaleGroup';
16
+ changes: {
17
+ beat: number;
18
+ timeScale: number;
19
+ }[];
20
+ };
21
+ type BaseUSCNote = BaseUSCObject & {
22
+ lane: number;
23
+ size: number;
24
+ };
25
+ export type USCSingleNote = BaseUSCNote & {
26
+ type: 'single';
27
+ critical: boolean;
28
+ trace: boolean;
29
+ direction?: 'left' | 'up' | 'right' | 'none';
30
+ };
31
+ export type USCDamageNote = BaseUSCNote & {
32
+ type: 'damage';
33
+ };
34
+ export type USCConnectionStartNote = BaseUSCNote & {
35
+ type: 'start';
36
+ critical: boolean;
37
+ ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
38
+ judgeType: 'normal' | 'trace' | 'none';
39
+ };
40
+ export type USCConnectionTickNote = BaseUSCNote & {
41
+ type: 'tick';
42
+ critical?: boolean;
43
+ ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
44
+ };
45
+ export type USCConnectionAttachNote = Omit<BaseUSCObject, 'timeScaleGroup'> & {
46
+ type: 'attach';
47
+ critical?: boolean;
48
+ timeScaleGroup?: number;
49
+ };
50
+ export type USCConnectionEndNote = BaseUSCNote & {
51
+ type: 'end';
52
+ critical: boolean;
53
+ direction?: 'left' | 'up' | 'right';
54
+ judgeType: 'normal' | 'trace' | 'none';
55
+ };
56
+ export type USCSlideNote = {
57
+ type: 'slide';
58
+ critical: boolean;
59
+ connections: [
60
+ USCConnectionStartNote,
61
+ ...(USCConnectionTickNote | USCConnectionAttachNote)[],
62
+ USCConnectionEndNote
63
+ ];
64
+ };
65
+ export declare const USCColor: {
66
+ neutral: number;
67
+ red: number;
68
+ green: number;
69
+ blue: number;
70
+ yellow: number;
71
+ purple: number;
72
+ cyan: number;
73
+ black: number;
74
+ };
75
+ export type USCColor = keyof typeof USCColor;
76
+ export type USCGuideMidpointNote = BaseUSCNote & {
77
+ ease: 'out' | 'linear' | 'in' | 'inout' | 'outin';
78
+ };
79
+ export declare const USCFade: {
80
+ in: number;
81
+ out: number;
82
+ none: number;
83
+ };
84
+ export type USCFade = keyof typeof USCFade;
85
+ export type USCGuideNote = {
86
+ type: 'guide';
87
+ color: USCColor;
88
+ fade: USCFade;
89
+ midpoints: USCGuideMidpointNote[];
90
+ };
91
+ export {};