Files
Brachistochrone/resolute_driver.gd
T
scorpion21211-cmd 965f0d7584 Initial commit
2026-10-03 23:29:41 -04:00

371 lines
12 KiB
GDScript

extends Node3D
## NAF Resolute CVT-07 — analog 0..1 clips + socket FX.
## Attach to the Resolute hull root, NOT the CharacterBody3D.
## Keep names: LandingGear, Radiators, hatch_airlock_outer, HelmCamera, PlayerSpawn,
## Vfx_DrivePlume, *_Vfx, PDT_*_Muzzle, Vfx_Chaff, gimbal_pivot.
@export_range(0.0, 1.0) var gear_deploy := 1.0 : set = set_gear_deploy
@export_range(0.0, 1.0) var radiators_deploy := 1.0 : set = set_radiators_deploy
@export_range(0.0, 1.0) var hatch_airlock_open := 0.0 : set = set_hatch_airlock_open
@export_range(0.0, 1.0) var hatch_dock_open := 0.0 : set = set_hatch_dock_open
@export_range(0.0, 1.0) var hatch_cargo_open := 0.0 : set = set_hatch_cargo_open
@export_range(0.0, 1.0) var hatch_deck_open := 0.0 : set = set_hatch_deck_open
@export_range(0.0, 1.0) var torpedo_doors_open := 0.0 : set = set_torpedo_doors_open
@export_range(0.0, 1.0) var throttle := 0.0
@export var drive_on := false
@export_enum("Off", "Nose", "Tail", "Starboard", "Port", "Dorsal", "Ventral") var rcs_direction := 0
@export var rcs_attitude := false
@export var pdc_fire := false
@export var chaff_fire := false
@export var battle_lights := false : set = set_battle_lights
@export_range(-0.244, 0.244, 0.001) var gimbal_pitch := 0.0 : set = set_gimbal_pitch
@export_range(-0.244, 0.244, 0.001) var gimbal_yaw := 0.0 : set = set_gimbal_yaw
## Plug: flight / combat UI sets these later. Driver does not aim PDCs itself.
var pdc_aim_dir: Vector3 = Vector3.FORWARD
var radar_contacts: Array = []
var _heat := 0.0
var _rad_mats: Array[StandardMaterial3D] = []
var _drive_mats: Array[StandardMaterial3D] = []
var _plume: GPUParticles3D
var _rcs: Array[GPUParticles3D] = []
var _pdc: Array[GPUParticles3D] = []
var _chaff: Array[GPUParticles3D] = []
var _gimbal_rest: Basis
var _gimbal_ready := false
var _cabin_ready := false
var _cabin_lights: Array[Light3D] = []
var _cabin_rest: Array[Dictionary] = []
var _cabin_mats: Array[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:
_seek("gear_deploy", gear_deploy)
_seek("radiators_deploy", radiators_deploy)
_collect_glow()
_ensure_plume()
_arm_all("*_Vfx", 8, 0.16, 9.0, 16.0, 8.0, Color(0.86, 0.95, 1.0), 1.6, _rcs, false)
_arm_all("PDT_*_Muzzle", 120, 0.2, 320.0, 500.0, 1.4, Color(1.0, 0.9, 0.62), 50.0, _pdc, true)
_arm_all("Vfx_Chaff", 28, 1.6, 3.0, 8.0, 22.0, Color(0.92, 0.94, 0.96), 2.5, _chaff, false)
var pivot := find_child("gimbal_pivot", true, false) as Node3D
if pivot:
_gimbal_rest = pivot.transform.basis
_gimbal_ready = true
_apply_gimbal()
_collect_cabin()
print("Resolute driver plume=", _plume != null, " rcs=", _rcs.size(), " pdc=", _pdc.size(), " chaff=", _chaff.size())
func set_gimbal_pitch(v: float) -> void:
gimbal_pitch = clampf(v, -0.244, 0.244)
if _gimbal_ready:
_apply_gimbal()
func set_gimbal_yaw(v: float) -> void:
gimbal_yaw = clampf(v, -0.244, 0.244)
if _gimbal_ready:
_apply_gimbal()
func _apply_gimbal() -> void:
var pivot := find_child("gimbal_pivot", true, false) as Node3D
if pivot == null:
return
var spin := Basis(Vector3.UP, gimbal_yaw) * Basis(Vector3.RIGHT, -gimbal_pitch)
pivot.transform.basis = _gimbal_rest * spin
func _physics_process(delta: float) -> void:
var power := throttle if drive_on else 0.0
var open := 0.0 if radiators_deploy < 0.15 else clampf((radiators_deploy - 0.15) / 0.7, 0.0, 1.0)
var hard := 0.0 if power < 0.72 else (power - 0.72) / 0.28
var target := hard * open
var tau := 18.0 if target > _heat else 22.0
_heat = lerpf(_heat, target, 1.0 - exp(-delta / tau))
var glow := 0.0 if _heat < 0.55 else pow((_heat - 0.55) / 0.45, 1.7)
var hot := Color(1.0, 0.35, 0.08)
for mat in _rad_mats:
mat.emission_enabled = glow > 0.02
mat.emission = hot
mat.emission_energy_multiplier = glow * 1.15
for mat in _drive_mats:
mat.emission_enabled = power > 0.02
mat.emission = Color(0.55 * power, 0.72 * power, power)
mat.emission_energy_multiplier = power * 7.0
if _plume:
_plume.emitting = power > 0.04
_plume.amount_ratio = clampf(power, 0.0, 1.0)
var thrust: Vector3 = RCS_THRUST[clampi(rcs_direction, 0, RCS_THRUST.size() - 1)]
var ship := global_transform.basis
for p in _rcs:
if not is_instance_valid(p):
continue
if rcs_attitude:
p.emitting = true
elif thrust == Vector3.ZERO:
p.emitting = false
else:
var exhaust := ship.inverse() * p.global_transform.basis.y
p.emitting = exhaust.dot(-thrust) > 0.7
for p in _pdc:
if is_instance_valid(p):
p.emitting = pdc_fire
for p in _chaff:
if is_instance_valid(p):
p.emitting = chaff_fire
func set_battle_lights(v: bool) -> void:
battle_lights = v
if _cabin_ready:
_apply_cabin()
func _cabin_light(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Nav"):
return false
if nm.begins_with("Light_") or nm == "HelmPracticalLight" or nm.begins_with("CabinLamp") or nm.begins_with("PointLight_") or nm == "CabinFill" or nm == "PanelLight" or nm == "AirlockFlood":
return true
var p := n.get_parent()
while p:
var pn := String(p.name)
if pn == "CompanionwayWalk" or pn == "Lights" or pn == "CabinLights":
return true
if pn.begins_with("Vfx"):
return false
p = p.get_parent()
return false
func _cabin_mesh(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Nav"):
return false
return "Lamp" in nm or nm.begins_with("LightStrip") or nm.begins_with("CabinGlow")
func _collect_cabin() -> void:
_cabin_lights.clear()
_cabin_rest.clear()
_cabin_mats.clear()
for n in find_children("*", "Light3D", true, false):
if not _cabin_light(n):
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 _cabin_mesh(n):
continue
var mat := _override_std(n)
if mat == null:
continue
_cabin_mats.append({"mat": mat, "energy": mat.emission_energy_multiplier, "color": mat.emission, "on": mat.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]
if battle_lights:
lamp.light_color = Color(1.0, 0.14, 0.1)
lamp.light_energy = maxf(0.05, float(rest.energy) * 0.2)
else:
lamp.light_color = rest.color
lamp.light_energy = float(rest.energy)
for row in _cabin_mats:
var mat := row.mat as StandardMaterial3D
if battle_lights:
mat.emission_enabled = true
mat.emission = Color(1.0, 0.13, 0.09)
mat.emission_energy_multiplier = maxf(0.28, float(row.energy) * 0.45)
else:
mat.emission_enabled = bool(row.on)
mat.emission = row.color
mat.emission_energy_multiplier = float(row.energy)
func set_gear_deploy(v: float) -> void:
gear_deploy = v
_seek("gear_deploy", v)
func set_radiators_deploy(v: float) -> void:
radiators_deploy = v
_seek("radiators_deploy", v)
func set_hatch_airlock_open(v: float) -> void:
hatch_airlock_open = v
_seek("hatch_airlock_open", v)
func set_hatch_dock_open(v: float) -> void:
hatch_dock_open = v
_seek("hatch_dock_open", v)
func set_hatch_cargo_open(v: float) -> void:
hatch_cargo_open = v
_seek("hatch_cargo_open", v)
func set_hatch_deck_open(v: float) -> void:
hatch_deck_open = v
_seek("hatch_deck_open", v)
func set_torpedo_doors_open(v: float) -> void:
torpedo_doors_open = v
_seek("torpedo_doors_open", v)
func _seek(clip: StringName, t: float) -> void:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap == null:
ap = find_child("AnimationPlayer", true, false) as AnimationPlayer
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 _collect_glow() -> void:
for n in find_children("*", "MeshInstance3D", true, false):
var mat := _override_std(n)
if mat == null:
continue
if n.name.begins_with("Radiator_") and ("Fin_" in n.name or "Edge_" in n.name or "Rail" in n.name):
_rad_mats.append(mat)
elif n.name == "DriveGlow" or n.name == "DrivePlasma":
_drive_mats.append(mat)
func _override_std(mesh: MeshInstance3D) -> StandardMaterial3D:
var mat := mesh.get_active_material(0)
if mat == null or not (mat is StandardMaterial3D):
return null
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
mesh.set_surface_override_material(0, copy)
return copy
func _ensure_plume() -> void:
var socket := find_child("Vfx_DrivePlume", true, false) as Node3D
if socket == null:
return
var existing := socket.get_node_or_null("Plume")
if existing is GPUParticles3D:
_plume = existing as GPUParticles3D
return
var p := GPUParticles3D.new()
p.name = "Plume"
p.amount = 48
p.lifetime = 0.42
p.local_coords = true
p.visibility_aabb = AABB(Vector3(-2, 0, -2), Vector3(4, 40, 4))
var proc := ParticleProcessMaterial.new()
proc.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
proc.emission_sphere_radius = 0.15
proc.direction = Vector3(0, 1, 0)
proc.spread = 1.5
proc.initial_velocity_min = 50.0
proc.initial_velocity_max = 85.0
proc.gravity = Vector3.ZERO
proc.scale_min = 0.25
proc.scale_max = 0.7
proc.color = Color(0.75, 0.88, 1.0, 0.85)
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_PARTICLES
draw.vertex_color_use_as_albedo = true
draw.albedo_color = Color(0.85, 0.93, 1.0)
p.material_override = draw
p.draw_pass_1 = QuadMesh.new()
p.emitting = false
var dir := socket.position
if dir.length() < 0.001:
dir = Vector3.DOWN
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)
_plume = p
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 n.name == "Plume" or n is GPUParticles3D:
continue
var p := _make_plume(n, amount, life, vmin, vmax, spread, color, reach, streak)
if p:
into.append(p)
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
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.7 if streak else 0.08
proc.scale_max = 1.05 if streak else 0.28
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_PARTICLES
draw.albedo_color = color
p.material_override = draw
var quad := QuadMesh.new()
quad.size = Vector2(0.012, 0.95) if streak else Vector2(0.12, 0.12)
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