using UnityEngine; using UnityEngine.AI; using System.Collections.Generic; [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Animator))] public class BossController : MonoBehaviour { #region インスペクター設定項目 [Header("ステータス設定")] [SerializeField] private float maxHp = 500f; [SerializeField] private float currentHp = 500f; [SerializeField] public float chaseRange = 15f; [SerializeField] public float biteRange = 5f; [Header("必殺技(天井)設定")] [Tooltip("天井の待機座標となる空のGameObjectのTransformを指定してください")] [SerializeField] public Transform ceilingAnchor; [Tooltip("地上に戻る際の、中央の着地座標のTransformを指定してください")] [SerializeField] public Transform groundAnchor; [Tooltip("天井から糸を発射する範囲の広さ")] [SerializeField] public float attackRadius = 10f; [Tooltip("糸が降る前の予兆マークのPrefab")] [SerializeField] private GameObject warningMarkerPrefab; [Tooltip("地面に残る糸(コライダー&鈍足スクリプト付き)のPrefab")] [SerializeField] private GameObject webPrefab; [Header("突進(体当たり)設定")] [SerializeField] public float chargeSpeed = 14f; [SerializeField] public float chargeDuration = 2f; [SerializeField] public float chargeMinimumDistance = 6f; [SerializeField] public float chargeCooldownDuration = 4f; // ─── ボス固有の通常移動パラメーター(Start時に自動キャッシュされます) ─── public float NormalSpeed { get; private set; } public float NormalAcceleration { get; private set; } private void Start() { // インセペクターで設定されたNavMeshAgentの初期値を記憶しておく if (Agent != null) { NormalSpeed = Agent.speed; NormalAcceleration = Agent.acceleration; } } #endregion #region コンポーネント・キャッシュ用(ステートからアクセス可能) [HideInInspector] public NavMeshAgent Agent { get; private set; } [HideInInspector] public Animator Anim { get; private set; } [HideInInspector] public Transform Player { get; private set; } public float ChargeCooldownTimer { get; private set; } = 0f; #endregion #region FSM(状態管理)システム private ISpiderState currentState; // ステートのインスタンスをキャッシュ(ガベージコレクションの発生を抑える) private Dictionary stateCache; // フェーズ管理用フラグ(50%と25%のトリガー管理) private bool triggered50 = false; private bool triggered25 = false; public bool PendingUltimate { get; set; } = false; // 必殺技の発動権があるか #endregion private void Awake() { Agent = GetComponent(); Anim = GetComponent(); GameObject pObj = GameObject.FindGameObjectWithTag("Player"); if (pObj != null) Player = pObj.transform; InitializeFSM(); } private void InitializeFSM() { stateCache = new Dictionary { { typeof(SpiderIdleState), new SpiderIdleState(this) }, { typeof(SpiderChaseState), new SpiderChaseState(this) }, { typeof(SpiderBiteState), new SpiderBiteState(this) }, { typeof(SpiderChargeState), new SpiderChargeState(this) }, { typeof(SpiderJumpToCeilingState), new SpiderJumpToCeilingState(this) }, { typeof(SpiderCeilingAttackState), new SpiderCeilingAttackState(this) }, { typeof(SpiderStunState), new SpiderStunState(this) } }; // 初期ステートをIdleに設定 ChangeState(); } private void Update() { currentState?.UpdateState(); if (ChargeCooldownTimer > 0f) { ChargeCooldownTimer -= Time.deltaTime; } } public void ChangeState() where T : ISpiderState { if (currentState != null) { currentState.ExitState(); Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}"); } currentState = stateCache[typeof(T)]; Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}"); currentState.EnterState(); } /// /// ダメージを受け取る外部窓口。HPトリガーによるフェーズ遷移をここで監視する /// public void TakeDamage(float damage) { currentHp -= damage; currentHp = Mathf.Clamp(currentHp, 0, maxHp); float hpPercentage = (currentHp / maxHp) * 100f; Debug.Log($"💥 ボスに {damage} ダメージ。残りHP: {hpPercentage:F1}%"); // 25%以下カットのトリガー if (hpPercentage <= 25f && !triggered25) { triggered25 = true; PendingUltimate = true; TriggerPhaseTransition(); } // 50%以下カットのトリガー else if (hpPercentage <= 50f && !triggered50 && hpPercentage > 25f) { triggered50 = true; PendingUltimate = true; TriggerPhaseTransition(); } } private void TriggerPhaseTransition() { // 咆哮、ジャンプ、スタン中以外であれば、現在の行動をキャンセルして即座に天井へエスケープ if (currentState is not SpiderJumpToCeilingState && currentState is not SpiderCeilingAttackState && currentState is not SpiderStunState) { ChangeState(); } } /// /// 【パリィ連携窓口】プレイヤーのVRInputPunchなどから、パリィ成功時にこのメソッドが叩かれる /// public void TriggerParryStun() { // パリィを受け付ける攻撃ステート(噛みつき、体当たり)のときのみスタンに移行する if (currentState is SpiderBiteState || currentState is SpiderChargeState) { ChangeState(); } } public void ResetChargeCooldown() { ChargeCooldownTimer = chargeCooldownDuration; } #region ファクトリメソッド(エフェクトやオブジェクト生成の保守性を担保) public void SpawnWarningAndWeb(Vector3 targetGroundPos, float delay, float webDuration) { StartCoroutine(SpawnWebRoutine(targetGroundPos, delay, webDuration)); } private System.Collections.IEnumerator SpawnWebRoutine(Vector3 pos, float delay, float duration) { // 1. 地面に予兆マークを生成 GameObject marker = null; if (warningMarkerPrefab != null) { marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.02f, 0), Quaternion.identity); } yield return new WaitForSeconds(delay); // 2. 予兆マークを消し、本物の糸オブジェクトを生成 if (marker != null) Destroy(marker); if (webPrefab != null) { GameObject web = Instantiate(webPrefab, pos, Quaternion.identity); Destroy(web, duration); // 数秒後に自動消滅 } } /// /// ★超重要:アニメーターに残ってしまった不要なトリガーをすべて強制消去(リセット)する /// これを各ステートのExit時に呼ぶことで、アニメーションが固まる・遅れて暴発するバグを根絶します /// public void ResetAllAnimationTriggers() { if (Anim == null) return; Anim.ResetTrigger("Attack_Bite"); Anim.ResetTrigger("Attack_Roar"); Anim.ResetTrigger("ChargeStart"); Anim.ResetTrigger("JumpUp"); Anim.ResetTrigger("Land"); Anim.ResetTrigger("StunStart"); } #endregion } #region 1. ステート共通インターフェース public interface ISpiderState { void EnterState(); void UpdateState(); void ExitState(); } #endregion