269 lines
8.0 KiB
C#
269 lines
8.0 KiB
C#
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.SceneManagement;
|
||
using UnityEngine.XR;
|
||
|
||
public class PlayerStatusManager : MonoBehaviour, IDamageable
|
||
{
|
||
public static PlayerStatusManager Instance { get; private set; }
|
||
|
||
[Header("HP・ステータス設定")]
|
||
[SerializeField] private float maxHp = 100f;
|
||
[SerializeField] private float currentHp = 100f;
|
||
[Tooltip("被ダメージ後の無敵時間(秒)")]
|
||
[SerializeField] private float invincibilityDuration = 0.8f;
|
||
|
||
[Header("デバッグ設定")]
|
||
[SerializeField] private bool ignoreGaugeForDebug = true;
|
||
|
||
[Header("入力設定")]
|
||
[SerializeField] private InputActionProperty zlButtonAction;
|
||
[SerializeField] private InputActionProperty zrButtonAction;
|
||
[SerializeField] private InputActionProperty moveAction;
|
||
[SerializeField] private InputActionProperty turnAction;
|
||
|
||
[Header("パズル制限時間")]
|
||
[SerializeField] private float codingLimitDuration = 15f;
|
||
|
||
[Header("ゲージ設定")]
|
||
[SerializeField] private float maxGauge = 100f;
|
||
[SerializeField] private float currentGauge = 0f;
|
||
|
||
[Header("UI & エフェクト")]
|
||
[SerializeField] private GameObject codingModeUI;
|
||
[SerializeField] private GameObject successEffectPrefab;
|
||
[SerializeField] private Transform effectSpawnPoint;
|
||
|
||
[Header("バフ機能")]
|
||
[SerializeField] private BuffHandler buffHandler = new BuffHandler();
|
||
|
||
private float codingTimer;
|
||
private bool isInvincible;
|
||
|
||
public bool IsCodingMode { get; private set; }
|
||
public bool IsDead { get; private set; }
|
||
public float CurrentHp => currentHp;
|
||
public float MaxHp => maxHp;
|
||
public float CurrentGauge => currentGauge;
|
||
public float MaxGauge => maxGauge;
|
||
public BuffHandler Buffs => buffHandler;
|
||
|
||
public event Action<float, float> OnHpChanged;
|
||
public event Action<float, float> OnGaugeChanged;
|
||
public event Action OnDeath;
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance == null) Instance = this;
|
||
else Destroy(gameObject);
|
||
|
||
currentHp = maxHp;
|
||
}
|
||
|
||
private void OnEnable() => SetActionsEnabled(true);
|
||
private void OnDisable() => SetActionsEnabled(false);
|
||
|
||
private void SetActionsEnabled(bool enable)
|
||
{
|
||
SetActionState(zlButtonAction, enable);
|
||
SetActionState(zrButtonAction, enable);
|
||
SetActionState(moveAction, enable);
|
||
SetActionState(turnAction, enable);
|
||
}
|
||
|
||
private void SetActionState(InputActionProperty property, bool enable)
|
||
{
|
||
if (property.action == null) return;
|
||
if (enable) property.action.Enable();
|
||
else property.action.Disable();
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
if (codingModeUI != null) codingModeUI.SetActive(false);
|
||
OnHpChanged?.Invoke(currentHp, maxHp);
|
||
OnGaugeChanged?.Invoke(currentGauge, maxGauge);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (IsDead) return;
|
||
|
||
CheckComboInput();
|
||
|
||
if (!IsCodingMode)
|
||
{
|
||
buffHandler.UpdateTimers(Time.deltaTime);
|
||
}
|
||
else
|
||
{
|
||
codingTimer -= Time.deltaTime;
|
||
if (codingTimer <= 0f)
|
||
{
|
||
FailCodingModeByTimeout();
|
||
}
|
||
}
|
||
}
|
||
|
||
public void AddGauge(float amount)
|
||
{
|
||
if (IsCodingMode || IsDead) return;
|
||
currentGauge = Mathf.Clamp(currentGauge + amount, 0f, maxGauge);
|
||
OnGaugeChanged?.Invoke(currentGauge, maxGauge);
|
||
}
|
||
|
||
public void TakeDamage(DamageInfo damageInfo)
|
||
{
|
||
if (IsDead || isInvincible || IsCodingMode) return;
|
||
|
||
// 防御バフ適用(倍率を掛けて被ダメージを軽減)
|
||
float finalDamage = damageInfo.amount * buffHandler.CurrentDefenseMultiplier;
|
||
currentHp = Mathf.Max(0f, currentHp - finalDamage);
|
||
|
||
OnHpChanged?.Invoke(currentHp, maxHp);
|
||
Debug.Log($"💥 プレイヤー被ダメージ: {finalDamage:F1} (残HP: {currentHp:F1} / {maxHp})");
|
||
|
||
// コントローラーへの被弾振動フィードバック
|
||
TriggerDamageHaptics(0.6f, 0.15f);
|
||
|
||
if (currentHp <= 0f)
|
||
{
|
||
Die();
|
||
}
|
||
else
|
||
{
|
||
StartCoroutine(InvincibilityRoutine());
|
||
}
|
||
}
|
||
|
||
public void Heal(float amount)
|
||
{
|
||
if (IsDead) return;
|
||
currentHp = Mathf.Min(maxHp, currentHp + amount);
|
||
OnHpChanged?.Invoke(currentHp, maxHp);
|
||
Debug.Log($"💚 HP回復: +{amount} (現在HP: {currentHp} / {maxHp})");
|
||
}
|
||
|
||
private IEnumerator InvincibilityRoutine()
|
||
{
|
||
isInvincible = true;
|
||
yield return new WaitForSeconds(invincibilityDuration);
|
||
isInvincible = false;
|
||
}
|
||
|
||
private void Die()
|
||
{
|
||
if (IsDead) return;
|
||
IsDead = true;
|
||
|
||
Debug.LogError("☠️ プレイヤー死亡:GAME OVER");
|
||
OnDeath?.Invoke();
|
||
|
||
SetActionsEnabled(false);
|
||
if (WorldPauseManager.Instance != null)
|
||
{
|
||
WorldPauseManager.Instance.PauseWorld();
|
||
}
|
||
|
||
// 死亡後、2秒待って現在のシーンをリロード(再開)
|
||
StartCoroutine(GameOverRestartRoutine());
|
||
}
|
||
|
||
private IEnumerator GameOverRestartRoutine()
|
||
{
|
||
yield return new WaitForSecondsRealtime(2.5f);
|
||
if (WorldPauseManager.Instance != null)
|
||
{
|
||
WorldPauseManager.Instance.ResumeWorld();
|
||
}
|
||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||
}
|
||
|
||
private void TriggerDamageHaptics(float amplitude, float duration)
|
||
{
|
||
UnityEngine.XR.InputDevice left = InputDevices.GetDeviceAtXRNode(XRNode.LeftHand);
|
||
if (left.isValid) left.SendHapticImpulse(0, amplitude, duration);
|
||
|
||
UnityEngine.XR.InputDevice right = InputDevices.GetDeviceAtXRNode(XRNode.RightHand);
|
||
if (right.isValid) right.SendHapticImpulse(0, amplitude, duration);
|
||
}
|
||
|
||
private void CheckComboInput()
|
||
{
|
||
if (zlButtonAction.action == null || zrButtonAction.action == null) return;
|
||
|
||
bool isBothHeld = zlButtonAction.action.ReadValue<float>() > 0.5f && zrButtonAction.action.ReadValue<float>() > 0.5f;
|
||
bool isAnyTriggered = zlButtonAction.action.triggered || zrButtonAction.action.triggered;
|
||
|
||
if (isBothHeld && isAnyTriggered)
|
||
{
|
||
if (!IsCodingMode)
|
||
{
|
||
if (ignoreGaugeForDebug || currentGauge >= maxGauge)
|
||
{
|
||
StartCodingMode();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
CancelCodingMode();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void StartCodingMode()
|
||
{
|
||
IsCodingMode = true;
|
||
codingTimer = codingLimitDuration;
|
||
|
||
SetActionState(moveAction, false);
|
||
SetActionState(turnAction, false);
|
||
|
||
PuzzleBlock.ResetAllBlocksInScene();
|
||
|
||
if (WorldPauseManager.Instance != null)
|
||
{
|
||
WorldPauseManager.Instance.PauseWorld();
|
||
}
|
||
|
||
if (codingModeUI != null) codingModeUI.SetActive(true);
|
||
}
|
||
|
||
// PlayerStatusManager.cs
|
||
public void ApplyBuffAndResume(BuffType buff)
|
||
{
|
||
if (successEffectPrefab != null && effectSpawnPoint != null)
|
||
{
|
||
Instantiate(successEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
|
||
}
|
||
|
||
buffHandler.ActivateBuff(buff);
|
||
ResumeWorldState(consumeGauge: true);
|
||
}
|
||
|
||
private void FailCodingModeByTimeout() => ResumeWorldState(consumeGauge: true);
|
||
public void CancelCodingMode() => ResumeWorldState(consumeGauge: false);
|
||
|
||
private void ResumeWorldState(bool consumeGauge)
|
||
{
|
||
if (WorldPauseManager.Instance != null)
|
||
{
|
||
WorldPauseManager.Instance.ResumeWorld();
|
||
}
|
||
|
||
SetActionState(moveAction, true);
|
||
SetActionState(turnAction, true);
|
||
|
||
PuzzleBlock.ResetAllBlocksInScene();
|
||
|
||
IsCodingMode = false;
|
||
if (codingModeUI != null) codingModeUI.SetActive(false);
|
||
if (consumeGauge)
|
||
{
|
||
currentGauge = 0f;
|
||
OnGaugeChanged?.Invoke(currentGauge, maxGauge);
|
||
}
|
||
}
|
||
} |