@rian8337/osu-droid-replay-analyzer 3.0.0-beta.31 → 3.0.0-beta.33
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/dist/index.js +436 -247
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/typings/index.d.ts +24 -8
package/dist/index.js
CHANGED
|
@@ -130,6 +130,12 @@ class CursorOccurrenceGroup {
|
|
|
130
130
|
get endTime() {
|
|
131
131
|
return this._up?.time ?? this._moves.at(-1)?.time ?? this._down.time;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The duration this cursor occurrence group is active for.
|
|
135
|
+
*/
|
|
136
|
+
get duration() {
|
|
137
|
+
return this.endTime - this.startTime;
|
|
138
|
+
}
|
|
133
139
|
/**
|
|
134
140
|
* All cursor occurrences in this group.
|
|
135
141
|
*
|
|
@@ -828,73 +834,69 @@ class ThreeFingerChecker {
|
|
|
828
834
|
* @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
|
|
829
835
|
*/
|
|
830
836
|
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
837
|
+
const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
838
|
+
const hitWindow50 = this.hitWindow.hitWindowFor50(isPrecise);
|
|
839
|
+
for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
|
|
840
|
+
const object = sectionObjects[i];
|
|
841
|
+
const objectData = sectionReplayObjectData[i];
|
|
842
|
+
if (object.object instanceof osuBase.Spinner ||
|
|
843
|
+
objectData.result === exports.HitResult.miss) {
|
|
844
|
+
continue;
|
|
839
845
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if (s.result === exports.HitResult.miss) {
|
|
846
|
+
// Exclude sliderbreaks.
|
|
847
|
+
if (object.object instanceof osuBase.Slider &&
|
|
848
|
+
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
844
849
|
continue;
|
|
845
850
|
}
|
|
846
|
-
|
|
851
|
+
const objectPosition = object.object.getStackedPosition(osuBase.Modes.droid);
|
|
852
|
+
const hitTime = object.object.startTime + objectData.accuracy;
|
|
853
|
+
// Observe the cursor position at the object's hit time.
|
|
847
854
|
for (let j = 0; j < cursorIndexes.length; ++j) {
|
|
848
|
-
|
|
849
|
-
// Cursor instances aren't always recorded at all times,
|
|
850
|
-
// therefore the game emulates the movement between
|
|
851
|
-
// movementType.MOVE cursors.
|
|
852
|
-
const hitTime = o.object.startTime + s.accuracy;
|
|
853
|
-
const cursorGroup = c.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
854
|
-
if (!cursorGroup) {
|
|
855
|
+
if (cursorIndexes[j] === -1) {
|
|
855
856
|
continue;
|
|
856
857
|
}
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
if (hitIndex <= -1) {
|
|
861
|
-
cursorIndexes[j] = -1;
|
|
858
|
+
const cursorData = this.data.cursorMovement[cursorIndexes[j]];
|
|
859
|
+
const cursorGroup = cursorData.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
860
|
+
if (!cursorGroup) {
|
|
862
861
|
continue;
|
|
863
862
|
}
|
|
864
|
-
const
|
|
865
|
-
let
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
isInObject =
|
|
863
|
+
const cursors = cursorGroup.allOccurrences;
|
|
864
|
+
for (let k = 1; k < cursors.length; ++k) {
|
|
865
|
+
const cursor = cursors[k];
|
|
866
|
+
const prevCursor = cursors[k - 1];
|
|
867
|
+
if (prevCursor.time <
|
|
868
|
+
object.object.startTime - hitWindow50) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (prevCursor.time >
|
|
872
|
+
object.object.startTime + hitWindow50) {
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
let isInObject = false;
|
|
876
|
+
switch (cursor.id) {
|
|
877
|
+
case exports.MovementType.up:
|
|
878
|
+
isInObject =
|
|
879
|
+
prevCursor.position.getDistance(objectPosition) <= object.object.getRadius(osuBase.Modes.droid);
|
|
880
880
|
break;
|
|
881
|
-
|
|
881
|
+
case exports.MovementType.move:
|
|
882
|
+
// Interpolate movement.
|
|
883
|
+
for (let mSecPassed = prevCursor.time; !isInObject &&
|
|
884
|
+
mSecPassed <=
|
|
885
|
+
Math.min(cursor.time, object.object.startTime + hitWindow50); ++mSecPassed) {
|
|
886
|
+
const t = (mSecPassed - prevCursor.time) /
|
|
887
|
+
(cursor.time - prevCursor.time);
|
|
888
|
+
const cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
|
|
889
|
+
isInObject =
|
|
890
|
+
objectPosition.getDistance(cursorPosition) <= object.object.getRadius(osuBase.Modes.droid);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!isInObject) {
|
|
894
|
+
cursorIndexes[j] = -1;
|
|
882
895
|
}
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
isInObject =
|
|
886
|
-
o.object
|
|
887
|
-
.getStackedPosition(osuBase.Modes.droid)
|
|
888
|
-
.getDistance(cursorPosition) <=
|
|
889
|
-
o.object.getRadius(osuBase.Modes.droid);
|
|
890
|
-
}
|
|
891
|
-
if (!isInObject) {
|
|
892
|
-
cursorIndexes[j] = -1;
|
|
893
896
|
}
|
|
894
897
|
}
|
|
895
|
-
cursorIndexes = cursorIndexes.filter((v) => v !== -1);
|
|
896
898
|
}
|
|
897
|
-
return cursorIndexes.
|
|
899
|
+
return cursorIndexes.find((v) => v !== -1) ?? -1;
|
|
898
900
|
}
|
|
899
901
|
/**
|
|
900
902
|
* Redivides the beatmap into sections.
|
|
@@ -994,13 +996,6 @@ class ThreeFingerChecker {
|
|
|
994
996
|
exports.HitResult.miss
|
|
995
997
|
? objectData[beatmapSection.lastObjectIndex].accuracy
|
|
996
998
|
: this.hitWindow.hitWindowFor50(isPrecise));
|
|
997
|
-
// Filter cursor instances during section.
|
|
998
|
-
this.downCursorInstances.forEach((c) => {
|
|
999
|
-
const i = c.findIndex((t) => t.time >= startTime);
|
|
1000
|
-
if (i !== -1) {
|
|
1001
|
-
c = c.slice(i);
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
999
|
const cursorAmounts = [];
|
|
1005
1000
|
const cursorVectorTimes = [];
|
|
1006
1001
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
@@ -1069,7 +1064,8 @@ class ThreeFingerChecker {
|
|
|
1069
1064
|
.slice(2);
|
|
1070
1065
|
// Ignore cursor presses that are only 1 for now since they are very likely to be accidental
|
|
1071
1066
|
if ((threeFingerRatio > this.threeFingerRatioThreshold &&
|
|
1072
|
-
cursorAmounts.filter((v) => v > 1).length >
|
|
1067
|
+
cursorAmounts.filter((v) => v > 1).length >
|
|
1068
|
+
fingerSplitIndex) ||
|
|
1073
1069
|
validPresses.length > 0) {
|
|
1074
1070
|
// Strain factor
|
|
1075
1071
|
const objectCount = beatmapSection.lastObjectIndex -
|
|
@@ -1130,6 +1126,20 @@ class IndexedHitObject {
|
|
|
1130
1126
|
* the hitobject or it did not meet the criteria for detection.
|
|
1131
1127
|
*/
|
|
1132
1128
|
cursorIndex;
|
|
1129
|
+
/**
|
|
1130
|
+
* The group index of the cursor within the cursor index that hits the hitobject.
|
|
1131
|
+
*
|
|
1132
|
+
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1133
|
+
* the hitobject or it did not meet the criteria for detection.
|
|
1134
|
+
*/
|
|
1135
|
+
groupIndex;
|
|
1136
|
+
/**
|
|
1137
|
+
* The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
|
|
1138
|
+
*
|
|
1139
|
+
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1140
|
+
* the hitobject or it did not meet the criteria for detection.
|
|
1141
|
+
*/
|
|
1142
|
+
occurrenceIndex;
|
|
1133
1143
|
/**
|
|
1134
1144
|
* The angle of the movement of the cursor towards the next hitobject.
|
|
1135
1145
|
*/
|
|
@@ -1143,23 +1153,34 @@ class IndexedHitObject {
|
|
|
1143
1153
|
*/
|
|
1144
1154
|
object;
|
|
1145
1155
|
/**
|
|
1146
|
-
*
|
|
1156
|
+
* The position of the cursor at the end of this hitobject.
|
|
1157
|
+
*
|
|
1158
|
+
* Will be altered during detection.
|
|
1147
1159
|
*/
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1160
|
+
endCursorPosition;
|
|
1161
|
+
/**
|
|
1162
|
+
* Whether the hitobject is likely two-handed.
|
|
1163
|
+
*/
|
|
1164
|
+
is2Handed;
|
|
1151
1165
|
/**
|
|
1152
1166
|
* @param object The underlying difficulty hitobject.
|
|
1153
1167
|
* @param cursorIndex The cursor index that moves towards the hitobject.
|
|
1168
|
+
* @param groupIndex The group index of the cursor within the cursor index that hits the hitobject.
|
|
1169
|
+
* @param occurrenceIndex The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
|
|
1154
1170
|
* @param angle The angle of the movement of the cursor that moves towards the hitobject.
|
|
1155
1171
|
*/
|
|
1156
|
-
constructor(object, cursorIndex, angle) {
|
|
1172
|
+
constructor(object, cursorIndex, groupIndex, occurrenceIndex, angle, is2Handed) {
|
|
1157
1173
|
this.object = object;
|
|
1158
1174
|
this.cursorIndex = cursorIndex;
|
|
1175
|
+
this.groupIndex = groupIndex;
|
|
1176
|
+
this.occurrenceIndex = occurrenceIndex;
|
|
1159
1177
|
this.angle = angle;
|
|
1178
|
+
this.is2Handed = is2Handed;
|
|
1179
|
+
this.endCursorPosition = this.object.object.getStackedEndPosition(osuBase.Modes.droid);
|
|
1160
1180
|
}
|
|
1161
1181
|
}
|
|
1162
1182
|
|
|
1183
|
+
// import { writeFileSync } from "fs";
|
|
1163
1184
|
/**
|
|
1164
1185
|
* Utility to check whether or not a beatmap is two-handed.
|
|
1165
1186
|
*/
|
|
@@ -1184,6 +1205,7 @@ class TwoHandChecker {
|
|
|
1184
1205
|
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
1185
1206
|
*/
|
|
1186
1207
|
hitWindow50;
|
|
1208
|
+
// private csvString: string;
|
|
1187
1209
|
/**
|
|
1188
1210
|
* @param calculator The difficulty calculator to analyze.
|
|
1189
1211
|
* @param data The data of the replay.
|
|
@@ -1198,17 +1220,86 @@ class TwoHandChecker {
|
|
|
1198
1220
|
}).calculate();
|
|
1199
1221
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
1200
1222
|
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1223
|
+
// this.csvString = `Mods,${
|
|
1224
|
+
// data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
|
|
1225
|
+
// }\nCombo,${data.maxCombo}\nAccuracy,"${(
|
|
1226
|
+
// data.accuracy.value() * 100
|
|
1227
|
+
// ).toFixed(2)}% [${data.accuracy.n300}/${data.accuracy.n100}/${
|
|
1228
|
+
// data.accuracy.n50
|
|
1229
|
+
// }/${
|
|
1230
|
+
// data.accuracy.nmiss
|
|
1231
|
+
// }]"\n\nIndex,Type,StartTime,EndTime,DeltaTime,CursorIndex,GroupIndex,CursorDuration\n`;
|
|
1201
1232
|
}
|
|
1202
1233
|
/**
|
|
1203
1234
|
* Checks if a beatmap is two-handed.
|
|
1204
1235
|
*/
|
|
1205
1236
|
check() {
|
|
1206
1237
|
if (this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 1) {
|
|
1207
|
-
return { is2Hand: false,
|
|
1238
|
+
return { is2Hand: false, twoHandedNoteCount: 0 };
|
|
1208
1239
|
}
|
|
1209
1240
|
this.indexHitObjects();
|
|
1210
|
-
this.applyPenalty();
|
|
1211
|
-
|
|
1241
|
+
// this.applyPenalty();
|
|
1242
|
+
// for (let i = 0; i < this.calculator.objects.length; ++i) {
|
|
1243
|
+
// const object: PlaceableHitObject =
|
|
1244
|
+
// this.calculator.objects[i].object;
|
|
1245
|
+
// if (object instanceof Spinner) {
|
|
1246
|
+
// continue;
|
|
1247
|
+
// }
|
|
1248
|
+
// let deltaTime: number = 0;
|
|
1249
|
+
// if (i > 0) {
|
|
1250
|
+
// const prevObject: PlaceableHitObject =
|
|
1251
|
+
// this.calculator.objects[i - 1].object;
|
|
1252
|
+
// deltaTime = Math.max(
|
|
1253
|
+
// object.startTime - prevObject.startTime,
|
|
1254
|
+
// 25
|
|
1255
|
+
// );
|
|
1256
|
+
// if (prevObject instanceof Slider) {
|
|
1257
|
+
// deltaTime = Math.max(
|
|
1258
|
+
// deltaTime - prevObject.lazyTravelTime,
|
|
1259
|
+
// 25
|
|
1260
|
+
// );
|
|
1261
|
+
// }
|
|
1262
|
+
// }
|
|
1263
|
+
// const objectInfo = this.getCursorPositionForObjectStart(i);
|
|
1264
|
+
// this.csvString += `${i},${object.typeStr()},${object.startTime},${
|
|
1265
|
+
// object instanceof Slider
|
|
1266
|
+
// ? object.startTime + object.lazyTravelTime
|
|
1267
|
+
// : object.endTime
|
|
1268
|
+
// },${deltaTime},${objectInfo.cursorIndex},${objectInfo.groupIndex},${
|
|
1269
|
+
// this.data.cursorMovement[objectInfo.cursorIndex]
|
|
1270
|
+
// ?.occurrenceGroups[objectInfo.groupIndex]?.duration ??
|
|
1271
|
+
// Number.POSITIVE_INFINITY
|
|
1272
|
+
// }\n`;
|
|
1273
|
+
// }
|
|
1274
|
+
// writeFileSync(
|
|
1275
|
+
// join(
|
|
1276
|
+
// process.cwd(),
|
|
1277
|
+
// "files",
|
|
1278
|
+
// `${this.data.playerName} - ${this.calculator.beatmap.metadata.fullTitle}.csv`
|
|
1279
|
+
// ),
|
|
1280
|
+
// this.csvString
|
|
1281
|
+
// );
|
|
1282
|
+
let twoHandedNoteCount = 0;
|
|
1283
|
+
const maxStrain = Math.max(...this.calculator.objects.map((v) => v.aimStrainWithSliders));
|
|
1284
|
+
if (maxStrain) {
|
|
1285
|
+
twoHandedNoteCount = this.indexedHitObjects.reduce((total, object) => {
|
|
1286
|
+
if (!object.is2Handed) {
|
|
1287
|
+
return total;
|
|
1288
|
+
}
|
|
1289
|
+
return (total +
|
|
1290
|
+
1 /
|
|
1291
|
+
(1 +
|
|
1292
|
+
Math.exp(-((object.object.aimStrainWithSliders /
|
|
1293
|
+
maxStrain) *
|
|
1294
|
+
12 -
|
|
1295
|
+
6))));
|
|
1296
|
+
}, 0);
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
is2Hand: twoHandedNoteCount >
|
|
1300
|
+
this.calculator.attributes.aimNoteCount * 0.15,
|
|
1301
|
+
twoHandedNoteCount: twoHandedNoteCount,
|
|
1302
|
+
};
|
|
1212
1303
|
}
|
|
1213
1304
|
/**
|
|
1214
1305
|
* Converts hitobjects into indexed hit objects.
|
|
@@ -1221,6 +1312,7 @@ class TwoHandChecker {
|
|
|
1221
1312
|
indexes.push(indexedHitObject.cursorIndex);
|
|
1222
1313
|
this.indexedHitObjects.push(indexedHitObject);
|
|
1223
1314
|
}
|
|
1315
|
+
// console.log("Aim note count:", this.calculator.attributes.aimNoteCount);
|
|
1224
1316
|
// console.log(
|
|
1225
1317
|
// this.indexedHitObjects.filter((v) => v.cursorIndex !== -1).length +
|
|
1226
1318
|
// 1,
|
|
@@ -1248,8 +1340,7 @@ class TwoHandChecker {
|
|
|
1248
1340
|
// (v) =>
|
|
1249
1341
|
// // v.cursorIndex === -1 &&
|
|
1250
1342
|
// // v.object.aimStrainWithSliders > 200 &&
|
|
1251
|
-
// v.object.deltaTime > 75 &&
|
|
1252
|
-
// v.is2Handed
|
|
1343
|
+
// v.object.deltaTime > 75 && v.is2Handed
|
|
1253
1344
|
// )
|
|
1254
1345
|
// .map((v) => {
|
|
1255
1346
|
// return {
|
|
@@ -1280,17 +1371,17 @@ class TwoHandChecker {
|
|
|
1280
1371
|
const { object } = diffObject;
|
|
1281
1372
|
// We don't care about the first object and spinners.
|
|
1282
1373
|
if (objectIndex === 0 || object instanceof osuBase.Spinner) {
|
|
1283
|
-
return new IndexedHitObject(diffObject, -1, null);
|
|
1374
|
+
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1284
1375
|
}
|
|
1285
1376
|
// We don't care if the aim strain is too low.
|
|
1286
|
-
if (diffObject.aimStrainWithSliders < 200) {
|
|
1287
|
-
|
|
1288
|
-
}
|
|
1377
|
+
// if (diffObject.aimStrainWithSliders < 200) {
|
|
1378
|
+
// return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1379
|
+
// }
|
|
1289
1380
|
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1290
1381
|
const prevObjectData = this.data.hitObjectData[objectIndex - 1];
|
|
1291
1382
|
if (prevObject instanceof osuBase.Spinner ||
|
|
1292
1383
|
prevObjectData.result === exports.HitResult.miss) {
|
|
1293
|
-
return new IndexedHitObject(diffObject, -1, null);
|
|
1384
|
+
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1294
1385
|
}
|
|
1295
1386
|
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1296
1387
|
let prevObjectEndPosition = prevObject.getStackedEndPosition(osuBase.Modes.droid);
|
|
@@ -1309,9 +1400,10 @@ class TwoHandChecker {
|
|
|
1309
1400
|
const prevToCurrentMovement = object
|
|
1310
1401
|
.getStackedPosition(osuBase.Modes.droid)
|
|
1311
1402
|
.subtract(prevObjectEndPosition);
|
|
1403
|
+
const radius = object.getRadius(osuBase.Modes.droid);
|
|
1312
1404
|
// Don't consider objects that are too close to each other.
|
|
1313
|
-
if (prevToCurrentMovement.length <=
|
|
1314
|
-
return new IndexedHitObject(diffObject, -1, null);
|
|
1405
|
+
if (prevToCurrentMovement.length <= radius) {
|
|
1406
|
+
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1315
1407
|
}
|
|
1316
1408
|
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
1317
1409
|
// the next object in fast patterns. We should not be worried about slow patterns as they will only
|
|
@@ -1322,10 +1414,21 @@ class TwoHandChecker {
|
|
|
1322
1414
|
// 2. The cursor position that presses the current object
|
|
1323
1415
|
// and then get the movement vector between both positions, then see if the cursor movement after the previous object
|
|
1324
1416
|
// was pressed produces an angle that is acute enough with respect to the aforementioned movement vector.
|
|
1417
|
+
// If the angle isn't fulfilled, check for the "initial velocity", which is the movement velocity from the
|
|
1418
|
+
// previous object's end time to the current object's start time and the "air velocity", which is the
|
|
1419
|
+
// velocity from the previous object's cursor release time to the next object's cursor press time. If the velocity
|
|
1420
|
+
// during the held tap is significantly less than the estimated velocity, it's considered two hand.
|
|
1325
1421
|
const objectInformation = this.getCursorPositionForObjectStart(objectIndex);
|
|
1326
1422
|
const prevObjectInformation = this.getCursorPositionForObjectEnd(objectIndex - 1);
|
|
1423
|
+
this.indexedHitObjects[objectIndex - 1].endCursorPosition =
|
|
1424
|
+
prevObjectInformation.position;
|
|
1327
1425
|
if (prevObjectInformation.position.x === Number.POSITIVE_INFINITY) {
|
|
1328
|
-
return new IndexedHitObject(diffObject, -1, null);
|
|
1426
|
+
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1427
|
+
}
|
|
1428
|
+
if (prevObjectInformation.cursorIndex ===
|
|
1429
|
+
objectInformation.cursorIndex &&
|
|
1430
|
+
prevObjectInformation.groupIndex === objectInformation.groupIndex) {
|
|
1431
|
+
return new IndexedHitObject(diffObject, objectInformation.cursorIndex, objectInformation.groupIndex, prevObjectInformation.occurrenceIndex, 0, false);
|
|
1329
1432
|
}
|
|
1330
1433
|
const cursorData = this.data.cursorMovement[prevObjectInformation.cursorIndex];
|
|
1331
1434
|
// There can be multiple angles to which the cursor moves towards the next object.
|
|
@@ -1334,7 +1437,7 @@ class TwoHandChecker {
|
|
|
1334
1437
|
const cursorGroup = cursorData.occurrenceGroups[prevObjectInformation.groupIndex];
|
|
1335
1438
|
const cursors = cursorGroup.allOccurrences;
|
|
1336
1439
|
const prevToCurrentCursorMovement = objectInformation.position.subtract(prevObjectInformation.position);
|
|
1337
|
-
for (let i = prevObjectInformation.
|
|
1440
|
+
for (let i = prevObjectInformation.occurrenceIndex + 1; i < cursors.length; ++i) {
|
|
1338
1441
|
const cursor = cursors[i];
|
|
1339
1442
|
const prevCursor = cursors[i - 1];
|
|
1340
1443
|
if (cursor.position.equals(prevCursor.position)) {
|
|
@@ -1350,167 +1453,64 @@ class TwoHandChecker {
|
|
|
1350
1453
|
const movementAngle = Math.abs(Math.atan2(det, dot));
|
|
1351
1454
|
finalAngle = Math.min(finalAngle, movementAngle);
|
|
1352
1455
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
// } else if (prevDiffObject.travelDistance === 0) {
|
|
1360
|
-
// // If the slider doesn't have any travel distance, use the start time as the checking point.
|
|
1361
|
-
// checkingTime = prevObject.startTime;
|
|
1362
|
-
// if (prevObjectData.accuracy !== Math.floor(this.hitWindow50) + 13) {
|
|
1363
|
-
// checkingTime += prevObjectData.accuracy;
|
|
1364
|
-
// }
|
|
1365
|
-
// }
|
|
1366
|
-
// let cursorTimeLimit: number = object.startTime;
|
|
1367
|
-
// if (object instanceof Circle) {
|
|
1368
|
-
// cursorTimeLimit += objectData.accuracy;
|
|
1369
|
-
// } else if (object instanceof Slider) {
|
|
1370
|
-
// if (objectData.accuracy !== Math.floor(this.hitWindow50) + 13) {
|
|
1371
|
-
// cursorTimeLimit += objectData.accuracy;
|
|
1372
|
-
// } else {
|
|
1373
|
-
// cursorTimeLimit += this.hitWindow50;
|
|
1374
|
-
// }
|
|
1375
|
-
// }
|
|
1376
|
-
// let hitWindow: number = this.hitWindow50;
|
|
1377
|
-
// const isPrecise: boolean = this.data.convertedMods.some(
|
|
1378
|
-
// (m) => m instanceof ModPrecise
|
|
1379
|
-
// );
|
|
1380
|
-
// // For sliders, set the hit window to as lenient as possible.
|
|
1381
|
-
// if (object instanceof Circle) {
|
|
1382
|
-
// switch (objectData.result) {
|
|
1383
|
-
// case HitResult.great:
|
|
1384
|
-
// hitWindow = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1385
|
-
// break;
|
|
1386
|
-
// case HitResult.good:
|
|
1387
|
-
// hitWindow = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1388
|
-
// break;
|
|
1389
|
-
// }
|
|
1390
|
-
// }
|
|
1391
|
-
// // There can be multiple angles to which every cursor moves towards the next object.
|
|
1392
|
-
// // For this, we take the smallest angle from a cursor.
|
|
1393
|
-
// const finalAngles: number[] = Utils.initializeArray(
|
|
1394
|
-
// this.data.cursorMovement.length,
|
|
1395
|
-
// Number.POSITIVE_INFINITY
|
|
1396
|
-
// );
|
|
1397
|
-
// for (
|
|
1398
|
-
// let i = prevObjectInformation.cursorIndex;
|
|
1399
|
-
// i < this.data.cursorMovement.length;
|
|
1400
|
-
// ++i
|
|
1401
|
-
// ) {
|
|
1402
|
-
// }
|
|
1403
|
-
// for (const cursor of this.data.cursorMovement) {
|
|
1404
|
-
// // TODO: use same method for getCursorPositionForObject
|
|
1405
|
-
// const cursorGroup: CursorOccurrenceGroup | undefined =
|
|
1406
|
-
// cursor.occurrenceGroups.find((v) => v.isActiveAt(checkingTime));
|
|
1407
|
-
// if (!cursorGroup) {
|
|
1408
|
-
// continue;
|
|
1409
|
-
// }
|
|
1410
|
-
// const cursors: CursorOccurrence[] = cursorGroup.allOccurrences;
|
|
1411
|
-
// // This is the best approximation we can come up with, which is the cursor position
|
|
1412
|
-
// // at the previous object's end time.
|
|
1413
|
-
// let cursorIndex: number = cursors.findIndex(
|
|
1414
|
-
// (v) => v.time >= checkingTime
|
|
1456
|
+
const is2Handed = finalAngle >= Math.PI / 6;
|
|
1457
|
+
// if (is2Handed) {
|
|
1458
|
+
// // If angle isn't fulfilled, check for cursor velocity.
|
|
1459
|
+
// let deltaTime: number = Math.max(
|
|
1460
|
+
// object.startTime - prevObject.startTime,
|
|
1461
|
+
// 25
|
|
1415
1462
|
// );
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
// continue;
|
|
1463
|
+
// if (prevObject instanceof Slider) {
|
|
1464
|
+
// deltaTime = Math.max(deltaTime - prevObject.lazyTravelTime, 25);
|
|
1419
1465
|
// }
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
//
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
// if (
|
|
1446
|
-
// cursorIndex === 0 ||
|
|
1447
|
-
// (cursorIndex < cursors.length - 2 &&
|
|
1448
|
-
// !cursors[cursorIndex + 1].position.equals(
|
|
1449
|
-
// endTimeCursor.position
|
|
1450
|
-
// ))
|
|
1466
|
+
// // "Initial velocity" is the movement velocity from the previous object's end time to the current object's start time.
|
|
1467
|
+
// const initialVelocity: number =
|
|
1468
|
+
// objectStartPosition.getDistance(prevObjectEndPosition) /
|
|
1469
|
+
// deltaTime;
|
|
1470
|
+
// // const finalCursor: CursorOccurrence =
|
|
1471
|
+
// // cursorGroup.moves.at(-1) ?? cursorGroup.down;
|
|
1472
|
+
// const currentCursorGroup: CursorOccurrenceGroup =
|
|
1473
|
+
// this.data.cursorMovement[objectInformation.cursorIndex]
|
|
1474
|
+
// .occurrenceGroups[objectInformation.groupIndex];
|
|
1475
|
+
// // const cursorDistance: number = currentCursorGroup.down.position.getDistance(
|
|
1476
|
+
// // finalCursor.position
|
|
1477
|
+
// // );
|
|
1478
|
+
// const cursorDuration: number = Math.max(
|
|
1479
|
+
// 0,
|
|
1480
|
+
// currentCursorGroup.startTime - cursorGroup.endTime
|
|
1481
|
+
// );
|
|
1482
|
+
// // "Air velocity" is the cursor velocity from the moment the previous cursor release action to the next cursor press action.
|
|
1483
|
+
// // const airVelocity: number = cursorDistance / cursorDuration;
|
|
1484
|
+
// is2Handed =
|
|
1485
|
+
// // airVelocity / initialVelocity < 0.8 &&
|
|
1486
|
+
// cursorDuration / deltaTime > 0.2;
|
|
1487
|
+
// for (
|
|
1488
|
+
// let i = prevObjectInformation.occurrenceIndex + 1;
|
|
1489
|
+
// i < cursors.length && is2Handed;
|
|
1490
|
+
// ++i
|
|
1451
1491
|
// ) {
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
// const endCursorToCurrentMovement: Vector2 =
|
|
1455
|
-
// objectStartPosition.subtract(actualEndCursorPosition);
|
|
1456
|
-
// for (; cursorIndex < cursors.length; ++cursorIndex) {
|
|
1457
|
-
// const cursor: CursorOccurrence = cursors[cursorIndex];
|
|
1458
|
-
// const prevCursor: CursorOccurrence = cursors[cursorIndex - 1];
|
|
1492
|
+
// const cursor: CursorOccurrence = cursors[i];
|
|
1493
|
+
// const prevCursor: CursorOccurrence = cursors[i - 1];
|
|
1459
1494
|
// if (
|
|
1460
|
-
// cursor.
|
|
1461
|
-
//
|
|
1495
|
+
// cursor.position.equals(prevCursor.position) ||
|
|
1496
|
+
// cursor.time === prevCursor.time
|
|
1462
1497
|
// ) {
|
|
1463
|
-
// break;
|
|
1464
|
-
// }
|
|
1465
|
-
// // Only consider occurrences that actually move from one place to another.
|
|
1466
|
-
// if (cursor.position.equals(prevCursor.position)) {
|
|
1467
1498
|
// continue;
|
|
1468
1499
|
// }
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
// );
|
|
1472
|
-
// if (distance > 1.5 * object.getRadius(Modes.droid)) {
|
|
1473
|
-
// continue;
|
|
1474
|
-
// }
|
|
1475
|
-
// const currentMovement: Vector2 = cursor.position.subtract(
|
|
1476
|
-
// prevCursor.position
|
|
1477
|
-
// );
|
|
1478
|
-
// const dot: number = currentMovement.dot(
|
|
1479
|
-
// endCursorToCurrentMovement
|
|
1480
|
-
// );
|
|
1481
|
-
// const det: number =
|
|
1482
|
-
// currentMovement.x * endCursorToCurrentMovement.y -
|
|
1483
|
-
// currentMovement.y * endCursorToCurrentMovement.x;
|
|
1484
|
-
// const movementAngle: number = Math.abs(Math.atan2(det, dot));
|
|
1485
|
-
// finalAngle = Math.min(finalAngle, movementAngle);
|
|
1486
|
-
// }
|
|
1487
|
-
// if (
|
|
1488
|
-
// finalAngle === Number.POSITIVE_INFINITY &&
|
|
1489
|
-
// (finalAngles.length === 0 ||
|
|
1490
|
-
// finalAngles.every((v) => v === Number.POSITIVE_INFINITY))
|
|
1491
|
-
// ) {
|
|
1492
|
-
// // Special case where drag occurrences are in the same spot as the press.
|
|
1493
|
-
// const { down, moves } = cursorGroup;
|
|
1494
|
-
// if (
|
|
1495
|
-
// // Don't consider this if the press occurrence does not hit the object.
|
|
1496
|
-
// down.position.getDistance(prevObjectEndPosition) <=
|
|
1497
|
-
// object.getRadius(Modes.droid) &&
|
|
1498
|
-
// moves.every((v) => v.position.equals(down.position))
|
|
1499
|
-
// ) {
|
|
1500
|
-
// finalAngle = 0;
|
|
1500
|
+
// if (cursor.id === MovementType.up) {
|
|
1501
|
+
// break;
|
|
1501
1502
|
// }
|
|
1503
|
+
// // "Cursor velocity" is the velocity from the previous object's cursor release time to the current object's press time.
|
|
1504
|
+
// const cursorVelocity: number =
|
|
1505
|
+
// cursor.position.getDistance(prevCursor.position) /
|
|
1506
|
+
// (cursor.time - prevCursor.time);
|
|
1507
|
+
// is2Handed = cursorVelocity / initialVelocity < 0.8;
|
|
1502
1508
|
// }
|
|
1503
|
-
// finalAngles.push(finalAngle);
|
|
1504
1509
|
// }
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
// return new IndexedHitObject(
|
|
1510
|
-
// diffObject,
|
|
1511
|
-
// Number.isFinite(smallestAngle) ? finalIndex : -1,
|
|
1512
|
-
// Number.isFinite(smallestAngle) ? smallestAngle : null
|
|
1513
|
-
// );
|
|
1510
|
+
if (!Number.isFinite(finalAngle)) {
|
|
1511
|
+
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1512
|
+
}
|
|
1513
|
+
return new IndexedHitObject(diffObject, prevObjectInformation.cursorIndex, prevObjectInformation.groupIndex, prevObjectInformation.occurrenceIndex, finalAngle, is2Handed);
|
|
1514
1514
|
}
|
|
1515
1515
|
/**
|
|
1516
1516
|
* Gets the position of the cursor that presses an object.
|
|
@@ -1527,7 +1527,7 @@ class TwoHandChecker {
|
|
|
1527
1527
|
position: objectPosition,
|
|
1528
1528
|
cursorIndex: 0,
|
|
1529
1529
|
groupIndex: Number.POSITIVE_INFINITY,
|
|
1530
|
-
|
|
1530
|
+
occurrenceIndex: Number.POSITIVE_INFINITY,
|
|
1531
1531
|
cursorTime: object.startTime,
|
|
1532
1532
|
};
|
|
1533
1533
|
}
|
|
@@ -1580,7 +1580,7 @@ class TwoHandChecker {
|
|
|
1580
1580
|
position: down.position,
|
|
1581
1581
|
cursorIndex: i,
|
|
1582
1582
|
groupIndex: j,
|
|
1583
|
-
|
|
1583
|
+
occurrenceIndex: 0,
|
|
1584
1584
|
cursorTime: down.time,
|
|
1585
1585
|
};
|
|
1586
1586
|
}
|
|
@@ -1590,7 +1590,7 @@ class TwoHandChecker {
|
|
|
1590
1590
|
position: down.position,
|
|
1591
1591
|
cursorIndex: i,
|
|
1592
1592
|
groupIndex: j,
|
|
1593
|
-
|
|
1593
|
+
occurrenceIndex: 0,
|
|
1594
1594
|
cursorTime: down.time,
|
|
1595
1595
|
};
|
|
1596
1596
|
}
|
|
@@ -1659,7 +1659,7 @@ class TwoHandChecker {
|
|
|
1659
1659
|
position: nearestPosition,
|
|
1660
1660
|
cursorIndex: Number.POSITIVE_INFINITY,
|
|
1661
1661
|
groupIndex: Number.POSITIVE_INFINITY,
|
|
1662
|
-
|
|
1662
|
+
occurrenceIndex: Number.POSITIVE_INFINITY,
|
|
1663
1663
|
cursorTime: object.startTime,
|
|
1664
1664
|
};
|
|
1665
1665
|
}
|
|
@@ -1667,7 +1667,7 @@ class TwoHandChecker {
|
|
|
1667
1667
|
position: nearestPosition,
|
|
1668
1668
|
cursorIndex: nearestCursorIndex,
|
|
1669
1669
|
groupIndex: nearestGroupIndex,
|
|
1670
|
-
|
|
1670
|
+
occurrenceIndex: nearestCursorGroupIndex,
|
|
1671
1671
|
cursorTime: nearestCursorTime,
|
|
1672
1672
|
};
|
|
1673
1673
|
}
|
|
@@ -1760,7 +1760,7 @@ class TwoHandChecker {
|
|
|
1760
1760
|
position: nearestPosition,
|
|
1761
1761
|
cursorIndex: Number.POSITIVE_INFINITY,
|
|
1762
1762
|
groupIndex: Number.POSITIVE_INFINITY,
|
|
1763
|
-
|
|
1763
|
+
occurrenceIndex: Number.POSITIVE_INFINITY,
|
|
1764
1764
|
cursorTime: object.endTime,
|
|
1765
1765
|
};
|
|
1766
1766
|
}
|
|
@@ -1768,7 +1768,7 @@ class TwoHandChecker {
|
|
|
1768
1768
|
position: nearestPosition,
|
|
1769
1769
|
cursorIndex: nearestCursorIndex,
|
|
1770
1770
|
groupIndex: nearestGroupIndex,
|
|
1771
|
-
|
|
1771
|
+
occurrenceIndex: nearestCursorGroupIndex,
|
|
1772
1772
|
cursorTime: nearestCursorTime,
|
|
1773
1773
|
};
|
|
1774
1774
|
}
|
|
@@ -1804,6 +1804,7 @@ class TwoHandChecker {
|
|
|
1804
1804
|
this.indexedHitObjects.forEach((o) => {
|
|
1805
1805
|
if (!o.is2Handed) {
|
|
1806
1806
|
beatmaps[0].hitObjects.add(o.object.object);
|
|
1807
|
+
addToSecondBeatmap = true;
|
|
1807
1808
|
return;
|
|
1808
1809
|
}
|
|
1809
1810
|
const beatmap = addToSecondBeatmap
|
|
@@ -1861,6 +1862,173 @@ class TwoHandChecker {
|
|
|
1861
1862
|
}
|
|
1862
1863
|
}
|
|
1863
1864
|
|
|
1865
|
+
/**
|
|
1866
|
+
* Utility to check whether relevant sliders in a beatmap are cheesed.
|
|
1867
|
+
*/
|
|
1868
|
+
class SliderCheeseChecker {
|
|
1869
|
+
/**
|
|
1870
|
+
* The difficulty calculator that is being analyzed.
|
|
1871
|
+
*/
|
|
1872
|
+
calculator;
|
|
1873
|
+
/**
|
|
1874
|
+
* The data of the replay.
|
|
1875
|
+
*/
|
|
1876
|
+
data;
|
|
1877
|
+
/**
|
|
1878
|
+
* The osu!droid hitwindow of the analyzed beatmap.
|
|
1879
|
+
*/
|
|
1880
|
+
hitWindow;
|
|
1881
|
+
/**
|
|
1882
|
+
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
1883
|
+
*/
|
|
1884
|
+
hitWindow50;
|
|
1885
|
+
/**
|
|
1886
|
+
* The indexes of objects that were cheesed.
|
|
1887
|
+
*/
|
|
1888
|
+
cheesedObjectIndexes = [];
|
|
1889
|
+
/**
|
|
1890
|
+
* @param calculator The difficulty calculator to analyze.
|
|
1891
|
+
* @param data The data of the replay.
|
|
1892
|
+
*/
|
|
1893
|
+
constructor(calculator, data) {
|
|
1894
|
+
this.calculator = calculator;
|
|
1895
|
+
this.data = data;
|
|
1896
|
+
const stats = new osuBase.MapStats({
|
|
1897
|
+
od: this.calculator.beatmap.difficulty.od,
|
|
1898
|
+
mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
1899
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
1900
|
+
}).calculate();
|
|
1901
|
+
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
1902
|
+
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
1906
|
+
*
|
|
1907
|
+
* Returns a number that can be passed to a `DroidPerformanceCalculator`
|
|
1908
|
+
* to alter the aim performance value.
|
|
1909
|
+
*/
|
|
1910
|
+
check() {
|
|
1911
|
+
if (this.calculator.attributes.sliderCount === 0) {
|
|
1912
|
+
return 1;
|
|
1913
|
+
}
|
|
1914
|
+
this.checkSliderCheesing();
|
|
1915
|
+
return this.calculateSliderCheesePenalty();
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Checks for sliders that were cheesed.
|
|
1919
|
+
*/
|
|
1920
|
+
checkSliderCheesing() {
|
|
1921
|
+
// Current loop indexes are stored for efficiency.
|
|
1922
|
+
const cursorLoopIndexes = osuBase.Utils.initializeArray(10, 0);
|
|
1923
|
+
const acceptableRadius = this.calculator.objects[0].object.getRadius(osuBase.Modes.droid) * 2;
|
|
1924
|
+
for (let i = 0; i < this.calculator.objects.length; ++i) {
|
|
1925
|
+
const diffObject = this.calculator.objects[i];
|
|
1926
|
+
const { object } = diffObject;
|
|
1927
|
+
if (diffObject.travelDistance === 0 ||
|
|
1928
|
+
!(object instanceof osuBase.Slider)) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1932
|
+
const objectData = this.data.hitObjectData[i];
|
|
1933
|
+
// If a slider break occurs, we disregard the check for that slider.
|
|
1934
|
+
if (objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
let isCheesed = false;
|
|
1938
|
+
for (let j = 0; j < this.data.cursorMovement.length; ++j) {
|
|
1939
|
+
if (isCheesed) {
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
const cursorGroups = this.data.cursorMovement[j].occurrenceGroups;
|
|
1943
|
+
for (let k = cursorLoopIndexes[j]; k < cursorGroups.length; k = ++cursorLoopIndexes[j]) {
|
|
1944
|
+
const group = cursorGroups[k];
|
|
1945
|
+
if (group.startTime < object.startTime - this.hitWindow50) {
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
if (group.startTime > object.startTime + this.hitWindow50) {
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
if (group.down.position.getDistance(objectStartPosition) >
|
|
1952
|
+
acceptableRadius) {
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
// Track cursor movement to see if it lands on every tick.
|
|
1956
|
+
let occurrenceLoopIndex = 1;
|
|
1957
|
+
let isSliderFulfilled = true;
|
|
1958
|
+
const { allOccurrences } = group;
|
|
1959
|
+
for (let l = 1; l < object.nestedHitObjects.length; ++l) {
|
|
1960
|
+
if (!isSliderFulfilled) {
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
const tickWasHit = objectData.tickset[l - 1];
|
|
1964
|
+
if (!tickWasHit) {
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
const nestedObject = object.nestedHitObjects[l];
|
|
1968
|
+
const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
|
|
1969
|
+
while (occurrenceLoopIndex < allOccurrences.length &&
|
|
1970
|
+
allOccurrences[occurrenceLoopIndex].time <
|
|
1971
|
+
nestedObject.startTime) {
|
|
1972
|
+
++occurrenceLoopIndex;
|
|
1973
|
+
}
|
|
1974
|
+
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
const occurrence = allOccurrences[occurrenceLoopIndex];
|
|
1978
|
+
const prevOccurrence = allOccurrences[occurrenceLoopIndex - 1];
|
|
1979
|
+
switch (occurrence.id) {
|
|
1980
|
+
case exports.MovementType.move: {
|
|
1981
|
+
// Interpolate cursor position during nested object time.
|
|
1982
|
+
const t = (nestedObject.startTime -
|
|
1983
|
+
prevOccurrence.time) /
|
|
1984
|
+
(occurrence.time - prevOccurrence.time);
|
|
1985
|
+
const cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevOccurrence.position.x, occurrence.position.x, t), osuBase.Interpolation.lerp(prevOccurrence.position.y, occurrence.position.y, t));
|
|
1986
|
+
const distance = cursorPosition.getDistance(nestedPosition);
|
|
1987
|
+
if (distance > acceptableRadius) {
|
|
1988
|
+
isSliderFulfilled = false;
|
|
1989
|
+
}
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
case exports.MovementType.up:
|
|
1993
|
+
if (prevOccurrence.position.getDistance(nestedPosition) > acceptableRadius) {
|
|
1994
|
+
isSliderFulfilled = false;
|
|
1995
|
+
break;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (!isSliderFulfilled) {
|
|
2000
|
+
isCheesed = true;
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (isCheesed) {
|
|
2006
|
+
this.cheesedObjectIndexes.push(i);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Calculates the slider cheese penalty.
|
|
2012
|
+
*/
|
|
2013
|
+
calculateSliderCheesePenalty() {
|
|
2014
|
+
let calculator;
|
|
2015
|
+
if (this.calculator instanceof osuDifficultyCalculator.DroidDifficultyCalculator) {
|
|
2016
|
+
calculator = new osuDifficultyCalculator.DroidDifficultyCalculator(this.calculator.beatmap);
|
|
2017
|
+
}
|
|
2018
|
+
else {
|
|
2019
|
+
calculator = new osuRebalanceDifficultyCalculator.DroidDifficultyCalculator(this.calculator.beatmap);
|
|
2020
|
+
}
|
|
2021
|
+
Object.assign(calculator.attributes, this.calculator.attributes);
|
|
2022
|
+
calculator.stats = this.calculator.stats;
|
|
2023
|
+
calculator.generateDifficultyHitObjects();
|
|
2024
|
+
for (const index of this.cheesedObjectIndexes) {
|
|
2025
|
+
calculator.objects[index].travelDistance = 0;
|
|
2026
|
+
}
|
|
2027
|
+
calculator.calculateAim();
|
|
2028
|
+
return this.calculator.aim / calculator.aim;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1864
2032
|
/**
|
|
1865
2033
|
* A replay analyzer that analyzes a replay from osu!droid.
|
|
1866
2034
|
*
|
|
@@ -1890,7 +2058,7 @@ class ReplayAnalyzer {
|
|
|
1890
2058
|
*/
|
|
1891
2059
|
is2Hand;
|
|
1892
2060
|
/**
|
|
1893
|
-
* The beatmap that is being analyzed. `
|
|
2061
|
+
* The beatmap that is being analyzed. `DroidDifficultyCalculator` or `RebalanceDroidDifficultyCalculator` is required for three finger or two hand analyzing.
|
|
1894
2062
|
*/
|
|
1895
2063
|
beatmap;
|
|
1896
2064
|
/**
|
|
@@ -1905,6 +2073,10 @@ class ReplayAnalyzer {
|
|
|
1905
2073
|
* Penalty value used to penalize dpp for 3 finger abuse.
|
|
1906
2074
|
*/
|
|
1907
2075
|
tapPenalty = 1;
|
|
2076
|
+
/**
|
|
2077
|
+
* Penalty value used to penalize dpp for slider cheesing.
|
|
2078
|
+
*/
|
|
2079
|
+
sliderCheesePenalty = 1;
|
|
1908
2080
|
/**
|
|
1909
2081
|
* Whether this replay has been checked against 3 finger usage.
|
|
1910
2082
|
*/
|
|
@@ -1914,11 +2086,13 @@ class ReplayAnalyzer {
|
|
|
1914
2086
|
*/
|
|
1915
2087
|
hasBeenCheckedFor2Hand = false;
|
|
1916
2088
|
/**
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
* This is filled after 2 hand usage has been checked.
|
|
2089
|
+
* Whether this repla has been checked against slider cheesing.
|
|
1920
2090
|
*/
|
|
1921
|
-
|
|
2091
|
+
hasBeenCheckedForSliderCheesing = false;
|
|
2092
|
+
/**
|
|
2093
|
+
* The amount of two-handed objects.
|
|
2094
|
+
*/
|
|
2095
|
+
twoHandedNoteCount = 0;
|
|
1922
2096
|
// Sizes of primitive data types in Java (in bytes)
|
|
1923
2097
|
BYTE_LENGTH = 1;
|
|
1924
2098
|
SHORT_LENGTH = 2;
|
|
@@ -2364,7 +2538,7 @@ class ReplayAnalyzer {
|
|
|
2364
2538
|
/**
|
|
2365
2539
|
* Checks if a play is using 3 fingers.
|
|
2366
2540
|
*
|
|
2367
|
-
* Requires `analyze()` to be called first and `map` to be defined as `
|
|
2541
|
+
* Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
|
|
2368
2542
|
*/
|
|
2369
2543
|
checkFor3Finger() {
|
|
2370
2544
|
if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
@@ -2381,7 +2555,7 @@ class ReplayAnalyzer {
|
|
|
2381
2555
|
/**
|
|
2382
2556
|
* Checks if a play is using 2 hands.
|
|
2383
2557
|
*
|
|
2384
|
-
* Requires `analyze()` to be called first and `map` to be defined as `
|
|
2558
|
+
* Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
|
|
2385
2559
|
*/
|
|
2386
2560
|
checkFor2Hand() {
|
|
2387
2561
|
if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
@@ -2392,9 +2566,24 @@ class ReplayAnalyzer {
|
|
|
2392
2566
|
const twoHandChecker = new TwoHandChecker(this.beatmap, this.data);
|
|
2393
2567
|
const result = twoHandChecker.check();
|
|
2394
2568
|
this.is2Hand = result.is2Hand;
|
|
2395
|
-
this.
|
|
2569
|
+
this.twoHandedNoteCount = result.twoHandedNoteCount;
|
|
2396
2570
|
this.hasBeenCheckedFor2Hand = true;
|
|
2397
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Checks if a play has cheesed sliders.
|
|
2574
|
+
*
|
|
2575
|
+
* Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
|
|
2576
|
+
*/
|
|
2577
|
+
checkForSliderCheesing() {
|
|
2578
|
+
if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2579
|
+
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
|
|
2580
|
+
!this.data) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
const sliderCheeseChecker = new SliderCheeseChecker(this.beatmap, this.data);
|
|
2584
|
+
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
2585
|
+
this.hasBeenCheckedForSliderCheesing = true;
|
|
2586
|
+
}
|
|
2398
2587
|
}
|
|
2399
2588
|
|
|
2400
2589
|
/**
|