using UnityEngine; using UnityEngine.AI; using System.Collections.Generic; [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(Animator))] public class BossController : MonoBehaviour, IDamageble { [SerializeField] private float maxHp = 500f; [SerializeField] private float currentHp = 500f; [field: SerializeField] public float ChaseRange { get; private set; } = 15f; [field: SerializeField] public float BiteRange { get; private set; } = 5f; [field: SerializeField] public float ChargeSpeed { get; private set; } = 14f; [field: SerializeField] public float ChargeDuration { get; private set; } = 2f; [field: SerializeField] public float ChargeMinimumDistance { get; private set; } = 6f; [field: SerializeField] public float ChargeCooldownDuration { get; private set; } = 4f; [field: SerializeField] public float ChargeRoarDuration { get; private set; } = 5f; [SerializeField] public Transform ceilingAnchor; [SerializeField] public Transform groundAnchor; [field: SerializeField] public float AttackRadius { get; private set; } = 15f; [field: SerializeField] public float JumpRoarDuration { get; private set; } = 2f; [field: SerializeField] public float CeilingAttackDuration { get; private set; } = 8.0f; [field: SerializeField] public float CeilingShotInterval { get; private set; } = 0.2f; [field: SerializeField] public float WebWarningDelay { get; private set; } = 0.8f; [field: SerializeField] public float WebDuration { get; private set; } = 3.0f; [Tooltip("★新設:すでに糸がある場所から、最低何メートル離すか(被り防止の半径)")] [field: SerializeField] public float WebAvoidanceRadius { get; private set; } = 2.5f; // ★新設:現在ステージ上に存在している(または予兆がある)糸の座標リスト public List ActiveWebPositions { get; private set; } = new List(); [SerializeField] private GameObject warningMarkerPrefab; [SerializeField] private GameObject webPrefab; public NavMeshAgent Agent { get; private set; } public Animator Anim { get; private set; } public Transform Player { get; private set; } public float ChargeCooldownTimer { get; private set; } = 0f; public float NormalSpeed { get; private set; } public float NormalAcceleration { get; private set; } private ISpiderState currentState; private Dictionary stateCache; // フェーズトリガー private bool triggered50 = false; private bool triggered25 = false; public bool PendingUltimate { get; set; } = false; private void Awake() { Agent = GetComponent(); Anim = GetComponent(); GameObject pObj = GameObject.FindGameObjectWithTag("Player"); if (pObj != null) Player = pObj.transform; InitializeFSM(); } private void Start() { if (Agent != null) { NormalSpeed = Agent.speed; NormalAcceleration = Agent.acceleration; } } 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) } }; ChangeState(); } private void Update() { currentState?.UpdateState(); if (ChargeCooldownTimer > 0f) { ChargeCooldownTimer -= Time.deltaTime; } } public void ChangeState() where T : ISpiderState { if (currentState != null) { currentState.ExitState(); ResetAllAnimationTriggers(); Debug.Log($"🕷️ BossSpider [Exit]: {currentState.GetType().Name}"); } currentState = stateCache[typeof(T)]; Debug.Log($"🕷️ BossSpider [Enter]: {currentState.GetType().Name}"); currentState.EnterState(); } public void TakeDamage(DamageInfo damageInfo) { currentHp -= damageInfo.amount; currentHp = Mathf.Clamp(currentHp, 0, maxHp); float hpPercentage = (currentHp / maxHp) * 100f; Debug.Log($"💥 ボスに {damageInfo.amount} ダメージ。残りHP: {hpPercentage:F1}%"); if (hpPercentage <= 25f && !triggered25) { triggered25 = true; PendingUltimate = true; TriggerPhaseTransition(); } 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(); } } public bool TriggerParryStun() { if (currentState is SpiderBiteState || currentState is SpiderChargeState) { ChangeState(); return true; } return false; } public void ResetChargeCooldown() { ChargeCooldownTimer = ChargeCooldownDuration; } 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. 生成する座標をアクティブリストに登録(この場所への他の割り込みを禁止する) ActiveWebPositions.Add(pos); // 2. 地面に予兆マークを生成 GameObject marker = null; if (warningMarkerPrefab != null) { marker = Instantiate(warningMarkerPrefab, pos + new Vector3(0, 0.02f, 0), Quaternion.identity); } yield return new WaitForSeconds(delay); if (marker != null) Destroy(marker); // 3. 本物の糸オブジェクトを生成(X軸90度回転) if (webPrefab != null) { Quaternion webRotation = Quaternion.Euler(270f, 0f, 0f); GameObject web = Instantiate(webPrefab, pos + new Vector3(0, 0.5f, 0), webRotation); // ★持続時間(duration)が終わるまで待機してからDestroyする形に変更 yield return new WaitForSeconds(duration); if (web != null) Destroy(web); } else { yield return new WaitForSeconds(duration); } // 4. 糸が消滅したので、リストから座標を削除してスペースを解放する ActiveWebPositions.Remove(pos); } 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"); } #region デバッグ用機能 /// /// Unityインスペクターの右上メニューから実行できるデバッグ用コマンドです /// [ContextMenu("🛠️ デバッグ:強制必殺技(天井ジャンプ)")] public void DebugTriggerUltimate() { if (!Application.isPlaying) { Debug.LogWarning("⚠️ ゲームが再生中(Playモード)の時のみ実行可能です。"); return; } Debug.Log("🛠️ 【デバッグ操作】必殺技の予約券を発行し、強制的に天井ジャンプステートへ遷移します。"); // 必殺技の発動権をONにし、現在の行動をキャンセルして天井ジャンプへ移行 PendingUltimate = true; ChangeState(); } #endregion } /// 共通データ(boss参照)を自動保持し、各子ステートの無駄なコードを極限まで削ります。 public abstract class SpiderStateBase : ISpiderState { protected readonly BossController boss; protected SpiderStateBase(BossController owner) { boss = owner; } public virtual void EnterState() { } public abstract void UpdateState(); public virtual void ExitState() { } } public interface ISpiderState { void EnterState(); void UpdateState(); void ExitState(); }