cirnogodot/Scripts/Actors/3D/Laser.cs
2026-01-02 16:06:26 +01:00

302 lines
8.1 KiB
C#

using Godot;
namespace Cirno.Scripts.Actors._3D;
public partial class Laser : Area3D
{
public enum LaserState
{
Inactive,
Warning,
Expanding,
Active,
Finished
}
// ================= CONFIG =================
[Export] public LaserConfig Config;
// ================= NODES =================
private MeshInstance3D _mesh;
private CollisionShape3D _collision;
private RayCast3D _ray;
private ShaderMaterial _beamMaterial;
private float _currentRadius;
// ================= STATE =================
private LaserState _state = LaserState.Inactive;
private float _stateTimer = 0f;
private Vector3 _origin;
private Vector3 _direction;
private float _currentLength;
// ================= SETUP =================
public override void _Ready()
{
_mesh = GetNode<MeshInstance3D>("MeshInstance3D");
_collision = GetNode<CollisionShape3D>("CollisionShape3D");
_ray = GetNode<RayCast3D>("RayCast3D");
_beamMaterial = _mesh.MaterialOverride as ShaderMaterial;
_ray.CollisionMask = Config.GeometryLayer;
ChangeCollisionStateDeferred(true);
//_collision.Disabled = true;
}
private void ChangeCollisionStateDeferred(bool value)
{
_collision.SetDeferred(CollisionShape3D.PropertyName.Disabled, value);
}
// ================= SPAWN API =================
public void SpawnFromDirection(Vector3 origin, Vector3 direction)
{
_origin = origin;
_direction = direction.Normalized();
StartLaser();
}
public void SpawnFromTarget(Vector3 origin, Vector3 target)
{
_origin = origin;
_direction = (target - origin).Normalized();
StartLaser();
}
private void StartLaser()
{
GlobalPosition = _origin;
//Basis = Basis.LookingAt(_origin + _direction, Vector3.Up);
_mesh.Scale = Vector3.One;
_collision.Scale = Vector3.One;
_stateTimer = 0f;
_state = Config.WarningDuration > 0
? LaserState.Warning
: LaserState.Expanding;
//UpdateLaserLength(MaxLength);
UpdateVisualOrientation();
SetRadius(Config.WarningRadius);
}
// ================= PROCESS =================
public override void _Process(double delta)
{
if (_state == LaserState.Finished)
return;
_stateTimer += (float)delta;
UpdateRaycast();
UpdateBeam();
switch (_state)
{
case LaserState.Warning:
if (_stateTimer >= Config.WarningDuration)
TransitionTo(LaserState.Expanding);
break;
case LaserState.Expanding:
HandleExpansion();
break;
case LaserState.Active:
if (Config.ActiveDuration >= 0 &&
_stateTimer >= Config.ActiveDuration)
TransitionTo(LaserState.Finished);
break;
}
}
// ================= STATE HANDLING =================
private void TransitionTo(LaserState next)
{
_state = next;
_stateTimer = 0f;
switch (next)
{
case LaserState.Expanding:
if (Config.ExpansionDuration <= 0f)
{
SetRadius(Config.DamageRadius);
EnableCollision();
TransitionTo(LaserState.Active);
}
break;
case LaserState.Active:
EnableCollision();
SetRadius(Config.DamageRadius);
break;
case LaserState.Finished:
QueueFree();
break;
}
}
private void HandleExpansion()
{
if (_stateTimer < Config.ExpansionDelay)
return;
var t = Mathf.Clamp(
(_stateTimer - Config.ExpansionDelay) / Mathf.Max(Config.ExpansionDuration, 0.001f),
0f, 1f
);
var radius = Mathf.Lerp(Config.WarningRadius, Config.DamageRadius, t);
SetRadius(radius);
if (t >= 1f)
TransitionTo(LaserState.Active);
}
// ================= RAYCAST =================
private void UpdateRaycast()
{
_ray.TargetPosition = _direction * Config.MaxLength;
_ray.CollisionMask = Config.GeometryLayer; // only solid obstacles
_ray.ForceRaycastUpdate();
_currentLength = _ray.IsColliding()
? GlobalPosition.DistanceTo(_ray.GetCollisionPoint())
: Config.MaxLength;
}
// ================= VISUALS & COLLISION =================
private void UpdateVisualOrientation()
{
// Godot's LookingAt aligns the -Z axis to the target direction.
// Our beam is built along +Y, so we must compensate.
Basis look = Basis.LookingAt(_direction, Vector3.Up);
// Rotate so +Y becomes forward instead of -Z
// This is a fixed correction: -Z → +Y
Basis correction = new Basis(Vector3.Right, Mathf.Pi / 2f);
Basis finalBasis = look * correction;
_mesh.Basis = finalBasis;
_collision.Basis = finalBasis;
}
private void UpdateBeam()
{
// 1. Set mesh height (NO SCALE)
if (_mesh.Mesh is CylinderMesh cyl)
cyl.Height = _currentLength;
// 2. Set collision height
if (_collision.Shape is CapsuleShape3D capsule)
capsule.Height = _currentLength;
// 3. Position at exact world-space center
Vector3 center = _origin + _direction * (_currentLength * 0.5f);
Transform3D meshXform = _mesh.GlobalTransform;
meshXform.Origin = center;
_mesh.GlobalTransform = meshXform;
Transform3D colXform = _collision.GlobalTransform;
colXform.Origin = center;
_collision.GlobalTransform = colXform;
}
private Vector3 GetBeamCenterWorld()
{
return _origin + _direction * (_currentLength * 0.5f);
}
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);
//_collision.Disabled = false;
}
// ======================================================
// POINT-INSIDE-LASER QUERY (NO PHYSICS REQUIRED)
// ======================================================
/// <summary>
/// Returns true if the given point lies within the
/// current laser segment and its damage radius.
/// </summary>
public bool IsPointInsideBeam(Vector3 worldPoint)
{
if (_state != LaserState.Active)
return false;
Vector3 local = worldPoint - _origin;
float projection = local.Dot(_direction);
if (projection < 0 || projection > _currentLength)
return false;
Vector3 closestPoint =
_origin + _direction * projection;
float distanceSq =
worldPoint.DistanceSquaredTo(closestPoint);
float radius = Config.DamageRadius;
return distanceSq <= radius * radius;
}
private void UpdateMaterial()
{
if (_beamMaterial == null)
return;
_beamMaterial.SetShaderParameter("beam_length", _currentLength);
_beamMaterial.SetShaderParameter("beam_radius", _currentRadius);
switch (_state)
{
case LaserState.Warning:
_beamMaterial.SetShaderParameter("beam_color", new Color(1f, 1f, 0.2f));
_beamMaterial.SetShaderParameter("intensity", 0.5f);
break;
case LaserState.Active:
_beamMaterial.SetShaderParameter("beam_color", new Color(1f, 0.2f, 0.2f));
_beamMaterial.SetShaderParameter("intensity", 1.0f);
break;
}
}
}