Files
bansonic_beta_main/Assets/playerInfoDisplay/uRader/uBehaviourRaderController.cs
T

617 lines
18 KiB
C#

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<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];
}
}
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<RecentPlayRecord> records)
{
BehaviourRadarSummary summary = BuildEmptySummary();
summary.rks = PlayerRksService.GetNormalizedOverallRks100();
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.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;
}
}