拼ui 一些业务逻辑x实现

This commit is contained in:
FloatGaming
2026-03-19 06:15:09 +08:00
parent f5c6f143c0
commit 49e45ac464
1118 changed files with 246518 additions and 5368 deletions
@@ -0,0 +1,431 @@
using System.Collections.Generic;
using EasyChart;
using EasyChart.UGUI;
using UnityEngine;
public class URadarChartController : MonoBehaviour
{
private const int AxisCount = 6;
[Header("Chart")]
public UGUIChartBridge chartBridge;
public bool rebuildOnEnable = true;
public bool showReferenceSeries = false;
public int enforcedSortOrder = 0;
[Header("Series Names")]
public string mainSeriesName = "Player";
public string referenceSeriesName = "Reference";
[Header("Axes")]
public List<URadarAxisEntry> axes = new List<URadarAxisEntry>
{
new URadarAxisEntry { label = "A", value = 72f, referenceValue = 60f },
new URadarAxisEntry { label = "B", value = 55f, referenceValue = 60f },
new URadarAxisEntry { label = "C", value = 68f, referenceValue = 60f },
new URadarAxisEntry { label = "D", value = 49f, referenceValue = 60f },
new URadarAxisEntry { label = "E", value = 80f, referenceValue = 60f },
new URadarAxisEntry { label = "F", value = 61f, referenceValue = 60f }
};
[Header("Range")]
public float minValue = 0f;
public float maxValue = 100f;
public int splitCount = 5;
[Header("Grid")]
public bool overrideGridStyle = false;
public Color gridLineColor = new Color32(255, 255, 255, 180);
public Color outerGridLineColor = Color.white;
public float gridLineWidth = 2f;
[Header("Labels")]
public bool overrideLabelStyle = false;
public bool overrideLabelFont = false;
public Font axisLabelFont;
public int axisLabelFontSize = 18;
public Color axisLabelColor = Color.white;
public Vector2 axisLabelOffset = Vector2.zero;
[Header("Style")]
public bool overrideSeriesStyle = true;
public Color mainStrokeColor = new Color32(82, 126, 255, 255);
public Color mainFillColor = new Color(82f / 255f, 126f / 255f, 255f / 255f, 0.28f);
public Color mainPointColor = Color.white;
public Color referenceStrokeColor = new Color32(72, 229, 229, 255);
public Color referenceFillColor = new Color(72f / 255f, 229f / 255f, 229f / 255f, 0.22f);
public Color referencePointColor = Color.white;
public float radarPlotPadding = 8f;
private ChartProfile sourceProfile;
private ChartProfile runtimeProfile;
private ChartTheme runtimeTheme;
private bool rebuildQueued;
private void Awake()
{
showReferenceSeries = false;
NormalizeAxes();
QueueRebuild();
}
private void OnEnable()
{
showReferenceSeries = false;
NormalizeAxes();
if (rebuildOnEnable)
{
QueueRebuild();
}
}
private void LateUpdate()
{
if (rebuildQueued)
{
TryRebuild();
}
}
private void OnValidate()
{
showReferenceSeries = false;
NormalizeAxes();
axisLabelFontSize = Mathf.Max(1, axisLabelFontSize);
gridLineWidth = Mathf.Max(0.1f, gridLineWidth);
QueueRebuild();
}
private void OnDestroy()
{
if (runtimeProfile != null)
{
DestroyImmediate(runtimeProfile);
runtimeProfile = null;
}
if (runtimeTheme != null)
{
DestroyImmediate(runtimeTheme);
runtimeTheme = null;
}
}
public void QueueRebuild()
{
rebuildQueued = true;
}
[ContextMenu("Rebuild Radar Chart")]
public void RebuildNow()
{
QueueRebuild();
TryRebuild();
}
public void SetAxisLabel(int index, string label)
{
if (!TryGetAxis(index, out URadarAxisEntry axis))
{
return;
}
axis.label = string.IsNullOrWhiteSpace(label) ? $"Axis {index + 1}" : label.Trim();
QueueRebuild();
}
public void SetAxisValue(int index, float value)
{
if (!TryGetAxis(index, out URadarAxisEntry axis))
{
return;
}
axis.value = Mathf.Clamp(value, minValue, maxValue);
QueueRebuild();
}
public void SetReferenceAxisValue(int index, float value)
{
if (!TryGetAxis(index, out URadarAxisEntry axis))
{
return;
}
axis.referenceValue = Mathf.Clamp(value, minValue, maxValue);
QueueRebuild();
}
private bool TryGetAxis(int index, out URadarAxisEntry axis)
{
NormalizeAxes();
if (index < 0 || index >= axes.Count)
{
axis = null;
return false;
}
axis = axes[index];
return axis != null;
}
private void TryRebuild()
{
if (chartBridge == null || chartBridge.Profile == null)
{
return;
}
if (sourceProfile == null)
{
sourceProfile = chartBridge.Profile;
}
if (sourceProfile == null)
{
return;
}
EnsureRuntimeProfile();
EnsureBridgePriority();
ConfigureRuntimeProfile();
chartBridge.Refresh();
ApplyRuntimeTheme();
rebuildQueued = false;
}
private void EnsureRuntimeProfile()
{
if (runtimeProfile != null)
{
if (!ReferenceEquals(chartBridge.Profile, runtimeProfile))
{
chartBridge.Profile = runtimeProfile;
}
return;
}
runtimeProfile = Instantiate(sourceProfile);
runtimeProfile.name = sourceProfile.name + "_URadar_Runtime";
runtimeProfile.hideFlags = HideFlags.DontSave;
chartBridge.Profile = runtimeProfile;
}
private void EnsureBridgePriority()
{
if (chartBridge != null && chartBridge.SortOrder < enforcedSortOrder)
{
chartBridge.SortOrder = enforcedSortOrder;
}
}
private void ConfigureRuntimeProfile()
{
NormalizeAxes();
runtimeProfile.coordinateSystem = CoordinateSystemType.Polar2D;
runtimeProfile.EnsureRuntimeData();
runtimeProfile.polarAxes.angleAxis.labels = BuildLabels();
runtimeProfile.polarAxes.angleAxis.visible = true;
runtimeProfile.polarAxes.angleAxis.showLabels = true;
if (runtimeProfile.polarAxes.angleAxis.labelStyle == null)
{
runtimeProfile.polarAxes.angleAxis.labelStyle = new LabelStyleSettings();
}
runtimeProfile.polarAxes.angleAxis.labelStyle.enabled = true;
runtimeProfile.polarAxes.radiusAxis.visible = true;
runtimeProfile.polarAxes.radiusAxis.autoRangeMin = false;
runtimeProfile.polarAxes.radiusAxis.autoRangeMax = false;
runtimeProfile.polarAxes.radiusAxis.minValue = minValue;
runtimeProfile.polarAxes.radiusAxis.maxValue = Mathf.Max(minValue + 1f, maxValue);
runtimeProfile.polarAxes.radiusAxis.splitCount = Mathf.Max(2, splitCount);
if (runtimeProfile.polarAxes.radiusAxis.labelStyle == null)
{
runtimeProfile.polarAxes.radiusAxis.labelStyle = new LabelStyleSettings();
}
runtimeProfile.polarAxes.radiusAxis.labelStyle.enabled = runtimeProfile.polarAxes.radiusAxis.showLabels;
if (overrideGridStyle)
{
runtimeProfile.polarAxes.angleAxis.color = gridLineColor;
runtimeProfile.polarAxes.angleAxis.width = gridLineWidth;
runtimeProfile.polarAxes.radiusAxis.color = gridLineColor;
runtimeProfile.polarAxes.radiusAxis.edgeColor = outerGridLineColor;
runtimeProfile.polarAxes.radiusAxis.width = gridLineWidth;
}
if (overrideLabelStyle)
{
runtimeProfile.polarAxes.angleAxis.labelColor = axisLabelColor;
runtimeProfile.polarAxes.angleAxis.fontSize = axisLabelFontSize;
runtimeProfile.polarAxes.angleAxis.labelOffset = axisLabelOffset;
runtimeProfile.polarAxes.angleAxis.labelStyle.fontSize = axisLabelFontSize;
runtimeProfile.polarAxes.angleAxis.labelStyle.color = axisLabelColor;
runtimeProfile.polarAxes.angleAxis.labelStyle.offset = axisLabelOffset;
runtimeProfile.polarAxes.radiusAxis.labelColor = axisLabelColor;
runtimeProfile.polarAxes.radiusAxis.fontSize = axisLabelFontSize;
runtimeProfile.polarAxes.radiusAxis.labelStyle.fontSize = axisLabelFontSize;
runtimeProfile.polarAxes.radiusAxis.labelStyle.color = axisLabelColor;
}
if (runtimeProfile.series == null)
{
runtimeProfile.series = new List<Serie>();
}
int targetSeriesCount = 1;
while (runtimeProfile.series.Count < targetSeriesCount)
{
runtimeProfile.series.Add(new Serie());
}
while (runtimeProfile.series.Count > targetSeriesCount)
{
runtimeProfile.series.RemoveAt(runtimeProfile.series.Count - 1);
}
ConfigureSeries(runtimeProfile.series[0], mainSeriesName, true);
runtimeProfile.EnsureRuntimeData();
}
private void ApplyRuntimeTheme()
{
if (chartBridge == null || chartBridge.ChartElement == null)
{
return;
}
if (!overrideLabelFont || axisLabelFont == null)
{
chartBridge.ChartElement.Theme = null;
return;
}
if (runtimeTheme == null)
{
runtimeTheme = ScriptableObject.CreateInstance<ChartTheme>();
runtimeTheme.name = "URadar_RuntimeTheme";
runtimeTheme.hideFlags = HideFlags.DontSave;
}
runtimeTheme.primaryFont = axisLabelFont;
runtimeTheme.axisFontSize = axisLabelFontSize;
chartBridge.ChartElement.Theme = runtimeTheme;
}
private void ConfigureSeries(Serie serie, string serieName, bool isMainSeries)
{
if (serie == null)
{
return;
}
serie.name = string.IsNullOrWhiteSpace(serieName) ? (isMainSeries ? "Player" : "Reference") : serieName.Trim();
serie.visible = true;
if (serie.type != SerieType.Radar)
{
serie.SetType(SerieType.Radar);
}
if (!(serie.settings is RadarSettings radarSettings))
{
radarSettings = new RadarSettings();
serie.settings = radarSettings;
}
radarSettings.radar.innerRadius = 0f;
radarSettings.radar.outerRadius = 0f;
radarSettings.radar.plot.padding = radarPlotPadding;
radarSettings.area.show = true;
radarSettings.point.show = true;
if (overrideSeriesStyle)
{
radarSettings.stroke.color = isMainSeries ? mainStrokeColor : referenceStrokeColor;
radarSettings.stroke.width = isMainSeries ? 2.5f : 2f;
radarSettings.area.textureFill.color = isMainSeries ? mainFillColor : referenceFillColor;
radarSettings.point.textureFill.color = isMainSeries ? mainPointColor : referencePointColor;
radarSettings.point.size = isMainSeries ? 8f : 7f;
}
if (serie.labelSettings != null)
{
serie.labelSettings.enabled = false;
}
if (serie.seriesData == null)
{
serie.seriesData = new List<SeriesData>();
}
else
{
serie.seriesData.Clear();
}
for (int i = 0; i < AxisCount; i++)
{
URadarAxisEntry axis = axes[i];
float value = isMainSeries ? axis.value : axis.referenceValue;
serie.seriesData.Add(new SeriesData
{
id = (isMainSeries ? "main_" : "ref_") + i,
name = axis.label,
x = i,
value = Mathf.Clamp(value, minValue, maxValue)
});
}
}
private List<string> BuildLabels()
{
List<string> labels = new List<string>(AxisCount);
for (int i = 0; i < AxisCount; i++)
{
string label = axes[i] != null ? axes[i].label : null;
labels.Add(string.IsNullOrWhiteSpace(label) ? $"Axis {i + 1}" : label.Trim());
}
return labels;
}
private void NormalizeAxes()
{
if (axes == null)
{
axes = new List<URadarAxisEntry>();
}
while (axes.Count < AxisCount)
{
axes.Add(new URadarAxisEntry
{
label = $"Axis {axes.Count + 1}",
value = 50f,
referenceValue = 60f
});
}
if (axes.Count > AxisCount)
{
axes.RemoveRange(AxisCount, axes.Count - AxisCount);
}
for (int i = 0; i < axes.Count; i++)
{
if (axes[i] == null)
{
axes[i] = new URadarAxisEntry
{
label = $"Axis {i + 1}",
value = 50f,
referenceValue = 60f
};
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fbf5b991cb51dd459154eacb5c2a0c0
@@ -0,0 +1,9 @@
using UnityEngine;
[System.Serializable]
public class URadarAxisEntry
{
public string label = "Axis";
[Range(0f, 100f)] public float value = 50f;
[Range(0f, 100f)] public float referenceValue = 60f;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ba52b8221926d44ea14998106dd8bcb
@@ -0,0 +1,579 @@
using System;
using System.Collections;
using System.Collections.Generic;
using EasyChart.UGUI;
using UnityEngine;
public class uBehaviourRaderController : MonoBehaviour
{
private const int AxisActive = 0;
private const int AxisChart = 1;
private const int AxisFormation = 2;
private const int AxisAcc = 3;
private const int AxisRks = 4;
private const int AxisRank = 5;
[Header("Objects")]
public GameObject uRaderObj;
public URadarChartController radarChartController;
public UGUIChartBridge chartBridge;
public RankConfig rankConfig;
[Header("Async")]
public bool reloadOnEnable = true;
public int maxInitAttempts = 20;
public float retryDelaySeconds = 0.15f;
[Header("Radar Baseline")]
[Range(0f, 100f)] public float neutralAxisValue = 50f;
[Range(0f, 100f)] public float referenceAxisValue = 60f;
[Header("Score Targets")]
public float chartScoreTarget = 1000000f;
public float idolScoreTarget = 1000000f;
[Header("Activity Tuning")]
public float activeRecencyHalfLifeDays = 7f;
public float activeGapScaleDays = 2f;
public int activeConfidenceFullCount = 12;
[Header("Performance Tuning")]
public int topSampleCount = 10;
public int performanceConfidenceFullCount = 20;
public float normalizedSpreadPenalty = 0.30f;
private Coroutine loadRoutine;
private bool settingsSubscribed;
private void Start()
{
BeginLoad();
}
private void OnEnable()
{
BindSettingsVisibility();
if (reloadOnEnable && Application.isPlaying)
{
BeginLoad();
}
}
private void OnDestroy()
{
UnsubscribeSettingsVisibility();
if (loadRoutine != null)
{
StopCoroutine(loadRoutine);
loadRoutine = null;
}
}
[ContextMenu("Refresh Behaviour Radar")]
public void RefreshNow()
{
BeginLoad();
}
private void BeginLoad()
{
if (!isActiveAndEnabled)
{
return;
}
if (loadRoutine != null)
{
StopCoroutine(loadRoutine);
}
loadRoutine = StartCoroutine(LoadAndApplyRoutine());
}
private IEnumerator LoadAndApplyRoutine()
{
for (int attempt = 0; attempt < maxInitAttempts; attempt++)
{
ResolveDependencies();
if (radarChartController != null &&
chartBridge != null &&
chartBridge.Profile != null)
{
ApplySettingsVisibility(btmandtopController.CurrentSettingsVisible);
ApplySummary(BuildSummary(RecentPlayHistoryStore.GetRecords()));
loadRoutine = null;
yield break;
}
yield return new WaitForSecondsRealtime(retryDelaySeconds);
}
ApplySummary(BuildEmptySummary());
loadRoutine = null;
}
private void ResolveDependencies()
{
if (uRaderObj == null)
{
uRaderObj = gameObject;
}
if (radarChartController == null)
{
radarChartController = GetComponent<URadarChartController>();
if (radarChartController == null)
{
radarChartController = GetComponentInChildren<URadarChartController>(true);
}
}
if (chartBridge == null && radarChartController != null)
{
chartBridge = radarChartController.chartBridge;
}
if (chartBridge == null)
{
chartBridge = GetComponent<UGUIChartBridge>();
if (chartBridge == null)
{
chartBridge = GetComponentInChildren<UGUIChartBridge>(true);
}
}
if (rankConfig == null)
{
RankConfig[] configs = Resources.LoadAll<RankConfig>(string.Empty);
if (configs != null && configs.Length > 0)
{
rankConfig = configs[0];
}
}
}
private void BindSettingsVisibility()
{
if (settingsSubscribed)
{
return;
}
btmandtopController.GlobalSettingsVisibilityChanged += HandleSettingsVisibilityChanged;
settingsSubscribed = true;
}
private void UnsubscribeSettingsVisibility()
{
if (!settingsSubscribed)
{
settingsSubscribed = false;
return;
}
btmandtopController.GlobalSettingsVisibilityChanged -= HandleSettingsVisibilityChanged;
settingsSubscribed = false;
}
private void HandleSettingsVisibilityChanged(bool visible)
{
ApplySettingsVisibility(visible);
}
private void ApplySettingsVisibility(bool settingsVisible)
{
if (uRaderObj == null)
{
return;
}
if (uRaderObj == gameObject)
{
Debug.LogWarning("[uBehaviourRaderController] uRaderObj should not be the same object as the controller host. Assign the radar display root instead.");
return;
}
uRaderObj.SetActive(!settingsVisible);
}
private void ApplySummary(BehaviourRadarSummary summary)
{
if (radarChartController == null)
{
return;
}
radarChartController.SetAxisLabel(AxisActive, "\u6d3b\u8dc3");
radarChartController.SetAxisLabel(AxisChart, "\u8c31\u9762");
radarChartController.SetAxisLabel(AxisFormation, "\u7f16\u961f");
radarChartController.SetAxisLabel(AxisAcc, "ACC");
radarChartController.SetAxisLabel(AxisRks, "RKS");
radarChartController.SetAxisLabel(AxisRank, "\u8bc4\u7ea7");
radarChartController.SetAxisValue(AxisActive, summary.active);
radarChartController.SetAxisValue(AxisChart, summary.chart);
radarChartController.SetAxisValue(AxisFormation, summary.formation);
radarChartController.SetAxisValue(AxisAcc, summary.acc);
radarChartController.SetAxisValue(AxisRks, summary.rks);
radarChartController.SetAxisValue(AxisRank, summary.rating);
for (int i = 0; i < 6; i++)
{
radarChartController.SetReferenceAxisValue(i, referenceAxisValue);
}
radarChartController.RebuildNow();
}
private BehaviourRadarSummary BuildSummary(IReadOnlyList<RecentPlayRecord> records)
{
BehaviourRadarSummary summary = BuildEmptySummary();
if (records == null || records.Count == 0)
{
return summary;
}
List<RecordSample> samples = BuildSamples(records);
if (samples.Count == 0)
{
return summary;
}
summary.active = ComputeActivityScore(samples);
summary.chart = ComputeChartScore(samples);
summary.formation = ComputeFormationScore(samples);
summary.acc = ComputeAccuracyScore(samples);
summary.rks = ComputeRksScore(samples);
summary.rating = ComputeRatingScore(samples);
return summary;
}
private BehaviourRadarSummary BuildEmptySummary()
{
return new BehaviourRadarSummary
{
active = neutralAxisValue,
chart = neutralAxisValue,
formation = neutralAxisValue,
acc = neutralAxisValue,
rks = neutralAxisValue,
rating = neutralAxisValue
};
}
private List<RecordSample> BuildSamples(IReadOnlyList<RecentPlayRecord> records)
{
List<RecordSample> samples = new List<RecordSample>(records.Count);
DateTime now = DateTime.Now;
for (int i = 0; i < records.Count; i++)
{
RecentPlayRecord record = records[i];
if (record == null)
{
continue;
}
RecordSample sample = new RecordSample();
sample.weight = Mathf.Exp(-i / 24f);
sample.playedAtLocal = TryParseLocalTime(record, now, out sample.hasPlayableTime);
sample.accuracy = Mathf.Clamp(record.accuracy, 0f, 100f);
sample.rks = Mathf.Clamp(record.srks, 0f, 100f);
if (record.hasScoreBreakdown)
{
sample.chartScore = Mathf.Max(0, record.chartScore);
sample.idolScore = Mathf.Max(0, record.idolScore);
sample.hasScoreBreakdown = true;
}
if (record.hasRankMarker && record.rankTierCount > 0)
{
sample.rankTierCount = record.rankTierCount;
sample.rankIndex = Mathf.Clamp(record.rankIndex, 0, sample.rankTierCount);
sample.hasRankMarker = true;
}
else if (record.scoreReadable)
{
sample.rankTierCount = GetRankTierCount();
sample.rankIndex = CalculateRankIndex(record.totalScore);
sample.hasRankMarker = sample.rankTierCount > 0;
}
samples.Add(sample);
}
return samples;
}
private DateTime TryParseLocalTime(RecentPlayRecord record, DateTime now, out bool hasValue)
{
hasValue = false;
if (record != null && !string.IsNullOrWhiteSpace(record.playedAtUtc))
{
if (DateTime.TryParse(record.playedAtUtc, null, System.Globalization.DateTimeStyles.RoundtripKind, out DateTime utcTime))
{
hasValue = true;
return utcTime.ToLocalTime();
}
}
if (record != null && !string.IsNullOrWhiteSpace(record.playedAt))
{
if (DateTime.TryParseExact(
record.playedAt,
"MM-dd, HH:mm",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None,
out DateTime partial))
{
DateTime candidate = new DateTime(now.Year, partial.Month, partial.Day, partial.Hour, partial.Minute, 0);
if (candidate > now.AddDays(1))
{
candidate = candidate.AddYears(-1);
}
hasValue = true;
return candidate;
}
}
return now;
}
private float ComputeActivityScore(List<RecordSample> samples)
{
List<DateTime> playedDays = new List<DateTime>();
for (int i = 0; i < samples.Count; i++)
{
if (!samples[i].hasPlayableTime)
{
continue;
}
playedDays.Add(samples[i].playedAtLocal.Date);
}
if (playedDays.Count == 0)
{
return neutralAxisValue;
}
playedDays.Sort((a, b) => b.CompareTo(a));
int uniqueDays = 0;
DateTime previousDay = DateTime.MinValue;
float totalGapDays = 0f;
int gapCount = 0;
DateTime newest = playedDays[0];
DateTime oldest = playedDays[playedDays.Count - 1];
for (int i = 0; i < playedDays.Count; i++)
{
DateTime day = playedDays[i];
if (day == previousDay)
{
continue;
}
if (previousDay != DateTime.MinValue)
{
totalGapDays += Mathf.Max(0f, (float)(previousDay - day).TotalDays);
gapCount++;
}
previousDay = day;
uniqueDays++;
}
float spanDays = Mathf.Max(1f, (float)(newest - oldest).TotalDays + 1f);
float density = Mathf.Clamp01(uniqueDays / spanDays);
float averageGap = gapCount > 0 ? totalGapDays / gapCount : 0f;
float cadence = Mathf.Exp(-averageGap / Mathf.Max(0.25f, activeGapScaleDays));
float recencyDays = Mathf.Max(0f, (float)(DateTime.Now - newest).TotalDays);
float recency = Mathf.Exp(-recencyDays / Mathf.Max(0.25f, activeRecencyHalfLifeDays));
float raw = (density * 0.5f + cadence * 0.3f + recency * 0.2f) * 100f;
return BlendWithNeutral(raw, uniqueDays, activeConfidenceFullCount);
}
private float ComputeChartScore(List<RecordSample> samples)
{
return ComputeNormalizedPerformanceScore(
samples,
sample => sample.hasScoreBreakdown,
sample => Mathf.Clamp01(sample.chartScore / Mathf.Max(1f, chartScoreTarget)));
}
private float ComputeFormationScore(List<RecordSample> samples)
{
return ComputeNormalizedPerformanceScore(
samples,
sample => sample.hasScoreBreakdown,
sample => Mathf.Clamp01(sample.idolScore / Mathf.Max(1f, idolScoreTarget)));
}
private float ComputeAccuracyScore(List<RecordSample> samples)
{
return ComputeNormalizedPerformanceScore(
samples,
sample => sample.accuracy > 0f,
sample => Mathf.Clamp01(sample.accuracy / 100f));
}
private float ComputeRksScore(List<RecordSample> samples)
{
return ComputeNormalizedPerformanceScore(
samples,
sample => sample.rks > 0f,
sample => Mathf.Clamp01(sample.rks / 100f));
}
private float ComputeRatingScore(List<RecordSample> samples)
{
int tierCount = GetRankTierCount();
if (tierCount <= 0)
{
return neutralAxisValue;
}
return ComputeNormalizedPerformanceScore(
samples,
sample => sample.hasRankMarker && sample.rankTierCount > 0,
sample =>
{
int count = Mathf.Max(1, sample.rankTierCount);
return Mathf.Clamp01(sample.rankIndex / (float)count);
});
}
private float ComputeNormalizedPerformanceScore(
List<RecordSample> samples,
Func<RecordSample, bool> predicate,
Func<RecordSample, float> selector)
{
List<float> values = new List<float>();
float weightedSum = 0f;
float totalWeight = 0f;
for (int i = 0; i < samples.Count; i++)
{
RecordSample sample = samples[i];
if (!predicate(sample))
{
continue;
}
float value = Mathf.Clamp01(selector(sample));
values.Add(value);
weightedSum += value * sample.weight;
totalWeight += sample.weight;
}
if (values.Count == 0 || totalWeight <= 0f)
{
return neutralAxisValue;
}
float weightedAverage = weightedSum / totalWeight;
List<float> sorted = new List<float>(values);
sorted.Sort((a, b) => b.CompareTo(a));
int topCount = Mathf.Min(topSampleCount, sorted.Count);
float topAverage = 0f;
for (int i = 0; i < topCount; i++)
{
topAverage += sorted[i];
}
topAverage /= Mathf.Max(1, topCount);
float variance = 0f;
for (int i = 0; i < values.Count; i++)
{
float delta = values[i] - weightedAverage;
variance += delta * delta;
}
variance /= Mathf.Max(1, values.Count);
float spread = Mathf.Sqrt(variance);
float stability = 1f - Mathf.Clamp01(spread / Mathf.Max(0.05f, normalizedSpreadPenalty));
float raw = (weightedAverage * 0.55f + topAverage * 0.25f + stability * 0.20f) * 100f;
return BlendWithNeutral(raw, values.Count, performanceConfidenceFullCount);
}
private float BlendWithNeutral(float raw, int sampleCount, int fullConfidenceCount)
{
float confidence = Mathf.Clamp01(sampleCount / Mathf.Max(1f, fullConfidenceCount));
return Mathf.Lerp(neutralAxisValue, Mathf.Clamp(raw, 0f, 100f), confidence);
}
private int GetRankTierCount()
{
if (rankConfig == null || rankConfig.thresholds == null)
{
return 0;
}
return Mathf.Max(0, rankConfig.thresholds.Count);
}
private int CalculateRankIndex(int totalScore)
{
if (rankConfig == null || rankConfig.thresholds == null || rankConfig.thresholds.Count == 0 || totalScore <= 0)
{
return 0;
}
int bestLevel = 0;
float highestMatchingPercent = -1f;
int thresholdCount = rankConfig.thresholds.Count;
for (int i = 0; i < thresholdCount; i++)
{
RankThreshold threshold = rankConfig.thresholds[i];
if (threshold == null)
{
continue;
}
float requiredScore = rankConfig.baseScore * threshold.thresholdPercent;
if (totalScore < requiredScore || threshold.thresholdPercent <= highestMatchingPercent)
{
continue;
}
highestMatchingPercent = threshold.thresholdPercent;
bestLevel = thresholdCount - i;
}
return bestLevel;
}
[Serializable]
private struct BehaviourRadarSummary
{
public float active;
public float chart;
public float formation;
public float acc;
public float rks;
public float rating;
}
private struct RecordSample
{
public float weight;
public bool hasPlayableTime;
public DateTime playedAtLocal;
public int chartScore;
public int idolScore;
public bool hasScoreBreakdown;
public float accuracy;
public float rks;
public int rankIndex;
public int rankTierCount;
public bool hasRankMarker;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0343fabebe70383449bc7c9e6af02d7f