Files
Papay-Shooter/characters/player_jet_vfx.gd
T
2026-08-09 01:31:44 -04:00

182 lines
5.5 KiB
GDScript

extends Node3D
class_name PlayerJetVFX
## Four restrained anime-thrust accents: two shoulder nozzles and two foot
## nozzles. The geometry is deliberately graphic instead of particle-heavy so
## the character remains 90% silhouette/animation and only 10% visible tech.
const DOUBLE_JUMP_TIME := 0.40
const FLAME_LENGTH := 0.28
const SHOULDER_SOCKETS := [
Vector3(-0.27, 1.34, 0.16),
Vector3(0.27, 1.34, 0.16),
]
const FOOT_SOCKETS := [
Vector3(-0.16, 0.14, 0.08),
Vector3(0.16, 0.14, 0.08),
]
var _flames: Array[Node3D] = []
var _burst_left := 0.0
var _dash_active := false
var _wall_glide_active := false
var _dash_tail := 0.0
var _render_enabled := true
var _exhaust_direction := Vector3.DOWN
var _time := 0.0
var _visual_model: Node3D = null
func _ready() -> void:
for socket in SHOULDER_SOCKETS + FOOT_SOCKETS:
_flames.append(_make_flame(socket))
visible = false
func _make_flame(socket: Vector3) -> Node3D:
var flame := Node3D.new()
flame.name = "JetFlame%02d" % _flames.size()
flame.position = socket
flame.set_meta("jet_flame", true)
add_child(flame)
var outer := MeshInstance3D.new()
outer.name = "OuterFlame"
outer.mesh = _cone_mesh(0.045, FLAME_LENGTH)
outer.material_override = _flame_material(
Color(1.0, 0.24, 0.05, 0.72), Color(1.0, 0.12, 0.02), 3.2)
outer.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
outer.position.y = FLAME_LENGTH * 0.5
flame.add_child(outer)
var inner := MeshInstance3D.new()
inner.name = "HotCore"
inner.mesh = _cone_mesh(0.016, FLAME_LENGTH * 0.72)
inner.material_override = _flame_material(
Color(0.82, 0.96, 1.0, 0.86), Color(0.36, 0.82, 1.0), 3.2)
inner.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
inner.position.y = FLAME_LENGTH * 0.36
flame.add_child(inner)
return flame
func _cone_mesh(radius: float, height: float) -> CylinderMesh:
var mesh := CylinderMesh.new()
mesh.bottom_radius = radius
mesh.top_radius = 0.002
mesh.height = height
mesh.radial_segments = 8
mesh.rings = 1
return mesh
func _flame_material(albedo: Color, emission: Color, energy: float) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
mat.albedo_color = albedo
mat.emission_enabled = true
mat.emission = emission
mat.emission_energy_multiplier = energy
return mat
func set_render_enabled(enabled: bool) -> void:
_render_enabled = enabled
if not enabled:
visible = false
func set_visual_model(model: Node3D) -> void:
_visual_model = model
func burst_double_jump() -> void:
_burst_left = DOUBLE_JUMP_TIME
func set_dash(active: bool, movement_local: Vector2) -> void:
if _dash_active and not active:
_dash_tail = 0.14
_dash_active = active
if not active:
return
var move := Vector3(movement_local.x, 0.0, -movement_local.y)
if move.length_squared() < 0.01:
move = Vector3.FORWARD
# Exhaust opposes travel and dips slightly, which makes the force direction
# readable without turning the flame into a large sci-fi effect.
_exhaust_direction = (-move.normalized() + Vector3.DOWN * 0.20).normalized()
func set_wall_glide(active: bool, movement_local: Vector2) -> void:
_wall_glide_active = active
if not active:
return
var move := Vector3(movement_local.x, 0.0, -movement_local.y)
if move.length_squared() < 0.01:
move = Vector3.FORWARD
# Continuous thrust opposes travel with only a slight downward cant. This
# reads as vector jets carrying the pilot along the wall, not a jump burst.
_exhaust_direction = (-move.normalized() + Vector3.DOWN * 0.08).normalized()
func get_flame_count() -> int:
return _flames.size()
func get_flame_world_positions() -> Array[Vector3]:
var result: Array[Vector3] = []
for flame in _flames:
result.append(flame.global_position)
return result
func get_exhaust_direction() -> Vector3:
return _exhaust_direction
func _process(delta: float) -> void:
_time += delta
_update_sockets()
_burst_left = maxf(0.0, _burst_left - delta)
_dash_tail = maxf(0.0, _dash_tail - delta)
var dash_visible := _dash_active or _dash_tail > 0.0
var active := dash_visible or _burst_left > 0.0 or _wall_glide_active
visible = _render_enabled and active
if not visible:
return
if _burst_left > 0.0 and not dash_visible:
# The double-jump jets push mostly down, with just enough rear cant to
# keep the four flames separated in the silhouette.
_exhaust_direction = Vector3(0.0, -1.0, 0.16).normalized()
var pulse := 0.90 + sin(_time * 54.0) * 0.08 + sin(_time * 31.0) * 0.04
if _wall_glide_active:
pulse *= 1.32
if _burst_left > 0.0 and not _dash_active:
pulse *= clampf(_burst_left / 0.08, 0.15, 1.0)
for i in _flames.size():
var flame := _flames[i]
# A tiny outward fan keeps all four graphic wedges legible from the
# chase camera, even when the centerline points almost straight at it.
var side: float = [0.13, -0.13, 0.06, -0.06][i]
var nozzle_dir := (_exhaust_direction + Vector3(side, 0.0, 0.0)).normalized()
flame.quaternion = Quaternion(Vector3.UP, nozzle_dir)
var stagger := 1.0 + sin(_time * 43.0 + float(i) * 1.7) * 0.05
flame.scale = Vector3(1.0, pulse * stagger, 1.0)
func _update_sockets() -> void:
if not is_instance_valid(_visual_model) \
or not _visual_model.has_method("get_jet_socket_world_positions"):
return
var sockets: Array = _visual_model.get_jet_socket_world_positions()
if sockets.size() != _flames.size():
return
for i in _flames.size():
_flames[i].position = to_local(sockets[i])