9a8a946931
arcology updated ship models in particle fx is broken on kestrel
358 lines
13 KiB
GDScript
358 lines
13 KiB
GDScript
extends Node3D
|
|
## NAF Resolute CVT-07 — analog 0..1 clip driver.
|
|
## Keep node names stable: LandingGear, Radiators, hatch_airlock_outer, HelmCamera, PlayerSpawn.
|
|
##
|
|
## Effects are inspector toggles, not clips.
|
|
## Particle look is PARTICLE_FX.md. Drive and RCS are crossed additive ribbons, not sprays.
|
|
## drive_on plus throttle runs the plume. Radiator panels stay grey metal unless the burn is hard and has been hard for a while.
|
|
## rcs_direction fires only the bells that push that way. Off fires nothing.
|
|
## pdc_fire is a stream of short tracer bolts, not one beam. chaff_fire dumps the dispensers.
|
|
## Torpedo bodies are not particles. Use the torpedo door clip, then fly a real round later.
|
|
## Sockets: Vfx_DrivePlume, *_Vfx, PDT_*_Muzzle, Vfx_Chaff.
|
|
|
|
const Fx = preload("res://naf_fx.gd")
|
|
|
|
@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 pdc_fire := false
|
|
@export var chaff_fire := false
|
|
## Cabin practicals. Off is the normal warm watch. On drops the hab to dim red battle lighting.
|
|
@export var battle_lights := false : set = set_battle_lights
|
|
## Torch vector. Positive pitch swings the bell ventral and the nose down. Positive yaw swings the bell to starboard and the nose to port. No roll. Limit ±14°.
|
|
@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
|
|
|
|
var _heat := 0.0
|
|
var _spool := 0.0
|
|
var _time := 0.0
|
|
var _rad_mats: Array[StandardMaterial3D] = []
|
|
var _drive_mats: Array[StandardMaterial3D] = []
|
|
var _drive_socket: Node3D
|
|
var _drive_core: Node3D
|
|
var _drive_sheath: Node3D
|
|
var _drive_light: SpotLight3D
|
|
var _rcs: Array[Node3D] = []
|
|
var _muzzles: Array[Node3D] = []
|
|
var _tracers: Array[Dictionary] = []
|
|
var _tracer_pool: Array[MeshInstance3D] = []
|
|
var _tracer_mesh: ArrayMesh
|
|
var _tracer_mat: StandardMaterial3D
|
|
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()
|
|
_arm_fx()
|
|
var pivot := find_child("gimbal_pivot", true, false) as Node3D
|
|
if pivot:
|
|
_gimbal_rest = pivot.transform.basis
|
|
_gimbal_ready = true
|
|
_apply_gimbal()
|
|
_collect_cabin()
|
|
set_physics_process(true)
|
|
|
|
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:
|
|
_time += delta
|
|
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
|
|
var spool_target := 1.0 if power > 0.04 else 0.0
|
|
_spool = Fx.smooth(_spool, spool_target, delta, 0.26 if spool_target > 0.0 else 0.07)
|
|
var plume := power * _spool
|
|
if _drive_core:
|
|
var scale := _drive_reach_scale()
|
|
Fx.set_spear(_drive_core, plume, scale, _time)
|
|
Fx.set_spear(_drive_sheath, plume * 0.82, scale, _time)
|
|
if _drive_light:
|
|
_drive_light.light_energy = plume * 5.0 if plume > 0.04 else 0.0
|
|
var reach := float(_drive_core.get_meta("len")) * scale
|
|
_drive_light.spot_range = minf(float(_drive_core.get_meta("len")), maxf(2.2, reach))
|
|
var thrust: Vector3 = _RCS_THRUST[clampi(rcs_direction, 0, _RCS_THRUST.size() - 1)]
|
|
var ship := global_transform.basis
|
|
for ribbon in _rcs:
|
|
var want := 0.0
|
|
if thrust != Vector3.ZERO:
|
|
var exhaust: Vector3 = ship.inverse() * ribbon.global_transform.basis.y
|
|
if exhaust.dot(-thrust) > 0.7:
|
|
want = 1.0
|
|
var burn := float(ribbon.get_meta("burn"))
|
|
burn = Fx.smooth(burn, want, delta, 0.08 if want > burn else 0.14)
|
|
ribbon.set_meta("burn", burn)
|
|
Fx.set_spear(ribbon, burn, 1.0, _time)
|
|
_step_pdc(delta)
|
|
for cloud in _chaff:
|
|
cloud.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 _is_rcs_socket(n: Node) -> bool:
|
|
var nm := String(n.name)
|
|
if nm == "Vfx_DrivePlume" or nm == "Vfx_Chaff" or nm.begins_with("Vfx_Abort"):
|
|
return false
|
|
return nm.ends_with("_Vfx") or nm.begins_with("Vfx_RCS_")
|
|
|
|
func _is_muzzle(n: Node) -> bool:
|
|
var nm := String(n.name)
|
|
return nm.begins_with("PDT_") and nm.ends_with("_Muzzle")
|
|
|
|
func _arm_fx() -> void:
|
|
_tracer_mesh = Fx.tracer_mesh()
|
|
_tracer_mat = Fx.tracer_mat()
|
|
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
|
|
if drive:
|
|
_drive_socket = drive
|
|
# Ice-white core, 38 m, three crossed planes. Blue sheath is shorter and wider.
|
|
_drive_core = Fx.spear(drive, 38.0, 1.35, Color(0.957, 0.973, 1.0, 0.95), Color(0.47, 0.71, 1.0, 0.35), Color(0.24, 0.47, 1.0, 0.0), 0.95, 3, 1.0, "DriveCore")
|
|
_drive_sheath = Fx.spear(drive, 26.0, 3.2, Color(0.353, 0.659, 1.0, 0.95), Color(0.17, 0.47, 1.0, 0.35), Color(0.08, 0.22, 0.55, 0.0), 0.5, 3, 2.0, "DriveSheath")
|
|
_drive_light = Fx.spot(drive, 0.35, Color(0.773, 0.863, 1.0), 0.3, 7.5, 2.0)
|
|
for n in find_children("*", "Node3D", true, false):
|
|
if _is_rcs_socket(n):
|
|
# 1.2 m pale spear, two crossed planes. One per bell.
|
|
_rcs.append(Fx.spear(n, 1.2, 0.09, Color(0.863, 0.937, 1.0, 0.95), Color(0.41, 0.67, 1.0, 0.35), Color(0.25, 0.45, 0.8, 0.0), 0.82, 2, 1.0, "Spear"))
|
|
elif _is_muzzle(n):
|
|
_muzzles.append(n)
|
|
n.set_meta("acc", randf() * 0.02)
|
|
elif String(n.name) == "Vfx_Chaff":
|
|
_chaff.append(Fx.chaff(n))
|
|
|
|
func _drive_reach_scale() -> float:
|
|
if _drive_socket == null or _drive_core == null:
|
|
return 1.0
|
|
var length := float(_drive_core.get_meta("len"))
|
|
var exhaust: Vector3 = (_drive_socket.global_transform.basis * _drive_core.basis.y).normalized()
|
|
if exhaust.y >= -0.35:
|
|
return 1.0
|
|
# Pad is world y=-2.45. Clip only when the bell is above it.
|
|
var room := (_drive_socket.global_position.y - (-2.45 + 0.22)) / -exhaust.y
|
|
if room <= 0.0:
|
|
return 1.0
|
|
var reach := maxf(0.9, minf(length, room))
|
|
return reach / length
|
|
|
|
func _take_tracer() -> MeshInstance3D:
|
|
if _tracer_pool.size() > 0:
|
|
return _tracer_pool.pop_back()
|
|
var m := MeshInstance3D.new()
|
|
m.mesh = _tracer_mesh
|
|
m.material_override = _tracer_mat
|
|
m.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
m.visible = false
|
|
add_child(m)
|
|
return m
|
|
|
|
func _step_pdc(delta: float) -> void:
|
|
if pdc_fire:
|
|
for mz in _muzzles:
|
|
var acc := float(mz.get_meta("acc")) - delta
|
|
var shots := 0
|
|
while acc <= 0.0 and shots < 2 and _tracers.size() < 90:
|
|
acc += 1.0 / 42.0
|
|
shots += 1
|
|
_spawn_tracer(mz)
|
|
mz.set_meta("acc", acc)
|
|
for i in range(_tracers.size() - 1, -1, -1):
|
|
var t: Dictionary = _tracers[i]
|
|
t.life = float(t.life) - delta
|
|
var mesh := t.mesh as MeshInstance3D
|
|
if float(t.life) <= 0.0:
|
|
mesh.visible = false
|
|
_tracer_pool.append(mesh)
|
|
_tracers.remove_at(i)
|
|
continue
|
|
var local_dir: Vector3 = t.dir
|
|
mesh.position += local_dir * float(t.speed) * delta
|
|
|
|
func _spawn_tracer(mz: Node3D) -> void:
|
|
var mesh := _take_tracer()
|
|
var world_dir: Vector3 = (mz.global_transform.basis * Fx.exhaust_local(mz)).normalized()
|
|
var shot := Fx.cone(world_dir, 1.15)
|
|
var origin := mz.global_position + world_dir * 0.18
|
|
var local_dir: Vector3 = global_transform.basis.inverse() * shot
|
|
mesh.position = to_local(origin)
|
|
mesh.basis = Fx.basis_y(local_dir)
|
|
mesh.scale = Vector3(1, randf_range(1.5, 2.3), 1)
|
|
mesh.visible = true
|
|
_tracers.append({
|
|
"mesh": mesh,
|
|
"dir": local_dir,
|
|
"life": randf_range(0.12, 0.2),
|
|
"speed": randf_range(90.0, 140.0),
|
|
})
|