extends Node const TRAIL_GHOSTS := 8 const TRAIL_SPACING := 3.0 const TRAIL_ALPHAS := [0.5, 0.42, 0.34, 0.27, 0.2, 0.14, 0.09, 0.05] const GLOW_PADDING := Vector2(6, 6) const GLOW_COLOR := Color(1, 0.3, 0.3, 0.35) var pulse_speed := 80.0 var pulse_speed_initial := 80.0 var pulse_speed_per_floor := 8.0 var pulse_speed_increase := 15.0 var pulse_x := 0.0 var pulse_active := false var pulse_gap := 1.0 var pulse_gap_decrease := 0.05 var pulse_gap_min := 0.3 var pulses_blocked := 0 var _sweep_line: ColorRect var _target_zone: ColorRect var _perfect_zone: ColorRect var _screen_width: float var _glow: ColorRect var _trail: Array[ColorRect] = [] signal pulse_started(duration: float) signal pulse_blocked signal pulse_blocked_perfect signal pulse_escaped func configure(sweep_line: ColorRect, target_zone: ColorRect, perfect_zone: ColorRect, screen_width: float): _sweep_line = sweep_line _target_zone = target_zone _perfect_zone = perfect_zone _screen_width = screen_width _build_visuals() func _build_visuals(): var host: Control = _sweep_line.get_parent() as Control _glow = ColorRect.new() _glow.color = GLOW_COLOR _glow.size = Vector2(_sweep_line.size.x + GLOW_PADDING.x * 2, _sweep_line.size.y + GLOW_PADDING.y * 2) _glow.visible = false host.add_child(_glow) host.move_child(_glow, 0) for i in TRAIL_GHOSTS: var ghost := ColorRect.new() ghost.color = Color(_sweep_line.color.r, _sweep_line.color.g, _sweep_line.color.b, TRAIL_ALPHAS[i]) ghost.size = _sweep_line.size ghost.visible = false host.add_child(ghost) host.move_child(ghost, 0) _trail.append(ghost) func start_floor(floor_num: int): pulses_blocked = 0 var floors_descended = EventBus.STARTING_FLOOR - floor_num pulse_speed = pulse_speed_initial + floors_descended * pulse_speed_per_floor pulse_gap = 1.0 pulse_active = false func launch(): pulse_x = 0.0 pulse_active = true _sweep_line.visible = true _sweep_line.position.x = 0 _glow.visible = true for ghost in _trail: ghost.visible = true pulse_started.emit(_screen_width / pulse_speed) func attempt_block() -> bool: if not pulse_active: return false var bz_left = _target_zone.position.x var bz_right = _target_zone.position.x + _target_zone.size.x if pulse_x >= bz_left and pulse_x <= bz_right: var perfect_left = bz_left + _perfect_zone.position.x var perfect_right = perfect_left + _perfect_zone.size.x var was_perfect = pulse_x >= perfect_left and pulse_x <= perfect_right pulse_active = false pulses_blocked += 1 pulse_speed += pulse_speed_increase pulse_gap = max(pulse_gap - pulse_gap_decrease, pulse_gap_min) _hide_visuals() pulse_blocked.emit() if was_perfect: pulse_blocked_perfect.emit() return true else: pulse_active = false _hide_visuals() pulse_escaped.emit() return false func get_pulse_gap() -> float: return pulse_gap func stop(): pulse_active = false _hide_visuals() func _process(delta): if not pulse_active: return pulse_x += pulse_speed * delta _sweep_line.position.x = pulse_x _glow.position = Vector2(pulse_x - GLOW_PADDING.x, _sweep_line.position.y - GLOW_PADDING.y) for i in _trail.size(): var gx = pulse_x - (i + 1) * TRAIL_SPACING _trail[i].visible = gx >= 0 _trail[i].position = Vector2(gx, _sweep_line.position.y) if pulse_x >= _screen_width: pulse_active = false _hide_visuals() pulse_escaped.emit() func _hide_visuals(): if _sweep_line: _sweep_line.visible = false if _glow: _glow.visible = false for ghost in _trail: ghost.visible = false