using System.Collections;
using System.Collections.Generic;
using Platformer.Gameplay;
using UnityEngine;
using static Platformer.Core.Simulation;
namespace Platformer.Mechanics
{
///
/// A simple controller for enemies. Provides movement control over a patrol path.
///
[RequireComponent(typeof(AnimationController), typeof(Collider2D))]
public class EnemyController : MonoBehaviour
{
public AudioClip ouch;
/// BT5-Dev #16 — Distance 기반 감지 영역 X 임계값 (전체 폭). 표준 platformer Enemy 옆 닿음 영역 0.6~0.8.
public float hitRangeX = 0.7f;
/// Y 임계값. 위/아래 둘 다 인정.
public float hitRangeY = 1.0f;
/// 밟기 판정 — Player가 Enemy보다 위 거리. 발 닿는 느낌 영역(0.05~0.15).
public float stompMinDy = 0.1f;
// PD 명시 2026-05-08 — 자동 patrol (생성 위치 기준 좌/우 random 50~75 왕복·BT87 절반 정정)
public float patrolMinRange = 50f;
public float patrolMaxRange = 75f;
public float patrolArriveThreshold = 0.5f;
public float cliffCheckDistance = 1.0f; // BT92: 0.8→1.0 — 더 일찍 검출
public float cliffCheckDepth = 2.0f;
public LayerMask groundLayerMask = (1 << 0) | (1 << 16); // Layer 0 (지면) + Layer 16 (발판)
public float stuckThresholdTime = 0.15f; // BT93: 0.3→0.15 (150ms·밀림 누적 차단)
public float stuckMoveThreshold = 0.02f; // 정지 판정 거리 임계값
public float waitMinTime = 1f; // patrol arrive·벽·절벽 후 대기 random 영역
public float waitMaxTime = 3f;
private float _startX;
private float _startY; // BT102: 시작 시 y 위치 — 떨어짐 검출 기준
public float fallThreshold = 1.0f; // BT102: _startY - fallThreshold 영역 미만 시 시작 위치 텔레포트
private float _targetX;
private int _patrolPhase; // 0: right out / 1: right back / 2: left out / 3: left back
private float _lastX; // 벽 정지 검출용
private float _stuckTimer; // 벽 정지 누적 시간
private float _waitTimer; // arrive 후 대기 누적 시간 (1~3초 random)
private float _phaseCooldown; // phase 전환 직후 절벽·벽 검출 비활성 시간
private const float PHASE_COOLDOWN = 1.0f; // BT95: 0.5→1.0 — 긴 영역 cooldown (좌우 반복 영구 차단)
private float _maxRightRange; // 시작 시 측정 — 우측 안전 patrol 거리
private float _maxLeftRange; // 시작 시 측정 — 좌측 안전 patrol 거리
private bool _isInitialized; // BT97: Start 측정 완료 영역 (Awake 이후 활성)
internal AnimationController control;
internal Collider2D _collider;
internal AudioSource _audio;
SpriteRenderer spriteRenderer;
Rigidbody2D _body; // BT96: KinematicObject body 영역 직접 set 위해 캐싱
public Bounds Bounds => _collider.bounds;
///
/// PD 지시 2026-05-07 — Enemy 시각(SpriteRenderer) 영역 기반 충돌 감지용. CapsuleCollider2D는 작은 ground sensor 영역(0.45×0.09)이라 감지에 부적합.
/// SpriteRenderer 부재 시 Collider Bounds로 fallback.
///
public Bounds VisualBounds => spriteRenderer != null ? spriteRenderer.bounds : _collider.bounds;
PlayerController _cachedPlayer;
bool _ignoreCollisionApplied;
void Awake()
{
control = GetComponent();
_collider = GetComponent();
_audio = GetComponent();
spriteRenderer = GetComponent();
_body = GetComponent();
// BT5-Dev #21 — Awake 시점 fallback 추가 (Player tag 영역 미설정 영역 대비)
var playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj == null)
{
var pcfb = Object.FindFirstObjectByType();
if (pcfb != null) playerObj = pcfb.gameObject;
}
if (playerObj != null && _collider != null)
{
var pc = playerObj.GetComponent();
if (pc != null)
{
Physics2D.IgnoreCollision(_collider, pc, true);
_ignoreCollisionApplied = true;
}
}
// PD 명시 2026-05-08 — 자동 patrol 시작 위치 저장 (측정·target은 Start 시점)
_startX = transform.position.x;
_startY = transform.position.y; // BT102: 떨어짐 검출 기준
_lastX = _startX;
_stuckTimer = 0f;
_phaseCooldown = 0f;
_patrolPhase = 0;
_isInitialized = false;
}
// BT97 — Start 시점 안전 거리 측정 (AutoForeground Tile data 활성 후·AfterSceneLoad 이후)
// BT104 — 시작 위치 발판 검증·자동 재배치 (PD가 PD Foreground·빈 영역 배치 시 가까운 발판 영역으로 이동)
void Start()
{
// BT109 — Raycast 영역 폐기 (BT104 영역 부정확). Tilemap cell 기반 시작 위치 발판 검증.
_startY = transform.position.y;
_maxRightRange = MeasureSafeWalkDistance(1f);
_maxLeftRange = MeasureSafeWalkDistance(-1f);
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
Debug.Log($"[Enemy@{name}] startX={_startX:F2} startY={_startY:F2} maxR={_maxRightRange:F2} maxL={_maxLeftRange:F2}");
#endif
SetNextPatrolTarget();
_isInitialized = true;
}
// BT96 — 절벽·벽 검출 시 즉시 반대 방향 이동 (transform + body 동시 push·KinematicObject 정합)
void TriggerReverse(float moveDir, float pushDistance)
{
_patrolPhase = (_patrolPhase + 2) % 4;
SetNextPatrolTarget();
// transform + body 동시 push (KinematicObject body.position 영역 동기화)
Vector3 newPos = transform.position + new Vector3(-moveDir * pushDistance, 0f, 0f);
transform.position = newPos;
if (_body != null) _body.position = newPos;
// velocity.x 즉시 반대 방향 + move.x 반대 (다음 frame 안정 이동)
if (control != null)
{
control.velocity = new Vector2(-moveDir * control.maxSpeed, control.velocity.y);
control.move.x = -moveDir;
}
_stuckTimer = 0f;
_phaseCooldown = PHASE_COOLDOWN;
_lastX = transform.position.x;
}
// BT107 — Tilemap 영역 cell 기반 측정 (PD 근본 진단: Raycast 영역 자체 부정확)
// Enemy 시작 위치 cell → 좌·우 연속 Tile 영역 끝 영역까지 거리 = 안전 patrol 거리
// BT110 (2026-05-08) — 다중 footPos offset fallback (PD 보고: 16건 중 8~9건 이동 X)
// 근본: Enemy 인스턴스마다 collider bounds·sprite bounds·transform.y 영역 변동
// → 다단 fallback (sprite/collider/transform × 다중 offset) 첫 HasTile cell 채택
float MeasureSafeWalkDistance(float dir)
{
if (_collider == null) return 0f;
var groundTilemaps = new System.Collections.Generic.List();
var levelGo = GameObject.Find("Level");
if (levelGo != null)
{
var t = levelGo.GetComponent();
if (t != null) groundTilemaps.Add(t);
}
var autoFgGo = GameObject.Find("AutoForeground");
if (autoFgGo != null)
{
var t = autoFgGo.GetComponent();
if (t != null) groundTilemaps.Add(t);
}
if (groundTilemaps.Count == 0) return 0f;
// BT110 — 다중 footPos offset fallback
// sprite·collider·transform 각 영역 발 후보 + 다중 offset (-0.1·-0.3·-0.5·-0.7·-1.0)
// 각 후보 → 모든 groundTilemap HasTile 검사 → 첫 HasTile cell 채택
float colliderFootY = _collider.bounds.min.y;
float spriteFootY = spriteRenderer != null ? spriteRenderer.bounds.min.y : colliderFootY;
float transformY = transform.position.y;
float[] candidateYs = new float[] {
spriteFootY - 0.1f, spriteFootY - 0.3f, spriteFootY - 0.5f, spriteFootY - 0.7f, spriteFootY - 1.0f,
colliderFootY - 0.1f, colliderFootY - 0.3f, colliderFootY - 0.5f, colliderFootY - 0.7f, colliderFootY - 1.0f,
transformY - 0.1f, transformY - 0.3f, transformY - 0.5f, transformY - 0.7f, transformY - 1.0f
};
UnityEngine.Tilemaps.Tilemap startTm = null;
Vector3Int startCell = Vector3Int.zero;
float chosenFootY = 0f;
foreach (float candY in candidateYs)
{
Vector3 footPos = new Vector3(_startX, candY, 0f);
foreach (var tm in groundTilemaps)
{
Vector3Int cell = tm.WorldToCell(footPos);
if (tm.HasTile(cell))
{
startTm = tm;
startCell = cell;
chosenFootY = candY;
break;
}
}
if (startTm != null) break;
}
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
Debug.Log($"[Enemy@{name}] dir={dir} startY={transformY:F2} colliderFoot={colliderFootY:F2} spriteFoot={spriteFootY:F2} chosenFootY={chosenFootY:F2} startTm={(startTm!=null?startTm.name:"NULL")} startCell={startCell}");
#endif
if (startTm == null) return 0f;
// 좌·우 연속 Tile 영역 끝 영역 검색
// BT111 (2026-05-08) — 위·아래 1 cell 인접 cell도 검색 (계단 영역 발판 연속 정합)
// PD 보고: 좁은 영역 1 cell 폭 발판 위 Enemy patrol 거리 0
// 근본: 좌·우 same-y cell 즉시 break → 계단·1 cell 폭 영역 발판 연속 미감지
// 정정: 다음 same-y / +1 / -1 중 어느 곳이라도 HasTile = 발판 연속
int dirCell = (int)Mathf.Sign(dir);
Vector3Int cellIter = startCell;
for (int steps = 0; steps < 500; steps++)
{
Vector3Int nextSame = cellIter + new Vector3Int(dirCell, 0, 0);
Vector3Int nextUp = nextSame + Vector3Int.up;
Vector3Int nextDown = nextSame + Vector3Int.down;
if (HasTileInAnyTilemap(groundTilemaps, nextSame)) cellIter = nextSame;
else if (HasTileInAnyTilemap(groundTilemaps, nextUp)) cellIter = nextUp;
else if (HasTileInAnyTilemap(groundTilemaps, nextDown)) cellIter = nextDown;
else break;
}
// 마지막 Tile cell 영역 center 영역까지 거리
Vector3 lastTileWorld = startTm.CellToWorld(cellIter) + new Vector3(0.5f, 0f, 0f);
float distance = Mathf.Abs(lastTileWorld.x - _startX);
return Mathf.Max(0f, distance - 0.5f); // 안전 margin 0.5m (Tile cell 영역 안)
}
static bool HasTileInAnyTilemap(System.Collections.Generic.List tms, Vector3Int cell)
{
foreach (var tm in tms) if (tm != null && tm.HasTile(cell)) return true;
return false;
}
void SetNextPatrolTarget()
{
float range = Random.Range(patrolMinRange, patrolMaxRange);
switch (_patrolPhase)
{
case 0: // 우측 random — 안전 거리 cap
_targetX = _startX + Mathf.Min(range, _maxRightRange);
break;
case 1:
_targetX = _startX;
break;
case 2: // 좌측 random — 안전 거리 cap
_targetX = _startX - Mathf.Min(range, _maxLeftRange);
break;
case 3:
_targetX = _startX;
break;
}
}
// BT89 — patrol·벽·절벽 검출·대기 영역 통합 처리
void UpdatePatrol()
{
// BT97 — Start 측정 완료 전까지 patrol 비활성 (AutoForeground Tile data 영역 활성 대기)
if (!_isInitialized)
{
if (control != null) control.move.x = 0f;
return;
}
// BT107 — BT106 y 강제 고정 영역 폐기 (PD 보고: 공중 부유 영역 원인)
// 대기 영역 — control.move.x = 0 + Timer 감소
if (_waitTimer > 0f)
{
_waitTimer -= Time.deltaTime;
if (control != null) control.move.x = 0f;
_lastX = transform.position.x;
_stuckTimer = 0f;
return;
}
// BT94 — phase cooldown 감소 (절벽·벽 검출 비활성 시간)
if (_phaseCooldown > 0f) _phaseCooldown -= Time.deltaTime;
float dx = _targetX - transform.position.x;
// patrol arrive — 1~3초 대기 후 다음 phase
if (Mathf.Abs(dx) < patrolArriveThreshold)
{
_patrolPhase = (_patrolPhase + 1) % 4;
SetNextPatrolTarget();
_waitTimer = Random.Range(waitMinTime, waitMaxTime);
if (control != null) control.move.x = 0f;
_lastX = transform.position.x;
_stuckTimer = 0f;
return;
}
float moveDir = Mathf.Sign(dx);
// BT90 — 수평 Raycast 영역 폐기 (BT89 거짓 양성 — 같은 Tile cell 영역 검출)
// 벽 영역 = stuckTimer 영역 (50ms 정지 시 즉시 phase+2)으로 처리
// BT94 — 절벽·벽 검출은 phase cooldown 영역 끝난 후 활성 (좌우 반복 차단)
if (_phaseCooldown <= 0f)
{
// BT96 — 절벽·벽 검출: transform + body 동시 push (가장자리에서 안전 영역으로 즉시 이동)
if (_collider != null)
{
Vector2 footAhead = new Vector2(
_collider.bounds.center.x + moveDir * cliffCheckDistance,
_collider.bounds.min.y + 0.05f
);
RaycastHit2D groundHit = Physics2D.Raycast(footAhead, Vector2.down, cliffCheckDepth, groundLayerMask);
if (groundHit.collider == null)
{
TriggerReverse(moveDir, 0.3f);
return;
}
}
if (Mathf.Abs(transform.position.x - _lastX) < stuckMoveThreshold)
{
_stuckTimer += Time.deltaTime;
if (_stuckTimer > stuckThresholdTime)
{
TriggerReverse(moveDir, 0.2f);
return;
}
}
else
{
_stuckTimer = 0f;
}
}
else
{
_stuckTimer = 0f;
}
_lastX = transform.position.x;
if (control != null) control.move.x = Mathf.Clamp(dx, -1, 1);
}
void Update()
{
// BT89 — 자동 patrol + 즉시 벽 검출 + 1~3초 대기 영역
UpdatePatrol();
// BT102 — 떨어짐 검출 영역: y < _startY - fallThreshold 시 = 시작 위치 텔레포트 복귀
// PD 명시 (2026-05-08): 투명벽 폐기·떨어짐 차단 다른 방법
// 단순·근본 방법 — 떨어진 후 검출 영역 즉시 복귀 (영구 떨어짐 X)
if (_isInitialized && transform.position.y < _startY - fallThreshold)
{
Vector3 safe = new Vector3(_startX, _startY, transform.position.z);
transform.position = safe;
if (_body != null) _body.position = safe;
if (control != null) control.velocity = Vector2.zero;
_patrolPhase = 0;
SetNextPatrolTarget();
_phaseCooldown = PHASE_COOLDOWN;
_stuckTimer = 0f;
_lastX = _startX;
_waitTimer = Random.Range(waitMinTime, waitMaxTime);
if (control != null) control.move.x = 0f;
}
// 이하 — Player 충돌 검출 영역 (patrol 영역과 분리)
// PD 지시 2026-05-07 — Player ↔ Enemy 통과 가능이지만 Bounds.Intersects로 매 프레임 감지
if (_cachedPlayer == null)
{
// 1차: tag 영역 발견
var pgo = GameObject.FindGameObjectWithTag("Player");
if (pgo != null) _cachedPlayer = pgo.GetComponent();
// 2차 fallback: tag 영역 미설정 영역에 대비해 PlayerController 영역 직접 검색
if (_cachedPlayer == null)
{
_cachedPlayer = Object.FindFirstObjectByType();
}
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
if (Time.frameCount % 60 == 0)
{
int allCount = Object.FindObjectsByType(FindObjectsSortMode.None).Length;
Debug.Log($"[BT17-Update@{name}] f={Time.frameCount} cached={(_cachedPlayer != null ? _cachedPlayer.name : "NULL")} pgoTag={(pgo != null ? pgo.name : "NULL")} allPCcount={allCount}");
}
#endif
}
// BT20 — IgnoreCollision 영역 Awake 시점 Player 발견 X 영역 fallback. Update 영역에서 발견 직후 1회 영역 호출.
if (_cachedPlayer != null && !_ignoreCollisionApplied && _collider != null)
{
var pc = _cachedPlayer.GetComponent();
if (pc != null)
{
Physics2D.IgnoreCollision(_collider, pc, true);
_ignoreCollisionApplied = true;
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
Debug.Log($"[BT20-Ignore@{name}] Player↔Enemy IgnoreCollision applied");
#endif
}
}
if (_cachedPlayer != null && _cachedPlayer.health != null && _cachedPlayer.health.IsAlive)
{
// BT5-Dev #36 — Distance 영역 폐기 → Bounds.Intersects 표준 영역 (Unity Physics AABB 정확)
// Player 빠른 통과 시 한 프레임 catch 정합. Layer 13↔14 OFF 영역 통과 가능 영역 그대로.
bool inRange = VisualBounds.Intersects(_cachedPlayer.Bounds);
const float PLAYER_COLLIDER_TO_VISUAL_FOOT = -0.17f;
float footY = _cachedPlayer.Bounds.min.y + PLAYER_COLLIDER_TO_VISUAL_FOOT;
float headY = VisualBounds.max.y;
float footHeadDelta = footY - headY;
if (inRange != _diagWasIntersecting)
{
#if UNITY_EDITOR && ENEMY_DIAG_VERBOSE
Debug.Log($"[EnemyDiag@{name}] inRange={inRange} footHeadDelta={footHeadDelta:F2} VB={VisualBounds.size} PB={_cachedPlayer.Bounds.size}");
#endif
_diagWasIntersecting = inRange;
}
if (inRange)
{
var ev = Schedule();
ev.player = _cachedPlayer;
ev.enemy = this;
ev.dyAtCollision = footHeadDelta;
}
}
}
bool _diagWasIntersecting;
void OnDrawGizmos()
{
// BT5-Dev #15 진단 — Scene 영역 시각화 (Editor에서만 표시)
if (Application.isPlaying && spriteRenderer != null)
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(VisualBounds.center, VisualBounds.size);
}
if (Application.isPlaying && _collider != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(_collider.bounds.center, _collider.bounds.size);
}
}
}
}