274 lines
10 KiB
C#
274 lines
10 KiB
C#
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
|
||
namespace _Project.Scripts.PlayerUtils
|
||
{
|
||
public class PlayerPossession : MonoBehaviour, IDamageable
|
||
{
|
||
[Header("Профиль Духа (Форма по умолчанию)")]
|
||
[SerializeField] private BodyProfile spiritProfile;
|
||
|
||
[Header("Настройки Вселения (Possession)")]
|
||
[Tooltip("Радиус зоны вселения в метрах")]
|
||
[SerializeField] private float possessRange = 12f;
|
||
[Tooltip("С какого максимального расстояния прицел замечает врага и показывает сферу зоны")]
|
||
[SerializeField] private float maxAimLookDistance = 60f;
|
||
[SerializeField] private float possessDashDuration = 0.15f;
|
||
[SerializeField] private LayerMask possessRayMask = ~0;
|
||
|
||
[Header("Визуал Сферы Зоны Вселения")]
|
||
[Tooltip("Если true — сфера рисуется вокруг врага (показывая зону, куда надо войти). Если false — вокруг игрока.")]
|
||
[SerializeField] private bool centerSphereOnTarget = true;
|
||
[SerializeField] private Color zoneReadyColor = new Color(0.1f, 0.95f, 1f, 0.10f);
|
||
[SerializeField] private Color zoneOutOfRangeColor = new Color(1f, 0.35f, 0.35f, 0.06f);
|
||
|
||
[Header("Замедление времени в форме Духа")]
|
||
[SerializeField, Range(0.1f, 1f)] private float spiritTimeScale = 0.45f;
|
||
|
||
// Ивент окончательной смерти игрока (когда убили в форме Духа)
|
||
public event Action OnPlayerDied;
|
||
|
||
public BodyProfile CurrentProfile { get; private set; }
|
||
public bool IsInSpiritForm => CurrentProfile != null && CurrentProfile.bodyClass == BodyClass.Spirit;
|
||
public bool IsDead { get; private set; }
|
||
public float CurrentHealth { get; private set; }
|
||
public float MaxHealth => CurrentProfile != null ? CurrentProfile.maxHealth : 25f;
|
||
|
||
public EnemyAI AimedEnemy { get; private set; }
|
||
public bool IsTargetInPossessZone { get; private set; }
|
||
|
||
public float PlayerTimeMultiplier =>
|
||
IsInSpiritForm ? (1f / Mathf.Max(0.1f, Time.timeScale)) * 0.85f : 1f;
|
||
|
||
private Transform _camTransform;
|
||
private bool _isTransitioning;
|
||
private float _invulnerableTimer;
|
||
|
||
private GameObject _zoneSphere;
|
||
private Renderer _zoneRenderer;
|
||
private Material _zoneMaterial;
|
||
|
||
private void Awake()
|
||
{
|
||
if (spiritProfile == null)
|
||
spiritProfile = BodyProfile.CreateDefault(BodyClass.Spirit);
|
||
|
||
CreatePossessionZoneSphere();
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
_camTransform = Player.Instance.CameraController.CameraTransform;
|
||
EnterSpiritForm(false);
|
||
}
|
||
|
||
private void CreatePossessionZoneSphere()
|
||
{
|
||
_zoneSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||
_zoneSphere.name = "PossessionZoneSphere";
|
||
|
||
var col = _zoneSphere.GetComponent<Collider>();
|
||
if (col != null) Destroy(col);
|
||
|
||
_zoneRenderer = _zoneSphere.GetComponent<Renderer>();
|
||
_zoneRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
_zoneRenderer.receiveShadows = false;
|
||
|
||
Shader shader = Shader.Find("Sprites/Default");
|
||
_zoneMaterial = new Material(shader);
|
||
_zoneMaterial.color = Color.clear;
|
||
_zoneRenderer.material = _zoneMaterial;
|
||
|
||
_zoneSphere.SetActive(false);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (IsDead) return;
|
||
if (_invulnerableTimer > 0f) _invulnerableTimer -= Time.unscaledDeltaTime;
|
||
|
||
if (_isTransitioning)
|
||
{
|
||
if (_zoneSphere != null) _zoneSphere.SetActive(false);
|
||
return;
|
||
}
|
||
|
||
UpdateAimedTarget();
|
||
UpdateZoneVisual();
|
||
HandleInput();
|
||
}
|
||
|
||
private void UpdateAimedTarget()
|
||
{
|
||
AimedEnemy = null;
|
||
IsTargetInPossessZone = false;
|
||
|
||
if (_camTransform == null) return;
|
||
|
||
Ray ray = new Ray(_camTransform.position, _camTransform.forward);
|
||
if (Physics.SphereCast(ray, 0.5f, out RaycastHit hit, maxAimLookDistance, possessRayMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
var enemy = hit.collider.GetComponentInParent<EnemyAI>();
|
||
if (enemy != null && enemy.CanBePossessed)
|
||
{
|
||
AimedEnemy = enemy;
|
||
float dist = Vector3.Distance(transform.position, enemy.transform.position);
|
||
IsTargetInPossessZone = dist <= possessRange;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateZoneVisual()
|
||
{
|
||
if (_zoneSphere == null) return;
|
||
|
||
if (AimedEnemy != null)
|
||
{
|
||
if (!_zoneSphere.activeSelf) _zoneSphere.SetActive(true);
|
||
|
||
float diameter = possessRange * 2f;
|
||
_zoneSphere.transform.localScale = new Vector3(diameter, diameter, diameter);
|
||
|
||
Vector3 center = centerSphereOnTarget ? AimedEnemy.transform.position : transform.position;
|
||
_zoneSphere.transform.position = center;
|
||
|
||
Color targetColor = IsTargetInPossessZone ? zoneReadyColor : zoneOutOfRangeColor;
|
||
_zoneMaterial.color = Color.Lerp(_zoneMaterial.color, targetColor, Time.unscaledDeltaTime * 12f);
|
||
}
|
||
else
|
||
{
|
||
if (_zoneSphere.activeSelf)
|
||
{
|
||
_zoneMaterial.color = Color.clear;
|
||
_zoneSphere.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void HandleInput()
|
||
{
|
||
if (Keyboard.current == null) return;
|
||
|
||
if (Keyboard.current.eKey.wasPressedThisFrame && AimedEnemy != null && IsTargetInPossessZone)
|
||
{
|
||
StartCoroutine(PossessRoutine(AimedEnemy));
|
||
}
|
||
else if (Keyboard.current.qKey.wasPressedThisFrame && !IsInSpiritForm)
|
||
{
|
||
EnterSpiritForm(true);
|
||
}
|
||
}
|
||
|
||
private IEnumerator PossessRoutine(EnemyAI target)
|
||
{
|
||
_isTransitioning = true;
|
||
if (_zoneSphere != null) _zoneSphere.SetActive(false);
|
||
|
||
BodyProfile newProfile = target.profile != null
|
||
? target.profile
|
||
: BodyProfile.CreateDefault(target.enemyClass);
|
||
float inheritedHealth = Mathf.Max(target.CurrentHealth, newProfile.maxHealth * 0.5f);
|
||
|
||
Vector3 startPos = transform.position;
|
||
Vector3 endPos = target.transform.position;
|
||
Quaternion targetRot = target.transform.rotation;
|
||
|
||
Destroy(target.gameObject);
|
||
|
||
float elapsed = 0f;
|
||
while (elapsed < possessDashDuration)
|
||
{
|
||
elapsed += Time.unscaledDeltaTime;
|
||
float t = Mathf.Clamp01(elapsed / possessDashDuration);
|
||
transform.position = Vector3.Lerp(startPos, endPos, t);
|
||
yield return null;
|
||
}
|
||
|
||
transform.position = endPos;
|
||
Player.Instance.CameraController.SetYaw(targetRot.eulerAngles.y);
|
||
|
||
ApplyProfile(newProfile, inheritedHealth);
|
||
_invulnerableTimer = 0.4f;
|
||
_isTransitioning = false;
|
||
}
|
||
|
||
public void EnterSpiritForm(bool ejectImpulse)
|
||
{
|
||
if (IsDead) return;
|
||
|
||
ApplyProfile(spiritProfile, spiritProfile.maxHealth);
|
||
_invulnerableTimer = 0.5f;
|
||
|
||
if (ejectImpulse && Player.Instance.Movement != null)
|
||
{
|
||
Player.Instance.Movement.AddVerticalImpulse(8f);
|
||
}
|
||
}
|
||
|
||
public void ApplyProfile(BodyProfile profile, float startHealth)
|
||
{
|
||
CurrentProfile = profile;
|
||
CurrentHealth = Mathf.Clamp(startHealth, 1f, profile.maxHealth);
|
||
|
||
bool isSpirit = profile.bodyClass == BodyClass.Spirit;
|
||
Time.timeScale = isSpirit ? spiritTimeScale : 1f;
|
||
Time.fixedDeltaTime = 0.02f * Time.timeScale;
|
||
|
||
Player.Instance.Movement.ApplyProfile(profile);
|
||
Player.Instance.CameraController.ApplyProfile(profile);
|
||
Player.Instance.Combat.ApplyProfile(profile);
|
||
Player.Instance.Sound.SetFootstepPitch(profile.footstepPitch);
|
||
}
|
||
|
||
public void TakeDamage(float amount)
|
||
{
|
||
if (IsDead || _invulnerableTimer > 0f || _isTransitioning) return;
|
||
|
||
CurrentHealth -= amount;
|
||
|
||
if (CurrentHealth <= 0f)
|
||
{
|
||
if (!IsInSpiritForm)
|
||
{
|
||
// Тело уничтожено — выбрасывает обратно в Духа
|
||
Debug.Log("Носитель уничтожен! Выброс в форму Духа!");
|
||
EnterSpiritForm(true);
|
||
}
|
||
else
|
||
{
|
||
// Убили самого Духа — проигрыш!
|
||
Die();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void Die()
|
||
{
|
||
if (IsDead) return;
|
||
IsDead = true;
|
||
CurrentHealth = 0f;
|
||
|
||
if (_zoneSphere != null) _zoneSphere.SetActive(false);
|
||
|
||
// Останавливаем время и отключаем управление персонажа
|
||
Time.timeScale = 0f;
|
||
Player.Instance.Controls.Player.Disable();
|
||
|
||
// Освобождаем курсор мыши для клика по кнопкам панели проигрыша
|
||
Cursor.lockState = CursorLockMode.None;
|
||
Cursor.visible = true;
|
||
|
||
Debug.Log("ДУХ РАЗВЕЯН! Игрок погиб.");
|
||
OnPlayerDied?.Invoke();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_zoneSphere != null) Destroy(_zoneSphere);
|
||
Time.timeScale = 1f;
|
||
Time.fixedDeltaTime = 0.02f;
|
||
}
|
||
}
|
||
} |