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; public bool hideWhenOverlayVisible = true; [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 overlaySubscribed; private void Start() { BeginLoad(); } private void OnEnable() { BindOverlayVisibility(); ApplyOverlayVisibility(btmandtopController.CurrentOverlayPanelsVisible); if (reloadOnEnable && Application.isPlaying) { BeginLoad(); } } private void OnDestroy() { UnsubscribeOverlayVisibility(); 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) { ApplyOverlayVisibility(btmandtopController.CurrentOverlayPanelsVisible); 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(); if (radarChartController == null) { radarChartController = GetComponentInChildren(true); } } if (chartBridge == null && radarChartController != null) { chartBridge = radarChartController.chartBridge; } if (chartBridge == null) { chartBridge = GetComponent(); if (chartBridge == null) { chartBridge = GetComponentInChildren(true); } } if (rankConfig == null) { RankConfig[] configs = Resources.LoadAll(string.Empty); if (configs != null && configs.Length > 0) { rankConfig = configs[0]; } } HideLegacyRadarSiblings(); } private void HideLegacyRadarSiblings() { if (uRaderObj == null) { return; } Transform host = transform; Transform target = uRaderObj.transform; for (int i = 0; i < host.childCount; i++) { Transform child = host.GetChild(i); if (child == null) { continue; } child.gameObject.SetActive(child == target); } bool shouldShowRadar = !hideWhenOverlayVisible || !btmandtopController.CurrentOverlayPanelsVisible; if (shouldShowRadar && !uRaderObj.activeSelf) { uRaderObj.SetActive(true); } } private void BindOverlayVisibility() { if (!hideWhenOverlayVisible) { return; } if (overlaySubscribed) { return; } btmandtopController.GlobalOverlayPanelVisibilityChanged += HandleOverlayPanelsVisibilityChanged; overlaySubscribed = true; } private void UnsubscribeOverlayVisibility() { if (!overlaySubscribed) { overlaySubscribed = false; return; } btmandtopController.GlobalOverlayPanelVisibilityChanged -= HandleOverlayPanelsVisibilityChanged; overlaySubscribed = false; } private void HandleOverlayPanelsVisibilityChanged(bool visible) { ApplyOverlayVisibility(visible); } private void ApplyOverlayVisibility(bool overlayVisible) { 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(!hideWhenOverlayVisible || !overlayVisible); } 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 records) { BehaviourRadarSummary summary = BuildEmptySummary(); summary.rks = PlayerRksService.GetNormalizedOverallRks100(); if (records == null || records.Count == 0) { return summary; } List 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.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 BuildSamples(IReadOnlyList records) { List samples = new List(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 samples) { List playedDays = new List(); 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 samples) { return ComputeNormalizedPerformanceScore( samples, sample => sample.hasScoreBreakdown, sample => Mathf.Clamp01(sample.chartScore / Mathf.Max(1f, chartScoreTarget))); } private float ComputeFormationScore(List samples) { return ComputeNormalizedPerformanceScore( samples, sample => sample.hasScoreBreakdown, sample => Mathf.Clamp01(sample.idolScore / Mathf.Max(1f, idolScoreTarget))); } private float ComputeAccuracyScore(List samples) { return ComputeNormalizedPerformanceScore( samples, sample => sample.accuracy > 0f, sample => Mathf.Clamp01(sample.accuracy / 100f)); } private float ComputeRksScore(List samples) { return ComputeNormalizedPerformanceScore( samples, sample => sample.rks > 0f, sample => Mathf.Clamp01(sample.rks / 100f)); } private float ComputeRatingScore(List 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 samples, Func predicate, Func selector) { List values = new List(); 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 sorted = new List(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; } }