Files
DeviantMobile-Rohan/Assets/Scripts/CashSystem/UI/TeamHUDPanel.cs

369 lines
14 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
/// <summary>
/// TeamHUDPanel — FRAG-style circular portrait HUD showing team health and character switching.
///
/// SETUP (Inspector):
/// 1. Create your HUD layout manually — place circular Image slots wherever you want.
/// 2. Assign them into the lists below:
/// - characterPortraits[] — Image where the character icon/portrait sprite will go
/// - healthDialImages[] — Image used as a radial-fill health ring around each portrait
/// - dialBackgrounds[] — (optional) background ring images behind the health dials
/// - nameLabels[] — (optional) TMP text for character names
/// 3. The script fills sprites and updates health dials every frame.
/// 4. Click/tap a portrait to switch control to that character.
///
/// Uses CanvasGroup to hide initially — stays alive for Initialize() call.
/// </summary>
public class TeamHUDPanel : MonoBehaviour
{
[Header("Character Portraits (assign in order: index 0 = team slot 0, etc.)")]
[Tooltip("Image components where character icon sprites will be placed")]
[SerializeField] private List<Image> characterPortraits = new List<Image>();
[Header("Health Dial Rings (same index order as portraits)")]
[Tooltip("Image components used as radial-fill health rings — set to Filled/Radial360 at runtime")]
[SerializeField] private List<Image> healthDialImages = new List<Image>();
[Header("Dial Backgrounds (optional, same index order)")]
[Tooltip("Background ring images behind each health dial")]
[SerializeField] private List<Image> dialBackgrounds = new List<Image>();
[Header("Name Labels (optional, same index order)")]
[SerializeField] private List<TextMeshProUGUI> nameLabels = new List<TextMeshProUGUI>();
[Header("Dead Overlay (optional, same index order)")]
[Tooltip("GameObjects to show when a character is dead (e.g. a dark overlay with X)")]
[SerializeField] private List<GameObject> deadOverlays = new List<GameObject>();
[Header("Highlight Border (optional, same index order)")]
[Tooltip("Image used as a highlight border — only visible on the active character")]
[SerializeField] private List<Image> highlightBorders = new List<Image>();
[Header("Dial Sprite")]
[Tooltip("Sprite for the health dial ring. If empty, tries to copy from ImportUIPanel's ProgressDial.")]
[SerializeField] private Sprite dialSprite;
[Header("Audio")]
[SerializeField] private AudioClip switchSFX;
[SerializeField] private AudioClip buttonHoverSFX;
[SerializeField] private float sfxVolume = 1f;
[Header("Colors")]
[SerializeField] private Color healthFullColor = new Color(0.30f, 0.87f, 0.30f, 1f);
[SerializeField] private Color healthLowColor = new Color(0.96f, 0.26f, 0.21f, 1f);
[SerializeField] private Color highlightColor = new Color(1f, 0.84f, 0f, 1f);
[SerializeField] private Color defaultBorderColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
// runtime
private List<GameObject> _characters = new List<GameObject>();
private List<HealthNew> _healthComps = new List<HealthNew>();
private int _currentHighlight = -1;
private CanvasGroup _canvasGroup;
private AudioSource _audio;
private static TeamHUDPanel _instance;
public static TeamHUDPanel Instance => _instance;
private void Awake()
{
_instance = this;
_canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null) _canvasGroup = gameObject.AddComponent<CanvasGroup>();
_audio = GetComponent<AudioSource>();
if (_audio == null) _audio = gameObject.AddComponent<AudioSource>();
_audio.playOnAwake = false;
_audio.spatialBlend = 0f;
// Hide via CanvasGroup until Initialize is called
HideImmediate();
// Try to find dial sprite from ImportUIPanel if not assigned
if (dialSprite == null)
{
var importPanel = GameObject.Find("ImportUIPanel");
if (importPanel != null)
{
var dial = importPanel.transform.Find("ProgressDial");
if (dial != null)
{
var img = dial.GetComponent<Image>();
if (img != null) dialSprite = img.sprite;
}
}
}
}
private void OnEnable()
{
GameEvents.OnCharacterSwitched += OnCharacterSwitched;
}
private void OnDisable()
{
GameEvents.OnCharacterSwitched -= OnCharacterSwitched;
}
// ─── Public API ──────────────────────────────────────────
/// <summary>
/// Initialize the HUD with the player team characters.
/// Call after characters are spawned (from CashSystemManager.SpawnTeams).
/// </summary>
public void Initialize(List<GameObject> playerTeamCharacters, int currentControlledIndex)
{
DevLog.Log($"[TeamHUDPanel] Initialize with {playerTeamCharacters?.Count ?? 0} characters, active={currentControlledIndex}");
_characters.Clear();
_healthComps.Clear();
if (playerTeamCharacters == null || playerTeamCharacters.Count == 0) return;
int count = Mathf.Min(playerTeamCharacters.Count, characterPortraits.Count);
for (int i = 0; i < count; i++)
{
GameObject character = playerTeamCharacters[i];
_characters.Add(character);
_healthComps.Add(character != null ? character.GetComponent<HealthNew>() : null);
// Set portrait sprite
if (i < characterPortraits.Count && characterPortraits[i] != null && character != null)
{
Sprite icon = GetCharacterIcon(character);
if (icon != null)
{
characterPortraits[i].sprite = icon;
characterPortraits[i].color = Color.white;
characterPortraits[i].preserveAspect = true;
}
// Add click handler
int capturedIndex = i;
Button btn = characterPortraits[i].GetComponent<Button>();
if (btn == null) btn = characterPortraits[i].gameObject.AddComponent<Button>();
btn.transition = Selectable.Transition.None;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => OnSlotClicked(capturedIndex));
}
// Configure health dial as radial fill
if (i < healthDialImages.Count && healthDialImages[i] != null)
{
Image dial = healthDialImages[i];
if (dialSprite != null) dial.sprite = dialSprite;
dial.type = Image.Type.Filled;
dial.fillMethod = Image.FillMethod.Radial360;
dial.fillOrigin = (int)Image.Origin360.Top;
dial.fillClockwise = true;
dial.fillAmount = 1f;
dial.color = healthFullColor;
}
// Set name label
if (i < nameLabels.Count && nameLabels[i] != null && character != null)
{
nameLabels[i].text = GetCharacterName(character);
}
// Hide dead overlay
if (i < deadOverlays.Count && deadOverlays[i] != null)
{
deadOverlays[i].SetActive(false);
}
// Default border
if (i < highlightBorders.Count && highlightBorders[i] != null)
{
highlightBorders[i].color = defaultBorderColor;
}
}
// Hide any extra slots beyond team size
for (int i = count; i < characterPortraits.Count; i++)
{
if (characterPortraits[i] != null) characterPortraits[i].gameObject.SetActive(false);
if (i < healthDialImages.Count && healthDialImages[i] != null) healthDialImages[i].gameObject.SetActive(false);
if (i < dialBackgrounds.Count && dialBackgrounds[i] != null) dialBackgrounds[i].gameObject.SetActive(false);
if (i < nameLabels.Count && nameLabels[i] != null) nameLabels[i].gameObject.SetActive(false);
if (i < deadOverlays.Count && deadOverlays[i] != null) deadOverlays[i].SetActive(false);
if (i < highlightBorders.Count && highlightBorders[i] != null) highlightBorders[i].gameObject.SetActive(false);
}
SetHighlight(currentControlledIndex);
// Show panel
_canvasGroup.alpha = 1f;
_canvasGroup.blocksRaycasts = true;
_canvasGroup.interactable = true;
}
/// <summary>Update health dials + keyboard shortcuts. Health polling throttled to ~10Hz.</summary>
private void Update()
{
// Keyboard shortcuts: 1, 2, 3 to switch characters (always responsive)
if (_characters.Count > 0 && _canvasGroup.alpha > 0f)
{
if (Input.GetKeyDown(KeyCode.Alpha1) && _characters.Count > 0) OnSlotClicked(0);
if (Input.GetKeyDown(KeyCode.Alpha2) && _characters.Count > 1) OnSlotClicked(1);
if (Input.GetKeyDown(KeyCode.Alpha3) && _characters.Count > 2) OnSlotClicked(2);
}
// Throttle health polling to every 6th frame (~10Hz at 60fps)
// Health bars update smoothly enough at 10Hz — indistinguishable from 60Hz
if (Time.frameCount % 6 != 0) return;
for (int i = 0; i < _healthComps.Count; i++)
{
HealthNew h = _healthComps[i];
if (h == null) continue;
// Health ring fill
float maxHP = h.maxHealth > 0 ? h.maxHealth : 100f;
float pct = Mathf.Clamp01((float)h.currentHealth / maxHP);
if (i < healthDialImages.Count && healthDialImages[i] != null)
{
healthDialImages[i].fillAmount = pct;
healthDialImages[i].color = Color.Lerp(healthLowColor, healthFullColor, pct);
}
// Dead overlay
bool dead = h.isDead;
if (i < deadOverlays.Count && deadOverlays[i] != null)
{
if (deadOverlays[i].activeSelf != dead)
deadOverlays[i].SetActive(dead);
}
// Disable click on dead characters
if (i < characterPortraits.Count && characterPortraits[i] != null)
{
Button btn = characterPortraits[i].GetComponent<Button>();
if (btn != null) btn.interactable = !dead;
}
}
}
// ─── Events ──────────────────────────────────────────────
private void OnCharacterSwitched(GameObject oldChar, GameObject newChar)
{
if (newChar == null) return;
for (int i = 0; i < _characters.Count; i++)
{
if (_characters[i] == newChar)
{
SetHighlight(i);
return;
}
}
}
private void SetHighlight(int index)
{
// Remove old highlight
if (_currentHighlight >= 0 && _currentHighlight < highlightBorders.Count
&& highlightBorders[_currentHighlight] != null)
{
highlightBorders[_currentHighlight].color = defaultBorderColor;
}
if (_currentHighlight >= 0 && _currentHighlight < characterPortraits.Count
&& characterPortraits[_currentHighlight] != null)
{
characterPortraits[_currentHighlight].transform.localScale = Vector3.one;
}
_currentHighlight = index;
// Apply new highlight
if (index >= 0 && index < highlightBorders.Count && highlightBorders[index] != null)
{
highlightBorders[index].color = highlightColor;
}
if (index >= 0 && index < characterPortraits.Count && characterPortraits[index] != null)
{
characterPortraits[index].transform.localScale = Vector3.one * 1.1f;
}
}
private void OnSlotClicked(int index)
{
if (index < 0 || index >= _characters.Count) return;
if (index < _healthComps.Count && _healthComps[index] != null && _healthComps[index].isDead) return;
if (index == _currentHighlight) return;
// Play switch SFX
if (switchSFX != null && _audio != null)
_audio.PlayOneShot(switchSFX, sfxVolume);
CashSystemManager mgr = CashSystemManager.Instance;
if (mgr != null)
{
mgr.SwitchToCharacterAtIndex(index);
}
}
private void HideImmediate()
{
_canvasGroup.alpha = 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
}
// ─── Utility ─────────────────────────────────────────────
private Sprite GetCharacterIcon(GameObject character)
{
if (character == null) return null;
var ai = character.GetComponent<CharacterAIController>();
if (ai != null)
{
var field = typeof(CharacterAIController)
.GetField("characterStats",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
if (field != null)
{
CharacterStats stats = field.GetValue(ai) as CharacterStats;
if (stats != null)
{
if (stats.characterIcon != null) return stats.characterIcon;
if (stats.characterPortrait != null) return stats.characterPortrait;
}
}
}
string cleanName = character.name.Replace("(Clone)", "").Trim();
return Resources.Load<Sprite>($"CharacterPortraits/{cleanName}");
}
private string GetCharacterName(GameObject character)
{
if (character == null) return "?";
var ai = character.GetComponent<CharacterAIController>();
if (ai != null)
{
var field = typeof(CharacterAIController)
.GetField("characterStats",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
if (field != null)
{
CharacterStats stats = field.GetValue(ai) as CharacterStats;
if (stats != null && !string.IsNullOrEmpty(stats.displayName))
return stats.displayName;
}
}
return character.name.Replace("(Clone)", "").Trim();
}
}