@rian8337/osu-droid-replay-analyzer 3.0.0-beta.1 → 3.0.0-beta.11
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 +384 -168
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/typings/index.d.ts +142 -31
package/dist/index.js
CHANGED
|
@@ -29,6 +29,27 @@ function _interopNamespace(e) {
|
|
|
29
29
|
|
|
30
30
|
var javaDeserialization__namespace = /*#__PURE__*/_interopNamespace(javaDeserialization);
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Movement types of a cursor in an osu!droid replay.
|
|
34
|
+
*
|
|
35
|
+
* The cursor movement is represented as a player's action on the screen.
|
|
36
|
+
*/
|
|
37
|
+
exports.MovementType = void 0;
|
|
38
|
+
(function (MovementType) {
|
|
39
|
+
/**
|
|
40
|
+
* The player places their finger on the screen.
|
|
41
|
+
*/
|
|
42
|
+
MovementType[MovementType["down"] = 0] = "down";
|
|
43
|
+
/**
|
|
44
|
+
* The player drags their finger on the screen.
|
|
45
|
+
*/
|
|
46
|
+
MovementType[MovementType["move"] = 1] = "move";
|
|
47
|
+
/**
|
|
48
|
+
* The player releases their finger from the screen.
|
|
49
|
+
*/
|
|
50
|
+
MovementType[MovementType["up"] = 2] = "up";
|
|
51
|
+
})(exports.MovementType || (exports.MovementType = {}));
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Represents a cursor's occurrence.
|
|
34
55
|
*/
|
|
@@ -52,21 +73,218 @@ class CursorOccurrence {
|
|
|
52
73
|
}
|
|
53
74
|
}
|
|
54
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Represents a group of cursor occurrences representing a cursor instance's
|
|
78
|
+
* movement when a player places their finger on the screen.
|
|
79
|
+
*/
|
|
80
|
+
class CursorOccurrenceGroup {
|
|
81
|
+
/**
|
|
82
|
+
* The cursor occurrence of movement type `movementType.DOWN`.
|
|
83
|
+
*/
|
|
84
|
+
get down() {
|
|
85
|
+
return this._down;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The cursor occurrence of movement type `movementType.DOWN`.
|
|
89
|
+
*/
|
|
90
|
+
set down(value) {
|
|
91
|
+
if (value.id !== exports.MovementType.down) {
|
|
92
|
+
throw new TypeError("Attempting to set the down cursor occurrence to one with different movement type.");
|
|
93
|
+
}
|
|
94
|
+
this._down = value;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The cursor occurrences of movement type `movementType.MOVE`.
|
|
98
|
+
*/
|
|
99
|
+
get moves() {
|
|
100
|
+
return this._moves;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The cursor occurrence of movement type `movementType.UP`.
|
|
104
|
+
*
|
|
105
|
+
* May not exist, such as when the player holds their cursor until the end of a beatmap.
|
|
106
|
+
*/
|
|
107
|
+
get up() {
|
|
108
|
+
return this._up;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The cursor occurrence of movement type `movementType.UP`.
|
|
112
|
+
*
|
|
113
|
+
* May not exist, such as when the player holds their cursor until the end of a beatmap.
|
|
114
|
+
*/
|
|
115
|
+
set up(value) {
|
|
116
|
+
if (value && value.id !== exports.MovementType.up) {
|
|
117
|
+
throw new TypeError("Attempting to set the up cursor occurrence to one with different movement type.");
|
|
118
|
+
}
|
|
119
|
+
this._up = value;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The time at which this cursor occurrence group starts.
|
|
123
|
+
*/
|
|
124
|
+
get startTime() {
|
|
125
|
+
return this._down.time;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The time at which this cursor occurrence group ends.
|
|
129
|
+
*/
|
|
130
|
+
get endTime() {
|
|
131
|
+
if (this._up) {
|
|
132
|
+
return this._up.time;
|
|
133
|
+
}
|
|
134
|
+
return this._moves.at(-1)?.time ?? this._down.time;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* All cursor occurrences in this group.
|
|
138
|
+
*
|
|
139
|
+
* This iterates all occurrences and as such should be used sparingly or stored locally.
|
|
140
|
+
*/
|
|
141
|
+
get allOccurrences() {
|
|
142
|
+
const cursors = [this._down, ...this._moves];
|
|
143
|
+
if (this._up) {
|
|
144
|
+
cursors.push(this._up);
|
|
145
|
+
}
|
|
146
|
+
return cursors;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The cursor occurrence of movement type `movementType.DOWN`.
|
|
150
|
+
*/
|
|
151
|
+
_down;
|
|
152
|
+
/**
|
|
153
|
+
* The cursor occurrences of movement type `movementType.MOVE`.
|
|
154
|
+
*/
|
|
155
|
+
_moves;
|
|
156
|
+
/**
|
|
157
|
+
* The cursor occurrence of movement type `movementType.UP`.
|
|
158
|
+
*
|
|
159
|
+
* May not exist, such as when the player holds their cursor until the end of a beatmap.
|
|
160
|
+
*/
|
|
161
|
+
_up;
|
|
162
|
+
constructor(down, moves, up) {
|
|
163
|
+
this._down = down;
|
|
164
|
+
this._moves = moves;
|
|
165
|
+
// Re-set down cursor occurrence for checking.
|
|
166
|
+
this.down = down;
|
|
167
|
+
this.up = up;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Determines whether this cursor occurrence group is active at the specified time.
|
|
171
|
+
*
|
|
172
|
+
* @param time The time.
|
|
173
|
+
* @returns Whether this cursor occurrence group is active at the specified time.
|
|
174
|
+
*/
|
|
175
|
+
isActiveAt(time) {
|
|
176
|
+
return time >= this.startTime && time <= this.endTime;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Finds the cursor occurrence that is active at a given time.
|
|
180
|
+
*
|
|
181
|
+
* @param time The time.
|
|
182
|
+
* @returns The cursor occurrence at the given time, `null` if not found.
|
|
183
|
+
*/
|
|
184
|
+
cursorAt(time) {
|
|
185
|
+
if (!this.isActiveAt(time)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (this._down.time === time) {
|
|
189
|
+
return this._down;
|
|
190
|
+
}
|
|
191
|
+
if (this._up?.time === time) {
|
|
192
|
+
return this._up;
|
|
193
|
+
}
|
|
194
|
+
let l = 0;
|
|
195
|
+
let r = this._moves.length - 2;
|
|
196
|
+
while (l <= r) {
|
|
197
|
+
const pivot = l + ((r - l) >> 1);
|
|
198
|
+
if (this._moves[pivot].time < time) {
|
|
199
|
+
l = pivot + 1;
|
|
200
|
+
}
|
|
201
|
+
else if (this._moves[pivot].time > time) {
|
|
202
|
+
r = pivot - 1;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
return this._moves[pivot];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// l will be the first cursor occurrence with time > this._moves[l].time, but we want the one before it
|
|
209
|
+
return this._moves[l - 1];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
55
213
|
/**
|
|
56
214
|
* Represents a cursor instance in an osu!droid replay.
|
|
57
215
|
*
|
|
58
|
-
* Stores cursor movement data in the form of `
|
|
216
|
+
* Stores cursor movement data in the form of `CursorOccurrenceGroup`s.
|
|
59
217
|
*
|
|
60
218
|
* This is used when analyzing replays using replay analyzer.
|
|
61
219
|
*/
|
|
62
220
|
class CursorData {
|
|
63
221
|
/**
|
|
64
|
-
* The
|
|
222
|
+
* The occurrence groups of this cursor instance.
|
|
223
|
+
*/
|
|
224
|
+
occurrenceGroups = [];
|
|
225
|
+
/**
|
|
226
|
+
* The time at which the first occurrence of this cursor instance occurs.
|
|
227
|
+
*
|
|
228
|
+
* Will return `null` if there are no occurrences.
|
|
65
229
|
*/
|
|
66
|
-
|
|
230
|
+
get earliestOccurrenceTime() {
|
|
231
|
+
return this.occurrenceGroups.at(0)?.startTime ?? null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The time at which the latest occurrence of this cursor instance occurs.
|
|
235
|
+
*
|
|
236
|
+
* Will return `null` if there are no occurrences.
|
|
237
|
+
*/
|
|
238
|
+
get latestOccurrenceTime() {
|
|
239
|
+
return this.occurrenceGroups.at(-1)?.endTime ?? null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The amount of cursor occurrences of this cursor instance.
|
|
243
|
+
*/
|
|
244
|
+
get totalOccurrences() {
|
|
245
|
+
return this.occurrenceGroups.reduce((a, v) => {
|
|
246
|
+
// Down cursor.
|
|
247
|
+
++a;
|
|
248
|
+
// Move cursors.
|
|
249
|
+
a += v.moves.length;
|
|
250
|
+
if (v.up) {
|
|
251
|
+
// Up cursor.
|
|
252
|
+
++a;
|
|
253
|
+
}
|
|
254
|
+
return a;
|
|
255
|
+
}, 0);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* All cursor occurrences of this cursor instnace.
|
|
259
|
+
*
|
|
260
|
+
* This iterates all occurrence groups and as such should be used sparingly or stored locally.
|
|
261
|
+
*/
|
|
262
|
+
get allOccurrences() {
|
|
263
|
+
return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
|
|
264
|
+
}
|
|
67
265
|
constructor(values) {
|
|
266
|
+
let downOccurrence = null;
|
|
267
|
+
let moveOccurrences = [];
|
|
68
268
|
for (let i = 0; i < values.size; ++i) {
|
|
69
|
-
|
|
269
|
+
const occurrence = new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]);
|
|
270
|
+
switch (occurrence.id) {
|
|
271
|
+
case exports.MovementType.down:
|
|
272
|
+
downOccurrence = occurrence;
|
|
273
|
+
break;
|
|
274
|
+
case exports.MovementType.move:
|
|
275
|
+
moveOccurrences.push(occurrence);
|
|
276
|
+
break;
|
|
277
|
+
case exports.MovementType.up:
|
|
278
|
+
if (downOccurrence) {
|
|
279
|
+
this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences, occurrence));
|
|
280
|
+
downOccurrence = null;
|
|
281
|
+
}
|
|
282
|
+
moveOccurrences = [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Add the final cursor occurrence group as the loop may not catch it for special cases.
|
|
286
|
+
if (downOccurrence && moveOccurrences.length > 0) {
|
|
287
|
+
this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences));
|
|
70
288
|
}
|
|
71
289
|
}
|
|
72
290
|
}
|
|
@@ -74,35 +292,25 @@ class CursorData {
|
|
|
74
292
|
/**
|
|
75
293
|
* The result of a hit in an osu!droid replay.
|
|
76
294
|
*/
|
|
77
|
-
exports.
|
|
78
|
-
(function (
|
|
295
|
+
exports.HitResult = void 0;
|
|
296
|
+
(function (HitResult) {
|
|
79
297
|
/**
|
|
80
298
|
* Miss (0).
|
|
81
299
|
*/
|
|
82
|
-
|
|
300
|
+
HitResult[HitResult["miss"] = 1] = "miss";
|
|
83
301
|
/**
|
|
84
302
|
* Meh (50).
|
|
85
303
|
*/
|
|
86
|
-
|
|
304
|
+
HitResult[HitResult["meh"] = 2] = "meh";
|
|
87
305
|
/**
|
|
88
|
-
*
|
|
306
|
+
* Good (100).
|
|
89
307
|
*/
|
|
90
|
-
|
|
308
|
+
HitResult[HitResult["good"] = 3] = "good";
|
|
91
309
|
/**
|
|
92
|
-
*
|
|
310
|
+
* Great (300).
|
|
93
311
|
*/
|
|
94
|
-
|
|
95
|
-
})(exports.
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Movement type of a cursor in an osu!droid replay.
|
|
99
|
-
*/
|
|
100
|
-
exports.movementType = void 0;
|
|
101
|
-
(function (movementType) {
|
|
102
|
-
movementType[movementType["DOWN"] = 0] = "DOWN";
|
|
103
|
-
movementType[movementType["MOVE"] = 1] = "MOVE";
|
|
104
|
-
movementType[movementType["UP"] = 2] = "UP";
|
|
105
|
-
})(exports.movementType || (exports.movementType = {}));
|
|
312
|
+
HitResult[HitResult["great"] = 4] = "great";
|
|
313
|
+
})(exports.HitResult || (exports.HitResult = {}));
|
|
106
314
|
|
|
107
315
|
/**
|
|
108
316
|
* Represents a replay data in an osu!droid replay.
|
|
@@ -327,9 +535,17 @@ class ThreeFingerChecker {
|
|
|
327
535
|
*/
|
|
328
536
|
breakPointAccurateTimes = [];
|
|
329
537
|
/**
|
|
330
|
-
* A cursor
|
|
538
|
+
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
539
|
+
*
|
|
540
|
+
* Each index represents the cursor index.
|
|
331
541
|
*/
|
|
332
542
|
downCursorInstances = [];
|
|
543
|
+
/**
|
|
544
|
+
* All cursor occurrences in the replay.
|
|
545
|
+
*
|
|
546
|
+
* Each index represents the cursor index.
|
|
547
|
+
*/
|
|
548
|
+
allCursorInstances;
|
|
333
549
|
/**
|
|
334
550
|
* Nerf factors from all sections that were three-fingered.
|
|
335
551
|
*/
|
|
@@ -341,10 +557,12 @@ class ThreeFingerChecker {
|
|
|
341
557
|
constructor(calculator, data) {
|
|
342
558
|
this.calculator = calculator;
|
|
343
559
|
this.data = data;
|
|
560
|
+
this.allCursorInstances = data.cursorMovement.map((v) => v.allOccurrences);
|
|
344
561
|
const stats = new osuBase.MapStats({
|
|
345
562
|
od: this.calculator.beatmap.difficulty.od,
|
|
346
563
|
mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
347
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)
|
|
564
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym) &&
|
|
565
|
+
!(m instanceof osuBase.ModPrecise)),
|
|
348
566
|
}).calculate();
|
|
349
567
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
350
568
|
const strainNotes = calculator.objects.filter(
|
|
@@ -374,16 +592,14 @@ class ThreeFingerChecker {
|
|
|
374
592
|
}
|
|
375
593
|
this.getAccurateBreakPoints();
|
|
376
594
|
this.filterCursorInstances();
|
|
377
|
-
if (this.downCursorInstances.filter((v) => v.
|
|
378
|
-
.length <= 3) {
|
|
595
|
+
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
379
596
|
return { is3Finger: false, penalty: 1 };
|
|
380
597
|
}
|
|
381
598
|
this.getBeatmapSections();
|
|
382
599
|
this.detectDragPlay();
|
|
383
600
|
this.getDetailedBeatmapSections();
|
|
384
601
|
this.preventAccidentalTaps();
|
|
385
|
-
if (this.downCursorInstances.filter((v) => v.
|
|
386
|
-
.length <= 3) {
|
|
602
|
+
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
387
603
|
return { is3Finger: false, penalty: 1 };
|
|
388
604
|
}
|
|
389
605
|
this.calculateNerfFactors();
|
|
@@ -406,11 +622,11 @@ class ThreeFingerChecker {
|
|
|
406
622
|
// For sliders and spinners, automatically set hit window length to be as lenient as possible.
|
|
407
623
|
let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
408
624
|
switch (objectData[beforeIndex].result) {
|
|
409
|
-
case exports.
|
|
625
|
+
case exports.HitResult.great:
|
|
410
626
|
beforeIndexHitWindowLength =
|
|
411
627
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
412
628
|
break;
|
|
413
|
-
case exports.
|
|
629
|
+
case exports.HitResult.good:
|
|
414
630
|
beforeIndexHitWindowLength =
|
|
415
631
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
416
632
|
break;
|
|
@@ -424,11 +640,11 @@ class ThreeFingerChecker {
|
|
|
424
640
|
// For sliders and spinners, automatically set hit window length to be as lenient as possible.
|
|
425
641
|
let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
426
642
|
switch (objectData[afterIndex].result) {
|
|
427
|
-
case exports.
|
|
643
|
+
case exports.HitResult.great:
|
|
428
644
|
afterIndexHitWindowLength =
|
|
429
645
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
430
646
|
break;
|
|
431
|
-
case exports.
|
|
647
|
+
case exports.HitResult.good:
|
|
432
648
|
afterIndexHitWindowLength =
|
|
433
649
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
434
650
|
break;
|
|
@@ -458,11 +674,11 @@ class ThreeFingerChecker {
|
|
|
458
674
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
|
|
459
675
|
if (objects[0].object instanceof osuBase.Circle) {
|
|
460
676
|
switch (firstObjectResult) {
|
|
461
|
-
case exports.
|
|
677
|
+
case exports.HitResult.great:
|
|
462
678
|
firstObjectHitWindow =
|
|
463
679
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
464
680
|
break;
|
|
465
|
-
case exports.
|
|
681
|
+
case exports.HitResult.good:
|
|
466
682
|
firstObjectHitWindow =
|
|
467
683
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
468
684
|
break;
|
|
@@ -475,11 +691,11 @@ class ThreeFingerChecker {
|
|
|
475
691
|
let lastObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
|
|
476
692
|
if (objects.at(-1).object instanceof osuBase.Circle) {
|
|
477
693
|
switch (lastObjectResult) {
|
|
478
|
-
case exports.
|
|
694
|
+
case exports.HitResult.great:
|
|
479
695
|
lastObjectHitWindow =
|
|
480
696
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
481
697
|
break;
|
|
482
|
-
case exports.
|
|
698
|
+
case exports.HitResult.good:
|
|
483
699
|
lastObjectHitWindow =
|
|
484
700
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
485
701
|
break;
|
|
@@ -495,27 +711,20 @@ class ThreeFingerChecker {
|
|
|
495
711
|
const lastObjectHitTime = objects.at(-1).object.startTime + lastObjectHitWindow;
|
|
496
712
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
497
713
|
const cursorInstance = this.data.cursorMovement[i];
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
id: [],
|
|
504
|
-
});
|
|
505
|
-
for (let j = 0; j < cursorInstance.occurrences.length; ++j) {
|
|
506
|
-
if (cursorInstance.occurrences[j].id !== exports.movementType.DOWN) {
|
|
714
|
+
const validOccurrences = [];
|
|
715
|
+
for (let j = 0; j < cursorInstance.occurrenceGroups.length; ++j) {
|
|
716
|
+
const group = cursorInstance.occurrenceGroups[j];
|
|
717
|
+
if (group.startTime < firstObjectHitTime ||
|
|
718
|
+
group.endTime > lastObjectHitTime) {
|
|
507
719
|
continue;
|
|
508
720
|
}
|
|
509
|
-
|
|
510
|
-
|
|
721
|
+
if (this.breakPointAccurateTimes.some((v) => group.startTime >= v.startTime &&
|
|
722
|
+
group.endTime <= v.endTime)) {
|
|
511
723
|
continue;
|
|
512
724
|
}
|
|
513
|
-
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
newCursorData.occurrences.push(new CursorOccurrence(time, cursorInstance.occurrences[j].position.x, cursorInstance.occurrences[j].position.y, cursorInstance.occurrences[j].id));
|
|
725
|
+
validOccurrences.push(group.down);
|
|
517
726
|
}
|
|
518
|
-
this.downCursorInstances.push(
|
|
727
|
+
this.downCursorInstances.push(validOccurrences);
|
|
519
728
|
}
|
|
520
729
|
}
|
|
521
730
|
/**
|
|
@@ -559,11 +768,11 @@ class ThreeFingerChecker {
|
|
|
559
768
|
let firstObjectMinHitTime = firstObject.object.startTime;
|
|
560
769
|
if (firstObject.object instanceof osuBase.Circle) {
|
|
561
770
|
switch (objectData[section.firstObjectIndex].result) {
|
|
562
|
-
case exports.
|
|
771
|
+
case exports.HitResult.great:
|
|
563
772
|
firstObjectMinHitTime -=
|
|
564
773
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
565
774
|
break;
|
|
566
|
-
case exports.
|
|
775
|
+
case exports.HitResult.good:
|
|
567
776
|
firstObjectMinHitTime -=
|
|
568
777
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
569
778
|
break;
|
|
@@ -578,11 +787,11 @@ class ThreeFingerChecker {
|
|
|
578
787
|
let lastObjectMaxHitTime = lastObject.object.startTime;
|
|
579
788
|
if (lastObject.object instanceof osuBase.Circle) {
|
|
580
789
|
switch (objectData[section.lastObjectIndex].result) {
|
|
581
|
-
case exports.
|
|
790
|
+
case exports.HitResult.great:
|
|
582
791
|
lastObjectMaxHitTime +=
|
|
583
792
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
584
793
|
break;
|
|
585
|
-
case exports.
|
|
794
|
+
case exports.HitResult.good:
|
|
586
795
|
lastObjectMaxHitTime +=
|
|
587
796
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
588
797
|
break;
|
|
@@ -599,18 +808,18 @@ class ThreeFingerChecker {
|
|
|
599
808
|
const cursorIndexes = [];
|
|
600
809
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
601
810
|
const c = this.data.cursorMovement[i];
|
|
602
|
-
if (c.
|
|
811
|
+
if (c.occurrenceGroups.length === 0) {
|
|
603
812
|
continue;
|
|
604
813
|
}
|
|
605
814
|
// Do not include cursors that don't have an occurence in this section
|
|
606
815
|
// this speeds up checking process.
|
|
607
|
-
if (c.
|
|
608
|
-
v.
|
|
816
|
+
if (c.occurrenceGroups.filter((v) => v.startTime >= firstObjectMinHitTime &&
|
|
817
|
+
v.endTime <= lastObjectMaxHitTime).length === 0) {
|
|
609
818
|
continue;
|
|
610
819
|
}
|
|
611
820
|
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
612
|
-
if (c.
|
|
613
|
-
|
|
821
|
+
if (c.occurrenceGroups.filter((v) => v.moves.length > 0).length ===
|
|
822
|
+
0) {
|
|
614
823
|
continue;
|
|
615
824
|
}
|
|
616
825
|
cursorIndexes.push(i);
|
|
@@ -626,7 +835,7 @@ class ThreeFingerChecker {
|
|
|
626
835
|
*/
|
|
627
836
|
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
628
837
|
let objectIndex = sectionObjects.findIndex((v, i) => !(v.object instanceof osuBase.Spinner) &&
|
|
629
|
-
sectionReplayObjectData[i].result !== exports.
|
|
838
|
+
sectionReplayObjectData[i].result !== exports.HitResult.miss);
|
|
630
839
|
if (objectIndex === -1) {
|
|
631
840
|
return -1;
|
|
632
841
|
}
|
|
@@ -637,7 +846,7 @@ class ThreeFingerChecker {
|
|
|
637
846
|
const o = sectionObjects[objectIndex];
|
|
638
847
|
const s = sectionReplayObjectData[objectIndex];
|
|
639
848
|
++objectIndex;
|
|
640
|
-
if (s.result === exports.
|
|
849
|
+
if (s.result === exports.HitResult.miss) {
|
|
641
850
|
continue;
|
|
642
851
|
}
|
|
643
852
|
// Get the cursor instance that is closest to the object's hit time.
|
|
@@ -647,29 +856,32 @@ class ThreeFingerChecker {
|
|
|
647
856
|
// therefore the game emulates the movement between
|
|
648
857
|
// movementType.MOVE cursors.
|
|
649
858
|
const hitTime = o.object.startTime + s.accuracy;
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
if (hitIndex <= -1) {
|
|
653
|
-
cursorIndexes[j] = -1;
|
|
859
|
+
const cursorGroup = c.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
860
|
+
if (!cursorGroup) {
|
|
654
861
|
continue;
|
|
655
862
|
}
|
|
656
|
-
|
|
863
|
+
const cursors = cursorGroup.allOccurrences;
|
|
864
|
+
const nextHitIndex = cursors.findIndex((v) => v.time >= hitTime);
|
|
865
|
+
const hitIndex = nextHitIndex - 1;
|
|
866
|
+
if (hitIndex <= -1) {
|
|
657
867
|
cursorIndexes[j] = -1;
|
|
658
868
|
continue;
|
|
659
869
|
}
|
|
660
|
-
const cursorPosition = new osuBase.Vector2(
|
|
870
|
+
const cursorPosition = new osuBase.Vector2(cursors[hitIndex].position.x, cursors[hitIndex].position.y);
|
|
661
871
|
let isInObject = false;
|
|
662
|
-
if (
|
|
663
|
-
c.occurrences[hitIndex].id === exports.movementType.MOVE) {
|
|
872
|
+
if (cursors[nextHitIndex].id === exports.MovementType.move) {
|
|
664
873
|
// Try to interpolate movement between two movementType.MOVE cursor every 1ms.
|
|
665
874
|
// This minimizes rounding error.
|
|
666
|
-
for (let mSecPassed =
|
|
667
|
-
const t = (mSecPassed -
|
|
668
|
-
(
|
|
669
|
-
|
|
670
|
-
cursorPosition.x = osuBase.Interpolation.lerp(
|
|
671
|
-
cursorPosition.y = osuBase.Interpolation.lerp(
|
|
672
|
-
if (o.object
|
|
875
|
+
for (let mSecPassed = cursors[hitIndex].time; mSecPassed <= cursors[nextHitIndex].time; ++mSecPassed) {
|
|
876
|
+
const t = (mSecPassed - cursors[nextHitIndex].time) /
|
|
877
|
+
(cursors[hitIndex].time -
|
|
878
|
+
cursors[nextHitIndex].time);
|
|
879
|
+
cursorPosition.x = osuBase.Interpolation.lerp(cursors[hitIndex].position.x, cursors[nextHitIndex].position.x, t);
|
|
880
|
+
cursorPosition.y = osuBase.Interpolation.lerp(cursors[hitIndex].position.y, cursors[nextHitIndex].position.y, t);
|
|
881
|
+
if (o.object
|
|
882
|
+
.getStackedPosition(osuBase.modes.droid)
|
|
883
|
+
.getDistance(cursorPosition) <=
|
|
884
|
+
o.object.getRadius(osuBase.modes.droid)) {
|
|
673
885
|
isInObject = true;
|
|
674
886
|
break;
|
|
675
887
|
}
|
|
@@ -677,8 +889,10 @@ class ThreeFingerChecker {
|
|
|
677
889
|
}
|
|
678
890
|
else {
|
|
679
891
|
isInObject =
|
|
680
|
-
o.object
|
|
681
|
-
|
|
892
|
+
o.object
|
|
893
|
+
.getStackedPosition(osuBase.modes.droid)
|
|
894
|
+
.getDistance(cursorPosition) <=
|
|
895
|
+
o.object.getRadius(osuBase.modes.droid);
|
|
682
896
|
}
|
|
683
897
|
if (!isInObject) {
|
|
684
898
|
cursorIndexes[j] = -1;
|
|
@@ -741,26 +955,26 @@ class ThreeFingerChecker {
|
|
|
741
955
|
* unnecessary taps.
|
|
742
956
|
*/
|
|
743
957
|
preventAccidentalTaps() {
|
|
744
|
-
let filledCursorAmount = this.downCursorInstances.filter((v) => v.
|
|
958
|
+
let filledCursorAmount = this.downCursorInstances.filter((v) => v.length > 0).length;
|
|
745
959
|
if (filledCursorAmount <= 3) {
|
|
746
960
|
return;
|
|
747
961
|
}
|
|
748
962
|
const objects = this.calculator.objects;
|
|
749
|
-
const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.
|
|
963
|
+
const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.length, 0);
|
|
750
964
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
751
965
|
if (filledCursorAmount <= 3) {
|
|
752
966
|
break;
|
|
753
967
|
}
|
|
754
|
-
const
|
|
968
|
+
const cursorInstances = this.downCursorInstances[i];
|
|
755
969
|
// Use an estimation for accidental tap threshold.
|
|
756
|
-
if (
|
|
970
|
+
if (cursorInstances.length <=
|
|
757
971
|
Math.ceil(objects.length / this.accidentalTapThreshold) &&
|
|
758
|
-
|
|
972
|
+
cursorInstances.length / totalCursorAmount <
|
|
759
973
|
this.threeFingerRatioThreshold * 2) {
|
|
760
974
|
--filledCursorAmount;
|
|
761
|
-
|
|
975
|
+
cursorInstances.length = 0;
|
|
762
976
|
}
|
|
763
|
-
this.downCursorInstances[i] =
|
|
977
|
+
this.downCursorInstances[i] = cursorInstances;
|
|
764
978
|
}
|
|
765
979
|
}
|
|
766
980
|
/**
|
|
@@ -778,19 +992,19 @@ class ThreeFingerChecker {
|
|
|
778
992
|
const dragIndex = beatmapSection.dragFingerIndex;
|
|
779
993
|
const startTime = objects[beatmapSection.firstObjectIndex].object.startTime +
|
|
780
994
|
(objectData[beatmapSection.firstObjectIndex].result !==
|
|
781
|
-
exports.
|
|
995
|
+
exports.HitResult.miss
|
|
782
996
|
? objectData[beatmapSection.firstObjectIndex].accuracy
|
|
783
997
|
: -this.hitWindow.hitWindowFor50(isPrecise));
|
|
784
998
|
const endTime = objects[beatmapSection.lastObjectIndex].object.endTime +
|
|
785
999
|
(objectData[beatmapSection.lastObjectIndex].result !==
|
|
786
|
-
exports.
|
|
1000
|
+
exports.HitResult.miss
|
|
787
1001
|
? objectData[beatmapSection.lastObjectIndex].accuracy
|
|
788
1002
|
: this.hitWindow.hitWindowFor50(isPrecise));
|
|
789
1003
|
// Filter cursor instances during section.
|
|
790
1004
|
this.downCursorInstances.forEach((c) => {
|
|
791
|
-
const i = c.
|
|
1005
|
+
const i = c.findIndex((t) => t.time >= startTime);
|
|
792
1006
|
if (i !== -1) {
|
|
793
|
-
c
|
|
1007
|
+
c = c.slice(i);
|
|
794
1008
|
}
|
|
795
1009
|
});
|
|
796
1010
|
const cursorAmounts = [];
|
|
@@ -800,15 +1014,15 @@ class ThreeFingerChecker {
|
|
|
800
1014
|
if (i === dragIndex) {
|
|
801
1015
|
continue;
|
|
802
1016
|
}
|
|
803
|
-
const
|
|
1017
|
+
const cursors = this.downCursorInstances[i];
|
|
804
1018
|
let amount = 0;
|
|
805
|
-
for (let j = 0; j <
|
|
806
|
-
if (
|
|
807
|
-
|
|
1019
|
+
for (let j = 0; j < cursors.length; ++j) {
|
|
1020
|
+
if (cursors[j].time >= startTime &&
|
|
1021
|
+
cursors[j].time <= endTime) {
|
|
808
1022
|
++amount;
|
|
809
1023
|
cursorVectorTimes.push({
|
|
810
|
-
vector: new osuBase.Vector2(
|
|
811
|
-
time:
|
|
1024
|
+
vector: new osuBase.Vector2(cursors[j].position.x, cursors[j].position.y),
|
|
1025
|
+
time: cursors[j].time,
|
|
812
1026
|
});
|
|
813
1027
|
}
|
|
814
1028
|
}
|
|
@@ -976,6 +1190,13 @@ class TwoHandChecker {
|
|
|
976
1190
|
* those that do not worth any strain.
|
|
977
1191
|
*/
|
|
978
1192
|
minCursorIndexCount = 5;
|
|
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 = [];
|
|
979
1200
|
/**
|
|
980
1201
|
* @param calculator The difficulty calculator to analyze.
|
|
981
1202
|
* @param data The data of the replay.
|
|
@@ -983,10 +1204,12 @@ class TwoHandChecker {
|
|
|
983
1204
|
constructor(calculator, data) {
|
|
984
1205
|
this.calculator = calculator;
|
|
985
1206
|
this.data = data;
|
|
1207
|
+
this.allCursorOccurrences = data.cursorMovement.map((v) => v.allOccurrences);
|
|
986
1208
|
const stats = new osuBase.MapStats({
|
|
987
1209
|
od: this.calculator.beatmap.difficulty.od,
|
|
988
1210
|
mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
989
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)
|
|
1211
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym) &&
|
|
1212
|
+
!(m instanceof osuBase.ModPrecise)),
|
|
990
1213
|
}).calculate();
|
|
991
1214
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
992
1215
|
}
|
|
@@ -994,8 +1217,7 @@ class TwoHandChecker {
|
|
|
994
1217
|
* Checks if a beatmap is two-handed.
|
|
995
1218
|
*/
|
|
996
1219
|
check() {
|
|
997
|
-
if (this.data.cursorMovement.filter((v) => v.
|
|
998
|
-
.length <= 1) {
|
|
1220
|
+
if (this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 1) {
|
|
999
1221
|
return { is2Hand: false, cursorIndexes: [] };
|
|
1000
1222
|
}
|
|
1001
1223
|
this.indexHitObjects();
|
|
@@ -1074,16 +1296,15 @@ class TwoHandChecker {
|
|
|
1074
1296
|
const deltaTimes = [];
|
|
1075
1297
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1076
1298
|
const c = this.data.cursorMovement[i];
|
|
1077
|
-
for (let j = 0; j < c.
|
|
1078
|
-
|
|
1299
|
+
for (let j = 0; j < c.occurrenceGroups.length; ++j) {
|
|
1300
|
+
const group = c.occurrenceGroups[j];
|
|
1301
|
+
const previousGroup = c.occurrenceGroups[j - 1];
|
|
1302
|
+
if (group.startTime <
|
|
1079
1303
|
this.calculator.beatmap.hitObjects.objects[0].startTime -
|
|
1080
1304
|
this.hitWindow.hitWindowFor50()) {
|
|
1081
1305
|
continue;
|
|
1082
1306
|
}
|
|
1083
|
-
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
const deltaTime = c.occurrences[j]?.time - c.occurrences[j - 1]?.time || 0;
|
|
1307
|
+
const deltaTime = group.startTime - previousGroup.endTime;
|
|
1087
1308
|
if (deltaTime > 0) {
|
|
1088
1309
|
deltaTimes.push(deltaTime);
|
|
1089
1310
|
}
|
|
@@ -1102,7 +1323,7 @@ class TwoHandChecker {
|
|
|
1102
1323
|
const object = this.calculator.objects[index];
|
|
1103
1324
|
const data = this.data.hitObjectData[index];
|
|
1104
1325
|
if (object.object instanceof osuBase.Spinner ||
|
|
1105
|
-
data.result === exports.
|
|
1326
|
+
data.result === exports.HitResult.miss) {
|
|
1106
1327
|
return new IndexedHitObject(object, -1, -1, -1);
|
|
1107
1328
|
}
|
|
1108
1329
|
const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
|
|
@@ -1110,10 +1331,10 @@ class TwoHandChecker {
|
|
|
1110
1331
|
let hitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
1111
1332
|
if (!(object.object instanceof osuBase.Slider)) {
|
|
1112
1333
|
switch (data.result) {
|
|
1113
|
-
case exports.
|
|
1334
|
+
case exports.HitResult.great:
|
|
1114
1335
|
hitWindowLength = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1115
1336
|
break;
|
|
1116
|
-
case exports.
|
|
1337
|
+
case exports.HitResult.good:
|
|
1117
1338
|
hitWindowLength = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1118
1339
|
break;
|
|
1119
1340
|
}
|
|
@@ -1123,26 +1344,26 @@ class TwoHandChecker {
|
|
|
1123
1344
|
const minimumHitTime = startTime - hitWindowLength - hitWindowOffset;
|
|
1124
1345
|
const maximumHitTime = startTime + hitWindowLength + hitWindowOffset;
|
|
1125
1346
|
const cursorInformations = [];
|
|
1126
|
-
for (let i = 0; i < this.
|
|
1127
|
-
const c = this.
|
|
1128
|
-
if (c.
|
|
1347
|
+
for (let i = 0; i < this.allCursorOccurrences.length; ++i) {
|
|
1348
|
+
const c = this.allCursorOccurrences[i];
|
|
1349
|
+
if (c.length === 0) {
|
|
1129
1350
|
continue;
|
|
1130
1351
|
}
|
|
1131
|
-
let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.
|
|
1132
|
-
let hitTimeAfterIndex = c.
|
|
1352
|
+
let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.findIndex((v) => v.time >= minimumHitTime), 1, c.length - 1) - 1;
|
|
1353
|
+
let hitTimeAfterIndex = c.findIndex(
|
|
1133
1354
|
// There is a special case for sliders where the time leniency in droid is a lot bigger compared to PC.
|
|
1134
1355
|
// To prevent slider end time from ending earlier than hit window leniency, we use the maximum value between both.
|
|
1135
1356
|
(v) => v.time >= Math.max(object.object.endTime, maximumHitTime));
|
|
1136
1357
|
if (hitTimeAfterIndex === -1) {
|
|
1137
1358
|
// Maximum hit time or object end time may be out of bounds for every presses.
|
|
1138
1359
|
// We set the index to the latest cursor occurrence if that happens.
|
|
1139
|
-
hitTimeAfterIndex = c.
|
|
1360
|
+
hitTimeAfterIndex = c.length;
|
|
1140
1361
|
}
|
|
1141
1362
|
--hitTimeAfterIndex;
|
|
1142
1363
|
// Sometimes a `movementType.UP` instance occurs at the same time as a `movementType.MOVE`
|
|
1143
1364
|
// or a cursor is recorded twice in one time, therefore this check is required.
|
|
1144
|
-
while (c
|
|
1145
|
-
c
|
|
1365
|
+
while (c[hitTimeBeforeIndex]?.time ===
|
|
1366
|
+
c[hitTimeBeforeIndex - 1]?.time &&
|
|
1146
1367
|
hitTimeBeforeIndex > 0) {
|
|
1147
1368
|
--hitTimeBeforeIndex;
|
|
1148
1369
|
}
|
|
@@ -1151,26 +1372,29 @@ class TwoHandChecker {
|
|
|
1151
1372
|
let distance = Number.POSITIVE_INFINITY;
|
|
1152
1373
|
let j = hitTimeBeforeIndex;
|
|
1153
1374
|
for (j; j <= hitTimeAfterIndex; ++j) {
|
|
1154
|
-
const occurrence = c
|
|
1155
|
-
const nextOccurrence = c
|
|
1375
|
+
const occurrence = c[j];
|
|
1376
|
+
const nextOccurrence = c[j + 1];
|
|
1156
1377
|
const cursorPosition = occurrence.position;
|
|
1157
1378
|
if (occurrence.time < minimumHitTime &&
|
|
1158
|
-
nextOccurrence?.id !== exports.
|
|
1379
|
+
nextOccurrence?.id !== exports.MovementType.move) {
|
|
1159
1380
|
continue;
|
|
1160
1381
|
}
|
|
1161
1382
|
if (occurrence.time > hitTime + hitWindowOffset) {
|
|
1162
1383
|
// Set distance to minimum just for the last.
|
|
1163
|
-
if (occurrence.id !== exports.
|
|
1164
|
-
distance = Math.min(distance, object.object
|
|
1384
|
+
if (occurrence.id !== exports.MovementType.up) {
|
|
1385
|
+
distance = Math.min(distance, object.object
|
|
1386
|
+
.getStackedPosition(osuBase.modes.droid)
|
|
1387
|
+
.getDistance(cursorPosition));
|
|
1165
1388
|
}
|
|
1166
1389
|
break;
|
|
1167
1390
|
}
|
|
1168
|
-
if (occurrence.id === exports.
|
|
1391
|
+
if (occurrence.id === exports.MovementType.up) {
|
|
1169
1392
|
continue;
|
|
1170
1393
|
}
|
|
1171
|
-
distance =
|
|
1172
|
-
|
|
1173
|
-
|
|
1394
|
+
distance = object.object
|
|
1395
|
+
.getStackedPosition(osuBase.modes.droid)
|
|
1396
|
+
.getDistance(cursorPosition);
|
|
1397
|
+
if (nextOccurrence?.id === exports.MovementType.move &&
|
|
1174
1398
|
occurrence.time !== nextOccurrence.time &&
|
|
1175
1399
|
!occurrence.position.equals(nextOccurrence.position)) {
|
|
1176
1400
|
// If next cursor is a `move` instance and it doesn't go out of time
|
|
@@ -1180,11 +1404,13 @@ class TwoHandChecker {
|
|
|
1180
1404
|
for (let mSecPassed = Math.max(minimumHitTime, occurrence.time); mSecPassed <= Math.min(hitTime, nextOccurrence.time); ++mSecPassed) {
|
|
1181
1405
|
const progress = (mSecPassed - occurrence.time) /
|
|
1182
1406
|
(nextOccurrence.time - occurrence.time);
|
|
1183
|
-
distance = object.object
|
|
1407
|
+
distance = object.object
|
|
1408
|
+
.getStackedPosition(osuBase.modes.droid)
|
|
1409
|
+
.getDistance(cursorPosition.add(displacement.scale(progress)));
|
|
1184
1410
|
}
|
|
1185
1411
|
}
|
|
1186
1412
|
}
|
|
1187
|
-
if (distance > object.object.
|
|
1413
|
+
if (distance > object.object.getRadius(osuBase.modes.droid)) {
|
|
1188
1414
|
continue;
|
|
1189
1415
|
}
|
|
1190
1416
|
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
@@ -1198,21 +1424,20 @@ class TwoHandChecker {
|
|
|
1198
1424
|
// Aside of angles, we need to consider if the player dragged from the previous object to the current object.
|
|
1199
1425
|
let isDragged = false;
|
|
1200
1426
|
// Get the latest down or movement cursor occurrence.
|
|
1201
|
-
while (c
|
|
1202
|
-
j > hitTimeBeforeIndex) {
|
|
1427
|
+
while (c[j]?.id === exports.MovementType.up && j > hitTimeBeforeIndex) {
|
|
1203
1428
|
--j;
|
|
1204
1429
|
}
|
|
1205
1430
|
if (object.object instanceof osuBase.Circle) {
|
|
1206
1431
|
// For circles, we only need to consider the actual press on the circle.
|
|
1207
1432
|
// Therefore, we need to get the latest down cursor occurrence instead.
|
|
1208
|
-
while (c
|
|
1433
|
+
while (c[j]?.id !== exports.MovementType.down &&
|
|
1209
1434
|
j > hitTimeBeforeIndex) {
|
|
1210
1435
|
--j;
|
|
1211
1436
|
}
|
|
1212
1437
|
}
|
|
1213
1438
|
// Theoretically there can only be 1 up occurrence, but this is a
|
|
1214
1439
|
// consideration if the user manually adds cursor occurrences.
|
|
1215
|
-
if (c
|
|
1440
|
+
if (c[j]?.id === exports.MovementType.up) {
|
|
1216
1441
|
++j;
|
|
1217
1442
|
}
|
|
1218
1443
|
// Some move instances move in the exact same place. Not sure why, most likely
|
|
@@ -1220,16 +1445,16 @@ class TwoHandChecker {
|
|
|
1220
1445
|
// convert into +1 or -1.
|
|
1221
1446
|
// let nextSignificantOccurrenceIndex: number = j + 1;
|
|
1222
1447
|
// while (
|
|
1223
|
-
// c
|
|
1224
|
-
// c
|
|
1225
|
-
// c
|
|
1226
|
-
// c
|
|
1448
|
+
// c[j] &&
|
|
1449
|
+
// c[nextSignificantOccurrenceIndex] &&
|
|
1450
|
+
// c[j].position.equals(
|
|
1451
|
+
// c[nextSignificantOccurrenceIndex].position
|
|
1227
1452
|
// )
|
|
1228
1453
|
// ) {
|
|
1229
1454
|
// ++nextSignificantOccurrenceIndex;
|
|
1230
1455
|
// }
|
|
1231
1456
|
// const nextSignificantOccurrence: CursorOccurrence =
|
|
1232
|
-
// c
|
|
1457
|
+
// c[nextSignificantOccurrenceIndex];
|
|
1233
1458
|
// const next: DifficultyHitObject | RebalanceDifficultyHitObject =
|
|
1234
1459
|
// this.map.objects[index + 1];
|
|
1235
1460
|
// Angle detection.
|
|
@@ -1267,7 +1492,7 @@ class TwoHandChecker {
|
|
|
1267
1492
|
);
|
|
1268
1493
|
|
|
1269
1494
|
const currentToNext: Vector2 =
|
|
1270
|
-
next.object.
|
|
1495
|
+
next.object.getStackedPosition(modes.droid).subtract(actualEndPosition);
|
|
1271
1496
|
|
|
1272
1497
|
const dot: number = currentToNext.dot(movementVec);
|
|
1273
1498
|
const det: number =
|
|
@@ -1296,24 +1521,22 @@ class TwoHandChecker {
|
|
|
1296
1521
|
}
|
|
1297
1522
|
}
|
|
1298
1523
|
let occurrenceStartIndex = hitTimeAfterIndex;
|
|
1299
|
-
while (c
|
|
1300
|
-
dragTimeThreshold &&
|
|
1524
|
+
while (c[occurrenceStartIndex]?.time >= dragTimeThreshold &&
|
|
1301
1525
|
occurrenceStartIndex > 0) {
|
|
1302
1526
|
--occurrenceStartIndex;
|
|
1303
1527
|
}
|
|
1304
1528
|
// The above loop will make the start index before or right when
|
|
1305
1529
|
// the previous object was hit or ended, but we want the index after it.
|
|
1306
1530
|
++occurrenceStartIndex;
|
|
1307
|
-
const dragOccurrences = c.
|
|
1531
|
+
const dragOccurrences = c.slice(occurrenceStartIndex, hitTimeAfterIndex);
|
|
1308
1532
|
isDragged =
|
|
1309
1533
|
dragOccurrences.length > 0 &&
|
|
1310
1534
|
// We only care when the cursor approaches the current object. It doesn't
|
|
1311
1535
|
// matter whether the previous object was pressed or dragged.
|
|
1312
1536
|
dragOccurrences
|
|
1313
1537
|
.at(-1)
|
|
1314
|
-
.position.getDistance(object.object.
|
|
1315
|
-
|
|
1316
|
-
dragOccurrences.every((v) => v.id === exports.movementType.MOVE);
|
|
1538
|
+
.position.getDistance(object.object.getStackedPosition(osuBase.modes.droid)) <= object.object.getRadius(osuBase.modes.droid) &&
|
|
1539
|
+
dragOccurrences.every((v) => v.id === exports.MovementType.move);
|
|
1317
1540
|
cursorInformations.push({
|
|
1318
1541
|
// If the angle is fulfilled or the player dragged,
|
|
1319
1542
|
// we set the cursor index to the main cursor index.
|
|
@@ -1344,12 +1567,12 @@ class TwoHandChecker {
|
|
|
1344
1567
|
*/
|
|
1345
1568
|
checkSliderCheesing(indexedHitObject, hitData, hitWindowOffset) {
|
|
1346
1569
|
if (!(indexedHitObject.object.object instanceof osuBase.Slider) ||
|
|
1347
|
-
hitData.result === exports.
|
|
1570
|
+
hitData.result === exports.HitResult.miss) {
|
|
1348
1571
|
return false;
|
|
1349
1572
|
}
|
|
1350
1573
|
let cursorLoopIndex = Math.max(0, indexedHitObject.occurrenceIndex);
|
|
1351
|
-
const c = this.
|
|
1352
|
-
const acceptableRadius = indexedHitObject.object.object.
|
|
1574
|
+
const c = this.allCursorOccurrences[indexedHitObject.actualCursorIndex];
|
|
1575
|
+
const acceptableRadius = indexedHitObject.object.object.getRadius(osuBase.modes.droid) * 2.4;
|
|
1353
1576
|
for (let i = 1; i < indexedHitObject.object.object.nestedHitObjects.length; ++i) {
|
|
1354
1577
|
const tickWasHit = hitData.tickset[i - 1];
|
|
1355
1578
|
if (!tickWasHit) {
|
|
@@ -1358,16 +1581,14 @@ class TwoHandChecker {
|
|
|
1358
1581
|
const object = indexedHitObject.object.object.nestedHitObjects[i];
|
|
1359
1582
|
let j = cursorLoopIndex;
|
|
1360
1583
|
let cursorHitTick = false;
|
|
1361
|
-
for (j; j < c.
|
|
1362
|
-
if (c
|
|
1363
|
-
object.startTime - hitWindowOffset) {
|
|
1584
|
+
for (j; j < c.length; ++j) {
|
|
1585
|
+
if (c[j].time < object.startTime - hitWindowOffset) {
|
|
1364
1586
|
continue;
|
|
1365
1587
|
}
|
|
1366
|
-
if (c
|
|
1367
|
-
object.startTime + hitWindowOffset) {
|
|
1588
|
+
if (c[j].time > object.startTime + hitWindowOffset) {
|
|
1368
1589
|
break;
|
|
1369
1590
|
}
|
|
1370
|
-
if (c
|
|
1591
|
+
if (c[j].position.getDistance(object.getStackedPosition(osuBase.modes.droid)) <= acceptableRadius) {
|
|
1371
1592
|
cursorHitTick = true;
|
|
1372
1593
|
break;
|
|
1373
1594
|
}
|
|
@@ -1602,7 +1823,7 @@ class ReplayAnalyzer {
|
|
|
1602
1823
|
resultObject.isFullCombo = !!rawObject[4][44];
|
|
1603
1824
|
resultObject.playerName = rawObject[5];
|
|
1604
1825
|
resultObject.rawMods = rawObject[6].elements;
|
|
1605
|
-
resultObject.convertedMods = this.
|
|
1826
|
+
resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(rawObject[6].elements));
|
|
1606
1827
|
// Determine rank
|
|
1607
1828
|
const totalHits = resultObject.accuracy.n300 +
|
|
1608
1829
|
resultObject.accuracy.n100 +
|
|
@@ -1688,7 +1909,7 @@ class ReplayAnalyzer {
|
|
|
1688
1909
|
bufferCounter += this.INT_LENGTH;
|
|
1689
1910
|
id[i] = time[i] & 3;
|
|
1690
1911
|
time[i] >>= 2;
|
|
1691
|
-
if (id[i] !== exports.
|
|
1912
|
+
if (id[i] !== exports.MovementType.up) {
|
|
1692
1913
|
if (resultObject.replayVersion >= 5) {
|
|
1693
1914
|
x[i] = replayDataBuffer.readFloatBE(bufferCounter);
|
|
1694
1915
|
bufferCounter += this.FLOAT_LENGTH;
|
|
@@ -1768,21 +1989,21 @@ class ReplayAnalyzer {
|
|
|
1768
1989
|
const hitObjectData = resultObject.hitObjectData[i];
|
|
1769
1990
|
const isNextNewCombo = i + 1 !== objects.length ? objects[i + 1].isNewCombo : true;
|
|
1770
1991
|
switch (hitObjectData.result) {
|
|
1771
|
-
case exports.
|
|
1992
|
+
case exports.HitResult.miss:
|
|
1772
1993
|
++hit0;
|
|
1773
1994
|
grantsGekiOrKatu = false;
|
|
1774
1995
|
break;
|
|
1775
|
-
case exports.
|
|
1996
|
+
case exports.HitResult.meh:
|
|
1776
1997
|
++hit50;
|
|
1777
1998
|
grantsGekiOrKatu = false;
|
|
1778
1999
|
break;
|
|
1779
|
-
case exports.
|
|
2000
|
+
case exports.HitResult.good:
|
|
1780
2001
|
++hit100;
|
|
1781
2002
|
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
1782
2003
|
++hit100k;
|
|
1783
2004
|
}
|
|
1784
2005
|
break;
|
|
1785
|
-
case exports.
|
|
2006
|
+
case exports.HitResult.great:
|
|
1786
2007
|
++hit300;
|
|
1787
2008
|
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
1788
2009
|
++hit300k;
|
|
@@ -1866,7 +2087,7 @@ class ReplayAnalyzer {
|
|
|
1866
2087
|
for (let i = 0; i < hitObjectData.length; ++i) {
|
|
1867
2088
|
const v = hitObjectData[i];
|
|
1868
2089
|
const o = objects[i];
|
|
1869
|
-
if (o instanceof osuBase.Spinner || v.result === exports.
|
|
2090
|
+
if (o instanceof osuBase.Spinner || v.result === exports.HitResult.miss) {
|
|
1870
2091
|
continue;
|
|
1871
2092
|
}
|
|
1872
2093
|
const accuracy = v.accuracy;
|
|
@@ -1882,7 +2103,7 @@ class ReplayAnalyzer {
|
|
|
1882
2103
|
return {
|
|
1883
2104
|
positiveAvg: positiveTotal / positiveCount || 0,
|
|
1884
2105
|
negativeAvg: negativeTotal / negativeCount || 0,
|
|
1885
|
-
unstableRate: osuBase.MathUtils.calculateStandardDeviation(hitObjectData.map((v, i) => v.result !== exports.
|
|
2106
|
+
unstableRate: osuBase.MathUtils.calculateStandardDeviation(hitObjectData.map((v, i) => v.result !== exports.HitResult.miss &&
|
|
1886
2107
|
!(objects[i] instanceof osuBase.Spinner)
|
|
1887
2108
|
? v.accuracy
|
|
1888
2109
|
: 0)) * 10,
|
|
@@ -1924,12 +2145,6 @@ class ReplayAnalyzer {
|
|
|
1924
2145
|
}
|
|
1925
2146
|
return modString;
|
|
1926
2147
|
}
|
|
1927
|
-
/**
|
|
1928
|
-
* Converts replay mods to regular mod string.
|
|
1929
|
-
*/
|
|
1930
|
-
convertMods(replayMods) {
|
|
1931
|
-
return osuBase.ModUtil.droidStringToMods(this.convertDroidMods(replayMods));
|
|
1932
|
-
}
|
|
1933
2148
|
/**
|
|
1934
2149
|
* Checks if a play is using 3 fingers.
|
|
1935
2150
|
*
|
|
@@ -1997,6 +2212,7 @@ class ReplayObjectData {
|
|
|
1997
2212
|
|
|
1998
2213
|
exports.CursorData = CursorData;
|
|
1999
2214
|
exports.CursorOccurrence = CursorOccurrence;
|
|
2215
|
+
exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
|
|
2000
2216
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
2001
2217
|
exports.ReplayData = ReplayData;
|
|
2002
2218
|
exports.ReplayObjectData = ReplayObjectData;
|