280 lines
8.7 KiB
GDScript
280 lines
8.7 KiB
GDScript
@tool
|
|
extends Node3D
|
|
## Kestrel EC-01 — analog 0..1 clip driver + socket plumes.
|
|
## Attach to the Kestrel child (inherited hull scene), NOT the CharacterBody3D.
|
|
## Sockets: Vfx_RCS_* and Vfx_Abort_* must be Node3D (not GPUParticles).
|
|
## Ship axes: nose +Y, starboard +X, dorsal +Z.
|
|
|
|
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
|
|
@export_range(0.0, 1.0) var hatch_crew_open := 0.0 : set = set_hatch_crew_open
|
|
@export_range(0.0, 1.0) var hatch_ladder_deploy := 0.0 : set = set_hatch_ladder_deploy
|
|
@export_range(0.0, 1.0) var hatch_dock_open := 0.0 : set = set_hatch_dock_open
|
|
@export_range(0.0, 1.0) var instruments_demo := 0.4 : set = set_instruments_demo
|
|
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
|
|
@export var att_pitch := 0.0
|
|
@export var att_yaw := 0.0
|
|
@export var att_roll := 0.0
|
|
@export var abort_burn := false
|
|
@export var cabin_dim := false : set = set_cabin_dim
|
|
@export var clip_speed := 1.35
|
|
|
|
var _rcs: Array[GPUParticles3D] = []
|
|
var _abort: Array[GPUParticles3D] = []
|
|
var _cabin_ready := false
|
|
var _cabin_lights: Array[Light3D] = []
|
|
var _cabin_rest: Array[Dictionary] = []
|
|
var _cabin_mats: Array[Dictionary] = []
|
|
var _clip_pos: Dictionary = {}
|
|
var _clip_tgt: Dictionary = {}
|
|
|
|
const RCS_THRUST := [
|
|
Vector3.ZERO,
|
|
Vector3(0, 1, 0),
|
|
Vector3(0, -1, 0),
|
|
Vector3(1, 0, 0),
|
|
Vector3(-1, 0, 0),
|
|
Vector3(0, 0, 1),
|
|
Vector3(0, 0, -1),
|
|
]
|
|
|
|
|
|
func _ready() -> void:
|
|
_clip_tgt = {
|
|
"gear_deploy": gear_deploy,
|
|
"hatch_crew_open": hatch_crew_open,
|
|
"hatch_ladder_deploy": hatch_ladder_deploy,
|
|
"hatch_dock_open": hatch_dock_open,
|
|
"instruments_demo": instruments_demo,
|
|
}
|
|
_clip_pos = _clip_tgt.duplicate()
|
|
for clip in _clip_tgt.keys():
|
|
_apply_clip(clip, float(_clip_pos[clip]))
|
|
_arm_all("Vfx_RCS_*", 18, 0.09, 14.0, 22.0, 2.2, Color(0.82, 0.93, 1.0), 1.4, _rcs, false)
|
|
_arm_all("Vfx_Abort_*", 36, 0.16, 28.0, 46.0, 1.6, Color(1.0, 0.58, 0.22), 5.0, _abort, true)
|
|
_collect_cabin()
|
|
set_process(true)
|
|
print("Kestrel driver ready rcs=", _rcs.size(), " abort=", _abort.size())
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_tick_clips(delta)
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
var cmd := Vector3(att_pitch, att_roll, -att_yaw)
|
|
var xlat: Vector3 = RCS_THRUST[clampi(rcs_direction, 0, RCS_THRUST.size() - 1)]
|
|
var origin := global_position
|
|
var ship := global_transform.basis
|
|
var turning := cmd.length() > 0.15
|
|
for p in _rcs:
|
|
if not is_instance_valid(p):
|
|
continue
|
|
var exhaust: Vector3 = (ship.inverse() * p.global_transform.basis.y).normalized()
|
|
var fire := false
|
|
if turning:
|
|
var local_pos: Vector3 = ship.inverse() * (p.global_position - origin)
|
|
if local_pos.length() < 0.05:
|
|
local_pos = -exhaust
|
|
var torque: Vector3 = local_pos.cross(-exhaust)
|
|
fire = torque.dot(cmd) > 0.12
|
|
if not fire and xlat != Vector3.ZERO:
|
|
fire = exhaust.dot(-xlat) > 0.55
|
|
p.emitting = fire
|
|
for p in _abort:
|
|
if is_instance_valid(p):
|
|
p.emitting = abort_burn
|
|
|
|
|
|
func set_cabin_dim(v: bool) -> void:
|
|
cabin_dim = v
|
|
if _cabin_ready:
|
|
_apply_cabin()
|
|
|
|
|
|
func set_gear_deploy(v: float) -> void:
|
|
gear_deploy = v
|
|
_clip_tgt["gear_deploy"] = clampf(v, 0.0, 1.0)
|
|
|
|
|
|
func set_hatch_crew_open(v: float) -> void:
|
|
hatch_crew_open = v
|
|
_clip_tgt["hatch_crew_open"] = clampf(v, 0.0, 1.0)
|
|
|
|
|
|
func set_hatch_ladder_deploy(v: float) -> void:
|
|
hatch_ladder_deploy = v
|
|
_clip_tgt["hatch_ladder_deploy"] = clampf(v, 0.0, 1.0)
|
|
|
|
|
|
func set_hatch_dock_open(v: float) -> void:
|
|
hatch_dock_open = v
|
|
_clip_tgt["hatch_dock_open"] = clampf(v, 0.0, 1.0)
|
|
|
|
|
|
func set_instruments_demo(v: float) -> void:
|
|
instruments_demo = v
|
|
_clip_tgt["instruments_demo"] = clampf(v, 0.0, 1.0)
|
|
|
|
|
|
func _tick_clips(delta: float) -> void:
|
|
for clip in _clip_tgt.keys():
|
|
var tgt := float(_clip_tgt[clip])
|
|
var pos := float(_clip_pos.get(clip, tgt))
|
|
if absf(pos - tgt) < 0.001:
|
|
if pos != tgt:
|
|
_clip_pos[clip] = tgt
|
|
_apply_clip(clip, tgt)
|
|
continue
|
|
pos = move_toward(pos, tgt, clip_speed * delta)
|
|
_clip_pos[clip] = pos
|
|
_apply_clip(clip, pos)
|
|
|
|
|
|
func _apply_clip(clip: StringName, t: float) -> void:
|
|
var ap := _ap()
|
|
if ap == null or not ap.has_animation(clip):
|
|
return
|
|
var len := ap.get_animation(clip).length
|
|
ap.play(clip)
|
|
ap.seek(clampf(t, 0.0, 1.0) * len, true)
|
|
ap.pause()
|
|
|
|
|
|
func _ap() -> AnimationPlayer:
|
|
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
|
|
if ap == null:
|
|
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
|
|
return ap
|
|
|
|
|
|
func _seek(clip: StringName, t: float) -> void:
|
|
_apply_clip(clip, t)
|
|
|
|
|
|
func _collect_cabin() -> void:
|
|
_cabin_lights.clear()
|
|
_cabin_rest.clear()
|
|
_cabin_mats.clear()
|
|
for n in find_children("*", "Light3D", true, false):
|
|
var nm := String(n.name)
|
|
if nm.begins_with("Nav"):
|
|
continue
|
|
if not (nm.begins_with("PointLight_") or nm == "CabinFill" or nm == "PanelLight" or nm.begins_with("CabinLamp")):
|
|
var p := n.get_parent()
|
|
var cabin := false
|
|
while p:
|
|
if String(p.name) == "CabinLights":
|
|
cabin = true
|
|
break
|
|
p = p.get_parent()
|
|
if not cabin:
|
|
continue
|
|
_cabin_lights.append(n)
|
|
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
|
|
for n in find_children("*", "MeshInstance3D", true, false):
|
|
if not String(n.name).begins_with("LightStrip"):
|
|
continue
|
|
var mesh := n as MeshInstance3D
|
|
var mat := mesh.get_active_material(0)
|
|
if mat == null or not (mat is StandardMaterial3D):
|
|
continue
|
|
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
|
|
mesh.set_surface_override_material(0, copy)
|
|
_cabin_mats.append({
|
|
"mat": copy,
|
|
"energy": copy.emission_energy_multiplier,
|
|
"color": copy.emission,
|
|
"on": copy.emission_enabled
|
|
})
|
|
_cabin_ready = true
|
|
_apply_cabin()
|
|
|
|
|
|
func _apply_cabin() -> void:
|
|
for i in _cabin_lights.size():
|
|
var lamp := _cabin_lights[i]
|
|
var rest: Dictionary = _cabin_rest[i]
|
|
lamp.light_color = rest.color
|
|
lamp.light_energy = float(rest.energy) * (0.16 if cabin_dim else 1.0)
|
|
for row in _cabin_mats:
|
|
var mat := row.mat as StandardMaterial3D
|
|
mat.emission_enabled = true if cabin_dim else bool(row.on)
|
|
mat.emission = row.color
|
|
mat.emission_energy_multiplier = float(row.energy) * (0.18 if cabin_dim else 1.0)
|
|
|
|
|
|
func _arm_all(pattern: String, amount: int, life: float, vmin: float, vmax: float, spread: float, color: Color, reach: float, into: Array[GPUParticles3D], streak: bool) -> void:
|
|
for n in find_children(pattern, "Node3D", true, false):
|
|
if String(n.name) == "Plume":
|
|
continue
|
|
if n is GPUParticles3D:
|
|
into.append(n as GPUParticles3D)
|
|
continue
|
|
var authored := _find_authored_plume(n)
|
|
if authored:
|
|
into.append(authored)
|
|
continue
|
|
var p := _make_plume(n, amount, life, vmin, vmax, spread, color, reach, streak)
|
|
if p:
|
|
into.append(p)
|
|
|
|
|
|
func _find_authored_plume(socket: Node3D) -> GPUParticles3D:
|
|
if socket is GPUParticles3D:
|
|
return socket as GPUParticles3D
|
|
for child in socket.get_children():
|
|
if child is GPUParticles3D and String(child.name) != "Plume":
|
|
return child as GPUParticles3D
|
|
var ours := socket.get_node_or_null("Plume")
|
|
if ours is GPUParticles3D:
|
|
return ours as GPUParticles3D
|
|
return null
|
|
|
|
|
|
func _make_plume(socket: Node3D, amount: int, life: float, vmin: float, vmax: float, spread: float, color: Color, reach: float, streak: bool) -> GPUParticles3D:
|
|
var existing := socket.get_node_or_null("Plume")
|
|
if existing is GPUParticles3D:
|
|
return existing as GPUParticles3D
|
|
if existing:
|
|
existing.free()
|
|
var p := GPUParticles3D.new()
|
|
p.name = "Plume"
|
|
p.amount = amount
|
|
p.lifetime = life
|
|
p.local_coords = true
|
|
p.visibility_aabb = AABB(Vector3(-reach, 0, -reach), Vector3(reach * 2.0, reach * 4.0, reach * 2.0))
|
|
var proc := ParticleProcessMaterial.new()
|
|
proc.direction = Vector3(0, 1, 0)
|
|
proc.spread = spread
|
|
proc.initial_velocity_min = vmin
|
|
proc.initial_velocity_max = vmax
|
|
proc.gravity = Vector3.ZERO
|
|
proc.scale_min = 0.55 if streak else 0.04
|
|
proc.scale_max = 0.95 if streak else 0.09
|
|
proc.color = color
|
|
if streak:
|
|
proc.particle_flag_align_y = true
|
|
p.process_material = proc
|
|
var draw := StandardMaterial3D.new()
|
|
draw.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
draw.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
|
draw.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
draw.billboard_mode = BaseMaterial3D.BILLBOARD_DISABLED if streak else BaseMaterial3D.BILLBOARD_PARTICLES
|
|
draw.albedo_color = color
|
|
p.material_override = draw
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(0.07, 0.55) if streak else Vector2(0.035, 0.035)
|
|
p.draw_pass_1 = quad
|
|
p.emitting = false
|
|
var dir := socket.position
|
|
if dir.length() < 0.001:
|
|
dir = Vector3.UP
|
|
dir = (socket.quaternion.inverse() * dir).normalized()
|
|
var y := dir
|
|
var up := Vector3.FORWARD if absf(y.dot(Vector3.UP)) > 0.9 else Vector3.UP
|
|
var x := up.cross(y).normalized()
|
|
var z := x.cross(y).normalized()
|
|
p.basis = Basis(x, y, z)
|
|
socket.add_child(p)
|
|
return p
|