using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using Ashwild.Core;
namespace Ashwild.Player
{
///
/// The single, persistent source of input. It reads the project InputActionAsset and republishes
/// every action onto the static PlayerEvents bus, so gameplay and UI both consume input the same
/// way without depending on a player GameObject. Persists across scenes via its PersistentRoot
/// parent, so it works in every scene. Replaces the old PlayerInput component + PlayerInputRouter.
///
/// The UI map (Cancel) is always active; the Player map is only active in-game (between
/// LocalPlayerSpawned and SessionStopped) so menu typing never triggers gameplay actions.
///
[DisallowMultipleComponent]
public class InputManager : MonoBehaviour
{
#region Constants
private const string PlayerMapName = "Player";
private const string UIMapName = "UI";
#endregion
#region Serialized Fields
[Header("Input")]
[Tooltip("The InputSystem_Actions asset (Player + UI maps).")]
[SerializeField] private InputActionAsset actions;
#endregion
#region State
///
/// Global access point (rarely needed — consumers read PlayerEvents, not this).
///
public static InputManager Instance { get; private set; }
private InputActionMap playerMap;
private InputActionMap uiMap;
///
/// Undo callbacks recorded while wiring actions, invoked on teardown.
///
private readonly List teardown = new List();
#endregion
#region Unity Lifecycle
///
/// Establishes the persistent singleton and wires every action to the bus.
///
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
// Detach to the scene root and persist — leaves the (possibly mixed) Managers parent
// behind so non-persistent siblings die with the scene as usual.
Persistence.Persist(gameObject);
if (actions == null)
{
Debug.LogError("[InputManager] No InputActionAsset assigned.", this);
return;
}
playerMap = actions.FindActionMap(PlayerMapName, false);
uiMap = actions.FindActionMap(UIMapName, false);
WirePlayerMap();
WireUIMap();
// Cancel (back / pause / close) must work everywhere, menu included.
uiMap?.Enable();
}
///
/// Subscribes to the session lifecycle that gates the gameplay map.
///
private void OnEnable()
{
PlayerEvents.LocalPlayerSpawned += EnableGameplay;
PlayerEvents.SessionStopped += DisableGameplay;
}
///
/// Unsubscribes — mirrors OnEnable.
///
private void OnDisable()
{
PlayerEvents.LocalPlayerSpawned -= EnableGameplay;
PlayerEvents.SessionStopped -= DisableGameplay;
}
///
/// Tears down all action subscriptions and disables the maps.
///
private void OnDestroy()
{
if (Instance == this) Instance = null;
foreach (Action undo in teardown) undo();
teardown.Clear();
playerMap?.Disable();
uiMap?.Disable();
}
#endregion
#region Gameplay Map Gating
///
/// Enables gameplay input once the local player exists.
///
private void EnableGameplay() => playerMap?.Enable();
///
/// Disables gameplay input when the session ends (back to the menu).
///
private void DisableGameplay() => playerMap?.Disable();
#endregion
#region Wiring
///
/// Maps every gameplay action to its PlayerEvents raiser.
///
private void WirePlayerMap()
{
if (playerMap == null)
{
Debug.LogError($"[InputManager] No '{PlayerMapName}' action map found.", this);
return;
}
WireVector2(playerMap, "Move", PlayerEvents.RaiseMoveInput);
WireVector2(playerMap, "Look", PlayerEvents.RaiseLookInput);
WireButton(playerMap, "Jump", PlayerEvents.RaiseJumpPressed);
WireHold(playerMap, "Sprint", PlayerEvents.RaiseSprintHeld);
WireButton(playerMap, "Crouch", PlayerEvents.RaiseCrouchPressed);
WireButton(playerMap, "Attack", PlayerEvents.RaiseAttackPressed);
WireButton(playerMap, "Interact", PlayerEvents.RaiseInteractPressed);
WireButton(playerMap, "Drop", PlayerEvents.RaiseDropPressed);
WireButton(playerMap, "Inventory", PlayerEvents.RaiseInventoryTogglePressed);
WireScrollY(playerMap, "HotbarScroll", PlayerEvents.RaiseHotbarScroll);
for (int i = 0; i < 10; i++)
{
int slot = i;
WireButton(playerMap, $"HotbarSlot{i + 1}", () => PlayerEvents.RaiseHotbarSlotPressed(slot));
}
}
///
/// Maps the UI Cancel action to the bus.
///
private void WireUIMap()
{
if (uiMap == null)
{
Debug.LogError($"[InputManager] No '{UIMapName}' action map found.", this);
return;
}
WireButton(uiMap, "Cancel", PlayerEvents.RaiseCancelPressed);
}
#endregion
#region Wiring Helpers
///
/// Button press → parameterless raise.
///
private void WireButton(InputActionMap map, string name, Action raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Handler(InputAction.CallbackContext _) => raise();
action.performed += Handler;
teardown.Add(() => action.performed -= Handler);
}
///
/// Hold action → true on press, false on release.
///
private void WireHold(InputActionMap map, string name, Action raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Down(InputAction.CallbackContext _) => raise(true);
void Up(InputAction.CallbackContext _) => raise(false);
action.performed += Down;
action.canceled += Up;
teardown.Add(() => { action.performed -= Down; action.canceled -= Up; });
}
///
/// Vector2 value → current value on change, zero on release.
///
private void WireVector2(InputActionMap map, string name, Action raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Value(InputAction.CallbackContext c) => raise(c.ReadValue());
void Reset(InputAction.CallbackContext _) => raise(Vector2.zero);
action.performed += Value;
action.canceled += Reset;
teardown.Add(() => { action.performed -= Value; action.canceled -= Reset; });
}
///
/// Scroll value → forwards its Y delta.
///
private void WireScrollY(InputActionMap map, string name, Action raise)
{
InputAction action = Resolve(map, name);
if (action == null) return;
void Value(InputAction.CallbackContext c) => raise(c.ReadValue().y);
action.performed += Value;
teardown.Add(() => action.performed -= Value);
}
///
/// Finds an action in a map, warning (not throwing) when it is missing.
///
private InputAction Resolve(InputActionMap map, string name)
{
InputAction action = map.FindAction(name, false);
if (action == null)
Debug.LogWarning($"[InputManager] Action '{name}' not found in map '{map.name}'.", this);
return action;
}
#endregion
}
}