diff --git a/JumpRoyale/src/Arena.cs b/JumpRoyale/src/Arena.cs index 3fe82ddd..b01037f8 100644 --- a/JumpRoyale/src/Arena.cs +++ b/JumpRoyale/src/Arena.cs @@ -100,6 +100,16 @@ public override void _UnhandledInput(InputEvent @event) commandHandler.SpawnFakePlayers(); } } + + // Make everyone jump, no exceptions + if (Input.IsPhysicalKeyPressed(Key.J)) + { + foreach (Jumper jumper in ActiveJumpers.Instance.AllJumpers()) + { + jumper.RandomJump(); + jumper.FlashPlayerName(); + } + } } /// diff --git a/JumpRoyale/src/Jumper.cs b/JumpRoyale/src/Jumper.cs index ba9f9b40..6dc844a9 100644 --- a/JumpRoyale/src/Jumper.cs +++ b/JumpRoyale/src/Jumper.cs @@ -15,13 +15,16 @@ public partial class Jumper : CharacterBody2D /// private readonly HashSet _recentPosition = []; + private AnimatedSprite2D _animatedSprite2D = null!; + private RichTextLabel _nameLabel = null!; + private CpuParticles2D _cpuParticles2D = null!; + /// /// Used to block the fadeout in some situations, e.g. at the start of the game. This is automatically set to true /// on every jump. /// private bool _canFadePlayerName; private bool _lastJumpZeroAngle; - private bool _wasOnFloor; // Get the gravity from the project settings to be synced with RigidBody nodes. private float _gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle(); @@ -53,13 +56,37 @@ public partial class Jumper : CharacterBody2D public void Init(int x, int y, [NotNull] PlayerData playerData) { PlayerData = playerData; - Position = new Vector2(x, y); Name = PlayerData.Name; + } + + public override void _Ready() + { + _animatedSprite2D = GetNode(SpriteNodeName); + _nameLabel = GetNode(NameNodeName); + _cpuParticles2D = GetNode(ParticleSystemNodeName); SetCharacter(); SetPlayerName(); SetGlow(); + + _animatedSprite2D.AnimationFinished += OnSpriteAnimationFinished; + } + + public override void _PhysicsProcess(double delta) + { + StopOnFloor(); + ApplyInitialGravity(delta); + ApplyJumpVelocity(); + RotateInAir(delta); + BounceOffWall(); + PlayNotGroundedAnimation(); + UpdateNameTransparency(); + + _previousXVelocity = Velocity.X; + + MoveAndSlide(); + StorePosition(); } /// @@ -71,22 +98,17 @@ public void SetPlayerName() // Note: ToHTML() excludes alpha component to avoid transparent names string colorCode = Color.FromString(PlayerData.PlayerNameColor, GameConstants.DefaultNameColor).ToHtml(false); - RichTextLabel nameLabel = GetNode(NameNodeName); - - nameLabel.Text = $"[center][color={colorCode}]{PlayerData.Name}[/color][/center]"; + _nameLabel.Text = $"[center][color={colorCode}]{PlayerData.Name}[/color][/center]"; } public void SetCrazyParticles() { - CpuParticles2D particles = GetGlowNode(); - // Make sure we can repeatedly call this function without unbounded growth. - particles.Amount = Math.Min(particles.Amount * 5, 500); + _cpuParticles2D.Amount = Math.Min(_cpuParticles2D.Amount * 5, 500); } public void SetCharacter() { - AnimatedSprite2D sprite = GetNode(SpriteNodeName); int choice = PlayerData.CharacterChoice; string gender = choice > 9 ? "f" : "m"; int charNumber = ((choice - 1) % 9 / 3) + 1; @@ -94,11 +116,15 @@ public void SetCharacter() GD.Print("Choice: " + choice + " Gender: " + gender + " Char: " + charNumber + " Clothing: " + clothingNumber); - sprite.SpriteFrames = SpriteFrameCreator.Instance.GetSpriteFrames(gender, charNumber, clothingNumber); + _animatedSprite2D.SpriteFrames = SpriteFrameCreator.Instance.GetSpriteFrames( + gender, + charNumber, + clothingNumber + ); if (IsOnFloor()) { - sprite.Play(JumperAnimations.AnimationIdle); + _animatedSprite2D.Play(JumperAnimations.AnimationIdle); } } @@ -115,29 +141,21 @@ public void SetGlow() return; } - CpuParticles2D particles = GetGlowNode(); Color color = Color.FromHtml(colorString); - color.A = 1f; - particles.SelfModulate = color; - particles.Visible = true; + color.A = 1f; + _cpuParticles2D.SelfModulate = color; + _cpuParticles2D.Visible = true; } public void DisableGlow() { - CpuParticles2D particles = GetGlowNode(); - - particles.Visible = false; - } - - public override void _Ready() - { - GetNode(SpriteNodeName).AnimationFinished += OnSpriteAnimationFinished; + _cpuParticles2D.Visible = false; } public void RandomJump() { - Jump(Rng.IntRange(45, 135), Rng.IntRange(10, 100)); + Jump(Rng.IntRange(45, 135), Rng.IntRange(75, 100)); } public void Jump(int angle, int power) @@ -149,11 +167,10 @@ public void Jump(int angle, int power) _jumpVelocity.X = Mathf.Cos(Mathf.DegToRad(angle + 180)); _jumpVelocity.Y = Mathf.Sin(Mathf.DegToRad(angle + 180)); _jumpVelocity = _jumpVelocity.Normalized() * (float)normalizedPower; - - PlayerData.NumJumps++; - _canFadePlayerName = true; _lastJumpZeroAngle = angle == 90; // 0 in the command is expressed here as 90. + + PlayerData.NumJumps++; } } @@ -165,104 +182,57 @@ public void DisableNameFadeout() _canFadePlayerName = false; } - public void SetColor(string hexColor) + public void OnSpriteAnimationFinished() { - AnimatedSprite2D sprite = GetNode(SpriteNodeName); - - sprite.Modulate = Color.FromHtml(hexColor); - sprite.Modulate = new Color(sprite.Modulate.R, sprite.Modulate.G, sprite.Modulate.B, 1f); + if (_animatedSprite2D.Animation == JumperAnimations.AnimationLand) + { + _animatedSprite2D.Play(JumperAnimations.AnimationIdle); + } } - public override void _PhysicsProcess(double delta) + /// + /// Resets the alpha component to 1 on player's name label and resets the name fadeout timer. + /// + public void FlashPlayerName() { - Vector2 velocity = Velocity; + SetNameAlpha(1f); + ResetNameTimer(); + } + private void StopOnFloor() + { if (IsOnFloor()) { - velocity.Y = 0; - velocity.X = 0; + Velocity = Vector2.Zero; } + } - // Add the gravity. + private void ApplyInitialGravity(double delta) + { if (!IsOnFloor()) { - velocity.Y += _gravity * (float)delta; - } - - AnimatedSprite2D sprite = GetNode(SpriteNodeName); - - if (_jumpVelocity != Vector2.Zero) - { - velocity = _jumpVelocity; - - if (!_lastJumpZeroAngle) - { - sprite.FlipH = velocity.X < 0; - } - - _jumpVelocity = Vector2.Zero; - } - - if (IsOnWall()) - { - velocity.X = _previousXVelocity * -0.75f; + Velocity = new(Velocity.X, Velocity.Y + _gravity * (float)delta); } - - Velocity = velocity; - - if (Velocity.Y > 0) - { - sprite.Play(JumperAnimations.AnimationJump); - } - else if (Velocity.Y < 0) - { - sprite.Play(JumperAnimations.AnimationFall); - } - - bool justLanded = !_wasOnFloor && IsOnFloor(); - bool stuckInAir = - (sprite.Animation == JumperAnimations.AnimationFall || sprite.Animation == JumperAnimations.AnimationJump) - && Velocity.Y == 0; - - if (justLanded || stuckInAir) - { - sprite.Play(JumperAnimations.AnimationLand); - } - - _wasOnFloor = IsOnFloor(); - - UpdateNameTransparency(); - - _previousXVelocity = Velocity.X; - - MoveAndSlide(); - StorePosition(); } - public void OnSpriteAnimationFinished() + private void ApplyJumpVelocity() { - AnimatedSprite2D sprite = GetNode(SpriteNodeName); + Velocity = !_jumpVelocity.IsEqualApprox(Vector2.Zero) ? _jumpVelocity : Velocity; - if (sprite.Animation == JumperAnimations.AnimationLand) + // Flip the sprite based on our x velocity, but only if we recently jumped at a non-zero angle + if (!Mathf.IsZeroApprox(Velocity.X) && !_lastJumpZeroAngle) { - sprite.Play(JumperAnimations.AnimationIdle); + _animatedSprite2D.FlipH = Velocity.X < 0; } - } - /// - /// Resets the alpha component to 1 on player's name label and resets the name fadeout timer. - /// - public void FlashPlayerName() - { - SetNameAlpha(1f); - ResetNameTimer(); + // Reset the jump velocity to indicate that we should not continuously apply the calculated velocity + _jumpVelocity = Vector2.Zero; } private void ResetNameTimer() { _fontVisibilityTimerStartTime = Time.GetTicksMsec(); - - GetNode(NameNodeName).Visible = true; + _nameLabel.Visible = true; } private void UpdateNameTransparency() @@ -286,14 +256,47 @@ private float CalculateFontAlpha() return Math.Max(0, 1 - diff); } - private CpuParticles2D GetGlowNode() + private void SetNameAlpha(float alpha) { - return GetNode(ParticleSystemNodeName); + _nameLabel.Modulate = new Color(1, 1, 1, alpha); } - private void SetNameAlpha(float alpha) + /// + /// Causes the character to rotate based on its X velocity, but only at non-zero angles. + /// + private void RotateInAir(double delta) + { + // We don't want to rotate if we just jumped straight up + if (_lastJumpZeroAngle) + { + // Edge case reset when we jump at the very moment we hit the floor and we keep the previous rotation + _animatedSprite2D.RotationDegrees = 0; + + return; + } + + // Formula to automatically calculate the rotation speed based on the character's x jump velocity. The maximum + // velocity is 700, but it's a bit too fast, so we want to clamp it at around 600. The formula was shortened and + // tweaked to always output 1 at non-zero angle with low value, with a maximum of 7 at angle of 60, which should + // not increase linearly, but a bit slower at the start. Assuming the full power jump. + // Visualization, caps at J60. Approximately every +100 velocity on the plot is the next 10 angles: + // https://www.wolframalpha.com/input?i=min%28max%28%284sin%28%28abs%28x%29%2F280%29+%2B+300%29%2B5%29%2C1%29%2C+7%29+%3Bx+from+0+to+700 + float velocity = Math.Abs(Velocity.X); + float rotationSpeedMultiplier = (float)(4 * Math.Sin((velocity / 280) + 300) + 5); + float clampedMultiplier = Math.Clamp(rotationSpeedMultiplier, 1, 7); + + // Calculate the rotation factor and flip the value if we are going left (rotating in the right direction) + float rotationFactor = 200 * (float)delta * (Velocity.X < 0 ? -1 : 1) * clampedMultiplier; + + _animatedSprite2D.RotationDegrees = IsOnFloor() ? 0 : _animatedSprite2D.RotationDegrees + rotationFactor; + } + + private void BounceOffWall() { - GetNode(NameNodeName).Modulate = new Color(1, 1, 1, alpha); + if (IsOnWall()) + { + Velocity = new(_previousXVelocity * -0.75f, Velocity.Y); + } } /// @@ -329,4 +332,27 @@ private void StorePosition() Position += Vector2.Up * 16; } } + + private void PlayNotGroundedAnimation() + { + // Going up -> Jump, down -> Fall. Removing the check causes infinite animation start + if (!IsOnFloor()) + { + string animation = Velocity.Y < 0 ? JumperAnimations.AnimationJump : JumperAnimations.AnimationFall; + + _animatedSprite2D.Play(animation); + } + + // Describes a situation when we stopped, but the animation is still playing the Jump/Fall frames + bool hasLandedButStillAnimating = + ( + _animatedSprite2D.Animation == JumperAnimations.AnimationFall + || _animatedSprite2D.Animation == JumperAnimations.AnimationJump + ) && Velocity.IsEqualApprox(Vector2.Zero); + + if (hasLandedButStillAnimating) + { + _animatedSprite2D.Play(JumperAnimations.AnimationLand); + } + } }