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