using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using System.Collections;
using BulletHellTemplate.Core.Events;
using Cysharp.Threading.Tasks;
using System.Threading;
using System;
using System.Linq;
#if FUSION2
using Fusion;
#endif
namespace BulletHellTemplate
{
///
/// Manages the user interface during gameplay, including player characterStatsComponent, skills, and input handling for both mobile and PC platforms.
///
public class UIGameplay : MonoBehaviour
{
#region Mobile Controls
[Header("Mobile Controls")]
[Tooltip("Reference to the Joystick for mobile movement")]
public Joystick joystick; // Reference to the Joystick for mobile movement
[Header("Mobile Skill Joysticks")]
[Tooltip("Array to hold skill joysticks for mobile")]
public SkillJoystick[] skillJoysticks; // Array to hold skill joysticks for mobile
#endregion
#region PC Controls
[Header("PC Controls")]
[Tooltip("Reference to the PC Input Controller")]
public PCInputController pcInputController; // Reference to the PC Input Controller
[Tooltip("Reference to the PC Skill Controller")]
public PCSkillController pcSkillController; // Reference to the PC Skill Controller
#endregion
[Header("Skill Icons")]
[Tooltip("Array to hold skill images")]
public SkillImage[] skillImages;
[Header("Final Boss")]
[Tooltip("Final boss message GameObject")]
public GameObject finalBossMessage;
private MonsterEntity finalBossEntity;
[Tooltip("Time to close the final boss message")]
public float timeToCloseMessage = 3f;
[Header("Gameplay Rules Info")]
[Tooltip("Timer text")]
public TimeFormat timeDisplayFormat = TimeFormat.Seconds;
public TextMeshProUGUI timer;
[Tooltip("Monsters killed text")]
public TextMeshProUGUI monstersKilled;
[Tooltip("Gold gain text")]
public TextMeshProUGUI goldGain;
[Tooltip("Insufficient MP text")]
public TextMeshProUGUI insufficientMPText;
[Tooltip("Pause menu GameObject")]
public GameObject pauseMenu;
public Image minimapImage;
[Header("Revive UI")]
public GameObject revivePanel;
public TextMeshProUGUI reviveCountdownText;
[Header("Upgrade Infos")]
[Tooltip("Perk entry passEntryPrefab")]
public PerkEntry perkEntryPrefab;
[Tooltip("Perk containerPassItems transform")]
public Transform perkContainer;
public Button openUpgradesButton;
public GameObject updateAvailable;
[Header("Skill Perk Images")]
[Tooltip("Array to hold skill perk images")]
public SkillsPerkImage[] skillPerkImages; // Array to hold skill perk images
[Header("Stat Perk Images")]
[Tooltip("Array to hold stat perk images")]
public StatPerkImage[] statPerkImages; // Array to hold stat perk images
[Header("Boss HP Bar")]
[Tooltip("Boss HP bar Image")]
public Image bossHpBar;
[Tooltip("Boss HP containerPassItems GameObject")]
public GameObject bossHpContainer;
[Tooltip("Timer containerPassItems GameObject")]
public GameObject timerContainer;
[Header("Death Entries UI")]
public Transform deathEntriesRoot;
public PlayerDeathEntryUI deathEntryPrefab;
[Header("PVP — Allies")]
public Transform alliesContainer;
public PvpTeamPlayerEntryUI allyEntryPrefab;
[Header("PVP — Kill Feed")]
public Transform killFeedContainer;
public KillFeedEntryUI killFeedEntryPrefab;
public int killFeedMaxEntries = 6;
[Header("PVP — Scoreboard (TDM)")]
public GameObject tdmScoreRoot;
public TextMeshProUGUI tdmTimerText;
public TextMeshProUGUI[] tdmTeamScores;
[Header("PVP — Scoreboard (Arena)")]
public GameObject arenaScoreRoot;
public TextMeshProUGUI[] arenaTeamScores;
public TextMeshProUGUI arenaKillLimitText;
[Header("PVP — Scoreboard (BR)")]
public GameObject brScoreRoot;
public TextMeshProUGUI brAliveText;
[Header("End Game Screen")]
[Tooltip("End game screen GameObject")]
public GameObject endGameScreen;
[Tooltip("End game message text")]
public TextMeshProUGUI endGameMessage;
[Tooltip("Victory objects to activate")]
public GameObject[] VictoryObjects;
[Tooltip("Defeat objects to activate")]
public GameObject[] DefeatObjects;
[Tooltip("Victory audio clip")]
public AudioClip VictoryAudio;
[Tooltip("Defeat audio clip")]
public AudioClip DefeatAudio;
public Button reviveButton;
// --- PVP End Panels --- //
[Header("PVP End Panels")]
public GameObject pvpEndRoot;
[Header("TDM End Panel")]
public GameObject tdmEndPanel;
public TextMeshProUGUI tdmWinnerText;
public TextMeshProUGUI tdmScoreText;
[Header("Arena End Panel")]
public GameObject arenaEndPanel;
public TextMeshProUGUI arenaWinnerText;
public TextMeshProUGUI arenaDetailText;
[Header("Battle Royale End Panel")]
public GameObject brEndPanel;
public TextMeshProUGUI brPlacementText;
public GameObject brTop3Badge;
[Header("Locked Perk Sprites")]
[Tooltip("Sprite for locked skills")]
public Sprite lockedSkillSprite;
[Tooltip("Sprite for locked characterStatsComponent")]
public Sprite lockedStatSprite;
[Tooltip("Sprite for unlocked slots")]
public Sprite unlockedSprite;
[Header("UI Character Info")]
public Image characterIcon;
[Tooltip("HealthComponent bar Image")]
public Image hpBar;
[Tooltip("Mana bar Image")]
public Image mpBar;
[Tooltip("Experience bar Image")]
public Image xpBar;
[Tooltip("Current text")]
public TextMeshProUGUI hpCurrent;
public TextMeshProUGUI energyCurrent;
public TextMeshProUGUI xpCurrent;
[Tooltip("Level text")]
public TextMeshProUGUI level;
[Tooltip("HealthComponent text")]
public TextMeshProUGUI hpText;
[Tooltip("HealthComponent regeneration text")]
public TextMeshProUGUI hpRegenText;
[Tooltip("HealthComponent leech text")]
public TextMeshProUGUI hpLeechText;
[Tooltip("Mana text")]
public TextMeshProUGUI mpText;
[Tooltip("Mana regeneration text")]
public TextMeshProUGUI mpRegenText;
[Tooltip("Damage text")]
public TextMeshProUGUI damageText;
[Tooltip("Attack speed text")]
public TextMeshProUGUI attackSpeedText;
[Tooltip("Cooldown reduction text")]
public TextMeshProUGUI cooldownReductionText;
[Tooltip("Critical rate text")]
public TextMeshProUGUI criticalRateText;
[Tooltip("Critical damage multiplier text")]
public TextMeshProUGUI criticalDamageMultiplierText;
[Tooltip("Defense text")]
public TextMeshProUGUI defenseText;
[Tooltip("Shield text")]
public TextMeshProUGUI shieldText;
[Tooltip("Move speed text")]
public TextMeshProUGUI moveSpeedText;
[Tooltip("Collect range text")]
public TextMeshProUGUI collectRangeText;
private CharacterEntity characterEntity;
private int upgradeAmount = 0;
private int _lastKnownLevel = -1;
private bool _perksOpen = false;
public static UIGameplay Singleton { get; private set; }
#if FUSION2
private readonly Dictionary _deathEntries = new();
#endif
#if FUSION2
private readonly Dictionary _allyEntries = new();
private byte _localTeamId;
#endif
private readonly Queue _killFeedQueue = new();
//Cancel Tokens
private CancellationTokenSource hpBarCts;
private CancellationTokenSource blinkCts;
private void Awake()
{
if (Singleton == null)
{
Singleton = this;
}
else
{
Destroy(gameObject);
}
reviveButton.onClick.AddListener(Revive);
EventBus.Subscribe(OnPlayerHealthChanged);
EventBus.Subscribe(OnPlayerEnergyChanged);
EventBus.Subscribe(OnPlayerShieldChanged);
EventBus.Subscribe(OnUpdateStatPerkUI);
EventBus.Subscribe(OnPlayerStatsChanged);
EventBus.Subscribe(OnTimerTick);
EventBus.Subscribe(OnMonstersKilledChanged);
EventBus.Subscribe(OnGoldChanged);
EventBus.Subscribe(OnSkillCooldownChanged);
EventBus.Subscribe(OnBossHealthChanged);
EventBus.Subscribe(OnUpgradePointsChanged);
EventBus.Subscribe(OnPlayerLevelUp);
}
private void OnDisable()
{
EventBus.Unsubscribe(OnPlayerHealthChanged);
EventBus.Unsubscribe(OnPlayerEnergyChanged);
EventBus.Unsubscribe(OnPlayerShieldChanged);
EventBus.Unsubscribe(OnUpdateStatPerkUI);
EventBus.Unsubscribe(OnPlayerStatsChanged);
EventBus.Unsubscribe(OnTimerTick);
EventBus.Unsubscribe(OnMonstersKilledChanged);
EventBus.Unsubscribe(OnSkillCooldownChanged);
EventBus.Unsubscribe(OnBossHealthChanged);
EventBus.Unsubscribe(OnUpgradePointsChanged);
EventBus.Unsubscribe(OnPlayerLevelUp);
}
private void Start()
{
monstersKilled.text = "0";
goldGain.text = "0";
InitializeAsync().Forget();
if(GameplayManager.Singleton.reviveLimit <= 0 && reviveButton) reviveButton.gameObject.SetActive(false);
}
///
/// Attempts to initialize the upgrade button when GameplayManager is ready.
///
private async UniTaskVoid InitializeAsync()
{
await UniTask.WaitUntil(() => GameplayManager.Singleton != null,
cancellationToken: this.GetCancellationTokenOnDestroy());
if (openUpgradesButton)
{
if (GameplayManager.Singleton.upgradeMode == UpgradeMode.UpgradeOnButtonClick)
{
openUpgradesButton.interactable = false;
openUpgradesButton.onClick.AddListener(OnClickToChoicePowerUp);
}
else
{
openUpgradesButton.gameObject.SetActive(false);
}
}
if (updateAvailable)
BlinkAsync(updateAvailable, this.GetCancellationTokenOnDestroy()).Forget();
}
private void OnPlayerHealthChanged(PlayerHealthChangedEvent evt)
{
if (evt.Target != characterEntity) return;
hpBarCts?.Cancel();
hpBarCts = new CancellationTokenSource();
hpCurrent.text = $"{Mathf.CeilToInt(evt.CurrentHP)} / {Mathf.CeilToInt(evt.MaxHP)}";
float startFill = hpBar.fillAmount;
float endFill = evt.CurrentHP / evt.MaxHP;
UpdateHpBarAsync(startFill, endFill, hpBarCts.Token).Forget();
}
private void OnPlayerEnergyChanged(PlayerEnergyChangedEvent evt)
{
if (evt.Target != characterEntity) return;
mpBar.fillAmount = evt.CurrentMP / evt.MaxMP;
energyCurrent.text = $"{Mathf.CeilToInt(evt.CurrentMP)} / {Mathf.CeilToInt(evt.MaxMP)}";
}
private void OnPlayerShieldChanged(PlayerShieldChangedEvent evt)
{
if (evt.Target != characterEntity) return;
shieldText.text = $"Shield: {Mathf.CeilToInt(evt.CurrentShield)}";
// shieldBar.fillAmount = evt.CurrentShield / evt.MaxHP;
}
private void OnPlayerLevelUp(PlayerEXPChangeEvent evt)
{
if (evt.Target != characterEntity) return;
level.text = $"Level: {evt.NewLevel}";
xpCurrent.text = $"{evt.CurrentXP} / {evt.NextLevelXP}";
xpBar.fillAmount = evt.CurrentXP / evt.NextLevelXP;
bool online = GameplayManager.Singleton.IsRunnerActive;
if (_lastKnownLevel < 0)
{
_lastKnownLevel = evt.NewLevel;
return;
}
if (evt.NewLevel <= _lastKnownLevel)
return;
_lastKnownLevel = evt.NewLevel;
if (online)
{
AddUpgradePoints();
return;
}
// Offline
if (GameplayManager.Singleton.upgradeMode == UpgradeMode.UpgradeOnLevelUp)
{
GameplayManager.Singleton.TogglePause();
if (!_perksOpen) OnLevelUpChoicePowerUp();
}
else if (GameplayManager.Singleton.upgradeMode == UpgradeMode.UpgradeOnButtonClick)
{
AddUpgradePoints();
}
}
public void OnUpdateStatPerkUI(StatPerkUpdatedEvent evt)
{
if (characterEntity == null || characterEntity != evt.Target) return;
UpdateStatPerkUI();
}
private void OnTimerTick(GameTimerTickEvent evt)
{
timer.text = FormatSeconds(evt.SecondsElapsed);
}
private void OnMonstersKilledChanged(MonstersKilledChangedEvent evt)
{
monstersKilled.text = evt.TotalKilled.ToString();
}
private void OnGoldChanged(GoldChangedEvent evt)
{
goldGain.text = evt.TotalGold.ToString();
}
private void OnSkillCooldownChanged(PlayerSkillCooldownChangedEvent evt)
{
if (evt.Target != characterEntity) return;
SkillImage img = skillImages[evt.SkillIndex];
if (img.ImageComponent == null) return;
if (evt.MaxCooldown > 0 && evt.CurrentCooldown > 0)
{
img.CooldownImage.fillAmount = evt.CurrentCooldown / evt.MaxCooldown;
img.CooldownText.text = Mathf.CeilToInt(evt.CurrentCooldown).ToString();
}
else
{
img.CooldownImage.fillAmount = 0;
img.CooldownText.text = string.Empty;
}
}
private void OnBossHealthChanged(BossHealthChangedEvent evt)
{
if (finalBossEntity == null)
finalBossEntity = evt.Boss;
if (evt.Boss != finalBossEntity) return;
bossHpBar.fillAmount = evt.CurrentHP / evt.MaxHP;
}
private void OnUpgradePointsChanged(UpgradePointsChangedEvent evt)
{
if (updateAvailable == null) return;
blinkCts?.Cancel();
blinkCts = new();
if (evt.Amount > 0)
BlinkAsync(updateAvailable, blinkCts.Token).Forget();
else
updateAvailable.SetActive(false);
}
private async UniTaskVoid BlinkAsync(GameObject target, CancellationToken token)
{
var renderer = target;
bool state = true;
while (!token.IsCancellationRequested)
{
if (upgradeAmount > 0)
{
state = !state;
renderer.SetActive(state);
await UniTask.Delay(500, cancellationToken: token);
}
else
{
renderer.SetActive(true);
await UniTask.NextFrame(token);
}
}
}
private void OnPlayerStatsChanged(PlayerStatsChangedEvent evt)
{
if (evt.Target != characterEntity) return;
var s = evt.playerStats; // snapshot
hpText.text = $"HP: {Mathf.CeilToInt(s.baseHP)}";
hpRegenText.text = $"HP Regen: {Mathf.CeilToInt(s.baseHPRegen)}";
hpLeechText.text = $"HP Leech: {s.baseHPLeech:F2}%";
mpText.text = $"MP: {Mathf.CeilToInt(s.baseMP)}";
mpRegenText.text = $"MP Regen: {Mathf.CeilToInt(s.baseMPRegen)}";
damageText.text = $"Damage: {Mathf.CeilToInt(s.baseDamage)}";
attackSpeedText.text = $"Atk Spd: {s.baseAttackSpeed:F2}";
cooldownReductionText.text = $"CDR: {s.baseCooldownReduction:F2}%";
criticalRateText.text = $"Crit: {s.baseCriticalRate:F2}%";
criticalDamageMultiplierText.text = $"Crit Dmg: x{Mathf.CeilToInt(s.baseCriticalDamageMultiplier)}";
defenseText.text = $"Def: {Mathf.CeilToInt(s.baseDefense)}";
shieldText.text = $"Shield: {Mathf.CeilToInt(s.baseShield)}";
moveSpeedText.text = $"Move Spd: {s.baseMoveSpeed:F2}";
collectRangeText.text = $"Collect: {Mathf.CeilToInt(s.baseCollectRange)}";
}
public string FormatSeconds(int seconds)
{
return timeDisplayFormat switch
{
TimeFormat.Seconds => $"{seconds}s",
TimeFormat.MinutesSeconds => $"{seconds / 60:00}:{seconds % 60:00}",
_ => seconds.ToString()
};
}
void Update()
{
if (characterEntity == null) return;
if (GameInstance.Singleton.platformType != PlatformType.PC && joystick)
{
var dir = new Vector3(joystick.Horizontal, 0, joystick.Vertical);
characterEntity.Move(dir);
}
// Update skill cooldowns
UpdateSkillCooldowns();
if (skillJoysticks != null)
{
foreach (var sj in skillJoysticks)
{
if (sj == null) continue;
var d = new Vector2(sj.Horizontal, sj.Vertical);
characterEntity.UpdateDirectionalAim(d, sj.skillIndex);
}
}
#if FUSION2
UpdatePvpScoreboard();
#endif
}
///
/// Shows the final boss message and triggers camera shake.
///
public void ShowFinalBossMessage()
{
if (finalBossMessage != null)
{
finalBossMessage.SetActive(true);
HideFinalBossMessageAsync(timeToCloseMessage, this.GetCancellationTokenOnDestroy()).Forget();
if (TopDownCameraController.Singleton != null)
{
TopDownCameraController.Singleton.TriggerCameraShake();
}
}
}
private async UniTaskVoid HideFinalBossMessageAsync(float delay, CancellationToken token)
{
await UniTask.Delay(TimeSpan.FromSeconds(delay), cancellationToken: token);
finalBossMessage.SetActive(false);
timerContainer.SetActive(false);
bossHpContainer.SetActive(true);
}
///
/// Displays the power-up choices when leveling up.
///
public void OnLevelUpChoicePowerUp()
{
foreach (Transform child in perkContainer) Destroy(child.gameObject);
List