Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 673c2c567619408db1bf9b1e3e5d07f8
|
||||
@@ -0,0 +1,278 @@
|
||||
using BumpCombat.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
public enum ActiveArtifactEffect
|
||||
{
|
||||
Dash,
|
||||
Pulse,
|
||||
Phoenix,
|
||||
Cyclone,
|
||||
ThunderCrash,
|
||||
ChainLightning,
|
||||
}
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "ActiveArtifact",
|
||||
menuName = "BumpCombat/Active Artifact")]
|
||||
public sealed class ActiveArtifactDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string artifactId;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private string placeholderSymbol;
|
||||
[SerializeField] private ActiveArtifactEffect effect;
|
||||
[SerializeField] private DamageTag damageTag = DamageTag.Collision;
|
||||
[SerializeField] private Color iconColor = Color.white;
|
||||
[SerializeField] private Color effectColor = Color.white;
|
||||
[SerializeField, Min(0f)] private float normalGaugeCost = 25f;
|
||||
[SerializeField, Min(0f)] private float chargedGaugeCost = 60f;
|
||||
[SerializeField, Min(0.01f)] private float chargeDuration = 0.8f;
|
||||
[SerializeField, Min(0f)] private float normalDamage = 15f;
|
||||
[SerializeField, Min(0f)] private float chargedDamage = 30f;
|
||||
[SerializeField, Min(0f)] private float normalRange = 1.25f;
|
||||
[SerializeField, Min(0f)] private float chargedRange = 2f;
|
||||
[SerializeField, Min(0f)] private float normalKnockback = 1.5f;
|
||||
[SerializeField, Min(0f)] private float chargedKnockback = 3f;
|
||||
[SerializeField, Min(0f)] private float normalWidth = 0.5f;
|
||||
[SerializeField, Min(0f)] private float chargedWidth = 1f;
|
||||
[SerializeField, Min(0f)] private float normalMoveDistance;
|
||||
[SerializeField, Min(0f)] private float chargedMoveDistance;
|
||||
[SerializeField, Min(0f)] private float normalDuration = 0.15f;
|
||||
[SerializeField, Min(0f)] private float chargedDuration = 0.5f;
|
||||
[SerializeField, Min(0.01f)] private float hitInterval = 0.15f;
|
||||
[SerializeField, Min(1)] private int normalMaxTargets = 1;
|
||||
[SerializeField, Min(1)] private int chargedMaxTargets = 12;
|
||||
[SerializeField, Min(1)] private int chargedMaxHitsPerTarget = 1;
|
||||
[SerializeField, Min(0f)] private float chainRange = 2.5f;
|
||||
[SerializeField, Min(1)] private int normalChainDepth = 4;
|
||||
[SerializeField, Min(1)] private int chargedChainDepth = 4;
|
||||
[SerializeField, Min(1)] private int normalBranchCount = 1;
|
||||
[SerializeField, Min(1)] private int chargedBranchCount = 2;
|
||||
[SerializeField, Range(0f, 1f)] private float damageMultiplierPerChain = 0.9f;
|
||||
[SerializeField, Min(0f)] private float normalInvulnerabilityDuration;
|
||||
[SerializeField, Min(0f)] private float chargedInvulnerabilityDuration;
|
||||
[SerializeField, Range(0f, 1f)] private float normalDamageReduction;
|
||||
[SerializeField, Range(0f, 1f)] private float chargedDamageReduction;
|
||||
[SerializeField, Min(0f)] private float normalBuffDuration;
|
||||
[SerializeField, Min(0f)] private float chargedBuffDuration;
|
||||
[SerializeField, Range(0f, 1f)] private float normalMoveSpeedIncrease;
|
||||
[SerializeField, Range(0f, 1f)] private float chargedMoveSpeedIncrease;
|
||||
[SerializeField, Min(0f)] private float normalIgniteDuration;
|
||||
[SerializeField, Min(0f)] private float chargedIgniteDuration;
|
||||
[SerializeField, Min(0f)] private float normalIgniteInterval = 0.5f;
|
||||
[SerializeField, Min(0f)] private float chargedIgniteInterval = 0.5f;
|
||||
[SerializeField, Min(0f)] private float normalIgniteTickDamage;
|
||||
[SerializeField, Min(0f)] private float chargedIgniteTickDamage;
|
||||
[SerializeField, Range(0f, 1f)] private float normalVulnerabilityIncrease;
|
||||
[SerializeField, Range(0f, 1f)] private float chargedVulnerabilityIncrease;
|
||||
[SerializeField, Min(0f)] private float normalVulnerabilityDuration;
|
||||
[SerializeField, Min(0f)] private float chargedVulnerabilityDuration;
|
||||
[SerializeField, Range(0f, 1f)] private float normalShockChance;
|
||||
[SerializeField, Range(0f, 1f)] private float chargedShockChance;
|
||||
[SerializeField, Range(0f, 1f)] private float normalShockIncrease;
|
||||
[SerializeField, Range(0f, 1f)] private float chargedShockIncrease;
|
||||
[SerializeField, Min(0f)] private float normalShockDuration;
|
||||
[SerializeField, Min(0f)] private float chargedShockDuration;
|
||||
|
||||
public string ArtifactId => artifactId;
|
||||
public string DisplayName => displayName;
|
||||
public string PlaceholderSymbol => placeholderSymbol;
|
||||
public ActiveArtifactEffect Effect => effect;
|
||||
public ArtifactColor ArtifactColor => GetArtifactColor(effect);
|
||||
public DamageTag DamageTag => damageTag;
|
||||
public Color IconColor => iconColor;
|
||||
public Color EffectColor => effectColor;
|
||||
public float NormalGaugeCost => normalGaugeCost;
|
||||
public float ChargedGaugeCost => chargedGaugeCost;
|
||||
public float ChargeDuration => chargeDuration;
|
||||
public float NormalDamage => normalDamage;
|
||||
public float ChargedDamage => chargedDamage;
|
||||
public float NormalRange => normalRange;
|
||||
public float ChargedRange => chargedRange;
|
||||
public float NormalKnockback => normalKnockback;
|
||||
public float ChargedKnockback => chargedKnockback;
|
||||
public float NormalWidth => normalWidth;
|
||||
public float ChargedWidth => chargedWidth;
|
||||
public float NormalMoveDistance => normalMoveDistance;
|
||||
public float ChargedMoveDistance => chargedMoveDistance;
|
||||
public float NormalDuration => normalDuration;
|
||||
public float ChargedDuration => chargedDuration;
|
||||
public float HitInterval => hitInterval;
|
||||
public int NormalMaxTargets => normalMaxTargets;
|
||||
public int ChargedMaxTargets => chargedMaxTargets;
|
||||
public int ChargedMaxHitsPerTarget => chargedMaxHitsPerTarget;
|
||||
public float ChainRange => chainRange;
|
||||
public int NormalChainDepth => normalChainDepth;
|
||||
public int ChargedChainDepth => chargedChainDepth;
|
||||
public int NormalBranchCount => normalBranchCount;
|
||||
public int ChargedBranchCount => chargedBranchCount;
|
||||
public float DamageMultiplierPerChain => damageMultiplierPerChain;
|
||||
public float NormalInvulnerabilityDuration => normalInvulnerabilityDuration;
|
||||
public float ChargedInvulnerabilityDuration => chargedInvulnerabilityDuration;
|
||||
public float NormalDamageReduction => normalDamageReduction;
|
||||
public float ChargedDamageReduction => chargedDamageReduction;
|
||||
public float NormalBuffDuration => normalBuffDuration;
|
||||
public float ChargedBuffDuration => chargedBuffDuration;
|
||||
public float NormalMoveSpeedIncrease => normalMoveSpeedIncrease;
|
||||
public float ChargedMoveSpeedIncrease => chargedMoveSpeedIncrease;
|
||||
public float NormalIgniteDuration => normalIgniteDuration;
|
||||
public float ChargedIgniteDuration => chargedIgniteDuration;
|
||||
public float NormalIgniteInterval => normalIgniteInterval;
|
||||
public float ChargedIgniteInterval => chargedIgniteInterval;
|
||||
public float NormalIgniteTickDamage => normalIgniteTickDamage;
|
||||
public float ChargedIgniteTickDamage => chargedIgniteTickDamage;
|
||||
public float NormalVulnerabilityIncrease => normalVulnerabilityIncrease;
|
||||
public float ChargedVulnerabilityIncrease => chargedVulnerabilityIncrease;
|
||||
public float NormalVulnerabilityDuration => normalVulnerabilityDuration;
|
||||
public float ChargedVulnerabilityDuration => chargedVulnerabilityDuration;
|
||||
public float NormalShockChance => normalShockChance;
|
||||
public float ChargedShockChance => chargedShockChance;
|
||||
public float NormalShockIncrease => normalShockIncrease;
|
||||
public float ChargedShockIncrease => chargedShockIncrease;
|
||||
public float NormalShockDuration => normalShockDuration;
|
||||
public float ChargedShockDuration => chargedShockDuration;
|
||||
|
||||
public static ArtifactColor GetArtifactColor(ActiveArtifactEffect effectKind)
|
||||
{
|
||||
return effectKind switch
|
||||
{
|
||||
ActiveArtifactEffect.Dash
|
||||
or ActiveArtifactEffect.Cyclone => ArtifactColor.Green,
|
||||
ActiveArtifactEffect.Pulse
|
||||
or ActiveArtifactEffect.Phoenix => ArtifactColor.Red,
|
||||
ActiveArtifactEffect.ThunderCrash
|
||||
or ActiveArtifactEffect.ChainLightning => ArtifactColor.Blue,
|
||||
_ => ArtifactColor.Green,
|
||||
};
|
||||
}
|
||||
|
||||
public static Color32 GetArtifactPaletteColor(ArtifactColor color)
|
||||
{
|
||||
return color switch
|
||||
{
|
||||
ArtifactColor.Green => new Color32(103, 231, 178, 255),
|
||||
ArtifactColor.Red => new Color32(255, 100, 100, 255),
|
||||
_ => new Color32(104, 179, 255, 255),
|
||||
};
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void Configure(
|
||||
string id,
|
||||
string name,
|
||||
string symbol,
|
||||
ActiveArtifactEffect effectKind,
|
||||
DamageTag tag,
|
||||
Color icon,
|
||||
Color effectTint,
|
||||
float normalCost,
|
||||
float chargedCost,
|
||||
float chargeTime,
|
||||
float normalAttackDamage,
|
||||
float chargedAttackDamage,
|
||||
float normalEffectRange,
|
||||
float chargedEffectRange,
|
||||
float normalKnockbackForce,
|
||||
float chargedKnockbackForce,
|
||||
float normalEffectWidth = 0.5f,
|
||||
float chargedEffectWidth = 1f,
|
||||
float normalMovementDistance = 0f,
|
||||
float chargedMovementDistance = 0f,
|
||||
float normalEffectDuration = 0.15f,
|
||||
float chargedEffectDuration = 0.5f,
|
||||
float multiHitInterval = 0.15f,
|
||||
int normalTargetLimit = 1,
|
||||
int chargedTargetLimit = 12,
|
||||
float chainHopRange = 2.5f,
|
||||
int normalMaximumChainDepth = 4,
|
||||
int chargedMaximumChainDepth = 4,
|
||||
int normalBranches = 1,
|
||||
int chargedBranches = 2,
|
||||
float chainDamageMultiplier = 0.9f,
|
||||
float normalInvulnerability = 0f,
|
||||
float chargedInvulnerability = 0f,
|
||||
int chargedHitLimitPerTarget = 1,
|
||||
float normalIncomingDamageReduction = 0f,
|
||||
float chargedIncomingDamageReduction = 0f,
|
||||
float normalPlayerBuffDuration = 0f,
|
||||
float chargedPlayerBuffDuration = 0f,
|
||||
float normalMovementSpeedIncrease = 0f,
|
||||
float chargedMovementSpeedIncrease = 0f,
|
||||
float normalBurnDuration = 0f,
|
||||
float chargedBurnDuration = 0f,
|
||||
float normalBurnInterval = 0.5f,
|
||||
float chargedBurnInterval = 0.5f,
|
||||
float normalBurnTickDamage = 0f,
|
||||
float chargedBurnTickDamage = 0f,
|
||||
float normalBumpVulnerabilityIncrease = 0f,
|
||||
float chargedBumpVulnerabilityIncrease = 0f,
|
||||
float normalBumpVulnerabilityDuration = 0f,
|
||||
float chargedBumpVulnerabilityDuration = 0f,
|
||||
float normalElectrocuteChance = 0f,
|
||||
float chargedElectrocuteChance = 0f,
|
||||
float normalElectrocuteIncrease = 0f,
|
||||
float chargedElectrocuteIncrease = 0f,
|
||||
float normalElectrocuteDuration = 0f,
|
||||
float chargedElectrocuteDuration = 0f)
|
||||
{
|
||||
artifactId = id;
|
||||
displayName = name;
|
||||
placeholderSymbol = symbol;
|
||||
effect = effectKind;
|
||||
damageTag = tag;
|
||||
iconColor = icon;
|
||||
effectColor = effectTint;
|
||||
normalGaugeCost = normalCost;
|
||||
chargedGaugeCost = chargedCost;
|
||||
chargeDuration = chargeTime;
|
||||
normalDamage = normalAttackDamage;
|
||||
chargedDamage = chargedAttackDamage;
|
||||
normalRange = normalEffectRange;
|
||||
chargedRange = chargedEffectRange;
|
||||
normalKnockback = normalKnockbackForce;
|
||||
chargedKnockback = chargedKnockbackForce;
|
||||
normalWidth = normalEffectWidth;
|
||||
chargedWidth = chargedEffectWidth;
|
||||
normalMoveDistance = normalMovementDistance;
|
||||
chargedMoveDistance = chargedMovementDistance;
|
||||
normalDuration = normalEffectDuration;
|
||||
chargedDuration = chargedEffectDuration;
|
||||
hitInterval = multiHitInterval;
|
||||
normalMaxTargets = normalTargetLimit;
|
||||
chargedMaxTargets = chargedTargetLimit;
|
||||
chainRange = chainHopRange;
|
||||
normalChainDepth = normalMaximumChainDepth;
|
||||
chargedChainDepth = chargedMaximumChainDepth;
|
||||
normalBranchCount = normalBranches;
|
||||
chargedBranchCount = chargedBranches;
|
||||
damageMultiplierPerChain = chainDamageMultiplier;
|
||||
normalInvulnerabilityDuration = normalInvulnerability;
|
||||
chargedInvulnerabilityDuration = chargedInvulnerability;
|
||||
chargedMaxHitsPerTarget = chargedHitLimitPerTarget;
|
||||
normalDamageReduction = normalIncomingDamageReduction;
|
||||
chargedDamageReduction = chargedIncomingDamageReduction;
|
||||
normalBuffDuration = normalPlayerBuffDuration;
|
||||
chargedBuffDuration = chargedPlayerBuffDuration;
|
||||
normalMoveSpeedIncrease = normalMovementSpeedIncrease;
|
||||
chargedMoveSpeedIncrease = chargedMovementSpeedIncrease;
|
||||
normalIgniteDuration = normalBurnDuration;
|
||||
chargedIgniteDuration = chargedBurnDuration;
|
||||
normalIgniteInterval = normalBurnInterval;
|
||||
chargedIgniteInterval = chargedBurnInterval;
|
||||
normalIgniteTickDamage = normalBurnTickDamage;
|
||||
chargedIgniteTickDamage = chargedBurnTickDamage;
|
||||
normalVulnerabilityIncrease = normalBumpVulnerabilityIncrease;
|
||||
chargedVulnerabilityIncrease = chargedBumpVulnerabilityIncrease;
|
||||
normalVulnerabilityDuration = normalBumpVulnerabilityDuration;
|
||||
chargedVulnerabilityDuration = chargedBumpVulnerabilityDuration;
|
||||
normalShockChance = normalElectrocuteChance;
|
||||
chargedShockChance = chargedElectrocuteChance;
|
||||
normalShockIncrease = normalElectrocuteIncrease;
|
||||
chargedShockIncrease = chargedElectrocuteIncrease;
|
||||
normalShockDuration = normalElectrocuteDuration;
|
||||
chargedShockDuration = chargedElectrocuteDuration;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01e09802b0334be29db2e63037c28782
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.Collections;
|
||||
using BumpCombat.Combat;
|
||||
using BumpCombat.Constants;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
[RequireComponent(typeof(Rigidbody2D), typeof(PlayerController))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
public sealed class DashController : MonoBehaviour
|
||||
{
|
||||
private float dashDuration => GameplayConstants.Current.Artifacts.DashDuration;
|
||||
private float strongDashDuration => GameplayConstants.Current.Artifacts.ChargedDashDuration;
|
||||
|
||||
private Rigidbody2D body;
|
||||
private PlayerController playerController;
|
||||
private BumpCombatResolver combatResolver;
|
||||
private Animator animator;
|
||||
private CombatFeedback combatFeedback;
|
||||
private static readonly int IsDashingParameter = Animator.StringToHash("IsDashing");
|
||||
|
||||
public bool IsDashing { get; private set; }
|
||||
public bool IsStrongDashing { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
body = GetComponent<Rigidbody2D>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
combatResolver = GetComponent<BumpCombatResolver>();
|
||||
animator = GetComponent<Animator>();
|
||||
combatFeedback = GetComponent<CombatFeedback>();
|
||||
if (combatFeedback == null)
|
||||
{
|
||||
combatFeedback = gameObject.AddComponent<CombatFeedback>();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryArtifactDash(
|
||||
bool isCharged,
|
||||
ActiveArtifactDefinition definition,
|
||||
int castIdentity = 0)
|
||||
{
|
||||
if (definition == null
|
||||
|| IsDashing
|
||||
|| playerController == null
|
||||
|| playerController.IsDamageKnockbackActive
|
||||
|| playerController.IsHurtMovementLocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StartCoroutine(DashRoutine(
|
||||
GetDashDirection(),
|
||||
isCharged,
|
||||
definition,
|
||||
castIdentity));
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2 GetDashDirection()
|
||||
{
|
||||
return playerController.MoveDirection.sqrMagnitude > 0f
|
||||
? playerController.MoveDirection.normalized
|
||||
: playerController.FacingDirection.normalized;
|
||||
}
|
||||
|
||||
private IEnumerator DashRoutine(
|
||||
Vector2 direction,
|
||||
bool isStrongDash,
|
||||
ActiveArtifactDefinition artifactDefinition,
|
||||
int castIdentity)
|
||||
{
|
||||
IsDashing = true;
|
||||
IsStrongDashing = isStrongDash;
|
||||
animator.SetBool(IsDashingParameter, true);
|
||||
float distance = isStrongDash
|
||||
? artifactDefinition.ChargedRange
|
||||
: artifactDefinition.NormalRange;
|
||||
float duration = isStrongDash
|
||||
? strongDashDuration
|
||||
: dashDuration;
|
||||
Vector2 start = body.position;
|
||||
Vector2 end = playerController.ClampToArena(start + direction * distance);
|
||||
|
||||
combatResolver.BeginDashPathContactSession();
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
yield return new WaitForFixedUpdate();
|
||||
combatFeedback.EmitDashAfterimage(body.position, isStrongDash);
|
||||
Vector2 segmentStart = body.position;
|
||||
elapsed += Time.fixedDeltaTime;
|
||||
Vector2 segmentEnd = Vector2.Lerp(
|
||||
start,
|
||||
end,
|
||||
Mathf.Clamp01(elapsed / duration));
|
||||
Vector2 segment = segmentEnd - segmentStart;
|
||||
if (segment.sqrMagnitude > 0.000001f)
|
||||
{
|
||||
combatResolver.ResolveDashPathSegment(
|
||||
segmentStart,
|
||||
segment.normalized,
|
||||
segment.magnitude,
|
||||
isStrongDash,
|
||||
isStrongDash
|
||||
? artifactDefinition.ChargedDamage
|
||||
: artifactDefinition.NormalDamage,
|
||||
isStrongDash
|
||||
? artifactDefinition.ChargedKnockback
|
||||
: artifactDefinition.NormalKnockback,
|
||||
artifactDefinition.ArtifactId,
|
||||
artifactDefinition.DamageTag,
|
||||
artifactDefinition.ArtifactColor,
|
||||
castIdentity,
|
||||
artifactDefinition.Effect);
|
||||
}
|
||||
|
||||
body.MovePosition(segmentEnd);
|
||||
}
|
||||
|
||||
body.position = end;
|
||||
IsDashing = false;
|
||||
IsStrongDashing = false;
|
||||
animator.SetBool(IsDashingParameter, false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77ef8e2be8dd8104ba9fe7c7d8113073
|
||||
@@ -0,0 +1,342 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Constants;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
[RequireComponent(typeof(Rigidbody2D), typeof(SpriteRenderer), typeof(Animator))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
public sealed class PlayerController : MonoBehaviour
|
||||
{
|
||||
private static readonly int IsMovingParameter = Animator.StringToHash("IsMoving");
|
||||
private static readonly int BumpAttackState =
|
||||
Animator.StringToHash("Base Layer.BumpAttack");
|
||||
private static readonly int BackBumpAttackState =
|
||||
Animator.StringToHash("Base Layer.BackBumpAttack");
|
||||
private static readonly int ArtifactUseState =
|
||||
Animator.StringToHash("Base Layer.ArtifactUse");
|
||||
|
||||
public const string BumpAttackStateName = "BumpAttack";
|
||||
public const string BackBumpAttackStateName = "BackBumpAttack";
|
||||
public const string ArtifactUseStateName = "ArtifactUse";
|
||||
public const string HurtStateName = "Hurt";
|
||||
public const string DashStateName = "Dash";
|
||||
public const string DeathStateName = "Death";
|
||||
|
||||
private Rigidbody2D body;
|
||||
private SpriteRenderer spriteRenderer;
|
||||
private Animator animator;
|
||||
private PlayerStats playerStats;
|
||||
private DashController dashController;
|
||||
private ActiveArtifactController activeArtifactController;
|
||||
private PlayerHealth playerHealth;
|
||||
private Vector2 moveInput;
|
||||
private Vector2 pendingImpactRecoil;
|
||||
private Vector2 damageKnockbackDirection;
|
||||
private float damageKnockbackDistance;
|
||||
private float damageKnockbackDuration;
|
||||
private float damageKnockbackElapsed;
|
||||
|
||||
public Vector2 MoveDirection { get; private set; }
|
||||
public Vector2 FacingDirection { get; private set; } = Vector2.right;
|
||||
public Vector2 CurrentVelocity { get; private set; }
|
||||
public Vector2 AttackIntentVelocity { get; private set; }
|
||||
public bool IsDamageKnockbackActive { get; private set; }
|
||||
public bool IsHurtMovementLocked =>
|
||||
playerHealth != null && playerHealth.IsHurtMovementLocked;
|
||||
public Vector2 PlayerClampHalfExtents
|
||||
{
|
||||
get
|
||||
{
|
||||
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||
return bounds != null
|
||||
? bounds.PlayerClampHalfExtents
|
||||
: new Vector2(
|
||||
Mathf.Max(0f, GameplayConstants.Current.Player.ArenaHalfExtents.x
|
||||
- GameplayConstants.Current.Player.ArenaPadding.x),
|
||||
Mathf.Max(0f, GameplayConstants.Current.Player.ArenaHalfExtents.y
|
||||
- GameplayConstants.Current.Player.ArenaPadding.y));
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
body = GetComponent<Rigidbody2D>();
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
animator = GetComponent<Animator>();
|
||||
playerStats = GetComponent<PlayerStats>();
|
||||
dashController = GetComponent<DashController>();
|
||||
activeArtifactController = GetComponent<ActiveArtifactController>();
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (animator == null
|
||||
|| animator.runtimeAnimatorController == null
|
||||
|| !animator.isActiveAndEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
animator.SetBool(IsMovingParameter, false);
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
}
|
||||
|
||||
public bool TryPlayBumpAttackAnimation(bool isBackHit = false)
|
||||
{
|
||||
return TryPlayCombatAnimation(isBackHit ? BackBumpAttackState : BumpAttackState, true);
|
||||
}
|
||||
|
||||
public bool TryPlayArtifactUseAnimation()
|
||||
{
|
||||
return TryPlayCombatAnimation(ArtifactUseState, false);
|
||||
}
|
||||
|
||||
private bool TryPlayCombatAnimation(
|
||||
int stateHash,
|
||||
bool blockActiveArtifactUse)
|
||||
{
|
||||
if (animator == null
|
||||
|| animator.runtimeAnimatorController == null
|
||||
|| !animator.isActiveAndEnabled
|
||||
|| !animator.HasState(0, stateHash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AnimatorStateInfo current = animator.GetCurrentAnimatorStateInfo(0);
|
||||
AnimatorStateInfo next = animator.IsInTransition(0)
|
||||
? animator.GetNextAnimatorStateInfo(0)
|
||||
: default;
|
||||
bool priorityStateActive = IsPriorityAnimationState(current)
|
||||
|| IsPriorityAnimationState(next);
|
||||
bool artifactUseStateActive = blockActiveArtifactUse
|
||||
&& (IsAnimationState(current, ArtifactUseStateName)
|
||||
|| IsAnimationState(next, ArtifactUseStateName));
|
||||
bool isDead = playerHealth != null
|
||||
&& playerHealth.CurrentHealth <= 0f;
|
||||
bool isDashing = dashController != null && dashController.IsDashing;
|
||||
if (isDead
|
||||
|| IsHurtMovementLocked
|
||||
|| isDashing
|
||||
|| priorityStateActive
|
||||
|| artifactUseStateActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
animator.Play(stateHash, 0, 0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPriorityAnimationState(AnimatorStateInfo state)
|
||||
{
|
||||
return IsAnimationState(state, HurtStateName)
|
||||
|| IsAnimationState(state, DashStateName)
|
||||
|| IsAnimationState(state, DeathStateName);
|
||||
}
|
||||
|
||||
private static bool IsAnimationState(
|
||||
AnimatorStateInfo state,
|
||||
string stateName)
|
||||
{
|
||||
return state.IsName(stateName);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!RunManager.GameplayInputEnabled || IsHurtMovementLocked)
|
||||
{
|
||||
SetMoveInput(Vector2.zero);
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard == null)
|
||||
{
|
||||
SetMoveInput(Vector2.zero);
|
||||
return;
|
||||
}
|
||||
|
||||
float horizontal = (keyboard.rightArrowKey.isPressed ? 1f : 0f)
|
||||
- (keyboard.leftArrowKey.isPressed ? 1f : 0f);
|
||||
float vertical = (keyboard.upArrowKey.isPressed ? 1f : 0f)
|
||||
- (keyboard.downArrowKey.isPressed ? 1f : 0f);
|
||||
|
||||
SetMoveInput(Vector2.ClampMagnitude(new Vector2(horizontal, vertical), 1f));
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
AttackIntentVelocity = Vector2.zero;
|
||||
if (dashController != null && dashController.IsDashing)
|
||||
{
|
||||
pendingImpactRecoil = Vector2.zero;
|
||||
IsDamageKnockbackActive = false;
|
||||
CurrentVelocity = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 startPosition = body.position;
|
||||
if (IsDamageKnockbackActive)
|
||||
{
|
||||
UpdateDamageKnockback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeArtifactController != null
|
||||
&& activeArtifactController.LocksMovement)
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
CurrentVelocity = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsHurtMovementLocked)
|
||||
{
|
||||
body.linearVelocity = Vector2.zero;
|
||||
CurrentVelocity = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingImpactRecoil.sqrMagnitude > 0f)
|
||||
{
|
||||
Vector2 recoilTarget = ClampToArena(startPosition + pendingImpactRecoil);
|
||||
pendingImpactRecoil = Vector2.zero;
|
||||
body.linearVelocity = Vector2.zero;
|
||||
body.MovePosition(recoilTarget);
|
||||
CurrentVelocity = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
AttackIntentVelocity = moveInput * playerStats.MoveSpeed;
|
||||
Vector2 targetPosition = ClampToArena(
|
||||
startPosition + AttackIntentVelocity * Time.fixedDeltaTime);
|
||||
|
||||
body.MovePosition(targetPosition);
|
||||
CurrentVelocity = (targetPosition - startPosition) / Time.fixedDeltaTime;
|
||||
if (CurrentVelocity.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
activeArtifactController?.AddMovementCharge(Time.fixedDeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 ClampToArena(Vector2 position)
|
||||
{
|
||||
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||
return bounds != null
|
||||
? bounds.ClampPlayerPosition(position)
|
||||
: new Vector2(
|
||||
Mathf.Clamp(
|
||||
position.x,
|
||||
-GameplayConstants.Current.Player.ArenaHalfExtents.x
|
||||
+ GameplayConstants.Current.Player.ArenaPadding.x,
|
||||
GameplayConstants.Current.Player.ArenaHalfExtents.x
|
||||
- GameplayConstants.Current.Player.ArenaPadding.x),
|
||||
Mathf.Clamp(
|
||||
position.y,
|
||||
-GameplayConstants.Current.Player.ArenaHalfExtents.y
|
||||
+ GameplayConstants.Current.Player.ArenaPadding.y,
|
||||
GameplayConstants.Current.Player.ArenaHalfExtents.y
|
||||
- GameplayConstants.Current.Player.ArenaPadding.y));
|
||||
}
|
||||
|
||||
public bool TryReposition(Vector2 position)
|
||||
{
|
||||
if (body == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingImpactRecoil = Vector2.zero;
|
||||
IsDamageKnockbackActive = false;
|
||||
body.linearVelocity = Vector2.zero;
|
||||
body.position = ClampToArena(position);
|
||||
transform.position = body.position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ApplyImpactRecoil(Vector2 direction, float distance)
|
||||
{
|
||||
if (direction.sqrMagnitude <= 0f || distance <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 recoil = direction.normalized * distance;
|
||||
pendingImpactRecoil = Vector2.ClampMagnitude(
|
||||
pendingImpactRecoil + recoil,
|
||||
distance);
|
||||
}
|
||||
|
||||
public void ApplyDamageKnockback(Vector2 direction, float distance, float duration)
|
||||
{
|
||||
if (direction.sqrMagnitude <= 0f || distance <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pendingImpactRecoil = Vector2.zero;
|
||||
body.linearVelocity = Vector2.zero;
|
||||
damageKnockbackDirection = direction.normalized;
|
||||
damageKnockbackDistance = distance;
|
||||
damageKnockbackDuration = Mathf.Max(duration, Time.fixedDeltaTime);
|
||||
damageKnockbackElapsed = 0f;
|
||||
IsDamageKnockbackActive = true;
|
||||
CurrentVelocity = Vector2.zero;
|
||||
}
|
||||
|
||||
private void UpdateDamageKnockback()
|
||||
{
|
||||
float previousProgress = Mathf.Clamp01(
|
||||
damageKnockbackElapsed / damageKnockbackDuration);
|
||||
damageKnockbackElapsed = Mathf.Min(
|
||||
damageKnockbackElapsed + Time.fixedDeltaTime,
|
||||
damageKnockbackDuration);
|
||||
float currentProgress = Mathf.Clamp01(
|
||||
damageKnockbackElapsed / damageKnockbackDuration);
|
||||
float previousEase = 1f - (1f - previousProgress) * (1f - previousProgress);
|
||||
float currentEase = 1f - (1f - currentProgress) * (1f - currentProgress);
|
||||
float stepDistance = damageKnockbackDistance * (currentEase - previousEase);
|
||||
|
||||
body.linearVelocity = Vector2.zero;
|
||||
body.MovePosition(ClampToArena(
|
||||
body.position + damageKnockbackDirection * stepDistance));
|
||||
CurrentVelocity = Vector2.zero;
|
||||
if (damageKnockbackElapsed >= damageKnockbackDuration)
|
||||
{
|
||||
IsDamageKnockbackActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetMoveInput(Vector2 input)
|
||||
{
|
||||
moveInput = input;
|
||||
MoveDirection = input;
|
||||
if (IsDamageKnockbackActive
|
||||
|| IsHurtMovementLocked
|
||||
|| (activeArtifactController != null
|
||||
&& activeArtifactController.LocksMovement))
|
||||
{
|
||||
animator.SetBool(IsMovingParameter, false);
|
||||
return;
|
||||
}
|
||||
|
||||
animator.SetBool(IsMovingParameter, input.sqrMagnitude > 0f);
|
||||
|
||||
if (input.sqrMagnitude <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FacingDirection = input.normalized;
|
||||
if (!Mathf.Approximately(input.x, 0f))
|
||||
{
|
||||
spriteRenderer.flipX = input.x < 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfdd4de5148b1a64b867f0b64a67b5d8
|
||||
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using BumpCombat.Combat;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Constants;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
[RequireComponent(typeof(PlayerController), typeof(Animator))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
public sealed class PlayerHealth : MonoBehaviour
|
||||
{
|
||||
private static readonly int HurtParameter = Animator.StringToHash("Hurt");
|
||||
|
||||
// Nonserialized overrides retain isolated test control; normal gameplay
|
||||
// reads its values from the centralized PlayerConstants asset.
|
||||
private float invulnerabilityDuration = -1f;
|
||||
private float knockbackDuration = -1f;
|
||||
private float hurtMovementLockDuration = -1f;
|
||||
private float guardCooldownDuration = -1f;
|
||||
private float guardDuration = -1f;
|
||||
private float guardSuccessCooldownRechargePercent = -1f;
|
||||
|
||||
private PlayerStats playerStats;
|
||||
private PlayerController playerController;
|
||||
private Animator animator;
|
||||
private DashController dashController;
|
||||
private float invulnerableUntil;
|
||||
private float hurtMovementLockedUntil;
|
||||
private float guardUntil;
|
||||
private float guardCooldownUntil;
|
||||
private int nextGuardActivationIdentity;
|
||||
private int activeGuardActivationIdentity;
|
||||
private bool activeGuardHasBlockedAttack;
|
||||
private RunManager subscribedRunManager;
|
||||
|
||||
public event Action<float, float> OnHealthChanged;
|
||||
public event Action<Vector2, float> OnDamaged;
|
||||
/// <summary>Raised only for positive health actually lost after mitigation and lethal clamping.</summary>
|
||||
public event Action<float> OnDamageTaken;
|
||||
public event Action<int> OnGuardAttackBlocked;
|
||||
public event Action<Vector2> OnGuardImpact;
|
||||
public event Action OnDied;
|
||||
|
||||
public float MaxHealth => playerStats.MaxHealth;
|
||||
public float CurrentHealth { get; private set; }
|
||||
public static float DefaultGuardCooldown =>
|
||||
GameplayConstants.Current.Player.GuardCooldownDuration;
|
||||
public static float DefaultGuardDuration =>
|
||||
GameplayConstants.Current.Player.GuardDuration;
|
||||
public static float DefaultGuardSuccessCooldownRechargePercent =>
|
||||
GameplayConstants.Current.Player.GuardSuccessCooldownRechargePercent;
|
||||
public float InvulnerabilityDuration => Mathf.Max(
|
||||
ResolveOverride(invulnerabilityDuration, GameplayConstants.Current.Player.InvulnerabilityDuration),
|
||||
ResolveOverride(hurtMovementLockDuration, GameplayConstants.Current.Player.HurtMovementLockDuration)
|
||||
+ GameplayConstants.Current.Player.HurtRecoveryPadding);
|
||||
public bool IsHurtMovementLocked => Time.time < hurtMovementLockedUntil;
|
||||
public float GuardCooldownDuration => ResolveOverride(
|
||||
guardCooldownDuration,
|
||||
GameplayConstants.Current.Player.GuardCooldownDuration);
|
||||
public float GuardDuration => ResolveOverride(
|
||||
guardDuration,
|
||||
GameplayConstants.Current.Player.GuardDuration);
|
||||
public float GuardSuccessCooldownRechargePercent =>
|
||||
ResolveOverride(
|
||||
guardSuccessCooldownRechargePercent,
|
||||
GameplayConstants.Current.Player.GuardSuccessCooldownRechargePercent);
|
||||
public bool IsGuardAvailable => RunManager.Instance?.IsGuardUnlocked ?? true;
|
||||
public bool IsGuarding =>
|
||||
IsGuardAvailable
|
||||
&& isActiveAndEnabled
|
||||
&& CurrentHealth > 0f
|
||||
&& Time.time < guardUntil;
|
||||
public int ActiveGuardActivationIdentity =>
|
||||
IsGuarding ? activeGuardActivationIdentity : 0;
|
||||
public float GuardCooldownRemaining =>
|
||||
Mathf.Max(0f, guardCooldownUntil - Time.time);
|
||||
/// <summary>Remaining cooldown as a fraction: 1 immediately after use, 0 when ready.</summary>
|
||||
public float GuardCooldownNormalized => GuardCooldownDuration > 0f
|
||||
? Mathf.Clamp01(GuardCooldownRemaining / GuardCooldownDuration)
|
||||
: 0f;
|
||||
/// <summary>Guard readiness as a fraction: 0 during cooldown, 1 when ready.</summary>
|
||||
public float GuardReadinessNormalized => IsGuardAvailable
|
||||
? 1f - GuardCooldownNormalized
|
||||
: 0f;
|
||||
public bool IsInvulnerable =>
|
||||
IsGuarding
|
||||
|| Time.time < invulnerableUntil
|
||||
|| (dashController != null && dashController.IsDashing);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
playerStats = GetComponent<PlayerStats>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
animator = GetComponent<Animator>();
|
||||
dashController = GetComponent<DashController>();
|
||||
CurrentHealth = MaxHealth;
|
||||
playerStats.OnStatChanged += HandleStatChanged;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
RefreshRunManagerSubscription();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
InputSystem.onAfterUpdate += HandleInputSystemUpdate;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (playerStats != null)
|
||||
{
|
||||
playerStats.OnStatChanged -= HandleStatChanged;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
InputSystem.onAfterUpdate -= HandleInputSystemUpdate;
|
||||
// A disabled player cannot remain visibly guarded when re-enabled.
|
||||
// Keep the cooldown so disabling cannot refresh a spent guard.
|
||||
guardUntil = 0f;
|
||||
UnsubscribeRunManager();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshRunManagerSubscription();
|
||||
}
|
||||
|
||||
private void HandleInputSystemUpdate()
|
||||
{
|
||||
InputUpdateType currentUpdateType = InputState.currentUpdateType;
|
||||
if ((currentUpdateType == InputUpdateType.Dynamic
|
||||
|| currentUpdateType == InputUpdateType.Fixed
|
||||
|| currentUpdateType == InputUpdateType.Manual)
|
||||
&& Keyboard.current?.dKey.wasPressedThisFrame == true)
|
||||
{
|
||||
TryActivateGuard();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryActivateGuard()
|
||||
{
|
||||
if (!isActiveAndEnabled
|
||||
|| Time.timeScale <= 0f
|
||||
|| CurrentHealth <= 0f
|
||||
|| !IsGuardAvailable
|
||||
|| !RunManager.GameplayInputEnabled
|
||||
|| IsHurtMovementLocked
|
||||
|| IsGuarding
|
||||
|| GuardCooldownRemaining > 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float now = Time.time;
|
||||
nextGuardActivationIdentity = nextGuardActivationIdentity == int.MaxValue
|
||||
? 1
|
||||
: nextGuardActivationIdentity + 1;
|
||||
activeGuardActivationIdentity = nextGuardActivationIdentity;
|
||||
activeGuardHasBlockedAttack = false;
|
||||
guardUntil = now + GuardDuration;
|
||||
guardCooldownUntil = now + GuardCooldownDuration;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTakeDamage(float damage, Vector2 knockbackDirection, float knockbackDistance)
|
||||
{
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float damageTaken = Mathf.Max(
|
||||
0f,
|
||||
DamageCalculator.FinalizeDamage(
|
||||
playerStats.CalculateIncomingDamage(damage)));
|
||||
if (damageTaken <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsGuarding)
|
||||
{
|
||||
if (!activeGuardHasBlockedAttack)
|
||||
{
|
||||
activeGuardHasBlockedAttack = true;
|
||||
float cooldownRecharge = GuardCooldownDuration
|
||||
* Mathf.Clamp01(
|
||||
GuardSuccessCooldownRechargePercent / 100f);
|
||||
guardCooldownUntil = Mathf.Max(
|
||||
Time.time,
|
||||
guardCooldownUntil - cooldownRecharge);
|
||||
}
|
||||
|
||||
OnGuardAttackBlocked?.Invoke(activeGuardActivationIdentity);
|
||||
OnGuardImpact?.Invoke(knockbackDirection.normalized);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsInvulnerable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float healthBefore = CurrentHealth;
|
||||
|
||||
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageTaken);
|
||||
float actualDamageTaken = healthBefore - CurrentHealth;
|
||||
invulnerableUntil = Time.time + InvulnerabilityDuration;
|
||||
hurtMovementLockedUntil = Time.time + ResolveOverride(
|
||||
hurtMovementLockDuration,
|
||||
GameplayConstants.Current.Player.HurtMovementLockDuration);
|
||||
playerController.ApplyDamageKnockback(
|
||||
knockbackDirection,
|
||||
knockbackDistance,
|
||||
ResolveOverride(
|
||||
knockbackDuration,
|
||||
GameplayConstants.Current.Player.KnockbackDuration));
|
||||
animator.SetTrigger(HurtParameter);
|
||||
OnDamaged?.Invoke(knockbackDirection.normalized, knockbackDistance);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
if (actualDamageTaken > 0f)
|
||||
{
|
||||
OnDamageTaken?.Invoke(actualDamageTaken);
|
||||
}
|
||||
|
||||
if (CurrentHealth <= 0f)
|
||||
{
|
||||
OnDied?.Invoke();
|
||||
RunManager.Instance?.EndRun();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static float ResolveOverride(float value, float configuredValue)
|
||||
{
|
||||
return value >= 0f ? value : configuredValue;
|
||||
}
|
||||
|
||||
public void Heal(float amount)
|
||||
{
|
||||
if (amount <= 0f || CurrentHealth <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
}
|
||||
|
||||
public void GrantInvulnerability(float duration)
|
||||
{
|
||||
if (duration <= 0f || CurrentHealth <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
invulnerableUntil = Mathf.Max(
|
||||
invulnerableUntil,
|
||||
Time.time + duration);
|
||||
}
|
||||
|
||||
private void HandleStatChanged(CharacterStat stat)
|
||||
{
|
||||
if (stat != CharacterStat.MaxHealth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentHealth = Mathf.Min(CurrentHealth, MaxHealth);
|
||||
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||
}
|
||||
|
||||
private void HandleRunStarted()
|
||||
{
|
||||
guardUntil = 0f;
|
||||
guardCooldownUntil = 0f;
|
||||
nextGuardActivationIdentity = 0;
|
||||
activeGuardActivationIdentity = 0;
|
||||
activeGuardHasBlockedAttack = false;
|
||||
}
|
||||
|
||||
private void RefreshRunManagerSubscription()
|
||||
{
|
||||
RunManager current = RunManager.Instance;
|
||||
if (subscribedRunManager == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
subscribedRunManager = current;
|
||||
if (subscribedRunManager != null)
|
||||
{
|
||||
subscribedRunManager.OnRunStarted += HandleRunStarted;
|
||||
subscribedRunManager.OnGuardAvailabilityChanged +=
|
||||
HandleGuardAvailabilityChanged;
|
||||
HandleGuardAvailabilityChanged(
|
||||
subscribedRunManager.IsGuardUnlocked);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGuardAvailabilityChanged(bool isAvailable)
|
||||
{
|
||||
if (!isAvailable)
|
||||
{
|
||||
guardUntil = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeRunManager()
|
||||
{
|
||||
if (subscribedRunManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subscribedRunManager.OnRunStarted -= HandleRunStarted;
|
||||
subscribedRunManager.OnGuardAvailabilityChanged -=
|
||||
HandleGuardAvailabilityChanged;
|
||||
subscribedRunManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4085b12c1120684fa6ce9e4550d2ef8
|
||||
@@ -0,0 +1,263 @@
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Combat;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Constants;
|
||||
using BumpCombat.Enemies;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Player
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class PlayerStats : CharacterModel
|
||||
{
|
||||
private const string PressureStreakSourceId = "player.pressure-streak";
|
||||
private readonly HashSet<int> pressureStreakProcessedEnemyLifetimes = new();
|
||||
private RunManager subscribedRunManager;
|
||||
private PlayerHealth playerHealth;
|
||||
private float pressureStreakExpiresAt = -1f;
|
||||
private float pressureStreakLastKillAt = -1f;
|
||||
|
||||
public override float MoveSpeed => Evaluate(
|
||||
CharacterStat.MoveSpeed,
|
||||
GameplayConstants.Current.Player.BaseMoveSpeed);
|
||||
public override float MaxHealth => Evaluate(
|
||||
CharacterStat.MaxHealth,
|
||||
GameplayConstants.Current.Player.BaseMaxHealth);
|
||||
|
||||
public int PressureStreakKillCount { get; private set; }
|
||||
public int PressureStreakKillTarget => Mathf.Max(
|
||||
1,
|
||||
GameplayConstants.Current.Player.PressureStreakKillTarget);
|
||||
public float PressureStreakRemaining => Mathf.Max(
|
||||
0f,
|
||||
pressureStreakExpiresAt - GetCombatTime());
|
||||
public bool IsPressureStreakWindowActive => PressureStreakRemaining > 0f;
|
||||
public bool IsPressureStreakActive => IsPressureStreakWindowActive
|
||||
&& GetStackCount(
|
||||
PressureStreakSourceId,
|
||||
CharacterStat.MoveSpeed) > 0;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
if (playerHealth != null)
|
||||
{
|
||||
playerHealth.OnDied += HandlePlayerDied;
|
||||
}
|
||||
|
||||
CombatEvents.OnValidHit += HandleValidHit;
|
||||
RefreshRunManagerSubscription();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
RefreshRunManagerSubscription();
|
||||
if (subscribedRunManager != null
|
||||
&& !subscribedRunManager.IsTitleScreen
|
||||
&& !subscribedRunManager.IsGameOver)
|
||||
{
|
||||
HandleRunStarted();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshRunManagerSubscription();
|
||||
RefreshPressureStreak();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CombatEvents.OnValidHit -= HandleValidHit;
|
||||
if (playerHealth != null)
|
||||
{
|
||||
playerHealth.OnDied -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
ResetPressureStreak();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CombatEvents.OnValidHit -= HandleValidHit;
|
||||
if (playerHealth != null)
|
||||
{
|
||||
playerHealth.OnDied -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
}
|
||||
|
||||
public void RefreshPressureStreak()
|
||||
{
|
||||
float now = GetCombatTime();
|
||||
PrunePressureStreak(now);
|
||||
if (pressureStreakExpiresAt > 0f && now >= pressureStreakExpiresAt)
|
||||
{
|
||||
ClearPressureStreakBuff();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleValidHit(CombatHitResult result)
|
||||
{
|
||||
if (result.Attacker != gameObject
|
||||
|| !result.IsOrdinaryBump
|
||||
|| result.IsArtifactHit
|
||||
|| result.Damage <= 0f
|
||||
|| result.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((playerHealth != null && playerHealth.CurrentHealth <= 0f)
|
||||
|| RunManager.Instance?.IsGameOver == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyController enemy = result.Target.GetComponent<EnemyController>()
|
||||
?? result.Target.GetComponentInParent<EnemyController>();
|
||||
if (enemy == null || !enemy.IsDead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int lifetimeIdentity = enemy.SpawnLifetimeIdentity;
|
||||
if (lifetimeIdentity <= 0)
|
||||
{
|
||||
lifetimeIdentity = enemy.GetInstanceID();
|
||||
}
|
||||
|
||||
if (!pressureStreakProcessedEnemyLifetimes.Add(lifetimeIdentity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float now = GetCombatTime();
|
||||
RefreshPressureStreak();
|
||||
if (pressureStreakLastKillAt < 0f
|
||||
|| now - pressureStreakLastKillAt
|
||||
> Mathf.Max(
|
||||
0f,
|
||||
GameplayConstants.Current.Player.PressureStreakWindowSeconds))
|
||||
{
|
||||
PressureStreakKillCount = 0;
|
||||
}
|
||||
|
||||
PressureStreakKillCount++;
|
||||
pressureStreakLastKillAt = now;
|
||||
pressureStreakExpiresAt = now
|
||||
+ Mathf.Max(
|
||||
0f,
|
||||
GameplayConstants.Current.Player.PressureStreakDuration);
|
||||
|
||||
if (PressureStreakKillCount >= PressureStreakKillTarget)
|
||||
{
|
||||
EnsurePressureStreakBuff();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveModifiersFromSource(PressureStreakSourceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrunePressureStreak(float now)
|
||||
{
|
||||
if (pressureStreakLastKillAt >= 0f
|
||||
&& now - pressureStreakLastKillAt
|
||||
> Mathf.Max(
|
||||
0f,
|
||||
GameplayConstants.Current.Player.PressureStreakWindowSeconds))
|
||||
{
|
||||
PressureStreakKillCount = 0;
|
||||
pressureStreakLastKillAt = -1f;
|
||||
ClearPressureStreakBuff();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePressureStreakBuff()
|
||||
{
|
||||
if (GetStackCount(
|
||||
PressureStreakSourceId,
|
||||
CharacterStat.MoveSpeed) > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddModifier(new StatModifier(
|
||||
PressureStreakSourceId,
|
||||
CharacterStat.MoveSpeed,
|
||||
ModifierOperation.Increased,
|
||||
Mathf.Max(
|
||||
0f,
|
||||
GameplayConstants.Current.Player.PressureStreakMoveSpeedIncrease)));
|
||||
}
|
||||
|
||||
private void ClearPressureStreakBuff()
|
||||
{
|
||||
RemoveModifiersFromSource(PressureStreakSourceId);
|
||||
pressureStreakExpiresAt = -1f;
|
||||
}
|
||||
|
||||
private void ResetPressureStreak()
|
||||
{
|
||||
pressureStreakProcessedEnemyLifetimes.Clear();
|
||||
PressureStreakKillCount = 0;
|
||||
pressureStreakLastKillAt = -1f;
|
||||
ClearPressureStreakBuff();
|
||||
}
|
||||
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
ResetPressureStreak();
|
||||
}
|
||||
|
||||
private void HandleRunStarted()
|
||||
{
|
||||
ResetPressureStreak();
|
||||
}
|
||||
|
||||
private void HandleGameOver()
|
||||
{
|
||||
ResetPressureStreak();
|
||||
}
|
||||
|
||||
private float GetCombatTime()
|
||||
{
|
||||
RunManager current = RunManager.Instance;
|
||||
return current != null ? current.ElapsedTime : Time.time;
|
||||
}
|
||||
|
||||
private void RefreshRunManagerSubscription()
|
||||
{
|
||||
RunManager current = RunManager.Instance;
|
||||
if (subscribedRunManager == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
subscribedRunManager = current;
|
||||
if (subscribedRunManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subscribedRunManager.OnRunStarted += HandleRunStarted;
|
||||
subscribedRunManager.OnGameOver += HandleGameOver;
|
||||
}
|
||||
|
||||
private void UnsubscribeRunManager()
|
||||
{
|
||||
if (subscribedRunManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subscribedRunManager.OnRunStarted -= HandleRunStarted;
|
||||
subscribedRunManager.OnGameOver -= HandleGameOver;
|
||||
subscribedRunManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd485475b35048168a803a2b70638abc
|
||||
Reference in New Issue
Block a user