sonolus-pjsekai-js 1.0.7

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 ADDED
@@ -0,0 +1,153 @@
1
+ # Sonolus Pjsekai Js
2
+
3
+ Compiled and expanded version of [pjsekai-sonolus-engine](https://github.com/NonSpicyBurrito/sonolus-pjsekai-engine)
4
+
5
+ ## Custom Resources
6
+
7
+ ### Skin Sprites
8
+
9
+ | Name |
10
+ | --------------------------------------------- |
11
+ | `Sekai Stage` |
12
+ | `Sekai Note Red Left` |
13
+ | `Sekai Note Red Middle` |
14
+ | `Sekai Note Red Right` |
15
+ | `Sekai Note Green Left` |
16
+ | `Sekai Note Green Middle` |
17
+ | `Sekai Note Green Right` |
18
+ | `Sekai Note Yellow Left` |
19
+ | `Sekai Note Yellow Middle` |
20
+ | `Sekai Note Yellow Right` |
21
+ | `Sekai Note Cyan Left` |
22
+ | `Sekai Note Cyan Middle` |
23
+ | `Sekai Note Cyan Right` |
24
+ | `Sekai Diamond Green` |
25
+ | `Sekai Diamond Yellow` |
26
+ | `Sekai Trace Note Red` |
27
+ | `Sekai Trace Note Red Left` |
28
+ | `Sekai Trace Note Red Middle` |
29
+ | `Sekai Trace Note Red Right` |
30
+ | `Sekai Trace Note Green` |
31
+ | `Sekai Trace Note Green Left` |
32
+ | `Sekai Trace Note Green Middle` |
33
+ | `Sekai Trace Note Green Right` |
34
+ | `Sekai Trace Note Yellow` |
35
+ | `Sekai Trace Note Yellow Left` |
36
+ | `Sekai Trace Note Yellow Middle` |
37
+ | `Sekai Trace Note Yellow Right` |
38
+ | `Sekai Trace Diamond Red` |
39
+ | `Sekai Trace Diamond Green` |
40
+ | `Sekai Trace Diamond Yellow` |
41
+ | `Sekai Slide Connection Green` |
42
+ | `Sekai Slide Connection Green Active` |
43
+ | `Sekai Slide Connection Yellow` |
44
+ | `Sekai Slide Connection Yellow Active` |
45
+ | `Sekai Active Slide Connection Green` |
46
+ | `Sekai Active Slide Connection Green Active` |
47
+ | `Sekai Active Slide Connection Yellow` |
48
+ | `Sekai Active Slide Connection Yellow Active` |
49
+ | `Sekai Slot Glow Red` |
50
+ | `Sekai Slot Glow Green` |
51
+ | `Sekai Slot Glow Yellow` |
52
+ | `Sekai Slot Glow Cyan` |
53
+ | `Sekai Slot Red` |
54
+ | `Sekai Slot Green` |
55
+ | `Sekai Slot Yellow` |
56
+ | `Sekai Slot Cyan` |
57
+ | `Sekai Flick Arrow Red Up 1` |
58
+ | `Sekai Flick Arrow Red Up 2` |
59
+ | `Sekai Flick Arrow Red Up 3` |
60
+ | `Sekai Flick Arrow Red Up 4` |
61
+ | `Sekai Flick Arrow Red Up 5` |
62
+ | `Sekai Flick Arrow Red Up 6` |
63
+ | `Sekai Flick Arrow Red Left 1` |
64
+ | `Sekai Flick Arrow Red Left 2` |
65
+ | `Sekai Flick Arrow Red Left 3` |
66
+ | `Sekai Flick Arrow Red Left 4` |
67
+ | `Sekai Flick Arrow Red Left 5` |
68
+ | `Sekai Flick Arrow Red Left 6` |
69
+ | `Sekai Flick Arrow Yellow Up 1` |
70
+ | `Sekai Flick Arrow Yellow Up 2` |
71
+ | `Sekai Flick Arrow Yellow Up 3` |
72
+ | `Sekai Flick Arrow Yellow Up 4` |
73
+ | `Sekai Flick Arrow Yellow Up 5` |
74
+ | `Sekai Flick Arrow Yellow Up 6` |
75
+ | `Sekai Flick Arrow Yellow Left 1` |
76
+ | `Sekai Flick Arrow Yellow Left 2` |
77
+ | `Sekai Flick Arrow Yellow Left 3` |
78
+ | `Sekai Flick Arrow Yellow Left 4` |
79
+ | `Sekai Flick Arrow Yellow Left 5` |
80
+ | `Sekai Flick Arrow Yellow Left 6` |
81
+ | `Perfect` |
82
+ | `Great` |
83
+ | `Good` |
84
+ | `Bad` |
85
+ | `Miss` |
86
+ | `C0 - C9` |
87
+ | `AP0 - AP9` |
88
+ | `Combo` |
89
+ | `Fast` |
90
+ | `Late` |
91
+ | `Flick` |
92
+
93
+ ### Effect Clips
94
+
95
+ | Name |
96
+ | ---------------------- |
97
+ | `Sekai Tick` |
98
+ | `Sekai Trace` |
99
+ | `Sekai Critical Tap` |
100
+ | `Sekai Critical Trace` |
101
+ | `Sekai Critical Flick` |
102
+ | `Sekai Critical Hold` |
103
+ | `Sekai Critical Tick` |
104
+
105
+ ### Particle Effects
106
+
107
+ | Name |
108
+ | -------------------------------------- |
109
+ | `Sekai Trace Note Circular Green` |
110
+ | `Sekai Trace Note Linear Green` |
111
+ | `Sekai Trace Note Circular Yellow` |
112
+ | `Sekai Trace Note Linear Yellow` |
113
+ | `Sekai Note Lane Linear` |
114
+ | `Sekai Critical Lane Linear` |
115
+ | `Sekai Critical Flick Lane Linear` |
116
+ | `Sekai Critical Slide Circular Yellow` |
117
+ | `Sekai Critical Slide Linear Yellow` |
118
+ | `Sekai Critical Flick Circular Yellow` |
119
+ | `Sekai Critical Flick Linear Yellow` |
120
+
121
+ ## Documentation
122
+
123
+ ### `version`
124
+
125
+ Package version.
126
+
127
+ ### `databaseEngineItem`
128
+
129
+ Partial database engine item compatible with [sonolus-express](https://github.com/NonSpicyBurrito/sonolus-express).
130
+
131
+ ### `susToUSC(sus)`
132
+
133
+ Converts sus chart to USC (Universal Sekai Chart).
134
+
135
+ - `sus`: sus chart.
136
+
137
+ ### `uscToLevelData(usc, offset?)`
138
+
139
+ Converts USC (Universal Sekai Chart) to Level Data.
140
+
141
+ - `usc`: usc chart.
142
+ - `offset`: offset (default: `0`).
143
+
144
+ ### Assets
145
+
146
+ The following assets are exposed as package entry points:
147
+
148
+ - `EngineConfiguration`
149
+ - `EnginePlayData`
150
+ - `EngineWatchData`
151
+ - `EnginePreviewData`
152
+ - `EngineTutorialData`
153
+ - `EngineThumbnail`
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/index.cjs ADDED
@@ -0,0 +1,66 @@
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 __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
+ exports.databaseEngineItem = exports.version = exports.uscToLevelData = exports.susToUSC = void 0;
18
+ var convert_cjs_1 = require("./sus/convert.cjs");
19
+ Object.defineProperty(exports, "susToUSC", { enumerable: true, get: function () { return convert_cjs_1.susToUSC; } });
20
+ var convert_cjs_2 = require("./usc/convert.cjs");
21
+ Object.defineProperty(exports, "uscToLevelData", { enumerable: true, get: function () { return convert_cjs_2.uscToLevelData; } });
22
+ __exportStar(require("./usc/index.cjs"), exports);
23
+ exports.version = '1.0.7';
24
+ exports.databaseEngineItem = {
25
+ name: 'prosekaR',
26
+ version: 13,
27
+ title: {
28
+ en: 'ProSeka R',
29
+ ja: 'プロセカ R',
30
+ ko: '프로세카 R',
31
+ zhs: '世界计划 R',
32
+ zht: '世界計劃 R',
33
+ },
34
+ subtitle: {
35
+ en: 'ProSeka Rush',
36
+ ja: 'プロセカ ラッシュ',
37
+ ko: '프로세카 러쉬',
38
+ zhs: '世界计划 匆忙',
39
+ zht: '世界計劃 匆忙',
40
+ },
41
+ author: {
42
+ en: 'Hyeon2#7895',
43
+ },
44
+ description: {
45
+ en: [
46
+ 'A recreation of Project Sekai: Colorful Stage! engine in Sonolus.',
47
+ `Version: ${exports.version}`,
48
+ '',
49
+ 'Forked from the pjsekai engine by Burrito#1000.',
50
+ 'https://github.com/NonSpicyBurrito/sonolus-pjsekai-engine',
51
+ '',
52
+ 'Github:',
53
+ 'https://github.com/hyeon2006/sonolus-pjsekai-js'
54
+ ].join('\n'),
55
+ ko: [
56
+ '프로젝트 세카이: 컬러풀 스테이지! 엔진을 Sonolus로 재현했습니다.',
57
+ `버전: ${exports.version}`,
58
+ '',
59
+ 'Burrito#1000의 pjsekai 엔진에서 포크되었습니다.',
60
+ 'https://github.com/NonSpicyBurrito/sonolus-pjsekai-engine',
61
+ '',
62
+ '깃허브:',
63
+ 'https://github.com/hyeon2006/sonolus-pjsekai-js'
64
+ ].join('\n'),
65
+ },
66
+ };
@@ -0,0 +1,29 @@
1
+ export { susToUSC } from './sus/convert.cjs';
2
+ export { uscToLevelData } from './usc/convert.cjs';
3
+ export * from './usc/index.cjs';
4
+ export declare const version = "1.0.7";
5
+ export declare const databaseEngineItem: {
6
+ readonly name: "prosekaR";
7
+ readonly version: 13;
8
+ readonly title: {
9
+ readonly en: "ProSeka R";
10
+ readonly ja: "プロセカ R";
11
+ readonly ko: "프로세카 R";
12
+ readonly zhs: "世界计划 R";
13
+ readonly zht: "世界計劃 R";
14
+ };
15
+ readonly subtitle: {
16
+ readonly en: "ProSeka Rush";
17
+ readonly ja: "プロセカ ラッシュ";
18
+ readonly ko: "프로세카 러쉬";
19
+ readonly zhs: "世界计划 匆忙";
20
+ readonly zht: "世界計劃 匆忙";
21
+ };
22
+ readonly author: {
23
+ readonly en: "Hyeon2#7895";
24
+ };
25
+ readonly description: {
26
+ readonly en: string;
27
+ readonly ko: string;
28
+ };
29
+ };
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyze = void 0;
4
+ const analyze = (sus) => {
5
+ const { lines, measureChanges, meta } = parse(sus);
6
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
7
+ const offset = -+(meta.get('WAVEOFFSET') || '0');
8
+ if (Number.isNaN(offset))
9
+ throw new Error('Unexpected offset');
10
+ const ticksPerBeat = getTicksPerBeat(meta);
11
+ if (!ticksPerBeat)
12
+ throw new Error('Missing or unexpected ticks per beat');
13
+ const barLengths = getBarLengths(lines, measureChanges);
14
+ const toTick = getToTick(barLengths, ticksPerBeat);
15
+ const bpms = new Map();
16
+ const bpmChanges = [];
17
+ const timeScaleChanges = [];
18
+ const tapNotes = [];
19
+ const directionalNotes = [];
20
+ const streams = new Map();
21
+ for (const [index, line] of lines.entries()) {
22
+ const [header, data] = line;
23
+ const measureOffset = measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0;
24
+ // Time Scale Changes
25
+ if (header.length === 5 && header.startsWith('TIL')) {
26
+ timeScaleChanges.push(...toTimeScaleChanges(line, toTick));
27
+ continue;
28
+ }
29
+ // BPM
30
+ if (header.length === 5 && header.startsWith('BPM')) {
31
+ bpms.set(header.substring(3), +data);
32
+ continue;
33
+ }
34
+ // BPM Changes
35
+ if (header.length === 5 && header.endsWith('08')) {
36
+ bpmChanges.push(...toBpmChanges(line, measureOffset, bpms, toTick));
37
+ continue;
38
+ }
39
+ // Tap Notes
40
+ if (header.length === 5 && header[3] === '1') {
41
+ tapNotes.push(...toNotes(line, measureOffset, toTick));
42
+ continue;
43
+ }
44
+ // Streams
45
+ if (header.length === 6 && (header[3] === '3' || header[3] === '9')) {
46
+ const key = `${header[5]}-${header[3]}`;
47
+ const stream = streams.get(key);
48
+ if (stream) {
49
+ stream.notes.push(...toNotes(line, measureOffset, toTick));
50
+ }
51
+ else {
52
+ streams.set(key, {
53
+ type: +header[3],
54
+ notes: toNotes(line, measureOffset, toTick),
55
+ });
56
+ }
57
+ continue;
58
+ }
59
+ // Directional Notes
60
+ if (header.length === 5 && header[3] === '5') {
61
+ directionalNotes.push(...toNotes(line, measureOffset, toTick));
62
+ continue;
63
+ }
64
+ }
65
+ const slides = [...streams.values()].flatMap(toSlides);
66
+ return {
67
+ offset,
68
+ ticksPerBeat,
69
+ timeScaleChanges,
70
+ bpmChanges,
71
+ tapNotes,
72
+ directionalNotes,
73
+ slides,
74
+ };
75
+ };
76
+ exports.analyze = analyze;
77
+ const parse = (sus) => {
78
+ const lines = [];
79
+ const measureChanges = [];
80
+ const meta = new Map();
81
+ for (const line of sus
82
+ .split('\n')
83
+ .map((line) => line.trim())
84
+ .filter((line) => line.startsWith('#'))) {
85
+ const isLine = line.includes(':');
86
+ const index = line.indexOf(isLine ? ':' : ' ');
87
+ if (index === -1)
88
+ continue;
89
+ const left = line.substring(1, index).trim();
90
+ const right = line.substring(index + 1).trim();
91
+ if (isLine) {
92
+ lines.push([left, right]);
93
+ }
94
+ else if (left === 'MEASUREBS') {
95
+ measureChanges.unshift([lines.length, +right]);
96
+ }
97
+ else {
98
+ meta.set(left, right);
99
+ }
100
+ }
101
+ return {
102
+ lines,
103
+ measureChanges,
104
+ meta,
105
+ };
106
+ };
107
+ const getTicksPerBeat = (meta) => {
108
+ const request = meta.get('REQUEST');
109
+ if (!request)
110
+ return;
111
+ if (!request.startsWith('"ticks_per_beat ') || !request.endsWith('"'))
112
+ return;
113
+ return +request.slice(16, -1);
114
+ };
115
+ const getBarLengths = (lines, measureChanges) => {
116
+ const barLengths = [];
117
+ for (const [index, line] of lines.entries()) {
118
+ const [header, data] = line;
119
+ if (header.length !== 5)
120
+ continue;
121
+ if (!header.endsWith('02'))
122
+ continue;
123
+ const measure = +header.substring(0, 3) +
124
+ (measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0);
125
+ if (Number.isNaN(measure))
126
+ continue;
127
+ barLengths.push({ measure, length: +data });
128
+ }
129
+ return barLengths;
130
+ };
131
+ const getToTick = (barLengths, ticksPerBeat) => {
132
+ let ticks = 0;
133
+ const bars = barLengths
134
+ .sort((a, b) => a.measure - b.measure)
135
+ .map(({ measure, length }, i, values) => {
136
+ if (i) {
137
+ const prev = values[i - 1];
138
+ ticks += (measure - prev.measure) * prev.length * ticksPerBeat;
139
+ }
140
+ return { measure, ticksPerMeasure: length * ticksPerBeat, ticks };
141
+ })
142
+ .reverse();
143
+ return (measure, p, q) => {
144
+ const bar = bars.find((bar) => measure >= bar.measure);
145
+ if (!bar)
146
+ throw new Error('Unexpected missing bar');
147
+ return (bar.ticks +
148
+ (measure - bar.measure) * bar.ticksPerMeasure +
149
+ (p * bar.ticksPerMeasure) / q);
150
+ };
151
+ };
152
+ const toBpmChanges = (line, measureOffset, bpms, toTick) => toRaws(line, measureOffset, toTick).map(({ tick, value }) => ({
153
+ tick,
154
+ bpm: bpms.get(value) ?? 0,
155
+ }));
156
+ const toTimeScaleChanges = ([, data], toTick) => {
157
+ if (!data.startsWith('"') || !data.endsWith('"'))
158
+ throw new Error('Unexpected time scale changes');
159
+ return data
160
+ .slice(1, -1)
161
+ .split(',')
162
+ .map((segment) => segment.trim())
163
+ .filter((segment) => !!segment)
164
+ .map((segment) => {
165
+ const [l, rest] = segment.split("'");
166
+ const [m, r] = rest.split(':');
167
+ const measure = +l;
168
+ const tick = +m;
169
+ const timeScale = +r;
170
+ if (Number.isNaN(measure) || Number.isNaN(tick) || Number.isNaN(timeScale))
171
+ throw new Error('Unexpected time scale change');
172
+ return {
173
+ tick: toTick(measure, 0, 1) + tick,
174
+ timeScale,
175
+ };
176
+ })
177
+ .sort((a, b) => a.tick - b.tick);
178
+ };
179
+ const toNotes = (line, measureOffset, toTick) => {
180
+ const [header] = line;
181
+ const lane = parseInt(header[4], 36);
182
+ return toRaws(line, measureOffset, toTick).map(({ tick, value }) => {
183
+ const width = parseInt(value[1], 36);
184
+ return {
185
+ tick,
186
+ lane,
187
+ width,
188
+ type: parseInt(value[0], 36),
189
+ };
190
+ });
191
+ };
192
+ const toSlides = (stream) => {
193
+ const slides = [];
194
+ let notes;
195
+ for (const note of stream.notes.sort((a, b) => a.tick - b.tick)) {
196
+ if (!notes) {
197
+ notes = [];
198
+ slides.push({
199
+ type: stream.type,
200
+ notes,
201
+ });
202
+ }
203
+ notes.push(note);
204
+ if (note.type === 2) {
205
+ notes = undefined;
206
+ }
207
+ }
208
+ return slides;
209
+ };
210
+ const toRaws = ([header, data], measureOffset, toTick) => {
211
+ const measure = +header.substring(0, 3) + measureOffset;
212
+ return (data.match(/.{2}/g) ?? [])
213
+ .map((value, i, values) => value !== '00' && {
214
+ tick: toTick(measure, i, values.length),
215
+ value,
216
+ })
217
+ .filter((object) => !!object);
218
+ };
@@ -0,0 +1,28 @@
1
+ export type TimeScaleChangeObject = {
2
+ tick: number;
3
+ timeScale: number;
4
+ };
5
+ export type BpmChangeObject = {
6
+ tick: number;
7
+ bpm: number;
8
+ };
9
+ export type NoteObject = {
10
+ tick: number;
11
+ lane: number;
12
+ width: number;
13
+ type: number;
14
+ };
15
+ export type SlideObject = {
16
+ type: number;
17
+ notes: NoteObject[];
18
+ };
19
+ export type Score = {
20
+ offset: number;
21
+ ticksPerBeat: number;
22
+ timeScaleChanges: TimeScaleChangeObject[];
23
+ bpmChanges: BpmChangeObject[];
24
+ tapNotes: NoteObject[];
25
+ directionalNotes: NoteObject[];
26
+ slides: SlideObject[];
27
+ };
28
+ export declare const analyze: (sus: string) => Score;
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.susToUSC = void 0;
4
+ const analyze_cjs_1 = require("./analyze.cjs");
5
+ const susToUSC = (sus) => {
6
+ const score = (0, analyze_cjs_1.analyze)(sus);
7
+ const flickMods = new Map();
8
+ const traceMods = new Set();
9
+ const criticalMods = new Set();
10
+ const tickRemoveMods = new Set();
11
+ const slideStartEndRemoveMods = new Set();
12
+ const easeMods = new Map();
13
+ const preventSingles = new Set();
14
+ const dedupeSingles = new Set();
15
+ const dedupeSlides = new Map();
16
+ for (const slide of score.slides) {
17
+ if (slide.type !== 3)
18
+ continue;
19
+ for (const note of slide.notes) {
20
+ const key = getKey(note);
21
+ switch (note.type) {
22
+ case 1:
23
+ case 2:
24
+ case 3:
25
+ case 5:
26
+ preventSingles.add(key);
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ for (const note of score.directionalNotes) {
32
+ const key = getKey(note);
33
+ switch (note.type) {
34
+ case 1:
35
+ flickMods.set(key, 'up');
36
+ break;
37
+ case 3:
38
+ flickMods.set(key, 'left');
39
+ break;
40
+ case 4:
41
+ flickMods.set(key, 'right');
42
+ break;
43
+ case 2:
44
+ easeMods.set(key, 'in');
45
+ break;
46
+ case 5:
47
+ case 6:
48
+ easeMods.set(key, 'out');
49
+ break;
50
+ }
51
+ }
52
+ for (const note of score.tapNotes) {
53
+ const key = getKey(note);
54
+ switch (note.type) {
55
+ case 2:
56
+ criticalMods.add(key);
57
+ break;
58
+ case 5:
59
+ traceMods.add(key);
60
+ break;
61
+ case 6:
62
+ traceMods.add(key);
63
+ criticalMods.add(key);
64
+ break;
65
+ case 3:
66
+ tickRemoveMods.add(key);
67
+ break;
68
+ case 7:
69
+ slideStartEndRemoveMods.add(key);
70
+ break;
71
+ case 8:
72
+ criticalMods.add(key);
73
+ slideStartEndRemoveMods.add(key);
74
+ break;
75
+ }
76
+ }
77
+ const objects = [];
78
+ for (const timeScaleChange of score.timeScaleChanges) {
79
+ objects.push({
80
+ type: 'timeScale',
81
+ beat: timeScaleChange.tick / score.ticksPerBeat,
82
+ timeScale: timeScaleChange.timeScale,
83
+ });
84
+ }
85
+ for (const bpmChange of score.bpmChanges) {
86
+ objects.push({
87
+ type: 'bpm',
88
+ beat: bpmChange.tick / score.ticksPerBeat,
89
+ bpm: bpmChange.bpm,
90
+ });
91
+ }
92
+ for (const note of score.tapNotes) {
93
+ if (note.lane <= 1 || note.lane >= 14)
94
+ continue;
95
+ if (note.type !== 1 && note.type !== 2 && note.type !== 5 && note.type !== 6)
96
+ continue;
97
+ const key = getKey(note);
98
+ if (preventSingles.has(key))
99
+ continue;
100
+ if (dedupeSingles.has(key))
101
+ continue;
102
+ dedupeSingles.add(key);
103
+ const object = {
104
+ type: 'single',
105
+ beat: note.tick / score.ticksPerBeat,
106
+ lane: note.lane - 8 + note.width / 2,
107
+ size: note.width / 2,
108
+ trace: note.type === 5 || note.type === 6,
109
+ critical: note.type === 2 || note.type === 6,
110
+ };
111
+ const flickMod = flickMods.get(key);
112
+ if (flickMod)
113
+ object.direction = flickMod;
114
+ objects.push(object);
115
+ }
116
+ for (const slide of score.slides) {
117
+ const startNote = slide.notes.find(({ type }) => type === 1 || type === 2);
118
+ if (!startNote)
119
+ continue;
120
+ const object = {
121
+ type: 'slide',
122
+ active: slide.type === 3,
123
+ critical: criticalMods.has(getKey(startNote)),
124
+ connections: [],
125
+ };
126
+ for (const note of slide.notes) {
127
+ const key = getKey(note);
128
+ const beat = note.tick / score.ticksPerBeat;
129
+ const lane = note.lane - 8 + note.width / 2;
130
+ const size = note.width / 2;
131
+ const trace = traceMods.has(key);
132
+ const critical = object.critical || criticalMods.has(key);
133
+ const ease = easeMods.get(key) ?? 'linear';
134
+ switch (note.type) {
135
+ case 1: {
136
+ if (!object.active || slideStartEndRemoveMods.has(key)) {
137
+ const connection = {
138
+ type: 'ignore',
139
+ beat,
140
+ lane,
141
+ size,
142
+ ease,
143
+ };
144
+ object.connections.push(connection);
145
+ }
146
+ else {
147
+ const connection = {
148
+ type: 'start',
149
+ beat,
150
+ lane,
151
+ size,
152
+ trace,
153
+ critical,
154
+ ease: easeMods.get(key) ?? 'linear',
155
+ };
156
+ object.connections.push(connection);
157
+ }
158
+ break;
159
+ }
160
+ case 2: {
161
+ if (!object.active || slideStartEndRemoveMods.has(key)) {
162
+ const connection = {
163
+ type: 'ignore',
164
+ beat,
165
+ lane,
166
+ size,
167
+ ease,
168
+ };
169
+ object.connections.push(connection);
170
+ }
171
+ else {
172
+ const connection = {
173
+ type: 'end',
174
+ beat,
175
+ lane,
176
+ size,
177
+ trace,
178
+ critical,
179
+ };
180
+ const flickMod = flickMods.get(key);
181
+ if (flickMod)
182
+ connection.direction = flickMod;
183
+ object.connections.push(connection);
184
+ }
185
+ break;
186
+ }
187
+ case 3: {
188
+ if (tickRemoveMods.has(key)) {
189
+ const connection = {
190
+ type: 'attach',
191
+ beat,
192
+ critical,
193
+ };
194
+ object.connections.push(connection);
195
+ }
196
+ else {
197
+ const connection = {
198
+ type: 'tick',
199
+ beat,
200
+ lane,
201
+ size,
202
+ trace,
203
+ critical,
204
+ ease,
205
+ };
206
+ object.connections.push(connection);
207
+ }
208
+ break;
209
+ }
210
+ case 5: {
211
+ if (tickRemoveMods.has(key))
212
+ break;
213
+ const connection = {
214
+ type: 'ignore',
215
+ beat,
216
+ lane,
217
+ size,
218
+ ease,
219
+ };
220
+ object.connections.push(connection);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ objects.push(object);
226
+ if (!object.active)
227
+ continue;
228
+ const key = getKey(startNote);
229
+ const dupe = dedupeSlides.get(key);
230
+ if (dupe)
231
+ objects.splice(objects.indexOf(dupe), 1);
232
+ dedupeSlides.set(key, object);
233
+ }
234
+ return {
235
+ offset: score.offset,
236
+ objects,
237
+ };
238
+ };
239
+ exports.susToUSC = susToUSC;
240
+ const getKey = (note) => `${note.lane}-${note.tick}`;
@@ -0,0 +1,2 @@
1
+ import { USC } from '../usc/index.cjs';
2
+ export declare const susToUSC: (sus: string) => USC;
Binary file
@@ -0,0 +1,379 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uscToLevelData = void 0;
4
+ const core_1 = require("@sonolus/core");
5
+ const uscToLevelData = (usc, offset = 0) => {
6
+ const entities = [];
7
+ const timeToIntermediates = new Map();
8
+ const intermediateToRef = new Map();
9
+ const intermediateToEntity = new Map();
10
+ let i = 0;
11
+ const getRef = (intermediate) => {
12
+ let ref = intermediateToRef.get(intermediate);
13
+ if (ref)
14
+ return ref;
15
+ ref = (i++).toString(36);
16
+ intermediateToRef.set(intermediate, ref);
17
+ const entity = intermediateToEntity.get(intermediate);
18
+ if (entity)
19
+ entity.name = ref;
20
+ return ref;
21
+ };
22
+ const append = (intermediate) => {
23
+ const entity = {
24
+ archetype: intermediate.archetype,
25
+ data: [],
26
+ };
27
+ if (intermediate.sim) {
28
+ const beat = intermediate.data[core_1.EngineArchetypeDataName.Beat];
29
+ if (typeof beat !== 'number')
30
+ throw new Error('Unexpected beat');
31
+ const intermediates = timeToIntermediates.get(beat);
32
+ if (intermediates) {
33
+ intermediates.push(intermediate);
34
+ }
35
+ else {
36
+ timeToIntermediates.set(beat, [intermediate]);
37
+ }
38
+ }
39
+ const ref = intermediateToRef.get(intermediate);
40
+ if (ref)
41
+ entity.name = ref;
42
+ intermediateToEntity.set(intermediate, entity);
43
+ entities.push(entity);
44
+ for (const [name, value] of Object.entries(intermediate.data)) {
45
+ if (value === undefined)
46
+ continue;
47
+ if (typeof value === 'number') {
48
+ entity.data.push({
49
+ name,
50
+ value,
51
+ });
52
+ }
53
+ else {
54
+ entity.data.push({
55
+ name,
56
+ ref: getRef(value),
57
+ });
58
+ }
59
+ }
60
+ };
61
+ append({
62
+ archetype: 'Initialization',
63
+ data: {},
64
+ sim: false,
65
+ });
66
+ append({
67
+ archetype: 'Stage',
68
+ data: {},
69
+ sim: false,
70
+ });
71
+ for (const object of usc.objects) {
72
+ handlers[object.type](object, append);
73
+ }
74
+ for (const intermediates of timeToIntermediates.values()) {
75
+ for (let i = 1; i < intermediates.length; i++) {
76
+ append({
77
+ archetype: 'SimLine',
78
+ data: {
79
+ a: intermediates[i - 1],
80
+ b: intermediates[i],
81
+ },
82
+ sim: false,
83
+ });
84
+ }
85
+ }
86
+ return {
87
+ bgmOffset: usc.offset + offset,
88
+ entities,
89
+ };
90
+ };
91
+ exports.uscToLevelData = uscToLevelData;
92
+ const directions = {
93
+ left: -1,
94
+ up: 0,
95
+ right: 1,
96
+ };
97
+ const eases = {
98
+ out: -1,
99
+ linear: 0,
100
+ in: 1,
101
+ };
102
+ const bpm = (object, append) => {
103
+ append({
104
+ archetype: core_1.EngineArchetypeName.BpmChange,
105
+ data: {
106
+ [core_1.EngineArchetypeDataName.Beat]: object.beat,
107
+ [core_1.EngineArchetypeDataName.Bpm]: object.bpm,
108
+ },
109
+ sim: false,
110
+ });
111
+ };
112
+ const timeScale = (object, append) => {
113
+ append({
114
+ archetype: core_1.EngineArchetypeName.TimeScaleChange,
115
+ data: {
116
+ [core_1.EngineArchetypeDataName.Beat]: object.beat,
117
+ [core_1.EngineArchetypeDataName.TimeScale]: object.timeScale,
118
+ },
119
+ sim: false,
120
+ });
121
+ };
122
+ const single = (object, append) => {
123
+ const intermediate = {
124
+ archetype: object.direction
125
+ ? object.trace
126
+ ? object.critical
127
+ ? 'CriticalTraceFlickNote'
128
+ : 'NormalTraceFlickNote'
129
+ : object.critical
130
+ ? 'CriticalFlickNote'
131
+ : 'NormalFlickNote'
132
+ : object.trace
133
+ ? object.critical
134
+ ? 'CriticalTraceNote'
135
+ : 'NormalTraceNote'
136
+ : object.critical
137
+ ? 'CriticalTapNote'
138
+ : 'NormalTapNote',
139
+ data: {
140
+ [core_1.EngineArchetypeDataName.Beat]: object.beat,
141
+ lane: object.lane,
142
+ size: object.size,
143
+ direction: object.direction && directions[object.direction],
144
+ },
145
+ sim: true,
146
+ };
147
+ append(intermediate);
148
+ };
149
+ const slide = (object, append) => {
150
+ const cis = [];
151
+ const joints = [];
152
+ const attaches = [];
153
+ const ends = [];
154
+ const connections = getConnections(object);
155
+ for (const [i, connection] of connections.entries()) {
156
+ if (i === 0) {
157
+ switch (connection.type) {
158
+ case 'start': {
159
+ const ci = {
160
+ archetype: connection.trace
161
+ ? connection.critical
162
+ ? 'CriticalSlideTraceNote'
163
+ : 'NormalSlideTraceNote'
164
+ : connection.critical
165
+ ? 'CriticalSlideStartNote'
166
+ : 'NormalSlideStartNote',
167
+ data: {
168
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
169
+ lane: connection.lane,
170
+ size: connection.size,
171
+ },
172
+ sim: true,
173
+ ease: connection.ease,
174
+ };
175
+ cis.push(ci);
176
+ joints.push(ci);
177
+ continue;
178
+ }
179
+ case 'ignore': {
180
+ const ci = {
181
+ archetype: 'IgnoredSlideTickNote',
182
+ data: {
183
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
184
+ lane: connection.lane,
185
+ size: connection.size,
186
+ },
187
+ sim: false,
188
+ ease: connection.ease,
189
+ };
190
+ cis.push(ci);
191
+ joints.push(ci);
192
+ continue;
193
+ }
194
+ default:
195
+ throw new Error('Unexpected slide start');
196
+ }
197
+ }
198
+ if (i === connections.length - 1) {
199
+ switch (connection.type) {
200
+ case 'end': {
201
+ const ci = {
202
+ archetype: connection.direction
203
+ ? connection.trace
204
+ ? connection.critical
205
+ ? 'CriticalTraceFlickNote'
206
+ : 'NormalTraceFlickNote'
207
+ : connection.critical
208
+ ? 'CriticalSlideEndFlickNote'
209
+ : 'NormalSlideEndFlickNote'
210
+ : connection.trace
211
+ ? connection.critical
212
+ ? 'CriticalSlideEndTraceNote'
213
+ : 'NormalSlideEndTraceNote'
214
+ : connection.critical
215
+ ? 'CriticalSlideEndNote'
216
+ : 'NormalSlideEndNote',
217
+ data: {
218
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
219
+ lane: connection.lane,
220
+ size: connection.size,
221
+ direction: connection.direction && directions[connection.direction],
222
+ },
223
+ sim: true,
224
+ };
225
+ cis.push(ci);
226
+ joints.push(ci);
227
+ ends.push(ci);
228
+ continue;
229
+ }
230
+ case 'ignore': {
231
+ const ci = {
232
+ archetype: 'IgnoredSlideTickNote',
233
+ data: {
234
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
235
+ lane: connection.lane,
236
+ size: connection.size,
237
+ },
238
+ sim: false,
239
+ ease: connection.ease,
240
+ };
241
+ cis.push(ci);
242
+ joints.push(ci);
243
+ continue;
244
+ }
245
+ default:
246
+ throw new Error('Unexpected slide end');
247
+ }
248
+ }
249
+ switch (connection.type) {
250
+ case 'ignore': {
251
+ const ci = {
252
+ archetype: 'IgnoredSlideTickNote',
253
+ data: {
254
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
255
+ lane: connection.lane,
256
+ size: connection.size,
257
+ },
258
+ sim: false,
259
+ ease: connection.ease,
260
+ };
261
+ cis.push(ci);
262
+ joints.push(ci);
263
+ break;
264
+ }
265
+ case 'tick': {
266
+ const ci = {
267
+ archetype: connection.trace
268
+ ? connection.critical
269
+ ? 'CriticalSlideTraceNote'
270
+ : 'NormalSlideTraceNote'
271
+ : connection.critical
272
+ ? 'CriticalSlideTickNote'
273
+ : 'NormalSlideTickNote',
274
+ data: {
275
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
276
+ lane: connection.lane,
277
+ size: connection.size,
278
+ },
279
+ sim: false,
280
+ ease: connection.ease,
281
+ };
282
+ cis.push(ci);
283
+ joints.push(ci);
284
+ break;
285
+ }
286
+ case 'hidden': {
287
+ const ci = {
288
+ archetype: 'HiddenSlideTickNote',
289
+ data: {
290
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
291
+ },
292
+ sim: false,
293
+ };
294
+ cis.push(ci);
295
+ attaches.push(ci);
296
+ break;
297
+ }
298
+ case 'attach': {
299
+ const ci = {
300
+ archetype: connection.critical
301
+ ? 'CriticalAttachedSlideTickNote'
302
+ : 'NormalAttachedSlideTickNote',
303
+ data: {
304
+ [core_1.EngineArchetypeDataName.Beat]: connection.beat,
305
+ },
306
+ sim: false,
307
+ };
308
+ cis.push(ci);
309
+ attaches.push(ci);
310
+ break;
311
+ }
312
+ default:
313
+ throw new Error('Unexpected slide tick');
314
+ }
315
+ }
316
+ const connectors = [];
317
+ const start = cis[0];
318
+ const end = cis[cis.length - 1];
319
+ for (const [i, joint] of joints.entries()) {
320
+ if (i === 0)
321
+ continue;
322
+ const head = joints[i - 1];
323
+ if (!head.ease)
324
+ throw new Error('Unexpected missing ease');
325
+ connectors.push({
326
+ archetype: object.active
327
+ ? object.critical
328
+ ? 'CriticalActiveSlideConnector'
329
+ : 'NormalActiveSlideConnector'
330
+ : object.critical
331
+ ? 'CriticalSlideConnector'
332
+ : 'NormalSlideConnector',
333
+ data: {
334
+ start,
335
+ end,
336
+ head,
337
+ tail: joint,
338
+ ease: eases[head.ease],
339
+ },
340
+ sim: false,
341
+ });
342
+ }
343
+ for (const attach of attaches) {
344
+ const index = cis.indexOf(attach);
345
+ const tailIndex = joints.findIndex((c) => cis.indexOf(c) > index);
346
+ attach.data.attach = connectors[tailIndex - 1];
347
+ }
348
+ for (const end of ends) {
349
+ end.data.slide = connectors[connectors.length - 1];
350
+ }
351
+ for (const ci of cis) {
352
+ append(ci);
353
+ }
354
+ for (const connector of connectors) {
355
+ append(connector);
356
+ }
357
+ };
358
+ const handlers = {
359
+ bpm,
360
+ single,
361
+ timeScale,
362
+ slide,
363
+ };
364
+ const getConnections = (object) => {
365
+ if (!object.active)
366
+ return object.connections;
367
+ const connections = [...object.connections];
368
+ const beats = connections.map(({ beat }) => beat).sort((a, b) => a - b);
369
+ const min = beats[0];
370
+ const max = beats[beats.length - 1];
371
+ const start = Math.max(Math.ceil(min / 0.5) * 0.5, Math.floor(min / 0.5 + 1) * 0.5);
372
+ for (let beat = start; beat < max; beat += 0.5) {
373
+ connections.push({
374
+ type: 'hidden',
375
+ beat,
376
+ });
377
+ }
378
+ return connections.sort((a, b) => a.beat - b.beat);
379
+ };
@@ -0,0 +1,3 @@
1
+ import { LevelData } from '@sonolus/core';
2
+ import { USC } from './index.cjs';
3
+ export declare const uscToLevelData: (usc: USC, offset?: number) => LevelData;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,66 @@
1
+ export type USC = {
2
+ offset: number;
3
+ objects: USCObject[];
4
+ };
5
+ export type USCObject = USCBpmChange | USCTimeScaleChange | USCSingleNote | USCSlideNote;
6
+ type BaseUSCObject = {
7
+ beat: number;
8
+ };
9
+ export type USCBpmChange = BaseUSCObject & {
10
+ type: 'bpm';
11
+ bpm: number;
12
+ };
13
+ export type USCTimeScaleChange = BaseUSCObject & {
14
+ type: 'timeScale';
15
+ timeScale: number;
16
+ };
17
+ type BaseUSCNote = BaseUSCObject & {
18
+ lane: number;
19
+ size: number;
20
+ };
21
+ export type USCSingleNote = BaseUSCNote & {
22
+ type: 'single';
23
+ trace: boolean;
24
+ critical: boolean;
25
+ direction?: 'left' | 'up' | 'right';
26
+ };
27
+ export type USCConnectionStartNote = BaseUSCNote & {
28
+ type: 'start';
29
+ trace: boolean;
30
+ critical: boolean;
31
+ ease: 'out' | 'linear' | 'in';
32
+ };
33
+ export type USCConnectionIgnoreNote = BaseUSCNote & {
34
+ type: 'ignore';
35
+ ease: 'out' | 'linear' | 'in';
36
+ };
37
+ export type USCConnectionTickNote = BaseUSCNote & {
38
+ type: 'tick';
39
+ trace: boolean;
40
+ critical: boolean;
41
+ ease: 'out' | 'linear' | 'in';
42
+ };
43
+ export type USCConnectionHiddenNote = BaseUSCObject & {
44
+ type: 'hidden';
45
+ };
46
+ export type USCConnectionAttachNote = BaseUSCObject & {
47
+ type: 'attach';
48
+ critical: boolean;
49
+ };
50
+ export type USCConnectionEndNote = BaseUSCNote & {
51
+ type: 'end';
52
+ trace: boolean;
53
+ critical: boolean;
54
+ direction?: 'left' | 'up' | 'right';
55
+ };
56
+ export type USCSlideNote = {
57
+ type: 'slide';
58
+ active: boolean;
59
+ critical: boolean;
60
+ connections: [
61
+ USCConnectionStartNote | USCConnectionIgnoreNote,
62
+ ...(USCConnectionIgnoreNote | USCConnectionTickNote | USCConnectionHiddenNote | USCConnectionAttachNote)[],
63
+ USCConnectionEndNote | USCConnectionIgnoreNote
64
+ ];
65
+ };
66
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "sonolus-pjsekai-js",
3
+ "version": "1.0.7",
4
+ "description": "A recreation of Project Sekai: Colorful Stage! engine in Sonolus",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": "./dist/index.cjs",
11
+ "./EngineConfiguration": "./dist/EngineConfiguration",
12
+ "./EnginePlayData": "./dist/EnginePlayData",
13
+ "./EngineWatchData": "./dist/EngineWatchData",
14
+ "./EnginePreviewData": "./dist/EnginePreviewData",
15
+ "./EngineTutorialData": "./dist/EngineTutorialData",
16
+ "./EngineThumbnail": "./dist/thumbnail.png"
17
+ },
18
+ "scripts": {
19
+ "dev:play": "sonolus-cli --dev ./play",
20
+ "dev:watch": "sonolus-cli --dev ./watch",
21
+ "dev:preview": "sonolus-cli --dev ./preview",
22
+ "dev:tutorial": "sonolus-cli --dev ./tutorial",
23
+ "build": "tsc -p ./lib && sonolus-cli --build ./play && sonolus-cli --build ./watch && sonolus-cli --build ./preview && sonolus-cli --build ./tutorial && node ./lib/build.mjs"
24
+ },
25
+ "devDependencies": {
26
+ "@sonolus/core": "~7.13.1",
27
+ "@sonolus/sonolus.js": "~9.5.5",
28
+ "typescript": "~5.8.3"
29
+ },
30
+ "dependencies": {
31
+ "sonolus-pjsekai-js": "file:"
32
+ }
33
+ }