修复成绩分析中的acc计算问题;增加判定分析辅助工具

This commit is contained in:
LA
2026-01-12 16:55:24 +08:00
parent 642b08e5a6
commit 092d3525e6
14 changed files with 1271 additions and 825 deletions

View File

@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[Test]
public void TestEmptyScore()
{
EzHitEventHeatmapGraph graph = null;
EzManiaScoreGraph graph = null;
AddStep("Create graph with empty score", () =>
{
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Mods = Array.Empty<Mod>()
};
Child = graph = new EzHitEventHeatmapGraph(emptyScore, testBeatmap)
Child = graph = new EzManiaScoreGraph(emptyScore, testBeatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -76,13 +76,13 @@ namespace osu.Game.Rulesets.Mania.Tests
[Test]
public void TestPerfectScore()
{
EzHitEventHeatmapGraph graph = null;
EzManiaScoreGraph graph = null;
AddStep("Create graph with perfect score", () =>
{
var perfectScore = createPerfectScore(testBeatmap);
Child = graph = new EzHitEventHeatmapGraph(perfectScore, testBeatmap)
Child = graph = new EzManiaScoreGraph(perfectScore, testBeatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -98,11 +98,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[Test]
public void TestMixedResults()
{
EzHitEventHeatmapGraph graph = null;
EzManiaScoreGraph graph = null;
AddStep("Create graph with test score", () =>
{
Child = graph = new EzHitEventHeatmapGraph(testScore, testBeatmap)
Child = graph = new EzManiaScoreGraph(testScore, testBeatmap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Ruleset = beatmap.BeatmapInfo.Ruleset,
HitEvents = hitEvents,
Accuracy = accuracy,
TotalScore = 950000,
TotalScore = 424000,
MaxCombo = hitEvents.Count,
Mods = Array.Empty<Mod>()
};

View File

@@ -1,690 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.LAsEzMania.Analysis
{
/// <summary>
/// 结算成绩分析。显示命中时间偏移的热力图。带有血量折线和命中结果边界线。
/// </summary>
public partial class EzHitEventHeatmapGraph : CompositeDrawable
{
private readonly IReadOnlyList<HitEvent> hitEvents;
// 注意:这是从外部传入的,会跟随 HitMode 切换而变化
private readonly ManiaHitWindows hitWindows = new ManiaHitWindows();
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private readonly double drainRate;
private readonly double overallDifficulty;
private const int current_offset = 0;
private const int time_bins = 50; // 时间分段数
private const float circle_size = 5f; // 圆形大小
private float leftMarginConst { get; set; } = 158; // 左侧预留空间
private float rightMarginConst { get; set; } = 7; // 右侧预留空间
private double binSize;
private double maxTime;
private double minTime;
private double timeRange;
[Resolved]
private OsuColour colours { get; set; } = null!;
// [Resolved]
// private OsuConfigManager config { get; set; } = null!;
public EzHitEventHeatmapGraph(ScoreInfo score, IBeatmap playableBeatmap)
{
this.score = score;
hitEvents = score.HitEvents;
// this.hitEvents = hitEvents.Where(e => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsBasic()).ToList();
// this.hitWindows = hitWindows;
this.playableBeatmap = playableBeatmap;
drainRate = playableBeatmap.Difficulty.DrainRate;
overallDifficulty = playableBeatmap.Difficulty.OverallDifficulty;
}
[BackgroundDependencyLoader]
private void load()
{
if (hitEvents.Count == 0)
return;
binSize = Math.Ceiling(hitEvents.Max(e => e.HitObject.StartTime) / time_bins);
binSize = Math.Max(1, binSize);
maxTime = hitEvents.Count > 0 ? hitEvents.Max(e => e.HitObject.StartTime) : 1;
minTime = hitEvents.Count > 0 ? hitEvents.Min(e => e.HitObject.StartTime) : 0;
timeRange = maxTime - minTime;
Scheduler.AddOnce(updateDisplay);
// TODO: 音频偏移功能待实现, 目前下面的代码无法实现,会报错
// offsetBindable = config.GetBindable<int>(OsuSetting.AudioOffset);
// currentOffset = offsetBindable.Value;
//
// offsetBindable.BindValueChanged(updateOffset);
}
private (double accuracy, long score, Dictionary<HitResult, int> counts) calculateV1Accuracy()
{
var maniaHitWindows = new ManiaHitWindows
{
CustomHitWindows = false,
ClassicModActive = true,
IsConvert = false,
ScoreV2Active = false,
SpeedMultiplier = hitWindows.SpeedMultiplier,
DifficultyMultiplier = hitWindows.DifficultyMultiplier
};
maniaHitWindows.ResetRange();
maniaHitWindows.SetDifficulty(overallDifficulty);
double PerfectRange = maniaHitWindows.WindowFor(HitResult.Perfect);
double GreatRange = maniaHitWindows.WindowFor(HitResult.Great);
double GoodRange = maniaHitWindows.WindowFor(HitResult.Good);
double OkRange = maniaHitWindows.WindowFor(HitResult.Ok);
double MehRange = maniaHitWindows.WindowFor(HitResult.Meh);
double MissRange = maniaHitWindows.WindowFor(HitResult.Miss);
Logger.Log($"[EzHitEventHeatmapGraph] V1 HitWindows: P{PerfectRange} G{GreatRange} Go{GoodRange} O{OkRange} M{MehRange} Mi{MissRange}");
double[] HeadOffsets = new double[18];
double MaxPoints = 0;
double TotalPoints = 0;
// double TotalMultiplier = hitWindows.SpeedMultiplier / hitWindows.DifficultyMultiplier;
//
// double invertedOd = 10 - overallDifficulty;
//
// double PerfectRange = Math.Floor(16 * TotalMultiplier) + 0;
// double GreatRange = Math.Floor((34 + 3 * invertedOd)) * TotalMultiplier + 0;
// double GoodRange = Math.Floor((67 + 3 * invertedOd)) * TotalMultiplier + 0;
// double OkRange = Math.Floor((97 + 3 * invertedOd)) * TotalMultiplier + 0;
// double MehRange = Math.Floor((121 + 3 * invertedOd)) * TotalMultiplier + 0;
// double MissRange = Math.Floor((158 + 3 * invertedOd)) * TotalMultiplier + 0;
HitResult getResultByOffset(double offset) =>
offset < PerfectRange ? HitResult.Perfect :
offset < GreatRange ? HitResult.Great :
offset < GoodRange ? HitResult.Good :
offset < OkRange ? HitResult.Ok :
offset < MehRange ? HitResult.Meh :
offset < MissRange ? HitResult.Miss :
HitResult.None;
double getLNScore(double head, double tail)
{
double combined = head + tail;
(double range, double headFactor, double combinedFactor, double score)[] rules = new[]
{
(PerfectRange, 1.2, 2.4, 300.0),
(GreatRange, 1.1, 2.2, 300),
(GoodRange, 1.0, 2.0, 200),
(OkRange, 1.0, 2.0, 100),
(MehRange, 1.0, 2.0, 50),
};
foreach (var (range, headFactor, combinedFactor, lnScore) in rules)
{
if (head < range * headFactor && combined < range * combinedFactor)
{
return lnScore;
}
}
return 0;
}
Dictionary<HitResult, int> v1Counts = new Dictionary<HitResult, int>();
foreach (var hit in score.HitEvents.Where(e => e.Result.IsBasic()))
{
double offset = Math.Abs(hit.TimeOffset);
var result = getResultByOffset(offset);
HitResult hitResult = maniaHitWindows.ResultFor(offset);
v1Counts[result] = v1Counts.GetValueOrDefault(result, 0) + 1;
var hitObject = (ManiaHitObject)hit.HitObject;
if (hitObject is HeadNote)
{
HeadOffsets[hitObject.Column] = offset;
}
else if (hitObject is TailNote)
{
MaxPoints += 300;
TotalPoints += getLNScore(HeadOffsets[hitObject.Column], offset);
HeadOffsets[hitObject.Column] = 0;
}
else if (hitObject is Note)
{
MaxPoints += 300;
TotalPoints += result switch
{
HitResult.Perfect => 300,
HitResult.Great => 300,
HitResult.Good => 200,
HitResult.Ok => 100,
HitResult.Meh => 50,
_ => 0
};
}
}
double accuracy = TotalPoints / MaxPoints;
return (accuracy, (long)TotalPoints, v1Counts);
}
private (double accuracy, long score, Dictionary<HitResult, int> counts) calculateV2Accuracy()
{
hitWindows.ClassicModActive = false;
hitWindows.IsConvert = false;
hitWindows.ScoreV2Active = true;
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
// 创建一个简单的 beatmap
var beatmap = new Beatmap
{
BeatmapInfo = score.BeatmapInfo ?? new BeatmapInfo(),
ControlPointInfo = new ControlPointInfo()
};
foreach (var hitEvent in hitEvents)
beatmap.HitObjects.Add(hitEvent.HitObject);
scoreProcessor.ApplyBeatmap(beatmap);
// 应用所有结果
foreach (var hitEvent in hitEvents)
{
var judgement = hitEvent.HitObject.CreateJudgement();
scoreProcessor.ApplyResult(new JudgementResult(hitEvent.HitObject, judgement) { Type = hitEvent.Result });
}
Dictionary<HitResult, int> v2Counts = hitEvents.GroupBy(e => e.Result).ToDictionary(g => g.Key, g => g.Count());
double accuracy = scoreProcessor.Accuracy.Value;
long totalScore = scoreProcessor.TotalScore.Value;
return (accuracy, totalScore, v2Counts);
}
private void updateDisplay()
{
ClearInternal();
double scAcc = score.Accuracy * 100;
var (v1Acc, v1Score, v1Counts) = calculateV1Accuracy();
var (v2Acc, v2Score, v2Counts) = calculateV2Accuracy();
long scScore = score.TotalScore;
// Add overlay text box in top-left corner
AddInternal(new GridContainer
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Position = Vector2.Zero,
// Padding = new MarginPadding { Top = 5, Bottom = 5 },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc org",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {scAcc:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc v2",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Acc * 100:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc v1",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v1Acc * 100:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr org",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {scScore / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr v2",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Score / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr v1",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v1Score / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Pauses",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {score.Pauses.Count}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "PERFECT",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Perfect, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Perfect, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "GREAT",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Great, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Great, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "GOOD",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Good, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Good, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "OK",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Ok, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Ok, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "MEH",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Meh, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Meh, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "MISS",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {v2Counts.GetValueOrDefault(HitResult.Miss, 0)}\\{v1Counts.GetValueOrDefault(HitResult.Miss, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
}
});
// 遍历所有有效的 HitResult绘制边界线
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).Cast<HitResult>().Where(r => r <= HitResult.Perfect && r >= HitResult.Meh))
{
double boundary = hitWindows.WindowFor(result);
drawBoundaryLine(boundary, result);
drawBoundaryLine(-boundary, result);
}
// 绘制每个 HitEvent 的圆点
var sortedHitEvents = hitEvents.OrderBy(e => e.HitObject.StartTime).ToList();
foreach (var e in sortedHitEvents)
{
double time = e.HitObject.StartTime;
float xPosition = timeRange > 0 ? (float)((time - minTime) / timeRange) : 0; // 计算 x 轴位置
float yPosition = (float)(e.TimeOffset + current_offset);
AddInternal(new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = (xPosition * (DrawWidth - leftMarginConst - rightMarginConst)) - (DrawWidth / 2) + leftMarginConst,
Y = yPosition,
Alpha = 0.8f,
Colour = colours.ForHitResult(e.Result),
});
}
// 计算并绘制血量折线
drawHealthLine();
}
private void drawHealthLine()
{
var sortedEvents = hitEvents.OrderBy(e => e.HitObject.StartTime).ToList();
double currentHealth = 0.0; // 初始血量
List<Vector2> healthPoints = new List<Vector2>();
foreach (var e in sortedEvents)
{
var judgement = e.HitObject.CreateJudgement();
var judgementResult = new JudgementResult(e.HitObject, judgement) { Type = e.Result };
double healthIncrease = judgement.HealthIncreaseFor(judgementResult);
currentHealth = Math.Clamp(currentHealth + healthIncrease, 0, 1);
double time = e.HitObject.StartTime;
float xPosition = timeRange > 0 ? (float)((time - minTime) / timeRange) : 0;
float x = (xPosition * (DrawWidth - leftMarginConst - rightMarginConst)) - (DrawWidth / 2) + leftMarginConst;
float y = (float)((1 - currentHealth) * DrawHeight - DrawHeight / 2);
healthPoints.Add(new Vector2(x, y));
}
if (healthPoints.Count > 1)
{
AddInternal(new Path
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
PathRadius = 1,
Colour = Color4.Red,
Alpha = 0.3f,
Vertices = healthPoints.ToArray()
});
}
}
private void drawBoundaryLine(double boundary, HitResult result)
{
// // 计算当前区间内的 note 数量占比
// int notesInBoundary = hitEvents.Count(e => e.TimeOffset <= boundary);
// float noteRatio = (float)notesInBoundary / hitEvents.Count;
//
// // 根据 noteRatio 动态调整透明度noteRatio 越大透明度越低
// float adjustedAlpha = 0.1f + (1 - noteRatio) * 0.3f; // 最低透明度为 0.2f,最高为 0.5f
float availableWidth = DrawWidth - leftMarginConst - rightMarginConst + 20;
float relativeWidth = availableWidth / DrawWidth;
// 绘制中心轴 (0ms)
AddInternal(new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Height = 1,
Width = relativeWidth,
// X = centerOffset,
Alpha = 0.1f,
Colour = Color4.Gray,
});
AddInternal(new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Height = 1,
Width = relativeWidth,
Alpha = 0.1f,
Colour = colours.ForHitResult(result),
Y = (float)(boundary + current_offset),
});
AddInternal(new OsuSpriteText
{
Text = $"{boundary:+0.##;-0.##}",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(size: 12),
Colour = Color4.White,
X = leftMarginConst - 25,
Y = (float)(boundary + current_offset),
});
}
}
public partial class CreateRotatedColumnGraphs : CompositeDrawable
{
private const float horizontal_spacing_ratio = 0.015f;
private const float top_margin = 20;
private const float bottom_margin = 10;
private const float horizontal_margin = 10;
private readonly List<IGrouping<int, HitEvent>> hitEventsByColumn;
public CreateRotatedColumnGraphs(List<IGrouping<int, HitEvent>> hitEventsByColumn)
{
this.hitEventsByColumn = hitEventsByColumn;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
if (hitEventsByColumn.Count == 0)
return;
// 创建所有UI元素
for (int i = 0; i < hitEventsByColumn.Count; i++)
{
var column = hitEventsByColumn[i];
// 添加标题
AddInternal(new OsuSpriteText
{
Text = $"Column {column.Key + 1}",
Font = OsuFont.GetFont(size: 14),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopCentre
});
// 添加图表
AddInternal(new HitEventTimingDistributionGraph(column.ToList())
{
RelativeSizeAxes = Axes.None,
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomLeft,
Rotation = 90
});
}
updateLayout();
}
protected override void Update()
{
base.Update();
if (InternalChildren.Count == 0 || DrawWidth <= 0)
return;
updateLayout();
}
private void updateLayout()
{
int columnCount = hitEventsByColumn.Count;
if (columnCount == 0) return;
float effectiveWidth = DrawWidth - (2 * horizontal_margin);
float totalSpacingWidth = horizontal_spacing_ratio * (columnCount - 1);
float columnWidthRatio = (1f - totalSpacingWidth) / columnCount;
float xPosition = horizontal_margin;
for (int i = 0; i < columnCount; i++)
{
float columnWidth = columnWidthRatio * effectiveWidth;
float spacingWidth = horizontal_spacing_ratio * effectiveWidth;
int titleIndex = i * 2;
int graphIndex = i * 2 + 1;
// 更新标题位置
if (titleIndex < InternalChildren.Count && InternalChildren[titleIndex] is OsuSpriteText titleText)
{
titleText.X = xPosition + columnWidth / 2;
titleText.Y = 0;
}
// 更新图表位置和尺寸
if (graphIndex < InternalChildren.Count && InternalChildren[graphIndex] is HitEventTimingDistributionGraph graph)
{
graph.Width = DrawHeight - top_margin - bottom_margin;
graph.Height = columnWidth;
graph.X = xPosition;
graph.Y = top_margin;
}
xPosition += columnWidth + spacingWidth;
}
}
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.LAsEZMania.Helper;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.LAsEzMania.Analysis
{
/// <summary>
/// Mania-specific implementation of score graph that extends BaseEzScoreGraph.
/// Provides LN (Long Note) aware scoring calculation for Classic mode.
/// </summary>
public partial class EzManiaScoreGraph : BaseEzScoreGraph
{
private readonly ManiaHitWindows maniaHitWindows = new ManiaHitWindows();
public EzManiaScoreGraph(ScoreInfo score, IBeatmap beatmap)
: base(score, beatmap, new ManiaHitWindows())
{
maniaHitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
}
protected override double UpdateBoundary(HitResult result)
{
return maniaHitWindows.WindowFor(result);
}
/// <summary>
/// Override to get Mania-specific hit events for V1 calculation.
/// </summary>
protected override IEnumerable<HitEvent> GetApplicableHitEvents()
{
return Score.HitEvents.Where(e => e.Result.IsBasic());
}
private readonly CustomHitWindowsHelper hitWindows1 = new CustomHitWindowsHelper(EzMUGHitMode.Classic)
{
OverallDifficulty = OD
};
private readonly CustomHitWindowsHelper hitWindows2 = new CustomHitWindowsHelper
{
OverallDifficulty = OD
};
protected override HitResult RecalculateV1Result(HitEvent hitEvent)
{
return hitWindows1.ResultFor(hitEvent.TimeOffset);
}
protected override HitResult RecalculateV2Result(HitEvent hitEvent)
{
return hitWindows2.ResultFor(hitEvent.TimeOffset);
}
}
}
// protected override void CalculateV1Accuracy()
// {
// base.CalculateV1Accuracy();
//
// var maniaHitWindows = new ManiaHitWindows
// {
// CustomHitWindows = false,
// ClassicModActive = true,
// IsConvert = false,
// ScoreV2Active = false,
// SpeedMultiplier = (HitWindows as ManiaHitWindows)?.SpeedMultiplier ?? 1.0,
// DifficultyMultiplier = (HitWindows as ManiaHitWindows)?.DifficultyMultiplier ?? 1.0
// };
//
// maniaHitWindows.ResetRange();
// maniaHitWindows.SetDifficulty(OD);
//
// double PerfectRange = maniaHitWindows.WindowFor(HitResult.Perfect);
// double GreatRange = maniaHitWindows.WindowFor(HitResult.Great);
// double GoodRange = maniaHitWindows.WindowFor(HitResult.Good);
// double OkRange = maniaHitWindows.WindowFor(HitResult.Ok);
// double MehRange = maniaHitWindows.WindowFor(HitResult.Meh);
// double MissRange = maniaHitWindows.WindowFor(HitResult.Miss);
//
// Logger.Log($"[EzManiaScoreGraph] V1 HitWindows: P{PerfectRange} G{GreatRange} Go{GoodRange} O{OkRange} M{MehRange} Mi{MissRange}");
//
// double[] HeadOffsets = new double[18];
// double MaxPoints = 0;
// double TotalPoints = 0;
//
// double getLNScore(double head, double tail)
// {
// double combined = head + tail;
//
// (double range, double headFactor, double combinedFactor, double scoreVal)[] rules = new[]
// {
// (PerfectRange, 1.2, 2.4, 300.0),
// (GreatRange, 1.1, 2.2, 300.0),
// (GoodRange, 1.0, 2.0, 200.0),
// (OkRange, 1.0, 2.0, 100.0),
// (MehRange, 1.0, 2.0, 50.0),
// };
//
// foreach (var (range, headFactor, combinedFactor, scoreValue) in rules)
// {
// if (head < range * headFactor && combined < range * combinedFactor)
// {
// return scoreValue;
// }
// }
//
// return 0;
// }
//
// Dictionary<HitResult, int> v1Counts = new Dictionary<HitResult, int>();
//
// foreach (var hit in Score.HitEvents.Where(e => e.Result.IsBasic()))
// {
// double offset = Math.Abs(hit.TimeOffset);
// HitResult result = maniaHitWindows.ResultFor(offset);
// v1Counts[result] = v1Counts.GetValueOrDefault(result, 0) + 1;
//
// var hitObject = (ManiaHitObject)hit.HitObject;
//
// if (hitObject is HeadNote)
// {
// HeadOffsets[hitObject.Column] = offset;
// }
// else if (hitObject is TailNote)
// {
// MaxPoints += 300;
// TotalPoints += getLNScore(HeadOffsets[hitObject.Column], offset);
// HeadOffsets[hitObject.Column] = 0;
// }
// else if (hitObject is Note)
// {
// MaxPoints += 300;
// TotalPoints += result switch
// {
// HitResult.Perfect => 300,
// HitResult.Great => 300,
// HitResult.Good => 200,
// HitResult.Ok => 100,
// HitResult.Meh => 50,
// _ => 0
// };
// }
// }
//
// double accuracy = MaxPoints > 0 ? TotalPoints / MaxPoints : 0;
// V1Accuracy = accuracy;
// V1Counts = v1Counts;
// // V1Score = TotalPoints;
// }

View File

@@ -0,0 +1,346 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.LAsEzExtensions.Background;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Rulesets.Mania.Objects.EzCurrentHitObject;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.LAsEZMania.Helper
{
public class CustomHitWindowsHelper
{
private EzMUGHitMode hitMode = EzMUGHitMode.Classic;
public EzMUGHitMode HitMode
{
get => hitMode;
set
{
hitMode = value;
updateRanges();
}
}
private double totalMultiplier = 1.0;
public double TotalMultiplier
{
get => totalMultiplier;
set
{
totalMultiplier = value;
updateRanges();
}
}
private double overallDifficulty = 1.0;
public double OverallDifficulty
{
get => overallDifficulty;
set
{
overallDifficulty = value;
updateRanges();
}
}
private double bpm;
public double BPM
{
get => bpm;
set
{
bpm = value;
updateRanges();
}
}
// Ranges compatible with Mania naming used elsewhere (Range305 == Perfect, Range300 == Great, ...)
public double Range305 { get; private set; }
public double Range300 { get; private set; }
public double Range200 { get; private set; }
public double Range100 { get; private set; }
public double Range050 { get; private set; }
public double Range000 { get; private set; }
public double PoolRange { get; private set; }
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
private const double pool_offset = 150.0;
public CustomHitWindowsHelper()
: this(GlobalConfigStore.EzConfig?.Get<EzMUGHitMode>(Ez2Setting.HitMode) ?? EzMUGHitMode.Classic)
{
}
public CustomHitWindowsHelper(EzMUGHitMode hitMode)
{
HitMode = hitMode;
// UpdateRanges is called by the property setter
}
public double[] GetHitWindowsClassic()
{
double invertedOd = Math.Clamp(10 - OverallDifficulty, 0, 10);
Range305 = Math.Floor(16 * TotalMultiplier) + 0.5;
Range300 = Math.Floor((34 + 3 * invertedOd) * TotalMultiplier) + 0.5;
Range200 = Math.Floor((67 + 3 * invertedOd) * TotalMultiplier) + 0.5;
Range100 = Math.Floor((97 + 3 * invertedOd) * TotalMultiplier) + 0.5;
Range050 = Math.Floor((121 + 3 * invertedOd) * TotalMultiplier) + 0.5;
Range000 = Math.Floor((158 + 3 * invertedOd) * TotalMultiplier) + 0.5;
PoolRange = Range000 + pool_offset;
return new[] { Range305, Range300, Range200, Range100, Range050, Range000, PoolRange };
}
public double[] GetHitWindowsO2Jam(double bpm)
{
Range305 = 7500.0 / bpm * TotalMultiplier;
Range300 = Range305;
Range200 = 22500.0 / bpm * TotalMultiplier;
Range100 = Range200;
Range050 = 31250.0 / bpm * TotalMultiplier;
Range000 = Range050;
PoolRange = Range000 + pool_offset;
return new[] { Range305, Range300, Range200, Range100, Range050, Range000, PoolRange };
}
public double[] GetHitWindowsEZ2AC()
{
Range305 = 18.0 * TotalMultiplier;
Range300 = 38.0 * TotalMultiplier;
Range200 = 68.0 * TotalMultiplier;
Range100 = 88.0 * TotalMultiplier;
Range050 = 88.0 * TotalMultiplier;
Range000 = 100.0 * TotalMultiplier;
PoolRange = pool_offset;
return new[] { Range305, Range300, Range200, Range100, Range050, Range000, PoolRange };
}
public double[] GetHitWindowsIIDX()
{
Range305 = 20.0 * TotalMultiplier;
Range300 = 40.0 * TotalMultiplier;
Range200 = 60.0 * TotalMultiplier;
Range100 = 70.0 * TotalMultiplier;
Range050 = 80.0 * TotalMultiplier;
Range000 = 100.0 * TotalMultiplier;
PoolRange = Range000 + pool_offset;
return new[] { Range305, Range300, Range200, Range100, Range050, Range000, PoolRange };
}
public double[] GetHitWindowsMelody()
{
Range305 = 20.0 * TotalMultiplier;
Range300 = 40.0 * TotalMultiplier;
Range200 = 60.0 * TotalMultiplier;
Range100 = 80.0 * TotalMultiplier;
Range050 = 100.0 * TotalMultiplier;
Range000 = 120.0 * TotalMultiplier;
PoolRange = Range000 + pool_offset;
return new[] { Range305, Range300, Range200, Range100, Range050, Range000, PoolRange };
}
public double[] GetHitWindowsFromHitMode(EzMUGHitMode hitMode, IBeatmap beatmap, int? level = null)
{
switch (hitMode)
{
case EzMUGHitMode.O2Jam:
double bpmFromBeatmap = beatmap.BeatmapInfo.BPM <= 0 ? O2HitModeExtension.DEFAULT_BPM : beatmap.BeatmapInfo.BPM;
return GetHitWindowsO2Jam(bpmFromBeatmap);
case EzMUGHitMode.EZ2AC:
return GetHitWindowsEZ2AC();
case EzMUGHitMode.IIDX:
return GetHitWindowsIIDX();
case EzMUGHitMode.Melody:
return GetHitWindowsMelody();
default:
return GetHitWindowsClassic();
}
}
private void updateRanges()
{
switch (HitMode)
{
case EzMUGHitMode.O2Jam:
double usedBpm = BPM > 0 ? BPM : O2HitModeExtension.DEFAULT_BPM;
SetRanges(GetHitWindowsO2Jam(usedBpm));
break;
case EzMUGHitMode.Lazer:
double perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, perfect_window_range) * TotalMultiplier) + 0.5;
double great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, great_window_range) * TotalMultiplier) + 0.5;
double good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, good_window_range) * TotalMultiplier) + 0.5;
double ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, ok_window_range) * TotalMultiplier) + 0.5;
double meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, meh_window_range) * TotalMultiplier) + 0.5;
double miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(OverallDifficulty, miss_window_range) * TotalMultiplier) + 0.5;
double pool = miss + pool_offset;
SetRanges(new[] { perfect, great, good, ok, meh, miss, pool });
break;
case EzMUGHitMode.EZ2AC:
SetRanges(GetHitWindowsEZ2AC());
break;
case EzMUGHitMode.IIDX:
SetRanges(GetHitWindowsIIDX());
break;
case EzMUGHitMode.Melody:
SetRanges(GetHitWindowsMelody());
break;
default:
SetRanges(GetHitWindowsClassic());
break;
}
}
public HitResult ResultFor(double timeOffset)
{
timeOffset = Math.Abs(timeOffset);
if (AllowPoolEnabled)
{
if (IsHitResultAllowed(HitResult.Pool))
{
double miss = WindowFor(HitResult.Miss);
double poolEarlyWindow = miss + 50;
double poolLateWindow = miss + 50;
if (timeOffset > -poolEarlyWindow &&
timeOffset < -miss ||
timeOffset < poolLateWindow &&
timeOffset > miss)
return HitResult.Pool;
}
}
for (var result = HitResult.Perfect; result >= HitResult.Miss; --result)
{
if (IsHitResultAllowed(result) && timeOffset <= WindowFor(result))
return result;
}
return HitResult.None;
}
public virtual bool AllowPoolEnabled => GlobalConfigStore.EzConfig?.Get<bool>(Ez2Setting.CustomPoorHitResult) ?? false;
public virtual bool IsHitResultAllowed(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
case HitResult.Great:
case HitResult.Good:
case HitResult.Ok:
case HitResult.Meh:
case HitResult.Miss:
return true;
case HitResult.Pool:
return AllowPoolEnabled;
default:
return false;
}
}
public double WindowFor(HitResult result)
{
switch (result)
{
case HitResult.Perfect: return Range305;
case HitResult.Great: return Range300;
case HitResult.Good: return Range200;
case HitResult.Ok: return Range100;
case HitResult.Meh: return Range050;
case HitResult.Pool: return PoolRange;
case HitResult.Miss: return Range000;
default: throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
/// <summary>
/// Allow external code to replace the current windows (e.g. when switching hit modes).
/// </summary>
public void SetRanges(double[] ranges)
{
if (ranges == null) return;
if (ranges.Length >= 6)
{
Range305 = ranges[0];
Range300 = ranges[1];
Range200 = ranges[2];
Range100 = ranges[3];
Range050 = ranges[4];
Range000 = ranges[5];
}
if (ranges.Length >= 7)
PoolRange = ranges[6];
else
PoolRange = Range000 + pool_offset;
}
/// <summary>
/// Compute LN (long note) tail score given head and tail offsets using this helper's ranges.
/// </summary>
public double GetLNScore(double head, double tail)
{
// This LN scoring method is Classic-specific: always use Classic hit windows
double[] classicRanges = GetHitWindowsClassic();
double r305 = classicRanges[0];
double r300 = classicRanges[1];
double r200 = classicRanges[2];
double r100 = classicRanges[3];
double r050 = classicRanges[4];
double combined = head + tail;
(double range, double headFactor, double combinedFactor, double score)[] rules = new[]
{
(range: r305, headFactor: 1.2, combinedFactor: 2.4, score: 300.0),
(range: r300, headFactor: 1.1, combinedFactor: 2.2, score: 300),
(range: r200, headFactor: 1.0, combinedFactor: 2.0, score: 200),
(range: r100, headFactor: 1.0, combinedFactor: 2.0, score: 100),
(range: r050, headFactor: 1.0, combinedFactor: 2.0, score: 50),
};
foreach (var (range, headFactor, combinedFactor, score) in rules)
{
if (head < range * headFactor && combined < range * combinedFactor)
return score;
}
return 0;
}
}
}

View File

@@ -478,7 +478,7 @@ namespace osu.Game.Rulesets.Mania
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
new StatisticItem("Space Graph", () => new EzHitEventHeatmapGraph(score, playableBeatmap) //TODO:改成可实时变更
new StatisticItem("Space Graph", () => new EzManiaScoreGraph(score, playableBeatmap) //TODO:改成可实时变更
{
RelativeSizeAxes = Axes.X,
Height = 200

View File

@@ -7,11 +7,13 @@ using osu.Game.LAsEzExtensions.Background;
using osu.Game.Rulesets.Scoring;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Rulesets.Mania.Objects.EzCurrentHitObject;
using osu.Game.Rulesets.Mania.LAsEZMania.Helper;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHitWindows : HitWindows
{
private readonly CustomHitWindowsHelper customHelper = new CustomHitWindowsHelper(GlobalConfigStore.EzConfig?.Get<EzMUGHitMode>(Ez2Setting.HitMode) ?? EzMUGHitMode.Lazer);
public static readonly DifficultyRange PERFECT_WINDOW_RANGE = new DifficultyRange(22.4D, 19.4D, 13.9D);
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
@@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
}
private bool customHitWindows;
private static bool customHitWindows;
public bool CustomHitWindows
{
@@ -155,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
public void SetSpecialDifficultyRange(double perfect, double great, double good, double ok, double meh, double miss)
{
if (!customHitWindows) return;
customHitWindows = true;
PerfectRange = perfect;
GreatRange = great;
@@ -168,7 +170,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
public void SetSpecialDifficultyRange(double[] difficultyRangeArray)
{
if (!customHitWindows) return;
customHitWindows = true;
PerfectRange = difficultyRangeArray[0];
GreatRange = difficultyRangeArray[1];
@@ -187,7 +189,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
public void SetHitMode(IBeatmap beatmap)
{
if (!CustomHitWindows) return;
// if (!CustomHitWindows) return;
EzMUGHitMode hitMode = GlobalConfigStore.EzConfig?.Get<EzMUGHitMode>(Ez2Setting.HitMode) ?? EzMUGHitMode.Lazer;
@@ -198,31 +200,25 @@ namespace osu.Game.Rulesets.Mania.Scoring
? O2HitModeExtension.DEFAULT_BPM
: beatmap.BeatmapInfo.BPM;
double coolRange = 7500.0 / bpm * totalMultiplier;
double goodRange = 22500.0 / bpm * totalMultiplier;
double badRange = 31250.0 / bpm * totalMultiplier;
// 注意O2Jam的判定窗口是基于BPM变化的。Bad约等于om的Miss。
// 此处的作用只是为了方便显示和计算,并不会影响实际判定。
SetSpecialDifficultyRange(new[] { coolRange, coolRange, goodRange, goodRange, badRange, badRange });
// 这里是真正影响判定的BPM设定
O2HitModeExtension.NowBeatmapBPM = bpm;
O2HitModeExtension.PillCount.Value = 0;
O2HitModeExtension.CoolCombo = 0;
O2HitModeExtension.PillActivated = true;
SetSpecialDifficultyRange(customHelper.GetHitWindowsO2Jam(bpm));
break;
case EzMUGHitMode.EZ2AC:
SetSpecialDifficultyRange(new[] { 18.0, 38.0, 68.0, 88.0, 88.0, 100.0 });
SetSpecialDifficultyRange(customHelper.GetHitWindowsEZ2AC());
break;
case EzMUGHitMode.IIDX:
SetSpecialDifficultyRange(new[] { 20.0, 40.0, 60.0, 70.0, 80.0, 100.0 });
SetSpecialDifficultyRange(customHelper.GetHitWindowsIIDX());
break;
case EzMUGHitMode.Melody:
SetSpecialDifficultyRange(new[] { 20.0, 40.0, 60.0, 80.0, 100.0, 120.0 });
SetSpecialDifficultyRange(customHelper.GetHitWindowsMelody());
break;
default:
@@ -235,30 +231,14 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
if (customHitWindows)
{
if (ClassicModActive)
{
double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10);
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
great = Math.Floor((34 + 3 * invertedOd) * totalMultiplier) + 0.5;
good = Math.Floor((67 + 3 * invertedOd) * totalMultiplier) + 0.5;
ok = Math.Floor((97 + 3 * invertedOd) * totalMultiplier) + 0.5;
meh = Math.Floor((121 + 3 * invertedOd) * totalMultiplier) + 0.5;
miss = Math.Floor((158 + 3 * invertedOd) * totalMultiplier) + 0.5;
}
else
{
perfect = PerfectRange * totalMultiplier;
great = GreatRange * totalMultiplier;
good = GoodRange * totalMultiplier;
ok = OkRange * totalMultiplier;
meh = MehRange * totalMultiplier;
miss = MissRange * totalMultiplier;
return;
}
perfect = PerfectRange;
great = GreatRange;
good = GoodRange;
ok = OkRange;
meh = MehRange;
miss = MissRange;
}
if (ClassicModActive && !ScoreV2Active)
else if (ClassicModActive && !ScoreV2Active)
{
if (IsConvert)
{

View File

@@ -5,7 +5,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.LAsEZMania.Helper;
using osu.Game.Rulesets.Mania.Mods.YuLiangSSSMods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@@ -23,10 +26,18 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
=> base.EnumerateHitObjects(beatmap).Order(JudgementOrderComparer.DEFAULT);
{
od = Mods.Value.OfType<ManiaModAdjust>().FirstOrDefault(m => m.OverallDifficulty.Value is not null && m.CustomOD.Value)
?.OverallDifficulty.Value ?? beatmap.Difficulty.OverallDifficulty;
return base.EnumerateHitObjects(beatmap).Order(JudgementOrderComparer.DEFAULT);
}
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
// if (IsLegacyScore)
// return ;
return 150000 * comboProgress
+ 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
@@ -42,6 +53,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
switch (result)
{
case HitResult.Perfect:
if (IsLegacyScore)
return 300;
return 305;
}
@@ -101,26 +115,18 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
}
private double od = 8;
protected override void ApplyScoreChange(JudgementResult judgement)
{
// if (!IsLegacyScore) return;
var hitWindows = new ManiaHitWindows
var hitWindows = new CustomHitWindowsHelper(EzMUGHitMode.Classic)
{
ClassicModActive = true,
IsConvert = false,
ScoreV2Active = false,
OverallDifficulty = od
};
PerfectRange = hitWindows.WindowFor(HitResult.Perfect);
GreatRange = hitWindows.WindowFor(HitResult.Great);
GoodRange = hitWindows.WindowFor(HitResult.Good);
OkRange = hitWindows.WindowFor(HitResult.Ok);
MehRange = hitWindows.WindowFor(HitResult.Meh);
MissRange = hitWindows.WindowFor(HitResult.Miss);
double offset = Math.Abs(judgement.TimeOffset);
var result = GetResultByOffset(offset);
var result = hitWindows.ResultFor(offset);
var hitObject = (ManiaHitObject)judgement.HitObject;
if (hitObject is HeadNote)
@@ -130,7 +136,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
else if (hitObject is TailNote)
{
ClassicMaxBaseScore += 300;
ClassicBaseScore += GetLNScore(headOffsets[hitObject.Column], offset);
ClassicBaseScore += hitWindows.GetLNScore(headOffsets[hitObject.Column], offset);
headOffsets[hitObject.Column] = 0;
}
else if (hitObject is Note)
@@ -146,68 +152,24 @@ namespace osu.Game.Rulesets.Mania.Scoring
_ => 0
};
}
UpdateScoreClassic();
}
protected HitResult GetResultByOffset(double offset) =>
offset < PerfectRange ? HitResult.Perfect :
offset < GreatRange ? HitResult.Great :
offset < GoodRange ? HitResult.Good :
offset < OkRange ? HitResult.Ok :
offset < MehRange ? HitResult.Meh :
offset < MissRange ? HitResult.Miss :
HitResult.None;
public double GetLNScore(double head, double tail)
{
double combined = head + tail;
(double range, double headFactor, double combinedFactor, double score)[] rules = new[]
{
(PerfectRange, 1.2, 2.4, 300.0),
(GreatRange, 1.1, 2.2, 300),
(GoodRange, 1.0, 2.0, 200),
(OkRange, 1.0, 2.0, 100),
(MehRange, 1.0, 2.0, 50),
};
foreach (var (range, headFactor, combinedFactor, score) in rules)
{
if (head < range * headFactor && combined < range * combinedFactor)
return score;
}
return 0;
}
// LN scoring logic moved to CustomHitWindowsHelper.GetLNScore(head, tail).
private readonly double[] headOffsets = new double[18];
public double PerfectRange = 16;
public double GreatRange = 34;
public double GoodRange = 67;
public double OkRange = 97;
public double MehRange = 121;
public double MissRange = 158;
protected override void RemoveScoreChange(JudgementResult judgement)
{
// if (!IsLegacyScore) return;
var hitWindows = new ManiaHitWindows
var hitWindows = new CustomHitWindowsHelper(EzMUGHitMode.Classic)
{
ClassicModActive = true,
IsConvert = false,
ScoreV2Active = false
OverallDifficulty = od
};
PerfectRange = hitWindows.WindowFor(HitResult.Perfect);
GreatRange = hitWindows.WindowFor(HitResult.Great);
GoodRange = hitWindows.WindowFor(HitResult.Good);
OkRange = hitWindows.WindowFor(HitResult.Ok);
MehRange = hitWindows.WindowFor(HitResult.Meh);
MissRange = hitWindows.WindowFor(HitResult.Miss);
double offset = Math.Abs(judgement.TimeOffset);
var result = GetResultByOffset(offset);
var result = hitWindows.ResultFor(offset);
var hitObject = (ManiaHitObject)judgement.HitObject;
if (hitObject is HeadNote)
@@ -217,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
else if (hitObject is TailNote)
{
ClassicMaxBaseScore -= 300;
ClassicBaseScore -= GetLNScore(headOffsets[hitObject.Column], offset);
ClassicBaseScore -= hitWindows.GetLNScore(headOffsets[hitObject.Column], offset);
headOffsets[hitObject.Column] = 0;
}
else if (hitObject is Note)
@@ -233,6 +195,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
_ => 0
};
}
UpdateScoreClassic();
}
}
}

View File

@@ -0,0 +1,546 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.LAsEzExtensions.Analysis
{
/// <summary>
/// 基类分数图表,用于分析和可视化得分数据。
/// 继承类应实现特定规则集的逻辑。
/// </summary>
public abstract partial class BaseEzScoreGraph : CompositeDrawable
{
protected readonly IReadOnlyList<HitEvent> HitEvents;
protected readonly ScoreInfo Score;
protected readonly IBeatmap Beatmap;
protected ScoreProcessor ScoreProcessor { get; set; }
protected HitWindows HitWindows { get; set; }
protected virtual HitResult GetHitResult()
{
return HitResult.Perfect;
}
protected static double HP;
protected static double OD;
private const int current_offset = 0;
private const int time_bins = 50;
private const float circle_size = 5f;
private double binSize;
private double maxTime;
private double minTime;
private double timeRange;
protected float LeftMarginConst { get; set; } = 158;
protected float RightMarginConst { get; set; } = 7;
protected double V1Accuracy { get; set; }
protected long V1Score { get; set; }
protected Dictionary<HitResult, int> V1Counts { get; set; } = new Dictionary<HitResult, int>();
// V2 Accuracy calculation properties
protected double V2Accuracy { get; set; }
protected long V2Score { get; set; }
protected Dictionary<HitResult, int> V2Counts { get; set; } = new Dictionary<HitResult, int>();
[Resolved]
private OsuColour colours { get; set; } = null!;
protected BaseEzScoreGraph(ScoreInfo score, IBeatmap beatmap, HitWindows hitWindows)
{
Score = score;
HitWindows = hitWindows;
HitEvents = score.HitEvents;
Beatmap = beatmap;
HP = beatmap.Difficulty.DrainRate;
// OD = beatmap.Difficulty.OverallDifficulty;
ScoreProcessor = Score.Ruleset.CreateInstance().CreateScoreProcessor();
}
[BackgroundDependencyLoader]
private void load()
{
if (HitEvents.Count == 0)
return;
binSize = Math.Ceiling(HitEvents.Max(e => e.HitObject.StartTime) / time_bins);
binSize = Math.Max(1, binSize);
maxTime = HitEvents.Count > 0 ? HitEvents.Max(e => e.HitObject.StartTime) : 1;
minTime = HitEvents.Count > 0 ? HitEvents.Min(e => e.HitObject.StartTime) : 0;
timeRange = maxTime - minTime;
Scheduler.AddOnce(updateDisplay);
}
/// <summary>
/// Calculate V1 (Classic) accuracy. Subclasses should override CalculateV1ScoresManually instead of this method.
/// Sets V1Accuracy, V1Score, and V1Counts properties instead of returning values.
/// </summary>
protected virtual void CalculateV1Accuracy()
{
ScoreProcessor.Mods.Value = Score.Mods;
ScoreProcessor.ApplyBeatmap(Beatmap);
var v1Counts = new Dictionary<HitResult, int>();
foreach (var hitEvent in GetApplicableHitEvents())
{
var recalculated = RecalculateV1Result(hitEvent);
// track recalculated counts for V1
v1Counts[recalculated] = v1Counts.GetValueOrDefault(recalculated, 0) + 1;
ScoreProcessor.ApplyResult(new JudgementResult(hitEvent.HitObject, hitEvent.HitObject.CreateJudgement())
{
Type = recalculated,
TimeOffset = hitEvent.TimeOffset
});
}
double accuracy = ScoreProcessor.AccuracyClassic.Value;
long totalScore = ScoreProcessor.TotalScore.Value;
Logger.Log($"[V1 ScoreProcessor]: {accuracy * 100:F2}%, Score: {totalScore / 10000}w");
// Set properties instead of returning
V1Accuracy = accuracy;
V1Score = totalScore;
V1Counts = v1Counts;
}
protected virtual IEnumerable<HitEvent> GetApplicableHitEvents()
{
return Score.HitEvents.Where(e => e.Result.IsBasic());
}
/// <summary>
/// Recalculate the V1-style HitResult for a given <see cref="HitEvent"/>.
/// Subclasses may override to provide ruleset-specific V1 judgement logic (e.g. Mania's CustomHitWindowsHelper).
/// </summary>
/// <param name="hitEvent">The hit event to recalculate for.</param>
/// <returns>The recalculated <see cref="HitResult"/> for V1 accuracy.</returns>
protected virtual HitResult RecalculateV1Result(HitEvent hitEvent)
{
double offset = Math.Abs(hitEvent.TimeOffset);
return HitWindows.ResultFor(offset);
}
/// <summary>
/// Calculate V2 accuracy. Subclasses can override to customize calculation.
/// Sets V2Accuracy, V2Score, and V2Counts properties instead of returning values.
/// </summary>
protected virtual void CalculateV2Accuracy()
{
// Create a fresh ScoreProcessor for V2 calculation (V1 already used one)
var v2ScoreProcessor = Score.Ruleset.CreateInstance().CreateScoreProcessor();
if (v2ScoreProcessor == null)
{
V2Accuracy = 0;
V2Score = 0;
V2Counts = new Dictionary<HitResult, int>();
return;
}
v2ScoreProcessor.Mods.Value = Score.Mods;
v2ScoreProcessor.ApplyBeatmap(Beatmap);
foreach (var hitEvent in GetApplicableHitEvents())
{
var judgement = hitEvent.HitObject.CreateJudgement();
v2ScoreProcessor.ApplyResult(new JudgementResult(hitEvent.HitObject, judgement)
{
Type = hitEvent.Result,
TimeOffset = hitEvent.TimeOffset
});
}
Dictionary<HitResult, int> v2Counts = HitEvents.GroupBy(e => e.Result).ToDictionary(g => g.Key, g => g.Count());
double accuracy = v2ScoreProcessor.Accuracy.Value;
long totalScore = v2ScoreProcessor.TotalScore.Value;
Logger.Log($"[V2 ScoreProcessor] Accuracy: {accuracy * 100:F2}%, Score: {totalScore / 10000}w");
// Set properties instead of returning
V2Accuracy = accuracy;
V2Score = totalScore;
V2Counts = v2Counts;
}
private void updateDisplay()
{
ClearInternal();
double scAcc = Score.Accuracy * 100;
long scScore = Score.TotalScore;
CalculateV1Accuracy();
CalculateV2Accuracy();
AddInternal(new GridContainer
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Position = Vector2.Zero,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc org",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {scAcc:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc v2",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Accuracy * 100:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Acc v1",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V1Accuracy * 100:F1}%",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr org",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {scScore / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr v2",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Score / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Scr v1",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V1Score / 1000.0:F0}k",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "Pauses",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {Score.Pauses.Count}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "PERFECT",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Perfect, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Perfect, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "GREAT",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Great, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Great, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "GOOD",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Good, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Good, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "OK",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Ok, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Ok, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "MEH",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Meh, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Meh, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
new Drawable[]
{
new OsuSpriteText
{
Text = "MISS",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
new OsuSpriteText
{
Text = $" : {V2Counts.GetValueOrDefault(HitResult.Miss, 0)}\\{V1Counts.GetValueOrDefault(HitResult.Miss, 0)}",
Font = OsuFont.GetFont(size: 14),
Colour = Color4.White,
},
},
}
});
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).Cast<HitResult>().Where(r => r <= HitResult.Perfect && r >= HitResult.Meh))
{
double boundary = UpdateBoundary(result);
drawBoundaryLine(boundary, result);
drawBoundaryLine(-boundary, result);
}
var sortedHitEvents = HitEvents.OrderBy(e => e.HitObject.StartTime).ToList();
foreach (var e in sortedHitEvents)
{
double time = e.HitObject.StartTime;
float xPosition = timeRange > 0 ? (float)((time - minTime) / timeRange) : 0;
float yPosition = (float)(e.TimeOffset + current_offset);
AddInternal(new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = (xPosition * (DrawWidth - LeftMarginConst - RightMarginConst)) - (DrawWidth / 2) + LeftMarginConst,
Y = yPosition,
Alpha = 0.8f,
Colour = colours.ForHitResult(e.Result),
});
}
drawHealthLine();
}
protected virtual double UpdateBoundary(HitResult result)
{
return HitWindows.WindowFor(result);
}
private void drawHealthLine()
{
var sortedEvents = HitEvents.OrderBy(e => e.HitObject.StartTime).ToList();
double currentHealth = 0.0;
List<Vector2> healthPoints = new List<Vector2>();
foreach (var e in sortedEvents)
{
var judgement = e.HitObject.CreateJudgement();
var judgementResult = new JudgementResult(e.HitObject, judgement) { Type = e.Result };
double healthIncrease = judgement.HealthIncreaseFor(judgementResult);
currentHealth = Math.Clamp(currentHealth + healthIncrease, 0, 1);
double time = e.HitObject.StartTime;
float xPosition = timeRange > 0 ? (float)((time - minTime) / timeRange) : 0;
float x = (xPosition * (DrawWidth - LeftMarginConst - RightMarginConst)) - (DrawWidth / 2) + LeftMarginConst;
float y = (float)((1 - currentHealth) * DrawHeight - DrawHeight / 2);
healthPoints.Add(new Vector2(x, y));
}
if (healthPoints.Count > 1)
{
AddInternal(new Path
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
PathRadius = 1,
Colour = Color4.Red,
Alpha = 0.3f,
Vertices = healthPoints.ToArray()
});
}
}
private void drawBoundaryLine(double boundary, HitResult result)
{
float availableWidth = DrawWidth - LeftMarginConst - RightMarginConst + 20;
float relativeWidth = availableWidth / DrawWidth;
AddInternal(new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Height = 1,
Width = relativeWidth,
Alpha = 0.1f,
Colour = Color4.Gray,
});
AddInternal(new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Height = 1,
Width = relativeWidth,
Alpha = 0.1f,
Colour = colours.ForHitResult(result),
Y = (float)(boundary + current_offset),
});
AddInternal(new OsuSpriteText
{
Text = $"{boundary:+0.##;-0.##}",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(size: 12),
Colour = Color4.White,
X = LeftMarginConst - 25,
Y = (float)(boundary + current_offset),
});
}
protected abstract HitResult RecalculateV2Result(HitEvent hitEvent);
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.LAsEzExtensions.Analysis
{
/// <summary>
/// Default implementation of score graph for general rulesets.
/// Inherits from BaseEzScoreGraph to leverage shared hit event analysis and visualization.
/// </summary>
public partial class EzScoreGraph : BaseEzScoreGraph
{
public EzScoreGraph(ScoreInfo score, IBeatmap beatmap)
: base(score, beatmap, new DefaultHitWindows())
{
HitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
}
protected override HitResult RecalculateV2Result(HitEvent hitEvent)
{
return HitWindows.ResultFor(hitEvent.TimeOffset);
}
}
public partial class CreateRotatedColumnGraphs : CompositeDrawable
{
private const float horizontal_spacing_ratio = 0.015f;
private const float top_margin = 20;
private const float bottom_margin = 10;
private const float horizontal_margin = 10;
private readonly List<IGrouping<int, HitEvent>> hitEventsByColumn;
public CreateRotatedColumnGraphs(List<IGrouping<int, HitEvent>> hitEventsByColumn)
{
this.hitEventsByColumn = hitEventsByColumn;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
if (hitEventsByColumn.Count == 0)
return;
// 创建所有UI元素
for (int i = 0; i < hitEventsByColumn.Count; i++)
{
var column = hitEventsByColumn[i];
// 添加标题
AddInternal(new OsuSpriteText
{
Text = $"Column {column.Key + 1}",
Font = OsuFont.GetFont(size: 14),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopCentre
});
// 添加图表
AddInternal(new HitEventTimingDistributionGraph(column.ToList())
{
RelativeSizeAxes = Axes.None,
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomLeft,
Rotation = 90
});
}
updateLayout();
}
protected override void Update()
{
base.Update();
if (InternalChildren.Count == 0 || DrawWidth <= 0)
return;
updateLayout();
}
private void updateLayout()
{
int columnCount = hitEventsByColumn.Count;
if (columnCount == 0) return;
float effectiveWidth = DrawWidth - (2 * horizontal_margin);
float totalSpacingWidth = horizontal_spacing_ratio * (columnCount - 1);
float columnWidthRatio = (1f - totalSpacingWidth) / columnCount;
float xPosition = horizontal_margin;
for (int i = 0; i < columnCount; i++)
{
float columnWidth = columnWidthRatio * effectiveWidth;
float spacingWidth = horizontal_spacing_ratio * effectiveWidth;
int titleIndex = i * 2;
int graphIndex = i * 2 + 1;
// 更新标题位置
if (titleIndex < InternalChildren.Count && InternalChildren[titleIndex] is OsuSpriteText titleText)
{
titleText.X = xPosition + columnWidth / 2;
titleText.Y = 0;
}
// 更新图表位置和尺寸
if (graphIndex < InternalChildren.Count && InternalChildren[graphIndex] is HitEventTimingDistributionGraph graph)
{
graph.Width = DrawHeight - top_margin - bottom_margin;
graph.Height = columnWidth;
graph.X = xPosition;
graph.Y = top_margin;
}
xPosition += columnWidth + spacingWidth;
}
}
}
}

View File

@@ -21,5 +21,8 @@ namespace osu.Game.LAsEzExtensions.Configuration
[Description("O2JAM Style")]
O2Jam,
[Description("Classic Style(NoAction)")]
Classic,
}
}

View File

@@ -205,18 +205,20 @@ namespace osu.Game.Rulesets.Scoring
public bool ApplyNewJudgementsWhenFailed { get; set; }
protected double ClassicBaseScore { get; set; }
protected double ClassicMaxBaseScore { get; set; }
public double ClassicBaseScore { get; protected set; }
public double ClassicMaxBaseScore { get; protected set; }
public virtual bool IsLegacyScore { get; set; }
protected virtual void ApplyScoreChangeClassic(JudgementResult result)
public bool IsLegacyScore
{
get => isLegacyScore;
set
{
isLegacyScore = value;
updateScore();
}
}
protected virtual void RemoveScoreChangeClassic(JudgementResult result)
{
}
private bool isLegacyScore;
private static double accS;
@@ -273,8 +275,6 @@ namespace osu.Game.Rulesets.Scoring
Combo.Value = 0;
}
ApplyScoreChangeClassic(result);
HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value);
result.ComboAfterJudgement = Combo.Value;
@@ -348,8 +348,6 @@ namespace osu.Game.Rulesets.Scoring
RemoveScoreChange(result);
RemoveScoreChangeClassic(result);
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
hitEvents.RemoveAt(hitEvents.Count - 1);
@@ -414,12 +412,15 @@ namespace osu.Game.Rulesets.Scoring
{
}
public void UpdateScoreClassic()
{
updateScore();
}
private void updateScore()
{
Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1;
AccuracyClassic.Value = ClassicMaxBaseScore > 0 ? ClassicBaseScore / ClassicMaxBaseScore : 1;
Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1;
MinimumAccuracy.Value = maximumBaseScore > 0 ? currentBaseScore / maximumBaseScore : 0;
MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1;
@@ -483,6 +484,7 @@ namespace osu.Game.Rulesets.Scoring
ClassicBaseScore = 0;
ClassicMaxBaseScore = 0;
AccuracyClassic.Value = 1;
currentBaseScore = 0;
currentMaximumBaseScore = 0;

View File

@@ -265,19 +265,13 @@ namespace osu.Game.Screens.SelectV2
{
updateCriteria();
if (ruleset.Value.OnlineID == 2)
if (ruleset.Value.OnlineID == 1) // Taiko
{
csSelector.Hide();
xxySrFilterButton.Hide();
}
else
{
csSelector.Show();
if (ruleset.Value.OnlineID == 3)
{
xxySrFilterButton.Show();
}
}
});
mods.BindValueChanged(m =>

View File

@@ -363,9 +363,11 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JIT/@EntryIndexedValue">JIT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KDDK/@EntryIndexedValue">KDDK</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KKDD/@EntryIndexedValue">KKDD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LN/@EntryIndexedValue">LN</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue">LTRB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD5</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NS/@EntryIndexedValue">NS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OD/@EntryIndexedValue">OD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PM/@EntryIndexedValue">PM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGB/@EntryIndexedValue">RGB</s:String>
@@ -1069,4 +1071,6 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=velopack/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Welford_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zoomable/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xxysr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=XXY/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zoomable/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>