big
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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 _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 get_flame_count() -> int:
|
||||
return _flames.size()
|
||||
|
||||
|
||||
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
|
||||
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 _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])
|
||||
Reference in New Issue
Block a user