using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// GAME 1303 Tier 2 — A boss is a loop: wait, pick an attack, do it, recover.
/// Phases change the list of attacks (and speed) as health drops.
///
/// SETUP
/// 1. Boss root: Rigidbody2D + Health (big number, Invulnerability 0, Destroy On Death
/// off) + Knockback (Resistance 1) + this script. Hurtbox child (Team = Enemy).
/// 2. Build attacks as UnityEvents. Each Attack entry has:
/// Name – for your reference and the Animator trigger (optional)
/// Duration – how long the boss is "busy" before choosing again
/// On Execute – what happens: ProjectileShooter.Fire, MeleeAttack.Attack,
/// Hitbox.gameObject.SetActive(true), a Dash script, etc.
/// Weight – relative chance of being picked (2 = twice as likely as 1)
/// 3. Phases: each has Health Percent (phase starts when health drops to/below it)
/// and its own list of attacks. Phase 0 should be at 100.
/// Example (Blaster): Phase 100% = [Shoot, Walk]; Phase 50% = [Shoot, TripleShot, Charge].
/// 4. Wire Health.OnDied -> BossController.OnDefeated is automatic; hook
/// OnBossDefeated to a door, GameManager.LevelComplete, or a cutscene.
/// 5. Optional Face Player: flips scale toward the player before each attack.
///
/// Movement between attacks is up to you: put an EnemyChase (with OnAttack empty)
/// or EnemyPatrol on the same object, or let it stand still like a turret.
/// </summary>
public class BossController : MonoBehaviour
{
[System.Serializable]
public class BossAttack
{
public string name = "Attack";
public float duration = 1f;
[Min(0.01f)] public float weight = 1f;
public string animatorTrigger;
public UnityEvent onExecute;
}
[System.Serializable]
public class Phase
{
public string name = "Phase 1";
[Range(0, 100)] public int healthPercent = 100;
[Tooltip("Idle time between attacks in this phase.")]
public float restBetweenAttacks = 1.5f;
public List<BossAttack> attacks = new List<BossAttack>();
public UnityEvent onPhaseStart; // change music, roar, spawn adds
}
[Header("Setup")]
public List<Phase> phases = new List<Phase>();
public bool facePlayer = true;
public bool artFacesRight = true;
[Tooltip("ON = fight begins when the scene starts. OFF = wait until something calls StartBoss() (use BossArenaTrigger).")]
public bool startAutomatically = true;
[Tooltip("Don't repeat the same attack twice in a row.")]
public bool avoidRepeats = true;
[Header("Events")]
public UnityEvent OnBossStarted;
public UnityEvent<int> OnPhaseChanged; // phase index
public UnityEvent OnBossDefeated;
public int CurrentPhaseIndex { get; private set; } = -1;
public bool IsActive { get; private set; }
private Health health;
private Animator anim;
private Transform player;
private BossAttack lastAttack;
private Coroutine loop;
private bool facingRight;
void Awake()
{
health = GetComponent<Health>();
anim = GetComponent<Animator>();
facingRight = artFacesRight ? transform.localScale.x > 0 : transform.localScale.x < 0;
if (health != null)
{
health.OnHealthChanged.AddListener(CheckPhase);
health.OnDied.AddListener(OnDefeated);
}
// Sort phases by health percent descending so 100 is first
phases.Sort((a, b) => b.healthPercent.CompareTo(a.healthPercent));
}
void Start()
{
var p = GameObject.FindGameObjectWithTag("Player");
if (p != null) player = p.transform;
if (startAutomatically) StartBoss();
}
/// <summary>Call from an arena trigger to begin the fight.</summary>
public void StartBoss()
{
if (IsActive || phases.Count == 0) return;
IsActive = true;
EnterPhase(0);
OnBossStarted?.Invoke();
loop = StartCoroutine(AttackLoop());
}
IEnumerator AttackLoop()
{
while (IsActive)
{
Phase phase = phases[CurrentPhaseIndex];
yield return new WaitForSeconds(phase.restBetweenAttacks);
if (!IsActive) yield break;
BossAttack atk = PickAttack(phase);
if (atk == null) continue;
if (facePlayer && player != null) FaceToward(player.position.x);
if (anim != null && !string.IsNullOrEmpty(atk.animatorTrigger)) anim.SetTrigger(atk.animatorTrigger);
atk.onExecute?.Invoke();
lastAttack = atk;
yield return new WaitForSeconds(atk.duration);
}
}
BossAttack PickAttack(Phase phase)
{
if (phase.attacks.Count == 0) return null;
if (phase.attacks.Count == 1) return phase.attacks[0];
float total = 0f;
foreach (var a in phase.attacks)
if (!(avoidRepeats && a == lastAttack)) total += a.weight;
float r = Random.value * total;
foreach (var a in phase.attacks)
{
if (avoidRepeats && a == lastAttack) continue;
r -= a.weight;
if (r <= 0f) return a;
}
return phase.attacks[0];
}
void CheckPhase(int current, int max)
{
if (!IsActive) return;
float pct = 100f * current / max;
// Find the lowest-threshold phase we qualify for
int best = CurrentPhaseIndex;
for (int i = 0; i < phases.Count; i++)
if (pct <= phases[i].healthPercent) best = i;
if (best != CurrentPhaseIndex) EnterPhase(best);
}
void EnterPhase(int index)
{
CurrentPhaseIndex = index;
phases[index].onPhaseStart?.Invoke();
OnPhaseChanged?.Invoke(index);
}
void OnDefeated()
{
IsActive = false;
if (loop != null) StopCoroutine(loop);
// Turn off any attack hitboxes still active
foreach (var hb in GetComponentsInChildren<Hitbox>()) hb.gameObject.SetActive(false);
OnBossDefeated?.Invoke();
}
void FaceToward(float worldX)
{
bool shouldFaceRight = worldX > transform.position.x;
if (shouldFaceRight != facingRight)
{
facingRight = shouldFaceRight;
Vector3 s = transform.localScale; s.x *= -1f; transform.localScale = s;
}
}
}