convert game playing..
This commit is contained in:
@@ -6,6 +6,7 @@ using BumpCombat.Constants;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Player;
|
||||
using BumpCombat.Progression;
|
||||
using BumpCombat.Stage;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Enemies
|
||||
@@ -49,6 +50,7 @@ namespace BumpCombat.Enemies
|
||||
Active,
|
||||
Recovery,
|
||||
Dead,
|
||||
Idle,
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D), typeof(SpriteRenderer))]
|
||||
@@ -68,6 +70,7 @@ namespace BumpCombat.Enemies
|
||||
private static readonly int HurtParameter = Animator.StringToHash("Hurt");
|
||||
private static readonly int DeathState = Animator.StringToHash("Base Layer.Death");
|
||||
private static readonly Collider2D[] SeparationBuffer = new Collider2D[16];
|
||||
private static readonly Dictionary<int, HashSet<EnemyController>> EncounterGroups = new();
|
||||
private static Sprite launchWhiteSprite;
|
||||
private static int nextSpawnLifetimeIdentity;
|
||||
[SerializeField] private EnemyDefinition definition;
|
||||
@@ -85,6 +88,9 @@ namespace BumpCombat.Enemies
|
||||
private EnemyAttack enemyAttack;
|
||||
private EnemyModel enemyModel;
|
||||
private ArtifactStatusVisual statusVisual;
|
||||
private readonly List<Renderer> fogRenderers = new();
|
||||
private readonly List<bool> fogRendererForceOffStates = new();
|
||||
private bool fogVisibilityHidden;
|
||||
private ActiveArtifactController activeArtifactController;
|
||||
private Transform player;
|
||||
private PlayerHealth playerHealth;
|
||||
@@ -97,6 +103,7 @@ namespace BumpCombat.Enemies
|
||||
private float igniteNextTickIn;
|
||||
private float igniteTickInterval;
|
||||
private float igniteTickDamage;
|
||||
private EnemyDamageSource igniteDamageSource;
|
||||
private float shockRemaining;
|
||||
private float shockIncrease;
|
||||
private float vulnerabilityRemaining;
|
||||
@@ -123,6 +130,7 @@ namespace BumpCombat.Enemies
|
||||
private IEnemyDamageGate damageGate;
|
||||
private IEnemyAttackPatternGate attackPatternGate;
|
||||
private IEnemyActionGate actionGate;
|
||||
private BumpCombat.Defense.DefenseRouteAgent defenseRouteAgent;
|
||||
private bool summoned;
|
||||
private bool phaseSummon;
|
||||
private bool summonedEventLike;
|
||||
@@ -142,6 +150,15 @@ namespace BumpCombat.Enemies
|
||||
private readonly HashSet<string> recentArtifactContactKeys = new(StringComparer.Ordinal);
|
||||
private readonly Queue<string> recentArtifactContactOrder = new();
|
||||
private const int MaximumRememberedArtifactContacts = 128;
|
||||
private const int RoamTargetSelectionAttempts = 12;
|
||||
private bool hasDetectedPlayer;
|
||||
private int encounterGroupId = -1;
|
||||
private bool hasRoamAnchor;
|
||||
private Vector2 roamAnchor;
|
||||
private Vector2 roamTarget;
|
||||
private float roamPauseRemaining;
|
||||
private float sightMemoryRemaining;
|
||||
private Vector2 lastSeenPlayerPosition;
|
||||
|
||||
public static int AliveCount { get; private set; }
|
||||
public static int CrowdAliveCount { get; private set; }
|
||||
@@ -160,6 +177,8 @@ namespace BumpCombat.Enemies
|
||||
public float CurrentHealth { get; private set; }
|
||||
public float LastAppliedDamage { get; private set; }
|
||||
public float LastReportedDamage { get; private set; }
|
||||
public EnemyDamageSource LastDamageSource { get; private set; }
|
||||
public float MaximumHealthAtDeath { get; private set; }
|
||||
public int CurrentAttackPatternIndex => attackPatternIndex;
|
||||
public EnemyAttackPattern CurrentAttackPattern => definition != null
|
||||
? definition.GetAttackPattern(attackPatternIndex)
|
||||
@@ -216,6 +235,12 @@ namespace BumpCombat.Enemies
|
||||
public Vector2 GroundAnchorPosition => body != null
|
||||
? body.position
|
||||
: transform.position;
|
||||
public Transform CurrentVisualTransform => airborneVisualObject != null
|
||||
? airborneVisualObject.transform
|
||||
: transform;
|
||||
public int CurrentVisualSortingOrder => airborneVisualObject != null
|
||||
? spriteRenderer.sortingOrder + 2
|
||||
: spriteRenderer.sortingOrder;
|
||||
public bool IsKnockbackActive => knockbackVelocity.sqrMagnitude > 0.0025f;
|
||||
public bool IsLaunchVisualActive => launchVisualCoroutine != null;
|
||||
public bool IsAttackSequenceInProgress => State == EnemyState.Warning
|
||||
@@ -228,6 +253,11 @@ namespace BumpCombat.Enemies
|
||||
public EnemyController SummonOwner => summonOwner;
|
||||
public bool IsProtectedFromCleanup => runEventRole.HasValue || phaseSummon;
|
||||
public bool HasContactDamage => definition != null && definition.HasContactDamage;
|
||||
public bool IsFlying => definition != null && definition.IsFlying;
|
||||
public Collider2D BodyCollider => bodyCollider != null ? bodyCollider : (bodyCollider = GetComponent<Collider2D>());
|
||||
public bool HasDetectedPlayer => hasDetectedPlayer;
|
||||
public int EncounterGroupId => encounterGroupId;
|
||||
public float SightRange => enemyModel != null ? enemyModel.SightRange : (definition != null ? definition.SightRange : 8f);
|
||||
public bool IsRangedAttackStartAllowed => definition == null
|
||||
|| !definition.IsRanged
|
||||
|| CanStartRangedAttack();
|
||||
@@ -369,6 +399,21 @@ namespace BumpCombat.Enemies
|
||||
}
|
||||
#endif
|
||||
|
||||
public void ConfigureEncounterGroup(int groupId, Vector2 anchor)
|
||||
{
|
||||
UnregisterEncounterGroup();
|
||||
encounterGroupId = groupId;
|
||||
roamAnchor = anchor;
|
||||
roamTarget = anchor;
|
||||
hasRoamAnchor = true;
|
||||
RegisterEncounterGroup();
|
||||
}
|
||||
|
||||
public void ConfigureSightRangeMultiplier(float multiplier)
|
||||
{
|
||||
EnsureEnemyModel().ConfigureSightRangeMultiplier(multiplier);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
nextSpawnLifetimeIdentity = nextSpawnLifetimeIdentity == int.MaxValue
|
||||
@@ -386,6 +431,7 @@ namespace BumpCombat.Enemies
|
||||
}
|
||||
bodyCollider = GetComponent<Collider2D>();
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
CacheFogRenderers();
|
||||
animator = GetComponent<Animator>();
|
||||
enemyAttack = GetComponent<EnemyAttack>();
|
||||
ConfigureEnemyModel();
|
||||
@@ -433,6 +479,12 @@ namespace BumpCombat.Enemies
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (fogVisibilityHidden)
|
||||
{
|
||||
ForceFogRenderersOff();
|
||||
}
|
||||
|
||||
RegisterEncounterGroup();
|
||||
if (enemyModel != null)
|
||||
{
|
||||
enemyModel.OnStatChanged += HandleCharacterStatChanged;
|
||||
@@ -455,12 +507,20 @@ namespace BumpCombat.Enemies
|
||||
}
|
||||
|
||||
KeepBodyInsideArena();
|
||||
if (IsFlying)
|
||||
{
|
||||
StageObstacle.RegisterFlyingEnemy(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeGroggyTierFromDefinition();
|
||||
KeepBodyInsideArena();
|
||||
if (IsFlying)
|
||||
{
|
||||
StageObstacle.RegisterFlyingEnemy(this);
|
||||
}
|
||||
if (definition == null)
|
||||
{
|
||||
enabled = false;
|
||||
@@ -477,8 +537,16 @@ namespace BumpCombat.Enemies
|
||||
player = playerController.transform;
|
||||
playerHealth = playerController.GetComponent<PlayerHealth>();
|
||||
activeArtifactController = playerController.GetComponent<ActiveArtifactController>();
|
||||
if (!hasRoamAnchor)
|
||||
{
|
||||
roamAnchor = body != null ? body.position : (Vector2)transform.position;
|
||||
roamTarget = roamAnchor;
|
||||
hasRoamAnchor = true;
|
||||
}
|
||||
InitializeInitialShieldColor();
|
||||
CurrentHealth = MaximumHealth;
|
||||
LastDamageSource = EnemyDamageSource.None;
|
||||
MaximumHealthAtDeath = 0f;
|
||||
float summonDuration = summoned ? GetSummonAnimationDuration() : 0f;
|
||||
bool playSummonAnimation = summonDuration > 0f;
|
||||
EnterState(
|
||||
@@ -507,6 +575,7 @@ namespace BumpCombat.Enemies
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnregisterEncounterGroup();
|
||||
if (enemyModel != null)
|
||||
{
|
||||
enemyModel.OnStatChanged -= HandleCharacterStatChanged;
|
||||
@@ -601,6 +670,13 @@ namespace BumpCombat.Enemies
|
||||
return;
|
||||
}
|
||||
|
||||
defenseRouteAgent ??= GetComponent<BumpCombat.Defense.DefenseRouteAgent>();
|
||||
if (defenseRouteAgent != null
|
||||
&& defenseRouteAgent.TryDriveFixedUpdate(Time.fixedDeltaTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AllowsStateMovement(State, AttackShape))
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
@@ -611,12 +687,35 @@ namespace BumpCombat.Enemies
|
||||
{
|
||||
case EnemyState.Spawn:
|
||||
if (stateTimer <= 0f)
|
||||
{
|
||||
if (CheckPlayerDetection())
|
||||
{
|
||||
EnterState(EnemyState.Chase, 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnterState(EnemyState.Idle, 0f);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EnemyState.Idle:
|
||||
SetAttacking(false);
|
||||
if (CheckPlayerDetection())
|
||||
{
|
||||
EnterState(EnemyState.Chase, 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateRoaming();
|
||||
}
|
||||
break;
|
||||
|
||||
case EnemyState.Chase:
|
||||
if (!RefreshChaseAwareness())
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (actionGate != null && actionGate.IsAttackLocked)
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
@@ -731,7 +830,8 @@ namespace BumpCombat.Enemies
|
||||
Vector2 knockbackDirection,
|
||||
float knockbackForce,
|
||||
bool playHurtAnimation = true,
|
||||
bool isNormalBump = false)
|
||||
bool isNormalBump = false,
|
||||
EnemyDamageSource damageSource = EnemyDamageSource.None)
|
||||
{
|
||||
return TryTakeDamageWithEffects(
|
||||
damage,
|
||||
@@ -741,6 +841,7 @@ namespace BumpCombat.Enemies
|
||||
isNormalBump,
|
||||
IsShocked,
|
||||
IsBumpVulnerable,
|
||||
damageSource,
|
||||
out _);
|
||||
}
|
||||
|
||||
@@ -752,9 +853,11 @@ namespace BumpCombat.Enemies
|
||||
bool isNormalBump,
|
||||
bool isShockedAtHit,
|
||||
bool isBumpVulnerableAtHit,
|
||||
EnemyDamageSource damageSource,
|
||||
out bool noDamageAfterEffects)
|
||||
{
|
||||
noDamageAfterEffects = false;
|
||||
Alert();
|
||||
if (IsDead || damage <= 0f)
|
||||
{
|
||||
LastAppliedDamage = 0f;
|
||||
@@ -802,13 +905,16 @@ namespace BumpCombat.Enemies
|
||||
? damageTaken
|
||||
: calculatedDamage;
|
||||
float healthBeforeDamage = CurrentHealth;
|
||||
float maximumHealthBeforeDamage = MaximumHealth;
|
||||
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageTaken);
|
||||
LastAppliedDamage = healthBeforeDamage - CurrentHealth;
|
||||
LastDamageSource = damageSource;
|
||||
OnDamageApplied?.Invoke(healthBeforeDamage, LastAppliedDamage);
|
||||
bool damageReactionBlocked = damageGate != null
|
||||
&& damageGate.IsDamageBlocked;
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
MaximumHealthAtDeath = maximumHealthBeforeDamage;
|
||||
Die();
|
||||
}
|
||||
else if (damageReactionBlocked)
|
||||
@@ -817,13 +923,13 @@ namespace BumpCombat.Enemies
|
||||
// desperation phase. Do not append the opening hit's
|
||||
// knockback or hurt reaction after that transition.
|
||||
knockbackVelocity = Vector2.zero;
|
||||
body.linearVelocity = Vector2.zero;
|
||||
if (body != null) body.linearVelocity = Vector2.zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (knockbackVelocity.sqrMagnitude <= 0.0025f)
|
||||
{
|
||||
knockbackStartPosition = body.position;
|
||||
knockbackStartPosition = body != null ? body.position : (Vector2)transform.position;
|
||||
}
|
||||
knockbackVelocity += knockbackDirection.normalized
|
||||
* knockbackForce
|
||||
@@ -914,7 +1020,8 @@ namespace BumpCombat.Enemies
|
||||
float knockbackForce,
|
||||
ArtifactColor artifactColor,
|
||||
int castIdentity,
|
||||
bool playHurtAnimation = true)
|
||||
bool playHurtAnimation = true,
|
||||
EnemyDamageSource damageSource = EnemyDamageSource.None)
|
||||
{
|
||||
lastArtifactContactCounted = false;
|
||||
if (IsDead)
|
||||
@@ -956,7 +1063,8 @@ namespace BumpCombat.Enemies
|
||||
knockbackDirection,
|
||||
knockbackForce,
|
||||
playHurtAnimation,
|
||||
false)
|
||||
false,
|
||||
damageSource)
|
||||
? EnemyArtifactHitResult.DamageApplied
|
||||
: EnemyArtifactHitResult.Rejected;
|
||||
}
|
||||
@@ -1004,7 +1112,8 @@ namespace BumpCombat.Enemies
|
||||
public bool ApplyIgnite(
|
||||
float duration,
|
||||
float tickInterval,
|
||||
float damagePerTick)
|
||||
float damagePerTick,
|
||||
EnemyDamageSource damageSource = EnemyDamageSource.Player)
|
||||
{
|
||||
if (IsDead
|
||||
|| duration <= 0f
|
||||
@@ -1017,6 +1126,7 @@ namespace BumpCombat.Enemies
|
||||
bool wasInactive = igniteRemaining <= 0f;
|
||||
igniteRemaining = Mathf.Max(igniteRemaining, duration);
|
||||
igniteTickDamage = Mathf.Max(igniteTickDamage, damagePerTick);
|
||||
igniteDamageSource = damageSource;
|
||||
if (wasInactive)
|
||||
{
|
||||
igniteTickInterval = tickInterval;
|
||||
@@ -1085,6 +1195,7 @@ namespace BumpCombat.Enemies
|
||||
false,
|
||||
isShockedAtTick,
|
||||
false,
|
||||
igniteDamageSource,
|
||||
out bool noDamageAfterEffects);
|
||||
if ((!damageApplied && !noDamageAfterEffects) || IsDead)
|
||||
{
|
||||
@@ -1821,19 +1932,28 @@ namespace BumpCombat.Enemies
|
||||
Vector2 attackSlot = GetHorizontalRootAttackSlot(
|
||||
attackTarget,
|
||||
direction);
|
||||
if (!IsFlying)
|
||||
{
|
||||
ZoneInstance currentZone = StageManager.Instance != null
|
||||
? StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
if (currentZone != null
|
||||
&& currentZone.Preset != null
|
||||
&& RoomPathfinder.TryGetNextWaypoint(
|
||||
currentZone,
|
||||
body.position,
|
||||
attackSlot,
|
||||
false,
|
||||
out Vector2 waypoint))
|
||||
{
|
||||
attackSlot = waypoint;
|
||||
}
|
||||
}
|
||||
Vector2 toAttackSlot = attackSlot - body.position;
|
||||
movementDirection = toAttackSlot.sqrMagnitude > 0.0001f
|
||||
? toAttackSlot.normalized
|
||||
: Vector2.zero;
|
||||
}
|
||||
if (IsCrowd)
|
||||
{
|
||||
Vector2 toApproachPoint =
|
||||
(Vector2)player.position + crowdApproachOffset - body.position;
|
||||
movementDirection = toApproachPoint.sqrMagnitude > 0.01f
|
||||
? toApproachPoint.normalized
|
||||
: Vector2.zero;
|
||||
}
|
||||
else if (definition.IsRanged && rangedAttackOutsideGate)
|
||||
{
|
||||
// A ranged attacker must enter the visible, reachable area
|
||||
@@ -1845,11 +1965,45 @@ namespace BumpCombat.Enemies
|
||||
{
|
||||
movementDirection = -direction;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 targetPoint = attackTarget;
|
||||
if (IsCrowd)
|
||||
{
|
||||
float distToPlayer = (attackTarget - body.position).magnitude;
|
||||
targetPoint = distToPlayer > 2.5f
|
||||
? attackTarget
|
||||
: (Vector2)player.position + crowdApproachOffset;
|
||||
}
|
||||
|
||||
if (!IsFlying)
|
||||
{
|
||||
ZoneInstance currentZone = StageManager.Instance != null ? StageManager.Instance.CurrentZone : null;
|
||||
if (currentZone != null && currentZone.Preset != null)
|
||||
{
|
||||
if (RoomPathfinder.TryGetNextWaypoint(
|
||||
currentZone,
|
||||
body.position,
|
||||
targetPoint,
|
||||
IsFlying,
|
||||
out Vector2 waypoint))
|
||||
{
|
||||
targetPoint = waypoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 toApproachPoint = targetPoint - body.position;
|
||||
movementDirection = toApproachPoint.sqrMagnitude > 0.01f
|
||||
? toApproachPoint.normalized
|
||||
: Vector2.zero;
|
||||
}
|
||||
|
||||
Vector2 separation = CalculateSeparation();
|
||||
Vector2 velocity = Vector2.ClampMagnitude(
|
||||
Vector2 desiredVelocity = Vector2.ClampMagnitude(
|
||||
movementDirection + separation * separationStrength,
|
||||
1f) * MoveSpeed;
|
||||
Vector2 velocity = CalculateWallSlideVelocity(desiredVelocity);
|
||||
body.MovePosition(ClampEnemyPosition(
|
||||
body.position + velocity * Time.fixedDeltaTime));
|
||||
SetMoving(velocity.sqrMagnitude > 0.0001f);
|
||||
@@ -1864,10 +2018,11 @@ namespace BumpCombat.Enemies
|
||||
if (bounds != null)
|
||||
{
|
||||
Vector2 actorExtents = bounds.SharedActorClampHalfExtents;
|
||||
Vector2 center = bounds.ZoneCenter;
|
||||
boundedTarget.y = Mathf.Clamp(
|
||||
attackTarget.y,
|
||||
-actorExtents.y,
|
||||
actorExtents.y);
|
||||
center.y - actorExtents.y,
|
||||
center.y + actorExtents.y);
|
||||
}
|
||||
|
||||
// Horizontal-root attacks measure range to the player's collider
|
||||
@@ -1919,9 +2074,10 @@ namespace BumpCombat.Enemies
|
||||
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
||||
Vector2 reachable = bounds.GetReachableHalfExtents(
|
||||
tuning.RangedApproachArenaInset);
|
||||
Vector2 center = bounds.ZoneCenter;
|
||||
Vector2 interiorTarget = new(
|
||||
Mathf.Clamp(targetPosition.x, -reachable.x, reachable.x),
|
||||
Mathf.Clamp(targetPosition.y, -reachable.y, reachable.y));
|
||||
Mathf.Clamp(targetPosition.x, center.x - reachable.x, center.x + reachable.x),
|
||||
Mathf.Clamp(targetPosition.y, center.y - reachable.y, center.y + reachable.y));
|
||||
Camera camera = Camera.main;
|
||||
if (camera != null)
|
||||
{
|
||||
@@ -1933,10 +2089,10 @@ namespace BumpCombat.Enemies
|
||||
// Keep the approach point in the intersection of the
|
||||
// reachable and visible interiors. This is essential when
|
||||
// the player is held against an outer map edge.
|
||||
float minimumX = Mathf.Max(-reachable.x, cameraCenter.x - visible.x);
|
||||
float maximumX = Mathf.Min(reachable.x, cameraCenter.x + visible.x);
|
||||
float minimumY = Mathf.Max(-reachable.y, cameraCenter.y - visible.y);
|
||||
float maximumY = Mathf.Min(reachable.y, cameraCenter.y + visible.y);
|
||||
float minimumX = Mathf.Max(center.x - reachable.x, cameraCenter.x - visible.x);
|
||||
float maximumX = Mathf.Min(center.x + reachable.x, cameraCenter.x + visible.x);
|
||||
float minimumY = Mathf.Max(center.y - reachable.y, cameraCenter.y - visible.y);
|
||||
float maximumY = Mathf.Min(center.y + reachable.y, cameraCenter.y + visible.y);
|
||||
if (minimumX <= maximumX)
|
||||
{
|
||||
interiorTarget.x = Mathf.Clamp(
|
||||
@@ -2095,6 +2251,11 @@ namespace BumpCombat.Enemies
|
||||
return cachedSeparation;
|
||||
}
|
||||
|
||||
public void SetFacingDirection(Vector2 direction)
|
||||
{
|
||||
SetFacing(direction);
|
||||
}
|
||||
|
||||
private void SetFacing(Vector2 direction)
|
||||
{
|
||||
if (direction.sqrMagnitude <= 0f)
|
||||
@@ -2126,6 +2287,276 @@ namespace BumpCombat.Enemies
|
||||
|| (state == EnemyState.Warning && attackAnimationStarted));
|
||||
}
|
||||
|
||||
public void Alert()
|
||||
{
|
||||
MarkPlayerDetected(false);
|
||||
if (State == EnemyState.Idle)
|
||||
{
|
||||
EnterState(EnemyState.Chase, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckPlayerDetection()
|
||||
{
|
||||
if (!CanCurrentlyDetectPlayer()) return false;
|
||||
MarkPlayerDetected(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CanCurrentlyDetectPlayer()
|
||||
{
|
||||
EnsurePlayerReference();
|
||||
if (player == null) return false;
|
||||
|
||||
Vector2 myPos = GroundAnchorPosition;
|
||||
float distance = Vector2.Distance(myPos, player.position);
|
||||
if (distance > SightRange) return false;
|
||||
|
||||
float proximityRange = GameplayConstants.Current.Enemies.ProximityAlertRange;
|
||||
return distance <= proximityRange
|
||||
|| RoomPathfinder.HasLineOfSight(myPos, player.position, IsFlying, bodyCollider);
|
||||
}
|
||||
|
||||
private void EnsurePlayerReference()
|
||||
{
|
||||
if (player != null) return;
|
||||
|
||||
PlayerController playerController = FindAnyObjectByType<PlayerController>();
|
||||
if (playerController == null) return;
|
||||
|
||||
player = playerController.transform;
|
||||
playerHealth = playerController.GetComponent<PlayerHealth>();
|
||||
activeArtifactController = playerController.GetComponent<ActiveArtifactController>();
|
||||
}
|
||||
|
||||
private void MarkPlayerDetected(bool alertGroup)
|
||||
{
|
||||
EnsurePlayerReference();
|
||||
hasDetectedPlayer = true;
|
||||
sightMemoryRemaining = GameplayConstants.Current.Enemies.LostSightMemoryDuration;
|
||||
if (player != null)
|
||||
{
|
||||
lastSeenPlayerPosition = player.position;
|
||||
}
|
||||
|
||||
if (alertGroup && encounterGroupId >= 0
|
||||
&& EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||
{
|
||||
foreach (EnemyController member in group)
|
||||
{
|
||||
if (member == null || member == this || member.IsDead || member.IsEventEnemy)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
member.MarkPlayerDetected(false);
|
||||
if (member.State == EnemyState.Idle)
|
||||
{
|
||||
member.EnterState(EnemyState.Chase, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool RefreshChaseAwareness()
|
||||
{
|
||||
if (IsEventEnemy)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CanCurrentlyDetectPlayer())
|
||||
{
|
||||
MarkPlayerDetected(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
sightMemoryRemaining = Mathf.Max(0f, sightMemoryRemaining - Time.fixedDeltaTime);
|
||||
if (sightMemoryRemaining > 0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector2 toLastSeen = lastSeenPlayerPosition - body.position;
|
||||
if (toLastSeen.sqrMagnitude <= 0.16f)
|
||||
{
|
||||
hasDetectedPlayer = false;
|
||||
body.linearVelocity = Vector2.zero;
|
||||
SetMoving(false);
|
||||
EnterState(EnemyState.Idle, 0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
MoveTowardPoint(lastSeenPlayerPosition, MoveSpeed);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateRoaming()
|
||||
{
|
||||
if (IsEventEnemy || !hasRoamAnchor)
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
SetMoving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
||||
if (roamPauseRemaining > 0f)
|
||||
{
|
||||
roamPauseRemaining = Mathf.Max(0f, roamPauseRemaining - Time.fixedDeltaTime);
|
||||
body.linearVelocity = Vector2.zero;
|
||||
SetMoving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((roamTarget - body.position).sqrMagnitude <= 0.16f)
|
||||
{
|
||||
if (!TryChooseRoamTarget(tuning))
|
||||
{
|
||||
roamTarget = body.position;
|
||||
}
|
||||
roamPauseRemaining = UnityEngine.Random.Range(tuning.RoamPauseMin, tuning.RoamPauseMax);
|
||||
body.linearVelocity = Vector2.zero;
|
||||
SetMoving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MoveTowardPoint(roamTarget, MoveSpeed * 0.6f);
|
||||
}
|
||||
|
||||
private bool TryChooseRoamTarget(EnemyConstants tuning)
|
||||
{
|
||||
ZoneInstance currentZone = StageManager.Instance != null
|
||||
? StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
|
||||
for (int attempt = 0; attempt < RoamTargetSelectionAttempts; attempt++)
|
||||
{
|
||||
float radius = UnityEngine.Random.Range(tuning.RoamRadiusMin, tuning.RoamRadiusMax);
|
||||
Vector2 offset = UnityEngine.Random.insideUnitCircle.normalized * radius;
|
||||
Vector2 candidate = ClampEnemyPosition(roamAnchor + offset);
|
||||
|
||||
if (currentZone == null || currentZone.Preset == null)
|
||||
{
|
||||
roamTarget = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (RoomPathfinder.TryGetReachableRoamPoint(
|
||||
currentZone,
|
||||
body.position,
|
||||
candidate,
|
||||
IsFlying,
|
||||
out Vector2 reachableTarget))
|
||||
{
|
||||
roamTarget = reachableTarget;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void MoveTowardPoint(Vector2 targetPoint, float speed)
|
||||
{
|
||||
if (!IsFlying)
|
||||
{
|
||||
ZoneInstance currentZone = StageManager.Instance != null
|
||||
? StageManager.Instance.CurrentZone
|
||||
: null;
|
||||
if (currentZone != null && currentZone.Preset != null
|
||||
&& RoomPathfinder.TryGetNextWaypoint(
|
||||
currentZone,
|
||||
body.position,
|
||||
targetPoint,
|
||||
false,
|
||||
out Vector2 waypoint))
|
||||
{
|
||||
targetPoint = waypoint;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 direction = targetPoint - body.position;
|
||||
if (direction.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
SetMoving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
direction.Normalize();
|
||||
SetFacing(direction);
|
||||
Vector2 desiredVelocity = direction * speed;
|
||||
Vector2 velocity = CalculateWallSlideVelocity(desiredVelocity);
|
||||
body.MovePosition(ClampEnemyPosition(
|
||||
body.position + velocity * Time.fixedDeltaTime));
|
||||
SetMoving(velocity.sqrMagnitude > 0.0001f);
|
||||
}
|
||||
|
||||
private void RegisterEncounterGroup()
|
||||
{
|
||||
if (encounterGroupId < 0) return;
|
||||
if (!EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||
{
|
||||
group = new HashSet<EnemyController>();
|
||||
EncounterGroups.Add(encounterGroupId, group);
|
||||
}
|
||||
group.Add(this);
|
||||
}
|
||||
|
||||
private void UnregisterEncounterGroup()
|
||||
{
|
||||
if (encounterGroupId < 0
|
||||
|| !EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
group.Remove(this);
|
||||
if (group.Count == 0)
|
||||
{
|
||||
EncounterGroups.Remove(encounterGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 CalculateWallSlideVelocity(Vector2 velocity)
|
||||
{
|
||||
if (velocity.sqrMagnitude < 0.0001f) return Vector2.zero;
|
||||
|
||||
float radius = GameplayConstants.Current.Enemies.WallSlideCastRadius;
|
||||
float distance = GameplayConstants.Current.Enemies.WallSlideCastDistance;
|
||||
Vector2 dir = velocity.normalized;
|
||||
|
||||
RaycastHit2D hit = Physics2D.CircleCast(
|
||||
body.position,
|
||||
radius,
|
||||
dir,
|
||||
distance);
|
||||
|
||||
if (hit.collider != null && !hit.collider.isTrigger && hit.collider != bodyCollider)
|
||||
{
|
||||
bool isObstacle = hit.collider.GetComponentInParent<StageObstacle>() != null;
|
||||
bool isWall = hit.collider.name.StartsWith("Wall_") || hit.collider.gameObject.name.Contains("Wall");
|
||||
|
||||
if ((isObstacle && !IsFlying) || isWall)
|
||||
{
|
||||
Vector2 normal = hit.normal;
|
||||
Vector2 tangent = new Vector2(-normal.y, normal.x);
|
||||
if (Vector2.Dot(velocity, tangent) < 0f)
|
||||
{
|
||||
tangent = -tangent;
|
||||
}
|
||||
|
||||
float forwardProjection = Vector2.Dot(velocity, tangent);
|
||||
if (forwardProjection > 0.01f)
|
||||
{
|
||||
return tangent * (forwardProjection * 1.05f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public static bool ShouldStartAttackAnimation(
|
||||
float warningTimeRemaining,
|
||||
float leadTime,
|
||||
@@ -2294,6 +2725,89 @@ namespace BumpCombat.Enemies
|
||||
ReleaseSpriteVisibilityLease(reason);
|
||||
}
|
||||
|
||||
public void SetFogVisibility(bool visible)
|
||||
{
|
||||
if (visible)
|
||||
{
|
||||
if (!fogVisibilityHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < fogRenderers.Count; i++)
|
||||
{
|
||||
if (fogRenderers[i] != null)
|
||||
{
|
||||
fogRenderers[i].forceRenderingOff = fogRendererForceOffStates[i];
|
||||
}
|
||||
}
|
||||
|
||||
fogRenderers.Clear();
|
||||
fogRendererForceOffStates.Clear();
|
||||
fogVisibilityHidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fogVisibilityHidden)
|
||||
{
|
||||
CacheFogRenderers();
|
||||
fogVisibilityHidden = true;
|
||||
}
|
||||
|
||||
ForceFogRenderersOff();
|
||||
}
|
||||
|
||||
private void CacheFogRenderers()
|
||||
{
|
||||
if (fogVisibilityHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fogRenderers.Clear();
|
||||
fogRendererForceOffStates.Clear();
|
||||
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
|
||||
for (int i = 0; i < renderers.Length; i++)
|
||||
{
|
||||
fogRenderers.Add(renderers[i]);
|
||||
fogRendererForceOffStates.Add(renderers[i].forceRenderingOff);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceFogRenderersOff()
|
||||
{
|
||||
for (int i = 0; i < fogRenderers.Count; i++)
|
||||
{
|
||||
if (fogRenderers[i] != null)
|
||||
{
|
||||
fogRenderers[i].forceRenderingOff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTransformChildrenChanged()
|
||||
{
|
||||
if (!fogVisibilityHidden)
|
||||
{
|
||||
CacheFogRenderers();
|
||||
return;
|
||||
}
|
||||
|
||||
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
|
||||
for (int i = 0; i < renderers.Length; i++)
|
||||
{
|
||||
if (fogRenderers.Contains(renderers[i]))
|
||||
{
|
||||
renderers[i].forceRenderingOff = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
fogRenderers.Add(renderers[i]);
|
||||
fogRendererForceOffStates.Add(renderers[i].forceRenderingOff);
|
||||
renderers[i].forceRenderingOff = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AcquireSpriteVisibilityLease(string reason)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
|
||||
Reference in New Issue
Block a user