Files
Brachistochrone/addons/Assets/enviroment assets/grok-alec/fleet/hongqi.gd
T
scorpion21211-cmd b844c1a053 changelog
arcology updated
ship models in
particle fx is broken on kestrel
2026-10-04 19:37:48 -04:00

313 lines
11 KiB
GDScript

extends Node3D
## Hongqi 红旗-07. Godot 4.7. Units: 1 m.
## Axes: nose +Y, starboard +X, dorsal +Z. Aft exhaust is -Y.
## Clips are 0..1 on the AnimationPlayer imported with the glb.
## Particles are spawned on the Vfx sockets at runtime. They are not in the mesh.
## Drive is a cinnabar torch: white-gold core, red sheath, plus a smaller plume on each outboard bell.
## 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 radiators_deploy := 1.0 : set = set_radiators_deploy
@export_range(0.0, 1.0) var hatch_boarding_port := 0.0 : set = set_hatch_boarding_port
@export_range(0.0, 1.0) var hatch_boarding_stbd := 0.0 : set = set_hatch_boarding_stbd
@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_deck_open := 0.0 : set = set_hatch_deck_open
@export_range(0.0, 1.0) var hatch_ramp_open := 0.0 : set = set_hatch_ramp_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
@export var battle_lights := false : set = set_battle_lights
var _heat := 0.0
var _spool := 0.0
var _time := 0.0
var _rad_mats: Array[StandardMaterial3D] = []
var _drive_socket: Node3D
var _drive_core: Node3D
var _drive_sheath: Node3D
var _drive_light: SpotLight3D
var _aux: Array[Dictionary] = []
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] = []
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()
_collect_cabin()
set_physics_process(true)
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
_heat = lerpf(_heat, hard * open, 1.0 - exp(-delta / (18.0 if hard * open > _heat else 22.0)))
var glow := 0.0 if _heat < 0.55 else pow((_heat - 0.55) / 0.45, 1.7)
for mat in _rad_mats:
mat.emission_enabled = glow > 0.02
mat.emission = Color(1.0, 0.35, 0.08)
mat.emission_energy_multiplier = glow * 1.15
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(_drive_socket, _drive_core)
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
for aux in _aux:
var socket := aux.socket as Node3D
var core := aux.core as Node3D
var sheath := aux.sheath as Node3D
var scale := _drive_reach_scale(socket, core)
Fx.set_spear(core, plume, scale, _time + 0.4)
Fx.set_spear(sheath, plume * 0.7, scale, _time + 0.4)
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_battle_lights(v: bool) -> void:
battle_lights = v
if _cabin_ready:
_apply_cabin()
func _collect_cabin() -> void:
for n in find_children("*", "Light3D", true, false):
var nm := String(n.name)
if nm.begins_with("Nav") or nm.begins_with("Vfx"):
continue
if not (nm.begins_with("PointLight_") or nm.begins_with("CabinLamp") or nm == "CabinFill"):
continue
_cabin_lights.append(n)
_cabin_rest.append({"energy": n.light_energy, "color": n.light_color})
for n in find_children("*", "MeshInstance3D", true, false):
var nm := String(n.name)
if nm.find("Lamp") < 0 and not nm.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
func _apply_cabin() -> void:
var red := Color(0.769, 0.204, 0.173)
for i in _cabin_lights.size():
var lamp := _cabin_lights[i]
var rest: Dictionary = _cabin_rest[i]
if battle_lights:
lamp.light_color = red
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 = red
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_boarding_port(v: float) -> void:
hatch_boarding_port = v
_seek("hatch_boarding_port", v)
func set_hatch_boarding_stbd(v: float) -> void:
hatch_boarding_stbd = v
_seek("hatch_boarding_stbd", v)
func set_hatch_airlock_open(v: float) -> void:
hatch_airlock_open = v
_seek("hatch_airlock_open", v)
func set_hatch_deck_open(v: float) -> void:
hatch_deck_open = v
_seek("hatch_deck_open", v)
func set_hatch_ramp_open(v: float) -> void:
hatch_ramp_open = v
_seek("hatch_ramp_open", v)
func set_torpedo_doors_open(v: float) -> void:
torpedo_doors_open = v
_seek("torpedo_doors_open", v)
func _player() -> AnimationPlayer:
var ap := get_node_or_null("AnimationPlayer") as AnimationPlayer
if ap:
return ap
return find_child("AnimationPlayer", true, false) as AnimationPlayer
func _seek(clip: StringName, t: float) -> void:
var ap := _player()
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 nm := String(n.name)
if not nm.begins_with("Radiator_"):
continue
if nm.find("Fin_") < 0 and nm.find("Edge_") < 0:
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)
_rad_mats.append(copy)
func _arm_fx() -> void:
_tracer_mesh = Fx.tracer_mesh()
_tracer_mat = Fx.tracer_mat()
var gold := Color(1.0, 0.965, 0.831, 0.95)
var gold_mid := Color(1.0, 0.72, 0.35, 0.4)
var gold_tip := Color(0.9, 0.4, 0.1, 0.0)
var red := Color(0.886, 0.231, 0.184, 0.9)
var red_mid := Color(0.55, 0.08, 0.05, 0.35)
var red_tip := Color(0.25, 0.02, 0.01, 0.0)
var drive := find_child("Vfx_DrivePlume", true, false) as Node3D
if drive:
_drive_socket = drive
_drive_core = Fx.spear(drive, 30.0, 1.15, gold, gold_mid, gold_tip, 0.92, 3, 1.0, "DriveCore")
_drive_sheath = Fx.spear(drive, 22.0, 2.7, red, red_mid, red_tip, 0.5, 3, 2.0, "DriveSheath")
_drive_light = Fx.spot(drive, 0.35, Color(1.0, 0.882, 0.69), 0.55, 7.5, 2.0)
for n in find_children("*", "Node3D", true, false):
var nm := String(n.name)
if nm == "Vfx_Drive_Port" or nm == "Vfx_Drive_Stbd":
_aux.append({
"socket": n,
"core": Fx.spear(n, 12.0, 0.55, gold, gold_mid, gold_tip, 0.9, 3, 3.0, "DriveCore"),
"sheath": Fx.spear(n, 8.0, 1.2, red, red_mid, red_tip, 0.45, 3, 4.0, "DriveSheath"),
})
elif _is_rcs_socket(n):
_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 _is_rcs_socket(n: Node) -> bool:
var nm := String(n.name)
if nm.begins_with("Vfx_Drive") or nm == "Vfx_Chaff" or nm.begins_with("Vfx_Abort"):
return false
return nm.ends_with("_Vfx") or nm.begins_with("Vfx_RCS_")
func _drive_reach_scale(socket: Node3D, core: Node3D) -> float:
if socket == null or core == null:
return 1.0
var length := float(core.get_meta("len"))
var exhaust: Vector3 = (socket.global_transform.basis * 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.
# At the origin the bells sit below that plane, so the torch stays full length.
var room := (socket.global_position.y - (-2.45 + 0.22)) / -exhaust.y
if room <= 0.0:
return 1.0
return maxf(0.9, minf(length, room)) / length
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
mesh.position += (t.dir as Vector3) * 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)})
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