implemented player health, knockback, invicibility frames and hitstop
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 20s
Create tag and build when new code gets to main / Export (push) Successful in 9m56s

This commit is contained in:
2026-01-20 12:05:31 +01:00
parent 87a9fad005
commit 2e5fcb6a75
10 changed files with 206 additions and 39 deletions

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using GodotStateCharts;
using Movementtests.addons.godot_state_charts.csharp;
@@ -9,7 +11,9 @@ using RustyOptions;
public partial class PlayerController : CharacterBody3D,
IDamageable,
IDamageDealer
IDamageDealer,
IHealthable,
IKnockbackable
{
// Enums
public enum AllowedInputs
@@ -68,8 +72,7 @@ public partial class PlayerController : CharacterBody3D,
[ExportGroup("Damage")]
[Export]
public RDamage RDamage { get; set; }
[Export]
public CDamageable CDamage { get; set; }
public Node CDamageable { get; set; }
[ExportCategory("Movement")]
[ExportGroup("Ground")]
@@ -279,6 +282,8 @@ public partial class PlayerController : CharacterBody3D,
private Timer _simpleDashCooldownTimer;
private Timer _airborneDashCooldownTimer;
private Timer _powerCooldownTimer;
private Timer _invincibilityTimer;
private Timer _attackCooldown;
// State chart
private StateChart _playerState;
@@ -316,6 +321,8 @@ public partial class PlayerController : CharacterBody3D,
private Transition _onGroundSlideJump;
private Transition _onAirGlideDoubleJump;
private List<IDamageable> _hitEnemies = new List<IDamageable>();
public override void _Ready()
{
@@ -366,8 +373,29 @@ public partial class PlayerController : CharacterBody3D,
var playerShape = StandingCollider.GetShape() as CapsuleShape3D;
_playerHeight = playerShape!.Height;
_playerRadius = playerShape.Radius;
// Combat stuff
WeaponHitbox = GetNode<Area3D>("%WeaponHitbox");
WeaponHitbox.Monitoring = false;
WeaponHitbox.BodyEntered += RegisterHitEnnemy;
CHealth = GetNode<Node>("CHealth");
if (CHealth is IHealthable healthable && RHealth != null)
{
healthable.RHealth = RHealth;
healthable.CurrentHealth = RHealth.StartingHealth;
}
CKnockback = GetNode<Node>("CKnockback");
if (CKnockback is IKnockbackable knockbackable && RKnockback != null) knockbackable.RKnockback = RKnockback;
CDamageable = GetNode<Node>("CDamageable");
if (CDamageable is IDamageable damageable)
{
damageable.DamageTaken += ReduceHealth;
damageable.DamageTaken += RegisterKnockback;
}
if (CHealth is IHealthable healthable2)
healthable2.HealthDepleted += Kill;
// State management
_playerState = StateChart.Of(GetNode("StateChart"));
@@ -412,6 +440,8 @@ public partial class PlayerController : CharacterBody3D,
_timeScaleAimInAirTimer = GetNode<Timer>("TimeScaleAimInAir");
_simpleDashCooldownTimer = GetNode<Timer>("DashCooldown");
_airborneDashCooldownTimer = GetNode<Timer>("AirborneDashCooldown");
_invincibilityTimer = GetNode<Timer>("InvincibilityTime");
_attackCooldown = GetNode<Timer>("AttackCooldown");
///////////////////////////
// Initialize components //
@@ -419,6 +449,8 @@ public partial class PlayerController : CharacterBody3D,
// Camera stuff
HeadSystem.Init();
HeadSystem.HitboxActivated += OnHitboxActivated;
HeadSystem.HitboxDeactivated += OnHitboxDeactivated;
// Movement stuff
// Getting universal setting from GODOT editor to be in sync
@@ -437,6 +469,9 @@ public partial class PlayerController : CharacterBody3D,
///////////////////////////
// Signal setup ///////////
///////////////////////////
_invincibilityTimer.Timeout += ResetInvincibility;
_attackCooldown.Timeout += ResetAttackCooldown;
_aiming.StatePhysicsProcessing += HandleAiming;
_aiming.StateEntered += OnAimingEntered;
_aiming.StateExited += ResetTimeScale;
@@ -1267,6 +1302,7 @@ public partial class PlayerController : CharacterBody3D,
///////////////////////////
private bool _isSlideInputDown = false;
public void OnInputSlideStarted()
{
_isSlideInputDown = true;
@@ -1701,6 +1737,7 @@ public partial class PlayerController : CharacterBody3D,
if (_currentInputBufferFrames > 0) _currentInputBufferFrames -= 1;
LookAround(delta);
Velocity += ComputeKnockback();
MoveSlideAndHandleStairs((float) delta);
MantleSystem.ProcessMantle(_grounded.Active);
@@ -1719,8 +1756,17 @@ public partial class PlayerController : CharacterBody3D,
public DamageRecord TakeDamage(DamageRecord damageRecord)
{
var finalDamage = CDamage.TakeDamage(damageRecord);
if (CDamageable is not IDamageable damageable || _isInvincible)
return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) };
var finalDamage = damageable.TakeDamage(damageRecord);
DamageTaken?.Invoke(this, finalDamage);
TriggerHitstop();
_isInvincible = true;
_invincibilityTimer.Start();
return finalDamage;
}
@@ -1735,18 +1781,99 @@ public partial class PlayerController : CharacterBody3D,
}
if (!WeaponSystem.InHandState.Active) return;
if (!_canAttack) return;
_canAttack = false;
_attackCooldown.Start();
PerformHit();
}
public void ResetAttackCooldown()
{
_canAttack = true;
}
public void PerformHit()
{
HeadSystem.OnHit();
}
public void OnHitboxActivated()
{
WeaponHitbox.Monitoring = true;
}
public void OnHitboxDeactivated()
{
WeaponHitbox.Monitoring = false;
TriggerDamage();
}
public void RegisterHitEnnemy(Node3D body)
{
if (body is not IDamageable damageable) return;
_hitEnemies.Add(damageable);
}
public void TriggerDamage()
{
if (_hitEnemies.Count == 0) return;
var bodies = WeaponHitbox.GetOverlappingBodies();
foreach (var body in bodies)
foreach (var damageable in _hitEnemies)
{
if(body is IDamageable spawnable)
spawnable.TakeDamage(new DamageRecord(this, RDamage));
damageable.TakeDamage(new DamageRecord(this, RDamage));
}
_hitEnemies.Clear();
TriggerHitstop();
}
public void TriggerHitstop()
{
Engine.SetTimeScale(0.01);
var timer = GetTree().CreateTimer(0.1, true, false, true);
timer.Timeout += OnHitstopEnded;
}
public void OnHitstopEnded()
{
ResetTimeScale();
}
public Node CHealth { get; set; }
public Node CKnockback { get; set; }
public event Action<IHealthable, float> HealthChanged;
public event Action<IHealthable> HealthDepleted;
public RHealth RHealth { get; set; }
public float CurrentHealth { get; set; }
public void ReduceHealth(IDamageable source, DamageRecord damageRecord)
{
if (CHealth is not IHealthable healthable) return;
healthable.ReduceHealth(source, damageRecord);
HealthChanged?.Invoke(this, healthable.CurrentHealth);
}
public RKnockback RKnockback { get; set; }
public void RegisterKnockback(IDamageable source, DamageRecord damageRecord)
{
if (CKnockback is not IKnockbackable knockbackable) return;
knockbackable.RegisterKnockback(source, damageRecord);
}
public Vector3 ComputeKnockback()
{
if (CKnockback is not IKnockbackable knockbackable) return Vector3.Zero;
return knockbackable.ComputeKnockback();
}
public void Kill(IHealthable source)
{
GD.Print("Player died!");
}
private bool _isInvincible;
private bool _canAttack = true;
public void ResetInvincibility()
{
_isInvincible = false;
}
}