Files

527 lines
19 KiB
C#

using System.Collections.Generic;
using BumpCombat.Core;
using BumpCombat.Constants;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Combat
{
[RequireComponent(typeof(PlayerController))]
[RequireComponent(typeof(PlayerStats))]
public sealed class BumpCombatResolver : MonoBehaviour
{
private float baseDamage => GameplayConstants.Current.Combat.BaseDamage;
private float dashDamage => GameplayConstants.Current.Combat.DashDamage;
private float strongDashDamage => GameplayConstants.Current.Combat.StrongDashDamage;
private float minimumAttackSpeed => GameplayConstants.Current.Combat.MinimumAttackSpeed;
private float minimumApproachDot => GameplayConstants.Current.Combat.MinimumApproachDot;
private float knockbackForce => GameplayConstants.Current.Combat.KnockbackForce;
private float dashKnockbackForce => GameplayConstants.Current.Combat.DashKnockbackForce;
private float strongDashKnockbackForce => GameplayConstants.Current.Combat.StrongDashKnockbackForce;
private float strongDashLaunchHeight => GameplayConstants.Current.Combat.StrongDashLaunchHeight;
private float strongDashLaunchDuration => GameplayConstants.Current.Combat.StrongDashLaunchDuration;
private float playerImpactRecoilDistance => GameplayConstants.Current.Combat.PlayerImpactRecoilDistance;
private float frontKnockbackMultiplier => GameplayConstants.Current.Combat.FrontKnockbackMultiplier;
private float frontPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.FrontPlayerImpactRecoilDistance;
private float eventBackPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.EventBackPlayerImpactRecoilDistance;
private float shieldedPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.ShieldedPlayerImpactRecoilDistance;
private float minimumOffsetDistance => GameplayConstants.Current.Combat.MinimumOffsetDistance;
private float normalHitInterval => GameplayConstants.Current.Combat.NormalHitInterval;
private float dashContactSkin => GameplayConstants.Current.Combat.DashContactSkin;
private readonly HashSet<int> dashTargets = new();
private readonly RaycastHit2D[] dashHits = new RaycastHit2D[64];
private readonly Dictionary<int, float> nextNormalHitTimes = new();
private PlayerStats playerStats;
private PlayerController playerController;
private DashController dashController;
private ActiveArtifactController activeArtifactController;
private CircleCollider2D bodyCollider;
private void Awake()
{
playerStats = GetComponent<PlayerStats>();
playerController = GetComponent<PlayerController>();
dashController = GetComponent<DashController>();
activeArtifactController = GetComponent<ActiveArtifactController>();
bodyCollider = GetComponent<CircleCollider2D>();
}
public void BeginDashPathContactSession()
{
dashTargets.Clear();
}
private void OnCollisionEnter2D(Collision2D collision)
{
TryResolveNormalCollision(collision);
}
private void OnCollisionStay2D(Collision2D collision)
{
TryResolveNormalCollision(collision);
}
private void OnTriggerEnter2D(Collider2D other)
{
TryResolveCrowdTrigger(other);
}
private void OnTriggerStay2D(Collider2D other)
{
TryResolveCrowdTrigger(other);
}
private void TryResolveNormalCollision(
Collision2D collision)
{
EnemyController enemy = collision.collider.GetComponentInParent<EnemyController>();
if (enemy == null || enemy.IsCrowd)
{
return;
}
Vector2 hitPosition = collision.contactCount > 0
? collision.GetContact(0).point
: Vector2.Lerp(transform.position, enemy.transform.position, 0.5f);
TryResolveNormalContact(enemy, hitPosition);
}
private void TryResolveCrowdTrigger(
Collider2D other)
{
EnemyController enemy = other.GetComponentInParent<EnemyController>();
if (enemy == null || !enemy.IsCrowd)
{
return;
}
Vector2 hitPosition = Vector2.Lerp(
transform.position,
enemy.transform.position,
0.5f);
TryResolveNormalContact(enemy, hitPosition);
}
private void TryResolveNormalContact(
EnemyController enemy,
Vector2 hitPosition)
{
if (!RunManager.GameplayInputEnabled
|| playerController.IsHurtMovementLocked
|| playerController.IsDamageKnockbackActive
|| dashController.IsDashing
|| (activeArtifactController != null
&& activeArtifactController.LocksMovement))
{
return;
}
if (enemy.IsDead)
{
return;
}
if (nextNormalHitTimes.TryGetValue(
enemy.GetInstanceID(),
out float nextNormalHitTime)
&& Time.time < nextNormalHitTime)
{
return;
}
Vector2 toEnemy = ((Vector2)enemy.transform.position - (Vector2)transform.position).normalized;
if (playerController.AttackIntentVelocity.magnitude < minimumAttackSpeed
|| playerController.MoveDirection.sqrMagnitude <= 0f
|| Vector2.Dot(playerController.MoveDirection, toEnemy) < minimumApproachDot)
{
return;
}
Vector2 enemyToPlayer =
((Vector2)transform.position - (Vector2)enemy.transform.position).normalized;
HitSide side = BumpCombatMath.ClassifySide(
enemy.FacingDirection,
enemyToPlayer);
if (enemy.IsGroggyTierGated && enemy.IsDamageInvulnerable)
{
playerController.ApplyImpactRecoil(
enemyToPlayer,
shieldedPlayerImpactRecoilDistance);
nextNormalHitTimes[enemy.GetInstanceID()] =
Time.time + normalHitInterval;
CombatFeedback.ShowBlockedBump(
gameObject,
hitPosition,
playerController.MoveDirection);
return;
}
ResolveEnemy(
enemy,
false,
hitPosition,
transform.position,
playerController.MoveDirection,
side);
}
public void ResolveDashPath(
Vector2 start,
Vector2 direction,
float distance,
bool isStrongDash = false,
float? damageOverride = null,
float? knockbackOverride = null,
string sourceId = null,
DamageTag damageTag = DamageTag.Collision,
ArtifactColor? artifactColor = null,
int castIdentity = 0,
ActiveArtifactEffect? artifactEffect = null)
{
ResolveDashPathInternal(
start,
direction,
distance,
isStrongDash,
damageOverride,
knockbackOverride,
sourceId,
damageTag,
artifactColor,
castIdentity,
artifactEffect,
true);
}
public void ResolveDashPathSegment(
Vector2 start,
Vector2 direction,
float distance,
bool isStrongDash = false,
float? damageOverride = null,
float? knockbackOverride = null,
string sourceId = null,
DamageTag damageTag = DamageTag.Collision,
ArtifactColor? artifactColor = null,
int castIdentity = 0,
ActiveArtifactEffect? artifactEffect = null)
{
ResolveDashPathInternal(
start,
direction,
distance,
isStrongDash,
damageOverride,
knockbackOverride,
sourceId,
damageTag,
artifactColor,
castIdentity,
artifactEffect,
false);
}
private void ResolveDashPathInternal(
Vector2 start,
Vector2 direction,
float distance,
bool isStrongDash,
float? damageOverride,
float? knockbackOverride,
string sourceId,
DamageTag damageTag,
ArtifactColor? artifactColor,
int castIdentity,
ActiveArtifactEffect? artifactEffect,
bool resetDashTargets)
{
if (resetDashTargets)
{
dashTargets.Clear();
}
Vector2 castStart = start;
float radius = GameplayConstants.Current.Combat.DashFallbackCastRadius;
if (bodyCollider != null)
{
Vector2 colliderOffset =
(Vector2)bodyCollider.bounds.center - (Vector2)transform.position;
castStart += colliderOffset;
radius = Mathf.Max(
bodyCollider.bounds.extents.x,
bodyCollider.bounds.extents.y)
+ dashContactSkin;
}
int hitCount = Physics2D.CircleCast(
castStart,
radius,
direction,
ContactFilter2D.noFilter,
dashHits,
distance);
for (int i = 0; i < hitCount; i++)
{
EnemyController enemy = dashHits[i].collider.GetComponentInParent<EnemyController>();
if (enemy == null || enemy.IsDead || !dashTargets.Add(enemy.GetInstanceID()))
{
continue;
}
Vector2 hitPosition = dashHits[i].point;
if (hitPosition == Vector2.zero)
{
hitPosition = dashHits[i].collider.ClosestPoint(
castStart + direction * dashHits[i].distance);
}
Vector2 attackerPositionAtHit =
castStart + direction * dashHits[i].distance;
ResolveEnemy(
enemy,
true,
hitPosition,
attackerPositionAtHit,
direction,
null,
isStrongDash,
damageOverride,
knockbackOverride,
sourceId,
damageTag,
artifactColor,
castIdentity,
artifactEffect);
}
}
private bool ResolveEnemy(
EnemyController enemy,
bool isDash,
Vector2 hitPosition,
Vector2 attackerPosition,
Vector2 attackDirection,
HitSide? knownSide = null,
bool isStrongDash = false,
float? damageOverride = null,
float? knockbackOverride = null,
string sourceId = null,
DamageTag damageTag = DamageTag.Collision,
ArtifactColor? artifactColor = null,
int castIdentity = 0,
ActiveArtifactEffect? artifactEffect = null)
{
Vector2 enemyToPlayer =
(attackerPosition - (Vector2)enemy.transform.position).normalized;
HitSide side = knownSide
?? BumpCombatMath.ClassifySide(enemy.FacingDirection, enemyToPlayer);
bool isEventEnemy = enemy.IsEventEnemy;
bool isOffsetHit = !isDash && BumpCombatMath.IsOffsetHit(
side,
attackDirection,
attackerPosition,
enemy.transform.position,
minimumOffsetDistance);
float rawDamage = damageOverride
?? (isStrongDash
? strongDashDamage
: isDash
? dashDamage
: baseDamage);
float finalDamage = BumpCombatMath.ApplySideMultiplier(
playerStats.CalculateDamage(rawDamage, damageTag),
side,
isOffsetHit);
Vector2 knockbackDirection = -enemyToPlayer;
bool suppressOrdinaryEventReaction = !isDash && isEventEnemy;
float appliedKnockback = knockbackOverride
?? (isStrongDash
? strongDashKnockbackForce
: isDash
? dashKnockbackForce
: side == HitSide.Front
? knockbackForce * frontKnockbackMultiplier
: knockbackForce);
if (suppressOrdinaryEventReaction)
{
appliedKnockback = 0f;
}
EnemyArtifactHitResult artifactHitResult =
EnemyArtifactHitResult.Rejected;
bool acceptedHit;
if (artifactColor.HasValue)
{
artifactHitResult = enemy.TryTakeArtifactHit(
finalDamage,
knockbackDirection,
appliedKnockback,
artifactColor.Value,
castIdentity,
!suppressOrdinaryEventReaction);
acceptedHit = artifactHitResult
!= EnemyArtifactHitResult.Rejected;
}
else
{
acceptedHit = enemy.TryTakeDamage(
finalDamage,
knockbackDirection,
appliedKnockback,
!suppressOrdinaryEventReaction,
!isDash);
}
if (!acceptedHit)
{
return false;
}
if (artifactHitResult == EnemyArtifactHitResult.Shielded)
{
if (enemy.LastArtifactContactCounted
&& artifactEffect.HasValue)
{
CombatEvents.RaiseArtifactEffectiveHit(
enemy.gameObject,
castIdentity,
artifactEffect.Value,
isStrongDash,
true);
CombatFeedback.ShowArtifactImpact(
CombatHitResult.ForArtifactContact(
gameObject,
enemy.gameObject,
artifactEffect.Value,
isStrongDash,
side,
knockbackDirection,
hitPosition,
damageTag,
sourceId,
isOffsetHit));
}
// The contact has already advanced the groggy stagger pip.
// A shielded dash must not become a bump, interrupt, launch,
// recoil, gauge charge, or damage-hit feedback.
return false;
}
float damageTaken = enemy.LastAppliedDamage;
if (damageTaken <= 0f)
{
return false;
}
if (artifactColor.HasValue && artifactEffect.HasValue)
{
CombatEvents.RaiseArtifactEffectiveHit(
enemy.gameObject,
castIdentity,
artifactEffect.Value,
isStrongDash,
false);
}
float reportedDamage = enemy.LastReportedDamage;
bool protectionBegan = enemy.IsDamageInvulnerable;
if (protectionBegan)
{
appliedKnockback = 0f;
}
if (!isDash)
{
playerController.TryPlayBumpAttackAnimation(
side == HitSide.Back);
}
if (!isDash)
{
nextNormalHitTimes[enemy.GetInstanceID()] =
Time.time + normalHitInterval;
}
bool launchSucceeded = false;
if (!protectionBegan && isStrongDash)
{
enemy.TryInterruptAttackFromLaunch();
if (CanLaunchAfterHit(enemy.CanBeLaunched, enemy.IsDead))
{
launchSucceeded = true;
enemy.PlayLaunchVisual(
knockbackDirection,
strongDashLaunchHeight,
strongDashLaunchDuration);
}
else if (ShouldShowLaunchResist(enemy.CanBeLaunched))
{
CombatEvents.RaiseLaunchResisted(enemy.gameObject);
}
}
else if (!protectionBegan && side == HitSide.Back)
{
bool broke = enemy.TryInterruptAttackFromBackHit();
if (broke && !isDash)
{
CombatEvents.RaiseRearAttackCancelled(enemy.gameObject);
}
if (suppressOrdinaryEventReaction && broke)
{
enemy.PlayHurtAnimation();
}
}
if (!isDash && !protectionBegan)
{
if (!enemy.IsCrowd
&& BumpCombatMath.AppliesPlayerImpactRecoil(side, isEventEnemy))
{
playerController.ApplyImpactRecoil(
enemyToPlayer,
side == HitSide.Front
? frontPlayerImpactRecoilDistance
: side == HitSide.Back && isEventEnemy
? eventBackPlayerImpactRecoilDistance
: playerImpactRecoilDistance);
}
activeArtifactController?.AddNormalHitCharge();
}
CombatEvents.RaiseValidHit(new CombatHitResult(
gameObject,
enemy.gameObject,
isDash,
side,
reportedDamage,
knockbackDirection,
appliedKnockback,
hitPosition,
isOffsetHit,
isStrongDash,
damageTag,
sourceId,
true,
true,
launchSucceeded,
artifactEffect.HasValue,
artifactEffect.GetValueOrDefault(),
artifactEffect.HasValue && isStrongDash));
return true;
}
public static bool CanLaunchAfterHit(bool canBeLaunched, bool isDead)
{
return canBeLaunched && !isDead;
}
public static bool ShouldShowLaunchResist(bool canBeLaunched)
{
return !canBeLaunched;
}
}
}