@rian8337/osu-droid-replay-analyzer 3.0.0-beta.26 → 3.0.0-beta.30
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 +549 -367
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/typings/index.d.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -128,10 +128,7 @@ class CursorOccurrenceGroup {
|
|
|
128
128
|
* The time at which this cursor occurrence group ends.
|
|
129
129
|
*/
|
|
130
130
|
get endTime() {
|
|
131
|
-
|
|
132
|
-
return this._up.time;
|
|
133
|
-
}
|
|
134
|
-
return this._moves.at(-1)?.time ?? this._down.time;
|
|
131
|
+
return this._up?.time ?? this._moves.at(-1)?.time ?? this._down.time;
|
|
135
132
|
}
|
|
136
133
|
/**
|
|
137
134
|
* All cursor occurrences in this group.
|
|
@@ -561,13 +558,10 @@ class ThreeFingerChecker {
|
|
|
561
558
|
const stats = new osuBase.MapStats({
|
|
562
559
|
od: this.calculator.beatmap.difficulty.od,
|
|
563
560
|
mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
564
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)
|
|
565
|
-
!(m instanceof osuBase.ModPrecise)),
|
|
561
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
566
562
|
}).calculate();
|
|
567
563
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
568
|
-
const strainNotes = calculator.objects.filter(
|
|
569
|
-
//@ts-expect-error: No overloads match, but this is fine.
|
|
570
|
-
(v) => v.originalTapStrain >= ThreeFingerChecker.strainThreshold);
|
|
564
|
+
const strainNotes = calculator.objects.filter((v) => v.originalTapStrain >= ThreeFingerChecker.strainThreshold);
|
|
571
565
|
this.strainNoteCount = strainNotes.length;
|
|
572
566
|
}
|
|
573
567
|
/**
|
|
@@ -1130,17 +1124,16 @@ class ThreeFingerChecker {
|
|
|
1130
1124
|
*/
|
|
1131
1125
|
class IndexedHitObject {
|
|
1132
1126
|
/**
|
|
1133
|
-
* The
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
* The actual index of the cursor that hits the hitobject.
|
|
1127
|
+
* The cursor index that hits the hitobject.
|
|
1128
|
+
*
|
|
1129
|
+
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1130
|
+
* the hitobject or it did not meet the criteria for detection.
|
|
1138
1131
|
*/
|
|
1139
|
-
|
|
1132
|
+
cursorIndex;
|
|
1140
1133
|
/**
|
|
1141
|
-
* The
|
|
1134
|
+
* The angle of the movement of the cursor towards the next hitobject.
|
|
1142
1135
|
*/
|
|
1143
|
-
|
|
1136
|
+
angle;
|
|
1144
1137
|
/**
|
|
1145
1138
|
* If this is a slider, whether the slider was cheesed.
|
|
1146
1139
|
*/
|
|
@@ -1149,17 +1142,21 @@ class IndexedHitObject {
|
|
|
1149
1142
|
* The underlying difficulty hitobject.
|
|
1150
1143
|
*/
|
|
1151
1144
|
object;
|
|
1145
|
+
/**
|
|
1146
|
+
* Whether the hitobject was most likely two-handed.
|
|
1147
|
+
*/
|
|
1148
|
+
get is2Handed() {
|
|
1149
|
+
return this.angle !== null && this.angle >= Math.PI / 6;
|
|
1150
|
+
}
|
|
1152
1151
|
/**
|
|
1153
1152
|
* @param object The underlying difficulty hitobject.
|
|
1154
|
-
* @param
|
|
1155
|
-
* @param
|
|
1156
|
-
* @param occurrenceIndex The occurrence index of the cursor that hits the hitobject.
|
|
1153
|
+
* @param cursorIndex The cursor index that moves towards the hitobject.
|
|
1154
|
+
* @param angle The angle of the movement of the cursor that moves towards the hitobject.
|
|
1157
1155
|
*/
|
|
1158
|
-
constructor(object,
|
|
1156
|
+
constructor(object, cursorIndex, angle) {
|
|
1159
1157
|
this.object = object;
|
|
1160
|
-
this.
|
|
1161
|
-
this.
|
|
1162
|
-
this.occurrenceIndex = occurrenceIndex;
|
|
1158
|
+
this.cursorIndex = cursorIndex;
|
|
1159
|
+
this.angle = angle;
|
|
1163
1160
|
}
|
|
1164
1161
|
}
|
|
1165
1162
|
|
|
@@ -1184,19 +1181,9 @@ class TwoHandChecker {
|
|
|
1184
1181
|
*/
|
|
1185
1182
|
hitWindow;
|
|
1186
1183
|
/**
|
|
1187
|
-
* The
|
|
1188
|
-
*
|
|
1189
|
-
* This is used to prevent excessive penalty by splitting the beatmap into
|
|
1190
|
-
* those that do not worth any strain.
|
|
1184
|
+
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
1191
1185
|
*/
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* A cursor occurrence nested array containing all cursor occurrences.
|
|
1195
|
-
*
|
|
1196
|
-
* Each index represents the cursor index.
|
|
1197
|
-
*/
|
|
1198
|
-
// TODO: replace with group at some point
|
|
1199
|
-
allCursorOccurrences = [];
|
|
1186
|
+
hitWindow50;
|
|
1200
1187
|
/**
|
|
1201
1188
|
* @param calculator The difficulty calculator to analyze.
|
|
1202
1189
|
* @param data The data of the replay.
|
|
@@ -1204,14 +1191,13 @@ class TwoHandChecker {
|
|
|
1204
1191
|
constructor(calculator, data) {
|
|
1205
1192
|
this.calculator = calculator;
|
|
1206
1193
|
this.data = data;
|
|
1207
|
-
this.allCursorOccurrences = data.cursorMovement.map((v) => v.allOccurrences);
|
|
1208
1194
|
const stats = new osuBase.MapStats({
|
|
1209
1195
|
od: this.calculator.beatmap.difficulty.od,
|
|
1210
1196
|
mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
1211
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)
|
|
1212
|
-
!(m instanceof osuBase.ModPrecise)),
|
|
1197
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
1213
1198
|
}).calculate();
|
|
1214
1199
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
1200
|
+
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1215
1201
|
}
|
|
1216
1202
|
/**
|
|
1217
1203
|
* Checks if a beatmap is two-handed.
|
|
@@ -1222,337 +1208,549 @@ class TwoHandChecker {
|
|
|
1222
1208
|
}
|
|
1223
1209
|
this.indexHitObjects();
|
|
1224
1210
|
this.applyPenalty();
|
|
1225
|
-
|
|
1226
|
-
for (const object of this.indexedHitObjects) {
|
|
1227
|
-
++indexes[object.acceptedCursorIndex];
|
|
1228
|
-
}
|
|
1229
|
-
return {
|
|
1230
|
-
is2Hand: indexes.filter((v) => v > 0).length !== 1,
|
|
1231
|
-
cursorIndexes: this.indexedHitObjects.map((v) => v.acceptedCursorIndex),
|
|
1232
|
-
};
|
|
1211
|
+
return { is2Hand: false, cursorIndexes: [] };
|
|
1233
1212
|
}
|
|
1234
1213
|
/**
|
|
1235
1214
|
* Converts hitobjects into indexed hit objects.
|
|
1236
1215
|
*/
|
|
1237
1216
|
indexHitObjects() {
|
|
1238
|
-
const hitWindowOffset = this.getHitWindowOffset();
|
|
1239
1217
|
const indexes = [];
|
|
1240
1218
|
for (let i = 0; i < this.calculator.objects.length; ++i) {
|
|
1241
|
-
const indexedHitObject = this.getIndexedHitObject(i
|
|
1242
|
-
|
|
1219
|
+
const indexedHitObject = this.getIndexedHitObject(i);
|
|
1220
|
+
indexedHitObject.sliderCheesed = this.checkSliderCheesing(indexedHitObject, this.data.hitObjectData[i]);
|
|
1221
|
+
indexes.push(indexedHitObject.cursorIndex);
|
|
1243
1222
|
this.indexedHitObjects.push(indexedHitObject);
|
|
1244
1223
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
continue;
|
|
1249
|
-
}
|
|
1250
|
-
++indexCounts[index];
|
|
1251
|
-
}
|
|
1252
|
-
const mainCursorIndex = indexCounts.length > 0
|
|
1253
|
-
? indexCounts.indexOf(Math.max(...indexCounts))
|
|
1254
|
-
: 0;
|
|
1255
|
-
const ignoredCursorIndexes = [];
|
|
1256
|
-
for (let i = 0; i < indexCounts.length; ++i) {
|
|
1257
|
-
if (indexCounts[i] < this.minCursorIndexCount &&
|
|
1258
|
-
i !== mainCursorIndex) {
|
|
1259
|
-
ignoredCursorIndexes.push(i);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
// Add cursor presses that don't fulfill minimum cursor
|
|
1263
|
-
// count to the farthest cursor index that isn't 0.
|
|
1264
|
-
let defaultMinCursorCountIndex = this.data.cursorMovement.length - 1;
|
|
1265
|
-
for (; defaultMinCursorCountIndex > 0; --defaultMinCursorCountIndex) {
|
|
1266
|
-
if (indexCounts[defaultMinCursorCountIndex] >=
|
|
1267
|
-
this.minCursorIndexCount &&
|
|
1268
|
-
!ignoredCursorIndexes.includes(defaultMinCursorCountIndex)) {
|
|
1269
|
-
break;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
this.indexedHitObjects.forEach((indexedHitObject, i) => {
|
|
1273
|
-
if (indexedHitObject.acceptedCursorIndex === -1 ||
|
|
1274
|
-
indexedHitObject.actualCursorIndex === -1) {
|
|
1275
|
-
indexedHitObject.acceptedCursorIndex = mainCursorIndex;
|
|
1276
|
-
indexedHitObject.actualCursorIndex = mainCursorIndex;
|
|
1277
|
-
}
|
|
1278
|
-
if (ignoredCursorIndexes.includes(indexedHitObject.acceptedCursorIndex)) {
|
|
1279
|
-
indexedHitObject.acceptedCursorIndex =
|
|
1280
|
-
defaultMinCursorCountIndex;
|
|
1281
|
-
}
|
|
1282
|
-
// For sliders, we need to consider two cases. The first case is when the player doesn't drag
|
|
1283
|
-
// the slider. The second case is when the player drags the slider.
|
|
1284
|
-
if (indexedHitObject.object.object instanceof osuBase.Slider) {
|
|
1285
|
-
indexedHitObject.sliderCheesed = this.checkSliderCheesing(indexedHitObject, this.data.hitObjectData[i], hitWindowOffset);
|
|
1286
|
-
}
|
|
1287
|
-
});
|
|
1224
|
+
console.log(this.indexedHitObjects.filter((v) => v.cursorIndex !== -1).length +
|
|
1225
|
+
1, "indexes found,", this.indexedHitObjects.filter((v) => v.cursorIndex === -1).length -
|
|
1226
|
+
1, "not found,", this.indexedHitObjects.filter((v) => v.is2Handed).length, "2 handed,", this.indexedHitObjects.filter((v) => !v.is2Handed).length, "not 2 handed");
|
|
1288
1227
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1289
|
-
console.log("Index", i, "count:", this.indexedHitObjects.filter((v) => v.
|
|
1290
|
-
}
|
|
1228
|
+
console.log("Index", i, "count:", this.indexedHitObjects.filter((v) => v.cursorIndex === i).length);
|
|
1229
|
+
}
|
|
1230
|
+
// TODO: solve -1 cursor indexes
|
|
1231
|
+
console.table(this.indexedHitObjects
|
|
1232
|
+
.filter((v) =>
|
|
1233
|
+
// v.cursorIndex === -1 &&
|
|
1234
|
+
// v.object.aimStrainWithSliders > 200 &&
|
|
1235
|
+
v.object.deltaTime > 75 &&
|
|
1236
|
+
v.is2Handed)
|
|
1237
|
+
.map((v) => {
|
|
1238
|
+
return {
|
|
1239
|
+
startTime: v.object.object.startTime,
|
|
1240
|
+
type: v.object.object.typeStr(),
|
|
1241
|
+
strain: v.object.aimStrainWithSliders,
|
|
1242
|
+
hitAngle: v.angle !== null
|
|
1243
|
+
? osuBase.MathUtils.radiansToDegrees(v.angle)
|
|
1244
|
+
: null,
|
|
1245
|
+
objectAngle: v.object.angle !== null
|
|
1246
|
+
? osuBase.MathUtils.radiansToDegrees(v.object.angle)
|
|
1247
|
+
: null,
|
|
1248
|
+
cursorIndex: v.cursorIndex,
|
|
1249
|
+
};
|
|
1250
|
+
}));
|
|
1291
1251
|
}
|
|
1292
1252
|
/**
|
|
1293
|
-
* Gets the
|
|
1253
|
+
* Gets the cursor index that hits the given object.
|
|
1254
|
+
*
|
|
1255
|
+
* @param objectIndex The index of the object to check.
|
|
1256
|
+
* @returns The cursor index that hits the given object, -1 if the index is not found, the object is a spinner, or the object was missed.
|
|
1294
1257
|
*/
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1258
|
+
getIndexedHitObject(objectIndex) {
|
|
1259
|
+
const diffObject = this.calculator.objects[objectIndex];
|
|
1260
|
+
const { object } = diffObject;
|
|
1261
|
+
// We don't care about the first object and spinners.
|
|
1262
|
+
if (objectIndex === 0 || object instanceof osuBase.Spinner) {
|
|
1263
|
+
return new IndexedHitObject(diffObject, -1, null);
|
|
1264
|
+
}
|
|
1265
|
+
// We don't care if the aim strain is too low.
|
|
1266
|
+
if (diffObject.aimStrainWithSliders < 200) {
|
|
1267
|
+
return new IndexedHitObject(diffObject, -1, null);
|
|
1268
|
+
}
|
|
1269
|
+
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1270
|
+
const prevObjectData = this.data.hitObjectData[objectIndex - 1];
|
|
1271
|
+
if (prevObject instanceof osuBase.Spinner ||
|
|
1272
|
+
prevObjectData.result === exports.HitResult.miss) {
|
|
1273
|
+
return new IndexedHitObject(diffObject, -1, null);
|
|
1274
|
+
}
|
|
1275
|
+
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1276
|
+
let prevObjectEndPosition = prevObject.getStackedEndPosition(osuBase.Modes.droid);
|
|
1277
|
+
if (prevObject instanceof osuBase.Slider) {
|
|
1278
|
+
if (prevObject.lazyTravelDistance > 0) {
|
|
1279
|
+
const lazyEndMovement = objectStartPosition.subtract(prevObject.lazyEndPosition);
|
|
1280
|
+
const actualEndMovement = objectStartPosition.subtract(prevObjectEndPosition);
|
|
1281
|
+
if (lazyEndMovement.length < actualEndMovement.length) {
|
|
1282
|
+
prevObjectEndPosition = prevObject.lazyEndPosition;
|
|
1310
1283
|
}
|
|
1311
1284
|
}
|
|
1285
|
+
else {
|
|
1286
|
+
prevObjectEndPosition = prevObject.getStackedPosition(osuBase.Modes.droid);
|
|
1287
|
+
}
|
|
1312
1288
|
}
|
|
1313
|
-
|
|
1289
|
+
const prevToCurrentMovement = object
|
|
1290
|
+
.getStackedPosition(osuBase.Modes.droid)
|
|
1291
|
+
.subtract(prevObjectEndPosition);
|
|
1292
|
+
// Don't consider objects that are too close to each other.
|
|
1293
|
+
if (prevToCurrentMovement.length <= object.getRadius(osuBase.Modes.droid)) {
|
|
1294
|
+
return new IndexedHitObject(diffObject, -1, null);
|
|
1295
|
+
}
|
|
1296
|
+
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
1297
|
+
// the next object in fast patterns. We should not be worried about slow patterns as they will only
|
|
1298
|
+
// make a minimal difference and aim strain threshold should filter them out.
|
|
1299
|
+
// In order to verify if the player does that, we check if the movement towards the current
|
|
1300
|
+
// object is sufficient enough to be two-handed. This is done by first gathering info about two cursor positions:
|
|
1301
|
+
// 1. The cursor position at the end of the previous object
|
|
1302
|
+
// 2. The cursor position that presses the current object
|
|
1303
|
+
// and then get the movement vector between both positions, then see if the cursor movement after the previous object
|
|
1304
|
+
// was pressed produces an angle that is acute enough with respect to the aforementioned movement vector.
|
|
1305
|
+
const objectInformation = this.getCursorPositionForObjectStart(objectIndex);
|
|
1306
|
+
const prevObjectInformation = this.getCursorPositionForObjectEnd(objectIndex - 1);
|
|
1307
|
+
if (prevObjectInformation.position.x === Number.POSITIVE_INFINITY) {
|
|
1308
|
+
return new IndexedHitObject(diffObject, -1, null);
|
|
1309
|
+
}
|
|
1310
|
+
const cursorData = this.data.cursorMovement[prevObjectInformation.cursorIndex];
|
|
1311
|
+
// There can be multiple angles to which the cursor moves towards the next object.
|
|
1312
|
+
// For this, we take the smallest angle.
|
|
1313
|
+
let finalAngle = Number.POSITIVE_INFINITY;
|
|
1314
|
+
const cursorGroup = cursorData.occurrenceGroups[prevObjectInformation.groupIndex];
|
|
1315
|
+
const cursors = cursorGroup.allOccurrences;
|
|
1316
|
+
const prevToCurrentCursorMovement = objectInformation.position.subtract(prevObjectInformation.position);
|
|
1317
|
+
for (let i = prevObjectInformation.cursorGroupIndex + 1; i < cursors.length; ++i) {
|
|
1318
|
+
const cursor = cursors[i];
|
|
1319
|
+
const prevCursor = cursors[i - 1];
|
|
1320
|
+
if (cursor.position.equals(prevCursor.position)) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (cursor.id === exports.MovementType.up) {
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
const currentMovement = cursor.position.subtract(prevCursor.position);
|
|
1327
|
+
const dot = prevToCurrentCursorMovement.dot(currentMovement);
|
|
1328
|
+
const det = prevToCurrentCursorMovement.x * currentMovement.y -
|
|
1329
|
+
prevToCurrentCursorMovement.y * currentMovement.x;
|
|
1330
|
+
const movementAngle = Math.abs(Math.atan2(det, dot));
|
|
1331
|
+
finalAngle = Math.min(finalAngle, movementAngle);
|
|
1332
|
+
}
|
|
1333
|
+
return new IndexedHitObject(diffObject, Number.isFinite(finalAngle)
|
|
1334
|
+
? prevObjectInformation.cursorIndex
|
|
1335
|
+
: -1, Number.isFinite(finalAngle) ? finalAngle : null);
|
|
1336
|
+
// let checkingTime: number = prevObject.endTime;
|
|
1337
|
+
// if (prevObject instanceof Circle) {
|
|
1338
|
+
// checkingTime += prevObjectData.accuracy;
|
|
1339
|
+
// } else if (prevDiffObject.travelDistance === 0) {
|
|
1340
|
+
// // If the slider doesn't have any travel distance, use the start time as the checking point.
|
|
1341
|
+
// checkingTime = prevObject.startTime;
|
|
1342
|
+
// if (prevObjectData.accuracy !== Math.floor(this.hitWindow50) + 13) {
|
|
1343
|
+
// checkingTime += prevObjectData.accuracy;
|
|
1344
|
+
// }
|
|
1345
|
+
// }
|
|
1346
|
+
// let cursorTimeLimit: number = object.startTime;
|
|
1347
|
+
// if (object instanceof Circle) {
|
|
1348
|
+
// cursorTimeLimit += objectData.accuracy;
|
|
1349
|
+
// } else if (object instanceof Slider) {
|
|
1350
|
+
// if (objectData.accuracy !== Math.floor(this.hitWindow50) + 13) {
|
|
1351
|
+
// cursorTimeLimit += objectData.accuracy;
|
|
1352
|
+
// } else {
|
|
1353
|
+
// cursorTimeLimit += this.hitWindow50;
|
|
1354
|
+
// }
|
|
1355
|
+
// }
|
|
1356
|
+
// let hitWindow: number = this.hitWindow50;
|
|
1357
|
+
// const isPrecise: boolean = this.data.convertedMods.some(
|
|
1358
|
+
// (m) => m instanceof ModPrecise
|
|
1359
|
+
// );
|
|
1360
|
+
// // For sliders, set the hit window to as lenient as possible.
|
|
1361
|
+
// if (object instanceof Circle) {
|
|
1362
|
+
// switch (objectData.result) {
|
|
1363
|
+
// case HitResult.great:
|
|
1364
|
+
// hitWindow = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1365
|
+
// break;
|
|
1366
|
+
// case HitResult.good:
|
|
1367
|
+
// hitWindow = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1368
|
+
// break;
|
|
1369
|
+
// }
|
|
1370
|
+
// }
|
|
1371
|
+
// // There can be multiple angles to which every cursor moves towards the next object.
|
|
1372
|
+
// // For this, we take the smallest angle from a cursor.
|
|
1373
|
+
// const finalAngles: number[] = Utils.initializeArray(
|
|
1374
|
+
// this.data.cursorMovement.length,
|
|
1375
|
+
// Number.POSITIVE_INFINITY
|
|
1376
|
+
// );
|
|
1377
|
+
// for (
|
|
1378
|
+
// let i = prevObjectInformation.cursorIndex;
|
|
1379
|
+
// i < this.data.cursorMovement.length;
|
|
1380
|
+
// ++i
|
|
1381
|
+
// ) {
|
|
1382
|
+
// }
|
|
1383
|
+
// for (const cursor of this.data.cursorMovement) {
|
|
1384
|
+
// // TODO: use same method for getCursorPositionForObject
|
|
1385
|
+
// const cursorGroup: CursorOccurrenceGroup | undefined =
|
|
1386
|
+
// cursor.occurrenceGroups.find((v) => v.isActiveAt(checkingTime));
|
|
1387
|
+
// if (!cursorGroup) {
|
|
1388
|
+
// continue;
|
|
1389
|
+
// }
|
|
1390
|
+
// const cursors: CursorOccurrence[] = cursorGroup.allOccurrences;
|
|
1391
|
+
// // This is the best approximation we can come up with, which is the cursor position
|
|
1392
|
+
// // at the previous object's end time.
|
|
1393
|
+
// let cursorIndex: number = cursors.findIndex(
|
|
1394
|
+
// (v) => v.time >= checkingTime
|
|
1395
|
+
// );
|
|
1396
|
+
// const endTimeCursor: CursorOccurrence = cursors[cursorIndex];
|
|
1397
|
+
// if (endTimeCursor.id === MovementType.up) {
|
|
1398
|
+
// continue;
|
|
1399
|
+
// }
|
|
1400
|
+
// let actualEndCursorPosition: Vector2 = endTimeCursor.position;
|
|
1401
|
+
// if (endTimeCursor.id === MovementType.move) {
|
|
1402
|
+
// // Interpolate end cursor up to the point of checking time.
|
|
1403
|
+
// const prevCursor: CursorOccurrence = cursors[cursorIndex - 1];
|
|
1404
|
+
// const t: number =
|
|
1405
|
+
// (checkingTime - prevCursor.time) /
|
|
1406
|
+
// (endTimeCursor.time - prevCursor.time);
|
|
1407
|
+
// actualEndCursorPosition = new Vector2(
|
|
1408
|
+
// Interpolation.lerp(
|
|
1409
|
+
// prevCursor.position.x,
|
|
1410
|
+
// endTimeCursor.position.x,
|
|
1411
|
+
// t
|
|
1412
|
+
// ),
|
|
1413
|
+
// Interpolation.lerp(
|
|
1414
|
+
// prevCursor.position.y,
|
|
1415
|
+
// endTimeCursor.position.y,
|
|
1416
|
+
// t
|
|
1417
|
+
// )
|
|
1418
|
+
// );
|
|
1419
|
+
// }
|
|
1420
|
+
// // There can be multiple angles to which the cursor moves towards the next object.
|
|
1421
|
+
// // For this, we take the smallest angle.
|
|
1422
|
+
// let finalAngle: number = Number.POSITIVE_INFINITY;
|
|
1423
|
+
// // Start the loop at index + 1 so that the loop considers the next cursor as opposed to
|
|
1424
|
+
// // the assigned cursor above. Don't do it if the next cursor is an up occurrence, though.
|
|
1425
|
+
// if (
|
|
1426
|
+
// cursorIndex === 0 ||
|
|
1427
|
+
// (cursorIndex < cursors.length - 2 &&
|
|
1428
|
+
// !cursors[cursorIndex + 1].position.equals(
|
|
1429
|
+
// endTimeCursor.position
|
|
1430
|
+
// ))
|
|
1431
|
+
// ) {
|
|
1432
|
+
// ++cursorIndex;
|
|
1433
|
+
// }
|
|
1434
|
+
// const endCursorToCurrentMovement: Vector2 =
|
|
1435
|
+
// objectStartPosition.subtract(actualEndCursorPosition);
|
|
1436
|
+
// for (; cursorIndex < cursors.length; ++cursorIndex) {
|
|
1437
|
+
// const cursor: CursorOccurrence = cursors[cursorIndex];
|
|
1438
|
+
// const prevCursor: CursorOccurrence = cursors[cursorIndex - 1];
|
|
1439
|
+
// if (
|
|
1440
|
+
// cursor.id === MovementType.up ||
|
|
1441
|
+
// prevCursor.time >= cursorTimeLimit
|
|
1442
|
+
// ) {
|
|
1443
|
+
// break;
|
|
1444
|
+
// }
|
|
1445
|
+
// // Only consider occurrences that actually move from one place to another.
|
|
1446
|
+
// if (cursor.position.equals(prevCursor.position)) {
|
|
1447
|
+
// continue;
|
|
1448
|
+
// }
|
|
1449
|
+
// const distance: number = cursor.position.getDistance(
|
|
1450
|
+
// actualEndCursorPosition
|
|
1451
|
+
// );
|
|
1452
|
+
// if (distance > 1.5 * object.getRadius(Modes.droid)) {
|
|
1453
|
+
// continue;
|
|
1454
|
+
// }
|
|
1455
|
+
// const currentMovement: Vector2 = cursor.position.subtract(
|
|
1456
|
+
// prevCursor.position
|
|
1457
|
+
// );
|
|
1458
|
+
// const dot: number = currentMovement.dot(
|
|
1459
|
+
// endCursorToCurrentMovement
|
|
1460
|
+
// );
|
|
1461
|
+
// const det: number =
|
|
1462
|
+
// currentMovement.x * endCursorToCurrentMovement.y -
|
|
1463
|
+
// currentMovement.y * endCursorToCurrentMovement.x;
|
|
1464
|
+
// const movementAngle: number = Math.abs(Math.atan2(det, dot));
|
|
1465
|
+
// finalAngle = Math.min(finalAngle, movementAngle);
|
|
1466
|
+
// }
|
|
1467
|
+
// if (
|
|
1468
|
+
// finalAngle === Number.POSITIVE_INFINITY &&
|
|
1469
|
+
// (finalAngles.length === 0 ||
|
|
1470
|
+
// finalAngles.every((v) => v === Number.POSITIVE_INFINITY))
|
|
1471
|
+
// ) {
|
|
1472
|
+
// // Special case where drag occurrences are in the same spot as the press.
|
|
1473
|
+
// const { down, moves } = cursorGroup;
|
|
1474
|
+
// if (
|
|
1475
|
+
// // Don't consider this if the press occurrence does not hit the object.
|
|
1476
|
+
// down.position.getDistance(prevObjectEndPosition) <=
|
|
1477
|
+
// object.getRadius(Modes.droid) &&
|
|
1478
|
+
// moves.every((v) => v.position.equals(down.position))
|
|
1479
|
+
// ) {
|
|
1480
|
+
// finalAngle = 0;
|
|
1481
|
+
// }
|
|
1482
|
+
// }
|
|
1483
|
+
// finalAngles.push(finalAngle);
|
|
1484
|
+
// }
|
|
1485
|
+
// const smallestAngle: number = Math.min(...finalAngles);
|
|
1486
|
+
// const finalIndex: number = finalAngles.findIndex(
|
|
1487
|
+
// (v) => v === smallestAngle
|
|
1488
|
+
// );
|
|
1489
|
+
// return new IndexedHitObject(
|
|
1490
|
+
// diffObject,
|
|
1491
|
+
// Number.isFinite(smallestAngle) ? finalIndex : -1,
|
|
1492
|
+
// Number.isFinite(smallestAngle) ? smallestAngle : null
|
|
1493
|
+
// );
|
|
1314
1494
|
}
|
|
1315
1495
|
/**
|
|
1316
|
-
* Gets the cursor
|
|
1496
|
+
* Gets the position of the cursor that presses an object.
|
|
1317
1497
|
*
|
|
1318
|
-
* @param
|
|
1319
|
-
* @
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
if (object
|
|
1326
|
-
|
|
1327
|
-
|
|
1498
|
+
* @param objectIndex THe index of the object.
|
|
1499
|
+
* @returns The position of the cursor that presses the object.
|
|
1500
|
+
*/
|
|
1501
|
+
getCursorPositionForObjectStart(objectIndex) {
|
|
1502
|
+
const object = this.calculator.beatmap.hitObjects.objects[objectIndex];
|
|
1503
|
+
const data = this.data.hitObjectData[objectIndex];
|
|
1504
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1505
|
+
if (object instanceof osuBase.Spinner) {
|
|
1506
|
+
return {
|
|
1507
|
+
position: objectPosition,
|
|
1508
|
+
cursorIndex: 0,
|
|
1509
|
+
groupIndex: Number.POSITIVE_INFINITY,
|
|
1510
|
+
cursorGroupIndex: Number.POSITIVE_INFINITY,
|
|
1511
|
+
cursorTime: object.startTime,
|
|
1512
|
+
};
|
|
1328
1513
|
}
|
|
1514
|
+
const radius = object.getRadius(osuBase.Modes.droid);
|
|
1515
|
+
let hitWindow = this.hitWindow50;
|
|
1329
1516
|
const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1330
|
-
// For sliders,
|
|
1331
|
-
|
|
1332
|
-
if (!(object.object instanceof osuBase.Slider)) {
|
|
1517
|
+
// For sliders, set the hit window to as lenient as possible.
|
|
1518
|
+
if (object instanceof osuBase.Circle) {
|
|
1333
1519
|
switch (data.result) {
|
|
1334
1520
|
case exports.HitResult.great:
|
|
1335
|
-
|
|
1521
|
+
hitWindow = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1336
1522
|
break;
|
|
1337
1523
|
case exports.HitResult.good:
|
|
1338
|
-
|
|
1524
|
+
hitWindow = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1339
1525
|
break;
|
|
1340
1526
|
}
|
|
1341
1527
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
let hitTimeAfterIndex = c.findIndex(
|
|
1354
|
-
// There is a special case for sliders where the time leniency in droid is a lot bigger compared to PC.
|
|
1355
|
-
// To prevent slider end time from ending earlier than hit window leniency, we use the maximum value between both.
|
|
1356
|
-
(v) => v.time >= Math.max(object.object.endTime, maximumHitTime));
|
|
1357
|
-
if (hitTimeAfterIndex === -1) {
|
|
1358
|
-
// Maximum hit time or object end time may be out of bounds for every presses.
|
|
1359
|
-
// We set the index to the latest cursor occurrence if that happens.
|
|
1360
|
-
hitTimeAfterIndex = c.length;
|
|
1361
|
-
}
|
|
1362
|
-
--hitTimeAfterIndex;
|
|
1363
|
-
// Sometimes a `movementType.UP` instance occurs at the same time as a `movementType.MOVE`
|
|
1364
|
-
// or a cursor is recorded twice in one time, therefore this check is required.
|
|
1365
|
-
while (c[hitTimeBeforeIndex]?.time ===
|
|
1366
|
-
c[hitTimeBeforeIndex - 1]?.time &&
|
|
1367
|
-
hitTimeBeforeIndex > 0) {
|
|
1368
|
-
--hitTimeBeforeIndex;
|
|
1528
|
+
// TODO: what to do for head sliderbreaks?
|
|
1529
|
+
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
|
|
1530
|
+
let nearestCursorIndex = 0;
|
|
1531
|
+
let nearestGroupIndex = 0;
|
|
1532
|
+
let nearestCursorGroupIndex = 0;
|
|
1533
|
+
let nearestCursorTime = 0;
|
|
1534
|
+
const minimumActiveTime = object.startTime - hitWindow;
|
|
1535
|
+
const maximumActiveTime = object.startTime + hitWindow;
|
|
1536
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1537
|
+
if (nearestPosition.getDistance(objectPosition) <= radius) {
|
|
1538
|
+
break;
|
|
1369
1539
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
let j = hitTimeBeforeIndex;
|
|
1374
|
-
for (j; j <= hitTimeAfterIndex; ++j) {
|
|
1375
|
-
const occurrence = c[j];
|
|
1376
|
-
const nextOccurrence = c[j + 1];
|
|
1377
|
-
const cursorPosition = occurrence.position;
|
|
1378
|
-
if (occurrence.time < minimumHitTime &&
|
|
1379
|
-
nextOccurrence?.id !== exports.MovementType.move) {
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
if (occurrence.time > hitTime + hitWindowOffset) {
|
|
1383
|
-
// Set distance to minimum just for the last.
|
|
1384
|
-
if (occurrence.id !== exports.MovementType.up) {
|
|
1385
|
-
distance = Math.min(distance, object.object
|
|
1386
|
-
.getStackedPosition(osuBase.Modes.droid)
|
|
1387
|
-
.getDistance(cursorPosition));
|
|
1388
|
-
}
|
|
1540
|
+
const cursorData = this.data.cursorMovement[i];
|
|
1541
|
+
for (let j = 0; j < cursorData.occurrenceGroups.length; ++j) {
|
|
1542
|
+
if (nearestPosition.getDistance(objectPosition) <= radius) {
|
|
1389
1543
|
break;
|
|
1390
1544
|
}
|
|
1391
|
-
|
|
1545
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
1546
|
+
if (cursorGroup.endTime < minimumActiveTime) {
|
|
1392
1547
|
continue;
|
|
1393
1548
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1549
|
+
if (cursorGroup.startTime > maximumActiveTime) {
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
// Validate the down press first.
|
|
1553
|
+
const { down } = cursorGroup;
|
|
1554
|
+
if (down.position.getDistance(objectPosition) <= radius &&
|
|
1555
|
+
Math.abs(down.time - object.startTime) <= hitWindow) {
|
|
1556
|
+
if (objectIndex > 0) {
|
|
1557
|
+
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1558
|
+
if (down.time > prevObject.endTime) {
|
|
1559
|
+
return {
|
|
1560
|
+
position: down.position,
|
|
1561
|
+
cursorIndex: i,
|
|
1562
|
+
groupIndex: j,
|
|
1563
|
+
cursorGroupIndex: 0,
|
|
1564
|
+
cursorTime: down.time,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
return {
|
|
1570
|
+
position: down.position,
|
|
1571
|
+
cursorIndex: i,
|
|
1572
|
+
groupIndex: j,
|
|
1573
|
+
cursorGroupIndex: 0,
|
|
1574
|
+
cursorTime: down.time,
|
|
1575
|
+
};
|
|
1410
1576
|
}
|
|
1411
1577
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1578
|
+
const cursors = cursorGroup.allOccurrences;
|
|
1579
|
+
for (let k = 1; k < cursors.length; ++k) {
|
|
1580
|
+
const cursor = cursors[k];
|
|
1581
|
+
const prevCursor = cursors[k - 1];
|
|
1582
|
+
if (cursor.time < minimumActiveTime) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (prevCursor.time > maximumActiveTime) {
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1588
|
+
let cursorPosition;
|
|
1589
|
+
if (cursor.id === exports.MovementType.up) {
|
|
1590
|
+
cursorPosition = prevCursor.position;
|
|
1591
|
+
const distance = cursorPosition.getDistance(objectPosition);
|
|
1592
|
+
if (distance <
|
|
1593
|
+
nearestPosition.getDistance(objectPosition)) {
|
|
1594
|
+
nearestPosition = cursorPosition;
|
|
1595
|
+
nearestCursorIndex = i;
|
|
1596
|
+
nearestGroupIndex = j;
|
|
1597
|
+
nearestCursorGroupIndex = k;
|
|
1598
|
+
nearestCursorTime = prevCursor.time;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
// Check for cursor presses inbetween occurrences.
|
|
1603
|
+
for (let k = 0; k < this.data.cursorMovement.length; ++k) {
|
|
1604
|
+
// Skip the current cursor index.
|
|
1605
|
+
if (k === i) {
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const cursorGroup = this.data.cursorMovement[k].occurrenceGroups.find((v) => v.down.time >= prevCursor.time &&
|
|
1609
|
+
v.down.time <= cursor.time);
|
|
1610
|
+
if (!cursorGroup) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const t = (cursorGroup.down.time - prevCursor.time) /
|
|
1614
|
+
(cursor.time - prevCursor.time);
|
|
1615
|
+
cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
|
|
1616
|
+
const distance = cursorPosition.getDistance(objectPosition);
|
|
1617
|
+
if (distance <
|
|
1618
|
+
nearestPosition.getDistance(objectPosition)) {
|
|
1619
|
+
nearestPosition = cursorPosition;
|
|
1620
|
+
nearestCursorIndex = i;
|
|
1621
|
+
nearestGroupIndex = j;
|
|
1622
|
+
nearestCursorGroupIndex = k;
|
|
1623
|
+
nearestCursorTime = cursorGroup.down.time;
|
|
1624
|
+
}
|
|
1625
|
+
if (distance <= radius) {
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (nearestPosition.getDistance(objectPosition) <= radius) {
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1436
1633
|
}
|
|
1437
1634
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1635
|
+
}
|
|
1636
|
+
if (nearestPosition.getDistance(objectPosition) ===
|
|
1637
|
+
Number.POSITIVE_INFINITY) {
|
|
1638
|
+
return {
|
|
1639
|
+
position: nearestPosition,
|
|
1640
|
+
cursorIndex: Number.POSITIVE_INFINITY,
|
|
1641
|
+
groupIndex: Number.POSITIVE_INFINITY,
|
|
1642
|
+
cursorGroupIndex: Number.POSITIVE_INFINITY,
|
|
1643
|
+
cursorTime: object.startTime,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
position: nearestPosition,
|
|
1648
|
+
cursorIndex: nearestCursorIndex,
|
|
1649
|
+
groupIndex: nearestGroupIndex,
|
|
1650
|
+
cursorGroupIndex: nearestCursorGroupIndex,
|
|
1651
|
+
cursorTime: nearestCursorTime,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Gets the position of the cursor that at an object's end position.
|
|
1656
|
+
*
|
|
1657
|
+
* @param objectIndex THe index of the object.
|
|
1658
|
+
* @returns The position of the cursor at the object's end position.
|
|
1659
|
+
*/
|
|
1660
|
+
getCursorPositionForObjectEnd(objectIndex) {
|
|
1661
|
+
const object = this.calculator.beatmap.hitObjects.objects[objectIndex];
|
|
1662
|
+
if (!(object instanceof osuBase.Slider)) {
|
|
1663
|
+
return this.getCursorPositionForObjectStart(objectIndex);
|
|
1664
|
+
}
|
|
1665
|
+
const nextObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1666
|
+
let objectEndPosition = object.getStackedEndPosition(osuBase.Modes.droid);
|
|
1667
|
+
if (object.lazyTravelDistance > 0 && nextObject) {
|
|
1668
|
+
const nextStartPosition = nextObject.getStackedPosition(osuBase.Modes.droid);
|
|
1669
|
+
const lazyEndMovement = nextStartPosition.subtract(object.lazyEndPosition);
|
|
1670
|
+
const actualEndMovement = nextStartPosition.subtract(objectEndPosition);
|
|
1671
|
+
if (lazyEndMovement.length < actualEndMovement.length) {
|
|
1672
|
+
objectEndPosition = object.lazyEndPosition;
|
|
1442
1673
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
// const next: DifficultyHitObject | RebalanceDifficultyHitObject =
|
|
1459
|
-
// this.map.objects[index + 1];
|
|
1460
|
-
// Angle detection.
|
|
1461
|
-
/* if (nextSignificantOccurrence?.id === movementType.MOVE && next) {
|
|
1462
|
-
// Get the object's actual end position.
|
|
1463
|
-
let actualEndPosition: Vector2 =
|
|
1464
|
-
object.object.stackedEndPosition;
|
|
1465
|
-
|
|
1466
|
-
if (
|
|
1467
|
-
object.object instanceof Slider &&
|
|
1468
|
-
object.object.lazyEndPosition
|
|
1469
|
-
) {
|
|
1470
|
-
// For sliders, we take the closest distance between the lazy end position
|
|
1471
|
-
// and stacked end position towards the next significant cursor occurrence.
|
|
1472
|
-
// This assumes that the player takes the simpler movement.
|
|
1473
|
-
const lazyEndDistance: number =
|
|
1474
|
-
object.object.lazyEndPosition.getDistance(
|
|
1475
|
-
nextSignificantOccurrence.position
|
|
1476
|
-
);
|
|
1477
|
-
const actualEndDistance: number =
|
|
1478
|
-
object.object.stackedEndPosition.getDistance(
|
|
1479
|
-
nextSignificantOccurrence.position
|
|
1480
|
-
);
|
|
1481
|
-
|
|
1482
|
-
if (lazyEndDistance < actualEndDistance) {
|
|
1483
|
-
actualEndPosition = object.object.lazyEndPosition;
|
|
1484
|
-
}
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
objectEndPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1677
|
+
}
|
|
1678
|
+
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
|
|
1679
|
+
let nearestCursorIndex = 0;
|
|
1680
|
+
let nearestGroupIndex = 0;
|
|
1681
|
+
let nearestCursorGroupIndex = 0;
|
|
1682
|
+
let nearestCursorTime = 0;
|
|
1683
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1684
|
+
const cursorData = this.data.cursorMovement[i];
|
|
1685
|
+
for (let j = 0; j < cursorData.occurrenceGroups.length; ++j) {
|
|
1686
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
1687
|
+
if (cursorGroup.endTime < object.startTime) {
|
|
1688
|
+
continue;
|
|
1485
1689
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
// the next cursor occurrence with the object's position.
|
|
1489
|
-
const movementVec: Vector2 =
|
|
1490
|
-
nextSignificantOccurrence.position.subtract(
|
|
1491
|
-
actualEndPosition
|
|
1492
|
-
);
|
|
1493
|
-
|
|
1494
|
-
const currentToNext: Vector2 =
|
|
1495
|
-
next.object.getStackedPosition(Modes.droid).subtract(actualEndPosition);
|
|
1496
|
-
|
|
1497
|
-
const dot: number = currentToNext.dot(movementVec);
|
|
1498
|
-
const det: number =
|
|
1499
|
-
currentToNext.x * movementVec.y -
|
|
1500
|
-
currentToNext.y * movementVec.x;
|
|
1501
|
-
|
|
1502
|
-
const movementToNextAngle: number = Math.abs(
|
|
1503
|
-
Math.atan2(det, dot)
|
|
1504
|
-
);
|
|
1505
|
-
|
|
1506
|
-
isAngleFulfilled = movementToNextAngle < Math.PI / 6;
|
|
1507
|
-
} */
|
|
1508
|
-
// Dragging detection.
|
|
1509
|
-
let dragTimeThreshold = 0;
|
|
1510
|
-
const prev = this.calculator.objects[index - 1];
|
|
1511
|
-
if (prev) {
|
|
1512
|
-
// The previous object might be a slider, so we need to get
|
|
1513
|
-
// the hit data of it to get an accurate time threshold.
|
|
1514
|
-
const prevData = this.data.hitObjectData[index - 1];
|
|
1515
|
-
dragTimeThreshold = prev.object.startTime;
|
|
1516
|
-
if (!(prev.object instanceof osuBase.Spinner)) {
|
|
1517
|
-
dragTimeThreshold += prevData.accuracy;
|
|
1690
|
+
if (cursorGroup.startTime > object.endTime) {
|
|
1691
|
+
break;
|
|
1518
1692
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1693
|
+
const cursors = cursorGroup.allOccurrences;
|
|
1694
|
+
for (let k = 0; k < cursors.length; ++k) {
|
|
1695
|
+
const cursor = cursors[k];
|
|
1696
|
+
let cursorPosition;
|
|
1697
|
+
switch (cursor.id) {
|
|
1698
|
+
case exports.MovementType.down:
|
|
1699
|
+
cursorPosition = cursor.position;
|
|
1700
|
+
break;
|
|
1701
|
+
case exports.MovementType.up: {
|
|
1702
|
+
const prevCursor = cursors[k - 1];
|
|
1703
|
+
cursorPosition = prevCursor.position;
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
case exports.MovementType.move: {
|
|
1707
|
+
const prevCursor = cursors[k - 1];
|
|
1708
|
+
const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
|
|
1709
|
+
(cursor.time - prevCursor.time), 0, 1);
|
|
1710
|
+
cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (cursorPosition.getDistance(objectEndPosition) <
|
|
1715
|
+
nearestPosition.getDistance(objectEndPosition)) {
|
|
1716
|
+
nearestPosition = cursorPosition;
|
|
1717
|
+
nearestCursorIndex = i;
|
|
1718
|
+
nearestGroupIndex = j;
|
|
1719
|
+
switch (cursor.id) {
|
|
1720
|
+
case exports.MovementType.down:
|
|
1721
|
+
nearestCursorGroupIndex = k;
|
|
1722
|
+
nearestCursorTime = cursor.time;
|
|
1723
|
+
break;
|
|
1724
|
+
case exports.MovementType.up:
|
|
1725
|
+
nearestCursorGroupIndex = k - 1;
|
|
1726
|
+
nearestCursorTime = cursors[k - 1].time;
|
|
1727
|
+
break;
|
|
1728
|
+
case exports.MovementType.move:
|
|
1729
|
+
nearestCursorGroupIndex = k;
|
|
1730
|
+
nearestCursorTime = object.endTime;
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1521
1734
|
}
|
|
1522
1735
|
}
|
|
1523
|
-
let occurrenceStartIndex = hitTimeAfterIndex;
|
|
1524
|
-
while (c[occurrenceStartIndex]?.time >= dragTimeThreshold &&
|
|
1525
|
-
occurrenceStartIndex > 0) {
|
|
1526
|
-
--occurrenceStartIndex;
|
|
1527
|
-
}
|
|
1528
|
-
// The above loop will make the start index before or right when
|
|
1529
|
-
// the previous object was hit or ended, but we want the index after it.
|
|
1530
|
-
++occurrenceStartIndex;
|
|
1531
|
-
const dragOccurrences = c.slice(occurrenceStartIndex, hitTimeAfterIndex);
|
|
1532
|
-
isDragged =
|
|
1533
|
-
dragOccurrences.length > 0 &&
|
|
1534
|
-
// We only care when the cursor approaches the current object. It doesn't
|
|
1535
|
-
// matter whether the previous object was pressed or dragged.
|
|
1536
|
-
dragOccurrences
|
|
1537
|
-
.at(-1)
|
|
1538
|
-
.position.getDistance(object.object.getStackedPosition(osuBase.Modes.droid)) <= object.object.getRadius(osuBase.Modes.droid) &&
|
|
1539
|
-
dragOccurrences.every((v) => v.id === exports.MovementType.move);
|
|
1540
|
-
cursorInformations.push({
|
|
1541
|
-
// If the angle is fulfilled or the player dragged,
|
|
1542
|
-
// we set the cursor index to the main cursor index.
|
|
1543
|
-
acceptedCursorIndex: /* isAngleFulfilled || */ isDragged
|
|
1544
|
-
? -1
|
|
1545
|
-
: i,
|
|
1546
|
-
actualCursorIndex: i,
|
|
1547
|
-
occurrenceIndex: j,
|
|
1548
|
-
distanceDiff: distance,
|
|
1549
|
-
});
|
|
1550
1736
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1737
|
+
if (nearestPosition.getDistance(objectEndPosition) ===
|
|
1738
|
+
Number.POSITIVE_INFINITY) {
|
|
1739
|
+
return {
|
|
1740
|
+
position: nearestPosition,
|
|
1741
|
+
cursorIndex: Number.POSITIVE_INFINITY,
|
|
1742
|
+
groupIndex: Number.POSITIVE_INFINITY,
|
|
1743
|
+
cursorGroupIndex: Number.POSITIVE_INFINITY,
|
|
1744
|
+
cursorTime: object.endTime,
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
position: nearestPosition,
|
|
1749
|
+
cursorIndex: nearestCursorIndex,
|
|
1750
|
+
groupIndex: nearestGroupIndex,
|
|
1751
|
+
cursorGroupIndex: nearestCursorGroupIndex,
|
|
1752
|
+
cursorTime: nearestCursorTime,
|
|
1753
|
+
};
|
|
1556
1754
|
}
|
|
1557
1755
|
/**
|
|
1558
1756
|
* Checks whether a slider was cheesed.
|
|
@@ -1561,57 +1759,38 @@ class TwoHandChecker {
|
|
|
1561
1759
|
*
|
|
1562
1760
|
* @param indexedHitObject The indexed slider.
|
|
1563
1761
|
* @param hitData The hit data of the slider.
|
|
1564
|
-
* @param actualCursorIndex The actual cursor index that hit the slider.
|
|
1565
|
-
* @param hitWindowOffset The offset that was calculated by `getHitWindowOffset()`
|
|
1566
1762
|
* @returns Whether the slider was cheesed.
|
|
1567
1763
|
*/
|
|
1568
|
-
checkSliderCheesing(indexedHitObject, hitData
|
|
1764
|
+
checkSliderCheesing(indexedHitObject, hitData) {
|
|
1569
1765
|
if (!(indexedHitObject.object.object instanceof osuBase.Slider) ||
|
|
1570
|
-
hitData.result === exports.HitResult.miss
|
|
1766
|
+
hitData.result === exports.HitResult.miss ||
|
|
1767
|
+
indexedHitObject.cursorIndex === -1) {
|
|
1571
1768
|
return false;
|
|
1572
1769
|
}
|
|
1573
|
-
let cursorLoopIndex = Math.max(0, indexedHitObject.occurrenceIndex);
|
|
1574
|
-
const c = this.allCursorOccurrences[indexedHitObject.actualCursorIndex];
|
|
1575
|
-
const acceptableRadius = indexedHitObject.object.object.getRadius(osuBase.Modes.droid) * 2.4;
|
|
1576
|
-
for (let i = 1; i < indexedHitObject.object.object.nestedHitObjects.length; ++i) {
|
|
1577
|
-
const tickWasHit = hitData.tickset[i - 1];
|
|
1578
|
-
if (!tickWasHit) {
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
const object = indexedHitObject.object.object.nestedHitObjects[i];
|
|
1582
|
-
let j = cursorLoopIndex;
|
|
1583
|
-
let cursorHitTick = false;
|
|
1584
|
-
for (j; j < c.length; ++j) {
|
|
1585
|
-
if (c[j].time < object.startTime - hitWindowOffset) {
|
|
1586
|
-
continue;
|
|
1587
|
-
}
|
|
1588
|
-
if (c[j].time > object.startTime + hitWindowOffset) {
|
|
1589
|
-
break;
|
|
1590
|
-
}
|
|
1591
|
-
if (c[j].position.getDistance(object.getStackedPosition(osuBase.Modes.droid)) <= acceptableRadius) {
|
|
1592
|
-
cursorHitTick = true;
|
|
1593
|
-
break;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
if (!cursorHitTick) {
|
|
1597
|
-
return true;
|
|
1598
|
-
}
|
|
1599
|
-
cursorLoopIndex = j;
|
|
1600
|
-
}
|
|
1601
1770
|
return false;
|
|
1602
1771
|
}
|
|
1603
1772
|
/**
|
|
1604
1773
|
* Applies penalty to the original star rating instance.
|
|
1605
1774
|
*/
|
|
1606
1775
|
applyPenalty() {
|
|
1607
|
-
const beatmaps =
|
|
1776
|
+
const beatmaps = [
|
|
1777
|
+
osuBase.Utils.deepCopy(this.calculator.beatmap),
|
|
1778
|
+
osuBase.Utils.deepCopy(this.calculator.beatmap),
|
|
1779
|
+
];
|
|
1780
|
+
for (const beatmap of beatmaps) {
|
|
1781
|
+
beatmap.hitObjects.clear();
|
|
1782
|
+
}
|
|
1783
|
+
let addToSecondBeatmap = true;
|
|
1608
1784
|
this.indexedHitObjects.forEach((o) => {
|
|
1609
|
-
if (!
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
beatmaps[o.acceptedCursorIndex] = map;
|
|
1785
|
+
if (!o.is2Handed) {
|
|
1786
|
+
beatmaps[0].hitObjects.add(o.object.object);
|
|
1787
|
+
return;
|
|
1613
1788
|
}
|
|
1614
|
-
|
|
1789
|
+
const beatmap = addToSecondBeatmap
|
|
1790
|
+
? beatmaps[1]
|
|
1791
|
+
: beatmaps[0];
|
|
1792
|
+
beatmap.hitObjects.add(o.object.object);
|
|
1793
|
+
addToSecondBeatmap = !addToSecondBeatmap;
|
|
1615
1794
|
});
|
|
1616
1795
|
// Preserve some values that aren't reasonable for them to be changed.
|
|
1617
1796
|
const preservedValues = this.calculator.objects.map((v) => {
|
|
@@ -1624,7 +1803,7 @@ class TwoHandChecker {
|
|
|
1624
1803
|
});
|
|
1625
1804
|
this.calculator.objects.length = 0;
|
|
1626
1805
|
beatmaps.forEach((beatmap) => {
|
|
1627
|
-
if (
|
|
1806
|
+
if (beatmap.hitObjects.objects.length === 0) {
|
|
1628
1807
|
return;
|
|
1629
1808
|
}
|
|
1630
1809
|
const difficultyCalculator = Object.assign(osuBase.Utils.deepCopy(this.calculator), { beatmap: beatmap });
|
|
@@ -1641,6 +1820,9 @@ class TwoHandChecker {
|
|
|
1641
1820
|
const diffObject = this.calculator.objects[i];
|
|
1642
1821
|
const indexedHitObject = this.indexedHitObjects[i];
|
|
1643
1822
|
const preservedValue = preservedValues[i];
|
|
1823
|
+
diffObject.index = i - 1;
|
|
1824
|
+
diffObject.hitObjects.length = 0;
|
|
1825
|
+
diffObject.hitObjects.push(...this.calculator.objects);
|
|
1644
1826
|
diffObject.noteDensity = preservedValue.noteDensity;
|
|
1645
1827
|
diffObject.overlappingFactor = preservedValue.overlappingFactor;
|
|
1646
1828
|
diffObject.rhythmStrain = preservedValue.rhythmStrain;
|
|
@@ -2207,7 +2389,7 @@ class ReplayObjectData {
|
|
|
2207
2389
|
* For circles, this is the offset at which the circle was hit. If the hit accuracy is 10000, it means the circle was tapped too late ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/HitCircle.java#L298-L306)).
|
|
2208
2390
|
*
|
|
2209
2391
|
* For sliders, this is the offset at which the slider head was hit. For
|
|
2210
|
-
* sliderbreaks, the accuracy would be `(hit window 50)
|
|
2392
|
+
* sliderbreaks, the accuracy would be `Math.floor(<hit window 50>ms) + 13ms` ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821)).
|
|
2211
2393
|
*
|
|
2212
2394
|
* For spinners, this is the total amount at which the spinner was spinned:
|
|
2213
2395
|
* ```js
|