拼ui 一些业务逻辑x实现
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user