Files
Trinittu/dynamic-sky/scripts/weather_kill_volume.gd
2026-02-03 11:26:48 +01:00

278 lines
6.8 KiB
GDScript

@tool
class_name WeatherKillVolume
extends Area3D
enum VolumeShape {
BOX,
SPHERE,
CYLINDER
}
@export_group("Volume Settings")
@export var volume_shape: VolumeShape = VolumeShape.BOX:
set(value):
volume_shape = value
_update_collision_shape()
@export var volume_size: Vector3 = Vector3(10.0, 5.0, 10.0):
set(value):
volume_size = value
_update_collision_shape()
@export var volume_radius: float = 5.0:
set(value):
volume_radius = value
_update_collision_shape()
@export_group("Kill Behavior")
@export var enabled: bool = true
@export var priority: int = 0
@export var instant_kill: bool = true
@export var fade_out_margin: float = 1.0
@export var fade_out_duration: float = 0.3
@export_group("Particle Types")
@export var block_rain: bool = true
@export var block_snow: bool = true
@export var block_all_precipitation: bool = true
@export_group("Debug")
@export var show_debug_gizmo: bool = true
@export var debug_color: Color = Color(0.2, 0.5, 1.0, 0.3)
var _collision_shape: CollisionShape3D
var _particles_inside: Dictionary = {}
func _ready() -> void:
_setup_collision()
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
area_entered.connect(_on_area_entered)
area_exited.connect(_on_area_exited)
func _setup_collision() -> void:
_collision_shape = CollisionShape3D.new()
_collision_shape.name = "KillVolumeShape"
add_child(_collision_shape)
_update_collision_shape()
collision_layer = 0
collision_mask = 1 << 20
monitorable = true
monitoring = true
func _update_collision_shape() -> void:
if _collision_shape == null:
return
var shape: Shape3D
match volume_shape:
VolumeShape.BOX:
var box := BoxShape3D.new()
box.size = volume_size
shape = box
VolumeShape.SPHERE:
var sphere := SphereShape3D.new()
sphere.radius = volume_radius
shape = sphere
VolumeShape.CYLINDER:
var cylinder := CylinderShape3D.new()
cylinder.radius = volume_radius
cylinder.height = volume_size.y
shape = cylinder
_collision_shape.shape = shape
func _on_body_entered(body: Node3D) -> void:
if not enabled:
return
if _is_precipitation_particle(body):
_handle_particle_enter(body)
func _on_body_exited(body: Node3D) -> void:
_handle_particle_exit(body)
func _on_area_entered(area: Area3D) -> void:
if not enabled:
return
if area.has_meta("precipitation_particle"):
_handle_particle_enter(area)
func _on_area_exited(area: Area3D) -> void:
_handle_particle_exit(area)
func _is_precipitation_particle(node: Node3D) -> bool:
if node.has_meta("particle_type"):
var particle_type: String = node.get_meta("particle_type")
if block_all_precipitation:
return particle_type in ["rain", "snow", "hail", "sleet"]
if block_rain and particle_type == "rain":
return true
if block_snow and particle_type == "snow":
return true
return false
func _handle_particle_enter(node: Node3D) -> void:
if instant_kill:
_kill_particle(node)
else:
_particles_inside[node.get_instance_id()] = {
"node": node,
"fade_time": 0.0
}
func _handle_particle_exit(node: Node3D) -> void:
var id := node.get_instance_id()
if _particles_inside.has(id):
_particles_inside.erase(id)
func _kill_particle(node: Node3D) -> void:
if node.has_method("kill"):
node.call("kill")
elif node is GPUParticles3D:
node.emitting = false
elif node is CPUParticles3D:
node.emitting = false
func _process(delta: float) -> void:
if instant_kill or _particles_inside.is_empty():
return
var to_remove: Array[int] = []
for id in _particles_inside:
var data: Dictionary = _particles_inside[id]
var node: Node3D = data["node"]
if not is_instance_valid(node):
to_remove.append(id)
continue
data["fade_time"] += delta
var fade_progress: float = data["fade_time"] / fade_out_duration
if fade_progress >= 1.0:
_kill_particle(node)
to_remove.append(id)
else:
_apply_fade(node, 1.0 - fade_progress)
for id in to_remove:
_particles_inside.erase(id)
func _apply_fade(node: Node3D, alpha: float) -> void:
if node.has_method("set_alpha"):
node.call("set_alpha", alpha)
elif node is GPUParticles3D or node is CPUParticles3D:
pass
func is_point_inside(point: Vector3) -> bool:
var local_point := to_local(point)
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
return abs(local_point.x) <= half_size.x and \
abs(local_point.y) <= half_size.y and \
abs(local_point.z) <= half_size.z
VolumeShape.SPHERE:
return local_point.length() <= volume_radius
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
return horizontal_dist <= volume_radius and abs(local_point.y) <= half_height
return false
func is_point_in_fade_margin(point: Vector3) -> bool:
if fade_out_margin <= 0:
return false
var local_point := to_local(point)
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
var inner_half := half_size - Vector3(fade_out_margin, fade_out_margin, fade_out_margin)
var in_outer := abs(local_point.x) <= half_size.x and \
abs(local_point.y) <= half_size.y and \
abs(local_point.z) <= half_size.z
var in_inner := abs(local_point.x) <= inner_half.x and \
abs(local_point.y) <= inner_half.y and \
abs(local_point.z) <= inner_half.z
return in_outer and not in_inner
VolumeShape.SPHERE:
var dist := local_point.length()
return dist <= volume_radius and dist >= volume_radius - fade_out_margin
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
var in_outer := horizontal_dist <= volume_radius and abs(local_point.y) <= half_height
var in_inner := horizontal_dist <= volume_radius - fade_out_margin and \
abs(local_point.y) <= half_height - fade_out_margin
return in_outer and not in_inner
return false
func get_fade_factor(point: Vector3) -> float:
if not is_point_inside(point):
return 1.0
if not is_point_in_fade_margin(point):
return 0.0
var local_point := to_local(point)
var min_distance_to_edge := fade_out_margin
match volume_shape:
VolumeShape.BOX:
var half_size := volume_size * 0.5
min_distance_to_edge = minf(min_distance_to_edge, half_size.x - abs(local_point.x))
min_distance_to_edge = minf(min_distance_to_edge, half_size.y - abs(local_point.y))
min_distance_to_edge = minf(min_distance_to_edge, half_size.z - abs(local_point.z))
VolumeShape.SPHERE:
min_distance_to_edge = volume_radius - local_point.length()
VolumeShape.CYLINDER:
var horizontal_dist := Vector2(local_point.x, local_point.z).length()
var half_height := volume_size.y * 0.5
min_distance_to_edge = minf(volume_radius - horizontal_dist, half_height - abs(local_point.y))
return clampf(min_distance_to_edge / fade_out_margin, 0.0, 1.0)