rhythia-api 185.0.0 → 187.0.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/.prettierrc.json +6 -6
- package/api/addCollectionMap.ts +82 -82
- package/api/approveMap.ts +78 -78
- package/api/chartPublicStats.ts +32 -32
- package/api/createBeatmap.ts +168 -168
- package/api/createBeatmapPage.ts +64 -64
- package/api/createClan.ts +81 -81
- package/api/createCollection.ts +58 -58
- package/api/deleteBeatmapPage.ts +77 -77
- package/api/deleteCollection.ts +59 -59
- package/api/deleteCollectionMap.ts +71 -71
- package/api/editAboutMe.ts +91 -91
- package/api/editClan.ts +90 -90
- package/api/editCollection.ts +77 -77
- package/api/editProfile.ts +123 -123
- package/api/getAvatarUploadUrl.ts +85 -85
- package/api/getBadgedUsers.ts +56 -56
- package/api/getBeatmapComments.ts +57 -57
- package/api/getBeatmapPage.ts +106 -106
- package/api/getBeatmapPageById.ts +99 -99
- package/api/getBeatmapStarRating.ts +53 -53
- package/api/getBeatmaps.ts +159 -159
- package/api/getClan.ts +77 -77
- package/api/getCollection.ts +130 -130
- package/api/getCollections.ts +132 -130
- package/api/getLeaderboard.ts +136 -136
- package/api/getMapUploadUrl.ts +93 -93
- package/api/getPassToken.ts +55 -55
- package/api/getProfile.ts +146 -146
- package/api/getPublicStats.ts +180 -180
- package/api/getRawStarRating.ts +57 -57
- package/api/getScore.ts +85 -85
- package/api/getTimestamp.ts +23 -23
- package/api/getUserScores.ts +175 -175
- package/api/nominateMap.ts +82 -82
- package/api/postBeatmapComment.ts +59 -59
- package/api/rankMapsArchive.ts +64 -64
- package/api/searchUsers.ts +56 -56
- package/api/setPasskey.ts +59 -59
- package/api/submitScore.ts +433 -433
- package/api/updateBeatmapPage.ts +229 -229
- package/handleApi.ts +21 -20
- package/index.html +2 -2
- package/index.ts +867 -866
- package/package.json +1 -1
- package/types/database.ts +800 -798
- package/utils/getUserBySession.ts +48 -48
- package/utils/requestUtils.ts +87 -87
- package/utils/security.ts +20 -20
- package/utils/star-calc/index.ts +72 -72
- package/utils/star-calc/osuUtils.ts +53 -53
- package/utils/star-calc/sspmParser.ts +398 -398
- package/utils/star-calc/sspmv1Parser.ts +165 -165
- package/utils/supabase.ts +13 -13
- package/utils/test +4 -4
- package/utils/validateToken.ts +7 -7
- package/vercel.json +12 -12
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { User } from "@supabase/supabase-js";
|
|
2
|
-
import { decryptString } from "./security";
|
|
3
|
-
import { supabase } from "./supabase";
|
|
4
|
-
|
|
5
|
-
export async function getUserBySession(session: string): Promise<User | null> {
|
|
6
|
-
const user = (await supabase.auth.getUser(session)).data.user;
|
|
7
|
-
|
|
8
|
-
if (user) {
|
|
9
|
-
return user;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
console.log("trying legacy token");
|
|
14
|
-
const decryptedToken = JSON.parse(decryptString(session)) as {
|
|
15
|
-
userId: number;
|
|
16
|
-
email: string;
|
|
17
|
-
passKey: string;
|
|
18
|
-
computerName: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
for (const key of Object.keys(decryptedToken)) {
|
|
22
|
-
if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(decryptedToken);
|
|
28
|
-
|
|
29
|
-
let { data: queryPasskey, error } = await supabase
|
|
30
|
-
.from("passkeys")
|
|
31
|
-
.select("*,profiles(uid)")
|
|
32
|
-
.eq("id", decryptedToken.userId)
|
|
33
|
-
.eq("email", decryptedToken.email)
|
|
34
|
-
.eq("passkey", decryptedToken.passKey)
|
|
35
|
-
.single();
|
|
36
|
-
|
|
37
|
-
console.log(queryPasskey);
|
|
38
|
-
|
|
39
|
-
if (!queryPasskey) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
|
|
44
|
-
.data.user;
|
|
45
|
-
} catch (error) {}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
1
|
+
import { User } from "@supabase/supabase-js";
|
|
2
|
+
import { decryptString } from "./security";
|
|
3
|
+
import { supabase } from "./supabase";
|
|
4
|
+
|
|
5
|
+
export async function getUserBySession(session: string): Promise<User | null> {
|
|
6
|
+
const user = (await supabase.auth.getUser(session)).data.user;
|
|
7
|
+
|
|
8
|
+
if (user) {
|
|
9
|
+
return user;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
console.log("trying legacy token");
|
|
14
|
+
const decryptedToken = JSON.parse(decryptString(session)) as {
|
|
15
|
+
userId: number;
|
|
16
|
+
email: string;
|
|
17
|
+
passKey: string;
|
|
18
|
+
computerName: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const key of Object.keys(decryptedToken)) {
|
|
22
|
+
if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(decryptedToken);
|
|
28
|
+
|
|
29
|
+
let { data: queryPasskey, error } = await supabase
|
|
30
|
+
.from("passkeys")
|
|
31
|
+
.select("*,profiles(uid)")
|
|
32
|
+
.eq("id", decryptedToken.userId)
|
|
33
|
+
.eq("email", decryptedToken.email)
|
|
34
|
+
.eq("passkey", decryptedToken.passKey)
|
|
35
|
+
.single();
|
|
36
|
+
|
|
37
|
+
console.log(queryPasskey);
|
|
38
|
+
|
|
39
|
+
if (!queryPasskey) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
|
|
44
|
+
.data.user;
|
|
45
|
+
} catch (error) {}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
package/utils/requestUtils.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { set, ZodObject } from "zod";
|
|
3
|
-
import { getUserBySession } from "./getUserBySession";
|
|
4
|
-
import { supabase } from "./supabase";
|
|
5
|
-
|
|
6
|
-
interface Props<
|
|
7
|
-
K = (...args: any[]) => Promise<NextResponse<any>>,
|
|
8
|
-
T = ZodObject<any>
|
|
9
|
-
> {
|
|
10
|
-
request: Request;
|
|
11
|
-
schema: { input: T; output: T };
|
|
12
|
-
authorization?: Function;
|
|
13
|
-
activity: K;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function protectedApi({
|
|
17
|
-
request,
|
|
18
|
-
schema,
|
|
19
|
-
authorization,
|
|
20
|
-
activity,
|
|
21
|
-
}: Props) {
|
|
22
|
-
try {
|
|
23
|
-
const toParse = await request.json();
|
|
24
|
-
const data = schema.input.parse(toParse);
|
|
25
|
-
|
|
26
|
-
const dataClone = structuredClone(data);
|
|
27
|
-
if (dataClone) {
|
|
28
|
-
if (dataClone["token"]) {
|
|
29
|
-
dataClone["token"] = "********";
|
|
30
|
-
}
|
|
31
|
-
Object.keys(dataClone).forEach((key) => {
|
|
32
|
-
console.log("KEY: ", key, dataClone[key]);
|
|
33
|
-
if (key == "data") {
|
|
34
|
-
try {
|
|
35
|
-
Object.keys(dataClone[key]).forEach((key2) => {
|
|
36
|
-
console.log("KEY2: ", key2, dataClone[key][key2]);
|
|
37
|
-
});
|
|
38
|
-
} catch (error) {}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
setActivity(data);
|
|
44
|
-
if (authorization) {
|
|
45
|
-
const authorizationResponse = await authorization(data);
|
|
46
|
-
if (authorizationResponse) {
|
|
47
|
-
return authorizationResponse;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return await activity(data, request);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return NextResponse.json({ error: error.toString() }, { status: 400 });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function setActivity(data: Record<string, any>) {
|
|
57
|
-
if (data.session) {
|
|
58
|
-
const user = (await supabase.auth.getUser(data.session)).data.user;
|
|
59
|
-
if (user) {
|
|
60
|
-
await supabase.from("profileActivities").upsert({
|
|
61
|
-
uid: user.id,
|
|
62
|
-
last_activity: Date.now(),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function validUser(data) {
|
|
69
|
-
if (!data.session) {
|
|
70
|
-
return NextResponse.json(
|
|
71
|
-
{
|
|
72
|
-
error: "Session is missing",
|
|
73
|
-
},
|
|
74
|
-
{ status: 501 }
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const user = await getUserBySession(data.session);
|
|
79
|
-
if (!user) {
|
|
80
|
-
return NextResponse.json(
|
|
81
|
-
{
|
|
82
|
-
error: "Invalid user session",
|
|
83
|
-
},
|
|
84
|
-
{ status: 400 }
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { set, ZodObject } from "zod";
|
|
3
|
+
import { getUserBySession } from "./getUserBySession";
|
|
4
|
+
import { supabase } from "./supabase";
|
|
5
|
+
|
|
6
|
+
interface Props<
|
|
7
|
+
K = (...args: any[]) => Promise<NextResponse<any>>,
|
|
8
|
+
T = ZodObject<any>
|
|
9
|
+
> {
|
|
10
|
+
request: Request;
|
|
11
|
+
schema: { input: T; output: T };
|
|
12
|
+
authorization?: Function;
|
|
13
|
+
activity: K;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function protectedApi({
|
|
17
|
+
request,
|
|
18
|
+
schema,
|
|
19
|
+
authorization,
|
|
20
|
+
activity,
|
|
21
|
+
}: Props) {
|
|
22
|
+
try {
|
|
23
|
+
const toParse = await request.json();
|
|
24
|
+
const data = schema.input.parse(toParse);
|
|
25
|
+
|
|
26
|
+
const dataClone = structuredClone(data);
|
|
27
|
+
if (dataClone) {
|
|
28
|
+
if (dataClone["token"]) {
|
|
29
|
+
dataClone["token"] = "********";
|
|
30
|
+
}
|
|
31
|
+
Object.keys(dataClone).forEach((key) => {
|
|
32
|
+
console.log("KEY: ", key, dataClone[key]);
|
|
33
|
+
if (key == "data") {
|
|
34
|
+
try {
|
|
35
|
+
Object.keys(dataClone[key]).forEach((key2) => {
|
|
36
|
+
console.log("KEY2: ", key2, dataClone[key][key2]);
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setActivity(data);
|
|
44
|
+
if (authorization) {
|
|
45
|
+
const authorizationResponse = await authorization(data);
|
|
46
|
+
if (authorizationResponse) {
|
|
47
|
+
return authorizationResponse;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return await activity(data, request);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return NextResponse.json({ error: error.toString() }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function setActivity(data: Record<string, any>) {
|
|
57
|
+
if (data.session) {
|
|
58
|
+
const user = (await supabase.auth.getUser(data.session)).data.user;
|
|
59
|
+
if (user) {
|
|
60
|
+
await supabase.from("profileActivities").upsert({
|
|
61
|
+
uid: user.id,
|
|
62
|
+
last_activity: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function validUser(data) {
|
|
69
|
+
if (!data.session) {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{
|
|
72
|
+
error: "Session is missing",
|
|
73
|
+
},
|
|
74
|
+
{ status: 501 }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user = await getUserBySession(data.session);
|
|
79
|
+
if (!user) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{
|
|
82
|
+
error: "Invalid user session",
|
|
83
|
+
},
|
|
84
|
+
{ status: 400 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/utils/security.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
2
|
-
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
3
|
-
import {
|
|
4
|
-
bytesToHex,
|
|
5
|
-
bytesToUtf8,
|
|
6
|
-
hexToBytes,
|
|
7
|
-
utf8ToBytes,
|
|
8
|
-
} from "@noble/ciphers/utils";
|
|
9
|
-
const key = hexToBytes(process.env.TOKEN_SECRET || "");
|
|
10
|
-
const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
|
|
11
|
-
|
|
12
|
-
export function encryptString(str: string) {
|
|
13
|
-
const data = utf8ToBytes(str);
|
|
14
|
-
return bytesToHex(chacha.encrypt(data));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function decryptString(str: string) {
|
|
18
|
-
const data = hexToBytes(str);
|
|
19
|
-
return bytesToUtf8(chacha.decrypt(data));
|
|
20
|
-
}
|
|
1
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
2
|
+
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
bytesToUtf8,
|
|
6
|
+
hexToBytes,
|
|
7
|
+
utf8ToBytes,
|
|
8
|
+
} from "@noble/ciphers/utils";
|
|
9
|
+
const key = hexToBytes(process.env.TOKEN_SECRET || "");
|
|
10
|
+
const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
|
|
11
|
+
|
|
12
|
+
export function encryptString(str: string) {
|
|
13
|
+
const data = utf8ToBytes(str);
|
|
14
|
+
return bytesToHex(chacha.encrypt(data));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decryptString(str: string) {
|
|
18
|
+
const data = hexToBytes(str);
|
|
19
|
+
return bytesToUtf8(chacha.decrypt(data));
|
|
20
|
+
}
|
package/utils/star-calc/index.ts
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
|
|
2
|
-
import { StandardRuleset } from "osu-standard-stable";
|
|
3
|
-
import { sampleMap } from "./osuUtils";
|
|
4
|
-
import { SSPMParsedMap } from "./sspmParser";
|
|
5
|
-
import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
|
|
6
|
-
|
|
7
|
-
export function rateMap(map: SSPMParsedMap) {
|
|
8
|
-
let notes = map.markers
|
|
9
|
-
.filter((marker) => marker.type === 0)
|
|
10
|
-
.map((marker) => ({
|
|
11
|
-
time: marker.position,
|
|
12
|
-
x: marker.data["field0"].x,
|
|
13
|
-
y: marker.data["field0"].y,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
return rate(notes);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function rateMapNotes(map: [number, number, number][]) {
|
|
20
|
-
let notes = map.map((marker) => ({
|
|
21
|
-
time: marker[2],
|
|
22
|
-
x: marker[0],
|
|
23
|
-
y: marker[1],
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
return rate(notes);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function rateMapOld(map: SSPMMap) {
|
|
30
|
-
let notes = map.notes.map((marker) => ({
|
|
31
|
-
time: marker.position,
|
|
32
|
-
x: marker.x,
|
|
33
|
-
y: marker.y,
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
return rate(notes);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function rate(
|
|
40
|
-
notes: {
|
|
41
|
-
time: number;
|
|
42
|
-
x: number;
|
|
43
|
-
y: number;
|
|
44
|
-
}[]
|
|
45
|
-
) {
|
|
46
|
-
notes = notes.sort((a, b) => a.time - b.time);
|
|
47
|
-
const decoder = new BeatmapDecoder();
|
|
48
|
-
const beatmap1 = decoder.decodeFromString(sampleMap);
|
|
49
|
-
let i = 0;
|
|
50
|
-
while (i < notes.length - 1) {
|
|
51
|
-
const note = notes[i];
|
|
52
|
-
const nextNote = notes[i + 1];
|
|
53
|
-
if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
|
|
54
|
-
notes.splice(i + 1, 1);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const hittable = beatmap1.hitObjects[0].clone();
|
|
59
|
-
hittable.startX = Math.round((note.x / 2) * 100);
|
|
60
|
-
hittable.startY = Math.round((note.y / 2) * 100);
|
|
61
|
-
hittable.startTime = note.time;
|
|
62
|
-
beatmap1.hitObjects.push(hittable);
|
|
63
|
-
i++;
|
|
64
|
-
}
|
|
65
|
-
const ruleset = new StandardRuleset();
|
|
66
|
-
const mods = ruleset.createModCombination("RX");
|
|
67
|
-
const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
|
|
68
|
-
const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
|
|
69
|
-
return difficultyAttributes.starRating;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
|
|
1
|
+
import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
|
|
2
|
+
import { StandardRuleset } from "osu-standard-stable";
|
|
3
|
+
import { sampleMap } from "./osuUtils";
|
|
4
|
+
import { SSPMParsedMap } from "./sspmParser";
|
|
5
|
+
import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
|
|
6
|
+
|
|
7
|
+
export function rateMap(map: SSPMParsedMap) {
|
|
8
|
+
let notes = map.markers
|
|
9
|
+
.filter((marker) => marker.type === 0)
|
|
10
|
+
.map((marker) => ({
|
|
11
|
+
time: marker.position,
|
|
12
|
+
x: marker.data["field0"].x,
|
|
13
|
+
y: marker.data["field0"].y,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
return rate(notes);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rateMapNotes(map: [number, number, number][]) {
|
|
20
|
+
let notes = map.map((marker) => ({
|
|
21
|
+
time: marker[2],
|
|
22
|
+
x: marker[0],
|
|
23
|
+
y: marker[1],
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
return rate(notes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function rateMapOld(map: SSPMMap) {
|
|
30
|
+
let notes = map.notes.map((marker) => ({
|
|
31
|
+
time: marker.position,
|
|
32
|
+
x: marker.x,
|
|
33
|
+
y: marker.y,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return rate(notes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function rate(
|
|
40
|
+
notes: {
|
|
41
|
+
time: number;
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}[]
|
|
45
|
+
) {
|
|
46
|
+
notes = notes.sort((a, b) => a.time - b.time);
|
|
47
|
+
const decoder = new BeatmapDecoder();
|
|
48
|
+
const beatmap1 = decoder.decodeFromString(sampleMap);
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < notes.length - 1) {
|
|
51
|
+
const note = notes[i];
|
|
52
|
+
const nextNote = notes[i + 1];
|
|
53
|
+
if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
|
|
54
|
+
notes.splice(i + 1, 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hittable = beatmap1.hitObjects[0].clone();
|
|
59
|
+
hittable.startX = Math.round((note.x / 2) * 100);
|
|
60
|
+
hittable.startY = Math.round((note.y / 2) * 100);
|
|
61
|
+
hittable.startTime = note.time;
|
|
62
|
+
beatmap1.hitObjects.push(hittable);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
const ruleset = new StandardRuleset();
|
|
66
|
+
const mods = ruleset.createModCombination("RX");
|
|
67
|
+
const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
|
|
68
|
+
const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
|
|
69
|
+
return difficultyAttributes.starRating;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
export const sampleMap = `osu file format v14
|
|
2
|
-
|
|
3
|
-
[General]
|
|
4
|
-
AudioFilename: audio.mp3
|
|
5
|
-
AudioLeadIn: 0
|
|
6
|
-
PreviewTime: 99664
|
|
7
|
-
Countdown: 0
|
|
8
|
-
SampleSet: Normal
|
|
9
|
-
StackLeniency: 0
|
|
10
|
-
Mode: 0
|
|
11
|
-
LetterboxInBreaks: 0
|
|
12
|
-
UseSkinSprites: 1
|
|
13
|
-
SkinPreference:Default
|
|
14
|
-
WidescreenStoryboard: 1
|
|
15
|
-
SamplesMatchPlaybackRate: 1
|
|
16
|
-
|
|
17
|
-
[Editor]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[Metadata]
|
|
21
|
-
Title:new beginnings
|
|
22
|
-
TitleUnicode:new beginnings
|
|
23
|
-
Artist:nekodex
|
|
24
|
-
ArtistUnicode:nekodex
|
|
25
|
-
Creator:pishifat
|
|
26
|
-
Version:tutorial
|
|
27
|
-
Source:
|
|
28
|
-
Tags:
|
|
29
|
-
BeatmapID:2116202
|
|
30
|
-
BeatmapSetID:1011011
|
|
31
|
-
|
|
32
|
-
[Difficulty]
|
|
33
|
-
HPDrainRate:5
|
|
34
|
-
CircleSize:2
|
|
35
|
-
OverallDifficulty:8
|
|
36
|
-
ApproachRate:8
|
|
37
|
-
SliderMultiplier:1
|
|
38
|
-
SliderTickRate:1
|
|
39
|
-
|
|
40
|
-
[Events]
|
|
41
|
-
//Background and Video events
|
|
42
|
-
0,0,"new-beginnings.jpg",0,0
|
|
43
|
-
|
|
44
|
-
[TimingPoints]
|
|
45
|
-
-28,461.538461538462,4,1,0,100,1,0
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
[Colours]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[HitObjects]
|
|
52
|
-
256,192,24202,1,0,0:0:0:0:
|
|
53
|
-
`;
|
|
1
|
+
export const sampleMap = `osu file format v14
|
|
2
|
+
|
|
3
|
+
[General]
|
|
4
|
+
AudioFilename: audio.mp3
|
|
5
|
+
AudioLeadIn: 0
|
|
6
|
+
PreviewTime: 99664
|
|
7
|
+
Countdown: 0
|
|
8
|
+
SampleSet: Normal
|
|
9
|
+
StackLeniency: 0
|
|
10
|
+
Mode: 0
|
|
11
|
+
LetterboxInBreaks: 0
|
|
12
|
+
UseSkinSprites: 1
|
|
13
|
+
SkinPreference:Default
|
|
14
|
+
WidescreenStoryboard: 1
|
|
15
|
+
SamplesMatchPlaybackRate: 1
|
|
16
|
+
|
|
17
|
+
[Editor]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
[Metadata]
|
|
21
|
+
Title:new beginnings
|
|
22
|
+
TitleUnicode:new beginnings
|
|
23
|
+
Artist:nekodex
|
|
24
|
+
ArtistUnicode:nekodex
|
|
25
|
+
Creator:pishifat
|
|
26
|
+
Version:tutorial
|
|
27
|
+
Source:
|
|
28
|
+
Tags:
|
|
29
|
+
BeatmapID:2116202
|
|
30
|
+
BeatmapSetID:1011011
|
|
31
|
+
|
|
32
|
+
[Difficulty]
|
|
33
|
+
HPDrainRate:5
|
|
34
|
+
CircleSize:2
|
|
35
|
+
OverallDifficulty:8
|
|
36
|
+
ApproachRate:8
|
|
37
|
+
SliderMultiplier:1
|
|
38
|
+
SliderTickRate:1
|
|
39
|
+
|
|
40
|
+
[Events]
|
|
41
|
+
//Background and Video events
|
|
42
|
+
0,0,"new-beginnings.jpg",0,0
|
|
43
|
+
|
|
44
|
+
[TimingPoints]
|
|
45
|
+
-28,461.538461538462,4,1,0,100,1,0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
[Colours]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
[HitObjects]
|
|
52
|
+
256,192,24202,1,0,0:0:0:0:
|
|
53
|
+
`;
|