2026-01-31 10:23:10 +01:00
|
|
|
|
using Cirno.Scripts.Actors._3D;
|
|
|
|
|
|
using Cirno.Scripts.Components;
|
|
|
|
|
|
using Cirno.Scripts.Controllers;
|
|
|
|
|
|
using Cirno.Scripts.Utils;
|
|
|
|
|
|
using Godot;
|
2025-09-29 10:45:57 +02:00
|
|
|
|
|
2026-01-31 10:23:10 +01:00
|
|
|
|
namespace Cirno.Scripts.Weapons;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Laser implementation that conforms to the IBullet interface.
|
|
|
|
|
|
/// Wraps the Laser class functionality to integrate with the bullet system.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public partial class LaserBullet3D : Area3D, IBullet
|
2025-09-29 10:45:57 +02:00
|
|
|
|
{
|
2026-01-31 10:23:10 +01:00
|
|
|
|
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>("MeshInstance3D");
|
|
|
|
|
|
_collision = GetNode<CollisionShape3D>("CollisionShape3D");
|
|
|
|
|
|
_ray = GetNode<RayCast3D>("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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|