| 471 | } |
| 472 | |
| 473 | function stackHitObjects(track) { |
| 474 | // stack coinciding objects to make them easier to see. |
| 475 | // stacked objects form chains (probably not with consecutive index) |
| 476 | const AR = track.difficulty.ApproachRate; |
| 477 | const approachTime = AR < 5 ? 1800 - 120 * AR : 1950 - 150 * AR; |
| 478 | const stackDistance = 3; |
| 479 | const stackThreshold = approachTime * track.general.StackLeniency; |
| 480 | |
| 481 | // time interval between hitobject A and hitobject B |
| 482 | // (it's guaranteed that A and B are not spinners) |
| 483 | function getintv(A, B) { |
| 484 | let endTime = A.time; |
| 485 | if (A.type == "slider") { |
| 486 | // add slider duration |
| 487 | endTime += |
| 488 | (A.repeat * |
| 489 | A.timing.millisecondsPerBeat * |
| 490 | (A.pixelLength / track.difficulty.SliderMultiplier)) / |
| 491 | 100; |
| 492 | } |
| 493 | return B.time - endTime; |
| 494 | } |
| 495 | // distance (in osu! pixels) between hitobject A and hitobject B |
| 496 | // (it's guaranteed that A and B are not spinners) |
| 497 | function getdist(A, B) { |
| 498 | let x = A.x; |
| 499 | let y = A.y; |
| 500 | if (A.type == "slider" && A.repeat % 2 == 1) { |
| 501 | x = A.curve.curve[A.curve.curve.length - 1].x; |
| 502 | y = A.curve.curve[A.curve.curve.length - 1].y; |
| 503 | } |
| 504 | return Math.hypot(x - B.x, y - B.y); |
| 505 | } |
| 506 | |
| 507 | let chains = new Array(); // array of chains represented by array of index |
| 508 | let stacked = new Array(track.hitObjects.length); // whether a hitobject has been added to chains |
| 509 | stacked.fill(false); |
| 510 | for (let i = 0; i < track.hitObjects.length; ++i) { |
| 511 | if (stacked[i]) continue; |
| 512 | let hitI = track.hitObjects[i]; |
| 513 | if (hitI.type == "spinner") continue; |
| 514 | // start a new chain |
| 515 | stacked[i] = true; |
| 516 | let newchain = [hitI]; |
| 517 | // finding chain starting from hitI |
| 518 | for (let j = i + 1; j < track.hitObjects.length; ++j) { |
| 519 | let hitJ = track.hitObjects[j]; |
| 520 | if (hitJ.type == "spinner") break; |
| 521 | if (getintv(newchain[newchain.length - 1], hitJ) > stackThreshold) |
| 522 | break; |
| 523 | // append hitJ to the chain if it's close in space & time |
| 524 | if (getdist(newchain[newchain.length - 1], hitJ) <= stackDistance) { |
| 525 | // first check if hitJ is already stacked |
| 526 | if (stacked[j]) { |
| 527 | // intersecting with a previous chain. |
| 528 | // this shouldn't happen in a usual beatmap. |
| 529 | console.warn( |
| 530 | "[preproc]", |