private void CalculateSpecificStrain(DifficultyHitObjectOsu PreviousHitObject, BeatmapDifficultyCalculatorOsu.DifficultyType Type, double TimeRate) { double Addition = 0; double TimeElapsed = (double)(BaseHitObject.StartTime - PreviousHitObject.BaseHitObject.StartTime) / TimeRate; double Decay = Math.Pow(DECAY_BASE[(int)Type], TimeElapsed / 1000); if (BaseHitObject.IsType(HitObjectType.Spinner)) { // Do nothing for spinners } else if (BaseHitObject.IsType(HitObjectType.Slider)) { switch (Type) { case BeatmapDifficultyCalculatorOsu.DifficultyType.Speed: // For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast. // The spacing weight exists to differentiate between being able to easily alternate or having to single. Addition = SpacingWeight(PreviousHitObject.LazySliderLengthFirst + PreviousHitObject.LazySliderLengthSubsequent * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + DistanceTo(PreviousHitObject), Type) * SPACING_WEIGHT_SCALING[(int)Type]; break; case BeatmapDifficultyCalculatorOsu.DifficultyType.Aim: // For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference // to multiple jumps. Addition = ( SpacingWeight(PreviousHitObject.LazySliderLengthFirst, Type) + SpacingWeight(PreviousHitObject.LazySliderLengthSubsequent, Type) * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + SpacingWeight(DistanceTo(PreviousHitObject), Type) ) * SPACING_WEIGHT_SCALING[(int)Type]; break; } } else if (BaseHitObject.IsType(HitObjectType.Normal)) { Addition = SpacingWeight(DistanceTo(PreviousHitObject), Type) * SPACING_WEIGHT_SCALING[(int)Type]; } // Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero. // You will never find maps that require this amongst ranked maps. Addition /= Math.Max(TimeElapsed, 50); Strains[(int)Type] = PreviousHitObject.Strains[(int)Type] * Decay + Addition; }
internal void CalculateStrains(DifficultyHitObjectTaiko PreviousHitObject, double TimeRate) { // Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make. // See Taiko feedback thread. TimeElapsed = (double)(BaseHitObject.StartTime - PreviousHitObject.BaseHitObject.StartTime) / TimeRate; double Decay = Math.Pow(DECAY_BASE, TimeElapsed / 1000); double Addition = 1; // Only if we are no slider or spinner we get an extra addition if (PreviousHitObject.BaseHitObject.IsType(HitObjectType.Normal) && BaseHitObject.IsType(HitObjectType.Normal) && BaseHitObject.StartTime - PreviousHitObject.BaseHitObject.StartTime < 1000) // And we only want to check out hitobjects which aren't so far in the past { Addition += ColorChangeAddition(PreviousHitObject); Addition += RhythmChangeAddition(PreviousHitObject); } double AdditionFactor = 1.0; // Scale AdditionFactor linearly from 0.4 to 1 for TimeElapsed from 0 to 50 if (TimeElapsed < 50.0) { AdditionFactor = 0.4 + 0.6 * TimeElapsed / 50.0; } Strain = PreviousHitObject.Strain * Decay + Addition * AdditionFactor; }
protected override void UpdateHitObject(HitObject currHitObject) { SliderOsu slider = currHitObject as SliderOsu; if (slider != null) { if (AudioEngine.Time >= slider.StartTime && AudioEngine.Time <= slider.EndTime) { slider.InitSlide(); sliderActive = true; } } if (AudioEngine.AudioState == AudioStates.Playing && AudioEngine.FrameAverage < 300) { if ((currHitObject.StartTime <= AudioEngine.Time && currHitObject.StartTime >= AudioEngine.TimeLastFrame && !currHitObject.IsType(HitObjectType.Spinner)) || (currHitObject.EndTime <= AudioEngine.Time && currHitObject.EndTime >= AudioEngine.TimeLastFrame)) { if (!currHitObject.Sounded) { currHitObject.PlaySound(); currHitObject.Sounded = true; } } else if (currHitObject.Sounded && Math.Abs(currHitObject.StartTime - AudioEngine.Time) > AudioEngine.FrameAverage * 4) { currHitObject.Sounded = false; } } base.UpdateHitObject(currHitObject); }
/// <summary> /// Calculates the hp drop rate via some insane looping through the map. /// Oh god. /// </summary> /// <returns></returns> internal double CalculateHpDropRate() { double testDrop = 0.05; double lowestHpEver = hitObjectManager.MapDifficultyRange(BeatmapManager.Current.DifficultyHpDrainRate, 195, 160, 60); double lowestHpComboEnd = hitObjectManager.MapDifficultyRange(BeatmapManager.Current.DifficultyHpDrainRate, 198, 170, 80); double lowestHpEnd = hitObjectManager.MapDifficultyRange(BeatmapManager.Current.DifficultyHpDrainRate, 198, 180, 80); double HpRecoveryAvailable = hitObjectManager.MapDifficultyRange(BeatmapManager.Current.DifficultyHpDrainRate, 8, 4, 0); do { TotalHitsPossible = 0; HpBar.Reset(); ComboCounter.HitCombo = 0; Player.currentScore.TotalScore = 0; double lowestHp = HpBar.CurrentHp; int lastTime = hitObjectManager.hitObjects[0].StartTime - hitObjectManager.PreEmpt; bool fail = false; int comboTooLowCount = 0; int breakCount = player.eventManager.eventBreaks.Count; int breakNumber = 0; for (int i = 0; i < hitObjectManager.hitObjectsCount; i++) { HitObject h = hitObjectManager.hitObjects[i]; //Find active break (between current and lastTime) int localLastTime = lastTime; int breakTime = 0; if (breakCount > 0 && breakNumber < breakCount) { Event e = player.eventManager.eventBreaks[breakNumber]; if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) { breakTime = e.Length; breakNumber++; } } HpBar.ReduceCurrentHp(testDrop * (h.StartTime - lastTime - breakTime)); lastTime = h.EndTime; if (HpBar.CurrentHp < lowestHp) { lowestHp = HpBar.CurrentHp; } if (HpBar.CurrentHp <= lowestHpEver) { //Debug.Print("Overall score drops below " + lowestHpEver + " at " + lastTime + " (" + testDrop + ", lowest " + lowestHp + ")"); fail = true; testDrop *= 0.96; break; } if (h is HitCircleFruitsTick) { IncreaseScoreHit(IncreaseScoreType.FruitTick, h); } else if (h is HitCircleFruitsTickTiny) { IncreaseScoreHit(IncreaseScoreType.FruitTickTiny, h); } else if (h is HitCircleFruitsSpin) { HpBar.IncreaseCurrentHp(HpMultiplierNormal / 2); } else { if (i == hitObjectManager.hitObjectsCount - 1 || hitObjectManager.hitObjects[i + 1].NewCombo) { IncreaseScoreHit(IncreaseScoreType.Hit300g, h); if (HpBar.CurrentHp < lowestHpComboEnd) { if (++comboTooLowCount > 2) { HpMultiplierComboEnd *= 1.07; HpMultiplierNormal *= 1.03; fail = true; break; } } } else { IncreaseScoreHit(IncreaseScoreType.Hit300, h); } } if (!(h is HitCircleFruitsTickTiny) && !(h is HitCircleFruitsSpin)) { TotalHitsPossible++; } h.MaxHp = HpBar.CurrentHp; } if (!fail && HpBar.CurrentHp < lowestHpEnd) { //Debug.Print("Song ends on " + currentHp + " - being more lenient"); fail = true; testDrop *= 0.94; HpMultiplierComboEnd *= 1.01; HpMultiplierNormal *= 1.01; } double recovery = (HpBar.CurrentHpUncapped - HP_BAR_MAXIMUM) / hitObjectManager.hitObjectsCount; if (!fail && recovery < HpRecoveryAvailable) { //Debug.Print("Song has average " + recovery + " recovery - being more lenient"); fail = true; testDrop *= 0.96; HpMultiplierComboEnd *= 1.02; HpMultiplierNormal *= 1.01; } if (fail) { continue; } if (GameBase.TestMode) { PlayerTest pt = player as PlayerTest; if (pt != null) { pt.testMaxScore = Player.currentScore.TotalScore; pt.testMaxCombo = ComboCounter.HitCombo; } } Debug.Print("Loaded Beatmap " + BeatmapManager.Current.Filename); Debug.Print(string.Empty); Debug.Print(" stars: " + BeatmapManager.Current.DifficultyEyupStars); Debug.Print(" stars: " + BeatmapManager.Current.DifficultyEchoStars); Debug.Print(" slider rate: " + BeatmapManager.Current.DifficultySliderMultiplier); Debug.Print(" playable length: " + BeatmapManager.Current.DrainLength); Debug.Print(" hitobjects: " + BeatmapManager.Current.ObjectCount); Debug.Print(" hitcircles: " + hitObjectManager.hitObjects.FindAll(h => h.IsType(HitObjectType.Normal)).Count); Debug.Print(" sliders: " + hitObjectManager.hitObjects.FindAll(h => h.IsType(HitObjectType.Slider)).Count); Debug.Print(" spinners: " + hitObjectManager.hitObjects.FindAll(h => h.IsType(HitObjectType.Spinner)).Count); Debug.Print(" drain rate: {0:00.00}%/s", (testDrop * 60) / 2); Debug.Print(" lowest hp: {0:00.00}%", lowestHp / 2); Debug.Print("normal multiplier: {0:00.00}x", HpMultiplierNormal); Debug.Print("combo multiplier: {0:00.00}x", HpMultiplierComboEnd); Debug.Print(" excess hp recov: {0:00.00}%/hitobject", (float)(HpBar.CurrentHpUncapped - HP_BAR_MAXIMUM) / 2 / hitObjectManager.hitObjects.Count); Debug.Print(" max final hp: {0:00.00}%", HpBar.CurrentHp / 2); Debug.Print(" max combo: " + TotalHitsPossible); Debug.Print(string.Empty); return(testDrop); } while (true); }
/// <summary> /// Handle splitting up and labelling the hitObjects into different player's scopes. /// </summary> internal override void InitializeHitObjectPostProcessing() { bMatch match = PlayerVs.Match; usedPlayerSlots = new List <int>(); int playerCount = 0; for (int i = 0; i < bMatch.MAX_PLAYERS; i++) { if ((match.slotStatus[i] & SlotStatus.Playing) > 0 && match.slotTeam[i] == match.slotTeam[player.localPlayerMatchId]) { usedPlayerSlots.Add(i); playerCount++; } } HitObjectManager hitObjectManager = player.hitObjectManager; int currentPlayer = -1; localUserActiveTime = new List <EventBreak>(); EventBreak currentBreak = null; bool firstCombo = true; bool customTagColor; if (customTagColor = MatchSetup.TagComboColour != Color.TransparentWhite) { GameBase.Scheduler.Add(delegate { SkinManager.SliderRenderer.Tag = MatchSetup.TagComboColour; }); } for (int i = 0; i < hitObjectManager.hitObjectsCount; i++) { HitObject h = hitObjectManager.hitObjects[i]; //HitObject hLast = i > 0 ? hitObjectManager.hitObjects[i-1] : hitObjectManager.hitObjects[i]; bool firstInCombo = h.NewCombo || firstCombo; bool spinner = h.IsType(HitObjectType.Spinner) || h is HitCircleFruitsSpin; if (firstInCombo) { if (!spinner) { currentPlayer = (currentPlayer + 1) % playerCount; firstCombo = false; } if (spinner || usedPlayerSlots[currentPlayer] == player.localPlayerMatchId) { //The local player starts playing at this point. int st = (i > 0 ? Math.Max(h.StartTime - hitObjectManager.HitWindow50, hitObjectManager.hitObjects[i - 1].EndTime + 1) : h.StartTime - hitObjectManager.HitWindow50); int ed = h.EndTime; currentBreak = new EventBreak(st, ed); localUserActiveTime.Add(currentBreak); } else { //Another play has taken over. currentBreak = null; } } if (spinner || usedPlayerSlots[currentPlayer] == player.localPlayerMatchId) { if (currentBreak != null) { //The local player finishes playing at this point (or further). currentBreak.SetEndTime(h.EndTime + hitObjectManager.HitWindow50); } if (customTagColor) { h.SetColour(MatchSetup.TagComboColour); } } else { h.IsScorable = false; h.SetColour(Color.Gray); } h.TagNumeric = spinner ? -2 : usedPlayerSlots[currentPlayer]; } InitializeWarningArrows(); }
internal DifficultyHitObjectOsu(HitObject BaseHitObject, float CircleRadius) { this.BaseHitObject = BaseHitObject; if (BaseHitObject.IsType(HitObjectType.Slider)) { SliderOsu Slider = (SliderOsu)BaseHitObject; MaxCombo = 1 + Slider.sliderScoreTimingPoints.Count; } else { MaxCombo = 1; } // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. float ScalingFactor = (52.0f / CircleRadius); if (CircleRadius < 30) { float smallCircleBonus = Math.Min(30.0f - CircleRadius, 5.0f) / 50.0f; ScalingFactor *= 1.0f + smallCircleBonus; } NormalizedStartPosition = BaseHitObject.Position * ScalingFactor; // Calculate approximation of lazy movement on the slider if (BaseHitObject.IsType(HitObjectType.Slider)) { float SliderFollowCircleRadius = CircleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests. int SegmentLength = Math.Min(BaseHitObject.Length / BaseHitObject.SegmentCount, 60000); // We don't want infinite loops if someone decides to make a too long slider. (MillhioreF, I am talking about you! https://osu.ppy.sh/b/326585) int SegmentEndTime = BaseHitObject.StartTime + SegmentLength; // For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later Vector2 CursorPos = BaseHitObject.Position; // //Debug.Print("" + (BaseHitObject.StartTime + LAZY_SLIDER_STEP_LENGTH) + " " + SegmentEndTime + " " + BaseHitObject.EndTime + " " + BaseHitObject.SegmentCount); // Actual computation of the first lazy curve for (int Time = BaseHitObject.StartTime + LAZY_SLIDER_STEP_LENGTH; Time < SegmentEndTime; Time += LAZY_SLIDER_STEP_LENGTH) { Vector2 Difference = BaseHitObject.PositionAtTime(Time) - CursorPos; float Distance = Difference.Length(); // Did we move away too far? if (Distance > SliderFollowCircleRadius) { // Yep, we need to move the cursor Difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference Distance -= SliderFollowCircleRadius; CursorPos += Difference * Distance; // We move the cursor just as far as needed to stay in the follow circle LazySliderLengthFirst += Distance; } } LazySliderLengthFirst *= ScalingFactor; // If we have an odd amount of repetitions the current position will be the end of the slider. Note that this will -always- be triggered if // BaseHitObject.SegmentCount <= 1, because BaseHitObject.SegmentCount can not be smaller than 1. Therefore NormalizedEndPosition will always be initialized if (BaseHitObject.SegmentCount % 2 == 1) { NormalizedEndPosition = CursorPos * ScalingFactor; } // If we have more than one segment, then we also need to compute the length ob subsequent lazy curves. They are different from the first one, since the first // one starts right at the beginning of the slider. if (BaseHitObject.SegmentCount > 1) { // Use the next segment SegmentEndTime += SegmentLength; for (int Time = SegmentEndTime - SegmentLength + LAZY_SLIDER_STEP_LENGTH; Time < SegmentEndTime; Time += LAZY_SLIDER_STEP_LENGTH) { Vector2 Difference = BaseHitObject.PositionAtTime(Time) - CursorPos; float Distance = Difference.Length(); // Did we move away too far? if (Distance > SliderFollowCircleRadius) { // Yep, we need to move the cursor Difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference Distance -= SliderFollowCircleRadius; CursorPos += Difference * Distance; // We move the cursor just as far as needed to stay in the follow circle LazySliderLengthSubsequent += Distance; } } LazySliderLengthSubsequent *= ScalingFactor; // If we have an even amount of repetitions the current position will be the end of the slider if (BaseHitObject.SegmentCount % 2 == 0) { NormalizedEndPosition = CursorPos * ScalingFactor; } } } // We have a normal HitCircle or a spinner else { NormalizedEndPosition = NormalizedStartPosition; } }