Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user