using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 3 — Switch between characters on ONE player object. Roach Invasion
/// (4 insects with different HP and abilities), Spark of Corruption (4 party members).
///
/// Design: rather than four separate player prefabs (which fight over the camera,
/// PlayerInput, RespawnManager...), the player is a single object carrying every ability
/// component. A CharacterDefinition says which abilities are on, what the stats are, and
/// which Animator Controller to show. Swapping = apply a different definition.
///
/// SETUP
/// 1. Player has PlayerController2D v1.1, Health v1.1, Animator, SpriteRenderer, and
/// every ability component any character might use (DoubleJump, Glide, Dash,
/// WallSlideJump, LadderClimber, MeleeAttack, ProjectileShooter...). Add this script.
/// 2. Create a CharacterDefinition asset per character and drag them into Characters.
/// 3. Input: default "Previous" / "Next" actions (Q/E, gamepad bumpers) cycle characters.
/// Optional: number keys via the Input System "Player" map — add actions named
/// "Character1"... and they'll be picked up automatically (OnCharacter1 etc.).
/// 4. Separate Health Per Character: ON for Roach Invasion (each insect keeps its own HP;
/// a dead insect can't be selected). OFF for shared health.
/// 5. OnCharacterChanged(definition) -> HUD portrait/name. Swap Cooldown prevents
/// spamming. Can't swap mid-dash/launch (MovementLocked).
/// 6. Make sure the ability names in each definition match the component class names exactly.
/// </summary>
public class CharacterSwapper : MonoBehaviour
{
public List<CharacterDefinition> characters = new List<CharacterDefinition>();
public int startIndex = 0;
public float swapCooldown = 0.3f;
public bool separateHealthPerCharacter = true;
public bool allowSwapInAir = true;
[Header("Managed ability components (auto-filled from the object if empty)")]
public List<MonoBehaviour> abilityComponents = new List<MonoBehaviour>();
[Header("Events")]
public UnityEvent<CharacterDefinition> OnCharacterChanged;
public UnityEvent<int> OnCharacterIndexChanged;
public int CurrentIndex { get; private set; } = -1;
public CharacterDefinition Current => CurrentIndex >= 0 ? characters[CurrentIndex] : null;
private PlayerController2D controller;
private Health health;
private Animator anim;
private SpriteRenderer sr;
private Rigidbody2D rb;
private CameraTargetAssigner camAssigner;
private float cooldown;
private int[] storedHealth;
// Type names we consider "abilities" when auto-filling
static readonly string[] KnownAbilities =
{ "DoubleJump", "Glide", "Dash", "Sprint", "WallSlideJump", "LadderClimber", "GrapplingHook",
"MagnetPull", "RailRide", "MomentumLauncher", "MeleeAttack", "ProjectileShooter" };
void Awake()
{
controller = GetComponent<PlayerController2D>();
health = GetComponent<Health>();
anim = GetComponent<Animator>();
sr = GetComponentInChildren<SpriteRenderer>();
rb = GetComponent<Rigidbody2D>();
camAssigner = FindFirstObjectByType<CameraTargetAssigner>();
if (abilityComponents.Count == 0)
foreach (var mb in GetComponents<MonoBehaviour>())
if (System.Array.IndexOf(KnownAbilities, mb.GetType().Name) >= 0) abilityComponents.Add(mb);
storedHealth = new int[characters.Count];
for (int i = 0; i < characters.Count; i++) storedHealth[i] = characters[i].maxHealth;
}
void Start()
{
if (characters.Count > 0) Apply(Mathf.Clamp(startIndex, 0, characters.Count - 1), true);
}
void Update() { cooldown -= Time.deltaTime; }
// ---- Input ----
void OnNext(InputValue v) { if (v.isPressed) Cycle(+1); }
void OnPrevious(InputValue v) { if (v.isPressed) Cycle(-1); }
void OnCharacter1(InputValue v) { if (v.isPressed) TrySwap(0); }
void OnCharacter2(InputValue v) { if (v.isPressed) TrySwap(1); }
void OnCharacter3(InputValue v) { if (v.isPressed) TrySwap(2); }
void OnCharacter4(InputValue v) { if (v.isPressed) TrySwap(3); }
public void Cycle(int step)
{
if (characters.Count < 2) return;
int idx = CurrentIndex;
for (int tries = 0; tries < characters.Count; tries++)
{
idx = (idx + step + characters.Count) % characters.Count;
if (IsSelectable(idx)) { TrySwap(idx); return; }
}
}
public bool TrySwap(int index)
{
if (index < 0 || index >= characters.Count || index == CurrentIndex) return false;
if (cooldown > 0f || !IsSelectable(index)) return false;
if (controller != null && (controller.MovementLocked || !controller.InputEnabled)) return false;
if (!allowSwapInAir && controller != null && !controller.IsGrounded) return false;
Apply(index, false);
return true;
}
bool IsSelectable(int index)
{
if (!separateHealthPerCharacter) return true;
return storedHealth[index] > 0;
}
void Apply(int index, bool first)
{
// Save outgoing health
if (!first && separateHealthPerCharacter && health != null && CurrentIndex >= 0)
storedHealth[CurrentIndex] = health.CurrentHealth;
CurrentIndex = index;
var def = characters[index];
cooldown = swapCooldown;
// Look
if (anim != null && def.animatorController != null) anim.runtimeAnimatorController = def.animatorController;
else if (sr != null && def.idleSprite != null) sr.sprite = def.idleSprite;
if (sr != null) sr.color = def.tint;
// Stats
if (controller != null)
{
controller.moveSpeed = def.moveSpeed;
controller.jumpForce = def.jumpForce;
}
if (rb != null) rb.gravityScale = def.gravityScale;
if (health != null)
{
if (separateHealthPerCharacter)
{
health.SetMaxHealth(def.maxHealth, false);
health.SetCurrentHealth(storedHealth[index]);
}
else health.SetMaxHealth(def.maxHealth, first);
}
// Abilities
foreach (var mb in abilityComponents)
{
if (mb == null) continue;
mb.enabled = def.enabledAbilities.Contains(mb.GetType().Name);
}
// Camera stays on this object — nothing to do, but re-assert in case the
// student's version swaps objects instead.
camAssigner?.SetTarget(transform);
if (!first && def.swapInSound != null) AudioManager.Instance?.PlaySFX(def.swapInSound);
OnCharacterChanged?.Invoke(def);
OnCharacterIndexChanged?.Invoke(index);
}
/// <summary>
/// SEPARATE-HEALTH MODE: wire player Health.OnDied -> this (INSTEAD of RespawnManager.OnPlayerDied).
/// Marks the current character dead and swaps to a living one. When nobody is left it
/// calls RespawnManager itself. Wire RespawnManager.OnRespawned -> ReviveAll.
/// </summary>
public void OnCurrentCharacterDied()
{
if (!separateHealthPerCharacter) { RespawnManager.Instance?.OnPlayerDied(); return; }
storedHealth[CurrentIndex] = 0;
for (int i = 0; i < characters.Count; i++)
if (storedHealth[i] > 0) { Apply(i, false); return; }
RespawnManager.Instance?.OnPlayerDied(); // whole party is down
}
public void ReviveAll()
{
for (int i = 0; i < characters.Count; i++) storedHealth[i] = characters[i].maxHealth;
if (CurrentIndex >= 0 && health != null) health.SetCurrentHealth(storedHealth[CurrentIndex]);
}
}