extends Node3D ## NAF Phantom II (shrike). Godot 4.7. Units: 1 m. ## Axes: nose +Z, belly -Y, starboard +X. The torch exhausts -Z. ## rcs_direction uses those axes. Nose thrust is +Z. Dorsal thrust is +Y. ## See PARTICLE_FX.md. 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 canopy_open := 0.0 : set = set_canopy_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 @export var cabin_dim := false : set = set_cabin_dim var _time := 0.0 var _spool := 0.0 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 _cabin_ready := false var _cabin_lights: Array[Light3D] = [] var _cabin_rest: Array[Dictionary] = [] const _RCS_THRUST := [ Vector3.ZERO, Vector3(0, 0, 1), Vector3(0, 0, -1), Vector3(1, 0, 0), Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, -1, 0), ] func _ready() -> void: _seek("gear_deploy", gear_deploy) _arm_fx() _collect_cabin() set_physics_process(true) func _physics_process(delta: float) -> void: _time += delta var power := throttle if drive_on else 0.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: Fx.set_spear(_drive_core, plume, 1.0, _time) Fx.set_spear(_drive_sheath, plume * 0.82, 1.0, _time) if _drive_light: _drive_light.light_energy = plume * 5.0 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) for cloud in _chaff: cloud.emitting = chaff_fire _step_pdc(delta) func set_cabin_dim(v: bool) -> void: cabin_dim = v if _cabin_ready: for i in _cabin_lights.size(): var lamp := _cabin_lights[i] var rest: Dictionary = _cabin_rest[i] lamp.light_energy = float(rest.energy) * (0.16 if cabin_dim else 1.0) lamp.light_color = rest.color func _collect_cabin() -> void: for n in find_children("*", "Light3D", true, false): var nm := String(n.name) if nm.begins_with("PointLight_") or nm.begins_with("CabinLamp") or nm == "CabinFill": _cabin_lights.append(n) _cabin_rest.append({"energy": n.light_energy, "color": n.light_color}) _cabin_ready = true func set_gear_deploy(v: float) -> void: gear_deploy = v _seek("gear_deploy", v) func set_canopy_open(v: float) -> void: canopy_open = v _seek("canopy_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 ap.play(clip) ap.seek(clampf(t, 0.0, 1.0) * ap.get_animation(clip).length, true) ap.pause() 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 _drive_core = Fx.spear(drive, 22.0, 0.68, 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.92, 3, 1.0, "DriveCore") _drive_sheath = Fx.spear(drive, 14.0, 1.7, Color(0.353, 0.659, 1.0, 0.9), 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.3, Color(0.773, 0.863, 1.0), 0.35, 7.5, 2.0) for n in find_children("*", "Node3D", true, false): var nm := String(n.name) if nm.begins_with("Vfx_RCS_"): _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 nm.begins_with("PDT_") and nm.ends_with("_Muzzle"): _muzzles.append(n) n.set_meta("acc", randf() * 0.02) elif nm == "Vfx_Chaff": _chaff.append(Fx.chaff(n)) 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() < 40: acc += 1.0 / 42.0 shots += 1 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)}) 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 mesh.position += (t.dir as Vector3) * float(t.speed) * delta 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