mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-15 03:20:27 +00:00
修复成绩分析中的acc计算问题;增加判定分析辅助工具
This commit is contained in:
@@ -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>()
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
osu.Game.Rulesets.Mania/LAsEzMania/Analysis/EzManiaScoreGraph.cs
Normal file
160
osu.Game.Rulesets.Mania/LAsEzMania/Analysis/EzManiaScoreGraph.cs
Normal 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;
|
||||
// }
|
||||
|
||||
|
||||
346
osu.Game.Rulesets.Mania/LAsEzMania/Helper/CustomHitWindows.cs
Normal file
346
osu.Game.Rulesets.Mania/LAsEzMania/Helper/CustomHitWindows.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
546
osu.Game/LAsEzExtensions/Analysis/BaseEzScoreGraph.cs
Normal file
546
osu.Game/LAsEzExtensions/Analysis/BaseEzScoreGraph.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
137
osu.Game/LAsEzExtensions/Analysis/EzScoreGraph.cs
Normal file
137
osu.Game/LAsEzExtensions/Analysis/EzScoreGraph.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,5 +21,8 @@ namespace osu.Game.LAsEzExtensions.Configuration
|
||||
|
||||
[Description("O2JAM Style")]
|
||||
O2Jam,
|
||||
|
||||
[Description("Classic Style(NoAction)")]
|
||||
Classic,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user