using Cirno.Scripts.Actors._3D; using Cirno.Scripts.Components; using Cirno.Scripts.Controllers; using Cirno.Scripts.Utils; using Godot; namespace Cirno.Scripts.Weapons; /// /// Laser implementation that conforms to the IBullet interface. /// Wraps the Laser class functionality to integrate with the bullet system. /// public partial class LaserBullet3D : Area3D, IBullet { private BulletInfo _bulletInfo; public float Speed { get; set; } public BulletOwner BulletOwner => _bulletInfo?.Owner ?? BulletOwner.None; public float Damage => _bulletInfo?.Damage ?? 1; public DamageType DamageType => _bulletInfo?.DamageType ?? DamageType.Neutral; public BulletInfo BulletInfo => _bulletInfo; public bool IsGrazed { get; set; } public bool IsFrozen { get; private set; } public bool Enabled { get; private set; } private Vector2 _direction = Vector2.Right; private double _elapsedTime; private MeshInstance3D _mesh; private CollisionShape3D _collision; private RayCast3D _ray; private ShaderMaterial _beamMaterial; private float _currentRadius; private Laser.LaserState _state = Laser.LaserState.Inactive; private float _stateTimer; private Vector3 _origin; private Vector3 _laserDirection; private float _currentLength; [Signal] public delegate void OnDestroyEventHandler(); [Signal] public delegate void InitializedEventHandler(); public override void _Ready() { _mesh = GetNode("MeshInstance3D"); _collision = GetNode("CollisionShape3D"); _ray = GetNode("RayCast3D"); _beamMaterial = _mesh.MaterialOverride as ShaderMaterial; } public void Initialize(BulletInfo bulletInfo) { _bulletInfo = bulletInfo; _elapsedTime = 0f; _direction = bulletInfo.Direction.Normalized(); IsGrazed = false; IsFrozen = false; // Setup laser from direction _origin = GlobalPosition; _laserDirection = new Vector3(_direction.X, 0, _direction.Y).Normalized(); if (_bulletInfo.LaserConfig != null) { _ray.CollisionMask = _bulletInfo.LaserConfig.GeometryLayer; } ChangeCollisionStateDeferred(true); StartLaser(); EmitSignal(SignalName.Initialized); } private void StartLaser() { _mesh.Scale = Vector3.One; _collision.Scale = Vector3.One; _stateTimer = 0f; _state = _bulletInfo.LaserConfig.WarningDuration > 0 ? Laser.LaserState.Warning : Laser.LaserState.Expanding; UpdateVisualOrientation(); SetRadius(_bulletInfo.LaserConfig.WarningRadius); } public void Enable() { Enabled = true; Show(); if (_collision != null) { _collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, false); } } public void Disable(bool hideSprite = true) { Enabled = false; if (hideSprite) { Hide(); } if (_collision != null) { _collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, true); } } public void Graze() { if (!Enabled) return; IsGrazed = true; } public void RotateBullet(float degrees) { float radians = Mathf.DegToRad(degrees); _direction = _direction.Rotated(radians).Normalized(); _laserDirection = new Vector3(_direction.X, 0, _direction.Y).Normalized(); UpdateVisualOrientation(); } public void RotateSpriteDegrees(float degrees) { // Lasers don't rotate sprite independently RotateBullet(degrees); } public void RotateSprite(float radians) { // Lasers don't rotate sprite independently RotateBullet(Mathf.RadToDeg(radians)); } public void FacePlayer() { if (GameController.Instance.PlayerPosition.HasValue) { _direction = (GameController.Instance.PlayerPosition.Value.ToVector2() - GlobalPosition.ToVector2()).Normalized(); _laserDirection = new Vector3(_direction.X, 0, _direction.Y).Normalized(); UpdateVisualOrientation(); } } public void SetDirection(Vector2 direction) { _direction = direction.Normalized(); _laserDirection = new Vector3(_direction.X, 0, _direction.Y).Normalized(); UpdateVisualOrientation(); } public bool CanHit(BulletOwner bulletOwner, BulletOwner targetGroup) { if (bulletOwner == BulletOwner.None || targetGroup == BulletOwner.None) { return true; } return bulletOwner != targetGroup; } public void RequestCollisionDestruction() { // Lasers typically don't get destroyed on collision if (_bulletInfo.DestroyOnCollision) { Destroy(); } } public void Freeze() { IsFrozen = true; EmitSignal(SignalName.OnDestroy); PoolingManager.Instance.DisableBullet(this); } public override void _Process(double delta) { if (!Enabled) return; _elapsedTime += delta; if (_elapsedTime >= _bulletInfo.LifeTime) { Destroy(); return; } if (_state == Laser.LaserState.Finished) return; _stateTimer += (float)delta; UpdateRaycast(); UpdateBeam(); switch (_state) { case Laser.LaserState.Warning: if (_stateTimer >= _bulletInfo.LaserConfig.WarningDuration) TransitionTo(Laser.LaserState.Expanding); break; case Laser.LaserState.Expanding: HandleExpansion(); break; case Laser.LaserState.Active: if (_bulletInfo.LaserConfig.ActiveDuration >= 0 && _stateTimer >= _bulletInfo.LaserConfig.ActiveDuration) TransitionTo(Laser.LaserState.Finished); break; } } private void TransitionTo(Laser.LaserState next) { _state = next; _stateTimer = 0f; switch (next) { case Laser.LaserState.Expanding: if (_bulletInfo.LaserConfig.ExpansionDuration <= 0f) { SetRadius(_bulletInfo.LaserConfig.DamageRadius); EnableCollision(); TransitionTo(Laser.LaserState.Active); } break; case Laser.LaserState.Active: EnableCollision(); SetRadius(_bulletInfo.LaserConfig.DamageRadius); break; case Laser.LaserState.Finished: Destroy(); break; } } private void HandleExpansion() { if (_stateTimer < _bulletInfo.LaserConfig.ExpansionDelay) return; var t = Mathf.Clamp( (_stateTimer - _bulletInfo.LaserConfig.ExpansionDelay) / Mathf.Max(_bulletInfo.LaserConfig.ExpansionDuration, 0.001f), 0f, 1f ); var radius = Mathf.Lerp(_bulletInfo.LaserConfig.WarningRadius, _bulletInfo.LaserConfig.DamageRadius, t); SetRadius(radius); if (t >= 1f) TransitionTo(Laser.LaserState.Active); } private void UpdateRaycast() { _ray.TargetPosition = _laserDirection * _bulletInfo.LaserConfig.MaxLength; _ray.ForceRaycastUpdate(); _currentLength = _ray.IsColliding() ? GlobalPosition.DistanceTo(_ray.GetCollisionPoint()) : _bulletInfo.LaserConfig.MaxLength; } private void UpdateVisualOrientation() { Basis look = Basis.LookingAt(_laserDirection, Vector3.Up); Basis correction = new Basis(Vector3.Right, Mathf.Pi / 2f); Basis finalBasis = look * correction; _mesh.Basis = finalBasis; _collision.Basis = finalBasis; } private void UpdateBeam() { if (_mesh.Mesh is CylinderMesh cyl) cyl.Height = _currentLength; if (_collision.Shape is CapsuleShape3D capsule) capsule.Height = _currentLength; Vector3 center = _origin + _laserDirection * (_currentLength * 0.5f); Transform3D meshXform = _mesh.GlobalTransform; meshXform.Origin = center; _mesh.GlobalTransform = meshXform; Transform3D colXform = _collision.GlobalTransform; colXform.Origin = center; _collision.GlobalTransform = colXform; } private void SetRadius(float radius) { _currentRadius = radius; if (_mesh.Mesh is CylinderMesh cyl) { cyl.TopRadius = radius; cyl.BottomRadius = radius; } if (_collision.Shape is CapsuleShape3D capsule) capsule.Radius = radius; UpdateMaterial(); } private void EnableCollision() { ChangeCollisionStateDeferred(false); } private void ChangeCollisionStateDeferred(bool value) { _collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, value); } private void UpdateMaterial() { if (_beamMaterial == null) return; _beamMaterial.SetShaderParameter("beam_length", _currentLength); _beamMaterial.SetShaderParameter("beam_radius", _currentRadius); switch (_state) { case Laser.LaserState.Warning: _beamMaterial.SetShaderParameter("beam_color", new Color(1f, 1f, 0.2f)); _beamMaterial.SetShaderParameter("intensity", 0.5f); break; case Laser.LaserState.Active: _beamMaterial.SetShaderParameter("beam_color", new Color(1f, 0.2f, 0.2f)); _beamMaterial.SetShaderParameter("intensity", 1.0f); break; } } public bool IsPointInsideBeam(Vector3 worldPoint) { if (_state != Laser.LaserState.Active) return false; Vector3 local = worldPoint - _origin; float projection = local.Dot(_laserDirection); if (projection < 0 || projection > _currentLength) return false; Vector3 closestPoint = _origin + _laserDirection * projection; float distanceSq = worldPoint.DistanceSquaredTo(closestPoint); float radius = _bulletInfo.LaserConfig.DamageRadius; return distanceSq <= radius * radius; } private void _on_area_entered(Area3D area) { if (!Enabled || _state != Laser.LaserState.Active) return; if (area.IsInGroup("Destroyable") && area is IDestructible destructible && CanHit(BulletOwner, destructible.BulletGroup)) { destructible.Hit(Damage, DamageType); RequestCollisionDestruction(); } } private void _on_body_entered(Node3D body) { if (!Enabled || _state != Laser.LaserState.Active) return; if (body.IsInGroup("Destroyable") && body is IDestructible destructible && CanHit(BulletOwner, destructible.BulletGroup)) { destructible.Hit(Damage, DamageType); RequestCollisionDestruction(); } } private void Destroy() { EmitSignal(SignalName.OnDestroy); PoolingManager.Instance.DisableBullet(this); } }