using GameFramework.DataTable; using GeometryTD.DataTable; using GeometryTD.Definition; using UnityEngine; namespace GeometryTD.CustomUtility { public static class IconColorGenerator { private const int StatCount = 5; private const float MinSaturation = 0.4f; private const float MinValue = 0.6f; private const float TinyValue = 0.0001f; private static readonly float[] HueMap = { 0f, 30f, 200f, 120f, 270f }; private static StatRange _attackDamageRange = new StatRange(1f, 300f); private static StatRange _damageRandomRateRange = new StatRange(0f, 0.3f); private static StatRange _rotateSpeedRange = new StatRange(10f, 240f); private static StatRange _attackRangeRange = new StatRange(2f, 8f); private static StatRange _attackSpeedRange = new StatRange(0.5f, 4f); private static bool s_HasMuzzleRange; private static bool s_HasBearingRange; private static bool s_HasBaseRange; public static Color GenerateForComponent(TowerCompItemData item) { if (item == null) { return Color.white; } float attackDamage = 0f; float damageRandomRate = 0f; float rotateSpeed = 0f; float attackRange = 0f; float attackSpeed = 0f; if (item is MuzzleCompItemData muzzle) { attackDamage = ResolveArrayValue(muzzle.AttackDamage); damageRandomRate = Mathf.Max(0f, muzzle.DamageRandomRate); } else if (item is BearingCompItemData bearing) { rotateSpeed = ResolveArrayValue(bearing.RotateSpeed); attackRange = ResolveArrayValue(bearing.AttackRange); } else if (item is BaseCompItemData baseComp) { attackSpeed = ResolveArrayValue(baseComp.AttackSpeed); } return GenerateColor( attackDamage, damageRandomRate, rotateSpeed, attackRange, attackSpeed, item.SlotType); } public static Color GenerateForTower(TowerItemData tower) { if (tower?.Stats == null) { return Color.white; } TowerStatsData stats = tower.Stats; return GenerateColor( ResolveArrayValue(stats.AttackDamage), stats.DamageRandomRate, ResolveArrayValue(stats.RotateSpeed), ResolveArrayValue(stats.AttackRange), ResolveArrayValue(stats.AttackSpeed), TowerCompSlotType.None); } private static Color GenerateColor( float attackDamage, float damageRandomRate, float rotateSpeed, float attackRange, float attackSpeed, TowerCompSlotType slotType) { TryRefreshRangesFromDataTables(); float[] normalizedStats = new float[StatCount]; normalizedStats[0] = Normalize(attackDamage, _attackDamageRange); normalizedStats[1] = Normalize(damageRandomRate, _damageRandomRateRange); normalizedStats[2] = Normalize(rotateSpeed, _rotateSpeedRange); normalizedStats[3] = Normalize(attackRange, _attackRangeRange); normalizedStats[4] = Normalize(attackSpeed, _attackSpeedRange); FindTopTwoIndexes(normalizedStats, out int primaryIndex, out int secondaryIndex); float denominator = normalizedStats[primaryIndex] + normalizedStats[secondaryIndex] + TinyValue; float blendWeight = normalizedStats[secondaryIndex] / denominator; float hue = LerpHue(HueMap[primaryIndex], HueMap[secondaryIndex], blendWeight); hue = NormalizeHue(hue + ResolveSlotHueOffset(slotType)); float purity = normalizedStats[primaryIndex] - normalizedStats[secondaryIndex]; float saturation = Mathf.Lerp(0.45f, 0.9f, Mathf.Clamp01(purity * 1.5f)); saturation = Mathf.Max(saturation, MinSaturation); float intensity = 0f; for (int i = 0; i < normalizedStats.Length; i++) { intensity += normalizedStats[i]; } float value = Mathf.Lerp(0.55f, 0.95f, intensity / StatCount); value = Mathf.Max(value, MinValue); Color color = Color.HSVToRGB(hue / 360f, saturation, value); color.a = 1f; return color; } private static void TryRefreshRangesFromDataTables() { if (GameEntry.DataTable == null) { return; } if (!s_HasMuzzleRange && TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange)) { _attackDamageRange = attackDamageRange; _damageRandomRateRange = damageRandomRateRange; s_HasMuzzleRange = true; } if (!s_HasBearingRange && TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange)) { _rotateSpeedRange = rotateSpeedRange; _attackRangeRange = attackRangeRange; s_HasBearingRange = true; } if (!s_HasBaseRange && TryResolveBaseRange(out StatRange attackSpeedRange)) { _attackSpeedRange = attackSpeedRange; s_HasBaseRange = true; } } private static bool TryResolveMuzzleRange(out StatRange attackDamageRange, out StatRange damageRandomRateRange) { attackDamageRange = _attackDamageRange; damageRandomRateRange = _damageRandomRateRange; IDataTable dataTable = GameEntry.DataTable.GetDataTable(); if (dataTable == null || dataTable.Count <= 0) { return false; } DRMuzzleComp[] rows = dataTable.GetAllDataRows(); if (rows == null || rows.Length <= 0) { return false; } bool hasAttackDamage = false; bool hasDamageRandomRate = false; float minAttackDamage = float.MaxValue; float maxAttackDamage = float.MinValue; float minDamageRandomRate = float.MaxValue; float maxDamageRandomRate = float.MinValue; for (int i = 0; i < rows.Length; i++) { DRMuzzleComp row = rows[i]; if (row == null) { continue; } IncludeValues(row.AttackDamage, ref minAttackDamage, ref maxAttackDamage, ref hasAttackDamage); IncludeValue(Mathf.Max(0f, row.DamageRandomRate), ref minDamageRandomRate, ref maxDamageRandomRate, ref hasDamageRandomRate); } if (!hasAttackDamage || !hasDamageRandomRate) { return false; } attackDamageRange = BuildRange(minAttackDamage, maxAttackDamage, _attackDamageRange); damageRandomRateRange = BuildRange(minDamageRandomRate, maxDamageRandomRate, _damageRandomRateRange); return true; } private static bool TryResolveBearingRange(out StatRange rotateSpeedRange, out StatRange attackRangeRange) { rotateSpeedRange = _rotateSpeedRange; attackRangeRange = _attackRangeRange; IDataTable dataTable = GameEntry.DataTable.GetDataTable(); if (dataTable == null || dataTable.Count <= 0) { return false; } DRBearingComp[] rows = dataTable.GetAllDataRows(); if (rows == null || rows.Length <= 0) { return false; } bool hasRotateSpeed = false; bool hasAttackRange = false; float minRotateSpeed = float.MaxValue; float maxRotateSpeed = float.MinValue; float minAttackRange = float.MaxValue; float maxAttackRange = float.MinValue; for (int i = 0; i < rows.Length; i++) { DRBearingComp row = rows[i]; if (row == null) { continue; } IncludeValues(row.RotateSpeed, ref minRotateSpeed, ref maxRotateSpeed, ref hasRotateSpeed); IncludeValues(row.AttackRange, ref minAttackRange, ref maxAttackRange, ref hasAttackRange); } if (!hasRotateSpeed || !hasAttackRange) { return false; } rotateSpeedRange = BuildRange(minRotateSpeed, maxRotateSpeed, _rotateSpeedRange); attackRangeRange = BuildRange(minAttackRange, maxAttackRange, _attackRangeRange); return true; } private static bool TryResolveBaseRange(out StatRange attackSpeedRange) { attackSpeedRange = _attackSpeedRange; IDataTable dataTable = GameEntry.DataTable.GetDataTable(); if (dataTable == null || dataTable.Count <= 0) { return false; } DRBaseComp[] rows = dataTable.GetAllDataRows(); if (rows == null || rows.Length <= 0) { return false; } bool hasAttackSpeed = false; float minAttackSpeed = float.MaxValue; float maxAttackSpeed = float.MinValue; for (int i = 0; i < rows.Length; i++) { DRBaseComp row = rows[i]; if (row == null) { continue; } IncludeValues(row.AttackSpeed, ref minAttackSpeed, ref maxAttackSpeed, ref hasAttackSpeed); } if (!hasAttackSpeed) { return false; } attackSpeedRange = BuildRange(minAttackSpeed, maxAttackSpeed, _attackSpeedRange); return true; } private static void FindTopTwoIndexes(float[] values, out int primaryIndex, out int secondaryIndex) { primaryIndex = 0; secondaryIndex = 1; if (values[secondaryIndex] > values[primaryIndex]) { (primaryIndex, secondaryIndex) = (secondaryIndex, primaryIndex); } for (int i = 2; i < values.Length; i++) { float value = values[i]; if (value > values[primaryIndex]) { secondaryIndex = primaryIndex; primaryIndex = i; } else if (value > values[secondaryIndex]) { secondaryIndex = i; } } } private static float ResolveSlotHueOffset(TowerCompSlotType slotType) { return slotType switch { TowerCompSlotType.Bearing => 15f, TowerCompSlotType.Base => -15f, _ => 0f }; } private static float ResolveArrayValue(int[] values) { if (values == null || values.Length <= 0) { return 0f; } return Mathf.Max(0f, values[0]); } private static float ResolveArrayValue(float[] values) { if (values == null || values.Length <= 0) { return 0f; } return Mathf.Max(0f, values[0]); } private static void IncludeValues(int[] values, ref float min, ref float max, ref bool hasValue) { if (values == null || values.Length <= 0) { return; } for (int i = 0; i < values.Length; i++) { IncludeValue(Mathf.Max(0f, values[i]), ref min, ref max, ref hasValue); } } private static void IncludeValues(float[] values, ref float min, ref float max, ref bool hasValue) { if (values == null || values.Length <= 0) { return; } for (int i = 0; i < values.Length; i++) { IncludeValue(Mathf.Max(0f, values[i]), ref min, ref max, ref hasValue); } } private static void IncludeValue(float value, ref float min, ref float max, ref bool hasValue) { if (float.IsNaN(value) || float.IsInfinity(value)) { return; } if (!hasValue) { min = value; max = value; hasValue = true; return; } if (value < min) { min = value; } if (value > max) { max = value; } } private static float Normalize(float value, StatRange range) { float denominator = Mathf.Max(TinyValue, range.Max - range.Min); return Mathf.Clamp01((value - range.Min) / denominator); } private static StatRange BuildRange(float min, float max, StatRange fallback) { if (float.IsNaN(min) || float.IsInfinity(min) || float.IsNaN(max) || float.IsInfinity(max)) { return fallback; } if (max - min < TinyValue) { return fallback; } return new StatRange(min, max); } private static float LerpHue(float fromDegree, float toDegree, float t) { float delta = Mathf.DeltaAngle(fromDegree, toDegree); return fromDegree + delta * Mathf.Clamp01(t); } private static float NormalizeHue(float hueDegree) { float normalized = hueDegree % 360f; if (normalized < 0f) { normalized += 360f; } return normalized; } private readonly struct StatRange { public StatRange(float min, float max) { Min = min; Max = max; } public float Min { get; } public float Max { get; } } } }