using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Components; using Cirno.Scripts.Controllers; using Cirno.Scripts.Resources; using Cirno.Scripts.Utils; using Godot; namespace Cirno.Scripts.Weapons; public partial class Bullet3D : Area3D, IBullet { [Export] public float Speed { get; set; } = 1900f; public BulletOwner BulletOwner => _bulletInfo?.Owner ?? BulletOwner.None; public float Damage => _bulletInfo?.Damage ?? 1; public DamageType DamageType => _bulletInfo?.DamageType ?? DamageType.Neutral; protected Vector2 _direction = Vector2.Right; private double _elapsedTime = 0f; private BulletInfo _bulletInfo; public BulletInfo BulletInfo => _bulletInfo; private List _modifiers = new(); public bool IsGrazed { get; set; } = false; public bool IsFrozen { get; private set; } = false; public bool Enabled { get; private set; } = false; //public float SpriteRotation { get; private set; } = 0f; [Signal] public delegate void OnDestroyEventHandler(); [Signal] public delegate void InitializedEventHandler(); private AudioStreamPlayer3D _grazeSound; private GpuParticles3D _grazeParticles; private CollisionShape3D _collisionShape; private Sprite3D _sprite; private Decal _shadow; private readonly Vector3 _defaultRotation = new( Mathf.DegToRad(-45f), Mathf.DegToRad(45f), 0f ); private Vector3 MakeRotationVectorDegrees(float rotation) { return new( Mathf.DegToRad(-45f), Mathf.DegToRad(45f), Mathf.DegToRad(rotation) ); } private Vector3 MakeRotationVectorRad(float rotation) { return new( Mathf.DegToRad(-45f), Mathf.DegToRad(45f), rotation ); } public override void _Ready() { _grazeSound = GetNodeOrNull("GrazeSound"); _grazeParticles = GetNodeOrNull("GrazeParticles"); _collisionShape = GetNode("CollisionShape"); _sprite = GetNodeOrNull("Sprite"); _shadow = GetNodeOrNull("Shadow"); } public void Initialize(BulletInfo bulletInfo) { _bulletInfo = bulletInfo; _elapsedTime = 0f; this.Speed = bulletInfo.Speed; SetSpriteRotationToDirection(); //SpriteRotation = 0f; if (_sprite is not null && bulletInfo.OriginalBulletResource.BulletSprite is not null) { _sprite.Texture = _bulletInfo.OriginalBulletResource.BulletSprite; } if (_collisionShape.Shape is SphereShape3D sphere && bulletInfo.OriginalBulletResource.BulletSize > 0) { sphere.Radius = bulletInfo.OriginalBulletResource.BulletSize; } IsGrazed = false; IsFrozen = false; if (_shadow is not null) { var shadowSize = _bulletInfo.OriginalBulletResource.BulletSize * 3; _shadow.Size = new Vector3(shadowSize, _shadow.Size.Y, shadowSize); } // Need to clone them here // _modifiers = _bulletInfo.TimeModifiers.Select(x => x.MakeClone()).ToList(); // var clonedModifiers = _bulletInfo.TimeModifiers.Select(x => x.MakeClone()); // _modifiers = clonedModifiers.ToList(); // Ugly hack to make instances unique _modifiers = _bulletInfo.TimeModifiers.Select(x => x.Wrap()).ToList(); EmitSignalInitialized(); } private void SetSpriteRotationToDirection() { if (!BulletInfo.Attributes.HasFlag(BulletFlags.Rotateable)) return; _sprite?.SetRotation(MakeRotationFromDirection(_direction)); } private Vector3 MakeRotationFromDirection(Vector2 direction) { // atan2 gives angle in radians around Z float zRotation2 = Mathf.Atan2(-direction.Y, direction.X) + Mathf.Pi; // Apply correction for your sprite's local forward axis zRotation2 += Mathf.DegToRad(45f); // tweak to +45 or -45 as needed return new Vector3( Mathf.DegToRad(-45f), // X tilt (to match camera) Mathf.DegToRad(45f), // Y tilt (to match camera) zRotation2 // Facing direction ); // var rotatedVector = direction.Rotated(Mathf.DegToRad(-45)); // var asrt = Vector2.ang // return MakeRotationVectorRad(zRotation); // Rotate input by -45 degrees to counter camera isometry // float cos = Mathf.Cos(-Mathf.Pi / 4f); // float sin = Mathf.Sin(-Mathf.Pi / 4f); // // Vector2 rotatedDir = new Vector2( // direction.X * cos - direction.Y * sin, // direction.X * sin + direction.Y * cos // ); // // //float zRotation = Mathf.Atan2(rotatedDir.Y, rotatedDir.X)/* - Mathf.Pi / 2f*/; // // float zRotation = Mathf.Atan2(-rotatedDir.Y, rotatedDir.X) + Mathf.Pi; // var zRotationDegrees = Mathf.RadToDeg(zRotation); // return MakeRotationVectorRad(zRotation); } /// /// Enables the bullet, shows the sprite and activates collisions /// public void Enable() { Enabled = true; Show(); if (this._collisionShape is null) { _collisionShape = GetNode("CollisionShape"); } _collisionShape.SetDeferred(CollisionShape3D.PropertyName.Disabled, false); } /// /// Disables the bullet, hides the sprite and disables collisions /// public void Disable(bool hideSprite = true) { Enabled = false; if (hideSprite && !BulletInfo.Attributes.HasFlag(BulletFlags.PersistSprite)) { Hide(); } if (this._collisionShape is null) { _collisionShape = GetNode("CollisionShape2D"); } _collisionShape.SetDeferred(CollisionShape2D.PropertyName.Disabled, true); } public void Graze() { if (!Enabled) return; _grazeSound?.Play(); if (_grazeParticles is not null) { _grazeParticles.Emitting = true; } IsGrazed = true; } private void ApplyTimeModifiers(double delta) { foreach (var modifier in _modifiers) { if (_elapsedTime >= modifier.TimeModifier.TimeInSeconds) { if (!modifier.Applied) { modifier.Applied = true; modifier.TimeModifier.Start(this); modifier.Elapsed = 0; } else { modifier.Elapsed += delta; } modifier.TimeModifier.Update(this, delta, modifier.Elapsed); } } } public virtual void RotateBullet(float degrees) { float radians = Mathf.DegToRad(degrees); _direction = _direction.Rotated(radians).Normalized(); // Rotate direction if (!BulletInfo.Attributes.HasFlag(BulletFlags.Rotateable)) return; SetSpriteRotationToDirection(); //SetRotation(Rotation + radians); } public virtual void RotateSpriteDegrees(float degrees) { if (!BulletInfo.Attributes.HasFlag(BulletFlags.Rotateable)) return; var currentRotation = _sprite.GetRotationDegrees().Z; _sprite.RotateZ(Mathf.DegToRad(currentRotation + degrees)); // SpriteRotation = Mathf.DegToRad(Mathf.RadToDeg(SpriteRotation) + degrees); //SetRotationDegrees(RotationDegrees + degrees); } public virtual void RotateSprite(float radians) { if (!BulletInfo.Attributes.HasFlag(BulletFlags.Rotateable)) return; var currentRotation = _sprite.GetRotation().Z; var axis = Basis.FromEuler(_defaultRotation).Z; _sprite?.Rotate(axis, currentRotation + radians); //_sprite.SetRotation(new Vector3()); //Rotate(axis, radians); //_sprite?.Rotate(Vector3.Forward, radians); //_sprite?.RotateZ(radians); //SetRotation(Rotation + radians); } public void FacePlayer() { if (GameController.Instance.PlayerPosition.HasValue) { _direction = (GameController.Instance.PlayerPosition.Value.ToVector2() - this.GlobalPosition.ToVector2()) .Normalized(); RotateBullet(0); // quick hack to rotate lasers //LookAt(player.GlobalPosition); } } public void SetDirection(Vector2 direction) { var normalized = direction.Normalized(); _direction = normalized; if (!BulletInfo.Attributes.HasFlag(BulletFlags.Rotateable)) return; SetSpriteRotationToDirection(); //SetRotation(Mathf.Atan2(normalized.Y, normalized.X) + Mathf.Pi / 2); } // Called every frame. 'delta' is the elapsed time since the previous frame. public override void _Process(double delta) { if (!Enabled) return; _elapsedTime += delta; if (_elapsedTime >= _bulletInfo.LifeTime) { Destroy(); } if (GameController.Instance is not null && GameController.Instance.DebugDraw) { #if !DISABLE_DD3D DebugDraw3D.DrawSphere(this.GlobalPosition, this._bulletInfo.OriginalBulletResource.BulletSize, Colors.DarkRed); #endif } } public override void _PhysicsProcess(double delta) { if (!Enabled) return; if (_bulletInfo != null) { ApplyTimeModifiers(delta); } if (BulletInfo.Attributes.HasFlag(BulletFlags.Controllable)) { ControlBullet(delta); } var newPos2D = ((float)(Speed * delta) * _direction); this.Position += new Vector3(newPos2D.X, 0, newPos2D.Y); } private void ControlBullet(double delta) { if (!Enabled) return; var axis = Input.GetAxis("left", "right"); if (axis != 0) { float rotationSpeed = 180f; // Degrees per second RotateBullet(axis * rotationSpeed * (float)delta); } } private void _on_visible_on_screen_notifier_2d_screen_exited() { if (!Enabled) return; if (!BulletInfo.Attributes.HasFlag(BulletFlags.DieOutOfScreen)) return; //Debug.WriteLine("Destroy bullet out of screen"); Destroy(); } private readonly StringName SolidGroup = "Solid"; private readonly StringName PermeableGroup = "Permeable"; private readonly StringName DestroyableGroup = "Destroyable"; private void _on_body_entered(Node3D body) { if (body.IsInGroup(DestroyableGroup) && body is IDestructible destructible && CanHit(BulletOwner, destructible.BulletGroup)) { // hit destructible.Hit(Damage, DamageType); RequestCollisionDestruction(); return; } if (body.IsInGroup(SolidGroup) && !body.IsInGroup(PermeableGroup)) { //Debug.WriteLine("Collision"); RequestCollisionDestruction(); } //// Do not Collide with body for purpose of destroying bullets // else if (body.IsInGroup("Destroyable")) // { // Debug.WriteLine("Collision with destroyable object body"); // QueueFree(); // } } private void _on_area_entered(Area3D area) { if (area.IsInGroup("Solid")) { RequestCollisionDestruction(); return; } if (area.IsInGroup("Destroyable") && area is IDestructible destructible && CanHit(BulletOwner, destructible.BulletGroup)) { // hit destructible.Hit(Damage, DamageType); RequestCollisionDestruction(); } } public bool CanHit(BulletOwner bulletOwner, BulletOwner targetGroup) { // If either is None, it always hits if (bulletOwner == BulletOwner.None || targetGroup == BulletOwner.None) { return true; } // Otherwise, it hits only if they are different groups return bulletOwner != targetGroup; } public void RequestCollisionDestruction() { if (!Enabled) return; if (_bulletInfo.DestroyOnCollision) { Destroy(); } } private void Destroy() { if (_bulletInfo?.OriginalBulletResource.DestructionParticlesBullet != null) { var particleData = _bulletInfo?.OriginalBulletResource.DestructionParticlesBullet.MakeBullet( this.GlobalPosition.ToVector2()); var particle = PoolingManager.Instance.SpawnBullet(particleData.OriginalBulletResource); particle.GlobalPosition = this.GlobalPosition; particle.Initialize(particleData); //this.CreateSibling(_bulletInfo.DestructionParticlesScene); //particle.Init(); } EmitSignal(Bullet.SignalName.OnDestroy); //QueueFree(); PoolingManager.Instance.DisableBullet(this); } public void Freeze() { IsFrozen = true; EmitSignal(Bullet.SignalName.OnDestroy); //QueueFree(); PoolingManager.Instance.DisableBullet(this); } }