@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 kill_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: bool = 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: bool = 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: bool = horizontal_dist <= volume_radius and abs(local_point.y) <= half_height var in_inner: bool = 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)