mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(vfx): animate movement and scoring effects
This commit is contained in:
@@ -97,6 +97,7 @@ signal altitude_changed(altitude: float)
|
||||
signal thrust_changed(thrust_percent: float)
|
||||
signal angular_velocity_changed(angular_speed: float)
|
||||
signal heading_changed(heading_degrees: float)
|
||||
signal ball_contact(intensity: float, world_position: Vector3)
|
||||
|
||||
# Performance optimization - track last emitted values to avoid unnecessary signals
|
||||
var _last_speed: float = -1.0
|
||||
@@ -115,6 +116,12 @@ const ANGULAR_THRESHOLD = 0.01 # rad/s
|
||||
const ATTITUDE_THRESHOLD = 1.0 # degrees
|
||||
const THRUST_THRESHOLD = 1.0 # percent
|
||||
|
||||
var _engine_cores: Array[MeshInstance3D] = []
|
||||
var _engine_plumes: Array[GPUParticles3D] = []
|
||||
var _turbo_flames: Array[GPUParticles3D] = []
|
||||
var _engine_lights: Array[OmniLight3D] = []
|
||||
var _impact_sparks: GPUParticles3D
|
||||
|
||||
|
||||
func _ready():
|
||||
# Add ship to group for instrument discovery
|
||||
@@ -148,6 +155,8 @@ func _ready():
|
||||
set_physics_process(false)
|
||||
else:
|
||||
_build_merged_hull()
|
||||
_build_movement_vfx()
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _apply_team_color() -> void:
|
||||
@@ -197,10 +206,139 @@ func set_controller(new_controller: ShipController) -> void:
|
||||
|
||||
|
||||
func _physics_process(_delta):
|
||||
_update_movement_vfx()
|
||||
if _has_telemetry_listeners():
|
||||
_emit_telemetry_data()
|
||||
|
||||
|
||||
func _build_movement_vfx() -> void:
|
||||
for x in [-0.42, 0.42]:
|
||||
var engine_pos := Vector3(x, -0.05, 1.42)
|
||||
|
||||
var core_mat := _vfx_material(Color(0.35, 0.85, 1.0, 1.0), 1.0)
|
||||
var core_mesh := SphereMesh.new()
|
||||
core_mesh.radius = 0.11
|
||||
core_mesh.height = 0.22
|
||||
core_mesh.material = core_mat
|
||||
var core := MeshInstance3D.new()
|
||||
core.name = "EngineCoreL" if x < 0.0 else "EngineCoreR"
|
||||
core.position = engine_pos
|
||||
core.mesh = core_mesh
|
||||
core.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(core)
|
||||
_engine_cores.append(core)
|
||||
|
||||
var plume := _make_particles(
|
||||
"EnginePlumeL" if x < 0.0 else "EnginePlumeR",
|
||||
Color(0.3, 0.82, 1.0, 0.72), Vector2(0.16, 0.42), 36, 0.24,
|
||||
Vector3(0, 0, 1), 12.0, 2.0, 5.0
|
||||
)
|
||||
plume.position = engine_pos + Vector3(0, 0, 0.08)
|
||||
add_child(plume)
|
||||
_engine_plumes.append(plume)
|
||||
|
||||
var turbo := _make_particles(
|
||||
"TurboFlameL" if x < 0.0 else "TurboFlameR",
|
||||
Color(0.78, 0.34, 1.0, 0.9), Vector2(0.22, 0.72), 48, 0.18,
|
||||
Vector3(0, 0, 1), 7.0, 6.0, 10.0
|
||||
)
|
||||
turbo.position = engine_pos + Vector3(0, 0, 0.12)
|
||||
add_child(turbo)
|
||||
_turbo_flames.append(turbo)
|
||||
|
||||
var light := OmniLight3D.new()
|
||||
light.name = "EngineLightL" if x < 0.0 else "EngineLightR"
|
||||
light.position = engine_pos
|
||||
light.light_color = Color(0.3, 0.75, 1.0)
|
||||
light.omni_range = 3.5
|
||||
light.omni_attenuation = 2.0
|
||||
light.shadow_enabled = false
|
||||
add_child(light)
|
||||
_engine_lights.append(light)
|
||||
|
||||
_impact_sparks = _make_particles(
|
||||
"ImpactSparks", Color(1.0, 0.72, 0.22, 1.0), Vector2(0.09, 0.3),
|
||||
28, 0.32, Vector3.UP, 180.0, 5.0, 12.0
|
||||
)
|
||||
_impact_sparks.one_shot = true
|
||||
_impact_sparks.explosiveness = 1.0
|
||||
_impact_sparks.local_coords = false
|
||||
_impact_sparks.emitting = false
|
||||
add_child(_impact_sparks)
|
||||
|
||||
|
||||
func _vfx_material(color: Color, energy: float) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.albedo_color = color
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(color.r, color.g, color.b)
|
||||
mat.emission_energy_multiplier = energy
|
||||
return mat
|
||||
|
||||
|
||||
func _make_particles(
|
||||
particle_name: String, color: Color, size: Vector2, amount: int,
|
||||
lifetime: float, direction: Vector3, spread: float,
|
||||
velocity_min: float, velocity_max: float
|
||||
) -> GPUParticles3D:
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = size
|
||||
quad.material = _vfx_material(color, 2.2)
|
||||
var process := ParticleProcessMaterial.new()
|
||||
process.direction = direction
|
||||
process.spread = spread
|
||||
process.initial_velocity_min = velocity_min
|
||||
process.initial_velocity_max = velocity_max
|
||||
process.gravity = Vector3.ZERO
|
||||
process.scale_min = 0.35
|
||||
process.scale_max = 1.0
|
||||
var particles := GPUParticles3D.new()
|
||||
particles.name = particle_name
|
||||
particles.amount = amount
|
||||
particles.lifetime = lifetime
|
||||
particles.local_coords = true
|
||||
particles.process_material = process
|
||||
particles.draw_pass_1 = quad
|
||||
particles.visibility_aabb = AABB(Vector3(-3, -3, -1), Vector3(6, 6, 12))
|
||||
particles.emitting = false
|
||||
return particles
|
||||
|
||||
|
||||
func _update_movement_vfx() -> void:
|
||||
if _engine_plumes.is_empty():
|
||||
return
|
||||
# These are the two rear main engines, so lateral/vertical maneuvering jets
|
||||
# must not make them flare. Reverse thrust also comes from separate attitude
|
||||
# jets conceptually; only positive Z drives this rear-facing plume.
|
||||
var thrust := clampf(maxf(_current_action.thrust.z, 0.0), 0.0, 1.0)
|
||||
var turbo := _current_action.turbo and thrust > 0.05
|
||||
for i in _engine_plumes.size():
|
||||
_engine_plumes[i].emitting = thrust > 0.02
|
||||
_engine_plumes[i].amount_ratio = 0.25 + thrust * 0.75
|
||||
_engine_plumes[i].speed_scale = 0.65 + thrust * 0.75
|
||||
_turbo_flames[i].emitting = turbo
|
||||
_engine_lights[i].light_energy = 0.35 + thrust * 2.1 + (2.3 if turbo else 0.0)
|
||||
var core_mat := _engine_cores[i].mesh.material as StandardMaterial3D
|
||||
core_mat.emission_energy_multiplier = 0.65 + thrust * 2.0 + (2.0 if turbo else 0.0)
|
||||
_engine_cores[i].scale = Vector3.ONE * (0.8 + thrust * 0.3 + (0.25 if turbo else 0.0))
|
||||
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if not body is Ball:
|
||||
return
|
||||
var relative_speed := (linear_velocity - (body as Ball).linear_velocity).length()
|
||||
var intensity := clampf(inverse_lerp(3.0, 24.0, relative_speed), 0.12, 1.0)
|
||||
var contact_pos := (global_position + (body as Ball).global_position) * 0.5
|
||||
if _impact_sparks:
|
||||
_impact_sparks.position = to_local(contact_pos)
|
||||
_impact_sparks.amount_ratio = lerpf(0.25, 1.0, intensity)
|
||||
_impact_sparks.restart()
|
||||
ball_contact.emit(intensity, contact_pos)
|
||||
|
||||
|
||||
# Covers a second/AI ship with no HUD watching it - headless mode is already
|
||||
# handled by disabling _physics_process entirely in _ready.
|
||||
func _has_telemetry_listeners() -> bool:
|
||||
|
||||
Reference in New Issue
Block a user