300 lines
8.1 KiB
GDScript
300 lines
8.1 KiB
GDScript
@tool
|
|
class_name VFXController
|
|
extends Node3D
|
|
|
|
signal shooting_star_spawned(position: Vector3, direction: Vector3)
|
|
signal meteor_shower_started()
|
|
signal meteor_shower_ended()
|
|
|
|
@export_group("Shooting Stars")
|
|
@export var shooting_stars_enabled: bool = true
|
|
@export var shooting_star_frequency: float = 0.05
|
|
@export var min_spawn_interval: float = 5.0
|
|
@export var max_spawn_interval: float = 30.0
|
|
|
|
@export_group("Shooting Star Appearance")
|
|
@export var trail_length: float = 2.0
|
|
@export var trail_width: float = 0.05
|
|
@export var star_brightness: float = 1.5
|
|
@export var star_color: Color = Color(1.0, 1.0, 0.9)
|
|
@export var trail_fade_time: float = 0.5
|
|
|
|
@export_group("Meteor Shower")
|
|
@export var meteor_shower_active: bool = false
|
|
@export var meteor_shower_intensity: float = 1.0
|
|
@export var meteors_per_second: float = 3.0
|
|
@export var meteor_shower_duration: float = 60.0
|
|
|
|
@export_group("Meteor Appearance")
|
|
@export var meteor_trail_length: float = 4.0
|
|
@export var meteor_brightness: float = 2.0
|
|
@export var meteor_color: Color = Color(1.0, 0.8, 0.5)
|
|
|
|
@export_group("Spawn Area")
|
|
@export var spawn_radius: float = 500.0
|
|
@export var spawn_height_min: float = 100.0
|
|
@export var spawn_height_max: float = 200.0
|
|
@export var spawn_direction_variance: float = 30.0
|
|
|
|
@export_group("Audio")
|
|
@export var meteor_shower_audio_event: String = ""
|
|
|
|
var _spawn_timer: float = 0.0
|
|
var _next_spawn_time: float = 0.0
|
|
var _meteor_timer: float = 0.0
|
|
var _meteor_spawn_accumulator: float = 0.0
|
|
var _meteor_shower_elapsed: float = 0.0
|
|
var _active_stars: Array[Dictionary] = []
|
|
var _star_mesh: MeshInstance3D
|
|
var _star_material: ShaderMaterial
|
|
var _is_night: bool = false
|
|
|
|
|
|
func _ready() -> void:
|
|
_setup_star_mesh()
|
|
_schedule_next_shooting_star()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not _is_night:
|
|
return
|
|
|
|
if shooting_stars_enabled:
|
|
_process_shooting_stars(delta)
|
|
|
|
if meteor_shower_active:
|
|
_process_meteor_shower(delta)
|
|
|
|
_update_active_stars(delta)
|
|
|
|
|
|
func _setup_star_mesh() -> void:
|
|
_star_material = ShaderMaterial.new()
|
|
_star_material.shader = _get_star_shader()
|
|
_star_material.set_shader_parameter("color", star_color)
|
|
_star_material.set_shader_parameter("brightness", star_brightness)
|
|
|
|
|
|
func _get_star_shader() -> Shader:
|
|
var shader := Shader.new()
|
|
shader.code = """
|
|
shader_type spatial;
|
|
render_mode unshaded, depth_draw_never, cull_disabled, blend_add;
|
|
|
|
uniform vec4 color : source_color = vec4(1.0, 1.0, 0.9, 1.0);
|
|
uniform float brightness = 1.5;
|
|
uniform float fade = 1.0;
|
|
uniform float trail_progress = 0.0;
|
|
|
|
void vertex() {
|
|
VERTEX.y *= mix(0.1, 1.0, trail_progress);
|
|
}
|
|
|
|
void fragment() {
|
|
float gradient = UV.y;
|
|
float alpha = gradient * fade;
|
|
ALBEDO = color.rgb * brightness;
|
|
ALPHA = alpha * color.a;
|
|
}
|
|
"""
|
|
return shader
|
|
|
|
|
|
func _schedule_next_shooting_star() -> void:
|
|
_next_spawn_time = randf_range(min_spawn_interval, max_spawn_interval)
|
|
_next_spawn_time /= maxf(0.01, shooting_star_frequency)
|
|
_spawn_timer = 0.0
|
|
|
|
|
|
func _process_shooting_stars(delta: float) -> void:
|
|
_spawn_timer += delta
|
|
|
|
if _spawn_timer >= _next_spawn_time:
|
|
_spawn_shooting_star()
|
|
_schedule_next_shooting_star()
|
|
|
|
|
|
func _process_meteor_shower(delta: float) -> void:
|
|
_meteor_shower_elapsed += delta
|
|
|
|
if _meteor_shower_elapsed >= meteor_shower_duration:
|
|
stop_meteor_shower()
|
|
return
|
|
|
|
_meteor_spawn_accumulator += meteors_per_second * meteor_shower_intensity * delta
|
|
|
|
while _meteor_spawn_accumulator >= 1.0:
|
|
_meteor_spawn_accumulator -= 1.0
|
|
_spawn_meteor()
|
|
|
|
|
|
func _spawn_shooting_star() -> void:
|
|
var spawn_pos := _get_random_sky_position()
|
|
var direction := _get_random_fall_direction()
|
|
|
|
var star_data := {
|
|
"position": spawn_pos,
|
|
"direction": direction,
|
|
"speed": randf_range(50.0, 100.0),
|
|
"lifetime": 0.0,
|
|
"max_lifetime": trail_fade_time + randf_range(0.2, 0.5),
|
|
"trail_length": trail_length * randf_range(0.8, 1.2),
|
|
"brightness": star_brightness * randf_range(0.8, 1.2),
|
|
"color": star_color,
|
|
"mesh": _create_star_trail(spawn_pos, direction),
|
|
"is_meteor": false
|
|
}
|
|
|
|
_active_stars.append(star_data)
|
|
shooting_star_spawned.emit(spawn_pos, direction)
|
|
|
|
|
|
func _spawn_meteor() -> void:
|
|
var spawn_pos := _get_random_sky_position()
|
|
var direction := _get_random_fall_direction()
|
|
|
|
var star_data := {
|
|
"position": spawn_pos,
|
|
"direction": direction,
|
|
"speed": randf_range(80.0, 150.0),
|
|
"lifetime": 0.0,
|
|
"max_lifetime": trail_fade_time + randf_range(0.3, 0.7),
|
|
"trail_length": meteor_trail_length * randf_range(0.8, 1.2),
|
|
"brightness": meteor_brightness * randf_range(0.8, 1.2),
|
|
"color": meteor_color,
|
|
"mesh": _create_star_trail(spawn_pos, direction, true),
|
|
"is_meteor": true
|
|
}
|
|
|
|
_active_stars.append(star_data)
|
|
|
|
|
|
func _get_random_sky_position() -> Vector3:
|
|
var angle := randf() * TAU
|
|
var radius := randf_range(spawn_radius * 0.3, spawn_radius)
|
|
var height := randf_range(spawn_height_min, spawn_height_max)
|
|
|
|
return Vector3(
|
|
cos(angle) * radius,
|
|
height,
|
|
sin(angle) * radius
|
|
)
|
|
|
|
|
|
func _get_random_fall_direction() -> Vector3:
|
|
var base_dir := Vector3(-0.5, -0.8, -0.3).normalized()
|
|
|
|
var variance := deg_to_rad(spawn_direction_variance)
|
|
var rotation := Basis(Vector3.UP, randf() * TAU)
|
|
rotation = rotation.rotated(Vector3.RIGHT, randf_range(-variance, variance))
|
|
|
|
return (rotation * base_dir).normalized()
|
|
|
|
|
|
func _create_star_trail(position: Vector3, direction: Vector3, is_meteor: bool = false) -> MeshInstance3D:
|
|
var mesh_instance := MeshInstance3D.new()
|
|
|
|
var length := meteor_trail_length if is_meteor else trail_length
|
|
var width := trail_width * (1.5 if is_meteor else 1.0)
|
|
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(width, length)
|
|
mesh_instance.mesh = quad
|
|
|
|
var material := _star_material.duplicate() as ShaderMaterial
|
|
material.set_shader_parameter("color", meteor_color if is_meteor else star_color)
|
|
material.set_shader_parameter("brightness", meteor_brightness if is_meteor else star_brightness)
|
|
mesh_instance.material_override = material
|
|
|
|
mesh_instance.position = position
|
|
mesh_instance.look_at(position + direction, Vector3.UP)
|
|
mesh_instance.rotate_object_local(Vector3.RIGHT, PI / 2.0)
|
|
|
|
add_child(mesh_instance)
|
|
return mesh_instance
|
|
|
|
|
|
func _update_active_stars(delta: float) -> void:
|
|
var stars_to_remove: Array[int] = []
|
|
|
|
for i in _active_stars.size():
|
|
var star: Dictionary = _active_stars[i]
|
|
star["lifetime"] += delta
|
|
|
|
var progress: float = star["lifetime"] / star["max_lifetime"]
|
|
|
|
if progress >= 1.0:
|
|
stars_to_remove.append(i)
|
|
continue
|
|
|
|
var mesh: MeshInstance3D = star["mesh"]
|
|
if is_instance_valid(mesh):
|
|
star["position"] += star["direction"] * star["speed"] * delta
|
|
mesh.position = star["position"]
|
|
|
|
var fade := 1.0 - smoothstep(0.5, 1.0, progress)
|
|
var material := mesh.material_override as ShaderMaterial
|
|
if material:
|
|
material.set_shader_parameter("fade", fade)
|
|
material.set_shader_parameter("trail_progress", minf(progress * 3.0, 1.0))
|
|
|
|
for i in range(stars_to_remove.size() - 1, -1, -1):
|
|
var idx: int = stars_to_remove[i]
|
|
var star: Dictionary = _active_stars[idx]
|
|
var mesh: MeshInstance3D = star["mesh"]
|
|
if is_instance_valid(mesh):
|
|
mesh.queue_free()
|
|
_active_stars.remove_at(idx)
|
|
|
|
|
|
func set_night_mode(is_night: bool) -> void:
|
|
_is_night = is_night
|
|
|
|
if not is_night:
|
|
_clear_all_stars()
|
|
|
|
|
|
func _clear_all_stars() -> void:
|
|
for star in _active_stars:
|
|
var mesh: MeshInstance3D = star["mesh"]
|
|
if is_instance_valid(mesh):
|
|
mesh.queue_free()
|
|
_active_stars.clear()
|
|
|
|
|
|
func start_meteor_shower(duration: float = -1.0) -> void:
|
|
if duration > 0:
|
|
meteor_shower_duration = duration
|
|
|
|
meteor_shower_active = true
|
|
_meteor_shower_elapsed = 0.0
|
|
_meteor_spawn_accumulator = 0.0
|
|
|
|
meteor_shower_started.emit()
|
|
|
|
if meteor_shower_audio_event != "":
|
|
pass
|
|
|
|
|
|
func stop_meteor_shower() -> void:
|
|
meteor_shower_active = false
|
|
_meteor_shower_elapsed = 0.0
|
|
meteor_shower_ended.emit()
|
|
|
|
|
|
func trigger_shooting_star() -> void:
|
|
if _is_night:
|
|
_spawn_shooting_star()
|
|
|
|
|
|
func set_shooting_star_appearance(color: Color, brightness: float, length: float) -> void:
|
|
star_color = color
|
|
star_brightness = brightness
|
|
trail_length = length
|
|
|
|
|
|
func set_meteor_appearance(color: Color, brightness: float, length: float) -> void:
|
|
meteor_color = color
|
|
meteor_brightness = brightness
|
|
meteor_trail_length = length
|