mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat(goals): add cinematic celebration sequence
This commit is contained in:
@@ -20,6 +20,13 @@ var _ship_spawn_transforms := {}
|
||||
var _hit_stop_generation := 0
|
||||
var _hit_stop_active := false
|
||||
var _time_scale_before_hit_stop := 1.0
|
||||
var _camera_rig: ShipCameraRig
|
||||
var _goal_slowmo_active := false
|
||||
var _time_scale_before_goal := 1.0
|
||||
var _goal_in_progress := false
|
||||
|
||||
const GOAL_CELEBRATION_SECONDS := 1.6
|
||||
const GOAL_SLOWMO_SCALE := 0.22
|
||||
|
||||
# Shared by modes that keep score (Match, Spectate); Free Play never
|
||||
# references this or emits a score_changed signal, and HUDController relies
|
||||
@@ -66,16 +73,58 @@ func _on_goal_scored(_conceding_team: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Virtual: immediate, non-presentational consequences at sensor time. Modes
|
||||
# with a score/clock override this so a last-second goal is authoritative
|
||||
# before the cinematic; reset/kickoff remains in _on_goal_scored afterwards.
|
||||
func _on_goal_registered(_conceding_team: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Debounce: a fast ball can re-trigger the goal area before the deferred
|
||||
# reset teleports it away, which would double-count the goal.
|
||||
var _goal_cooldown := false
|
||||
|
||||
func _handle_goal_scored(conceding_team: int) -> void:
|
||||
if _goal_cooldown:
|
||||
if _goal_in_progress:
|
||||
return
|
||||
_goal_cooldown = true
|
||||
get_tree().create_timer(0.5).timeout.connect(func(): _goal_cooldown = false)
|
||||
_on_goal_scored(conceding_team)
|
||||
_goal_in_progress = true
|
||||
_on_goal_registered(conceding_team)
|
||||
await _play_goal_celebration(1 - conceding_team, conceding_team)
|
||||
await _on_goal_scored(conceding_team)
|
||||
_goal_in_progress = false
|
||||
|
||||
|
||||
func _play_goal_celebration(scoring_team: int, conceding_team: int) -> void:
|
||||
# Training/headless modes never spawn a camera or HUD and must not pay a
|
||||
# real-time presentation delay between episodes.
|
||||
if DisplayServer.get_name() == "headless" or not is_instance_valid(_camera_rig):
|
||||
return
|
||||
_restore_hit_stop()
|
||||
_goal_slowmo_active = true
|
||||
_time_scale_before_goal = Engine.time_scale
|
||||
Engine.time_scale = minf(Engine.time_scale, GOAL_SLOWMO_SCALE)
|
||||
var goal_position := Vector3.ZERO
|
||||
for goal in arena.get_goals():
|
||||
if goal.team == conceding_team:
|
||||
goal_position = goal.global_position
|
||||
break
|
||||
_camera_rig.begin_goal_cut(goal_position)
|
||||
if hud:
|
||||
hud.show_goal_celebration(scoring_team)
|
||||
await get_tree().create_timer(GOAL_CELEBRATION_SECONDS, true, false, true).timeout
|
||||
if is_instance_valid(_camera_rig):
|
||||
_camera_rig.end_goal_cut()
|
||||
if hud and is_instance_valid(hud):
|
||||
hud.hide_goal_celebration()
|
||||
# Defensive unwind: impact feedback is suppressed while goal slow-mo owns
|
||||
# time scale, but restore any hit-stop that was already queued this frame.
|
||||
_restore_hit_stop()
|
||||
_restore_goal_slowmo()
|
||||
|
||||
|
||||
func _restore_goal_slowmo() -> void:
|
||||
if not _goal_slowmo_active:
|
||||
return
|
||||
Engine.time_scale = _time_scale_before_goal
|
||||
_goal_slowmo_active = false
|
||||
|
||||
|
||||
func spawn_ball() -> RigidBody3D:
|
||||
@@ -104,6 +153,7 @@ func spawn_ship(team: int, spawn_index: int = 0, controller: ShipController = nu
|
||||
func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||
var rig: ShipCameraRig = CAMERA_RIG_SCENE.instantiate()
|
||||
add_child(rig)
|
||||
_camera_rig = rig
|
||||
rig.target = target
|
||||
rig.impact_feedback.connect(_on_player_impact)
|
||||
# Also wires the scene's static HUD (if any) to the same ship, rather
|
||||
@@ -116,10 +166,13 @@ func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||
func _on_player_impact(intensity: float) -> void:
|
||||
if hud:
|
||||
hud.flash_impact(intensity)
|
||||
_run_hit_stop(intensity)
|
||||
if not _goal_slowmo_active:
|
||||
_run_hit_stop(intensity)
|
||||
|
||||
|
||||
func _run_hit_stop(intensity: float) -> void:
|
||||
if _goal_slowmo_active:
|
||||
return
|
||||
_hit_stop_generation += 1
|
||||
var generation := _hit_stop_generation
|
||||
if not _hit_stop_active:
|
||||
@@ -145,6 +198,7 @@ func _restore_hit_stop() -> void:
|
||||
func _exit_tree() -> void:
|
||||
# A scene change during the unscaled timer must never strand global time.
|
||||
_restore_hit_stop()
|
||||
_restore_goal_slowmo()
|
||||
|
||||
|
||||
# Given an already-resolved (path, reaction_ticks, action_noise) — callers
|
||||
|
||||
Reference in New Issue
Block a user