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"); _collision = GetNode("CollisionShape3D"); _ray = GetNode("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) // ====================================================== /// /// Returns true if the given point lies within the /// current laser segment and its damage radius. /// 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; } } }